@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
@@ -0,0 +1,38 @@
1
+ export type OverlayConfig = {
2
+ /** Whether the overlay is currently open. */
3
+ open: () => boolean;
4
+ /** The dialog panel element (component owns `bind:this`, passes a getter). */
5
+ getDialog: () => HTMLElement | null;
6
+ /** Close the overlay (set your `open` state false + notify). */
7
+ onClose: () => void;
8
+ /** Allow Escape to close. Default true. */
9
+ closeOnEsc?: () => boolean;
10
+ /** Allow an outside/backdrop click to close. Default true. */
11
+ closeOnBackdrop?: () => boolean;
12
+ /** Run once each time the overlay opens - reset transient state (drag, resize,
13
+ * swipe offset) before the trap/lock/layer are wired. */
14
+ onOpen?: () => void;
15
+ /** Trap focus inside the dialog while open. Default true. */
16
+ trapFocus?: () => boolean;
17
+ /** Lock body scroll while open. Default true. */
18
+ scrollLock?: () => boolean;
19
+ /** Element to focus first when the dialog opens (e.g. a search input).
20
+ * Defaults to the trap's first focusable. */
21
+ initialFocus?: () => HTMLElement | null;
22
+ };
23
+ export type DialogProps = {
24
+ role: 'dialog';
25
+ 'aria-modal': 'true';
26
+ 'aria-labelledby': string | undefined;
27
+ 'aria-label': string | undefined;
28
+ tabindex: -1;
29
+ };
30
+ export declare function createOverlay(config: OverlayConfig): {
31
+ /** Spread onto the dialog panel element. Pass `labelledBy` (an id, when there
32
+ * is a visible title) or `label` (an accessible name when there isn't). */
33
+ dialogProps: (opts?: {
34
+ labelledBy?: string;
35
+ label?: string;
36
+ }) => DialogProps;
37
+ };
38
+ export type Overlay = ReturnType<typeof createOverlay>;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * createOverlay - the HEADLESS core behind the dialog-type overlays (<SvModal>,
3
+ * <SvDrawer>): the open -> (focus-trap + scroll-lock + Escape/backdrop dismissal)
4
+ * -> restore lifecycle, wired to the shared a11y primitives so nested overlays
5
+ * close top-first via the layer stack. Owns no markup and no styles - the
6
+ * component renders the backdrop/panel and spreads `dialogProps()` for the ARIA.
7
+ *
8
+ * ```svelte
9
+ * <script lang="ts">
10
+ * import { createOverlay } from '@svgrid/grid'
11
+ * let dialogEl = $state<HTMLElement | null>(null)
12
+ * const overlay = createOverlay({
13
+ * open: () => open,
14
+ * getDialog: () => dialogEl,
15
+ * onClose: () => (open = false),
16
+ * })
17
+ * </script>
18
+ * {#if open}
19
+ * <div class="backdrop">
20
+ * <div bind:this={dialogEl} {...overlay.dialogProps({ labelledBy: titleId })}>…</div>
21
+ * </div>
22
+ * {/if}
23
+ * ```
24
+ */
25
+ import { createFocusTrap } from './a11y/focus-trap';
26
+ import { lockScroll } from './a11y/scroll-lock';
27
+ import { createDismissableLayer } from './a11y/dismissable';
28
+ export function createOverlay(config) {
29
+ // While open: reset transient state, then hand focus, scroll-lock and
30
+ // Escape/backdrop dismissal to the shared primitives (one tested
31
+ // implementation; nested overlays close top-first via the layer stack).
32
+ $effect(() => {
33
+ if (!config.open() || !config.getDialog())
34
+ return;
35
+ config.onOpen?.();
36
+ const el = config.getDialog();
37
+ const trap = (config.trapFocus?.() ?? true)
38
+ ? createFocusTrap(el, config.initialFocus ? { initialFocus: config.initialFocus } : undefined)
39
+ : null;
40
+ trap?.activate();
41
+ const unlockScroll = (config.scrollLock?.() ?? true) ? lockScroll() : null;
42
+ const layer = createDismissableLayer({
43
+ element: () => config.getDialog(),
44
+ onDismiss: (reason) => {
45
+ const allow = reason === 'escape' ? (config.closeOnEsc?.() ?? true) : (config.closeOnBackdrop?.() ?? true);
46
+ if (allow)
47
+ config.onClose();
48
+ },
49
+ });
50
+ layer.activate();
51
+ return () => {
52
+ layer.release();
53
+ unlockScroll?.();
54
+ trap?.release();
55
+ };
56
+ });
57
+ return {
58
+ /** Spread onto the dialog panel element. Pass `labelledBy` (an id, when there
59
+ * is a visible title) or `label` (an accessible name when there isn't). */
60
+ dialogProps: (opts) => ({
61
+ role: 'dialog',
62
+ 'aria-modal': 'true',
63
+ 'aria-labelledby': opts?.labelledBy,
64
+ 'aria-label': opts?.label,
65
+ tabindex: -1,
66
+ }),
67
+ };
68
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * createPagination - the HEADLESS core behind <SvPagination>: the page/ellipsis
3
+ * sequence (via the pure `paginationRange`) plus navigation (first/prev/next/last
4
+ * with clamping) and prop-getters you spread onto your own markup. Pure getters,
5
+ * no runes - so it is directly unit-testable and framework-free.
6
+ *
7
+ * ```svelte
8
+ * <script lang="ts">
9
+ * import { createPagination } from '@svgrid/grid'
10
+ * const pg = createPagination({ page: () => page, pageCount: () => 20, onChange: (p) => (page = p) })
11
+ * </script>
12
+ * <nav {...pg.navProps()}>
13
+ * <button {...pg.prevProps()}>‹</button>
14
+ * {#each pg.items as it}
15
+ * {#if typeof it === 'number'}<button {...pg.pageButtonProps(it)}>{it}</button>{:else}…{/if}
16
+ * {/each}
17
+ * <button {...pg.nextProps()}>›</button>
18
+ * </nav>
19
+ * ```
20
+ */
21
+ import { type PaginationItem } from './paginate';
22
+ export type PaginationConfig = {
23
+ /** 1-based current page. */
24
+ page: () => number;
25
+ pageCount: () => number;
26
+ onChange?: (page: number) => void;
27
+ siblingCount?: () => number;
28
+ boundaryCount?: () => number;
29
+ disabled?: () => boolean;
30
+ };
31
+ export declare function createPagination(config: PaginationConfig): {
32
+ /** The page/ellipsis sequence to render, recomputed from the live inputs. */
33
+ readonly items: PaginationItem[];
34
+ go: (p: number) => void;
35
+ first: () => void;
36
+ prev: () => void;
37
+ next: () => void;
38
+ last: () => void;
39
+ canPrev: () => boolean;
40
+ canNext: () => boolean;
41
+ isActive: (p: number) => boolean;
42
+ /** Spread onto the `<nav>` wrapper. */
43
+ navProps: (ariaLabel?: string) => {
44
+ 'aria-label': string;
45
+ };
46
+ /** Spread onto a numbered page button. */
47
+ pageButtonProps: (p: number) => {
48
+ 'aria-current': "page" | undefined;
49
+ 'aria-label': string;
50
+ disabled: boolean;
51
+ onclick: () => void;
52
+ };
53
+ firstProps: () => {
54
+ 'aria-label': string;
55
+ disabled: boolean;
56
+ onclick: () => void;
57
+ };
58
+ prevProps: () => {
59
+ 'aria-label': string;
60
+ disabled: boolean;
61
+ onclick: () => void;
62
+ };
63
+ nextProps: () => {
64
+ 'aria-label': string;
65
+ disabled: boolean;
66
+ onclick: () => void;
67
+ };
68
+ lastProps: () => {
69
+ 'aria-label': string;
70
+ disabled: boolean;
71
+ onclick: () => void;
72
+ };
73
+ };
74
+ export type Pagination = ReturnType<typeof createPagination>;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * createPagination - the HEADLESS core behind <SvPagination>: the page/ellipsis
3
+ * sequence (via the pure `paginationRange`) plus navigation (first/prev/next/last
4
+ * with clamping) and prop-getters you spread onto your own markup. Pure getters,
5
+ * no runes - so it is directly unit-testable and framework-free.
6
+ *
7
+ * ```svelte
8
+ * <script lang="ts">
9
+ * import { createPagination } from '@svgrid/grid'
10
+ * const pg = createPagination({ page: () => page, pageCount: () => 20, onChange: (p) => (page = p) })
11
+ * </script>
12
+ * <nav {...pg.navProps()}>
13
+ * <button {...pg.prevProps()}>‹</button>
14
+ * {#each pg.items as it}
15
+ * {#if typeof it === 'number'}<button {...pg.pageButtonProps(it)}>{it}</button>{:else}…{/if}
16
+ * {/each}
17
+ * <button {...pg.nextProps()}>›</button>
18
+ * </nav>
19
+ * ```
20
+ */
21
+ import { paginationRange } from './paginate';
22
+ export function createPagination(config) {
23
+ const page = () => config.page();
24
+ const pageCount = () => config.pageCount();
25
+ const disabled = () => config.disabled?.() ?? false;
26
+ function go(p) {
27
+ if (disabled())
28
+ return;
29
+ const next = Math.min(Math.max(p, 1), pageCount());
30
+ if (next !== page())
31
+ config.onChange?.(next);
32
+ }
33
+ const first = () => go(1);
34
+ const prev = () => go(page() - 1);
35
+ const next = () => go(page() + 1);
36
+ const last = () => go(pageCount());
37
+ const canPrev = () => !disabled() && page() > 1;
38
+ const canNext = () => !disabled() && page() < pageCount();
39
+ return {
40
+ /** The page/ellipsis sequence to render, recomputed from the live inputs. */
41
+ get items() {
42
+ return paginationRange({
43
+ page: page(),
44
+ pageCount: pageCount(),
45
+ siblingCount: config.siblingCount?.() ?? 1,
46
+ boundaryCount: config.boundaryCount?.() ?? 1,
47
+ });
48
+ },
49
+ go,
50
+ first,
51
+ prev,
52
+ next,
53
+ last,
54
+ canPrev,
55
+ canNext,
56
+ isActive: (p) => p === page(),
57
+ /** Spread onto the `<nav>` wrapper. */
58
+ navProps: (ariaLabel = 'Pagination') => ({ 'aria-label': ariaLabel }),
59
+ /** Spread onto a numbered page button. */
60
+ pageButtonProps: (p) => ({
61
+ 'aria-current': (p === page() ? 'page' : undefined),
62
+ 'aria-label': `Page ${p}`,
63
+ disabled: disabled(),
64
+ onclick: () => go(p),
65
+ }),
66
+ firstProps: () => ({ 'aria-label': 'First page', disabled: !canPrev(), onclick: first }),
67
+ prevProps: () => ({ 'aria-label': 'Previous page', disabled: !canPrev(), onclick: prev }),
68
+ nextProps: () => ({ 'aria-label': 'Next page', disabled: !canNext(), onclick: next }),
69
+ lastProps: () => ({ 'aria-label': 'Last page', disabled: !canNext(), onclick: last }),
70
+ };
71
+ }
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { anchoredRect } from './popover';
16
16
  import { createDismissableLayer } from './a11y/dismissable';
17
+ import { enabledIndices as enabledIndicesOf, wrapMove } from './list-nav';
17
18
  let uidSeq = 0;
18
19
  export function createPopoverSelect(config) {
19
20
  const id = `${config.idPrefix ?? 'sv-pop'}-${uidSeq++}`;
@@ -21,16 +22,11 @@ export function createPopoverSelect(config) {
21
22
  const optionId = (i) => `${id}-opt-${i}`;
22
23
  let open = $state(false);
23
24
  let active = $state(0);
24
- let rect = $state({ top: 0, left: 0, width: 0, openUpward: false });
25
+ let rect = $state({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 });
25
26
  const count = () => config.itemCount();
26
27
  const isDisabled = (i) => config.disabled?.(i) ?? false;
27
28
  function enabledIndices() {
28
- const out = [];
29
- const n = count();
30
- for (let i = 0; i < n; i++)
31
- if (!isDisabled(i))
32
- out.push(i);
33
- return out;
29
+ return enabledIndicesOf(count(), isDisabled);
34
30
  }
35
31
  function clampActive(i) {
36
32
  const n = count();
@@ -54,11 +50,9 @@ export function createPopoverSelect(config) {
54
50
  open ? setOpen(false) : openPanel();
55
51
  }
56
52
  function move(delta) {
57
- const list = enabledIndices();
58
- if (!list.length)
59
- return;
60
- const pos = list.indexOf(active);
61
- active = list[(pos + delta + list.length) % list.length] ?? list[0];
53
+ const next = wrapMove(enabledIndices(), active, delta);
54
+ if (next >= 0)
55
+ active = next;
62
56
  }
63
57
  function first() { active = enabledIndices()[0] ?? 0; }
64
58
  function last() { active = enabledIndices().at(-1) ?? 0; }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * createStepper - the HEADLESS core behind <SvStepper>: per-step status
3
+ * (complete / active / upcoming), the linear-vs-free clickability rule, and
4
+ * navigation, as pure getters + prop-getters. No runes, framework-free, directly
5
+ * unit-testable.
6
+ *
7
+ * ```svelte
8
+ * <script lang="ts">
9
+ * import { createStepper } from '@svgrid/grid'
10
+ * const st = createStepper({ count: () => steps.length, current: () => i, onChange: (n) => (i = n) })
11
+ * </script>
12
+ * <ol>
13
+ * {#each steps as step, idx}
14
+ * <li class="is-{st.statusOf(idx)}">
15
+ * <button {...st.stepButtonProps(idx)}>{step.label}</button>
16
+ * </li>
17
+ * {/each}
18
+ * </ol>
19
+ * ```
20
+ */
21
+ export type StepStatus = 'complete' | 'active' | 'upcoming';
22
+ export type StepperConfig = {
23
+ count: () => number;
24
+ /** 0-based index of the active step. */
25
+ current: () => number;
26
+ onChange?: (index: number) => void;
27
+ /** Only allow navigating to already-reached steps. Default true. */
28
+ linear?: () => boolean;
29
+ };
30
+ export declare function createStepper(config: StepperConfig): {
31
+ statusOf: (i: number) => StepStatus;
32
+ clickable: (i: number) => boolean;
33
+ go: (i: number) => void;
34
+ next: () => void;
35
+ prev: () => void;
36
+ isActive: (i: number) => boolean;
37
+ /** Spread onto a step's button. */
38
+ stepButtonProps: (i: number) => {
39
+ 'aria-current': "step" | undefined;
40
+ disabled: boolean;
41
+ onclick: () => void;
42
+ };
43
+ };
44
+ export type Stepper = ReturnType<typeof createStepper>;
@@ -0,0 +1,26 @@
1
+ export function createStepper(config) {
2
+ const current = () => config.current();
3
+ const linear = () => config.linear?.() ?? true;
4
+ const statusOf = (i) => i < current() ? 'complete' : i === current() ? 'active' : 'upcoming';
5
+ const clickable = (i) => (linear() ? i <= current() : true);
6
+ const go = (i) => {
7
+ if (clickable(i) && i !== current())
8
+ config.onChange?.(i);
9
+ };
10
+ const next = () => go(current() + 1);
11
+ const prev = () => go(current() - 1);
12
+ return {
13
+ statusOf,
14
+ clickable,
15
+ go,
16
+ next,
17
+ prev,
18
+ isActive: (i) => i === current(),
19
+ /** Spread onto a step's button. */
20
+ stepButtonProps: (i) => ({
21
+ 'aria-current': (statusOf(i) === 'active' ? 'step' : undefined),
22
+ disabled: !clickable(i),
23
+ onclick: () => go(i),
24
+ }),
25
+ };
26
+ }
@@ -0,0 +1,32 @@
1
+ export type TooltipConfig = {
2
+ /** The tooltip text; the tooltip never opens when it is empty. */
3
+ text?: () => string | undefined;
4
+ /** Suppress the tooltip entirely. */
5
+ disabled?: () => boolean;
6
+ /** Show delay in ms after hover/focus. Default 300. */
7
+ delay?: () => number;
8
+ };
9
+ export type TooltipAnchorProps = {
10
+ 'aria-describedby': string | undefined;
11
+ onpointerenter: () => void;
12
+ onpointerleave: () => void;
13
+ onfocusin: () => void;
14
+ onfocusout: () => void;
15
+ };
16
+ export type TooltipProps = {
17
+ id: string;
18
+ role: 'tooltip';
19
+ };
20
+ export declare function createTooltip(config?: TooltipConfig): {
21
+ readonly open: boolean;
22
+ /** Stable id of the tooltip element (for `aria-describedby` + the tip's id). */
23
+ tipId: string;
24
+ show: () => void;
25
+ hide: () => void;
26
+ /** Spread onto the anchor/trigger wrapper. Also mirror `tipId` onto the real
27
+ * focusable child's `aria-describedby` so AT announces it on focus. */
28
+ anchorProps: () => TooltipAnchorProps;
29
+ /** Spread onto the tooltip element. */
30
+ tooltipProps: () => TooltipProps;
31
+ };
32
+ export type Tooltip = ReturnType<typeof createTooltip>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * createTooltip - the HEADLESS core behind <SvTooltip>: the hover/focus open
3
+ * state machine with a show delay, Escape-to-hide, and the `aria-describedby` /
4
+ * `role="tooltip"` wiring - as prop-getters you spread onto your own anchor and
5
+ * tooltip elements. No styles, no positioning (that is a DOM concern the renderer
6
+ * owns), like `createListbox`.
7
+ *
8
+ * ```svelte
9
+ * <script lang="ts">
10
+ * import { createTooltip } from '@svgrid/grid'
11
+ * const tip = createTooltip({ text: () => label })
12
+ * </script>
13
+ * <span {...tip.anchorProps()}>{@render children?.()}</span>
14
+ * {#if tip.open}
15
+ * <div {...tip.tooltipProps()}>{label}</div>
16
+ * {/if}
17
+ * ```
18
+ */
19
+ import { nextEditorId } from './editor-contract';
20
+ export function createTooltip(config = {}) {
21
+ const id = nextEditorId('sv-tip');
22
+ let open = $state(false);
23
+ let showTimer;
24
+ function show() {
25
+ if ((config.disabled?.() ?? false) || !(config.text?.() ?? ''))
26
+ return;
27
+ clearTimeout(showTimer);
28
+ showTimer = setTimeout(() => { open = true; }, config.delay?.() ?? 300);
29
+ }
30
+ function hide() {
31
+ clearTimeout(showTimer);
32
+ open = false;
33
+ }
34
+ // Escape hides the tooltip while it is open (WAI-ARIA tooltip pattern).
35
+ $effect(() => {
36
+ if (!open)
37
+ return;
38
+ const onKey = (e) => { if (e.key === 'Escape')
39
+ hide(); };
40
+ document.addEventListener('keydown', onKey);
41
+ return () => document.removeEventListener('keydown', onKey);
42
+ });
43
+ return {
44
+ get open() { return open; },
45
+ /** Stable id of the tooltip element (for `aria-describedby` + the tip's id). */
46
+ tipId: id,
47
+ show,
48
+ hide,
49
+ /** Spread onto the anchor/trigger wrapper. Also mirror `tipId` onto the real
50
+ * focusable child's `aria-describedby` so AT announces it on focus. */
51
+ anchorProps: () => ({
52
+ 'aria-describedby': open ? id : undefined,
53
+ onpointerenter: show,
54
+ onpointerleave: hide,
55
+ onfocusin: show,
56
+ onfocusout: hide,
57
+ }),
58
+ /** Spread onto the tooltip element. */
59
+ tooltipProps: () => ({ id, role: 'tooltip' }),
60
+ };
61
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Pure, framework-free IANA-timezone helpers, built on `Intl` (no date library).
3
+ *
4
+ * The scheduler renders in a chosen `timeZone` via a "pseudo-local" trick:
5
+ * {@link toZonedLocal} turns an absolute instant into a Date whose *local*
6
+ * wall-clock equals the wall-clock in `timeZone`, so all the existing
7
+ * local-based date math (startOfDay, minuteOfDay, layout) runs unchanged; then
8
+ * {@link fromZonedLocal} converts a user edit back to a real instant. When
9
+ * `timeZone` is undefined every function is the identity on the browser's zone.
10
+ *
11
+ * These functions are DST-aware and take no ambient "now" (safe for the model /
12
+ * headless contexts that forbid `Date.now()`).
13
+ */
14
+ export type ZoneParts = {
15
+ year: number;
16
+ month: number;
17
+ day: number;
18
+ hour: number;
19
+ minute: number;
20
+ second: number;
21
+ };
22
+ /** The wall-clock parts of `instant` in `timeZone` (browser-local if undefined). */
23
+ export declare function zoneParts(instant: Date, timeZone?: string): ZoneParts;
24
+ /** Milliseconds `timeZone` is ahead of UTC at `instant` (DST-aware; negative west of UTC). */
25
+ export declare function zoneOffsetMs(instant: Date, timeZone?: string): number;
26
+ /** An instant -> a Date whose LOCAL fields equal its wall-clock in `timeZone`. */
27
+ export declare function toZonedLocal(instant: Date, timeZone?: string): Date;
28
+ /** The instant whose wall-clock in `timeZone` is the given Y-M-D H:M:S. */
29
+ export declare function instantFromWallClock(year: number, month: number, // 1-12
30
+ day: number, hour: number, minute: number, second?: number, timeZone?: string, ms?: number): Date;
31
+ /** Inverse of {@link toZonedLocal}: a pseudo-local Date -> the real instant. */
32
+ export declare function fromZonedLocal(pseudo: Date, timeZone?: string): Date;
33
+ /** Short zone name at `instant`, e.g. "EDT" / "GMT+5:30" - for a ruler header. */
34
+ export declare function zoneAbbr(instant: Date, timeZone?: string): string;
35
+ /** Validate an IANA zone id; returns it if usable, else undefined (falls back to local). */
36
+ export declare function normalizeTimeZone(timeZone?: string): string | undefined;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Pure, framework-free IANA-timezone helpers, built on `Intl` (no date library).
3
+ *
4
+ * The scheduler renders in a chosen `timeZone` via a "pseudo-local" trick:
5
+ * {@link toZonedLocal} turns an absolute instant into a Date whose *local*
6
+ * wall-clock equals the wall-clock in `timeZone`, so all the existing
7
+ * local-based date math (startOfDay, minuteOfDay, layout) runs unchanged; then
8
+ * {@link fromZonedLocal} converts a user edit back to a real instant. When
9
+ * `timeZone` is undefined every function is the identity on the browser's zone.
10
+ *
11
+ * These functions are DST-aware and take no ambient "now" (safe for the model /
12
+ * headless contexts that forbid `Date.now()`).
13
+ */
14
+ const fmtCache = new Map();
15
+ function partsFormat(timeZone) {
16
+ let f = fmtCache.get(timeZone);
17
+ if (!f) {
18
+ f = new Intl.DateTimeFormat('en-US', {
19
+ timeZone,
20
+ hourCycle: 'h23',
21
+ year: 'numeric',
22
+ month: '2-digit',
23
+ day: '2-digit',
24
+ hour: '2-digit',
25
+ minute: '2-digit',
26
+ second: '2-digit',
27
+ });
28
+ fmtCache.set(timeZone, f);
29
+ }
30
+ return f;
31
+ }
32
+ /** The wall-clock parts of `instant` in `timeZone` (browser-local if undefined). */
33
+ export function zoneParts(instant, timeZone) {
34
+ if (!timeZone) {
35
+ return {
36
+ year: instant.getFullYear(),
37
+ month: instant.getMonth() + 1,
38
+ day: instant.getDate(),
39
+ hour: instant.getHours(),
40
+ minute: instant.getMinutes(),
41
+ second: instant.getSeconds(),
42
+ };
43
+ }
44
+ const map = {};
45
+ for (const p of partsFormat(timeZone).formatToParts(instant)) {
46
+ if (p.type !== 'literal')
47
+ map[p.type] = Number(p.value);
48
+ }
49
+ let hour = map.hour ?? 0;
50
+ if (hour === 24)
51
+ hour = 0; // some engines emit '24' for midnight under h23
52
+ return { year: map.year, month: map.month, day: map.day, hour, minute: map.minute ?? 0, second: map.second ?? 0 };
53
+ }
54
+ /** Milliseconds `timeZone` is ahead of UTC at `instant` (DST-aware; negative west of UTC). */
55
+ export function zoneOffsetMs(instant, timeZone) {
56
+ if (!timeZone)
57
+ return -instant.getTimezoneOffset() * 60000;
58
+ const p = zoneParts(instant, timeZone);
59
+ const asUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
60
+ // `asUtc` reads the wall-clock as if it were UTC; its distance from the real
61
+ // instant (truncated to whole seconds, matching the parts' resolution) is the offset.
62
+ return asUtc - Math.floor(instant.getTime() / 1000) * 1000;
63
+ }
64
+ /** An instant -> a Date whose LOCAL fields equal its wall-clock in `timeZone`. */
65
+ export function toZonedLocal(instant, timeZone) {
66
+ if (!timeZone)
67
+ return instant;
68
+ const p = zoneParts(instant, timeZone);
69
+ return new Date(p.year, p.month - 1, p.day, p.hour, p.minute, p.second, instant.getMilliseconds());
70
+ }
71
+ /** The instant whose wall-clock in `timeZone` is the given Y-M-D H:M:S. */
72
+ export function instantFromWallClock(year, month, // 1-12
73
+ day, hour, minute, second = 0, timeZone, ms = 0) {
74
+ if (!timeZone)
75
+ return new Date(year, month - 1, day, hour, minute, second, ms);
76
+ const wallUtc = Date.UTC(year, month - 1, day, hour, minute, second, ms);
77
+ // Assume the wall-clock is UTC, subtract the offset there, then correct once
78
+ // in case the candidate instant lands on the other side of a DST transition.
79
+ const off1 = zoneOffsetMs(new Date(wallUtc), timeZone);
80
+ let inst = new Date(wallUtc - off1);
81
+ const off2 = zoneOffsetMs(inst, timeZone);
82
+ if (off2 !== off1)
83
+ inst = new Date(wallUtc - off2);
84
+ return inst;
85
+ }
86
+ /** Inverse of {@link toZonedLocal}: a pseudo-local Date -> the real instant. */
87
+ export function fromZonedLocal(pseudo, timeZone) {
88
+ if (!timeZone)
89
+ return pseudo;
90
+ return instantFromWallClock(pseudo.getFullYear(), pseudo.getMonth() + 1, pseudo.getDate(), pseudo.getHours(), pseudo.getMinutes(), pseudo.getSeconds(), timeZone, pseudo.getMilliseconds());
91
+ }
92
+ /** Short zone name at `instant`, e.g. "EDT" / "GMT+5:30" - for a ruler header. */
93
+ export function zoneAbbr(instant, timeZone) {
94
+ if (!timeZone)
95
+ return '';
96
+ const parts = new Intl.DateTimeFormat('en-US', { timeZone, hour: '2-digit', timeZoneName: 'short' }).formatToParts(instant);
97
+ return parts.find((p) => p.type === 'timeZoneName')?.value ?? '';
98
+ }
99
+ /** Validate an IANA zone id; returns it if usable, else undefined (falls back to local). */
100
+ export function normalizeTimeZone(timeZone) {
101
+ if (!timeZone)
102
+ return undefined;
103
+ try {
104
+ new Intl.DateTimeFormat('en-US', { timeZone });
105
+ return timeZone;
106
+ }
107
+ catch {
108
+ return undefined;
109
+ }
110
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Shared context for <SvDockLayout> and its recursive <DockNodeView>. Carries
3
+ * the pane-content snippet and the handful of callbacks the tree view needs, so
4
+ * the recursive renderer reaches them without prop-drilling through every level.
5
+ */
6
+ import type { Snippet } from 'svelte';
7
+ import type { DockPane, DockTabs, DockZone } from './dock-model';
8
+ export type DockContext = {
9
+ /** Renders a pane's content. Called as `{@render pane(dockPane)}`. */
10
+ pane: Snippet<[DockPane]>;
11
+ /**
12
+ * Optional stack-header controls for a leaf (maximize / auto-hide / pop-out /
13
+ * float), rendered once per leaf at the right of its tab strip.
14
+ */
15
+ leafActions?: Snippet<[DockTabs]>;
16
+ /** Make tab `index` active in the given leaf. */
17
+ activate: (tabsId: string, index: number) => void;
18
+ /** Close (remove) a pane. */
19
+ close: (paneId: string) => void;
20
+ /** Start dragging a pane's tab to re-dock it. */
21
+ beginDrag: (event: PointerEvent, paneId: string, tabsId: string) => void;
22
+ /** Commit new size weights for a group after a splitter drag. */
23
+ resize: (groupId: string, sizes: number[]) => void;
24
+ /** Minimum pane size (px) along the split axis. */
25
+ minSize: () => number;
26
+ /**
27
+ * The leaf the pointer is over while dragging, for the dock guide. `zone` is
28
+ * the guide chip under the pointer (drop docks there) or `null` (over the leaf
29
+ * but off the guide - the drop will float). `centerOnly` shows just the centre
30
+ * chip (floating windows accept tabs only).
31
+ */
32
+ dropTarget: () => {
33
+ tabsId: string;
34
+ zone: DockZone | null;
35
+ centerOnly?: boolean;
36
+ } | null;
37
+ /** The live tab-reorder target, for the insertion-line indicator. */
38
+ reorderTarget?: () => {
39
+ tabsId: string;
40
+ index: number;
41
+ } | null;
42
+ /** Which side of each leaf the tab strip sits on. Default `'top'`. */
43
+ headerPosition?: () => 'top' | 'bottom' | 'left' | 'right';
44
+ /** The id of the currently focused leaf, for the active-panel highlight. */
45
+ focusedLeaf?: () => string | null;
46
+ /** Keep inactive tabs mounted (hidden) so their content state persists. */
47
+ keepAlive?: () => boolean;
48
+ /** For a single-pane leaf, suppress the redundant tab button (e.g. an auto-hide fly-out,
49
+ * where the edge strip already names the panel). Leaf actions still render. */
50
+ hideSingleTab?: () => boolean;
51
+ };
52
+ export declare const DOCK_CONTEXT: unique symbol;
@@ -0,0 +1 @@
1
+ export const DOCK_CONTEXT = Symbol('svgrid-dock');