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