jfs-components 0.1.30 → 0.1.33
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 +29 -0
- package/D2C.md +118 -0
- package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
- package/lib/commonjs/components/CompareTable/CompareTable.js +140 -84
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +28 -5
- package/lib/commonjs/components/CounterBadge/CounterBadge.js +78 -0
- package/lib/commonjs/components/FavoriteToggle/FavoriteToggle.js +180 -0
- package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +287 -0
- package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +219 -0
- package/lib/commonjs/components/index.js +35 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +40095 -1
- package/lib/commonjs/design-tokens/figma-modes.generated.js +3 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/CategoryCard/CategoryCard.js +258 -0
- package/lib/module/components/CompareTable/CompareTable.js +140 -84
- package/lib/module/components/ContentSheet/ContentSheet.js +29 -6
- package/lib/module/components/CounterBadge/CounterBadge.js +73 -0
- package/lib/module/components/FavoriteToggle/FavoriteToggle.js +174 -0
- package/lib/module/components/HelloJioInput/HelloJioInput.js +281 -0
- package/lib/module/components/ValueBackMetric/ValueBackMetric.js +214 -0
- package/lib/module/components/index.js +6 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +40095 -1
- package/lib/module/design-tokens/figma-modes.generated.js +3 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +72 -0
- package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +16 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +7 -1
- package/lib/typescript/src/components/CounterBadge/CounterBadge.d.ts +19 -0
- package/lib/typescript/src/components/FavoriteToggle/FavoriteToggle.d.ts +66 -0
- package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +111 -0
- package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +86 -0
- package/lib/typescript/src/components/index.d.ts +5 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +7 -1
- package/scripts/extract-figma-modes.js +359 -0
- package/src/components/CategoryCard/CategoryCard.tsx +404 -0
- package/src/components/CompareTable/CompareTable.tsx +201 -101
- package/src/components/ContentSheet/ContentSheet.tsx +40 -11
- package/src/components/CounterBadge/CounterBadge.tsx +95 -0
- package/src/components/FavoriteToggle/FavoriteToggle.tsx +243 -0
- package/src/components/HelloJioInput/HelloJioInput.tsx +513 -0
- package/src/components/ValueBackMetric/ValueBackMetric.tsx +298 -0
- package/src/components/index.ts +5 -0
- package/src/design-tokens/Coin Variables-variables-full.json +40095 -1
- package/src/design-tokens/figma-modes.generated.ts +3 -0
- package/src/icons/registry.ts +1 -1
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Extract explicit variable modes per layer from a Figma file.
|
|
4
|
+
*
|
|
5
|
+
* Figma MCP already gives agents the component structure, props, and variants.
|
|
6
|
+
* The one thing it does NOT expose is which variable modes are explicitly set
|
|
7
|
+
* on each layer — that is all this script outputs.
|
|
8
|
+
*
|
|
9
|
+
* Resolution is collection-key based, exclusively:
|
|
10
|
+
* - In consumer files, every subscribed collection id in explicitVariableModes
|
|
11
|
+
* embeds the library collection key: "VariableCollectionId:<40-hex key>/<localId>".
|
|
12
|
+
* The key never changes across publishes.
|
|
13
|
+
* - The packaged variables JSON (src/design-tokens/*-variables-full.json)
|
|
14
|
+
* carries the same key on each collection, so key -> collection -> modeId
|
|
15
|
+
* -> mode name is exact. Mode ids are looked up only *within* the matched
|
|
16
|
+
* collection — never globally, since mode ids can collide across
|
|
17
|
+
* collections in complex projects.
|
|
18
|
+
*
|
|
19
|
+
* REST API only. No Figma Plugin API anywhere, so consumers with view-only /
|
|
20
|
+
* Dev seats can run this — any token with file read access works. One
|
|
21
|
+
* GET /v1/files/:key call per extraction.
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* export FIGMA_ACCESS_TOKEN=figd_...
|
|
25
|
+
* npx jfs-extract-modes "https://www.figma.com/design/<fileKey>/...?node-id=1-10636"
|
|
26
|
+
* npx jfs-extract-modes <fileKey> <nodeId> [--out modes.json]
|
|
27
|
+
*
|
|
28
|
+
* One-time maintenance (library maintainers, when the variables JSON export
|
|
29
|
+
* lacks collection keys): patch keys into the JSON from the master library
|
|
30
|
+
* file via REST variables/local:
|
|
31
|
+
* npx jfs-extract-modes --add-keys <master-library-fileKey-or-url>
|
|
32
|
+
*
|
|
33
|
+
* Output schema (only what Figma MCP doesn't provide):
|
|
34
|
+
* {
|
|
35
|
+
* "file": "D2C Test File",
|
|
36
|
+
* "root": "CC / Unsub",
|
|
37
|
+
* "modes": [
|
|
38
|
+
* { "layer": "AppBar", "nodeId": "1:10638", "path": "CC / Unsub > AppBar",
|
|
39
|
+
* "modes": { "Context2": "AppBar" } }
|
|
40
|
+
* ]
|
|
41
|
+
* }
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
"use strict";
|
|
45
|
+
|
|
46
|
+
const fs = require("fs");
|
|
47
|
+
const path = require("path");
|
|
48
|
+
|
|
49
|
+
const API_BASE = "https://api.figma.com/v1";
|
|
50
|
+
const TOKENS_DIR = path.join(__dirname, "..", "src", "design-tokens");
|
|
51
|
+
const COLLECTION_KEY_PATTERN = /^VariableCollectionId:([0-9a-f]{40})\//;
|
|
52
|
+
|
|
53
|
+
function usage(exitCode) {
|
|
54
|
+
console.error(
|
|
55
|
+
`Usage:
|
|
56
|
+
FIGMA_ACCESS_TOKEN=... jfs-extract-modes <figma-url> [--out modes.json]
|
|
57
|
+
FIGMA_ACCESS_TOKEN=... jfs-extract-modes <fileKey> <nodeId> [--out modes.json]
|
|
58
|
+
FIGMA_ACCESS_TOKEN=... jfs-extract-modes --add-keys <master-library-fileKey-or-url>`,
|
|
59
|
+
);
|
|
60
|
+
process.exit(exitCode);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function fileKeyFromUrlOrKey(value) {
|
|
64
|
+
if (/^https?:\/\//.test(value)) {
|
|
65
|
+
const match = new URL(value).pathname.match(/\/(?:design|file)\/([^/]+)/);
|
|
66
|
+
if (!match) {
|
|
67
|
+
console.error("Could not parse fileKey from URL.");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
return match[1];
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseArgs(argv) {
|
|
76
|
+
const args = argv.slice(2);
|
|
77
|
+
const out = { fileKey: null, nodeId: null, outPath: null, addKeysFile: null };
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
80
|
+
const arg = args[i];
|
|
81
|
+
if (arg === "--out") {
|
|
82
|
+
out.outPath = args[++i];
|
|
83
|
+
} else if (arg === "--add-keys") {
|
|
84
|
+
out.addKeysFile = fileKeyFromUrlOrKey(args[++i] ?? "");
|
|
85
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
86
|
+
usage(0);
|
|
87
|
+
} else if (/^https?:\/\//.test(arg)) {
|
|
88
|
+
const url = new URL(arg);
|
|
89
|
+
out.fileKey = fileKeyFromUrlOrKey(arg);
|
|
90
|
+
const nodeParam = url.searchParams.get("node-id");
|
|
91
|
+
if (nodeParam) out.nodeId = nodeParam.replace(/-/g, ":");
|
|
92
|
+
} else if (!out.fileKey) {
|
|
93
|
+
out.fileKey = arg;
|
|
94
|
+
} else if (!out.nodeId) {
|
|
95
|
+
out.nodeId = arg.replace(/-/g, ":");
|
|
96
|
+
} else {
|
|
97
|
+
usage(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (out.addKeysFile) return out;
|
|
102
|
+
if (!out.fileKey || !out.nodeId) usage(1);
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getToken() {
|
|
107
|
+
const token = process.env.FIGMA_ACCESS_TOKEN || process.env.FIGMA_TOKEN;
|
|
108
|
+
if (!token) {
|
|
109
|
+
console.error("Set FIGMA_ACCESS_TOKEN (needs file read access only).");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
return token;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function figmaGet(urlPath, token) {
|
|
116
|
+
const response = await fetch(`${API_BASE}${urlPath}`, {
|
|
117
|
+
headers: { "X-Figma-Token": token },
|
|
118
|
+
});
|
|
119
|
+
const body = await response.json().catch(() => ({}));
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Figma API ${response.status} for ${urlPath}: ${body.err || body.message || "request failed"}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return body;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function listVariablesJsonFiles() {
|
|
129
|
+
let files;
|
|
130
|
+
try {
|
|
131
|
+
files = fs
|
|
132
|
+
.readdirSync(TOKENS_DIR)
|
|
133
|
+
.filter((name) => name.endsWith("-variables-full.json"))
|
|
134
|
+
.map((name) => path.join(TOKENS_DIR, name));
|
|
135
|
+
} catch {
|
|
136
|
+
files = [];
|
|
137
|
+
}
|
|
138
|
+
if (files.length === 0) {
|
|
139
|
+
console.error(`No *-variables-full.json found in ${TOKENS_DIR}.`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
return files;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* collection key -> { name, modes: Map<modeId, modeName> }
|
|
147
|
+
* Exits with instructions if any collection is missing its key — key-based
|
|
148
|
+
* resolution is the only supported path.
|
|
149
|
+
*/
|
|
150
|
+
function buildKeyIndex() {
|
|
151
|
+
const byKey = new Map();
|
|
152
|
+
const missingKeys = [];
|
|
153
|
+
const files = listVariablesJsonFiles();
|
|
154
|
+
|
|
155
|
+
for (const file of files) {
|
|
156
|
+
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
157
|
+
for (const collection of data.collections ?? []) {
|
|
158
|
+
if (typeof collection.key !== "string" || collection.key.length === 0) {
|
|
159
|
+
missingKeys.push(`${collection.name} (${path.basename(file)})`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const modes = new Map();
|
|
163
|
+
for (const mode of collection.modes ?? []) {
|
|
164
|
+
modes.set(mode.modeId, mode.name);
|
|
165
|
+
}
|
|
166
|
+
byKey.set(collection.key, { name: collection.name, modes });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (byKey.size === 0) {
|
|
171
|
+
console.error(
|
|
172
|
+
"The variables JSON has no collection keys, so modes cannot be resolved.\n" +
|
|
173
|
+
"Run this once against the master variables library file to patch them in:\n" +
|
|
174
|
+
" jfs-extract-modes --add-keys <master-library-fileKey-or-url>",
|
|
175
|
+
);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
if (missingKeys.length > 0) {
|
|
179
|
+
console.error(
|
|
180
|
+
`Warning: ${missingKeys.length} collection(s) have no key and were skipped:\n ` +
|
|
181
|
+
missingKeys.slice(0, 10).join("\n "),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.error(
|
|
186
|
+
`Indexed ${byKey.size} collection(s) by key from ${files.length} variables JSON file(s).`,
|
|
187
|
+
);
|
|
188
|
+
return byKey;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function findNodePath(node, targetId, trail = []) {
|
|
192
|
+
const next = [...trail, node];
|
|
193
|
+
if (node.id === targetId) return next;
|
|
194
|
+
for (const child of node.children ?? []) {
|
|
195
|
+
const found = findNodePath(child, targetId, next);
|
|
196
|
+
if (found) return found;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function extractModes(fileResponse, nodeId, byKey) {
|
|
202
|
+
const pathToRoot = findNodePath(fileResponse.document, nodeId);
|
|
203
|
+
if (!pathToRoot) {
|
|
204
|
+
throw new Error(`Node "${nodeId}" not found in file.`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const root = pathToRoot[pathToRoot.length - 1];
|
|
208
|
+
const layers = [];
|
|
209
|
+
const unresolved = [];
|
|
210
|
+
|
|
211
|
+
function resolveNodeModes(node) {
|
|
212
|
+
const modes = {};
|
|
213
|
+
for (const [collectionId, modeId] of Object.entries(
|
|
214
|
+
node.explicitVariableModes ?? {},
|
|
215
|
+
)) {
|
|
216
|
+
const keyMatch = String(collectionId).match(COLLECTION_KEY_PATTERN);
|
|
217
|
+
if (!keyMatch) {
|
|
218
|
+
// Local (non-subscribed) collection in the consumer file: not part of
|
|
219
|
+
// the published library, nothing to resolve against.
|
|
220
|
+
unresolved.push({
|
|
221
|
+
layer: node.name,
|
|
222
|
+
collectionId,
|
|
223
|
+
modeId,
|
|
224
|
+
reason: "local collection (no library key in id)",
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const collection = byKey.get(keyMatch[1]);
|
|
229
|
+
if (!collection) {
|
|
230
|
+
unresolved.push({
|
|
231
|
+
layer: node.name,
|
|
232
|
+
collectionId,
|
|
233
|
+
modeId,
|
|
234
|
+
reason: "key not in variables JSON (library out of date?)",
|
|
235
|
+
});
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const modeName = collection.modes.get(modeId);
|
|
239
|
+
if (!modeName) {
|
|
240
|
+
unresolved.push({
|
|
241
|
+
layer: node.name,
|
|
242
|
+
collectionId,
|
|
243
|
+
modeId,
|
|
244
|
+
reason: `mode not in collection "${collection.name}" (re-export variables JSON?)`,
|
|
245
|
+
});
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
modes[collection.name] = modeName;
|
|
249
|
+
}
|
|
250
|
+
return modes;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Modes set on ancestors above the requested node (inherited by everything below)
|
|
254
|
+
const inherited = {};
|
|
255
|
+
for (const ancestor of pathToRoot.slice(0, -1)) {
|
|
256
|
+
Object.assign(inherited, resolveNodeModes(ancestor));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function walk(node, trail) {
|
|
260
|
+
const modes = resolveNodeModes(node);
|
|
261
|
+
if (Object.keys(modes).length > 0) {
|
|
262
|
+
layers.push({
|
|
263
|
+
layer: node.name,
|
|
264
|
+
nodeId: node.id,
|
|
265
|
+
path: trail.join(" > "),
|
|
266
|
+
modes,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
for (const child of node.children ?? []) {
|
|
270
|
+
walk(child, [...trail, child.name]);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
walk(root, [root.name]);
|
|
275
|
+
|
|
276
|
+
const result = {
|
|
277
|
+
file: fileResponse.name ?? null,
|
|
278
|
+
root: root.name,
|
|
279
|
+
note: "explicit variable modes per layer; children inherit ancestor modes unless overridden",
|
|
280
|
+
modes: layers,
|
|
281
|
+
};
|
|
282
|
+
if (Object.keys(inherited).length > 0) result.inheritedFromAncestors = inherited;
|
|
283
|
+
if (unresolved.length > 0) result.unresolved = unresolved;
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* One-time maintenance: fetch collection keys from the master library file
|
|
289
|
+
* (REST variables/local returns `key` for every collection) and write them
|
|
290
|
+
* into the packaged variables JSON, matching by local collection id.
|
|
291
|
+
*/
|
|
292
|
+
async function addKeys(masterFileKey, token) {
|
|
293
|
+
const response = await figmaGet(
|
|
294
|
+
`/files/${masterFileKey}/variables/local`,
|
|
295
|
+
token,
|
|
296
|
+
);
|
|
297
|
+
const remote = Object.values(response.meta?.variableCollections ?? {});
|
|
298
|
+
const keyById = new Map(
|
|
299
|
+
remote
|
|
300
|
+
.filter((collection) => collection.key)
|
|
301
|
+
.map((collection) => [collection.id, collection.key]),
|
|
302
|
+
);
|
|
303
|
+
console.error(
|
|
304
|
+
`Fetched ${keyById.size} collection key(s) from ${masterFileKey}.`,
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
for (const file of listVariablesJsonFiles()) {
|
|
308
|
+
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
309
|
+
let patched = 0;
|
|
310
|
+
let missing = 0;
|
|
311
|
+
for (const collection of data.collections ?? []) {
|
|
312
|
+
const key = keyById.get(collection.id);
|
|
313
|
+
if (key) {
|
|
314
|
+
collection.key = key;
|
|
315
|
+
patched += 1;
|
|
316
|
+
} else if (!collection.key) {
|
|
317
|
+
missing += 1;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
|
|
321
|
+
console.error(
|
|
322
|
+
`${path.basename(file)}: patched ${patched} key(s)` +
|
|
323
|
+
(missing ? `, ${missing} collection(s) still without key` : ""),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function main() {
|
|
329
|
+
const args = parseArgs(process.argv);
|
|
330
|
+
const token = getToken();
|
|
331
|
+
|
|
332
|
+
if (args.addKeysFile) {
|
|
333
|
+
await addKeys(args.addKeysFile, token);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const byKey = buildKeyIndex();
|
|
338
|
+
const fileResponse = await figmaGet(
|
|
339
|
+
`/files/${args.fileKey}?ids=${encodeURIComponent(args.nodeId)}`,
|
|
340
|
+
token,
|
|
341
|
+
);
|
|
342
|
+
const result = extractModes(fileResponse, args.nodeId, byKey);
|
|
343
|
+
|
|
344
|
+
const json = JSON.stringify(result, null, 2);
|
|
345
|
+
if (args.outPath) {
|
|
346
|
+
fs.writeFileSync(path.resolve(args.outPath), `${json}\n`);
|
|
347
|
+
console.error(`Wrote ${args.outPath}`);
|
|
348
|
+
}
|
|
349
|
+
process.stdout.write(`${json}\n`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (require.main === module) {
|
|
353
|
+
main().catch((error) => {
|
|
354
|
+
console.error(error.message || error);
|
|
355
|
+
process.exit(1);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
module.exports = { buildKeyIndex, extractModes };
|