jfs-components 0.1.34 → 0.1.36

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/D2C.md +82 -87
  3. package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +5 -5
  4. package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +2 -2
  5. package/lib/commonjs/components/PageHero/PageHero.js +73 -76
  6. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +42204 -1
  7. package/lib/commonjs/design-tokens/figma-modes.generated.js +52 -37
  8. package/lib/commonjs/icons/registry.js +1 -1
  9. package/lib/module/components/LottiePlayer/LottiePlayer.js +5 -5
  10. package/lib/module/components/LottiePlayer/LottiePlayer.web.js +2 -2
  11. package/lib/module/components/PageHero/PageHero.js +73 -76
  12. package/lib/module/design-tokens/Coin Variables-variables-full.json +42204 -1
  13. package/lib/module/design-tokens/figma-modes.generated.js +52 -37
  14. package/lib/module/icons/registry.js +1 -1
  15. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +6 -6
  16. package/lib/typescript/src/components/PageHero/PageHero.d.ts +33 -25
  17. package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +14 -1
  18. package/lib/typescript/src/icons/registry.d.ts +1 -1
  19. package/package.json +3 -1
  20. package/scripts/extract-figma-modes.js +150 -121
  21. package/src/components/LottiePlayer/LottiePlayer.tsx +8 -8
  22. package/src/components/LottiePlayer/LottiePlayer.web.tsx +2 -2
  23. package/src/components/PageHero/PageHero.tsx +142 -106
  24. package/src/design-tokens/Coin Variables-variables-full.json +42204 -1
  25. package/src/design-tokens/figma-modes.generated.ts +52 -37
  26. package/src/icons/registry.ts +1 -1
@@ -21,7 +21,8 @@
21
21
  * GET /v1/files/:key call per extraction.
22
22
  *
23
23
  * Usage:
24
- * export FIGMA_ACCESS_TOKEN=figd_...
24
+ * # Either add FIGMA_ACCESS_TOKEN=figd_... to .env in the current project,
25
+ * # or export it in the shell (shell values take precedence over .env).
25
26
  * npx jfs-extract-modes "https://www.figma.com/design/<fileKey>/...?node-id=1-10636"
26
27
  * npx jfs-extract-modes <fileKey> <nodeId> [--out modes.json]
27
28
  *
@@ -41,105 +42,132 @@
41
42
  * }
42
43
  */
43
44
 
44
- "use strict";
45
+ "use strict"
45
46
 
46
- const fs = require("fs");
47
- const path = require("path");
47
+ const fs = require("fs")
48
+ const path = require("path")
49
+ const dotenv = require("dotenv")
48
50
 
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})\//;
51
+ const API_BASE = "https://api.figma.com/v1"
52
+ const TOKENS_DIR = path.join(__dirname, "..", "src", "design-tokens")
53
+ const COLLECTION_KEY_PATTERN = /^VariableCollectionId:([0-9a-f]{40})\//
54
+
55
+ /**
56
+ * Load the consumer project's root .env without replacing values that were
57
+ * explicitly exported in the shell. The CLI is normally launched by npx from
58
+ * the consumer project, so process.cwd() is the correct project root rather
59
+ * than the installed package directory.
60
+ */
61
+ function loadProjectEnv(cwd = process.cwd()) {
62
+ const envPath = path.resolve(cwd, ".env")
63
+ if (!fs.existsSync(envPath)) return null
64
+
65
+ const result = dotenv.config({
66
+ path: envPath,
67
+ override: false,
68
+ quiet: true,
69
+ })
70
+ if (result.error) {
71
+ throw new Error(`Could not load ${envPath}: ${result.error.message}`)
72
+ }
73
+ return envPath
74
+ }
52
75
 
