@inkra/react 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +113 -3
  2. package/dist/Button.js +3 -2
  3. package/dist/PencilIcon.d.ts +10 -0
  4. package/dist/PencilIcon.js +83 -39
  5. package/dist/PencilLine.js +9 -1
  6. package/dist/PencilText.js +15 -9
  7. package/dist/accordion/Accordion.js +65 -5
  8. package/dist/alert-dialog/index.js +14 -4
  9. package/dist/audio-player/index.d.ts +2 -0
  10. package/dist/audio-player/index.js +131 -34
  11. package/dist/breadcrumb/index.js +4 -1
  12. package/dist/calendar/index.d.ts +4 -1
  13. package/dist/calendar/index.js +37 -12
  14. package/dist/calendar/locales.d.ts +7 -0
  15. package/dist/calendar/locales.js +2 -0
  16. package/dist/carousel/index.js +20 -10
  17. package/dist/choice-grid/index.d.ts +6 -1
  18. package/dist/choice-grid/index.js +63 -33
  19. package/dist/color-picker/GradientEditor.d.ts +40 -0
  20. package/dist/color-picker/GradientEditor.js +64 -0
  21. package/dist/color-picker/gradient.d.ts +25 -0
  22. package/dist/color-picker/gradient.js +60 -0
  23. package/dist/color-picker/index.d.ts +27 -7
  24. package/dist/color-picker/index.js +144 -40
  25. package/dist/color-swatch/index.d.ts +10 -0
  26. package/dist/color-swatch/index.js +9 -0
  27. package/dist/command/index.js +27 -4
  28. package/dist/content/index.d.ts +5 -0
  29. package/dist/content/index.js +15 -3
  30. package/dist/context-menu/index.js +1 -1
  31. package/dist/data-table/index.d.ts +7 -1
  32. package/dist/data-table/index.js +67 -16
  33. package/dist/data-table/model.d.ts +2 -0
  34. package/dist/data-table/model.js +7 -0
  35. package/dist/date-picker/index.d.ts +2 -0
  36. package/dist/date-picker/index.js +20 -21
  37. package/dist/day-agenda/index.d.ts +6 -1
  38. package/dist/day-agenda/index.js +49 -21
  39. package/dist/day-agenda/layout.js +65 -9
  40. package/dist/display/ProgressRing.js +7 -3
  41. package/dist/display/index.d.ts +4 -1
  42. package/dist/display/index.js +3 -2
  43. package/dist/dockable-panel/index.js +15 -13
  44. package/dist/drawer/index.js +11 -5
  45. package/dist/editable/index.d.ts +24 -2
  46. package/dist/editable/index.js +148 -40
  47. package/dist/empty/illustrations.d.ts +8 -0
  48. package/dist/empty/illustrations.js +78 -0
  49. package/dist/empty/index.d.ts +6 -1
  50. package/dist/empty/index.js +11 -3
  51. package/dist/flip/index.js +28 -8
  52. package/dist/form/Field.js +11 -10
  53. package/dist/form/Input.js +4 -1
  54. package/dist/form/Textarea.d.ts +3 -1
  55. package/dist/form/Textarea.js +9 -5
  56. package/dist/heatmap/index.js +4 -1
  57. package/dist/hotkey/index.d.ts +1 -0
  58. package/dist/hotkey/index.js +9 -4
  59. package/dist/hover-card/index.js +1 -1
  60. package/dist/i18n/catalog.d.ts +5 -0
  61. package/dist/i18n/catalog.js +289 -0
  62. package/dist/i18n/index.d.ts +23 -0
  63. package/dist/i18n/index.js +50 -0
  64. package/dist/image-cropper/index.js +21 -17
  65. package/dist/image-viewer/VideoResource.d.ts +21 -0
  66. package/dist/image-viewer/VideoResource.js +196 -0
  67. package/dist/image-viewer/index.d.ts +14 -0
  68. package/dist/image-viewer/index.js +141 -86
  69. package/dist/image-viewer/lucideGeometry.d.ts +3 -0
  70. package/dist/image-viewer/lucideGeometry.js +19 -0
  71. package/dist/index.d.ts +4 -0
  72. package/dist/index.js +4 -0
  73. package/dist/input-number/index.js +13 -5
  74. package/dist/internal/cache.d.ts +4 -0
  75. package/dist/internal/cache.js +18 -1
  76. package/dist/internal/iconGeometry.d.ts +2 -0
  77. package/dist/internal/iconGeometry.js +32 -0
  78. package/dist/internal/renderTokens.d.ts +4 -0
  79. package/dist/internal/renderTokens.js +4 -0
  80. package/dist/internal/stroke.d.ts +7 -0
  81. package/dist/internal/stroke.js +63 -7
  82. package/dist/internal/surfaceBudget.d.ts +20 -0
  83. package/dist/internal/surfaceBudget.js +69 -0
  84. package/dist/internal/surfaceRenderer.js +87 -28
  85. package/dist/internal/surfaceVisibility.d.ts +3 -0
  86. package/dist/internal/surfaceVisibility.js +60 -0
  87. package/dist/internal/textRenderer.d.ts +3 -0
  88. package/dist/internal/textRenderer.js +13 -5
  89. package/dist/internal/usePencilSurface.js +17 -1
  90. package/dist/internal/wash.js +4 -3
  91. package/dist/journey-map/index.d.ts +1 -1
  92. package/dist/journey-map/index.js +6 -3
  93. package/dist/journey-map/terrain.js +10 -5
  94. package/dist/keypad/index.js +4 -1
  95. package/dist/layout/index.js +1 -1
  96. package/dist/log/index.js +42 -33
  97. package/dist/markdown/CodeBlock.d.ts +19 -0
  98. package/dist/markdown/CodeBlock.js +86 -0
  99. package/dist/markdown/Deletion.d.ts +2 -0
  100. package/dist/markdown/Deletion.js +69 -0
  101. package/dist/markdown/icons.d.ts +14 -0
  102. package/dist/markdown/icons.js +16 -0
  103. package/dist/markdown/index.d.ts +17 -0
  104. package/dist/markdown/index.js +177 -0
  105. package/dist/node-graph/index.d.ts +3 -1
  106. package/dist/node-graph/index.js +63 -28
  107. package/dist/number-ticker/index.js +4 -1
  108. package/dist/number-ticker/precision.js +9 -2
  109. package/dist/overflow-list/index.js +9 -6
  110. package/dist/overlay/Overlay.js +8 -3
  111. package/dist/overlay/Popover.js +3 -3
  112. package/dist/overlay/themeBridge.js +14 -2
  113. package/dist/pagination/index.d.ts +10 -1
  114. package/dist/pagination/index.js +13 -9
  115. package/dist/pan-zoom/index.d.ts +2 -0
  116. package/dist/pan-zoom/index.js +12 -8
  117. package/dist/pencil.d.ts +1 -0
  118. package/dist/pencil.js +4 -4
  119. package/dist/radar/index.d.ts +2 -0
  120. package/dist/radar/index.js +44 -21
  121. package/dist/range-slider/index.js +4 -3
  122. package/dist/rating/index.d.ts +4 -0
  123. package/dist/rating/index.js +131 -24
  124. package/dist/resizable/index.d.ts +1 -1
  125. package/dist/resizable/index.js +5 -2
  126. package/dist/scroll-area/index.d.ts +1 -1
  127. package/dist/scroll-area/index.js +5 -2
  128. package/dist/select/MultipleSelect.js +5 -4
  129. package/dist/select/SearchableSelect.js +64 -7
  130. package/dist/select/Select.d.ts +1 -0
  131. package/dist/select/Select.js +6 -2
  132. package/dist/select/StepperSelect.d.ts +29 -0
  133. package/dist/select/StepperSelect.js +122 -0
  134. package/dist/select/index.d.ts +2 -0
  135. package/dist/select/index.js +1 -0
  136. package/dist/select/stepperModel.d.ts +7 -0
  137. package/dist/select/stepperModel.js +29 -0
  138. package/dist/selection/Checkbox.js +4 -3
  139. package/dist/selection/RadioGroup.js +29 -13
  140. package/dist/selection/Switch.js +4 -3
  141. package/dist/selection/useNativeChecked.d.ts +2 -1
  142. package/dist/selection/useNativeChecked.js +26 -8
  143. package/dist/slider/index.js +50 -6
  144. package/dist/sortable/index.js +17 -11
  145. package/dist/sparkline/index.js +21 -6
  146. package/dist/sparkline/model.js +11 -4
  147. package/dist/steps/index.js +4 -1
  148. package/dist/styles/accordion.css +228 -0
  149. package/dist/styles/alert-dialog.css +139 -0
  150. package/dist/styles/audio-player.css +22 -0
  151. package/dist/styles/base.css +638 -0
  152. package/dist/styles/breadcrumb.css +7 -0
  153. package/dist/styles/calendar.css +62 -0
  154. package/dist/styles/carousel.css +173 -0
  155. package/dist/styles/choice-grid.css +226 -0
  156. package/dist/styles/color-picker.css +281 -0
  157. package/dist/styles/color-swatch.css +47 -0
  158. package/dist/styles/command.css +52 -0
  159. package/dist/styles/content.css +79 -0
  160. package/dist/styles/context-menu.css +35 -0
  161. package/dist/styles/data-table.css +31 -0
  162. package/dist/styles/date-picker.css +107 -0
  163. package/dist/styles/day-agenda.css +30 -0
  164. package/dist/styles/display.css +304 -0
  165. package/dist/styles/dockable-panel.css +110 -0
  166. package/dist/styles/drawer.css +133 -0
  167. package/dist/styles/dropdown-menu.css +68 -0
  168. package/dist/styles/editable.css +145 -0
  169. package/dist/styles/empty.css +108 -0
  170. package/dist/styles/flip.css +101 -0
  171. package/dist/styles/form.css +634 -0
  172. package/dist/styles/heatmap.css +256 -0
  173. package/dist/styles/hotkey.css +189 -0
  174. package/dist/styles/hover-card.css +27 -0
  175. package/dist/styles/image-cropper.css +205 -0
  176. package/dist/styles/image-viewer.css +422 -0
  177. package/dist/styles/input-number.css +21 -0
  178. package/dist/styles/journey-map.css +19 -0
  179. package/dist/styles/keypad.css +92 -0
  180. package/dist/styles/layout.css +72 -0
  181. package/dist/styles/leaflet.css +661 -0
  182. package/dist/styles/log.css +203 -0
  183. package/dist/styles/markdown.css +379 -0
  184. package/dist/styles/node-graph.css +142 -0
  185. package/dist/styles/number-ticker.css +83 -0
  186. package/dist/styles/overflow-list.css +51 -0
  187. package/dist/styles/overlay.css +284 -0
  188. package/dist/styles/pagination.css +21 -0
  189. package/dist/styles/pan-zoom.css +136 -0
  190. package/dist/styles/radar.css +1 -0
  191. package/dist/styles/rating.css +121 -0
  192. package/dist/styles/resizable.css +108 -0
  193. package/dist/styles/scroll-area.css +122 -0
  194. package/dist/styles/select.css +554 -0
  195. package/dist/styles/selection.css +264 -0
  196. package/dist/styles/slider.css +145 -0
  197. package/dist/styles/slot.css +109 -0
  198. package/dist/styles/sortable.css +159 -0
  199. package/dist/styles/sparkline.css +24 -0
  200. package/dist/styles/steps.css +27 -0
  201. package/dist/styles/tabs.css +79 -0
  202. package/dist/styles/tag-input.css +213 -0
  203. package/dist/styles/time-picker.css +66 -0
  204. package/dist/styles/timeline.css +24 -0
  205. package/dist/styles/toast.css +160 -0
  206. package/dist/styles/toggle-group.css +107 -0
  207. package/dist/styles/tooltip.css +78 -0
  208. package/dist/styles/transfer.css +148 -0
  209. package/dist/styles/tree-select.css +131 -0
  210. package/dist/styles/tree.css +59 -0
  211. package/dist/styles/typewriter.css +62 -0
  212. package/dist/styles/upload.css +60 -0
  213. package/dist/styles/virtual-list.css +112 -0
  214. package/dist/styles.css +2091 -421
  215. package/dist/tabs/index.d.ts +9 -0
  216. package/dist/tabs/index.js +69 -16
  217. package/dist/tabs/useOverflow.d.ts +20 -0
  218. package/dist/tabs/useOverflow.js +145 -0
  219. package/dist/tag-input/index.d.ts +7 -2
  220. package/dist/tag-input/index.js +81 -42
  221. package/dist/theme/index.d.ts +11 -0
  222. package/dist/theme/index.js +23 -0
  223. package/dist/time-picker/index.js +45 -27
  224. package/dist/timeline/index.js +4 -1
  225. package/dist/toast/index.d.ts +19 -11
  226. package/dist/toast/index.js +51 -8
  227. package/dist/toast/manager.d.ts +42 -0
  228. package/dist/toast/manager.js +85 -0
  229. package/dist/toast/useToastNavigation.d.ts +3 -0
  230. package/dist/toast/useToastNavigation.js +75 -0
  231. package/dist/tokens.css +4 -0
  232. package/dist/tooltip/Tooltip.js +3 -3
  233. package/dist/transfer/index.d.ts +5 -0
  234. package/dist/transfer/index.js +120 -53
  235. package/dist/transfer/model.d.ts +4 -0
  236. package/dist/transfer/model.js +37 -11
  237. package/dist/tree/index.d.ts +5 -1
  238. package/dist/tree/index.js +11 -6
  239. package/dist/tree-select/index.js +70 -31
  240. package/dist/tree-select/model.d.ts +11 -5
  241. package/dist/tree-select/model.js +111 -83
  242. package/dist/typewriter/index.js +10 -10
  243. package/dist/upload/index.js +50 -33
  244. package/dist/virtual-list/index.js +11 -6
  245. package/package.json +423 -3
