@inkra/react 0.3.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/README.md +15 -3
  2. package/dist/Button.js +3 -2
  3. package/dist/PencilIcon.d.ts +10 -0
  4. package/dist/PencilIcon.js +83 -39
  5. package/dist/PencilLine.js +9 -1
  6. package/dist/PencilText.js +15 -9
  7. package/dist/accordion/Accordion.js +65 -5
  8. package/dist/alert-dialog/index.js +14 -4
  9. package/dist/audio-player/index.d.ts +2 -0
  10. package/dist/audio-player/index.js +126 -31
  11. package/dist/calendar/index.d.ts +4 -1
  12. package/dist/calendar/index.js +31 -12
  13. package/dist/carousel/index.js +13 -6
  14. package/dist/choice-grid/index.d.ts +6 -1
  15. package/dist/choice-grid/index.js +61 -33
  16. package/dist/color-picker/GradientEditor.d.ts +39 -0
  17. package/dist/color-picker/GradientEditor.js +63 -0
  18. package/dist/color-picker/gradient.d.ts +25 -0
  19. package/dist/color-picker/gradient.js +60 -0
  20. package/dist/color-picker/index.d.ts +27 -7
  21. package/dist/color-picker/index.js +134 -32
  22. package/dist/color-swatch/index.d.ts +10 -0
  23. package/dist/color-swatch/index.js +9 -0
  24. package/dist/command/index.js +27 -4
  25. package/dist/content/index.d.ts +5 -0
  26. package/dist/content/index.js +12 -3
  27. package/dist/context-menu/index.js +1 -1
  28. package/dist/data-table/index.d.ts +7 -1
  29. package/dist/data-table/index.js +64 -16
  30. package/dist/data-table/model.d.ts +2 -0
  31. package/dist/data-table/model.js +7 -0
  32. package/dist/date-picker/index.d.ts +2 -0
  33. package/dist/date-picker/index.js +10 -9
  34. package/dist/day-agenda/index.d.ts +6 -1
  35. package/dist/day-agenda/index.js +46 -21
  36. package/dist/day-agenda/layout.js +65 -9
  37. package/dist/display/ProgressRing.js +7 -3
  38. package/dist/display/index.d.ts +4 -1
  39. package/dist/display/index.js +3 -2
  40. package/dist/dockable-panel/index.js +5 -5
  41. package/dist/drawer/index.js +4 -3
  42. package/dist/editable/index.d.ts +24 -2
  43. package/dist/editable/index.js +142 -37
  44. package/dist/empty/illustrations.d.ts +8 -0
  45. package/dist/empty/illustrations.js +78 -0
  46. package/dist/empty/index.d.ts +6 -1
  47. package/dist/empty/index.js +11 -3
  48. package/dist/flip/index.js +28 -8
  49. package/dist/form/Field.js +11 -10
  50. package/dist/form/Textarea.d.ts +3 -1
  51. package/dist/form/Textarea.js +6 -4
  52. package/dist/hotkey/index.d.ts +1 -0
  53. package/dist/hotkey/index.js +6 -4
  54. package/dist/hover-card/index.js +1 -1
  55. package/dist/image-cropper/index.js +4 -2
  56. package/dist/image-viewer/VideoResource.d.ts +21 -0
  57. package/dist/image-viewer/VideoResource.js +194 -0
  58. package/dist/image-viewer/index.d.ts +14 -0
  59. package/dist/image-viewer/index.js +118 -65
  60. package/dist/image-viewer/lucideGeometry.d.ts +3 -0
  61. package/dist/image-viewer/lucideGeometry.js +19 -0
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.js +2 -0
  64. package/dist/input-number/index.js +9 -4
  65. package/dist/internal/cache.d.ts +4 -0
  66. package/dist/internal/cache.js +18 -1
  67. package/dist/internal/iconGeometry.d.ts +2 -0
  68. package/dist/internal/iconGeometry.js +32 -0
  69. package/dist/internal/renderTokens.d.ts +4 -0
  70. package/dist/internal/renderTokens.js +4 -0
  71. package/dist/internal/stroke.d.ts +7 -0
  72. package/dist/internal/stroke.js +63 -7
  73. package/dist/internal/surfaceBudget.d.ts +20 -0
  74. package/dist/internal/surfaceBudget.js +69 -0
  75. package/dist/internal/surfaceRenderer.js +87 -28
  76. package/dist/internal/surfaceVisibility.d.ts +3 -0
  77. package/dist/internal/surfaceVisibility.js +60 -0
  78. package/dist/internal/textRenderer.d.ts +2 -0
  79. package/dist/internal/textRenderer.js +3 -3
  80. package/dist/internal/usePencilSurface.js +17 -1
  81. package/dist/internal/wash.js +4 -3
  82. package/dist/journey-map/terrain.js +10 -5
  83. package/dist/layout/index.js +1 -1
  84. package/dist/log/index.js +29 -22
  85. package/dist/markdown/CodeBlock.d.ts +19 -0
  86. package/dist/markdown/CodeBlock.js +86 -0
  87. package/dist/markdown/Deletion.d.ts +2 -0
  88. package/dist/markdown/Deletion.js +69 -0
  89. package/dist/markdown/icons.d.ts +14 -0
  90. package/dist/markdown/icons.js +16 -0
  91. package/dist/markdown/index.d.ts +17 -0
  92. package/dist/markdown/index.js +175 -0
  93. package/dist/node-graph/index.d.ts +2 -0
  94. package/dist/node-graph/index.js +49 -25
  95. package/dist/number-ticker/precision.js +9 -2
  96. package/dist/overflow-list/index.js +5 -2
  97. package/dist/overlay/Overlay.js +1 -1
  98. package/dist/overlay/Popover.js +3 -3
  99. package/dist/overlay/themeBridge.js +14 -2
  100. package/dist/pagination/index.d.ts +10 -1
  101. package/dist/pagination/index.js +6 -4
  102. package/dist/pan-zoom/index.d.ts +2 -0
  103. package/dist/pan-zoom/index.js +7 -6
  104. package/dist/pencil.d.ts +1 -0
  105. package/dist/pencil.js +4 -4
  106. package/dist/radar/index.d.ts +2 -0
  107. package/dist/radar/index.js +40 -20
  108. package/dist/rating/index.d.ts +4 -0
  109. package/dist/rating/index.js +128 -24
  110. package/dist/resizable/index.js +1 -1
  111. package/dist/scroll-area/index.d.ts +1 -1
  112. package/dist/scroll-area/index.js +5 -2
  113. package/dist/select/MultipleSelect.js +5 -4
  114. package/dist/select/SearchableSelect.js +61 -6
  115. package/dist/select/Select.d.ts +1 -0
  116. package/dist/select/Select.js +3 -1
  117. package/dist/select/StepperSelect.d.ts +29 -0
  118. package/dist/select/StepperSelect.js +119 -0
  119. package/dist/select/index.d.ts +2 -0
  120. package/dist/select/index.js +1 -0
  121. package/dist/select/stepperModel.d.ts +7 -0
  122. package/dist/select/stepperModel.js +29 -0
  123. package/dist/selection/Checkbox.js +4 -3
  124. package/dist/selection/RadioGroup.js +29 -13
  125. package/dist/selection/Switch.js +4 -3
  126. package/dist/selection/useNativeChecked.d.ts +2 -1
  127. package/dist/selection/useNativeChecked.js +26 -8
  128. package/dist/slider/index.js +50 -6
  129. package/dist/sortable/index.js +6 -2
  130. package/dist/sparkline/index.js +17 -2
  131. package/dist/sparkline/model.js +11 -4
  132. package/dist/styles/accordion.css +228 -0
  133. package/dist/styles/alert-dialog.css +139 -0
  134. package/dist/styles/audio-player.css +22 -0
  135. package/dist/styles/base.css +638 -0
  136. package/dist/styles/breadcrumb.css +7 -0
  137. package/dist/styles/calendar.css +62 -0
  138. package/dist/styles/carousel.css +173 -0
  139. package/dist/styles/choice-grid.css +226 -0
  140. package/dist/styles/color-picker.css +281 -0
  141. package/dist/styles/color-swatch.css +47 -0
  142. package/dist/styles/command.css +52 -0
  143. package/dist/styles/content.css +79 -0
  144. package/dist/styles/context-menu.css +35 -0
  145. package/dist/styles/data-table.css +31 -0
  146. package/dist/styles/date-picker.css +107 -0
  147. package/dist/styles/day-agenda.css +30 -0
  148. package/dist/styles/display.css +304 -0
  149. package/dist/styles/dockable-panel.css +110 -0
  150. package/dist/styles/drawer.css +133 -0
  151. package/dist/styles/dropdown-menu.css +68 -0
  152. package/dist/styles/editable.css +145 -0
  153. package/dist/styles/empty.css +108 -0
  154. package/dist/styles/flip.css +101 -0
  155. package/dist/styles/form.css +634 -0
  156. package/dist/styles/heatmap.css +256 -0
  157. package/dist/styles/hotkey.css +189 -0
  158. package/dist/styles/hover-card.css +27 -0
  159. package/dist/styles/image-cropper.css +205 -0
  160. package/dist/styles/image-viewer.css +422 -0
  161. package/dist/styles/input-number.css +21 -0
  162. package/dist/styles/journey-map.css +19 -0
  163. package/dist/styles/keypad.css +92 -0
  164. package/dist/styles/layout.css +72 -0
  165. package/dist/styles/leaflet.css +661 -0
  166. package/dist/styles/log.css +203 -0
  167. package/dist/styles/markdown.css +379 -0
  168. package/dist/styles/node-graph.css +142 -0
  169. package/dist/styles/number-ticker.css +83 -0
  170. package/dist/styles/overflow-list.css +51 -0
  171. package/dist/styles/overlay.css +284 -0
  172. package/dist/styles/pagination.css +21 -0
  173. package/dist/styles/pan-zoom.css +136 -0
  174. package/dist/styles/radar.css +1 -0
  175. package/dist/styles/rating.css +121 -0
  176. package/dist/styles/resizable.css +108 -0
  177. package/dist/styles/scroll-area.css +122 -0
  178. package/dist/styles/select.css +557 -0
  179. package/dist/styles/selection.css +264 -0
  180. package/dist/styles/slider.css +145 -0
  181. package/dist/styles/slot.css +109 -0
  182. package/dist/styles/sortable.css +159 -0
  183. package/dist/styles/sparkline.css +24 -0
  184. package/dist/styles/steps.css +27 -0
  185. package/dist/styles/tabs.css +79 -0
  186. package/dist/styles/tag-input.css +213 -0
  187. package/dist/styles/time-picker.css +66 -0
  188. package/dist/styles/timeline.css +24 -0
  189. package/dist/styles/toast.css +160 -0
  190. package/dist/styles/toggle-group.css +107 -0
  191. package/dist/styles/tooltip.css +78 -0
  192. package/dist/styles/transfer.css +148 -0
  193. package/dist/styles/tree-select.css +131 -0
  194. package/dist/styles/tree.css +59 -0
  195. package/dist/styles/typewriter.css +62 -0
  196. package/dist/styles/upload.css +60 -0
  197. package/dist/styles/virtual-list.css +112 -0
  198. package/dist/styles.css +2094 -421
  199. package/dist/tabs/index.d.ts +9 -0
  200. package/dist/tabs/index.js +66 -16
  201. package/dist/tabs/useOverflow.d.ts +20 -0
  202. package/dist/tabs/useOverflow.js +145 -0
  203. package/dist/tag-input/index.d.ts +7 -2
  204. package/dist/tag-input/index.js +70 -26
  205. package/dist/time-picker/index.js +38 -20
  206. package/dist/toast/index.d.ts +19 -11
  207. package/dist/toast/index.js +48 -8
  208. package/dist/toast/manager.d.ts +42 -0
  209. package/dist/toast/manager.js +85 -0
  210. package/dist/toast/useToastNavigation.d.ts +3 -0
  211. package/dist/toast/useToastNavigation.js +75 -0
  212. package/dist/tokens.css +4 -0
  213. package/dist/tooltip/Tooltip.js +3 -3
  214. package/dist/transfer/index.d.ts +5 -0
  215. package/dist/transfer/index.js +93 -29
  216. package/dist/transfer/model.d.ts +4 -0
  217. package/dist/transfer/model.js +37 -11
  218. package/dist/tree/index.d.ts +5 -1
  219. package/dist/tree/index.js +5 -5
  220. package/dist/tree-select/index.js +67 -30
  221. package/dist/tree-select/model.d.ts +11 -5
  222. package/dist/tree-select/model.js +111 -83
  223. package/dist/typewriter/index.js +10 -10
  224. package/dist/upload/index.js +30 -16
  225. package/dist/virtual-list/index.js +7 -5
  226. package/package.json +413 -3
