@shadow-js/ui 1.0.2 → 1.0.3

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.3
4
+
5
+ ### Fixed
6
+
7
+ - `shadow-ui init` checks every path (`components.json`, the theme file, the stylesheet, `lib/utils`) before it writes the first file, so a refused `--css ../outside.css` leaves the project untouched and a later `init` works.
8
+ - Alias mapping uses the most specific `paths` pattern (the longest prefix, as TypeScript does), so `@/components/*` wins over `@/*`.
9
+ - `paths` and `baseUrl` are read through `extends` (relative files, packages in `node_modules`, arrays), resolved from the config that declares them, with a bounded and cycle-safe chain.
10
+ - The package manager is found in parent folders too (`packageManager` field and lockfiles, nearest folder wins), so a package in a workspace no longer gets npm.
11
+ - `shadow-ui remove` keeps a file that differs from the registry (exit 1, named in the output and in `--json` as `kept`) unless `--force` is given.
12
+ - `ThemeProvider` treats a `storage` event with a null key (`localStorage.clear()` in another tab) as the theme being removed and goes back to the default theme.
13
+
3
14
  ## 1.0.2
4
15
 
5
16
  ### Fixed
package/README.md CHANGED
@@ -123,7 +123,7 @@ primitive) without a version.
123
123
  | `--skip-install` | print the install command instead of running it |
124
124
  | `--path <dir>` | `add`: write `ui` components to `<dir>` instead of the configured alias |
125
125
  | `--all` / `-a` | `add`: every component |
126
- | `--force` | `remove`: even when another component in your project imports it |
126
+ | `--force` | `remove`: even when another component in your project imports it, or a file differs from the registry (you edited it) |
127
127
  | `--cwd <dir>` | run in another directory |
128
128
  | `--json` | every command: print one JSON document on stdout instead of words, and ask nothing (see "Output for scripts") |
129
129
  | `--base-color <color>` | `init`: `neutral` (default), `zinc`, `slate`, `stone` or `gray` |
@@ -175,7 +175,7 @@ command: `ls` is `shadow-ui list`, `rm` is `shadow-ui remove`.
175
175
  | `add` | `dryRun`, `requested`, `dependencies` (what came with them), `files` (`path`, `action`: `create`, `unchanged`, `overwrite`, `kept` or `differs` for a dry run, plus the `diff` of a file that differs), `installs` |
176
176
  | `list` | `initialized` (is there a `components.json`), `items` (`name`, `type`, `description`, `installed`) |
177
177
  | `diff` | `components` (`name`, `path`, `status`: `up-to-date`, `differs` with its `diff`, or `missing`), `checked`, `differing` |
178
- | `remove` | `dryRun`, `removed` (`name`, `path`), `blocked` (`name`, `usedBy`) |
178
+ | `remove` | `dryRun`, `removed` (`name`, `path`), `kept` (`name`, `path`: edited files), `blocked` (`name`, `usedBy`) |
179
179
  | `completion` | `shell`, `script` |
180
180
  | `--help`, `--version` | `usage`, `version` |
181
181
 
@@ -184,7 +184,7 @@ command: `ls` is `shadow-ui list`, `rm` is `shadow-ui remove`.
184
184
  `SHADOW_UI_MISSING_COMMAND`, `SHADOW_UI_INVALID_SHELL`, `SHADOW_UI_INVALID_BASE_COLOR`, `SHADOW_UI_NO_PROJECT` (no
185
185
  `package.json`), `SHADOW_UI_NO_CONFIG` (run `init` first), `SHADOW_UI_NO_COMPONENTS`, `SHADOW_UI_UNKNOWN_COMPONENT`,
186
186
  `SHADOW_UI_INVALID_NAME`, `SHADOW_UI_REGISTRY_CONFLICT`, `SHADOW_UI_INVALID_FILE`, `SHADOW_UI_UNSAFE_PATH`,
187
- `SHADOW_UI_FILES_DIFFER` (`add` kept a file), `SHADOW_UI_DIFFERS` (`diff` found a difference), `SHADOW_UI_IN_USE`
187
+ `SHADOW_UI_FILES_DIFFER` (`add` or `remove` kept a file), `SHADOW_UI_DIFFERS` (`diff` found a difference), `SHADOW_UI_IN_USE`
188
188
  (`remove` refused), `SHADOW_UI_INSTALL_FAILED`, `SHADOW_UI_FAILED` and `SHADOW_UI_UNEXPECTED`.
189
189
 
