rafters 0.0.71 → 0.0.73

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/index.js CHANGED
@@ -437,7 +437,7 @@ function generateThemeInlineBlock(semanticTokens) {
437
437
  function generateRootBlock(semanticTokens, darkMode = "class") {
438
438
  const semanticMappings = getSemanticMappingsFromTokens(semanticTokens);
439
439
  const lines = [];
440
- lines.push(":root {");
440
+ lines.push(":root, :host {");
441
441
  for (const [name, mapping] of Object.entries(semanticMappings)) {
442
442
  lines.push(` --rafters-${name}: var(--color-${mapping.light});`);
443
443
  }
@@ -750,6 +750,37 @@ function generateTypographyCompositeUtilities(compositeTokens) {
750
750
  }
751
751
  return lines.join("\n");
752
752
  }
753
+ function generateMotionUtilities(motionTokens) {
754
+ const semanticTokens = motionTokens.filter((t) => t.name.startsWith("motion-semantic-"));
755
+ if (semanticTokens.length === 0) {
756
+ return "";
757
+ }
758
+ const lines = ["/* Semantic motion utilities -- transition longhand per token */"];
759
+ for (const token of semanticTokens) {
760
+ if (typeof token.value !== "string") continue;
761
+ let spec;
762
+ try {
763
+ spec = JSON.parse(token.value);
764
+ } catch {
765
+ continue;
766
+ }
767
+ const className = token.name.replace("motion-semantic-", "motion-");
768
+ lines.push(`@utility ${className} {`);
769
+ lines.push(` transition-property: ${spec.properties.join(", ")};`);
770
+ lines.push(` transition-duration: var(--duration-${spec.durationTier});`);
771
+ lines.push(` transition-timing-function: var(--ease-${spec.curve});`);
772
+ if (spec.reducedMotion) {
773
+ lines.push(" @media (prefers-reduced-motion: reduce) {");
774
+ lines.push(` transition-property: ${spec.reducedMotion.properties.join(", ")};`);
775
+ if (typeof spec.reducedMotion.ms === "number") {
776
+ lines.push(` transition-duration: ${spec.reducedMotion.ms}ms;`);
777
+ }
778
+ lines.push(" }");
779
+ }
780
+ lines.push("}");
781
+ }
782
+ return lines.join("\n");
783
+ }
753
784
  function overridePropertyToUtility(property, value) {
754
785
  switch (property) {
755
786
  case "fontFamily":
@@ -825,6 +856,11 @@ function tokensToTailwind(tokens, options = {}, typographyOverrides = []) {
825
856
  sections.push("");
826
857
  sections.push(depthUtilities);
827
858
  }
859
+ const motionUtilities = generateMotionUtilities(groups.motion);
860
+ if (motionUtilities) {
861
+ sections.push("");
862
+ sections.push(motionUtilities);
863
+ }
828
864
  const overrideCSS = generateTypographyOverrideCSS(typographyOverrides);
829
865
  if (overrideCSS) {
830
866
  sections.push("");
@@ -876,6 +912,147 @@ ${themeBody}`;
876
912
  rmSync(tempDir, { recursive: true, force: true });
877
913
  }
878
914
  }
915
+ var COLOR_UTILITIES = [
916
+ "bg",
917
+ "text",
918
+ "border",
919
+ "ring",
920
+ "outline",
921
+ "fill",
922
+ "stroke",
923
+ "accent",
924
+ "caret",
925
+ "decoration",
926
+ "divide",
927
+ "shadow",
928
+ "placeholder"
929
+ ];
930
+ var SPACING_UTILITIES = [
931
+ "p",
932
+ "px",
933
+ "py",
934
+ "pt",
935
+ "pr",
936
+ "pb",
937
+ "pl",
938
+ "m",
939
+ "mx",
940
+ "my",
941
+ "mt",
942
+ "mr",
943
+ "mb",
944
+ "ml",
945
+ "gap",
946
+ "gap-x",
947
+ "gap-y",
948
+ "w",
949
+ "h",
950
+ "size",
951
+ "inset",
952
+ "top",
953
+ "right",
954
+ "bottom",
955
+ "left",
956
+ "space-x",
957
+ "space-y"
958
+ ];
959
+ var RADIUS_UTILITIES = [
960
+ "rounded",
961
+ "rounded-t",
962
+ "rounded-r",
963
+ "rounded-b",
964
+ "rounded-l",
965
+ "rounded-tl",
966
+ "rounded-tr",
967
+ "rounded-br",
968
+ "rounded-bl"
969
+ ];
970
+ var STATE_VARIANTS = ["hover", "focus-visible", "focus", "active", "dark"];
971
+ function deriveCandidates(themeCSS) {
972
+ const themeVarNames = [];
973
+ const themeBlockRe = /@theme\s*(?:inline\s*)?\{([^}]*)\}/gs;
974
+ let blockMatch;
975
+ while ((blockMatch = themeBlockRe.exec(themeCSS)) !== null) {
976
+ const block = blockMatch[1] ?? "";
977
+ const varRe = /--([a-z][a-z0-9-]*)\s*:/g;
978
+ let varMatch;
979
+ while ((varMatch = varRe.exec(block)) !== null) {
980
+ if (varMatch[1]) themeVarNames.push(varMatch[1]);
981
+ }
982
+ }
983
+ const candidates = /* @__PURE__ */ new Set();
984
+ for (const name of themeVarNames) {
985
+ if (name.startsWith("color-")) {
986
+ const slug = name.slice(6);
987
+ for (const p2 of COLOR_UTILITIES) candidates.add(`${p2}-${slug}`);
988
+ } else if (name.startsWith("spacing-")) {
989
+ const slug = name.slice(8);
990
+ for (const p2 of SPACING_UTILITIES) candidates.add(`${p2}-${slug}`);
991
+ } else if (name.startsWith("radius-")) {
992
+ const slug = name.slice(7);
993
+ for (const p2 of RADIUS_UTILITIES) candidates.add(`${p2}-${slug}`);
994
+ } else if (name.startsWith("shadow-") && !/-(blur|spread|offset|color|inset)/.test(name)) {
995
+ candidates.add(`shadow-${name.slice(7)}`);
996
+ } else if (name.startsWith("font-size-")) {
997
+ candidates.add(`text-${name.slice(10)}`);
998
+ } else if (name.startsWith("font-weight-")) {
999
+ candidates.add(`font-${name.slice(12)}`);
1000
+ } else if (name.startsWith("ease-")) {
1001
+ candidates.add(name);
1002
+ } else if (name.startsWith("animate-")) {
1003
+ candidates.add(name);
1004
+ }
1005
+ }
1006
+ const base = [...candidates];
1007
+ for (const variant of STATE_VARIANTS) {
1008
+ for (const c4 of base) {
1009
+ candidates.add(`${variant}:${c4}`);
1010
+ }
1011
+ }
1012
+ return [...candidates];
1013
+ }
1014
+ async function registryToDocumentation(registry2, options = {}) {
1015
+ const { minify = true } = options;
1016
+ const themeBody = registryToTailwind(registry2, { includeImport: false });
1017
+ const candidates = deriveCandidates(themeBody);
1018
+ const { mkdtempSync, writeFileSync: writeFileSync2, rmSync } = await import("fs");
1019
+ const { join: join15, dirname: dirname4 } = await import("path");
1020
+ const { createRequire: createRequire2 } = await import("module");
1021
+ const require2 = createRequire2(import.meta.url);
1022
+ let pkgDir;
1023
+ try {
1024
+ const pkgJsonPath = require2.resolve("@tailwindcss/cli/package.json");
1025
+ pkgDir = dirname4(pkgJsonPath);
1026
+ } catch {
1027
+ throw new Error(
1028
+ "Failed to resolve @tailwindcss/cli -- install it to generate documentation CSS"
1029
+ );
1030
+ }
1031
+ const tempDir = mkdtempSync(join15(pkgDir, ".tmp-doc-compile-"));
1032
+ const candidateFile = join15(tempDir, "candidates.txt");
1033
+ writeFileSync2(candidateFile, candidates.join("\n"));
1034
+ const sourceDirective = `@source "${candidateFile}";`;
1035
+ const input = `@import "tailwindcss" source(none);
1036
+ ${sourceDirective}
1037
+ ${themeBody}`;
1038
+ const tempInput = join15(tempDir, "input.css");
1039
+ const tempOutput = join15(tempDir, "output.css");
1040
+ try {
1041
+ writeFileSync2(tempInput, input);
1042
+ const { execFileSync } = await import("child_process");
1043
+ const binPath = join15(pkgDir, "dist", "index.mjs");
1044
+ const args = [binPath, "-i", tempInput, "-o", tempOutput];
1045
+ if (minify) args.push("--minify");
1046
+ execFileSync("node", args, { stdio: "pipe", timeout: 6e4, cwd: pkgDir });
1047
+ const { readFileSync: readFileSync3 } = await import("fs");
1048
+ return readFileSync3(tempOutput, "utf-8");
1049
+ } catch (error47) {
1050
+ const message = error47 instanceof Error ? error47.message : String(error47);
1051
+ throw new Error(`Failed to compile documentation CSS: ${message}`);
1052
+ } finally {
1053
+ rmSync(tempDir, { recursive: true, force: true });
1054
+ }
1055
+ }
879
1056
 
880
1057
  // ../design-tokens/src/exporters/typescript.ts
881
1058
  function tokenValueToTS(token) {
@@ -910,6 +1087,13 @@ function escapeStringValue(value) {
910
1087
  }
911
1088
  return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
912
1089
  }
1090
+ var VALID_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1091
+ function namespaceKey(namespace) {
1092
+ return VALID_IDENTIFIER.test(namespace) ? namespace : `'${namespace}'`;
1093
+ }
1094
+ function namespaceAccess(namespace) {
1095
+ return VALID_IDENTIFIER.test(namespace) ? `tokens.${namespace}` : `tokens['${namespace}']`;
1096
+ }
913
1097
  function namespaceToTypeName(namespace) {
914
1098
  return namespace.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
915
1099
  }
@@ -930,7 +1114,7 @@ function generateTokensObject(tokensByNamespace, includeJSDoc) {
930
1114
  const namespaces = Array.from(tokensByNamespace.keys()).sort();
931
1115
  for (const namespace of namespaces) {
932
1116
  const tokens = tokensByNamespace.get(namespace) || [];
933
- lines.push(` ${namespace}: {`);
1117
+ lines.push(` ${namespaceKey(namespace)}: {`);
934
1118
  for (const token of tokens) {
935
1119
  const value = tokenValueToTS(token);
936
1120
  if (includeJSDoc && token.semanticMeaning) {
@@ -949,7 +1133,7 @@ function generateTypeAliases(namespaces) {
949
1133
  lines.push("");
950
1134
  for (const namespace of namespaces) {
951
1135
  const typeName = `${namespaceToTypeName(namespace)}Token`;
952
- lines.push(`export type ${typeName} = keyof typeof tokens.${namespace};`);
1136
+ lines.push(`export type ${typeName} = keyof (typeof ${namespaceAccess(namespace)});`);
953
1137
  }
954
1138
  if (namespaces.length > 0) {
955
1139
  lines.push("");
@@ -1124,15 +1308,21 @@ var DEPTH_LEVELS = [
1124
1308
  "tooltip",
1125
1309
  "overlay"
1126
1310
  ];
1127
- var MOTION_DURATION_SCALE = ["instant", "fast", "normal", "slow", "slower"];
1311
+ var MOTION_DURATION_SCALE = [
1312
+ "instant",
1313
+ "micro",
1314
+ "fast",
1315
+ "moderate",
1316
+ "normal",
1317
+ "slow"
1318
+ ];
1128
1319
  var EASING_CURVES = [
1320
+ "standard",
1321
+ "enter",
1322
+ "exit",
1129
1323
  "linear",
1130
- "ease-in",
1131
- "ease-out",
1132
- "ease-in-out",
1133
- "productive",
1134
- "expressive",
1135
- "spring"
1324
+ "spring-smooth",
1325
+ "spring-snappy"
1136
1326
  ];
1137
1327
  var BREAKPOINT_SCALE = ["sm", "md", "lg", "xl", "2xl"];
1138
1328
 
@@ -21442,6 +21632,13 @@ var ExampleSchema = external_exports.object({
21442
21632
  description: external_exports.string().optional()
21443
21633
  });
21444
21634
  var ColorIntelligenceSchema = external_exports.object({
21635
+ // Designer-facing evocative handle ("Aged Terracotta", "Solar Citrine") from
21636
+ // the intelligence generator. Language, not identity: the deterministic
21637
+ // `name` on ColorValue is the stable, parseable address; `label` is the
21638
+ // voice. Optional because pre-label intelligence persists in Vectorize and
21639
+ // token files. Backported from platform's generateColorIntelligence, which
21640
+ // requires it on generation.
21641
+ label: external_exports.string().optional(),
21445
21642
  reasoning: external_exports.string(),
21446
21643
  emotionalImpact: external_exports.string(),
21447
21644
  culturalContext: external_exports.string(),
@@ -21684,7 +21881,7 @@ var TokenSchema = external_exports.object({
21684
21881
  motionIntent: external_exports.enum(["enter", "exit", "emphasis", "transition"]).optional(),
21685
21882
  easingCurve: external_exports.tuple([external_exports.number(), external_exports.number(), external_exports.number(), external_exports.number()]).optional(),
21686
21883
  // cubicBezier [x1, y1, x2, y2]
21687
- easingName: external_exports.enum(["linear", "ease-in", "ease-out", "ease-in-out", "productive", "expressive", "spring"]).optional(),
21884
+ easingName: external_exports.enum(["standard", "enter", "exit", "linear", "spring-smooth", "spring-snappy"]).optional(),
21688
21885
  delayMs: external_exports.number().optional(),
21689
21886
  // Delay before animation starts
21690
21887
  // Keyframe tokens (CSS @keyframes content / animation step definitions)
@@ -22301,78 +22498,216 @@ var DEFAULT_SHADOW_DEFINITIONS = {
22301
22498
  };
22302
22499
  var DEFAULT_DURATION_DEFINITIONS = {
22303
22500
  instant: {
22304
- step: "instant",
22305
- meaning: "Instant - no animation",
22306
- contexts: ["disabled-motion", "prefers-reduced-motion"],
22501
+ ms: 0,
22502
+ band: "",
22503
+ meaning: "No perceptible transition. Cursor changes, text selection, badge counts. Below all perception -- there is nothing to track, so nothing is communicated.",
22504
+ contexts: ["disabled-motion", "prefers-reduced-motion", "cursor", "badge-count"],
22505
+ motionIntent: "transition"
22506
+ },
22507
+ micro: {
22508
+ ms: 100,
22509
+ band: "at the instantaneous threshold (Nielsen 0.1s)",
22510
+ meaning: "Immediate but visible. Focus rings and press feedback. At the instantaneous threshold -- acknowledgment that input landed, not communication of a change.",
22511
+ contexts: ["focus", "press", "micro-feedback"],
22307
22512
  motionIntent: "transition"
22308
22513
  },
22309
22514
  fast: {
22310
- step: -1,
22311
- meaning: "Fast - micro-interactions, hover states",
22312
- contexts: ["hover", "focus", "active", "micro-feedback"],
22515
+ ms: 150,
22516
+ band: "below the communicative window",
22517
+ meaning: "Hover states. The cursor is already there, so the response must match its speed. Below the communicative window -- acknowledgment, not communication.",
22518
+ contexts: ["hover", "micro-feedback"],
22313
22519
  motionIntent: "transition"
22314
22520
  },
22315
- normal: {
22316
- step: 0,
22317
- meaning: "Normal - standard UI transitions",
22318
- contexts: ["buttons", "toggles", "state-changes"],
22521
+ moderate: {
22522
+ ms: 250,
22523
+ band: "communicative (~200-300ms)",
22524
+ meaning: "Dropdowns, tab switches, small reveals. The communicative window: fast enough to feel responsive, slow enough for the eye to track a trajectory and build a spatial model.",
22525
+ contexts: ["dropdowns", "tab-switches", "small-reveals"],
22319
22526
  motionIntent: "transition"
22320
22527
  },
22321
- slow: {
22322
- step: 1,
22323
- meaning: "Slow - enter/exit animations",
22324
- contexts: ["modals", "dialogs", "panels", "enter-exit"],
22528
+ normal: {
22529
+ ms: 350,
22530
+ band: "communicative, larger movement",
22531
+ meaning: "The workhorse -- modal entrances, toggles, standard state transitions. The communicative window for larger movement.",
22532
+ contexts: ["modals", "toggles", "state-changes"],
22325
22533
  motionIntent: "enter"
22326
22534
  },
22327
- slower: {
22328
- step: 2,
22329
- meaning: "Slower - emphasis, large element transitions",
22330
- contexts: ["page-transitions", "hero-animations", "emphasis"],
22331
- motionIntent: "emphasis"
22535
+ slow: {
22536
+ ms: 500,
22537
+ band: "at the sluggish boundary",
22538
+ meaning: "Sheets, page transitions, large spatial movement where the user needs orientation. At the sluggish boundary -- the ceiling for anything but full-screen spatial transitions.",
22539
+ contexts: ["sheets", "page-transitions", "large-spatial-movement"],
22540
+ motionIntent: "enter"
22332
22541
  }
22333
22542
  };
22334
22543
  var DEFAULT_EASING_DEFINITIONS = {
22544
+ standard: {
22545
+ curve: [0.25, 0.1, 0.25, 1],
22546
+ meaning: "Precision -- arrives exactly where it should, engineered rather than thrown. Decelerates into its final position. General-purpose state transitions and hover.",
22547
+ contexts: ["state-changes", "hover", "general-purpose"],
22548
+ css: "cubic-bezier(0.25, 0.1, 0.25, 1)"
22549
+ },
22550
+ enter: {
22551
+ curve: [0, 0, 0.2, 1],
22552
+ meaning: "Arrival, welcome, settling into place. The fast start communicates responsiveness; the slow finish communicates care. Anything entering the viewport. Paired with `exit` as a deliberately asymmetric couple. (Dragicevic 2011: slow-in/slow-out outperforms constant speed for tracking.)",
22553
+ contexts: ["enter-animations", "elements-appearing"],
22554
+ css: "cubic-bezier(0, 0, 0.2, 1)"
22555
+ },
22556
+ exit: {
22557
+ curve: [0.4, 0, 1, 1],
22558
+ meaning: "Withdrawal, giving space. The brief hesitation acknowledges the user; the fast departure avoids lingering. Anything leaving the viewport. Paired with `enter` as a deliberately asymmetric couple -- exits are faster than entrances by design.",
22559
+ contexts: ["exit-animations", "elements-leaving"],
22560
+ css: "cubic-bezier(0.4, 0, 1, 1)"
22561
+ },
22335
22562
  linear: {
22336
22563
  curve: [0, 0, 1, 1],
22337
- meaning: "Linear - constant speed, mechanical feel",
22338
- contexts: ["progress-bars", "loading-spinners", "opacity-fades"],
22564
+ meaning: "Mechanical, procedural, without personality -- a process, not a gesture. Progress and loading, where the system is working rather than interacting. Never for interactive spatial transitions.",
22565
+ contexts: ["progress-bars", "loading-spinners", "opacity-fades", "focus-ring-fade"],
22339
22566
  css: "linear"
22340
22567
  },
22341
- "ease-in": {
22342
- curve: [0.42, 0, 1, 1],
22343
- meaning: "Ease in - starts slow, accelerates (exiting)",
22344
- contexts: ["exit-animations", "elements-leaving"],
22345
- css: "cubic-bezier(0.42, 0, 1, 1)"
22568
+ "spring-smooth": {
22569
+ curve: [0.2, 0.9, 0.3, 1],
22570
+ meaning: "Alive, coming physically to rest -- a critically-damped spring with no overshoot. Page transitions, sheets, large tracked spatial movement. (Johansson 1973 / Pratt 2010: animate motion is perceived as alive and captures attention.)",
22571
+ contexts: ["page-transitions", "sheets", "large-spatial-movement"],
22572
+ css: "cubic-bezier(0.2, 0.9, 0.3, 1)"
22346
22573
  },
22347
- "ease-out": {
22348
- curve: [0, 0, 0.58, 1],
22349
- meaning: "Ease out - starts fast, decelerates (entering)",
22350
- contexts: ["enter-animations", "elements-appearing"],
22351
- css: "cubic-bezier(0, 0, 0.58, 1)"
22352
- },
22353
- "ease-in-out": {
22354
- curve: [0.42, 0, 0.58, 1],
22355
- meaning: "Ease in-out - symmetric acceleration/deceleration",
22356
- contexts: ["state-changes", "transforms", "general-purpose"],
22357
- css: "cubic-bezier(0.42, 0, 0.58, 1)"
22358
- },
22359
- productive: {
22360
- curve: [0.2, 0, 0.38, 0.9],
22361
- meaning: "Productive - quick, efficient, minimal overshoot",
22362
- contexts: ["work-ui", "data-displays", "business-apps"],
22363
- css: "cubic-bezier(0.2, 0, 0.38, 0.9)"
22364
- },
22365
- expressive: {
22366
- curve: [0.4, 0.14, 0.3, 1],
22367
- meaning: "Expressive - dramatic, attention-grabbing",
22368
- contexts: ["marketing", "onboarding", "celebrations", "emphasis"],
22369
- css: "cubic-bezier(0.4, 0.14, 0.3, 1)"
22370
- },
22371
- spring: {
22372
- curve: [0.175, 0.885, 0.32, 1.275],
22373
- meaning: "Spring - bouncy overshoot for playful feel",
22374
- contexts: ["buttons", "icons", "playful-ui", "celebrations"],
22375
- css: "cubic-bezier(0.175, 0.885, 0.32, 1.275)"
22574
+ "spring-snappy": {
22575
+ curve: [0.2, 0.8, 0.2, 1],
22576
+ meaning: "Alive, tight, following input closely -- a tighter spring with less settle and no overshoot. Toggles, presses, interactions that track input.",
22577
+ contexts: ["toggles", "presses", "input-tracking"],
22578
+ css: "cubic-bezier(0.2, 0.8, 0.2, 1)"
22579
+ }
22580
+ };
22581
+ var DEFAULT_MOTION_SEMANTIC_MAPPINGS = {
22582
+ hover: {
22583
+ properties: ["color", "background-color", "border-color"],
22584
+ durationTier: "fast",
22585
+ curve: "standard",
22586
+ reducedMotion: null,
22587
+ category: "interaction",
22588
+ sizeReasoning: "Colour only, no movement -- the cursor is already on target, so the response matches its speed at the fast tier.",
22589
+ meaning: "Hover-state colour transition. Acknowledges pointer presence.",
22590
+ contexts: ["hover", "links", "interactive-surfaces"]
22591
+ },
22592
+ focus: {
22593
+ properties: ["box-shadow", "outline-color"],
22594
+ durationTier: "micro",
22595
+ curve: "linear",
22596
+ reducedMotion: null,
22597
+ category: "interaction",
22598
+ sizeReasoning: "A ring appearing, not moving -- micro tier at linear velocity reads as the system marking focus, not a gesture. (Tailwind ring is box-shadow.)",
22599
+ meaning: "Focus-ring transition. Marks the focused element.",
22600
+ contexts: ["focus-visible", "keyboard-navigation"]
22601
+ },
22602
+ press: {
22603
+ properties: ["transform", "color", "background-color"],
22604
+ durationTier: "micro",
22605
+ curve: "spring-snappy",
22606
+ reducedMotion: { properties: ["color", "background-color"] },
22607
+ category: "interaction",
22608
+ sizeReasoning: "The fastest, tightest feedback -- micro tier with a snappy spring follows the finger. Under reduced motion the transform drops; the colour change survives.",
22609
+ meaning: "Press/active feedback. Confirms the input was received.",
22610
+ contexts: ["press", "active", "buttons"]
22611
+ },
22612
+ toggle: {
22613
+ properties: ["color", "background-color", "transform"],
22614
+ durationTier: "moderate",
22615
+ curve: "spring-snappy",
22616
+ reducedMotion: { properties: ["color", "background-color"] },
22617
+ category: "interaction",
22618
+ sizeReasoning: "A thumb travelling a track is a small, tracked movement -- moderate tier, snappy spring. Reduced motion drops the transform to a colour cross-fade.",
22619
+ meaning: "Toggle/switch state change. Shows the new state.",
22620
+ contexts: ["switch", "toggle", "checkbox"]
22621
+ },
22622
+ "dropdown-in": {
22623
+ properties: ["opacity", "transform"],
22624
+ durationTier: "moderate",
22625
+ curve: "enter",
22626
+ reducedMotion: { properties: ["opacity"], ms: 100 },
22627
+ category: "enter",
22628
+ sizeReasoning: "A dropdown is small and travels a short distance -- moderate tier, one step below the modal, with the arrival curve.",
22629
+ meaning: "Dropdown/menu entrance.",
22630
+ contexts: ["dropdown", "menu", "select", "popover"]
22631
+ },
22632
+ "dropdown-out": {
22633
+ properties: ["opacity", "transform"],
22634
+ durationTier: "fast",
22635
+ curve: "exit",
22636
+ reducedMotion: { properties: ["opacity"], ms: 100 },
22637
+ category: "exit",
22638
+ sizeReasoning: "The exit of a small element -- fast tier (shorter than its moderate entrance) with the departure curve. The user already chose to dismiss it.",
22639
+ meaning: "Dropdown/menu exit.",
22640
+ contexts: ["dropdown", "menu", "select", "popover"]
22641
+ },
22642
+ "modal-in": {
22643
+ properties: ["opacity", "transform"],
22644
+ durationTier: "normal",
22645
+ curve: "enter",
22646
+ reducedMotion: { properties: ["opacity"], ms: 150 },
22647
+ category: "enter",
22648
+ sizeReasoning: "A modal is larger and travels farther than a dropdown -- normal tier, one step up, with the arrival curve. Size and distance produce the longer duration.",
22649
+ meaning: "Modal/dialog entrance.",
22650
+ contexts: ["modal", "dialog", "alert-dialog"]
22651
+ },
22652
+ "modal-out": {
22653
+ properties: ["opacity", "transform"],
22654
+ durationTier: "moderate",
22655
+ curve: "exit",
22656
+ reducedMotion: { properties: ["opacity"], ms: 150 },
22657
+ category: "exit",
22658
+ sizeReasoning: "The modal exit -- moderate tier (shorter than its normal entrance) with the departure curve.",
22659
+ meaning: "Modal/dialog exit.",
22660
+ contexts: ["modal", "dialog", "alert-dialog"]
22661
+ },
22662
+ "sheet-in": {
22663
+ properties: ["transform"],
22664
+ durationTier: "slow",
22665
+ curve: "spring-smooth",
22666
+ reducedMotion: { properties: ["opacity"], ms: 250 },
22667
+ category: "enter",
22668
+ sizeReasoning: "A sheet is the largest spatial movement -- slow tier with the physical settle of a smooth spring, because the user must track it into place. Reduced motion becomes a cross-fade.",
22669
+ meaning: "Sheet/drawer entrance.",
22670
+ contexts: ["sheet", "drawer", "side-panel"]
22671
+ },
22672
+ "sheet-out": {
22673
+ properties: ["transform"],
22674
+ durationTier: "normal",
22675
+ curve: "exit",
22676
+ reducedMotion: { properties: ["opacity"], ms: 250 },
22677
+ category: "exit",
22678
+ sizeReasoning: "The sheet exit -- normal tier (shorter than its slow entrance) with the departure curve.",
22679
+ meaning: "Sheet/drawer exit.",
22680
+ contexts: ["sheet", "drawer", "side-panel"]
22681
+ },
22682
+ expand: {
22683
+ properties: ["grid-template-rows", "opacity"],
22684
+ durationTier: "normal",
22685
+ curve: "enter",
22686
+ reducedMotion: { properties: ["opacity"] },
22687
+ category: "enter",
22688
+ sizeReasoning: "Content unfolding to its natural height -- normal tier with the arrival curve. Transitions grid-template-rows (0fr->1fr), the transitionable stand-in for height:auto. Reduced motion snaps the rows and fades opacity.",
22689
+ meaning: "Expand/reveal collapsible content (accordion, disclosure).",
22690
+ contexts: ["accordion", "collapsible", "disclosure"]
22691
+ },
22692
+ collapse: {
22693
+ properties: ["grid-template-rows", "opacity"],
22694
+ durationTier: "moderate",
22695
+ curve: "exit",
22696
+ reducedMotion: { properties: ["opacity"] },
22697
+ category: "exit",
22698
+ sizeReasoning: "Content folding away -- moderate tier (shorter than its normal expansion) with the departure curve. Reduced motion snaps the rows.",
22699
+ meaning: "Collapse/hide collapsible content (accordion, disclosure).",
22700
+ contexts: ["accordion", "collapsible", "disclosure"]
22701
+ },
22702
+ page: {
22703
+ properties: ["opacity", "transform"],
22704
+ durationTier: "slow",
22705
+ curve: "spring-smooth",
22706
+ reducedMotion: { properties: ["opacity"], ms: 200 },
22707
+ category: "enter",
22708
+ sizeReasoning: "A whole-view transition -- slow tier with the physical settle of a smooth spring, because the user reorients across the largest possible distance.",
22709
+ meaning: "Page/route transition.",
22710
+ contexts: ["page-transition", "route-change", "view-switch"]
22376
22711
  }
22377
22712
  };
22378
22713
  var DEFAULT_DELAY_DEFINITIONS = {
@@ -24997,7 +25332,14 @@ function tryParseUnit(cssValue, registry2 = DEFAULT_UNITS) {
24997
25332
  }
24998
25333
 
24999
25334
  // ../design-tokens/src/generators/motion.ts
25000
- function generateMotionTokens(config2, durationDefs, easingDefs, delayDefs) {
25335
+ var LEGACY_EASING_REMAP = {
25336
+ linear: "linear",
25337
+ "ease-out": "enter",
25338
+ "ease-in": "exit",
25339
+ "ease-in-out": "standard",
25340
+ spring: "spring-snappy"
25341
+ };
25342
+ function generateMotionTokens(config2, durationDefs, easingDefs, delayDefs, semanticMappings) {
25001
25343
  const tokens = [];
25002
25344
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
25003
25345
  const { baseTransitionDuration, progressionRatio } = config2;
@@ -25009,52 +25351,44 @@ function generateMotionTokens(config2, durationDefs, easingDefs, delayDefs) {
25009
25351
  value: `${baseTransitionDuration}ms`,
25010
25352
  category: "motion",
25011
25353
  namespace: "motion",
25012
- semanticMeaning: "Base transition duration - all motion timing derives from this",
25013
- usageContext: ["calculation-reference"],
25354
+ semanticMeaning: "Legacy base transition duration. The perceptual duration scale (motion-duration-*) no longer derives from this; retained only as a reference value and delay-progression base.",
25355
+ usageContext: ["calculation-reference", "delay-base"],
25014
25356
  progressionSystem: progressionRatio,
25015
- description: `Base duration (${baseTransitionDuration}ms). Motion uses ${progressionRatio} progression (ratio ${ratioVal}).`,
25357
+ description: `Base duration (${baseTransitionDuration}ms). Delay tokens use ${progressionRatio} progression (ratio ${ratioVal}); duration tiers are perceptually derived literals.`,
25016
25358
  generatedAt: timestamp,
25017
25359
  containerQueryAware: false,
25018
25360
  reducedMotionAware: true,
25019
25361
  userOverride: null,
25020
25362
  usagePatterns: {
25021
- do: ["Reference as the calculation base"],
25022
- never: ["Change without understanding full motion scale impact"]
25363
+ do: ["Reference as the delay-progression base"],
25364
+ never: ["Assume the perceptual duration tiers derive from this"]
25023
25365
  }
25024
25366
  });
25025
25367
  for (const scale2 of MOTION_DURATION_SCALE) {
25026
25368
  const def = durationDefs[scale2];
25027
25369
  if (!def) continue;
25028
25370
  const scaleIndex = MOTION_DURATION_SCALE.indexOf(scale2);
25029
- let durationMs;
25030
- let mathRelationship;
25031
- if (def.step === "instant") {
25032
- durationMs = 0;
25033
- mathRelationship = "0";
25034
- } else {
25035
- durationMs = Math.round(computeStep(baseTransitionDuration, def.step));
25036
- mathRelationship = def.step === 0 ? `${baseTransitionDuration}ms (base)` : `${baseTransitionDuration} \xD7 ${ratioVal}^${def.step}`;
25037
- }
25371
+ const durationMs = def.ms;
25372
+ const bandNote = def.band ? ` Band: ${def.band}.` : "";
25038
25373
  tokens.push({
25039
25374
  name: `motion-duration-${scale2}`,
25040
- value: durationMs === 0 ? "0ms" : `${durationMs}ms`,
25375
+ value: `${durationMs}ms`,
25041
25376
  category: "motion",
25042
25377
  namespace: "motion",
25043
- semanticMeaning: def.meaning,
25378
+ semanticMeaning: `${def.meaning}${bandNote}`,
25044
25379
  usageContext: def.contexts,
25045
25380
  scalePosition: scaleIndex,
25046
- progressionSystem: progressionRatio,
25047
25381
  motionIntent: def.motionIntent,
25048
25382
  motionDuration: durationMs,
25049
- mathRelationship,
25050
- dependsOn: def.step === "instant" ? [] : ["motion-duration-base"],
25051
- description: `Duration ${scale2}: ${durationMs}ms. ${def.meaning}`,
25383
+ mathRelationship: def.band ? `${durationMs}ms (perceptual: ${def.band})` : `${durationMs}ms`,
25384
+ dependsOn: [],
25385
+ description: `Duration ${scale2}: ${durationMs}ms.${bandNote} ${def.meaning}`,
25052
25386
  generatedAt: timestamp,
25053
25387
  containerQueryAware: false,
25054
25388
  reducedMotionAware: true,
25055
25389
  userOverride: null,
25056
25390
  usagePatterns: {
25057
- do: scale2 === "instant" ? ["Use for prefers-reduced-motion", "Use for disabled animations"] : scale2 === "fast" ? ["Use for hover/focus states", "Use for micro-interactions"] : scale2 === "normal" ? ["Use for most UI transitions", "Use for state changes"] : ["Use for enter/exit animations", "Use for emphasis"],
25391
+ do: scale2 === "instant" ? ["Use for prefers-reduced-motion", "Use for disabled animations"] : scale2 === "micro" || scale2 === "fast" ? ["Use for acknowledgment feedback (hover, focus, press)"] : ["Use for communicative transitions where the user must track the change"],
25058
25392
  never: ["Ignore prefers-reduced-motion", "Use slow animations for frequent actions"]
25059
25393
  }
25060
25394
  });
@@ -25077,10 +25411,10 @@ function generateMotionTokens(config2, durationDefs, easingDefs, delayDefs) {
25077
25411
  reducedMotionAware: true,
25078
25412
  userOverride: null,
25079
25413
  usagePatterns: {
25080
- do: curve === "linear" ? ["Use for opacity fades", "Use for progress indicators"] : curve === "ease-out" ? ["Use for entering elements", "Use for appearing content"] : curve === "ease-in" ? ["Use for exiting elements", "Use for disappearing content"] : ["Use for general transitions", "Match context to curve feel"],
25414
+ do: curve === "linear" ? ["Use for progress and loading, where the system is working not interacting"] : curve === "enter" ? ["Use for anything entering the viewport"] : curve === "exit" ? ["Use for anything leaving the viewport"] : ["Match the curve register to the interaction feel"],
25081
25415
  never: [
25082
- "Use ease-in for entering (feels sluggish)",
25083
- "Use ease-out for exiting (feels abrupt)"
25416
+ "Use exit for entering (arrives reluctantly)",
25417
+ "Use linear for interactive spatial transitions (reads as mechanical)"
25084
25418
  ]
25085
25419
  }
25086
25420
  });
@@ -25417,13 +25751,13 @@ function generateMotionTokens(config2, durationDefs, easingDefs, delayDefs) {
25417
25751
  } else {
25418
25752
  const durationDef = durationDefs[anim.duration];
25419
25753
  if (!durationDef) continue;
25420
- const durationMs = durationDef.step === "instant" ? 0 : Math.round(computeStep(baseTransitionDuration, durationDef.step));
25421
- durationValue = `${durationMs}ms`;
25754
+ durationValue = `${durationDef.ms}ms`;
25422
25755
  durationRef = `var(--motion-duration-${anim.duration})`;
25423
25756
  }
25424
- const easingDef = easingDefs[anim.easing];
25757
+ const easingKey = LEGACY_EASING_REMAP[anim.easing] ?? anim.easing;
25758
+ const easingDef = easingDefs[easingKey];
25425
25759
  if (!easingDef) continue;
25426
- const easingRef = `var(--motion-easing-${anim.easing})`;
25760
+ const easingRef = `var(--motion-easing-${easingKey})`;
25427
25761
  const iterations = anim.iterations || "";
25428
25762
  const animValue = iterations ? `${anim.keyframe} ${durationRef} ${easingRef} ${iterations}` : `${anim.keyframe} ${durationRef} ${easingRef}`;
25429
25763
  tokens.push({
@@ -25441,7 +25775,7 @@ function generateMotionTokens(config2, durationDefs, easingDefs, delayDefs) {
25441
25775
  dependsOn: [
25442
25776
  `motion-keyframe-${anim.keyframe}`,
25443
25777
  ...anim.duration.endsWith("s") || anim.duration.endsWith("ms") ? [] : [`motion-duration-${anim.duration}`],
25444
- `motion-easing-${anim.easing}`
25778
+ `motion-easing-${easingKey}`
25445
25779
  ],
25446
25780
  description: `Animation ${anim.name}: ${anim.meaning}`,
25447
25781
  generatedAt: timestamp,
@@ -25489,14 +25823,10 @@ function generateMotionTokens(config2, durationDefs, easingDefs, delayDefs) {
25489
25823
  ];
25490
25824
  for (const comp of composites2) {
25491
25825
  const durationDef = durationDefs[comp.duration];
25492
- const easingDef = easingDefs[comp.easing];
25826
+ const easingKey = LEGACY_EASING_REMAP[comp.easing] ?? comp.easing;
25827
+ const easingDef = easingDefs[easingKey];
25493
25828
  if (!durationDef || !easingDef) continue;
25494
- let durationMs;
25495
- if (durationDef.step === "instant") {
25496
- durationMs = 0;
25497
- } else {
25498
- durationMs = Math.round(computeStep(baseTransitionDuration, durationDef.step));
25499
- }
25829
+ const durationMs = durationDef.ms;
25500
25830
  tokens.push({
25501
25831
  name: comp.name,
25502
25832
  value: `${durationMs}ms ${easingDef.css}`,
@@ -25506,27 +25836,54 @@ function generateMotionTokens(config2, durationDefs, easingDefs, delayDefs) {
25506
25836
  usageContext: comp.contexts,
25507
25837
  motionDuration: durationMs,
25508
25838
  easingCurve: easingDef.curve,
25509
- easingName: comp.easing,
25510
- dependsOn: [`motion-duration-${comp.duration}`, `motion-easing-${comp.easing}`],
25511
- description: `${comp.meaning}. Combines ${comp.duration} duration with ${comp.easing} easing.`,
25839
+ easingName: easingKey,
25840
+ dependsOn: [`motion-duration-${comp.duration}`, `motion-easing-${easingKey}`],
25841
+ description: `${comp.meaning}. Combines ${comp.duration} duration with ${easingKey} easing.`,
25512
25842
  generatedAt: timestamp,
25513
25843
  containerQueryAware: false,
25514
25844
  reducedMotionAware: true,
25515
25845
  userOverride: null
25516
25846
  });
25517
25847
  }
25848
+ for (const [name, mapping] of Object.entries(semanticMappings)) {
25849
+ tokens.push({
25850
+ name: `motion-semantic-${name}`,
25851
+ value: JSON.stringify({
25852
+ properties: mapping.properties,
25853
+ durationTier: mapping.durationTier,
25854
+ curve: mapping.curve,
25855
+ reducedMotion: mapping.reducedMotion
25856
+ }),
25857
+ category: "motion",
25858
+ namespace: "motion",
25859
+ semanticMeaning: `${mapping.meaning} ${mapping.sizeReasoning}`,
25860
+ usageContext: mapping.contexts,
25861
+ motionIntent: mapping.category === "enter" ? "enter" : mapping.category === "exit" ? "exit" : "transition",
25862
+ generateUtilityClass: true,
25863
+ dependsOn: [`motion-duration-${mapping.durationTier}`, `motion-easing-${mapping.curve}`],
25864
+ description: `Semantic motion motion-${name}: ${mapping.properties.join(", ")} over ${mapping.durationTier} with ${mapping.curve}. ${mapping.sizeReasoning}`,
25865
+ generatedAt: timestamp,
25866
+ containerQueryAware: false,
25867
+ reducedMotionAware: true,
25868
+ userOverride: null,
25869
+ usagePatterns: {
25870
+ do: [`Apply class motion-${name} and let a state/presence change trigger the transition`],
25871
+ never: ["Hardcode a raw numeric duration in place of this token"]
25872
+ }
25873
+ });
25874
+ }
25518
25875
  tokens.push({
25519
25876
  name: "motion-progression",
25520
25877
  value: JSON.stringify({
25521
25878
  ratio: progressionRatio,
25522
25879
  ratioValue: ratioVal,
25523
25880
  baseDuration: baseTransitionDuration,
25524
- note: "Motion timing uses step-based progression (base * ratio^step) for unified feel"
25881
+ note: "Duration tiers are perceptually derived literals (docs/MOTION.md); delay tokens use the workspace progression ratio from the base duration."
25525
25882
  }),
25526
25883
  category: "motion",
25527
25884
  namespace: "motion",
25528
- semanticMeaning: "Metadata about the motion progression system",
25529
- description: `Motion uses ${progressionRatio} progression from ${baseTransitionDuration}ms base.`,
25885
+ semanticMeaning: "Metadata about the motion system",
25886
+ description: `Duration tiers are perceptual literals; delays use ${progressionRatio} progression from ${baseTransitionDuration}ms base.`,
25530
25887
  generatedAt: timestamp,
25531
25888
  containerQueryAware: false,
25532
25889
  userOverride: null
@@ -26436,7 +26793,8 @@ function createGeneratorDefs(colorPaletteBases) {
26436
26793
  config2,
26437
26794
  DEFAULT_DURATION_DEFINITIONS,
26438
26795
  DEFAULT_EASING_DEFINITIONS,
26439
- DEFAULT_DELAY_DEFINITIONS
26796
+ DEFAULT_DELAY_DEFINITIONS,
26797
+ DEFAULT_MOTION_SEMANTIC_MAPPINGS
26440
26798
  )
26441
26799
  },
26442
26800
  {
@@ -27085,6 +27443,11 @@ async function regenerateOutputs(registry2, input, hooks2 = {}) {
27085
27443
  await writeFile(join2(outputDir2, "rafters.standalone.css"), compiled);
27086
27444
  written.push("rafters.standalone.css");
27087
27445
  }
27446
+ if (exports.documentation) {
27447
+ const doc = await registryToDocumentation(registry2);
27448
+ await writeFile(join2(outputDir2, "rafters.documentation.css"), doc);
27449
+ written.push("rafters.documentation.css");
27450
+ }
27088
27451
  hooks2.notify?.();
27089
27452
  return written;
27090
27453
  }
@@ -27444,7 +27807,7 @@ var RegistryFileSchema = external_exports.object({
27444
27807
  devDependencies: external_exports.array(external_exports.string()).default([])
27445
27808
  // e.g., ["vitest"] - from @devDependencies JSDoc
27446
27809
  });
27447
- var RegistryItemTypeSchema = external_exports.enum(["ui", "primitive", "composite", "rule"]);
27810
+ var RegistryItemTypeSchema = external_exports.enum(["ui", "primitive", "composite", "rule", "substrate"]);
27448
27811
  var RegistryItemIntelligenceSchema = external_exports.object({
27449
27812
  cognitiveLoad: external_exports.number().optional(),
27450
27813
  attentionEconomics: external_exports.string().optional(),
@@ -27472,7 +27835,8 @@ var RegistryIndexSchema = external_exports.object({
27472
27835
  components: external_exports.array(external_exports.string()),
27473
27836
  primitives: external_exports.array(external_exports.string()),
27474
27837
  composites: external_exports.array(external_exports.string()).default([]),
27475
- rules: external_exports.array(external_exports.string()).default([])
27838
+ rules: external_exports.array(external_exports.string()).default([]),
27839
+ substrate: external_exports.array(external_exports.string()).default([])
27476
27840
  });
27477
27841
 
27478
27842
  // src/registry/client.ts
@@ -27481,7 +27845,8 @@ var REGISTRY_SOURCE = {
27481
27845
  ui: { folder: "components", label: "Component" },
27482
27846
  primitive: { folder: "primitives", label: "Primitive" },
27483
27847
  composite: { folder: "composites", label: "Composite" },
27484
- rule: { folder: "rules", label: "Rule" }
27848
+ rule: { folder: "rules", label: "Rule" },
27849
+ substrate: { folder: "substrate", label: "Substrate" }
27485
27850
  };
27486
27851
  var FETCH_ORDER = Object.keys(REGISTRY_SOURCE);
27487
27852
  var RegistryClient = class {
@@ -27517,7 +27882,8 @@ var RegistryClient = class {
27517
27882
  ui: fetchFrom("ui"),
27518
27883
  primitive: fetchFrom("primitive"),
27519
27884
  composite: fetchFrom("composite"),
27520
- rule: fetchFrom("rule")
27885
+ rule: fetchFrom("rule"),
27886
+ substrate: fetchFrom("substrate")
27521
27887
  };
27522
27888
  this.fetchComponent = this.fetchByType.ui;
27523
27889
  this.fetchPrimitive = this.fetchByType.primitive;
@@ -27799,7 +28165,8 @@ var DEFAULT_EXPORTS = {
27799
28165
  tailwind: true,
27800
28166
  typescript: true,
27801
28167
  dtcg: false,
27802
- compiled: false
28168
+ compiled: false,
28169
+ documentation: false
27803
28170
  };
27804
28171
  var EXPORT_CHOICES = [
27805
28172
  {
@@ -27821,6 +28188,11 @@ var EXPORT_CHOICES = [
27821
28188
  name: "Standalone CSS (pre-built, no Tailwind required)",
27822
28189
  value: "compiled",
27823
28190
  checked: false
28191
+ },
28192
+ {
28193
+ name: "Documentation CSS (complete utility surface for docs/previews)",
28194
+ value: "documentation",
28195
+ checked: false
27824
28196
  }
27825
28197
  ];
27826
28198
  var FUTURE_EXPORTS = [
@@ -27844,7 +28216,8 @@ function selectionsToConfig(selections) {
27844
28216
  tailwind: selections.includes("tailwind"),
27845
28217
  typescript: selections.includes("typescript"),
27846
28218
  dtcg: selections.includes("dtcg"),
27847
- compiled: selections.includes("compiled")
28219
+ compiled: selections.includes("compiled"),
28220
+ documentation: selections.includes("documentation")
27848
28221
  };
27849
28222
  }
27850
28223
 
@@ -28488,6 +28861,7 @@ function isSharedFile(path) {
28488
28861
  }
28489
28862
  return false;
28490
28863
  }
28864
+ var REACT_FALLBACK_TARGETS = /* @__PURE__ */ new Set(["astro", "vue", "svelte"]);
28491
28865
  function selectFilesForFramework(files, target) {
28492
28866
  const preferredExt = targetToExtension(target);
28493
28867
  const shared = files.filter((f) => isSharedFile(f.path));
@@ -28495,12 +28869,15 @@ function selectFilesForFramework(files, target) {
28495
28869
  if (matched.length > 0) {
28496
28870
  return { files: [...matched, ...shared], fallback: false };
28497
28871
  }
28498
- if (target !== "react") {
28872
+ if (REACT_FALLBACK_TARGETS.has(target)) {
28499
28873
  const fallbackFiles = files.filter((f) => f.path.endsWith(".tsx"));
28500
28874
  if (fallbackFiles.length > 0) {
28501
28875
  return { files: [...fallbackFiles, ...shared], fallback: true };
28502
28876
  }
28503
28877
  }
28878
+ if (target !== "react" && !REACT_FALLBACK_TARGETS.has(target)) {
28879
+ return { files: shared, fallback: false };
28880
+ }
28504
28881
  return { files, fallback: false };
28505
28882
  }
28506
28883
  var TARGET_GATED_COMPOSITE_FILES = [
@@ -28515,12 +28892,13 @@ function selectCompositeFiles(files, target) {
28515
28892
  var FOLDER_NAMES = /* @__PURE__ */ new Set(["composites"]);
28516
28893
  function isAlreadyInstalled(config2, item) {
28517
28894
  if (!config2?.installed) return false;
28518
- const { components, primitives, composites: composites2, rules } = config2.installed;
28895
+ const { components, primitives, composites: composites2, rules, substrate } = config2.installed;
28519
28896
  const bucketByType = {
28520
28897
  ui: components,
28521
28898
  primitive: primitives,
28522
28899
  composite: composites2 ?? [],
28523
- rule: rules ?? []
28900
+ rule: rules ?? [],
28901
+ substrate: substrate ?? []
28524
28902
  };
28525
28903
  return bucketByType[item.type].includes(item.name);
28526
28904
  }
@@ -28539,11 +28917,13 @@ function trackInstalled(config2, items) {
28539
28917
  const installed = config2.installed;
28540
28918
  if (!installed.composites) installed.composites = [];
28541
28919
  if (!installed.rules) installed.rules = [];
28920
+ if (!installed.substrate) installed.substrate = [];
28542
28921
  const bucketByType = {
28543
28922
  ui: installed.components,
28544
28923
  primitive: installed.primitives,
28545
28924
  composite: installed.composites,
28546
- rule: installed.rules
28925
+ rule: installed.rules,
28926
+ substrate: installed.substrate
28547
28927
  };
28548
28928
  for (const item of items) {
28549
28929
  const bucket = bucketByType[item.type];
@@ -28553,11 +28933,12 @@ function trackInstalled(config2, items) {
28553
28933
  installed.primitives.sort();
28554
28934
  installed.composites.sort();
28555
28935
  installed.rules.sort();
28936
+ installed.substrate.sort();
28556
28937
  }
28557
28938
  function rootFor(field, cwd, fallback) {
28558
28939
  return field === void 0 ? fallback : resolveRoot(field, cwd, fallback);
28559
28940
  }
28560
- function transformPath(registryPath, config2, cwd) {
28941
+ function transformPath(registryPath, config2, cwd = process.cwd()) {
28561
28942
  if (!config2) return registryPath;
28562
28943
  const replacements = [
28563
28944
  ["components/ui/", config2.componentsPath, "components/ui"],
@@ -28572,10 +28953,16 @@ function transformPath(registryPath, config2, cwd) {
28572
28953
  }
28573
28954
  return registryPath;
28574
28955
  }
28956
+ function substrateProjectPath(registryPath, config2, cwd = process.cwd()) {
28957
+ const componentsResolved = rootFor(config2?.componentsPath, cwd, "components/ui");
28958
+ const sourceRoot = componentsResolved.replace(/\/?components\/ui$/, "");
28959
+ return sourceRoot ? join7(sourceRoot, registryPath) : registryPath;
28960
+ }
28575
28961
  function fileExists(cwd, relativePath) {
28576
28962
  return existsSync3(join7(cwd, relativePath));
28577
28963
  }
28578
- function transformFileContent(content, config2, fileType = "component", cwd = process.cwd()) {
28964
+ function transformFileContent(content, config2, fileType = "component", cwd = process.cwd(), opts = {}) {
28965
+ const { substrateKinds = [], installPath } = opts;
28579
28966
  let transformed = content;
28580
28967
  const componentsPath = rootFor(config2?.componentsPath, cwd, "components/ui");
28581
28968
  const primitivesPath = rootFor(config2?.primitivesPath, cwd, "lib/primitives");
@@ -28590,26 +28977,23 @@ function transformFileContent(content, config2, fileType = "component", cwd = pr
28590
28977
  /from\s+['"]\.\.\/primitives\/([^'"]+)['"]/g,
28591
28978
  `from '@/${aliasPrimitives}/$1'`
28592
28979
  );
28593
- const aliasSibling = fileType === "primitive" ? aliasPrimitives : aliasComponents;
28980
+ const substrateOwnDir = fileType === "substrate" && installPath ? stripSourceRoot(dirname(installPath)) : null;
28981
+ const aliasSibling = fileType === "primitive" ? aliasPrimitives : substrateOwnDir ?? aliasComponents;
28594
28982
  transformed = transformed.replace(/from\s+['"]\.\/([^'"]+)['"]/g, `from '@/${aliasSibling}/$1'`);
28595
- const aliasLib = stripSourceRoot(dirname(primitivesPath));
28596
- transformed = transformed.replace(
28597
- /from\s+['"]\.\.\/lib\/([^'"]+)['"]/g,
28598
- `from '@/${aliasLib}/$1'`
28599
- );
28600
- const componentsMatch = aliasComponents.match(/^(.*)components\/ui$/);
28601
- const aliasHooks = componentsMatch ? `${componentsMatch[1]}hooks`.replace(/^\//, "") : "hooks";
28602
- transformed = transformed.replace(
28603
- /from\s+['"]\.\.\/hooks\/([^'"]+)['"]/g,
28604
- `from '@/${aliasHooks}/$1'`
28605
- );
28983
+ if (substrateKinds.length > 0) {
28984
+ const kindAlternation = substrateKinds.join("|");
28985
+ transformed = transformed.replace(
28986
+ new RegExp(`from\\s+['"](?:\\.\\./){1,2}(${kindAlternation})/([^'"]+)['"]`, "g"),
28987
+ "from '@/$1/$2'"
28988
+ );
28989
+ }
28606
28990
  transformed = transformed.replace(
28607
- /from\s+['"]\.\.\/(?!lib\/|hooks\/)([^'"]+)['"]/g,
28991
+ /from\s+['"]\.\.\/([^'"]+)['"]/g,
28608
28992
  `from '@/${aliasComponents}/$1'`
28609
28993
  );
28610
28994
  return transformed;
28611
28995
  }
28612
- async function installItem(cwd, item, options, config2) {
28996
+ async function installItem(cwd, item, options, config2, substrateKinds = []) {
28613
28997
  const installedFiles = [];
28614
28998
  let skipped = false;
28615
28999
  let filesToInstall = item.files;
@@ -28625,11 +29009,17 @@ async function installItem(cwd, item, options, config2) {
28625
29009
  message: `No ${targetToExtension(target)} version available for ${item.name}. Installing React version.`
28626
29010
  });
28627
29011
  }
29012
+ const hasComponentFile = selection.files.some((f) => !isSharedFile(f.path));
29013
+ if (!hasComponentFile) {
29014
+ throw new Error(
29015
+ `No ${targetToExtension(target)} version available for "${item.name}" and no compatible fallback exists.`
29016
+ );
29017
+ }
28628
29018
  } else if (item.type === "composite") {
28629
29019
  filesToInstall = selectCompositeFiles(item.files, getComponentTarget(config2));
28630
29020
  }
28631
29021
  for (const file2 of filesToInstall) {
28632
- const projectPath2 = transformPath(file2.path, config2, cwd);
29022
+ const projectPath2 = item.type === "substrate" ? substrateProjectPath(file2.path, config2, cwd) : transformPath(file2.path, config2, cwd);
28633
29023
  const targetPath = join7(cwd, projectPath2);
28634
29024
  if (fileExists(cwd, projectPath2)) {
28635
29025
  if (!options.overwrite) {
@@ -28644,8 +29034,11 @@ async function installItem(cwd, item, options, config2) {
28644
29034
  }
28645
29035
  }
28646
29036
  await mkdir2(dirname(targetPath), { recursive: true });
28647
- const fileType = item.type === "primitive" ? "primitive" : "component";
28648
- const transformedContent = transformFileContent(file2.content, config2, fileType, cwd);
29037
+ const fileType = item.type === "primitive" ? "primitive" : item.type === "substrate" ? "substrate" : "component";
29038
+ const transformedContent = transformFileContent(file2.content, config2, fileType, cwd, {
29039
+ substrateKinds,
29040
+ installPath: file2.path
29041
+ });
28649
29042
  await writeFile2(targetPath, transformedContent, "utf-8");
28650
29043
  installedFiles.push(projectPath2);
28651
29044
  }
@@ -28758,6 +29151,11 @@ async function add(componentArgs, options) {
28758
29151
  return;
28759
29152
  }
28760
29153
  }
29154
+ const substrateKinds = [
29155
+ ...new Set(
29156
+ allItems.filter((item) => item.type === "substrate").map((item) => item.files[0]?.path.split("/")[0]).filter((segment) => Boolean(segment))
29157
+ )
29158
+ ];
28761
29159
  const installed = [];
28762
29160
  const skipped = [];
28763
29161
  const installedItems = [];
@@ -28781,7 +29179,7 @@ async function add(componentArgs, options) {
28781
29179
  });
28782
29180
  }
28783
29181
  try {
28784
- const result = await installItem(cwd, item, options, config2);
29182
+ const result = await installItem(cwd, item, options, config2, substrateKinds);
28785
29183
  if (result.installed) {
28786
29184
  installed.push(item.name);
28787
29185
  installedItems.push(item);
@@ -30931,9 +31329,10 @@ function studioApiPlugin() {
30931
31329
  const config2 = await loadStudioConfig();
30932
31330
  const exports = config2?.exports ?? {
30933
31331
  tailwind: true,
30934
- typescript: false,
31332
+ typescript: true,
30935
31333
  dtcg: false,
30936
- compiled: false
31334
+ compiled: false,
31335
+ documentation: false
30937
31336
  };
30938
31337
  await regenerateOutputs(
30939
31338
  registry2,
@@ -30970,21 +31369,27 @@ function studioApiPlugin() {
30970
31369
  }
30971
31370
  };
30972
31371
  {
30973
- const watchConfig = await loadStudioConfig();
30974
- const classDirs = watchConfig ? resolveContentSources(projectPath, watchConfig) : [];
30975
- const watchTargets = [
30976
- join14(tokensDir, "*.rafters.json"),
30977
- configPath,
30978
- ...classDirs.map((dir) => join14(dir, "**/*.classes.ts"))
30979
- ];
30980
- server.watcher.add(watchTargets);
31372
+ const trackedClassDirs = /* @__PURE__ */ new Set();
31373
+ const syncWatchTargets = async () => {
31374
+ const watchConfig = await loadStudioConfig();
31375
+ const classDirs = watchConfig ? resolveContentSources(projectPath, watchConfig) : [];
31376
+ const newDirs = classDirs.filter((d2) => !trackedClassDirs.has(d2));
31377
+ if (newDirs.length > 0) {
31378
+ server.watcher.add(newDirs.map((dir) => join14(dir, "**/*.classes.ts")));
31379
+ for (const d2 of newDirs) trackedClassDirs.add(d2);
31380
+ }
31381
+ };
31382
+ server.watcher.add([join14(tokensDir, "*.rafters.json"), configPath]);
31383
+ await syncWatchTargets();
30981
31384
  let debounce = null;
30982
31385
  const onChange = (changed) => {
30983
31386
  const watched = changed.endsWith(".classes.ts") || changed.endsWith(".rafters.json") || changed === configPath;
30984
31387
  if (!watched) return;
30985
31388
  if (debounce) clearTimeout(debounce);
30986
31389
  debounce = setTimeout(() => {
30987
- void reloadAndRegenerate();
31390
+ const isConfigChange = changed === configPath;
31391
+ const run = isConfigChange ? syncWatchTargets().then(reloadAndRegenerate) : reloadAndRegenerate();
31392
+ void run;
30988
31393
  }, 150);
30989
31394
  };
30990
31395
  server.watcher.on("change", onChange);
@@ -19,13 +19,19 @@ declare const RegistryFileSchema: z.ZodObject<{
19
19
  }, z.core.$strip>;
20
20
  type RegistryFile = z.infer<typeof RegistryFileSchema>;
21
21
  /**
22
- * Item type in registry
22
+ * Item type in registry.
23
+ * `substrate` is the behavior-layer runtime (the score contract, compose
24
+ * slices, reactive hooks -- everything under ui/src outside components /
25
+ * primitives / composites). It is a copy-in shared dependency resolved like a
26
+ * primitive; the specific dir it installs into (`@/lib`, `@/hooks`, ...) is
27
+ * carried in the item's file path, so the type never grows per kind.
23
28
  */
24
29
  declare const RegistryItemTypeSchema: z.ZodEnum<{
25
30
  ui: "ui";
26
31
  primitive: "primitive";
27
32
  composite: "composite";
28
33
  rule: "rule";
34
+ substrate: "substrate";
29
35
  }>;
30
36
  type RegistryItemType = z.infer<typeof RegistryItemTypeSchema>;
31
37
  /**
@@ -59,6 +65,7 @@ declare const RegistryItemSchema: z.ZodObject<{
59
65
  primitive: "primitive";
60
66
  composite: "composite";
61
67
  rule: "rule";
68
+ substrate: "substrate";
62
69
  }>;
63
70
  description: z.ZodOptional<z.ZodString>;
64
71
  primitives: z.ZodArray<z.ZodString>;
@@ -93,6 +100,7 @@ declare const RegistryIndexSchema: z.ZodObject<{
93
100
  primitives: z.ZodArray<z.ZodString>;
94
101
  composites: z.ZodDefault<z.ZodArray<z.ZodString>>;
95
102
  rules: z.ZodDefault<z.ZodArray<z.ZodString>>;
103
+ substrate: z.ZodDefault<z.ZodArray<z.ZodString>>;
96
104
  }, z.core.$strip>;
97
105
  type RegistryIndex = z.infer<typeof RegistryIndexSchema>;
98
106
 
@@ -8,7 +8,7 @@ var RegistryFileSchema = z.object({
8
8
  devDependencies: z.array(z.string()).default([])
9
9
  // e.g., ["vitest"] - from @devDependencies JSDoc
10
10
  });
11
- var RegistryItemTypeSchema = z.enum(["ui", "primitive", "composite", "rule"]);
11
+ var RegistryItemTypeSchema = z.enum(["ui", "primitive", "composite", "rule", "substrate"]);
12
12
  var RegistryItemIntelligenceSchema = z.object({
13
13
  cognitiveLoad: z.number().optional(),
14
14
  attentionEconomics: z.string().optional(),
@@ -36,7 +36,8 @@ var RegistryIndexSchema = z.object({
36
36
  components: z.array(z.string()),
37
37
  primitives: z.array(z.string()),
38
38
  composites: z.array(z.string()).default([]),
39
- rules: z.array(z.string()).default([])
39
+ rules: z.array(z.string()).default([]),
40
+ substrate: z.array(z.string()).default([])
40
41
  });
41
42
  export {
42
43
  RegistryFileSchema,
package/package.json CHANGED
@@ -1,13 +1,22 @@
1
1
  {
2
2
  "name": "rafters",
3
- "version": "0.0.71",
3
+ "version": "0.0.73",
4
4
  "description": "Design Intelligence CLI. Scaffold tokens, import existing shadcn/Tailwind v4 sources, add components, and serve an MCP server so AI agents read decisions instead of guessing.",
5
5
  "homepage": "https://rafters.studio",
6
6
  "license": "MIT",
7
- "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/rafters-studio/rafters.git",
10
+ "directory": "packages/cli"
11
+ },
8
12
  "bin": {
9
13
  "rafters": "./dist/index.js"
10
14
  },
15
+ "files": [
16
+ "dist",
17
+ "LICENSE"
18
+ ],
19
+ "type": "module",
11
20
  "exports": {
12
21
  ".": "./dist/index.js",
13
22
  "./registry/types": {
@@ -15,10 +24,9 @@
15
24
  "import": "./dist/registry/types.js"
16
25
  }
17
26
  },
18
- "files": [
19
- "dist",
20
- "LICENSE"
21
- ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
22
30
  "scripts": {
23
31
  "build": "tsup",
24
32
  "dev": "tsup --watch",
@@ -57,13 +65,5 @@
57
65
  "typescript": "catalog:",
58
66
  "vitest": "catalog:",
59
67
  "zocker": "catalog:"
60
- },
61
- "repository": {
62
- "type": "git",
63
- "url": "git+https://github.com/rafters-studio/rafters.git",
64
- "directory": "packages/cli"
65
- },
66
- "publishConfig": {
67
- "access": "public"
68
68
  }
69
69
  }