figma-plugin-utilities 0.1.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/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # figma-plugin-utils
2
+
3
+ Shared Svelte components and utilities for Figma plugins.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install figma-plugin-utils
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Import Everything
14
+
15
+ ```javascript
16
+ import {
17
+ // Components
18
+ PluginLayout,
19
+ Header,
20
+ Footer,
21
+ StatusBar,
22
+ EmptyState,
23
+ ListItem,
24
+ LoadingState,
25
+ FieldGroup,
26
+ // Utilities
27
+ sendToPlugin,
28
+ createMessageHandler,
29
+ rgbToHex,
30
+ hexToRgb,
31
+ validateUrl,
32
+ validateJsonString,
33
+ sanitizeName,
34
+ } from "figma-plugin-utils";
35
+ ```
36
+
37
+ ### Import Specific Modules
38
+
39
+ ```javascript
40
+ // Components only
41
+ import { PluginLayout, Header, Footer } from "figma-plugin-utils/components";
42
+
43
+ // Utilities only
44
+ import { sendToPlugin, createMessageHandler } from "figma-plugin-utils/lib";
45
+ ```
46
+
47
+ ## Components
48
+
49
+ | Component | Description |
50
+ |-----------|-------------|
51
+ | `PluginLayout` | Main content wrapper with scrollable area |
52
+ | `Header` | Header bar with `left`, `center`, `right` slots and optional title |
53
+ | `Footer` | Footer with `right`, `split`, and `full` layout variants |
54
+ | `StatusBar` | Toast notifications with auto-dismiss (info/success/error/warning) |
55
+ | `EmptyState` | Empty/error states with optional icon and action buttons |
56
+ | `ListItem` | Selectable list items with metadata slot and action menu |
57
+ | `LoadingState` | Centered loading indicator with custom message |
58
+ | `FieldGroup` | Label + input wrapper for form fields |
59
+
60
+ ### Header
61
+
62
+ ```svelte
63
+ <Header title="My Plugin">
64
+ <svelte:fragment slot="left">
65
+ <IconButton iconName={IconBack} on:click={goBack} />
66
+ </svelte:fragment>
67
+ <svelte:fragment slot="right">
68
+ <IconButton iconName={IconSettings} />
69
+ </svelte:fragment>
70
+ </Header>
71
+
72
+ <!-- Without border -->
73
+ <Header title="Settings" noBorder />
74
+ ```
75
+
76
+ ### Footer
77
+
78
+ ```svelte
79
+ <!-- Right-aligned (default) -->
80
+ <Footer>
81
+ <Button variant="primary">Save</Button>
82
+ </Footer>
83
+
84
+ <!-- Split layout -->
85
+ <Footer variant="split">
86
+ <svelte:fragment slot="left">
87
+ <Button variant="secondary">Cancel</Button>
88
+ </svelte:fragment>
89
+ <svelte:fragment slot="right">
90
+ <Button variant="primary">Save</Button>
91
+ </svelte:fragment>
92
+ </Footer>
93
+
94
+ <!-- Full-width buttons -->
95
+ <Footer variant="full">
96
+ <Button variant="primary">Generate</Button>
97
+ </Footer>
98
+ ```
99
+
100
+ ### StatusBar
101
+
102
+ ```svelte
103
+ <StatusBar
104
+ message={status.message}
105
+ type={status.type}
106
+ on:close={() => status = { message: '', type: 'info' }}
107
+ />
108
+ ```
109
+
110
+ Types: `info`, `success`, `error`, `warning`. Auto-dismisses after 4s for `info` and `success`.
111
+
112
+ ### EmptyState
113
+
114
+ ```svelte
115
+ <EmptyState
116
+ message="No items yet"
117
+ icon="search"
118
+ actions={[
119
+ { label: "Add Item", handler: handleAdd },
120
+ { label: "Import", handler: handleImport }
121
+ ]}
122
+ />
123
+ ```
124
+
125
+ ### ListItem
126
+
127
+ ```svelte
128
+ <ListItem
129
+ id="item-1"
130
+ title="My Item"
131
+ active={selectedId === 'item-1'}
132
+ menuItems={[
133
+ { label: 'Edit', value: 'edit' },
134
+ { label: 'Delete', value: 'delete' }
135
+ ]}
136
+ on:click={handleSelect}
137
+ on:menuSelect={handleMenuAction}
138
+ >
139
+ <span>Additional metadata</span>
140
+ </ListItem>
141
+ ```
142
+
143
+ ## Utilities
144
+
145
+ ### Messages (`lib/messages.js`)
146
+
147
+ ```javascript
148
+ // Send message to plugin code
149
+ sendToPlugin("my-action", { data: "value" });
150
+
151
+ // Handle messages from plugin
152
+ window.onmessage = createMessageHandler({
153
+ success: (msg) => console.log("Success:", msg),
154
+ error: (msg) => console.error("Error:", msg),
155
+ });
156
+ ```
157
+
158
+ ### Colors (`lib/colors.js`)
159
+
160
+ ```javascript
161
+ // Convert between formats (Figma uses 0-1 range)
162
+ const rgb = hexToRgb("#FF0000"); // { r: 1, g: 0, b: 0 }
163
+ const hex = rgbToHex({ r: 1, g: 0, b: 0 }); // "#FF0000"
164
+
165
+ // Calculate contrast
166
+ const ratio = getContrastRatio(color1, color2);
167
+ const passes = meetsContrastLevel(ratio, "AA"); // true/false
168
+ ```
169
+
170
+ ### Validation (`lib/validation.js`)
171
+
172
+ ```javascript
173
+ const urlResult = validateUrl("https://example.com");
174
+ // { valid: true } or { valid: false, error: "..." }
175
+
176
+ const jsonResult = validateJsonString('{"key": "value"}');
177
+ // { valid: true, parsed: {...} } or { valid: false, error: "..." }
178
+
179
+ const clean = sanitizeName("My Plugin!!!"); // "My Plugin"
180
+ ```
181
+
182
+ ### Error Handling (`lib/errorHandling.js`)
183
+
184
+ ```javascript
185
+ // Safe async operations
186
+ const result = await safeAsync(
187
+ () => fetch(url),
188
+ "Loading data"
189
+ );
190
+ if (result.ok) {
191
+ console.log(result.value);
192
+ } else {
193
+ console.error(result.error.userMessage);
194
+ }
195
+
196
+ // Parse JSON safely
197
+ const parsed = parseJsonSafe(jsonString);
198
+ // { ok: true, value: {...} } or { ok: false, error: "..." }
199
+ ```
200
+
201
+ ### Figma Helpers (`lib/figma-helpers.ts`)
202
+
203
+ For use in `code.ts`:
204
+
205
+ ```typescript
206
+ import { sendToUI, showError, focusNodes, loadFont } from "figma-plugin-utils/lib/figma-helpers";
207
+
208
+ // Send message to UI
209
+ sendToUI("success", { message: "Done!" });
210
+
211
+ // Show notification
212
+ showError("Something went wrong");
213
+
214
+ // Focus viewport on nodes
215
+ focusNodes(figma.currentPage.selection);
216
+
217
+ // Load font before using
218
+ await loadFont("Inter", "Regular");
219
+
220
+ // Client storage
221
+ await saveToStorage("settings", { theme: "dark" });
222
+ const settings = await loadFromStorage("settings", { theme: "light" });
223
+ ```
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "figma-plugin-utilities",
3
+ "version": "0.1.0",
4
+ "description": "Shared Svelte components and utilities for Figma plugins",
5
+ "type": "module",
6
+ "svelte": "./src/index.js",
7
+ "main": "./src/index.js",
8
+ "module": "./src/index.js",
9
+ "exports": {
10
+ ".": {
11
+ "svelte": "./src/index.js",
12
+ "import": "./src/index.js",
13
+ "default": "./src/index.js"
14
+ },
15
+ "./components": {
16
+ "svelte": "./src/components/index.js",
17
+ "import": "./src/components/index.js",
18
+ "default": "./src/components/index.js"
19
+ },
20
+ "./lib": {
21
+ "import": "./src/lib/index.js",
22
+ "default": "./src/lib/index.js"
23
+ },
24
+ "./lib/figma-helpers": {
25
+ "types": "./src/lib/figma-helpers.ts",
26
+ "import": "./src/lib/figma-helpers.ts",
27
+ "default": "./src/lib/figma-helpers.ts"
28
+ }
29
+ },
30
+ "files": ["src"],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/mariusroosendaal/figma-plugin-utilities.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/mariusroosendaal/figma-plugin-utilities/issues"
37
+ },
38
+ "homepage": "https://github.com/mariusroosendaal/figma-plugin-utilities#readme",
39
+ "keywords": [
40
+ "figma",
41
+ "plugin",
42
+ "svelte",
43
+ "components",
44
+ "utilities",
45
+ "figma-plugin"
46
+ ],
47
+ "author": "Marius Roosendaal",
48
+ "license": "MIT",
49
+ "dependencies": {
50
+ "figma-ui3-kit-svelte": "^0.4.0",
51
+ "svelte": "^4.2.20"
52
+ }
53
+ }
@@ -0,0 +1,116 @@
1
+ <script>
2
+ import { Button, Icon } from "figma-ui3-kit-svelte";
3
+
4
+ /**
5
+ * Empty state display with optional icon and action buttons
6
+ *
7
+ * @example
8
+ * <EmptyState
9
+ * message="No items found"
10
+ * icon="search"
11
+ * actions={[{ label: "Add Item", handler: handleAdd }]}
12
+ * />
13
+ */
14
+
15
+ /** Message to display */
16
+ export let message = "";
17
+
18
+ /** Optional icon (string name or component) */
19
+ export let icon = null;
20
+
21
+ /** Single action for backward compatibility { label, handler } */
22
+ export let action = null;
23
+
24
+ /** Multiple actions [{ label, handler }] */
25
+ export let actions = null;
26
+
27
+ /** Size variant: 'small', 'medium', 'large' */
28
+ export let size = "medium";
29
+
30
+ /** Whether to center vertically */
31
+ export let centered = true;
32
+
33
+ // Normalize actions
34
+ $: normalizedActions = actions ? actions : action ? [action] : null;
35
+ </script>
36
+
37
+ <div
38
+ class="empty-state"
39
+ class:centered
40
+ class:small={size === "small"}
41
+ class:large={size === "large"}
42
+ >
43
+ {#if icon}
44
+ <div class="empty-state__icon">
45
+ {#if typeof icon === "string"}
46
+ <Icon iconName={icon} />
47
+ {:else}
48
+ <svelte:component this={icon} />
49
+ {/if}
50
+ </div>
51
+ {/if}
52
+
53
+ <div class="empty-state__message">
54
+ {message}
55
+ </div>
56
+
57
+ {#if normalizedActions && normalizedActions.length > 0}
58
+ <div class="empty-state__actions">
59
+ {#each normalizedActions as actionItem}
60
+ <Button variant="secondary" on:click={actionItem.handler}>
61
+ {actionItem.label}
62
+ </Button>
63
+ {/each}
64
+ </div>
65
+ {/if}
66
+ </div>
67
+
68
+ <style>
69
+ .empty-state {
70
+ text-align: center;
71
+ padding: var(--size-xsmall);
72
+ color: var(--figma-color-text-secondary);
73
+ display: flex;
74
+ flex-direction: column;
75
+ align-items: center;
76
+ gap: var(--size-xsmall);
77
+ font-family: var(--font-stack);
78
+ text-wrap: balance;
79
+ flex: 1;
80
+ }
81
+
82
+ .empty-state.centered {
83
+ justify-content: center;
84
+ align-items: center;
85
+ }
86
+
87
+ .empty-state.small {
88
+ font-size: var(--body-medium-font-size);
89
+ font-weight: var(--body-medium-font-weight);
90
+ letter-spacing: var(--body-medium-letter-spacing);
91
+ line-height: var(--body-medium-line-height);
92
+ }
93
+
94
+ .empty-state:not(.small):not(.large) {
95
+ font-size: var(--body-medium-font-size);
96
+ font-weight: var(--body-medium-font-weight);
97
+ letter-spacing: var(--body-medium-letter-spacing);
98
+ line-height: var(--body-medium-line-height);
99
+ }
100
+
101
+ .empty-state.large {
102
+ font-size: var(--body-large-font-size);
103
+ font-weight: var(--body-large-font-weight);
104
+ letter-spacing: var(--body-large-letter-spacing);
105
+ line-height: var(--body-large-line-height);
106
+ }
107
+
108
+ .empty-state__actions {
109
+ display: flex;
110
+ flex-direction: row;
111
+ gap: var(--size-xxsmall);
112
+ align-items: center;
113
+ justify-content: center;
114
+ flex-wrap: wrap;
115
+ }
116
+ </style>
@@ -0,0 +1,34 @@
1
+ <script>
2
+ import { Label } from "figma-ui3-kit-svelte";
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
+ /** Label text (optional) */
15
+ export let label = "";
16
+
17
+ /** For attribute for the label (optional) */
18
+ export let labelFor = "";
19
+ </script>
20
+
21
+ <div class="field-group">
22
+ {#if label}
23
+ <Label for={labelFor}>{label}</Label>
24
+ {/if}
25
+ <slot />
26
+ </div>
27
+
28
+ <style>
29
+ .field-group {
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: var(--size-xxsmall);
33
+ }
34
+ </style>
@@ -0,0 +1,110 @@
1
+ <script>
2
+ /**
3
+ * Plugin footer with layout variants
4
+ *
5
+ * @example
6
+ * <!-- Right-aligned (default) -->
7
+ * <Footer>
8
+ * <Button>Save</Button>
9
+ * </Footer>
10
+ *
11
+ * <!-- Split layout -->
12
+ * <Footer variant="split">
13
+ * <svelte:fragment slot="left">
14
+ * <Button variant="secondary">Cancel</Button>
15
+ * </svelte:fragment>
16
+ * <svelte:fragment slot="right">
17
+ * <Button variant="primary">Save</Button>
18
+ * </svelte:fragment>
19
+ * </Footer>
20
+ *
21
+ * <!-- Full width buttons -->
22
+ * <Footer variant="full">
23
+ * <Button variant="primary">Create item</Button>
24
+ * </Footer>
25
+ */
26
+
27
+ /** Layout variant: 'right', 'split', 'full' */
28
+ export let variant = "right";
29
+
30
+ /** Additional CSS class */
31
+ export let className = "";
32
+ </script>
33
+
34
+ <div class="footer footer--{variant} {className}">
35
+ {#if variant === "right"}
36
+ <div class="footer__right">
37
+ <slot />
38
+ </div>
39
+ {:else if variant === "split"}
40
+ <div class="footer__left">
41
+ <slot name="left" />
42
+ </div>
43
+ <div class="footer__right">
44
+ <slot name="right" />
45
+ </div>
46
+ {:else if variant === "full"}
47
+ <slot />
48
+ {/if}
49
+ </div>
50
+
51
+ <style>
52
+ .footer {
53
+ height: var(--size-large);
54
+ bottom: 0;
55
+ left: 0;
56
+ right: 0;
57
+ display: flex;
58
+ gap: var(--size-xxsmall);
59
+ padding: var(--size-xxsmall);
60
+ border-top: 1px solid var(--figma-color-border);
61
+ background: var(--figma-color-bg);
62
+ z-index: 10;
63
+ }
64
+
65
+ .footer :global(> *),
66
+ .footer :global(> * > *) {
67
+ display: flex;
68
+ gap: var(--size-xxsmall);
69
+ align-items: center;
70
+ min-width: 0;
71
+ }
72
+
73
+ .footer--right {
74
+ justify-content: flex-end;
75
+ }
76
+
77
+ .footer--right .footer__right {
78
+ display: flex;
79
+ gap: var(--size-xxsmall);
80
+ align-items: center;
81
+ }
82
+
83
+ .footer--split {
84
+ justify-content: space-between;
85
+ }
86
+
87
+ .footer--split .footer__left {
88
+ display: flex;
89
+ gap: var(--size-xxsmall);
90
+ align-items: center;
91
+ flex: 1;
92
+ }
93
+
94
+ .footer--split .footer__right {
95
+ display: flex;
96
+ gap: var(--size-xxsmall);
97
+ align-items: center;
98
+ justify-content: end;
99
+ }
100
+
101
+ .footer--full {
102
+ display: flex;
103
+ width: 100%;
104
+ }
105
+
106
+ .footer--full :global(button) {
107
+ flex: 1;
108
+ width: 100%;
109
+ }
110
+ </style>
@@ -0,0 +1,81 @@
1
+ <script>
2
+ // Header component with left/center/right slots
3
+
4
+ /** Additional CSS class */
5
+ export let className = "";
6
+
7
+ /** Title text (displayed in left section) */
8
+ export let title = "";
9
+
10
+ /** Remove bottom border */
11
+ export let noBorder = false;
12
+ </script>
13
+
14
+ <div
15
+ class="header {className}"
16
+ class:has-left-content={$$slots.left}
17
+ class:no-border={noBorder}
18
+ >
19
+ <div class="header__left">
20
+ <slot name="left" />
21
+ {#if title}
22
+ <h2 class="header__title">{title}</h2>
23
+ {/if}
24
+ </div>
25
+ <div class="header__center">
26
+ <slot name="center" />
27
+ </div>
28
+ <div class="header__right">
29
+ <slot name="right" />
30
+ </div>
31
+ </div>
32
+
33
+ <style>
34
+ .header {
35
+ height: var(--size-large);
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: space-between;
39
+ padding: 0 var(--size-xxsmall) 0 var(--size-xsmall);
40
+ border-bottom: 1px solid var(--figma-color-border);
41
+ background: var(--figma-color-bg);
42
+ gap: var(--size-xsmall);
43
+ }
44
+
45
+ .header.has-left-content {
46
+ padding-left: var(--size-xxsmall);
47
+ }
48
+
49
+ .header.no-border {
50
+ border-bottom: none;
51
+ }
52
+
53
+ .header__left {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: var(--size-xxsmall);
57
+ }
58
+
59
+ .header__center {
60
+ flex: 1;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ }
65
+
66
+ .header__title {
67
+ font-size: var(--body-medium-font-size);
68
+ font-weight: var(--body-medium-font-weight);
69
+ letter-spacing: var(--body-medium-letter-spacing);
70
+ line-height: var(--body-medium-line-height);
71
+ color: var(--figma-color-text);
72
+ margin: 0;
73
+ }
74
+
75
+ .header__right {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: var(--size-xxsmall);
79
+ position: relative;
80
+ }
81
+ </style>