190
190
  ```bash
package/dist/cli.mjs CHANGED
@@ -13,6 +13,27 @@ import { fileURLToPath } from "node:url";
13
13
  */
14
14
  /** Symbolic links followed while resolving one path; the same bound the operating system uses. */
15
15
  const MAX_LINK_HOPS = 40;
16
+ /** Folders `ancestors` climbs at most: far beyond any real project, so a path loop cannot spin. */
17
+ const MAX_LEVELS = 64;
18
+ /**
19
+ * `start` and its parent folders up to the filesystem root, nearest first (at most 64).
20
+ *
21
+ * @param start An absolute folder.
22
+ * @returns The folders, `start` first.
23
+ * @example
24
+ * ancestors("/a/b"); // ["/a/b", "/a", "/"]
25
+ * @since 1.0.0
26
+ *
27
+ * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
28
+ */
29
+ function ancestors(start) {
30
+ const folders = [start];
31
+ for (let folder = dirname(start); folders.length < MAX_LEVELS; folder = dirname(folder)) {
32
+ if (folder === folders[folders.length - 1]) break;
33
+ folders.push(folder);
34
+ }
35
+ return folders;
36
+ }
16
37
  /** Whether a failed file call means "nothing is there" (or a file stands where a directory should). */
17
38
  function isMissing(error) {
18
39
  const code = error.code;
@@ -675,9 +696,59 @@ function parseJsonc(text) {
675
696
  function asRecord(value) {
676
697
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
677
698
  }
699
+ /** The `extends` links followed from one tsconfig: far beyond any real chain. */
700
+ const MAX_EXTENDS = 16;
701
+ /** The file an `extends` entry names, or `undefined`: a path, or a package looked up in `node_modules`. */
702
+ function extendsFile(fs, folder, entry) {
703
+ const candidates = (base) => base.endsWith(".json") ? [base] : [`${base}.json`, join(base, "tsconfig.json")];
704
+ if (entry.startsWith(".") || isAbsolute(entry)) return candidates(resolve(folder, entry)).find((file) => fs.exists(file));
705
+ for (const parent of ancestors(folder)) {
706
+ const found = candidates(join(parent, "node_modules", entry)).find((file) => fs.exists(file));
707
+ if (found !== void 0) return found;
708
+ }
709
+ }
710
+ /**
711
+ * The `paths` and `baseUrl` of one tsconfig with its `extends` merged in, as TypeScript does: later
712
+ * bases override earlier ones, the config itself overrides its bases, and `paths` is replaced as a
713
+ * whole. `seen` holds the chain so far, so a cycle or a very long chain ends.
714
+ */
715
+ function readOptions(fs, file, label, seen) {
716
+ const text = fs.read(file);
717
+ if (text === void 0) return void 0;
718
+ let parsed;
719
+ try {
720
+ parsed = parseJsonc(text);
721
+ } catch (error) {
722
+ if (error instanceof SyntaxError) throw new SchemaError(`${label} is not valid JSON: ${error.message}`);
723
+ throw error;
724
+ }
725
+ const config = asRecord(parsed);
726
+ const options = asRecord(config?.["compilerOptions"]);
727
+ const folder = dirname(file);
728
+ let merged = {};
729
+ const bases = [config?.["extends"]].flat();
730
+ for (const entry of seen.length < MAX_EXTENDS ? bases : []) {
731
+ const base = typeof entry === "string" ? extendsFile(fs, folder, entry) : void 0;
732
+ if (base !== void 0 && !seen.includes(base)) merged = {
733
+ ...merged,
734
+ ...readOptions(fs, base, base, [...seen, base])
735
+ };
736
+ }
737
+ const paths = asRecord(options?.["paths"]);
738
+ const baseUrl = options?.["baseUrl"];
739
+ return {
740
+ ...merged,
741
+ ...paths !== void 0 && {
742
+ paths,
743
+ pathsDir: folder
744
+ },
745
+ ...typeof baseUrl === "string" && { baseUrl: resolve(folder, baseUrl) }
746
+ };
747
+ }
678
748
  /**
679
749
  * Reads the `compilerOptions.paths` of the project's tsconfig files (`tsconfig.json`,
680
- * `tsconfig.app.json`). Targets are resolved against `baseUrl` or the config's directory.
750
+ * `tsconfig.app.json`), following their `extends` (relative files and packages in `node_modules`).
751
+ * Targets are resolved against `baseUrl` or the directory of the config that declares `paths`.
681
752
  *
682
753
  * @param fs The project filesystem.
683
754
  * @param root The project root.
@@ -691,19 +762,10 @@ function asRecord(value) {
691
762
  function readPathMappings(fs, root) {
692
763
  const mappings = [];
693
764
  for (const name of ["tsconfig.json", "tsconfig.app.json"]) {
694
- const text = fs.read(join(root, name));
695
- if (text === void 0) continue;
696
- let parsed;
697
- try {
698
- parsed = parseJsonc(text);
699
- } catch (error) {
700
- if (error instanceof SyntaxError) throw new SchemaError(`${name} is not valid JSON: ${error.message}`);
701
- throw error;
702
- }
703
- const options = asRecord(asRecord(parsed)?.["compilerOptions"]);
704
- const paths = asRecord(options?.["paths"]);
765
+ const file = join(root, name);
766
+ const { paths, pathsDir, baseUrl } = readOptions(fs, file, name, [file]) ?? {};
705
767
  if (paths === void 0) continue;
706
- const base = typeof options?.["baseUrl"] === "string" ? resolve(root, options["baseUrl"]) : root;
768
+ const base = baseUrl ?? pathsDir ?? root;
707
769
  for (const [pattern, targets] of Object.entries(paths)) {
708
770
  if (!Array.isArray(targets)) continue;
709
771
  mappings.push({
@@ -730,19 +792,26 @@ function readPathMappings(fs, root) {
730
792
  * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
731
793
  */
732
794
  function aliasToDirectory(alias, mappings) {
795
+ let best;
733
796
  for (const { pattern, targets } of mappings) {
797
+ const target = targets[0];
798
+ if (target === void 0) continue;
734
799
  const star = pattern.indexOf("*");
735
800
  if (star === -1) {
736
- if (pattern === alias && targets[0] !== void 0) return targets[0];
801
+ if (pattern === alias) return target;
737
802
  continue;
738
803
  }
739
804
  const prefix = pattern.slice(0, star);
740
805
  const suffix = pattern.slice(star + 1);
741
- if (!alias.startsWith(prefix) || !alias.endsWith(suffix) || alias.length < prefix.length + suffix.length) continue;
742
- const rest = alias.slice(prefix.length, alias.length - suffix.length);
743
- const target = targets[0];
744
- if (target !== void 0) return target.replace("*", rest);
806
+ if (alias.startsWith(prefix) && alias.endsWith(suffix) && alias.length >= prefix.length + suffix.length && (best === void 0 || prefix.length > best.length)) {
807
+ const rest = alias.slice(prefix.length, alias.length - suffix.length);
808
+ best = {
809
+ length: prefix.length,
810
+ directory: target.replace("*", rest)
811
+ };
812
+ }
745
813
  }
814
+ if (best !== void 0) return best.directory;
746
815
  throw new PathError(`The alias "${alias}" is not covered by "paths" in tsconfig.json. Add, for example:\n "compilerOptions": { "paths": { "@/*": ["./src/*"] } }`);
747
816
  }
748
817
  /**
@@ -792,12 +861,31 @@ function resolveInsideReal(fs, root, target) {
792
861
  // src/cli/pm.ts
793
862
  /**
794
863
  * Package manager detection and the install command line. npm is the default; pnpm, yarn and bun
795
- * are used when their lockfile (or the `packageManager` field) says so.
864
+ * are used when their lockfile (or the `packageManager` field) says so, in the project or a parent folder.
796
865
  * @packageDocumentation
797
866
  */
867
+ /** The package manager one folder says it uses (field first, then lockfiles), or `undefined`. */
868
+ function managerOf(fs, folder) {
869
+ const manifest = fs.read(join(folder, "package.json"));
870
+ if (manifest !== void 0) try {
871
+ const field = JSON.parse(manifest).packageManager;
872
+ if (typeof field === "string") {
873
+ const name = field.split("@")[0];
874
+ if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
875
+ }
876
+ } catch (error) {
877
+ if (!(error instanceof SyntaxError)) throw error;
878
+ }
879
+ if (fs.exists(join(folder, "pnpm-lock.yaml"))) return "pnpm";
880
+ if (fs.exists(join(folder, "yarn.lock"))) return "yarn";
881
+ if (fs.exists(join(folder, "bun.lock")) || fs.exists(join(folder, "bun.lockb"))) return "bun";
882
+ if (fs.exists(join(folder, "package-lock.json"))) return "npm";
883
+ }
798
884
  /**
799
- * Detects the project's package manager: the `packageManager` field of `package.json` first, then
800
- * lockfiles, otherwise npm.
885
+ * Detects the project's package manager. In the project folder and then each parent folder (a
886
+ * package inside a workspace is managed from the workspace root) it reads the `packageManager`
887
+ * field of `package.json`, then the lockfiles; the nearest folder that says anything wins, and npm
888
+ * is the default.
801
889
  *
802
890
  * @param fs The project filesystem.
803
891
  * @param root The project root.
@@ -809,19 +897,10 @@ function resolveInsideReal(fs, root, target) {
809
897
  * @internal Not exported from `@shadow-js/ui`: part of the shadow-ui command, whose public surface is the CLI.
810
898
  */
811
899
  function detectPackageManager(fs, root) {
812
- const manifest = fs.read(join(root, "package.json"));
813
- if (manifest !== void 0) try {
814
- const field = JSON.parse(manifest).packageManager;
815
- if (typeof field === "string") {
816
- const name = field.split("@")[0];
817
- if (name === "npm" || name === "pnpm" || name === "yarn" || name === "bun") return name;
818
- }
819
- } catch (error) {
820
- if (!(error instanceof SyntaxError)) throw error;
900
+ for (const folder of ancestors(root)) {
901
+ const manager = managerOf(fs, folder);
902
+ if (manager !== void 0) return manager;
821
903
  }
822
- if (fs.exists(join(root, "pnpm-lock.yaml"))) return "pnpm";
823
- if (fs.exists(join(root, "yarn.lock"))) return "yarn";
824
- if (fs.exists(join(root, "bun.lock")) || fs.exists(join(root, "bun.lockb"))) return "bun";
825
904
  return "npm";
826
905
  }
827
906
  /**
@@ -1598,6 +1677,11 @@ async function runInit(ctx, flags) {
1598
1677
  recordFile(ctx, label, current === void 0 ? "create" : "update");
1599
1678
  return "created";
1600
1679
  };
1680
+ const configPath = resolveInsideReal(ctx.fs, ctx.cwd, "components.json");
1681
+ const themeFile = themePathFor(config.tailwind.css);
1682
+ resolveInsideReal(ctx.fs, ctx.cwd, themeFile);
1683
+ const cssPath = resolveInsideReal(ctx.fs, ctx.cwd, config.tailwind.css);
1684
+ const utils = planInitUtils(ctx, config);
1601
1685
  ctx.report?.set("dryRun", flags.dryRun);
1602
1686
  ctx.report?.set("config", {
1603
1687
  baseColor,
@@ -1606,14 +1690,12 @@ async function runInit(ctx, flags) {
1606
1690
  ctx.report?.set("files", []);
1607
1691
  ctx.out("Setting up @shadow-js/ui");
1608
1692
  const configText = serializeConfig(config);
1609
- const configPath = resolveInsideReal(ctx.fs, ctx.cwd, "components.json");
1610
1693
  const configNow = ctx.fs.read(configPath);
1611
1694
  if (existing !== void 0 && !flags.overwrite && configNow !== void 0 && (config.tailwind.baseColor !== existing.tailwind.baseColor || config.tailwind.css !== existing.tailwind.css) && !sameContent(configNow, configText)) {
1612
1695
  ctx.out(` ${flags.dryRun ? "would update" : "updated"} components.json (base color ${config.tailwind.baseColor}, stylesheet ${config.tailwind.css})`);
1613
1696
  if (!flags.dryRun) ctx.fs.write(configPath, configText);
1614
1697
  recordFile(ctx, "components.json", "update");
1615
1698
  } else write("components.json", configText, "components.json");
1616
- const themeFile = themePathFor(config.tailwind.css);
1617
1699
  const themeLabel = themeFile.replaceAll("\\", "/");
1618
1700
  const themeText = buildThemeCss(ctx.registry, config.tailwind.baseColor);
1619
1701
  const themeCurrent = ctx.fs.read(resolveInsideReal(ctx.fs, ctx.cwd, themeFile));
@@ -1626,7 +1708,6 @@ async function runInit(ctx, flags) {
1626
1708
  ctx.out(` kept ${themeLabel} (edited; pass --overwrite to regenerate it for ${config.tailwind.baseColor})`);
1627
1709
  recordFile(ctx, themeLabel, "kept");
1628
1710
  } else write(themeFile, themeText, themeLabel);
1629
- const cssPath = resolveInsideReal(ctx.fs, ctx.cwd, config.tailwind.css);
1630
1711
  const css = ctx.fs.read(cssPath);
1631
1712
  if (css === void 0) write(config.tailwind.css, `@import "tailwindcss";\n${THEME_IMPORT_LINE}\n`, config.tailwind.css);
1632
1713
  else {
@@ -1646,7 +1727,7 @@ async function runInit(ctx, flags) {
1646
1727
  recordFile(ctx, config.tailwind.css, "skipped");
1647
1728
  }
1648
1729
  }
1649
- const added = await runAddForInit(ctx, config, flags);
1730
+ const added = await runAddForInit(ctx, utils, flags);
1650
1731
  if (added !== OK) return added;
1651
1732
  const tooling = [];
1652
1733
  if (!info.hasTailwind) tooling.push("tailwindcss");
@@ -1658,14 +1739,21 @@ async function runInit(ctx, flags) {
1658
1739
  return failed(ctx, error);
1659
1740
  }
1660
1741
  }
1661
- /** `init` copies `utils` through the same pipeline as `add`. */
1662
- async function runAddForInit(ctx, config, flags) {
1742
+ /** Plans `utils` through the same pipeline as `add`. */
1743
+ function planInitUtils(ctx, config) {
1663
1744
  const plan = resolveItems(ctx.registry.index, ["utils"]);
1664
1745
  const mappings = [...readPathMappings(ctx.fs, ctx.cwd), {
1665
1746
  pattern: "@/*",
1666
1747
  targets: [join(ctx.cwd, "src", "*")]
1667
1748
  }];
1668
- for (const entry of planFiles(ctx, config, plan.items, void 0, mappings)) {
1749
+ return {
1750
+ plan,
1751
+ entries: planFiles(ctx, config, plan.items, void 0, mappings)
1752
+ };
1753
+ }
1754
+ /** `init` copies `utils` through the same pipeline as `add`. */
1755
+ async function runAddForInit(ctx, { plan, entries }, flags) {
1756
+ for (const entry of entries) {
1669
1757
  const where = display(ctx.cwd, entry.target);
1670
1758
  if (entry.status === "unchanged") {
1671
1759
  ctx.out(` unchanged ${where}`);
@@ -1821,7 +1909,8 @@ function runDiff(ctx, names) {
1821
1909
  }
1822
1910
  /**
1823
1911
  * Deletes components' files from the project. Refuses when another component that is in the
1824
- * project imports one of them (unless `force`), and never removes npm packages.
1912
+ * project imports one of them, and keeps (exit 1) a file that differs from the registry, because
1913
+ * the user edited it; `force` overrides both. It never removes npm packages.
1825
1914
  *
1826
1915
  * @param ctx The CLI context.
1827
1916
  * @param names Component names.
@@ -1848,6 +1937,7 @@ function runRemove(ctx, names, flags) {
1848
1937
  ctx.report?.set("dryRun", flags.dryRun);
1849
1938
  ctx.report?.set("removed", []);
1850
1939
  let code = OK;
1940
+ let kept = 0;
1851
1941
  for (const name of removing) {
1852
1942
  const blockers = dependentsOf(ctx.registry.index, name, installedNames).filter((other) => !removing.includes(other));
1853
1943
  if (blockers.length > 0 && !flags.force) {
@@ -1861,18 +1951,30 @@ function runRemove(ctx, names, flags) {
1861
1951
  }
1862
1952
  const item = findItem(ctx.registry.index, name);
1863
1953
  if (item === void 0) continue;
1864
- for (const file of item.files) {
1865
- const target = targetFor(item, file, config, mappings, ctx, void 0);
1866
- if (!ctx.fs.exists(target)) continue;
1867
- const where = display(ctx.cwd, target);
1954
+ for (const entry of planFiles(ctx, config, [item], void 0, mappings)) {
1955
+ if (entry.status === "create") continue;
1956
+ const where = display(ctx.cwd, entry.target);
1957
+ if (entry.status === "differs" && !flags.force) {
1958
+ kept += 1;
1959
+ ctx.out(` kept ${where} (differs from the registry; pass --force to remove it)`);
1960
+ ctx.report?.push("kept", {
1961
+ name,
1962
+ path: where
1963
+ });
1964
+ continue;
1965
+ }
1868
1966
  ctx.out(` ${flags.dryRun ? "would remove" : "remove"} ${where}`);
1869
- if (!flags.dryRun) ctx.fs.remove(target);
1967
+ if (!flags.dryRun) ctx.fs.remove(entry.target);
1870
1968
  ctx.report?.push("removed", {
1871
1969
  name,
1872
1970
  path: where
1873
1971
  });
1874
1972
  }
1875
1973
  }
1974
+ if (kept > 0) {
1975
+ problem(ctx, "SHADOW_UI_FILES_DIFFER", `${kept} file(s) differ from the registry and were kept; pass --force to remove them.`);
1976
+ code = FAILED;
1977
+ }
1876
1978
  return code;
1877
1979
  } catch (error) {
1878
1980
  return failed(ctx, error);
@@ -2341,7 +2443,7 @@ function realContext(cwd) {
2341
2443
  cwd: resolve(cwd ?? process.cwd()),
2342
2444
  fs: nodeFs(),
2343
2445
  registry: loadRegistrySource(),
2344
- version: "1.0.2",
2446
+ version: "1.0.3",
2345
2447
  out: (line) => process.stdout.write(`${line}\n`),
2346
2448
  err: (line) => process.stderr.write(`${line}\n`),
2347
2449
  async confirm(question) {
@@ -82,7 +82,7 @@ function ThemeProvider(props) {
82
82
  setSystemDark(event.matches);
83
83
  };
84
84
  const onStorage = (event) => {
85
- if (event.key !== storageKey()) return;
85
+ if (event.key !== null && event.key !== storageKey()) return;
86
86
  setThemeState(isTheme(event.newValue) ? event.newValue : defaultTheme());
87
87
  };
88
88
  query.addEventListener("change", onSystemChange);
@@ -1 +1 @@
1
- {"version":3,"file":"theme-provider.js","names":[],"sources":["../../../src/theme/theme-provider.ts"],"sourcesContent":["import {\n type Accessor,\n batch,\n createContext,\n onCleanup,\n onMount,\n untrack,\n useContext,\n useEffect,\n useMemo,\n useSignal,\n} from \"@shadow-js/core\";\nimport type { JsxChild } from \"@shadow-js/web/jsx-runtime\";\nimport {\n applyTheme,\n DEFAULT_STORAGE_KEY,\n isTheme,\n type ResolvedTheme,\n readStoredTheme,\n resolveTheme,\n type Theme,\n type ThemeOptions,\n writeStoredTheme,\n} from \"./theme-logic\";\n\n/**\n * What {@link useTheme} returns. Every value is an accessor: call it to read.\n * @since 1.0.0\n */\nexport interface ThemeContextValue {\n /** The user's choice: `\"light\"`, `\"dark\"` or `\"system\"`. Call it to read the value. */\n readonly theme: Accessor<Theme>;\n /**\n * The theme that is shown: `\"light\"` or `\"dark\"`. `undefined` on the server and until the provider\n * has mounted, so markup that depends on it hydrates without a mismatch.\n */\n readonly resolvedTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * The operating system preference: `\"light\"` or `\"dark\"`. `undefined` on the server and until the\n * provider has mounted.\n */\n readonly systemTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * Chooses a theme, or derives one from the previous choice, and stores it under the provider's\n * `storageKey`. Throws a `TypeError` for a value that is not `\"light\"`, `\"dark\"` or `\"system\"`.\n */\n readonly setTheme: (theme: Theme | ((previous: Theme) => Theme)) => void;\n}\n\n/**\n * Props of {@link ThemeProvider}: the {@link ThemeOptions} plus a transition switch and the children.\n * @since 1.0.0\n */\nexport interface ThemeProviderProps extends ThemeOptions {\n /**\n * Stops CSS transitions for one frame while the theme changes, so colors flip at once instead\n * of fading piecemeal.\n * @default false\n */\n readonly disableTransitionOnChange?: boolean | undefined;\n /** The app to render inside the theme context. */\n readonly children?: JsxChild | undefined;\n}\n\nconst MISSING: ThemeContextValue = {\n theme: () => \"system\",\n resolvedTheme: () => undefined,\n systemTheme: () => undefined,\n setTheme: () => undefined,\n};\n\nconst ThemeContext = createContext<ThemeContextValue>(MISSING, { name: \"ShadowUITheme\" });\n\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\n\n/** Disables transitions until the next frame; returns nothing, cleans itself up. */\nfunction suppressTransitions(): void {\n const style = document.createElement(\"style\");\n style.textContent =\n \"*,*::before,*::after{transition:none!important;animation-duration:0s!important}\";\n document.head.append(style);\n // Reading a computed style forces the browser to apply the change with transitions off.\n window.getComputedStyle(document.body).getPropertyValue(\"opacity\");\n window.setTimeout(() => style.remove(), 1);\n}\n\n/**\n * The page's `localStorage`, or `undefined` when the browser blocks it: reading the property\n * itself throws a `DOMException` in some sandboxed and locked-down contexts.\n */\nfunction localStorageOrUndefined(): Storage | undefined {\n try {\n return window.localStorage;\n } catch (error) {\n if (error instanceof DOMException) return undefined;\n throw error;\n }\n}\n\n/**\n * Provides the color theme to the app: `\"light\"`, `\"dark\"` or `\"system\"`. The choice is persisted in\n * `localStorage`, applied to `<html>` as a `light` or `dark` class (or a `data-*` attribute), and kept\n * in sync across tabs and with the operating system. The provider renders no element of its own, and\n * nothing touches the DOM until it has mounted, so it is safe to render on the server. Pair it with\n * {@link themeScript} in the document head so the first paint already has the right theme.\n *\n * @param props The {@link ThemeProviderProps}: theme options (`storageKey`, `defaultTheme`,\n * `attribute`, `enableSystem`), `disableTransitionOnChange` and `children`.\n * @returns The children inside the theme context.\n * @example\n * import { ThemeProvider } from \"@shadow-js/ui\";\n *\n * function App() {\n * return <p>Hello</p>;\n * }\n *\n * export const root = (\n * <ThemeProvider defaultTheme=\"system\">\n * <App />\n * </ThemeProvider>\n * );\n * @see {@link useTheme}\n * @since 1.0.0\n */\nexport function ThemeProvider(props: ThemeProviderProps): JsxChild {\n const storageKey = (): string => props.storageKey ?? DEFAULT_STORAGE_KEY;\n const defaultTheme = (): Theme => props.defaultTheme ?? \"system\";\n const attribute = (): \"class\" | `data-${string}` => props.attribute ?? \"class\";\n const enableSystem = (): boolean => props.enableSystem ?? true;\n\n const [theme, setThemeState] = useSignal<Theme>(untrack(defaultTheme));\n const [systemDark, setSystemDark] = useSignal<boolean | undefined>(undefined);\n const resolved = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n if (dark === undefined) return undefined;\n return resolveTheme(theme(), dark, enableSystem());\n });\n const system = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n return dark === undefined ? undefined : dark ? \"dark\" : \"light\";\n });\n\n onMount(() => {\n const query = window.matchMedia(DARK_QUERY);\n batch(() => {\n const storage = localStorageOrUndefined();\n const stored = storage === undefined ? undefined : readStoredTheme(storage, storageKey());\n if (stored !== undefined) setThemeState(stored);\n setSystemDark(query.matches);\n });\n const onSystemChange = (event: MediaQueryListEvent): void => {\n setSystemDark(event.matches);\n };\n const onStorage = (event: StorageEvent): void => {\n if (event.key !== storageKey()) return;\n setThemeState(isTheme(event.newValue) ? event.newValue : defaultTheme());\n };\n query.addEventListener(\"change\", onSystemChange);\n window.addEventListener(\"storage\", onStorage);\n onCleanup(() => {\n query.removeEventListener(\"change\", onSystemChange);\n window.removeEventListener(\"storage\", onStorage);\n });\n });\n\n useEffect(() => {\n const value = resolved();\n if (value === undefined) return;\n const attributeName = attribute();\n const current = document.documentElement;\n if (props.disableTransitionOnChange === true) suppressTransitions();\n applyTheme(current, value, attributeName);\n });\n\n const value: ThemeContextValue = {\n theme,\n resolvedTheme: resolved,\n systemTheme: system,\n setTheme(next) {\n const chosen = typeof next === \"function\" ? next(untrack(theme)) : next;\n if (!isTheme(chosen)) {\n throw new TypeError(\n `setTheme: expected \"light\", \"dark\" or \"system\", received ${String(chosen)}`,\n );\n }\n setThemeState(chosen);\n const storage = localStorageOrUndefined();\n if (storage !== undefined) writeStoredTheme(storage, untrack(storageKey), chosen);\n },\n };\n\n return ThemeContext.Provider({\n value,\n get children() {\n return props.children;\n },\n }) as JsxChild;\n}\n\n/**\n * Reads the theme and its setter. Call it inside a component rendered under {@link ThemeProvider}.\n *\n * @returns The {@link ThemeContextValue}: accessors for the chosen, the resolved and the system\n * theme, and `setTheme`.\n * @throws Error when no `ThemeProvider` is above the caller.\n * @example\n * import { useTheme } from \"@shadow-js/ui\";\n *\n * export function ThemeToggle() {\n * const { resolvedTheme, setTheme } = useTheme();\n * return (\n * <button onClick={() => setTheme(resolvedTheme() === \"dark\" ? \"light\" : \"dark\")}>\n * Toggle theme\n * </button>\n * );\n * }\n * @see {@link ThemeProvider}\n * @since 1.0.0\n */\nexport function useTheme(): ThemeContextValue {\n const value = useContext(ThemeContext);\n if (value === MISSING) throw new Error(\"useTheme() must be used within <ThemeProvider>\");\n return value;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"theme-provider.js","names":[],"sources":["../../../src/theme/theme-provider.ts"],"sourcesContent":["import {\n type Accessor,\n batch,\n createContext,\n onCleanup,\n onMount,\n untrack,\n useContext,\n useEffect,\n useMemo,\n useSignal,\n} from \"@shadow-js/core\";\nimport type { JsxChild } from \"@shadow-js/web/jsx-runtime\";\nimport {\n applyTheme,\n DEFAULT_STORAGE_KEY,\n isTheme,\n type ResolvedTheme,\n readStoredTheme,\n resolveTheme,\n type Theme,\n type ThemeOptions,\n writeStoredTheme,\n} from \"./theme-logic\";\n\n/**\n * What {@link useTheme} returns. Every value is an accessor: call it to read.\n * @since 1.0.0\n */\nexport interface ThemeContextValue {\n /** The user's choice: `\"light\"`, `\"dark\"` or `\"system\"`. Call it to read the value. */\n readonly theme: Accessor<Theme>;\n /**\n * The theme that is shown: `\"light\"` or `\"dark\"`. `undefined` on the server and until the provider\n * has mounted, so markup that depends on it hydrates without a mismatch.\n */\n readonly resolvedTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * The operating system preference: `\"light\"` or `\"dark\"`. `undefined` on the server and until the\n * provider has mounted.\n */\n readonly systemTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * Chooses a theme, or derives one from the previous choice, and stores it under the provider's\n * `storageKey`. Throws a `TypeError` for a value that is not `\"light\"`, `\"dark\"` or `\"system\"`.\n */\n readonly setTheme: (theme: Theme | ((previous: Theme) => Theme)) => void;\n}\n\n/**\n * Props of {@link ThemeProvider}: the {@link ThemeOptions} plus a transition switch and the children.\n * @since 1.0.0\n */\nexport interface ThemeProviderProps extends ThemeOptions {\n /**\n * Stops CSS transitions for one frame while the theme changes, so colors flip at once instead\n * of fading piecemeal.\n * @default false\n */\n readonly disableTransitionOnChange?: boolean | undefined;\n /** The app to render inside the theme context. */\n readonly children?: JsxChild | undefined;\n}\n\nconst MISSING: ThemeContextValue = {\n theme: () => \"system\",\n resolvedTheme: () => undefined,\n systemTheme: () => undefined,\n setTheme: () => undefined,\n};\n\nconst ThemeContext = createContext<ThemeContextValue>(MISSING, { name: \"ShadowUITheme\" });\n\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\n\n/** Disables transitions until the next frame; returns nothing, cleans itself up. */\nfunction suppressTransitions(): void {\n const style = document.createElement(\"style\");\n style.textContent =\n \"*,*::before,*::after{transition:none!important;animation-duration:0s!important}\";\n document.head.append(style);\n // Reading a computed style forces the browser to apply the change with transitions off.\n window.getComputedStyle(document.body).getPropertyValue(\"opacity\");\n window.setTimeout(() => style.remove(), 1);\n}\n\n/**\n * The page's `localStorage`, or `undefined` when the browser blocks it: reading the property\n * itself throws a `DOMException` in some sandboxed and locked-down contexts.\n */\nfunction localStorageOrUndefined(): Storage | undefined {\n try {\n return window.localStorage;\n } catch (error) {\n if (error instanceof DOMException) return undefined;\n throw error;\n }\n}\n\n/**\n * Provides the color theme to the app: `\"light\"`, `\"dark\"` or `\"system\"`. The choice is persisted in\n * `localStorage`, applied to `<html>` as a `light` or `dark` class (or a `data-*` attribute), and kept\n * in sync across tabs and with the operating system. The provider renders no element of its own, and\n * nothing touches the DOM until it has mounted, so it is safe to render on the server. Pair it with\n * {@link themeScript} in the document head so the first paint already has the right theme.\n *\n * @param props The {@link ThemeProviderProps}: theme options (`storageKey`, `defaultTheme`,\n * `attribute`, `enableSystem`), `disableTransitionOnChange` and `children`.\n * @returns The children inside the theme context.\n * @example\n * import { ThemeProvider } from \"@shadow-js/ui\";\n *\n * function App() {\n * return <p>Hello</p>;\n * }\n *\n * export const root = (\n * <ThemeProvider defaultTheme=\"system\">\n * <App />\n * </ThemeProvider>\n * );\n * @see {@link useTheme}\n * @since 1.0.0\n */\nexport function ThemeProvider(props: ThemeProviderProps): JsxChild {\n const storageKey = (): string => props.storageKey ?? DEFAULT_STORAGE_KEY;\n const defaultTheme = (): Theme => props.defaultTheme ?? \"system\";\n const attribute = (): \"class\" | `data-${string}` => props.attribute ?? \"class\";\n const enableSystem = (): boolean => props.enableSystem ?? true;\n\n const [theme, setThemeState] = useSignal<Theme>(untrack(defaultTheme));\n const [systemDark, setSystemDark] = useSignal<boolean | undefined>(undefined);\n const resolved = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n if (dark === undefined) return undefined;\n return resolveTheme(theme(), dark, enableSystem());\n });\n const system = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n return dark === undefined ? undefined : dark ? \"dark\" : \"light\";\n });\n\n onMount(() => {\n const query = window.matchMedia(DARK_QUERY);\n batch(() => {\n const storage = localStorageOrUndefined();\n const stored = storage === undefined ? undefined : readStoredTheme(storage, storageKey());\n if (stored !== undefined) setThemeState(stored);\n setSystemDark(query.matches);\n });\n const onSystemChange = (event: MediaQueryListEvent): void => {\n setSystemDark(event.matches);\n };\n const onStorage = (event: StorageEvent): void => {\n // A null key is `storage.clear()`: the stored theme is gone, like a removed key.\n if (event.key !== null && event.key !== storageKey()) return;\n setThemeState(isTheme(event.newValue) ? event.newValue : defaultTheme());\n };\n query.addEventListener(\"change\", onSystemChange);\n window.addEventListener(\"storage\", onStorage);\n onCleanup(() => {\n query.removeEventListener(\"change\", onSystemChange);\n window.removeEventListener(\"storage\", onStorage);\n });\n });\n\n useEffect(() => {\n const value = resolved();\n if (value === undefined) return;\n const attributeName = attribute();\n const current = document.documentElement;\n if (props.disableTransitionOnChange === true) suppressTransitions();\n applyTheme(current, value, attributeName);\n });\n\n const value: ThemeContextValue = {\n theme,\n resolvedTheme: resolved,\n systemTheme: system,\n setTheme(next) {\n const chosen = typeof next === \"function\" ? next(untrack(theme)) : next;\n if (!isTheme(chosen)) {\n throw new TypeError(\n `setTheme: expected \"light\", \"dark\" or \"system\", received ${String(chosen)}`,\n );\n }\n setThemeState(chosen);\n const storage = localStorageOrUndefined();\n if (storage !== undefined) writeStoredTheme(storage, untrack(storageKey), chosen);\n },\n };\n\n return ThemeContext.Provider({\n value,\n get children() {\n return props.children;\n },\n }) as JsxChild;\n}\n\n/**\n * Reads the theme and its setter. Call it inside a component rendered under {@link ThemeProvider}.\n *\n * @returns The {@link ThemeContextValue}: accessors for the chosen, the resolved and the system\n * theme, and `setTheme`.\n * @throws Error when no `ThemeProvider` is above the caller.\n * @example\n * import { useTheme } from \"@shadow-js/ui\";\n *\n * export function ThemeToggle() {\n * const { resolvedTheme, setTheme } = useTheme();\n * return (\n * <button onClick={() => setTheme(resolvedTheme() === \"dark\" ? \"light\" : \"dark\")}>\n * Toggle theme\n * </button>\n * );\n * }\n * @see {@link ThemeProvider}\n * @since 1.0.0\n */\nexport function useTheme(): ThemeContextValue {\n const value = useContext(ThemeContext);\n if (value === MISSING) throw new Error(\"useTheme() must be used within <ThemeProvider>\");\n return value;\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{applyTheme as e,isTheme as t,readStoredTheme as n,resolveTheme as r,writeStoredTheme as i}from"./theme-logic.js";import{batch as a,createContext as o,onCleanup as s,onMount as c,untrack as l,useContext as u,useEffect as d,useMemo as f,useSignal as p}from"@shadow-js/core";const m={theme:()=>`system`,resolvedTheme:()=>void 0,systemTheme:()=>void 0,setTheme:()=>void 0},h=o(m,{name:`ShadowUITheme`});function g(){let e=document.createElement(`style`);e.textContent=`*,*::before,*::after{transition:none!important;animation-duration:0s!important}`,document.head.append(e),window.getComputedStyle(document.body).getPropertyValue(`opacity`),window.setTimeout(()=>e.remove(),1)}function _(){try{return window.localStorage}catch(e){if(e instanceof DOMException)return;throw e}}function v(o){let u=()=>o.storageKey??`shadow-ui-theme`,m=()=>o.defaultTheme??`system`,v=()=>o.attribute??`class`,y=()=>o.enableSystem??!0,[b,x]=p(l(m)),[S,C]=p(void 0),w=f(()=>{let e=S();if(e!==void 0)return r(b(),e,y())}),T=f(()=>{let e=S();return e===void 0?void 0:e?`dark`:`light`});c(()=>{let e=window.matchMedia(`(prefers-color-scheme: dark)`);a(()=>{let t=_(),r=t===void 0?void 0:n(t,u());r!==void 0&&x(r),C(e.matches)});let r=e=>{C(e.matches)},i=e=>{e.key===u()&&x(t(e.newValue)?e.newValue:m())};e.addEventListener(`change`,r),window.addEventListener(`storage`,i),s(()=>{e.removeEventListener(`change`,r),window.removeEventListener(`storage`,i)})}),d(()=>{let t=w();if(t===void 0)return;let n=v(),r=document.documentElement;o.disableTransitionOnChange===!0&&g(),e(r,t,n)});let E={theme:b,resolvedTheme:w,systemTheme:T,setTheme(e){let n=typeof e==`function`?e(l(b)):e;if(!t(n))throw TypeError(`setTheme: expected "light", "dark" or "system", received ${String(n)}`);x(n);let r=_();r!==void 0&&i(r,l(u),n)}};return h.Provider({value:E,get children(){return o.children}})}function y(){let e=u(h);if(e===m)throw Error(`useTheme() must be used within <ThemeProvider>`);return e}export{v as ThemeProvider,y as useTheme};
1
+ import{applyTheme as e,isTheme as t,readStoredTheme as n,resolveTheme as r,writeStoredTheme as i}from"./theme-logic.js";import{batch as a,createContext as o,onCleanup as s,onMount as c,untrack as l,useContext as u,useEffect as d,useMemo as f,useSignal as p}from"@shadow-js/core";const m={theme:()=>`system`,resolvedTheme:()=>void 0,systemTheme:()=>void 0,setTheme:()=>void 0},h=o(m,{name:`ShadowUITheme`});function g(){let e=document.createElement(`style`);e.textContent=`*,*::before,*::after{transition:none!important;animation-duration:0s!important}`,document.head.append(e),window.getComputedStyle(document.body).getPropertyValue(`opacity`),window.setTimeout(()=>e.remove(),1)}function _(){try{return window.localStorage}catch(e){if(e instanceof DOMException)return;throw e}}function v(o){let u=()=>o.storageKey??`shadow-ui-theme`,m=()=>o.defaultTheme??`system`,v=()=>o.attribute??`class`,y=()=>o.enableSystem??!0,[b,x]=p(l(m)),[S,C]=p(void 0),w=f(()=>{let e=S();if(e!==void 0)return r(b(),e,y())}),T=f(()=>{let e=S();return e===void 0?void 0:e?`dark`:`light`});c(()=>{let e=window.matchMedia(`(prefers-color-scheme: dark)`);a(()=>{let t=_(),r=t===void 0?void 0:n(t,u());r!==void 0&&x(r),C(e.matches)});let r=e=>{C(e.matches)},i=e=>{(e.key===null||e.key===u())&&x(t(e.newValue)?e.newValue:m())};e.addEventListener(`change`,r),window.addEventListener(`storage`,i),s(()=>{e.removeEventListener(`change`,r),window.removeEventListener(`storage`,i)})}),d(()=>{let t=w();if(t===void 0)return;let n=v(),r=document.documentElement;o.disableTransitionOnChange===!0&&g(),e(r,t,n)});let E={theme:b,resolvedTheme:w,systemTheme:T,setTheme(e){let n=typeof e==`function`?e(l(b)):e;if(!t(n))throw TypeError(`setTheme: expected "light", "dark" or "system", received ${String(n)}`);x(n);let r=_();r!==void 0&&i(r,l(u),n)}};return h.Provider({value:E,get children(){return o.children}})}function y(){let e=u(h);if(e===m)throw Error(`useTheme() must be used within <ThemeProvider>`);return e}export{v as ThemeProvider,y as useTheme};
2
2
  //# sourceMappingURL=theme-provider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme-provider.js","names":["MISSING","ThemeContext","createContext","suppressTransitions","style","localStorageOrUndefined","error","ThemeProvider","storageKey","props","defaultTheme","attribute","enableSystem","theme","setThemeState","useSignal","untrack","systemDark","setSystemDark","resolved","useMemo","dark","resolveTheme","system","onMount","query","batch","storage","stored","readStoredTheme","onSystemChange","event","onStorage","isTheme","onCleanup","useEffect","value","attributeName","current","applyTheme","chosen","next","writeStoredTheme","useTheme","useContext"],"sources":["../../../src/theme/theme-provider.ts"],"sourcesContent":["import {\n type Accessor,\n batch,\n createContext,\n onCleanup,\n onMount,\n untrack,\n useContext,\n useEffect,\n useMemo,\n useSignal,\n} from \"@shadow-js/core\";\nimport type { JsxChild } from \"@shadow-js/web/jsx-runtime\";\nimport {\n applyTheme,\n DEFAULT_STORAGE_KEY,\n isTheme,\n type ResolvedTheme,\n readStoredTheme,\n resolveTheme,\n type Theme,\n type ThemeOptions,\n writeStoredTheme,\n} from \"./theme-logic\";\n\n/**\n * What {@link useTheme} returns. Every value is an accessor: call it to read.\n * @since 1.0.0\n */\nexport interface ThemeContextValue {\n /** The user's choice: `\"light\"`, `\"dark\"` or `\"system\"`. Call it to read the value. */\n readonly theme: Accessor<Theme>;\n /**\n * The theme that is shown: `\"light\"` or `\"dark\"`. `undefined` on the server and until the provider\n * has mounted, so markup that depends on it hydrates without a mismatch.\n */\n readonly resolvedTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * The operating system preference: `\"light\"` or `\"dark\"`. `undefined` on the server and until the\n * provider has mounted.\n */\n readonly systemTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * Chooses a theme, or derives one from the previous choice, and stores it under the provider's\n * `storageKey`. Throws a `TypeError` for a value that is not `\"light\"`, `\"dark\"` or `\"system\"`.\n */\n readonly setTheme: (theme: Theme | ((previous: Theme) => Theme)) => void;\n}\n\n/**\n * Props of {@link ThemeProvider}: the {@link ThemeOptions} plus a transition switch and the children.\n * @since 1.0.0\n */\nexport interface ThemeProviderProps extends ThemeOptions {\n /**\n * Stops CSS transitions for one frame while the theme changes, so colors flip at once instead\n * of fading piecemeal.\n * @default false\n */\n readonly disableTransitionOnChange?: boolean | undefined;\n /** The app to render inside the theme context. */\n readonly children?: JsxChild | undefined;\n}\n\nconst MISSING: ThemeContextValue = {\n theme: () => \"system\",\n resolvedTheme: () => undefined,\n systemTheme: () => undefined,\n setTheme: () => undefined,\n};\n\nconst ThemeContext = createContext<ThemeContextValue>(MISSING, { name: \"ShadowUITheme\" });\n\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\n\n/** Disables transitions until the next frame; returns nothing, cleans itself up. */\nfunction suppressTransitions(): void {\n const style = document.createElement(\"style\");\n style.textContent =\n \"*,*::before,*::after{transition:none!important;animation-duration:0s!important}\";\n document.head.append(style);\n // Reading a computed style forces the browser to apply the change with transitions off.\n window.getComputedStyle(document.body).getPropertyValue(\"opacity\");\n window.setTimeout(() => style.remove(), 1);\n}\n\n/**\n * The page's `localStorage`, or `undefined` when the browser blocks it: reading the property\n * itself throws a `DOMException` in some sandboxed and locked-down contexts.\n */\nfunction localStorageOrUndefined(): Storage | undefined {\n try {\n return window.localStorage;\n } catch (error) {\n if (error instanceof DOMException) return undefined;\n throw error;\n }\n}\n\n/**\n * Provides the color theme to the app: `\"light\"`, `\"dark\"` or `\"system\"`. The choice is persisted in\n * `localStorage`, applied to `<html>` as a `light` or `dark` class (or a `data-*` attribute), and kept\n * in sync across tabs and with the operating system. The provider renders no element of its own, and\n * nothing touches the DOM until it has mounted, so it is safe to render on the server. Pair it with\n * {@link themeScript} in the document head so the first paint already has the right theme.\n *\n * @param props The {@link ThemeProviderProps}: theme options (`storageKey`, `defaultTheme`,\n * `attribute`, `enableSystem`), `disableTransitionOnChange` and `children`.\n * @returns The children inside the theme context.\n * @example\n * import { ThemeProvider } from \"@shadow-js/ui\";\n *\n * function App() {\n * return <p>Hello</p>;\n * }\n *\n * export const root = (\n * <ThemeProvider defaultTheme=\"system\">\n * <App />\n * </ThemeProvider>\n * );\n * @see {@link useTheme}\n * @since 1.0.0\n */\nexport function ThemeProvider(props: ThemeProviderProps): JsxChild {\n const storageKey = (): string => props.storageKey ?? DEFAULT_STORAGE_KEY;\n const defaultTheme = (): Theme => props.defaultTheme ?? \"system\";\n const attribute = (): \"class\" | `data-${string}` => props.attribute ?? \"class\";\n const enableSystem = (): boolean => props.enableSystem ?? true;\n\n const [theme, setThemeState] = useSignal<Theme>(untrack(defaultTheme));\n const [systemDark, setSystemDark] = useSignal<boolean | undefined>(undefined);\n const resolved = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n if (dark === undefined) return undefined;\n return resolveTheme(theme(), dark, enableSystem());\n });\n const system = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n return dark === undefined ? undefined : dark ? \"dark\" : \"light\";\n });\n\n onMount(() => {\n const query = window.matchMedia(DARK_QUERY);\n batch(() => {\n const storage = localStorageOrUndefined();\n const stored = storage === undefined ? undefined : readStoredTheme(storage, storageKey());\n if (stored !== undefined) setThemeState(stored);\n setSystemDark(query.matches);\n });\n const onSystemChange = (event: MediaQueryListEvent): void => {\n setSystemDark(event.matches);\n };\n const onStorage = (event: StorageEvent): void => {\n if (event.key !== storageKey()) return;\n setThemeState(isTheme(event.newValue) ? event.newValue : defaultTheme());\n };\n query.addEventListener(\"change\", onSystemChange);\n window.addEventListener(\"storage\", onStorage);\n onCleanup(() => {\n query.removeEventListener(\"change\", onSystemChange);\n window.removeEventListener(\"storage\", onStorage);\n });\n });\n\n useEffect(() => {\n const value = resolved();\n if (value === undefined) return;\n const attributeName = attribute();\n const current = document.documentElement;\n if (props.disableTransitionOnChange === true) suppressTransitions();\n applyTheme(current, value, attributeName);\n });\n\n const value: ThemeContextValue = {\n theme,\n resolvedTheme: resolved,\n systemTheme: system,\n setTheme(next) {\n const chosen = typeof next === \"function\" ? next(untrack(theme)) : next;\n if (!isTheme(chosen)) {\n throw new TypeError(\n `setTheme: expected \"light\", \"dark\" or \"system\", received ${String(chosen)}`,\n );\n }\n setThemeState(chosen);\n const storage = localStorageOrUndefined();\n if (storage !== undefined) writeStoredTheme(storage, untrack(storageKey), chosen);\n },\n };\n\n return ThemeContext.Provider({\n value,\n get children() {\n return props.children;\n },\n }) as JsxChild;\n}\n\n/**\n * Reads the theme and its setter. Call it inside a component rendered under {@link ThemeProvider}.\n *\n * @returns The {@link ThemeContextValue}: accessors for the chosen, the resolved and the system\n * theme, and `setTheme`.\n * @throws Error when no `ThemeProvider` is above the caller.\n * @example\n * import { useTheme } from \"@shadow-js/ui\";\n *\n * export function ThemeToggle() {\n * const { resolvedTheme, setTheme } = useTheme();\n * return (\n * <button onClick={() => setTheme(resolvedTheme() === \"dark\" ? \"light\" : \"dark\")}>\n * Toggle theme\n * </button>\n * );\n * }\n * @see {@link ThemeProvider}\n * @since 1.0.0\n */\nexport function useTheme(): ThemeContextValue {\n const value = useContext(ThemeContext);\n if (value === MISSING) throw new Error(\"useTheme() must be used within <ThemeProvider>\");\n return value;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"theme-provider.js","names":["MISSING","ThemeContext","createContext","suppressTransitions","style","localStorageOrUndefined","error","ThemeProvider","storageKey","props","defaultTheme","attribute","enableSystem","theme","setThemeState","useSignal","untrack","systemDark","setSystemDark","resolved","useMemo","dark","resolveTheme","system","onMount","query","batch","storage","stored","readStoredTheme","onSystemChange","event","onStorage","isTheme","onCleanup","useEffect","value","attributeName","current","applyTheme","chosen","next","writeStoredTheme","useTheme","useContext"],"sources":["../../../src/theme/theme-provider.ts"],"sourcesContent":["import {\n type Accessor,\n batch,\n createContext,\n onCleanup,\n onMount,\n untrack,\n useContext,\n useEffect,\n useMemo,\n useSignal,\n} from \"@shadow-js/core\";\nimport type { JsxChild } from \"@shadow-js/web/jsx-runtime\";\nimport {\n applyTheme,\n DEFAULT_STORAGE_KEY,\n isTheme,\n type ResolvedTheme,\n readStoredTheme,\n resolveTheme,\n type Theme,\n type ThemeOptions,\n writeStoredTheme,\n} from \"./theme-logic\";\n\n/**\n * What {@link useTheme} returns. Every value is an accessor: call it to read.\n * @since 1.0.0\n */\nexport interface ThemeContextValue {\n /** The user's choice: `\"light\"`, `\"dark\"` or `\"system\"`. Call it to read the value. */\n readonly theme: Accessor<Theme>;\n /**\n * The theme that is shown: `\"light\"` or `\"dark\"`. `undefined` on the server and until the provider\n * has mounted, so markup that depends on it hydrates without a mismatch.\n */\n readonly resolvedTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * The operating system preference: `\"light\"` or `\"dark\"`. `undefined` on the server and until the\n * provider has mounted.\n */\n readonly systemTheme: Accessor<ResolvedTheme | undefined>;\n /**\n * Chooses a theme, or derives one from the previous choice, and stores it under the provider's\n * `storageKey`. Throws a `TypeError` for a value that is not `\"light\"`, `\"dark\"` or `\"system\"`.\n */\n readonly setTheme: (theme: Theme | ((previous: Theme) => Theme)) => void;\n}\n\n/**\n * Props of {@link ThemeProvider}: the {@link ThemeOptions} plus a transition switch and the children.\n * @since 1.0.0\n */\nexport interface ThemeProviderProps extends ThemeOptions {\n /**\n * Stops CSS transitions for one frame while the theme changes, so colors flip at once instead\n * of fading piecemeal.\n * @default false\n */\n readonly disableTransitionOnChange?: boolean | undefined;\n /** The app to render inside the theme context. */\n readonly children?: JsxChild | undefined;\n}\n\nconst MISSING: ThemeContextValue = {\n theme: () => \"system\",\n resolvedTheme: () => undefined,\n systemTheme: () => undefined,\n setTheme: () => undefined,\n};\n\nconst ThemeContext = createContext<ThemeContextValue>(MISSING, { name: \"ShadowUITheme\" });\n\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\n\n/** Disables transitions until the next frame; returns nothing, cleans itself up. */\nfunction suppressTransitions(): void {\n const style = document.createElement(\"style\");\n style.textContent =\n \"*,*::before,*::after{transition:none!important;animation-duration:0s!important}\";\n document.head.append(style);\n // Reading a computed style forces the browser to apply the change with transitions off.\n window.getComputedStyle(document.body).getPropertyValue(\"opacity\");\n window.setTimeout(() => style.remove(), 1);\n}\n\n/**\n * The page's `localStorage`, or `undefined` when the browser blocks it: reading the property\n * itself throws a `DOMException` in some sandboxed and locked-down contexts.\n */\nfunction localStorageOrUndefined(): Storage | undefined {\n try {\n return window.localStorage;\n } catch (error) {\n if (error instanceof DOMException) return undefined;\n throw error;\n }\n}\n\n/**\n * Provides the color theme to the app: `\"light\"`, `\"dark\"` or `\"system\"`. The choice is persisted in\n * `localStorage`, applied to `<html>` as a `light` or `dark` class (or a `data-*` attribute), and kept\n * in sync across tabs and with the operating system. The provider renders no element of its own, and\n * nothing touches the DOM until it has mounted, so it is safe to render on the server. Pair it with\n * {@link themeScript} in the document head so the first paint already has the right theme.\n *\n * @param props The {@link ThemeProviderProps}: theme options (`storageKey`, `defaultTheme`,\n * `attribute`, `enableSystem`), `disableTransitionOnChange` and `children`.\n * @returns The children inside the theme context.\n * @example\n * import { ThemeProvider } from \"@shadow-js/ui\";\n *\n * function App() {\n * return <p>Hello</p>;\n * }\n *\n * export const root = (\n * <ThemeProvider defaultTheme=\"system\">\n * <App />\n * </ThemeProvider>\n * );\n * @see {@link useTheme}\n * @since 1.0.0\n */\nexport function ThemeProvider(props: ThemeProviderProps): JsxChild {\n const storageKey = (): string => props.storageKey ?? DEFAULT_STORAGE_KEY;\n const defaultTheme = (): Theme => props.defaultTheme ?? \"system\";\n const attribute = (): \"class\" | `data-${string}` => props.attribute ?? \"class\";\n const enableSystem = (): boolean => props.enableSystem ?? true;\n\n const [theme, setThemeState] = useSignal<Theme>(untrack(defaultTheme));\n const [systemDark, setSystemDark] = useSignal<boolean | undefined>(undefined);\n const resolved = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n if (dark === undefined) return undefined;\n return resolveTheme(theme(), dark, enableSystem());\n });\n const system = useMemo((): ResolvedTheme | undefined => {\n const dark = systemDark();\n return dark === undefined ? undefined : dark ? \"dark\" : \"light\";\n });\n\n onMount(() => {\n const query = window.matchMedia(DARK_QUERY);\n batch(() => {\n const storage = localStorageOrUndefined();\n const stored = storage === undefined ? undefined : readStoredTheme(storage, storageKey());\n if (stored !== undefined) setThemeState(stored);\n setSystemDark(query.matches);\n });\n const onSystemChange = (event: MediaQueryListEvent): void => {\n setSystemDark(event.matches);\n };\n const onStorage = (event: StorageEvent): void => {\n // A null key is `storage.clear()`: the stored theme is gone, like a removed key.\n if (event.key !== null && event.key !== storageKey()) return;\n setThemeState(isTheme(event.newValue) ? event.newValue : defaultTheme());\n };\n query.addEventListener(\"change\", onSystemChange);\n window.addEventListener(\"storage\", onStorage);\n onCleanup(() => {\n query.removeEventListener(\"change\", onSystemChange);\n window.removeEventListener(\"storage\", onStorage);\n });\n });\n\n useEffect(() => {\n const value = resolved();\n if (value === undefined) return;\n const attributeName = attribute();\n const current = document.documentElement;\n if (props.disableTransitionOnChange === true) suppressTransitions();\n applyTheme(current, value, attributeName);\n });\n\n const value: ThemeContextValue = {\n theme,\n resolvedTheme: resolved,\n systemTheme: system,\n setTheme(next) {\n const chosen = typeof next === \"function\" ? next(untrack(theme)) : next;\n if (!isTheme(chosen)) {\n throw new TypeError(\n `setTheme: expected \"light\", \"dark\" or \"system\", received ${String(chosen)}`,\n );\n }\n setThemeState(chosen);\n const storage = localStorageOrUndefined();\n if (storage !== undefined) writeStoredTheme(storage, untrack(storageKey), chosen);\n },\n };\n\n return ThemeContext.Provider({\n value,\n get children() {\n return props.children;\n },\n }) as JsxChild;\n}\n\n/**\n * Reads the theme and its setter. Call it inside a component rendered under {@link ThemeProvider}.\n *\n * @returns The {@link ThemeContextValue}: accessors for the chosen, the resolved and the system\n * theme, and `setTheme`.\n * @throws Error when no `ThemeProvider` is above the caller.\n * @example\n * import { useTheme } from \"@shadow-js/ui\";\n *\n * export function ThemeToggle() {\n * const { resolvedTheme, setTheme } = useTheme();\n * return (\n * <button onClick={() => setTheme(resolvedTheme() === \"dark\" ? \"light\" : \"dark\")}>\n * Toggle theme\n * </button>\n * );\n * }\n * @see {@link ThemeProvider}\n * @since 1.0.0\n */\nexport function useTheme(): ThemeContextValue {\n const value = useContext(ThemeContext);\n if (value === MISSING) throw new Error(\"useTheme() must be used within <ThemeProvider>\");\n return value;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shadow-js/ui",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "shadcn-style components you add to your project with a CLI, styled with Tailwind v4",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -60,18 +60,18 @@
