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.
Files changed (33) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.md +61 -39
  3. package/package.json +58 -18
  4. package/src/components/CheckboxCard.svelte +38 -32
  5. package/src/components/CodeExportModal.svelte +42 -22
  6. package/src/components/ConfirmModal.svelte +6 -6
  7. package/src/components/DataTable.svelte +78 -74
  8. package/src/components/EmptyState.svelte +52 -29
  9. package/src/components/FieldGrid.svelte +6 -3
  10. package/src/components/FieldGroup.svelte +57 -11
  11. package/src/components/Footer.svelte +30 -13
  12. package/src/components/Header.svelte +30 -15
  13. package/src/components/LadderBadges.svelte +8 -5
  14. package/src/components/ListItem.svelte +73 -48
  15. package/src/components/LoadingState.svelte +7 -5
  16. package/src/components/MappingChip.svelte +50 -38
  17. package/src/components/PluginLayout.svelte +11 -4
  18. package/src/components/RampCurve.svelte +177 -114
  19. package/src/components/Section.svelte +14 -9
  20. package/src/components/StatusBar.svelte +38 -31
  21. package/src/components/SteppedField.svelte +18 -12
  22. package/src/components/data-table.ts +33 -0
  23. package/src/{index.js → index.ts} +10 -2
  24. package/src/lib/{errorHandling.js → errorHandling.ts} +50 -37
  25. package/src/lib/figma-helpers.ts +1 -1
  26. package/src/lib/{index.js → index.ts} +7 -7
  27. package/src/lib/messages.ts +104 -0
  28. package/src/lib/{resize.js → resize.ts} +48 -28
  29. package/src/lib/{validation.js → validation.ts} +33 -32
  30. package/src/vite.d.ts +13 -0
  31. package/src/vite.js +165 -0
  32. package/src/lib/messages.js +0 -36
  33. /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
- - Built against **figma-ui3-kit-svelte** 0.7.0, which `confirmDiscardChanges` needs for Modal's `beforeClose`, and **ConfirmModal** over another modal for Escape to close only the top one
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 grey bar
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` slots and optional title |
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` 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 |
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` 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
+ | `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` slot above the code |
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
- <svelte:fragment slot="left">
102
- <IconButton iconName={IconBack} on:click={goBack} />
103
- </svelte:fragment>
104
- <svelte:fragment slot="right">
103
+ {#snippet left()}
104
+ <IconButton iconName={IconBack} onclick={goBack} />
105
+ {/snippet}
106
+ {#snippet right()}
105
107
  <IconButton iconName={IconSettings} />
106
- </svelte:fragment>
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
- <svelte:fragment slot="left">
125
+ {#snippet left()}
124
126
  <Button variant="secondary">Cancel</Button>
125
- </svelte:fragment>
126
- <svelte:fragment slot="right">
127
+ {/snippet}
128
+ {#snippet right()}
127
129
  <Button variant="primary">Save</Button>
128
- </svelte:fragment>
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
- on:close={() => status = { message: '', type: 'info' }}
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
- on:click={handleSelect}
174
- on:menuSelect={handleMenuAction}
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
- on:change={handleToggle}
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
- on:change={handleToggle}
198
+ onchange={handleToggle}
197
199
  >
198
200
  Small
199
- <svelte:fragment slot="secondary">400px</svelte:fragment>
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
- on:step={(e) => setOffset(set.offset + e.detail)}
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
- on:click={() => reveal(row)}
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` slot takes a marker ahead of the lead, and `element` binds the button.
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
- on:change={(e) => (ramp = { ...ramp, ...e.detail })}
283
- on:select={(e) => (selected = e.detail)}
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
- onClose={() => (exportOpen = false)}
300
+ onclose={() => (exportOpen = false)}
299
301
  >
300
- <svelte:fragment slot="controls">
302
+ {#snippet controls()}
301
303
  <SegmentedControl …/>
302
- </svelte:fragment>
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.js`)
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
- ### Colors (`lib/colors.js`)
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.js`)
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.js`)
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.js`)
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.5.1",
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.js",
7
- "main": "./src/index.js",
8
- "module": "./src/index.js",
6
+ "svelte": "./src/index.ts",
7
+ "main": "./src/index.ts",
8
+ "module": "./src/index.ts",
9
9
  "exports": {
10
10
  ".": {
11
- "svelte": "./src/index.js",
12
- "import": "./src/index.js",
13
- "default": "./src/index.js"
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
- "svelte": "./src/components/index.js",
17
- "import": "./src/components/index.js",
18
- "default": "./src/components/index.js"
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
- "import": "./src/lib/index.js",
22
- "default": "./src/lib/index.js"
23
+ "types": "./src/lib/index.ts",
24
+ "import": "./src/lib/index.ts",
25
+ "default": "./src/lib/index.ts"
23
26
  },
24
- "./lib/*": {
25
- "import": "./src/lib/*.js"
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": "^3.0.2",
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.7.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": "^4.2.20",
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 { createEventDispatcher } from "svelte";
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
- * <svelte:fragment slot="secondary">400px</svelte:fragment>
17
+ * {#snippet secondary()}400px{/snippet}
24
18
  * </CheckboxCard>
25
19
  */
26
20
 
27
- const dispatch = createEventDispatcher();
28
-
29
- /** Whether checkbox is checked */
30
- export let checked = false;
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
- /** Whether checkbox is disabled */
33
- export let disabled = false;
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.target.checked;
40
- dispatch("change", { checked });
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 a11y-click-events-have-key-events a11y-no-static-element-interactions -->
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
- on:click={handleCardClick}
63
+ onclick={handleCardClick}
59
64
  >
60
- <Checkbox {checked} {disabled} on:change={handleChange}>
61
- <slot />
65
+ <Checkbox {checked} {disabled} onchange={handleChange}>
66
+ {@render children?.()}
62
67
  </Checkbox>
63
- {#if $$slots.secondary}
68
+ {#if secondary}
64
69
  <div class="checkbox-card__secondary">
65
- <slot name="secondary" />
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
- .checkbox-card :global(.checkbox-box) {
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
- 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;
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 = null;
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
- $: if (isOpen) copied = false;
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
- {isOpen}
76
+ bind:isOpen
57
77
  {title}
58
78
  {position}
59
79
  {width}
60
80
  {height}
61
81
  overlayPadding="0px"
62
- {onClose}
82
+ {onclose}
63
83
  >
64
84
  <div class="export-content">
65
85
  <!-- Options for what's exported, above the code. -->
66
- <slot name="controls" />
86
+ {@render controls?.()}
67
87
  <Textarea {value} readonly {ariaLabel} variant="code" />
68
88
  </div>
69
- <svelte:fragment slot="footer-right">
70
- <Button variant="primary" on:click={handleCopy}>
89
+ {#snippet footerRight()}
90
+ <Button variant="primary" onclick={handleCopy}>
71
91
  {copied ? "Copied" : copyLabel}
72
92
  </Button>
73
- </svelte:fragment>
93
+ {/snippet}
74
94
  </Modal>
75
95
 
76
96
  <style>