@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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Avatar
|
|
3
|
+
|
|
4
|
+
User avatar with image, initials fallback, and status indicator.
|
|
5
|
+
|
|
6
|
+
@example With image
|
|
7
|
+
<Avatar src="/user.jpg" alt="John Doe" />
|
|
8
|
+
|
|
9
|
+
@example With initials fallback
|
|
10
|
+
<Avatar name="John Doe" />
|
|
11
|
+
|
|
12
|
+
@example With status
|
|
13
|
+
<Avatar src="/user.jpg" status="online" />
|
|
14
|
+
-->
|
|
15
|
+
<script>
|
|
16
|
+
import { cv } from '../../utils/cn.svelte.js';
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
src = '',
|
|
20
|
+
alt = '',
|
|
21
|
+
name = '',
|
|
22
|
+
size = 'md',
|
|
23
|
+
status = null,
|
|
24
|
+
class: className = ''
|
|
25
|
+
} = $props();
|
|
26
|
+
|
|
27
|
+
// Generate initials from name
|
|
28
|
+
const initials = $derived.by(() => {
|
|
29
|
+
if (!name) return '';
|
|
30
|
+
const parts = name.trim().split(/\s+/);
|
|
31
|
+
if (parts.length >= 2) {
|
|
32
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
33
|
+
}
|
|
34
|
+
return parts[0].slice(0, 2).toUpperCase();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Generate consistent color from name
|
|
38
|
+
const bgColor = $derived.by(() => {
|
|
39
|
+
if (!name) return 'var(--color-border)';
|
|
40
|
+
let hash = 0;
|
|
41
|
+
for (let i = 0; i < name.length; i++) {
|
|
42
|
+
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
43
|
+
}
|
|
44
|
+
const colors = [
|
|
45
|
+
'var(--color-error)', // red
|
|
46
|
+
'var(--orange)', // orange
|
|
47
|
+
'var(--color-success)', // green
|
|
48
|
+
'var(--color-warning)', // yellow
|
|
49
|
+
'var(--color-info)', // blue
|
|
50
|
+
'var(--color-primary)', // magenta
|
|
51
|
+
'var(--peach)', // peach
|
|
52
|
+
'var(--color-accent)' // cyan
|
|
53
|
+
];
|
|
54
|
+
return colors[Math.abs(hash) % colors.length];
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
let imgError = $state(false);
|
|
58
|
+
const showImage = $derived(src && !imgError);
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<div class="avatar avatar-{size} {className}">
|
|
62
|
+
{#if showImage}
|
|
63
|
+
<img
|
|
64
|
+
{src}
|
|
65
|
+
alt={alt || name}
|
|
66
|
+
class="avatar-image"
|
|
67
|
+
onerror={() => imgError = true}
|
|
68
|
+
/>
|
|
69
|
+
{:else}
|
|
70
|
+
<span class="avatar-initials" style="background: {bgColor};">
|
|
71
|
+
{initials}
|
|
72
|
+
</span>
|
|
73
|
+
{/if}
|
|
74
|
+
|
|
75
|
+
{#if status}
|
|
76
|
+
<span class="avatar-status avatar-status-{status}"></span>
|
|
77
|
+
{/if}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<style>
|
|
81
|
+
.avatar {
|
|
82
|
+
position: relative;
|
|
83
|
+
display: inline-flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: center;
|
|
86
|
+
border-radius: 50%;
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
flex-shrink: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Size variants */
|
|
92
|
+
.avatar-xs { width: 1.5rem; height: 1.5rem; font-size: 0.625rem; }
|
|
93
|
+
.avatar-sm { width: 2rem; height: 2rem; font-size: 0.75rem; }
|
|
94
|
+
.avatar-md { width: 2.5rem; height: 2.5rem; font-size: 0.875rem; }
|
|
95
|
+
.avatar-lg { width: 3rem; height: 3rem; font-size: 1rem; }
|
|
96
|
+
.avatar-xl { width: 4rem; height: 4rem; font-size: 1.25rem; }
|
|
97
|
+
.avatar-2xl { width: 5rem; height: 5rem; font-size: 1.5rem; }
|
|
98
|
+
|
|
99
|
+
.avatar-image {
|
|
100
|
+
width: 100%;
|
|
101
|
+
height: 100%;
|
|
102
|
+
object-fit: cover;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.avatar-initials {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: center;
|
|
109
|
+
width: 100%;
|
|
110
|
+
height: 100%;
|
|
111
|
+
color: white;
|
|
112
|
+
font-weight: 600;
|
|
113
|
+
letter-spacing: 0.02em;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.avatar-status {
|
|
117
|
+
position: absolute;
|
|
118
|
+
bottom: 0;
|
|
119
|
+
right: 0;
|
|
120
|
+
width: 25%;
|
|
121
|
+
height: 25%;
|
|
122
|
+
min-width: 8px;
|
|
123
|
+
min-height: 8px;
|
|
124
|
+
border-radius: 50%;
|
|
125
|
+
border: 2px solid var(--color-bg);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.avatar-status-online { background: var(--color-success); }
|
|
129
|
+
.avatar-status-offline { background: var(--color-text-muted); }
|
|
130
|
+
.avatar-status-busy { background: var(--color-error); }
|
|
131
|
+
.avatar-status-away { background: var(--color-warning); }
|
|
132
|
+
</style>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Badge
|
|
3
|
+
|
|
4
|
+
A versatile badge/tag component for status, labels, and counts.
|
|
5
|
+
Consolidates Badge, StatusBadge, and Status patterns into one component.
|
|
6
|
+
|
|
7
|
+
@example
|
|
8
|
+
<Badge>Default</Badge>
|
|
9
|
+
<Badge variant="success">Active</Badge>
|
|
10
|
+
<Badge variant="error" size="sm">Error</Badge>
|
|
11
|
+
|
|
12
|
+
@example
|
|
13
|
+
// With icon
|
|
14
|
+
<Badge variant="primary">
|
|
15
|
+
{#snippet iconLeft()}<CheckIcon size={12} />{/snippet}
|
|
16
|
+
Verified
|
|
17
|
+
</Badge>
|
|
18
|
+
|
|
19
|
+
@example
|
|
20
|
+
// Clickable badge
|
|
21
|
+
<Badge onclick={() => filter('active')} clickable>Active</Badge>
|
|
22
|
+
-->
|
|
23
|
+
<script module>
|
|
24
|
+
import { cv } from '../../utils/cn.svelte.js';
|
|
25
|
+
|
|
26
|
+
export const badgeStyles = cv({
|
|
27
|
+
base: [
|
|
28
|
+
'inline-flex items-center justify-center gap-1',
|
|
29
|
+
'font-medium rounded-full',
|
|
30
|
+
'transition-colors'
|
|
31
|
+
].join(' '),
|
|
32
|
+
|
|
33
|
+
variants: {
|
|
34
|
+
variant: {
|
|
35
|
+
default: 'bg-[var(--color-surface-alt)] text-[var(--color-text)]',
|
|
36
|
+
primary: 'bg-[color-mix(in_srgb,var(--color-primary)_15%,transparent)] text-[var(--color-primary)]',
|
|
37
|
+
secondary: 'bg-[color-mix(in_srgb,var(--color-secondary)_15%,transparent)] text-[var(--color-secondary)]',
|
|
38
|
+
success: 'bg-[color-mix(in_srgb,var(--color-success)_15%,transparent)] text-[var(--color-success)]',
|
|
39
|
+
warning: 'bg-[color-mix(in_srgb,var(--color-warning)_15%,transparent)] text-[var(--color-warning)]',
|
|
40
|
+
error: 'bg-[color-mix(in_srgb,var(--color-error)_15%,transparent)] text-[var(--color-error)]',
|
|
41
|
+
info: 'bg-[color-mix(in_srgb,var(--color-info)_15%,transparent)] text-[var(--color-info)]'
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
size: {
|
|
45
|
+
sm: 'px-2 py-0.5 text-xs',
|
|
46
|
+
md: 'px-2.5 py-1 text-xs',
|
|
47
|
+
lg: 'px-3 py-1.5 text-sm'
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
clickable: {
|
|
51
|
+
true: 'cursor-pointer hover:opacity-80',
|
|
52
|
+
false: ''
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
defaults: {
|
|
57
|
+
variant: 'default',
|
|
58
|
+
size: 'md',
|
|
59
|
+
clickable: 'false'
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<script>
|
|
65
|
+
import { cn } from '../../utils/cn.svelte.js';
|
|
66
|
+
|
|
67
|
+
let {
|
|
68
|
+
children,
|
|
69
|
+
iconLeft,
|
|
70
|
+
iconRight,
|
|
71
|
+
variant = 'default',
|
|
72
|
+
size = 'md',
|
|
73
|
+
clickable = false,
|
|
74
|
+
class: className = '',
|
|
75
|
+
onclick,
|
|
76
|
+
...rest
|
|
77
|
+
} = $props();
|
|
78
|
+
|
|
79
|
+
const badgeClass = $derived(
|
|
80
|
+
badgeStyles({
|
|
81
|
+
variant,
|
|
82
|
+
size,
|
|
83
|
+
clickable: (clickable || onclick) ? 'true' : 'false',
|
|
84
|
+
class: className
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
{#if onclick || clickable}
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
class={badgeClass}
|
|
93
|
+
{onclick}
|
|
94
|
+
{...rest}
|
|
95
|
+
>
|
|
96
|
+
{#if iconLeft}
|
|
97
|
+
<span class="shrink-0">{@render iconLeft()}</span>
|
|
98
|
+
{/if}
|
|
99
|
+
{#if children}
|
|
100
|
+
{@render children()}
|
|
101
|
+
{/if}
|
|
102
|
+
{#if iconRight}
|
|
103
|
+
<span class="shrink-0">{@render iconRight()}</span>
|
|
104
|
+
{/if}
|
|
105
|
+
</button>
|
|
106
|
+
{:else}
|
|
107
|
+
<span class={badgeClass} {...rest}>
|
|
108
|
+
{#if iconLeft}
|
|
109
|
+
<span class="shrink-0">{@render iconLeft()}</span>
|
|
110
|
+
{/if}
|
|
111
|
+
{#if children}
|
|
112
|
+
{@render children()}
|
|
113
|
+
{/if}
|
|
114
|
+
{#if iconRight}
|
|
115
|
+
<span class="shrink-0">{@render iconRight()}</span>
|
|
116
|
+
{/if}
|
|
117
|
+
</span>
|
|
118
|
+
{/if}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Button
|
|
3
|
+
|
|
4
|
+
A polymorphic button component with variants, sizes, and loading states.
|
|
5
|
+
Demonstrates advanced Svelte 5 patterns:
|
|
6
|
+
|
|
7
|
+
- Polymorphic rendering (as button, a, or custom element)
|
|
8
|
+
- Reactive class composition with cv()
|
|
9
|
+
- Snippets for icon slots
|
|
10
|
+
- $derived for computed properties
|
|
11
|
+
- TypeScript-style prop definitions
|
|
12
|
+
|
|
13
|
+
@example
|
|
14
|
+
<Button>Click me</Button>
|
|
15
|
+
<Button variant="secondary" size="lg">Large Secondary</Button>
|
|
16
|
+
<Button href="/about">Link Button</Button>
|
|
17
|
+
<Button loading>Loading...</Button>
|
|
18
|
+
<Button disabled>Disabled</Button>
|
|
19
|
+
<Button>
|
|
20
|
+
{#snippet iconLeft()}<IconPlus />{/snippet}
|
|
21
|
+
Add Item
|
|
22
|
+
</Button>
|
|
23
|
+
-->
|
|
24
|
+
<script module>
|
|
25
|
+
import { cv } from '../../utils/cn.svelte.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Button style variants using cv() for type-safe composition
|
|
29
|
+
*/
|
|
30
|
+
export const buttonStyles = cv({
|
|
31
|
+
base: [
|
|
32
|
+
'inline-flex items-center justify-center gap-2',
|
|
33
|
+
'font-medium rounded-lg',
|
|
34
|
+
'transition-all duration-150 ease-out',
|
|
35
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
|
|
36
|
+
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none',
|
|
37
|
+
'select-none'
|
|
38
|
+
].join(' '),
|
|
39
|
+
|
|
40
|
+
variants: {
|
|
41
|
+
variant: {
|
|
42
|
+
primary: [
|
|
43
|
+
'bg-primary text-white',
|
|
44
|
+
'hover:brightness-110 active:brightness-95',
|
|
45
|
+
'focus-visible:ring-primary/50'
|
|
46
|
+
].join(' '),
|
|
47
|
+
|
|
48
|
+
secondary: [
|
|
49
|
+
'bg-surface-alt text-text-strong border border-border',
|
|
50
|
+
'hover:bg-hover hover:border-muted',
|
|
51
|
+
'focus-visible:ring-border'
|
|
52
|
+
].join(' '),
|
|
53
|
+
|
|
54
|
+
ghost: [
|
|
55
|
+
'bg-transparent text-text',
|
|
56
|
+
'hover:bg-hover',
|
|
57
|
+
'focus-visible:ring-border'
|
|
58
|
+
].join(' '),
|
|
59
|
+
|
|
60
|
+
outline: [
|
|
61
|
+
'bg-transparent text-primary border border-primary/40',
|
|
62
|
+
'hover:bg-primary/5 hover:border-primary',
|
|
63
|
+
'focus-visible:ring-primary/50'
|
|
64
|
+
].join(' '),
|
|
65
|
+
|
|
66
|
+
danger: [
|
|
67
|
+
'bg-error text-white',
|
|
68
|
+
'hover:brightness-110 active:brightness-95',
|
|
69
|
+
'focus-visible:ring-error/50'
|
|
70
|
+
].join(' '),
|
|
71
|
+
|
|
72
|
+
success: [
|
|
73
|
+
'bg-success text-white',
|
|
74
|
+
'hover:brightness-110 active:brightness-95',
|
|
75
|
+
'focus-visible:ring-success/50'
|
|
76
|
+
].join(' ')
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
size: {
|
|
80
|
+
xs: 'h-7 px-2.5 text-xs',
|
|
81
|
+
sm: 'h-8 px-3 text-sm',
|
|
82
|
+
md: 'h-10 px-4 text-sm',
|
|
83
|
+
lg: 'h-12 px-6 text-base',
|
|
84
|
+
xl: 'h-14 px-8 text-lg'
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
fullWidth: {
|
|
88
|
+
true: 'w-full',
|
|
89
|
+
false: ''
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
iconOnly: {
|
|
93
|
+
true: 'aspect-square p-0',
|
|
94
|
+
false: ''
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
compounds: [
|
|
99
|
+
// Icon-only buttons need adjusted padding
|
|
100
|
+
{ condition: { iconOnly: 'true', size: 'xs' }, class: 'w-7' },
|
|
101
|
+
{ condition: { iconOnly: 'true', size: 'sm' }, class: 'w-8' },
|
|
102
|
+
{ condition: { iconOnly: 'true', size: 'md' }, class: 'w-10' },
|
|
103
|
+
{ condition: { iconOnly: 'true', size: 'lg' }, class: 'w-12' },
|
|
104
|
+
{ condition: { iconOnly: 'true', size: 'xl' }, class: 'w-14' }
|
|
105
|
+
],
|
|
106
|
+
|
|
107
|
+
defaults: {
|
|
108
|
+
variant: 'primary',
|
|
109
|
+
size: 'md',
|
|
110
|
+
fullWidth: 'false',
|
|
111
|
+
iconOnly: 'false'
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<script>
|
|
117
|
+
import { cn } from '../../utils/cn.svelte.js';
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @typedef {import('svelte').Snippet} Snippet
|
|
121
|
+
* @typedef {'primary' | 'secondary' | 'ghost' | 'outline' | 'danger' | 'success'} Variant
|
|
122
|
+
* @typedef {'xs' | 'sm' | 'md' | 'lg' | 'xl'} Size
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
let {
|
|
126
|
+
children,
|
|
127
|
+
iconLeft,
|
|
128
|
+
iconRight,
|
|
129
|
+
variant = 'primary',
|
|
130
|
+
size = 'md',
|
|
131
|
+
disabled = false,
|
|
132
|
+
loading = false,
|
|
133
|
+
fullWidth = false,
|
|
134
|
+
href,
|
|
135
|
+
type = 'button',
|
|
136
|
+
class: className = '',
|
|
137
|
+
onclick,
|
|
138
|
+
...restProps
|
|
139
|
+
} = $props();
|
|
140
|
+
|
|
141
|
+
// Derived state
|
|
142
|
+
const isLink = $derived(!!href);
|
|
143
|
+
const isIconOnly = $derived(!children && (!!iconLeft || !!iconRight));
|
|
144
|
+
const isDisabled = $derived(disabled || loading);
|
|
145
|
+
|
|
146
|
+
// Reactive class composition
|
|
147
|
+
const buttonClass = $derived(
|
|
148
|
+
buttonStyles({
|
|
149
|
+
variant,
|
|
150
|
+
size,
|
|
151
|
+
fullWidth: fullWidth ? 'true' : 'false',
|
|
152
|
+
iconOnly: isIconOnly ? 'true' : 'false',
|
|
153
|
+
class: className
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Loading spinner
|
|
158
|
+
const spinnerSize = $derived({
|
|
159
|
+
xs: 12,
|
|
160
|
+
sm: 14,
|
|
161
|
+
md: 16,
|
|
162
|
+
lg: 18,
|
|
163
|
+
xl: 20
|
|
164
|
+
}[size]);
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<!--
|
|
168
|
+
Polymorphic rendering: renders as <a> if href provided, otherwise <button>
|
|
169
|
+
Uses Svelte 5's element binding with spread props
|
|
170
|
+
-->
|
|
171
|
+
{#if isLink}
|
|
172
|
+
<a
|
|
173
|
+
{href}
|
|
174
|
+
class={buttonClass}
|
|
175
|
+
aria-disabled={isDisabled || undefined}
|
|
176
|
+
role="button"
|
|
177
|
+
{...restProps}
|
|
178
|
+
>
|
|
179
|
+
{#if loading}
|
|
180
|
+
<svg
|
|
181
|
+
class="animate-spin"
|
|
182
|
+
width={spinnerSize}
|
|
183
|
+
height={spinnerSize}
|
|
184
|
+
viewBox="0 0 24 24"
|
|
185
|
+
fill="none"
|
|
186
|
+
stroke="currentColor"
|
|
187
|
+
stroke-width="2"
|
|
188
|
+
>
|
|
189
|
+
<circle cx="12" cy="12" r="10" opacity="0.25" />
|
|
190
|
+
<path d="M12 2a10 10 0 0 1 10 10" opacity="0.75" />
|
|
191
|
+
</svg>
|
|
192
|
+
{:else if iconLeft}
|
|
193
|
+
<span class="inline-flex shrink-0">
|
|
194
|
+
{@render iconLeft()}
|
|
195
|
+
</span>
|
|
196
|
+
{/if}
|
|
197
|
+
|
|
198
|
+
{#if children}
|
|
199
|
+
<span class={cn(loading && 'opacity-0')}>
|
|
200
|
+
{@render children()}
|
|
201
|
+
</span>
|
|
202
|
+
{/if}
|
|
203
|
+
|
|
204
|
+
{#if iconRight && !loading}
|
|
205
|
+
<span class="inline-flex shrink-0">
|
|
206
|
+
{@render iconRight()}
|
|
207
|
+
</span>
|
|
208
|
+
{/if}
|
|
209
|
+
</a>
|
|
210
|
+
{:else}
|
|
211
|
+
<button
|
|
212
|
+
{type}
|
|
213
|
+
class={buttonClass}
|
|
214
|
+
disabled={isDisabled}
|
|
215
|
+
aria-busy={loading || undefined}
|
|
216
|
+
{onclick}
|
|
217
|
+
{...restProps}
|
|
218
|
+
>
|
|
219
|
+
{#if loading}
|
|
220
|
+
<svg
|
|
221
|
+
class="animate-spin"
|
|
222
|
+
width={spinnerSize}
|
|
223
|
+
height={spinnerSize}
|
|
224
|
+
viewBox="0 0 24 24"
|
|
225
|
+
fill="none"
|
|
226
|
+
stroke="currentColor"
|
|
227
|
+
stroke-width="2"
|
|
228
|
+
>
|
|
229
|
+
<circle cx="12" cy="12" r="10" opacity="0.25" />
|
|
230
|
+
<path d="M12 2a10 10 0 0 1 10 10" opacity="0.75" />
|
|
231
|
+
</svg>
|
|
232
|
+
{:else if iconLeft}
|
|
233
|
+
<span class="inline-flex shrink-0">
|
|
234
|
+
{@render iconLeft()}
|
|
235
|
+
</span>
|
|
236
|
+
{/if}
|
|
237
|
+
|
|
238
|
+
{#if children}
|
|
239
|
+
<span class={cn(loading && 'opacity-70')}>
|
|
240
|
+
{@render children()}
|
|
241
|
+
</span>
|
|
242
|
+
{/if}
|
|
243
|
+
|
|
244
|
+
{#if iconRight && !loading}
|
|
245
|
+
<span class="inline-flex shrink-0">
|
|
246
|
+
{@render iconRight()}
|
|
247
|
+
</span>
|
|
248
|
+
{/if}
|
|
249
|
+
</button>
|
|
250
|
+
{/if}
|
|
251
|
+
|
|
252
|
+
<style>
|
|
253
|
+
@keyframes spin {
|
|
254
|
+
to {
|
|
255
|
+
transform: rotate(360deg);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.animate-spin {
|
|
260
|
+
animation: spin 1s linear infinite;
|
|
261
|
+
}
|
|
262
|
+
</style>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Card
|
|
3
|
+
|
|
4
|
+
A flexible card container with optional title and variants.
|
|
5
|
+
|
|
6
|
+
@example Basic
|
|
7
|
+
<Card>
|
|
8
|
+
<p>Card content here</p>
|
|
9
|
+
</Card>
|
|
10
|
+
|
|
11
|
+
@example With title
|
|
12
|
+
<Card title="Settings">
|
|
13
|
+
<p>Your settings content</p>
|
|
14
|
+
</Card>
|
|
15
|
+
|
|
16
|
+
@example Danger variant
|
|
17
|
+
<Card title="Danger Zone" variant="danger">
|
|
18
|
+
<Button variant="danger">Delete Account</Button>
|
|
19
|
+
</Card>
|
|
20
|
+
-->
|
|
21
|
+
<script>
|
|
22
|
+
let {
|
|
23
|
+
title = '',
|
|
24
|
+
variant = 'default',
|
|
25
|
+
class: className = '',
|
|
26
|
+
children
|
|
27
|
+
} = $props();
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div class="card card-{variant} {className}">
|
|
31
|
+
{#if title}
|
|
32
|
+
<h3 class="card-title">{@html title}</h3>
|
|
33
|
+
{/if}
|
|
34
|
+
<div class="card-content">
|
|
35
|
+
{@render children?.()}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
.card {
|
|
41
|
+
background: transparent;
|
|
42
|
+
border: 1px solid var(--color-border-muted);
|
|
43
|
+
border-radius: var(--radius-xl);
|
|
44
|
+
padding: var(--space-6);
|
|
45
|
+
transition: border-color 0.2s ease;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.card:hover {
|
|
49
|
+
border-color: var(--color-border);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.card-danger {
|
|
53
|
+
border-color: color-mix(in srgb, var(--color-error) 30%, transparent);
|
|
54
|
+
background: color-mix(in srgb, var(--color-error) 3%, transparent);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.card-danger:hover {
|
|
58
|
+
border-color: color-mix(in srgb, var(--color-error) 40%, transparent);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.card-warning {
|
|
62
|
+
border-color: color-mix(in srgb, var(--color-warning) 30%, transparent);
|
|
63
|
+
background: color-mix(in srgb, var(--color-warning) 3%, transparent);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.card-warning:hover {
|
|
67
|
+
border-color: color-mix(in srgb, var(--color-warning) 40%, transparent);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.card-success {
|
|
71
|
+
border-color: color-mix(in srgb, var(--color-success) 30%, transparent);
|
|
72
|
+
background: color-mix(in srgb, var(--color-success) 3%, transparent);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.card-success:hover {
|
|
76
|
+
border-color: color-mix(in srgb, var(--color-success) 40%, transparent);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.card-title {
|
|
80
|
+
margin: 0 0 var(--space-5) 0;
|
|
81
|
+
font-size: var(--text-base);
|
|
82
|
+
font-weight: 500;
|
|
83
|
+
color: var(--color-text-strong);
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: var(--space-2);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.card-danger .card-title {
|
|
90
|
+
color: var(--color-error);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.card-warning .card-title {
|
|
94
|
+
color: var(--color-warning);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.card-success .card-title {
|
|
98
|
+
color: var(--color-success);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.card-content {
|
|
102
|
+
/* Allow child components to handle their own spacing */
|
|
103
|
+
}
|
|
104
|
+
</style>
|