figma-plugin-utilities 0.1.0 → 0.3.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 +52 -0
- package/LICENSE +21 -0
- package/README.md +122 -9
- package/package.json +11 -3
- package/src/components/CheckboxCard.svelte +108 -0
- package/src/components/EmptyState.svelte +4 -2
- package/src/components/FieldGroup.svelte +13 -2
- package/src/components/Footer.svelte +2 -2
- package/src/components/Header.svelte +3 -3
- package/src/components/ListItem.svelte +7 -2
- package/src/components/LoadingState.svelte +4 -1
- package/src/components/PluginLayout.svelte +5 -3
- package/src/components/StatusBar.svelte +6 -1
- package/src/components/index.js +1 -0
- package/src/index.js +4 -0
- package/src/lib/colors.js +3 -1
- package/src/lib/resize.js +2 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
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.0] - 2026-05-06
|
|
11
|
+
|
|
12
|
+
WCAG 2.2 AA accessibility audit and remediation across all components.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `class` prop passthrough to **ListItem**, **LoadingState**, and **StatusBar** — consistent with other components
|
|
16
|
+
- `role="alert"` for error/warning and `role="status"` for info/success on **StatusBar** — messages are now announced by screen readers on insertion
|
|
17
|
+
- `"AAA-large"` case (4.5:1) to `meetsContrastLevel` in `lib/colors.js` — covers WCAG 1.4.6 large text at AAA level
|
|
18
|
+
- GitHub Actions publish workflow (`.github/workflows/publish.yml`) — triggers `npm publish` on GitHub release creation
|
|
19
|
+
- `aria-pressed={active}` to **ListItem** — communicates selection state to assistive technology
|
|
20
|
+
- Space key activation to **ListItem** — keyboard users can now toggle items with Space as well as Enter
|
|
21
|
+
- `ariaLabel="{title} options"` to the **ListItem** menu `IconButton` — gives the icon-only button an accessible name
|
|
22
|
+
- `role="status"` to **LoadingState** — loading message is announced as a polite live region
|
|
23
|
+
- `aria-hidden="true"` to the decorative icon in **EmptyState** — prevents redundant AT announcement
|
|
24
|
+
- `aria-disabled` attribute to **CheckboxCard** — reflects disabled state without removing from the accessibility tree
|
|
25
|
+
- Dev-mode `console.warn` to **FieldGroup** when `label` is provided but `labelFor` is empty
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- **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)
|
|
29
|
+
- **Footer**: outer element changed from `<div>` to `<footer>`
|
|
30
|
+
- **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
|
|
31
|
+
- **CheckboxCard**: added `user-select: none` to prevent text selection on double-click
|
|
32
|
+
|
|
33
|
+
## [0.1.0] - 2026-04-17
|
|
34
|
+
|
|
35
|
+
Initial release as `figma-plugin-utilities`.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- **PluginLayout** — main content wrapper with scrollable area
|
|
39
|
+
- **Header** — header bar with left/center/right slots and optional title
|
|
40
|
+
- **Footer** — footer bar with right, split, and full layout variants
|
|
41
|
+
- **StatusBar** — toast-style notifications with auto-dismiss for info/success types
|
|
42
|
+
- **EmptyState** — empty/error state display with optional icon and action buttons
|
|
43
|
+
- **ListItem** — selectable list item with metadata slot and context menu
|
|
44
|
+
- **LoadingState** — centered loading indicator
|
|
45
|
+
- **FieldGroup** — label + input wrapper for form fields
|
|
46
|
+
- **CheckboxCard** — large-target checkbox with card styling
|
|
47
|
+
- `lib/messages.js` — `sendToPlugin` and `createMessageHandler`
|
|
48
|
+
- `lib/colors.js` — `rgbToHex`, `hexToRgb`, `getLuminance`, `getContrastRatio`, `meetsContrastLevel`
|
|
49
|
+
- `lib/validation.js` — `validateUrl`, `validateJsonString`, `validateEmail`, `validateNumber`, `sanitizeName`, `sanitizeInput`, `isEmpty`
|
|
50
|
+
- `lib/errorHandling.js` — `safeAsync`, `parseJsonSafe`, `notifyError`, `notifySuccess`, `notifyWarning`, and more
|
|
51
|
+
- `lib/resize.js` — `resizeToFit`, `autoResize`, `setDefaultWidth`, `getContentHeight`
|
|
52
|
+
- `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
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# figma-plugin-
|
|
1
|
+
# figma-plugin-utilities
|
|
2
2
|
|
|
3
3
|
Shared Svelte components and utilities for Figma plugins.
|
|
4
4
|
|
|
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
|
|
@@ -23,25 +23,46 @@ import {
|
|
|
23
23
|
ListItem,
|
|
24
24
|
LoadingState,
|
|
25
25
|
FieldGroup,
|
|
26
|
-
|
|
26
|
+
CheckboxCard,
|
|
27
|
+
// Messages
|
|
27
28
|
sendToPlugin,
|
|
28
29
|
createMessageHandler,
|
|
30
|
+
// Colors
|
|
29
31
|
rgbToHex,
|
|
30
32
|
hexToRgb,
|
|
33
|
+
getLuminance,
|
|
34
|
+
getContrastRatio,
|
|
35
|
+
meetsContrastLevel,
|
|
36
|
+
// Validation
|
|
31
37
|
validateUrl,
|
|
32
38
|
validateJsonString,
|
|
39
|
+
validateEmail,
|
|
40
|
+
validateNumber,
|
|
33
41
|
sanitizeName,
|
|
34
|
-
|
|
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";
|
|
35
56
|
```
|
|
36
57
|
|
|
37
58
|
### Import Specific Modules
|
|
38
59
|
|
|
39
60
|
```javascript
|
|
40
61
|
// Components only
|
|
41
|
-
import { PluginLayout, Header, Footer } from "figma-plugin-
|
|
62
|
+
import { PluginLayout, Header, Footer } from "figma-plugin-utilities/components";
|
|
42
63
|
|
|
43
64
|
// Utilities only
|
|
44
|
-
import { sendToPlugin, createMessageHandler } from "figma-plugin-
|
|
65
|
+
import { sendToPlugin, createMessageHandler } from "figma-plugin-utilities/lib";
|
|
45
66
|
```
|
|
46
67
|
|
|
47
68
|
## Components
|
|
@@ -56,6 +77,7 @@ import { sendToPlugin, createMessageHandler } from "figma-plugin-utils/lib";
|
|
|
56
77
|
| `ListItem` | Selectable list items with metadata slot and action menu |
|
|
57
78
|
| `LoadingState` | Centered loading indicator with custom message |
|
|
58
79
|
| `FieldGroup` | Label + input wrapper for form fields |
|
|
80
|
+
| `CheckboxCard` | Large checkbox with card styling and better touch targets |
|
|
59
81
|
|
|
60
82
|
### Header
|
|
61
83
|
|
|
@@ -140,6 +162,37 @@ Types: `info`, `success`, `error`, `warning`. Auto-dismisses after 4s for `info`
|
|
|
140
162
|
</ListItem>
|
|
141
163
|
```
|
|
142
164
|
|
|
165
|
+
### CheckboxCard
|
|
166
|
+
|
|
167
|
+
Large checkbox with card-style background and better touch targets.
|
|
168
|
+
|
|
169
|
+
```svelte
|
|
170
|
+
<!-- Basic usage -->
|
|
171
|
+
<CheckboxCard
|
|
172
|
+
checked={isSelected}
|
|
173
|
+
on:change={handleToggle}
|
|
174
|
+
>
|
|
175
|
+
Small
|
|
176
|
+
</CheckboxCard>
|
|
177
|
+
|
|
178
|
+
<!-- With secondary text -->
|
|
179
|
+
<CheckboxCard
|
|
180
|
+
checked={isSelected}
|
|
181
|
+
on:change={handleToggle}
|
|
182
|
+
>
|
|
183
|
+
Small
|
|
184
|
+
<svelte:fragment slot="secondary">400px</svelte:fragment>
|
|
185
|
+
</CheckboxCard>
|
|
186
|
+
|
|
187
|
+
<!-- Disabled state -->
|
|
188
|
+
<CheckboxCard
|
|
189
|
+
checked={true}
|
|
190
|
+
disabled={true}
|
|
191
|
+
>
|
|
192
|
+
Large
|
|
193
|
+
</CheckboxCard>
|
|
194
|
+
```
|
|
195
|
+
|
|
143
196
|
## Utilities
|
|
144
197
|
|
|
145
198
|
### Messages (`lib/messages.js`)
|
|
@@ -162,7 +215,8 @@ window.onmessage = createMessageHandler({
|
|
|
162
215
|
const rgb = hexToRgb("#FF0000"); // { r: 1, g: 0, b: 0 }
|
|
163
216
|
const hex = rgbToHex({ r: 1, g: 0, b: 0 }); // "#FF0000"
|
|
164
217
|
|
|
165
|
-
//
|
|
218
|
+
// Contrast utilities
|
|
219
|
+
const luminance = getLuminance({ r: 1, g: 0, b: 0 });
|
|
166
220
|
const ratio = getContrastRatio(color1, color2);
|
|
167
221
|
const passes = meetsContrastLevel(ratio, "AA"); // true/false
|
|
168
222
|
```
|
|
@@ -176,7 +230,12 @@ const urlResult = validateUrl("https://example.com");
|
|
|
176
230
|
const jsonResult = validateJsonString('{"key": "value"}');
|
|
177
231
|
// { valid: true, parsed: {...} } or { valid: false, error: "..." }
|
|
178
232
|
|
|
233
|
+
validateEmail("user@example.com"); // { valid: true }
|
|
234
|
+
validateNumber("42", { min: 0, max: 100 }); // { valid: true, value: 42 }
|
|
235
|
+
|
|
179
236
|
const clean = sanitizeName("My Plugin!!!"); // "My Plugin"
|
|
237
|
+
sanitizeInput("<script>alert(1)</script>"); // escaped string
|
|
238
|
+
isEmpty(""); // true
|
|
180
239
|
```
|
|
181
240
|
|
|
182
241
|
### Error Handling (`lib/errorHandling.js`)
|
|
@@ -196,20 +255,71 @@ if (result.ok) {
|
|
|
196
255
|
// Parse JSON safely
|
|
197
256
|
const parsed = parseJsonSafe(jsonString);
|
|
198
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");
|
|
263
|
+
```
|
|
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);
|
|
199
286
|
```
|
|
200
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
|
+
|
|
201
290
|
### Figma Helpers (`lib/figma-helpers.ts`)
|
|
202
291
|
|
|
203
292
|
For use in `code.ts`:
|
|
204
293
|
|
|
205
294
|
```typescript
|
|
206
|
-
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";
|
|
207
308
|
|
|
208
309
|
// Send message to UI
|
|
209
310
|
sendToUI("success", { message: "Done!" });
|
|
210
311
|
|
|
211
|
-
// Show
|
|
312
|
+
// Show notifications
|
|
212
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
|
|
213
323
|
|
|
214
324
|
// Focus viewport on nodes
|
|
215
325
|
focusNodes(figma.currentPage.selection);
|
|
@@ -220,4 +330,7 @@ await loadFont("Inter", "Regular");
|
|
|
220
330
|
// Client storage
|
|
221
331
|
await saveToStorage("settings", { theme: "dark" });
|
|
222
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);
|
|
223
336
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-plugin-utilities",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Shared Svelte components and utilities for Figma plugins",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./src/index.js",
|
|
@@ -21,13 +21,21 @@
|
|
|
21
21
|
"import": "./src/lib/index.js",
|
|
22
22
|
"default": "./src/lib/index.js"
|
|
23
23
|
},
|
|
24
|
+
"./lib/*": {
|
|
25
|
+
"import": "./src/lib/*.js"
|
|
26
|
+
},
|
|
24
27
|
"./lib/figma-helpers": {
|
|
25
28
|
"types": "./src/lib/figma-helpers.ts",
|
|
26
29
|
"import": "./src/lib/figma-helpers.ts",
|
|
27
30
|
"default": "./src/lib/figma-helpers.ts"
|
|
28
31
|
}
|
|
29
32
|
},
|
|
30
|
-
"files": [
|
|
33
|
+
"files": [
|
|
34
|
+
"src",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"CHANGELOG.md"
|
|
38
|
+
],
|
|
31
39
|
"repository": {
|
|
32
40
|
"type": "git",
|
|
33
41
|
"url": "git+https://github.com/mariusroosendaal/figma-plugin-utilities.git"
|
|
@@ -47,7 +55,7 @@
|
|
|
47
55
|
"author": "Marius Roosendaal",
|
|
48
56
|
"license": "MIT",
|
|
49
57
|
"dependencies": {
|
|
50
|
-
"figma-ui3-kit-svelte": "^0.
|
|
58
|
+
"figma-ui3-kit-svelte": "^0.5.0",
|
|
51
59
|
"svelte": "^4.2.20"
|
|
52
60
|
}
|
|
53
61
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { createEventDispatcher } from "svelte";
|
|
3
|
+
import { Checkbox } from "figma-ui3-kit-svelte";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Large checkbox card component with better touch targets
|
|
7
|
+
* Wraps the standard checkbox in a card-like layout
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* <CheckboxCard
|
|
11
|
+
* checked={isSelected}
|
|
12
|
+
* on:change={handleToggle}
|
|
13
|
+
* >
|
|
14
|
+
* Small
|
|
15
|
+
* </CheckboxCard>
|
|
16
|
+
*
|
|
17
|
+
* @example with secondary text
|
|
18
|
+
* <CheckboxCard
|
|
19
|
+
* checked={isSelected}
|
|
20
|
+
* on:change={handleToggle}
|
|
21
|
+
* >
|
|
22
|
+
* Small
|
|
23
|
+
* <svelte:fragment slot="secondary">400px</svelte:fragment>
|
|
24
|
+
* </CheckboxCard>
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const dispatch = createEventDispatcher();
|
|
28
|
+
|
|
29
|
+
/** Whether checkbox is checked */
|
|
30
|
+
export let checked = false;
|
|
31
|
+
|
|
32
|
+
/** Whether checkbox is disabled */
|
|
33
|
+
export let disabled = false;
|
|
34
|
+
|
|
35
|
+
let cardEl;
|
|
36
|
+
|
|
37
|
+
function handleChange(e) {
|
|
38
|
+
if (disabled) return;
|
|
39
|
+
checked = e.target.checked;
|
|
40
|
+
dispatch("change", { checked });
|
|
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
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
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"
|
|
55
|
+
class:disabled
|
|
56
|
+
aria-disabled={disabled || undefined}
|
|
57
|
+
bind:this={cardEl}
|
|
58
|
+
on:click={handleCardClick}
|
|
59
|
+
>
|
|
60
|
+
<Checkbox {checked} {disabled} on:change={handleChange}>
|
|
61
|
+
<slot />
|
|
62
|
+
</Checkbox>
|
|
63
|
+
{#if $$slots.secondary}
|
|
64
|
+
<div class="checkbox-card__secondary">
|
|
65
|
+
<slot name="secondary" />
|
|
66
|
+
</div>
|
|
67
|
+
{/if}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<style>
|
|
71
|
+
.checkbox-card {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: space-between;
|
|
75
|
+
padding: 0 var(--size-xxxsmall);
|
|
76
|
+
background: var(--figma-color-bg-secondary);
|
|
77
|
+
border: 1px solid transparent;
|
|
78
|
+
border-radius: var(--border-radius-medium);
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
min-height: 24px;
|
|
81
|
+
user-select: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.checkbox-card:hover {
|
|
85
|
+
border-color: var(--figma-color-border);
|
|
86
|
+
background: var(--figma-color-bg-secondary);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.checkbox-card.disabled {
|
|
90
|
+
opacity: 0.4;
|
|
91
|
+
cursor: not-allowed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Custom checkbox styling for card variant */
|
|
95
|
+
.checkbox-card :global(.checkbox-box) {
|
|
96
|
+
background-color: var(--figma-color-bg);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.checkbox-card__secondary {
|
|
100
|
+
font-size: var(--body-small-font-size);
|
|
101
|
+
font-weight: var(--body-small-font-weight);
|
|
102
|
+
letter-spacing: var(--body-small-letter-spacing);
|
|
103
|
+
line-height: var(--body-small-line-height);
|
|
104
|
+
color: var(--figma-color-text-secondary);
|
|
105
|
+
margin-left: auto;
|
|
106
|
+
padding-left: var(--size-xsmall);
|
|
107
|
+
}
|
|
108
|
+
</style>
|
|
@@ -30,18 +30,20 @@
|
|
|
30
30
|
/** Whether to center vertically */
|
|
31
31
|
export let centered = true;
|
|
32
32
|
|
|
33
|
+
let className = '';
|
|
34
|
+
export { className as class };
|
|
33
35
|
// Normalize actions
|
|
34
36
|
$: normalizedActions = actions ? actions : action ? [action] : null;
|
|
35
37
|
</script>
|
|
36
38
|
|
|
37
39
|
<div
|
|
38
|
-
class="empty-state"
|
|
40
|
+
class="empty-state {className}"
|
|
39
41
|
class:centered
|
|
40
42
|
class:small={size === "small"}
|
|
41
43
|
class:large={size === "large"}
|
|
42
44
|
>
|
|
43
45
|
{#if icon}
|
|
44
|
-
<div class="empty-state__icon">
|
|
46
|
+
<div class="empty-state__icon" aria-hidden="true">
|
|
45
47
|
{#if typeof icon === "string"}
|
|
46
48
|
<Icon iconName={icon} />
|
|
47
49
|
{:else}
|
|
@@ -16,11 +16,18 @@
|
|
|
16
16
|
|
|
17
17
|
/** For attribute for the label (optional) */
|
|
18
18
|
export let labelFor = "";
|
|
19
|
+
|
|
20
|
+
/** Size of the label (optional) */
|
|
21
|
+
export let size = undefined;
|
|
22
|
+
|
|
23
|
+
$: if (typeof window !== "undefined" && label && !labelFor) {
|
|
24
|
+
console.warn("[FieldGroup] A label is rendered but no labelFor is set. Associate the label with its control using the labelFor prop.");
|
|
25
|
+
}
|
|
19
26
|
</script>
|
|
20
27
|
|
|
21
|
-
<div class="field-group">
|
|
28
|
+
<div class="field-group" class:small={size === "small"}>
|
|
22
29
|
{#if label}
|
|
23
|
-
<Label for={labelFor}>{label}</Label>
|
|
30
|
+
<Label for={labelFor} {size}>{label}</Label>
|
|
24
31
|
{/if}
|
|
25
32
|
<slot />
|
|
26
33
|
</div>
|
|
@@ -31,4 +38,8 @@
|
|
|
31
38
|
flex-direction: column;
|
|
32
39
|
gap: var(--size-xxsmall);
|
|
33
40
|
}
|
|
41
|
+
|
|
42
|
+
.field-group.small {
|
|
43
|
+
gap: var(--size-xxxsmall);
|
|
44
|
+
}
|
|
34
45
|
</style>
|
|
@@ -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 {
|
|
@@ -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 {
|
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
/** Whether to show badge slot */
|
|
45
45
|
export let hasBadge = false;
|
|
46
46
|
|
|
47
|
+
let className = "";
|
|
48
|
+
export { className as class };
|
|
49
|
+
|
|
47
50
|
function handleClick() {
|
|
48
51
|
dispatch("click", { id });
|
|
49
52
|
}
|
|
@@ -65,14 +68,15 @@
|
|
|
65
68
|
}
|
|
66
69
|
</script>
|
|
67
70
|
|
|
68
|
-
<div class="list-item-wrapper">
|
|
71
|
+
<div class="list-item-wrapper {className}">
|
|
69
72
|
<div
|
|
70
73
|
class="list-item"
|
|
71
74
|
class:active
|
|
72
75
|
on:click={handleClick}
|
|
73
|
-
on:keydown={(e) => e.key === "Enter"
|
|
76
|
+
on:keydown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleClick(); } }}
|
|
74
77
|
role="button"
|
|
75
78
|
tabindex="0"
|
|
79
|
+
aria-pressed={active}
|
|
76
80
|
>
|
|
77
81
|
<div class="list-item__content">
|
|
78
82
|
<div class="list-item__title">{title}</div>
|
|
@@ -92,6 +96,7 @@
|
|
|
92
96
|
{#if menuItems.length > 0}
|
|
93
97
|
<IconButton
|
|
94
98
|
iconName={IconMore}
|
|
99
|
+
ariaLabel="{title} options"
|
|
95
100
|
bind:element={menuButtonElement}
|
|
96
101
|
on:click={handleMenuToggle}
|
|
97
102
|
/>
|
|
@@ -15,9 +15,12 @@
|
|
|
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">
|
|
23
|
+
<div class="loading-state {className}" role="status">
|
|
21
24
|
<Text variant="body-medium" color="--figma-color-text-secondary">{message}</Text>
|
|
22
25
|
</div>
|
|
23
26
|
|
|
@@ -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>
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
/** Status type: 'info', 'success', 'error', 'warning' */
|
|
15
15
|
export let type = "info";
|
|
16
16
|
|
|
17
|
+
let className = "";
|
|
18
|
+
export { className as class };
|
|
19
|
+
|
|
17
20
|
let visible = false;
|
|
18
21
|
let timeoutId;
|
|
19
22
|
|
|
@@ -53,14 +56,16 @@
|
|
|
53
56
|
|
|
54
57
|
{#if visible && message}
|
|
55
58
|
<div
|
|
56
|
-
class="status-bar"
|
|
59
|
+
class="status-bar {className}"
|
|
57
60
|
class:status-bar--error={type === "error"}
|
|
58
61
|
class:status-bar--success={type === "success"}
|
|
59
62
|
class:status-bar--warning={type === "warning"}
|
|
63
|
+
role={type === "error" || type === "warning" ? "alert" : "status"}
|
|
60
64
|
>
|
|
61
65
|
<span>{message}</span>
|
|
62
66
|
<IconButton
|
|
63
67
|
iconName={IconClose}
|
|
68
|
+
ariaLabel="Dismiss"
|
|
64
69
|
on:click={handleClose}
|
|
65
70
|
iconColor={computedIconColor}
|
|
66
71
|
/>
|
package/src/components/index.js
CHANGED
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Re-export all components
|
|
2
2
|
export {
|
|
3
|
+
CheckboxCard,
|
|
3
4
|
EmptyState,
|
|
4
5
|
FieldGroup,
|
|
5
6
|
Footer,
|
|
@@ -37,6 +38,9 @@ export {
|
|
|
37
38
|
withErrorHandling,
|
|
38
39
|
safeAsync,
|
|
39
40
|
parseJsonSafe,
|
|
41
|
+
notifyError,
|
|
42
|
+
notifySuccess,
|
|
43
|
+
notifyWarning,
|
|
40
44
|
// Resize
|
|
41
45
|
setDefaultWidth,
|
|
42
46
|
getContentHeight,
|
package/src/lib/colors.js
CHANGED
|
@@ -60,13 +60,15 @@ export function getContrastRatio(color1, color2) {
|
|
|
60
60
|
/**
|
|
61
61
|
* Check if contrast ratio meets WCAG level
|
|
62
62
|
* @param {number} ratio - Contrast ratio
|
|
63
|
-
* @param {"AA" | "AAA" | "AA-large"} level - WCAG level to check
|
|
63
|
+
* @param {"AA" | "AAA" | "AA-large" | "AAA-large"} level - WCAG level to check
|
|
64
64
|
* @returns {boolean} Whether the ratio meets the level
|
|
65
65
|
*/
|
|
66
66
|
export function meetsContrastLevel(ratio, level) {
|
|
67
67
|
switch (level) {
|
|
68
68
|
case "AAA":
|
|
69
69
|
return ratio >= 7;
|
|
70
|
+
case "AAA-large":
|
|
71
|
+
return ratio >= 4.5;
|
|
70
72
|
case "AA":
|
|
71
73
|
return ratio >= 4.5;
|
|
72
74
|
case "AA-large":
|
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
|
*/
|