@joewinke/jatui 0.1.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 (62) hide show
  1. package/package.json +46 -0
  2. package/src/lib/components/AudioWaveform.svelte +694 -0
  3. package/src/lib/components/AvailabilityModal.svelte +173 -0
  4. package/src/lib/components/Badge.svelte +38 -0
  5. package/src/lib/components/BookingForm.svelte +276 -0
  6. package/src/lib/components/Button.svelte +72 -0
  7. package/src/lib/components/CalendarPicker.svelte +284 -0
  8. package/src/lib/components/Card.svelte +67 -0
  9. package/src/lib/components/CharacterCounter.svelte +82 -0
  10. package/src/lib/components/ChipInput.svelte +596 -0
  11. package/src/lib/components/ColorSelector.svelte +163 -0
  12. package/src/lib/components/ConfirmModal.svelte +75 -0
  13. package/src/lib/components/CountdownTimer.svelte +94 -0
  14. package/src/lib/components/DateRangePicker.svelte +192 -0
  15. package/src/lib/components/Drawer.svelte +110 -0
  16. package/src/lib/components/FilterDropdown.svelte +202 -0
  17. package/src/lib/components/ImageUpload.svelte +97 -0
  18. package/src/lib/components/InlineEdit.svelte +283 -0
  19. package/src/lib/components/LazyImage.svelte +122 -0
  20. package/src/lib/components/LoadingSpinner.svelte +102 -0
  21. package/src/lib/components/Modal.svelte +208 -0
  22. package/src/lib/components/PhoneInput.svelte +92 -0
  23. package/src/lib/components/ResizableDivider.svelte +305 -0
  24. package/src/lib/components/ResizablePanel.svelte +302 -0
  25. package/src/lib/components/SearchDropdown.svelte +341 -0
  26. package/src/lib/components/SelectInput.svelte +215 -0
  27. package/src/lib/components/SignaturePad.svelte +171 -0
  28. package/src/lib/components/SortDropdown.svelte +148 -0
  29. package/src/lib/components/Sparkline.svelte +107 -0
  30. package/src/lib/components/SpeechForm.svelte +114 -0
  31. package/src/lib/components/StatusBadge.svelte +155 -0
  32. package/src/lib/components/TextArea.svelte +143 -0
  33. package/src/lib/components/TextInput.svelte +108 -0
  34. package/src/lib/components/ThemeSelector.svelte +195 -0
  35. package/src/lib/components/TimeSlotPicker.svelte +162 -0
  36. package/src/lib/components/VoicePlayer.svelte +420 -0
  37. package/src/lib/components/messaging/Avatar.svelte +81 -0
  38. package/src/lib/components/messaging/ChannelInfoModal.svelte +163 -0
  39. package/src/lib/components/messaging/ChannelList.svelte +107 -0
  40. package/src/lib/components/messaging/ChannelMemberAvatarStack.svelte +69 -0
  41. package/src/lib/components/messaging/ChannelMembersModal.svelte +182 -0
  42. package/src/lib/components/messaging/CreateChannelModal.svelte +190 -0
  43. package/src/lib/components/messaging/DirectMessageList.svelte +145 -0
  44. package/src/lib/components/messaging/EmojiSelector.svelte +260 -0
  45. package/src/lib/components/messaging/MentionAutocomplete.svelte +193 -0
  46. package/src/lib/components/messaging/MessageAttachment.svelte +270 -0
  47. package/src/lib/components/messaging/MessageAttachmentUpload.svelte +243 -0
  48. package/src/lib/components/messaging/MessageInput.svelte +451 -0
  49. package/src/lib/components/messaging/MessageItem.svelte +338 -0
  50. package/src/lib/components/messaging/MessageThread.svelte +306 -0
  51. package/src/lib/components/messaging/NotificationSettingsModal.svelte +234 -0
  52. package/src/lib/components/messaging/QuotedMessageDisplay.svelte +118 -0
  53. package/src/lib/components/messaging/StartDMModal.svelte +100 -0
  54. package/src/lib/components/messaging/ThreadPanel.svelte +153 -0
  55. package/src/lib/index.ts +185 -0
  56. package/src/lib/types/booking.ts +143 -0
  57. package/src/lib/types/messaging.ts +459 -0
  58. package/src/lib/utils/currency.ts +20 -0
  59. package/src/lib/utils/daisyuiColors.ts +243 -0
  60. package/src/lib/utils/dateFormatters.ts +153 -0
  61. package/src/lib/utils/mentionParser.ts +188 -0
  62. package/src/lib/utils/phoneFormat.ts +74 -0
