@1agh/maude 0.58.2 → 0.58.3

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 (105) hide show
  1. package/apps/studio/annotations-bindings.ts +83 -4
  2. package/apps/studio/api.ts +6 -1
  3. package/apps/studio/bin/_fetch-asset.mjs +169 -5
  4. package/apps/studio/bin/_import-asset.mjs +72 -0
  5. package/apps/studio/bin/_import-figma.mjs +1121 -0
  6. package/apps/studio/bin/_video-playwright.mjs +86 -3
  7. package/apps/studio/bin/import-figma.sh +38 -0
  8. package/apps/studio/bin/read-annotations.mjs +11 -1
  9. package/apps/studio/bun.lock +16 -22
  10. package/apps/studio/canvas-edit.ts +29 -5
  11. package/apps/studio/client/app.jsx +44 -1
  12. package/apps/studio/client/export-center.jsx +42 -4
  13. package/apps/studio/client/panels/CloudBar.jsx +92 -1
  14. package/apps/studio/client/panels/FigmaImportPanel.jsx +264 -0
  15. package/apps/studio/client/panels/GitPanel.jsx +26 -6
  16. package/apps/studio/client/panels/SettingsPanel.jsx +181 -0
  17. package/apps/studio/client/panels/SetupChecklist.jsx +26 -2
  18. package/apps/studio/client/panels/TimelinePanel.jsx +2 -2
  19. package/apps/studio/client/panels/timeline-parse.js +3 -3
  20. package/apps/studio/client/styles/3-shell-maude.css +7 -0
  21. package/apps/studio/client/styles/4-components.css +134 -0
  22. package/apps/studio/clip-ops.ts +93 -17
  23. package/apps/studio/cloud/endpoints.ts +78 -10
  24. package/apps/studio/cloud/renew.ts +183 -0
  25. package/apps/studio/context.ts +2 -1
  26. package/apps/studio/dist/client.bundle.js +1261 -1261
  27. package/apps/studio/dist/runtime/@remotion_media.js +56 -136
  28. package/apps/studio/dist/runtime/@remotion_player.js +18 -18
  29. package/apps/studio/dist/runtime/@remotion_transitions.js +9 -9
  30. package/apps/studio/dist/runtime/@remotion_transitions_clock-wipe.js +1 -1
  31. package/apps/studio/dist/runtime/remotion.js +12 -12
  32. package/apps/studio/dist/styles.css +1 -1
  33. package/apps/studio/exporters/_browser-bundles.ts +20 -6
  34. package/apps/studio/exporters/_runtime.ts +19 -0
  35. package/apps/studio/exporters/degraded.ts +92 -0
  36. package/apps/studio/exporters/index.ts +5 -0
  37. package/apps/studio/exporters/jobs.ts +19 -0
  38. package/apps/studio/exporters/unsupported-media.ts +170 -0
  39. package/apps/studio/exporters/video-encode-lib.ts +27 -1
  40. package/apps/studio/exporters/video-render-lib.ts +6 -0
  41. package/apps/studio/exporters/video.ts +62 -1
  42. package/apps/studio/figma/assets.test.ts +372 -0
  43. package/apps/studio/figma/assets.ts +398 -0
  44. package/apps/studio/figma/client.test.ts +395 -0
  45. package/apps/studio/figma/client.ts +513 -0
  46. package/apps/studio/figma/comments-to-strokes.test.ts +194 -0
  47. package/apps/studio/figma/comments-to-strokes.ts +173 -0
  48. package/apps/studio/figma/endpoints.ts +200 -0
  49. package/apps/studio/figma/sanitize.test.ts +256 -0
  50. package/apps/studio/figma/sanitize.ts +315 -0
  51. package/apps/studio/figma/style-map.ts +352 -0
  52. package/apps/studio/figma/to-artboard.test.ts +808 -0
  53. package/apps/studio/figma/to-artboard.ts +701 -0
  54. package/apps/studio/figma/to-render.test.ts +180 -0
  55. package/apps/studio/figma/to-render.ts +306 -0
  56. package/apps/studio/figma/to-strokes-roundtrip.test.ts +152 -0
  57. package/apps/studio/figma/to-strokes.test.ts +705 -0
  58. package/apps/studio/figma/to-strokes.ts +749 -0
  59. package/apps/studio/figma/to-tokens.test.ts +321 -0
  60. package/apps/studio/figma/to-tokens.ts +305 -0
  61. package/apps/studio/figma/types.ts +539 -0
  62. package/apps/studio/figma/url.test.ts +167 -0
  63. package/apps/studio/figma/url.ts +160 -0
  64. package/apps/studio/http.ts +129 -0
  65. package/apps/studio/sync/asset-push.ts +124 -0
  66. package/apps/studio/sync/connection-state.ts +11 -0
  67. package/apps/studio/sync/hub-link.ts +63 -7
  68. package/apps/studio/sync/hubs-config.ts +31 -3
  69. package/apps/studio/sync/index.ts +276 -26
  70. package/apps/studio/sync/migrate-flat-fallback.ts +121 -0
  71. package/apps/studio/sync/presentation.ts +45 -1
  72. package/apps/studio/sync/supervisor.ts +5 -1
  73. package/apps/studio/sync/workspace-signin.ts +7 -3
  74. package/apps/studio/test/annotations-bindings.test.ts +150 -12
  75. package/apps/studio/test/canvas-create-api.test.ts +4 -1
  76. package/apps/studio/test/canvas-origin-gate.test.ts +13 -0
  77. package/apps/studio/test/capture-determinism-shape.test.ts +135 -0
  78. package/apps/studio/test/clip-addressing.test.ts +6 -1
  79. package/apps/studio/test/clip-ops.test.ts +5 -1
  80. package/apps/studio/test/cloud-endpoints.test.ts +96 -0
  81. package/apps/studio/test/cloud-renew.test.ts +205 -0
  82. package/apps/studio/test/cloud-shell-surfaces.test.ts +11 -2
  83. package/apps/studio/test/exporters/degraded-propagation.test.ts +123 -0
  84. package/apps/studio/test/exporters/unsupported-media.test.ts +123 -0
  85. package/apps/studio/test/fetch-asset-gate.test.ts +189 -0
  86. package/apps/studio/test/figma-provenance.test.ts +108 -0
  87. package/apps/studio/test/figma-routes.test.ts +294 -0
  88. package/apps/studio/test/git-cloud-posture.test.ts +50 -0
  89. package/apps/studio/test/hub-link.test.ts +11 -0
  90. package/apps/studio/test/import-figma.test.ts +479 -0
  91. package/apps/studio/test/sync-asset-push.test.ts +124 -0
  92. package/apps/studio/test/sync-connection-state.test.ts +13 -0
  93. package/apps/studio/test/sync-hubs-config.test.ts +5 -0
  94. package/apps/studio/test/sync-migrate-flat-fallback.test.ts +98 -0
  95. package/apps/studio/test/sync-path-pull.test.ts +63 -0
  96. package/apps/studio/test/sync-presentation.test.ts +77 -0
  97. package/apps/studio/test/sync-runtime.test.ts +316 -1
  98. package/apps/studio/test/video-comp.test.ts +23 -1
  99. package/apps/studio/test/workspace-containment.test.ts +1 -0
  100. package/apps/studio/video-comp.tsx +70 -6
  101. package/apps/studio/whats-new.json +27 -0
  102. package/apps/studio/workspace-mode.ts +4 -0
  103. package/cli/commands/design.mjs +8 -0
  104. package/package.json +8 -8
  105. package/plugins/flow/.claude-plugin/config.schema.json +3 -3
