@scattered-light/loomiere 0.4.0 → 0.5.0

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 (58) hide show
  1. package/README.md +3 -1
  2. package/bin/loomiere.mjs +3 -1
  3. package/build/build-tokens.mjs +180 -35
  4. package/exporters/design-system.mjs +7 -0
  5. package/exporters/figma.mjs +545 -478
  6. package/exporters/index.mjs +4 -0
  7. package/figma/README.md +24 -8
  8. package/figma/library/README.md +92 -0
  9. package/figma/library/check/known-differences.md +20 -0
  10. package/figma/library/check/mojave.json +246 -0
  11. package/figma/library/kit/base.js +89 -0
  12. package/figma/library/kit/components.js +95 -0
  13. package/figma/library/kit/docs.js +37 -0
  14. package/figma/library/kit/fingerprint.js +143 -0
  15. package/figma/library/pages/02-palettes.js +40 -0
  16. package/figma/library/pages/03-color.js +126 -0
  17. package/figma/library/pages/04-type.js +56 -0
  18. package/figma/library/pages/05-scale.js +58 -0
  19. package/figma/library/pages/06-size.js +111 -0
  20. package/figma/library/pages/07-elevation-focus-motion.js +108 -0
  21. package/figma/library/pages/08-themes.js +36 -0
  22. package/figma/library/pages/09-fields/01-input.js +55 -0
  23. package/figma/library/pages/09-fields/02-checks.js +82 -0
  24. package/figma/library/pages/09-fields/03-field.js +32 -0
  25. package/figma/library/pages/09-fields/04-more-fields.js +144 -0
  26. package/figma/library/pages/09-fields/05-labels.js +14 -0
  27. package/figma/library/pages/09-fields/06-doc.js +75 -0
  28. package/figma/library/pages/10-buttons/01-buttons.js +97 -0
  29. package/figma/library/pages/10-buttons/02-doc.js +55 -0
  30. package/figma/library/pages/11-popups/01-popups.js +166 -0
  31. package/figma/library/pages/11-popups/02-more-popups.js +90 -0
  32. package/figma/library/pages/11-popups/03-labels.js +14 -0
  33. package/figma/library/pages/11-popups/04-doc.js +89 -0
  34. package/figma/library/pages/12-overlays/01-overlays.js +135 -0
  35. package/figma/library/pages/12-overlays/02-labels.js +10 -0
  36. package/figma/library/pages/12-overlays/03-doc.js +80 -0
  37. package/figma/library/pages/13-disclosure/01-disclosure.js +205 -0
  38. package/figma/library/pages/13-disclosure/02-collapsible.js +39 -0
  39. package/figma/library/pages/13-disclosure/03-labels.js +16 -0
  40. package/figma/library/pages/13-disclosure/04-doc.js +91 -0
  41. package/figma/library/pages/14-display/01-display.js +165 -0
  42. package/figma/library/pages/14-display/02-more-display.js +55 -0
  43. package/figma/library/pages/14-display/03-labels.js +14 -0
  44. package/figma/library/pages/14-display/04-doc.js +96 -0
  45. package/figma/library/pages/15-cover.js +87 -0
  46. package/figma/library/run-order.json +21 -0
  47. package/figma/library/tools/compare.mjs +11 -0
  48. package/figma/library/tools/diff-snippet.js +15 -0
  49. package/figma/library/tools/kit-script.mjs +21 -0
  50. package/figma/library/tools/run.mjs +27 -0
  51. package/figma/library/tools/runs.mjs +34 -0
  52. package/generator/brand.schema.json +83 -0
  53. package/generator/generate.mjs +26 -6
  54. package/generator/looks.mjs +62 -0
  55. package/lib/contrast.mjs +24 -6
  56. package/lib/figma.mjs +58 -23
  57. package/package.json +2 -1
  58. package/tokens/README.md +26 -0
