@marianmeres/stuic 2.18.0 → 2.19.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.
@@ -2,53 +2,87 @@
2
2
  export interface Props {
3
3
  class?: string;
4
4
  enabled?: boolean;
5
+ /** Animation cycle duration in ms (default: 1000) */
6
+ speed?: number;
5
7
  }
6
8
  </script>
7
9
 
8
10
  <script lang="ts">
9
- import { createTickerRAF } from "@marianmeres/ticker";
10
- import { onMount } from "svelte";
11
-
12
- let { class: _class, enabled = true }: Props = $props();
13
-
14
- const speed = 250;
15
- let visible = $state([false, false, false]);
16
- let i = $state(0);
17
-
18
- onMount(() => {
19
- const ticker = createTickerRAF(speed, true);
20
- const unsub = ticker.subscribe((t) => {
21
- if (i > visible.length - 1) {
22
- i = 0;
23
- visible = visible.map((v) => false);
24
- } else {
25
- visible[i] = true;
26
- i++;
27
- }
28
- });
29
- return () => {
30
- ticker.stop();
31
- unsub();
32
- };
33
- });
11
+ let { class: _class, enabled = true, speed = 1000 }: Props = $props();
34
12
  </script>
35
13
 
36
14
  <!-- prettier-ignore -->
37
- <span class={_class}>
38
- <span
39
- class={visible[0] || !enabled ? 'opacity-100' : 'opacity-0'}
40
- style="transition-duration: {speed}ms;"
41
- >.</span><span
42
- class={visible[1] || !enabled ? 'opacity-100' : 'opacity-0'}
43
- style="transition-duration: {speed}ms;"
44
- >.</span><span
45
- class={visible[2] || !enabled ? 'opacity-100' : 'opacity-0'}
46
- style="transition-duration: {speed}ms;"
47
- >.</span>
48
- </span>
15
+ <span class={_class} style:--duration="{speed}ms"
16
+ ><span class="dot dot1" class:paused={!enabled}>.</span
17
+ ><span class="dot dot2" class:paused={!enabled}>.</span
18
+ ><span class="dot dot3" class:paused={!enabled}>.</span
19
+ ></span>
49
20
 
50
21
  <style>
