@zentauri-ui/zentauri-components 2.1.4 → 2.1.5

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/README.md CHANGED
@@ -29,8 +29,8 @@ Generated from the component package Vitest JSON report via `pnpm --filter @zent
29
29
 
30
30
  | Metric | Result |
31
31
  | ---------- | ---------------- |
32
- | Test files | 93 passed (93) |
33
- | Tests | 752 passed (752) |
32
+ | Test files | 94 passed (94) |
33
+ | Tests | 754 passed (754) |
34
34
 
35
35
  | Area | Test files | Tests |
36
36
  | ------------------------------ | ---------- | ----- |
@@ -38,7 +38,7 @@ Generated from the component package Vitest JSON report via `pnpm --filter @zent
38
38
  | Standalone animations | 1 | 45 |
39
39
  | React hooks | 41 | 174 |
40
40
  | Design system facade | 1 | 11 |
41
- | CLI and import rewriting | 2 | 24 |
41
+ | CLI and import rewriting | 3 | 26 |
42
42
  | Accessibility (axe + keyboard) | 2 | 42 |
43
43
 
44
44
  ### Per-suite snapshot
@@ -51,7 +51,7 @@ Generated from the component package Vitest JSON report via `pnpm --filter @zent
51
51
  | `src/ui/peer-isolation.test.ts` | 29 |
52
52
  | `src/accessibility/axe-core.test.tsx` | 24 |
53
53
  | `src/ui/combobox/combobox.test.tsx` | 24 |
54
- | `cli/cli.integration.test.ts` | 19 |
54
+ | `cli/cli.integration.test.ts` | 20 |
55
55
  | `src/accessibility/keyboard-interaction.test.tsx` | 18 |
56
56
  | `src/ui/pagination/pagination.test.tsx` | 15 |
57
57
  | `src/ui/timeline/timeline.test.tsx` | 14 |
@@ -131,6 +131,7 @@ Generated from the component package Vitest JSON report via `pnpm --filter @zent
131
131
  | `src/hooks/useMediaQuery/useMediaQuery.test.ts` | 2 |
132
132
  | `src/hooks/useNetworkStatus/useNetworkStatus.test.ts` | 2 |
133
133
  | `src/hooks/useResizeObserver/useResizeObserver.test.ts` | 2 |
134
+ | `cli/props.test.ts` | 1 |
134
135
  | `src/hooks/useInView/useInView.test.ts` | 1 |
135
136
  | `src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.test.ts` | 1 |
136
137
  | `src/hooks/usePageVisibility/usePageVisibility.test.ts` | 1 |
@@ -847,7 +848,7 @@ From this package directory in the monorepo:
847
848
 
848
849
  - `pnpm build` (or `npm run build`) — production bundle via `tsup` (Rollup treeshake + `scripts/prepend-use-client.mjs` via `onSuccess` so each UI entry under `dist/ui/`, animation entry under `dist/animations/`, chart entry under `dist/charts/`, and `dist/ui/<name>/animated.*` starts with `"use client"` where needed)
849
850
  - `pnpm dev` — `tsup` watch mode (same `onSuccess` hook after each rebuild)
850
- - `pnpm test` / `pnpm test:watch` — **Vitest** and **Testing Library** unit tests // currently covered 752 test cases in total
851
+ - `pnpm test` / `pnpm test:watch` — **Vitest** and **Testing Library** unit tests // currently covered 754 test cases in total
851
852
  - `pnpm test:a11y` — focused accessibility coverage for package-level UI primitives and compound components: **axe-core** audits for every interactive component plus **keyboard-interaction** tests (focus order, arrow-key nav, Home/End, Escape/Enter) for the compound components
852
853
  - `pnpm check:tokens` — enforce the `--zui-*` token contract across design-system, variant, and local custom-property usage without generating a large checked-in token catalog
853
854
  - **`pnpm run generate:registry`** — runs `scripts/generate-registry.mjs`, which reads **`uiComponentNames`**, **`uiAnimatedComponentNames`**, **`animationEntryNames`**, **`chartEntryNames`**, and **`hooksEntryNames`** from `tsup.config.ts`, applies fixed **`nameAliases`**, scans each component/chart source to build **`peerHints`**, and writes **`cli/registry.json`** (`components` + `animations` + `hooks` + `peerHints`). Run this after adding or renaming UI, animation, chart, or hook entries so the CLI stays in sync (the script prints counts).
@@ -1,5 +1,12 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import {
3
+ existsSync,
4
+ mkdtempSync,
5
+ readdirSync,
6
+ readFileSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from "node:fs";
3
10
  import { tmpdir } from "node:os";
4
11
  import { dirname, join } from "node:path";
5
12
  import { fileURLToPath } from "node:url";
@@ -106,6 +113,35 @@ describe("zentauri-ui CLI", () => {
106
113
  }
107
114
  });
108
115
 
