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.
@@ -0,0 +1,172 @@
1
+ <script>
2
+ import { createEventDispatcher } from "svelte";
3
+ import { IconButton, IconMore, Menu } from "figma-ui3-kit-svelte";
4
+
5
+ /**
6
+ * List item with optional action menu
7
+ *
8
+ * @example
9
+ * <ListItem
10
+ * id="item-1"
11
+ * title="My Item"
12
+ * active={selectedId === 'item-1'}
13
+ * menuItems={[
14
+ * { label: 'Edit', value: 'edit' },
15
+ * { label: 'Delete', value: 'delete' }
16
+ * ]}
17
+ * on:click={handleSelect}
18
+ * on:menuSelect={handleMenuAction}
19
+ * >
20
+ * <span>Additional info</span>
21
+ * </ListItem>
22
+ */
23
+
24
+ const dispatch = createEventDispatcher();
25
+
26
+ /** Unique identifier */
27
+ export let id;
28
+
29
+ /** Display title */
30
+ export let title;
31
+
32
+ /** Whether item is selected/active */
33
+ export let active = false;
34
+
35
+ /** Menu items [{ label, value }] */
36
+ export let menuItems = [];
37
+
38
+ /** Whether menu is open (bindable) */
39
+ export let menuOpen = false;
40
+
41
+ /** Reference to menu button element */
42
+ export let menuButtonElement = null;
43
+
44
+ /** Whether to show badge slot */
45
+ export let hasBadge = false;
46
+
47
+ function handleClick() {
48
+ dispatch("click", { id });
49
+ }
50
+
51
+ function handleMenuToggle(e) {
52
+ e.stopPropagation();
53
+ menuOpen = !menuOpen;
54
+ dispatch("menuToggle", { id, open: menuOpen });
55
+ }
56
+
57
+ function handleMenuSelect(e) {
58
+ dispatch("menuSelect", { id, action: e.detail.value });
59
+ menuOpen = false;
60
+ }
61
+
62
+ function handleMenuClose() {
63
+ menuOpen = false;
64
+ dispatch("menuClose", { id });
65
+ }
66
+ </script>
67
+
68
+ <div class="list-item-wrapper">
69
+ <div
70
+ class="list-item"
71
+ class:active
72
+ on:click={handleClick}
73
+ on:keydown={(e) => e.key === "Enter" && handleClick()}
74
+ role="button"
75
+ tabindex="0"
76
+ >
77
+ <div class="list-item__content">
78
+ <div class="list-item__title">{title}</div>
79
+ {#if $$slots.default}
80
+ <div class="list-item__meta">
81
+ <slot />
82
+ </div>
83
+ {/if}
84
+ {#if hasBadge && $$slots.badge}
85
+ <div class="list-item__badge">
86
+ <slot name="badge" />
87
+ </div>
88
+ {/if}
89
+ </div>
90
+ </div>
91
+
92
+ {#if menuItems.length > 0}
93
+ <IconButton
94
+ iconName={IconMore}
95
+ bind:element={menuButtonElement}
96
+ on:click={handleMenuToggle}
97
+ />
98
+ <Menu
99
+ bind:isOpen={menuOpen}
100
+ {menuItems}
101
+ anchorElement={menuButtonElement}
102
+ on:select={handleMenuSelect}
103
+ on:close={handleMenuClose}
104
+ />
105
+ {/if}
106
+ </div>
107
+
108
+ <style>
109
+ .list-item-wrapper {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: var(--size-xxxsmall);
113
+ padding: var(--size-xxxsmall) 0;
114
+ }
115
+
116
+ .list-item {
117
+ flex: 1;
118
+ display: flex;
119
+ align-items: center;
120
+ padding: var(--size-xxsmall);
121
+ background: var(--figma-color-bg-secondary);
122
+ border: 1px solid transparent;
123
+ border-radius: var(--border-radius-medium);
124
+ cursor: pointer;
125
+ transition: border-color 0.15s ease;
126
+ min-width: 0;
127
+ }
128
+
129
+ .list-item:hover {
130
+ border-color: var(--figma-color-border-selected);
131
+ }
132
+
133
+ .list-item.active {
134
+ border-color: var(--figma-color-border-brand-strong);
135
+ background: var(--figma-color-bg-brand-tertiary);
136
+ }
137
+
138
+ .list-item__content {
139
+ flex: 1;
140
+ display: flex;
141
+ flex-direction: column;
142
+ gap: var(--size-xxxsmall);
143
+ min-width: 0;
144
+ }
145
+
146
+ .list-item__title {
147
+ font-size: var(--body-medium-font-size);
148
+ font-weight: var(--body-medium-font-weight);
149
+ letter-spacing: var(--body-medium-letter-spacing);
150
+ line-height: var(--body-medium-line-height);
151
+ overflow: hidden;
152
+ text-overflow: ellipsis;
153
+ white-space: nowrap;
154
+ }
155
+
156
+ .list-item__meta {
157
+ display: flex;
158
+ align-items: center;
159
+ gap: var(--size-xxxsmall);
160
+ font-size: var(--body-medium-font-size);
161
+ font-weight: var(--body-medium-font-weight);
162
+ letter-spacing: var(--body-medium-letter-spacing);
163
+ line-height: var(--body-medium-line-height);
164
+ color: var(--figma-color-text-secondary);
165
+ }
166
+
167
+ .list-item__badge {
168
+ display: block;
169
+ min-width: 0;
170
+ overflow: hidden;
171
+ }
172
+ </style>
@@ -0,0 +1,32 @@
1
+ <script>
2
+ import { Text } from "figma-ui3-kit-svelte";
3
+
4
+ /**
5
+ * Loading state indicator
6
+ * Displays a centered message while content is loading
7
+ *
8
+ * @example
9
+ * {#if isLoading}
10
+ * <LoadingState message="Loading collections..." />
11
+ * {:else}
12
+ * <MainContent />
13
+ * {/if}
14
+ */
15
+
16
+ /** Message to display */
17
+ export let message = "Loading...";
18
+ </script>
19
+
20
+ <div class="loading-state">
21
+ <Text variant="body-medium" color="--figma-color-text-secondary">{message}</Text>
22
+ </div>
23
+
24
+ <style>
25
+ .loading-state {
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ height: 100%;
30
+ padding: var(--size-medium);
31
+ }
32
+ </style>
@@ -0,0 +1,43 @@
1
+ <script>
2
+ /**
3
+ * Standard plugin layout wrapper
4
+ * Provides consistent structure with main content area and optional footer
5
+ *
6
+ * @example
7
+ * <PluginLayout>
8
+ * <p>Main content here</p>
9
+ * <svelte:fragment slot="footer">
10
+ * <Button variant="primary">Create item</Button>
11
+ * </svelte:fragment>
12
+ * </PluginLayout>
13
+ */
14
+
15
+ /** Additional CSS classes for the wrapper */
16
+ export let className = "";
17
+ </script>
18
+
19
+ <div class="plugin-wrapper {className}">
20
+ <main class="plugin-main">
21
+ <slot />
22
+ </main>
23
+ </div>
24
+
25
+ <style>
26
+ .plugin-wrapper {
27
+ flex: 1;
28
+ display: flex;
29
+ flex-direction: column;
30
+ color: var(--figma-color-text);
31
+ font-family: var(--figma-font-stack);
32
+ overflow: hidden;
33
+ }
34
+
35
+ .plugin-main {
36
+ flex: 1;
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: var(--size-xsmall);
40
+ overflow-y: auto;
41
+ padding: var(--size-xsmall);
42
+ }
43
+ </style>
@@ -0,0 +1,106 @@
1
+ <script>
2
+ import { onDestroy, createEventDispatcher } from "svelte";
3
+ import { IconButton, IconClose } from "figma-ui3-kit-svelte";
4
+
5
+ // Status bar for notifications with auto-dismiss
6
+ // Supports types: 'info', 'success', 'error', 'warning'
7
+ // Auto-dismisses after 4s for 'info' and 'success' types
8
+
9
+ const dispatch = createEventDispatcher();
10
+
11
+ /** Message to display */
12
+ export let message = "";
13
+
14
+ /** Status type: 'info', 'success', 'error', 'warning' */
15
+ export let type = "info";
16
+
17
+ let visible = false;
18
+ let timeoutId;
19
+
20
+ $: shouldAutoDismiss = type === "success" || type === "info";
21
+
22
+ // Compute icon color based on type
23
+ $: computedIconColor =
24
+ type === "error"
25
+ ? "--figma-color-icon-ondanger"
26
+ : type === "success"
27
+ ? "--figma-color-icon-onsuccess"
28
+ : type === "warning"
29
+ ? "--figma-color-icon-onwarning"
30
+ : "--figma-color-icon";
31
+
32
+ // Show status when message changes
33
+ $: if (message) {
34
+ visible = true;
35
+ if (shouldAutoDismiss) {
36
+ clearTimeout(timeoutId);
37
+ timeoutId = setTimeout(() => handleClose(), 4000);
38
+ }
39
+ } else {
40
+ visible = false;
41
+ }
42
+
43
+ function handleClose() {
44
+ visible = false;
45
+ clearTimeout(timeoutId);
46
+ dispatch("close");
47
+ }
48
+
49
+ onDestroy(() => {
50
+ clearTimeout(timeoutId);
51
+ });
52
+ </script>
53
+
54
+ {#if visible && message}
55
+ <div
56
+ class="status-bar"
57
+ class:status-bar--error={type === "error"}
58
+ class:status-bar--success={type === "success"}
59
+ class:status-bar--warning={type === "warning"}
60
+ >
61
+ <span>{message}</span>
62
+ <IconButton
63
+ iconName={IconClose}
64
+ on:click={handleClose}
65
+ iconColor={computedIconColor}
66
+ />
67
+ </div>
68
+ {/if}
69
+
70
+ <style>
71
+ .status-bar {
72
+ position: relative;
73
+ height: var(--size-large);
74
+ padding: 0 var(--size-xxsmall) 0 var(--size-xsmall);
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: space-between;
78
+ background: var(--figma-color-bg-secondary);
79
+ font-size: var(--body-medium-font-size);
80
+ font-weight: var(--body-medium-font-weight);
81
+ letter-spacing: var(--body-medium-letter-spacing);
82
+ line-height: var(--body-medium-line-height);
83
+ overflow: hidden;
84
+ }
85
+
86
+ .status-bar > span {
87
+ white-space: nowrap;
88
+ overflow: hidden;
89
+ text-overflow: ellipsis;
90
+ }
91
+
92
+ .status-bar--error {
93
+ background: var(--figma-color-bg-danger);
94
+ color: var(--figma-color-text-ondanger);
95
+ }
96
+
97
+ .status-bar--success {
98
+ background: var(--figma-color-bg-success);
99
+ color: var(--figma-color-text-onsuccess);
100
+ }
101
+
102
+ .status-bar--warning {
103
+ background: var(--figma-color-bg-warning);
104
+ color: var(--figma-color-text-onwarning);
105
+ }
106
+ </style>
@@ -0,0 +1,9 @@
1
+ // Svelte components
2
+ export { default as EmptyState } from "./EmptyState.svelte";
3
+ export { default as FieldGroup } from "./FieldGroup.svelte";
4
+ export { default as Footer } from "./Footer.svelte";
5
+ export { default as ListItem } from "./ListItem.svelte";
6
+ export { default as LoadingState } from "./LoadingState.svelte";
7
+ export { default as PluginLayout } from "./PluginLayout.svelte";
8
+ export { default as StatusBar } from "./StatusBar.svelte";
9
+ export { default as Header } from "./Header.svelte";
package/src/index.js ADDED
@@ -0,0 +1,45 @@
1
+ // Re-export all components
2
+ export {
3
+ EmptyState,
4
+ FieldGroup,
5
+ Footer,
6
+ ListItem,
7
+ LoadingState,
8
+ PluginLayout,
9
+ StatusBar,
10
+ Header,
11
+ } from "./components/index.js";
12
+
13
+ // Re-export all utilities
14
+ export {
15
+ // Messages
16
+ sendToPlugin,
17
+ createMessageHandler,
18
+ // Colors
19
+ rgbToHex,
20
+ hexToRgb,
21
+ getLuminance,
22
+ getContrastRatio,
23
+ meetsContrastLevel,
24
+ // Validation
25
+ validateUrl,
26
+ validateJsonString,
27
+ sanitizeInput,
28
+ sanitizeName,
29
+ validateEmail,
30
+ validateNumber,
31
+ isEmpty,
32
+ // Error handling
33
+ formatErrorMessage,
34
+ handleAsyncError,
35
+ createUserErrorMessage,
36
+ logError,
37
+ withErrorHandling,
38
+ safeAsync,
39
+ parseJsonSafe,
40
+ // Resize
41
+ setDefaultWidth,
42
+ getContentHeight,
43
+ resizeToFit,
44
+ autoResize,
45
+ } from "./lib/index.js";
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Color utilities for Figma plugins
3
+ */
4
+
5
+ /**
6
+ * Convert RGB color (0-1 range) to HEX string
7
+ * @param {{r: number, g: number, b: number}} color - RGB color with values 0-1
8
+ * @returns {string} HEX color string (e.g., "#FF0000")
9
+ */
10
+ export function rgbToHex({ r, g, b }) {
11
+ const toHex = (value) => {
12
+ const hex = Math.round(value * 255)
13
+ .toString(16)
14
+ .padStart(2, "0");
15
+ return hex;
16
+ };
17
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
18
+ }
19
+
20
+ /**
21
+ * Convert HEX string to RGB color (0-1 range)
22
+ * @param {string} hex - HEX color string (e.g., "#FF0000" or "FF0000")
23
+ * @returns {{r: number, g: number, b: number} | null} RGB color with values 0-1, or null if invalid
24
+ */
25
+ export function hexToRgb(hex) {
26
+ const cleanHex = hex.replace(/^#/, "");
27
+ if (!/^[0-9A-Fa-f]{6}$/.test(cleanHex)) {
28
+ return null;
29
+ }
30
+ const r = parseInt(cleanHex.substring(0, 2), 16) / 255;
31
+ const g = parseInt(cleanHex.substring(2, 4), 16) / 255;
32
+ const b = parseInt(cleanHex.substring(4, 6), 16) / 255;
33
+ return { r, g, b };
34
+ }
35
+
36
+ /**
37
+ * Get relative luminance of a color (for contrast calculations)
38
+ * @param {{r: number, g: number, b: number}} color - RGB color with values 0-1
39
+ * @returns {number} Relative luminance (0-1)
40
+ */
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));
43
+ return 0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b);
44
+ }
45
+
46
+ /**
47
+ * Calculate contrast ratio between two colors (WCAG formula)
48
+ * @param {{r: number, g: number, b: number}} color1 - First RGB color (0-1 range)
49
+ * @param {{r: number, g: number, b: number}} color2 - Second RGB color (0-1 range)
50
+ * @returns {number} Contrast ratio (1-21)
51
+ */
52
+ export function getContrastRatio(color1, color2) {
53
+ const l1 = getLuminance(color1);
54
+ const l2 = getLuminance(color2);
55
+ const lighter = Math.max(l1, l2);
56
+ const darker = Math.min(l1, l2);
57
+ return (lighter + 0.05) / (darker + 0.05);
58
+ }
59
+
60
+ /**
61
+ * Check if contrast ratio meets WCAG level
62
+ * @param {number} ratio - Contrast ratio
63
+ * @param {"AA" | "AAA" | "AA-large"} level - WCAG level to check
64
+ * @returns {boolean} Whether the ratio meets the level
65
+ */
66
+ export function meetsContrastLevel(ratio, level) {
67
+ switch (level) {
68
+ case "AAA":
69
+ return ratio >= 7;
70
+ case "AA":
71
+ return ratio >= 4.5;
72
+ case "AA-large":
73
+ return ratio >= 3;
74
+ default:
75
+ return false;
76
+ }
77
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Error handling utilities
3
+ *
4
+ * Provides standardized error handling functions for consistent
5
+ * error messages and error handling patterns across the plugin.
6
+ */
7
+
8
+ /**
9
+ * @typedef {Object} FormattedError
10
+ * @property {string} message - Technical error message
11
+ * @property {string} userMessage - User-friendly error message
12
+ * @property {string} [technical] - Stack trace or detailed info
13
+ */
14
+
15
+ /**
16
+ * Format an error into a user-friendly message
17
+ * @param {unknown} error - The error to format
18
+ * @param {string} [context] - Optional context to add to the message
19
+ * @returns {FormattedError}
20
+ */
21
+ export function formatErrorMessage(error, context) {
22
+ let message = "An unexpected error occurred";
23
+ let technical = "";
24
+
25
+ if (error instanceof Error) {
26
+ message = error.message;
27
+ technical = error.stack || error.message;
28
+ } else if (typeof error === "string") {
29
+ message = error;
30
+ technical = error;
31
+ } else {
32
+ technical = String(error);
33
+ message = "An unknown error occurred";
34
+ }
35
+
36
+ // Add context if provided
37
+ if (context) {
38
+ message = `${context}: ${message}`;
39
+ }
40
+
41
+ // Create user-friendly version by removing technical details
42
+ let userMessage = message;
43
+
44
+ // Common error patterns to make more user-friendly
45
+ if (message.includes("Failed to fetch") || message.includes("NetworkError")) {
46
+ userMessage =
47
+ "Network error: Could not connect to the server. Please check your internet connection and try again.";
48
+ } else if (message.includes("CORS")) {
49
+ userMessage =
50
+ "CORS error: The resource host does not allow plugin access. Try resources from allowed domains.";
51
+ } else if (message.includes("JSON")) {
52
+ userMessage = "Invalid JSON format. Please check your data and try again.";
53
+ } else if (message.includes("not found")) {
54
+ userMessage = message
55
+ .replace(/not found/gi, "not found")
56
+ .replace(/^Error: /, "");
57
+ } else if (message.includes("Missing")) {
58
+ userMessage = message.replace(/^Error: /, "");
59
+ }
60
+
61
+ return {
62
+ message,
63
+ userMessage,
64
+ technical: technical || undefined,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Handle async errors with standardized formatting
70
+ * @param {unknown} error - The error to handle
71
+ * @param {string} operation - Description of the operation that failed
72
+ * @returns {FormattedError}
73
+ */
74
+ export function handleAsyncError(error, operation) {
75
+ return formatErrorMessage(error, operation);
76
+ }
77
+
78
+ /**
79
+ * Create a user-friendly error message for UI display
80
+ * @param {unknown} error - The error to format
81
+ * @param {string} operation - Description of the operation that failed
82
+ * @returns {string}
83
+ */
84
+ export function createUserErrorMessage(error, operation) {
85
+ return handleAsyncError(error, operation).userMessage;
86
+ }
87
+
88
+ /**
89
+ * Log error with context (for debugging)
90
+ * @param {unknown} error - The error to log
91
+ * @param {string} context - Context description
92
+ */
93
+ export function logError(error, context) {
94
+ const formatted = formatErrorMessage(error, context);
95
+ console.error(`[${context}]`, formatted.message);
96
+ if (formatted.technical && formatted.technical !== formatted.message) {
97
+ console.error("Technical details:", formatted.technical);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Wrap an async function with error handling
103
+ * @template T
104
+ * @param {() => Promise<T>} fn - The async function to wrap
105
+ * @param {string} operation - Description of the operation
106
+ * @returns {Promise<T>}
107
+ */
108
+ export async function withErrorHandling(fn, operation) {
109
+ try {
110
+ return await fn();
111
+ } catch (error) {
112
+ logError(error, operation);
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Wrap an async function and return a result object instead of throwing
119
+ * @template T
120
+ * @param {() => Promise<T>} fn - The async function to wrap
121
+ * @param {string} operation - Description of the operation
122
+ * @returns {Promise<{ ok: true, value: T } | { ok: false, error: FormattedError }>}
123
+ */
124
+ export async function safeAsync(fn, operation) {
125
+ try {
126
+ const value = await fn();
127
+ return { ok: true, value };
128
+ } catch (error) {
129
+ const formatted = handleAsyncError(error, operation);
130
+ return { ok: false, error: formatted };
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Parse JSON safely without throwing
136
+ * @param {string} jsonString - The JSON string to parse
137
+ * @returns {{ ok: true, value: any } | { ok: false, error: string }}
138
+ */
139
+ export function parseJsonSafe(jsonString) {
140
+ try {
141
+ const value = JSON.parse(jsonString);
142
+ return { ok: true, value };
143
+ } catch (error) {
144
+ const message = error instanceof Error ? error.message : String(error);
145
+ return { ok: false, error: message };
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Show an error notification to the user with standardized options.
151
+ * Use this in code.ts files for consistent error notifications.
152
+ * @param {string} message - The error message to display
153
+ * @param {unknown} [error] - Optional error object for logging
154
+ * @param {string} [context] - Optional context for logging
155
+ */
156
+ export function notifyError(message, error, context) {
157
+ if (error) {
158
+ logError(error, context || message);
159
+ }
160
+ // @ts-ignore - figma global is available in plugin context
161
+ if (typeof figma !== "undefined") {
162
+ figma.notify(message, { error: true });
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Show a success notification to the user with standardized options.
168
+ * @param {string} message - The success message to display
169
+ */
170
+ export function notifySuccess(message) {
171
+ // @ts-ignore - figma global is available in plugin context
172
+ if (typeof figma !== "undefined") {
173
+ figma.notify(message);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Show a warning notification to the user.
179
+ * @param {string} message - The warning message to display
180
+ */
181
+ export function notifyWarning(message) {
182
+ // @ts-ignore - figma global is available in plugin context
183
+ if (typeof figma !== "undefined") {
184
+ figma.notify(message, { timeout: 5000 });
185
+ }
186
+ }