synthesisui 0.16.30 → 0.16.32

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.
@@ -16,7 +16,7 @@ export function decisionLine(d, slug) {
16
16
  case "authored":
17
17
  return ` ✓ ${d.id} authored on the platform${note}. Get it: npx synthesisui@latest upgrade ${slug}`;
18
18
  case "declined":
19
- return ` ✕ ${d.id} declined${note}. The boundary stands - the system stays without it.`;
19
+ return ` ✕ ${d.id} declined${note}. Now a RULE of the system - it travels with the next upgrade, and agents obey it.`;
20
20
  case "platform":
21
21
  return ` → ${d.id} routed to the synthesisui team${note}. It is their queue now.`;
22
22
  default:
@@ -127,14 +127,48 @@ export function propsTypeName(reactMajor) {
127
127
  ? "ComponentProps"
128
128
  : "ComponentPropsWithoutRef";
129
129
  }
130
- function propsType(axes, tag, base) {
130
+ function propsType(axes, tag, base, withAs = false) {
131
131
  const extras = axes.map((a) => a.boolean
132
132
  ? ` ${a.prop}?: boolean;`
133
133
  : ` ${a.prop}?: ${a.options.map((o) => `"${o}"`).join(" | ")};`);
134
+ if (withAs)
135
+ extras.push(" /** Render as a different element (th, dt, h2, li…) - the recipe is", " * styling only, so the document's semantics stay yours. */", " as?: ElementType;");
134
136
  if (extras.length === 0)
135
137
  return `${base}<"${tag}">`;
136
138
  return `${base}<"${tag}"> & {\n${extras.join("\n")}\n}`;
137
139
  }
140
+ /**
141
+ * THE ELEMENT IS THE CALLER'S DECISION (1kro2o, 29/07).
142
+ *
143
+ * The generated table rendered every part as a <div>, so a pricing
144
+ * comparison could not express <th scope="col"> - and the agent hand-extended
145
+ * Heading/Text with an `as` prop knowing regeneration would erase it. The
146
+ * recipe is TYPE STYLING; the outline, the scopes, the list semantics belong
147
+ * to the markup's author. The cast to the default tag is deliberate: a union
148
+ * of intrinsic tags is more than the JSX spread can narrow, and every tag
149
+ * offered takes the same attributes in practice - the pattern is the exact
150
+ * one the field agent wrote by hand.
151
+ */
152
+ const asTagLine = (tag) => ` const Tag = (as ?? "${tag}") as "${tag}";`;
153
+ /** The render pieces for an element that offers `as`. `type="button"` must
154
+ * follow the RENDERED tag - a Button rendered as an <a> with type="button"
155
+ * is invalid HTML wearing a straight face. Void elements never offer `as`:
156
+ * their children/attribute contracts do not survive a tag change. */
157
+ function asElement(tag, attrs, voidEl) {
158
+ if (voidEl)
159
+ return { jsxTag: tag, jsxAttrs: attrs, setup: "", offersAs: false };
160
+ const hasTypeButton = attrs.includes(' type="button"');
161
+ const staticAttrs = attrs.replace(' type="button"', "");
162
+ const typeLine = hasTypeButton
163
+ ? `\n {...(Tag === "button" ? { type: "button" as const } : {})}`
164
+ : "";
165
+ return {
166
+ jsxTag: "Tag",
167
+ jsxAttrs: `${staticAttrs}${typeLine}`,
168
+ setup: `${asTagLine(tag)}\n`,
169
+ offersAs: true,
170
+ };
171
+ }
138
172
  function dataAttrLines(axes) {
139
173
  return axes
140
174
  .map((a) => a.boolean
@@ -471,43 +505,51 @@ ${inner}
471
505
  }
472
506
  function emitCssMode(slug, name, recipe, version, props) {
473
507
  const { tag, attrs, voidEl } = elementFor(name, recipe);
508
+ const el = asElement(tag, attrs, voidEl);
474
509
  const axes = axesOf(recipe.variants);
475
510
  const comp = pascal(name);
476
511
  const propNames = axes.map((a) => a.prop);
477
- const destructure = [...propNames, "className", "...props"].join(", ");
478
- const rootJsx = ` <${tag}${attrs}\n className={${joinCls([`"ds-${name}"`, "className"])}}\n${dataAttrLines(axes)}${axes.length ? "\n" : ""} {...props}\n />`;
512
+ const destructure = [
513
+ ...propNames,
514
+ ...(el.offersAs ? ["as"] : []),
515
+ "className",
516
+ "...props",
517
+ ].join(", ");
518
+ const rootJsx = ` <${el.jsxTag}${el.jsxAttrs}\n className={${joinCls([`"ds-${name}"`, "className"])}}\n${dataAttrLines(axes)}${axes.length ? "\n" : ""} {...props}\n />`;
479
519
  const parts = Object.entries(recipe.parts ?? {}).map(([partName, part]) => {
480
520
  const partAxes = axesOf(part.variants ?? {});
481
521
  const partComp = `${comp}${pascal(partName)}`;
522
+ const control = partIsInteractive(part);
523
+ const partTag = control ? "button" : "div";
524
+ const partEl = asElement(partTag, control ? ' type="button"' : "", false);
482
525
  const partDestructure = [
483
526
  ...partAxes.map((a) => a.prop),
527
+ "as",
484
528
  "className",
485
529
  "...props",
486
530
  ].join(", ");
487
- const control = partIsInteractive(part);
488
- const partTag = control ? "button" : "div";
489
- const partAttrs = control ? '\n type="button"' : "";
490
531
  return `
491
532
  /** Part "${partName}" of ${comp} - compose it inside <${comp}>. */
492
- export function ${partComp}({ ${partDestructure} }: ${propsType(partAxes, partTag, props)}) {
493
- return (
494
- <${partTag}${partAttrs}
533
+ export function ${partComp}({ ${partDestructure} }: ${propsType(partAxes, partTag, props, true)}) {
534
+ ${partEl.setup} return (
535
+ <${partEl.jsxTag}${partEl.jsxAttrs}
495
536
  className={${joinCls([`"ds-${name}-${kebab(partName)}"`, "className"])}}
496
537
  ${dataAttrLines(partAxes)}${partAxes.length ? "\n" : ""} {...props}
497
538
  />
498
539
  );
499
540
  }`;
500
541
  });
542
+ const needsElementType = el.offersAs || Object.keys(recipe.parts ?? {}).length > 0;
501
543
  return `${header(slug, name, version, "css")}
502
544
  import "./${name}.css";
503
545
 
504
- import type { ${props} } from "react";
546
+ import type { ${needsElementType ? `ElementType, ${props}` : props} } from "react";
505
547
 
506
- type ${comp}Props = ${propsType(axes, tag, props)};
548
+ type ${comp}Props = ${propsType(axes, tag, props, el.offersAs)};
507
549
 
508
550
  ${compositionHint(comp, name, recipe, voidEl)}
509
551
  export function ${comp}({ ${destructure} }: ${comp}Props) {
510
- return (
552
+ ${el.setup} return (
511
553
  ${rootJsx}
512
554
  );
513
555
  }
@@ -515,6 +557,7 @@ ${parts.join("\n")}`;
515
557
  }
516
558
  function emitTailwindMode(slug, name, recipe, version, props) {
517
559
  const { tag, attrs, voidEl } = elementFor(name, recipe);
560
+ const el = asElement(tag, attrs, voidEl);
518
561
  const axes = axesOf(recipe.variants);
519
562
  const comp = pascal(name);
520
563
  const variantConsts = axes
@@ -543,39 +586,71 @@ function emitTailwindMode(slug, name, recipe, version, props) {
543
586
  ];
544
587
  const destructure = [
545
588
  ...axes.map((a) => a.prop),
589
+ ...(el.offersAs ? ["as"] : []),
546
590
  "className",
547
591
  "...props",
548
592
  ].join(", ");
593
+ const needsElementType = el.offersAs || Object.keys(recipe.parts ?? {}).length > 0;
549
594
  return `${header(slug, name, version, "tailwind")}
550
595
 
551
- import type { ${props} } from "react";
596
+ import type { ${needsElementType ? `ElementType, ${props}` : props} } from "react";
552
597
  import { cn } from "../cn";
553
598
 
554
599
  const BASE = ${JSON.stringify(tailwindClassList(recipe, excluded))};
555
600
  ${[...variantConsts, ...booleanConsts].join("\n")}
556
601
 
557
- type ${comp}Props = ${propsType(axes, tag, props)};
602
+ type ${comp}Props = ${propsType(axes, tag, props, el.offersAs)};
558
603
 
559
604
  ${compositionHint(comp, name, recipe, voidEl)}
560
605
  export function ${comp}({ ${destructure} }: ${comp}Props) {
561
- return (
562
- <${tag}${attrs}
606
+ ${el.setup} return (
607
+ <${el.jsxTag}${el.jsxAttrs}
563
608
  className={${resolveCls(clsParts)}}
564
- {...props}
609
+ ${dataAttrLines(axes)}${axes.length ? "\n" : ""} {...props}
565
610
  />
566
611
  );
567
612
  }
568
613
  ${Object.entries(recipe.parts ?? {})
569
614
  .map(([partName, part]) => {
570
615
  const partComp = `${comp}${pascal(partName)}`;
616
+ const partAxes = axesOf(part.variants ?? {});
571
617
  const control = partIsInteractive(part);
572
618
  const partTag = control ? "button" : "div";
573
- const partAttrs = control ? ' type="button"' : "";
619
+ const partEl = asElement(partTag, control ? ' type="button"' : "", false);
620
+ /**
621
+ * THE STATE THE CATALOGUE DOCUMENTS, DELIVERED (1kro2o, 29/07).
622
+ *
623
+ * A part's variants used to be dropped entirely in this flavour: the
624
+ * GUIDE documented data-active="true", tokens.css styled it, and the
625
+ * generated TabsTrigger rendered an active trigger identical to an
626
+ * inactive one - the promise died in the last mile of codegen.
627
+ *
628
+ * They emit as data-[attr=option]: utilities, not as const-switching
629
+ * like the root: the compiled selector is class+attribute, which
630
+ * outranks the plain base honestly (the same ladder css mode gets from
631
+ * [data-*] selectors) - AND the styling answers the data attribute
632
+ * itself, so it works whether the caller passes the typed prop or
633
+ * toggles data-active from its own state, which is how the GUIDE
634
+ * documents the contract.
635
+ */
636
+ const partVariantClasses = partAxes.flatMap((a) => a.options.flatMap((o) => blockToTailwind(part.variants?.[a.key]?.[o] ?? {}, `data-[${a.attr}=${o}]:`)));
637
+ const partCls = [tailwindClassList(part), ...partVariantClasses]
638
+ .filter(Boolean)
639
+ .join(" ");
640
+ const partDestructure = [
641
+ ...partAxes.map((a) => a.prop),
642
+ "as",
643
+ "className",
644
+ "...props",
645
+ ].join(", ");
574
646
  return `
575
647
  /** Part "${partName}" of ${comp} - compose it inside <${comp}>. */
576
- export function ${partComp}({ className, ...props }: ${props}<"${partTag}">) {
577
- return (
578
- <${partTag}${partAttrs} className={${resolveCls([JSON.stringify(tailwindClassList(part)), "className"])}} {...props} />
648
+ export function ${partComp}({ ${partDestructure} }: ${propsType(partAxes, partTag, props, true)}) {
649
+ ${partEl.setup} return (
650
+ <${partEl.jsxTag}${partEl.jsxAttrs}
651
+ className={${resolveCls([JSON.stringify(partCls), "className"])}}
652
+ ${dataAttrLines(partAxes)}${partAxes.length ? "\n" : ""} {...props}
653
+ />
579
654
  );
580
655
  }`;
581
656
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.30",
3
+ "version": "0.16.32",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {