figma-plugin-utilities 0.5.1 → 1.0.0-rc.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 +51 -2
- package/README.md +61 -39
- package/package.json +58 -18
- package/src/components/CheckboxCard.svelte +38 -32
- package/src/components/CodeExportModal.svelte +42 -22
- package/src/components/ConfirmModal.svelte +6 -6
- package/src/components/DataTable.svelte +78 -74
- package/src/components/EmptyState.svelte +52 -29
- package/src/components/FieldGrid.svelte +6 -3
- package/src/components/FieldGroup.svelte +57 -11
- package/src/components/Footer.svelte +30 -13
- package/src/components/Header.svelte +30 -15
- package/src/components/LadderBadges.svelte +8 -5
- package/src/components/ListItem.svelte +73 -48
- package/src/components/LoadingState.svelte +7 -5
- package/src/components/MappingChip.svelte +50 -38
- package/src/components/PluginLayout.svelte +11 -4
- package/src/components/RampCurve.svelte +177 -114
- package/src/components/Section.svelte +14 -9
- package/src/components/StatusBar.svelte +38 -31
- package/src/components/SteppedField.svelte +18 -12
- package/src/components/data-table.ts +33 -0
- package/src/{index.js → index.ts} +10 -2
- package/src/lib/{errorHandling.js → errorHandling.ts} +50 -37
- package/src/lib/figma-helpers.ts +1 -1
- package/src/lib/{index.js → index.ts} +7 -7
- package/src/lib/messages.ts +104 -0
- package/src/lib/{resize.js → resize.ts} +48 -28
- package/src/lib/{validation.js → validation.ts} +33 -32
- package/src/vite.d.ts +13 -0
- package/src/vite.js +165 -0
- package/src/lib/messages.js +0 -36
- /package/src/components/{index.js → index.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Breaking
|
|
6
|
+
- Needs Svelte 5.35 or later: `svelte` is a peer dependency, `^5.35.0`, as the kit's is, and the optional `@sveltejs/vite-plugin-svelte` peer is `^4.0.0`. The components are Svelte 5 runes components with typed props, as the kit's are, so their events are callback props that get what was `event.detail`, and their named slots are snippets. See figma-ui3-kit-svelte's changelog for how a call changes
|
|
7
|
+
- Renamed, component by component:
|
|
8
|
+
|
|
9
|
+
| Component | Svelte 4 | Svelte 5 |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| CheckboxCard | `on:change`; slot `secondary` | `onchange({ checked })`; snippet `secondary` |
|
|
12
|
+
| CodeExportModal | `onClose`; slot `controls` | `onclose`; snippet `controls` |
|
|
13
|
+
| DataTable | `on:select`, `on:column`; slots `action` and `editor` with `let:row`, `note` | `onselect(row)`, `oncolumn(index)`; snippets `action(row)`, `editor(row)`, `note` |
|
|
14
|
+
| FieldGroup | slot `hint` | `hint` takes a snippet as well as a string |
|
|
15
|
+
| Footer | `className`; slots `left`, `right` | `class`, as the kit's components; snippets `left`, `right` |
|
|
16
|
+
| Header | `className`; slots `left`, `center`, `right` | `class`; snippets `left`, `center`, `right` |
|
|
17
|
+
| ListItem | `on:click`, `on:menuToggle`, `on:menuSelect`, `on:menuClose`; slots `badge`, `actions` | `onclick({ id })`, `onmenutoggle({ id, open })`, `onmenuselect({ id, action })`, `onmenuclose({ id })`; snippets `badge`, `actions` |
|
|
18
|
+
| PluginLayout | `className` | `class` |
|
|
19
|
+
| MappingChip | `on:click`; slot `lead` | `onclick(event)`, not while disabled; snippet `lead` |
|
|
20
|
+
| RampCurve | `on:change`, `on:select` | `onchange(patch)`, `onselect(index)` |
|
|
21
|
+
| Section | slot `actions` | snippet `actions` |
|
|
22
|
+
| StatusBar | `on:close` | `onclose()` |
|
|
23
|
+
| SteppedField | `on:step` | `onstep(step)`, -1 or 1 |
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- Types for plugins written in TypeScript: `import type { DataTableRow, DataTableColumn, DataTableCell } from "figma-plugin-utilities"`. The entry points are TypeScript, so the types reach a plugin that installs the package from npm. ListItem takes the type of its `menuItems`, so `onmenuselect` hands back the row's own `value` type
|
|
27
|
+
- Typed messages: with the messages typed as one union per direction, `const send: Send<ToPlugin> = sendToPlugin` (or `sendToUI` in `code.ts`) checks each message's name and data, and `createMessageHandler<ToUI>({ … })` hands each handler its own message. `Send`, `Handlers` and `Msg` come from `figma-plugin-utilities` and `figma-plugin-utilities/lib/messages`. Untyped calls work as before
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- `sendToPlugin` copies the arrays and plain objects in its data before posting them, so a message can hold Svelte 5 `$state`, a Proxy that postMessage can't clone
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- StatusBar: an error or warning that follows an info or success message within 4 seconds stays until it's dismissed, instead of closing on the earlier message's timer
|
|
34
|
+
- DataTable and EmptyState: two columns or actions with the same label no longer break the table or the buttons
|
|
35
|
+
|
|
36
|
+
## [0.6.0] - 2026-10-10
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- `figma-plugin-utilities/vite` — `figmaPluginConfig(import.meta.url)`, a plugin's whole Vite config: each thread built on its own (`vite build && vite build --mode code`), so both may import the same module, the UI's CSS and JS inlined into `dist/index.html`, `src/manifest.json` copied, at `es2017`. `moduleScript` inlines the UI as a module script. `ui3InlineSvg` and `inlineFigmaHtml` are exported too. `vite` and `@sveltejs/vite-plugin-svelte` are optional peer dependencies
|
|
40
|
+
- **EmptyState** — `iconSize`, the icon's size in px (24 by default), for an icon given as SVG markup
|
|
41
|
+
- **FieldGroup** — `hint`, a line of secondary text under the control: what to enter, or what the choice does. Empty shows none, so a conditional hint is a string; the `hint` slot takes markup and always shows. Like the label, it can't be selected. The hint and the control's error take the label's size: body-medium, or body-small in a small group. The Figma component has the hint too, which Code Connect and the mockup builder read
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
- **figma-ui3-kit-svelte** peer is `^0.8.0`, whose Input, Textarea, Dropdown and Dropzone size their error by the `--field-error-*` properties FieldGroup sets
|
|
45
|
+
- `resizeToFit` — measures only the `container` you pass, and without one or a `height` warns and leaves the window as it is. It measured `document.body` by default, which fills the window, so the window could grow but never shrink
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- `sendToUI`, `sendToPlugin` — a `type` field in the data no longer replaces the message's type
|
|
49
|
+
- `formatErrorMessage` — the network, CORS and JSON messages follow the copy guidelines: no "Please" or "Network error:" prefix
|
|
50
|
+
- **FieldGroup** — `labelFor` ties the label to its control, so clicking the label focuses an `Input` or `Textarea`. It was passed to Label as `for`, which Label doesn't take, so no label was tied to anything
|
|
51
|
+
- **EmptyState** — `icon`, `action` and `actions` have types, so a plugin's `svelte-check` with `checkJs` passes
|
|
52
|
+
- `autoResize` — the options default to `{ container: null }`, which matches their type; without a container it still warns and does nothing
|
|
53
|
+
|
|
5
54
|
## [0.5.1] - 2026-10-07
|
|
6
55
|
|
|
7
56
|
### Changed
|
|
8
|
-
-
|
|
57
|
+
- **figma-ui3-kit-svelte** is a peer dependency, `^0.7.0`: install it beside the utilities. 0.7.0 is what `confirmDiscardChanges` needs for Modal's `beforeClose`, and **ConfirmModal** over another modal for Escape to close only the top one
|
|
9
58
|
|
|
10
59
|
## [0.5.0] - 2026-10-07
|
|
11
60
|
|
|
@@ -61,7 +110,7 @@
|
|
|
61
110
|
- 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
|
|
62
111
|
|
|
63
112
|
### Fixed
|
|
64
|
-
- **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
|
|
113
|
+
- **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 gray bar
|
|
65
114
|
- **EmptyState** — the actions are a keyed `{#each}`, so swapping one action for another reuses the right button rather than repainting the row
|
|
66
115
|
- **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
|
|
67
116
|
|
package/README.md
CHANGED
|
@@ -5,9 +5,11 @@ Shared Svelte components and utilities for Figma plugins.
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install figma-plugin-utilities
|
|
8
|
+
npm install figma-plugin-utilities figma-ui3-kit-svelte
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
The components are built from `figma-ui3-kit-svelte`, a peer dependency: install it beside the utilities.
|
|
12
|
+
|
|
11
13
|
## Usage
|
|
12
14
|
|
|
13
15
|
### Import Everything
|
|
@@ -75,22 +77,22 @@ import { sendToPlugin, createMessageHandler } from "figma-plugin-utilities/lib";
|
|
|
75
77
|
| Component | Description |
|
|
76
78
|
|-----------|-------------|
|
|
77
79
|
| `PluginLayout` | Main content wrapper with scrollable area |
|
|
78
|
-
| `Header` | Header bar with `left`, `center`, `right`
|
|
80
|
+
| `Header` | Header bar with `left`, `center`, `right` snippets and optional title |
|
|
79
81
|
| `Footer` | Footer with `right`, `split`, and `full` layout variants |
|
|
80
82
|
| `StatusBar` | Toast notifications with auto-dismiss (info/success/error/warning) |
|
|
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`
|
|
83
|
-
| `LoadingState` |
|
|
84
|
-
| `FieldGroup` | Label + input wrapper; `labelFor` binds the label to a text control |
|
|
83
|
+
| `EmptyState` | Empty/error states with optional icon and action buttons; `size`, `iconSize`, `centered`, and `role="alert"` for failures |
|
|
84
|
+
| `ListItem` | Selectable list items with metadata and a `badge` snippet, an action menu (`menuOpen`, `onmenutoggle`, `onmenuclose`) |
|
|
85
|
+
| `LoadingState` | Centered message as `role="status"` (text only, no spinner) |
|
|
86
|
+
| `FieldGroup` | Label + input wrapper; `labelFor` binds the label to a text control, `hint` adds a line under it |
|
|
85
87
|
| `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`
|
|
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`
|
|
88
|
+
| `Section` | Titled group of fields as in Figma's panels: a `Header` with the title and an `actions` snippet, content padded by the section itself |
|
|
89
|
+
| `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` snippet, notes as badges, with tooltips, and a `+N` count past two, an `action` snippet, values as badges or variable chips, removed rows and a marked column |
|
|
88
90
|
| `FieldGrid` | Fields side by side in equal columns (`columns`, default 2) that shrink below their content |
|
|
89
91
|
| `SteppedField` | A field with − and + icon buttons after it, as one grid cell; `step` event with -1 or 1 |
|
|
90
92
|
| `LadderBadges` | A scale's sizes as badges, outlined where used and archived where not, each with the caller's tooltip |
|
|
91
93
|
| `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
94
|
| `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`
|
|
95
|
+
| `CodeExportModal` | Read-only code in a modal with a copy button that reads "Copied" for 2s; a `controls` snippet above the code |
|
|
94
96
|
|
|
95
97
|
Every component also takes a `class` (or `className`) prop.
|
|
96
98
|
|
|
@@ -98,12 +100,12 @@ Every component also takes a `class` (or `className`) prop.
|
|
|
98
100
|
|
|
99
101
|
```svelte
|
|
100
102
|
<Header title="My Plugin">
|
|
101
|
-
|
|
102
|
-
<IconButton iconName={IconBack}
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
{#snippet left()}
|
|
104
|
+
<IconButton iconName={IconBack} onclick={goBack} />
|
|
105
|
+
{/snippet}
|
|
106
|
+
{#snippet right()}
|
|
105
107
|
<IconButton iconName={IconSettings} />
|
|
106
|
-
|
|
108
|
+
{/snippet}
|
|
107
109
|
</Header>
|
|
108
110
|
|
|
109
111
|
<!-- Without border -->
|
|
@@ -120,12 +122,12 @@ Every component also takes a `class` (or `className`) prop.
|
|
|
120
122
|
|
|
121
123
|
<!-- Split layout -->
|
|
122
124
|
<Footer variant="split">
|
|
123
|
-
|
|
125
|
+
{#snippet left()}
|
|
124
126
|
<Button variant="secondary">Cancel</Button>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
{/snippet}
|
|
128
|
+
{#snippet right()}
|
|
127
129
|
<Button variant="primary">Save</Button>
|
|
128
|
-
|
|
130
|
+
{/snippet}
|
|
129
131
|
</Footer>
|
|
130
132
|
|
|
131
133
|
<!-- Full-width buttons -->
|
|
@@ -140,7 +142,7 @@ Every component also takes a `class` (or `className`) prop.
|
|
|
140
142
|
<StatusBar
|
|
141
143
|
message={status.message}
|
|
142
144
|
type={status.type}
|
|
143
|
-
|
|
145
|
+
onclose={() => status = { message: '', type: 'info' }}
|
|
144
146
|
/>
|
|
145
147
|
```
|
|
146
148
|
|
|
@@ -170,8 +172,8 @@ Types: `info`, `success`, `error`, `warning`. Auto-dismisses after 4s for `info`
|
|
|
170
172
|
{ label: 'Edit', value: 'edit' },
|
|
171
173
|
{ label: 'Delete', value: 'delete' }
|
|
172
174
|
]}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
onclick={handleSelect}
|
|
176
|
+
onmenuselect={handleMenuAction}
|
|
175
177
|
>
|
|
176
178
|
<span>Additional metadata</span>
|
|
177
179
|
</ListItem>
|
|
@@ -185,7 +187,7 @@ Large checkbox with card-style background and better touch targets.
|
|
|
185
187
|
<!-- Basic usage -->
|
|
186
188
|
<CheckboxCard
|
|
187
189
|
checked={isSelected}
|
|
188
|
-
|
|
190
|
+
onchange={handleToggle}
|
|
189
191
|
>
|
|
190
192
|
Small
|
|
191
193
|
</CheckboxCard>
|
|
@@ -193,10 +195,10 @@ Large checkbox with card-style background and better touch targets.
|
|
|
193
195
|
<!-- With secondary text -->
|
|
194
196
|
<CheckboxCard
|
|
195
197
|
checked={isSelected}
|
|
196
|
-
|
|
198
|
+
onchange={handleToggle}
|
|
197
199
|
>
|
|
198
200
|
Small
|
|
199
|
-
|
|
201
|
+
{#snippet secondary()}400px{/snippet}
|
|
200
202
|
</CheckboxCard>
|
|
201
203
|
|
|
202
204
|
<!-- Disabled state -->
|
|
@@ -224,7 +226,7 @@ Large checkbox with card-style background and better touch targets.
|
|
|
224
226
|
<SteppedField
|
|
225
227
|
downLabel="Step {set.name} down"
|
|
226
228
|
upLabel="Step {set.name} up"
|
|
227
|
-
|
|
229
|
+
onstep={(step) => setOffset(set.offset + step)}
|
|
228
230
|
>
|
|
229
231
|
<FieldGroup label="Steps off the curve" labelFor="offset" size="small">
|
|
230
232
|
<NumericInput id="offset" value={set.offset} precision={0} />
|
|
@@ -255,11 +257,11 @@ Each badge's accessible name is `"{value}px, used"` or `"{value}px, unused"`.
|
|
|
255
257
|
count={row.uses}
|
|
256
258
|
tone="secondary"
|
|
257
259
|
title="Select these icons"
|
|
258
|
-
|
|
260
|
+
onclick={() => reveal(row)}
|
|
259
261
|
/>
|
|
260
262
|
```
|
|
261
263
|
|
|
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`
|
|
264
|
+
`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` snippet takes a marker ahead of the lead, and `element` binds the button.
|
|
263
265
|
|
|
264
266
|
### RampCurve
|
|
265
267
|
|
|
@@ -279,8 +281,8 @@ Each badge's accessible name is `"{value}px, used"` or `"{value}px, unused"`.
|
|
|
279
281
|
bottomLabel="Smallest level"
|
|
280
282
|
topLabel="Largest level"
|
|
281
283
|
along="levels"
|
|
282
|
-
|
|
283
|
-
|
|
284
|
+
onchange={(patch) => (ramp = { ...ramp, ...patch })}
|
|
285
|
+
onselect={(index) => (selected = index)}
|
|
284
286
|
/>
|
|
285
287
|
```
|
|
286
288
|
|
|
@@ -295,11 +297,11 @@ Each badge's accessible name is `"{value}px, used"` or `"{value}px, unused"`.
|
|
|
295
297
|
value={css}
|
|
296
298
|
ariaLabel="Exported CSS"
|
|
297
299
|
copyLabel="Copy CSS"
|
|
298
|
-
|
|
300
|
+
onclose={() => (exportOpen = false)}
|
|
299
301
|
>
|
|
300
|
-
|
|
302
|
+
{#snippet controls()}
|
|
301
303
|
<SegmentedControl …/>
|
|
302
|
-
|
|
304
|
+
{/snippet}
|
|
303
305
|
</CodeExportModal>
|
|
304
306
|
```
|
|
305
307
|
|
|
@@ -307,7 +309,7 @@ Copies with `execCommand`, since the plugin iframe isn't granted clipboard-write
|
|
|
307
309
|
|
|
308
310
|
## Utilities
|
|
309
311
|
|
|
310
|
-
### Messages (`lib/messages.
|
|
312
|
+
### Messages (`lib/messages.ts`)
|
|
311
313
|
|
|
312
314
|
```javascript
|
|
313
315
|
// Send message to plugin code
|
|
@@ -320,7 +322,27 @@ window.onmessage = createMessageHandler({
|
|
|
320
322
|
});
|
|
321
323
|
```
|
|
322
324
|
|
|
323
|
-
|
|
325
|
+
In TypeScript, type the messages as one union per direction, and the helpers check each message's name and data:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
// messages.ts
|
|
329
|
+
export type ToPlugin = { type: "run"; options: Options } | { type: "cancel" };
|
|
330
|
+
export type ToUI = { type: "done"; count: number };
|
|
331
|
+
|
|
332
|
+
// PluginUI.svelte
|
|
333
|
+
import { sendToPlugin, createMessageHandler, type Send } from "figma-plugin-utilities";
|
|
334
|
+
const send: Send<ToPlugin> = sendToPlugin;
|
|
335
|
+
send("run", { options }); // the data is required, since `run` has it
|
|
336
|
+
window.onmessage = createMessageHandler<ToUI>({ done: (msg) => (count = msg.count) });
|
|
337
|
+
|
|
338
|
+
// code.ts
|
|
339
|
+
import { sendToUI } from "figma-plugin-utilities/lib/figma-helpers";
|
|
340
|
+
import type { Send } from "figma-plugin-utilities/lib/messages";
|
|
341
|
+
const send: Send<ToUI> = sendToUI;
|
|
342
|
+
figma.ui.onmessage = (msg: ToPlugin) => { /* … */ };
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Colors (`lib/colors.ts`)
|
|
324
346
|
|
|
325
347
|
```javascript
|
|
326
348
|
// Convert between formats (Figma uses 0-1 range)
|
|
@@ -333,7 +355,7 @@ const ratio = getContrastRatio(color1, color2);
|
|
|
333
355
|
const passes = meetsContrastLevel(ratio, "AA"); // true/false
|
|
334
356
|
```
|
|
335
357
|
|
|
336
|
-
### Validation (`lib/validation.
|
|
358
|
+
### Validation (`lib/validation.ts`)
|
|
337
359
|
|
|
338
360
|
```javascript
|
|
339
361
|
const urlResult = validateUrl("https://example.com");
|
|
@@ -353,7 +375,7 @@ sanitizeInput(input, 50); // stringify, truncate to maxLength, strip control cha
|
|
|
353
375
|
isEmpty(""); // true — also for [] and {}
|
|
354
376
|
```
|
|
355
377
|
|
|
356
|
-
### Error Handling (`lib/errorHandling.
|
|
378
|
+
### Error Handling (`lib/errorHandling.ts`)
|
|
357
379
|
|
|
358
380
|
```javascript
|
|
359
381
|
// Safe async operations
|
|
@@ -372,13 +394,13 @@ const parsed = parseJsonSafe(jsonString);
|
|
|
372
394
|
// { ok: true, value: {...} } or { ok: false, error: "..." }
|
|
373
395
|
```
|
|
374
396
|
|
|
375
|
-
### Resize (`lib/resize.
|
|
397
|
+
### Resize (`lib/resize.ts`)
|
|
376
398
|
|
|
377
399
|
Utilities for dynamically resizing the plugin window to fit its content.
|
|
378
400
|
|
|
379
401
|
```javascript
|
|
380
|
-
// One-time resize to fit content
|
|
381
|
-
resizeToFit({ width: 300, minHeight: 100, maxHeight: 600 });
|
|
402
|
+
// One-time resize to fit content: measure a naturally-flowing wrapper, not document.body
|
|
403
|
+
resizeToFit({ container: myContainerEl, width: 300, minHeight: 100, maxHeight: 600 });
|
|
382
404
|
|
|
383
405
|
// Watch for content changes and auto-resize
|
|
384
406
|
const cleanup = autoResize({
|
package/package.json
CHANGED
|
@@ -1,28 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-plugin-utilities",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-rc.0",
|
|
4
4
|
"description": "Shared Svelte components and utilities for Figma plugins",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"svelte": "./src/index.
|
|
7
|
-
"main": "./src/index.
|
|
8
|
-
"module": "./src/index.
|
|
6
|
+
"svelte": "./src/index.ts",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"module": "./src/index.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
11
|
+
"types": "./src/index.ts",
|
|
12
|
+
"svelte": "./src/index.ts",
|
|
13
|
+
"import": "./src/index.ts",
|
|
14
|
+
"default": "./src/index.ts"
|
|
14
15
|
},
|
|
15
16
|
"./components": {
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
17
|
+
"types": "./src/components/index.ts",
|
|
18
|
+
"svelte": "./src/components/index.ts",
|
|
19
|
+
"import": "./src/components/index.ts",
|
|
20
|
+
"default": "./src/components/index.ts"
|
|
19
21
|
},
|
|
20
22
|
"./lib": {
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
+
"types": "./src/lib/index.ts",
|
|
24
|
+
"import": "./src/lib/index.ts",
|
|
25
|
+
"default": "./src/lib/index.ts"
|
|
23
26
|
},
|
|
24
|
-
"./lib
|
|
25
|
-
"
|
|
27
|
+
"./lib/errorHandling": {
|
|
28
|
+
"types": "./src/lib/errorHandling.ts",
|
|
29
|
+
"import": "./src/lib/errorHandling.ts",
|
|
30
|
+
"default": "./src/lib/errorHandling.ts"
|
|
31
|
+
},
|
|
32
|
+
"./lib/validation": {
|
|
33
|
+
"types": "./src/lib/validation.ts",
|
|
34
|
+
"import": "./src/lib/validation.ts",
|
|
35
|
+
"default": "./src/lib/validation.ts"
|
|
36
|
+
},
|
|
37
|
+
"./lib/resize": {
|
|
38
|
+
"types": "./src/lib/resize.ts",
|
|
39
|
+
"import": "./src/lib/resize.ts",
|
|
40
|
+
"default": "./src/lib/resize.ts"
|
|
41
|
+
},
|
|
42
|
+
"./lib/messages": {
|
|
43
|
+
"types": "./src/lib/messages.ts",
|
|
44
|
+
"import": "./src/lib/messages.ts",
|
|
45
|
+
"default": "./src/lib/messages.ts"
|
|
26
46
|
},
|
|
27
47
|
"./lib/figma-helpers": {
|
|
28
48
|
"types": "./src/lib/figma-helpers.ts",
|
|
@@ -58,6 +78,11 @@
|
|
|
58
78
|
"types": "./src/lib/confirm.ts",
|
|
59
79
|
"import": "./src/lib/confirm.ts",
|
|
60
80
|
"default": "./src/lib/confirm.ts"
|
|
81
|
+
},
|
|
82
|
+
"./vite": {
|
|
83
|
+
"types": "./src/vite.d.ts",
|
|
84
|
+
"import": "./src/vite.js",
|
|
85
|
+
"default": "./src/vite.js"
|
|
61
86
|
}
|
|
62
87
|
},
|
|
63
88
|
"files": [
|
|
@@ -84,25 +109,40 @@
|
|
|
84
109
|
],
|
|
85
110
|
"author": "Marius Roosendaal",
|
|
86
111
|
"license": "MIT",
|
|
112
|
+
"peerDependencies": {
|
|
113
|
+
"svelte": "^5.35.0",
|
|
114
|
+
"figma-ui3-kit-svelte": "^1.0.0-rc.0",
|
|
115
|
+
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
116
|
+
"vite": "^5.2.0"
|
|
117
|
+
},
|
|
118
|
+
"peerDependenciesMeta": {
|
|
119
|
+
"@sveltejs/vite-plugin-svelte": {
|
|
120
|
+
"optional": true
|
|
121
|
+
},
|
|
122
|
+
"vite": {
|
|
123
|
+
"optional": true
|
|
124
|
+
}
|
|
125
|
+
},
|
|
87
126
|
"devDependencies": {
|
|
88
127
|
"@eslint/js": "^9.39.2",
|
|
89
128
|
"@figma/plugin-typings": "^1.138.0",
|
|
90
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
129
|
+
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
|
91
130
|
"@types/node": "^22.13.4",
|
|
92
131
|
"eslint": "^9.39.2",
|
|
93
132
|
"eslint-plugin-svelte": "^3.23.0",
|
|
94
|
-
"figma-ui3-kit-svelte": "^0.
|
|
133
|
+
"figma-ui3-kit-svelte": "^1.0.0-rc.0",
|
|
95
134
|
"globals": "^17.12.0",
|
|
96
135
|
"prettier": "^3.9.8",
|
|
97
136
|
"prettier-plugin-svelte": "^3.4.0",
|
|
98
|
-
"svelte": "^
|
|
137
|
+
"svelte": "^5.57.2",
|
|
138
|
+
"svelte-check": "^4.3.6",
|
|
99
139
|
"svelte-eslint-parser": "^1.8.1",
|
|
100
140
|
"typescript": "^5.9.3",
|
|
101
141
|
"typescript-eslint": "^8.70.0",
|
|
102
142
|
"vite": "^5.2.0"
|
|
103
143
|
},
|
|
104
144
|
"scripts": {
|
|
105
|
-
"lint": "tsc --noEmit && eslint . --ext .ts,.js,.svelte && npx prettier --check \"src/**/*.{js,ts,svelte,html,css}\"",
|
|
145
|
+
"lint": "tsc --noEmit && svelte-check --tsconfig ./tsconfig.json --fail-on-warnings && eslint . --ext .ts,.js,.svelte && npx prettier --check \"src/**/*.{js,ts,svelte,html,css}\"",
|
|
106
146
|
"prettier": "npx prettier --write \"src/**/*.{js,ts,svelte,html,css}\""
|
|
107
147
|
}
|
|
108
148
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
import {
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
3
|
import { Checkbox } from "figma-ui3-kit-svelte";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -7,62 +7,67 @@
|
|
|
7
7
|
* Wraps the standard checkbox in a card-like layout
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
|
-
* <CheckboxCard
|
|
11
|
-
* checked={isSelected}
|
|
12
|
-
* on:change={handleToggle}
|
|
13
|
-
* >
|
|
10
|
+
* <CheckboxCard bind:checked={isSelected} onchange={handleToggle}>
|
|
14
11
|
* Small
|
|
15
12
|
* </CheckboxCard>
|
|
16
13
|
*
|
|
17
14
|
* @example with secondary text
|
|
18
|
-
* <CheckboxCard
|
|
19
|
-
* checked={isSelected}
|
|
20
|
-
* on:change={handleToggle}
|
|
21
|
-
* >
|
|
15
|
+
* <CheckboxCard bind:checked={isSelected} onchange={handleToggle}>
|
|
22
16
|
* Small
|
|
23
|
-
*
|
|
17
|
+
* {#snippet secondary()}400px{/snippet}
|
|
24
18
|
* </CheckboxCard>
|
|
25
19
|
*/
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
interface Props {
|
|
22
|
+
checked?: boolean;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
/** The label */
|
|
25
|
+
children?: Snippet;
|
|
26
|
+
/** A line under the label, such as a size */
|
|
27
|
+
secondary?: Snippet;
|
|
28
|
+
/** After `checked` updates */
|
|
29
|
+
onchange?: (detail: { checked: boolean }) => void;
|
|
30
|
+
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
let {
|
|
33
|
+
checked = $bindable(),
|
|
34
|
+
disabled = false,
|
|
35
|
+
children,
|
|
36
|
+
secondary,
|
|
37
|
+
onchange,
|
|
38
|
+
}: Props = $props();
|
|
34
39
|
|
|
35
|
-
let cardEl;
|
|
40
|
+
let cardEl: HTMLDivElement | undefined = $state();
|
|
36
41
|
|
|
37
|
-
function handleChange(e) {
|
|
42
|
+
function handleChange(e: Event & { currentTarget: HTMLInputElement }) {
|
|
38
43
|
if (disabled) return;
|
|
39
|
-
checked = e.
|
|
40
|
-
|
|
44
|
+
checked = e.currentTarget.checked;
|
|
45
|
+
onchange?.({ checked });
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
function handleCardClick(e) {
|
|
48
|
+
function handleCardClick(e: MouseEvent) {
|
|
44
49
|
if (disabled) return;
|
|
45
50
|
// Clicks inside the Checkbox component (label/input) are handled natively
|
|
46
|
-
if (e.target.closest(".checkbox-container")) return;
|
|
47
|
-
cardEl?.querySelector('input[type="checkbox"]')?.click();
|
|
51
|
+
if ((e.target as Element).closest(".checkbox-container")) return;
|
|
52
|
+
cardEl?.querySelector<HTMLInputElement>('input[type="checkbox"]')?.click();
|
|
48
53
|
}
|
|
49
54
|
</script>
|
|
50
55
|
|
|
51
|
-
<!-- svelte-ignore
|
|
56
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
52
57
|
<!-- Keyboard users interact with the native checkbox input inside; this div is a mouse-only larger click target -->
|
|
53
58
|
<div
|
|
54
59
|
class="checkbox-card"
|
|
55
60
|
class:disabled
|
|
56
61
|
aria-disabled={disabled || undefined}
|
|
57
62
|
bind:this={cardEl}
|
|
58
|
-
|
|
63
|
+
onclick={handleCardClick}
|
|
59
64
|
>
|
|
60
|
-
<Checkbox {checked} {disabled}
|
|
61
|
-
|
|
65
|
+
<Checkbox {checked} {disabled} onchange={handleChange}>
|
|
66
|
+
{@render children?.()}
|
|
62
67
|
</Checkbox>
|
|
63
|
-
{#if
|
|
68
|
+
{#if secondary}
|
|
64
69
|
<div class="checkbox-card__secondary">
|
|
65
|
-
|
|
70
|
+
{@render secondary()}
|
|
66
71
|
</div>
|
|
67
72
|
{/if}
|
|
68
73
|
</div>
|
|
@@ -91,8 +96,9 @@
|
|
|
91
96
|
cursor: not-allowed;
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
/* Custom checkbox styling for card variant
|
|
95
|
-
|
|
99
|
+
/* Custom checkbox styling for card variant: an empty box takes the window's
|
|
100
|
+
fill. Checkbox's own rules style the checked, mixed and disabled ones. */
|
|
101
|
+
.checkbox-card :global(.checkbox-box:not(.checked, .mixed, .disabled)) {
|
|
96
102
|
background-color: var(--figma-color-bg);
|
|
97
103
|
}
|
|
98
104
|
|
|
@@ -2,28 +2,48 @@
|
|
|
2
2
|
Read-only code in a modal, with a button that copies it. Each modal keeps
|
|
3
3
|
its own "Copied" state, so copying one never marks the other.
|
|
4
4
|
-->
|
|
5
|
-
<script>
|
|
6
|
-
import { onDestroy } from "svelte";
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import { onDestroy, type Snippet } from "svelte";
|
|
7
7
|
import { Button, Modal, Textarea } from "figma-ui3-kit-svelte";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
interface Props {
|
|
10
|
+
isOpen?: boolean;
|
|
11
|
+
title: string;
|
|
12
|
+
value?: string;
|
|
13
|
+
/** Names the code for assistive tech, e.g. "Exported token JSON". */
|
|
14
|
+
ariaLabel: string;
|
|
15
|
+
/** The copy button's label, e.g. "Copy JSON". */
|
|
16
|
+
copyLabel: string;
|
|
17
|
+
position?: "center" | "left" | "right" | "bottom";
|
|
18
|
+
width?: string | number;
|
|
19
|
+
height?: string | number;
|
|
20
|
+
/** Options for what's exported, above the code */
|
|
21
|
+
controls?: Snippet;
|
|
22
|
+
/** X, Escape or a click outside */
|
|
23
|
+
onclose?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
isOpen = $bindable(),
|
|
28
|
+
title,
|
|
29
|
+
value = "",
|
|
30
|
+
ariaLabel,
|
|
31
|
+
copyLabel,
|
|
32
|
+
position = "bottom",
|
|
33
|
+
width = "medium",
|
|
34
|
+
height = "auto",
|
|
35
|
+
controls,
|
|
36
|
+
onclose,
|
|
37
|
+
}: Props = $props();
|
|
20
38
|
|
|
21
|
-
let copied = false;
|
|
22
|
-
let copyTimer
|
|
39
|
+
let copied = $state(false);
|
|
40
|
+
let copyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
23
41
|
onDestroy(() => clearTimeout(copyTimer));
|
|
24
42
|
|
|
25
43
|
// Reopened, the button reads as it would before a copy.
|
|
26
|
-
|
|
44
|
+
$effect.pre(() => {
|
|
45
|
+
if (isOpen) copied = false;
|
|
46
|
+
});
|
|
27
47
|
|
|
28
48
|
// execCommand, not the Clipboard API: the plugin iframe isn't granted
|
|
29
49
|
// clipboard-write.
|
|
@@ -53,24 +73,24 @@
|
|
|
53
73
|
</script>
|
|
54
74
|
|
|
55
75
|
<Modal
|
|
56
|
-
|
|
76
|
+
bind:isOpen
|
|
57
77
|
{title}
|
|
58
78
|
{position}
|
|
59
79
|
{width}
|
|
60
80
|
{height}
|
|
61
81
|
overlayPadding="0px"
|
|
62
|
-
{
|
|
82
|
+
{onclose}
|
|
63
83
|
>
|
|
64
84
|
<div class="export-content">
|
|
65
85
|
<!-- Options for what's exported, above the code. -->
|
|
66
|
-
|
|
86
|
+
{@render controls?.()}
|
|
67
87
|
<Textarea {value} readonly {ariaLabel} variant="code" />
|
|
68
88
|
</div>
|
|
69
|
-
|
|
70
|
-
<Button variant="primary"
|
|
89
|
+
{#snippet footerRight()}
|
|
90
|
+
<Button variant="primary" onclick={handleCopy}>
|
|
71
91
|
{copied ? "Copied" : copyLabel}
|
|
72
92
|
</Button>
|
|
73
|
-
|
|
93
|
+
{/snippet}
|
|
74
94
|
</Modal>
|
|
75
95
|
|
|
76
96
|
<style>
|