figma-plugin-utilities 0.2.0 → 0.3.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 +62 -0
- package/LICENSE +21 -0
- package/README.md +89 -9
- package/package.json +27 -5
- package/src/components/CheckboxCard.svelte +19 -20
- package/src/components/EmptyState.svelte +7 -3
- package/src/components/FieldGroup.svelte +1 -11
- package/src/components/Footer.svelte +6 -2
- package/src/components/Header.svelte +3 -3
- package/src/components/ListItem.svelte +14 -3
- package/src/components/LoadingState.svelte +7 -2
- package/src/components/PluginLayout.svelte +5 -3
- package/src/components/StatusBar.svelte +8 -2
- package/src/index.js +3 -0
- package/src/lib/colors.js +5 -2
- package/src/lib/figma-helpers.ts +5 -8
- package/src/lib/index.js +1 -4
- package/src/lib/resize.js +4 -4
- package/src/lib/validation.js +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.3.1] - 2026-05-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- ESLint configuration with TypeScript and Svelte support for code linting
|
|
14
|
+
- Prettier setup with Svelte plugin for consistent code formatting
|
|
15
|
+
- `lint` and `prettier` npm scripts for development workflow
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Updated **ListItem** and **StatusBar** to use icon imports from `figma-ui3-kit-svelte/icons` after the UI kit icon export restructure.
|
|
19
|
+
|
|
20
|
+
## [0.3.0] - 2026-05-06
|
|
21
|
+
|
|
22
|
+
WCAG 2.2 AA accessibility audit and remediation across all components.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- `class` prop passthrough to **ListItem**, **LoadingState**, and **StatusBar** — consistent with other components
|
|
26
|
+
- `role="alert"` for error/warning and `role="status"` for info/success on **StatusBar** — messages are now announced by screen readers on insertion
|
|
27
|
+
- `"AAA-large"` case (4.5:1) to `meetsContrastLevel` in `lib/colors.js` — covers WCAG 1.4.6 large text at AAA level
|
|
28
|
+
- GitHub Actions publish workflow (`.github/workflows/publish.yml`) — triggers `npm publish` on GitHub release creation
|
|
29
|
+
- `aria-pressed={active}` to **ListItem** — communicates selection state to assistive technology
|
|
30
|
+
- Space key activation to **ListItem** — keyboard users can now toggle items with Space as well as Enter
|
|
31
|
+
- `ariaLabel="{title} options"` to the **ListItem** menu `IconButton` — gives the icon-only button an accessible name
|
|
32
|
+
- `role="status"` to **LoadingState** — loading message is announced as a polite live region
|
|
33
|
+
- `aria-hidden="true"` to the decorative icon in **EmptyState** — prevents redundant AT announcement
|
|
34
|
+
- `aria-disabled` attribute to **CheckboxCard** — reflects disabled state without removing from the accessibility tree
|
|
35
|
+
- Dev-mode `console.warn` to **FieldGroup** when `label` is provided but `labelFor` is empty
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- **Header**: outer element changed from `<div>` to `<header>`; title changed from `<h2>` to `<h1>` (plugin UI runs in its own iframe, so the heading hierarchy starts fresh)
|
|
39
|
+
- **Footer**: outer element changed from `<div>` to `<footer>`
|
|
40
|
+
- **CheckboxCard**: removed `role="button"` and `tabindex` from the wrapper div — the native checkbox input is the sole interactive/focusable element; the wrapper remains clickable for mouse users via a delegating click handler
|
|
41
|
+
- **CheckboxCard**: added `user-select: none` to prevent text selection on double-click
|
|
42
|
+
|
|
43
|
+
## [0.1.0] - 2026-04-17
|
|
44
|
+
|
|
45
|
+
Initial release as `figma-plugin-utilities`.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- **PluginLayout** — main content wrapper with scrollable area
|
|
49
|
+
- **Header** — header bar with left/center/right slots and optional title
|
|
50
|
+
- **Footer** — footer bar with right, split, and full layout variants
|
|
51
|
+
- **StatusBar** — toast-style notifications with auto-dismiss for info/success types
|
|
52
|
+
- **EmptyState** — empty/error state display with optional icon and action buttons
|
|
53
|
+
- **ListItem** — selectable list item with metadata slot and context menu
|
|
54
|
+
- **LoadingState** — centered loading indicator
|
|
55
|
+
- **FieldGroup** — label + input wrapper for form fields
|
|
56
|
+
- **CheckboxCard** — large-target checkbox with card styling
|
|
57
|
+
- `lib/messages.js` — `sendToPlugin` and `createMessageHandler`
|
|
58
|
+
- `lib/colors.js` — `rgbToHex`, `hexToRgb`, `getLuminance`, `getContrastRatio`, `meetsContrastLevel`
|
|
59
|
+
- `lib/validation.js` — `validateUrl`, `validateJsonString`, `validateEmail`, `validateNumber`, `sanitizeName`, `sanitizeInput`, `isEmpty`
|
|
60
|
+
- `lib/errorHandling.js` — `safeAsync`, `parseJsonSafe`, `notifyError`, `notifySuccess`, `notifyWarning`, and more
|
|
61
|
+
- `lib/resize.js` — `resizeToFit`, `autoResize`, `setDefaultWidth`, `getContentHeight`
|
|
62
|
+
- `lib/figma-helpers.ts` — `sendToUI`, `showError`, `showSuccess`, `getCollections`, `getVariables`, `getSelection`, `focusNodes`, `loadFont`, `saveToStorage`, `loadFromStorage`, `handleResize`
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marius Roosendaal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Shared Svelte components and utilities for Figma plugins.
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install figma-plugin-
|
|
8
|
+
npm install figma-plugin-utilities
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
@@ -24,25 +24,45 @@ import {
|
|
|
24
24
|
LoadingState,
|
|
25
25
|
FieldGroup,
|
|
26
26
|
CheckboxCard,
|
|
27
|
-
//
|
|
27
|
+
// Messages
|
|
28
28
|
sendToPlugin,
|
|
29
29
|
createMessageHandler,
|
|
30
|
+
// Colors
|
|
30
31
|
rgbToHex,
|
|
31
32
|
hexToRgb,
|
|
33
|
+
getLuminance,
|
|
34
|
+
getContrastRatio,
|
|
35
|
+
meetsContrastLevel,
|
|
36
|
+
// Validation
|
|
32
37
|
validateUrl,
|
|
33
38
|
validateJsonString,
|
|
39
|
+
validateEmail,
|
|
40
|
+
validateNumber,
|
|
34
41
|
sanitizeName,
|
|
35
|
-
|
|
42
|
+
sanitizeInput,
|
|
43
|
+
isEmpty,
|
|
44
|
+
// Error handling
|
|
45
|
+
safeAsync,
|
|
46
|
+
parseJsonSafe,
|
|
47
|
+
notifyError,
|
|
48
|
+
notifySuccess,
|
|
49
|
+
notifyWarning,
|
|
50
|
+
// Resize
|
|
51
|
+
setDefaultWidth,
|
|
52
|
+
getContentHeight,
|
|
53
|
+
resizeToFit,
|
|
54
|
+
autoResize,
|
|
55
|
+
} from "figma-plugin-utilities";
|
|
36
56
|
```
|
|
37
57
|
|
|
38
58
|
### Import Specific Modules
|
|
39
59
|
|
|
40
60
|
```javascript
|
|
41
61
|
// Components only
|
|
42
|
-
import { PluginLayout, Header, Footer } from "figma-plugin-
|
|
62
|
+
import { PluginLayout, Header, Footer } from "figma-plugin-utilities/components";
|
|
43
63
|
|
|
44
64
|
// Utilities only
|
|
45
|
-
import { sendToPlugin, createMessageHandler } from "figma-plugin-
|
|
65
|
+
import { sendToPlugin, createMessageHandler } from "figma-plugin-utilities/lib";
|
|
46
66
|
```
|
|
47
67
|
|
|
48
68
|
## Components
|
|
@@ -144,7 +164,7 @@ Types: `info`, `success`, `error`, `warning`. Auto-dismisses after 4s for `info`
|
|
|
144
164
|
|
|
145
165
|
### CheckboxCard
|
|
146
166
|
|
|
147
|
-
Large checkbox with card-style background and better touch targets.
|
|
167
|
+
Large checkbox with card-style background and better touch targets.
|
|
148
168
|
|
|
149
169
|
```svelte
|
|
150
170
|
<!-- Basic usage -->
|
|
@@ -195,7 +215,8 @@ window.onmessage = createMessageHandler({
|
|
|
195
215
|
const rgb = hexToRgb("#FF0000"); // { r: 1, g: 0, b: 0 }
|
|
196
216
|
const hex = rgbToHex({ r: 1, g: 0, b: 0 }); // "#FF0000"
|
|
197
217
|
|
|
198
|
-
//
|
|
218
|
+
// Contrast utilities
|
|
219
|
+
const luminance = getLuminance({ r: 1, g: 0, b: 0 });
|
|
199
220
|
const ratio = getContrastRatio(color1, color2);
|
|
200
221
|
const passes = meetsContrastLevel(ratio, "AA"); // true/false
|
|
201
222
|
```
|
|
@@ -209,7 +230,12 @@ const urlResult = validateUrl("https://example.com");
|
|
|
209
230
|
const jsonResult = validateJsonString('{"key": "value"}');
|
|
210
231
|
// { valid: true, parsed: {...} } or { valid: false, error: "..." }
|
|
211
232
|
|
|
233
|
+
validateEmail("user@example.com"); // { valid: true }
|
|
234
|
+
validateNumber("42", { min: 0, max: 100 }); // { valid: true, value: 42 }
|
|
235
|
+
|
|
212
236
|
const clean = sanitizeName("My Plugin!!!"); // "My Plugin"
|
|
237
|
+
sanitizeInput("<script>alert(1)</script>"); // escaped string
|
|
238
|
+
isEmpty(""); // true
|
|
213
239
|
```
|
|
214
240
|
|
|
215
241
|
### Error Handling (`lib/errorHandling.js`)
|
|
@@ -229,20 +255,71 @@ if (result.ok) {
|
|
|
229
255
|
// Parse JSON safely
|
|
230
256
|
const parsed = parseJsonSafe(jsonString);
|
|
231
257
|
// { ok: true, value: {...} } or { ok: false, error: "..." }
|
|
258
|
+
|
|
259
|
+
// Figma notifications
|
|
260
|
+
notifySuccess("Done!");
|
|
261
|
+
notifyError("Something went wrong");
|
|
262
|
+
notifyWarning("Check your input");
|
|
232
263
|
```
|
|
233
264
|
|
|
265
|
+
### Resize (`lib/resize.js`)
|
|
266
|
+
|
|
267
|
+
Utilities for dynamically resizing the plugin window to fit its content.
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
// One-time resize to fit content
|
|
271
|
+
resizeToFit({ width: 300, minHeight: 100, maxHeight: 600 });
|
|
272
|
+
|
|
273
|
+
// Watch for content changes and auto-resize
|
|
274
|
+
const cleanup = autoResize({
|
|
275
|
+
container: myContainerEl, // bind:this on a naturally-flowing wrapper
|
|
276
|
+
width: 300,
|
|
277
|
+
minHeight: 100,
|
|
278
|
+
maxHeight: 600,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Call cleanup when the component is destroyed
|
|
282
|
+
onDestroy(cleanup);
|
|
283
|
+
|
|
284
|
+
// Set default width used across all resize calls
|
|
285
|
+
setDefaultWidth(320);
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
> **Note:** The `container` element passed to `autoResize` must **not** have `height: 100%` or a fixed height — it should flow naturally with its content so `scrollHeight` can be measured accurately.
|
|
289
|
+
|
|
234
290
|
### Figma Helpers (`lib/figma-helpers.ts`)
|
|
235
291
|
|
|
236
292
|
For use in `code.ts`:
|
|
237
293
|
|
|
238
294
|
```typescript
|
|
239
|
-
import {
|
|
295
|
+
import {
|
|
296
|
+
sendToUI,
|
|
297
|
+
showError,
|
|
298
|
+
showSuccess,
|
|
299
|
+
getCollections,
|
|
300
|
+
getVariables,
|
|
301
|
+
getSelection,
|
|
302
|
+
focusNodes,
|
|
303
|
+
loadFont,
|
|
304
|
+
saveToStorage,
|
|
305
|
+
loadFromStorage,
|
|
306
|
+
handleResize,
|
|
307
|
+
} from "figma-plugin-utilities/lib/figma-helpers";
|
|
240
308
|
|
|
241
309
|
// Send message to UI
|
|
242
310
|
sendToUI("success", { message: "Done!" });
|
|
243
311
|
|
|
244
|
-
// Show
|
|
312
|
+
// Show notifications
|
|
245
313
|
showError("Something went wrong");
|
|
314
|
+
showSuccess("Created!");
|
|
315
|
+
|
|
316
|
+
// Variables
|
|
317
|
+
const collections = await getCollections();
|
|
318
|
+
const colorVars = await getVariables("COLOR");
|
|
319
|
+
|
|
320
|
+
// Selection
|
|
321
|
+
const selected = getSelection(); // all selected nodes
|
|
322
|
+
const frames = getSelection("FRAME"); // filtered by type
|
|
246
323
|
|
|
247
324
|
// Focus viewport on nodes
|
|
248
325
|
focusNodes(figma.currentPage.selection);
|
|
@@ -253,4 +330,7 @@ await loadFont("Inter", "Regular");
|
|
|
253
330
|
// Client storage
|
|
254
331
|
await saveToStorage("settings", { theme: "dark" });
|
|
255
332
|
const settings = await loadFromStorage("settings", { theme: "light" });
|
|
333
|
+
|
|
334
|
+
// Handle resize message from UI (call in your message handler)
|
|
335
|
+
if (msg.type === "resize") handleResize(msg);
|
|
256
336
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-plugin-utilities",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Shared Svelte components and utilities for Figma plugins",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./src/index.js",
|
|
@@ -30,7 +30,12 @@
|
|
|
30
30
|
"default": "./src/lib/figma-helpers.ts"
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
|
-
"files": [
|
|
33
|
+
"files": [
|
|
34
|
+
"src",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"CHANGELOG.md"
|
|
38
|
+
],
|
|
34
39
|
"repository": {
|
|
35
40
|
"type": "git",
|
|
36
41
|
"url": "git+https://github.com/mariusroosendaal/figma-plugin-utilities.git"
|
|
@@ -49,8 +54,25 @@
|
|
|
49
54
|
],
|
|
50
55
|
"author": "Marius Roosendaal",
|
|
51
56
|
"license": "MIT",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@eslint/js": "^9.39.1",
|
|
59
|
+
"@figma/plugin-typings": "^1.51.0",
|
|
60
|
+
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
|
61
|
+
"@types/node": "^22.13.4",
|
|
62
|
+
"eslint": "^9.39.1",
|
|
63
|
+
"eslint-plugin-svelte": "^2.46.0",
|
|
64
|
+
"figma-ui3-kit-svelte": "file:..//_packages/figma-ui3-kit-svelte",
|
|
65
|
+
"globals": "^16.5.0",
|
|
66
|
+
"prettier": "3.7.3",
|
|
67
|
+
"prettier-plugin-svelte": "^3.4.0",
|
|
68
|
+
"svelte": "^4.2.20",
|
|
69
|
+
"svelte-eslint-parser": "^0.39.0",
|
|
70
|
+
"typescript": "^5.9.3",
|
|
71
|
+
"typescript-eslint": "^8.48.0",
|
|
72
|
+
"vite": "^5.2.0"
|
|
73
|
+
},
|
|
74
|
+
"scripts": {
|
|
75
|
+
"lint": "tsc --noEmit && eslint . --ext .ts,.js,.svelte && npx prettier --check \"src/**/*.{js,ts,svelte,html,css}\"",
|
|
76
|
+
"prettier": "npx prettier --write \"src/**/*.{js,ts,svelte,html,css}\""
|
|
55
77
|
}
|
|
56
78
|
}
|
|
@@ -32,34 +32,32 @@
|
|
|
32
32
|
/** Whether checkbox is disabled */
|
|
33
33
|
export let disabled = false;
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
let cardEl;
|
|
36
|
+
|
|
37
|
+
function handleChange(e) {
|
|
36
38
|
if (disabled) return;
|
|
37
|
-
|
|
38
|
-
// Don't toggle if clicking directly on the checkbox or label
|
|
39
|
-
// (let the checkbox handle it natively)
|
|
40
|
-
const target = e.target;
|
|
41
|
-
const isCheckboxOrLabel =
|
|
42
|
-
target.tagName === "INPUT" ||
|
|
43
|
-
target.tagName === "LABEL" ||
|
|
44
|
-
target.closest("label");
|
|
45
|
-
|
|
46
|
-
if (isCheckboxOrLabel) return;
|
|
47
|
-
|
|
48
|
-
// Toggle and dispatch change event
|
|
49
|
-
checked = !checked;
|
|
39
|
+
checked = e.target.checked;
|
|
50
40
|
dispatch("change", { checked });
|
|
51
41
|
}
|
|
42
|
+
|
|
43
|
+
function handleCardClick(e) {
|
|
44
|
+
if (disabled) return;
|
|
45
|
+
// 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();
|
|
48
|
+
}
|
|
52
49
|
</script>
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
|
52
|
+
<!-- Keyboard users interact with the native checkbox input inside; this div is a mouse-only larger click target -->
|
|
53
|
+
<div
|
|
54
|
+
class="checkbox-card"
|
|
56
55
|
class:disabled
|
|
56
|
+
aria-disabled={disabled || undefined}
|
|
57
|
+
bind:this={cardEl}
|
|
57
58
|
on:click={handleCardClick}
|
|
58
|
-
on:keydown={(e) => e.key === "Enter" && handleCardClick(e)}
|
|
59
|
-
role="button"
|
|
60
|
-
tabindex={disabled ? -1 : 0}
|
|
61
59
|
>
|
|
62
|
-
<Checkbox {checked} {disabled} on:change>
|
|
60
|
+
<Checkbox {checked} {disabled} on:change={handleChange}>
|
|
63
61
|
<slot />
|
|
64
62
|
</Checkbox>
|
|
65
63
|
{#if $$slots.secondary}
|
|
@@ -80,6 +78,7 @@
|
|
|
80
78
|
border-radius: var(--border-radius-medium);
|
|
81
79
|
cursor: pointer;
|
|
82
80
|
min-height: 24px;
|
|
81
|
+
user-select: none;
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
.checkbox-card:hover {
|
|
@@ -30,9 +30,12 @@
|
|
|
30
30
|
/** Whether to center vertically */
|
|
31
31
|
export let centered = true;
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
/** ARIA role: "status" for info messages, "alert" for errors */
|
|
34
|
+
export let role = "status";
|
|
35
|
+
|
|
36
|
+
let className = "";
|
|
34
37
|
export { className as class };
|
|
35
|
-
|
|
38
|
+
|
|
36
39
|
$: normalizedActions = actions ? actions : action ? [action] : null;
|
|
37
40
|
</script>
|
|
38
41
|
|
|
@@ -41,9 +44,10 @@
|
|
|
41
44
|
class:centered
|
|
42
45
|
class:small={size === "small"}
|
|
43
46
|
class:large={size === "large"}
|
|
47
|
+
{role}
|
|
44
48
|
>
|
|
45
49
|
{#if icon}
|
|
46
|
-
<div class="empty-state__icon">
|
|
50
|
+
<div class="empty-state__icon" aria-hidden="true">
|
|
47
51
|
{#if typeof icon === "string"}
|
|
48
52
|
<Icon iconName={icon} />
|
|
49
53
|
{:else}
|
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { Label } from "figma-ui3-kit-svelte";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Field group wrapper
|
|
6
|
-
* Wraps a form field with an optional label
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* <FieldGroup label="Collection">
|
|
10
|
-
* <Dropdown menuItems={options} bind:value={selected} />
|
|
11
|
-
* </FieldGroup>
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
4
|
/** Label text (optional) */
|
|
15
5
|
export let label = "";
|
|
16
6
|
|
|
17
|
-
/**
|
|
7
|
+
/** id of the associated control (optional) */
|
|
18
8
|
export let labelFor = "";
|
|
19
9
|
|
|
20
10
|
/** Size of the label (optional) */
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
export let className = "";
|
|
32
32
|
</script>
|
|
33
33
|
|
|
34
|
-
<
|
|
34
|
+
<footer class="footer footer--{variant} {className}">
|
|
35
35
|
{#if variant === "right"}
|
|
36
36
|
<div class="footer__right">
|
|
37
37
|
<slot />
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
{:else if variant === "full"}
|
|
47
47
|
<slot />
|
|
48
48
|
{/if}
|
|
49
|
-
</
|
|
49
|
+
</footer>
|
|
50
50
|
|
|
51
51
|
<style>
|
|
52
52
|
.footer {
|
|
@@ -103,6 +103,10 @@
|
|
|
103
103
|
width: 100%;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
.footer--full :global(> *) {
|
|
107
|
+
flex: 1;
|
|
108
|
+
}
|
|
109
|
+
|
|
106
110
|
.footer--full :global(button) {
|
|
107
111
|
flex: 1;
|
|
108
112
|
width: 100%;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
export let noBorder = false;
|
|
12
12
|
</script>
|
|
13
13
|
|
|
14
|
-
<
|
|
14
|
+
<header
|
|
15
15
|
class="header {className}"
|
|
16
16
|
class:has-left-content={$$slots.left}
|
|
17
17
|
class:no-border={noBorder}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<div class="header__left">
|
|
20
20
|
<slot name="left" />
|
|
21
21
|
{#if title}
|
|
22
|
-
<
|
|
22
|
+
<h1 class="header__title">{title}</h1>
|
|
23
23
|
{/if}
|
|
24
24
|
</div>
|
|
25
25
|
<div class="header__center">
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
<div class="header__right">
|
|
29
29
|
<slot name="right" />
|
|
30
30
|
</div>
|
|
31
|
-
</
|
|
31
|
+
</header>
|
|
32
32
|
|
|
33
33
|
<style>
|
|
34
34
|
.header {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { createEventDispatcher } from "svelte";
|
|
3
|
-
import { IconButton,
|
|
3
|
+
import { IconButton, Menu } from "figma-ui3-kit-svelte";
|
|
4
|
+
import { IconMore } from "figma-ui3-kit-svelte/icons";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* List item with optional action menu
|
|
@@ -44,6 +45,9 @@
|
|
|
44
45
|
/** Whether to show badge slot */
|
|
45
46
|
export let hasBadge = false;
|
|
46
47
|
|
|
48
|
+
let className = "";
|
|
49
|
+
export { className as class };
|
|
50
|
+
|
|
47
51
|
function handleClick() {
|
|
48
52
|
dispatch("click", { id });
|
|
49
53
|
}
|
|
@@ -65,14 +69,20 @@
|
|
|
65
69
|
}
|
|
66
70
|
</script>
|
|
67
71
|
|
|
68
|
-
<div class="list-item-wrapper">
|
|
72
|
+
<div class="list-item-wrapper {className}">
|
|
69
73
|
<div
|
|
70
74
|
class="list-item"
|
|
71
75
|
class:active
|
|
72
76
|
on:click={handleClick}
|
|
73
|
-
on:keydown={(e) =>
|
|
77
|
+
on:keydown={(e) => {
|
|
78
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
handleClick();
|
|
81
|
+
}
|
|
82
|
+
}}
|
|
74
83
|
role="button"
|
|
75
84
|
tabindex="0"
|
|
85
|
+
aria-pressed={active}
|
|
76
86
|
>
|
|
77
87
|
<div class="list-item__content">
|
|
78
88
|
<div class="list-item__title">{title}</div>
|
|
@@ -92,6 +102,7 @@
|
|
|
92
102
|
{#if menuItems.length > 0}
|
|
93
103
|
<IconButton
|
|
94
104
|
iconName={IconMore}
|
|
105
|
+
ariaLabel="{title} options"
|
|
95
106
|
bind:element={menuButtonElement}
|
|
96
107
|
on:click={handleMenuToggle}
|
|
97
108
|
/>
|
|
@@ -15,10 +15,15 @@
|
|
|
15
15
|
|
|
16
16
|
/** Message to display */
|
|
17
17
|
export let message = "Loading...";
|
|
18
|
+
|
|
19
|
+
let className = "";
|
|
20
|
+
export { className as class };
|
|
18
21
|
</script>
|
|
19
22
|
|
|
20
|
-
<div class="loading-state">
|
|
21
|
-
<Text variant="body-medium" color="--figma-color-text-secondary"
|
|
23
|
+
<div class="loading-state {className}" role="status">
|
|
24
|
+
<Text variant="body-medium" color="--figma-color-text-secondary"
|
|
25
|
+
>{message}</Text
|
|
26
|
+
>
|
|
22
27
|
</div>
|
|
23
28
|
|
|
24
29
|
<style>
|
|
@@ -6,9 +6,7 @@
|
|
|
6
6
|
* @example
|
|
7
7
|
* <PluginLayout>
|
|
8
8
|
* <p>Main content here</p>
|
|
9
|
-
* <
|
|
10
|
-
* <Button variant="primary">Create item</Button>
|
|
11
|
-
* </svelte:fragment>
|
|
9
|
+
* <Button variant="primary">Create item</Button>
|
|
12
10
|
* </PluginLayout>
|
|
13
11
|
*/
|
|
14
12
|
|
|
@@ -40,4 +38,8 @@
|
|
|
40
38
|
overflow-y: auto;
|
|
41
39
|
padding: var(--size-xsmall);
|
|
42
40
|
}
|
|
41
|
+
|
|
42
|
+
.plugin-footer {
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
}
|
|
43
45
|
</style>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { onDestroy, createEventDispatcher } from "svelte";
|
|
3
|
-
import { IconButton
|
|
3
|
+
import { IconButton } from "figma-ui3-kit-svelte";
|
|
4
|
+
import { IconClose } from "figma-ui3-kit-svelte/icons";
|
|
4
5
|
|
|
5
6
|
// Status bar for notifications with auto-dismiss
|
|
6
7
|
// Supports types: 'info', 'success', 'error', 'warning'
|
|
@@ -14,6 +15,9 @@
|
|
|
14
15
|
/** Status type: 'info', 'success', 'error', 'warning' */
|
|
15
16
|
export let type = "info";
|
|
16
17
|
|
|
18
|
+
let className = "";
|
|
19
|
+
export { className as class };
|
|
20
|
+
|
|
17
21
|
let visible = false;
|
|
18
22
|
let timeoutId;
|
|
19
23
|
|
|
@@ -53,14 +57,16 @@
|
|
|
53
57
|
|
|
54
58
|
{#if visible && message}
|
|
55
59
|
<div
|
|
56
|
-
class="status-bar"
|
|
60
|
+
class="status-bar {className}"
|
|
57
61
|
class:status-bar--error={type === "error"}
|
|
58
62
|
class:status-bar--success={type === "success"}
|
|
59
63
|
class:status-bar--warning={type === "warning"}
|
|
64
|
+
role={type === "error" || type === "warning" ? "alert" : "status"}
|
|
60
65
|
>
|
|
61
66
|
<span>{message}</span>
|
|
62
67
|
<IconButton
|
|
63
68
|
iconName={IconClose}
|
|
69
|
+
ariaLabel="Dismiss"
|
|
64
70
|
on:click={handleClose}
|
|
65
71
|
iconColor={computedIconColor}
|
|
66
72
|
/>
|
package/src/index.js
CHANGED
package/src/lib/colors.js
CHANGED
|
@@ -39,7 +39,8 @@ export function hexToRgb(hex) {
|
|
|
39
39
|
* @returns {number} Relative luminance (0-1)
|
|
40
40
|
*/
|
|
41
41
|
export function getLuminance({ r, g, b }) {
|
|
42
|
-
const adjust = (c) =>
|
|
42
|
+
const adjust = (c) =>
|
|
43
|
+
c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
43
44
|
return 0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b);
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -60,13 +61,15 @@ export function getContrastRatio(color1, color2) {
|
|
|
60
61
|
/**
|
|
61
62
|
* Check if contrast ratio meets WCAG level
|
|
62
63
|
* @param {number} ratio - Contrast ratio
|
|
63
|
-
* @param {"AA" | "AAA" | "AA-large"} level - WCAG level to check
|
|
64
|
+
* @param {"AA" | "AAA" | "AA-large" | "AAA-large"} level - WCAG level to check
|
|
64
65
|
* @returns {boolean} Whether the ratio meets the level
|
|
65
66
|
*/
|
|
66
67
|
export function meetsContrastLevel(ratio, level) {
|
|
67
68
|
switch (level) {
|
|
68
69
|
case "AAA":
|
|
69
70
|
return ratio >= 7;
|
|
71
|
+
case "AAA-large":
|
|
72
|
+
return ratio >= 4.5;
|
|
70
73
|
case "AA":
|
|
71
74
|
return ratio >= 4.5;
|
|
72
75
|
case "AA-large":
|
package/src/lib/figma-helpers.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export function sendToUI<T extends Record<string, unknown>>(
|
|
11
11
|
type: string,
|
|
12
|
-
data?: T
|
|
12
|
+
data?: T,
|
|
13
13
|
): void {
|
|
14
14
|
if (data) {
|
|
15
15
|
figma.ui.postMessage({ type, ...data });
|
|
@@ -32,7 +32,7 @@ export async function getCollections(): Promise<VariableCollection[]> {
|
|
|
32
32
|
* @returns Promise resolving to array of variables
|
|
33
33
|
*/
|
|
34
34
|
export async function getVariables(
|
|
35
|
-
type?: VariableResolvedDataType
|
|
35
|
+
type?: VariableResolvedDataType,
|
|
36
36
|
): Promise<Variable[]> {
|
|
37
37
|
return figma.variables.getLocalVariablesAsync(type);
|
|
38
38
|
}
|
|
@@ -61,7 +61,7 @@ export function showSuccess(message: string, timeout = 3000): void {
|
|
|
61
61
|
* @returns Array of selected nodes
|
|
62
62
|
*/
|
|
63
63
|
export function getSelection<T extends SceneNode>(
|
|
64
|
-
nodeType?: NodeType
|
|
64
|
+
nodeType?: NodeType,
|
|
65
65
|
): readonly T[] {
|
|
66
66
|
const selection = figma.currentPage.selection;
|
|
67
67
|
if (nodeType) {
|
|
@@ -85,10 +85,7 @@ export function focusNodes(nodes: readonly SceneNode[]): void {
|
|
|
85
85
|
* @param family - Font family name
|
|
86
86
|
* @param style - Font style (e.g., "Regular", "Bold")
|
|
87
87
|
*/
|
|
88
|
-
export async function loadFont(
|
|
89
|
-
family: string,
|
|
90
|
-
style: string
|
|
91
|
-
): Promise<void> {
|
|
88
|
+
export async function loadFont(family: string, style: string): Promise<void> {
|
|
92
89
|
await figma.loadFontAsync({ family, style });
|
|
93
90
|
}
|
|
94
91
|
|
|
@@ -109,7 +106,7 @@ export async function saveToStorage<T>(key: string, value: T): Promise<void> {
|
|
|
109
106
|
*/
|
|
110
107
|
export async function loadFromStorage<T>(
|
|
111
108
|
key: string,
|
|
112
|
-
defaultValue?: T
|
|
109
|
+
defaultValue?: T,
|
|
113
110
|
): Promise<T | undefined> {
|
|
114
111
|
try {
|
|
115
112
|
const value = await figma.clientStorage.getAsync(key);
|
package/src/lib/index.js
CHANGED
package/src/lib/resize.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Auto-resize utilities for Figma plugin windows
|
|
3
3
|
*
|
|
4
4
|
* Usage in UI:
|
|
5
|
-
* import { resizeToFit, autoResize } from "figma-plugin-
|
|
5
|
+
* import { resizeToFit, autoResize } from "figma-plugin-utilities";
|
|
6
6
|
* resizeToFit(); // One-time resize
|
|
7
7
|
* autoResize(); // Watch for changes and auto-resize
|
|
8
8
|
*
|
|
9
9
|
* Usage in code.ts:
|
|
10
|
-
* import { handleResize } from "figma-plugin-
|
|
10
|
+
* import { handleResize } from "figma-plugin-utilities/lib/figma-helpers";
|
|
11
11
|
* // In your message handler:
|
|
12
12
|
* if (msg.type === "resize") handleResize(msg);
|
|
13
13
|
*/
|
|
@@ -73,10 +73,10 @@ export function resizeToFit(options = {}) {
|
|
|
73
73
|
/**
|
|
74
74
|
* Set up automatic resizing when content changes
|
|
75
75
|
* Uses ResizeObserver to watch for size changes
|
|
76
|
-
*
|
|
76
|
+
*
|
|
77
77
|
* IMPORTANT: The container element must NOT have height: 100% or fixed height.
|
|
78
78
|
* Use bind:this on a wrapper element that flows naturally with content.
|
|
79
|
-
*
|
|
79
|
+
*
|
|
80
80
|
* @param {object} options - Auto-resize options
|
|
81
81
|
* @param {HTMLElement} options.container - Container element to observe (required, must not have fixed height)
|
|
82
82
|
* @param {number} [options.width] - Width in pixels (uses default if not specified)
|
package/src/lib/validation.js
CHANGED
|
@@ -58,7 +58,7 @@ export function validateUrl(url, options = { required: true }) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
return { valid: true };
|
|
61
|
-
} catch
|
|
61
|
+
} catch {
|
|
62
62
|
return {
|
|
63
63
|
valid: false,
|
|
64
64
|
error: "Invalid URL format",
|
|
@@ -148,6 +148,7 @@ export function sanitizeInput(input, maxLength) {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// Remove null bytes and control characters (except newlines and tabs)
|
|
151
|
+
// eslint-disable-next-line no-control-regex
|
|
151
152
|
str = str.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, "");
|
|
152
153
|
|
|
153
154
|
// Trim whitespace
|