@marianmeres/stuic 2.51.0 → 2.52.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.
@@ -0,0 +1,237 @@
1
+ <script lang="ts" module>
2
+ export type IconFn = (opts?: { size?: number; class?: string }) => string;
3
+ export type AvatarFallback = "icon" | "initials" | { icon: IconFn } | { initials: string };
4
+
5
+ export interface Props {
6
+ /** Photo URL - when provided, renders in photo mode */
7
+ src?: string;
8
+ /** Alt text for photo mode */
9
+ alt?: string;
10
+ /** String to extract initials from. Supports: "AB", "John Doe", or "john.doe@example.com" */
11
+ initials?: string;
12
+ /** Icon function to display - when provided alone, renders in icon mode */
13
+ icon?: IconFn;
14
+ /** Fallback when photo fails to load. Defaults to "icon" */
15
+ fallback?: AvatarFallback;
16
+ /** Optional string for color hash calculation (e.g., email, user ID). Falls back to `initials` */
17
+ hashSource?: string;
18
+ /** Size preset or custom Tailwind size class */
19
+ size?: "sm" | "md" | "lg" | "xl" | string;
20
+ /** Click handler - when provided, renders as a button */
21
+ onclick?: (event: MouseEvent) => void;
22
+ /** Background color (Tailwind class). Ignored if autoColor=true */
23
+ bg?: string;
24
+ /** Text color (Tailwind class). Ignored if autoColor=true */
25
+ textColor?: string;
26
+ /** Generate deterministic pastel colors from hashSource/initials */
27
+ autoColor?: boolean;
28
+ /** CSS class override */
29
+ class?: string;
30
+ /** Bindable element reference */
31
+ el?: HTMLDivElement | HTMLButtonElement;
32
+ }
33
+ </script>
34
+
35
+ <script lang="ts">
36
+ import { twMerge } from "../../utils/tw-merge.js";
37
+ import { generateAvatarColors } from "../../utils/avatar-colors.js";
38
+ import { iconUser as defaultIconUser } from "../../icons/index.js";
39
+
40
+ let {
41
+ src,
42
+ alt,
43
+ initials: initialsProp,
44
+ icon,
45
+ fallback = "icon",
46
+ hashSource,
47
+ size = "md",
48
+ onclick,
49
+ bg,
50
+ textColor,
51
+ autoColor = false,
52
+ class: classProp,
53
+ el = $bindable(),
54
+ }: Props = $props();
55
+
56
+ const SIZE_PRESETS: Record<string, { container: string; icon: number }> = {
57
+ sm: { container: "size-8 text-xs", icon: 16 },
58
+ md: { container: "size-10 text-sm", icon: 20 },
59
+ lg: { container: "size-14 text-base", icon: 28 },
60
+ xl: { container: "size-16 text-lg", icon: 32 },
61
+ };
62
+
63
+ // Image loading state
64
+ let imageError = $state(false);
65
+
66
+ // Reset image state when src changes
67
+ $effect(() => {
68
+ if (src) {
69
+ imageError = false;
70
+ }
71
+ });
72
+
73
+ // Extract initials from string
74
+ let extractedInitials = $derived.by(() => {
75
+ let _input = (initialsProp || "").trim();
76
+
77
+ if (!_input) return "?";
78
+
79
+ // Check if input looks like an email
80
+ if (_input.includes("@")) {
81
+ const username = _input.split("@")[0];
82
+ // Split by common separators (., _, -)
83
+ const parts = username.split(/[._+-]/).filter(Boolean);
84
+ if (parts.length > 1) {
85
+ _input = parts.map((p) => p.charAt(0)).join("");
86
+ } else {
87
+ _input = username;
88
+ }
89
+ }
90
+ // Check if input looks like a full name (multiple words)
91
+ else if (_input.length > 2 && /\s/.test(_input)) {
92
+ _input = _input
93
+ .split(/\s/)
94
+ .map((v) => v.trim())
95
+ .filter(Boolean)
96
+ .map((v) => v.charAt(0))
97
+ .join("");
98
+ }
99
+
100
+ // Extract first 2 chars, uppercase
101
+ return _input.slice(0, 2).toUpperCase();
102
+ });
103
+
104
+ // Determine the current render mode
105
+ let renderMode = $derived.by((): "photo" | "initials" | "icon" => {
106
+ // Photo mode (if src provided and no error)
107
+ if (src && !imageError) return "photo";
108
+
109
+ // Photo error - determine fallback
110
+ if (src && imageError) {
111
+ if (fallback === "initials") return "initials";
112
+ if (typeof fallback === "object" && "initials" in fallback) return "initials";
113
+ return "icon";
114
+ }
115
+
116
+ // No src - determine from other props
117
+ if (initialsProp) return "initials";
118
+ return "icon";
119
+ });
120
+
121
+ // Get fallback initials (from fallback prop or initialsProp)
122
+ let fallbackInitials = $derived.by(() => {
123
+ if (typeof fallback === "object" && "initials" in fallback) {
124
+ const _input = (fallback.initials || "").trim();
125
+ if (!_input) return "?";
126
+
127
+ // Apply same extraction logic
128
+ let result = _input;
129
+ if (_input.includes("@")) {
130
+ const username = _input.split("@")[0];
131
+ const parts = username.split(/[._+-]/).filter(Boolean);
132
+ if (parts.length > 1) {
133
+ result = parts.map((p) => p.charAt(0)).join("");
134
+ } else {
135
+ result = username;
136
+ }
137
+ } else if (_input.length > 2 && /\s/.test(_input)) {
138
+ result = _input
139
+ .split(/\s/)
140
+ .map((v) => v.trim())
141
+ .filter(Boolean)
142
+ .map((v) => v.charAt(0))
143
+ .join("");
144
+ }
145
+ return result.slice(0, 2).toUpperCase();
146
+ }
147
+ return extractedInitials;
148
+ });
149
+
150
+ // Get the icon to render
151
+ let iconToRender = $derived.by(() => {
152
+ // If fallback specifies a custom icon
153
+ if (imageError && typeof fallback === "object" && "icon" in fallback) {
154
+ return fallback.icon;
155
+ }
156
+ // Use provided icon or default
157
+ return icon || defaultIconUser;
158
+ });
159
+
160
+ // Get icon size based on preset or custom size
161
+ let iconSize = $derived.by(() => {
162
+ const preset = SIZE_PRESETS[size];
163
+ if (preset) return preset.icon;
164
+
165
+ // For custom sizes, try to parse size-N pattern
166
+ const match = size?.match(/size-(\d+)/);
167
+ if (match) {
168
+ // size-N = N * 4px, icon should be ~50%
169
+ return parseInt(match[1]) * 2;
170
+ }
171
+
172
+ return 20; // Default fallback
173
+ });
174
+
175
+ let colors = $derived(
176
+ autoColor ? generateAvatarColors(hashSource || initialsProp || "") : null
177
+ );
178
+
179
+ let sizeClass = $derived(SIZE_PRESETS[size]?.container || size);
180
+
181
+ let style = $derived(
182
+ autoColor && colors
183
+ ? `background-color: ${colors.bg}; color: ${colors.text};`
184
+ : undefined
185
+ );
186
+
187
+ let baseClass = $derived(
188
+ twMerge(
189
+ "stuic-avatar",
190
+ "inline-flex items-center justify-center",
191
+ "rounded-full font-medium shrink-0 overflow-hidden",
192
+ !autoColor &&
193
+ "bg-neutral-200 text-neutral-700 dark:bg-neutral-700 dark:text-neutral-200",
194
+ sizeClass,
195
+ !autoColor && bg,
196
+ !autoColor && textColor,
197
+ onclick && "select-none cursor-pointer",
198
+ classProp
199
+ )
200
+ );
201
+
202
+ function handleImageError() {
203
+ imageError = true;
204
+ }
205
+ </script>
206
+
207
+ {#if onclick}
208
+ <button bind:this={el} type="button" class={baseClass} {style} {onclick}>
209
+ {#if renderMode === "photo"}
210
+ <img
211
+ {src}
212
+ {alt}
213
+ class="size-full object-cover"
214
+ onerror={handleImageError}
215
+ />
216
+ {:else if renderMode === "initials"}
217
+ {fallbackInitials}
218
+ {:else}
219
+ {@html iconToRender({ size: iconSize })}
220
+ {/if}
221
+ </button>
222
+ {:else}
223
+ <div bind:this={el} class={baseClass} {style}>
224
+ {#if renderMode === "photo"}
225
+ <img
226
+ {src}
227
+ {alt}
228
+ class="size-full object-cover"
229
+ onerror={handleImageError}
230
+ />
231
+ {:else if renderMode === "initials"}
232
+ {fallbackInitials}
233
+ {:else}
234
+ {@html iconToRender({ size: iconSize })}
235
+ {/if}
236
+ </div>
237
+ {/if}
@@ -0,0 +1,40 @@
1
+ export type IconFn = (opts?: {
2
+ size?: number;
3
+ class?: string;
4
+ }) => string;
5
+ export type AvatarFallback = "icon" | "initials" | {
6
+ icon: IconFn;
7
+ } | {
8
+ initials: string;
9
+ };
10
+ export interface Props {
11
+ /** Photo URL - when provided, renders in photo mode */
12
+ src?: string;
13
+ /** Alt text for photo mode */
14
+ alt?: string;
15
+ /** String to extract initials from. Supports: "AB", "John Doe", or "john.doe@example.com" */
16
+ initials?: string;
17
+ /** Icon function to display - when provided alone, renders in icon mode */
18
+ icon?: IconFn;
19
+ /** Fallback when photo fails to load. Defaults to "icon" */
20
+ fallback?: AvatarFallback;
21
+ /** Optional string for color hash calculation (e.g., email, user ID). Falls back to `initials` */
22
+ hashSource?: string;
23
+ /** Size preset or custom Tailwind size class */
24
+ size?: "sm" | "md" | "lg" | "xl" | string;
25
+ /** Click handler - when provided, renders as a button */
26
+ onclick?: (event: MouseEvent) => void;
27
+ /** Background color (Tailwind class). Ignored if autoColor=true */
28
+ bg?: string;
29
+ /** Text color (Tailwind class). Ignored if autoColor=true */
30
+ textColor?: string;
31
+ /** Generate deterministic pastel colors from hashSource/initials */
32
+ autoColor?: boolean;
33
+ /** CSS class override */
34
+ class?: string;
35
+ /** Bindable element reference */
36
+ el?: HTMLDivElement | HTMLButtonElement;
37
+ }
38
+ declare const Avatar: import("svelte").Component<Props, {}, "el">;
39
+ type Avatar = ReturnType<typeof Avatar>;
40
+ export default Avatar;
@@ -0,0 +1 @@
1
+ export { default as Avatar, type Props as AvatarProps, type IconFn, type AvatarFallback, } from "./Avatar.svelte";
@@ -0,0 +1 @@
1
+ export { default as Avatar, } from "./Avatar.svelte";
@@ -34,3 +34,4 @@ export { iconLucideCheck as iconCheck } from "@marianmeres/icons-fns/lucide/icon
34
34
  export { iconLucideCircle as iconCircle } from "@marianmeres/icons-fns/lucide/iconLucideCircle.js";
35
35
  export { iconLucideSquare as iconSquare } from "@marianmeres/icons-fns/lucide/iconLucideSquare.js";
36
36
  export { iconLucideMenu as iconMenu } from "@marianmeres/icons-fns/lucide/iconLucideMenu.js";
37
+ export { iconLucideUser as iconUser } from "@marianmeres/icons-fns/lucide/iconLucideUser.js";
@@ -38,3 +38,4 @@ export { iconLucideCheck as iconCheck } from "@marianmeres/icons-fns/lucide/icon
38
38
  export { iconLucideCircle as iconCircle } from "@marianmeres/icons-fns/lucide/iconLucideCircle.js";
39
39
  export { iconLucideSquare as iconSquare } from "@marianmeres/icons-fns/lucide/iconLucideSquare.js";
40
40
  export { iconLucideMenu as iconMenu } from "@marianmeres/icons-fns/lucide/iconLucideMenu.js";
41
+ export { iconLucideUser as iconUser } from "@marianmeres/icons-fns/lucide/iconLucideUser.js";
package/dist/index.d.ts CHANGED
@@ -25,7 +25,7 @@ export * from "./components/AlertConfirmPrompt/index.js";
25
25
  export * from "./components/AnimatedElipsis/index.js";
26
26
  export * from "./components/AppShell/index.js";
27
27
  export * from "./components/AssetsPreview/index.js";
28
- export * from "./components/AvatarInitials/index.js";
28
+ export * from "./components/Avatar/index.js";
29
29
  export * from "./components/Backdrop/index.js";
30
30
  export * from "./components/Button/index.js";
31
31
  export * from "./components/ButtonGroupRadio/index.js";
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ export * from "./components/AlertConfirmPrompt/index.js";
26
26
  export * from "./components/AnimatedElipsis/index.js";
27
27
  export * from "./components/AppShell/index.js";
28
28
  export * from "./components/AssetsPreview/index.js";
29
- export * from "./components/AvatarInitials/index.js";
29
+ export * from "./components/Avatar/index.js";
30
30
  export * from "./components/Backdrop/index.js";
31
31
  export * from "./components/Button/index.js";
32
32
  export * from "./components/ButtonGroupRadio/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "2.51.0",
3
+ "version": "2.52.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",
@@ -1,113 +0,0 @@
1
- <script lang="ts" module>
2
- export interface Props {
3
- /** String to extract initials from. Supports: "AB", "John Doe", or "john.doe@example.com" */
4
- input: string;
5
- /** Optional string for color hash calculation (e.g., email, user ID). Falls back to `input` */
6
- hashSource?: string;
7
- /** Size preset or custom Tailwind size class */
8
- size?: "sm" | "md" | "lg" | "xl" | string;
9
- /** Click handler - when provided, renders as a button */
10
- onclick?: (event: MouseEvent) => void;
11
- /** Background color (Tailwind class). Ignored if autoColor=true */
12
- bg?: string;
13
- /** Text color (Tailwind class). Ignored if autoColor=true */
14
- textColor?: string;
15
- /** Generate deterministic pastel colors from hashSource/input */
16
- autoColor?: boolean;
17
- /** CSS class override */
18
- class?: string;
19
- /** Bindable element reference */
20
- el?: HTMLDivElement | HTMLButtonElement;
21
- }
22
- </script>
23
-
24
- <script lang="ts">
25
- import { twMerge } from "../../utils/tw-merge.js";
26
- import { generateAvatarColors } from "../../utils/avatar-colors.js";
27
-
28
- let {
29
- input,
30
- hashSource,
31
- size = "md",
32
- onclick,
33
- bg,
34
- textColor,
35
- autoColor = false,
36
- class: classProp,
37
- el = $bindable(),
38
- }: Props = $props();
39
-
40
- const SIZE_PRESETS: Record<string, string> = {
41
- sm: "size-8 text-xs",
42
- md: "size-10 text-sm",
43
- lg: "size-14 text-base",
44
- xl: "size-16 text-lg",
45
- };
46
-
47
- let initials = $derived.by(() => {
48
- let _input = (input || "").trim();
49
-
50
- if (!_input) return "?";
51
-
52
- // Check if input looks like an email
53
- if (_input.includes("@")) {
54
- const username = _input.split("@")[0];
55
- // Split by common separators (., _, -)
56
- const parts = username.split(/[._+-]/).filter(Boolean);
57
- if (parts.length > 1) {
58
- _input = parts.map((p) => p.charAt(0)).join("");
59
- } else {
60
- _input = username;
61
- }
62
- }
63
- // Check if input looks like a full name (multiple words)
64
- else if (_input.length > 2 && /\s/.test(_input)) {
65
- _input = _input
66
- .split(/\s/)
67
- .map((v) => v.trim())
68
- .filter(Boolean)
69
- .map((v) => v.charAt(0))
70
- .join("");
71
- }
72
-
73
- // Extract first 2 chars, uppercase
74
- return _input.slice(0, 2).toUpperCase();
75
- });
76
-
77
- let colors = $derived(
78
- autoColor ? generateAvatarColors(hashSource || input || "") : null
79
- );
80
-
81
- let sizeClass = $derived(SIZE_PRESETS[size] || size);
82
-
83
- let style = $derived(
84
- autoColor && colors
85
- ? `background-color: ${colors.bg}; color: ${colors.text};`
86
- : undefined
87
- );
88
-
89
- let baseClass = $derived(
90
- twMerge(
91
- "stuic-avatar-initials",
92
- "inline-flex items-center justify-center",
93
- "rounded-full font-medium shrink-0",
94
- !autoColor &&
95
- "bg-neutral-200 text-neutral-700 dark:bg-neutral-700 dark:text-neutral-200",
96
- sizeClass,
97
- !autoColor && bg,
98
- !autoColor && textColor,
99
- onclick && "select-none cursor-pointer",
100
- classProp
101
- )
102
- );
103
- </script>
104
-
105
- {#if onclick}
106
- <button bind:this={el} type="button" class={baseClass} {style} {onclick}>
107
- {initials}
108
- </button>
109
- {:else}
110
- <div bind:this={el} class={baseClass} {style}>
111
- {initials}
112
- </div>
113
- {/if}
@@ -1,23 +0,0 @@
1
- export interface Props {
2
- /** String to extract initials from. Supports: "AB", "John Doe", or "john.doe@example.com" */
3
- input: string;
4
- /** Optional string for color hash calculation (e.g., email, user ID). Falls back to `input` */
5
- hashSource?: string;
6
- /** Size preset or custom Tailwind size class */
7
- size?: "sm" | "md" | "lg" | "xl" | string;
8
- /** Click handler - when provided, renders as a button */
9
- onclick?: (event: MouseEvent) => void;
10
- /** Background color (Tailwind class). Ignored if autoColor=true */
11
- bg?: string;
12
- /** Text color (Tailwind class). Ignored if autoColor=true */
13
- textColor?: string;
14
- /** Generate deterministic pastel colors from hashSource/input */
15
- autoColor?: boolean;
16
- /** CSS class override */
17
- class?: string;
18
- /** Bindable element reference */
19
- el?: HTMLDivElement | HTMLButtonElement;
20
- }
21
- declare const AvatarInitials: import("svelte").Component<Props, {}, "el">;
22
- type AvatarInitials = ReturnType<typeof AvatarInitials>;
23
- export default AvatarInitials;
@@ -1,169 +0,0 @@
1
- # AvatarInitials
2
-
3
- A circular avatar component displaying initials extracted from names or emails, with optional auto-generated colors and size presets.
4
-
5
- ## Props
6
-
7
- | Prop | Type | Default | Description |
8
- |------|------|---------|-------------|
9
- | `input` | `string` | - | String to extract initials from (name, initials, or email) |
10
- | `hashSource` | `string` | - | Optional string for color hash calculation (falls back to `input`) |
11
- | `size` | `"sm" \| "md" \| "lg" \| "xl" \| string` | `"md"` | Size preset or custom Tailwind class |
12
- | `onclick` | `(event: MouseEvent) => void` | - | Click handler (renders as button when provided) |
13
- | `bg` | `string` | - | Background color Tailwind class (ignored if autoColor) |
14
- | `textColor` | `string` | - | Text color Tailwind class (ignored if autoColor) |
15
- | `autoColor` | `boolean` | `false` | Generate deterministic pastel colors from input |
16
- | `class` | `string` | - | Additional CSS classes |
17
- | `el` | `HTMLDivElement \| HTMLButtonElement` | - | Element reference (bindable) |
18
-
19
- ## Size Presets
20
-
21
- | Size | Dimensions | Font Size |
22
- |------|------------|-----------|
23
- | `sm` | 32px (size-8) | text-xs |
24
- | `md` | 40px (size-10) | text-sm |
25
- | `lg` | 56px (size-14) | text-base |
26
- | `xl` | 64px (size-16) | text-lg |
27
-
28
- Custom sizes can be passed as Tailwind classes: `size="size-20 text-2xl"`
29
-
30
- ## Initials Extraction Logic
31
-
32
- The component intelligently extracts up to 2 characters from the input:
33
-
34
- 1. **Email addresses** (`john.doe@example.com`):
35
- - Splits username by `.`, `_`, `+`, `-`
36
- - Takes first letter of each part
37
- - Result: `JD`
38
-
39
- 2. **Full names** (`John Doe`):
40
- - Splits by whitespace
41
- - Takes first letter of each word
42
- - Result: `JD`
43
-
44
- 3. **Short strings** (`AB` or `Jo`):
45
- - Takes first 2 characters
46
- - Result: `AB` or `JO`
47
-
48
- 4. **Empty input**:
49
- - Returns `?`
50
-
51
- All initials are uppercase.
52
-
53
- ## Auto Color Generation
54
-
55
- When `autoColor` is enabled, the component generates deterministic pastel colors:
56
-
57
- - Colors are derived from a hash of `hashSource` or `input`
58
- - Same input always produces the same color
59
- - Colors are designed as accessible pastel tones
60
- - Text color automatically contrasts with background
61
-
62
- ## Usage
63
-
64
- ### Basic Display
65
-
66
- ```svelte
67
- <script lang="ts">
68
- import { AvatarInitials } from 'stuic';
69
- </script>
70
-
71
- <AvatarInitials input="John Doe" />
72
- <AvatarInitials input="jane.smith@example.com" />
73
- <AvatarInitials input="AB" />
74
- ```
75
-
76
- ### Size Variants
77
-
78
- ```svelte
79
- <AvatarInitials input="JD" size="sm" />
80
- <AvatarInitials input="JD" size="md" />
81
- <AvatarInitials input="JD" size="lg" />
82
- <AvatarInitials input="JD" size="xl" />
83
-
84
- <!-- Custom size -->
85
- <AvatarInitials input="JD" size="size-24 text-3xl" />
86
- ```
87
-
88
- ### Auto Color (Deterministic)
89
-
90
- ```svelte
91
- <!-- Same email always produces same color -->
92
- <AvatarInitials input="john@example.com" autoColor />
93
- <AvatarInitials input="jane@example.com" autoColor />
94
-
95
- <!-- Use ID for consistent color regardless of display name -->
96
- <AvatarInitials input="John Doe" hashSource="user-123" autoColor />
97
- ```
98
-
99
- ### Custom Colors
100
-
101
- ```svelte
102
- <AvatarInitials
103
- input="JD"
104
- bg="bg-blue-500"
105
- textColor="text-white"
106
- />
107
-
108
- <AvatarInitials
109
- input="AB"
110
- bg="bg-gradient-to-br from-purple-500 to-pink-500"
111
- textColor="text-white"
112
- />
113
- ```
114
-
115
- ### Clickable Avatar
116
-
117
- ```svelte
118
- <script lang="ts">
119
- function handleClick() {
120
- console.log('Avatar clicked');
121
- }
122
- </script>
123
-
124
- <AvatarInitials
125
- input="john@example.com"
126
- autoColor
127
- onclick={handleClick}
128
- />
129
- ```
130
-
131
- ### In Header Dropdown
132
-
133
- ```svelte
134
- <script lang="ts">
135
- import { AvatarInitials, DropdownMenu } from 'stuic';
136
- </script>
137
-
138
- <DropdownMenu
139
- items={[
140
- { type: "action", id: "profile", label: "View Profile" },
141
- { type: "action", id: "logout", label: "Logout" },
142
- ]}
143
- >
144
- {#snippet trigger({ toggle })}
145
- <AvatarInitials
146
- input={userEmail}
147
- onclick={toggle}
148
- autoColor
149
- class="cursor-pointer hover:ring-2 hover:ring-blue-500"
150
- />
151
- {/snippet}
152
- </DropdownMenu>
153
- ```
154
-
155
- ### Avatar List
156
-
157
- ```svelte
158
- <div class="flex -space-x-2">
159
- {#each users as user}
160
- <AvatarInitials
161
- input={user.email}
162
- hashSource={user.id}
163
- autoColor
164
- size="sm"
165
- class="ring-2 ring-white"
166
- />
167
- {/each}
168
- </div>
169
- ```
@@ -1 +0,0 @@
1
- export { default as AvatarInitials, type Props as AvatarInitialsProps, } from "./AvatarInitials.svelte";
@@ -1 +0,0 @@
1
- export { default as AvatarInitials, } from "./AvatarInitials.svelte";