@miozu/jera 0.0.2 → 0.4.2

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.
Files changed (82) hide show
  1. package/CLAUDE.md +734 -0
  2. package/README.md +219 -1
  3. package/llms.txt +97 -0
  4. package/package.json +54 -14
  5. package/src/actions/index.js +375 -0
  6. package/src/components/docs/CodeBlock.svelte +203 -0
  7. package/src/components/docs/DocSection.svelte +120 -0
  8. package/src/components/docs/PropsTable.svelte +136 -0
  9. package/src/components/docs/SplitPane.svelte +98 -0
  10. package/src/components/docs/index.js +14 -0
  11. package/src/components/feedback/Alert.svelte +234 -0
  12. package/src/components/feedback/EmptyState.svelte +179 -0
  13. package/src/components/feedback/ProgressBar.svelte +116 -0
  14. package/src/components/feedback/Skeleton.svelte +107 -0
  15. package/src/components/feedback/Spinner.svelte +77 -0
  16. package/src/components/feedback/Toast.svelte +261 -0
  17. package/src/components/forms/Checkbox.svelte +147 -0
  18. package/src/components/forms/Dropzone.svelte +248 -0
  19. package/src/components/forms/FileUpload.svelte +266 -0
  20. package/src/components/forms/IconInput.svelte +184 -0
  21. package/src/components/forms/Input.svelte +121 -0
  22. package/src/components/forms/NumberInput.svelte +225 -0
  23. package/src/components/forms/PinInput.svelte +169 -0
  24. package/src/components/forms/Radio.svelte +143 -0
  25. package/src/components/forms/RadioGroup.svelte +62 -0
  26. package/src/components/forms/RangeSlider.svelte +212 -0
  27. package/src/components/forms/SearchInput.svelte +175 -0
  28. package/src/components/forms/Select.svelte +324 -0
  29. package/src/components/forms/Switch.svelte +159 -0
  30. package/src/components/forms/Textarea.svelte +122 -0
  31. package/src/components/navigation/Accordion.svelte +65 -0
  32. package/src/components/navigation/AccordionItem.svelte +146 -0
  33. package/src/components/navigation/NavigationContainer.svelte +344 -0
  34. package/src/components/navigation/Sidebar.svelte +334 -0
  35. package/src/components/navigation/SidebarAccountGroup.svelte +495 -0
  36. package/src/components/navigation/SidebarAccountItem.svelte +492 -0
  37. package/src/components/navigation/SidebarGroup.svelte +230 -0
  38. package/src/components/navigation/SidebarGroupSwitcher.svelte +262 -0
  39. package/src/components/navigation/SidebarItem.svelte +210 -0
  40. package/src/components/navigation/SidebarNavigationItem.svelte +470 -0
  41. package/src/components/navigation/SidebarPopover.svelte +145 -0
  42. package/src/components/navigation/SidebarSearch.svelte +236 -0
  43. package/src/components/navigation/SidebarSection.svelte +158 -0
  44. package/src/components/navigation/SidebarToggle.svelte +86 -0
  45. package/src/components/navigation/Tabs.svelte +239 -0
  46. package/src/components/navigation/WorkspaceMenu.svelte +416 -0
  47. package/src/components/navigation/blocks/NavigationAccountGroup.svelte +396 -0
  48. package/src/components/navigation/blocks/NavigationCustomBlock.svelte +74 -0
  49. package/src/components/navigation/blocks/NavigationGroupSwitcher.svelte +277 -0
  50. package/src/components/navigation/blocks/NavigationSearch.svelte +300 -0
  51. package/src/components/navigation/blocks/NavigationSection.svelte +230 -0
  52. package/src/components/navigation/index.js +22 -0
  53. package/src/components/overlays/ConfirmDialog.svelte +272 -0
  54. package/src/components/overlays/Dropdown.svelte +153 -0
  55. package/src/components/overlays/DropdownDivider.svelte +23 -0
  56. package/src/components/overlays/DropdownItem.svelte +97 -0
  57. package/src/components/overlays/Modal.svelte +232 -0
  58. package/src/components/overlays/Popover.svelte +206 -0
  59. package/src/components/primitives/Avatar.svelte +132 -0
  60. package/src/components/primitives/Badge.svelte +118 -0
  61. package/src/components/primitives/Button.svelte +214 -0
  62. package/src/components/primitives/Card.svelte +104 -0
  63. package/src/components/primitives/Divider.svelte +105 -0
  64. package/src/components/primitives/LazyImage.svelte +104 -0
  65. package/src/components/primitives/Link.svelte +122 -0
  66. package/src/components/primitives/Stat.svelte +197 -0
  67. package/src/components/primitives/StatusBadge.svelte +122 -0
  68. package/src/index.js +183 -0
  69. package/src/tokens/colors.css +157 -0
  70. package/src/tokens/effects.css +128 -0
  71. package/src/tokens/index.css +81 -0
  72. package/src/tokens/spacing.css +49 -0
  73. package/src/tokens/typography.css +79 -0
  74. package/src/utils/cn.svelte.js +175 -0
  75. package/src/utils/highlighter.js +124 -0
  76. package/src/utils/index.js +22 -0
  77. package/src/utils/navigation.svelte.js +423 -0
  78. package/src/utils/reactive.svelte.js +328 -0
  79. package/src/utils/sidebar.svelte.js +211 -0
  80. package/jera.js +0 -135
  81. package/www/components/jera/Input/Input.svelte +0 -63
  82. package/www/components/jera/Input/index.js +0 -1
