@svgrid/grid 2.1.21 → 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.
Files changed (219) hide show
  1. package/README.md +9 -2
  2. package/dist/DockNodeView.svelte +306 -0
  3. package/dist/DockNodeView.svelte.d.ts +7 -0
  4. package/dist/DockPopoutHost.svelte +17 -0
  5. package/dist/DockPopoutHost.svelte.d.ts +14 -0
  6. package/dist/GridMenus.svelte +83 -3
  7. package/dist/SvAutoComplete.svelte +5 -3
  8. package/dist/SvCarousel.svelte +20 -42
  9. package/dist/SvColorInput.svelte +4 -3
  10. package/dist/SvColorInput.svelte.d.ts +1 -1
  11. package/dist/SvComboBox.svelte +7 -3
  12. package/dist/SvCommand.svelte +28 -85
  13. package/dist/SvCountryInput.svelte +1 -1
  14. package/dist/SvDateRangeInput.svelte +1 -1
  15. package/dist/SvDateTimePicker.svelte +9 -3
  16. package/dist/SvDateTimePicker.svelte.d.ts +3 -0
  17. package/dist/SvDockLayout.svelte +244 -0
  18. package/dist/SvDockLayout.svelte.d.ts +77 -0
  19. package/dist/SvDockManager.svelte +922 -0
  20. package/dist/SvDockManager.svelte.d.ts +106 -0
  21. package/dist/SvDrawer.svelte +69 -47
  22. package/dist/SvDrawer.svelte.d.ts +4 -0
  23. package/dist/SvDropDownList.svelte +85 -13
  24. package/dist/SvDropDownList.svelte.d.ts +3 -0
  25. package/dist/SvField.svelte +21 -4
  26. package/dist/SvField.svelte.d.ts +1 -0
  27. package/dist/SvForm.svelte +201 -91
  28. package/dist/SvForm.svelte.d.ts +15 -22
  29. package/dist/SvGrid.controller.svelte.d.ts +32 -1
  30. package/dist/SvGrid.controller.svelte.js +156 -27
  31. package/dist/SvGrid.css +193 -5
  32. package/dist/SvGrid.svelte +357 -12
  33. package/dist/SvGrid.types.d.ts +452 -1
  34. package/dist/SvGridDropdown.svelte +8 -1
  35. package/dist/SvGridSelect.svelte +1 -0
  36. package/dist/SvListBox.svelte +36 -14
  37. package/dist/SvMaskedInput.svelte +4 -3
  38. package/dist/SvMaskedInput.svelte.d.ts +1 -1
  39. package/dist/SvMenu.svelte +1 -1
  40. package/dist/SvMenuList.svelte +23 -71
  41. package/dist/SvMenuList.svelte.d.ts +2 -13
  42. package/dist/SvModal.svelte +12 -29
  43. package/dist/SvMultiSelect.svelte +1 -0
  44. package/dist/SvNumberInput.svelte +4 -3
  45. package/dist/SvNumberInput.svelte.d.ts +1 -1
  46. package/dist/SvPagination.svelte +18 -18
  47. package/dist/SvPasswordInput.svelte +3 -2
  48. package/dist/SvPasswordInput.svelte.d.ts +1 -1
  49. package/dist/SvPhoneInput.svelte +3 -2
  50. package/dist/SvPhoneInput.svelte.d.ts +1 -1
  51. package/dist/SvPopover.svelte +1 -1
  52. package/dist/SvStepper.svelte +6 -9
  53. package/dist/SvTagsInput.svelte +3 -2
  54. package/dist/SvTagsInput.svelte.d.ts +1 -1
  55. package/dist/SvTooltip.svelte +12 -23
  56. package/dist/SvTour.svelte +32 -4
  57. package/dist/SvTreeSelect.svelte +1 -0
  58. package/dist/build-api.js +76 -54
  59. package/dist/cdn/svgrid.js +10907 -6444
  60. package/dist/cdn/svgrid.svelte-external.js +9622 -5475
  61. package/dist/columns.js +6 -0
  62. package/dist/conditional-formatting.d.ts +67 -3
  63. package/dist/conditional-formatting.js +124 -17
  64. package/dist/createCarousel.svelte.d.ts +72 -0
  65. package/dist/createCarousel.svelte.js +80 -0
  66. package/dist/createCommand.svelte.d.ts +62 -0
  67. package/dist/createCommand.svelte.js +109 -0
  68. package/dist/createForm.svelte.d.ts +62 -0
  69. package/dist/createForm.svelte.js +374 -0
  70. package/dist/createMenu.svelte.d.ts +88 -0
  71. package/dist/createMenu.svelte.js +153 -0
  72. package/dist/createOverlay.svelte.d.ts +38 -0
  73. package/dist/createOverlay.svelte.js +68 -0
  74. package/dist/createPagination.d.ts +74 -0
  75. package/dist/createPagination.js +71 -0
  76. package/dist/createPopoverSelect.svelte.js +6 -12
  77. package/dist/createStepper.d.ts +44 -0
  78. package/dist/createStepper.js +26 -0
  79. package/dist/createTooltip.svelte.d.ts +32 -0
  80. package/dist/createTooltip.svelte.js +61 -0
  81. package/dist/datetime/timezone.d.ts +36 -0
  82. package/dist/datetime/timezone.js +110 -0
  83. package/dist/dock-context.d.ts +52 -0
  84. package/dist/dock-context.js +1 -0
  85. package/dist/dock-manager-model.d.ts +136 -0
  86. package/dist/dock-manager-model.js +514 -0
  87. package/dist/dock-model.d.ts +105 -0
  88. package/dist/dock-model.js +288 -0
  89. package/dist/dock-popout.d.ts +30 -0
  90. package/dist/dock-popout.js +81 -0
  91. package/dist/editor-contract.d.ts +2 -0
  92. package/dist/filter-operators.d.ts +6 -0
  93. package/dist/filter-operators.js +35 -2
  94. package/dist/filtering/excel-filters.d.ts +18 -1
  95. package/dist/filtering/excel-filters.js +56 -0
  96. package/dist/form-field.d.ts +73 -0
  97. package/dist/form-field.js +15 -0
  98. package/dist/index.d.ts +25 -5
  99. package/dist/index.js +40 -4
  100. package/dist/list-nav.d.ts +18 -0
  101. package/dist/list-nav.js +30 -0
  102. package/dist/list-option.d.ts +2 -0
  103. package/dist/menu-item.d.ts +20 -0
  104. package/dist/menu-item.js +1 -0
  105. package/dist/menus.d.ts +5 -0
  106. package/dist/menus.js +68 -16
  107. package/dist/panel-resize.d.ts +25 -0
  108. package/dist/panel-resize.js +40 -0
  109. package/dist/popover.d.ts +29 -3
  110. package/dist/popover.js +22 -6
  111. package/dist/recurrence.d.ts +33 -7
  112. package/dist/recurrence.js +151 -13
  113. package/dist/row-drag.d.ts +8 -0
  114. package/dist/row-drag.js +11 -0
  115. package/dist/scheduler-ical.d.ts +30 -0
  116. package/dist/scheduler-ical.js +180 -0
  117. package/dist/scheduler-model.d.ts +269 -0
  118. package/dist/scheduler-model.js +602 -0
  119. package/dist/scheduler-view.svelte.d.ts +22 -0
  120. package/dist/scheduler-view.svelte.js +13 -0
  121. package/dist/spreadsheet.js +20 -12
  122. package/dist/ui-app.types.d.ts +3 -0
  123. package/package.json +5 -1
  124. package/src/DockNodeView.svelte +306 -0
  125. package/src/DockPopoutHost.svelte +17 -0
  126. package/src/GridMenus.svelte +83 -3
  127. package/src/SvAutoComplete.svelte +5 -3
  128. package/src/SvCarousel.svelte +20 -42
  129. package/src/SvCarousel.test.ts +68 -0
  130. package/src/SvColorInput.svelte +4 -3
  131. package/src/SvComboBox.svelte +7 -3
  132. package/src/SvCommand.svelte +28 -85
  133. package/src/SvCommand.test.ts +78 -0
  134. package/src/SvCountryInput.svelte +1 -1
  135. package/src/SvDateRangeInput.svelte +1 -1
  136. package/src/SvDateTimePicker.svelte +9 -3
  137. package/src/SvDockLayout.svelte +244 -0
  138. package/src/SvDockManager.svelte +922 -0
  139. package/src/SvDrawer.svelte +69 -47
  140. package/src/SvDropDownList.svelte +85 -13
  141. package/src/SvField.svelte +21 -4
  142. package/src/SvForm.svelte +201 -91
  143. package/src/SvForm.test.ts +299 -0
  144. package/src/SvGrid.controller.svelte.ts +152 -28
  145. package/src/SvGrid.css +193 -5
  146. package/src/SvGrid.svelte +357 -12
  147. package/src/SvGrid.types.ts +463 -1
  148. package/src/SvGridDropdown.svelte +8 -1
  149. package/src/SvGridSelect.svelte +1 -0
  150. package/src/SvListBox.svelte +36 -14
  151. package/src/SvMaskedInput.svelte +4 -3
  152. package/src/SvMenu.svelte +1 -1
  153. package/src/SvMenu.test.ts +24 -0
  154. package/src/SvMenuList.svelte +23 -71
  155. package/src/SvModal.svelte +12 -29
  156. package/src/SvModal.test.ts +82 -0
  157. package/src/SvMultiSelect.svelte +1 -0
  158. package/src/SvNumberInput.svelte +4 -3
  159. package/src/SvPagination.svelte +18 -18
  160. package/src/SvPasswordInput.svelte +3 -2
  161. package/src/SvPhoneInput.svelte +3 -2
  162. package/src/SvPopover.svelte +1 -1
  163. package/src/SvStepper.svelte +6 -9
  164. package/src/SvTagsInput.svelte +3 -2
  165. package/src/SvTooltip.svelte +12 -23
  166. package/src/SvTooltip.test.ts +73 -0
  167. package/src/SvTour.svelte +32 -4
  168. package/src/SvTreeSelect.svelte +1 -0
  169. package/src/build-api.ts +74 -51
  170. package/src/columns.ts +5 -0
  171. package/src/conditional-formatting.test.ts +104 -0
  172. package/src/conditional-formatting.ts +176 -19
  173. package/src/createCarousel.svelte.ts +104 -0
  174. package/src/createCommand.svelte.ts +125 -0
  175. package/src/createForm.svelte.ts +333 -0
  176. package/src/createMenu.svelte.ts +202 -0
  177. package/src/createOverlay.svelte.ts +101 -0
  178. package/src/createPagination.test.ts +65 -0
  179. package/src/createPagination.ts +84 -0
  180. package/src/createPopoverSelect.svelte.ts +5 -9
  181. package/src/createStepper.test.ts +62 -0
  182. package/src/createStepper.ts +61 -0
  183. package/src/createTooltip.svelte.ts +86 -0
  184. package/src/datetime/timezone.test.ts +69 -0
  185. package/src/datetime/timezone.ts +134 -0
  186. package/src/dock-context.ts +47 -0
  187. package/src/dock-manager-model.test.ts +275 -0
  188. package/src/dock-manager-model.ts +593 -0
  189. package/src/dock-model.test.ts +163 -0
  190. package/src/dock-model.ts +368 -0
  191. package/src/dock-popout.ts +94 -0
  192. package/src/editor-contract.ts +2 -0
  193. package/src/filter-operators.test.ts +59 -0
  194. package/src/filter-operators.ts +35 -2
  195. package/src/filtering/excel-filters.test.ts +86 -1
  196. package/src/filtering/excel-filters.ts +65 -0
  197. package/src/form-field.ts +87 -0
  198. package/src/index.ts +158 -4
  199. package/src/list-nav.test.ts +49 -0
  200. package/src/list-nav.ts +29 -0
  201. package/src/list-option.ts +2 -0
  202. package/src/menu-item.ts +21 -0
  203. package/src/menus.test.ts +48 -0
  204. package/src/menus.ts +73 -16
  205. package/src/panel-resize.ts +52 -0
  206. package/src/popover.bounds.test.ts +93 -0
  207. package/src/popover.ts +51 -7
  208. package/src/recurrence.test.ts +88 -1
  209. package/src/recurrence.ts +166 -15
  210. package/src/row-drag.ts +10 -0
  211. package/src/scheduler-ical.test.ts +68 -0
  212. package/src/scheduler-ical.ts +180 -0
  213. package/src/scheduler-model.test.ts +546 -0
  214. package/src/scheduler-model.ts +853 -0
  215. package/src/scheduler-view.svelte.ts +33 -0
  216. package/src/spreadsheet.ts +20 -12
  217. package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
  218. package/src/ui-app.types.ts +3 -0
  219. 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 adds Excel / PDF / CSV / TSV / HTML export, paginated print, pivot tables with a drag-and-drop Designer, and AI helpers. OSS projects receive an Enterprise key free. See [Pricing](https://svgrid.com/pricing).
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
+ >&times;</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,7 @@
1
+ import { type DockNode } from './dock-model';
2
+ type $$ComponentProps = {
3
+ node: DockNode;
4
+ };
5
+ declare const DockNodeView: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type DockNodeView = ReturnType<typeof DockNodeView>;
7
+ export default DockNodeView;
@@ -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;
@@ -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" ? "From" : "Filter value..."}
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
- rect = anchoredRect(fieldEl.getBoundingClientRect(), { estimatedHeight: Math.min(ac.filtered.length, 8) * 34 + 8 })
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>
@@ -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
- const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
45
- const rtl = $derived(resolvedDir === 'rtl')
46
-
47
- function go(i: number) {
48
- if (count === 0) return
49
- current = loop ? (i + count) % count : Math.max(0, Math.min(i, count - 1))
50
- }
51
- const next = () => go(current + 1)
52
- const prev = () => go(current - 1)
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" role="group" aria-roledescription="slide" aria-label={`${i + 1} of ${count}`} aria-hidden={i !== current} inert={i !== current}>
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" aria-label={rtl ? 'Next slide' : 'Previous slide'} onclick={rtl ? next : prev} disabled={rtl ? (!loop && current === count - 1) : (!loop && current === 0)}>{rtl ? '›' : '‹'}</button>
106
- <button type="button" class="sv-carousel__arrow is-next" aria-label={rtl ? 'Previous slide' : 'Next slide'} onclick={rtl ? prev : next} disabled={rtl ? (!loop && current === 0) : (!loop && current === count - 1)}>{rtl ? '‹' : '›'}</button>
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 === current} aria-label={`Go to slide ${i + 1}`} aria-current={i === current ? 'true' : undefined} onclick={() => go(i)}></button>
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
 
@@ -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
- onChange: (hex) => onChange?.(hex),
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