inkbridge 0.1.0-beta.4 → 0.1.0-beta.6

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/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <img src="https://raw.githubusercontent.com/inkn9ne/inkbridge/main/public/inkbridge-logo.png" alt="Inkbridge" width="80" />
2
+
1
3
  # Inkbridge
2
4
 
3
5
  Generates native Figma frames from your Tailwind React components and syncs design tokens back to your codebase via GitHub PRs.
@@ -63,7 +65,7 @@ The plugin auto-discovers your server on ports `4000`, `3000`, and `5173`.
63
65
  1. Start dev server: `pnpm figma:dev`
64
66
  2. Open any Figma file
65
67
  3. **Plugins → Development → Inkbridge → Generate Design System Page**
66
- 4. The plugin scans your Storybook stories and builds a "Design System" page
68
+ 4. The plugin scans your Storybook stories and builds a "Design System" page with token tables, themed columns, grouped component sections, and responsive/state preview blocks where relevant
67
69
 
68
70
  Component data is always scanned live on every run — never stale. Re-run at any time to pick up changes.
69
71
 
package/code.js CHANGED
@@ -26,7 +26,7 @@
26
26
  repo: "",
27
27
  baseBranch: "main",
28
28
  tokenPath: "design-tokens/tokens.dtcg.json",
29
- tokenSourceMode: "auto",
29
+ tokenSourceMode: "css",
30
30
  cssTokenPath: "",
31
31
  syncDtcgOnPush: false,
32
32
  allowNewTokensFromFigma: false,
@@ -44,8 +44,8 @@
44
44
  return tokenPath;
45
45
  }
46
46
  function normalizeTokenSourceMode(mode) {
47
- if (mode === "css" || mode === "dtcg" || mode === "auto") return mode;
48
- return "auto";
47
+ if (mode === "dtcg") return "dtcg";
48
+ return "css";
49
49
  }
