@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 +11 -0
- package/README.md +49 -15
- package/dist/index.d.ts +1 -1
- package/dist/manager.js +329 -287
- package/dist/preset.js +35 -2
- package/package.json +12 -4
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
+
**Minimal `preview.ts`:**
|
|
49
65
|
|
|
50
66
|
```ts
|
|
51
|
-
import registry from '../
|
|
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
|
-
**
|
|
76
|
+
**With a shared `preview`** — merge **`parameters`** so shared decorators/globals stay intact:
|
|
61
77
|
|
|
62
78
|
```ts
|
|
63
|
-
import
|
|
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.
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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() ?
|
|
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.
|
|
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": "
|
|
29
|
-
"
|
|
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",
|