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 +223 -0
- package/package.json +53 -0
- package/src/components/EmptyState.svelte +116 -0
- package/src/components/FieldGroup.svelte +34 -0
- package/src/components/Footer.svelte +110 -0
- package/src/components/Header.svelte +81 -0
- package/src/components/ListItem.svelte +172 -0
- package/src/components/LoadingState.svelte +32 -0
- package/src/components/PluginLayout.svelte +43 -0
- package/src/components/StatusBar.svelte +106 -0
- package/src/components/index.js +9 -0
- package/src/index.js +45 -0
- package/src/lib/colors.js +77 -0
- package/src/lib/errorHandling.js +186 -0
- package/src/lib/figma-helpers.ts +129 -0
- package/src/lib/index.js +47 -0
- package/src/lib/messages.js +36 -0
- package/src/lib/resize.js +147 -0
- package/src/lib/validation.js +234 -0
|
@@ -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
|
+
}
|