@inkra/react 0.3.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 (226) hide show
  1. package/README.md +15 -3
  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.js +14 -4
  9. package/dist/audio-player/index.d.ts +2 -0
  10. package/dist/audio-player/index.js +126 -31
  11. package/dist/calendar/index.d.ts +4 -1
  12. package/dist/calendar/index.js +31 -12
  13. package/dist/carousel/index.js +13 -6
  14. package/dist/choice-grid/index.d.ts +6 -1
  15. package/dist/choice-grid/index.js +61 -33
  16. package/dist/color-picker/GradientEditor.d.ts +39 -0
  17. package/dist/color-picker/GradientEditor.js +63 -0
  18. package/dist/color-picker/gradient.d.ts +25 -0
  19. package/dist/color-picker/gradient.js +60 -0
  20. package/dist/color-picker/index.d.ts +27 -7
  21. package/dist/color-picker/index.js +134 -32
  22. package/dist/color-swatch/index.d.ts +10 -0
  23. package/dist/color-swatch/index.js +9 -0
  24. package/dist/command/index.js +27 -4
  25. package/dist/content/index.d.ts +5 -0
  26. package/dist/content/index.js +12 -3
  27. package/dist/context-menu/index.js +1 -1
  28. package/dist/data-table/index.d.ts +7 -1
  29. package/dist/data-table/index.js +64 -16
  30. package/dist/data-table/model.d.ts +2 -0
  31. package/dist/data-table/model.js +7 -0
  32. package/dist/date-picker/index.d.ts +2 -0
  33. package/dist/date-picker/index.js +10 -9
  34. package/dist/day-agenda/index.d.ts +6 -1
  35. package/dist/day-agenda/index.js +46 -21
  36. package/dist/day-agenda/layout.js +65 -9
  37. package/dist/display/ProgressRing.js +7 -3
  38. package/dist/display/index.d.ts +4 -1
  39. package/dist/display/index.js +3 -2
  40. package/dist/dockable-panel/index.js +5 -5
  41. package/dist/drawer/index.js +4 -3
  42. package/dist/editable/index.d.ts +24 -2
  43. package/dist/editable/index.js +142 -37
  44. package/dist/empty/illustrations.d.ts +8 -0
  45. package/dist/empty/illustrations.js +78 -0
  46. package/dist/empty/index.d.ts +6 -1
  47. package/dist/empty/index.js +11 -3
  48. package/dist/flip/index.js +28 -8
  49. package/dist/form/Field.js +11 -10
  50. package/dist/form/Textarea.d.ts +3 -1
  51. package/dist/form/Textarea.js +6 -4
  52. package/dist/hotkey/index.d.ts +1 -0
  53. package/dist/hotkey/index.js +6 -4
  54. package/dist/hover-card/index.js +1 -1
  55. package/dist/image-cropper/index.js +4 -2
  56. package/dist/image-viewer/VideoResource.d.ts +21 -0
  57. package/dist/image-viewer/VideoResource.js +194 -0
  58. package/dist/image-viewer/index.d.ts +14 -0
  59. package/dist/image-viewer/index.js +118 -65
  60. package/dist/image-viewer/lucideGeometry.d.ts +3 -0
  61. package/dist/image-viewer/lucideGeometry.js +19 -0
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.js +2 -0
  64. package/dist/input-number/index.js +9 -4
  65. package/dist/internal/cache.d.ts +4 -0
  66. package/dist/internal/cache.js +18 -1
  67. package/dist/internal/iconGeometry.d.ts +2 -0
  68. package/dist/internal/iconGeometry.js +32 -0
  69. package/dist/internal/renderTokens.d.ts +4 -0
  70. package/dist/internal/renderTokens.js +4 -0
  71. package/dist/internal/stroke.d.ts +7 -0
  72. package/dist/internal/stroke.js +63 -7
  73. package/dist/internal/surfaceBudget.d.ts +20 -0
  74. package/dist/internal/surfaceBudget.js +69 -0
  75. package/dist/internal/surfaceRenderer.js +87 -28
  76. package/dist/internal/surfaceVisibility.d.ts +3 -0
  77. package/dist/internal/surfaceVisibility.js +60 -0
  78. package/dist/internal/textRenderer.d.ts +2 -0
  79. package/dist/internal/textRenderer.js +3 -3
  80. package/dist/internal/usePencilSurface.js +17 -1
  81. package/dist/internal/wash.js +4 -3
  82. package/dist/journey-map/terrain.js +10 -5
  83. package/dist/layout/index.js +1 -1
  84. package/dist/log/index.js +29 -22
  85. package/dist/markdown/CodeBlock.d.ts +19 -0
  86. package/dist/markdown/CodeBlock.js +86 -0
  87. package/dist/markdown/Deletion.d.ts +2 -0
  88. package/dist/markdown/Deletion.js +69 -0
  89. package/dist/markdown/icons.d.ts +14 -0
  90. package/dist/markdown/icons.js +16 -0
  91. package/dist/markdown/index.d.ts +17 -0
  92. package/dist/markdown/index.js +175 -0
  93. package/dist/node-graph/index.d.ts +2 -0
  94. package/dist/node-graph/index.js +49 -25
  95. package/dist/number-ticker/precision.js +9 -2
  96. package/dist/overflow-list/index.js +5 -2
  97. package/dist/overlay/Overlay.js +1 -1
  98. package/dist/overlay/Popover.js +3 -3
  99. package/dist/overlay/themeBridge.js +14 -2
  100. package/dist/pagination/index.d.ts +10 -1
  101. package/dist/pagination/index.js +6 -4
  102. package/dist/pan-zoom/index.d.ts +2 -0
  103. package/dist/pan-zoom/index.js +7 -6
  104. package/dist/pencil.d.ts +1 -0
  105. package/dist/pencil.js +4 -4
  106. package/dist/radar/index.d.ts +2 -0
  107. package/dist/radar/index.js +40 -20
  108. package/dist/rating/index.d.ts +4 -0
  109. package/dist/rating/index.js +128 -24
  110. package/dist/resizable/index.js +1 -1
  111. package/dist/scroll-area/index.d.ts +1 -1
  112. package/dist/scroll-area/index.js +5 -2
  113. package/dist/select/MultipleSelect.js +5 -4
  114. package/dist/select/SearchableSelect.js +61 -6
  115. package/dist/select/Select.d.ts +1 -0
  116. package/dist/select/Select.js +3 -1
  117. package/dist/select/StepperSelect.d.ts +29 -0
  118. package/dist/select/StepperSelect.js +119 -0
  119. package/dist/select/index.d.ts +2 -0
  120. package/dist/select/index.js +1 -0
  121. package/dist/select/stepperModel.d.ts +7 -0
  122. package/dist/select/stepperModel.js +29 -0
  123. package/dist/selection/Checkbox.js +4 -3
  124. package/dist/selection/RadioGroup.js +29 -13
  125. package/dist/selection/Switch.js +4 -3
  126. package/dist/selection/useNativeChecked.d.ts +2 -1
  127. package/dist/selection/useNativeChecked.js +26 -8
  128. package/dist/slider/index.js +50 -6
  129. package/dist/sortable/index.js +6 -2
  130. package/dist/sparkline/index.js +17 -2
  131. package/dist/sparkline/model.js +11 -4
  132. package/dist/styles/accordion.css +228 -0
  133. package/dist/styles/alert-dialog.css +139 -0
  134. package/dist/styles/audio-player.css +22 -0
  135. package/dist/styles/base.css +638 -0
  136. package/dist/styles/breadcrumb.css +7 -0
  137. package/dist/styles/calendar.css +62 -0
  138. package/dist/styles/carousel.css +173 -0
  139. package/dist/styles/choice-grid.css +226 -0
  140. package/dist/styles/color-picker.css +281 -0
  141. package/dist/styles/color-swatch.css +47 -0
  142. package/dist/styles/command.css +52 -0
  143. package/dist/styles/content.css +79 -0
  144. package/dist/styles/context-menu.css +35 -0
  145. package/dist/styles/data-table.css +31 -0
  146. package/dist/styles/date-picker.css +107 -0
  147. package/dist/styles/day-agenda.css +30 -0
  148. package/dist/styles/display.css +304 -0
  149. package/dist/styles/dockable-panel.css +110 -0
  150. package/dist/styles/drawer.css +133 -0
  151. package/dist/styles/dropdown-menu.css +68 -0
  152. package/dist/styles/editable.css +145 -0
  153. package/dist/styles/empty.css +108 -0
  154. package/dist/styles/flip.css +101 -0
  155. package/dist/styles/form.css +634 -0
  156. package/dist/styles/heatmap.css +256 -0
  157. package/dist/styles/hotkey.css +189 -0
  158. package/dist/styles/hover-card.css +27 -0
  159. package/dist/styles/image-cropper.css +205 -0
  160. package/dist/styles/image-viewer.css +422 -0
  161. package/dist/styles/input-number.css +21 -0
  162. package/dist/styles/journey-map.css +19 -0
  163. package/dist/styles/keypad.css +92 -0
  164. package/dist/styles/layout.css +72 -0
  165. package/dist/styles/leaflet.css +661 -0
  166. package/dist/styles/log.css +203 -0
  167. package/dist/styles/markdown.css +379 -0
  168. package/dist/styles/node-graph.css +142 -0
  169. package/dist/styles/number-ticker.css +83 -0
  170. package/dist/styles/overflow-list.css +51 -0
  171. package/dist/styles/overlay.css +284 -0
  172. package/dist/styles/pagination.css +21 -0
  173. package/dist/styles/pan-zoom.css +136 -0
  174. package/dist/styles/radar.css +1 -0
  175. package/dist/styles/rating.css +121 -0
  176. package/dist/styles/resizable.css +108 -0
  177. package/dist/styles/scroll-area.css +122 -0
  178. package/dist/styles/select.css +557 -0
  179. package/dist/styles/selection.css +264 -0
  180. package/dist/styles/slider.css +145 -0
  181. package/dist/styles/slot.css +109 -0
  182. package/dist/styles/sortable.css +159 -0
  183. package/dist/styles/sparkline.css +24 -0
  184. package/dist/styles/steps.css +27 -0
  185. package/dist/styles/tabs.css +79 -0
  186. package/dist/styles/tag-input.css +213 -0
  187. package/dist/styles/time-picker.css +66 -0
  188. package/dist/styles/timeline.css +24 -0
  189. package/dist/styles/toast.css +160 -0
  190. package/dist/styles/toggle-group.css +107 -0
  191. package/dist/styles/tooltip.css +78 -0
  192. package/dist/styles/transfer.css +148 -0
  193. package/dist/styles/tree-select.css +131 -0
  194. package/dist/styles/tree.css +59 -0
  195. package/dist/styles/typewriter.css +62 -0
  196. package/dist/styles/upload.css +60 -0
  197. package/dist/styles/virtual-list.css +112 -0
  198. package/dist/styles.css +2094 -421
  199. package/dist/tabs/index.d.ts +9 -0
  200. package/dist/tabs/index.js +66 -16
  201. package/dist/tabs/useOverflow.d.ts +20 -0
  202. package/dist/tabs/useOverflow.js +145 -0
  203. package/dist/tag-input/index.d.ts +7 -2
  204. package/dist/tag-input/index.js +70 -26
  205. package/dist/time-picker/index.js +38 -20
  206. package/dist/toast/index.d.ts +19 -11
  207. package/dist/toast/index.js +48 -8
  208. package/dist/toast/manager.d.ts +42 -0
  209. package/dist/toast/manager.js +85 -0
  210. package/dist/toast/useToastNavigation.d.ts +3 -0
  211. package/dist/toast/useToastNavigation.js +75 -0
  212. package/dist/tokens.css +4 -0
  213. package/dist/tooltip/Tooltip.js +3 -3
  214. package/dist/transfer/index.d.ts +5 -0
  215. package/dist/transfer/index.js +93 -29
  216. package/dist/transfer/model.d.ts +4 -0
  217. package/dist/transfer/model.js +37 -11
  218. package/dist/tree/index.d.ts +5 -1
  219. package/dist/tree/index.js +5 -5
  220. package/dist/tree-select/index.js +67 -30
  221. package/dist/tree-select/model.d.ts +11 -5
  222. package/dist/tree-select/model.js +111 -83
  223. package/dist/typewriter/index.js +10 -10
  224. package/dist/upload/index.js +30 -16
  225. package/dist/virtual-list/index.js +7 -5
  226. package/package.json +413 -3