53
76
  function usage(exitCode) {
54
77
  console.error(
55
78
  `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);
79
+ jfs-extract-modes <figma-url> [--out modes.json]
80
+ jfs-extract-modes <fileKey> <nodeId> [--out modes.json]
81
+ jfs-extract-modes --add-keys <master-library-fileKey-or-url>
82
+
83
+ Set FIGMA_ACCESS_TOKEN in the shell or in .env in the current project.`,
84
+ )
85
+ process.exit(exitCode)
61
86
  }
62
87
 
63
88
  function fileKeyFromUrlOrKey(value) {
64
89
  if (/^https?:\/\//.test(value)) {
65
- const match = new URL(value).pathname.match(/\/(?:design|file)\/([^/]+)/);
90
+ const match = new URL(value).pathname.match(/\/(?:design|file)\/([^/]+)/)
66
91
  if (!match) {
67
- console.error("Could not parse fileKey from URL.");
68
- process.exit(1);
92
+ console.error("Could not parse fileKey from URL.")
93
+ process.exit(1)
69
94
  }
70
- return match[1];
95
+ return match[1]
71
96
  }
72
- return value;
97
+ return value
73
98
  }
74
99
 
75
100
  function parseArgs(argv) {
76
- const args = argv.slice(2);
77
- const out = { fileKey: null, nodeId: null, outPath: null, addKeysFile: null };
101
+ const args = argv.slice(2)
102
+ const out = { fileKey: null, nodeId: null, outPath: null, addKeysFile: null }
78
103
 
79
104
  for (let i = 0; i < args.length; i += 1) {
80
- const arg = args[i];
105
+ const arg = args[i]
81
106
  if (arg === "--out") {
82
- out.outPath = args[++i];
107
+ out.outPath = args[++i]
83
108
  } else if (arg === "--add-keys") {
84
- out.addKeysFile = fileKeyFromUrlOrKey(args[++i] ?? "");
109
+ out.addKeysFile = fileKeyFromUrlOrKey(args[++i] ?? "")
85
110
  } else if (arg === "--help" || arg === "-h") {
86
- usage(0);
111
+ usage(0)
87
112
  } 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, ":");
113
+ const url = new URL(arg)
114
+ out.fileKey = fileKeyFromUrlOrKey(arg)
115
+ const nodeParam = url.searchParams.get("node-id")
116
+ if (nodeParam) out.nodeId = nodeParam.replace(/-/g, ":")
92
117
  } else if (!out.fileKey) {
93
- out.fileKey = arg;
118
+ out.fileKey = arg
94
119
  } else if (!out.nodeId) {
95
- out.nodeId = arg.replace(/-/g, ":");
120
+ out.nodeId = arg.replace(/-/g, ":")
96
121
  } else {
97
- usage(1);
122
+ usage(1)
98
123
  }
99
124
  }
100
125
 
101
- if (out.addKeysFile) return out;
102
- if (!out.fileKey || !out.nodeId) usage(1);
103
- return out;
126
+ if (out.addKeysFile) return out
127
+ if (!out.fileKey || !out.nodeId) usage(1)
128
+ return out
104
129
  }
105
130
 
106
131
  function getToken() {
107
- const token = process.env.FIGMA_ACCESS_TOKEN || process.env.FIGMA_TOKEN;
132
+ const token = process.env.FIGMA_ACCESS_TOKEN || process.env.FIGMA_TOKEN
108
133
  if (!token) {
109
- console.error("Set FIGMA_ACCESS_TOKEN (needs file read access only).");
110
- process.exit(1);
134
+ console.error(
135
+ "Set FIGMA_ACCESS_TOKEN in the shell or in the project-root .env " +
136
+ "(requires only the Figma file_content:read scope).",
137
+ )
138
+ process.exit(1)
111
139
  }
112
- return token;
140
+ return token
113
141
  }
114
142
 
