Skip to content

Commit aec8418

Browse files
authored
Merge pull request #45 from singyichen/feat/appearance-toggle-mvp
feat(shared): add sidebar shortcut utility
2 parents f1badf6 + c6bea31 commit aec8418

31 files changed

Lines changed: 2908 additions & 532 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// FOUC prevention — resolve theme synchronously before paint.
2+
(function () {
3+
try {
4+
var choice = localStorage.getItem('label-suite-theme') || 'system';
5+
var resolved = (choice === 'dark') ? 'dark' : 'light';
6+
document.documentElement.setAttribute('data-theme', resolved);
7+
} catch (e) {
8+
document.documentElement.setAttribute('data-theme', 'light');
9+
}
10+
})();

design/prototype/assets/theme.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/* global window, document */
2+
(function bootstrapLabelSuiteTheme(windowObj, documentObj) {
3+
const STORAGE_KEY = 'label-suite-theme';
4+
const VALID_CHOICES = ['system', 'light', 'dark'];
5+
const DEFAULT_CHOICE = 'system';
6+
const listeners = new Set();
7+
8+
function getStoredChoice() {
9+
try {
10+
const value = windowObj.localStorage.getItem(STORAGE_KEY);
11+
return VALID_CHOICES.indexOf(value) >= 0 ? value : DEFAULT_CHOICE;
12+
} catch (_err) {
13+
return DEFAULT_CHOICE;
14+
}
15+
}
16+
17+
function resolveChoice(choice) {
18+
return choice === 'dark' ? 'dark' : 'light';
19+
}
20+
21+
function applyResolved(resolved) {
22+
documentObj.documentElement.setAttribute('data-theme', resolved);
23+
}
24+
25+
function setChoice(choice) {
26+
if (VALID_CHOICES.indexOf(choice) < 0) return;
27+
try {
28+
windowObj.localStorage.setItem(STORAGE_KEY, choice);
29+
} catch (_err) {
30+
// localStorage unavailable (private mode etc) — apply for this session only
31+
}
32+
const resolved = resolveChoice(choice);
33+
applyResolved(resolved);
34+
listeners.forEach((cb) => {
35+
try { cb({ choice: choice, resolved: resolved }); } catch (_err) { /* ignore */ }
36+
});
37+
}
38+
39+
function onChange(callback) {
40+
if (typeof callback === 'function') {
41+
listeners.add(callback);
42+
return function unsubscribe() { listeners.delete(callback); };
43+
}
44+
return function noop() {};
45+
}
46+
47+
// Apply on script load (the inline <head> snippet has already set
48+
// data-theme to prevent FOUC; this is a no-op if values already match).
49+
applyResolved(resolveChoice(getStoredChoice()));
50+
51+
windowObj.LabelSuiteTheme = {
52+
STORAGE_KEY: STORAGE_KEY,
53+
VALID_CHOICES: VALID_CHOICES.slice(),
54+
getStoredChoice: getStoredChoice,
55+
getResolved: function () { return resolveChoice(getStoredChoice()); },
56+
setChoice: setChoice,
57+
onChange: onChange,
58+
};
59+
})(window, document);

design/prototype/assets/tokens.css

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
--color-white: #FFFFFF;
1717
--color-ink: #1E1B4B; /* Indigo 950 — body text */
1818
--color-primary-soft-bg: #EEF2FF;
19+
--color-primary-border: #C7D2FE; /* Indigo 200 */
1920

