@scattered-light/loomiere 0.5.1 → 0.5.2
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/engine.js +22 -0
- package/figma/library/README.md +2 -2
- package/figma/library/check/mojave.json +41 -1
- package/figma/library/kit/base.js +1 -1
- package/figma/library/pages/15-cover.js +2 -1
- package/figma/library/pages/16-placeholders/01-placeholders.js +108 -0
- package/figma/library/pages/16-placeholders/02-labels.js +12 -0
- package/figma/library/pages/16-placeholders/03-doc.js +28 -0
- package/figma/library/pages/17-annotations/01-annotations.js +93 -0
- package/figma/library/pages/17-annotations/02-labels.js +10 -0
- package/figma/library/pages/17-annotations/03-doc.js +22 -0
- package/figma/library/run-order.json +2 -0
- package/figma/plugin/code.js +22 -0
- package/package.json +1 -1
package/figma/engine.js
CHANGED
|
@@ -345,6 +345,25 @@ async function loomiereMigrate(dryRun) {
|
|
|
345
345
|
return renamed;
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
async function loomiereLoadFonts(data) {
|
|
349
|
+
var families = {};
|
|
350
|
+
(data.textStyles || []).forEach(function (t) {
|
|
351
|
+
if (t.family) families[t.family] = true;
|
|
352
|
+
});
|
|
353
|
+
var want = [];
|
|
354
|
+
(await figma.getLocalTextStylesAsync()).forEach(function (s) {
|
|
355
|
+
want.push(s.fontName);
|
|
356
|
+
});
|
|
357
|
+
(await figma.listAvailableFontsAsync()).forEach(function (f) {
|
|
358
|
+
if (families[f.fontName.family]) want.push(f.fontName);
|
|
359
|
+
});
|
|
360
|
+
for (var i = 0; i < want.length; i++) {
|
|
361
|
+
try {
|
|
362
|
+
await figma.loadFontAsync(want[i]);
|
|
363
|
+
} catch (e) {}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
348
367
|
async function loomiereSync(data, options) {
|
|
349
368
|
options = options || {};
|
|
350
369
|
if (!data || data.format !== "loomiere-figma")
|
|
@@ -353,6 +372,9 @@ async function loomiereSync(data, options) {
|
|
|
353
372
|
);
|
|
354
373
|
var dryRun = !!options.dryRun;
|
|
355
374
|
var warnings = [];
|
|
375
|
+
// Text styles bind fontFamily to a variable, so changing that variable needs the
|
|
376
|
+
// new family loaded in every style the file's text styles use, and the old ones too.
|
|
377
|
+
if (!dryRun) await loomiereLoadFonts(data);
|
|
356
378
|
var report = {
|
|
357
379
|
dryRun: dryRun,
|
|
358
380
|
brand: data.brand,
|
package/figma/library/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Figma library pages
|
|
2
2
|
|
|
3
3
|
The scripts that draw the library's pages in a Figma file: the foundation pages (Palettes, Color,
|
|
4
|
-
Type, Scale, Size, Elevation, Themes), every component set with its labels,
|
|
5
|
-
frame, and the Cover. They read the file's variables, text styles and effect styles by name, so
|
|
4
|
+
Type, Scale, Size, Elevation, Themes), every component set with its labels, the mockup pages
|
|
5
|
+
(Placeholders and Annotations), each page's doc frame, and the Cover. They read the file's variables, text styles and effect styles by name, so
|
|
6
6
|
the same scripts build the Mojave file or any copy synced with another brand.
|
|
7
7
|
|
|
8
8
|
They used to live only in the Mojave file. Here they can rebuild a page in any copy, or the whole
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"taken": "2026-10-23",
|
|
4
4
|
"pages": {
|
|
5
5
|
"Cover": {
|
|
6
|
-
"frame:Cover": "
|
|
6
|
+
"frame:Cover": "cec8276d"
|
|
7
7
|
},
|
|
8
8
|
"Palettes": {
|
|
9
9
|
"doc:Palettes(frame)": "770c0cb8",
|
|
@@ -241,6 +241,46 @@
|
|
|
241
241
|
"wrap:Table": "c3b9a6c7",
|
|
242
242
|
"wrap:Separator": "4187560c",
|
|
243
243
|
"wrap:Scroll area": "d9c44942"
|
|
244
|
+
},
|
|
245
|
+
"Placeholders": {
|
|
246
|
+
"set:Image": "6b5d6656",
|
|
247
|
+
"set:Logo": "47c823c8",
|
|
248
|
+
"set:Chart": "28776d88",
|
|
249
|
+
"set:Map": "4f92f366",
|
|
250
|
+
"set:Video": "a65cf8d7",
|
|
251
|
+
"set:Icon slot": "b77e9819",
|
|
252
|
+
"frame:Components(shell)": "32cad7bf",
|
|
253
|
+
"wrap:Image": "56eca3f2",
|
|
254
|
+
"wrap:Logo": "3e10c93a",
|
|
255
|
+
"wrap:Chart": "c3dc3c8d",
|
|
256
|
+
"wrap:Map": "f5824235",
|
|
257
|
+
"wrap:Video": "89f27b8f",
|
|
258
|
+
"wrap:Icon slot": "58f36772",
|
|
259
|
+
"doc:Placeholders(frame)": "281f55e4",
|
|
260
|
+
"doc:Placeholders/Header": "81e6daf5",
|
|
261
|
+
"doc:Placeholders/Choosing": "6ec814fe",
|
|
262
|
+
"doc:Placeholders/Media": "9950079c",
|
|
263
|
+
"doc:Placeholders/Charts, logos and icons": "702641d2",
|
|
264
|
+
"doc:Placeholders/Usage": "8edc5116",
|
|
265
|
+
"doc:Placeholders/Spec": "79eb1b4c"
|
|
266
|
+
},
|
|
267
|
+
"Annotations": {
|
|
268
|
+
"set:Callout": "f16d1f59",
|
|
269
|
+
"set:Sticky note": "6cfb774e",
|
|
270
|
+
"set:Tag": "ae26bcd4",
|
|
271
|
+
"set:Device": "09608176",
|
|
272
|
+
"frame:Components(shell)": "f88f8da8",
|
|
273
|
+
"wrap:Callout": "e712b7b4",
|
|
274
|
+
"wrap:Sticky note": "80097e51",
|
|
275
|
+
"wrap:Tag": "522f2bdc",
|
|
276
|
+
"wrap:Device": "786ca9fb",
|
|
277
|
+
"doc:Annotations(frame)": "94fe33c1",
|
|
278
|
+
"doc:Annotations/Header": "65ff052c",
|
|
279
|
+
"doc:Annotations/Callouts and tags": "966065c9",
|
|
280
|
+
"doc:Annotations/Sticky notes": "297719ff",
|
|
281
|
+
"doc:Annotations/Devices": "72832fc3",
|
|
282
|
+
"doc:Annotations/Usage": "4bcc02ea",
|
|
283
|
+
"doc:Annotations/Spec": "344c308c"
|
|
244
284
|
}
|
|
245
285
|
}
|
|
246
286
|
}
|
|
@@ -15,7 +15,7 @@ Object.keys(TS).forEach(function (k) { var f = TS[k].fontName; FONTS[f.family +
|
|
|
15
15
|
for (var fk in FONTS) await figma.loadFontAsync(FONTS[fk]);
|
|
16
16
|
var PREFIX = figma.root.getSharedPluginData('loomiere', 'pagePrefix') || '';
|
|
17
17
|
// The library's pages, in file order.
|
|
18
|
-
var PAGES = ['Cover', 'Palettes', 'Color', 'Type', 'Scale', 'Size', 'Elevation, focus and motion', 'Themes', 'Fields', 'Buttons', 'Popups', 'Overlays', 'Disclosure and navigation', 'Display'];
|
|
18
|
+
var PAGES = ['Cover', 'Palettes', 'Color', 'Type', 'Scale', 'Size', 'Elevation, focus and motion', 'Themes', 'Fields', 'Buttons', 'Popups', 'Overlays', 'Disclosure and navigation', 'Display', 'Placeholders', 'Annotations'];
|
|
19
19
|
function v(k) { if (!V[k]) throw new Error('No variable ' + k); return V[k]; }
|
|
20
20
|
function paint(k) { return figma.variables.setBoundVariableForPaint({ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', v(k)); }
|
|
21
21
|
function fill(n, k) { n.fills = k ? [paint(k)] : []; return n; }
|
|
@@ -6,6 +6,7 @@ var LEAD = 'A desert theme with a violet lead. Every color, size and type style
|
|
|
6
6
|
var BASE_UI = '1.9';
|
|
7
7
|
var FOUNDATIONS = ['Palettes', 'Color', 'Type', 'Scale', 'Size', 'Elevation, focus and motion', 'Themes'];
|
|
8
8
|
var COMPONENTS = ['Fields', 'Buttons', 'Popups', 'Overlays', 'Disclosure and navigation', 'Display'];
|
|
9
|
+
var MOCKUPS = ['Placeholders', 'Annotations'];
|
|
9
10
|
var PALETTES = ['primary', 'secondary', 'tertiary', 'quaternary', 'neutral'];
|
|
10
11
|
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
11
12
|
var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
|
|
@@ -63,7 +64,7 @@ cov.appendChild(tb); tb.x = 96; tb.y = 120;
|
|
|
63
64
|
|
|
64
65
|
// Contents: the page names, by group.
|
|
65
66
|
var ct = AL('HORIZONTAL', { name: 'Contents', itemSpacing: 96 });
|
|
66
|
-
for (var g of [['Foundations', FOUNDATIONS], ['Components', COMPONENTS]]) {
|
|
67
|
+
for (var g of [['Foundations', FOUNDATIONS], ['Components', COMPONENTS], ['Mockups', MOCKUPS]]) {
|
|
67
68
|
var col = AL('VERTICAL', { name: g[0], itemSpacing: 8 }); add(col, [await T(g[0].toUpperCase(), 'eyebrow-condense', 'Theme:text/tertiary')]);
|
|
68
69
|
for (var n of g[1]) add(col, [await T(n, 'subtitle-condense')]);
|
|
69
70
|
add(ct, [col]);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Placeholders: Image, Logo, Chart, Map, Video, Icon slot. Stand-ins for content a mockup doesn't
|
|
2
|
+
// have yet. Built only from Theme and Size tokens, so they follow the brand, look and theme.
|
|
3
|
+
// Rebuilds the page's Components column.
|
|
4
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
5
|
+
var C = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'compHelpers'))(figma);
|
|
6
|
+
var v = C.v, paint = C.paint, fill = C.fill, stroke = C.stroke, bind = C.bind, radius = C.radius, AL = C.AL, add = C.add, T = C.T;
|
|
7
|
+
var icon = C.icon, comp = C.comp, textProp = C.textProp, place = C.place, removeOld = C.removeOld;
|
|
8
|
+
await C.page('Placeholders');
|
|
9
|
+
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; }
|
|
10
|
+
// Variants in a row, centered on one line; top aligns them instead, so labelSets sees one row.
|
|
11
|
+
function row(set, gap, top) {
|
|
12
|
+
set.layoutMode = 'NONE'; var x = 32, h = 0;
|
|
13
|
+
set.children.forEach(function (k) { k.x = x; k.y = 48; x += Math.max(k.width, 64) + gap; h = Math.max(h, k.height); });
|
|
14
|
+
set.children.forEach(function (k) { k.y = top ? 48 : 48 + Math.round((h - k.height) / 2); });
|
|
15
|
+
set.resize(x - gap + 32, h + 96); frameSet(set);
|
|
16
|
+
}
|
|
17
|
+
function box(name, w, h) { var c = comp(name); c.layoutMode = 'NONE'; c.resize(w, h); c.clipsContent = true; return c; }
|
|
18
|
+
function center(n, parent) { parent.appendChild(n); n.x = Math.round((parent.width - n.width) / 2); n.y = Math.round((parent.height - n.height) / 2); n.constraints = { horizontal: 'CENTER', vertical: 'CENTER' }; return n; }
|
|
19
|
+
function line(name, d, w, h, colorKey, weight) {
|
|
20
|
+
var vec = figma.createVector(); vec.name = name; vec.vectorPaths = [{ windingRule: 'NONZERO', data: d }];
|
|
21
|
+
vec.fills = []; vec.strokes = [paint(colorKey)]; vec.strokeWeight = weight || 1; vec.strokeCap = 'ROUND'; vec.strokeJoin = 'ROUND';
|
|
22
|
+
return vec;
|
|
23
|
+
}
|
|
24
|
+
var IMG = 'M 2 3 L 14 3 L 14 13 L 2 13 Z M 2 11 L 6 7 L 9 10 L 11 8 L 14 11 M 10.5 5.5 L 11.5 5.5';
|
|
25
|
+
async function chip(d) {
|
|
26
|
+
var c = figma.createFrame(); c.name = 'Glyph'; c.resize(36, 36); c.cornerRadius = 18; fill(c, 'Theme:surface/canvas');
|
|
27
|
+
var g = icon('Icon', d, 20, 'Theme:text/tertiary'); c.appendChild(g); g.x = 8; g.y = 8; return c;
|
|
28
|
+
}
|
|
29
|
+
figma.currentPage.children.filter(function (n) { return n.name === 'Components'; }).forEach(function (n) { n.remove(); });
|
|
30
|
+
|
|
31
|
+
// Image: a crossed box, the oldest wireframe sign for "a picture goes here". Lines scale with the box.
|
|
32
|
+
var RATIOS = [['1:1', 240, 240], ['4:3', 240, 180], ['3:2', 240, 160], ['16:9', 240, 135], ['3:1', 240, 80]], imgs = [];
|
|
33
|
+
for (var r of RATIOS) {
|
|
34
|
+
var c = box('Ratio=' + r[0], r[1], r[2]); fill(c, 'Theme:fill/subtle'); stroke(c, 'Theme:border/default', 'Size:stroke/default'); radius(c, 'Size:radius/box');
|
|
35
|
+
for (var d of ['M 0 0 L ' + r[1] + ' ' + r[2], 'M ' + r[1] + ' 0 L 0 ' + r[2]]) { var l = line('Cross', d, r[1], r[2], 'Theme:border/strong'); c.appendChild(l); l.x = 0; l.y = 0; l.constraints = { horizontal: 'SCALE', vertical: 'SCALE' }; }
|
|
36
|
+
center(await chip(IMG), c);
|
|
37
|
+
imgs.push(c);
|
|
38
|
+
}
|
|
39
|
+
var imgSet = figma.combineAsVariants(imgs, figma.currentPage); imgSet.name = 'Image'; row(imgSet, 40, true);
|
|
40
|
+
imgSet.description = 'A picture goes here: a crossed box at five ratios. Resize freely; the cross scales and the glyph stays centered. fill/subtle, border/default, radius/box.';
|
|
41
|
+
place(imgSet);
|
|
42
|
+
|
|
43
|
+
// Logo
|
|
44
|
+
var logos = [];
|
|
45
|
+
for (var s of [['Wordmark', 160, 40], ['Mark', 40, 40]]) {
|
|
46
|
+
var c = comp('Shape=' + s[0]); c.primaryAxisAlignItems = 'CENTER'; c.counterAxisAlignItems = 'CENTER';
|
|
47
|
+
c.resize(s[1], s[2]); c.primaryAxisSizingMode = 'FIXED'; c.counterAxisSizingMode = 'FIXED';
|
|
48
|
+
stroke(c, 'Theme:border/strong', 'Size:stroke/default'); c.dashPattern = [4, 3]; radius(c, 'Size:radius/minimal');
|
|
49
|
+
var t = await T(s[0] === 'Mark' ? 'L' : 'LOGO', 'eyebrow-condense', 'Theme:text/tertiary'); t.name = 'Label'; add(c, [t]);
|
|
50
|
+
logos.push(c);
|
|
51
|
+
}
|
|
52
|
+
var logoSet = figma.combineAsVariants(logos, figma.currentPage); logoSet.name = 'Logo'; row(logoSet, 40);
|
|
53
|
+
logoSet.description = 'Where a logo goes: a dashed outline so it never reads as a real mark. border/strong, radius/minimal, eyebrow-condense.';
|
|
54
|
+
place(logoSet);
|
|
55
|
+
|
|
56
|
+
// Chart
|
|
57
|
+
var charts = [];
|
|
58
|
+
for (var kind of ['Bar', 'Line', 'Donut']) {
|
|
59
|
+
var c = box('Type=' + kind, 240, 160); fill(c, 'Theme:fill/subtle'); radius(c, 'Size:radius/box');
|
|
60
|
+
if (kind === 'Bar') {
|
|
61
|
+
var H = [52, 88, 70, 112, 64, 96];
|
|
62
|
+
H.forEach(function (h, i) { var b = figma.createRectangle(); b.name = 'Bar'; b.resize(20, h); b.x = 24 + i * 34; b.y = 136 - h; fill(b, i === 3 ? 'Theme:fill/stronger' : 'Theme:fill/strong'); b.topLeftRadius = b.topRightRadius = 3; b.constraints = { horizontal: 'SCALE', vertical: 'SCALE' }; c.appendChild(b); });
|
|
63
|
+
} else if (kind === 'Line') {
|
|
64
|
+
for (var y of [40, 72, 104, 136]) { var g = line('Grid', 'M 0 0 L 192 0', 192, 0, 'Theme:border/subtle'); c.appendChild(g); g.x = 24; g.y = y; g.constraints = { horizontal: 'SCALE', vertical: 'SCALE' }; }
|
|
65
|
+
var p = line('Series', 'M 0 70 L 38 52 L 76 64 L 114 30 L 152 42 L 192 8', 192, 70, 'Theme:text/secondary', 2); c.appendChild(p); p.x = 24; p.y = 48; p.constraints = { horizontal: 'SCALE', vertical: 'SCALE' };
|
|
66
|
+
} else {
|
|
67
|
+
var seg = [['Theme:fill/stronger', 0, 2.4], ['Theme:fill/strong', 2.4, 4.4], ['Theme:fill/default', 4.4, 2 * Math.PI]];
|
|
68
|
+
seg.forEach(function (s) { var e = figma.createEllipse(); e.name = 'Segment'; e.resize(112, 112); e.x = 64; e.y = 24; e.arcData = { startingAngle: s[1] - Math.PI / 2, endingAngle: s[2] - Math.PI / 2, innerRadius: 0.6 }; fill(e, s[0]); e.constraints = { horizontal: 'CENTER', vertical: 'CENTER' }; c.appendChild(e); });
|
|
69
|
+
}
|
|
70
|
+
charts.push(c);
|
|
71
|
+
}
|
|
72
|
+
var chartSet = figma.combineAsVariants(charts, figma.currentPage); chartSet.name = 'Chart'; row(chartSet, 40);
|
|
73
|
+
chartSet.description = 'A chart goes here: bars, a line or a donut in grays, so nobody mistakes it for real data. fill/subtle background, fill/default to fill/stronger marks.';
|
|
74
|
+
place(chartSet);
|
|
75
|
+
|
|
76
|
+
// Map
|
|
77
|
+
var m = box('Style=Default', 240, 160); fill(m, 'Theme:fill/subtle'); radius(m, 'Size:radius/box');
|
|
78
|
+
var roads = ['M 0 110 L 90 96 L 150 120 L 240 100', 'M 70 0 L 92 96 L 104 160', 'M 150 120 L 176 0', 'M 0 40 L 240 56'];
|
|
79
|
+
roads.forEach(function (d, i) { var r = line('Road', d, 240, 160, 'Theme:surface/canvas', i < 2 ? 6 : 3); m.appendChild(r); var xs = [], ys = []; d.replace(/(-?[\d.]+) (-?[\d.]+)/g, function (q, x, y) { xs.push(+x); ys.push(+y); }); r.x = Math.min.apply(null, xs); r.y = Math.min.apply(null, ys); r.constraints = { horizontal: 'SCALE', vertical: 'SCALE' }; });
|
|
80
|
+
var pin = icon('Pin', 'M 8 14.5 C 8 14.5 3 9.5 3 6.2 C 3 3.3 5.2 1.2 8 1.2 C 10.8 1.2 13 3.3 13 6.2 C 13 9.5 8 14.5 8 14.5 Z M 8 4.7 L 8 7.3', 28, 'Theme:text/primary', 1.6);
|
|
81
|
+
m.appendChild(pin); pin.x = 106; pin.y = 58; pin.constraints = { horizontal: 'CENTER', vertical: 'CENTER' };
|
|
82
|
+
var mapSet = figma.combineAsVariants([m], figma.currentPage); mapSet.name = 'Map'; row(mapSet, 40);
|
|
83
|
+
mapSet.description = 'A map goes here: roads on a gray ground and a pin. fill/subtle, surface/canvas roads, text/primary pin.';
|
|
84
|
+
place(mapSet);
|
|
85
|
+
|
|
86
|
+
// Video
|
|
87
|
+
var vid = box('Style=Default', 240, 135); fill(vid, 'Theme:surface/inverse'); radius(vid, 'Size:radius/box');
|
|
88
|
+
var play = figma.createFrame(); play.name = 'Play'; play.resize(44, 44); play.cornerRadius = 22; fill(play, 'Theme:surface/canvas');
|
|
89
|
+
var tri = figma.createVector(); tri.name = 'Triangle'; tri.vectorPaths = [{ windingRule: 'NONZERO', data: 'M 0 0 L 14 8 L 0 16 Z' }]; fill(tri, 'Theme:text/primary'); play.appendChild(tri); tri.x = 17; tri.y = 14;
|
|
90
|
+
center(play, vid);
|
|
91
|
+
var track = figma.createRectangle(); track.name = 'Track'; track.resize(216, 4); track.x = 12; track.y = 119; track.cornerRadius = 2; fill(track, 'Theme:text/tertiary'); track.constraints = { horizontal: 'STRETCH', vertical: 'MAX' }; vid.appendChild(track);
|
|
92
|
+
var prog = figma.createRectangle(); prog.name = 'Played'; prog.resize(72, 4); prog.x = 12; prog.y = 119; prog.cornerRadius = 2; fill(prog, 'Theme:text/on-inverse'); prog.constraints = { horizontal: 'SCALE', vertical: 'MAX' }; vid.appendChild(prog);
|
|
93
|
+
var vidSet = figma.combineAsVariants([vid], figma.currentPage); vidSet.name = 'Video'; row(vidSet, 40);
|
|
94
|
+
vidSet.description = 'A video goes here: the inverse surface, a play button and a progress track. surface/inverse, surface/canvas, text/on-inverse.';
|
|
95
|
+
place(vidSet);
|
|
96
|
+
|
|
97
|
+
// Icon slot
|
|
98
|
+
var slots = [];
|
|
99
|
+
for (var z of ['minimize', 'condense', 'base', 'magnify', 'maximize']) {
|
|
100
|
+
var c = box('Size=' + z, 16, 16); bind(c, ['width', 'height'], 'Size:icon-size/' + z);
|
|
101
|
+
stroke(c, 'Theme:border/strong', 1); c.dashPattern = [2, 2]; radius(c, 'Size:radius/minimal'); c.clipsContent = false;
|
|
102
|
+
var x = line('Cross', 'M 0 0 L 16 16 M 16 0 L 0 16', 16, 16, 'Theme:border/default'); c.appendChild(x); x.x = 0; x.y = 0; x.constraints = { horizontal: 'SCALE', vertical: 'SCALE' };
|
|
103
|
+
slots.push(c);
|
|
104
|
+
}
|
|
105
|
+
var slotSet = figma.combineAsVariants(slots, figma.currentPage); slotSet.name = 'Icon slot'; row(slotSet, 32);
|
|
106
|
+
slotSet.description = 'An icon goes here, at the icon-size steps. Swap it for a real icon later. border/strong dashed, radius/minimal.';
|
|
107
|
+
place(slotSet);
|
|
108
|
+
return { built: ['Image', 'Logo', 'Chart', 'Map', 'Video', 'Icon slot'], page: figma.currentPage.id };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Placeholders: a header 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('Placeholders');
|
|
5
|
+
return await C.labelSets({
|
|
6
|
+
Image: 'A crossed box at five ratios: a picture goes here. Resize freely; the glyph stays centered.',
|
|
7
|
+
Logo: 'A dashed outline where a logo goes, so it never reads as a real mark.',
|
|
8
|
+
Chart: 'Bars, a line or a donut in grays, so nobody mistakes it for real data.',
|
|
9
|
+
Map: 'Roads on a gray ground and a pin: a map goes here.',
|
|
10
|
+
Video: 'The inverse surface, a play button and a progress track: a video goes here.',
|
|
11
|
+
'Icon slot': 'An icon goes here, at the five icon-size steps.'
|
|
12
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Placeholders doc frame. Run after every Placeholders set is built and labelled.
|
|
2
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
3
|
+
var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
|
|
4
|
+
await L.page('Placeholders');
|
|
5
|
+
var K = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docKit'))(figma);
|
|
6
|
+
var AL = L.AL, add = L.add, T = L.T, section = L.section, hrow = L.hrow, divider = L.divider, inst = K.inst, stage = K.stage, captioned = K.captioned, notes = K.notes, usage = K.usage;
|
|
7
|
+
var doc = await L.docFrame('Placeholders', 'MOCKUPS', 'Placeholders', 'Stand-ins for content a mockup does not have yet: pictures, logos, charts, maps, video and icons. They are drawn only with theme tokens, so they follow the brand, the look and light or dark. Components sit to the right.');
|
|
8
|
+
doc.x = 0; doc.y = 400;
|
|
9
|
+
var pick = await section(doc, 'Choosing', 'Pick the one that says what will go there, so reviewers read the layout, not the art.');
|
|
10
|
+
var picks = [['A photo or illustration', 'Image', 'Pick the ratio the real picture will have.'], ['A brand or partner mark', 'Logo', 'Wordmark for a header, Mark for tight spots.'], ['Numbers shown as a graphic', 'Chart', 'Bar, Line or Donut, in grays so nobody reads the data.'], ['A place', 'Map', 'Stretch it; the roads scale with the box.'], ['Something that plays', 'Video', 'Sits on the inverse surface like a real player.'], ['An icon not chosen yet', 'Icon slot', 'At the icon-size step the real icon will use.']];
|
|
11
|
+
var pt = AL('VERTICAL', { name: 'Picker' }); add(pick, [pt]); pt.layoutSizingHorizontal = 'FILL';
|
|
12
|
+
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)]); }
|
|
13
|
+
// Placeholders sit on fill/subtle themselves, so their stages use the page color with an edge.
|
|
14
|
+
function plain(parent, name) { var st = stage(parent, name); L.fill(st, 'Theme:surface/canvas'); L.stroke(st, 'Theme:border/subtle', 1); return st; }
|
|
15
|
+
var media = await section(doc, 'Media', 'A crossed box is the oldest sign for "a picture goes here". Image comes in five ratios; resize any of them and the cross scales while the glyph stays centered. Map and Video stretch the same way.');
|
|
16
|
+
var s1 = plain(media, 'Images');
|
|
17
|
+
for (var q of ['1:1', '4:3', '16:9', '3:1']) add(s1, [await captioned(inst('Image', 'Ratio=' + q), q)]);
|
|
18
|
+
var s1b = plain(media, 'Map and video');
|
|
19
|
+
add(s1b, [await captioned(inst('Map', 'Style=Default'), 'Map'), await captioned(inst('Video', 'Style=Default'), 'Video')]);
|
|
20
|
+
var marks = await section(doc, 'Charts, logos and icons', 'Charts use the fill steps so they read as shapes, never as real numbers. Logos are dashed outlines so they never pass for a real mark. Icon slots hold the place at each icon size.');
|
|
21
|
+
var s2 = plain(marks, 'Charts');
|
|
22
|
+
for (var c of ['Bar', 'Line', 'Donut']) add(s2, [await captioned(inst('Chart', 'Type=' + c), c)]);
|
|
23
|
+
var s2b = plain(marks, 'Logos and icons'); s2b.counterAxisAlignItems = 'CENTER';
|
|
24
|
+
add(s2b, [await captioned(inst('Logo', 'Shape=Wordmark'), 'Wordmark'), await captioned(inst('Logo', 'Shape=Mark'), 'Mark')]);
|
|
25
|
+
for (var z of ['minimize', 'condense', 'base', 'magnify', 'maximize']) add(s2b, [await captioned(inst('Icon slot', 'Size=' + z), z)]);
|
|
26
|
+
await usage(doc, ['Mockups and wireframes before the real content exists.', 'Showing the size and ratio the real content will need.'], ['Shipping them in a product. They are for design files only.', 'Lorem ipsum text. Write real words, or use skeleton lines.']);
|
|
27
|
+
await notes(doc, [['Ground', 'fill/subtle · radius/box', 'Quiet, so the layout leads.'], ['Edge', 'border/default · stroke/default', 'Shows the box without weight.'], ['Cross and outlines', 'border/strong', 'Dashed for logos and icon slots.'], ['Marks', 'fill/default · fill/strong · fill/stronger', 'Chart bars and segments.'], ['Video', 'surface/inverse · text/on-inverse', 'Reads as a player in any theme.'], ['Icon slot', 'icon-size/*', 'The same steps as real icons.']]);
|
|
28
|
+
return { id: doc.id, h: Math.round(doc.height), overflow: K.overflow(doc) };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Annotations: Callout, Sticky note, Tag, Device. Marks for reviews and handoff on any mockup.
|
|
2
|
+
// Callouts, tags and notes use fixed colors on purpose, so they never read as part of the design.
|
|
3
|
+
// Device frames use theme tokens, so they follow the look. Rebuilds the page's Components column.
|
|
4
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
5
|
+
var C = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'compHelpers'))(figma);
|
|
6
|
+
var v = C.v, paint = C.paint, fill = C.fill, stroke = C.stroke, bind = C.bind, radius = C.radius, AL = C.AL, add = C.add, T = C.T;
|
|
7
|
+
var comp = C.comp, textProp = C.textProp, place = C.place, removeOld = C.removeOld;
|
|
8
|
+
await C.page('Annotations');
|
|
9
|
+
function hex(h) { return { r: parseInt(h.slice(1, 3), 16) / 255, g: parseInt(h.slice(3, 5), 16) / 255, b: parseInt(h.slice(5, 7), 16) / 255 }; }
|
|
10
|
+
function solid(n, h) { n.fills = [{ type: 'SOLID', color: hex(h) }]; return n; }
|
|
11
|
+
var MARK = '#2F6BFF', MARK_TEXT = '#1F4FD1', MARK_TINT = '#EAF0FF', NOTE_TEXT = '#26231F';
|
|
12
|
+
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; }
|
|
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; h = Math.max(h, k.height); });
|
|
16
|
+
set.children.forEach(function (k) { k.y = 48; });
|
|
17
|
+
set.resize(x - gap + 32, h + 96); frameSet(set);
|
|
18
|
+
}
|
|
19
|
+
figma.currentPage.children.filter(function (n) { return n.name === 'Components'; }).forEach(function (n) { n.remove(); });
|
|
20
|
+
|
|
21
|
+
// Callout: a numbered dot, or a dot with a short label
|
|
22
|
+
var calls = [];
|
|
23
|
+
for (var kind of ['Number', 'Label']) {
|
|
24
|
+
var c = comp('Type=' + kind); c.counterAxisAlignItems = 'CENTER'; c.itemSpacing = 8;
|
|
25
|
+
if (kind === 'Label') { solid(c, MARK); c.cornerRadius = 14; c.paddingLeft = 2; c.paddingRight = 12; c.paddingTop = c.paddingBottom = 2; }
|
|
26
|
+
var dot = AL('HORIZONTAL', { name: 'Dot', primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER' });
|
|
27
|
+
dot.resize(24, 24); dot.primaryAxisSizingMode = 'FIXED'; dot.counterAxisSizingMode = 'FIXED'; dot.cornerRadius = 12;
|
|
28
|
+
solid(dot, kind === 'Label' ? '#FFFFFF' : MARK);
|
|
29
|
+
var n = await T('1', 'subtitle-condense'); n.name = 'Number'; solid(n, kind === 'Label' ? MARK_TEXT : '#FFFFFF'); add(dot, [n]);
|
|
30
|
+
add(c, [dot]);
|
|
31
|
+
if (kind === 'Label') { var l = await T('Opens the date picker', 'label-base'); l.name = 'Label'; solid(l, '#FFFFFF'); add(c, [l]); }
|
|
32
|
+
calls.push(c);
|
|
33
|
+
}
|
|
34
|
+
var callSet = figma.combineAsVariants(calls, figma.currentPage); callSet.name = 'Callout'; row(callSet, 40);
|
|
35
|
+
textProp(callSet, 'Number', '1', 'Number'); textProp(callSet, 'Label', 'Opens the date picker', 'Label');
|
|
36
|
+
callSet.description = 'Number a spot in a wireframe and explain it in the notes, or label it right there. A fixed blue that no theme uses, so it never reads as part of the design.';
|
|
37
|
+
place(callSet);
|
|
38
|
+
|
|
39
|
+
// Sticky note
|
|
40
|
+
var NOTES = { Yellow: '#FFF0A6', Pink: '#FFD5E5', Blue: '#D6E6FF' }, stickies = [];
|
|
41
|
+
for (var col in NOTES) {
|
|
42
|
+
var c = comp('Color=' + col, 'VERTICAL'); c.itemSpacing = 6; c.paddingTop = c.paddingBottom = c.paddingLeft = c.paddingRight = 16;
|
|
43
|
+
c.resize(200, 120); c.counterAxisSizingMode = 'FIXED'; c.primaryAxisSizingMode = 'AUTO'; solid(c, NOTES[col]); c.cornerRadius = 2;
|
|
44
|
+
c.effects = [{ type: 'DROP_SHADOW', color: { r: 0, g: 0, b: 0, a: 0.12 }, offset: { x: 0, y: 2 }, radius: 6, spread: 0, visible: true, blendMode: 'NORMAL' }];
|
|
45
|
+
var t = await T('Question', 'subtitle-base'); t.name = 'Title'; solid(t, NOTE_TEXT);
|
|
46
|
+
var b = await T('Do we need a guest checkout here, or can people book without an account?', 'body-condense'); b.name = 'Note'; solid(b, NOTE_TEXT);
|
|
47
|
+
add(c, [t, b]); t.layoutSizingHorizontal = 'FILL'; b.layoutSizingHorizontal = 'FILL'; b.textAutoResize = 'HEIGHT';
|
|
48
|
+
stickies.push(c);
|
|
49
|
+
}
|
|
50
|
+
var stickySet = figma.combineAsVariants(stickies, figma.currentPage); stickySet.name = 'Sticky note'; row(stickySet, 32);
|
|
51
|
+
textProp(stickySet, 'Title', 'Question', 'Title'); textProp(stickySet, 'Note', 'Do we need a guest checkout here, or can people book without an account?', 'Note');
|
|
52
|
+
stickySet.description = 'A note for reviewers: questions, ideas, decisions. Pick a color per person or per kind of note. Fixed colors, outside the theme.';
|
|
53
|
+
place(stickySet);
|
|
54
|
+
|
|
55
|
+
// Tag
|
|
56
|
+
var tag = comp('Style=Default'); tag.counterAxisAlignItems = 'CENTER'; tag.paddingLeft = tag.paddingRight = 8; tag.paddingTop = tag.paddingBottom = 2; tag.cornerRadius = 4;
|
|
57
|
+
solid(tag, MARK_TINT); tag.strokes = [{ type: 'SOLID', color: hex(MARK) }]; tag.strokeWeight = 1; tag.strokeAlign = 'INSIDE';
|
|
58
|
+
var tl = await T('TBD', 'eyebrow-condense'); tl.name = 'Label'; solid(tl, MARK_TEXT); add(tag, [tl]);
|
|
59
|
+
var tagSet = figma.combineAsVariants([tag], figma.currentPage); tagSet.name = 'Tag'; row(tagSet, 40);
|
|
60
|
+
var tk = tagSet.addComponentProperty('Label', 'TEXT', 'TBD'); tl.componentPropertyReferences = { characters: tk };
|
|
61
|
+
tagSet.description = 'Mark something as not settled: TBD, Question, Changed, Idea. Same fixed blue as callouts.';
|
|
62
|
+
place(tagSet);
|
|
63
|
+
|
|
64
|
+
// Device: phone, tablet and desktop frames with a Content area to design in
|
|
65
|
+
var DEV = [['Phone', 390, 844, 44, 40], ['Tablet', 834, 1194, 28, 24], ['Desktop', 1440, 900, 48, 10]], devs = [];
|
|
66
|
+
for (var d of DEV) {
|
|
67
|
+
var c = comp('Device=' + d[0], 'VERTICAL'); c.resize(d[1], d[2]); c.primaryAxisSizingMode = 'FIXED'; c.counterAxisSizingMode = 'FIXED'; c.clipsContent = true;
|
|
68
|
+
fill(c, 'Theme:surface/canvas'); stroke(c, 'Theme:border/strong', 'Size:stroke/default'); c.cornerRadius = d[4];
|
|
69
|
+
var bar = AL('HORIZONTAL', { name: d[0] === 'Desktop' ? 'Browser bar' : 'Status bar', counterAxisAlignItems: 'CENTER', itemSpacing: 8, paddingLeft: d[0] === 'Desktop' ? 16 : 28, paddingRight: d[0] === 'Desktop' ? 16 : 28 });
|
|
70
|
+
bar.resize(d[1], d[3]); bar.counterAxisSizingMode = 'FIXED'; add(c, [bar]); bar.layoutSizingHorizontal = 'FILL';
|
|
71
|
+
if (d[0] === 'Desktop') {
|
|
72
|
+
fill(bar, 'Theme:surface/tray'); bar.strokes = [paint('Theme:border/default')]; bar.strokeTopWeight = bar.strokeLeftWeight = bar.strokeRightWeight = 0; bar.strokeBottomWeight = 1;
|
|
73
|
+
for (var q = 0; q < 3; q++) { var e = figma.createEllipse(); e.name = 'Dot'; e.resize(12, 12); fill(e, 'Theme:fill/strong'); add(bar, [e]); }
|
|
74
|
+
var url = AL('HORIZONTAL', { name: 'Address', counterAxisAlignItems: 'CENTER', paddingLeft: 12, paddingRight: 12 }); url.resize(560, 28); url.counterAxisSizingMode = 'FIXED'; url.primaryAxisSizingMode = 'FIXED';
|
|
75
|
+
fill(url, 'Theme:surface/canvas'); stroke(url, 'Theme:border/default', 1); radius(url, 'Size:radius/control');
|
|
76
|
+
add(url, [await T('example.com', 'body-condense', 'Theme:text/tertiary')]); var sp = AL('HORIZONTAL', { name: 'Spacer' }); add(bar, [sp, url]); sp.resize(160, 1);
|
|
77
|
+
} else {
|
|
78
|
+
bar.primaryAxisAlignItems = 'SPACE_BETWEEN';
|
|
79
|
+
var time = await T('9:41', 'subtitle-condense'); var ind = AL('HORIZONTAL', { name: 'Indicators', itemSpacing: 4, counterAxisAlignItems: 'CENTER' });
|
|
80
|
+
[[16, 10], [14, 10], [24, 11]].forEach(function (s) { var r = figma.createRectangle(); r.resize(s[0], s[1]); r.cornerRadius = 3; fill(r, 'Theme:text/primary'); add(ind, [r]); });
|
|
81
|
+
add(bar, [time, ind]);
|
|
82
|
+
}
|
|
83
|
+
var content = AL('VERTICAL', { name: 'Content' }); add(c, [content]); content.layoutSizingHorizontal = 'FILL'; content.layoutSizingVertical = 'FILL';
|
|
84
|
+
if (d[0] !== 'Desktop') {
|
|
85
|
+
var home = AL('HORIZONTAL', { name: 'Home indicator', primaryAxisAlignItems: 'CENTER', counterAxisAlignItems: 'CENTER' }); home.resize(d[1], 34); home.counterAxisSizingMode = 'FIXED';
|
|
86
|
+
var hb = figma.createRectangle(); hb.name = 'Bar'; hb.resize(134, 5); hb.cornerRadius = 3; fill(hb, 'Theme:text/primary'); add(home, [hb]); add(c, [home]); home.layoutSizingHorizontal = 'FILL';
|
|
87
|
+
}
|
|
88
|
+
devs.push(c);
|
|
89
|
+
}
|
|
90
|
+
var devSet = figma.combineAsVariants(devs, figma.currentPage); devSet.name = 'Device'; row(devSet, 80);
|
|
91
|
+
devSet.description = 'A screen to sketch in: phone (390×844), tablet (834×1194) and desktop browser (1440×900). Put your layout in Content. Chrome uses surface/canvas, surface/tray and border/strong, so it follows the look.';
|
|
92
|
+
place(devSet);
|
|
93
|
+
return { built: ['Callout', 'Sticky note', 'Tag', 'Device'], page: figma.currentPage.id };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Annotations: a header 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('Annotations');
|
|
5
|
+
return await C.labelSets({
|
|
6
|
+
Callout: 'Number a spot and explain it in the notes, or label it in place. Fixed blue, outside the theme.',
|
|
7
|
+
'Sticky note': 'Questions and ideas for reviewers. A color per person or per kind of note.',
|
|
8
|
+
Tag: 'Marks something as not settled. Change the label: TBD, Question, Changed, Idea.',
|
|
9
|
+
Device: 'Phone, tablet and desktop browser frames. Put the layout in Content.'
|
|
10
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Annotations doc frame. Run after every Annotations set is built and labelled.
|
|
2
|
+
var AF = Object.getPrototypeOf(async function () {}).constructor;
|
|
3
|
+
var L = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
|
|
4
|
+
await L.page('Annotations');
|
|
5
|
+
var K = await new AF('figma', figma.root.getSharedPluginData('loomiere', 'docKit'))(figma);
|
|
6
|
+
var add = L.add, section = L.section, inst = K.inst, stage = K.stage, captioned = K.captioned, notes = K.notes, usage = K.usage;
|
|
7
|
+
var doc = await L.docFrame('Annotations', 'MOCKUPS', 'Annotations', 'Marks for reviews and handoff: numbered callouts, sticky notes, tags for open questions, and device frames to design in. Callouts, notes and tags use fixed colors that no theme uses, so markup never reads as part of the design. Components sit to the right.');
|
|
8
|
+
doc.x = 0; doc.y = 400;
|
|
9
|
+
var mk = await section(doc, 'Callouts and tags', 'Number a spot and explain it in notes beside the frame, or label it in place. Tag anything not settled yet.');
|
|
10
|
+
var s1 = stage(mk, 'Marks'); s1.counterAxisAlignItems = 'CENTER';
|
|
11
|
+
add(s1, [await captioned(inst('Callout', 'Type=Number'), 'Number'), await captioned(inst('Callout', 'Type=Label'), 'Label')]);
|
|
12
|
+
for (var w of ['TBD', 'Question', 'Changed']) add(s1, [await captioned(inst('Tag', 'Style=Default', { Label: w }), w)]);
|
|
13
|
+
var st = await section(doc, 'Sticky notes', 'Questions, ideas and decisions for reviewers. Give each person or each kind of note its own color.');
|
|
14
|
+
var s2 = stage(st, 'Notes');
|
|
15
|
+
for (var c of ['Yellow', 'Pink', 'Blue']) add(s2, [await captioned(inst('Sticky note', 'Color=' + c), c)]);
|
|
16
|
+
var dv = await section(doc, 'Devices', 'Phone (390×844), tablet (834×1194) and desktop browser (1440×900). Put the layout in the Content frame. The chrome uses theme tokens, so it follows the look.');
|
|
17
|
+
var s3 = stage(dv, 'Devices'); s3.counterAxisAlignItems = 'MAX';
|
|
18
|
+
var ph = inst('Device', 'Device=Phone'); ph.rescale(0.5); var tb = inst('Device', 'Device=Tablet'); tb.rescale(0.35); var dk = inst('Device', 'Device=Desktop'); dk.rescale(0.4);
|
|
19
|
+
add(s3, [await captioned(ph, 'Phone, at 50%'), await captioned(tb, 'Tablet, at 35%'), await captioned(dk, 'Desktop, at 40%')]);
|
|
20
|
+
await usage(doc, ['Reviews, handoff notes and open questions.', 'Numbered callouts with the explanation beside the frame.'], ['Theme colors for markup. It would read as part of the design.', 'Notes left in a file after the decision is made.']);
|
|
21
|
+
await notes(doc, [['Mark', '#2F6BFF fill · #1F4FD1 text', 'A fixed blue, outside every theme.'], ['Tag', '#EAF0FF ground · #2F6BFF edge', 'Same blue, quieter.'], ['Notes', '#FFF0A6 · #FFD5E5 · #D6E6FF', 'Paper colors with #26231F text.'], ['Device', 'surface/canvas · surface/tray · border/strong', 'Follows the look and theme.']]);
|
|
22
|
+
return { id: doc.id, h: Math.round(doc.height), overflow: K.overflow(doc) };
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
{ "page": "Overlays", "files": ["pages/12-overlays/01-overlays.js", "pages/12-overlays/02-labels.js", "pages/12-overlays/03-doc.js"] },
|
|
17
17
|
{ "page": "Disclosure and navigation", "files": ["pages/13-disclosure/01-disclosure.js", "pages/13-disclosure/02-collapsible.js", "pages/13-disclosure/03-labels.js", "pages/13-disclosure/04-doc.js"] },
|
|
18
18
|
{ "page": "Display", "files": ["pages/14-display/01-display.js", "pages/14-display/02-more-display.js", "pages/14-display/03-labels.js", "pages/14-display/04-doc.js"] },
|
|
19
|
+
{ "page": "Placeholders", "files": ["pages/16-placeholders/01-placeholders.js", "pages/16-placeholders/02-labels.js", "pages/16-placeholders/03-doc.js"] },
|
|
20
|
+
{ "page": "Annotations", "files": ["pages/17-annotations/01-annotations.js", "pages/17-annotations/02-labels.js", "pages/17-annotations/03-doc.js"] },
|
|
19
21
|
{ "page": "Cover", "files": ["pages/15-cover.js"] }
|
|
20
22
|
]
|
|
21
23
|
}
|
package/figma/plugin/code.js
CHANGED
|
@@ -346,6 +346,25 @@ async function loomiereMigrate(dryRun) {
|
|
|
346
346
|
return renamed;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
async function loomiereLoadFonts(data) {
|
|
350
|
+
var families = {};
|
|
351
|
+
(data.textStyles || []).forEach(function (t) {
|
|
352
|
+
if (t.family) families[t.family] = true;
|
|
353
|
+
});
|
|
354
|
+
var want = [];
|
|
355
|
+
(await figma.getLocalTextStylesAsync()).forEach(function (s) {
|
|
356
|
+
want.push(s.fontName);
|
|
357
|
+
});
|
|
358
|
+
(await figma.listAvailableFontsAsync()).forEach(function (f) {
|
|
359
|
+
if (families[f.fontName.family]) want.push(f.fontName);
|
|
360
|
+
});
|
|
361
|
+
for (var i = 0; i < want.length; i++) {
|
|
362
|
+
try {
|
|
363
|
+
await figma.loadFontAsync(want[i]);
|
|
364
|
+
} catch (e) {}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
349
368
|
async function loomiereSync(data, options) {
|
|
350
369
|
options = options || {};
|
|
351
370
|
if (!data || data.format !== "loomiere-figma")
|
|
@@ -354,6 +373,9 @@ async function loomiereSync(data, options) {
|
|
|
354
373
|
);
|
|
355
374
|
var dryRun = !!options.dryRun;
|
|
356
375
|
var warnings = [];
|
|
376
|
+
// Text styles bind fontFamily to a variable, so changing that variable needs the
|
|
377
|
+
// new family loaded in every style the file's text styles use, and the old ones too.
|
|
378
|
+
if (!dryRun) await loomiereLoadFonts(data);
|
|
357
379
|
var report = {
|
|
358
380
|
dryRun: dryRun,
|
|
359
381
|
brand: data.brand,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scattered-light/loomiere",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Turns a small brand file into a full design token theme: OKLCH color ramps, a type scale, light, dark and high-contrast modes, plus Tailwind, MUI, Chakra, AG Grid and Figma exports.",
|
|
5
5
|
"keywords": ["design-tokens", "theme", "style-dictionary", "dtcg", "oklch", "base-ui", "ag-grid", "tailwind"],
|
|
6
6
|
"repository": {
|