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