figma-plugin-utilities 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -4
- package/README.md +168 -35
- package/package.json +40 -10
- package/src/components/CodeExportModal.svelte +98 -0
- package/src/components/ConfirmModal.svelte +66 -0
- package/src/components/DataTable.svelte +349 -0
- package/src/components/EmptyState.svelte +1 -1
- package/src/components/FieldGrid.svelte +18 -0
- package/src/components/Footer.svelte +10 -0
- package/src/components/Header.svelte +6 -1
- package/src/components/LadderBadges.svelte +35 -0
- package/src/components/ListItem.svelte +53 -28
- package/src/components/MappingChip.svelte +150 -0
- package/src/components/RampCurve.svelte +383 -0
- package/src/components/Section.svelte +49 -0
- package/src/components/StatusBar.svelte +6 -1
- package/src/components/SteppedField.svelte +52 -0
- package/src/components/index.js +9 -0
- package/src/index.js +19 -3
- package/src/lib/colors.ts +99 -0
- package/src/lib/confirm.ts +55 -0
- package/src/lib/errorHandling.js +0 -39
- package/src/lib/figma-frame-builders.ts +467 -0
- package/src/lib/figma-helpers.ts +103 -59
- package/src/lib/figma-variables.ts +151 -0
- package/src/lib/format.ts +19 -0
- package/src/lib/index.js +13 -4
- package/src/lib/scale.ts +147 -0
- package/src/lib/colors.js +0 -80
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
## [0.5.0] - 2026-10-07
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
### Added
|
|
8
|
+
- **ConfirmModal** with `confirmAction`, from Data Mapper — a confirmation in a small modal in place of the browser's `confirm()`, resolving true or false, its buttons stacked at full width in a window too narrow for them side by side — and `confirmDiscardChanges`, the "Discard changes?" prompt before unsaved edits go, for Modal's `beforeClose`
|
|
9
|
+
- **MappingChip** — one side of a source → target row as a filled 24px chip: a button with a lead icon or chit, a truncating `label`, an optional `preview` and `count`. `tone` is `default`, `secondary` or `component`; `selected` draws the selection border
|
|
10
|
+
- **FieldGrid** — fields side by side in `columns` equal columns (2 by default) that shrink below their content
|
|
11
|
+
- **SteppedField** — a field with − and + icon buttons after it, named by `downLabel` and `upLabel`, firing `step` with -1 or 1
|
|
12
|
+
- **LadderBadges** — a scale's sizes as badges, outlined where used and `archived` where not, each with a tooltip
|
|
13
|
+
- **RampCurve** — a ramp's quadratic Bézier at one breakpoint, the others faint behind it. At the smallest and largest breakpoint the ends and the bend are draggable or stepped with the arrow keys. Fires `change` and `select`
|
|
14
|
+
- **CodeExportModal** — read-only code in a modal with a copy button that reads "Copied" for two seconds, and a `controls` slot for options above the code
|
|
15
|
+
- **Section** — a titled group of fields as in Figma's panels, with icon buttons in an `actions` slot. It pads its own content and shrinks with its container
|
|
16
|
+
- **DataTable** — named rows with a cell per column. Selectable rows open an `editor` slot; with `selectable={false}` it's a read-only table. Rows take badges with tooltips, and cells are badges, variable chips or text, colored as new, changed or danger
|
|
17
|
+
- `lib/scale` — responsive scale math with no Figma API: the ramp's Bézier (`bezier`, `clampPosition`, `levelT`), `blendAt`, `valueAtWidth`, fallback breakpoint widths (`FALLBACK_WIDTHS`, `isBreakpointName`, `widthsFor`), `fluidClamp`, `trimNumber`, `lerp` and `roundHalfDown`
|
|
18
|
+
- `lib/figma-variables` — `isVariableAlias`, `isColorValue`, `toRgba`, `getVariableLookup`, and `resolveVariableValue` and `resolveVariableValueAsync`, which follow aliases at a mode; the async one follows library variables too
|
|
19
|
+
- **figma-helpers** — `fontsOf`, `loadFontOnce`, `loadNodeFonts` and `setText` write text in the layer's own fonts, loading each font once per run; `createSettingsStore` keeps settings in clientStorage, cleaned by one `sanitize` on load and on save
|
|
20
|
+
- `isValidHex` — six HEX digits in either case, the "#" required unless `requireHash` is false
|
|
21
|
+
- `plural` and `joinList` — "3 layers", "a, b and c"; from the root, or `lib/format` in code.ts
|
|
22
|
+
- **figma-helpers** — `showNotice`, a regular notification for a run with nothing to do, where `showError` is for failures
|
|
23
|
+
- `UNDO` — "Press Ctrl/Cmd+Z to undo.", the last sentence of a success notification for a change to the file
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- **ListItem** — an `actions` slot puts buttons inside the item, after its text and outside its clickable area, in the Figma component too (**Actions slot**)
|
|
27
|
+
- **Header** — `level` sets the title's heading level (1 by default). The left padding is 8px when the left slot has content and 16px before a title alone, in the Figma component too
|
|
28
|
+
- **Footer** — a kit `Text` at the edge of a split footer sits 16px in, where buttons sit 8px in
|
|
29
|
+
- `rgbToHex` takes `lowercase`, `hash` and `alpha` options and clamps channels to 0–1
|
|
30
|
+
- The color utilities are TypeScript, with their own `lib/colors` entry for code.ts
|
|
31
|
+
- `loadFont` loads each font once per run
|
|
32
|
+
- `showError` and `showSuccess` stay up long enough to read by default, about 60ms a character, and at least 5s and 3s
|
|
33
|
+
- **figma-frame-builders** — `createTokenChip` pads its label 6px on each side, and `specTokens.accentColors.green` is #40C459, as in the Vitrine spec library
|
|
34
|
+
|
|
35
|
+
### Removed
|
|
36
|
+
- `getCollections`, `getVariables` and `getSelection` — call the Figma API directly
|
|
37
|
+
- `saveToStorage` and `loadFromStorage` — use `createSettingsStore`
|
|
38
|
+
- `notifyError`, `notifySuccess` and `notifyWarning`, which did nothing in the UI — use `showError` and `showSuccess` in code.ts
|
|
39
|
+
|
|
40
|
+
## [0.4.0] - 2026-09-21
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- `figma-frame-builders.ts` — new module with Figma frame and component builder utilities, imported from `figma-plugin-utilities/lib/figma-frame-builders` (its own export entry). They mirror the Vitrine spec library — colors, typography, spacing and layer names (`label` chip and text, a `tokens` row in token cells, `title` in both header variants):
|
|
44
|
+
- `createAutoLayoutFrame` — creates a `FrameNode` with auto-layout configured
|
|
45
|
+
- `createAutoLayoutComponent` — creates a `ComponentNode` with auto-layout configured
|
|
46
|
+
- `createText` — creates a styled `TextNode`
|
|
47
|
+
- `createTokenChip` — creates a rounded chip frame for displaying color tokens
|
|
48
|
+
- `createColorSwatch` — creates a color swatch frame
|
|
49
|
+
- `createTableCell` — creates a table cell frame
|
|
50
|
+
- `createTableHeader` — creates a table header frame
|
|
51
|
+
- `loadSpecFonts` — loads Inter and IBM Plex Mono font faces in parallel
|
|
52
|
+
- `specTokens` — design token constants (accent colors, font specs, light/dark themes with an optional `headerBorder`)
|
|
53
|
+
- `PaddingSpec`, `SpecTheme`, `NodeKind` and `NodeFor` types
|
|
54
|
+
|
|
55
|
+
### Removed
|
|
56
|
+
- The dev-mode `console.warn` **FieldGroup** logged when `label` was set without `labelFor` — a `Dropdown` is a button and cannot be a `<label for>` target, so it fired on correct code. Dropped in the a11y pass, recorded late
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
- **StatusBar** — the default `info` type sets `color: var(--figma-color-text)`. The `error`, `success` and `warning` types each set a foreground; the default one relied on inheritance, and nothing up the tree sets `color`, so the message rendered in the UA's black on the dark theme's grey bar
|
|
60
|
+
- **EmptyState** — the actions are a keyed `{#each}`, so swapping one action for another reuses the right button rather than repainting the row
|
|
61
|
+
- **docs** — `figma-frame-builders` is documented, `sanitizeInput` no longer claims to escape HTML (it stringifies, truncates, strips control characters and trims), and `formatErrorMessage`, `handleAsyncError`, `withErrorHandling` and `logError` are documented with their real signatures. `withErrorHandling(fn, operation)` calls `fn()` with no arguments and returns its result; it was documented as returning a wrapped function
|
|
9
62
|
|
|
10
63
|
## [0.3.1] - 2026-05-13
|
|
11
64
|
|
package/README.md
CHANGED
|
@@ -24,6 +24,14 @@ import {
|
|
|
24
24
|
LoadingState,
|
|
25
25
|
FieldGroup,
|
|
26
26
|
CheckboxCard,
|
|
27
|
+
Section,
|
|
28
|
+
DataTable,
|
|
29
|
+
FieldGrid,
|
|
30
|
+
SteppedField,
|
|
31
|
+
LadderBadges,
|
|
32
|
+
CodeExportModal,
|
|
33
|
+
RampCurve,
|
|
34
|
+
MappingChip,
|
|
27
35
|
// Messages
|
|
28
36
|
sendToPlugin,
|
|
29
37
|
createMessageHandler,
|
|
@@ -44,9 +52,6 @@ import {
|
|
|
44
52
|
// Error handling
|
|
45
53
|
safeAsync,
|
|
46
54
|
parseJsonSafe,
|
|
47
|
-
notifyError,
|
|
48
|
-
notifySuccess,
|
|
49
|
-
notifyWarning,
|
|
50
55
|
// Resize
|
|
51
56
|
setDefaultWidth,
|
|
52
57
|
getContentHeight,
|
|
@@ -73,11 +78,21 @@ import { sendToPlugin, createMessageHandler } from "figma-plugin-utilities/lib";
|
|
|
73
78
|
| `Header` | Header bar with `left`, `center`, `right` slots and optional title |
|
|
74
79
|
| `Footer` | Footer with `right`, `split`, and `full` layout variants |
|
|
75
80
|
| `StatusBar` | Toast notifications with auto-dismiss (info/success/error/warning) |
|
|
76
|
-
| `EmptyState` | Empty/error states with optional icon and action buttons |
|
|
77
|
-
| `ListItem` | Selectable list items with metadata
|
|
78
|
-
| `LoadingState` |
|
|
79
|
-
| `FieldGroup` | Label + input wrapper
|
|
80
|
-
| `CheckboxCard` | Large checkbox with card styling and better touch targets |
|
|
81
|
+
| `EmptyState` | Empty/error states with optional icon and action buttons; `size`, `centered`, and `role="alert"` for failures |
|
|
82
|
+
| `ListItem` | Selectable list items with metadata and `badge` slots, an action menu (`menuOpen`, `menuToggle`, `menuClose`) |
|
|
83
|
+
| `LoadingState` | Centred message as `role="status"` (text only, no spinner) |
|
|
84
|
+
| `FieldGroup` | Label + input wrapper; `labelFor` binds the label to a text control |
|
|
85
|
+
| `CheckboxCard` | Large checkbox with card styling and better touch targets; `change` event |
|
|
86
|
+
| `Section` | Titled group of fields as in Figma's panels: a `Header` with the title and an `actions` slot, content padded by the section itself |
|
|
87
|
+
| `DataTable` | Named rows with a cell per column (a set at each breakpoint, a style before and after): columns with their own width and alignment, a read-only mode with table roles, row selection with an `editor` slot, notes as badges, with tooltips, and a `+N` count past two, an `action` slot, values as badges or variable chips, removed rows and a marked column |
|
|
88
|
+
| `FieldGrid` | Fields side by side in equal columns (`columns`, default 2) that shrink below their content |
|
|
89
|
+
| `SteppedField` | A field with − and + icon buttons after it, as one grid cell; `step` event with -1 or 1 |
|
|
90
|
+
| `LadderBadges` | A scale's sizes as badges, outlined where used and archived where not, each with the caller's tooltip |
|
|
91
|
+
| `RampCurve` | A ramp's Bézier at the breakpoint shown, in the caller's units: handles for the ends and the bend at the smallest and largest breakpoint, blends between; `change` and `select` events |
|
|
92
|
+
| `MappingChip` | One side of a source → target row: a filled 24px chip with a lead icon or chit, a truncating label, a `preview` after a dot and a trailing `count`; `click` event |
|
|
93
|
+
| `CodeExportModal` | Read-only code in a modal with a copy button that reads "Copied" for 2s; a `controls` slot above the code |
|
|
94
|
+
|
|
95
|
+
Every component also takes a `class` (or `className`) prop.
|
|
81
96
|
|
|
82
97
|
### Header
|
|
83
98
|
|
|
@@ -193,6 +208,103 @@ Large checkbox with card-style background and better touch targets.
|
|
|
193
208
|
</CheckboxCard>
|
|
194
209
|
```
|
|
195
210
|
|
|
211
|
+
### FieldGrid
|
|
212
|
+
|
|
213
|
+
```svelte
|
|
214
|
+
<FieldGrid columns={3}>
|
|
215
|
+
<FieldGroup label="Base">…</FieldGroup>
|
|
216
|
+
<FieldGroup label="Ratio">…</FieldGroup>
|
|
217
|
+
<FieldGroup label="Steps">…</FieldGroup>
|
|
218
|
+
</FieldGrid>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### SteppedField
|
|
222
|
+
|
|
223
|
+
```svelte
|
|
224
|
+
<SteppedField
|
|
225
|
+
downLabel="Step {set.name} down"
|
|
226
|
+
upLabel="Step {set.name} up"
|
|
227
|
+
on:step={(e) => setOffset(set.offset + e.detail)}
|
|
228
|
+
>
|
|
229
|
+
<FieldGroup label="Steps off the curve" labelFor="offset" size="small">
|
|
230
|
+
<NumericInput id="offset" value={set.offset} precision={0} />
|
|
231
|
+
</FieldGroup>
|
|
232
|
+
</SteppedField>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### LadderBadges
|
|
236
|
+
|
|
237
|
+
```svelte
|
|
238
|
+
<LadderBadges
|
|
239
|
+
ariaLabel="Ladder sizes"
|
|
240
|
+
badges={ladder.map((value, i) => ({
|
|
241
|
+
value,
|
|
242
|
+
used: used.has(i),
|
|
243
|
+
title: used.has(i) ? "Used by a style" : "Unused",
|
|
244
|
+
}))}
|
|
245
|
+
/>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Each badge's accessible name is `"{value}px, used"` or `"{value}px, unused"`.
|
|
249
|
+
|
|
250
|
+
### MappingChip
|
|
251
|
+
|
|
252
|
+
```svelte
|
|
253
|
+
<MappingChip
|
|
254
|
+
label={row.sourceName}
|
|
255
|
+
count={row.uses}
|
|
256
|
+
tone="secondary"
|
|
257
|
+
title="Select these icons"
|
|
258
|
+
on:click={() => reveal(row)}
|
|
259
|
+
/>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`iconName` or `chit` (which wins) adds a lead that hangs into the padding; `preview` follows the label as "label · preview"; `tone` is `default`, `secondary` or `component`; `selected` draws the selection border. The `lead` slot takes a marker ahead of the lead, and `element` binds the button.
|
|
263
|
+
|
|
264
|
+
### RampCurve
|
|
265
|
+
|
|
266
|
+
```svelte
|
|
267
|
+
<RampCurve
|
|
268
|
+
{ramp}
|
|
269
|
+
curves={breakpoints.map((_, b) => rampAt(ramp, b))}
|
|
270
|
+
span={[lo, hi]}
|
|
271
|
+
grid={ladder.map((size, rung) => ({ key: rung, y: size, label: size }))}
|
|
272
|
+
dots={levels.map((size, k) => ({ key: k, x: k / (levels.length - 1), y: size }))}
|
|
273
|
+
{breakpoints}
|
|
274
|
+
{selected}
|
|
275
|
+
rungCount={ladder.length}
|
|
276
|
+
rungAt={(px) => nearestRung(ladder, px)}
|
|
277
|
+
format={(px) => `${Math.round(px)}px`}
|
|
278
|
+
ariaLabel="Heading ramp"
|
|
279
|
+
bottomLabel="Smallest level"
|
|
280
|
+
topLabel="Largest level"
|
|
281
|
+
along="levels"
|
|
282
|
+
on:change={(e) => (ramp = { ...ramp, ...e.detail })}
|
|
283
|
+
on:select={(e) => (selected = e.detail)}
|
|
284
|
+
/>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`ramp` holds the ends as rungs and the bends at each end (`bottomSm`, `topSm`, `bendSm`, `bottomLg`, `topLg`, `bendLg`) and `bendPosition`; `change` patches those keys. Everything else on the y axis is in the caller's units.
|
|
288
|
+
|
|
289
|
+
### CodeExportModal
|
|
290
|
+
|
|
291
|
+
```svelte
|
|
292
|
+
<CodeExportModal
|
|
293
|
+
isOpen={exportOpen}
|
|
294
|
+
title="Export CSS"
|
|
295
|
+
value={css}
|
|
296
|
+
ariaLabel="Exported CSS"
|
|
297
|
+
copyLabel="Copy CSS"
|
|
298
|
+
onClose={() => (exportOpen = false)}
|
|
299
|
+
>
|
|
300
|
+
<svelte:fragment slot="controls">
|
|
301
|
+
<SegmentedControl …/>
|
|
302
|
+
</svelte:fragment>
|
|
303
|
+
</CodeExportModal>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Copies with `execCommand`, since the plugin iframe isn't granted clipboard-write. `position` (default `"bottom"`), `width` (`"medium"`) and `height` (`"auto"`) pass through to `Modal`.
|
|
307
|
+
|
|
196
308
|
## Utilities
|
|
197
309
|
|
|
198
310
|
### Messages (`lib/messages.js`)
|
|
@@ -231,11 +343,14 @@ const jsonResult = validateJsonString('{"key": "value"}');
|
|
|
231
343
|
// { valid: true, parsed: {...} } or { valid: false, error: "..." }
|
|
232
344
|
|
|
233
345
|
validateEmail("user@example.com"); // { valid: true }
|
|
234
|
-
validateNumber("42", { min: 0, max: 100 }); // { valid: true, value: 42 }
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
346
|
+
validateNumber("42", { min: 0, max: 100, integer: true }); // { valid: true, value: 42 }
|
|
347
|
+
validateUrl("", { required: false }); // { valid: true } — empty is allowed
|
|
348
|
+
validateJsonString(text, { maxSizeKB: 512, requireObject: true });
|
|
349
|
+
|
|
350
|
+
const clean = sanitizeName("My Plugin!!!", 200); // "My Plugin" — "Untitled" if nothing survives
|
|
351
|
+
sanitizeInput(input, 50); // stringify, truncate to maxLength, strip control characters, trim
|
|
352
|
+
// Note: sanitizeInput does NOT escape HTML. Escape at the point of rendering instead.
|
|
353
|
+
isEmpty(""); // true — also for [] and {}
|
|
239
354
|
```
|
|
240
355
|
|
|
241
356
|
### Error Handling (`lib/errorHandling.js`)
|
|
@@ -255,11 +370,6 @@ if (result.ok) {
|
|
|
255
370
|
// Parse JSON safely
|
|
256
371
|
const parsed = parseJsonSafe(jsonString);
|
|
257
372
|
// { ok: true, value: {...} } or { ok: false, error: "..." }
|
|
258
|
-
|
|
259
|
-
// Figma notifications
|
|
260
|
-
notifySuccess("Done!");
|
|
261
|
-
notifyError("Something went wrong");
|
|
262
|
-
notifyWarning("Check your input");
|
|
263
373
|
```
|
|
264
374
|
|
|
265
375
|
### Resize (`lib/resize.js`)
|
|
@@ -287,6 +397,34 @@ setDefaultWidth(320);
|
|
|
287
397
|
|
|
288
398
|
> **Note:** The `container` element passed to `autoResize` must **not** have `height: 100%` or a fixed height — it should flow naturally with its content so `scrollHeight` can be measured accurately.
|
|
289
399
|
|
|
400
|
+
### Spec Frame Builders (`lib/figma-frame-builders.ts`)
|
|
401
|
+
|
|
402
|
+
Typed builders for canvas frames in a spec or documentation generator — auto-layout frames and components, text, token chips, color swatches, table cells and headers, with light and dark palettes.
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
import {
|
|
406
|
+
specTokens, loadSpecFonts,
|
|
407
|
+
createAutoLayoutFrame, createAutoLayoutComponent, createText,
|
|
408
|
+
createTokenChip, createColorSwatch, createTableCell, createTableHeader,
|
|
409
|
+
} from "figma-plugin-utilities/lib/figma-frame-builders";
|
|
410
|
+
|
|
411
|
+
await loadSpecFonts(); // once, before drawing
|
|
412
|
+
const theme = specTokens.themes.dark;
|
|
413
|
+
|
|
414
|
+
const row = createAutoLayoutFrame({ name: "row", direction: "HORIZONTAL", spacing: 8, fill: theme.cellFill });
|
|
415
|
+
row.appendChild(createTokenChip({ label: "#FFFFFF", background: theme.chipBg, textColor: theme.text }));
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`specTokens` carries `accentColors`, `fonts` and `themes` (`light`, `dark`). Builders that can return either node take `as: "component"` for a `ComponentNode` instead of a `FrameNode`. Exported types: `PaddingSpec`, `SpecTheme`, `NodeKind`, `NodeFor`.
|
|
419
|
+
|
|
420
|
+
### Scale Math (`lib/scale.ts`)
|
|
421
|
+
|
|
422
|
+
Pure math for scales shaped across breakpoints — the ramp's quadratic Bézier (`bezier`, `clampPosition`, `levelT`), blending by viewport width (`blendAt`), values between breakpoints (`valueAtWidth`), fallback widths (`FALLBACK_WIDTHS`, `isBreakpointName`, `widthsFor`) and fluid CSS (`fluidClamp`, `trimNumber`), plus `lerp` and `roundHalfDown`. No Figma API: both threads may import it, provided the plugin builds each thread separately.
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
import { widthsFor, blendAt, fluidClamp } from "figma-plugin-utilities/lib/scale";
|
|
426
|
+
```
|
|
427
|
+
|
|
290
428
|
### Figma Helpers (`lib/figma-helpers.ts`)
|
|
291
429
|
|
|
292
430
|
For use in `code.ts`:
|
|
@@ -296,13 +434,10 @@ import {
|
|
|
296
434
|
sendToUI,
|
|
297
435
|
showError,
|
|
298
436
|
showSuccess,
|
|
299
|
-
getCollections,
|
|
300
|
-
getVariables,
|
|
301
|
-
getSelection,
|
|
302
437
|
focusNodes,
|
|
303
438
|
loadFont,
|
|
304
|
-
|
|
305
|
-
|
|
439
|
+
setText,
|
|
440
|
+
createSettingsStore,
|
|
306
441
|
handleResize,
|
|
307
442
|
} from "figma-plugin-utilities/lib/figma-helpers";
|
|
308
443
|
|
|
@@ -313,23 +448,21 @@ sendToUI("success", { message: "Done!" });
|
|
|
313
448
|
showError("Something went wrong");
|
|
314
449
|
showSuccess("Created!");
|
|
315
450
|
|
|
316
|
-
// Variables
|
|
317
|
-
const collections = await getCollections();
|
|
318
|
-
const colorVars = await getVariables("COLOR");
|
|
319
|
-
|
|
320
|
-
// Selection
|
|
321
|
-
const selected = getSelection(); // all selected nodes
|
|
322
|
-
const frames = getSelection("FRAME"); // filtered by type
|
|
323
|
-
|
|
324
451
|
// Focus viewport on nodes
|
|
325
452
|
focusNodes(figma.currentPage.selection);
|
|
326
453
|
|
|
327
|
-
// Load font before using
|
|
454
|
+
// Load a font before using it (once per run)
|
|
328
455
|
await loadFont("Inter", "Regular");
|
|
329
456
|
|
|
330
|
-
//
|
|
331
|
-
await
|
|
332
|
-
|
|
457
|
+
// Set a text layer's characters in its own fonts
|
|
458
|
+
await setText(textNode, "Hello");
|
|
459
|
+
|
|
460
|
+
// Settings in client storage, cleaned on load and on save
|
|
461
|
+
const store = createSettingsStore("settings", (raw) => ({
|
|
462
|
+
theme: (raw as { theme?: string })?.theme === "dark" ? "dark" : "light",
|
|
463
|
+
}));
|
|
464
|
+
const settings = await store.load();
|
|
465
|
+
await store.save({ ...settings, theme: "dark" });
|
|
333
466
|
|
|
334
467
|
// Handle resize message from UI (call in your message handler)
|
|
335
468
|
if (msg.type === "resize") handleResize(msg);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-plugin-utilities",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Shared Svelte components and utilities for Figma plugins",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./src/index.js",
|
|
@@ -28,6 +28,36 @@
|
|
|
28
28
|
"types": "./src/lib/figma-helpers.ts",
|
|
29
29
|
"import": "./src/lib/figma-helpers.ts",
|
|
30
30
|
"default": "./src/lib/figma-helpers.ts"
|
|
31
|
+
},
|
|
32
|
+
"./lib/figma-frame-builders": {
|
|
33
|
+
"types": "./src/lib/figma-frame-builders.ts",
|
|
34
|
+
"import": "./src/lib/figma-frame-builders.ts",
|
|
35
|
+
"default": "./src/lib/figma-frame-builders.ts"
|
|
36
|
+
},
|
|
37
|
+
"./lib/figma-variables": {
|
|
38
|
+
"types": "./src/lib/figma-variables.ts",
|
|
39
|
+
"import": "./src/lib/figma-variables.ts",
|
|
40
|
+
"default": "./src/lib/figma-variables.ts"
|
|
41
|
+
},
|
|
42
|
+
"./lib/colors": {
|
|
43
|
+
"types": "./src/lib/colors.ts",
|
|
44
|
+
"import": "./src/lib/colors.ts",
|
|
45
|
+
"default": "./src/lib/colors.ts"
|
|
46
|
+
},
|
|
47
|
+
"./lib/scale": {
|
|
48
|
+
"types": "./src/lib/scale.ts",
|
|
49
|
+
"import": "./src/lib/scale.ts",
|
|
50
|
+
"default": "./src/lib/scale.ts"
|
|
51
|
+
},
|
|
52
|
+
"./lib/format": {
|
|
53
|
+
"types": "./src/lib/format.ts",
|
|
54
|
+
"import": "./src/lib/format.ts",
|
|
55
|
+
"default": "./src/lib/format.ts"
|
|
56
|
+
},
|
|
57
|
+
"./lib/confirm": {
|
|
58
|
+
"types": "./src/lib/confirm.ts",
|
|
59
|
+
"import": "./src/lib/confirm.ts",
|
|
60
|
+
"default": "./src/lib/confirm.ts"
|
|
31
61
|
}
|
|
32
62
|
},
|
|
33
63
|
"files": [
|
|
@@ -55,20 +85,20 @@
|
|
|
55
85
|
"author": "Marius Roosendaal",
|
|
56
86
|
"license": "MIT",
|
|
57
87
|
"devDependencies": {
|
|
58
|
-
"@eslint/js": "^9.39.
|
|
59
|
-
"@figma/plugin-typings": "^1.
|
|
88
|
+
"@eslint/js": "^9.39.2",
|
|
89
|
+
"@figma/plugin-typings": "^1.138.0",
|
|
60
90
|
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
|
61
91
|
"@types/node": "^22.13.4",
|
|
62
|
-
"eslint": "^9.39.
|
|
63
|
-
"eslint-plugin-svelte": "^
|
|
64
|
-
"figma-ui3-kit-svelte": "
|
|
65
|
-
"globals": "^
|
|
66
|
-
"prettier": "3.
|
|
92
|
+
"eslint": "^9.39.2",
|
|
93
|
+
"eslint-plugin-svelte": "^3.23.0",
|
|
94
|
+
"figma-ui3-kit-svelte": "^0.6.0",
|
|
95
|
+
"globals": "^17.12.0",
|
|
96
|
+
"prettier": "^3.9.8",
|
|
67
97
|
"prettier-plugin-svelte": "^3.4.0",
|
|
68
98
|
"svelte": "^4.2.20",
|
|
69
|
-
"svelte-eslint-parser": "^
|
|
99
|
+
"svelte-eslint-parser": "^1.8.1",
|
|
70
100
|
"typescript": "^5.9.3",
|
|
71
|
-
"typescript-eslint": "^8.
|
|
101
|
+
"typescript-eslint": "^8.70.0",
|
|
72
102
|
"vite": "^5.2.0"
|
|
73
103
|
},
|
|
74
104
|
"scripts": {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Read-only code in a modal, with a button that copies it. Each modal keeps
|
|
3
|
+
its own "Copied" state, so copying one never marks the other.
|
|
4
|
+
-->
|
|
5
|
+
<script>
|
|
6
|
+
import { onDestroy } from "svelte";
|
|
7
|
+
import { Button, Modal, Textarea } from "figma-ui3-kit-svelte";
|
|
8
|
+
|
|
9
|
+
export let isOpen = false;
|
|
10
|
+
export let title;
|
|
11
|
+
export let value = "";
|
|
12
|
+
/** Names the code for assistive tech, e.g. "Exported token JSON". */
|
|
13
|
+
export let ariaLabel;
|
|
14
|
+
/** The copy button's label, e.g. "Copy JSON". */
|
|
15
|
+
export let copyLabel;
|
|
16
|
+
export let position = "bottom";
|
|
17
|
+
export let width = "medium";
|
|
18
|
+
export let height = "auto";
|
|
19
|
+
export let onClose = null;
|
|
20
|
+
|
|
21
|
+
let copied = false;
|
|
22
|
+
let copyTimer = null;
|
|
23
|
+
onDestroy(() => clearTimeout(copyTimer));
|
|
24
|
+
|
|
25
|
+
// Reopened, the button reads as it would before a copy.
|
|
26
|
+
$: if (isOpen) copied = false;
|
|
27
|
+
|
|
28
|
+
// execCommand, not the Clipboard API: the plugin iframe isn't granted
|
|
29
|
+
// clipboard-write.
|
|
30
|
+
function handleCopy() {
|
|
31
|
+
try {
|
|
32
|
+
const prevActive = document.activeElement;
|
|
33
|
+
const ta = document.createElement("textarea");
|
|
34
|
+
ta.value = value;
|
|
35
|
+
ta.style.position = "fixed";
|
|
36
|
+
ta.style.left = "-999999px";
|
|
37
|
+
ta.style.top = "-999999px";
|
|
38
|
+
document.body.appendChild(ta);
|
|
39
|
+
ta.focus();
|
|
40
|
+
ta.select();
|
|
41
|
+
const ok = document.execCommand("copy");
|
|
42
|
+
ta.remove();
|
|
43
|
+
if (prevActive instanceof HTMLElement) prevActive.focus();
|
|
44
|
+
if (ok) {
|
|
45
|
+
copied = true;
|
|
46
|
+
clearTimeout(copyTimer);
|
|
47
|
+
copyTimer = setTimeout(() => (copied = false), 2000);
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// execCommand unavailable — nothing to do.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<Modal
|
|
56
|
+
{isOpen}
|
|
57
|
+
{title}
|
|
58
|
+
{position}
|
|
59
|
+
{width}
|
|
60
|
+
{height}
|
|
61
|
+
overlayPadding="0px"
|
|
62
|
+
{onClose}
|
|
63
|
+
>
|
|
64
|
+
<div class="export-content">
|
|
65
|
+
<!-- Options for what's exported, above the code. -->
|
|
66
|
+
<slot name="controls" />
|
|
67
|
+
<Textarea {value} readonly {ariaLabel} variant="code" />
|
|
68
|
+
</div>
|
|
69
|
+
<svelte:fragment slot="footer-right">
|
|
70
|
+
<Button variant="primary" on:click={handleCopy}>
|
|
71
|
+
{copied ? "Copied" : copyLabel}
|
|
72
|
+
</Button>
|
|
73
|
+
</svelte:fragment>
|
|
74
|
+
</Modal>
|
|
75
|
+
|
|
76
|
+
<style>
|
|
77
|
+
.export-content {
|
|
78
|
+
flex: 1;
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
gap: var(--size-xsmall);
|
|
82
|
+
min-height: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.export-content :global(.textarea) {
|
|
86
|
+
flex: 1;
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
min-height: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.export-content :global(textarea) {
|
|
93
|
+
flex: 1;
|
|
94
|
+
min-height: 0;
|
|
95
|
+
resize: none;
|
|
96
|
+
white-space: pre;
|
|
97
|
+
}
|
|
98
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The dialog confirmAction() opens: mount one in the plugin's UI, after
|
|
3
|
+
everything else, so it shows above any modal that asks.
|
|
4
|
+
-->
|
|
5
|
+
<script>
|
|
6
|
+
import { Modal, Button, Text } from "figma-ui3-kit-svelte";
|
|
7
|
+
import { confirmRequest, answerConfirm } from "../lib/confirm";
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<Modal
|
|
11
|
+
isOpen={!!$confirmRequest}
|
|
12
|
+
title={$confirmRequest?.title ?? ""}
|
|
13
|
+
width="small"
|
|
14
|
+
position="center"
|
|
15
|
+
on:close={() => answerConfirm(false)}
|
|
16
|
+
>
|
|
17
|
+
<Text>{$confirmRequest?.message ?? ""}</Text>
|
|
18
|
+
|
|
19
|
+
<svelte:fragment slot="footer-full">
|
|
20
|
+
<div class="confirm-footer">
|
|
21
|
+
<div class="confirm-actions">
|
|
22
|
+
<Button variant="secondary" on:click={() => answerConfirm(false)}>
|
|
23
|
+
{$confirmRequest?.cancelLabel || "Cancel"}
|
|
24
|
+
</Button>
|
|
25
|
+
<Button
|
|
26
|
+
variant={$confirmRequest?.destructive ? "destructive" : "primary"}
|
|
27
|
+
on:click={() => answerConfirm(true)}
|
|
28
|
+
>
|
|
29
|
+
{$confirmRequest?.confirmLabel ?? ""}
|
|
30
|
+
</Button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</svelte:fragment>
|
|
34
|
+
</Modal>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
/* The kit's footer is 40px tall; this one grows with stacked buttons. */
|
|
38
|
+
:global(.modal-footer:has(.confirm-footer)) {
|
|
39
|
+
height: auto;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* The footer's width, which the buttons are laid out by. */
|
|
43
|
+
.confirm-footer {
|
|
44
|
+
container-type: inline-size;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.confirm-actions {
|
|
48
|
+
display: flex;
|
|
49
|
+
justify-content: flex-end;
|
|
50
|
+
gap: var(--size-xxsmall);
|
|
51
|
+
width: 100%;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* In a narrow window, such as a 240px plugin, the buttons don't fit side
|
|
55
|
+
by side: they stack at full width, the confirming one on top. */
|
|
56
|
+
@container (max-width: 215px) {
|
|
57
|
+
.confirm-actions {
|
|
58
|
+
flex-direction: column-reverse;
|
|
59
|
+
align-items: stretch;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.confirm-actions > :global(*) {
|
|
63
|
+
width: 100%;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
</style>
|