@@ -4,7 +4,7 @@ import { Button } from '../Button.js';
4
4
  import { PencilIcon } from '../PencilIcon.js';
5
5
  import { PencilSurface } from '../PencilSurface.js';
6
6
  import { PencilText } from '../PencilText.js';
7
- import { Select } from '../select/index.js';
7
+ import { SelectRoot, SelectTrigger, SelectValue, SelectPortal, SelectContent, SelectViewport, SelectItem, SelectItemText, SelectItemIndicator } from '../select/Select.js';
8
8
  import { Slider } from '../slider/index.js';
9
9
  const pausePaths = [
10
10
  { d: 'M 15 3 H 18 A 1 1 0 0 1 19 4 V 20 A 1 1 0 0 1 18 21 H 15 A 1 1 0 0 1 14 20 V 4 A 1 1 0 0 1 15 3 Z' },
@@ -22,7 +22,7 @@ const mutePaths = [
22
22
  { d: 'm16.5 9.5 5 5' },
23
23
  ];
24
24
  const DEFAULT_LABELS = {
25
- play: 'Play', pause: 'Pause', mute: 'Mute', unmute: 'Unmute', seek: 'Seek', volume: 'Volume', rate: 'Playback speed', loading: 'Loading audio', blocked: 'Playback was blocked. Press play to try again.', error: 'Audio failed to load.',
25
+ controls: 'Audio controls', audio: 'Audio', play: 'Play', pause: 'Pause', mute: 'Mute', unmute: 'Unmute', seek: 'Seek', volume: 'Volume', rate: 'Playback speed', loading: 'Loading audio', blocked: 'Playback was blocked. Press play to try again.', error: 'Audio failed to load.',
26
26
  };
27
27
  const rates = [0.75, 1, 1.25, 1.5, 2];
28
28
  function clamp(value, min, max) { return Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : min; }
@@ -32,16 +32,28 @@ export const AudioPlayer = forwardRef(function AudioPlayer({ src, preload = 'met
32
32
  const id = useId();
33
33
  const audioRef = useRef(null);
34
34
  const rootRef = useRef(null);
35
+ const sourceVersionRef = useRef(0);
36
+ const requestedSourceRef = useRef(src);
37
+ requestedSourceRef.current = src;
35
38
  const [state, setState] = useState({ status: src ? 'loading' : 'idle', currentTime: 0, duration: 0, buffered: 0, volume: clamp(volume ?? defaultVolume, 0, 1), muted: muted ?? defaultMuted, playbackRate: clamp(playbackRate ?? defaultPlaybackRate, 0.25, 4), canPlay: false, error: null });
36
39
  const stateRef = useRef(state);
37
40
  stateRef.current = state;
41
+ const controlledRef = useRef({ volume, muted, playbackRate });
42
+ controlledRef.current = { volume, muted, playbackRate };
43
+ const callbacksRef = useRef({ onVolumeChange, onMutedChange, onPlaybackRateChange, onTimeChange, onStateChange, onError });
44
+ callbacksRef.current = { onVolumeChange, onMutedChange, onPlaybackRateChange, onTimeChange, onStateChange, onError };
38
45
  const mergedLabels = { ...DEFAULT_LABELS, ...labels };
39
46
  const update = useCallback((patch) => {
40
- const next = { ...stateRef.current, ...patch };
41
- stateRef.current = next;
42
- setState(next);
43
- onStateChange?.(next);
44
- }, [onStateChange]);
47
+ const current = stateRef.current;
48
+ const changed = Object.keys(patch).some(key => !Object.is(current[key], patch[key]));
49
+ const next = changed ? { ...current, ...patch } : current;
50
+ if (changed) {
51
+ stateRef.current = next;
52
+ setState(next);
53
+ }
54
+ // Native events remain observable without rendering identical media snapshots.
55
+ callbacksRef.current.onStateChange?.(next);
56
+ }, []);
45
57
  const sync = useCallback(() => {
46
58
  const audio = audioRef.current;
47
59
  if (!audio)
@@ -49,14 +61,42 @@ export const AudioPlayer = forwardRef(function AudioPlayer({ src, preload = 'met
49
61
  const buffered = audio.duration > 0 && audio.buffered.length ? audio.buffered.end(audio.buffered.length - 1) / audio.duration : 0;
50
62
  update({ currentTime: Number.isFinite(audio.currentTime) ? audio.currentTime : 0, duration: Number.isFinite(audio.duration) ? audio.duration : 0, buffered: clamp(buffered, 0, 1), volume: audio.volume, muted: audio.muted, playbackRate: audio.playbackRate, canPlay: audio.readyState >= 3 });
51
63
  }, [update]);
52
- const play = useCallback(async () => { const audio = audioRef.current; if (!audio)
53
- return; try {
54
- await audio.play();
55
- }
56
- catch (error) {
57
- update({ status: 'blocked' });
58
- onError?.(error instanceof DOMException ? error : null);
59
- } }, [onError, update]);
64
+ const applyControlledSettings = useCallback(() => {
65
+ const audio = audioRef.current;
66
+ if (!audio)
67
+ return;
68
+ const controlled = controlledRef.current;
69
+ if (controlled.volume !== undefined) {
70
+ const value = clamp(controlled.volume, 0, 1);
71
+ if (audio.volume !== value)
72
+ audio.volume = value;
73
+ }
74
+ if (controlled.muted !== undefined && audio.muted !== controlled.muted)
75
+ audio.muted = controlled.muted;
76
+ if (controlled.playbackRate !== undefined) {
77
+ const value = clamp(controlled.playbackRate, 0.25, 4);
78
+ if (audio.defaultPlaybackRate !== value)
79
+ audio.defaultPlaybackRate = value;
80
+ if (audio.playbackRate !== value)
81
+ audio.playbackRate = value;
82
+ }
83
+ }, []);
84
+ const play = useCallback(async () => {
85
+ const audio = audioRef.current;
86
+ if (!audio)
87
+ return;
88
+ const sourceVersion = sourceVersionRef.current;
89
+ try {
90
+ await audio.play();
91
+ }
92
+ catch (error) {
93
+ // Loading another source or unmounting can reject an earlier play request.
94
+ if (audioRef.current !== audio || sourceVersionRef.current !== sourceVersion)
95
+ return;
96
+ update({ status: 'blocked' });
97
+ callbacksRef.current.onError?.(error instanceof DOMException ? error : null);
98
+ }
99
+ }, [update]);
60
100
  const pause = useCallback(() => { audioRef.current?.pause(); }, []);
61
101
  const togglePlay = useCallback(async () => { const audio = audioRef.current; if (!audio)
62
102
  return; if (audio.paused)
@@ -64,38 +104,93 @@ export const AudioPlayer = forwardRef(function AudioPlayer({ src, preload = 'met
64
104
  else
65
105
  pause(); }, [pause, play]);
66
106
  const seek = useCallback((time) => { const audio = audioRef.current; if (!audio || !Number.isFinite(time))
67
- return; audio.currentTime = clamp(time, 0, Number.isFinite(audio.duration) ? audio.duration : Math.max(0, time)); sync(); onTimeChange?.(audio.currentTime); }, [onTimeChange, sync]);
68
- const setVolume = useCallback((next) => { const audio = audioRef.current; if (!audio)
69
- return; audio.volume = clamp(next, 0, 1); onVolumeChange?.(audio.volume); sync(); }, [onVolumeChange, sync]);
70
- const setMuted = useCallback((next) => { const audio = audioRef.current; if (!audio)
71
- return; audio.muted = next; onMutedChange?.(next); sync(); }, [onMutedChange, sync]);
72
- const setPlaybackRate = useCallback((next) => { const audio = audioRef.current; if (!audio)
73
- return; audio.playbackRate = clamp(next, 0.25, 4); onPlaybackRateChange?.(audio.playbackRate); sync(); }, [onPlaybackRateChange, sync]);
107
+ return; audio.currentTime = clamp(time, 0, Number.isFinite(audio.duration) ? audio.duration : Math.max(0, time)); sync(); callbacksRef.current.onTimeChange?.(audio.currentTime); }, [sync]);
108
+ const setVolume = useCallback((next) => {
109
+ const audio = audioRef.current;
110
+ if (!audio)
111
+ return;
112
+ const value = clamp(next, 0, 1);
113
+ const current = controlledRef.current.volume ?? stateRef.current.volume;
114
+ if (value === current)
115
+ return;
116
+ if (controlledRef.current.volume === undefined) {
117
+ audio.volume = value;
118
+ sync();
119
+ }
120
+ callbacksRef.current.onVolumeChange?.(value);
121
+ }, [sync]);
122
+ const setMuted = useCallback((next) => {
123
+ const audio = audioRef.current;
124
+ if (!audio)
125
+ return;
126
+ const current = controlledRef.current.muted ?? stateRef.current.muted;
127
+ if (next === current)
128
+ return;
129
+ if (controlledRef.current.muted === undefined) {
130
+ audio.muted = next;
131
+ sync();
132
+ }
133
+ callbacksRef.current.onMutedChange?.(next);
134
+ }, [sync]);
135
+ const setPlaybackRate = useCallback((next) => {
136
+ const audio = audioRef.current;
137
+ if (!audio)
138
+ return;
139
+ const value = clamp(next, 0.25, 4);
140
+ const current = controlledRef.current.playbackRate ?? stateRef.current.playbackRate;
141
+ if (value === current)
142
+ return;
143
+ if (controlledRef.current.playbackRate === undefined) {
144
+ audio.defaultPlaybackRate = value;
145
+ audio.playbackRate = value;
146
+ sync();
147
+ }
148
+ callbacksRef.current.onPlaybackRateChange?.(value);
149
+ }, [sync]);
74
150
  const focus = useCallback(() => { rootRef.current?.querySelector('[data-audio-player-control]')?.focus(); }, []);
75
151
  const controls = { audio: audioRef.current, state, play, pause, togglePlay, seek, setVolume, setMuted, setPlaybackRate, focus };
76
152
  useImperativeHandle(forwardedRef, () => controls, [state, play, pause, togglePlay, seek, setVolume, setMuted, setPlaybackRate, focus]);
77
- useEffect(() => { const audio = audioRef.current; if (!audio)
78
- return; audio.volume = clamp(volume ?? defaultVolume, 0, 1); audio.muted = muted ?? defaultMuted; audio.playbackRate = clamp(playbackRate ?? defaultPlaybackRate, 0.25, 4); sync(); }, [defaultMuted, defaultPlaybackRate, defaultVolume, muted, playbackRate, src, volume, sync]);
79
153
  useEffect(() => {
154
+ sourceVersionRef.current += 1;
80
155
  const audio = audioRef.current;
81
- if (!audio || !src)
156
+ if (!audio)
82
157
  return;
83
158
  audio.pause();
84
- audio.currentTime = 0;
85
- update({ status: 'loading', currentTime: 0, duration: 0, buffered: 0, canPlay: false, error: null });
159
+ try {
160
+ audio.currentTime = 0;
161
+ }
162
+ catch { /* Empty media has no seekable timeline. */ }
163
+ if (!src)
164
+ audio.removeAttribute('src');
165
+ update({ status: src ? 'loading' : 'idle', currentTime: 0, duration: 0, buffered: 0, canPlay: false, error: null });
166
+ const current = stateRef.current;
167
+ audio.volume = clamp(controlledRef.current.volume ?? current.volume, 0, 1);
168
+ audio.muted = controlledRef.current.muted ?? current.muted;
169
+ const rate = clamp(controlledRef.current.playbackRate ?? current.playbackRate, 0.25, 4);
170
+ audio.defaultPlaybackRate = rate;
171
+ audio.playbackRate = rate;
86
172
  audio.load();
87
173
  }, [src, update]);
174
+ useEffect(() => { applyControlledSettings(); sync(); }, [applyControlledSettings, muted, playbackRate, sync, volume]);
88
175
  useEffect(() => {
89
176
  const audio = audioRef.current;
90
177
  if (!audio)
91
178
  return;
179
+ const hasSource = () => Boolean(requestedSourceRef.current || audio.getAttribute('src'));
92
180
  const handlers = {
93
- loadstart: () => update({ status: 'loading', error: null, canPlay: false }), loadedmetadata: () => { sync(); update({ status: 'ready' }); }, canplay: () => { sync(); update({ canPlay: true, status: audio.paused ? 'paused' : 'playing' }); }, play: () => update({ status: 'playing' }), pause: () => update({ status: audio.ended ? 'ended' : 'paused' }), playing: () => update({ status: 'playing', canPlay: true }), waiting: () => update({ status: 'buffering' }), progress: sync, timeupdate: () => { sync(); onTimeChange?.(audio.currentTime); }, ratechange: sync, volumechange: () => { sync(); onVolumeChange?.(audio.volume); onMutedChange?.(audio.muted); }, ended: () => update({ status: 'ended', currentTime: audio.duration || audio.currentTime }), error: () => { const error = audio.error; update({ status: 'error', error }); onError?.(error); }, abort: () => update({ status: 'idle' }),
181
+ loadstart: () => update({ status: hasSource() ? 'loading' : 'idle', error: null, canPlay: false }), loadedmetadata: () => { applyControlledSettings(); sync(); update({ status: 'ready' }); }, canplay: () => { sync(); update({ canPlay: true, status: audio.paused ? 'paused' : 'playing' }); }, play: () => update({ status: 'playing' }), pause: () => update({ status: hasSource() ? (audio.ended ? 'ended' : 'paused') : 'idle' }), playing: () => update({ status: 'playing', canPlay: true }), waiting: () => update({ status: 'buffering' }), progress: sync, timeupdate: () => { sync(); callbacksRef.current.onTimeChange?.(audio.currentTime); }, ratechange: () => { const previous = stateRef.current.playbackRate; applyControlledSettings(); sync(); if (controlledRef.current.playbackRate === undefined && previous !== audio.playbackRate)
182
+ callbacksRef.current.onPlaybackRateChange?.(audio.playbackRate); }, volumechange: () => { const previous = stateRef.current; applyControlledSettings(); sync(); if (controlledRef.current.volume === undefined && previous.volume !== audio.volume)
183
+ callbacksRef.current.onVolumeChange?.(audio.volume); if (controlledRef.current.muted === undefined && previous.muted !== audio.muted)
184
+ callbacksRef.current.onMutedChange?.(audio.muted); }, ended: () => update({ status: 'ended', currentTime: audio.duration || audio.currentTime }), error: () => { if (!hasSource()) {
185
+ update({ status: 'idle', error: null, canPlay: false });
186
+ return;
187
+ } const error = audio.error; update({ status: 'error', error }); callbacksRef.current.onError?.(error); }, abort: () => update({ status: 'idle' }), emptied: () => { if (!hasSource())
188
+ update({ status: 'idle', currentTime: 0, duration: 0, buffered: 0, canPlay: false, error: null }); },
94
189
  };
95
190
  Object.entries(handlers).forEach(([name, handler]) => audio.addEventListener(name, handler));
96
191
  return () => Object.entries(handlers).forEach(([name, handler]) => audio.removeEventListener(name, handler));
97
- }, [onError, onMutedChange, onTimeChange, onVolumeChange, sync, src, update]);
192
+ }, [applyControlledSettings, sync, update]);
98
193
  const playLabel = state.status === 'playing' ? mergedLabels.pause : mergedLabels.play;
99
- const defaultControls = _jsxs("div", { className: "inkra-audio-player__controls", role: "group", "aria-label": "Audio controls", children: [_jsx(Button, { "data-audio-player-control": true, "data-tooltip": playLabel, type: "button", size: "icon-sm", variant: "outline", "aria-label": playLabel, onClick: () => void togglePlay(), children: _jsx(PencilIcon, { paths: state.status === 'playing' ? pausePaths : playPaths, size: 20, sourceSize: 24 }) }), _jsx("div", { className: "inkra-audio-player__seek", children: _jsx(Slider, { "data-audio-player-control": true, min: 0, max: state.duration || 0, step: 0.01, value: [state.currentTime], onValueChange: values => seek(values[0] ?? 0), disabled: !state.duration, "aria-label": mergedLabels.seek, getValueLabel: value => labels?.time?.(value, state.duration) ?? `${formatTime(value)} / ${formatTime(state.duration)}`, size: "sm", variant: "inset" }) }), _jsx(PencilText, { as: "span", className: "inkra-audio-player__time", "aria-live": "off", children: labels?.time?.(state.currentTime, state.duration) ?? `${formatTime(state.currentTime)} / ${formatTime(state.duration)}` }), _jsxs("div", { className: "inkra-audio-player__settings", children: [_jsx(Button, { "data-audio-player-control": true, "data-tooltip": state.muted ? mergedLabels.unmute : mergedLabels.mute, type: "button", size: "icon-sm", variant: "ghost", "aria-label": state.muted ? mergedLabels.unmute : mergedLabels.mute, onClick: () => setMuted(!state.muted), children: _jsx(PencilIcon, { paths: state.muted ? mutePaths : volumePaths, size: 20, sourceSize: 24 }) }), _jsx("div", { className: "inkra-audio-player__volume", children: _jsx(Slider, { "data-audio-player-control": true, min: 0, max: 1, step: 0.01, value: [state.muted ? 0 : state.volume], onValueChange: values => { setMuted(false); setVolume(values[0] ?? 0); }, "aria-label": mergedLabels.volume, getValueLabel: value => `${Math.round(value * 100)}%`, size: "sm", variant: "inset" }) }), _jsxs(Select.Root, { value: String(state.playbackRate), onValueChange: value => setPlaybackRate(Number(value)), children: [_jsx(Select.Trigger, { "data-audio-player-control": true, "aria-label": mergedLabels.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)) }) }) })] })] })] });
100
- return _jsxs(PencilSurface, { ref: rootRef, ...props, className: ['inkra-audio-player', className].filter(Boolean).join(' '), variant: "paper", radius: 8, "data-status": state.status, "data-audio-player-id": id, children: [_jsx("audio", { ...audioProps, ref: audioRef, "aria-label": audioProps?.['aria-label'] ?? 'Audio', autoPlay: autoPlay, crossOrigin: crossOrigin, muted: state.muted, preload: preload, src: src }), state.status === 'blocked' && _jsx(PencilText, { as: "p", className: "inkra-audio-player__message", role: "status", children: mergedLabels.blocked }), state.status === 'error' && _jsx(PencilText, { as: "p", className: "inkra-audio-player__message", role: "alert", children: mergedLabels.error }), renderControls ? renderControls(controls) : defaultControls] });
194
+ const defaultControls = _jsxs("div", { className: "inkra-audio-player__controls", role: "group", "aria-label": mergedLabels.controls, children: [_jsx(Button, { "data-audio-player-control": true, "data-tooltip": playLabel, type: "button", size: "icon-sm", variant: "outline", "aria-label": playLabel, onClick: () => void togglePlay(), children: _jsx(PencilIcon, { paths: state.status === 'playing' ? pausePaths : playPaths, size: 20, sourceSize: 24 }) }), _jsx("div", { className: "inkra-audio-player__seek", children: _jsx(Slider, { "data-audio-player-control": true, min: 0, max: state.duration || 0, step: 0.01, value: [state.currentTime], onValueChange: values => seek(values[0] ?? 0), disabled: !state.duration, "aria-label": mergedLabels.seek, getValueLabel: value => labels?.time?.(value, state.duration) ?? `${formatTime(value)} / ${formatTime(state.duration)}`, size: "sm", variant: "inset" }) }), _jsx(PencilText, { as: "span", className: "inkra-audio-player__time", "aria-live": "off", children: labels?.time?.(state.currentTime, state.duration) ?? `${formatTime(state.currentTime)} / ${formatTime(state.duration)}` }), _jsxs("div", { className: "inkra-audio-player__settings", children: [_jsx(Button, { "data-audio-player-control": true, "data-tooltip": state.muted ? mergedLabels.unmute : mergedLabels.mute, type: "button", size: "icon-sm", variant: "ghost", "aria-label": state.muted ? mergedLabels.unmute : mergedLabels.mute, onClick: () => setMuted(!state.muted), children: _jsx(PencilIcon, { paths: state.muted ? mutePaths : volumePaths, size: 20, sourceSize: 24 }) }), _jsx("div", { className: "inkra-audio-player__volume", children: _jsx(Slider, { "data-audio-player-control": true, min: 0, max: 1, step: 0.01, value: [state.muted ? 0 : state.volume], onValueChange: values => { setMuted(false); setVolume(values[0] ?? 0); }, "aria-label": mergedLabels.volume, getValueLabel: value => `${Math.round(value * 100)}%`, size: "sm", variant: "inset" }) }), _jsxs(SelectRoot, { value: String(state.playbackRate), onValueChange: value => setPlaybackRate(Number(value)), children: [_jsx(SelectTrigger, { "data-audio-player-control": true, "aria-label": mergedLabels.rate, fullWidth: false, size: "sm", children: _jsx(SelectValue, {}) }), _jsx(SelectPortal, { children: _jsx(SelectContent, { position: "popper", sideOffset: 4, children: _jsx(SelectViewport, { children: rates.map(rate => _jsxs(SelectItem, { value: String(rate), children: [_jsx(SelectItemText, { children: `${rate}x` }), _jsx(SelectItemIndicator, {})] }, rate)) }) }) })] })] })] });
195
+ return _jsxs(PencilSurface, { ref: rootRef, ...props, className: ['inkra-audio-player', className].filter(Boolean).join(' '), variant: "paper", radius: 8, "data-status": state.status, "data-audio-player-id": id, children: [_jsx("audio", { ...audioProps, ref: audioRef, "aria-label": audioProps?.['aria-label'] ?? mergedLabels.audio, autoPlay: autoPlay, crossOrigin: crossOrigin, muted: state.muted, preload: preload, src: src }), state.status === 'blocked' && _jsx(PencilText, { as: "p", className: "inkra-audio-player__message", role: "status", children: mergedLabels.blocked }), state.status === 'error' && _jsx(PencilText, { as: "p", className: "inkra-audio-player__message", role: "alert", children: mergedLabels.error }), renderControls ? renderControls(controls) : defaultControls] });
101
196
  });
