@zenginui/registry 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/build.js ADDED
@@ -0,0 +1,344 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import { LAYOUT, REGISTRY_SCHEMA } from "./schema.js";
4
+ import { ICON_SETS, REACT_ICONS_VERSION, renderIconsModule } from "./icons.js";
5
+ import { buildTokensCss } from "./tokens.js";
6
+ /**
7
+ * Builds the registry from the repository: every component in packages/ui, the shared lib and foundation
8
+ * files, and the example apps as templates. Imports are rewritten from the package layout to the project
9
+ * layout (`@/components/ui`, `@/lib/cx`), so what a project receives is what it would have written itself.
10
+ */
11
+ export function buildRegistry(opts) {
12
+ const ui = join(opts.root, "packages", "ui");
13
+ const version = opts.version ?? JSON.parse(read(join(ui, "package.json"))).version;
14
+ const uiPkg = JSON.parse(read(join(ui, "package.json")));
15
+ const manifests = JSON.parse(read(join(ui, "zengin", "components.json")));
16
+ const items = [];
17
+ // Every file in src/internal is a lib item; components depend on the ones they import.
18
+ const LIB_DESCRIPTIONS = {
19
+ cx: "Joins class names, dropping falsy values. Every component imports it.",
20
+ icons: "The icon vocabulary: one component per name, drawn by Zengin UI until zengin icons <set> points the names at a react-icons set.",
21
+ chart: "Scales, paths and the width hook the chart components share.",
22
+ markdown: "The markdown subset models produce, parsed into blocks for the Markdown component.",
23
+ };
24
+ for (const file of readdirSync(join(ui, "src", "internal")).sort()) {
25
+ if (!/\.tsx?$/.test(file))
26
+ continue;
27
+ const name = file.replace(/\.tsx?$/, "");
28
+ // The icons lib carries the Icon manifest: it shadows the icon packages, so the project draws from the vocabulary.
29
+ const manifest = name === "icons" ? manifests.find((m) => m.name === "Icon") : undefined;
30
+ items.push({
31
+ name: `lib-${name}`,
32
+ type: "lib",
33
+ title: name,
34
+ description: LIB_DESCRIPTIONS[name] ?? `${name} helpers from Zengin UI.`,
35
+ dependencies: {},
36
+ devDependencies: {},
37
+ registryDependencies: [],
38
+ files: [{ path: `${LAYOUT.libDir}/${file}`, kind: "lib", content: rewriteComponent(read(join(ui, "src", "internal", file))) }],
39
+ ...(manifest ? { manifest: { ...manifest, export: { from: "@/lib/icons", name: "Icon" } } } : {}),
40
+ });
41
+ }
42
+ items.push({
43
+ name: "foundation",
44
+ type: "definitions",
45
+ title: "Foundation",
46
+ description: "The token definitions (light and dark) and the base stylesheet every component assumes.",
47
+ dependencies: {},
48
+ devDependencies: {},
49
+ registryDependencies: [],
50
+ files: [
51
+ { path: `${LAYOUT.definitionsDir}/tokens.json`, kind: "definitions", content: read(join(ui, "zengin", "tokens.json")) },
52
+ { path: `${LAYOUT.definitionsDir}/tokens.dark.json`, kind: "definitions", content: read(join(ui, "zengin", "tokens.dark.json")) },
53
+ { path: "src/styles/base.css", kind: "style", content: read(join(ui, "src", "styles", "base.css")) },
54
+ { path: "src/styles/chart.css", kind: "style", content: read(join(ui, "src", "styles", "chart.css")) },
55
+ { path: "src/styles/motion.css", kind: "style", content: read(join(ui, "src", "styles", "motion.css")) },
56
+ ],
57
+ });
58
+ const componentsDir = join(ui, "src", "components");
59
+ for (const dir of readdirSync(componentsDir).sort()) {
60
+ const tsxPath = join(componentsDir, dir, `${dir}.tsx`);
61
+ if (!existsSync(tsxPath))
62
+ continue;
63
+ const tsx = read(tsxPath);
64
+ const manifest = manifests.find((m) => kebab(m.name) === dir);
65
+ if (!manifest)
66
+ throw new Error(`packages/ui component "${dir}" has no manifest entry`);
67
+ const dependencies = {};
68
+ for (const m of tsx.matchAll(/from\s+"(@radix-ui\/[^"]+|radix-ui)"/g)) {
69
+ const pkg = m[1];
70
+ const v = uiPkg.dependencies?.[pkg];
71
+ if (!v)
72
+ throw new Error(`packages/ui/package.json has no version for ${pkg}, used by ${dir}`);
73
+ dependencies[pkg] = v;
74
+ }
75
+ const registryDependencies = ["foundation"];
76
+ for (const m of tsx.matchAll(/from\s+"\.\.\/\.\.\/internal\/([\w-]+)\.js"/g))
77
+ registryDependencies.push(`lib-${m[1]}`);
78
+ for (const m of tsx.matchAll(/from\s+"\.\.\/([\w-]+)\/[\w-]+\.js"/g))
79
+ registryDependencies.push(m[1]);
80
+ const files = [
81
+ { path: `${LAYOUT.componentsDir}/${dir}/${dir}.tsx`, kind: "component", content: rewriteComponent(tsx) },
82
+ ];
83
+ const cssPath = join(componentsDir, dir, `${dir}.css`);
84
+ if (existsSync(cssPath))
85
+ files.push({ path: `${LAYOUT.componentsDir}/${dir}/${dir}.css`, kind: "style", content: read(cssPath) });
86
+ const storyPath = join(ui, "stories", `${dir}.stories.tsx`);
87
+ if (existsSync(storyPath))
88
+ files.push({ path: `${LAYOUT.storiesDir}/${dir}.stories.tsx`, kind: "story", content: rewriteStory(read(storyPath)) });
89
+ items.push({
90
+ name: dir,
91
+ type: "component",
92
+ title: manifest.name,
93
+ description: describe(tsx) ?? `${manifest.name} from Zengin UI.`,
94
+ dependencies,
95
+ devDependencies: {},
96
+ registryDependencies,
97
+ files,
98
+ manifest: { ...manifest, export: { ...manifest.export, from: LAYOUT.alias } },
99
+ });
100
+ }
101
+ const componentNames = new Set(items.filter((i) => i.type === "component").map((i) => i.name));
102
+ items.push(templateFrom({
103
+ root: opts.root,
104
+ dir: "examples/blank",
105
+ name: "blank",
106
+ title: "Blank",
107
+ description: "A page with one card and one button, ready to be replaced.",
108
+ rootFiles: ["index.html"],
109
+ componentNames,
110
+ }));
111
+ items.push(templateFrom({
112
+ root: opts.root,
113
+ dir: "examples/marketing-site",
114
+ name: "marketing",
115
+ title: "Marketing page",
116
+ description: "A product page: hero with a live panel, tabs, a rules table, specimens, and a brand as one token file. Archivo, black rules, cobalt.",
117
+ rootFiles: ["index.html", "vercel.json"],
118
+ componentNames,
119
+ }));
120
+ items.push(templateFrom({
121
+ root: opts.root,
122
+ dir: "examples/review-workspace",
123
+ name: "review",
124
+ title: "Review workspace",
125
+ description: "An application workflow: a review queue with filtering, status badges, a confirmation dialog, and a theme toggle.",
126
+ rootFiles: ["index.html", "mock.json"],
127
+ componentNames,
128
+ }));
129
+ items.push(templateFrom({
130
+ root: opts.root,
131
+ dir: "examples/saas",
132
+ name: "saas",
133
+ title: "SaaS dashboard",
134
+ description: "An admin app: overview with stat cards and charts, a customers table with row actions and a detail sheet, billing with quotas, settings that save with a toast. Sidebar, top bar, both schemes.",
135
+ rootFiles: ["index.html", "mock.json"],
136
+ componentNames,
137
+ }));
138
+ items.push(templateFrom({
139
+ root: opts.root,
140
+ dir: "examples/chat",
141
+ name: "chat",
142
+ title: "AI chat",
143
+ description: "An assistant: conversation with streamed markdown, reasoning, tool calls and sources, a prompt with suggestions, a model picker. On the Vercel AI SDK, with a scripted transport so it runs without a key.",
144
+ rootFiles: ["index.html", "mock.json"],
145
+ componentNames,
146
+ }));
147
+ items.push(templateFrom({
148
+ root: opts.root,
149
+ dir: "examples/auth",
150
+ name: "auth",
151
+ title: "Auth",
152
+ description: "Sign in, create account, reset and verify, one card on @zenginui/ui with real validation against mock accounts.",
153
+ rootFiles: ["index.html", "mock.json"],
154
+ componentNames,
155
+ }));
156
+ items.push(templateFrom({
157
+ root: opts.root,
158
+ dir: "examples/docs",
159
+ name: "docs",
160
+ title: "Docs",
161
+ description: "A sidebar of sections, markdown pages with code and tables, an on-this-page outline, search, previous and next, on @zenginui/ui.",
162
+ rootFiles: ["index.html", "mock.json"],
163
+ componentNames,
164
+ }));
165
+ items.push(templateFrom({
166
+ root: opts.root,
167
+ dir: "examples/storefront",
168
+ name: "storefront",
169
+ title: "Storefront",
170
+ description: "A product grid with search, filters and sort, a cart sheet with quantities and totals, and a checkout dialog that places the order, on @zenginui/ui.",
171
+ rootFiles: ["index.html", "mock.json"],
172
+ componentNames,
173
+ }));
174
+ // Themes: one directory each under packages/ui/themes, a theme.json beside a brand.css.
175
+ const themesDir = join(ui, "themes");
176
+ for (const dir of existsSync(themesDir) ? readdirSync(themesDir).sort() : []) {
177
+ const meta = join(themesDir, dir, "theme.json");
178
+ const css = join(themesDir, dir, "brand.css");
179
+ if (!existsSync(meta) || !existsSync(css))
180
+ continue;
181
+ const t = JSON.parse(read(meta));
182
+ // The default theme is the system's own tokens, spelled out, so applying it over any brand is a real reset.
183
+ const content = dir === "default"
184
+ ? `/*\n * The default theme: Zengin UI's own tokens, every one, so that applying it over another brand resets\n * everything. Generated from zengin/tokens.json and tokens.dark.json. Edit freely, or run \`zengin brand\`.\n */\n\n${buildTokensCss(join(ui, "zengin")).css.replace(/^\/\*.*\*\/\n\n/, "")}`
185
+ : read(css);
186
+ items.push({
187
+ name: dir,
188
+ type: "theme",
189
+ title: t.title,
190
+ description: t.description,
191
+ dependencies: {},
192
+ devDependencies: {},
193
+ registryDependencies: [],
194
+ files: [{ path: "src/theme/brand.css", kind: "theme", content }],
195
+ fonts: t.fonts ?? [],
196
+ });
197
+ }
198
+ // Font pairings: one directory each under packages/ui/fonts, a fonts.json naming the three roles.
199
+ const fontsDir = join(ui, "fonts");
200
+ for (const dir of existsSync(fontsDir) ? readdirSync(fontsDir).sort() : []) {
201
+ const meta = join(fontsDir, dir, "fonts.json");
202
+ if (!existsSync(meta))
203
+ continue;
204
+ const f = JSON.parse(read(meta));
205
+ items.push({
206
+ name: `fonts-${dir}`,
207
+ type: "fonts",
208
+ title: f.title,
209
+ description: f.description,
210
+ dependencies: {},
211
+ devDependencies: {},
212
+ registryDependencies: [],
213
+ files: [],
214
+ pairing: { display: f.display, sans: f.sans, mono: f.mono },
215
+ });
216
+ }
217
+ // Icon sets: the vocabulary drawn by a react-icons module. Installing one replaces src/lib/icons.tsx.
218
+ for (const [name, set] of Object.entries(ICON_SETS)) {
219
+ items.push({
220
+ name: `icons-${name}`,
221
+ type: "icons",
222
+ title: set.title,
223
+ description: set.description,
224
+ dependencies: { "react-icons": REACT_ICONS_VERSION },
225
+ devDependencies: {},
226
+ registryDependencies: [],
227
+ files: [{ path: `${LAYOUT.libDir}/icons.tsx`, kind: "lib", content: renderIconsModule(name, set) }],
228
+ iconSet: { module: set.module, names: set.names },
229
+ });
230
+ }
231
+ return { schema: REGISTRY_SCHEMA, name: "zengin", version, generatedAt: new Date().toISOString(), items };
232
+ }
233
+ /** Writes index.json and items/<name>.json so the registry can be served as static files. */
234
+ export function writeRegistry(registry, outDir) {
235
+ mkdirSync(join(outDir, "items"), { recursive: true });
236
+ const index = {
237
+ ...registry,
238
+ items: registry.items.map(({ files: _f, manifest: _m, ...rest }) => rest),
239
+ };
240
+ const written = [join(outDir, "index.json")];
241
+ writeFileSync(written[0], JSON.stringify(index, null, 2) + "\n");
242
+ for (const item of registry.items) {
243
+ const p = join(outDir, "items", `${item.name}.json`);
244
+ writeFileSync(p, JSON.stringify(item, null, 2) + "\n");
245
+ written.push(p);
246
+ }
247
+ return written;
248
+ }
249
+ function templateFrom(opts) {
250
+ const base = join(opts.root, opts.dir);
251
+ const files = [];
252
+ const used = new Set();
253
+ for (const f of opts.rootFiles) {
254
+ const p = join(base, f);
255
+ if (existsSync(p))
256
+ files.push({ path: f, kind: "template", content: read(p) });
257
+ }
258
+ for (const p of walk(join(base, "src"))) {
259
+ const rel = relative(base, p).replace(/\\/g, "/");
260
+ if (rel.startsWith("src/styles/generated/"))
261
+ continue;
262
+ if (rel === "src/preview-theme.ts")
263
+ continue; // the site's preview harness, not part of the template
264
+ let content = read(p);
265
+ if (rel === "src/main.tsx")
266
+ content = content.replace(/import "\.\/preview-theme";\r?\n/, "");
267
+ if (/\.(tsx?|css)$/.test(rel)) {
268
+ for (const m of content.matchAll(/import\s*\{([^}]+)\}\s*from\s*"@zenginui\/ui"/g)) {
269
+ for (const name of m[1].split(",")) {
270
+ const clean = name.replace(/^\s*type\s+/, "").replace(/\s+as\s+\w+\s*$/, "").trim(); // `type X` and `X as Y` both name X
271
+ if (!clean)
272
+ continue;
273
+ const k = kebab(clean);
274
+ if (opts.componentNames.has(k))
275
+ used.add(k);
276
+ }
277
+ }
278
+ content = content.replace(/"@zenginui\/ui\/styles\.css"/g, '"./styles/index.css"').replace(/"@zenginui\/ui"/g, `"${LAYOUT.alias}"`);
279
+ }
280
+ // The entry point must load the brand file, or themes and brands change nothing. The examples that
281
+ // consume the package have no brand file; the project has one.
282
+ if (rel === "src/main.tsx" && !content.includes("theme/brand.css")) {
283
+ content = content.includes('import "./styles/index.css";')
284
+ ? content.replace('import "./styles/index.css";', 'import "./styles/index.css";\nimport "./theme/brand.css";')
285
+ : `import "./theme/brand.css";\n${content}`;
286
+ }
287
+ files.push({ path: rel, kind: "template", content });
288
+ }
289
+ return {
290
+ name: opts.name,
291
+ type: "template",
292
+ title: opts.title,
293
+ description: opts.description,
294
+ dependencies: {},
295
+ devDependencies: {},
296
+ registryDependencies: ["foundation", "lib-cx", ...[...used].sort()],
297
+ files,
298
+ source: opts.dir,
299
+ };
300
+ }
301
+ /** Package-relative imports become project-alias imports; `.js` suffixes on relative imports go. */
302
+ function rewriteComponent(tsx) {
303
+ return tsx.replace(/"\.\.\/\.\.\/internal\/([\w-]+)\.js"/g, '"@/lib/$1"').replace(/from\s+"(\.\.?\/[^"]+)\.js"/g, 'from "$1"');
304
+ }
305
+ function rewriteStory(tsx) {
306
+ return tsx.replace(/from\s+"\.\.\/src"/g, `from "${LAYOUT.alias}"`);
307
+ }
308
+ /** The first doc comment in the file, one paragraph. */
309
+ function describe(tsx) {
310
+ const m = /\/\*\*\s*([\s\S]*?)\*\//.exec(tsx);
311
+ if (!m)
312
+ return undefined;
313
+ const text = m[1]
314
+ .split("\n")
315
+ .map((l) => l.replace(/^\s*\*\s?/, "").trim())
316
+ .filter(Boolean)
317
+ .join(" ");
318
+ return text.length > 220 ? `${text.slice(0, 217)}...` : text;
319
+ }
320
+ function walk(dir) {
321
+ if (!existsSync(dir))
322
+ return [];
323
+ const out = [];
324
+ for (const entry of readdirSync(dir).sort()) {
325
+ const p = join(dir, entry);
326
+ if (statSync(p).isDirectory())
327
+ out.push(...walk(p));
328
+ else
329
+ out.push(p);
330
+ }
331
+ return out;
332
+ }
333
+ function read(p) {
334
+ return readFileSync(p, "utf8").replace(/\r\n/g, "\n");
335
+ }
336
+ export function kebab(name) {
337
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
338
+ }
339
+ export function pascal(name) {
340
+ return name
341
+ .split(/[-_]/)
342
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
343
+ .join("");
344
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Color math for palette derivation: sRGB hex to OKLCH and back, gamut clipping by chroma, and WCAG
3
+ * contrast. OKLCH is used because equal steps in L look equal, which is what a hover or a soft tint needs.
4
+ */
5
+ export interface Oklch {
6
+ l: number;
7
+ c: number;
8
+ h: number;
9
+ }
10
+ export declare function hexToRgb(hex: string): [number, number, number];
11
+ export declare function rgbToHex([r, g, b]: [number, number, number]): string;
12
+ export declare function rgbToOklch([r, g, b]: [number, number, number]): Oklch;
13
+ /** OKLCH to hex, lowering chroma until the color fits sRGB so hue and lightness survive. */
14
+ export declare function oklchToHex(color: Oklch): string;
15
+ export declare function hexToOklch(hex: string): Oklch;
16
+ /** WCAG 2 relative luminance of a hex color. */
17
+ export declare function luminance(hex: string): number;
18
+ /** WCAG 2 contrast ratio, 1 to 21. */
19
+ export declare function contrast(a: string, b: string): number;
20
+ /**
21
+ * Moves `color` in lightness, step by step in `direction`, until it reaches `ratio` against `against`.
22
+ * Returns the hex reached, or the furthest tried when the ratio cannot be met.
23
+ */
24
+ export declare function pushForContrast(color: Oklch, against: string, ratio: number, direction: "darker" | "lighter"): string;
package/dist/color.js ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Color math for palette derivation: sRGB hex to OKLCH and back, gamut clipping by chroma, and WCAG
3
+ * contrast. OKLCH is used because equal steps in L look equal, which is what a hover or a soft tint needs.
4
+ */
5
+ export function hexToRgb(hex) {
6
+ const m = /^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(hex.trim());
7
+ if (!m)
8
+ throw new Error(`Not a hex color: ${hex}`);
9
+ let s = m[1];
10
+ if (s.length === 3)
11
+ s = s.split("").map((ch) => ch + ch).join("");
12
+ return [parseInt(s.slice(0, 2), 16) / 255, parseInt(s.slice(2, 4), 16) / 255, parseInt(s.slice(4, 6), 16) / 255];
13
+ }
14
+ export function rgbToHex([r, g, b]) {
15
+ const to = (v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0").toUpperCase();
16
+ return `#${to(r)}${to(g)}${to(b)}`;
17
+ }
18
+ const toLinear = (v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
19
+ const toGamma = (v) => (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055);
20
+ export function rgbToOklch([r, g, b]) {
21
+ const lr = toLinear(r), lg = toLinear(g), lb = toLinear(b);
22
+ const l_ = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
23
+ const m_ = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
24
+ const s_ = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
25
+ const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
26
+ const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
27
+ const bb = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
28
+ const c = Math.hypot(a, bb);
29
+ let h = (Math.atan2(bb, a) * 180) / Math.PI;
30
+ if (h < 0)
31
+ h += 360;
32
+ return { l: L, c, h: c < 1e-4 ? 0 : h };
33
+ }
34
+ function oklchToLinearRgb({ l, c, h }) {
35
+ const a = c * Math.cos((h * Math.PI) / 180);
36
+ const bb = c * Math.sin((h * Math.PI) / 180);
37
+ const l_ = (l + 0.3963377774 * a + 0.2158037573 * bb) ** 3;
38
+ const m_ = (l - 0.1055613458 * a - 0.0638541728 * bb) ** 3;
39
+ const s_ = (l - 0.0894841775 * a - 1.291485548 * bb) ** 3;
40
+ return [
41
+ 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_,
42
+ -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_,
43
+ -0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_,
44
+ ];
45
+ }
46
+ const inGamut = (rgb) => rgb.every((v) => v >= -0.0005 && v <= 1.0005);
47
+ /** OKLCH to hex, lowering chroma until the color fits sRGB so hue and lightness survive. */
48
+ export function oklchToHex(color) {
49
+ let c = color.c;
50
+ let lin = oklchToLinearRgb({ ...color, c });
51
+ for (let i = 0; i < 24 && !inGamut(lin); i++) {
52
+ c *= 0.9;
53
+ lin = oklchToLinearRgb({ ...color, c });
54
+ }
55
+ if (!inGamut(lin))
56
+ lin = oklchToLinearRgb({ ...color, c: 0 });
57
+ return rgbToHex([toGamma(clamp01(lin[0])), toGamma(clamp01(lin[1])), toGamma(clamp01(lin[2]))]);
58
+ }
59
+ export function hexToOklch(hex) {
60
+ return rgbToOklch(hexToRgb(hex));
61
+ }
62
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
63
+ /** WCAG 2 relative luminance of a hex color. */
64
+ export function luminance(hex) {
65
+ const [r, g, b] = hexToRgb(hex);
66
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
67
+ }
68
+ /** WCAG 2 contrast ratio, 1 to 21. */
69
+ export function contrast(a, b) {
70
+ const la = luminance(a), lb = luminance(b);
71
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
72
+ return (hi + 0.05) / (lo + 0.05);
73
+ }
74
+ /**
75
+ * Moves `color` in lightness, step by step in `direction`, until it reaches `ratio` against `against`.
76
+ * Returns the hex reached, or the furthest tried when the ratio cannot be met.
77
+ */
78
+ export function pushForContrast(color, against, ratio, direction) {
79
+ let l = color.l;
80
+ let hex = oklchToHex({ ...color, l });
81
+ for (let i = 0; i < 60 && contrast(hex, against) < ratio; i++) {
82
+ l += direction === "darker" ? -0.01 : 0.01;
83
+ if (l < 0 || l > 1)
84
+ break;
85
+ hex = oklchToHex({ ...color, l });
86
+ }
87
+ return hex;
88
+ }
@@ -0,0 +1,55 @@
1
+ import { type InstallResult } from "./install.js";
2
+ import type { RegistrySource } from "./load.js";
3
+ export interface CreateOptions {
4
+ /** Directory to create; must not exist or must be empty. */
5
+ dir: string;
6
+ /** Package name; defaults to the directory's basename. */
7
+ name?: string;
8
+ template?: string;
9
+ /** A theme from the registry to apply after the template; the template's own brand file otherwise. */
10
+ theme?: string;
11
+ source: RegistrySource;
12
+ /** Write the Storybook config and the stories that come with the components. Default true. */
13
+ storybook?: boolean;
14
+ /**
15
+ * Path to a Zengin repository checkout. The Zengin packages are then linked with `file:` instead of
16
+ * pulled from npm, which is how the generator is exercised before the first release.
17
+ */
18
+ local?: string;
19
+ /**
20
+ * `vite` (default): an SPA with index.html and src/main.tsx. `next`: the App Router under src/app, the
21
+ * template's App mounted client-side from page.tsx, its head in layout.tsx. Everything else is the same.
22
+ */
23
+ framework?: "vite" | "next";
24
+ }
25
+ export interface CreateResult {
26
+ dir: string;
27
+ name: string;
28
+ template: string;
29
+ framework: "vite" | "next";
30
+ version: string;
31
+ install: InstallResult;
32
+ tokens: {
33
+ light: number;
34
+ dark: number;
35
+ };
36
+ /** The engine on the fresh project. Zero, or something is wrong with the registry. */
37
+ violations: number;
38
+ }
39
+ /** Versions pinned into a generated package.json. One place to bump. */
40
+ export declare const VERSIONS: {
41
+ readonly react: "^19.3.0";
42
+ readonly "react-dom": "^19.3.0";
43
+ readonly "@types/react": "^19.3.0";
44
+ readonly "@types/react-dom": "^19.3.0";
45
+ readonly "@vitejs/plugin-react": "^6.1.1";
46
+ readonly typescript: "^5.9.2";
47
+ readonly vite: "^8.3.0";
48
+ readonly next: "^16.1.0";
49
+ readonly storybook: "^10.6.0";
50
+ readonly "@storybook/react-vite": "^10.6.0";
51
+ readonly "@storybook/addon-docs": "^10.6.0";
52
+ readonly "@storybook/addon-a11y": "^10.6.0";
53
+ readonly zengin: "^0.1.0";
54
+ };
55
+ export declare function createProject(opts: CreateOptions): Promise<CreateResult>;