@inkra/react 0.3.0 → 0.5.0

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 (245) hide show
  1. package/README.md +113 -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 +131 -34
  11. package/dist/breadcrumb/index.js +4 -1
  12. package/dist/calendar/index.d.ts +4 -1
  13. package/dist/calendar/index.js +37 -12
  14. package/dist/calendar/locales.d.ts +7 -0
  15. package/dist/calendar/locales.js +2 -0
  16. package/dist/carousel/index.js +20 -10
  17. package/dist/choice-grid/index.d.ts +6 -1
  18. package/dist/choice-grid/index.js +63 -33
  19. package/dist/color-picker/GradientEditor.d.ts +40 -0
  20. package/dist/color-picker/GradientEditor.js +64 -0
  21. package/dist/color-picker/gradient.d.ts +25 -0
  22. package/dist/color-picker/gradient.js +60 -0
  23. package/dist/color-picker/index.d.ts +27 -7
  24. package/dist/color-picker/index.js +144 -40
  25. package/dist/color-swatch/index.d.ts +10 -0
  26. package/dist/color-swatch/index.js +9 -0
  27. package/dist/command/index.js +27 -4
  28. package/dist/content/index.d.ts +5 -0
  29. package/dist/content/index.js +15 -3
  30. package/dist/context-menu/index.js +1 -1
  31. package/dist/data-table/index.d.ts +7 -1
  32. package/dist/data-table/index.js +67 -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 +20 -21
  37. package/dist/day-agenda/index.d.ts +6 -1
  38. package/dist/day-agenda/index.js +49 -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 +15 -13
  44. package/dist/drawer/index.js +11 -5
  45. package/dist/editable/index.d.ts +24 -2
  46. package/dist/editable/index.js +148 -40
  47. package/dist/empty/illustrations.d.ts +8 -0
  48. package/dist/empty/illustrations.js +78 -0
  49. package/dist/empty/index.d.ts +6 -1
  50. package/dist/empty/index.js +11 -3
  51. package/dist/flip/index.js +28 -8
  52. package/dist/form/Field.js +11 -10
  53. package/dist/form/Input.js +4 -1
  54. package/dist/form/Textarea.d.ts +3 -1
  55. package/dist/form/Textarea.js +9 -5
  56. package/dist/heatmap/index.js +4 -1
  57. package/dist/hotkey/index.d.ts +1 -0
  58. package/dist/hotkey/index.js +9 -4
  59. package/dist/hover-card/index.js +1 -1
  60. package/dist/i18n/catalog.d.ts +5 -0
  61. package/dist/i18n/catalog.js +289 -0
  62. package/dist/i18n/index.d.ts +23 -0
  63. package/dist/i18n/index.js +50 -0
  64. package/dist/image-cropper/index.js +21 -17
  65. package/dist/image-viewer/VideoResource.d.ts +21 -0
  66. package/dist/image-viewer/VideoResource.js +196 -0
  67. package/dist/image-viewer/index.d.ts +14 -0
  68. package/dist/image-viewer/index.js +141 -86
  69. package/dist/image-viewer/lucideGeometry.d.ts +3 -0
  70. package/dist/image-viewer/lucideGeometry.js +19 -0
  71. package/dist/index.d.ts +4 -0
  72. package/dist/index.js +4 -0
  73. package/dist/input-number/index.js +13 -5
  74. package/dist/internal/cache.d.ts +4 -0
  75. package/dist/internal/cache.js +18 -1
  76. package/dist/internal/iconGeometry.d.ts +2 -0
  77. package/dist/internal/iconGeometry.js +32 -0
  78. package/dist/internal/renderTokens.d.ts +4 -0
  79. package/dist/internal/renderTokens.js +4 -0
  80. package/dist/internal/stroke.d.ts +7 -0
  81. package/dist/internal/stroke.js +63 -7
  82. package/dist/internal/surfaceBudget.d.ts +20 -0
  83. package/dist/internal/surfaceBudget.js +69 -0
  84. package/dist/internal/surfaceRenderer.js +87 -28
  85. package/dist/internal/surfaceVisibility.d.ts +3 -0
  86. package/dist/internal/surfaceVisibility.js +60 -0
  87. package/dist/internal/textRenderer.d.ts +3 -0
  88. package/dist/internal/textRenderer.js +13 -5
  89. package/dist/internal/usePencilSurface.js +17 -1
  90. package/dist/internal/wash.js +4 -3
  91. package/dist/journey-map/index.d.ts +1 -1
  92. package/dist/journey-map/index.js +6 -3
  93. package/dist/journey-map/terrain.js +10 -5
  94. package/dist/keypad/index.js +4 -1
  95. package/dist/layout/index.js +1 -1
  96. package/dist/log/index.js +42 -33
  97. package/dist/markdown/CodeBlock.d.ts +19 -0
  98. package/dist/markdown/CodeBlock.js +86 -0
  99. package/dist/markdown/Deletion.d.ts +2 -0
  100. package/dist/markdown/Deletion.js +69 -0
  101. package/dist/markdown/icons.d.ts +14 -0
  102. package/dist/markdown/icons.js +16 -0
  103. package/dist/markdown/index.d.ts +17 -0
  104. package/dist/markdown/index.js +177 -0
  105. package/dist/node-graph/index.d.ts +3 -1
  106. package/dist/node-graph/index.js +63 -28
  107. package/dist/number-ticker/index.js +4 -1
  108. package/dist/number-ticker/precision.js +9 -2
  109. package/dist/overflow-list/index.js +9 -6
  110. package/dist/overlay/Overlay.js +8 -3
  111. package/dist/overlay/Popover.js +3 -3
  112. package/dist/overlay/themeBridge.js +14 -2
  113. package/dist/pagination/index.d.ts +10 -1
  114. package/dist/pagination/index.js +13 -9
  115. package/dist/pan-zoom/index.d.ts +2 -0
  116. package/dist/pan-zoom/index.js +12 -8
  117. package/dist/pencil.d.ts +1 -0
  118. package/dist/pencil.js +4 -4
  119. package/dist/radar/index.d.ts +2 -0
  120. package/dist/radar/index.js +44 -21
  121. package/dist/range-slider/index.js +4 -3
  122. package/dist/rating/index.d.ts +4 -0
  123. package/dist/rating/index.js +131 -24
  124. package/dist/resizable/index.d.ts +1 -1
  125. package/dist/resizable/index.js +5 -2
  126. package/dist/scroll-area/index.d.ts +1 -1
  127. package/dist/scroll-area/index.js +5 -2
  128. package/dist/select/MultipleSelect.js +5 -4
  129. package/dist/select/SearchableSelect.js +64 -7
  130. package/dist/select/Select.d.ts +1 -0
  131. package/dist/select/Select.js +6 -2
  132. package/dist/select/StepperSelect.d.ts +29 -0
  133. package/dist/select/StepperSelect.js +122 -0
  134. package/dist/select/index.d.ts +2 -0
  135. package/dist/select/index.js +1 -0
  136. package/dist/select/stepperModel.d.ts +7 -0
  137. package/dist/select/stepperModel.js +29 -0
  138. package/dist/selection/Checkbox.js +4 -3
  139. package/dist/selection/RadioGroup.js +29 -13
  140. package/dist/selection/Switch.js +4 -3
  141. package/dist/selection/useNativeChecked.d.ts +2 -1
  142. package/dist/selection/useNativeChecked.js +26 -8
  143. package/dist/slider/index.js +50 -6
  144. package/dist/sortable/index.js +17 -11
  145. package/dist/sparkline/index.js +21 -6
  146. package/dist/sparkline/model.js +11 -4
  147. package/dist/steps/index.js +4 -1
  148. package/dist/styles/accordion.css +228 -0
  149. package/dist/styles/alert-dialog.css +139 -0
  150. package/dist/styles/audio-player.css +22 -0
  151. package/dist/styles/base.css +638 -0
  152. package/dist/styles/breadcrumb.css +7 -0
  153. package/dist/styles/calendar.css +62 -0
  154. package/dist/styles/carousel.css +173 -0
  155. package/dist/styles/choice-grid.css +226 -0
  156. package/dist/styles/color-picker.css +281 -0
  157. package/dist/styles/color-swatch.css +47 -0
  158. package/dist/styles/command.css +52 -0
  159. package/dist/styles/content.css +79 -0
  160. package/dist/styles/context-menu.css +35 -0
  161. package/dist/styles/data-table.css +31 -0
  162. package/dist/styles/date-picker.css +107 -0
  163. package/dist/styles/day-agenda.css +30 -0
  164. package/dist/styles/display.css +304 -0
  165. package/dist/styles/dockable-panel.css +110 -0
  166. package/dist/styles/drawer.css +133 -0
  167. package/dist/styles/dropdown-menu.css +68 -0
  168. package/dist/styles/editable.css +145 -0
  169. package/dist/styles/empty.css +108 -0
  170. package/dist/styles/flip.css +101 -0
  171. package/dist/styles/form.css +634 -0
  172. package/dist/styles/heatmap.css +256 -0
  173. package/dist/styles/hotkey.css +189 -0
  174. package/dist/styles/hover-card.css +27 -0
  175. package/dist/styles/image-cropper.css +205 -0
  176. package/dist/styles/image-viewer.css +422 -0
  177. package/dist/styles/input-number.css +21 -0
  178. package/dist/styles/journey-map.css +19 -0
  179. package/dist/styles/keypad.css +92 -0
  180. package/dist/styles/layout.css +72 -0
  181. package/dist/styles/leaflet.css +661 -0
  182. package/dist/styles/log.css +203 -0
  183. package/dist/styles/markdown.css +379 -0
  184. package/dist/styles/node-graph.css +142 -0
  185. package/dist/styles/number-ticker.css +83 -0
  186. package/dist/styles/overflow-list.css +51 -0
  187. package/dist/styles/overlay.css +284 -0
  188. package/dist/styles/pagination.css +21 -0
  189. package/dist/styles/pan-zoom.css +136 -0
  190. package/dist/styles/radar.css +1 -0
  191. package/dist/styles/rating.css +121 -0
  192. package/dist/styles/resizable.css +108 -0
  193. package/dist/styles/scroll-area.css +122 -0
  194. package/dist/styles/select.css +554 -0
  195. package/dist/styles/selection.css +264 -0
  196. package/dist/styles/slider.css +145 -0
  197. package/dist/styles/slot.css +109 -0
  198. package/dist/styles/sortable.css +159 -0
  199. package/dist/styles/sparkline.css +24 -0
  200. package/dist/styles/steps.css +27 -0
  201. package/dist/styles/tabs.css +79 -0
  202. package/dist/styles/tag-input.css +213 -0
  203. package/dist/styles/time-picker.css +66 -0
  204. package/dist/styles/timeline.css +24 -0
  205. package/dist/styles/toast.css +160 -0
  206. package/dist/styles/toggle-group.css +107 -0
  207. package/dist/styles/tooltip.css +78 -0
  208. package/dist/styles/transfer.css +148 -0
  209. package/dist/styles/tree-select.css +131 -0
  210. package/dist/styles/tree.css +59 -0
  211. package/dist/styles/typewriter.css +62 -0
  212. package/dist/styles/upload.css +60 -0
  213. package/dist/styles/virtual-list.css +112 -0
  214. package/dist/styles.css +2091 -421
  215. package/dist/tabs/index.d.ts +9 -0
  216. package/dist/tabs/index.js +69 -16
  217. package/dist/tabs/useOverflow.d.ts +20 -0
  218. package/dist/tabs/useOverflow.js +145 -0
  219. package/dist/tag-input/index.d.ts +7 -2
  220. package/dist/tag-input/index.js +81 -42
  221. package/dist/theme/index.d.ts +11 -0
  222. package/dist/theme/index.js +23 -0
  223. package/dist/time-picker/index.js +45 -27
  224. package/dist/timeline/index.js +4 -1
  225. package/dist/toast/index.d.ts +19 -11
  226. package/dist/toast/index.js +51 -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.js +3 -3
  233. package/dist/transfer/index.d.ts +5 -0
  234. package/dist/transfer/index.js +120 -53
  235. package/dist/transfer/model.d.ts +4 -0
  236. package/dist/transfer/model.js +37 -11
  237. package/dist/tree/index.d.ts +5 -1
  238. package/dist/tree/index.js +11 -6
  239. package/dist/tree-select/index.js +70 -31
  240. package/dist/tree-select/model.d.ts +11 -5
  241. package/dist/tree-select/model.js +111 -83
  242. package/dist/typewriter/index.js +10 -10
  243. package/dist/upload/index.js +50 -33
  244. package/dist/virtual-list/index.js +11 -6
  245. package/package.json +423 -3
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useInkraLocale } from '../i18n/index.js';
2
3
  import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';