@@ -0,0 +1,102 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LoadingSpinner Component
4
+ *
5
+ * Customizable DaisyUI loading spinner with text, overlay, and speed options.
6
+ */
7
+
8
+ interface Props {
9
+ type?: 'spinner' | 'dots' | 'ring' | 'ball' | 'bars' | 'infinity' | 'pulse';
10
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
11
+ text?: string;
12
+ textPosition?: 'top' | 'bottom' | 'left' | 'right';
13
+ overlay?: boolean;
14
+ centered?: boolean;
15
+ color?: string;
16
+ ariaLabel?: string;
17
+ visible?: boolean;
18
+ speed?: 'slow' | 'normal' | 'fast';
19
+ class?: string;
20
+ }
21
+
22
+ let {
23
+ type = 'spinner',
24
+ size = 'md',
25
+ text = '',
26
+ textPosition = 'bottom',
27
+ overlay = false,
28
+ centered = false,
29
+ color = '',
30
+ ariaLabel = 'Loading',
31
+ visible = true,
32
+ speed = 'normal',
33
+ class: className = ''
34
+ }: Props = $props();
35
+
36
+ const sizeClass = $derived(
37
+ { xs: 'loading-xs', sm: 'loading-sm', md: 'loading-md', lg: 'loading-lg', xl: 'loading-xl' }[size]
38
+ );
39
+
40
+ const typeClass = $derived(
41
+ {
42
+ spinner: 'loading-spinner',
43
+ dots: 'loading-dots',
44
+ ring: 'loading-ring',
45
+ ball: 'loading-ball',
46
+ bars: 'loading-bars',
47
+ infinity: 'loading-infinity',
48
+ pulse: 'loading-pulse'
49
+ }[type]
50
+ );
51
+
52
+ const speedClass = $derived(
53
+ { slow: '[animation-duration:2s]', normal: '', fast: '[animation-duration:0.5s]' }[speed]
54
+ );
55
+
56
+ const spinnerClass = $derived(
57
+ ['loading', typeClass, sizeClass, speedClass, color ? '' : 'text-primary', className]
58
+ .filter(Boolean)
59
+ .join(' ')
60
+ );
61
+
62
+ const containerClass = $derived(
63
+ [
64
+ overlay ? 'fixed inset-0 bg-black/50 flex items-center justify-center z-50' : '',
65
+ centered && !overlay ? 'flex items-center justify-center' : '',
66
+ textPosition === 'left' || textPosition === 'right' ? 'flex items-center' : '',
67
+ textPosition === 'top' || textPosition === 'bottom' ? 'flex flex-col items-center' : ''
68
+ ]
69
+ .filter(Boolean)
70
+ .join(' ')
71
+ );
72
+
73
+ const textClass = $derived(
74
+ ['text-sm text-base-content/70', { top: 'mb-2', bottom: 'mt-2', left: 'mr-2', right: 'ml-2' }[textPosition]]
75
+ .filter(Boolean)
76
+ .join(' ')
77
+ );
78
+
79
+ const spinnerStyle = $derived(color ? `color: ${color}` : '');
80
+ </script>
81
+
82
+ {#if visible}
83
+ <div class={containerClass} aria-label={ariaLabel} role="status" aria-live="polite">
84
+ {#if text && textPosition === 'top'}
85
+ <div class={textClass}>{text}</div>
86
+ {/if}
87
+ {#if text && textPosition === 'left'}
88
+ <div class={textClass}>{text}</div>
89
+ {/if}
90
+
91
+ <div class={spinnerClass} style={spinnerStyle}></div>
92
+
93
+ {#if text && textPosition === 'right'}
94
+ <div class={textClass}>{text}</div>
95
+ {/if}
96
+ {#if text && textPosition === 'bottom'}
97
+ <div class={textClass}>{text}</div>
98
+ {/if}
99
+
100
+ <span class="sr-only">{ariaLabel}</span>
101
+ </div>
102
+ {/if}
@@ -0,0 +1,208 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Modal Component
4
+ *
5
+ * Overlay dialog with focus trap, backdrop click, escape key, and body scroll lock.
6
+ * Converted from Svelte 4 to Svelte 5 runes.
7
+ */
8
+
9
+ import { onMount } from 'svelte';
10
+
11
+ interface Props {
12
+ open?: boolean;
13
+ title?: string;
14
+ size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
15
+ showCloseButton?: boolean;
16
+ closeOnBackdrop?: boolean;
17
+ closeOnEscape?: boolean;
18
+ showHeader?: boolean;
19
+ showFooter?: boolean;
20
+ class?: string;
21
+ contentClass?: string;
22
+ ariaLabel?: string;
23
+ dismissible?: boolean;
24
+ zIndex?: number;
25
+ onclose?: () => void;
26
+ onopen?: () => void;
27
+ header?: import('svelte').Snippet;
28
+ footer?: import('svelte').Snippet;
29
+ children?: import('svelte').Snippet;
30
+ }
31
+
32
+ let {
33
+ open = $bindable(false),
34
+ title = '',
35
+ size = 'md',
36
+ showCloseButton = true,
37
+ closeOnBackdrop = true,
38
+ closeOnEscape = true,
39
+ showHeader = true,
40
+ showFooter = false,
41
+ class: className = '',
42
+ contentClass = '',
43
+ ariaLabel = '',
44
+ dismissible = true,
45
+ zIndex = 1000,
46
+ onclose,
47
+ onopen,
48
+ header,
49
+ footer,
50
+ children
51
+ }: Props = $props();
52
+
53
+ let modalElement: HTMLDialogElement | undefined = $state();
54
+ let previousFocus: HTMLElement | null = null;
55
+
56
+ const sizeClass = $derived(
57
+ { sm: 'max-w-sm', md: 'max-w-md', lg: 'max-w-lg', xl: 'max-w-xl', full: 'max-w-full h-full' }[size]
58
+ );
59
+
60
+ const modalClass = $derived(['modal', open ? 'modal-open' : '', className].filter(Boolean).join(' '));
61
+
62
+ const contentClasses = $derived(
63
+ [
64
+ 'modal-box relative bg-base-100 text-base-content',
65
+ sizeClass,
66
+ size === 'full' ? 'h-full rounded-none' : '',
67
+ contentClass
68
+ ]
69
+ .filter(Boolean)
70
+ .join(' ')
71
+ );
72
+
73
+ $effect(() => {
74
+ if (typeof document === 'undefined') return;
75
+ if (open) {
76
+ previousFocus = document.activeElement as HTMLElement;
77
+ document.body.style.overflow = 'hidden';
78
+ requestAnimationFrame(() => modalElement?.focus());
79
+ onopen?.();
80
+ } else {
81
+ document.body.style.overflow = '';
82
+ previousFocus?.focus();
83
+ previousFocus = null;
84
+ }
85
+ });
86
+
87
+ function close() {
88
+ if (dismissible) {
89
+ open = false;
90
+ onclose?.();
91
+ }
92
+ }
93
+
94
+ function handleBackdropClick(event: Event) {
95
+ if (closeOnBackdrop && event.target === event.currentTarget) {
96
+ close();
97
+ }
98
+ }
99
+
100
+ function handleKeyDown(event: KeyboardEvent) {
101
+ if (event.key === 'Tab') {
102
+ trapFocus(event);
103
+ }
104
+ }
105
+
106
+ function handleWindowKeyDown(event: KeyboardEvent) {
107
+ if (open && event.key === 'Escape' && closeOnEscape) {
108
+ event.preventDefault();
109
+ event.stopImmediatePropagation();
110
+ close();
111
+ }
112
+ }
113
+
114
+ function trapFocus(event: KeyboardEvent) {
115
+ if (!modalElement) return;
116
+ const focusableElements = modalElement.querySelectorAll(
117
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
118
+ );
119
+ const firstFocusable = focusableElements[0] as HTMLElement;
120
+ const lastFocusable = focusableElements[focusableElements.length - 1] as HTMLElement;
121
+
122
+ if (event.shiftKey) {
123
+ if (document.activeElement === firstFocusable) {
124
+ event.preventDefault();
125
+ lastFocusable.focus();
126
+ }
127
+ } else {
128
+ if (document.activeElement === lastFocusable) {
129
+ event.preventDefault();
130
+ firstFocusable.focus();
131
+ }
132
+ }
133
+ }
134
+
135
+ onMount(() => {
136
+ return () => {
137
+ if (typeof document !== 'undefined') {
138
+ document.body.style.overflow = '';
139
+ }
140
+ };
141
+ });
142
+ </script>
143
+
144
+ <svelte:window onkeydown={handleWindowKeyDown} />
145
+
146
+ {#if open}
147
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
148
+ <dialog
149
+ bind:this={modalElement}
150
+ class={modalClass}
151
+ style="z-index: {zIndex}"
152
+ aria-modal="true"
153
+ aria-label={ariaLabel || title}
154
+ open
155
+ onkeydown={handleKeyDown}
156
+ tabindex="-1"
157
+ >
158
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
159
+ <div class="modal-backdrop" onclick={handleBackdropClick}>
160
+ <div class={contentClasses}>
161
+ {#if showHeader && (title || showCloseButton || header)}
162
+ <div class="flex items-center justify-between mb-4">
163
+ {#if header}
164
+ {@render header()}
165
+ {:else if title}
166
+ <h2 class="text-lg font-semibold">{title}</h2>
167
+ {/if}
168
+
169
+ {#if showCloseButton && dismissible}
170
+ <button type="button" class="btn btn-sm btn-circle btn-primary" onclick={close} aria-label="Close modal">
171
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
172
+ <path
173
+ stroke-linecap="round"
174
+ stroke-linejoin="round"
175
+ stroke-width="2"
176
+ d="M6 18L18 6M6 6l12 12"
177
+ />
178
+ </svg>
179
+ </button>
180
+ {/if}
181
+ </div>
182
+ {/if}
183
+
184
+ <div class="modal-content">
185
+ {#if children}
186
+ {@render children()}
187
+ {/if}
188
+ </div>
189
+
190
+ {#if showFooter && footer}
191
+ <div class="modal-action mt-6">
192
+ {@render footer()}
193
+ </div>
194
+ {/if}
195
+ </div>
196
+ </div>
197
+ </dialog>
198
+ {/if}
199
+
200
+ <style>
201
+ .modal-backdrop {
202
+ position: absolute;
203
+ inset: 0;
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ }
208
+ </style>
@@ -0,0 +1,92 @@
1
+ <script lang="ts">
2
+ /**
3
+ * PhoneInput Component
4
+ *
5
+ * Tel input with basic validation and format indicator.
6
+ * Converted from Svelte 4 to Svelte 5 runes.
7
+ */
8
+
9
+ interface Props {
10
+ label: string;
11
+ value?: string;
12
+ required?: boolean;
13
+ disabled?: boolean;
14
+ error?: string;
15
+ helpText?: string;
16
+ placeholder?: string;
17
+ class?: string;
18
+ id?: string;
19
+ name?: string;
20
+ oninput?: (value: string, isValid: boolean) => void;
21
+ }
22
+
23
+ let {
24
+ label,
25
+ value = $bindable(''),
26
+ required = false,
27
+ disabled = false,
28
+ error = '',
29
+ helpText = '',
30
+ placeholder = '',
31
+ class: className = '',
32
+ id = `phone-input-${Math.random().toString(36).substr(2, 9)}`,
33
+ name = '',
34
+ oninput
35
+ }: Props = $props();
36
+
37
+ const displayPlaceholder = $derived(placeholder || '(555) 123-4567');
38
+ const isValid = $derived(
39
+ /^[\d\s\-\(\)\.+]{10,}$/.test(value) && value.replace(/\D/g, '').length === 10
40
+ );
41
+
42
+ function handleInput(event: Event) {
43
+ const target = event.target as HTMLInputElement;
44
+ value = target.value;
45
+ oninput?.(value, isValid);
46
+ }
47
+ </script>
48
+
49
+ <div class="form-control w-full">
50
+ <label for={id} class="label">
51
+ <span class="label-text font-medium">
52
+ {label}
53
+ {#if required}
54
+ <span class="text-error ml-1">*</span>
55
+ {/if}
56
+ </span>
57
+ </label>
58
+
59
+ <input
60
+ {id}
61
+ {name}
62
+ type="tel"
63
+ {required}
64
+ {disabled}
65
+ placeholder={displayPlaceholder}
66
+ {value}
67
+ oninput={handleInput}
68
+ pattern="[0-9\s\-\(\)\.+]*"
69
+ class="input input-bordered w-full {error ? 'input-error' : ''} {className}"
70
+ aria-describedby={error ? `${id}-error` : helpText ? `${id}-help` : undefined}
71
+ aria-invalid={error ? 'true' : 'false'}
72
+ autocomplete="tel"
73
+ />
74
+
75
+ {#if error}
76
+ <div class="label">
77
+ <span id="{id}-error" class="label-text-alt text-error" role="alert">{error}</span>
78
+ </div>
79
+ {:else if helpText}
80
+ <div class="label">
81
+ <span id="{id}-help" class="label-text-alt text-base-content/70">{helpText}</span>
82
+ </div>
83
+ {/if}
84
+
85
+ {#if value && !error}
86
+ <div class="label">
87
+ <span class="label-text-alt {isValid ? 'text-success' : 'text-warning'}">
88
+ {isValid ? '✓ Valid phone number' : '⚠ Invalid phone number'}
89
+ </span>
90
+ </div>
91
+ {/if}
92
+ </div>
@@ -0,0 +1,305 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ResizableDivider Component
4
+ * A draggable divider for resizing split panels.
5
+ *
6
+ * Features:
7
+ * - Horizontal drag handle with grippy visual
8
+ * - Mouse and touch support
9
+ * - Emits percentage changes via callback
10
+ * - Visual feedback on hover/drag
11
+ * - Snap-to-collapse support with click-to-restore
12
+ * - Proximity detection: grows larger when mouse approaches
13
+ * - Glow effect on hover for better visibility
14
+ */
15
+
16
+ interface Props {
17
+ onResize: (deltaY: number) => void;
18
+ class?: string;
19
+ /** Whether a panel is currently collapsed */
20
+ isCollapsed?: boolean;
21
+ /** Which direction the panel collapsed ('top' = top panel hidden, 'bottom' = bottom panel hidden) */
22
+ collapsedDirection?: 'top' | 'bottom' | null;
23
+ /** Callback when clicking the divider while collapsed (to restore) */
24
+ onCollapsedClick?: () => void;
25
+ }
26
+
27
+ let {
28
+ onResize,
29
+ class: className = '',
30
+ isCollapsed = false,
31
+ collapsedDirection = null,
32
+ onCollapsedClick
33
+ }: Props = $props();
34
+
35
+ let isDragging = $state(false);
36
+ let isNearby = $state(false);
37
+ let isHovering = $state(false);
38
+ let startY = $state(0);
39
+ let dividerElement: HTMLDivElement | undefined = $state();
40
+
41
+ // Proximity detection threshold in pixels
42
+ const PROXIMITY_THRESHOLD = 24;
43
+
44
+ function handleMouseDown(e: MouseEvent) {
45
+ if (isCollapsed && onCollapsedClick) {
46
+ onCollapsedClick();
47
+ return;
48
+ }
49
+
50
+ e.preventDefault();
51
+ isDragging = true;
52
+ startY = e.clientY;
53
+
54
+ document.addEventListener('mousemove', handleMouseMove);
55
+ document.addEventListener('mouseup', handleMouseUp);
56
+ }
57
+
58
+ function handleMouseMove(e: MouseEvent) {
59
+ if (!isDragging) return;
60
+ const deltaY = e.clientY - startY;
61
+ startY = e.clientY;
62
+ onResize(deltaY);
63
+ }
64
+
65
+ function handleMouseUp() {
66
+ isDragging = false;
67
+ document.removeEventListener('mousemove', handleMouseMove);
68
+ document.removeEventListener('mouseup', handleMouseUp);
69
+ }
70
+
71
+ function handleTouchStart(e: TouchEvent) {
72
+ if (isCollapsed && onCollapsedClick) {
73
+ onCollapsedClick();
74
+ return;
75
+ }
76
+
77
+ if (e.touches.length !== 1) return;
78
+ isDragging = true;
79
+ startY = e.touches[0].clientY;
80
+ }
81
+
82
+ function handleTouchMove(e: TouchEvent) {
83
+ if (!isDragging || e.touches.length !== 1) return;
84
+ const deltaY = e.touches[0].clientY - startY;
85
+ startY = e.touches[0].clientY;
86
+ onResize(deltaY);
87
+ }
88
+
89
+ function handleTouchEnd() {
90
+ isDragging = false;
91
+ }
92
+
93
+ function handleMouseEnter() {
94
+ isHovering = true;
95
+ }
96
+
97
+ function handleMouseLeave() {
98
+ isHovering = false;
99
+ }
100
+
101
+ function handleProximityMove(e: MouseEvent) {
102
+ if (!dividerElement || isDragging) return;
103
+
104
+ const rect = dividerElement.getBoundingClientRect();
105
+ const dividerCenterY = rect.top + rect.height / 2;
106
+ const distance = Math.abs(e.clientY - dividerCenterY);
107
+
108
+ isNearby = distance <= PROXIMITY_THRESHOLD;
109
+ }
110
+
111
+ $effect(() => {
112
+ if (typeof window === 'undefined') return;
113
+
114
+ document.addEventListener('mousemove', handleProximityMove);
115
+
116
+ return () => {
117
+ document.removeEventListener('mousemove', handleProximityMove);
118
+ };
119
+ });
120
+
121
+ const isExpanded = $derived(isDragging || isHovering || isNearby);
122
+ </script>
123
+
124
+ <div
125
+ bind:this={dividerElement}
126
+ class="divider-container flex items-center justify-center select-none {className} {isCollapsed ? 'cursor-pointer divider-collapsed' : 'cursor-row-resize'} {isDragging ? 'bg-primary/25' : isExpanded && isCollapsed ? 'bg-primary/40' : isExpanded ? 'bg-primary/15' : ''}"
127
+ class:expanded={isExpanded}
128
+ class:dragging={isDragging}
129
+ role="separator"
130
+ aria-orientation="horizontal"
131
+ aria-expanded={!isCollapsed}
132
+ tabindex="0"
133
+ onmousedown={handleMouseDown}
134
+ onmouseenter={handleMouseEnter}
135
+ onmouseleave={handleMouseLeave}
136
+ ontouchstart={handleTouchStart}
137
+ ontouchmove={handleTouchMove}
138
+ ontouchend={handleTouchEnd}
139
+ >
140
+ {#if isCollapsed}
141
+ <div class="collapsed-indicator">
142
+ <div class="indicator-line"></div>
143
+ <div class="indicator-dots">
144
+ <span></span>
145
+ <span></span>
146
+ <span></span>
147
+ </div>
148
+ <div class="indicator-line"></div>
149
+ </div>
150
+ {:else}
151
+ <div class="grippy-handle">
152
+ <div class="grip-line"></div>
153
+ <div class="grip-line"></div>
154
+ </div>
155
+ {/if}
156
+ </div>
157
+
158
+ <style>
159
+ .divider-container {
160
+ height: 8px;
161
+ min-height: 8px;
162
+ transition:
163
+ height 200ms cubic-bezier(0.4, 0, 0.2, 1),
164
+ background 200ms ease,
165
+ box-shadow 200ms ease;
166
+ }
167
+
168
+ .divider-container.expanded {
169
+ height: 16px;
170
+ min-height: 16px;
171
+ box-shadow:
172
+ 0 0 12px color-mix(in oklch, var(--color-primary) 40%, transparent),
173
+ inset 0 0 8px color-mix(in oklch, var(--color-primary) 20%, transparent);
174
+ }
175
+
176
+ .divider-container.dragging {
177
+ height: 16px;
178
+ min-height: 16px;
179
+ box-shadow:
180
+ 0 0 20px color-mix(in oklch, var(--color-primary) 50%, transparent),
181
+ inset 0 0 12px color-mix(in oklch, var(--color-primary) 30%, transparent);
182
+ }
183
+
184
+ .grippy-handle {
185
+ display: flex;
186
+ flex-direction: column;
187
+ gap: 3px;
188
+ padding: 4px 32px;
189
+ border-radius: 4px;
190
+ transition:
191
+ opacity 200ms ease,
192
+ transform 200ms ease;
193
+ opacity: 0.4;
194
+ }
195
+
196
+ .divider-container.expanded .grippy-handle {
197
+ opacity: 1;
198
+ transform: scaleY(1.2);
199
+ }
200
+
201
+ .divider-container.dragging .grippy-handle {
202
+ opacity: 1;
203
+ transform: scaleY(1.3);
204
+ }
205
+
206
+ .grip-line {
207
+ width: 32px;
208
+ height: 2px;
209
+ border-radius: 1px;
210
+ background: var(--color-base-content);
211
+ opacity: 0.4;
212
+ transition: background 200ms ease, box-shadow 200ms ease, opacity 200ms ease;
213
+ }
214
+
215
+ .divider-container.expanded .grip-line {
216
+ background: var(--color-primary);
217
+ opacity: 0.8;
218
+ box-shadow: 0 0 6px color-mix(in oklch, var(--color-primary) 50%, transparent);
219
+ }
220
+
221
+ .divider-container.dragging .grip-line {
222
+ background: var(--color-primary);
223
+ opacity: 1;
224
+ box-shadow: 0 0 8px color-mix(in oklch, var(--color-primary) 60%, transparent);
225
+ }
226
+
227
+ .divider-collapsed {
228
+ height: 6px;
229
+ min-height: 6px;
230
+ background: var(--color-base-200);
231
+ border-color: var(--color-base-300) !important;
232
+ }
233
+
234
+ .divider-collapsed.expanded {
235
+ height: 20px;
236
+ min-height: 20px;
237
+ box-shadow:
238
+ 0 0 16px color-mix(in oklch, var(--color-primary) 50%, transparent),
239
+ inset 0 0 10px color-mix(in oklch, var(--color-primary) 30%, transparent);
240
+ }
241
+
242
+ .collapsed-indicator {
243
+ display: flex;
244
+ align-items: center;
245
+ gap: 8px;
246
+ width: 100%;
247
+ padding: 0 16px;
248
+ transition: transform 200ms ease;
249
+ }
250
+
251
+ .divider-collapsed.expanded .collapsed-indicator {
252
+ transform: scaleY(1.5);
253
+ }
254
+
255
+ .indicator-line {
256
+ flex: 1;
257
+ height: 1px;
258
+ background: linear-gradient(
259
+ 90deg,
260
+ transparent 0%,
261
+ color-mix(in oklch, var(--color-base-content) 30%, transparent) 20%,
262
+ color-mix(in oklch, var(--color-base-content) 30%, transparent) 80%,
263
+ transparent 100%
264
+ );
265
+ transition: background 200ms ease, height 200ms ease;
266
+ }
267
+
268
+ .divider-collapsed.expanded .indicator-line {
269
+ height: 2px;
270
+ background: linear-gradient(
271
+ 90deg,
272
+ transparent 0%,
273
+ color-mix(in oklch, var(--color-primary) 70%, transparent) 15%,
274
+ color-mix(in oklch, var(--color-primary) 70%, transparent) 85%,
275
+ transparent 100%
276
+ );
277
+ }
278
+
279
+ .indicator-dots {
280
+ display: flex;
281
+ gap: 3px;
282
+ opacity: 0.5;
283
+ transition: opacity 200ms ease, transform 200ms ease, gap 200ms ease;
284
+ }
285
+
286
+ .indicator-dots span {
287
+ width: 3px;
288
+ height: 3px;
289
+ border-radius: 50%;
290
+ background: var(--color-base-content);
291
+ transition: width 200ms ease, height 200ms ease, background 200ms ease, box-shadow 200ms ease;
292
+ }
293
+
294
+ .divider-collapsed.expanded .indicator-dots {
295
+ opacity: 1;
296
+ gap: 5px;
297
+ }
298
+
299
+ .divider-collapsed.expanded .indicator-dots span {
300
+ width: 5px;
301
+ height: 5px;
302
+ background: var(--color-primary);
303
+ box-shadow: 0 0 8px color-mix(in oklch, var(--color-primary) 70%, transparent);
304
+ }
305
+ </style>