@urbicon-ui/blocks 6.34.0 → 6.36.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 (127) hide show
  1. package/README.md +11 -1
  2. package/dist/components/Calendar/CalendarHeader.svelte +32 -26
  3. package/dist/components/Calendar/CalendarMiniMonth.svelte +10 -10
  4. package/dist/components/Calendar/calendar.variants.js +19 -5
  5. package/dist/components/Chat/Chat/Chat.svelte +53 -0
  6. package/dist/components/Chat/Chat/Chat.svelte.d.ts +4 -0
  7. package/dist/components/Chat/Chat/chat.variants.d.ts +20 -0
  8. package/dist/components/Chat/Chat/chat.variants.js +15 -0
  9. package/dist/components/Chat/Chat/index.d.ts +47 -0
  10. package/dist/components/Chat/Chat/index.js +2 -0
  11. package/dist/components/Chat/ChatMessage/ChatMessage.svelte +342 -0
  12. package/dist/components/Chat/ChatMessage/ChatMessage.svelte.d.ts +4 -0
  13. package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +138 -0
  14. package/dist/components/Chat/ChatMessage/chat-message.variants.js +118 -0
  15. package/dist/components/Chat/ChatMessage/index.d.ts +102 -0
  16. package/dist/components/Chat/ChatMessage/index.js +2 -0
  17. package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte +301 -0
  18. package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte.d.ts +6 -0
  19. package/dist/components/Chat/ChatMessageList/chat-message-list.variants.d.ts +23 -0
  20. package/dist/components/Chat/ChatMessageList/chat-message-list.variants.js +32 -0
  21. package/dist/components/Chat/ChatMessageList/chat-scroll.d.ts +45 -0
  22. package/dist/components/Chat/ChatMessageList/chat-scroll.js +52 -0
  23. package/dist/components/Chat/ChatMessageList/index.d.ts +93 -0
  24. package/dist/components/Chat/ChatMessageList/index.js +3 -0
  25. package/dist/components/Chat/CitationChip/CitationChip.svelte +107 -0
  26. package/dist/components/Chat/CitationChip/CitationChip.svelte.d.ts +4 -0
  27. package/dist/components/Chat/CitationChip/citation-chip.variants.d.ts +40 -0
  28. package/dist/components/Chat/CitationChip/citation-chip.variants.js +42 -0
  29. package/dist/components/Chat/CitationChip/index.d.ts +69 -0
  30. package/dist/components/Chat/CitationChip/index.js +2 -0
  31. package/dist/components/Chat/CodeBlock/CodeBlock.svelte +120 -0
  32. package/dist/components/Chat/CodeBlock/CodeBlock.svelte.d.ts +4 -0
  33. package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +40 -0
  34. package/dist/components/Chat/CodeBlock/code-block.variants.js +51 -0
  35. package/dist/components/Chat/CodeBlock/index.d.ts +55 -0
  36. package/dist/components/Chat/CodeBlock/index.js +2 -0
  37. package/dist/components/Chat/PromptInput/PromptInput.svelte +432 -0
  38. package/dist/components/Chat/PromptInput/PromptInput.svelte.d.ts +4 -0
  39. package/dist/components/Chat/PromptInput/index.d.ts +165 -0
  40. package/dist/components/Chat/PromptInput/index.js +2 -0
  41. package/dist/components/Chat/PromptInput/prompt-input.variants.d.ts +90 -0
  42. package/dist/components/Chat/PromptInput/prompt-input.variants.js +102 -0
  43. package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte +107 -0
  44. package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte.d.ts +4 -0
  45. package/dist/components/Chat/ReasoningDisclosure/index.d.ts +85 -0
  46. package/dist/components/Chat/ReasoningDisclosure/index.js +2 -0
  47. package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.d.ts +20 -0
  48. package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.js +31 -0
  49. package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte +99 -0
  50. package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte.d.ts +9 -0
  51. package/dist/components/Chat/StreamingMarkdown/MdInline.svelte +82 -0
  52. package/dist/components/Chat/StreamingMarkdown/MdInline.svelte.d.ts +9 -0
  53. package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte +118 -0
  54. package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte.d.ts +4 -0
  55. package/dist/components/Chat/StreamingMarkdown/index.d.ts +86 -0
  56. package/dist/components/Chat/StreamingMarkdown/index.js +2 -0
  57. package/dist/components/Chat/StreamingMarkdown/md-context.d.ts +58 -0
  58. package/dist/components/Chat/StreamingMarkdown/md-context.js +1 -0
  59. package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.d.ts +145 -0
  60. package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.js +73 -0
  61. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +174 -0
  62. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte.d.ts +4 -0
  63. package/dist/components/Chat/ToolCallCard/index.d.ts +84 -0
  64. package/dist/components/Chat/ToolCallCard/index.js +2 -0
  65. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +38 -0
  66. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +38 -0
  67. package/dist/components/Chat/chat.types.d.ts +82 -0
  68. package/dist/components/Chat/chat.types.js +7 -0
  69. package/dist/components/Chat/index.d.ts +22 -0
  70. package/dist/components/Chat/index.js +14 -0
  71. package/dist/components/Chat/markdown/blocks.d.ts +54 -0
  72. package/dist/components/Chat/markdown/blocks.js +462 -0
  73. package/dist/components/Chat/markdown/inline.d.ts +31 -0
  74. package/dist/components/Chat/markdown/inline.js +446 -0
  75. package/dist/components/Chat/markdown/repair.d.ts +23 -0
  76. package/dist/components/Chat/markdown/repair.js +337 -0
  77. package/dist/components/Chat/markdown/streaming-harness.d.ts +23 -0
  78. package/dist/components/Chat/markdown/streaming-harness.js +62 -0
  79. package/dist/components/Chat/markdown/types.d.ts +139 -0
  80. package/dist/components/Chat/markdown/types.js +13 -0
  81. package/dist/components/Chat/markdown/url-policy.d.ts +33 -0
  82. package/dist/components/Chat/markdown/url-policy.js +109 -0
  83. package/dist/components/FileUpload/FileUpload.svelte +52 -125
  84. package/dist/components/FileUpload/FileUpload.svelte.d.ts +1 -1
  85. package/dist/components/FileUpload/index.d.ts +2 -28
  86. package/dist/components/Planner/PlannerHeader.svelte +16 -13
  87. package/dist/components/Planner/planner.variants.js +14 -3
  88. package/dist/components/index.d.ts +1 -0
  89. package/dist/components/index.js +1 -0
  90. package/dist/i18n/index.d.ts +268 -382
  91. package/dist/i18n/index.js +83 -9
  92. package/dist/internal/core/CoreIconButton.svelte +66 -0
  93. package/dist/internal/core/CoreIconButton.svelte.d.ts +13 -0
  94. package/dist/internal/core/CoreSpinner.svelte +49 -0
  95. package/dist/internal/core/CoreSpinner.svelte.d.ts +8 -0
  96. package/dist/internal/core/spinner-geometry.d.ts +6 -0
  97. package/dist/internal/core/spinner-geometry.js +6 -0
  98. package/dist/mint/registry.d.ts +20 -2
  99. package/dist/mint/registry.js +20 -2
  100. package/dist/primitives/Badge/Badge.svelte +5 -5
  101. package/dist/primitives/Badge/badge.variants.js +17 -1
  102. package/dist/primitives/Button/Button.svelte +4 -2
  103. package/dist/primitives/Collapsible/Collapsible.svelte +5 -0
  104. package/dist/primitives/Dialog/Dialog.svelte +7 -5
  105. package/dist/primitives/Dialog/dialog.variants.d.ts +7 -0
  106. package/dist/primitives/Dialog/dialog.variants.js +17 -0
  107. package/dist/primitives/Drawer/Drawer.svelte +7 -5
  108. package/dist/primitives/Drawer/drawer.variants.d.ts +8 -0
  109. package/dist/primitives/Drawer/drawer.variants.js +17 -0
  110. package/dist/primitives/Spinner/Spinner.svelte +3 -1
  111. package/dist/primitives/SplitPane/SplitPane.svelte +293 -0
  112. package/dist/primitives/SplitPane/SplitPane.svelte.d.ts +4 -0
  113. package/dist/primitives/SplitPane/index.d.ts +88 -0
  114. package/dist/primitives/SplitPane/index.js +2 -0
  115. package/dist/primitives/SplitPane/split-pane.utils.d.ts +73 -0
  116. package/dist/primitives/SplitPane/split-pane.utils.js +94 -0
  117. package/dist/primitives/SplitPane/split-pane.variants.d.ts +40 -0
  118. package/dist/primitives/SplitPane/split-pane.variants.js +56 -0
  119. package/dist/primitives/Toast/Toaster.svelte +17 -2
  120. package/dist/primitives/index.d.ts +2 -0
  121. package/dist/primitives/index.js +1 -0
  122. package/dist/utils/file-intake.d.ts +78 -0
  123. package/dist/utils/file-intake.js +144 -0
  124. package/dist/utils/index.d.ts +1 -0
  125. package/dist/utils/index.js +1 -0
  126. package/dist/utils/variants.js +44 -10
  127. package/package.json +4 -6