@@ -0,0 +1,196 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useInkraLocale } from '../i18n/index.js';
3
+ import { __iconData as pauseIcon } from 'lucide-react/dist/esm/icons/pause.mjs';
4
+ import { __iconData as playIcon } from 'lucide-react/dist/esm/icons/play.mjs';
5
+ import { __iconData as volumeIcon } from 'lucide-react/dist/esm/icons/volume-2.mjs';
6
+ import { __iconData as muteIcon } from 'lucide-react/dist/esm/icons/volume-x.mjs';
7
+ import { useCallback, useEffect, useRef, useState } from 'react';
8
+ import { Button } from '../Button.js';
9
+ import { PencilIcon } from '../PencilIcon.js';
10
+ import { PencilText } from '../PencilText.js';
11
+ import { Select } from '../select/index.js';
12
+ import { Slider } from '../slider/index.js';
13
+ function number(attributes, name) {
14
+ const value = Number(attributes[name]);
15
+ return Number.isFinite(value) ? value : 0;
16
+ }
17
+ function iconPaths(nodes) {
18
+ return nodes.flatMap(([tag, attributes]) => {
19
+ if (tag === 'path' && attributes.d)
20
+ return [{ d: attributes.d }];
21
+ if (tag === 'line')
22
+ return [{ d: `M${number(attributes, 'x1')} ${number(attributes, 'y1')}L${number(attributes, 'x2')} ${number(attributes, 'y2')}` }];
23
+ if (tag === 'rect') {
24
+ const x = number(attributes, 'x');
25
+ const y = number(attributes, 'y');
26
+ const width = number(attributes, 'width');
27
+ const height = number(attributes, 'height');
28
+ const radius = Math.min(number(attributes, 'rx'), width / 2, height / 2);
29
+ return [{ d: radius
30
+ ? `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`
31
+ : `M${x} ${y}H${x + width}V${y + height}H${x}Z` }];
32
+ }
33
+ return [];
34
+ });
35
+ }
36
+ const playPaths = iconPaths(playIcon.node);
37
+ const pausePaths = iconPaths(pauseIcon.node);
38
+ const volumePaths = iconPaths(volumeIcon.node);
39
+ const mutePaths = iconPaths(muteIcon.node);
40
+ const rates = [0.75, 1, 1.25, 1.5, 2];
41
+ const defaultLabels = (t) => ({
42
+ controls: t('video.controls'),
43
+ play: t('video.play'),
44
+ pause: t('video.pause'),
45
+ seek: t('video.seek'),
46
+ mute: t('video.mute'),
47
+ unmute: t('video.unmute'),
48
+ volume: t('video.volume'),
49
+ rate: t('audio.rate'),
50
+ playbackError: t('video.error'),
51
+ time: (current, duration) => `${formatTime(current)} / ${formatTime(duration)}`,
52
+ });
53
+ function clamp(value, min, max) {
54
+ return Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : min;
55
+ }
56
+ function formatTime(value) {
57
+ if (!Number.isFinite(value) || value < 0)
58
+ return '0:00';
59
+ const seconds = Math.floor(value);
60
+ return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
61
+ }
62
+ export function VideoResource({ alt, labels: overrides, onError, onLoad, poster, src }) {
63
+ const { t } = useInkraLocale();
64
+ const videoRef = useRef(null);
65
+ const sourceRef = useRef(src);
66
+ sourceRef.current = src;
67
+ const mountedRef = useRef(false);
68
+ const generationRef = useRef(0);
69
+ const playRequestRef = useRef(0);
70
+ const callbacksRef = useRef({ onError, onLoad });
71
+ callbacksRef.current = { onError, onLoad };
72
+ const [playing, setPlaying] = useState(false);
73
+ const [currentTime, setCurrentTime] = useState(0);
74
+ const [duration, setDuration] = useState(0);
75
+ const [volume, setVolumeState] = useState(1);
76
+ const [muted, setMutedState] = useState(false);
77
+ const [playbackRate, setPlaybackRateState] = useState(1);
78
+ const [playbackError, setPlaybackError] = useState(false);
79
+ const labels = { ...defaultLabels(t), ...overrides };
80
+ const sync = useCallback(() => {
81
+ const video = videoRef.current;
82
+ if (!video)
83
+ return;
84
+ setPlaying(!video.paused && !video.ended);
85
+ setCurrentTime(Number.isFinite(video.currentTime) ? video.currentTime : 0);
86
+ setDuration(Number.isFinite(video.duration) ? video.duration : 0);
87
+ setVolumeState(video.volume);
88
+ setMutedState(video.muted);
89
+ setPlaybackRateState(video.playbackRate);
90
+ }, []);
91
+ useEffect(() => {
92
+ mountedRef.current = true;
93
+ return () => {
94
+ mountedRef.current = false;
95
+ generationRef.current += 1;
96
+ playRequestRef.current += 1;
97
+ videoRef.current?.pause();
98
+ };
99
+ }, []);
100
+ useEffect(() => {
101
+ const video = videoRef.current;
102
+ if (!video)
103
+ return undefined;
104
+ const handlers = {
105
+ loadedmetadata: () => { sync(); callbacksRef.current.onLoad?.(); },
106
+ durationchange: sync,
107
+ timeupdate: sync,
108
+ play: sync,
109
+ pause: sync,
110
+ ended: sync,
111
+ volumechange: sync,
112
+ ratechange: sync,
113
+ emptied: sync,
114
+ error: () => { sync(); callbacksRef.current.onError?.(); },
115
+ };
116
+ Object.entries(handlers).forEach(([name, handler]) => video.addEventListener(name, handler));
117
+ return () => Object.entries(handlers).forEach(([name, handler]) => video.removeEventListener(name, handler));
118
+ }, [sync]);
119
+ useEffect(() => {
120
+ const video = videoRef.current;
121
+ if (!video)
122
+ return undefined;
123
+ generationRef.current += 1;
124
+ playRequestRef.current += 1;
125
+ video.pause();
126
+ try {
127
+ video.currentTime = 0;
128
+ }
129
+ catch { /* A source may not have a timeline yet. */ }
130
+ setPlaying(false);
131
+ setCurrentTime(0);
132
+ setDuration(0);
133
+ setPlaybackError(false);
134
+ video.load();
135
+ return () => { video.pause(); };
136
+ }, [src]);
137
+ const togglePlay = async () => {
138
+ const video = videoRef.current;
139
+ if (!video)
140
+ return;
141
+ if (!video.paused) {
142
+ playRequestRef.current += 1;
143
+ video.pause();
144
+ return;
145
+ }
146
+ const generation = generationRef.current;
147
+ const source = sourceRef.current;
148
+ const request = ++playRequestRef.current;
149
+ setPlaybackError(false);
150
+ try {
151
+ await video.play();
152
+ if (!mountedRef.current || generation !== generationRef.current || source !== sourceRef.current || request !== playRequestRef.current)
153
+ return;
154
+ sync();
155
+ }
156
+ catch {
157
+ if (!mountedRef.current || generation !== generationRef.current || source !== sourceRef.current || request !== playRequestRef.current)
158
+ return;
159
+ setPlaybackError(true);
160
+ sync();
161
+ }
162
+ };
163
+ const seek = (next) => {
164
+ const video = videoRef.current;
165
+ if (!video || !Number.isFinite(next))
166
+ return;
167
+ video.currentTime = clamp(next, 0, Number.isFinite(video.duration) ? video.duration : Math.max(0, next));
168
+ sync();
169
+ };
170
+ const setVolume = (next) => {
171
+ const video = videoRef.current;
172
+ if (!video)
173
+ return;
174
+ video.volume = clamp(next, 0, 1);
175
+ if (video.muted && video.volume > 0)
176
+ video.muted = false;
177
+ sync();
178
+ };
179
+ const setMuted = (next) => {
180
+ const video = videoRef.current;
181
+ if (!video)
182
+ return;
183
+ video.muted = next;
184
+ sync();
185
+ };
186
+ const setRate = (next) => {
187
+ const video = videoRef.current;
188
+ if (!video)
189
+ return;
190
+ const rate = clamp(next, 0.25, 4);
191
+ video.defaultPlaybackRate = rate;
192
+ video.playbackRate = rate;
193
+ sync();
194
+ };
195
+ 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)) }) }) })] })] })] })] });
196
+ }
@@ -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';
@@ -1,7 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useInkraLocale } from '../i18n/index.js';
2
3
  import { __iconData as chevronLeftIcon } from 'lucide-react/dist/esm/icons/chevron-left.mjs';