3
- import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useRef, useState, } from 'react';
4
+ import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useRef, } from 'react';
4
5
  import { PencilSurface } from '../PencilSurface.js';
5
6
  const defaultTransform = { x: 0, y: 0, scale: 1 };
6
7
  const interactionExclusions = [
@@ -26,16 +27,20 @@ function transformsEqual(left, right) {
26
27
  function prefersReducedMotion(element) {
27
28
  return element?.ownerDocument.defaultView?.matchMedia('(prefers-reduced-motion: reduce)').matches ?? false;
28
29
  }
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) {
30
+ function PanZoomRoot(inkraProps, forwardedRef) {
31
+ const { t } = useInkraLocale();
32
+ const { 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 = t('panZoom.label'), ...props } = inkraProps;
30
33
  const resolvedMinScale = Math.max(Number.isFinite(minScale) ? minScale : 0.25, 0.0001);
31
34
  const resolvedMaxScale = Math.max(Number.isFinite(maxScale) ? maxScale : 8, resolvedMinScale);
35
+ const resolvedWheelStep = Number.isFinite(wheelStep) && wheelStep > 0 ? wheelStep : 0.001;
32
36
  const controlled = value !== undefined;
33
37
  const initialTransform = normalizedTransform(value ?? defaultValue, resolvedMinScale, resolvedMaxScale);
34
- const [uncontrolledTransform, setUncontrolledTransform] = useState(initialTransform);
35
- const visibleTransform = normalizedTransform(value ?? uncontrolledTransform, resolvedMinScale, resolvedMaxScale);
36
38
  const engineRef = useRef(null);
37
39
  const instructionsId = useId();
40
+ // The engine owns the live DOM transform. Keep its snapshot for imperative reads,
41
+ // rather than rerendering the complete content subtree on every gesture frame.
38
42
  const committedRef = useRef(initialTransform);
43
+ const visibleTransform = normalizedTransform(value ?? committedRef.current, resolvedMinScale, resolvedMaxScale);
39
44
  const programmaticRef = useRef(false);
40
45
  const pointerBlocksPanRef = useRef(false);
41
46
  const wheelTargetBlocksZoomRef = useRef(false);
@@ -120,8 +125,8 @@ function PanZoomRoot({ children, value, defaultValue = defaultTransform, onTrans
120
125
  ? (keys) => !wheelTargetBlocksZoomRef.current && (keys.includes('Control') || keys.includes('Meta'))
121
126
  : () => !wheelTargetBlocksZoomRef.current;
122
127
  const defaultInstructions = wheel === 'modifier'
123
- ? 'Use Control or Command with the mouse wheel to zoom. Use arrow keys to pan, plus or minus to zoom, and 0 to reset.'
124
- : 'Use arrow keys to pan, plus or minus to zoom, and 0 to reset.';
128
+ ? t('panZoom.modifierInstructions')
129
+ : t('panZoom.instructions');
125
130
  const wheelHint = instructions ?? defaultInstructions;
126
131
  const mergedViewportLabel = viewportProps?.['aria-label'] ?? ariaLabel;
127
132
  const describedBy = [viewportProps?.['aria-describedby'], keyboard && !disabled ? instructionsId : null]
@@ -132,6 +137,7 @@ function PanZoomRoot({ children, value, defaultValue = defaultTransform, onTrans
132
137
  excluded: interactionExclusions,
133
138
  activationKeys: () => !pointerBlocksPanRef.current,
134
139
  }, pinch: { disabled: userInteractionDisabled || !pinch, excluded: interactionExclusions }, wheel: {
140
+ step: resolvedWheelStep,
135
141
  disabled: userInteractionDisabled || wheel === 'disabled',
136
142
  activationKeys: wheelActivation,
137
143
  excluded: interactionExclusions,
@@ -144,8 +150,6 @@ function PanZoomRoot({ children, value, defaultValue = defaultTransform, onTrans
144
150
  }, 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
151
  const next = normalizedTransform({ x: state.positionX, y: state.positionY, scale: state.scale }, resolvedMinScale, resolvedMaxScale);
146
152
  committedRef.current = next;
147
- if (!controlled)
148
- setUncontrolledTransform(next);
149
153
  onTransformChange?.(next, { reason: programmaticRef.current ? 'programmatic' : 'gesture' });
150
154
  }, 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
155
  ...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;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useInkraLocale } from '../i18n/index.js';
2
3
  import { scaleLinear } from 'd3-scale';
3
4
  import { curveLinearClosed, lineRadial } from 'd3-shape';
4
5
  import { forwardRef, useEffect, useId, useMemo, useRef, useState } from 'react';
@@ -87,10 +88,13 @@ function RadarInk({ axisAngles, axes, levels, radius, center, seriesPoints, }) {
87
88
  const valid = points.filter(point => point.value != null).map(point => coordinate(point.angle, point.radius));
88
89
  if (valid.length < 2)
89
90
  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]));
91
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
92
+ for (const [x, y] of valid) {
93
+ minX = Math.min(minX, x);
94
+ maxX = Math.max(maxX, x);
95
+ minY = Math.min(minY, y);
96
+ maxY = Math.max(maxY, y);
97
+ }
94
98
  context.save();
95
99
  context.beginPath();
96
100
  valid.forEach(([x, y], pointIndex) => pointIndex ? context.lineTo(x, y) : context.moveTo(x, y));
@@ -139,6 +143,7 @@ function RadarInk({ axisAngles, axes, levels, radius, center, seriesPoints, }) {
139
143
  }, [axes, axisAngles, center, levels, radius, seriesPoints]);
140
144
  return _jsx("canvas", { "aria-hidden": "true", className: "inkra-radar__ink", "data-ready": ready || undefined, ref: canvasRef });
141
145
  }
146
+ const noAxes = [];
142
147
  const palette = ['#4b96aa', '#994f3f', '#629568', '#8d6b9c', '#c58b47'];
143
148
  function keyPart(value) { return JSON.stringify([typeof value, value]); }
144
149
  function sameKey(a, b) { return a != null && b != null && a === b; }
@@ -156,8 +161,14 @@ function textNode(value, seed) {
156
161
  return typeof value === 'string' || typeof value === 'number' ? _jsx(PencilText, { seed: seed, children: String(value) }) : value;
157
162
  }
158
163
  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;
164
+ let minimum = values.length ? values[0] : 0;
165
+ let maximum = values.length ? values[0] : 1;
166
+ for (let index = 1; index < values.length; index++) {
167
+ minimum = Math.min(minimum, values[index]);
168
+ maximum = Math.max(maximum, values[index]);
169
+ }
170
+ let min = finite(axis.min) ? axis.min : minimum;
171
+ let max = finite(axis.max) ? axis.max : maximum;
161
172
  if (min > max)
162
173
  [min, max] = [max, min];
163
174
  if (min === max) {
@@ -169,13 +180,15 @@ function domainForAxis(axis, values) {
169
180
  function paintStyle(color) {
170
181
  return { '--inkra-accent': color, '--inkra-accent-soft': color, '--inkra-accent-strong': color };
171
182
  }
172
- export const Radar = forwardRef(function Radar({ axes, series, label, description, size = 'min(100%, 34rem)', maxRadius, levels = 4, showAxes = true, showLabels = true, showLegend = true, showValues = true, showTable = true, legendLabel = 'Series', tableCaption = 'Radar data', seriesLabel = 'Series', missingLabel = 'Missing', emptyMessage = 'No radar data', selectedSeries, defaultSelectedSeries = null, onSelectedSeriesChange, renderAxisLabel, renderValueLabel, getValueLabel, className, style, ...props }, forwardedRef) {
183
+ export const Radar = forwardRef(function Radar(inkraProps, forwardedRef) {
184
+ const { t } = useInkraLocale();
185
+ const { axes, series, label, description, size = 'min(100%, 34rem)', maxRadius, levels = 4, showAxes = true, showLabels = true, showLegend = true, showValues = true, showTable = true, legendLabel = t('radar.series'), tableCaption = t('radar.caption'), seriesLabel = t('radar.series'), missingLabel = t('common.missing'), emptyMessage = t('radar.empty'), selectedSeries, defaultSelectedSeries = null, onSelectedSeriesChange, renderAxisLabel, renderValueLabel, getValueLabel, className, style, ...props } = inkraProps;
173
186
  const id = useId();
174
187
  const labelId = `inkra-radar-${id}-label`;
175
188
  const descriptionId = description == null ? undefined : `inkra-radar-${id}-description`;
176
189
  const [internalSelected, setInternalSelected] = useState(defaultSelectedSeries);
177
190
  const selected = selectedSeries === undefined ? internalSelected : selectedSeries ?? null;
178
- const visibleSeries = series.filter(item => !item.hidden);
191
+ const visibleSeries = useMemo(() => series.filter(item => !item.hidden), [series]);
179
192
  const domains = useMemo(() => new Map(axes.map(axis => {
180
193
  const values = visibleSeries.flatMap(item => item.data.filter(point => point.axisId === axis.id && finite(point.value)).map(point => point.value));
181
194
  return [keyPart(axis.id), domainForAxis(axis, values)];
@@ -183,15 +196,27 @@ export const Radar = forwardRef(function Radar({ axes, series, label, descriptio
183
196
  const side = 1000;
184
197
  const radius = Math.max(120, Math.min(maxRadius ?? 390, 470));
185
198
  const axisCount = axes.length;
186
- const angles = axes.map((_, index) => (index * Math.PI * 2) / Math.max(1, axisCount));
199
+ const angles = useMemo(() => axes.map((_, index) => (index * Math.PI * 2) / Math.max(1, axisCount)), [axes, axisCount]);
187
200
  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
- });
201
+ const scales = useMemo(() => axes.map(axis => scaleLinear()
202
+ .domain(domains.get(keyPart(axis.id)) ?? [0, 1]).range([0, radius]).clamp(true)), [axes, domains, radius]);
203
+ const seriesPoints = useMemo(() => visibleSeries.map((item, index) => {
204
+ // Preserve the first point for duplicate axis ids, matching Array.find.
205
+ const pointsByAxis = new Map();
206
+ for (const point of item.data) {
207
+ // Map uses SameValueZero, while the prior lookup used strict equality.
208
+ if (typeof point.axisId === 'number' && Number.isNaN(point.axisId))
209
+ continue;
210
+ if (!pointsByAxis.has(point.axisId))
211
+ pointsByAxis.set(point.axisId, point);
212
+ }
213
+ const points = axes.map((axis, axisIndex) => {
214
+ const point = pointsByAxis.get(axis.id) ?? { axisId: axis.id, value: null };
215
+ const value = finite(point.value) ? point.value : null;
216
+ return { angle: angles[axisIndex], radius: value == null ? 0 : scales[axisIndex](value), value, point, axis };
217
+ });
218
+ return { item, points, color: item.color ?? palette[index % palette.length], index };
219
+ }), [angles, axes, scales, visibleSeries]);
195
220
  const line = useMemo(() => lineRadial().angle(point => point.angle).radius(point => point.radius).curve(curveLinearClosed), []);
196
221
  const rootStyle = { ...style, '--inkra-radar-size': cssSize(size, 'min(100%, 34rem)'), '--inkra-radar-levels': Math.max(2, Math.min(8, Math.round(levels))) };
197
222
  const selectSeries = (item) => {
@@ -205,16 +230,14 @@ export const Radar = forwardRef(function Radar({ axes, series, label, descriptio
205
230
  return renderValueLabel(context);
206
231
  return context.missing ? missingLabel : showValues ? formatNumber(context.value, context.axis, getValueLabel) : '•';
207
232
  };
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) => {
233
+ 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
234
  const x = 50 + Math.sin(angles[index]) * 48;
210
235
  const y = 50 - Math.cos(angles[index]) * 48;
211
236
  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];
237
+ }) }) : null] }), _jsx("div", { "aria-describedby": descriptionId, "aria-labelledby": labelId, className: "inkra-radar__summary", role: "group", children: seriesPoints.map(({ item, points, color }) => {
215
238
  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
239
  const context = { axis: point.axis, series: item, point: point.point, value: point.value, missing: point.value == null };
217
240
  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
241
  }) })] }, 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] })] });