@@ -0,0 +1,37 @@
1
+ // The kit for doc pages: everything in base.js, plus instances of the page's component sets,
2
+ // spec tables, example stages, captions and do/don't columns. Stored as ('loomiere', 'docKit').
3
+ var L = await new (Object.getPrototypeOf(async function () {}).constructor)('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
4
+ var fill = L.fill, paint = L.paint, AL = L.AL, add = L.add, T = L.T, hrow = L.hrow, divider = L.divider;
5
+ var sets = {}; figma.currentPage.findAllWithCriteria({ types: ['COMPONENT_SET'] }).forEach(function (s) { sets[s.name] = s; });
6
+ function inst(set, name, props) {
7
+ var c = sets[set].children.filter(function (x) { return x.name === name; })[0]; if (!c) throw new Error(set + ' ' + name);
8
+ var i = c.createInstance();
9
+ if (props) { var p = {}; Object.keys(i.componentProperties).forEach(function (k) { var base = k.split('#')[0]; if (props[base] !== undefined) p[k] = props[base]; }); i.setProperties(p); }
10
+ return i;
11
+ }
12
+ async function notes(parent, rows) {
13
+ var tbl = AL('VERTICAL', { name: 'Spec' }); add(parent, [tbl]); tbl.layoutSizingHorizontal = 'FILL';
14
+ var hd = hrow(tbl); hd.paddingTop = 0; divider(hd);
15
+ add(hd, [await T('Part', 'eyebrow-condense', 'Theme:text/tertiary', 180), await T('Token', 'eyebrow-condense', 'Theme:text/tertiary', 340), await T('Why', 'eyebrow-condense', 'Theme:text/tertiary', 640)]);
16
+ for (var i = 0; i < rows.length; i++) {
17
+ var r = hrow(tbl); r.counterAxisAlignItems = 'MIN'; r.paddingTop = r.paddingBottom = 10; divider(r);
18
+ add(r, [await T(rows[i][0], 'subtitle-base', 'Theme:text/primary', 180), await T(rows[i][1], 'code-condense', 'Theme:text/accent', 340), await T(rows[i][2] || ' ', 'body-condense', 'Theme:text/secondary', 640)]);
19
+ }
20
+ }
21
+ function stage(parent, name) {
22
+ var s = AL('HORIZONTAL', { name: name || 'Examples', itemSpacing: 32, counterAxisAlignItems: 'MIN', paddingTop: 32, paddingBottom: 32, paddingLeft: 32, paddingRight: 32, layoutWrap: 'WRAP', counterAxisSpacing: 24 });
23
+ fill(s, 'Theme:fill/subtle'); s.cornerRadius = 12; add(parent, [s]); s.layoutSizingHorizontal = 'FILL'; return s;
24
+ }
25
+ async function captioned(node, cap) { var c = AL('VERTICAL', { name: cap, itemSpacing: 10 }); add(c, [node, await T(cap, 'body-condense', 'Theme:text/tertiary')]); return c; }
26
+ async function usage(parent, dos, donts) {
27
+ var r = AL('HORIZONTAL', { name: 'Usage', itemSpacing: 24 }); add(parent, [r]); r.layoutSizingHorizontal = 'FILL';
28
+ var pairs = [['Use it for', dos, 'Theme:status/success'], ['Avoid', donts, 'Theme:status/error']];
29
+ for (var i = 0; i < 2; i++) {
30
+ var col = AL('VERTICAL', { name: pairs[i][0], itemSpacing: 8, paddingLeft: 16 }); add(r, [col]); col.layoutSizingHorizontal = 'FILL';
31
+ col.strokes = [paint(pairs[i][2])]; col.strokeLeftWeight = 3; col.strokeTopWeight = col.strokeRightWeight = col.strokeBottomWeight = 0;
32
+ add(col, [await T(pairs[i][0], 'subtitle-base')]);
33
+ for (var j = 0; j < pairs[i][1].length; j++) { var t = await T(pairs[i][1][j], 'body-base', 'Theme:text/secondary'); add(col, [t]); t.layoutSizingHorizontal = 'FILL'; t.textAutoResize = 'HEIGHT'; }
34
+ }
35
+ }
36
+ function overflow(doc) { var over = []; doc.findAll(function (n) { return n.type === 'TEXT'; }).forEach(function (t) { var b = t.absoluteBoundingBox, p = doc.absoluteBoundingBox; if (b.x + b.width > p.x + p.width + 1) over.push(t.characters.slice(0, 30)); }); return over; }
37
+ return Object.assign({}, L, { sets: sets, inst: inst, notes: notes, stage: stage, captioned: captioned, usage: usage, overflow: overflow });
@@ -0,0 +1,143 @@
1
+ // Fingerprints for the library: a short hash for each top-level frame, component set and doc
2
+ // section on a page, built from what the layer looks like (sizes, layout, paints by variable
3
+ // name, text by style name, instances by the component they show), never from node IDs. Two
4
+ // files built the same way give the same hashes, so a rebuild can be checked against Mojave
5
+ // without reading the layers out. Stored as ('loomiere', 'fingerprint'). lines(node) gives the
6
+ // flat "path | prop=value" records behind a hash, for finding what differs.
7
+ var cols = await figma.variables.getLocalVariableCollectionsAsync();
8
+ var CN = {}; cols.forEach(function (c) { CN[c.id] = c; });
9
+ var VK = {}; (await figma.variables.getLocalVariablesAsync()).forEach(function (x) { VK[x.id] = CN[x.variableCollectionId].name + ':' + x.name; });
10
+ var SN = {};
11
+ (await figma.getLocalTextStylesAsync()).concat(await figma.getLocalEffectStylesAsync(), await figma.getLocalPaintStylesAsync()).forEach(function (s) { SN[s.id] = s.name; });
12
+ function r(x) { return Math.round(x * 2) / 2; }
13
+ function hex(c) { var p = function (x) { return ('0' + Math.round(x * 255).toString(16)).slice(-2); }; return p(c.r) + p(c.g) + p(c.b) + (c.a === undefined || c.a === 1 ? '' : p(c.a)); }
14
+ function vk(a) { return a ? (VK[a.id] || 'remote') : ''; }
15
+ function paints(list) {
16
+ if (!Array.isArray(list)) return 'mixed';
17
+ return list.map(function (p) {
18
+ var b = p.boundVariables && p.boundVariables.color;
19
+ return p.type + (p.visible === false ? '!hidden' : '') + (b ? '@' + vk(b) : p.color ? '#' + hex(p.color) : '') + (p.opacity !== undefined && p.opacity !== 1 ? '*' + r(p.opacity * 100) : '');
20
+ }).join(',');
21
+ }
22
+ function effects(n) {
23
+ if (n.effectStyleId) return 'style:' + (SN[n.effectStyleId] || '?');
24
+ return n.effects.map(function (e) {
25
+ var b = e.boundVariables && e.boundVariables.color;
26
+ return e.type + (e.visible === false ? '!hidden' : '') + (e.offset ? ' ' + e.offset.x + ',' + e.offset.y : '') + ' ' + e.radius + (e.spread ? ' s' + e.spread : '') + (b ? '@' + vk(b) : e.color ? '#' + hex(e.color) : '');
27
+ }).join(',');
28
+ }
29
+ var LAYOUT = ['layoutMode', 'primaryAxisSizingMode', 'counterAxisSizingMode', 'primaryAxisAlignItems', 'counterAxisAlignItems', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight', 'itemSpacing', 'counterAxisSpacing', 'layoutWrap', 'strokesIncludedInLayout', 'itemReverseZIndex', 'clipsContent'];
30
+ var CHILD = ['layoutSizingHorizontal', 'layoutSizingVertical', 'layoutGrow', 'layoutAlign', 'layoutPositioning', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight'];
31
+ var TEXT = ['characters', 'textAutoResize', 'textAlignHorizontal', 'textAlignVertical', 'textTruncation', 'maxLines'];
32
+ function props(n) {
33
+ var o = {};
34
+ o.t = n.type;
35
+ if (n.visible === false) o.visible = false;
36
+ if ('opacity' in n && n.opacity !== 1) o.opacity = r(n.opacity * 100);
37
+ if ('rotation' in n && n.rotation) o.rotation = r(n.rotation);
38
+ if (n.isMask) o.mask = true;
39
+ if ('width' in n) o.size = r(n.width) + 'x' + r(n.height);
40
+ var p = n.parent;
41
+ if (p && (p.type === 'PAGE' || !('layoutMode' in p) || p.layoutMode === 'NONE' || n.layoutPositioning === 'ABSOLUTE')) o.xy = r(n.x) + ',' + r(n.y);
42
+ if ('layoutMode' in n && n.type !== 'TEXT') LAYOUT.forEach(function (k) { if (k in n && n[k] !== undefined) o[k] = n[k]; });
43
+ if (n.layoutMode === 'NONE') LAYOUT.slice(1, 15).forEach(function (k) { if (k !== 'clipsContent') delete o[k]; });
44
+ if (p && 'layoutMode' in p && p.layoutMode !== 'NONE') CHILD.forEach(function (k) { if (k in n && n[k] !== null && n[k] !== undefined && n[k] !== 0 && n[k] !== 'INHERIT' && n[k] !== 'AUTO') o[k] = n[k]; });
45
+ if ('fills' in n) { var f = paints(n.fills); if (f) o.fills = f; }
46
+ if ('strokes' in n && n.strokes.length) {
47
+ o.strokes = paints(n.strokes); o.strokeAlign = n.strokeAlign;
48
+ o.strokeWeight = n.strokeWeight === figma.mixed ? [n.strokeTopWeight, n.strokeRightWeight, n.strokeBottomWeight, n.strokeLeftWeight].join(' ') : n.strokeWeight;
49
+ if (n.dashPattern && n.dashPattern.length) o.dash = n.dashPattern.join(' ');
50
+ if (n.type === 'VECTOR') { o.cap = n.strokeCap; o.join = n.strokeJoin; }
51
+ }
52
+ if ('topLeftRadius' in n) { var rr = [n.topLeftRadius, n.topRightRadius, n.bottomRightRadius, n.bottomLeftRadius]; if (rr.some(function (x) { return x; })) o.radius = rr.join(' '); }
53
+ if ('effects' in n && (n.effects.length || n.effectStyleId)) o.effects = effects(n);
54
+ if (n.explicitVariableModes) { var m = []; Object.keys(n.explicitVariableModes).forEach(function (cid) { var c = CN[cid]; if (!c) return m.push('remote'); var md = c.modes.filter(function (x) { return x.modeId === n.explicitVariableModes[cid]; })[0]; m.push(c.name + '=' + (md ? md.name : '?')); }); if (m.length) o.modes = m.sort().join(','); }
55
+ if (n.boundVariables) { var b = []; Object.keys(n.boundVariables).forEach(function (k) { if (k === 'fills' || k === 'strokes' || k === 'effects' || k === 'textRangeFills') return; var a = n.boundVariables[k]; b.push(k + '@' + (Array.isArray(a) ? a.map(vk).join('+') : vk(a))); }); if (b.length) o.bound = b.sort().join(','); }
56
+ if (n.type === 'TEXT') {
57
+ TEXT.forEach(function (k) { if (n[k] !== undefined && n[k] !== null && n[k] !== 'DISABLED') o[k] = n[k]; });
58
+ o.style = n.textStyleId === figma.mixed ? 'mixed' : SN[n.textStyleId] || 'none';
59
+ if (o.style === 'none' || o.style === 'mixed') { o.font = n.fontName === figma.mixed ? 'mixed' : n.fontName.family + ' ' + n.fontName.style; o.fontSize = n.fontSize; }
60
+ }
61
+ if (n.type === 'VECTOR') o.paths = n.vectorPaths.map(function (v) { return v.data; }).join(';');
62
+ if (n.componentPropertyReferences) { var cr = []; Object.keys(n.componentPropertyReferences).forEach(function (k) { cr.push(k + '>' + n.componentPropertyReferences[k].split('#')[0]); }); if (cr.length) o.refs = cr.sort().join(','); }
63
+ if (n.type === 'COMPONENT_SET' || (n.type === 'COMPONENT' && (!p || p.type !== 'COMPONENT_SET'))) {
64
+ var d = n.componentPropertyDefinitions;
65
+ o.props = Object.keys(d).map(function (k) { var x = d[k]; return k.split('#')[0] + ':' + x.type + '=' + x.defaultValue + (x.variantOptions ? '[' + x.variantOptions.join('|') + ']' : ''); }).sort().join(';');
66
+ }
67
+ if ((n.type === 'COMPONENT' || n.type === 'COMPONENT_SET') && n.description) o.desc = n.description;
68
+ return o;
69
+ }
70
+ async function mainOf(n) {
71
+ var m = await n.getMainComponentAsync();
72
+ if (!m) return 'missing';
73
+ return (m.parent && m.parent.type === 'COMPONENT_SET' ? m.parent.name + '/' : '') + m.name;
74
+ }
75
+ function h32(s) { var h = 0x811c9dc5; for (var i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; } return ('0000000' + h.toString(16)).slice(-8); }
76
+ async function rec(n) {
77
+ var o = props(n);
78
+ if (n.type === 'INSTANCE') {
79
+ o.main = await mainOf(n);
80
+ var cp = n.componentProperties; o.inst = Object.keys(cp).map(function (k) { return k.split('#')[0] + '=' + cp[k].value; }).sort().join(';');
81
+ }
82
+ return o;
83
+ }
84
+ function line(o) { return Object.keys(o).map(function (k) { return k + '=' + (typeof o[k] === 'string' ? o[k].replace(/\n/g, '\\n') : o[k]); }).join(' '); }
85
+ async function hash(n, skipSets) {
86
+ var s = line(await rec(n));
87
+ if (skipSets && n.type === 'COMPONENT_SET') return h32(s);
88
+ if ('children' in n) for (var i = 0; i < n.children.length; i++) s += '|' + (await hash(n.children[i], skipSets));
89
+ return h32(s);
90
+ }
91
+ async function lines(n, path, out, depth) {
92
+ out = out || []; path = path || n.name;
93
+ out.push(path + ' | ' + line(await rec(n)));
94
+ if ('children' in n && (depth === undefined || depth > 0)) {
95
+ var seen = {};
96
+ for (var i = 0; i < n.children.length; i++) { var c = n.children[i]; seen[c.name] = (seen[c.name] || 0) + 1; await lines(c, path + ' > ' + c.name + (seen[c.name] > 1 ? '#' + seen[c.name] : ''), out, depth === undefined ? undefined : depth - 1); }
97
+ }
98
+ return out;
99
+ }
100
+ // One 16-character entry per layer (hash of its path, hash of its record), joined, so two files
101
+ // can be compared line by line and only the lines that differ read out.
102
+ async function lineHashes(n) { var ls = await lines(n); return ls.map(function (l) { var i = l.indexOf(' | '); return h32(l.slice(0, i)) + h32(l.slice(i + 3)); }).join(''); }
103
+ // What differs between two layers: the properties that differ, matched by layer path, at most
104
+ // `max` entries. The two can be on different pages of one file (a check page and the real one).
105
+ async function recs(n, path, out) {
106
+ out[path] = await rec(n);
107
+ if ('children' in n) { var seen = {}; for (var i = 0; i < n.children.length; i++) { var c = n.children[i]; seen[c.name] = (seen[c.name] || 0) + 1; await recs(c, path + ' > ' + c.name + (seen[c.name] > 1 ? '#' + seen[c.name] : ''), out); } }
108
+ return out;
109
+ }
110
+ async function diff(a, b, max) {
111
+ var ra = await recs(a, '', {}), rb = await recs(b, '', {}), out = [];
112
+ Object.keys(ra).forEach(function (k) {
113
+ if (!rb[k]) return out.push(k + ': missing in check');
114
+ var d = [];
115
+ Object.keys(ra[k]).concat(Object.keys(rb[k])).forEach(function (p) { if (d.some(function (x) { return x.indexOf(p + ':') === 0; })) return; if (String(ra[k][p]) !== String(rb[k][p])) d.push(p + ': ' + ra[k][p] + ' → ' + rb[k][p]); });
116
+ if (d.length) out.push(k + ' | ' + d.join(' ; '));
117
+ });
118
+ Object.keys(rb).forEach(function (k) { if (!ra[k]) out.push(k + ': only in check'); });
119
+ return { count: out.length, layers: Object.keys(ra).length + '/' + Object.keys(rb).length, first: out.slice(0, max || 12) };
120
+ }
121
+ // The checked units on the current page: top-level frames, each component set (and lone
122
+ // component), and each section of a doc frame.
123
+ async function units() {
124
+ var out = {};
125
+ var put = async function (key, n, skipSets) { var k = key, i = 2; while (out[k]) k = key + '#' + i++; out[k] = await hash(n, skipSets); };
126
+ var pg = figma.currentPage;
127
+ for (var i = 0; i < pg.children.length; i++) {
128
+ var top = pg.children[i];
129
+ if (top.name === 'Components' && 'children' in top) {
130
+ var sets = top.findAll(function (x) { return x.type === 'COMPONENT_SET' || (x.type === 'COMPONENT' && x.parent.type !== 'COMPONENT_SET'); });
131
+ for (var j = 0; j < sets.length; j++) await put('set:' + sets[j].name, sets[j]);
132
+ var shell = line(props(top)) + '|' + top.children.map(function (c) { return c.name + ':' + line(props(c)); }).join('|');
133
+ out['frame:Components(shell)'] = h32(shell);
134
+ // Each set's wrapper (header, row and column labels), without the set itself.
135
+ for (var w = 0; w < top.children.length; w++) if (top.children[w].type === 'FRAME') await put('wrap:' + top.children[w].name, top.children[w], true);
136
+ } else if (top.type === 'FRAME' && 'children' in top && top.children.length > 1 && top.children[0].name === 'Header') {
137
+ out['doc:' + top.name + '(frame)'] = h32(line(props(top)));
138
+ for (var k = 0; k < top.children.length; k++) await put('doc:' + top.name + '/' + top.children[k].name, top.children[k]);
139
+ } else await put(top.type.toLowerCase() + ':' + top.name, top);
140
+ }
141
+ return out;
142
+ }
143
+ return { hash: hash, lines: lines, lineHashes: lineHashes, diff: diff, units: units, rec: rec };
@@ -0,0 +1,40 @@
1
+ // Palettes page: every Brand ramp, one column per color vision group.
2
+ var AF = Object.getPrototypeOf(async function () {}).constructor;
3
+ var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
4
+ var V = L.V, v = L.v, fill = L.fill, stroke = L.stroke, AL = L.AL, add = L.add, T = L.T, page = L.page, docFrame = L.docFrame, section = L.section, hex = L.hex;
5
+ var p = await page('Palettes');
6
+ var doc = await docFrame('Palettes', 'FOUNDATIONS', 'Palettes', 'The raw ramps the brand file makes, in the hidden Brand collection. Designs never use these directly: Theme points its jobs at them. Each column is one Brand group. Default is what most people see; the four vision groups are the same ramps retargeted so each color stays distinct for that kind of color vision. A hex marked • differs from Default.');
7
+ doc.x = 0; doc.y = 400;
8
+ var G = [['default', 'Default'], ['deuteranopia', 'Deuteranopia'], ['protanopia', 'Protanopia'], ['tritanopia', 'Tritanopia'], ['achromatopsia', 'Achromatopsia']];
9
+ var PALS = [
10
+ ['neutral', 'Neutral', 'Grays toned by the brand. Surfaces, text and borders come from here.'],
11
+ ['primary', 'Primary', 'The main brand color. Usually the accent.'],
12
+ ['secondary', 'Secondary', ''], ['tertiary', 'Tertiary', ''], ['quaternary', 'Quaternary', ''],
13
+ ['info', 'Info', 'Status palettes. 500 is always at least 4.5:1 on paper.'], ['success', 'Success', ''], ['warning', 'Warning', ''], ['error', 'Error', ''],
14
+ ['data', 'Data colors', 'Named categorical colors for charts. Theme data/1 to data/20 point at these in an order each theme can change.']
15
+ ];
16
+ function steps(pal) { return Object.keys(V).filter(function (k) { return k.indexOf('Brand:default/color/' + pal + '/') === 0 && k.split('/').length === 4; }).map(function (k) { return k.split('/')[3]; }); }
17
+ function val(k) { return V[k] ? V[k].resolveForConsumer(doc).value : null; }
18
+ for (var i = 0; i < PALS.length; i++) {
19
+ var pal = PALS[i][0];
20
+ var sec = await section(doc, PALS[i][1], PALS[i][2] || null);
21
+ var grid = AL('HORIZONTAL', { name: 'Columns', itemSpacing: 16 });
22
+ add(sec, [grid]); grid.layoutSizingHorizontal = 'FILL';
23
+ var st = steps(pal);
24
+ for (var g = 0; g < G.length; g++) {
25
+ var col = AL('VERTICAL', { name: G[g][1], itemSpacing: 4 });
26
+ add(grid, [col]); col.layoutSizingHorizontal = 'FILL';
27
+ add(col, [await T(G[g][1].toUpperCase(), 'eyebrow-condense', 'Theme:text/tertiary')]);
28
+ for (var s = 0; s < st.length; s++) {
29
+ var key = 'Brand:' + G[g][0] + '/color/' + pal + '/' + st[s];
30
+ var base = 'Brand:default/color/' + pal + '/' + st[s];
31
+ if (!V[key]) key = base;
32
+ var row = AL('HORIZONTAL', { name: st[s], itemSpacing: 10, counterAxisAlignItems: 'CENTER' });
33
+ add(col, [row]); row.layoutSizingHorizontal = 'FILL';
34
+ var sw = figma.createRectangle(); sw.name = 'Swatch'; sw.resize(56, 28); sw.cornerRadius = 4; fill(sw, key); stroke(sw, 'Theme:border/subtle', 1, 'OUTSIDE');
35
+ var h = hex(val(key)), diff = g > 0 && h !== hex(val(base));
36
+ add(row, [sw, await T(st[s], 'subtitle-condense', 'Theme:text/primary', 70), await T((diff ? '• ' : '') + h, 'code-condense', diff ? 'Theme:text/accent' : 'Theme:text/secondary')]);
37
+ }
38
+ }
39
+ }
40
+ return doc.id;
@@ -0,0 +1,126 @@
1
+ // Color page: how the collections stack, every Theme job in light and dark, and the
2
+ // Status × Emphasis grid.
3
+ var AF = Object.getPrototypeOf(async function () {}).constructor;
4
+ var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
5
+ var V = L.V, v = L.v, fill = L.fill, stroke = L.stroke, bind = L.bind, radius = L.radius, AL = L.AL, add = L.add, T = L.T, mode = L.mode, page = L.page, docFrame = L.docFrame, section = L.section, hrow = L.hrow, divider = L.divider, unescape = L.unescape, ratio = L.ratio, resolved = L.resolved;
6
+ await page('Color');
7
+ var doc = await docFrame('Color', 'FOUNDATIONS', 'Color', 'Every color comes from a few brand inputs: seed colors, paper and ink. They are worked into ramps (see the Palettes page), and Theme names them by job. Pick a color by what it does, never by how it looks, and light, dark, high contrast and the vision themes follow on their own.');
8
+ doc.x = 0; doc.y = 400;
9
+ var groups = [
10
+ ['surface', 'Surfaces', 'Backgrounds, from the page up. Each one lifts content one level, and dark mode lifts by getting lighter.'],
11
+ ['text', 'Text', 'Body text is the darkest neutral. Secondary and tertiary step back but still pass AA on every surface. The ratio shown is against the page.'],
12
+ ['border', 'Borders', 'Control borders are 3:1 against the page so a field can be found before it is filled. Subtle lines are for inside a surface only.'],
13
+ ['fill', 'Fills', 'Neutral fills for tracks, chips, hovered rows and scrollbar thumbs.'],
14
+ ['interactive', 'Interactive', 'The accent that says "you can act on this". Tints are the quiet washes behind ghost controls and selected items.'],
15
+ ['status', 'Status', 'Error, warning, success and info, each with a darker step for text and a light step for backgrounds.'],
16
+ ['focus', 'Focus and overlay', 'The focus ring and the scrims behind modals.'],
17
+ ['content', 'Long-form content', 'Colors for prose: articles, help pages and rich text. Links, quotes, inline code, highlights, selection and scrollbars. Most point at a job above, so content follows the theme with the rest of the UI.']
18
+ ];
19
+ var extra = { focus: ['overlay', 'modal'] };
20
+ var created = [doc.id];
21
+ for (var g = 0; g < groups.length; g++) {
22
+ var sec = await section(doc, groups[g][1], groups[g][2]);
23
+ var prefixes = [groups[g][0]].concat(extra[groups[g][0]] || []);
24
+ var head = hrow(sec); head.paddingTop = 0; head.paddingBottom = 8; divider(head);
25
+ add(head, [await T('LIGHT', 'eyebrow-condense', 'Theme:text/tertiary', 120), await T('DARK', 'eyebrow-condense', 'Theme:text/tertiary', 120), await T('TOKEN', 'eyebrow-condense', 'Theme:text/tertiary', 320), await T('USE IT FOR', 'eyebrow-condense', 'Theme:text/tertiary', 400)]);
26
+ head.children[3].layoutSizingHorizontal = 'FILL';
27
+ var keys = Object.keys(V).filter(function (k) { return prefixes.some(function (p) { return k.indexOf('Theme:' + p + '/') === 0; }); });
28
+ for (var r = 0; r < keys.length; r++) {
29
+ var k = keys[r], name = k.slice(6), kind = groups[g][0];
30
+ var row = hrow(sec); divider(row); row.name = name;
31
+ var notes = [];
32
+ for (var s = 0; s < 2; s++) {
33
+ var cell = AL('HORIZONTAL', { name: s ? 'Dark' : 'Light', primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER', cornerRadius: 8, paddingTop: 12, paddingBottom: 12 });
34
+ fill(cell, 'Theme:surface/canvas'); stroke(cell, 'Theme:border/subtle', 1);
35
+ mode(cell, 'Theme', s ? 'Dark' : 'Light');
36
+ add(row, [cell]); cell.resize(120, 64); cell.counterAxisSizingMode = 'FIXED'; cell.primaryAxisSizingMode = 'FIXED';
37
+ var sw, bgKey = 'Theme:surface/canvas';
38
+ if (name.indexOf('text/') === 0 || name === 'content/prose-color' || name === 'content/link-color' || name === 'content/code-text' || name === 'content/blockquote-cite') {
39
+ sw = await T('Aa', 'title-amplify', k);
40
+ if (name.indexOf('text/on-') === 0) {
41
+ var bg = { 'text/on-primary': 'Theme:interactive/default', 'text/on-tint': 'Theme:interactive/tint', 'text/on-tint-strong': 'Theme:interactive/tint-active', 'text/on-inverse': 'Theme:fill/inverse', 'text/on-inverse-surface': 'Theme:surface/inverse' }[name];
42
+ fill(cell, bg); bgKey = bg;
43
+ }
44
+ } else if (kind === 'border' || /border|ring/.test(name)) {
45
+ sw = figma.createRectangle(); sw.resize(64, 36); sw.cornerRadius = 6; sw.fills = []; stroke(sw, k, 2);
46
+ } else {
47
+ sw = figma.createRectangle(); sw.resize(64, 36); sw.cornerRadius = 6; fill(sw, k); stroke(sw, 'Theme:border/subtle', 1, 'OUTSIDE');
48
+ }
49
+ sw.name = 'Swatch';
50
+ add(cell, [sw]);
51
+ if (sw.type === 'TEXT') notes.push((s ? 'Dark ' : 'Light ') + ratio(resolved(k, sw), resolved(bgKey, cell)).toFixed(1) + ':1');
52
+ }
53
+ var tok = AL('VERTICAL', { name: 'Token', itemSpacing: 4 });
54
+ add(tok, [await T(name, 'subtitle-base'), await T('var(--sl-' + name.replace(/\//g, '-') + ')', 'code-condense', 'Theme:text/tertiary')]);
55
+ if (notes.length) add(tok, [await T(notes.join(' '), 'body-condense', 'Theme:text/secondary')]);
56
+ add(row, [tok]); tok.resize(320, tok.height); tok.counterAxisSizingMode = 'FIXED';
57
+ var use = await T(unescape(v(k).description), 'body-base', 'Theme:text/secondary', 400);
58
+ add(row, [use]); use.layoutSizingHorizontal = 'FILL';
59
+ }
60
+ created.push(sec.id);
61
+ }
62
+
63
+
64
+ // How the layers fit, as a diagram
65
+ var how = await section(doc, 'How the layers fit', 'Each collection aliases the one before it, so switching a mode on a frame flows down to everything inside. Hidden collections hold raw values; published ones are what you pick and switch.');
66
+ doc.insertChild(1, how);
67
+ async function box(name, state, modes, job) {
68
+ var b = AL('VERTICAL', { name: name, itemSpacing: 6, paddingTop: 16, paddingBottom: 16, paddingLeft: 16, paddingRight: 16, cornerRadius: 8 });
69
+ fill(b, state === 'Hidden' ? 'Theme:surface/tray' : 'Theme:surface/raised');
70
+ stroke(b, state === 'Hidden' ? 'Theme:border/subtle' : 'Theme:border/accent', state === 'Hidden' ? 1 : 2);
71
+ if (state === 'Hidden') b.dashPattern = [4, 4];
72
+ var top = AL('HORIZONTAL', { itemSpacing: 8, counterAxisAlignItems: 'CENTER' });
73
+ add(top, [await T(name, 'subtitle-base'), await T(state.toUpperCase(), 'eyebrow-minimize', state === 'Hidden' ? 'Theme:text/tertiary' : 'Theme:text/accent')]);
74
+ add(b, [top, await T(modes, 'selected-condense', 'Theme:text/primary', 196), await T(job, 'body-condense', 'Theme:text/secondary', 196)]);
75
+ b.resize(228, b.height);
76
+ return b;
77
+ }
78
+ async function arrow() { return await T('→', 'title-amplify', 'Theme:text/tertiary'); }
79
+ async function lane(label, items, note) {
80
+ var l = AL('VERTICAL', { name: label + ' lane', itemSpacing: 12 });
81
+ var row = AL('HORIZONTAL', { name: 'Flow', itemSpacing: 12, counterAxisAlignItems: 'CENTER' });
82
+ for (var i = 0; i < items.length; i++) { if (i) add(row, [await arrow()]); add(row, [await box.apply(null, items[i])]); }
83
+ add(l, [await T(label.toUpperCase(), 'eyebrow-condense', 'Theme:text/tertiary'), row, await T(note, 'body-condense', 'Theme:text/secondary', 1000)]);
84
+ add(how, [l]);
85
+ }
86
+ await lane('Color', [
87
+ ['Brand', 'Hidden', 'One mode per look. Groups: default, deuteranopia, protanopia, tritanopia, achromatopsia, look', 'The ramps and 20 named data colors, a full set per group. look/ holds what changes between looks.'],
88
+ ['Theme', 'Published', 'Light, Dark, Light/Dark high contrast, 4 vision themes', 'The jobs: surface, text, border, fill, interactive, status, pairs, data/1-20.'],
89
+ ['Status', 'Published', 'Default, Info, Success, Warning, Error', 'What a control is about. Default is the brand accent; the rest are status palettes.'],
90
+ ['Emphasis', 'Published', 'Bold, Standard, Subtle, Quiet', 'How loud a control is. Components bind emphasis/bg, fg and border.']
91
+ ], 'Set Theme on the page frame. Set Status and Emphasis on a component. A bold warning button stays bold and warning when the page switches to dark or tritanopia, because both read through Theme.');
92
+ await lane('Size', [
93
+ ['Scale', 'Hidden', 'One mode. Font sizes in desktop, tablet and mobile groups', 'The raw steps: space, radius, stroke, font size. Named by size, not by use.'],
94
+ ['Size', 'Published', 'Desktop, Tablet (768px), Mobile', 'Named uses (radius/control, gap/stack-md) and component sizes, each picking a Scale step.']
95
+ ], 'Set Size on the page frame. Type and spacing follow the screen width, like the fluid sizes in code.');
96
+
97
+ // Status and emphasis grid, light and dark
98
+ var se = await section(doc, 'Status and emphasis', 'Rows are Status modes, columns are Emphasis modes. Every cell is the same layer with two modes set. In code: data-status and data-emphasis. Default status uses the brand accent for bold and standard, and the neutral chrome for subtle and quiet. Brand files can swap any cell for a brand pair.');
99
+ var ST = ['Default', 'Info', 'Success', 'Warning', 'Error'], EM = ['Bold', 'Standard', 'Subtle', 'Quiet'];
100
+ var pair = AL('HORIZONTAL', { name: 'Light and dark', itemSpacing: 24 });
101
+ add(se, [pair]); pair.layoutSizingHorizontal = 'FILL';
102
+ for (var t = 0; t < 2; t++) {
103
+ var g = AL('VERTICAL', { name: t ? 'Dark' : 'Light', itemSpacing: 12, paddingTop: 24, paddingBottom: 24, paddingLeft: 24, paddingRight: 24, cornerRadius: 12 });
104
+ fill(g, 'Theme:surface/canvas'); stroke(g, 'Theme:border/subtle', 1);
105
+ mode(g, 'Theme', t ? 'Dark' : 'Light');
106
+ add(pair, [g]); g.layoutSizingHorizontal = 'FILL';
107
+ var hr = AL('HORIZONTAL', { name: 'Header', itemSpacing: 8 });
108
+ add(g, [hr]); hr.layoutSizingHorizontal = 'FILL';
109
+ add(hr, [await T(t ? 'DARK' : 'LIGHT', 'eyebrow-condense', 'Theme:text/accent', 80)]);
110
+ for (var e = 0; e < EM.length; e++) { var h = await T(EM[e].toUpperCase(), 'eyebrow-condense', 'Theme:text/tertiary'); add(hr, [h]); h.layoutSizingHorizontal = 'FILL'; }
111
+ for (var s = 0; s < ST.length; s++) {
112
+ var r = AL('HORIZONTAL', { name: ST[s], itemSpacing: 8, counterAxisAlignItems: 'CENTER' });
113
+ add(g, [r]); r.layoutSizingHorizontal = 'FILL';
114
+ add(r, [await T(ST[s], 'subtitle-condense', 'Theme:text/secondary', 80)]);
115
+ for (var e2 = 0; e2 < EM.length; e2++) {
116
+ var c = AL('HORIZONTAL', { name: ST[s] + ' / ' + EM[e2], primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER', paddingTop: 8, paddingBottom: 8, paddingLeft: 12, paddingRight: 12 });
117
+ fill(c, 'Emphasis:emphasis/bg'); stroke(c, 'Emphasis:emphasis/border', 'Size:stroke/default'); radius(c, 'Size:button/radius');
118
+ mode(c, 'Status', ST[s]); mode(c, 'Emphasis', EM[e2]);
119
+ add(c, [await T(ST[s] === 'Default' ? 'Save' : ST[s], 'trigger-base', 'Emphasis:emphasis/fg')]);
120
+ add(r, [c]); c.layoutSizingHorizontal = 'FILL';
121
+ }
122
+ }
123
+ }
124
+ var parts = await T('Each level has six parts: bg, bg-hover, bg-active, fg, border, border-hover. Hover and pressed states bind the -hover and -active parts.', 'body-condense', 'Theme:text/secondary', 820);
125
+ add(se, [parts]);
126
+ return { how: how.id, grid: se.id };
@@ -0,0 +1,56 @@
1
+ var AF = Object.getPrototypeOf(async function () {}).constructor;
2
+ var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
3
+ var V = L.V, COL = L.COL, TS = L.TS, v = L.v, fill = L.fill, AL = L.AL, add = L.add, T = L.T, unescape = L.unescape, mode = L.mode, page = L.page, docFrame = L.docFrame, section = L.section, hrow = L.hrow, divider = L.divider;
4
+ await page('Type');
5
+ var doc = await docFrame('Type', 'FOUNDATIONS', 'Type', 'Every style is named role first, then tier. The role is the job (title, body, label, trigger…); the tier sets the size, named for what it does to the text: minimize, condense, base, magnify, amplify, maximize. Every text layer uses a whole shared style. Each row shows one style at Mobile, Tablet and Desktop, the three Size modes; the samples follow the mode set on their column.');
6
+ doc.x = 0; doc.y = 400;
7
+ var byId = {}; Object.keys(V).forEach(function (k) { byId[V[k].id] = V[k]; });
8
+ var MODES = ['Mobile', 'Tablet', 'Desktop'];
9
+ function sizeIn(style, modeName) {
10
+ var b = style.boundVariables && style.boundVariables.fontSize; if (!b) return style.fontSize;
11
+ var m = COL['Size'].modes.filter(function (x) { return x.name === modeName; })[0].modeId, x = byId[b.id], val = x.valuesByMode[m];
12
+ while (val && val.type === 'VARIABLE_ALIAS') { var t = byId[val.id]; val = t.valuesByMode[m] !== undefined ? t.valuesByMode[m] : t.valuesByMode[Object.keys(t.valuesByMode)[0]]; }
13
+ return Math.round(val * 10) / 10;
14
+ }
15
+ function spec(s) {
16
+ var lh = s.lineHeight.unit === 'PERCENT' ? Math.round(s.lineHeight.value) + '%' : s.lineHeight.unit === 'PIXELS' ? s.lineHeight.value + 'px' : 'auto';
17
+ var sz = MODES.map(function (m) { return sizeIn(s, m); }), same = sz[0] === sz[2];
18
+ return s.fontName.family + ' ' + s.fontName.style + ' · ' + (same ? sz[0] + 'px' : sz.join(' / ') + 'px') + ' · ' + lh;
19
+ }
20
+ var ROLES = [
21
+ ['title', 'Title', 'Names something: a page, a section, a dialog, a card, a toast. Display family. Tiers: maximize for the page head, amplify for dialogs, drawers and sections, magnify for popovers and cards, base for toasts and accordion rows, condense for group headers.', 'Title'],
22
+ ['body', 'Body', 'Running text and UI copy. The default is body-base.', 'Body text and UI copy'],
23
+ ['label', 'Label', 'Form labels.', 'Field label'],
24
+ ['trigger', 'Trigger', 'Buttons, tabs and other things you press.', 'Button label'],
25
+ ['selected', 'Selected', 'The current tab, toggle or nav item: the same size as body, a heavier weight.', 'Selected tab'],
26
+ ['link', 'Link', 'Links in text. The same type as body: a link stands out by color and underline.', 'A link in text'],
27
+ ['subtitle', 'Subtitle', 'Subheads inside running text.', 'Subtitle'],
28
+ ['eyebrow', 'Eyebrow', 'Small uppercase labels above a group.', 'Eyebrow'],
29
+ ['code', 'Code', 'The mono family, so characters line up.', 'const size = 16;']
30
+ ];
31
+ var ORDER = ['maximize', 'amplify', 'magnify', 'base', 'condense', 'minimize'];
32
+ for (var i = 0; i < ROLES.length; i++) {
33
+ var role = ROLES[i][0];
34
+ var names = Object.keys(TS).filter(function (n) { return n.indexOf(role + '-') === 0; });
35
+ names.sort(function (a, b) { return ORDER.indexOf(a.split('-').pop()) - ORDER.indexOf(b.split('-').pop()); });
36
+ var sec = await section(doc, ROLES[i][1], ROLES[i][2]);
37
+ var head = hrow(sec); head.paddingTop = 0; head.paddingBottom = 8; divider(head);
38
+ add(head, [await T('STYLE', 'eyebrow-condense', 'Theme:text/tertiary', 260)]);
39
+ for (var m = 0; m < 3; m++) { var h = await T(MODES[m].toUpperCase(), 'eyebrow-condense', 'Theme:text/tertiary'); add(head, [h]); h.layoutSizingHorizontal = 'FILL'; }
40
+ for (var j = 0; j < names.length; j++) {
41
+ var s = TS[names[j]];
42
+ var row = hrow(sec); divider(row); row.name = names[j]; row.counterAxisAlignItems = 'MIN';
43
+ var lab = AL('VERTICAL', { name: 'Label', itemSpacing: 2 });
44
+ add(lab, [await T(names[j], 'subtitle-base'), await T(spec(s), 'body-condense', 'Theme:text/secondary', 260)]);
45
+ lab.resize(260, lab.height); lab.counterAxisSizingMode = 'FIXED';
46
+ add(row, [lab]);
47
+ var sample = role === 'title' ? { maximize: 'Page head', amplify: 'Dialog title', magnify: 'Card title', base: 'Toast title', condense: 'Group header' }[names[j].split('-').pop()] : ROLES[i][3];
48
+ for (var m = 0; m < 3; m++) {
49
+ var cell = AL('VERTICAL', { name: MODES[m] }); mode(cell, 'Size', MODES[m]);
50
+ add(row, [cell]); cell.layoutSizingHorizontal = 'FILL';
51
+ var t = await T(sample, names[j], role === 'link' ? 'Theme:content/link-color' : 'Theme:text/primary');
52
+ add(cell, [t]); t.layoutSizingHorizontal = 'FILL'; t.textAutoResize = 'HEIGHT';
53
+ }
54
+ }
55
+ }
56
+ return { doc: doc.id, h: Math.round(doc.height) };
@@ -0,0 +1,58 @@
1
+ var AF = Object.getPrototypeOf(async function () {}).constructor;
2
+ var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
3
+ var V = L.V, COL = L.COL, v = L.v, fill = L.fill, stroke = L.stroke, bind = L.bind, radius = L.radius, AL = L.AL, add = L.add, T = L.T, page = L.page, docFrame = L.docFrame, section = L.section, hrow = L.hrow, divider = L.divider;
4
+ var p = await page('Scale');
5
+ function raw(k) { var x = v(k); return Math.round(x.valuesByMode[Object.keys(x.valuesByMode)[0]] * 100) / 100; }
6
+ var unit = raw('Scale:space/1');
7
+ var doc = await docFrame('Scale', 'FOUNDATIONS', 'Scale', 'The raw steps, in the hidden Scale collection. They are named by size, not by use: space/4 is four base units, radius/md is a middle roundness. Designs pick from the Size page instead, where each use names the step it takes. Change the brand\'s base unit (' + unit + 'px) and every step moves together.');
8
+ doc.x = 0; doc.y = 400;
9
+ function keysOf(prefix) { return Object.keys(V).filter(function (k) { return k.indexOf('Scale:' + prefix + '/') === 0; }); }
10
+ async function label(name, value, w) {
11
+ var box = AL('VERTICAL', { name: 'Label', itemSpacing: 2 });
12
+ add(box, [await T(name, 'subtitle-base'), await T(value, 'body-condense', 'Theme:text/secondary')]);
13
+ box.resize(w || 200, box.height); box.counterAxisSizingMode = 'FIXED';
14
+ return box;
15
+ }
16
+ var sp = await section(doc, 'Space', 'Multiples of the base unit. The name is the multiple, so space/4 is always 4 × ' + unit + 'px, whatever the brand.');
17
+ var sk = keysOf('space').sort(function (a, b) { return parseFloat(a.split('/')[1].replace('-', '.')) - parseFloat(b.split('/')[1].replace('-', '.')); });
18
+ for (var i = 0; i < sk.length; i++) {
19
+ var n = sk[i].split('/')[1].replace('-', '.');
20
+ var row = hrow(sp); row.paddingTop = 3; row.paddingBottom = 3; row.name = sk[i];
21
+ add(row, [await label(sk[i].slice(6), raw(sk[i]) + 'px · ' + n + ' × unit')]);
22
+ var bar = figma.createRectangle(); bar.name = 'Bar'; bar.cornerRadius = 2; fill(bar, 'Theme:interactive/tint-active'); bar.resize(10, 14);
23
+ add(row, [bar]); bind(bar, 'width', sk[i]);
24
+ }
25
+ var rad = await section(doc, 'Radius', 'Roundness from square to the brand\'s largest. The Size page picks one for each job: controls, surfaces, modals.');
26
+ var rg = AL('HORIZONTAL', { name: 'Radii', itemSpacing: 24, layoutWrap: 'WRAP', counterAxisSpacing: 32 }); add(rad, [rg]); rg.layoutSizingHorizontal = 'FILL';
27
+ var rk = keysOf('radius');
28
+ for (var i = 0; i < rk.length; i++) {
29
+ var cell = AL('VERTICAL', { name: rk[i], itemSpacing: 12 });
30
+ var sq = figma.createRectangle(); sq.name = 'Shape'; sq.resize(96, 72); fill(sq, 'Theme:interactive/tint'); stroke(sq, 'Theme:border/accent', 1.5);
31
+ radius(sq, rk[i]); add(cell, [sq, await label(rk[i].slice(6), raw(rk[i]) + 'px', 120)]);
32
+ add(rg, [cell]);
33
+ }
34
+ var st = await section(doc, 'Stroke', 'Line weights from thin to thick.');
35
+ var stk = keysOf('stroke');
36
+ for (var i = 0; i < stk.length; i++) {
37
+ var row = hrow(st); row.name = stk[i];
38
+ add(row, [await label(stk[i].slice(6), raw(stk[i]) + 'px')]);
39
+ var ln = figma.createRectangle(); ln.name = 'Line'; ln.resize(320, 40); ln.fills = []; ln.cornerRadius = 4; stroke(ln, 'Theme:border/strong', stk[i]);
40
+ add(row, [ln]);
41
+ }
42
+ var fs = await section(doc, 'Font size', 'A type scale from the brand\'s base size and ratio. Below the base, sizes are fixed so small text never shrinks further. From the base up they are fluid in code; Figma keeps three snapshots: desktop (1280px wide), tablet (768px) and mobile (320px). Each sample is bound to its step.');
43
+ var head = hrow(fs); head.paddingTop = 0; divider(head);
44
+ var W = ['desktop', 'tablet', 'mobile'];
45
+ add(head, [await T('STEP', 'eyebrow-condense', 'Theme:text/tertiary', 120)]);
46
+ for (var w = 0; w < 3; w++) { var h = await T(W[w].toUpperCase(), 'eyebrow-condense', 'Theme:text/tertiary'); add(head, [h]); h.layoutSizingHorizontal = 'FILL'; }
47
+ var steps = keysOf('desktop/font-size').map(function (k) { return k.split('/').pop(); });
48
+ for (var i = 0; i < steps.length; i++) {
49
+ var row = hrow(fs); divider(row); row.name = steps[i]; row.counterAxisAlignItems = 'MAX';
50
+ add(row, [await T(steps[i], 'subtitle-base', 'Theme:text/primary', 120)]);
51
+ for (var w = 0; w < 3; w++) {
52
+ var k = 'Scale:' + W[w] + '/font-size/' + steps[i];
53
+ var cell = AL('VERTICAL', { name: W[w], itemSpacing: 4 }); add(row, [cell]); cell.layoutSizingHorizontal = 'FILL';
54
+ var s = await T('Ag', 'title-base'); bind(s, 'fontSize', k);
55
+ add(cell, [s, await T(raw(k) + 'px', 'body-condense', 'Theme:text/secondary')]);
56
+ }
57
+ }
58
+ return { doc: doc.id, h: Math.round(doc.height) };
@@ -0,0 +1,111 @@
1
+ var AF = Object.getPrototypeOf(async function () {}).constructor;
2
+ var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
3
+ var V = L.V, COL = L.COL, v = L.v, fill = L.fill, stroke = L.stroke, bind = L.bind, radius = L.radius, AL = L.AL, add = L.add, T = L.T, unescape = L.unescape, page = L.page, docFrame = L.docFrame, section = L.section, hrow = L.hrow, divider = L.divider;
4
+ var p = await page('Size');
5
+ var doc = await docFrame('Size', 'FOUNDATIONS', 'Size', 'What designs use: each size named by its job. Every job picks a step from the Scale page, shown in the tag beside it, so a brand change moves everything in step. The Size collection has Desktop, Tablet and Mobile modes: set it on a frame and type and spacing follow. Figures are Desktop / Tablet / Mobile when they differ.');
6
+ doc.name = 'Size'; doc.x = 0; doc.y = 400;
7
+ var byId = {}, keyOf = {}; Object.keys(V).forEach(function (k) { byId[V[k].id] = V[k]; keyOf[V[k].id] = k; });
8
+ var MODES = ['Desktop', 'Tablet', 'Mobile'];
9
+ function num(k, modeName) {
10
+ var m = COL['Size'].modes.filter(function (x) { return x.name === modeName; })[0].modeId;
11
+ var x = v(k), val = x.valuesByMode[m];
12
+ while (val && val.type === 'VARIABLE_ALIAS') { var t = byId[val.id]; val = t.valuesByMode[m] !== undefined ? t.valuesByMode[m] : t.valuesByMode[Object.keys(t.valuesByMode)[0]]; }
13
+ return Math.round(val * 100) / 100;
14
+ }
15
+ function fig(k) { var a = MODES.map(function (m) { return num(k, m); }); return a[0] === a[1] && a[1] === a[2] ? a[0] + '' : a.join(' / '); }
16
+ // The Scale step a use lands on, following aliases through other uses.
17
+ function step(k) {
18
+ var m = COL['Size'].modes[0].modeId, val = v(k).valuesByMode[m], via = [];
19
+ while (val && val.type === 'VARIABLE_ALIAS') {
20
+ var key = keyOf[val.id];
21
+ if (key.indexOf('Scale:') === 0) return { step: key.slice(6).replace(/^desktop\//, ''), via: via };
22
+ via.push(key.slice(5)); var t = byId[val.id]; val = t.valuesByMode[m];
23
+ }
24
+ return { step: null, via: via };
25
+ }
26
+ async function tag(k) {
27
+ var s = step(k), box = AL('HORIZONTAL', { name: 'Tags', itemSpacing: 6, counterAxisAlignItems: 'CENTER' });
28
+ if (s.via.length) add(box, [await T('via ' + s.via.join(' → '), 'body-condense', 'Theme:text/tertiary')]);
29
+ var chip = AL('HORIZONTAL', { name: 'Scale step', paddingTop: 2, paddingBottom: 2, paddingLeft: 8, paddingRight: 8, cornerRadius: 999 });
30
+ fill(chip, s.step ? 'Theme:interactive/tint' : 'Theme:surface/tray');
31
+ add(chip, [await T(s.step ? 'Scale: ' + s.step : 'fixed', 'selected-condense', s.step ? 'Theme:text/on-tint' : 'Theme:text/secondary')]);
32
+ add(box, [chip]);
33
+ return box;
34
+ }
35
+ function keysOf(prefix) { return Object.keys(V).filter(function (k) { return k.indexOf('Size:' + prefix + '/') === 0; }); }
36
+ async function label(k, w) {
37
+ var box = AL('VERTICAL', { name: 'Label', itemSpacing: 4 });
38
+ add(box, [await T(k.slice(5), 'subtitle-base'), await T(fig(k) + ' px', 'body-condense', 'Theme:text/secondary'), await tag(k)]);
39
+ box.resize(w || 260, box.height); box.counterAxisSizingMode = 'FIXED';
40
+ return box;
41
+ }
42
+ async function desc(k, w) { var d = unescape(v(k).description); return d ? await T(d, 'body-condense', 'Theme:text/tertiary', w || 480) : null; }
43
+ var sp = await section(doc, 'Space', 'Spacing jobs for stacks and rows. Component padding and gaps are in the reference at the end.');
44
+ var spk = keysOf('space');
45
+ for (var i = 0; i < spk.length; i++) {
46
+ var row = hrow(sp); row.paddingTop = 6; row.paddingBottom = 6; row.name = spk[i];
47
+ add(row, [await label(spk[i])]);
48
+ var bar = figma.createRectangle(); bar.name = 'Bar'; bar.cornerRadius = 2; fill(bar, 'Theme:interactive/tint-active'); bar.resize(10, 16);
49
+ add(row, [bar]); bind(bar, 'width', spk[i]);
50
+ add(row, [await desc(spk[i])]);
51
+ }
52
+ var rad = await section(doc, 'Radius', 'Corners by job. A control, a surface and a modal each have their own radius, so changing the brand\'s roundness never makes a nested corner look wrong.');
53
+ var rg = AL('HORIZONTAL', { name: 'Radii', itemSpacing: 24, layoutWrap: 'WRAP', counterAxisSpacing: 32 }); add(rad, [rg]); rg.layoutSizingHorizontal = 'FILL';
54
+ var rk = keysOf('radius');
55
+ for (var i = 0; i < rk.length; i++) {
56
+ var cell = AL('VERTICAL', { name: rk[i], itemSpacing: 12 });
57
+ var sq = figma.createRectangle(); sq.name = 'Shape'; sq.resize(96, 72); fill(sq, 'Theme:interactive/tint'); stroke(sq, 'Theme:border/accent', 1.5);
58
+ radius(sq, rk[i]); add(cell, [sq, await label(rk[i], 220), await desc(rk[i], 220)]);
59
+ add(rg, [cell]);
60
+ }
61
+ var st = await section(doc, 'Stroke', 'Line weights by job. Default is for control borders, emphasis for selected and focus states.');
62
+ var sk = keysOf('stroke');
63
+ for (var i = 0; i < sk.length; i++) {
64
+ var row = hrow(st); row.name = sk[i];
65
+ add(row, [await label(sk[i])]);
66
+ var ln = figma.createRectangle(); ln.name = 'Line'; ln.resize(240, 40); ln.fills = []; ln.cornerRadius = 4; stroke(ln, 'Theme:border/strong', sk[i]);
67
+ add(row, [ln, await desc(sk[i])]);
68
+ }
69
+ var ct = await section(doc, 'Controls and icons', 'Four control heights, each paired with its text tier and icon size: xs with minimize, sm with condense, md with base, lg with magnify. Every control in a row shares one height.');
70
+ var hs = ['xs', 'sm', 'md', 'lg'], tiers = { xs: 'trigger-minimize', sm: 'trigger-condense', md: 'trigger-base', lg: 'trigger-magnify' };
71
+ var crow = AL('HORIZONTAL', { name: 'Heights', itemSpacing: 24, counterAxisAlignItems: 'MAX' }); add(ct, [crow]);
72
+ for (var i = 0; i < hs.length; i++) {
73
+ var col = AL('VERTICAL', { name: hs[i], itemSpacing: 12 });
74
+ var c = AL('HORIZONTAL', { name: 'Control', counterAxisAlignItems: 'CENTER' });
75
+ fill(c, 'Theme:surface/canvas'); stroke(c, 'Theme:border/control', 'Size:stroke/default'); radius(c, 'Size:radius/control');
76
+ bind(c, ['paddingLeft', 'paddingRight'], 'Size:control/padding-inline' + (hs[i] === 'md' ? '' : '-' + hs[i]));
77
+ add(c, [await T('Control ' + hs[i], tiers[hs[i]])]);
78
+ add(col, [c]); c.counterAxisSizingMode = 'FIXED'; bind(c, 'height', 'Size:control/height-' + hs[i]);
79
+ add(col, [await label('Size:control/height-' + hs[i], 200)]);
80
+ add(crow, [col]);
81
+ }
82
+ var irow = AL('HORIZONTAL', { name: 'Icons', itemSpacing: 24, counterAxisAlignItems: 'MAX' }); add(ct, [irow]);
83
+ var ik = keysOf('icon-size');
84
+ for (var i = 0; i < ik.length; i++) {
85
+ var col = AL('VERTICAL', { name: ik[i], itemSpacing: 12 });
86
+ var sq = figma.createRectangle(); sq.name = 'Icon box'; sq.cornerRadius = 2; fill(sq, 'Theme:text/primary'); sq.resize(16, 16);
87
+ add(col, [sq]); bind(sq, 'width', ik[i]); bind(sq, 'height', ik[i]);
88
+ add(col, [await label(ik[i], 200)]);
89
+ add(irow, [col]);
90
+ }
91
+ var ref = await section(doc, 'Spec reference', 'Every size job, grouped by the concept that uses it, with the Scale step it lands on. Components share these, so a field, a select and a number field are the same height and padding by construction.');
92
+ var groups = {};
93
+ Object.keys(V).forEach(function (k) { if (k.indexOf('Size:') !== 0) return; var g = k.slice(5).split('/')[0]; if (['space', 'radius', 'stroke', 'font-size', 'icon-size'].indexOf(g) !== -1) return; (groups[g] = groups[g] || []).push(k); });
94
+ var gnames = Object.keys(groups);
95
+ var cols3 = AL('HORIZONTAL', { name: 'Columns', itemSpacing: 40 }); add(ref, [cols3]); cols3.layoutSizingHorizontal = 'FILL';
96
+ var colsN = [AL('VERTICAL', { itemSpacing: 32 }), AL('VERTICAL', { itemSpacing: 32 }), AL('VERTICAL', { itemSpacing: 32 })];
97
+ colsN.forEach(function (c, i) { c.name = 'Column ' + (i + 1); add(cols3, [c]); c.layoutSizingHorizontal = 'FILL'; });
98
+ var heights = [0, 0, 0];
99
+ for (var g = 0; g < gnames.length; g++) {
100
+ var target = heights.indexOf(Math.min.apply(null, heights));
101
+ var box = AL('VERTICAL', { name: gnames[g], itemSpacing: 4 }); add(colsN[target], [box]); box.layoutSizingHorizontal = 'FILL';
102
+ add(box, [await T(gnames[g].toUpperCase(), 'eyebrow-condense', 'Theme:text/accent')]);
103
+ for (var j = 0; j < groups[gnames[g]].length; j++) {
104
+ var k = groups[gnames[g]][j], s = step(k);
105
+ var r = AL('HORIZONTAL', { name: k, itemSpacing: 8, paddingTop: 4, paddingBottom: 4 }); divider(r); add(box, [r]); r.layoutSizingHorizontal = 'FILL';
106
+ var nm = await T(k.slice(5 + gnames[g].length + 1), 'body-condense', 'Theme:text/primary'); add(r, [nm]); nm.layoutSizingHorizontal = 'FILL';
107
+ add(r, [await T(fig(k), 'body-condense', 'Theme:text/secondary'), await T(s.step || 'fixed', 'selected-condense', s.step ? 'Theme:text/accent' : 'Theme:text/tertiary')]);
108
+ }
109
+ heights[target] += groups[gnames[g]].length + 2;
110
+ }
111
+ return { doc: doc.id, h: Math.round(doc.height), groups: gnames.length };