2021
/* ── Supporting ───────────────────────────────────────────── */
2122
--color-border: #E2E8F0; /* slate-200 */
@@ -118,8 +119,81 @@
118119
--fg-3: var(--color-text-muted);
119120
--bg-page: var(--color-surface);
120121
--bg-surface: var(--color-white);
122+
123+
/* Nav-active background — overridden in dark mode so the active pill
124+
reads as "raised" (lighter than navbar) instead of fading into bg. */
125+
--nav-active-bg: var(--color-surface);
126+
}
127+
128+
/* ═══════════════════════════════════════════════════════════════
129+
Dark Theme — applied via <html data-theme="dark">
130+
Strategy: re-map the same token names so every page that already
131+
uses var(--color-*) flips automatically. No HTML changes needed.
132+
133+
Specificity note: existing prototype pages redefine these tokens
134+
inside inline <style>{ :root { ... } } blocks. `:root` has
135+
specificity (0,1,0); `html[data-theme="dark"]` has (0,1,1), so the
136+
dark overrides win regardless of source order.
137+
═══════════════════════════════════════════════════════════════ */
138+
html[data-theme="dark"] {
139+
/* ── Core palette ─────────────────────────────────────────── */
140+
--color-primary: #818CF8; /* indigo-400, brighter on dark */
141+
--color-secondary: #A5B4FC; /* indigo-300 */
142+
--color-cta: #34D399; /* emerald-400 */
143+
--color-cta-hover: #10B981;
144+
--color-surface: #0B0B12; /* page bg, near-black */
145+
--color-white: #16161F; /* card / sidebar bg, raised 1 step */
146+
--color-ink: #E2E8F0; /* slate-200, primary text */
147+
--color-primary-soft-bg: #1E1B4B; /* indigo-950 */
148+
--color-primary-border: #3730A3; /* Indigo 800 */
149+
150+
/* ── Supporting ───────────────────────────────────────────── */
151+
--color-border: #2A2A35;
152+
--color-border-muted: #1F1F28;
153+
--color-text-muted: #9CA3AF; /* slate-400 — raised from zinc-500 for WCAG AA */
154+
--color-text-soft: #A1A1AA; /* zinc-400 */
155+
--color-slate-50: #1F1F28; /* hover bg in dark */
156+
157+
/* Aliases */
158+
--color-ink-muted: #9CA3AF; /* = --color-text-muted */
159+
160+
/* ── Semantic state — desaturated for dark backgrounds ────── */
161+
--color-error: #F87171;
162+
--color-error-bg: #2A1414;
163+
--color-error-border: #5B2222;
164+
--color-error-soft-bg: #2A1414;
165+
--color-error-soft-border:#5B2222;
166+
167+
--color-success: #4ADE80;
168+
--color-success-bg: #0F2A18;
169+
--color-success-border: #1F5132;
170+
--color-success-soft-bg: #0F2A18;
171+
--color-success-soft-border: #1F5132;
172+
173+
--color-warning: #FACC15;
174+
--color-warning-bg: #2A2210;
175+
--color-warning-border: #5C4A1A;
176+
--color-warning-soft-bg: #2A2210;
177+
--color-warning-soft-border: #5C4A1A;
178+
179+
--color-info: #60A5FA;
180+
--color-info-bg: #0F1F33;
181+
--color-info-border: #1E3A66;
182+
183+
/* ── Shadows — much subtler on dark, mostly ring-style ────── */
184+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.40);
185+
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.45);
186+
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.50);
187+
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.55);
188+
--shadow-card: 0 0 0 1px rgba(129, 140, 248, 0.10), 0 4px 24px rgba(0, 0, 0, 0.40);
189+
190+
/* Nav-active pill: lighter than --color-white (#16161F) so it reads as raised */
191+
--nav-active-bg: #2A2A35;
121192
}
122193

194+
/* `data-theme` is always resolved to "light" or "dark" by theme.js.
195+
The "system" choice is resolved client-side using matchMedia. */
196+
123197
/* ── Semantic type rules ──────────────────────────────────── */
124198
html[lang="zh-TW"] body,
125199
html[lang="zh"] body { line-height: var(--leading-body-zh); }

design/prototype/pages/account/forgot-password.html

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,33 @@
314314
@media (prefers-reduced-motion: reduce) {
315315
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
316316
}
317+
318+
/* ── Dark mode overrides ──────────────────────────────────── */
319+
html[data-theme="dark"] {
320+
--color-background: #0B0B12;
321+
--color-surface: #16161F;
322+
--color-border: #2A2A35;
323+
--color-border-focus: #818CF8;
324+
--color-text: #E2E8F0;
325+
--color-text-muted: #9CA3AF;
326+
--color-link: #818CF8;
327+
--color-primary: #818CF8;
328+
--color-primary-light: #1E1B4B;
329+
--color-error: #F87171;
330+
--color-error-bg: #2A1414;
331+
--color-error-border: #5B2222;
332+
--color-success: #4ADE80;
333+
--color-success-bg: #0F2A18;
334+
--color-success-border:#1F5132;
335+
--shadow-card: 0 0 0 1px rgba(129,140,248,0.10), 0 4px 24px rgba(0,0,0,0.40);
336+
337+
/* CTA button */
338+
.submit-btn { color: #064E3B; background: #34D399; }
339+
.submit-btn:hover { background: #10B981; }
340+
341+
/* Lang toggle hover */
342+
.lang-toggle:hover { background: #1E1B4B; }
343+
}
317344
</style>
318345
</head>
319346
<body>

design/prototype/pages/account/login.html

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,34 @@
362362
@media (prefers-reduced-motion: reduce) {
363363
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
364364
}
365+
366+
/* ── Dark mode overrides ──────────────────────────────────── */
367+
html[data-theme="dark"] {
368+
/* Re-map local token names — these pages do not import tokens.css */
369+
--color-background: #0B0B12;
370+
--color-surface: #16161F;
371+
--color-border: #2A2A35;
372+
--color-border-focus: #818CF8;
373+
--color-text: #E2E8F0;
374+
--color-text-muted: #9CA3AF;
375+
--color-link: #818CF8;
376+
--color-primary: #818CF8;
377+
--color-primary-light: #1E1B4B;
378+
--color-error: #F87171;
379+
--color-error-bg: #2A1414;
380+
--color-error-border: #5B2222;
381+
--shadow-card: 0 0 0 1px rgba(129,140,248,0.10), 0 4px 24px rgba(0,0,0,0.40);
382+
383+
/* CTA button — dark bg, dark text on bright emerald for contrast */
384+
.login-btn { color: #064E3B; background: #34D399; }
385+
.login-btn:hover { background: #10B981; }
386+
387+
/* SSO button hover */
388+
.sso-btn:hover { background: #1E1B4B; border-color: #818CF8; }
389+
390+
/* Lang toggle hover */
391+
.lang-toggle:hover { background: #1E1B4B; }
392+
}
365393
</style>
366394
</head>
367395
<body>

0 commit comments

Comments
 (0)