@svgrid/grid 2.1.22 → 2.1.23
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/README.md +9 -2
- package/dist/DockNodeView.svelte +306 -0
- package/dist/DockNodeView.svelte.d.ts +7 -0
- package/dist/DockPopoutHost.svelte +17 -0
- package/dist/DockPopoutHost.svelte.d.ts +14 -0
- package/dist/GridMenus.svelte +83 -3
- package/dist/SvAutoComplete.svelte +5 -3
- package/dist/SvCarousel.svelte +20 -42
- package/dist/SvColorInput.svelte +4 -3
- package/dist/SvColorInput.svelte.d.ts +1 -1
- package/dist/SvComboBox.svelte +7 -3
- package/dist/SvCommand.svelte +28 -85
- package/dist/SvCountryInput.svelte +1 -1
- package/dist/SvDateRangeInput.svelte +1 -1
- package/dist/SvDateTimePicker.svelte +9 -3
- package/dist/SvDateTimePicker.svelte.d.ts +3 -0
- package/dist/SvDockLayout.svelte +244 -0
- package/dist/SvDockLayout.svelte.d.ts +77 -0
- package/dist/SvDockManager.svelte +922 -0
- package/dist/SvDockManager.svelte.d.ts +106 -0
- package/dist/SvDrawer.svelte +69 -47
- package/dist/SvDrawer.svelte.d.ts +4 -0
- package/dist/SvDropDownList.svelte +85 -13
- package/dist/SvDropDownList.svelte.d.ts +3 -0
- package/dist/SvField.svelte +21 -4
- package/dist/SvField.svelte.d.ts +1 -0
- package/dist/SvForm.svelte +201 -91
- package/dist/SvForm.svelte.d.ts +15 -22
- package/dist/SvGrid.controller.svelte.d.ts +32 -1
- package/dist/SvGrid.controller.svelte.js +156 -27
- package/dist/SvGrid.css +193 -5
- package/dist/SvGrid.svelte +357 -12
- package/dist/SvGrid.types.d.ts +452 -1
- package/dist/SvGridDropdown.svelte +8 -1
- package/dist/SvGridSelect.svelte +1 -0
- package/dist/SvListBox.svelte +36 -14
- package/dist/SvMaskedInput.svelte +4 -3
- package/dist/SvMaskedInput.svelte.d.ts +1 -1
- package/dist/SvMenu.svelte +1 -1
- package/dist/SvMenuList.svelte +23 -71
- package/dist/SvMenuList.svelte.d.ts +2 -13
- package/dist/SvModal.svelte +12 -29
- package/dist/SvMultiSelect.svelte +1 -0
- package/dist/SvNumberInput.svelte +4 -3
- package/dist/SvNumberInput.svelte.d.ts +1 -1
- package/dist/SvPagination.svelte +18 -18
- package/dist/SvPasswordInput.svelte +3 -2
- package/dist/SvPasswordInput.svelte.d.ts +1 -1
- package/dist/SvPhoneInput.svelte +3 -2
- package/dist/SvPhoneInput.svelte.d.ts +1 -1
- package/dist/SvPopover.svelte +1 -1
- package/dist/SvStepper.svelte +6 -9
- package/dist/SvTagsInput.svelte +3 -2
- package/dist/SvTagsInput.svelte.d.ts +1 -1
- package/dist/SvTooltip.svelte +12 -23
- package/dist/SvTour.svelte +32 -4
- package/dist/SvTreeSelect.svelte +1 -0
- package/dist/build-api.js +76 -54
- package/dist/cdn/svgrid.js +10907 -6444
- package/dist/cdn/svgrid.svelte-external.js +9622 -5475
- package/dist/columns.js +6 -0
- package/dist/conditional-formatting.d.ts +67 -3
- package/dist/conditional-formatting.js +124 -17
- package/dist/createCarousel.svelte.d.ts +72 -0
- package/dist/createCarousel.svelte.js +80 -0
- package/dist/createCommand.svelte.d.ts +62 -0
- package/dist/createCommand.svelte.js +109 -0
- package/dist/createForm.svelte.d.ts +62 -0
- package/dist/createForm.svelte.js +374 -0
- package/dist/createMenu.svelte.d.ts +88 -0
- package/dist/createMenu.svelte.js +153 -0
- package/dist/createOverlay.svelte.d.ts +38 -0
- package/dist/createOverlay.svelte.js +68 -0
- package/dist/createPagination.d.ts +74 -0
- package/dist/createPagination.js +71 -0
- package/dist/createPopoverSelect.svelte.js +6 -12
- package/dist/createStepper.d.ts +44 -0
- package/dist/createStepper.js +26 -0
- package/dist/createTooltip.svelte.d.ts +32 -0
- package/dist/createTooltip.svelte.js +61 -0
- package/dist/datetime/timezone.d.ts +36 -0
- package/dist/datetime/timezone.js +110 -0
- package/dist/dock-context.d.ts +52 -0
- package/dist/dock-context.js +1 -0
- package/dist/dock-manager-model.d.ts +136 -0
- package/dist/dock-manager-model.js +514 -0
- package/dist/dock-model.d.ts +105 -0
- package/dist/dock-model.js +288 -0
- package/dist/dock-popout.d.ts +30 -0
- package/dist/dock-popout.js +81 -0
- package/dist/editor-contract.d.ts +2 -0
- package/dist/filter-operators.d.ts +6 -0
- package/dist/filter-operators.js +35 -2
- package/dist/filtering/excel-filters.d.ts +18 -1
- package/dist/filtering/excel-filters.js +56 -0
- package/dist/form-field.d.ts +73 -0
- package/dist/form-field.js +15 -0
- package/dist/index.d.ts +25 -5
- package/dist/index.js +40 -4
- package/dist/list-nav.d.ts +18 -0
- package/dist/list-nav.js +30 -0
- package/dist/list-option.d.ts +2 -0
- package/dist/menu-item.d.ts +20 -0
- package/dist/menu-item.js +1 -0
- package/dist/menus.d.ts +5 -0
- package/dist/menus.js +68 -16
- package/dist/panel-resize.d.ts +25 -0
- package/dist/panel-resize.js +40 -0
- package/dist/popover.d.ts +29 -3
- package/dist/popover.js +22 -6
- package/dist/recurrence.d.ts +33 -7
- package/dist/recurrence.js +151 -13
- package/dist/row-drag.d.ts +8 -0
- package/dist/row-drag.js +11 -0
- package/dist/scheduler-ical.d.ts +30 -0
- package/dist/scheduler-ical.js +180 -0
- package/dist/scheduler-model.d.ts +269 -0
- package/dist/scheduler-model.js +602 -0
- package/dist/scheduler-view.svelte.d.ts +22 -0
- package/dist/scheduler-view.svelte.js +13 -0
- package/dist/spreadsheet.js +20 -12
- package/dist/ui-app.types.d.ts +3 -0
- package/package.json +5 -1
- package/src/DockNodeView.svelte +306 -0
- package/src/DockPopoutHost.svelte +17 -0
- package/src/GridMenus.svelte +83 -3
- package/src/SvAutoComplete.svelte +5 -3
- package/src/SvCarousel.svelte +20 -42
- package/src/SvCarousel.test.ts +68 -0
- package/src/SvColorInput.svelte +4 -3
- package/src/SvComboBox.svelte +7 -3
- package/src/SvCommand.svelte +28 -85
- package/src/SvCommand.test.ts +78 -0
- package/src/SvCountryInput.svelte +1 -1
- package/src/SvDateRangeInput.svelte +1 -1
- package/src/SvDateTimePicker.svelte +9 -3
- package/src/SvDockLayout.svelte +244 -0
- package/src/SvDockManager.svelte +922 -0
- package/src/SvDrawer.svelte +69 -47
- package/src/SvDropDownList.svelte +85 -13
- package/src/SvField.svelte +21 -4
- package/src/SvForm.svelte +201 -91
- package/src/SvForm.test.ts +299 -0
- package/src/SvGrid.controller.svelte.ts +152 -28
- package/src/SvGrid.css +193 -5
- package/src/SvGrid.svelte +357 -12
- package/src/SvGrid.types.ts +463 -1
- package/src/SvGridDropdown.svelte +8 -1
- package/src/SvGridSelect.svelte +1 -0
- package/src/SvListBox.svelte +36 -14
- package/src/SvMaskedInput.svelte +4 -3
- package/src/SvMenu.svelte +1 -1
- package/src/SvMenu.test.ts +24 -0
- package/src/SvMenuList.svelte +23 -71
- package/src/SvModal.svelte +12 -29
- package/src/SvModal.test.ts +82 -0
- package/src/SvMultiSelect.svelte +1 -0
- package/src/SvNumberInput.svelte +4 -3
- package/src/SvPagination.svelte +18 -18
- package/src/SvPasswordInput.svelte +3 -2
- package/src/SvPhoneInput.svelte +3 -2
- package/src/SvPopover.svelte +1 -1
- package/src/SvStepper.svelte +6 -9
- package/src/SvTagsInput.svelte +3 -2
- package/src/SvTooltip.svelte +12 -23
- package/src/SvTooltip.test.ts +73 -0
- package/src/SvTour.svelte +32 -4
- package/src/SvTreeSelect.svelte +1 -0
- package/src/build-api.ts +74 -51
- package/src/columns.ts +5 -0
- package/src/conditional-formatting.test.ts +104 -0
- package/src/conditional-formatting.ts +176 -19
- package/src/createCarousel.svelte.ts +104 -0
- package/src/createCommand.svelte.ts +125 -0
- package/src/createForm.svelte.ts +333 -0
- package/src/createMenu.svelte.ts +202 -0
- package/src/createOverlay.svelte.ts +101 -0
- package/src/createPagination.test.ts +65 -0
- package/src/createPagination.ts +84 -0
- package/src/createPopoverSelect.svelte.ts +5 -9
- package/src/createStepper.test.ts +62 -0
- package/src/createStepper.ts +61 -0
- package/src/createTooltip.svelte.ts +86 -0
- package/src/datetime/timezone.test.ts +69 -0
- package/src/datetime/timezone.ts +134 -0
- package/src/dock-context.ts +47 -0
- package/src/dock-manager-model.test.ts +275 -0
- package/src/dock-manager-model.ts +593 -0
- package/src/dock-model.test.ts +163 -0
- package/src/dock-model.ts +368 -0
- package/src/dock-popout.ts +94 -0
- package/src/editor-contract.ts +2 -0
- package/src/filter-operators.test.ts +59 -0
- package/src/filter-operators.ts +35 -2
- package/src/filtering/excel-filters.test.ts +86 -1
- package/src/filtering/excel-filters.ts +65 -0
- package/src/form-field.ts +87 -0
- package/src/index.ts +158 -4
- package/src/list-nav.test.ts +49 -0
- package/src/list-nav.ts +29 -0
- package/src/list-option.ts +2 -0
- package/src/menu-item.ts +21 -0
- package/src/menus.test.ts +48 -0
- package/src/menus.ts +73 -16
- package/src/panel-resize.ts +52 -0
- package/src/popover.bounds.test.ts +93 -0
- package/src/popover.ts +51 -7
- package/src/recurrence.test.ts +88 -1
- package/src/recurrence.ts +166 -15
- package/src/row-drag.ts +10 -0
- package/src/scheduler-ical.test.ts +68 -0
- package/src/scheduler-ical.ts +180 -0
- package/src/scheduler-model.test.ts +546 -0
- package/src/scheduler-model.ts +853 -0
- package/src/scheduler-view.svelte.ts +33 -0
- package/src/spreadsheet.ts +20 -12
- package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
- package/src/ui-app.types.ts +3 -0
- package/src/virtual.test.ts +6 -4
package/README.md
CHANGED
|
@@ -118,9 +118,16 @@ Compatibility aliases are provided for the framework-neutral names: `createGrid`
|
|
|
118
118
|
|
|
119
119
|
SvGrid publishes an [MCP server](https://www.npmjs.com/package/@svgrid/mcp) and `llms.txt` / `llms-full.txt`, so Claude, Cursor, and Zed answer with accurate, version-pinned APIs and all 280+ demo sources as context - not hallucinated methods.
|
|
120
120
|
|
|
121
|
-
## Enterprise
|
|
121
|
+
## Enterprise features
|
|
122
122
|
|
|
123
|
-
The MIT core is free for any use, including commercial. The optional [`@svgrid/enterprise`](https://www.npmjs.com/package/@svgrid/enterprise) pack
|
|
123
|
+
The MIT core is free for any use, including commercial, with **no feature gating** - sorting, filtering, editing, virtualization, and the **server-side row model** (sort / filter / group pushdown) all ship in the free package. The optional [`@svgrid/enterprise`](https://www.npmjs.com/package/@svgrid/enterprise) pack layers on the export- and analytics-heavy features teams tend to need last:
|
|
124
|
+
|
|
125
|
+
- **Excel / PDF / CSV / TSV / HTML export** plus paginated print.
|
|
126
|
+
- **Pivot tables** with a drag-and-drop pivot Designer.
|
|
127
|
+
- **Scheduler / calendar view** and Kanban roll-ups for larger deployments.
|
|
128
|
+
- **AI helpers** and SvGrid Studio for data-app scaffolding.
|
|
129
|
+
|
|
130
|
+
OSS projects receive an Enterprise key free. See the [Enterprise docs](https://svgrid.com/docs) and [Pricing](https://svgrid.com/pricing).
|
|
124
131
|
|
|
125
132
|
## Documentation
|
|
126
133
|
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DockNodeView - the recursive renderer for one node of a <SvDockLayout> tree.
|
|
4
|
+
* A `group` becomes a flex row/column of children with draggable splitters
|
|
5
|
+
* between them; a `tabs` leaf becomes a tab strip over the active pane's
|
|
6
|
+
* content. All tree edits go through the pure `dock-model`; this file only
|
|
7
|
+
* paints and wires gestures. It renders itself for nested groups.
|
|
8
|
+
*/
|
|
9
|
+
import { getContext } from 'svelte'
|
|
10
|
+
import Self from './DockNodeView.svelte'
|
|
11
|
+
import { DOCK_CONTEXT, type DockContext } from './dock-context'
|
|
12
|
+
import { leafMinSize, type DockNode, type DockGroup } from './dock-model'
|
|
13
|
+
|
|
14
|
+
let { node }: { node: DockNode } = $props()
|
|
15
|
+
|
|
16
|
+
const ctx = getContext<DockContext>(DOCK_CONTEXT)
|
|
17
|
+
|
|
18
|
+
// ---- splitter drag (group children resize) ----------------------------
|
|
19
|
+
let groupEl = $state<HTMLElement | null>(null)
|
|
20
|
+
|
|
21
|
+
/** A child's minimum size (px): a leaf's largest per-pane `minSize`, else 0. */
|
|
22
|
+
function nodeMin(n: DockNode | undefined): number {
|
|
23
|
+
return n && n.type === 'tabs' ? leafMinSize(n) : 0
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function onSplitterDown(e: PointerEvent, group: DockGroup, index: number) {
|
|
27
|
+
if (e.button !== 0 || !groupEl) return
|
|
28
|
+
e.preventDefault()
|
|
29
|
+
const vertical = group.direction === 'column'
|
|
30
|
+
const rect = groupEl.getBoundingClientRect()
|
|
31
|
+
const total = vertical ? rect.height : rect.width
|
|
32
|
+
if (total <= 0) return
|
|
33
|
+
const startSizes = group.sizes.slice()
|
|
34
|
+
const startPos = vertical ? e.clientY : e.clientX
|
|
35
|
+
// Only the two panes adjacent to this gutter move; the rest stay put. Each
|
|
36
|
+
// honors its own minimum (a leaf's largest per-pane `minSize`, else global).
|
|
37
|
+
const a = index
|
|
38
|
+
const b = index + 1
|
|
39
|
+
const minA = Math.max(ctx.minSize(), nodeMin(group.children[a])) / total
|
|
40
|
+
const minB = Math.max(ctx.minSize(), nodeMin(group.children[b])) / total
|
|
41
|
+
const pairTotal = startSizes[a]! + startSizes[b]!
|
|
42
|
+
|
|
43
|
+
const onMove = (ev: PointerEvent) => {
|
|
44
|
+
const delta = ((vertical ? ev.clientY : ev.clientX) - startPos) / total
|
|
45
|
+
let fa = Math.min(Math.max(startSizes[a]! + delta, minA), pairTotal - minB)
|
|
46
|
+
const next = startSizes.slice()
|
|
47
|
+
next[a] = fa
|
|
48
|
+
next[b] = pairTotal - fa
|
|
49
|
+
ctx.resize(group.id, next)
|
|
50
|
+
}
|
|
51
|
+
const onUp = () => {
|
|
52
|
+
window.removeEventListener('pointermove', onMove)
|
|
53
|
+
window.removeEventListener('pointerup', onUp)
|
|
54
|
+
document.body.style.userSelect = ''
|
|
55
|
+
document.body.style.cursor = ''
|
|
56
|
+
}
|
|
57
|
+
document.body.style.userSelect = 'none'
|
|
58
|
+
document.body.style.cursor = vertical ? 'row-resize' : 'col-resize'
|
|
59
|
+
window.addEventListener('pointermove', onMove)
|
|
60
|
+
window.addEventListener('pointerup', onUp)
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
{#if node.type === 'group'}
|
|
65
|
+
<div
|
|
66
|
+
class="sv-dock__group sv-dock__group--{node.direction}"
|
|
67
|
+
bind:this={groupEl}
|
|
68
|
+
role="group"
|
|
69
|
+
>
|
|
70
|
+
{#each node.children as child, i (child.id)}
|
|
71
|
+
<div class="sv-dock__cell" style:flex-grow={node.sizes[i] ?? 1} style:flex-basis="0">
|
|
72
|
+
<Self node={child} />
|
|
73
|
+
</div>
|
|
74
|
+
{#if i < node.children.length - 1}
|
|
75
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
76
|
+
<div
|
|
77
|
+
class="sv-dock__splitter sv-dock__splitter--{node.direction}"
|
|
78
|
+
role="separator"
|
|
79
|
+
aria-orientation={node.direction === 'column' ? 'horizontal' : 'vertical'}
|
|
80
|
+
onpointerdown={(e) => onSplitterDown(e, node as DockGroup, i)}
|
|
81
|
+
></div>
|
|
82
|
+
{/if}
|
|
83
|
+
{/each}
|
|
84
|
+
</div>
|
|
85
|
+
{:else}
|
|
86
|
+
{@const active = node.panes[node.active] ?? node.panes[0]}
|
|
87
|
+
{@const reorder = ctx.reorderTarget?.() ?? null}
|
|
88
|
+
{@const reorderHere = reorder && reorder.tabsId === node.id ? reorder.index : -1}
|
|
89
|
+
{@const hp = ctx.headerPosition?.() ?? 'top'}
|
|
90
|
+
<!-- A single-pane leaf in an auto-hide fly-out drops its (redundant with the edge strip)
|
|
91
|
+
tab button, leaving just the leaf actions (pin). -->
|
|
92
|
+
{@const bareSingle = !!ctx.hideSingleTab?.() && node.panes.length === 1}
|
|
93
|
+
<div class="sv-dock__leaf sv-dock__leaf--h-{hp}" class:is-focused={ctx.focusedLeaf?.() === node.id} data-dock-tabs={node.id}>
|
|
94
|
+
<div class="sv-dock__tabstrip" class:is-bare={bareSingle} role="tablist">
|
|
95
|
+
{#if !bareSingle}
|
|
96
|
+
{#each node.panes as p, i (p.id)}
|
|
97
|
+
{#if reorderHere === i}<div class="sv-dock__ins" aria-hidden="true"></div>{/if}
|
|
98
|
+
<div
|
|
99
|
+
class="sv-dock__tab"
|
|
100
|
+
class:is-active={i === node.active}
|
|
101
|
+
role="tab"
|
|
102
|
+
aria-selected={i === node.active}
|
|
103
|
+
tabindex={i === node.active ? 0 : -1}
|
|
104
|
+
data-dock-tab={p.id}
|
|
105
|
+
data-tab-index={i}
|
|
106
|
+
onpointerdown={(e) => {
|
|
107
|
+
if (e.button === 0) { ctx.activate(node.id, i); ctx.beginDrag(e, p.id, node.id) }
|
|
108
|
+
}}
|
|
109
|
+
onkeydown={(e) => {
|
|
110
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); ctx.activate(node.id, i) }
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<span class="sv-dock__tab-label">{p.title}</span>
|
|
114
|
+
{#if p.closable !== false}
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
class="sv-dock__tab-close"
|
|
118
|
+
aria-label={`Close ${p.title}`}
|
|
119
|
+
title="Close"
|
|
120
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
121
|
+
onclick={(e) => { e.stopPropagation(); ctx.close(p.id) }}
|
|
122
|
+
>×</button>
|
|
123
|
+
{/if}
|
|
124
|
+
</div>
|
|
125
|
+
{/each}
|
|
126
|
+
{#if reorderHere === node.panes.length}<div class="sv-dock__ins" aria-hidden="true"></div>{/if}
|
|
127
|
+
{/if}
|
|
128
|
+
{#if ctx.leafActions}
|
|
129
|
+
<div class="sv-dock__leafctl">{@render ctx.leafActions(node)}</div>
|
|
130
|
+
{/if}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{#if ctx.keepAlive?.()}
|
|
134
|
+
<!-- Keep every tab mounted; hide the inactive ones so their state persists. -->
|
|
135
|
+
{#each node.panes as p (p.id)}
|
|
136
|
+
<div class="sv-dock__content" role="tabpanel" style:display={p.id === active?.id ? '' : 'none'}>
|
|
137
|
+
{@render ctx.pane(p)}
|
|
138
|
+
</div>
|
|
139
|
+
{/each}
|
|
140
|
+
{:else}
|
|
141
|
+
<!-- Only the active tab renders; `{#key}` remounts on switch so a pane's
|
|
142
|
+
content never bleeds into the next (use keepAlive to persist state). -->
|
|
143
|
+
{#if active}
|
|
144
|
+
{#key active.id}
|
|
145
|
+
<div class="sv-dock__content" role="tabpanel">{@render ctx.pane(active)}</div>
|
|
146
|
+
{/key}
|
|
147
|
+
{/if}
|
|
148
|
+
{/if}
|
|
149
|
+
|
|
150
|
+
{#if ctx.dropTarget()?.tabsId === node.id}
|
|
151
|
+
{@const dt = ctx.dropTarget()!}
|
|
152
|
+
{@const zone = dt.zone}
|
|
153
|
+
{@const centerOnly = dt.centerOnly === true}
|
|
154
|
+
<div class="sv-dock__dropzone" aria-hidden="true">
|
|
155
|
+
<!-- Preview: the region the panel will occupy (only when over a chip). -->
|
|
156
|
+
{#if zone}<div class="sv-dock__drop-hint sv-dock__drop-hint--{zone}"></div>{/if}
|
|
157
|
+
<!-- Guide: dock targets. Drop ON a chip to dock; off it, the tab floats. -->
|
|
158
|
+
<div class="sv-dock__guide" class:is-center-only={centerOnly}>
|
|
159
|
+
{#if !centerOnly}
|
|
160
|
+
<div class="sv-dock__guide-cell sv-dock__guide-cell--top" class:is-on={zone === 'top'}></div>
|
|
161
|
+
{/if}
|
|
162
|
+
<div class="sv-dock__guide-row">
|
|
163
|
+
{#if !centerOnly}
|
|
164
|
+
<div class="sv-dock__guide-cell sv-dock__guide-cell--left" class:is-on={zone === 'left'}></div>
|
|
165
|
+
{/if}
|
|
166
|
+
<div class="sv-dock__guide-cell sv-dock__guide-cell--center" class:is-on={zone === 'center'}></div>
|
|
167
|
+
{#if !centerOnly}
|
|
168
|
+
<div class="sv-dock__guide-cell sv-dock__guide-cell--right" class:is-on={zone === 'right'}></div>
|
|
169
|
+
{/if}
|
|
170
|
+
</div>
|
|
171
|
+
{#if !centerOnly}
|
|
172
|
+
<div class="sv-dock__guide-cell sv-dock__guide-cell--bottom" class:is-on={zone === 'bottom'}></div>
|
|
173
|
+
{/if}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
{/if}
|
|
177
|
+
</div>
|
|
178
|
+
{/if}
|
|
179
|
+
|
|
180
|
+
<style>
|
|
181
|
+
.sv-dock__group { display: flex; width: 100%; height: 100%; min-width: 0; min-height: 0; }
|
|
182
|
+
.sv-dock__group--row { flex-direction: row; }
|
|
183
|
+
.sv-dock__group--column { flex-direction: column; }
|
|
184
|
+
.sv-dock__cell { min-width: 0; min-height: 0; overflow: hidden; display: flex; }
|
|
185
|
+
.sv-dock__cell > :global(*) { flex: 1; min-width: 0; min-height: 0; }
|
|
186
|
+
|
|
187
|
+
.sv-dock__splitter { flex: none; background: var(--sg-border, #e2e8f0); position: relative; touch-action: none; }
|
|
188
|
+
.sv-dock__splitter::after { content: ''; position: absolute; inset: -3px; }
|
|
189
|
+
.sv-dock__splitter--row { width: 4px; cursor: col-resize; }
|
|
190
|
+
.sv-dock__splitter--column { height: 4px; cursor: row-resize; }
|
|
191
|
+
.sv-dock__splitter:hover { background: var(--sg-accent, #2563eb); }
|
|
192
|
+
|
|
193
|
+
.sv-dock__leaf {
|
|
194
|
+
position: relative; display: flex; flex-direction: column; width: 100%; height: 100%;
|
|
195
|
+
min-width: 0; min-height: 0; background: var(--sg-bg, #fff);
|
|
196
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 8px; overflow: hidden;
|
|
197
|
+
}
|
|
198
|
+
.sv-dock__tabstrip {
|
|
199
|
+
display: flex; flex: none; gap: 2px; padding: 4px 4px 0; overflow-x: auto;
|
|
200
|
+
background: var(--sg-header-bg, #f6f7f9); border-bottom: 1px solid var(--sg-border, #e2e8f0);
|
|
201
|
+
}
|
|
202
|
+
/* Focused leaf: accent-tinted header + active-tab underline. */
|
|
203
|
+
.sv-dock__leaf.is-focused > .sv-dock__tabstrip { background: color-mix(in srgb, var(--sg-accent, #2563eb) 8%, var(--sg-header-bg, #f6f7f9)); }
|
|
204
|
+
.sv-dock__leaf.is-focused .sv-dock__tab.is-active { box-shadow: inset 0 -2px 0 var(--sg-accent, #2563eb); }
|
|
205
|
+
/* Header on the bottom: content first, strip below with flipped chrome. */
|
|
206
|
+
.sv-dock__leaf--h-bottom { flex-direction: column-reverse; }
|
|
207
|
+
.sv-dock__leaf--h-bottom .sv-dock__tabstrip { padding: 0 4px 4px; border-bottom: none; border-top: 1px solid var(--sg-border, #e2e8f0); }
|
|
208
|
+
.sv-dock__leaf--h-bottom .sv-dock__tab { border-radius: 0 0 7px 7px; border-top: none; border-bottom: 1px solid transparent; }
|
|
209
|
+
.sv-dock__leaf--h-bottom .sv-dock__tab.is-active { border-color: var(--sg-border, #e2e8f0); border-top: none; }
|
|
210
|
+
|
|
211
|
+
/* Header on the left/right: a VERTICAL tab strip beside the content. */
|
|
212
|
+
.sv-dock__leaf--h-left { flex-direction: row; }
|
|
213
|
+
.sv-dock__leaf--h-right { flex-direction: row-reverse; }
|
|
214
|
+
.sv-dock__leaf--h-left .sv-dock__tabstrip,
|
|
215
|
+
.sv-dock__leaf--h-right .sv-dock__tabstrip {
|
|
216
|
+
flex-direction: column; overflow-x: hidden; overflow-y: auto; padding: 4px 0 4px 4px;
|
|
217
|
+
border-bottom: none;
|
|
218
|
+
}
|
|
219
|
+
.sv-dock__leaf--h-left .sv-dock__tabstrip { border-right: 1px solid var(--sg-border, #e2e8f0); }
|
|
220
|
+
.sv-dock__leaf--h-right .sv-dock__tabstrip { padding: 4px 4px 4px 0; border-left: 1px solid var(--sg-border, #e2e8f0); }
|
|
221
|
+
.sv-dock__leaf--h-left .sv-dock__tab,
|
|
222
|
+
.sv-dock__leaf--h-right .sv-dock__tab {
|
|
223
|
+
writing-mode: vertical-rl; padding: 10px 6px; border-radius: 7px 0 0 7px; border-bottom: 1px solid transparent;
|
|
224
|
+
max-height: 200px; overflow: hidden;
|
|
225
|
+
}
|
|
226
|
+
.sv-dock__leaf--h-right .sv-dock__tab { border-radius: 0 7px 7px 0; }
|
|
227
|
+
.sv-dock__leaf--h-left .sv-dock__tab.is-active,
|
|
228
|
+
.sv-dock__leaf--h-right .sv-dock__tab.is-active { border-color: var(--sg-border, #e2e8f0); }
|
|
229
|
+
/* Stack controls go to the bottom of a vertical strip. */
|
|
230
|
+
.sv-dock__leaf--h-left .sv-dock__leafctl,
|
|
231
|
+
.sv-dock__leaf--h-right .sv-dock__leafctl { flex-direction: column; margin-inline-start: 0; margin-top: auto; padding: 2px 0; align-self: center; }
|
|
232
|
+
/* Reorder insertion line runs horizontally between stacked tabs. */
|
|
233
|
+
.sv-dock__leaf--h-left .sv-dock__ins,
|
|
234
|
+
.sv-dock__leaf--h-right .sv-dock__ins { width: auto; height: 3px; align-self: stretch; margin: 0 3px; }
|
|
235
|
+
.sv-dock__tab {
|
|
236
|
+
display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; cursor: grab;
|
|
237
|
+
font-size: 12.5px; font-weight: 600; color: var(--sg-muted, #64748b); white-space: nowrap;
|
|
238
|
+
border: 1px solid transparent; border-bottom: none; border-radius: 7px 7px 0 0; user-select: none;
|
|
239
|
+
touch-action: none;
|
|
240
|
+
}
|
|
241
|
+
.sv-dock__tab:hover { color: var(--sg-fg, #0f172a); }
|
|
242
|
+
.sv-dock__tab.is-active {
|
|
243
|
+
color: var(--sg-fg, #0f172a); background: var(--sg-bg, #fff);
|
|
244
|
+
border-color: var(--sg-border, #e2e8f0);
|
|
245
|
+
}
|
|
246
|
+
.sv-dock__tab-close {
|
|
247
|
+
display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px;
|
|
248
|
+
padding: 0; font-size: 15px; line-height: 1; color: var(--sg-muted, #94a3b8); cursor: pointer;
|
|
249
|
+
background: none; border: none; border-radius: 4px;
|
|
250
|
+
}
|
|
251
|
+
.sv-dock__tab-close:hover { background: var(--sg-row-hover-bg, #f1f5f9); color: var(--sg-danger, #dc2626); }
|
|
252
|
+
/* Stack-header controls, pinned to the right of the tab strip. */
|
|
253
|
+
.sv-dock__leafctl { margin-inline-start: auto; align-self: center; display: flex; align-items: center; gap: 1px; padding: 0 2px 3px; }
|
|
254
|
+
.sv-dock__content { flex: 1; min-height: 0; overflow: auto; }
|
|
255
|
+
|
|
256
|
+
/* Tab-reorder insertion line. */
|
|
257
|
+
.sv-dock__ins {
|
|
258
|
+
flex: none; align-self: stretch; width: 3px; margin: 3px 0; border-radius: 2px;
|
|
259
|
+
background: var(--sg-accent, #2563eb); box-shadow: 0 0 0 1px color-mix(in srgb, var(--sg-accent, #2563eb) 40%, transparent);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* Drop-zone indicators shown on the leaf under the pointer while dragging. */
|
|
263
|
+
.sv-dock__dropzone { position: absolute; inset: 0; pointer-events: none; z-index: 6; }
|
|
264
|
+
/* Ring around the whole targeted leaf, so which pane you're over is obvious. */
|
|
265
|
+
.sv-dock__dropzone::before {
|
|
266
|
+
content: ''; position: absolute; inset: 2px; border-radius: 7px;
|
|
267
|
+
box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--sg-accent, #2563eb) 32%, transparent);
|
|
268
|
+
}
|
|
269
|
+
.sv-dock__drop-hint {
|
|
270
|
+
position: absolute; background: color-mix(in srgb, var(--sg-accent, #2563eb) 22%, transparent);
|
|
271
|
+
border: 2px solid var(--sg-accent, #2563eb); border-radius: 6px; transition: inset 90ms ease;
|
|
272
|
+
}
|
|
273
|
+
.sv-dock__drop-hint--center { inset: 8px; }
|
|
274
|
+
.sv-dock__drop-hint--left { inset: 4px 50% 4px 4px; }
|
|
275
|
+
.sv-dock__drop-hint--right { inset: 4px 4px 4px 50%; }
|
|
276
|
+
.sv-dock__drop-hint--top { inset: 4px 4px 50% 4px; }
|
|
277
|
+
.sv-dock__drop-hint--bottom { inset: 50% 4px 4px 4px; }
|
|
278
|
+
|
|
279
|
+
/* The 5-direction dock guide (VS-style): a plus of targets, active one lit. */
|
|
280
|
+
.sv-dock__guide {
|
|
281
|
+
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
|
|
282
|
+
display: flex; flex-direction: column; align-items: center; gap: 4px;
|
|
283
|
+
padding: 6px; border-radius: 10px;
|
|
284
|
+
background: color-mix(in srgb, var(--sg-bg, #fff) 82%, transparent);
|
|
285
|
+
box-shadow: 0 6px 20px -6px rgba(15,23,42,0.45); border: 1px solid var(--sg-border, #e2e8f0);
|
|
286
|
+
}
|
|
287
|
+
.sv-dock__guide-row { display: flex; gap: 4px; }
|
|
288
|
+
.sv-dock__guide-cell {
|
|
289
|
+
width: 26px; height: 26px; border-radius: 6px; position: relative;
|
|
290
|
+
background: var(--sg-header-bg, #f1f5f9); border: 1px solid var(--sg-border, #cbd5e1);
|
|
291
|
+
}
|
|
292
|
+
.sv-dock__guide-cell::after {
|
|
293
|
+
content: ''; position: absolute; border-radius: 3px;
|
|
294
|
+
background: color-mix(in srgb, var(--sg-accent, #2563eb) 55%, transparent);
|
|
295
|
+
}
|
|
296
|
+
.sv-dock__guide-cell--center::after { inset: 5px; }
|
|
297
|
+
.sv-dock__guide-cell--left::after { inset: 5px 13px 5px 5px; }
|
|
298
|
+
.sv-dock__guide-cell--right::after { inset: 5px 5px 5px 13px; }
|
|
299
|
+
.sv-dock__guide-cell--top::after { inset: 5px 5px 13px 5px; }
|
|
300
|
+
.sv-dock__guide-cell--bottom::after { inset: 13px 5px 5px 5px; }
|
|
301
|
+
.sv-dock__guide-cell.is-on {
|
|
302
|
+
background: color-mix(in srgb, var(--sg-accent, #2563eb) 18%, var(--sg-bg, #fff));
|
|
303
|
+
border-color: var(--sg-accent, #2563eb);
|
|
304
|
+
}
|
|
305
|
+
.sv-dock__guide-cell.is-on::after { background: var(--sg-accent, #2563eb); }
|
|
306
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DockPopoutHost - the tiny root mounted into a popped-out browser window. It
|
|
4
|
+
* just renders the manager's `pane` snippet for the popped pane, so a pop-out
|
|
5
|
+
* shows the exact same content as the docked view.
|
|
6
|
+
*/
|
|
7
|
+
import type { Snippet } from 'svelte'
|
|
8
|
+
import type { DockPane } from './dock-model'
|
|
9
|
+
|
|
10
|
+
let { pane, paneData }: { pane: Snippet<[DockPane]>; paneData: DockPane } = $props()
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div class="sv-popout-host">{@render pane(paneData)}</div>
|
|
14
|
+
|
|
15
|
+
<style>
|
|
16
|
+
.sv-popout-host { width: 100%; height: 100vh; overflow: auto; box-sizing: border-box; }
|
|
17
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DockPopoutHost - the tiny root mounted into a popped-out browser window. It
|
|
3
|
+
* just renders the manager's `pane` snippet for the popped pane, so a pop-out
|
|
4
|
+
* shows the exact same content as the docked view.
|
|
5
|
+
*/
|
|
6
|
+
import type { Snippet } from 'svelte';
|
|
7
|
+
import type { DockPane } from './dock-model';
|
|
8
|
+
type $$ComponentProps = {
|
|
9
|
+
pane: Snippet<[DockPane]>;
|
|
10
|
+
paneData: DockPane;
|
|
11
|
+
};
|
|
12
|
+
declare const DockPopoutHost: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type DockPopoutHost = ReturnType<typeof DockPopoutHost>;
|
|
14
|
+
export default DockPopoutHost;
|
package/dist/GridMenus.svelte
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
applyExcelFilter,
|
|
4
4
|
normalizeForFilter,
|
|
5
|
+
splitInTokens,
|
|
5
6
|
createColumnVirtualizer,
|
|
6
7
|
createCoreRowModel,
|
|
7
8
|
createExpandedRowModel,
|
|
@@ -124,6 +125,12 @@
|
|
|
124
125
|
const operatorsForColumn = $derived(ctrl.operatorsForColumn);
|
|
125
126
|
const defaultOperatorFor = $derived(ctrl.defaultOperatorFor);
|
|
126
127
|
const operatorLabelFor = $derived(ctrl.operatorLabelFor);
|
|
128
|
+
const filterRowValues = $derived(ctrl.filterRowValues);
|
|
129
|
+
const inSuggestFor = $derived(ctrl.inSuggestFor);
|
|
130
|
+
const inSuggestPos = $derived(ctrl.inSuggestPos);
|
|
131
|
+
const inSuggestValues = $derived(ctrl.inSuggestValues);
|
|
132
|
+
const toggleFilterToken = $derived(ctrl.toggleFilterToken);
|
|
133
|
+
const facetValuesForColumn = $derived(ctrl.facetValuesForColumn);
|
|
127
134
|
|
|
128
135
|
// Patch a column's menu filter (used for the optional 2nd condition + join
|
|
129
136
|
// of multi-condition filtering). Merges into the existing entry, creating a
|
|
@@ -135,6 +142,22 @@
|
|
|
135
142
|
}
|
|
136
143
|
const isColumnFiltered = $derived(ctrl.isColumnFiltered);
|
|
137
144
|
const closeMenus = $derived(ctrl.closeMenus);
|
|
145
|
+
|
|
146
|
+
// Column/filter/operator menus are position:fixed and anchored to their
|
|
147
|
+
// trigger by a one-time measurement taken when they open. On scroll they'd
|
|
148
|
+
// otherwise detach and float over the wrong place (#88), so close them on any
|
|
149
|
+
// scroll (capture phase, to catch inner scroll containers) or resize.
|
|
150
|
+
$effect(() => {
|
|
151
|
+
if (!(columnMenuFor || filterMenuFor || operatorMenuFor || chooseColumnsPos)) return;
|
|
152
|
+
const close = () => closeMenus();
|
|
153
|
+
window.addEventListener('scroll', close, true);
|
|
154
|
+
window.addEventListener('resize', close);
|
|
155
|
+
return () => {
|
|
156
|
+
window.removeEventListener('scroll', close, true);
|
|
157
|
+
window.removeEventListener('resize', close);
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
|
|
138
161
|
const autosizeColumn = $derived(ctrl.autosizeColumn);
|
|
139
162
|
const autosizeAllColumns = $derived(ctrl.autosizeAllColumns);
|
|
140
163
|
const resetColumns = $derived(ctrl.resetColumns);
|
|
@@ -202,14 +225,30 @@
|
|
|
202
225
|
<option value={option.value}>{operatorLabelFor(option, menuColumn)}</option>
|
|
203
226
|
{/each}
|
|
204
227
|
</select>
|
|
205
|
-
{#if menuActiveOperator !== "isBlank"}
|
|
228
|
+
{#if menuActiveOperator !== "isBlank" && menuActiveOperator !== "isNotBlank"}
|
|
229
|
+
{@const isSetOp =
|
|
230
|
+
menuActiveOperator === "in" || menuActiveOperator === "notIn"}
|
|
206
231
|
<input
|
|
207
232
|
class="sv-grid-menu-condition-value"
|
|
208
|
-
type={menuInputType}
|
|
233
|
+
type={menuActiveOperator === "regex" ? "text" : menuInputType}
|
|
234
|
+
list={isSetOp ? `sv-in-list-${colId}` : undefined}
|
|
209
235
|
value={filterMenuValues[colId]?.value ?? ""}
|
|
210
|
-
placeholder={menuActiveOperator === "between"
|
|
236
|
+
placeholder={menuActiveOperator === "between"
|
|
237
|
+
? "From"
|
|
238
|
+
: isSetOp
|
|
239
|
+
? "value, value..."
|
|
240
|
+
: menuActiveOperator === "regex"
|
|
241
|
+
? "Pattern..."
|
|
242
|
+
: "Filter value..."}
|
|
211
243
|
oninput={(event) => updateFilterMenuValue(colId, (event.currentTarget as HTMLInputElement).value)}
|
|
212
244
|
/>
|
|
245
|
+
{#if isSetOp}
|
|
246
|
+
<datalist id={`sv-in-list-${colId}`}>
|
|
247
|
+
{#each facetValuesForColumn(colId).slice(0, 200) as v (v)}
|
|
248
|
+
<option value={v}></option>
|
|
249
|
+
{/each}
|
|
250
|
+
</datalist>
|
|
251
|
+
{/if}
|
|
213
252
|
{#if menuActiveOperator === "between"}
|
|
214
253
|
<input
|
|
215
254
|
class="sv-grid-menu-condition-value"
|
|
@@ -585,6 +624,47 @@
|
|
|
585
624
|
</div>
|
|
586
625
|
{/if}
|
|
587
626
|
|
|
627
|
+
{#if inSuggestFor}
|
|
628
|
+
{@const suggestColId = inSuggestFor}
|
|
629
|
+
{@const selectedTokens = new Set(
|
|
630
|
+
splitInTokens(filterRowValues[suggestColId] ?? ""),
|
|
631
|
+
)}
|
|
632
|
+
<!-- Value suggestions for an `in` / `notIn` chip input. Items use
|
|
633
|
+
`onmousedown` (preventDefault) so clicking one keeps focus in the
|
|
634
|
+
chip input and the dropdown stays open for multi-select; the input's
|
|
635
|
+
own blur handler closes it when focus truly leaves. -->
|
|
636
|
+
<div
|
|
637
|
+
class="sv-grid-menu sv-grid-in-suggest"
|
|
638
|
+
role="listbox"
|
|
639
|
+
aria-multiselectable="true"
|
|
640
|
+
aria-label="Filter values"
|
|
641
|
+
style={`left: ${inSuggestPos.x}px; top: ${inSuggestPos.y}px;`}
|
|
642
|
+
>
|
|
643
|
+
{#each inSuggestValues as value (value)}
|
|
644
|
+
{@const checked = selectedTokens.has(value)}
|
|
645
|
+
<button
|
|
646
|
+
type="button"
|
|
647
|
+
class="sv-grid-menu-item sv-grid-in-suggest-item"
|
|
648
|
+
role="option"
|
|
649
|
+
aria-selected={checked}
|
|
650
|
+
onmousedown={(event) => {
|
|
651
|
+
event.preventDefault();
|
|
652
|
+
toggleFilterToken(suggestColId, value);
|
|
653
|
+
}}
|
|
654
|
+
>
|
|
655
|
+
<span class="sv-grid-in-suggest-check" aria-hidden="true"
|
|
656
|
+
>{checked ? "✓" : ""}</span
|
|
657
|
+
>
|
|
658
|
+
<span class="sv-grid-in-suggest-label"
|
|
659
|
+
>{value === "" ? "(Blanks)" : value}</span
|
|
660
|
+
>
|
|
661
|
+
</button>
|
|
662
|
+
{:else}
|
|
663
|
+
<div class="sv-grid-facet-empty">No values</div>
|
|
664
|
+
{/each}
|
|
665
|
+
</div>
|
|
666
|
+
{/if}
|
|
667
|
+
|
|
588
668
|
{#if contextMenuFor}
|
|
589
669
|
<div
|
|
590
670
|
class="sv-grid-menu-backdrop"
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
let fieldEl = $state<HTMLInputElement | null>(null)
|
|
49
49
|
let panelEl = $state<HTMLDivElement | null>(null)
|
|
50
|
-
let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
|
|
50
|
+
let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
|
|
51
51
|
|
|
52
52
|
const ac = createAutocomplete({
|
|
53
53
|
value: () => value,
|
|
@@ -66,7 +66,9 @@
|
|
|
66
66
|
|
|
67
67
|
function updatePos() {
|
|
68
68
|
if (!fieldEl) return
|
|
69
|
-
|
|
69
|
+
// estimatedHeight only picks the flip direction; real max-height is availHeight
|
|
70
|
+
// below, so the panel takes its natural content height. See SvComboBox.
|
|
71
|
+
rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: Math.min(ac.filtered.length, 8) * 34 + 12 })
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
$effect(() => {
|
|
@@ -92,7 +94,7 @@
|
|
|
92
94
|
</SvField>
|
|
93
95
|
|
|
94
96
|
{#if ac.open}
|
|
95
|
-
<div bind:this={panelEl} class="sv-ddl__panel" use:portalToBody use:popIn={{ up: rect.openUpward }} style:position="fixed" style:top={`${rect.top}px`} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} {...ac.listboxProps()}>
|
|
97
|
+
<div bind:this={panelEl} class="sv-ddl__panel" use:portalToBody use:popIn={{ up: rect.openUpward }} style:position="fixed" style:top={rect.openUpward ? undefined : `${rect.top}px`} style:bottom={rect.openUpward ? `${rect.bottom}px` : undefined} style:left={`${rect.left}px`} style:min-width={`${rect.width}px`} style:max-height={`${Math.min(rect.availHeight, 288)}px`} {...ac.listboxProps()}>
|
|
96
98
|
{#each ac.filtered as opt, i (opt.value)}
|
|
97
99
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
|
|
98
100
|
<div class="sv-ddl__opt" class:is-active={ac.isActive(i)} {...ac.optionProps(i)}>{opt.label}</div>
|
package/dist/SvCarousel.svelte
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { Snippet } from 'svelte'
|
|
15
15
|
import { type EditorDir } from './editor-contract'
|
|
16
|
+
import { createCarousel } from './createCarousel.svelte'
|
|
16
17
|
|
|
17
18
|
type Props = {
|
|
18
19
|
count: number
|
|
@@ -41,28 +42,20 @@
|
|
|
41
42
|
dir,
|
|
42
43
|
}: Props = $props()
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
current
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Autoplay, paused while hovered/focused, or stopped via the persistent
|
|
55
|
-
// play/pause toggle (WCAG 2.2.2: auto-advancing content needs a way to stop
|
|
56
|
-
// it that doesn't depend on hover/focus).
|
|
57
|
-
let paused = $state(false)
|
|
58
|
-
let userPaused = $state(false)
|
|
59
|
-
$effect(() => {
|
|
60
|
-
if (!autoplay || paused || userPaused || count < 2) return
|
|
61
|
-
const t = setInterval(next, autoplay)
|
|
62
|
-
return () => clearInterval(t)
|
|
45
|
+
// Navigation, autoplay (pause on hover/focus + WCAG 2.2.2 stop toggle) and the
|
|
46
|
+
// rtl-aware arrow wiring live in the shared headless core; this component keeps
|
|
47
|
+
// only the visual track + swipe. See createCarousel.svelte.ts.
|
|
48
|
+
const car = createCarousel({
|
|
49
|
+
count: () => count,
|
|
50
|
+
current: () => current,
|
|
51
|
+
onChange: (i) => (current = i),
|
|
52
|
+
loop: () => loop,
|
|
53
|
+
autoplay: () => autoplay,
|
|
54
|
+
dir: () => (dir === 'ltr' || dir === 'rtl' ? dir : undefined),
|
|
63
55
|
})
|
|
56
|
+
const rtl = $derived(car.rtl())
|
|
64
57
|
|
|
65
|
-
// Swipe.
|
|
58
|
+
// Swipe (a DOM gesture the renderer owns; delegates to the core's next/prev).
|
|
66
59
|
let dragging = $state(false)
|
|
67
60
|
let startX = 0
|
|
68
61
|
let dx = $state(0)
|
|
@@ -75,26 +68,17 @@
|
|
|
75
68
|
function up() {
|
|
76
69
|
if (!dragging) return
|
|
77
70
|
dragging = false
|
|
78
|
-
if (Math.abs(dx) > 50) (dx < 0 ? next() : prev())
|
|
71
|
+
if (Math.abs(dx) > 50) (dx < 0 ? car.next() : car.prev())
|
|
79
72
|
dx = 0
|
|
80
73
|
}
|
|
81
74
|
</script>
|
|
82
75
|
|
|
83
|
-
<div
|
|
84
|
-
class="sv-carousel"
|
|
85
|
-
role="group"
|
|
86
|
-
aria-roledescription="carousel"
|
|
87
|
-
aria-label={ariaLabel}
|
|
88
|
-
onpointerenter={() => (paused = true)}
|
|
89
|
-
onpointerleave={() => (paused = false)}
|
|
90
|
-
onfocusin={() => (paused = true)}
|
|
91
|
-
onfocusout={() => (paused = false)}
|
|
92
|
-
>
|
|
76
|
+
<div class="sv-carousel" {...car.rootProps(ariaLabel)}>
|
|
93
77
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
94
78
|
<div class="sv-carousel__viewport" onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up}>
|
|
95
79
|
<div class="sv-carousel__track" class:is-dragging={dragging} style:transform={`translateX(calc(${(rtl ? current : -current) * 100}% + ${dx}px))`}>
|
|
96
80
|
{#each Array(count) as _, i (i)}
|
|
97
|
-
<div class="sv-carousel__slide"
|
|
81
|
+
<div class="sv-carousel__slide" {...car.slideProps(i)}>
|
|
98
82
|
{@render slide?.(i)}
|
|
99
83
|
</div>
|
|
100
84
|
{/each}
|
|
@@ -102,26 +86,20 @@
|
|
|
102
86
|
</div>
|
|
103
87
|
|
|
104
88
|
{#if arrows && count > 1}
|
|
105
|
-
<button type="button" class="sv-carousel__arrow is-prev"
|
|
106
|
-
<button type="button" class="sv-carousel__arrow is-next"
|
|
89
|
+
<button type="button" class="sv-carousel__arrow is-prev" {...car.prevArrowProps()}>{rtl ? '›' : '‹'}</button>
|
|
90
|
+
<button type="button" class="sv-carousel__arrow is-next" {...car.nextArrowProps()}>{rtl ? '‹' : '›'}</button>
|
|
107
91
|
{/if}
|
|
108
92
|
|
|
109
93
|
{#if dots && count > 1}
|
|
110
94
|
<div class="sv-carousel__dots">
|
|
111
95
|
{#each Array(count) as _, i (i)}
|
|
112
|
-
<button type="button" class="sv-carousel__dot" class:is-active={i
|
|
96
|
+
<button type="button" class="sv-carousel__dot" class:is-active={car.isActive(i)} {...car.dotProps(i)}></button>
|
|
113
97
|
{/each}
|
|
114
98
|
</div>
|
|
115
99
|
{/if}
|
|
116
100
|
|
|
117
101
|
{#if autoplay > 0}
|
|
118
|
-
<button
|
|
119
|
-
type="button"
|
|
120
|
-
class="sv-carousel__playpause"
|
|
121
|
-
aria-label={userPaused ? 'Play autoplay' : 'Pause autoplay'}
|
|
122
|
-
aria-pressed={userPaused}
|
|
123
|
-
onclick={() => (userPaused = !userPaused)}
|
|
124
|
-
>{userPaused ? '▶' : '⏸'}</button>
|
|
102
|
+
<button type="button" class="sv-carousel__playpause" {...car.playPauseProps()}>{car.userPaused ? '▶' : '⏸'}</button>
|
|
125
103
|
{/if}
|
|
126
104
|
</div>
|
|
127
105
|
|
package/dist/SvColorInput.svelte
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
let {
|
|
32
|
-
value = '#3b82f6',
|
|
32
|
+
value = $bindable('#3b82f6'),
|
|
33
33
|
onChange,
|
|
34
34
|
disabled = false,
|
|
35
35
|
readonly = false,
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
|
|
55
55
|
const col = createColorInput({
|
|
56
56
|
value: () => value,
|
|
57
|
-
|
|
57
|
+
// Write `value` (so `bind:value` works) AND fire `onChange` (callback users).
|
|
58
|
+
onChange: (hex) => { value = hex; onChange?.(hex) },
|
|
58
59
|
disabled: () => disabled,
|
|
59
60
|
readonly: () => readonly,
|
|
60
61
|
palette: () => palette as string[],
|
|
@@ -68,7 +69,7 @@
|
|
|
68
69
|
|
|
69
70
|
let triggerEl = $state<HTMLButtonElement | null>(null)
|
|
70
71
|
let panelEl = $state<HTMLDivElement | null>(null)
|
|
71
|
-
let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
|
|
72
|
+
let panelRect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
|
|
72
73
|
|
|
73
74
|
function updatePos() {
|
|
74
75
|
if (!triggerEl) return
|