@inkra/react 0.2.0 → 0.4.3

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 (246) hide show
  1. package/README.md +101 -7
  2. package/dist/Button.js +3 -2
  3. package/dist/PencilIcon.d.ts +10 -0
  4. package/dist/PencilIcon.js +83 -39
  5. package/dist/PencilLine.js +9 -1
  6. package/dist/PencilText.js +15 -9
  7. package/dist/accordion/Accordion.js +65 -5
  8. package/dist/alert-dialog/index.d.ts +1 -1
  9. package/dist/alert-dialog/index.js +18 -6
  10. package/dist/audio-player/index.d.ts +2 -0
  11. package/dist/audio-player/index.js +126 -31
  12. package/dist/calendar/index.d.ts +4 -1
  13. package/dist/calendar/index.js +31 -12
  14. package/dist/carousel/index.js +13 -6
  15. package/dist/choice-grid/index.d.ts +6 -1
  16. package/dist/choice-grid/index.js +61 -33
  17. package/dist/color-picker/GradientEditor.d.ts +39 -0
  18. package/dist/color-picker/GradientEditor.js +63 -0
  19. package/dist/color-picker/gradient.d.ts +25 -0
  20. package/dist/color-picker/gradient.js +60 -0
  21. package/dist/color-picker/index.d.ts +27 -7
  22. package/dist/color-picker/index.js +134 -32
  23. package/dist/color-swatch/index.d.ts +10 -0
  24. package/dist/color-swatch/index.js +9 -0
  25. package/dist/command/index.d.ts +24 -2
  26. package/dist/command/index.js +29 -6
  27. package/dist/content/index.d.ts +5 -0
  28. package/dist/content/index.js +12 -3
  29. package/dist/context-menu/index.d.ts +2 -2
  30. package/dist/context-menu/index.js +4 -3
  31. package/dist/data-table/index.d.ts +7 -1
  32. package/dist/data-table/index.js +64 -16
  33. package/dist/data-table/model.d.ts +2 -0
  34. package/dist/data-table/model.js +7 -0
  35. package/dist/date-picker/index.d.ts +2 -0
  36. package/dist/date-picker/index.js +10 -9
  37. package/dist/day-agenda/index.d.ts +6 -1
  38. package/dist/day-agenda/index.js +46 -21
  39. package/dist/day-agenda/layout.js +65 -9
  40. package/dist/display/ProgressRing.js +7 -3
  41. package/dist/display/index.d.ts +4 -1
  42. package/dist/display/index.js +3 -2
  43. package/dist/dockable-panel/index.js +5 -5
  44. package/dist/drawer/index.d.ts +1 -1
  45. package/dist/drawer/index.js +9 -6
  46. package/dist/dropdown-menu/index.d.ts +1 -1
  47. package/dist/dropdown-menu/index.js +3 -2
  48. package/dist/editable/index.d.ts +24 -2
  49. package/dist/editable/index.js +142 -37
  50. package/dist/empty/illustrations.d.ts +8 -0
  51. package/dist/empty/illustrations.js +78 -0
  52. package/dist/empty/index.d.ts +6 -1
  53. package/dist/empty/index.js +11 -3
  54. package/dist/flip/index.js +28 -8
  55. package/dist/form/EditablePencilText.js +34 -0
  56. package/dist/form/Field.js +11 -10
  57. package/dist/form/Input.js +1 -1
  58. package/dist/form/Textarea.d.ts +3 -1
  59. package/dist/form/Textarea.js +6 -4
  60. package/dist/hotkey/index.d.ts +1 -0
  61. package/dist/hotkey/index.js +6 -4
  62. package/dist/hover-card/index.js +1 -1
  63. package/dist/image-cropper/index.js +4 -2
  64. package/dist/image-viewer/VideoResource.d.ts +21 -0
  65. package/dist/image-viewer/VideoResource.js +194 -0
  66. package/dist/image-viewer/index.d.ts +14 -0
  67. package/dist/image-viewer/index.js +118 -65
  68. package/dist/image-viewer/lucideGeometry.d.ts +3 -0
  69. package/dist/image-viewer/lucideGeometry.js +19 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +2 -0
  72. package/dist/input-number/index.d.ts +6 -0
  73. package/dist/input-number/index.js +131 -15
  74. package/dist/input-number/model.d.ts +13 -0
  75. package/dist/input-number/model.js +50 -0
  76. package/dist/internal/cache.d.ts +4 -0
  77. package/dist/internal/cache.js +18 -1
  78. package/dist/internal/iconGeometry.d.ts +2 -0
  79. package/dist/internal/iconGeometry.js +32 -0
  80. package/dist/internal/renderTokens.d.ts +4 -0
  81. package/dist/internal/renderTokens.js +4 -0
  82. package/dist/internal/stroke.d.ts +7 -0
  83. package/dist/internal/stroke.js +63 -7
  84. package/dist/internal/surfaceBudget.d.ts +20 -0
  85. package/dist/internal/surfaceBudget.js +69 -0
  86. package/dist/internal/surfaceRenderer.js +87 -28
  87. package/dist/internal/surfaceVisibility.d.ts +3 -0
  88. package/dist/internal/surfaceVisibility.js +60 -0
  89. package/dist/internal/textRenderer.d.ts +2 -0
  90. package/dist/internal/textRenderer.js +3 -3
  91. package/dist/internal/usePencilSurface.js +17 -1
  92. package/dist/internal/wash.js +4 -3
  93. package/dist/journey-map/terrain.js +10 -5
  94. package/dist/layout/index.js +1 -1
  95. package/dist/log/index.js +29 -22
  96. package/dist/markdown/CodeBlock.d.ts +19 -0
  97. package/dist/markdown/CodeBlock.js +86 -0
  98. package/dist/markdown/Deletion.d.ts +2 -0
  99. package/dist/markdown/Deletion.js +69 -0
  100. package/dist/markdown/icons.d.ts +14 -0
  101. package/dist/markdown/icons.js +16 -0
  102. package/dist/markdown/index.d.ts +17 -0
  103. package/dist/markdown/index.js +175 -0
  104. package/dist/node-graph/index.d.ts +2 -0
  105. package/dist/node-graph/index.js +49 -25
  106. package/dist/number-ticker/index.d.ts +11 -9
  107. package/dist/number-ticker/index.js +14 -11
  108. package/dist/number-ticker/model.d.ts +38 -5
  109. package/dist/number-ticker/model.js +139 -10
  110. package/dist/number-ticker/precision.d.ts +7 -0
  111. package/dist/number-ticker/precision.js +141 -0
  112. package/dist/overflow-list/index.js +5 -2
  113. package/dist/overlay/Overlay.d.ts +1 -1
  114. package/dist/overlay/Overlay.js +5 -3
  115. package/dist/overlay/Popover.d.ts +1 -1
  116. package/dist/overlay/Popover.js +6 -5
  117. package/dist/overlay/themeBridge.d.ts +12 -1
  118. package/dist/overlay/themeBridge.js +41 -3
  119. package/dist/pagination/index.d.ts +10 -1
  120. package/dist/pagination/index.js +6 -4
  121. package/dist/pan-zoom/index.d.ts +2 -0
  122. package/dist/pan-zoom/index.js +7 -6
  123. package/dist/pencil.d.ts +1 -0
  124. package/dist/pencil.js +4 -4
  125. package/dist/radar/index.d.ts +4 -0
  126. package/dist/radar/index.js +41 -21
  127. package/dist/rating/index.d.ts +4 -0
  128. package/dist/rating/index.js +128 -24
  129. package/dist/resizable/index.js +1 -1
  130. package/dist/scroll-area/index.d.ts +1 -1
  131. package/dist/scroll-area/index.js +5 -2
  132. package/dist/select/MultipleSelect.js +5 -4
  133. package/dist/select/SearchableSelect.js +61 -6
  134. package/dist/select/Select.d.ts +1 -0
  135. package/dist/select/Select.js +3 -1
  136. package/dist/select/StepperSelect.d.ts +29 -0
  137. package/dist/select/StepperSelect.js +119 -0
  138. package/dist/select/index.d.ts +2 -0
  139. package/dist/select/index.js +1 -0
  140. package/dist/select/stepperModel.d.ts +7 -0
  141. package/dist/select/stepperModel.js +29 -0
  142. package/dist/selection/Checkbox.js +4 -3
  143. package/dist/selection/RadioGroup.js +29 -13
  144. package/dist/selection/Switch.js +4 -3
  145. package/dist/selection/useNativeChecked.d.ts +2 -1
  146. package/dist/selection/useNativeChecked.js +26 -8
  147. package/dist/slider/index.js +50 -6
  148. package/dist/sortable/index.js +15 -6
  149. package/dist/sparkline/index.js +17 -2
  150. package/dist/sparkline/model.js +11 -4
  151. package/dist/styles/accordion.css +228 -0
  152. package/dist/styles/alert-dialog.css +139 -0
  153. package/dist/styles/audio-player.css +22 -0
  154. package/dist/styles/base.css +638 -0
  155. package/dist/styles/breadcrumb.css +7 -0
  156. package/dist/styles/calendar.css +62 -0
  157. package/dist/styles/carousel.css +173 -0
  158. package/dist/styles/choice-grid.css +226 -0
  159. package/dist/styles/color-picker.css +281 -0
  160. package/dist/styles/color-swatch.css +47 -0
  161. package/dist/styles/command.css +52 -0
  162. package/dist/styles/content.css +79 -0
  163. package/dist/styles/context-menu.css +35 -0
  164. package/dist/styles/data-table.css +31 -0
  165. package/dist/styles/date-picker.css +107 -0
  166. package/dist/styles/day-agenda.css +30 -0
  167. package/dist/styles/display.css +304 -0
  168. package/dist/styles/dockable-panel.css +110 -0
  169. package/dist/styles/drawer.css +133 -0
  170. package/dist/styles/dropdown-menu.css +68 -0
  171. package/dist/styles/editable.css +145 -0
  172. package/dist/styles/empty.css +108 -0
  173. package/dist/styles/flip.css +101 -0
  174. package/dist/styles/form.css +634 -0
  175. package/dist/styles/heatmap.css +256 -0
  176. package/dist/styles/hotkey.css +189 -0
  177. package/dist/styles/hover-card.css +27 -0
  178. package/dist/styles/image-cropper.css +205 -0
  179. package/dist/styles/image-viewer.css +422 -0
  180. package/dist/styles/input-number.css +21 -0
  181. package/dist/styles/journey-map.css +19 -0
  182. package/dist/styles/keypad.css +92 -0
  183. package/dist/styles/layout.css +72 -0
  184. package/dist/styles/leaflet.css +661 -0
  185. package/dist/styles/log.css +203 -0
  186. package/dist/styles/markdown.css +379 -0
  187. package/dist/styles/node-graph.css +142 -0
  188. package/dist/styles/number-ticker.css +83 -0
  189. package/dist/styles/overflow-list.css +51 -0
  190. package/dist/styles/overlay.css +284 -0
  191. package/dist/styles/pagination.css +21 -0
  192. package/dist/styles/pan-zoom.css +136 -0
  193. package/dist/styles/radar.css +1 -0
  194. package/dist/styles/rating.css +121 -0
  195. package/dist/styles/resizable.css +108 -0
  196. package/dist/styles/scroll-area.css +122 -0
  197. package/dist/styles/select.css +557 -0
  198. package/dist/styles/selection.css +264 -0
  199. package/dist/styles/slider.css +145 -0
  200. package/dist/styles/slot.css +109 -0
  201. package/dist/styles/sortable.css +159 -0
  202. package/dist/styles/sparkline.css +24 -0
  203. package/dist/styles/steps.css +27 -0
  204. package/dist/styles/tabs.css +79 -0
  205. package/dist/styles/tag-input.css +213 -0
  206. package/dist/styles/time-picker.css +66 -0
  207. package/dist/styles/timeline.css +24 -0
  208. package/dist/styles/toast.css +160 -0
  209. package/dist/styles/toggle-group.css +107 -0
  210. package/dist/styles/tooltip.css +78 -0
  211. package/dist/styles/transfer.css +148 -0
  212. package/dist/styles/tree-select.css +131 -0
  213. package/dist/styles/tree.css +59 -0
  214. package/dist/styles/typewriter.css +62 -0
  215. package/dist/styles/upload.css +60 -0
  216. package/dist/styles/virtual-list.css +112 -0
  217. package/dist/styles.css +2241 -443
  218. package/dist/tabs/index.d.ts +9 -0
  219. package/dist/tabs/index.js +66 -16
  220. package/dist/tabs/useOverflow.d.ts +20 -0
  221. package/dist/tabs/useOverflow.js +145 -0
  222. package/dist/tag-input/index.d.ts +7 -2
  223. package/dist/tag-input/index.js +70 -26
  224. package/dist/time-picker/index.js +38 -20
  225. package/dist/toast/index.d.ts +19 -11
  226. package/dist/toast/index.js +48 -8
  227. package/dist/toast/manager.d.ts +42 -0
  228. package/dist/toast/manager.js +85 -0
  229. package/dist/toast/useToastNavigation.d.ts +3 -0
  230. package/dist/toast/useToastNavigation.js +75 -0
  231. package/dist/tokens.css +4 -0
  232. package/dist/tooltip/Tooltip.d.ts +1 -1
  233. package/dist/tooltip/Tooltip.js +6 -5
  234. package/dist/transfer/index.d.ts +5 -0
  235. package/dist/transfer/index.js +93 -29
  236. package/dist/transfer/model.d.ts +4 -0
  237. package/dist/transfer/model.js +37 -11
  238. package/dist/tree/index.d.ts +5 -1
  239. package/dist/tree/index.js +5 -5
  240. package/dist/tree-select/index.js +67 -30
  241. package/dist/tree-select/model.d.ts +11 -5
  242. package/dist/tree-select/model.js +111 -83
  243. package/dist/typewriter/index.js +10 -10
  244. package/dist/upload/index.js +30 -16
  245. package/dist/virtual-list/index.js +10 -7
  246. package/package.json +416 -5
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as AvatarPrimitive from '@radix-ui/react-avatar';
3
- import { createContext, useContext, forwardRef, useId } from 'react';
3
+ import { createContext, useContext, forwardRef, useId, useState } from 'react';
4
+ import { Button } from '../Button.js';
5
+ import { PencilIcon } from '../PencilIcon.js';
4
6
  import { PencilSurface } from '../PencilSurface.js';
