@hyvnt/hyvui 0.2.0 → 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.
Files changed (86) hide show
  1. package/README.md +264 -253
  2. package/dist/components/ambient/CornerBrackets.svelte +83 -87
  3. package/dist/components/ambient/DataStream.svelte +111 -94
  4. package/dist/components/ambient/GlyphMark.svelte +69 -69
  5. package/dist/components/ambient/GridOverlay.svelte +26 -28
  6. package/dist/components/ambient/ParallaxLayer.svelte +37 -41
  7. package/dist/components/ambient/ScanBand.svelte +95 -91
  8. package/dist/components/ambient/SignalRing.svelte +100 -100
  9. package/dist/components/ambient/ThreadLine.svelte +71 -78
  10. package/dist/components/ambient/Vignette.svelte +24 -26
  11. package/dist/components/depth/DepthLayer.svelte +22 -27
  12. package/dist/components/depth/DepthStage.svelte +63 -62
  13. package/dist/components/depth/FloatCard.svelte +113 -104
  14. package/dist/components/depth/HorizonGrid.svelte +216 -160
  15. package/dist/components/depth/Plinth.svelte +52 -57
  16. package/dist/components/display/Avatar.svelte +64 -69
  17. package/dist/components/display/Badge.svelte +59 -63
  18. package/dist/components/display/Blockquote.svelte +31 -34
  19. package/dist/components/display/CodeBlock.svelte +71 -76
  20. package/dist/components/display/MetricCard.svelte +77 -83
  21. package/dist/components/display/Table.svelte +99 -104
  22. package/dist/components/feedback/Alert.svelte +71 -76
  23. package/dist/components/feedback/EmptyState.svelte +68 -68
  24. package/dist/components/feedback/ErrorState.svelte +73 -73
  25. package/dist/components/feedback/Skeleton.svelte +52 -52
  26. package/dist/components/feedback/StatusDot.svelte +49 -54
  27. package/dist/components/feedback/StatusLine.svelte +122 -122
  28. package/dist/components/feedback/Toast.svelte +130 -136
  29. package/dist/components/inputs/Button.svelte +240 -237
  30. package/dist/components/inputs/Checkbox.svelte +104 -105
  31. package/dist/components/inputs/FileUpload.svelte +165 -163
  32. package/dist/components/inputs/Input.svelte +145 -147
  33. package/dist/components/inputs/Select.svelte +156 -150
  34. package/dist/components/inputs/Textarea.svelte +153 -154
  35. package/dist/components/inputs/Toggle.svelte +120 -120
  36. package/dist/components/layout/Card.svelte +70 -76
  37. package/dist/components/layout/Drawer.svelte +133 -109
  38. package/dist/components/layout/Grid.svelte +118 -43
  39. package/dist/components/layout/Grid.svelte.d.ts +8 -2
  40. package/dist/components/layout/Modal.svelte +176 -159
  41. package/dist/components/layout/Panel.svelte +49 -54
  42. package/dist/components/layout/Popover.svelte +178 -67
  43. package/dist/components/layout/Popover.svelte.d.ts +10 -1
  44. package/dist/components/layout/Stack.svelte +53 -53
  45. package/dist/components/navigation/Breadcrumb.svelte +70 -73
  46. package/dist/components/navigation/DropdownMenu.svelte +167 -124
  47. package/dist/components/navigation/DropdownMenu.svelte.d.ts +12 -2
  48. package/dist/components/navigation/SidebarNav.svelte +86 -90
  49. package/dist/components/navigation/Tabs.svelte +81 -86
  50. package/dist/components/navigation/Topbar.svelte +85 -85
  51. package/dist/components/patterns/ActionBar.svelte +71 -76
  52. package/dist/components/patterns/ConfirmDialog.svelte +63 -64
  53. package/dist/components/patterns/PageHeader.svelte +109 -114
  54. package/dist/components/patterns/SearchBar.svelte +54 -59
  55. package/dist/components/patterns/TerminalBoot.svelte +104 -104
  56. package/dist/components/primitives/Divider.svelte +26 -29
  57. package/dist/components/primitives/Icon.svelte +44 -49
  58. package/dist/components/primitives/Label.svelte +39 -44
  59. package/dist/components/primitives/Surface.svelte +89 -87
  60. package/dist/components/primitives/Text.svelte +98 -98
  61. package/dist/components/scenes/ArchiveScene.svelte +92 -95
  62. package/dist/components/scenes/ArchiveScene.svelte.d.ts +7 -1
  63. package/dist/components/scenes/LogScene.svelte +72 -77
  64. package/dist/components/scenes/NarrativeScene.svelte +91 -92
  65. package/dist/components/scenes/ReadoutScene.svelte +120 -107
  66. package/dist/components/scenes/ReadoutScene.svelte.d.ts +3 -1
  67. package/dist/components/scenes/StageScene.svelte +97 -104
  68. package/dist/examples/FieldReport.svelte +226 -223
  69. package/dist/examples/ObservationDeck.svelte +333 -317
  70. package/dist/examples/SignalLost.svelte +191 -191
  71. package/dist/styles.css +113 -0
  72. package/dist/system/actions/echo.js +9 -9
  73. package/dist/system/actions/resolve.js +9 -9
  74. package/dist/system/actions/reveal.js +1 -1
  75. package/dist/system/actions/surface.js +13 -1
  76. package/dist/system/depth/depth.css +49 -49
  77. package/dist/system/depth/depth.js +1 -1
  78. package/dist/system/expressions.css +80 -80
  79. package/dist/system/override-template.css +72 -72
  80. package/dist/system/register.css +74 -74
  81. package/dist/system/scroll-lock.d.ts +6 -0
  82. package/dist/system/scroll-lock.js +23 -0
  83. package/dist/tokens/tokens.css +100 -86
  84. package/dist/tokens/tokens.js +4 -4
  85. package/dist/utils/motion.js +1 -1
  86. package/package.json +67 -60
