@inkra/react 0.2.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/README.md +101 -7
  2. package/dist/Button.js +3 -2
  3. package/dist/PencilIcon.d.ts +10 -0
  4. package/dist/PencilIcon.js +83 -39
  5. package/dist/PencilLine.js +9 -1
  6. package/dist/PencilText.js +15 -9
  7. package/dist/accordion/Accordion.js +65 -5
  8. package/dist/alert-dialog/index.d.ts +1 -1
  9. package/dist/alert-dialog/index.js +18 -6
  10. package/dist/audio-player/index.d.ts +2 -0
  11. package/dist/audio-player/index.js +126 -31
  12. package/dist/calendar/index.d.ts +4 -1
  13. package/dist/calendar/index.js +31 -12
  14. package/dist/carousel/index.js +13 -6
  15. package/dist/choice-grid/index.d.ts +6 -1
  16. package/dist/choice-grid/index.js +61 -33
  17. package/dist/color-picker/GradientEditor.d.ts +39 -0
  18. package/dist/color-picker/GradientEditor.js +63 -0
  19. package/dist/color-picker/gradient.d.ts +25 -0
  20. package/dist/color-picker/gradient.js +60 -0
  21. package/dist/color-picker/index.d.ts +27 -7
  22. package/dist/color-picker/index.js +134 -32
  23. package/dist/color-swatch/index.d.ts +10 -0
  24. package/dist/color-swatch/index.js +9 -0
  25. package/dist/command/index.d.ts +24 -2
  26. package/dist/command/index.js +29 -6
  27. package/dist/content/index.d.ts +5 -0
  28. package/dist/content/index.js +12 -3
  29. package/dist/context-menu/index.d.ts +2 -2
  30. package/dist/context-menu/index.js +4 -3
  31. package/dist/data-table/index.d.ts +7 -1
  32. package/dist/data-table/index.js +64 -16
  33. package/dist/data-table/model.d.ts +2 -0
  34. package/dist/data-table/model.js +7 -0
  35. package/dist/date-picker/index.d.ts +2 -0
  36. package/dist/date-picker/index.js +10 -9
  37. package/dist/day-agenda/index.d.ts +6 -1
  38. package/dist/day-agenda/index.js +46 -21
  39. package/dist/day-agenda/layout.js +65 -9
  40. package/dist/display/ProgressRing.js +7 -3
  41. package/dist/display/index.d.ts +4 -1
  42. package/dist/display/index.js +3 -2
  43. package/dist/dockable-panel/index.js +5 -5
  44. package/dist/drawer/index.d.ts +1 -1
  45. package/dist/drawer/index.js +9 -6
  46. package/dist/dropdown-menu/index.d.ts +1 -1
  47. package/dist/dropdown-menu/index.js +3 -2
  48. package/dist/editable/index.d.ts +24 -2
  49. package/dist/editable/index.js +142 -37
  50. package/dist/empty/illustrations.d.ts +8 -0
  51. package/dist/empty/illustrations.js +78 -0
  52. package/dist/empty/index.d.ts +6 -1
  53. package/dist/empty/index.js +11 -3
  54. package/dist/flip/index.js +28 -8
  55. package/dist/form/EditablePencilText.js +34 -0
  56. package/dist/form/Field.js +11 -10
  57. package/dist/form/Input.js +1 -1
  58. package/dist/form/Textarea.d.ts +3 -1
  59. package/dist/form/Textarea.js +6 -4
  60. package/dist/hotkey/index.d.ts +1 -0
  61. package/dist/hotkey/index.js +6 -4
  62. package/dist/hover-card/index.js +1 -1
  63. package/dist/image-cropper/index.js +4 -2
  64. package/dist/image-viewer/VideoResource.d.ts +21 -0
  65. package/dist/image-viewer/VideoResource.js +194 -0
  66. package/dist/image-viewer/index.d.ts +14 -0
  67. package/dist/image-viewer/index.js +118 -65
  68. package/dist/image-viewer/lucideGeometry.d.ts +3 -0
  69. package/dist/image-viewer/lucideGeometry.js +19 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +2 -0
  72. package/dist/input-number/index.d.ts +6 -0
  73. package/dist/input-number/index.js +131 -15
  74. package/dist/input-number/model.d.ts +13 -0
  75. package/dist/input-number/model.js +50 -0
  76. package/dist/internal/cache.d.ts +4 -0
  77. package/dist/internal/cache.js +18 -1
  78. package/dist/internal/iconGeometry.d.ts +2 -0
  79. package/dist/internal/iconGeometry.js +32 -0
  80. package/dist/internal/renderTokens.d.ts +4 -0
  81. package/dist/internal/renderTokens.js +4 -0
  82. package/dist/internal/stroke.d.ts +7 -0
  83. package/dist/internal/stroke.js +63 -7
  84. package/dist/internal/surfaceBudget.d.ts +20 -0
  85. package/dist/internal/surfaceBudget.js +69 -0
  86. package/dist/internal/surfaceRenderer.js +87 -28
  87. package/dist/internal/surfaceVisibility.d.ts +3 -0
  88. package/dist/internal/surfaceVisibility.js +60 -0
  89. package/dist/internal/textRenderer.d.ts +2 -0
  90. package/dist/internal/textRenderer.js +3 -3
  91. package/dist/internal/usePencilSurface.js +17 -1
  92. package/dist/internal/wash.js +4 -3
  93. package/dist/journey-map/terrain.js +10 -5
  94. package/dist/layout/index.js +1 -1
  95. package/dist/log/index.js +29 -22
  96. package/dist/markdown/CodeBlock.d.ts +19 -0
  97. package/dist/markdown/CodeBlock.js +86 -0
  98. package/dist/markdown/Deletion.d.ts +2 -0
  99. package/dist/markdown/Deletion.js +69 -0
  100. package/dist/markdown/icons.d.ts +14 -0
  101. package/dist/markdown/icons.js +16 -0
  102. package/dist/markdown/index.d.ts +17 -0
  103. package/dist/markdown/index.js +175 -0
  104. package/dist/node-graph/index.d.ts +2 -0
  105. package/dist/node-graph/index.js +49 -25
  106. package/dist/number-ticker/index.d.ts +11 -9
  107. package/dist/number-ticker/index.js +14 -11
  108. package/dist/number-ticker/model.d.ts +38 -5
  109. package/dist/number-ticker/model.js +139 -10
  110. package/dist/number-ticker/precision.d.ts +7 -0
  111. package/dist/number-ticker/precision.js +141 -0
  112. package/dist/overflow-list/index.js +5 -2
  113. package/dist/overlay/Overlay.d.ts +1 -1
  114. package/dist/overlay/Overlay.js +5 -3
  115. package/dist/overlay/Popover.d.ts +1 -1
  116. package/dist/overlay/Popover.js +6 -5
  117. package/dist/overlay/themeBridge.d.ts +12 -1
  118. package/dist/overlay/themeBridge.js +41 -3
  119. package/dist/pagination/index.d.ts +10 -1
  120. package/dist/pagination/index.js +6 -4
  121. package/dist/pan-zoom/index.d.ts +2 -0
  122. package/dist/pan-zoom/index.js +7 -6
  123. package/dist/pencil.d.ts +1 -0
  124. package/dist/pencil.js +4 -4
  125. package/dist/radar/index.d.ts +4 -0
  126. package/dist/radar/index.js +41 -21
  127. package/dist/rating/index.d.ts +4 -0
  128. package/dist/rating/index.js +128 -24
  129. package/dist/resizable/index.js +1 -1
  130. package/dist/scroll-area/index.d.ts +1 -1
  131. package/dist/scroll-area/index.js +5 -2
  132. package/dist/select/MultipleSelect.js +5 -4
  133. package/dist/select/SearchableSelect.js +61 -6
  134. package/dist/select/Select.d.ts +1 -0
  135. package/dist/select/Select.js +3 -1
  136. package/dist/select/StepperSelect.d.ts +29 -0
  137. package/dist/select/StepperSelect.js +119 -0
  138. package/dist/select/index.d.ts +2 -0
  139. package/dist/select/index.js +1 -0
  140. package/dist/select/stepperModel.d.ts +7 -0
  141. package/dist/select/stepperModel.js +29 -0
  142. package/dist/selection/Checkbox.js +4 -3
  143. package/dist/selection/RadioGroup.js +29 -13
  144. package/dist/selection/Switch.js +4 -3
  145. package/dist/selection/useNativeChecked.d.ts +2 -1
  146. package/dist/selection/useNativeChecked.js +26 -8
  147. package/dist/slider/index.js +50 -6
  148. package/dist/sortable/index.js +15 -6
  149. package/dist/sparkline/index.js +17 -2
  150. package/dist/sparkline/model.js +11 -4
  151. package/dist/styles/accordion.css +228 -0
  152. package/dist/styles/alert-dialog.css +139 -0
  153. package/dist/styles/audio-player.css +22 -0
  154. package/dist/styles/base.css +638 -0
  155. package/dist/styles/breadcrumb.css +7 -0
  156. package/dist/styles/calendar.css +62 -0
  157. package/dist/styles/carousel.css +173 -0
  158. package/dist/styles/choice-grid.css +226 -0
  159. package/dist/styles/color-picker.css +281 -0
  160. package/dist/styles/color-swatch.css +47 -0
  161. package/dist/styles/command.css +52 -0
  162. package/dist/styles/content.css +79 -0
  163. package/dist/styles/context-menu.css +35 -0
  164. package/dist/styles/data-table.css +31 -0
  165. package/dist/styles/date-picker.css +107 -0
  166. package/dist/styles/day-agenda.css +30 -0
  167. package/dist/styles/display.css +304 -0
  168. package/dist/styles/dockable-panel.css +110 -0
  169. package/dist/styles/drawer.css +133 -0
  170. package/dist/styles/dropdown-menu.css +68 -0
  171. package/dist/styles/editable.css +145 -0
  172. package/dist/styles/empty.css +108 -0
  173. package/dist/styles/flip.css +101 -0
  174. package/dist/styles/form.css +634 -0
  175. package/dist/styles/heatmap.css +256 -0
  176. package/dist/styles/hotkey.css +189 -0
  177. package/dist/styles/hover-card.css +27 -0
  178. package/dist/styles/image-cropper.css +205 -0
  179. package/dist/styles/image-viewer.css +422 -0
  180. package/dist/styles/input-number.css +21 -0
  181. package/dist/styles/journey-map.css +19 -0
  182. package/dist/styles/keypad.css +92 -0
  183. package/dist/styles/layout.css +72 -0
  184. package/dist/styles/leaflet.css +661 -0
  185. package/dist/styles/log.css +203 -0
  186. package/dist/styles/markdown.css +379 -0
  187. package/dist/styles/node-graph.css +142 -0
  188. package/dist/styles/number-ticker.css +83 -0
  189. package/dist/styles/overflow-list.css +51 -0
  190. package/dist/styles/overlay.css +284 -0
  191. package/dist/styles/pagination.css +21 -0
  192. package/dist/styles/pan-zoom.css +136 -0
  193. package/dist/styles/radar.css +1 -0
  194. package/dist/styles/rating.css +121 -0
  195. package/dist/styles/resizable.css +108 -0
  196. package/dist/styles/scroll-area.css +122 -0
  197. package/dist/styles/select.css +557 -0
  198. package/dist/styles/selection.css +264 -0
  199. package/dist/styles/slider.css +145 -0
  200. package/dist/styles/slot.css +109 -0
  201. package/dist/styles/sortable.css +159 -0
  202. package/dist/styles/sparkline.css +24 -0
  203. package/dist/styles/steps.css +27 -0
  204. package/dist/styles/tabs.css +79 -0
  205. package/dist/styles/tag-input.css +213 -0
  206. package/dist/styles/time-picker.css +66 -0
  207. package/dist/styles/timeline.css +24 -0
  208. package/dist/styles/toast.css +160 -0
  209. package/dist/styles/toggle-group.css +107 -0
  210. package/dist/styles/tooltip.css +78 -0
  211. package/dist/styles/transfer.css +148 -0
  212. package/dist/styles/tree-select.css +131 -0
  213. package/dist/styles/tree.css +59 -0
  214. package/dist/styles/typewriter.css +62 -0
  215. package/dist/styles/upload.css +60 -0
  216. package/dist/styles/virtual-list.css +112 -0
  217. package/dist/styles.css +2241 -443
  218. package/dist/tabs/index.d.ts +9 -0
  219. package/dist/tabs/index.js +66 -16
  220. package/dist/tabs/useOverflow.d.ts +20 -0
  221. package/dist/tabs/useOverflow.js +145 -0
  222. package/dist/tag-input/index.d.ts +7 -2
  223. package/dist/tag-input/index.js +70 -26
  224. package/dist/time-picker/index.js +38 -20
  225. package/dist/toast/index.d.ts +19 -11
  226. package/dist/toast/index.js +48 -8
  227. package/dist/toast/manager.d.ts +42 -0
  228. package/dist/toast/manager.js +85 -0
  229. package/dist/toast/useToastNavigation.d.ts +3 -0
  230. package/dist/toast/useToastNavigation.js +75 -0
  231. package/dist/tokens.css +4 -0
  232. package/dist/tooltip/Tooltip.d.ts +1 -1
  233. package/dist/tooltip/Tooltip.js +6 -5
  234. package/dist/transfer/index.d.ts +5 -0
  235. package/dist/transfer/index.js +93 -29
  236. package/dist/transfer/model.d.ts +4 -0
  237. package/dist/transfer/model.js +37 -11
  238. package/dist/tree/index.d.ts +5 -1
  239. package/dist/tree/index.js +5 -5
  240. package/dist/tree-select/index.js +67 -30
  241. package/dist/tree-select/model.d.ts +11 -5
  242. package/dist/tree-select/model.js +111 -83
  243. package/dist/typewriter/index.js +10 -10
  244. package/dist/upload/index.js +30 -16
  245. package/dist/virtual-list/index.js +10 -7
  246. package/package.json +416 -5
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from 'react';
3
- import { Button } from '../Button.js';
2
+ import { forwardRef, memo, useCallback, useEffect, useId, useImperativeHandle, useMemo, useRef, useState } from 'react';
3
+ import { Button, ButtonGroup } from '../Button.js';
4
4
  import { useResolvedControlProps } from '../form/controlProps.js';
5
5
  import { mergeAriaIds, useFieldContext } from '../form/context.js';
6
6
  import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
@@ -19,15 +19,19 @@ const pad = (value) => {
19
19
  return `${whole.padStart(2, '0')}${fraction ? `.${fraction}` : ''}`;
20
20
  };
21
21
  const times = (length, start = 0) => Array.from({ length }, (_, index) => index + start);
22
+ const hourOptions24 = times(24);
23
+ const hourOptions12 = times(12, 1);
24
+ const minuteSecondOptions = times(60);
25
+ const periodOptions = [0, 1];
22
26
  function partsOf(value) {
23
27
  if (!/^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d{1,3})?)?$/.test(value))
24
28
  return undefined;
25
29
  const [hour, minute, second = 0] = value.split(':').map(Number);
26
30
  return [hour, minute, second];
27
31
  }
28
- function TimeColumn({ name, label, value, options, onChange, format = pad }) {
32
+ const TimeColumn = memo(function TimeColumn({ name, label, value, options, onChange, format = pad }) {
29
33
  return _jsx(RadioGroup, { name: name, label: label, value: String(value), variant: "segmented", className: "inkra-time-picker__column", onValueChange: next => onChange(Number(next)), children: options.map(option => _jsx(RadioGroupItem, { value: String(option), label: format(option) }, option)) });
30
- }
34
+ });
31
35
  export const TimePicker = forwardRef(function TimePicker(allProps, forwardedRef) {
32
36
  const { value, defaultValue, onValueChange, onChange, onClear, clearable = true, clearButtonLabel, hourCycle = 'h24', locale, lang, showSeconds = false, step, min, max, name, form, id, disabled, readOnly, required, invalid, 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, style, size = 'md', fullWidth = true, startIcon, endIcon, endAction, startAction, prefix, suffix, placeholder, seed, variant = 'outline', textAlign = 'start', loading, classNames, ...nativeProps } = allProps;
33
37
  const generatedId = useId();
@@ -40,6 +44,8 @@ export const TimePicker = forwardRef(function TimePicker(allProps, forwardedRef)
40
44
  const [internalValue, setInternalValue] = useState(() => String(defaultValue ?? ''));
41
45
  const [open, setOpen] = useState(false);
42
46
  const [draft, setDraft] = useState('00:00');
47
+ const draftRef = useRef(draft);
48
+ draftRef.current = draft;
43
49
  const [draftValid, setDraftValid] = useState(true);
44
50
  const [constraintInvalid, setConstraintInvalid] = useState(false);
45
51
  useImperativeHandle(forwardedRef, () => inputRef.current);
@@ -59,16 +65,20 @@ export const TimePicker = forwardRef(function TimePicker(allProps, forwardedRef)
59
65
  : { title: 'Choose time', hour: 'Hour', minute: 'Minute', second: 'Second', period: 'Period', am: 'AM', pm: 'PM',
60
66
  done: 'Apply', cancel: 'Cancel', clear: 'Clear time', invalid: 'Choose a time within the allowed range and step.' };
61
67
  const currentParts = partsOf(currentValue);
68
+ const hasCurrentParts = currentParts !== undefined;
69
+ const formatter = useMemo(() => hasCurrentParts ? new Intl.DateTimeFormat(resolvedLocale, {
70
+ hour: '2-digit', minute: '2-digit', ...(secondsVisible ? { second: '2-digit' } : {}),
71
+ hourCycle: hourCycle === 'h12' ? 'h12' : 'h23',
72
+ }) : null, [resolvedLocale, secondsVisible, hourCycle, hasCurrentParts]);
62
73
  const displayValue = currentParts
63
- ? new Intl.DateTimeFormat(resolvedLocale, {
64
- hour: '2-digit', minute: '2-digit', ...(secondsVisible ? { second: '2-digit' } : {}),
65
- hourCycle: hourCycle === 'h12' ? 'h12' : 'h23',
66
- }).format(new Date(2000, 0, 1, ...currentParts))
74
+ ? formatter.format(new Date(2000, 0, 1, ...currentParts))
67
75
  : placeholder ?? copy.title;
68
76
  const canClear = clearable && Boolean(currentValue) && !state.disabled && !state.readOnly;
77
+ const buttonVariant = variant === 'soft' ? 'secondary' : variant === 'underline' ? 'outline' : variant;
78
+ const clearSize = size === 'sm' ? 'icon-sm' : size === 'lg' ? 'icon-lg' : 'icon';
69
79
  const valueId = field ? `${state.id}-value` : undefined;
70
80
  const [hour, minute, second] = partsOf(draft) ?? [0, 0, 0];
71
- const validDraft = (candidate = draft) => {
81
+ const validDraft = useCallback((candidate = draftRef.current) => {
72
82
  // Native validation owns overnight ranges, fractional steps and step bases.
73
83
  const input = document.createElement('input');
74
84
  input.type = 'time';
@@ -81,7 +91,7 @@ export const TimePicker = forwardRef(function TimePicker(allProps, forwardedRef)
81
91
  input.required = true;
82
92
  input.value = candidate;
83
93
  return input.validity.valid;
84
- };
94
+ }, [min, max, resolvedStep, stepBase]);
85
95
  const updateOpen = (next) => {
86
96
  if (next && (state.disabled || state.readOnly))
87
97
  return;
@@ -93,13 +103,21 @@ export const TimePicker = forwardRef(function TimePicker(allProps, forwardedRef)
93
103
  }
94
104
  setOpen(next);
95
105
  };
96
- const updatePart = (part, next) => {
97
- const values = [hour, minute, second];
106
+ const updatePart = useCallback((part, next) => {
107
+ const values = partsOf(draftRef.current) ?? [0, 0, 0];
98
108
  values[part] = next;
99
109
  const nextDraft = values.slice(0, secondsVisible ? 3 : 2).map(pad).join(':');
110
+ draftRef.current = nextDraft;
100
111
  setDraft(nextDraft);
101
112
  setDraftValid(validDraft(nextDraft));
102
- };
113
+ }, [secondsVisible, validDraft]);
114
+ const period = hour >= 12 ? 1 : 0;
115
+ const hourWithinPeriod = hour % 12;
116
+ const changeHour = useCallback((next) => updatePart(0, hourCycle === 'h12' ? next % 12 + period * 12 : next), [hourCycle, period, updatePart]);
117
+ const changeMinute = useCallback((next) => updatePart(1, next), [updatePart]);
118
+ const changeSecond = useCallback((next) => updatePart(2, next), [updatePart]);
119
+ const changePeriod = useCallback((next) => updatePart(0, hourWithinPeriod + next * 12), [hourWithinPeriod, updatePart]);
120
+ const formatPeriod = useCallback((next) => next ? copy.pm : copy.am, [copy.pm, copy.am]);
103
121
  const commit = (next, reason) => {
104
122
  if (!controlled)
105
123
  setInternalValue(next);
@@ -107,7 +125,7 @@ export const TimePicker = forwardRef(function TimePicker(allProps, forwardedRef)
107
125
  onValueChange?.(next, { reason });
108
126
  setOpen(false);
109
127
  };
110
- useEffect(() => { setDraftValid(validDraft()); }, [draft, min, max, resolvedStep, stepBase]);
128
+ useEffect(() => { setDraftValid(validDraft()); }, [draft, validDraft]);
111
129
  useBrowserLayoutEffect(() => {
112
130
  // React's controlled value attribute must not move the native step base on each edit.
113
131
  if (inputRef.current)
@@ -134,11 +152,11 @@ export const TimePicker = forwardRef(function TimePicker(allProps, forwardedRef)
134
152
  }, [controlled, currentValue, defaultValue, form]);
135
153
  useEffect(() => { if (state.disabled || state.readOnly)
136
154
  setOpen(false); }, [state.disabled, state.readOnly]);
137
- return _jsxs(Popover.Root, { open: open, onOpenChange: updateOpen, children: [_jsxs("div", { className: ['inkra-time-picker', classNames?.root, className].filter(Boolean).join(' '), style: style, dir: nativeProps.dir, "data-full-width": fullWidth, "data-invalid": state.invalid || constraintInvalid || undefined, "data-readonly": state.readOnly || undefined, "data-disabled": state.disabled || undefined, children: [startAction, _jsx(Popover.Trigger, { asChild: true, children: _jsxs(Button, { className: ['inkra-time-picker__trigger', classNames?.control].filter(Boolean).join(' '), id: state.id, ref: triggerRef, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": mergeAriaIds(state.describedBy, valueId), "aria-invalid": constraintInvalid || state.ariaInvalid, "aria-errormessage": state.errorMessage, "aria-required": state.required || undefined, "aria-readonly": state.readOnly || undefined, disabled: state.disabled, fullWidth: true, align: textAlign, variant: variant === 'soft' ? 'secondary' : variant === 'underline' ? 'outline' : variant, size: size, seed: seed, loading: loading, autoFocus: nativeProps.autoFocus, startIcon: startIcon ?? _jsx(PencilIcon, { paths: clockPaths, sourceSize: 24, size: 22 }), endIcon: endIcon, children: [prefix, _jsx(PencilText, { children: displayValue }), suffix] }) }), valueId && _jsx("span", { className: "inkra-time-picker__accessible-value", id: valueId, children: displayValue }), clearable && _jsx(Button, { className: "inkra-time-picker__clear", "aria-label": clearButtonLabel ?? copy.clear, title: clearButtonLabel ?? copy.clear, "aria-hidden": !canClear || undefined, disabled: !canClear, tabIndex: canClear ? 0 : -1, variant: "outline", size: "icon", onClick: event => {
138
- commit('', 'clear');
139
- onClear?.(event);
140
- triggerRef.current?.focus({ preventScroll: true });
141
- }, children: _jsx(PencilIcon, { paths: clearPaths, sourceSize: 24, size: 22 }) }), endAction, _jsx("input", { ...nativeProps, ref: inputRef, type: "time", className: "inkra-time-picker__native", id: `${state.id}-native`, "aria-hidden": "true", tabIndex: -1, name: name, form: form, value: currentValue, min: min, max: max, step: resolvedStep, lang: lang ?? resolvedLocale, "data-hour-cycle": hourCycle, disabled: state.disabled, readOnly: state.readOnly, required: state.required, onChange: event => {
155
+ return _jsxs(Popover.Root, { open: open, onOpenChange: updateOpen, children: [_jsxs("div", { className: ['inkra-time-picker', classNames?.root, className].filter(Boolean).join(' '), style: style, dir: nativeProps.dir, "data-full-width": fullWidth, "data-invalid": state.invalid || constraintInvalid || undefined, "data-readonly": state.readOnly || undefined, "data-disabled": state.disabled || undefined, "data-size": size, children: [startAction, _jsxs(ButtonGroup, { "aria-label": copy.title, className: ['inkra-time-picker__control', classNames?.control].filter(Boolean).join(' '), "data-size": size, fullWidth: fullWidth, size: size, variant: buttonVariant, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsxs(Button, { className: "inkra-time-picker__trigger", id: state.id, ref: triggerRef, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": mergeAriaIds(state.describedBy, valueId), "aria-invalid": constraintInvalid || state.ariaInvalid, "aria-errormessage": state.errorMessage, "aria-required": state.required || undefined, "aria-readonly": state.readOnly || undefined, disabled: state.disabled, fullWidth: fullWidth, align: textAlign, seed: seed, loading: loading, autoFocus: nativeProps.autoFocus, startIcon: startIcon ?? _jsx(PencilIcon, { paths: clockPaths, sourceSize: 24, size: 22 }), endIcon: endIcon, children: [prefix, _jsx(PencilText, { children: displayValue }), suffix] }) }), clearable && _jsx(Button, { className: "inkra-time-picker__clear", "aria-label": clearButtonLabel ?? copy.clear, title: clearButtonLabel ?? copy.clear, "aria-hidden": !canClear || undefined, disabled: !canClear, tabIndex: canClear ? 0 : -1, variant: "ghost", size: clearSize, onPointerDown: event => event.preventDefault(), onClick: event => {
156
+ commit('', 'clear');
157
+ onClear?.(event);
158
+ triggerRef.current?.focus({ preventScroll: true });
159
+ }, children: _jsx(PencilIcon, { paths: clearPaths, sourceSize: 24, size: 22 }) })] }), valueId && _jsx("span", { className: "inkra-time-picker__accessible-value", id: valueId, children: displayValue }), endAction, _jsx("input", { ...nativeProps, ref: inputRef, type: "time", className: "inkra-time-picker__native", id: `${state.id}-native`, "aria-hidden": "true", tabIndex: -1, name: name, form: form, value: currentValue, min: min, max: max, step: resolvedStep, lang: lang ?? resolvedLocale, "data-hour-cycle": hourCycle, disabled: state.disabled, readOnly: state.readOnly, required: state.required, onChange: event => {
142
160
  if (!controlled)
143
161
  setInternalValue(event.currentTarget.value);
144
162
  onChange?.(event);
@@ -161,7 +179,7 @@ export const TimePicker = forwardRef(function TimePicker(allProps, forwardedRef)
161
179
  (content?.querySelector('input:checked')
162
180
  ?? content?.querySelector('input'))?.focus({ preventScroll: true });
163
181
  });
164
- }, children: [_jsx(Popover.Title, { children: copy.title }), _jsx(PencilLine, {}), _jsxs("div", { className: "inkra-time-picker__columns", children: [_jsx(TimeColumn, { name: `${generatedId}-hour`, label: copy.hour, value: hourCycle === 'h12' ? hour % 12 || 12 : hour, options: hourCycle === 'h12' ? times(12, 1) : times(24), onChange: next => updatePart(0, hourCycle === 'h12' ? next % 12 + (hour >= 12 ? 12 : 0) : next) }), _jsx(TimeColumn, { name: `${generatedId}-minute`, label: copy.minute, value: minute, options: times(60), onChange: next => updatePart(1, next) }), secondsVisible && _jsx(TimeColumn, { name: `${generatedId}-second`, label: copy.second, value: Math.floor(second), options: times(60), onChange: next => updatePart(2, next) }), hourCycle === 'h12' && _jsx(TimeColumn, { name: `${generatedId}-period`, label: copy.period, value: hour >= 12 ? 1 : 0, options: [0, 1], format: next => next ? copy.pm : copy.am, onChange: next => updatePart(0, hour % 12 + next * 12) })] }), !draftValid && _jsx(PencilText, { as: "p", className: "inkra-time-picker__error", role: "status", children: copy.invalid }), _jsx(PencilLine, {}), _jsxs("div", { className: "inkra-time-picker__footer", children: [_jsx(Popover.Close, { asChild: true, children: _jsx(Button, { variant: "outline", children: copy.cancel }) }), _jsx(Button, { disabled: !draftValid || state.disabled || state.readOnly, onClick: () => {
182
+ }, children: [_jsx(Popover.Title, { children: copy.title }), _jsx(PencilLine, {}), _jsxs("div", { className: "inkra-time-picker__columns", children: [_jsx(TimeColumn, { name: `${generatedId}-hour`, label: copy.hour, value: hourCycle === 'h12' ? hour % 12 || 12 : hour, options: hourCycle === 'h12' ? hourOptions12 : hourOptions24, onChange: changeHour }), _jsx(TimeColumn, { name: `${generatedId}-minute`, label: copy.minute, value: minute, options: minuteSecondOptions, onChange: changeMinute }), secondsVisible && _jsx(TimeColumn, { name: `${generatedId}-second`, label: copy.second, value: Math.floor(second), options: minuteSecondOptions, onChange: changeSecond }), hourCycle === 'h12' && _jsx(TimeColumn, { name: `${generatedId}-period`, label: copy.period, value: period, options: periodOptions, format: formatPeriod, onChange: changePeriod })] }), !draftValid && _jsx(PencilText, { as: "p", className: "inkra-time-picker__error", role: "status", children: copy.invalid }), _jsx(PencilLine, {}), _jsxs("div", { className: "inkra-time-picker__footer", children: [_jsx(Popover.Close, { asChild: true, children: _jsx(Button, { variant: "outline", children: copy.cancel }) }), _jsx(Button, { disabled: !draftValid || state.disabled || state.readOnly, onClick: () => {
165
183
  if (validDraft())
166
184
  commit(draft, 'input');
167
185
  }, children: copy.done })] })] }) })] });
@@ -1,27 +1,35 @@
1
- import { Toast as BaseToast } from '@base-ui/react/toast';
2
- import type { ToastManager as BaseToastManager } from '@base-ui/react/toast';
3
1
  import type { ReactNode } from 'react';
4
- export type ToastPlacement = 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
2
+ import { createToastManager } from './manager.js';
3
+ import type { ToastManager, ToastObject, ToastPlacement, ToastVariant } from './manager.js';
4
+ export { createToastManager } from './manager.js';
5
+ export type { ToastManager, ToastObject, ToastPlacement, ToastVariant, ToastAddOptions, ToastUpdateOptions, ToastPromiseOptions } from './manager.js';
5
6
  export type ToastTone = 'neutral' | 'success' | 'warning' | 'danger' | 'info';
6
7
  export type ToastSwipeDirection = 'up' | 'down' | 'left' | 'right';
7
- export type ToastManager<Data extends object = any> = BaseToastManager<Data>;
8
8
  export interface ToastProviderProps {
9
9
  children?: ReactNode;
10
10
  timeout?: number;
11
11
  limit?: number;
12
12
  toastManager?: ToastManager;
13
13
  placement?: ToastPlacement;
14
+ variant?: ToastVariant;
15
+ /** Per-position overrides; limit otherwise applies independently to every queue. */
16
+ limits?: Partial<Record<ToastPlacement, number>>;
14
17
  swipeDirection?: ToastSwipeDirection | ToastSwipeDirection[];
15
18
  closeLabel?: string;
16
- viewportLabel?: string;
19
+ viewportLabel?: string | ((placement: ToastPlacement) => string);
17
20
  viewportClassName?: string;
18
21
  }
19
- declare function Provider({ children, closeLabel, limit, placement, swipeDirection, timeout, toastManager, viewportClassName, viewportLabel, }: ToastProviderProps): import("react").JSX.Element;
20
- export declare const useToastManager: typeof BaseToast.useToastManager;
21
- export declare const createToastManager: typeof BaseToast.createToastManager;
22
+ declare function Provider({ children, closeLabel, limit, limits, placement, variant, swipeDirection, timeout, toastManager, viewportClassName, viewportLabel, }: ToastProviderProps): import("react").JSX.Element;
23
+ export declare function useToastManager<Data extends object = any>(): {
24
+ toasts: ToastObject<Data>[];
25
+ add: <T extends Data = Data>(options: import("./manager.js").ToastAddOptions<T>) => string;
26
+ update: <T extends Data = Data>(id: string, options: import("./manager.js").ToastUpdateOptions<T> | ((previous: ToastObject<T>) => import("./manager.js").ToastUpdateOptions<T>)) => void;
27
+ close: (id?: string) => void;
28
+ closeAll: (placement?: ToastPlacement) => void;
29
+ promise: <Value, T extends Data = Data>(value: Promise<Value>, options: import("./manager.js").ToastPromiseOptions<Value, T>) => Promise<Value>;
30
+ };
22
31
  export declare const Toast: {
23
32
  Provider: typeof Provider;
24
- useToastManager: typeof BaseToast.useToastManager;
25
- createToastManager: typeof BaseToast.createToastManager;
33
+ useToastManager: typeof useToastManager;
34
+ createToastManager: typeof createToastManager;
26
35
  };
27
- export {};
@@ -1,10 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Toast as BaseToast } from '@base-ui/react/toast';
3
+ import { createContext, useContext, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
3
4
  import { Button } from '../Button.js';
4
5
  import { PencilIcon } from '../PencilIcon.js';
5
6
  import { PencilSurface } from '../PencilSurface.js';
6
7
  import { PencilText } from '../PencilText.js';
8
+ import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
7
9
  import { PortalThemeBridge, PortalThemeProvider, usePortalTriggerRef, } from '../overlay/themeBridge.js';
10
+ import { createToastManager, getToastRouter, toastPlacements } from './manager.js';
11
+ import { useToastNavigation } from './useToastNavigation.js';
12
+ export { createToastManager } from './manager.js';
13
+ const ManagerContext = createContext(null);
14
+ const emptyToasts = [];
15
+ const getServerSnapshot = () => emptyToasts;
8
16
  const closePaths = [
9
17
  { d: 'M6 6 18 18' },
10
18
  { d: 'M18 6 6 18' },
@@ -13,6 +21,8 @@ function text(content) {
13
21
  return typeof content === 'string' ? _jsx(PencilText, { children: content }) : content;
14
22
  }
15
23
  function toneOf(type) {
24
+ if (type === 'error')
25
+ return 'danger';
16
26
  return type === 'success' || type === 'warning' || type === 'danger' || type === 'info'
17
27
  ? type
18
28
  : 'neutral';
@@ -21,16 +31,46 @@ function ToastThemeAnchor() {
21
31
  const ref = usePortalTriggerRef(null);
22
32
  return _jsx("span", { "aria-hidden": "true", className: "inkra-toast__theme-anchor", ref: ref });
23
33
  }
24
- function ToastQueue({ closeLabel, placement, swipeDirection, viewportClassName, viewportLabel, }) {
25
- const { toasts } = BaseToast.useToastManager();
26
- return (_jsx(BaseToast.Portal, { children: _jsx(PortalThemeBridge, { children: _jsx(BaseToast.Viewport, { "aria-label": viewportLabel, className: ['inkra-toast__viewport', viewportClassName].filter(Boolean).join(' '), "data-placement": placement, children: toasts.map((toastItem) => {
34
+ function ToastDefaults({ manager, placement, variant }) {
35
+ const router = getToastRouter(manager);
36
+ useBrowserLayoutEffect(() => { router.defaults = { placement, variant }; }, [router, placement, variant]);
37
+ return null;
38
+ }
39
+ function ToastQueue({ closeLabel, placement, swipeDirection, viewportClassName, viewportLabel, manager, }) {
40
+ const controller = BaseToast.useToastManager();
41
+ const { toasts } = controller;
42
+ const router = getToastRouter(manager);
43
+ useBrowserLayoutEffect(() => { router.controllers.set(placement, controller); }, [router, placement, controller]);
44
+ useBrowserLayoutEffect(() => () => { router.controllers.delete(placement); }, [router, placement]);
45
+ useBrowserLayoutEffect(() => { router.report(placement, toasts); }, [router, placement, toasts]);
46
+ useEffect(() => () => { router.report(placement, emptyToasts); }, [router, placement]);
47
+ const directions = swipeDirection ?? (placement.startsWith('top') ? ['up', 'left', 'right'] : placement.startsWith('bottom') ? ['down', 'left', 'right'] : ['left', 'right']);
48
+ if (!toasts.length)
49
+ return null;
50
+ return (_jsx(BaseToast.Portal, { children: _jsx(PortalThemeBridge, { children: _jsx(BaseToast.Viewport, { ref: element => { if (element)
51
+ router.viewports.set(placement, element);
52
+ else
53
+ router.viewports.delete(placement); }, "aria-label": typeof viewportLabel === 'function' ? viewportLabel(placement) : viewportLabel, className: ['inkra-toast__viewport', viewportClassName].filter(Boolean).join(' '), "data-placement": placement, children: toasts.map((toastItem) => {
27
54
  const tone = toneOf(toastItem.type);
28
- return (_jsxs(BaseToast.Root, { className: "inkra-toast__root", "data-tone": tone, swipeDirection: swipeDirection, toast: toastItem, children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-toast__surface", seed: `toast-${toastItem.id}-${toastItem.updateKey ?? 0}`, variant: tone === 'neutral' ? 'paper' : 'quiet' }), _jsxs(BaseToast.Content, { className: "inkra-toast__content", children: [_jsxs("div", { className: "inkra-toast__copy", children: [toastItem.title != null && (_jsx(BaseToast.Title, { className: "inkra-toast__title", children: text(toastItem.title) })), toastItem.description != null && (_jsx(BaseToast.Description, { className: "inkra-toast__description", children: text(toastItem.description) }))] }), _jsxs("div", { className: "inkra-toast__actions", children: [toastItem.actionProps?.children != null && (_jsx(BaseToast.Action, { className: "inkra-toast__action", render: _jsx(Button, { size: "sm", variant: "outline" }) })), _jsx(BaseToast.Close, { "aria-label": closeLabel, className: "inkra-toast__close", render: _jsx(Button, { size: "icon-sm", variant: "ghost" }), children: _jsx(PencilIcon, { paths: closePaths, size: 20, sourceSize: 24, decorative: true }) })] })] })] }, toastItem.id));
55
+ const variant = !toastItem.variant || toastItem.variant === 'auto'
56
+ ? (/^(top|bottom)-(start|end)$/.test(placement) ? 'card' : 'compact')
57
+ : toastItem.variant;
58
+ return (_jsxs(BaseToast.Root, { className: "inkra-toast__root", "data-tone": tone, "data-variant": variant, "data-toast-id": toastItem.id, swipeDirection: directions, toast: toastItem, children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-toast__surface", seed: `toast-${toastItem.id}`, variant: variant === 'compact' ? 'full' : tone === 'neutral' ? 'paper' : 'quiet' }), variant === 'compact' && _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-toast__wash", seed: `toast-wash-${toastItem.id}`, variant: "selection" }), _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-toast__focus", seed: `toast-focus-${toastItem.id}`, variant: "outline" }), _jsxs(BaseToast.Content, { className: "inkra-toast__content", children: [_jsxs("div", { className: "inkra-toast__copy", children: [toastItem.title != null && (_jsx(BaseToast.Title, { className: "inkra-toast__title", children: text(toastItem.title) })), toastItem.description != null && (_jsx(BaseToast.Description, { className: "inkra-toast__description", children: text(toastItem.description) }))] }), _jsxs("div", { className: "inkra-toast__actions", children: [toastItem.actionProps?.children != null && (_jsx(BaseToast.Action, { className: "inkra-toast__action", render: _jsx(Button, { size: "sm", variant: "outline" }) })), _jsx(BaseToast.Close, { "aria-label": closeLabel, className: "inkra-toast__close", render: _jsx(Button, { size: "icon-sm", variant: "ghost" }), children: _jsx(PencilIcon, { paths: closePaths, size: 20, sourceSize: 24, decorative: true }) })] })] })] }, toastItem.id));
29
59
  }) }) }) }));
30
60
  }
31
- function Provider({ children, closeLabel = 'Close notification', limit, placement = 'bottom-end', swipeDirection = ['down', 'right'], timeout, toastManager, viewportClassName, viewportLabel = 'Notifications', }) {
32
- return (_jsx(PortalThemeProvider, { children: _jsxs(BaseToast.Provider, { limit: limit, timeout: timeout, toastManager: toastManager, children: [_jsx(ToastThemeAnchor, {}), children, _jsx(ToastQueue, { closeLabel: closeLabel, placement: placement, swipeDirection: swipeDirection, viewportClassName: viewportClassName, viewportLabel: viewportLabel })] }) }));
61
+ function Provider({ children, closeLabel = 'Close notification', limit, limits, placement = 'bottom-end', variant = 'auto', swipeDirection, timeout, toastManager, viewportClassName, viewportLabel = 'Notifications', }) {
62
+ const [localManager] = useState(() => createToastManager());
63
+ const manager = toastManager ?? localManager;
64
+ const router = getToastRouter(manager);
65
+ useToastNavigation(manager);
66
+ return (_jsx(PortalThemeProvider, { children: _jsxs(ManagerContext.Provider, { value: manager, children: [_jsx(ToastDefaults, { manager: manager, placement: placement, variant: variant }), _jsx(ToastThemeAnchor, {}), toastPlacements.map(position => (_jsx(BaseToast.Provider, { limit: limits?.[position] ?? limit, timeout: timeout, toastManager: router.queues[position], children: _jsx(ToastQueue, { manager: manager, closeLabel: closeLabel, placement: position, swipeDirection: swipeDirection, viewportClassName: viewportClassName, viewportLabel: viewportLabel }) }, position))), children] }) }));
67
+ }
68
+ export function useToastManager() {
69
+ const manager = useContext(ManagerContext);
70
+ if (!manager)
71
+ throw new Error('useToastManager must be used within Toast.Provider.');
72
+ const router = getToastRouter(manager);
73
+ const toasts = useSyncExternalStore(router.subscribe, router.getSnapshot, getServerSnapshot);
74
+ return useMemo(() => ({ ...manager, toasts }), [manager, toasts]);
33
75
  }
34
- export const useToastManager = BaseToast.useToastManager;
35
- export const createToastManager = BaseToast.createToastManager;
36
76
  export const Toast = { Provider, useToastManager, createToastManager };
@@ -0,0 +1,42 @@
1
+ import type { ToastManagerAddOptions as BaseAddOptions, ToastManagerUpdateOptions as BaseUpdateOptions, ToastManagerPromiseOptions as BasePromiseOptions, ToastObject as BaseToastObject } from '@base-ui/react/toast';
2
+ export declare const toastPlacements: readonly ["top-start", "top-center", "top-end", "center-start", "center", "center-end", "bottom-start", "bottom-center", "bottom-end"];
3
+ export type ToastPlacement = typeof toastPlacements[number];
4
+ export type ToastVariant = 'auto' | 'card' | 'compact';
5
+ export type ToastObject<Data extends object = any> = BaseToastObject<Data> & {
6
+ placement?: ToastPlacement;
7
+ variant?: ToastVariant;
8
+ };
9
+ export type ToastAddOptions<Data extends object = any> = BaseAddOptions<Data> & {
10
+ placement?: ToastPlacement;
11
+ variant?: ToastVariant;
12
+ };
13
+ export type ToastUpdateOptions<Data extends object = any> = BaseUpdateOptions<Data> & {
14
+ variant?: ToastVariant;
15
+ };
16
+ export type ToastPromiseOptions<Value, Data extends object = any> = BasePromiseOptions<Value, Data> & {
17
+ placement?: ToastPlacement;
18
+ variant?: ToastVariant;
19
+ };
20
+ export interface ToastManager<Data extends object = any> {
21
+ add: <T extends Data = Data>(options: ToastAddOptions<T>) => string;
22
+ update: <T extends Data = Data>(id: string, options: ToastUpdateOptions<T> | ((previous: ToastObject<T>) => ToastUpdateOptions<T>)) => void;
23
+ close: (id?: string) => void;
24
+ closeAll: (placement?: ToastPlacement) => void;
25
+ promise: <Value, T extends Data = Data>(value: Promise<Value>, options: ToastPromiseOptions<Value, T>) => Promise<Value>;
26
+ }
27
+ export declare function getToastRouter(manager: ToastManager): {
28
+ queues: Record<"center" | "top-start" | "top-end" | "bottom-start" | "bottom-end" | "top-center" | "center-start" | "center-end" | "bottom-center", import("@base-ui/react/toast").ToastManager<object>>;
29
+ controllers: Map<"center" | "top-start" | "top-end" | "bottom-start" | "bottom-end" | "top-center" | "center-start" | "center-end" | "bottom-center", import("@base-ui/react/toast").UseToastManagerReturnValue<object>>;
30
+ engine: (placement: ToastPlacement) => import("@base-ui/react/toast").ToastManager<object> | import("@base-ui/react/toast").UseToastManagerReturnValue<object>;
31
+ viewports: Map<"center" | "top-start" | "top-end" | "bottom-start" | "bottom-end" | "top-center" | "center-start" | "center-end" | "bottom-center", HTMLElement>;
32
+ owners: Map<string, "center" | "top-start" | "top-end" | "bottom-start" | "bottom-end" | "top-center" | "center-start" | "center-end" | "bottom-center">;
33
+ defaults: {
34
+ placement: ToastPlacement;
35
+ variant: ToastVariant;
36
+ };
37
+ getSnapshot: () => ToastObject<any>[];
38
+ subscribe(listener: () => void): () => void;
39
+ report(placement: ToastPlacement, toasts: ToastObject[]): void;
40
+ };
41
+ /** Routes notifications; Base UI still owns each queue, timer and lifecycle. */
42
+ export declare function createToastManager<Data extends object = any>(): ToastManager<Data>;
@@ -0,0 +1,85 @@
1
+ import { Toast as BaseToast } from '@base-ui/react/toast';
2
+ export const toastPlacements = [
3
+ 'top-start', 'top-center', 'top-end',
4
+ 'center-start', 'center', 'center-end',
5
+ 'bottom-start', 'bottom-center', 'bottom-end',
6
+ ];
7
+ function createRouter() {
8
+ const queues = Object.fromEntries(toastPlacements.map(placement => [placement, BaseToast.createToastManager()]));
9
+ const controllers = new Map();
10
+ const snapshots = new Map();
11
+ const owners = new Map();
12
+ const listeners = new Set();
13
+ let snapshot = [];
14
+ return {
15
+ queues,
16
+ controllers,
17
+ engine: (placement) => controllers.get(placement) ?? queues[placement],
18
+ viewports: new Map(),
19
+ owners,
20
+ defaults: { placement: 'bottom-end', variant: 'auto' },
21
+ getSnapshot: () => snapshot,
22
+ subscribe(listener) {
23
+ listeners.add(listener);
24
+ return () => { listeners.delete(listener); };
25
+ },
26
+ report(placement, toasts) {
27
+ if (snapshots.get(placement) === toasts)
28
+ return;
29
+ const currentIds = new Set(toasts.map(toast => toast.id));
30
+ for (const previous of snapshots.get(placement) ?? []) {
31
+ if (!currentIds.has(previous.id))
32
+ owners.delete(previous.id);
33
+ }
34
+ for (const toast of toasts)
35
+ owners.set(toast.id, placement);
36
+ snapshots.set(placement, toasts);
37
+ snapshot = toastPlacements.flatMap(position => snapshots.get(position) ?? []);
38
+ listeners.forEach(listener => listener());
39
+ },
40
+ };
41
+ }
42
+ const routers = new WeakMap();
43
+ export function getToastRouter(manager) {
44
+ const router = routers.get(manager);
45
+ if (!router)
46
+ throw new Error('Use createToastManager from @inkra/react with Toast.Provider.');
47
+ return router;
48
+ }
49
+ /** Routes notifications; Base UI still owns each queue, timer and lifecycle. */
50
+ export function createToastManager() {
51
+ const router = createRouter();
52
+ const manager = {
53
+ add(options) {
54
+ // An upsert stays in its original queue, even if the default position changed.
55
+ const placement = (options.id && router.owners.get(options.id)) || options.placement || router.defaults.placement;
56
+ const id = router.engine(placement).add({ ...options, placement, variant: options.variant ?? router.defaults.variant });
57
+ router.owners.set(id, placement);
58
+ return id;
59
+ },
60
+ update(id, options) {
61
+ for (const placement of toastPlacements)
62
+ router.engine(placement).update(id, options);
63
+ },
64
+ close(id) {
65
+ for (const placement of toastPlacements)
66
+ router.engine(placement).close(id);
67
+ },
68
+ closeAll(placement) {
69
+ if (placement)
70
+ router.engine(placement).close();
71
+ else
72
+ manager.close();
73
+ },
74
+ promise(value, options) {
75
+ const placement = options.placement ?? router.defaults.placement;
76
+ const loading = typeof options.loading === 'string' ? { title: options.loading } : options.loading;
77
+ return router.engine(placement).promise(value, {
78
+ ...options,
79
+ loading: { ...loading, placement, variant: options.variant ?? router.defaults.variant },
80
+ });
81
+ },
82
+ };
83
+ routers.set(manager, router);
84
+ return manager;
85
+ }
@@ -0,0 +1,3 @@
1
+ import type { ToastManager } from './manager.js';
2
+ /** Coordinate F6 once: native per-queue listeners would otherwise compete for focus. */
3
+ export declare function useToastNavigation(manager: ToastManager): void;
@@ -0,0 +1,75 @@
1
+ import { useEffect } from 'react';
2
+ import { getToastRouter, toastPlacements } from './manager.js';
3
+ /** Coordinate F6 once: native per-queue listeners would otherwise compete for focus. */
4
+ export function useToastNavigation(manager) {
5
+ useEffect(() => {
6
+ const router = getToastRouter(manager);
7
+ let returnTarget = null;
8
+ let focusedToastElement = null;
9
+ const restore = () => {
10
+ if (returnTarget?.isConnected)
11
+ returnTarget.focus({ preventScroll: true });
12
+ returnTarget = null;
13
+ };
14
+ const focusin = (event) => {
15
+ const target = event.target;
16
+ if ([...router.viewports.values()].some(viewport => viewport.contains(target))) {
17
+ focusedToastElement = target;
18
+ }
19
+ else {
20
+ focusedToastElement = null;
21
+ returnTarget = null;
22
+ }
23
+ };
24
+ const unsubscribe = router.subscribe(() => {
25
+ if (returnTarget && focusedToastElement && !focusedToastElement.isConnected && document.activeElement === document.body)
26
+ restore();
27
+ });
28
+ const focusable = (viewport) => Array.from(viewport.querySelectorAll('button:not(:disabled), a[href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex="0"]')).filter(element => element.getClientRects().length && !element.closest('[inert], [data-ending-style], [data-limited]'));
29
+ const keydown = (event) => {
30
+ const viewports = toastPlacements.map(placement => router.viewports.get(placement))
31
+ .filter((viewport) => !!viewport?.isConnected && focusable(viewport).length > 0);
32
+ if (!viewports.length)
33
+ return;
34
+ const active = document.activeElement;
35
+ const index = viewports.findIndex(viewport => viewport.contains(active));
36
+ if (event.key === 'F6' && !event.altKey && !event.ctrlKey && !event.metaKey) {
37
+ event.preventDefault();
38
+ event.stopImmediatePropagation();
39
+ if (index === -1)
40
+ returnTarget = active;
41
+ const next = index === -1 ? (event.shiftKey ? viewports.length - 1 : 0)
42
+ : (index + (event.shiftKey ? -1 : 1) + viewports.length) % viewports.length;
43
+ focusable(viewports[next])[0]?.focus({ preventScroll: true });
44
+ }
45
+ else if (index !== -1 && returnTarget && event.key === 'Tab') {
46
+ const items = focusable(viewports[index]);
47
+ if ((event.shiftKey && items[0] === active) || (!event.shiftKey && items[items.length - 1] === active)) {
48
+ event.preventDefault();
49
+ event.stopImmediatePropagation();
50
+ restore();
51
+ }
52
+ }
53
+ else if (index !== -1 && returnTarget && event.key === 'Escape') {
54
+ const toast = active?.closest('[data-toast-id]');
55
+ if (!toast?.dataset.toastId)
56
+ return;
57
+ event.preventDefault();
58
+ event.stopImmediatePropagation();
59
+ manager.close(toast.dataset.toastId);
60
+ const next = focusable(viewports[index]).find(element => !toast.contains(element));
61
+ if (next)
62
+ next.focus({ preventScroll: true });
63
+ else
64
+ restore();
65
+ }
66
+ };
67
+ window.addEventListener('keydown', keydown, true);
68
+ document.addEventListener('focusin', focusin, true);
69
+ return () => {
70
+ unsubscribe();
71
+ window.removeEventListener('keydown', keydown, true);
72
+ document.removeEventListener('focusin', focusin, true);
73
+ };
74
+ }, [manager]);
75
+ }
package/dist/tokens.css CHANGED
@@ -166,6 +166,10 @@
166
166
  --inkra-z-tooltip: 40;
167
167
 
168
168
  --inkra-stroke-width: 2px;
169
+ --inkra-icon-stroke-width: 1.5px;
170
+ --inkra-icon-breaks: 0.18;
171
+ --inkra-icon-break-count: 1;
172
+ --inkra-icon-pressure-variation: 0.82;
169
173
  --inkra-stroke-variation: 0.82;
170
174
  --inkra-stroke-grain: 0.7;
171
175
  --inkra-stroke-breaks: 0.2;
@@ -7,7 +7,7 @@ export declare function TooltipRoot(props: TooltipRootProps): import("react").JS
7
7
  export type TooltipTriggerProps = TooltipPrimitive.TooltipTriggerProps;
8
8
  export declare const TooltipTrigger: import("react").ForwardRefExoticComponent<TooltipPrimitive.TooltipTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
9
9
  export type TooltipPortalProps = TooltipPrimitive.TooltipPortalProps;
10
- export declare function TooltipPortal({ children, ...props }: TooltipPortalProps): import("react").JSX.Element;
10
+ export declare function TooltipPortal({ children, ...props }: TooltipPortalProps): import("react").ReactNode;
11
11
  export interface TooltipContentProps extends TooltipPrimitive.TooltipContentProps {
12
12
  seed?: string | number;
13
13
  surfaceClassName?: string;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
3
3
  import { forwardRef, useId } from 'react';
4
- import { PortalThemeBridge, PortalThemeProvider, usePortalTriggerRef } from '../overlay/themeBridge.js';
4
+ import { mapPortalChildren, PortalThemeProvider, usePortalThemeProps, usePortalTriggerRef } from '../overlay/themeBridge.js';
5
5
  import { PencilSurface } from '../PencilSurface.js';
6
6
  import { PencilText } from '../PencilText.js';
7
7
  function classes(...values) {
@@ -16,14 +16,15 @@ export const TooltipTrigger = forwardRef(function TooltipTrigger(props, forwarde
16
16
  return _jsx(TooltipPrimitive.Trigger, { ...props, ref: ref });
17
17
  });
18
18
  export function TooltipPortal({ children, ...props }) {
19
- return (_jsx(TooltipPrimitive.Portal, { ...props, children: _jsx(PortalThemeBridge, { children: children }) }));
19
+ return mapPortalChildren(children, child => _jsx(TooltipPrimitive.Portal, { ...props, children: child }));
20
20
  }
21
- export const TooltipContent = forwardRef(function TooltipContent({ children, className, seed, sideOffset = 6, surfaceClassName, surfaceRadius = 6, surfaceVariant = 'paper', ...props }, ref) {
21
+ export const TooltipContent = forwardRef(function TooltipContent({ children, className, collisionPadding = 8, seed, sideOffset = 6, surfaceClassName, surfaceRadius = 6, surfaceVariant = 'paper', ...props }, ref) {
22
22
  const generatedId = useId();
23
+ props = usePortalThemeProps(props);
23
24
  const resolvedSeed = seed ?? `inkra-tooltip-${generatedId}`;
24
- return (_jsx(TooltipPrimitive.Content, { ...props, className: classes('inkra-tooltip__content', className), ref: ref, sideOffset: sideOffset, children: _jsx(PencilSurface, { className: classes('inkra-tooltip__surface', surfaceClassName), radius: surfaceRadius, seed: resolvedSeed, variant: surfaceVariant, children: _jsx("div", { className: "inkra-tooltip__content-body", children: typeof children === 'string' ? (_jsx(PencilText, { seed: `${resolvedSeed}-text`, children: children })) : children }) }) }));
25
+ return (_jsx(TooltipPrimitive.Content, { ...props, className: classes('inkra-tooltip__content', className), collisionPadding: collisionPadding, ref: ref, sideOffset: sideOffset, children: _jsx(PencilSurface, { className: classes('inkra-tooltip__surface', surfaceClassName), radius: surfaceRadius, seed: resolvedSeed, variant: surfaceVariant, children: _jsx("div", { className: "inkra-tooltip__content-body", children: typeof children === 'string' ? (_jsx(PencilText, { seed: `${resolvedSeed}-text`, children: children })) : children }) }) }));
25
26
  });
26
- export const Tooltip = Object.freeze({
27
+ export const Tooltip = /* @__PURE__ */ Object.freeze({
27
28
  Provider: TooltipProvider,
28
29
  Root: TooltipRoot,
29
30
  Trigger: TooltipTrigger,
@@ -28,6 +28,8 @@ export interface TransferLabels {
28
28
  moveSelectedToTarget?: string;
29
29
  moveSelectedToSource?: string;
30
30
  batchActions?: string;
31
+ previousPage?: string;
32
+ nextPage?: string;
31
33
  }
32
34
  export interface TransferCountDetails {
33
35
  side: TransferSide;
@@ -58,6 +60,9 @@ export interface TransferProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onC
58
60
  filterItem?: (item: TransferItem, query: string, side: TransferSide) => boolean;
59
61
  renderItem?: (item: TransferItem, state: TransferItemRenderState) => ReactNode;
60
62
  formatCount?: (details: TransferCountDetails) => ReactNode;
63
+ /** Opt-in panel pagination. All items remain searchable and movable. */
64
+ pageSize?: number;
65
+ pageLabel?: (page: number, pageCount: number) => ReactNode;
61
66
  }
62
67
  export declare const Transfer: import("react").ForwardRefExoticComponent<TransferProps & import("react").RefAttributes<HTMLDivElement>>;
63
68
  export { moveTransferKeys, normalizeTargetKeys, splitTransferItems, uniqueTransferItems } from './model.js';