@shadow-js/ui 0.0.0-stage → 1.0.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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +598 -2
- package/dist/cli.mjs +2293 -0
- package/dist/dev/cli/schema.d.ts +73 -0
- package/dist/dev/index.d.ts +6 -0
- package/dist/dev/index.js +5 -0
- package/dist/dev/locales/ar.d.ts +141 -0
- package/dist/dev/locales/ar.js +128 -0
- package/dist/dev/locales/ar.js.map +1 -0
- package/dist/dev/locales/en.d.ts +141 -0
- package/dist/dev/locales/en.js +128 -0
- package/dist/dev/locales/en.js.map +1 -0
- package/dist/dev/registry/utils/utils.d.ts +29 -0
- package/dist/dev/registry/utils/utils.js +27 -0
- package/dist/dev/registry/utils/utils.js.map +1 -0
- package/dist/dev/theme/theme-logic.d.ts +187 -0
- package/dist/dev/theme/theme-logic.js +184 -0
- package/dist/dev/theme/theme-logic.js.map +1 -0
- package/dist/dev/theme/theme-provider.d.ts +90 -0
- package/dist/dev/theme/theme-provider.js +150 -0
- package/dist/dev/theme/theme-provider.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/locales/ar.js +2 -0
- package/dist/prod/locales/ar.js.map +1 -0
- package/dist/prod/locales/en.js +2 -0
- package/dist/prod/locales/en.js.map +1 -0
- package/dist/prod/registry/utils/utils.js +2 -0
- package/dist/prod/registry/utils/utils.js.map +1 -0
- package/dist/prod/theme/theme-logic.js +2 -0
- package/dist/prod/theme/theme-logic.js.map +1 -0
- package/dist/prod/theme/theme-provider.js +2 -0
- package/dist/prod/theme/theme-provider.js.map +1 -0
- package/package.json +102 -4
- package/registry/accordion/accordion.tsx +210 -0
- package/registry/accordion/item.json +4 -0
- package/registry/alert/alert.tsx +200 -0
- package/registry/alert/item.json +4 -0
- package/registry/alert-dialog/alert-dialog.tsx +530 -0
- package/registry/alert-dialog/item.json +4 -0
- package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
- package/registry/aspect-ratio/item.json +4 -0
- package/registry/attachment/attachment.tsx +459 -0
- package/registry/attachment/item.json +4 -0
- package/registry/avatar/avatar.tsx +385 -0
- package/registry/avatar/item.json +4 -0
- package/registry/badge/badge.tsx +108 -0
- package/registry/badge/item.json +4 -0
- package/registry/breadcrumb/breadcrumb.tsx +354 -0
- package/registry/breadcrumb/item.json +4 -0
- package/registry/bubble/bubble.tsx +260 -0
- package/registry/bubble/item.json +4 -0
- package/registry/button/button.tsx +125 -0
- package/registry/button/item.json +4 -0
- package/registry/button-group/button-group.tsx +189 -0
- package/registry/button-group/item.json +4 -0
- package/registry/calendar/calendar.tsx +348 -0
- package/registry/calendar/item.json +4 -0
- package/registry/card/card.tsx +311 -0
- package/registry/card/item.json +4 -0
- package/registry/carousel/carousel.tsx +851 -0
- package/registry/carousel/item.json +4 -0
- package/registry/chart/chart.tsx +1956 -0
- package/registry/chart/item.json +4 -0
- package/registry/chart-scale/chart-scale.ts +670 -0
- package/registry/chart-scale/item.json +5 -0
- package/registry/checkbox/checkbox.tsx +79 -0
- package/registry/checkbox/item.json +4 -0
- package/registry/code-block/code-block.tsx +470 -0
- package/registry/code-block/item.json +4 -0
- package/registry/collapsible/collapsible.tsx +117 -0
- package/registry/collapsible/item.json +4 -0
- package/registry/combobox/combobox.tsx +977 -0
- package/registry/combobox/item.json +4 -0
- package/registry/command/command.tsx +757 -0
- package/registry/command/item.json +4 -0
- package/registry/context-menu/context-menu.tsx +697 -0
- package/registry/context-menu/item.json +4 -0
- package/registry/data-table/data-table.tsx +437 -0
- package/registry/data-table/item.json +4 -0
- package/registry/date-picker/date-picker.tsx +349 -0
- package/registry/date-picker/item.json +4 -0
- package/registry/dialog/dialog.tsx +412 -0
- package/registry/dialog/item.json +4 -0
- package/registry/direction/direction.tsx +88 -0
- package/registry/direction/item.json +4 -0
- package/registry/drawer/drawer.tsx +787 -0
- package/registry/drawer/item.json +4 -0
- package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
- package/registry/dropdown-menu/item.json +4 -0
- package/registry/empty/empty.tsx +240 -0
- package/registry/empty/item.json +4 -0
- package/registry/field/field.tsx +624 -0
- package/registry/field/item.json +4 -0
- package/registry/form/form.tsx +1112 -0
- package/registry/form/item.json +4 -0
- package/registry/hover-card/hover-card.tsx +144 -0
- package/registry/hover-card/item.json +4 -0
- package/registry/icons/icons.tsx +1691 -0
- package/registry/icons/item.json +4 -0
- package/registry/input/input.tsx +43 -0
- package/registry/input/item.json +4 -0
- package/registry/input-group/input-group.tsx +315 -0
- package/registry/input-group/item.json +4 -0
- package/registry/input-otp/input-otp.tsx +329 -0
- package/registry/input-otp/item.json +4 -0
- package/registry/item/item.json +4 -0
- package/registry/item/item.tsx +439 -0
- package/registry/kbd/item.json +4 -0
- package/registry/kbd/kbd.tsx +65 -0
- package/registry/label/item.json +4 -0
- package/registry/label/label.tsx +60 -0
- package/registry/marker/item.json +4 -0
- package/registry/marker/marker.tsx +175 -0
- package/registry/menubar/item.json +4 -0
- package/registry/menubar/menubar.tsx +694 -0
- package/registry/message/item.json +4 -0
- package/registry/message/message.tsx +280 -0
- package/registry/message-scroller/item.json +4 -0
- package/registry/message-scroller/message-scroller.tsx +262 -0
- package/registry/native-select/item.json +4 -0
- package/registry/native-select/native-select.tsx +113 -0
- package/registry/navigation-menu/item.json +4 -0
- package/registry/navigation-menu/navigation-menu.tsx +362 -0
- package/registry/pagination/item.json +4 -0
- package/registry/pagination/pagination.tsx +460 -0
- package/registry/password-input/item.json +5 -0
- package/registry/password-input/password-input.tsx +494 -0
- package/registry/popover/item.json +4 -0
- package/registry/popover/popover.tsx +284 -0
- package/registry/progress/item.json +4 -0
- package/registry/progress/progress.tsx +190 -0
- package/registry/questionnaire/item.json +4 -0
- package/registry/questionnaire/questionnaire.tsx +569 -0
- package/registry/radio-group/item.json +4 -0
- package/registry/radio-group/radio-group.tsx +101 -0
- package/registry/resizable/item.json +4 -0
- package/registry/resizable/resizable.tsx +455 -0
- package/registry/scroll-area/item.json +4 -0
- package/registry/scroll-area/scroll-area.tsx +149 -0
- package/registry/select/item.json +4 -0
- package/registry/select/select.tsx +571 -0
- package/registry/separator/item.json +4 -0
- package/registry/separator/separator.tsx +52 -0
- package/registry/sheet/item.json +4 -0
- package/registry/sheet/sheet.tsx +476 -0
- package/registry/sidebar/item.json +4 -0
- package/registry/sidebar/sidebar.tsx +1870 -0
- package/registry/skeleton/item.json +4 -0
- package/registry/skeleton/skeleton.tsx +47 -0
- package/registry/slider/item.json +4 -0
- package/registry/slider/slider.tsx +149 -0
- package/registry/spinner/item.json +4 -0
- package/registry/spinner/spinner.tsx +51 -0
- package/registry/switch/item.json +4 -0
- package/registry/switch/switch.tsx +93 -0
- package/registry/table/item.json +4 -0
- package/registry/table/table.tsx +373 -0
- package/registry/tabs/item.json +4 -0
- package/registry/tabs/tabs.tsx +209 -0
- package/registry/textarea/item.json +4 -0
- package/registry/textarea/textarea.tsx +51 -0
- package/registry/toast/item.json +4 -0
- package/registry/toast/toast.tsx +949 -0
- package/registry/toggle/item.json +4 -0
- package/registry/toggle/toggle.tsx +110 -0
- package/registry/toggle-group/item.json +4 -0
- package/registry/toggle-group/toggle-group.tsx +189 -0
- package/registry/tooltip/item.json +4 -0
- package/registry/tooltip/tooltip.tsx +169 -0
- package/registry/trace/item.json +4 -0
- package/registry/trace/trace.tsx +356 -0
- package/registry/ui-strings/item.json +5 -0
- package/registry/ui-strings/ui-strings.tsx +454 -0
- package/registry/use-highlighted-code/item.json +5 -0
- package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
- package/registry/use-mobile/item.json +5 -0
- package/registry/use-mobile/use-mobile.ts +46 -0
- package/registry/utils/item.json +5 -0
- package/registry/utils/utils.ts +38 -0
- package/registry.json +3274 -0
- package/styles/animations.css +321 -0
- package/styles/theme-core.css +55 -0
- package/styles/theme.css +12 -0
- package/styles/themes/gray.css +69 -0
- package/styles/themes/neutral.css +69 -0
- package/styles/themes/slate.css +69 -0
- package/styles/themes/stone.css +69 -0
- package/styles/themes/zinc.css +69 -0
- package/styles/typeset.css +251 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region registry/utils/utils.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Anything a ShadowJS `class` attribute accepts: strings, numbers, nested (readonly) arrays and
|
|
4
|
+
* `{ name: condition }` objects. Falsy values are skipped.
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*/
|
|
7
|
+
export type ClassValue = string | number | bigint | boolean | null | undefined | readonly ClassValue[] | {
|
|
8
|
+
readonly [name: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Joins class names (strings, arrays, `{ name: condition }` objects) and resolves Tailwind
|
|
12
|
+
* conflicts, so the last utility wins: `cn("px-2", props.class)` lets the caller override `px-2`.
|
|
13
|
+
*
|
|
14
|
+
* @param inputs - Any mix of class values; falsy values are skipped.
|
|
15
|
+
* @returns One class string with conflicting Tailwind utilities removed.
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { cn } from "@/lib/utils";
|
|
19
|
+
*
|
|
20
|
+
* const active = true;
|
|
21
|
+
* export const classes = cn("px-2 py-1", active && "bg-primary", "px-4");
|
|
22
|
+
* // "py-1 bg-primary px-4"
|
|
23
|
+
* ```
|
|
24
|
+
* @since 1.0.0
|
|
25
|
+
* @see {@link ClassValue}
|
|
26
|
+
*/
|
|
27
|
+
export declare function cn(...inputs: ClassValue[]): string;
|
|
28
|
+
//#endregion
|
|
29
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { clsx } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
//#region registry/utils/utils.ts
|
|
4
|
+
/**
|
|
5
|
+
* Joins class names (strings, arrays, `{ name: condition }` objects) and resolves Tailwind
|
|
6
|
+
* conflicts, so the last utility wins: `cn("px-2", props.class)` lets the caller override `px-2`.
|
|
7
|
+
*
|
|
8
|
+
* @param inputs - Any mix of class values; falsy values are skipped.
|
|
9
|
+
* @returns One class string with conflicting Tailwind utilities removed.
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { cn } from "@/lib/utils";
|
|
13
|
+
*
|
|
14
|
+
* const active = true;
|
|
15
|
+
* export const classes = cn("px-2 py-1", active && "bg-primary", "px-4");
|
|
16
|
+
* // "py-1 bg-primary px-4"
|
|
17
|
+
* ```
|
|
18
|
+
* @since 1.0.0
|
|
19
|
+
* @see {@link ClassValue}
|
|
20
|
+
*/
|
|
21
|
+
function cn(...inputs) {
|
|
22
|
+
return twMerge(clsx(...inputs));
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { cn };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","names":[],"sources":["../../../../registry/utils/utils.ts"],"sourcesContent":["import { clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Anything a ShadowJS `class` attribute accepts: strings, numbers, nested (readonly) arrays and\n * `{ name: condition }` objects. Falsy values are skipped.\n * @since 1.0.0\n */\nexport type ClassValue =\n | string\n | number\n | bigint\n | boolean\n | null\n | undefined\n | readonly ClassValue[]\n | { readonly [name: string]: unknown };\n\n/**\n * Joins class names (strings, arrays, `{ name: condition }` objects) and resolves Tailwind\n * conflicts, so the last utility wins: `cn(\"px-2\", props.class)` lets the caller override `px-2`.\n *\n * @param inputs - Any mix of class values; falsy values are skipped.\n * @returns One class string with conflicting Tailwind utilities removed.\n * @example\n * ```ts\n * import { cn } from \"@/lib/utils\";\n *\n * const active = true;\n * export const classes = cn(\"px-2 py-1\", active && \"bg-primary\", \"px-4\");\n * // \"py-1 bg-primary px-4\"\n * ```\n * @since 1.0.0\n * @see {@link ClassValue}\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(...(inputs as Parameters<typeof clsx>)));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,GAAG,GAAG,QAA8B;CAClD,OAAO,QAAQ,KAAK,GAAI,MAAkC,CAAC;AAC7D"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
//#region src/theme/theme-logic.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Pure theme logic, shared by the provider and the no-flash script so both agree on what
|
|
4
|
+
* "system", "light" and "dark" mean. Nothing here touches `window` unless it is handed an object.
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The theme a user picks: `"light"`, `"dark"`, or `"system"` to follow the operating system.
|
|
9
|
+
* @since 1.0.0
|
|
10
|
+
*/
|
|
11
|
+
export type Theme = "light" | "dark" | "system";
|
|
12
|
+
/**
|
|
13
|
+
* The theme that is actually shown: `"light"` or `"dark"`.
|
|
14
|
+
* @since 1.0.0
|
|
15
|
+
*/
|
|
16
|
+
export type ResolvedTheme = "light" | "dark";
|
|
17
|
+
/**
|
|
18
|
+
* Where the resolved theme is written on the root element. `"class"` (the default) sets a `light`
|
|
19
|
+
* or `dark` class, which the generated stylesheet matches for its dark styles (the `dark` variant of
|
|
20
|
+
* Tailwind is `.dark`). A `data-*` name such as `"data-theme"` sets that attribute to `"light"` or
|
|
21
|
+
* `"dark"` instead; the stylesheet then has to match that attribute as well.
|
|
22
|
+
* @since 1.0.0
|
|
23
|
+
*/
|
|
24
|
+
export type ThemeAttribute = "class" | `data-${string}`;
|
|
25
|
+
/**
|
|
26
|
+
* Options shared by `ThemeProvider` and `themeScript`. Pass the same values to both, or the page can
|
|
27
|
+
* show one theme before the provider switches to another.
|
|
28
|
+
* @since 1.0.0
|
|
29
|
+
*/
|
|
30
|
+
export interface ThemeOptions {
|
|
31
|
+
/**
|
|
32
|
+
* `localStorage` key that persists the choice. The stored value is `"light"`, `"dark"` or
|
|
33
|
+
* `"system"`; a missing or invalid value falls back to `defaultTheme`.
|
|
34
|
+
* @default "shadow-ui-theme"
|
|
35
|
+
*/
|
|
36
|
+
readonly storageKey?: string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Theme used before the user chooses one, and when the stored value is missing or invalid.
|
|
39
|
+
* @default "system"
|
|
40
|
+
*/
|
|
41
|
+
readonly defaultTheme?: Theme | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Where the resolved theme is written: `"class"` for a `light` or `dark` class on `<html>`, or a
|
|
44
|
+
* `data-*` attribute name such as `"data-theme"` (see {@link ThemeAttribute}).
|
|
45
|
+
* @default "class"
|
|
46
|
+
*/
|
|
47
|
+
readonly attribute?: ThemeAttribute | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Follow `prefers-color-scheme` while the theme is `"system"`. When `false`, `"system"` shows light.
|
|
50
|
+
* @default true
|
|
51
|
+
*/
|
|
52
|
+
readonly enableSystem?: boolean | undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The `localStorage` key that holds the theme choice when `storageKey` is not given.
|
|
56
|
+
* @example
|
|
57
|
+
* import { DEFAULT_STORAGE_KEY } from "@shadow-js/ui";
|
|
58
|
+
*
|
|
59
|
+
* window.localStorage.getItem(DEFAULT_STORAGE_KEY);
|
|
60
|
+
* @since 1.0.0
|
|
61
|
+
*/
|
|
62
|
+
export declare const DEFAULT_STORAGE_KEY = "shadow-ui-theme";
|
|
63
|
+
/**
|
|
64
|
+
* Narrows an unknown value to a {@link Theme}. Use it on anything read from storage or from an event
|
|
65
|
+
* before trusting it.
|
|
66
|
+
*
|
|
67
|
+
* @param value Anything, such as a value read from storage.
|
|
68
|
+
* @returns `true` for `"light"`, `"dark"` and `"system"`, and `false` for everything else.
|
|
69
|
+
* @example
|
|
70
|
+
* import { DEFAULT_STORAGE_KEY, isTheme } from "@shadow-js/ui";
|
|
71
|
+
*
|
|
72
|
+
* isTheme(window.localStorage.getItem(DEFAULT_STORAGE_KEY)); // false when nothing is stored
|
|
73
|
+
* @since 1.0.0
|
|
74
|
+
*/
|
|
75
|
+
export declare function isTheme(value: unknown): value is Theme;
|
|
76
|
+
/**
|
|
77
|
+
* Turns a theme choice into the theme to show. Only `"system"` depends on the operating system.
|
|
78
|
+
*
|
|
79
|
+
* @param theme The user's choice.
|
|
80
|
+
* @param systemPrefersDark Whether the operating system prefers a dark scheme.
|
|
81
|
+
* @param enableSystem When `false`, `"system"` resolves to light. Default `true`.
|
|
82
|
+
* @returns `"light"` or `"dark"`.
|
|
83
|
+
* @example
|
|
84
|
+
* import { resolveTheme } from "@shadow-js/ui";
|
|
85
|
+
*
|
|
86
|
+
* resolveTheme("system", true); // "dark"
|
|
87
|
+
* @since 1.0.0
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveTheme(theme: Theme, systemPrefersDark: boolean, enableSystem?: boolean): ResolvedTheme;
|
|
90
|
+
/**
|
|
91
|
+
* Writes the resolved theme to the root element: sets the `light` or `dark` class (removing the
|
|
92
|
+
* other) or the `data-*` attribute, and sets `color-scheme` so form controls and scrollbars follow.
|
|
93
|
+
*
|
|
94
|
+
* @param root The element to write to, usually `document.documentElement`.
|
|
95
|
+
* @param resolved The theme to show.
|
|
96
|
+
* @param attribute Where to write it: `"class"` (the default) or a `data-*` attribute name.
|
|
97
|
+
* @example
|
|
98
|
+
* import { applyTheme } from "@shadow-js/ui";
|
|
99
|
+
*
|
|
100
|
+
* applyTheme(document.documentElement, "dark");
|
|
101
|
+
* @since 1.0.0
|
|
102
|
+
*/
|
|
103
|
+
export declare function applyTheme(root: HTMLElement, resolved: ResolvedTheme, attribute?: ThemeAttribute): void;
|
|
104
|
+
/**
|
|
105
|
+
* Reads the stored theme. A missing or invalid value gives `undefined`. A storage the browser
|
|
106
|
+
* blocks (private windows, disabled site data) or has run out of room for also gives `undefined`:
|
|
107
|
+
* those raise a `SecurityError`, `NotAllowedError` or `QuotaExceededError` `DOMException`.
|
|
108
|
+
*
|
|
109
|
+
* @param storage The storage to read, usually `window.localStorage`.
|
|
110
|
+
* @param key The storage key, such as {@link DEFAULT_STORAGE_KEY}.
|
|
111
|
+
* @returns The stored theme, or `undefined` when it is missing, invalid or unavailable.
|
|
112
|
+
* @throws The original error when it is not one of the storage errors above, because that is a bug.
|
|
113
|
+
* @example
|
|
114
|
+
* import { DEFAULT_STORAGE_KEY, readStoredTheme } from "@shadow-js/ui";
|
|
115
|
+
*
|
|
116
|
+
* readStoredTheme(window.localStorage, DEFAULT_STORAGE_KEY);
|
|
117
|
+
* @since 1.0.0
|
|
118
|
+
*/
|
|
119
|
+
export declare function readStoredTheme(storage: Storage, key: string): Theme | undefined;
|
|
120
|
+
/**
|
|
121
|
+
* Stores the theme under the key. A storage that is blocked or full does not throw: the function
|
|
122
|
+
* returns `false` instead.
|
|
123
|
+
*
|
|
124
|
+
* @param storage The storage to write, usually `window.localStorage`.
|
|
125
|
+
* @param key The storage key, such as {@link DEFAULT_STORAGE_KEY}.
|
|
126
|
+
* @param theme The theme to store.
|
|
127
|
+
* @returns `true` when the value was written, `false` when the storage refused it.
|
|
128
|
+
* @throws The original error when it is not one of the storage errors listed in {@link readStoredTheme}.
|
|
129
|
+
* @example
|
|
130
|
+
* import { DEFAULT_STORAGE_KEY, writeStoredTheme } from "@shadow-js/ui";
|
|
131
|
+
*
|
|
132
|
+
* writeStoredTheme(window.localStorage, DEFAULT_STORAGE_KEY, "dark");
|
|
133
|
+
* @since 1.0.0
|
|
134
|
+
*/
|
|
135
|
+
export declare function writeStoredTheme(storage: Storage, key: string, theme: Theme): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* The inline script that applies the stored theme before the first paint, so a dark-mode user does
|
|
138
|
+
* not see a flash of the light theme. Put the returned text in a `<script>` in the document
|
|
139
|
+
* `<head>`: a `<script>` in `index.html`, or the `document.head` option of Dusk's `createHandler`.
|
|
140
|
+
* An inline script in the head runs before the body is rendered. Pass the same options to
|
|
141
|
+
* `ThemeProvider`.
|
|
142
|
+
*
|
|
143
|
+
* The script is plain ES5 with no dependencies. It reads the stored theme, falls back to
|
|
144
|
+
* `defaultTheme` when the value is missing or invalid or when storage is blocked, and writes the same
|
|
145
|
+
* class or attribute and `color-scheme` as {@link applyTheme}. The function returns text only, so you
|
|
146
|
+
* place the tag yourself. {@link themeScriptTag} returns the same script in a tag with a CSP `nonce`.
|
|
147
|
+
*
|
|
148
|
+
* @param options Must match the `ThemeProvider` options.
|
|
149
|
+
* @returns JavaScript source text, without `<script>` tags.
|
|
150
|
+
* @example
|
|
151
|
+
* import { themeScript } from "@shadow-js/ui";
|
|
152
|
+
*
|
|
153
|
+
* const html = `<script>${themeScript({ defaultTheme: "dark" })}<\/script>`;
|
|
154
|
+
* @see {@link themeScriptTag}
|
|
155
|
+
* @since 1.0.0
|
|
156
|
+
*/
|
|
157
|
+
export declare function themeScript(options?: ThemeOptions): string;
|
|
158
|
+
/**
|
|
159
|
+
* {@link themeScript} wrapped in a `<script>` element, as an HTML string, for a server that builds the
|
|
160
|
+
* document. Pass the page's CSP nonce so the inline script is allowed by a policy that uses nonces. In
|
|
161
|
+
* Dusk, the nonce of each response is `event.nonce`, available in the `document.head` function.
|
|
162
|
+
* Only base64 characters (`A-Z`, `a-z`, `0-9`, `+`, `/`, `=`, `_`, `-`) are kept from the nonce.
|
|
163
|
+
*
|
|
164
|
+
* @param options The theme options plus the CSP `nonce` of the page. Without a nonce the tag has no
|
|
165
|
+
* `nonce` attribute.
|
|
166
|
+
* @returns A `<script>` element as HTML text.
|
|
167
|
+
* @example
|
|
168
|
+
* import { createHandler, type DuskRuntimeManifest } from "@shadow-js/dusk/server";
|
|
169
|
+
* import { pages } from "@shadow-js/dusk/features/pages";
|
|
170
|
+
* import { themeScriptTag } from "@shadow-js/ui";
|
|
171
|
+
*
|
|
172
|
+
* declare const manifest: DuskRuntimeManifest;
|
|
173
|
+
*
|
|
174
|
+
* export const handler = createHandler({
|
|
175
|
+
* features: [pages()],
|
|
176
|
+
* manifest,
|
|
177
|
+
* security: { csp: {} },
|
|
178
|
+
* document: { head: (event) => themeScriptTag({ nonce: event.nonce }) },
|
|
179
|
+
* });
|
|
180
|
+
* @see {@link themeScript}
|
|
181
|
+
* @since 1.0.0
|
|
182
|
+
*/
|
|
183
|
+
export declare function themeScriptTag(options?: ThemeOptions & {
|
|
184
|
+
readonly nonce?: string | undefined;
|
|
185
|
+
}): string;
|
|
186
|
+
//#endregion
|
|
187
|
+
//# sourceMappingURL=theme-logic.d.ts.map
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
//#region src/theme/theme-logic.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `localStorage` key that holds the theme choice when `storageKey` is not given.
|
|
4
|
+
* @example
|
|
5
|
+
* import { DEFAULT_STORAGE_KEY } from "@shadow-js/ui";
|
|
6
|
+
*
|
|
7
|
+
* window.localStorage.getItem(DEFAULT_STORAGE_KEY);
|
|
8
|
+
* @since 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_STORAGE_KEY = "shadow-ui-theme";
|
|
11
|
+
/**
|
|
12
|
+
* Narrows an unknown value to a {@link Theme}. Use it on anything read from storage or from an event
|
|
13
|
+
* before trusting it.
|
|
14
|
+
*
|
|
15
|
+
* @param value Anything, such as a value read from storage.
|
|
16
|
+
* @returns `true` for `"light"`, `"dark"` and `"system"`, and `false` for everything else.
|
|
17
|
+
* @example
|
|
18
|
+
* import { DEFAULT_STORAGE_KEY, isTheme } from "@shadow-js/ui";
|
|
19
|
+
*
|
|
20
|
+
* isTheme(window.localStorage.getItem(DEFAULT_STORAGE_KEY)); // false when nothing is stored
|
|
21
|
+
* @since 1.0.0
|
|
22
|
+
*/
|
|
23
|
+
function isTheme(value) {
|
|
24
|
+
return value === "light" || value === "dark" || value === "system";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Turns a theme choice into the theme to show. Only `"system"` depends on the operating system.
|
|
28
|
+
*
|
|
29
|
+
* @param theme The user's choice.
|
|
30
|
+
* @param systemPrefersDark Whether the operating system prefers a dark scheme.
|
|
31
|
+
* @param enableSystem When `false`, `"system"` resolves to light. Default `true`.
|
|
32
|
+
* @returns `"light"` or `"dark"`.
|
|
33
|
+
* @example
|
|
34
|
+
* import { resolveTheme } from "@shadow-js/ui";
|
|
35
|
+
*
|
|
36
|
+
* resolveTheme("system", true); // "dark"
|
|
37
|
+
* @since 1.0.0
|
|
38
|
+
*/
|
|
39
|
+
function resolveTheme(theme, systemPrefersDark, enableSystem = true) {
|
|
40
|
+
if (theme === "system") return enableSystem && systemPrefersDark ? "dark" : "light";
|
|
41
|
+
return theme;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Writes the resolved theme to the root element: sets the `light` or `dark` class (removing the
|
|
45
|
+
* other) or the `data-*` attribute, and sets `color-scheme` so form controls and scrollbars follow.
|
|
46
|
+
*
|
|
47
|
+
* @param root The element to write to, usually `document.documentElement`.
|
|
48
|
+
* @param resolved The theme to show.
|
|
49
|
+
* @param attribute Where to write it: `"class"` (the default) or a `data-*` attribute name.
|
|
50
|
+
* @example
|
|
51
|
+
* import { applyTheme } from "@shadow-js/ui";
|
|
52
|
+
*
|
|
53
|
+
* applyTheme(document.documentElement, "dark");
|
|
54
|
+
* @since 1.0.0
|
|
55
|
+
*/
|
|
56
|
+
function applyTheme(root, resolved, attribute = "class") {
|
|
57
|
+
if (attribute === "class") {
|
|
58
|
+
root.classList.remove("light", "dark");
|
|
59
|
+
root.classList.add(resolved);
|
|
60
|
+
} else root.setAttribute(attribute, resolved);
|
|
61
|
+
root.style.colorScheme = resolved;
|
|
62
|
+
}
|
|
63
|
+
/** The storage error names a locked-down browser raises; anything else is a bug and propagates. */
|
|
64
|
+
function isStorageUnavailable(error) {
|
|
65
|
+
return error instanceof DOMException && (error.name === "SecurityError" || error.name === "QuotaExceededError" || error.name === "NotAllowedError");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Reads the stored theme. A missing or invalid value gives `undefined`. A storage the browser
|
|
69
|
+
* blocks (private windows, disabled site data) or has run out of room for also gives `undefined`:
|
|
70
|
+
* those raise a `SecurityError`, `NotAllowedError` or `QuotaExceededError` `DOMException`.
|
|
71
|
+
*
|
|
72
|
+
* @param storage The storage to read, usually `window.localStorage`.
|
|
73
|
+
* @param key The storage key, such as {@link DEFAULT_STORAGE_KEY}.
|
|
74
|
+
* @returns The stored theme, or `undefined` when it is missing, invalid or unavailable.
|
|
75
|
+
* @throws The original error when it is not one of the storage errors above, because that is a bug.
|
|
76
|
+
* @example
|
|
77
|
+
* import { DEFAULT_STORAGE_KEY, readStoredTheme } from "@shadow-js/ui";
|
|
78
|
+
*
|
|
79
|
+
* readStoredTheme(window.localStorage, DEFAULT_STORAGE_KEY);
|
|
80
|
+
* @since 1.0.0
|
|
81
|
+
*/
|
|
82
|
+
function readStoredTheme(storage, key) {
|
|
83
|
+
try {
|
|
84
|
+
const value = storage.getItem(key);
|
|
85
|
+
return isTheme(value) ? value : void 0;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (isStorageUnavailable(error)) return void 0;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Stores the theme under the key. A storage that is blocked or full does not throw: the function
|
|
93
|
+
* returns `false` instead.
|
|
94
|
+
*
|
|
95
|
+
* @param storage The storage to write, usually `window.localStorage`.
|
|
96
|
+
* @param key The storage key, such as {@link DEFAULT_STORAGE_KEY}.
|
|
97
|
+
* @param theme The theme to store.
|
|
98
|
+
* @returns `true` when the value was written, `false` when the storage refused it.
|
|
99
|
+
* @throws The original error when it is not one of the storage errors listed in {@link readStoredTheme}.
|
|
100
|
+
* @example
|
|
101
|
+
* import { DEFAULT_STORAGE_KEY, writeStoredTheme } from "@shadow-js/ui";
|
|
102
|
+
*
|
|
103
|
+
* writeStoredTheme(window.localStorage, DEFAULT_STORAGE_KEY, "dark");
|
|
104
|
+
* @since 1.0.0
|
|
105
|
+
*/
|
|
106
|
+
function writeStoredTheme(storage, key, theme) {
|
|
107
|
+
try {
|
|
108
|
+
storage.setItem(key, theme);
|
|
109
|
+
return true;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (isStorageUnavailable(error)) return false;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The inline script that applies the stored theme before the first paint, so a dark-mode user does
|
|
117
|
+
* not see a flash of the light theme. Put the returned text in a `<script>` in the document
|
|
118
|
+
* `<head>`: a `<script>` in `index.html`, or the `document.head` option of Dusk's `createHandler`.
|
|
119
|
+
* An inline script in the head runs before the body is rendered. Pass the same options to
|
|
120
|
+
* `ThemeProvider`.
|
|
121
|
+
*
|
|
122
|
+
* The script is plain ES5 with no dependencies. It reads the stored theme, falls back to
|
|
123
|
+
* `defaultTheme` when the value is missing or invalid or when storage is blocked, and writes the same
|
|
124
|
+
* class or attribute and `color-scheme` as {@link applyTheme}. The function returns text only, so you
|
|
125
|
+
* place the tag yourself. {@link themeScriptTag} returns the same script in a tag with a CSP `nonce`.
|
|
126
|
+
*
|
|
127
|
+
* @param options Must match the `ThemeProvider` options.
|
|
128
|
+
* @returns JavaScript source text, without `<script>` tags.
|
|
129
|
+
* @example
|
|
130
|
+
* import { themeScript } from "@shadow-js/ui";
|
|
131
|
+
*
|
|
132
|
+
* const html = `<script>${themeScript({ defaultTheme: "dark" })}<\/script>`;
|
|
133
|
+
* @see {@link themeScriptTag}
|
|
134
|
+
* @since 1.0.0
|
|
135
|
+
*/
|
|
136
|
+
function themeScript(options = {}) {
|
|
137
|
+
const key = options.storageKey ?? "shadow-ui-theme";
|
|
138
|
+
const fallback = options.defaultTheme ?? "system";
|
|
139
|
+
const attribute = options.attribute ?? "class";
|
|
140
|
+
const system = options.enableSystem ?? true;
|
|
141
|
+
const literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
142
|
+
return [
|
|
143
|
+
"(function(){",
|
|
144
|
+
`var k=${literal(key)},f=${literal(fallback)},a=${literal(attribute)},s=${literal(system)};`,
|
|
145
|
+
"var r=document.documentElement;",
|
|
146
|
+
"function p(t){if(t==='system')return s&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';return t}",
|
|
147
|
+
"function w(m){if(a==='class'){r.classList.remove('light','dark');r.classList.add(m)}else{r.setAttribute(a,m)}r.style.colorScheme=m}",
|
|
148
|
+
"try{var t=localStorage.getItem(k);if(t!=='light'&&t!=='dark'&&t!=='system')t=f;w(p(t))}",
|
|
149
|
+
"catch(e){w(p(f))}",
|
|
150
|
+
"})();"
|
|
151
|
+
].join("");
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* {@link themeScript} wrapped in a `<script>` element, as an HTML string, for a server that builds the
|
|
155
|
+
* document. Pass the page's CSP nonce so the inline script is allowed by a policy that uses nonces. In
|
|
156
|
+
* Dusk, the nonce of each response is `event.nonce`, available in the `document.head` function.
|
|
157
|
+
* Only base64 characters (`A-Z`, `a-z`, `0-9`, `+`, `/`, `=`, `_`, `-`) are kept from the nonce.
|
|
158
|
+
*
|
|
159
|
+
* @param options The theme options plus the CSP `nonce` of the page. Without a nonce the tag has no
|
|
160
|
+
* `nonce` attribute.
|
|
161
|
+
* @returns A `<script>` element as HTML text.
|
|
162
|
+
* @example
|
|
163
|
+
* import { createHandler, type DuskRuntimeManifest } from "@shadow-js/dusk/server";
|
|
164
|
+
* import { pages } from "@shadow-js/dusk/features/pages";
|
|
165
|
+
* import { themeScriptTag } from "@shadow-js/ui";
|
|
166
|
+
*
|
|
167
|
+
* declare const manifest: DuskRuntimeManifest;
|
|
168
|
+
*
|
|
169
|
+
* export const handler = createHandler({
|
|
170
|
+
* features: [pages()],
|
|
171
|
+
* manifest,
|
|
172
|
+
* security: { csp: {} },
|
|
173
|
+
* document: { head: (event) => themeScriptTag({ nonce: event.nonce }) },
|
|
174
|
+
* });
|
|
175
|
+
* @see {@link themeScript}
|
|
176
|
+
* @since 1.0.0
|
|
177
|
+
*/
|
|
178
|
+
function themeScriptTag(options = {}) {
|
|
179
|
+
return `<script${options.nonce === void 0 ? "" : ` nonce="${options.nonce.replace(/[^A-Za-z0-9+/=_-]/g, "")}"`}>${themeScript(options)}<\/script>`;
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
export { DEFAULT_STORAGE_KEY, applyTheme, isTheme, readStoredTheme, resolveTheme, themeScript, themeScriptTag, writeStoredTheme };
|
|
183
|
+
|
|
184
|
+
//# sourceMappingURL=theme-logic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-logic.js","names":[],"sources":["../../../src/theme/theme-logic.ts"],"sourcesContent":["/**\n * Pure theme logic, shared by the provider and the no-flash script so both agree on what\n * \"system\", \"light\" and \"dark\" mean. Nothing here touches `window` unless it is handed an object.\n * @packageDocumentation\n */\n\n/**\n * The theme a user picks: `\"light\"`, `\"dark\"`, or `\"system\"` to follow the operating system.\n * @since 1.0.0\n */\nexport type Theme = \"light\" | \"dark\" | \"system\";\n\n/**\n * The theme that is actually shown: `\"light\"` or `\"dark\"`.\n * @since 1.0.0\n */\nexport type ResolvedTheme = \"light\" | \"dark\";\n\n/**\n * Where the resolved theme is written on the root element. `\"class\"` (the default) sets a `light`\n * or `dark` class, which the generated stylesheet matches for its dark styles (the `dark` variant of\n * Tailwind is `.dark`). A `data-*` name such as `\"data-theme\"` sets that attribute to `\"light\"` or\n * `\"dark\"` instead; the stylesheet then has to match that attribute as well.\n * @since 1.0.0\n */\nexport type ThemeAttribute = \"class\" | `data-${string}`;\n\n/**\n * Options shared by `ThemeProvider` and `themeScript`. Pass the same values to both, or the page can\n * show one theme before the provider switches to another.\n * @since 1.0.0\n */\nexport interface ThemeOptions {\n /**\n * `localStorage` key that persists the choice. The stored value is `\"light\"`, `\"dark\"` or\n * `\"system\"`; a missing or invalid value falls back to `defaultTheme`.\n * @default \"shadow-ui-theme\"\n */\n readonly storageKey?: string | undefined;\n /**\n * Theme used before the user chooses one, and when the stored value is missing or invalid.\n * @default \"system\"\n */\n readonly defaultTheme?: Theme | undefined;\n /**\n * Where the resolved theme is written: `\"class\"` for a `light` or `dark` class on `<html>`, or a\n * `data-*` attribute name such as `\"data-theme\"` (see {@link ThemeAttribute}).\n * @default \"class\"\n */\n readonly attribute?: ThemeAttribute | undefined;\n /**\n * Follow `prefers-color-scheme` while the theme is `\"system\"`. When `false`, `\"system\"` shows light.\n * @default true\n */\n readonly enableSystem?: boolean | undefined;\n}\n\n/**\n * The `localStorage` key that holds the theme choice when `storageKey` is not given.\n * @example\n * import { DEFAULT_STORAGE_KEY } from \"@shadow-js/ui\";\n *\n * window.localStorage.getItem(DEFAULT_STORAGE_KEY);\n * @since 1.0.0\n */\nexport const DEFAULT_STORAGE_KEY = \"shadow-ui-theme\";\n\n/**\n * Narrows an unknown value to a {@link Theme}. Use it on anything read from storage or from an event\n * before trusting it.\n *\n * @param value Anything, such as a value read from storage.\n * @returns `true` for `\"light\"`, `\"dark\"` and `\"system\"`, and `false` for everything else.\n * @example\n * import { DEFAULT_STORAGE_KEY, isTheme } from \"@shadow-js/ui\";\n *\n * isTheme(window.localStorage.getItem(DEFAULT_STORAGE_KEY)); // false when nothing is stored\n * @since 1.0.0\n */\nexport function isTheme(value: unknown): value is Theme {\n return value === \"light\" || value === \"dark\" || value === \"system\";\n}\n\n/**\n * Turns a theme choice into the theme to show. Only `\"system\"` depends on the operating system.\n *\n * @param theme The user's choice.\n * @param systemPrefersDark Whether the operating system prefers a dark scheme.\n * @param enableSystem When `false`, `\"system\"` resolves to light. Default `true`.\n * @returns `\"light\"` or `\"dark\"`.\n * @example\n * import { resolveTheme } from \"@shadow-js/ui\";\n *\n * resolveTheme(\"system\", true); // \"dark\"\n * @since 1.0.0\n */\nexport function resolveTheme(\n theme: Theme,\n systemPrefersDark: boolean,\n enableSystem = true,\n): ResolvedTheme {\n if (theme === \"system\") return enableSystem && systemPrefersDark ? \"dark\" : \"light\";\n return theme;\n}\n\n/**\n * Writes the resolved theme to the root element: sets the `light` or `dark` class (removing the\n * other) or the `data-*` attribute, and sets `color-scheme` so form controls and scrollbars follow.\n *\n * @param root The element to write to, usually `document.documentElement`.\n * @param resolved The theme to show.\n * @param attribute Where to write it: `\"class\"` (the default) or a `data-*` attribute name.\n * @example\n * import { applyTheme } from \"@shadow-js/ui\";\n *\n * applyTheme(document.documentElement, \"dark\");\n * @since 1.0.0\n */\nexport function applyTheme(\n root: HTMLElement,\n resolved: ResolvedTheme,\n attribute: ThemeAttribute = \"class\",\n): void {\n if (attribute === \"class\") {\n root.classList.remove(\"light\", \"dark\");\n root.classList.add(resolved);\n } else {\n root.setAttribute(attribute, resolved);\n }\n root.style.colorScheme = resolved;\n}\n\n/** The storage error names a locked-down browser raises; anything else is a bug and propagates. */\nfunction isStorageUnavailable(error: unknown): boolean {\n return (\n error instanceof DOMException &&\n (error.name === \"SecurityError\" ||\n error.name === \"QuotaExceededError\" ||\n error.name === \"NotAllowedError\")\n );\n}\n\n/**\n * Reads the stored theme. A missing or invalid value gives `undefined`. A storage the browser\n * blocks (private windows, disabled site data) or has run out of room for also gives `undefined`:\n * those raise a `SecurityError`, `NotAllowedError` or `QuotaExceededError` `DOMException`.\n *\n * @param storage The storage to read, usually `window.localStorage`.\n * @param key The storage key, such as {@link DEFAULT_STORAGE_KEY}.\n * @returns The stored theme, or `undefined` when it is missing, invalid or unavailable.\n * @throws The original error when it is not one of the storage errors above, because that is a bug.\n * @example\n * import { DEFAULT_STORAGE_KEY, readStoredTheme } from \"@shadow-js/ui\";\n *\n * readStoredTheme(window.localStorage, DEFAULT_STORAGE_KEY);\n * @since 1.0.0\n */\nexport function readStoredTheme(storage: Storage, key: string): Theme | undefined {\n try {\n const value = storage.getItem(key);\n return isTheme(value) ? value : undefined;\n } catch (error) {\n if (isStorageUnavailable(error)) return undefined;\n throw error;\n }\n}\n\n/**\n * Stores the theme under the key. A storage that is blocked or full does not throw: the function\n * returns `false` instead.\n *\n * @param storage The storage to write, usually `window.localStorage`.\n * @param key The storage key, such as {@link DEFAULT_STORAGE_KEY}.\n * @param theme The theme to store.\n * @returns `true` when the value was written, `false` when the storage refused it.\n * @throws The original error when it is not one of the storage errors listed in {@link readStoredTheme}.\n * @example\n * import { DEFAULT_STORAGE_KEY, writeStoredTheme } from \"@shadow-js/ui\";\n *\n * writeStoredTheme(window.localStorage, DEFAULT_STORAGE_KEY, \"dark\");\n * @since 1.0.0\n */\nexport function writeStoredTheme(storage: Storage, key: string, theme: Theme): boolean {\n try {\n storage.setItem(key, theme);\n return true;\n } catch (error) {\n if (isStorageUnavailable(error)) return false;\n throw error;\n }\n}\n\n/**\n * The inline script that applies the stored theme before the first paint, so a dark-mode user does\n * not see a flash of the light theme. Put the returned text in a `<script>` in the document\n * `<head>`: a `<script>` in `index.html`, or the `document.head` option of Dusk's `createHandler`.\n * An inline script in the head runs before the body is rendered. Pass the same options to\n * `ThemeProvider`.\n *\n * The script is plain ES5 with no dependencies. It reads the stored theme, falls back to\n * `defaultTheme` when the value is missing or invalid or when storage is blocked, and writes the same\n * class or attribute and `color-scheme` as {@link applyTheme}. The function returns text only, so you\n * place the tag yourself. {@link themeScriptTag} returns the same script in a tag with a CSP `nonce`.\n *\n * @param options Must match the `ThemeProvider` options.\n * @returns JavaScript source text, without `<script>` tags.\n * @example\n * import { themeScript } from \"@shadow-js/ui\";\n *\n * const html = `<script>${themeScript({ defaultTheme: \"dark\" })}</script>`;\n * @see {@link themeScriptTag}\n * @since 1.0.0\n */\nexport function themeScript(options: ThemeOptions = {}): string {\n const key = options.storageKey ?? DEFAULT_STORAGE_KEY;\n const fallback = options.defaultTheme ?? \"system\";\n const attribute = options.attribute ?? \"class\";\n const system = options.enableSystem ?? true;\n // `<` is escaped so a stored value can never close the surrounding <script> element.\n const literal = (value: unknown): string => JSON.stringify(value).replace(/</g, \"\\\\u003c\");\n return [\n \"(function(){\",\n `var k=${literal(key)},f=${literal(fallback)},a=${literal(attribute)},s=${literal(system)};`,\n \"var r=document.documentElement;\",\n \"function p(t){if(t==='system')return s&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';return t}\",\n \"function w(m){if(a==='class'){r.classList.remove('light','dark');r.classList.add(m)}else{r.setAttribute(a,m)}r.style.colorScheme=m}\",\n \"try{var t=localStorage.getItem(k);if(t!=='light'&&t!=='dark'&&t!=='system')t=f;w(p(t))}\",\n \"catch(e){w(p(f))}\",\n \"})();\",\n ].join(\"\");\n}\n\n/**\n * {@link themeScript} wrapped in a `<script>` element, as an HTML string, for a server that builds the\n * document. Pass the page's CSP nonce so the inline script is allowed by a policy that uses nonces. In\n * Dusk, the nonce of each response is `event.nonce`, available in the `document.head` function.\n * Only base64 characters (`A-Z`, `a-z`, `0-9`, `+`, `/`, `=`, `_`, `-`) are kept from the nonce.\n *\n * @param options The theme options plus the CSP `nonce` of the page. Without a nonce the tag has no\n * `nonce` attribute.\n * @returns A `<script>` element as HTML text.\n * @example\n * import { createHandler, type DuskRuntimeManifest } from \"@shadow-js/dusk/server\";\n * import { pages } from \"@shadow-js/dusk/features/pages\";\n * import { themeScriptTag } from \"@shadow-js/ui\";\n *\n * declare const manifest: DuskRuntimeManifest;\n *\n * export const handler = createHandler({\n * features: [pages()],\n * manifest,\n * security: { csp: {} },\n * document: { head: (event) => themeScriptTag({ nonce: event.nonce }) },\n * });\n * @see {@link themeScript}\n * @since 1.0.0\n */\nexport function themeScriptTag(\n options: ThemeOptions & { readonly nonce?: string | undefined } = {},\n): string {\n const nonce =\n options.nonce === undefined\n ? \"\"\n : ` nonce=\"${options.nonce.replace(/[^A-Za-z0-9+/=_-]/g, \"\")}\"`;\n return `<script${nonce}>${themeScript(options)}</script>`;\n}\n"],"mappings":";;;;;;;;;AAiEA,MAAa,sBAAsB;;;;;;;;;;;;;AAcnC,SAAgB,QAAQ,OAAgC;CACtD,OAAO,UAAU,WAAW,UAAU,UAAU,UAAU;AAC5D;;;;;;;;;;;;;;AAeA,SAAgB,aACd,OACA,mBACA,eAAe,MACA;CACf,IAAI,UAAU,UAAU,OAAO,gBAAgB,oBAAoB,SAAS;CAC5E,OAAO;AACT;;;;;;;;;;;;;;AAeA,SAAgB,WACd,MACA,UACA,YAA4B,SACtB;CACN,IAAI,cAAc,SAAS;EACzB,KAAK,UAAU,OAAO,SAAS,MAAM;EACrC,KAAK,UAAU,IAAI,QAAQ;CAC7B,OACE,KAAK,aAAa,WAAW,QAAQ;CAEvC,KAAK,MAAM,cAAc;AAC3B;;AAGA,SAAS,qBAAqB,OAAyB;CACrD,OACE,iBAAiB,iBAChB,MAAM,SAAS,mBACd,MAAM,SAAS,wBACf,MAAM,SAAS;AAErB;;;;;;;;;;;;;;;;AAiBA,SAAgB,gBAAgB,SAAkB,KAAgC;CAChF,IAAI;EACF,MAAM,QAAQ,QAAQ,QAAQ,GAAG;EACjC,OAAO,QAAQ,KAAK,IAAI,QAAQ,KAAA;CAClC,SAAS,OAAO;EACd,IAAI,qBAAqB,KAAK,GAAG,OAAO,KAAA;EACxC,MAAM;CACR;AACF;;;;;;;;;;;;;;;;AAiBA,SAAgB,iBAAiB,SAAkB,KAAa,OAAuB;CACrF,IAAI;EACF,QAAQ,QAAQ,KAAK,KAAK;EAC1B,OAAO;CACT,SAAS,OAAO;EACd,IAAI,qBAAqB,KAAK,GAAG,OAAO;EACxC,MAAM;CACR;AACF;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,YAAY,UAAwB,CAAC,GAAW;CAC9D,MAAM,MAAM,QAAQ,cAAA;CACpB,MAAM,WAAW,QAAQ,gBAAgB;CACzC,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,SAAS,QAAQ,gBAAgB;CAEvC,MAAM,WAAW,UAA2B,KAAK,UAAU,KAAK,CAAC,CAAC,QAAQ,MAAM,SAAS;CACzF,OAAO;EACL;EACA,SAAS,QAAQ,GAAG,EAAE,KAAK,QAAQ,QAAQ,EAAE,KAAK,QAAQ,SAAS,EAAE,KAAK,QAAQ,MAAM,EAAE;EAC1F;EACA;EACA;EACA;EACA;EACA;CACF,CAAC,CAAC,KAAK,EAAE;AACX;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,eACd,UAAkE,CAAC,GAC3D;CAKR,OAAO,UAHL,QAAQ,UAAU,KAAA,IACd,KACA,WAAW,QAAQ,MAAM,QAAQ,sBAAsB,EAAE,EAAE,GAC1C,GAAG,YAAY,OAAO,EAAE;AACjD"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ResolvedTheme, Theme, ThemeOptions } from "./theme-logic.js";
|
|
2
|
+
import { Accessor } from "@shadow-js/core";
|
|
3
|
+
import { JsxChild } from "@shadow-js/web/jsx-runtime";
|
|
4
|
+
//#region src/theme/theme-provider.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* What {@link useTheme} returns. Every value is an accessor: call it to read.
|
|
7
|
+
* @since 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
export interface ThemeContextValue {
|
|
10
|
+
/** The user's choice: `"light"`, `"dark"` or `"system"`. Call it to read the value. */
|
|
11
|
+
readonly theme: Accessor<Theme>;
|
|
12
|
+
/**
|
|
13
|
+
* The theme that is shown: `"light"` or `"dark"`. `undefined` on the server and until the provider
|
|
14
|
+
* has mounted, so markup that depends on it hydrates without a mismatch.
|
|
15
|
+
*/
|
|
16
|
+
readonly resolvedTheme: Accessor<ResolvedTheme | undefined>;
|
|
17
|
+
/**
|
|
18
|
+
* The operating system preference: `"light"` or `"dark"`. `undefined` on the server and until the
|
|
19
|
+
* provider has mounted.
|
|
20
|
+
*/
|
|
21
|
+
readonly systemTheme: Accessor<ResolvedTheme | undefined>;
|
|
22
|
+
/**
|
|
23
|
+
* Chooses a theme, or derives one from the previous choice, and stores it under the provider's
|
|
24
|
+
* `storageKey`. Throws a `TypeError` for a value that is not `"light"`, `"dark"` or `"system"`.
|
|
25
|
+
*/
|
|
26
|
+
readonly setTheme: (theme: Theme | ((previous: Theme) => Theme)) => void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Props of {@link ThemeProvider}: the {@link ThemeOptions} plus a transition switch and the children.
|
|
30
|
+
* @since 1.0.0
|
|
31
|
+
*/
|
|
32
|
+
export interface ThemeProviderProps extends ThemeOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Stops CSS transitions for one frame while the theme changes, so colors flip at once instead
|
|
35
|
+
* of fading piecemeal.
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
readonly disableTransitionOnChange?: boolean | undefined;
|
|
39
|
+
/** The app to render inside the theme context. */
|
|
40
|
+
readonly children?: JsxChild | undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Provides the color theme to the app: `"light"`, `"dark"` or `"system"`. The choice is persisted in
|
|
44
|
+
* `localStorage`, applied to `<html>` as a `light` or `dark` class (or a `data-*` attribute), and kept
|
|
45
|
+
* in sync across tabs and with the operating system. The provider renders no element of its own, and
|
|
46
|
+
* nothing touches the DOM until it has mounted, so it is safe to render on the server. Pair it with
|
|
47
|
+
* {@link themeScript} in the document head so the first paint already has the right theme.
|
|
48
|
+
*
|
|
49
|
+
* @param props The {@link ThemeProviderProps}: theme options (`storageKey`, `defaultTheme`,
|
|
50
|
+
* `attribute`, `enableSystem`), `disableTransitionOnChange` and `children`.
|
|
51
|
+
* @returns The children inside the theme context.
|
|
52
|
+
* @example
|
|
53
|
+
* import { ThemeProvider } from "@shadow-js/ui";
|
|
54
|
+
*
|
|
55
|
+
* function App() {
|
|
56
|
+
* return <p>Hello</p>;
|
|
57
|
+
* }
|
|
58
|
+
*
|
|
59
|
+
* export const root = (
|
|
60
|
+
* <ThemeProvider defaultTheme="system">
|
|
61
|
+
* <App />
|
|
62
|
+
* </ThemeProvider>
|
|
63
|
+
* );
|
|
64
|
+
* @see {@link useTheme}
|
|
65
|
+
* @since 1.0.0
|
|
66
|
+
*/
|
|
67
|
+
export declare function ThemeProvider(props: ThemeProviderProps): JsxChild;
|
|
68
|
+
/**
|
|
69
|
+
* Reads the theme and its setter. Call it inside a component rendered under {@link ThemeProvider}.
|
|
70
|
+
*
|
|
71
|
+
* @returns The {@link ThemeContextValue}: accessors for the chosen, the resolved and the system
|
|
72
|
+
* theme, and `setTheme`.
|
|
73
|
+
* @throws Error when no `ThemeProvider` is above the caller.
|
|
74
|
+
* @example
|
|
75
|
+
* import { useTheme } from "@shadow-js/ui";
|
|
76
|
+
*
|
|
77
|
+
* export function ThemeToggle() {
|
|
78
|
+
* const { resolvedTheme, setTheme } = useTheme();
|
|
79
|
+
* return (
|
|
80
|
+
* <button onClick={() => setTheme(resolvedTheme() === "dark" ? "light" : "dark")}>
|
|
81
|
+
* Toggle theme
|
|
82
|
+
* </button>
|
|
83
|
+
* );
|
|
84
|
+
* }
|
|
85
|
+
* @see {@link ThemeProvider}
|
|
86
|
+
* @since 1.0.0
|
|
87
|
+
*/
|
|
88
|
+
export declare function useTheme(): ThemeContextValue;
|
|
89
|
+
//#endregion
|
|
90
|
+
//# sourceMappingURL=theme-provider.d.ts.map
|