@sxl-studio/storybook-addon 1.0.7 → 1.0.8

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.
Files changed (2) hide show
  1. package/dist/manager.js +178 -34
  2. package/package.json +1 -1
package/dist/manager.js CHANGED
@@ -9,7 +9,61 @@ var PARAM_KEY = "sxl";
9
9
 
10
10
  // src/components/SxlPanel.tsx
11
11
  import React from "react";
12
- import { useParameter } from "@storybook/manager-api";
12
+ import { useParameter, useStorybookState } from "@storybook/manager-api";
13
+
14
+ // src/convert.ts
15
+ function fromDiffCodeConnect(data) {
16
+ if (!data || typeof data !== "object") {
17
+ return { version: 1, figmaFileKey: "", entries: [] };
18
+ }
19
+ const d = data;
20
+ const fileKey = typeof d.$figmaFileKey === "string" ? d.$figmaFileKey : "";
21
+ const fileName = typeof d.$figmaFileName === "string" ? d.$figmaFileName : void 0;
22
+ const rawEntries = Array.isArray(d.entries) ? d.entries : [];
23
+ const entries = rawEntries.filter((e) => {
24
+ if (!e || typeof e !== "object") return false;
25
+ const o = e;
26
+ return o.linked === true && !!o.binding;
27
+ }).map((e) => {
28
+ const b = e.binding;
29
+ const sb = b.storybook ?? {};
30
+ const nodeId = String(e.nodeId ?? "");
31
+ const figmaUrl = fileKey ? `https://www.figma.com/design/${fileKey}?node-id=${nodeId.replace(":", "-")}` : void 0;
32
+ const tokenStatus = sb.tokensReady === true ? "assigned" : void 0;
33
+ const statusRaw = typeof sb.status === "string" ? sb.status : void 0;
34
+ const readiness = statusRaw === "complete" || statusRaw === "ready-for-dev" || statusRaw === "in-progress" || statusRaw === "backlog" ? statusRaw : void 0;
35
+ const rawFiles = Array.isArray(b.files) ? b.files : [];
36
+ const files = rawFiles.filter((f) => !!f && typeof f === "object").map((f) => ({
37
+ framework: String(f.framework ?? ""),
38
+ filePath: String(f.filePath ?? ""),
39
+ ...f.componentName ? { componentName: String(f.componentName) } : {},
40
+ ...f.importPath ? { importPath: String(f.importPath) } : {}
41
+ }));
42
+ return {
43
+ nodeId,
44
+ displayName: String(b.displayName ?? e.nodeName ?? ""),
45
+ description: sb.description ? String(sb.description) : void 0,
46
+ figmaUrl,
47
+ designEmbed: sb.designEmbed === true,
48
+ compositionJson: sb.compositionJson === true,
49
+ metadata: sb.metadata === true,
50
+ updatedAt: typeof e.updatedAt === "string" ? e.updatedAt : void 0,
51
+ importPath: typeof b.importPath === "string" ? b.importPath : void 0,
52
+ snippetTemplate: typeof b.snippetTemplate === "string" ? b.snippetTemplate : void 0,
53
+ files: files.length > 0 ? files : void 0,
54
+ meta: {
55
+ ...tokenStatus ? { tokenStatus } : {},
56
+ ...readiness ? { readiness } : {}
57
+ }
58
+ };
59
+ });
60
+ return {
61
+ version: 1,
62
+ figmaFileKey: fileKey,
63
+ figmaFileName: fileName,
64
+ entries
65
+ };
66
+ }
13
67
 
14
68
  // src/components/styles.ts
15
69
  var FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
@@ -237,25 +291,39 @@ var READINESS_MAP = {
237
291
  "in-progress": { label: "In Progress", fg: "#92400e", bg: "#fef3c7", dot: "#f59e0b" },
238
292
  "backlog": { label: "Backlog", fg: "#6b7280", bg: "#f3f4f6", dot: "#9ca3af" }
239
293
  };
