@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.
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
@@ -10,6 +10,56 @@ import type {
10
10
  } from "./index";
11
11
  import type { ConditionalFormat } from "./conditional-formatting";
12
12
  import type { MenuItem } from "./SvMenuList.svelte";
13
+ import type { RecurrenceRule } from "./recurrence";
14
+
15
+ /** The calendar views the scheduler can render. {@link SchedulerConfig}. The
16
+ * `timeline*` views are horizontal: time runs left→right and resources are
17
+ * rows (a single "All" row without `resourceField`). */
18
+ export type SchedulerView =
19
+ | "month"
20
+ | "week"
21
+ | "day"
22
+ | "agenda"
23
+ | "timelineDay"
24
+ | "timelineWeek"
25
+ | "timelineMonth"
26
+ | "timelineYear";
27
+
28
+ /**
29
+ * How overlapping ("colliding") time-grid events are laid out:
30
+ * - `split` - every collision divides the column width evenly (default).
31
+ * - `cap` - show up to `maxColumns` columns; the rest collapse into a
32
+ * clickable `+N more` overflow tile.
33
+ * - `stack` - overlapping events overlap with a horizontal offset + z-order
34
+ * instead of shrinking, so each stays readable (hover to raise).
35
+ */
36
+ export type SchedulerCollisionMode = "split" | "cap" | "stack";
37
+
38
+ /** A scheduler resource - a person / room / machine an event can be assigned to. */
39
+ export type SchedulerResource = {
40
+ id: string;
41
+ title?: string;
42
+ color?: string;
43
+ /**
44
+ * This resource's own working windows (hours), optionally per weekday - e.g. a
45
+ * doctor available Mon/Wed/Fri 9-13 and Tue/Thu 14-18. Overrides the global
46
+ * `businessHours` / `nonWorkingDays` for this resource's columns: time outside
47
+ * every matching window is shaded, and (with `restrictToBusinessHours`)
48
+ * non-bookable. A weekday with no matching window is a full day off.
49
+ */
50
+ availability?: ReadonlyArray<{ days?: ReadonlyArray<number>; start: number; end: number }>;
51
+ /**
52
+ * Per-date overrides to this resource's weekly `availability` - a specific day
53
+ * off (`off: true`, e.g. vacation) or custom hours (`windows`) for that one
54
+ * date. Matched by calendar date; takes precedence over `availability` for the
55
+ * matching day.
56
+ */
57
+ dateOverrides?: ReadonlyArray<{
58
+ date: Date | number | string;
59
+ off?: boolean;
60
+ windows?: ReadonlyArray<{ start: number; end: number }>;
61
+ }>;
62
+ };
13
63
  import type {
14
64
  ChartType,
15
65
  ChartSpec,
@@ -450,6 +500,398 @@ export type BoardConfig<TFeatures extends TableFeatures = TableFeatures, TData e
450
500
  onLayoutChange?: (layout: BoardLayout) => void;
451
501
  };
452
502
 