@@ -1,160 +1,216 @@
1
- <script lang="ts">
2
- import { cn } from '../../utils/cn.js';
3
- import { onMount } from 'svelte';
4
-
5
- interface Props {
6
- /** Number of horizontal lines receding toward the vanishing point. */
7
- rows?: number;
8
- /** Number of vertical convergence lines. */
9
- cols?: number;
10
- /** Normalized Y position of vanishing point (0-1). */
11
- vanishY?: number;
12
- /** If true, lines slowly drift toward the viewer. */
13
- animated?: boolean;
14
- /** Additional CSS classes. */
15
- class?: string;
16
- }
17
-
18
- let {
19
- rows = 16,
20
- cols = 12,
21
- vanishY = 0.38,
22
- animated = false,
23
- class: className = '',
24
- }: Props = $props();
25
-
26
- let canvasEl: HTMLCanvasElement | undefined = $state();
27
- let animFrame = 0;
28
- let offset = $state(0);
29
-
30
- const prefersReduced =
31
- typeof window !== 'undefined'
32
- ? window.matchMedia('(prefers-reduced-motion: reduce)').matches
33
- : false;
34
-
35
- function draw(canvas: HTMLCanvasElement, t: number) {
36
- const ctx = canvas.getContext('2d');
37
- if (!ctx) return;
38
-
39
- const w = canvas.width;
40
- const h = canvas.height;
41
- const vpX = w / 2;
42
- const vpY = h * vanishY;
43
-
44
- ctx.clearRect(0, 0, w, h);
45
-
46
- // horizontal lines receding toward vanishing point
47
- for (let i = 0; i < rows; i++) {
48
- const progress = (i + t) / rows;
49
- if (progress > 1) continue;
50
- const y = vpY + (h - vpY) * Math.pow(progress, 1.6);
51
- const nearness = Math.pow(progress, 0.8);
52
-
53
- // color: gold near, teal far
54
- const r = Math.round(199 * (1 - nearness) + 121 * nearness);
55
- const g = Math.round(156 * (1 - nearness) + 166 * nearness);
56
- const b = Math.round(87 * (1 - nearness) + 163 * nearness);
57
- const alpha = 0.18 * (1 - nearness) + 0.06 * nearness;
58
-
59
- // fade at vanishing point
60
- const fadeNear = Math.min(1, progress * 4);
61
- const finalAlpha = alpha * fadeNear;
62
-
63
- ctx.beginPath();
64
- ctx.moveTo(0, y);
65
- ctx.lineTo(w, y);
66
- ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${finalAlpha})`;
67
- ctx.lineWidth = 1;
68
- ctx.stroke();
69
- }
70
-
71
- // vertical convergence lines
72
- for (let i = 0; i < cols; i++) {
73
- const xBottom = (i / (cols - 1)) * w;
74
- const progress = Math.abs(i / (cols - 1) - 0.5) * 2; // 0 at center, 1 at edges
75
-
76
- // fade at edges
77
- const edgeFade = 1 - Math.pow(progress, 2) * 0.7;
78
- const alpha = 0.1 * edgeFade;
79
-
80
- ctx.beginPath();
81
- ctx.moveTo(vpX, vpY);
82
- ctx.lineTo(xBottom, h);
83
- ctx.strokeStyle = `rgba(199, 156, 87, ${alpha})`;
84
- ctx.lineWidth = 1;
85
- ctx.stroke();
86
- }
87
- }
88
-
89
- function animate() {
90
- if (!canvasEl) return;
91
- if (animated && !prefersReduced) {
92
- offset = (offset + 0.003) % 1;
93
- }
94
- draw(canvasEl, offset);
95
- if (animated && !prefersReduced) {
96
- animFrame = requestAnimationFrame(animate);
97
- }
98
- }
99
-
100
- onMount(() => {
101
- if (!canvasEl) return;
102
-
103
- const container = canvasEl.parentElement;
104
- if (!container) return;
105
-
106
- const ro = new ResizeObserver((entries) => {
107
- for (const entry of entries) {
108
- const { width, height } = entry.contentRect;
109
- const dpr = window.devicePixelRatio || 1;
110
- canvasEl!.width = width * dpr;
111
- canvasEl!.height = height * dpr;
112
- canvasEl!.style.width = `${width}px`;
113
- canvasEl!.style.height = `${height}px`;
114
- const ctx = canvasEl!.getContext('2d');
115
- if (ctx) ctx.scale(dpr, dpr);
116
- // set logical dimensions for draw
117
- canvasEl!.dataset.logicalW = String(width);
118
- canvasEl!.dataset.logicalH = String(height);
119
- draw(canvasEl!, offset);
120
- }
121
- });
122
-
123
- ro.observe(container);
124
-
125
- if (animated && !prefersReduced) {
126
- animFrame = requestAnimationFrame(animate);
127
- }
128
-
129
- return () => {
130
- ro.disconnect();
131
- if (animFrame) cancelAnimationFrame(animFrame);
132
- };
133
- });
134
-
135
- // redraw on offset changes when animated
136
- $effect(() => {
137
- if (canvasEl && animated && !prefersReduced) {
138
- draw(canvasEl, offset);
139
- }
140
- });
141
- </script>
142
-
143
- <div class={cn('hyvui-horizon-grid', className)} aria-hidden="true">
144
- <canvas bind:this={canvasEl}></canvas>
145
- </div>
146
-
147
- <style>
148
- .hyvui-horizon-grid {
149
- position: absolute;
150
- inset: 0;
151
- pointer-events: none;
152
- overflow: hidden;
153
- }
154
-
155
- .hyvui-horizon-grid canvas {
156
- display: block;
157
- width: 100%;
158
- height: 100%;
159
- }
160
- </style>
1
+ <script lang="ts">
2
+ import { cn } from '../../utils/cn.js';
3
+ import { onMount } from 'svelte';
4
+
5
+ interface Props {
6
+ /** Number of horizontal lines receding toward the vanishing point. */
7
+ rows?: number;
8
+ /** Number of vertical convergence lines. */
9
+ cols?: number;
10
+ /** Normalized Y position of vanishing point (0-1). */
11
+ vanishY?: number;
12
+ /** If true, lines slowly drift toward the viewer. */
13
+ animated?: boolean;
14
+ /** Additional CSS classes. */
15
+ class?: string;
16
+ }
17
+
18
+ let {
19
+ rows = 16,
20
+ cols = 12,
21
+ vanishY = 0.38,
22
+ animated = false,
23
+ class: className = ''
24
+ }: Props = $props();
25
+
26
+ let rootEl: HTMLDivElement | undefined = $state();
27
+ let canvasEl: HTMLCanvasElement | undefined = $state();
28
+ let animFrame = 0;
29
+ let isVisible = $state(true);
30
+
31
+ let ctx: CanvasRenderingContext2D | null = null;
32
+ let logicalW = 0;
33
+ let logicalH = 0;
34
+ let offset = 0;
35
+ let lastT = 0;
36
+
37
+ const prefersReduced =
38
+ typeof window !== 'undefined'
39
+ ? window.matchMedia('(prefers-reduced-motion: reduce)').matches
40
+ : false;
41
+
42
+ function draw(t: number) {
43
+ if (!ctx) return;
44
+ if (!logicalW || !logicalH) return;
45
+
46
+ const w = logicalW;
47
+ const h = logicalH;
48
+ const vpX = w / 2;
49
+ const vpY = h * vanishY;
50
+
51
+ ctx.clearRect(0, 0, w, h);
52
+
53
+ // horizontal lines receding toward vanishing point
54
+ for (let i = 0; i < rows; i++) {
55
+ const progress = (i + t) / rows;
56
+ if (progress > 1) continue;
57
+ const y = vpY + (h - vpY) * Math.pow(progress, 1.6);
58
+ const nearness = Math.pow(progress, 0.8);
59
+
60
+ // color: gold near, teal far
61
+ const r = Math.round(199 * (1 - nearness) + 121 * nearness);
62
+ const g = Math.round(156 * (1 - nearness) + 166 * nearness);
63
+ const b = Math.round(87 * (1 - nearness) + 163 * nearness);
64
+ const alpha = 0.18 * (1 - nearness) + 0.06 * nearness;
65
+
66
+ // fade at vanishing point
67
+ const fadeNear = Math.min(1, progress * 4);
68
+ const finalAlpha = alpha * fadeNear;
69
+
70
+ ctx.beginPath();
71
+ ctx.moveTo(0, y);
72
+ ctx.lineTo(w, y);
73
+ ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${finalAlpha})`;
74
+ ctx.lineWidth = 1;
75
+ ctx.stroke();
76
+ }
77
+
78
+ // vertical convergence lines
79
+ for (let i = 0; i < cols; i++) {
80
+ const xBottom = (i / (cols - 1)) * w;
81
+ const progress = Math.abs(i / (cols - 1) - 0.5) * 2; // 0 at center, 1 at edges
82
+
83
+ // fade at edges
84
+ const edgeFade = 1 - Math.pow(progress, 2) * 0.7;
85
+ const alpha = 0.1 * edgeFade;
86
+
87
+ ctx.beginPath();
88
+ ctx.moveTo(vpX, vpY);
89
+ ctx.lineTo(xBottom, h);
90
+ ctx.strokeStyle = `rgba(199, 156, 87, ${alpha})`;
91
+ ctx.lineWidth = 1;
92
+ ctx.stroke();
93
+ }
94
+ }
95
+
96
+ function stop() {
97
+ if (animFrame) cancelAnimationFrame(animFrame);
98
+ animFrame = 0;
99
+ }
100
+
101
+ function tick(t: number) {
102
+ if (!animated || prefersReduced || document.hidden || !isVisible) {
103
+ stop();
104
+ return;
105
+ }
106
+
107
+ const dt = lastT ? t - lastT : 16;
108
+ lastT = t;
109
+
110
+ // Drift speed tuned to roughly match the previous "0.003 per frame" feel.
111
+ offset = (offset + dt * 0.00018) % 1;
112
+ draw(offset);
113
+ animFrame = requestAnimationFrame(tick);
114
+ }
115
+
116
+ function start() {
117
+ if (animFrame) return;
118
+ lastT = 0;
119
+ animFrame = requestAnimationFrame(tick);
120
+ }
121
+
122
+ function syncCanvasSize(width: number, height: number) {
123
+ if (!canvasEl) return;
124
+ if (!ctx) ctx = canvasEl.getContext('2d');
125
+ if (!ctx) return;
126
+
127
+ const dpr = window.devicePixelRatio || 1;
128
+ logicalW = width;
129
+ logicalH = height;
130
+
131
+ canvasEl.width = Math.max(1, Math.floor(width * dpr));
132
+ canvasEl.height = Math.max(1, Math.floor(height * dpr));
133
+ canvasEl.style.width = `${width}px`;
134
+ canvasEl.style.height = `${height}px`;
135
+
136
+ // Reset transform before applying DPR scaling (avoids cumulative scaling).
137
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
138
+ draw(offset);
139
+ }
140
+
141
+ onMount(() => {
142
+ if (!rootEl || !canvasEl) return;
143
+
144
+ ctx = canvasEl.getContext('2d');
145
+
146
+ const ro = new ResizeObserver((entries) => {
147
+ const entry = entries[0];
148
+ if (!entry) return;
149
+ const { width, height } = entry.contentRect;
150
+ syncCanvasSize(width, height);
151
+ });
152
+
153
+ ro.observe(rootEl);
154
+
155
+ const io = new IntersectionObserver(
156
+ (entries) => {
157
+ const entry = entries[0];
158
+ if (!entry) return;
159
+ isVisible = entry.isIntersecting;
160
+ },
161
+ { root: null, threshold: 0 }
162
+ );
163
+
164
+ io.observe(rootEl);
165
+
166
+ function onVisibility() {
167
+ if (document.hidden) stop();
168
+ else if (animated && !prefersReduced && isVisible) start();
169
+ }
170
+
171
+ document.addEventListener('visibilitychange', onVisibility);
172
+
173
+ return () => {
174
+ document.removeEventListener('visibilitychange', onVisibility);
175
+ io.disconnect();
176
+ ro.disconnect();
177
+ stop();
178
+ };
179
+ });
180
+
181
+ $effect(() => {
182
+ if (!canvasEl) return;
183
+ draw(offset);
184
+
185
+ if (
186
+ animated &&
187
+ !prefersReduced &&
188
+ isVisible &&
189
+ typeof document !== 'undefined' &&
190
+ !document.hidden
191
+ ) {
192
+ start();
193
+ } else {
194
+ stop();
195
+ }
196
+ });
197
+ </script>
198
+
199
+ <div bind:this={rootEl} class={cn('hyvui-horizon-grid', className)} aria-hidden="true">
200
+ <canvas bind:this={canvasEl}></canvas>
201
+ </div>
202
+
203
+ <style>
204
+ .hyvui-horizon-grid {
205
+ position: absolute;
206
+ inset: 0;
207
+ pointer-events: none;
208
+ overflow: hidden;
209
+ }
210
+
211
+ .hyvui-horizon-grid canvas {
212
+ display: block;
213
+ width: 100%;
214
+ height: 100%;
215
+ }
216
+ </style>
@@ -1,57 +1,52 @@
1
- <script lang="ts">
2
- import { cn } from '../../utils/cn.js';
3
-
4
- interface Props {
5
- /** Width of the plinth surface. */
6
- width?: string;
7
- /** How far the plinth recedes (visual depth). */
8
- depth?: string;
9
- /** Surface tint color. */
10
- color?: string;
11
- /** Additional CSS classes. */
12
- class?: string;
13
- }
14
-
15
- let {
16
- width = '100%',
17
- depth = '40px',
18
- color = 'rgba(199, 156, 87, 0.03)',
19
- class: className = '',
20
- }: Props = $props();
21
- </script>
22
-
23
- <div class={cn('hyvui-plinth-wrap', className)}>
24
- <div
25
- class="hyvui-plinth"
26
- style:width={width}
27
- style:height={depth}
28
- style:background-color={color}
29
- ></div>
30
- </div>
31
-
32
- <style>
33
- .hyvui-plinth-wrap {
34
- perspective: var(--perspective-mid);
35
- perspective-origin: 50% 0%;
36
- display: flex;
37
- justify-content: center;
38
- pointer-events: none;
39
- }
40
-
41
- .hyvui-plinth {
42
- transform: rotateX(70deg) translateZ(-20px);
43
- border-top: 1px solid rgba(199, 156, 87, 0.08);
44
- opacity: 0.6;
45
- }
46
-
47
- @media (prefers-reduced-motion: reduce) {
48
- .hyvui-plinth-wrap {
49
- perspective: none;
50
- }
51
-
52
- .hyvui-plinth {
53
- transform: none;
54
- opacity: 0.3;
55
- }
56
- }
57
- </style>
1
+ <script lang="ts">
2
+ import { cn } from '../../utils/cn.js';
3
+
4
+ interface Props {
5
+ /** Width of the plinth surface. */
6
+ width?: string;
7
+ /** How far the plinth recedes (visual depth). */
8
+ depth?: string;
9
+ /** Surface tint color. */
10
+ color?: string;
11
+ /** Additional CSS classes. */
12
+ class?: string;
13
+ }
14
+
15
+ let {
16
+ width = '100%',
17
+ depth = '40px',
18
+ color = 'rgba(199, 156, 87, 0.03)',
19
+ class: className = ''
20
+ }: Props = $props();
21
+ </script>
22
+
23
+ <div class={cn('hyvui-plinth-wrap', className)}>
24
+ <div class="hyvui-plinth" style:width style:height={depth} style:background-color={color}></div>
25
+ </div>
26
+
27
+ <style>
28
+ .hyvui-plinth-wrap {
29
+ perspective: var(--perspective-mid);
30
+ perspective-origin: 50% 0%;
31
+ display: flex;
32
+ justify-content: center;
33
+ pointer-events: none;
34
+ }
35
+
36
+ .hyvui-plinth {
37
+ transform: rotateX(70deg) translateZ(-20px);
38
+ border-top: 1px solid rgba(199, 156, 87, 0.08);
39
+ opacity: 0.6;
40
+ }
41
+
42
+ @media (prefers-reduced-motion: reduce) {
43
+ .hyvui-plinth-wrap {
44
+ perspective: none;
45
+ }
46
+
47
+ .hyvui-plinth {
48
+ transform: none;
49
+ opacity: 0.3;
50
+ }
51
+ }
52
+ </style>
@@ -1,69 +1,64 @@
1
- <script lang="ts">
2
- import { cn } from '../../utils/cn.js';
3
-
4
- interface Props {
5
- /** Image source URL. */
6
- src?: string;
7
- /** Name for initials fallback. */
8
- name?: string;
9
- /** Avatar diameter in pixels. */
10
- size?: number;
11
- /** Additional CSS classes. */
12
- class?: string;
13
- }
14
-
15
- let {
16
- src = '',
17
- name = '',
18
- size = 32,
19
- class: className = '',
20
- }: Props = $props();
21
-
22
- const initials = $derived(
23
- name
24
- .split(' ')
25
- .map((w) => w[0])
26
- .join('')
27
- .slice(0, 2)
28
- .toUpperCase()
29
- );
30
- </script>
31
-
32
- <span
33
- class={cn('hyvui-avatar', className)}
34
- style:width="{size}px"
35
- style:height="{size}px"
36
- style:font-size="{size * 0.38}px"
37
- >
38
- {#if src}
39
- <img {src} alt={name} class="hyvui-avatar-img" />
40
- {:else}
41
- <span class="hyvui-avatar-initials">{initials}</span>
42
- {/if}
43
- </span>
44
-
45
- <style>
46
- .hyvui-avatar {
47
- display: inline-flex;
48
- align-items: center;
49
- justify-content: center;
50
- border-radius: 50%;
51
- background-color: var(--bg-elev);
52
- border: 1px solid var(--line);
53
- overflow: hidden;
54
- flex-shrink: 0;
55
- }
56
-
57
- .hyvui-avatar-img {
58
- width: 100%;
59
- height: 100%;
60
- object-fit: cover;
61
- }
62
-
63
- .hyvui-avatar-initials {
64
- font-family: var(--font-mono);
65
- font-weight: 400;
66
- color: var(--muted);
67
- letter-spacing: 0.04em;
68
- }
69
- </style>
1
+ <script lang="ts">
2
+ import { cn } from '../../utils/cn.js';
3
+
4
+ interface Props {
5
+ /** Image source URL. */
6
+ src?: string;
7
+ /** Name for initials fallback. */
8
+ name?: string;
9
+ /** Avatar diameter in pixels. */
10
+ size?: number;
11
+ /** Additional CSS classes. */
12
+ class?: string;
13
+ }
14
+
15
+ let { src = '', name = '', size = 32, class: className = '' }: Props = $props();
16
+
17
+ const initials = $derived(
18
+ name
19
+ .split(' ')
20
+ .map((w) => w[0])
21
+ .join('')
22
+ .slice(0, 2)
23
+ .toUpperCase()
24
+ );
25
+ </script>
26
+
27
+ <span
28
+ class={cn('hyvui-avatar', className)}
29
+ style:width="{size}px"
30
+ style:height="{size}px"
31
+ style:font-size="{size * 0.38}px"
32
+ >
33
+ {#if src}
34
+ <img {src} alt={name} class="hyvui-avatar-img" />
35
+ {:else}
36
+ <span class="hyvui-avatar-initials">{initials}</span>
37
+ {/if}
38
+ </span>
39
+
40
+ <style>
41
+ .hyvui-avatar {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ border-radius: 50%;
46
+ background-color: var(--bg-elev);
47
+ border: 1px solid var(--line);
48
+ overflow: hidden;
49
+ flex-shrink: 0;
50
+ }
51
+
52
+ .hyvui-avatar-img {
53
+ width: 100%;
54
+ height: 100%;
55
+ object-fit: cover;
56
+ }
57
+
58
+ .hyvui-avatar-initials {
59
+ font-family: var(--font-mono);
60
+ font-weight: 400;
61
+ color: var(--muted);
62
+ letter-spacing: 0.04em;
63
+ }
64
+ </style>