@urbicon-ui/blocks 6.35.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.
- package/dist/components/Chat/Chat/Chat.svelte +53 -0
- package/dist/components/Chat/Chat/Chat.svelte.d.ts +4 -0
- package/dist/components/Chat/Chat/chat.variants.d.ts +20 -0
- package/dist/components/Chat/Chat/chat.variants.js +15 -0
- package/dist/components/Chat/Chat/index.d.ts +47 -0
- package/dist/components/Chat/Chat/index.js +2 -0
- package/dist/components/Chat/ChatMessage/ChatMessage.svelte +342 -0
- package/dist/components/Chat/ChatMessage/ChatMessage.svelte.d.ts +4 -0
- package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +138 -0
- package/dist/components/Chat/ChatMessage/chat-message.variants.js +118 -0
- package/dist/components/Chat/ChatMessage/index.d.ts +102 -0
- package/dist/components/Chat/ChatMessage/index.js +2 -0
- package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte +301 -0
- package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte.d.ts +6 -0
- package/dist/components/Chat/ChatMessageList/chat-message-list.variants.d.ts +23 -0
- package/dist/components/Chat/ChatMessageList/chat-message-list.variants.js +32 -0
- package/dist/components/Chat/ChatMessageList/chat-scroll.d.ts +45 -0
- package/dist/components/Chat/ChatMessageList/chat-scroll.js +52 -0
- package/dist/components/Chat/ChatMessageList/index.d.ts +93 -0
- package/dist/components/Chat/ChatMessageList/index.js +3 -0
- package/dist/components/Chat/CitationChip/CitationChip.svelte +107 -0
- package/dist/components/Chat/CitationChip/CitationChip.svelte.d.ts +4 -0
- package/dist/components/Chat/CitationChip/citation-chip.variants.d.ts +40 -0
- package/dist/components/Chat/CitationChip/citation-chip.variants.js +42 -0
- package/dist/components/Chat/CitationChip/index.d.ts +69 -0
- package/dist/components/Chat/CitationChip/index.js +2 -0
- package/dist/components/Chat/CodeBlock/CodeBlock.svelte +120 -0
- package/dist/components/Chat/CodeBlock/CodeBlock.svelte.d.ts +4 -0
- package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +40 -0
- package/dist/components/Chat/CodeBlock/code-block.variants.js +51 -0
- package/dist/components/Chat/CodeBlock/index.d.ts +55 -0
- package/dist/components/Chat/CodeBlock/index.js +2 -0
- package/dist/components/Chat/PromptInput/PromptInput.svelte +432 -0
- package/dist/components/Chat/PromptInput/PromptInput.svelte.d.ts +4 -0
- package/dist/components/Chat/PromptInput/index.d.ts +165 -0
- package/dist/components/Chat/PromptInput/index.js +2 -0
- package/dist/components/Chat/PromptInput/prompt-input.variants.d.ts +90 -0
- package/dist/components/Chat/PromptInput/prompt-input.variants.js +102 -0
- package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte +107 -0
- package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte.d.ts +4 -0
- package/dist/components/Chat/ReasoningDisclosure/index.d.ts +85 -0
- package/dist/components/Chat/ReasoningDisclosure/index.js +2 -0
- package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.d.ts +20 -0
- package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.js +31 -0
- package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte +99 -0
- package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte.d.ts +9 -0
- package/dist/components/Chat/StreamingMarkdown/MdInline.svelte +82 -0
- package/dist/components/Chat/StreamingMarkdown/MdInline.svelte.d.ts +9 -0
- package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte +118 -0
- package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte.d.ts +4 -0
- package/dist/components/Chat/StreamingMarkdown/index.d.ts +86 -0
- package/dist/components/Chat/StreamingMarkdown/index.js +2 -0
- package/dist/components/Chat/StreamingMarkdown/md-context.d.ts +58 -0
- package/dist/components/Chat/StreamingMarkdown/md-context.js +1 -0
- package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.d.ts +145 -0
- package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.js +73 -0
- package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +174 -0
- package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte.d.ts +4 -0
- package/dist/components/Chat/ToolCallCard/index.d.ts +84 -0
- package/dist/components/Chat/ToolCallCard/index.js +2 -0
- package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +38 -0
- package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +38 -0
- package/dist/components/Chat/chat.types.d.ts +82 -0
- package/dist/components/Chat/chat.types.js +7 -0
- package/dist/components/Chat/index.d.ts +22 -0
- package/dist/components/Chat/index.js +14 -0
- package/dist/components/Chat/markdown/blocks.d.ts +54 -0
- package/dist/components/Chat/markdown/blocks.js +462 -0
- package/dist/components/Chat/markdown/inline.d.ts +31 -0
- package/dist/components/Chat/markdown/inline.js +446 -0
- package/dist/components/Chat/markdown/repair.d.ts +23 -0
- package/dist/components/Chat/markdown/repair.js +337 -0
- package/dist/components/Chat/markdown/streaming-harness.d.ts +23 -0
- package/dist/components/Chat/markdown/streaming-harness.js +62 -0
- package/dist/components/Chat/markdown/types.d.ts +139 -0
- package/dist/components/Chat/markdown/types.js +13 -0
- package/dist/components/Chat/markdown/url-policy.d.ts +33 -0
- package/dist/components/Chat/markdown/url-policy.js +109 -0
- package/dist/components/FileUpload/FileUpload.svelte +35 -123
- package/dist/components/FileUpload/FileUpload.svelte.d.ts +1 -1
- package/dist/components/FileUpload/index.d.ts +2 -28
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/primitives/Collapsible/Collapsible.svelte +5 -0
- package/dist/primitives/SplitPane/SplitPane.svelte +293 -0
- package/dist/primitives/SplitPane/SplitPane.svelte.d.ts +4 -0
- package/dist/primitives/SplitPane/index.d.ts +88 -0
- package/dist/primitives/SplitPane/index.js +2 -0
- package/dist/primitives/SplitPane/split-pane.utils.d.ts +73 -0
- package/dist/primitives/SplitPane/split-pane.utils.js +94 -0
- package/dist/primitives/SplitPane/split-pane.variants.d.ts +40 -0
- package/dist/primitives/SplitPane/split-pane.variants.js +56 -0
- package/dist/primitives/index.d.ts +2 -0
- package/dist/primitives/index.js +1 -0
- package/dist/utils/file-intake.d.ts +78 -0
- package/dist/utils/file-intake.js +144 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +3 -3
|
@@ -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,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,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
|
+
});
|
|
@@ -55,6 +55,8 @@ export type { SliderMark, SliderProps } from './Slider/index.js';
|
|
|
55
55
|
export * from './Slider/index.js';
|
|
56
56
|
export type { SpinnerProps } from './Spinner/index.js';
|
|
57
57
|
export * from './Spinner/index.js';
|
|
58
|
+
export type { SplitPaneProps } from './SplitPane/index.js';
|
|
59
|
+
export * from './SplitPane/index.js';
|
|
58
60
|
export type { StepperProps, StepperStepProps } from './Stepper/index.js';
|
|
59
61
|
export * from './Stepper/index.js';
|
|
60
62
|
export type { TabItemProps, TabPanelProps, TabProps } from './Tab/index.js';
|