@sxl-studio/storybook-addon 1.1.1 → 1.1.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - **Embed debug:** `parameters.sxl.debugFigmaEmbed` now works in runtime. The panel logs resolved embed diagnostics and iframe load/error events when enabled.
6
+ - **Token status compatibility:** `sxl.tokenStatus` now maps to `tokensBool` in runtime (deprecated but supported), so documented override behavior is consistent.
7
+ - **CSP merge hardening:** `mergeSxlFigmaFrameSrcHeader` now merges into an existing `frame-src` directive instead of appending raw strings.
8
+ - **Quality gates:** added ESLint pipeline and unit tests for entry resolution, composition loading fallback chain, and Storybook preset behavior.
9
+
3
10
  ## 1.1.1
4
11
 
5
12
  - **Registry matching:** removed the behavior where a **single** registry entry was applied to **every** story. Matching now always uses the same story-context heuristic (or explicit `sxl.component` / `sxl.figmaNodeId`). Unrelated stories show the “no Figma integration” state.
package/README.md CHANGED
@@ -175,13 +175,14 @@ export const Default = {
175
175
  | `sxl.figmaNodeId` | `string` | Match entry by Figma node ID |
176
176
  | `sxl.figmaUrl` | `string` | Direct Figma URL (no registry needed) |
177
177
  | `sxl.description` | `string` | Override description |
178
- | `sxl.tokenStatus` | `"assigned" \| "partial" \| "none"` | Override token status |
178
+ | `sxl.tokensBool` | `"true" \| "false"` | Explicit override for Tokens badge |
179
+ | `sxl.tokenStatus` | `"assigned" \| "partial" \| "none"` | Deprecated compatibility override; internally mapped to `tokensBool` |
179
180
  | `sxl.readiness` | `"complete" \| "ready-for-dev" \| "in-progress" \| "backlog"` | Override readiness |
180
181
  | `sxl.compositionSources` | `Record<string, string>` | Map repo-relative composition paths → raw JSON (recommended) |
181
182
  | `sxl.compositionFetchBaseUrl` | `string` | Base URL to `fetch()` composition by path (e.g. static dir) |
182
183
  | `sxl.compositionDevProxyPrefix` | `string` | Same-origin prefix (e.g. Vite proxy) so fetches avoid CORS to private Git |
183
184
  | `sxl.resolveComposition` | `(path) => Promise<string \| undefined>` | Custom loader for composition file content |
184
- | `sxl.debugFigmaEmbed` | `boolean` | Log embed URL and iframe `load` to the console |
185
+ | `sxl.debugFigmaEmbed` | `boolean` | Log embed diagnostics (`resolved`, `iframe-load`, `iframe-error`) to the console |
185
186
 
186
187
  ## Composition JSON (repo file, not inlined in diff)
187
188
 
package/dist/index.d.ts CHANGED
@@ -92,7 +92,7 @@ type SxlStoryParameters = {
92
92
  figmaUrl?: string;
93
93
  /** Direct description (overrides registry). */
94
94
  description?: string;
95
- /** Override token badge per-story. */
95
+ /** @deprecated Prefer `tokensBool`; kept for backward compatibility and mapped to `tokensBool` internally. */
96
96
  tokenStatus?: SxlTokenStatus;
97
97
  tokensBool?: SxlTokensBool;
98
98
  /** Override readiness per-story. */
package/dist/manager.js CHANGED
@@ -12,123 +12,6 @@ var SXL_TOKENS_URL_PREFIX = "/sxl-tokens";
12
12
  import React2, { useCallback as useCallback3, useEffect, useState } from "react";
13
13
  import { useParameter, useStorybookState } from "@storybook/manager-api";
14
14
 
15
- // src/convert.ts
16
- function fromDiffCodeConnect(data) {
17
- if (!data || typeof data !== "object") {
18
- return { version: 1, figmaFileKey: "", entries: [] };
19
- }
20
- const d = data;
21
- const fileKey = typeof d.$figmaFileKey === "string" ? d.$figmaFileKey : "";
22
- const fileName = typeof d.$figmaFileName === "string" ? d.$figmaFileName : void 0;
23
- const repository = d.repository && typeof d.repository === "object" ? d.repository : void 0;
24
- const isV2 = Array.isArray(d.components);
25
- const entries = isV2 ? convertV2Components(d.components, fileKey) : convertV1Entries(Array.isArray(d.entries) ? d.entries : [], fileKey);
26
- return {
27
- version: 1,
28
- figmaFileKey: fileKey,
29
- figmaFileName: fileName,
30
- repository: repository ? {
31
- ...typeof repository.url === "string" ? { url: repository.url } : {},
32
- ...typeof repository.documentationUrl === "string" ? { documentationUrl: repository.documentationUrl } : {}
33
- } : void 0,
34
- entries
35
- };
36
- }
37
- function tokensBoolFromStorybook(sb) {
38
- if (sb.tokensReady === true) return "true";
39
- return "false";
40
- }
41
- function convertV2Components(components, fileKey) {
42
- return components.filter((c) => c.linked === true).map((c) => {
43
- const nodeId = String(c.nodeId ?? "");
44
- const sb = c.storybook ?? {};
45
- const cc = c.codeConnect ?? {};
46
- const figmaUrl = typeof sb.figmaUrl === "string" && sb.figmaUrl.trim() ? sb.figmaUrl.trim() : fileKey ? `https://www.figma.com/design/${fileKey}?node-id=${nodeId.replace(":", "-")}` : void 0;
47
- const statusRaw = typeof sb.status === "string" ? sb.status : void 0;
48
- const readiness = statusRaw === "complete" || statusRaw === "ready-for-dev" || statusRaw === "in-progress" || statusRaw === "backlog" ? statusRaw : void 0;
49
- const files = parseFiles(Array.isArray(cc.files) ? cc.files : []);
50
- const componentApi = parseComponentApi(sb.componentApi);
51
- return {
52
- nodeId,
53
- displayName: String(c.displayName ?? c.name ?? ""),
54
- description: sb.description ? String(sb.description) : void 0,
55
- figmaUrl,
56
- designEmbed: sb.designEmbed === true,
57
- compositionJson: sb.compositionJson === true,
58
- metadata: sb.metadata === true,
59
- updatedAt: typeof c.updatedAt === "string" ? c.updatedAt : void 0,
60
- importPath: typeof cc.importPath === "string" ? cc.importPath : void 0,
61
- snippetTemplate: typeof cc.snippetTemplate === "string" ? cc.snippetTemplate : void 0,
62
- files: files.length > 0 ? files : void 0,
63
- compositionFilePath: typeof sb.compositionFilePath === "string" ? sb.compositionFilePath : void 0,
64
- compositionSnapshot: typeof sb.compositionSnapshot === "string" ? sb.compositionSnapshot : void 0,
65
- compositionName: typeof sb.compositionName === "string" ? sb.compositionName : void 0,
66
- componentApi,
67
- meta: {
68
- tokensBool: tokensBoolFromStorybook(sb),
69
- ...readiness ? { readiness } : {}
70
- }
71
- };
72
- });
73
- }
74
- function parseComponentApi(raw) {
75
- if (!raw || typeof raw !== "object") return void 0;
76
- const o = raw;
77
- if (!Array.isArray(o.properties)) return void 0;
78
- const properties = o.properties.filter((p) => !!p && typeof p === "object").map((p) => ({
79
- name: String(p.name ?? ""),
80
- kind: String(p.kind ?? ""),
81
- ...p.defaultValue !== void 0 ? { defaultValue: p.defaultValue } : {},
82
- ...Array.isArray(p.options) ? { options: p.options.map(String) } : {}
83
- }));
84
- return properties.length ? { properties } : void 0;
85
- }
86
- function convertV1Entries(rawEntries, fileKey) {
87
- return rawEntries.filter((e) => {
88
- if (!e || typeof e !== "object") return false;
89
- const o = e;
90
- return o.linked === true && !!o.binding;
91
- }).map((e) => {
92
- const b = e.binding;
93
- const sb = b.storybook ?? {};
94
- const nodeId = String(e.nodeId ?? "");
95
- const figmaUrl = typeof sb.figmaUrl === "string" && sb.figmaUrl.trim() ? sb.figmaUrl.trim() : fileKey ? `https://www.figma.com/design/${fileKey}?node-id=${nodeId.replace(":", "-")}` : void 0;
96
- const statusRaw = typeof sb.status === "string" ? sb.status : void 0;
97
- const readiness = statusRaw === "complete" || statusRaw === "ready-for-dev" || statusRaw === "in-progress" || statusRaw === "backlog" ? statusRaw : void 0;
98
- const files = parseFiles(Array.isArray(b.files) ? b.files : []);
99
- const componentApi = parseComponentApi(sb.componentApi);
100
- return {
101
- nodeId,
102
- displayName: String(b.displayName ?? e.nodeName ?? ""),
103
- description: sb.description ? String(sb.description) : void 0,
104
- figmaUrl,
105
- designEmbed: sb.designEmbed === true,
106
- compositionJson: sb.compositionJson === true,
107
- metadata: sb.metadata === true,
108
- updatedAt: typeof e.updatedAt === "string" ? e.updatedAt : void 0,
109
- importPath: typeof b.importPath === "string" ? b.importPath : void 0,
110
- snippetTemplate: typeof b.snippetTemplate === "string" ? b.snippetTemplate : void 0,
111
- files: files.length > 0 ? files : void 0,
112
- compositionFilePath: typeof sb.compositionFilePath === "string" ? sb.compositionFilePath : void 0,
113
- compositionSnapshot: typeof sb.compositionSnapshot === "string" ? sb.compositionSnapshot : void 0,
114
- compositionName: typeof sb.compositionName === "string" ? sb.compositionName : void 0,
115
- componentApi,
116
- meta: {
117
- tokensBool: tokensBoolFromStorybook(sb),
118
- ...readiness ? { readiness } : {}
119
- }
120
- };
121
- });
122
- }
123
- function parseFiles(raw) {
124
- return raw.filter((f) => !!f && typeof f === "object").map((f) => ({
125
- framework: String(f.framework ?? ""),
126
- filePath: String(f.filePath ?? ""),
127
- ...f.componentName ? { componentName: String(f.componentName) } : {},
128
- ...f.importPath ? { importPath: String(f.importPath) } : {}
129
- }));
130
- }
131
-
132
15
  // src/compositionLoad.ts
133
16
  function normalizeCompositionPath(p) {
134
17
  return p.trim().replace(/\\/g, "/").replace(/^\/+/, "");
@@ -278,28 +161,304 @@ async function loadCompositionJsonText(path, legacyInline, params) {
278
161
  };
279
162
  }
280
163
  }
281
- const sxlPresetUrls = candidateRelativePaths(p).map((rel) => joinFetchUrl(SXL_TOKENS_URL_PREFIX, rel));
282
- const sxlPresetHit = await fetchFirstOk(sxlPresetUrls);
283
- if (sxlPresetHit) {
284
- return { text: sxlPresetHit.text, source: "fetch" };
164
+ const sxlPresetUrls = candidateRelativePaths(p).map((rel) => joinFetchUrl(SXL_TOKENS_URL_PREFIX, rel));
165
+ const sxlPresetHit = await fetchFirstOk(sxlPresetUrls);
166
+ if (sxlPresetHit) {
167
+ return { text: sxlPresetHit.text, source: "fetch" };
168
+ }
169
+ const repoUrl = extractRepositoryUrl(params);
170
+ if (repoUrl) {
171
+ const gitlabCandidates = buildGitlabRawCandidates(repoUrl, p);
172
+ const hit = await fetchFirstOk(gitlabCandidates);
173
+ if (hit) {
174
+ return { text: hit.text, source: "fetch" };
175
+ }
176
+ }
177
+ const localCandidates = candidateRelativePaths(p).map((rel) => `/${rel}`);
178
+ const localHit = await fetchFirstOk(localCandidates);
179
+ if (localHit) {
180
+ return { text: localHit.text, source: "fetch" };
181
+ }
182
+ return {
183
+ error: "Composition file is not inlined and auto-fetch failed. Use compositionSources, compositionFetchBaseUrl + staticDirs, resolveComposition(), or compositionDevProxyPrefix (same-origin proxy; private Git hosts usually block CORS) \u2014 see @sxl-studio/storybook-addon README."
184
+ };
185
+ }
186
+
187
+ // src/convert.ts
188
+ function fromDiffCodeConnect(data) {
189
+ if (!data || typeof data !== "object") {
190
+ return { version: 1, figmaFileKey: "", entries: [] };
191
+ }
192
+ const d = data;
193
+ const fileKey = typeof d.$figmaFileKey === "string" ? d.$figmaFileKey : "";
194
+ const fileName = typeof d.$figmaFileName === "string" ? d.$figmaFileName : void 0;
195
+ const repository = d.repository && typeof d.repository === "object" ? d.repository : void 0;
196
+ const isV2 = Array.isArray(d.components);
197
+ const entries = isV2 ? convertV2Components(d.components, fileKey) : convertV1Entries(Array.isArray(d.entries) ? d.entries : [], fileKey);
198
+ return {
199
+ version: 1,
200
+ figmaFileKey: fileKey,
201
+ figmaFileName: fileName,
202
+ repository: repository ? {
203
+ ...typeof repository.url === "string" ? { url: repository.url } : {},
204
+ ...typeof repository.documentationUrl === "string" ? { documentationUrl: repository.documentationUrl } : {}
205
+ } : void 0,
206
+ entries
207
+ };
208
+ }
209
+ function tokensBoolFromStorybook(sb) {
210
+ if (sb.tokensReady === true) return "true";
211
+ return "false";
212
+ }
213
+ function convertV2Components(components, fileKey) {
214
+ return components.filter((c) => c.linked === true).map((c) => {
215
+ const nodeId = String(c.nodeId ?? "");
216
+ const sb = c.storybook ?? {};
217
+ const cc = c.codeConnect ?? {};
218
+ const figmaUrl = typeof sb.figmaUrl === "string" && sb.figmaUrl.trim() ? sb.figmaUrl.trim() : fileKey ? `https://www.figma.com/design/${fileKey}?node-id=${nodeId.replace(":", "-")}` : void 0;
219
+ const statusRaw = typeof sb.status === "string" ? sb.status : void 0;
220
+ const readiness = statusRaw === "complete" || statusRaw === "ready-for-dev" || statusRaw === "in-progress" || statusRaw === "backlog" ? statusRaw : void 0;
221
+ const files = parseFiles(Array.isArray(cc.files) ? cc.files : []);
222
+ const componentApi = parseComponentApi(sb.componentApi);
223
+ return {
224
+ nodeId,
225
+ displayName: String(c.displayName ?? c.name ?? ""),
226
+ description: sb.description ? String(sb.description) : void 0,
227
+ figmaUrl,
228
+ designEmbed: sb.designEmbed === true,
229
+ compositionJson: sb.compositionJson === true,
230
+ metadata: sb.metadata === true,
231
+ updatedAt: typeof c.updatedAt === "string" ? c.updatedAt : void 0,
232
+ importPath: typeof cc.importPath === "string" ? cc.importPath : void 0,
233
+ snippetTemplate: typeof cc.snippetTemplate === "string" ? cc.snippetTemplate : void 0,
234
+ files: files.length > 0 ? files : void 0,
235
+ compositionFilePath: typeof sb.compositionFilePath === "string" ? sb.compositionFilePath : void 0,
236
+ compositionSnapshot: typeof sb.compositionSnapshot === "string" ? sb.compositionSnapshot : void 0,
237
+ compositionName: typeof sb.compositionName === "string" ? sb.compositionName : void 0,
238
+ componentApi,
239
+ meta: {
240
+ tokensBool: tokensBoolFromStorybook(sb),
241
+ ...readiness ? { readiness } : {}
242
+ }
243
+ };
244
+ });
245
+ }
246
+ function parseComponentApi(raw) {
247
+ if (!raw || typeof raw !== "object") return void 0;
248
+ const o = raw;
249
+ if (!Array.isArray(o.properties)) return void 0;
250
+ const properties = o.properties.filter((p) => !!p && typeof p === "object").map((p) => ({
251
+ name: String(p.name ?? ""),
252
+ kind: String(p.kind ?? ""),
253
+ ...p.defaultValue !== void 0 ? { defaultValue: p.defaultValue } : {},
254
+ ...Array.isArray(p.options) ? { options: p.options.map(String) } : {}
255
+ }));
256
+ return properties.length ? { properties } : void 0;
257
+ }
258
+ function convertV1Entries(rawEntries, fileKey) {
259
+ return rawEntries.filter((e) => {
260
+ if (!e || typeof e !== "object") return false;
261
+ const o = e;
262
+ return o.linked === true && !!o.binding;
263
+ }).map((e) => {
264
+ const b = e.binding;
265
+ const sb = b.storybook ?? {};
266
+ const nodeId = String(e.nodeId ?? "");
267
+ const figmaUrl = typeof sb.figmaUrl === "string" && sb.figmaUrl.trim() ? sb.figmaUrl.trim() : fileKey ? `https://www.figma.com/design/${fileKey}?node-id=${nodeId.replace(":", "-")}` : void 0;
268
+ const statusRaw = typeof sb.status === "string" ? sb.status : void 0;
269
+ const readiness = statusRaw === "complete" || statusRaw === "ready-for-dev" || statusRaw === "in-progress" || statusRaw === "backlog" ? statusRaw : void 0;
270
+ const files = parseFiles(Array.isArray(b.files) ? b.files : []);
271
+ const componentApi = parseComponentApi(sb.componentApi);
272
+ return {
273
+ nodeId,
274
+ displayName: String(b.displayName ?? e.nodeName ?? ""),
275
+ description: sb.description ? String(sb.description) : void 0,
276
+ figmaUrl,
277
+ designEmbed: sb.designEmbed === true,
278
+ compositionJson: sb.compositionJson === true,
279
+ metadata: sb.metadata === true,
280
+ updatedAt: typeof e.updatedAt === "string" ? e.updatedAt : void 0,
281
+ importPath: typeof b.importPath === "string" ? b.importPath : void 0,
282
+ snippetTemplate: typeof b.snippetTemplate === "string" ? b.snippetTemplate : void 0,
283
+ files: files.length > 0 ? files : void 0,
284
+ compositionFilePath: typeof sb.compositionFilePath === "string" ? sb.compositionFilePath : void 0,
285
+ compositionSnapshot: typeof sb.compositionSnapshot === "string" ? sb.compositionSnapshot : void 0,
286
+ compositionName: typeof sb.compositionName === "string" ? sb.compositionName : void 0,
287
+ componentApi,
288
+ meta: {
289
+ tokensBool: tokensBoolFromStorybook(sb),
290
+ ...readiness ? { readiness } : {}
291
+ }
292
+ };
293
+ });
294
+ }
295
+ function parseFiles(raw) {
296
+ return raw.filter((f) => !!f && typeof f === "object").map((f) => ({
297
+ framework: String(f.framework ?? ""),
298
+ filePath: String(f.filePath ?? ""),
299
+ ...f.componentName ? { componentName: String(f.componentName) } : {},
300
+ ...f.importPath ? { importPath: String(f.importPath) } : {}
301
+ }));
302
+ }
303
+
304
+ // src/entryResolve.ts
305
+ function getRegistryFigmaFileKey(raw) {
306
+ if (!raw || typeof raw !== "object") return "";
307
+ const fk = raw.figmaFileKey;
308
+ return typeof fk === "string" ? fk.trim() : "";
309
+ }
310
+ function buildFigmaDesignUrlFromFileKey(fileKey, nodeId) {
311
+ if (!fileKey || !nodeId) return void 0;
312
+ return `https://www.figma.com/design/${fileKey}?node-id=${String(nodeId).replace(/:/g, "-")}`;
313
+ }
314
+ function tokenStatusToTokensBool(status) {
315
+ if (status === "assigned") return "true";
316
+ if (status === "partial" || status === "none") return "false";
317
+ return void 0;
318
+ }
319
+ function normalizeRegistry(raw) {
320
+ if (!raw || typeof raw !== "object") return null;
321
+ const obj = raw;
322
+ if (Array.isArray(obj.components)) {
323
+ return fromDiffCodeConnect(raw);
324
+ }
325
+ const entries = Array.isArray(obj.entries) ? obj.entries : [];
326
+ if (entries.length === 0) return null;
327
+ const first = entries[0];
328
+ if (first && "binding" in first && "linked" in first) {
329
+ return fromDiffCodeConnect(raw);
330
+ }
331
+ if (first && "nodeId" in first && "displayName" in first) {
332
+ return raw;
333
+ }
334
+ return null;
335
+ }
336
+ function resolveEntry(params, ctx) {
337
+ if (!params) return { status: "no-registry" };
338
+ const registry = normalizeRegistry(params.registry);
339
+ const tokensOverride = params.tokensBool ?? tokenStatusToTokensBool(params.tokenStatus);
340
+ if (params.figmaUrl || params.description) {
341
+ return {
342
+ status: "found",
343
+ entry: {
344
+ nodeId: params.figmaNodeId ?? "",
345
+ displayName: params.component ?? params.componentName ?? "",
346
+ description: params.description,
347
+ figmaUrl: params.figmaUrl,
348
+ designEmbed: params.designEmbed ?? !!params.figmaUrl,
349
+ compositionJson: params.compositionJson,
350
+ metadata: params.metadata,
351
+ meta: {
352
+ tokensBool: tokensOverride,
353
+ readiness: params.readiness,
354
+ tokenStatus: params.tokenStatus
355
+ }
356
+ }
357
+ };
358
+ }
359
+ if (!registry) return { status: "no-registry" };
360
+ let found;
361
+ if (params.figmaNodeId) {
362
+ found = registry.entries.find((e) => e.nodeId === params.figmaNodeId);
285
363
  }
286
- const repoUrl = extractRepositoryUrl(params);
287
- if (repoUrl) {
288
- const gitlabCandidates = buildGitlabRawCandidates(repoUrl, p);
289
- const hit = await fetchFirstOk(gitlabCandidates);
290
- if (hit) {
291
- return { text: hit.text, source: "fetch" };
364
+ if (!found) {
365
+ const name = (params.component ?? params.componentName ?? "").toLowerCase();
366
+ if (name) {
367
+ found = registry.entries.find((e) => e.displayName.toLowerCase() === name);
292
368
  }
293
369
  }
294
- const localCandidates = candidateRelativePaths(p).map((rel) => `/${rel}`);
295
- const localHit = await fetchFirstOk(localCandidates);
296
- if (localHit) {
297
- return { text: localHit.text, source: "fetch" };
370
+ if (!found) {
371
+ found = matchByStoryContext(registry.entries, ctx);
298
372
  }
373
+ if (!found) {
374
+ const hint = extractComponentName(ctx.title) || ctx.name || "";
375
+ return { status: "no-match", componentHint: hint };
376
+ }
377
+ const rootFileKey = getRegistryFigmaFileKey(params.registry);
378
+ const figmaUrlResolved = found.figmaUrl ?? buildFigmaDesignUrlFromFileKey(rootFileKey, found.nodeId);
299
379
  return {
300
- error: "Composition file is not inlined and auto-fetch failed. Use compositionSources, compositionFetchBaseUrl + staticDirs, resolveComposition(), or compositionDevProxyPrefix (same-origin proxy; private Git hosts usually block CORS) \u2014 see @sxl-studio/storybook-addon README."
380
+ status: "found",
381
+ entry: {
382
+ ...found,
383
+ figmaUrl: figmaUrlResolved,
384
+ meta: {
385
+ ...found.meta,
386
+ tokensBool: tokensOverride ?? found.meta?.tokensBool ?? tokenStatusToTokensBool(found.meta?.tokenStatus),
387
+ tokenStatus: params.tokenStatus ?? found.meta?.tokenStatus,
388
+ readiness: params.readiness ?? found.meta?.readiness
389
+ },
390
+ description: params.description ?? found.description,
391
+ compositionJson: params.compositionJson ?? found.compositionJson,
392
+ metadata: params.metadata ?? found.metadata,
393
+ designEmbed: params.designEmbed ?? found.designEmbed,
394
+ compositionFilePath: found.compositionFilePath,
395
+ compositionSnapshot: found.compositionSnapshot,
396
+ componentApi: found.componentApi
397
+ }
301
398
  };
302
399
  }
400
+ function resolveTokensBool(entry, params) {
401
+ const explicit = params?.tokensBool ?? tokenStatusToTokensBool(params?.tokenStatus);
402
+ if (explicit) return explicit;
403
+ const fromEntry = entry.meta?.tokensBool ?? tokenStatusToTokensBool(entry.meta?.tokenStatus);
404
+ if (fromEntry) return fromEntry;
405
+ return "false";
406
+ }
407
+ function matchByStoryContext(entries, ctx) {
408
+ const tokens = [
409
+ extractComponentName(ctx.title),
410
+ ctx.name,
411
+ ctx.storyId,
412
+ ctx.importPath,
413
+ fileStem(ctx.importPath)
414
+ ].filter(Boolean).map(norm);
415
+ if (tokens.length === 0) return void 0;
416
+ let best;
417
+ let bestScore = 0;
418
+ let tie = false;
419
+ for (const entry of entries) {
420
+ const sc = score(entry, tokens);
421
+ if (sc > bestScore) {
422
+ bestScore = sc;
423
+ best = entry;
424
+ tie = false;
425
+ } else if (sc === bestScore && sc > 0) {
426
+ tie = true;
427
+ }
428
+ }
429
+ if (tie || bestScore < 60) return void 0;
430
+ return best;
431
+ }
432
+ function score(entry, tokens) {
433
+ const display = norm(entry.displayName);
434
+ const imprt = norm(entry.importPath ?? "");
435
+ const stems = (entry.files ?? []).map((f) => norm(fileStem(f.filePath))).filter(Boolean);
436
+ let s = 0;
437
+ for (const t of tokens) {
438
+ if (!t) continue;
439
+ if (display && t === display) s += 120;
440
+ else if (display && t.includes(display)) s += 70;
441
+ else if (display && display.includes(t)) s += 65;
442
+ if (imprt && t.includes(imprt)) s += 55;
443
+ for (const stem of stems) {
444
+ if (stem && t.includes(stem)) s += 30;
445
+ }
446
+ }
447
+ return s;
448
+ }
449
+ function extractComponentName(title) {
450
+ if (!title) return "";
451
+ const parts = title.split("/");
452
+ return parts[parts.length - 1].trim();
453
+ }
454
+ function norm(v) {
455
+ return v.toLowerCase().replace(/[^a-z0-9]+/g, "");
456
+ }
457
+ function fileStem(p) {
458
+ const f = (p || "").split("/").pop() ?? "";
459
+ const d = f.lastIndexOf(".");
460
+ return d > 0 ? f.slice(0, d) : f;
461
+ }
303
462
 
304
463
  // src/components/JsonHighlighted.tsx
305
464
  import React from "react";
@@ -3642,154 +3801,12 @@ var JsonHighlighted = ({ code }) => {
3642
3801
  };
3643
3802
 
3644
3803
  // src/components/SxlPanel.tsx
3645
- function getRegistryFigmaFileKey(raw) {
3646
- if (!raw || typeof raw !== "object") return "";
3647
- const fk = raw.figmaFileKey;
3648
- return typeof fk === "string" ? fk.trim() : "";
3649
- }
3650
- function buildFigmaDesignUrlFromFileKey(fileKey, nodeId) {
3651
- if (!fileKey || !nodeId) return void 0;
3652
- return `https://www.figma.com/design/${fileKey}?node-id=${String(nodeId).replace(/:/g, "-")}`;
3653
- }
3654
3804
  var READINESS_BADGE = {
3655
3805
  backlog: { label: "Backlog", bg: "#111827", fg: "#f9fafb" },
3656
3806
  "in-progress": { label: "In Progress", bg: "#ea580c", fg: "#ffffff" },
3657
3807
  "ready-for-dev": { label: "Ready for Dev", bg: "#16a34a", fg: "#ffffff" },
3658
3808
  complete: { label: "Completed", bg: "#9ca3af", fg: "#111827" }
3659
3809
  };
3660
- function normalizeRegistry(raw) {
3661
- if (!raw || typeof raw !== "object") return null;
3662
- const obj = raw;
3663
- if (Array.isArray(obj.components)) {
3664
- return fromDiffCodeConnect(raw);
3665
- }
3666
- const entries = Array.isArray(obj.entries) ? obj.entries : [];
3667
- if (entries.length === 0) return null;
3668
- const first = entries[0];
3669
- if (first && "binding" in first && "linked" in first) {
3670
- return fromDiffCodeConnect(raw);
3671
- }
3672
- if (first && "nodeId" in first && "displayName" in first) {
3673
- return raw;
3674
- }
3675
- return null;
3676
- }
3677
- function resolveEntry(params, ctx) {
3678
- if (!params) return { status: "no-registry" };
3679
- const registry = normalizeRegistry(params.registry);
3680
- if (params.figmaUrl || params.description) {
3681
- return {
3682
- status: "found",
3683
- entry: {
3684
- nodeId: params.figmaNodeId ?? "",
3685
- displayName: params.component ?? params.componentName ?? "",
3686
- description: params.description,
3687
- figmaUrl: params.figmaUrl,
3688
- designEmbed: params.designEmbed ?? !!params.figmaUrl,
3689
- compositionJson: params.compositionJson,
3690
- metadata: params.metadata,
3691
- meta: {
3692
- tokensBool: params.tokensBool,
3693
- readiness: params.readiness
3694
- }
3695
- }
3696
- };
3697
- }
3698
- if (!registry) return { status: "no-registry" };
3699
- let found;
3700
- if (params.figmaNodeId) {
3701
- found = registry.entries.find((e) => e.nodeId === params.figmaNodeId);
3702
- }
3703
- if (!found) {
3704
- const name = (params.component ?? params.componentName ?? "").toLowerCase();
3705
- if (name) {
3706
- found = registry.entries.find((e) => e.displayName.toLowerCase() === name);
3707
- }
3708
- }
3709
- if (!found) {
3710
- found = matchByStoryContext(registry.entries, ctx);
3711
- }
3712
- if (!found) {
3713
- const hint = extractComponentName(ctx.title) || ctx.name || "";
3714
- return { status: "no-match", componentHint: hint };
3715
- }
3716
- const rootFileKey = getRegistryFigmaFileKey(params.registry);
3717
- const figmaUrlResolved = found.figmaUrl ?? buildFigmaDesignUrlFromFileKey(rootFileKey, found.nodeId);
3718
- return {
3719
- status: "found",
3720
- entry: {
3721
- ...found,
3722
- figmaUrl: figmaUrlResolved,
3723
- meta: {
3724
- ...found.meta,
3725
- tokensBool: params.tokensBool ?? found.meta?.tokensBool,
3726
- readiness: params.readiness ?? found.meta?.readiness
3727
- },
3728
- description: params.description ?? found.description,
3729
- compositionJson: params.compositionJson ?? found.compositionJson,
3730
- metadata: params.metadata ?? found.metadata,
3731
- designEmbed: params.designEmbed ?? found.designEmbed,
3732
- compositionFilePath: found.compositionFilePath,
3733
- compositionSnapshot: found.compositionSnapshot,
3734
- componentApi: found.componentApi
3735
- }
3736
- };
3737
- }
3738
- function matchByStoryContext(entries, ctx) {
3739
- const tokens = [
3740
- extractComponentName(ctx.title),
3741
- ctx.name,
3742
- ctx.storyId,
3743
- ctx.importPath,
3744
- fileStem(ctx.importPath)
3745
- ].filter(Boolean).map(norm);
3746
- if (tokens.length === 0) return void 0;
3747
- let best;
3748
- let bestScore = 0;
3749
- let tie = false;
3750
- for (const entry of entries) {
3751
- const sc = score(entry, tokens);
3752
- if (sc > bestScore) {
3753
- bestScore = sc;
3754
- best = entry;
3755
- tie = false;
3756
- } else if (sc === bestScore && sc > 0) {
3757
- tie = true;
3758
- }
3759
- }
3760
- if (tie || bestScore < 60) return void 0;
3761
- return best;
3762
- }
3763
- function score(entry, tokens) {
3764
- const display = norm(entry.displayName);
3765
- const imprt = norm(entry.importPath ?? "");
3766
- const stems = (entry.files ?? []).map((f) => norm(fileStem(f.filePath))).filter(Boolean);
3767
- let s = 0;
3768
- for (const t of tokens) {
3769
- if (!t) continue;
3770
- if (display && t === display) s += 120;
3771
- else if (display && t.includes(display)) s += 70;
3772
- else if (display && display.includes(t)) s += 65;
3773
- if (imprt && t.includes(imprt)) s += 55;
3774
- for (const stem of stems) {
3775
- if (stem && t.includes(stem)) s += 30;
3776
- }
3777
- }
3778
- return s;
3779
- }
3780
- function extractComponentName(title) {
3781
- if (!title) return "";
3782
- const parts = title.split("/");
3783
- return parts[parts.length - 1].trim();
3784
- }
3785
- function norm(v) {
3786
- return v.toLowerCase().replace(/[^a-z0-9]+/g, "");
3787
- }
3788
- function fileStem(p) {
3789
- const f = (p || "").split("/").pop() ?? "";
3790
- const d = f.lastIndexOf(".");
3791
- return d > 0 ? f.slice(0, d) : f;
3792
- }
3793
3810
  function buildEmbedUrl(figmaUrl) {
3794
3811
  if (figmaUrl.includes("figma.com/embed")) return figmaUrl;
3795
3812
  return `https://www.figma.com/embed?embed_host=share&url=${encodeURIComponent(figmaUrl)}`;
@@ -3818,12 +3835,6 @@ function formatDate(iso) {
3818
3835
  return iso;
3819
3836
  }
3820
3837
  }
3821
- function resolveTokensBool(entry, params) {
3822
- const p = params?.tokensBool ?? entry.meta?.tokensBool;
3823
- if (p === "true" || p === "false") return p;
3824
- if (entry.meta?.tokenStatus === "assigned") return "true";
3825
- return "false";
3826
- }
3827
3838
  function formatJsonSnapshot(raw) {
3828
3839
  if (!raw) return "";
3829
3840
  try {
@@ -3832,6 +3843,10 @@ function formatJsonSnapshot(raw) {
3832
3843
  return raw;
3833
3844
  }
3834
3845
  }
3846
+ function logFigmaEmbedDebug(enabled, event, payload) {
3847
+ if (!enabled) return;
3848
+ console.debug("[SXL Studio addon] Figma embed", { event, ...payload });
3849
+ }
3835
3850
  var TokensBadge = ({ value }) => {
3836
3851
  const on = value === "true";
3837
3852
  return /* @__PURE__ */ React2.createElement(
@@ -3945,6 +3960,8 @@ var SxlPanel = () => {
3945
3960
  const entryForRender = result.status === "found" ? result.entry : null;
3946
3961
  const showEmbed = !!entryForRender?.figmaUrl && entryForRender.designEmbed !== false;
3947
3962
  const renderEmbedSection = entryForRender?.designEmbed === true || !!entryForRender?.figmaUrl;
3963
+ const debugFigmaEmbed = params?.debugFigmaEmbed === true;
3964
+ const embedUrl = showEmbed && entryForRender?.figmaUrl ? buildEmbedUrl(entryForRender.figmaUrl) : void 0;
3948
3965
  useEffect(() => {
3949
3966
  if (!entryForRender) return;
3950
3967
  if (entryForRender.designEmbed === true && !entryForRender.figmaUrl) {
@@ -3953,7 +3970,23 @@ var SxlPanel = () => {
3953
3970
  { nodeId: entryForRender.nodeId, displayName: entryForRender.displayName }
3954
3971
  );
3955
3972
  }
3956
- }, [entryForRender?.designEmbed, entryForRender?.figmaUrl, entryForRender?.nodeId, entryForRender?.displayName]);
3973
+ logFigmaEmbedDebug(debugFigmaEmbed, "resolved", {
3974
+ nodeId: entryForRender.nodeId,
3975
+ displayName: entryForRender.displayName,
3976
+ designEmbed: entryForRender.designEmbed ?? null,
3977
+ figmaUrl: entryForRender.figmaUrl ?? null,
3978
+ embedUrl: embedUrl ?? null,
3979
+ storyId: ctx.storyId || null
3980
+ });
3981
+ }, [
3982
+ debugFigmaEmbed,
3983
+ embedUrl,
3984
+ ctx.storyId,
3985
+ entryForRender?.designEmbed,
3986
+ entryForRender?.figmaUrl,
3987
+ entryForRender?.nodeId,
3988
+ entryForRender?.displayName
3989
+ ]);
3957
3990
  if (result.status === "no-registry") {
3958
3991
  return React2.createElement(NoRegistryState, null);
3959
3992
  }
@@ -3971,16 +4004,28 @@ var SxlPanel = () => {
3971
4004
  "iframe",
3972
4005
  {
3973
4006
  title: "Figma embed",
3974
- src: buildEmbedUrl(entry.figmaUrl),
4007
+ src: embedUrl,
3975
4008
  style: iframe,
3976
4009
  allowFullScreen: true,
3977
4010
  allow: "clipboard-write; fullscreen",
3978
- referrerPolicy: "no-referrer-when-downgrade"
4011
+ referrerPolicy: "no-referrer-when-downgrade",
4012
+ onLoad: () => {
4013
+ logFigmaEmbedDebug(debugFigmaEmbed, "iframe-load", {
4014
+ nodeId: entry.nodeId,
4015
+ embedUrl: embedUrl ?? null
4016
+ });
4017
+ },
4018
+ onError: () => {
4019
+ logFigmaEmbedDebug(debugFigmaEmbed, "iframe-error", {
4020
+ nodeId: entry.nodeId,
4021
+ embedUrl: embedUrl ?? null
4022
+ });
4023
+ }
3979
4024
  }
3980
4025
  ), /* @__PURE__ */ React2.createElement("div", { style: embedFooter }, /* @__PURE__ */ React2.createElement(
3981
4026
  "a",
3982
4027
  {
3983
- href: buildEmbedUrl(entry.figmaUrl),
4028
+ href: embedUrl,
3984
4029
  target: "_blank",
3985
4030
  rel: "noopener noreferrer",
3986
4031
  style: { ...link, marginRight: "12px" }
package/dist/preset.js CHANGED
@@ -166,9 +166,16 @@ function staticDirs(entries, options) {
166
166
  return [...e, { from: root, to: `/${mount}` }];
167
167
  }
168
168
  function mergeSxlFigmaFrameSrcHeader(config) {
169
- const directive = "frame-src 'self' https://www.figma.com https://*.figma.com data: blob:;";
169
+ const requiredSources = [
170
+ "'self'",
171
+ "https://www.figma.com",
172
+ "https://*.figma.com",
173
+ "data:",
174
+ "blob:"
175
+ ];
176
+ const directive = `frame-src ${requiredSources.join(" ")}`;
170
177
  const prev = config.server?.headers?.["Content-Security-Policy"];
171
- const merged = typeof prev === "string" && prev.trim() ? `${prev.trim()}; ${directive}` : directive;
178
+ const merged = typeof prev === "string" && prev.trim() ? mergeCspFrameSrcDirective(prev, requiredSources) : directive;
172
179
  return {
173
180
  ...config,
174
181
  server: {
@@ -180,6 +187,32 @@ function mergeSxlFigmaFrameSrcHeader(config) {
180
187
  }
181
188
  };
182
189
  }
190
+ function mergeCspFrameSrcDirective(policy, requiredSources) {
191
+ const directives = policy.split(";").map((d) => d.trim()).filter(Boolean);
192
+ let hasFrameSrc = false;
193
+ const mergedDirectives = directives.map((directive) => {
194
+ const parts = directive.split(/\s+/).filter(Boolean);
195
+ if (parts.length === 0) return directive;
196
+ const name = parts[0].toLowerCase();
197
+ if (name !== "frame-src") return directive;
198
+ hasFrameSrc = true;
199
+ const existing = parts.slice(1);
200
+ const next = [...existing];
201
+ const seen = new Set(existing.map((v) => v.toLowerCase()));
202
+ for (const src of requiredSources) {
203
+ const key = src.toLowerCase();
204
+ if (!seen.has(key)) {
205
+ seen.add(key);
206
+ next.push(src);
207
+ }
208
+ }
209
+ return `frame-src ${next.join(" ")}`;
210
+ });
211
+ if (!hasFrameSrc) {
212
+ mergedDirectives.push(`frame-src ${requiredSources.join(" ")}`);
213
+ }
214
+ return mergedDirectives.join("; ");
215
+ }
183
216
  export {
184
217
  managerEntries,
185
218
  mergeSxlFigmaFrameSrcHeader,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sxl-studio/storybook-addon",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
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",
@@ -25,7 +25,9 @@
25
25
  "scripts": {
26
26
  "build": "tsup",
27
27
  "dev": "tsup --watch",
28
- "lint": "tsc --noEmit",
28
+ "lint": "eslint \"src/**/*.{ts,tsx}\" --max-warnings=0",
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "node --import tsx --test src/**/*.test.ts",
29
31
  "prepublishOnly": "npm run build",
30
32
  "push": "npm run build && npm publish"
31
33
  },
@@ -44,14 +46,19 @@
44
46
  "storybook": "^8.0.0 || ^9.0.0 || ^10.0.0"
45
47
  },
46
48
  "devDependencies": {
49
+ "@eslint/js": "^9.27.0",
47
50
  "@storybook/components": "^8.6.14",
48
51
  "@storybook/manager-api": "^8.6.14",
49
52
  "@types/node": "^22.19.17",
50
53
  "@types/react": "^18.0.0",
54
+ "eslint": "^9.27.0",
55
+ "globals": "^15.15.0",
51
56
  "react": "^18.0.0",
52
57
  "storybook": "^8.0.0",
58
+ "tsx": "^4.19.2",
53
59
  "tsup": "^8.5.1",
54
- "typescript": "^5.7.0"
60
+ "typescript": "^5.7.0",
61
+ "typescript-eslint": "^8.33.1"
55
62
  },
56
63
  "storybook": {
57
64
  "preset": "./preset.js",