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 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-ulities
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
- // Utilities
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
- } from "figma-plugin-utils";
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-utils/components";
62
+ import { PluginLayout, Header, Footer } from "figma-plugin-utilities/components";
43
63
 
44
64
  // Utilities only
45
- import { sendToPlugin, createMessageHandler } from "figma-plugin-utils/lib";
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
- // Calculate contrast
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 { sendToUI, showError, focusNodes, loadFont } from "figma-plugin-utils/lib/figma-helpers";
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 notification
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.2.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": ["src"],
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
- "dependencies": {
53
- "figma-ui3-kit-svelte": "file:../figma-ui3-kit-svelte",
54
- "svelte": "^4.2.20"
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
- function handleCardClick(e) {
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
- <div
55
- class="checkbox-card"
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
- let className = '';
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
- // Normalize actions
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
- /** For attribute for the label (optional) */
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
- <div class="footer footer--{variant} {className}">
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
- </div>
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
- <div
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
- <h2 class="header__title">{title}</h2>
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
- </div>
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, IconMore, Menu } from "figma-ui3-kit-svelte";
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) => e.key === "Enter" && handleClick()}
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">{message}</Text>
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
- * <svelte:fragment slot="footer">
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, IconClose } from "figma-ui3-kit-svelte";
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
@@ -38,6 +38,9 @@ export {
38
38
  withErrorHandling,
39
39
  safeAsync,
40
40
  parseJsonSafe,
41
+ notifyError,
42
+ notifySuccess,
43
+ notifyWarning,
41
44
  // Resize
42
45
  setDefaultWidth,
43
46
  getContentHeight,
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) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
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":
@@ -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
@@ -1,8 +1,5 @@
1
1
  // Message utilities
2
- export {
3
- sendToPlugin,
4
- createMessageHandler,
5
- } from "./messages.js";
2
+ export { sendToPlugin, createMessageHandler } from "./messages.js";
6
3
 
7
4
  // Color utilities
8
5
  export {
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-utils";
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-utils/lib/figma-helpers";
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)
@@ -58,7 +58,7 @@ export function validateUrl(url, options = { required: true }) {
58
58
  }
59
59
 
60
60
  return { valid: true };
61
- } catch (err) {
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