@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
@@ -21,11 +21,17 @@ export interface FilteredTreeSelectNodes {
21
21
  visibleIds: ReadonlySet<string>;
22
22
  expandedIds: ReadonlySet<string>;
23
23
  }
24
- export declare function normalizeTreeSelectNodes(nodes: readonly TreeSelectNode[]): readonly TreeSelectNode[];
25
- export declare function indexTreeSelectNodes(nodes: readonly TreeSelectNode[]): TreeSelectIndex;
26
- export declare function filterTreeSelectNodes(nodes: readonly TreeSelectNode[], query: string): FilteredTreeSelectNodes;
27
- export declare function updateTreeSelectSelection(nodes: readonly TreeSelectNode[], current: readonly string[], toggledId: string, selected: boolean, options: {
24
+ interface TreeSelectSelectionOptions {
28
25
  multiple: boolean;
29
26
  branchSelection: TreeSelectBranchSelection;
30
27
  selectionPropagation: TreeSelectSelectionPropagation;
31
- }): string[];
28
+ }
29
+ export declare function normalizeTreeSelectNodes(nodes: readonly TreeSelectNode[]): readonly TreeSelectNode[];
30
+ export declare function indexTreeSelectNodes(nodes: readonly TreeSelectNode[]): TreeSelectIndex;
31
+ export declare function filterTreeSelectNodes(nodes: readonly TreeSelectNode[], query: string): FilteredTreeSelectNodes;
32
+ /** Reuse the component's index instead of cloning every node on each search. */
33
+ export declare function filterTreeSelectNodesFromIndex(index: TreeSelectIndex, query: string): FilteredTreeSelectNodes;
34
+ export declare function updateTreeSelectSelection(nodes: readonly TreeSelectNode[], current: readonly string[], toggledId: string, selected: boolean, options: TreeSelectSelectionOptions): string[];
35
+ /** Reuse the component's index for selection and aggregate each subtree once. */
36
+ export declare function updateTreeSelectSelectionFromIndex(index: TreeSelectIndex, current: readonly string[], toggledId: string, selected: boolean, options: TreeSelectSelectionOptions): string[];
37
+ export {};
@@ -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,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef, useEffect, useId, useRef, useState, } from 'react';
2
+ import { useInkraLocale } from '../i18n/index.js';
3
+ import { forwardRef, memo, useCallback, useEffect, useId, useMemo, useRef, useState, } from 'react';
3
4
  import { Button } from '../Button.js';
4
5
  import { Progress } from '../display/index.js';
5
6
  import { PencilIcon } from '../PencilIcon.js';
@@ -10,23 +11,23 @@ const uploadPaths = [
10
11
  { d: 'm17 8-5-5-5 5' },
11
12
  { d: 'M12 3v12' },
12
13
  ];
