@scattered-light/loomiere 0.4.0 → 0.4.1
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/figma/README.md +8 -7
- 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/package.json +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
2
|
+
var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
|
|
3
|
+
var V = L.V, v = L.v, fill = L.fill, stroke = L.stroke, bind = L.bind, radius = L.radius, AL = L.AL, add = L.add, T = L.T, mode = L.mode, page = L.page, docFrame = L.docFrame, section = L.section, hrow = L.hrow, divider = L.divider;
|
|
4
|
+
var p = await page('Elevation, focus and motion');
|
|
5
|
+
var doc = await docFrame('Elevation, focus and motion', 'FOUNDATIONS', 'Elevation, focus and motion', 'How things sit above the page, how focus shows, and how things move. Shadows are effect styles: apply elevation/xs to 2xl, focus/ring, focus/field or focus/field-error from the style picker, and they stay in sync with the tokens. Motion has no Figma equivalent, so its values are listed for prototypes.');
|
|
6
|
+
doc.x = 0; doc.y = 400;
|
|
7
|
+
var ES = {}; (await figma.getLocalEffectStylesAsync()).forEach(function (s) { ES[s.name] = s; });
|
|
8
|
+
function sh(y, b, s, a) { return { type: 'DROP_SHADOW', color: { r: 0, g: 0, b: 0, a: a }, offset: { x: 0, y: y }, radius: b, spread: s, visible: true, blendMode: 'NORMAL', showShadowBehindNode: false }; }
|
|
9
|
+
async function label(name, value, w) {
|
|
10
|
+
var box = AL('VERTICAL', { name: 'Label', itemSpacing: 2 });
|
|
11
|
+
add(box, [await T(name, 'subtitle-base'), await T(value, 'body-condense', 'Theme:text/secondary', w || 200)]);
|
|
12
|
+
box.resize(w || 200, box.height); box.counterAxisSizingMode = 'FIXED';
|
|
13
|
+
return box;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Elevation
|
|
17
|
+
var el = await section(doc, 'Elevation', 'Seven steps of shadow, from a hairline to a large soft drop, each an effect style named elevation/<step>. Components name a job, not a step: popups (menus, selects, popovers, tooltips) use lg, modals (dialogs, drawers) use xl.');
|
|
18
|
+
var E = [
|
|
19
|
+
['xs', '0 0 0 1px · 5%', [sh(0, 0, 1, 0.05)], 'Hairline edge. The reset puts it on images.'],
|
|
20
|
+
['sm', '0 1 2 · 5%', [sh(1, 2, 0, 0.05)], 'Switch thumb, avatar.'],
|
|
21
|
+
['base', '0 1 3 · 10% + 0 1 2 · 6%', [sh(1, 3, 0, 0.1), sh(1, 2, 0, 0.06)], 'Resting cards.'],
|
|
22
|
+
['md', '0 4 6 -1 · 10% + 0 2 4 -1 · 6%', [sh(4, 6, -1, 0.1), sh(2, 4, -1, 0.06)], 'Raised cards, hover lift.'],
|
|
23
|
+
['lg', '0 10 15 -3 · 10% + 0 4 6 -2 · 5%', [sh(10, 15, -3, 0.1), sh(4, 6, -2, 0.05)], 'popup/shadow: menus, selects, popovers, tooltips.'],
|
|
24
|
+
['xl', '0 20 25 -5 · 10% + 0 10 10 -5 · 4%', [sh(20, 25, -5, 0.1), sh(10, 10, -5, 0.04)], 'modal/shadow: dialogs, drawers.'],
|
|
25
|
+
['2xl', '0 25 50 -12 · 25%', [sh(25, 50, -12, 0.25)], 'The largest lift. Spare.']
|
|
26
|
+
];
|
|
27
|
+
var eg = AL('HORIZONTAL', { name: 'Steps', itemSpacing: 32, counterAxisSpacing: 40, layoutWrap: 'WRAP', paddingTop: 16, paddingBottom: 16, paddingLeft: 16, paddingRight: 16, cornerRadius: 12 });
|
|
28
|
+
fill(eg, 'Theme:surface/tray'); add(el, [eg]); eg.layoutSizingHorizontal = 'FILL';
|
|
29
|
+
for (var i = 0; i < E.length; i++) {
|
|
30
|
+
var cell = AL('VERTICAL', { name: 'elevation/' + E[i][0], itemSpacing: 12 });
|
|
31
|
+
var card = figma.createFrame(); card.name = 'Card'; card.resize(160, 96); fill(card, 'Theme:surface/canvas'); radius(card, 'Size:radius/surface'); await card.setEffectStyleIdAsync(ES['elevation/' + E[i][0]].id);
|
|
32
|
+
// A spread shadow only shows on a frame that clips.
|
|
33
|
+
card.clipsContent = E[i][2].some(function (e) { return e.spread; });
|
|
34
|
+
add(cell, [card, await label('elevation/' + E[i][0], E[i][1], 160), await T(E[i][3], 'body-condense', 'Theme:text/tertiary', 160)]);
|
|
35
|
+
add(eg, [cell]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Focus
|
|
39
|
+
var fo = await section(doc, 'Focus', 'Keyboard focus is a 2px ring with a 2px gap, so it never blends into a control\'s border, plus a pale halo just outside it. The ring carries the contrast (WCAG 2.4.11 and 1.4.11); the halo only softens it. Fields show focus differently: their border turns accent and a 3px tint glows around it, red when the field has an error. Effect styles: focus/ring for controls, focus/field and focus/field-error for fields.');
|
|
40
|
+
var fspec = AL('HORIZONTAL', { name: 'Parts', itemSpacing: 32 }); add(fo, [fspec]);
|
|
41
|
+
add(fspec, [await label('focus/ring-width', '2px · Size', 180), await label('focus/outline-offset', '2px gap · Size', 180), await label('focus/ring', 'Color · Theme', 180), await label('Halo', 'gap + ring + 2px of interactive/tint-hover', 220)]);
|
|
42
|
+
async function ring(child, rk) {
|
|
43
|
+
var w = AL('HORIZONTAL', { name: 'Focus ring', paddingTop: 2, paddingBottom: 2, paddingLeft: 2, paddingRight: 2 });
|
|
44
|
+
bind(w, ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], 'Size:focus/outline-offset');
|
|
45
|
+
w.fills = []; stroke(w, 'Theme:focus/ring', 'Size:focus/ring-width'); w.strokeAlign = 'OUTSIDE'; radius(w, rk);
|
|
46
|
+
var e = { type: 'DROP_SHADOW', color: { r: 0, g: 0, b: 0, a: 1 }, offset: { x: 0, y: 0 }, radius: 0, spread: 4, visible: true, blendMode: 'NORMAL', showShadowBehindNode: false };
|
|
47
|
+
var hv = V['Theme:interactive/tint-hover']; if (hv) e = figma.variables.setBoundVariableForEffect(e, 'color', hv);
|
|
48
|
+
w.effects = [e]; w.clipsContent = true; add(w, [child]); return w;
|
|
49
|
+
}
|
|
50
|
+
async function btn(text, em) {
|
|
51
|
+
var c = AL('HORIZONTAL', { name: em, primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER', paddingTop: 8, paddingBottom: 8, paddingLeft: 16, paddingRight: 16 });
|
|
52
|
+
fill(c, 'Emphasis:emphasis/bg'); stroke(c, 'Emphasis:emphasis/border', 'Size:stroke/default'); radius(c, 'Size:button/radius'); mode(c, 'Emphasis', em);
|
|
53
|
+
add(c, [await T(text, 'trigger-base', 'Emphasis:emphasis/fg')]); return c;
|
|
54
|
+
}
|
|
55
|
+
async function field(text, err, focused) {
|
|
56
|
+
var c = AL('HORIZONTAL', { name: err ? 'Field error' : 'Field', counterAxisAlignItems: 'CENTER', paddingLeft: 12, paddingRight: 12 });
|
|
57
|
+
fill(c, 'Theme:surface/canvas'); stroke(c, err ? 'Theme:status/error' : (focused ? 'Theme:border/accent' : 'Theme:border/control'), 'Size:stroke/default'); radius(c, 'Size:radius/control');
|
|
58
|
+
add(c, [await T(text, 'body-base', 'Theme:text/primary')]);
|
|
59
|
+
c.resize(172, 40); c.counterAxisSizingMode = 'FIXED'; c.primaryAxisSizingMode = 'FIXED';
|
|
60
|
+
if (V['Size:control/height-md']) bind(c, 'height', 'Size:control/height-md');
|
|
61
|
+
if (focused) { await c.setEffectStyleIdAsync(ES[err ? 'focus/field-error' : 'focus/field'].id); c.clipsContent = true; }
|
|
62
|
+
return c;
|
|
63
|
+
}
|
|
64
|
+
var fg = AL('HORIZONTAL', { name: 'Light and dark', itemSpacing: 16 }); add(fo, [fg]); fg.layoutSizingHorizontal = 'FILL';
|
|
65
|
+
var MODES = ['Light', 'Dark'];
|
|
66
|
+
for (var m = 0; m < MODES.length; m++) {
|
|
67
|
+
var f = AL('VERTICAL', { name: MODES[m], itemSpacing: 20, paddingTop: 24, paddingBottom: 28, paddingLeft: 24, paddingRight: 24, cornerRadius: 12 });
|
|
68
|
+
fill(f, 'Theme:surface/canvas'); stroke(f, 'Theme:border/default', 1); mode(f, 'Theme', MODES[m]);
|
|
69
|
+
add(fg, [f]); f.layoutSizingHorizontal = 'FILL';
|
|
70
|
+
add(f, [await T(MODES[m].toUpperCase(), 'eyebrow-condense', 'Theme:text/tertiary')]);
|
|
71
|
+
var r1 = AL('HORIZONTAL', { name: 'Ring', itemSpacing: 24, counterAxisAlignItems: 'CENTER' });
|
|
72
|
+
add(r1, [await ring(await btn('Save', 'Bold'), 'Size:button/radius'), await ring(await btn('Cancel', 'Standard'), 'Size:button/radius'), await ring(await btn('Skip', 'Quiet'), 'Size:button/radius')]);
|
|
73
|
+
add(f, [await T('Ring and halo on buttons', 'subtitle-condense', 'Theme:text/secondary'), r1]);
|
|
74
|
+
var r2 = AL('HORIZONTAL', { name: 'Fields', itemSpacing: 16 });
|
|
75
|
+
add(r2, [await field('Resting', false, false), await field('Focused', false, true), await field('Focused with error', true, true)]);
|
|
76
|
+
add(f, [await T('Fields: accent border and a 3px glow', 'subtitle-condense', 'Theme:text/secondary'), r2]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Motion
|
|
80
|
+
var mo = await section(doc, 'Motion', 'Three durations and four easings. Durations are the brand\'s speed times a base, so one brand input makes everything quicker or slower. Each job picks a pair. Figma has no motion variables, so use these values in prototypes.');
|
|
81
|
+
var dur = [['duration/fast', 100, 'Hover and press, small state changes.'], ['duration/normal', 200, 'Most transitions: chevrons, switches, field focus, modals.'], ['duration/slow', 300, 'Large moves, such as the navigation menu panel.']];
|
|
82
|
+
for (var i = 0; i < dur.length; i++) {
|
|
83
|
+
var row = hrow(mo); row.name = dur[i][0]; row.counterAxisAlignItems = 'CENTER';
|
|
84
|
+
add(row, [await label(dur[i][0], dur[i][1] + 'ms · ' + (dur[i][1] / 100) + ' × base')]);
|
|
85
|
+
var bar = figma.createRectangle(); bar.name = 'Bar'; bar.cornerRadius = 2; fill(bar, 'Theme:interactive/tint-active'); bar.resize(dur[i][1], 14);
|
|
86
|
+
add(row, [bar, await T(dur[i][2], 'body-condense', 'Theme:text/tertiary', 360)]);
|
|
87
|
+
}
|
|
88
|
+
var EZ = [['easing/default', [0.4, 0, 0.2, 1], 'Most UI changes: a quick start and a gentle settle.'], ['easing/out', [0, 0, 0.2, 1], 'Things arriving: popups and modals opening.'], ['easing/in', [0.4, 0, 1, 1], 'Things leaving the screen.'], ['easing/in-out', [0.4, 0, 0.2, 1], 'Things moving from one place to another, like a switch thumb.']];
|
|
89
|
+
var eg2 = AL('HORIZONTAL', { name: 'Easings', itemSpacing: 32, paddingTop: 16 }); add(mo, [eg2]);
|
|
90
|
+
for (var i = 0; i < EZ.length; i++) {
|
|
91
|
+
var c = AL('VERTICAL', { name: EZ[i][0], itemSpacing: 12 });
|
|
92
|
+
var box = figma.createFrame(); box.name = 'Curve'; box.resize(96, 72); fill(box, 'Theme:surface/tray'); box.cornerRadius = 8; box.clipsContent = false;
|
|
93
|
+
var b = EZ[i][1], W = 80, H = 56, ox = 8, oy = 8;
|
|
94
|
+
var vec = figma.createVector(); vec.name = 'Bezier';
|
|
95
|
+
vec.vectorPaths = [{ windingRule: 'NONE', data: 'M 0 ' + H + ' C ' + (b[0] * W) + ' ' + (H - b[1] * H) + ' ' + (b[2] * W) + ' ' + (H - b[3] * H) + ' ' + W + ' 0' }];
|
|
96
|
+
vec.fills = []; stroke(vec, 'Theme:text/accent', 2); box.appendChild(vec); vec.x = ox; vec.y = oy;
|
|
97
|
+
add(c, [box, await label(EZ[i][0], 'cubic-bezier(' + b.join(', ') + ')', 180), await T(EZ[i][2], 'body-condense', 'Theme:text/tertiary', 180)]);
|
|
98
|
+
add(eg2, [c]);
|
|
99
|
+
}
|
|
100
|
+
var jobs = await section(doc, 'Jobs', 'The pairs components use. In code these are --sl-transition-* tokens or the duration and easing directly.');
|
|
101
|
+
var J = [['transition/button', 'fast', 'default', 'Buttons respond at once.'], ['transition/dropdown', 'fast', 'out', 'Dropdowns open fast and settle softly.'], ['transition/input', 'normal', 'default', 'Field focus is noticeable, not slow.'], ['transition/modal', 'normal', 'out', 'Modals cover the page, so they take a little longer.'], ['Switch', 'normal', 'in-out', 'The thumb slides across.'], ['Navigation menu panel', 'slow', 'out', 'The largest move on screen.'], ['Reduced motion', '—', '—', 'With prefers-reduced-motion, every duration drops to 1ms: things change at once instead of moving. Shadows stay.']];
|
|
102
|
+
var head = hrow(jobs); head.paddingTop = 0; divider(head);
|
|
103
|
+
add(head, [await T('JOB', 'eyebrow-condense', 'Theme:text/tertiary', 220), await T('DURATION', 'eyebrow-condense', 'Theme:text/tertiary', 120), await T('EASING', 'eyebrow-condense', 'Theme:text/tertiary', 120), await T('WHY', 'eyebrow-condense', 'Theme:text/tertiary', 420)]);
|
|
104
|
+
for (var i = 0; i < J.length; i++) {
|
|
105
|
+
var row = hrow(jobs); divider(row); row.name = J[i][0];
|
|
106
|
+
add(row, [await T(J[i][0], 'subtitle-base', 'Theme:text/primary', 220), await T(J[i][1], 'body-condense', 'Theme:text/secondary', 120), await T(J[i][2], 'body-condense', 'Theme:text/secondary', 120), await T(J[i][3], 'body-condense', 'Theme:text/tertiary', 420)]);
|
|
107
|
+
}
|
|
108
|
+
return { page: p.id, doc: doc.id, h: Math.round(doc.height) };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Themes: the same controls in all eight Theme modes. Run after the Fields page.
|
|
2
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
3
|
+
var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
|
|
4
|
+
var fill = L.fill, stroke = L.stroke, radius = L.radius, AL = L.AL, add = L.add, T = L.T, mode = L.mode, page = L.page, docFrame = L.docFrame, section = L.section;
|
|
5
|
+
var fp = L.findPage('Fields'); await fp.loadAsync();
|
|
6
|
+
var sets = {}; fp.findAllWithCriteria({ types: ['COMPONENT_SET'] }).forEach(function (s) { sets[s.name] = s; });
|
|
7
|
+
function inst(set, props) { var c = sets[set].children.find(function (k) { return Object.keys(props).every(function (p) { return k.variantProperties[p] === props[p]; }); }) || sets[set].defaultVariant; return c.createInstance(); }
|
|
8
|
+
var p = await page('Themes');
|
|
9
|
+
var doc = await docFrame('Themes', 'FOUNDATIONS', 'Themes', 'The same controls in all eight Theme modes. Nothing here is redrawn: each panel is one frame with a Theme mode set, and the components inside read from it. Vision themes are light only and keep each color distinct for that kind of color vision.');
|
|
10
|
+
doc.x = 0; doc.y = 400;
|
|
11
|
+
var THEMES = [['Light', 'The default.'], ['Dark', 'Surfaces lift by getting lighter.'], ['Light high contrast', 'For prefers-contrast: more.'], ['Dark high contrast', 'Dark with stronger text and borders.'], ['Deuteranopia', 'Red-green, green weak.'], ['Protanopia', 'Red-green, red weak.'], ['Tritanopia', 'Blue-yellow.'], ['Achromatopsia', 'No color: contrast carries everything.']];
|
|
12
|
+
var sec = await section(doc, 'All eight themes', 'Set Theme on any frame to see a design this way. In code: data-theme on the root or any element.');
|
|
13
|
+
var grid = AL('HORIZONTAL', { name: 'Grid', itemSpacing: 16, counterAxisSpacing: 16, layoutWrap: 'WRAP' });
|
|
14
|
+
add(sec, [grid]); grid.layoutSizingHorizontal = 'FILL';
|
|
15
|
+
for (var i = 0; i < THEMES.length; i++) {
|
|
16
|
+
var f = AL('VERTICAL', { name: THEMES[i][0], itemSpacing: 16, paddingTop: 20, paddingBottom: 24, paddingLeft: 20, paddingRight: 20, cornerRadius: 12 });
|
|
17
|
+
fill(f, 'Theme:surface/canvas'); stroke(f, 'Theme:border/default', 1);
|
|
18
|
+
mode(f, 'Theme', THEMES[i][0]);
|
|
19
|
+
add(grid, [f]); f.resize(300, f.height); f.counterAxisSizingMode = 'FIXED';
|
|
20
|
+
var hd = AL('VERTICAL', { name: 'Header', itemSpacing: 2 });
|
|
21
|
+
add(hd, [await T(THEMES[i][0], 'subtitle-base'), await T(THEMES[i][1], 'body-condense', 'Theme:text/secondary')]);
|
|
22
|
+
add(f, [hd]);
|
|
23
|
+
var kids = [inst('Field', { State: 'Default' }), inst('Field', { State: 'Error' }), inst('Select', { Size: 'md', State: 'Default', Content: 'Value' }), inst('Checkbox', { Size: 'md', Value: 'On', State: 'Default' }), inst('Switch', { Size: 'md', Value: 'On', State: 'Default' })];
|
|
24
|
+
for (var k = 0; k < kids.length; k++) { add(f, [kids[k]]); try { kids[k].layoutSizingHorizontal = k < 3 ? 'FILL' : 'HUG'; } catch (e) {} }
|
|
25
|
+
var btns = AL('HORIZONTAL', { name: 'Buttons', itemSpacing: 8 });
|
|
26
|
+
var EM = [['Bold', 'Save'], ['Standard', 'Cancel'], ['Quiet', 'Skip']];
|
|
27
|
+
for (var b = 0; b < EM.length; b++) {
|
|
28
|
+
var c = AL('HORIZONTAL', { name: EM[b][0], primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER', paddingTop: 8, paddingBottom: 8, paddingLeft: 16, paddingRight: 16 });
|
|
29
|
+
fill(c, 'Emphasis:emphasis/bg'); stroke(c, 'Emphasis:emphasis/border', 'Size:stroke/default'); radius(c, 'Size:button/radius');
|
|
30
|
+
mode(c, 'Emphasis', EM[b][0]);
|
|
31
|
+
add(c, [await T(EM[b][1], 'trigger-base', 'Emphasis:emphasis/fg')]);
|
|
32
|
+
add(btns, [c]);
|
|
33
|
+
}
|
|
34
|
+
add(f, [btns]);
|
|
35
|
+
}
|
|
36
|
+
return { doc: doc.id, h: Math.round(doc.height), w: Math.round(grid.width) };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Fields: Input, Select and Textarea.
|
|
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 ES = {}; (await figma.getLocalEffectStylesAsync()).forEach(function (s) { ES[s.name] = s; });
|
|
7
|
+
await page('Fields');
|
|
8
|
+
var SIZES = { xs: ['body-minimize', 'xs'], sm: ['body-condense', 'sm'], md: ['body-base', ''], lg: ['body-magnify', 'lg'] };
|
|
9
|
+
var CHEV = 'M 4 6 L 8 10 L 12 6';
|
|
10
|
+
async function control(kind, size, state, content) {
|
|
11
|
+
var name = 'Size=' + size + ', State=' + state + (content ? ', Content=' + content : '');
|
|
12
|
+
if (kind === 'Textarea') name = 'State=' + state;
|
|
13
|
+
var c = comp(name, kind === 'Textarea' ? 'VERTICAL' : 'HORIZONTAL');
|
|
14
|
+
c.counterAxisAlignItems = kind === 'Textarea' ? 'MIN' : 'CENTER';
|
|
15
|
+
var sfx = SIZES[size][1] ? '-' + SIZES[size][1] : '';
|
|
16
|
+
fill(c, state === 'Disabled' ? 'Theme:surface/overlay' : 'Theme:surface/canvas');
|
|
17
|
+
stroke(c, state === 'Error' ? 'Theme:status/error' : state === 'Focus' ? 'Theme:focus/ring' : state === 'Hover' ? 'Theme:border/control-hover' : 'Theme:border/control', 'Size:field/border-width');
|
|
18
|
+
radius(c, 'Size:field/radius');
|
|
19
|
+
pad(c, 'Size:control/padding-block' + sfx, 'Size:control/padding-inline' + sfx);
|
|
20
|
+
if (state === 'Focus') await c.setEffectStyleIdAsync(ES['focus/field'].id);
|
|
21
|
+
if (kind === 'Select') { c.itemSpacing = 8; bind(c, 'itemSpacing', 'Size:gap/control'); }
|
|
22
|
+
var placeholder = content === 'Placeholder';
|
|
23
|
+
var txt = await T(kind === 'Select' ? (placeholder ? 'Choose a region' : 'Mojave Desert') : kind === 'Textarea' ? 'Tell us about the trip' : (placeholder ? 'name@example.com' : 'sarah@scatteredlight.com'), SIZES[size][0], state === 'Disabled' ? 'Theme:text/disabled' : placeholder ? 'Theme:text/tertiary' : 'Theme:text/primary');
|
|
24
|
+
txt.name = content || 'Text'; add(c, [txt]); txt.layoutSizingHorizontal = 'FILL';
|
|
25
|
+
if (kind === 'Select') { var ic = icon('Chevron', CHEV, 16, state === 'Disabled' ? 'Theme:text/disabled' : 'Theme:text/tertiary'); add(c, [ic]); bind(ic, ['width', 'height'], 'Size:icon-size/base'); }
|
|
26
|
+
c.primaryAxisSizingMode = 'FIXED'; c.counterAxisSizingMode = 'FIXED';
|
|
27
|
+
if (kind === 'Textarea') { c.resize(320, 96); c.layoutMode = 'VERTICAL'; c.primaryAxisSizingMode = 'FIXED'; }
|
|
28
|
+
else { c.resize(280, 44); bind(c, 'height', 'Size:control/height-' + size); }
|
|
29
|
+
return c;
|
|
30
|
+
}
|
|
31
|
+
var out = {};
|
|
32
|
+
var specs = [
|
|
33
|
+
['Input', ['xs', 'sm', 'md', 'lg'], ['Default', 'Hover', 'Focus', 'Error', 'Disabled'], ['Placeholder', 'Value'], 1266],
|
|
34
|
+
['Select', ['sm', 'md', 'lg'], ['Default', 'Hover', 'Focus', 'Disabled'], ['Placeholder', 'Value'], 962],
|
|
35
|
+
['Textarea', ['md'], ['Default', 'Focus', 'Error', 'Disabled'], [null], 1440]
|
|
36
|
+
];
|
|
37
|
+
for (var s = 0; s < specs.length; s++) {
|
|
38
|
+
var sp = specs[s]; removeOld(sp[0]);
|
|
39
|
+
var comps = [];
|
|
40
|
+
// Rows are state then content; sizes run across.
|
|
41
|
+
for (var a = 0; a < sp[2].length; a++) for (var c2 = 0; c2 < sp[3].length; c2++) for (var b = 0; b < sp[1].length; b++) comps.push(await control(sp[0], sp[1][b], sp[2][a], sp[3][c2]));
|
|
42
|
+
var set = figma.combineAsVariants(comps, figma.currentPage);
|
|
43
|
+
set.name = sp[0];
|
|
44
|
+
arrange(set, sp[4]); set.clipsContent = false;
|
|
45
|
+
if (sp[0] === 'Textarea') textProp(set, 'Text', 'Tell us about the trip', 'Text');
|
|
46
|
+
else { textProp(set, 'Placeholder', sp[0] === 'Select' ? 'Choose a region' : 'name@example.com', 'Placeholder'); textProp(set, 'Value', sp[0] === 'Select' ? 'Mojave Desert' : 'sarah@scatteredlight.com', 'Value'); }
|
|
47
|
+
set.description = {
|
|
48
|
+
Input: 'A single line of text: names, email, search. Pair it with a Field for a label and help text. Size follows the row it sits in (xs with minimize text, sm condense, md base, lg magnify). Content shows the placeholder or a typed value. Focus is a 3px tint halo with the ring color on the border; Error swaps the border to status/error.',
|
|
49
|
+
Select: 'Pick one option from a list the person can see. The trigger is the same field as Input, so a form of inputs and selects lines up. Use a radio group instead when there are five options or fewer and space allows.',
|
|
50
|
+
Textarea: 'Several lines of text. Starts at 80px tall and grows with the content; the person can drag it taller. Same field look as Input.'
|
|
51
|
+
}[sp[0]];
|
|
52
|
+
place(set);
|
|
53
|
+
out[sp[0]] = { id: set.id, variants: comps.length };
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Fields: Checkbox, Radio and Switch.
|
|
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 ES = {}; (await figma.getLocalEffectStylesAsync()).forEach(function (s) { ES[s.name] = s; });
|
|
7
|
+
await page('Fields');
|
|
8
|
+
var CHECK = 'M 3.5 8.5 L 6.5 11.5 L 12.5 4.5', DASH = 'M 4 8 L 12 8';
|
|
9
|
+
// Per size: box icon size, label style, box px, mark px.
|
|
10
|
+
var BOX = { sm: ['base', 'body-condense', 16, 12], md: ['magnify', 'body-base', 20, 14], lg: ['maximize', 'body-magnify', 24, 16] };
|
|
11
|
+
var SW = { sm: ['-sm', 'eyebrow-minimize', 'body-minimize', 44, 20], md: ['', 'eyebrow-minimize', 'label-base', 48, 24], lg: ['-lg', 'eyebrow-condense', 'body-base', 68, 32] };
|
|
12
|
+
var STATES = ['Default', 'Hover', 'Focus', 'Disabled'];
|
|
13
|
+
function row(name) { var r = comp(name); r.counterAxisAlignItems = 'CENTER'; r.itemSpacing = 8; bind(r, 'itemSpacing', 'Size:gap/control'); return r; }
|
|
14
|
+
async function label(r, str, style, state) { var t = await T(str, style, state === 'Disabled' ? 'Theme:text/disabled' : 'Theme:text/primary'); t.name = 'Label'; add(r, [t]); }
|
|
15
|
+
async function check(kind, size, value, state) {
|
|
16
|
+
var b = BOX[size], r = row('Size=' + size + ', Value=' + value + ', State=' + state);
|
|
17
|
+
var on = value !== 'Off';
|
|
18
|
+
var box = figma.createFrame(); box.name = kind === 'Radio' ? 'Circle' : 'Box';
|
|
19
|
+
box.layoutMode = 'HORIZONTAL'; box.primaryAxisAlignItems = 'CENTER'; box.counterAxisAlignItems = 'CENTER';
|
|
20
|
+
box.primaryAxisSizingMode = 'FIXED'; box.counterAxisSizingMode = 'FIXED'; box.resize(b[2], b[2]); bind(box, ['width', 'height'], 'Size:icon-size/' + b[0]);
|
|
21
|
+
fill(box, on ? 'Theme:interactive/default' : 'Theme:surface/canvas');
|
|
22
|
+
stroke(box, on ? 'Theme:border/accent' : state === 'Hover' ? 'Theme:border/control-hover' : 'Theme:border/control', 'Size:check/border-width', 'INSIDE');
|
|
23
|
+
if (kind === 'Radio') box.cornerRadius = 999; else radius(box, 'Size:check/radius');
|
|
24
|
+
// Only the focus state clips, so its spread rings show.
|
|
25
|
+
box.clipsContent = state === 'Focus';
|
|
26
|
+
if (state === 'Focus') box.effects = focusRing().reverse();
|
|
27
|
+
if (state === 'Disabled') bind(box, 'opacity', 'Size:check/opacity-disabled');
|
|
28
|
+
if (on && kind === 'Checkbox') { var m = icon(value === 'On' ? 'Check' : 'Dash', value === 'On' ? CHECK : DASH, b[3], 'Theme:text/on-primary', 2); add(box, [m]); }
|
|
29
|
+
if (on && kind === 'Radio') { var d = figma.createEllipse(); d.name = 'Dot'; var s = Math.round(b[2] * 0.4); d.resize(s, s); fill(d, 'Theme:text/on-primary'); add(box, [d]); }
|
|
30
|
+
add(r, [box]);
|
|
31
|
+
await label(r, kind === 'Radio' ? 'Joshua Tree' : 'Email me trip updates', b[1], state);
|
|
32
|
+
return r;
|
|
33
|
+
}
|
|
34
|
+
async function sw(size, value, state) {
|
|
35
|
+
var s = SW[size], on = value === 'On', r = row('Size=' + size + ', Value=' + value + ', State=' + state);
|
|
36
|
+
var tr = figma.createFrame(); tr.name = 'Track'; tr.layoutMode = 'HORIZONTAL'; tr.counterAxisAlignItems = 'CENTER';
|
|
37
|
+
tr.primaryAxisSizingMode = 'FIXED'; tr.counterAxisSizingMode = 'FIXED'; tr.resize(s[3], s[4]);
|
|
38
|
+
bind(tr, 'width', 'Size:switch/track-width' + s[0]); bind(tr, 'height', 'Size:switch/track-height' + s[0]);
|
|
39
|
+
bind(tr, ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], 'Size:switch/track-padding');
|
|
40
|
+
tr.cornerRadius = 999;
|
|
41
|
+
fill(tr, on ? (state === 'Hover' ? 'Theme:interactive/hover' : 'Theme:interactive/default') : (state === 'Hover' ? 'Theme:fill/strong' : 'Theme:fill/default'));
|
|
42
|
+
stroke(tr, on ? 'Theme:border/accent' : 'Theme:border/control', 'Size:check/border-width', 'INSIDE');
|
|
43
|
+
tr.clipsContent = state === 'Focus';
|
|
44
|
+
if (state === 'Focus') tr.effects = focusRing().reverse();
|
|
45
|
+
if (state === 'Disabled') bind(tr, 'opacity', 'Size:check/opacity-disabled');
|
|
46
|
+
var th = figma.createEllipse(); th.name = 'Thumb'; var d = s[4] - 6; th.resize(d, d); fill(th, 'Theme:surface/canvas');
|
|
47
|
+
th.effects = [shadow(null, 0, { y: 1, blur: 2, rgba: { r: 0, g: 0, b: 0, a: 0.12 } })];
|
|
48
|
+
var t = await T(on ? 'on' : 'off', s[1], on ? 'Theme:text/on-primary' : 'Theme:text/secondary'); t.name = 'Track text'; t.textAlignHorizontal = 'CENTER';
|
|
49
|
+
add(tr, on ? [t, th] : [th, t]); t.layoutSizingHorizontal = 'FILL';
|
|
50
|
+
add(r, [tr]);
|
|
51
|
+
await label(r, 'Offline maps', s[2], state);
|
|
52
|
+
return r;
|
|
53
|
+
}
|
|
54
|
+
var out = {};
|
|
55
|
+
// Rows are states; columns are sizes, grouped by value. Each column is as wide as its widest
|
|
56
|
+
// variant, 40 apart within a value and 64 between values; rows are as tall as the tallest, 28 apart.
|
|
57
|
+
function grid(set, sizes, values) {
|
|
58
|
+
set.layoutMode = 'NONE';
|
|
59
|
+
var at = function (st, val, sz) { return set.children.filter(function (c) { var p = c.variantProperties; return p.State === st && p.Value === val && p.Size === sz; })[0]; };
|
|
60
|
+
var x = 32, colX = {};
|
|
61
|
+
values.forEach(function (val, vi) { if (vi) x += 24; sizes.forEach(function (sz) { colX[val + sz] = x; x += Math.max.apply(null, STATES.map(function (st) { return at(st, val, sz).width; })) + 40; }); });
|
|
62
|
+
var maxH = Math.max.apply(null, set.children.map(function (c) { return c.height; }));
|
|
63
|
+
STATES.forEach(function (st, r) { values.forEach(function (val) { sizes.forEach(function (sz) { var c = at(st, val, sz); c.x = colX[val + sz]; c.y = 32 + r * (maxH + 28) + (maxH - c.height) / 2; }); }); });
|
|
64
|
+
set.resize(x - 40 + 32, 64 + STATES.length * (maxH + 28) - 28);
|
|
65
|
+
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;
|
|
66
|
+
set.clipsContent = false;
|
|
67
|
+
}
|
|
68
|
+
async function build(name, make, sizes, values, width, labelText, desc) {
|
|
69
|
+
removeOld(name); var comps = [];
|
|
70
|
+
for (var a = 0; a < STATES.length; a++) for (var b = 0; b < values.length; b++) for (var c = 0; c < sizes.length; c++) comps.push(await make(sizes[c], values[b], STATES[a]));
|
|
71
|
+
var set = figma.combineAsVariants(comps, figma.currentPage); set.name = name;
|
|
72
|
+
grid(set, sizes, values);
|
|
73
|
+
textProp(set, 'Label', labelText, 'Label'); set.description = desc; place(set);
|
|
74
|
+
out[name] = { id: set.id, variants: comps.length };
|
|
75
|
+
}
|
|
76
|
+
await build('Checkbox', function (s, v, st) { return check('Checkbox', s, v, st); }, ['sm', 'md', 'lg'], ['Off', 'On', 'Indeterminate'], 1300, 'Email me trip updates',
|
|
77
|
+
'A yes or no the person sets on its own, or one of several picks in a group. The box is one icon size above its label and the mark one below, so the three sizes keep the same proportions. Indeterminate is the parent of a group that is partly ticked. Focus is the shared ring with a soft halo; Disabled fades the box to half.');
|
|
78
|
+
await build('Radio', function (s, v, st) { return check('Radio', s, v, st); }, ['sm', 'md', 'lg'], ['Off', 'On'], 1000, 'Joshua Tree',
|
|
79
|
+
'One choice from a short list the person can see all at once. Same box as Checkbox, always round; the dot is 40% of the circle. Use a Select when the list is longer than about five.');
|
|
80
|
+
await build('Switch', sw, ['sm', 'md', 'lg'], ['Off', 'On'], 1000, 'Offline maps',
|
|
81
|
+
'Turns a setting on or off right away, with no Save button. The track says off or on so the state never depends on color alone. Its label runs one step smaller than a checkbox label. Use a Checkbox when the choice only applies after the form is sent.');
|
|
82
|
+
return out;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Fields: Field (label, control, message). Run after 01-input.js.
|
|
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 ES = {}; (await figma.getLocalEffectStylesAsync()).forEach(function (s) { ES[s.name] = s; });
|
|
7
|
+
await page('Fields');
|
|
8
|
+
removeOld('Field');
|
|
9
|
+
var inputSet = figma.currentPage.findOne(function (n) { return n.type === 'COMPONENT_SET' && n.name === 'Input'; });
|
|
10
|
+
function variant(set, name) { return set.children.filter(function (c) { return c.name === name; })[0]; }
|
|
11
|
+
var comps = [];
|
|
12
|
+
var cfg = [['Default', 'State=Default, Content=Placeholder'], ['Error', 'State=Error, Content=Value'], ['Disabled', 'State=Disabled, Content=Placeholder']];
|
|
13
|
+
for (var i = 0; i < cfg.length; i++) {
|
|
14
|
+
var st = cfg[i][0], c = comp('State=' + st, 'VERTICAL'); c.itemSpacing = 6; bind(c, 'itemSpacing', 'Size:field/gap');
|
|
15
|
+
c.primaryAxisSizingMode = 'AUTO'; c.counterAxisSizingMode = 'FIXED'; c.resize(320, 100);
|
|
16
|
+
var lw = AL('VERTICAL', { name: 'Label wrap' }); bind(lw, 'paddingBottom', 'Size:field/label-spacing');
|
|
17
|
+
var lab = await T('Email', 'label-base', 'Theme:text/primary'); lab.name = 'Label'; add(lw, [lab]);
|
|
18
|
+
var inst = variant(inputSet, 'Size=md, ' + cfg[i][1]).createInstance(); inst.name = 'Control';
|
|
19
|
+
var mw = AL('VERTICAL', { name: 'Message' }); bind(mw, 'paddingTop', 'Size:field/message-spacing');
|
|
20
|
+
var msg = st === 'Error' ? await T('Enter an email with an @, like name@example.com', 'label-base', 'Theme:status/error') : await T('We only use it to send your trip plan.', 'body-condense', 'Theme:text/secondary');
|
|
21
|
+
msg.name = st === 'Error' ? 'Error' : 'Description'; add(mw, [msg]);
|
|
22
|
+
add(c, [lw, inst, mw]); [lw, inst, mw].forEach(function (n) { n.layoutSizingHorizontal = 'FILL'; }); msg.layoutSizingHorizontal = 'FILL';
|
|
23
|
+
if (st === 'Disabled') bind(c, 'opacity', 'Size:field/opacity-disabled');
|
|
24
|
+
comps.push(c);
|
|
25
|
+
}
|
|
26
|
+
var set = figma.combineAsVariants(comps, figma.currentPage); set.name = 'Field'; arrange(set, 1100); set.clipsContent = false;
|
|
27
|
+
textProp(set, 'Label', 'Email', 'Label'); textProp(set, 'Description', 'We only use it to send your trip plan.', 'Description'); textProp(set, 'Error', 'Enter an email with an @, like name@example.com', 'Error');
|
|
28
|
+
boolProp(set, 'Message', true, 'Message');
|
|
29
|
+
set.description = 'Label, control and one line of help or error, spaced by the field concept so every form in the product has the same rhythm. Swap the Control instance for a Select or Textarea. Error replaces the help text instead of adding to it, so the field never grows two lines of small print. Disabled fades the whole field.';
|
|
30
|
+
place(set);
|
|
31
|
+
var col = set.parent; col.insertChild(0, set);
|
|
32
|
+
return { id: set.id };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Fields: Combobox field, Autocomplete, Number field, OTP field, Checkbox group, Fieldset, Form.
|
|
2
|
+
// Run after 09-fields/03-field.js and 10-buttons/01-buttons.js (Form uses Button).
|
|
3
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
4
|
+
var C = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'compHelpers'))(figma);
|
|
5
|
+
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;
|
|
6
|
+
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;
|
|
7
|
+
var ES = {}; (await figma.getLocalEffectStylesAsync()).forEach(function (s) { ES[s.name] = s; });
|
|
8
|
+
await page('Fields');
|
|
9
|
+
var here = function (n) { return figma.currentPage.findOne(function (x) { return x.type === 'COMPONENT_SET' && x.name === n; }); };
|
|
10
|
+
var INP = here('Input'), FLD = here('Field'), CHK = here('Checkbox'), BTN = await C.setOn('Buttons', 'Button');
|
|
11
|
+
['Combobox field', 'Autocomplete', 'Number field', 'OTP field', 'Checkbox group', 'Fieldset', 'Form'].forEach(function (n) { figma.currentPage.findAll(function (x) { return (x.type === 'COMPONENT_SET' || (x.type === 'FRAME' && x.getSharedPluginData('loomiere', 'wrap') === '1')) && x.name === n; }).forEach(function (x) { x.remove(); }); });
|
|
12
|
+
var DOWN = 'M 4 6 L 8 10 L 12 6', UP = 'M 4 10 L 8 6 L 12 10', XP = 'M 4 4 L 12 12 M 12 4 L 4 12';
|
|
13
|
+
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; }
|
|
14
|
+
function grid(set, cols, colW, rowH) {
|
|
15
|
+
set.layoutMode = 'NONE';
|
|
16
|
+
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); });
|
|
17
|
+
set.resize(64 + cols * colW - 24, 64 + Math.ceil(set.children.length / cols) * rowH); frameSet(set);
|
|
18
|
+
}
|
|
19
|
+
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; }
|
|
20
|
+
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(); }
|
|
21
|
+
function input(size, st, content, text) { var i = pick(INP, 'Size=' + size + ', State=' + st + ', Content=' + content); if (text !== undefined) setP(i, content === 'Value' ? { Value: text } : { Placeholder: text }); return i; }
|
|
22
|
+
function fillW(n) { n.layoutSizingHorizontal = 'FILL'; return n; }
|
|
23
|
+
function iconBtn(path, name) {
|
|
24
|
+
var b = AL('HORIZONTAL', { name: name, primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER' });
|
|
25
|
+
b.resize(24, 24); b.primaryAxisSizingMode = 'FIXED'; b.counterAxisSizingMode = 'FIXED'; bind(b, ['width', 'height'], 'Size:icon-button/size'); radius(b, 'Size:icon-button/radius');
|
|
26
|
+
add(b, [icon('Icon', path, 16, 'Theme:text/tertiary')]); return b;
|
|
27
|
+
}
|
|
28
|
+
function pinRight(parent, node, inset) { parent.appendChild(node); node.layoutPositioning = 'ABSOLUTE'; node.x = parent.width - node.width - inset; node.y = Math.round((parent.height - node.height) / 2); node.constraints = { horizontal: 'MAX', vertical: 'CENTER' }; return node; }
|
|
29
|
+
function shell(name, inst) { var c = comp(name); c.clipsContent = false; add(c, [inst]); return c; }
|
|
30
|
+
|
|
31
|
+
// Combobox field
|
|
32
|
+
var cb = [];
|
|
33
|
+
for (var content of ['Placeholder', 'Value', 'Chips']) for (var st of ['Default', 'Focus']) {
|
|
34
|
+
var inp = content === 'Value' ? input('md', st, 'Value', 'Mojave Desert') : input('md', st, 'Placeholder', content === 'Chips' ? ' ' : 'Pick a region');
|
|
35
|
+
var c = shell('Content=' + content + ', State=' + st, inp);
|
|
36
|
+
var acts = AL('HORIZONTAL', { name: 'Actions', counterAxisAlignItems: 'CENTER' }); bind(acts, 'itemSpacing', 'Scale:space/1');
|
|
37
|
+
if (content !== 'Placeholder') add(acts, [iconBtn(XP, 'Clear')]);
|
|
38
|
+
add(acts, [iconBtn(DOWN, 'Open')]); pinRight(c, acts, 12);
|
|
39
|
+
if (content === 'Chips') {
|
|
40
|
+
var chips = AL('HORIZONTAL', { name: 'Chips', counterAxisAlignItems: 'CENTER' }); bind(chips, 'itemSpacing', 'Scale:space/0-5');
|
|
41
|
+
for (var label of ['Mojave', 'Sonoran']) {
|
|
42
|
+
var ch = AL('HORIZONTAL', { name: 'Chip', counterAxisAlignItems: 'CENTER' }); bind(ch, ['paddingTop', 'paddingBottom'], 'Scale:space/0-5'); bind(ch, 'paddingLeft', 'Scale:space/1-5'); bind(ch, 'paddingRight', 'Scale:space/0-5'); bind(ch, 'itemSpacing', 'Scale:space/1');
|
|
43
|
+
radius(ch, 'Size:radius/item'); fill(ch, 'Theme:fill/subtle');
|
|
44
|
+
var rm = AL('HORIZONTAL', { name: 'Remove' }); bind(rm, ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], 'Scale:space/1'); radius(rm, 'Size:radius/item'); add(rm, [icon('X', XP, 12, 'Theme:text/primary')]);
|
|
45
|
+
add(ch, [await T(label, 'body-condense', 'Theme:text/primary'), rm]); add(chips, [ch]);
|
|
46
|
+
}
|
|
47
|
+
add(chips, [await T('Add', 'body-base', 'Theme:text/tertiary')]);
|
|
48
|
+
c.appendChild(chips); chips.layoutPositioning = 'ABSOLUTE'; chips.x = 6; chips.y = Math.round((c.height - chips.height) / 2);
|
|
49
|
+
}
|
|
50
|
+
cb.push(c);
|
|
51
|
+
}
|
|
52
|
+
var cbSet = figma.combineAsVariants(cb, figma.currentPage); cbSet.name = 'Combobox field'; grid(cbSet, 2, 320, 80);
|
|
53
|
+
cbSet.description = 'The Combobox input: type to filter, with a clear X once there is a value and a chevron to open the list (List popup on the Popups page). Chips is multiple selection: each pick becomes a removable chip inside the field.';
|
|
54
|
+
// Autocomplete
|
|
55
|
+
var ac = [];
|
|
56
|
+
for (var content of ['Placeholder', 'Value']) for (var st of ['Default', 'Focus']) {
|
|
57
|
+
var inp = content === 'Value' ? input('md', st, 'Value', 'Joshua Tree') : input('md', st, 'Placeholder', 'Search parks');
|
|
58
|
+
var c = shell('Content=' + content + ', State=' + st, inp);
|
|
59
|
+
if (content === 'Value') pinRight(c, iconBtn(XP, 'Clear'), 12);
|
|
60
|
+
ac.push(c);
|
|
61
|
+
}
|
|
62
|
+
var acSet = figma.combineAsVariants(ac, figma.currentPage); acSet.name = 'Autocomplete'; grid(acSet, 2, 320, 80);
|
|
63
|
+
acSet.description = 'A text input that suggests as you type, but keeps whatever you typed: no chevron, and the value need not be in the list. A clear X appears once there is text. Its suggestions use the List popup.';
|
|
64
|
+
// Number field
|
|
65
|
+
function stepper(hover) {
|
|
66
|
+
var s = AL('VERTICAL', { name: 'Steppers' }); bind(s, 'itemSpacing', 'Scale:space/0-5');
|
|
67
|
+
for (var dir of ['Increment', 'Decrement']) {
|
|
68
|
+
var b = AL('HORIZONTAL', { name: dir, primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER' });
|
|
69
|
+
b.resize(28, 18); b.primaryAxisSizingMode = 'FIXED'; b.counterAxisSizingMode = 'FIXED'; bind(b, 'width', 'Size:control/height-xs'); radius(b, 'Size:radius/item');
|
|
70
|
+
var on = hover && dir === 'Increment'; if (on) fill(b, 'Theme:fill/subtle');
|
|
71
|
+
add(b, [icon('Chevron', dir === 'Increment' ? UP : DOWN, 12, on ? 'Theme:text/primary' : 'Theme:text/secondary')]); add(s, [b]);
|
|
72
|
+
}
|
|
73
|
+
return s;
|
|
74
|
+
}
|
|
75
|
+
var nf = [];
|
|
76
|
+
for (var st of ['Default', 'Hover', 'Focus', 'Disabled']) {
|
|
77
|
+
var c = shell('State=' + st, input('md', st === 'Hover' ? 'Default' : st, 'Value', '2'));
|
|
78
|
+
var s = stepper(st === 'Hover'); pinRight(c, s, 3); if (st === 'Disabled') { s.opacity = 0.5; }
|
|
79
|
+
nf.push(c);
|
|
80
|
+
}
|
|
81
|
+
var nfSet = figma.combineAsVariants(nf, figma.currentPage); nfSet.name = 'Number field'; grid(nfSet, 4, 320, 80);
|
|
82
|
+
nfSet.description = 'A number input with increment and decrement buttons stacked inside its right edge, each control/height-xs wide. Hover shows the step button under the pointer; the field itself works like Input.';
|
|
83
|
+
// OTP field
|
|
84
|
+
var OSZ = { sm: ['Size:control/height-sm', 'Scale:space/1-5'], md: ['Size:control/height-md', 'Scale:space/2'], lg: ['Size:control/height-lg', 'Scale:space/3'] };
|
|
85
|
+
var otp = [];
|
|
86
|
+
for (var z of ['sm', 'md', 'lg']) for (var st of ['Empty', 'Typing', 'Complete', 'Error']) {
|
|
87
|
+
var c = comp('Size=' + z + ', State=' + st); c.counterAxisAlignItems = 'CENTER'; bind(c, 'itemSpacing', OSZ[z][1]);
|
|
88
|
+
var digits = ['4', '8', '1', '5', '9', '2'];
|
|
89
|
+
for (var k = 0; k < 6; k++) {
|
|
90
|
+
if (k === 3) { var sp = figma.createRectangle(); sp.name = 'Separator'; sp.resize(12, 2); fill(sp, 'Theme:border/control'); sp.cornerRadius = 999; bind(sp, 'width', 'Scale:space/3'); bind(sp, 'height', 'Scale:stroke/medium'); add(c, [sp]); }
|
|
91
|
+
var filled = st === 'Complete' || st === 'Error' || (st === 'Typing' && k < 3);
|
|
92
|
+
var cellState = st === 'Error' ? 'Error' : (st === 'Typing' && k === 3) ? 'Focus' : 'Default';
|
|
93
|
+
var cell = input(z, cellState, 'Value', filled ? digits[k] : ' ');
|
|
94
|
+
var h = cell.height; cell.resize(h, h); cell.paddingLeft = cell.paddingRight = 0; cell.primaryAxisAlignItems = 'CENTER';
|
|
95
|
+
var tx = cell.findOne(function (n) { return n.type === 'TEXT'; }); tx.textAlignHorizontal = 'CENTER';
|
|
96
|
+
add(c, [cell]);
|
|
97
|
+
}
|
|
98
|
+
otp.push(c);
|
|
99
|
+
}
|
|
100
|
+
var otpSet = figma.combineAsVariants(otp, figma.currentPage); otpSet.name = 'OTP field'; grid(otpSet, 4, 420, 72);
|
|
101
|
+
otpSet.description = 'One-time code: a square Input per digit, the control height of its size, with a short dash in border/control after the third. Typing shows the focused cell; Error marks every cell.';
|
|
102
|
+
// Checkbox group
|
|
103
|
+
var cg = [];
|
|
104
|
+
for (var st of ['Default', 'Disabled']) {
|
|
105
|
+
var c = comp('State=' + st, 'VERTICAL'); bind(c, 'itemSpacing', 'Scale:space/0-5');
|
|
106
|
+
var lab = await T('Amenities', 'label-base', 'Theme:text/primary'); lab.name = 'Label'; var lw = AL('VERTICAL', { name: 'Label wrap' }); bind(lw, 'paddingBottom', 'Size:field/label-spacing'); add(lw, [lab]); add(c, [lw]);
|
|
107
|
+
var items = [['Water', true], ['Fire pit', true], ['Hookups', false]];
|
|
108
|
+
items.forEach(function (x) { var i = pick(CHK, 'Size=md, Value=' + (x[1] ? 'On' : 'Off') + ', State=Default'); setP(i, { Label: x[0] }); add(c, [i]); });
|
|
109
|
+
if (st === 'Disabled') { c.opacity = 0.5; bind(c, 'opacity', 'Size:check/opacity-disabled'); }
|
|
110
|
+
cg.push(c);
|
|
111
|
+
}
|
|
112
|
+
var cgSet = figma.combineAsVariants(cg, figma.currentPage); cgSet.name = 'Checkbox group'; grid(cgSet, 2, 240, 170);
|
|
113
|
+
cgSet.description = 'Checkboxes for one question, space/0-5 apart under a label. Disabled fades the whole group.';
|
|
114
|
+
// Fieldset
|
|
115
|
+
var fs = [];
|
|
116
|
+
for (var st of ['Default', 'Disabled']) {
|
|
117
|
+
var c = comp('State=' + st, 'VERTICAL'); bind(c, 'itemSpacing', 'Size:gap/stack-lg'); c.counterAxisSizingMode = 'FIXED'; c.resize(320, 10); c.primaryAxisSizingMode = 'AUTO';
|
|
118
|
+
var hd = AL('VERTICAL', { name: 'Header' }); bind(hd, 'itemSpacing', 'Size:space/block-sm');
|
|
119
|
+
var lg = await T('Trip details', 'title-magnify', 'Theme:text/primary'); lg.name = 'Legend';
|
|
120
|
+
var ds = await T('Where you are going and where you start.', 'body-condense', 'Theme:text/secondary'); ds.name = 'Description';
|
|
121
|
+
add(hd, [lg, ds]); add(c, [hd]); fillW(hd); fillW(lg); fillW(ds); ds.textAutoResize = 'HEIGHT';
|
|
122
|
+
for (var l of ['Trip name', 'Leaving from']) { var f = pick(FLD, 'State=Default'); setP(f, { Label: l }); add(c, [f]); fillW(f); }
|
|
123
|
+
if (st === 'Disabled') { c.opacity = 0.5; bind(c, 'opacity', 'Size:field/opacity-disabled'); }
|
|
124
|
+
fs.push(c);
|
|
125
|
+
}
|
|
126
|
+
var fsSet = figma.combineAsVariants(fs, figma.currentPage); fsSet.name = 'Fieldset'; grid(fsSet, 2, 360, 340);
|
|
127
|
+
fsSet.description = 'A titled group of fields: a legend in title-magnify and a short description, then Field instances gap/stack-lg apart. Disabled fades every field in it.';
|
|
128
|
+
// Form
|
|
129
|
+
var fm = [];
|
|
130
|
+
for (var st of ['Default', 'Error']) {
|
|
131
|
+
var c = comp('State=' + st, 'VERTICAL'); bind(c, 'itemSpacing', 'Size:space/block-lg'); c.counterAxisSizingMode = 'FIXED'; c.resize(320, 10); c.primaryAxisSizingMode = 'AUTO';
|
|
132
|
+
var f1 = pick(FLD, 'State=' + (st === 'Error' ? 'Error' : 'Default')); setP(f1, { Label: 'Email' }); add(c, [f1]); fillW(f1);
|
|
133
|
+
var f2 = pick(FLD, 'State=Default'); setP(f2, { Label: 'Trip name' }); add(c, [f2]); fillW(f2);
|
|
134
|
+
var ar = AL('HORIZONTAL', { name: 'Actions', counterAxisAlignItems: 'CENTER' }); bind(ar, 'itemSpacing', 'Scale:space/3'); bind(ar, 'paddingTop', 'Size:space/block-md');
|
|
135
|
+
var b1 = pick(BTN, 'Emphasis=Bold, Size=md, State=Default, Content=Label'); setP(b1, { Label: 'Save trip' });
|
|
136
|
+
var b2 = pick(BTN, 'Emphasis=Standard, Size=md, State=Default, Content=Label'); setP(b2, { Label: 'Cancel' });
|
|
137
|
+
add(ar, [b1, b2]); add(c, [ar]);
|
|
138
|
+
fm.push(c);
|
|
139
|
+
}
|
|
140
|
+
var fmSet = figma.combineAsVariants(fm, figma.currentPage); fmSet.name = 'Form'; grid(fmSet, 2, 360, 360);
|
|
141
|
+
fmSet.description = 'Fields space/block-lg apart, then the actions: the main one first and Bold, space/3 apart, with space/block-md above. Error shows a field that failed on submit.';
|
|
142
|
+
var col = figma.currentPage.findOne(function (n) { return n.type === 'FRAME' && n.name === 'Components' && n.parent === figma.currentPage; });
|
|
143
|
+
[cbSet, acSet, nfSet, otpSet, cgSet, fsSet, fmSet].forEach(function (s) { col.appendChild(s); });
|
|
144
|
+
return { sets: [cbSet, acSet, nfSet, otpSet, cgSet, fsSet, fmSet].map(function (s) { return s.name + ' ' + s.id + ' ' + Math.round(s.width) + 'x' + Math.round(s.height); }) };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Fields: a header and row and column labels for each set. Run after 01 to 04.
|
|
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('Fields');
|
|
5
|
+
// Short header lines; sets not listed use their descriptions.
|
|
6
|
+
return await C.labelSets({
|
|
7
|
+
Field: 'Label, control and message together. Swap the control inside for Input, Select or Textarea.',
|
|
8
|
+
Input: 'Single-line text entry.',
|
|
9
|
+
Select: 'Pick one option from a list. Same sizes as Input so they line up in a form.',
|
|
10
|
+
Textarea: 'Multi-line text entry.',
|
|
11
|
+
Checkbox: 'On, off or mixed. Each column is a value and size; each row a state.',
|
|
12
|
+
Radio: 'One choice from a group.',
|
|
13
|
+
Switch: 'An instant on/off setting.'
|
|
14
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Fields doc frame. Run after every Fields set is built (01 to 05).
|
|
2
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
3
|
+
var B = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
|
|
4
|
+
await B.page('Fields');
|
|
5
|
+
var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docKit'))(figma);
|
|
6
|
+
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, 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;
|
|
8
|
+
// The first sections center their examples; the later ones top-align them.
|
|
9
|
+
function stage(parent, name) { var s = L.stage(parent, name); s.counterAxisAlignItems = 'CENTER'; return s; }
|
|
10
|
+
var doc = await docFrame('Fields', 'COMPONENTS', 'Fields', 'Everything a person types into or picks from. Every control here shares one field look and one size scale, so a form built from any mix of them lines up without nudging. Components sit to the right; this page says when to use each one and which token sets each part. To see them in every theme, open the Themes page.');
|
|
11
|
+
doc.x = 0; doc.y = 400;
|
|
12
|
+
|
|
13
|
+
var pick = await section(doc, 'Choosing a control', 'Start from what the person is answering, not from how it looks.');
|
|
14
|
+
var picks = [['A short piece of text', 'Input', 'Names, email, search, a number with its own keyboard.'], ['Several sentences', 'Textarea', 'Notes and messages. It grows with what is typed.'], ['One of more than about five', 'Select', 'The list stays out of the way until it is needed.'], ['One of five or fewer', 'Radio group', 'Every option is visible, so the person can compare before picking.'], ['One of a long list, found by typing', 'Combobox', 'Regions, people, airports. Chips when several are allowed.'], ['Free text with suggestions', 'Autocomplete', 'Search boxes. Whatever is typed is kept.'], ['A number people nudge up or down', 'Number field', 'Guests, nights, quantity. Steps by one with buttons or arrow keys.'], ['A one-time code', 'OTP field', 'One box per digit, so a pasted code fills them all.'], ['A yes or no that applies when the form is sent', 'Checkbox', 'Also for picking several from a list.'], ['A setting that applies the moment it changes', 'Switch', 'No Save button. If there is one, use a Checkbox instead.']];
|
|
15
|
+
var pt = AL('VERTICAL', { name: 'Picker' }); add(pick, [pt]); pt.layoutSizingHorizontal = 'FILL';
|
|
16
|
+
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', 160), await T(picks[i][2], 'body-condense', 'Theme:text/secondary', 560)]); }
|
|
17
|
+
|
|
18
|
+
var fs = await section(doc, 'Field', 'The wrapper that gives any control a label and one line of help. Put every Input, Select and Textarea inside one, so the label is always tied to its control and errors always land in the same spot.');
|
|
19
|
+
var st = stage(fs);
|
|
20
|
+
for (var s of ['Default', 'Error', 'Disabled']) add(st, [await captioned(inst('Field', 'State=' + s), s)]);
|
|
21
|
+
await usage(fs, ['A label that says what to enter, in a few words.', 'Help text for a format or a reason ("We only use it to send your trip plan").', 'An error that says how to fix it, shown after the person leaves the field.'], ['Placeholder text as the only label. It disappears as soon as someone types.', 'Help and error at once. The error replaces the help.', 'Disabling a field without saying why nearby.']);
|
|
22
|
+
await notes(fs, [['Gap', 'field/gap', 'Space between label, control and message.'], ['Label', 'base-label · text/primary', 'The 12px field label, with field/label-spacing below it.'], ['Help', 'condense · text/secondary', 'One tier under the input text so it reads as secondary.'], ['Error', 'base-label · status/error', 'Same size as the label so it is noticed, with field/message-spacing above.'], ['Disabled', 'field/opacity-disabled (0.6)', 'The whole field fades as one, so label and control never disagree.']]);
|
|
23
|
+
|
|
24
|
+
var is = await section(doc, 'Input, Select and Textarea', 'One field look for all three. Size follows the row the field sits in: xs with minimize text, sm condense, md base, lg magnify. Every control in a row shares one height.');
|
|
25
|
+
var st2 = stage(is);
|
|
26
|
+
for (var s of ['Default', 'Hover', 'Focus', 'Error', 'Disabled']) add(st2, [await captioned(inst('Input', 'Size=md, State=' + s + ', Content=' + (s === 'Error' ? 'Value' : 'Placeholder')), s)]);
|
|
27
|
+
var st3 = stage(is, 'Sizes');
|
|
28
|
+
for (var z of ['xs', 'sm', 'md', 'lg']) add(st3, [await captioned(inst('Input', 'Size=' + z + ', State=Default, Content=Value'), 'Input ' + z)]);
|
|
29
|
+
for (var z of ['sm', 'md', 'lg']) add(st3, [await captioned(inst('Select', 'Size=' + z + ', State=Default, Content=Value'), 'Select ' + z)]);
|
|
30
|
+
add(st3, [await captioned(inst('Textarea', 'State=Default'), 'Textarea')]);
|
|
31
|
+
await notes(is, [['Height', 'control/height-xs … lg', 'Matches buttons of the same size, so a field and its button line up.'], ['Padding', 'control/padding-block, padding-inline', 'Per size, from the base unit.'], ['Fill', 'surface/canvas', 'Disabled uses surface/overlay so it reads as not for typing.'], ['Border', 'field/border-width · border/control', 'Passes 3:1 against the page, so the field can be found before it is touched.'], ['Hover', 'border/control-hover', 'One step darker.'], ['Focus', 'focus/ring + 3px interactive/tint-hover', 'Border takes the ring color and a soft halo sits outside it.'], ['Error', 'status/error', 'Border only; the message under the field says what to fix.'], ['Text', 'text/primary · placeholder text/tertiary', 'Placeholder is a hint, never the label.'], ['Radius', 'field/radius', 'Shared by every field and the select trigger.']]);
|
|
32
|
+
|
|
33
|
+
var cs = await section(doc, 'Checkbox and Radio', 'Same box, one square and one round. The box is one icon size above its label and the mark one below, so all three sizes keep their proportions.');
|
|
34
|
+
var st4 = stage(cs);
|
|
35
|
+
for (var cv of [['Checkbox', 'Off'], ['Checkbox', 'On'], ['Checkbox', 'Indeterminate'], ['Radio', 'Off'], ['Radio', 'On']]) add(st4, [await captioned(inst(cv[0], 'Size=md, Value=' + cv[1] + ', State=Default'), cv[0] + ' ' + cv[1].toLowerCase())]);
|
|
36
|
+
var st5 = stage(cs, 'States');
|
|
37
|
+
for (var s of ['Hover', 'Focus', 'Disabled']) { add(st5, [await captioned(inst('Checkbox', 'Size=md, Value=On, State=' + s), s)]); }
|
|
38
|
+
await usage(cs, ['Checkbox: picking any number from a list, or one yes or no.', 'Radio: exactly one from a short list, with a sensible default picked.', 'Indeterminate: a parent row when only some children are ticked.'], ['A single radio button. It cannot be unticked.', 'A checkbox for a setting that applies right away. That is a Switch.', 'Labels that run onto several lines; shorten them or add help text.']);
|
|
39
|
+
await notes(cs, [['Box size', 'icon-size/base · magnify · maximize', 'sm, md, lg. One step above the label text.'], ['Mark', 'icon-size/minimize · condense · base', 'One step below the label, in text/on-primary.'], ['Edge', 'check/border-width · border/control', 'Hover border/control-hover; ticked border/accent.'], ['Ticked fill', 'interactive/default', 'The brand\'s interactive color.'], ['Radio dot', 'check/dot-scale (0.4)', 'A share of the circle, rounded to whole pixels.'], ['Radius', 'check/radius', 'Radio is always round.'], ['Gap', 'gap/control', 'Between the box and its label.'], ['Disabled', 'check/opacity-disabled (0.5)', 'The box fades; the label turns text/disabled.']]);
|
|
40
|
+
|
|
41
|
+
var ss = await section(doc, 'Switch', 'A setting that takes effect as soon as it changes. The track says off or on, so the state never depends on color alone.');
|
|
42
|
+
var st6 = stage(ss);
|
|
43
|
+
for (var s of ['Default', 'Hover', 'Focus', 'Disabled']) for (var val of ['Off', 'On']) add(st6, [await captioned(inst('Switch', 'Size=md, Value=' + val + ', State=' + s), s + ', ' + val.toLowerCase())]);
|
|
44
|
+
await notes(ss, [['Track', 'switch/track-width · track-height', 'Per size: 44×20, 48×24, 68×32.'], ['Padding', 'switch/track-padding', 'The thumb fills the height inside it and travels width minus height.'], ['Off', 'fill/default · hover fill/strong', 'Track text text/secondary.'], ['On', 'interactive/default · hover interactive/hover', 'Track text text/on-primary.'], ['Thumb', 'surface/canvas + small shadow', 'Same fill as an empty field.'], ['Track text', 'minimize-subtitle-label · condense-subtitle-label', 'Whole text styles. Code currently sets a 9px size on sm; Figma uses the nearest style instead.'], ['Label', 'minimize · base-label · base', 'One step smaller than a checkbox label.']]);
|
|
45
|
+
|
|
46
|
+
doc.children.filter(function (n) { return ['Combobox and Autocomplete', 'Number field and OTP field', 'Checkbox group', 'Fieldset and Form'].indexOf(n.name) !== -1; }).forEach(function (n) { n.remove(); });
|
|
47
|
+
var a = await section(doc, 'Combobox and Autocomplete', 'Both are an Input with a List popup under it. A combobox picks from the list: it has a chevron to open it, and clears with an X once something is chosen. Chips is the multiple version, each pick a removable chip inside the field. An autocomplete only suggests, so it has no chevron and keeps whatever was typed.');
|
|
48
|
+
a.name = 'Combobox and Autocomplete';
|
|
49
|
+
var s1 = stage(a, 'Combobox'); s1.counterAxisAlignItems = 'MIN';
|
|
50
|
+
for (var c of ['Placeholder', 'Value', 'Chips']) add(s1, [await captioned(inst('Combobox field', 'Content=' + c + ', State=Default'), c === 'Chips' ? 'Chips (multiple)' : c)]);
|
|
51
|
+
add(s1, [await captioned(inst('Autocomplete', 'Content=Value, State=Focus'), 'Autocomplete, focused')]);
|
|
52
|
+
await usage(a, ['Combobox when the answer must be one of a long list, like a region or a person.', 'Chips when several answers are allowed.', 'Autocomplete for free text with helpful suggestions, like a search box.'], ['A combobox for a short list people can scan. Use Select or Radio.', 'Autocomplete when only listed values are valid.']);
|
|
53
|
+
await notes(a, [['Field', 'Input tokens', 'The same box, states and sizes as Input.'], ['Actions', 'icon-button/size · text/tertiary', 'Clear and open, space/1 apart, space/3 from the right edge.'], ['Chip', 'fill/subtle · radius/item · body-condense', 'Quieter than a button; the X is its own hit area.'], ['List', 'List popup (Popups page)', 'Shared with Select, so menus and lists match.']]);
|
|
54
|
+
|
|
55
|
+
var b = await section(doc, 'Number field and OTP field', 'A number field is an Input with step buttons stacked inside its right edge; arrow keys step too. An OTP field is one square Input per digit, with a short dash after the third so a six-digit code reads as two groups.');
|
|
56
|
+
b.name = 'Number field and OTP field';
|
|
57
|
+
var s2 = stage(b, 'Number field'); s2.counterAxisAlignItems = 'MIN';
|
|
58
|
+
for (var st of ['Default', 'Hover', 'Focus', 'Disabled']) add(s2, [await captioned(inst('Number field', 'State=' + st), st)]);
|
|
59
|
+
var s3 = stage(b, 'OTP field'); s3.counterAxisAlignItems = 'MIN';
|
|
60
|
+
for (var st of ['Empty', 'Typing', 'Complete', 'Error']) add(s3, [await captioned(inst('OTP field', 'Size=md, State=' + st), st)]);
|
|
61
|
+
await notes(b, [['Step buttons', 'control/height-xs wide · radius/item', 'Small enough to sit inside the field; fill/subtle on hover.'], ['Chevrons', 'text/secondary', 'text/primary under the pointer.'], ['OTP cell', 'control/height-* square', 'Each cell is an Input, so focus and error match every field.'], ['OTP gap', 'space/1-5 · space/2 · space/3', 'Grows with the size.'], ['Dash', 'border/control · space/3 × stroke/medium', 'Groups the digits without a label.']]);
|
|
62
|
+
|
|
63
|
+
var g = await section(doc, 'Checkbox group', 'Checkboxes that answer one question, under a label. Each box is still its own choice. Disabled fades the whole group.');
|
|
64
|
+
g.name = 'Checkbox group';
|
|
65
|
+
var s4 = stage(g, 'States'); s4.counterAxisAlignItems = 'MIN';
|
|
66
|
+
for (var st of ['Default', 'Disabled']) add(s4, [await captioned(inst('Checkbox group', 'State=' + st), st)]);
|
|
67
|
+
await notes(g, [['Label', 'label-base · field/label-spacing', 'The same label as a Field.'], ['Gap', 'space/0-5', 'Tight, so the boxes read as one list.'], ['Disabled', 'check/opacity-disabled', 'The same fade as a single checkbox.']]);
|
|
68
|
+
|
|
69
|
+
var f = await section(doc, 'Fieldset and Form', 'A fieldset groups related fields under a legend and a short description. A form stacks fields, then the actions: the main one first and Bold, the rest Standard. Fields in a form show their errors after submit.');
|
|
70
|
+
f.name = 'Fieldset and Form';
|
|
71
|
+
var s5 = stage(f, 'Examples'); s5.counterAxisAlignItems = 'MIN';
|
|
72
|
+
add(s5, [await captioned(inst('Fieldset', 'State=Default'), 'Fieldset'), await captioned(inst('Form', 'State=Default'), 'Form'), await captioned(inst('Form', 'State=Error'), 'Form after a failed submit')]);
|
|
73
|
+
await usage(f, ['A fieldset for a group with its own heading, like an address.', 'One Bold action per form, first in the row.'], ['Fieldsets around a single field.', 'Several Bold buttons side by side.']);
|
|
74
|
+
await notes(f, [['Legend', 'title-magnify', 'A heading for the group.'], ['Description', 'body-condense · text/secondary · space/block-sm', 'Close under the legend.'], ['Fieldset gap', 'gap/stack-lg', 'Between fields in the group.'], ['Form gap', 'space/block-lg', 'Between fields and fieldsets.'], ['Actions', 'space/3 apart · space/block-md above', 'A little extra room before the buttons.']]);
|
|
75
|
+
return { id: doc.id, h: Math.round(doc.height) };
|