@joewinke/jatui 0.1.27 → 0.1.29
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/CLAUDE.md +3 -3
- package/LICENSE +21 -0
- package/dist/actions/collapse.d.ts +1 -1
- package/dist/actions/collapse.js +16 -30
- package/dist/actions/listNav.d.ts +134 -0
- package/dist/actions/listNav.js +643 -0
- package/dist/actions/railNav.js +78 -89
- package/dist/components/InlineEdit.svelte +26 -1
- package/dist/components/InlineEdit.svelte.d.ts +13 -0
- package/dist/components/PhoneInput.svelte +70 -20
- package/dist/components/PhoneInput.svelte.d.ts +5 -2
- package/dist/components/ResizableDivider.svelte +148 -18
- package/dist/components/ResizableDivider.svelte.d.ts +14 -3
- package/dist/components/SignaturePad.svelte +13 -6
- package/dist/components/SignaturePad.svelte.d.ts +1 -0
- package/dist/components/ThemeSelector.svelte +61 -2
- package/dist/components/ThemeSelector.svelte.d.ts +4 -0
- package/dist/components/markup.js +59 -62
- package/dist/components/session-nav/transcriptModel.js +49 -66
- package/dist/data/emojis.js +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/stores/confirmDialog.svelte.js +5 -5
- package/dist/stores/inputDialog.svelte.js +5 -5
- package/dist/types/booking.js +4 -4
- package/dist/types/comments.js +2 -2
- package/dist/types/messaging.js +3 -3
- package/dist/types/pipeline.js +2 -2
- package/dist/utils/currency.js +2 -4
- package/dist/utils/daisyuiColors.js +25 -27
- package/dist/utils/dateFormatters.js +44 -44
- package/dist/utils/googleMapsLoader.js +46 -100
- package/dist/utils/mentionParser.js +30 -30
- package/dist/utils/phone.js +21 -27
- package/dist/utils/phoneFormat.js +9 -9
- package/dist/utils/taskUtils.js +1 -1
- package/package.json +11 -5
|
@@ -14,12 +14,23 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
interface Props {
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Drag delta callback. For `horizontal` orientation this is the vertical
|
|
19
|
+
* pointer delta (deltaY); for `vertical` orientation it is the horizontal
|
|
20
|
+
* pointer delta (deltaX). Positive = down / right.
|
|
21
|
+
*/
|
|
22
|
+
onResize: (delta: number) => void;
|
|
18
23
|
class?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Divider axis. `horizontal` = a horizontal bar that resizes panels stacked
|
|
26
|
+
* vertically (drag up/down). `vertical` = a vertical bar that resizes panels
|
|
27
|
+
* laid out side-by-side (drag left/right). Default: horizontal.
|
|
28
|
+
*/
|
|
29
|
+
orientation?: 'horizontal' | 'vertical';
|
|
19
30
|
/** Whether a panel is currently collapsed */
|
|
20
31
|
isCollapsed?: boolean;
|
|
21
|
-
/** Which direction the panel collapsed (
|
|
22
|
-
collapsedDirection?: 'top' | 'bottom' | null;
|
|
32
|
+
/** Which direction the panel collapsed (hidden panel side) */
|
|
33
|
+
collapsedDirection?: 'top' | 'bottom' | 'left' | 'right' | null;
|
|
23
34
|
/** Callback when clicking the divider while collapsed (to restore) */
|
|
24
35
|
onCollapsedClick?: () => void;
|
|
25
36
|
}
|
|
@@ -27,21 +38,26 @@
|
|
|
27
38
|
let {
|
|
28
39
|
onResize,
|
|
29
40
|
class: className = '',
|
|
41
|
+
orientation = 'horizontal',
|
|
30
42
|
isCollapsed = false,
|
|
31
43
|
collapsedDirection = null,
|
|
32
44
|
onCollapsedClick
|
|
33
45
|
}: Props = $props();
|
|
34
46
|
|
|
47
|
+
const isVertical = $derived(orientation === 'vertical');
|
|
48
|
+
|
|
35
49
|
let isDragging = $state(false);
|
|
36
50
|
let isNearby = $state(false);
|
|
37
51
|
let isHovering = $state(false);
|
|
38
|
-
|
|
52
|
+
// Drag origin along the active axis (clientY for horizontal, clientX for vertical).
|
|
53
|
+
let startPos = $state(0);
|
|
39
54
|
let dividerElement: HTMLDivElement | undefined = $state();
|
|
40
55
|
|
|
41
56
|
// Proximity detection threshold in pixels
|
|
42
57
|
const PROXIMITY_THRESHOLD = 24;
|
|
43
58
|
|
|
44
59
|
function handleMouseDown(e: MouseEvent) {
|
|
60
|
+
// If collapsed and we have a restore handler, treat click as restore
|
|
45
61
|
if (isCollapsed && onCollapsedClick) {
|
|
46
62
|
onCollapsedClick();
|
|
47
63
|
return;
|
|
@@ -49,7 +65,7 @@
|
|
|
49
65
|
|
|
50
66
|
e.preventDefault();
|
|
51
67
|
isDragging = true;
|
|
52
|
-
|
|
68
|
+
startPos = isVertical ? e.clientX : e.clientY;
|
|
53
69
|
|
|
54
70
|
document.addEventListener('mousemove', handleMouseMove);
|
|
55
71
|
document.addEventListener('mouseup', handleMouseUp);
|
|
@@ -57,9 +73,10 @@
|
|
|
57
73
|
|
|
58
74
|
function handleMouseMove(e: MouseEvent) {
|
|
59
75
|
if (!isDragging) return;
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
const pos = isVertical ? e.clientX : e.clientY;
|
|
77
|
+
const delta = pos - startPos;
|
|
78
|
+
startPos = pos;
|
|
79
|
+
onResize(delta);
|
|
63
80
|
}
|
|
64
81
|
|
|
65
82
|
function handleMouseUp() {
|
|
@@ -69,6 +86,7 @@
|
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
function handleTouchStart(e: TouchEvent) {
|
|
89
|
+
// If collapsed and we have a restore handler, treat tap as restore
|
|
72
90
|
if (isCollapsed && onCollapsedClick) {
|
|
73
91
|
onCollapsedClick();
|
|
74
92
|
return;
|
|
@@ -76,14 +94,15 @@
|
|
|
76
94
|
|
|
77
95
|
if (e.touches.length !== 1) return;
|
|
78
96
|
isDragging = true;
|
|
79
|
-
|
|
97
|
+
startPos = isVertical ? e.touches[0].clientX : e.touches[0].clientY;
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
function handleTouchMove(e: TouchEvent) {
|
|
83
101
|
if (!isDragging || e.touches.length !== 1) return;
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
102
|
+
const pos = isVertical ? e.touches[0].clientX : e.touches[0].clientY;
|
|
103
|
+
const delta = pos - startPos;
|
|
104
|
+
startPos = pos;
|
|
105
|
+
onResize(delta);
|
|
87
106
|
}
|
|
88
107
|
|
|
89
108
|
function handleTouchEnd() {
|
|
@@ -98,16 +117,19 @@
|
|
|
98
117
|
isHovering = false;
|
|
99
118
|
}
|
|
100
119
|
|
|
120
|
+
// Proximity detection: track mouse movement near the divider
|
|
101
121
|
function handleProximityMove(e: MouseEvent) {
|
|
102
122
|
if (!dividerElement || isDragging) return;
|
|
103
123
|
|
|
104
124
|
const rect = dividerElement.getBoundingClientRect();
|
|
105
|
-
const
|
|
106
|
-
|
|
125
|
+
const distance = isVertical
|
|
126
|
+
? Math.abs(e.clientX - (rect.left + rect.width / 2))
|
|
127
|
+
: Math.abs(e.clientY - (rect.top + rect.height / 2));
|
|
107
128
|
|
|
108
129
|
isNearby = distance <= PROXIMITY_THRESHOLD;
|
|
109
130
|
}
|
|
110
131
|
|
|
132
|
+
// Set up proximity tracking on mount
|
|
111
133
|
$effect(() => {
|
|
112
134
|
if (typeof window === 'undefined') return;
|
|
113
135
|
|
|
@@ -118,19 +140,21 @@
|
|
|
118
140
|
};
|
|
119
141
|
});
|
|
120
142
|
|
|
143
|
+
// Derived state for expanded appearance
|
|
121
144
|
const isExpanded = $derived(isDragging || isHovering || isNearby);
|
|
122
145
|
</script>
|
|
123
146
|
|
|
124
|
-
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
125
147
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
148
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
126
149
|
<div
|
|
127
150
|
bind:this={dividerElement}
|
|
128
|
-
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' : ''}"
|
|
151
|
+
class="divider-container flex items-center justify-center select-none {className} {isCollapsed ? 'cursor-pointer divider-collapsed' : isVertical ? 'cursor-col-resize' : 'cursor-row-resize'} {isDragging ? 'bg-primary/25' : isExpanded && isCollapsed ? 'bg-primary/40' : isExpanded ? 'bg-primary/15' : ''}"
|
|
129
152
|
class:expanded={isExpanded}
|
|
130
153
|
class:dragging={isDragging}
|
|
154
|
+
class:vertical={isVertical}
|
|
131
155
|
role="separator"
|
|
132
|
-
aria-orientation=
|
|
133
|
-
aria-
|
|
156
|
+
aria-orientation={isVertical ? 'vertical' : 'horizontal'}
|
|
157
|
+
aria-valuenow={isCollapsed ? 0 : 1}
|
|
134
158
|
tabindex="0"
|
|
135
159
|
onmousedown={handleMouseDown}
|
|
136
160
|
onmouseenter={handleMouseEnter}
|
|
@@ -140,6 +164,7 @@
|
|
|
140
164
|
ontouchend={handleTouchEnd}
|
|
141
165
|
>
|
|
142
166
|
{#if isCollapsed}
|
|
167
|
+
<!-- Collapsed state: minimal industrial indicator -->
|
|
143
168
|
<div class="collapsed-indicator">
|
|
144
169
|
<div class="indicator-line"></div>
|
|
145
170
|
<div class="indicator-dots">
|
|
@@ -150,6 +175,7 @@
|
|
|
150
175
|
<div class="indicator-line"></div>
|
|
151
176
|
</div>
|
|
152
177
|
{:else}
|
|
178
|
+
<!-- Normal state: grippy handle with proximity-aware sizing -->
|
|
153
179
|
<div class="grippy-handle">
|
|
154
180
|
<div class="grip-line"></div>
|
|
155
181
|
<div class="grip-line"></div>
|
|
@@ -158,6 +184,7 @@
|
|
|
158
184
|
</div>
|
|
159
185
|
|
|
160
186
|
<style>
|
|
187
|
+
/* Main divider container with smooth transitions */
|
|
161
188
|
.divider-container {
|
|
162
189
|
height: 8px;
|
|
163
190
|
min-height: 8px;
|
|
@@ -167,6 +194,8 @@
|
|
|
167
194
|
box-shadow 200ms ease;
|
|
168
195
|
}
|
|
169
196
|
|
|
197
|
+
/* Expanded state: larger hit target + glow */
|
|
198
|
+
/* NOTE: background uses Tailwind class (bg-primary/15) on element instead of CSS */
|
|
170
199
|
.divider-container.expanded {
|
|
171
200
|
height: 16px;
|
|
172
201
|
min-height: 16px;
|
|
@@ -175,6 +204,8 @@
|
|
|
175
204
|
inset 0 0 8px color-mix(in oklch, var(--color-primary) 20%, transparent);
|
|
176
205
|
}
|
|
177
206
|
|
|
207
|
+
/* Dragging state: stronger glow */
|
|
208
|
+
/* NOTE: background uses Tailwind class (bg-primary/25) on element instead of CSS */
|
|
178
209
|
.divider-container.dragging {
|
|
179
210
|
height: 16px;
|
|
180
211
|
min-height: 16px;
|
|
@@ -183,6 +214,7 @@
|
|
|
183
214
|
inset 0 0 12px color-mix(in oklch, var(--color-primary) 30%, transparent);
|
|
184
215
|
}
|
|
185
216
|
|
|
217
|
+
/* Grippy handle styling */
|
|
186
218
|
.grippy-handle {
|
|
187
219
|
display: flex;
|
|
188
220
|
flex-direction: column;
|
|
@@ -226,6 +258,7 @@
|
|
|
226
258
|
box-shadow: 0 0 8px color-mix(in oklch, var(--color-primary) 60%, transparent);
|
|
227
259
|
}
|
|
228
260
|
|
|
261
|
+
/* Collapsed state styling */
|
|
229
262
|
.divider-collapsed {
|
|
230
263
|
height: 6px;
|
|
231
264
|
min-height: 6px;
|
|
@@ -233,6 +266,8 @@
|
|
|
233
266
|
border-color: var(--color-base-300) !important;
|
|
234
267
|
}
|
|
235
268
|
|
|
269
|
+
/* Collapsed + expanded (proximity/hover): grow larger for easy targeting */
|
|
270
|
+
/* NOTE: background uses Tailwind class (bg-primary/40) on element instead of CSS */
|
|
236
271
|
.divider-collapsed.expanded {
|
|
237
272
|
height: 20px;
|
|
238
273
|
min-height: 20px;
|
|
@@ -250,6 +285,7 @@
|
|
|
250
285
|
transition: transform 200ms ease;
|
|
251
286
|
}
|
|
252
287
|
|
|
288
|
+
/* Scale up the indicator when expanded */
|
|
253
289
|
.divider-collapsed.expanded .collapsed-indicator {
|
|
254
290
|
transform: scaleY(1.5);
|
|
255
291
|
}
|
|
@@ -267,6 +303,7 @@
|
|
|
267
303
|
transition: background 200ms ease, height 200ms ease;
|
|
268
304
|
}
|
|
269
305
|
|
|
306
|
+
/* Brighter lines when expanded */
|
|
270
307
|
.divider-collapsed.expanded .indicator-line {
|
|
271
308
|
height: 2px;
|
|
272
309
|
background: linear-gradient(
|
|
@@ -293,6 +330,7 @@
|
|
|
293
330
|
transition: width 200ms ease, height 200ms ease, background 200ms ease, box-shadow 200ms ease;
|
|
294
331
|
}
|
|
295
332
|
|
|
333
|
+
/* Expanded state: larger, brighter dots with glow */
|
|
296
334
|
.divider-collapsed.expanded .indicator-dots {
|
|
297
335
|
opacity: 1;
|
|
298
336
|
gap: 5px;
|
|
@@ -304,4 +342,96 @@
|
|
|
304
342
|
background: var(--color-primary);
|
|
305
343
|
box-shadow: 0 0 8px color-mix(in oklch, var(--color-primary) 70%, transparent);
|
|
306
344
|
}
|
|
345
|
+
|
|
346
|
+
/* ── Vertical orientation ─────────────────────────────────────────────────
|
|
347
|
+
Mirrors the horizontal styles across the X axis: the bar runs top-to-bottom
|
|
348
|
+
and resizes side-by-side panels (drag left/right). Width replaces height
|
|
349
|
+
everywhere, and the grippy handle / collapsed indicator rotate 90°. */
|
|
350
|
+
.divider-container.vertical {
|
|
351
|
+
height: auto;
|
|
352
|
+
min-height: 0;
|
|
353
|
+
width: 8px;
|
|
354
|
+
min-width: 8px;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.divider-container.vertical.expanded {
|
|
358
|
+
height: auto;
|
|
359
|
+
min-height: 0;
|
|
360
|
+
width: 16px;
|
|
361
|
+
min-width: 16px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.divider-container.vertical.dragging {
|
|
365
|
+
height: auto;
|
|
366
|
+
min-height: 0;
|
|
367
|
+
width: 16px;
|
|
368
|
+
min-width: 16px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.divider-container.vertical .grippy-handle {
|
|
372
|
+
flex-direction: row;
|
|
373
|
+
padding: 32px 4px;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.divider-container.vertical.expanded .grippy-handle {
|
|
377
|
+
transform: scaleX(1.2);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.divider-container.vertical.dragging .grippy-handle {
|
|
381
|
+
transform: scaleX(1.3);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.divider-container.vertical .grip-line {
|
|
385
|
+
width: 2px;
|
|
386
|
+
height: 32px;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.divider-collapsed.vertical {
|
|
390
|
+
height: auto;
|
|
391
|
+
min-height: 0;
|
|
392
|
+
width: 6px;
|
|
393
|
+
min-width: 6px;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.divider-collapsed.vertical.expanded {
|
|
397
|
+
height: auto;
|
|
398
|
+
min-height: 0;
|
|
399
|
+
width: 20px;
|
|
400
|
+
min-width: 20px;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.divider-container.vertical .collapsed-indicator {
|
|
404
|
+
flex-direction: column;
|
|
405
|
+
width: auto;
|
|
406
|
+
height: 100%;
|
|
407
|
+
padding: 16px 0;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.divider-collapsed.vertical.expanded .collapsed-indicator {
|
|
411
|
+
transform: scaleX(1.5);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.divider-container.vertical .indicator-line {
|
|
415
|
+
width: 1px;
|
|
416
|
+
height: auto;
|
|
417
|
+
background: linear-gradient(
|
|
418
|
+
180deg,
|
|
419
|
+
transparent 0%,
|
|
420
|
+
color-mix(in oklch, var(--color-base-content) 30%, transparent) 20%,
|
|
421
|
+
color-mix(in oklch, var(--color-base-content) 30%, transparent) 80%,
|
|
422
|
+
transparent 100%
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.divider-collapsed.vertical.expanded .indicator-line {
|
|
427
|
+
width: 2px;
|
|
428
|
+
height: auto;
|
|
429
|
+
background: linear-gradient(
|
|
430
|
+
180deg,
|
|
431
|
+
transparent 0%,
|
|
432
|
+
color-mix(in oklch, var(--color-primary) 70%, transparent) 15%,
|
|
433
|
+
color-mix(in oklch, var(--color-primary) 70%, transparent) 85%,
|
|
434
|
+
transparent 100%
|
|
435
|
+
);
|
|
436
|
+
}
|
|
307
437
|
</style>
|
|
@@ -12,12 +12,23 @@
|
|
|
12
12
|
* - Glow effect on hover for better visibility
|
|
13
13
|
*/
|
|
14
14
|
interface Props {
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Drag delta callback. For `horizontal` orientation this is the vertical
|
|
17
|
+
* pointer delta (deltaY); for `vertical` orientation it is the horizontal
|
|
18
|
+
* pointer delta (deltaX). Positive = down / right.
|
|
19
|
+
*/
|
|
20
|
+
onResize: (delta: number) => void;
|
|
16
21
|
class?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Divider axis. `horizontal` = a horizontal bar that resizes panels stacked
|
|
24
|
+
* vertically (drag up/down). `vertical` = a vertical bar that resizes panels
|
|
25
|
+
* laid out side-by-side (drag left/right). Default: horizontal.
|
|
26
|
+
*/
|
|
27
|
+
orientation?: 'horizontal' | 'vertical';
|
|
17
28
|
/** Whether a panel is currently collapsed */
|
|
18
29
|
isCollapsed?: boolean;
|
|
19
|
-
/** Which direction the panel collapsed (
|
|
20
|
-
collapsedDirection?: 'top' | 'bottom' | null;
|
|
30
|
+
/** Which direction the panel collapsed (hidden panel side) */
|
|
31
|
+
collapsedDirection?: 'top' | 'bottom' | 'left' | 'right' | null;
|
|
21
32
|
/** Callback when clicking the divider while collapsed (to restore) */
|
|
22
33
|
onCollapsedClick?: () => void;
|
|
23
34
|
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Exports `clear()` and `isEmpty()` methods. Outputs signature as base64 PNG data URL
|
|
6
6
|
* via the bindable `signatureData` prop or `onSignatureChange` callback.
|
|
7
|
+
*
|
|
8
|
+
* Pass `disabled` to render a non-interactive, dimmed pad (e.g. form-builder previews).
|
|
7
9
|
*/
|
|
8
10
|
import { onMount } from 'svelte';
|
|
9
11
|
|
|
@@ -17,6 +19,7 @@
|
|
|
17
19
|
label = 'Signature',
|
|
18
20
|
required = false,
|
|
19
21
|
placeholder = 'Sign here',
|
|
22
|
+
disabled = false,
|
|
20
23
|
}: {
|
|
21
24
|
signatureData?: string;
|
|
22
25
|
onSignatureChange?: (data: string) => void;
|
|
@@ -27,6 +30,7 @@
|
|
|
27
30
|
label?: string;
|
|
28
31
|
required?: boolean;
|
|
29
32
|
placeholder?: string;
|
|
33
|
+
disabled?: boolean;
|
|
30
34
|
} = $props();
|
|
31
35
|
|
|
32
36
|
let canvas: HTMLCanvasElement;
|
|
@@ -68,6 +72,7 @@
|
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
function startDrawing(e: MouseEvent | TouchEvent) {
|
|
75
|
+
if (disabled) return;
|
|
71
76
|
e.preventDefault();
|
|
72
77
|
isDrawing = true;
|
|
73
78
|
const coords = getCoordinates(e);
|
|
@@ -76,7 +81,7 @@
|
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
function draw(e: MouseEvent | TouchEvent) {
|
|
79
|
-
if (!isDrawing || !ctx) return;
|
|
84
|
+
if (disabled || !isDrawing || !ctx) return;
|
|
80
85
|
e.preventDefault();
|
|
81
86
|
|
|
82
87
|
const coords = getCoordinates(e);
|
|
@@ -145,19 +150,21 @@
|
|
|
145
150
|
<span class="text-error">*</span>
|
|
146
151
|
{/if}
|
|
147
152
|
</span>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
{#if !disabled}
|
|
154
|
+
<button type="button" onclick={() => clear()} class="btn btn-ghost btn-xs">
|
|
155
|
+
Clear
|
|
156
|
+
</button>
|
|
157
|
+
{/if}
|
|
151
158
|
</div>
|
|
152
159
|
{/if}
|
|
153
160
|
|
|
154
161
|
<div class="rounded-lg bg-base-100 overflow-hidden relative transition-all duration-300
|
|
155
|
-
{hasDrawn ? 'border-2 border-success/60' : 'border-2 border-base-300'}">
|
|
162
|
+
{disabled ? 'opacity-50 border-2 border-base-300' : hasDrawn ? 'border-2 border-success/60' : 'border-2 border-base-300'}">
|
|
156
163
|
<canvas
|
|
157
164
|
bind:this={canvas}
|
|
158
165
|
{width}
|
|
159
166
|
{height}
|
|
160
|
-
class="w-full touch-none cursor-crosshair"
|
|
167
|
+
class="w-full touch-none {disabled ? 'cursor-not-allowed' : 'cursor-crosshair'}"
|
|
161
168
|
></canvas>
|
|
162
169
|
|
|
163
170
|
{#if !hasDrawn}
|
|
@@ -53,12 +53,18 @@
|
|
|
53
53
|
defaultTheme = 'light',
|
|
54
54
|
storageKey = 'theme',
|
|
55
55
|
onThemeChange,
|
|
56
|
+
compact = false,
|
|
57
|
+
inline = false,
|
|
56
58
|
}: {
|
|
57
59
|
dropUp?: boolean;
|
|
58
60
|
themes?: ThemeOption[];
|
|
59
61
|
defaultTheme?: string;
|
|
60
62
|
storageKey?: string;
|
|
61
63
|
onThemeChange?: (theme: string) => void;
|
|
64
|
+
/** Icon-only trigger (label hidden, shown as tooltip). Dropdown mode only. */
|
|
65
|
+
compact?: boolean;
|
|
66
|
+
/** Render a collapsible `<details>` theme list instead of a dropdown — for embedding inside another menu. */
|
|
67
|
+
inline?: boolean;
|
|
62
68
|
} = $props();
|
|
63
69
|
|
|
64
70
|
// svelte-ignore state_referenced_locally
|
|
@@ -104,6 +110,51 @@
|
|
|
104
110
|
}
|
|
105
111
|
</script>
|
|
106
112
|
|
|
113
|
+
{#if inline}
|
|
114
|
+
<!-- Inline mode: collapsible theme list with color previews, for embedding inside another menu -->
|
|
115
|
+
<details class="w-full theme-details">
|
|
116
|
+
<summary class="flex items-center gap-2 cursor-pointer hover:bg-base-300 transition-colors px-2 py-1.5 rounded list-none">
|
|
117
|
+
<div
|
|
118
|
+
data-theme={currentTheme}
|
|
119
|
+
class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded p-0.5 w-4 h-4 transition-transform duration-300 ease-out"
|
|
120
|
+
class:theme-spin={isAnimating}
|
|
121
|
+
>
|
|
122
|
+
<div class="bg-base-content size-1 rounded-full transition-all duration-300 theme-dot" class:theme-dot-animate={isAnimating} style="--dot-delay: 0ms"></div>
|
|
123
|
+
<div class="bg-primary size-1 rounded-full transition-all duration-300 theme-dot" class:theme-dot-animate={isAnimating} style="--dot-delay: 50ms"></div>
|
|
124
|
+
<div class="bg-secondary size-1 rounded-full transition-all duration-300 theme-dot" class:theme-dot-animate={isAnimating} style="--dot-delay: 100ms"></div>
|
|
125
|
+
<div class="bg-accent size-1 rounded-full transition-all duration-300 theme-dot" class:theme-dot-animate={isAnimating} style="--dot-delay: 150ms"></div>
|
|
126
|
+
</div>
|
|
127
|
+
<span class="text-[0.75rem] flex-1 text-left text-base-content/70">{currentThemeLabel}</span>
|
|
128
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3 h-3 text-base-content/45 transition-transform duration-200 details-chevron">
|
|
129
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
|
130
|
+
</svg>
|
|
131
|
+
</summary>
|
|
132
|
+
<div class="max-h-48 overflow-y-auto mt-1 ml-2 border-l border-base-content/10 pl-2">
|
|
133
|
+
{#each themes as theme}
|
|
134
|
+
<button
|
|
135
|
+
class="flex items-center gap-2 w-full px-2 py-1 rounded transition-colors hover:bg-base-300 {currentTheme === theme.name ? 'bg-primary/20' : ''}"
|
|
136
|
+
onclick={() => handleThemeChange(theme.name)}
|
|
137
|
+
>
|
|
138
|
+
<div
|
|
139
|
+
data-theme={theme.name}
|
|
140
|
+
class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded p-0.5 w-4 h-4"
|
|
141
|
+
>
|
|
142
|
+
<div class="bg-base-content size-1 rounded-full"></div>
|
|
143
|
+
<div class="bg-primary size-1 rounded-full"></div>
|
|
144
|
+
<div class="bg-secondary size-1 rounded-full"></div>
|
|
145
|
+
<div class="bg-accent size-1 rounded-full"></div>
|
|
146
|
+
</div>
|
|
147
|
+
<span class="text-[0.75rem] text-base-content/70 flex-1 text-left">{theme.label}</span>
|
|
148
|
+
{#if currentTheme === theme.name}
|
|
149
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-3 h-3 text-primary">
|
|
150
|
+
<path d="M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z"></path>
|
|
151
|
+
</svg>
|
|
152
|
+
{/if}
|
|
153
|
+
</button>
|
|
154
|
+
{/each}
|
|
155
|
+
</div>
|
|
156
|
+
</details>
|
|
157
|
+
{:else}
|
|
107
158
|
<div class="dropdown dropdown-end" class:dropdown-top={dropUp}>
|
|
108
159
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
109
160
|
<div
|
|
@@ -111,7 +162,7 @@
|
|
|
111
162
|
role="button"
|
|
112
163
|
class="flex items-center gap-2 cursor-pointer hover:bg-base-300 transition-colors px-2 py-1 rounded"
|
|
113
164
|
aria-label="Change Theme"
|
|
114
|
-
title=
|
|
165
|
+
title={compact ? currentThemeLabel : 'Change Theme'}
|
|
115
166
|
>
|
|
116
167
|
<div
|
|
117
168
|
data-theme={currentTheme}
|
|
@@ -123,7 +174,9 @@
|
|
|
123
174
|
<div class="bg-secondary size-1 rounded-full transition-all duration-300 theme-dot" class:theme-dot-animate={isAnimating} style="--dot-delay: 100ms"></div>
|
|
124
175
|
<div class="bg-accent size-1 rounded-full transition-all duration-300 theme-dot" class:theme-dot-animate={isAnimating} style="--dot-delay: 150ms"></div>
|
|
125
176
|
</div>
|
|
126
|
-
|
|
177
|
+
{#if !compact}
|
|
178
|
+
<span class="text-sm font-medium transition-opacity duration-200" class:opacity-50={isAnimating}>{currentThemeLabel}</span>
|
|
179
|
+
{/if}
|
|
127
180
|
</div>
|
|
128
181
|
|
|
129
182
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
@@ -171,8 +224,14 @@
|
|
|
171
224
|
</ul>
|
|
172
225
|
</div>
|
|
173
226
|
</div>
|
|
227
|
+
{/if}
|
|
174
228
|
|
|
175
229
|
<style>
|
|
230
|
+
/* Rotate chevron when the inline details list is open */
|
|
231
|
+
.theme-details[open] .details-chevron {
|
|
232
|
+
transform: rotate(180deg);
|
|
233
|
+
}
|
|
234
|
+
|
|
176
235
|
/* NOTE: DaisyUI v5.0.0 has a stuck-animation bug on `.dropdown-content`
|
|
177
236
|
(`@keyframes dropdown { 0% { opacity: 0 } }` pins opacity to 0). If the
|
|
178
237
|
theme dropdown opens but stays invisible, the consuming project needs
|
|
@@ -8,6 +8,10 @@ type $$ComponentProps = {
|
|
|
8
8
|
defaultTheme?: string;
|
|
9
9
|
storageKey?: string;
|
|
10
10
|
onThemeChange?: (theme: string) => void;
|
|
11
|
+
/** Icon-only trigger (label hidden, shown as tooltip). Dropdown mode only. */
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
/** Render a collapsible `<details>` theme list instead of a dropdown — for embedding inside another menu. */
|
|
14
|
+
inline?: boolean;
|
|
11
15
|
};
|
|
12
16
|
declare const ThemeSelector: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
17
|
type ThemeSelector = ReturnType<typeof ThemeSelector>;
|