@ansh-ui/cli 0.2.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/LICENSE +21 -0
- package/dist/cli.js +680 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +161 -0
- package/dist/index.js +371 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ansh contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'util';
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
4
|
+
import { join, dirname, resolve } from 'path';
|
|
5
|
+
import { parseColor, generatePalette, rgbToOklch, toOklchString, toHex, createGradient, createScale, checkContrast, harmony, effects, behaviours } from '@ansh-ui/core';
|
|
6
|
+
|
|
7
|
+
function findRoot(cwd) {
|
|
8
|
+
let current = resolve(cwd);
|
|
9
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
10
|
+
if (existsSync(join(current, "package.json"))) return current;
|
|
11
|
+
const parent = resolve(current, "..");
|
|
12
|
+
if (parent === current) break;
|
|
13
|
+
current = parent;
|
|
14
|
+
}
|
|
15
|
+
return resolve(cwd);
|
|
16
|
+
}
|
|
17
|
+
function readJson(path) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function detectProject(cwd = process.cwd()) {
|
|
25
|
+
const root = findRoot(cwd);
|
|
26
|
+
const pkg = readJson(join(root, "package.json")) ?? {};
|
|
27
|
+
const deps = {
|
|
28
|
+
...pkg.dependencies ?? {},
|
|
29
|
+
...pkg.devDependencies ?? {}
|
|
30
|
+
};
|
|
31
|
+
const framework = deps.nuxt ? "nuxt" : deps.next ? "next" : deps.vue ? "vue" : deps.react ? "react" : "unknown";
|
|
32
|
+
const typescript = Boolean(deps.typescript) || existsSync(join(root, "tsconfig.json"));
|
|
33
|
+
const packageManager = existsSync(join(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock")) ? "bun" : existsSync(join(root, "yarn.lock")) ? "yarn" : "npm";
|
|
34
|
+
return {
|
|
35
|
+
root,
|
|
36
|
+
framework,
|
|
37
|
+
typescript,
|
|
38
|
+
componentsDir: detectComponentsDir(root, framework),
|
|
39
|
+
alias: detectAlias(root),
|
|
40
|
+
packageManager,
|
|
41
|
+
installed: Boolean(
|
|
42
|
+
deps["@ansh-ui/react"] || deps["@ansh-ui/vue"] || deps["@ansh-ui/nuxt"] || deps["@ansh-ui/core"]
|
|
43
|
+
)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function detectComponentsDir(root, framework) {
|
|
47
|
+
const candidates = framework === "nuxt" ? ["components", "app/components", "src/components"] : ["src/components", "components", "app/components", "src/lib/components", "app/ui", "src/ui"];
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
if (existsSync(join(root, candidate))) return candidate;
|
|
50
|
+
}
|
|
51
|
+
if (existsSync(join(root, "src"))) return "src/components";
|
|
52
|
+
if (framework === "nuxt") return "components";
|
|
53
|
+
return "components";
|
|
54
|
+
}
|
|
55
|
+
function detectAlias(root) {
|
|
56
|
+
for (const file of ["tsconfig.json", "jsconfig.json"]) {
|
|
57
|
+
const config = readJson(join(root, file));
|
|
58
|
+
if (!config) continue;
|
|
59
|
+
const compilerOptions = config.compilerOptions;
|
|
60
|
+
const paths = compilerOptions?.paths;
|
|
61
|
+
if (!paths) continue;
|
|
62
|
+
for (const pattern of Object.keys(paths)) {
|
|
63
|
+
if (pattern.endsWith("/*")) return pattern.slice(0, -1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function installCommand(info, packages) {
|
|
69
|
+
const list = packages.join(" ");
|
|
70
|
+
switch (info.packageManager) {
|
|
71
|
+
case "pnpm":
|
|
72
|
+
return `pnpm add ${list}`;
|
|
73
|
+
case "yarn":
|
|
74
|
+
return `yarn add ${list}`;
|
|
75
|
+
case "bun":
|
|
76
|
+
return `bun add ${list}`;
|
|
77
|
+
default:
|
|
78
|
+
return `npm install ${list}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
var COMPONENT_NAMES = {
|
|
82
|
+
aurora: "AuroraBackground",
|
|
83
|
+
"mesh-gradient": "MeshGradient",
|
|
84
|
+
"animated-gradient": "AnimatedGradient",
|
|
85
|
+
"particle-field": "ParticleField",
|
|
86
|
+
constellation: "Constellation",
|
|
87
|
+
meteors: "MeteorShower",
|
|
88
|
+
rain: "Rain",
|
|
89
|
+
snow: "Snow",
|
|
90
|
+
fireflies: "Fireflies",
|
|
91
|
+
waves: "Waves",
|
|
92
|
+
blob: "LiquidBlob",
|
|
93
|
+
"noise-layer": "NoiseLayer",
|
|
94
|
+
spotlight: "Spotlight",
|
|
95
|
+
magnetic: "MagneticButton",
|
|
96
|
+
tilt: "TiltCard",
|
|
97
|
+
ripple: "Ripple",
|
|
98
|
+
marquee: "Marquee",
|
|
99
|
+
"scroll-reveal": "ScrollReveal",
|
|
100
|
+
parallax: "Parallax",
|
|
101
|
+
"split-text": "SplitText"
|
|
102
|
+
};
|
|
103
|
+
function componentNameFor(name) {
|
|
104
|
+
return COMPONENT_NAMES[name] ?? name.replace(/(^|-)([a-z])/g, (_, __, letter) => letter.toUpperCase());
|
|
105
|
+
}
|
|
106
|
+
function listCatalogue(filter) {
|
|
107
|
+
const entries = [];
|
|
108
|
+
if (filter !== "behaviour") {
|
|
109
|
+
for (const [name, definition] of Object.entries(effects)) {
|
|
110
|
+
entries.push({
|
|
111
|
+
name,
|
|
112
|
+
kind: "effect",
|
|
113
|
+
component: componentNameFor(name),
|
|
114
|
+
options: describeOptions(definition.defaults)
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (filter !== "effect") {
|
|
119
|
+
for (const [name, definition] of Object.entries(behaviours)) {
|
|
120
|
+
entries.push({
|
|
121
|
+
name,
|
|
122
|
+
kind: "behaviour",
|
|
123
|
+
component: componentNameFor(name),
|
|
124
|
+
options: describeOptions(definition.defaults)
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
129
|
+
}
|
|
130
|
+
function describeOptions(defaults) {
|
|
131
|
+
return Object.entries(defaults).map(([name, value]) => ({ name, default: value })).sort((a, b) => a.name.localeCompare(b.name));
|
|
132
|
+
}
|
|
133
|
+
function findEntry(query) {
|
|
134
|
+
const needle = query.toLowerCase();
|
|
135
|
+
return listCatalogue().find(
|
|
136
|
+
(entry) => entry.name.toLowerCase() === needle || entry.component.toLowerCase() === needle
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/commands/add.ts
|
|
141
|
+
function addComponent(options) {
|
|
142
|
+
const project = detectProject(options.cwd);
|
|
143
|
+
const entry = findEntry(options.name);
|
|
144
|
+
if (!entry) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Unknown component "${options.name}". Run \`ansh list\` to see what's available.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (project.framework === "unknown") {
|
|
150
|
+
throw new Error(
|
|
151
|
+
"Could not detect a React, Next, Vue or Nuxt project. Run this from your project root, or install the framework first."
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const isVue = project.framework === "vue" || project.framework === "nuxt";
|
|
155
|
+
const componentName = options.as ?? entry.component;
|
|
156
|
+
const extension = isVue ? "vue" : project.typescript ? "tsx" : "jsx";
|
|
157
|
+
const directory = options.dir ?? project.componentsDir;
|
|
158
|
+
const path = join(project.root, directory, `${componentName}.${extension}`);
|
|
159
|
+
const contents = isVue ? renderVue(componentName, entry.component, entry.options, project) : renderReact(componentName, entry.component, entry.options, project);
|
|
160
|
+
const missingDependencies = missingFor(project);
|
|
161
|
+
if (options.dryRun) {
|
|
162
|
+
return { path, contents, written: false, missingDependencies };
|
|
163
|
+
}
|
|
164
|
+
if (existsSync(path) && !options.force) {
|
|
165
|
+
throw new Error(`${path} already exists. Pass --force to overwrite.`);
|
|
166
|
+
}
|
|
167
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
168
|
+
writeFileSync(path, contents, "utf8");
|
|
169
|
+
return { path, contents, written: true, missingDependencies };
|
|
170
|
+
}
|
|
171
|
+
function missingFor(project) {
|
|
172
|
+
if (project.installed) return [];
|
|
173
|
+
switch (project.framework) {
|
|
174
|
+
case "nuxt":
|
|
175
|
+
return ["@ansh-ui/nuxt"];
|
|
176
|
+
case "vue":
|
|
177
|
+
return ["@ansh-ui/vue", "@ansh-ui/tokens"];
|
|
178
|
+
default:
|
|
179
|
+
return ["@ansh-ui/react", "@ansh-ui/tokens"];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function literal(value) {
|
|
183
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
184
|
+
if (Array.isArray(value)) return `[${value.map(literal).join(", ")}]`;
|
|
185
|
+
if (value === null) return "null";
|
|
186
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
187
|
+
return String(value);
|
|
188
|
+
}
|
|
189
|
+
var INTERESTING_OPTIONS = /* @__PURE__ */ new Set([
|
|
190
|
+
"colors",
|
|
191
|
+
"speed",
|
|
192
|
+
"intensity",
|
|
193
|
+
"opacity",
|
|
194
|
+
"blur",
|
|
195
|
+
"seed",
|
|
196
|
+
"strength",
|
|
197
|
+
"radius",
|
|
198
|
+
"max",
|
|
199
|
+
"distance",
|
|
200
|
+
"amount",
|
|
201
|
+
"count"
|
|
202
|
+
]);
|
|
203
|
+
function pickDefaults(options) {
|
|
204
|
+
return options.filter((option) => INTERESTING_OPTIONS.has(option.name));
|
|
205
|
+
}
|
|
206
|
+
function renderReact(componentName, sourceComponent, options, project) {
|
|
207
|
+
const ts = project.typescript;
|
|
208
|
+
const defaults = pickDefaults(options);
|
|
209
|
+
const defaultLines = defaults.map((option) => ` ${option.name}: ${literal(option.default)},`).join("\n");
|
|
210
|
+
const propsType = ts ? `
|
|
211
|
+
type ${componentName}Props = ComponentProps<typeof ${sourceComponent}>
|
|
212
|
+
` : "";
|
|
213
|
+
const signature = ts ? `export function ${componentName}({ children, ...props }: ${componentName}Props) {` : `export function ${componentName}({ children, ...props }) {`;
|
|
214
|
+
const typeImport = ts ? `import type { ComponentProps } from 'react'
|
|
215
|
+
` : "";
|
|
216
|
+
return `${typeImport}import { ${sourceComponent} } from '@ansh-ui/react'
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* ${componentName} \u2014 a project-local wrapper around Ansh's ${sourceComponent}.
|
|
220
|
+
*
|
|
221
|
+
* Edit the defaults below to match your design system. Every option remains overridable per
|
|
222
|
+
* instance, because props spread after the defaults.
|
|
223
|
+
*
|
|
224
|
+
* Generated by \`ansh add\`. Safe to edit \u2014 it will not be regenerated unless you ask.
|
|
225
|
+
*/
|
|
226
|
+
const defaults = {
|
|
227
|
+
${defaultLines}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
${propsType}
|
|
231
|
+
${signature}
|
|
232
|
+
return (
|
|
233
|
+
<${sourceComponent} {...defaults} {...props}>
|
|
234
|
+
{children}
|
|
235
|
+
</${sourceComponent}>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
`;
|
|
239
|
+
}
|
|
240
|
+
function renderVue(componentName, sourceComponent, options, project) {
|
|
241
|
+
const defaults = pickDefaults(options);
|
|
242
|
+
const defaultLines = defaults.map((option) => ` ${option.name}: ${literal(option.default)},`).join("\n");
|
|
243
|
+
const lang = project.typescript ? ' lang="ts"' : "";
|
|
244
|
+
const importLine = project.framework === "nuxt" ? `// ${sourceComponent} is auto-imported by @ansh-ui/nuxt.` : `import { ${sourceComponent} } from '@ansh-ui/vue'`;
|
|
245
|
+
return `<script setup${lang}>
|
|
246
|
+
${importLine}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* ${componentName} \u2014 a project-local wrapper around Ansh's ${sourceComponent}.
|
|
250
|
+
*
|
|
251
|
+
* Edit the defaults below to match your design system. Every option remains overridable per
|
|
252
|
+
* instance, because \`$attrs\` is bound after the defaults.
|
|
253
|
+
*
|
|
254
|
+
* Generated by \`ansh add\`. Safe to edit \u2014 it will not be regenerated unless you ask.
|
|
255
|
+
*/
|
|
256
|
+
const defaults = {
|
|
257
|
+
${defaultLines}
|
|
258
|
+
}
|
|
259
|
+
</script>
|
|
260
|
+
|
|
261
|
+
<template>
|
|
262
|
+
<${sourceComponent} v-bind="{ ...defaults, ...$attrs }">
|
|
263
|
+
<slot />
|
|
264
|
+
</${sourceComponent}>
|
|
265
|
+
</template>
|
|
266
|
+
`;
|
|
267
|
+
}
|
|
268
|
+
function generatePaletteReport(options = {}) {
|
|
269
|
+
const parsed = options.colors && options.colors.length > 0 ? options.colors.map((color) => parseColor(color)) : generatePalette({
|
|
270
|
+
count: options.count ?? 5,
|
|
271
|
+
seed: options.seed ?? 1,
|
|
272
|
+
lightness: options.lightness ?? 0.65,
|
|
273
|
+
saturation: options.saturation ?? 0.85,
|
|
274
|
+
spread: options.spread ?? 120
|
|
275
|
+
});
|
|
276
|
+
const colors = parsed.map((color) => {
|
|
277
|
+
const lch = rgbToOklch(color);
|
|
278
|
+
return {
|
|
279
|
+
hex: toHex(color),
|
|
280
|
+
oklch: toOklchString(color),
|
|
281
|
+
lightness: Number(lch.l.toFixed(4)),
|
|
282
|
+
chroma: Number(lch.c.toFixed(4)),
|
|
283
|
+
hue: Number(lch.h.toFixed(2))
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
const gradientSamples = options.gradient === false || parsed.length < 2 ? [] : createGradient(parsed, { space: "oklch" }).sample(9).map((color) => toHex(color));
|
|
287
|
+
const snippet = `colors={[${colors.map((c) => `'${c.hex}'`).join(", ")}]}`;
|
|
288
|
+
return { colors, gradientSamples, snippet };
|
|
289
|
+
}
|
|
290
|
+
var TAILWIND_KEYS = ["50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950"];
|
|
291
|
+
function generateTheme(options) {
|
|
292
|
+
const base = parseColor(options.color);
|
|
293
|
+
const baseLightness = rgbToOklch(base).l;
|
|
294
|
+
const scale = createScale(base, {
|
|
295
|
+
steps: 11,
|
|
296
|
+
hueShift: options.hueShift ?? 0,
|
|
297
|
+
chromaFalloff: options.chromaFalloff ?? 0.35
|
|
298
|
+
});
|
|
299
|
+
const unsafeOnLight = [];
|
|
300
|
+
const unsafeOnDark = [];
|
|
301
|
+
const lines = [];
|
|
302
|
+
let nearestStep = "500";
|
|
303
|
+
let nearestDistance = Infinity;
|
|
304
|
+
scale.colors.forEach((color, index) => {
|
|
305
|
+
const key = TAILWIND_KEYS[index];
|
|
306
|
+
const onLight = checkContrast(color, "#ffffff");
|
|
307
|
+
const onDark = checkContrast(color, "#0b0b0f");
|
|
308
|
+
if (!onLight.passes) unsafeOnLight.push(key);
|
|
309
|
+
if (!onDark.passes) unsafeOnDark.push(key);
|
|
310
|
+
const distance = Math.abs(rgbToOklch(color).l - baseLightness);
|
|
311
|
+
if (distance < nearestDistance) {
|
|
312
|
+
nearestDistance = distance;
|
|
313
|
+
nearestStep = key;
|
|
314
|
+
}
|
|
315
|
+
lines.push(
|
|
316
|
+
` --ansh-brand-${key}: ${toOklchString(color)}; /* ${toHex(color)} on-white ${onLight.ratio.toFixed(2)}:1 on-black ${onDark.ratio.toFixed(2)}:1 */`
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
let accentLines = "";
|
|
320
|
+
if (options.withAccent) {
|
|
321
|
+
const [, accent] = harmony(base, "splitComplementary");
|
|
322
|
+
const accentScale = createScale(accent, { steps: 11 });
|
|
323
|
+
accentLines = "\n /* Accent \u2014 split-complementary partner to the brand hue. */\n" + accentScale.colors.map((color, index) => ` --ansh-accent-${TAILWIND_KEYS[index]}: ${toOklchString(color)};`).join("\n") + "\n";
|
|
324
|
+
}
|
|
325
|
+
const css = `/**
|
|
326
|
+
* Ansh theme override.
|
|
327
|
+
*
|
|
328
|
+
* Generated by \`ansh theme ${options.color}\`.
|
|
329
|
+
* Import this *after* \`@ansh-ui/tokens/ansh.css\` so it wins in the cascade.
|
|
330
|
+
*
|
|
331
|
+
* Contrast ratios are annotated per step. Steps marked unsafe cannot legally hold body text on the
|
|
332
|
+
* corresponding surface \u2014 use them for fills, borders and large display type instead.
|
|
333
|
+
*/
|
|
334
|
+
|
|
335
|
+
:root {
|
|
336
|
+
${lines.join("\n")}
|
|
337
|
+
${accentLines}
|
|
338
|
+
/* Wire the semantic accent tokens to the generated scale. */
|
|
339
|
+
--ansh-accent-subtle: var(--ansh-brand-50);
|
|
340
|
+
--ansh-accent-muted: var(--ansh-brand-100);
|
|
341
|
+
--ansh-accent-default: var(--ansh-brand-600);
|
|
342
|
+
--ansh-accent-hover: var(--ansh-brand-700);
|
|
343
|
+
--ansh-accent-active: var(--ansh-brand-800);
|
|
344
|
+
--ansh-accent-text: var(--ansh-brand-700);
|
|
345
|
+
--ansh-border-focus: var(--ansh-brand-600);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Dark theme.
|
|
350
|
+
*
|
|
351
|
+
* Not an inversion. Accents step *lighter* rather than mirroring their light-mode index, because
|
|
352
|
+
* saturated colour reads as less vivid against a dark background (Helmholtz\u2013Kohlrausch).
|
|
353
|
+
*/
|
|
354
|
+
[data-theme='dark'] {
|
|
355
|
+
--ansh-accent-subtle: color-mix(in oklab, var(--ansh-brand-950) 40%, transparent);
|
|
356
|
+
--ansh-accent-muted: var(--ansh-brand-950);
|
|
357
|
+
--ansh-accent-default: var(--ansh-brand-500);
|
|
358
|
+
--ansh-accent-hover: var(--ansh-brand-400);
|
|
359
|
+
--ansh-accent-active: var(--ansh-brand-300);
|
|
360
|
+
--ansh-accent-text: var(--ansh-brand-300);
|
|
361
|
+
--ansh-border-focus: var(--ansh-brand-400);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
@media (prefers-color-scheme: dark) {
|
|
365
|
+
:root:not([data-theme]) {
|
|
366
|
+
--ansh-accent-subtle: color-mix(in oklab, var(--ansh-brand-950) 40%, transparent);
|
|
367
|
+
--ansh-accent-muted: var(--ansh-brand-950);
|
|
368
|
+
--ansh-accent-default: var(--ansh-brand-500);
|
|
369
|
+
--ansh-accent-hover: var(--ansh-brand-400);
|
|
370
|
+
--ansh-accent-active: var(--ansh-brand-300);
|
|
371
|
+
--ansh-accent-text: var(--ansh-brand-300);
|
|
372
|
+
--ansh-border-focus: var(--ansh-brand-400);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
`;
|
|
376
|
+
if (options.out) {
|
|
377
|
+
mkdirSync(dirname(options.out), { recursive: true });
|
|
378
|
+
writeFileSync(options.out, css, "utf8");
|
|
379
|
+
}
|
|
380
|
+
return { css, unsafeOnLight, unsafeOnDark, nearestStep };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/cli.ts
|
|
384
|
+
var useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
385
|
+
var paint = (code, text) => useColor ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
386
|
+
var bold = (text) => paint("1", text);
|
|
387
|
+
var dim = (text) => paint("2", text);
|
|
388
|
+
var cyan = (text) => paint("36", text);
|
|
389
|
+
var green = (text) => paint("32", text);
|
|
390
|
+
var yellow = (text) => paint("33", text);
|
|
391
|
+
var red = (text) => paint("31", text);
|
|
392
|
+
var VERSION = "0.1.0";
|
|
393
|
+
function help() {
|
|
394
|
+
console.log(`
|
|
395
|
+
${bold("ansh")} ${dim(`v${VERSION}`)} \u2014 Motion, refined.
|
|
396
|
+
|
|
397
|
+
${bold("USAGE")}
|
|
398
|
+
ansh <command> [options]
|
|
399
|
+
|
|
400
|
+
${bold("COMMANDS")}
|
|
401
|
+
${cyan("list")} [--effects|--behaviours] List the component catalogue
|
|
402
|
+
${cyan("info")} <name> Show a component's full option surface
|
|
403
|
+
${cyan("add")} <name> [--as <Name>] Generate a local wrapper component
|
|
404
|
+
${cyan("theme")} <color> [--out <file>] Generate a token override stylesheet
|
|
405
|
+
${cyan("palette")} [colors...] Generate or inspect a palette
|
|
406
|
+
${cyan("doctor")} Report the detected project setup
|
|
407
|
+
|
|
408
|
+
${bold("EXAMPLES")}
|
|
409
|
+
${dim("# See what is available")}
|
|
410
|
+
ansh list
|
|
411
|
+
|
|
412
|
+
${dim("# Generate a HeroBackground wrapper around the aurora effect")}
|
|
413
|
+
ansh add aurora --as HeroBackground
|
|
414
|
+
|
|
415
|
+
${dim("# Build a theme from a brand colour, with contrast annotations")}
|
|
416
|
+
ansh theme '#0275f4' --out src/styles/theme.css --with-accent
|
|
417
|
+
|
|
418
|
+
${dim("# Inspect how a palette interpolates")}
|
|
419
|
+
ansh palette '#0275f4' '#9f0550'
|
|
420
|
+
|
|
421
|
+
${dim("# Reproducible generated palette")}
|
|
422
|
+
ansh palette --count 5 --seed hero-section
|
|
423
|
+
`);
|
|
424
|
+
}
|
|
425
|
+
function fail(message, hint) {
|
|
426
|
+
console.error(`${red("error")} ${message}`);
|
|
427
|
+
if (hint) console.error(`${dim("hint")} ${hint}`);
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
function commandList(argv) {
|
|
431
|
+
const { values } = parseArgs({
|
|
432
|
+
args: argv,
|
|
433
|
+
options: {
|
|
434
|
+
effects: { type: "boolean", default: false },
|
|
435
|
+
behaviours: { type: "boolean", default: false },
|
|
436
|
+
json: { type: "boolean", default: false }
|
|
437
|
+
},
|
|
438
|
+
allowPositionals: true
|
|
439
|
+
});
|
|
440
|
+
const filter = values.effects ? "effect" : values.behaviours ? "behaviour" : void 0;
|
|
441
|
+
const entries = listCatalogue(filter);
|
|
442
|
+
if (values.json) {
|
|
443
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const effects2 = entries.filter((entry) => entry.kind === "effect");
|
|
447
|
+
const behaviours2 = entries.filter((entry) => entry.kind === "behaviour");
|
|
448
|
+
const print = (title, list) => {
|
|
449
|
+
if (list.length === 0) return;
|
|
450
|
+
console.log(`
|
|
451
|
+
${bold(title)}`);
|
|
452
|
+
for (const entry of list) {
|
|
453
|
+
console.log(
|
|
454
|
+
` ${cyan(entry.name.padEnd(20))} ${entry.component.padEnd(20)} ` + dim(`${entry.options.length} options`)
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
print(`Effects ${dim("(canvas)")}`, effects2);
|
|
459
|
+
print(`Behaviours ${dim("(DOM)")}`, behaviours2);
|
|
460
|
+
console.log(`
|
|
461
|
+
${dim(`${entries.length} total. Run \`ansh info <name>\` for details.`)}
|
|
462
|
+
`);
|
|
463
|
+
}
|
|
464
|
+
function commandInfo(argv) {
|
|
465
|
+
const name = argv[0];
|
|
466
|
+
if (!name) fail("Missing component name.", "ansh info aurora");
|
|
467
|
+
const entry = findEntry(name);
|
|
468
|
+
if (!entry) fail(`Unknown component "${name}".`, "Run `ansh list` to see what is available.");
|
|
469
|
+
console.log(`
|
|
470
|
+
${bold(entry.component)} ${dim(`(${entry.kind} "${entry.name}")`)}
|
|
471
|
+
`);
|
|
472
|
+
console.log(bold(" OPTIONS"));
|
|
473
|
+
const width = Math.max(...entry.options.map((option) => option.name.length));
|
|
474
|
+
for (const option of entry.options) {
|
|
475
|
+
const value = typeof option.default === "string" ? `'${option.default}'` : Array.isArray(option.default) ? `[${option.default.map((v) => typeof v === "string" ? `'${v}'` : String(v)).join(", ")}]` : String(option.default);
|
|
476
|
+
console.log(` ${option.name.padEnd(width)} ${dim("=")} ${green(value)}`);
|
|
477
|
+
}
|
|
478
|
+
console.log(
|
|
479
|
+
`
|
|
480
|
+
${dim(` Docs: https://ansh.dev/docs/components/${entry.name}`)}
|
|
481
|
+
${dim(` Add: ansh add ${entry.name}`)}
|
|
482
|
+
`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
function commandAdd(argv) {
|
|
486
|
+
const { values, positionals } = parseArgs({
|
|
487
|
+
args: argv,
|
|
488
|
+
options: {
|
|
489
|
+
as: { type: "string" },
|
|
490
|
+
dir: { type: "string" },
|
|
491
|
+
force: { type: "boolean", default: false },
|
|
492
|
+
"dry-run": { type: "boolean", default: false }
|
|
493
|
+
},
|
|
494
|
+
allowPositionals: true
|
|
495
|
+
});
|
|
496
|
+
const name = positionals[0];
|
|
497
|
+
if (!name) fail("Missing component name.", "ansh add aurora --as HeroBackground");
|
|
498
|
+
try {
|
|
499
|
+
const result = addComponent({
|
|
500
|
+
name,
|
|
501
|
+
as: values.as,
|
|
502
|
+
dir: values.dir,
|
|
503
|
+
force: values.force,
|
|
504
|
+
dryRun: values["dry-run"]
|
|
505
|
+
});
|
|
506
|
+
if (!result.written) {
|
|
507
|
+
console.log(`${dim(`--- ${result.path} ---`)}
|
|
508
|
+
`);
|
|
509
|
+
console.log(result.contents);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
console.log(`${green("created")} ${result.path}`);
|
|
513
|
+
if (result.missingDependencies.length > 0) {
|
|
514
|
+
const project = detectProject();
|
|
515
|
+
console.log(
|
|
516
|
+
`
|
|
517
|
+
${yellow("next")} Install the runtime packages:
|
|
518
|
+
${installCommand(project, result.missingDependencies)}`
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
} catch (error) {
|
|
522
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function commandTheme(argv) {
|
|
526
|
+
const { values, positionals } = parseArgs({
|
|
527
|
+
args: argv,
|
|
528
|
+
options: {
|
|
529
|
+
out: { type: "string" },
|
|
530
|
+
"hue-shift": { type: "string" },
|
|
531
|
+
"chroma-falloff": { type: "string" },
|
|
532
|
+
"with-accent": { type: "boolean", default: false }
|
|
533
|
+
},
|
|
534
|
+
allowPositionals: true
|
|
535
|
+
});
|
|
536
|
+
const color = positionals[0];
|
|
537
|
+
if (!color) fail("Missing brand colour.", "ansh theme '#0275f4' --out theme.css");
|
|
538
|
+
try {
|
|
539
|
+
const report = generateTheme({
|
|
540
|
+
color,
|
|
541
|
+
out: values.out,
|
|
542
|
+
hueShift: values["hue-shift"] ? Number(values["hue-shift"]) : void 0,
|
|
543
|
+
chromaFalloff: values["chroma-falloff"] ? Number(values["chroma-falloff"]) : void 0,
|
|
544
|
+
withAccent: values["with-accent"]
|
|
545
|
+
});
|
|
546
|
+
if (!values.out) {
|
|
547
|
+
console.log(report.css);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
console.log(`${green("created")} ${values.out}`);
|
|
551
|
+
console.log(`${dim("nearest")} your input is closest to step ${report.nearestStep}`);
|
|
552
|
+
if (report.unsafeOnLight.length > 0) {
|
|
553
|
+
console.log(
|
|
554
|
+
`${yellow("warn")} steps ${report.unsafeOnLight.join(", ")} are below 4.5:1 on white \u2014 use them for fills and borders, not body text`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
if (report.unsafeOnDark.length > 0) {
|
|
558
|
+
console.log(
|
|
559
|
+
`${yellow("warn")} steps ${report.unsafeOnDark.join(", ")} are below 4.5:1 on near-black`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
} catch (error) {
|
|
563
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function commandPalette(argv) {
|
|
567
|
+
const { values, positionals } = parseArgs({
|
|
568
|
+
args: argv,
|
|
569
|
+
options: {
|
|
570
|
+
count: { type: "string" },
|
|
571
|
+
seed: { type: "string" },
|
|
572
|
+
lightness: { type: "string" },
|
|
573
|
+
saturation: { type: "string" },
|
|
574
|
+
spread: { type: "string" },
|
|
575
|
+
json: { type: "boolean", default: false }
|
|
576
|
+
},
|
|
577
|
+
allowPositionals: true
|
|
578
|
+
});
|
|
579
|
+
try {
|
|
580
|
+
const report = generatePaletteReport({
|
|
581
|
+
colors: positionals.length > 0 ? positionals : void 0,
|
|
582
|
+
count: values.count ? Number(values.count) : void 0,
|
|
583
|
+
seed: values.seed,
|
|
584
|
+
lightness: values.lightness ? Number(values.lightness) : void 0,
|
|
585
|
+
saturation: values.saturation ? Number(values.saturation) : void 0,
|
|
586
|
+
spread: values.spread ? Number(values.spread) : void 0
|
|
587
|
+
});
|
|
588
|
+
if (values.json) {
|
|
589
|
+
console.log(JSON.stringify(report, null, 2));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
console.log(`
|
|
593
|
+
${bold("PALETTE")}
|
|
594
|
+
`);
|
|
595
|
+
for (const color of report.colors) {
|
|
596
|
+
console.log(
|
|
597
|
+
` ${swatch(color.hex)} ${color.hex.padEnd(9)} ${dim(color.oklch.padEnd(28))} ` + dim(
|
|
598
|
+
`L ${color.lightness.toFixed(3)} C ${color.chroma.toFixed(3)} H ${color.hue.toFixed(1)}`
|
|
599
|
+
)
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (report.gradientSamples.length > 0) {
|
|
603
|
+
console.log(`
|
|
604
|
+
${bold("GRADIENT")} ${dim("(OKLCH interpolation)")}
|
|
605
|
+
`);
|
|
606
|
+
console.log(" " + report.gradientSamples.map((hex) => swatch(hex)).join(""));
|
|
607
|
+
console.log(" " + dim(report.gradientSamples.join(" ")));
|
|
608
|
+
}
|
|
609
|
+
console.log(`
|
|
610
|
+
${bold("SNIPPET")}
|
|
611
|
+
|
|
612
|
+
${green(report.snippet)}
|
|
613
|
+
`);
|
|
614
|
+
} catch (error) {
|
|
615
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function commandDoctor() {
|
|
619
|
+
const project = detectProject();
|
|
620
|
+
console.log(`
|
|
621
|
+
${bold("PROJECT")}
|
|
622
|
+
`);
|
|
623
|
+
console.log(` root ${project.root}`);
|
|
624
|
+
console.log(
|
|
625
|
+
` framework ${project.framework === "unknown" ? yellow("not detected") : green(project.framework)}`
|
|
626
|
+
);
|
|
627
|
+
console.log(` typescript ${project.typescript ? green("yes") : dim("no")}`);
|
|
628
|
+
console.log(` package manager ${project.packageManager}`);
|
|
629
|
+
console.log(` components dir ${project.componentsDir}`);
|
|
630
|
+
console.log(` import alias ${project.alias ?? dim("none")}`);
|
|
631
|
+
console.log(` ansh ${project.installed ? green("installed") : yellow("not installed")}`);
|
|
632
|
+
if (!project.installed) {
|
|
633
|
+
const packages = project.framework === "nuxt" ? ["@ansh-ui/nuxt"] : project.framework === "vue" ? ["@ansh-ui/vue", "@ansh-ui/tokens"] : ["@ansh-ui/react", "@ansh-ui/tokens"];
|
|
634
|
+
console.log(`
|
|
635
|
+
${yellow("next")} ${installCommand(project, packages)}
|
|
636
|
+
`);
|
|
637
|
+
} else {
|
|
638
|
+
console.log("");
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function swatch(hex) {
|
|
642
|
+
if (!useColor) return "\u25A0";
|
|
643
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
644
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
645
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
646
|
+
return `\x1B[48;2;${r};${g};${b}m \x1B[0m`;
|
|
647
|
+
}
|
|
648
|
+
function main() {
|
|
649
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
650
|
+
switch (command) {
|
|
651
|
+
case "list":
|
|
652
|
+
case "ls":
|
|
653
|
+
return commandList(rest);
|
|
654
|
+
case "info":
|
|
655
|
+
case "show":
|
|
656
|
+
return commandInfo(rest);
|
|
657
|
+
case "add":
|
|
658
|
+
return commandAdd(rest);
|
|
659
|
+
case "theme":
|
|
660
|
+
return commandTheme(rest);
|
|
661
|
+
case "palette":
|
|
662
|
+
return commandPalette(rest);
|
|
663
|
+
case "doctor":
|
|
664
|
+
return commandDoctor();
|
|
665
|
+
case "--version":
|
|
666
|
+
case "-v":
|
|
667
|
+
console.log(VERSION);
|
|
668
|
+
return;
|
|
669
|
+
case void 0:
|
|
670
|
+
case "help":
|
|
671
|
+
case "--help":
|
|
672
|
+
case "-h":
|
|
673
|
+
return help();
|
|
674
|
+
default:
|
|
675
|
+
fail(`Unknown command "${command}".`, "Run `ansh help` to see available commands.");
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
main();
|
|
679
|
+
//# sourceMappingURL=cli.js.map
|
|
680
|
+
//# sourceMappingURL=cli.js.map
|