@@ -0,0 +1,352 @@
1
+ /**
2
+ * @file figma/style-map.ts — Figma paint/effect/typeStyle → CSS.
3
+ * @scope apps/studio/figma/style-map.ts
4
+ * @purpose Turn a node's visual properties into CSS declarations, resolving
5
+ * every colour to the NEAREST active-DS token rather than baking a
6
+ * hex into an imported canvas.
7
+ *
8
+ * @invariant IMPORTED FRAMES MUST NOT HARDCODE HEX. A canvas whose colours are
9
+ * literals is off-brand the moment the DS moves, and it defeats the
10
+ * point of importing INTO a design system. Every paint resolves to
11
+ * `var(--token)` inside a configurable ΔE threshold; outside it, the
12
+ * literal ships WITH a `/* figma: no near token *​/` marker so the
13
+ * drift is auditable rather than silent. Threshold and fallback are
14
+ * per-import flags, never hidden constants.
15
+ *
16
+ * @invariant EVERY EMITTED VALUE IS GRAMMAR-VALIDATED FIRST (DDR-216 D6, which
17
+ * reuses DDR-172 Decision 4 verbatim): printable-ASCII pre-filter,
18
+ * no `m`/`s` regex flags, shape AND magnitude bounds, no unbounded
19
+ * free-text capture. A value that fails is DROPPED and reported —
20
+ * never "cleaned up" and never partially applied. This matters more
21
+ * here than in `import-tokens`: a canvas stylesheet is rendered live
22
+ * in the canvas iframe.
23
+ *
24
+ * @invariant DEPENDENCY-FREE beyond `draw/palette.ts` (the colorspace helpers
25
+ * `import-tokens` already reuses). No fs, no network.
26
+ */
27
+
28
+ import { parseColor, rgbToOklch } from '../draw/palette.ts';
29
+ import type { FigmaColor, FigmaEffect, FigmaNode, FigmaPaint, FigmaTypeStyle } from './types.ts';
30
+
31
+ /** Default perceptual distance under which a paint snaps to a DS token. */
32
+ export const DEFAULT_TOKEN_THRESHOLD = 0.08;
33
+
34
+ /** The marker that makes an un-snapped literal auditable rather than silent. */
35
+ export const NO_TOKEN_MARKER = '/* figma: no near token */';
36
+
37
+ export interface DsToken {
38
+ /** e.g. `--accent` — the name only, never a value the caller invented. */
39
+ name: string;
40
+ /** Resolved hex for distance comparison. */
41
+ hex: string;
42
+ }
43
+
44
+ // ── Value grammars (DDR-172 Decision 4, reused verbatim per DDR-216 D6) ─────
45
+ //
46
+ // Deliberately NO unbounded free-text capture anywhere, and every numeric
47
+ // grammar is paired with a magnitude bound: a grammar-valid `99999999px` is a
48
+ // live-render hang vector even though its SHAPE is fine.
49
+
50
+ const PRINTABLE_ASCII_RE = /^[\x20-\x7E]*$/;
51
+ const HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
52
+ const RGBA_RE = /^rgba?\( ?[\d.]+[ ,]+[\d.]+[ ,]+[\d.]+(?: ?[,/] ?[\d.]+%?)? ?\)$/;
53
+ const VAR_RE = /^var\(--[a-z0-9-]{1,64}\)$/;
54
+ const DIM_MAX = 4096;
55
+ const FONT_SIZE_MAX = 512;
56
+
57
+ /** True when a string is safe to place in a CSS value position at all. */
58
+ function printableAscii(v: string): boolean {
59
+ // Checked BEFORE any grammar regex, and no grammar here uses `m`/`s` — an
60
+ // anchor is only as strong as the flags it runs under, and a multiline value
61
+ // would otherwise match just its first line.
62
+ return PRINTABLE_ASCII_RE.test(v);
63
+ }
64
+
65
+ export function isValidColorValue(v: string): boolean {
66
+ if (!printableAscii(v)) return false;
67
+ return HEX_RE.test(v) || RGBA_RE.test(v) || VAR_RE.test(v);
68
+ }
69
+
70
+ export function isValidDimension(v: string, max = DIM_MAX): boolean {
71
+ if (!printableAscii(v)) return false;
72
+ if (v === '0') return true;
73
+ const m = /^(-?[\d.]{1,7})(px|rem|em|%)$/.exec(v);
74
+ if (!m) return false;
75
+ const n = Number.parseFloat(m[1]);
76
+ return Number.isFinite(n) && Math.abs(n) <= max;
77
+ }
78
+
79
+ // ── Colour ──────────────────────────────────────────────────────────────────
80
+
81
+ function figmaColorToHex(c: FigmaColor): string {
82
+ const to = (v: number) =>
83
+ Math.max(0, Math.min(255, Math.round(v * 255)))
84
+ .toString(16)
85
+ .padStart(2, '0');
86
+ return `#${to(c.r)}${to(c.g)}${to(c.b)}`;
87
+ }
88
+
89
+ /** Perceptual distance in OKLCH — the same space `import-tokens` compares in. */
90
+ export function perceptualDistance(aHex: string, bHex: string): number {
91
+ try {
92
+ const a = rgbToOklch(parseColor(aHex));
93
+ const b = rgbToOklch(parseColor(bHex));
94
+ // Chroma/hue folded into a Cartesian pair so a hue difference at low chroma
95
+ // (where hue is perceptually meaningless) doesn't dominate the distance.
96
+ const aa = a.c * Math.cos((a.h * Math.PI) / 180);
97
+ const ab = a.c * Math.sin((a.h * Math.PI) / 180);
98
+ const ba = b.c * Math.cos((b.h * Math.PI) / 180);
99
+ const bb = b.c * Math.sin((b.h * Math.PI) / 180);
100
+ return Math.hypot(a.l - b.l, aa - ba, ab - bb);
101
+ } catch {
102
+ return Number.POSITIVE_INFINITY;
103
+ }
104
+ }
105
+
106
+ export interface ResolvedColor {
107
+ /** The CSS value to emit — `var(--token)` or a literal hex. */
108
+ value: string;
109
+ /** Set when no token was near enough; the caller emits it as a comment. */
110
+ marker?: string;
111
+ /** Which token matched, for the report. */
112
+ token?: string;
113
+ }
114
+
115
+ /**
116
+ * Resolve a hex to the nearest DS token, or fall back to the literal WITH a
117
+ * marker. Never invents a token name and never silently ships a hex.
118
+ */
119
+ export function resolveColor(
120
+ hex: string,
121
+ tokens: readonly DsToken[],
122
+ threshold = DEFAULT_TOKEN_THRESHOLD
123
+ ): ResolvedColor {
124
+ let best: DsToken | null = null;
125
+ let bestDist = Number.POSITIVE_INFINITY;
126
+ for (const t of tokens) {
127
+ const d = perceptualDistance(hex, t.hex);
128
+ if (d < bestDist) {
129
+ bestDist = d;
130
+ best = t;
131
+ }
132
+ }
133
+ if (best && bestDist <= threshold) {
134
+ return { value: `var(${best.name})`, token: best.name };
135
+ }
136
+ return { value: hex, marker: NO_TOKEN_MARKER };
137
+ }
138
+
139
+ // ── Paints / effects / type ─────────────────────────────────────────────────
140
+
141
+ export interface StyleMapOptions {
142
+ tokens: readonly DsToken[];
143
+ threshold?: number;
144
+ }
145
+
146
+ export interface MappedStyle {
147
+ /** CSS property → value. Every value has already passed its grammar. */
148
+ declarations: Record<string, string>;
149
+ /** Properties dropped because a value failed its grammar (reported). */
150
+ rejected: string[];
151
+ /** Properties that shipped a literal because no token was near (reported). */
152
+ unTokenized: string[];
153
+ }
154
+
155
+ function emptyStyle(): MappedStyle {
156
+ return { declarations: {}, rejected: [], unTokenized: [] };
157
+ }
158
+
159
+ function firstVisible<T extends { visible: boolean }>(list: readonly T[] | undefined): T | null {
160
+ for (const p of list ?? []) if (p.visible) return p;
161
+ return null;
162
+ }
163
+
164
+ /** A linear gradient → the CSS equivalent. Angle is derived from the handles. */
165
+ function gradientCss(paint: FigmaPaint, opts: StyleMapOptions, out: MappedStyle): string | null {
166
+ const stops = paint.gradientStops;
167
+ if (!stops || stops.length < 2) return null;
168
+ const parts: string[] = [];
169
+ for (const s of stops) {
170
+ const resolved = resolveColor(figmaColorToHex(s.color), opts.tokens, opts.threshold);
171
+ if (!isValidColorValue(resolved.value)) return null;
172
+ if (resolved.marker) out.unTokenized.push('background-image');
173
+ const pct = Math.max(0, Math.min(100, Math.round(s.position * 100)));
174
+ parts.push(`${resolved.value} ${pct}%`);
175
+ }
176
+ const handles = paint.gradientHandlePositions;
177
+ let angle = 180;
178
+ if (handles && handles.length >= 2) {
179
+ const dx = handles[1].x - handles[0].x;
180
+ const dy = handles[1].y - handles[0].y;
181
+ angle = Math.round((Math.atan2(dy, dx) * 180) / Math.PI + 90);
182
+ }
183
+ return `linear-gradient(${angle}deg, ${parts.join(', ')})`;
184
+ }
185
+
186
+ /** Effects → `box-shadow`. Every layer is strict-numeric; no free-text capture. */
187
+ function shadowCss(effects: readonly FigmaEffect[], opts: StyleMapOptions): string | null {
188
+ const layers: string[] = [];
189
+ for (const e of effects) {
190
+ if (!e.visible) continue;
191
+ if (e.type !== 'DROP_SHADOW' && e.type !== 'INNER_SHADOW') continue;
192
+ const c = e.color ? figmaColorToHex(e.color) : '#000000';
193
+ const resolved = resolveColor(c, opts.tokens, opts.threshold);
194
+ if (!isValidColorValue(resolved.value)) continue;
195
+ const x = Math.round(e.offset?.x ?? 0);
196
+ const y = Math.round(e.offset?.y ?? 0);
197
+ const blur = Math.round(e.radius ?? 0);
198
+ const spread = Math.round(e.spread ?? 0);
199
+ if ([x, y, blur, spread].some((n) => !Number.isFinite(n) || Math.abs(n) > 512)) continue;
200
+ const inset = e.type === 'INNER_SHADOW' ? 'inset ' : '';
201
+ layers.push(`${inset}${x}px ${y}px ${blur}px ${spread}px ${resolved.value}`);
202
+ }
203
+ return layers.length ? layers.join(', ') : null;
204
+ }
205
+
206
+ /** A node's fills/strokes/effects/corner radius → CSS declarations. */
207
+ export function mapNodeStyle(node: FigmaNode, opts: StyleMapOptions): MappedStyle {
208
+ const out = emptyStyle();
209
+ const set = (prop: string, value: string, validator: (v: string) => boolean) => {
210
+ if (!validator(value)) {
211
+ out.rejected.push(prop);
212
+ return;
213
+ }
214
+ out.declarations[prop] = value;
215
+ };
216
+
217
+ const fill = firstVisible(node.fills);
218
+ if (fill) {
219
+ if (fill.type === 'SOLID' && fill.color) {
220
+ const resolved = resolveColor(figmaColorToHex(fill.color), opts.tokens, opts.threshold);
221
+ if (resolved.marker) out.unTokenized.push('background');
222
+ set('background', resolved.value, isValidColorValue);
223
+ } else if (fill.type.startsWith('GRADIENT')) {
224
+ const css = gradientCss(fill, opts, out);
225
+ // A gradient is not a colour value — it has its own (bounded) shape,
226
+ // built entirely from already-validated colour values plus integers.
227
+ // camelCase, NOT `background-image`: these declarations are emitted into
228
+ // a JSX style OBJECT, where a hyphenated key is a syntax error that takes
229
+ // the whole canvas down. One gradient anywhere on a page was enough to
230
+ // make the file unparseable.
231
+ if (css) out.declarations.backgroundImage = css;
232
+ else out.rejected.push('background-image');
233
+ }
234
+ }
235
+
236
+ const stroke = firstVisible(node.strokes);
237
+ if (stroke?.type === 'SOLID' && stroke.color) {
238
+ const resolved = resolveColor(figmaColorToHex(stroke.color), opts.tokens, opts.threshold);
239
+ if (resolved.marker) out.unTokenized.push('border-color');
240
+ const w = Math.round(node.strokeWeight ?? 1);
241
+ if (Number.isFinite(w) && w > 0 && w <= 64 && isValidColorValue(resolved.value)) {
242
+ out.declarations.border = `${w}px solid ${resolved.value}`;
243
+ } else {
244
+ out.rejected.push('border');
245
+ }
246
+ }
247
+
248
+ if (node.cornerRadius !== undefined) {
249
+ set('borderRadius', `${Math.round(node.cornerRadius)}px`, (v) => isValidDimension(v, 512));
250
+ }
251
+
252
+ if (node.effects?.length) {
253
+ const css = shadowCss(node.effects, opts);
254
+ if (css) out.declarations.boxShadow = css;
255
+ }
256
+
257
+ // Node opacity is carried ONLY when it is not a hiding mechanism. D6b makes
258
+ // the translator guarantee visibility, so an opacity that would render the
259
+ // subtree effectively invisible is dropped rather than reproduced.
260
+ if (node.opacity !== undefined && node.opacity < 1) {
261
+ if (node.opacity >= 0.15) out.declarations.opacity = String(Number(node.opacity.toFixed(3)));
262
+ else out.rejected.push('opacity');
263
+ }
264
+
265
+ return out;
266
+ }
267
+
268
+ /** A TEXT node's `style` block → CSS declarations. */
269
+ export function mapTypeStyle(
270
+ style: FigmaTypeStyle | undefined,
271
+ opts: StyleMapOptions
272
+ ): MappedStyle {
273
+ const out = emptyStyle();
274
+ if (!style) return out;
275
+
276
+ if (style.fontSize !== undefined) {
277
+ const v = `${Math.round(style.fontSize)}px`;
278
+ if (isValidDimension(v, FONT_SIZE_MAX)) out.declarations.fontSize = v;
279
+ else out.rejected.push('fontSize');
280
+ }
281
+ if (style.fontWeight !== undefined) {
282
+ const w = Math.round(style.fontWeight);
283
+ if (w >= 100 && w <= 1000) out.declarations.fontWeight = String(w);
284
+ else out.rejected.push('fontWeight');
285
+ }
286
+ if (style.lineHeightPx !== undefined) {
287
+ const v = `${Math.round(style.lineHeightPx)}px`;
288
+ if (isValidDimension(v, FONT_SIZE_MAX)) out.declarations.lineHeight = v;
289
+ else out.rejected.push('lineHeight');
290
+ }
291
+ if (style.letterSpacing !== undefined && style.letterSpacing !== 0) {
292
+ const v = `${style.letterSpacing.toFixed(2)}px`;
293
+ if (isValidDimension(v, 64)) out.declarations.letterSpacing = v;
294
+ else out.rejected.push('letterSpacing');
295
+ }
296
+ if (style.textAlignHorizontal) {
297
+ const align = style.textAlignHorizontal.toLowerCase();
298
+ if (['left', 'center', 'right', 'justified'].includes(align)) {
299
+ out.declarations.textAlign = align === 'justified' ? 'justify' : align;
300
+ }
301
+ }
302
+ if (style.textCase === 'UPPER') out.declarations.textTransform = 'uppercase';
303
+ if (style.textCase === 'LOWER') out.declarations.textTransform = 'lowercase';
304
+ if (style.textDecoration === 'UNDERLINE') out.declarations.textDecoration = 'underline';
305
+ if (style.textDecoration === 'STRIKETHROUGH') out.declarations.textDecoration = 'line-through';
306
+
307
+ // FONT FAMILY IS DELIBERATELY NOT CARRIED. A Figma font name is free text
308
+ // from the document, and DDR-172 Decision 4's font grammar exists precisely
309
+ // because a family value reaches a live stylesheet. An imported canvas
310
+ // inherits the DS's own type stack instead — which is also the editability
311
+ // choice: an imported frame should look like it belongs to the project.
312
+
313
+ return out;
314
+ }
315
+
316
+ /** Auto-layout → flex. The thing that makes the handle surfaces work at all. */
317
+ export function mapAutoLayout(node: FigmaNode): Record<string, string> {
318
+ if (node.layoutMode !== 'HORIZONTAL' && node.layoutMode !== 'VERTICAL') return {};
319
+ const out: Record<string, string> = {
320
+ display: 'flex',
321
+ flexDirection: node.layoutMode === 'HORIZONTAL' ? 'row' : 'column',
322
+ };
323
+ if (node.itemSpacing) out.gap = `${Math.round(node.itemSpacing)}px`;
324
+ const pad = [node.paddingTop, node.paddingRight, node.paddingBottom, node.paddingLeft];
325
+ if (pad.some((p) => p !== undefined && p !== 0)) {
326
+ out.padding = pad.map((p) => `${Math.round(p ?? 0)}px`).join(' ');
327
+ }
328
+ // `Object.create(null)`: these are indexed by DOCUMENT-CONTROLLED strings, so a
329
+ // plain literal returns `Object.prototype.constructor` (a FUNCTION) for
330
+ // `primaryAxisAlignItems: "constructor"`, which then stringifies into an
331
+ // emitted style value (review F11). D5 mandates this shape for exactly this.
332
+ const MAIN: Record<string, string> = Object.assign(Object.create(null), {
333
+ MIN: 'flex-start',
334
+ CENTER: 'center',
335
+ MAX: 'flex-end',
336
+ SPACE_BETWEEN: 'space-between',
337
+ });
338
+ const CROSS: Record<string, string> = Object.assign(Object.create(null), {
339
+ MIN: 'flex-start',
340
+ CENTER: 'center',
341
+ MAX: 'flex-end',
342
+ BASELINE: 'baseline',
343
+ });
344
+ if (node.primaryAxisAlignItems && MAIN[node.primaryAxisAlignItems]) {
345
+ out.justifyContent = MAIN[node.primaryAxisAlignItems];
346
+ }
347
+ if (node.counterAxisAlignItems && CROSS[node.counterAxisAlignItems]) {
348
+ out.alignItems = CROSS[node.counterAxisAlignItems];
349
+ }
350
+ if (node.layoutWrap === 'WRAP') out.flexWrap = 'wrap';
351
+ return out;
352
+ }