@shadow-js/ui 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +598 -2
  4. package/dist/cli.mjs +2281 -0
  5. package/dist/dev/cli/schema.d.ts +71 -0
  6. package/dist/dev/index.d.ts +6 -0
  7. package/dist/dev/index.js +5 -0
  8. package/dist/dev/locales/ar.d.ts +139 -0
  9. package/dist/dev/locales/ar.js +127 -0
  10. package/dist/dev/locales/ar.js.map +1 -0
  11. package/dist/dev/locales/en.d.ts +139 -0
  12. package/dist/dev/locales/en.js +127 -0
  13. package/dist/dev/locales/en.js.map +1 -0
  14. package/dist/dev/registry/utils/utils.d.ts +27 -0
  15. package/dist/dev/registry/utils/utils.js +26 -0
  16. package/dist/dev/registry/utils/utils.js.map +1 -0
  17. package/dist/dev/theme/theme-logic.d.ts +185 -0
  18. package/dist/dev/theme/theme-logic.js +183 -0
  19. package/dist/dev/theme/theme-logic.js.map +1 -0
  20. package/dist/dev/theme/theme-provider.d.ts +88 -0
  21. package/dist/dev/theme/theme-provider.js +149 -0
  22. package/dist/dev/theme/theme-provider.js.map +1 -0
  23. package/dist/prod/index.js +1 -0
  24. package/dist/prod/locales/ar.js +2 -0
  25. package/dist/prod/locales/ar.js.map +1 -0
  26. package/dist/prod/locales/en.js +2 -0
  27. package/dist/prod/locales/en.js.map +1 -0
  28. package/dist/prod/registry/utils/utils.js +2 -0
  29. package/dist/prod/registry/utils/utils.js.map +1 -0
  30. package/dist/prod/theme/theme-logic.js +2 -0
  31. package/dist/prod/theme/theme-logic.js.map +1 -0
  32. package/dist/prod/theme/theme-provider.js +2 -0
  33. package/dist/prod/theme/theme-provider.js.map +1 -0
  34. package/package.json +102 -4
  35. package/registry/accordion/accordion.tsx +210 -0
  36. package/registry/accordion/item.json +4 -0
  37. package/registry/alert/alert.tsx +200 -0
  38. package/registry/alert/item.json +4 -0
  39. package/registry/alert-dialog/alert-dialog.tsx +530 -0
  40. package/registry/alert-dialog/item.json +4 -0
  41. package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
  42. package/registry/aspect-ratio/item.json +4 -0
  43. package/registry/attachment/attachment.tsx +459 -0
  44. package/registry/attachment/item.json +4 -0
  45. package/registry/avatar/avatar.tsx +385 -0
  46. package/registry/avatar/item.json +4 -0
  47. package/registry/badge/badge.tsx +108 -0
  48. package/registry/badge/item.json +4 -0
  49. package/registry/breadcrumb/breadcrumb.tsx +354 -0
  50. package/registry/breadcrumb/item.json +4 -0
  51. package/registry/bubble/bubble.tsx +260 -0
  52. package/registry/bubble/item.json +4 -0
  53. package/registry/button/button.tsx +125 -0
  54. package/registry/button/item.json +4 -0
  55. package/registry/button-group/button-group.tsx +189 -0
  56. package/registry/button-group/item.json +4 -0
  57. package/registry/calendar/calendar.tsx +348 -0
  58. package/registry/calendar/item.json +4 -0
  59. package/registry/card/card.tsx +311 -0
  60. package/registry/card/item.json +4 -0
  61. package/registry/carousel/carousel.tsx +851 -0
  62. package/registry/carousel/item.json +4 -0
  63. package/registry/chart/chart.tsx +1956 -0
  64. package/registry/chart/item.json +4 -0
  65. package/registry/chart-scale/chart-scale.ts +670 -0
  66. package/registry/chart-scale/item.json +5 -0
  67. package/registry/checkbox/checkbox.tsx +79 -0
  68. package/registry/checkbox/item.json +4 -0
  69. package/registry/code-block/code-block.tsx +470 -0
  70. package/registry/code-block/item.json +4 -0
  71. package/registry/collapsible/collapsible.tsx +117 -0
  72. package/registry/collapsible/item.json +4 -0
  73. package/registry/combobox/combobox.tsx +977 -0
  74. package/registry/combobox/item.json +4 -0
  75. package/registry/command/command.tsx +757 -0
  76. package/registry/command/item.json +4 -0
  77. package/registry/context-menu/context-menu.tsx +697 -0
  78. package/registry/context-menu/item.json +4 -0
  79. package/registry/data-table/data-table.tsx +437 -0
  80. package/registry/data-table/item.json +4 -0
  81. package/registry/date-picker/date-picker.tsx +349 -0
  82. package/registry/date-picker/item.json +4 -0
  83. package/registry/dialog/dialog.tsx +412 -0
  84. package/registry/dialog/item.json +4 -0
  85. package/registry/direction/direction.tsx +88 -0
  86. package/registry/direction/item.json +4 -0
  87. package/registry/drawer/drawer.tsx +787 -0
  88. package/registry/drawer/item.json +4 -0
  89. package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
  90. package/registry/dropdown-menu/item.json +4 -0
  91. package/registry/empty/empty.tsx +240 -0
  92. package/registry/empty/item.json +4 -0
  93. package/registry/field/field.tsx +624 -0
  94. package/registry/field/item.json +4 -0
  95. package/registry/form/form.tsx +1112 -0
  96. package/registry/form/item.json +4 -0
  97. package/registry/hover-card/hover-card.tsx +144 -0
  98. package/registry/hover-card/item.json +4 -0
  99. package/registry/icons/icons.tsx +1691 -0
  100. package/registry/icons/item.json +4 -0
  101. package/registry/input/input.tsx +43 -0
  102. package/registry/input/item.json +4 -0
  103. package/registry/input-group/input-group.tsx +315 -0
  104. package/registry/input-group/item.json +4 -0
  105. package/registry/input-otp/input-otp.tsx +329 -0
  106. package/registry/input-otp/item.json +4 -0
  107. package/registry/item/item.json +4 -0
  108. package/registry/item/item.tsx +439 -0
  109. package/registry/kbd/item.json +4 -0
  110. package/registry/kbd/kbd.tsx +65 -0
  111. package/registry/label/item.json +4 -0
  112. package/registry/label/label.tsx +60 -0
  113. package/registry/marker/item.json +4 -0
  114. package/registry/marker/marker.tsx +175 -0
  115. package/registry/menubar/item.json +4 -0
  116. package/registry/menubar/menubar.tsx +694 -0
  117. package/registry/message/item.json +4 -0
  118. package/registry/message/message.tsx +280 -0
  119. package/registry/message-scroller/item.json +4 -0
  120. package/registry/message-scroller/message-scroller.tsx +262 -0
  121. package/registry/native-select/item.json +4 -0
  122. package/registry/native-select/native-select.tsx +113 -0
  123. package/registry/navigation-menu/item.json +4 -0
  124. package/registry/navigation-menu/navigation-menu.tsx +362 -0
  125. package/registry/pagination/item.json +4 -0
  126. package/registry/pagination/pagination.tsx +460 -0
  127. package/registry/password-input/item.json +5 -0
  128. package/registry/password-input/password-input.tsx +494 -0
  129. package/registry/popover/item.json +4 -0
  130. package/registry/popover/popover.tsx +284 -0
  131. package/registry/progress/item.json +4 -0
  132. package/registry/progress/progress.tsx +190 -0
  133. package/registry/questionnaire/item.json +4 -0
  134. package/registry/questionnaire/questionnaire.tsx +569 -0
  135. package/registry/radio-group/item.json +4 -0
  136. package/registry/radio-group/radio-group.tsx +101 -0
  137. package/registry/resizable/item.json +4 -0
  138. package/registry/resizable/resizable.tsx +455 -0
  139. package/registry/scroll-area/item.json +4 -0
  140. package/registry/scroll-area/scroll-area.tsx +149 -0
  141. package/registry/select/item.json +4 -0
  142. package/registry/select/select.tsx +571 -0
  143. package/registry/separator/item.json +4 -0
  144. package/registry/separator/separator.tsx +52 -0
  145. package/registry/sheet/item.json +4 -0
  146. package/registry/sheet/sheet.tsx +476 -0
  147. package/registry/sidebar/item.json +4 -0
  148. package/registry/sidebar/sidebar.tsx +1870 -0
  149. package/registry/skeleton/item.json +4 -0
  150. package/registry/skeleton/skeleton.tsx +47 -0
  151. package/registry/slider/item.json +4 -0
  152. package/registry/slider/slider.tsx +149 -0
  153. package/registry/spinner/item.json +4 -0
  154. package/registry/spinner/spinner.tsx +51 -0
  155. package/registry/switch/item.json +4 -0
  156. package/registry/switch/switch.tsx +93 -0
  157. package/registry/table/item.json +4 -0
  158. package/registry/table/table.tsx +373 -0
  159. package/registry/tabs/item.json +4 -0
  160. package/registry/tabs/tabs.tsx +209 -0
  161. package/registry/textarea/item.json +4 -0
  162. package/registry/textarea/textarea.tsx +51 -0
  163. package/registry/toast/item.json +4 -0
  164. package/registry/toast/toast.tsx +949 -0
  165. package/registry/toggle/item.json +4 -0
  166. package/registry/toggle/toggle.tsx +110 -0
  167. package/registry/toggle-group/item.json +4 -0
  168. package/registry/toggle-group/toggle-group.tsx +189 -0
  169. package/registry/tooltip/item.json +4 -0
  170. package/registry/tooltip/tooltip.tsx +169 -0
  171. package/registry/trace/item.json +4 -0
  172. package/registry/trace/trace.tsx +356 -0
  173. package/registry/ui-strings/item.json +5 -0
  174. package/registry/ui-strings/ui-strings.tsx +454 -0
  175. package/registry/use-highlighted-code/item.json +5 -0
  176. package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
  177. package/registry/use-mobile/item.json +5 -0
  178. package/registry/use-mobile/use-mobile.ts +46 -0
  179. package/registry/utils/item.json +5 -0
  180. package/registry/utils/utils.ts +38 -0
  181. package/registry.json +3274 -0
  182. package/styles/animations.css +321 -0
  183. package/styles/theme-core.css +55 -0
  184. package/styles/theme.css +12 -0
  185. package/styles/themes/gray.css +69 -0
  186. package/styles/themes/neutral.css +69 -0
  187. package/styles/themes/slate.css +69 -0
  188. package/styles/themes/stone.css +69 -0
  189. package/styles/themes/zinc.css +69 -0
  190. package/styles/typeset.css +251 -0