13
- const defaultLabels = {
14
- browse: 'Choose files',
15
- drop: 'or drop files here',
16
- selected: 'Selected',
17
- upload: 'Upload',
18
- uploading: 'Uploading',
19
- success: 'Uploaded',
20
- error: 'Upload failed',
21
- cancelled: 'Cancelled',
22
- retry: 'Retry',
23
- cancel: 'Cancel',
24
- remove: 'Remove',
25
- rejected: 'Files not added',
26
- acceptError: 'This file type is not accepted.',
27
- sizeError: maxSize => `File exceeds the ${formatBytes(maxSize)} limit.`,
28
- countError: maxFiles => `Only ${maxFiles} file${maxFiles === 1 ? '' : 's'} may be selected.`,
29
- };
14
+ const defaultLabels = (t) => ({
15
+ browse: t('upload.browse'),
16
+ drop: t('upload.drop'),
17
+ selected: t('common.selected'),
18
+ upload: t('upload.upload'),
19
+ uploading: t('upload.uploading'),
20
+ success: t('upload.success'),
21
+ error: t('upload.failed'),
22
+ cancelled: t('upload.cancelled'),
23
+ retry: t('common.retry'),
24
+ cancel: t('common.cancel'),
25
+ remove: t('common.remove'),
26
+ rejected: t('upload.rejected'),
27
+ acceptError: t('upload.acceptError'),
28
+ sizeError: maxSize => t('upload.sizeError', { size: formatBytes(maxSize) }),
29
+ countError: maxFiles => t(maxFiles === 1 ? 'upload.countErrorOne' : 'upload.countError', { count: maxFiles }),
30
+ });
30
31
  function formatBytes(value) {
31
32
  if (value < 1024)
32
33
  return `${value} B`;
@@ -56,7 +57,11 @@ function asErrorMessage(error, fallback) {
56
57
  function statusLabel(status, labels) {
57
58
  return labels[status];
58
59
  }
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) {
60
+ const UploadRow = memo(function UploadRow({ item, labels, disabled, canUpload, startUpload, cancel, remove }) {
61
+ 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 })] })] }) });
62
+ });
63
+ 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) {
64
+ const { t } = useInkraLocale();
60
65
  const instanceId = useId();
61
66
  const inputRef = useRef(null);
62
67
  const itemCounter = useRef(0);
@@ -71,7 +76,15 @@ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, ca
71
76
  const itemsRef = useRef(items);
72
77
  itemsRef.current = items;
73
78
  disabledRef.current = disabled;
74
- const labels = { ...defaultLabels, ...labelOverrides };
79
+ const localizedDefaults = useMemo(() => defaultLabels(t), [t]);
80
+ const labelValues = { ...localizedDefaults, ...labelOverrides };
81
+ // Snapshot scalar/function values so same-object label edits remain observable.
82
+ const labels = useMemo(() => labelValues, [
83
+ labelValues.browse, labelValues.drop, labelValues.selected, labelValues.upload,
84
+ labelValues.uploading, labelValues.success, labelValues.error, labelValues.cancelled,
85
+ labelValues.retry, labelValues.cancel, labelValues.remove, labelValues.rejected,
86
+ labelValues.acceptError, labelValues.sizeError, labelValues.countError,
87
+ ]);
75
88
  const commitRef = useRef(() => { });
76
89
  const commit = (next, details) => {
77
90
  if (!controlled) {
@@ -81,7 +94,7 @@ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, ca
81
94
  onFilesChange?.(next, details);
82
95
  };
83
96
  commitRef.current = commit;
84
- const updateItem = (id, update, details) => {
97
+ const updateItem = useCallback((id, update, details) => {
85
98
  const current = itemsRef.current;
86
99
  const index = current.findIndex(item => item.id === id);
87
100
  if (index < 0)
@@ -89,8 +102,8 @@ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, ca
89
102
  const next = [...current];
90
103
  next[index] = update(current[index]);
91
104
  commitRef.current(next, { ...details, item: next[index] });
92
- };
93
- const startUpload = (id, reason = 'upload-start') => {
105
+ }, []);
106
+ const startUpload = useCallback((id, reason = 'upload-start') => {
94
107
  const item = itemsRef.current.find(candidate => candidate.id === id);
95
108
  if (!item || !onUpload || disabledRef.current)
96
109
  return;
@@ -123,7 +136,7 @@ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, ca
123
136
  controllers.current.delete(id);
124
137
  updateItem(id, current => ({ ...current, status: 'error', progress: undefined, error: asErrorMessage(error, labels.error) }), { reason: 'upload-error' });
125
138
  });
126
- };
139
+ }, [onUpload, labels.error, updateItem]);
127
140
  const addFiles = (incoming) => {
128
141
  if (disabled || incoming.length === 0)
129
142
  return;
@@ -173,21 +186,21 @@ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, ca
173
186
  });
174
187
  });
175
188
  };
176
- const remove = (id) => {
177
- if (disabled)
189
+ const remove = useCallback((id) => {
190
+ if (disabledRef.current)
178
191
  return;
179
192
  controllers.current.get(id)?.controller.abort();
180
193
  controllers.current.delete(id);
181
194
  const item = itemsRef.current.find(candidate => candidate.id === id);
182
195
  commitRef.current(itemsRef.current.filter(candidate => candidate.id !== id), { reason: 'remove', item });
183
- };
184
- const cancel = (id) => {
185
- if (disabled)
196
+ }, []);
197
+ const cancel = useCallback((id) => {
198
+ if (disabledRef.current)
186
199
  return;
187
200
  controllers.current.get(id)?.controller.abort();
188
201
  controllers.current.delete(id);
189
202
  updateItem(id, item => ({ ...item, status: 'cancelled', progress: undefined, error: undefined }), { reason: 'cancel' });
190
- };
203
+ }, [updateItem]);
191
204
  useEffect(() => {
192
205
  mounted.current = true;
193
206
  return () => {
@@ -205,6 +218,10 @@ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, ca
205
218
  }
206
219
  });
207
220
  }, [items]);