@@ -17,5 +17,8 @@ export type CalendarProps = DayPickerProps & {
17
17
  from: Date;
18
18
  to: Date;
19
19
  };
20
+ /** Opt-in itinerary summary; every event can be revealed for its day. */
21
+ maxEventsPerDay?: number;
22
+ showDayEventsLabel?: (date: Date, total: number, expanded: boolean) => string;
20
23
  };
21
- export declare function Calendar({ events, eventIndicator, appearance, onEventSelect, renderDayAccessory, displayRange, className, components, showOutsideDays, ...props }: CalendarProps): import("react").JSX.Element;
24
+ export declare function Calendar({ events, eventIndicator, appearance, onEventSelect, renderDayAccessory, displayRange, maxEventsPerDay, showDayEventsLabel, className, components, showOutsideDays, ...props }: CalendarProps): import("react").JSX.Element;
@@ -1,13 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createContext, useContext } from 'react';
2
+ import { createContext, useContext, useMemo, useState } from 'react';
3
3
  import { CaptionLabel, Day, DayButton, DayPicker, Weekday } from 'react-day-picker';
4
4
  import { Button } from '../Button.js';
5
5
  import { PencilIcon } from '../PencilIcon.js';
6
6
  import { PencilSurface } from '../PencilSurface.js';