242
+ }) }), 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
243
  });
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef } from 'react';
3
+ import { useInkraLocale } from '../i18n/index.js';
3
4
  import { Slider } from '../slider/index.js';
4
- const DEFAULT_LABELS = ['Minimum value', 'Maximum value'];
5
5
  function asRangeValue(value) {
6
6
  return [value[0], value[1]];
7
7
  }
@@ -15,8 +15,9 @@ function getMinimumSteps(minDistance, step) {
15
15
  return 0;
16
16
  return Math.ceil(minDistance / step);
17
17
  }
18
- export const RangeSlider = forwardRef(function RangeSlider({ allowCross = false, defaultValue, max = 100, min = 0, minDistance = 0, onValueChange, onValueCommit, step = 1, thumbLabels = DEFAULT_LABELS, value, ...props }, forwardedRef) {
18
+ export const RangeSlider = forwardRef(function RangeSlider({ allowCross = false, defaultValue, max = 100, min = 0, minDistance = 0, onValueChange, onValueCommit, step = 1, thumbLabels, value, ...props }, forwardedRef) {
19
+ const { t } = useInkraLocale();
19
20
  const controlledValue = value === undefined ? undefined : normalizeRangeValue(value);
20
21
  const uncontrolledDefault = value === undefined ? normalizeRangeValue(defaultValue ?? [min, max]) : undefined;
21
- return _jsx(Slider, { ...props, defaultValue: uncontrolledDefault, max: max, min: min, minStepsBetweenThumbs: allowCross ? 0 : getMinimumSteps(minDistance, step), onValueChange: next => onValueChange?.(asRangeValue(next)), onValueCommit: next => onValueCommit?.(asRangeValue(next)), preserveThumbOrder: !allowCross, ref: forwardedRef, step: step, thumbLabels: thumbLabels, value: controlledValue });
22
+ return _jsx(Slider, { ...props, defaultValue: uncontrolledDefault, max: max, min: min, minStepsBetweenThumbs: allowCross ? 0 : getMinimumSteps(minDistance, step), onValueChange: next => onValueChange?.(asRangeValue(next)), onValueCommit: next => onValueCommit?.(asRangeValue(next)), preserveThumbOrder: !allowCross, ref: forwardedRef, step: step, thumbLabels: thumbLabels ?? [t('rangeSlider.minimum'), t('rangeSlider.maximum')], value: controlledValue });
22
23
  });
@@ -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,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef, useEffect, useId, useMemo, useState, } from 'react';
2
+ import { useInkraLocale } from '../i18n/index.js';
3
+ import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useMemo, useRef, useState, } from 'react';
3
4
  import { PencilIcon } from '../PencilIcon.js';