116
+ it("should not use a monorepo root config when add runs inside a package", () => {
117
+ const dir = mkdtempSync(join(tmpdir(), "zentauri-cli-monorepo-"));
118
+ try {
119
+ const appDir = join(dir, "apps/web");
120
+ execFileSync(process.execPath, [
121
+ "-e",
122
+ `require("node:fs").mkdirSync(${JSON.stringify(appDir)}, { recursive: true })`,
123
+ ]);
124
+ writeFileSync(
125
+ join(dir, "package.json"),
126
+ JSON.stringify({ private: true, workspaces: ["apps/*"] }),
127
+ );
128
+ writeFileSync(join(appDir, "package.json"), JSON.stringify({}));
129
+
130
+ runCli(dir, ["init"]);
131
+ const stderr = runCliError(appDir, ["add", "button"]);
132
+
133
+ expect(stderr).toContain("No components.json found");
134
+ expect(
135
+ existsSync(join(dir, "src/components/ui/buttons/button.tsx")),
136
+ ).toBe(false);
137
+ expect(
138
+ existsSync(join(appDir, "src/components/ui/buttons/button.tsx")),
139
+ ).toBe(false);
140
+ } finally {
141
+ rmSync(dir, { recursive: true, force: true });
142
+ }
143
+ });
144
+
109
145
  it("should add an animated component explicitly and report missing peers", () => {
110
146
  const dir = mkdtempSync(join(tmpdir(), "zentauri-cli-add-animated-"));
111
147
  try {
@@ -162,11 +198,17 @@ describe("zentauri-ui CLI", () => {
162
198
  expect(
163
199
  existsSync(join(dir, "src/components/design-system/button.ts")),
164
200
  ).toBe(true);
201
+ expect(
202
+ existsSync(join(dir, "src/components/design-system/tokens.ts")),
203
+ ).toBe(true);
204
+ expect(
205
+ readdirSync(join(dir, "src/components/design-system")).sort(),
206
+ ).toEqual(["button.ts", "tokens.ts"]);
165
207
  const variants = readFileSync(
166
208
  join(dir, "src/components/ui/buttons/variants.ts"),
167
209
  "utf8",
168
210
  );
169
- expect(variants).toContain("../../design-system");
211
+ expect(variants).toContain("../../design-system/button");
170
212
  } finally {
171
213
  rmSync(dir, { recursive: true, force: true });
172
214
  }
package/cli/index.mjs CHANGED
@@ -238,8 +238,9 @@ async function walkFiles(dir) {
238
238
  }
239
239
 
240
240
  /**
241
- * Walks upward from `startDir` toward the filesystem root until `components.json`
242
- * exists, or returns `undefined` if none found (e.g. user forgot `init`).
241
+ * Walks upward from `startDir` until `components.json` exists. If the command
242
+ * starts inside a package, discovery stops at that package's `package.json`
243
+ * boundary so monorepo root config cannot capture nested package installs.
243
244
  *
244
245
  * @param {string} startDir — typically `process.cwd()` or `--cwd` resolved path
245
246
  * @returns {string | undefined} — absolute path to `components.json` if found
@@ -253,11 +254,17 @@ async function walkFiles(dir) {
253
254
  */
254
255
  async function findComponentsJson(startDir) {
255
256
  let d = startDir;
257
+ const packagePath = findPackageJson(startDir);
258
+ const packageBoundary = packagePath ? dirname(packagePath) : undefined;
259
+
256
260
  for (;;) {
257
261
  const p = join(d, "components.json");
258
262
  if (existsSync(p)) {
259
263
  return p;
260
264
  }
265
+ if (packageBoundary && d === packageBoundary) {
266
+ return undefined;
267
+ }
261
268
  const parent = dirname(d);
262
269
  if (parent === d) {
263
270
  return undefined;
@@ -957,6 +964,7 @@ async function copyUiComponent(componentName, config, configDir, packageRoot) {
957
964
  return true;
958
965
  });
959
966
  const usedHooks = new Set();
967
+ const designSystemImportTarget = getDesignSystemEntryName(componentName);
960
968
 
961
969
  for (const absSrc of files) {
962
970
  const rel = relative(srcRoot, absSrc);
@@ -972,10 +980,13 @@ async function copyUiComponent(componentName, config, configDir, packageRoot) {
972
980
  hooksAlias: config.aliases.hooks,
973
981
  uiAlias: config.aliases.ui,
974
982
  });
983
+ const rewrittenCode = designSystemImportTarget
984
+ ? rewriteDesignSystemBarrelImports(code, designSystemImportTarget)
985
+ : code;
975
986
  for (const h of uh) {
976
987
  usedHooks.add(h);
977
988
  }
978
- await writeFile(absDest, code, "utf8");
989
+ await writeFile(absDest, rewrittenCode, "utf8");
979
990
  } else {
980
991
  await copyFile(absSrc, absDest);
981
992
  }
@@ -1022,11 +1033,57 @@ async function copyHookFolder(hookName, config, configDir, packageRoot) {
1022
1033
  }
1023
1034
 
1024
1035
  /**
1025
- * Copies shared component design tokens/variant maps beside the consumer's UI
1026
- * folder so vendored components can keep relative `../../design-system/*`
1027
- * imports without adding a new public alias to `components.json`.
1036
+ * Maps registry component names to their matching design-system token file.
1037
+ */
1038
+ function getDesignSystemEntryName(componentName) {
1039
+ if (
1040
+ componentName.startsWith("charts/") ||
1041
+ componentName.startsWith("animations/")
1042
+ ) {
1043
+ return undefined;
1044
+ }
1045
+ if (componentName === "buttons") {
1046
+ return "button";
1047
+ }
1048
+ return componentName;
1049
+ }
1050
+
1051
+ /**
1052
+ * Narrows vendored imports from the design-system barrel to the selected token
1053
+ * file. This lets a component like `buttons` vendor `design-system/button.ts`
1054
+ * without also requiring `design-system/index.ts`.
1028
1055
  */
1029
- async function copyDesignSystemFolder(config, configDir, packageRoot) {
1056
+ function rewriteDesignSystemBarrelImports(source, designSystemEntryName) {
1057
+ return source.replace(
1058
+ /from\s+(["'])((?:\.\.\/)+)design-system\1/g,
1059
+ (_, quote, rel) =>
1060
+ `from ${quote}${rel}design-system/${designSystemEntryName}${quote}`,
1061
+ );
1062
+ }
1063
+
1064
+ /**
1065
+ * Extracts local design-system dependencies from a design token file.
1066
+ */
1067
+ function extractDesignSystemDependencies(source) {
1068
+ const deps = new Set();
1069
+ const re = /from\s+["']\.\/([^"']+)["']/g;
1070
+ let match;
1071
+ while ((match = re.exec(source)) !== null) {
1072
+ deps.add(match[1].replace(/\.(tsx?|jsx?)$/, ""));
1073
+ }
1074
+ return [...deps];
1075
+ }
1076
+
1077
+ /**
1078
+ * Copies only the design-system token files required by selected components,
1079
+ * plus local token dependencies such as `tokens.ts`.
1080
+ */
1081
+ async function copyDesignSystemFiles(
1082
+ componentNames,
1083
+ config,
1084
+ configDir,
1085
+ packageRoot,
1086
+ ) {
1030
1087
  const srcRoot = join(packageRoot, "src", "design-system");
1031
1088
  if (!existsSync(srcRoot)) {
1032
1089
  return;
@@ -1039,28 +1096,34 @@ async function copyDesignSystemFolder(config, configDir, packageRoot) {
1039
1096
  dirname(config.resolvedPaths.ui),
1040
1097
  "design-system",
1041
1098
  );
1042
- const files = await walkFiles(srcRoot);
1043
- for (const absSrc of files) {
1044
- const rel = relative(srcRoot, absSrc);
1045
- if (isTestFile(rel)) {
1099
+ const pending = componentNames
1100
+ .map((name) => getDesignSystemEntryName(name))
1101
+ .filter(Boolean);
1102
+ const copied = new Set();
1103
+
1104
+ while (pending.length > 0) {
1105
+ const entryName = pending.shift();
1106
+ if (copied.has(entryName)) {
1046
1107
  continue;
1047
1108
  }
1109
+ const absSrc = join(srcRoot, `${entryName}.ts`);
1110
+ if (!existsSync(absSrc)) {
1111
+ continue;
1112
+ }
1113
+ copied.add(entryName);
1114
+ const rel = relative(srcRoot, absSrc);
1048
1115
  const absDest = join(destRoot, rel);
1116
+ const raw = await readFile(absSrc, "utf8");
1117
+ for (const dep of extractDesignSystemDependencies(raw)) {
1118
+ if (!copied.has(dep)) {
1119
+ pending.push(dep);
1120
+ }
1121
+ }
1049
1122
  if (existsSync(absDest)) {
1050
1123
  continue;
1051
1124
  }
1052
1125
  await mkdir(dirname(absDest), { recursive: true });
1053
- if (/\.(tsx?|jsx?)$/.test(absSrc)) {
1054
- const raw = await readFile(absSrc, "utf8");
1055
- const { code } = rewriteImports(raw, {
1056
- utilsAlias: config.aliases.utils,
1057
- hooksAlias: config.aliases.hooks,
1058
- uiAlias: config.aliases.ui,
1059
- });
1060
- await writeFile(absDest, code, "utf8");
1061
- } else {
1062
- await copyFile(absSrc, absDest);
1063
- }
1126
+ await writeFile(absDest, raw, "utf8");
1064
1127
  }
1065
1128
  }
1066
1129
 
@@ -1258,7 +1321,7 @@ async function cmdAdd(names, cwd, options = {}) {
1258
1321
  }
1259
1322
 
1260
1323
  await ensureUtilsFile(config, configDir, packageRoot);
1261
- await copyDesignSystemFolder(config, configDir, packageRoot);
1324
+ await copyDesignSystemFiles(resolvedNames, config, configDir, packageRoot);
1262
1325
 
1263
1326
  const allHooks = new Set();
1264
1327
  for (const name of resolvedNames) {