5
7
  import { PencilText } from '../PencilText.js';
6
8
  import { PencilLine } from '../PencilLine.js';
@@ -29,8 +31,11 @@ const CardFooter = forwardRef(function CardFooter({ className, children, divided
29
31
  return _jsxs("div", { ...props, ref: ref, "data-justify": justify, "data-padding": padding, "data-orientation": orientation, className: classes('inkra-card__footer', className), children: [(divided ?? inherited) && _jsx(PencilLine, { className: "inkra-card__divider" }), children] });
30
32
  });
31
33
  export const Card = { Root: CardRoot, Header: CardHeader, Title: CardTitle, Description: CardDescription, Content: CardContent, Footer: CardFooter };
32
- const AlertRoot = forwardRef(function AlertRoot({ className, variant = 'info', icon, seed, style, children, role = 'status', size = 'md', appearance = 'soft', actions, ...props }, ref) {
34
+ const AlertRoot = forwardRef(function AlertRoot({ className, variant = 'info', icon, seed, style, children, role = 'status', size = 'md', appearance = 'soft', actions, closable = false, open, defaultOpen = true, onOpenChange, closeLabel = 'Dismiss notification', ...props }, ref) {
33
35
  const id = useId();
36
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
37
+ if (!(open ?? internalOpen))
38
+ return null;
34
39
  const tokens = {
35
40
  color: `var(--inkra-${variant}-foreground)`,
36
41
  '--inkra-paper': `var(--inkra-${variant}-surface)`,
@@ -40,7 +45,11 @@ const AlertRoot = forwardRef(function AlertRoot({ className, variant = 'info', i
40
45
  '--inkra-border': `var(--inkra-${variant}-foreground)`,
41
46
  ...style,
42
47
  };
43
- return _jsxs(PencilSurface, { ...props, ref: ref, role: role, className: classes('inkra-alert', className), "data-variant": variant, "data-size": size, "data-appearance": appearance, variant: appearance === 'outline' ? 'outline' : 'action', radius: 8, style: tokens, seed: seed ?? `alert-${id}`, children: [icon && _jsx("span", { className: "inkra-alert__icon", "aria-hidden": "true", children: icon }), _jsxs("div", { className: "inkra-alert__body", children: [children, actions != null && _jsx("div", { className: "inkra-alert__actions", children: actions })] })] });
48
+ return _jsxs(PencilSurface, { ...props, ref: ref, role: role, className: classes('inkra-alert', className), "data-variant": variant, "data-size": size, "data-appearance": appearance, variant: appearance === 'outline' ? 'outline' : 'action', radius: 8, style: tokens, seed: seed ?? `alert-${id}`, children: [icon && _jsx("span", { className: "inkra-alert__icon", "aria-hidden": "true", children: icon }), _jsxs("div", { className: "inkra-alert__body", children: [children, actions != null && _jsx("div", { className: "inkra-alert__actions", children: actions })] }), closable && _jsx(Button, { className: "inkra-alert__close", variant: "ghost", size: "icon", "aria-label": closeLabel, title: closeLabel, onClick: () => {
49
+ if (open === undefined)
50
+ setInternalOpen(false);
51
+ onOpenChange?.(false);
52
+ }, children: _jsx(PencilIcon, { paths: [{ d: 'M18 6 6 18M6 6l12 12' }], sourceSize: 24, size: 20 }) })] });
44
53
  });
45
54
  const AlertTitle = forwardRef(function AlertTitle({ className, children, ...props }, ref) {
46
55
  return _jsx("p", { ...props, ref: ref, className: classes('inkra-alert__title', className), children: copy(children) });
@@ -31,7 +31,7 @@ export interface ContextMenuTriggerProps extends Omit<HTMLAttributes<HTMLElement
31
31
  }
32
32
  export declare const ContextMenuTrigger: import("react").ForwardRefExoticComponent<ContextMenuTriggerProps & import("react").RefAttributes<HTMLElement>>;
33
33
  export type ContextMenuPortalProps = ComponentPropsWithoutRef<typeof DropdownMenu.Portal>;
34
- export declare const ContextMenuPortal: ({ children, ...props }: import("../dropdown-menu/index.js").DropdownMenuPortalProps) => import("react").JSX.Element;
34
+ export declare const ContextMenuPortal: ({ children, ...props }: import("../dropdown-menu/index.js").DropdownMenuPortalProps) => ReactNode;
35
35
  export type ContextMenuContentProps = DropdownMenuContentProps;
36
36
  export declare const ContextMenuContent: import("react").ForwardRefExoticComponent<DropdownMenuContentProps & import("react").RefAttributes<HTMLDivElement>>;
37
37
  export type ContextMenuItemProps = ComponentPropsWithoutRef<typeof DropdownMenu.Item>;
@@ -61,7 +61,7 @@ export declare const ContextMenuSubContent: import("react").ForwardRefExoticComp
61
61
  export declare const ContextMenu: Readonly<{
62
62
  Root: typeof ContextMenuRoot;
63
63
  Trigger: import("react").ForwardRefExoticComponent<ContextMenuTriggerProps & import("react").RefAttributes<HTMLElement>>;
64
- Portal: ({ children, ...props }: import("../dropdown-menu/index.js").DropdownMenuPortalProps) => import("react").JSX.Element;
64
+ Portal: ({ children, ...props }: import("../dropdown-menu/index.js").DropdownMenuPortalProps) => ReactNode;
65
65
  Content: import("react").ForwardRefExoticComponent<DropdownMenuContentProps & import("react").RefAttributes<HTMLDivElement>>;
66
66
  Item: import("react").ForwardRefExoticComponent<import("../dropdown-menu/index.js").DropdownMenuItemProps & import("react").RefAttributes<HTMLDivElement>>;
67
67
  CheckboxItem: import("react").ForwardRefExoticComponent<import("../dropdown-menu/index.js").DropdownMenuCheckboxItemProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -5,7 +5,7 @@ import { DropdownMenu } from '../dropdown-menu/index.js';
5
5
  import { PencilIcon } from '../PencilIcon.js';
6
6
  import { PencilSurface } from '../PencilSurface.js';
7
7
  import { PencilText } from '../PencilText.js';
8
- import { PortalThemeBridge, PortalThemeProvider, usePortalTriggerRef, } from '../overlay/themeBridge.js';
8
+ import { usePortalThemeProps, PortalThemeProvider, usePortalTriggerRef, } from '../overlay/themeBridge.js';
9
9
  const DEFAULT_EXCLUDE = '[data-context-menu-native]';
10
10
  const DEFAULT_LONG_PRESS_DELAY = 600;
11
11
  const DEFAULT_LONG_PRESS_TOLERANCE = 10;
@@ -220,9 +220,10 @@ export const ContextMenuSubTrigger = forwardRef(function ContextMenuSubTrigger({
220
220
  });
221
221
  export const ContextMenuSubContent = forwardRef(function ContextMenuSubContent({ children, className, collisionPadding = 8, seed, sideOffset = 4, ...props }, ref) {
222
222
  const id = useId();
223
- return (_jsx(Primitive.Portal, { children: _jsx(PortalThemeBridge, { children: _jsx(Primitive.SubContent, { ...props, className: classes('inkra-menu', 'inkra-context-menu__sub-content', className), collisionPadding: collisionPadding, ref: ref, sideOffset: sideOffset, children: _jsx(PencilSurface, { className: "inkra-menu__paper", radius: 8, seed: seed ?? `context-submenu-${id}`, variant: "paper", children: children }) }) }) }));
223
+ props = usePortalThemeProps(props);
224
+ return (_jsx(Primitive.Portal, { children: _jsx(Primitive.SubContent, { ...props, className: classes('inkra-menu', 'inkra-context-menu__sub-content', className), collisionPadding: collisionPadding, ref: ref, sideOffset: sideOffset, children: _jsx(PencilSurface, { className: "inkra-menu__paper", radius: 8, seed: seed ?? `context-submenu-${id}`, variant: "paper", children: children }) }) }));
224
225
  });
225
- export const ContextMenu = Object.freeze({
226
+ export const ContextMenu = /* @__PURE__ */ Object.freeze({
226
227
  Root: ContextMenuRoot,
227
228
  Trigger: ContextMenuTrigger,
228
229
  Portal: ContextMenuPortal,
@@ -25,6 +25,12 @@ export interface DataTableProps<TData> extends Omit<HTMLAttributes<HTMLDivElemen
25
25
  defaultSorting?: SortingState;
26
26
  onSortingChange?: OnChangeFn<SortingState>;
27
27
  pagination?: boolean;
28
+ /** Supply only the current server page in data. rowCount is the known total. */
29
+ manualPagination?: boolean;
30
+ rowCount?: number;
31
+ /** Server owns sorting/filtering; callbacks still report the requested state. */
32
+ manualSorting?: boolean;
33
+ manualFiltering?: boolean;
28
34
  paginationState?: PaginationState;
29
35
  defaultPagination?: PaginationState;
30
36
  onPaginationChange?: OnChangeFn<PaginationState>;
@@ -44,4 +50,4 @@ export interface DataTableProps<TData> extends Omit<HTMLAttributes<HTMLDivElemen
44
50
  pageLabel?: (page: number, pageCount: number) => ReactNode;
45
51
  sortStatusLabel?: (columnId: string, direction: false | 'asc' | 'desc') => string;
46
52
  }
47
- export declare function DataTable<TData>({ data, columns, getRowId, caption, density, layout, striped, hoverable, stickyHeader, captionSide, filterable, filterLabel, filterPlaceholder, globalFilter, defaultGlobalFilter, onGlobalFilterChange, sorting, defaultSorting, onSortingChange, pagination, paginationState, defaultPagination, onPaginationChange, selectable, rowSelection, defaultRowSelection, onRowSelectionChange, getRowCanSelect, loading, loadingMessage, emptyMessage, selectAllLabel, selectRowLabel, previousPageLabel, nextPageLabel, paginationLabel, pageLabel, sortStatusLabel, className, ...props }: DataTableProps<TData>): import("react").JSX.Element;
53
+ export declare function DataTable<TData>({ data, columns, getRowId, caption, density, layout, striped, hoverable, stickyHeader, captionSide, filterable, filterLabel, filterPlaceholder, globalFilter, defaultGlobalFilter, onGlobalFilterChange, sorting, defaultSorting, onSortingChange, pagination, manualPagination, rowCount, manualSorting, manualFiltering, paginationState, defaultPagination, onPaginationChange, selectable, rowSelection, defaultRowSelection, onRowSelectionChange, getRowCanSelect, loading, loadingMessage, emptyMessage, selectAllLabel, selectRowLabel, previousPageLabel, nextPageLabel, paginationLabel, pageLabel, sortStatusLabel, className, ...props }: DataTableProps<TData>): import("react").JSX.Element;
@@ -1,12 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { flexRender, functionalUpdate, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from '@tanstack/react-table';
3
- import { useEffect, useState } from 'react';
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { Button } from '../Button.js';
5
5
  import { PencilIcon } from '../PencilIcon.js';
6
+ import { PencilSurface } from '../PencilSurface.js';
7
+ import { PencilText } from '../PencilText.js';
6
8
  import { Input } from '../form/Input.js';
7
9
  import { Table } from '../content/index.js';
8
10
  import { Checkbox } from '../selection/Checkbox.js';
11
+ import { normalizeDataTablePagination } from './model.js';
9
12
  const sortIcon = [{ d: 'M 8 10 L 12 6 L 16 10 M 8 14 L 12 18 L 16 14' }];
13
+ const sortAscendingIcon = [{ d: 'M 7 15 L 12 10 L 17 15' }];
14
+ const sortDescendingIcon = [{ d: 'M 7 9 L 12 14 L 17 9' }];
10
15
  const previousIcon = [{ d: 'M 14 7 L 9 12 L 14 17' }];
11
16
  const nextIcon = [{ d: 'M 10 7 L 15 12 L 10 17' }];
12
17
  function classes(...values) {
@@ -15,27 +20,41 @@ function classes(...values) {
15
20
  function resolve(updater, current) {
16
21
  return functionalUpdate(updater, current);
17
22
  }
23
+ function pencilCopy(value, seed) {
24
+ return typeof value === 'string' || typeof value === 'number'
25
+ ? _jsx(PencilText, { seed: seed, children: String(value) })
26
+ : value;
27
+ }
18
28
  function SelectionCheckbox(props) {
19
29
  return _jsx(Checkbox, { className: "inkra-data-table__selection", checked: props.checked, indeterminate: props.indeterminate, disabled: props.disabled, label: _jsx("span", { className: "inkra-visually-hidden", children: props.label }), onCheckedChange: props.onChange });
20
30
  }
21
- export function DataTable({ data, columns, getRowId, caption, density = 'comfortable', layout = 'auto', striped = false, hoverable = true, stickyHeader = false, captionSide = 'top', filterable = false, filterLabel = 'Filter rows', filterPlaceholder = 'Filter rows', globalFilter, defaultGlobalFilter = '', onGlobalFilterChange, sorting, defaultSorting = [], onSortingChange, pagination = false, paginationState, defaultPagination = { pageIndex: 0, pageSize: 10 }, onPaginationChange, selectable = false, rowSelection, defaultRowSelection = {}, onRowSelectionChange, getRowCanSelect, loading = false, loadingMessage = 'Loading rows', emptyMessage = 'No rows', selectAllLabel = 'Select all rows on this page', selectRowLabel = row => `Select row ${row.id}`, previousPageLabel = 'Previous page', nextPageLabel = 'Next page', paginationLabel = 'Table pagination', pageLabel = (page, pageCount) => `Page ${page} of ${pageCount}`, sortStatusLabel = (columnId, direction) => direction === 'asc' ? `${columnId}: sorted ascending` : direction === 'desc' ? `${columnId}: sorted descending` : `${columnId}: not sorted`, className, ...props }) {
31
+ export function DataTable({ data, columns, getRowId, caption, density = 'comfortable', layout = 'auto', striped = false, hoverable = true, stickyHeader = false, captionSide = 'top', filterable = false, filterLabel = 'Filter rows', filterPlaceholder = 'Filter rows', globalFilter, defaultGlobalFilter = '', onGlobalFilterChange, sorting, defaultSorting = [], onSortingChange, pagination = false, manualPagination = false, rowCount, manualSorting = false, manualFiltering = false, paginationState, defaultPagination = { pageIndex: 0, pageSize: 10 }, onPaginationChange, selectable = false, rowSelection, defaultRowSelection = {}, onRowSelectionChange, getRowCanSelect, loading = false, loadingMessage = 'Loading rows', emptyMessage = 'No rows', selectAllLabel = 'Select all rows on this page', selectRowLabel = row => `Select row ${row.id}`, previousPageLabel = 'Previous page', nextPageLabel = 'Next page', paginationLabel = 'Table pagination', pageLabel = (page, pageCount) => `Page ${page} of ${pageCount}`, sortStatusLabel = (columnId, direction) => direction === 'asc' ? `${columnId}: sorted ascending` : direction === 'desc' ? `${columnId}: sorted descending` : `${columnId}: not sorted`, className, ...props }) {
22
32
  const [internalFilter, setInternalFilter] = useState(defaultGlobalFilter);
23
33
  const [internalSorting, setInternalSorting] = useState(defaultSorting);
24
34
  const [internalPagination, setInternalPagination] = useState(defaultPagination);
25
35
  const [internalSelection, setInternalSelection] = useState(defaultRowSelection);
36
+ const paginationCorrection = useRef('');
26
37
  const resolvedFilter = globalFilter ?? internalFilter;
27
38
  const resolvedSorting = sorting ?? internalSorting;
28
39
  const resolvedPagination = paginationState ?? internalPagination;
29
40
  const resolvedSelection = rowSelection ?? internalSelection;
41
+ const paginationRowCount = manualPagination && pagination && rowCount !== undefined
42
+ ? (Number.isFinite(rowCount) ? Math.max(0, Math.floor(rowCount)) : data.length)
43
+ : data.length;
44
+ const initialPagination = useMemo(() => normalizeDataTablePagination(resolvedPagination, paginationRowCount), [resolvedPagination.pageIndex, resolvedPagination.pageSize, paginationRowCount]);
30
45
  const handleSorting = updater => {
31
46
  if (sorting === undefined)
32
47
  setInternalSorting(current => resolve(updater, current));
33
48
  onSortingChange?.(updater);
34
49
  };
35
50
  const handlePagination = updater => {
51
+ const normalizedUpdater = current => {
52
+ const rowCount = manualPagination ? paginationRowCount : table.getPrePaginationRowModel().rows.length;
53
+ return normalizeDataTablePagination(resolve(updater, normalizeDataTablePagination(current, rowCount)), rowCount);
54
+ };
36
55
  if (paginationState === undefined)
37
- setInternalPagination(current => resolve(updater, current));
38
- onPaginationChange?.(updater);
56
+ setInternalPagination(normalizedUpdater);
57
+ onPaginationChange?.(normalizedUpdater);
39
58
  };
40
59
  const handleSelection = updater => {
41
60
  if (rowSelection === undefined)
@@ -53,39 +72,68 @@ export function DataTable({ data, columns, getRowId, caption, density = 'comfort
53
72
  data,
54
73
  columns,
55
74
  getRowId,
75
+ manualPagination: pagination && manualPagination,
76
+ rowCount: pagination && manualPagination ? paginationRowCount : undefined,
77
+ manualSorting,
78
+ manualFiltering,
56
79
  state: {
57
80
  globalFilter: resolvedFilter,
58
81
  sorting: resolvedSorting,
59
- pagination: resolvedPagination,
82
+ pagination: initialPagination,
60
83
  rowSelection: resolvedSelection,
61
84
  },
62
85
  onGlobalFilterChange: updater => handleFilter(resolve(updater, resolvedFilter)),
63
86
  onSortingChange: handleSorting,
64
87
  onPaginationChange: handlePagination,
65
88
  onRowSelectionChange: handleSelection,
89
+ defaultColumn: {
90
+ header: context => pencilCopy(context.column.id, `data-table-header-${context.header.id}`),
91
+ cell: context => pencilCopy(context.renderValue()?.toString?.() ?? null, `data-table-cell-${context.cell.id}`),
92
+ },
66
93
  enableRowSelection: selectable ? getRowCanSelect ?? true : false,
67
94
  getCoreRowModel: getCoreRowModel(),
68
95
  getFilteredRowModel: getFilteredRowModel(),
69
96
  getSortedRowModel: getSortedRowModel(),
70
97
  getPaginationRowModel: pagination ? getPaginationRowModel() : undefined,
71
98
  });
99
+ const filteredRowCount = pagination && manualPagination ? paginationRowCount : table.getPrePaginationRowModel().rows.length;
100
+ const effectivePagination = useMemo(() => normalizeDataTablePagination(initialPagination, filteredRowCount), [initialPagination, filteredRowCount]);
101
+ if (table.getState().pagination.pageIndex !== effectivePagination.pageIndex
102
+ || table.getState().pagination.pageSize !== effectivePagination.pageSize) {
103
+ table.setOptions(previous => ({
104
+ ...previous,
105
+ state: { ...previous.state, pagination: effectivePagination },
106
+ }));
107
+ }
72
108
  const rows = table.getRowModel().rows;
73
- const filteredRowCount = table.getPrePaginationRowModel().rows.length;
74
- const pageCount = Math.max(1, table.getPageCount());
109
+ const pageCount = filteredRowCount === 0 ? 0 : Math.ceil(filteredRowCount / effectivePagination.pageSize);
75
110
  const visibleColumnCount = Math.max(1, table.getVisibleLeafColumns().length + (selectable ? 1 : 0));
76
111
  const hasSelectableRows = rows.some(row => row.getCanSelect());
77
112
  useEffect(() => {
78
113
  if (!pagination)
79
114
  return;
80
- const lastPageIndex = Math.max(0, Math.ceil(filteredRowCount / resolvedPagination.pageSize) - 1);
81
- if (resolvedPagination.pageIndex <= lastPageIndex)
115
+ const matches = resolvedPagination.pageIndex === effectivePagination.pageIndex
116
+ && resolvedPagination.pageSize === effectivePagination.pageSize;
117
+ if (matches) {
118
+ paginationCorrection.current = '';
119
+ return;
120
+ }
121
+ const signature = `${filteredRowCount}:${resolvedPagination.pageIndex}:${resolvedPagination.pageSize}->${effectivePagination.pageIndex}:${effectivePagination.pageSize}`;
122
+ if (paginationCorrection.current === signature)
82
123
  return;
83
- const next = { ...resolvedPagination, pageIndex: lastPageIndex };
124
+ paginationCorrection.current = signature;
84
125
  if (paginationState === undefined)
85
- setInternalPagination(next);
86
- onPaginationChange?.(next);
87
- }, [filteredRowCount, onPaginationChange, pagination, paginationState, resolvedPagination]);
88
- return _jsxs("div", { ...props, className: classes('inkra-data-table', className), "data-loading": loading || undefined, children: [filterable && _jsx("div", { className: "inkra-data-table__toolbar", children: _jsxs("label", { className: "inkra-data-table__filter", children: [_jsx("span", { children: filterLabel }), _jsx(Input, { type: "search", value: resolvedFilter, placeholder: filterPlaceholder, onChange: event => handleFilter(event.currentTarget.value) })] }) }), _jsx(Table.Container, { className: "inkra-data-table__container", children: _jsxs(Table.Root, { density: density, layout: layout, striped: striped, hoverable: hoverable, stickyHeader: stickyHeader, captionSide: captionSide, children: [caption != null && _jsx(Table.Caption, { children: caption }), _jsx(Table.Header, { children: table.getHeaderGroups().map(headerGroup => _jsxs(Table.Row, { children: [selectable && _jsx(Table.Head, { className: "inkra-data-table__select-cell", children: _jsx(SelectionCheckbox, { checked: pagination ? table.getIsAllPageRowsSelected() : table.getIsAllRowsSelected(), indeterminate: pagination ? table.getIsSomePageRowsSelected() : table.getIsSomeRowsSelected(), label: selectAllLabel, disabled: loading || !hasSelectableRows, onChange: checked => pagination ? table.toggleAllPageRowsSelected(checked) : table.toggleAllRowsSelected(checked) }) }), headerGroup.headers.map(header => _jsx(Table.Head, { colSpan: header.colSpan, "aria-sort": header.column.getIsSorted() === 'asc' ? 'ascending' : header.column.getIsSorted() === 'desc' ? 'descending' : header.column.getCanSort() ? 'none' : undefined, children: header.isPlaceholder ? null : header.column.getCanSort() ? _jsxs("button", { type: "button", className: "inkra-data-table__sort", "data-sorted": header.column.getIsSorted() || undefined, onClick: header.column.getToggleSortingHandler(), children: [_jsx("span", { children: flexRender(header.column.columnDef.header, header.getContext()) }), _jsx(PencilIcon, { "aria-hidden": "true", paths: sortIcon, size: 18, sourceSize: 24, active: Boolean(header.column.getIsSorted()) }), _jsx("span", { className: "inkra-visually-hidden", children: sortStatusLabel(header.column.id, header.column.getIsSorted()) })] }) : flexRender(header.column.columnDef.header, header.getContext()) }, header.id))] }, headerGroup.id)) }), _jsx(Table.Body, { children: loading ? _jsx(Table.Row, { children: _jsx(Table.Cell, { colSpan: visibleColumnCount, children: _jsx("div", { className: "inkra-data-table__state", role: "status", children: loadingMessage }) }) })
89
- : rows.length === 0 ? _jsx(Table.Row, { children: _jsx(Table.Cell, { colSpan: visibleColumnCount, children: _jsx("div", { className: "inkra-data-table__state", children: emptyMessage }) }) })
90
- : rows.map(row => _jsxs(Table.Row, { selected: row.getIsSelected(), children: [selectable && _jsx(Table.Cell, { className: "inkra-data-table__select-cell", children: _jsx(SelectionCheckbox, { checked: row.getIsSelected(), label: selectRowLabel(row), disabled: !row.getCanSelect(), onChange: checked => row.toggleSelected(checked) }) }), row.getVisibleCells().map(cell => _jsx(Table.Cell, { children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))] }, row.id)) })] }) }), pagination && _jsxs("nav", { className: "inkra-data-table__pagination", "aria-label": paginationLabel, children: [_jsx(Button, { size: "icon-xs", variant: "ghost", "aria-label": previousPageLabel, disabled: loading || !table.getCanPreviousPage(), onClick: () => table.previousPage(), children: _jsx(PencilIcon, { paths: previousIcon, size: 18, sourceSize: 24 }) }), _jsx("span", { "aria-live": "polite", children: pageLabel(table.getState().pagination.pageIndex + 1, pageCount) }), _jsx(Button, { size: "icon-xs", variant: "ghost", "aria-label": nextPageLabel, disabled: loading || !table.getCanNextPage(), onClick: () => table.nextPage(), children: _jsx(PencilIcon, { paths: nextIcon, size: 18, sourceSize: 24 }) })] })] });
126
+ setInternalPagination(effectivePagination);
127
+ onPaginationChange?.(effectivePagination);
128
+ }, [effectivePagination.pageIndex, effectivePagination.pageSize, filteredRowCount, onPaginationChange, pagination,
129
+ paginationState, resolvedPagination.pageIndex, resolvedPagination.pageSize]);
130
+ return _jsxs("div", { ...props, className: classes('inkra-data-table', className), "data-loading": loading || undefined, children: [filterable && _jsx("div", { className: "inkra-data-table__toolbar", children: _jsxs("label", { className: "inkra-data-table__filter", children: [_jsx("span", { children: pencilCopy(filterLabel, 'data-table-filter-label') }), _jsx(Input, { type: "search", value: resolvedFilter, placeholder: filterPlaceholder, onChange: event => handleFilter(event.currentTarget.value) })] }) }), _jsx(Table.Container, { className: "inkra-data-table__container", children: _jsxs(Table.Root, { density: density, layout: layout, striped: striped, hoverable: hoverable, stickyHeader: stickyHeader, captionSide: captionSide, children: [caption != null && _jsx(Table.Caption, { children: caption }), _jsx(Table.Header, { children: table.getHeaderGroups().map(headerGroup => _jsxs(Table.Row, { children: [selectable && _jsx(Table.Head, { className: "inkra-data-table__select-cell", children: _jsx(SelectionCheckbox, { checked: pagination ? table.getIsAllPageRowsSelected() : table.getIsAllRowsSelected(), indeterminate: pagination ? table.getIsSomePageRowsSelected() : table.getIsSomeRowsSelected(), label: selectAllLabel, disabled: loading || !hasSelectableRows, onChange: checked => pagination ? table.toggleAllPageRowsSelected(checked) : table.toggleAllRowsSelected(checked) }) }), headerGroup.headers.map(header => _jsx(Table.Head, { colSpan: header.colSpan, "aria-sort": header.column.getIsSorted() === 'asc' ? 'ascending' : header.column.getIsSorted() === 'desc' ? 'descending' : header.column.getCanSort() ? 'none' : undefined, children: header.isPlaceholder ? null : (() => {
131
+ const rendered = flexRender(header.column.columnDef.header, header.getContext());
132
+ const direction = header.column.getIsSorted();
133
+ if (!header.column.getCanSort())
134
+ return pencilCopy(rendered, `data-table-header-${header.id}`);
135
+ return _jsxs("button", { type: "button", className: "inkra-data-table__sort", "data-sorted": direction || undefined, onClick: header.column.getToggleSortingHandler(), children: [_jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-data-table__sort-focus", variant: "outline", radius: 6, seed: `data-table-sort-${header.id}-focus` }), _jsx("span", { className: "inkra-data-table__sort-label", children: pencilCopy(rendered, `data-table-sort-${header.id}`) }), _jsx(PencilIcon, { "aria-hidden": "true", className: "inkra-data-table__sort-icon", "data-direction": direction || 'none', paths: direction === 'asc' ? sortAscendingIcon : direction === 'desc' ? sortDescendingIcon : sortIcon, size: 18, sourceSize: 24, active: Boolean(direction) }), _jsx("span", { className: "inkra-visually-hidden", children: sortStatusLabel(header.column.id, direction) })] });
136
+ })() }, header.id))] }, headerGroup.id)) }), _jsx(Table.Body, { children: loading ? _jsx(Table.Row, { children: _jsx(Table.Cell, { colSpan: visibleColumnCount, children: _jsx("div", { className: "inkra-data-table__state", role: "status", children: pencilCopy(loadingMessage, 'data-table-loading') }) }) })
137
+ : rows.length === 0 ? _jsx(Table.Row, { children: _jsx(Table.Cell, { colSpan: visibleColumnCount, children: _jsx("div", { className: "inkra-data-table__state", children: pencilCopy(emptyMessage, 'data-table-empty') }) }) })
138
+ : rows.map(row => _jsxs(Table.Row, { selected: row.getIsSelected(), children: [selectable && _jsx(Table.Cell, { className: "inkra-data-table__select-cell", children: _jsx(SelectionCheckbox, { checked: row.getIsSelected(), label: selectRowLabel(row), disabled: !row.getCanSelect(), onChange: checked => row.toggleSelected(checked) }) }), row.getVisibleCells().map(cell => _jsx(Table.Cell, { children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))] }, row.id)) })] }) }), pagination && _jsxs("nav", { className: "inkra-data-table__pagination", "aria-label": paginationLabel, children: [_jsx(Button, { size: "icon-xs", variant: "ghost", "aria-label": previousPageLabel, disabled: loading || pageCount === 0 || effectivePagination.pageIndex <= 0, onClick: () => table.previousPage(), children: _jsx(PencilIcon, { className: "inkra-data-table__page-arrow", paths: previousIcon, size: 18, sourceSize: 24 }) }), _jsx("span", { "aria-live": "polite", "aria-atomic": "true", children: pencilCopy(pageLabel(pageCount === 0 ? 0 : effectivePagination.pageIndex + 1, pageCount), 'data-table-page-label') }), _jsx(Button, { size: "icon-xs", variant: "ghost", "aria-label": nextPageLabel, disabled: loading || pageCount === 0 || effectivePagination.pageIndex >= pageCount - 1, onClick: () => table.nextPage(), children: _jsx(PencilIcon, { className: "inkra-data-table__page-arrow", paths: nextIcon, size: 18, sourceSize: 24 }) })] })] });
91
139
  }
@@ -0,0 +1,2 @@
1
+ import type { PaginationState } from '@tanstack/react-table';
2
+ export declare function normalizeDataTablePagination(state: PaginationState, rowCount: number): PaginationState;
@@ -0,0 +1,7 @@
1
+ export function normalizeDataTablePagination(state, rowCount) {
2
+ const pageSize = Number.isFinite(state.pageSize) ? Math.max(1, Math.floor(state.pageSize)) : 10;
3
+ const count = Number.isFinite(rowCount) ? Math.max(0, Math.floor(rowCount)) : 0;
4
+ const pageCount = count === 0 ? 0 : Math.ceil(count / pageSize);
5
+ const requestedIndex = Number.isFinite(state.pageIndex) ? Math.max(0, Math.floor(state.pageIndex)) : 0;
6
+ return { pageIndex: pageCount === 0 ? 0 : Math.min(requestedIndex, pageCount - 1), pageSize };
7
+ }
@@ -1,6 +1,7 @@
1
1
  import type { AriaAttributes, CSSProperties, FocusEventHandler } from 'react';
2
2
  import type { DateRange } from 'react-day-picker';
3
3
  import type { CalendarProps } from '../calendar/index.js';
4
+ import type { FieldControlSize } from '../form/types.js';
4
5
  export type DatePickerMode = 'single' | 'range';
5
6
  export type DatePickerRange = DateRange;
6
7
  export type DatePickerValue = Date | DatePickerRange | undefined;
@@ -23,6 +24,7 @@ export interface DatePickerProps {
23
24
  onBlur?: FocusEventHandler<HTMLButtonElement>;
24
25
  onFocus?: FocusEventHandler<HTMLButtonElement>;
25
26
  required?: boolean;
27
+ size?: FieldControlSize;
26
28
  style?: CSSProperties;
27
29
  mode?: DatePickerMode;
28
30
  value?: DatePickerValue;
@@ -1,6 +1,6 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { forwardRef, useEffect, useId, useImperativeHandle, useMemo, useRef, useState, } from 'react';
3
- import { Button } from '../Button.js';
3
+ import { Button, ButtonGroup } from '../Button.js';
4
4
  import { Calendar } from '../calendar/index.js';
5
5
  import { mergeAriaIds, useFieldContext } from '../form/context.js';
6
6
  import { useResolvedControlProps } from '../form/controlProps.js';
@@ -52,7 +52,7 @@ function defaultCopy(locale) {
52
52
  export const DatePicker = forwardRef(function DatePicker(allProps, forwardedRef) {
53
53
  const valueControlled = Object.prototype.hasOwnProperty.call(allProps, 'value');
54
54
  const openControlled = Object.prototype.hasOwnProperty.call(allProps, 'open');
55
- const { 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, 'aria-label': ariaLabel, autoFocus, calendarLabel, calendarProps = {}, className, clearable = true, clearButtonLabel, defaultOpen = false, defaultValue, dir, disabled = false, endName, form, fullWidth = true, id, invalid = false, locale = 'en-US', mode = 'single', name, onBlur, onFocus, onOpenChange, onValueChange, open, openButtonLabel, placeholder, rangeSeparator = ' – ', readOnly = false, required = false, style, value, } = allProps;
55
+ const { 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, 'aria-label': ariaLabel, autoFocus, calendarLabel, calendarProps = {}, className, clearable = true, clearButtonLabel, defaultOpen = false, defaultValue, dir, disabled = false, endName, form, fullWidth = true, id, invalid = false, locale = 'en-US', mode = 'single', name, onBlur, onFocus, onOpenChange, onValueChange, open, openButtonLabel, placeholder, rangeSeparator = ' – ', readOnly = false, required = false, size = 'md', style, value, } = allProps;
56
56
  const generatedId = useId();
57
57
  const field = useFieldContext();
58
58
  const triggerRef = useRef(null);
@@ -85,6 +85,7 @@ export const DatePicker = forwardRef(function DatePicker(allProps, forwardedRef)
85
85
  const captionFormatter = useMemo(() => new Intl.DateTimeFormat(locale, {
86
86
  month: 'long', year: 'numeric',
87
87
  }), [locale]);
88
+ const dayFormatter = useMemo(() => new Intl.NumberFormat(locale), [locale]);
88
89
  const weekdayFormatter = useMemo(() => new Intl.DateTimeFormat(locale, { weekday: 'short' }), [locale]);
89
90
  const displayValue = singleValue
90
91
  ? formatter.format(singleValue)
@@ -128,7 +129,7 @@ export const DatePicker = forwardRef(function DatePicker(allProps, forwardedRef)
128
129
  const { disabled: disabledDays, formatters, labels, defaultMonth, ...restCalendarProps } = calendarProps;
129
130
  const localizedFormatters = {
130
131
  formatCaption: date => captionFormatter.format(date),
131
- formatDay: date => new Intl.NumberFormat(locale).format(date.getDate()),
132
+ formatDay: date => dayFormatter.format(date.getDate()),
132
133
  formatWeekdayName: date => weekdayFormatter.format(date),
133
134
  ...formatters,
134
135
  };
@@ -150,11 +151,11 @@ export const DatePicker = forwardRef(function DatePicker(allProps, forwardedRef)
150
151
  formatters: localizedFormatters,
151
152
  labels: localizedLabels,
152
153
  };
153
- return (_jsxs(Popover.Root, { open: currentOpen, onOpenChange: updateOpen, children: [_jsxs("span", { className: classes('inkra-date-picker', className), "data-disabled": state.disabled || undefined, "data-full-width": fullWidth || undefined, "data-has-value": hasValue || undefined, "data-invalid": state.invalid || constraintInvalid || undefined, "data-mode": mode, "data-readonly": state.readOnly || undefined, dir: dir, style: style, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsx(Button, { "aria-describedby": mergeAriaIds(state.describedBy, accessibleValueId), "aria-errormessage": state.errorMessage, "aria-invalid": constraintInvalid || state.ariaInvalid, "aria-label": ariaLabel ?? (openButtonLabel ? `${openButtonLabel}: ${displayValue}` : undefined), "aria-required": state.required || undefined, autoFocus: autoFocus, className: "inkra-date-picker__trigger", disabled: state.disabled, form: form, fullWidth: fullWidth, id: triggerId, onBlur: onBlur, onFocus: onFocus, ref: triggerRef, startIcon: _jsx(PencilIcon, { paths: calendarPaths, sourceSize: 24, size: 22 }), variant: "outline", children: _jsx("span", { className: "inkra-date-picker__value", "data-placeholder": !hasValue || undefined, children: displayValue }) }) }), accessibleValueId ? _jsx("span", { className: "inkra-date-picker__accessible-value", id: accessibleValueId, children: displayValue }) : null, clearable ? (_jsx(Button, { "aria-hidden": !hasValue || undefined, "aria-label": clearButtonLabel ?? copy.clear, className: "inkra-date-picker__clear", disabled: !hasValue || state.disabled || state.readOnly, onClick: () => {
154
- commit(undefined, 'clear');
155
- updateOpen(false);
156
- triggerRef.current?.focus({ preventScroll: true });
157
- }, size: "icon", tabIndex: hasValue ? 0 : -1, variant: "ghost", children: _jsx(PencilIcon, { paths: clearPaths, sourceSize: 24, size: 22 }) })) : null, name || state.required ? (mode === 'range' ? _jsxs(_Fragment, { children: [_jsx("input", { "aria-hidden": "true", className: "inkra-date-picker__form-field", disabled: state.disabled, form: form, name: name, onChange: () => undefined, onInvalid: event => { event.preventDefault(); setConstraintInvalid(true); triggerRef.current?.focus(); }, readOnly: state.readOnly, required: state.required, tabIndex: -1, type: "text", value: datePickerLocalValue(rangeValue?.from) }), _jsx("input", { "aria-hidden": "true", className: "inkra-date-picker__form-field", disabled: state.disabled, form: form, name: endName ?? (name ? `${name}End` : undefined), onChange: () => undefined, onInvalid: event => { event.preventDefault(); setConstraintInvalid(true); triggerRef.current?.focus(); }, readOnly: state.readOnly, required: state.required, tabIndex: -1, type: "text", value: datePickerLocalValue(rangeValue?.to) })] }) : _jsx("input", { "aria-hidden": "true", className: "inkra-date-picker__form-field", disabled: state.disabled, form: form, name: name, onChange: () => undefined, onInvalid: event => { event.preventDefault(); setConstraintInvalid(true); triggerRef.current?.focus(); }, readOnly: state.readOnly, required: state.required, tabIndex: -1, type: "text", value: datePickerLocalValue(singleValue) })) : null] }), _jsx(Popover.Portal, { children: _jsxs(Popover.Content, { align: "start", "aria-label": calendarLabel ?? copy.calendar, className: "inkra-date-picker__popover", collisionPadding: 12, onOpenAutoFocus: event => {
154
+ return (_jsxs(Popover.Root, { open: currentOpen, onOpenChange: updateOpen, children: [_jsxs("span", { className: classes('inkra-date-picker', className), "data-disabled": state.disabled || undefined, "data-full-width": fullWidth || undefined, "data-has-value": hasValue || undefined, "data-invalid": state.invalid || constraintInvalid || undefined, "data-mode": mode, "data-readonly": state.readOnly || undefined, "data-size": size, dir: dir, style: style, children: [_jsxs(ButtonGroup, { "aria-label": copy.calendar, className: "inkra-date-picker__control", "data-size": size, fullWidth: fullWidth, size: size, variant: "outline", children: [_jsx(Popover.Trigger, { asChild: true, children: _jsx(Button, { "aria-describedby": mergeAriaIds(state.describedBy, accessibleValueId), "aria-errormessage": state.errorMessage, "aria-invalid": constraintInvalid || state.ariaInvalid, "aria-label": ariaLabel ?? (openButtonLabel ? `${openButtonLabel}: ${displayValue}` : undefined), "aria-required": state.required || undefined, autoFocus: autoFocus, className: "inkra-date-picker__trigger", disabled: state.disabled, form: form, fullWidth: fullWidth, id: triggerId, onBlur: onBlur, onFocus: onFocus, ref: triggerRef, startIcon: _jsx(PencilIcon, { paths: calendarPaths, sourceSize: 24, size: 22 }), children: _jsx("span", { className: "inkra-date-picker__value", "data-placeholder": !hasValue || undefined, children: displayValue }) }) }), clearable ? (_jsx(Button, { "aria-hidden": !hasValue || undefined, "aria-label": clearButtonLabel ?? copy.clear, className: "inkra-date-picker__clear", disabled: !hasValue || state.disabled || state.readOnly, onPointerDown: event => event.preventDefault(), onClick: () => {
155
+ commit(undefined, 'clear');
156
+ updateOpen(false);
157
+ triggerRef.current?.focus({ preventScroll: true });
158
+ }, size: size === 'sm' ? 'icon-sm' : size === 'lg' ? 'icon-lg' : 'icon', tabIndex: hasValue ? 0 : -1, variant: "ghost", children: _jsx(PencilIcon, { paths: clearPaths, sourceSize: 24, size: 22 }) })) : null] }), accessibleValueId ? _jsx("span", { className: "inkra-date-picker__accessible-value", id: accessibleValueId, children: displayValue }) : null, name || state.required ? (mode === 'range' ? _jsxs(_Fragment, { children: [_jsx("input", { "aria-hidden": "true", className: "inkra-date-picker__form-field", disabled: state.disabled, form: form, name: name, onChange: () => undefined, onInvalid: event => { event.preventDefault(); setConstraintInvalid(true); triggerRef.current?.focus(); }, readOnly: state.readOnly, required: state.required, tabIndex: -1, type: "text", value: datePickerLocalValue(rangeValue?.from) }), _jsx("input", { "aria-hidden": "true", className: "inkra-date-picker__form-field", disabled: state.disabled, form: form, name: endName ?? (name ? `${name}End` : undefined), onChange: () => undefined, onInvalid: event => { event.preventDefault(); setConstraintInvalid(true); triggerRef.current?.focus(); }, readOnly: state.readOnly, required: state.required, tabIndex: -1, type: "text", value: datePickerLocalValue(rangeValue?.to) })] }) : _jsx("input", { "aria-hidden": "true", className: "inkra-date-picker__form-field", disabled: state.disabled, form: form, name: name, onChange: () => undefined, onInvalid: event => { event.preventDefault(); setConstraintInvalid(true); triggerRef.current?.focus(); }, readOnly: state.readOnly, required: state.required, tabIndex: -1, type: "text", value: datePickerLocalValue(singleValue) })) : null] }), _jsx(Popover.Portal, { children: _jsxs(Popover.Content, { align: "start", "aria-label": calendarLabel ?? copy.calendar, className: "inkra-date-picker__popover", collisionPadding: 12, onOpenAutoFocus: event => {
158
159
  event.preventDefault();
159
160
  requestAnimationFrame(() => {
160
161
  requestAnimationFrame(() => {
@@ -13,6 +13,7 @@ export interface AgendaEvent {
13
13
  }
14
14
  export interface DayAgendaProps extends Omit<HTMLAttributes<HTMLElement>, 'onSelect'> {
15
15
  date: string;
16
+ /** Immutable snapshots: replace the events array and changed event records when editing. */
16
17
  events: readonly AgendaEvent[];
17
18
  selectedEvent?: string;
18
19
  onEventSelect?: (event: AgendaEvent) => void;
@@ -32,6 +33,10 @@ export interface DayAgendaProps extends Omit<HTMLAttributes<HTMLElement>, 'onSel
32
33
  previous: string;
33
34
  next: string;
34
35
  };
36
+ /** Opt-in initial summary. Layout uses every event and all can be revealed. */
37
+ maxVisibleEvents?: number;
38
+ showAllEventsLabel?: (count: number) => string;
39
+ showFewerEventsLabel?: string;
35
40
  }
36
41
  export declare function agendaTime(minute: number, hourCycle?: 'h12' | 'h23', locale?: string): string;
37
42
  export declare function agendaInterval(event: AgendaEvent): {
@@ -40,4 +45,4 @@ export declare function agendaInterval(event: AgendaEvent): {
40
45
  before: boolean;
41
46
  after: boolean;
42
47
  } | null;
43
- export declare function DayAgenda({ date, events, selectedEvent, onEventSelect, locale, timeZoneLabel, emptyLabel, untimedLabel, showOverview, hourCycle, density, showDescriptions, divided, disabled, onPreviousDay, onNextDay, navigationLabels, className, ...props }: DayAgendaProps): import("react").JSX.Element;
48
+ export declare function DayAgenda({ date, events, selectedEvent, onEventSelect, locale, timeZoneLabel, emptyLabel, untimedLabel, showOverview, hourCycle, density, showDescriptions, divided, disabled, onPreviousDay, onNextDay, navigationLabels, maxVisibleEvents, showAllEventsLabel, showFewerEventsLabel, className, ...props }: DayAgendaProps): import("react").JSX.Element;
@@ -1,19 +1,24 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useId } from 'react';
2
+ import { useId, useMemo, useState } from 'react';
3
3
  import { TravelIcon } from '../internal/travelIcons.js';
4
4
  import { Button } from '../Button.js';
5
5
  import { createAgendaScale, layoutAgendaCards } from './layout.js';
6
6
  import { PencilSurface } from '../PencilSurface.js';
7
7
  import { PencilLine } from '../PencilLine.js';
8
8
  import { PencilText } from '../PencilText.js';
9
- export function agendaTime(minute, hourCycle = 'h23', locale = 'en') {
9
+ function formatAgendaMinute(minute, formatter) {
10
10
  const value = Math.max(0, Math.min(1440, Math.floor(minute)));
11
- if (hourCycle === 'h12') {
11
+ if (formatter) {
12
12
  const time = new Date(Date.UTC(2000, 0, 1, 0, value));
13
- return new Intl.DateTimeFormat(locale, { timeZone: 'UTC', hour: 'numeric', minute: '2-digit', hourCycle }).format(time) + (value === 1440 ? ' +1' : '');
13
+ return formatter.format(time) + (value === 1440 ? ' +1' : '');
14
14
  }
15
15
  return value === 1440 ? '00:00 +1' : `${String(Math.floor(value / 60)).padStart(2, '0')}:${String(value % 60).padStart(2, '0')}`;
16
16
  }
17
+ export function agendaTime(minute, hourCycle = 'h23', locale = 'en') {
18
+ return formatAgendaMinute(minute, hourCycle === 'h12'
19
+ ? new Intl.DateTimeFormat(locale, { timeZone: 'UTC', hour: 'numeric', minute: '2-digit', hourCycle })
20
+ : undefined);
21
+ }
17
22
  export function agendaInterval(event) {
18
23
  if (event.startMinute === undefined || !Number.isFinite(event.startMinute))
19
24
  return null;
@@ -24,24 +29,44 @@ export function agendaInterval(event) {
24
29
  return null;
25
30
  return { start: Math.max(0, event.startMinute), end: Math.min(1440, end ?? event.startMinute), before: event.startMinute < 0, after: (end ?? 0) > 1440 };
26
31
  }
27
- export function DayAgenda({ date, events, selectedEvent, onEventSelect, locale = 'en', timeZoneLabel, emptyLabel = 'No events', untimedLabel = 'Time pending', showOverview = true, hourCycle = 'h23', density = 'comfortable', showDescriptions = true, divided = true, disabled = false, onPreviousDay, onNextDay, navigationLabels = { previous: 'Previous day', next: 'Next day' }, className, ...props }) {
32
+ export function DayAgenda({ date, events, selectedEvent, onEventSelect, locale = 'en', timeZoneLabel, emptyLabel = 'No events', untimedLabel = 'Time pending', showOverview = true, hourCycle = 'h23', density = 'comfortable', showDescriptions = true, divided = true, disabled = false, onPreviousDay, onNextDay, navigationLabels = { previous: 'Previous day', next: 'Next day' }, maxVisibleEvents, showAllEventsLabel = count => `Show all ${count} events`, showFewerEventsLabel = 'Show fewer events', className, ...props }) {
28
33
  const id = useId();
29
- const validDate = /^\d{4}-\d{2}-\d{2}$/.test(date) ? new Date(`${date}T12:00:00Z`) : new Date(NaN);
30
- const title = Number.isFinite(validDate.getTime()) ? new Intl.DateTimeFormat(locale, { timeZone: 'UTC', month: 'long', day: 'numeric', weekday: 'short' }).format(validDate) : date;
31
- const ordered = [...events].sort((a, b) => (agendaInterval(a)?.start ?? Infinity) - (agendaInterval(b)?.start ?? Infinity));
32
- const formatTime = (minute) => agendaTime(minute, hourCycle, locale);
33
- const timed = ordered.flatMap(event => {
34
- const interval = agendaInterval(event);
35
- return interval ? [{ ...interval, id: event.id, event }] : [];
36
- });
37
- const intervals = timed.filter(row => row.end > row.start);
38
- const points = timed.filter(row => row.end === row.start);
39
- const scale = createAgendaScale(intervals, points.map(row => row.start), density === 'compact');
40
- const cards = layoutAgendaCards(timed, scale).sort((a, b) => a.start - b.start);
41
- const ticks = Array.from({ length: 25 }, (_, hour) => hour * 60).filter((minute, index, list) => index === 0 || index === 24 || scale.position(minute) - scale.position(list[index - 1]) >= 25);
34
+ const [expandedDate, setExpandedDate] = useState();
35
+ const displayLimit = maxVisibleEvents !== undefined && Number.isFinite(maxVisibleEvents) ? Math.max(0, Math.floor(maxVisibleEvents)) : Infinity;
36
+ const expanded = expandedDate === date;
37
+ const title = useMemo(() => {
38
+ const validDate = /^\d{4}-\d{2}-\d{2}$/.test(date) ? new Date(`${date}T12:00:00Z`) : new Date(NaN);
39
+ return Number.isFinite(validDate.getTime()) ? new Intl.DateTimeFormat(locale, { timeZone: 'UTC', month: 'long', day: 'numeric', weekday: 'short' }).format(validDate) : date;
40
+ }, [date, locale]);
41
+ const timeFormatter = useMemo(() => hourCycle === 'h12'
42
+ ? new Intl.DateTimeFormat(locale, { timeZone: 'UTC', hour: 'numeric', minute: '2-digit', hourCycle })
43
+ : undefined, [hourCycle, locale]);
44
+ const { ordered, pending, scale, cards, ticks } = useMemo(() => {
45
+ const records = events.map(event => ({ event, interval: agendaInterval(event) }))
46
+ .sort((a, b) => (a.interval?.start ?? Infinity) - (b.interval?.start ?? Infinity));
47
+ const ordered = records.map(row => row.event);
48
+ const pending = records.filter(row => !row.interval).map(row => row.event);
49
+ const timed = records.flatMap(({ event, interval }) => interval ? [{ ...interval, id: event.id, event }] : []);
50
+ const intervals = timed.filter(row => row.end > row.start);
51
+ const points = timed.filter(row => row.end === row.start);
52
+ const scale = createAgendaScale(intervals, points.map(row => row.start), density === 'compact');
53
+ const formatTime = (minute) => formatAgendaMinute(minute, timeFormatter);
54
+ const cards = layoutAgendaCards(timed, scale).sort((a, b) => a.start - b.start).map(row => ({
55
+ ...row,
56
+ time: `${row.before ? '\u2190 ' : ''}${formatTime(row.start)}${row.event.endMinute !== undefined ? ` - ${formatTime(row.end)}` : ''}${row.after ? ' \u2192' : ''}`,
57
+ }));
58
+ const ticks = Array.from({ length: 25 }, (_, hour) => hour * 60)
59
+ .filter((minute, index, list) => index === 0 || index === 24 || scale.position(minute) - scale.position(list[index - 1]) >= 25)
60
+ .map(minute => ({ minute, label: formatTime(minute) }));
61
+ return { ordered, pending, scale, cards, ticks };
62
+ }, [density, events, timeFormatter]);
63
+ const shownIds = useMemo(() => expanded || displayLimit >= ordered.length ? null
64
+ : new Set([...ordered.slice(0, displayLimit).map(event => event.id), ...(selectedEvent ? [selectedEvent] : [])]), [expanded, displayLimit, ordered, selectedEvent]);
65
+ const visibleCards = shownIds === null ? cards : cards.filter(row => shownIds.has(row.id));
66
+ const visiblePending = shownIds === null ? pending : pending.filter(event => shownIds.has(event.id));
42
67
  const renderCard = (event, time) => _jsxs(PencilSurface, { variant: "quiet", radius: 6, className: "inkra-agenda__card", "data-selected": selectedEvent === event.id, style: { '--inkra-accent-soft': event.color ?? (event.kind === 'stay' ? '#c8c38f' : event.kind === 'travel' ? '#87bbc0' : '#a7c7ad') }, children: [onEventSelect && _jsx("button", { type: "button", className: "inkra-agenda__hit", disabled: disabled || event.disabled, "aria-label": `${time}, ${event.title}`, "aria-pressed": selectedEvent === event.id, onClick: () => onEventSelect(event) }), _jsxs("div", { className: "inkra-agenda__card-copy", children: [_jsxs("span", { className: "inkra-agenda__card-title", title: event.title, children: [event.icon && _jsx("span", { "aria-hidden": "true", children: event.icon }), _jsx(PencilText, { ellipsis: true, children: event.title })] }), showDescriptions && event.description && _jsx("span", { className: "inkra-agenda__description", children: event.description })] })] });
43
- return _jsxs("section", { ...props, "data-density": density, "data-hour-cycle": hourCycle, className: ['inkra-agenda', className].filter(Boolean).join(' '), "aria-labelledby": props['aria-labelledby'] ?? id, children: [_jsxs("header", { className: "inkra-agenda__heading", children: [_jsx(PencilText, { as: "h3", id: id, children: title }), timeZoneLabel && _jsx(PencilText, { children: timeZoneLabel })] }), _jsx(PencilLine, {}), ordered.length === 0 ? _jsx(PencilText, { as: "p", children: emptyLabel }) : _jsxs(_Fragment, { children: [_jsx("div", { className: "inkra-agenda__timeline", "data-overview": showOverview, children: _jsxs("div", { className: "inkra-agenda__scale", style: { height: scale.height + 24 }, children: [ticks.map(minute => _jsxs("div", { className: "inkra-agenda__tick", style: { top: scale.position(minute) + 12 }, children: [_jsx("span", { children: _jsx(PencilText, { children: formatTime(minute) }) }), divided && _jsx(PencilLine, {})] }, minute)), _jsx("ol", { className: "inkra-agenda__lanes", children: cards.map(row => {
44
- const time = `${row.before ? '\u2190 ' : ''}${formatTime(row.start)}${row.event.endMinute !== undefined ? ` - ${formatTime(row.end)}` : ''}${row.after ? ' \u2192' : ''}`;
68
+ return _jsxs("section", { ...props, "data-density": density, "data-hour-cycle": hourCycle, className: ['inkra-agenda', className].filter(Boolean).join(' '), "aria-labelledby": props['aria-labelledby'] ?? id, children: [_jsxs("header", { className: "inkra-agenda__heading", children: [_jsx(PencilText, { as: "h3", id: id, children: title }), timeZoneLabel && _jsx(PencilText, { children: timeZoneLabel })] }), _jsx(PencilLine, {}), ordered.length === 0 ? _jsx(PencilText, { as: "p", children: emptyLabel }) : _jsxs(_Fragment, { children: [_jsx("div", { className: "inkra-agenda__timeline", "data-overview": showOverview, children: _jsxs("div", { className: "inkra-agenda__scale", style: { height: scale.height + 24 }, children: [ticks.map(({ minute, label }) => _jsxs("div", { className: "inkra-agenda__tick", style: { top: scale.position(minute) + 12 }, children: [_jsx("span", { children: _jsx(PencilText, { children: label }) }), divided && _jsx(PencilLine, {})] }, minute)), _jsx("ol", { className: "inkra-agenda__lanes", children: visibleCards.map(row => {
69
+ const time = row.time;
45
70
  return _jsxs("li", { "data-kind": row.event.kind, style: { top: row.top + 14, height: Math.max(44, row.height - 4), insetInlineStart: `${row.lane / row.lanes * 100}%`, width: `${100 / row.lanes}%` }, children: [renderCard(row.event, time), _jsx("span", { className: "inkra-agenda__accessible-time", children: time })] }, row.id);
46
- }) })] }) }), ordered.some(event => !agendaInterval(event)) && _jsxs("div", { className: "inkra-agenda__pending", children: [_jsx(PencilText, { children: untimedLabel }), ordered.filter(event => !agendaInterval(event)).map(event => _jsx("div", { children: renderCard(event, untimedLabel) }, event.id))] })] }), (onPreviousDay || onNextDay) && _jsxs("footer", { className: "inkra-agenda__navigation", children: [_jsx(PencilLine, {}), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": navigationLabels.previous, title: navigationLabels.previous, disabled: !onPreviousDay, onClick: onPreviousDay, children: _jsx(TravelIcon, { name: "previous" }) }), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": navigationLabels.next, title: navigationLabels.next, disabled: !onNextDay, onClick: onNextDay, children: _jsx(TravelIcon, { name: "next" }) })] })] });
71
+ }) })] }) }), visiblePending.length > 0 && _jsxs("div", { className: "inkra-agenda__pending", children: [_jsx(PencilText, { children: untimedLabel }), visiblePending.map(event => _jsx("div", { children: renderCard(event, untimedLabel) }, event.id))] }), ordered.length > displayLimit && _jsx(Button, { size: "sm", variant: "ghost", "aria-expanded": expanded, onClick: () => setExpandedDate(expanded ? undefined : date), children: expanded ? showFewerEventsLabel : showAllEventsLabel(ordered.length) })] }), (onPreviousDay || onNextDay) && _jsxs("footer", { className: "inkra-agenda__navigation", children: [_jsx(PencilLine, {}), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": navigationLabels.previous, title: navigationLabels.previous, disabled: !onPreviousDay, onClick: onPreviousDay, children: _jsx(TravelIcon, { name: "previous" }) }), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": navigationLabels.next, title: navigationLabels.next, disabled: !onNextDay, onClick: onNextDay, children: _jsx(TravelIcon, { name: "next" }) })] })] });
47
72
  }
@@ -1,16 +1,56 @@
1
+ function heapPush(heap, value, less) {
2
+ let index = heap.length;
3
+ heap.push(value);
4
+ while (index > 0) {
5
+ const parent = (index - 1) >>> 1;
6
+ if (!less(value, heap[parent]))
7
+ break;
8
+ heap[index] = heap[parent];
9
+ index = parent;
10
+ }
11
+ heap[index] = value;
12
+ }
13
+ function heapPop(heap, less) {
14
+ const first = heap[0];
15
+ const last = heap.pop();
16
+ if (heap.length) {
17
+ let index = 0;
18
+ while (index * 2 + 1 < heap.length) {
19
+ let child = index * 2 + 1;
20
+ if (child + 1 < heap.length && less(heap[child + 1], heap[child]))
21
+ child++;
22
+ if (!less(heap[child], last))
23
+ break;
24
+ heap[index] = heap[child];
25
+ index = child;
26
+ }
27
+ heap[index] = last;
28
+ }
29
+ return first;
30
+ }
31
+ const lowerLane = (left, right) => left < right;
32
+ const earlierEnd = (left, right) => left.end < right.end || (left.end === right.end && left.lane < right.lane);
1
33
  export function layoutAgendaIntervals(intervals) {
2
34
  const sorted = [...intervals].sort((a, b) => a.start - b.start || a.id.localeCompare(b.id));
3
35
  const result = [];
4
- let group = [], groupEnd = -Infinity, laneEnds = [];
5
- const flush = () => { for (const item of group)
6
- result.push({ ...item, lanes: laneEnds.length }); group = []; laneEnds = []; };
36
+ let group = [], groupEnd = -Infinity, laneCount = 0;
37
+ let occupied = [], available = [];
38
+ const flush = () => {
39
+ for (const item of group)
40
+ result.push({ ...item, lanes: laneCount });
41
+ group = [];
42
+ occupied = [];
43
+ available = [];
44
+ laneCount = 0;
45
+ };
7
46
  for (const item of sorted) {
8
47
  if (item.start >= groupEnd)
9
48
  flush();
10
- let lane = laneEnds.findIndex(end => end <= item.start);
11
- if (lane < 0)
12
- lane = laneEnds.length;
13
- laneEnds[lane] = item.end;
49
+ while (occupied.length && occupied[0].end <= item.start) {
50
+ heapPush(available, heapPop(occupied, earlierEnd).lane, lowerLane);
51
+ }
52
+ const lane = available.length ? heapPop(available, lowerLane) : laneCount++;
53
+ heapPush(occupied, { end: item.end, lane }, earlierEnd);
14
54
  group.push({ ...item, lane });
15
55
  groupEnd = Math.max(groupEnd, item.end);
16
56
  }
@@ -23,6 +63,10 @@ export function createAgendaScale(intervals, pointTimes, compact) {
23
63
  const boundaries = [...new Set([0, 1440, ...pointTimes, ...intervals.flatMap(row => [row.start, row.end])])].sort((a, b) => a - b);
24
64
  const before = new Map(), after = new Map();
25
65
  const segments = [];
66
+ // Boundaries already contain every endpoint, so the furthest active end is
67
+ // enough to decide occupancy without rescanning every event for every segment.
68
+ const ordered = [...intervals].sort((a, b) => a.start - b.start);
69
+ let intervalIndex = 0, occupiedUntil = -Infinity;
26
70
  let height = 0;
27
71
  for (let index = 0; index < boundaries.length; index++) {
28
72
  const time = boundaries[index];
@@ -33,7 +77,11 @@ export function createAgendaScale(intervals, pointTimes, compact) {
33
77
  const next = boundaries[index + 1];
34
78
  if (next === undefined)
35
79
  continue;
36
- const occupied = intervals.some(row => row.start < next && row.end > time);
80
+ while (intervalIndex < ordered.length && ordered[intervalIndex].start < next) {
81
+ occupiedUntil = Math.max(occupiedUntil, ordered[intervalIndex].end);
82
+ intervalIndex++;
83
+ }
84
+ const occupied = occupiedUntil > time;
37
85
  const size = Math.max(occupied ? rowHeight : 16, (next - time) / 60 * 32);
38
86
  segments.push({ start: time, end: next, top: height, size });
39
87
  height += size;
@@ -43,7 +91,15 @@ export function createAgendaScale(intervals, pointTimes, compact) {
43
91
  const exact = (edge === 'after' ? after : before).get(time);
44
92
  if (exact !== undefined)
45
93
  return exact;
46
- const segment = segments.find(row => time >= row.start && time <= row.end);
94
+ let low = 0, high = segments.length - 1;
95
+ while (low <= high) {
96
+ const middle = (low + high) >>> 1;
97
+ if (segments[middle].start <= time)
98
+ low = middle + 1;
99
+ else
100
+ high = middle - 1;
101
+ }
102
+ const segment = high >= 0 && time <= segments[high].end ? segments[high] : undefined;
47
103
  return segment ? segment.top + (time - segment.start) / (segment.end - segment.start) * segment.size : 0;
48
104
  };
49
105
  return { position, height, rowHeight };