synthesisui 0.1.24 → 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.
@@ -1,27 +1,70 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { resolveRegistry } from "../config.js";
4
- import { fetchComponent } from "../registry.js";
3
+ import { generateComponentFiles } from "../component-codegen.js";
4
+ import { readProjectConfig, resolveRegistry } from "../config.js";
5
+ import { fetchComponent, RegistryError } from "../registry.js";
6
+ /** Slugs/names are kebab-case by contract; reject anything else before it ever
7
+ * reaches a filesystem path (defense-in-depth against `../` traversal). */
8
+ const SAFE_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
5
9
  /**
6
10
  * Brings ONE component from a design system into the project (granular "bring
7
- * specific", INS-18 fatia 3) - its recipe + compiled CSS, written under
8
- * `_synthesisui/ds/<slug>/components/`. Handy for a component you refit/created
9
- * on the platform. The component's styles reference the DS tokens, so the system
10
- * itself must be installed (`synthesisui add <slug>`) for `tokens.css` to resolve.
11
+ * specific", INS-18 fatia 3):
12
+ *
13
+ * 1. Artifacts (source of truth) `_synthesisui/ds/<slug>/components/`:
14
+ * the recipe (.json, for agents/tooling) + compiled CSS (.css).
15
+ * 2. YOUR component (unless --artifacts-only, target "next") →
16
+ * `<componentsDir>/<name>/` from `_synthesisui/config.json`: a real
17
+ * `export function <Pascal>()` with variants as typed props, in the
18
+ * project's chosen flavor (`styles: "css" | "tailwind"`).
19
+ *
20
+ * The component's styles reference the DS tokens, so the system itself must be
21
+ * installed (`synthesisui add <slug>`) for `tokens.css`/`theme.css` to resolve.
11
22
  */
