@sxl-studio/storybook-addon 1.1.0 → 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,16 @@
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
+
10
+ ## 1.1.1
11
+
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.
13
+
3
14
  ## 1.1.0
4
15
 
5
16
  - **Preset (zero-config):** merges Content-Security-Policy so the Figma embed iframe can load (`frame-src` includes `https://www.figma.com`).
package/README.md CHANGED
@@ -4,7 +4,7 @@ Storybook addon for [SXL Studio](https://sxl-studio.com) — displays Figma Embe
4
4
 
5
5
  ## Changelog
6
6
 
7
- See [CHANGELOG.md](./CHANGELOG.md) (e.g. **1.1.0**: preset CSP, `/sxl-tokens` for local composition, optional legacy registry filename alias, quieter logs).
7
+ See [CHANGELOG.md](./CHANGELOG.md) (e.g. **1.1.1** registry matching fix; **1.1.0** preset CSP, `/sxl-tokens`, legacy filename alias, quieter logs).
8
8
 
9
9
  ## Features
10
10
 
@@ -21,9 +21,11 @@ npm install @sxl-studio/storybook-addon --save-dev
21
21
 
22
22
  ## Setup
23
23
 
24
- ### 1. Register the addon
24
+ ### 1. Register the addon (`main`)
25
25
 
26
- `.storybook/main.ts`:
26
+ List **`@sxl-studio/storybook-addon`** in **`addons`**. The preset ships with the package and is applied when Storybook loads the addon (no separate preset import in `main` unless your setup requires the explicit `@sxl-studio/storybook-addon/preset` entry — see troubleshooting in docs).
27
+
28
+ **Minimal `main.ts`:**
27
29
 
28
30
  ```ts
29
31
  export default {
@@ -34,21 +36,35 @@ export default {
34
36
  };
35
37
  ```
36
38
 
37
- ### 2. Import the registry
39
+ **With a shared Storybook config** (internal design-system package, monorepo): **append** to the shared addons array — do not drop existing entries.
40
+
41
+ ```ts
42
+ import sharedMain from '@your-org/storybook-vue/main';
43
+
44
+ const config = {
45
+ ...sharedMain,
46
+ addons: [...(sharedMain.addons ?? []), '@sxl-studio/storybook-addon'],
47
+ };
48
+
49
+ export default config;
50
+ ```
51
+
52
+ ### 2. Import the registry (`preview`)
38
53
 
39
- The SXL Studio Figma plugin generates a `sxl-codeconnect.json` (or `diff-code-connect.<fileKey>.json`) file that maps Figma components to your codebase. Import it in your Storybook preview config.
54
+ The SXL Studio Figma plugin generates **`diff-code-connect.<figmaFileKey>.json`** (or an exported `sxl-codeconnect.json`). Put it in **`parameters.sxl.registry`**. The **relative import path is up to your repo** (e.g. `../../tokens/tokens/diff-code-connect.xxx.json`).
40
55
 
41
- **Preset (automatic):** when the addon preset is loaded (default if the package is listed under `addons`), it:
56
+ - **`fromDiffCodeConnect(raw)`** explicit normalization; use if you prefer one code path.
57
+ - **Raw `import registry from '...json'`** — often enough for current plugin output.
42
58
 
43
- - Serves the sibling folder `tokens/tokens` (if present next to the Storybook app or two levels above `.storybook`) at **`/sxl-tokens/…`**, so composition JSON paths from the registry resolve in dev and via `staticDirs` in `storybook build`.
44
- - If `diff-code-connect.SXL-Components.json` is **missing** but another `diff-code-connect.*.json` exists in that folder, Vite gets a **`resolve.alias`** from the legacy path to the first matching file (sorted by name). You can keep a stable import path in preview while the plugin emits `diff-code-connect.<figmaFileKey>.json`.
59
+ **Preset (automatic):** when the addon preset runs, it:
45
60
 
46
- **Option A** using `sxl-codeconnect.json` (exported from plugin):
61
+ - Serves a nearby **`tokens/tokens`** folder at **`/sxl-tokens/…`** (dev + `storybook build` via `staticDirs`) so `compositionFilePath` in the registry resolves without extra Vite config.
62
+ - Can **`resolve.alias`** a stable filename like `diff-code-connect.SXL-Components.json` to the real `diff-code-connect.*.json` when only one candidate exists in that folder (see CHANGELOG).
47
63
 
48
- `.storybook/preview.ts`:
64
+ **Minimal `preview.ts`:**
49
65
 
50
66
  ```ts
51
- import registry from '../sxl-codeconnect.json';
67
+ import registry from '../path/to/diff-code-connect.<fileKey>.json';
52
68
 
53
69
  export default {
54
70
  parameters: {
@@ -57,10 +73,25 @@ export default {
57
73
  };
58
74
  ```
59
75
 
60
- **Option B**using the existing `diff-code-connect` file:
76
+ **With a shared `preview`** merge **`parameters`** so shared decorators/globals stay intact:
61
77
 
62
78
  ```ts
63
- import raw from '../diff-code-connect.abc123.json';
79
+ import sharedPreview from '@your-org/storybook-vue/preview';
80
+ import registry from '../../tokens/tokens/diff-code-connect.<fileKey>.json';
81
+
82
+ export default {
83
+ ...sharedPreview,
84
+ parameters: {
85
+ ...sharedPreview.parameters,
86
+ sxl: { registry },
87
+ },
88
+ };
89
+ ```
90
+
91
+ **Alternative — converter:**
92
+
93
+ ```ts
94
+ import raw from '../diff-code-connect.<fileKey>.json';
64
95
  import { fromDiffCodeConnect } from '@sxl-studio/storybook-addon';
65
96
 
66
97
  export default {
@@ -72,6 +103,8 @@ export default {
72
103
 
73
104
  ### 3. Match stories to Figma components
74
105
 
106
+ Stories are matched to registry entries by explicit `sxl.component` / `sxl.figmaNodeId`, or by a **heuristic** on story title and file path. **A registry with a single component is not shown on every story** — unrelated stories show a “no integration” state until the story context matches or you set parameters manually.
107
+
75
108
  Per-story matching:
76
109
 
77
110
  ```ts
@@ -142,13 +175,14 @@ export const Default = {
142
175
  | `sxl.figmaNodeId` | `string` | Match entry by Figma node ID |
143
176
  | `sxl.figmaUrl` | `string` | Direct Figma URL (no registry needed) |
144
177
  | `sxl.description` | `string` | Override description |
145
- | `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` |
146
180
  | `sxl.readiness` | `"complete" \| "ready-for-dev" \| "in-progress" \| "backlog"` | Override readiness |
147
181
  | `sxl.compositionSources` | `Record<string, string>` | Map repo-relative composition paths → raw JSON (recommended) |
148
182
  | `sxl.compositionFetchBaseUrl` | `string` | Base URL to `fetch()` composition by path (e.g. static dir) |
149
183
  | `sxl.compositionDevProxyPrefix` | `string` | Same-origin prefix (e.g. Vite proxy) so fetches avoid CORS to private Git |
150
184
  | `sxl.resolveComposition` | `(path) => Promise<string \| undefined>` | Custom loader for composition file content |
151
- | `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 |
152
186
 
153
187
  ## Composition JSON (repo file, not inlined in diff)
154
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,157 +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 && registry.entries.length === 1) {
3710
- found = registry.entries[0];
3711
- }
3712
- if (!found && registry.entries.length > 1) {
3713
- found = matchByStoryContext(registry.entries, ctx);
3714
- }
3715
- if (!found) {
3716
- const hint = extractComponentName(ctx.title) || ctx.name || "";
3717
- return { status: "no-match", componentHint: hint };
3718
- }
3719
- const rootFileKey = getRegistryFigmaFileKey(params.registry);
3720
- const figmaUrlResolved = found.figmaUrl ?? buildFigmaDesignUrlFromFileKey(rootFileKey, found.nodeId);
3721
- return {
3722
- status: "found",
3723
- entry: {
3724
- ...found,
3725
- figmaUrl: figmaUrlResolved,
3726
- meta: {
3727
- ...found.meta,
3728
- tokensBool: params.tokensBool ?? found.meta?.tokensBool,
3729
- readiness: params.readiness ?? found.meta?.readiness
3730
- },
3731
- description: params.description ?? found.description,
3732
- compositionJson: params.compositionJson ?? found.compositionJson,
3733
- metadata: params.metadata ?? found.metadata,
3734
- designEmbed: params.designEmbed ?? found.designEmbed,
3735
- compositionFilePath: found.compositionFilePath,
3736
- compositionSnapshot: found.compositionSnapshot,
3737
- componentApi: found.componentApi
3738
- }
3739
- };
3740
- }
3741
- function matchByStoryContext(entries, ctx) {
3742
- const tokens = [
3743
- extractComponentName(ctx.title),
3744
- ctx.name,
3745
- ctx.storyId,
3746
- ctx.importPath,
3747
- fileStem(ctx.importPath)
3748
- ].filter(Boolean).map(norm);
3749
- if (tokens.length === 0) return void 0;
3750
- let best;
3751
- let bestScore = 0;
3752
- let tie = false;
3753
- for (const entry of entries) {
3754
- const sc = score(entry, tokens);
3755
- if (sc > bestScore) {
3756
- bestScore = sc;
3757
- best = entry;
3758
- tie = false;
3759
- } else if (sc === bestScore && sc > 0) {
3760
- tie = true;
3761
- }
3762
- }
3763
- if (tie || bestScore < 60) return void 0;
3764
- return best;
3765
- }
3766
- function score(entry, tokens) {
3767
- const display = norm(entry.displayName);
3768
- const imprt = norm(entry.importPath ?? "");
3769
- const stems = (entry.files ?? []).map((f) => norm(fileStem(f.filePath))).filter(Boolean);
3770
- let s = 0;
3771
- for (const t of tokens) {
3772
- if (!t) continue;
3773
- if (display && t === display) s += 120;
3774
- else if (display && t.includes(display)) s += 70;
3775
- else if (display && display.includes(t)) s += 65;
3776
- if (imprt && t.includes(imprt)) s += 55;
3777
- for (const stem of stems) {
3778
- if (stem && t.includes(stem)) s += 30;
3779
- }
3780
- }
3781
- return s;
3782
- }
3783
- function extractComponentName(title) {
3784
- if (!title) return "";
3785
- const parts = title.split("/");
3786
- return parts[parts.length - 1].trim();
3787
- }
3788
- function norm(v) {
3789
- return v.toLowerCase().replace(/[^a-z0-9]+/g, "");
3790
- }
3791
- function fileStem(p) {
3792
- const f = (p || "").split("/").pop() ?? "";
3793
- const d = f.lastIndexOf(".");
3794
- return d > 0 ? f.slice(0, d) : f;
3795
- }
3796
3810
  function buildEmbedUrl(figmaUrl) {
3797
3811
  if (figmaUrl.includes("figma.com/embed")) return figmaUrl;
3798
3812
  return `https://www.figma.com/embed?embed_host=share&url=${encodeURIComponent(figmaUrl)}`;
@@ -3821,12 +3835,6 @@ function formatDate(iso) {
3821
3835
  return iso;
3822
3836
  }
3823
3837
  }
3824
- function resolveTokensBool(entry, params) {
3825
- const p = params?.tokensBool ?? entry.meta?.tokensBool;
3826
- if (p === "true" || p === "false") return p;
3827
- if (entry.meta?.tokenStatus === "assigned") return "true";
3828
- return "false";
3829
- }
3830
3838
  function formatJsonSnapshot(raw) {
3831
3839
  if (!raw) return "";
3832
3840
  try {
@@ -3835,6 +3843,10 @@ function formatJsonSnapshot(raw) {
3835
3843
  return raw;
3836
3844
  }
3837
3845
  }
3846
+ function logFigmaEmbedDebug(enabled, event, payload) {
3847
+ if (!enabled) return;
3848
+ console.debug("[SXL Studio addon] Figma embed", { event, ...payload });
3849
+ }
3838
3850
  var TokensBadge = ({ value }) => {
3839
3851
  const on = value === "true";
3840
3852
  return /* @__PURE__ */ React2.createElement(
@@ -3948,6 +3960,8 @@ var SxlPanel = () => {
3948
3960
  const entryForRender = result.status === "found" ? result.entry : null;
3949
3961
  const showEmbed = !!entryForRender?.figmaUrl && entryForRender.designEmbed !== false;
3950
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;
3951
3965
  useEffect(() => {
3952
3966
  if (!entryForRender) return;
3953
3967
  if (entryForRender.designEmbed === true && !entryForRender.figmaUrl) {
@@ -3956,7 +3970,23 @@ var SxlPanel = () => {
3956
3970
  { nodeId: entryForRender.nodeId, displayName: entryForRender.displayName }
3957
3971
  );
3958
3972
  }
3959
- }, [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
+ ]);
3960
3990
  if (result.status === "no-registry") {
3961
3991
  return React2.createElement(NoRegistryState, null);
3962
3992
  }
@@ -3974,16 +4004,28 @@ var SxlPanel = () => {
3974
4004
  "iframe",
3975
4005
  {
3976
4006
  title: "Figma embed",
3977
- src: buildEmbedUrl(entry.figmaUrl),
4007
+ src: embedUrl,
3978
4008
  style: iframe,
3979
4009
  allowFullScreen: true,
3980
4010
  allow: "clipboard-write; fullscreen",
3981
- 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
+ }
3982
4024
  }
3983
4025
  ), /* @__PURE__ */ React2.createElement("div", { style: embedFooter }, /* @__PURE__ */ React2.createElement(
3984
4026
  "a",
3985
4027
  {
3986
- href: buildEmbedUrl(entry.figmaUrl),
4028
+ href: embedUrl,
3987
4029
  target: "_blank",
3988
4030
  rel: "noopener noreferrer",
3989
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.0",
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,8 +25,11 @@
25
25
  "scripts": {
26
26
  "build": "tsup",
27
27
  "dev": "tsup --watch",
28
- "lint": "tsc --noEmit",
29
- "prepublishOnly": "npm run build"
28
+ "lint": "eslint \"src/**/*.{ts,tsx}\" --max-warnings=0",
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "node --import tsx --test src/**/*.test.ts",
31
+ "prepublishOnly": "npm run build",
32
+ "push": "npm run build && npm publish"
30
33
  },
31
34
  "keywords": [
32
35
  "storybook",
@@ -43,14 +46,19 @@
43
46
  "storybook": "^8.0.0 || ^9.0.0 || ^10.0.0"
44
47
  },
45
48
  "devDependencies": {
49
+ "@eslint/js": "^9.27.0",
46
50
  "@storybook/components": "^8.6.14",
47
51
  "@storybook/manager-api": "^8.6.14",
48
52
  "@types/node": "^22.19.17",
49
53
  "@types/react": "^18.0.0",
54
+ "eslint": "^9.27.0",
55
+ "globals": "^15.15.0",
50
56
  "react": "^18.0.0",
51
57
  "storybook": "^8.0.0",
58
+ "tsx": "^4.19.2",
52
59
  "tsup": "^8.5.1",
53
- "typescript": "^5.7.0"
60
+ "typescript": "^5.7.0",
61
+ "typescript-eslint": "^8.33.1"
54
62
  },
55
63
  "storybook": {
56
64
  "preset": "./preset.js",