3
4
  import { __iconData as chevronRightIcon } from 'lucide-react/dist/esm/icons/chevron-right.mjs';
4
5
  import { __iconData as imageOffIcon } from 'lucide-react/dist/esm/icons/image-off.mjs';
6
+ import { __iconData as playIcon } from 'lucide-react/dist/esm/icons/play.mjs';
5
7
  import { __iconData as rotateIcon } from 'lucide-react/dist/esm/icons/rotate-cw.mjs';
6
8
  import { __iconData as zoomInIcon } from 'lucide-react/dist/esm/icons/zoom-in.mjs';
7
9
  import { __iconData as zoomOutIcon } from 'lucide-react/dist/esm/icons/zoom-out.mjs';
@@ -11,14 +13,17 @@ import { PencilIcon } from '../PencilIcon.js';
11
13
  import { PencilSurface } from '../PencilSurface.js';
12
14
  import { PencilText } from '../PencilText.js';
13
15
  import { PanZoom } from '../pan-zoom/index.js';
14
- import { OverlayBackdrop, OverlayContent, OverlayDescription, OverlayHeader, OverlayPortal, OverlayRoot, OverlayTitle, OverlayTrigger, } from '../overlay/index.js';
16
+ import { OverlayBackdrop, OverlayCloseButton, OverlayContent, OverlayDescription, OverlayPortal, OverlayRoot, OverlayTitle, OverlayTrigger, } from '../overlay/index.js';
17
+ import { lucideNodesToPencilPaths } from './lucideGeometry.js';
15
18
  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 }));
