@godxjp/ui 13.14.0 → 13.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/app-provider.d.ts +1 -1
- package/dist/app/app-provider.js +103 -7
- package/dist/app/index.d.ts +1 -0
- package/dist/app/index.js +1 -0
- package/dist/app/storage.d.ts +6 -0
- package/dist/app/storage.js +11 -1
- package/dist/app/theme-axes.d.ts +30 -0
- package/dist/app/theme-axes.js +38 -0
- package/dist/components/layout/page-container.js +4 -2
- package/dist/components/navigation/app-setting-picker.d.ts +4 -3
- package/dist/components/navigation/app-setting-picker.js +56 -6
- package/dist/i18n/messages/en.json +26 -0
- package/dist/i18n/messages/ja.json +26 -0
- package/dist/i18n/messages/vi.json +26 -0
- package/dist/props/components/app.prop.d.ts +29 -1
- package/dist/styles/density.css +55 -41
- package/dist/tokens/axes.css +59 -0
- package/dist/tokens/base.css +1 -0
- package/dist/tokens/foundation.css +5 -2
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type AppDateFormat, type AppLocale, type AppTimeFormat, type AppTimezone } from "./types";
|
|
2
2
|
import type { AppContextValue, AppProviderProp } from "../props/components/app.prop";
|
|
3
3
|
export type { AppProviderProp, AppContextValue } from "../props/components/app.prop";
|
|
4
|
-
export declare function AppProvider({ children, defaultLocale, fallbackLocale, defaultTimezone, systemTimezone, defaultTimeFormat, defaultDateFormat, timezoneOptions, storageKey, persist, onLocaleChange, onTimezoneChange, onTimeFormatChange, onDateFormatChange, }: AppProviderProp): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
export declare function AppProvider({ children, defaultLocale, fallbackLocale, defaultTimezone, systemTimezone, defaultTimeFormat, defaultDateFormat, timezoneOptions, storageKey, persist, theme: initialTheme, brand: initialBrand, density: initialDensity, fontSize: initialFontSize, onLocaleChange, onTimezoneChange, onTimeFormatChange, onDateFormatChange, onThemeChange, onBrandChange, onDensityChange, onFontSizeChange, }: AppProviderProp): import("react/jsx-runtime").JSX.Element;
|
|
5
5
|
export declare function useAppContext(): AppContextValue;
|
|
6
6
|
/** Returns null outside AppProvider — used by pickers for optional context. */
|
|
7
7
|
export declare function useOptionalAppContext(): AppContextValue | null;
|
package/dist/app/app-provider.js
CHANGED
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
syncDatetimeContext
|
|
15
15
|
} from "../lib/datetime";
|
|
16
16
|
import { DEFAULT_STORAGE_KEY, readStoredPreferences, writeStoredPreferences } from "./storage";
|
|
17
|
+
import {
|
|
18
|
+
applyThemeAxes
|
|
19
|
+
} from "./theme-axes";
|
|
17
20
|
import { resolveDefaultTimeFormat } from "./time-format-labels";
|
|
18
21
|
import { resolveDefaultTimezone } from "./timezones";
|
|
19
22
|
import {
|
|
@@ -56,10 +59,18 @@ function AppProvider({
|
|
|
56
59
|
timezoneOptions,
|
|
57
60
|
storageKey = DEFAULT_STORAGE_KEY,
|
|
58
61
|
persist = true,
|
|
62
|
+
theme: initialTheme = "light",
|
|
63
|
+
brand: initialBrand = null,
|
|
64
|
+
density: initialDensity = "default",
|
|
65
|
+
fontSize: initialFontSize = "default",
|
|
59
66
|
onLocaleChange,
|
|
60
67
|
onTimezoneChange,
|
|
61
68
|
onTimeFormatChange,
|
|
62
|
-
onDateFormatChange
|
|
69
|
+
onDateFormatChange,
|
|
70
|
+
onThemeChange,
|
|
71
|
+
onBrandChange,
|
|
72
|
+
onDensityChange,
|
|
73
|
+
onFontSizeChange
|
|
63
74
|
}) {
|
|
64
75
|
const initialLocale = defaultLocale;
|
|
65
76
|
const [locale, setLocaleState] = React.useState(initialLocale);
|
|
@@ -72,8 +83,21 @@ function AppProvider({
|
|
|
72
83
|
const [dateFormat, setDateFormatState] = React.useState(
|
|
73
84
|
() => resolveInitialDateFormat(void 0, defaultDateFormat, initialLocale)
|
|
74
85
|
);
|
|
86
|
+
const [theme, setThemeState] = React.useState(initialTheme);
|
|
87
|
+
const [brand, setBrandState] = React.useState(initialBrand);
|
|
88
|
+
const [density, setDensityState] = React.useState(initialDensity);
|
|
89
|
+
const [fontSize, setFontSizeState] = React.useState(initialFontSize);
|
|
75
90
|
const hasMountedRef = React.useRef(false);
|
|
76
|
-
const prefsRef = React.useRef({
|
|
91
|
+
const prefsRef = React.useRef({
|
|
92
|
+
locale,
|
|
93
|
+
timezone,
|
|
94
|
+
timeFormat,
|
|
95
|
+
dateFormat,
|
|
96
|
+
theme,
|
|
97
|
+
brand,
|
|
98
|
+
density,
|
|
99
|
+
fontSize
|
|
100
|
+
});
|
|
77
101
|
React.useEffect(() => {
|
|
78
102
|
const stored = persist ? readStoredPreferences(storageKey) : {};
|
|
79
103
|
const nextLocale = stored.locale ?? defaultLocale;
|
|
@@ -88,28 +112,48 @@ function AppProvider({
|
|
|
88
112
|
defaultDateFormat,
|
|
89
113
|
nextLocale
|
|
90
114
|
);
|
|
115
|
+
const nextTheme = stored.theme ?? initialTheme;
|
|
116
|
+
const nextBrand = stored.brand ?? initialBrand;
|
|
117
|
+
const nextDensity = stored.density ?? initialDensity;
|
|
118
|
+
const nextFontSize = stored.fontSize ?? initialFontSize;
|
|
91
119
|
prefsRef.current = {
|
|
92
120
|
locale: nextLocale,
|
|
93
121
|
timezone: nextTimezone,
|
|
94
122
|
timeFormat: nextTimeFormat,
|
|
95
|
-
dateFormat: nextDateFormat
|
|
123
|
+
dateFormat: nextDateFormat,
|
|
124
|
+
theme: nextTheme,
|
|
125
|
+
brand: nextBrand,
|
|
126
|
+
density: nextDensity,
|
|
127
|
+
fontSize: nextFontSize
|
|
96
128
|
};
|
|
97
129
|
setLocaleState(nextLocale);
|
|
98
130
|
setTimezoneState(nextTimezone);
|
|
99
131
|
setTimeFormatState(nextTimeFormat);
|
|
100
132
|
setDateFormatState(nextDateFormat);
|
|
133
|
+
setThemeState(nextTheme);
|
|
134
|
+
setBrandState(nextBrand);
|
|
135
|
+
setDensityState(nextDensity);
|
|
136
|
+
setFontSizeState(nextFontSize);
|
|
101
137
|
}, [
|
|
102
138
|
defaultDateFormat,
|
|
103
139
|
defaultLocale,
|
|
104
140
|
defaultTimeFormat,
|
|
105
141
|
defaultTimezone,
|
|
142
|
+
initialTheme,
|
|
143
|
+
initialBrand,
|
|
144
|
+
initialDensity,
|
|
145
|
+
initialFontSize,
|
|
106
146
|
persist,
|
|
107
147
|
storageKey,
|
|
108
148
|
systemTimezone
|
|
109
149
|
]);
|
|
110
150
|
React.useEffect(() => {
|
|
111
|
-
prefsRef.current = { locale, timezone, timeFormat, dateFormat };
|
|
112
|
-
}, [locale, timezone, timeFormat, dateFormat]);
|
|
151
|
+
prefsRef.current = { locale, timezone, timeFormat, dateFormat, theme, brand, density, fontSize };
|
|
152
|
+
}, [locale, timezone, timeFormat, dateFormat, theme, brand, density, fontSize]);
|
|
153
|
+
React.useEffect(() => {
|
|
154
|
+
if (typeof document === "undefined") return;
|
|
155
|
+
applyThemeAxes(document.documentElement, { theme, brand, density, fontSize });
|
|
156
|
+
}, [theme, brand, density, fontSize]);
|
|
113
157
|
const setLocale = React.useCallback(
|
|
114
158
|
(next) => {
|
|
115
159
|
prefsRef.current = { ...prefsRef.current, locale: next };
|
|
@@ -146,6 +190,42 @@ function AppProvider({
|
|
|
146
190
|
},
|
|
147
191
|
[onDateFormatChange, persist, storageKey]
|
|
148
192
|
);
|
|
193
|
+
const setTheme = React.useCallback(
|
|
194
|
+
(next) => {
|
|
195
|
+
prefsRef.current = { ...prefsRef.current, theme: next };
|
|
196
|
+
setThemeState(next);
|
|
197
|
+
onThemeChange?.(next);
|
|
198
|
+
if (persist) writeStoredPreferences(storageKey, prefsRef.current);
|
|
199
|
+
},
|
|
200
|
+
[onThemeChange, persist, storageKey]
|
|
201
|
+
);
|
|
202
|
+
const setBrand = React.useCallback(
|
|
203
|
+
(next) => {
|
|
204
|
+
prefsRef.current = { ...prefsRef.current, brand: next };
|
|
205
|
+
setBrandState(next);
|
|
206
|
+
onBrandChange?.(next);
|
|
207
|
+
if (persist) writeStoredPreferences(storageKey, prefsRef.current);
|
|
208
|
+
},
|
|
209
|
+
[onBrandChange, persist, storageKey]
|
|
210
|
+
);
|
|
211
|
+
const setDensity = React.useCallback(
|
|
212
|
+
(next) => {
|
|
213
|
+
prefsRef.current = { ...prefsRef.current, density: next };
|
|
214
|
+
setDensityState(next);
|
|
215
|
+
onDensityChange?.(next);
|
|
216
|
+
if (persist) writeStoredPreferences(storageKey, prefsRef.current);
|
|
217
|
+
},
|
|
218
|
+
[onDensityChange, persist, storageKey]
|
|
219
|
+
);
|
|
220
|
+
const setFontSize = React.useCallback(
|
|
221
|
+
(next) => {
|
|
222
|
+
prefsRef.current = { ...prefsRef.current, fontSize: next };
|
|
223
|
+
setFontSizeState(next);
|
|
224
|
+
onFontSizeChange?.(next);
|
|
225
|
+
if (persist) writeStoredPreferences(storageKey, prefsRef.current);
|
|
226
|
+
},
|
|
227
|
+
[onFontSizeChange, persist, storageKey]
|
|
228
|
+
);
|
|
149
229
|
const requestHeaders = React.useMemo(
|
|
150
230
|
() => buildRequestHeaders(locale, timezone, timeFormat, dateFormat),
|
|
151
231
|
[locale, timezone, timeFormat, dateFormat]
|
|
@@ -185,10 +265,18 @@ function AppProvider({
|
|
|
185
265
|
dayPickerLocale: getDayPickerLocale(locale),
|
|
186
266
|
requestHeaders,
|
|
187
267
|
timezoneOptions,
|
|
268
|
+
theme,
|
|
269
|
+
brand,
|
|
270
|
+
density,
|
|
271
|
+
fontSize,
|
|
188
272
|
setLocale,
|
|
189
273
|
setTimezone,
|
|
190
274
|
setTimeFormat,
|
|
191
|
-
setDateFormat
|
|
275
|
+
setDateFormat,
|
|
276
|
+
setTheme,
|
|
277
|
+
setBrand,
|
|
278
|
+
setDensity,
|
|
279
|
+
setFontSize
|
|
192
280
|
}),
|
|
193
281
|
[
|
|
194
282
|
locale,
|
|
@@ -199,10 +287,18 @@ function AppProvider({
|
|
|
199
287
|
dateFnsLocale,
|
|
200
288
|
requestHeaders,
|
|
201
289
|
timezoneOptions,
|
|
290
|
+
theme,
|
|
291
|
+
brand,
|
|
292
|
+
density,
|
|
293
|
+
fontSize,
|
|
202
294
|
setLocale,
|
|
203
295
|
setTimezone,
|
|
204
296
|
setTimeFormat,
|
|
205
|
-
setDateFormat
|
|
297
|
+
setDateFormat,
|
|
298
|
+
setTheme,
|
|
299
|
+
setBrand,
|
|
300
|
+
setDensity,
|
|
301
|
+
setFontSize
|
|
206
302
|
]
|
|
207
303
|
);
|
|
208
304
|
return /* @__PURE__ */ jsx(AppContext.Provider, { value, children });
|
package/dist/app/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from "./date-formats";
|
|
|
6
6
|
export * from "./time-format-labels";
|
|
7
7
|
export * from "./date-format-labels";
|
|
8
8
|
export * from "./storage";
|
|
9
|
+
export * from "./theme-axes";
|
|
9
10
|
export * from "./request-headers";
|
|
10
11
|
export { AppProvider, useAppContext, useOptionalAppContext, useAppLocale, useAppTimezone, useAppTimeFormat, useAppDateFormat, } from "./app-provider";
|
|
11
12
|
export { useFormatting, useDateTime } from "./use-formatting";
|
package/dist/app/index.js
CHANGED
package/dist/app/storage.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AppBrand, type AppDensity, type AppFontSize, type AppTheme } from "./theme-axes";
|
|
1
2
|
import type { AppLocale, AppTimezone, AppTimeFormat, AppDateFormat } from "./types";
|
|
2
3
|
declare const DEFAULT_STORAGE_KEY = "godxjp.app";
|
|
3
4
|
export type StoredAppPreferences = {
|
|
@@ -5,6 +6,11 @@ export type StoredAppPreferences = {
|
|
|
5
6
|
timezone?: AppTimezone;
|
|
6
7
|
timeFormat?: AppTimeFormat;
|
|
7
8
|
dateFormat?: AppDateFormat;
|
|
9
|
+
theme?: AppTheme;
|
|
10
|
+
/** `null` = explicit opt-out (keep app token); on read, null/invalid → undefined. */
|
|
11
|
+
brand?: AppBrand | null;
|
|
12
|
+
density?: AppDensity;
|
|
13
|
+
fontSize?: AppFontSize;
|
|
8
14
|
};
|
|
9
15
|
export declare function readStoredPreferences(storageKey: string): StoredAppPreferences;
|
|
10
16
|
export declare function writeStoredPreferences(storageKey: string, preferences: StoredAppPreferences): void;
|
package/dist/app/storage.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { isAppLocale } from "./locales";
|
|
2
2
|
import { isAppDateFormat } from "./date-formats";
|
|
3
3
|
import { isAppTimeFormat } from "./time-formats";
|
|
4
|
+
import {
|
|
5
|
+
isAppBrand,
|
|
6
|
+
isAppDensity,
|
|
7
|
+
isAppFontSize,
|
|
8
|
+
isAppTheme
|
|
9
|
+
} from "./theme-axes";
|
|
4
10
|
const DEFAULT_STORAGE_KEY = "godxjp.app";
|
|
5
11
|
function readStoredPreferences(storageKey) {
|
|
6
12
|
if (typeof window === "undefined") return {};
|
|
@@ -12,7 +18,11 @@ function readStoredPreferences(storageKey) {
|
|
|
12
18
|
locale: isAppLocale(parsed.locale) ? parsed.locale : void 0,
|
|
13
19
|
timezone: typeof parsed.timezone === "string" ? parsed.timezone : void 0,
|
|
14
20
|
timeFormat: isAppTimeFormat(parsed.timeFormat) ? parsed.timeFormat : void 0,
|
|
15
|
-
dateFormat: isAppDateFormat(parsed.dateFormat) ? parsed.dateFormat : void 0
|
|
21
|
+
dateFormat: isAppDateFormat(parsed.dateFormat) ? parsed.dateFormat : void 0,
|
|
22
|
+
theme: isAppTheme(parsed.theme) ? parsed.theme : void 0,
|
|
23
|
+
brand: isAppBrand(parsed.brand) ? parsed.brand : void 0,
|
|
24
|
+
density: isAppDensity(parsed.density) ? parsed.density : void 0,
|
|
25
|
+
fontSize: isAppFontSize(parsed.fontSize) ? parsed.fontSize : void 0
|
|
16
26
|
};
|
|
17
27
|
} catch {
|
|
18
28
|
return {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PageDensityProp } from "../props/vocabulary/layout.prop";
|
|
2
|
+
/** Light / dark spine — `data-theme` (alias of the legacy `.dark` class). */
|
|
3
|
+
export type AppTheme = "light" | "dark";
|
|
4
|
+
/** Primary-palette preset — `data-brand`. `null` = keep the app's own `--primary`. */
|
|
5
|
+
export type AppBrand = "brand" | "crm" | "logistics" | "partner" | "slate";
|
|
6
|
+
/** Control / table / spacing density — `data-density` (same vocab as PageContainer). */
|
|
7
|
+
export type AppDensity = PageDensityProp;
|
|
8
|
+
/** Base type size — `data-font-size`; a preset rescales the whole golden scale. */
|
|
9
|
+
export type AppFontSize = "sm" | "default" | "lg";
|
|
10
|
+
export declare const APP_THEMES: readonly ["light", "dark"];
|
|
11
|
+
export declare const APP_BRANDS: readonly ["brand", "crm", "logistics", "partner", "slate"];
|
|
12
|
+
export declare const APP_DENSITIES: readonly ["compact", "default", "comfortable"];
|
|
13
|
+
export declare const APP_FONT_SIZES: readonly ["sm", "default", "lg"];
|
|
14
|
+
/** The four axes as held by AppProvider. `brand: null` opts out (app token wins). */
|
|
15
|
+
export type AppThemeAxes = {
|
|
16
|
+
theme: AppTheme;
|
|
17
|
+
brand: AppBrand | null;
|
|
18
|
+
density: AppDensity;
|
|
19
|
+
fontSize: AppFontSize;
|
|
20
|
+
};
|
|
21
|
+
export declare const isAppTheme: (v: unknown) => v is AppTheme;
|
|
22
|
+
export declare const isAppBrand: (v: unknown) => v is AppBrand;
|
|
23
|
+
export declare const isAppDensity: (v: unknown) => v is AppDensity;
|
|
24
|
+
export declare const isAppFontSize: (v: unknown) => v is AppFontSize;
|
|
25
|
+
/**
|
|
26
|
+
* Write the axis attributes onto a root element (default `<html>`). Pass `brand: null`
|
|
27
|
+
* to remove `data-brand` so the app's own `--primary` token applies. SSR-safe: callers
|
|
28
|
+
* guard `typeof document`. Exported for non-React / imperative consumers too.
|
|
29
|
+
*/
|
|
30
|
+
export declare function applyThemeAxes(el: HTMLElement, axes: Partial<AppThemeAxes>): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const APP_THEMES = ["light", "dark"];
|
|
2
|
+
const APP_BRANDS = [
|
|
3
|
+
"brand",
|
|
4
|
+
"crm",
|
|
5
|
+
"logistics",
|
|
6
|
+
"partner",
|
|
7
|
+
"slate"
|
|
8
|
+
];
|
|
9
|
+
const APP_DENSITIES = [
|
|
10
|
+
"compact",
|
|
11
|
+
"default",
|
|
12
|
+
"comfortable"
|
|
13
|
+
];
|
|
14
|
+
const APP_FONT_SIZES = ["sm", "default", "lg"];
|
|
15
|
+
const isAppTheme = (v) => APP_THEMES.includes(v);
|
|
16
|
+
const isAppBrand = (v) => APP_BRANDS.includes(v);
|
|
17
|
+
const isAppDensity = (v) => APP_DENSITIES.includes(v);
|
|
18
|
+
const isAppFontSize = (v) => APP_FONT_SIZES.includes(v);
|
|
19
|
+
function applyThemeAxes(el, axes) {
|
|
20
|
+
if (axes.theme !== void 0) el.dataset.theme = axes.theme;
|
|
21
|
+
if (axes.density !== void 0) el.dataset.density = axes.density;
|
|
22
|
+
if (axes.fontSize !== void 0) el.dataset.fontSize = axes.fontSize;
|
|
23
|
+
if (axes.brand !== void 0) {
|
|
24
|
+
if (axes.brand === null) delete el.dataset.brand;
|
|
25
|
+
else el.dataset.brand = axes.brand;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
APP_BRANDS,
|
|
30
|
+
APP_DENSITIES,
|
|
31
|
+
APP_FONT_SIZES,
|
|
32
|
+
APP_THEMES,
|
|
33
|
+
applyThemeAxes,
|
|
34
|
+
isAppBrand,
|
|
35
|
+
isAppDensity,
|
|
36
|
+
isAppFontSize,
|
|
37
|
+
isAppTheme
|
|
38
|
+
};
|
|
@@ -38,7 +38,7 @@ function PageContainerRoot({
|
|
|
38
38
|
footer,
|
|
39
39
|
breadcrumb,
|
|
40
40
|
linkComponent: LinkComponent = "a",
|
|
41
|
-
density
|
|
41
|
+
density,
|
|
42
42
|
variant = "default",
|
|
43
43
|
stickyFooter = false,
|
|
44
44
|
footerReveal = "always",
|
|
@@ -54,7 +54,9 @@ function PageContainerRoot({
|
|
|
54
54
|
"data-revealed": revealed ? "true" : void 0,
|
|
55
55
|
className: cn(
|
|
56
56
|
"ui-page-container",
|
|
57
|
-
|
|
57
|
+
// Unset → no class, so the page inherits the global density axis
|
|
58
|
+
// (:root[data-density]); an explicit prop emits a class that overrides it.
|
|
59
|
+
density && densityClass[density],
|
|
58
60
|
pageContainerVariantClass[variant],
|
|
59
61
|
stickyFooter && "ui-page-container--sticky-footer",
|
|
60
62
|
reveal && "ui-page-container--reveal-footer",
|
|
@@ -2,8 +2,9 @@ import * as React from "react";
|
|
|
2
2
|
import type { AppSettingPickerProp } from "../../props/components/app.prop";
|
|
3
3
|
export type { AppSettingKind, AppSettingPickerProp, AppSettingPickerProp as AppSettingPickerProps, } from "../../props/components/app.prop";
|
|
4
4
|
/**
|
|
5
|
-
* One provider-bound Select for any single AppProvider setting —
|
|
6
|
-
*
|
|
7
|
-
* reads/writes the matching context (`kind`); or pass
|
|
5
|
+
* One provider-bound Select for any single AppProvider setting — locale / timezone /
|
|
6
|
+
* date-format / time-format and the four theme axes (theme / brand / density / fontSize).
|
|
7
|
+
* Mount under `<AppProvider>` and it reads/writes the matching context (`kind`); or pass
|
|
8
|
+
* value + onValueChange to control it.
|
|
8
9
|
*/
|
|
9
10
|
export declare const AppSettingPicker: React.ForwardRefExoticComponent<AppSettingPickerProp & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CalendarDays,
|
|
5
|
+
Clock,
|
|
6
|
+
Globe,
|
|
7
|
+
Languages,
|
|
8
|
+
Palette,
|
|
9
|
+
Rows3,
|
|
10
|
+
SunMoon,
|
|
11
|
+
Type
|
|
12
|
+
} from "lucide-react";
|
|
4
13
|
import { APP_DATE_FORMAT_OPTIONS, getDateFormatLabel } from "../../app/date-format-labels";
|
|
5
14
|
import { APP_TIME_FORMAT_OPTIONS, getTimeFormatLabel } from "../../app/time-format-labels";
|
|
6
15
|
import { getTimezoneLabel, resolveTimezonePickerOptions } from "../../app/timezones";
|
|
7
16
|
import { APP_LOCALES } from "../../app/types";
|
|
17
|
+
import {
|
|
18
|
+
APP_BRANDS,
|
|
19
|
+
APP_DENSITIES,
|
|
20
|
+
APP_FONT_SIZES,
|
|
21
|
+
APP_THEMES
|
|
22
|
+
} from "../../app/theme-axes";
|
|
8
23
|
import { useOptionalAppContext } from "../../app/app-provider";
|
|
9
24
|
import { useTranslation } from "../../i18n/use-translation";
|
|
10
25
|
import { cn } from "../../lib/utils";
|
|
@@ -19,30 +34,48 @@ const ICON = {
|
|
|
19
34
|
locale: Languages,
|
|
20
35
|
timezone: Globe,
|
|
21
36
|
dateFormat: CalendarDays,
|
|
22
|
-
timeFormat: Clock
|
|
37
|
+
timeFormat: Clock,
|
|
38
|
+
theme: SunMoon,
|
|
39
|
+
brand: Palette,
|
|
40
|
+
density: Rows3,
|
|
41
|
+
fontSize: Type
|
|
23
42
|
};
|
|
24
43
|
const TRIGGER_WIDTH = {
|
|
25
44
|
locale: "sm:w-40",
|
|
26
45
|
timezone: "sm:w-56",
|
|
27
46
|
dateFormat: "sm:w-44",
|
|
28
|
-
timeFormat: "sm:w-44"
|
|
47
|
+
timeFormat: "sm:w-44",
|
|
48
|
+
theme: "sm:w-36",
|
|
49
|
+
brand: "sm:w-44",
|
|
50
|
+
density: "sm:w-40",
|
|
51
|
+
fontSize: "sm:w-36"
|
|
29
52
|
};
|
|
30
53
|
const ARIA_KEY = {
|
|
31
54
|
locale: "navigation.localePicker.ariaLabel",
|
|
32
55
|
timezone: "navigation.timezonePicker.ariaLabel",
|
|
33
56
|
dateFormat: "navigation.dateFormatPicker.ariaLabel",
|
|
34
|
-
timeFormat: "navigation.timeFormatPicker.ariaLabel"
|
|
57
|
+
timeFormat: "navigation.timeFormatPicker.ariaLabel",
|
|
58
|
+
theme: "navigation.themePicker.ariaLabel",
|
|
59
|
+
brand: "navigation.brandPicker.ariaLabel",
|
|
60
|
+
density: "navigation.densityPicker.ariaLabel",
|
|
61
|
+
fontSize: "navigation.fontSizePicker.ariaLabel"
|
|
35
62
|
};
|
|
63
|
+
const BRAND_NONE = "__app__";
|
|
36
64
|
const AppSettingPicker = React.forwardRef(
|
|
37
65
|
function AppSettingPicker2({ kind, className, disabled, id, name, value, onValueChange }, ref) {
|
|
38
66
|
const ctx = useOptionalAppContext();
|
|
39
67
|
const { t, locale, fallbackLocale } = useTranslation();
|
|
40
|
-
const
|
|
68
|
+
const raw = value ?? ctx?.[kind];
|
|
69
|
+
const current = raw == null ? kind === "brand" ? BRAND_NONE : void 0 : String(raw);
|
|
41
70
|
const setter = ctx ? {
|
|
42
71
|
locale: ctx.setLocale,
|
|
43
72
|
timezone: ctx.setTimezone,
|
|
44
73
|
dateFormat: ctx.setDateFormat,
|
|
45
|
-
timeFormat: ctx.setTimeFormat
|
|
74
|
+
timeFormat: ctx.setTimeFormat,
|
|
75
|
+
theme: ctx.setTheme,
|
|
76
|
+
density: ctx.setDensity,
|
|
77
|
+
fontSize: ctx.setFontSize,
|
|
78
|
+
brand: (next) => ctx.setBrand(next === BRAND_NONE ? null : next)
|
|
46
79
|
}[kind] : void 0;
|
|
47
80
|
const handleChange = onValueChange ?? setter;
|
|
48
81
|
const items = React.useMemo(() => {
|
|
@@ -64,6 +97,23 @@ const AppSettingPicker = React.forwardRef(
|
|
|
64
97
|
value: option.value,
|
|
65
98
|
label: getTimeFormatLabel(option.value, locale, fallbackLocale)
|
|
66
99
|
}));
|
|
100
|
+
case "theme":
|
|
101
|
+
return APP_THEMES.map((v) => ({ value: v, label: t(`navigation.themePicker.${v}`) }));
|
|
102
|
+
case "density":
|
|
103
|
+
return APP_DENSITIES.map((v) => ({
|
|
104
|
+
value: v,
|
|
105
|
+
label: t(`navigation.densityPicker.${v}`)
|
|
106
|
+
}));
|
|
107
|
+
case "fontSize":
|
|
108
|
+
return APP_FONT_SIZES.map((v) => ({
|
|
109
|
+
value: v,
|
|
110
|
+
label: t(`navigation.fontSizePicker.${v}`)
|
|
111
|
+
}));
|
|
112
|
+
case "brand":
|
|
113
|
+
return [
|
|
114
|
+
{ value: BRAND_NONE, label: t("navigation.brandPicker.none") },
|
|
115
|
+
...APP_BRANDS.map((v) => ({ value: v, label: t(`navigation.brandPicker.${v}`) }))
|
|
116
|
+
];
|
|
67
117
|
}
|
|
68
118
|
}, [kind, ctx?.timezoneOptions, current, t, locale, fallbackLocale]);
|
|
69
119
|
const unbound = current === void 0 || !handleChange;
|
|
@@ -201,6 +201,32 @@
|
|
|
201
201
|
"one": "Total {total} item",
|
|
202
202
|
"other": "Total {total} items"
|
|
203
203
|
}
|
|
204
|
+
},
|
|
205
|
+
"themePicker": {
|
|
206
|
+
"ariaLabel": "Theme",
|
|
207
|
+
"light": "Light",
|
|
208
|
+
"dark": "Dark"
|
|
209
|
+
},
|
|
210
|
+
"densityPicker": {
|
|
211
|
+
"ariaLabel": "Density",
|
|
212
|
+
"compact": "Compact",
|
|
213
|
+
"default": "Default",
|
|
214
|
+
"comfortable": "Comfortable"
|
|
215
|
+
},
|
|
216
|
+
"fontSizePicker": {
|
|
217
|
+
"ariaLabel": "Font size",
|
|
218
|
+
"sm": "Small",
|
|
219
|
+
"default": "Default",
|
|
220
|
+
"lg": "Large"
|
|
221
|
+
},
|
|
222
|
+
"brandPicker": {
|
|
223
|
+
"ariaLabel": "Brand",
|
|
224
|
+
"none": "App default",
|
|
225
|
+
"brand": "GodX Navy",
|
|
226
|
+
"crm": "CRM Violet",
|
|
227
|
+
"logistics": "Logistics Teal",
|
|
228
|
+
"partner": "Partner Orange",
|
|
229
|
+
"slate": "HQ Slate"
|
|
204
230
|
}
|
|
205
231
|
},
|
|
206
232
|
"locale": {
|
|
@@ -198,6 +198,32 @@
|
|
|
198
198
|
"pageSize": "ページサイズ",
|
|
199
199
|
"pageSizeOption": "{size} / ページ",
|
|
200
200
|
"total": "全 {total} 件"
|
|
201
|
+
},
|
|
202
|
+
"themePicker": {
|
|
203
|
+
"ariaLabel": "テーマ",
|
|
204
|
+
"light": "ライト",
|
|
205
|
+
"dark": "ダーク"
|
|
206
|
+
},
|
|
207
|
+
"densityPicker": {
|
|
208
|
+
"ariaLabel": "密度",
|
|
209
|
+
"compact": "コンパクト",
|
|
210
|
+
"default": "標準",
|
|
211
|
+
"comfortable": "広め"
|
|
212
|
+
},
|
|
213
|
+
"fontSizePicker": {
|
|
214
|
+
"ariaLabel": "文字サイズ",
|
|
215
|
+
"sm": "小",
|
|
216
|
+
"default": "標準",
|
|
217
|
+
"lg": "大"
|
|
218
|
+
},
|
|
219
|
+
"brandPicker": {
|
|
220
|
+
"ariaLabel": "ブランド",
|
|
221
|
+
"none": "アプリ既定",
|
|
222
|
+
"brand": "GodX Navy",
|
|
223
|
+
"crm": "CRM Violet",
|
|
224
|
+
"logistics": "Logistics Teal",
|
|
225
|
+
"partner": "Partner Orange",
|
|
226
|
+
"slate": "HQ Slate"
|
|
201
227
|
}
|
|
202
228
|
},
|
|
203
229
|
"locale": {
|
|
@@ -198,6 +198,32 @@
|
|
|
198
198
|
"pageSize": "Số dòng mỗi trang",
|
|
199
199
|
"pageSizeOption": "{size} / trang",
|
|
200
200
|
"total": "Tổng {total} mục"
|
|
201
|
+
},
|
|
202
|
+
"themePicker": {
|
|
203
|
+
"ariaLabel": "Giao diện",
|
|
204
|
+
"light": "Sáng",
|
|
205
|
+
"dark": "Tối"
|
|
206
|
+
},
|
|
207
|
+
"densityPicker": {
|
|
208
|
+
"ariaLabel": "Mật độ",
|
|
209
|
+
"compact": "Gọn",
|
|
210
|
+
"default": "Mặc định",
|
|
211
|
+
"comfortable": "Thoáng"
|
|
212
|
+
},
|
|
213
|
+
"fontSizePicker": {
|
|
214
|
+
"ariaLabel": "Cỡ chữ",
|
|
215
|
+
"sm": "Nhỏ",
|
|
216
|
+
"default": "Mặc định",
|
|
217
|
+
"lg": "Lớn"
|
|
218
|
+
},
|
|
219
|
+
"brandPicker": {
|
|
220
|
+
"ariaLabel": "Thương hiệu",
|
|
221
|
+
"none": "Mặc định app",
|
|
222
|
+
"brand": "GodX Navy",
|
|
223
|
+
"crm": "CRM Violet",
|
|
224
|
+
"logistics": "Logistics Teal",
|
|
225
|
+
"partner": "Partner Orange",
|
|
226
|
+
"slate": "HQ Slate"
|
|
201
227
|
}
|
|
202
228
|
},
|
|
203
229
|
"locale": {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Locale } from "date-fns";
|
|
3
3
|
import type { DayPickerProps } from "react-day-picker";
|
|
4
4
|
import type { AppLocale, AppRequestHeaders, AppTimeFormat, AppTimezone, AppTimezoneDefault, AppDateFormat } from "../../app/types";
|
|
5
|
+
import type { AppBrand, AppDensity, AppFontSize, AppTheme } from "../../app/theme-axes";
|
|
5
6
|
import type { ChildrenProp, ClassNameProp, DisabledProp, IdProp, NameProp, OnValueChangeProp, ValueProp } from "../vocabulary";
|
|
6
7
|
/** @see AppProvider */
|
|
7
8
|
export type AppProviderProp = {
|
|
@@ -27,13 +28,31 @@ export type AppProviderProp = {
|
|
|
27
28
|
storageKey?: string;
|
|
28
29
|
/** Persist user choices. Default: true. */
|
|
29
30
|
persist?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Initial light/dark theme — written to `<html data-theme>`. Default: `"light"`.
|
|
33
|
+
* (The legacy `.dark` class still works as an equal alias.)
|
|
34
|
+
*/
|
|
35
|
+
theme?: AppTheme;
|
|
36
|
+
/**
|
|
37
|
+
* Initial brand palette preset — written to `<html data-brand>`. OPT-IN: omit
|
|
38
|
+
* (or `null`) to keep the brand `--primary` your own `theme.css` defines.
|
|
39
|
+
*/
|
|
40
|
+
brand?: AppBrand | null;
|
|
41
|
+
/** Initial density — written to `<html data-density>`. Default: `"default"`. */
|
|
42
|
+
density?: AppDensity;
|
|
43
|
+
/** Initial base type size — written to `<html data-font-size>`. Default: `"default"`. */
|
|
44
|
+
fontSize?: AppFontSize;
|
|
30
45
|
onLocaleChange?: (locale: AppLocale) => void;
|
|
31
46
|
onTimezoneChange?: (timezone: AppTimezone) => void;
|
|
32
47
|
onTimeFormatChange?: (timeFormat: AppTimeFormat) => void;
|
|
33
48
|
onDateFormatChange?: (dateFormat: AppDateFormat) => void;
|
|
49
|
+
onThemeChange?: (theme: AppTheme) => void;
|
|
50
|
+
onBrandChange?: (brand: AppBrand | null) => void;
|
|
51
|
+
onDensityChange?: (density: AppDensity) => void;
|
|
52
|
+
onFontSizeChange?: (fontSize: AppFontSize) => void;
|
|
34
53
|
};
|
|
35
54
|
/** Which AppProvider setting the {@link AppSettingPicker} reads/writes. */
|
|
36
|
-
export type AppSettingKind = "locale" | "timezone" | "dateFormat" | "timeFormat";
|
|
55
|
+
export type AppSettingKind = "locale" | "timezone" | "dateFormat" | "timeFormat" | "theme" | "brand" | "density" | "fontSize";
|
|
37
56
|
/**
|
|
38
57
|
* @see AppSettingPicker — one provider-bound Select for any single AppProvider setting.
|
|
39
58
|
* Replaces the former Locale/Timezone/Date-format/Time-format pickers; pick the target
|
|
@@ -62,8 +81,17 @@ export type AppContextValue = {
|
|
|
62
81
|
requestHeaders: AppRequestHeaders;
|
|
63
82
|
/** Configured timezone list; `undefined` → full IANA in the timezone-picker recipe. */
|
|
64
83
|
timezoneOptions?: readonly AppTimezone[];
|
|
84
|
+
/** Current theme axes (mirror `<html data-*>`). */
|
|
85
|
+
theme: AppTheme;
|
|
86
|
+
brand: AppBrand | null;
|
|
87
|
+
density: AppDensity;
|
|
88
|
+
fontSize: AppFontSize;
|
|
65
89
|
setLocale: (locale: AppLocale) => void;
|
|
66
90
|
setTimezone: (timezone: AppTimezone) => void;
|
|
67
91
|
setTimeFormat: (timeFormat: AppTimeFormat) => void;
|
|
68
92
|
setDateFormat: (dateFormat: AppDateFormat) => void;
|
|
93
|
+
setTheme: (theme: AppTheme) => void;
|
|
94
|
+
setBrand: (brand: AppBrand | null) => void;
|
|
95
|
+
setDensity: (density: AppDensity) => void;
|
|
96
|
+
setFontSize: (fontSize: AppFontSize) => void;
|
|
69
97
|
};
|
package/dist/styles/density.css
CHANGED
|
@@ -1,47 +1,61 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* DENSITY — one knob retunes φ-unit, controls, tables
|
|
3
|
-
*
|
|
2
|
+
* DENSITY — one knob retunes φ-unit, controls, tables. Two entry points, same vars:
|
|
3
|
+
* • `<PageContainer density>` → `.ui-density-*` class (scoped to a descendant,
|
|
4
|
+
* wins for its subtree by proximity).
|
|
5
|
+
* • the density AXIS → `:root[data-density="*"]` on <html> (AppProvider density prop),
|
|
6
|
+
* so density cascades app-wide incl. portalled overlays. PageContainer inherits it
|
|
7
|
+
* when its `density` prop is unset (emits no class).
|
|
8
|
+
*
|
|
9
|
+
* INTENTIONALLY UNLAYERED — do NOT wrap in `@layer components`. The active
|
|
10
|
+
* tokens (--control-height, --table-row-height, --phi-unit…) get their defaults
|
|
11
|
+
* from UNLAYERED `:root` rules in tokens/components/*.css. A layered block always
|
|
12
|
+
* loses to an unlayered one regardless of specificity, so a layered
|
|
13
|
+
* `:root[data-density]` would silently never override those defaults (the axis
|
|
14
|
+
* would be a no-op on <html>). Unlayered, `:root[data-density="x"]` (0,2,0) beats
|
|
15
|
+
* the `:root` default (0,1,0) by specificity. Apps use the prop / axis, never
|
|
16
|
+
* these classes directly.
|
|
4
17
|
*/
|
|
5
18
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
.ui-density-compact,
|
|
20
|
+
:root[data-density="compact"] {
|
|
21
|
+
--control-height: var(--control-height-compact);
|
|
22
|
+
--control-padding-x: var(--control-padding-x-compact);
|
|
23
|
+
--table-row-height: var(--table-row-height-compact);
|
|
24
|
+
--phi-unit: var(--space-3);
|
|
25
|
+
--checkbox-size: var(--checkbox-size-compact);
|
|
26
|
+
--switch-width: var(--switch-width-compact);
|
|
27
|
+
--switch-height: var(--switch-height-compact);
|
|
28
|
+
--switch-thumb-size: var(--switch-thumb-size-compact);
|
|
29
|
+
--switch-thumb-translate: var(--switch-thumb-translate-compact);
|
|
30
|
+
--table-cell-padding-y: var(--space-1);
|
|
31
|
+
/* Tighter overlay/card chrome at compact density (12px vertical). */
|
|
32
|
+
--space-chrome-y: var(--space-3);
|
|
33
|
+
}
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
.ui-density-default,
|
|
36
|
+
:root[data-density="default"] {
|
|
37
|
+
--control-height: var(--control-height-default);
|
|
38
|
+
--control-padding-x: var(--control-padding-x-default);
|
|
39
|
+
--table-row-height: var(--table-row-height-default);
|
|
40
|
+
--table-cell-padding-y: var(--space-2);
|
|
41
|
+
}
|
|
28
42
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
.ui-density-comfortable,
|
|
44
|
+
:root[data-density="comfortable"] {
|
|
45
|
+
--control-height: var(--control-height-comfortable);
|
|
46
|
+
/* Comfortable steps 44 → 36 → 32 (design spacing-density), steeper than the
|
|
47
|
+
* default 4px step so sm/xs aren't left 4px too tall. */
|
|
48
|
+
--control-height-sm: calc(var(--control-height) - 0.5rem);
|
|
49
|
+
--control-height-xs: calc(var(--control-height) - 0.75rem);
|
|
50
|
+
--control-padding-x: var(--control-padding-x-comfortable);
|
|
51
|
+
--table-row-height: var(--table-row-height-comfortable);
|
|
52
|
+
--phi-unit: var(--space-6);
|
|
53
|
+
--checkbox-size: var(--checkbox-size-comfortable);
|
|
54
|
+
--switch-width: var(--switch-width-comfortable);
|
|
55
|
+
--switch-height: var(--switch-height-comfortable);
|
|
56
|
+
--switch-thumb-size: var(--switch-thumb-size-comfortable);
|
|
57
|
+
--switch-thumb-translate: var(--switch-thumb-translate-comfortable);
|
|
58
|
+
--table-cell-padding-y: var(--space-2);
|
|
59
|
+
/* Roomier overlay/card chrome at comfortable density (24px vertical). */
|
|
60
|
+
--space-chrome-y: var(--space-6);
|
|
47
61
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/* THEME AXES — global consumer API. AppProvider's theme/brand/density/fontSize
|
|
2
|
+
* props write `data-*` attributes onto <html>; the selectors here bind each axis
|
|
3
|
+
* to tokens (Rule #21 "every component honours every theme axis"). Density lives
|
|
4
|
+
* in density.css (shared with the .ui-density-* classes); the dark theme aliases
|
|
5
|
+
* in foundation.css. Every axis is a NO-OP at its default value, so apps that set
|
|
6
|
+
* nothing — or override tokens in their own theme.css — are unaffected.
|
|
7
|
+
*
|
|
8
|
+
* `:root[data-…]` (specificity 0,2,0) intentionally outranks an app theme.css
|
|
9
|
+
* `:root` (0,1,0): when an axis IS set it wins; when it is NOT set the app token
|
|
10
|
+
* override wins. brand is therefore fully opt-in (no default rule). */
|
|
11
|
+
|
|
12
|
+
/* ── font-size axis — base-driven: a preset sets ONLY --font-size-base and the
|
|
13
|
+
* whole golden modular scale (foundation.css) rescales from it. The `default`
|
|
14
|
+
* value emits no rule → foundation 14px stands. */
|
|
15
|
+
:root[data-font-size="sm"] {
|
|
16
|
+
--font-size-base: 0.8125rem; /* 13px */
|
|
17
|
+
}
|
|
18
|
+
:root[data-font-size="lg"] {
|
|
19
|
+
--font-size-base: 1rem; /* 16px */
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* ── brand axis — primary-palette presets (canonical values; src/test/theme-globals.ts
|
|
23
|
+
* mirrors these for the Storybook toolbar). OPT-IN: no `default` rule, so an app
|
|
24
|
+
* that pins --primary in its own theme.css keeps it unless it sets data-brand. */
|
|
25
|
+
:root[data-brand="brand"] {
|
|
26
|
+
--primary: 211 73% 15%;
|
|
27
|
+
--primary-foreground: 0 0% 100%;
|
|
28
|
+
--ring: 24 99% 46%;
|
|
29
|
+
--accent: 24 99% 95%;
|
|
30
|
+
--accent-foreground: 24 99% 28%;
|
|
31
|
+
}
|
|
32
|
+
:root[data-brand="crm"] {
|
|
33
|
+
--primary: 262 83% 58%;
|
|
34
|
+
--primary-foreground: 0 0% 100%;
|
|
35
|
+
--ring: 262 83% 58%;
|
|
36
|
+
--accent: 262 83% 96%;
|
|
37
|
+
--accent-foreground: 262 83% 28%;
|
|
38
|
+
}
|
|
39
|
+
:root[data-brand="logistics"] {
|
|
40
|
+
--primary: 173 80% 36%;
|
|
41
|
+
--primary-foreground: 0 0% 100%;
|
|
42
|
+
--ring: 173 80% 36%;
|
|
43
|
+
--accent: 173 80% 94%;
|
|
44
|
+
--accent-foreground: 173 80% 22%;
|
|
45
|
+
}
|
|
46
|
+
:root[data-brand="partner"] {
|
|
47
|
+
--primary: 24 95% 53%;
|
|
48
|
+
--primary-foreground: 0 0% 100%;
|
|
49
|
+
--ring: 24 95% 53%;
|
|
50
|
+
--accent: 24 95% 95%;
|
|
51
|
+
--accent-foreground: 24 95% 28%;
|
|
52
|
+
}
|
|
53
|
+
:root[data-brand="slate"] {
|
|
54
|
+
--primary: 215 25% 27%;
|
|
55
|
+
--primary-foreground: 0 0% 100%;
|
|
56
|
+
--ring: 215 25% 27%;
|
|
57
|
+
--accent: 215 25% 94%;
|
|
58
|
+
--accent-foreground: 215 25% 20%;
|
|
59
|
+
}
|
package/dist/tokens/base.css
CHANGED
|
@@ -162,8 +162,11 @@
|
|
|
162
162
|
--phi-p2: calc(var(--phi-p1) * var(--ratio-phi));
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
.dark
|
|
166
|
-
|
|
165
|
+
.dark,
|
|
166
|
+
:root[data-theme="dark"] {
|
|
167
|
+
/* Warm-tinted dark spine — keeps the hue-60 character at low lightness.
|
|
168
|
+
* `data-theme="dark"` is the theme axis (AppProvider theme prop → <html>); the
|
|
169
|
+
* legacy `.dark` class stays an equal alias so existing toggles keep working. */
|
|
167
170
|
--background: 48 9% 9%;
|
|
168
171
|
--foreground: 60 20% 96%;
|
|
169
172
|
--card: 48 8% 12%;
|