@@ -0,0 +1,97 @@
1
+ <!--
2
+ @component DropdownItem
3
+
4
+ An item within a Dropdown menu.
5
+
6
+ @example
7
+ <DropdownItem onclick={handleAction}>Action</DropdownItem>
8
+
9
+ @example
10
+ <DropdownItem onclick={handleDelete} variant="danger">Delete</DropdownItem>
11
+ -->
12
+ <script>
13
+ import { cn } from '../../utils/cn.svelte.js';
14
+
15
+ let {
16
+ variant = 'default',
17
+ disabled = false,
18
+ icon,
19
+ children,
20
+ onclick,
21
+ class: className = '',
22
+ ...rest
23
+ } = $props();
24
+ </script>
25
+
26
+ <button
27
+ type="button"
28
+ class={cn(
29
+ 'dropdown-item',
30
+ `dropdown-item-${variant}`,
31
+ disabled && 'dropdown-item-disabled',
32
+ className
33
+ )}
34
+ {disabled}
35
+ {onclick}
36
+ {...rest}
37
+ >
38
+ {#if icon}
39
+ <span class="dropdown-item-icon">
40
+ {@render icon()}
41
+ </span>
42
+ {/if}
43
+ <span class="dropdown-item-label">
44
+ {@render children?.()}
45
+ </span>
46
+ </button>
47
+
48
+ <style>
49
+ .dropdown-item {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: var(--space-2);
53
+ width: 100%;
54
+ padding: var(--space-2) var(--space-3);
55
+ font-size: var(--text-sm);
56
+ text-align: left;
57
+ color: var(--color-base05);
58
+ background: transparent;
59
+ border: none;
60
+ border-radius: var(--radius-md);
61
+ cursor: pointer;
62
+ transition: var(--transition-colors);
63
+ }
64
+
65
+ .dropdown-item:hover:not(:disabled) {
66
+ background: var(--color-base02);
67
+ }
68
+
69
+ .dropdown-item:focus-visible {
70
+ outline: none;
71
+ background: var(--color-base02);
72
+ }
73
+
74
+ .dropdown-item-disabled {
75
+ opacity: 0.5;
76
+ cursor: not-allowed;
77
+ }
78
+
79
+ .dropdown-item-danger {
80
+ color: var(--color-base08);
81
+ }
82
+
83
+ .dropdown-item-danger:hover:not(:disabled) {
84
+ background: color-mix(in srgb, var(--color-base08) 10%, transparent);
85
+ }
86
+
87
+ .dropdown-item-icon {
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ flex-shrink: 0;
92
+ }
93
+
94
+ .dropdown-item-label {
95
+ flex: 1;
96
+ }
97
+ </style>
@@ -0,0 +1,232 @@
1
+ <!--
2
+ @component Modal
3
+
4
+ A flexible modal dialog component with backdrop, focus trap, and escape key support.
5
+
6
+ @example
7
+ <Modal bind:open={showModal} title="Confirm Action">
8
+ <p>Are you sure you want to proceed?</p>
9
+ {#snippet footer()}
10
+ <Button variant="ghost" onclick={() => showModal = false}>Cancel</Button>
11
+ <Button variant="primary" onclick={handleConfirm}>Confirm</Button>
12
+ {/snippet}
13
+ </Modal>
14
+ -->
15
+ <script>
16
+ import { focusTrap, escapeKey, portal } from '../../actions/index.js';
17
+ import { cv } from '../../utils/cn.svelte.js';
18
+
19
+ let {
20
+ open = $bindable(false),
21
+ title = '',
22
+ size = 'md',
23
+ variant = 'default',
24
+ closeOnBackdrop = true,
25
+ closeOnEscape = true,
26
+ showClose = true,
27
+ children,
28
+ footer,
29
+ icon,
30
+ onclose = () => {},
31
+ class: className = ''
32
+ } = $props();
33
+
34
+ // Variant styles for the icon container
35
+ const iconVariants = {
36
+ default: { bg: 'var(--color-base02)', color: 'var(--color-base05)' },
37
+ danger: { bg: 'color-mix(in srgb, var(--color-base08) 10%, transparent)', color: 'var(--color-base08)' },
38
+ warning: { bg: 'color-mix(in srgb, var(--color-base0A) 10%, transparent)', color: 'var(--color-base0A)' },
39
+ success: { bg: 'color-mix(in srgb, var(--color-base0B) 10%, transparent)', color: 'var(--color-base0B)' },
40
+ info: { bg: 'color-mix(in srgb, var(--color-base0D) 10%, transparent)', color: 'var(--color-base0D)' }
41
+ };
42
+
43
+ const iconStyle = $derived(iconVariants[variant] || iconVariants.default);
44
+
45
+ function close() {
46
+ open = false;
47
+ onclose();
48
+ }
49
+
50
+ function handleBackdropClick(e) {
51
+ if (closeOnBackdrop && e.target === e.currentTarget) {
52
+ close();
53
+ }
54
+ }
55
+
56
+ function handleEscape() {
57
+ if (closeOnEscape) {
58
+ close();
59
+ }
60
+ }
61
+ </script>
62
+
63
+ {#if open}
64
+ <div
65
+ class="modal-backdrop"
66
+ onclick={handleBackdropClick}
67
+ role="dialog"
68
+ aria-modal="true"
69
+ aria-labelledby={title ? 'modal-title' : undefined}
70
+ use:portal={'body'}
71
+ use:focusTrap={{ enabled: open }}
72
+ use:escapeKey={handleEscape}
73
+ >
74
+ <div class="modal modal-{size} {className}">
75
+ <!-- Close button -->
76
+ {#if showClose}
77
+ <button
78
+ class="modal-close"
79
+ onclick={close}
80
+ aria-label="Close modal"
81
+ type="button"
82
+ >
83
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
84
+ <line x1="18" y1="6" x2="6" y2="18"></line>
85
+ <line x1="6" y1="6" x2="18" y2="18"></line>
86
+ </svg>
87
+ </button>
88
+ {/if}
89
+
90
+ <!-- Content -->
91
+ <div class="modal-content">
92
+ {#if icon || title}
93
+ <div class="modal-header">
94
+ {#if icon}
95
+ <div class="modal-icon" style="background: {iconStyle.bg}; color: {iconStyle.color};">
96
+ {@render icon()}
97
+ </div>
98
+ {/if}
99
+
100
+ {#if title || children}
101
+ <div class="modal-text">
102
+ {#if title}
103
+ <h3 id="modal-title" class="modal-title">{title}</h3>
104
+ {/if}
105
+ {#if children}
106
+ <div class="modal-body">
107
+ {@render children()}
108
+ </div>
109
+ {/if}
110
+ </div>
111
+ {/if}
112
+ </div>
113
+ {:else if children}
114
+ <div class="modal-body">
115
+ {@render children()}
116
+ </div>
117
+ {/if}
118
+
119
+ {#if footer}
120
+ <div class="modal-footer">
121
+ {@render footer()}
122
+ </div>
123
+ {/if}
124
+ </div>
125
+ </div>
126
+ </div>
127
+ {/if}
128
+
129
+ <style>
130
+ .modal-backdrop {
131
+ position: fixed;
132
+ inset: 0;
133
+ z-index: 100;
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+ background: color-mix(in srgb, var(--color-base00) 80%, transparent);
138
+ backdrop-filter: blur(4px);
139
+ }
140
+
141
+ .modal {
142
+ position: relative;
143
+ background: var(--color-base01);
144
+ border-radius: 0.75rem;
145
+ box-shadow: var(--shadow-2xl);
146
+ border: 1px solid var(--color-base03);
147
+ margin: 1rem;
148
+ animation: modal-enter 0.2s ease-out;
149
+ }
150
+
151
+ /* Size variants */
152
+ .modal-sm { width: 100%; max-width: 20rem; }
153
+ .modal-md { width: 100%; max-width: 28rem; }
154
+ .modal-lg { width: 100%; max-width: 36rem; }
155
+ .modal-xl { width: 100%; max-width: 48rem; }
156
+ .modal-full { width: 100%; max-width: calc(100vw - 2rem); max-height: calc(100vh - 2rem); }
157
+
158
+ .modal-close {
159
+ position: absolute;
160
+ top: 1rem;
161
+ right: 1rem;
162
+ padding: 0.25rem;
163
+ background: transparent;
164
+ border: none;
165
+ border-radius: 0.5rem;
166
+ color: var(--color-base05);
167
+ cursor: pointer;
168
+ transition: background 0.15s, color 0.15s;
169
+ }
170
+
171
+ .modal-close:hover {
172
+ background: var(--color-base02);
173
+ color: var(--color-base07);
174
+ }
175
+
176
+ .modal-content {
177
+ padding: 1.5rem;
178
+ }
179
+
180
+ .modal-header {
181
+ display: flex;
182
+ align-items: flex-start;
183
+ gap: 1rem;
184
+ }
185
+
186
+ .modal-icon {
187
+ flex-shrink: 0;
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: center;
191
+ width: 2.5rem;
192
+ height: 2.5rem;
193
+ border-radius: 0.5rem;
194
+ }
195
+
196
+ .modal-text {
197
+ flex: 1;
198
+ padding-top: 0.25rem;
199
+ }
200
+
201
+ .modal-title {
202
+ margin: 0 0 0.5rem 0;
203
+ font-size: 1.125rem;
204
+ font-weight: 600;
205
+ color: var(--color-base07);
206
+ line-height: 1.4;
207
+ }
208
+
209
+ .modal-body {
210
+ font-size: 0.875rem;
211
+ color: var(--color-base05);
212
+ line-height: 1.5;
213
+ }
214
+
215
+ .modal-footer {
216
+ display: flex;
217
+ gap: 0.75rem;
218
+ margin-top: 1.5rem;
219
+ justify-content: flex-end;
220
+ }
221
+
222
+ @keyframes modal-enter {
223
+ from {
224
+ opacity: 0;
225
+ transform: scale(0.95) translateY(10px);
226
+ }
227
+ to {
228
+ opacity: 1;
229
+ transform: scale(1) translateY(0);
230
+ }
231
+ }
232
+ </style>
@@ -0,0 +1,206 @@
1
+ <!--
2
+ @component Popover
3
+
4
+ A tooltip/popover component with smart positioning and hover interaction.
5
+
6
+ @example
7
+ <Popover content="This is helpful information" position="top">
8
+ <Button>Hover me</Button>
9
+ </Popover>
10
+
11
+ @example With custom content
12
+ <Popover position="bottom">
13
+ {#snippet content()}
14
+ <div>Custom <strong>HTML</strong> content</div>
15
+ {/snippet}
16
+ <Button>Hover for details</Button>
17
+ </Popover>
18
+ -->
19
+ <script>
20
+ import { fly } from 'svelte/transition';
21
+
22
+ let {
23
+ children,
24
+ content = '',
25
+ position = 'top',
26
+ delay = { show: 100, hide: 100 },
27
+ offset = 8,
28
+ class: className = ''
29
+ } = $props();
30
+
31
+ let visible = $state(false);
32
+ let timeoutId = $state(null);
33
+ let popoverEl = $state(null);
34
+ let triggerEl = $state(null);
35
+ let isHoveringPopover = $state(false);
36
+ let windowWidth = $state(0);
37
+ let windowHeight = $state(0);
38
+
39
+ // Animation configs based on position
40
+ const animations = {
41
+ top: { in: { y: 8, duration: 200 }, out: { y: -8, duration: 150 } },
42
+ bottom: { in: { y: -8, duration: 200 }, out: { y: 8, duration: 150 } },
43
+ left: { in: { x: 8, duration: 200 }, out: { x: -8, duration: 150 } },
44
+ right: { in: { x: -8, duration: 200 }, out: { x: 8, duration: 150 } }
45
+ };
46
+
47
+ const anim = $derived(animations[position] || animations.top);
48
+
49
+ function handleMouseEnter(event) {
50
+ clearTimeout(timeoutId);
51
+ triggerEl = event.currentTarget;
52
+ timeoutId = setTimeout(() => {
53
+ visible = true;
54
+ requestAnimationFrame(() => {
55
+ requestAnimationFrame(() => {
56
+ positionPopover();
57
+ });
58
+ });
59
+ }, delay.show);
60
+ }
61
+
62
+ function handleMouseLeave() {
63
+ clearTimeout(timeoutId);
64
+ timeoutId = setTimeout(() => {
65
+ if (!isHoveringPopover) {
66
+ visible = false;
67
+ }
68
+ }, delay.hide);
69
+ }
70
+
71
+ function handlePopoverEnter() {
72
+ clearTimeout(timeoutId);
73
+ isHoveringPopover = true;
74
+ }
75
+
76
+ function handlePopoverLeave() {
77
+ isHoveringPopover = false;
78
+ handleMouseLeave();
79
+ }
80
+
81
+ function positionPopover() {
82
+ if (!popoverEl || !triggerEl) return;
83
+
84
+ const triggerRect = triggerEl.getBoundingClientRect();
85
+ const popoverWidth = popoverEl.offsetWidth;
86
+ const popoverHeight = popoverEl.offsetHeight;
87
+
88
+ const triggerCenterX = triggerRect.left + triggerRect.width / 2;
89
+ const triggerCenterY = triggerRect.top + triggerRect.height / 2;
90
+
91
+ let left = 0;
92
+ let top = 0;
93
+
94
+ switch (position) {
95
+ case 'top':
96
+ left = triggerCenterX - popoverWidth / 2;
97
+ top = triggerRect.top - popoverHeight - offset;
98
+ break;
99
+ case 'bottom':
100
+ left = triggerCenterX - popoverWidth / 2;
101
+ top = triggerRect.bottom + offset;
102
+ break;
103
+ case 'left':
104
+ left = triggerRect.left - popoverWidth - offset;
105
+ top = triggerCenterY - popoverHeight / 2;
106
+ break;
107
+ case 'right':
108
+ left = triggerRect.right + offset;
109
+ top = triggerCenterY - popoverHeight / 2;
110
+ break;
111
+ }
112
+
113
+ // Viewport boundary detection
114
+ const margin = 8;
115
+
116
+ if (left < margin) {
117
+ left = margin;
118
+ } else if (left + popoverWidth > windowWidth - margin) {
119
+ left = windowWidth - popoverWidth - margin;
120
+ }
121
+
122
+ if (top < margin) {
123
+ if (position === 'top') {
124
+ top = triggerRect.bottom + offset;
125
+ } else {
126
+ top = margin;
127
+ }
128
+ } else if (top + popoverHeight > windowHeight - margin) {
129
+ if (position === 'bottom') {
130
+ top = triggerRect.top - popoverHeight - offset;
131
+ } else {
132
+ top = windowHeight - popoverHeight - margin;
133
+ }
134
+ }
135
+
136
+ popoverEl.style.left = `${left}px`;
137
+ popoverEl.style.top = `${top}px`;
138
+ }
139
+
140
+ function handleScroll() {
141
+ if (visible && triggerEl && popoverEl) {
142
+ requestAnimationFrame(positionPopover);
143
+ }
144
+ }
145
+ </script>
146
+
147
+ <svelte:window
148
+ bind:innerWidth={windowWidth}
149
+ bind:innerHeight={windowHeight}
150
+ onscroll={visible ? handleScroll : undefined}
151
+ onresize={visible ? handleScroll : undefined}
152
+ />
153
+
154
+ <div
155
+ class="popover-wrapper {className}"
156
+ onmouseenter={handleMouseEnter}
157
+ onmouseleave={handleMouseLeave}
158
+ >
159
+ {@render children?.()}
160
+
161
+ {#if visible}
162
+ <div
163
+ class="popover"
164
+ role="tooltip"
165
+ in:fly={anim.in}
166
+ out:fly={anim.out}
167
+ bind:this={popoverEl}
168
+ onmouseenter={handlePopoverEnter}
169
+ onmouseleave={handlePopoverLeave}
170
+ >
171
+ {#if typeof content === 'string'}
172
+ {content}
173
+ {:else if typeof content === 'function'}
174
+ {@render content()}
175
+ {:else}
176
+ {content}
177
+ {/if}
178
+ </div>
179
+ {/if}
180
+ </div>
181
+
182
+ <style>
183
+ .popover-wrapper {
184
+ position: relative;
185
+ display: inline-block;
186
+ }
187
+
188
+ .popover {
189
+ position: fixed;
190
+ z-index: 9999;
191
+ min-width: 8rem;
192
+ max-width: 18rem;
193
+ width: max-content;
194
+ padding: 0.5rem 0.75rem;
195
+ background: var(--color-base01);
196
+ color: var(--color-base07);
197
+ font-size: 0.875rem;
198
+ line-height: 1.5;
199
+ border-radius: 0.5rem;
200
+ box-shadow: var(--shadow-lg);
201
+ border: 1px solid var(--color-base03);
202
+ pointer-events: auto;
203
+ word-wrap: break-word;
204
+ hyphens: auto;
205
+ }
206
+ </style>
@@ -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-base03)';
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-base08)', // red
46
+ 'var(--orange)', // orange
47
+ 'var(--color-base0B)', // green
48
+ 'var(--color-base0A)', // yellow
49
+ 'var(--color-base0D)', // blue
50
+ 'var(--color-base0D)', // magenta
51
+ 'var(--peach)', // peach
52
+ 'var(--color-base0E)' // 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-base00);
126
+ }
127
+
128
+ .avatar-status-online { background: var(--color-base0B); }
129
+ .avatar-status-offline { background: var(--color-base04); }
130
+ .avatar-status-busy { background: var(--color-base08); }
131
+ .avatar-status-away { background: var(--color-base0A); }
132
+ </style>