@joewinke/jatui 0.1.27 → 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.
@@ -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 { AsYouType, parsePhoneNumberFromString, type CountryCode } from "libphonenumber-js";
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
- value = parsed?.isValid() ? parsed.number : "";
105
+ setValue(parsed?.isValid() ? parsed.number : display || "");
74
106
  }
75
107
 
76
108
  function onInput(e: Event) {
77
- const raw = (e.target as HTMLInputElement).value;
109
+ const inputEl = e.target as HTMLInputElement;
110
+ const raw = inputEl.value;
78
111
  if (raw.startsWith("+")) {
79
- const parsed = parsePhoneNumberFromString(raw);
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
- display = parsed.formatNational();
83
- value = parsed.isValid() ? parsed.number : "";
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 = raw;
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(raw);
92
- updateE164(raw, country);
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 class="flex w-full {klass}">
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-60" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
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-200 border border-base-300 rounded-lg"
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/60 font-mono text-xs">{opt.dialCode}</span>
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/60 font-mono text-xs">{opt.dialCode}</span>
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/60">No matches</li>
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
- onResize: (deltaY: number) => void;
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 ('top' = top panel hidden, 'bottom' = bottom panel hidden) */
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
- let startY = $state(0);
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
- startY = e.clientY;
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 deltaY = e.clientY - startY;
61
- startY = e.clientY;
62
- onResize(deltaY);
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
- startY = e.touches[0].clientY;
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 deltaY = e.touches[0].clientY - startY;
85
- startY = e.touches[0].clientY;
86
- onResize(deltaY);
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 dividerCenterY = rect.top + rect.height / 2;
106
- const distance = Math.abs(e.clientY - dividerCenterY);
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="horizontal"
133
- aria-label={isCollapsed ? 'Expand panel' : 'Resize panel'}
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
- onResize: (deltaY: number) => void;
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 ('top' = top panel hidden, 'bottom' = bottom panel hidden) */
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
  }