@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
@@ -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';
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { __iconData as chevronLeftIcon } from 'lucide-react/dist/esm/icons/chevron-left.mjs';
3
3
  import { __iconData as chevronRightIcon } from 'lucide-react/dist/esm/icons/chevron-right.mjs';
4
4
  import { __iconData as imageOffIcon } from 'lucide-react/dist/esm/icons/image-off.mjs';
5
+ import { __iconData as playIcon } from 'lucide-react/dist/esm/icons/play.mjs';
5
6
  import { __iconData as rotateIcon } from 'lucide-react/dist/esm/icons/rotate-cw.mjs';
6
7
  import { __iconData as zoomInIcon } from 'lucide-react/dist/esm/icons/zoom-in.mjs';
7
8
  import { __iconData as zoomOutIcon } from 'lucide-react/dist/esm/icons/zoom-out.mjs';
@@ -11,14 +12,17 @@ import { PencilIcon } from '../PencilIcon.js';
11
12
  import { PencilSurface } from '../PencilSurface.js';
12
13
  import { PencilText } from '../PencilText.js';
13
14
  import { PanZoom } from '../pan-zoom/index.js';
14
- import { OverlayBackdrop, OverlayContent, OverlayDescription, OverlayHeader, OverlayPortal, OverlayRoot, OverlayTitle, OverlayTrigger, } from '../overlay/index.js';
15
+ import { OverlayBackdrop, OverlayCloseButton, OverlayContent, OverlayDescription, OverlayPortal, OverlayRoot, OverlayTitle, OverlayTrigger, } from '../overlay/index.js';
16
+ import { lucideNodesToPencilPaths } from './lucideGeometry.js';
15
17
  import { nextImageViewerIndex, resolveImageViewerSelection } from './model.js';
16
- const chevronLeftPaths = chevronLeftIcon.node.map(([, attributes]) => ({ d: attributes.d }));
17
- const chevronRightPaths = chevronRightIcon.node.map(([, attributes]) => ({ d: attributes.d }));
18
- const imageOffPaths = imageOffIcon.node.map(([, attributes]) => ({ d: attributes.d }));
19
- const rotatePaths = rotateIcon.node.map(([, attributes]) => ({ d: attributes.d }));
20
- const zoomInPaths = zoomInIcon.node.map(([, attributes]) => ({ d: attributes.d }));
21
- const zoomOutPaths = zoomOutIcon.node.map(([, attributes]) => ({ d: attributes.d }));
18
+ import { VideoResource } from './VideoResource.js';
19
+ const chevronLeftPaths = lucideNodesToPencilPaths(chevronLeftIcon.node);
20
+ const chevronRightPaths = lucideNodesToPencilPaths(chevronRightIcon.node);
21
+ const imageOffPaths = lucideNodesToPencilPaths(imageOffIcon.node);
22
+ const playPaths = lucideNodesToPencilPaths(playIcon.node);
23
+ const rotatePaths = lucideNodesToPencilPaths(rotateIcon.node);
24
+ const zoomInPaths = lucideNodesToPencilPaths(zoomInIcon.node);
25
+ const zoomOutPaths = lucideNodesToPencilPaths(zoomOutIcon.node);
22
26
  const ImageViewerContext = createContext(null);
