@marianmeres/stuic 2.13.0 → 2.16.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/dist/components/AvatarInitials/AvatarInitials.svelte +112 -0
- package/dist/components/AvatarInitials/AvatarInitials.svelte.d.ts +23 -0
- package/dist/components/AvatarInitials/index.d.ts +1 -0
- package/dist/components/AvatarInitials/index.js +1 -0
- package/dist/components/DropdownMenu/DropdownMenu.svelte +6 -6
- package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/avatar-colors.d.ts +24 -0
- package/dist/utils/avatar-colors.js +37 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
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" | 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
|
+
};
|
|
45
|
+
|
|
46
|
+
let initials = $derived.by(() => {
|
|
47
|
+
let _input = (input || "").trim();
|
|
48
|
+
|
|
49
|
+
if (!_input) return "?";
|
|
50
|
+
|
|
51
|
+
// Check if input looks like an email
|
|
52
|
+
if (_input.includes("@")) {
|
|
53
|
+
const username = _input.split("@")[0];
|
|
54
|
+
// Split by common separators (., _, -)
|
|
55
|
+
const parts = username.split(/[._-]/).filter(Boolean);
|
|
56
|
+
if (parts.length > 1) {
|
|
57
|
+
_input = parts.map((p) => p.charAt(0)).join("");
|
|
58
|
+
} else {
|
|
59
|
+
_input = username;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Check if input looks like a full name (multiple words)
|
|
63
|
+
else if (_input.length > 2 && /\s/.test(_input)) {
|
|
64
|
+
_input = _input
|
|
65
|
+
.split(/\s/)
|
|
66
|
+
.map((v) => v.trim())
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.map((v) => v.charAt(0))
|
|
69
|
+
.join("");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Extract first 2 chars, uppercase
|
|
73
|
+
return _input.slice(0, 2).toUpperCase();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
let colors = $derived(
|
|
77
|
+
autoColor ? generateAvatarColors(hashSource || input || "") : null
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
let sizeClass = $derived(SIZE_PRESETS[size] || size);
|
|
81
|
+
|
|
82
|
+
let style = $derived(
|
|
83
|
+
autoColor && colors
|
|
84
|
+
? `background-color: ${colors.bg}; color: ${colors.text};`
|
|
85
|
+
: undefined
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
let baseClass = $derived(
|
|
89
|
+
twMerge(
|
|
90
|
+
"stuic-avatar-initials",
|
|
91
|
+
"inline-flex items-center justify-center",
|
|
92
|
+
"rounded-full font-medium shrink-0",
|
|
93
|
+
!autoColor &&
|
|
94
|
+
"bg-neutral-200 text-neutral-700 dark:bg-neutral-700 dark:text-neutral-200",
|
|
95
|
+
sizeClass,
|
|
96
|
+
!autoColor && bg,
|
|
97
|
+
!autoColor && textColor,
|
|
98
|
+
onclick && "select-none cursor-pointer",
|
|
99
|
+
classProp
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
{#if onclick}
|
|
105
|
+
<button bind:this={el} type="button" class={baseClass} {style} {onclick}>
|
|
106
|
+
{initials}
|
|
107
|
+
</button>
|
|
108
|
+
{:else}
|
|
109
|
+
<div bind:this={el} class={baseClass} {style}>
|
|
110
|
+
{initials}
|
|
111
|
+
</div>
|
|
112
|
+
{/if}
|
|
@@ -0,0 +1,23 @@
|
|
|
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" | 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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as AvatarInitials, type Props as AvatarInitialsProps, } from "./AvatarInitials.svelte";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as AvatarInitials, } from "./AvatarInitials.svelte";
|
|
@@ -159,8 +159,8 @@
|
|
|
159
159
|
onOpen?: () => void;
|
|
160
160
|
/** Called when menu closes */
|
|
161
161
|
onClose?: () => void;
|
|
162
|
-
/** Called when any action item is selected */
|
|
163
|
-
onSelect?: (item: DropdownMenuActionItem) => void;
|
|
162
|
+
/** Called when any action item is selected (fallback if item has no onSelect) */
|
|
163
|
+
onSelect?: (item: DropdownMenuActionItem) => void | boolean;
|
|
164
164
|
/** Reference to trigger element */
|
|
165
165
|
triggerEl?: HTMLButtonElement;
|
|
166
166
|
/** Reference to dropdown element */
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
bg-white dark:bg-neutral-800
|
|
214
214
|
text-neutral-900 dark:text-neutral-100
|
|
215
215
|
border border-neutral-200 dark:border-neutral-700
|
|
216
|
-
rounded-md shadow-
|
|
216
|
+
rounded-md shadow-md
|
|
217
217
|
p-1
|
|
218
218
|
overflow-y-auto
|
|
219
219
|
z-50
|
|
@@ -299,7 +299,7 @@
|
|
|
299
299
|
let wrapperEl: HTMLDivElement = $state()!;
|
|
300
300
|
let activeItemEl: HTMLButtonElement | undefined = $state();
|
|
301
301
|
const reducedMotion = prefersReducedMotion();
|
|
302
|
-
const isSupported = !forceFallback && isAnchorPositioningSupported();
|
|
302
|
+
const isSupported = untrack(() => !forceFallback && isAnchorPositioningSupported());
|
|
303
303
|
|
|
304
304
|
// Track expanded sections (independent toggle - multiple can be open)
|
|
305
305
|
let expandedSections = $state<Set<string | number>>(new Set());
|
|
@@ -432,8 +432,8 @@
|
|
|
432
432
|
function selectItem(item: DropdownMenuActionItem) {
|
|
433
433
|
if (item.disabled) return;
|
|
434
434
|
|
|
435
|
-
|
|
436
|
-
onSelect?.(item);
|
|
435
|
+
// Call item's onSelect if defined, otherwise fall back to component's onSelect
|
|
436
|
+
const result = item.onSelect ? item.onSelect() : onSelect?.(item);
|
|
437
437
|
|
|
438
438
|
if (result !== false && closeOnSelect) {
|
|
439
439
|
isOpen = false;
|
|
@@ -129,8 +129,8 @@ export interface Props extends Omit<HTMLButtonAttributes, "children"> {
|
|
|
129
129
|
onOpen?: () => void;
|
|
130
130
|
/** Called when menu closes */
|
|
131
131
|
onClose?: () => void;
|
|
132
|
-
/** Called when any action item is selected */
|
|
133
|
-
onSelect?: (item: DropdownMenuActionItem) => void;
|
|
132
|
+
/** Called when any action item is selected (fallback if item has no onSelect) */
|
|
133
|
+
onSelect?: (item: DropdownMenuActionItem) => void | boolean;
|
|
134
134
|
/** Reference to trigger element */
|
|
135
135
|
triggerEl?: HTMLButtonElement;
|
|
136
136
|
/** Reference to dropdown element */
|
|
@@ -138,7 +138,7 @@ export interface Props extends Omit<HTMLButtonAttributes, "children"> {
|
|
|
138
138
|
}
|
|
139
139
|
export declare const DROPDOWN_MENU_BASE_CLASSES = "stuic-dropdown-menu relative inline-block";
|
|
140
140
|
export declare const DROPDOWN_MENU_TRIGGER_CLASSES = "\n\t\tinline-flex items-center justify-center gap-2\n\t\tpx-3 py-2\n\t\trounded-md border\n\t\tbg-white dark:bg-neutral-800\n\t\ttext-neutral-900 dark:text-neutral-100\n\t\tborder-neutral-200 dark:border-neutral-700\n\t\thover:brightness-95 dark:hover:brightness-110\n\t\tfocus-visible:outline-2 focus-visible:outline-offset-2\n\t\tcursor-pointer\n\t";
|
|
141
|
-
export declare const DROPDOWN_MENU_DROPDOWN_CLASSES = "\n\t\tstuic-dropdown-menu-dropdown\n\t\tbg-white dark:bg-neutral-800\n\t\ttext-neutral-900 dark:text-neutral-100\n\t\tborder border-neutral-200 dark:border-neutral-700\n\t\trounded-md shadow-
|
|
141
|
+
export declare const DROPDOWN_MENU_DROPDOWN_CLASSES = "\n\t\tstuic-dropdown-menu-dropdown\n\t\tbg-white dark:bg-neutral-800\n\t\ttext-neutral-900 dark:text-neutral-100\n\t\tborder border-neutral-200 dark:border-neutral-700\n\t\trounded-md shadow-md\n\t\tp-1\n\t\toverflow-y-auto\n\t\tz-50\n\t\tmin-w-48\n\t";
|
|
142
142
|
export declare const DROPDOWN_MENU_ITEM_CLASSES = "\n\t\tw-full\n\t\tflex items-center gap-2\n\t\tpx-2 py-1.5\n\t\tmin-h-[44px]\n\t\ttext-left text-sm\n\t\trounded-sm\n\t\tcursor-pointer\n\t\ttouch-action-manipulation\n\t\thover:bg-neutral-100 dark:hover:bg-neutral-700\n\t\tfocus:outline-none\n\t\tfocus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-600\n\t";
|
|
143
143
|
export declare const DROPDOWN_MENU_DIVIDER_CLASSES = "\n\t\th-px my-1\n\t\tbg-neutral-200 dark:bg-neutral-700\n\t";
|
|
144
144
|
export declare const DROPDOWN_MENU_HEADER_CLASSES = "\n\t\tpx-2 py-1.5\n\t\ttext-xs font-semibold uppercase tracking-wide\n\t\ttext-neutral-500 dark:text-neutral-400\n\t\tselect-none\n\t";
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
export * from "./components/AlertConfirmPrompt/index.js";
|
|
25
25
|
export * from "./components/AnimatedElipsis/index.js";
|
|
26
26
|
export * from "./components/AppShell/index.js";
|
|
27
|
+
export * from "./components/AvatarInitials/index.js";
|
|
27
28
|
export * from "./components/Backdrop/index.js";
|
|
28
29
|
export * from "./components/Button/index.js";
|
|
29
30
|
export * from "./components/ButtonGroupRadio/index.js";
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
export * from "./components/AlertConfirmPrompt/index.js";
|
|
26
26
|
export * from "./components/AnimatedElipsis/index.js";
|
|
27
27
|
export * from "./components/AppShell/index.js";
|
|
28
|
+
export * from "./components/AvatarInitials/index.js";
|
|
28
29
|
export * from "./components/Backdrop/index.js";
|
|
29
30
|
export * from "./components/Button/index.js";
|
|
30
31
|
export * from "./components/ButtonGroupRadio/index.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates deterministic, harmonious pastel colors for avatar backgrounds.
|
|
3
|
+
*
|
|
4
|
+
* The colors are derived from a hash of the input string, ensuring the same
|
|
5
|
+
* input always produces the same color combination. The background is a pastel
|
|
6
|
+
* color (high lightness, moderate saturation) and the text is a darker shade
|
|
7
|
+
* of the same hue for visual harmony and good contrast.
|
|
8
|
+
*
|
|
9
|
+
* @param source - The string to hash for color generation (e.g., email, user ID)
|
|
10
|
+
* @returns Object with `bg` and `text` CSS color strings in HSL format
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* generateAvatarColors("user@example.com");
|
|
15
|
+
* // { bg: "hsl(142, 52%, 68%)", text: "hsl(142, 62%, 28%)" }
|
|
16
|
+
*
|
|
17
|
+
* generateAvatarColors("another@example.com");
|
|
18
|
+
* // { bg: "hsl(287, 48%, 71%)", text: "hsl(287, 58%, 28%)" }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function generateAvatarColors(source: string): {
|
|
22
|
+
bg: string;
|
|
23
|
+
text: string;
|
|
24
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { strHash } from "./str-hash.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generates deterministic, harmonious pastel colors for avatar backgrounds.
|
|
4
|
+
*
|
|
5
|
+
* The colors are derived from a hash of the input string, ensuring the same
|
|
6
|
+
* input always produces the same color combination. The background is a pastel
|
|
7
|
+
* color (high lightness, moderate saturation) and the text is a darker shade
|
|
8
|
+
* of the same hue for visual harmony and good contrast.
|
|
9
|
+
*
|
|
10
|
+
* @param source - The string to hash for color generation (e.g., email, user ID)
|
|
11
|
+
* @returns Object with `bg` and `text` CSS color strings in HSL format
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* generateAvatarColors("user@example.com");
|
|
16
|
+
* // { bg: "hsl(142, 52%, 68%)", text: "hsl(142, 62%, 28%)" }
|
|
17
|
+
*
|
|
18
|
+
* generateAvatarColors("another@example.com");
|
|
19
|
+
* // { bg: "hsl(287, 48%, 71%)", text: "hsl(287, 58%, 28%)" }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function generateAvatarColors(source) {
|
|
23
|
+
// Get hash as hex string and convert to number
|
|
24
|
+
const hashHex = strHash(source || "");
|
|
25
|
+
const hash = parseInt(hashHex, 16);
|
|
26
|
+
// Derive hue (0-360) from hash
|
|
27
|
+
const hue = hash % 360;
|
|
28
|
+
// Derive saturation and lightness variations from hash for slight variety
|
|
29
|
+
// Saturation: 40-55% (pastel range - soft, not too vivid)
|
|
30
|
+
const saturation = 40 + (hash % 15);
|
|
31
|
+
// Lightness: 75-82% (high lightness ensures dark text is always readable)
|
|
32
|
+
const lightness = 75 + (hash % 7);
|
|
33
|
+
const bg = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
|
34
|
+
// Text: same hue, more saturated, very dark for strong contrast
|
|
35
|
+
const text = `hsl(${hue}, ${saturation + 15}%, 22%)`;
|
|
36
|
+
return { bg, text };
|
|
37
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED