@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,135 @@
|
|
|
1
|
+
// Overlays: Dialog, Drawer, Toast. Rebuilds the page's Components column. Uses Button, Field and Checkbox.
|
|
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 pp = await page('Overlays');
|
|
8
|
+
figma.currentPage.children.filter(function (n) { return n.name === 'Components'; }).forEach(function (n) { n.remove(); });
|
|
9
|
+
var ES = {}; (await figma.getLocalEffectStylesAsync()).forEach(function (s) { ES[s.name] = s; });
|
|
10
|
+
var XP = 'M 4 4 L 12 12 M 12 4 L 4 12';
|
|
11
|
+
var BTN = await C.setOn('Buttons', 'Button');
|
|
12
|
+
var FIELD = await C.setOn('Fields', 'Field'), CHECK = await C.setOn('Fields', 'Checkbox');
|
|
13
|
+
function row(set, gap) {
|
|
14
|
+
set.layoutMode = 'NONE'; var x = 32, h = 0;
|
|
15
|
+
set.children.forEach(function (k) { k.x = x; k.y = 48; x += k.width + (gap || 64); h = Math.max(h, k.height); });
|
|
16
|
+
set.resize(x - (gap || 64) + 32, h + 96);
|
|
17
|
+
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;
|
|
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 btn(em, label, status) {
|
|
21
|
+
var i = BTN.children.find(function (c) { return c.name === 'Emphasis=' + em + ', Size=md, State=Default, Content=Label'; }).createInstance();
|
|
22
|
+
setP(i, { Label: label }); if (status) mode(i, 'Status', status); return i;
|
|
23
|
+
}
|
|
24
|
+
function field(label) { var i = FIELD.children.find(function (c) { return c.name === 'State=Default'; }).createInstance(); setP(i, { Label: label }); return i; }
|
|
25
|
+
function check(label, on) { var i = CHECK.children.find(function (c) { return c.name === 'Size=md, Value=' + (on ? 'On' : 'Off') + ', State=Default'; }).createInstance(); setP(i, { Label: label }); return i; }
|
|
26
|
+
function closeX(colorKey) {
|
|
27
|
+
var b = AL('HORIZONTAL', { name: 'Close', primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER' });
|
|
28
|
+
b.resize(24, 24); b.primaryAxisSizingMode = 'FIXED'; b.counterAxisSizingMode = 'FIXED'; bind(b, ['width', 'height'], 'Size:icon-button/size'); radius(b, 'Size:icon-button/radius');
|
|
29
|
+
add(b, [icon('X', XP, 16, colorKey || 'Theme:text/tertiary')]); return b;
|
|
30
|
+
}
|
|
31
|
+
function pinClose(parent, x, colorKey) {
|
|
32
|
+
var c = closeX(colorKey); parent.appendChild(c); c.layoutPositioning = 'ABSOLUTE';
|
|
33
|
+
c.x = parent.width - c.width - 8; c.y = 8; c.constraints = { horizontal: 'MAX', vertical: 'MIN' }; return c;
|
|
34
|
+
}
|
|
35
|
+
async function heading(parent, title, desc, titleStyle, titleColor, descColor) {
|
|
36
|
+
var t = await T(title, titleStyle || 'title-amplify', titleColor || 'Theme:text/primary'); t.name = 'Title';
|
|
37
|
+
var d = await T(desc, 'body-base', descColor || 'Theme:text/secondary'); d.name = 'Description';
|
|
38
|
+
add(parent, [t, d]); [t, d].forEach(function (n) { n.layoutSizingHorizontal = 'FILL'; n.textAutoResize = 'HEIGHT'; });
|
|
39
|
+
return [t, d];
|
|
40
|
+
}
|
|
41
|
+
function actions(kids, justify) {
|
|
42
|
+
var a = AL('HORIZONTAL', { name: 'Actions', primaryAxisAlignItems: justify || 'MAX', counterAxisAlignItems: 'CENTER' });
|
|
43
|
+
bind(a, 'itemSpacing', 'Size:modal/actions-gap'); bind(a, 'paddingTop', 'Size:modal/actions-spacing');
|
|
44
|
+
add(a, kids); return a;
|
|
45
|
+
}
|
|
46
|
+
async function modalSurface(c) {
|
|
47
|
+
fill(c, 'Theme:surface/modal'); stroke(c, 'Theme:border/default', 'Size:modal/border-width');
|
|
48
|
+
bind(c, ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], 'Size:modal/padding'); bind(c, 'itemSpacing', 'Size:modal/gap');
|
|
49
|
+
await c.setEffectStyleIdAsync(ES['elevation/xl'].id);
|
|
50
|
+
}
|
|
51
|
+
function fillW(n) { n.layoutSizingHorizontal = 'FILL'; return n; }
|
|
52
|
+
function addF(p, n) { p.appendChild(n); return fillW(n); }
|
|
53
|
+
|
|
54
|
+
// Dialog
|
|
55
|
+
var dl = [];
|
|
56
|
+
var D = [
|
|
57
|
+
['Message', 'Before you go', 'Park roads close at sunset from November to March. Plan to be back at the trailhead by 4 pm.', null],
|
|
58
|
+
['Form', 'Rename trip', 'The new name shows on your itinerary and in shared links.', null],
|
|
59
|
+
['Alert', 'Delete this trip?', 'This removes the itinerary and every booking in it. You can\'t undo this.', null]
|
|
60
|
+
];
|
|
61
|
+
for (var i = 0; i < D.length; i++) {
|
|
62
|
+
var c = comp('Content=' + D[i][0], 'VERTICAL'); await modalSurface(c); radius(c, 'Size:modal/radius');
|
|
63
|
+
c.counterAxisSizingMode = 'FIXED'; c.resize(500, 100); bind(c, 'width', 'Size:modal/max-width');
|
|
64
|
+
await heading(c, D[i][1], D[i][2]);
|
|
65
|
+
if (D[i][0] === 'Form') { var body = AL('VERTICAL', { name: 'Content' }); bind(body, 'itemSpacing', 'Size:modal/gap'); add(c, [body]); fillW(body); var f1 = field('Trip name'); add(body, [f1]); fillW(f1); }
|
|
66
|
+
if (D[i][0] === 'Message') addF(c, actions([btn('Bold', 'Got it')]));
|
|
67
|
+
if (D[i][0] === 'Form') addF(c, actions([btn('Standard', 'Cancel'), btn('Bold', 'Save')]));
|
|
68
|
+
if (D[i][0] === 'Alert') addF(c, actions([btn('Standard', 'Cancel'), btn('Bold', 'Delete trip', 'Error')]));
|
|
69
|
+
if (D[i][0] !== 'Alert') pinClose(c);
|
|
70
|
+
c.clipsContent = true; dl.push(c);
|
|
71
|
+
}
|
|
72
|
+
var dset = figma.combineAsVariants(dl, figma.currentPage); dset.name = 'Dialog'; row(dset, 64);
|
|
73
|
+
// No text props: Figma shares one value across variants, and each variant has its own example text.
|
|
74
|
+
boolProp(dset, 'Close', true, 'Close');
|
|
75
|
+
dset.description = 'A centered modal. Message tells something and has one action; Form collects a little input; Alert asks to confirm something that can\'t be undone, so it has no X and its action takes the Error status. Surface: surface/modal, border/default, modal/radius, elevation/xl; padding modal/padding, gap modal/gap; title-amplify.';
|
|
76
|
+
place(dset);
|
|
77
|
+
|
|
78
|
+
// Drawer
|
|
79
|
+
var dr = [];
|
|
80
|
+
var b = comp('Side=Bottom', 'VERTICAL'); await modalSurface(b);
|
|
81
|
+
bind(b, ['topLeftRadius', 'topRightRadius'], 'Size:modal/radius'); b.strokeBottomWeight = 0;
|
|
82
|
+
b.counterAxisSizingMode = 'FIXED'; b.resize(390, 100);
|
|
83
|
+
var hw = AL('HORIZONTAL', { name: 'Handle', primaryAxisAlignItems: 'CENTER' }); var hb = figma.createFrame(); hb.name = 'Bar'; hb.resize(40, 4); fill(hb, 'Theme:border/strong'); hb.cornerRadius = 999;
|
|
84
|
+
bind(hb, 'width', 'Size:drawer/handle-width'); bind(hb, 'height', 'Size:drawer/handle-height'); add(hw, [hb]); add(b, [hw]); fillW(hw);
|
|
85
|
+
await heading(b, 'Filters', 'Show only campsites that match.');
|
|
86
|
+
var bc = AL('VERTICAL', { name: 'Content', itemSpacing: 12 }); add(b, [bc]); fillW(bc); add(bc, [check('Free cancellation', true), check('Pets allowed', false), check('Hookups', false)]);
|
|
87
|
+
addF(b, actions([btn('Standard', 'Clear'), btn('Bold', 'Show 24 sites')]));
|
|
88
|
+
b.clipsContent = true; dr.push(b);
|
|
89
|
+
var r = comp('Side=Right', 'VERTICAL'); await modalSurface(r);
|
|
90
|
+
bind(r, ['topLeftRadius', 'bottomLeftRadius'], 'Size:modal/radius'); r.strokeRightWeight = 0;
|
|
91
|
+
r.counterAxisSizingMode = 'FIXED'; r.primaryAxisSizingMode = 'FIXED'; r.resize(420, 640);
|
|
92
|
+
await heading(r, 'Trip details', 'Edit the name and notes. Changes save when you press Save.');
|
|
93
|
+
var rc = AL('VERTICAL', { name: 'Content' }); bind(rc, 'itemSpacing', 'Size:modal/gap'); add(r, [rc]); fillW(rc); rc.layoutSizingVertical = 'FILL';
|
|
94
|
+
var f2 = field('Trip name'), f3 = field('Leaving from'); add(rc, [f2, f3]); fillW(f2); fillW(f3);
|
|
95
|
+
addF(r, actions([btn('Standard', 'Cancel'), btn('Bold', 'Save')]));
|
|
96
|
+
r.clipsContent = true; dr.push(r);
|
|
97
|
+
var drset = figma.combineAsVariants(dr, figma.currentPage); drset.name = 'Drawer'; row(drset, 64);
|
|
98
|
+
drset.description = 'A panel that slides in from an edge. Bottom is the phone sheet, full width with a grab bar; Right (or Left) is a side panel 420 wide. It shares the dialog surface, padding and title, and has no border or rounding on the edge it comes from.';
|
|
99
|
+
place(drset);
|
|
100
|
+
|
|
101
|
+
// Toast
|
|
102
|
+
function glyph(f, d, colorKey) {
|
|
103
|
+
var vec = figma.createVector(); vec.vectorPaths = [{ windingRule: 'NONE', data: d }]; vec.fills = []; vec.strokes = [paint(colorKey)]; vec.strokeWeight = 1.5; vec.strokeCap = 'ROUND'; vec.strokeJoin = 'ROUND';
|
|
104
|
+
var xs = [], ys = []; d.replace(/(-?[\d.]+) (-?[\d.]+)/g, function (m, x, y) { xs.push(+x); ys.push(+y); });
|
|
105
|
+
f.appendChild(vec); vec.x = Math.min.apply(null, xs); vec.y = Math.min.apply(null, ys); return vec;
|
|
106
|
+
}
|
|
107
|
+
function statusIcon(kind, colorKey) {
|
|
108
|
+
var f = figma.createFrame(); f.name = 'Icon'; f.fills = []; f.resize(20, 20); f.clipsContent = false;
|
|
109
|
+
if (kind === 'Warning') glyph(f, 'M 10 2.5 L 18 16.5 L 2 16.5 Z', colorKey);
|
|
110
|
+
else { var e = figma.createEllipse(); e.name = 'Ring'; e.resize(16, 16); e.x = 2; e.y = 2; e.fills = []; e.strokes = [paint(colorKey)]; e.strokeWeight = 1.5; f.appendChild(e); }
|
|
111
|
+
var g = { Info: 'M 10 9 L 10 14 M 10 6.4 L 10 6.5', Success: 'M 6.5 10 L 9 12.5 L 13.5 7.5', Warning: 'M 10 7.5 L 10 11 M 10 13.5 L 10 13.6', Error: 'M 7.5 7.5 L 12.5 12.5 M 12.5 7.5 L 7.5 12.5' }[kind];
|
|
112
|
+
glyph(f, g, colorKey); return f;
|
|
113
|
+
}
|
|
114
|
+
var TT = [['Neutral', 'Trip saved', 'Find it under My trips.'], ['Info', 'Road work ahead', 'Expect delays on Route 62 this weekend.'], ['Success', 'Booking confirmed', 'We sent the details to your email.'], ['Warning', 'Only 2 sites left', 'Book soon to keep these dates.'], ['Error', 'Payment didn\'t go through', 'Check your card details and try again.']];
|
|
115
|
+
var tl = [];
|
|
116
|
+
for (var i = 0; i < TT.length; i++) {
|
|
117
|
+
var k = TT[i][0], s = k.toLowerCase();
|
|
118
|
+
var bg = k === 'Neutral' ? 'Theme:surface/canvas' : 'Theme:scope/' + s + '/soft', bd = k === 'Neutral' ? 'Theme:border/default' : 'Theme:scope/' + s + '/border';
|
|
119
|
+
var fgT = k === 'Neutral' ? 'Theme:text/primary' : 'Theme:scope/' + s + '/on-soft', fgD = k === 'Neutral' ? 'Theme:text/secondary' : fgT, fgX = k === 'Neutral' ? 'Theme:text/tertiary' : fgT;
|
|
120
|
+
var c = comp('Type=' + k, 'HORIZONTAL'); c.counterAxisAlignItems = 'MIN';
|
|
121
|
+
fill(c, bg); stroke(c, bd, 'Size:popup/border-width'); radius(c, 'Size:popup/radius'); await c.setEffectStyleIdAsync(ES['elevation/lg'].id);
|
|
122
|
+
bind(c, ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], 'Size:notice/padding'); bind(c, 'itemSpacing', 'Size:notice/gap');
|
|
123
|
+
c.primaryAxisSizingMode = 'FIXED'; c.resize(300, 80); c.counterAxisSizingMode = 'AUTO'; bind(c, 'width', 'Size:toast/viewport-width-md');
|
|
124
|
+
if (k !== 'Neutral') add(c, [statusIcon(k, fgT)]);
|
|
125
|
+
var ct = AL('VERTICAL', { name: 'Content' }); bind(ct, 'itemSpacing', 'Size:notice/text-gap'); bind(ct, 'paddingRight', 'Size:icon-button/size'); add(c, [ct]); ct.layoutSizingHorizontal = 'FILL';
|
|
126
|
+
await heading(ct, TT[i][1], TT[i][2], 'title-base', fgT, fgD);
|
|
127
|
+
pinClose(c, 0, fgX);
|
|
128
|
+
c.clipsContent = true; tl.push(c);
|
|
129
|
+
}
|
|
130
|
+
var tset = figma.combineAsVariants(tl, figma.currentPage); tset.name = 'Toast'; row(tset, 40);
|
|
131
|
+
boolProp(tset, 'Close', true, 'Close');
|
|
132
|
+
tset.description = 'A short notice that appears in a corner and goes away on its own. Neutral sits on the popup surface; Info, Success, Warning and Error take that palette\'s soft fill, border and text, plus an icon so color is not the only signal. Title in title-base, padding notice/padding.';
|
|
133
|
+
place(tset);
|
|
134
|
+
var col = figma.currentPage.children.filter(function (n) { return n.name === 'Components'; })[0];
|
|
135
|
+
return { col: col.id, sets: [dset.id, drset.id, tset.id] };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Overlays: a header and row and column labels for each set. Run after 01.
|
|
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('Overlays');
|
|
5
|
+
// Short header lines; sets not listed use their descriptions.
|
|
6
|
+
return await C.labelSets({
|
|
7
|
+
Dialog: "A centered modal. Message has one action; Form collects a little input; Alert confirms something that can't be undone, so it has no X and its main action takes the Error status.",
|
|
8
|
+
Drawer: "A panel that slides in from an edge. Bottom is the phone sheet with a grab bar; Right is a 420-wide side panel. No border or rounding on the edge it comes from.",
|
|
9
|
+
Toast: "A short notice that goes away on its own. Status types take their palette's soft fill, border and text, plus an icon."
|
|
10
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Overlays doc frame. Run after 01 and 02.
|
|
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('Overlays');
|
|
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 === 'Overlays' && n.type === 'FRAME'; }).forEach(function (n) { n.remove(); });
|
|
38
|
+
var doc = await docFrame('Overlays', 'COMPONENTS', 'Overlays', 'Things that cover the page: dialogs and drawers that need an answer before the person goes on, and toasts that report what just happened. Dialogs and drawers share the modal surface (surface/modal, modal/radius, modal/padding, the elevation/xl shadow) and a scrim behind them. Components sit to the right.');
|
|
39
|
+
doc.x = 0; doc.y = 400;
|
|
40
|
+
function scene(parent, name, w, h) {
|
|
41
|
+
var f = figma.createFrame(); f.name = name; f.resize(w, h); fill(f, 'Theme:surface/canvas'); f.strokes = [paint('Theme:border/subtle')]; f.strokeWeight = 1; f.cornerRadius = 12; f.clipsContent = true;
|
|
42
|
+
var y = 32; for (var i = 0; i < 6; i++) { var bar = figma.createRectangle(); bar.name = 'Page'; bar.resize(i % 3 === 0 ? w * 0.4 : w - 64, i % 3 === 0 ? 20 : 12); bar.x = 32; bar.y = y; bar.cornerRadius = 4; fill(bar, 'Theme:fill/subtle'); f.appendChild(bar); y += i % 3 === 0 ? 40 : 28; }
|
|
43
|
+
var sc = figma.createRectangle(); sc.name = 'Scrim'; sc.resize(w, h); fill(sc, 'Theme:modal/backdrop'); f.appendChild(sc);
|
|
44
|
+
parent.appendChild(f); return f;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
var pick = await section(doc, 'Choosing an overlay', 'Overlays interrupt. Start with the least interrupting thing that works.');
|
|
48
|
+
var picks = [['Confirm something that can\'t be undone', 'Dialog · Alert', 'Delete, discard, leave without saving. No X: the person must choose.'], ['A short task that blocks the page', 'Dialog · Form', 'Rename, invite, a few fields. Close X, Cancel and one main action.'], ['Tell something before the person goes on', 'Dialog · Message', 'Rare. One action that closes it.'], ['Longer edits or filters beside the page', 'Drawer · Right', 'The page stays in view, so people can compare.'], ['Choices on a phone', 'Drawer · Bottom', 'A sheet that slides up and can be swiped away.'], ['Report that something happened', 'Toast', 'Saved, sent, failed. Goes away on its own.'], ['Extra detail that doesn\'t block', 'Popover', 'On the Popups page.']];
|
|
49
|
+
var pt = AL('VERTICAL', { name: 'Picker' }); add(pick, [pt]); pt.layoutSizingHorizontal = 'FILL';
|
|
50
|
+
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)]); }
|
|
51
|
+
|
|
52
|
+
var ds = await section(doc, 'Dialog', 'A centered modal over a scrim. The title is title-amplify; the description is body text in text/secondary. Actions sit at the bottom right, the main one last. Alert dialogs have no X, so the only ways out are the two buttons.');
|
|
53
|
+
var s1 = stage(ds, 'On a page'); var sc1 = scene(s1, 'Dialog on a page', 1184, 520);
|
|
54
|
+
var d1 = inst('Dialog', 'Content=Form'); sc1.appendChild(d1); d1.x = Math.round((1184 - d1.width) / 2); d1.y = Math.round((520 - d1.height) / 2);
|
|
55
|
+
var s1b = stage(ds, 'Kinds'); s1b.counterAxisAlignItems = 'MIN';
|
|
56
|
+
add(s1b, [await captioned(inst('Dialog', 'Content=Message'), 'Message'), await captioned(inst('Dialog', 'Content=Alert'), 'Alert')]);
|
|
57
|
+
await usage(ds, ['A decision the person must make before going on.', 'A short form that belongs to one action, such as Rename.', 'Action labels that say what happens: Delete trip, not OK.'], ['Information people could read in the page.', 'Long forms. Use a page or a Right drawer.', 'Dialogs that open other dialogs.']);
|
|
58
|
+
await notes(ds, [['Surface', 'surface/modal · border/default', 'A step above the page so it reads as in front.'], ['Shadow', 'elevation/xl (effect style)', 'The highest on the page short of 2xl.'], ['Radius', 'modal/radius', 'Rounder than cards.'], ['Width', 'modal/max-width', '500 wide; 90vw on small screens.'], ['Padding', 'modal/padding · modal/gap', 'Between title, description and content.'], ['Actions', 'modal/actions-spacing · modal/actions-gap', 'Extra room above the buttons so they read as the end.'], ['Close', 'icon-button/size · icon-button/inset', 'The standard quiet X in the corner.'], ['Scrim', 'modal/backdrop', 'Dims the page so the dialog is the only thing to look at.']]);
|
|
59
|
+
|
|
60
|
+
var drs = await section(doc, 'Drawer', 'A panel that slides in from an edge and can be swiped away. It uses the dialog surface and heading, and drops the border and rounding on the edge it comes from. Bottom is for phones and has a grab bar; Right keeps the page in view.');
|
|
61
|
+
var s2 = stage(drs, 'On a page'); s2.counterAxisAlignItems = 'MIN';
|
|
62
|
+
var sc2 = scene(s2, 'Right drawer', 820, 640); var d2 = inst('Drawer', 'Side=Right'); sc2.appendChild(d2); d2.x = 820 - d2.width; d2.y = 0;
|
|
63
|
+
var sc3 = scene(s2, 'Bottom drawer', 300, 640); var d3 = inst('Drawer', 'Side=Bottom'); sc3.appendChild(d3); d3.resize(300, d3.height); d3.x = 0; d3.y = 640 - d3.height;
|
|
64
|
+
await usage(drs, ['Filters, settings and details that relate to the page behind.', 'Phone menus and choices, as a bottom sheet.'], ['A quick yes or no. That is a Dialog.', 'Navigation on wide screens, where a side nav fits.']);
|
|
65
|
+
await notes(drs, [['Surface', 'modal surface · elevation/xl', 'The same as a dialog.'], ['Width', 'drawer/popup-size', 'Right and left drawers: 420, or 90vw.'], ['Height', 'drawer/popup-max-size', 'Bottom drawers: up to 85vh.'], ['Grab bar', 'drawer/handle-width · drawer/handle-height · border/strong', 'Shows the sheet can be swiped down.'], ['Edge', 'no border, no radius', 'On the side it slides from, so it looks attached.']]);
|
|
66
|
+
|
|
67
|
+
var tos = await section(doc, 'Toast', 'A short notice in the corner that reports what just happened and goes away on its own. Neutral sits on the popup surface; the status types take their palette\'s soft fill, border and text, plus an icon so color is not the only signal. New toasts stack in front; the ones behind shrink and peek out above.');
|
|
68
|
+
var s3 = stage(tos, 'Types'); s3.counterAxisAlignItems = 'MIN';
|
|
69
|
+
for (var k of ['Neutral', 'Info', 'Success', 'Warning', 'Error']) add(s3, [await captioned(inst('Toast', 'Type=' + k), k)]);
|
|
70
|
+
var s4 = stage(tos, 'Stack');
|
|
71
|
+
var stack = figma.createFrame(); stack.name = 'Stack'; stack.fills = []; stack.resize(360, 200); stack.clipsContent = false; s4.appendChild(stack);
|
|
72
|
+
var front = inst('Toast', 'Type=Success'); var fh = front.height, fw = front.width, frontTop = 200 - fh;
|
|
73
|
+
for (var j = 2; j >= 1; j--) { var t = inst('Toast', 'Type=Neutral'); var sc = 1 - j * 0.1; stack.appendChild(t); t.resize(fw, fh); t.rescale(sc); t.x = Math.round((360 - fw * sc) / 2); t.y = frontTop - j * 12; }
|
|
74
|
+
stack.appendChild(front); front.x = Math.round((360 - fw) / 2); front.y = frontTop;
|
|
75
|
+
add(s4, [await T('Hover the stack to spread it out. Each toast behind is 10% smaller and peeks notice/peek above the one in front.', 'body-condense', 'Theme:text/tertiary', 360)]);
|
|
76
|
+
await usage(tos, ['Confirming something saved, sent or copied.', 'A failure the person can retry, with an action in the toast.', 'Short titles: what happened, in a few words.'], ['Anything the person must read or act on. It disappears.', 'More than one or two lines of text.', 'Errors in a form. Show them on the field.']);
|
|
77
|
+
await notes(tos, [['Surface', 'popup surface · elevation/lg', 'Neutral: surface/canvas and border/default.'], ['Status', 'scope/<status>/soft · border · on-soft', 'The palette\'s soft fill, edge and text.'], ['Width', 'toast/viewport-width-md', '300 wide; 250 on small screens.'], ['Padding', 'notice/padding · notice/gap', 'Between the icon and the text.'], ['Text', 'title-base · body-base', 'notice/text-gap between them.'], ['Place', 'toast/viewport-margin-md', 'From the bottom right corner.'], ['Stack', 'notice/peek', 'How far each toast behind shows above.']]);
|
|
78
|
+
var over = [];
|
|
79
|
+
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)); });
|
|
80
|
+
return { id: doc.id, h: Math.round(doc.height) };
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Disclosure and navigation: Tab, Tab list, Accordion item, Accordion, Nav trigger, Nav link, Navigation menu,
|
|
2
|
+
// Toolbar, Toggle group. Rebuilds the page's Components column. Uses Button and Toggle.
|
|
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 here = function (n) { return figma.currentPage.findOne(function (x) { return x.type === 'COMPONENT_SET' && x.name === n; }); };
|
|
8
|
+
var PAGE = 'Disclosure and navigation';
|
|
9
|
+
var pp = await page(PAGE);
|
|
10
|
+
figma.currentPage.children.filter(function (n) { return n.name === 'Components'; }).forEach(function (n) { n.remove(); });
|
|
11
|
+
var ES = {}; (await figma.getLocalEffectStylesAsync()).forEach(function (s) { ES[s.name] = s; });
|
|
12
|
+
var DOWN = 'M 4 6 L 8 10 L 12 6', UP = 'M 4 10 L 8 6 L 12 10';
|
|
13
|
+
var BTN = await C.setOn('Buttons', 'Button'), TOG = await C.setOn('Buttons', 'Toggle');
|
|
14
|
+
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; }
|
|
15
|
+
function grid(set, cols, colW, rowH) {
|
|
16
|
+
set.layoutMode = 'NONE';
|
|
17
|
+
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); });
|
|
18
|
+
set.resize(64 + cols * colW - 24, 64 + Math.ceil(set.children.length / cols) * rowH); frameSet(set);
|
|
19
|
+
}
|
|
20
|
+
function row(set, gap) {
|
|
21
|
+
set.layoutMode = 'NONE'; var x = 32, h = 0;
|
|
22
|
+
set.children.forEach(function (k) { k.x = x; k.y = 48; x += k.width + gap; h = Math.max(h, k.height); });
|
|
23
|
+
set.resize(x - gap + 32, h + 96); frameSet(set);
|
|
24
|
+
}
|
|
25
|
+
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; }
|
|
26
|
+
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(); }
|
|
27
|
+
function ring(n) { n.strokes = [paint('Theme:focus/ring')]; n.strokeWeight = 2; n.strokeAlign = 'INSIDE'; }
|
|
28
|
+
function edge(n, side, colorKey, wKey) {
|
|
29
|
+
n.strokes = [paint(colorKey)]; n.strokeAlign = 'INSIDE';
|
|
30
|
+
['Top', 'Right', 'Bottom', 'Left'].forEach(function (s) { n['stroke' + s + 'Weight'] = 0; });
|
|
31
|
+
n['stroke' + side + 'Weight'] = 1; bind(n, 'stroke' + side + 'Weight', wKey);
|
|
32
|
+
}
|
|
33
|
+
function fillW(n) { n.layoutSizingHorizontal = 'FILL'; return n; }
|
|
34
|
+
function bar(name) {
|
|
35
|
+
var b = AL('HORIZONTAL', { name: name || 'Bar', counterAxisAlignItems: 'CENTER' });
|
|
36
|
+
fill(b, 'Theme:surface/tray'); stroke(b, 'Theme:border/default', 'Size:bar/border-width'); radius(b, 'Size:bar/radius');
|
|
37
|
+
bind(b, ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], 'Size:bar/padding'); bind(b, 'itemSpacing', 'Size:bar/gap'); return b;
|
|
38
|
+
}
|
|
39
|
+
function sep() {
|
|
40
|
+
var w = AL('HORIZONTAL', { name: 'Separator', counterAxisAlignItems: 'CENTER' }); bind(w, ['paddingLeft', 'paddingRight'], 'Size:bar/separator-spacing');
|
|
41
|
+
var r = figma.createRectangle(); r.name = 'Line'; r.resize(1, 24); fill(r, 'Theme:border/default'); bind(r, 'width', 'Size:bar/border-width'); bind(r, 'height', 'Size:bar/separator-length'); add(w, [r]); return w;
|
|
42
|
+
}
|
|
43
|
+
function tog(label, on, size) { var i = pick(TOG, 'Pressed=' + (on ? 'On' : 'Off') + ', Size=' + (size || 'sm') + ', State=Default'); setP(i, { Label: label, Icon: false }); return i; }
|
|
44
|
+
|
|
45
|
+
// Tab
|
|
46
|
+
var PADS = { sm: ['Scale:space/1-5', 'Scale:space/3'], md: ['Scale:space/2', 'Scale:space/4'], lg: ['Scale:space/3', 'Scale:space/6'] };
|
|
47
|
+
var TST = ['Default', 'Hover', 'Selected', 'Focus', 'Disabled'], tabs = [];
|
|
48
|
+
async function tab(orient, size, st) {
|
|
49
|
+
var c = comp('Orientation=' + orient + ', Size=' + size + ', State=' + st); c.counterAxisAlignItems = 'CENTER'; c.primaryAxisAlignItems = orient === 'Vertical' ? 'MIN' : 'CENTER';
|
|
50
|
+
pad(c, PADS[size][0], PADS[size][1]);
|
|
51
|
+
var sel = st === 'Selected', style = (sel ? 'selected-' : 'body-') + (size === 'sm' ? 'minimize' : 'base');
|
|
52
|
+
var fg = { Default: 'Theme:text/secondary', Hover: 'Theme:text/primary', Selected: 'Theme:text/accent', Focus: 'Theme:text/secondary', Disabled: 'Theme:text/disabled' }[st];
|
|
53
|
+
var t = await T('Itinerary', style, fg); t.name = 'Label'; add(c, [t]);
|
|
54
|
+
if (st === 'Hover') fill(c, 'Theme:fill/subtle');
|
|
55
|
+
if (sel) edge(c, orient === 'Vertical' ? 'Right' : 'Bottom', 'Theme:border/accent', 'Size:disclosure/indicator-width');
|
|
56
|
+
if (st === 'Focus') ring(c);
|
|
57
|
+
if (st === 'Disabled') { c.opacity = 0.5; bind(c, 'opacity', 'Size:disclosure/opacity-disabled'); }
|
|
58
|
+
return c;
|
|
59
|
+
}
|
|
60
|
+
for (var z of ['sm', 'md', 'lg']) for (var s of TST) tabs.push(await tab('Horizontal', z, s));
|
|
61
|
+
for (var s of TST) tabs.push(await tab('Vertical', 'md', s));
|
|
62
|
+
var tabSet = figma.combineAsVariants(tabs, figma.currentPage); tabSet.name = 'Tab'; grid(tabSet, 5, 150, 64);
|
|
63
|
+
textProp(tabSet, 'Label', 'Itinerary', 'Label');
|
|
64
|
+
tabSet.description = 'One tab. Selected takes the accent text, the selected text style and a disclosure/indicator-width line in border/accent on the edge facing the panel. Hover is the fill/subtle wash. Sizes change the text and padding; tabs have no rounding.';
|
|
65
|
+
place(tabSet);
|
|
66
|
+
var lists = [];
|
|
67
|
+
for (var o of ['Horizontal', 'Vertical']) {
|
|
68
|
+
var c = comp('Orientation=' + o, o === 'Horizontal' ? 'HORIZONTAL' : 'VERTICAL'); c.clipsContent = false;
|
|
69
|
+
var L4 = [['Overview', 'Selected'], ['Itinerary', 'Default'], ['Bookings', 'Default'], ['Archive', 'Disabled']];
|
|
70
|
+
for (var j = 0; j < L4.length; j++) { var ti = pick(tabSet, 'Orientation=' + o + ', Size=md, State=' + L4[j][1]); setP(ti, { Label: L4[j][0] }); add(c, [ti]); if (o === 'Vertical') fillW(ti); }
|
|
71
|
+
if (o === 'Vertical') { c.counterAxisSizingMode = 'FIXED'; c.resize(180, c.height); }
|
|
72
|
+
var dv = figma.createRectangle(); dv.name = 'Divider'; fill(dv, 'Theme:border/default'); c.insertChild(0, dv); dv.layoutPositioning = 'ABSOLUTE';
|
|
73
|
+
if (o === 'Horizontal') { dv.resize(c.width, 1); bind(dv, 'height', 'Size:stroke/default'); dv.x = 0; dv.y = c.height - 1; dv.constraints = { horizontal: 'STRETCH', vertical: 'MAX' }; }
|
|
74
|
+
else { dv.resize(1, c.height); bind(dv, 'width', 'Size:stroke/default'); dv.x = c.width - 1; dv.y = 0; dv.constraints = { horizontal: 'MAX', vertical: 'STRETCH' }; }
|
|
75
|
+
lists.push(c);
|
|
76
|
+
}
|
|
77
|
+
var listSet = figma.combineAsVariants(lists, figma.currentPage); listSet.name = 'Tab list'; row(listSet, 80);
|
|
78
|
+
listSet.description = 'A row (or column) of Tab instances over a border/default divider. The selected tab\'s line sits on the divider. Swap each tab\'s State; put the panel underneath with space/4 padding.';
|
|
79
|
+
place(listSet);
|
|
80
|
+
|
|
81
|
+
// Accordion item
|
|
82
|
+
var acc = [];
|
|
83
|
+
async function accItem(open, st) {
|
|
84
|
+
var c = comp('Panel=' + (open ? 'Open' : 'Closed') + ', State=' + st, 'VERTICAL'); c.counterAxisSizingMode = 'FIXED'; c.resize(480, 10); c.primaryAxisSizingMode = 'AUTO';
|
|
85
|
+
edge(c, 'Bottom', 'Theme:border/default', 'Size:stroke/default');
|
|
86
|
+
var tr = AL('HORIZONTAL', { name: 'Trigger', counterAxisAlignItems: 'CENTER', primaryAxisAlignItems: 'SPACE_BETWEEN' });
|
|
87
|
+
pad(tr, 'Scale:space/4', 'Scale:space/6'); bind(tr, 'itemSpacing', 'Scale:space/4');
|
|
88
|
+
if (st === 'Hover') fill(tr, 'Theme:fill/subtle');
|
|
89
|
+
if (st === 'Focus') { ring(tr); radius(tr, 'Size:radius/item'); }
|
|
90
|
+
var t = await T('What should I pack for the desert?', 'title-base', 'Theme:text/primary'); t.name = 'Title';
|
|
91
|
+
add(tr, [t, icon('Chevron', open ? UP : DOWN, 20, 'Theme:text/secondary')]); add(c, [tr]); fillW(tr); t.layoutSizingHorizontal = 'FILL'; t.textAutoResize = 'HEIGHT';
|
|
92
|
+
if (open) {
|
|
93
|
+
var pn = AL('VERTICAL', { name: 'Panel' }); bind(pn, 'paddingBottom', 'Scale:space/4'); bind(pn, ['paddingLeft', 'paddingRight'], 'Scale:space/6');
|
|
94
|
+
var a = await T('Water, then more water. Layers for cold nights, a hat, and sun protection. Most parks have no shade at the trailhead.', 'body-base', 'Theme:text/secondary'); a.name = 'Answer';
|
|
95
|
+
add(pn, [a]); add(c, [pn]); fillW(pn); fillW(a); a.textAutoResize = 'HEIGHT';
|
|
96
|
+
}
|
|
97
|
+
if (st === 'Disabled') { c.opacity = 0.5; bind(c, 'opacity', 'Size:disclosure/opacity-disabled'); }
|
|
98
|
+
return c;
|
|
99
|
+
}
|
|
100
|
+
for (var op of [false, true]) for (var s of ['Default', 'Hover', 'Focus', 'Disabled']) acc.push(await accItem(op, s));
|
|
101
|
+
var accSet = figma.combineAsVariants(acc, figma.currentPage); accSet.name = 'Accordion item';
|
|
102
|
+
// States across, Closed over Open; variants top-aligned in 520 by 143 cells.
|
|
103
|
+
accSet.layoutMode = 'NONE'; accSet.children.forEach(function (k, i) { k.x = 32 + (i % 4) * 520; k.y = 32 + Math.floor(i / 4) * 143; });
|
|
104
|
+
accSet.resize(64 + 4 * 520 - 24, 64 + 2 * 143); frameSet(accSet);
|
|
105
|
+
textProp(accSet, 'Title', 'What should I pack for the desert?', 'Title'); textProp(accSet, 'Answer', 'Water, then more water. Layers for cold nights, a hat, and sun protection. Most parks have no shade at the trailhead.', 'Answer');
|
|
106
|
+
accSet.description = 'One question and its answer. The trigger is title-base text with a chevron that flips when open; the answer is body-base in text/secondary. A border/default line separates items.';
|
|
107
|
+
place(accSet);
|
|
108
|
+
var QS = [['What should I pack for the desert?', true], ['Do I need a park pass?', false], ['Can I bring my dog?', false]];
|
|
109
|
+
var accs = [];
|
|
110
|
+
for (var vname of ['Default', 'Separated', 'Bordered']) {
|
|
111
|
+
var c = comp('Variant=' + vname, 'VERTICAL'); c.counterAxisSizingMode = 'FIXED'; c.resize(480, 10); c.primaryAxisSizingMode = 'AUTO';
|
|
112
|
+
if (vname === 'Separated') bind(c, 'itemSpacing', 'Scale:space/2');
|
|
113
|
+
if (vname === 'Bordered') { stroke(c, 'Theme:border/default', 'Size:stroke/default'); radius(c, 'Size:radius/surface'); c.clipsContent = true; }
|
|
114
|
+
for (var q = 0; q < QS.length; q++) {
|
|
115
|
+
var it = pick(accSet, 'Panel=' + (QS[q][1] ? 'Open' : 'Closed') + ', State=Default'); setP(it, { Title: QS[q][0] }); add(c, [it]); fillW(it); it.layoutSizingVertical = 'HUG';
|
|
116
|
+
if (vname === 'Separated') { it.strokeTopWeight = it.strokeLeftWeight = it.strokeRightWeight = it.strokeBottomWeight = 1; radius(it, 'Size:radius/chrome'); it.clipsContent = true; }
|
|
117
|
+
else if (q === QS.length - 1) it.strokeBottomWeight = 0;
|
|
118
|
+
}
|
|
119
|
+
accs.push(c);
|
|
120
|
+
}
|
|
121
|
+
var accGroup = figma.combineAsVariants(accs, figma.currentPage); accGroup.name = 'Accordion'; row(accGroup, 64);
|
|
122
|
+
accGroup.description = 'Accordion item instances stacked. Default divides them with lines; Separated gives each its own border and radius/chrome corners with space/2 between; Bordered wraps the whole list in a radius/surface box.';
|
|
123
|
+
place(accGroup);
|
|
124
|
+
|
|
125
|
+
// Navigation menu
|
|
126
|
+
var navs = [];
|
|
127
|
+
async function trig(kind, st) {
|
|
128
|
+
var c = comp('Kind=' + kind + ', State=' + st); c.counterAxisAlignItems = 'CENTER';
|
|
129
|
+
c.counterAxisSizingMode = 'FIXED'; c.resize(100, 36); c.primaryAxisSizingMode = 'AUTO'; bind(c, 'height', 'Size:control/height-md');
|
|
130
|
+
bind(c, ['paddingLeft', 'paddingRight'], 'Size:control/padding-inline'); bind(c, 'itemSpacing', 'Size:gap/control'); radius(c, 'Size:radius/item');
|
|
131
|
+
var fg = st === 'Open' ? 'Theme:text/on-tint' : 'Theme:text/primary';
|
|
132
|
+
if (st === 'Hover') fill(c, 'Theme:fill/subtle'); if (st === 'Open') fill(c, 'Theme:interactive/tint-hover'); if (st === 'Focus') ring(c);
|
|
133
|
+
var t = await T('Destinations', 'trigger-base', fg); t.name = 'Label'; add(c, [t]);
|
|
134
|
+
if (kind === 'Menu') add(c, [icon('Chevron', st === 'Open' ? UP : DOWN, 16, fg)]);
|
|
135
|
+
return c;
|
|
136
|
+
}
|
|
137
|
+
for (var s of ['Default', 'Hover', 'Open', 'Focus']) navs.push(await trig('Menu', s));
|
|
138
|
+
for (var s of ['Default', 'Hover', 'Focus']) navs.push(await trig('Link', s));
|
|
139
|
+
var trigSet = figma.combineAsVariants(navs, figma.currentPage); trigSet.name = 'Nav trigger'; grid(trigSet, 4, 180, 64);
|
|
140
|
+
textProp(trigSet, 'Label', 'Destinations', 'Label');
|
|
141
|
+
trigSet.description = 'An item on the navigation bar. Menu opens a panel and shows a chevron; Link goes straight to a page. Control height and trigger-base text, so the bar lines up with buttons. Open is the menu tint.';
|
|
142
|
+
place(trigSet);
|
|
143
|
+
var links = [];
|
|
144
|
+
for (var s of ['Default', 'Hover']) {
|
|
145
|
+
var c = comp('State=' + s, 'VERTICAL'); bind(c, ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], 'Scale:space/2'); bind(c, 'itemSpacing', 'Scale:space/1'); radius(c, 'Size:radius/item');
|
|
146
|
+
c.counterAxisSizingMode = 'FIXED'; c.resize(192, 10); c.primaryAxisSizingMode = 'AUTO'; bind(c, 'width', 'Size:navigation-menu/column-width');
|
|
147
|
+
if (s === 'Hover') fill(c, 'Theme:fill/subtle');
|
|
148
|
+
var t = await T('Mojave', 'trigger-base', 'Theme:text/primary'); t.name = 'Title';
|
|
149
|
+
var d = await T('Joshua trees, lava tubes and dunes.', 'body-condense', 'Theme:text/secondary'); d.name = 'Description';
|
|
150
|
+
add(c, [t, d]); fillW(t); fillW(d); d.textAutoResize = 'HEIGHT'; links.push(c);
|
|
151
|
+
}
|
|
152
|
+
var linkSet = figma.combineAsVariants(links, figma.currentPage); linkSet.name = 'Nav link'; row(linkSet, 48);
|
|
153
|
+
textProp(linkSet, 'Title', 'Mojave', 'Title'); textProp(linkSet, 'Description', 'Joshua trees, lava tubes and dunes.', 'Description');
|
|
154
|
+
linkSet.description = 'A link inside an open navigation panel: a title in trigger-base over a short description. Column width navigation-menu/column-width.';
|
|
155
|
+
place(linkSet);
|
|
156
|
+
function navBar(open) {
|
|
157
|
+
var b = bar('Bar');
|
|
158
|
+
var a = pick(trigSet, 'Kind=Link, State=Default'); setP(a, { Label: 'Overview' });
|
|
159
|
+
var m = pick(trigSet, 'Kind=Menu, State=' + (open ? 'Open' : 'Default')); setP(m, { Label: 'Destinations' });
|
|
160
|
+
var p = pick(trigSet, 'Kind=Menu, State=Default'); setP(p, { Label: 'Plan a trip' });
|
|
161
|
+
add(b, [a, m, p]); return b;
|
|
162
|
+
}
|
|
163
|
+
var navSets = [];
|
|
164
|
+
var n1 = comp('Content=Closed', 'VERTICAL'); add(n1, [navBar(false)]); navSets.push(n1);
|
|
165
|
+
var n2 = comp('Content=Open', 'VERTICAL'); bind(n2, 'itemSpacing', 'Scale:space/2-5'); add(n2, [navBar(true)]);
|
|
166
|
+
var pop = AL('VERTICAL', { name: 'Popup' }); fill(pop, 'Theme:surface/canvas'); stroke(pop, 'Theme:border/default', 'Size:popup/border-width'); radius(pop, 'Size:popup/radius');
|
|
167
|
+
await pop.setEffectStyleIdAsync(ES['elevation/lg'].id); bind(pop, ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], 'Size:popup/padding-panel'); bind(pop, 'itemSpacing', 'Scale:space/2');
|
|
168
|
+
var hd = await T('DESERTS', 'eyebrow-condense', 'Theme:text/secondary'); hd.name = 'Heading';
|
|
169
|
+
var grid2 = AL('HORIZONTAL', { name: 'Links', layoutWrap: 'WRAP' }); bind(grid2, 'itemSpacing', 'Scale:space/2'); bind(grid2, 'counterAxisSpacing', 'Scale:space/2');
|
|
170
|
+
var LK = [['Mojave', 'Joshua trees, lava tubes and dunes.'], ['Sonoran', 'Saguaros and summer monsoons.'], ['Great Basin', 'Sagebrush, bristlecones and cold nights.'], ['Chihuahuan', 'Big Bend and white sand.']];
|
|
171
|
+
for (var q = 0; q < LK.length; q++) { var li = pick(linkSet, 'State=' + (q === 0 ? 'Hover' : 'Default')); setP(li, { Title: LK[q][0], Description: LK[q][1] }); add(grid2, [li]); }
|
|
172
|
+
grid2.primaryAxisSizingMode = 'FIXED'; grid2.resize(392, grid2.height);
|
|
173
|
+
add(pop, [hd, grid2]); pop.clipsContent = true;
|
|
174
|
+
var popWrap = AL('HORIZONTAL', { name: 'Under Destinations', paddingLeft: 60 }); add(popWrap, [pop]); add(n2, [popWrap]); navSets.push(n2);
|
|
175
|
+
var navSet = figma.combineAsVariants(navSets, figma.currentPage); navSet.name = 'Navigation menu'; row(navSet, 64);
|
|
176
|
+
navSet.description = 'Site navigation on a bar surface (surface/tray, bar/padding, bar/radius). A Menu trigger opens a popup panel of Nav links, on the shared popup surface with popup/padding-panel.';
|
|
177
|
+
place(navSet);
|
|
178
|
+
|
|
179
|
+
// Toolbar and Toggle group
|
|
180
|
+
var tbs = [];
|
|
181
|
+
var t1 = comp('Content=Formatting'); t1.counterAxisAlignItems = 'CENTER';
|
|
182
|
+
var tb1 = bar('Toolbar'); bind(tb1, 'itemSpacing', 'Size:bar/group-gap');
|
|
183
|
+
var g1 = AL('HORIZONTAL', { name: 'Group', counterAxisAlignItems: 'CENTER' }); bind(g1, 'itemSpacing', 'Size:bar/gap'); add(g1, [tog('Bold', true), tog('Italic', false), tog('Underline', false)]);
|
|
184
|
+
var g2 = AL('HORIZONTAL', { name: 'Group', counterAxisAlignItems: 'CENTER' }); bind(g2, 'itemSpacing', 'Size:bar/gap'); add(g2, [tog('Left', true), tog('Center', false)]);
|
|
185
|
+
add(tb1, [g1, sep(), g2]); add(t1, [tb1]); tbs.push(t1);
|
|
186
|
+
var t2 = comp('Content=Mixed'); t2.counterAxisAlignItems = 'CENTER';
|
|
187
|
+
var tb2 = bar('Toolbar'); bind(tb2, 'itemSpacing', 'Size:bar/group-gap');
|
|
188
|
+
var g3 = AL('HORIZONTAL', { name: 'Group', counterAxisAlignItems: 'CENTER' }); bind(g3, 'itemSpacing', 'Size:bar/gap'); add(g3, [tog('Bold', false), tog('Italic', false)]);
|
|
189
|
+
var sh = pick(BTN, 'Emphasis=Quiet, Size=sm, State=Default, Content=Label'); setP(sh, { Label: 'Share' });
|
|
190
|
+
var lk = AL('HORIZONTAL', { name: 'Link', counterAxisAlignItems: 'CENTER' }); bind(lk, ['paddingLeft', 'paddingRight'], 'Size:control/padding-inline-sm'); add(lk, [await T('Edited 2 min ago', 'body-base', 'Theme:text/secondary')]);
|
|
191
|
+
add(tb2, [g3, sep(), sh, lk]); add(t2, [tb2]); tbs.push(t2);
|
|
192
|
+
var tbSet = figma.combineAsVariants(tbs, figma.currentPage); tbSet.name = 'Toolbar'; row(tbSet, 64);
|
|
193
|
+
tbSet.description = 'A bar of controls for the thing below it. Groups of Toggles and Buttons sit bar/group-gap apart with a separator between; inside a group they are bar/gap apart.';
|
|
194
|
+
place(tbSet);
|
|
195
|
+
var tgs = [];
|
|
196
|
+
for (var kd of [['Single', [['Day', false], ['Week', true], ['Month', false]]], ['Multiple', [['Tents', true], ['RVs', true], ['Cabins', false]]]]) {
|
|
197
|
+
var c = comp('Kind=' + kd[0]); c.counterAxisAlignItems = 'CENTER';
|
|
198
|
+
var g = bar('Group'); kd[1].forEach(function (x) { var ti = tog(x[0], x[1], 'md'); radius(ti, 'Size:radius/item'); add(g, [ti]); });
|
|
199
|
+
add(c, [g]); tgs.push(c);
|
|
200
|
+
}
|
|
201
|
+
var tgSet = figma.combineAsVariants(tgs, figma.currentPage); tgSet.name = 'Toggle group'; row(tgSet, 64);
|
|
202
|
+
tgSet.description = 'Toggles on a bar surface, with radius/item corners so they read as one set. Single allows one on at a time, like a segmented control; Multiple allows any.';
|
|
203
|
+
place(tgSet);
|
|
204
|
+
var col = figma.currentPage.children.filter(function (n) { return n.name === 'Components'; })[0];
|
|
205
|
+
return { col: col.id, sets: figma.currentPage.findAllWithCriteria({ types: ['COMPONENT_SET'] }).map(function (s) { return s.name + ' ' + s.id; }) };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Disclosure and navigation: Collapsible. Run after 01-disclosure.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 here = function (n) { return figma.currentPage.findOne(function (x) { return x.type === 'COMPONENT_SET' && x.name === n; }); };
|
|
7
|
+
await page('Disclosure and navigation');
|
|
8
|
+
figma.currentPage.findAll(function (x) { return (x.type === 'COMPONENT_SET' || (x.type === 'FRAME' && x.getSharedPluginData('loomiere', 'wrap') === '1')) && x.name === 'Collapsible'; }).forEach(function (x) { x.remove(); });
|
|
9
|
+
var ES = {}; (await figma.getLocalEffectStylesAsync()).forEach(function (s) { ES[s.name] = s; });
|
|
10
|
+
function frameSet(set) { fill(set, 'Theme:surface/canvas'); set.strokes = [{ type: 'SOLID', color: { r: 0.592, g: 0.278, b: 1 } }]; set.dashPattern = [6, 4]; set.strokeWeight = 1; set.cornerRadius = 8; set.clipsContent = false; }
|
|
11
|
+
function grid(set, cols, colW, rowH) {
|
|
12
|
+
set.layoutMode = 'NONE';
|
|
13
|
+
set.children.forEach(function (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); });
|
|
14
|
+
set.resize(64 + cols * colW - 24, 64 + Math.ceil(set.children.length / cols) * rowH); frameSet(set);
|
|
15
|
+
}
|
|
16
|
+
var RIGHT = 'M 6 4 L 10 8 L 6 12', DOWN = 'M 4 6 L 8 10 L 12 6';
|
|
17
|
+
var cs = [];
|
|
18
|
+
for (var panel of ['Closed', 'Open']) for (var st of ['Default', 'Hover', 'Focus', 'Disabled']) {
|
|
19
|
+
var c = comp('Panel=' + panel + ', State=' + st, 'VERTICAL'); bind(c, 'itemSpacing', 'Scale:space/1'); c.counterAxisSizingMode = 'FIXED'; c.resize(360, 10); c.primaryAxisSizingMode = 'AUTO'; c.clipsContent = false;
|
|
20
|
+
var tr = AL('HORIZONTAL', { name: 'Trigger', counterAxisAlignItems: 'CENTER' }); bind(tr, 'itemSpacing', 'Scale:space/2'); bind(tr, ['paddingTop', 'paddingBottom'], 'Scale:space/1'); bind(tr, ['paddingLeft', 'paddingRight'], 'Scale:space/2'); radius(tr, 'Size:item/radius');
|
|
21
|
+
if (st === 'Hover') fill(tr, 'Theme:fill/subtle');
|
|
22
|
+
if (st === 'Focus') await tr.setEffectStyleIdAsync(ES['focus/ring'].id);
|
|
23
|
+
var lab = await T('Trip details', 'subtitle-base', 'Theme:text/primary'); lab.name = 'Label';
|
|
24
|
+
add(tr, [icon('Chevron', panel === 'Open' ? DOWN : RIGHT, 16, 'Theme:text/secondary'), lab]);
|
|
25
|
+
add(c, [tr]); tr.layoutSizingHorizontal = 'FILL';
|
|
26
|
+
if (panel === 'Open') {
|
|
27
|
+
var pn = AL('VERTICAL', { name: 'Panel' }); fill(pn, 'Theme:surface/overlay'); radius(pn, 'Size:item/radius'); bind(pn, ['paddingTop', 'paddingBottom', 'paddingRight'], 'Scale:space/2'); pn.paddingLeft = 32;
|
|
28
|
+
var tx = await T('Three nights at Jumbo Rocks, site 42. Check in after noon; quiet hours start at ten.', 'body-base', 'Theme:text/secondary'); tx.name = 'Content';
|
|
29
|
+
add(c, [pn]); pn.layoutSizingHorizontal = 'FILL'; add(pn, [tx]); tx.layoutSizingHorizontal = 'FILL'; tx.textAutoResize = 'HEIGHT';
|
|
30
|
+
}
|
|
31
|
+
if (st === 'Disabled') bind(c, 'opacity', 'Size:disclosure/opacity-disabled');
|
|
32
|
+
cs.push(c);
|
|
33
|
+
}
|
|
34
|
+
var set = figma.combineAsVariants(cs, figma.currentPage); set.name = 'Collapsible'; grid(set, 4, 400, 140);
|
|
35
|
+
textProp(set, 'Label', 'Trip details', 'Label');
|
|
36
|
+
set.description = 'One panel that opens and closes under its trigger. The chevron points right when closed and down when open; the panel sits on the overlay surface, its text lined up with the trigger label.';
|
|
37
|
+
var col = figma.currentPage.findOne(function (n) { return n.type === 'FRAME' && n.name === 'Components' && n.parent === figma.currentPage; });
|
|
38
|
+
col.appendChild(set);
|
|
39
|
+
return set.id + ' ' + Math.round(set.width) + 'x' + Math.round(set.height) + ' [' + set.children.map(function (k) { return Math.round(k.width) + 'x' + Math.round(k.height); }).join(' ') + ']';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Disclosure and navigation: 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('Disclosure and navigation');
|
|
5
|
+
// Short header lines; sets not listed use their descriptions.
|
|
6
|
+
return await C.labelSets({
|
|
7
|
+
Tab: "One tab. Selected takes accent text, the selected text style and an indicator line on the edge facing the panel. Hover is the fill/subtle wash.",
|
|
8
|
+
"Tab list": "Tab instances over a border/default divider; the selected line sits on it.",
|
|
9
|
+
"Accordion item": "A question and its answer. title-base trigger with a chevron that flips when open; body-base answer in text/secondary.",
|
|
10
|
+
Accordion: "Items stacked three ways: divided by lines, separated with their own borders, or wrapped in one box.",
|
|
11
|
+
"Nav trigger": "An item on the navigation bar. Menu opens a panel; Link goes straight to a page. Control height and trigger-base text.",
|
|
12
|
+
"Nav link": "A link in an open navigation panel: trigger-base title over a short description.",
|
|
13
|
+
"Navigation menu": "Site navigation on a bar surface; a Menu trigger opens a popup panel of Nav links.",
|
|
14
|
+
Toolbar: "Groups of controls for the content below, bar/group-gap apart with a separator between.",
|
|
15
|
+
"Toggle group": "Toggles on a bar surface with radius/item corners. Single works like a segmented control."
|
|
16
|
+
});
|