@miozu/jera 0.0.2 → 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/CLAUDE.md +443 -0
- package/README.md +211 -1
- package/llms.txt +64 -0
- package/package.json +44 -14
- package/src/actions/index.js +375 -0
- package/src/components/feedback/EmptyState.svelte +179 -0
- package/src/components/feedback/ProgressBar.svelte +116 -0
- package/src/components/feedback/Skeleton.svelte +107 -0
- package/src/components/feedback/Spinner.svelte +77 -0
- package/src/components/feedback/Toast.svelte +297 -0
- package/src/components/forms/Checkbox.svelte +147 -0
- package/src/components/forms/Dropzone.svelte +248 -0
- package/src/components/forms/FileUpload.svelte +266 -0
- package/src/components/forms/IconInput.svelte +184 -0
- package/src/components/forms/Input.svelte +121 -0
- package/src/components/forms/NumberInput.svelte +225 -0
- package/src/components/forms/PinInput.svelte +169 -0
- package/src/components/forms/Radio.svelte +143 -0
- package/src/components/forms/RadioGroup.svelte +62 -0
- package/src/components/forms/RangeSlider.svelte +212 -0
- package/src/components/forms/SearchInput.svelte +175 -0
- package/src/components/forms/Select.svelte +326 -0
- package/src/components/forms/Switch.svelte +159 -0
- package/src/components/forms/Textarea.svelte +122 -0
- package/src/components/navigation/Accordion.svelte +65 -0
- package/src/components/navigation/AccordionItem.svelte +146 -0
- package/src/components/navigation/Tabs.svelte +239 -0
- package/src/components/overlays/ConfirmDialog.svelte +272 -0
- package/src/components/overlays/Dropdown.svelte +153 -0
- package/src/components/overlays/DropdownDivider.svelte +23 -0
- package/src/components/overlays/DropdownItem.svelte +97 -0
- package/src/components/overlays/Modal.svelte +232 -0
- package/src/components/overlays/Popover.svelte +206 -0
- package/src/components/primitives/Avatar.svelte +132 -0
- package/src/components/primitives/Badge.svelte +118 -0
- package/src/components/primitives/Button.svelte +262 -0
- package/src/components/primitives/Card.svelte +104 -0
- package/src/components/primitives/Divider.svelte +105 -0
- package/src/components/primitives/LazyImage.svelte +104 -0
- package/src/components/primitives/Link.svelte +122 -0
- package/src/components/primitives/StatusBadge.svelte +122 -0
- package/src/index.js +128 -0
- package/src/tokens/colors.css +189 -0
- package/src/tokens/effects.css +128 -0
- package/src/tokens/index.css +81 -0
- package/src/tokens/spacing.css +49 -0
- package/src/tokens/typography.css +79 -0
- package/src/utils/cn.svelte.js +175 -0
- package/src/utils/index.js +17 -0
- package/src/utils/reactive.svelte.js +239 -0
- package/jera.js +0 -135
- package/www/components/jera/Input/Input.svelte +0 -63
- package/www/components/jera/Input/index.js +0 -1
package/package.json
CHANGED
|
@@ -1,24 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miozu/jera",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "jera.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"jera": "./jera.js"
|
|
8
|
-
},
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Minimal, reactive component library for Svelte 5",
|
|
9
5
|
"type": "module",
|
|
10
6
|
"scripts": {
|
|
11
|
-
"
|
|
7
|
+
"prepublishOnly": "echo 'Publishing @miozu/jera...' && test -f src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"svelte": "./src/index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"svelte": "./src/index.js",
|
|
13
|
+
"default": "./src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./tokens": "./src/tokens/index.css",
|
|
16
|
+
"./tokens/colors": "./src/tokens/colors.css",
|
|
17
|
+
"./tokens/spacing": "./src/tokens/spacing.css",
|
|
18
|
+
"./tokens/typography": "./src/tokens/typography.css",
|
|
19
|
+
"./tokens/effects": "./src/tokens/effects.css",
|
|
20
|
+
"./utils": "./src/utils/index.js",
|
|
21
|
+
"./actions": "./src/actions/index.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"llms.txt",
|
|
26
|
+
"CLAUDE.md"
|
|
27
|
+
],
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"svelte": "^5.0.0"
|
|
12
30
|
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"svelte": "^5.41.0"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"svelte",
|
|
36
|
+
"svelte5",
|
|
37
|
+
"components",
|
|
38
|
+
"design-system",
|
|
39
|
+
"ui",
|
|
40
|
+
"miozu",
|
|
41
|
+
"base16",
|
|
42
|
+
"dark-theme"
|
|
43
|
+
],
|
|
44
|
+
"author": "Nicholas Glazer <glazer.nicholas@gmail.com>",
|
|
45
|
+
"license": "MIT",
|
|
13
46
|
"repository": {
|
|
14
47
|
"type": "git",
|
|
15
|
-
"url": "https://
|
|
48
|
+
"url": "https://github.com/miozu-com/jera"
|
|
16
49
|
},
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
"packageManager": "pnpm@10.6.5",
|
|
21
|
-
"dependencies": {
|
|
22
|
-
"commander": "^13.1.0"
|
|
50
|
+
"homepage": "https://miozu.com",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/miozu-com/jera/issues"
|
|
23
53
|
}
|
|
24
54
|
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte Actions Library
|
|
3
|
+
*
|
|
4
|
+
* Reusable element behaviors that can be composed onto any element.
|
|
5
|
+
* Actions are the Svelte way to add imperative logic to elements.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <div use:clickOutside={handleClose}>...</div>
|
|
9
|
+
* <input use:focus />
|
|
10
|
+
* <button use:longPress={{ duration: 500, onLongPress: handler }}>...</button>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Click Outside Action
|
|
15
|
+
*
|
|
16
|
+
* Fires callback when clicking outside the element.
|
|
17
|
+
* Useful for dropdowns, modals, popovers.
|
|
18
|
+
*
|
|
19
|
+
* @param {HTMLElement} node
|
|
20
|
+
* @param {() => void} callback
|
|
21
|
+
* @returns {{ destroy: () => void, update: (cb: () => void) => void }}
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <div use:clickOutside={() => isOpen = false}>Dropdown</div>
|
|
25
|
+
*/
|
|
26
|
+
export function clickOutside(node, callback) {
|
|
27
|
+
let handler = callback;
|
|
28
|
+
|
|
29
|
+
function handleClick(event) {
|
|
30
|
+
if (!node.contains(event.target) && !event.defaultPrevented) {
|
|
31
|
+
handler?.();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Use mousedown for better UX (fires before focus changes)
|
|
36
|
+
document.addEventListener('mousedown', handleClick, true);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
update(newCallback) {
|
|
40
|
+
handler = newCallback;
|
|
41
|
+
},
|
|
42
|
+
destroy() {
|
|
43
|
+
document.removeEventListener('mousedown', handleClick, true);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Focus Trap Action
|
|
50
|
+
*
|
|
51
|
+
* Traps focus within an element. Essential for accessible modals.
|
|
52
|
+
*
|
|
53
|
+
* @param {HTMLElement} node
|
|
54
|
+
* @param {{ enabled?: boolean, initialFocus?: string }} [options]
|
|
55
|
+
* @returns {{ destroy: () => void, update: (opts: { enabled?: boolean }) => void }}
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* <dialog use:focusTrap={{ enabled: isOpen }}>...</dialog>
|
|
59
|
+
*/
|
|
60
|
+
export function focusTrap(node, options = {}) {
|
|
61
|
+
let { enabled = true, initialFocus } = options;
|
|
62
|
+
|
|
63
|
+
const focusableSelectors = [
|
|
64
|
+
'a[href]',
|
|
65
|
+
'button:not([disabled])',
|
|
66
|
+
'input:not([disabled])',
|
|
67
|
+
'select:not([disabled])',
|
|
68
|
+
'textarea:not([disabled])',
|
|
69
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
70
|
+
].join(',');
|
|
71
|
+
|
|
72
|
+
function getFocusable() {
|
|
73
|
+
return Array.from(node.querySelectorAll(focusableSelectors));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleKeydown(event) {
|
|
77
|
+
if (!enabled || event.key !== 'Tab') return;
|
|
78
|
+
|
|
79
|
+
const focusable = getFocusable();
|
|
80
|
+
if (focusable.length === 0) return;
|
|
81
|
+
|
|
82
|
+
const first = focusable[0];
|
|
83
|
+
const last = focusable[focusable.length - 1];
|
|
84
|
+
|
|
85
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
last.focus();
|
|
88
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
first.focus();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function setInitialFocus() {
|
|
95
|
+
if (!enabled) return;
|
|
96
|
+
|
|
97
|
+
const target = initialFocus
|
|
98
|
+
? node.querySelector(initialFocus)
|
|
99
|
+
: getFocusable()[0];
|
|
100
|
+
|
|
101
|
+
target?.focus();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
node.addEventListener('keydown', handleKeydown);
|
|
105
|
+
// Delay initial focus to allow transitions
|
|
106
|
+
requestAnimationFrame(setInitialFocus);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
update(newOptions) {
|
|
110
|
+
enabled = newOptions.enabled ?? true;
|
|
111
|
+
initialFocus = newOptions.initialFocus;
|
|
112
|
+
if (enabled) setInitialFocus();
|
|
113
|
+
},
|
|
114
|
+
destroy() {
|
|
115
|
+
node.removeEventListener('keydown', handleKeydown);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Auto-focus Action
|
|
122
|
+
*
|
|
123
|
+
* Focuses element on mount.
|
|
124
|
+
*
|
|
125
|
+
* @param {HTMLElement} node
|
|
126
|
+
* @param {{ delay?: number, select?: boolean }} [options]
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* <input use:autoFocus />
|
|
130
|
+
* <input use:autoFocus={{ delay: 100, select: true }} />
|
|
131
|
+
*/
|
|
132
|
+
export function autoFocus(node, options = {}) {
|
|
133
|
+
const { delay = 0, select = false } = options;
|
|
134
|
+
|
|
135
|
+
const timeout = setTimeout(() => {
|
|
136
|
+
node.focus();
|
|
137
|
+
if (select && 'select' in node) {
|
|
138
|
+
node.select();
|
|
139
|
+
}
|
|
140
|
+
}, delay);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
destroy() {
|
|
144
|
+
clearTimeout(timeout);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Long Press Action
|
|
151
|
+
*
|
|
152
|
+
* Detects long press/hold gesture.
|
|
153
|
+
*
|
|
154
|
+
* @param {HTMLElement} node
|
|
155
|
+
* @param {{ duration?: number, onLongPress: () => void }} options
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* <button use:longPress={{ duration: 500, onLongPress: handleLongPress }}>
|
|
159
|
+
* Hold me
|
|
160
|
+
* </button>
|
|
161
|
+
*/
|
|
162
|
+
export function longPress(node, options) {
|
|
163
|
+
let { duration = 500, onLongPress } = options;
|
|
164
|
+
let timeout = null;
|
|
165
|
+
|
|
166
|
+
function handleStart() {
|
|
167
|
+
timeout = setTimeout(() => {
|
|
168
|
+
onLongPress?.();
|
|
169
|
+
}, duration);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function handleEnd() {
|
|
173
|
+
if (timeout) {
|
|
174
|
+
clearTimeout(timeout);
|
|
175
|
+
timeout = null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
node.addEventListener('mousedown', handleStart);
|
|
180
|
+
node.addEventListener('touchstart', handleStart, { passive: true });
|
|
181
|
+
node.addEventListener('mouseup', handleEnd);
|
|
182
|
+
node.addEventListener('mouseleave', handleEnd);
|
|
183
|
+
node.addEventListener('touchend', handleEnd);
|
|
184
|
+
node.addEventListener('touchcancel', handleEnd);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
update(newOptions) {
|
|
188
|
+
duration = newOptions.duration ?? 500;
|
|
189
|
+
onLongPress = newOptions.onLongPress;
|
|
190
|
+
},
|
|
191
|
+
destroy() {
|
|
192
|
+
handleEnd();
|
|
193
|
+
node.removeEventListener('mousedown', handleStart);
|
|
194
|
+
node.removeEventListener('touchstart', handleStart);
|
|
195
|
+
node.removeEventListener('mouseup', handleEnd);
|
|
196
|
+
node.removeEventListener('mouseleave', handleEnd);
|
|
197
|
+
node.removeEventListener('touchend', handleEnd);
|
|
198
|
+
node.removeEventListener('touchcancel', handleEnd);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Escape Key Action
|
|
205
|
+
*
|
|
206
|
+
* Fires callback when Escape is pressed while element or children have focus.
|
|
207
|
+
*
|
|
208
|
+
* @param {HTMLElement} node
|
|
209
|
+
* @param {() => void} callback
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* <dialog use:escapeKey={handleClose}>...</dialog>
|
|
213
|
+
*/
|
|
214
|
+
export function escapeKey(node, callback) {
|
|
215
|
+
let handler = callback;
|
|
216
|
+
|
|
217
|
+
function handleKeydown(event) {
|
|
218
|
+
if (event.key === 'Escape') {
|
|
219
|
+
event.preventDefault();
|
|
220
|
+
handler?.();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Listen on document for global escape handling
|
|
225
|
+
document.addEventListener('keydown', handleKeydown);
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
update(newCallback) {
|
|
229
|
+
handler = newCallback;
|
|
230
|
+
},
|
|
231
|
+
destroy() {
|
|
232
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Portal Action
|
|
239
|
+
*
|
|
240
|
+
* Moves element to a different location in the DOM.
|
|
241
|
+
* Useful for modals, toasts, tooltips.
|
|
242
|
+
*
|
|
243
|
+
* @param {HTMLElement} node
|
|
244
|
+
* @param {string | HTMLElement} [target] - CSS selector or element (default: document.body)
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* <div use:portal>Portaled to body</div>
|
|
248
|
+
* <div use:portal="#modal-root">Portaled to #modal-root</div>
|
|
249
|
+
*/
|
|
250
|
+
export function portal(node, target = 'body') {
|
|
251
|
+
let targetEl;
|
|
252
|
+
|
|
253
|
+
function update(newTarget) {
|
|
254
|
+
targetEl = typeof newTarget === 'string'
|
|
255
|
+
? document.querySelector(newTarget)
|
|
256
|
+
: newTarget;
|
|
257
|
+
|
|
258
|
+
if (targetEl) {
|
|
259
|
+
targetEl.appendChild(node);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
update(target);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
update,
|
|
267
|
+
destroy() {
|
|
268
|
+
node.remove();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Intersection Observer Action
|
|
275
|
+
*
|
|
276
|
+
* Observes element visibility in viewport.
|
|
277
|
+
*
|
|
278
|
+
* @param {HTMLElement} node
|
|
279
|
+
* @param {{ onIntersect: (entry: IntersectionObserverEntry) => void, options?: IntersectionObserverInit }} config
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* <div use:intersect={{ onIntersect: (e) => isVisible = e.isIntersecting }}>
|
|
283
|
+
* Lazy loaded content
|
|
284
|
+
* </div>
|
|
285
|
+
*/
|
|
286
|
+
export function intersect(node, config) {
|
|
287
|
+
let { onIntersect, options = {} } = config;
|
|
288
|
+
|
|
289
|
+
const observer = new IntersectionObserver((entries) => {
|
|
290
|
+
entries.forEach(entry => onIntersect?.(entry));
|
|
291
|
+
}, options);
|
|
292
|
+
|
|
293
|
+
observer.observe(node);
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
update(newConfig) {
|
|
297
|
+
onIntersect = newConfig.onIntersect;
|
|
298
|
+
},
|
|
299
|
+
destroy() {
|
|
300
|
+
observer.disconnect();
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Resize Observer Action
|
|
307
|
+
*
|
|
308
|
+
* Observes element size changes.
|
|
309
|
+
*
|
|
310
|
+
* @param {HTMLElement} node
|
|
311
|
+
* @param {(entry: ResizeObserverEntry) => void} callback
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* <div use:resize={(entry) => width = entry.contentRect.width}>
|
|
315
|
+
* Responsive content
|
|
316
|
+
* </div>
|
|
317
|
+
*/
|
|
318
|
+
export function resize(node, callback) {
|
|
319
|
+
let handler = callback;
|
|
320
|
+
|
|
321
|
+
const observer = new ResizeObserver((entries) => {
|
|
322
|
+
entries.forEach(entry => handler?.(entry));
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
observer.observe(node);
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
update(newCallback) {
|
|
329
|
+
handler = newCallback;
|
|
330
|
+
},
|
|
331
|
+
destroy() {
|
|
332
|
+
observer.disconnect();
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Copy to Clipboard Action
|
|
339
|
+
*
|
|
340
|
+
* Copies element's text content or provided value on click.
|
|
341
|
+
*
|
|
342
|
+
* @param {HTMLElement} node
|
|
343
|
+
* @param {{ value?: string, onCopy?: () => void, onError?: (err: Error) => void }} [options]
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* <button use:copy={{ value: 'Text to copy', onCopy: () => showToast('Copied!') }}>
|
|
347
|
+
* Copy
|
|
348
|
+
* </button>
|
|
349
|
+
*/
|
|
350
|
+
export function copy(node, options = {}) {
|
|
351
|
+
let { value, onCopy, onError } = options;
|
|
352
|
+
|
|
353
|
+
async function handleClick() {
|
|
354
|
+
try {
|
|
355
|
+
const text = value ?? node.textContent ?? '';
|
|
356
|
+
await navigator.clipboard.writeText(text);
|
|
357
|
+
onCopy?.();
|
|
358
|
+
} catch (err) {
|
|
359
|
+
onError?.(err);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
node.addEventListener('click', handleClick);
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
update(newOptions) {
|
|
367
|
+
value = newOptions.value;
|
|
368
|
+
onCopy = newOptions.onCopy;
|
|
369
|
+
onError = newOptions.onError;
|
|
370
|
+
},
|
|
371
|
+
destroy() {
|
|
372
|
+
node.removeEventListener('click', handleClick);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component EmptyState
|
|
3
|
+
|
|
4
|
+
A complete empty state UI with icon, title, description, and action buttons.
|
|
5
|
+
|
|
6
|
+
@example Basic
|
|
7
|
+
<EmptyState
|
|
8
|
+
title="No items found"
|
|
9
|
+
description="Try adjusting your search or filters"
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
@example With icon and action
|
|
13
|
+
<EmptyState
|
|
14
|
+
title="No messages"
|
|
15
|
+
description="Start a conversation to see messages here"
|
|
16
|
+
>
|
|
17
|
+
{#snippet icon()}
|
|
18
|
+
<MessageIcon size={32} />
|
|
19
|
+
{/snippet}
|
|
20
|
+
{#snippet actions()}
|
|
21
|
+
<Button variant="primary" onclick={startChat}>New Message</Button>
|
|
22
|
+
{/snippet}
|
|
23
|
+
</EmptyState>
|
|
24
|
+
|
|
25
|
+
@example Compact size
|
|
26
|
+
<EmptyState size="compact" title="No results" />
|
|
27
|
+
-->
|
|
28
|
+
<script>
|
|
29
|
+
let {
|
|
30
|
+
title = 'No data found',
|
|
31
|
+
description = '',
|
|
32
|
+
size = 'default',
|
|
33
|
+
class: className = '',
|
|
34
|
+
icon,
|
|
35
|
+
actions
|
|
36
|
+
} = $props();
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<div class="empty-state empty-state-{size} {className}">
|
|
40
|
+
<div class="empty-state-content">
|
|
41
|
+
{#if icon}
|
|
42
|
+
<div class="empty-state-icon">
|
|
43
|
+
{@render icon()}
|
|
44
|
+
</div>
|
|
45
|
+
{/if}
|
|
46
|
+
|
|
47
|
+
<div class="empty-state-text">
|
|
48
|
+
<h3 class="empty-state-title">{title}</h3>
|
|
49
|
+
{#if description}
|
|
50
|
+
<p class="empty-state-description">{description}</p>
|
|
51
|
+
{/if}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{#if actions}
|
|
55
|
+
<div class="empty-state-actions">
|
|
56
|
+
{@render actions()}
|
|
57
|
+
</div>
|
|
58
|
+
{/if}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
.empty-state {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
width: 100%;
|
|
68
|
+
padding: var(--space-16) var(--space-6);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.empty-state-compact {
|
|
72
|
+
padding: var(--space-8) var(--space-4);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.empty-state-large {
|
|
76
|
+
padding: var(--space-24) var(--space-6);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.empty-state-content {
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
align-items: center;
|
|
83
|
+
text-align: center;
|
|
84
|
+
max-width: 24rem;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.empty-state-icon {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
justify-content: center;
|
|
91
|
+
width: 4rem;
|
|
92
|
+
height: 4rem;
|
|
93
|
+
margin-bottom: var(--space-6);
|
|
94
|
+
border-radius: 9999px;
|
|
95
|
+
background: var(--color-surface);
|
|
96
|
+
color: var(--color-text-muted);
|
|
97
|
+
transition: background 0.3s ease, color 0.3s ease, transform 0.3s ease;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.empty-state-compact .empty-state-icon {
|
|
101
|
+
width: 3rem;
|
|
102
|
+
height: 3rem;
|
|
103
|
+
margin-bottom: var(--space-4);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.empty-state-large .empty-state-icon {
|
|
107
|
+
width: 5rem;
|
|
108
|
+
height: 5rem;
|
|
109
|
+
margin-bottom: var(--space-8);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.empty-state:hover .empty-state-icon {
|
|
113
|
+
background: var(--color-surface-hover);
|
|
114
|
+
color: var(--color-text);
|
|
115
|
+
transform: scale(1.05);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.empty-state-text {
|
|
119
|
+
margin-bottom: var(--space-6);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.empty-state-compact .empty-state-text {
|
|
123
|
+
margin-bottom: var(--space-4);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.empty-state-large .empty-state-text {
|
|
127
|
+
margin-bottom: var(--space-8);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.empty-state-title {
|
|
131
|
+
margin: 0 0 var(--space-2) 0;
|
|
132
|
+
font-size: var(--text-lg);
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
color: var(--color-text-strong);
|
|
135
|
+
line-height: 1.3;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.empty-state-compact .empty-state-title {
|
|
139
|
+
font-size: var(--text-base);
|
|
140
|
+
font-weight: 500;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.empty-state-large .empty-state-title {
|
|
144
|
+
font-size: var(--text-xl);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.empty-state-description {
|
|
148
|
+
margin: 0;
|
|
149
|
+
font-size: var(--text-sm);
|
|
150
|
+
color: var(--color-text-muted);
|
|
151
|
+
line-height: 1.5;
|
|
152
|
+
max-width: 20rem;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.empty-state-compact .empty-state-description {
|
|
156
|
+
font-size: var(--text-xs);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.empty-state-large .empty-state-description {
|
|
160
|
+
font-size: var(--text-base);
|
|
161
|
+
max-width: 24rem;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.empty-state-actions {
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: var(--space-3);
|
|
168
|
+
flex-wrap: wrap;
|
|
169
|
+
justify-content: center;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.empty-state-compact .empty-state-actions {
|
|
173
|
+
gap: var(--space-2);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.empty-state-large .empty-state-actions {
|
|
177
|
+
gap: var(--space-4);
|
|
178
|
+
}
|
|
179
|
+
</style>
|