@@ -0,0 +1,293 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import { mintRegistry } from '../../mint';
4
+ import { getBlocksConfig, resolveSlotClasses } from '../../provider';
5
+ import type { SplitPaneProps } from './index';
6
+ import { clampRatio, parseLimit, ratioFromPointer, resolveDragRatio } from './split-pane.utils';
7
+ import { splitPaneVariants, type SplitPaneVariants } from './split-pane.variants';
8
+
9
+ let {
10
+ start,
11
+ end,
12
+ handle,
13
+ orientation = 'horizontal',
14
+ defaultRatio = 0.5,
15
+ ratio = $bindable(defaultRatio),
16
+ min = '10%',
17
+ max = '90%',
18
+ collapsible = false,
19
+ collapseThreshold = 48,
20
+ disabled = false,
21
+ onRatioChange,
22
+ onCollapsedChange,
23
+ handleLabel = 'Resize panes',
24
+ mint = 'none',
25
+ class: className = '',
26
+ unstyled: unstyledProp = false,
27
+ slotClasses: slotClassesProp = {},
28
+ preset,
29
+ ...restProps
30
+ }: SplitPaneProps = $props();
31
+
32
+ const blocksConfig = getBlocksConfig();
33
+ const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
34
+
35
+ // aria-controls needs a stable id for the first pane. `$props.id()` must be a
36
+ // top-level initializer (compiler rule) — SplitPane has no `id` prop, so the
37
+ // single-step form is enough.
38
+ const propsId = $props.id();
39
+ const startPaneId = `splitpane-${propsId}-start`;
40
+
41
+ // Slider performs NO right-to-left inversion in its pointer/keyboard handling
42
+ // (physical coordinates, ArrowRight = increase). SplitPane mirrors that role
43
+ // model exactly, so `rtl` stays false here. The geometry util still accepts an
44
+ // `rtl` flag (and is unit-tested for it) so a future house-wide RTL pass has
45
+ // one seam to wire — see the primitive's open issues.
46
+ const rtl = false;
47
+
48
+ let rootRef = $state<HTMLDivElement>();
49
+ let handleRef = $state<HTMLDivElement>();
50
+ let dragging = $state(false);
51
+ // Collapsed is DERIVED from the bindable ratio, never tracked separately —
52
+ // an external `bind:ratio` write must move this flag too, or the Enter
53
+ // toggle acts on stale state (review finding, P1 wave).
54
+ const collapsed = $derived(collapsible && ratio <= 0);
55
+ // Remembers the ratio to restore when re-expanding via Enter. Seeded once
56
+ // from the initial defaultRatio — `untrack` marks the one-time snapshot so it
57
+ // does not read as a missed reactive dependency.
58
+ let lastExpandedRatio = $state(untrack(() => defaultRatio));
59
+
60
+ // Keep the restore target fresh from every expanded ratio (interactive or
61
+ // consumer-driven), and fire onCollapsedChange on actual transitions of the
62
+ // derived flag — regardless of which side caused them.
63
+ let lastCollapsed = untrack(() => collapsed);
64
+ $effect(() => {
65
+ if (ratio > 0) lastExpandedRatio = ratio;
66
+ });
67
+ $effect(() => {
68
+ if (collapsed !== lastCollapsed) {
69
+ lastCollapsed = collapsed;
70
+ onCollapsedChange?.(collapsed);
71
+ }
72
+ });
73
+ // Last measured container size on the active axis — feeds px-limit resolution
74
+ // and the aria-value* bounds. Kept fresh via a ResizeObserver (no-op in jsdom).
75
+ let measuredPx = $state(0);
76
+
77
+ $effect(() => {
78
+ const el = rootRef;
79
+ if (!el) return;
80
+ const measure = () => {
81
+ const rect = el.getBoundingClientRect();
82
+ measuredPx = orientation === 'horizontal' ? rect.width : rect.height;
83
+ };
84
+ measure();
85
+ if (typeof ResizeObserver === 'undefined') return;
86
+ const ro = new ResizeObserver(measure);
87
+ ro.observe(el);
88
+ return () => ro.disconnect();
89
+ });
90
+
91
+ // mint is wired on the interactive element — the divider — exactly as Slider
92
+ // applies it to its track ref.
93
+ $effect(() => {
94
+ if (handleRef && mint && mint !== 'none' && !disabled) {
95
+ return mintRegistry.apply(handleRef, mint);
96
+ }
97
+ });
98
+
99
+ function containerSize(): number {
100
+ if (!rootRef) return measuredPx;
101
+ const rect = rootRef.getBoundingClientRect();
102
+ const px = orientation === 'horizontal' ? rect.width : rect.height;
103
+ if (px > 0) measuredPx = px;
104
+ return px > 0 ? px : measuredPx;
105
+ }
106
+
107
+ function emitRatio(next: number) {
108
+ if (next !== ratio) {
109
+ ratio = next;
110
+ onRatioChange?.(next);
111
+ }
112
+ }
113
+
114
+ // ─── Pointer drag ────────────────────────────────────────────────────────
115
+ // setPointerCapture routes every subsequent pointer event to the divider, so
116
+ // move/up listeners live on the divider itself — no window listeners, nothing
117
+ // to leak. Cancel restores the resting state.
118
+ function onHandlePointerDown(event: PointerEvent) {
119
+ if (disabled) return;
120
+ event.preventDefault();
121
+ handleRef?.focus();
122
+ handleRef?.setPointerCapture?.(event.pointerId);
123
+ dragging = true;
124
+ }
125
+
126
+ function onHandlePointerMove(event: PointerEvent) {
127
+ if (!dragging || disabled || !rootRef) return;
128
+ const rect = rootRef.getBoundingClientRect();
129
+ const px = orientation === 'horizontal' ? rect.width : rect.height;
130
+ if (px > 0) measuredPx = px;
131
+ const clientPos = orientation === 'horizontal' ? event.clientX : event.clientY;
132
+ const raw = ratioFromPointer(clientPos, rect, orientation, rtl);
133
+ const resolved = resolveDragRatio(
134
+ raw,
135
+ px,
136
+ { min, max, collapsible, collapseThreshold },
137
+ collapsed
138
+ );
139
+ emitRatio(resolved.ratio);
140
+ }
141
+
142
+ function endDrag(event: PointerEvent) {
143
+ if (!dragging) return;
144
+ handleRef?.releasePointerCapture?.(event.pointerId);
145
+ dragging = false;
146
+ }
147
+
148
+ function onHandleDblClick() {
149
+ if (disabled) return;
150
+ emitRatio(clampRatio(defaultRatio, min, max, containerSize()));
151
+ }
152
+
153
+ // ─── Keyboard ────────────────────────────────────────────────────────────
154
+ // Arrow keys map to the layout axis (horizontal → Left/Right, vertical →
155
+ // Up/Down): the "increase" key grows the first pane. ±2% default, ±10% with
156
+ // Shift; Home/End jump to the min/max limits. Enter follows the APG window
157
+ // splitter: with `collapsible` it toggles collapse, otherwise it restores
158
+ // defaultRatio — which also gives keyboard users the reset that double-click
159
+ // provides for pointers.
160
+ function onHandleKeydown(event: KeyboardEvent) {
161
+ if (disabled) return;
162
+
163
+ const px = containerSize();
164
+ const stepAmount = event.shiftKey ? 0.1 : 0.02;
165
+ const incKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
166
+ const decKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
167
+
168
+ switch (event.key) {
169
+ case incKey: {
170
+ event.preventDefault();
171
+ emitRatio(clampRatio(ratio + stepAmount, min, max, px));
172
+ return;
173
+ }
174
+ case decKey: {
175
+ event.preventDefault();
176
+ emitRatio(clampRatio(ratio - stepAmount, min, max, px));
177
+ return;
178
+ }
179
+ case 'Home': {
180
+ event.preventDefault();
181
+ emitRatio(parseLimit(min, px) ?? 0);
182
+ return;
183
+ }
184
+ case 'End': {
185
+ event.preventDefault();
186
+ emitRatio(parseLimit(max, px) ?? 1);
187
+ return;
188
+ }
189
+ case 'Enter': {
190
+ event.preventDefault();
191
+ if (!collapsible) {
192
+ emitRatio(clampRatio(defaultRatio, min, max, px));
193
+ } else if (collapsed) {
194
+ const restore = lastExpandedRatio > 0 ? lastExpandedRatio : defaultRatio;
195
+ emitRatio(clampRatio(restore, min, max, px));
196
+ } else {
197
+ emitRatio(0);
198
+ }
199
+ return;
200
+ }
201
+ default:
202
+ return;
203
+ }
204
+ }
205
+
206
+ const separatorOrientation = $derived(orientation === 'horizontal' ? 'vertical' : 'horizontal');
207
+
208
+ // The DISPLAYED ratio is always clamped into [min, max] (collapse being the
209
+ // one legitimate exception), so an out-of-window initial/controlled value or
210
+ // a container resize can never render the pane — or report aria-valuenow —
211
+ // outside the configured bounds. The bound `ratio` itself is left untouched
212
+ // (no write-backs from render state).
213
+ const effectiveRatio = $derived.by(() => {
214
+ const bounded = Math.max(0, Math.min(1, ratio));
215
+ if (collapsed) return 0;
216
+ return clampRatio(bounded, min, max, measuredPx);
217
+ });
218
+ const startBasisPct = $derived(effectiveRatio * 100);
219
+
220
+ const valueNow = $derived(Math.round(effectiveRatio * 100));
221
+ // With `collapsible`, 0 is a legitimate value of the separator's range —
222
+ // otherwise a collapsed pane would report valuenow below valuemin.
223
+ const valueMin = $derived(collapsible ? 0 : Math.round((parseLimit(min, measuredPx) ?? 0) * 100));
224
+ const valueMax = $derived(Math.round((parseLimit(max, measuredPx) ?? 1) * 100));
225
+
226
+ const variantProps: SplitPaneVariants = $derived({
227
+ orientation,
228
+ dragging: dragging || undefined,
229
+ disabled: disabled || undefined
230
+ });
231
+
232
+ const styles = $derived(splitPaneVariants(variantProps));
233
+ const slotClasses = $derived(
234
+ resolveSlotClasses(blocksConfig, 'SplitPane', preset, variantProps, slotClassesProp)
235
+ );
236
+ </script>
237
+
238
+ <div
239
+ {...restProps}
240
+ bind:this={rootRef}
241
+ class={unstyled
242
+ ? [slotClasses?.root, className].filter(Boolean).join(' ')
243
+ : styles.root({ class: [slotClasses?.root, className] })}
244
+ data-orientation={orientation}
245
+ data-dragging={dragging || undefined}
246
+ data-collapsed={collapsed || undefined}
247
+ >
248
+ <div
249
+ id={startPaneId}
250
+ class={unstyled
251
+ ? (slotClasses?.startPane ?? '')
252
+ : styles.startPane({ class: slotClasses?.startPane })}
253
+ style="flex: 0 0 {startBasisPct}%"
254
+ >
255
+ {@render start()}
256
+ </div>
257
+
258
+ <!-- The divider is the ARIA "window splitter": role="separator" made focusable
259
+ (tabindex) with aria-value* + keyboard resize. Svelte's a11y rules treat
260
+ any separator as non-interactive; this pattern is the sanctioned exception. -->
261
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
262
+ <div
263
+ bind:this={handleRef}
264
+ role="separator"
265
+ tabindex={disabled ? -1 : 0}
266
+ aria-label={handleLabel}
267
+ aria-controls={startPaneId}
268
+ aria-orientation={separatorOrientation}
269
+ aria-valuenow={valueNow}
270
+ aria-valuemin={valueMin}
271
+ aria-valuemax={valueMax}
272
+ aria-disabled={disabled || undefined}
273
+ data-dragging={dragging || undefined}
274
+ class={unstyled ? (slotClasses?.handle ?? '') : styles.handle({ class: slotClasses?.handle })}
275
+ onpointerdown={onHandlePointerDown}
276
+ onpointermove={onHandlePointerMove}
277
+ onpointerup={endDrag}
278
+ onpointercancel={endDrag}
279
+ ondblclick={onHandleDblClick}
280
+ onkeydown={onHandleKeydown}
281
+ >
282
+ {@render handle?.()}
283
+ </div>
284
+
285
+ <div
286
+ class={unstyled
287
+ ? (slotClasses?.endPane ?? '')
288
+ : styles.endPane({ class: slotClasses?.endPane })}
289
+ style="flex: 1 1 0%"
290
+ >
291
+ {@render end()}
292
+ </div>
293
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { SplitPaneProps } from './index.js';
2
+ declare const SplitPane: import("svelte").Component<SplitPaneProps, {}, "ratio">;
3
+ type SplitPane = ReturnType<typeof SplitPane>;
4
+ export default SplitPane;
@@ -0,0 +1,88 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { MintProp } from '../../mint/index.js';
4
+ import type { SplitPaneLimit } from './split-pane.utils.js';
5
+ import type { SplitPaneSlots, SplitPaneVariants } from './split-pane.variants.js';
6
+ /**
7
+ * @description Resizable two-pane layout. A draggable divider (ARIA "window
8
+ * splitter", role="separator") splits the container into a `start` and an `end`
9
+ * pane; the first pane's share is controlled by `ratio` (0–1) and supports
10
+ * `bind:ratio`. Resize by pointer drag or keyboard (Arrow / Home / End), with
11
+ * optional collapse-to-zero. Panes clip their own overflow so their content
12
+ * scrolls independently. For an application shell with a permanent nav rail
13
+ * prefer SidebarLayout / Sidebar; reach for SplitPane when both regions are
14
+ * primary content the user should be able to rebalance.
15
+ *
16
+ * @tag layout
17
+ * @related SidebarLayout
18
+ * @related Sidebar
19
+ * @stability experimental
20
+ *
21
+ * @example
22
+ * ```svelte
23
+ * <SplitPane bind:ratio={split} min="20%" max="80%">
24
+ * {#snippet start()}<nav class="p-4">…</nav>{/snippet}
25
+ * {#snippet end()}<main class="p-4">…</main>{/snippet}
26
+ * </SplitPane>
27
+ * ```
28
+ *
29
+ * @example
30
+ * ```svelte
31
+ * <SplitPane orientation="vertical" collapsible defaultRatio={0.3}>
32
+ * {#snippet start()}<section class="overflow-auto">Editor</section>{/snippet}
33
+ * {#snippet end()}<section class="overflow-auto">Preview</section>{/snippet}
34
+ * </SplitPane>
35
+ * ```
36
+ */
37
+ export interface SplitPaneProps extends Omit<SplitPaneVariants, 'dragging' | 'orientation' | 'disabled'>, Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'class'> {
38
+ /** First pane. In `horizontal` orientation it is the leading (left) pane; in `vertical` the top pane. Required. */
39
+ start: Snippet;
40
+ /** Second pane — fills the space the first pane leaves. Required. */
41
+ end: Snippet;
42
+ /** Custom content rendered inside the divider (e.g. a grip icon). Replaces the default line; the divider stays the focusable separator. */
43
+ handle?: Snippet;
44
+ /** Layout axis. `horizontal` places panes side by side (vertical divider); `vertical` stacks them (horizontal divider). @default 'horizontal' */
45
+ orientation?: 'horizontal' | 'vertical';
46
+ /** First pane's share of the container, `0`–`1`. Supports `bind:ratio`. When omitted, starts at `defaultRatio` and is managed internally. */
47
+ ratio?: number;
48
+ /** Initial ratio for uncontrolled use; also the target of a double-click reset. @default 0.5 */
49
+ defaultRatio?: number;
50
+ /** Lower bound for the first pane — px (`number`) or percentage string (`'20%'`). @default '10%' */
51
+ min?: SplitPaneLimit;
52
+ /** Upper bound for the first pane — px (`number`) or percentage string (`'90%'`). @default '90%' */
53
+ max?: SplitPaneLimit;
54
+ /** Allow the first pane to collapse to zero: dragging below `collapseThreshold`, or pressing Enter on the divider, snaps it shut. @default false */
55
+ collapsible?: boolean;
56
+ /** Pixel width/height of the first pane below which a drag snaps it collapsed. Only used when `collapsible`. @default 48 */
57
+ collapseThreshold?: number;
58
+ /** Disable resizing — the divider becomes inert (not focusable, no pointer/keyboard response). Panes stay visible. @default false */
59
+ disabled?: boolean;
60
+ /** Fires after a drag/keyboard interaction changes the ratio. Receives the new ratio (`0`–`1`). Does not fire for consumer-driven `bind:ratio` writes. */
61
+ onRatioChange?: (ratio: number) => void;
62
+ /** Fires when the first pane collapses (`true`) or re-expands (`false`) via drag or Enter. */
63
+ onCollapsedChange?: (collapsed: boolean) => void;
64
+ /** Accessible name for the divider (its `aria-label`). @default 'Resize panes' */
65
+ handleLabel?: string;
66
+ /** Micro-interaction preset applied to the divider. @default 'none' */
67
+ mint?: MintProp;
68
+ /** Extra classes merged onto the root container (the flex box). */
69
+ class?: string;
70
+ /** Remove all default tv() classes; combine with `slotClasses` to rebuild the look. */
71
+ unstyled?: boolean;
72
+ /**
73
+ * Per-slot class overrides merged with tv() styles. Slots: root (the flex
74
+ * container — what `class` also targets) | startPane | endPane | handle (the
75
+ * draggable divider / separator).
76
+ */
77
+ slotClasses?: Partial<Record<SplitPaneSlots, string>>;
78
+ /**
79
+ * Apply a named preset registered via `<BlocksProvider presets={{ SplitPane: {...} }}>`.
80
+ * Prefer this over `class` overrides when the requested look falls outside the
81
+ * semantic intent palette — presets keep hover/active/dark-mode logic coherent
82
+ * and make the custom look reusable across the project.
83
+ */
84
+ preset?: string;
85
+ }
86
+ export { default as SplitPane } from './SplitPane.svelte';
87
+ export type { SplitPaneLimit, SplitPaneOrientation } from './split-pane.utils.js';
88
+ export { type SplitPaneVariants, splitPaneVariants } from './split-pane.variants.js';
@@ -0,0 +1,2 @@
1
+ export { default as SplitPane } from './SplitPane.svelte';
2
+ export { splitPaneVariants } from './split-pane.variants.js';
@@ -0,0 +1,73 @@
1
+ /** Layout axis. `horizontal` = panes side by side; `vertical` = stacked. */
2
+ export type SplitPaneOrientation = 'horizontal' | 'vertical';
3
+ /**
4
+ * A min/max boundary for the first pane. A raw `number` is pixels; a string
5
+ * ending in `%` (e.g. `'20%'`) is a percentage of the container. A bare numeric
6
+ * string (`'200'`) is also treated as pixels.
7
+ */
8
+ export type SplitPaneLimit = number | string;
9
+ /** The four rect fields the pointer math reads — a structural subset of DOMRect. */
10
+ export interface RectLike {
11
+ left: number;
12
+ top: number;
13
+ width: number;
14
+ height: number;
15
+ }
16
+ /**
17
+ * Resolve a min/max limit to a ratio in `[0, 1]`.
18
+ *
19
+ * - Percentage strings (`'20%'`) convert directly and need no measurement.
20
+ * - Pixel values (number or bare numeric string) are divided by `containerPx`.
21
+ *
22
+ * Returns `null` when a pixel limit cannot be resolved because the container has
23
+ * not been measured yet (`containerPx <= 0`). Callers treat a `null` minimum as
24
+ * `0` and a `null` maximum as `1`, so an unmeasured px limit degrades to "no
25
+ * constraint" until the first layout pass, then clamps at runtime.
26
+ */
27
+ export declare function parseLimit(limit: SplitPaneLimit, containerPx: number): number | null;
28
+ /**
29
+ * Clamp a ratio into the `[min, max]` window, resolving px/percent limits
30
+ * against `containerPx`. Unresolved px limits fall back to `0` (min) / `1`
31
+ * (max). A reversed window (`min > max`) is tolerated by swapping the bounds so
32
+ * the result is always well-defined. A non-finite input ratio snaps to the
33
+ * lower bound.
34
+ */
35
+ export declare function clampRatio(ratio: number, min: SplitPaneLimit, max: SplitPaneLimit, containerPx: number): number;
36
+ /**
37
+ * Convert a pointer coordinate to the first-pane ratio (`[0, 1]`), before
38
+ * min/max clamping.
39
+ *
40
+ * - `horizontal`: uses `clientX` against `rect.left` / `rect.width`. When `rtl`
41
+ * is true the axis is mirrored (the first pane grows from the right edge).
42
+ * - `vertical`: uses `clientY` against `rect.top` / `rect.height`; `rtl` has no
43
+ * effect on the vertical axis.
44
+ *
45
+ * A zero-sized container yields `0` rather than dividing by zero.
46
+ */
47
+ export declare function ratioFromPointer(clientPos: number, rect: RectLike, orientation: SplitPaneOrientation, rtl?: boolean): number;
48
+ /** Limits + collapse config consumed by {@link resolveDragRatio}. */
49
+ export interface DragResolveOptions {
50
+ min: SplitPaneLimit;
51
+ max: SplitPaneLimit;
52
+ collapsible: boolean;
53
+ collapseThreshold: number;
54
+ }
55
+ /**
56
+ * Dead-band added on top of `collapseThreshold` before a collapsed pane
57
+ * re-expands during a drag. Without it, pointer jitter around the threshold
58
+ * flip-flops ratio between `0` and `min` and bursts the change callbacks.
59
+ */
60
+ export declare const COLLAPSE_HYSTERESIS_PX = 12;
61
+ /**
62
+ * Decide the resting ratio and collapsed flag for a raw pointer ratio.
63
+ *
64
+ * Collapse wins over the min clamp: while `collapsible`, dragging the first pane
65
+ * below `collapseThreshold` px snaps it fully shut (`ratio: 0`, `collapsed:
66
+ * true`). While `currentlyCollapsed`, re-expanding requires clearing the
67
+ * threshold plus {@link COLLAPSE_HYSTERESIS_PX} (drag hysteresis). Otherwise the
68
+ * ratio is clamped into the `[min, max]` window and `collapsed` is `false`.
69
+ */
70
+ export declare function resolveDragRatio(raw: number, containerPx: number, { min, max, collapsible, collapseThreshold }: DragResolveOptions, currentlyCollapsed?: boolean): {
71
+ ratio: number;
72
+ collapsed: boolean;
73
+ };
@@ -0,0 +1,94 @@
1
+ // Pure geometry for the SplitPane primitive. Every function here is
2
+ // side-effect-free and DOM-free so the resize math can be unit-tested in the
3
+ // node environment (no jsdom) and the `.svelte` file can stay a thin shell over
4
+ // it — exactly the split Slider draws between its keyboard math and its markup.
5
+ const clamp01 = (n) => Math.min(1, Math.max(0, n));
6
+ /**
7
+ * Resolve a min/max limit to a ratio in `[0, 1]`.
8
+ *
9
+ * - Percentage strings (`'20%'`) convert directly and need no measurement.
10
+ * - Pixel values (number or bare numeric string) are divided by `containerPx`.
11
+ *
12
+ * Returns `null` when a pixel limit cannot be resolved because the container has
13
+ * not been measured yet (`containerPx <= 0`). Callers treat a `null` minimum as
14
+ * `0` and a `null` maximum as `1`, so an unmeasured px limit degrades to "no
15
+ * constraint" until the first layout pass, then clamps at runtime.
16
+ */
17
+ export function parseLimit(limit, containerPx) {
18
+ if (typeof limit === 'string') {
19
+ const trimmed = limit.trim();
20
+ if (trimmed.endsWith('%')) {
21
+ const pct = Number.parseFloat(trimmed.slice(0, -1));
22
+ return Number.isFinite(pct) ? clamp01(pct / 100) : null;
23
+ }
24
+ const px = Number.parseFloat(trimmed);
25
+ if (!Number.isFinite(px))
26
+ return null;
27
+ return containerPx > 0 ? clamp01(px / containerPx) : null;
28
+ }
29
+ if (!Number.isFinite(limit))
30
+ return null;
31
+ return containerPx > 0 ? clamp01(limit / containerPx) : null;
32
+ }
33
+ /**
34
+ * Clamp a ratio into the `[min, max]` window, resolving px/percent limits
35
+ * against `containerPx`. Unresolved px limits fall back to `0` (min) / `1`
36
+ * (max). A reversed window (`min > max`) is tolerated by swapping the bounds so
37
+ * the result is always well-defined. A non-finite input ratio snaps to the
38
+ * lower bound.
39
+ */
40
+ export function clampRatio(ratio, min, max, containerPx) {
41
+ const lo = parseLimit(min, containerPx) ?? 0;
42
+ const hi = parseLimit(max, containerPx) ?? 1;
43
+ const low = Math.min(lo, hi);
44
+ const high = Math.max(lo, hi);
45
+ const r = Number.isFinite(ratio) ? ratio : low;
46
+ return Math.min(high, Math.max(low, r));
47
+ }
48
+ /**
49
+ * Convert a pointer coordinate to the first-pane ratio (`[0, 1]`), before
50
+ * min/max clamping.
51
+ *
52
+ * - `horizontal`: uses `clientX` against `rect.left` / `rect.width`. When `rtl`
53
+ * is true the axis is mirrored (the first pane grows from the right edge).
54
+ * - `vertical`: uses `clientY` against `rect.top` / `rect.height`; `rtl` has no
55
+ * effect on the vertical axis.
56
+ *
57
+ * A zero-sized container yields `0` rather than dividing by zero.
58
+ */
59
+ export function ratioFromPointer(clientPos, rect, orientation, rtl = false) {
60
+ if (orientation === 'vertical') {
61
+ if (!(rect.height > 0))
62
+ return 0;
63
+ return clamp01((clientPos - rect.top) / rect.height);
64
+ }
65
+ if (!(rect.width > 0))
66
+ return 0;
67
+ const raw = (clientPos - rect.left) / rect.width;
68
+ return clamp01(rtl ? 1 - raw : raw);
69
+ }
70
+ /**
71
+ * Dead-band added on top of `collapseThreshold` before a collapsed pane
72
+ * re-expands during a drag. Without it, pointer jitter around the threshold
73
+ * flip-flops ratio between `0` and `min` and bursts the change callbacks.
74
+ */
75
+ export const COLLAPSE_HYSTERESIS_PX = 12;
76
+ /**
77
+ * Decide the resting ratio and collapsed flag for a raw pointer ratio.
78
+ *
79
+ * Collapse wins over the min clamp: while `collapsible`, dragging the first pane
80
+ * below `collapseThreshold` px snaps it fully shut (`ratio: 0`, `collapsed:
81
+ * true`). While `currentlyCollapsed`, re-expanding requires clearing the
82
+ * threshold plus {@link COLLAPSE_HYSTERESIS_PX} (drag hysteresis). Otherwise the
83
+ * ratio is clamped into the `[min, max]` window and `collapsed` is `false`.
84
+ */
85
+ export function resolveDragRatio(raw, containerPx, { min, max, collapsible, collapseThreshold }, currentlyCollapsed = false) {
86
+ if (collapsible && containerPx > 0) {
87
+ const releasePx = currentlyCollapsed
88
+ ? collapseThreshold + COLLAPSE_HYSTERESIS_PX
89
+ : collapseThreshold;
90
+ if (raw * containerPx < releasePx)
91
+ return { ratio: 0, collapsed: true };
92
+ }
93
+ return { ratio: clampRatio(raw, min, max, containerPx), collapsed: false };
94
+ }
@@ -0,0 +1,40 @@
1
+ import { type SlotNames, type VariantProps } from '../../utils/variants.js';
2
+ export declare const splitPaneVariants: ((props?: {
3
+ orientation?: "horizontal" | "vertical" | undefined;
4
+ dragging?: boolean | undefined;
5
+ disabled?: boolean | undefined;
6
+ } | undefined) => {
7
+ root: (props?: ({
8
+ orientation?: "horizontal" | "vertical" | undefined;
9
+ dragging?: boolean | undefined;
10
+ disabled?: boolean | undefined;
11
+ } & {
12
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
13
+ }) | undefined) => string;
14
+ startPane: (props?: ({
15
+ orientation?: "horizontal" | "vertical" | undefined;
16
+ dragging?: boolean | undefined;
17
+ disabled?: boolean | undefined;
18
+ } & {
19
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
20
+ }) | undefined) => string;
21
+ endPane: (props?: ({
22
+ orientation?: "horizontal" | "vertical" | undefined;
23
+ dragging?: boolean | undefined;
24
+ disabled?: boolean | undefined;
25
+ } & {
26
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
27
+ }) | undefined) => string;
28
+ handle: (props?: ({
29
+ orientation?: "horizontal" | "vertical" | undefined;
30
+ dragging?: boolean | undefined;
31
+ disabled?: boolean | undefined;
32
+ } & {
33
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
34
+ }) | undefined) => string;
35
+ }) & {
36
+ readonly config: import("../../utils/variants.js").TVConfig;
37
+ };
38
+ export type SplitPaneVariants = VariantProps<typeof splitPaneVariants>;
39
+ /** Slot names derived from the `tv()` config above — single source of truth for `slotClasses`. */
40
+ export type SplitPaneSlots = SlotNames<typeof splitPaneVariants>;
@@ -0,0 +1,56 @@
1
+ import { tv } from '../../utils/variants.js';
2
+ export const splitPaneVariants = tv({
3
+ slots: {
4
+ // Flex container. `h-full`/`w-full` inherit the consumer's box; the panes
5
+ // size via flex-basis so the divider can sit at any ratio.
6
+ root: ['relative flex w-full h-full min-w-0 min-h-0'],
7
+ // Panes clip their own overflow and let long content scroll internally —
8
+ // min-w-0/min-h-0 defeats flexbox's default min-content floor so a pane can
9
+ // actually shrink below its content size.
10
+ startPane: ['overflow-hidden min-w-0 min-h-0'],
11
+ endPane: ['overflow-hidden min-w-0 min-h-0'],
12
+ // The divider is a thin visible line (bg-border-default) that is itself the
13
+ // role="separator". It is positioned (`relative`) with no z-index, so — like
14
+ // Slider's rail thumb — it and its transparent `::before` hit-expander paint
15
+ // above the statically-flowed panes. The `::before` swells the grab target
16
+ // to ≥24px (WCAG 2.5.8 AA) without widening the visible gutter.
17
+ handle: [
18
+ 'relative shrink-0 touch-none select-none',
19
+ 'bg-border-default',
20
+ 'transition-[background-color] duration-[var(--blocks-duration-fast)]',
21
+ 'hover:bg-primary',
22
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-1 focus-visible:ring-offset-surface-base',
23
+ "before:content-[''] before:absolute"
24
+ ]
25
+ },
26
+ variants: {
27
+ orientation: {
28
+ horizontal: {
29
+ root: 'flex-row',
30
+ // Vertical rule between side-by-side panes. Hit area: 4px + 2×10px = 24px.
31
+ handle: 'self-stretch w-1 cursor-col-resize before:inset-y-0 before:-inset-x-2.5'
32
+ },
33
+ vertical: {
34
+ root: 'flex-col',
35
+ // Horizontal rule between stacked panes.
36
+ handle: 'w-full h-1 cursor-row-resize before:inset-x-0 before:-inset-y-2.5'
37
+ }
38
+ },
39
+ // State axes last (house order): a state must dominate the resting look.
40
+ dragging: {
41
+ true: { handle: 'bg-primary' }
42
+ },
43
+ // Declared after `dragging` so its hover-neutralising override wins the
44
+ // `hover:bg` bucket even if a stale `dragging` were ever set together.
45
+ disabled: {
46
+ true: {
47
+ handle: 'cursor-default pointer-events-none hover:bg-border-default'
48
+ }
49
+ }
50
+ },
51
+ defaultVariants: {
52
+ orientation: 'horizontal',
53
+ dragging: false,
54
+ disabled: false
55
+ }
56
+ });