19
+ import { VideoResource } from './VideoResource.js';
20
+ const chevronLeftPaths = lucideNodesToPencilPaths(chevronLeftIcon.node);
21
+ const chevronRightPaths = lucideNodesToPencilPaths(chevronRightIcon.node);
22
+ const imageOffPaths = lucideNodesToPencilPaths(imageOffIcon.node);
23
+ const playPaths = lucideNodesToPencilPaths(playIcon.node);
24
+ const rotatePaths = lucideNodesToPencilPaths(rotateIcon.node);
25
+ const zoomInPaths = lucideNodesToPencilPaths(zoomInIcon.node);
26
+ const zoomOutPaths = lucideNodesToPencilPaths(zoomOutIcon.node);
22
27
  const ImageViewerContext = createContext(null);
23
28
  function useImageViewerContext() {
24
29
  const context = useContext(ImageViewerContext);
@@ -73,9 +78,10 @@ export function ImageViewerRoot({ activeId, defaultActiveId, defaultOpen = false
73
78
  activeItem: selection.index < 0 ? null : items[selection.index],
74
79
  activeIndex: selection.index,
75
80
  loop,
81
+ open: openControlled ? Boolean(open) : internalOpen,
76
82
  selectIndex,
77
83
  step,
78
- }), [items, loop, selectIndex, selection.index, step]);
84
+ }), [internalOpen, items, loop, open, openControlled, selectIndex, selection.index, step]);
79
85
  return (_jsx(ImageViewerContext.Provider, { value: context, children: _jsx(OverlayRoot, { ...props, open: openControlled ? open : internalOpen, onOpenChange: (nextOpen) => {
80
86
  if (!openControlled)
81
87
  setInternalOpen(nextOpen);
@@ -85,46 +91,70 @@ export function ImageViewerRoot({ activeId, defaultActiveId, defaultOpen = false
85
91
  export const ImageViewerTrigger = forwardRef(function ImageViewerTrigger(props, ref) {
86
92
  return _jsx(OverlayTrigger, { ...props, ref: ref });
87
93
  });
88
- const defaultLabels = {
89
- title: 'Image viewer',
90
- description: 'Use the previous and next controls or arrow keys to browse images.',
91
- close: 'Close image viewer',
92
- previous: 'Previous image',
93
- next: 'Next image',
94
- thumbnails: 'Image thumbnails',
95
- imagePosition: (current, total) => `${current} of ${total}`,
96
- thumbnail: (item, current, total) => `Show ${item.alt}, ${current} of ${total}`,
97
- failedTitle: 'Image unavailable',
98
- failedDescription: item => `${item.alt} could not be loaded.`,
99
- retry: 'Retry',
100
- zoomIn: 'Zoom in',
101
- zoomOut: 'Zoom out',
102
- resetZoom: 'Reset zoom',
103
- rotate: 'Rotate clockwise',
104
- panZoom: item => `Pan and zoom ${item.alt}`,
105
- emptyTitle: 'No images',
106
- emptyDescription: 'There are no images to show.',
107
- };
94
+ const defaultLabels = (t) => ({
95
+ title: t('viewer.title'),
96
+ description: t('viewer.description'),
97
+ close: t('viewer.close'),
98
+ previous: t('viewer.previous'),
99
+ next: t('viewer.next'),
100
+ thumbnails: t('viewer.thumbnails'),
101
+ imagePosition: (current, total) => t('viewer.position', { current, total }),
102
+ thumbnail: (item, current, total) => t('viewer.thumbnail', { label: item.alt, current, total }),
103
+ failedTitle: t('viewer.failed'),
104
+ failedDescription: item => t('viewer.failedDescription', { label: item.alt }),
105
+ retry: t('common.retry'),
106
+ zoomIn: t('common.zoomIn'),
107
+ zoomOut: t('common.zoomOut'),
108
+ resetZoom: t('viewer.reset'),
109
+ rotate: t('viewer.rotate'),
110
+ panZoom: item => t('viewer.panZoom', { label: item.alt }),
111
+ emptyTitle: t('viewer.empty'),
112
+ emptyDescription: t('viewer.emptyDescription'),
113
+ });
108
114
  function directionFor(node) {
109
115
  return getComputedStyle(node).direction === 'rtl' ? 'rtl' : 'ltr';
110
116
  }
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();
113
- const labels = { ...defaultLabels, ...labelOverrides };
117
+ 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) {
118
+ const { t } = useInkraLocale();
119
+ const { activeIndex, activeItem, items, loop, open, selectIndex, step } = useImageViewerContext();
120
+ const labels = { ...defaultLabels(t), ...labelOverrides };
114
121
  const stageRef = useRef(null);
122
+ const thumbnailsRef = useRef(null);
115
123
  const thumbnailRefs = useRef(new Map());
116
124
  const panZoomRef = useRef(null);
117
- const [failedIds, setFailedIds] = useState(() => new Set());
125
+ const focusFrameRef = useRef(null);
126
+ const fitFrameRef = useRef(null);
127
+ useEffect(() => () => {
128
+ if (focusFrameRef.current !== null)
129
+ cancelAnimationFrame(focusFrameRef.current);
130
+ if (fitFrameRef.current !== null)
131
+ cancelAnimationFrame(fitFrameRef.current);
132
+ }, []);
133
+ const [failedResources, setFailedResources] = useState(() => new Set());
118
134
  const [attempts, setAttempts] = useState({});
119
135
  const [rotation, setRotation] = useState(0);
120
136
  const total = items.length;
121
137
  const atStart = activeIndex <= 0;
122
138
  const atEnd = activeIndex >= total - 1;
139
+ const activeItemType = activeItem?.type ?? 'image';
140
+ const activeItemKey = activeItem ? JSON.stringify([activeItemType, activeItem.id, activeItem.src]) : undefined;
141
+ const activeResourceRef = useRef(activeItemKey);
142
+ activeResourceRef.current = activeItemKey;
123
143
  useEffect(() => {
124
- if (activeItem)
125
- thumbnailRefs.current.get(activeItem.id)?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
144
+ const thumbnail = activeItem ? thumbnailRefs.current.get(activeItem.id) : undefined;
145
+ const viewport = thumbnailsRef.current;
146
+ if (thumbnail && viewport) {
147
+ const item = thumbnail.getBoundingClientRect();
148
+ const clip = viewport.getBoundingClientRect();
149
+ if (item.left < clip.left)
150
+ viewport.scrollLeft -= clip.left - item.left;
151
+ else if (item.right > clip.right)
152
+ viewport.scrollLeft += item.right - clip.right;
153
+ }
126
154
  setRotation(0);
127
- }, [activeItem]);
155
+ setFailedResources(current => activeItemKey && current.has(activeItemKey) ? new Set([activeItemKey]) : new Set());
156
+ setAttempts(current => activeItemKey && current[activeItemKey] !== undefined ? { [activeItemKey]: current[activeItemKey] } : {});
157
+ }, [activeItemKey]);
128
158
  const handleStageKeyDown = (event) => {
129
159
  if (event.target !== event.currentTarget)
130
160
  return;
@@ -149,72 +179,97 @@ export const ImageViewerContent = forwardRef(function ImageViewerContent({ class
149
179
  const selectThumbnail = (index, reason = 'thumbnail') => {
150
180
  selectIndex(index, reason);
151
181
  const item = items[index];
152
- if (item)
153
- requestAnimationFrame(() => thumbnailRefs.current.get(item.id)?.focus());
182
+ if (focusFrameRef.current !== null)
183
+ cancelAnimationFrame(focusFrameRef.current);
184
+ focusFrameRef.current = item ? requestAnimationFrame(() => {
185
+ focusFrameRef.current = null;
186
+ thumbnailRefs.current.get(item.id)?.focus();
187
+ }) : null;
154
188
  };
155
189
  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;
190
+ const failed = activeItemKey ? failedResources.has(activeItemKey) : false;
191
+ const imageProps = activeItem && activeItemKey && activeItemType === 'image' ? (() => {
192
+ const resourceKey = activeItemKey;
193
+ return {
194
+ alt: activeItem.alt,
195
+ className: 'inkra-image-viewer__image',
196
+ height: activeItem.height,
197
+ key: `${resourceKey}:${attempts[resourceKey] ?? 0}`,
198
+ onError: () => setFailedResources(current => new Set(current).add(resourceKey)),
199
+ onLoad: () => setFailedResources(current => {
200
+ if (!current.has(resourceKey))
201
+ return current;
202
+ const next = new Set(current);
203
+ next.delete(resourceKey);
204
+ return next;
205
+ }),
206
+ src: activeItem.src,
207
+ width: activeItem.width,
208
+ };
209
+ })() : null;
173
210
  return (_jsxs(OverlayPortal, { children: [_jsx(OverlayBackdrop, {}), _jsxs(OverlayContent, { ...props, className: ['inkra-image-viewer', className].filter(Boolean).join(' '), layout: "sections", onOpenAutoFocus: (event) => {
174
211
  onOpenAutoFocus?.(event);
175
212
  if (event.defaultPrevented)
176
213
  return;
177
214
  event.preventDefault();
178
215
  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 => {
216
+ }, 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: () => {
217
+ if (!activeItemKey)
218
+ return;
219
+ const failedKey = activeItemKey;
220
+ setFailedResources(current => {
182
221
  const next = new Set(current);
183
- next.delete(failedId);
222
+ next.delete(failedKey);
184
223
  return next;
185
224
  });
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: () => {
225
+ setAttempts(current => ({ ...current, [failedKey]: (current[failedKey] ?? 0) + 1 }));
226
+ }, children: labels.retry })] }) : activeItemType === 'video' && open ? _jsx(VideoResource, { alt: activeItem.alt, labels: videoLabels, onError: () => setFailedResources(current => new Set(current).add(activeItemKey)), onLoad: () => setFailedResources(current => {
227
+ if (!current.has(activeItemKey))
228
+ return current;
229
+ const next = new Set(current);
230
+ next.delete(activeItemKey);
231
+ return next;
232
+ }), 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
233
  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] })] }));
234
+ if (fitFrameRef.current !== null)
235
+ cancelAnimationFrame(fitFrameRef.current);
236
+ const resourceKey = activeItemKey;
237
+ fitFrameRef.current = requestAnimationFrame(() => {
238
+ fitFrameRef.current = null;
239
+ if (activeResourceRef.current === resourceKey)
240
+ void panZoomRef.current?.fit({ mode: 'contain' });
241
+ });
242
+ }, 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) => {
243
+ const selected = index === activeIndex;
244
+ 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) => {
245
+ const rtl = directionFor(event.currentTarget) === 'rtl';
246
+ let nextIndex = index;
247
+ if (event.key === 'ArrowLeft')
248
+ nextIndex = nextImageViewerIndex(total, index, rtl ? 1 : -1, loop);
249
+ else if (event.key === 'ArrowRight')
250
+ nextIndex = nextImageViewerIndex(total, index, rtl ? -1 : 1, loop);
251
+ else if (event.key === 'Home')
252
+ nextIndex = 0;
253
+ else if (event.key === 'End')
254
+ nextIndex = total - 1;
255
+ else
256
+ return;
257
+ event.preventDefault();
258
+ selectThumbnail(nextIndex, event.key === 'Home' ? 'home' : event.key === 'End' ? 'end' : 'thumbnail');
259
+ }, ref: (node) => {
260
+ if (node)
261
+ thumbnailRefs.current.set(item.id, node);
262
+ else
263
+ thumbnailRefs.current.delete(item.id);
264
+ }, 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))
265
+ ? _jsx("img", { alt: "", className: "inkra-image-viewer__thumbnail-image", draggable: false, src: item.thumbnailSrc ?? item.poster ?? item.src })
266
+ : _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);
267
+ }) }) : null] }) : null] })] }));
214
268
  });