115
143
  async function figmaGet(urlPath, token) {
116
144
  const response = await fetch(`${API_BASE}${urlPath}`, {
117
145
  headers: { "X-Figma-Token": token },
118
- });
119
- const body = await response.json().catch(() => ({}));
146
+ })
147
+ const body = await response.json().catch(() => ({}))
120
148
  if (!response.ok) {
121
149
  throw new Error(
122
150
  `Figma API ${response.status} for ${urlPath}: ${body.err || body.message || "request failed"}`,
123
- );
151
+ )
124
152
  }
125
- return body;
153
+ return body
126
154
  }
127
155
 
128
156
  function listVariablesJsonFiles() {
129
- let files;
157
+ let files
130
158
  try {
131
159
  files = fs
132
160
  .readdirSync(TOKENS_DIR)
133
161
  .filter((name) => name.endsWith("-variables-full.json"))
134
- .map((name) => path.join(TOKENS_DIR, name));
162
+ .map((name) => path.join(TOKENS_DIR, name))
135
163
  } catch {
136
- files = [];
164
+ files = []
137
165
  }
138
166
  if (files.length === 0) {
139
- console.error(`No *-variables-full.json found in ${TOKENS_DIR}.`);
140
- process.exit(1);
167
+ console.error(`No *-variables-full.json found in ${TOKENS_DIR}.`)
168
+ process.exit(1)
141
169
  }
142
- return files;
170
+ return files
143
171
  }
144
172
 
145
173
  /**
@@ -148,22 +176,22 @@ function listVariablesJsonFiles() {
148
176
  * resolution is the only supported path.
149
177
  */
150
178
  function buildKeyIndex() {
151
- const byKey = new Map();
152
- const missingKeys = [];
153
- const files = listVariablesJsonFiles();
179
+ const byKey = new Map()
180
+ const missingKeys = []
181
+ const files = listVariablesJsonFiles()
154
182
 
155
183
  for (const file of files) {
156
- const data = JSON.parse(fs.readFileSync(file, "utf8"));
184
+ const data = JSON.parse(fs.readFileSync(file, "utf8"))
157
185
  for (const collection of data.collections ?? []) {
158
186
  if (typeof collection.key !== "string" || collection.key.length === 0) {
159
- missingKeys.push(`${collection.name} (${path.basename(file)})`);
160
- continue;
187
+ missingKeys.push(`${collection.name} (${path.basename(file)})`)
188
+ continue
161
189
  }
162
- const modes = new Map();
190
+ const modes = new Map()
163
191
  for (const mode of collection.modes ?? []) {
164
- modes.set(mode.modeId, mode.name);
192
+ modes.set(mode.modeId, mode.name)
165
193
  }
166
- byKey.set(collection.key, { name: collection.name, modes });
194
+ byKey.set(collection.key, { name: collection.name, modes })
167
195
  }
168
196
  }
169
197
 
@@ -172,48 +200,48 @@ function buildKeyIndex() {
172
200
  "The variables JSON has no collection keys, so modes cannot be resolved.\n" +
173
201
  "Run this once against the master variables library file to patch them in:\n" +
174
202
  " jfs-extract-modes --add-keys <master-library-fileKey-or-url>",
175
- );
176
- process.exit(1);
203
+ )
204
+ process.exit(1)
177
205
  }
178
206
  if (missingKeys.length > 0) {
179
207
  console.error(
180
208
  `Warning: ${missingKeys.length} collection(s) have no key and were skipped:\n ` +
181
209
  missingKeys.slice(0, 10).join("\n "),
182
- );
210
+ )
183
211
  }
184
212
 
185
213
  console.error(
186
214
  `Indexed ${byKey.size} collection(s) by key from ${files.length} variables JSON file(s).`,
187
- );
188
- return byKey;
215
+ )
216
+ return byKey
189
217
  }
190
218
 
191
219
  function findNodePath(node, targetId, trail = []) {
192
- const next = [...trail, node];
193
- if (node.id === targetId) return next;
220
+ const next = [...trail, node]
221
+ if (node.id === targetId) return next
194
222
  for (const child of node.children ?? []) {
195
- const found = findNodePath(child, targetId, next);
196
- if (found) return found;
223
+ const found = findNodePath(child, targetId, next)
224
+ if (found) return found
197
225
  }
198
- return null;
226
+ return null
199
227
  }
