@godxjp/ui 13.13.0 → 13.15.0
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/feedback/sonner.js +7 -3
- package/dist/components/layout/page-container.d.ts +1 -1
- package/dist/components/layout/page-container.js +36 -3
- 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/props/components/layout.prop.d.ts +10 -0
- package/dist/styles/density.css +12 -5
- package/dist/styles/layout.css +25 -0
- package/dist/tokens/axes.css +59 -0
- package/dist/tokens/base.css +1 -0
- package/dist/tokens/components/control.css +8 -7
- 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
|
+
};
|
|
@@ -34,9 +34,13 @@ function Toaster({ ...props }) {
|
|
|
34
34
|
loading: /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": "true" })
|
|
35
35
|
},
|
|
36
36
|
style: {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
// Color tokens are raw HSL triplets (consumed as hsl(var(--token)));
|
|
38
|
+
// sonner uses these vars verbatim as CSS colors, so wrap with hsl()
|
|
39
|
+
// here — unwrapped they are invalid values and the toast renders
|
|
40
|
+
// transparent.
|
|
41
|
+
"--normal-bg": "hsl(var(--popover))",
|
|
42
|
+
"--normal-text": "hsl(var(--popover-foreground))",
|
|
43
|
+
"--normal-border": "hsl(var(--border))",
|
|
40
44
|
"--border-radius": "var(--radius)"
|
|
41
45
|
},
|
|
42
46
|
position: "bottom-right",
|
|
@@ -2,7 +2,7 @@ import type { PageContainerProp, PageInsetProp } from "../../props/components/la
|
|
|
2
2
|
export type { PageContainerProp, PageContainerProp as PageContainerProps, } from "../../props/components/layout.prop";
|
|
3
3
|
export type { BreadcrumbItemProp, BreadcrumbItemProp as BreadcrumbItem, } from "../../props/vocabulary/navigation.prop";
|
|
4
4
|
export declare function PageContainerInset({ className, children, ...props }: PageInsetProp): import("react/jsx-runtime").JSX.Element;
|
|
5
|
-
declare function PageContainerRoot({ title, subtitle, extra, footer, breadcrumb, linkComponent: LinkComponent, density, variant, stickyFooter, fill, children, className, }: PageContainerProp): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
declare function PageContainerRoot({ title, subtitle, extra, footer, breadcrumb, linkComponent: LinkComponent, density, variant, stickyFooter, footerReveal, fill, children, className, }: PageContainerProp): import("react/jsx-runtime").JSX.Element;
|
|
6
6
|
export declare const PageContainer: typeof PageContainerRoot & {
|
|
7
7
|
Inset: typeof PageContainerInset;
|
|
8
8
|
};
|
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
3
|
import { ChevronRight } from "lucide-react";
|
|
3
4
|
import { cn } from "../../lib/utils";
|
|
4
5
|
import { densityClass, pageContainerVariantClass } from "../../lib/variants";
|
|
6
|
+
function scrollParent(el) {
|
|
7
|
+
let node = el?.parentElement ?? null;
|
|
8
|
+
while (node) {
|
|
9
|
+
const overflowY = getComputedStyle(node).overflowY;
|
|
10
|
+
if (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") return node;
|
|
11
|
+
node = node.parentElement;
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
function useFooterReveal(enabled) {
|
|
16
|
+
const headerRef = useRef(null);
|
|
17
|
+
const [revealed, setRevealed] = useState(false);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!enabled) return;
|
|
20
|
+
const el = headerRef.current;
|
|
21
|
+
if (!el || typeof IntersectionObserver === "undefined") return;
|
|
22
|
+
const observer = new IntersectionObserver(
|
|
23
|
+
([entry]) => setRevealed(!entry.isIntersecting),
|
|
24
|
+
{ root: scrollParent(el), threshold: 0 }
|
|
25
|
+
);
|
|
26
|
+
observer.observe(el);
|
|
27
|
+
return () => observer.disconnect();
|
|
28
|
+
}, [enabled]);
|
|
29
|
+
return { headerRef, revealed: enabled && revealed };
|
|
30
|
+
}
|
|
5
31
|
function PageContainerInset({ className, children, ...props }) {
|
|
6
32
|
return /* @__PURE__ */ jsx("div", { className: cn("ui-page-container-inset", className), ...props, children });
|
|
7
33
|
}
|
|
@@ -12,26 +38,33 @@ function PageContainerRoot({
|
|
|
12
38
|
footer,
|
|
13
39
|
breadcrumb,
|
|
14
40
|
linkComponent: LinkComponent = "a",
|
|
15
|
-
density
|
|
41
|
+
density,
|
|
16
42
|
variant = "default",
|
|
17
43
|
stickyFooter = false,
|
|
44
|
+
footerReveal = "always",
|
|
18
45
|
fill = false,
|
|
19
46
|
children,
|
|
20
47
|
className
|
|
21
48
|
}) {
|
|
49
|
+
const reveal = stickyFooter && footer != null && footerReveal === "onScroll";
|
|
50
|
+
const { headerRef, revealed } = useFooterReveal(reveal);
|
|
22
51
|
return /* @__PURE__ */ jsxs(
|
|
23
52
|
"div",
|
|
24
53
|
{
|
|
54
|
+
"data-revealed": revealed ? "true" : void 0,
|
|
25
55
|
className: cn(
|
|
26
56
|
"ui-page-container",
|
|
27
|
-
|
|
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],
|
|
28
60
|
pageContainerVariantClass[variant],
|
|
29
61
|
stickyFooter && "ui-page-container--sticky-footer",
|
|
62
|
+
reveal && "ui-page-container--reveal-footer",
|
|
30
63
|
fill && "ui-page-container--fill",
|
|
31
64
|
className
|
|
32
65
|
),
|
|
33
66
|
children: [
|
|
34
|
-
/* @__PURE__ */ jsxs("header", { className: "ui-page-header", children: [
|
|
67
|
+
/* @__PURE__ */ jsxs("header", { ref: headerRef, className: "ui-page-header", children: [
|
|
35
68
|
breadcrumb && breadcrumb.length > 0 && /* @__PURE__ */ jsx("nav", { "aria-label": "Breadcrumb", className: "ui-breadcrumb", children: /* @__PURE__ */ jsx("ol", { className: "ui-breadcrumb-list", children: breadcrumb.map((item, i) => {
|
|
36
69
|
const isLast = i === breadcrumb.length - 1;
|
|
37
70
|
return /* @__PURE__ */ jsxs("li", { className: "ui-inline-xs", children: [
|
|
@@ -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
|
};
|
|
@@ -14,6 +14,16 @@ export type PageContainerProp = {
|
|
|
14
14
|
variant?: PageContainerVariantProp;
|
|
15
15
|
/** Pin footer to viewport bottom on scroll — pairs well with `variant="narrow"`. */
|
|
16
16
|
stickyFooter?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* When the footer is sticky, control WHEN it shows. `"always"` (default)
|
|
19
|
+
* keeps it pinned the whole time. `"onScroll"` hides it until the header
|
|
20
|
+
* (title + `extra` actions) scrolls out of view, then slides it up — the
|
|
21
|
+
* standard edit/create "save bar" so the primary actions stay reachable as
|
|
22
|
+
* the form scrolls, without cluttering the top. The footer stays mounted
|
|
23
|
+
* (no layout reflow → no jitter); observed against the nearest scroll
|
|
24
|
+
* container.
|
|
25
|
+
*/
|
|
26
|
+
footerReveal?: "always" | "onScroll";
|
|
17
27
|
/**
|
|
18
28
|
* Grow the body to fill the remaining shell height. Default `false` (top-packed,
|
|
19
29
|
* content-height — short pages leave no stretched void, gh#103). Enable for a
|
package/dist/styles/density.css
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
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, overrides global).
|
|
4
|
+
* • the density AXIS → `:root[data-density="*"]` on <html> (AppProvider density prop),
|
|
5
|
+
* so density cascades app-wide incl. portalled overlays. PageContainer inherits it
|
|
6
|
+
* when its `density` prop is unset (emits no class).
|
|
7
|
+
* Apps use the prop / axis, never these classes directly.
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
@layer components {
|
|
7
|
-
.ui-density-compact
|
|
11
|
+
.ui-density-compact,
|
|
12
|
+
:root[data-density="compact"] {
|
|
8
13
|
--control-height: var(--control-height-compact);
|
|
9
14
|
--control-padding-x: var(--control-padding-x-compact);
|
|
10
15
|
--table-row-height: var(--table-row-height-compact);
|
|
@@ -19,14 +24,16 @@
|
|
|
19
24
|
--space-chrome-y: var(--space-3);
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
.ui-density-default
|
|
27
|
+
.ui-density-default,
|
|
28
|
+
:root[data-density="default"] {
|
|
23
29
|
--control-height: var(--control-height-default);
|
|
24
30
|
--control-padding-x: var(--control-padding-x-default);
|
|
25
31
|
--table-row-height: var(--table-row-height-default);
|
|
26
32
|
--table-cell-padding-y: var(--space-2);
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
.ui-density-comfortable
|
|
35
|
+
.ui-density-comfortable,
|
|
36
|
+
:root[data-density="comfortable"] {
|
|
30
37
|
--control-height: var(--control-height-comfortable);
|
|
31
38
|
/* Comfortable steps 44 → 36 → 32 (design spacing-density), steeper than the
|
|
32
39
|
* default 4px step so sm/xs aren't left 4px too tall. */
|
package/dist/styles/layout.css
CHANGED
|
@@ -308,6 +308,31 @@
|
|
|
308
308
|
background-color: hsl(var(--background));
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/* footerReveal="onScroll": the sticky footer stays mounted (reserving its
|
|
312
|
+
* flow space, so toggling never reflows the body → no scroll jitter) but is
|
|
313
|
+
* slid below the fold until the header leaves the viewport, then slides up.
|
|
314
|
+
* data-revealed is toggled by an IntersectionObserver on the header. */
|
|
315
|
+
.ui-page-container--reveal-footer .ui-page-footer {
|
|
316
|
+
transform: translateY(100%);
|
|
317
|
+
opacity: 0;
|
|
318
|
+
pointer-events: none;
|
|
319
|
+
transition:
|
|
320
|
+
transform 200ms ease-out,
|
|
321
|
+
opacity 200ms ease-out;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.ui-page-container--reveal-footer[data-revealed="true"] .ui-page-footer {
|
|
325
|
+
transform: none;
|
|
326
|
+
opacity: 1;
|
|
327
|
+
pointer-events: auto;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@media (prefers-reduced-motion: reduce) {
|
|
331
|
+
.ui-page-container--reveal-footer .ui-page-footer {
|
|
332
|
+
transition: none;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
311
336
|
.ui-page-header {
|
|
312
337
|
display: flex;
|
|
313
338
|
flex-direction: column;
|
|
@@ -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
|
@@ -62,6 +62,14 @@
|
|
|
62
62
|
--search-input-end-padding: calc(
|
|
63
63
|
var(--search-input-edge-inset) + var(--control-icon-size) + var(--control-gap)
|
|
64
64
|
);
|
|
65
|
+
|
|
66
|
+
--choice-description-font-size: var(--font-size-xs);
|
|
67
|
+
--color-picker-hex-font-size: var(--font-size-xs);
|
|
68
|
+
--command-group-heading-font-size: var(--font-size-xs);
|
|
69
|
+
--search-input-label-font-size: var(--font-size-xs);
|
|
70
|
+
--tag-input-chip-font-size: var(--font-size-xs);
|
|
71
|
+
--toggle-sm-font-size: var(--font-size-xs);
|
|
72
|
+
--button-sm-font-size: var(--font-size-xs);
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
/* Rule #24 — on touch devices (coarse pointer) interactive controls keep a ≥44px tap target
|
|
@@ -72,11 +80,4 @@
|
|
|
72
80
|
--control-height-compact: 2.75rem;
|
|
73
81
|
--control-height-default: 2.75rem;
|
|
74
82
|
}
|
|
75
|
-
--choice-description-font-size: var(--font-size-xs);
|
|
76
|
-
--color-picker-hex-font-size: var(--font-size-xs);
|
|
77
|
-
--command-group-heading-font-size: var(--font-size-xs);
|
|
78
|
-
--search-input-label-font-size: var(--font-size-xs);
|
|
79
|
-
--tag-input-chip-font-size: var(--font-size-xs);
|
|
80
|
-
--toggle-sm-font-size: var(--font-size-xs);
|
|
81
|
-
--button-sm-font-size: var(--font-size-xs);
|
|
82
83
|
}
|
|
@@ -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%;
|