@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
package/README.md CHANGED
@@ -48,6 +48,104 @@ export function Actions() {
48
48
 
49
49
  To use only the design tokens, import `@inkra/react/tokens.css` instead.
50
50
 
51
+ ## Theme Provider
52
+
53
+ ```tsx
54
+ import '@inkra/react/styles.css';
55
+ import { ThemeProvider } from '@inkra/react/theme';
56
+ import type { InkraThemeTokens } from '@inkra/react/theme';
57
+
58
+ const tokens = {
59
+ '--inkra-accent': '#4b96aa',
60
+ '--inkra-icon-stroke-width': '1.5px',
61
+ } satisfies InkraThemeTokens;
62
+
63
+ <ThemeProvider theme="auto" tokens={tokens}>
64
+ <App />
65
+ </ThemeProvider>
66
+ ```
67
+
68
+ `ThemeProvider` is also exported from the package root. It renders a local `div`
69
+ scope, accepts its normal HTML attributes and forwards its ref. It never changes
70
+ the document theme or stores a preference. `theme` supports `light`, `dark`, and
71
+ `auto`; omit it to inherit the surrounding CSS theme. Auto uses native CSS media
72
+ queries and works with SSR without resolving the system preference in JavaScript.
73
+
74
+ `tokens` accepts partial `--inkra-*` variables for color, fonts, geometry and
75
+ pencil rendering. Supply units for lengths (`"1.5px"`); unitless numbers work for
76
+ intensity and similar values. An inner scope without `theme` inherits outer
77
+ tokens; an explicit mode establishes that mode's built-in variables before local
78
+ overrides. `style` takes precedence over `tokens`. User token overrides are
79
+ suspended while forced colors are active so the system palette can take over.
80
+
81
+ Custom colors do not automatically adapt to light/dark modes. Supply appropriate
82
+ variables for each mode, or use CSS media queries. Existing component portals
83
+ inherit the trigger scope and track its live changes. The scope has no default
84
+ background or spacing; use a surface or your own layout when needed. It composes
85
+ with `InkraProvider` for language configuration without changing input state.
86
+
87
+ ## Internationalization
88
+
89
+ Wrap your application in `InkraProvider` to localize component-owned copy and
90
+ accessible labels. Built-in dictionaries are `en`, `zh-CN`, `ja`, `ko`, and `fr`.
91
+ All five share the same typed keys. Regional tags such as `en-GB`, `ja-JP`,
92
+ `ko-KR`, `fr-CA` and `zh-Hans-CN` select the matching language while keeping
93
+ regional date/number formatting. No provider preserves the English defaults.
94
+
95
+ ```tsx
96
+ import { InkraProvider, Input, Pagination } from '@inkra/react';
97
+ import type { InkraDictionary } from '@inkra/react/i18n';
98
+
99
+ const messages = {
100
+ 'input.clear': 'Clear note',
101
+ 'pagination.page': 'Open page {page}',
102
+ } satisfies InkraDictionary;
103
+
104
+ <InkraProvider locale="fr-CA" messages={messages}>
105
+ <Input clearable defaultValue="My note" aria-label="Note" />
106
+ <Pagination totalPages={5} />
107
+ </InkraProvider>;
108
+ ```
109
+
110
+ `messages` is a partial dictionary; developers can import JSON or adapt their
111
+ existing i18n resource into these flat keys. Inspect `inkraLocales.en` for the
112
+ complete key list and source templates, or copy another built-in dictionary.
113
+ The provider, dictionaries, types, `useInkraLocale` and
114
+ `createInkraTranslator` are also exported from `@inkra/react/i18n`.
115
+
116
+ - Precedence: explicit component labels/props, inner provider overrides, inherited
117
+ overrides, then the built-in dictionary. Unknown languages use English copy
118
+ while valid `Intl` locale formatting is preserved. Invalid locale strings use `en-US`.
119
+ - Nested providers merge by key, even when the inner locale changes. Pass the
120
+ desired override dictionary on language changes; inherited overrides are intentional.
121
+ - `undefined` entries do not override; empty strings do. Avoid empty accessible
122
+ labels on interactive controls. Use `satisfies InkraDictionary` to catch key typos.
123
+ - Preserve template placeholders such as `{page}`, `{count}` and `{label}`.
124
+ Interpolation is a single literal-text pass, not HTML, ICU, or executable code.
125
+ Missing placeholder values are left visible rather than silently removed.
126
+ - Explicit component `locale` props take priority over the provider. Calendar
127
+ also retains its DayPicker locale/labels/formatters API. NumberTicker's
128
+ explicit compact-unit policies remain independent of regional number formatting.
129
+ - Language and dictionary updates do not remount children or clear form drafts.
130
+ The provider adds no HTML wrapper and does not mutate the document language.
131
+ Set `lang` on your page or locale-scoped container; set `dir` separately if needed.
132
+ - Application labels, option content, errors supplied by your application, and
133
+ stored/submitted values are not automatically translated. InkraProvider also
134
+ passes its locale to React Aria's internal announcements; third-party/native
135
+ messages are not keys in the Inkra dictionary.
136
+ - Use identical locale/dictionary data for SSR and hydration. Messages may change
137
+ after hydration; the provider does not read browser language or local storage.
138
+
139
+ ```tsx
140
+ import { InkraProvider, inkraLocales } from '@inkra/react/i18n';
141
+
142
+ const customLanguage = { ...inkraLocales.en, 'input.clear': 'Effacer la note' };
143
+
144
+ <InkraProvider locale="fr" messages={customLanguage}>
145
+ {children}
146
+ </InkraProvider>;
147
+ ```
148
+
51
149
  ## Button
52
150
 
53
151
  `Button` forwards its ref and native button attributes. Its default `type` is `button`, so placing it inside a form will not submit unexpectedly.
@@ -348,7 +446,18 @@ All examples use public sample data.
348
446
 
349
447
  `PencilText` keeps its string in the DOM for selection, copy, search, and assistive technology. Browser enhancement paints the same glyph geometry with the traced C lettering preset; unavailable canvas or font loading leaves the readable DOM fallback visible.
350
448
 
351
- `PencilIcon` accepts familiar SVG path data and deposits pigment along sampled geometry. See `docs/pencil-contract.md` in the workspace for the current alpha contract.
449
+ `PencilIcon` accepts familiar SVG path data and deposits pigment along sampled geometry.
450
+ Use `size` for the displayed canvas size (default 40px), `strokeWidth` (0.3-3,
451
+ default 1.5), `intensity` (0-1, default 1), `breaks` (0-1, default 0.18),
452
+ `pressureVariation` (0-1, default 0.82), and `breakCount` (integer 0-12, default 1).
453
+ The gap count is a maximum: short displayed paths use fewer or no gaps, while
454
+ larger icons can use more up to that limit. Device pixel ratio does not change
455
+ the count. Zero breaks or zero breakCount removes intentional gaps; zero pressure
456
+ variation keeps constant pressure without removing the grain or endpoint taper.
457
+ The matching inherited tokens are `--inkra-icon-stroke-width`,
458
+ `--inkra-icon-intensity`, `--inkra-icon-breaks`, `--inkra-icon-pressure-variation`
459
+ and `--inkra-icon-break-count`. Finite explicit props override those tokens.
460
+ See `docs/pencil-contract.md` in the workspace for the material contract.
352
461
 
353
462
  `PencilLine` is an aria-hidden canvas decoration using the same pressure, grain, breaks, and repeated-pass renderer as surfaces and icons. It defaults to horizontal and supports a native vertical stroke without rotating a horizontal bitmap:
354
463
 
@@ -367,8 +476,9 @@ text within that scope without changing layout, selectable text, or labels.
367
476
 
368
477
  `PencilLine` reads `--inkra-stroke-width` (0.3-3) and
369
478
  `--inkra-stroke-amplitude` (0-4) when `weight` or `roughness` is omitted.
370
- Explicit props take precedence. These tokens also scale surface outlines and icon
371
- strokes relative to their original presets, including button/card/input borders.
479
+ Explicit props take precedence. These tokens also scale surface outlines,
480
+ including button/card/input borders. Icon weight uses its independent token;
481
+ the general amplitude token still controls icon roughness.
372
482
  The default values are 2 and 1.5. They do not change layout or text lettering.
373
483
 
374
484
  Surface fills read `--inkra-wash-strength` (0-1, default 0.8),
package/dist/Button.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { createContext, forwardRef, useContext, useId } from 'react';
2
+ import { createContext, forwardRef, useContext, useId, useMemo } from 'react';
3
3
  import { PencilSurface } from './PencilSurface.js';
4
4
  import { PencilText } from './PencilText.js';
5
5
  import { PencilLine } from './PencilLine.js';
@@ -85,5 +85,6 @@ export const ButtonLink = forwardRef(function ButtonLink({ children, 'aria-label
85
85
  export const ButtonGroup = forwardRef(function ButtonGroup({ children, className, orientation = 'horizontal', size, variant, disabled = false, fullWidth = false, equalWidth = false, ...props }, ref) {
86
86
  const classes = ['inkra-button-group', className].filter(Boolean).join(' ');
87
87
  const id = useId();
88
- return (_jsx(ButtonGroupContext.Provider, { value: { orientation, size, variant, disabled }, children: _jsx(PencilSurface, { ...props, className: classes, "data-orientation": orientation, "data-full-width": fullWidth || undefined, "data-equal-width": equalWidth || undefined, "aria-disabled": disabled || undefined, radius: 8, ref: ref, role: "group", seed: `inkra-button-group-${id}`, variant: "outline", children: children }) }));
88
+ const context = useMemo(() => ({ orientation, size, variant, disabled }), [orientation, size, variant, disabled]);
89
+ return (_jsx(ButtonGroupContext.Provider, { value: context, children: _jsx(PencilSurface, { ...props, className: classes, "data-orientation": orientation, "data-full-width": fullWidth || undefined, "data-equal-width": equalWidth || undefined, "aria-disabled": disabled || undefined, radius: 8, ref: ref, role: "group", seed: `inkra-button-group-${id}`, variant: "outline", children: children }) }));
89
90
  });
@@ -17,6 +17,16 @@ export interface PencilIconProps extends Omit<CanvasHTMLAttributes<HTMLCanvasEle
17
17
  size?: number;
18
18
  sourceSize?: number;
19
19
  active?: boolean;
20
+ /** Overrides icon/theme stroke weight. Clamped to 0.3-3. */
21
+ strokeWidth?: number;
22
+ /** Pigment opacity, clamped to 0-1. Defaults to the icon intensity token or 1. */
23
+ intensity?: number;
24
+ /** Stroke gap amount, clamped to 0-1. Zero disables gaps, not pigment grain. */
25
+ breaks?: number;
26
+ /** Maximum gaps per continuous path, limited by displayed length; floored/clamped to 0-12, default 1. */
27
+ breakCount?: number;
28
+ /** Pressure-driven stroke width variation, clamped to 0-1. Defaults to 0.82. */
29
+ pressureVariation?: number;
20
30
  decorative?: boolean;
21
31
  }
22
32
  export declare const PencilIcon: import("react").ForwardRefExoticComponent<PencilIconProps & import("react").RefAttributes<HTMLCanvasElement>>;
@@ -1,32 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef, useEffect, useImperativeHandle, 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 { pencilStroke } from './internal/stroke.js';
7
7
  import { maxStrokeRoughness, pencilRenderDefaults, readRenderToken } from './internal/renderTokens.js';
8
- const geometryCache = new BoundedCache(256, 180000);
8
+ import { cachedPathParts } from './internal/iconGeometry.js';
9
9
  const imageCache = new BoundedCache(128, 24 * 1024 * 1024);
10
- function cachedPathParts(data) {
11
- const key = `${PENCIL_RENDERER_VERSION}:${data}`;
12
- const cached = geometryCache.get(key);
13
- if (cached)
14
- return cached;
15
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
16
- path.setAttribute('d', data);
17
- const length = path.getTotalLength();
18
- const count = Math.max(1, Math.ceil(length / 0.25));
19
- const parts = [[]];
20
- for (let index = 0; index <= count; index += 1) {
21
- const point = path.getPointAtLength(length * index / count);
22
- const activePart = parts[parts.length - 1];
23
- const previous = activePart[activePart.length - 1];
24
- if (previous && Math.hypot(point.x - previous[0], point.y - previous[1]) > 0.5)
25
- parts.push([]);
26
- parts[parts.length - 1].push([point.x, point.y]);
27
- }
28
- return geometryCache.set(key, parts, parts.reduce((total, part) => total + part.length, 0));
29
- }
30
10
  function drawFallbackPath(context, data, color, mode, weightScale = 1) {
31
11
  try {
32
12
  const path = new Path2D(data);
@@ -47,9 +27,18 @@ function renderIcon(paths, options) {
47
27
  const { seed, size, sourceSize, active, color, dpr } = options;
48
28
  const weightScale = options.weightScale ?? 1;
49
29
  const roughnessScale = options.roughnessScale ?? 1;
30
+ const breaks = options.breaks ?? pencilRenderDefaults.iconBreaks;
31
+ const breakCount = options.breakCount ?? pencilRenderDefaults.iconBreakCount;
32
+ const pressureVariation = options.pressureVariation ?? pencilRenderDefaults.iconPressureVariation;
33
+ const variationScale = pressureVariation / pencilRenderDefaults.iconPressureVariation;
34
+ const pixels = Math.round(size * dpr);
35
+ boundedCanvasSize(pixels, pixels, 1);
36
+ if (!Number.isFinite(sourceSize) || sourceSize <= 0 || sourceSize > 4096 || paths.length > 1024) {
37
+ throw new RangeError('Pencil icon exceeds the bounded geometry budget');
38
+ }
50
39
  const canvas = document.createElement('canvas');
51
- canvas.width = Math.round(size * dpr);
52
- canvas.height = Math.round(size * dpr);
40
+ canvas.width = pixels;
41
+ canvas.height = pixels;
53
42
  const context = canvas.getContext('2d');
54
43
  if (!context)
55
44
  return canvas;
@@ -57,7 +46,10 @@ function renderIcon(paths, options) {
57
46
  context.scale(dpr * coordinateScale, dpr * coordinateScale);
58
47
  const offset = (40 - sourceSize) / 2;
59
48
  context.translate(offset, offset);
49
+ let sampledPoints = 0;
60
50
  paths.forEach((path, index) => {
51
+ if (path.d.length > 65536)
52
+ return;
61
53
  const mode = path.mode ?? 'stroke';
62
54
  const pathSeed = seed + index * 137;
63
55
  if (mode === 'wash') {
@@ -66,7 +58,7 @@ function renderIcon(paths, options) {
66
58
  context.clip(new Path2D(path.d));
67
59
  context.globalAlpha = active ? 0.76 : 0.34;
68
60
  for (let y = sourceSize * 0.18; y < sourceSize; y += 1.6) {
69
- pencilStroke(context, [[sourceSize * 0.15, y + 3], [sourceSize * 0.85, y - 3]], color, 1.1, pathSeed + Math.round(y * 17), 0.25, 2, false, { variation: 0.85, breaks: 0.25, grain: 0.7, gain: 2.2, step: 0.35 });
61
+ pencilStroke(context, [[sourceSize * 0.15, y + 3], [sourceSize * 0.85, y - 3]], color, 1.1 * weightScale / (pencilRenderDefaults.strokeWidth / 1.2), pathSeed + Math.round(y * 17), 0.25, 2, false, { variation: Math.min(1, 0.85 * variationScale), fitPressure: true, breaks, breakCount, breakScale: coordinateScale, continuousBreaks: true, grain: 0.7, gain: 2.2, step: 0.35 });
70
62
  }
71
63
  context.restore();
72
64
  }
@@ -76,11 +68,24 @@ function renderIcon(paths, options) {
76
68
  return;
77
69
  }
78
70
  try {
71
+ if (sampledPoints >= 65536) {
72
+ drawFallbackPath(context, path.d, color, mode, weightScale);
73
+ return;
74
+ }
79
75
  const parts = cachedPathParts(path.d);
76
+ sampledPoints += parts.reduce((total, part) => total + part.length, 0);
77
+ if (sampledPoints > 65536) {
78
+ drawFallbackPath(context, path.d, color, mode, weightScale);
79
+ return;
80
+ }
80
81
  for (const points of parts) {
81
82
  pencilStroke(context, points, color, (mode === 'scuff' ? 0.55 : 1.65) * weightScale, pathSeed, 0.3 * roughnessScale, mode === 'scuff' ? 1 : 3, false, {
82
- variation: 0.82,
83
- breaks: 0.18,
83
+ variation: pressureVariation,
84
+ fitPressure: true,
85
+ breaks,
86
+ breakCount,
87
+ breakScale: coordinateScale,
88
+ continuousBreaks: true,
84
89
  grain: 0.68,
85
90
  gain: mode === 'scuff' ? 1.5 : 3,
86
91
  step: 0.35,
@@ -93,7 +98,7 @@ function renderIcon(paths, options) {
93
98
  }
94
99
  });
95
100
  if (active) {
96
- pencilStroke(context, [[sourceSize * 0.2, sourceSize - 2], [sourceSize * 0.5, sourceSize - 1.5], [sourceSize * 0.8, sourceSize - 2.2]], color, 1.1 * weightScale, seed + 977, 0.25 * roughnessScale, 3, false, { variation: 0.8, breaks: 0.25, grain: 0.7, gain: 2.8, step: 0.35 });
101
+ pencilStroke(context, [[sourceSize * 0.2, sourceSize - 2], [sourceSize * 0.5, sourceSize - 1.5], [sourceSize * 0.8, sourceSize - 2.2]], color, 1.1 * weightScale, seed + 977, 0.25 * roughnessScale, 3, false, { variation: 0.8 * variationScale, fitPressure: true, breaks, breakCount, breakScale: coordinateScale, continuousBreaks: true, grain: 0.7, gain: 2.8, step: 0.35 });
97
102
  }
98
103
  if (options.badge?.paths.length) {
99
104
  const badge = options.badge;
@@ -106,7 +111,7 @@ function renderIcon(paths, options) {
106
111
  const y = position.startsWith('top') ? 0 : 40 - extent;
107
112
  const badgeImage = renderIcon(badge.paths, {
108
113
  seed: seed + 1543, size: size * scale, sourceSize: badgeSourceSize, active: false, color, dpr,
109
- weightScale: weightScale / scale, roughnessScale,
114
+ weightScale: weightScale / scale, roughnessScale, breaks, breakCount, pressureVariation,
110
115
  });
111
116
  const mask = document.createElement('canvas');
112
117
  mask.width = badgeImage.width;
@@ -135,29 +140,47 @@ function renderIcon(paths, options) {
135
140
  }
136
141
  return canvas;
137
142
  }
138
- export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge, className = '', decorative = true, paths, seed, size = 40, sourceSize = 32, ...props }, forwardedRef) {
143
+ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge, className = '', decorative = true, paths, seed, size = 40, sourceSize = 32, strokeWidth, intensity, breaks, breakCount, pressureVariation, ...props }, forwardedRef) {
139
144
  const canvasRef = useRef(null);
140
145
  const lastPaintRef = useRef('');
141
146
  useImperativeHandle(forwardedRef, () => canvasRef.current);
142
- const geometryKey = useMemo(() => JSON.stringify(paths), [paths]);
143
- const badgeKey = useMemo(() => badge ? JSON.stringify(badge) : '', [badge]);
147
+ // Compare values, not caller object identity. Serializing each render also
148
+ // detects in-place edits; the effect receives a stable snapshot of that value.
149
+ const geometryKey = JSON.stringify(paths);
150
+ const badgeKey = badge ? JSON.stringify(badge) : '';
151
+ const stablePaths = useMemo(() => JSON.parse(geometryKey), [geometryKey]);
152
+ const stableBadge = useMemo(() => badgeKey ? JSON.parse(badgeKey) : undefined, [badgeKey]);
144
153
  const resolvedSeed = seedToNumber(seed ?? geometryKey);
145
154
  useEffect(() => {
146
155
  const canvas = canvasRef.current;
147
156
  if (!canvas)
148
157
  return undefined;
149
158
  let disposed = false;
150
- const paint = () => {
159
+ const paintIcon = () => {
151
160
  if (disposed)
152
161
  return;
153
162
  const dpr = effectiveDpr(3);
154
163
  const computed = getComputedStyle(canvas);
155
164
  const color = computed.color;
156
165
  const rtl = computed.direction === 'rtl';
157
- const weightScale = readRenderToken(computed, '--inkra-stroke-width', pencilRenderDefaults.strokeWidth, 0.3, 3) / 1.2;
166
+ const iconWeight = readRenderToken(computed, '--inkra-icon-stroke-width', pencilRenderDefaults.iconStrokeWidth, 0.3, 3);
167
+ const weightScale = (typeof strokeWidth === 'number' && Number.isFinite(strokeWidth) ? Math.min(3, Math.max(0.3, strokeWidth)) : iconWeight) / 1.2;
168
+ const pigmentOpacity = typeof intensity === 'number' && Number.isFinite(intensity)
169
+ ? Math.min(1, Math.max(0, intensity))
170
+ : readRenderToken(computed, '--inkra-icon-intensity', 1, 0, 1);
158
171
  const roughnessScale = readRenderToken(computed, '--inkra-stroke-amplitude', pencilRenderDefaults.strokeRoughness, 0, maxStrokeRoughness) / 0.6;
172
+ const strokeBreaks = typeof breaks === 'number' && Number.isFinite(breaks)
173
+ ? Math.min(1, Math.max(0, breaks))
174
+ : readRenderToken(computed, '--inkra-icon-breaks', pencilRenderDefaults.iconBreaks, 0, 1);
175
+ const strokeVariation = typeof pressureVariation === 'number' && Number.isFinite(pressureVariation)
176
+ ? Math.min(1, Math.max(0, pressureVariation))
177
+ : readRenderToken(computed, '--inkra-icon-pressure-variation', pencilRenderDefaults.iconPressureVariation, 0, 1);
178
+ const strokeBreakCount = Math.floor(typeof breakCount === 'number' && Number.isFinite(breakCount)
179
+ ? Math.min(12, Math.max(0, breakCount))
180
+ : readRenderToken(computed, '--inkra-icon-break-count', pencilRenderDefaults.iconBreakCount, 0, 12));
159
181
  const key = JSON.stringify([
160
182
  PENCIL_RENDERER_VERSION,
183
+ 'icon-path-pressure-gaps-v4',
161
184
  geometryKey,
162
185
  badgeKey,
163
186
  rtl,
@@ -167,17 +190,19 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
167
190
  active,
168
191
  color,
169
192
  dpr,
170
- weightScale, roughnessScale,
193
+ weightScale, roughnessScale, strokeBreaks, strokeBreakCount, strokeVariation,
171
194
  ]);
172
- if (key === lastPaintRef.current && canvasIsUsable(canvas))
195
+ const paintKey = `${key}:${pigmentOpacity}`;
196
+ if (paintKey === lastPaintRef.current && canvasIsUsable(canvas))
173
197
  return;
198
+ delete canvas.dataset.pencilIconReady;
174
199
  let image = imageCache.get(key);
175
200
  if (image && !canvasIsUsable(image)) {
176
201
  imageCache.delete(key);
177
202
  image = undefined;
178
203
  }
179
204
  if (!image) {
180
- image = renderIcon(paths, { seed: resolvedSeed, size, sourceSize, active, color, dpr, badge, rtl, weightScale, roughnessScale });
205
+ image = renderIcon(stablePaths, { seed: resolvedSeed, size, sourceSize, active, color, dpr, badge: stableBadge, rtl, weightScale, roughnessScale, breaks: strokeBreaks, breakCount: strokeBreakCount, pressureVariation: strokeVariation });
181
206
  imageCache.set(key, image, canvasByteCost(image));
182
207
  }
183
208
  canvas.width = image.width;
@@ -185,9 +210,27 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
185
210
  const context = canvas.getContext('2d');
186
211
  if (!context || context.isContextLost?.())
187
212
  return;
213
+ // Composite once so overlapping strokes and badges share the same intensity.
214
+ context.save();
215
+ context.globalAlpha = pigmentOpacity;
188
216
  context.drawImage(image, 0, 0);
189
- lastPaintRef.current = key;
217
+ context.restore();
218
+ lastPaintRef.current = paintKey;
219
+ canvas.dataset.pencilIconReady = 'true';
220
+ };
221
+ const paint = () => {
222
+ try {
223
+ paintIcon();
224
+ }
225
+ catch {
226
+ // Invalid/oversized decoration must not throw from a React effect.
227
+ canvas.width = canvas.height = 0;
228
+ lastPaintRef.current = '';
229
+ delete canvas.dataset.pencilIconReady;
230
+ }
190
231
  };
232
+ const onContextLost = () => { delete canvas.dataset.pencilIconReady; lastPaintRef.current = ''; };
233
+ canvas.addEventListener('contextlost', onContextLost);
191
234
  const inheritedColor = new MutationObserver(paint);
192
235
  for (let element = canvas; element; element = element.parentElement) {
193
236
  inheritedColor.observe(element, { attributes: true, attributeFilter: ['style', 'class', 'dir', 'data-theme', 'data-inkra-theme'] });
@@ -198,11 +241,12 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
198
241
  paint();
199
242
  return () => {
200
243
  disposed = true;
244
+ canvas.removeEventListener('contextlost', onContextLost);
201
245
  stopRecovery();
202
246
  inheritedColor.disconnect();
203
247
  colorScheme.removeEventListener('change', paint);
204
248
  };
205
- }, [active, badge, badgeKey, geometryKey, paths, resolvedSeed, size, sourceSize]);
249
+ }, [active, stableBadge, badgeKey, geometryKey, stablePaths, resolvedSeed, size, sourceSize, strokeWidth, intensity, breaks, breakCount, pressureVariation]);
206
250
  const accessibilityProps = decorative
207
251
  ? { 'aria-hidden': true }
208
252
  : { role: props.role ?? 'img' };
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef, useEffect, useImperativeHandle, 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 { createPencilLinePoints, pencilLineHasDrawableSize } from './internal/line.js';
@@ -37,6 +37,14 @@ export const PencilLine = forwardRef(function PencilLine({ className = '', orien
37
37
  if (!pencilLineHasDrawableSize(width, height, orientation))
38
38
  return;
39
39
  const dpr = effectiveDpr(2);
40
+ try {
41
+ boundedCanvasSize(width, height, dpr);
42
+ }
43
+ catch {
44
+ canvas.width = canvas.height = 0;
45
+ lastPaint = '';
46
+ return;
47
+ }
40
48
  const style = getComputedStyle(canvas);
41
49
  const color = style.color;
42
50
  const resolvedWeight = weight ?? readRenderToken(style, '--inkra-stroke-width', pencilRenderDefaults.strokeWidth, 0.3, 3);
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
3
3
  import { useBrowserLayoutEffect } from './internal/useBrowserLayoutEffect.js';
4
- import { effectiveDpr } from './internal/cache.js';
4
+ import { boundedCanvasSize, effectiveDpr } from './internal/cache.js';
5
5
  import { observeCanvasRecovery } from './internal/recovery.js';
6
6
  import { seedToNumber } from './internal/random.js';
7
7
  import { pencilLettering, renderPencilText } from './internal/textRenderer.js';
@@ -108,8 +108,9 @@ function paintText(element, source, canvas, text, seed, glyphSeed) {
108
108
  const dpr = effectiveDpr(2);
109
109
  if (width * height * dpr ** 2 > 2 * 1024 * 1024 || !context || context.isContextLost?.())
110
110
  return false;
111
- canvas.width = Math.ceil(width * dpr);
112
- canvas.height = Math.ceil(height * dpr);
111
+ const pixels = boundedCanvasSize(width, height, dpr);
112
+ canvas.width = pixels.width;
113
+ canvas.height = pixels.height;
113
114
  Object.assign(canvas.style, {
114
115
  left: `-${padding}px`,
115
116
  top: `-${padding}px`,
@@ -119,7 +120,7 @@ function paintText(element, source, canvas, text, seed, glyphSeed) {
119
120
  height: `${height}px`,
120
121
  });
121
122
  context.scale(dpr, dpr);
122
- context.font = `400 ${size}px ${style.fontFamily}`;
123
+ context.font = `${style.fontStyle} ${style.fontWeight} ${size}px ${style.fontFamily}`;
123
124
  context.fontKerning = 'none';
124
125
  const ascent = context.measureText(text).fontBoundingBoxAscent ?? size;
125
126
  const lines = measureLocalTextLines(element, source, text, localWidth, localHeight);
@@ -133,6 +134,8 @@ function paintText(element, source, canvas, text, seed, glyphSeed) {
133
134
  glyphSeed,
134
135
  color: style.color,
135
136
  family: style.fontFamily,
137
+ weight: style.fontWeight,
138
+ style: style.fontStyle,
136
139
  dpr,
137
140
  settings: {
138
141
  variation: readRenderToken(style, '--inkra-letter-variation', pencilLettering.variation, 0, 1),
@@ -176,7 +179,7 @@ const PencilTextImplementation = forwardRef(function PencilText({ as: Element =
176
179
  const context = canvas.getContext('2d');
177
180
  if (!context || context.isContextLost?.())
178
181
  return;
179
- context.font = `400 ${style.fontSize} ${style.fontFamily}`;
182
+ context.font = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
180
183
  context.fontKerning = 'none';
181
184
  const measure = (value) => context.measureText(value).width;
182
185
  if (measure(children) <= width) {
@@ -248,8 +251,8 @@ const PencilTextImplementation = forwardRef(function PencilText({ as: Element =
248
251
  return JSON.stringify([
249
252
  localBox.width, localBox.height, effectiveDpr(2),
250
253
  style.fontSize && style.fontFamily
251
- ? document.fonts?.check(`400 ${style.fontSize} ${style.fontFamily}`, text) : false,
252
- ...['color', 'font-family', 'font-size', 'line-height', 'text-align', 'text-indent',
254
+ ? document.fonts?.check(`${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`, text) : false,
255
+ ...['color', 'font-family', 'font-size', 'font-weight', 'font-style', 'line-height', 'text-align', 'text-indent',
253
256
  'letter-spacing', 'white-space', 'word-break', 'overflow-wrap', 'direction',
254
257
  'padding-top', 'padding-left', 'padding-right', 'padding-bottom',
255
258
  '--inkra-letter-variation', '--inkra-letter-breaks', '--inkra-letter-outline',
@@ -260,12 +263,15 @@ const PencilTextImplementation = forwardRef(function PencilText({ as: Element =
260
263
  pending = null;
261
264
  if (!disposed && visible && dirty) {
262
265
  try {
263
- if (paintText(node, source, canvas, text, resolvedSeed, glyphSeed))
264
- lastAppearance = appearanceKey();
266
+ paintText(node, source, canvas, text, resolvedSeed, glyphSeed);
265
267
  }
266
268
  catch {
267
269
  delete node.dataset.pencilReady;
268
270
  }
271
+ // Remember fallback appearances too. An oversized/unsupported label
272
+ // must not retry expensive measurement for unrelated ancestor styles.
273
+ // Theme/font/size changes and explicit canvas recovery still retry.
274
+ lastAppearance = appearanceKey();
269
275
  dirty = false;
270
276
  }
271
277
  };
@@ -1,23 +1,49 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as AccordionPrimitive from '@radix-ui/react-accordion';
3
- import { createContext, forwardRef, useContext, useId } from 'react';
3
+ import { createContext, forwardRef, useCallback, useContext, useId, useRef, useState } from 'react';
4
4
  import { PencilIcon } from '../PencilIcon.js';
5
5
  import { PencilLine } from '../PencilLine.js';
6
6
  import { PencilSurface } from '../PencilSurface.js';
7
7
  import { PencilText } from '../PencilText.js';
8
+ import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
8
9
  const chevronDownPaths = [
9
10
  { d: 'M6 9L12 15L18 9' },
10
11
  ];
11
12
  const AccordionOrientationContext = createContext('vertical');
13
+ const AccordionOpenValuesContext = createContext(new Set());
14
+ const AccordionItemOpenContext = createContext(false);
12
15
  function classes(...values) {
13
16
  return values.filter(Boolean).join(' ');
14
17
  }
18
+ function assignRef(ref, value) {
19
+ if (typeof ref === 'function')
20
+ return ref(value);
21
+ if (ref)
22
+ ref.current = value;
23
+ }
24
+ function values(value) {
25
+ if (Array.isArray(value))
26
+ return value;
27
+ return value ? [value] : [];
28
+ }
15
29
  export const AccordionRoot = forwardRef(function AccordionRoot({ className, orientation = 'vertical', ...props }, ref) {
16
- return (_jsx(AccordionOrientationContext.Provider, { value: orientation, children: _jsx(AccordionPrimitive.Root, { ...props, className: classes('inkra-accordion', className), orientation: orientation, ref: ref }) }));
30
+ const controlled = props.value !== undefined;
31
+ const [uncontrolledValues, setUncontrolledValues] = useState(() => values(props.defaultValue));
32
+ const currentValues = controlled ? values(props.value) : uncontrolledValues;
33
+ const handleValueChange = (nextValue) => {
34
+ if (!controlled)
35
+ setUncontrolledValues(values(nextValue));
36
+ if (props.type === 'single')
37
+ props.onValueChange?.(nextValue);
38
+ else
39
+ props.onValueChange?.(nextValue);
40
+ };
41
+ return (_jsx(AccordionOrientationContext.Provider, { value: orientation, children: _jsx(AccordionOpenValuesContext.Provider, { value: new Set(currentValues), children: props.type === 'single' ? (_jsx(AccordionPrimitive.Root, { ...props, className: classes('inkra-accordion', className), onValueChange: handleValueChange, orientation: orientation, ref: ref })) : (_jsx(AccordionPrimitive.Root, { ...props, className: classes('inkra-accordion', className), onValueChange: handleValueChange, orientation: orientation, ref: ref })) }) }));
17
42
  });
18
43
  export const AccordionItem = forwardRef(function AccordionItem({ children, className, dividerSeed, showDivider = true, value, ...props }, ref) {
19
44
  const orientation = useContext(AccordionOrientationContext);
20
- return (_jsxs(AccordionPrimitive.Item, { ...props, className: classes('inkra-accordion__item', className), ref: ref, value: value, children: [children, showDivider ? (_jsx(PencilLine, { className: "inkra-accordion__divider", orientation: orientation === 'horizontal' ? 'vertical' : 'horizontal', seed: dividerSeed ?? `inkra-accordion-divider-${value}`, weight: 0.8 })) : null] }));
45
+ const open = useContext(AccordionOpenValuesContext).has(value);
46
+ return (_jsx(AccordionItemOpenContext.Provider, { value: open, children: _jsxs(AccordionPrimitive.Item, { ...props, className: classes('inkra-accordion__item', className), ref: ref, value: value, children: [children, showDivider ? (_jsx(PencilLine, { className: "inkra-accordion__divider", orientation: orientation === 'horizontal' ? 'vertical' : 'horizontal', seed: dividerSeed ?? `inkra-accordion-divider-${value}`, weight: 0.8 })) : null] }) }));
21
47
  });
22
48
  export const AccordionHeader = forwardRef(function AccordionHeader({ className, ...props }, ref) {
23
49
  return (_jsx(AccordionPrimitive.Header, { ...props, className: classes('inkra-accordion__header', className), ref: ref }));
@@ -29,9 +55,43 @@ export const AccordionTrigger = forwardRef(function AccordionTrigger({ children,
29
55
  return (_jsxs(AccordionPrimitive.Trigger, { ...props, className: classes('inkra-accordion__trigger', className), ref: ref, children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-accordion__focus-ring", radius: 6, seed: `${resolvedSeed}-focus`, variant: "outline" }), _jsx("span", { className: "inkra-accordion__trigger-label", children: typeof children === 'string' ? (_jsx(PencilText, { seed: `${resolvedSeed}-text`, children: children })) : children }), triggerIcon == null ? null : (_jsx("span", { "aria-hidden": "true", className: "inkra-accordion__trigger-icon", children: triggerIcon }))] }));
30
56
  });
31
57
  export const AccordionContent = forwardRef(function AccordionContent({ children, className, contentClassName, ...props }, ref) {
32
- return (_jsx(AccordionPrimitive.Content, { ...props, className: classes('inkra-accordion__content', className), ref: ref, children: _jsx("div", { className: classes('inkra-accordion__content-body', contentClassName), children: children }) }));
58
+ const open = useContext(AccordionItemOpenContext);
59
+ const contentRef = useRef(null);
60
+ const focusWithinRef = useRef(false);
61
+ const callerAriaHidden = props['aria-hidden'];
62
+ const callerInert = props.inert;
63
+ const callerOnBlurCapture = props.onBlurCapture;
64
+ const callerOnFocusCapture = props.onFocusCapture;
65
+ const setContentRef = useCallback((node) => {
66
+ contentRef.current = node;
67
+ const cleanup = assignRef(ref, node);
68
+ return () => {
69
+ contentRef.current = null;
70
+ if (typeof cleanup === 'function')
71
+ cleanup();
72
+ else
73
+ assignRef(ref, null);
74
+ };
75
+ }, [ref]);
76
+ useBrowserLayoutEffect(() => {
77
+ const content = contentRef.current;
78
+ if (open || !content || !focusWithinRef.current)
79
+ return;
80
+ focusWithinRef.current = false;
81
+ const triggerId = content.getAttribute('aria-labelledby');
82
+ if (triggerId)
83
+ content.ownerDocument.getElementById(triggerId)?.focus({ preventScroll: true });
84
+ }, [open]);
85
+ return (_jsx(AccordionPrimitive.Content, { ...props, "aria-hidden": open ? callerAriaHidden : true, className: classes('inkra-accordion__content', className), inert: open ? callerInert : true, onBlurCapture: event => {
86
+ callerOnBlurCapture?.(event);
87
+ if (open && !event.currentTarget.contains(event.relatedTarget))
88
+ focusWithinRef.current = false;
89
+ }, onFocusCapture: event => {
90
+ focusWithinRef.current = true;
91
+ callerOnFocusCapture?.(event);
92
+ }, ref: setContentRef, children: _jsx("div", { className: classes('inkra-accordion__content-body', contentClassName), children: children }) }));
33
93
  });
34
- export const Accordion = Object.freeze({
94
+ export const Accordion = /* @__PURE__ */ Object.freeze({
35
95
  Root: AccordionRoot,
36
96
  Item: AccordionItem,
37
97
  Header: AccordionHeader,