figma-plugin-utilities 0.6.0 → 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 CHANGED
@@ -2,6 +2,37 @@
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
+
5
36
  ## [0.6.0] - 2026-10-10
6
37
 
7
38
  ### Added
package/README.md CHANGED
@@ -77,22 +77,22 @@ import { sendToPlugin, createMessageHandler } from "figma-plugin-utilities/lib";
77
77
  | Component | Description |
78
78
  |-----------|-------------|
79
79
  | `PluginLayout` | Main content wrapper with scrollable area |
80
- | `Header` | Header bar with `left`, `center`, `right` slots and optional title |
80
+ | `Header` | Header bar with `left`, `center`, `right` snippets and optional title |
81
81
  | `Footer` | Footer with `right`, `split`, and `full` layout variants |
82
82
  | `StatusBar` | Toast notifications with auto-dismiss (info/success/error/warning) |
83
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 `badge` slots, an action menu (`menuOpen`, `menuToggle`, `menuClose`) |
84
+ | `ListItem` | Selectable list items with metadata and a `badge` snippet, an action menu (`menuOpen`, `onmenutoggle`, `onmenuclose`) |
85
85
  | `LoadingState` | Centered message as `role="status"` (text only, no spinner) |
86
86
  | `FieldGroup` | Label + input wrapper; `labelFor` binds the label to a text control, `hint` adds a line under it |
87
87
  | `CheckboxCard` | Large checkbox with card styling and better touch targets; `change` event |
88
- | `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 |
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` 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 |
90
90
  | `FieldGrid` | Fields side by side in equal columns (`columns`, default 2) that shrink below their content |
91
91
  | `SteppedField` | A field with − and + icon buttons after it, as one grid cell; `step` event with -1 or 1 |
92
92
  | `LadderBadges` | A scale's sizes as badges, outlined where used and archived where not, each with the caller's tooltip |
93
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 |
94
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 |
95
- | `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 |
96
96
 
97
97
  Every component also takes a `class` (or `className`) prop.
98
98
 
@@ -100,12 +100,12 @@ Every component also takes a `class` (or `className`) prop.
100
100
 
101
101
  ```svelte
102
102
  <Header title="My Plugin">
