@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
@@ -1,6 +1,8 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useInkraLocale, resolveInkraLocale } from '../i18n/index.js';
3
+ import { calendarLocales } from '../calendar/locales.js';
2
4
  import { forwardRef, useEffect, useId, useImperativeHandle, useMemo, useRef, useState, } from 'react';
3
- import { Button } from '../Button.js';
5
+ import { Button, ButtonGroup } from '../Button.js';
4
6
  import { Calendar } from '../calendar/index.js';
5
7
  import { mergeAriaIds, useFieldContext } from '../form/context.js';
6
8
  import { useResolvedControlProps } from '../form/controlProps.js';
@@ -35,24 +37,19 @@ function initialForMode(mode, value) {
35
37
  return validDate(value) ? value : undefined;
36
38
  return isRange(value) ? value : undefined;
37
39
  }
38
- function defaultCopy(locale) {
39
- const chinese = locale.toLowerCase().startsWith('zh');
40
+ function defaultCopy(t) {
40
41
  return {
41
- calendar: chinese ? '选择日期' : 'Choose date',
42
- clear: chinese ? '清空日期' : 'Clear date',
43
- next: chinese ? '下个月' : 'Next month',
44
- open: chinese ? '打开日期选择器' : 'Open date picker',
45
- placeholder: chinese ? '选择日期' : 'Choose a date',
46
- previous: chinese ? '上个月' : 'Previous month',
47
- rangePlaceholder: chinese ? '选择日期范围' : 'Choose a date range',
48
- selected: chinese ? '已选择' : 'selected',
49
- today: chinese ? '今天' : 'today',
42
+ calendar: t('datePicker.calendar'), clear: t('datePicker.clear'),
43
+ next: t('datePicker.next'), open: t('datePicker.open'),
44
+ placeholder: t('datePicker.placeholder'), previous: t('datePicker.previous'),
45
+ rangePlaceholder: t('datePicker.range'), selected: t('datePicker.selected'), today: t('datePicker.today'),
50
46
  };
51
47
  }
52
48
  export const DatePicker = forwardRef(function DatePicker(allProps, forwardedRef) {
53
49
  const valueControlled = Object.prototype.hasOwnProperty.call(allProps, 'value');
54
50
  const openControlled = Object.prototype.hasOwnProperty.call(allProps, 'open');
55
- const { 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, 'aria-label': ariaLabel, autoFocus, calendarLabel, calendarProps = {}, className, clearable = true, clearButtonLabel, defaultOpen = false, defaultValue, dir, disabled = false, endName, form, fullWidth = true, id, invalid = false, locale = 'en-US', mode = 'single', name, onBlur, onFocus, onOpenChange, onValueChange, open, openButtonLabel, placeholder, rangeSeparator = ' – ', readOnly = false, required = false, style, value, } = allProps;
51
+ const { 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, 'aria-label': ariaLabel, autoFocus, calendarLabel, calendarProps = {}, className, clearable = true, clearButtonLabel, defaultOpen = false, defaultValue, dir, disabled = false, endName, form, fullWidth = true, id, invalid = false, locale: localeProp, mode = 'single', name, onBlur, onFocus, onOpenChange, onValueChange, open, openButtonLabel, placeholder, rangeSeparator = ' – ', readOnly = false, required = false, size = 'md', style, value, } = allProps;
52
+ const { locale, t } = useInkraLocale(localeProp);
56
53
  const generatedId = useId();
57
54
  const field = useFieldContext();
58
55
  const triggerRef = useRef(null);
@@ -71,7 +68,7 @@ export const DatePicker = forwardRef(function DatePicker(allProps, forwardedRef)
71
68
  'aria-errormessage': ariaErrorMessage,
72
69
  'aria-invalid': ariaInvalid,
73
70
  });
74
- const copy = defaultCopy(locale);
71
+ const copy = defaultCopy(t);
75
72
  const currentValue = initialForMode(mode, valueControlled ? value : uncontrolledValue);
76
73
  const currentOpen = openControlled ? Boolean(open) : uncontrolledOpen;
77
74
  const singleValue = validDate(currentValue) ? currentValue : undefined;
@@ -85,6 +82,7 @@ export const DatePicker = forwardRef(function DatePicker(allProps, forwardedRef)
85
82
  const captionFormatter = useMemo(() => new Intl.DateTimeFormat(locale, {
86
83
  month: 'long', year: 'numeric',
87
84
  }), [locale]);
85
+ const dayFormatter = useMemo(() => new Intl.NumberFormat(locale), [locale]);
88
86
  const weekdayFormatter = useMemo(() => new Intl.DateTimeFormat(locale, { weekday: 'short' }), [locale]);
89
87
  const displayValue = singleValue
90
88
  ? formatter.format(singleValue)
@@ -128,7 +126,7 @@ export const DatePicker = forwardRef(function DatePicker(allProps, forwardedRef)
128
126
  const { disabled: disabledDays, formatters, labels, defaultMonth, ...restCalendarProps } = calendarProps;
129
127
  const localizedFormatters = {
130
128
  formatCaption: date => captionFormatter.format(date),
131
- formatDay: date => new Intl.NumberFormat(locale).format(date.getDate()),
129
+ formatDay: date => dayFormatter.format(date.getDate()),
132
130
  formatWeekdayName: date => weekdayFormatter.format(date),
133
131
  ...formatters,
134
132
  };
@@ -143,6 +141,7 @@ export const DatePicker = forwardRef(function DatePicker(allProps, forwardedRef)
143
141
  ...labels,
144
142
  };
145
143
  const sharedCalendarProps = {
144
+ locale: calendarLocales[resolveInkraLocale(locale)],
146
145
  ...restCalendarProps,
147
146
  appearance: 'picker',
148
147
  defaultMonth: defaultMonth ?? firstDate,
@@ -150,11 +149,11 @@ export const DatePicker = forwardRef(function DatePicker(allProps, forwardedRef)
150
149
  formatters: localizedFormatters,
151
150
  labels: localizedLabels,
152
151
  };
153
- return (_jsxs(Popover.Root, { open: currentOpen, onOpenChange: updateOpen, children: [_jsxs("span", { className: classes('inkra-date-picker', className), "data-disabled": state.disabled || undefined, "data-full-width": fullWidth || undefined, "data-has-value": hasValue || undefined, "data-invalid": state.invalid || constraintInvalid || undefined, "data-mode": mode, "data-readonly": state.readOnly || undefined, dir: dir, style: style, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsx(Button, { "aria-describedby": mergeAriaIds(state.describedBy, accessibleValueId), "aria-errormessage": state.errorMessage, "aria-invalid": constraintInvalid || state.ariaInvalid, "aria-label": ariaLabel ?? (openButtonLabel ? `${openButtonLabel}: ${displayValue}` : undefined), "aria-required": state.required || undefined, autoFocus: autoFocus, className: "inkra-date-picker__trigger", disabled: state.disabled, form: form, fullWidth: fullWidth, id: triggerId, onBlur: onBlur, onFocus: onFocus, ref: triggerRef, startIcon: _jsx(PencilIcon, { paths: calendarPaths, sourceSize: 24, size: 22 }), variant: "outline", children: _jsx("span", { className: "inkra-date-picker__value", "data-placeholder": !hasValue || undefined, children: displayValue }) }) }), accessibleValueId ? _jsx("span", { className: "inkra-date-picker__accessible-value", id: accessibleValueId, children: displayValue }) : null, clearable ? (_jsx(Button, { "aria-hidden": !hasValue || undefined, "aria-label": clearButtonLabel ?? copy.clear, className: "inkra-date-picker__clear", disabled: !hasValue || state.disabled || state.readOnly, onClick: () => {
154
- commit(undefined, 'clear');
155
- updateOpen(false);
156
- triggerRef.current?.focus({ preventScroll: true });
157
- }, size: "icon", tabIndex: hasValue ? 0 : -1, variant: "ghost", children: _jsx(PencilIcon, { paths: clearPaths, sourceSize: 24, size: 22 }) })) : null, name || state.required ? (mode === 'range' ? _jsxs(_Fragment, { children: [_jsx("input", { "aria-hidden": "true", className: "inkra-date-picker__form-field", disabled: state.disabled, form: form, name: name, onChange: () => undefined, onInvalid: event => { event.preventDefault(); setConstraintInvalid(true); triggerRef.current?.focus(); }, readOnly: state.readOnly, required: state.required, tabIndex: -1, type: "text", value: datePickerLocalValue(rangeValue?.from) }), _jsx("input", { "aria-hidden": "true", className: "inkra-date-picker__form-field", disabled: state.disabled, form: form, name: endName ?? (name ? `${name}End` : undefined), onChange: () => undefined, onInvalid: event => { event.preventDefault(); setConstraintInvalid(true); triggerRef.current?.focus(); }, readOnly: state.readOnly, required: state.required, tabIndex: -1, type: "text", value: datePickerLocalValue(rangeValue?.to) })] }) : _jsx("input", { "aria-hidden": "true", className: "inkra-date-picker__form-field", disabled: state.disabled, form: form, name: name, onChange: () => undefined, onInvalid: event => { event.preventDefault(); setConstraintInvalid(true); triggerRef.current?.focus(); }, readOnly: state.readOnly, required: state.required, tabIndex: -1, type: "text", value: datePickerLocalValue(singleValue) })) : null] }), _jsx(Popover.Portal, { children: _jsxs(Popover.Content, { align: "start", "aria-label": calendarLabel ?? copy.calendar, className: "inkra-date-picker__popover", collisionPadding: 12, onOpenAutoFocus: event => {
152
+ return (_jsxs(Popover.Root, { open: currentOpen, onOpenChange: updateOpen, children: [_jsxs("span", { className: classes('inkra-date-picker', className), "data-disabled": state.disabled || undefined, "data-full-width": fullWidth || undefined, "data-has-value": hasValue || undefined, "data-invalid": state.invalid || constraintInvalid || undefined, "data-mode": mode, "data-readonly": state.readOnly || undefined, "data-size": size, dir: dir, style: style, children: [_jsxs(ButtonGroup, { "aria-label": copy.calendar, className: "inkra-date-picker__control", "data-size": size, fullWidth: fullWidth, size: size, variant: "outline", children: [_jsx(Popover.Trigger, { asChild: true, children: _jsx(Button, { "aria-describedby": mergeAriaIds(state.describedBy, accessibleValueId), "aria-errormessage": state.errorMessage, "aria-invalid": constraintInvalid || state.ariaInvalid, "aria-label": ariaLabel ?? (openButtonLabel ? `${openButtonLabel}: ${displayValue}` : undefined), "aria-required": state.required || undefined, autoFocus: autoFocus, className: "inkra-date-picker__trigger", disabled: state.disabled, form: form, fullWidth: fullWidth, id: triggerId, onBlur: onBlur, onFocus: onFocus, ref: triggerRef, startIcon: _jsx(PencilIcon, { paths: calendarPaths, sourceSize: 24, size: 22 }), children: _jsx("span", { className: "inkra-date-picker__value", "data-placeholder": !hasValue || undefined, children: displayValue }) }) }), clearable ? (_jsx(Button, { "aria-hidden": !hasValue || undefined, "aria-label": clearButtonLabel ?? copy.clear, className: "inkra-date-picker__clear", disabled: !hasValue || state.disabled || state.readOnly, onPointerDown: event => event.preventDefault(), onClick: () => {
153
+ commit(undefined, 'clear');
154
+ updateOpen(false);
155
+ triggerRef.current?.focus({ preventScroll: true });
156
+ }, size: size === 'sm' ? 'icon-sm' : size === 'lg' ? 'icon-lg' : 'icon', tabIndex: hasValue ? 0 : -1, variant: "ghost", children: _jsx(PencilIcon, { paths: clearPaths, sourceSize: 24, size: 22 }) })) : null] }), accessibleValueId ? _jsx("span", { className: "inkra-date-picker__accessible-value", id: accessibleValueId, children: displayValue }) : null, name || state.required ? (mode === 'range' ? _jsxs(_Fragment, { children: [_jsx("input", { "aria-hidden": "true", className: "inkra-date-picker__form-field", disabled: state.disabled, form: form, name: name, onChange: () => undefined, onInvalid: event => { event.preventDefault(); setConstraintInvalid(true); triggerRef.current?.focus(); }, readOnly: state.readOnly, required: state.required, tabIndex: -1, type: "text", value: datePickerLocalValue(rangeValue?.from) }), _jsx("input", { "aria-hidden": "true", className: "inkra-date-picker__form-field", disabled: state.disabled, form: form, name: endName ?? (name ? `${name}End` : undefined), onChange: () => undefined, onInvalid: event => { event.preventDefault(); setConstraintInvalid(true); triggerRef.current?.focus(); }, readOnly: state.readOnly, required: state.required, tabIndex: -1, type: "text", value: datePickerLocalValue(rangeValue?.to) })] }) : _jsx("input", { "aria-hidden": "true", className: "inkra-date-picker__form-field", disabled: state.disabled, form: form, name: name, onChange: () => undefined, onInvalid: event => { event.preventDefault(); setConstraintInvalid(true); triggerRef.current?.focus(); }, readOnly: state.readOnly, required: state.required, tabIndex: -1, type: "text", value: datePickerLocalValue(singleValue) })) : null] }), _jsx(Popover.Portal, { children: _jsxs(Popover.Content, { align: "start", "aria-label": calendarLabel ?? copy.calendar, className: "inkra-date-picker__popover", collisionPadding: 12, onOpenAutoFocus: event => {
158
157
  event.preventDefault();
159
158
  requestAnimationFrame(() => {
160
159
  requestAnimationFrame(() => {
@@ -13,6 +13,7 @@ export interface AgendaEvent {
13
13
  }
14
14
  export interface DayAgendaProps extends Omit<HTMLAttributes<HTMLElement>, 'onSelect'> {
15
15
  date: string;
16
+ /** Immutable snapshots: replace the events array and changed event records when editing. */
16
17
  events: readonly AgendaEvent[];
17
18
  selectedEvent?: string;
18
19
  onEventSelect?: (event: AgendaEvent) => void;
@@ -32,6 +33,10 @@ export interface DayAgendaProps extends Omit<HTMLAttributes<HTMLElement>, 'onSel
32
33
  previous: string;
33
34
  next: string;
34
35
  };
36
+ /** Opt-in initial summary. Layout uses every event and all can be revealed. */
37
+ maxVisibleEvents?: number;
38
+ showAllEventsLabel?: (count: number) => string;
39
+ showFewerEventsLabel?: string;
35
40
  }
36
41
  export declare function agendaTime(minute: number, hourCycle?: 'h12' | 'h23', locale?: string): string;
37
42
  export declare function agendaInterval(event: AgendaEvent): {
@@ -40,4 +45,4 @@ export declare function agendaInterval(event: AgendaEvent): {
40
45
  before: boolean;
41
46
  after: boolean;
42
47
  } | null;
43
- export declare function DayAgenda({ date, events, selectedEvent, onEventSelect, locale, timeZoneLabel, emptyLabel, untimedLabel, showOverview, hourCycle, density, showDescriptions, divided, disabled, onPreviousDay, onNextDay, navigationLabels, className, ...props }: DayAgendaProps): import("react").JSX.Element;
48
+ export declare function DayAgenda(inkraProps: DayAgendaProps): import("react").JSX.Element;
@@ -1,19 +1,25 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useId } from 'react';
2
+ import { useId, useMemo, useState } from 'react';
3
+ import { useInkraLocale } from '../i18n/index.js';
3
4
  import { TravelIcon } from '../internal/travelIcons.js';
4
5
  import { Button } from '../Button.js';
5
6
  import { createAgendaScale, layoutAgendaCards } from './layout.js';
6
7
  import { PencilSurface } from '../PencilSurface.js';
7
8
  import { PencilLine } from '../PencilLine.js';
8
9
  import { PencilText } from '../PencilText.js';
9
- export function agendaTime(minute, hourCycle = 'h23', locale = 'en') {
10
+ function formatAgendaMinute(minute, formatter) {
10
11
  const value = Math.max(0, Math.min(1440, Math.floor(minute)));
11
- if (hourCycle === 'h12') {
12
+ if (formatter) {
12
13
  const time = new Date(Date.UTC(2000, 0, 1, 0, value));
13
- return new Intl.DateTimeFormat(locale, { timeZone: 'UTC', hour: 'numeric', minute: '2-digit', hourCycle }).format(time) + (value === 1440 ? ' +1' : '');
14
+ return formatter.format(time) + (value === 1440 ? ' +1' : '');
14
15
  }
15
16
  return value === 1440 ? '00:00 +1' : `${String(Math.floor(value / 60)).padStart(2, '0')}:${String(value % 60).padStart(2, '0')}`;
16
17
  }
18
+ export function agendaTime(minute, hourCycle = 'h23', locale = 'en') {
19
+ return formatAgendaMinute(minute, hourCycle === 'h12'
20
+ ? new Intl.DateTimeFormat(locale, { timeZone: 'UTC', hour: 'numeric', minute: '2-digit', hourCycle })
21
+ : undefined);
22
+ }
17
23
  export function agendaInterval(event) {
18
24
  if (event.startMinute === undefined || !Number.isFinite(event.startMinute))
19
25
  return null;
@@ -24,24 +30,46 @@ export function agendaInterval(event) {
24
30
  return null;
25
31
  return { start: Math.max(0, event.startMinute), end: Math.min(1440, end ?? event.startMinute), before: event.startMinute < 0, after: (end ?? 0) > 1440 };
26
32
  }
27
- export function DayAgenda({ date, events, selectedEvent, onEventSelect, locale = 'en', timeZoneLabel, emptyLabel = 'No events', untimedLabel = 'Time pending', showOverview = true, hourCycle = 'h23', density = 'comfortable', showDescriptions = true, divided = true, disabled = false, onPreviousDay, onNextDay, navigationLabels = { previous: 'Previous day', next: 'Next day' }, className, ...props }) {
33
+ export function DayAgenda(inkraProps) {
34
+ const { locale, t } = useInkraLocale(inkraProps.locale);
35
+ const { date, events, selectedEvent, onEventSelect, locale: _locale, timeZoneLabel, emptyLabel = t('agenda.empty'), untimedLabel = t('agenda.pending'), showOverview = true, hourCycle = 'h23', density = 'comfortable', showDescriptions = true, divided = true, disabled = false, onPreviousDay, onNextDay, navigationLabels = { previous: t('agenda.previous'), next: t('agenda.next') }, maxVisibleEvents, showAllEventsLabel = count => t('agenda.all', { count }), showFewerEventsLabel = t('agenda.fewer'), className, ...props } = inkraProps;
28
36
  const id = useId();
29
- const validDate = /^\d{4}-\d{2}-\d{2}$/.test(date) ? new Date(`${date}T12:00:00Z`) : new Date(NaN);
30
- const title = Number.isFinite(validDate.getTime()) ? new Intl.DateTimeFormat(locale, { timeZone: 'UTC', month: 'long', day: 'numeric', weekday: 'short' }).format(validDate) : date;
31
- const ordered = [...events].sort((a, b) => (agendaInterval(a)?.start ?? Infinity) - (agendaInterval(b)?.start ?? Infinity));
32
- const formatTime = (minute) => agendaTime(minute, hourCycle, locale);
33
- const timed = ordered.flatMap(event => {
34
- const interval = agendaInterval(event);
35
- return interval ? [{ ...interval, id: event.id, event }] : [];
36
- });
37
- const intervals = timed.filter(row => row.end > row.start);
38
- const points = timed.filter(row => row.end === row.start);
39
- const scale = createAgendaScale(intervals, points.map(row => row.start), density === 'compact');
40
- const cards = layoutAgendaCards(timed, scale).sort((a, b) => a.start - b.start);
41
- const ticks = Array.from({ length: 25 }, (_, hour) => hour * 60).filter((minute, index, list) => index === 0 || index === 24 || scale.position(minute) - scale.position(list[index - 1]) >= 25);
37
+ const [expandedDate, setExpandedDate] = useState();
38
+ const displayLimit = maxVisibleEvents !== undefined && Number.isFinite(maxVisibleEvents) ? Math.max(0, Math.floor(maxVisibleEvents)) : Infinity;
39
+ const expanded = expandedDate === date;
40
+ const title = useMemo(() => {
41
+ const validDate = /^\d{4}-\d{2}-\d{2}$/.test(date) ? new Date(`${date}T12:00:00Z`) : new Date(NaN);
42
+ return Number.isFinite(validDate.getTime()) ? new Intl.DateTimeFormat(locale, { timeZone: 'UTC', month: 'long', day: 'numeric', weekday: 'short' }).format(validDate) : date;
43
+ }, [date, locale]);
44
+ const timeFormatter = useMemo(() => hourCycle === 'h12'
45
+ ? new Intl.DateTimeFormat(locale, { timeZone: 'UTC', hour: 'numeric', minute: '2-digit', hourCycle })
46
+ : undefined, [hourCycle, locale]);
47
+ const { ordered, pending, scale, cards, ticks } = useMemo(() => {
48
+ const records = events.map(event => ({ event, interval: agendaInterval(event) }))
49
+ .sort((a, b) => (a.interval?.start ?? Infinity) - (b.interval?.start ?? Infinity));
50
+ const ordered = records.map(row => row.event);
51
+ const pending = records.filter(row => !row.interval).map(row => row.event);
52
+ const timed = records.flatMap(({ event, interval }) => interval ? [{ ...interval, id: event.id, event }] : []);
53
+ const intervals = timed.filter(row => row.end > row.start);
54
+ const points = timed.filter(row => row.end === row.start);
55
+ const scale = createAgendaScale(intervals, points.map(row => row.start), density === 'compact');
56
+ const formatTime = (minute) => formatAgendaMinute(minute, timeFormatter);
57
+ const cards = layoutAgendaCards(timed, scale).sort((a, b) => a.start - b.start).map(row => ({
58
+ ...row,
59
+ time: `${row.before ? '\u2190 ' : ''}${formatTime(row.start)}${row.event.endMinute !== undefined ? ` - ${formatTime(row.end)}` : ''}${row.after ? ' \u2192' : ''}`,
60
+ }));
61
+ const ticks = Array.from({ length: 25 }, (_, hour) => hour * 60)
62
+ .filter((minute, index, list) => index === 0 || index === 24 || scale.position(minute) - scale.position(list[index - 1]) >= 25)
63
+ .map(minute => ({ minute, label: formatTime(minute) }));
64
+ return { ordered, pending, scale, cards, ticks };
65
+ }, [density, events, timeFormatter]);
66
+ const shownIds = useMemo(() => expanded || displayLimit >= ordered.length ? null
67
+ : new Set([...ordered.slice(0, displayLimit).map(event => event.id), ...(selectedEvent ? [selectedEvent] : [])]), [expanded, displayLimit, ordered, selectedEvent]);
68
+ const visibleCards = shownIds === null ? cards : cards.filter(row => shownIds.has(row.id));
69
+ const visiblePending = shownIds === null ? pending : pending.filter(event => shownIds.has(event.id));
42
70
  const renderCard = (event, time) => _jsxs(PencilSurface, { variant: "quiet", radius: 6, className: "inkra-agenda__card", "data-selected": selectedEvent === event.id, style: { '--inkra-accent-soft': event.color ?? (event.kind === 'stay' ? '#c8c38f' : event.kind === 'travel' ? '#87bbc0' : '#a7c7ad') }, children: [onEventSelect && _jsx("button", { type: "button", className: "inkra-agenda__hit", disabled: disabled || event.disabled, "aria-label": `${time}, ${event.title}`, "aria-pressed": selectedEvent === event.id, onClick: () => onEventSelect(event) }), _jsxs("div", { className: "inkra-agenda__card-copy", children: [_jsxs("span", { className: "inkra-agenda__card-title", title: event.title, children: [event.icon && _jsx("span", { "aria-hidden": "true", children: event.icon }), _jsx(PencilText, { ellipsis: true, children: event.title })] }), showDescriptions && event.description && _jsx("span", { className: "inkra-agenda__description", children: event.description })] })] });
43
- return _jsxs("section", { ...props, "data-density": density, "data-hour-cycle": hourCycle, className: ['inkra-agenda', className].filter(Boolean).join(' '), "aria-labelledby": props['aria-labelledby'] ?? id, children: [_jsxs("header", { className: "inkra-agenda__heading", children: [_jsx(PencilText, { as: "h3", id: id, children: title }), timeZoneLabel && _jsx(PencilText, { children: timeZoneLabel })] }), _jsx(PencilLine, {}), ordered.length === 0 ? _jsx(PencilText, { as: "p", children: emptyLabel }) : _jsxs(_Fragment, { children: [_jsx("div", { className: "inkra-agenda__timeline", "data-overview": showOverview, children: _jsxs("div", { className: "inkra-agenda__scale", style: { height: scale.height + 24 }, children: [ticks.map(minute => _jsxs("div", { className: "inkra-agenda__tick", style: { top: scale.position(minute) + 12 }, children: [_jsx("span", { children: _jsx(PencilText, { children: formatTime(minute) }) }), divided && _jsx(PencilLine, {})] }, minute)), _jsx("ol", { className: "inkra-agenda__lanes", children: cards.map(row => {
44
- const time = `${row.before ? '\u2190 ' : ''}${formatTime(row.start)}${row.event.endMinute !== undefined ? ` - ${formatTime(row.end)}` : ''}${row.after ? ' \u2192' : ''}`;
71
+ return _jsxs("section", { ...props, "data-density": density, "data-hour-cycle": hourCycle, className: ['inkra-agenda', className].filter(Boolean).join(' '), "aria-labelledby": props['aria-labelledby'] ?? id, children: [_jsxs("header", { className: "inkra-agenda__heading", children: [_jsx(PencilText, { as: "h3", id: id, children: title }), timeZoneLabel && _jsx(PencilText, { children: timeZoneLabel })] }), _jsx(PencilLine, {}), ordered.length === 0 ? _jsx(PencilText, { as: "p", children: emptyLabel }) : _jsxs(_Fragment, { children: [_jsx("div", { className: "inkra-agenda__timeline", "data-overview": showOverview, children: _jsxs("div", { className: "inkra-agenda__scale", style: { height: scale.height + 24 }, children: [ticks.map(({ minute, label }) => _jsxs("div", { className: "inkra-agenda__tick", style: { top: scale.position(minute) + 12 }, children: [_jsx("span", { children: _jsx(PencilText, { children: label }) }), divided && _jsx(PencilLine, {})] }, minute)), _jsx("ol", { className: "inkra-agenda__lanes", children: visibleCards.map(row => {
72
+ const time = row.time;
45
73
  return _jsxs("li", { "data-kind": row.event.kind, style: { top: row.top + 14, height: Math.max(44, row.height - 4), insetInlineStart: `${row.lane / row.lanes * 100}%`, width: `${100 / row.lanes}%` }, children: [renderCard(row.event, time), _jsx("span", { className: "inkra-agenda__accessible-time", children: time })] }, row.id);
46
- }) })] }) }), ordered.some(event => !agendaInterval(event)) && _jsxs("div", { className: "inkra-agenda__pending", children: [_jsx(PencilText, { children: untimedLabel }), ordered.filter(event => !agendaInterval(event)).map(event => _jsx("div", { children: renderCard(event, untimedLabel) }, event.id))] })] }), (onPreviousDay || onNextDay) && _jsxs("footer", { className: "inkra-agenda__navigation", children: [_jsx(PencilLine, {}), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": navigationLabels.previous, title: navigationLabels.previous, disabled: !onPreviousDay, onClick: onPreviousDay, children: _jsx(TravelIcon, { name: "previous" }) }), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": navigationLabels.next, title: navigationLabels.next, disabled: !onNextDay, onClick: onNextDay, children: _jsx(TravelIcon, { name: "next" }) })] })] });
74
+ }) })] }) }), visiblePending.length > 0 && _jsxs("div", { className: "inkra-agenda__pending", children: [_jsx(PencilText, { children: untimedLabel }), visiblePending.map(event => _jsx("div", { children: renderCard(event, untimedLabel) }, event.id))] }), ordered.length > displayLimit && _jsx(Button, { size: "sm", variant: "ghost", "aria-expanded": expanded, onClick: () => setExpandedDate(expanded ? undefined : date), children: expanded ? showFewerEventsLabel : showAllEventsLabel(ordered.length) })] }), (onPreviousDay || onNextDay) && _jsxs("footer", { className: "inkra-agenda__navigation", children: [_jsx(PencilLine, {}), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": navigationLabels.previous, title: navigationLabels.previous, disabled: !onPreviousDay, onClick: onPreviousDay, children: _jsx(TravelIcon, { name: "previous" }) }), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": navigationLabels.next, title: navigationLabels.next, disabled: !onNextDay, onClick: onNextDay, children: _jsx(TravelIcon, { name: "next" }) })] })] });
47
75
  }
@@ -1,16 +1,56 @@
1
+ function heapPush(heap, value, less) {
2
+ let index = heap.length;
3
+ heap.push(value);
4
+ while (index > 0) {
5
+ const parent = (index - 1) >>> 1;
6
+ if (!less(value, heap[parent]))
7
+ break;
8
+ heap[index] = heap[parent];
9
+ index = parent;
10
+ }
11
+ heap[index] = value;
12
+ }
13
+ function heapPop(heap, less) {
14
+ const first = heap[0];
15
+ const last = heap.pop();
16
+ if (heap.length) {
17
+ let index = 0;
18
+ while (index * 2 + 1 < heap.length) {
19
+ let child = index * 2 + 1;
20
+ if (child + 1 < heap.length && less(heap[child + 1], heap[child]))
21
+ child++;
22
+ if (!less(heap[child], last))
23
+ break;
24
+ heap[index] = heap[child];
25
+ index = child;
26
+ }
27
+ heap[index] = last;
28
+ }
29
+ return first;
30
+ }
31
+ const lowerLane = (left, right) => left < right;
32
+ const earlierEnd = (left, right) => left.end < right.end || (left.end === right.end && left.lane < right.lane);
1
33
  export function layoutAgendaIntervals(intervals) {
2
34
  const sorted = [...intervals].sort((a, b) => a.start - b.start || a.id.localeCompare(b.id));
3
35
  const result = [];
4
- let group = [], groupEnd = -Infinity, laneEnds = [];
5
- const flush = () => { for (const item of group)
6
- result.push({ ...item, lanes: laneEnds.length }); group = []; laneEnds = []; };
36
+ let group = [], groupEnd = -Infinity, laneCount = 0;
37
+ let occupied = [], available = [];
38
+ const flush = () => {
39
+ for (const item of group)
40
+ result.push({ ...item, lanes: laneCount });
41
+ group = [];
42
+ occupied = [];
43
+ available = [];
44
+ laneCount = 0;
45
+ };
7
46
  for (const item of sorted) {
8
47
  if (item.start >= groupEnd)
9
48
  flush();
10
- let lane = laneEnds.findIndex(end => end <= item.start);
11
- if (lane < 0)
12
- lane = laneEnds.length;
13
- laneEnds[lane] = item.end;
49
+ while (occupied.length && occupied[0].end <= item.start) {
50
+ heapPush(available, heapPop(occupied, earlierEnd).lane, lowerLane);
51
+ }
52
+ const lane = available.length ? heapPop(available, lowerLane) : laneCount++;
53
+ heapPush(occupied, { end: item.end, lane }, earlierEnd);
14
54
  group.push({ ...item, lane });
15
55
  groupEnd = Math.max(groupEnd, item.end);
16
56
  }
@@ -23,6 +63,10 @@ export function createAgendaScale(intervals, pointTimes, compact) {
23
63
  const boundaries = [...new Set([0, 1440, ...pointTimes, ...intervals.flatMap(row => [row.start, row.end])])].sort((a, b) => a - b);
24
64
  const before = new Map(), after = new Map();
25
65
  const segments = [];
66
+ // Boundaries already contain every endpoint, so the furthest active end is
67
+ // enough to decide occupancy without rescanning every event for every segment.
68
+ const ordered = [...intervals].sort((a, b) => a.start - b.start);
69
+ let intervalIndex = 0, occupiedUntil = -Infinity;
26
70
  let height = 0;
27
71
  for (let index = 0; index < boundaries.length; index++) {
28
72
  const time = boundaries[index];
@@ -33,7 +77,11 @@ export function createAgendaScale(intervals, pointTimes, compact) {
33
77
  const next = boundaries[index + 1];
34
78
  if (next === undefined)
35
79
  continue;
36
- const occupied = intervals.some(row => row.start < next && row.end > time);
80
+ while (intervalIndex < ordered.length && ordered[intervalIndex].start < next) {
81
+ occupiedUntil = Math.max(occupiedUntil, ordered[intervalIndex].end);
82
+ intervalIndex++;
83
+ }
84
+ const occupied = occupiedUntil > time;
37
85
  const size = Math.max(occupied ? rowHeight : 16, (next - time) / 60 * 32);
38
86
  segments.push({ start: time, end: next, top: height, size });
39
87
  height += size;
@@ -43,7 +91,15 @@ export function createAgendaScale(intervals, pointTimes, compact) {
43
91
  const exact = (edge === 'after' ? after : before).get(time);
44
92
  if (exact !== undefined)
45
93
  return exact;
46
- const segment = segments.find(row => time >= row.start && time <= row.end);
94
+ let low = 0, high = segments.length - 1;
95
+ while (low <= high) {
96
+ const middle = (low + high) >>> 1;
97
+ if (segments[middle].start <= time)
98
+ low = middle + 1;
99
+ else
100
+ high = middle - 1;
101
+ }
102
+ const segment = high >= 0 && time <= segments[high].end ? segments[high] : undefined;
47
103
  return segment ? segment.top + (time - segment.start) / (segment.end - segment.start) * segment.size : 0;
48
104
  };
49
105
  return { position, height, rowHeight };
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef } from 'react';
3
- import { BoundedCache, PENCIL_RENDERER_VERSION, canvasByteCost, canvasIsUsable, effectiveDpr } from '../internal/cache.js';
3
+ import { BoundedCache, PENCIL_RENDERER_VERSION, boundedCanvasSize, canvasByteCost, canvasIsUsable, effectiveDpr } from '../internal/cache.js';
4
4
  import { observeCanvasRecovery } from '../internal/recovery.js';
5
5
  import { seedToNumber } from '../internal/random.js';
6
6
  import { maxStrokeRoughness, pencilRenderDefaults, readRenderToken } from '../internal/renderTokens.js';
@@ -84,6 +84,7 @@ export function ProgressRing({ diameter, thickness, percent, seed }) {
84
84
  const weight = readRenderToken(style, '--inkra-stroke-width', pencilRenderDefaults.strokeWidth, 0.3, 3);
85
85
  const roughness = readRenderToken(style, '--inkra-stroke-amplitude', pencilRenderDefaults.strokeRoughness, 0, maxStrokeRoughness);
86
86
  const dpr = effectiveDpr(2);
87
+ const size = boundedCanvasSize(width, width, dpr);
87
88
  const key = JSON.stringify([PENCIL_RENDERER_VERSION, diameter, thickness, width, dpr, style.color, weight, roughness, seed]);
88
89
  if (key === lastPaint && canvasIsUsable(canvas))
89
90
  return;
@@ -94,7 +95,9 @@ export function ProgressRing({ diameter, thickness, percent, seed }) {
94
95
  }
95
96
  if (!image) {
96
97
  image = document.createElement('canvas');
97
- image.width = image.height = Math.ceil(width * dpr);
98
+ image.width = size.width;
99
+ image.height = size.height;
100
+ image.addEventListener('contextlost', () => ringCache.delete(key), { once: true });
98
101
  const context = image.getContext('2d');
99
102
  if (!context)
100
103
  throw new Error('Canvas unavailable');
@@ -116,6 +119,7 @@ export function ProgressRing({ diameter, thickness, percent, seed }) {
116
119
  lastPaint = key;
117
120
  }
118
121
  catch {
122
+ canvas.width = canvas.height = 0;
119
123
  delete canvas.dataset.ready;
120
124
  lastPaint = '';
121
125
  }
@@ -131,7 +135,7 @@ export function ProgressRing({ diameter, thickness, percent, seed }) {
131
135
  const scheme = matchMedia('(prefers-color-scheme: dark)');
132
136
  scheme.addEventListener('change', paint);
133
137
  window.addEventListener('resize', paint);
134
- const stopRecovery = observeCanvasRecovery(paint);
138
+ const stopRecovery = observeCanvasRecovery(() => { lastPaint = ''; paint(); });
135
139
  paint();
136
140
  return () => {
137
141
  observer.disconnect();
@@ -1,4 +1,4 @@
1
- import type { HTMLAttributes, ReactNode } from 'react';
1
+ import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
2
2
  import { type ProgressPartialSegment, type ProgressThreshold } from '../segmented-progress/SegmentedProgress.js';
3
3
  export type { ProgressPartialSegment, ProgressThreshold } from '../segmented-progress/SegmentedProgress.js';
4
4
  export type BadgeVariant = 'neutral' | 'accent' | 'success' | 'warning' | 'danger' | 'info';
@@ -22,6 +22,9 @@ export declare const Separator: import("react").ForwardRefExoticComponent<Separa
22
22
  export interface SkeletonProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
23
23
  seed?: string | number;
24
24
  radius?: number;
25
+ width?: CSSProperties['width'];
26
+ height?: CSSProperties['height'];
27
+ animation?: 'none' | 'pulse';
25
28
  }
26
29
  export declare const Skeleton: import("react").ForwardRefExoticComponent<SkeletonProps & import("react").RefAttributes<HTMLDivElement>>;
27
30
  export interface ProgressRenderState {
@@ -30,8 +30,9 @@ export const Separator = forwardRef(function Separator({ className, decorative =
30
30
  : { 'aria-orientation': orientation, role: 'separator' };
31
31
  return (_jsx("div", { ...props, ...semanticProps, className: ['inkra-separator', className].filter(Boolean).join(' '), "data-orientation": orientation, ref: ref, children: _jsx(PencilLine, { className: "inkra-separator__line", orientation: orientation, seed: seed ?? `inkra-separator-${orientation}`, weight: Number.isFinite(weight) ? Math.max(0.3, Math.min(2, weight)) : 0.9 }) }));
32
32
  });
33
- export const Skeleton = forwardRef(function Skeleton({ className, seed = 'inkra-skeleton', radius, ...props }, ref) {
34
- return (_jsx(PencilSurface, { ...props, "aria-hidden": "true", as: "div", className: ['inkra-skeleton', className].filter(Boolean).join(' '), ref: ref, seed: seed, radius: radius, variant: "quiet" }));
33
+ export const Skeleton = forwardRef(function Skeleton({ className, seed = 'inkra-skeleton', radius = 7, width, height, animation = 'none', style, ...props }, ref) {
34
+ const resolvedRadius = Number.isFinite(radius) ? Math.max(0, Math.min(radius, 8)) : 7;
35
+ return (_jsx(PencilSurface, { ...props, "aria-hidden": "true", as: "div", className: ['inkra-skeleton', className].filter(Boolean).join(' '), "data-animation": animation, style: { width, height, borderRadius: resolvedRadius, ...style }, ref: ref, seed: seed, radius: resolvedRadius, variant: "selection" }));
35
36
  });
36
37
  function normalizeMax(max) {
37
38
  return typeof max === 'number' && Number.isFinite(max) && max > 0 ? max : 100;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useInkraLocale } from '../i18n/index.js';
2
3
  import { DndContext, MouseSensor, TouchSensor, useDraggable, useSensor, useSensors, } from '@dnd-kit/core';
3
4
  import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
4
5
  import { Button } from '../Button.js';
@@ -43,7 +44,7 @@ export const DockablePanelWorkspace = forwardRef(function DockablePanelWorkspace
43
44
  return () => observer.disconnect();
44
45
  }, []);
45
46
  const activate = useCallback((id) => {
46
- setOrder(current => promoteDockablePanel(current, id));
47
+ setOrder(current => current[current.length - 1] === id ? current : promoteDockablePanel(current, id));
47
48
  if (activeId === undefined)
48
49
  setUncontrolledActiveId(id);
49
50
  if (resolvedActiveId !== id)
@@ -51,7 +52,7 @@ export const DockablePanelWorkspace = forwardRef(function DockablePanelWorkspace
51
52
  }, [activeId, onActiveChange, resolvedActiveId]);
52
53
  useEffect(() => {
53
54
  if (activeId != null)
54
- setOrder(current => promoteDockablePanel(current, activeId));
55
+ setOrder(current => current[current.length - 1] === activeId ? current : promoteDockablePanel(current, activeId));
55
56
  }, [activeId]);
56
57
  const register = useCallback((id) => {
57
58
  setOrder(current => current.includes(id) ? current : [...current, id]);
@@ -72,18 +73,18 @@ export const DockablePanelWorkspace = forwardRef(function DockablePanelWorkspace
72
73
  titleBarVisibility,
73
74
  width: size.width,
74
75
  }), [boundaryPadding, size.height, size.width, titleBarVisibility]);
76
+ const layers = useMemo(() => new Map(order.map((id, index) => [id, index])), [order]);
75
77
  const context = useMemo(() => ({
76
78
  activeId: resolvedActiveId,
77
79
  activate,
78
80
  bounds,
79
81
  commitDrag,
80
82
  layer(id) {
81
- const index = order.indexOf(id);
82
- return baseZIndex + (resolvedActiveId === id ? order.length : Math.max(0, index));
83
+ return baseZIndex + (resolvedActiveId === id ? order.length : (layers.get(id) ?? 0));
83
84
  },
84
85
  register,
85
86
  snapThreshold,
86
- }), [activate, baseZIndex, bounds, commitDrag, order, register, resolvedActiveId, snapThreshold]);
87
+ }), [activate, baseZIndex, bounds, commitDrag, layers, order.length, register, resolvedActiveId, snapThreshold]);
87
88
  function handleDragEnd(event) {
88
89
  const id = String(event.active.id);
89
90
  dragCallbacks.current.get(id)?.({ x: event.delta.x, y: event.delta.y });
@@ -96,13 +97,13 @@ const resetPaths = [
96
97
  { d: 'M4 12a8 8 0 1 0 2.34-5.66L4 8.68' },
97
98
  { d: 'M4 4v4.68h4.68' },
98
99
  ];
99
- const defaultLabels = {
100
- move: 'Move panel',
101
- collapse: 'Collapse panel',
102
- expand: 'Expand panel',
103
- reset: 'Restore panel position',
104
- keyboardInstructions: 'Use the arrow keys to move this panel. Hold Shift for larger steps. Press Home to restore its position.',
105
- };
100
+ const defaultLabels = (t) => ({
101
+ move: t('dockable.move'),
102
+ collapse: t('dockable.collapse'),
103
+ expand: t('dockable.expand'),
104
+ reset: t('dockable.reset'),
105
+ keyboardInstructions: t('dockable.instructions'),
106
+ });
106
107
  function renderTitle(title) {
107
108
  return typeof title === 'string' ? _jsx(PencilText, { children: title }) : title;
108
109
  }
@@ -110,6 +111,7 @@ function isInteractiveTarget(target) {
110
111
  return target instanceof Element && Boolean(target.closest('button, a, input, select, textarea, [contenteditable="true"], [data-dockable-panel-no-drag]'));
111
112
  }
112
113
  export const DockablePanel = forwardRef(function DockablePanel({ children, className, collapsed, collapsible = true, defaultCollapsed = false, defaultPosition = { x: 8, y: 8 }, headerActions, id, keyboardStep = 8, labels: labelOverrides, movable = true, onCollapsedChange, onFocusCapture, onPointerDown, onPositionChange, position, style, title, ...props }, forwardedRef) {
114
+ const { t } = useInkraLocale();
113
115
  const workspace = useContext(WorkspaceContext);
114
116
  if (!workspace)
115
117
  throw new Error('DockablePanel must be rendered inside DockablePanelWorkspace.');
@@ -125,7 +127,7 @@ export const DockablePanel = forwardRef(function DockablePanel({ children, class
125
127
  const resolvedCollapsed = collapsed ?? uncontrolledCollapsed;
126
128
  const positionRef = useRef(resolvedPosition);
127
129
  positionRef.current = resolvedPosition;
128
- const labels = { ...defaultLabels, ...labelOverrides };
130
+ const labels = { ...defaultLabels(t), ...labelOverrides };
129
131
  const { activate, bounds, commitDrag, layer, register, snapThreshold, } = workspace;
130
132
  const { attributes, isDragging, listeners, setActivatorNodeRef, setNodeRef, transform } = useDraggable({
131
133
  id,
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useInkraLocale } from '../i18n/index.js';
2
3
  import * as DialogPrimitive from '@radix-ui/react-dialog';
3
4
  import { forwardRef, useId } from 'react';
4
5
  import { __iconData as closeIcon } from 'lucide-react/dist/esm/icons/x.mjs';
@@ -40,7 +41,7 @@ export const DrawerContent = forwardRef(function DrawerContent({ children, class
40
41
  const themedProps = usePortalThemeProps({ ...props, style });
41
42
  const mergedStyle = { ...themedProps.style, '--inkra-drawer-size': drawerSize(size) };
42
43
  const edgeOrientation = direction === 'left' || direction === 'right' ? 'vertical' : 'horizontal';
43
- return (_jsx(DialogPrimitive.Content, { ...themedProps, className: classes('inkra-drawer__content', className), "data-direction": direction, ref: ref, style: mergedStyle, children: _jsxs(PencilSurface, { className: "inkra-drawer__surface", radius: 0, seed: seed ?? `inkra-drawer-${id}`, variant: "full", children: [_jsx(PencilLine, { className: "inkra-drawer__edge", orientation: edgeOrientation, seed: `drawer-edge-${id}` }), _jsx("div", { className: "inkra-drawer__layout", children: children })] }) }));
44
+ return (_jsxs(DialogPrimitive.Content, { ...themedProps, className: classes('inkra-drawer__content', className), "data-direction": direction, ref: ref, style: mergedStyle, children: [_jsx(PencilSurface, { "aria-hidden": "true", className: "inkra-drawer__focus-ring", radius: 0, seed: `drawer-focus-${id}`, variant: "outline" }), _jsxs(PencilSurface, { className: "inkra-drawer__surface", radius: 0, seed: seed ?? `inkra-drawer-${id}`, variant: "full", children: [_jsx(PencilLine, { className: "inkra-drawer__edge", orientation: edgeOrientation, seed: `drawer-edge-${id}` }), _jsx("div", { className: "inkra-drawer__layout", children: children })] })] }));
44
45
  });
45
46
  export const DrawerTitle = forwardRef(function DrawerTitle({ children, className, seed, ...props }, ref) {
46
47
  const id = useId();
@@ -53,7 +54,9 @@ export const DrawerDescription = forwardRef(function DrawerDescription({ childre
53
54
  export const DrawerClose = forwardRef(function DrawerClose(props, ref) {
54
55
  return _jsx(DialogPrimitive.Close, { ...props, ref: ref });
55
56
  });
56
- export const DrawerCloseButton = forwardRef(function DrawerCloseButton({ className, label = 'Close', onFocus, ...props }, ref) {
57
+ export const DrawerCloseButton = forwardRef(function DrawerCloseButton(inkraProps, ref) {
58
+ const { t } = useInkraLocale();
59
+ const { className, label = t('common.close'), onFocus, ...props } = inkraProps;
57
60
  return _jsx(Tooltip.Provider, { children: _jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { asChild: true, children: _jsx(DialogPrimitive.Close, { asChild: true, children: _jsx(Button, { ...props, "aria-label": label, className: classes('inkra-drawer__close-button', className), ref: ref, size: "icon", type: "button", variant: "outline", onFocus: event => {
58
61
  onFocus?.(event);
59
62
  const dialog = event.currentTarget.closest('[role="dialog"]');
@@ -61,9 +64,12 @@ export const DrawerCloseButton = forwardRef(function DrawerCloseButton({ classNa
61
64
  event.preventDefault();
62
65
  }, children: _jsx(PencilIcon, { decorative: true, paths: closePaths, size: 30, sourceSize: 24 }) }) }) }), _jsx(Tooltip.Portal, { children: _jsx(Tooltip.Content, { align: "end", side: "bottom", children: label }) })] }) });
63
66
  });
64
- export const DrawerHeader = forwardRef(function DrawerHeader({ action, children, className, closeLabel = 'Close', divided = true, showClose = true, ...props }, ref) {
67
+ export const DrawerHeader = forwardRef(function DrawerHeader(inkraProps, ref) {
68
+ const { t } = useInkraLocale();
69
+ const { action, children, className, closeLabel = t('common.close'), divided = true, showClose = true, ...props } = inkraProps;
65
70
  const id = useId();
66
- return _jsxs("div", { ...props, className: classes('inkra-drawer__header', className), "data-divided": divided || undefined, ref: ref, children: [_jsx("div", { className: "inkra-drawer__heading", children: children }), (action || showClose) ? _jsxs("div", { className: "inkra-drawer__header-actions", children: [action, showClose ? _jsx(DrawerCloseButton, { label: closeLabel }) : null] }) : null, divided ? _jsx(PencilLine, { className: "inkra-drawer__divider", seed: `drawer-header-${id}` }) : null] });
71
+ const hasActions = Boolean(action || showClose);
72
+ return _jsxs("div", { ...props, className: classes('inkra-drawer__header', className), "data-actions": hasActions || undefined, "data-custom-action": action ? true : undefined, "data-divided": divided || undefined, ref: ref, children: [_jsx("div", { className: "inkra-drawer__heading", children: children }), hasActions ? _jsxs("div", { className: "inkra-drawer__header-actions", children: [action, showClose ? _jsx(DrawerCloseButton, { label: closeLabel }) : null] }) : null, divided ? _jsx(PencilLine, { className: "inkra-drawer__divider", seed: `drawer-header-${id}` }) : null] });
67
73
  });
68
74
  export const DrawerBody = forwardRef(function DrawerBody({ className, ...props }, ref) {
69
75
  return _jsx("div", { ...props, className: classes('inkra-drawer__body', className), ref: ref });
@@ -72,7 +78,7 @@ export const DrawerFooter = forwardRef(function DrawerFooter({ align = 'end', ch
72
78
  const id = useId();
73
79
  return _jsxs("div", { ...props, className: classes('inkra-drawer__footer', className), "data-align": align, "data-divided": divided || undefined, ref: ref, children: [divided ? _jsx(PencilLine, { className: "inkra-drawer__divider", seed: `drawer-footer-${id}` }) : null, children] });
74
80
  });
75
- export const Drawer = Object.freeze({
81
+ export const Drawer = /* @__PURE__ */ Object.freeze({
76
82
  Root: DrawerRoot,
77
83
  Trigger: DrawerTrigger,
78
84
  Portal: DrawerPortal,
@@ -1,7 +1,23 @@
1
- import type { HTMLAttributes, KeyboardEvent, ReactNode } from 'react';
1
+ import type { FocusEvent, HTMLAttributes, KeyboardEvent, ReactNode } from 'react';
2
+ import type { SearchableSelectOption } from '../select/index.js';
2
3
  import type { InputProps } from '../form/Input.js';
3
4
  import type { TextareaProps } from '../form/Textarea.js';
4
5
  export type EditableBlurBehavior = 'ignore' | 'save' | 'cancel';
6
+ export type EditableEditorType = 'input' | 'textarea' | 'select';
7
+ export interface EditableSelectProps {
8
+ items: readonly SearchableSelectOption[];
9
+ mode?: 'dropdown' | 'searchable' | 'stepper';
10
+ size?: 'sm' | 'md' | 'lg';
11
+ dir?: 'ltr' | 'rtl';
12
+ searchPlaceholder?: string;
13
+ emptyContent?: ReactNode;
14
+ clearable?: boolean;
15
+ clearLabel?: string;
16
+ openLabel?: string;
17
+ previousLabel?: string;
18
+ nextLabel?: string;
19
+ loop?: boolean;
20
+ }
5
21
  export type EditableSaveResult = boolean | string | void | null | undefined;
6
22
  export type EditableSaveHandler = (value: string, previousValue: string) => EditableSaveResult | Promise<EditableSaveResult>;
7
23
  export type EditableValidateHandler = (value: string, previousValue: string) => EditableSaveResult | Promise<EditableSaveResult>;
@@ -9,12 +25,16 @@ export interface EditableEditorProps {
9
25
  value: string;
10
26
  onValueChange: (value: string) => void;
11
27
  onKeyDown: (event: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
12
- onBlur: () => void;
28
+ onBlur: (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
13
29
  disabled?: boolean;
30
+ readOnly?: boolean;
14
31
  invalid?: boolean;
15
32
  error?: string;
16
33
  autoFocus?: boolean;
17
34
  'aria-label'?: string;
35
+ 'aria-describedby'?: string;
36
+ 'aria-errormessage'?: string;
37
+ 'aria-invalid'?: boolean;
18
38
  }
19
39
  export type EditableEditor = ReactNode | ((props: EditableEditorProps) => ReactNode);
20
40
  export interface EditableProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onChange'> {
@@ -29,6 +49,8 @@ export interface EditableProps extends Omit<HTMLAttributes<HTMLDivElement>, 'chi
29
49
  onCancel?: (value: string) => void;
30
50
  display?: ReactNode | ((value: string) => ReactNode);
31
51
  editor?: EditableEditor;
52
+ editorType?: EditableEditorType;
53
+ selectProps?: EditableSelectProps;
32
54
  multiline?: boolean;
33
55
  inputProps?: Omit<InputProps, 'value' | 'defaultValue' | 'onValueChange' | 'onChange'>;
34
56
  textareaProps?: Omit<TextareaProps, 'value' | 'defaultValue' | 'onChange'>;