50
50
  function normalizeCssTokenPath(cssTokenPath) {
51
51
  if (typeof cssTokenPath !== "string") return "";
@@ -1210,11 +1210,36 @@
1210
1210
  function hasKeys(group) {
1211
1211
  return !!group && Object.keys(group).length > 0;
1212
1212
  }
1213
+ function enrichFontValue(value) {
1214
+ const parts = value.split(",");
1215
+ let hasLiteral = false;
1216
+ let insertAfterIdx = -1;
1217
+ for (let i = 0; i < parts.length; i++) {
1218
+ const trimmed = parts[i].trim();
1219
+ if (/^var\(/.test(trimmed)) {
1220
+ if (insertAfterIdx === -1) insertAfterIdx = i;
1221
+ continue;
1222
+ }
1223
+ const lower = trimmed.toLowerCase().replace(/^["']|["']$/g, "");
1224
+ if (SYSTEM_FONT_KEYWORDS.has(lower)) continue;
1225
+ if (trimmed.replace(/^["']|["']$/g, "").trim()) {
1226
+ hasLiteral = true;
1227
+ break;
1228
+ }
1229
+ }
1230
+ if (hasLiteral || insertAfterIdx === -1) return value;
1231
+ const varMatch = parts[insertAfterIdx].trim().match(/^var\(--font-([a-z0-9-]+)\)/i);
1232
+ if (!varMatch) return value;
1233
+ const literal = varMatch[1].split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1234
+ const result = [...parts];
1235
+ result.splice(insertAfterIdx + 1, 0, ` "${literal}"`);
1236
+ return result.join(",");
1237
+ }
1213
1238
  function buildTokenPatchFromScanned(map) {
1214
1239
  const patch = {};
1215
1240
  const primary = {};
1216
1241
  if (hasKeys(map.colors)) primary.color = __spreadValues({}, map.colors);
1217
- if (hasKeys(map.fonts)) primary.font = __spreadValues({}, map.fonts);
1242
+ if (hasKeys(map.fonts)) primary.font = Object.fromEntries(Object.entries(map.fonts).map(([k, v]) => [k, enrichFontValue(String(v))]));
1218
1243
  if (hasKeys(map.radius)) primary.radius = mapDimensionGroup(map.radius);
1219
1244
  if (hasKeys(map.spacing)) primary.spacing = mapDimensionGroup(map.spacing);
1220
1245
  if (hasKeys(map.fontSize)) primary.fontSize = mapDimensionGroup(map.fontSize);
@@ -1225,7 +1250,7 @@
1225
1250
  if (!scanned) continue;
1226
1251
  const themedPatch = {};
1227
1252
  if (hasKeys(scanned.colors)) themedPatch.color = __spreadValues({}, scanned.colors);
1228
- if (hasKeys(scanned.fonts)) themedPatch.font = __spreadValues({}, scanned.fonts);
1253
+ if (hasKeys(scanned.fonts)) themedPatch.font = Object.fromEntries(Object.entries(scanned.fonts).map(([k, v]) => [k, enrichFontValue(v)]));
1229
1254
  if (hasKeys(scanned.radius)) themedPatch.radius = mapDimensionGroup(scanned.radius);
1230
1255
  if (hasKeys(scanned.spacing)) themedPatch.spacing = mapDimensionGroup(scanned.spacing);
1231
1256
  if (hasKeys(scanned.fontSize)) themedPatch.fontSize = mapDimensionGroup(scanned.fontSize);
@@ -1301,6 +1326,54 @@
1301
1326
  );
1302
1327
  });
1303
1328
  }
1329
+ var SYSTEM_FONT_KEYWORDS = /* @__PURE__ */ new Set([
1330
+ // Generic families & system keywords
1331
+ "ui-sans-serif",
1332
+ "ui-serif",
1333
+ "ui-monospace",
1334
+ "system-ui",
1335
+ "sans-serif",
1336
+ "serif",
1337
+ "monospace",
1338
+ "-apple-system",
1339
+ "inherit",
1340
+ "initial",
1341
+ // Common web-safe fonts that appear as fallbacks, not as the intended design font
1342
+ "arial",
1343
+ "helvetica",
1344
+ "georgia",
1345
+ "verdana",
1346
+ "tahoma",
1347
+ "trebuchet ms",
1348
+ "times new roman",
1349
+ "courier new",
1350
+ "courier",
1351
+ "palatino",
1352
+ "garamond",
1353
+ "bookman",
1354
+ "comic sans ms",
1355
+ "impact",
1356
+ "lucida sans unicode"
1357
+ ]);
1358
+ function extractFontName(raw) {
1359
+ const parts = String(raw || "").split(",");
1360
+ let varFallback = null;
1361
+ for (const part of parts) {
1362
+ const trimmed = part.trim();
1363
+ const varMatch = trimmed.match(/^var\(--font-([a-z0-9-]+)\)/i);
1364
+ if (varMatch) {
1365
+ if (!varFallback) {
1366
+ varFallback = varMatch[1].split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1367
+ }
1368
+ continue;
1369
+ }
1370
+ const lower = trimmed.toLowerCase().replace(/^["']|["']$/g, "");
1371
+ if (SYSTEM_FONT_KEYWORDS.has(lower)) continue;
1372
+ const name = trimmed.replace(/^["']|["']$/g, "");
1373
+ if (name) return name;
1374
+ }
1375
+ return varFallback;
1376
+ }
1304
1377
  function getThemeFontFamily(tokens, theme, role = "sans") {
1305
1378
  const block = tokens[theme];
1306
1379
  if (block && "font" in block) {
@@ -1308,8 +1381,8 @@
1308
1381
  if (font) {
1309
1382
  const raw = font[role] || (role !== "sans" ? font.sans : null) || Object.values(font)[0];
1310
1383
  if (raw) {
1311
- const first = String(raw).split(",")[0].trim().replace(/^["']|["']$/g, "");
1312
- if (first) return first;
1384
+ const name = extractFontName(raw);
1385
+ if (name) return name;
1313
1386
  }
1314
1387
  }
1315
1388
  }
@@ -1623,7 +1696,7 @@
1623
1696
  function normalizeTokenMap(raw) {
1624
1697
  if (!raw || !isPlainObject3(raw)) return void 0;
1625
1698
  const mode = raw.mode === "css" || raw.mode === "dtcg" || raw.mode === "embedded" ? raw.mode : "embedded";
1626
- const requestedMode = raw.requestedMode === "css" || raw.requestedMode === "dtcg" || raw.requestedMode === "auto" ? raw.requestedMode : void 0;
1699
+ const requestedMode = raw.requestedMode === "css" || raw.requestedMode === "dtcg" ? raw.requestedMode : void 0;
1627
1700
  const out = createEmptyScannedTokenMap(
1628
1701
  mode,
1629
1702
  typeof raw.source === "string" && raw.source ? raw.source : "embedded:tokens.ts",
@@ -1730,7 +1803,7 @@
1730
1803
  const baseUrl = "http://localhost:" + port + "/" + path;
1731
1804
  const query = [];
1732
1805
  const requestedMode = config == null ? void 0 : config.tokenSourceMode;
1733
- if (requestedMode === "auto" || requestedMode === "css" || requestedMode === "dtcg") {
1806
+ if (requestedMode === "css" || requestedMode === "dtcg") {
1734
1807
  query.push("tokenSourceMode=" + encodeURIComponent(requestedMode));
1735
1808
  }
1736
1809
  const cssTokenPath = ((config == null ? void 0 : config.cssTokenPath) || "").trim();
@@ -4329,7 +4402,7 @@
4329
4402
  const modeRaw = raw && typeof raw === "object" ? raw.mode : null;
4330
4403
  const mode = modeRaw === "css" || modeRaw === "dtcg" || modeRaw === "embedded" ? modeRaw : "embedded";
4331
4404
  const requestedRaw = raw && typeof raw === "object" ? raw.requestedMode : null;
4332
- const requestedMode = requestedRaw === "auto" || requestedRaw === "css" || requestedRaw === "dtcg" ? requestedRaw : void 0;
4405
+ const requestedMode = requestedRaw === "css" || requestedRaw === "dtcg" ? requestedRaw : void 0;
4333
4406
  return { source, mode, requestedMode };
4334
4407
  }
4335
4408
  async function loadTokenSourceInfo() {
@@ -4750,8 +4823,8 @@
4750
4823
  return true;
4751
4824
  }
4752
4825
  function resolveConfiguredMode(mode) {
4753
- if (mode === "css" || mode === "dtcg" || mode === "auto") return mode;
4754
- return "auto";
4826
+ if (mode === "dtcg") return "dtcg";
4827
+ return "css";
4755
4828
  }
4756
4829
  async function buildTokenCommitPlan(token) {
4757
4830
  const tokensBeforeVariablePatch = JSON.parse(JSON.stringify(TOKENS));
@@ -4765,7 +4838,7 @@
4765
4838
  const configuredMode = resolveConfiguredMode(GITHUB_CONFIG.tokenSourceMode);
4766
4839
  const dtcgPath = normalizePath(GITHUB_CONFIG.tokenPath, DEFAULT_DTCG_PATH);
4767
4840
  const dtcgExisting = await fetchFileContent(token, dtcgPath, GITHUB_CONFIG.baseBranch);
4768
- const preferDtcg = configuredMode === "dtcg" || configuredMode === "auto" && sourceInfo.mode === "dtcg";
4841
+ const preferDtcg = configuredMode === "dtcg" || configuredMode === "css" && sourceInfo.mode === "dtcg";
4769
4842
  if (preferDtcg) {
4770
4843
  const dtcg = tokensToDTCG(workingTokens);
4771
4844
  const dtcgContent = JSON.stringify(dtcg, null, 2) + "\n";
@@ -7212,6 +7285,62 @@
7212
7285
 
7213
7286
  // src/story-builder.ts
7214
7287
  var THEME_CONTEXT_CACHE = {};
7288
+ var BOARD_LAYOUT = {
7289
+ boardGap: 96,
7290
+ sectionPaddingX: 64,
7291
+ sectionPaddingY: 64,
7292
+ columnsGap: 320,
7293
+ columnGap: 144,
7294
+ columnPaddingX: 0,
7295
+ columnPaddingY: 160,
7296
+ sectionGroupGap: 176,
7297
+ sectionTitleGap: 48,
7298
+ componentBlockGap: 128,
7299
+ componentTitleGap: 48,
7300
+ storyGap: 32,
7301
+ showcaseGap: 128,
7302
+ stateMatrixGap: 40,
7303
+ stateMatrixAxisGap: 64,
7304
+ responsiveBlockGap: 32,
7305
+ responsiveColumnGap: 64,
7306
+ responsiveLabelGap: 20
7307
+ };
7308
+ var COMPONENT_SECTION_ORDER = [
7309
+ {
7310
+ key: "actions-forms",
7311
+ title: "Actions & Forms",
7312
+ description: "Buttons, fields, feedback, and overlay primitives."
7313
+ },
7314
+ {
7315
+ key: "content-data",
7316
+ title: "Content & Data",
7317
+ description: "Cards, tables, media, and information-heavy components."
7318
+ },
7319
+ {
7320
+ key: "navigation-shell",
7321
+ title: "Navigation & Shell",
7322
+ description: "Headers, footers, and navigation patterns."
7323
+ },
7324
+ {
7325
+ key: "marketing-sections",
7326
+ title: "Marketing Sections",
7327
+ description: "Landing-page compositions and promotional blocks."
7328
+ },
7329
+ {
7330
+ key: "pricing",
7331
+ title: "Pricing",
7332
+ description: "Pricing components and plan comparisons."
7333
+ },
7334
+ {
7335
+ key: "other",
7336
+ title: "Additional Components",
7337
+ description: "Everything else that does not fit the main groups."
7338
+ }
7339
+ ];
7340
+ var COMPONENT_SECTION_BY_KEY = COMPONENT_SECTION_ORDER.reduce(function(map, section) {
7341
+ map[section.key] = section;
7342
+ return map;
7343
+ }, {});
7215
7344
  function getThemeContext(theme) {
7216
7345
  if (THEME_CONTEXT_CACHE[theme]) return THEME_CONTEXT_CACHE[theme];
7217
7346
  const colorGroup = getThemeColors(TOKENS, theme);
@@ -7224,6 +7353,100 @@
7224
7353
  THEME_CONTEXT_CACHE[theme] = ctx;
7225
7354
  return ctx;
7226
7355
  }
7356
+ function isTextNode(node) {
7357
+ return !!node && node.type === "TEXT";
7358
+ }
7359
+ function removeDirectTextChildren(parent, names) {
7360
+ if (!parent || !Array.isArray(parent.children)) return;
7361
+ const stale = parent.children.filter(function(child) {
7362
+ if (!isTextNode(child)) return false;
7363
+ if (!names || names.length === 0) return true;
7364
+ return names.indexOf(child.name) !== -1 || names.indexOf(child.characters) !== -1;
7365
+ });
7366
+ for (let i = 0; i < stale.length; i++) {
7367
+ stale[i].remove();
7368
+ }
7369
+ }
7370
+ function ensureHeaderBlock(parent, frameName, title, description, opts) {
7371
+ let frame = findChildByName(parent, frameName);
7372
+ if (!frame) {
7373
+ frame = figma.createFrame();
7374
+ frame.name = frameName;
7375
+ }
7376
+ frame.layoutMode = "VERTICAL";
7377
+ frame.primaryAxisSizingMode = "AUTO";
7378
+ frame.counterAxisSizingMode = "AUTO";
7379
+ frame.counterAxisAlignItems = "MIN";
7380
+ frame.itemSpacing = description ? 10 : 0;
7381
+ frame.fills = [];
7382
+ const titleNode = createTextNode(title, {
7383
+ fontSize: opts && opts.titleSize ? opts.titleSize : 20,
7384
+ lineHeight: opts && opts.titleLineHeight ? opts.titleLineHeight : void 0,
7385
+ bold: true
7386
+ });
7387
+ titleNode.name = "Title";
7388
+ frame.children.slice().forEach(function(child) {
7389
+ child.remove();
7390
+ });
7391
+ frame.appendChild(titleNode);
7392
+ if (description) {
7393
+ const descriptionNode = createTextNode(description, {
7394
+ fontSize: opts && opts.descriptionSize ? opts.descriptionSize : 14,
7395
+ lineHeight: 20,
7396
+ opacity: 0.62
7397
+ });
7398
+ descriptionNode.name = "Description";
7399
+ frame.appendChild(descriptionNode);
7400
+ }
7401
+ return frame;
7402
+ }
7403
+ function getComponentSectionName(section) {
7404
+ return "Section / " + section.title;
7405
+ }
7406
+ function inferComponentSection(def) {
7407
+ const name = String(def && def.name ? def.name : "").toLowerCase();
7408
+ const filePath = String(def && def.filePath ? def.filePath : "").toLowerCase();
7409
+ if (filePath.includes("/feature/marketing/") || ["comparison-table", "compatibility-bar", "cta", "faq-section", "features", "hero-section", "how-it-works"].includes(name)) {
7410
+ return COMPONENT_SECTION_BY_KEY["marketing-sections"];
7411
+ }
7412
+ if (filePath.includes("/feature/pricing/") || name === "plan-card") {
7413
+ return COMPONENT_SECTION_BY_KEY.pricing;
7414
+ }
7415
+ if (["navbar", "footer", "hero", "mobile-nav-closed", "mobile-nav-open"].includes(name)) {
7416
+ return COMPONENT_SECTION_BY_KEY["navigation-shell"];
7417
+ }
7418
+ if (["button", "badge", "alert", "input", "select", "dialog", "dropdown-menu", "sheet"].includes(name) || filePath.includes("/components/ui/") && ["button", "badge", "alert", "input", "select", "dialog", "dropdown-menu", "sheet"].some(function(token) {
7419
+ return name === token;
7420
+ })) {
7421
+ return COMPONENT_SECTION_BY_KEY["actions-forms"];
7422
+ }
7423
+ if (["card", "table", "separator", "media-card", "skeleton", "text-truncation"].includes(name) || filePath.includes("/components/")) {
7424
+ return COMPONENT_SECTION_BY_KEY["content-data"];
7425
+ }
7426
+ return COMPONENT_SECTION_BY_KEY.other;
7427
+ }
7428
+ function groupComponentDefs(defs) {
7429
+ const grouped = {};
7430
+ for (let i = 0; i < defs.length; i++) {
7431
+ const def = defs[i];
7432
+ const section = inferComponentSection(def);
7433
+ if (!grouped[section.key]) grouped[section.key] = [];
7434
+ grouped[section.key].push(def);
7435
+ }
7436
+ const out = [];
7437
+ for (let i = 0; i < COMPONENT_SECTION_ORDER.length; i++) {
7438
+ const section = COMPONENT_SECTION_ORDER[i];
7439
+ const entries = grouped[section.key];
7440
+ if (!entries || entries.length === 0) continue;
7441
+ out.push({
7442
+ section,
7443
+ defs: entries.sort(function(a, b) {
7444
+ return a.name.localeCompare(b.name);
7445
+ })
7446
+ });
7447
+ }
7448
+ return out;
7449
+ }
7227
7450
  function applyStyleToFrame(frame, style, theme) {
7228
7451
  if (!style) return;
7229
7452
  if (style.bg) {
@@ -7641,7 +7864,7 @@
7641
7864
  const block = figma.createFrame();
7642
7865
  block.name = "States";
7643
7866
  block.layoutMode = "VERTICAL";
7644
- block.itemSpacing = 16;
7867
+ block.itemSpacing = BOARD_LAYOUT.stateMatrixGap;
7645
7868
  block.primaryAxisSizingMode = "AUTO";
7646
7869
  block.counterAxisSizingMode = "AUTO";
7647
7870
  block.fills = [];
@@ -7736,7 +7959,7 @@
7736
7959
  const axesRow = figma.createFrame();
7737
7960
  axesRow.name = "State Axes";
7738
7961
  axesRow.layoutMode = "HORIZONTAL";
7739
- axesRow.itemSpacing = 28;
7962
+ axesRow.itemSpacing = BOARD_LAYOUT.stateMatrixAxisGap;
7740
7963
  axesRow.primaryAxisSizingMode = "AUTO";
7741
7964
  axesRow.counterAxisSizingMode = "AUTO";
7742
7965
  axesRow.fills = [];
@@ -7747,7 +7970,7 @@
7747
7970
  const table = figma.createFrame();
7748
7971
  table.name = "State Table";
7749
7972
  table.layoutMode = "VERTICAL";
7750
- table.itemSpacing = 12;
7973
+ table.itemSpacing = BOARD_LAYOUT.stateMatrixGap;
7751
7974
  table.primaryAxisSizingMode = "AUTO";
7752
7975
  table.counterAxisSizingMode = "AUTO";
7753
7976
  table.fills = [];
@@ -7755,7 +7978,7 @@
7755
7978
  const tableHeader = figma.createFrame();
7756
7979
  tableHeader.name = "State Table Header";
7757
7980
  tableHeader.layoutMode = "HORIZONTAL";
7758
- tableHeader.itemSpacing = 28;
7981
+ tableHeader.itemSpacing = BOARD_LAYOUT.stateMatrixAxisGap;
7759
7982
  tableHeader.primaryAxisSizingMode = "AUTO";
7760
7983
  tableHeader.counterAxisSizingMode = "AUTO";
7761
7984
  tableHeader.fills = [];
@@ -7779,7 +8002,7 @@
7779
8002
  const row = figma.createFrame();
7780
8003
  row.name = "State Row/" + stateNames[si];
7781
8004
  row.layoutMode = "HORIZONTAL";
7782
- row.itemSpacing = 28;
8005
+ row.itemSpacing = BOARD_LAYOUT.stateMatrixAxisGap;
7783
8006
  row.primaryAxisSizingMode = "AUTO";
7784
8007
  row.counterAxisSizingMode = "AUTO";
7785
8008
  row.counterAxisAlignItems = "CENTER";
@@ -7866,7 +8089,7 @@
7866
8089
  const block = figma.createFrame();
7867
8090
  block.name = "Responsive";
7868
8091
  block.layoutMode = "VERTICAL";
7869
- block.itemSpacing = 8;
8092
+ block.itemSpacing = BOARD_LAYOUT.responsiveBlockGap;
7870
8093
  block.primaryAxisSizingMode = "AUTO";
7871
8094
  block.counterAxisSizingMode = "AUTO";
7872
8095
  block.fills = [];
@@ -7875,7 +8098,7 @@
7875
8098
  const row = figma.createFrame();
7876
8099
  row.name = "Responsive Strip";
7877
8100
  row.layoutMode = "HORIZONTAL";
7878
- row.itemSpacing = 16;
8101
+ row.itemSpacing = BOARD_LAYOUT.responsiveColumnGap;
7879
8102
  row.primaryAxisSizingMode = "AUTO";
7880
8103
  row.counterAxisSizingMode = "AUTO";
7881
8104
  row.fills = [];
@@ -7887,7 +8110,7 @@
7887
8110
  const col = figma.createFrame();
7888
8111
  col.name = getBreakpointLabel(bp.name, bp.minWidth);
7889
8112
  col.layoutMode = "VERTICAL";
7890
- col.itemSpacing = 6;
8113
+ col.itemSpacing = BOARD_LAYOUT.responsiveLabelGap;
7891
8114
  col.primaryAxisSizingMode = "AUTO";
7892
8115
  col.counterAxisSizingMode = "AUTO";
7893
8116
  col.fills = [];
@@ -8286,24 +8509,40 @@
8286
8509
  debug("createUIComponents start", { opts: options });
8287
8510
  const sectionTitle = options.sectionTitle || "UI Components";
8288
8511
  const tokenHash = typeof options.tokenHash === "string" ? options.tokenHash : "";
8512
+ const showSectionHeader = options.showSectionHeader !== false;
8513
+ const sectionPaddingX = typeof options.sectionPaddingX === "number" ? options.sectionPaddingX : showSectionHeader ? BOARD_LAYOUT.sectionPaddingX : 0;
8514
+ const sectionPaddingY = typeof options.sectionPaddingY === "number" ? options.sectionPaddingY : showSectionHeader ? BOARD_LAYOUT.sectionPaddingY : 0;
8289
8515
  let section = findChildByName(parent, sectionTitle);
8290
8516
  if (!section) {
8291
8517
  section = figma.createFrame();
8292
8518
  section.name = sectionTitle;
8293
- section.layoutMode = "VERTICAL";
8294
- section.itemSpacing = 32;
8295
- section.primaryAxisSizingMode = "AUTO";
8296
- section.counterAxisSizingMode = "AUTO";
8297
- section.paddingLeft = section.paddingRight = 24;
8298
- section.paddingTop = section.paddingBottom = 24;
8299
- section.fills = [];
8300
- ctx.applyClipBehavior(section, []);
8301
8519
  const offsetY = typeof options.yOffset === "number" ? options.yOffset : 0;
8302
8520
  const offsetX = typeof options.xOffset === "number" ? options.xOffset : 0;
8303
8521
  section.x = offsetX;
8304
8522
  section.y = offsetY;
8305
8523
  parent.appendChild(section);
8306
- section.appendChild(createTextNode(sectionTitle, { fontSize: 28, bold: true }));
8524
+ }
8525
+ section.layoutMode = "VERTICAL";
8526
+ section.itemSpacing = showSectionHeader ? BOARD_LAYOUT.boardGap : 0;
8527
+ section.primaryAxisSizingMode = "AUTO";
8528
+ section.counterAxisSizingMode = "AUTO";
8529
+ section.paddingLeft = section.paddingRight = sectionPaddingX;
8530
+ section.paddingTop = section.paddingBottom = sectionPaddingY;
8531
+ section.counterAxisAlignItems = "MIN";
8532
+ section.fills = [];
8533
+ ctx.applyClipBehavior(section, []);
8534
+ removeDirectTextChildren(section);
8535
+ const existingSectionHeader = findChildByName(section, "Section Header");
8536
+ if (showSectionHeader) {
8537
+ const sectionHeader = ensureHeaderBlock(section, "Section Header", sectionTitle, null, {
8538
+ titleSize: 32,
8539
+ titleLineHeight: 38
8540
+ });
8541
+ if (findChildIndexByName(section, "Section Header") !== 0) {
8542
+ section.insertChild(0, sectionHeader);
8543
+ }
8544
+ } else if (existingSectionHeader) {
8545
+ existingSectionHeader.remove();
8307
8546
  }
8308
8547
  function formatThemeLabel(theme) {
8309
8548
  if (!theme) return "Theme";
@@ -8316,25 +8555,26 @@
8316
8555
  const block = figma.createFrame();
8317
8556
  block.name = def.name;
8318
8557
  block.layoutMode = "VERTICAL";
8319
- block.itemSpacing = 12;
8558
+ block.itemSpacing = BOARD_LAYOUT.componentTitleGap;
8320
8559
  block.primaryAxisSizingMode = "AUTO";
8321
8560
  block.counterAxisSizingMode = "AUTO";
8561
+ block.counterAxisAlignItems = "MIN";
8322
8562
  block.fills = [];
8323
8563
  ctx.applyClipBehavior(block, []);
8324
- block.appendChild(createTextNode(def.name, { fontSize: 16, bold: true }));
8564
+ block.appendChild(createTextNode(def.name, { fontSize: 16, lineHeight: 22, bold: true }));
8325
8565
  const storyList = def.stories || [];
8326
8566
  for (let storyIndex = 0; storyIndex < storyList.length; storyIndex++) {
8327
8567
  const story = storyList[storyIndex];
8328
8568
  const storyWrap = figma.createFrame();
8329
8569
  storyWrap.name = story.name;
8330
8570
  storyWrap.layoutMode = "VERTICAL";
8331
- storyWrap.itemSpacing = 8;
8571
+ storyWrap.itemSpacing = BOARD_LAYOUT.storyGap;
8332
8572
  storyWrap.primaryAxisSizingMode = "AUTO";
8333
8573
  storyWrap.counterAxisSizingMode = "AUTO";
8334
8574
  storyWrap.counterAxisAlignItems = "MIN";
8335
8575
  storyWrap.fills = [];
8336
8576
  ctx.applyClipBehavior(storyWrap, []);
8337
- storyWrap.appendChild(createTextNode(story.name, { fontSize: 12, opacity: 0.6, textAlignHorizontal: "LEFT" }));
8577
+ storyWrap.appendChild(createTextNode(story.name, { fontSize: 14, lineHeight: 20, opacity: 0.6, textAlignHorizontal: "LEFT" }));
8338
8578
  const layout = figma.createFrame();
8339
8579
  layout.name = "Story Layout";
8340
8580
  layout.primaryAxisSizingMode = "AUTO";
@@ -8397,7 +8637,7 @@
8397
8637
  layout.counterAxisSizingMode = "AUTO";
8398
8638
  }
8399
8639
  let added = 0;
8400
- if ((def.type === "compound" || def.type === "simple") && story.jsxTree) {
8640
+ if ((def.type === "compound" || def.type === "simple" || def.type === "cva") && story.jsxTree) {
8401
8641
  let rendered = false;
8402
8642
  const allowAbsolute = ctx.hasExplicitHeight(layoutClasses);
8403
8643
  const rootNode = story.jsxTree;
@@ -8537,13 +8777,26 @@
8537
8777
  if (!colFrame) {
8538
8778
  colFrame = figma.createFrame();
8539
8779
  colFrame.name = label + " Column";
8540
- colFrame.layoutMode = "VERTICAL";
8541
- colFrame.itemSpacing = 24;
8542
- colFrame.primaryAxisSizingMode = "AUTO";
8543
- colFrame.counterAxisSizingMode = "AUTO";
8544
- colFrame.fills = [];
8545
- ctx.applyClipBehavior(colFrame, []);
8546
- colFrame.appendChild(createTextNode(label + " Theme", { fontSize: 18, bold: true }));
8780
+ }
8781
+ colFrame.layoutMode = "VERTICAL";
8782
+ colFrame.itemSpacing = BOARD_LAYOUT.columnGap;
8783
+ colFrame.primaryAxisSizingMode = "AUTO";
8784
+ colFrame.counterAxisSizingMode = "AUTO";
8785
+ colFrame.counterAxisAlignItems = "MIN";
8786
+ colFrame.paddingLeft = colFrame.paddingRight = BOARD_LAYOUT.columnPaddingX;
8787
+ colFrame.paddingTop = colFrame.paddingBottom = BOARD_LAYOUT.columnPaddingY;
8788
+ colFrame.fills = [];
8789
+ ctx.applyClipBehavior(colFrame, []);
8790
+ removeDirectTextChildren(colFrame);
8791
+ const themeHeader = ensureHeaderBlock(
8792
+ colFrame,
8793
+ "Theme Header",
8794
+ label + " Theme",
8795
+ "Generated component board with grouped stories and larger comparison spacing.",
8796
+ { titleSize: 32, titleLineHeight: 38, descriptionSize: 14 }
8797
+ );
8798
+ if (findChildIndexByName(colFrame, "Theme Header") !== 0) {
8799
+ colFrame.insertChild(0, themeHeader);
8547
8800
  }
8548
8801
  const pack = getActivePack();
8549
8802
  const packStories = pack && pack.stories ? pack.stories : [];
@@ -8554,52 +8807,63 @@
8554
8807
  return tags.indexOf(storyTagFilter) !== -1;
8555
8808
  });
8556
8809
  const packStoriesHash = hashString(JSON.stringify(filteredStories) + theme);
8557
- let packStoriesFrame = findChildByName(colFrame, "Pack Stories");
8810
+ let packStoriesFrame = findChildByName(colFrame, "Showcase Stories");
8558
8811
  if (!packStoriesFrame || getFrameHash(packStoriesFrame) !== packStoriesHash) {
8559
8812
  if (packStoriesFrame) packStoriesFrame.remove();
8813
+ const stalePackFrame = findChildByName(colFrame, "Pack Stories");
8814
+ if (stalePackFrame) stalePackFrame.remove();
8560
8815
  if (filteredStories.length > 0) {
8561
8816
  packStoriesFrame = figma.createFrame();
8562
- packStoriesFrame.name = "Pack Stories";
8817
+ packStoriesFrame.name = "Showcase Stories";
8563
8818
  packStoriesFrame.layoutMode = "VERTICAL";
8564
- packStoriesFrame.itemSpacing = 12;
8819
+ packStoriesFrame.itemSpacing = BOARD_LAYOUT.showcaseGap;
8565
8820
  packStoriesFrame.primaryAxisSizingMode = "AUTO";
8566
8821
  packStoriesFrame.counterAxisSizingMode = "AUTO";
8822
+ packStoriesFrame.counterAxisAlignItems = "MIN";
8567
8823
  packStoriesFrame.fills = [];
8568
8824
  ctx.applyClipBehavior(packStoriesFrame, []);
8825
+ packStoriesFrame.appendChild(ensureHeaderBlock(
8826
+ packStoriesFrame,
8827
+ "Section Header",
8828
+ "Showcase Stories",
8829
+ "Larger composite stories rendered before the component catalog.",
8830
+ { titleSize: 20, titleLineHeight: 26, descriptionSize: 14 }
8831
+ ));
8569
8832
  for (let si = 0; si < filteredStories.length; si++) {
8570
8833
  const story = filteredStories[si];
8571
8834
  const storyBlock = figma.createFrame();
8572
8835
  storyBlock.name = story.name || "Story";
8573
8836
  storyBlock.layoutMode = "VERTICAL";
8574
- storyBlock.itemSpacing = 12;
8837
+ storyBlock.itemSpacing = BOARD_LAYOUT.componentTitleGap;
8575
8838
  storyBlock.primaryAxisSizingMode = "AUTO";
8576
8839
  storyBlock.counterAxisSizingMode = "AUTO";
8840
+ storyBlock.counterAxisAlignItems = "MIN";
8577
8841
  storyBlock.fills = [];
8578
8842
  ctx.applyClipBehavior(storyBlock, []);
8579
- storyBlock.appendChild(createTextNode(story.name || "Story", { fontSize: 16, bold: true }));
8843
+ storyBlock.appendChild(createTextNode(story.name || "Story", { fontSize: 16, lineHeight: 22, bold: true }));
8580
8844
  storyBlock.appendChild(renderStandaloneStory(story, theme, ctx));
8581
8845
  packStoriesFrame.appendChild(storyBlock);
8582
8846
  }
8583
- const headerExists = colFrame.children && colFrame.children.length > 0;
8584
- if (headerExists) {
8585
- colFrame.insertChild(1, packStoriesFrame);
8586
- } else {
8587
- colFrame.appendChild(packStoriesFrame);
8588
- }
8847
+ colFrame.appendChild(packStoriesFrame);
8589
8848
  setFrameHash(packStoriesFrame, packStoriesHash);
8590
8849
  }
8591
8850
  }
8851
+ packStoriesFrame = findChildByName(colFrame, "Showcase Stories");
8852
+ if (packStoriesFrame && findChildIndexByName(colFrame, "Showcase Stories") !== 1) {
8853
+ colFrame.insertChild(1, packStoriesFrame);
8854
+ }
8592
8855
  let componentList = findChildByName(colFrame, "Component Blocks");
8593
8856
  if (!componentList) {
8594
8857
  componentList = figma.createFrame();
8595
8858
  componentList.name = "Component Blocks";
8596
8859
  componentList.layoutMode = "VERTICAL";
8597
- componentList.primaryAxisSizingMode = "AUTO";
8598
- componentList.counterAxisSizingMode = "AUTO";
8599
- componentList.itemSpacing = colFrame.itemSpacing * 6;
8600
- componentList.fills = [];
8601
- ctx.applyClipBehavior(componentList, []);
8602
8860
  }
8861
+ componentList.primaryAxisSizingMode = "AUTO";
8862
+ componentList.counterAxisSizingMode = "AUTO";
8863
+ componentList.counterAxisAlignItems = "MIN";
8864
+ componentList.itemSpacing = BOARD_LAYOUT.sectionGroupGap;
8865
+ componentList.fills = [];
8866
+ ctx.applyClipBehavior(componentList, []);
8603
8867
  const defsRaw = COMPONENT_DEFS && COMPONENT_DEFS.components ? COMPONENT_DEFS.components : [];
8604
8868
  const onlyComponents = options.onlyComponents;
8605
8869
  const excludeComponents = options.excludeComponents;
@@ -8611,57 +8875,122 @@
8611
8875
  }).sort(function(a, b) {
8612
8876
  return a.name.localeCompare(b.name);
8613
8877
  });
8614
- const expectedNames = {};
8615
- for (let di = 0; di < defs.length; di++) {
8616
- expectedNames[defs[di].name] = true;
8617
- }
8618
- const existingBlocks = componentList.children ? Array.from(componentList.children) : [];
8619
- for (let ei = 0; ei < existingBlocks.length; ei++) {
8620
- const existingBlock = existingBlocks[ei];
8621
- if (!expectedNames[existingBlock.name]) {
8622
- existingBlock.remove();
8623
- debug("Removed stale component block", { name: existingBlock.name });
8624
- }
8625
- }
8626
- for (let di = 0; di < defs.length; di++) {
8627
- const def = defs[di];
8628
- const blockHash = hashDef(def) + ":" + tokenHash;
8629
- const existingBlock = findChildByName(componentList, def.name);
8630
- if (existingBlock && getFrameHash(existingBlock) === blockHash) {
8631
- debug("Component block unchanged \u2014 skipped", { name: def.name });
8632
- continue;
8878
+ const groupedDefs = groupComponentDefs(defs);
8879
+ const expectedSectionNames = {};
8880
+ for (let gi = 0; gi < groupedDefs.length; gi++) {
8881
+ expectedSectionNames[getComponentSectionName(groupedDefs[gi].section)] = true;
8882
+ }
8883
+ const existingGroups = componentList.children ? Array.from(componentList.children) : [];
8884
+ for (let gi = 0; gi < existingGroups.length; gi++) {
8885
+ const existingGroup = existingGroups[gi];
8886
+ if (!expectedSectionNames[existingGroup.name]) {
8887
+ existingGroup.remove();
8888
+ }
8889
+ }
8890
+ for (let gi = 0; gi < groupedDefs.length; gi++) {
8891
+ const group = groupedDefs[gi];
8892
+ const sectionName = getComponentSectionName(group.section);
8893
+ let sectionFrame = findChildByName(componentList, sectionName);
8894
+ if (!sectionFrame) {
8895
+ sectionFrame = figma.createFrame();
8896
+ sectionFrame.name = sectionName;
8897
+ }
8898
+ sectionFrame.layoutMode = "VERTICAL";
8899
+ sectionFrame.primaryAxisSizingMode = "AUTO";
8900
+ sectionFrame.counterAxisSizingMode = "AUTO";
8901
+ sectionFrame.counterAxisAlignItems = "MIN";
8902
+ sectionFrame.itemSpacing = BOARD_LAYOUT.sectionTitleGap;
8903
+ sectionFrame.fills = [];
8904
+ ctx.applyClipBehavior(sectionFrame, []);
8905
+ const groupHeader = ensureHeaderBlock(
8906
+ sectionFrame,
8907
+ "Section Header",
8908
+ group.section.title,
8909
+ group.section.description,
8910
+ { titleSize: 20, titleLineHeight: 26, descriptionSize: 14 }
8911
+ );
8912
+ if (findChildIndexByName(sectionFrame, "Section Header") !== 0) {
8913
+ sectionFrame.insertChild(0, groupHeader);
8914
+ }
8915
+ let groupBlocks = findChildByName(sectionFrame, "Blocks");
8916
+ if (!groupBlocks) {
8917
+ groupBlocks = figma.createFrame();
8918
+ groupBlocks.name = "Blocks";
8919
+ sectionFrame.appendChild(groupBlocks);
8920
+ }
8921
+ groupBlocks.layoutMode = "VERTICAL";
8922
+ groupBlocks.primaryAxisSizingMode = "AUTO";
8923
+ groupBlocks.counterAxisSizingMode = "AUTO";
8924
+ groupBlocks.counterAxisAlignItems = "MIN";
8925
+ groupBlocks.itemSpacing = BOARD_LAYOUT.componentBlockGap;
8926
+ groupBlocks.fills = [];
8927
+ ctx.applyClipBehavior(groupBlocks, []);
8928
+ const expectedNames = {};
8929
+ for (let di = 0; di < group.defs.length; di++) {
8930
+ expectedNames[group.defs[di].name] = true;
8931
+ }
8932
+ const existingBlocks = groupBlocks.children ? Array.from(groupBlocks.children) : [];
8933
+ for (let ei = 0; ei < existingBlocks.length; ei++) {
8934
+ const existingBlock = existingBlocks[ei];
8935
+ if (!expectedNames[existingBlock.name]) {
8936
+ existingBlock.remove();
8937
+ debug("Removed stale component block", { name: existingBlock.name });
8938
+ }
8633
8939
  }
8634
- let insertIndex = -1;
8635
- if (existingBlock) {
8636
- insertIndex = findChildIndexByName(componentList, def.name);
8637
- existingBlock.remove();
8638
- debug("Component block changed \u2014 rebuilding", { name: def.name });
8940
+ for (let di = 0; di < group.defs.length; di++) {
8941
+ const def = group.defs[di];
8942
+ const blockHash = hashDef(def) + ":" + tokenHash;
8943
+ const existingBlock = findChildByName(groupBlocks, def.name);
8944
+ if (existingBlock && getFrameHash(existingBlock) === blockHash) {
8945
+ const currentIndex = findChildIndexByName(groupBlocks, def.name);
8946
+ if (currentIndex !== di) {
8947
+ groupBlocks.insertChild(di, existingBlock);
8948
+ }
8949
+ debug("Component block unchanged \u2014 skipped", { name: def.name });
8950
+ continue;
8951
+ }
8952
+ let insertIndex = di;
8953
+ if (existingBlock) {
8954
+ existingBlock.remove();
8955
+ debug("Component block changed \u2014 rebuilding", { name: def.name });
8956
+ }
8957
+ const newBlock = buildComponentBlock(def, theme, colFrame);
8958
+ setFrameHash(newBlock, blockHash);
8959
+ if (insertIndex >= 0 && insertIndex < groupBlocks.children.length) {
8960
+ groupBlocks.insertChild(insertIndex, newBlock);
8961
+ } else {
8962
+ groupBlocks.appendChild(newBlock);
8963
+ }
8639
8964
  }
8640
- const newBlock = buildComponentBlock(def, theme, colFrame);
8641
- setFrameHash(newBlock, blockHash);
8642
- if (insertIndex >= 0 && insertIndex < componentList.children.length) {
8643
- componentList.insertChild(insertIndex, newBlock);
8644
- } else {
8645
- componentList.appendChild(newBlock);
8965
+ const currentSectionIndex = findChildIndexByName(componentList, sectionName);
8966
+ if (currentSectionIndex !== gi) {
8967
+ componentList.insertChild(gi, sectionFrame);
8646
8968
  }
8647
8969
  }
8648
8970
  if (componentList.children.length > 0 && !findChildByName(colFrame, "Component Blocks")) {
8649
8971
  colFrame.appendChild(componentList);
8650
8972
  }
8973
+ if (findChildByName(colFrame, "Component Blocks")) {
8974
+ const componentListIndex = packStoriesFrame ? 2 : 1;
8975
+ if (findChildIndexByName(colFrame, "Component Blocks") !== componentListIndex) {
8976
+ colFrame.insertChild(componentListIndex, componentList);
8977
+ }
8978
+ }
8651
8979
  return colFrame;
8652
8980
  }
8653
8981
  let columns = findChildByName(section, "Columns");
8654
8982
  if (!columns) {
8655
8983
  columns = figma.createFrame();
8656
8984
  columns.name = "Columns";
8657
- columns.layoutMode = "HORIZONTAL";
8658
- columns.itemSpacing = 32;
8659
- columns.primaryAxisSizingMode = "AUTO";
8660
- columns.counterAxisSizingMode = "AUTO";
8661
- columns.fills = [];
8662
- ctx.applyClipBehavior(columns, []);
8663
8985
  section.appendChild(columns);
8664
8986
  }
8987
+ columns.layoutMode = "HORIZONTAL";
8988
+ columns.itemSpacing = BOARD_LAYOUT.columnsGap;
8989
+ columns.primaryAxisSizingMode = "AUTO";
8990
+ columns.counterAxisSizingMode = "AUTO";
8991
+ columns.counterAxisAlignItems = "MIN";
8992
+ columns.fills = [];
8993
+ ctx.applyClipBehavior(columns, []);
8665
8994
  const requestedThemes = Array.isArray(options.themeNames) ? options.themeNames.filter(function(theme, index, list) {
8666
8995
  return typeof theme === "string" && theme.trim().length > 0 && list.indexOf(theme) === index;
8667
8996
  }) : [];
@@ -8673,16 +9002,17 @@
8673
9002
  if (multiMode) {
8674
9003
  const activeTheme = requestedThemes[0] || "primary";
8675
9004
  const singleCol = addColumn("Theme", activeTheme);
8676
- if (!findChildByName(columns, "Theme Column")) {
8677
- columns.appendChild(singleCol);
9005
+ if (findChildIndexByName(columns, "Theme Column") !== 0) {
9006
+ columns.insertChild(0, singleCol);
8678
9007
  }
8679
9008
  setThemeMode(singleCol, activeTheme);
8680
9009
  } else {
8681
9010
  for (let ti = 0; ti < requestedThemes.length; ti++) {
8682
9011
  const themeName = requestedThemes[ti];
8683
9012
  const themeCol = addColumn(formatThemeLabel(themeName), themeName);
8684
- if (!findChildByName(columns, formatThemeLabel(themeName) + " Column")) {
8685
- columns.appendChild(themeCol);
9013
+ const columnName = formatThemeLabel(themeName) + " Column";
9014
+ if (findChildIndexByName(columns, columnName) !== ti) {
9015
+ columns.insertChild(ti, themeCol);
8686
9016
  }
8687
9017
  setThemeMode(themeCol, themeName);
8688
9018
  }
@@ -10235,6 +10565,7 @@
10235
10565
  function normalizeComponentDef(raw) {
10236
10566
  if (!raw || !raw.analysis) return raw;
10237
10567
  return __spreadProps(__spreadValues({}, raw.analysis), {
10568
+ filePath: raw.analysis.filePath || raw.filePath,
10238
10569
  stories: Array.isArray(raw.analysis.stories) ? raw.analysis.stories : Array.isArray(raw.stories) ? raw.stories : [],
10239
10570
  hasStory: typeof raw.analysis.hasStory === "boolean" ? raw.analysis.hasStory : !!raw.hasStory,
10240
10571
  layout: raw.layout,
@@ -11342,6 +11673,16 @@
11342
11673
 
11343
11674
  // src/design-system.ts
11344
11675
  var EFFECTS_COMPONENTS = ["Gradient-showcase"];
11676
+ function removeStalePageLabels(page, labels) {
11677
+ if (!page || !Array.isArray(page.children)) return;
11678
+ const targets = new Set(labels);
11679
+ const staleNodes = page.children.filter(function(node) {
11680
+ return node && node.type === "TEXT" && typeof node.characters === "string" && targets.has(node.characters);
11681
+ });
11682
+ for (let i = 0; i < staleNodes.length; i++) {
11683
+ staleNodes[i].remove();
11684
+ }
11685
+ }
11345
11686
  function buildDesignSystemSinglePage() {
11346
11687
  let ds = figma.root.children.find((p) => p.name === "Design System");
11347
11688
  if (!ds) {
@@ -11349,6 +11690,7 @@
11349
11690
  ds.name = "Design System";
11350
11691
  }
11351
11692
  figma.currentPage = ds;
11693
+ removeStalePageLabels(ds, ["Design Tokens", "UI Components", "Effects"]);
11352
11694
  const themeNames = getThemeNames(TOKENS);
11353
11695
  const tokenHash = hashString(JSON.stringify(TOKENS));
11354
11696
  let tokensRow = findChildByName(ds, "Design Tokens");
@@ -11384,7 +11726,8 @@
11384
11726
  yOffset: defaultUiY,
11385
11727
  xOffset: 48,
11386
11728
  excludeComponents: EFFECTS_COMPONENTS,
11387
- tokenHash
11729
+ tokenHash,
11730
+ showSectionHeader: false
11388
11731
  });
11389
11732
  const uiSection = findChildByName(ds, "UI Components");
11390
11733
  const defaultEffectsY = uiSection ? uiSection.y + uiSection.height + 80 : defaultUiY + 800;
@@ -11394,7 +11737,8 @@
11394
11737
  xOffset: 48,
11395
11738
  sectionTitle: "Effects",
11396
11739
  onlyComponents: EFFECTS_COMPONENTS,
11397
- tokenHash
11740
+ tokenHash,
11741
+ showSectionHeader: false
11398
11742
  });
11399
11743
  }
11400
11744
 
@@ -11416,7 +11760,7 @@
11416
11760
  }
11417
11761
  function coerceTokenSourceInfo(raw) {
11418
11762
  const mode = raw && (raw.mode === "css" || raw.mode === "dtcg" || raw.mode === "embedded") ? raw.mode : "embedded";
11419
- const requestedMode = raw && (raw.requestedMode === "auto" || raw.requestedMode === "css" || raw.requestedMode === "dtcg") ? raw.requestedMode : void 0;
11763
+ const requestedMode = raw && (raw.requestedMode === "css" || raw.requestedMode === "dtcg") ? raw.requestedMode : void 0;
11420
11764
  const source = raw && typeof raw.source === "string" && raw.source.trim() ? raw.source.trim() : "embedded:tokens.ts";
11421
11765
  return { source, mode, requestedMode };
11422
11766
  }
@@ -11757,7 +12101,7 @@
11757
12101
  repo: msg.repo || "",
11758
12102
  baseBranch: msg.baseBranch || "main",
11759
12103
  tokenPath: msg.tokenPath || "design-tokens/tokens.dtcg.json",
11760
- tokenSourceMode: msg.tokenSourceMode || "auto",
12104
+ tokenSourceMode: msg.tokenSourceMode || "css",
11761
12105
  cssTokenPath: msg.cssTokenPath || "",
11762
12106
  syncDtcgOnPush: msg.syncDtcgOnPush === true,
11763
12107
  allowNewTokensFromFigma: msg.allowNewTokensFromFigma === true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inkbridge",
3
- "version": "0.1.0-beta.4",
3
+ "version": "0.1.0-beta.6",
4
4
  "description": "Figma plugin that generates a pixel-accurate design system from your Tailwind React components.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,10 +42,5 @@
42
42
  "storybook"
43
43
  ],
44
44
  "license": "MIT",
45
- "homepage": "https://inkbridge.io",
46
- "repository": {
47
- "type": "git",
48
- "url": "https://github.com/inkn9ne/inkbridge.git",
49
- "directory": "tools/figma-plugin-tailwind-tokens"
50
- }
45
+ "homepage": "https://inkbridge.ink"
51
46
  }
@@ -2457,10 +2457,105 @@ export class ComponentScanner {
2457
2457
  }
2458
2458
  return parts.join(' ').trim();
2459
2459
  }
2460
+ // Try to resolve as a CVA variant function call (e.g., alertVariants({ variant }))
2461
+ const cvaResult = this.resolveCvaFunctionCall(expr, propsContext);
2462
+ if (cvaResult !== null) return cvaResult;
2460
2463
  }
2461
2464
  return '';
2462
2465
  }
2463
2466
 
2467
+ /**
2468
+ * Resolve a CVA variant function call like `alertVariants({ variant })` to its
2469
+ * combined class string. Looks up the function definition in the same source file,
2470
+ * then merges base classes with the appropriate variant classes.
2471
+ */
2472
+ private resolveCvaFunctionCall(callExpr: Node, propsContext: Map<string, any>): string | null {
2473
+ if (!Node.isCallExpression(callExpr)) return null;
2474
+ const funcName = callExpr.getExpression().getText();
2475
+ const sourceFile = callExpr.getSourceFile();
2476
+
2477
+ // Find `const funcName = cva(...)` in the source file
2478
+ for (const varStmt of sourceFile.getVariableStatements()) {
2479
+ for (const decl of varStmt.getDeclarationList().getDeclarations()) {
2480
+ if (decl.getName() !== funcName) continue;
2481
+ const init = decl.getInitializer();
2482
+ if (!init || !Node.isCallExpression(init)) continue;
2483
+ if (init.getExpression().getText() !== 'cva') continue;
2484
+
2485
+ const cvaArgs = init.getArguments();
2486
+ if (cvaArgs.length === 0) return '';
2487
+
2488
+ const baseClasses = this.extractStringValue(cvaArgs[0]);
2489
+ const classes: string[] = baseClasses ? [baseClasses] : [];
2490
+
2491
+ if (cvaArgs.length >= 2 && Node.isObjectLiteralExpression(cvaArgs[1])) {
2492
+ const configObj = cvaArgs[1];
2493
+
2494
+ // Parse the call argument object e.g. { variant } or { variant: "destructive" }
2495
+ const callArgs = callExpr.getArguments();
2496
+ const requestedVariants: Record<string, string> = {};
2497
+ if (callArgs.length > 0 && Node.isObjectLiteralExpression(callArgs[0])) {
2498
+ for (const prop of callArgs[0].getProperties()) {
2499
+ if (Node.isPropertyAssignment(prop)) {
2500
+ const key = prop.getName();
2501
+ const valInit = prop.getInitializer();
2502
+ if (!valInit) continue;
2503
+ const resolved = this.resolveExpressionValue(valInit, propsContext);
2504
+ if (typeof resolved === 'string') requestedVariants[key] = resolved;
2505
+ else if (Node.isStringLiteral(valInit)) requestedVariants[key] = valInit.getLiteralValue();
2506
+ } else if (Node.isShorthandPropertyAssignment(prop)) {
2507
+ // { variant } shorthand — look up value from propsContext
2508
+ const key = prop.getName();
2509
+ const resolved = propsContext.get(key);
2510
+ if (typeof resolved === 'string') requestedVariants[key] = resolved;
2511
+ }
2512
+ }
2513
+ }
2514
+
2515
+ // Extract defaultVariants
2516
+ const defaultVariants: Record<string, string> = {};
2517
+ const defaultVariantsProp = configObj.getProperty('defaultVariants');
2518
+ if (defaultVariantsProp && Node.isPropertyAssignment(defaultVariantsProp)) {
2519
+ const defaultObj = defaultVariantsProp.getInitializer();
2520
+ if (defaultObj && Node.isObjectLiteralExpression(defaultObj)) {
2521
+ for (const prop of defaultObj.getProperties()) {
2522
+ if (!Node.isPropertyAssignment(prop)) continue;
2523
+ const val = this.extractStringValue(prop.getInitializer()!);
2524
+ defaultVariants[prop.getName()] = val.replace(/['"]/g, '');
2525
+ }
2526
+ }
2527
+ }
2528
+
2529
+ // Look up each variant and add its classes
2530
+ const variantsProp = configObj.getProperty('variants');
2531
+ if (variantsProp && Node.isPropertyAssignment(variantsProp)) {
2532
+ const variantsObj = variantsProp.getInitializer();
2533
+ if (variantsObj && Node.isObjectLiteralExpression(variantsObj)) {
2534
+ for (const variantProp of variantsObj.getProperties()) {
2535
+ if (!Node.isPropertyAssignment(variantProp)) continue;
2536
+ const variantName = variantProp.getName();
2537
+ const selectedValue = requestedVariants[variantName] ?? defaultVariants[variantName];
2538
+ if (!selectedValue) continue;
2539
+
2540
+ const variantValuesObj = variantProp.getInitializer();
2541
+ if (!variantValuesObj || !Node.isObjectLiteralExpression(variantValuesObj)) continue;
2542
+
2543
+ const matchedProp = variantValuesObj.getProperty(selectedValue);
2544
+ if (matchedProp && Node.isPropertyAssignment(matchedProp)) {
2545
+ const variantClassStr = this.extractStringValue(matchedProp.getInitializer()!);
2546
+ if (variantClassStr) classes.push(variantClassStr);
2547
+ }
2548
+ }
2549
+ }
2550
+ }
2551
+ }
2552
+
2553
+ return classes.filter(Boolean).join(' ').trim();
2554
+ }
2555
+ }
2556
+ return null;
2557
+ }
2558
+
2464
2559
  /**
2465
2560
  * Extract props from a JSX node (opening element or self-closing element).
2466
2561
  * Uses getDescendantsOfKind to safely get only JsxAttribute nodes,
@@ -9,10 +9,10 @@ import {
9
9
  } from '../src/token-source';
10
10
 
11
11
  const CSS_DISCOVERY_PATHS = [
12
- 'src/app/tokens.css',
13
12
  'src/app/globals.css',
14
13
  'app/globals.css',
15
14
  'styles/globals.css',
15
+ 'src/app/tokens.css',
16
16
  ];
17
17
 
18
18
  const DEFAULT_DTCG_PATH = 'design-tokens/tokens.dtcg.json';
@@ -84,6 +84,10 @@ function resolveImportedCssPath(baseFilePath: string, params: string): string |
84
84
  }
85
85
 
86
86
  function cssHasTokenDeclarations(cssText: string): boolean {
87
+ // Only count declarations inside :root {} or .[theme] {} rules — the same
88
+ // selectors that patchCssVariables targets. We intentionally skip @theme
89
+ // at-rules (Tailwind v4 utility mappings) because those contain var()
90
+ // references, not source values, and the patcher cannot update them.
87
91
  try {
88
92
  const root = postcss.parse(cssText);
89
93
  let found = false;
@@ -91,16 +95,9 @@ function cssHasTokenDeclarations(cssText: string): boolean {
91
95
  if (found) return;
92
96
  if (!decl.prop || !decl.prop.startsWith('--')) return;
93
97
  const parent = decl.parent;
94
- if (!parent) return;
95
- if (parent.type === 'atrule') {
96
- const at = parent as AtRule;
97
- if ((at.name || '').toLowerCase() === 'theme') found = true;
98
- return;
99
- }
100
- if (parent.type === 'rule') {
101
- const selector = (parent as Rule).selector || '';
102
- if (parseThemeSelectors(selector).length > 0) found = true;
103
- }
98
+ if (!parent || parent.type !== 'rule') return;
99
+ const selector = (parent as Rule).selector || '';
100
+ if (parseThemeSelectors(selector).length > 0) found = true;
104
101
  });
105
102
  return found;
106
103
  } catch {
@@ -108,7 +105,7 @@ function cssHasTokenDeclarations(cssText: string): boolean {
108
105
  }
109
106
  }
110
107
 
111
- function resolveCssTokenPathFromImports(filePath: string, visited: Set<string> = new Set()): string {
108
+ export function resolveCssTokenPathFromImports(filePath: string, visited: Set<string> = new Set()): string {
112
109
  const absolute = path.resolve(filePath);
113
110
  if (visited.has(absolute)) return absolute;
114
111
  visited.add(absolute);
@@ -179,12 +176,11 @@ function readCssWithImports(filePath: string, visited: Set<string> = new Set()):
179
176
 
180
177
  export function discoverCssTokenPath(projectRoot: string, explicitPath?: string): string | null {
181
178
  if (explicitPath && explicitPath.trim()) {
182
- const explicit = discoverFilePath(projectRoot, explicitPath.trim());
183
- return explicit ? resolveCssTokenPathFromImports(explicit) : null;
179
+ return discoverFilePath(projectRoot, explicitPath.trim());
184
180
  }
185
181
  for (const rel of CSS_DISCOVERY_PATHS) {
186
182
  const found = discoverFilePath(projectRoot, rel);
187
- if (found) return resolveCssTokenPathFromImports(found);
183
+ if (found) return found;
188
184
  }
189
185
  return null;
190
186
  }
@@ -384,7 +380,7 @@ function walkCssNodes(map: ScannedTokenMap, container: postcss.Container): void
384
380
  }
385
381
  }
386
382
 
387
- export function parseCssTokenMap(cssText: string, source: string, requestedMode: TokenSourceMode = 'auto'): ScannedTokenMap {
383
+ export function parseCssTokenMap(cssText: string, source: string, requestedMode: TokenSourceMode = 'css'): ScannedTokenMap {
388
384
  const map = createEmptyScannedTokenMap('css', source, requestedMode);
389
385
  const root = postcss.parse(cssText);
390
386
  walkCssNodes(map, root);
@@ -425,7 +421,7 @@ function applyDtcgDimensionGroup(
425
421
  function parseDtcgTokenMap(
426
422
  dtcgJson: unknown,
427
423
  source: string,
428
- requestedMode: TokenSourceMode = 'auto'
424
+ requestedMode: TokenSourceMode = 'dtcg'
429
425
  ): ScannedTokenMap {
430
426
  const map = createEmptyScannedTokenMap('dtcg', source, requestedMode);
431
427
  if (!isPlainObject(dtcgJson)) return map;
@@ -453,16 +449,10 @@ function parseDtcgTokenMap(
453
449
 
454
450
  export function readTokenSourceMap(options: ReadTokenSourceOptions): ScannedTokenMap {
455
451
  const projectRoot = path.resolve(options.projectRoot);
456
- const requestedMode = options.tokenSourceMode || 'auto';
452
+ const requestedMode: TokenSourceMode = options.tokenSourceMode === 'dtcg' ? 'dtcg' : 'css';
457
453
  const cssPath = discoverCssTokenPath(projectRoot, options.cssTokenPath);
458
454
  const dtcgPath = discoverDtcgTokenPath(projectRoot, options.dtcgTokenPath);
459
455
 
460
- if (requestedMode === 'css') {
461
- if (!cssPath) return createEmptyScannedTokenMap('embedded', 'embedded:tokens.ts', requestedMode);
462
- const cssText = readCssWithImports(cssPath);
463
- return parseCssTokenMap(cssText, toDisplayPath(projectRoot, cssPath), requestedMode);
464
- }
465
-
466
456
  if (requestedMode === 'dtcg') {
467
457
  if (!dtcgPath) return createEmptyScannedTokenMap('embedded', 'embedded:tokens.ts', requestedMode);
468
458
  const dtcgText = fs.readFileSync(dtcgPath, 'utf-8');
@@ -470,10 +460,14 @@ export function readTokenSourceMap(options: ReadTokenSourceOptions): ScannedToke
470
460
  return parseDtcgTokenMap(dtcgJson, toDisplayPath(projectRoot, dtcgPath), requestedMode);
471
461
  }
472
462
 
473
- // auto mode: CSS always wins when present.
463
+ // css mode: CSS preferred; fall back to DTCG if no CSS file found, then embedded.
474
464
  if (cssPath) {
475
465
  const cssText = readCssWithImports(cssPath);
476
- return parseCssTokenMap(cssText, toDisplayPath(projectRoot, cssPath), requestedMode);
466
+ // Use the file that actually contains the declarations as source so the
467
+ // write-back path (PR/patch) targets the right file even when globals.css
468
+ // delegates token definitions to an imported file like tokens.css.
469
+ const writeTarget = resolveCssTokenPathFromImports(cssPath);
470
+ return parseCssTokenMap(cssText, toDisplayPath(projectRoot, writeTarget), requestedMode);
477
471
  }
478
472
  if (dtcgPath) {
479
473
  const dtcgText = fs.readFileSync(dtcgPath, 'utf-8');
@@ -1,4 +1,4 @@
1
- export type TokenSourceMode = 'auto' | 'css' | 'dtcg';
1
+ export type TokenSourceMode = 'css' | 'dtcg';
2
2
  export type ResolvedTokenSourceMode = 'css' | 'dtcg' | 'embedded';
3
3
 
4
4
  export interface ScannedThemeTokens {
package/ui.html CHANGED
@@ -205,7 +205,7 @@
205
205
  </div>
206
206
  <div id="tokenSourceInfoPush" class="repo-display" style="display:none;padding:6px 8px;">
207
207
  Last scan source: <strong id="tokenSourceLabelPush"></strong><br>
208
- Configured mode: <strong id="configuredTokenSourceModePush">auto</strong>
208
+ Configured mode: <strong id="configuredTokenSourceModePush">css</strong>
209
209
  </div>
210
210
 
211
211
  <div class="field">
@@ -250,7 +250,7 @@
250
250
 
251
251
  <div id="tokenSourceInfoSettings" class="repo-display" style="display:none;padding:6px 8px;">
252
252
  Last scan source: <strong id="tokenSourceLabelSettings"></strong><br>
253
- Configured mode: <strong id="configuredTokenSourceModeSettings">auto</strong>
253
+ Configured mode: <strong id="configuredTokenSourceModeSettings">css</strong>
254
254
  </div>
255
255
 
256
256
  <div class="field">
@@ -271,11 +271,10 @@
271
271
  <div class="field">
272
272
  <label>Token Source Mode</label>
273
273
  <select id="settingsTokenSourceMode">
274
- <option value="auto">auto (prefer CSS)</option>
275
- <option value="css">css (force CSS only)</option>
276
- <option value="dtcg">dtcg (force DTCG only)</option>
274
+ <option value="css">css (auto-discover CSS, fallback to DTCG)</option>
275
+ <option value="dtcg">dtcg (force DTCG file only)</option>
277
276
  </select>
278
- <p class="hint">`auto` prefers CSS and falls back to DTCG, then embedded tokens.</p>
277
+ <p class="hint">CSS mode auto-discovers your globals.css and falls back to DTCG if not found. Use DTCG to force the legacy token file.</p>
279
278
  </div>
280
279
 
281
280
  <div class="field" id="tokenPathField">
@@ -291,25 +290,25 @@
291
290
  </div>
292
291
 
293
292
  <div class="field" id="syncDtcgOnPushField">
294
- <label style="display:flex;align-items:center;gap:8px;">
295
- <input type="checkbox" id="settingsSyncDtcgOnPush">
296
- Also update DTCG on Push to Code
297
- </label>
293
+ <div style="display:flex;align-items:center;gap:8px;">
294
+ <input type="checkbox" id="settingsSyncDtcgOnPush" style="width:auto;flex-shrink:0;">
295
+ <label for="settingsSyncDtcgOnPush" style="display:inline;margin:0;cursor:pointer;">Also update DTCG on Push to Code</label>
296
+ </div>
298
297
  <p class="hint">When enabled, the plugin also commits <code>tokens.dtcg.json</code> as a generated artifact.</p>
299
298
  </div>
300
299
 
301
300
  <div class="field">
302
- <label style="display:flex;align-items:center;gap:8px;">
303
- <input type="checkbox" id="settingsAllowNewTokensFromFigma">
304
- Allow New Tokens from Figma
305
- </label>
301
+ <div style="display:flex;align-items:center;gap:8px;">
302
+ <input type="checkbox" id="settingsAllowNewTokensFromFigma" style="width:auto;flex-shrink:0;">
303
+ <label for="settingsAllowNewTokensFromFigma" style="display:inline;margin:0;cursor:pointer;">Allow New Tokens from Figma</label>
304
+ </div>
306
305
  <p class="hint">Disabled by default. When enabled, new token keys can be added to code on push.</p>
307
306
  </div>
308
307
 
309
- <div class="field">
308
+ <div class="field" id="newTokenPrefixesField" style="display:none;">
310
309
  <label>New Token Prefixes (optional)</label>
311
310
  <input type="text" id="settingsNewTokenPrefixes" placeholder="chart-, brand-">
312
- <p class="hint">Comma-separated. If empty, all new keys are allowed. If set, only matching prefixes are allowed.</p>
311
+ <p class="hint">Comma-separated. Only tokens with these prefixes can be added. Leave empty to allow all new tokens.</p>
313
312
  </div>
314
313
 
315
314
  <div class="divider"></div>
@@ -410,7 +409,7 @@
410
409
  </div>
411
410
  <div id="tokenSourceInfoSync" class="repo-display" style="display:none;padding:6px 8px;">
412
411
  Last scan source: <strong id="tokenSourceLabelSync"></strong><br>
413
- Configured mode: <strong id="configuredTokenSourceModeSync">auto</strong>
412
+ Configured mode: <strong id="configuredTokenSourceModeSync">css</strong>
414
413
  </div>
415
414
 
416
415
  <p style="font-size: 11px; color: #666; margin-bottom: 12px;">
@@ -531,6 +530,7 @@
531
530
  var syncDtcgOnPushField = document.getElementById('syncDtcgOnPushField');
532
531
  var settingsAllowNewTokensFromFigma = document.getElementById('settingsAllowNewTokensFromFigma');
533
532
  var settingsNewTokenPrefixes = document.getElementById('settingsNewTokenPrefixes');
533
+ var newTokenPrefixesField = document.getElementById('newTokenPrefixesField');
534
534
  var settingsToken = document.getElementById('settingsToken');
535
535
  var settingsProjectName = document.getElementById('settingsProjectName');
536
536
  var settingsLicenseKey = document.getElementById('settingsLicenseKey');
@@ -538,7 +538,7 @@
538
538
  // Current detected changes
539
539
  var detectedChanges = { tokens: true, components: [] };
540
540
  var lastTokenSourceInfo = null;
541
- var configuredTokenSourceMode = 'auto';
541
+ var configuredTokenSourceMode = 'css';
542
542
 
543
543
  function resizeToContent() {
544
544
  // Wait for layout to settle before measuring
@@ -580,25 +580,20 @@
580
580
  }
581
581
 
582
582
  function normalizeConfiguredMode(mode) {
583
- if (mode === 'css' || mode === 'dtcg' || mode === 'auto') return mode;
584
- return 'auto';
583
+ if (mode === 'dtcg') return 'dtcg';
584
+ return 'css';
585
585
  }
586
586
 
587
587
  function applyTokenSourceModeVisibility(mode) {
588
588
  var normalized = normalizeConfiguredMode(mode);
589
- if (normalized === 'css') {
590
- tokenPathField.style.display = 'none';
591
- cssTokenPathField.style.display = 'block';
592
- syncDtcgOnPushField.style.display = 'block';
593
- return;
594
- }
595
589
  if (normalized === 'dtcg') {
596
590
  tokenPathField.style.display = 'block';
597
591
  cssTokenPathField.style.display = 'none';
598
592
  syncDtcgOnPushField.style.display = 'none';
599
593
  return;
600
594
  }
601
- tokenPathField.style.display = 'block';
595
+ // css mode
596
+ tokenPathField.style.display = 'none';
602
597
  cssTokenPathField.style.display = 'block';
603
598
  syncDtcgOnPushField.style.display = 'block';
604
599
  }
@@ -980,7 +975,8 @@
980
975
  settingsCssTokenPath.value = config.cssTokenPath || '';
981
976
  settingsSyncDtcgOnPush.checked = config.syncDtcgOnPush === true;
982
977
  settingsAllowNewTokensFromFigma.checked = config.allowNewTokensFromFigma === true;
983
- settingsNewTokenPrefixes.value = Array.isArray(config.newTokenPrefixes) ? config.newTokenPrefixes.join(', ') : '';
978
+ settingsNewTokenPrefixes.value = Array.isArray(config.newTokenPrefixes) ? config.newTokenPrefixes.join(', ') : (typeof config.newTokenPrefixes === 'string' ? config.newTokenPrefixes : '');
979
+ newTokenPrefixesField.style.display = config.allowNewTokensFromFigma === true ? 'block' : 'none';
984
980
  settingsProjectName.value = config.projectName || '';
985
981
  configuredTokenSourceMode = tokenSourceMode;
986
982
  applyTokenSourceModeVisibility(tokenSourceMode);
@@ -1197,7 +1193,7 @@
1197
1193
  repo: repo,
1198
1194
  baseBranch: settingsBranch.value.trim() || 'main',
1199
1195
  tokenPath: settingsTokenPath.value.trim() || 'design-tokens/tokens.dtcg.json',
1200
- tokenSourceMode: settingsTokenSourceMode.value || 'auto',
1196
+ tokenSourceMode: settingsTokenSourceMode.value || 'css',
1201
1197
  cssTokenPath: settingsCssTokenPath.value.trim(),
1202
1198
  syncDtcgOnPush: settingsSyncDtcgOnPush.checked === true,
1203
1199
  allowNewTokensFromFigma: settingsAllowNewTokensFromFigma.checked === true,
@@ -1217,6 +1213,10 @@
1217
1213
  applyTokenSourceModeVisibility(configuredTokenSourceMode);
1218
1214
  setTokenSourceInfo(lastTokenSourceInfo);
1219
1215
  };
1216
+
1217
+ settingsAllowNewTokensFromFigma.onchange = function() {
1218
+ newTokenPrefixesField.style.display = settingsAllowNewTokensFromFigma.checked ? 'block' : 'none';
1219
+ };
1220
1220
  </script>
1221
1221
  </body>
1222
1222
  </html>