200
228
 
201
229
  function extractModes(fileResponse, nodeId, byKey) {
202
- const pathToRoot = findNodePath(fileResponse.document, nodeId);
230
+ const pathToRoot = findNodePath(fileResponse.document, nodeId)
203
231
  if (!pathToRoot) {
204
- throw new Error(`Node "${nodeId}" not found in file.`);
232
+ throw new Error(`Node "${nodeId}" not found in file.`)
205
233
  }
206
234
 
207
- const root = pathToRoot[pathToRoot.length - 1];
208
- const layers = [];
209
- const unresolved = [];
235
+ const root = pathToRoot[pathToRoot.length - 1]
236
+ const layers = []
237
+ const unresolved = []
210
238
 
211
239
  function resolveNodeModes(node) {
212
- const modes = {};
240
+ const modes = {}
213
241
  for (const [collectionId, modeId] of Object.entries(
214
242
  node.explicitVariableModes ?? {},
215
243
  )) {
216
- const keyMatch = String(collectionId).match(COLLECTION_KEY_PATTERN);
244
+ const keyMatch = String(collectionId).match(COLLECTION_KEY_PATTERN)
217
245
  if (!keyMatch) {
218
246
  // Local (non-subscribed) collection in the consumer file: not part of
219
247
  // the published library, nothing to resolve against.
@@ -222,66 +250,66 @@ function extractModes(fileResponse, nodeId, byKey) {
222
250
  collectionId,
223
251
  modeId,
224
252
  reason: "local collection (no library key in id)",
225
- });
226
- continue;
253
+ })
254
+ continue
227
255
  }
228
- const collection = byKey.get(keyMatch[1]);
256
+ const collection = byKey.get(keyMatch[1])
229
257
  if (!collection) {
230
258
  unresolved.push({
231
259
  layer: node.name,
232
260
  collectionId,
233
261
  modeId,
234
262
  reason: "key not in variables JSON (library out of date?)",
235
- });
236
- continue;
263
+ })
264
+ continue
237
265
  }
238
- const modeName = collection.modes.get(modeId);
266
+ const modeName = collection.modes.get(modeId)
239
267
  if (!modeName) {
240
268
  unresolved.push({
241
269
  layer: node.name,
242
270
  collectionId,
243
271
  modeId,
244
272
  reason: `mode not in collection "${collection.name}" (re-export variables JSON?)`,
245
- });
246
- continue;
273
+ })
274
+ continue
247
275
  }
248
- modes[collection.name] = modeName;
276
+ modes[collection.name] = modeName
249
277
  }
250
- return modes;
278
+ return modes
251
279
  }
252
280
 
253
281
  // Modes set on ancestors above the requested node (inherited by everything below)
254
- const inherited = {};
282
+ const inherited = {}
255
283
  for (const ancestor of pathToRoot.slice(0, -1)) {
256
- Object.assign(inherited, resolveNodeModes(ancestor));
284
+ Object.assign(inherited, resolveNodeModes(ancestor))
257
285
  }
258
286
 
259
287
  function walk(node, trail) {
260
- const modes = resolveNodeModes(node);
288
+ const modes = resolveNodeModes(node)
261
289
  if (Object.keys(modes).length > 0) {
262
290
  layers.push({
263
291
  layer: node.name,
264
292
  nodeId: node.id,
265
293
  path: trail.join(" > "),
266
294
  modes,
267
- });
295
+ })
268
296
  }
269
297
  for (const child of node.children ?? []) {
270
- walk(child, [...trail, child.name]);
298
+ walk(child, [...trail, child.name])
271
299
  }
272
300
  }
273
301
 
