@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.
- package/README.md +3 -1
- package/bin/loomiere.mjs +3 -1
- package/build/build-tokens.mjs +180 -35
- package/exporters/design-system.mjs +7 -0
- package/exporters/figma.mjs +545 -478
- package/exporters/index.mjs +4 -0
- package/figma/README.md +24 -8
- package/figma/library/README.md +92 -0
- package/figma/library/check/known-differences.md +20 -0
- package/figma/library/check/mojave.json +246 -0
- package/figma/library/kit/base.js +89 -0
- package/figma/library/kit/components.js +95 -0
- package/figma/library/kit/docs.js +37 -0
- package/figma/library/kit/fingerprint.js +143 -0
- package/figma/library/pages/02-palettes.js +40 -0
- package/figma/library/pages/03-color.js +126 -0
- package/figma/library/pages/04-type.js +56 -0
- package/figma/library/pages/05-scale.js +58 -0
- package/figma/library/pages/06-size.js +111 -0
- package/figma/library/pages/07-elevation-focus-motion.js +108 -0
- package/figma/library/pages/08-themes.js +36 -0
- package/figma/library/pages/09-fields/01-input.js +55 -0
- package/figma/library/pages/09-fields/02-checks.js +82 -0
- package/figma/library/pages/09-fields/03-field.js +32 -0
- package/figma/library/pages/09-fields/04-more-fields.js +144 -0
- package/figma/library/pages/09-fields/05-labels.js +14 -0
- package/figma/library/pages/09-fields/06-doc.js +75 -0
- package/figma/library/pages/10-buttons/01-buttons.js +97 -0
- package/figma/library/pages/10-buttons/02-doc.js +55 -0
- package/figma/library/pages/11-popups/01-popups.js +166 -0
- package/figma/library/pages/11-popups/02-more-popups.js +90 -0
- package/figma/library/pages/11-popups/03-labels.js +14 -0
- package/figma/library/pages/11-popups/04-doc.js +89 -0
- package/figma/library/pages/12-overlays/01-overlays.js +135 -0
- package/figma/library/pages/12-overlays/02-labels.js +10 -0
- package/figma/library/pages/12-overlays/03-doc.js +80 -0
- package/figma/library/pages/13-disclosure/01-disclosure.js +205 -0
- package/figma/library/pages/13-disclosure/02-collapsible.js +39 -0
- package/figma/library/pages/13-disclosure/03-labels.js +16 -0
- package/figma/library/pages/13-disclosure/04-doc.js +91 -0
- package/figma/library/pages/14-display/01-display.js +165 -0
- package/figma/library/pages/14-display/02-more-display.js +55 -0
- package/figma/library/pages/14-display/03-labels.js +14 -0
- package/figma/library/pages/14-display/04-doc.js +96 -0
- package/figma/library/pages/15-cover.js +87 -0
- package/figma/library/run-order.json +21 -0
- package/figma/library/tools/compare.mjs +11 -0
- package/figma/library/tools/diff-snippet.js +15 -0
- package/figma/library/tools/kit-script.mjs +21 -0
- package/figma/library/tools/run.mjs +27 -0
- package/figma/library/tools/runs.mjs +34 -0
- package/generator/brand.schema.json +83 -0
- package/generator/generate.mjs +26 -6
- package/generator/looks.mjs +62 -0
- package/lib/contrast.mjs +24 -6
- package/lib/figma.mjs +58 -23
- package/package.json +2 -1
- package/tokens/README.md +26 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Disclosure and navigation doc frame. Run after every set on the page is built and labelled.
|
|
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, COL = L.COL, v = L.v, paint = L.paint, fill = L.fill, stroke = L.stroke, bind = L.bind, radius = L.radius, pad = L.pad, 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;
|
|
5
|
+
await page('Disclosure and navigation');
|
|
6
|
+
var sets = {}; figma.currentPage.findAllWithCriteria({ types: ['COMPONENT_SET'] }).forEach(function (s) { sets[s.name] = s; });
|
|
7
|
+
function inst(set, name, props) {
|
|
8
|
+
var c = sets[set].children.filter(function (x) { return x.name === name; })[0]; if (!c) throw new Error(set + ' ' + name);
|
|
9
|
+
var i = c.createInstance();
|
|
10
|
+
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); }
|
|
11
|
+
return i;
|
|
12
|
+
}
|
|
13
|
+
async function notes(parent, rows) {
|
|
14
|
+
var tbl = AL('VERTICAL', { name: 'Spec' }); add(parent, [tbl]); tbl.layoutSizingHorizontal = 'FILL';
|
|
15
|
+
var hd = hrow(tbl); hd.paddingTop = 0; divider(hd);
|
|
16
|
+
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)]);
|
|
17
|
+
for (var i = 0; i < rows.length; i++) {
|
|
18
|
+
var r = hrow(tbl); r.counterAxisAlignItems = 'MIN'; r.paddingTop = r.paddingBottom = 10; divider(r);
|
|
19
|
+
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)]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function stage(parent, name) {
|
|
23
|
+
var s = AL('HORIZONTAL', { name: name || 'Examples', itemSpacing: 32, counterAxisAlignItems: 'CENTER', paddingTop: 32, paddingBottom: 32, paddingLeft: 32, paddingRight: 32, layoutWrap: 'WRAP', counterAxisSpacing: 24 });
|
|
24
|
+
fill(s, 'Theme:fill/subtle'); s.cornerRadius = 12; add(parent, [s]); s.layoutSizingHorizontal = 'FILL'; return s;
|
|
25
|
+
}
|
|
26
|
+
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; }
|
|
27
|
+
async function usage(parent, dos, donts) {
|
|
28
|
+
var r = AL('HORIZONTAL', { name: 'Usage', itemSpacing: 24 }); add(parent, [r]); r.layoutSizingHorizontal = 'FILL';
|
|
29
|
+
var pairs = [['Use it for', dos, 'Theme:status/success'], ['Avoid', donts, 'Theme:status/error']];
|
|
30
|
+
for (var i = 0; i < 2; i++) {
|
|
31
|
+
var col = AL('VERTICAL', { name: pairs[i][0], itemSpacing: 8, paddingLeft: 16 }); add(r, [col]); col.layoutSizingHorizontal = 'FILL';
|
|
32
|
+
col.strokes = [paint(pairs[i][2])]; col.strokeLeftWeight = 3; col.strokeTopWeight = col.strokeRightWeight = col.strokeBottomWeight = 0;
|
|
33
|
+
add(col, [await T(pairs[i][0], 'subtitle-base')]);
|
|
34
|
+
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'; }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
figma.currentPage.children.filter(function (n) { return n.name === 'Disclosure and navigation' && n.type === 'FRAME'; }).forEach(function (n) { n.remove(); });
|
|
38
|
+
var doc = await docFrame('Disclosure and navigation', 'COMPONENTS', 'Disclosure and navigation', 'Ways to show one part of the content at a time and to move around: tabs, accordions, the site navigation bar, toolbars and toggle groups. Tabs and accordions share the disclosure concept (fill/subtle hover, border/default dividers, a border/accent indicator); the navigation bar, toolbar and toggle group share the bar surface. Components sit to the right.');
|
|
39
|
+
doc.x = 0; doc.y = 400;
|
|
40
|
+
var pick = await section(doc, 'Choosing', 'Start from what the person needs to see at once.');
|
|
41
|
+
var picks = [['Switch between views of the same thing', 'Tabs', 'Overview, Itinerary, Bookings. One panel at a time, all labels visible.'], ['Let people open only what they need', 'Accordion', 'FAQs and settings sections. Several can be open.'], ['One optional block of detail', 'Collapsible', 'Advanced settings, a long note. Lighter than an Accordion.'], ['Move between sections of a site', 'Navigation menu', 'Top-level links, with panels for big sections.'], ['Act on the content below', 'Toolbar', 'Formatting, sharing, view options in one strip.'], ['Pick one or several options in a row', 'Toggle group', 'Day, Week, Month. Like a segmented control.']];
|
|
42
|
+
var pt = AL('VERTICAL', { name: 'Picker' }); add(pick, [pt]); pt.layoutSizingHorizontal = 'FILL';
|
|
43
|
+
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)]); }
|
|
44
|
+
|
|
45
|
+
var ts = await section(doc, 'Tabs', 'A row of labels that switch the panel below. The selected tab takes the accent color, the selected text style and a line on the divider; the rest are secondary text. Vertical tabs put the line on the right.');
|
|
46
|
+
var s1 = stage(ts, 'Tabs'); s1.counterAxisAlignItems = 'MIN';
|
|
47
|
+
var hz = AL('VERTICAL', { name: 'Horizontal with panel' }); add(hz, [inst('Tab list', 'Orientation=Horizontal')]);
|
|
48
|
+
var panel = AL('VERTICAL', { name: 'Panel', paddingTop: 16, paddingBottom: 16, paddingLeft: 16, paddingRight: 16 }); add(panel, [await T('Three nights in Joshua Tree, then south to Anza-Borrego.', 'body-base', 'Theme:text/primary', 420)]); add(hz, [panel]);
|
|
49
|
+
add(s1, [await captioned(hz, 'Horizontal, with its panel'), await captioned(inst('Tab list', 'Orientation=Vertical'), 'Vertical')]);
|
|
50
|
+
var s1b = stage(ts, 'Sizes');
|
|
51
|
+
for (var z of ['sm', 'md', 'lg']) add(s1b, [await captioned(inst('Tab', 'Orientation=Horizontal, Size=' + z + ', State=Selected'), z)]);
|
|
52
|
+
await usage(ts, ['Two to six views of one thing.', 'Short labels, one or two words.', 'Views people move between often.'], ['Steps in a process. Use a stepper or pages.', 'Content people need to compare side by side.', 'Navigation to other pages. Use the navigation menu.']);
|
|
53
|
+
await notes(ts, [['Text', 'body-base · selected-base', 'Selected changes weight as well as color, so color is not the only signal.'], ['Selected', 'text/accent · border/accent', 'disclosure/indicator-width line on the divider.'], ['Divider', 'border/default · stroke/default', 'Under the whole list.'], ['Hover', 'fill/subtle', 'The same wash as an accordion trigger.'], ['Padding', 'space/2 · space/4 (md)', 'sm space/1-5 · space/3; lg space/3 · space/6.'], ['Disabled', 'text/disabled · disclosure/opacity-disabled', 'Greyed and faded, still in place.']]);
|
|
54
|
+
|
|
55
|
+
var as = await section(doc, 'Accordion', 'A stack of questions or sections that open in place. The trigger is title-base text with a chevron that flips when open; the answer is body-base in text/secondary.');
|
|
56
|
+
var s2 = stage(as, 'Variants'); s2.counterAxisAlignItems = 'MIN';
|
|
57
|
+
s2.primaryAxisAlignItems = 'MIN';
|
|
58
|
+
for (var vn of ['Default', 'Separated', 'Bordered']) {
|
|
59
|
+
var card = AL('VERTICAL', { name: 'Card', paddingTop: 24, paddingBottom: 24, paddingLeft: 24, paddingRight: 24, cornerRadius: 12 }); fill(card, 'Theme:surface/canvas');
|
|
60
|
+
var a = inst('Accordion', 'Variant=' + vn); a.resize(360, a.height); add(card, [a]); a.layoutSizingVertical = 'HUG';
|
|
61
|
+
add(s2, [await captioned(card, vn)]);
|
|
62
|
+
}
|
|
63
|
+
await usage(as, ['FAQs and long settings pages.', 'Details most people skip.', 'Separated or Bordered when the accordion sits among other content.'], ['Content everyone needs. Show it.', 'Nesting accordions inside accordions.']);
|
|
64
|
+
await notes(as, [['Trigger', 'title-base · text/primary', 'The question reads as a heading.'], ['Padding', 'space/4 · space/6', 'Generous so the whole row is easy to hit.'], ['Chevron', 'icon-size/magnify · text/secondary', 'Flips when open.'], ['Answer', 'body-base · text/secondary', 'Lined up with the question.'], ['Divider', 'border/default', 'Between items.'], ['Hover', 'fill/subtle', 'The same wash as a hovered tab.'], ['Corners', 'radius/chrome (Separated) · radius/surface (Bordered)', 'Only when items have their own edges.']]);
|
|
65
|
+
|
|
66
|
+
var ns = await section(doc, 'Navigation menu', 'Site navigation on a bar surface. Link items go straight to a page; Menu items open a panel of links with short descriptions, on the shared popup surface.');
|
|
67
|
+
var s3 = stage(ns, 'Navigation'); s3.counterAxisAlignItems = 'MIN';
|
|
68
|
+
add(s3, [await captioned(inst('Navigation menu', 'Content=Closed'), 'Closed'), await captioned(inst('Navigation menu', 'Content=Open'), 'Open on Destinations')]);
|
|
69
|
+
await usage(ns, ['The top-level sections of a site.', 'Panels that help people choose with a line of description.'], ['Actions. Use a Menu or Toolbar.', 'More than about seven top-level items.']);
|
|
70
|
+
await notes(ns, [['Bar', 'surface/tray · border/default · bar/radius · bar/padding', 'The shared bar surface.'], ['Trigger', 'control/height-md · trigger-base · radius/item', 'Lines up with buttons.'], ['Open', 'interactive/tint-hover · text/on-tint', 'The menu tint, so the open item is clear.'], ['Panel', 'popup surface · popup/padding-panel', 'Min width navigation-menu/popup-min-width.'], ['Link', 'trigger-base · body-condense · space/2', 'Columns navigation-menu/column-width wide.']]);
|
|
71
|
+
|
|
72
|
+
var bs = await section(doc, 'Toolbar and Toggle group', 'Both sit on the bar surface. A toolbar groups controls for the content below it, with separators between groups. A toggle group is a set of Toggles where one (Single) or several (Multiple) can be on.');
|
|
73
|
+
var s4 = stage(bs, 'Bars'); s4.counterAxisAlignItems = 'MIN';
|
|
74
|
+
add(s4, [await captioned(inst('Toolbar', 'Content=Formatting'), 'Toolbar, formatting'), await captioned(inst('Toolbar', 'Content=Mixed'), 'Toolbar, mixed'), await captioned(inst('Toggle group', 'Kind=Single'), 'Toggle group, single'), await captioned(inst('Toggle group', 'Kind=Multiple'), 'Toggle group, multiple')]);
|
|
75
|
+
await usage(bs, ['Editors and canvases: formatting, zoom, sharing.', 'Switching a view between a few options.'], ['A toolbar with one control. Just place the control.', 'Toggle groups for actions. Toggles hold state.']);
|
|
76
|
+
await notes(bs, [['Surface', 'surface/tray · border/default · bar/radius', 'A tray, a step back from the page.'], ['Padding', 'bar/padding', 'A thin inset around the controls.'], ['Gaps', 'bar/gap · bar/group-gap', 'Tight inside a group, wider between groups.'], ['Separator', 'border/default · bar/separator-length', 'bar/separator-spacing on each side.'], ['Toggle corners', 'radius/item', 'In a group, so they read as one set.']]);
|
|
77
|
+
var over = [];
|
|
78
|
+
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)); });
|
|
79
|
+
|
|
80
|
+
// Sections for the later sets use the shared doc kit; its stages top-align their examples.
|
|
81
|
+
var K = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docKit'))(figma);
|
|
82
|
+
var cls = await section(doc, 'Collapsible', 'A single panel that opens and closes under its trigger. Lighter than an Accordion: no border or row, just a label with a chevron that turns from right to down. The panel sits on the overlay surface, its text lined up with the label above it.');
|
|
83
|
+
var k1 = K.stage(cls, 'States');
|
|
84
|
+
for (var cv of [['Closed', 'Default'], ['Closed', 'Hover'], ['Open', 'Default'], ['Open', 'Hover']]) {
|
|
85
|
+
var card = AL('VERTICAL', { name: 'Card', paddingTop: 24, paddingBottom: 24, paddingLeft: 24, paddingRight: 24, cornerRadius: 12 }); fill(card, 'Theme:surface/canvas');
|
|
86
|
+
add(card, [K.inst('Collapsible', 'Panel=' + cv[0] + ', State=' + cv[1])]);
|
|
87
|
+
add(k1, [await K.captioned(card, cv[0] + ', ' + cv[1].toLowerCase())]);
|
|
88
|
+
}
|
|
89
|
+
await K.usage(cls, ['One optional block of detail, like advanced settings or a long note.', 'Show more under a summary.'], ['A stack of several. Use Accordion.', 'Hiding something people need to finish the task.']);
|
|
90
|
+
await K.notes(cls, [['Trigger', 'subtitle-base · space/1 space/2 · item/radius', 'Reads as a label, not a button.'], ['Hover', 'fill/subtle', 'The same as an Accordion trigger.'], ['Chevron', 'text/secondary', 'Right when closed, down when open.'], ['Panel', 'surface/overlay · item/radius · space/2', 'Indented by the chevron and gap so text lines up with the label.'], ['Disabled', 'disclosure/opacity-disabled', 'Fades trigger and panel.']]);
|
|
91
|
+
return { id: doc.id, h: Math.round(doc.height) };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Display: Avatar, Avatar group, Meter, Progress, Slider, Table row, Table. Rebuilds the page's Components column.
|
|
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, TS = C.TS, v = C.v, paint = C.paint, fill = C.fill, stroke = C.stroke, bind = C.bind, radius = C.radius, pad = C.pad, AL = C.AL, add = C.add, T = C.T, mode = C.mode, page = C.page;
|
|
5
|
+
var shadow = C.shadow, focusRing = C.focusRing, icon = C.icon, comp = C.comp, textProp = C.textProp, boolProp = C.boolProp, place = C.place, removeOld = C.removeOld, arrange = C.arrange;
|
|
6
|
+
var here = function (n) { return figma.currentPage.findOne(function (x) { return x.type === 'COMPONENT_SET' && x.name === n; }); };
|
|
7
|
+
var PAGE = 'Display';
|
|
8
|
+
var pp = await page(PAGE);
|
|
9
|
+
figma.currentPage.children.filter(function (n) { return n.name === 'Components'; }).forEach(function (n) { n.remove(); });
|
|
10
|
+
var ES = {}; (await figma.getLocalEffectStylesAsync()).forEach(function (s) { ES[s.name] = s; });
|
|
11
|
+
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; }
|
|
12
|
+
function grid(set, cols, colW, rowH) {
|
|
13
|
+
set.layoutMode = 'NONE';
|
|
14
|
+
set.children.forEach(function (k, i) { var r = Math.floor(i / cols); k.x = 32 + (i % cols) * colW; k.y = 32 + r * rowH + Math.round((rowH - k.height) / 2); });
|
|
15
|
+
set.resize(64 + cols * colW - 24, 64 + Math.ceil(set.children.length / cols) * rowH); frameSet(set);
|
|
16
|
+
}
|
|
17
|
+
function row(set, gap) {
|
|
18
|
+
set.layoutMode = 'NONE'; var x = 32, h = 0;
|
|
19
|
+
set.children.forEach(function (k) { k.x = x; k.y = 48; x += k.width + gap; h = Math.max(h, k.height); });
|
|
20
|
+
set.children.forEach(function (k) { k.y = 48 + Math.round((h - k.height) / 2); });
|
|
21
|
+
set.resize(x - gap + 32, h + 96); frameSet(set);
|
|
22
|
+
}
|
|
23
|
+
function pick(set, name) { var c = set.children.find(function (x) { return x.name === name; }); if (!c) throw new Error(set.name + ' ' + name); return c.createInstance(); }
|
|
24
|
+
function fillW(n) { n.layoutSizingHorizontal = 'FILL'; return n; }
|
|
25
|
+
function hug(c) { c.primaryAxisSizingMode = 'AUTO'; return c; }
|
|
26
|
+
|
|
27
|
+
// Avatar
|
|
28
|
+
var SZ = ['xs', 'sm', 'md', 'lg', 'xl'], PX = { xs: 32, sm: 40, md: 48, lg: 64, xl: 96 }, av = [];
|
|
29
|
+
for (var kind of ['Initials', 'Image']) for (var z of SZ) {
|
|
30
|
+
var c = comp('Content=' + kind + ', Size=' + z); c.primaryAxisAlignItems = 'CENTER'; c.counterAxisAlignItems = 'CENTER';
|
|
31
|
+
c.resize(PX[z], PX[z]); c.primaryAxisSizingMode = 'FIXED'; c.counterAxisSizingMode = 'FIXED'; bind(c, ['width', 'height'], 'Size:avatar/size-' + z);
|
|
32
|
+
radius(c, 'Size:radius/pill'); stroke(c, 'Theme:surface/canvas', 'Size:stroke/emphasis'); await c.setEffectStyleIdAsync(ES['elevation/sm'].id); c.clipsContent = true;
|
|
33
|
+
if (kind === 'Initials') {
|
|
34
|
+
fill(c, 'Emphasis:emphasis/bg');
|
|
35
|
+
var t = await T('SP', 'title-base', 'Emphasis:emphasis/fg'); t.name = 'Initials'; t.setBoundVariable('fontSize', v('Size:avatar/initials-font-size-' + z)); add(c, [t]);
|
|
36
|
+
} else {
|
|
37
|
+
fill(c, 'Theme:fill/subtle'); c.layoutMode = 'NONE';
|
|
38
|
+
var head = figma.createEllipse(); head.name = 'Head'; head.resize(PX[z] * 0.36, PX[z] * 0.36); head.x = PX[z] * 0.32; head.y = PX[z] * 0.2; fill(head, 'Theme:fill/strong');
|
|
39
|
+
var body = figma.createEllipse(); body.name = 'Body'; body.resize(PX[z] * 0.7, PX[z] * 0.6); body.x = PX[z] * 0.15; body.y = PX[z] * 0.62; fill(body, 'Theme:fill/strong');
|
|
40
|
+
c.appendChild(head); c.appendChild(body);
|
|
41
|
+
[head, body].forEach(function (n) { n.constraints = { horizontal: 'SCALE', vertical: 'SCALE' }; });
|
|
42
|
+
}
|
|
43
|
+
av.push(c);
|
|
44
|
+
}
|
|
45
|
+
var avSet = figma.combineAsVariants(av, figma.currentPage); avSet.name = 'Avatar'; grid(avSet, 5, 130, 130);
|
|
46
|
+
textProp(avSet, 'Initials', 'SP', 'Initials');
|
|
47
|
+
avSet.description = 'A person: their photo, or their initials when there is none. Initials paint with the emphasis level (Bold unless the Emphasis mode says otherwise) and avatar/initials-font-size-*. A stroke/emphasis ring in the page color and the elevation/sm shadow lift it off busy backgrounds.';
|
|
48
|
+
place(avSet);
|
|
49
|
+
var groups = [];
|
|
50
|
+
for (var z of ['sm', 'md']) {
|
|
51
|
+
var c = comp('Size=' + z); c.counterAxisAlignItems = 'CENTER'; c.itemSpacing = -Math.round(PX[z] * 0.33); c.clipsContent = false;
|
|
52
|
+
var inits = ['SP', 'JR', 'AM'];
|
|
53
|
+
for (var q = 0; q < 4; q++) {
|
|
54
|
+
var a = pick(avSet, 'Content=' + (q === 1 ? 'Image' : 'Initials') + ', Size=' + z);
|
|
55
|
+
if (q !== 1) { var p = {}; Object.keys(a.componentProperties).forEach(function (k) { if (/^Initials#/.test(k)) p[k] = q === 3 ? '+4' : inits[q === 0 ? 0 : q - 1]; }); a.setProperties(p); }
|
|
56
|
+
if (q === 2) mode(a, 'Emphasis', 'Subtle'); if (q === 3) mode(a, 'Emphasis', 'Standard');
|
|
57
|
+
add(c, [a]);
|
|
58
|
+
}
|
|
59
|
+
groups.push(c);
|
|
60
|
+
}
|
|
61
|
+
var grSet = figma.combineAsVariants(groups, figma.currentPage); grSet.name = 'Avatar group'; row(grSet, 64);
|
|
62
|
+
grSet.description = 'Avatars overlapping by a third of their size. The ring in the page color keeps each one distinct. End with a count when there are more people than fit.';
|
|
63
|
+
place(grSet);
|
|
64
|
+
|
|
65
|
+
// Gauges
|
|
66
|
+
async function header(label, value) {
|
|
67
|
+
var h = AL('HORIZONTAL', { name: 'Header', primaryAxisAlignItems: 'SPACE_BETWEEN', counterAxisAlignItems: 'CENTER' });
|
|
68
|
+
var l = await T(label, 'label-base', 'Theme:text/primary'); l.name = 'Label';
|
|
69
|
+
var vv = await T(value, 'body-minimize', 'Theme:text/secondary'); vv.name = 'Value';
|
|
70
|
+
add(h, [l, vv]); return h;
|
|
71
|
+
}
|
|
72
|
+
function track(hKey, pct, offset) {
|
|
73
|
+
var tr = AL('HORIZONTAL', { name: 'Track' }); fill(tr, 'Theme:fill/track'); radius(tr, 'Size:gauge/radius'); tr.clipsContent = true;
|
|
74
|
+
tr.counterAxisSizingMode = 'FIXED'; tr.resize(280, 8); bind(tr, 'height', hKey);
|
|
75
|
+
if (offset) tr.paddingLeft = Math.round(280 * offset);
|
|
76
|
+
var ind = figma.createFrame(); ind.name = 'Indicator'; ind.resize(Math.round(280 * pct), 8); fill(ind, 'Status:bold/bg'); radius(ind, 'Size:gauge/radius');
|
|
77
|
+
add(tr, [ind]); ind.layoutSizingVertical = 'FILL'; return tr;
|
|
78
|
+
}
|
|
79
|
+
var HK = { sm: 'Size:gauge/height-sm', md: 'Size:gauge/height', lg: 'Size:gauge/height-lg' };
|
|
80
|
+
var meters = [];
|
|
81
|
+
for (var z of ['sm', 'md', 'lg']) {
|
|
82
|
+
var c = comp('Size=' + z, 'VERTICAL'); bind(c, 'itemSpacing', 'Size:gauge/gap'); c.counterAxisSizingMode = 'FIXED'; c.resize(280, 40); hug(c);
|
|
83
|
+
var hd = await header('Water left', '62%'); add(c, [hd]); fillW(hd); var tr = track(HK[z], 0.62); add(c, [tr]); fillW(tr); meters.push(c);
|
|
84
|
+
}
|
|
85
|
+
var meterSet = figma.combineAsVariants(meters, figma.currentPage); meterSet.name = 'Meter'; row(meterSet, 48);
|
|
86
|
+
textProp(meterSet, 'Label', 'Water left', 'Label'); textProp(meterSet, 'Value', '62%', 'Value');
|
|
87
|
+
meterSet.description = 'A measurement inside a known range: storage, battery, water left. The fill is the Status collection\'s bold color, so set the Status mode to Warning or Error when the value is in a bad range. Label in label-base, value in body-minimize.';
|
|
88
|
+
place(meterSet);
|
|
89
|
+
var progs = [];
|
|
90
|
+
for (var z of ['sm', 'md', 'lg']) {
|
|
91
|
+
var c = comp('Value=Determinate, Size=' + z, 'VERTICAL'); bind(c, 'itemSpacing', 'Size:gauge/gap'); c.counterAxisSizingMode = 'FIXED'; c.resize(280, 40); hug(c);
|
|
92
|
+
var hd = await header('Uploading photos', '3 of 8'); add(c, [hd]); fillW(hd); var tr = track(HK[z], 0.375); add(c, [tr]); fillW(tr); progs.push(c);
|
|
93
|
+
}
|
|
94
|
+
var c = comp('Value=Indeterminate, Size=md', 'VERTICAL'); bind(c, 'itemSpacing', 'Size:gauge/gap'); c.counterAxisSizingMode = 'FIXED'; c.resize(280, 40); hug(c);
|
|
95
|
+
var hd = await header('Uploading photos', ' '); add(c, [hd]); fillW(hd); var tr = track(HK.md, 0.5, 0.25); add(c, [tr]); fillW(tr); progs.push(c);
|
|
96
|
+
var progSet = figma.combineAsVariants(progs, figma.currentPage); progSet.name = 'Progress'; row(progSet, 48);
|
|
97
|
+
textProp(progSet, 'Label', 'Uploading photos', 'Label');
|
|
98
|
+
progSet.description = 'How far a task has got. Indeterminate is for work with no known end: a half-width bar slides across the track. Same track, fill and text as Meter.';
|
|
99
|
+
place(progSet);
|
|
100
|
+
|
|
101
|
+
// Slider
|
|
102
|
+
function thumb(st) {
|
|
103
|
+
var t = figma.createFrame(); t.name = 'Thumb'; t.resize(20, 20); bind(t, ['width', 'height'], 'Size:slider/thumb-size'); radius(t, 'Size:radius/pill');
|
|
104
|
+
fill(t, 'Theme:surface/canvas'); stroke(t, 'Status:bold/border', 'Size:stroke/emphasis'); t.clipsContent = true;
|
|
105
|
+
if (st === 'Hover') t.effects = [shadow('Theme:scope/accent/soft-hover', 4)];
|
|
106
|
+
if (st === 'Dragging') t.effects = [shadow('Theme:scope/accent/soft-active', 6)];
|
|
107
|
+
if (st === 'Focus') t.effects = focusRing();
|
|
108
|
+
return t;
|
|
109
|
+
}
|
|
110
|
+
var sliders = [];
|
|
111
|
+
for (var kind of ['Single', 'Range']) for (var st of ['Default', 'Hover', 'Focus', 'Dragging', 'Disabled']) {
|
|
112
|
+
var c = comp('Kind=' + kind + ', State=' + st, 'VERTICAL'); bind(c, 'itemSpacing', 'Size:gauge/gap'); c.counterAxisSizingMode = 'FIXED'; c.resize(280, 40); hug(c);
|
|
113
|
+
var hd = await header(kind === 'Range' ? 'Price per night' : 'Distance', kind === 'Range' ? '$40 – $120' : '15 mi'); add(c, [hd]); fillW(hd);
|
|
114
|
+
var ctl = figma.createFrame(); ctl.name = 'Control'; ctl.fills = []; ctl.resize(280, 20); ctl.clipsContent = false;
|
|
115
|
+
var tk = figma.createRectangle(); tk.name = 'Track'; tk.resize(280, 4); tk.y = 8; fill(tk, 'Theme:fill/track'); tk.cornerRadius = 999; bind(tk, 'height', 'Size:gauge/height-sm'); ctl.appendChild(tk);
|
|
116
|
+
var a0 = kind === 'Range' ? 0.25 : 0, a1 = kind === 'Range' ? 0.7 : 0.6;
|
|
117
|
+
var ind = figma.createRectangle(); ind.name = 'Indicator'; ind.resize(Math.round(280 * (a1 - a0)), 4); ind.x = Math.round(280 * a0); ind.y = 8; fill(ind, 'Status:bold/bg'); ind.cornerRadius = 999; ctl.appendChild(ind);
|
|
118
|
+
var stops = kind === 'Range' ? [a0, a1] : [a1];
|
|
119
|
+
stops.forEach(function (s, i) { var th = thumb(i === stops.length - 1 ? st : 'Default'); ctl.appendChild(th); th.x = Math.round(280 * s) - 10; th.y = 0; });
|
|
120
|
+
add(c, [ctl]); fillW(ctl);
|
|
121
|
+
if (st === 'Disabled') { c.opacity = 0.5; bind(c, 'opacity', 'Size:check/opacity-disabled'); }
|
|
122
|
+
sliders.push(c);
|
|
123
|
+
}
|
|
124
|
+
var slSet = figma.combineAsVariants(sliders, figma.currentPage); slSet.name = 'Slider'; grid(slSet, 5, 320, 96);
|
|
125
|
+
textProp(slSet, 'Label', 'Distance', 'Label');
|
|
126
|
+
slSet.description = 'Pick a value, or a range with two thumbs, by dragging. The track is gauge/height-sm; the thumb is slider/thumb-size on the field surface with a Status bold edge. Hover and Dragging grow a soft halo; Focus is the standard ring.';
|
|
127
|
+
place(slSet);
|
|
128
|
+
|
|
129
|
+
// Table
|
|
130
|
+
var COLS = [['Site', 180, 'MIN'], ['Park', 200, 'MIN'], ['Nights', 90, 'MAX'], ['Status', 130, 'MIN']];
|
|
131
|
+
async function tableRow(kind) {
|
|
132
|
+
var c = comp('Row=' + kind); c.counterAxisAlignItems = 'CENTER'; c.counterAxisSizingMode = 'FIXED';
|
|
133
|
+
c.resize(600, 32); bind(c, 'height', kind === 'Header' ? 'Size:table/header-height' : 'Size:table/row-height');
|
|
134
|
+
if (kind === 'Header') fill(c, 'Theme:surface/tray'); if (kind === 'Hover') fill(c, 'Theme:interactive/tint-hover'); if (kind === 'Selected') fill(c, 'Theme:interactive/tint-active');
|
|
135
|
+
c.strokes = [paint(kind === 'Header' ? 'Theme:border/default' : 'Theme:border/subtle')]; c.strokeAlign = 'INSIDE'; c.strokeTopWeight = c.strokeLeftWeight = c.strokeRightWeight = 0; c.strokeBottomWeight = 1; bind(c, 'strokeBottomWeight', 'Size:stroke/default');
|
|
136
|
+
var vals = ['Jumbo Rocks 42', 'Joshua Tree', '3', 'Booked'];
|
|
137
|
+
var fg = kind === 'Hover' ? 'Theme:text/on-tint' : kind === 'Selected' ? 'Theme:text/on-tint-strong' : 'Theme:text/primary';
|
|
138
|
+
for (var i = 0; i < COLS.length; i++) {
|
|
139
|
+
var cell = AL('HORIZONTAL', { name: COLS[i][0], counterAxisAlignItems: 'CENTER', primaryAxisAlignItems: COLS[i][2] }); bind(cell, ['paddingLeft', 'paddingRight'], 'Size:table/cell-padding-inline');
|
|
140
|
+
cell.resize(COLS[i][1], 32); cell.primaryAxisSizingMode = 'FIXED';
|
|
141
|
+
var t = kind === 'Header' ? await T(COLS[i][0], 'subtitle-condense', 'Theme:text/primary') : await T(vals[i], 'body-condense', i === 1 && kind === 'Default' ? 'Theme:text/secondary' : fg);
|
|
142
|
+
t.name = 'Text'; add(cell, [t]); add(c, [cell]); cell.layoutSizingVertical = 'FILL';
|
|
143
|
+
}
|
|
144
|
+
c.primaryAxisSizingMode = 'AUTO';
|
|
145
|
+
return c;
|
|
146
|
+
}
|
|
147
|
+
var rows = []; for (var k of ['Header', 'Default', 'Hover', 'Selected']) rows.push(await tableRow(k));
|
|
148
|
+
var rowSet = figma.combineAsVariants(rows, figma.currentPage); rowSet.name = 'Table row'; grid(rowSet, 1, 640, 56);
|
|
149
|
+
rowSet.description = 'One row of a data table. Header sits on surface/tray at table/header-height; body rows are table/row-height with a border/subtle line. Hover and Selected use the same tints as list items. Numbers align right.';
|
|
150
|
+
place(rowSet);
|
|
151
|
+
var DATA = [['Jumbo Rocks 42', 'Joshua Tree', '3', 'Booked'], ['Hole-in-the-Wall 7', 'Mojave Preserve', '2', 'Waitlist'], ['Furnace Creek 118', 'Death Valley', '4', 'Booked'], ['Cottonwood 15', 'Joshua Tree', '1', 'Cancelled']];
|
|
152
|
+
async function setTexts(inst, vals) { var ts = inst.findAll(function (n) { return n.type === 'TEXT'; }); for (var i = 0; i < ts.length; i++) { await figma.loadFontAsync(ts[i].fontName); ts[i].characters = vals[i]; } }
|
|
153
|
+
var tables = [];
|
|
154
|
+
for (var kind of ['Data', 'Empty']) {
|
|
155
|
+
var c = comp('Content=' + kind, 'VERTICAL'); stroke(c, 'Theme:border/default', 'Size:stroke/default'); radius(c, 'Size:table/radius'); fill(c, 'Theme:surface/canvas'); c.clipsContent = true;
|
|
156
|
+
add(c, [pick(rowSet, 'Row=Header')]);
|
|
157
|
+
if (kind === 'Data') for (var q = 0; q < DATA.length; q++) { var r = pick(rowSet, 'Row=' + (q === 1 ? 'Hover' : q === 2 ? 'Selected' : 'Default')); await setTexts(r, DATA[q]); if (q === DATA.length - 1) r.strokeBottomWeight = 0; add(c, [r]); }
|
|
158
|
+
else { var e = AL('HORIZONTAL', { name: 'Empty', primaryAxisAlignItems: 'CENTER', paddingTop: 40, paddingBottom: 40 }); add(e, [await T('No trips yet', 'body-condense', 'Theme:text/tertiary')]); add(c, [e]); fillW(e); }
|
|
159
|
+
tables.push(c);
|
|
160
|
+
}
|
|
161
|
+
var tblSet = figma.combineAsVariants(tables, figma.currentPage); tblSet.name = 'Table'; row(tblSet, 64);
|
|
162
|
+
tblSet.description = 'Table row instances in a table/radius box with a border/default edge. These are the same tokens the AG Grid themes use, so a hand-built table and a grid look alike.';
|
|
163
|
+
place(tblSet);
|
|
164
|
+
var col = figma.currentPage.children.filter(function (n) { return n.name === 'Components'; })[0];
|
|
165
|
+
return { col: col.id, sets: figma.currentPage.findAllWithCriteria({ types: ['COMPONENT_SET'] }).map(function (s) { return s.name + ' ' + s.id + ' ' + Math.round(s.width) + 'x' + Math.round(s.height); }), sizes: [meters[1].height, progs[3].height, sliders[0].height, rows[1].height, tables[0].height] };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Display: Separator and Scroll area. Run after 01-display.js.
|
|
2
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
3
|
+
var C = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'compHelpers'))(figma);
|
|
4
|
+
var fill = C.fill, stroke = C.stroke, bind = C.bind, radius = C.radius, AL = C.AL, add = C.add, T = C.T, page = C.page, comp = C.comp, place = C.place, removeOld = C.removeOld;
|
|
5
|
+
await page('Display');
|
|
6
|
+
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; }
|
|
7
|
+
function at(set, xy, w, h) { set.layoutMode = 'NONE'; set.children.forEach(function (k, i) { k.x = xy[i][0]; k.y = xy[i][1]; }); set.resize(w, h); frameSet(set); }
|
|
8
|
+
|
|
9
|
+
// Separator
|
|
10
|
+
removeOld('Separator');
|
|
11
|
+
var seps = [];
|
|
12
|
+
for (var o of ['Horizontal', 'Vertical']) {
|
|
13
|
+
var h = o === 'Horizontal', c = comp('Orientation=' + o, h ? 'VERTICAL' : 'HORIZONTAL');
|
|
14
|
+
c.primaryAxisSizingMode = c.counterAxisSizingMode = 'FIXED'; c.resize(h ? 320 : 1, h ? 1 : 48); c.clipsContent = false;
|
|
15
|
+
var line = figma.createRectangle(); line.name = 'Line'; line.resize(h ? 320 : 1, h ? 1 : 48); fill(line, 'Theme:border/default'); bind(line, h ? 'height' : 'width', 'Size:stroke/default');
|
|
16
|
+
add(c, [line]); if (h) { line.layoutSizingHorizontal = 'FILL'; line.layoutSizingVertical = 'FIXED'; } else { line.layoutSizingHorizontal = 'FIXED'; line.layoutSizingVertical = 'FILL'; }
|
|
17
|
+
seps.push(c);
|
|
18
|
+
}
|
|
19
|
+
var sepSet = figma.combineAsVariants(seps, figma.currentPage); sepSet.name = 'Separator'; at(sepSet, [[32, 72], [392, 48]], 760, 144);
|
|
20
|
+
sepSet.description = 'A thin line in border/default that splits content into groups, across or down. In code it keeps space/4 on both sides; in Figma, set that spacing on the parent auto layout.';
|
|
21
|
+
place(sepSet);
|
|
22
|
+
|
|
23
|
+
// Scroll area: the viewport is scrolled a little so the thumb sits part way down.
|
|
24
|
+
removeOld('Scroll area');
|
|
25
|
+
var PLACES = ['Jumbo Rocks', 'Hidden Valley', 'Ryan', 'Belle', 'White Tank', 'Indian Cove', 'Black Rock', 'Cottonwood', 'Sheep Pass'];
|
|
26
|
+
async function area(o, st) {
|
|
27
|
+
var v = o === 'Vertical', hover = st === 'Hover';
|
|
28
|
+
var c = comp('Orientation=' + o + ', State=' + st); c.layoutMode = 'NONE'; c.resize(v ? 240 : 360, v ? 200 : 112); c.clipsContent = true;
|
|
29
|
+
fill(c, 'Theme:surface/canvas'); stroke(c, 'Theme:border/subtle', 'Size:stroke/default'); radius(c, 'Size:radius/chrome');
|
|
30
|
+
var vp = AL(v ? 'VERTICAL' : 'HORIZONTAL', { name: 'Viewport' }); bind(vp, ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], 'Scale:space/3'); bind(vp, 'itemSpacing', v ? 'Scale:space/2' : 'Scale:space/3');
|
|
31
|
+
var names = v ? PLACES : PLACES.slice(0, 6);
|
|
32
|
+
for (var i = 0; i < names.length; i++) {
|
|
33
|
+
if (v) { var t = await T(names[i], 'body-base', 'Theme:text/primary'); t.name = names[i]; add(vp, [t]); continue; }
|
|
34
|
+
var card = AL('VERTICAL', { name: 'Card' }); bind(card, ['paddingTop', 'paddingLeft'], 'Scale:space/2'); fill(card, 'Theme:surface/tray'); radius(card, 'Size:radius/item');
|
|
35
|
+
var ct = await T(names[i], 'subtitle-condense', 'Theme:text/primary'); ct.name = names[i]; add(card, [ct]); add(vp, [card]);
|
|
36
|
+
card.primaryAxisSizingMode = card.counterAxisSizingMode = 'FIXED'; card.resize(120, 64);
|
|
37
|
+
}
|
|
38
|
+
c.appendChild(vp); vp.x = 0; vp.y = v ? -24 : 0;
|
|
39
|
+
var bar = AL(v ? 'VERTICAL' : 'HORIZONTAL', { name: 'Scrollbar' }); bar.primaryAxisSizingMode = bar.counterAxisSizingMode = 'FIXED';
|
|
40
|
+
bar.resize(v ? 12 : 360, v ? 200 : 12); bind(bar, v ? 'width' : 'height', 'Scale:space/3');
|
|
41
|
+
bind(bar, v ? ['paddingLeft', 'paddingRight', 'paddingBottom'] : ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], 'Scale:stroke/medium');
|
|
42
|
+
if (v) bar.paddingTop = 20;
|
|
43
|
+
if (hover) fill(bar, 'Theme:fill/subtle');
|
|
44
|
+
var th = AL('VERTICAL', { name: 'Thumb' }); th.primaryAxisSizingMode = th.counterAxisSizingMode = 'FIXED'; th.resize(v ? 8 : 150, v ? 80 : 8); th.cornerRadius = 999;
|
|
45
|
+
fill(th, hover ? 'Theme:content/scrollbar-thumb-hover' : 'Theme:content/scrollbar-thumb');
|
|
46
|
+
add(bar, [th]); if (v) { th.layoutSizingHorizontal = 'FILL'; th.layoutSizingVertical = 'FIXED'; } else { th.layoutSizingHorizontal = 'FIXED'; th.layoutSizingVertical = 'FILL'; }
|
|
47
|
+
c.appendChild(bar); bar.x = v ? 228 : 0; bar.y = v ? 0 : 100;
|
|
48
|
+
return c;
|
|
49
|
+
}
|
|
50
|
+
var areas = [];
|
|
51
|
+
for (var o of ['Vertical', 'Horizontal']) for (var st of ['Default', 'Hover']) areas.push(await area(o, st));
|
|
52
|
+
var areaSet = figma.combineAsVariants(areas, figma.currentPage); areaSet.name = 'Scroll area'; at(areaSet, [[32, 52], [432, 52], [32, 336], [432, 336]], 840, 544);
|
|
53
|
+
areaSet.description = 'A scrolling region with a slim custom scrollbar: a pill thumb space/3 wide that darkens on hover, on a track that only tints on hover. The frame around it here is just for the example.';
|
|
54
|
+
place(areaSet);
|
|
55
|
+
return [sepSet.id, areaSet.id];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Display: a header and row and column labels for each set. Run after 01 and 02.
|
|
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('Display');
|
|
5
|
+
// Short header lines; sets not listed use their descriptions.
|
|
6
|
+
return await C.labelSets({
|
|
7
|
+
Avatar: "A photo, or initials in the emphasis colors at avatar/initials-font-size-*. A ring in the page color and the elevation/sm shadow.",
|
|
8
|
+
"Avatar group": "Avatars overlapping by a third of their size, ending with a count.",
|
|
9
|
+
Meter: "A value inside a known range. The fill follows the Status mode, so Warning and Error recolor it.",
|
|
10
|
+
Progress: "How far a task has got; Indeterminate when there is no known end.",
|
|
11
|
+
Slider: "Pick a value or a range by dragging. Hover and Dragging grow a soft halo; Focus is the standard ring.",
|
|
12
|
+
"Table row": "Header on surface/tray; rows at table/row-height with list-item hover and selected tints.",
|
|
13
|
+
Table: "Rows in a bordered table/radius box, with the same tokens as the AG Grid themes."
|
|
14
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Display doc frame. Run after every Display set is built and labelled.
|
|
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, COL = L.COL, v = L.v, paint = L.paint, fill = L.fill, stroke = L.stroke, bind = L.bind, radius = L.radius, pad = L.pad, 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;
|
|
5
|
+
await page('Display');
|
|
6
|
+
var sets = {}; figma.currentPage.findAllWithCriteria({ types: ['COMPONENT_SET'] }).forEach(function (s) { sets[s.name] = s; });
|
|
7
|
+
function inst(set, name, props) {
|
|
8
|
+
var c = sets[set].children.filter(function (x) { return x.name === name; })[0]; if (!c) throw new Error(set + ' ' + name);
|
|
9
|
+
var i = c.createInstance();
|
|
10
|
+
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); }
|
|
11
|
+
return i;
|
|
12
|
+
}
|
|
13
|
+
async function notes(parent, rows) {
|
|
14
|
+
var tbl = AL('VERTICAL', { name: 'Spec' }); add(parent, [tbl]); tbl.layoutSizingHorizontal = 'FILL';
|
|
15
|
+
var hd = hrow(tbl); hd.paddingTop = 0; divider(hd);
|
|
16
|
+
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)]);
|
|
17
|
+
for (var i = 0; i < rows.length; i++) {
|
|
18
|
+
var r = hrow(tbl); r.counterAxisAlignItems = 'MIN'; r.paddingTop = r.paddingBottom = 10; divider(r);
|
|
19
|
+
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)]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function stage(parent, name) {
|
|
23
|
+
var s = AL('HORIZONTAL', { name: name || 'Examples', itemSpacing: 32, counterAxisAlignItems: 'CENTER', paddingTop: 32, paddingBottom: 32, paddingLeft: 32, paddingRight: 32, layoutWrap: 'WRAP', counterAxisSpacing: 24 });
|
|
24
|
+
fill(s, 'Theme:fill/subtle'); s.cornerRadius = 12; add(parent, [s]); s.layoutSizingHorizontal = 'FILL'; return s;
|
|
25
|
+
}
|
|
26
|
+
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; }
|
|
27
|
+
async function usage(parent, dos, donts) {
|
|
28
|
+
var r = AL('HORIZONTAL', { name: 'Usage', itemSpacing: 24 }); add(parent, [r]); r.layoutSizingHorizontal = 'FILL';
|
|
29
|
+
var pairs = [['Use it for', dos, 'Theme:status/success'], ['Avoid', donts, 'Theme:status/error']];
|
|
30
|
+
for (var i = 0; i < 2; i++) {
|
|
31
|
+
var col = AL('VERTICAL', { name: pairs[i][0], itemSpacing: 8, paddingLeft: 16 }); add(r, [col]); col.layoutSizingHorizontal = 'FILL';
|
|
32
|
+
col.strokes = [paint(pairs[i][2])]; col.strokeLeftWeight = 3; col.strokeTopWeight = col.strokeRightWeight = col.strokeBottomWeight = 0;
|
|
33
|
+
add(col, [await T(pairs[i][0], 'subtitle-base')]);
|
|
34
|
+
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'; }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
figma.currentPage.children.filter(function (n) { return n.name === 'Display' && n.type === 'FRAME'; }).forEach(function (n) { n.remove(); });
|
|
38
|
+
var doc = await docFrame('Display', 'COMPONENTS', 'Display', 'Things that show information rather than ask for it: avatars for people, meters and progress bars for amounts, sliders for picking a value along a range, and tables for rows of data. Meters, progress bars and sliders share the gauge concept (gauge/height, fill/track, a Status bold fill). Components sit to the right.');
|
|
39
|
+
doc.x = 0; doc.y = 400;
|
|
40
|
+
var pick = await section(doc, 'Choosing', 'Start from what the number means.');
|
|
41
|
+
var picks = [['Show who someone is', 'Avatar', 'A photo, or initials when there is none.'], ['A value inside a known range', 'Meter', 'Storage, battery, water left. It can go up or down.'], ['How far a task has got', 'Progress', 'Uploading, installing. It only goes up, then ends.'], ['Let people pick a value along a range', 'Slider', 'Distance, price. Show the value next to the label.'], ['Rows of data to scan and compare', 'Table', 'Small sets by hand; large or sortable sets use the AG Grid themes.']];
|
|
42
|
+
var pt = AL('VERTICAL', { name: 'Picker' }); add(pick, [pt]); pt.layoutSizingHorizontal = 'FILL';
|
|
43
|
+
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)]); }
|
|
44
|
+
|
|
45
|
+
var avs = await section(doc, 'Avatar', 'Five sizes, xs to xl. Initials use the emphasis colors, Bold by default; set the Emphasis mode on an instance for a quieter avatar, and the Status mode for a palette. Groups overlap by a third.');
|
|
46
|
+
var s1 = stage(avs, 'Sizes');
|
|
47
|
+
for (var z of ['xs', 'sm', 'md', 'lg', 'xl']) add(s1, [await captioned(inst('Avatar', 'Content=Initials, Size=' + z), z)]);
|
|
48
|
+
add(s1, [await captioned(inst('Avatar', 'Content=Image, Size=md'), 'Photo')]);
|
|
49
|
+
var s1b = stage(avs, 'Emphasis and groups');
|
|
50
|
+
for (var em of ['Bold', 'Standard', 'Subtle', 'Quiet']) { var a = inst('Avatar', 'Content=Initials, Size=md'); mode(a, 'Emphasis', em); add(s1b, [await captioned(a, em)]); }
|
|
51
|
+
add(s1b, [await captioned(inst('Avatar group', 'Size=md'), 'Group')]);
|
|
52
|
+
await notes(avs, [['Size', 'avatar/size-xs … xl', 'Five steps of the spacing scale.'], ['Initials', 'title-base · avatar/initials-font-size-*', 'Scaled to the circle.'], ['Colors', 'emphasis/bg · emphasis/fg', 'Bold unless the Emphasis mode says otherwise.'], ['Ring', 'stroke/emphasis · surface/canvas', 'Keeps overlapping avatars apart.'], ['Shadow', 'elevation/sm', 'A slight lift.']]);
|
|
53
|
+
|
|
54
|
+
var gs = await section(doc, 'Meter and Progress', 'The same track and fill. A meter shows where a value sits in a range; progress shows how far a task has got. The fill is the Status collection\'s bold color, so set Status to Warning or Error when a meter is in a bad range.');
|
|
55
|
+
var s2 = stage(gs, 'Meters'); s2.counterAxisAlignItems = 'MIN';
|
|
56
|
+
var MS = [['Default', 'Water left', '62%'], ['Warning', 'Fuel', '18%'], ['Error', 'Battery', '6%']];
|
|
57
|
+
for (var m of MS) { var a = inst('Meter', 'Size=md', { Label: m[1], Value: m[2] }); mode(a, 'Status', m[0]); add(s2, [await captioned(a, m[0])]); }
|
|
58
|
+
var s2b = stage(gs, 'Progress'); s2b.counterAxisAlignItems = 'MIN';
|
|
59
|
+
for (var z of ['sm', 'md', 'lg']) add(s2b, [await captioned(inst('Progress', 'Value=Determinate, Size=' + z), z)]);
|
|
60
|
+
add(s2b, [await captioned(inst('Progress', 'Value=Indeterminate, Size=md'), 'Indeterminate')]);
|
|
61
|
+
await usage(gs, ['A label that says what is measured, and the value as text.', 'Warning and Error status for meters in a bad range.'], ['Color alone to show a bad value. Say it in the label or value too.', 'Progress bars that go backwards.']);
|
|
62
|
+
await notes(gs, [['Track', 'fill/track · gauge/radius', 'Fully rounded.'], ['Height', 'gauge/height-sm · height · height-lg', 'space/1, 2 and 3.'], ['Fill', 'Status bold/bg', 'Accent by default; follows the Status mode.'], ['Label', 'label-base · text/primary', 'Says what is measured.'], ['Value', 'body-minimize · text/secondary', 'The number as text, so color is not the only signal.'], ['Gap', 'gauge/gap', 'Between the label row and the track.']]);
|
|
63
|
+
|
|
64
|
+
var ss = await section(doc, 'Slider', 'Drag a thumb along the track to pick a value, or two thumbs for a range. The thumb sits on the field surface with a Status bold edge. Hover and dragging grow a soft accent halo; keyboard focus shows the standard ring.');
|
|
65
|
+
var s3 = stage(ss, 'States'); s3.counterAxisAlignItems = 'MIN';
|
|
66
|
+
for (var st of ['Default', 'Hover', 'Focus', 'Dragging', 'Disabled']) add(s3, [await captioned(inst('Slider', 'Kind=Single, State=' + st), st)]);
|
|
67
|
+
add(s3, [await captioned(inst('Slider', 'Kind=Range, State=Default'), 'Range')]);
|
|
68
|
+
await notes(ss, [['Track', 'gauge/height-sm · fill/track', 'Thinner than a meter, so the thumb leads.'], ['Thumb', 'slider/thumb-size · surface/canvas · stroke/emphasis', 'Status bold/border edge.'], ['Halo', 'scope soft-hover · soft-active', 'Grows on hover and while dragging.'], ['Focus', 'focus/ring (effect style)', 'The same ring as every control.'], ['Disabled', 'check/opacity-disabled', 'Fades as a whole, like a checkbox.']]);
|
|
69
|
+
|
|
70
|
+
var tbs = await section(doc, 'Table', 'Rows of data in a bordered box. The header sits on the tray surface; rows are separated by subtle lines, and hover and selection use the same tints as list items. These are the tokens the AG Grid themes read, so a small hand-built table and a full grid match. Base UI has no table, so in code this is a plain HTML <table class="Table"> styled by loomiere-base-ui.');
|
|
71
|
+
var s4 = stage(tbs, 'Tables'); s4.counterAxisAlignItems = 'MIN';
|
|
72
|
+
add(s4, [await captioned(inst('Table', 'Content=Data'), 'Data, with a hovered and a selected row'), await captioned(inst('Table', 'Content=Empty'), 'Empty')]);
|
|
73
|
+
await usage(tbs, ['Data people compare across rows.', 'Numbers aligned right so digits line up.', 'An empty state that says what will appear.'], ['Layout. Use a grid of cards.', 'Large, sortable or editable data by hand. Use AG Grid with the Loomiere theme.']);
|
|
74
|
+
await notes(tbs, [['Header', 'surface/tray · subtitle-condense · table/header-height', 'Set apart from the rows.'], ['Row', 'table/row-height · body-condense', 'The small control height, so rows stay compact.'], ['Lines', 'border/subtle (rows) · border/default (edge)', 'Quiet inside, firmer at the edge.'], ['Hover', 'interactive/tint-hover · text/on-tint', 'The same as a list item.'], ['Selected', 'interactive/tint-active · text/on-tint-strong', 'Stronger than hover.'], ['Cell padding', 'table/cell-padding-inline', 'The small control padding.'], ['Corners', 'table/radius', 'The surface radius, like cards.']]);
|
|
75
|
+
var over = [];
|
|
76
|
+
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)); });
|
|
77
|
+
|
|
78
|
+
// Sections for the later sets use the shared doc kit; its stages top-align their examples.
|
|
79
|
+
var K = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docKit'))(figma);
|
|
80
|
+
var sps = await section(doc, 'Separator', 'A thin line that splits content into groups, across or down. Use it when space alone does not make the groups clear.');
|
|
81
|
+
var k1 = K.stage(sps, 'Orientation');
|
|
82
|
+
var c1 = AL('VERTICAL', { name: 'Card', itemSpacing: 16, paddingTop: 24, paddingBottom: 24, paddingLeft: 24, paddingRight: 24, cornerRadius: 12 }); fill(c1, 'Theme:surface/canvas');
|
|
83
|
+
add(c1, [await T('Trip details', 'subtitle-base'), await T('Joshua Tree, three nights', 'body-condense', 'Theme:text/secondary')]);
|
|
84
|
+
var sh = K.inst('Separator', 'Orientation=Horizontal'); add(c1, [sh]);
|
|
85
|
+
add(c1, [await T('Gear list', 'subtitle-base'), await T('Tent, stove, 6 gallons of water', 'body-condense', 'Theme:text/secondary')]);
|
|
86
|
+
c1.resize(360, c1.height); c1.counterAxisSizingMode = 'FIXED'; sh.layoutSizingHorizontal = 'FILL';
|
|
87
|
+
var c2 = AL('HORIZONTAL', { name: 'Card', itemSpacing: 16, counterAxisAlignItems: 'CENTER', paddingTop: 24, paddingBottom: 24, paddingLeft: 24, paddingRight: 24, cornerRadius: 12 }); fill(c2, 'Theme:surface/canvas');
|
|
88
|
+
var sv = K.inst('Separator', 'Orientation=Vertical'); sv.resize(1, 24);
|
|
89
|
+
add(c2, [await T('Edit', 'trigger-condense'), await T('Share', 'trigger-condense'), sv, await T('Delete', 'trigger-condense', 'Theme:text/secondary')]);
|
|
90
|
+
add(k1, [await K.captioned(c1, 'Horizontal: between groups'), await K.captioned(c2, 'Vertical: between sets of actions')]);
|
|
91
|
+
await K.notes(sps, [['Line', 'border/default · stroke/default', 'Visible, but quieter than a control edge.'], ['Space', 'space/4 each side', 'Set on the parent auto layout in Figma.']]);
|
|
92
|
+
var sas = await section(doc, 'Scroll area', 'A scrolling region with a slim scrollbar that matches the theme instead of the browser default. The thumb is a pill that darkens on hover; the track stays clear until the pointer is over it.');
|
|
93
|
+
var k2 = K.stage(sas, 'States');
|
|
94
|
+
for (var o of ['Vertical', 'Horizontal']) for (var st of ['Default', 'Hover']) add(k2, [await K.captioned(K.inst('Scroll area', 'Orientation=' + o + ', State=' + st), o + ', ' + st.toLowerCase())]);
|
|
95
|
+
await K.notes(sas, [['Scrollbar', 'space/3 wide · stroke/medium padding', 'Slim, but the hit area is at least 44px in code.'], ['Thumb', 'content/scrollbar-thumb · radius/pill', 'scrollbar-thumb-hover on hover and while dragging.'], ['Track', 'transparent · fill/subtle on hover', 'Out of the way until needed.']]);
|
|
96
|
+
return { id: doc.id, h: Math.round(doc.height) };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Cover: the file's first page. A title block, the contents, a palette strip and a showcase of live
|
|
2
|
+
// components. Run last, after every component page. Edit NAME, LEAD and BASE_UI for a new brand.
|
|
3
|
+
// Figma can't set the file thumbnail from a script: right-click the Cover frame > Set as thumbnail.
|
|
4
|
+
var NAME = 'Mojave';
|
|
5
|
+
var LEAD = 'A desert theme with a violet lead. Every color, size and type style is a variable, and every Base UI component is here, ready for light, dark, high contrast and color vision themes.';
|
|
6
|
+
var BASE_UI = '1.9';
|
|
7
|
+
var FOUNDATIONS = ['Palettes', 'Color', 'Type', 'Scale', 'Size', 'Elevation, focus and motion', 'Themes'];
|
|
8
|
+
var COMPONENTS = ['Fields', 'Buttons', 'Popups', 'Overlays', 'Disclosure and navigation', 'Display'];
|
|
9
|
+
var PALETTES = ['primary', 'secondary', 'tertiary', 'quaternary', 'neutral'];
|
|
10
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
11
|
+
var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
|
|
12
|
+
var fill = L.fill, radius = L.radius, AL = L.AL, add = L.add, T = L.T, mode = L.mode, page = L.page, COL = L.COL;
|
|
13
|
+
var ES = {}; (await figma.getLocalEffectStylesAsync()).forEach(function (s) { ES[s.name] = s; });
|
|
14
|
+
var sets = {};
|
|
15
|
+
async function setOn(pageName, name) { var s = await L.setOn(pageName, name); sets[name] = s; return s; }
|
|
16
|
+
function inst(name, variant, props) {
|
|
17
|
+
var c = sets[name].children.filter(function (x) { return x.name === variant; })[0]; if (!c) throw new Error(name + ' ' + variant);
|
|
18
|
+
var i = c.createInstance();
|
|
19
|
+
if (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); }
|
|
20
|
+
return i;
|
|
21
|
+
}
|
|
22
|
+
for (var s of [['Fields', 'Field'], ['Fields', 'Select'], ['Fields', 'Checkbox'], ['Fields', 'Switch'], ['Buttons', 'Button'], ['Disclosure and navigation', 'Tab list'], ['Display', 'Meter'], ['Display', 'Slider'], ['Display', 'Avatar group'], ['Overlays', 'Toast']]) await setOn(s[0], s[1]);
|
|
23
|
+
|
|
24
|
+
var p = await page('Cover');
|
|
25
|
+
if (!L.PREFIX) figma.root.insertChild(0, p);
|
|
26
|
+
p.children.filter(function (n) { return n.name === 'Cover'; }).forEach(function (n) { n.remove(); });
|
|
27
|
+
var cov = figma.createFrame(); cov.name = 'Cover'; cov.resize(1600, 960); cov.x = 0; cov.y = 0; fill(cov, 'Theme:surface/canvas'); cov.clipsContent = true;
|
|
28
|
+
|
|
29
|
+
// Showcase: real instances on the accent soft fill, so the cover follows the theme.
|
|
30
|
+
var show = figma.createFrame(); show.name = 'Showcase'; show.resize(760, 960); fill(show, 'Theme:scope/accent/soft'); show.clipsContent = true;
|
|
31
|
+
cov.appendChild(show); show.x = 840; show.y = 0;
|
|
32
|
+
function card(name, w) {
|
|
33
|
+
var c = AL('VERTICAL', { name: name, itemSpacing: 20, paddingTop: 28, paddingBottom: 28, paddingLeft: 28, paddingRight: 28 });
|
|
34
|
+
fill(c, 'Theme:surface/canvas'); radius(c, 'Size:radius/chrome'); c.counterAxisSizingMode = 'FIXED'; c.resize(w, 100); c.primaryAxisSizingMode = 'AUTO';
|
|
35
|
+
return c;
|
|
36
|
+
}
|
|
37
|
+
var book = card('Booking', 360); await book.setEffectStyleIdAsync(ES['elevation/lg'].id);
|
|
38
|
+
add(book, [await T('Book a campsite', 'title-magnify')]);
|
|
39
|
+
var f = inst('Field', 'State=Default', { Label: 'Email', Description: 'We only use it to send your trip plan.' }); add(book, [f]); f.layoutSizingHorizontal = 'FILL';
|
|
40
|
+
var sel = inst('Select', 'Size=md, State=Default, Content=Value', { Value: 'Joshua Tree' }); add(book, [sel]); sel.layoutSizingHorizontal = 'FILL';
|
|
41
|
+
add(book, [inst('Checkbox', 'Size=md, Value=On, State=Default', { Label: 'Email me a reminder' })]);
|
|
42
|
+
var acts = AL('HORIZONTAL', { name: 'Actions', itemSpacing: 12 });
|
|
43
|
+
var b1 = inst('Button', 'Emphasis=Bold, Size=md, State=Default, Content=Label', { Label: 'Book site' }); mode(b1, 'Emphasis', 'Bold');
|
|
44
|
+
var b2 = inst('Button', 'Emphasis=Standard, Size=md, State=Default, Content=Label', { Label: 'Cancel' }); mode(b2, 'Emphasis', 'Standard');
|
|
45
|
+
add(acts, [b1, b2]); add(book, [acts]);
|
|
46
|
+
show.appendChild(book); book.x = 72; book.y = 88;
|
|
47
|
+
var trip = card('Trip', 400); await trip.setEffectStyleIdAsync(ES['elevation/md'].id);
|
|
48
|
+
add(trip, [inst('Tab list', 'Orientation=Horizontal')]);
|
|
49
|
+
var m = inst('Meter', 'Size=md'); add(trip, [m]); m.layoutSizingHorizontal = 'FILL';
|
|
50
|
+
var sl = inst('Slider', 'Kind=Single, State=Default'); add(trip, [sl]); sl.layoutSizingHorizontal = 'FILL';
|
|
51
|
+
var ppl = AL('HORIZONTAL', { name: 'People', itemSpacing: 12, counterAxisAlignItems: 'CENTER' });
|
|
52
|
+
add(ppl, [inst('Avatar group', 'Size=md'), inst('Switch', 'Size=md, Value=On, State=Default', { Label: 'Shared' })]); add(trip, [ppl]);
|
|
53
|
+
show.appendChild(trip); trip.x = 296; trip.y = 500;
|
|
54
|
+
var toast = inst('Toast', 'Type=Success'); show.appendChild(toast); toast.x = 72; toast.y = 812;
|
|
55
|
+
|
|
56
|
+
// Title block
|
|
57
|
+
var tb = AL('VERTICAL', { name: 'Title block', itemSpacing: 20 });
|
|
58
|
+
var lead = await T(LEAD, 'body-magnify', 'Theme:text/secondary');
|
|
59
|
+
add(tb, [await T('LOOMIERE DESIGN SYSTEM', 'eyebrow-condense', 'Theme:text/accent'), await T(NAME, 'title-maximize'), lead]);
|
|
60
|
+
// FIXED: a hugging title block would shrink to the title, since the lead fills it.
|
|
61
|
+
tb.counterAxisSizingMode = 'FIXED'; tb.resize(600, tb.height); lead.layoutSizingHorizontal = 'FILL'; lead.textAutoResize = 'HEIGHT';
|
|
62
|
+
cov.appendChild(tb); tb.x = 96; tb.y = 120;
|
|
63
|
+
|
|
64
|
+
// Contents: the page names, by group.
|
|
65
|
+
var ct = AL('HORIZONTAL', { name: 'Contents', itemSpacing: 96 });
|
|
66
|
+
for (var g of [['Foundations', FOUNDATIONS], ['Components', COMPONENTS]]) {
|
|
67
|
+
var col = AL('VERTICAL', { name: g[0], itemSpacing: 8 }); add(col, [await T(g[0].toUpperCase(), 'eyebrow-condense', 'Theme:text/tertiary')]);
|
|
68
|
+
for (var n of g[1]) add(col, [await T(n, 'subtitle-condense')]);
|
|
69
|
+
add(ct, [col]);
|
|
70
|
+
}
|
|
71
|
+
cov.appendChild(ct); ct.x = 96; ct.y = 400;
|
|
72
|
+
|
|
73
|
+
// Palette strip: steps 100 to 1000 of each role palette, straight from the Brand collection.
|
|
74
|
+
var pal = AL('VERTICAL', { name: 'Palettes', itemSpacing: 6 });
|
|
75
|
+
for (var name of PALETTES) {
|
|
76
|
+
var r = AL('HORIZONTAL', { name: name, itemSpacing: 4 });
|
|
77
|
+
for (var step = 100; step <= 1000; step += 100) { var sw = figma.createRectangle(); sw.name = String(step); sw.resize(56, 28); sw.cornerRadius = 6; fill(sw, 'Brand:default/color/' + name + '/' + step); add(r, [sw]); }
|
|
78
|
+
add(pal, [r]);
|
|
79
|
+
}
|
|
80
|
+
cov.appendChild(pal); pal.x = 96; pal.y = 680;
|
|
81
|
+
|
|
82
|
+
var MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], d = new Date();
|
|
83
|
+
var themes = COL.Theme.modes.length;
|
|
84
|
+
var foot = await T('Base UI ' + BASE_UI + ' · ' + themes + ' themes · Desktop, tablet and mobile sizes · Updated ' + d.getDate() + ' ' + MONTHS[d.getMonth()] + ' ' + d.getFullYear(), 'body-condense', 'Theme:text/tertiary');
|
|
85
|
+
cov.appendChild(foot); foot.x = 96; foot.y = 880;
|
|
86
|
+
p.appendChild(cov);
|
|
87
|
+
return { id: cov.id };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": "The order to build the library pages in. Later steps use components from earlier ones: Fields uses Button, Themes uses Fields, and every page after Buttons uses Button.",
|
|
3
|
+
"steps": [
|
|
4
|
+
{ "page": "Palettes", "files": ["pages/02-palettes.js"] },
|
|
5
|
+
{ "page": "Color", "files": ["pages/03-color.js"] },
|
|
6
|
+
{ "page": "Type", "files": ["pages/04-type.js"] },
|
|
7
|
+
{ "page": "Scale", "files": ["pages/05-scale.js"] },
|
|
8
|
+
{ "page": "Size", "files": ["pages/06-size.js"] },
|
|
9
|
+
{ "page": "Elevation, focus and motion", "files": ["pages/07-elevation-focus-motion.js"] },
|
|
10
|
+
{ "page": "Fields", "files": ["pages/09-fields/01-input.js", "pages/09-fields/02-checks.js", "pages/09-fields/03-field.js"] },
|
|
11
|
+
{ "page": "Buttons", "files": ["pages/10-buttons/01-buttons.js"] },
|
|
12
|
+
{ "page": "Fields", "files": ["pages/09-fields/04-more-fields.js", "pages/09-fields/05-labels.js", "pages/09-fields/06-doc.js"] },
|
|
13
|
+
{ "page": "Themes", "files": ["pages/08-themes.js"] },
|
|
14
|
+
{ "page": "Buttons", "files": ["pages/10-buttons/02-doc.js"] },
|
|
15
|
+
{ "page": "Popups", "files": ["pages/11-popups/01-popups.js", "pages/11-popups/02-more-popups.js", "pages/11-popups/03-labels.js", "pages/11-popups/04-doc.js"] },
|
|
16
|
+
{ "page": "Overlays", "files": ["pages/12-overlays/01-overlays.js", "pages/12-overlays/02-labels.js", "pages/12-overlays/03-doc.js"] },
|
|
17
|
+
{ "page": "Disclosure and navigation", "files": ["pages/13-disclosure/01-disclosure.js", "pages/13-disclosure/02-collapsible.js", "pages/13-disclosure/03-labels.js", "pages/13-disclosure/04-doc.js"] },
|
|
18
|
+
{ "page": "Display", "files": ["pages/14-display/01-display.js", "pages/14-display/02-more-display.js", "pages/14-display/03-labels.js", "pages/14-display/04-doc.js"] },
|
|
19
|
+
{ "page": "Cover", "files": ["pages/15-cover.js"] }
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Compares a check run's fingerprints with check/mojave.json: node tools/compare.mjs <Page> '<units json>'
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
const base = JSON.parse(fs.readFileSync(new URL('../check/mojave.json', import.meta.url), 'utf8')).pages;
|
|
4
|
+
const [page, json] = process.argv.slice(2);
|
|
5
|
+
const want = base[page], got = JSON.parse(json);
|
|
6
|
+
if (!want) throw new Error(`No page ${page} in mojave.json`);
|
|
7
|
+
const keys = [...new Set([...Object.keys(want), ...Object.keys(got)])];
|
|
8
|
+
const bad = keys.filter((k) => want[k] !== got[k]);
|
|
9
|
+
console.log(`${page}: ${keys.length - bad.length}/${keys.length} match`);
|
|
10
|
+
for (const k of bad) console.log(` ${k}: ${want[k] ?? 'missing in Mojave'} → ${got[k] ?? 'missing in check'}`);
|
|
11
|
+
process.exitCode = bad.length ? 1 : 0;
|