@scattered-light/loomiere 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +3 -1
  2. package/bin/loomiere.mjs +3 -1
  3. package/build/build-tokens.mjs +180 -35
  4. package/exporters/design-system.mjs +7 -0
  5. package/exporters/figma.mjs +545 -478
  6. package/exporters/index.mjs +4 -0
  7. package/figma/README.md +24 -8
  8. package/figma/library/README.md +92 -0
  9. package/figma/library/check/known-differences.md +20 -0
  10. package/figma/library/check/mojave.json +246 -0
  11. package/figma/library/kit/base.js +89 -0
  12. package/figma/library/kit/components.js +95 -0
  13. package/figma/library/kit/docs.js +37 -0
  14. package/figma/library/kit/fingerprint.js +143 -0
  15. package/figma/library/pages/02-palettes.js +40 -0
  16. package/figma/library/pages/03-color.js +126 -0
  17. package/figma/library/pages/04-type.js +56 -0
  18. package/figma/library/pages/05-scale.js +58 -0
  19. package/figma/library/pages/06-size.js +111 -0
  20. package/figma/library/pages/07-elevation-focus-motion.js +108 -0
  21. package/figma/library/pages/08-themes.js +36 -0
  22. package/figma/library/pages/09-fields/01-input.js +55 -0
  23. package/figma/library/pages/09-fields/02-checks.js +82 -0
  24. package/figma/library/pages/09-fields/03-field.js +32 -0
  25. package/figma/library/pages/09-fields/04-more-fields.js +144 -0
  26. package/figma/library/pages/09-fields/05-labels.js +14 -0
  27. package/figma/library/pages/09-fields/06-doc.js +75 -0
  28. package/figma/library/pages/10-buttons/01-buttons.js +97 -0
  29. package/figma/library/pages/10-buttons/02-doc.js +55 -0
  30. package/figma/library/pages/11-popups/01-popups.js +166 -0
  31. package/figma/library/pages/11-popups/02-more-popups.js +90 -0
  32. package/figma/library/pages/11-popups/03-labels.js +14 -0
  33. package/figma/library/pages/11-popups/04-doc.js +89 -0
  34. package/figma/library/pages/12-overlays/01-overlays.js +135 -0
  35. package/figma/library/pages/12-overlays/02-labels.js +10 -0
  36. package/figma/library/pages/12-overlays/03-doc.js +80 -0
  37. package/figma/library/pages/13-disclosure/01-disclosure.js +205 -0
  38. package/figma/library/pages/13-disclosure/02-collapsible.js +39 -0
  39. package/figma/library/pages/13-disclosure/03-labels.js +16 -0
  40. package/figma/library/pages/13-disclosure/04-doc.js +91 -0
  41. package/figma/library/pages/14-display/01-display.js +165 -0
  42. package/figma/library/pages/14-display/02-more-display.js +55 -0
  43. package/figma/library/pages/14-display/03-labels.js +14 -0
  44. package/figma/library/pages/14-display/04-doc.js +96 -0
  45. package/figma/library/pages/15-cover.js +87 -0
  46. package/figma/library/run-order.json +21 -0
  47. package/figma/library/tools/compare.mjs +11 -0
  48. package/figma/library/tools/diff-snippet.js +15 -0
  49. package/figma/library/tools/kit-script.mjs +21 -0
  50. package/figma/library/tools/run.mjs +27 -0
  51. package/figma/library/tools/runs.mjs +34 -0
  52. package/generator/brand.schema.json +83 -0
  53. package/generator/generate.mjs +26 -6
  54. package/generator/looks.mjs +62 -0
  55. package/lib/contrast.mjs +24 -6
  56. package/lib/figma.mjs +58 -23
  57. package/package.json +2 -1
  58. package/tokens/README.md +26 -0
