@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 +11 -0
- package/README.md +3 -3
- package/dist/cli.mjs +149 -47
- package/dist/dev/theme/theme-provider.js +1 -1
- package/dist/dev/theme/theme-provider.js.map +1 -1
- package/dist/prod/theme/theme-provider.js +1 -1
- package/dist/prod/theme/theme-provider.js.map +1 -1
- package/package.json +11 -11
- package/registry.json +2 -2
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`)
|
|
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
|
|
695
|
-
|
|
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 =
|
|
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
|
|
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 (
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
|
800
|
-
*
|
|
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
|
|
813
|
-
|
|
814
|
-
|
|
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,
|
|
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
|
-
/**
|
|
1662
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
64
|
-
"@shadow-js/primitives": "^1.0.
|
|
65
|
-
"@shadow-js/web": "^1.0.
|
|
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.
|
|
70
|
-
"@shadow-js/core": "^1.0.
|
|
71
|
-
"@shadow-js/forms": "^1.0.
|
|
72
|
-
"@shadow-js/primitives": "^1.0.
|
|
73
|
-
"@shadow-js/testing": "^1.0.
|
|
74
|
-
"@shadow-js/web": "^1.0.
|
|
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.
|
|
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.
|
|
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.
|
|
1066
|
+
"@shadow-js/table": "1.0.3"
|
|
1067
1067
|
},
|
|
1068
1068
|
"exports": [
|
|
1069
1069
|
"DataTable",
|