274
- walk(root, [root.name]);
302
+ walk(root, [root.name])
275
303
 
276
304
  const result = {
277
305
  file: fileResponse.name ?? null,
278
306
  root: root.name,
279
307
  note: "explicit variable modes per layer; children inherit ancestor modes unless overridden",
280
308
  modes: layers,
281
- };
282
- if (Object.keys(inherited).length > 0) result.inheritedFromAncestors = inherited;
283
- if (unresolved.length > 0) result.unresolved = unresolved;
284
- return result;
309
+ }
310
+ if (Object.keys(inherited).length > 0) result.inheritedFromAncestors = inherited
311
+ if (unresolved.length > 0) result.unresolved = unresolved
312
+ return result
285
313
  }
286
314
 
287
315
  /**
@@ -293,67 +321,68 @@ async function addKeys(masterFileKey, token) {
293
321
  const response = await figmaGet(
294
322
  `/files/${masterFileKey}/variables/local`,
295
323
  token,
296
- );
297
- const remote = Object.values(response.meta?.variableCollections ?? {});
324
+ )
325
+ const remote = Object.values(response.meta?.variableCollections ?? {})
298
326
  const keyById = new Map(
299
327
  remote
300
328
  .filter((collection) => collection.key)
301
329
  .map((collection) => [collection.id, collection.key]),
302
- );
330
+ )
303
331
  console.error(
304
332
  `Fetched ${keyById.size} collection key(s) from ${masterFileKey}.`,
305
- );
333
+ )
306
334
 
307
335
  for (const file of listVariablesJsonFiles()) {
308
- const data = JSON.parse(fs.readFileSync(file, "utf8"));
309
- let patched = 0;
310
- let missing = 0;
336
+ const data = JSON.parse(fs.readFileSync(file, "utf8"))
337
+ let patched = 0
338
+ let missing = 0
311
339
  for (const collection of data.collections ?? []) {
312
- const key = keyById.get(collection.id);
340
+ const key = keyById.get(collection.id)
313
341
  if (key) {
314
- collection.key = key;
315
- patched += 1;
342
+ collection.key = key
343
+ patched += 1
316
344
  } else if (!collection.key) {
317
- missing += 1;
345
+ missing += 1
318
346
  }
319
347
  }
320
- fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
348
+ fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`)
321
349
  console.error(
322
350
  `${path.basename(file)}: patched ${patched} key(s)` +
323
351
  (missing ? `, ${missing} collection(s) still without key` : ""),
324
- );
352
+ )
325
353
  }
326
354
  }
327
355
 
328
356
  async function main() {
329
- const args = parseArgs(process.argv);
330
- const token = getToken();
357
+ const args = parseArgs(process.argv)
358
+ loadProjectEnv()
359
+ const token = getToken()
331
360
 
332
361
  if (args.addKeysFile) {
333
- await addKeys(args.addKeysFile, token);
334
- return;
362
+ await addKeys(args.addKeysFile, token)
363
+ return
335
364
  }
336
365
 
337
- const byKey = buildKeyIndex();
366
+ const byKey = buildKeyIndex()
338
367
  const fileResponse = await figmaGet(
339
368
  `/files/${args.fileKey}?ids=${encodeURIComponent(args.nodeId)}`,
340
369
  token,
341
- );
342
- const result = extractModes(fileResponse, args.nodeId, byKey);
370
+ )
371
+ const result = extractModes(fileResponse, args.nodeId, byKey)
343
372
 
344
- const json = JSON.stringify(result, null, 2);
373
+ const json = JSON.stringify(result, null, 2)
345
374
  if (args.outPath) {
346
- fs.writeFileSync(path.resolve(args.outPath), `${json}\n`);
347
- console.error(`Wrote ${args.outPath}`);
375
+ fs.writeFileSync(path.resolve(args.outPath), `${json}\n`)
376
+ console.error(`Wrote ${args.outPath}`)
348
377
  }
349
- process.stdout.write(`${json}\n`);
378
+ process.stdout.write(`${json}\n`)
350
379
  }
351
380
 
352
381
  if (require.main === module) {
353
382
  main().catch((error) => {
354
- console.error(error.message || error);
355
- process.exit(1);
356
- });
383
+ console.error(error.message || error)
384
+ process.exit(1)
385
+ })
357
386
  }
358
387
 
359
- module.exports = { buildKeyIndex, extractModes };
388
+ module.exports = { buildKeyIndex, extractModes, loadProjectEnv }
@@ -29,10 +29,10 @@ export type LottiePlayerProps = {
29
29
  * Override the rendered size. Pass a number for a square box, or
30
30
  * `{ width, height }` for non-square.
31
31
  *
32
- * When omitted, size is resolved from the `media/width` and `media/height`
33
- * design tokens (default `117 × 117`). The `Media / Output` collection
32
+ * When omitted, size is resolved from the `video/width` and `video/height`
33
+ * design tokens (default `117 × 117`). The `Video / Output` collection
34
34
  * exposes `L | M | S` modes (117 / 70 / 20) — pass
35
- * `modes={{ 'Media / Output': 'M' }}` to render at 70×70, etc.
35
+ * `modes={{ 'Video / Output': 'M' }}` to render at 70×70, etc.
36
36
  */