503
+ /**
504
+ * The built-in event detail drawer for the scheduler. Set `scheduler.drawer`
505
+ * (`true` for all fields, or this object) and clicking an event opens a drawer
506
+ * with an `SvForm` of its fields. Mirrors {@link BoardDrawerConfig}.
507
+ */
508
+ export type SchedulerDrawerConfig<TData extends RowData = RowData> = {
509
+ /** Fields to show, in order. Omit for every column that has a `field`. */
510
+ fields?: ReadonlyArray<keyof TData & string>;
511
+ /** Drawer title - a string or derived from the row. Defaults to the title field. */
512
+ title?: string | ((row: TData) => string);
513
+ /** Which edge the drawer opens from. Defaults to `'right'`. */
514
+ side?: "right" | "left" | "top" | "bottom";
515
+ /** Drawer size (any CSS length). Defaults to `'380px'`. */
516
+ size?: string;
517
+ /** Save button label. Defaults to `'Save'`. */
518
+ submitLabel?: string;
519
+ /** Form columns inside the drawer. Defaults to `1`. */
520
+ columns?: number;
521
+ };
522
+
523
+ /** Emitted when an event is dragged to a new time (and/or resource). */
524
+ export type SchedulerEventMoveEvent<TData extends RowData = RowData> = {
525
+ /** The dragged row. */
526
+ row: TData;
527
+ /** New start / end after the move. */
528
+ start: Date;
529
+ end: Date;
530
+ /** Whether the event is all-day after the move. */
531
+ allDay: boolean;
532
+ /** Resource it came from (only with `resourceField`). */
533
+ fromResource?: string;
534
+ /** Resource it was dropped on (only with `resourceField`). */
535
+ toResource?: string;
536
+ };
537
+
538
+ /** Emitted when an event's start or end edge is dragged to resize it. */
539
+ export type SchedulerEventResizeEvent<TData extends RowData = RowData> = {
540
+ row: TData;
541
+ start: Date;
542
+ end: Date;
543
+ };
544
+
545
+ /**
546
+ * A drag-selected range (see `rangeSelectable`). `start`/`end` are a single
547
+ * CONTINUOUS datetime range - dragging across days makes it a longer span, not a
548
+ * per-day rectangle. `days` / `resourceIds` list the days / resources the range
549
+ * touched (for reference); `allDay` is true when the all-day row was dragged.
550
+ */
551
+ export type SchedulerRangeSelection = {
552
+ start: Date;
553
+ end: Date;
554
+ allDay?: boolean;
555
+ /** Every day the range touched (one entry for a within-a-day drag). */
556
+ days: Date[];
557
+ /** Every resource the range touched (empty without `resourceField`). */
558
+ resourceIds: string[];
559
+ };
560
+
561
+ /** Emitted when an event's built-in editor/drawer is saved. */
562
+ export type SchedulerEventCommitEvent<TData extends RowData = RowData> = {
563
+ row: TData;
564
+ /** Only the fields that changed, `{ field: newValue }`. */
565
+ changes: Record<string, unknown>;
566
+ /** The full set of edited field values. */
567
+ values: Record<string, unknown>;
568
+ };
569
+
570
+ /**
571
+ * One override of a single occurrence of a recurring event (stored in the row's
572
+ * `recurrenceExceptionsField`). `occurrenceStart` identifies the occurrence (its
573
+ * original start, like an iCal `RECURRENCE-ID`); `deleted` removes just that one,
574
+ * otherwise the given fields override it. Times are the same ISO / Date shape you
575
+ * store on the row.
576
+ */
577
+ export type SchedulerException = {
578
+ occurrenceStart: string | number | Date;
579
+ deleted?: boolean;
580
+ start?: string | number | Date;
581
+ end?: string | number | Date;
582
+ title?: string;
583
+ allDay?: boolean;
584
+ };
585
+
586
+ /** How a change to a recurring event should apply. */
587
+ export type SchedulerEditScope = "occurrence" | "following" | "series";
588
+
589
+ /**
590
+ * Fired when a single occurrence of a recurring event is edited or deleted (the
591
+ * user chose "This event"). Append/merge `exception` into the row's
592
+ * `recurrenceExceptionsField`; for `deleted` it carries `{ deleted: true }`.
593
+ */
594
+ export type SchedulerOccurrenceChangeEvent<TData extends RowData = RowData> = {
595
+ row: TData;
596
+ /** The occurrence's original start (its identity within the series). */
597
+ occurrenceStart: Date;
598
+ /** The override to store (already in real-instant time). */
599
+ exception: SchedulerException;
600
+ /**
601
+ * Which scope the user chose. `'occurrence'` (default): merge `exception` into
602
+ * the row. `'following'` ("this and all following"): split the series - stop
603
+ * the original series before `occurrenceStart` and start a NEW series at it
604
+ * carrying `exception`'s start/end/deleted. `'series'` never reaches here (it
605
+ * edits the base row directly). Handle `'following'` in your own data.
606
+ */
607
+ scope: SchedulerEditScope;
608
+ };
609
+
610
+ /**
611
+ * Turns the grid into a calendar / scheduler. Set `scheduler` and the grid
612
+ * renders its rows as events on a Month / Week / Day / Agenda calendar (bucketed
613
+ * by time, and optionally split into per-resource columns) instead of a table.
614
+ * Dragging an event fires {@link SchedulerConfig.onEventMove} where you reassign
615
+ * the start / end (and resource) on your own data. Like the Kanban board, it is
616
+ * a pure *view of the grid*: it renders the grid's filtered + sorted rows and
617
+ * writes back only through callbacks, never mutating your data.
618
+ */
619
+ export type SchedulerConfig<
620
+ TFeatures extends TableFeatures = TableFeatures,
621
+ TData extends RowData = RowData,
622
+ > = {
623
+ /** Field holding each event's start (`Date` | epoch-ms | ISO string). Required. */
624
+ startField: keyof TData & string;
625
+ /** Field holding the end. Omit to use `defaultDurationMin` from the start. */
626
+ endField?: keyof TData & string;
627
+ /** Boolean field marking an event as all-day (rendered in the all-day row). */
628
+ allDayField?: keyof TData & string;
629
+ /** Field for the event title. Defaults to the first column's field. */
630
+ titleField?: keyof TData & string;
631
+ /** Field holding a per-event accent color (any CSS color). Else `color`. */
632
+ colorField?: keyof TData & string;
633
+ /** Fallback accent color for every event. */
634
+ color?: string;
635
+ /**
636
+ * Field holding a SECONDARY accent color, rendered as a strip on the LEFT edge
637
+ * of the event - distinct from the main `colorField`. Lets an event encode two
638
+ * dimensions at once (e.g. main color = person, left strip = role).
639
+ */
640
+ secondaryColorField?: keyof TData & string;
641
+ /**
642
+ * Field holding a free/busy status that drives a distinct visual treatment
643
+ * (Outlook-style): `'busy'` (default solid), `'free'` (hollow / outline),
644
+ * `'tentative'` (hatched), `'oof'` / `'outOfOffice'` (distinct tint). Ties to
645
+ * iCal `STATUS`. Unrecognised values render as busy.
646
+ */
647
+ statusField?: keyof TData & string;
648
+ /**
649
+ * Field holding minutes-before-start for a reminder. When set (and the event
650
+ * is upcoming) the scheduler fires `onReminder` once as that lead time is
651
+ * crossed - pair it with a toast. Ties to iCal `VALARM`.
652
+ */
653
+ reminderField?: keyof TData & string;
654
+ /** Fired once when an event's reminder lead time is crossed (see `reminderField`). */
655
+ onReminder?: (row: TData, minutesUntil: number) => void;
656
+ /**
657
+ * Field holding a {@link RecurrenceRule} (or array) - the row renders as one
658
+ * event per matching day in view, keeping its time-of-day + duration.
659
+ */
660
+ recurrenceField?: keyof TData & string;
661
+ /**
662
+ * Field holding an array of {@link SchedulerException} - per-occurrence
663
+ * overrides of a recurring event (a moved / edited / deleted single instance).
664
+ * When set, editing a recurring occurrence offers "This event" vs "All events"
665
+ * and "This event" fires {@link onOccurrenceChange}.
666
+ */
667
+ recurrenceExceptionsField?: keyof TData & string;
668
+ /** Event length in minutes when a row has a start but no `endField`. Default 60. */
669
+ defaultDurationMin?: number;
670
+
671
+ /** Which views to offer in the toolbar. Default all four. */
672
+ views?: ReadonlyArray<SchedulerView>;
673
+ /** The view shown first. Default `'month'`. */
674
+ initialView?: SchedulerView;
675
+ /** The date the calendar opens on. Defaults to "today". */
676
+ initialDate?: Date | number | string;
677
+ /**
678
+ * Controlled anchor date - when this changes the calendar navigates to it (a
679
+ * mini date-picker or external "go to" can drive the view). Pair with
680
+ * {@link onNavigate} for two-way sync.
681
+ */
682
+ date?: Date | number | string;
683
+ /** Fired when the visible date changes (prev / next / today). */
684
+ onNavigate?: (date: Date) => void;
685
+ /** First day of the week, 0-6 (0 = Sunday). Default 0. */
686
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
687
+ /** Time-grid slot size in minutes (week / day snap granularity). Default 30. */
688
+ slotMinutes?: number;
689
+ /**
690
+ * Slot sizes (minutes) offered as a runtime picker in the Week / Day time-grid
691
+ * ruler, e.g. `[60, 30, 15, 5]`. When set, a size selector appears in the
692
+ * toolbar and the ruler subdivides accordingly; the active size starts at
693
+ * `slotMinutes`. Omit to hide the picker (the ruler still uses `slotMinutes`).
694
+ */
695
+ slotSizes?: ReadonlyArray<number>;
696
+ /** First / last hour shown in the time-grid band (0-24). Default 0..24. */
697
+ dayStartHour?: number;
698
+ dayEndHour?: number;
699
+
700
+ // --- booking rules: working hours, non-working days, conflicts -------------
701
+ /**
702
+ * Working-hours window (hours, e.g. `{ start: 9, end: 17 }`). Time outside it
703
+ * is shaded in the Week / Day time-grid. Visual only - see `disableConflicts`
704
+ * to enforce.
705
+ */
706
+ businessHours?: { start: number; end: number };
707
+ /** Weekday numbers (0 = Sun … 6 = Sat) shaded as non-working (e.g. `[0, 6]`). */
708
+ nonWorkingDays?: ReadonlyArray<number>;
709
+ /** Shade elapsed time on today's column (the past is "used up"). */
710
+ shadeUntilNow?: boolean;
711
+ /**
712
+ * Enforce the shaded time: reject a drag / resize / create that lands outside
713
+ * `businessHours` or on a `nonWorkingDays` day (snap back + flash). Without
714
+ * this the shading is a visual hint only.
715
+ */
716
+ restrictToBusinessHours?: boolean;
717
+ /**
718
+ * Prevent double-booking: dragging / resizing / creating an event so it
719
+ * overlaps another event **on the same resource** is rejected and reverted.
720
+ */
721
+ disableConflicts?: boolean;
722
+ /**
723
+ * Hard-blocked hour bands (e.g. a daily maintenance window `[{ start: 12, end: 13 }]`).
724
+ * Unlike `businessHours` these are ALWAYS non-bookable (drop / create rejected,
725
+ * no opt-in needed) and rendered as a distinct restricted (hatched) band.
726
+ */
727
+ restrictedHours?: ReadonlyArray<{ start: number; end: number }>;
728
+ /** Specific calendar dates that are fully blocked (no bookings) - e.g. closures. */
729
+ restrictedDates?: ReadonlyArray<Date | number | string>;
730
+ /**
731
+ * Highlighted dates (holidays, launches …) - a coloured strip + optional label
732
+ * on that day's column / month cell. Decorative; does not block booking.
733
+ */
734
+ specialDates?: ReadonlyArray<{ date: Date | number | string; label?: string; color?: string }>;
735
+ /** Earliest navigable / bookable date. Prev-nav stops here and creates are blocked before it. */
736
+ minDate?: Date | number | string;
737
+ /** Latest navigable / bookable date. Next-nav stops here and creates are blocked after it. */
738
+ maxDate?: Date | number | string;
739
+ /**
740
+ * Cap concurrent events per resource at any moment: a drag / resize / create
741
+ * that would make more than N events overlap on the same resource is rejected.
742
+ */
743
+ maxEventsPerSlot?: number;
744
+ /**
745
+ * Enable **undo / redo** of drag-move + resize with `Ctrl/Cmd+Z` and
746
+ * `Ctrl/Cmd+Shift+Z` (or `Ctrl+Y`). The scheduler re-emits the move/resize
747
+ * callbacks with the reversed values, so your data follows.
748
+ */
749
+ history?: boolean;
750
+ /**
751
+ * Show the current-time indicator - a line across today's column(s) in the
752
+ * Week / Day time-grid and the Day timeline, tracking the local time. On by
753
+ * default; set to `false` to hide it.
754
+ */
755
+ nowIndicator?: boolean;
756
+ /**
757
+ * IANA time zone (e.g. `'America/New_York'`) the whole calendar is shown in -
758
+ * the hour ruler, event positions, day boundaries, all-day grouping and the
759
+ * now-line. Defaults to the browser's local zone. Event `start`/`end` must be
760
+ * instant-unambiguous (UTC / offset ISO or epoch ms) for this to be correct;
761
+ * bare local strings are read as wall-clock in this zone.
762
+ */
763
+ timeZone?: string;
764
+ /**
765
+ * Extra read-only hour rulers for other zones (a "world clock"), shown left of
766
+ * the primary ruler in the Week / Day time-grid. Each entry's `label` heads its
767
+ * column (defaults to a short zone abbreviation).
768
+ */
769
+ secondaryTimeZones?: ReadonlyArray<{ id: string; label?: string }>;
770
+ /**
771
+ * How overlapping time-grid events are laid out. `split` (default) divides the
772
+ * width evenly per collision; `cap` shows up to `maxColumns` then a clickable
773
+ * `+N more` tile; `stack` overlaps them with an offset instead of shrinking.
774
+ */
775
+ collisionMode?: SchedulerCollisionMode;
776
+ /** For `collisionMode: 'cap'`: max side-by-side columns before overflow (min 2). Default 3. */
777
+ maxColumns?: number;
778
+ /** How many days the agenda view spans. Default 30. */
779
+ agendaDays?: number;
780
+
781
+ /**
782
+ * Field whose value groups events by resource (people, rooms, machines). In
783
+ * the Week / Day time-grid the columns split into resource x day groups; a
784
+ * resource filter/legend appears in every view. See {@link SchedulerResource}.
785
+ */
786
+ resourceField?: keyof TData & string;
787
+ /** Explicit, ordered resources (with colors). Omit to derive from the data. */
788
+ resources?: ReadonlyArray<SchedulerResource>;
789
+ /**
790
+ * Time-grid resource grouping order. `false` (default) groups by resource then
791
+ * day (resource-major); `true` groups by day then resource (date-major), like
792
+ * Smart's `groupByDate`. Only affects Week / Day when `resourceField` is set.
793
+ */
794
+ groupByDate?: boolean;
795
+
796
+ /** Timeline views only: width (px) of the left resource-label gutter. Default 160. */
797
+ resourceAreaWidth?: number;
798
+ /** `timelineDay` tick size + move/resize snap, in minutes. Default = `slotMinutes`. */
799
+ timelineSlotMinutes?: number;
800
+ /** Timeline views only: height (px) of one event lane inside a resource row. Default 26. */
801
+ timelineLaneHeight?: number;
802
+
803
+ /** Enable drag-to-move and edge-resize. Without it the calendar is read-only. */
804
+ editable?: boolean;
805
+ /** Fired when an event is dragged to a new time / resource. */
806
+ onEventMove?: (event: SchedulerEventMoveEvent<TData>) => void;
807
+ /** Fired when an event edge is dragged to resize it. */
808
+ onEventResize?: (event: SchedulerEventResizeEvent<TData>) => void;
809
+ /**
810
+ * Fired to create an event - double-clicking an empty slot, or confirming a
811
+ * pending range when no {@link onRangeSelect} is set. `allDay` is `true` when
812
+ * the slot / range is an all-day one (the all-day row, a month day cell, or a
813
+ * multi-day timeline zoom), so the handler can set the `allDayField`.
814
+ */
815
+ onEventAdd?: (start: Date, end: Date, resourceId?: string, allDay?: boolean) => void;
816
+ /**
817
+ * Fired when the event's Delete button is used. Setting this shows a Delete
818
+ * button in the detail drawer (and a `Delete` item is easy to add via
819
+ * `eventMenu`). Remove the row from your data in the handler.
820
+ */
821
+ onEventDelete?: (row: TData) => void;
822
+ /**
823
+ * Fired when a single occurrence of a recurring event is moved / resized /
824
+ * deleted with scope "This event". Merge `event.exception` into the row's
825
+ * `recurrenceExceptionsField` array (keyed by `occurrenceStart`).
826
+ */
827
+ onOccurrenceChange?: (event: SchedulerOccurrenceChangeEvent<TData>) => void;
828
+
829
+ /**
830
+ * Selecting empty cells: click or drag across the time-grid / timeline to mark
831
+ * a time range (and, across days / resources, a rectangle of cells). Arrow keys
832
+ * move the selection and `Shift`+arrows extend it. Marking only *marks* the
833
+ * range - it does NOT create anything. The event is created on an explicit
834
+ * confirm: press `Enter`, or right-click the selection and choose "Add Event".
835
+ * Confirming fires {@link onRangeSelect}, or falls back to `onEventAdd` for the
836
+ * first cell; `Escape` cancels the marker.
837
+ *
838
+ * On by default; set to `false` to disable.
839
+ */
840
+ rangeSelectable?: boolean;
841
+ /**
842
+ * Fired when a pending cell range is confirmed with `Enter` or the "Add Event"
843
+ * context menu (see {@link rangeSelectable}) - not on drag release.
844
+ */
845
+ onRangeSelect?: (selection: SchedulerRangeSelection) => void;
846
+ /**
847
+ * Multi-selecting existing events: Ctrl/Cmd-click toggles, Shift-click
848
+ * range-selects; a plain click clears. Selected events move together when one
849
+ * is dragged, and `Delete` removes them (via `onEventDelete`).
850
+ *
851
+ * On by default; set to `false` to disable.
852
+ */
853
+ eventSelectable?: boolean;
854
+ /** Fired when the set of multi-selected events changes. */
855
+ onEventSelectionChange?: (rows: TData[]) => void;
856
+
857
+ /**
858
+ * An "unscheduled" backlog list shown beside the Week / Day grid: drag an item
859
+ * onto the calendar to schedule it. Each item is `{ id, title, durationMin?,
860
+ * color? }`. Dropping fires {@link onSchedule} with the drop time + resource.
861
+ */
862
+ unscheduled?: ReadonlyArray<{ id: string; title: string; durationMin?: number; color?: string }>;
863
+ /** Optional heading for the unscheduled backlog panel. Default "Unscheduled". */
864
+ backlogTitle?: string;
865
+ /** Fired when an unscheduled item is dropped on the grid - create an event from it. */
866
+ onSchedule?: (item: { id: string; title: string; durationMin?: number; color?: string }, start: Date, resourceId?: string) => void;
867
+ /**
868
+ * Fired when an event is dragged OFF the calendar onto the backlog panel -
869
+ * remove it from the schedule (and typically push it back to `unscheduled`).
870
+ * Requires the backlog panel (an `unscheduled` list) to be visible.
871
+ */
872
+ onUnschedule?: (row: TData) => void;
873
+
874
+ /** Custom event body. Receives the row. Omit for the built-in default (title). */
875
+ event?: Snippet<[TData]>;
876
+ /**
877
+ * Hover tooltip for an event. Set a `Snippet<[TData]>` for custom content, or
878
+ * `true` for the built-in tooltip (title + time + resource). Omit to disable.
879
+ */
880
+ tooltip?: boolean | Snippet<[TData]>;
881
+ /** Delay (ms) before the hover tooltip opens. Default 400. */
882
+ tooltipDelay?: number;
883
+ /** Built-in detail drawer: `true` for all fields, or a config object. */
884
+ drawer?: boolean | SchedulerDrawerConfig<TData>;
885
+ /** Fired when the drawer / editor is saved. */
886
+ onEventCommit?: (event: SchedulerEventCommitEvent<TData>) => void;
887
+ /** Right-click menu for an event. Return items or `undefined` to suppress. */
888
+ eventMenu?: (row: TData) => MenuItem[] | undefined;
889
+
890
+ /** Show the search box (binds to the grid's global filter). Default `true`. */
891
+ searchable?: boolean;
892
+ searchPlaceholder?: string;
893
+ };
894
+
453
895
  export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData> = {
454
896
  data: ReadonlyArray<TData>;
455
897
  columns: Array<ColumnDef<TFeatures, TData>>;
@@ -459,6 +901,12 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
459
901
  * {@link BoardConfig}.
460
902
  */
461
903
  board?: BoardConfig<TFeatures, TData>;
904
+ /**
905
+ * Scheduler / calendar mode. When set, the grid renders its rows as events on
906
+ * a Month / Week / Day / Agenda calendar instead of a table. See
907
+ * {@link SchedulerConfig}.
908
+ */
909
+ scheduler?: SchedulerConfig<TFeatures, TData>;
462
910
  /**
463
911
  * Right-click context menu. `true` shows the default item set (copy, cut,
464
912
  * paste, clear, insert row above/below, remove row, remove column). Pass an
@@ -808,6 +1256,13 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
808
1256
  * or omit it to apply to every column. Later entries win on conflict.
809
1257
  */
810
1258
  conditionalFormats?: ReadonlyArray<ConditionalFormat<TData>>;
1259
+ /**
1260
+ * Which rows feed the min/max range that `colorScale` / `dataBar` formats
1261
+ * scale against. `visible` (default): the currently displayed rows (after
1262
+ * filtering + paging), so the heat map adapts to what's on screen. `all`:
1263
+ * the full unfiltered dataset, for a scale that stays put as you filter.
1264
+ */
1265
+ conditionalStatScope?: "visible" | "all";
811
1266
  onCellValueChange?: (event: {
812
1267
  rowIndex: number;
813
1268
  columnId: string;
@@ -1013,12 +1468,19 @@ export type CellEditState = {
1013
1468
  } | null;
1014
1469
  export type FilterOperator =
1015
1470
  | "contains"
1471
+ | "notContains"
1016
1472
  | "equals"
1473
+ | "notEquals"
1017
1474
  | "startsWith"
1475
+ | "endsWith"
1476
+ | "regex"
1477
+ | "in"
1478
+ | "notIn"
1018
1479
  | "greaterThan"
1019
1480
  | "lessThan"
1020
1481
  | "between"
1021
- | "isBlank";
1482
+ | "isBlank"
1483
+ | "isNotBlank";
1022
1484
  export type FilterOption = {
1023
1485
  value: FilterOperator;
1024
1486
  label: string;
@@ -88,6 +88,8 @@
88
88
  left: 0,
89
89
  width: 0,
90
90
  openUpward: false,
91
+ maxHeight: 0,
92
+ availHeight: 0,
91
93
  })
92
94
 
93
95
  const selectedArr = $derived(
@@ -376,7 +378,12 @@
376
378
  Panel is `position: fixed` + portal'd to <body> so it escapes
377
379
  the grid's overflow:hidden scroll container. -->
378
380
  {@const needsScroll = options.length > 10}
379
- {@const panelMax = needsScroll ? 10 * 32 + (multiple ? 40 : 0) + 8 : null}
381
+ <!-- Cap to the comfortable 10-row height AND the room the viewport allows,
382
+ so a panel near the bottom edge scrolls instead of overflowing. -->
383
+ {@const panelMax = Math.min(
384
+ needsScroll ? 10 * 32 + (multiple ? 40 : 0) + 8 : Number.POSITIVE_INFINITY,
385
+ panelRect.maxHeight,
386
+ )}
380
387
  <div
381
388
  class="sv-grid-dropdown-panel"
382
389
  class:sv-grid-dropdown-panel-fits={!needsScroll}
@@ -153,6 +153,7 @@
153
153
  style:top={`${gs.rect.top}px`}
154
154
  style:left={`${gs.rect.left}px`}
155
155
  style:min-width={`${Math.max(gs.rect.width, 320)}px`}
156
+ style:max-height={`${gs.rect.maxHeight}px`}
156
157
  onkeydown={gs.onPanelKeydown}
157
158
  >
158
159
  {#if searchable}
@@ -5,7 +5,7 @@
5
5
  * via `value` (scalar or array) + `onChange`. Scales to huge option sets with
6
6
  * `virtual` (fixed-row windowing), and each row can be an `itemTemplate`.
7
7
  */
8
- import { flushSync, type Snippet } from 'svelte'
8
+ import { type Snippet } from 'svelte'
9
9
  import { groupOptions, hasGroups, type ListOption } from './list-option'
10
10
  import { virtualRange, scrollToIndex } from './virtual'
11
11
  import { createListbox } from './createListbox.svelte'
@@ -101,17 +101,25 @@
101
101
  class:is-invalid={invalid}
102
102
  class:is-virtual={useVirtual}
103
103
  style:--sv-rows={rows}
104
- onscroll={(e) => { scrollTop = e.currentTarget.scrollTop; if (useVirtual) flushSync() }}
104
+ style:--sv-row-h={`${rowHeight}px`}
105
+ onscroll={(e) => (scrollTop = e.currentTarget.scrollTop)}
105
106
  bind:clientHeight={viewportH}
106
107
  {...lb.rootProps()}
107
108
  >
108
109
  {#if useVirtual}
109
- <!-- Fixed-height sizer establishes the scroll range; rows are absolutely
110
- positioned by transform so scrolling never reflows (no flash). -->
111
- <li class="sv-listbox__sizer" aria-hidden="true" style:height={`${vr.totalHeight}px`}></li>
110
+ <!-- Windowing by top/bottom spacers with the visible rows IN FLOW between
111
+ them (padTop + rows + padBottom always sum to the full height). Plain
112
+ flow rows (no absolute positioning, no will-change) keep the scroller
113
+ off its own compositor layer, so a fast jump repaints in step with the
114
+ scroll instead of showing the blank frame a promoted layer would. The
115
+ window updates from `scrollTop` state, which Svelte flushes before the
116
+ next paint - no flushSync (it over-flushes app-wide and, on a fling,
117
+ starves the very handler that has to keep up with the scroll). -->
118
+ <li class="sv-listbox__spacer" aria-hidden="true" style:height={`${vr.padTop}px`}></li>
112
119
  {#each windowed as opt (opt.value)}
113
120
  {@render optionLi(opt, opt.index)}
114
121
  {/each}
122
+ <li class="sv-listbox__spacer" aria-hidden="true" style:height={`${vr.padBottom}px`}></li>
115
123
  {:else}
116
124
  {#each groupOptions(options) as g (g.group ?? ' ')}
117
125
  {#if g.group != null}<li class="sv-listbox__group" role="presentation">{g.group}</li>{/if}
@@ -132,7 +140,6 @@
132
140
  class:is-active={lb.isActive(index)}
133
141
  class:is-disabled={opt.disabled}
134
142
  style:height={`${rowHeight}px`}
135
- style:transform={useVirtual ? `translateY(${index * rowHeight}px)` : undefined}
136
143
  {...lb.optionProps(index)}
137
144
  >
138
145
  {#if multiple}<span class="sv-listbox__check" aria-hidden="true">{isSel(opt) ? '✓' : ''}</span>{/if}
@@ -144,7 +151,7 @@
144
151
  .sv-listbox {
145
152
  --_accent: var(--sg-accent, #2563eb);
146
153
  margin: 0; padding: 4px; list-style: none;
147
- max-height: calc(var(--sv-rows, 7) * 32px + 8px); overflow-y: auto;
154
+ max-height: calc(var(--sv-rows, 7) * var(--sv-row-h, 32px) + 8px); overflow-y: auto;
148
155
  background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
149
156
  border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
150
157
  width: 220px; outline: none;
@@ -152,15 +159,30 @@
152
159
  .sv-listbox:focus-visible { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
153
160
  .sv-listbox.is-invalid { border-color: var(--sg-danger, #dc2626); }
154
161
  .sv-listbox.is-invalid:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
155
- /* Pre-promote the scrolling-contents layer at mount so the FIRST scroll
156
- doesn't trigger a one-time layer promotion + repaint (the initial flash). */
157
- .sv-listbox.is-virtual { position: relative; will-change: scroll-position; }
158
- .sv-listbox__sizer { padding: 0; margin: 0; pointer-events: none; }
159
- .sv-listbox.is-virtual .sv-listbox__opt {
160
- position: absolute; inset-inline: 4px; top: 4px;
162
+ /* Spacers reserve the off-screen height above/below the windowed rows. They
163
+ also paint faint per-row skeleton bars: on a fast scrollbar-thumb drag the
164
+ compositor can reveal this padding before JS re-windows (a drag jumps many
165
+ rows in one frame - overscan can't cover it), so the user would see blank.
166
+ The skeleton fills that gap with placeholder rows that the real rows paint
167
+ over a frame later. Bars are one per `--sv-row-h` band, aligned to the row
168
+ grid (padTop/padBottom are exact multiples of the row height). */
169
+ .sv-listbox__spacer {
170
+ padding: 0; margin: 0; flex: none; pointer-events: none; list-style: none;
171
+ background-image: linear-gradient(
172
+ to bottom,
173
+ transparent 0,
174
+ transparent calc((var(--sv-row-h, 32px) - 10px) / 2),
175
+ var(--sg-skeleton, color-mix(in srgb, currentColor 9%, transparent)) calc((var(--sv-row-h, 32px) - 10px) / 2),
176
+ var(--sg-skeleton, color-mix(in srgb, currentColor 9%, transparent)) calc((var(--sv-row-h, 32px) + 10px) / 2),
177
+ transparent calc((var(--sv-row-h, 32px) + 10px) / 2),
178
+ transparent var(--sv-row-h, 32px)
179
+ );
180
+ background-size: 62% var(--sv-row-h, 32px);
181
+ background-repeat: repeat-y;
182
+ background-position: 12px 0;
161
183
  }
162
184
  .sv-listbox__opt {
163
- display: flex; align-items: center; gap: 8px; box-sizing: border-box; padding: 0 10px;
185
+ display: flex; align-items: center; gap: 8px; box-sizing: border-box; flex: none; padding: 0 10px;
164
186
  border-radius: 6px; cursor: pointer; font-size: 13px;
165
187
  }
166
188
  .sv-listbox__opt.is-active { background: var(--sg-row-hover-bg, #f1f5f9); }
@@ -27,7 +27,7 @@
27
27
  }
28
28
 
29
29
  let {
30
- value = '',
30
+ value = $bindable(''),
31
31
  onChange,
32
32
  mask = '',
33
33
  placeholder,
@@ -53,7 +53,8 @@
53
53
 
54
54
  const mi = createMaskedInput({
55
55
  value: () => value,
56
- onChange: (m, r, c) => onChange?.(m, r, c),
56
+ // Write the masked `value` (so `bind:value` works) AND fire `onChange`.
57
+ onChange: (m, r, c) => { value = m; onChange?.(m, r, c) },
57
58
  mask: () => mask,
58
59
  placeholder: () => placeholder,
59
60
  disabled: () => disabled,
@@ -72,7 +73,7 @@
72
73
  {#if prefixIcon}<span class="sv-masked__adorn">{@render prefixIcon()}</span>{/if}
73
74
  <input class="sv-masked__input" {...mi.inputProps()} />
74
75
  {#if clearable && mi.masked && !disabled && !readonly}
75
- <button type="button" class="sv-masked__clear" aria-label="Clear" tabindex="-1" onclick={() => onChange?.('', '', false)}>
76
+ <button type="button" class="sv-masked__clear" aria-label="Clear" tabindex="-1" onclick={() => { value = ''; onChange?.('', '', false) }}>
76
77
  <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
77
78
  </button>
78
79
  {/if}
package/src/SvMenu.svelte CHANGED
@@ -45,7 +45,7 @@
45
45
 
46
46
  let anchorEl = $state<HTMLSpanElement | null>(null)
47
47
  let panelEl = $state<HTMLDivElement | null>(null)
48
- let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
48
+ let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
49
49
 
50
50
  function setOpen(v: boolean) { if (v !== open) { open = v; onOpenChange?.(v) } }
51
51
  function toggle() { setOpen(!open) }