23
27
  function useImageViewerContext() {
24
28
  const context = useContext(ImageViewerContext);
@@ -73,9 +77,10 @@ export function ImageViewerRoot({ activeId, defaultActiveId, defaultOpen = false
73
77
  activeItem: selection.index < 0 ? null : items[selection.index],
74
78
  activeIndex: selection.index,
75
79
  loop,
80
+ open: openControlled ? Boolean(open) : internalOpen,
76
81
  selectIndex,
77
82
  step,
78
- }), [items, loop, selectIndex, selection.index, step]);
83
+ }), [internalOpen, items, loop, open, openControlled, selectIndex, selection.index, step]);
79
84
  return (_jsx(ImageViewerContext.Provider, { value: context, children: _jsx(OverlayRoot, { ...props, open: openControlled ? open : internalOpen, onOpenChange: (nextOpen) => {
80
85
  if (!openControlled)
81
86
  setInternalOpen(nextOpen);
@@ -108,23 +113,46 @@ const defaultLabels = {
108
113
  function directionFor(node) {
109
114
  return getComputedStyle(node).direction === 'rtl' ? 'rtl' : 'ltr';
110
115
  }
111
- export const ImageViewerContent = forwardRef(function ImageViewerContent({ className, emptyState, labels: labelOverrides, onOpenAutoFocus, renderImage, rotatable = false, showThumbnails = true, zoomable = true, minScale = 0.5, maxScale = 6, wheel = 'modifier', ...props }, forwardedRef) {
112
- const { activeIndex, activeItem, items, loop, selectIndex, step } = useImageViewerContext();
116
+ export const ImageViewerContent = forwardRef(function ImageViewerContent({ className, emptyState, labels: labelOverrides, onOpenAutoFocus, renderImage, rotatable = false, showThumbnails = true, zoomable = true, minScale = 0.5, maxScale = 6, videoLabels, wheel = 'modifier', ...props }, forwardedRef) {
117
+ const { activeIndex, activeItem, items, loop, open, selectIndex, step } = useImageViewerContext();
113
118
  const labels = { ...defaultLabels, ...labelOverrides };
114
119
  const stageRef = useRef(null);
120
+ const thumbnailsRef = useRef(null);
115
121
  const thumbnailRefs = useRef(new Map());
116
122
  const panZoomRef = useRef(null);
117
- const [failedIds, setFailedIds] = useState(() => new Set());
123
+ const focusFrameRef = useRef(null);
124
+ const fitFrameRef = useRef(null);
125
+ useEffect(() => () => {
126
+ if (focusFrameRef.current !== null)
127
+ cancelAnimationFrame(focusFrameRef.current);
128
+ if (fitFrameRef.current !== null)
129
+ cancelAnimationFrame(fitFrameRef.current);
130
+ }, []);
131
+ const [failedResources, setFailedResources] = useState(() => new Set());
118
132
  const [attempts, setAttempts] = useState({});
119
133
  const [rotation, setRotation] = useState(0);
120
134
  const total = items.length;
121
135
  const atStart = activeIndex <= 0;
122
136
  const atEnd = activeIndex >= total - 1;
137
+ const activeItemType = activeItem?.type ?? 'image';
138
+ const activeItemKey = activeItem ? JSON.stringify([activeItemType, activeItem.id, activeItem.src]) : undefined;
139
+ const activeResourceRef = useRef(activeItemKey);
140
+ activeResourceRef.current = activeItemKey;
123
141
  useEffect(() => {
124
- if (activeItem)
125
- thumbnailRefs.current.get(activeItem.id)?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
142
+ const thumbnail = activeItem ? thumbnailRefs.current.get(activeItem.id) : undefined;
143
+ const viewport = thumbnailsRef.current;
144
+ if (thumbnail && viewport) {
145
+ const item = thumbnail.getBoundingClientRect();
146
+ const clip = viewport.getBoundingClientRect();
147
+ if (item.left < clip.left)
148
+ viewport.scrollLeft -= clip.left - item.left;
149
+ else if (item.right > clip.right)
150
+ viewport.scrollLeft += item.right - clip.right;
151
+ }
126
152
  setRotation(0);
127
- }, [activeItem]);
153
+ setFailedResources(current => activeItemKey && current.has(activeItemKey) ? new Set([activeItemKey]) : new Set());
154
+ setAttempts(current => activeItemKey && current[activeItemKey] !== undefined ? { [activeItemKey]: current[activeItemKey] } : {});
155
+ }, [activeItemKey]);
128
156
  const handleStageKeyDown = (event) => {
129
157
  if (event.target !== event.currentTarget)
130
158
  return;
@@ -149,72 +177,97 @@ export const ImageViewerContent = forwardRef(function ImageViewerContent({ class
149
177
  const selectThumbnail = (index, reason = 'thumbnail') => {
150
178
  selectIndex(index, reason);
151
179
  const item = items[index];
152
- if (item)
153
- requestAnimationFrame(() => thumbnailRefs.current.get(item.id)?.focus());
180
+ if (focusFrameRef.current !== null)
181
+ cancelAnimationFrame(focusFrameRef.current);
182
+ focusFrameRef.current = item ? requestAnimationFrame(() => {
183
+ focusFrameRef.current = null;
184
+ thumbnailRefs.current.get(item.id)?.focus();
185
+ }) : null;
154
186
  };
155
187
  const renderDefaultImage = (item, { key, ...imageProps }) => (_jsx("img", { ...imageProps, draggable: false }, key));
156
- const failed = activeItem ? failedIds.has(activeItem.id) : false;
157
- const imageProps = activeItem ? {
158
- alt: activeItem.alt,
159
- className: 'inkra-image-viewer__image',
160
- height: activeItem.height,
161
- key: `${activeItem.id}:${attempts[activeItem.id] ?? 0}`,
162
- onError: () => setFailedIds(current => new Set(current).add(activeItem.id)),
163
- onLoad: () => setFailedIds(current => {
164
- if (!current.has(activeItem.id))
165
- return current;
166
- const next = new Set(current);
167
- next.delete(activeItem.id);
168
- return next;
169
- }),
170
- src: activeItem.src,
171
- width: activeItem.width,
172
- } : null;
188
+ const failed = activeItemKey ? failedResources.has(activeItemKey) : false;
189
+ const imageProps = activeItem && activeItemKey && activeItemType === 'image' ? (() => {
190
+ const resourceKey = activeItemKey;
191
+ return {
192
+ alt: activeItem.alt,
193
+ className: 'inkra-image-viewer__image',
194
+ height: activeItem.height,
195
+ key: `${resourceKey}:${attempts[resourceKey] ?? 0}`,
196
+ onError: () => setFailedResources(current => new Set(current).add(resourceKey)),
197
+ onLoad: () => setFailedResources(current => {
198
+ if (!current.has(resourceKey))
199
+ return current;
200
+ const next = new Set(current);
201
+ next.delete(resourceKey);
202
+ return next;
203
+ }),
204
+ src: activeItem.src,
205
+ width: activeItem.width,
206
+ };
207
+ })() : null;
173
208
  return (_jsxs(OverlayPortal, { children: [_jsx(OverlayBackdrop, {}), _jsxs(OverlayContent, { ...props, className: ['inkra-image-viewer', className].filter(Boolean).join(' '), layout: "sections", onOpenAutoFocus: (event) => {
174
209
  onOpenAutoFocus?.(event);
175
210
  if (event.defaultPrevented)
176
211
  return;
177
212
  event.preventDefault();
178
213
  stageRef.current?.focus();
179
- }, ref: forwardedRef, surfaceRadius: 8, children: [_jsxs(OverlayHeader, { closeLabel: labels.close, children: [_jsx(OverlayTitle, { children: labels.title }), _jsx(OverlayDescription, { className: "inkra-image-viewer__description", children: labels.description }), total > 0 ? _jsx(PencilText, { as: "span", className: "inkra-image-viewer__counter", children: labels.imagePosition(activeIndex + 1, total) }) : null] }), _jsx("div", { className: "inkra-image-viewer__body", children: activeItem && imageProps ? _jsx(_Fragment, { children: _jsxs("div", { "aria-label": activeItem.alt, className: "inkra-image-viewer__stage", onKeyDown: handleStageKeyDown, ref: stageRef, tabIndex: 0, children: [failed ? _jsxs("div", { className: "inkra-image-viewer__state", role: "alert", children: [_jsx(PencilIcon, { decorative: true, paths: imageOffPaths, size: 48, sourceSize: 24 }), _jsx(PencilText, { as: "h3", children: labels.failedTitle }), _jsx(PencilText, { as: "p", children: labels.failedDescription(activeItem) }), _jsx(Button, { variant: "outline", onClick: () => {
180
- const failedId = activeItem.id;
181
- setFailedIds(current => {
214
+ }, ref: forwardedRef, surfaceRadius: 0, surfaceVariant: "ghost", children: [_jsx(OverlayTitle, { className: "inkra-image-viewer__accessible-label", children: labels.title }), _jsx(OverlayDescription, { className: "inkra-image-viewer__accessible-label", children: labels.description }), _jsxs("div", { className: "inkra-image-viewer__close-control", children: [_jsx(PencilSurface, { "aria-hidden": "true", className: "inkra-image-viewer__close-paper", radius: 8, seed: "image-viewer-close-paper", variant: "paper" }), _jsx(OverlayCloseButton, { className: "inkra-image-viewer__close", label: labels.close })] }), _jsx("div", { className: "inkra-image-viewer__body", children: activeItem && activeItemKey ? _jsx(_Fragment, { children: _jsxs("div", { "aria-label": activeItem.alt, className: "inkra-image-viewer__stage", onKeyDown: handleStageKeyDown, ref: stageRef, tabIndex: 0, children: [_jsx(PencilSurface, { "aria-hidden": "true", className: "inkra-image-viewer__stage-focus", radius: 6, seed: `image-viewer-stage-${activeItem.id}`, variant: "outline" }), failed ? _jsxs("div", { className: "inkra-image-viewer__state", role: "alert", children: [_jsx(PencilIcon, { decorative: true, paths: imageOffPaths, size: 48, sourceSize: 24 }), _jsx(PencilText, { as: "h3", children: labels.failedTitle }), _jsx(PencilText, { as: "p", children: labels.failedDescription(activeItem) }), _jsx(Button, { variant: "outline", onClick: () => {
215
+ if (!activeItemKey)
216
+ return;
217
+ const failedKey = activeItemKey;
218
+ setFailedResources(current => {
182
219
  const next = new Set(current);
183
- next.delete(failedId);
220
+ next.delete(failedKey);
184
221
  return next;
185
222
  });
186
- setAttempts(current => ({ ...current, [failedId]: (current[failedId] ?? 0) + 1 }));
187
- }, children: labels.retry })] }) : _jsx(PanZoom, { "aria-label": labels.panZoom(activeItem), centerOnInit: true, className: "inkra-image-viewer__pan-zoom", fitOnInit: "contain", keyboard: zoomable, maxScale: maxScale, minScale: minScale, pan: zoomable, pinch: zoomable, readOnly: !zoomable, ref: panZoomRef, wheel: zoomable ? wheel : 'disabled', children: _jsx("div", { className: "inkra-image-viewer__image-transform", style: { transform: `rotate(${rotation}deg)` }, children: (renderImage ?? renderDefaultImage)(activeItem, imageProps) }) }, activeItem.id), !failed && (zoomable || rotatable) ? _jsxs("div", { className: "inkra-image-viewer__tools", role: "group", "aria-label": labels.panZoom(activeItem), "data-pan-zoom-no-drag": true, children: [zoomable ? _jsxs(_Fragment, { children: [_jsx(Button, { "aria-label": labels.zoomOut, onClick: () => void panZoomRef.current?.zoomOut(), size: "icon-sm", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: zoomOutPaths, size: 24, sourceSize: 24 }) }), _jsx(Button, { "aria-label": labels.resetZoom, onClick: () => void panZoomRef.current?.reset(), size: "sm", variant: "outline", children: "100%" }), _jsx(Button, { "aria-label": labels.zoomIn, onClick: () => void panZoomRef.current?.zoomIn(), size: "icon-sm", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: zoomInPaths, size: 24, sourceSize: 24 }) })] }) : null, rotatable ? _jsx(Button, { "aria-label": labels.rotate, onClick: () => {
223
+ setAttempts(current => ({ ...current, [failedKey]: (current[failedKey] ?? 0) + 1 }));
224
+ }, children: labels.retry })] }) : activeItemType === 'video' && open ? _jsx(VideoResource, { alt: activeItem.alt, labels: videoLabels, onError: () => setFailedResources(current => new Set(current).add(activeItemKey)), onLoad: () => setFailedResources(current => {
225
+ if (!current.has(activeItemKey))
226
+ return current;
227
+ const next = new Set(current);
228
+ next.delete(activeItemKey);
229
+ return next;
230
+ }), poster: activeItem.poster, src: activeItem.src }, activeItemKey) : imageProps ? _jsx(PanZoom, { "aria-label": labels.panZoom(activeItem), centerOnInit: true, className: "inkra-image-viewer__pan-zoom", fitOnInit: "contain", keyboard: zoomable, maxScale: maxScale, minScale: minScale, pan: zoomable, pinch: zoomable, readOnly: !zoomable, ref: panZoomRef, wheel: zoomable ? wheel : 'disabled', children: _jsx("div", { className: "inkra-image-viewer__image-transform", style: { transform: `rotate(${rotation}deg)` }, children: (renderImage ?? renderDefaultImage)(activeItem, imageProps) }) }, activeItemKey) : null, total > 1 && activeItemType === 'image' ? _jsxs("div", { className: "inkra-image-viewer__navigation", children: [_jsx(Button, { "aria-label": labels.previous, className: "inkra-image-viewer__previous", disabled: !loop && atStart, onClick: () => step(-1), size: "icon", variant: "secondary", children: _jsx(PencilIcon, { decorative: true, paths: chevronLeftPaths, size: 30, sourceSize: 24 }) }), _jsx(Button, { "aria-label": labels.next, className: "inkra-image-viewer__next", disabled: !loop && atEnd, onClick: () => step(1), size: "icon", variant: "secondary", children: _jsx(PencilIcon, { decorative: true, paths: chevronRightPaths, size: 30, sourceSize: 24 }) })] }) : null] }) }) : _jsxs("div", { className: "inkra-image-viewer__state", ref: stageRef, tabIndex: 0, children: [_jsx(PencilSurface, { "aria-hidden": "true", className: "inkra-image-viewer__stage-focus", radius: 6, seed: "image-viewer-empty-stage", variant: "outline" }), emptyState ?? _jsxs(_Fragment, { children: [_jsx(PencilIcon, { decorative: true, paths: imageOffPaths, size: 48, sourceSize: 24 }), _jsx(PencilText, { as: "h3", children: labels.emptyTitle }), _jsx(PencilText, { as: "p", children: labels.emptyDescription })] })] }) }), (activeItem || (showThumbnails && total > 1)) ? _jsxs("div", { className: "inkra-image-viewer__bottom", children: [_jsxs("div", { className: "inkra-image-viewer__bottom-row", children: [total > 0 ? _jsx(PencilText, { as: "span", className: "inkra-image-viewer__counter", dir: "auto", children: labels.imagePosition(activeIndex + 1, total) }) : null, activeItem && activeItemType === 'image' && !failed && (zoomable || rotatable) ? _jsxs("div", { className: "inkra-image-viewer__tools", role: "group", "aria-label": labels.panZoom(activeItem), "data-pan-zoom-no-drag": true, children: [_jsx(PencilSurface, { "aria-hidden": "true", className: "inkra-image-viewer__tools-paper", radius: 8, seed: `image-viewer-tools-${activeItem.id}`, variant: "paper" }), zoomable ? _jsxs(_Fragment, { children: [_jsx(Button, { "aria-label": labels.zoomOut, onClick: () => void panZoomRef.current?.zoomOut(), size: "icon-sm", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: zoomOutPaths, size: 24, sourceSize: 24 }) }), _jsx(Button, { "aria-label": labels.resetZoom, onClick: () => void panZoomRef.current?.reset(), size: "sm", variant: "outline", children: "100%" }), _jsx(Button, { "aria-label": labels.zoomIn, onClick: () => void panZoomRef.current?.zoomIn(), size: "icon-sm", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: zoomInPaths, size: 24, sourceSize: 24 }) })] }) : null, rotatable ? _jsx(Button, { "aria-label": labels.rotate, onClick: () => {
188
231
  setRotation(current => (current + 90) % 360);
189
- requestAnimationFrame(() => void panZoomRef.current?.fit({ mode: 'contain' }));
190
- }, size: "icon-sm", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: rotatePaths, size: 24, sourceSize: 24 }) }) : null] }) : null, total > 1 ? _jsxs("div", { className: "inkra-image-viewer__navigation", children: [_jsx(Button, { "aria-label": labels.previous, className: "inkra-image-viewer__previous", disabled: !loop && atStart, onClick: () => step(-1), size: "icon", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: chevronLeftPaths, size: 30, sourceSize: 24 }) }), _jsx(Button, { "aria-label": labels.next, className: "inkra-image-viewer__next", disabled: !loop && atEnd, onClick: () => step(1), size: "icon", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: chevronRightPaths, size: 30, sourceSize: 24 }) })] }) : null] }) }) : _jsx("div", { className: "inkra-image-viewer__state", ref: stageRef, tabIndex: 0, children: emptyState ?? _jsxs(_Fragment, { children: [_jsx(PencilIcon, { decorative: true, paths: imageOffPaths, size: 48, sourceSize: 24 }), _jsx(PencilText, { as: "h3", children: labels.emptyTitle }), _jsx(PencilText, { as: "p", children: labels.emptyDescription })] }) }) }), showThumbnails && total > 1 ? _jsx("div", { "aria-label": labels.thumbnails, className: "inkra-image-viewer__thumbnails", role: "listbox", children: items.map((item, index) => {
191
- const selected = index === activeIndex;
192
- return _jsxs("button", { "aria-label": labels.thumbnail(item, index + 1, total), "aria-selected": selected, className: "inkra-image-viewer__thumbnail", onClick: () => selectIndex(index, 'thumbnail'), onKeyDown: (event) => {
193
- const rtl = directionFor(event.currentTarget) === 'rtl';
194
- let nextIndex = index;
195
- if (event.key === 'ArrowLeft')
196
- nextIndex = nextImageViewerIndex(total, index, rtl ? 1 : -1, loop);
197
- else if (event.key === 'ArrowRight')
198
- nextIndex = nextImageViewerIndex(total, index, rtl ? -1 : 1, loop);
199
- else if (event.key === 'Home')
200
- nextIndex = 0;
201
- else if (event.key === 'End')
202
- nextIndex = total - 1;
203
- else
204
- return;
205
- event.preventDefault();
206
- selectThumbnail(nextIndex, event.key === 'Home' ? 'home' : event.key === 'End' ? 'end' : 'thumbnail');
207
- }, ref: (node) => {
208
- if (node)
209
- thumbnailRefs.current.set(item.id, node);
210
- else
211
- thumbnailRefs.current.delete(item.id);
212
- }, role: "option", tabIndex: selected ? 0 : -1, type: "button", children: [_jsx(PencilSurface, { as: "span", className: "inkra-image-viewer__thumbnail-surface", radius: 6, variant: selected ? 'selection' : 'outline', children: _jsx("img", { alt: "", className: "inkra-image-viewer__thumbnail-image", draggable: false, src: item.thumbnailSrc ?? item.src }) }), _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-image-viewer__thumbnail-focus", radius: 6, variant: "outline" })] }, item.id);
213
- }) }) : null] })] }));
232
+ if (fitFrameRef.current !== null)
233
+ cancelAnimationFrame(fitFrameRef.current);
234
+ const resourceKey = activeItemKey;
235
+ fitFrameRef.current = requestAnimationFrame(() => {
236
+ fitFrameRef.current = null;
237
+ if (activeResourceRef.current === resourceKey)
238
+ void panZoomRef.current?.fit({ mode: 'contain' });
239
+ });
240
+ }, size: "icon-sm", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: rotatePaths, size: 24, sourceSize: 24 }) }) : null] }) : null, activeItem && activeItemType === 'video' && total > 1 ? _jsxs("div", { className: "inkra-image-viewer__video-navigation", role: "group", "aria-label": labels.thumbnails, children: [_jsx(Button, { "aria-label": labels.previous, disabled: !loop && atStart, onClick: () => step(-1), size: "icon-sm", variant: "secondary", children: _jsx(PencilIcon, { decorative: true, paths: chevronLeftPaths, size: 24, sourceSize: 24 }) }), _jsx(Button, { "aria-label": labels.next, disabled: !loop && atEnd, onClick: () => step(1), size: "icon-sm", variant: "secondary", children: _jsx(PencilIcon, { decorative: true, paths: chevronRightPaths, size: 24, sourceSize: 24 }) })] }) : null] }), showThumbnails && total > 1 ? _jsx("div", { "aria-label": labels.thumbnails, className: "inkra-image-viewer__thumbnails", ref: thumbnailsRef, role: "listbox", children: items.map((item, index) => {
241
+ const selected = index === activeIndex;
242
+ return _jsxs("button", { "aria-label": labels.thumbnail(item, index + 1, total), "aria-selected": selected, className: "inkra-image-viewer__thumbnail", onClick: () => selectIndex(index, 'thumbnail'), onKeyDown: (event) => {
243
+ const rtl = directionFor(event.currentTarget) === 'rtl';
244
+ let nextIndex = index;
245
+ if (event.key === 'ArrowLeft')
246
+ nextIndex = nextImageViewerIndex(total, index, rtl ? 1 : -1, loop);
247
+ else if (event.key === 'ArrowRight')
248
+ nextIndex = nextImageViewerIndex(total, index, rtl ? -1 : 1, loop);
249
+ else if (event.key === 'Home')
250
+ nextIndex = 0;
251
+ else if (event.key === 'End')
252
+ nextIndex = total - 1;
253
+ else
254
+ return;
255
+ event.preventDefault();
256
+ selectThumbnail(nextIndex, event.key === 'Home' ? 'home' : event.key === 'End' ? 'end' : 'thumbnail');
257
+ }, ref: (node) => {
258
+ if (node)
259
+ thumbnailRefs.current.set(item.id, node);
260
+ else
261
+ thumbnailRefs.current.delete(item.id);
262
+ }, role: "option", tabIndex: selected ? 0 : -1, type: "button", children: [_jsx(PencilSurface, { as: "span", className: "inkra-image-viewer__thumbnail-surface", radius: 6, variant: selected ? 'selection' : 'outline', children: (item.thumbnailSrc ?? item.poster ?? (item.type === 'video' ? undefined : item.src))
263
+ ? _jsx("img", { alt: "", className: "inkra-image-viewer__thumbnail-image", draggable: false, src: item.thumbnailSrc ?? item.poster ?? item.src })
264
+ : _jsx(PencilIcon, { decorative: true, paths: item.type === 'video' ? playPaths : imageOffPaths, size: 30, sourceSize: 24 }) }), item.type === 'video' ? _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-image-viewer__thumbnail-video", radius: 6, variant: "paper", children: _jsx(PencilIcon, { decorative: true, paths: playPaths, size: 18, sourceSize: 24 }) }) : null, _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-image-viewer__thumbnail-focus", radius: 6, variant: "outline" })] }, item.id);
265
+ }) }) : null] }) : null] })] }));
214
266
  });