60
60
  "check": "npm run lint && npm run typecheck && npm run build && npm test && npm run test:perf && npm run test:gc"
61
61
  },
62
62
  "peerDependencies": {
63
- "@shadow-js/core": "^1.0.2",
64
- "@shadow-js/primitives": "^1.0.2",
65
- "@shadow-js/web": "^1.0.2"
63
+ "@shadow-js/core": "^1.0.3",
64
+ "@shadow-js/primitives": "^1.0.3",
65
+ "@shadow-js/web": "^1.0.3"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@biomejs/biome": "2.5.15",
69
- "@shadow-js/compiler": "^1.0.2",
70
- "@shadow-js/core": "^1.0.2",
71
- "@shadow-js/forms": "^1.0.2",
72
- "@shadow-js/primitives": "^1.0.2",
73
- "@shadow-js/testing": "^1.0.2",
74
- "@shadow-js/web": "^1.0.2",
69
+ "@shadow-js/compiler": "^1.0.3",
70
+ "@shadow-js/core": "^1.0.3",
71
+ "@shadow-js/forms": "^1.0.3",
72
+ "@shadow-js/primitives": "^1.0.3",
73
+ "@shadow-js/testing": "^1.0.3",
74
+ "@shadow-js/web": "^1.0.3",
75
75
  "@tailwindcss/node": "4.3.3",
76
76
  "@types/node": "^24.0.0",
77
77
  "fast-check": "^4.10.2",
@@ -84,7 +84,7 @@
84
84
  "vitest": "5.0.3"
85
85
  },
86
86
  "dependencies": {
87
- "@shadow-js/table": "1.0.2",
87
+ "@shadow-js/table": "1.0.3",
88
88
  "class-variance-authority": "0.7.1",
89
89
  "clsx": "2.1.1",
90
90
  "tailwind-merge": "3.7.0"
package/registry.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shadow-js/ui",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "frameworkDependencies": [
5
5
  "@shadow-js/core",
6
6
  "@shadow-js/primitives",
@@ -1063,7 +1063,7 @@
1063
1063
  "utils"
1064
1064
  ],
1065
1065
  "dependencies": {
1066
- "@shadow-js/table": "1.0.2"
1066
+ "@shadow-js/table": "1.0.3"
1067
1067
  },
1068
1068
  "exports": [
1069
1069
  "DataTable",