@inkra/react 0.3.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/README.md +15 -3
  2. package/dist/Button.js +3 -2
  3. package/dist/PencilIcon.d.ts +10 -0
  4. package/dist/PencilIcon.js +83 -39
  5. package/dist/PencilLine.js +9 -1
  6. package/dist/PencilText.js +15 -9
  7. package/dist/accordion/Accordion.js +65 -5
  8. package/dist/alert-dialog/index.js +14 -4
  9. package/dist/audio-player/index.d.ts +2 -0
  10. package/dist/audio-player/index.js +126 -31
  11. package/dist/calendar/index.d.ts +4 -1
  12. package/dist/calendar/index.js +31 -12
  13. package/dist/carousel/index.js +13 -6
  14. package/dist/choice-grid/index.d.ts +6 -1
  15. package/dist/choice-grid/index.js +61 -33
  16. package/dist/color-picker/GradientEditor.d.ts +39 -0
  17. package/dist/color-picker/GradientEditor.js +63 -0
  18. package/dist/color-picker/gradient.d.ts +25 -0
  19. package/dist/color-picker/gradient.js +60 -0
  20. package/dist/color-picker/index.d.ts +27 -7
  21. package/dist/color-picker/index.js +134 -32
  22. package/dist/color-swatch/index.d.ts +10 -0
  23. package/dist/color-swatch/index.js +9 -0
  24. package/dist/command/index.js +27 -4
  25. package/dist/content/index.d.ts +5 -0
  26. package/dist/content/index.js +12 -3
  27. package/dist/context-menu/index.js +1 -1
  28. package/dist/data-table/index.d.ts +7 -1
  29. package/dist/data-table/index.js +64 -16
  30. package/dist/data-table/model.d.ts +2 -0
  31. package/dist/data-table/model.js +7 -0
  32. package/dist/date-picker/index.d.ts +2 -0
  33. package/dist/date-picker/index.js +10 -9
  34. package/dist/day-agenda/index.d.ts +6 -1
  35. package/dist/day-agenda/index.js +46 -21
  36. package/dist/day-agenda/layout.js +65 -9
  37. package/dist/display/ProgressRing.js +7 -3
  38. package/dist/display/index.d.ts +4 -1
  39. package/dist/display/index.js +3 -2
  40. package/dist/dockable-panel/index.js +5 -5
  41. package/dist/drawer/index.js +4 -3
  42. package/dist/editable/index.d.ts +24 -2
  43. package/dist/editable/index.js +142 -37
  44. package/dist/empty/illustrations.d.ts +8 -0
  45. package/dist/empty/illustrations.js +78 -0
  46. package/dist/empty/index.d.ts +6 -1
  47. package/dist/empty/index.js +11 -3
  48. package/dist/flip/index.js +28 -8
  49. package/dist/form/Field.js +11 -10
  50. package/dist/form/Textarea.d.ts +3 -1
  51. package/dist/form/Textarea.js +6 -4
  52. package/dist/hotkey/index.d.ts +1 -0
  53. package/dist/hotkey/index.js +6 -4
  54. package/dist/hover-card/index.js +1 -1
  55. package/dist/image-cropper/index.js +4 -2
  56. package/dist/image-viewer/VideoResource.d.ts +21 -0
  57. package/dist/image-viewer/VideoResource.js +194 -0
  58. package/dist/image-viewer/index.d.ts +14 -0
  59. package/dist/image-viewer/index.js +118 -65
  60. package/dist/image-viewer/lucideGeometry.d.ts +3 -0
  61. package/dist/image-viewer/lucideGeometry.js +19 -0
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.js +2 -0
  64. package/dist/input-number/index.js +9 -4
  65. package/dist/internal/cache.d.ts +4 -0
  66. package/dist/internal/cache.js +18 -1
  67. package/dist/internal/iconGeometry.d.ts +2 -0
  68. package/dist/internal/iconGeometry.js +32 -0
  69. package/dist/internal/renderTokens.d.ts +4 -0
  70. package/dist/internal/renderTokens.js +4 -0
  71. package/dist/internal/stroke.d.ts +7 -0
  72. package/dist/internal/stroke.js +63 -7
  73. package/dist/internal/surfaceBudget.d.ts +20 -0
  74. package/dist/internal/surfaceBudget.js +69 -0
  75. package/dist/internal/surfaceRenderer.js +87 -28
  76. package/dist/internal/surfaceVisibility.d.ts +3 -0
  77. package/dist/internal/surfaceVisibility.js +60 -0
  78. package/dist/internal/textRenderer.d.ts +2 -0
  79. package/dist/internal/textRenderer.js +3 -3
  80. package/dist/internal/usePencilSurface.js +17 -1
  81. package/dist/internal/wash.js +4 -3
  82. package/dist/journey-map/terrain.js +10 -5
  83. package/dist/layout/index.js +1 -1
  84. package/dist/log/index.js +29 -22
  85. package/dist/markdown/CodeBlock.d.ts +19 -0
  86. package/dist/markdown/CodeBlock.js +86 -0
  87. package/dist/markdown/Deletion.d.ts +2 -0
  88. package/dist/markdown/Deletion.js +69 -0
  89. package/dist/markdown/icons.d.ts +14 -0
  90. package/dist/markdown/icons.js +16 -0
  91. package/dist/markdown/index.d.ts +17 -0
  92. package/dist/markdown/index.js +175 -0
  93. package/dist/node-graph/index.d.ts +2 -0
  94. package/dist/node-graph/index.js +49 -25
  95. package/dist/number-ticker/precision.js +9 -2
  96. package/dist/overflow-list/index.js +5 -2
  97. package/dist/overlay/Overlay.js +1 -1
  98. package/dist/overlay/Popover.js +3 -3
  99. package/dist/overlay/themeBridge.js +14 -2
  100. package/dist/pagination/index.d.ts +10 -1
  101. package/dist/pagination/index.js +6 -4
  102. package/dist/pan-zoom/index.d.ts +2 -0
  103. package/dist/pan-zoom/index.js +7 -6
  104. package/dist/pencil.d.ts +1 -0
  105. package/dist/pencil.js +4 -4
  106. package/dist/radar/index.d.ts +2 -0
  107. package/dist/radar/index.js +40 -20
  108. package/dist/rating/index.d.ts +4 -0
  109. package/dist/rating/index.js +128 -24
  110. package/dist/resizable/index.js +1 -1
  111. package/dist/scroll-area/index.d.ts +1 -1
  112. package/dist/scroll-area/index.js +5 -2
  113. package/dist/select/MultipleSelect.js +5 -4
  114. package/dist/select/SearchableSelect.js +61 -6
  115. package/dist/select/Select.d.ts +1 -0
  116. package/dist/select/Select.js +3 -1
  117. package/dist/select/StepperSelect.d.ts +29 -0
  118. package/dist/select/StepperSelect.js +119 -0
  119. package/dist/select/index.d.ts +2 -0
  120. package/dist/select/index.js +1 -0
  121. package/dist/select/stepperModel.d.ts +7 -0
  122. package/dist/select/stepperModel.js +29 -0
  123. package/dist/selection/Checkbox.js +4 -3
  124. package/dist/selection/RadioGroup.js +29 -13
  125. package/dist/selection/Switch.js +4 -3
  126. package/dist/selection/useNativeChecked.d.ts +2 -1
  127. package/dist/selection/useNativeChecked.js +26 -8
  128. package/dist/slider/index.js +50 -6
  129. package/dist/sortable/index.js +6 -2
  130. package/dist/sparkline/index.js +17 -2
  131. package/dist/sparkline/model.js +11 -4
  132. package/dist/styles/accordion.css +228 -0
  133. package/dist/styles/alert-dialog.css +139 -0
  134. package/dist/styles/audio-player.css +22 -0
  135. package/dist/styles/base.css +638 -0
  136. package/dist/styles/breadcrumb.css +7 -0
  137. package/dist/styles/calendar.css +62 -0
  138. package/dist/styles/carousel.css +173 -0
  139. package/dist/styles/choice-grid.css +226 -0
  140. package/dist/styles/color-picker.css +281 -0
  141. package/dist/styles/color-swatch.css +47 -0
  142. package/dist/styles/command.css +52 -0
  143. package/dist/styles/content.css +79 -0
  144. package/dist/styles/context-menu.css +35 -0
  145. package/dist/styles/data-table.css +31 -0
  146. package/dist/styles/date-picker.css +107 -0
  147. package/dist/styles/day-agenda.css +30 -0
  148. package/dist/styles/display.css +304 -0
  149. package/dist/styles/dockable-panel.css +110 -0
  150. package/dist/styles/drawer.css +133 -0
  151. package/dist/styles/dropdown-menu.css +68 -0
  152. package/dist/styles/editable.css +145 -0
  153. package/dist/styles/empty.css +108 -0
  154. package/dist/styles/flip.css +101 -0
  155. package/dist/styles/form.css +634 -0
  156. package/dist/styles/heatmap.css +256 -0
  157. package/dist/styles/hotkey.css +189 -0
  158. package/dist/styles/hover-card.css +27 -0
  159. package/dist/styles/image-cropper.css +205 -0
  160. package/dist/styles/image-viewer.css +422 -0
  161. package/dist/styles/input-number.css +21 -0
  162. package/dist/styles/journey-map.css +19 -0
  163. package/dist/styles/keypad.css +92 -0
  164. package/dist/styles/layout.css +72 -0
  165. package/dist/styles/leaflet.css +661 -0
  166. package/dist/styles/log.css +203 -0
  167. package/dist/styles/markdown.css +379 -0
  168. package/dist/styles/node-graph.css +142 -0
  169. package/dist/styles/number-ticker.css +83 -0
  170. package/dist/styles/overflow-list.css +51 -0
  171. package/dist/styles/overlay.css +284 -0
  172. package/dist/styles/pagination.css +21 -0
  173. package/dist/styles/pan-zoom.css +136 -0
  174. package/dist/styles/radar.css +1 -0
  175. package/dist/styles/rating.css +121 -0
  176. package/dist/styles/resizable.css +108 -0
  177. package/dist/styles/scroll-area.css +122 -0
  178. package/dist/styles/select.css +557 -0
  179. package/dist/styles/selection.css +264 -0
  180. package/dist/styles/slider.css +145 -0
  181. package/dist/styles/slot.css +109 -0
  182. package/dist/styles/sortable.css +159 -0
  183. package/dist/styles/sparkline.css +24 -0
  184. package/dist/styles/steps.css +27 -0
  185. package/dist/styles/tabs.css +79 -0
  186. package/dist/styles/tag-input.css +213 -0
  187. package/dist/styles/time-picker.css +66 -0
  188. package/dist/styles/timeline.css +24 -0
  189. package/dist/styles/toast.css +160 -0
  190. package/dist/styles/toggle-group.css +107 -0
  191. package/dist/styles/tooltip.css +78 -0
  192. package/dist/styles/transfer.css +148 -0
  193. package/dist/styles/tree-select.css +131 -0
  194. package/dist/styles/tree.css +59 -0
  195. package/dist/styles/typewriter.css +62 -0
  196. package/dist/styles/upload.css +60 -0
  197. package/dist/styles/virtual-list.css +112 -0
  198. package/dist/styles.css +2094 -421
  199. package/dist/tabs/index.d.ts +9 -0
  200. package/dist/tabs/index.js +66 -16
  201. package/dist/tabs/useOverflow.d.ts +20 -0
  202. package/dist/tabs/useOverflow.js +145 -0
  203. package/dist/tag-input/index.d.ts +7 -2
  204. package/dist/tag-input/index.js +70 -26
  205. package/dist/time-picker/index.js +38 -20
  206. package/dist/toast/index.d.ts +19 -11
  207. package/dist/toast/index.js +48 -8
  208. package/dist/toast/manager.d.ts +42 -0
  209. package/dist/toast/manager.js +85 -0
  210. package/dist/toast/useToastNavigation.d.ts +3 -0
  211. package/dist/toast/useToastNavigation.js +75 -0
  212. package/dist/tokens.css +4 -0
  213. package/dist/tooltip/Tooltip.js +3 -3
  214. package/dist/transfer/index.d.ts +5 -0
  215. package/dist/transfer/index.js +93 -29
  216. package/dist/transfer/model.d.ts +4 -0
  217. package/dist/transfer/model.js +37 -11
  218. package/dist/tree/index.d.ts +5 -1
  219. package/dist/tree/index.js +5 -5
  220. package/dist/tree-select/index.js +67 -30
  221. package/dist/tree-select/model.d.ts +11 -5
  222. package/dist/tree-select/model.js +111 -83
  223. package/dist/typewriter/index.js +10 -10
  224. package/dist/upload/index.js +30 -16
  225. package/dist/virtual-list/index.js +7 -5
  226. package/package.json +413 -3
@@ -1,15 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from 'react';
3
3
  import { PencilSurface } from '../PencilSurface.js';
4
+ import { PencilLine } from '../PencilLine.js';
4
5
  import { PencilText } from '../PencilText.js';
5
6
  import { PencilIcon } from '../PencilIcon.js';
6
7
  import { Button } from '../Button.js';
7
8
  import { Progress } from '../display/index.js';
8
9
  import { useResolvedControlProps } from './controlProps.js';
9
10
  import { EditablePencilText } from './EditablePencilText.js';
11
+ import { InputAction } from './InputAction.js';
10
12
  const resizePaths = [{ d: 'M12 3v18M8 7l4-4 4 4M8 17l4 4 4-4' }];
11
13
  const defaultCountLabel = ({ remaining }) => remaining < 0 ? `${-remaining} over limit` : `${remaining} remaining`;
12
- export const Textarea = forwardRef(function Textarea({ 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, className, disabled, id, invalid = false, readOnly, required, seed, size = 'md', countMode = 'remaining', countDisplay = 'auto', countThreshold = 0.8, getCountLabel = defaultCountLabel, resize = 'none', resizeLabel = 'Resize text area', ...props }, ref) {
14
+ export const Textarea = forwardRef(function Textarea({ 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, className, disabled, id, invalid = false, readOnly, required, seed, size = 'md', variant = 'outline', endAction, countMode = 'remaining', countDisplay = 'auto', countThreshold = 0.8, getCountLabel = defaultCountLabel, resize = 'none', resizeLabel = 'Resize text area', ...props }, ref) {
13
15
  const generatedId = useId();
14
16
  const textareaRef = useRef(null);
15
17
  const [uncontrolledCount, setUncontrolledCount] = useState(() => String(props.defaultValue ?? '').length);
@@ -69,11 +71,11 @@ export const Textarea = forwardRef(function Textarea({ 'aria-describedby': ariaD
69
71
  });
70
72
  const resolvedSeed = seed ?? `inkra-textarea-${state.id ?? generatedId}`;
71
73
  const classes = ['inkra-textarea', className].filter(Boolean).join(' ');
72
- return (_jsx("div", { className: "inkra-field-control-hit inkra-field-control-hit--textarea", "data-disabled": state.disabled || undefined, "data-invalid": state.invalid || undefined, "data-readonly": state.readOnly || undefined, "data-size": size, children: _jsxs(PencilSurface, { className: "inkra-field-control inkra-field-control--textarea", seed: resolvedSeed, variant: state.invalid ? 'danger' : 'outline', children: [_jsx("textarea", { ...props, "aria-describedby": [state.describedBy, showCount && countId].filter(Boolean).join(' ') || undefined, "aria-errormessage": state.errorMessage, "aria-invalid": state.ariaInvalid, className: classes, disabled: state.disabled, id: state.id, readOnly: state.readOnly, ref: textareaRef, required: state.required, style: { ...props.style, ...(resize === 'vertical' && height !== undefined ? { height } : {}), resize: 'none' }, onInput: event => {
74
+ return (_jsx("div", { className: "inkra-field-control-hit inkra-field-control-hit--textarea", "data-disabled": state.disabled || undefined, "data-invalid": state.invalid || undefined, "data-readonly": state.readOnly || undefined, "data-size": size, "data-variant": variant, "data-end-action": endAction != null || undefined, children: _jsxs(PencilSurface, { className: "inkra-field-control inkra-field-control--textarea", seed: resolvedSeed, variant: variant === 'underline' ? 'ghost' : state.invalid ? 'danger' : 'outline', children: [_jsx("textarea", { ...props, "aria-describedby": [state.describedBy, showCount && countId].filter(Boolean).join(' ') || undefined, "aria-errormessage": state.errorMessage, "aria-invalid": state.ariaInvalid, className: classes, disabled: state.disabled, id: state.id, readOnly: state.readOnly, ref: textareaRef, required: state.required, style: { ...props.style, ...(resize === 'vertical' && height !== undefined ? { height } : {}), resize: 'none' }, onInput: event => {
73
75
  if (!controlled)
74
76
  setUncontrolledCount(event.currentTarget.value.length);
75
77
  props.onInput?.(event);
76
- } }), _jsx(EditablePencilText, { controlRef: textareaRef, defaultValue: props.defaultValue, multiline: true, placeholder: props.placeholder, seed: `${resolvedSeed}-editable`, value: props.value }), (hasCount || resize === 'vertical') && _jsxs("div", { className: "inkra-textarea__footer", children: [hasCount && _jsxs("div", { className: "inkra-textarea__count", id: countId, "data-visible": showCount, "data-limit": remaining <= 0 ? 'reached' : 'near', "aria-hidden": !showCount || undefined, children: [countMode === 'progress' && _jsx(Progress, { size: "sm", value: maximum === 0 ? 1 : count, max: maximum === 0 ? 1 : maximum, "aria-label": countLabel, "aria-valuetext": countLabel }), _jsx(PencilText, { children: countMode === 'fraction' ? `${count} / ${maximum}` : countLabel })] }), resize === 'vertical' && _jsx(Button, { className: "inkra-textarea__resize", variant: "ghost", size: "icon", role: "slider", "aria-label": resizeLabel, title: resizeLabel, "aria-controls": state.id, "aria-orientation": "vertical", "aria-valuemin": minimumHeight, "aria-valuemax": 1200, "aria-valuenow": Math.round(measuredHeight ?? minimumHeight), disabled: state.disabled, onPointerDown: event => {
78
+ } }), _jsx(EditablePencilText, { controlRef: textareaRef, defaultValue: props.defaultValue, multiline: true, placeholder: props.placeholder, seed: `${resolvedSeed}-editable`, value: props.value }), endAction != null && _jsx("div", { className: "inkra-textarea__end-action", children: _jsx(InputAction, { disabled: Boolean(state.disabled), seed: `${resolvedSeed}-action`, children: endAction }) }), (hasCount || resize === 'vertical') && _jsxs("div", { className: "inkra-textarea__footer", children: [hasCount && _jsxs("div", { className: "inkra-textarea__count", id: countId, "data-visible": showCount, "data-limit": remaining <= 0 ? 'reached' : 'near', "aria-hidden": !showCount || undefined, children: [countMode === 'progress' && _jsx(Progress, { size: "sm", value: maximum === 0 ? 1 : count, max: maximum === 0 ? 1 : maximum, "aria-label": countLabel, "aria-valuetext": countLabel }), _jsx(PencilText, { children: countMode === 'fraction' ? `${count} / ${maximum}` : countLabel })] }), resize === 'vertical' && _jsx(Button, { className: "inkra-textarea__resize", variant: "ghost", size: "icon", role: "slider", "aria-label": resizeLabel, title: resizeLabel, "aria-controls": state.id, "aria-orientation": "vertical", "aria-valuemin": minimumHeight, "aria-valuemax": 1200, "aria-valuenow": Math.round(measuredHeight ?? minimumHeight), disabled: state.disabled, onPointerDown: event => {
77
79
  if (event.button !== 0)
78
80
  return;
79
81
  drag.current = { y: event.clientY, height: textareaRef.current?.getBoundingClientRect().height ?? minimumHeight };
@@ -98,5 +100,5 @@ export const Textarea = forwardRef(function Textarea({ 'aria-describedby': ariaD
98
100
  else
99
101
  return;
100
102
  event.preventDefault();
101
- }, children: _jsx(PencilIcon, { paths: resizePaths, size: 22 }) })] })] }) }));
103
+ }, children: _jsx(PencilIcon, { paths: resizePaths, size: 22 }) })] }), variant === 'underline' && _jsx(PencilLine, { className: "inkra-textarea__underline", seed: `${resolvedSeed}-underline` })] }) }));
102
104
  });
@@ -25,6 +25,7 @@ export interface HotkeyInputProps extends Omit<InputHTMLAttributes<HTMLInputElem
25
25
  conflictMessage?: string;
26
26
  platform?: HotkeyPlatform;
27
27
  placeholder?: string;
28
+ recordingLabel?: string;
28
29
  clearable?: boolean;
29
30
  clearButtonLabel?: string;
30
31
  blurOnCapture?: boolean;
@@ -34,7 +34,7 @@ export const Hotkey = forwardRef(function Hotkey({ 'aria-label': ariaLabel, clas
34
34
  const label = formatHotkey(normalized, resolvedPlatform);
35
35
  return (_jsx("kbd", { ...props, "aria-label": ariaLabel ?? label, className: classes('inkra-hotkey', className), "data-platform": resolvedPlatform, "data-size": size, ref: ref, children: tokens.map((token, index) => (_jsxs("span", { className: "inkra-hotkey__part", children: [index > 0 && resolvedPlatform !== 'mac' ? (_jsx("span", { "aria-hidden": "true", className: "inkra-hotkey__separator", children: separator ?? '+' })) : null, _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-hotkey__key", radius: 4, seed: `${hotkeySignature(normalized)}-${resolvedPlatform}-${index}`, variant: "quiet", children: _jsx(PencilText, { seed: `${hotkeySignature(normalized)}-${resolvedPlatform}-${index}-label`, children: token }) })] }, `${token}-${index}`))) }));
36
36
  });
37
- export const HotkeyInput = forwardRef(function HotkeyInput({ 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, 'aria-label': ariaLabel, blurOnCapture = true, className, classNames = {}, clearable = true, clearButtonLabel = 'Clear shortcut', conflict, conflictMessage = 'Shortcut already assigned', defaultValue = null, disabled, id, invalid = false, onBlur, onCancel, onCompositionEnd, onCompositionStart, onConflict, onFocus, onKeyDown, onValueChange, placeholder = 'Press shortcut', platform = 'auto', readOnly, required, seed, size = 'md', validateHotkey, value, ...props }, forwardedRef) {
37
+ export const HotkeyInput = forwardRef(function HotkeyInput({ 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, 'aria-label': ariaLabel, blurOnCapture = true, className, classNames = {}, clearable = true, clearButtonLabel = 'Clear shortcut', conflict, conflictMessage = 'Shortcut already assigned', defaultValue = null, disabled, id, invalid = false, onBlur, onCancel, onCompositionEnd, onCompositionStart, onConflict, onFocus, onKeyDown, onValueChange, placeholder = 'Press shortcut', platform = 'auto', readOnly, recordingLabel = 'Press a shortcut...', required, seed, size = 'md', validateHotkey, value, ...props }, forwardedRef) {
38
38
  const generatedId = useId();
39
39
  const inputRef = useRef(null);
40
40
  const composingRef = useRef(false);
@@ -48,18 +48,20 @@ export const HotkeyInput = forwardRef(function HotkeyInput({ 'aria-describedby':
48
48
  const currentSignature = hotkeySignature(currentValue);
49
49
  const validationMessage = externalConflict ?? (localConflict?.signature === currentSignature ? localConflict.message : undefined);
50
50
  const messageId = validationMessage ? `${id ?? generatedId}-hotkey-conflict` : undefined;
51
+ const statusId = `${id ?? generatedId}-hotkey-status`;
51
52
  const state = useResolvedControlProps({
52
53
  id,
53
54
  required,
54
55
  disabled,
55
56
  readOnly,
56
57
  invalid: invalid || Boolean(validationMessage),
57
- 'aria-describedby': [ariaDescribedBy, messageId].filter(Boolean).join(' ') || undefined,
58
+ 'aria-describedby': [ariaDescribedBy, statusId, messageId].filter(Boolean).join(' ') || undefined,
58
59
  'aria-errormessage': validationMessage ? messageId : ariaErrorMessage,
59
60
  'aria-invalid': ariaInvalid,
60
61
  });
61
62
  const resolvedSeed = seed ?? `inkra-hotkey-input-${state.id ?? generatedId}`;
62
63
  const displayValue = currentValue ? formatHotkey(currentValue, resolvedPlatform) : '';
64
+ const recording = focused && !state.disabled && !state.readOnly;
63
65
  useImperativeHandle(forwardedRef, () => inputRef.current);
64
66
  const updateValue = (nextValue, details) => {
65
67
  if (!isControlled)
@@ -119,12 +121,12 @@ export const HotkeyInput = forwardRef(function HotkeyInput({ 'aria-describedby':
119
121
  setFocused(false);
120
122
  onBlur?.(event);
121
123
  };
122
- return (_jsxs("span", { className: classes('inkra-hotkey-input', classNames.root), "data-disabled": state.disabled || undefined, "data-focus-within": focused || undefined, "data-invalid": state.invalid || undefined, "data-readonly": state.readOnly || undefined, "data-size": size, children: [_jsxs(PencilSurface, { as: "span", className: classes('inkra-hotkey-input__control', classNames.control), clipContent: true, seed: resolvedSeed, variant: state.invalid ? 'danger' : 'outline', children: [_jsx("input", { ...props, "aria-describedby": state.describedBy, "aria-errormessage": state.errorMessage, "aria-invalid": state.ariaInvalid, "aria-label": ariaLabel, autoComplete: "off", className: classes('inkra-hotkey-input__native', className, classNames.input), disabled: state.disabled, id: state.id, inputMode: "none", onBlur: handleBlur, onChange: () => { }, onCompositionEnd: event => {
124
+ return (_jsxs("span", { className: classes('inkra-hotkey-input', classNames.root), "data-disabled": state.disabled || undefined, "data-focus-within": focused || undefined, "data-invalid": state.invalid || undefined, "data-recording": recording || undefined, "data-readonly": state.readOnly || undefined, "data-size": size, children: [_jsxs(PencilSurface, { as: "span", className: classes('inkra-hotkey-input__control', classNames.control), clipContent: true, seed: resolvedSeed, variant: state.invalid ? 'danger' : 'outline', children: [_jsx("input", { ...props, "aria-describedby": state.describedBy, "aria-errormessage": state.errorMessage, "aria-invalid": state.ariaInvalid, "aria-label": ariaLabel, autoComplete: "off", className: classes('inkra-hotkey-input__native', className, classNames.input), disabled: state.disabled, id: state.id, inputMode: "none", onBlur: handleBlur, onChange: () => { }, onCompositionEnd: event => {
123
125
  composingRef.current = false;
124
126
  onCompositionEnd?.(event);
125
127
  }, onCompositionStart: event => {
126
128
  composingRef.current = true;
127
129
  onCompositionStart?.(event);
128
- }, onFocus: handleFocus, onKeyDown: handleKeyDown, readOnly: state.readOnly, ref: inputRef, required: state.required, spellCheck: false, value: displayValue }), _jsx("span", { "aria-hidden": "true", className: classes('inkra-hotkey-input__value', classNames.value), children: currentValue ? _jsx(Hotkey, { value: currentValue, platform: resolvedPlatform, size: size }) : _jsx(PencilText, { className: "inkra-hotkey-input__placeholder", seed: `${resolvedSeed}-placeholder`, children: placeholder }) }), clearable && currentValue ? (_jsx("button", { "aria-label": clearButtonLabel, className: "inkra-hotkey-input__clear", disabled: state.disabled || state.readOnly, onClick: clear, onPointerDown: event => event.preventDefault(), title: clearButtonLabel, type: "button", children: _jsx(PencilIcon, { paths: clearPaths, seed: `${resolvedSeed}-clear`, size: size === 'sm' ? 18 : size === 'lg' ? 26 : 22, sourceSize: 24 }) })) : null] }), validationMessage ? (_jsx(PencilText, { as: "span", className: classes('inkra-hotkey-input__message', classNames.message), id: messageId, role: "alert", seed: `${resolvedSeed}-message`, children: validationMessage })) : null] }));
130
+ }, onFocus: handleFocus, onKeyDown: handleKeyDown, readOnly: state.readOnly, ref: inputRef, required: state.required, spellCheck: false, value: displayValue }), _jsx("span", { "aria-hidden": "true", className: classes('inkra-hotkey-input__value', classNames.value), children: recording ? (_jsx(PencilText, { className: "inkra-hotkey-input__recording", seed: `${resolvedSeed}-recording`, children: recordingLabel })) : currentValue ? (_jsx(Hotkey, { value: currentValue, platform: resolvedPlatform, size: size })) : (_jsx(PencilText, { className: "inkra-hotkey-input__placeholder", seed: `${resolvedSeed}-placeholder`, children: placeholder })) }), clearable && currentValue ? (_jsx("button", { "aria-label": clearButtonLabel, className: "inkra-hotkey-input__clear", disabled: state.disabled || state.readOnly, onClick: clear, onPointerDown: event => event.preventDefault(), title: clearButtonLabel, type: "button", children: _jsx(PencilIcon, { paths: clearPaths, seed: `${resolvedSeed}-clear`, size: size === 'sm' ? 18 : size === 'lg' ? 26 : 22, sourceSize: 24 }) })) : null, _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-hotkey-input__focus-ring", seed: `${resolvedSeed}-focus`, variant: "outline" })] }), _jsx("span", { "aria-live": "polite", className: "inkra-hotkey-input__status", id: statusId, children: recording ? recordingLabel : '' }), validationMessage ? (_jsx(PencilText, { as: "span", className: classes('inkra-hotkey-input__message', classNames.message), id: messageId, role: "alert", seed: `${resolvedSeed}-message`, children: validationMessage })) : null] }));
129
131
  });
130
132
  export { detectHotkeyPlatform, formatHotkey, getHotkeyTokens, hotkeyFromKeyboardEvent, hotkeySignature, normalizeHotkey, };
@@ -131,7 +131,7 @@ export const HoverCardContent = forwardRef(function HoverCardContent({ avoidColl
131
131
  export const HoverCardTitle = PopoverTitle;
132
132
  export const HoverCardDescription = PopoverDescription;
133
133
  export const HoverCardClose = PopoverClose;
134
- export const HoverCard = Object.freeze({
134
+ export const HoverCard = /* @__PURE__ */ Object.freeze({
135
135
  Root: HoverCardRoot,
136
136
  Trigger: HoverCardTrigger,
137
137
  Content: HoverCardContent,
@@ -124,8 +124,10 @@ export const ImageCropper = forwardRef(function ImageCropper({ alt, aspect = 1,
124
124
  return undefined;
125
125
  const update = () => {
126
126
  const rect = stage.getBoundingClientRect();
127
- if (rect.width > 0 && rect.height > 0)
128
- setStageSize({ width: rect.width, height: rect.height });
127
+ if (rect.width > 0 && rect.height > 0) {
128
+ setStageSize(current => current.width === rect.width && current.height === rect.height
129
+ ? current : { width: rect.width, height: rect.height });
130
+ }
129
131
  };
130
132
  update();
131
133
  const observer = new ResizeObserver(update);
@@ -0,0 +1,21 @@
1
+ export interface VideoResourceLabels {
2
+ controls: string;
3
+ play: string;
4
+ pause: string;
5
+ seek: string;
6
+ mute: string;
7
+ unmute: string;
8
+ volume: string;
9
+ rate: string;
10
+ playbackError: string;
11
+ time: (current: number, duration: number) => string;
12
+ }
13
+ export interface VideoResourceProps {
14
+ src: string;
15
+ poster?: string;
16
+ alt: string;
17
+ labels?: Partial<VideoResourceLabels>;
18
+ onError?: () => void;
19
+ onLoad?: () => void;
20
+ }
21
+ export declare function VideoResource({ alt, labels: overrides, onError, onLoad, poster, src }: VideoResourceProps): import("react").JSX.Element;
@@ -0,0 +1,194 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { __iconData as pauseIcon } from 'lucide-react/dist/esm/icons/pause.mjs';
3
+ import { __iconData as playIcon } from 'lucide-react/dist/esm/icons/play.mjs';
4
+ import { __iconData as volumeIcon } from 'lucide-react/dist/esm/icons/volume-2.mjs';
5
+ import { __iconData as muteIcon } from 'lucide-react/dist/esm/icons/volume-x.mjs';
6
+ import { useCallback, useEffect, useRef, useState } from 'react';
7
+ import { Button } from '../Button.js';
8
+ import { PencilIcon } from '../PencilIcon.js';
9
+ import { PencilText } from '../PencilText.js';
10
+ import { Select } from '../select/index.js';
11
+ import { Slider } from '../slider/index.js';
12
+ function number(attributes, name) {
13
+ const value = Number(attributes[name]);
14
+ return Number.isFinite(value) ? value : 0;
15
+ }
16
+ function iconPaths(nodes) {
17
+ return nodes.flatMap(([tag, attributes]) => {
18
+ if (tag === 'path' && attributes.d)
19
+ return [{ d: attributes.d }];
20
+ if (tag === 'line')
21
+ return [{ d: `M${number(attributes, 'x1')} ${number(attributes, 'y1')}L${number(attributes, 'x2')} ${number(attributes, 'y2')}` }];
22
+ if (tag === 'rect') {
23
+ const x = number(attributes, 'x');
24
+ const y = number(attributes, 'y');
25
+ const width = number(attributes, 'width');
26
+ const height = number(attributes, 'height');
27
+ const radius = Math.min(number(attributes, 'rx'), width / 2, height / 2);
28
+ return [{ d: radius
29
+ ? `M${x + radius} ${y}H${x + width - radius}Q${x + width} ${y} ${x + width} ${y + radius}V${y + height - radius}Q${x + width} ${y + height} ${x + width - radius} ${y + height}H${x + radius}Q${x} ${y + height} ${x} ${y + height - radius}V${y + radius}Q${x} ${y} ${x + radius} ${y}Z`
30
+ : `M${x} ${y}H${x + width}V${y + height}H${x}Z` }];
31
+ }
32
+ return [];
33
+ });
34
+ }
35
+ const playPaths = iconPaths(playIcon.node);
36
+ const pausePaths = iconPaths(pauseIcon.node);
37
+ const volumePaths = iconPaths(volumeIcon.node);
38
+ const mutePaths = iconPaths(muteIcon.node);
39
+ const rates = [0.75, 1, 1.25, 1.5, 2];
40
+ const defaultLabels = {
41
+ controls: 'Video controls',
42
+ play: 'Play video',
43
+ pause: 'Pause video',
44
+ seek: 'Seek video',
45
+ mute: 'Mute video',
46
+ unmute: 'Unmute video',
47
+ volume: 'Video volume',
48
+ rate: 'Playback speed',
49
+ playbackError: 'Video playback could not start.',
50
+ time: (current, duration) => `${formatTime(current)} / ${formatTime(duration)}`,
51
+ };
52
+ function clamp(value, min, max) {
53
+ return Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : min;
54
+ }
55
+ function formatTime(value) {
56
+ if (!Number.isFinite(value) || value < 0)
57
+ return '0:00';
58
+ const seconds = Math.floor(value);
59
+ return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
60
+ }
61
+ export function VideoResource({ alt, labels: overrides, onError, onLoad, poster, src }) {
62
+ const videoRef = useRef(null);
63
+ const sourceRef = useRef(src);
64
+ sourceRef.current = src;
65
+ const mountedRef = useRef(false);
66
+ const generationRef = useRef(0);
67
+ const playRequestRef = useRef(0);
68
+ const callbacksRef = useRef({ onError, onLoad });
69
+ callbacksRef.current = { onError, onLoad };
70
+ const [playing, setPlaying] = useState(false);
71
+ const [currentTime, setCurrentTime] = useState(0);
72
+ const [duration, setDuration] = useState(0);
73
+ const [volume, setVolumeState] = useState(1);
74
+ const [muted, setMutedState] = useState(false);
75
+ const [playbackRate, setPlaybackRateState] = useState(1);
76
+ const [playbackError, setPlaybackError] = useState(false);
77
+ const labels = { ...defaultLabels, ...overrides };
78
+ const sync = useCallback(() => {
79
+ const video = videoRef.current;
80
+ if (!video)
81
+ return;
82
+ setPlaying(!video.paused && !video.ended);
83
+ setCurrentTime(Number.isFinite(video.currentTime) ? video.currentTime : 0);
84
+ setDuration(Number.isFinite(video.duration) ? video.duration : 0);
85
+ setVolumeState(video.volume);
86
+ setMutedState(video.muted);
87
+ setPlaybackRateState(video.playbackRate);
88
+ }, []);
89
+ useEffect(() => {
90
+ mountedRef.current = true;
91
+ return () => {
92
+ mountedRef.current = false;
93
+ generationRef.current += 1;
94
+ playRequestRef.current += 1;
95
+ videoRef.current?.pause();
96
+ };
97
+ }, []);
98
+ useEffect(() => {
99
+ const video = videoRef.current;
100
+ if (!video)
101
+ return undefined;
102
+ const handlers = {
103
+ loadedmetadata: () => { sync(); callbacksRef.current.onLoad?.(); },
104
+ durationchange: sync,
105
+ timeupdate: sync,
106
+ play: sync,
107
+ pause: sync,
108
+ ended: sync,
109
+ volumechange: sync,
110
+ ratechange: sync,
111
+ emptied: sync,
112
+ error: () => { sync(); callbacksRef.current.onError?.(); },
113
+ };
114
+ Object.entries(handlers).forEach(([name, handler]) => video.addEventListener(name, handler));
115
+ return () => Object.entries(handlers).forEach(([name, handler]) => video.removeEventListener(name, handler));
116
+ }, [sync]);
117
+ useEffect(() => {
118
+ const video = videoRef.current;
119
+ if (!video)
120
+ return undefined;
121
+ generationRef.current += 1;
122
+ playRequestRef.current += 1;
123
+ video.pause();
124
+ try {
125
+ video.currentTime = 0;
126
+ }
127
+ catch { /* A source may not have a timeline yet. */ }
128
+ setPlaying(false);
129
+ setCurrentTime(0);
130
+ setDuration(0);
131
+ setPlaybackError(false);
132
+ video.load();
133
+ return () => { video.pause(); };
134
+ }, [src]);
135
+ const togglePlay = async () => {
136
+ const video = videoRef.current;
137
+ if (!video)
138
+ return;
139
+ if (!video.paused) {
140
+ playRequestRef.current += 1;
141
+ video.pause();
142
+ return;
143
+ }
144
+ const generation = generationRef.current;
145
+ const source = sourceRef.current;
146
+ const request = ++playRequestRef.current;
147
+ setPlaybackError(false);
148
+ try {
149
+ await video.play();
150
+ if (!mountedRef.current || generation !== generationRef.current || source !== sourceRef.current || request !== playRequestRef.current)
151
+ return;
152
+ sync();
153
+ }
154
+ catch {
155
+ if (!mountedRef.current || generation !== generationRef.current || source !== sourceRef.current || request !== playRequestRef.current)
156
+ return;
157
+ setPlaybackError(true);
158
+ sync();
159
+ }
160
+ };
161
+ const seek = (next) => {
162
+ const video = videoRef.current;
163
+ if (!video || !Number.isFinite(next))
164
+ return;
165
+ video.currentTime = clamp(next, 0, Number.isFinite(video.duration) ? video.duration : Math.max(0, next));
166
+ sync();
167
+ };
168
+ const setVolume = (next) => {
169
+ const video = videoRef.current;
170
+ if (!video)
171
+ return;
172
+ video.volume = clamp(next, 0, 1);
173
+ if (video.muted && video.volume > 0)
174
+ video.muted = false;
175
+ sync();
176
+ };
177
+ const setMuted = (next) => {
178
+ const video = videoRef.current;
179
+ if (!video)
180
+ return;
181
+ video.muted = next;
182
+ sync();
183
+ };
184
+ const setRate = (next) => {
185
+ const video = videoRef.current;
186
+ if (!video)
187
+ return;
188
+ const rate = clamp(next, 0.25, 4);
189
+ video.defaultPlaybackRate = rate;
190
+ video.playbackRate = rate;
191
+ sync();
192
+ };
193
+ return _jsxs("div", { className: "inkra-image-viewer__video-resource", children: [_jsx("video", { "aria-label": alt, className: "inkra-image-viewer__video-media", controls: false, playsInline: true, poster: poster, preload: "metadata", ref: videoRef, src: src }), _jsxs("div", { "aria-label": labels.controls, className: "inkra-image-viewer__video-controls", role: "group", children: [playbackError ? _jsx(PencilText, { as: "p", className: "inkra-image-viewer__video-error", role: "alert", children: labels.playbackError }) : null, _jsx(Button, { "aria-label": playing ? labels.pause : labels.play, className: "inkra-image-viewer__video-play", onClick: () => void togglePlay(), size: "icon-sm", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: playing ? pausePaths : playPaths, size: 22, sourceSize: 24 }) }), _jsx("div", { className: "inkra-image-viewer__video-seek", children: _jsx(Slider, { "aria-label": labels.seek, disabled: !duration, getValueLabel: value => labels.time(value, duration), max: duration || 0, min: 0, onValueChange: values => seek(values[0] ?? 0), size: "sm", step: 0.01, value: [currentTime], variant: "inset" }) }), _jsx(PencilText, { "aria-live": "off", as: "span", className: "inkra-image-viewer__video-time", dir: "auto", children: labels.time(currentTime, duration) }), _jsxs("div", { className: "inkra-image-viewer__video-settings", children: [_jsx(Button, { "aria-label": muted ? labels.unmute : labels.mute, className: "inkra-image-viewer__video-mute", onClick: () => setMuted(!muted), size: "icon-sm", variant: "ghost", children: _jsx(PencilIcon, { decorative: true, paths: muted ? mutePaths : volumePaths, size: 22, sourceSize: 24 }) }), _jsx("div", { className: "inkra-image-viewer__video-volume", children: _jsx(Slider, { "aria-label": labels.volume, getValueLabel: value => `${Math.round(value * 100)}%`, max: 1, min: 0, onValueChange: values => setVolume(values[0] ?? 0), size: "sm", step: 0.01, value: [muted ? 0 : volume], variant: "inset" }) }), _jsxs(Select.Root, { onValueChange: value => setRate(Number(value)), value: String(playbackRate), children: [_jsx(Select.Trigger, { "aria-label": labels.rate, fullWidth: false, size: "sm", children: _jsx(Select.Value, {}) }), _jsx(Select.Portal, { children: _jsx(Select.Content, { position: "popper", sideOffset: 4, children: _jsx(Select.Viewport, { children: rates.map(rate => _jsxs(Select.Item, { value: String(rate), children: [_jsx(Select.ItemText, { children: `${rate}x` }), _jsx(Select.ItemIndicator, {})] }, rate)) }) }) })] })] })] })] });
194
+ }
@@ -1,10 +1,13 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { PanZoomWheelMode } from '../pan-zoom/index.js';
3
3
  import type { OverlayContentProps, OverlayRootProps, OverlayTriggerProps } from '../overlay/index.js';
4
+ import type { VideoResourceLabels } from './VideoResource.js';
4
5
  export interface ImageViewerItem {
5
6
  id: string;
6
7
  src: string;
7
8
  alt: string;
9
+ type?: 'image' | 'video';
10
+ poster?: string;
8
11
  thumbnailSrc?: string;
9
12
  thumbnailAlt?: string;
10
13
  width?: number;
@@ -68,6 +71,7 @@ export interface ImageViewerContentProps extends Omit<OverlayContentProps, 'chil
68
71
  minScale?: number;
69
72
  maxScale?: number;
70
73
  wheel?: PanZoomWheelMode;
74
+ videoLabels?: Partial<VideoResourceLabels>;
71
75
  }
72
76
  export declare const ImageViewerContent: import("react").ForwardRefExoticComponent<ImageViewerContentProps & import("react").RefAttributes<HTMLDivElement>>;
73
77
  export declare const ImageViewer: Readonly<{
@@ -75,5 +79,15 @@ export declare const ImageViewer: Readonly<{
75
79
  Trigger: import("react").ForwardRefExoticComponent<import("@radix-ui/react-dialog").DialogTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
76
80
  Content: import("react").ForwardRefExoticComponent<ImageViewerContentProps & import("react").RefAttributes<HTMLDivElement>>;
77
81
  }>;
82
+ export declare const MediaViewer: Readonly<{
83
+ Root: typeof ImageViewerRoot;
84
+ Trigger: import("react").ForwardRefExoticComponent<import("@radix-ui/react-dialog").DialogTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
85
+ Content: import("react").ForwardRefExoticComponent<ImageViewerContentProps & import("react").RefAttributes<HTMLDivElement>>;
86
+ }>;
87
+ export type MediaViewerItem = ImageViewerItem;
88
+ export type MediaViewerLabels = ImageViewerLabels;
89
+ export type MediaViewerRootProps = ImageViewerRootProps;
90
+ export type MediaViewerContentProps = ImageViewerContentProps;
91
+ export type { VideoResourceLabels } from './VideoResource.js';
78
92
  export { nextImageViewerIndex, resolveImageViewerSelection } from './model.js';
79
93
  export type { ImageViewerIdentifiable, ImageViewerSelection } from './model.js';