51
- span span {
52
- transition-property: opacity;
22
+ .dot {
23
+ opacity: 0;
24
+ }
25
+
26
+ .dot1 {
27
+ animation: dot1 var(--duration, 1s) infinite linear;
28
+ }
29
+ .dot2 {
30
+ animation: dot2 var(--duration, 1s) infinite linear;
31
+ }
32
+ .dot3 {
33
+ animation: dot3 var(--duration, 1s) infinite linear;
34
+ }
35
+
36
+ .dot.paused {
37
+ animation: none;
38
+ opacity: 1;
39
+ }
40
+
41
+ @keyframes dot1 {
42
+ 0%,
43
+ 20% {
44
+ opacity: 0;
45
+ }
46
+ 25% {
47
+ opacity: 1;
48
+ }
49
+ 80% {
50
+ opacity: 1;
51
+ }
52
+ 100% {
53
+ opacity: 0;
54
+ }
55
+ }
56
+
57
+ @keyframes dot2 {
58
+ 0%,
59
+ 45% {
60
+ opacity: 0;
61
+ }
62
+ 50% {
63
+ opacity: 1;
64
+ }
65
+ 80% {
66
+ opacity: 1;
67
+ }
68
+ 100% {
69
+ opacity: 0;
70
+ }
71
+ }
72
+
73
+ @keyframes dot3 {
74
+ 0%,
75
+ 70% {
76
+ opacity: 0;
77
+ }
78
+ 75% {
79
+ opacity: 1;
80
+ }
81
+ 80% {
82
+ opacity: 1;
83
+ }
84
+ 100% {
85
+ opacity: 0;
86
+ }
53
87
  }
54
88
  </style>
@@ -1,6 +1,8 @@
1
1
  export interface Props {
2
2
  class?: string;
3
3
  enabled?: boolean;
4
+ /** Animation cycle duration in ms (default: 1000) */
5
+ speed?: number;
4
6
  }
5
7
  declare const AnimatedEllipsis: import("svelte").Component<Props, {}, "">;
6
8
  type AnimatedEllipsis = ReturnType<typeof AnimatedEllipsis>;
@@ -0,0 +1,169 @@
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
+ ```
@@ -0,0 +1,315 @@
1
+ # DropdownMenu
2
+
3
+ A feature-rich dropdown menu component with CSS Anchor Positioning (with fallback), full keyboard navigation, and support for multiple item types including expandable sections.
4
+
5
+ ## Props
6
+
7
+ | Prop | Type | Default | Description |
8
+ |------|------|---------|-------------|
9
+ | `items` | `DropdownMenuItem[]` | - | Menu items to display |
10
+ | `isOpen` | `boolean` | `false` | Controlled open state (bindable) |
11
+ | `position` | `DropdownMenuPosition` | `"bottom-span-left"` | Popover position relative to trigger |
12
+ | `offset` | `string` | `"0.25rem"` | Offset from trigger element (CSS value) |
13
+ | `maxHeight` | `string` | `"300px"` | Max height of dropdown |
14
+ | `closeOnSelect` | `boolean` | `true` | Close menu when action item is selected |
15
+ | `closeOnClickOutside` | `boolean` | `true` | Close on click outside |
16
+ | `closeOnEscape` | `boolean` | `true` | Close on Escape key |
17
+ | `forceFallback` | `boolean` | `false` | Force fallback positioning (for testing) |
18
+ | `class` | `string` | - | Classes for wrapper element |
19
+ | `classTrigger` | `string` | - | Classes for trigger button |
20
+ | `classDropdown` | `string` | - | Classes for dropdown container |
21
+ | `classItem` | `string` | - | Classes for action items |
22
+ | `classItemActive` | `string` | - | Classes for active/focused item |
23
+ | `classItemDisabled` | `string` | - | Classes for disabled items |
24
+ | `classDivider` | `string` | - | Classes for dividers |
25
+ | `classHeader` | `string` | - | Classes for header items |
26
+ | `classExpandable` | `string` | - | Classes for expandable section header |
27
+ | `classExpandableContent` | `string` | - | Classes for expandable section content |
28
+ | `triggerEl` | `HTMLButtonElement` | - | Trigger element reference (bindable) |
29
+ | `dropdownEl` | `HTMLDivElement` | - | Dropdown element reference (bindable) |
30
+
31
+ ## Snippets
32
+
33
+ | Snippet | Parameters | Description |
34
+ |---------|------------|-------------|
35
+ | `trigger` | `{ isOpen, toggle, triggerProps }` | Custom trigger with full ARIA control |
36
+ | `children` | - | Simple content for default trigger button |
37
+
38
+ ## Item Types
39
+
40
+ ### Action Item
41
+
42
+ Clickable menu item with optional icon and shortcut.
43
+
44
+ ```typescript
45
+ interface DropdownMenuActionItem {
46
+ type: "action";
47
+ id: string | number;
48
+ label: THC; // Text, HTML, or component
49
+ icon?: THC; // Optional leading icon
50
+ shortcut?: string; // Keyboard shortcut hint
51
+ disabled?: boolean;
52
+ onSelect?: () => void | boolean;
53
+ class?: string;
54
+ data?: Record<string, any>;
55
+ }
56
+ ```
57
+
58
+ ### Divider Item
59
+
60
+ Visual separator between items.
61
+
62
+ ```typescript
63
+ interface DropdownMenuDividerItem {
64
+ type: "divider";
65
+ id?: string | number;
66
+ class?: string;
67
+ }
68
+ ```
69
+
70
+ ### Header Item
71
+
72
+ Non-interactive section header.
73
+
74
+ ```typescript
75
+ interface DropdownMenuHeaderItem {
76
+ type: "header";
77
+ id?: string | number;
78
+ label: THC;
79
+ class?: string;
80
+ }
81
+ ```
82
+
83
+ ### Custom Item
84
+
85
+ Render arbitrary content (non-interactive).
86
+
87
+ ```typescript
88
+ interface DropdownMenuCustomItem {
89
+ type: "custom";
90
+ id?: string | number;
91
+ content: THC;
92
+ class?: string;
93
+ }
94
+ ```
95
+
96
+ ### Expandable Item
97
+
98
+ Collapsible section containing nested items.
99
+
100
+ ```typescript
101
+ interface DropdownMenuExpandableItem {
102
+ type: "expandable";
103
+ id: string | number;
104
+ label: THC;
105
+ icon?: THC;
106
+ items: DropdownMenuFlatItem[]; // Nested items (no nested expandables)
107
+ defaultExpanded?: boolean;
108
+ disabled?: boolean;
109
+ class?: string;
110
+ }
111
+ ```
112
+
113
+ ## Position Options
114
+
115
+ | Position | Description |
116
+ |----------|-------------|
117
+ | `top`, `bottom` | Centered above/below trigger |
118
+ | `top-left`, `top-right` | Above, aligned to left/right edge |
119
+ | `bottom-left`, `bottom-right` | Below, aligned to left/right edge |
120
+ | `top-span-left`, `top-span-right` | Above, spanning from left/right |
121
+ | `bottom-span-left`, `bottom-span-right` | Below, spanning from left/right |
122
+ | `left`, `right` | Side-by-side with trigger |
123
+
124
+ ## Callbacks
125
+
126
+ | Callback | Parameters | Description |
127
+ |----------|------------|-------------|
128
+ | `onOpen` | - | Called when menu opens |
129
+ | `onClose` | - | Called when menu closes |
130
+ | `onSelect` | `(item: DropdownMenuActionItem)` | Called when action item selected (fallback) |
131
+
132
+ ## Keyboard Navigation
133
+
134
+ | Key | Action |
135
+ |-----|--------|
136
+ | `Arrow Down` | Move to next item |
137
+ | `Arrow Up` | Move to previous item |
138
+ | `Home` | Move to first item |
139
+ | `End` | Move to last item |
140
+ | `Cmd/Ctrl + Arrow` | Jump to first/last |
141
+ | `Enter` / `Space` | Select item or toggle expandable |
142
+ | `Arrow Right` | Expand section (on expandable) |
143
+ | `Arrow Left` | Collapse section (on expandable) |
144
+ | `Escape` | Close menu |
145
+ | `Tab` | Close menu |
146
+
147
+ ## Usage
148
+
149
+ ### Basic Menu
150
+
151
+ ```svelte
152
+ <script lang="ts">
153
+ import { DropdownMenu } from 'stuic';
154
+
155
+ const items = [
156
+ { type: "action", id: "edit", label: "Edit" },
157
+ { type: "action", id: "duplicate", label: "Duplicate" },
158
+ { type: "divider" },
159
+ { type: "action", id: "delete", label: "Delete", class: "text-red-500" },
160
+ ];
161
+ </script>
162
+
163
+ <DropdownMenu
164
+ {items}
165
+ onSelect={(item) => console.log('Selected:', item.id)}
166
+ >
167
+ Actions
168
+ </DropdownMenu>
169
+ ```
170
+
171
+ ### With Icons and Shortcuts
172
+
173
+ ```svelte
174
+ <script lang="ts">
175
+ import { DropdownMenu } from 'stuic';
176
+ import { iconLucideEdit } from '@marianmeres/icons-fns/lucide/iconLucideEdit.js';
177
+ import { iconLucideTrash } from '@marianmeres/icons-fns/lucide/iconLucideTrash.js';
178
+
179
+ const items = [
180
+ {
181
+ type: "action",
182
+ id: "edit",
183
+ label: "Edit",
184
+ icon: iconLucideEdit({ size: 16 }),
185
+ shortcut: "Cmd+E",
186
+ onSelect: () => handleEdit(),
187
+ },
188
+ { type: "divider" },
189
+ {
190
+ type: "action",
191
+ id: "delete",
192
+ label: "Delete",
193
+ icon: iconLucideTrash({ size: 16 }),
194
+ shortcut: "Cmd+D",
195
+ onSelect: () => handleDelete(),
196
+ },
197
+ ];
198
+ </script>
199
+
200
+ <DropdownMenu {items} position="bottom-right">
201
+ More Options
202
+ </DropdownMenu>
203
+ ```
204
+
205
+ ### With Section Headers
206
+
207
+ ```svelte
208
+ <DropdownMenu items={[
209
+ { type: "header", label: "Navigation" },
210
+ { type: "action", id: "dashboard", label: "Dashboard" },
211
+ { type: "action", id: "settings", label: "Settings" },
212
+ { type: "divider" },
213
+ { type: "header", label: "Account" },
214
+ { type: "action", id: "profile", label: "Profile" },
215
+ { type: "action", id: "logout", label: "Logout" },
216
+ ]} />
217
+ ```
218
+
219
+ ### Expandable Sections
220
+
221
+ ```svelte
222
+ <DropdownMenu items={[
223
+ { type: "action", id: "new", label: "New File" },
224
+ {
225
+ type: "expandable",
226
+ id: "recent",
227
+ label: "Recent Files",
228
+ defaultExpanded: true,
229
+ items: [
230
+ { type: "action", id: "file1", label: "document.pdf" },
231
+ { type: "action", id: "file2", label: "report.xlsx" },
232
+ { type: "action", id: "file3", label: "notes.txt" },
233
+ ],
234
+ },
235
+ { type: "divider" },
236
+ { type: "action", id: "settings", label: "Settings" },
237
+ ]} />
238
+ ```
239
+
240
+ ### Custom Trigger
241
+
242
+ ```svelte
243
+ <script lang="ts">
244
+ import { AvatarInitials, DropdownMenu } from 'stuic';
245
+ </script>
246
+
247
+ <DropdownMenu
248
+ items={[
249
+ { type: "action", id: "profile", label: "View Profile" },
250
+ { type: "action", id: "logout", label: "Logout" },
251
+ ]}
252
+ >
253
+ {#snippet trigger({ isOpen, toggle, triggerProps })}
254
+ <button {...triggerProps} onclick={toggle}>
255
+ <AvatarInitials input="john.doe@example.com" autoColor />
256
+ </button>
257
+ {/snippet}
258
+ </DropdownMenu>
259
+ ```
260
+
261
+ ### With Custom Content
262
+
263
+ ```svelte
264
+ <script lang="ts">
265
+ import { DropdownMenu } from 'stuic';
266
+ </script>
267
+
268
+ <DropdownMenu items={[
269
+ {
270
+ type: "custom",
271
+ content: customHeader,
272
+ },
273
+ { type: "divider" },
274
+ { type: "action", id: "settings", label: "Settings" },
275
+ { type: "action", id: "logout", label: "Logout" },
276
+ ]} />
277
+
278
+ {#snippet customHeader()}
279
+ <div class="px-3 py-2 text-center">
280
+ <img src="/avatar.jpg" class="w-12 h-12 rounded-full mx-auto" alt="User" />
281
+ <div class="mt-2 font-semibold">John Doe</div>
282
+ <div class="text-sm text-gray-500">john@example.com</div>
283
+ </div>
284
+ {/snippet}
285
+ ```
286
+
287
+ ### Controlled State
288
+
289
+ ```svelte
290
+ <script lang="ts">
291
+ import { DropdownMenu } from 'stuic';
292
+
293
+ let isOpen = $state(false);
294
+ </script>
295
+
296
+ <button onclick={() => isOpen = true}>Open Menu</button>
297
+
298
+ <DropdownMenu
299
+ bind:isOpen
300
+ items={[
301
+ { type: "action", id: "option1", label: "Option 1" },
302
+ { type: "action", id: "option2", label: "Option 2" },
303
+ ]}
304
+ />
305
+ ```
306
+
307
+ ## Features
308
+
309
+ - **CSS Anchor Positioning**: Uses modern CSS anchor positioning with automatic fallback for unsupported browsers
310
+ - **Full Keyboard Navigation**: Complete arrow key navigation with Home/End support
311
+ - **Expandable Sections**: Collapsible groups with independent toggle state
312
+ - **ARIA Compliant**: Proper menu roles and keyboard interaction
313
+ - **Reduced Motion**: Respects user's reduced motion preference
314
+ - **Click Outside**: Automatically closes when clicking outside
315
+ - **Focus Management**: Returns focus to trigger on close
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "2.18.0",
3
+ "version": "2.19.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",