12
23
  export async function component(slug, name, opts) {
13
24
  const base = resolveRegistry(opts.registry);
14
25
  const root = opts.dir ?? process.cwd();
26
+ if (!SAFE_NAME.test(slug)) {
27
+ throw new RegistryError(`Invalid slug "${slug}".`);
28
+ }
15
29
  console.log(`→ fetching "${name}" from "${slug}" …`);
16
30
  const res = await fetchComponent(base, slug, name, opts.version);
31
+ // The server should only ever return a kebab-case name, but never trust a
32
+ // network value as a path segment.
33
+ if (!SAFE_NAME.test(res.name)) {
34
+ throw new RegistryError(`Registry returned an unsafe component name.`);
35
+ }
17
36
  const dir = join(root, "_synthesisui", "ds", slug, "components");
18
37
  await mkdir(dir, { recursive: true });
19
38
  await writeFile(join(dir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
20
39
  await writeFile(join(dir, `${res.name}.css`), `${res.css}\n`, "utf8");
21
40
  console.log(`✓ ${res.name} → _synthesisui/ds/${slug}/components/${res.name}.{json,css} (${slug} v${res.version})`);
41
+ // 2. YOUR component - a real, importable `export function <Pascal>()` in the
42
+ // project's flavor (config: styles css|tailwind), under componentsDir.
43
+ const config = await readProjectConfig(root);
44
+ if (!opts.artifactsOnly && config.target === "next") {
45
+ const compDir = join(root, config.componentsDir, res.name);
46
+ await mkdir(compDir, { recursive: true });
47
+ const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, config.styles);
48
+ for (const file of files) {
49
+ await writeFile(join(compDir, file.filename), file.code, "utf8");
50
+ }
51
+ const names = files.map((f) => f.filename).join(", ");
52
+ console.log(`✓ ${config.componentsDir}/${res.name}/ → ${names} (styles: ${config.styles})`);
53
+ }
22
54
  console.log("");
23
55
  console.log("Use it:");
24
- console.log(` • ensure the DS is installed: synthesisui add ${slug} (provides tokens.css)`);
25
- console.log(` @import "_synthesisui/ds/${slug}/components/${res.name}.css" in your CSS`);
26
- console.log(` • <div data-ds="${slug}"><div class="ds-${res.name}">…</div></div>`);
56
+ console.log(` • once per app: synthesisui add ${slug} (tokens.css${config.styles === "tailwind" ? " + theme.css" : ""}), import it globally,`);
57
+ console.log(` and put data-ds="${slug}" on a root element (e.g. <body data-ds="${slug}">)`);
58
+ if (!opts.artifactsOnly && config.target === "next") {
59
+ const pascalName = res.name
60
+ .split(/[^a-zA-Z0-9]+/)
61
+ .filter(Boolean)
62
+ .map((p) => p[0].toUpperCase() + p.slice(1))
63
+ .join("");
64
+ console.log(` • import { ${pascalName} } from "./${config.componentsDir}/${res.name}" and render <${pascalName} />`);
65
+ console.log(` • or ask your agent: "use the ${pascalName} component from ${config.componentsDir}/${res.name} (SynthesisUI ${slug})"`);
66
+ }
67
+ else {
68
+ console.log(` • @import "_synthesisui/ds/${slug}/components/${res.name}.css" and use <div class="ds-${res.name}">…</div>`);
69
+ }
27
70
  }
@@ -14,12 +14,14 @@ export async function init(opts) {
14
14
  target,
15
15
  pagesDir: opts.pagesDir ?? (target === "next" ? "app" : DEFAULT_CONFIG.pagesDir),
16
16
  componentsDir: opts.componentsDir ?? DEFAULT_CONFIG.componentsDir,
17
+ styles: opts.styles === "tailwind" ? "tailwind" : "css",
17
18
  };
18
19
  await writeProjectConfig(root, config);
19
20
  console.log("✓ wrote _synthesisui/config.json");
20
21
  console.log(` target: ${config.target}`);
21
22
  console.log(` pagesDir: ${config.pagesDir}`);
22
23
  console.log(` componentsDir: ${config.componentsDir}`);
24
+ console.log(` styles: ${config.styles}`);
23
25
  // --ds bootstraps the project with a system in one step (tokens + philosophy
24
26
  // + rules + CLAUDE.md all arrive via `add`).
25
27
  if (opts.ds) {
@@ -0,0 +1,382 @@
1
+ const kebab = (v) => v.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
2
+ const pascal = (name) => name
3
+ .split(/[^a-zA-Z0-9]+/)
4
+ .filter(Boolean)
5
+ .map((p) => p[0].toUpperCase() + p.slice(1))
6
+ .join("");
7
+ const camel = (name) => {
8
+ const p = pascal(name);
9
+ return p[0].toLowerCase() + p.slice(1);
10
+ };
11
+ /** Intrinsic element + extra attrs per component, chosen like the platform
12
+ * renderer does (name first, then preview.kind). Fallback: div + children. */
13
+ function elementFor(name, recipe) {
14
+ const byName = {
15
+ input: { tag: "input" },
16
+ textarea: { tag: "textarea" },
17
+ select: { tag: "select" },
18
+ checkbox: { tag: "input", attrs: ' type="checkbox"' },
19
+ radio: { tag: "input", attrs: ' type="radio"' },
20
+ switch: { tag: "input", attrs: ' type="checkbox" role="switch"' },
21
+ slider: { tag: "input", attrs: ' type="range"' },
22
+ button: { tag: "button", attrs: ' type="button"' },
23
+ "icon-button": { tag: "button", attrs: ' type="button"' },
24
+ badge: { tag: "span" },
25
+ tag: { tag: "span" },
26
+ avatar: { tag: "span" },
27
+ divider: { tag: "hr" },
28
+ link: { tag: "a" },
29
+ };
30
+ const hit = byName[name] ??
31
+ (recipe.preview?.kind === "action"
32
+ ? { tag: "button", attrs: ' type="button"' }
33
+ : undefined);
34
+ const tag = hit?.tag ?? "div";
35
+ return {
36
+ tag,
37
+ attrs: hit?.attrs ?? "",
38
+ voidEl: tag === "input" || tag === "hr",
39
+ };
40
+ }
41
+ /** Variant axes → typed props. An axis whose options ⊆ {true,false} is a
42
+ * boolean prop; empty axes (no visual effect) are skipped. */
43
+ function axesOf(variants) {
44
+ const axes = [];
45
+ for (const [axis, options] of Object.entries(variants ?? {})) {
46
+ const keys = Object.keys(options).filter((k) => Object.keys(options[k] ?? {}).length > 0);
47
+ if (keys.length === 0)
48
+ continue;
49
+ axes.push({
50
+ key: axis,
51
+ prop: camel(axis),
52
+ attr: kebab(axis),
53
+ boolean: keys.every((k) => k === "true" || k === "false"),
54
+ styledFalse: keys.includes("false"),
55
+ options: keys,
56
+ });
57
+ }
58
+ return axes;
59
+ }
60
+ function propsType(axes, tag) {
61
+ const extras = axes.map((a) => a.boolean
62
+ ? ` ${a.prop}?: boolean;`
63
+ : ` ${a.prop}?: ${a.options.map((o) => `"${o}"`).join(" | ")};`);
64
+ if (extras.length === 0)
65
+ return `ComponentPropsWithoutRef<"${tag}">`;
66
+ return `ComponentPropsWithoutRef<"${tag}"> & {\n${extras.join("\n")}\n}`;
67
+ }
68
+ function dataAttrLines(axes) {
69
+ return axes
70
+ .map((a) => a.boolean
71
+ ? a.styledFalse
72
+ ? ` data-${a.attr}={${a.prop} ? "true" : "false"}`
73
+ : ` data-${a.attr}={${a.prop} ? "true" : undefined}`
74
+ : ` data-${a.attr}={${a.prop}}`)
75
+ .join("\n");
76
+ }
77
+ // ── Tailwind translation ─────────────────────────────────────────────────────
78
+ /**
79
+ * Namespace-aware token key: returns the utility suffix ONLY when the ref
80
+ * lives in a namespace the `theme.css` @theme adapter actually maps
81
+ * (semantic/series colors, spacing, radius, shadow, families, weights, type
82
+ * scale). Anything else (e.g. color primitives) must use the --ds-* fallback.
83
+ */
84
+ const nsKey = (v, ns) => {
85
+ const m = v.match(new RegExp(`^\\{${ns.replace(/\./g, "\\.")}\\.([a-zA-Z0-9-]+)\\}$`));
86
+ return m ? kebab(m[1]) : null;
87
+ };
88
+ /** Color refs the adapter maps: semantic → <role>, series → series-<n>. */
89
+ const colorKey = (v) => {
90
+ const semantic = nsKey(v, "color.semantic");
91
+ if (semantic)
92
+ return semantic;
93
+ const series = nsKey(v, "color.series");
94
+ if (series)
95
+ return `series-${series}`;
96
+ return null;
97
+ };
98
+ /** "{typography.scale.sm.fontSize}" → "sm" (the --text-<key> utility). */
99
+ const scaleKey = (v) => {
100
+ const m = v.match(/^\{typography\.scale\.([a-zA-Z0-9-]+)\.fontSize\}$/);
101
+ return m ? kebab(m[1]) : null;
102
+ };
103
+ /** "{color.semantic.primary}" → "var(--ds-color-semantic-primary)" - the raw
104
+ * scoped vars always exist, so arbitrary-property fallbacks never dangle. */
105
+ const refToDsVar = (v) => v.replace(/\{([a-z0-9.-]+)\}/gi, (_, path) => {
106
+ return `var(--ds-${path.split(".").map(kebab).join("-")})`;
107
+ });
108
+ /** Arbitrary-property escape hatch: guaranteed-faithful when no pretty utility
109
+ * exists. Spaces become underscores per Tailwind's arbitrary syntax. */
110
+ const arbitrary = (prop, value) => `[${kebab(prop)}:${refToDsVar(value).replace(/\s+/g, "_")}]`;
111
+ const STATIC = {
112
+ display: {
113
+ flex: "flex",
114
+ "inline-flex": "inline-flex",
115
+ grid: "grid",
116
+ block: "block",
117
+ "inline-block": "inline-block",
118
+ none: "hidden",
119
+ },
120
+ alignItems: {
121
+ center: "items-center",
122
+ "flex-start": "items-start",
123
+ "flex-end": "items-end",
124
+ baseline: "items-baseline",
125
+ stretch: "items-stretch",
126
+ },
127
+ justifyContent: {
128
+ center: "justify-center",
129
+ "space-between": "justify-between",
130
+ "flex-start": "justify-start",
131
+ "flex-end": "justify-end",
132
+ },
133
+ flexDirection: { column: "flex-col", row: "flex-row" },
134
+ textAlign: { center: "text-center", left: "text-left", right: "text-right" },
135
+ cursor: { pointer: "cursor-pointer", "not-allowed": "cursor-not-allowed" },
136
+ width: { "100%": "w-full" },
137
+ height: { "100%": "h-full" },
138
+ textDecoration: { none: "no-underline", underline: "underline" },
139
+ };
140
+ /** One declaration → Tailwind classes (pretty when mappable, arbitrary-property
141
+ * otherwise - never dropped). */
142
+ function declToTailwind(prop, value) {
143
+ const stat = STATIC[prop]?.[value];
144
+ if (stat)
145
+ return [stat];
146
+ switch (prop) {
147
+ case "backgroundColor": {
148
+ if (value === "transparent")
149
+ return ["bg-transparent"];
150
+ const key = colorKey(value);
151
+ if (key)
152
+ return [`bg-${key}`];
153
+ break;
154
+ }
155
+ case "color": {
156
+ const key = colorKey(value);
157
+ if (key)
158
+ return [`text-${key}`];
159
+ break;
160
+ }
161
+ case "borderColor": {
162
+ const key = colorKey(value);
163
+ if (key)
164
+ return [`border-${key}`];
165
+ break;
166
+ }
167
+ case "border": {
168
+ // "1px solid {color.semantic.x}" → border + border-<x>
169
+ const m = value.match(/^1px\s+solid\s+(\{[^}]+\})$/);
170
+ if (m) {
171
+ const key = colorKey(m[1]);
172
+ if (key)
173
+ return ["border", `border-${key}`];
174
+ }
175
+ break;
176
+ }
177
+ case "borderRadius": {
178
+ const key = nsKey(value, "radius");
179
+ if (key)
180
+ return [`rounded-${key}`];
181
+ break;
182
+ }
183
+ case "gap": {
184
+ const key = nsKey(value, "spacing");
185
+ if (key)
186
+ return [`gap-${key}`];
187
+ break;
188
+ }
189
+ case "padding": {
190
+ const keys = value.split(/\s+/).map((v) => nsKey(v, "spacing"));
191
+ if (keys.length === 1 && keys[0])
192
+ return [`p-${keys[0]}`];
193
+ if (keys.length === 2 && keys[0] && keys[1])
194
+ return [`py-${keys[0]}`, `px-${keys[1]}`];
195
+ break;
196
+ }
197
+ case "fontSize": {
198
+ const key = scaleKey(value);
199
+ if (key)
200
+ return [`text-${key}`];
201
+ break;
202
+ }
203
+ case "fontFamily": {
204
+ const key = nsKey(value, "typography.families");
205
+ if (key)
206
+ return [`font-${key}`];
207
+ break;
208
+ }
209
+ case "fontWeight": {
210
+ const key = nsKey(value, "typography.weights");
211
+ if (key)
212
+ return [`font-${key}`];
213
+ break;
214
+ }
215
+ case "boxShadow": {
216
+ const key = nsKey(value, "shadow");
217
+ if (key)
218
+ return [`shadow-${key}`];
219
+ break;
220
+ }
221
+ case "lineHeight":
222
+ // usually paired with the same scale's fontSize (text-<key> carries the
223
+ // scale's line-height via --text-<key>--line-height)
224
+ if (/^\{typography\.scale\./.test(value))
225
+ return [];
226
+ break;
227
+ }
228
+ return [arbitrary(prop, value)];
229
+ }
230
+ const STATE_PREFIX = {
231
+ hover: "hover:",
232
+ focus: "focus:",
233
+ focusVisible: "focus-visible:",
234
+ active: "active:",
235
+ disabled: "disabled:",
236
+ };
237
+ function blockToTailwind(block, prefix = "") {
238
+ return Object.entries(block).flatMap(([prop, value]) => declToTailwind(prop, value).map((cls) => `${prefix}${cls}`));
239
+ }
240
+ function tailwindClassList(recipe) {
241
+ const classes = [
242
+ ...blockToTailwind(recipe.base),
243
+ ...Object.entries(recipe.states ?? {}).flatMap(([state, block]) => STATE_PREFIX[state] ? blockToTailwind(block, STATE_PREFIX[state]) : []),
244
+ ];
245
+ return classes.join(" ");
246
+ }
247
+ // ── Emission ─────────────────────────────────────────────────────────────────
248
+ function header(slug, name, version, mode) {
249
+ const setup = mode === "tailwind"
250
+ ? `import _synthesisui/ds/${slug}/theme.css (Tailwind adapter) + tokens.css`
251
+ : `import _synthesisui/ds/${slug}/tokens.css`;
252
+ return [
253
+ `// Generated by SynthesisUI - "${name}" from the "${slug}" design system (v${version}).`,
254
+ `// On-system by construction: every style resolves to the DS tokens.`,
255
+ `// Global setup (once per app): ${setup}`,
256
+ `// and put data-ds="${slug}" on a root element (e.g. <body data-ds="${slug}">).`,
257
+ ].join("\n");
258
+ }
259
+ const joinCls = (parts) => `[${parts.join(", ")}].filter(Boolean).join(" ")`;
260
+ function emitCssMode(slug, name, recipe, version) {
261
+ const { tag, attrs, voidEl } = elementFor(name, recipe);
262
+ const axes = axesOf(recipe.variants);
263
+ const comp = pascal(name);
264
+ const propNames = axes.map((a) => a.prop);
265
+ const destructure = [...propNames, "className", "...props"].join(", ");
266
+ void voidEl; // both void and container elements self-close ({...props} carries children)
267
+ const rootJsx = ` <${tag}${attrs}\n className={${joinCls([`"ds-${name}"`, "className"])}}\n${dataAttrLines(axes)}${axes.length ? "\n" : ""} {...props}\n />`;
268
+ const parts = Object.entries(recipe.parts ?? {}).map(([partName, part]) => {
269
+ const partAxes = axesOf(part.variants ?? {});
270
+ const partComp = `${comp}${pascal(partName)}`;
271
+ const partDestructure = [
272
+ ...partAxes.map((a) => a.prop),
273
+ "className",
274
+ "...props",
275
+ ].join(", ");
276
+ return `
277
+ /** Part "${partName}" of ${comp} - compose it inside <${comp}>. */
278
+ export function ${partComp}({ ${partDestructure} }: ${propsType(partAxes, "div")}) {
279
+ return (
280
+ <div
281
+ className={${joinCls([`"ds-${name}-${kebab(partName)}"`, "className"])}}
282
+ ${dataAttrLines(partAxes)}${partAxes.length ? "\n" : ""} {...props}
283
+ />
284
+ );
285
+ }`;
286
+ });
287
+ return `${header(slug, name, version, "css")}
288
+ import "./${name}.css";
289
+
290
+ import type { ComponentPropsWithoutRef } from "react";
291
+
292
+ type ${comp}Props = ${propsType(axes, tag)};
293
+
294
+ export function ${comp}({ ${destructure} }: ${comp}Props) {
295
+ return (
296
+ ${rootJsx}
297
+ );
298
+ }
299
+ ${parts.join("\n")}`;
300
+ }
301
+ function emitTailwindMode(slug, name, recipe, version) {
302
+ const { tag, attrs, voidEl } = elementFor(name, recipe);
303
+ const axes = axesOf(recipe.variants);
304
+ const comp = pascal(name);
305
+ const variantConsts = axes
306
+ .filter((a) => !a.boolean)
307
+ .map((a) => {
308
+ const entries = a.options
309
+ .map((o) => ` ${JSON.stringify(o)}: ${JSON.stringify(blockToTailwind(recipe.variants[a.key]?.[o] ?? {}).join(" "))},`)
310
+ .join("\n");
311
+ return `const ${a.prop.toUpperCase()}: Record<string, string> = {\n${entries}\n};`;
312
+ });
313
+ const booleanConsts = axes
314
+ .filter((a) => a.boolean)
315
+ .map((a) => `const ${a.prop.toUpperCase()} = ${JSON.stringify(blockToTailwind(recipe.variants[a.key]?.true ?? {}).join(" "))};`);
316
+ const clsParts = [
317
+ "BASE",
318
+ ...axes.map((a) => a.boolean
319
+ ? `${a.prop} ? ${a.prop.toUpperCase()} : ""`
320
+ : `${a.prop} ? ${a.prop.toUpperCase()}[${a.prop}] : ""`),
321
+ "className",
322
+ ];
323
+ const destructure = [
324
+ ...axes.map((a) => a.prop),
325
+ "className",
326
+ "...props",
327
+ ].join(", ");
328
+ void voidEl;
329
+ return `${header(slug, name, version, "tailwind")}
330
+
331
+ import type { ComponentPropsWithoutRef } from "react";
332
+
333
+ const BASE = ${JSON.stringify(tailwindClassList(recipe))};
334
+ ${[...variantConsts, ...booleanConsts].join("\n")}
335
+
336
+ type ${comp}Props = ${propsType(axes, tag)};
337
+
338
+ export function ${comp}({ ${destructure} }: ${comp}Props) {
339
+ return (
340
+ <${tag}${attrs}
341
+ className={${joinCls(clsParts)}}
342
+ {...props}
343
+ />
344
+ );
345
+ }
346
+ ${Object.entries(recipe.parts ?? {})
347
+ .map(([partName, part]) => {
348
+ const partComp = `${comp}${pascal(partName)}`;
349
+ return `
350
+ /** Part "${partName}" of ${comp} - compose it inside <${comp}>. */
351
+ export function ${partComp}({ className, ...props }: ComponentPropsWithoutRef<"div">) {
352
+ return (
353
+ <div className={${joinCls([JSON.stringify(tailwindClassList(part)), "className"])}} {...props} />
354
+ );
355
+ }`;
356
+ })
357
+ .join("\n")}`;
358
+ }
359
+ /** All files for one component, under `<componentsDir>/<name>/`. */
360
+ export function generateComponentFiles(slug, name, recipe, css, version, styles) {
361
+ const comp = pascal(name);
362
+ const files = [];
363
+ if (styles === "css") {
364
+ files.push({
365
+ filename: `${name}.tsx`,
366
+ code: `${emitCssMode(slug, name, recipe, version)}\n`,
367
+ });
368
+ files.push({ filename: `${name}.css`, code: `${css}\n` });
369
+ }
370
+ else {
371
+ files.push({
372
+ filename: `${name}.tsx`,
373
+ code: `${emitTailwindMode(slug, name, recipe, version)}\n`,
374
+ });
375
+ }
376
+ files.push({
377
+ filename: "index.ts",
378
+ code: `export * from "./${name}";\n`,
379
+ });
380
+ void comp;
381
+ return files;
382
+ }
package/dist/config.js CHANGED
@@ -40,6 +40,7 @@ export const DEFAULT_CONFIG = {
40
40
  target: "next",
41
41
  pagesDir: "app",
42
42
  componentsDir: "components",
43
+ styles: "css",
43
44
  };
44
45
  const projectConfigPath = (root) => join(root, "_synthesisui", "config.json");
45
46
  /** Reads the project config, falling back to defaults when absent/invalid. */
@@ -55,6 +56,7 @@ export async function readProjectConfig(root) {
55
56
  componentsDir: typeof parsed.componentsDir === "string" && parsed.componentsDir
56
57
  ? parsed.componentsDir
57
58
  : DEFAULT_CONFIG.componentsDir,
59
+ styles: parsed.styles === "tailwind" ? "tailwind" : "css",
58
60
  };
59
61
  }
60
62
  catch {
package/dist/guide.js CHANGED
@@ -145,6 +145,18 @@ place** - wire real data, split into components, swap the chart/icon/media place
145
145
  \`data-ds="${slug}"\` wrapper and the \`.ds-*\` / layout classes so it stays on-system. Run
146
146
  \`synthesisui init\` once to set the target (next/general) and the output folder.
147
147
 
148
+ ## Single components as YOUR code
149
+
150
+ Bring one component in as a real, typed React component (variants become props):
151
+ \`\`\`bash
152
+ synthesisui component ${slug} button
153
+ \`\`\`
154
+ It writes \`<componentsDir>/button/\` with \`button.tsx\` (+ colocated \`button.css\` in the default
155
+ \`styles: "css"\` flavor, or Tailwind utilities inline with \`styles: "tailwind"\` - set once via
156
+ \`synthesisui init --styles tailwind\`). Import and render: \`import { Button } from "components/button"\`.
157
+ The boundary: tokens are global (\`tokens.css\` + the \`data-ds\` root attribute, once per app);
158
+ everything a component owns lives in its own folder.
159
+
148
160
  ---
149
161
  `
150
162
  : "";
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ Usage:
17
17
  synthesisui list [options] list the published design systems
18
18
  synthesisui add <slug> [options] materialize a DS into _synthesisui/ds/<slug>/
19
19
  synthesisui template <slug> <name> materialize a whole page from a DS template
20
- synthesisui component <slug> <name> bring one component (recipe + css) into the project
20
+ synthesisui component <slug> <name> bring one component in - artifacts + YOUR <Pascal>.tsx in componentsDir
21
21
  synthesisui use <slug> "<intent>" print a ready-to-paste agent prompt to build/modify on-system
22
22
  synthesisui advise "<value prop>" engagement-pattern proposals for this project (login required)
23
23
  synthesisui generate "<desc>" generate a token-only component recipe for your DS (login required)
@@ -31,6 +31,8 @@ Options:
31
31
  --target <t> template/init target: next | general (default: next)
32
32
  --pages-dir <dir> init: folder for generated pages (default: app)
33
33
  --components-dir <dir> init: folder where components live (default: components)
34
+ --styles <s> init: component code flavor: css | tailwind (default: css)
35
+ --artifacts-only component: skip the .tsx materialization (recipe + css only)
34
36
  --out <path> output path for the generated template (default: <pagesDir>/<file>)
35
37
  -h, --help this help
36
38
 
@@ -125,7 +127,16 @@ async function main() {
125
127
  ? flags["components-dir"]
126
128
  : undefined;
127
129
  const ds = typeof flags.ds === "string" ? flags.ds : undefined;
128
- await init({ dir, registry, target, pagesDir, componentsDir, ds });
130
+ const styles = typeof flags.styles === "string" ? flags.styles : undefined;
131
+ await init({
132
+ dir,
133
+ registry,
134
+ target,
135
+ pagesDir,
136
+ componentsDir,
137
+ styles,
138
+ ds,
139
+ });
129
140
  break;
130
141
  }
131
142
  // `page` is the legacy alias (renamed to `template`); it still works so
@@ -173,7 +184,12 @@ async function main() {
173
184
  return;
174
185
  }
175
186
  }
176
- await component(slug, name, { registry, dir, version });
187
+ await component(slug, name, {
188
+ registry,
189
+ dir,
190
+ version,
191
+ artifactsOnly: flags["artifacts-only"] === true,
192
+ });
177
193
  break;
178
194
  }
179
195
  case "use": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.1.24",
3
+ "version": "0.2.0",
4
4
  "description": "Traz design systems do SynthesisUI para qualquer projeto (materializa em _local/ds/).",
5
5
  "type": "module",
6
6
  "bin": {