37
37
  size?: number | { width: number; height: number }
38
38
  /** Play the animation on mount. Defaults to `true`. */
@@ -57,9 +57,9 @@ function resolveSize(
57
57
  if (typeof size === 'number') return { width: size, height: size }
58
58
  if (size && typeof size === 'object') return size
59
59
  const width =
60
- Number(getVariableByName('media/width', modes))
60
+ Number(getVariableByName('video/width', modes)) || DEFAULT_SIZE
61
61
  const height =
62
- Number(getVariableByName('media/height', modes))
62
+ Number(getVariableByName('video/height', modes)) || DEFAULT_SIZE
63
63
  return { width, height }
64
64
  }
65
65
 
@@ -83,8 +83,8 @@ function resolveSize(
83
83
  * webpack via platform extensions — same pattern as `MediaCard/GlassFill`.
84
84
  *
85
85
  * Token-driven sizing: when `size` is omitted, `LottiePlayer` reads
86
- * `media/width` and `media/height` from the Figma variables resolver, so the
87
- * animation matches the surrounding component's `Media / Output` mode
86
+ * `video/width` and `video/height` from the Figma variables resolver, so the
87
+ * animation matches the surrounding component's `Video / Output` mode
88
88
  * automatically. This is the same sizing contract `PageHero` and
89
89
  * `LottieIntroBlock` use for their `media` slots.
90
90
  *
@@ -95,7 +95,7 @@ function resolveSize(
95
95
  *
96
96
  * <LottiePlayer source={animation} /> // 117 × 117 (default)
97
97
  * <LottiePlayer source={animation} size={70} /> // 70 × 70
98
- * <LottiePlayer source={animation} modes={{ 'Media / Output': 'S' }} /> // 20 × 20
98
+ * <LottiePlayer source={animation} modes={{ 'Video / Output': 'S' }} /> // 20 × 20
99
99
  * <PageHero media={<LottiePlayer source={animation} />} />
100
100
  * ```
101
101
  */
@@ -28,9 +28,9 @@ function resolveSize(
28
28
  if (typeof size === 'number') return { width: size, height: size }
29
29
  if (size && typeof size === 'object') return size
30
30
  const width =
31
- Number(getVariableByName('media/width', modes))
31
+ Number(getVariableByName('video/width', modes)) || DEFAULT_SIZE
32
32
  const height =
33
- Number(getVariableByName('media/height', modes))
33
+ Number(getVariableByName('video/height', modes)) || DEFAULT_SIZE
34
34
  return { width, height }
35
35
  }
36
36