215
- export const ImageViewer = Object.freeze({
267
+ export const ImageViewer = /* @__PURE__ */ Object.freeze({
216
268
  Root: ImageViewerRoot,
217
269
  Trigger: ImageViewerTrigger,
218
270
  Content: ImageViewerContent,
219
271
  });
272
+ export const MediaViewer = ImageViewer;
220
273
  export { nextImageViewerIndex, resolveImageViewerSelection } from './model.js';
@@ -0,0 +1,3 @@
1
+ import type { PencilIconPath } from '../PencilIcon.js';
2
+ export type LucideIconNode = readonly [string, Record<string, string>];
3
+ export declare function lucideNodesToPencilPaths(nodes: readonly LucideIconNode[]): readonly PencilIconPath[];
@@ -0,0 +1,19 @@
1
+ function number(attributes, name) {
2
+ const value = Number(attributes[name]);
3
+ return Number.isFinite(value) ? value : 0;
4
+ }
5
+ export function lucideNodesToPencilPaths(nodes) {
6
+ return nodes.flatMap(([tag, attributes]) => {
7
+ if (tag === 'path' && attributes.d)
8
+ return [{ d: attributes.d }];
9
+ if (tag === 'line')
10
+ return [{ d: `M${number(attributes, 'x1')} ${number(attributes, 'y1')}L${number(attributes, 'x2')} ${number(attributes, 'y2')}` }];
11
+ if (tag === 'circle') {
12
+ const cx = number(attributes, 'cx');
13
+ const cy = number(attributes, 'cy');
14
+ const radius = number(attributes, 'r');
15
+ return [{ d: `M${cx - radius} ${cy}A${radius} ${radius} 0 1 0 ${cx + radius} ${cy}A${radius} ${radius} 0 1 0 ${cx - radius} ${cy}` }];
16
+ }
17
+ return [];
18
+ });
19
+ }
package/dist/index.d.ts CHANGED
@@ -38,6 +38,7 @@ export * from './audio-player/index.js';
38
38
  export * from './carousel/index.js';
39
39
  export * from './choice-grid/index.js';
40
40
  export * from './color-picker/index.js';
41
+ export * from './color-swatch/index.js';
41
42
  export * from './context-menu/index.js';
42
43
  export * from './dockable-panel/index.js';
43
44
  export * from './editable/index.js';
@@ -62,6 +63,7 @@ export * from './transfer/index.js';
62
63
  export * from './tree-select/index.js';
63
64
  export * from './typewriter/index.js';
64
65
  export * from './virtual-list/index.js';
66
+ export * from './markdown/index.js';
65
67
  export type { ButtonGroupOrientation, ButtonAlign, ButtonLoadingPosition, ButtonGroupProps, ButtonLinkProps, ButtonProps, ButtonSize, ButtonVariant, } from './Button.js';
66
68
  export { PencilLine } from './PencilLine.js';
67
69
  export type { PencilLineOrientation, PencilLineProps } from './PencilLine.js';
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ export * from './audio-player/index.js';
38
38
  export * from './carousel/index.js';
39
39
  export * from './choice-grid/index.js';
40
40
  export * from './color-picker/index.js';
41
+ export * from './color-swatch/index.js';
41
42
  export * from './context-menu/index.js';
42
43
  export * from './dockable-panel/index.js';
43
44
  export * from './editable/index.js';
@@ -62,6 +63,7 @@ export * from './transfer/index.js';
62
63
  export * from './tree-select/index.js';
63
64
  export * from './typewriter/index.js';
64
65
  export * from './virtual-list/index.js';
66
+ export * from './markdown/index.js';
65
67
  export { PencilLine } from './PencilLine.js';
66
68
  export { PencilSurface } from './PencilSurface.js';
67
69
  export { PencilText } from './PencilText.js';
@@ -1,4 +1,6 @@
1
1
  import type { InputProps } from '../form/Input.js';
2
+ import { type InputNumberStepModifier } from './model.js';
3
+ export type { InputNumberStepModifier } from './model.js';
2
4
  export interface InputNumberValueChangeDetails {
3
5
  reason: 'input' | 'increment' | 'decrement';
4
6
  }
@@ -9,6 +11,10 @@ export interface InputNumberProps extends Omit<InputProps, 'type' | 'value' | 'd
9
11
  min?: number;
10
12
  max?: number;
11
13
  step?: number | 'any';
14
+ stepModifiers?: readonly InputNumberStepModifier[];
15
+ pressAndHold?: boolean;
16
+ repeatDelay?: number;
17
+ repeatInterval?: number;
12
18
  controls?: boolean;
13
19
  incrementButtonLabel?: string;
14
20
  decrementButtonLabel?: string;