@joewinke/jatui 0.1.26 → 0.1.28
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/dist/actions/collapse.d.ts +1 -1
- package/dist/actions/collapse.js +1 -1
- package/dist/actions/listNav.d.ts +134 -0
- package/dist/actions/listNav.js +672 -0
- package/dist/actions/railNav.js +7 -1
- 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/SearchDropdown.svelte +437 -172
- package/dist/components/SearchDropdown.svelte.d.ts +17 -1
- 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/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +6 -2
|
@@ -46,6 +46,17 @@
|
|
|
46
46
|
truncate?: boolean;
|
|
47
47
|
/** Optional display segments for formula-aware rendering */
|
|
48
48
|
displaySegments?: DisplaySegment[];
|
|
49
|
+
/**
|
|
50
|
+
* Optional Svelte action applied to the textarea while editing. Lets the
|
|
51
|
+
* host inject richer paste/undo handling without this library depending on
|
|
52
|
+
* it. No-op by default.
|
|
53
|
+
*/
|
|
54
|
+
pasteAction?: (node: HTMLTextAreaElement) => { destroy?: () => void } | void;
|
|
55
|
+
/**
|
|
56
|
+
* Optional guard consulted on blur: return true to keep the editor open
|
|
57
|
+
* (e.g. a paste-undo toast is still showing). Receives the textarea node.
|
|
58
|
+
*/
|
|
59
|
+
keepOpenOnBlur?: (node: HTMLTextAreaElement) => boolean;
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
let {
|
|
@@ -58,9 +69,14 @@
|
|
|
58
69
|
rows = 3,
|
|
59
70
|
showButtons = false,
|
|
60
71
|
truncate = false,
|
|
61
|
-
displaySegments = []
|
|
72
|
+
displaySegments = [],
|
|
73
|
+
pasteAction,
|
|
74
|
+
keepOpenOnBlur
|
|
62
75
|
}: Props = $props();
|
|
63
76
|
|
|
77
|
+
// No-op action so `use:` always has a valid target.
|
|
78
|
+
const applyPaste = (node: HTMLTextAreaElement) => pasteAction?.(node);
|
|
79
|
+
|
|
64
80
|
// --- Formula rendering ---
|
|
65
81
|
let hasFormulas = $derived(displaySegments.length > 0);
|
|
66
82
|
|
|
@@ -127,6 +143,14 @@
|
|
|
127
143
|
// Handle blur (auto-save when not using buttons)
|
|
128
144
|
function handleBlur() {
|
|
129
145
|
if (!showButtons) {
|
|
146
|
+
// Host may veto auto-save (e.g. a paste-undo toast is still available).
|
|
147
|
+
if (
|
|
148
|
+
type === 'textarea' &&
|
|
149
|
+
inputElement instanceof HTMLTextAreaElement &&
|
|
150
|
+
keepOpenOnBlur?.(inputElement)
|
|
151
|
+
) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
130
154
|
saveValue();
|
|
131
155
|
}
|
|
132
156
|
}
|
|
@@ -178,6 +202,7 @@
|
|
|
178
202
|
onblur={handleBlur}
|
|
179
203
|
use:autofocus
|
|
180
204
|
use:autoresize
|
|
205
|
+
use:applyPaste
|
|
181
206
|
></textarea>
|
|
182
207
|
{:else}
|
|
183
208
|
<input
|
|
@@ -39,6 +39,19 @@ interface Props {
|
|
|
39
39
|
truncate?: boolean;
|
|
40
40
|
/** Optional display segments for formula-aware rendering */
|
|
41
41
|
displaySegments?: DisplaySegment[];
|
|
42
|
+
/**
|
|
43
|
+
* Optional Svelte action applied to the textarea while editing. Lets the
|
|
44
|
+
* host inject richer paste/undo handling without this library depending on
|
|
45
|
+
* it. No-op by default.
|
|
46
|
+
*/
|
|
47
|
+
pasteAction?: (node: HTMLTextAreaElement) => {
|
|
48
|
+
destroy?: () => void;
|
|
49
|
+
} | void;
|
|
50
|
+
/**
|
|
51
|
+
* Optional guard consulted on blur: return true to keep the editor open
|
|
52
|
+
* (e.g. a paste-undo toast is still showing). Receives the textarea node.
|
|
53
|
+
*/
|
|
54
|
+
keepOpenOnBlur?: (node: HTMLTextAreaElement) => boolean;
|
|
42
55
|
}
|
|
43
56
|
declare const InlineEdit: import("svelte").Component<Props, {}, "">;
|
|
44
57
|
type InlineEdit = ReturnType<typeof InlineEdit>;
|
|
@@ -3,13 +3,22 @@
|
|
|
3
3
|
* PhoneInput Component — International phone number input.
|
|
4
4
|
*
|
|
5
5
|
* Country selector dropdown (flag + dial code + search) alongside a tel input.
|
|
6
|
-
* Formats numbers with libphonenumber-js AsYouType as the user types
|
|
6
|
+
* Formats numbers with libphonenumber-js AsYouType as the user types, and caps
|
|
7
|
+
* national digits at the country's example-number length so users can't overtype.
|
|
7
8
|
* Exposes the parsed E.164 value via `bind:value` and a hidden form field
|
|
8
|
-
* named `name` so it submits cleanly with native `<form>` actions.
|
|
9
|
+
* named `name` so it submits cleanly with native `<form>` actions. Also fires the
|
|
10
|
+
* optional `onChange(value)` callback on every change.
|
|
9
11
|
*
|
|
10
12
|
* Auto-detects country when the user's input begins with a `+` prefix.
|
|
11
13
|
*/
|
|
12
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
AsYouType,
|
|
16
|
+
parsePhoneNumberFromString,
|
|
17
|
+
getCountryCallingCode,
|
|
18
|
+
getExampleNumber,
|
|
19
|
+
type CountryCode,
|
|
20
|
+
} from "libphonenumber-js";
|
|
21
|
+
import examples from "libphonenumber-js/examples.mobile.json";
|
|
13
22
|
import { listCountries, PRIORITY_COUNTRIES, type CountryOption } from "../utils/phone";
|
|
14
23
|
|
|
15
24
|
type Props = {
|
|
@@ -22,6 +31,7 @@
|
|
|
22
31
|
class?: string;
|
|
23
32
|
disabled?: boolean;
|
|
24
33
|
autocomplete?: HTMLInputElement["autocomplete"];
|
|
34
|
+
onChange?: (value: string) => void;
|
|
25
35
|
};
|
|
26
36
|
|
|
27
37
|
let {
|
|
@@ -34,8 +44,25 @@
|
|
|
34
44
|
class: klass = "",
|
|
35
45
|
disabled = false,
|
|
36
46
|
autocomplete = "tel" as HTMLInputElement["autocomplete"],
|
|
47
|
+
onChange,
|
|
37
48
|
}: Props = $props();
|
|
38
49
|
|
|
50
|
+
function maxNationalDigits(cc: CountryCode): number {
|
|
51
|
+
const example = getExampleNumber(cc, examples);
|
|
52
|
+
return example?.nationalNumber.length ?? 15;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function capNationalDigits(raw: string, cc: CountryCode, max: number): string {
|
|
56
|
+
const allDigits = raw.replace(/\D/g, "");
|
|
57
|
+
if (!allDigits) return "";
|
|
58
|
+
const callingCode = getCountryCallingCode(cc);
|
|
59
|
+
const stripped =
|
|
60
|
+
allDigits.length > max && allDigits.startsWith(callingCode)
|
|
61
|
+
? allDigits.slice(callingCode.length)
|
|
62
|
+
: allDigits;
|
|
63
|
+
return stripped.slice(0, max);
|
|
64
|
+
}
|
|
65
|
+
|
|
39
66
|
const all = listCountries();
|
|
40
67
|
const priorityOptions = PRIORITY_COUNTRIES.map((c) => all.find((o) => o.code === c)).filter(
|
|
41
68
|
(o): o is CountryOption => !!o,
|
|
@@ -68,28 +95,49 @@
|
|
|
68
95
|
);
|
|
69
96
|
});
|
|
70
97
|
|
|
98
|
+
function setValue(next: string) {
|
|
99
|
+
value = next;
|
|
100
|
+
onChange?.(next);
|
|
101
|
+
}
|
|
102
|
+
|
|
71
103
|
function updateE164(raw: string, cc: CountryCode) {
|
|
72
104
|
const parsed = parsePhoneNumberFromString(raw, cc);
|
|
73
|
-
|
|
105
|
+
setValue(parsed?.isValid() ? parsed.number : display || "");
|
|
74
106
|
}
|
|
75
107
|
|
|
76
108
|
function onInput(e: Event) {
|
|
77
|
-
const
|
|
109
|
+
const inputEl = e.target as HTMLInputElement;
|
|
110
|
+
const raw = inputEl.value;
|
|
78
111
|
if (raw.startsWith("+")) {
|
|
79
|
-
const
|
|
112
|
+
const allDigits = raw.replace(/\D/g, "");
|
|
113
|
+
let parsed = parsePhoneNumberFromString(`+${allDigits}`);
|
|
114
|
+
for (let len = allDigits.length - 1; !parsed?.country && len >= 1; len--) {
|
|
115
|
+
parsed = parsePhoneNumberFromString(`+${allDigits.slice(0, len)}`);
|
|
116
|
+
}
|
|
80
117
|
if (parsed?.country) {
|
|
81
118
|
country = parsed.country;
|
|
82
|
-
|
|
83
|
-
|
|
119
|
+
const max = maxNationalDigits(parsed.country);
|
|
120
|
+
const cc = getCountryCallingCode(parsed.country);
|
|
121
|
+
const national = allDigits.startsWith(cc) ? allDigits.slice(cc.length) : allDigits;
|
|
122
|
+
const capped = national.slice(0, max);
|
|
123
|
+
const formatter = new AsYouType();
|
|
124
|
+
display = formatter.input(`+${cc}${capped}`);
|
|
125
|
+
inputEl.value = display;
|
|
126
|
+
const reparsed = parsePhoneNumberFromString(`+${cc}${capped}`);
|
|
127
|
+
setValue(reparsed?.isValid() ? reparsed.number : display || "");
|
|
84
128
|
return;
|
|
85
129
|
}
|
|
86
|
-
display =
|
|
87
|
-
value =
|
|
130
|
+
display = `+${allDigits.slice(0, 15)}`;
|
|
131
|
+
inputEl.value = display;
|
|
132
|
+
setValue("");
|
|
88
133
|
return;
|
|
89
134
|
}
|
|
135
|
+
const max = maxNationalDigits(country);
|
|
136
|
+
const nationalDigits = capNationalDigits(raw, country, max);
|
|
90
137
|
const formatter = new AsYouType(country);
|
|
91
|
-
display = formatter.input(
|
|
92
|
-
|
|
138
|
+
display = formatter.input(nationalDigits);
|
|
139
|
+
inputEl.value = display;
|
|
140
|
+
updateE164(nationalDigits, country);
|
|
93
141
|
}
|
|
94
142
|
|
|
95
143
|
function pickCountry(code: CountryCode) {
|
|
@@ -118,11 +166,13 @@
|
|
|
118
166
|
});
|
|
119
167
|
</script>
|
|
120
168
|
|
|
121
|
-
<div
|
|
169
|
+
<div
|
|
170
|
+
class="flex w-full rounded-field has-[input:focus]:ring-2 has-[input:focus]:ring-primary/40 transition-shadow {klass}"
|
|
171
|
+
>
|
|
122
172
|
<div class="relative">
|
|
123
173
|
<button
|
|
124
174
|
type="button"
|
|
125
|
-
class="btn btn-outline rounded-r-none border-r-0 px-3 min-w-[5.5rem] justify-start"
|
|
175
|
+
class="btn btn-outline rounded-r-none border-r-0 px-3 min-w-[5.5rem] justify-start focus-visible:outline-0"
|
|
126
176
|
onclick={toggleDropdown}
|
|
127
177
|
{disabled}
|
|
128
178
|
aria-label="Select country code"
|
|
@@ -131,14 +181,14 @@
|
|
|
131
181
|
>
|
|
132
182
|
<span class="text-lg leading-none">{current?.flag}</span>
|
|
133
183
|
<span class="text-sm font-mono">{current?.dialCode}</span>
|
|
134
|
-
<svg class="w-3 h-3 opacity-
|
|
184
|
+
<svg class="w-3 h-3 opacity-45" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
135
185
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
|
136
186
|
</svg>
|
|
137
187
|
</button>
|
|
138
188
|
|
|
139
189
|
{#if dropdownOpen}
|
|
140
190
|
<div
|
|
141
|
-
class="absolute z-50 mt-1 w-72 max-h-80 overflow-y-auto bg-base-
|
|
191
|
+
class="absolute z-50 mt-1 w-72 max-h-80 overflow-y-auto bg-base-100 border border-base-300 rounded-box shadow-lg"
|
|
142
192
|
role="listbox"
|
|
143
193
|
>
|
|
144
194
|
<div class="sticky top-0 bg-base-100 p-2 border-b border-base-300">
|
|
@@ -163,7 +213,7 @@
|
|
|
163
213
|
>
|
|
164
214
|
<span class="text-lg leading-none">{opt.flag}</span>
|
|
165
215
|
<span class="flex-1 text-left">{opt.name}</span>
|
|
166
|
-
<span class="text-base-content/
|
|
216
|
+
<span class="text-base-content/45 font-mono text-xs">{opt.dialCode}</span>
|
|
167
217
|
</button>
|
|
168
218
|
</li>
|
|
169
219
|
{/each}
|
|
@@ -180,12 +230,12 @@
|
|
|
180
230
|
>
|
|
181
231
|
<span class="text-lg leading-none">{opt.flag}</span>
|
|
182
232
|
<span class="flex-1 text-left">{opt.name}</span>
|
|
183
|
-
<span class="text-base-content/
|
|
233
|
+
<span class="text-base-content/45 font-mono text-xs">{opt.dialCode}</span>
|
|
184
234
|
</button>
|
|
185
235
|
</li>
|
|
186
236
|
{/each}
|
|
187
237
|
{#if filteredOthers.length === 0}
|
|
188
|
-
<li class="px-3 py-2 text-sm text-base-content/
|
|
238
|
+
<li class="px-3 py-2 text-sm text-base-content/45">No matches</li>
|
|
189
239
|
{/if}
|
|
190
240
|
</ul>
|
|
191
241
|
</div>
|
|
@@ -194,7 +244,7 @@
|
|
|
194
244
|
|
|
195
245
|
<input
|
|
196
246
|
type="tel"
|
|
197
|
-
class="input input-bordered rounded-l-none flex-1 min-w-0"
|
|
247
|
+
class="input input-bordered rounded-l-none flex-1 min-w-0 focus:outline-0"
|
|
198
248
|
{id}
|
|
199
249
|
{placeholder}
|
|
200
250
|
{required}
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* PhoneInput Component — International phone number input.
|
|
3
3
|
*
|
|
4
4
|
* Country selector dropdown (flag + dial code + search) alongside a tel input.
|
|
5
|
-
* Formats numbers with libphonenumber-js AsYouType as the user types
|
|
5
|
+
* Formats numbers with libphonenumber-js AsYouType as the user types, and caps
|
|
6
|
+
* national digits at the country's example-number length so users can't overtype.
|
|
6
7
|
* Exposes the parsed E.164 value via `bind:value` and a hidden form field
|
|
7
|
-
* named `name` so it submits cleanly with native `<form>` actions.
|
|
8
|
+
* named `name` so it submits cleanly with native `<form>` actions. Also fires the
|
|
9
|
+
* optional `onChange(value)` callback on every change.
|
|
8
10
|
*
|
|
9
11
|
* Auto-detects country when the user's input begins with a `+` prefix.
|
|
10
12
|
*/
|
|
@@ -19,6 +21,7 @@ type Props = {
|
|
|
19
21
|
class?: string;
|
|
20
22
|
disabled?: boolean;
|
|
21
23
|
autocomplete?: HTMLInputElement["autocomplete"];
|
|
24
|
+
onChange?: (value: string) => void;
|
|
22
25
|
};
|
|
23
26
|
declare const PhoneInput: import("svelte").Component<Props, {}, "value">;
|
|
24
27
|
type PhoneInput = ReturnType<typeof PhoneInput>;
|
|
@@ -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
|
}
|