@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,17 +1,27 @@
1
- function normalizeCopy(nodes, seen) {
1
+ export function normalizeTreeSelectNodes(nodes) {
2
2
  const result = [];
3
- for (const node of nodes) {
3
+ const seen = new Set();
4
+ const stack = [{ nodes, position: 0, result }];
5
+ // Preserve preorder duplicate handling without using the JavaScript call stack.
6
+ while (stack.length) {
7
+ const frame = stack[stack.length - 1];
8
+ if (frame.position === frame.nodes.length) {
9
+ if (frame.parent && frame.result.length)
10
+ frame.parent.children = frame.result;
11
+ stack.pop();
12
+ continue;
13
+ }
14
+ const node = frame.nodes[frame.position++];
4
15
  if (!node.id || seen.has(node.id))
5
16
  continue;
6
17
  seen.add(node.id);
7
- const children = normalizeCopy(node.children ?? [], seen);
8
- result.push({ ...node, children: children.length ? children : undefined });
18
+ const copy = { ...node, children: undefined };
19
+ frame.result.push(copy);
20
+ if (node.children?.length)
21
+ stack.push({ nodes: node.children, position: 0, result: [], parent: copy });
9
22
  }
10
23
  return result;
11
24
  }
12
- export function normalizeTreeSelectNodes(nodes) {
13
- return normalizeCopy(nodes, new Set());
14
- }
15
25
  export function indexTreeSelectNodes(nodes) {
16
26
  const normalized = normalizeTreeSelectNodes(nodes);
17
27
  const orderedIds = [];
@@ -19,116 +29,134 @@ export function indexTreeSelectNodes(nodes) {
19
29
  const parentById = new Map();
20
30
  const branchIds = new Set();
21
31
  const disabledIds = new Set();
22
- const visit = (items, parentId) => {
23
- for (const node of items) {
24
- orderedIds.push(node.id);
25
- byId.set(node.id, node);
26
- parentById.set(node.id, parentId);
27
- if (node.disabled)
28
- disabledIds.add(node.id);
29
- if (node.children?.length) {
30
- branchIds.add(node.id);
31
- visit(node.children, node.id);
32
+ const stack = normalized.map(node => ({ node, parentId: null })).reverse();
33
+ while (stack.length) {
34
+ const { node, parentId } = stack.pop();
35
+ orderedIds.push(node.id);
36
+ byId.set(node.id, node);
37
+ parentById.set(node.id, parentId);
38
+ if (node.disabled)
39
+ disabledIds.add(node.id);
40
+ if (node.children?.length) {
41
+ branchIds.add(node.id);
42
+ for (let position = node.children.length - 1; position >= 0; position--) {
43
+ stack.push({ node: node.children[position], parentId: node.id });
32
44
  }
33
45
  }
34
- };
35
- visit(normalized, null);
46
+ }
36
47
  return { nodes: normalized, orderedIds, byId, parentById, branchIds, disabledIds };
37
48
  }
38
49
  function addVisibleSubtree(node, visibleIds, expandedIds) {
39
- visibleIds.add(node.id);
40
- if (node.children?.length) {
41
- expandedIds.add(node.id);
42
- for (const child of node.children)
43
- addVisibleSubtree(child, visibleIds, expandedIds);
50
+ const stack = [node];
51
+ while (stack.length) {
52
+ const current = stack.pop();
53
+ visibleIds.add(current.id);
54
+ if (current.children?.length) {
55
+ expandedIds?.add(current.id);
56
+ for (let position = current.children.length - 1; position >= 0; position--) {
57
+ stack.push(current.children[position]);
58
+ }
59
+ }
44
60
  }
45
61
  }
46
- export function filterTreeSelectNodes(nodes, query) {
47
- const normalized = normalizeTreeSelectNodes(nodes);
62
+ function filterNormalizedTreeSelectNodes(nodes, query) {
48
63
  const needle = query.trim().toLocaleLowerCase();
49
64
  const visibleIds = new Set();
50
65
  const expandedIds = new Set();
51
66
  if (!needle) {
52
- for (const node of normalized)
53
- addVisibleSubtree(node, visibleIds, new Set());
54
- return { nodes: normalized, visibleIds, expandedIds };
67
+ for (const node of nodes)
68
+ addVisibleSubtree(node, visibleIds);
69
+ return { nodes, visibleIds, expandedIds };
55
70
  }
56
- const visit = (node) => {
71
+ const result = [];
72
+ const stack = nodes.map(node => ({ node, destination: result })).reverse();
73
+ while (stack.length) {
74
+ const { node, destination, children } = stack.pop();
75
+ if (children) {
76
+ if (children.length) {
77
+ visibleIds.add(node.id);
78
+ expandedIds.add(node.id);
79
+ destination.push({ ...node, children });
80
+ }
81
+ continue;
82
+ }
57
83
  const haystack = `${node.label} ${node.keywords ?? ''}`.toLocaleLowerCase();
58
84
  if (haystack.includes(needle)) {
59
85
  addVisibleSubtree(node, visibleIds, expandedIds);
60
- return node;
86
+ destination.push(node);
87
+ }
88
+ else if (node.children?.length) {
89
+ const matches = [];
90
+ stack.push({ node, destination, children: matches });
91
+ for (let position = node.children.length - 1; position >= 0; position--) {
92
+ stack.push({ node: node.children[position], destination: matches });
93
+ }
61
94
  }
62
- const children = (node.children ?? []).map(visit).filter((child) => child !== null);
63
- if (!children.length)
64
- return null;
65
- visibleIds.add(node.id);
66
- expandedIds.add(node.id);
67
- return { ...node, children };
68
- };
69
- return {
70
- nodes: normalized.map(visit).filter((node) => node !== null),
71
- visibleIds,
72
- expandedIds,
73
- };
95
+ }
96
+ return { nodes: result, visibleIds, expandedIds };
97
+ }
98
+ export function filterTreeSelectNodes(nodes, query) {
99
+ return filterNormalizedTreeSelectNodes(normalizeTreeSelectNodes(nodes), query);
100
+ }
101
+ /** Reuse the component's index instead of cloning every node on each search. */
102
+ export function filterTreeSelectNodesFromIndex(index, query) {
103
+ return filterNormalizedTreeSelectNodes(index.nodes, query);
74
104
  }
75
105
  function isSelectable(index, id, branchSelection) {
76
106
  return index.byId.has(id)
77
107
  && !index.disabledIds.has(id)
78
108
  && (branchSelection === 'all' || !index.branchIds.has(id));
79
109
  }
80
- function descendants(index, id) {
81
- const node = index.byId.get(id);
82
- if (!node?.children?.length)
83
- return [];
84
- const result = [];
85
- const visit = (items) => {
86
- for (const child of items) {
87
- result.push(child.id);
88
- if (child.children?.length)
89
- visit(child.children);
90
- }
91
- };
92
- visit(node.children);
93
- return result;
94
- }
95
110
  function orderedSelection(index, selected, previous) {
96
111
  const known = index.orderedIds.filter(id => selected.has(id));
97
- const unknown = previous.filter((id, position) => !index.byId.has(id) && previous.indexOf(id) === position);
112
+ const unknown = [...new Set(previous)].filter(id => !index.byId.has(id));
98
113
  return [...known, ...unknown];
99
114
  }
100
115
  export function updateTreeSelectSelection(nodes, current, toggledId, selected, options) {
101
- const index = indexTreeSelectNodes(nodes);
116
+ return updateTreeSelectSelectionFromIndex(indexTreeSelectNodes(nodes), current, toggledId, selected, options);
117
+ }
118
+ /** Reuse the component's index for selection and aggregate each subtree once. */
119
+ export function updateTreeSelectSelectionFromIndex(index, current, toggledId, selected, options) {
102
120
  if (!isSelectable(index, toggledId, options.branchSelection))
103
121
  return [...current];
104
122
  if (!options.multiple)
105
123
  return selected ? [toggledId] : [];
106
124
  const next = new Set(current);
107
- const affected = [toggledId];
108
- if (options.selectionPropagation !== 'none' && index.branchIds.has(toggledId)) {
109
- affected.push(...descendants(index, toggledId));
110
- }
111
- for (const id of affected) {
112
- if (!isSelectable(index, id, options.branchSelection))
113
- continue;
114
- if (selected)
115
- next.add(id);
116
- else
117
- next.delete(id);
125
+ const stack = [index.byId.get(toggledId)];
126
+ while (stack.length) {
127
+ const node = stack.pop();
128
+ if (isSelectable(index, node.id, options.branchSelection)) {
129
+ if (selected)
130
+ next.add(node.id);
131
+ else
132
+ next.delete(node.id);
133
+ }
134
+ if (options.selectionPropagation !== 'none' && node.children?.length) {
135
+ for (const child of node.children)
136
+ stack.push(child);
137
+ }
118
138
  }
119
139
  if (options.selectionPropagation === 'both' && options.branchSelection === 'all') {
120
- const branches = index.orderedIds.filter(id => index.branchIds.has(id)).reverse();
121
- for (const branchId of branches) {
122
- if (index.disabledIds.has(branchId))
123
- continue;
124
- const selectableDescendants = descendants(index, branchId)
125
- .filter(id => isSelectable(index, id, options.branchSelection));
126
- if (!selectableDescendants.length)
127
- continue;
128
- if (selectableDescendants.every(id => next.has(id)))
129
- next.add(branchId);
130
- else
131
- next.delete(branchId);
140
+ const summaries = new Map();
141
+ // Reverse preorder visits children before parents. Include descendants under
142
+ // disabled branches, but never count a disabled node itself as selectable.
143
+ for (let position = index.orderedIds.length - 1; position >= 0; position--) {
144
+ const id = index.orderedIds[position];
145
+ const summary = summaries.get(id) ?? { count: 0, unselected: 0 };
146
+ const enabled = !index.disabledIds.has(id);
147
+ if (enabled && summary.count > 0) {
148
+ if (summary.unselected === 0)
149
+ next.add(id);
150
+ else
151
+ next.delete(id);
152
+ }
153
+ const parentId = index.parentById.get(id);
154
+ if (parentId != null) {
155
+ const parent = summaries.get(parentId) ?? { count: 0, unselected: 0 };
156
+ parent.count += summary.count + (enabled ? 1 : 0);
157
+ parent.unselected += summary.unselected + (enabled && !next.has(id) ? 1 : 0);
158
+ summaries.set(parentId, parent);
159
+ }
132
160
  }
133
161
  }
134
162
  return orderedSelection(index, next, current);
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
2
+ import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
3
3
  import { PencilText } from '../PencilText.js';
4
4
  import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
5
5
  import { splitTypewriterGraphemes, typewriterDelayAfter } from './model.js';
@@ -19,6 +19,14 @@ function usePrefersReducedMotion() {
19
19
  function safeDuration(value) {
20
20
  return Number.isFinite(value) ? Math.max(0, value) : 0;
21
21
  }
22
+ const TypewriterGrapheme = memo(function TypewriterGrapheme({ grapheme, index, revealed: isRevealed }) {
23
+ if (grapheme === '\n')
24
+ return _jsx("br", {});
25
+ if (/^\s+$/u.test(grapheme)) {
26
+ return _jsx("span", { className: "inkra-typewriter__space", "data-revealed": isRevealed, children: grapheme });
27
+ }
28
+ return _jsx(PencilText, { className: "inkra-typewriter__grapheme", "data-revealed": isRevealed, seed: `typewriter-${index}-${grapheme}`, children: grapheme });
29
+ });
22
30
  export const Typewriter = forwardRef(function Typewriter({ children, className, defaultPaused = false, locale, onComplete, onPausedChange, paused, punctuationDelay = 240, speed = 48, startDelay = 0, ...props }, forwardedRef) {
23
31
  const graphemes = useMemo(() => splitTypewriterGraphemes(children, locale), [children, locale]);
24
32
  const [internalPaused, setInternalPaused] = useState(defaultPaused);
@@ -88,14 +96,6 @@ export const Typewriter = forwardRef(function Typewriter({ children, className,
88
96
  }, [children, complete, graphemes, isPaused, punctuationDelay, reducedMotion, revealed, speed, startDelay]);
89
97
  const classes = ['inkra-typewriter', className].filter(Boolean).join(' ');
90
98
  const state = complete ? 'complete' : isPaused ? 'paused' : 'typing';
91
- return _jsxs("span", { ...props, className: classes, "data-state": state, "data-revealed": revealed, "data-total": graphemes.length, children: [_jsx("span", { "aria-hidden": "true", className: "inkra-typewriter__visual", children: graphemes.map((grapheme, index) => {
92
- const isRevealed = index < revealed;
93
- if (grapheme === '\n')
94
- return _jsx("br", {}, `line-${index}`);
95
- if (/^\s+$/u.test(grapheme)) {
96
- return _jsx("span", { className: "inkra-typewriter__space", "data-revealed": isRevealed, children: grapheme }, `space-${index}`);
97
- }
98
- return _jsx(PencilText, { className: "inkra-typewriter__grapheme", "data-revealed": isRevealed, seed: `typewriter-${index}-${grapheme}`, children: grapheme }, `${index}-${grapheme}`);
99
- }) }), _jsx("span", { className: "inkra-typewriter__accessible", children: children })] });
99
+ return _jsxs("span", { ...props, className: classes, "data-state": state, "data-revealed": revealed, "data-total": graphemes.length, children: [_jsx("span", { "aria-hidden": "true", className: "inkra-typewriter__visual", children: graphemes.map((grapheme, index) => _jsx(TypewriterGrapheme, { grapheme: grapheme, index: index, revealed: index < revealed }, `${index}-${grapheme}`)) }), _jsx("span", { className: "inkra-typewriter__accessible", children: children })] });
100
100
  });
101
101
  export { isTypewriterPunctuation, splitTypewriterGraphemes, typewriterDelayAfter, } from './model.js';
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef, useEffect, useId, useRef, useState, } from 'react';
2
+ import { forwardRef, memo, useCallback, useEffect, useId, useMemo, useRef, useState, } from 'react';
3
3
  import { Button } from '../Button.js';
4
4
  import { Progress } from '../display/index.js';
5
5
  import { PencilIcon } from '../PencilIcon.js';
@@ -56,7 +56,10 @@ function asErrorMessage(error, fallback) {
56
56
  function statusLabel(status, labels) {
57
57
  return labels[status];
58
58
  }
59
- export const Upload = forwardRef(function Upload({ accept, autoUpload = true, capture, className, defaultFiles = [], disabled = false, files, inputProps, labels: labelOverrides, maxFiles = Number.POSITIVE_INFINITY, maxSize = Number.POSITIVE_INFINITY, multiple = true, onFilesChange, onReject, onUpload, validate, ...props }, ref) {
59
+ const UploadRow = memo(function UploadRow({ item, labels, disabled, canUpload, startUpload, cancel, remove }) {
60
+ return _jsx("li", { className: "inkra-upload__item", "data-status": item.status, children: _jsxs(PencilSurface, { className: "inkra-upload__item-surface", variant: "paper", radius: 8, children: [_jsxs("div", { className: "inkra-upload__file", children: [_jsx(PencilText, { className: "inkra-upload__name", ellipsis: true, children: item.file.name }), _jsxs("span", { className: "inkra-upload__meta", children: [formatBytes(item.file.size), " \u00B7 ", statusLabel(item.status, labels)] }), item.status === 'uploading' && _jsx(Progress, { size: "sm", value: item.progress ?? 0, "aria-label": `${item.file.name}: ${Math.round(item.progress ?? 0)}%` }), item.error && _jsx("span", { className: "inkra-upload__error", children: item.error })] }), _jsxs("div", { className: "inkra-upload__actions", children: [item.status === 'uploading' && _jsx(Button, { disabled: disabled, size: "sm", variant: "ghost", onClick: () => cancel(item.id), children: labels.cancel }), canUpload && item.status === 'selected' && _jsx(Button, { disabled: disabled, size: "sm", variant: "ghost", onClick: () => startUpload(item.id), children: labels.upload }), canUpload && (item.status === 'error' || item.status === 'cancelled') && _jsx(Button, { disabled: disabled, size: "sm", variant: "ghost", onClick: () => startUpload(item.id, 'retry'), children: labels.retry }), _jsx(Button, { disabled: disabled, size: "sm", variant: "ghost", onClick: () => remove(item.id), children: labels.remove })] })] }) });
61
+ });
62
+ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, capture, className, defaultFiles = [], disabled = false, files, inputProps, labels: labelOverrides, maxFiles = Number.POSITIVE_INFINITY, maxSize = Number.POSITIVE_INFINITY, multiple = true, onFilesChange, onReject, onUpload, validate, 'aria-label': ariaLabel, ...props }, ref) {
60
63
  const instanceId = useId();
61
64
  const inputRef = useRef(null);
62
65
  const itemCounter = useRef(0);
@@ -71,7 +74,14 @@ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, ca
71
74
  const itemsRef = useRef(items);
72
75
  itemsRef.current = items;
73
76
  disabledRef.current = disabled;
74
- const labels = { ...defaultLabels, ...labelOverrides };
77
+ const labelValues = { ...defaultLabels, ...labelOverrides };
78
+ // Snapshot scalar/function values so same-object label edits remain observable.
79
+ const labels = useMemo(() => labelValues, [
80
+ labelValues.browse, labelValues.drop, labelValues.selected, labelValues.upload,
81
+ labelValues.uploading, labelValues.success, labelValues.error, labelValues.cancelled,
82
+ labelValues.retry, labelValues.cancel, labelValues.remove, labelValues.rejected,
83
+ labelValues.acceptError, labelValues.sizeError, labelValues.countError,
84
+ ]);
75
85
  const commitRef = useRef(() => { });
76
86
  const commit = (next, details) => {
77
87
  if (!controlled) {
@@ -81,7 +91,7 @@ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, ca
81
91
  onFilesChange?.(next, details);
82
92
  };
83
93
  commitRef.current = commit;
84
- const updateItem = (id, update, details) => {
94
+ const updateItem = useCallback((id, update, details) => {
85
95
  const current = itemsRef.current;
86
96
  const index = current.findIndex(item => item.id === id);
87
97
  if (index < 0)
@@ -89,8 +99,8 @@ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, ca
89
99
  const next = [...current];
90
100
  next[index] = update(current[index]);
91
101
  commitRef.current(next, { ...details, item: next[index] });
92
- };
93
- const startUpload = (id, reason = 'upload-start') => {
102
+ }, []);
103
+ const startUpload = useCallback((id, reason = 'upload-start') => {
94
104
  const item = itemsRef.current.find(candidate => candidate.id === id);
95
105
  if (!item || !onUpload || disabledRef.current)
96
106
  return;
@@ -123,7 +133,7 @@ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, ca
123
133
  controllers.current.delete(id);
124
134
  updateItem(id, current => ({ ...current, status: 'error', progress: undefined, error: asErrorMessage(error, labels.error) }), { reason: 'upload-error' });
125
135
  });
126
- };
136
+ }, [onUpload, labels.error, updateItem]);
127
137
  const addFiles = (incoming) => {
128
138
  if (disabled || incoming.length === 0)
129
139
  return;
@@ -173,21 +183,21 @@ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, ca
173
183
  });
174
184
  });
175
185
  };
176
- const remove = (id) => {
177
- if (disabled)
186
+ const remove = useCallback((id) => {
187
+ if (disabledRef.current)
178
188
  return;
179
189
  controllers.current.get(id)?.controller.abort();
180
190
  controllers.current.delete(id);
181
191
  const item = itemsRef.current.find(candidate => candidate.id === id);
182
192
  commitRef.current(itemsRef.current.filter(candidate => candidate.id !== id), { reason: 'remove', item });
183
- };
184
- const cancel = (id) => {
185
- if (disabled)
193
+ }, []);
194
+ const cancel = useCallback((id) => {
195
+ if (disabledRef.current)
186
196
  return;
187
197
  controllers.current.get(id)?.controller.abort();
188
198
  controllers.current.delete(id);
189
199
  updateItem(id, item => ({ ...item, status: 'cancelled', progress: undefined, error: undefined }), { reason: 'cancel' });
190
- };
200
+ }, [updateItem]);
191
201
  useEffect(() => {
192
202
  mounted.current = true;
193
203
  return () => {
@@ -205,6 +215,10 @@ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, ca
205
215
  }
206
216
  });
207
217
  }, [items]);
218
+ useEffect(() => {
219
+ if (disabled)
220
+ setDragActive(false);
221
+ }, [disabled]);
208
222
  const openPicker = () => {
209
223
  if (!disabled)
210
224
  inputRef.current?.click();
@@ -220,11 +234,11 @@ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, ca
220
234
  setDragActive(false);
221
235
  addFiles(Array.from(event.dataTransfer.files));
222
236
  };
223
- return _jsxs("div", { ...props, ref: ref, className: ['inkra-upload', className].filter(Boolean).join(' '), "data-disabled": disabled || undefined, children: [_jsx("input", { ...inputProps, ref: inputRef, className: ['inkra-upload__input', inputProps?.className].filter(Boolean).join(' '), type: "file", accept: accept, capture: capture, multiple: multiple, disabled: disabled, onChange: event => {
237
+ return _jsxs("div", { ...props, "aria-label": ariaLabel, ref: ref, className: ['inkra-upload', className].filter(Boolean).join(' '), "data-disabled": disabled || undefined, children: [_jsx("input", { ...inputProps, "aria-hidden": "true", ref: inputRef, className: ['inkra-upload__input', inputProps?.className].filter(Boolean).join(' '), type: "file", accept: accept, capture: capture, multiple: multiple, disabled: disabled, tabIndex: -1, onChange: event => {
224
238
  addFiles(Array.from(event.currentTarget.files ?? []));
225
239
  event.currentTarget.value = '';
226
- } }), _jsxs(PencilSurface, { as: "div", className: "inkra-upload__dropzone", "data-drag-active": dragActive || undefined, variant: dragActive ? 'selection' : 'outline', role: "button", tabIndex: disabled ? -1 : 0, "aria-disabled": disabled || undefined, onClick: openPicker, onKeyDown: handleKeyDown, onDragEnter: event => { event.preventDefault(); if (!disabled)
240
+ } }), _jsxs(PencilSurface, { as: "div", className: "inkra-upload__dropzone", "data-drag-active": dragActive || undefined, variant: dragActive ? 'selection' : 'outline', role: "button", "aria-label": ariaLabel, tabIndex: disabled ? -1 : 0, "aria-disabled": disabled || undefined, onClick: openPicker, onKeyDown: handleKeyDown, onDragEnter: event => { event.preventDefault(); if (!disabled)
227
241
  setDragActive(true); }, onDragOver: event => { event.preventDefault(); if (!disabled)
228
242
  event.dataTransfer.dropEffect = 'copy'; }, onDragLeave: event => { if (!event.currentTarget.contains(event.relatedTarget))
229
- setDragActive(false); }, onDrop: handleDrop, children: [_jsx(PencilIcon, { className: "inkra-upload__icon", paths: uploadPaths, size: 32, sourceSize: 24 }), _jsxs("span", { className: "inkra-upload__prompt", children: [_jsx(PencilText, { children: labels.browse }), _jsx("span", { children: labels.drop })] }), accept && _jsx("span", { className: "inkra-upload__hint", children: accept })] }), rejections.length > 0 && _jsxs("div", { className: "inkra-upload__rejections", role: "alert", children: [_jsx("strong", { children: labels.rejected }), _jsx("ul", { children: rejections.map((rejection, index) => _jsxs("li", { children: [rejection.file.name, ": ", rejection.message] }, `${rejection.file.name}-${index}`)) })] }), items.length > 0 && _jsx("ul", { className: "inkra-upload__list", "aria-live": "polite", children: items.map(item => _jsx("li", { className: "inkra-upload__item", "data-status": item.status, children: _jsxs(PencilSurface, { className: "inkra-upload__item-surface", variant: "paper", radius: 8, children: [_jsxs("div", { className: "inkra-upload__file", children: [_jsx("span", { className: "inkra-upload__name", children: item.file.name }), _jsxs("span", { className: "inkra-upload__meta", children: [formatBytes(item.file.size), " \u00B7 ", statusLabel(item.status, labels)] }), item.status === 'uploading' && _jsx(Progress, { size: "sm", value: item.progress ?? 0, "aria-label": `${item.file.name}: ${Math.round(item.progress ?? 0)}%` }), item.error && _jsx("span", { className: "inkra-upload__error", children: item.error })] }), _jsxs("div", { className: "inkra-upload__actions", children: [item.status === 'uploading' && _jsx(Button, { disabled: disabled, size: "sm", variant: "ghost", onClick: () => cancel(item.id), children: labels.cancel }), onUpload && item.status === 'selected' && _jsx(Button, { disabled: disabled, size: "sm", variant: "ghost", onClick: () => startUpload(item.id), children: labels.upload }), onUpload && (item.status === 'error' || item.status === 'cancelled') && _jsx(Button, { disabled: disabled, size: "sm", variant: "ghost", onClick: () => startUpload(item.id, 'retry'), children: labels.retry }), _jsx(Button, { disabled: disabled, size: "sm", variant: "ghost", onClick: () => remove(item.id), children: labels.remove })] })] }) }, item.id)) })] });
243
+ setDragActive(false); }, onDrop: handleDrop, children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-upload__focus-ring", seed: `${instanceId}-focus`, variant: "outline" }), _jsx(PencilIcon, { className: "inkra-upload__icon", paths: uploadPaths, size: 32, sourceSize: 24 }), _jsxs("span", { className: "inkra-upload__prompt", children: [_jsx(PencilText, { children: labels.browse }), _jsx("span", { children: labels.drop })] }), accept && _jsx("span", { className: "inkra-upload__hint", children: accept })] }), rejections.length > 0 && _jsxs("div", { className: "inkra-upload__rejections", role: "alert", children: [_jsx("strong", { children: labels.rejected }), _jsx("ul", { children: rejections.map((rejection, index) => _jsxs("li", { children: [rejection.file.name, ": ", rejection.message] }, `${rejection.file.name}-${index}`)) })] }), items.length > 0 && _jsx("ul", { className: "inkra-upload__list", "aria-live": "polite", children: items.map(item => _jsx(UploadRow, { item: item, labels: labels, disabled: disabled, canUpload: Boolean(onUpload), startUpload: startUpload, cancel: cancel, remove: remove }, item.id)) })] });
230
244
  });
@@ -26,12 +26,15 @@ function VirtualListInner({ 'aria-label': ariaLabel, className, empty, emptyLabe
26
26
  const safePaddingEnd = normalizeVirtualListSpacing(paddingEnd);
27
27
  useEffect(() => setHydrated(true), []);
28
28
  const resolveScrollElement = useCallback(() => getScrollElement?.() ?? rootRef.current, [getScrollElement]);
29
- const resolveItemKey = useCallback((index) => itemKey(itemsRef.current[index], index), [itemKey]);
29
+ const resolveItemKey = useCallback(
30
+ // The engine caches measurements by this callback, not by the items array
31
+ // or estimateSize callback. Invalidate it for immutable data/estimate updates.
32
+ (index) => itemKey(items[index], index), [items, itemKey, estimateSize]);
30
33
  const resolveEstimateSize = useCallback((index) => {
31
- const item = itemsRef.current[index];
34
+ const item = items[index];
32
35
  const value = typeof estimateSize === 'function' ? estimateSize(item, index) : estimateSize;
33
36
  return normalizeVirtualListSize(value);
34
- }, [estimateSize]);
37
+ }, [items, estimateSize]);
35
38
  const virtualizer = useVirtualizer({
36
39
  count: items.length,
37
40
  enabled: hydrated && items.length > 0,
@@ -90,10 +93,9 @@ function VirtualListInner({ 'aria-label': ariaLabel, className, empty, emptyLabe
90
93
  virtualizer.scrollToIndex(items.length - 1, { align: 'end' });
91
94
  }
92
95
  }
93
- const fallbackIndexes = getVirtualListSsrIndexes(items.length, ssrItemCount);
94
96
  const renderedRows = virtualized
95
97
  ? virtualRows.map(row => ({ index: row.index, key: row.key, start: row.start }))
96
- : fallbackIndexes.map(index => ({ index, key: itemKey(items[index], index), start: null }));
98
+ : getVirtualListSsrIndexes(items.length, ssrItemCount).map(index => ({ index, key: itemKey(items[index], index), start: null }));
97
99
  const rootStyle = externalScroll ? style : { blockSize: height, ...style };
98
100
  const content = _jsxs(_Fragment, { children: [_jsx("ul", { "aria-label": listLabel, className: "inkra-virtual-list__items", role: listRole, style: virtualized ? { blockSize: `${virtualizer.getTotalSize()}px` } : undefined, children: renderedRows.map(row => {
99
101
  const item = items[row.index];