7
7
  import { PencilText } from '../PencilText.js';
8
- const EventContext = createContext({ events: [], indicator: 'count', appearance: 'itinerary' });
9
- const matches = (event, date) => event.date.getFullYear() === date.getFullYear()
10
- && event.date.getMonth() === date.getMonth() && event.date.getDate() === date.getDate();
8
+ const emptyEvents = [];
9
+ const EventContext = createContext({ eventsByDay: new Map(), indicator: 'count', appearance: 'itinerary' });
10
+ // Match local calendar dates, not UTC dates, and retain input order within each day.
11
+ const dayKey = (date) => `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
11
12
  function CalendarCaption({ children, ...props }) {
12
13
  return _jsx(CaptionLabel, { ...props, children: typeof children === 'string' ? _jsx(PencilText, { children: children }) : children });
13
14
  }
@@ -24,18 +25,36 @@ function CalendarChevron({ className, orientation = 'left', size = 28 }) {
24
25
  }
25
26
  // The trip calendar uses a full-cell date target with sibling event buttons.
26
27
  function CalendarCell({ children, day, modifiers, ...props }) {
27
- const { events, appearance, onEventSelect, renderDayAccessory } = useContext(EventContext);
28
+ const { eventsByDay, appearance, onEventSelect, renderDayAccessory, maxEventsPerDay, showDayEventsLabel } = useContext(EventContext);
29
+ const [expandedDay, setExpandedDay] = useState();
28
30
  if (appearance === 'picker')
29
31
  return _jsx(Day, { ...props, day: day, modifiers: modifiers, children: children });
30
- const entries = events.filter(event => matches(event, day.date));
31
- return _jsx(Day, { ...props, day: day, modifiers: modifiers, children: _jsxs("div", { className: "inkra-calendar__day-card", children: [_jsx(PencilSurface, { variant: modifiers.selected ? 'action' : 'quiet', radius: 8, className: "inkra-calendar__day-paper", "aria-hidden": "true", seed: `day-${day.date.toISOString()}` }), children, renderDayAccessory && _jsx("div", { className: "inkra-calendar__accessory", children: renderDayAccessory(day.date) }), _jsx("div", { className: "inkra-calendar__events", children: entries.map((event, index) => _jsx(PencilSurface, { as: "button", variant: "quiet", radius: 3, className: "inkra-calendar__event-card", disabled: modifiers.disabled || !onEventSelect, style: event.color ? { '--inkra-accent-soft': event.color } : undefined, "aria-label": `${event.label}${event.description ? `, ${event.description}` : ''}`, title: event.description ?? event.label, onClick: click => { click.stopPropagation(); onEventSelect?.(event); }, children: _jsx(PencilText, { ellipsis: true, children: event.label }) }, event.id ?? index)) })] }) });
32
+ const entries = eventsByDay.get(dayKey(day.date)) ?? emptyEvents;
33
+ const limit = maxEventsPerDay !== undefined && Number.isFinite(maxEventsPerDay) ? Math.max(0, Math.floor(maxEventsPerDay)) : entries.length;
34
+ const expanded = expandedDay === dayKey(day.date);
35
+ const shown = expanded || limit >= entries.length ? entries : entries.slice(0, limit);
36
+ return _jsx(Day, { ...props, day: day, modifiers: modifiers, children: _jsxs("div", { className: "inkra-calendar__day-card", children: [_jsx(PencilSurface, { variant: modifiers.selected ? 'action' : 'quiet', radius: 8, className: "inkra-calendar__day-paper", "aria-hidden": "true", seed: `day-${day.date.toISOString()}` }), children, renderDayAccessory && _jsx("div", { className: "inkra-calendar__accessory", children: renderDayAccessory(day.date) }), _jsxs("div", { className: "inkra-calendar__events", children: [shown.map((event, index) => _jsx(PencilSurface, { as: "button", variant: "quiet", radius: 3, className: "inkra-calendar__event-card", disabled: modifiers.disabled || !onEventSelect, style: event.color ? { '--inkra-accent-soft': event.color } : undefined, "aria-label": `${event.label}${event.description ? `, ${event.description}` : ''}`, title: event.description ?? event.label, onClick: click => { click.stopPropagation(); onEventSelect?.(event); }, children: _jsx(PencilText, { ellipsis: true, children: event.label }) }, event.id ?? index)), entries.length > limit && _jsx(Button, { className: "inkra-calendar__events-toggle", size: "sm", variant: "ghost", "aria-expanded": expanded, onClick: click => { click.stopPropagation(); setExpandedDay(expanded ? undefined : dayKey(day.date)); }, children: showDayEventsLabel?.(day.date, entries.length, expanded) ?? (expanded ? 'Show fewer events' : `Show all ${entries.length} events`) })] })] }) });
32
37
  }
33
38
  function CalendarDay({ children, modifiers, day, ...props }) {
34
- const { events: allEvents, indicator, appearance } = useContext(EventContext);
35
- const events = allEvents.filter(event => matches(event, day.date));
36
- return _jsxs(DayButton, { ...props, day: day, modifiers: modifiers, "aria-label": [props['aria-label'], ...events.map(event => event.label)].filter(Boolean).join(', '), children: [modifiers.selected && appearance === 'picker' && _jsx(PencilSurface, { variant: "selection", className: "inkra-calendar__selection", "aria-hidden": "true" }), _jsx(PencilText, { children: String(children) }), appearance === 'picker' && events.length > 0 && indicator !== 'none' && _jsx("span", { className: "inkra-calendar__event", "data-indicator": indicator, "aria-hidden": "true", children: indicator === 'count' ? events.length : '\u2022' })] });
39
+ const { eventsByDay, indicator, appearance, maxEventsPerDay, showDayEventsLabel } = useContext(EventContext);
40
+ const events = eventsByDay.get(dayKey(day.date)) ?? emptyEvents;
41
+ return _jsxs(DayButton, { ...props, day: day, modifiers: modifiers, "aria-label": [props['aria-label'], ...(maxEventsPerDay !== undefined && Number.isFinite(maxEventsPerDay) && events.length > Math.max(0, Math.floor(maxEventsPerDay))
42
+ ? [showDayEventsLabel?.(day.date, events.length, false) ?? `${events.length} events`] : events.map(event => event.label))].filter(Boolean).join(', '), children: [modifiers.selected && appearance === 'picker' && _jsx(PencilSurface, { variant: "selection", className: "inkra-calendar__selection", "aria-hidden": "true" }), _jsx(PencilText, { children: String(children) }), appearance === 'picker' && events.length > 0 && indicator !== 'none' && _jsx("span", { className: "inkra-calendar__event", "data-indicator": indicator, "aria-hidden": "true", children: indicator === 'count' ? events.length : '\u2022' })] });
37
43
  }
38
- export function Calendar({ events = [], eventIndicator = 'count', appearance = 'itinerary', onEventSelect, renderDayAccessory, displayRange, className, components, showOutsideDays = true, ...props }) {
44
+ export function Calendar({ events = emptyEvents, eventIndicator = 'count', appearance = 'itinerary', onEventSelect, renderDayAccessory, displayRange, maxEventsPerDay, showDayEventsLabel, className, components, showOutsideDays = true, ...props }) {
45
+ const eventsByDay = useMemo(() => {
46
+ const index = new Map();
47
+ for (const event of events) {
48
+ const key = dayKey(event.date);
49
+ const entries = index.get(key);
50
+ if (entries)
51
+ entries.push(event);
52
+ else
53
+ index.set(key, [event]);
54
+ }
55
+ return index;
56
+ }, [events]);
57
+ const context = useMemo(() => ({ eventsByDay, indicator: eventIndicator, appearance, onEventSelect, renderDayAccessory, maxEventsPerDay, showDayEventsLabel }), [eventsByDay, eventIndicator, appearance, onEventSelect, renderDayAccessory, maxEventsPerDay, showDayEventsLabel]);
39
58
  const hidden = displayRange ? [...(Array.isArray(props.hidden) ? props.hidden : props.hidden ? [props.hidden] : []), { before: displayRange.from }, { after: displayRange.to }] : props.hidden;
40
- return _jsx(EventContext.Provider, { value: { events, indicator: eventIndicator, appearance, onEventSelect, renderDayAccessory }, children: _jsx("div", { className: "inkra-calendar-scroll", "data-appearance": appearance, children: _jsx(DayPicker, { ...props, hidden: hidden, "data-appearance": appearance, "data-week-numbers": props.showWeekNumber || undefined, showOutsideDays: showOutsideDays, className: ['inkra-calendar', className].filter(Boolean).join(' '), components: { CaptionLabel: CalendarCaption, Weekday: CalendarWeekday, Chevron: CalendarChevron, PreviousMonthButton: CalendarMonthButton, NextMonthButton: CalendarMonthButton, Day: CalendarCell, DayButton: CalendarDay, ...components } }) }) });
59
+ return _jsx(EventContext.Provider, { value: context, children: _jsx("div", { className: "inkra-calendar-scroll", "data-appearance": appearance, "data-event-summary": maxEventsPerDay !== undefined && Number.isFinite(maxEventsPerDay) || undefined, children: _jsx(DayPicker, { ...props, hidden: hidden, "data-appearance": appearance, "data-week-numbers": props.showWeekNumber || undefined, showOutsideDays: showOutsideDays, className: ['inkra-calendar', className].filter(Boolean).join(' '), components: { CaptionLabel: CalendarCaption, Weekday: CalendarWeekday, Chevron: CalendarChevron, PreviousMonthButton: CalendarMonthButton, NextMonthButton: CalendarMonthButton, Day: CalendarCell, DayButton: CalendarDay, ...components } }) }) });
41
60
  }
@@ -84,18 +84,25 @@ export const Carousel = forwardRef(function Carousel({ 'aria-label': ariaLabel =
84
84
  stopOnMouseEnter: false,
85
85
  }), [autoplayActive, autoplayOptions.delay, stopOnInteraction]);
86
86
  const plugins = useMemo(() => autoplayActive ? [autoplayPlugin] : [], [autoplayActive, autoplayPlugin]);
87
- const [viewportRef, emblaApi] = useEmblaCarousel({
88
- active: !disabled && count > 0,
87
+ // Ordinary selection changes use scrollTo, not an engine rebuild. When real
88
+ // options/plugins change, seed their new engine from the latest selection.
89
+ const activeIndexRef = useRef(activeIndex);
90
+ activeIndexRef.current = activeIndex;
91
+ const engineActive = !disabled && count > 0;
92
+ const engineLoop = loop && count > 1;
93
+ const engineOptions = useMemo(() => ({
94
+ active: engineActive,
89
95
  align,
90
96
  containScroll: 'trimSnaps',
91
97
  direction: resolvedDir,
92
98
  dragFree,
93
- loop: loop && count > 1,
99
+ loop: engineLoop,
94
100
  slidesToScroll: 1,
95
- startIndex: initialIndex,
101
+ startIndex: activeIndexRef.current,
96
102
  watchResize: true,
97
103
  watchSlides: true,
98
- }, plugins);
104
+ }), [align, dragFree, engineActive, engineLoop, plugins, resolvedDir]);
105
+ const [viewportRef, emblaApi] = useEmblaCarousel(engineOptions, plugins);
99
106
  const refresh = useCallback(() => {
100
107
  if (!emblaApi)
101
108
  return;
@@ -286,5 +293,5 @@ export const Carousel = forwardRef(function Carousel({ 'aria-label': ariaLabel =
286
293
  const statusLabel = labels?.status ?? ((index, total) => `Slide ${index} of ${total}`);
287
294
  const showNavigation = controls && count > 1;
288
295
  const showPagination = pagination && scrollSnaps.length > 1;
289
- return _jsxs("div", { ...props, "aria-label": ariaLabel, "aria-roledescription": "carousel", className: ['inkra-carousel', className].filter(Boolean).join(' '), "data-autoplay": autoplayEnabled || undefined, "data-disabled": disabled || undefined, "data-playing": playing || undefined, dir: resolvedDir, onBlurCapture: handleBlur, onFocusCapture: handleFocus, onKeyDown: handleKeyDown, onPointerEnter: handlePointerEnter, onPointerLeave: handlePointerLeave, ref: rootRef, role: role, style: rootStyle, tabIndex: !disabled && count > 1 ? (tabIndex ?? 0) : tabIndex, children: [count === 0 ? _jsx("div", { className: "inkra-carousel__empty", children: emptyState ?? labels?.empty ?? 'No items' }) : _jsxs(_Fragment, { children: [_jsx("div", { className: "inkra-carousel__viewport", ref: viewportRef, children: _jsx("div", { className: "inkra-carousel__container", children: slideItems.map((item, index) => _jsx("div", { "aria-label": slideLabel(index + 1, count), "aria-roledescription": "slide", className: "inkra-carousel__slide", "data-current": index === activeIndex || undefined, role: "group", children: item }, item.key ?? index)) }) }), (showNavigation || showPagination || autoplayEnabled) && _jsxs("div", { className: "inkra-carousel__toolbar", children: [showNavigation && _jsxs("div", { className: "inkra-carousel__navigation", children: [_jsx(Button, { "aria-label": labels?.previous ?? 'Previous slide', disabled: disabled || !canScrollPrevious, onClick: previous, size: "icon", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: resolvedDir === 'rtl' ? nextPaths : previousPaths, seed: `${generatedId}-previous`, size: 20, sourceSize: 24 }) }), _jsx(Button, { "aria-label": labels?.next ?? 'Next slide', disabled: disabled || !canScrollNext, onClick: next, size: "icon", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: resolvedDir === 'rtl' ? previousPaths : nextPaths, seed: `${generatedId}-next`, size: 20, sourceSize: 24 }) })] }), showPagination && _jsx("div", { "aria-label": labels?.pages ?? 'Carousel pages', className: "inkra-carousel__pagination", role: "group", children: scrollSnaps.map((_, index) => _jsx("button", { "aria-current": index === selectedIndex ? 'true' : undefined, "aria-label": pageLabel(index + 1, scrollSnaps.length), className: "inkra-carousel__page", disabled: disabled, onClick: () => scrollTo(index), type: "button", children: _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-carousel__page-mark", radius: 6, seed: `${generatedId}-page-${index}`, variant: index === selectedIndex ? 'selection' : 'outline' }) }, index)) }), autoplayEnabled && _jsx(Button, { "aria-label": playing ? (labels?.pause ?? 'Pause autoplay') : (labels?.play ?? 'Play autoplay'), disabled: !autoplayAllowed, onClick: toggleAutoplay, onPointerDown: () => { autoplayControlPointerRef.current = true; }, size: "icon", variant: "ghost", children: _jsx(PencilIcon, { decorative: true, paths: playing ? pausePaths : playPaths, seed: `${generatedId}-autoplay`, size: 20, sourceSize: 24 }) })] })] }), count > 0 && _jsx("span", { "aria-atomic": "true", "aria-live": playing ? 'off' : 'polite', className: "inkra-carousel__status", children: statusLabel(activeIndex + 1, count) })] });
296
+ return _jsxs("div", { ...props, "aria-label": ariaLabel, "aria-roledescription": "carousel", className: ['inkra-carousel', className].filter(Boolean).join(' '), "data-autoplay": autoplayEnabled || undefined, "data-disabled": disabled || undefined, "data-playing": playing || undefined, dir: resolvedDir, onBlurCapture: handleBlur, onFocusCapture: handleFocus, onKeyDown: handleKeyDown, onPointerEnter: handlePointerEnter, onPointerLeave: handlePointerLeave, ref: rootRef, role: role, style: rootStyle, tabIndex: !disabled && count > 1 ? (tabIndex ?? 0) : tabIndex, children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-carousel__focus-ring", radius: 8, seed: `${generatedId}-focus`, variant: "outline" }), count === 0 ? _jsx("div", { className: "inkra-carousel__empty", children: emptyState ?? labels?.empty ?? 'No items' }) : _jsxs(_Fragment, { children: [_jsx("div", { className: "inkra-carousel__viewport", ref: viewportRef, children: _jsx("div", { className: "inkra-carousel__container", children: slideItems.map((item, index) => _jsx("div", { "aria-label": slideLabel(index + 1, count), "aria-roledescription": "slide", className: "inkra-carousel__slide", "data-current": index === activeIndex || undefined, role: "group", children: item }, item.key ?? index)) }) }), (showNavigation || showPagination || autoplayEnabled) && _jsxs("div", { className: "inkra-carousel__toolbar", children: [showNavigation && _jsxs("div", { className: "inkra-carousel__navigation", children: [_jsx(Button, { "aria-label": labels?.previous ?? 'Previous slide', disabled: disabled || !canScrollPrevious, onClick: previous, size: "icon", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: resolvedDir === 'rtl' ? nextPaths : previousPaths, seed: `${generatedId}-previous`, size: 20, sourceSize: 24 }) }), _jsx(Button, { "aria-label": labels?.next ?? 'Next slide', disabled: disabled || !canScrollNext, onClick: next, size: "icon", variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: resolvedDir === 'rtl' ? previousPaths : nextPaths, seed: `${generatedId}-next`, size: 20, sourceSize: 24 }) })] }), showPagination && _jsx("div", { "aria-label": labels?.pages ?? 'Carousel pages', className: "inkra-carousel__pagination", role: "group", children: scrollSnaps.map((_, index) => _jsxs("button", { "aria-current": index === selectedIndex ? 'true' : undefined, "aria-label": pageLabel(index + 1, scrollSnaps.length), className: "inkra-carousel__page", disabled: disabled, onClick: () => scrollTo(index), type: "button", children: [_jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-carousel__page-mark", radius: 6, seed: `${generatedId}-page-${index}`, variant: index === selectedIndex ? 'selection' : 'outline' }), _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-carousel__page-focus", radius: 6, seed: `${generatedId}-page-${index}-focus`, variant: "outline" })] }, index)) }), autoplayEnabled && _jsx(Button, { "aria-label": playing ? (labels?.pause ?? 'Pause autoplay') : (labels?.play ?? 'Play autoplay'), disabled: !autoplayAllowed, onClick: toggleAutoplay, onPointerDown: () => { autoplayControlPointerRef.current = true; }, size: "icon", variant: "ghost", children: _jsx(PencilIcon, { decorative: true, paths: playing ? pausePaths : playPaths, seed: `${generatedId}-autoplay`, size: 20, sourceSize: 24 }) })] })] }), count > 0 && _jsx("span", { "aria-atomic": "true", "aria-live": playing ? 'off' : 'polite', className: "inkra-carousel__status", children: statusLabel(activeIndex + 1, count) })] });
290
297
  });
@@ -11,6 +11,11 @@ interface ChoiceGridCommonProps extends Omit<FieldsetHTMLAttributes<HTMLFieldSet
11
11
  error?: ReactNode;
12
12
  columns?: ChoiceGridColumns;
13
13
  minItemWidth?: string;
14
+ /** Opt-in paging for controlled, direct non-required ChoiceGridOption children. */
15
+ pageSize?: number;
16
+ previousPageLabel?: string;
17
+ nextPageLabel?: string;
18
+ pageLabel?: (page: number, pages: number) => ReactNode;
14
19
  }
15
20
  export interface SingleChoiceGridProps extends ChoiceGridCommonProps {
16
21
  type?: 'single';
@@ -33,6 +38,6 @@ export interface ChoiceGridOptionProps extends Omit<InputHTMLAttributes<HTMLInpu
33
38
  image?: ChoiceGridImage;
34
39
  seed?: string | number;
35
40
  }
36
- export declare const ChoiceGrid: import("react").ForwardRefExoticComponent<ChoiceGridProps & import("react").RefAttributes<HTMLFieldSetElement>>;
41
+ export declare const ChoiceGrid: import("react").NamedExoticComponent<ChoiceGridProps & import("react").RefAttributes<HTMLFieldSetElement>>;
37
42
  export declare const ChoiceGridOption: import("react").ForwardRefExoticComponent<ChoiceGridOptionProps & import("react").RefAttributes<HTMLInputElement>>;
38
43
  export {};
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { createContext, forwardRef, useContext, useId, useImperativeHandle, useRef, } from 'react';
2
+ import { Children, createContext, forwardRef, memo, isValidElement, useContext, useCallback, useMemo, useId, useImperativeHandle, useRef, useState, } from 'react';
3
+ import { Button } from '../Button.js';
3
4
  import { PencilSurface } from '../PencilSurface.js';
4
5
  import { PencilText } from '../PencilText.js';
5
6
  import { Checkbox } from '../selection/Checkbox.js';
6
7
  import { RadioGroup, RadioGroupItem } from '../selection/RadioGroup.js';
7
8
  const classes = (...values) => values.filter(Boolean).join(' ');
9
+ const noPagedOptions = [];
8
10
  function mergeIds(...values) {
9
11
  const merged = Array.from(new Set(values.flatMap(value => value?.split(/\s+/).filter(Boolean) ?? [])));
10
12
  return merged.length ? merged.join(' ') : undefined;
@@ -24,8 +26,8 @@ function gridStyle(style, columns, minItemWidth) {
24
26
  '--inkra-choice-grid-min-item': minItemWidth,
25
27
  };
26
28
  }
27
- export const ChoiceGrid = forwardRef(function ChoiceGrid(props, forwardedRef) {
28
- const { 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, children, className, columns, description, disabled = false, error, form, label, minItemWidth = '13rem', name, style, type = 'single', ...rest } = props;
29
+ export const ChoiceGrid = memo(forwardRef(function ChoiceGrid(props, forwardedRef) {
30
+ const { 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, children, className, columns, description, disabled = false, error, form, label, minItemWidth = '13rem', name, pageSize, previousPageLabel = 'Previous page', nextPageLabel = 'Next page', pageLabel = (page, pages) => `${page} / ${pages}`, style, type = 'single', ...rest } = props;
29
31
  const generatedId = useId();
30
32
  const descriptionId = description == null ? undefined : `inkra-choice-grid-${generatedId}-description`;
31
33
  const errorId = error == null ? undefined : `inkra-choice-grid-${generatedId}-error`;
@@ -34,31 +36,50 @@ export const ChoiceGrid = forwardRef(function ChoiceGrid(props, forwardedRef) {
34
36
  const invalid = Boolean(error) || rest['aria-invalid'] === true || rest['aria-invalid'] === 'true';
35
37
  const rootRef = useRef(null);
36
38
  useImperativeHandle(forwardedRef, () => rootRef.current);
39
+ const multipleValues = props.type === 'multiple' ? props.values : undefined;
40
+ const multipleDefaults = props.type === 'multiple' ? props.defaultValues : undefined;
41
+ const onValuesChange = props.type === 'multiple' ? props.onValuesChange : undefined;
42
+ const values = useMemo(() => multipleValues === undefined ? undefined : new Set(multipleValues), [multipleValues]);
43
+ const defaultValues = useMemo(() => multipleDefaults === undefined ? undefined : new Set(multipleDefaults), [multipleDefaults]);
44
+ const [page, setPage] = useState(0);
45
+ const directOptions = useMemo(() => pageSize === undefined ? noPagedOptions : Children.toArray(children), [children, pageSize]);
46
+ const controlled = props.type === 'multiple' ? props.values !== undefined : props.value !== undefined;
47
+ const paged = controlled && pageSize !== undefined && Number.isFinite(pageSize) && directOptions.length > 0
48
+ && directOptions.every(child => isValidElement(child) && child.type === ChoiceGridOption && !child.props.required);
49
+ const pageLength = paged ? Math.max(1, Math.floor(pageSize)) : 1;
50
+ const pageCount = paged ? Math.ceil(directOptions.length / pageLength) : 0;
51
+ const currentPage = Math.min(page, Math.max(0, pageCount - 1));
52
+ const pageChildren = paged ? directOptions.map((child, index) => {
53
+ const option = child;
54
+ if (index >= currentPage * pageLength && index < (currentPage + 1) * pageLength)
55
+ return child;
56
+ const selected = props.type === 'multiple' ? values?.has(option.props.value) : props.value === option.props.value;
57
+ return selected ? _jsx("input", { type: "hidden", name: name, value: option.props.value, disabled: disabled || option.props.disabled, form: option.props.form ?? form }, option.key) : null;
58
+ }) : children;
59
+ const pageControls = paged ? _jsxs("nav", { "aria-label": typeof label === 'string' ? label : undefined, children: [_jsx(Button, { size: "sm", variant: "ghost", "aria-label": previousPageLabel, disabled: disabled || currentPage === 0, onClick: () => setPage(currentPage - 1), children: previousPageLabel }), _jsx("span", { "aria-live": "polite", children: pageLabel(currentPage + 1, pageCount) }), _jsx(Button, { size: "sm", variant: "ghost", "aria-label": nextPageLabel, disabled: disabled || currentPage >= pageCount - 1, onClick: () => setPage(currentPage + 1), children: nextPageLabel })] }) : null;
60
+ const pagingValues = paged ? values : undefined;
61
+ const onMultipleChange = useCallback((optionValue, checked) => {
62
+ if (!onValuesChange)
63
+ return;
64
+ if (paged) {
65
+ onValuesChange(directOptions.filter(option => option.props.value === optionValue ? checked : pagingValues?.has(option.props.value)).map(option => option.props.value));
66
+ return;
67
+ }
68
+ const selected = Array.from(rootRef.current?.querySelectorAll('input[type="checkbox"][data-choice-grid-value]') ?? []).filter(input => input.value === optionValue ? checked : input.checked)
69
+ .map(input => input.value);
70
+ onValuesChange(selected);
71
+ }, [onValuesChange, paged, directOptions, pagingValues]);
72
+ const context = useMemo(() => ({
73
+ defaultValues, disabled, form, mode: type, name, onMultipleChange, values,
74
+ }), [defaultValues, disabled, form, type, name, onMultipleChange, values]);
37
75
  if (type === 'single') {
38
76
  const single = props;
39
77
  const { defaultValue: _defaultValue, onValueChange: _onValueChange, value: _value, ...singleFieldsetProps } = rest;
40
- return (_jsxs("div", { className: classes('inkra-choice-grid', className), "data-columns": columns, "data-disabled": disabled || undefined, "data-invalid": invalid || undefined, "data-type": "single", style: gridStyle(style, columns, minItemWidth), children: [_jsx(ChoiceGridContext.Provider, { value: { disabled, form, mode: 'single', name }, children: _jsx(RadioGroup, { ...singleFieldsetProps, "aria-describedby": describedBy, "aria-errormessage": errorMessage, "aria-invalid": invalid || undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, defaultValue: single.defaultValue, disabled: disabled, form: form, label: label, name: name, onValueChange: single.onValueChange, ref: rootRef, value: single.value, children: children }) }), _jsx(GroupMessages, { description: description, descriptionId: descriptionId, error: error, errorId: errorId })] }));
78
+ return (_jsxs("div", { className: classes('inkra-choice-grid', className), "data-columns": columns, "data-disabled": disabled || undefined, "data-invalid": invalid || undefined, "data-type": "single", style: gridStyle(style, columns, minItemWidth), children: [_jsxs(ChoiceGridContext.Provider, { value: context, children: [_jsx(RadioGroup, { ...singleFieldsetProps, "aria-describedby": describedBy, "aria-errormessage": errorMessage, "aria-invalid": invalid || undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, defaultValue: single.defaultValue, disabled: disabled, form: form, label: label, name: name, onValueChange: single.onValueChange, ref: rootRef, value: single.value, children: pageChildren }), pageControls] }), _jsx(GroupMessages, { description: description, descriptionId: descriptionId, error: error, errorId: errorId })] }));
41
79
  }
42
- const multiple = props;
43
80
  const { defaultValues: _defaultValues, onValuesChange: _onValuesChange, values: _values, ...multipleFieldsetProps } = rest;
44
- const onMultipleChange = (optionValue, checked) => {
45
- if (!multiple.onValuesChange)
46
- return;
47
- const selected = Array.from(rootRef.current?.querySelectorAll('input[type="checkbox"][data-choice-grid-value]') ?? []).filter(input => input.value === optionValue ? checked : input.checked)
48
- .map(input => input.value);
49
- multiple.onValuesChange(selected);
50
- };
51
- const context = {
52
- defaultValues: multiple.defaultValues,
53
- disabled,
54
- form,
55
- mode: 'multiple',
56
- name,
57
- onMultipleChange,
58
- values: multiple.values,
59
- };
60
- return (_jsxs("fieldset", { ...multipleFieldsetProps, "aria-describedby": describedBy, "aria-errormessage": errorMessage, "aria-invalid": invalid || undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, className: classes('inkra-choice-grid', className), "data-columns": columns, "data-disabled": disabled || undefined, "data-invalid": invalid || undefined, "data-type": "multiple", disabled: disabled, form: form, ref: rootRef, style: gridStyle(style, columns, minItemWidth), children: [label == null ? null : typeof label === 'string' ? (_jsx(PencilText, { as: "legend", className: "inkra-choice-grid__legend", seed: `inkra-choice-grid-${generatedId}-label`, children: label })) : _jsx("legend", { className: "inkra-choice-grid__legend", children: label }), _jsx(ChoiceGridContext.Provider, { value: context, children: _jsx("div", { className: "inkra-choice-grid__items", children: children }) }), _jsx(GroupMessages, { description: description, descriptionId: descriptionId, error: error, errorId: errorId })] }));
61
- });
81
+ return (_jsxs("fieldset", { ...multipleFieldsetProps, "aria-describedby": describedBy, "aria-errormessage": errorMessage, "aria-invalid": invalid || undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, className: classes('inkra-choice-grid', className), "data-columns": columns, "data-disabled": disabled || undefined, "data-invalid": invalid || undefined, "data-type": "multiple", disabled: disabled, form: form, ref: rootRef, style: gridStyle(style, columns, minItemWidth), children: [label == null ? null : typeof label === 'string' ? (_jsx(PencilText, { as: "legend", className: "inkra-choice-grid__legend", seed: `inkra-choice-grid-${generatedId}-label`, children: label })) : _jsx("legend", { className: "inkra-choice-grid__legend", children: label }), _jsxs(ChoiceGridContext.Provider, { value: context, children: [_jsx("div", { className: "inkra-choice-grid__items", children: pageChildren }), pageControls] }), _jsx(GroupMessages, { description: description, descriptionId: descriptionId, error: error, errorId: errorId })] }));
82
+ }));
62
83
  function OptionMedia({ icon, image }) {
63
84
  if (image) {
64
85
  return (_jsx("span", { "aria-hidden": "true", className: "inkra-choice-grid__media", "data-kind": "image", children: _jsx("img", { ...image, alt: "" }) }));
@@ -68,18 +89,25 @@ function OptionMedia({ icon, image }) {
68
89
  }
69
90
  return null;
70
91
  }
71
- export const ChoiceGridOption = forwardRef(function ChoiceGridOption({ className, description, disabled = false, form, icon, id, image, label, onChange, required = false, seed, value, ...props }, ref) {
72
- const context = useContext(ChoiceGridContext);
73
- if (!context)
74
- throw new Error('ChoiceGridOption must be rendered inside ChoiceGrid');
92
+ // Resolve context in a small adapter. A membership change must not rebuild the
93
+ // paper, text and controls of every option whose own checked state is unchanged.
94
+ const ChoiceGridOptionView = memo(function ChoiceGridOptionView({ className, description, disabled = false, form, icon, id, image, label, mode, inputRef, groupName, multipleChecked, multipleDefaultChecked, onMultipleChange, onChange, required = false, seed, value, ...props }) {
75
95
  const generatedId = useId();
76
96
  const inputId = id ?? `inkra-choice-grid-option-${generatedId}`;
77
97
  const resolvedSeed = seed ?? inputId;
78
- const itemDisabled = context.disabled || disabled;
79
- const content = (_jsxs(_Fragment, { children: [_jsx(OptionMedia, { icon: icon, image: image }), _jsx("span", { className: "inkra-choice-grid__option-label", children: label })] }));
80
- const control = context.mode === 'single' ? (_jsx(RadioGroupItem, { ...props, className: "inkra-choice-grid__control", description: description, disabled: itemDisabled, form: form ?? context.form, id: inputId, label: content, onChange: onChange, ref: ref, required: required, seed: resolvedSeed, value: value })) : (_jsx(Checkbox, { ...props, checked: context.values === undefined ? undefined : context.values.includes(value), className: "inkra-choice-grid__control", "data-choice-grid-value": "", defaultChecked: context.values === undefined ? context.defaultValues?.includes(value) : undefined, description: description, disabled: itemDisabled, form: form ?? context.form, id: inputId, label: content, name: context.name, onChange: (event) => {
98
+ const itemDisabled = disabled;
99
+ const content = (_jsxs(_Fragment, { children: [_jsx(OptionMedia, { icon: icon, image: image }), _jsx("span", { className: "inkra-choice-grid__option-label", children: typeof label === 'string'
100
+ ? _jsx(PencilText, { seed: `${resolvedSeed}-label`, children: label })
101
+ : label })] }));
102
+ const control = mode === 'single' ? (_jsx(RadioGroupItem, { ...props, className: "inkra-choice-grid__control", description: description, disabled: itemDisabled, form: form, id: inputId, label: content, onChange: onChange, ref: inputRef, required: required, seed: resolvedSeed, value: value })) : (_jsx(Checkbox, { ...props, checked: multipleChecked, className: "inkra-choice-grid__control", "data-choice-grid-value": "", defaultChecked: multipleDefaultChecked, description: description, disabled: itemDisabled, form: form, id: inputId, label: content, name: groupName, onChange: (event) => {
81
103
  onChange?.(event);
82
- context.onMultipleChange?.(value, event.currentTarget.checked);
83
- }, ref: ref, required: required, seed: resolvedSeed, value: value }));
84
- return (_jsxs(PencilSurface, { as: "div", className: classes('inkra-choice-grid__option', className), "data-disabled": itemDisabled || undefined, radius: 8, seed: `${resolvedSeed}-paper`, variant: "paper", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-choice-grid__selection", radius: 8, seed: `${resolvedSeed}-selection`, variant: "selection" }), control, _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-choice-grid__outline", radius: 8, seed: `${resolvedSeed}-outline`, variant: "outline" })] }));
104
+ onMultipleChange?.(value, event.currentTarget.checked);
105
+ }, ref: inputRef, required: required, seed: resolvedSeed, value: value }));
106
+ return (_jsxs(PencilSurface, { as: "div", className: classes('inkra-choice-grid__option', className), "data-disabled": itemDisabled || undefined, radius: 8, seed: `${resolvedSeed}-paper`, variant: "paper", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-choice-grid__selection", radius: 8, seed: `${resolvedSeed}-selection`, variant: "selection" }), _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-choice-grid__focus", radius: 8, seed: `${resolvedSeed}-focus`, variant: "outline" }), control, _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-choice-grid__outline", radius: 8, seed: `${resolvedSeed}-outline`, variant: "outline" })] }));
107
+ });
108
+ export const ChoiceGridOption = forwardRef(function ChoiceGridOption(props, ref) {
109
+ const context = useContext(ChoiceGridContext);
110
+ if (!context)
111
+ throw new Error('ChoiceGridOption must be rendered inside ChoiceGrid');
112
+ return _jsx(ChoiceGridOptionView, { ...props, disabled: context.disabled || props.disabled, form: props.form ?? context.form, groupName: context.name, mode: context.mode, multipleChecked: context.values === undefined ? undefined : context.values.has(props.value), multipleDefaultChecked: context.values === undefined ? context.defaultValues?.has(props.value) : undefined, onMultipleChange: context.onMultipleChange, inputRef: ref });
85
113
  });
@@ -0,0 +1,39 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ColorGradient } from './gradient.js';
3
+ import type { ColorValue } from './color.js';
4
+ export interface ColorGradientLabels {
5
+ type?: string;
6
+ solid?: string;
7
+ linear?: string;
8
+ radial?: string;
9
+ conic?: string;
10
+ preview?: string;
11
+ stops?: string;
12
+ stop?: string;
13
+ stopColor?: string;
14
+ addStop?: string;
15
+ removeStop?: string;
16
+ reverse?: string;
17
+ position?: string;
18
+ angle?: string;
19
+ centerX?: string;
20
+ centerY?: string;
21
+ shape?: string;
22
+ circle?: string;
23
+ ellipse?: string;
24
+ extent?: string;
25
+ closestSide?: string;
26
+ closestCorner?: string;
27
+ farthestSide?: string;
28
+ farthestCorner?: string;
29
+ repeating?: string;
30
+ }
31
+ export declare const defaultGradientLabels: Required<ColorGradientLabels>;
32
+ export declare function GradientEditor({ value, disabled, maxStops, labels, onChange, renderColorEditor }: {
33
+ value: ColorGradient;
34
+ disabled: boolean;
35
+ maxStops: number;
36
+ labels: Required<ColorGradientLabels>;
37
+ onChange: (value: ColorGradient, source: 'stop' | 'geometry') => void;
38
+ renderColorEditor: (value: ColorValue, onChange: (value: ColorValue) => void) => ReactNode;
39
+ }): import("react").JSX.Element;