215
- export const ImageViewer = Object.freeze({
269
+ export const ImageViewer = /* @__PURE__ */ Object.freeze({
216
270
  Root: ImageViewerRoot,
217
271
  Trigger: ImageViewerTrigger,
218
272
  Content: ImageViewerContent,
219
273
  });
274
+ export const MediaViewer = ImageViewer;
220
275
  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
@@ -1,4 +1,6 @@
1
1
  export { Button, ButtonGroup, ButtonLink } from './Button.js';
2
+ export * from './i18n/index.js';
3
+ export * from './theme/index.js';
2
4
  export * from './overlay/index.js';
3
5
  export * from './form/index.js';
4
6
  export * from './input-number/index.js';
@@ -38,6 +40,7 @@ export * from './audio-player/index.js';
38
40
  export * from './carousel/index.js';
39
41
  export * from './choice-grid/index.js';
40
42
  export * from './color-picker/index.js';
43
+ export * from './color-swatch/index.js';
41
44
  export * from './context-menu/index.js';
42
45
  export * from './dockable-panel/index.js';
43
46
  export * from './editable/index.js';
@@ -62,6 +65,7 @@ export * from './transfer/index.js';
62
65
  export * from './tree-select/index.js';
63
66
  export * from './typewriter/index.js';
64
67
  export * from './virtual-list/index.js';
68
+ export * from './markdown/index.js';
65
69
  export type { ButtonGroupOrientation, ButtonAlign, ButtonLoadingPosition, ButtonGroupProps, ButtonLinkProps, ButtonProps, ButtonSize, ButtonVariant, } from './Button.js';
66
70
  export { PencilLine } from './PencilLine.js';
67
71
  export type { PencilLineOrientation, PencilLineProps } from './PencilLine.js';
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export { Button, ButtonGroup, ButtonLink } from './Button.js';
2
+ export * from './i18n/index.js';
3
+ export * from './theme/index.js';
2
4
  export * from './overlay/index.js';
3
5
  export * from './form/index.js';
4
6
  export * from './input-number/index.js';
@@ -38,6 +40,7 @@ export * from './audio-player/index.js';
38
40
  export * from './carousel/index.js';
39
41
  export * from './choice-grid/index.js';
40
42
  export * from './color-picker/index.js';
43
+ export * from './color-swatch/index.js';
41
44
  export * from './context-menu/index.js';
42
45
  export * from './dockable-panel/index.js';
43
46
  export * from './editable/index.js';
@@ -62,6 +65,7 @@ export * from './transfer/index.js';
62
65
  export * from './tree-select/index.js';
63
66
  export * from './typewriter/index.js';
64
67
  export * from './virtual-list/index.js';
68
+ export * from './markdown/index.js';
65
69
  export { PencilLine } from './PencilLine.js';
66
70
  export { PencilSurface } from './PencilSurface.js';
67
71
  export { PencilText } from './PencilText.js';