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 +31 -0
- package/README.md +53 -33
- package/package.json +42 -20
- 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 +44 -45
- package/src/components/FieldGrid.svelte +6 -3
- package/src/components/FieldGroup.svelte +30 -19
- 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} +46 -34
- package/src/lib/{index.js → index.ts} +7 -7
- package/src/lib/messages.ts +104 -0
- package/src/lib/{resize.js → resize.ts} +41 -28
- package/src/lib/{validation.js → validation.ts} +33 -32
- package/src/lib/messages.js +0 -36
- /package/src/components/{index.js → index.ts} +0 -0
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`
|
|
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`
|
|
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`
|
|
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`
|
|
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`
|
|
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
|
-
|
|
104
|
-
<IconButton iconName={IconBack}
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
{#snippet left()}
|
|
104
|
+
<IconButton iconName={IconBack} onclick={goBack} />
|
|
105
|
+
{/snippet}
|
|
106
|
+
{#snippet right()}
|
|
107
107
|
<IconButton iconName={IconSettings} />
|
|
108
|
-
|
|
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
|
-
|
|
125
|
+
{#snippet left()}
|
|
126
126
|
<Button variant="secondary">Cancel</Button>
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
{/snippet}
|
|
128
|
+
{#snippet right()}
|
|
129
129
|
<Button variant="primary">Save</Button>
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
+
onchange={handleToggle}
|
|
199
199
|
>
|
|
200
200
|
Small
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
285
|
-
|
|
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
|
-
|
|
300
|
+
onclose={() => (exportOpen = false)}
|
|
301
301
|
>
|
|
302
|
-
|
|
302
|
+
{#snippet controls()}
|
|
303
303
|
<SegmentedControl …/>
|
|
304
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
|
@@ -90,8 +110,9 @@
|
|
|
90
110
|
"author": "Marius Roosendaal",
|
|
91
111
|
"license": "MIT",
|
|
92
112
|
"peerDependencies": {
|
|
93
|
-
"
|
|
94
|
-
"
|
|
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": "^
|
|
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.
|
|
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": "^
|
|
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 {
|
|
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>
|
|
@@ -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
|
-
|
|
15
|
+
onclose={() => answerConfirm(false)}
|
|
16
16
|
>
|
|
17
17
|
<Text>{$confirmRequest?.message ?? ""}</Text>
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
{#snippet footerFull()}
|
|
20
20
|
<div class="confirm-footer">
|
|
21
21
|
<div class="confirm-actions">
|
|
22
|
-
<Button variant="secondary"
|
|
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
|
-
|
|
27
|
+
onclick={() => answerConfirm(true)}
|
|
28
28
|
>
|
|
29
29
|
{$confirmRequest?.confirmLabel ?? ""}
|
|
30
30
|
</Button>
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
33
|
-
|
|
33
|
+
{/snippet}
|
|
34
34
|
</Modal>
|
|
35
35
|
|
|
36
36
|
<style>
|