@scattered-light/loomiere 0.5.3 → 0.7.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 (39) hide show
  1. package/build/build-tokens.mjs +33 -6
  2. package/exporters/chakra.mjs +74 -0
  3. package/exporters/component-styles.mjs +64 -0
  4. package/exporters/design-system.mjs +23 -7
  5. package/exporters/mui.mjs +60 -0
  6. package/figma/library/pages/18-custom/01-custom.js +57 -0
  7. package/figma/library/pages/18-custom/02-labels.js +7 -0
  8. package/figma/library/pages/18-custom/03-doc.js +28 -0
  9. package/figma/library/pages/18-custom/04-date-picker.js +327 -0
  10. package/figma/library/pages/18-custom/05-date-picker-labels.js +9 -0
  11. package/figma/library/pages/18-custom/06-date-picker-doc.js +25 -0
  12. package/figma/library/pages/18-custom/07-money-field.js +107 -0
  13. package/figma/library/pages/18-custom/08-money-field-labels.js +8 -0
  14. package/figma/library/pages/18-custom/09-money-field-doc.js +101 -0
  15. package/figma/library/pages/18-custom/10-splitter.js +100 -0
  16. package/figma/library/pages/18-custom/11-splitter-labels.js +8 -0
  17. package/figma/library/pages/18-custom/12-splitter-doc.js +92 -0
  18. package/figma/library/pages/18-custom/13-empty-state.js +114 -0
  19. package/figma/library/pages/18-custom/14-empty-state-labels.js +8 -0
  20. package/figma/library/pages/18-custom/15-empty-state-doc.js +58 -0
  21. package/figma/library/pages/18-custom/16-collection.js +255 -0
  22. package/figma/library/pages/18-custom/17-collection-doc.js +43 -0
  23. package/figma/library/run-order.json +1 -0
  24. package/lib/contrast.mjs +17 -0
  25. package/package.json +1 -1
  26. package/tokens/README.md +10 -3
  27. package/tokens/component/badge.json +53 -0
  28. package/tokens/component/collection.json +49 -0
  29. package/tokens/component/list-detail.json +29 -0
  30. package/tokens/component/money-field.json +9 -0
  31. package/tokens/component/splitter.json +25 -0
  32. package/tokens/semantic/accessibility.json +4 -0
  33. package/tokens/semantic/calendar.json +75 -0
  34. package/tokens/semantic/empty.json +79 -0
  35. package/tokens/semantic/field.json +4 -0
  36. package/tokens/semantic/popup.json +2 -2
  37. package/tokens/semantic/table.json +8 -0
  38. package/tools/brand-builder/builder.js +45 -0
  39. package/tools/brand-builder/index.html +6 -0
