diff --git a/capsules/_shared/elastos-theme.js b/capsules/_shared/elastos-theme.js new file mode 100644 index 000000000..4b7127693 --- /dev/null +++ b/capsules/_shared/elastos-theme.js @@ -0,0 +1,121 @@ +/* ElastOS UI — shared theme runtime (single source of truth). + * + * Vendored into each participating capsule's browser/ dir by `just vendor-ui`, + * next to elastos-ui.css. Everything is served same-origin from the gateway, + * so localStorage is the one canonical store (mirrors PC2's approach): + * + * localStorage["elastos.ui.theme"] = "dark" | "light" | "auto" + * localStorage["elastos.ui.accent"] = "blue" | "purple" | "pink" | "red" | + * "orange" | "yellow" | "green" | "graphite" + * + * Dark is the default theme when unset; blue is the default accent. "auto" + * follows prefers-color-scheme. The resolved theme is applied as + * data-el-theme="light" on (absence means dark); the accent as + * data-el-accent (absence means blue) — both activate token blocks in + * elastos-ui.css. Cross-document sync is the browser's own `storage` event — + * no shell messaging required, and sandboxed app frames (allow-same-origin) + * see it. + */ +(function () { + const KEY = "elastos.ui.theme"; + const ACCENT_KEY = "elastos.ui.accent"; + const ACCENTS = [ + "blue", + "purple", + "pink", + "red", + "orange", + "yellow", + "green", + "graphite", + ]; + const media = window.matchMedia + ? window.matchMedia("(prefers-color-scheme: light)") + : null; + + function preference() { + try { + const value = localStorage.getItem(KEY); + if (value === "light" || value === "dark" || value === "auto") { + return value; + } + } catch (_error) { + // Storage may be unavailable; fall through to the default. + } + return "dark"; + } + + function resolve(pref) { + if (pref === "auto") { + return media && media.matches ? "light" : "dark"; + } + return pref; + } + + function accentPreference() { + try { + const value = localStorage.getItem(ACCENT_KEY); + if (ACCENTS.includes(value)) { + return value; + } + } catch (_error) { + // Storage may be unavailable; fall through to the default. + } + return "blue"; + } + + function apply() { + if (resolve(preference()) === "light") { + document.documentElement.setAttribute("data-el-theme", "light"); + } else { + document.documentElement.removeAttribute("data-el-theme"); + } + const accent = accentPreference(); + if (accent === "blue") { + document.documentElement.removeAttribute("data-el-accent"); + } else { + document.documentElement.setAttribute("data-el-accent", accent); + } + } + + apply(); + + window.addEventListener("storage", (event) => { + if (event.key === KEY || event.key === ACCENT_KEY || event.key === null) { + apply(); + } + }); + + if (media && typeof media.addEventListener === "function") { + media.addEventListener("change", () => { + if (preference() === "auto") { + apply(); + } + }); + } + + window.elastosTheme = { + preference, + resolved: () => resolve(preference()), + set(value) { + const pref = value === "light" || value === "auto" ? value : "dark"; + try { + localStorage.setItem(KEY, pref); + } catch (_error) { + // Applying still works for this document. + } + apply(); + }, + accents: ACCENTS.slice(), + accent: accentPreference, + setAccent(value) { + const accent = ACCENTS.includes(value) ? value : "blue"; + try { + localStorage.setItem(ACCENT_KEY, accent); + } catch (_error) { + // Applying still works for this document. + } + apply(); + }, + }; +})(); diff --git a/capsules/_shared/elastos-ui.css b/capsules/_shared/elastos-ui.css new file mode 100644 index 000000000..b1a3075db --- /dev/null +++ b/capsules/_shared/elastos-ui.css @@ -0,0 +1,247 @@ +/* ElastOS UI — shared design tokens (single source of truth). + * + * This file lives at capsules/_shared/elastos-ui.css and is VENDORED into each + * participating capsule's browser/ dir by `just vendor-ui` (no gateway route, + * no shared runtime surface — capsules stay self-contained). Edit THIS file, + * then run `just vendor-ui`; `just alignment-check` fails on drift. + * + * Contents: Inter (OFL, latin variable subset), palette, type scale, radii, + * hairlines, blur/elevation steps, motion tokens, and minimal control + * primitives. Layout is each app's business — tokens only. + */ + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400 700; + font-display: optional; + src: url("./assets/fonts/Inter-latin-var.woff2") format("woff2"); +} + +:root { + color-scheme: dark; + + /* Palette */ + --el-bg: #0b0d10; + --el-surface: rgba(22, 24, 29, 0.92); + --el-surface-raised: #16181d; + --el-inset: rgba(255, 255, 255, 0.06); + --el-text: #f5f7fa; + --el-muted: rgba(245, 247, 250, 0.7); + --el-soft: rgba(245, 247, 250, 0.48); + --el-brand: #f6921a; + --el-brand-strong: #ffb457; + --el-focus: rgba(246, 146, 26, 0.55); + --el-danger: #ff8f8f; + --el-ok: #34d399; + + /* Hairlines (one pair, everywhere) */ + --el-hairline: rgba(255, 255, 255, 0.12); + --el-hairline-strong: rgba(255, 255, 255, 0.18); + + /* Type: 10/11/12/13/14/16/28; weights 400/500/600/700 */ + --el-font: "Inter", -apple-system, "Segoe UI", "SF Pro Text", sans-serif; + --el-font-mono: ui-monospace, "SF Mono", Menlo, monospace; + --el-text-2xs: 10px; + --el-text-xs: 11px; + --el-text-sm: 12px; + --el-text-md: 13px; + --el-text-lg: 14px; + --el-text-xl: 16px; + --el-text-display: 28px; + + /* Radii: 4/6/10/12/16/20 + pill */ + --el-radius-xs: 4px; + --el-radius-sm: 6px; + --el-radius-md: 10px; + --el-radius-lg: 12px; + --el-radius-xl: 16px; + --el-radius-2xl: 20px; + --el-radius-pill: 999px; + + /* Blur: three steps only */ + --el-blur-1: 8px; + --el-blur-2: 16px; + --el-blur-3: 24px; + + /* Elevation: three levels */ + --el-shadow-1: 0 2px 10px rgba(0, 0, 0, 0.22); + --el-shadow-2: 0 12px 28px rgba(0, 0, 0, 0.28); + --el-shadow-3: 0 24px 80px rgba(0, 0, 0, 0.35); + + /* Motion: 80/140/180/220, exits 150; one ease */ + --el-dur-1: 80ms; + --el-dur-2: 140ms; + --el-dur-3: 180ms; + --el-dur-4: 220ms; + --el-dur-exit: 150ms; + --el-ease: cubic-bezier(0.2, 0, 0, 1); + + /* System accent — ONE accent, chosen by the user in System > Personalization + (macOS model), obeyed by every app. Selected via data-el-accent on + (absence = blue). Apps consume the derived tones and never define their own + accent colors. */ + --el-accent: #0a84ff; + --el-accent-ink: #ffffff; + --el-accent-strong: color-mix(in srgb, var(--el-accent) 78%, #ffffff); + --el-accent-soft: color-mix(in srgb, var(--el-accent) 22%, transparent); + --el-accent-faint: color-mix(in srgb, var(--el-accent) 11%, transparent); +} + +/* The eight macOS accents. Yellow and orange carry dark ink on filled controls. */ +:root[data-el-accent="purple"] { + --el-accent: #bf5af2; +} + +:root[data-el-accent="pink"] { + --el-accent: #ff375f; +} + +:root[data-el-accent="red"] { + --el-accent: #ff453a; +} + +:root[data-el-accent="orange"] { + --el-accent: #f6921a; + --el-accent-ink: #161616; +} + +:root[data-el-accent="yellow"] { + --el-accent: #ffd60a; + --el-accent-ink: #161616; +} + +:root[data-el-accent="green"] { + --el-accent: #30b84f; +} + +:root[data-el-accent="graphite"] { + --el-accent: #8e8e93; +} + +/* Light theme: dark is the default (no attribute); the shell sets + data-el-theme="light" on and broadcasts changes to app frames. + Values restore the pre-token light look: white raised panels over a cool + pale stage, navy ink, the same brand orange. */ +:root[data-el-theme="light"] { + color-scheme: light; + + --el-bg: #eef1f6; + --el-surface: rgba(255, 255, 255, 0.88); + --el-surface-raised: #ffffff; + --el-inset: rgba(29, 36, 56, 0.055); + --el-text: #1d2438; + --el-muted: rgba(29, 36, 56, 0.72); + --el-soft: rgba(29, 36, 56, 0.5); + --el-brand: #f6921a; + --el-brand-strong: #e07f0a; + --el-focus: rgba(246, 146, 26, 0.55); + --el-danger: #b14c5a; + --el-ok: #0a7a52; + + --el-hairline: rgba(29, 36, 56, 0.14); + --el-hairline-strong: rgba(29, 36, 56, 0.24); + + --el-shadow-1: 0 2px 10px rgba(34, 49, 88, 0.1); + --el-shadow-2: 0 12px 28px rgba(34, 49, 88, 0.12); + --el-shadow-3: 0 24px 80px rgba(34, 49, 88, 0.16); + + /* "Strong" means higher contrast against the CURRENT background: lighter on + dark, deeper on light. */ + --el-accent-strong: color-mix(in srgb, var(--el-accent) 72%, #1d2438); +} + +/* App base: opt in by putting .el-app on
. Dark shell surface, shared + type. Layout untouched. */ +.el-app { + margin: 0; + background: var(--el-bg); + color: var(--el-text); + font-family: var(--el-font); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +.el-app :focus-visible { + outline: 2px solid var(--el-focus); + outline-offset: 2px; +} + +/* Minimal control primitives — enough that a capsule needs no bespoke button + or input styling to look native. */ +.el-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + min-height: 32px; + padding: 0 14px; + border: 1px solid var(--el-hairline); + border-radius: var(--el-radius-md); + background: var(--el-inset); + color: var(--el-text); + font: 500 var(--el-text-md) / 1.2 var(--el-font); + cursor: pointer; + transition: background-color var(--el-dur-2) var(--el-ease); +} + +/* Hover lift over inset surfaces — flips with the theme so apps don't need + their own literal rgba(255,255,255,x) hovers. */ +:root { + --el-lift: rgba(255, 255, 255, 0.1); + --el-lift-strong: rgba(255, 255, 255, 0.16); +} + +:root[data-el-theme="light"] { + --el-lift: rgba(29, 36, 56, 0.08); + --el-lift-strong: rgba(29, 36, 56, 0.14); +} + +.el-button:hover { + background: var(--el-lift); +} + +.el-button:active { + transform: scale(0.96); +} + +.el-button:disabled { + cursor: default; + opacity: 0.45; +} + +.el-button-primary { + border-color: transparent; + background: var(--el-brand); + color: #161616; + font-weight: 600; +} + +.el-button-primary:hover { + background: var(--el-brand-strong); +} + +.el-input { + min-height: 32px; + padding: 0 10px; + border: 1px solid var(--el-hairline); + border-radius: var(--el-radius-md); + background: var(--el-inset); + color: var(--el-text); + font: 400 var(--el-text-md) / 1.2 var(--el-font); +} + +.el-input::placeholder { + color: var(--el-soft); +} + +@media (prefers-reduced-motion: reduce) { + .el-app *, + .el-app *::before, + .el-app *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/capsules/_shared/fonts/Inter-latin-var.woff2 b/capsules/_shared/fonts/Inter-latin-var.woff2 new file mode 100644 index 000000000..91dc3e852 Binary files /dev/null and b/capsules/_shared/fonts/Inter-latin-var.woff2 differ diff --git a/capsules/archive-manager/browser/assets/fonts/Inter-latin-var.woff2 b/capsules/archive-manager/browser/assets/fonts/Inter-latin-var.woff2 new file mode 100644 index 000000000..91dc3e852 Binary files /dev/null and b/capsules/archive-manager/browser/assets/fonts/Inter-latin-var.woff2 differ diff --git a/capsules/archive-manager/browser/elastos-theme.js b/capsules/archive-manager/browser/elastos-theme.js new file mode 100644 index 000000000..bad19b950 --- /dev/null +++ b/capsules/archive-manager/browser/elastos-theme.js @@ -0,0 +1,122 @@ +/* GENERATED from capsules/_shared/elastos-theme.js — do not edit. Run `just vendor-ui`. */ +/* ElastOS UI — shared theme runtime (single source of truth). + * + * Vendored into each participating capsule's browser/ dir by `just vendor-ui`, + * next to elastos-ui.css. Everything is served same-origin from the gateway, + * so localStorage is the one canonical store (mirrors PC2's approach): + * + * localStorage["elastos.ui.theme"] = "dark" | "light" | "auto" + * localStorage["elastos.ui.accent"] = "blue" | "purple" | "pink" | "red" | + * "orange" | "yellow" | "green" | "graphite" + * + * Dark is the default theme when unset; blue is the default accent. "auto" + * follows prefers-color-scheme. The resolved theme is applied as + * data-el-theme="light" on (absence means dark); the accent as + * data-el-accent (absence means blue) — both activate token blocks in + * elastos-ui.css. Cross-document sync is the browser's own `storage` event — + * no shell messaging required, and sandboxed app frames (allow-same-origin) + * see it. + */ +(function () { + const KEY = "elastos.ui.theme"; + const ACCENT_KEY = "elastos.ui.accent"; + const ACCENTS = [ + "blue", + "purple", + "pink", + "red", + "orange", + "yellow", + "green", + "graphite", + ]; + const media = window.matchMedia + ? window.matchMedia("(prefers-color-scheme: light)") + : null; + + function preference() { + try { + const value = localStorage.getItem(KEY); + if (value === "light" || value === "dark" || value === "auto") { + return value; + } + } catch (_error) { + // Storage may be unavailable; fall through to the default. + } + return "dark"; + } + + function resolve(pref) { + if (pref === "auto") { + return media && media.matches ? "light" : "dark"; + } + return pref; + } + + function accentPreference() { + try { + const value = localStorage.getItem(ACCENT_KEY); + if (ACCENTS.includes(value)) { + return value; + } + } catch (_error) { + // Storage may be unavailable; fall through to the default. + } + return "blue"; + } + + function apply() { + if (resolve(preference()) === "light") { + document.documentElement.setAttribute("data-el-theme", "light"); + } else { + document.documentElement.removeAttribute("data-el-theme"); + } + const accent = accentPreference(); + if (accent === "blue") { + document.documentElement.removeAttribute("data-el-accent"); + } else { + document.documentElement.setAttribute("data-el-accent", accent); + } + } + + apply(); + + window.addEventListener("storage", (event) => { + if (event.key === KEY || event.key === ACCENT_KEY || event.key === null) { + apply(); + } + }); + + if (media && typeof media.addEventListener === "function") { + media.addEventListener("change", () => { + if (preference() === "auto") { + apply(); + } + }); + } + + window.elastosTheme = { + preference, + resolved: () => resolve(preference()), + set(value) { + const pref = value === "light" || value === "auto" ? value : "dark"; + try { + localStorage.setItem(KEY, pref); + } catch (_error) { + // Applying still works for this document. + } + apply(); + }, + accents: ACCENTS.slice(), + accent: accentPreference, + setAccent(value) { + const accent = ACCENTS.includes(value) ? value : "blue"; + try { + localStorage.setItem(ACCENT_KEY, accent); + } catch (_error) { + // Applying still works for this document. + } + apply(); + }, + }; +})(); diff --git a/capsules/archive-manager/browser/elastos-ui.css b/capsules/archive-manager/browser/elastos-ui.css new file mode 100644 index 000000000..7529596ef --- /dev/null +++ b/capsules/archive-manager/browser/elastos-ui.css @@ -0,0 +1,248 @@ +/* GENERATED from capsules/_shared/elastos-ui.css — do not edit. Run `just vendor-ui`. */ +/* ElastOS UI — shared design tokens (single source of truth). + * + * This file lives at capsules/_shared/elastos-ui.css and is VENDORED into each + * participating capsule's browser/ dir by `just vendor-ui` (no gateway route, + * no shared runtime surface — capsules stay self-contained). Edit THIS file, + * then run `just vendor-ui`; `just alignment-check` fails on drift. + * + * Contents: Inter (OFL, latin variable subset), palette, type scale, radii, + * hairlines, blur/elevation steps, motion tokens, and minimal control + * primitives. Layout is each app's business — tokens only. + */ + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400 700; + font-display: optional; + src: url("./assets/fonts/Inter-latin-var.woff2") format("woff2"); +} + +:root { + color-scheme: dark; + + /* Palette */ + --el-bg: #0b0d10; + --el-surface: rgba(22, 24, 29, 0.92); + --el-surface-raised: #16181d; + --el-inset: rgba(255, 255, 255, 0.06); + --el-text: #f5f7fa; + --el-muted: rgba(245, 247, 250, 0.7); + --el-soft: rgba(245, 247, 250, 0.48); + --el-brand: #f6921a; + --el-brand-strong: #ffb457; + --el-focus: rgba(246, 146, 26, 0.55); + --el-danger: #ff8f8f; + --el-ok: #34d399; + + /* Hairlines (one pair, everywhere) */ + --el-hairline: rgba(255, 255, 255, 0.12); + --el-hairline-strong: rgba(255, 255, 255, 0.18); + + /* Type: 10/11/12/13/14/16/28; weights 400/500/600/700 */ + --el-font: "Inter", -apple-system, "Segoe UI", "SF Pro Text", sans-serif; + --el-font-mono: ui-monospace, "SF Mono", Menlo, monospace; + --el-text-2xs: 10px; + --el-text-xs: 11px; + --el-text-sm: 12px; + --el-text-md: 13px; + --el-text-lg: 14px; + --el-text-xl: 16px; + --el-text-display: 28px; + + /* Radii: 4/6/10/12/16/20 + pill */ + --el-radius-xs: 4px; + --el-radius-sm: 6px; + --el-radius-md: 10px; + --el-radius-lg: 12px; + --el-radius-xl: 16px; + --el-radius-2xl: 20px; + --el-radius-pill: 999px; + + /* Blur: three steps only */ + --el-blur-1: 8px; + --el-blur-2: 16px; + --el-blur-3: 24px; + + /* Elevation: three levels */ + --el-shadow-1: 0 2px 10px rgba(0, 0, 0, 0.22); + --el-shadow-2: 0 12px 28px rgba(0, 0, 0, 0.28); + --el-shadow-3: 0 24px 80px rgba(0, 0, 0, 0.35); + + /* Motion: 80/140/180/220, exits 150; one ease */ + --el-dur-1: 80ms; + --el-dur-2: 140ms; + --el-dur-3: 180ms; + --el-dur-4: 220ms; + --el-dur-exit: 150ms; + --el-ease: cubic-bezier(0.2, 0, 0, 1); + + /* System accent — ONE accent, chosen by the user in System > Personalization + (macOS model), obeyed by every app. Selected via data-el-accent on + (absence = blue). Apps consume the derived tones and never define their own + accent colors. */ + --el-accent: #0a84ff; + --el-accent-ink: #ffffff; + --el-accent-strong: color-mix(in srgb, var(--el-accent) 78%, #ffffff); + --el-accent-soft: color-mix(in srgb, var(--el-accent) 22%, transparent); + --el-accent-faint: color-mix(in srgb, var(--el-accent) 11%, transparent); +} + +/* The eight macOS accents. Yellow and orange carry dark ink on filled controls. */ +:root[data-el-accent="purple"] { + --el-accent: #bf5af2; +} + +:root[data-el-accent="pink"] { + --el-accent: #ff375f; +} + +:root[data-el-accent="red"] { + --el-accent: #ff453a; +} + +:root[data-el-accent="orange"] { + --el-accent: #f6921a; + --el-accent-ink: #161616; +} + +:root[data-el-accent="yellow"] { + --el-accent: #ffd60a; + --el-accent-ink: #161616; +} + +:root[data-el-accent="green"] { + --el-accent: #30b84f; +} + +:root[data-el-accent="graphite"] { + --el-accent: #8e8e93; +} + +/* Light theme: dark is the default (no attribute); the shell sets + data-el-theme="light" on and broadcasts changes to app frames. + Values restore the pre-token light look: white raised panels over a cool + pale stage, navy ink, the same brand orange. */ +:root[data-el-theme="light"] { + color-scheme: light; + + --el-bg: #eef1f6; + --el-surface: rgba(255, 255, 255, 0.88); + --el-surface-raised: #ffffff; + --el-inset: rgba(29, 36, 56, 0.055); + --el-text: #1d2438; + --el-muted: rgba(29, 36, 56, 0.72); + --el-soft: rgba(29, 36, 56, 0.5); + --el-brand: #f6921a; + --el-brand-strong: #e07f0a; + --el-focus: rgba(246, 146, 26, 0.55); + --el-danger: #b14c5a; + --el-ok: #0a7a52; + + --el-hairline: rgba(29, 36, 56, 0.14); + --el-hairline-strong: rgba(29, 36, 56, 0.24); + + --el-shadow-1: 0 2px 10px rgba(34, 49, 88, 0.1); + --el-shadow-2: 0 12px 28px rgba(34, 49, 88, 0.12); + --el-shadow-3: 0 24px 80px rgba(34, 49, 88, 0.16); + + /* "Strong" means higher contrast against the CURRENT background: lighter on + dark, deeper on light. */ + --el-accent-strong: color-mix(in srgb, var(--el-accent) 72%, #1d2438); +} + +/* App base: opt in by putting .el-app on . Dark shell surface, shared + type. Layout untouched. */ +.el-app { + margin: 0; + background: var(--el-bg); + color: var(--el-text); + font-family: var(--el-font); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +.el-app :focus-visible { + outline: 2px solid var(--el-focus); + outline-offset: 2px; +} + +/* Minimal control primitives — enough that a capsule needs no bespoke button + or input styling to look native. */ +.el-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + min-height: 32px; + padding: 0 14px; + border: 1px solid var(--el-hairline); + border-radius: var(--el-radius-md); + background: var(--el-inset); + color: var(--el-text); + font: 500 var(--el-text-md) / 1.2 var(--el-font); + cursor: pointer; + transition: background-color var(--el-dur-2) var(--el-ease); +} + +/* Hover lift over inset surfaces — flips with the theme so apps don't need + their own literal rgba(255,255,255,x) hovers. */ +:root { + --el-lift: rgba(255, 255, 255, 0.1); + --el-lift-strong: rgba(255, 255, 255, 0.16); +} + +:root[data-el-theme="light"] { + --el-lift: rgba(29, 36, 56, 0.08); + --el-lift-strong: rgba(29, 36, 56, 0.14); +} + +.el-button:hover { + background: var(--el-lift); +} + +.el-button:active { + transform: scale(0.96); +} + +.el-button:disabled { + cursor: default; + opacity: 0.45; +} + +.el-button-primary { + border-color: transparent; + background: var(--el-brand); + color: #161616; + font-weight: 600; +} + +.el-button-primary:hover { + background: var(--el-brand-strong); +} + +.el-input { + min-height: 32px; + padding: 0 10px; + border: 1px solid var(--el-hairline); + border-radius: var(--el-radius-md); + background: var(--el-inset); + color: var(--el-text); + font: 400 var(--el-text-md) / 1.2 var(--el-font); +} + +.el-input::placeholder { + color: var(--el-soft); +} + +@media (prefers-reduced-motion: reduce) { + .el-app *, + .el-app *::before, + .el-app *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/capsules/archive-manager/browser/index.html b/capsules/archive-manager/browser/index.html index c61e10431..b045b0355 100644 --- a/capsules/archive-manager/browser/index.html +++ b/capsules/archive-manager/browser/index.html @@ -4,25 +4,27 @@