240
- function resolveEntry(params) {
241
- if (!params) return null;
242
- const registry = params.registry;
294
+ function normalizeRegistry(raw) {
295
+ if (!raw || typeof raw !== "object") return null;
296
+ const obj = raw;
297
+ const entries = Array.isArray(obj.entries) ? obj.entries : [];
298
+ if (entries.length === 0) return null;
299
+ const first = entries[0];
300
+ if (first && "binding" in first && "linked" in first) {
301
+ return fromDiffCodeConnect(raw);
302
+ }
303
+ if (first && "nodeId" in first && "displayName" in first) {
304
+ return raw;
305
+ }
306
+ return null;
307
+ }
308
+ function resolveEntry(params, ctx) {
309
+ if (!params) return { status: "no-registry" };
310
+ const registry = normalizeRegistry(params.registry);
243
311
  if (params.figmaUrl || params.description) {
244
312
  return {
245
- nodeId: params.figmaNodeId ?? "",
246
- displayName: params.component ?? params.componentName ?? "",
247
- description: params.description,
248
- figmaUrl: params.figmaUrl,
249
- designEmbed: params.designEmbed ?? !!params.figmaUrl,
250
- compositionJson: params.compositionJson,
251
- metadata: params.metadata,
252
- meta: {
253
- tokenStatus: params.tokenStatus,
254
- readiness: params.readiness
313
+ status: "found",
314
+ entry: {
315
+ nodeId: params.figmaNodeId ?? "",
316
+ displayName: params.component ?? params.componentName ?? "",
317
+ description: params.description,
318
+ figmaUrl: params.figmaUrl,
319
+ designEmbed: params.designEmbed ?? !!params.figmaUrl,
320
+ compositionJson: params.compositionJson,
321
+ metadata: params.metadata,
322
+ meta: { tokenStatus: params.tokenStatus, readiness: params.readiness }
255
323
  }
256
324
  };
257
325
  }
258
- if (!registry?.entries?.length) return null;
326
+ if (!registry) return { status: "no-registry" };
259
327
  let found;
260
328
  if (params.figmaNodeId) {
261
329
  found = registry.entries.find((e) => e.nodeId === params.figmaNodeId);
@@ -269,27 +337,92 @@ function resolveEntry(params) {
269
337
  if (!found && registry.entries.length === 1) {
270
338
  found = registry.entries[0];
271
339
  }
272
- if (!found) return null;
340
+ if (!found && registry.entries.length > 1) {
341
+ found = matchByStoryContext(registry.entries, ctx);
342
+ }
343
+ if (!found) {
344
+ const hint = extractComponentName(ctx.title) || ctx.name || "";
345
+ return { status: "no-match", componentHint: hint };
346
+ }
273
347
  return {
274
- ...found,
275
- meta: {
276
- ...found.meta,
277
- tokenStatus: params.tokenStatus ?? found.meta?.tokenStatus,
278
- readiness: params.readiness ?? found.meta?.readiness
279
- },
280
- description: params.description ?? found.description,
281
- compositionJson: params.compositionJson ?? found.compositionJson,
282
- metadata: params.metadata ?? found.metadata,
283
- designEmbed: params.designEmbed ?? found.designEmbed
348
+ status: "found",
349
+ entry: {
350
+ ...found,
351
+ meta: {
352
+ ...found.meta,
353
+ tokenStatus: params.tokenStatus ?? found.meta?.tokenStatus,
354
+ readiness: params.readiness ?? found.meta?.readiness
355
+ },
356
+ description: params.description ?? found.description,
357
+ compositionJson: params.compositionJson ?? found.compositionJson,
358
+ metadata: params.metadata ?? found.metadata,
359
+ designEmbed: params.designEmbed ?? found.designEmbed
360
+ }
284
361
  };
285
362
  }
363
+ function matchByStoryContext(entries, ctx) {
364
+ const tokens = [
365
+ extractComponentName(ctx.title),
366
+ ctx.name,
367
+ ctx.storyId,
368
+ ctx.importPath,
369
+ fileStem(ctx.importPath)
370
+ ].filter(Boolean).map(norm);
371
+ if (tokens.length === 0) return void 0;
372
+ let best;
373
+ let bestScore = 0;
374
+ let tie = false;
375
+ for (const entry of entries) {
376
+ const s = score(entry, tokens);
377
+ if (s > bestScore) {
378
+ bestScore = s;
379
+ best = entry;
380
+ tie = false;
381
+ } else if (s === bestScore && s > 0) {
382
+ tie = true;
383
+ }
384
+ }
385
+ if (tie || bestScore < 60) return void 0;
386
+ return best;
387
+ }
388
+ function score(entry, tokens) {
389
+ const display = norm(entry.displayName);
390
+ const imprt = norm(entry.importPath ?? "");
391
+ const stems = (entry.files ?? []).map((f) => norm(fileStem(f.filePath))).filter(Boolean);
392
+ let s = 0;
393
+ for (const t of tokens) {
394
+ if (!t) continue;
395
+ if (display && t === display) s += 120;
396
+ else if (display && t.includes(display)) s += 70;
397
+ else if (display && display.includes(t)) s += 65;
398
+ if (imprt && t.includes(imprt)) s += 55;
399
+ for (const stem of stems) {
400
+ if (stem && t.includes(stem)) s += 30;
401
+ }
402
+ }
403
+ return s;
404
+ }
405
+ function extractComponentName(title) {
406
+ if (!title) return "";
407
+ const parts = title.split("/");
408
+ return parts[parts.length - 1].trim();
409
+ }
410
+ function norm(v) {
411
+ return v.toLowerCase().replace(/[^a-z0-9]+/g, "");
412
+ }
413
+ function fileStem(p) {
414
+ const f = (p || "").split("/").pop() ?? "";
415
+ const d = f.lastIndexOf(".");
416
+ return d > 0 ? f.slice(0, d) : f;
417
+ }
286
418
  function buildEmbedUrl(figmaUrl) {
287
419
  if (figmaUrl.includes("figma.com/embed")) return figmaUrl;
288
420
  return `https://www.figma.com/embed?embed_host=storybook&url=${encodeURIComponent(figmaUrl)}`;
289
421
  }
290
422
  function buildDevModeUrl(figmaUrl) {
291
423
  try {
292
- const u = new URL(figmaUrl.includes("figma.com/embed") ? new URL(figmaUrl).searchParams.get("url") ?? figmaUrl : figmaUrl);
424
+ const raw = figmaUrl.includes("figma.com/embed") ? new URL(figmaUrl).searchParams.get("url") ?? figmaUrl : figmaUrl;
425
+ const u = new URL(raw);
293
426
  u.searchParams.set("m", "dev");
294
427
  return u.toString();
295
428
  } catch {
@@ -316,22 +449,33 @@ var Badge = ({
316
449
  bg,
317
450
  dot
318
451
  }) => /* @__PURE__ */ React.createElement("span", { style: { ...badge, color: fg, backgroundColor: bg } }, /* @__PURE__ */ React.createElement("span", { style: { ...badgeDot, backgroundColor: dot } }), label);
319
- var EmptyState = () => /* @__PURE__ */ React.createElement("div", { style: emptyContainer }, /* @__PURE__ */ React.createElement("p", { style: emptyTitle }, "No SXL data for this story"), /* @__PURE__ */ React.createElement("p", { style: emptyHint }, "Import the registry in ", /* @__PURE__ */ React.createElement("code", { style: emptyCode }, ".storybook/preview.ts"), ":"), /* @__PURE__ */ React.createElement("pre", { style: codeBlock }, `import raw from '../diff-code-connect.YOUR_KEY.json';
452
+ var NoRegistryState = () => /* @__PURE__ */ React.createElement("div", { style: emptyContainer }, /* @__PURE__ */ React.createElement("p", { style: emptyTitle }, "SXL Studio not configured"), /* @__PURE__ */ React.createElement("p", { style: emptyHint }, "Import the registry in ", /* @__PURE__ */ React.createElement("code", { style: emptyCode }, ".storybook/preview.ts"), ":"), /* @__PURE__ */ React.createElement("pre", { style: codeBlock }, `import raw from '../diff-code-connect.YOUR_KEY.json';
320
453
  import { fromDiffCodeConnect } from '@sxl-studio/storybook-addon';
321
454
 
322
455
  export default {
323
456
  parameters: {
324
457
  sxl: { registry: fromDiffCodeConnect(raw) },
325
458
  },
326
- };`), /* @__PURE__ */ React.createElement("p", { style: emptyHint }, "Then match a story to a Figma component:"), /* @__PURE__ */ React.createElement("pre", { style: codeBlock }, `export const Default = {
327
- parameters: {
328
- sxl: { component: 'ButtonPrimary' },
329
- },
330
459
  };`));
460
+ var NoMatchState = ({ componentHint }) => /* @__PURE__ */ React.createElement("div", { style: emptyContainer }, /* @__PURE__ */ React.createElement("p", { style: emptyTitle }, "No Figma integration for this component"), /* @__PURE__ */ React.createElement("p", { style: emptyHint }, componentHint ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("strong", null, componentHint), " has not been linked in the SXL Studio Figma plugin.") : /* @__PURE__ */ React.createElement(React.Fragment, null, "This story has not been linked to a Figma component.")), /* @__PURE__ */ React.createElement("p", { style: emptyHint }, "To add integration, open the component in Figma, go to ", /* @__PURE__ */ React.createElement("strong", null, "Code \u2192 Storybook Integration"), ", and click ", /* @__PURE__ */ React.createElement("strong", null, "Connect"), "."));
331
461
  var SxlPanel = () => {
332
462
  const params = useParameter(PARAM_KEY);
333
- const entry = resolveEntry(params);
334
- if (!entry) return React.createElement(EmptyState, null);
463
+ const state = useStorybookState();
464
+ const selected = state.storyId && state.index ? state.index[state.storyId] : void 0;
465
+ const ctx = {
466
+ storyId: state.storyId ?? "",
467
+ title: selected?.title ?? "",
468
+ name: selected?.name ?? "",
469
+ importPath: selected?.importPath ?? ""
470
+ };
471
+ const result = resolveEntry(params, ctx);
472
+ if (result.status === "no-registry") {
473
+ return React.createElement(NoRegistryState, null);
474
+ }
475
+ if (result.status === "no-match") {
476
+ return React.createElement(NoMatchState, { componentHint: result.componentHint });
477
+ }
478
+ const entry = result.entry;
335
479
  const tokenStatus = entry.meta?.tokenStatus;
336
480
  const readiness = entry.meta?.readiness;
337
481
  const tokenInfo = tokenStatus ? TOKEN_MAP[tokenStatus] : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sxl-studio/storybook-addon",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Storybook addon for SXL Studio — displays Figma Embed, component info and design token status for linked components",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",