@@ -0,0 +1,100 @@
1
+ // Custom: Splitter handle and the list-detail header. Run after the Badge scripts.
2
+ var AF = Object.getPrototypeOf(async function () {}).constructor;
3
+ var C = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'compHelpers'))(figma);
4
+ var V = C.V, COL = C.COL, v = C.v, fill = C.fill, stroke = C.stroke, bind = C.bind, radius = C.radius, add = C.add, T = C.T, mode = C.mode, page = C.page;
5
+ var icon = C.icon, comp = C.comp, textProp = C.textProp, boolProp = C.boolProp, place = C.place, removeOld = C.removeOld, AL = C.AL;
6
+ await page('Custom');
7
+ var ES = {}; (await figma.getLocalEffectStylesAsync()).forEach(function (s) { ES[s.name] = s; });
8
+ var XP = 'M 4 4 L 12 12 M 12 4 L 4 12';
9
+ function num(k) { var x = v(k); var m = COL[k.split(':')[0]].modes[0].modeId, val = x.valuesByMode[m]; while (val && val.type === 'VARIABLE_ALIAS') { var t = Object.values(V).find(function (y) { return y.id === val.id; }); val = t.valuesByMode[Object.keys(t.valuesByMode)[0]]; } return val; }
10
+ function frameSet(set) { fill(set, 'Theme:surface/canvas'); set.strokes = [{ type: 'SOLID', color: { r: 0.592, g: 0.278, b: 1 } }]; set.dashPattern = [6, 4]; set.strokeWeight = 1; set.cornerRadius = 8; set.clipsContent = false; }
11
+ function grid(set, cols, colW, rowH) {
12
+ set.layoutMode = 'NONE';
13
+ set.children.forEach(function (c, i) { c.x = 24 + (i % cols) * colW; c.y = 24 + Math.floor(i / cols) * rowH; });
14
+ frameSet(set);
15
+ set.resize(24 + cols * colW, 24 + Math.ceil(set.children.length / cols) * rowH);
16
+ }
17
+ function setP(i, props) { var p = {}; Object.keys(i.componentProperties).forEach(function (k) { var b = k.split('#')[0]; if (props[b] !== undefined) p[k] = props[b]; }); i.setProperties(p); return i; }
18
+
19
+ var ORIENT = ['Horizontal', 'Vertical'];
20
+ var STATES = ['Default', 'Hover', 'Dragging', 'Focus', 'Disabled'];
21
+ var GRIPS = ['Off', 'On'];
22
+ var hit = num('Size:splitter/handle-hit-size');
23
+ var lineW = num('Size:splitter/handle-size');
24
+ var grip = num('Size:splitter/grip-size');
25
+ removeOld('Splitter handle');
26
+ var handles = [];
27
+ for (var o = 0; o < ORIENT.length; o++) for (var s = 0; s < STATES.length; s++) for (var g = 0; g < GRIPS.length; g++) {
28
+ var orient = ORIENT[o], st = STATES[s], gripOn = GRIPS[g] === 'On';
29
+ var horizontal = orient === 'Horizontal';
30
+ var w = horizontal ? hit : 200, h = horizontal ? 160 : hit;
31
+ var c = comp('Orientation=' + orient + ', State=' + st + ', Grip=' + GRIPS[g]);
32
+ c.layoutMode = 'NONE';
33
+ c.resize(w, h);
34
+ if (horizontal) bind(c, 'width', 'Size:splitter/handle-hit-size');
35
+ else bind(c, 'height', 'Size:splitter/handle-hit-size');
36
+ var strong = st === 'Hover' || st === 'Dragging';
37
+ var line = figma.createRectangle(); line.name = 'Line';
38
+ fill(line, strong ? 'Theme:border/strong' : 'Theme:border/default');
39
+ if (horizontal) { line.resize(lineW, h); line.x = Math.round((w - lineW) / 2); line.y = 0; bind(line, 'width', 'Size:splitter/handle-size'); }
40
+ else { line.resize(w, lineW); line.x = 0; line.y = Math.round((h - lineW) / 2); bind(line, 'height', 'Size:splitter/handle-size'); }
41
+ c.appendChild(line);
42
+ if (gripOn) {
43
+ var dots = figma.createFrame(); dots.name = 'Grip'; dots.fills = []; dots.layoutMode = horizontal ? 'VERTICAL' : 'HORIZONTAL'; dots.itemSpacing = grip; dots.primaryAxisAlignItems = 'CENTER';
44
+ for (var d = 0; d < 3; d++) {
45
+ var dot = figma.createEllipse(); dot.resize(grip, grip); bind(dot, ['width', 'height'], 'Size:splitter/grip-size'); fill(dot, 'Theme:border/strong'); dots.appendChild(dot);
46
+ }
47
+ c.appendChild(dots);
48
+ dots.x = Math.round((w - dots.width) / 2); dots.y = Math.round((h - dots.height) / 2);
49
+ }
50
+ if (st === 'Focus') await c.setEffectStyleIdAsync(ES['focus/ring'].id);
51
+ handles.push(c);
52
+ }
53
+ var handleSet = figma.combineAsVariants(handles, figma.currentPage); handleSet.name = 'Splitter handle';
54
+ grid(handleSet, GRIPS.length, 240, 200);
55
+ handleSet.description = 'The separator between splitter panels. Orientation is the splitter\'s: Horizontal is a vertical bar. The line is stroke/default (border/strong on hover and while dragging). The component is the touch target, handle-hit-size across. Grip adds three dots. Focus uses the shared focus ring. Disabled does not move.';
56
+ place(handleSet);
57
+
58
+ var BTN = await C.setOn('Buttons', 'Button');
59
+ function button(variant, props) {
60
+ var child = BTN.children.find(function (n) { return n.name === variant; });
61
+ if (!child) throw new Error('Button ' + variant);
62
+ return setP(child.createInstance(), props || {});
63
+ }
64
+ function closeX() {
65
+ var iconSize = num('Size:icon-button/size');
66
+ var b = AL('HORIZONTAL', { name: 'Close', primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER' });
67
+ b.resize(iconSize, iconSize); b.primaryAxisSizingMode = 'FIXED'; b.counterAxisSizingMode = 'FIXED';
68
+ bind(b, ['width', 'height'], 'Size:icon-button/size'); radius(b, 'Size:icon-button/radius');
69
+ add(b, [icon('X', XP, 16, 'Theme:text/tertiary')]);
70
+ return b;
71
+ }
72
+ removeOld('Detail header');
73
+ var headers = [];
74
+ for (var lead of ['Close', 'Back']) {
75
+ var head = comp('Leading=' + lead);
76
+ head.counterAxisAlignItems = 'CENTER';
77
+ // 420 is the row width. There is no width token: the detail's share is a percent.
78
+ // resize() flips the axis modes. Height then hugs the padding and the title.
79
+ head.resize(420, num('Size:list-detail/header-padding'));
80
+ head.primaryAxisSizingMode = 'FIXED';
81
+ head.counterAxisSizingMode = 'AUTO';
82
+ fill(head, 'Theme:surface/raised');
83
+ bind(head, 'itemSpacing', 'Size:list-detail/header-gap');
84
+ bind(head, ['paddingLeft', 'paddingRight', 'paddingTop', 'paddingBottom'], 'Size:list-detail/header-padding');
85
+ if (lead === 'Back') add(head, [button('Emphasis=Quiet, Size=md, State=Default, Content=Label', { Label: 'Back' })]);
86
+ var title = await T('Transaction 1042', 'title-condense', 'Theme:text/primary'); title.name = 'Title';
87
+ add(head, [title]); title.layoutSizingHorizontal = 'FILL';
88
+ var actions = AL('HORIZONTAL', { name: 'Actions', counterAxisAlignItems: 'CENTER' });
89
+ bind(actions, 'itemSpacing', 'Size:space/2');
90
+ add(actions, [button('Emphasis=Quiet, Size=md, State=Default, Content=Label', { Label: 'Edit' })]);
91
+ add(head, [actions]);
92
+ if (lead === 'Close') add(head, [closeX()]);
93
+ headers.push(head);
94
+ }
95
+ var headerSet = figma.combineAsVariants(headers, figma.currentPage); headerSet.name = 'Detail header';
96
+ grid(headerSet, 1, 520, 96);
97
+ textProp(headerSet, 'Title', 'Transaction 1042', 'Title');
98
+ boolProp(headerSet, 'Actions', true, 'Actions');
99
+ headerSet.description = 'The header of a list detail pane. Title is title-condense, because this pane shares the view with the list. Actions is an optional slot (a quiet Button). Close is the quiet X; Back replaces it on the narrow page. Drawer mode uses the Drawer title and close instead of this header.';
100
+ place(headerSet);
@@ -0,0 +1,8 @@
1
+ // Custom: headers for the Splitter handle and Detail header sets. Run after 10.
2
+ var AF = Object.getPrototypeOf(async function () {}).constructor;
3
+ var C = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'compHelpers'))(figma);
4
+ await C.page('Custom');
5
+ return await C.labelSets({
6
+ 'Splitter handle': 'Drag or use the arrow keys to resize the panel beside it. The line is one stroke. The component is the touch target. Grip is off unless the handle asks for it.',
7
+ 'Detail header': 'The title of an open item, optional actions, and Close. Back replaces Close when the detail takes the whole page. A drawer uses the Drawer title instead.',
8
+ });
@@ -0,0 +1,92 @@
1
+ // Splitter and list detail doc. Run after 10 and 11. Sits clear of the Badge doc.
2
+ var AF = Object.getPrototypeOf(async function () {}).constructor;
3
+ var K = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
4
+ await K.page('Custom');
5
+ var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docKit'))(figma);
6
+ var AL = L.AL, add = L.add, T = L.T, fill = L.fill, docFrame = L.docFrame, section = L.section, hrow = L.hrow, divider = L.divider;
7
+ var inst = L.inst, notes = L.notes, captioned = L.captioned, usage = L.usage, stage = L.stage;
8
+ function setP(i, props) { var p = {}; Object.keys(i.componentProperties).forEach(function (k) { var b = k.split('#')[0]; if (props[b] !== undefined) p[k] = props[b]; }); i.setProperties(p); return i; }
9
+ async function fromPage(pageName, setName, variant, props) {
10
+ var set = await L.setOn(pageName, setName);
11
+ await K.page('Custom');
12
+ var child = set.children.find(function (n) { return n.name === variant; });
13
+ if (!child) throw new Error(setName + ' ' + variant);
14
+ return setP(child.createInstance(), props || {});
15
+ }
16
+
17
+ var doc = await docFrame('Splitter', 'COMPONENTS', 'Splitter and list detail', 'Splitter resizes regions that sit side by side or stacked. List detail shows one item next to its list, and becomes a drawer or a page when the container is narrow. Use Dialog when the detail has to block the list.');
18
+ doc.x = 1600; doc.y = 400;
19
+
20
+ var pick = await section(doc, 'Choosing', 'Start from whether the person is resizing regions, or reading one item.');
21
+ var picks = [
22
+ ['Resize two or more regions', 'Splitter', 'A sidebar, a documents column, a stacked editor.'],
23
+ ['Read one item from a list', 'List detail', 'Beside the list when there is room. A drawer or a page when there is not.'],
24
+ ['Block the page for a task', 'Dialog', 'The detail has to be finished before the list can be used.'],
25
+ ];
26
+ var pt = AL('VERTICAL', { name: 'Picker' }); add(pick, [pt]); pt.layoutSizingHorizontal = 'FILL';
27
+ for (var i = 0; i < picks.length; i++) { var r = hrow(pt); divider(r); add(r, [await T(picks[i][0], 'body-base', 'Theme:text/primary', 380), await T(picks[i][1], 'subtitle-base', 'Theme:text/accent', 200), await T(picks[i][2], 'body-condense', 'Theme:text/secondary', 520)]); }
28
+
29
+ var look = await section(doc, 'Splitter handle', 'One stroke, with a touch target around it. Horizontal is a vertical bar. Grip is off unless the handle asks for it.');
30
+ var s1 = stage(look, 'States');
31
+ add(s1, [await captioned(inst('Splitter handle', 'Orientation=Horizontal, State=Default, Grip=Off'), 'Default')]);
32
+ add(s1, [await captioned(inst('Splitter handle', 'Orientation=Horizontal, State=Hover, Grip=Off'), 'Hover')]);
33
+ add(s1, [await captioned(inst('Splitter handle', 'Orientation=Horizontal, State=Dragging, Grip=On'), 'Dragging, grip')]);
34
+ add(s1, [await captioned(inst('Splitter handle', 'Orientation=Vertical, State=Focus, Grip=Off'), 'Focus')]);
35
+ add(s1, [await captioned(inst('Splitter handle', 'Orientation=Vertical, State=Disabled, Grip=Off'), 'Disabled')]);
36
+ await usage(look, ['A sidebar and the page, or a list and its detail.', 'A documents column that can collapse.', 'A stacked editor and preview.'], ['A divider nobody can drag. Use Separator.', 'Resizing text or a single control. Use the control.', 'More than one filler panel. One region takes the leftover space.']);
37
+ await notes(look, [
38
+ ['Line', 'splitter/handle-size · border/default', 'One stroke, the same hairline as Separator.'],
39
+ ['Hit area', 'a11y/min-target · splitter/handle-hit-size', '24px for a pointer, on the next panel. 44px when the pointer is coarse.'],
40
+ ['Hover', 'border/strong', 'Also while dragging, and when the panel is collapsed.'],
41
+ ['Grip', 'splitter/grip-size · border/strong', 'Three dots. Off unless the handle asks for it.'],
42
+ ['Keys', 'splitter/key-step · key-step-large', 'Arrows move 5%. Shift moves 10%.'],
43
+ ['Focus', 'focus/ring', 'The shared ring. Home and End jump to min and max.'],
44
+ ]);
45
+
46
+ var layouts = await section(doc, 'List detail', 'The list stays on the page. The detail sits on surface/raised, with the handle as the only divider. Narrow, it is a drawer or it replaces the list.');
47
+ async function layoutFrame(name, wide) {
48
+ var frame = AL(wide ? 'HORIZONTAL' : 'VERTICAL', { name: name, itemSpacing: 0 });
49
+ frame.resize(wide ? 880 : 420, 360);
50
+ frame.primaryAxisSizingMode = 'FIXED';
51
+ frame.counterAxisSizingMode = 'FIXED';
52
+ frame.clipsContent = true;
53
+ fill(frame, 'Theme:surface/canvas');
54
+ return frame;
55
+ }
56
+ var split = await layoutFrame('Split', true);
57
+ var table = await fromPage('Display', 'Table', 'Content=Data');
58
+ add(split, [table]); table.layoutSizingHorizontal = 'FILL'; table.layoutSizingVertical = 'FILL';
59
+ var bar = inst('Splitter handle', 'Orientation=Horizontal, State=Default, Grip=Off');
60
+ add(split, [bar]); bar.layoutSizingVertical = 'FILL';
61
+ var detail = AL('VERTICAL', { name: 'Detail', itemSpacing: 0 });
62
+ fill(detail, 'Theme:surface/raised');
63
+ detail.resize(360, 360); detail.layoutSizingVertical = 'FILL';
64
+ var header = setP(inst('Detail header', 'Leading=Close'), { Title: 'Transaction 1042' });
65
+ add(detail, [header]); header.layoutSizingHorizontal = 'FILL';
66
+ var body = await T('A note about this transaction. The list stays visible beside it.', 'body-base', 'Theme:text/secondary', 320);
67
+ add(detail, [body]); body.layoutSizingHorizontal = 'FILL';
68
+ add(split, [detail]);
69
+
70
+ var drawerFrame = await layoutFrame('Drawer', false);
71
+ var table2 = await fromPage('Display', 'Table', 'Content=Data');
72
+ add(drawerFrame, [table2]); table2.layoutSizingHorizontal = 'FILL';
73
+ var drawer = await fromPage('Overlays', 'Drawer', 'Side=Right');
74
+ add(drawerFrame, [drawer]);
75
+
76
+ var pageFrame = await layoutFrame('Page', false);
77
+ var back = setP(inst('Detail header', 'Leading=Back'), { Title: 'Transaction 1042', Actions: false });
78
+ add(pageFrame, [back]); back.layoutSizingHorizontal = 'FILL';
79
+ var pageBody = await T('The list stays mounted underneath, so Back returns to the same scroll position.', 'body-base', 'Theme:text/secondary', 360);
80
+ add(pageFrame, [pageBody]); pageBody.layoutSizingHorizontal = 'FILL';
81
+
82
+ var s2 = stage(layouts, 'Layouts');
83
+ add(s2, [await captioned(split, 'Split'), await captioned(drawerFrame, 'Drawer'), await captioned(pageFrame, 'Page')]);
84
+ await notes(layouts, [
85
+ ['Switch', 'list-detail/split-min-width', '720px of this container, not the window.'],
86
+ ['Detail', 'list-detail/detail-default-size', '45%, so the list keeps about 55%.'],
87
+ ['Limits', 'detail-min-size · detail-max-size', '35% to 65%, so neither side disappears.'],
88
+ ['Header', 'header-gap · header-padding · title-condense', 'A pane title, not a dialog title.'],
89
+ ['Surface', 'surface/raised', 'One step off the page. The drawer keeps surface/modal.'],
90
+ ['List', 'page surface', 'The selected row uses the item selected state.'],
91
+ ]);
92
+ return { id: doc.id, h: Math.round(doc.height), overflow: L.overflow(doc) };
@@ -0,0 +1,114 @@
1
+ // Custom: Empty state. Kind × Size. Description and Actions are booleans. Media is an instance swap.
2
+ var AF = Object.getPrototypeOf(async function () {}).constructor;
3
+ var C = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'compHelpers'))(figma);
4
+ var V = C.V, COL = C.COL, v = C.v, fill = C.fill, bind = C.bind, radius = C.radius, add = C.add, T = C.T, mode = C.mode, page = C.page;
5
+ var icon = C.icon, comp = C.comp, textProp = C.textProp, boolProp = C.boolProp, place = C.place, removeOld = C.removeOld;
6
+ await page('Custom');
7
+ var buttons = await C.setOn('Buttons', 'Button');
8
+ function num(k) { var x = v(k); var m = COL[k.split(':')[0]].modes[0].modeId, val = x.valuesByMode[m]; while (val && val.type === 'VARIABLE_ALIAS') { var t = Object.values(V).find(function (y) { return y.id === val.id; }); val = t.valuesByMode[Object.keys(t.valuesByMode)[0]]; } return val; }
9
+ function frameSet(set, cols, colW, rowH) {
10
+ set.layoutMode = 'NONE';
11
+ set.children.forEach(function (c, i) { c.x = 24 + (i % cols) * colW; c.y = 24 + Math.floor(i / cols) * rowH; });
12
+ fill(set, 'Theme:surface/canvas'); set.strokes = [{ type: 'SOLID', color: { r: 0.592, g: 0.278, b: 1 } }]; set.dashPattern = [6, 4]; set.strokeWeight = 1; set.cornerRadius = 8; set.clipsContent = false;
13
+ set.resize(24 + cols * colW, 24 + Math.ceil(set.children.length / cols) * rowH);
14
+ }
15
+ function button(emphasis, label) {
16
+ var variant = buttons.children.filter(function (n) { return n.name === 'Emphasis=' + emphasis + ', Size=md, State=Default, Content=Label'; })[0];
17
+ var i = variant.createInstance();
18
+ var key = Object.keys(i.componentProperties).filter(function (k) { return k.split('#')[0] === 'Label'; })[0];
19
+ if (key) { var next = {}; next[key] = label; i.setProperties(next); }
20
+ return i;
21
+ }
22
+
23
+ // Glyphs the Media slot swaps. Unselected is text/tertiary; the others follow Emphasis, so Status tints them.
24
+ var PATHS = {
25
+ Start: 'M 2 7 L 8 11 L 14 7 M 2 7 L 2 14 L 14 14 L 14 7',
26
+ 'No results': 'M 4 4 L 9 4 L 11 7 L 9 10 L 4 10 L 2 7 Z M 10 9 L 14 13 M 3 13 L 12 4',
27
+ Error: 'M 8 2 L 14 13 L 2 13 Z M 8 6 L 8 9 M 8 11 L 8 12',
28
+ Done: 'M 3 8 L 7 12 L 13 4',
29
+ 'No access': 'M 5 7 L 5 4 L 11 4 L 11 7 M 3 7 L 13 7 L 13 14 L 3 14 Z',
30
+ Unselected: 'M 5 2 L 5 9 L 7 7 L 10 12 L 12 11 L 9 6 L 12 6 Z',
31
+ };
32
+ var KINDS = ['Start', 'No results', 'Error', 'Done', 'No access', 'Unselected'];
33
+ var SIZES = [['Compact', 'compact', 'condense'], ['Standard', 'standard', 'magnify'], ['Spacious', 'spacious', 'maximize']];
34
+ var STATUS = { Error: 'Error', Done: 'Success' };
35
+
36
+ removeOld('Empty state glyph');
37
+ var glyphComps = [];
38
+ for (var g = 0; g < KINDS.length; g++) {
39
+ var gk = KINDS[g];
40
+ var gc = comp('Kind=' + gk);
41
+ gc.primaryAxisAlignItems = 'CENTER'; gc.counterAxisAlignItems = 'CENTER';
42
+ add(gc, [icon('Icon', PATHS[gk], 16, gk === 'Unselected' ? 'Theme:text/tertiary' : 'Emphasis:emphasis/fg')]);
43
+ glyphComps.push(gc);
44
+ }
45
+ var glyphSet = figma.combineAsVariants(glyphComps, figma.currentPage);
46
+ glyphSet.name = 'Empty state glyph';
47
+ frameSet(glyphSet, KINDS.length, 80, 64);
48
+ glyphSet.description = 'The mark inside an empty state. Swap it when the app brings its own picture. Unselected stays text/tertiary. The others follow Emphasis and Status.';
49
+ place(glyphSet);
50
+
51
+ function glyphFor(kind) {
52
+ var node = glyphSet.children.filter(function (n) { return n.name === 'Kind=' + kind; })[0].createInstance();
53
+ node.name = 'Glyph';
54
+ return node;
55
+ }
56
+
57
+ removeOld('Empty state');
58
+ var comps = [];
59
+ for (var k = 0; k < KINDS.length; k++) for (var s = 0; s < SIZES.length; s++) {
60
+ var kind = KINDS[k], sizeName = SIZES[s][0], sizeKey = SIZES[s][1], tier = SIZES[s][2];
61
+ var circle = sizeName !== 'Compact' && kind !== 'Unselected';
62
+ var c = comp('Kind=' + kind + ', Size=' + sizeName, sizeName === 'Compact' ? 'HORIZONTAL' : 'VERTICAL');
63
+ c.primaryAxisAlignItems = 'CENTER'; c.counterAxisAlignItems = 'CENTER';
64
+ bind(c, ['paddingTop', 'paddingBottom'], 'Size:empty/padding-block/' + sizeKey);
65
+ bind(c, ['paddingLeft', 'paddingRight'], 'Size:empty/padding-inline/' + sizeKey);
66
+ bind(c, 'itemSpacing', 'Size:empty/gap/' + sizeKey);
67
+ var media = figma.createFrame(); media.name = 'Media'; media.layoutMode = 'HORIZONTAL';
68
+ media.primaryAxisAlignItems = 'CENTER'; media.counterAxisAlignItems = 'CENTER'; media.fills = [];
69
+ if (circle) {
70
+ mode(media, 'Emphasis', 'Subtle');
71
+ if (STATUS[kind]) mode(media, 'Status', STATUS[kind]);
72
+ fill(media, 'Emphasis:emphasis/bg');
73
+ radius(media, 'Size:radius/pill');
74
+ var d = num('Size:empty/media-size/' + sizeKey);
75
+ media.resize(d, d);
76
+ bind(media, ['width', 'height'], 'Size:empty/media-size/' + sizeKey);
77
+ media.primaryAxisSizingMode = 'FIXED'; media.counterAxisSizingMode = 'FIXED';
78
+ }
79
+ var glyph = glyphFor(kind);
80
+ mode(glyph, 'Emphasis', 'Subtle');
81
+ if (STATUS[kind]) mode(glyph, 'Status', STATUS[kind]);
82
+ var mark = num('Size:icon-size/' + tier);
83
+ glyph.resize(mark, mark);
84
+ add(media, [glyph]);
85
+ add(c, [media]);
86
+ var text = figma.createFrame(); text.name = 'Text'; text.layoutMode = 'VERTICAL'; text.itemSpacing = num('Size:empty/text-gap/' + sizeKey);
87
+ bind(text, 'itemSpacing', 'Size:empty/text-gap/' + sizeKey);
88
+ text.primaryAxisAlignItems = 'CENTER'; text.counterAxisAlignItems = 'CENTER'; text.fills = [];
89
+ var titleStyle = sizeName === 'Spacious' ? 'title-magnify' : sizeName === 'Compact' ? 'subtitle-condense' : 'title-condense';
90
+ if (kind === 'Unselected') titleStyle = sizeName === 'Spacious' ? 'title-condense' : 'subtitle-minimize';
91
+ var title = await T('Nothing here yet', titleStyle, 'Theme:text/primary', 220); title.name = 'Title';
92
+ var descStyle = sizeName === 'Compact' ? 'body-condense' : 'body-base';
93
+ var desc = await T('Add the first one to get started.', descStyle, 'Theme:text/secondary', 220); desc.name = 'Description';
94
+ add(text, [title, desc]);
95
+ add(c, [text]);
96
+ var actions = figma.createFrame(); actions.name = 'Actions'; actions.layoutMode = 'HORIZONTAL'; actions.itemSpacing = num('Size:empty/actions-gap');
97
+ bind(actions, 'itemSpacing', 'Size:empty/actions-gap');
98
+ actions.primaryAxisAlignItems = 'CENTER'; actions.fills = [];
99
+ add(actions, [button(kind === 'Start' ? 'Bold' : 'Standard', kind === 'Start' ? 'Create' : 'Retry')]);
100
+ add(c, [actions]);
101
+ comps.push(c);
102
+ }
103
+ var set = figma.combineAsVariants(comps, figma.currentPage); set.name = 'Empty state';
104
+ frameSet(set, SIZES.length, 300, 220);
105
+ textProp(set, 'Title', 'Nothing here yet', 'Title');
106
+ textProp(set, 'Description', 'Add the first one to get started.', 'Description');
107
+ boolProp(set, 'Description', true, 'Description');
108
+ boolProp(set, 'Actions', true, 'Actions');
109
+ var swap = set.addComponentProperty('Media', 'INSTANCE_SWAP', glyphSet.children[0].id);
110
+ set.findAll(function (n) { return n.type === 'INSTANCE' && n.name === 'Glyph'; }).forEach(function (n) {
111
+ n.componentPropertyReferences = Object.assign({}, n.componentPropertyReferences, { mainComponent: swap });
112
+ });
113
+ set.description = 'Nothing to show yet. Kind sets the icon and, for Error and Done, the Status mode on the media. Size is compact, standard or spacious. Description and Actions can be hidden. Media swaps for a picture the app owns. Title and description are one text value across the set; set them on the instance.';
114
+ place(set);
@@ -0,0 +1,8 @@
1
+ // Custom: headers for the empty state sets. Run after 13.
2
+ var AF = Object.getPrototypeOf(async function () {}).constructor;
3
+ var C = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'compHelpers'))(figma);
4
+ await C.page('Custom');
5
+ return await C.labelSets({
6
+ 'Empty state': 'Nothing to show. Kind sets the icon and the media tone. Size is where it sits. Description and Actions can be turned off. Media swaps for a picture.',
7
+ 'Empty state glyph': 'The mark the Media slot swaps. Unselected stays tertiary. The others follow Emphasis and Status.',
8
+ });
@@ -0,0 +1,58 @@
1
+ // Custom: Empty state doc. Run after 13 and 14. Not named Custom, so it does not replace the Badge doc.
2
+ var AF = Object.getPrototypeOf(async function () {}).constructor;
3
+ var K = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
4
+ await K.page('Custom');
5
+ var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docKit'))(figma);
6
+ var AL = L.AL, add = L.add, T = L.T, docFrame = L.docFrame, section = L.section, hrow = L.hrow, divider = L.divider;
7
+ var inst = L.inst, notes = L.notes, captioned = L.captioned, usage = L.usage, stage = L.stage;
8
+ function ES(kind, size) { return inst('Empty state', 'Kind=' + kind + ', Size=' + (size || 'Standard')); }
9
+
10
+ var doc = await docFrame('Empty state', 'COMPONENTS', 'Empty state', 'One block for every time there is nothing to show. The kind changes the icon and the tone of the circle, not the layout. Write the words yourself. Components sit to the right.');
11
+ doc.x = 1600; doc.y = 400;
12
+
13
+ var pick = await section(doc, 'Choosing', 'Start from what happened, then pick the kind. The words are yours.');
14
+ var picks = [
15
+ ['Nothing has been created yet', 'Start', 'A bold action that creates the first one.'],
16
+ ['A search or filter matched nothing', 'No results', 'Clear the filters. Announced when it appears.'],
17
+ ['The data could not load', 'Error', 'Retry. Quiet on arrival unless you ask it to shout.'],
18
+ ['The list was cleared on purpose', 'Done', 'Usually no action. Announced.'],
19
+ ['This person cannot see it', 'No access', 'Request access. No jokes.'],
20
+ ['A detail pane before a choice', 'Unselected', 'No action. Quieter than the list beside it.'],
21
+ ];
22
+ var pt = AL('VERTICAL', { name: 'Picker' }); add(pick, [pt]); pt.layoutSizingHorizontal = 'FILL';
23
+ for (var i = 0; i < picks.length; i++) {
24
+ var r = hrow(pt); divider(r);
25
+ add(r, [await T(picks[i][0], 'body-base', 'Theme:text/primary', 380), await T(picks[i][1], 'subtitle-base', 'Theme:text/accent', 160), await T(picks[i][2], 'body-condense', 'Theme:text/secondary', 480)]);
26
+ }
27
+
28
+ var look = await section(doc, 'Kinds', 'Standard size. Error and Done tint the circle through the Status mode. The title stays the normal text color.');
29
+ var s1 = stage(look, 'Kinds');
30
+ for (var k = 0; k < picks.length; k++) add(s1, [await captioned(ES(picks[k][1], 'Standard'), picks[k][1])]);
31
+ var s2 = stage(look, 'Sizes');
32
+ for (var z of ['Compact', 'Standard', 'Spacious']) add(s2, [await captioned(ES('No results', z), z)]);
33
+ await usage(look,
34
+ ['A table body that kept its header, with a compact empty state in the spanning cell.', 'A popup list, compact, inside Combobox.Empty, Autocomplete.Empty or Menu.Empty.', 'A page or a pane at standard or spacious.', 'The detail side of a split view, kind unselected, before a row is picked.'],
35
+ ['A toast. That is news about something that just happened.', 'A warning above content that is still there. Use a notice.', 'A joke on an error or on no access.', 'A title that only says "Oops". Say what is empty.']);
36
+ await notes(look, [
37
+ ['Compact', 'empty/padding-block/compact · gap/compact', 'A row: icon, text, actions. No circle.'],
38
+ ['Standard', 'empty/media-size/standard · title-condense', 'A pane. Circle around a magnify icon.'],
39
+ ['Spacious', 'empty/media-size/spacious · title-magnify', 'A page. More space above.'],
40
+ ['Description', 'measure narrow, 45ch', 'One sentence. The cap is a text measure, not a Size variable.'],
41
+ ['Circle', 'emphasis/bg · emphasis/fg · radius/pill', 'Subtle. Status or a parent color recolors it.'],
42
+ ['Title', 'text/primary', 'Never the error color. The words have to stay readable.'],
43
+ ['Description text', 'text/secondary', 'The second line.'],
44
+ ['Actions', 'empty/actions-gap', 'First is the main one. Bold for Start, standard otherwise, quiet for a second action.'],
45
+ ]);
46
+
47
+ var write = await section(doc, 'Writing', 'The title says what is empty. The description says what to do next. The action is a verb.');
48
+ var lines = [
49
+ ['Title', 'No invoices yet', 'Not "Oops" and not a joke.'],
50
+ ['Description', 'Create the first one and it will show up here.', 'One sentence.'],
51
+ ['Action', 'Create invoice', 'A verb. Clear filters, Retry, Request access.'],
52
+ ];
53
+ var wt = AL('VERTICAL', { name: 'Writing rows' }); add(write, [wt]); wt.layoutSizingHorizontal = 'FILL';
54
+ for (var w = 0; w < lines.length; w++) {
55
+ var wr = hrow(wt); divider(wr);
56
+ add(wr, [await T(lines[w][0], 'subtitle-base', 'Theme:text/primary', 180), await T(lines[w][1], 'body-base', 'Theme:text/primary', 420), await T(lines[w][2], 'body-condense', 'Theme:text/secondary', 420)]);
57
+ }
58
+ return { id: doc.id, h: Math.round(doc.height), overflow: L.overflow(doc) };