5
+ import { PencilSurface } from '../PencilSurface.js';
4
6
  const defaultSymbolPaths = [
5
7
  { d: 'M12 2.5 L21.5 12 L12 21.5 L2.5 12 Z' },
6
8
  { d: 'M7.5 12 L12 7.5 L16.5 12 L12 16.5 Z', mode: 'scuff' },
@@ -16,6 +18,10 @@ function decimalPlaces(value) {
16
18
  }
17
19
  export function normalizeRatingValue(value, count, step) {
18
20
  const numeric = typeof value === 'number' && Number.isFinite(value) ? value : 0;
21
+ if (numeric <= 0)
22
+ return 0;
23
+ if (numeric >= count)
24
+ return count;
19
25
  const precision = Math.min(8, Math.max(decimalPlaces(step), decimalPlaces(count)));
20
26
  const snapped = Math.round(numeric / step) * step;
21
27
  return Number(Math.min(count, Math.max(0, snapped)).toFixed(precision));
@@ -26,28 +32,88 @@ function allowsReselectClear(clearable) {
26
32
  function allowsKeyboardClear(clearable) {
27
33
  return clearable === true || clearable === 'keyboard';
28
34
  }
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) {
35
+ function previousRatingValue(value, count, step) {
36
+ if (value >= count) {
37
+ const precision = Math.min(8, Math.max(decimalPlaces(step), decimalPlaces(count)));
38
+ const previousIndex = Math.max(0, Math.ceil(count / step) - 1);
39
+ return Number((previousIndex * step).toFixed(precision));
40
+ }
41
+ return value - step;
42
+ }
43
+ 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: valueLabelProp, className = '', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, onBlur, onKeyDown, onPointerDown, onPointerLeave, ...props }, forwardedRef) {
44
+ const { t } = useInkraLocale();
45
+ const getValueLabel = valueLabelProp ?? ((value, count) => t('rating.value', { value, count }));
30
46
  const count = Math.max(1, Math.floor(finitePositive(countProp, 5)));
31
47
  const step = Math.min(count, finitePositive(stepProp, 1));
32
48
  const controlled = value !== undefined;
33
49
  const [uncontrolledValue, setUncontrolledValue] = useState(() => normalizeRatingValue(defaultValue, count, step));
34
50
  const committedValue = normalizeRatingValue(controlled ? value : uncontrolledValue, count, step);
35
51
  const [previewValue, setPreviewValue] = useState(null);
36
- const displayValue = previewValue ?? committedValue;
52
+ const previewRef = useRef(null);
53
+ const previewCallback = useRef(onPreviewChange);
54
+ previewCallback.current = onPreviewChange;
55
+ const resetState = useRef({ controlled, defaultValue, count, step });
56
+ resetState.current = { controlled, defaultValue, count, step };
57
+ const inputRef = useRef(null);
58
+ const rootRef = useRef(null);
59
+ const symbolsRef = useRef(null);
60
+ const pendingKeyboardValueRef = useRef(null);
61
+ const pendingKeyboardTimerRef = useRef(undefined);
62
+ useImperativeHandle(forwardedRef, () => rootRef.current);
63
+ const clearPendingKeyboard = useCallback(() => {
64
+ clearTimeout(pendingKeyboardTimerRef.current);
65
+ pendingKeyboardTimerRef.current = undefined;
66
+ pendingKeyboardValueRef.current = null;
67
+ }, []);
68
+ const changePreview = useCallback((next) => {
69
+ if (previewRef.current === next)
70
+ return;
71
+ previewRef.current = next;
72
+ setPreviewValue(next);
73
+ previewCallback.current?.(next);
74
+ }, []);
75
+ const previewing = hoverPreview && !disabled && !readOnly && previewValue !== null;
76
+ const displayValue = previewing ? normalizeRatingValue(previewValue, count, step) : committedValue;
37
77
  const symbolId = useId();
38
78
  const symbolIndexes = useMemo(() => Array.from({ length: count }, (_, index) => index), [count]);
39
79
  const defaultIconSize = size === 'sm' ? 26 : size === 'lg' ? 38 : 32;
40
80
  useEffect(() => {
41
- if (disabled || readOnly)
42
- setPreviewValue(null);
43
- }, [disabled, readOnly]);
81
+ if (disabled || readOnly || !hoverPreview)
82
+ changePreview(null);
83
+ }, [disabled, readOnly, hoverPreview, changePreview]);
84
+ useEffect(() => {
85
+ clearPendingKeyboard();
86
+ changePreview(null);
87
+ }, [count, step, changePreview, clearPendingKeyboard]);
88
+ useEffect(() => clearPendingKeyboard, [clearPendingKeyboard]);
89
+ useEffect(() => {
90
+ const owner = inputRef.current?.form;
91
+ if (!owner)
92
+ return;
93
+ let resetTimer;
94
+ const reset = (event) => {
95
+ clearTimeout(resetTimer);
96
+ resetTimer = setTimeout(() => {
97
+ if (event.defaultPrevented)
98
+ return;
99
+ clearPendingKeyboard();
100
+ changePreview(null);
101
+ const state = resetState.current;
102
+ if (!state.controlled)
103
+ setUncontrolledValue(normalizeRatingValue(state.defaultValue, state.count, state.step));
104
+ }, 0);
105
+ };
106
+ owner.addEventListener('reset', reset);
107
+ return () => { clearTimeout(resetTimer); owner.removeEventListener('reset', reset); };
108
+ }, [form, changePreview, clearPendingKeyboard]);
44
109
  const commit = (nextValue) => {
45
110
  if (disabled || readOnly)
46
111
  return;
112
+ clearPendingKeyboard();
47
113
  const normalized = normalizeRatingValue(nextValue, count, step);
48
114
  const cleared = normalized === committedValue && normalized !== 0 && allowsReselectClear(clearable);
49
115
  const resolved = cleared ? 0 : normalized;
50
- setPreviewValue(null);
116
+ changePreview(null);
51
117
  if (resolved === committedValue)
52
118
  return;
53
119
  if (!controlled)
@@ -63,6 +129,30 @@ export const Rating = forwardRef(function Rating({ value, defaultValue = 0, onVa
63
129
  const rawValue = index + Math.min(1, Math.max(0, logicalFraction));
64
130
  return normalizeRatingValue(Math.max(step, Math.ceil((rawValue - Number.EPSILON) / step) * step), count, step);
65
131
  };
132
+ const revealValue = useCallback((nextValue) => {
133
+ const scroller = symbolsRef.current;
134
+ if (!scroller)
135
+ return;
136
+ const targetIndex = Math.max(1, Math.ceil(normalizeRatingValue(nextValue, count, step)));
137
+ const target = scroller.querySelector(`[data-index="${targetIndex}"]`);
138
+ if (!target)
139
+ return;
140
+ const viewport = scroller.getBoundingClientRect();
141
+ const item = target.getBoundingClientRect();
142
+ const delta = item.left < viewport.left ? item.left - viewport.left
143
+ : item.right > viewport.right ? item.right - viewport.right
144
+ : 0;
145
+ if (delta)
146
+ scroller.scrollLeft += delta;
147
+ }, [count, step]);
148
+ useEffect(() => {
149
+ const pending = pendingKeyboardValueRef.current;
150
+ if (pending === null)
151
+ return;
152
+ clearPendingKeyboard();
153
+ if (pending === committedValue)
154
+ revealValue(committedValue);
155
+ }, [committedValue, revealValue, clearPendingKeyboard]);
66
156
  const handleKeyDown = (event) => {
67
157
  onKeyDown?.(event);
68
158
  if (event.defaultPrevented || disabled || readOnly)
@@ -72,11 +162,15 @@ export const Rating = forwardRef(function Rating({ value, defaultValue = 0, onVa
72
162
  if (event.key === 'ArrowUp')
73
163
  nextValue = committedValue + step;
74
164
  if (event.key === 'ArrowDown')
75
- nextValue = committedValue - step;
165
+ nextValue = previousRatingValue(committedValue, count, step);
76
166
  if (event.key === 'ArrowRight')
77
- nextValue = committedValue + (rtl ? -step : step);
167
+ nextValue = rtl
168
+ ? previousRatingValue(committedValue, count, step)
169
+ : committedValue + step;
78
170
  if (event.key === 'ArrowLeft')
79
- nextValue = committedValue + (rtl ? step : -step);
171
+ nextValue = rtl
172
+ ? committedValue + step
173
+ : previousRatingValue(committedValue, count, step);
80
174
  if (event.key === 'Home')
81
175
  nextValue = allowsKeyboardClear(clearable) ? 0 : Math.min(step, count);
82
176
  if (event.key === 'End')
@@ -86,13 +180,19 @@ export const Rating = forwardRef(function Rating({ value, defaultValue = 0, onVa
86
180
  if (nextValue === null)
87
181
  return;
88
182
  event.preventDefault();
89
- setPreviewValue(null);
183
+ changePreview(null);
90
184
  let normalized = normalizeRatingValue(nextValue, count, step);
91
185
  if (normalized === 0 && committedValue > 0 && !allowsKeyboardClear(clearable)) {
92
186
  normalized = Math.min(step, count);
93
187
  }
94
188
  if (normalized === committedValue)
95
189
  return;
190
+ clearPendingKeyboard();
191
+ pendingKeyboardValueRef.current = normalized;
192
+ pendingKeyboardTimerRef.current = setTimeout(() => {
193
+ pendingKeyboardTimerRef.current = undefined;
194
+ pendingKeyboardValueRef.current = null;
195
+ }, 100);
96
196
  if (!controlled)
97
197
  setUncontrolledValue(normalized);
98
198
  onValueChange?.(normalized);
@@ -102,7 +202,7 @@ export const Rating = forwardRef(function Rating({ value, defaultValue = 0, onVa
102
202
  index: index + 1,
103
203
  fraction,
104
204
  filled,
105
- previewing: previewValue !== null,
205
+ previewing,
106
206
  };
107
207
  if (renderSymbol)
108
208
  return renderSymbol(state);
@@ -110,18 +210,25 @@ export const Rating = forwardRef(function Rating({ value, defaultValue = 0, onVa
110
210
  return symbol;
111
211
  return _jsx(PencilIcon, { paths: defaultSymbolPaths, sourceSize: 24, size: defaultIconSize, active: filled, seed: `${symbolId}-${index}` });
112
212
  };
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);
213
+ return (_jsxs("div", { ...props, "aria-disabled": disabled || undefined, "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : t('rating.label')), "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) => {
214
+ clearPendingKeyboard();
215
+ changePreview(null);
115
216
  onBlur?.(event);
116
- }, onKeyDown: handleKeyDown, onPointerLeave: (event) => {
117
- setPreviewValue(null);
217
+ }, onKeyDown: handleKeyDown, onPointerDown: (event) => {
218
+ onPointerDown?.(event);
219
+ if (!event.defaultPrevented && !disabled) {
220
+ clearPendingKeyboard();
221
+ rootRef.current?.focus({ preventScroll: true });
222
+ }
223
+ }, onPointerLeave: (event) => {
224
+ changePreview(null);
118
225
  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
- }) }) }));
226
+ }, 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) => {
227
+ const fraction = Math.min(1, Math.max(0, displayValue - index));
228
+ return (_jsxs("span", { className: "inkra-rating__symbol-target", "data-index": index + 1, onClick: (event) => commit(valueFromPointer(event, index)), onPointerMove: (event) => {
229
+ if (hoverPreview && event.pointerType === 'mouse' && !disabled && !readOnly) {
230
+ changePreview(valueFromPointer(event, index));
231
+ }
232
+ }, 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));
233
+ }) })] }));
127
234
  });