@@ -1,4 +1,4 @@
1
- import type { HTMLAttributes } from 'react';
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
2
  export interface PaginationLabels {
3
3
  nav: string;
4
4
  first: string;
@@ -16,6 +16,15 @@ export interface PaginationProps extends Omit<HTMLAttributes<HTMLElement>, 'chil
16
16
  siblingCount?: number;
17
17
  boundaryCount?: number;
18
18
  showFirstLast?: boolean;
19
+ showPreviousNext?: boolean;
20
+ hideOnSinglePage?: boolean;
21
+ simple?: boolean;
22
+ /** Non-interactive content inside each numbered page button. */
23
+ renderPage?: (page: number, state: {
24
+ isCurrent: boolean;
25
+ }) => ReactNode;
26
+ /** Non-interactive current/total content in simple mode. */
27
+ renderSummary?: (page: number, totalPages: number) => ReactNode;
19
28
  disabled?: boolean;
20
29
  size?: 'sm' | 'md' | 'lg';
21
30
  labels?: Partial<PaginationLabels>;
@@ -9,7 +9,7 @@ const defaultLabels = {
9
9
  nav: 'Pagination', first: 'First page', previous: 'Previous page', next: 'Next page', last: 'Last page',
10
10
  page: page => `Page ${page}`, ellipsis: 'More pages',
11
11
  };
12
- export const Pagination = forwardRef(function Pagination({ totalPages, page, defaultPage = 1, onPageChange, siblingCount = 1, boundaryCount = 1, showFirstLast = false, disabled = false, size = 'md', labels, className, ...props }, ref) {
12
+ export const Pagination = forwardRef(function Pagination({ totalPages, page, defaultPage = 1, onPageChange, siblingCount = 1, boundaryCount = 1, showFirstLast = false, showPreviousNext = true, hideOnSinglePage = false, simple = false, renderPage, renderSummary, disabled = false, size = 'md', labels, className, ...props }, ref) {
13
13
  const total = normalizeTotal(totalPages);
14
14
  const [internalPage, setInternalPage] = useState(() => clampPage(defaultPage, total));
15
15
  const current = clampPage(page ?? internalPage, total);
@@ -35,7 +35,9 @@ export const Pagination = forwardRef(function Pagination({ totalPages, page, def
35
35
  paths.push({ d: 'M19 6v12' });
36
36
  return _jsx("li", { children: _jsx(Button, { variant: "ghost", size: size === 'md' ? 'icon' : `icon-${size}`, "aria-label": text[kind], title: text[kind], disabled: disabled || unavailable, onClick: () => select(target), children: _jsx(PencilIcon, { className: "inkra-pagination__arrow", paths: paths, size: 20, sourceSize: 24 }) }) }, kind);
37
37
  };
38
- return _jsx("nav", { "aria-label": text.nav, ...props, ref: ref, className: ['inkra-pagination', className].filter(Boolean).join(' '), "data-empty": !total || undefined, children: _jsxs("ul", { className: "inkra-pagination__list", children: [showFirstLast && arrow('first', 1, current <= 1), arrow('previous', current - 1, current <= 1), getPaginationEntries(total, current, siblingCount, boundaryCount).map(entry => typeof entry === 'number'
39
- ? _jsx("li", { children: _jsxs(Button, { size: size, variant: "ghost", className: "inkra-pagination__page", "aria-label": text.page(entry), "aria-current": entry === current ? 'page' : undefined, disabled: disabled, onClick: () => select(entry), children: [entry === current && _jsx(PencilSurface, { as: "span", "aria-hidden": "true", variant: "selection", className: "inkra-pagination__selection", seed: `page-${entry}` }), _jsx(PencilText, { children: String(entry) })] }) }, entry)
40
- : _jsxs("li", { className: "inkra-pagination__ellipsis", children: [_jsx("span", { className: "inkra-pagination__sr-only", children: text.ellipsis }), _jsx(PencilText, { "aria-hidden": "true", children: "..." })] }, entry)), arrow('next', current + 1, !total || current >= total), showFirstLast && arrow('last', total, !total || current >= total)] }) });
38
+ if (hideOnSinglePage && total <= 1)
39
+ return null;
40
+ return _jsx("nav", { "aria-label": text.nav, ...props, ref: ref, className: ['inkra-pagination', className].filter(Boolean).join(' '), "data-empty": !total || undefined, "data-simple": simple || undefined, children: _jsxs("ul", { className: "inkra-pagination__list", children: [showFirstLast && arrow('first', 1, current <= 1), showPreviousNext && arrow('previous', current - 1, current <= 1), simple ? _jsx("li", { className: "inkra-pagination__summary", "aria-live": "polite", "aria-atomic": "true", children: renderSummary ? renderSummary(current, total) : _jsx(PencilText, { children: `${current} / ${total}` }) }) : getPaginationEntries(total, current, siblingCount, boundaryCount).map(entry => typeof entry === 'number'
41
+ ? _jsx("li", { className: "inkra-pagination__page-item", children: _jsxs(Button, { size: size, variant: "ghost", className: "inkra-pagination__page", "aria-label": text.page(entry), "aria-current": entry === current ? 'page' : undefined, disabled: disabled, onClick: () => select(entry), children: [entry === current && _jsx(PencilSurface, { as: "span", "aria-hidden": "true", variant: "selection", className: "inkra-pagination__selection", seed: `page-${entry}` }), renderPage ? renderPage(entry, { isCurrent: entry === current }) : _jsx(PencilText, { children: String(entry) })] }) }, entry)
42
+ : _jsxs("li", { className: "inkra-pagination__ellipsis", children: [_jsx("span", { className: "inkra-pagination__sr-only", children: text.ellipsis }), _jsx(PencilText, { "aria-hidden": "true", children: "..." })] }, entry)), showPreviousNext && arrow('next', current + 1, !total || current >= total), showFirstLast && arrow('last', total, !total || current >= total)] }) });
41
43
  });
@@ -48,6 +48,8 @@ export interface PanZoomProps extends Omit<HTMLAttributes<HTMLDivElement>, 'chil
48
48
  minScale?: number;
49
49
  maxScale?: number;
50
50
  wheel?: PanZoomWheelMode;
51
+ /** Scale increment per wheel delta unit. Must be finite and positive. */
52
+ wheelStep?: number;
51
53
  pinch?: boolean;
52
54
  pan?: boolean;
53
55
  keyboard?: boolean;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';
3
- import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useRef, useState, } from 'react';
3
+ import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useRef, } from 'react';
4
4
  import { PencilSurface } from '../PencilSurface.js';
5
5
  const defaultTransform = { x: 0, y: 0, scale: 1 };
6
6
  const interactionExclusions = [
@@ -26,16 +26,18 @@ function transformsEqual(left, right) {
26
26
  function prefersReducedMotion(element) {
27
27
  return element?.ownerDocument.defaultView?.matchMedia('(prefers-reduced-motion: reduce)').matches ?? false;
28
28
  }
29
- function PanZoomRoot({ children, value, defaultValue = defaultTransform, onTransformChange, minScale = 0.25, maxScale = 8, wheel = 'modifier', pinch = true, pan = true, keyboard = true, keyboardPanStep = 48, keyboardZoomStep = 0.25, doubleClickZoom = false, disabled = false, readOnly = false, limitToBounds = true, centerZoomedOut = true, centerOnInit = false, fitOnInit = false, className, viewportClassName, contentClassName, style, viewportStyle, contentStyle, viewportProps, contentProps, instructions, 'aria-label': ariaLabel = 'Pan and zoom content', ...props }, forwardedRef) {
29
+ function PanZoomRoot({ children, value, defaultValue = defaultTransform, onTransformChange, minScale = 0.25, maxScale = 8, wheel = 'modifier', wheelStep = 0.001, pinch = true, pan = true, keyboard = true, keyboardPanStep = 48, keyboardZoomStep = 0.25, doubleClickZoom = false, disabled = false, readOnly = false, limitToBounds = true, centerZoomedOut = true, centerOnInit = false, fitOnInit = false, className, viewportClassName, contentClassName, style, viewportStyle, contentStyle, viewportProps, contentProps, instructions, 'aria-label': ariaLabel = 'Pan and zoom content', ...props }, forwardedRef) {
30
30
  const resolvedMinScale = Math.max(Number.isFinite(minScale) ? minScale : 0.25, 0.0001);
31
31
  const resolvedMaxScale = Math.max(Number.isFinite(maxScale) ? maxScale : 8, resolvedMinScale);
32
+ const resolvedWheelStep = Number.isFinite(wheelStep) && wheelStep > 0 ? wheelStep : 0.001;
32
33
  const controlled = value !== undefined;
33
34
  const initialTransform = normalizedTransform(value ?? defaultValue, resolvedMinScale, resolvedMaxScale);
34
- const [uncontrolledTransform, setUncontrolledTransform] = useState(initialTransform);
35
- const visibleTransform = normalizedTransform(value ?? uncontrolledTransform, resolvedMinScale, resolvedMaxScale);
36
35
  const engineRef = useRef(null);
37
36
  const instructionsId = useId();
37
+ // The engine owns the live DOM transform. Keep its snapshot for imperative reads,
38
+ // rather than rerendering the complete content subtree on every gesture frame.
38
39
  const committedRef = useRef(initialTransform);
40
+ const visibleTransform = normalizedTransform(value ?? committedRef.current, resolvedMinScale, resolvedMaxScale);
39
41
  const programmaticRef = useRef(false);
40
42
  const pointerBlocksPanRef = useRef(false);
41
43
  const wheelTargetBlocksZoomRef = useRef(false);
@@ -132,6 +134,7 @@ function PanZoomRoot({ children, value, defaultValue = defaultTransform, onTrans
132
134
  excluded: interactionExclusions,
133
135
  activationKeys: () => !pointerBlocksPanRef.current,
134
136
  }, pinch: { disabled: userInteractionDisabled || !pinch, excluded: interactionExclusions }, wheel: {
137
+ step: resolvedWheelStep,
135
138
  disabled: userInteractionDisabled || wheel === 'disabled',
136
139
  activationKeys: wheelActivation,
137
140
  excluded: interactionExclusions,
@@ -144,8 +147,6 @@ function PanZoomRoot({ children, value, defaultValue = defaultTransform, onTrans
144
147
  }, doubleClick: { disabled: userInteractionDisabled || !doubleClickZoom, mode: 'zoomIn', excluded: interactionExclusions }, zoomAnimation: { disabled: false, animationTime: 200 }, velocityAnimation: { disabled: prefersReducedMotion(engineRef.current?.instance.wrapperComponent ?? null) }, onPanningStop: reconcileControlled, onPinchStop: reconcileControlled, onWheelStop: reconcileControlled, onZoomStop: reconcileControlled, onTransform: (_, state) => {
145
148
  const next = normalizedTransform({ x: state.positionX, y: state.positionY, scale: state.scale }, resolvedMinScale, resolvedMaxScale);
146
149
  committedRef.current = next;
147
- if (!controlled)
148
- setUncontrolledTransform(next);
149
150
  onTransformChange?.(next, { reason: programmaticRef.current ? 'programmatic' : 'gesture' });
150
151
  }, children: _jsx(TransformComponent, { wrapperClass: ['inkra-pan-zoom__viewport', viewportClassName].filter(Boolean).join(' '), contentClass: ['inkra-pan-zoom__content', contentClassName].filter(Boolean).join(' '), wrapperStyle: viewportStyle, contentStyle: contentStyle, wrapperProps: {
151
152
  ...viewportProps,
package/dist/pencil.d.ts CHANGED
@@ -3,6 +3,7 @@ export interface PencilPathOptions {
3
3
  seed: string | number;
4
4
  width?: number;
5
5
  height?: number;
6
+ /** Point count, clamped to 2-65,536. Non-finite values use the default (7). */
6
7
  points?: number;
7
8
  roughness?: number;
8
9
  }
package/dist/pencil.js CHANGED
@@ -19,11 +19,11 @@ function createRandom(seed) {
19
19
  }
20
20
  export function createPencilPoints({ seed, width = 100, height = 8, points = 7, roughness = 1, }) {
21
21
  const random = createRandom(seed);
22
- const safeWidth = Math.max(1, width);
23
- const safeHeight = Math.max(1, height);
24
- const pointCount = Math.max(2, Math.floor(points));
22
+ const safeWidth = Number.isFinite(width) ? Math.max(1, width) : 100;
23
+ const safeHeight = Number.isFinite(height) ? Math.max(1, height) : 8;
24
+ const pointCount = Number.isFinite(points) ? Math.max(2, Math.min(65536, Math.floor(points))) : 7;
25
25
  const center = safeHeight / 2;
26
- const amplitude = Math.min(safeHeight * 0.32, Math.max(0, roughness));
26
+ const amplitude = Math.min(safeHeight * 0.32, Math.max(0, Number.isFinite(roughness) ? roughness : 1));
27
27
  return Array.from({ length: pointCount }, (_, index) => {
28
28
  const progress = index / (pointCount - 1);
29
29
  const xJitter = index === 0 || index === pointCount - 1 ? 0 : (random() - 0.5) * 1.4;
@@ -29,7 +29,9 @@ export interface RadarValueContext {
29
29
  missing: boolean;
30
30
  }
31
31
  export interface RadarProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onChange'> {
32
+ /** Immutable snapshots: replace the axes array when an axis changes. */
32
33
  axes: readonly RadarAxis[];
34
+ /** Immutable snapshots: replace series and changed records/data when values change. */
33
35
  series: readonly RadarSeries[];
34
36
  label: ReactNode;
35
37
  description?: ReactNode;
@@ -87,10 +87,13 @@ function RadarInk({ axisAngles, axes, levels, radius, center, seriesPoints, }) {
87
87
  const valid = points.filter(point => point.value != null).map(point => coordinate(point.angle, point.radius));
88
88
  if (valid.length < 2)
89
89
  return;
90
- const minX = Math.min(...valid.map(point => point[0]));
91
- const maxX = Math.max(...valid.map(point => point[0]));
92
- const minY = Math.min(...valid.map(point => point[1]));
93
- const maxY = Math.max(...valid.map(point => point[1]));
90
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
91
+ for (const [x, y] of valid) {
92
+ minX = Math.min(minX, x);
93
+ maxX = Math.max(maxX, x);
94
+ minY = Math.min(minY, y);
95
+ maxY = Math.max(maxY, y);
96
+ }
94
97
  context.save();
95
98
  context.beginPath();
96
99
  valid.forEach(([x, y], pointIndex) => pointIndex ? context.lineTo(x, y) : context.moveTo(x, y));
@@ -139,6 +142,7 @@ function RadarInk({ axisAngles, axes, levels, radius, center, seriesPoints, }) {
139
142
  }, [axes, axisAngles, center, levels, radius, seriesPoints]);
140
143
  return _jsx("canvas", { "aria-hidden": "true", className: "inkra-radar__ink", "data-ready": ready || undefined, ref: canvasRef });
141
144
  }
145
+ const noAxes = [];
142
146
  const palette = ['#4b96aa', '#994f3f', '#629568', '#8d6b9c', '#c58b47'];
143
147
  function keyPart(value) { return JSON.stringify([typeof value, value]); }
144
148
  function sameKey(a, b) { return a != null && b != null && a === b; }
@@ -156,8 +160,14 @@ function textNode(value, seed) {
156
160
  return typeof value === 'string' || typeof value === 'number' ? _jsx(PencilText, { seed: seed, children: String(value) }) : value;
157
161
  }
158
162
  function domainForAxis(axis, values) {
159
- let min = finite(axis.min) ? axis.min : values.length ? Math.min(...values) : 0;
160
- let max = finite(axis.max) ? axis.max : values.length ? Math.max(...values) : 1;
163
+ let minimum = values.length ? values[0] : 0;
164
+ let maximum = values.length ? values[0] : 1;
165
+ for (let index = 1; index < values.length; index++) {
166
+ minimum = Math.min(minimum, values[index]);
167
+ maximum = Math.max(maximum, values[index]);
168
+ }
169
+ let min = finite(axis.min) ? axis.min : minimum;
170
+ let max = finite(axis.max) ? axis.max : maximum;
161
171
  if (min > max)
162
172
  [min, max] = [max, min];
163
173
  if (min === max) {
@@ -175,7 +185,7 @@ export const Radar = forwardRef(function Radar({ axes, series, label, descriptio
175
185
  const descriptionId = description == null ? undefined : `inkra-radar-${id}-description`;
176
186
  const [internalSelected, setInternalSelected] = useState(defaultSelectedSeries);
177
187
  const selected = selectedSeries === undefined ? internalSelected : selectedSeries ?? null;
178
- const visibleSeries = series.filter(item => !item.hidden);
188
+ const visibleSeries = useMemo(() => series.filter(item => !item.hidden), [series]);
179
189
  const domains = useMemo(() => new Map(axes.map(axis => {
180
190
  const values = visibleSeries.flatMap(item => item.data.filter(point => point.axisId === axis.id && finite(point.value)).map(point => point.value));
181
191
  return [keyPart(axis.id), domainForAxis(axis, values)];
@@ -183,15 +193,27 @@ export const Radar = forwardRef(function Radar({ axes, series, label, descriptio
183
193
  const side = 1000;
184
194
  const radius = Math.max(120, Math.min(maxRadius ?? 390, 470));
185
195
  const axisCount = axes.length;
186
- const angles = axes.map((_, index) => (index * Math.PI * 2) / Math.max(1, axisCount));
196
+ const angles = useMemo(() => axes.map((_, index) => (index * Math.PI * 2) / Math.max(1, axisCount)), [axes, axisCount]);
187
197
  const center = side / 2;
188
- const valuePoints = (item) => axes.map((axis, index) => {
189
- const point = item.data.find(candidate => candidate.axisId === axis.id) ?? { axisId: axis.id, value: null };
190
- const domain = domains.get(keyPart(axis.id)) ?? [0, 1];
191
- const value = finite(point.value) ? point.value : null;
192
- const scale = scaleLinear().domain(domain).range([0, radius]).clamp(true);
193
- return { angle: angles[index], radius: value == null ? 0 : scale(value), value, point, axis };
194
- });
198
+ const scales = useMemo(() => axes.map(axis => scaleLinear()
199
+ .domain(domains.get(keyPart(axis.id)) ?? [0, 1]).range([0, radius]).clamp(true)), [axes, domains, radius]);
200
+ const seriesPoints = useMemo(() => visibleSeries.map((item, index) => {
201
+ // Preserve the first point for duplicate axis ids, matching Array.find.
202
+ const pointsByAxis = new Map();
203
+ for (const point of item.data) {
204
+ // Map uses SameValueZero, while the prior lookup used strict equality.
205
+ if (typeof point.axisId === 'number' && Number.isNaN(point.axisId))
206
+ continue;
207
+ if (!pointsByAxis.has(point.axisId))
208
+ pointsByAxis.set(point.axisId, point);
209
+ }
210
+ const points = axes.map((axis, axisIndex) => {
211
+ const point = pointsByAxis.get(axis.id) ?? { axisId: axis.id, value: null };
212
+ const value = finite(point.value) ? point.value : null;
213
+ return { angle: angles[axisIndex], radius: value == null ? 0 : scales[axisIndex](value), value, point, axis };
214
+ });
215
+ return { item, points, color: item.color ?? palette[index % palette.length], index };
216
+ }), [angles, axes, scales, visibleSeries]);
195
217
  const line = useMemo(() => lineRadial().angle(point => point.angle).radius(point => point.radius).curve(curveLinearClosed), []);
196
218
  const rootStyle = { ...style, '--inkra-radar-size': cssSize(size, 'min(100%, 34rem)'), '--inkra-radar-levels': Math.max(2, Math.min(8, Math.round(levels))) };
197
219
  const selectSeries = (item) => {
@@ -205,16 +227,14 @@ export const Radar = forwardRef(function Radar({ axes, series, label, descriptio
205
227
  return renderValueLabel(context);
206
228
  return context.missing ? missingLabel : showValues ? formatNumber(context.value, context.axis, getValueLabel) : '•';
207
229
  };
208
- return _jsxs("div", { ...props, ref: forwardedRef, className: ['inkra-radar', className].filter(Boolean).join(' '), style: rootStyle, children: [_jsxs("div", { className: "inkra-radar__heading", children: [_jsx("div", { className: "inkra-radar__label", id: labelId, children: typeof label === 'string' ? _jsx(PencilText, { seed: `${id}-label`, children: label }) : label }), description == null ? null : _jsx("div", { className: "inkra-radar__description", id: descriptionId, children: textNode(description, `${id}-description`) })] }), axes.length < 3 || visibleSeries.length === 0 ? _jsx("div", { className: "inkra-radar__empty", children: textNode(emptyMessage, `${id}-empty`) }) : _jsxs(_Fragment, { children: [_jsxs("div", { className: "inkra-radar__viewport", children: [_jsx(RadarInk, { axisAngles: angles, axes: showAxes ? axes : [], center: center, levels: Math.max(2, Math.min(8, Math.round(levels))), radius: radius, seriesPoints: visibleSeries.map((item, index) => ({ item, points: valuePoints(item), color: item.color ?? palette[index % palette.length], index })) }), _jsxs("svg", { "aria-hidden": "true", className: "inkra-radar__svg-fallback", viewBox: `0 0 ${side} ${side}`, focusable: "false", role: "presentation", children: [_jsx("circle", { cx: center, cy: center, r: radius, fill: "none", stroke: "currentColor", strokeWidth: "1" }), visibleSeries.map(item => _jsx("path", { d: line(valuePoints(item)) ?? undefined, transform: `translate(${center} ${center})` }, keyPart(item.id)))] }), showLabels ? _jsx("div", { "aria-hidden": "true", className: "inkra-radar__labels", children: axes.map((axis, index) => {
230
+ return _jsxs("div", { ...props, ref: forwardedRef, className: ['inkra-radar', className].filter(Boolean).join(' '), style: rootStyle, children: [_jsxs("div", { className: "inkra-radar__heading", children: [_jsx("div", { className: "inkra-radar__label", id: labelId, children: typeof label === 'string' ? _jsx(PencilText, { seed: `${id}-label`, children: label }) : label }), description == null ? null : _jsx("div", { className: "inkra-radar__description", id: descriptionId, children: textNode(description, `${id}-description`) })] }), axes.length < 3 || visibleSeries.length === 0 ? _jsx("div", { className: "inkra-radar__empty", children: textNode(emptyMessage, `${id}-empty`) }) : _jsxs(_Fragment, { children: [_jsxs("div", { className: "inkra-radar__viewport", children: [_jsx(RadarInk, { axisAngles: angles, axes: showAxes ? axes : noAxes, center: center, levels: Math.max(2, Math.min(8, Math.round(levels))), radius: radius, seriesPoints: seriesPoints }), _jsxs("svg", { "aria-hidden": "true", className: "inkra-radar__svg-fallback", viewBox: `0 0 ${side} ${side}`, focusable: "false", role: "presentation", children: [_jsx("circle", { cx: center, cy: center, r: radius, fill: "none", stroke: "currentColor", strokeWidth: "1" }), seriesPoints.map(({ item, points }) => _jsx("path", { d: line(points) ?? undefined, transform: `translate(${center} ${center})` }, keyPart(item.id)))] }), showLabels ? _jsx("div", { "aria-hidden": "true", className: "inkra-radar__labels", children: axes.map((axis, index) => {
209
231
  const x = 50 + Math.sin(angles[index]) * 48;
210
232
  const y = 50 - Math.cos(angles[index]) * 48;
211
233
  return _jsx("span", { className: "inkra-radar__axis-label", style: { left: `${x}%`, top: `${y}%` }, children: renderAxisLabel ? renderAxisLabel(axis) : textNode(axis.label, `${id}-axis-${String(axis.id)}`) }, keyPart(axis.id));
212
- }) }) : null] }), _jsx("div", { "aria-describedby": descriptionId, "aria-labelledby": labelId, className: "inkra-radar__summary", role: "group", children: visibleSeries.map((item, index) => {
213
- const points = valuePoints(item);
214
- const color = item.color ?? palette[index % palette.length];
234
+ }) }) : null] }), _jsx("div", { "aria-describedby": descriptionId, "aria-labelledby": labelId, className: "inkra-radar__summary", role: "group", children: seriesPoints.map(({ item, points, color }) => {
215
235
  return _jsxs("div", { className: "inkra-radar__series-summary", "data-series-id": item.id, children: [_jsxs(Button, { "aria-pressed": sameKey(selected, item.id), className: "inkra-radar__legend-button", onClick: () => selectSeries(item), size: "sm", variant: "ghost", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-radar__legend-mark", radius: 4, seed: `radar-${keyPart(item.id)}`, style: paintStyle(color), variant: sameKey(selected, item.id) ? 'selection' : 'quiet' }), textNode(item.label, `${id}-summary-${String(item.id)}`)] }), _jsx("div", { className: "inkra-radar__values", children: points.map((point, pointIndex) => {
216
236
  const context = { axis: point.axis, series: item, point: point.point, value: point.value, missing: point.value == null };
217
237
  return _jsxs("span", { className: "inkra-radar__value", "data-missing": context.missing || undefined, children: [_jsx("strong", { children: textNode(point.axis.label, `${id}-value-axis-${String(point.axis.id)}`) }), " ", textNode(renderValue(context), `${id}-value-${String(item.id)}-${String(point.axis.id)}`)] }, pointIndex);
218
238
  }) })] }, keyPart(item.id));
219
- }) }), showLegend ? _jsxs("div", { "aria-label": typeof legendLabel === 'string' ? legendLabel : undefined, className: "inkra-radar__legend", role: "group", children: [_jsx("span", { className: "inkra-radar__legend-title", children: textNode(legendLabel, `${id}-legend-title`) }), visibleSeries.map((item, index) => _jsxs(Button, { "aria-pressed": sameKey(selected, item.id), className: "inkra-radar__legend-button", onClick: () => selectSeries(item), size: "sm", variant: "ghost", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-radar__legend-mark", radius: 4, seed: `radar-legend-${keyPart(item.id)}`, style: paintStyle(item.color ?? palette[index % palette.length]), variant: sameKey(selected, item.id) ? 'selection' : 'quiet' }), textNode(item.label, `${id}-legend-${String(item.id)}`)] }, keyPart(item.id)))] }) : null, showTable ? _jsx(Table.Container, { "aria-label": typeof label === 'string' ? label : undefined, "aria-describedby": descriptionId, children: _jsxs(Table.Root, { className: "inkra-radar__table", children: [_jsx(Table.Caption, { children: textNode(tableCaption, `${id}-table-caption`) }), _jsx(Table.Header, { children: _jsxs(Table.Row, { children: [_jsx(Table.Head, { children: textNode(seriesLabel, `${id}-table-series`) }), axes.map(axis => _jsx(Table.Head, { children: textNode(axis.label, `${id}-table-axis-${String(axis.id)}`) }, keyPart(axis.id)))] }) }), _jsx(Table.Body, { children: visibleSeries.map(item => _jsxs(Table.Row, { children: [_jsx(Table.Head, { scope: "row", children: textNode(item.label, `${id}-table-series-${String(item.id)}`) }), axes.map(axis => { const point = item.data.find(candidate => candidate.axisId === axis.id); const missing = !finite(point?.value); return _jsx(Table.Cell, { "data-missing": missing || undefined, children: textNode(missing ? missingLabel : formatNumber(point?.value, axis, getValueLabel), `${id}-table-value-${String(item.id)}-${String(axis.id)}`) }, keyPart(axis.id)); })] }, keyPart(item.id))) })] }) }) : null] })] });
239
+ }) }), showLegend ? _jsxs("div", { "aria-label": typeof legendLabel === 'string' ? legendLabel : undefined, className: "inkra-radar__legend", role: "group", children: [_jsx("span", { className: "inkra-radar__legend-title", children: textNode(legendLabel, `${id}-legend-title`) }), visibleSeries.map((item, index) => _jsxs(Button, { "aria-pressed": sameKey(selected, item.id), className: "inkra-radar__legend-button", onClick: () => selectSeries(item), size: "sm", variant: "ghost", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-radar__legend-mark", radius: 4, seed: `radar-legend-${keyPart(item.id)}`, style: paintStyle(item.color ?? palette[index % palette.length]), variant: sameKey(selected, item.id) ? 'selection' : 'quiet' }), textNode(item.label, `${id}-legend-${String(item.id)}`)] }, keyPart(item.id)))] }) : null, showTable ? _jsx(Table.Container, { "aria-label": typeof label === 'string' ? label : undefined, "aria-describedby": descriptionId, children: _jsxs(Table.Root, { className: "inkra-radar__table", children: [_jsx(Table.Caption, { children: textNode(tableCaption, `${id}-table-caption`) }), _jsx(Table.Header, { children: _jsxs(Table.Row, { children: [_jsx(Table.Head, { children: textNode(seriesLabel, `${id}-table-series`) }), axes.map(axis => _jsx(Table.Head, { children: textNode(axis.label, `${id}-table-axis-${String(axis.id)}`) }, keyPart(axis.id)))] }) }), _jsx(Table.Body, { children: seriesPoints.map(({ item, points }) => _jsxs(Table.Row, { children: [_jsx(Table.Head, { scope: "row", children: textNode(item.label, `${id}-table-series-${String(item.id)}`) }), points.map(({ axis, value }) => { const missing = value === null; return _jsx(Table.Cell, { "data-missing": missing || undefined, children: textNode(missing ? missingLabel : formatNumber(value, axis, getValueLabel), `${id}-table-value-${String(item.id)}-${String(axis.id)}`) }, keyPart(axis.id)); })] }, keyPart(item.id))) })] }) }) : null] })] });
220
240
  });
@@ -13,6 +13,10 @@ export interface RatingProps extends Omit<HTMLAttributes<HTMLDivElement>, 'child
13
13
  value?: number;
14
14
  defaultValue?: number;
15
15
  onValueChange?: (value: number) => void;
16
+ name?: string;
17
+ form?: string;
18
+ hoverPreview?: boolean;
19
+ onPreviewChange?: (value: number | null) => void;
16
20
  count?: number;
17
21
  step?: number;
18
22
  clearable?: RatingClearStrategy;
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef, useEffect, useId, useMemo, useState, } from 'react';
2
+ import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useMemo, useRef, useState, } from 'react';
3
3
  import { PencilIcon } from '../PencilIcon.js';
4
+ import { PencilSurface } from '../PencilSurface.js';
4
5
  const defaultSymbolPaths = [
5
6
  { d: 'M12 2.5 L21.5 12 L12 21.5 L2.5 12 Z' },
6
7
  { d: 'M7.5 12 L12 7.5 L16.5 12 L12 16.5 Z', mode: 'scuff' },
@@ -16,6 +17,10 @@ function decimalPlaces(value) {
16
17
  }
17
18
  export function normalizeRatingValue(value, count, step) {
18
19
  const numeric = typeof value === 'number' && Number.isFinite(value) ? value : 0;
20
+ if (numeric <= 0)
21
+ return 0;
22
+ if (numeric >= count)
23
+ return count;
19
24
  const precision = Math.min(8, Math.max(decimalPlaces(step), decimalPlaces(count)));
20
25
  const snapped = Math.round(numeric / step) * step;
21
26
  return Number(Math.min(count, Math.max(0, snapped)).toFixed(precision));
@@ -26,28 +31,86 @@ function allowsReselectClear(clearable) {
26
31
  function allowsKeyboardClear(clearable) {
27
32
  return clearable === true || clearable === 'keyboard';
28
33
  }
29
- export const Rating = forwardRef(function Rating({ value, defaultValue = 0, onValueChange, count: countProp = 5, step: stepProp = 1, clearable = false, disabled = false, readOnly = false, size = 'md', dir, symbol, renderSymbol, getValueLabel = (nextValue, maximum) => `${nextValue} of ${maximum}`, className = '', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, onBlur, onKeyDown, onPointerLeave, ...props }, forwardedRef) {
34
+ function previousRatingValue(value, count, step) {
35
+ if (value >= count) {
36
+ const precision = Math.min(8, Math.max(decimalPlaces(step), decimalPlaces(count)));
37
+ const previousIndex = Math.max(0, Math.ceil(count / step) - 1);
38
+ return Number((previousIndex * step).toFixed(precision));
39
+ }
40
+ return value - step;
41
+ }
42
+ export const Rating = forwardRef(function Rating({ value, defaultValue = 0, onValueChange, name, form, hoverPreview = true, onPreviewChange, count: countProp = 5, step: stepProp = 1, clearable = false, disabled = false, readOnly = false, size = 'md', dir, symbol, renderSymbol, getValueLabel = (nextValue, maximum) => `${nextValue} of ${maximum}`, className = '', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, onBlur, onKeyDown, onPointerDown, onPointerLeave, ...props }, forwardedRef) {
30
43
  const count = Math.max(1, Math.floor(finitePositive(countProp, 5)));
31
44
  const step = Math.min(count, finitePositive(stepProp, 1));
32
45
  const controlled = value !== undefined;
33
46
  const [uncontrolledValue, setUncontrolledValue] = useState(() => normalizeRatingValue(defaultValue, count, step));
34
47
  const committedValue = normalizeRatingValue(controlled ? value : uncontrolledValue, count, step);
35
48
  const [previewValue, setPreviewValue] = useState(null);
36
- const displayValue = previewValue ?? committedValue;
49
+ const previewRef = useRef(null);
50
+ const previewCallback = useRef(onPreviewChange);
51
+ previewCallback.current = onPreviewChange;
52
+ const resetState = useRef({ controlled, defaultValue, count, step });
53
+ resetState.current = { controlled, defaultValue, count, step };
54
+ const inputRef = useRef(null);
55
+ const rootRef = useRef(null);
56
+ const symbolsRef = useRef(null);
57
+ const pendingKeyboardValueRef = useRef(null);
58
+ const pendingKeyboardTimerRef = useRef(undefined);
59
+ useImperativeHandle(forwardedRef, () => rootRef.current);
60
+ const clearPendingKeyboard = useCallback(() => {
61
+ clearTimeout(pendingKeyboardTimerRef.current);
62
+ pendingKeyboardTimerRef.current = undefined;
63
+ pendingKeyboardValueRef.current = null;
64
+ }, []);
65
+ const changePreview = useCallback((next) => {
66
+ if (previewRef.current === next)
67
+ return;
68
+ previewRef.current = next;
69
+ setPreviewValue(next);
70
+ previewCallback.current?.(next);
71
+ }, []);
72
+ const previewing = hoverPreview && !disabled && !readOnly && previewValue !== null;
73
+ const displayValue = previewing ? normalizeRatingValue(previewValue, count, step) : committedValue;
37
74
  const symbolId = useId();
38
75
  const symbolIndexes = useMemo(() => Array.from({ length: count }, (_, index) => index), [count]);
39
76
  const defaultIconSize = size === 'sm' ? 26 : size === 'lg' ? 38 : 32;
40
77
  useEffect(() => {
41
- if (disabled || readOnly)
42
- setPreviewValue(null);
43
- }, [disabled, readOnly]);
78
+ if (disabled || readOnly || !hoverPreview)
79
+ changePreview(null);
80
+ }, [disabled, readOnly, hoverPreview, changePreview]);
81
+ useEffect(() => {
82
+ clearPendingKeyboard();
83
+ changePreview(null);
84
+ }, [count, step, changePreview, clearPendingKeyboard]);
85
+ useEffect(() => clearPendingKeyboard, [clearPendingKeyboard]);
86
+ useEffect(() => {
87
+ const owner = inputRef.current?.form;
88
+ if (!owner)
89
+ return;
90
+ let resetTimer;
91
+ const reset = (event) => {
92
+ clearTimeout(resetTimer);
93
+ resetTimer = setTimeout(() => {
94
+ if (event.defaultPrevented)
95
+ return;
96
+ clearPendingKeyboard();
97
+ changePreview(null);
98
+ const state = resetState.current;
99
+ if (!state.controlled)
100
+ setUncontrolledValue(normalizeRatingValue(state.defaultValue, state.count, state.step));
101
+ }, 0);
102
+ };
103
+ owner.addEventListener('reset', reset);
104
+ return () => { clearTimeout(resetTimer); owner.removeEventListener('reset', reset); };
105
+ }, [form, changePreview, clearPendingKeyboard]);
44
106
  const commit = (nextValue) => {
45
107
  if (disabled || readOnly)
46
108
  return;
109
+ clearPendingKeyboard();
47
110
  const normalized = normalizeRatingValue(nextValue, count, step);
48
111
  const cleared = normalized === committedValue && normalized !== 0 && allowsReselectClear(clearable);
49
112
  const resolved = cleared ? 0 : normalized;
50
- setPreviewValue(null);
113
+ changePreview(null);
51
114
  if (resolved === committedValue)
52
115
  return;
53
116
  if (!controlled)
@@ -63,6 +126,30 @@ export const Rating = forwardRef(function Rating({ value, defaultValue = 0, onVa
63
126
  const rawValue = index + Math.min(1, Math.max(0, logicalFraction));
64
127
  return normalizeRatingValue(Math.max(step, Math.ceil((rawValue - Number.EPSILON) / step) * step), count, step);
65
128
  };
129
+ const revealValue = useCallback((nextValue) => {
130
+ const scroller = symbolsRef.current;
131
+ if (!scroller)
132
+ return;
133
+ const targetIndex = Math.max(1, Math.ceil(normalizeRatingValue(nextValue, count, step)));
134
+ const target = scroller.querySelector(`[data-index="${targetIndex}"]`);
135
+ if (!target)
136
+ return;
137
+ const viewport = scroller.getBoundingClientRect();
138
+ const item = target.getBoundingClientRect();
139
+ const delta = item.left < viewport.left ? item.left - viewport.left
140
+ : item.right > viewport.right ? item.right - viewport.right
141
+ : 0;
142
+ if (delta)
143
+ scroller.scrollLeft += delta;
144
+ }, [count, step]);
145
+ useEffect(() => {
146
+ const pending = pendingKeyboardValueRef.current;
147
+ if (pending === null)
148
+ return;
149
+ clearPendingKeyboard();
150
+ if (pending === committedValue)
151
+ revealValue(committedValue);
152
+ }, [committedValue, revealValue, clearPendingKeyboard]);
66
153
  const handleKeyDown = (event) => {
67
154
  onKeyDown?.(event);
68
155
  if (event.defaultPrevented || disabled || readOnly)
@@ -72,11 +159,15 @@ export const Rating = forwardRef(function Rating({ value, defaultValue = 0, onVa
72
159
  if (event.key === 'ArrowUp')
73
160
  nextValue = committedValue + step;
74
161
  if (event.key === 'ArrowDown')
75
- nextValue = committedValue - step;
162
+ nextValue = previousRatingValue(committedValue, count, step);
76
163
  if (event.key === 'ArrowRight')
77
- nextValue = committedValue + (rtl ? -step : step);
164
+ nextValue = rtl
165
+ ? previousRatingValue(committedValue, count, step)
166
+ : committedValue + step;
78
167
  if (event.key === 'ArrowLeft')
79
- nextValue = committedValue + (rtl ? step : -step);
168
+ nextValue = rtl
169
+ ? committedValue + step
170
+ : previousRatingValue(committedValue, count, step);
80
171
  if (event.key === 'Home')
81
172
  nextValue = allowsKeyboardClear(clearable) ? 0 : Math.min(step, count);
82
173
  if (event.key === 'End')
@@ -86,13 +177,19 @@ export const Rating = forwardRef(function Rating({ value, defaultValue = 0, onVa
86
177
  if (nextValue === null)
87
178
  return;
88
179
  event.preventDefault();
89
- setPreviewValue(null);
180
+ changePreview(null);
90
181
  let normalized = normalizeRatingValue(nextValue, count, step);
91
182
  if (normalized === 0 && committedValue > 0 && !allowsKeyboardClear(clearable)) {
92
183
  normalized = Math.min(step, count);
93
184
  }
94
185
  if (normalized === committedValue)
95
186
  return;
187
+ clearPendingKeyboard();
188
+ pendingKeyboardValueRef.current = normalized;
189
+ pendingKeyboardTimerRef.current = setTimeout(() => {
190
+ pendingKeyboardTimerRef.current = undefined;
191
+ pendingKeyboardValueRef.current = null;
192
+ }, 100);
96
193
  if (!controlled)
97
194
  setUncontrolledValue(normalized);
98
195
  onValueChange?.(normalized);
@@ -102,7 +199,7 @@ export const Rating = forwardRef(function Rating({ value, defaultValue = 0, onVa
102
199
  index: index + 1,
103
200
  fraction,
104
201
  filled,
105
- previewing: previewValue !== null,
202
+ previewing,
106
203
  };
107
204
  if (renderSymbol)
108
205
  return renderSymbol(state);
@@ -110,18 +207,25 @@ export const Rating = forwardRef(function Rating({ value, defaultValue = 0, onVa
110
207
  return symbol;
111
208
  return _jsx(PencilIcon, { paths: defaultSymbolPaths, sourceSize: 24, size: defaultIconSize, active: filled, seed: `${symbolId}-${index}` });
112
209
  };
113
- return (_jsx("div", { ...props, "aria-disabled": disabled || undefined, "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Rating'), "aria-labelledby": ariaLabelledBy, "aria-readonly": readOnly || undefined, "aria-valuemax": count, "aria-valuemin": 0, "aria-valuenow": committedValue, "aria-valuetext": getValueLabel(committedValue, count), className: ['inkra-rating', className].filter(Boolean).join(' '), "data-disabled": disabled || undefined, "data-previewing": previewValue !== null || undefined, "data-readonly": readOnly || undefined, "data-size": size, dir: dir, onBlur: (event) => {
114
- setPreviewValue(null);
210
+ return (_jsxs("div", { ...props, "aria-disabled": disabled || undefined, "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Rating'), "aria-labelledby": ariaLabelledBy, "aria-readonly": readOnly || undefined, "aria-valuemax": count, "aria-valuemin": 0, "aria-valuenow": committedValue, "aria-valuetext": getValueLabel(committedValue, count), className: ['inkra-rating', className].filter(Boolean).join(' '), "data-disabled": disabled || undefined, "data-previewing": previewing || undefined, "data-readonly": readOnly || undefined, "data-size": size, dir: dir, onBlur: (event) => {
211
+ clearPendingKeyboard();
212
+ changePreview(null);
115
213
  onBlur?.(event);
116
- }, onKeyDown: handleKeyDown, onPointerLeave: (event) => {
117
- setPreviewValue(null);
214
+ }, onKeyDown: handleKeyDown, onPointerDown: (event) => {
215
+ onPointerDown?.(event);
216
+ if (!event.defaultPrevented && !disabled) {
217
+ clearPendingKeyboard();
218
+ rootRef.current?.focus({ preventScroll: true });
219
+ }
220
+ }, onPointerLeave: (event) => {
221
+ changePreview(null);
118
222
  onPointerLeave?.(event);
119
- }, ref: forwardedRef, role: "slider", tabIndex: disabled ? undefined : (props.tabIndex ?? 0), children: _jsx("span", { className: "inkra-rating__symbols", "aria-hidden": "true", children: symbolIndexes.map((index) => {
120
- const fraction = Math.min(1, Math.max(0, displayValue - index));
121
- return (_jsxs("span", { className: "inkra-rating__symbol-target", "data-index": index + 1, onClick: (event) => commit(valueFromPointer(event, index)), onPointerMove: (event) => {
122
- if (event.pointerType === 'mouse' && !disabled && !readOnly) {
123
- setPreviewValue(valueFromPointer(event, index));
124
- }
125
- }, children: [_jsx("span", { className: "inkra-rating__symbol inkra-rating__symbol--empty", children: renderLayerSymbol(index, 0, false) }), _jsx("span", { className: "inkra-rating__fill", style: { inlineSize: `${fraction * 100}%` }, children: _jsx("span", { className: "inkra-rating__symbol inkra-rating__symbol--filled", children: renderLayerSymbol(index, fraction, true) }) })] }, index));
126
- }) }) }));
223
+ }, ref: rootRef, role: "slider", tabIndex: disabled ? undefined : (props.tabIndex ?? 0), children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-rating__focus-ring", variant: "outline", seed: `${symbolId}-focus` }), _jsx("input", { ref: inputRef, type: "hidden", name: name, form: form, value: committedValue, disabled: disabled }), _jsx("span", { className: "inkra-rating__symbols", "aria-hidden": "true", ref: symbolsRef, children: symbolIndexes.map((index) => {
224
+ const fraction = Math.min(1, Math.max(0, displayValue - index));
225
+ return (_jsxs("span", { className: "inkra-rating__symbol-target", "data-index": index + 1, onClick: (event) => commit(valueFromPointer(event, index)), onPointerMove: (event) => {
226
+ if (hoverPreview && event.pointerType === 'mouse' && !disabled && !readOnly) {
227
+ changePreview(valueFromPointer(event, index));
228
+ }
229
+ }, children: [_jsx("span", { className: "inkra-rating__symbol inkra-rating__symbol--empty", children: renderLayerSymbol(index, 0, false) }), _jsx("span", { className: "inkra-rating__fill", style: { inlineSize: `${fraction * 100}%` }, children: _jsx("span", { className: "inkra-rating__symbol inkra-rating__symbol--filled", children: renderLayerSymbol(index, fraction, true) }) })] }, index));
230
+ }) })] }));
127
231
  });
@@ -42,7 +42,7 @@ function ResizablePanel({ className, ...props }) {
42
42
  function ResizableHandle({ 'aria-label': ariaLabel, children, className, label = 'Resize panels', lineSeed = 'inkra-resizable-handle', ...props }) {
43
43
  return _jsxs(ResizableSeparatorPrimitive, { ...props, "aria-label": ariaLabel ?? label, className: classes('inkra-resizable__handle', className), children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-resizable__handle-focus", seed: `${lineSeed}-focus`, variant: "outline" }), _jsxs("span", { "aria-hidden": "true", className: "inkra-resizable__handle-mark", children: [_jsx(PencilLine, { className: "inkra-resizable__handle-line inkra-resizable__handle-line--horizontal", seed: `${lineSeed}-horizontal` }), _jsx(PencilLine, { className: "inkra-resizable__handle-line inkra-resizable__handle-line--vertical", orientation: "vertical", seed: `${lineSeed}-vertical` }), children] })] });
44
44
  }
45
- export const Resizable = Object.freeze({
45
+ export const Resizable = /* @__PURE__ */ Object.freeze({
46
46
  Root: ResizableRoot,
47
47
  Panel: ResizablePanel,
48
48
  Handle: ResizableHandle,
@@ -13,7 +13,7 @@ export interface ScrollAreaProps extends Omit<HTMLAttributes<HTMLDivElement>, 'c
13
13
  viewportLabel?: string;
14
14
  viewportProps?: Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'role'>;
15
15
  }
16
- export type ScrollAreaRootProps = PrimitiveRootProps;
16
+ export type ScrollAreaRootProps = ScrollAreaProps;
17
17
  export type ScrollAreaViewportProps = PrimitiveViewportProps;
18
18
  export type ScrollAreaScrollbarProps = PrimitiveScrollbarProps;
19
19
  export type ScrollAreaThumbProps = PrimitiveThumbProps;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { DirectionProvider, useDirection } from '@base-ui/react/direction-provider';
2
3
  import { ScrollArea as ScrollAreaPrimitive } from '@base-ui/react/scroll-area';
3
4
  import { forwardRef, useId } from 'react';
4
5
  import { PencilSurface } from '../PencilSurface.js';
@@ -10,13 +11,15 @@ function withClassName(base, value) {
10
11
  ? (state) => classes(base, value(state))
11
12
  : classes(base, value);
12
13
  }
13
- const ScrollAreaRoot = forwardRef(function ScrollAreaRoot({ 'aria-label': ariaLabel, children, className, overflowEdgeThreshold, seed, style, type = 'auto', viewportLabel, viewportProps, ...props }, ref) {
14
+ const ScrollAreaRoot = forwardRef(function ScrollAreaRoot({ 'aria-label': ariaLabel, children, className, dir, overflowEdgeThreshold, seed, style, type = 'auto', viewportLabel, viewportProps, ...props }, ref) {
15
+ const inheritedDirection = useDirection();
16
+ const resolvedDirection = dir === 'rtl' || dir === 'ltr' ? dir : inheritedDirection;
14
17
  const generatedId = useId();
15
18
  const resolvedSeed = seed ?? `inkra-scroll-area-${generatedId}`;
16
19
  const viewportClassName = classes('inkra-scroll-area__viewport', viewportProps?.className);
17
20
  const viewportStyle = viewportProps?.style;
18
21
  const viewportLabelValue = viewportProps?.['aria-label'] ?? viewportLabel ?? ariaLabel;
19
- return _jsxs(ScrollAreaPrimitive.Root, { ...props, ...(ariaLabel ? { 'aria-label': ariaLabel } : {}), className: classes('inkra-scroll-area', className), "data-type": type, overflowEdgeThreshold: overflowEdgeThreshold, ref: ref, style: style, children: [_jsx(ScrollAreaPrimitive.Viewport, { ...viewportProps, "aria-label": viewportLabelValue, className: viewportClassName, style: viewportStyle, children: children }), _jsxs(ScrollAreaPrimitive.Scrollbar, { className: "inkra-scroll-area__scrollbar", orientation: "vertical", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-scroll-area__track", radius: 3, seed: `${resolvedSeed}-vertical-track`, variant: "quiet" }), _jsx(ScrollAreaPrimitive.Thumb, { className: "inkra-scroll-area__thumb", children: _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-scroll-area__thumb-surface", radius: 3, seed: `${resolvedSeed}-vertical-thumb`, variant: "outline" }) })] }), _jsxs(ScrollAreaPrimitive.Scrollbar, { className: "inkra-scroll-area__scrollbar", orientation: "horizontal", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-scroll-area__track", radius: 3, seed: `${resolvedSeed}-horizontal-track`, variant: "quiet" }), _jsx(ScrollAreaPrimitive.Thumb, { className: "inkra-scroll-area__thumb", children: _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-scroll-area__thumb-surface", radius: 3, seed: `${resolvedSeed}-horizontal-thumb`, variant: "outline" }) })] }), _jsx(ScrollAreaPrimitive.Corner, { className: "inkra-scroll-area__corner" })] });
22
+ return _jsx(DirectionProvider, { direction: resolvedDirection, children: _jsxs(ScrollAreaPrimitive.Root, { ...props, ...(ariaLabel ? { 'aria-label': ariaLabel } : {}), className: classes('inkra-scroll-area', className), "data-type": type, dir: dir, overflowEdgeThreshold: overflowEdgeThreshold, ref: ref, style: style, children: [_jsx(ScrollAreaPrimitive.Viewport, { ...viewportProps, "aria-label": viewportLabelValue, className: viewportClassName, style: viewportStyle, children: children }), _jsxs(ScrollAreaPrimitive.Scrollbar, { className: "inkra-scroll-area__scrollbar", orientation: "vertical", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-scroll-area__track", radius: 3, seed: `${resolvedSeed}-vertical-track`, variant: "quiet" }), _jsx(ScrollAreaPrimitive.Thumb, { className: "inkra-scroll-area__thumb", children: _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-scroll-area__thumb-surface", radius: 3, seed: `${resolvedSeed}-vertical-thumb`, variant: "quiet" }) })] }), _jsxs(ScrollAreaPrimitive.Scrollbar, { className: "inkra-scroll-area__scrollbar", orientation: "horizontal", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-scroll-area__track", radius: 3, seed: `${resolvedSeed}-horizontal-track`, variant: "quiet" }), _jsx(ScrollAreaPrimitive.Thumb, { className: "inkra-scroll-area__thumb", children: _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-scroll-area__thumb-surface", radius: 3, seed: `${resolvedSeed}-horizontal-thumb`, variant: "quiet" }) })] }), _jsx(ScrollAreaPrimitive.Corner, { className: "inkra-scroll-area__corner" })] }) });
20
23
  });
21
24
  const Viewport = forwardRef(function Viewport({ className, ...props }, ref) {
22
25
  return _jsx(ScrollAreaPrimitive.Viewport, { ...props, className: withClassName('inkra-scroll-area__viewport', className), ref: ref });
@@ -15,13 +15,14 @@ export function MultipleSelectRoot({ value, defaultValue = emptyValues, onValueC
15
15
  const isControlled = value !== undefined;
16
16
  const contentEvents = useRef({});
17
17
  const labels = useMemo(() => new Map(items?.map(item => [item.value, item.label])), [items]);
18
- const change = (next) => {
18
+ const change = useCallback((next) => {
19
19
  if (disabled || readOnly)
20
20
  return;
21
- if (value === undefined)
21
+ if (!isControlled)
22
22
  setUncontrolled(next);
23
23
  onValueChange?.(next);
24
- };
24
+ }, [disabled, readOnly, isControlled, onValueChange]);
25
+ const context = useMemo(() => ({ values, labels, disabled, readOnly, change, triggerRef, contentEvents }), [values, labels, disabled, readOnly, change]);
25
26
  useEffect(() => {
26
27
  if (isControlled)
27
28
  return;
@@ -37,7 +38,7 @@ export function MultipleSelectRoot({ value, defaultValue = emptyValues, onValueC
37
38
  form?.addEventListener('reset', reset);
38
39
  return () => { clearTimeout(timer); form?.removeEventListener('reset', reset); };
39
40
  }, [isControlled, props.form]);
40
- return (_jsx(MultipleSelectContext.Provider, { value: { values, labels, disabled, readOnly, change, triggerRef, contentEvents }, children: _jsx(DirectionProvider, { direction: dir, children: _jsx(BaseSelect.Root, { ...props, multiple: true, disabled: disabled, readOnly: readOnly, open: readOnly ? false : props.open, value: values, onValueChange: change, items: items, inputRef: inputRef, onOpenChange: (open, details) => {
41
+ return (_jsx(MultipleSelectContext.Provider, { value: context, children: _jsx(DirectionProvider, { direction: dir, children: _jsx(BaseSelect.Root, { ...props, multiple: true, disabled: disabled, readOnly: readOnly, open: readOnly ? false : props.open, value: values, onValueChange: change, items: items, inputRef: inputRef, onOpenChange: (open, details) => {
41
42
  if (readOnly && open)
42
43
  details.cancel();
43
44
  if (details.reason === 'outside-press' || details.reason === 'cancel-open') {