@@ -0,0 +1,26 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ // 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
+ export { cn };
25
+
26
+ //# 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,185 @@
1
+ /**
2
+ * Pure theme logic, shared by the provider and the no-flash script so both agree on what
3
+ * "system", "light" and "dark" mean. Nothing here touches `window` unless it is handed an object.
4
+ * @packageDocumentation
5
+ */
6
+ /**
7
+ * The theme a user picks: `"light"`, `"dark"`, or `"system"` to follow the operating system.
8
+ * @since 1.0.0
9
+ */
10
+ export type Theme = "light" | "dark" | "system";
11
+ /**
12
+ * The theme that is actually shown: `"light"` or `"dark"`.
13
+ * @since 1.0.0
14
+ */
15
+ export type ResolvedTheme = "light" | "dark";
16
+ /**
17
+ * Where the resolved theme is written on the root element. `"class"` (the default) sets a `light`
18
+ * or `dark` class, which the generated stylesheet matches for its dark styles (the `dark` variant of
19
+ * Tailwind is `.dark`). A `data-*` name such as `"data-theme"` sets that attribute to `"light"` or
20
+ * `"dark"` instead; the stylesheet then has to match that attribute as well.
21
+ * @since 1.0.0
22
+ */
23
+ export type ThemeAttribute = "class" | `data-${string}`;
24
+ /**
25
+ * Options shared by `ThemeProvider` and `themeScript`. Pass the same values to both, or the page can
26
+ * show one theme before the provider switches to another.
27
+ * @since 1.0.0
28
+ */
29
+ export interface ThemeOptions {
30
+ /**
31
+ * `localStorage` key that persists the choice. The stored value is `"light"`, `"dark"` or
32
+ * `"system"`; a missing or invalid value falls back to `defaultTheme`.
33
+ * @default "shadow-ui-theme"
34
+ */
35
+ readonly storageKey?: string | undefined;
36
+ /**
37
+ * Theme used before the user chooses one, and when the stored value is missing or invalid.
38
+ * @default "system"
39
+ */
40
+ readonly defaultTheme?: Theme | undefined;
41
+ /**
42
+ * Where the resolved theme is written: `"class"` for a `light` or `dark` class on `<html>`, or a
43
+ * `data-*` attribute name such as `"data-theme"` (see {@link ThemeAttribute}).
44
+ * @default "class"
45
+ */
46
+ readonly attribute?: ThemeAttribute | undefined;
47
+ /**
48
+ * Follow `prefers-color-scheme` while the theme is `"system"`. When `false`, `"system"` shows light.
49
+ * @default true
50
+ */
51
+ readonly enableSystem?: boolean | undefined;
52
+ }
53
+ /**
54
+ * The `localStorage` key that holds the theme choice when `storageKey` is not given.
55
+ * @example
56
+ * import { DEFAULT_STORAGE_KEY } from "@shadow-js/ui";
57
+ *
58
+ * window.localStorage.getItem(DEFAULT_STORAGE_KEY);
59
+ * @since 1.0.0
60
+ */
61
+ export declare const DEFAULT_STORAGE_KEY = "shadow-ui-theme";
62
+ /**
63
+ * Narrows an unknown value to a {@link Theme}. Use it on anything read from storage or from an event
64
+ * before trusting it.
65
+ *
66
+ * @param value Anything, such as a value read from storage.
67
+ * @returns `true` for `"light"`, `"dark"` and `"system"`, and `false` for everything else.
68
+ * @example
69
+ * import { DEFAULT_STORAGE_KEY, isTheme } from "@shadow-js/ui";
70
+ *
71
+ * isTheme(window.localStorage.getItem(DEFAULT_STORAGE_KEY)); // false when nothing is stored
72
+ * @since 1.0.0
73
+ */
74
+ export declare function isTheme(value: unknown): value is Theme;
75
+ /**
76
+ * Turns a theme choice into the theme to show. Only `"system"` depends on the operating system.
77
+ *
78
+ * @param theme The user's choice.
79
+ * @param systemPrefersDark Whether the operating system prefers a dark scheme.
80
+ * @param enableSystem When `false`, `"system"` resolves to light. Default `true`.
81
+ * @returns `"light"` or `"dark"`.
82
+ * @example
83
+ * import { resolveTheme } from "@shadow-js/ui";
84
+ *
85
+ * resolveTheme("system", true); // "dark"
86
+ * @since 1.0.0
87
+ */
88
+ export declare function resolveTheme(theme: Theme, systemPrefersDark: boolean, enableSystem?: boolean): ResolvedTheme;
89
+ /**
90
+ * Writes the resolved theme to the root element: sets the `light` or `dark` class (removing the
91
+ * other) or the `data-*` attribute, and sets `color-scheme` so form controls and scrollbars follow.
92
+ *
93
+ * @param root The element to write to, usually `document.documentElement`.
94
+ * @param resolved The theme to show.
95
+ * @param attribute Where to write it: `"class"` (the default) or a `data-*` attribute name.
96
+ * @example
97
+ * import { applyTheme } from "@shadow-js/ui";
98
+ *
99
+ * applyTheme(document.documentElement, "dark");
100
+ * @since 1.0.0
101
+ */
102
+ export declare function applyTheme(root: HTMLElement, resolved: ResolvedTheme, attribute?: ThemeAttribute): void;
103
+ /**
104
+ * Reads the stored theme. A missing or invalid value gives `undefined`. A storage the browser
105
+ * blocks (private windows, disabled site data) or has run out of room for also gives `undefined`:
106
+ * those raise a `SecurityError`, `NotAllowedError` or `QuotaExceededError` `DOMException`.
107
+ *
108
+ * @param storage The storage to read, usually `window.localStorage`.
109
+ * @param key The storage key, such as {@link DEFAULT_STORAGE_KEY}.
110
+ * @returns The stored theme, or `undefined` when it is missing, invalid or unavailable.
111
+ * @throws The original error when it is not one of the storage errors above, because that is a bug.
112
+ * @example
113
+ * import { DEFAULT_STORAGE_KEY, readStoredTheme } from "@shadow-js/ui";
114
+ *
115
+ * readStoredTheme(window.localStorage, DEFAULT_STORAGE_KEY);
116
+ * @since 1.0.0
117
+ */
118
+ export declare function readStoredTheme(storage: Storage, key: string): Theme | undefined;
119
+ /**
120
+ * Stores the theme under the key. A storage that is blocked or full does not throw: the function
121
+ * returns `false` instead.
122
+ *
123
+ * @param storage The storage to write, usually `window.localStorage`.
124
+ * @param key The storage key, such as {@link DEFAULT_STORAGE_KEY}.
125
+ * @param theme The theme to store.
126
+ * @returns `true` when the value was written, `false` when the storage refused it.
127
+ * @throws The original error when it is not one of the storage errors listed in {@link readStoredTheme}.
128
+ * @example
129
+ * import { DEFAULT_STORAGE_KEY, writeStoredTheme } from "@shadow-js/ui";
130
+ *
131
+ * writeStoredTheme(window.localStorage, DEFAULT_STORAGE_KEY, "dark");
132
+ * @since 1.0.0
133
+ */
134
+ export declare function writeStoredTheme(storage: Storage, key: string, theme: Theme): boolean;
135
+ /**
136
+ * The inline script that applies the stored theme before the first paint, so a dark-mode user does
137
+ * not see a flash of the light theme. Put the returned text in a `<script>` in the document
138
+ * `<head>`: a `<script>` in `index.html`, or the `document.head` option of Dusk's `createHandler`.
139
+ * An inline script in the head runs before the body is rendered. Pass the same options to
140
+ * `ThemeProvider`.
141
+ *
142
+ * The script is plain ES5 with no dependencies. It reads the stored theme, falls back to
143
+ * `defaultTheme` when the value is missing or invalid or when storage is blocked, and writes the same
144
+ * class or attribute and `color-scheme` as {@link applyTheme}. The function returns text only, so you
145
+ * place the tag yourself. {@link themeScriptTag} returns the same script in a tag with a CSP `nonce`.
146
+ *
147
+ * @param options Must match the `ThemeProvider` options.
148
+ * @returns JavaScript source text, without `<script>` tags.
149
+ * @example
150
+ * import { themeScript } from "@shadow-js/ui";
151
+ *
152
+ * const html = `<script>${themeScript({ defaultTheme: "dark" })}<\/script>`;
153
+ * @see {@link themeScriptTag}
154
+ * @since 1.0.0
155
+ */
156
+ export declare function themeScript(options?: ThemeOptions): string;
157
+ /**
158
+ * {@link themeScript} wrapped in a `<script>` element, as an HTML string, for a server that builds the
159
+ * document. Pass the page's CSP nonce so the inline script is allowed by a policy that uses nonces. In
160
+ * Dusk, the nonce of each response is `event.nonce`, available in the `document.head` function.
161
+ * Only base64 characters (`A-Z`, `a-z`, `0-9`, `+`, `/`, `=`, `_`, `-`) are kept from the nonce.
162
+ *
163
+ * @param options The theme options plus the CSP `nonce` of the page. Without a nonce the tag has no
164
+ * `nonce` attribute.
165
+ * @returns A `<script>` element as HTML text.
166
+ * @example
167
+ * import { createHandler, type DuskRuntimeManifest } from "@shadow-js/dusk/server";
168
+ * import { pages } from "@shadow-js/dusk/features/pages";
169
+ * import { themeScriptTag } from "@shadow-js/ui";
170
+ *
171
+ * declare const manifest: DuskRuntimeManifest;
172
+ *
173
+ * export const handler = createHandler({
174
+ * features: [pages()],
175
+ * manifest,
176
+ * security: { csp: {} },
177
+ * document: { head: (event) => themeScriptTag({ nonce: event.nonce }) },
178
+ * });
179
+ * @see {@link themeScript}
180
+ * @since 1.0.0
181
+ */
182
+ export declare function themeScriptTag(options?: ThemeOptions & {
183
+ readonly nonce?: string | undefined;
184
+ }): string;
185
+ //# sourceMappingURL=theme-logic.d.ts.map
@@ -0,0 +1,183 @@
1
+ // 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
+ export { DEFAULT_STORAGE_KEY, applyTheme, isTheme, readStoredTheme, resolveTheme, themeScript, themeScriptTag, writeStoredTheme };
182
+
183
+ //# 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,88 @@
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
+ /**
5
+ * What {@link useTheme} returns. Every value is an accessor: call it to read.
6
+ * @since 1.0.0
7
+ */
8
+ export interface ThemeContextValue {
9
+ /** The user's choice: `"light"`, `"dark"` or `"system"`. Call it to read the value. */
10
+ readonly theme: Accessor<Theme>;
11
+ /**
12
+ * The theme that is shown: `"light"` or `"dark"`. `undefined` on the server and until the provider
13
+ * has mounted, so markup that depends on it hydrates without a mismatch.
14
+ */
15
+ readonly resolvedTheme: Accessor<ResolvedTheme | undefined>;
16
+ /**
17
+ * The operating system preference: `"light"` or `"dark"`. `undefined` on the server and until the
18
+ * provider has mounted.
19
+ */
20
+ readonly systemTheme: Accessor<ResolvedTheme | undefined>;
21
+ /**
22
+ * Chooses a theme, or derives one from the previous choice, and stores it under the provider's
23
+ * `storageKey`. Throws a `TypeError` for a value that is not `"light"`, `"dark"` or `"system"`.
24
+ */
25
+ readonly setTheme: (theme: Theme | ((previous: Theme) => Theme)) => void;
26
+ }
27
+ /**
28
+ * Props of {@link ThemeProvider}: the {@link ThemeOptions} plus a transition switch and the children.
29
+ * @since 1.0.0
30
+ */
31
+ export interface ThemeProviderProps extends ThemeOptions {
32
+ /**
33
+ * Stops CSS transitions for one frame while the theme changes, so colors flip at once instead
34
+ * of fading piecemeal.
35
+ * @default false
36
+ */
37
+ readonly disableTransitionOnChange?: boolean | undefined;
38
+ /** The app to render inside the theme context. */
39
+ readonly children?: JsxChild | undefined;
40
+ }
41
+ /**
42
+ * Provides the color theme to the app: `"light"`, `"dark"` or `"system"`. The choice is persisted in
43
+ * `localStorage`, applied to `<html>` as a `light` or `dark` class (or a `data-*` attribute), and kept
44
+ * in sync across tabs and with the operating system. The provider renders no element of its own, and
45
+ * nothing touches the DOM until it has mounted, so it is safe to render on the server. Pair it with
46
+ * {@link themeScript} in the document head so the first paint already has the right theme.
47
+ *
48
+ * @param props The {@link ThemeProviderProps}: theme options (`storageKey`, `defaultTheme`,
49
+ * `attribute`, `enableSystem`), `disableTransitionOnChange` and `children`.
50
+ * @returns The children inside the theme context.
51
+ * @example
52
+ * import { ThemeProvider } from "@shadow-js/ui";
53
+ *
54
+ * function App() {
55
+ * return <p>Hello</p>;
56
+ * }
57
+ *
58
+ * export const root = (
59
+ * <ThemeProvider defaultTheme="system">
60
+ * <App />
61
+ * </ThemeProvider>
62
+ * );
63
+ * @see {@link useTheme}
64
+ * @since 1.0.0
65
+ */
66
+ export declare function ThemeProvider(props: ThemeProviderProps): JsxChild;
67
+ /**
68
+ * Reads the theme and its setter. Call it inside a component rendered under {@link ThemeProvider}.
69
+ *
70
+ * @returns The {@link ThemeContextValue}: accessors for the chosen, the resolved and the system
71
+ * theme, and `setTheme`.
72
+ * @throws Error when no `ThemeProvider` is above the caller.
73
+ * @example
74
+ * import { useTheme } from "@shadow-js/ui";
75
+ *
76
+ * export function ThemeToggle() {
77
+ * const { resolvedTheme, setTheme } = useTheme();
78
+ * return (
79
+ * <button onClick={() => setTheme(resolvedTheme() === "dark" ? "light" : "dark")}>
80
+ * Toggle theme
81
+ * </button>
82
+ * );
83
+ * }
84
+ * @see {@link ThemeProvider}
85
+ * @since 1.0.0
86
+ */
87
+ export declare function useTheme(): ThemeContextValue;
88
+ //# sourceMappingURL=theme-provider.d.ts.map
@@ -0,0 +1,149 @@
1
+ import { applyTheme, isTheme, readStoredTheme, resolveTheme, writeStoredTheme } from "./theme-logic.js";
2
+ import { batch, createContext, onCleanup, onMount, untrack, useContext, useEffect, useMemo, useSignal } from "@shadow-js/core";
3
+ // src/theme/theme-provider.ts
4
+ const MISSING = {
5
+ theme: () => "system",
6
+ resolvedTheme: () => void 0,
7
+ systemTheme: () => void 0,
8
+ setTheme: () => void 0
9
+ };
10
+ const ThemeContext = createContext(MISSING, { name: "ShadowUITheme" });
11
+ const DARK_QUERY = "(prefers-color-scheme: dark)";
12
+ /** Disables transitions until the next frame; returns nothing, cleans itself up. */
13
+ function suppressTransitions() {
14
+ const style = document.createElement("style");
15
+ style.textContent = "*,*::before,*::after{transition:none!important;animation-duration:0s!important}";
16
+ document.head.append(style);
17
+ window.getComputedStyle(document.body).getPropertyValue("opacity");
18
+ window.setTimeout(() => style.remove(), 1);
19
+ }
20
+ /**
21
+ * The page's `localStorage`, or `undefined` when the browser blocks it: reading the property
22
+ * itself throws a `DOMException` in some sandboxed and locked-down contexts.
23
+ */
24
+ function localStorageOrUndefined() {
25
+ try {
26
+ return window.localStorage;
27
+ } catch (error) {
28
+ if (error instanceof DOMException) return void 0;
29
+ throw error;
30
+ }
31
+ }
32
+ /**
33
+ * Provides the color theme to the app: `"light"`, `"dark"` or `"system"`. The choice is persisted in
34
+ * `localStorage`, applied to `<html>` as a `light` or `dark` class (or a `data-*` attribute), and kept
35
+ * in sync across tabs and with the operating system. The provider renders no element of its own, and
36
+ * nothing touches the DOM until it has mounted, so it is safe to render on the server. Pair it with
37
+ * {@link themeScript} in the document head so the first paint already has the right theme.
38
+ *
39
+ * @param props The {@link ThemeProviderProps}: theme options (`storageKey`, `defaultTheme`,
40
+ * `attribute`, `enableSystem`), `disableTransitionOnChange` and `children`.
41
+ * @returns The children inside the theme context.
42
+ * @example
43
+ * import { ThemeProvider } from "@shadow-js/ui";
44
+ *
45
+ * function App() {
46
+ * return <p>Hello</p>;
47
+ * }
48
+ *
49
+ * export const root = (
50
+ * <ThemeProvider defaultTheme="system">
51
+ * <App />
52
+ * </ThemeProvider>
53
+ * );
54
+ * @see {@link useTheme}
55
+ * @since 1.0.0
56
+ */
57
+ function ThemeProvider(props) {
58
+ const storageKey = () => props.storageKey ?? "shadow-ui-theme";
59
+ const defaultTheme = () => props.defaultTheme ?? "system";
60
+ const attribute = () => props.attribute ?? "class";
61
+ const enableSystem = () => props.enableSystem ?? true;
62
+ const [theme, setThemeState] = useSignal(untrack(defaultTheme));
63
+ const [systemDark, setSystemDark] = useSignal(void 0);
64
+ const resolved = useMemo(() => {
65
+ const dark = systemDark();
66
+ if (dark === void 0) return void 0;
67
+ return resolveTheme(theme(), dark, enableSystem());
68
+ });
69
+ const system = useMemo(() => {
70
+ const dark = systemDark();
71
+ return dark === void 0 ? void 0 : dark ? "dark" : "light";
72
+ });
73
+ onMount(() => {
74
+ const query = window.matchMedia(DARK_QUERY);
75
+ batch(() => {
76
+ const storage = localStorageOrUndefined();
77
+ const stored = storage === void 0 ? void 0 : readStoredTheme(storage, storageKey());
78
+ if (stored !== void 0) setThemeState(stored);
79
+ setSystemDark(query.matches);
80
+ });
81
+ const onSystemChange = (event) => {
82
+ setSystemDark(event.matches);
83
+ };
84
+ const onStorage = (event) => {
85
+ if (event.key !== storageKey()) return;
86
+ setThemeState(isTheme(event.newValue) ? event.newValue : defaultTheme());
87
+ };
88
+ query.addEventListener("change", onSystemChange);
89
+ window.addEventListener("storage", onStorage);
90
+ onCleanup(() => {
91
+ query.removeEventListener("change", onSystemChange);
92
+ window.removeEventListener("storage", onStorage);
93
+ });
94
+ });
95
+ useEffect(() => {
96
+ const value = resolved();
97
+ if (value === void 0) return;
98
+ const attributeName = attribute();
99
+ const current = document.documentElement;
100
+ if (props.disableTransitionOnChange === true) suppressTransitions();
101
+ applyTheme(current, value, attributeName);
102
+ });
103
+ const value = {
104
+ theme,
105
+ resolvedTheme: resolved,
106
+ systemTheme: system,
107
+ setTheme(next) {
108
+ const chosen = typeof next === "function" ? next(untrack(theme)) : next;
109
+ if (!isTheme(chosen)) throw new TypeError(`setTheme: expected "light", "dark" or "system", received ${String(chosen)}`);
110
+ setThemeState(chosen);
111
+ const storage = localStorageOrUndefined();
112
+ if (storage !== void 0) writeStoredTheme(storage, untrack(storageKey), chosen);
113
+ }
114
+ };
115
+ return ThemeContext.Provider({
116
+ value,
117
+ get children() {
118
+ return props.children;
119
+ }
120
+ });
121
+ }
122
+ /**
123
+ * Reads the theme and its setter. Call it inside a component rendered under {@link ThemeProvider}.
124
+ *
125
+ * @returns The {@link ThemeContextValue}: accessors for the chosen, the resolved and the system
126
+ * theme, and `setTheme`.
127
+ * @throws Error when no `ThemeProvider` is above the caller.
128
+ * @example
129
+ * import { useTheme } from "@shadow-js/ui";
130
+ *
131
+ * export function ThemeToggle() {
132
+ * const { resolvedTheme, setTheme } = useTheme();
133
+ * return (
134
+ * <button onClick={() => setTheme(resolvedTheme() === "dark" ? "light" : "dark")}>
135
+ * Toggle theme
136
+ * </button>
137
+ * );
138
+ * }
139
+ * @see {@link ThemeProvider}
140
+ * @since 1.0.0
141
+ */
142
+ function useTheme() {
143
+ const value = useContext(ThemeContext);
144
+ if (value === MISSING) throw new Error("useTheme() must be used within <ThemeProvider>");
145
+ return value;
146
+ }
147
+ export { ThemeProvider, useTheme };
148
+
149
+ //# sourceMappingURL=theme-provider.js.map