221
+ useEffect(() => {
222
+ if (disabled)
223
+ setDragActive(false);
224
+ }, [disabled]);
208
225
  const openPicker = () => {
209
226
  if (!disabled)
210
227
  inputRef.current?.click();
@@ -220,11 +237,11 @@ export const Upload = forwardRef(function Upload({ accept, autoUpload = true, ca
220
237
  setDragActive(false);
221
238
  addFiles(Array.from(event.dataTransfer.files));
222
239
  };
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 => {
240
+ 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
241
  addFiles(Array.from(event.currentTarget.files ?? []));
225
242
  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)
243
+ } }), _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
244
  setDragActive(true); }, onDragOver: event => { event.preventDefault(); if (!disabled)
228
245
  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)) })] });
246
+ 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
247
  });
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createElement as _createElement } from "react";
3
+ import { useInkraLocale } from '../i18n/index.js';
3
4
  import { useVirtualizer } from '@tanstack/react-virtual';
4
5
  import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState, } from 'react';
5
6
  import { PencilSurface } from '../PencilSurface.js';
@@ -12,7 +13,9 @@ function classes(...values) {
12
13
  function toEngineScrollOptions(options) {
13
14
  return options;
14
15
  }
15
- function VirtualListInner({ 'aria-label': ariaLabel, className, empty, emptyLabel = 'No items', estimateSize, gap, getItemProps, getScrollElement, height, initialOffset = 0, itemKey, items, listLabel, listRole = 'list', measureItems = false, onKeyDown, overscan, paddingEnd, paddingStart, renderItem, role: roleProp, scrollMargin, ssrItemCount, style, tabIndex, ...props }, forwardedRef) {
16
+ function VirtualListInner(inkraProps, forwardedRef) {
17
+ const { t } = useInkraLocale();
18
+ const { 'aria-label': ariaLabel, className, empty, emptyLabel = t('common.noItems'), estimateSize, gap, getItemProps, getScrollElement, height, initialOffset = 0, itemKey, items, listLabel, listRole = 'list', measureItems = false, onKeyDown, overscan, paddingEnd, paddingStart, renderItem, role: roleProp, scrollMargin, ssrItemCount, style, tabIndex, ...props } = inkraProps;
16
19
  const rootRef = useRef(null);
17
20
  const itemsRef = useRef(items);
18
21
  itemsRef.current = items;
@@ -26,12 +29,15 @@ function VirtualListInner({ 'aria-label': ariaLabel, className, empty, emptyLabe
26
29
  const safePaddingEnd = normalizeVirtualListSpacing(paddingEnd);
27
30
  useEffect(() => setHydrated(true), []);
28
31
  const resolveScrollElement = useCallback(() => getScrollElement?.() ?? rootRef.current, [getScrollElement]);
29
- const resolveItemKey = useCallback((index) => itemKey(itemsRef.current[index], index), [itemKey]);
32
+ const resolveItemKey = useCallback(
33
+ // The engine caches measurements by this callback, not by the items array
34
+ // or estimateSize callback. Invalidate it for immutable data/estimate updates.
35
+ (index) => itemKey(items[index], index), [items, itemKey, estimateSize]);
30
36
  const resolveEstimateSize = useCallback((index) => {
31
- const item = itemsRef.current[index];
37
+ const item = items[index];
32
38
  const value = typeof estimateSize === 'function' ? estimateSize(item, index) : estimateSize;
33
39
  return normalizeVirtualListSize(value);
34
- }, [estimateSize]);
40
+ }, [items, estimateSize]);
35
41
  const virtualizer = useVirtualizer({
36
42
  count: items.length,
37
43
  enabled: hydrated && items.length > 0,
@@ -90,10 +96,9 @@ function VirtualListInner({ 'aria-label': ariaLabel, className, empty, emptyLabe
90
96
  virtualizer.scrollToIndex(items.length - 1, { align: 'end' });
91
97
  }
92
98
  }
93
- const fallbackIndexes = getVirtualListSsrIndexes(items.length, ssrItemCount);
94
99
  const renderedRows = virtualized
95
100
  ? virtualRows.map(row => ({ index: row.index, key: row.key, start: row.start }))
96
- : fallbackIndexes.map(index => ({ index, key: itemKey(items[index], index), start: null }));
101
+ : getVirtualListSsrIndexes(items.length, ssrItemCount).map(index => ({ index, key: itemKey(items[index], index), start: null }));
97
102
  const rootStyle = externalScroll ? style : { blockSize: height, ...style };
98
103
  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
104
  const item = items[row.index];