@@ -30,6 +30,10 @@ const wanted = (id, only) => only === 'all' || only.some((pick) => id === pick |
30
30
  */
31
31
  export function exportAll(catalog, roles, outDir, only = 'all', context = {}) {
32
32
  const theme = readTheme(catalog, roles);
33
+ // A brand with several looks: each look's own theme, the brand's own look first.
34
+ if (context.looks?.length > 1) {
35
+ theme.looks = context.looks.map(({ name, label, description, base, catalog: c, roles: r }) => ({ name, label, description, base, theme: base ? theme : readTheme(c, r) }));
36
+ }
33
37
  // tokens.css sits beside exports/; adapters that re-use its rules (Tailwind's pair and scope
34
38
  // utilities) read it from here.
35
39
  const tokensCss = path.join(outDir, '..', 'tokens.css');
package/figma/README.md CHANGED
@@ -27,7 +27,7 @@ group in a hidden collection, and the published collection's modes point at the
27
27
 
28
28
  | Collection | Modes | Holds |
29
29
  | -------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
- | Brand (hidden) | Default | Font families, and a complete group of the brand's colors per theme: `default/color/…`, then `deuteranopia/color/…` and the other color vision themes. Data colors are here by name (`color/data/violet`). A color a vision theme doesn't change points at default's. |
30
+ | Brand (hidden; published with looks) | Default, or one per look | Font families, and a complete group of the brand's colors per theme: `default/color/…`, then `deuteranopia/color/…` and the other color vision themes. Data colors are here by name (`color/data/violet`). A color a vision theme doesn't change points at default's. |
31
31
  | Scale (hidden) | Default | The raw steps: space, radius, stroke and icon size scales, and the font sizes as `desktop/`, `tablet/` and `mobile/font-size/…`, each `clamp()` read at that width (tablet at 768px). |
32
32
  | Size | Desktop, Tablet, Mobile | The sizes designers pick: font sizes, and the named semantic and component sizes (control heights, gaps, padding), each pointing at a Scale step. |
33
33
  | Theme | Light, Dark, Light high contrast, Dark high contrast, Deuteranopia, Protanopia, Tritanopia, Achromatopsia | The themes, as `data-theme` in CSS. Each color vision theme is a light theme of its own with the brand's colors redesigned for that kind of color vision. Holds every semantic color designers pick, the data series slots (`data/1`…, each pointing at the named data color that theme puts there), and, with no scopes, the palette scopes and each brand pair's parts (`pair/<name>/bg`…) that Status points at. |
@@ -50,6 +50,21 @@ Effect styles hold what a variable can't: elevation/xs to elevation/2xl from the
50
50
  focus/ring (gap, ring and halo), and focus/field and focus/field-error for fields. Shadow colors that are
51
51
  Theme tokens stay bound, so the focus styles follow light, dark and the other themes.
52
52
 
53
+ ### Looks
54
+
55
+ A brand with [looks](../tokens/README.md#looks) gets one Brand mode per look, the brand's own
56
+ first (Mojave, Classic). Everything else stays as it is: a value in Scale, Size or Theme that
57
+ differs between looks points at a Brand variable named `look/<collection>/<mode>/<name>`
58
+ (`look/theme/light/scope/accent/solid`), which holds each look's value. Set the Brand mode on a
59
+ frame to switch its look; Theme, Size, Status and Emphasis still switch on their own.
60
+
61
+ With looks, Brand is published, so a file that uses the library can set the look too. Its
62
+ variables still have no scopes, so they stay out of the color and number pickers.
63
+
64
+ Limits: text styles and effect styles come from the brand's own look. A text style's size and
65
+ family are variables, so they follow the look, but its line height and letter spacing are fixed
66
+ numbers. Shadow colors that aren't Theme tokens are the brand look's.
67
+
53
68
  ### Plans
54
69
 
55
70
  Modes per collection depend on the Figma plan: Professional allows 10, Organization 20. The sync
@@ -126,10 +141,11 @@ has.
126
141
 
127
142
  ## Files
128
143
 
129
- | File | What it is |
130
- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
131
- | `../exporters/figma.mjs` | Writes `figma.json` from the token catalog. |
132
- | `engine.js` | The sync engine: plain JavaScript for Figma's plugin API, with no imports, so the plugin, the push scripts and Node all run the same text. |
133
- | `../lib/figma.mjs` | `loomiere figma`: the push scripts, the four ways, `serve`. |
134
- | `plugin/` | The plugin. `code.js` is generated from `engine.js` and `plugin-main.js` by `npm run build -w @scattered-light/loomiere`. |
135
- | `../../../scripts/check-figma.mjs` | `npm run check:figma`: runs the engine against a stand-in for Figma's API in both layouts, whole and in parts, and checks a second run changes nothing. |
144
+ | File | What it is |
145
+ | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
146
+ | `../exporters/figma.mjs` | Writes `figma.json` from the token catalog. |
147
+ | `engine.js` | The sync engine: plain JavaScript for Figma's plugin API, with no imports, so the plugin, the push scripts and Node all run the same text. |
148
+ | `../lib/figma.mjs` | `loomiere figma`: the push scripts, the four ways, `serve`. |
149
+ | `library/` | The scripts that draw the library pages (foundations, components, docs, Cover) in a file, with a run order and checks against Mojave. See [library/README.md](library/README.md). |
150
+ | `plugin/` | The plugin. `code.js` is generated from `engine.js` and `plugin-main.js` by `npm run build -w @scattered-light/loomiere`. |
151
+ | `../../../scripts/check-figma.mjs` | `npm run check:figma`: runs the engine against a stand-in for Figma's API in both layouts, whole and in parts, and checks a second run changes nothing. |
@@ -0,0 +1,92 @@
1
+ # Figma library pages
2
+
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, each page's doc
5
+ frame, and the Cover. They read the file's variables, text styles and effect styles by name, so
6
+ the same scripts build the Mojave file or any copy synced with another brand.
7
+
8
+ They used to live only in the Mojave file. Here they can rebuild a page in any copy, or the whole
9
+ library in a new file, and a check run proves the result matches Mojave.
10
+
11
+ ## Before you start
12
+
13
+ The file needs the theme first: run a Figma sync (`loomiere figma push`, or "sync Figma" in a
14
+ chat; see [../README.md](../README.md)). The page scripts only draw. They never create variables or
15
+ styles, and they stop with "No variable …" if one is missing.
16
+
17
+ Scripts run through the Figma connector's `use_figma`, which takes up to 50,000 characters of
18
+ code and has no network access. So the shared code (the kit) is stored in the file itself, and
19
+ each page script loads it from there.
20
+
21
+ ## Build or patch a file
22
+
23
+ ```sh
24
+ # A new file, or after a kit change: store the kit, then every page in order.
25
+ node tools/runs.mjs --store
26
+ # Only some pages, in run order. Each script rebuilds its page's sets and doc frame.
27
+ node tools/runs.mjs --pages Fields,Themes
28
+ ```
29
+
30
+ `runs.mjs` packs [run-order.json](run-order.json) into as few scripts as fit under the limit and
31
+ writes them to `runs/` as `run-01.js`, `run-02.js`… Run them in the file in that order. Each
32
+ prints what it built; a script that fails partway can be run again, since every script removes
33
+ what it made last time before drawing.
34
+
35
+ The order matters because pages use each other's components: Fields needs Button for some of its
36
+ sets, Themes shows Field, Select, Checkbox and Switch, and the later pages use Button, Field and
37
+ Toggle. When a page changes, rebuild it and then the pages after it that use its sets.
38
+
39
+ ### What a rebuild replaces
40
+
41
+ A rebuild deletes a component set and draws it again. Instances inside the library pages are
42
+ redrawn with it. Instances anywhere else in the file, such as screens in a project copy, still
43
+ point at the deleted set and stop updating. So in a copy that already has designs:
44
+
45
+ - Change colors, sizes, type and effects with a Figma sync. It updates variables and styles in
46
+ place, and every bound layer follows.
47
+ - Add a new component by running its page's scripts. Sets that already exist are rebuilt in the
48
+ same place, so run only on files where nothing outside the library uses them yet, or rebuild
49
+ just the new set's script.
50
+
51
+ ## Check a run against Mojave
52
+
53
+ ```sh
54
+ node tools/runs.mjs --check --pages Overlays # draws on a "✱ check / Overlays" page
55
+ node tools/run.mjs --units Overlays # a script that returns the page's fingerprints
56
+ node tools/compare.mjs Overlays '<fingerprints>' # compares with check/mojave.json
57
+ node tools/run.mjs --diff Overlays=set:Dialog,doc:Overlays/Dialog # the layers that differ
58
+ ```
59
+
60
+ A check run builds on pages named "✱ check / …" beside the real ones and never touches the real
61
+ pages. Components it needs from pages that have no check page come from the real ones. Delete
62
+ the check pages when done, and clear the prefix with `node tools/run.mjs --real` (a script that
63
+ only resets it).
64
+
65
+ Fingerprints are short hashes of what each set, wrapper and doc section looks like: sizes,
66
+ layout, variables by name, text styles, instance targets. They never use node IDs, so two files
67
+ built the same way match. [check/known-differences.md](check/known-differences.md) lists where
68
+ Mojave and the scripts differ on purpose.
69
+
70
+ ## Files
71
+
72
+ | File | What it is |
73
+ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
74
+ | `kit/base.js` | Lookups for variables, styles and pages, and the small builders every script uses. Stored as `docHelpers`. |
75
+ | `kit/components.js` | base.js plus icons, focus rings, component helpers, the Components column and `labelSets`. Stored as `compHelpers`. |
76
+ | `kit/docs.js` | base.js plus stages, captions, usage columns and spec tables for doc frames. Stored as `docKit`. |
77
+ | `kit/fingerprint.js` | Fingerprints and layer diffs for checks. Stored as `fingerprint`. |
78
+ | `pages/` | One script per foundation page; a folder per component page, numbered in run order. |
79
+ | `run-order.json` | The order to run the page scripts in. |
80
+ | `tools/kit-script.mjs` | Writes the script that stores the kit (`--only docKit` for one part, `--hashes` to compare). |
81
+ | `tools/run.mjs` | Bundles scripts into one `use_figma` script; `--check`, `--real`, `--store`, `--units`, `--diff`. |
82
+ | `tools/runs.mjs` | Packs run-order.json into numbered scripts under the size limit. |
83
+ | `tools/compare.mjs`, `tools/diff-snippet.js` | Compare fingerprints with Mojave, and show the layers that differ. |
84
+ | `check/mojave.json` | Mojave's fingerprints, 23 October 2026. |
85
+
86
+ ## Adding a component
87
+
88
+ Write its script in the page's folder, after the existing ones: build the variants with the kit's
89
+ `comp`, combine them, give the set a description, then `place(set)` so it joins the Components
90
+ column. Add it to the page's labels script and doc script, and to `run-order.json`. Run it with
91
+ `--check` first, look at it, then run it for real. Once it is in Mojave, refresh `check/mojave.json`
92
+ with a `--units` run of that page.
@@ -0,0 +1,20 @@
1
+ # Known differences from Mojave
2
+
3
+ `check/mojave.json` holds the fingerprints of the Mojave file as it stood on 23 October 2026. A check
4
+ run of every page matches it, except for the differences below. In each case the scripts are right
5
+ and Mojave is out of date: either Mojave was built before a later change, or text was edited by
6
+ hand in Mojave and the scripts now carry the edit.
7
+
8
+ | Page | Where | Difference |
9
+ | ------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
10
+ | Color | Contrast tables | Mojave shows older ratios; the scripts compute them from the current variables when they run. |
11
+ | Color | Theme rows | Mojave's rows follow the old variable order; the scripts follow the current order. |
12
+ | Size | Opacity rows | Mojave shows opacity as 0.5; the scripts show 50, which is how Figma reads a bound opacity. |
13
+ | Fields | Picker rows 5 to 8, the Combobox "Avoid" line | Layer names. The text was edited in place in Mojave, so the layers kept their old names. |
14
+ | Buttons | Picker row 5 | Layer name, as above. |
15
+ | Popups | Picker rows 7 to 9 | Layer names, as above. |
16
+ | Cover | Lead text | Layer name, as above. |
17
+ | Cover | Title block | Mojave reports hug width while being 600 wide; the script fixes it at 600, since a hugging block would shrink to the title. |
18
+
19
+ When a check shows a difference that is not on this list, either a script or Mojave changed. Run
20
+ `node tools/run.mjs --diff Page=unit,unit` to see the layers that differ.
@@ -0,0 +1,246 @@
1
+ {
2
+ "file": "4vd4kRXowo3AjoQNWsDu0D",
3
+ "taken": "2026-10-23",
4
+ "pages": {
5
+ "Cover": {
6
+ "frame:Cover": "12ef885a"
7
+ },
8
+ "Palettes": {
9
+ "doc:Palettes(frame)": "770c0cb8",
10
+ "doc:Palettes/Header": "aef6c9ee",
11
+ "doc:Palettes/Neutral": "935b1685",
12
+ "doc:Palettes/Primary": "bb6c309c",
13
+ "doc:Palettes/Secondary": "3caecc37",
14
+ "doc:Palettes/Tertiary": "0ff1f3f0",
15
+ "doc:Palettes/Quaternary": "f256b3b6",
16
+ "doc:Palettes/Info": "e19b2086",
17
+ "doc:Palettes/Success": "906f40a1",
18
+ "doc:Palettes/Warning": "a2b3b587",
19
+ "doc:Palettes/Error": "e558e4ef",
20
+ "doc:Palettes/Data colors": "18c701f7"
21
+ },
22
+ "Color": {
23
+ "doc:Color(frame)": "bc97eff3",
24
+ "doc:Color/Header": "06674d9c",
25
+ "doc:Color/How the layers fit": "255539db",
26
+ "doc:Color/Surfaces": "0cddb652",
27
+ "doc:Color/Text": "96bca119",
28
+ "doc:Color/Borders": "89f1f42d",
29
+ "doc:Color/Fills": "fc7b43a6",
30
+ "doc:Color/Interactive": "9284003b",
31
+ "doc:Color/Status": "fa79dc92",
32
+ "doc:Color/Focus and overlay": "07a1d16c",
33
+ "doc:Color/Long-form content": "dc067f0a",
34
+ "doc:Color/Status and emphasis": "5ab44147"
35
+ },
36
+ "Type": {
37
+ "doc:Type(frame)": "50dfb9ed",
38
+ "doc:Type/Header": "f00b32a5",
39
+ "doc:Type/Title": "06b09c65",
40
+ "doc:Type/Body": "1d34862d",
41
+ "doc:Type/Label": "11175252",
42
+ "doc:Type/Trigger": "14d64d28",
43
+ "doc:Type/Selected": "c79e22e5",
44
+ "doc:Type/Link": "8ff20a84",
45
+ "doc:Type/Subtitle": "a0a22231",
46
+ "doc:Type/Eyebrow": "03c9c6ae",
47
+ "doc:Type/Code": "5d3c6d39"
48
+ },
49
+ "Scale": {
50
+ "doc:Scale(frame)": "14751dc4",
51
+ "doc:Scale/Header": "f303ef41",
52
+ "doc:Scale/Space": "efb36c2c",
53
+ "doc:Scale/Radius": "5342775b",
54
+ "doc:Scale/Stroke": "9477db2c",
55
+ "doc:Scale/Font size": "78890681"
56
+ },
57
+ "Size": {
58
+ "doc:Size(frame)": "3da2a99c",
59
+ "doc:Size/Header": "7de475f6",
60
+ "doc:Size/Space": "a549b294",
61
+ "doc:Size/Radius": "22610155",
62
+ "doc:Size/Stroke": "d68d19fa",
63
+ "doc:Size/Controls and icons": "460221cb",
64
+ "doc:Size/Spec reference": "7f6192bc"
65
+ },
66
+ "Elevation, focus and motion": {
67
+ "doc:Elevation, focus and motion(frame)": "f3ae9a72",
68
+ "doc:Elevation, focus and motion/Header": "5a542c9e",
69
+ "doc:Elevation, focus and motion/Elevation": "c20ef33d",
70
+ "doc:Elevation, focus and motion/Focus": "59275e2f",
71
+ "doc:Elevation, focus and motion/Motion": "5371fc6a",
72
+ "doc:Elevation, focus and motion/Jobs": "d02b4be0"
73
+ },
74
+ "Themes": {
75
+ "doc:Themes(frame)": "8afb7fff",
76
+ "doc:Themes/Header": "6c4eb058",
77
+ "doc:Themes/All eight themes": "8a55f844"
78
+ },
79
+ "Fields": {
80
+ "set:Field": "f7cb48fb",
81
+ "set:Input": "6c2b0d54",
82
+ "set:Select": "442baeee",
83
+ "set:Textarea": "92f7f774",
84
+ "set:Checkbox": "2599731a",
85
+ "set:Radio": "91b6e532",
86
+ "set:Switch": "19bb0959",
87
+ "set:Combobox field": "01c1d61f",
88
+ "set:Autocomplete": "a81a3346",
89
+ "set:Number field": "58d13ece",
90
+ "set:OTP field": "f6a1224f",
91
+ "set:Checkbox group": "f5f67eac",
92
+ "set:Fieldset": "70450478",
93
+ "set:Form": "012115e1",
94
+ "frame:Components(shell)": "a43d23d9",
95
+ "doc:Fields(frame)": "1f58d1ef",
96
+ "doc:Fields/Header": "ac90f480",
97
+ "doc:Fields/Choosing a control": "60930777",
98
+ "doc:Fields/Field": "52eb7ea8",
99
+ "doc:Fields/Input, Select and Textarea": "667585cd",
100
+ "doc:Fields/Checkbox and Radio": "98e44af9",
101
+ "doc:Fields/Switch": "9f6d0241",
102
+ "doc:Fields/Combobox and Autocomplete": "39ad22a9",
103
+ "doc:Fields/Number field and OTP field": "3d859762",
104
+ "doc:Fields/Checkbox group": "2f95ef55",
105
+ "doc:Fields/Fieldset and Form": "ee563701",
106
+ "wrap:Field": "760b8799",
107
+ "wrap:Input": "d5883aac",
108
+ "wrap:Select": "94429931",
109
+ "wrap:Textarea": "88452dd3",
110
+ "wrap:Checkbox": "d0ade3f2",
111
+ "wrap:Radio": "97c80ea7",
112
+ "wrap:Switch": "740522a5",
113
+ "wrap:Combobox field": "46e241db",
114
+ "wrap:Autocomplete": "ed53691e",
115
+ "wrap:Number field": "738d8ca7",
116
+ "wrap:OTP field": "8476481b",
117
+ "wrap:Checkbox group": "04546bc2",
118
+ "wrap:Fieldset": "049ea0b8",
119
+ "wrap:Form": "80971363"
120
+ },
121
+ "Buttons": {
122
+ "set:Button": "59742be5",
123
+ "set:Toggle": "f85382f0",
124
+ "frame:Components(shell)": "b607a157",
125
+ "doc:Buttons(frame)": "fb3c0173",
126
+ "doc:Buttons/Header": "33e88cd7",
127
+ "doc:Buttons/Choosing a button": "10202b27",
128
+ "doc:Buttons/Emphasis and status": "ec54ae73",
129
+ "doc:Buttons/Sizes and icons": "75bcfc1c",
130
+ "doc:Buttons/Icon only": "05a78b7a",
131
+ "doc:Buttons/Toggle": "1b4b097e",
132
+ "wrap:Button": "6b3cb817",
133
+ "wrap:Toggle": "1c3b06ef"
134
+ },
135
+ "Popups": {
136
+ "set:Menu item": "f7b7f316",
137
+ "set:List item": "e0831f16",
138
+ "set:Popup part": "c5922528",
139
+ "set:Menu": "6f370851",
140
+ "set:List popup": "20c43337",
141
+ "set:Popover": "09663bde",
142
+ "set:Tooltip": "a649c419",
143
+ "set:Menubar trigger": "3b163892",
144
+ "set:Menubar": "b48c1f59",
145
+ "set:Context menu": "6036f0e7",
146
+ "set:Preview card": "c3aa3f00",
147
+ "frame:Components(shell)": "e98d9c96",
148
+ "doc:Popups(frame)": "63b5c8ea",
149
+ "doc:Popups/Header": "f33b648c",
150
+ "doc:Popups/Choosing a popup": "b5405324",
151
+ "doc:Popups/Menu": "03945f79",
152
+ "doc:Popups/Select and Combobox lists": "dd7345e6",
153
+ "doc:Popups/Popover": "deedd40c",
154
+ "doc:Popups/Tooltip": "3b34f8c2",
155
+ "doc:Popups/Context menu and Menubar": "88aa53f4",
156
+ "doc:Popups/Preview card": "d7b5638d",
157
+ "wrap:Menu item": "d5656be8",
158
+ "wrap:List item": "d6e17ed3",
159
+ "wrap:Popup part": "bfa16f2c",
160
+ "wrap:Menu": "4e21aec7",
161
+ "wrap:List popup": "df9d7af3",
162
+ "wrap:Popover": "6bcad5ee",
163
+ "wrap:Tooltip": "1871ff95",
164
+ "wrap:Menubar trigger": "c6bd01e6",
165
+ "wrap:Menubar": "38c11276",
166
+ "wrap:Context menu": "6aed485d",
167
+ "wrap:Preview card": "87d2bd4a"
168
+ },
169
+ "Overlays": {
170
+ "set:Dialog": "0561ff0b",
171
+ "set:Drawer": "5d4508a8",
172
+ "set:Toast": "f7e3afc8",
173
+ "frame:Components(shell)": "5ba0aa18",
174
+ "doc:Overlays(frame)": "52a37527",
175
+ "doc:Overlays/Header": "9779ec4e",
176
+ "doc:Overlays/Choosing an overlay": "953a3511",
177
+ "doc:Overlays/Dialog": "7c1d85f6",
178
+ "doc:Overlays/Drawer": "58eb79fb",
179
+ "doc:Overlays/Toast": "430c4eee",
180
+ "wrap:Dialog": "39d7f8c9",
181
+ "wrap:Drawer": "a7547195",
182
+ "wrap:Toast": "0dbfb765"
183
+ },
184
+ "Disclosure and navigation": {
185
+ "set:Tab": "9956f55e",
186
+ "set:Tab list": "65ecc9ee",
187
+ "set:Accordion item": "4bc88874",
188
+ "set:Accordion": "08a20c6c",
189
+ "set:Nav trigger": "6dfd79f5",
190
+ "set:Nav link": "95643ccc",
191
+ "set:Navigation menu": "5fe72c8b",
192
+ "set:Toolbar": "20cbc081",
193
+ "set:Toggle group": "7f7d2f59",
194
+ "set:Collapsible": "470bec8d",
195
+ "frame:Components(shell)": "b3fd0dbc",
196
+ "doc:Disclosure and navigation(frame)": "9a5954ea",
197
+ "doc:Disclosure and navigation/Header": "9b37279e",
198
+ "doc:Disclosure and navigation/Choosing": "466ca5a9",
199
+ "doc:Disclosure and navigation/Tabs": "5e99defe",
200
+ "doc:Disclosure and navigation/Accordion": "255fff5e",
201
+ "doc:Disclosure and navigation/Navigation menu": "26b02c57",
202
+ "doc:Disclosure and navigation/Toolbar and Toggle group": "6c530db2",
203
+ "doc:Disclosure and navigation/Collapsible": "b4718c92",
204
+ "wrap:Tab": "3882aa7d",
205
+ "wrap:Tab list": "25caa0a9",
206
+ "wrap:Accordion item": "2a40ca7f",
207
+ "wrap:Accordion": "13924902",
208
+ "wrap:Nav trigger": "d34f44b0",
209
+ "wrap:Nav link": "8270ab86",
210
+ "wrap:Navigation menu": "943b347c",
211
+ "wrap:Toolbar": "def74e8f",
212
+ "wrap:Toggle group": "0a9794a6",
213
+ "wrap:Collapsible": "5b849605"
214
+ },
215
+ "Display": {
216
+ "set:Avatar": "ecb75184",
217
+ "set:Avatar group": "4b3380bf",
218
+ "set:Meter": "91905363",
219
+ "set:Progress": "2bf6bde2",
220
+ "set:Slider": "d0a9f876",
221
+ "set:Table row": "65b9d02d",
222
+ "set:Table": "504ce16f",
223
+ "set:Separator": "4fc10c4f",
224
+ "set:Scroll area": "d975ef8a",
225
+ "frame:Components(shell)": "2d92208b",
226
+ "doc:Display(frame)": "8e3f9ff8",
227
+ "doc:Display/Header": "ac51b33b",
228
+ "doc:Display/Choosing": "9c898af9",
229
+ "doc:Display/Avatar": "e22c9ae4",
230
+ "doc:Display/Meter and Progress": "1ba30ce0",
231
+ "doc:Display/Slider": "41be582c",
232
+ "doc:Display/Table": "a0cb6855",
233
+ "doc:Display/Separator": "4ca97dbe",
234
+ "doc:Display/Scroll area": "64eeb8b1",
235
+ "wrap:Avatar": "41a8b826",
236
+ "wrap:Avatar group": "f4b9cf55",
237
+ "wrap:Meter": "81dd7d37",
238
+ "wrap:Progress": "a1e0c526",
239
+ "wrap:Slider": "9e3cc9bb",
240
+ "wrap:Table row": "53077168",
241
+ "wrap:Table": "c3b9a6c7",
242
+ "wrap:Separator": "4187560c",
243
+ "wrap:Scroll area": "d9c44942"
244
+ }
245
+ }
246
+ }
@@ -0,0 +1,89 @@
1
+ // The library kit's base: lookups by name for variables, text styles and pages, and the small
2
+ // builders every page script uses (auto layout, text, fills bound to variables). Stored in the
3
+ // file as shared plugin data ('loomiere', 'docHelpers'); scripts load it with
4
+ // var L = await new AsyncFunction('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
5
+ // Pages are found by name. While a check runs, ('loomiere', 'pagePrefix') holds a prefix, so a
6
+ // script builds "<prefix>Buttons" instead of touching the real Buttons page.
7
+ var cols = await figma.variables.getLocalVariableCollectionsAsync(), allVars = await figma.variables.getLocalVariablesAsync();
8
+ var CN = {}, COL = {}, V = {}, TS = {};
9
+ cols.forEach(function (c) { CN[c.id] = c; COL[c.name] = c; });
10
+ allVars.forEach(function (x) { V[CN[x.variableCollectionId].name + ':' + x.name] = x; });
11
+ (await figma.getLocalTextStylesAsync()).forEach(function (s) { TS[s.name] = s; });
12
+ // Every font the text styles use, so any brand's fonts are ready before text is made.
13
+ var FONTS = {};
14
+ Object.keys(TS).forEach(function (k) { var f = TS[k].fontName; FONTS[f.family + '/' + f.style] = f; });
15
+ for (var fk in FONTS) await figma.loadFontAsync(FONTS[fk]);
16
+ var PREFIX = figma.root.getSharedPluginData('loomiere', 'pagePrefix') || '';
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'];
19
+ function v(k) { if (!V[k]) throw new Error('No variable ' + k); return V[k]; }
20
+ function paint(k) { return figma.variables.setBoundVariableForPaint({ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', v(k)); }
21
+ function fill(n, k) { n.fills = k ? [paint(k)] : []; return n; }
22
+ function stroke(n, k, w, align) { n.strokes = [paint(k)]; n.strokeAlign = align || 'INSIDE'; if (typeof w === 'string') n.setBoundVariable('strokeWeight', v(w)); else n.strokeWeight = w || 1; return n; }
23
+ function bind(n, fields, k) { (Array.isArray(fields) ? fields : [fields]).forEach(function (f) { n.setBoundVariable(f, v(k)); }); return n; }
24
+ function radius(n, k) { return bind(n, ['topLeftRadius', 'topRightRadius', 'bottomLeftRadius', 'bottomRightRadius'], k); }
25
+ function pad(n, block, inline) { if (block) bind(n, ['paddingTop', 'paddingBottom'], block); if (inline) bind(n, ['paddingLeft', 'paddingRight'], inline); return n; }
26
+ function AL(dir, props) { var f = figma.createAutoLayout(dir || 'HORIZONTAL'); f.fills = []; f.clipsContent = false; if (props) f.set(props); return f; }
27
+ function add(parent, kids) { kids.forEach(function (k) { if (k) parent.appendChild(k); }); return parent; }
28
+ async function T(str, style, colorKey, w, name) {
29
+ var t = figma.createText();
30
+ if (!TS[style]) throw new Error('No text style ' + style);
31
+ await t.setTextStyleIdAsync(TS[style].id);
32
+ t.characters = str;
33
+ fill(t, colorKey || 'Theme:text/primary');
34
+ if (w) { t.resize(w, t.height); t.textAutoResize = 'HEIGHT'; }
35
+ t.name = name || str.slice(0, 40);
36
+ return t;
37
+ }
38
+ function unescape(s) { return (s || '').replace(/&(quot|#39|lt|gt|amp);/g, function (m, e) { return { quot: '"', '#39': "'", lt: '<', gt: '>', amp: '&' }[e]; }); }
39
+ function mode(n, colName, modeName) { var c = COL[colName]; n.setExplicitVariableModeForCollection(c, c.modes.filter(function (m) { return m.name === modeName; })[0].modeId); return n; }
40
+ // A library page by its name (with the check prefix while a check runs), or null.
41
+ function findPage(name) { return figma.root.children.filter(function (x) { return x.name === PREFIX + name; })[0] || null; }
42
+ // Switch to a library page, making it in its place in PAGES if the file doesn't have it.
43
+ async function page(name) {
44
+ var p = findPage(name);
45
+ if (!p) {
46
+ p = figma.createPage(); p.name = PREFIX + name;
47
+ var at = PAGES.indexOf(name), next = null;
48
+ for (var i = at + 1; at !== -1 && i < PAGES.length && !next; i++) next = findPage(PAGES[i]);
49
+ if (next) figma.root.insertChild(figma.root.children.indexOf(next), p);
50
+ }
51
+ await figma.setCurrentPageAsync(p);
52
+ return p;
53
+ }
54
+ // A component set by name on another library page (loads that page).
55
+ async function setOn(pageName, setName) {
56
+ // A check run falls back to the real page for sets it hasn't rebuilt yet.
57
+ var p = findPage(pageName) || (PREFIX && figma.root.children.filter(function (x) { return x.name === pageName; })[0]);
58
+ if (!p) throw new Error('No page ' + pageName);
59
+ await p.loadAsync();
60
+ var s = p.findAllWithCriteria({ types: ['COMPONENT_SET', 'COMPONENT'] }).filter(function (n) { return n.name === setName && (n.type === 'COMPONENT_SET' || n.parent.type !== 'COMPONENT_SET'); })[0];
61
+ if (!s) throw new Error('No component ' + setName + ' on ' + pageName);
62
+ return s;
63
+ }
64
+ async function docFrame(name, eyebrow, title, lead) {
65
+ var old = figma.currentPage.children.filter(function (x) { return x.name === name; });
66
+ old.forEach(function (x) { x.remove(); });
67
+ var f = AL('VERTICAL', { name: name, itemSpacing: 72, paddingTop: 96, paddingBottom: 120, paddingLeft: 96, paddingRight: 96 });
68
+ fill(f, 'Theme:surface/canvas');
69
+ f.resize(1440, 100); f.counterAxisSizingMode = 'FIXED'; f.primaryAxisSizingMode = 'AUTO';
70
+ var head = AL('VERTICAL', { name: 'Header', itemSpacing: 16 });
71
+ add(head, [await T(eyebrow, 'eyebrow-condense', 'Theme:text/accent'), await T(title, 'title-maximize'), await T(lead, 'body-magnify', 'Theme:text/secondary', 820)]);
72
+ add(f, [head]);
73
+ return f;
74
+ }
75
+ async function section(parent, title, intro) {
76
+ var s = AL('VERTICAL', { name: title, itemSpacing: 24 });
77
+ add(parent, [s]); s.layoutSizingHorizontal = 'FILL';
78
+ var h = AL('VERTICAL', { name: 'Section header', itemSpacing: 8 });
79
+ add(h, [await T(title, 'title-amplify'), intro ? await T(intro, 'body-base', 'Theme:text/secondary', 820) : null]);
80
+ add(s, [h]);
81
+ return s;
82
+ }
83
+ function hrow(parent) { var r = AL('HORIZONTAL', { itemSpacing: 24, counterAxisAlignItems: 'CENTER', paddingTop: 12, paddingBottom: 12 }); add(parent, [r]); r.layoutSizingHorizontal = 'FILL'; return r; }
84
+ function divider(n) { n.strokes = [paint('Theme:border/subtle')]; n.strokeTopWeight = 0; n.strokeLeftWeight = 0; n.strokeRightWeight = 0; n.strokeBottomWeight = 1; return n; }
85
+ function lum(c) { var f = function (x) { return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); }; return 0.2126 * f(c.r) + 0.7152 * f(c.g) + 0.0722 * f(c.b); }
86
+ function ratio(a, b) { var x = lum(a), y = lum(b); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); }
87
+ function resolved(k, node) { var r = v(k).resolveForConsumer(node); return r.value; }
88
+ function hex(c) { var p = function (x) { return ('0' + Math.round(x * 255).toString(16)).slice(-2); }; return '#' + p(c.r) + p(c.g) + p(c.b); }
89
+ return { V: V, COL: COL, TS: TS, PAGES: PAGES, PREFIX: PREFIX, v: v, paint: paint, fill: fill, stroke: stroke, bind: bind, radius: radius, pad: pad, AL: AL, add: add, T: T, unescape: unescape, mode: mode, findPage: findPage, page: page, setOn: setOn, docFrame: docFrame, section: section, hrow: hrow, divider: divider, lum: lum, ratio: ratio, resolved: resolved, hex: hex };
@@ -0,0 +1,95 @@
1
+ // The kit for component scripts: everything in base.js, plus icons, focus rings, component and
2
+ // variant helpers, and the page's Components column. Stored as ('loomiere', 'compHelpers').
3
+ var L = await new (Object.getPrototypeOf(async function () {}).constructor)('figma', figma.root.getSharedPluginData('loomiere', 'docHelpers'))(figma);
4
+ var V = L.V, COL = L.COL, TS = L.TS, 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;
5
+ function shadow(color, spread, opts) {
6
+ var e = { type: 'DROP_SHADOW', color: { r: 0, g: 0, b: 0, a: 1 }, offset: { x: 0, y: (opts && opts.y) || 0 }, radius: (opts && opts.blur) || 0, spread: spread, visible: true, blendMode: 'NORMAL', showShadowBehindNode: false };
7
+ return color ? figma.variables.setBoundVariableForEffect(e, 'color', v(color)) : Object.assign(e, { color: opts.rgba });
8
+ }
9
+ function focusRing() { return [shadow('Theme:surface/canvas', 2), shadow('Theme:focus/ring', 4), shadow('Theme:interactive/tint-hover', 6)]; }
10
+ function icon(name, d, size, colorKey, weight) {
11
+ var f = figma.createFrame(); f.name = name; f.fills = []; f.resize(16, 16); f.clipsContent = false;
12
+ var vec = figma.createVector(); vec.vectorPaths = [{ windingRule: 'NONZERO', data: d }]; vec.fills = [];
13
+ vec.strokes = [paint(colorKey)]; vec.strokeWeight = weight || 1.5; vec.strokeCap = 'ROUND'; vec.strokeJoin = 'ROUND';
14
+ var xs = [], ys = []; d.replace(/(-?[\d.]+) (-?[\d.]+)/g, function (m, x, y) { xs.push(+x); ys.push(+y); });
15
+ f.appendChild(vec); vec.x = Math.min.apply(null, xs); vec.y = Math.min.apply(null, ys);
16
+ if (size !== 16) f.rescale(size / 16);
17
+ return f;
18
+ }
19
+ function comp(name, dir) { var c = figma.createComponent(); c.name = name; c.layoutMode = dir || 'HORIZONTAL'; c.primaryAxisSizingMode = 'AUTO'; c.counterAxisSizingMode = 'AUTO'; c.fills = []; return c; }
20
+ function arrange(set, width) {
21
+ set.layoutMode = 'HORIZONTAL'; set.layoutWrap = 'WRAP'; set.itemSpacing = 24; set.counterAxisSpacing = 24;
22
+ set.paddingTop = set.paddingBottom = set.paddingLeft = set.paddingRight = 32;
23
+ set.primaryAxisSizingMode = 'FIXED'; set.counterAxisSizingMode = 'AUTO'; set.resize(width, set.height);
24
+ 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;
25
+ }
26
+ function textProp(set, label, def, layerName) {
27
+ var key = set.addComponentProperty(label, 'TEXT', def);
28
+ set.findAllWithCriteria({ types: ['TEXT'] }).forEach(function (t) { if (t.name === layerName) t.componentPropertyReferences = Object.assign({}, t.componentPropertyReferences, { characters: key }); });
29
+ return key;
30
+ }
31
+ function boolProp(set, label, def, layerName) {
32
+ var key = set.addComponentProperty(label, 'BOOLEAN', def);
33
+ set.findAll(function (n) { return n.name === layerName; }).forEach(function (n) { n.componentPropertyReferences = Object.assign({}, n.componentPropertyReferences, { visible: key }); });
34
+ return key;
35
+ }
36
+ // Where a rebuilt set goes back: removeOld notes the slot of the set (or its labelled wrapper) it
37
+ // removes, so place puts the new one in the same spot instead of at the end.
38
+ var slot = null;
39
+ function place(set) {
40
+ var col = figma.currentPage.children.filter(function (n) { return n.name === 'Components'; })[0];
41
+ if (!col) { col = AL('VERTICAL', { name: 'Components', itemSpacing: 80 }); col.x = 1600; col.y = 400; }
42
+ if (slot && slot.name === set.name && slot.index <= col.children.length) col.insertChild(slot.index, set); else col.appendChild(set);
43
+ slot = null;
44
+ return col;
45
+ }
46
+ function isWrap(n) { return n.type === 'FRAME' && n.getSharedPluginData('loomiere', 'wrap') === '1'; }
47
+ function removeOld(name) {
48
+ figma.currentPage.findAll(function (n) { return n.name === name && (n.type === 'COMPONENT_SET' || isWrap(n)); }).forEach(function (n) {
49
+ if (n.removed) return;
50
+ if (n.parent && n.parent.name === 'Components') slot = { name: name, index: n.parent.children.indexOf(n) };
51
+ n.remove();
52
+ });
53
+ }
54
+ // A header and side labels for each bare set in the page's Components column: the set's name and
55
+ // a line from desc (or its description), its column values above and row values to the left.
56
+ // Sets already wrapped keep their labels; run it last on a page, after every set is built.
57
+ async function labelSets(desc) {
58
+ desc = desc || {};
59
+ var comp = figma.currentPage.children.filter(function (n) { return n.name === 'Components'; })[0];
60
+ function bucket(vals, tol) { var out = []; vals.sort(function (a, b) { return a - b; }).forEach(function (v) { if (!out.length || v - out[out.length - 1][0] > tol) out.push([v]); else out[out.length - 1].push(v); }); return out.map(function (b) { return b[0]; }); }
61
+ function common(nodes) { var keys = Object.keys(nodes[0].variantProperties); return keys.filter(function (k) { return nodes.every(function (n) { return n.variantProperties[k] === nodes[0].variantProperties[k]; }); }).map(function (k) { return nodes[0].variantProperties[k]; }); }
62
+ var sets = comp.children.filter(function (c) { return c.type === 'COMPONENT_SET'; });
63
+ comp.itemSpacing = 120;
64
+ var LW = 120;
65
+ for (var i = 0; i < sets.length; i++) {
66
+ var s = sets[i], kids = s.children;
67
+ var w = AL('VERTICAL', { name: s.name, itemSpacing: 16 }); w.setSharedPluginData('loomiere', 'wrap', '1');
68
+ comp.insertChild(comp.children.indexOf(s), w);
69
+ var head = AL('VERTICAL', { name: 'Header', itemSpacing: 4 });
70
+ add(head, [await T(s.name, 'title-amplify'), await T(desc[s.name] || s.description || '', 'body-base', 'Theme:text/secondary', 720)]);
71
+ add(w, [head]);
72
+ var xs = bucket(kids.map(function (k) { return Math.round(k.x); }), 8), ys = bucket(kids.map(function (k) { return Math.round(k.y); }), 12);
73
+ var top = AL('HORIZONTAL', { name: 'Column labels row' });
74
+ var spacer = figma.createFrame(); spacer.name = 'Corner'; spacer.fills = []; spacer.resize(LW, 20);
75
+ var cl = figma.createFrame(); cl.name = 'Column labels'; cl.fills = []; cl.clipsContent = false; cl.resize(s.width, 20);
76
+ add(top, [spacer, cl]); add(w, [top]);
77
+ if (xs.length > 1) for (var c = 0; c < xs.length; c++) {
78
+ var col = kids.filter(function (k) { return Math.abs(Math.round(k.x) - xs[c]) <= 8; });
79
+ var t = await T(common(col).join(' · ').toUpperCase(), 'eyebrow-condense', 'Theme:text/tertiary'); cl.appendChild(t); t.x = xs[c]; t.y = 0;
80
+ }
81
+ var body = AL('HORIZONTAL', { name: 'Body' });
82
+ var rl = figma.createFrame(); rl.name = 'Row labels'; rl.fills = []; rl.clipsContent = false; rl.resize(LW, s.height);
83
+ add(body, [rl]); body.appendChild(s); add(w, [body]);
84
+ if (ys.length > 1) for (var r = 0; r < ys.length; r++) {
85
+ var row = kids.filter(function (k) { return Math.abs(Math.round(k.y) - ys[r]) <= 12; });
86
+ var mid = row.reduce(function (a, k) { return a + k.y + k.height / 2; }, 0) / row.length;
87
+ var t2 = await T(common(row).join(' · '), 'subtitle-condense', 'Theme:text/secondary', LW - 16); rl.appendChild(t2); t2.x = 0; t2.y = Math.round(mid - t2.height / 2);
88
+ }
89
+ }
90
+ comp.clipsContent = false;
91
+ comp.findAll(function (n) { return n.type === 'FRAME' && (isWrap(n) || ['Header', 'Column labels row', 'Body', 'Corner'].indexOf(n.name) !== -1); }).forEach(function (n) { n.clipsContent = false; });
92
+ return sets.map(function (s) { return s.name; });
93
+ }
94
+
95
+ return Object.assign({}, L, { shadow: shadow, focusRing: focusRing, icon: icon, comp: comp, arrange: arrange, textProp: textProp, boolProp: boolProp, place: place, removeOld: removeOld, labelSets: labelSets });