@@ -23,7 +23,7 @@ export interface ResizableHandleProps extends SeparatorProps {
23
23
  label?: string;
24
24
  lineSeed?: string | number;
25
25
  }
26
- declare function ResizableHandle({ 'aria-label': ariaLabel, children, className, label, lineSeed, ...props }: ResizableHandleProps): import("react").JSX.Element;
26
+ declare function ResizableHandle(inkraProps: ResizableHandleProps): import("react").JSX.Element;
27
27
  export declare const Resizable: Readonly<{
28
28
  Root: typeof ResizableRoot;
29
29
  Panel: typeof ResizablePanel;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useInkraLocale } from '../i18n/index.js';
2
3
  import { Group as ResizableGroupPrimitive, Panel as ResizablePanelPrimitive, Separator as ResizableSeparatorPrimitive, useGroupCallbackRef, useGroupRef, usePanelCallbackRef, usePanelRef, } from 'react-resizable-panels';
3
4
  import { useCallback, useReducer } from 'react';
4
5
  import { PencilLine } from '../PencilLine.js';
@@ -39,10 +40,12 @@ function ResizableRoot({ apiRef, className, defaultLayout, layout, onLayoutChang
39
40
  function ResizablePanel({ className, ...props }) {
40
41
  return _jsx(ResizablePanelPrimitive, { ...props, className: classes('inkra-resizable__panel', className) });
41
42
  }
42
- function ResizableHandle({ 'aria-label': ariaLabel, children, className, label = 'Resize panels', lineSeed = 'inkra-resizable-handle', ...props }) {
43
+ function ResizableHandle(inkraProps) {
44
+ const { t } = useInkraLocale();
45
+ const { 'aria-label': ariaLabel, children, className, label = t('resizable.label'), lineSeed = 'inkra-resizable-handle', ...props } = inkraProps;
43
46
  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
47
  }
45
- export const Resizable = Object.freeze({
48
+ export const Resizable = /* @__PURE__ */ Object.freeze({
46
49
  Root: ResizableRoot,
47
50
  Panel: ResizablePanel,
48
51
  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') {