103
- <svelte:fragment slot="left">
104
- <IconButton iconName={IconBack} on:click={goBack} />
105
- </svelte:fragment>
106
- <svelte:fragment slot="right">
103
+ {#snippet left()}
104
+ <IconButton iconName={IconBack} onclick={goBack} />
105
+ {/snippet}
106
+ {#snippet right()}
107
107
  <IconButton iconName={IconSettings} />
108
- </svelte:fragment>
108
+ {/snippet}
109
109
  </Header>
110
110
 
111
111
  <!-- Without border -->
@@ -122,12 +122,12 @@ Every component also takes a `class` (or `className`) prop.
122
122
 
123
123
  <!-- Split layout -->
124
124
  <Footer variant="split">
125
- <svelte:fragment slot="left">
125
+ {#snippet left()}
126
126
  <Button variant="secondary">Cancel</Button>
127
- </svelte:fragment>
128
- <svelte:fragment slot="right">
127
+ {/snippet}
128
+ {#snippet right()}
129
129
  <Button variant="primary">Save</Button>
130
- </svelte:fragment>
130
+ {/snippet}
131
131
  </Footer>
132
132
 
133
133
  <!-- Full-width buttons -->
@@ -142,7 +142,7 @@ Every component also takes a `class` (or `className`) prop.
142
142
  <StatusBar
143
143
  message={status.message}
144
144
  type={status.type}
145
- on:close={() => status = { message: '', type: 'info' }}
145
+ onclose={() => status = { message: '', type: 'info' }}
146
146
  />
147
147
  ```
148
148
 
@@ -172,8 +172,8 @@ Types: `info`, `success`, `error`, `warning`. Auto-dismisses after 4s for `info`
172
172
  { label: 'Edit', value: 'edit' },
173
173
  { label: 'Delete', value: 'delete' }
174
174
  ]}
175
- on:click={handleSelect}
176
- on:menuSelect={handleMenuAction}
175
+ onclick={handleSelect}
176
+ onmenuselect={handleMenuAction}
177
177
  >
178
178
  <span>Additional metadata</span>
179
179
  </ListItem>
@@ -187,7 +187,7 @@ Large checkbox with card-style background and better touch targets.
187
187
  <!-- Basic usage -->
188
188
  <CheckboxCard
189
189
  checked={isSelected}
190
- on:change={handleToggle}
190
+ onchange={handleToggle}
191
191
  >
192
192
  Small
193
193
  </CheckboxCard>
@@ -195,10 +195,10 @@ Large checkbox with card-style background and better touch targets.
195
195
  <!-- With secondary text -->
196
196
  <CheckboxCard
197
197
  checked={isSelected}
198
- on:change={handleToggle}
198
+ onchange={handleToggle}
199
199
  >
200
200
  Small
201
- <svelte:fragment slot="secondary">400px</svelte:fragment>
201
+ {#snippet secondary()}400px{/snippet}
202
202
  </CheckboxCard>
203
203
 
204
204
  <!-- Disabled state -->
@@ -226,7 +226,7 @@ Large checkbox with card-style background and better touch targets.
226
226
  <SteppedField
227
227
  downLabel="Step {set.name} down"
228
228
  upLabel="Step {set.name} up"
229
- on:step={(e) => setOffset(set.offset + e.detail)}
229
+ onstep={(step) => setOffset(set.offset + step)}
230
230
  >
231
231
  <FieldGroup label="Steps off the curve" labelFor="offset" size="small">
232
232
  <NumericInput id="offset" value={set.offset} precision={0} />
@@ -257,11 +257,11 @@ Each badge's accessible name is `"{value}px, used"` or `"{value}px, unused"`.
257
257
  count={row.uses}
258
258
  tone="secondary"
259
259
  title="Select these icons"
260
- on:click={() => reveal(row)}
260
+ onclick={() => reveal(row)}
261
261
  />
262
262
  ```
263
263
 
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` 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.
265
265
 
266
266
  ### RampCurve
267
267
 
@@ -281,8 +281,8 @@ Each badge's accessible name is `"{value}px, used"` or `"{value}px, unused"`.
281
281
  bottomLabel="Smallest level"
282
282
  topLabel="Largest level"
283
283
  along="levels"
284
- on:change={(e) => (ramp = { ...ramp, ...e.detail })}
285
- on:select={(e) => (selected = e.detail)}
284
+ onchange={(patch) => (ramp = { ...ramp, ...patch })}
285
+ onselect={(index) => (selected = index)}
286
286
  />
287
287
  ```
288
288
 
@@ -297,11 +297,11 @@ Each badge's accessible name is `"{value}px, used"` or `"{value}px, unused"`.
297
297
  value={css}
298
298
  ariaLabel="Exported CSS"
299
299
  copyLabel="Copy CSS"
300
- onClose={() => (exportOpen = false)}
300
+ onclose={() => (exportOpen = false)}
301
301
  >
302
- <svelte:fragment slot="controls">
302
+ {#snippet controls()}
303
303
  <SegmentedControl …/>
304
- </svelte:fragment>
304
+ {/snippet}
305
305
  </CodeExportModal>
306
306
  ```
307
307
 
@@ -309,7 +309,7 @@ Copies with `execCommand`, since the plugin iframe isn't granted clipboard-write
309
309
 
310
310
  ## Utilities
311
311
 
312
- ### Messages (`lib/messages.js`)
312
+ ### Messages (`lib/messages.ts`)
313
313
 
314
314
  ```javascript
315
315
  // Send message to plugin code
@@ -322,7 +322,27 @@ window.onmessage = createMessageHandler({
322
322
  });
323
323
  ```
324
324
 
325
- ### 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`)
326
346
 
327
347
  ```javascript
328
348
  // Convert between formats (Figma uses 0-1 range)
@@ -335,7 +355,7 @@ const ratio = getContrastRatio(color1, color2);
335
355
  const passes = meetsContrastLevel(ratio, "AA"); // true/false
336
356
  ```
337
357
 
338
- ### Validation (`lib/validation.js`)
358
+ ### Validation (`lib/validation.ts`)
339
359
 
340
360
  ```javascript
341
361
  const urlResult = validateUrl("https://example.com");
@@ -355,7 +375,7 @@ sanitizeInput(input, 50); // stringify, truncate to maxLength, strip control cha
355
375
  isEmpty(""); // true — also for [] and {}
356
376
  ```
357
377
 
358
- ### Error Handling (`lib/errorHandling.js`)
378
+ ### Error Handling (`lib/errorHandling.ts`)
359
379
 
360
380
  ```javascript
361
381
  // Safe async operations
@@ -374,7 +394,7 @@ const parsed = parseJsonSafe(jsonString);
374
394
  // { ok: true, value: {...} } or { ok: false, error: "..." }
375
395
  ```
376
396
 
377
- ### Resize (`lib/resize.js`)
397
+ ### Resize (`lib/resize.ts`)
378
398
 
379
399
  Utilities for dynamically resizing the plugin window to fit its content.
380
400
 
package/package.json CHANGED
@@ -1,28 +1,48 @@
1
1
  {
2
2
  "name": "figma-plugin-utilities",
3
- "version": "0.6.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.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",
@@ -90,8 +110,9 @@
90
110
  "author": "Marius Roosendaal",
91
111
  "license": "MIT",
92
112
  "peerDependencies": {
93
- "figma-ui3-kit-svelte": "^0.8.0",
94
- "@sveltejs/vite-plugin-svelte": "^3.0.2",
113
+ "svelte": "^5.35.0",
114
+ "figma-ui3-kit-svelte": "^1.0.0-rc.0",
115
+ "@sveltejs/vite-plugin-svelte": "^4.0.0",
95
116
  "vite": "^5.2.0"
96
117
  },
97
118
  "peerDependenciesMeta": {
@@ -105,22 +126,23 @@
105
126
  "devDependencies": {
106
127
  "@eslint/js": "^9.39.2",
107
128
  "@figma/plugin-typings": "^1.138.0",
108
- "@sveltejs/vite-plugin-svelte": "^3.0.2",
129
+ "@sveltejs/vite-plugin-svelte": "^4.0.4",
109
130
  "@types/node": "^22.13.4",
110
131
  "eslint": "^9.39.2",
111
132
  "eslint-plugin-svelte": "^3.23.0",
112
- "figma-ui3-kit-svelte": "^0.8.0",
133
+ "figma-ui3-kit-svelte": "^1.0.0-rc.0",
113
134
  "globals": "^17.12.0",
114
135
  "prettier": "^3.9.8",
115
136
  "prettier-plugin-svelte": "^3.4.0",
116
- "svelte": "^4.2.20",
137
+ "svelte": "^5.57.2",
138
+ "svelte-check": "^4.3.6",
117
139
  "svelte-eslint-parser": "^1.8.1",
118
140
  "typescript": "^5.9.3",
119
141
  "typescript-eslint": "^8.70.0",
120
142
  "vite": "^5.2.0"
121
143
  },
122
144
  "scripts": {
123
- "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}\"",
124
146
  "prettier": "npx prettier --write \"src/**/*.{js,ts,svelte,html,css}\""
125
147
  }
126
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>
@@ -2,7 +2,7 @@
2
2
  The dialog confirmAction() opens: mount one in the plugin's UI, after
3
3
  everything else, so it shows above any modal that asks.
4
4
  -->
5
- <script>
5
+ <script lang="ts">
6
6
  import { Modal, Button, Text } from "figma-ui3-kit-svelte";
7
7
  import { confirmRequest, answerConfirm } from "../lib/confirm";
8
8
  </script>
@@ -12,25 +12,25 @@
12
12
  title={$confirmRequest?.title ?? ""}
13
13
  width="small"
14
14
  position="center"
15
- on:close={() => answerConfirm(false)}
15
+ onclose={() => answerConfirm(false)}
16
16
  >
17
17
  <Text>{$confirmRequest?.message ?? ""}</Text>
18
18
 
19
- <svelte:fragment slot="footer-full">
19
+ {#snippet footerFull()}
20
20
  <div class="confirm-footer">
21
21
  <div class="confirm-actions">
22
- <Button variant="secondary" on:click={() => answerConfirm(false)}>
22
+ <Button variant="secondary" onclick={() => answerConfirm(false)}>
23
23
  {$confirmRequest?.cancelLabel || "Cancel"}
24
24
  </Button>
25
25
  <Button
26
26
  variant={$confirmRequest?.destructive ? "destructive" : "primary"}
27
- on:click={() => answerConfirm(true)}
27
+ onclick={() => answerConfirm(true)}
28
28
  >
29
29
  {$confirmRequest?.confirmLabel ?? ""}
30
30
  </Button>
31
31
  </div>
32
32
  </div>
33
- </svelte:fragment>
33
+ {/snippet}
34
34
  </Modal>
35
35
 
36
36
  <style>