@inkra/react 0.2.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/README.md +101 -7
  2. package/dist/Button.js +3 -2
  3. package/dist/PencilIcon.d.ts +10 -0
  4. package/dist/PencilIcon.js +83 -39
  5. package/dist/PencilLine.js +9 -1
  6. package/dist/PencilText.js +15 -9
  7. package/dist/accordion/Accordion.js +65 -5
  8. package/dist/alert-dialog/index.d.ts +1 -1
  9. package/dist/alert-dialog/index.js +18 -6
  10. package/dist/audio-player/index.d.ts +2 -0
  11. package/dist/audio-player/index.js +126 -31
  12. package/dist/calendar/index.d.ts +4 -1
  13. package/dist/calendar/index.js +31 -12
  14. package/dist/carousel/index.js +13 -6
  15. package/dist/choice-grid/index.d.ts +6 -1
  16. package/dist/choice-grid/index.js +61 -33
  17. package/dist/color-picker/GradientEditor.d.ts +39 -0
  18. package/dist/color-picker/GradientEditor.js +63 -0
  19. package/dist/color-picker/gradient.d.ts +25 -0
  20. package/dist/color-picker/gradient.js +60 -0
  21. package/dist/color-picker/index.d.ts +27 -7
  22. package/dist/color-picker/index.js +134 -32
  23. package/dist/color-swatch/index.d.ts +10 -0
  24. package/dist/color-swatch/index.js +9 -0
  25. package/dist/command/index.d.ts +24 -2
  26. package/dist/command/index.js +29 -6
  27. package/dist/content/index.d.ts +5 -0
  28. package/dist/content/index.js +12 -3
  29. package/dist/context-menu/index.d.ts +2 -2
  30. package/dist/context-menu/index.js +4 -3
  31. package/dist/data-table/index.d.ts +7 -1
  32. package/dist/data-table/index.js +64 -16
  33. package/dist/data-table/model.d.ts +2 -0
  34. package/dist/data-table/model.js +7 -0
  35. package/dist/date-picker/index.d.ts +2 -0
  36. package/dist/date-picker/index.js +10 -9
  37. package/dist/day-agenda/index.d.ts +6 -1
  38. package/dist/day-agenda/index.js +46 -21
  39. package/dist/day-agenda/layout.js +65 -9
  40. package/dist/display/ProgressRing.js +7 -3
  41. package/dist/display/index.d.ts +4 -1
  42. package/dist/display/index.js +3 -2
  43. package/dist/dockable-panel/index.js +5 -5
  44. package/dist/drawer/index.d.ts +1 -1
  45. package/dist/drawer/index.js +9 -6
  46. package/dist/dropdown-menu/index.d.ts +1 -1
  47. package/dist/dropdown-menu/index.js +3 -2
  48. package/dist/editable/index.d.ts +24 -2
  49. package/dist/editable/index.js +142 -37
  50. package/dist/empty/illustrations.d.ts +8 -0
  51. package/dist/empty/illustrations.js +78 -0
  52. package/dist/empty/index.d.ts +6 -1
  53. package/dist/empty/index.js +11 -3
  54. package/dist/flip/index.js +28 -8
  55. package/dist/form/EditablePencilText.js +34 -0
  56. package/dist/form/Field.js +11 -10
  57. package/dist/form/Input.js +1 -1
  58. package/dist/form/Textarea.d.ts +3 -1
  59. package/dist/form/Textarea.js +6 -4
  60. package/dist/hotkey/index.d.ts +1 -0
  61. package/dist/hotkey/index.js +6 -4
  62. package/dist/hover-card/index.js +1 -1
  63. package/dist/image-cropper/index.js +4 -2
  64. package/dist/image-viewer/VideoResource.d.ts +21 -0
  65. package/dist/image-viewer/VideoResource.js +194 -0
  66. package/dist/image-viewer/index.d.ts +14 -0
  67. package/dist/image-viewer/index.js +118 -65
  68. package/dist/image-viewer/lucideGeometry.d.ts +3 -0
  69. package/dist/image-viewer/lucideGeometry.js +19 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +2 -0
  72. package/dist/input-number/index.d.ts +6 -0
  73. package/dist/input-number/index.js +131 -15
  74. package/dist/input-number/model.d.ts +13 -0
  75. package/dist/input-number/model.js +50 -0
  76. package/dist/internal/cache.d.ts +4 -0
  77. package/dist/internal/cache.js +18 -1
  78. package/dist/internal/iconGeometry.d.ts +2 -0
  79. package/dist/internal/iconGeometry.js +32 -0
  80. package/dist/internal/renderTokens.d.ts +4 -0
  81. package/dist/internal/renderTokens.js +4 -0
  82. package/dist/internal/stroke.d.ts +7 -0
  83. package/dist/internal/stroke.js +63 -7
  84. package/dist/internal/surfaceBudget.d.ts +20 -0
  85. package/dist/internal/surfaceBudget.js +69 -0
  86. package/dist/internal/surfaceRenderer.js +87 -28
  87. package/dist/internal/surfaceVisibility.d.ts +3 -0
  88. package/dist/internal/surfaceVisibility.js +60 -0
  89. package/dist/internal/textRenderer.d.ts +2 -0
  90. package/dist/internal/textRenderer.js +3 -3
  91. package/dist/internal/usePencilSurface.js +17 -1
  92. package/dist/internal/wash.js +4 -3
  93. package/dist/journey-map/terrain.js +10 -5
  94. package/dist/layout/index.js +1 -1
  95. package/dist/log/index.js +29 -22
  96. package/dist/markdown/CodeBlock.d.ts +19 -0
  97. package/dist/markdown/CodeBlock.js +86 -0
  98. package/dist/markdown/Deletion.d.ts +2 -0
  99. package/dist/markdown/Deletion.js +69 -0
  100. package/dist/markdown/icons.d.ts +14 -0
  101. package/dist/markdown/icons.js +16 -0
  102. package/dist/markdown/index.d.ts +17 -0
  103. package/dist/markdown/index.js +175 -0
  104. package/dist/node-graph/index.d.ts +2 -0
  105. package/dist/node-graph/index.js +49 -25
  106. package/dist/number-ticker/index.d.ts +11 -9
  107. package/dist/number-ticker/index.js +14 -11
  108. package/dist/number-ticker/model.d.ts +38 -5
  109. package/dist/number-ticker/model.js +139 -10
  110. package/dist/number-ticker/precision.d.ts +7 -0
  111. package/dist/number-ticker/precision.js +141 -0
  112. package/dist/overflow-list/index.js +5 -2
  113. package/dist/overlay/Overlay.d.ts +1 -1
  114. package/dist/overlay/Overlay.js +5 -3
  115. package/dist/overlay/Popover.d.ts +1 -1
  116. package/dist/overlay/Popover.js +6 -5
  117. package/dist/overlay/themeBridge.d.ts +12 -1
  118. package/dist/overlay/themeBridge.js +41 -3
  119. package/dist/pagination/index.d.ts +10 -1
  120. package/dist/pagination/index.js +6 -4
  121. package/dist/pan-zoom/index.d.ts +2 -0
  122. package/dist/pan-zoom/index.js +7 -6
  123. package/dist/pencil.d.ts +1 -0
  124. package/dist/pencil.js +4 -4
  125. package/dist/radar/index.d.ts +4 -0
  126. package/dist/radar/index.js +41 -21
  127. package/dist/rating/index.d.ts +4 -0
  128. package/dist/rating/index.js +128 -24
  129. package/dist/resizable/index.js +1 -1
  130. package/dist/scroll-area/index.d.ts +1 -1
  131. package/dist/scroll-area/index.js +5 -2
  132. package/dist/select/MultipleSelect.js +5 -4
  133. package/dist/select/SearchableSelect.js +61 -6
  134. package/dist/select/Select.d.ts +1 -0
  135. package/dist/select/Select.js +3 -1
  136. package/dist/select/StepperSelect.d.ts +29 -0
  137. package/dist/select/StepperSelect.js +119 -0
  138. package/dist/select/index.d.ts +2 -0
  139. package/dist/select/index.js +1 -0
  140. package/dist/select/stepperModel.d.ts +7 -0
  141. package/dist/select/stepperModel.js +29 -0
  142. package/dist/selection/Checkbox.js +4 -3
  143. package/dist/selection/RadioGroup.js +29 -13
  144. package/dist/selection/Switch.js +4 -3
  145. package/dist/selection/useNativeChecked.d.ts +2 -1
  146. package/dist/selection/useNativeChecked.js +26 -8
  147. package/dist/slider/index.js +50 -6
  148. package/dist/sortable/index.js +15 -6
  149. package/dist/sparkline/index.js +17 -2
  150. package/dist/sparkline/model.js +11 -4
  151. package/dist/styles/accordion.css +228 -0
  152. package/dist/styles/alert-dialog.css +139 -0
  153. package/dist/styles/audio-player.css +22 -0
  154. package/dist/styles/base.css +638 -0
  155. package/dist/styles/breadcrumb.css +7 -0
  156. package/dist/styles/calendar.css +62 -0
  157. package/dist/styles/carousel.css +173 -0
  158. package/dist/styles/choice-grid.css +226 -0
  159. package/dist/styles/color-picker.css +281 -0
  160. package/dist/styles/color-swatch.css +47 -0
  161. package/dist/styles/command.css +52 -0
  162. package/dist/styles/content.css +79 -0
  163. package/dist/styles/context-menu.css +35 -0
  164. package/dist/styles/data-table.css +31 -0
  165. package/dist/styles/date-picker.css +107 -0
  166. package/dist/styles/day-agenda.css +30 -0
  167. package/dist/styles/display.css +304 -0
  168. package/dist/styles/dockable-panel.css +110 -0
  169. package/dist/styles/drawer.css +133 -0
  170. package/dist/styles/dropdown-menu.css +68 -0
  171. package/dist/styles/editable.css +145 -0
  172. package/dist/styles/empty.css +108 -0
  173. package/dist/styles/flip.css +101 -0
  174. package/dist/styles/form.css +634 -0
  175. package/dist/styles/heatmap.css +256 -0
  176. package/dist/styles/hotkey.css +189 -0
  177. package/dist/styles/hover-card.css +27 -0
  178. package/dist/styles/image-cropper.css +205 -0
  179. package/dist/styles/image-viewer.css +422 -0
  180. package/dist/styles/input-number.css +21 -0
  181. package/dist/styles/journey-map.css +19 -0
  182. package/dist/styles/keypad.css +92 -0
  183. package/dist/styles/layout.css +72 -0
  184. package/dist/styles/leaflet.css +661 -0
  185. package/dist/styles/log.css +203 -0
  186. package/dist/styles/markdown.css +379 -0
  187. package/dist/styles/node-graph.css +142 -0
  188. package/dist/styles/number-ticker.css +83 -0
  189. package/dist/styles/overflow-list.css +51 -0
  190. package/dist/styles/overlay.css +284 -0
  191. package/dist/styles/pagination.css +21 -0
  192. package/dist/styles/pan-zoom.css +136 -0
  193. package/dist/styles/radar.css +1 -0
  194. package/dist/styles/rating.css +121 -0
  195. package/dist/styles/resizable.css +108 -0
  196. package/dist/styles/scroll-area.css +122 -0
  197. package/dist/styles/select.css +557 -0
  198. package/dist/styles/selection.css +264 -0
  199. package/dist/styles/slider.css +145 -0
  200. package/dist/styles/slot.css +109 -0
  201. package/dist/styles/sortable.css +159 -0
  202. package/dist/styles/sparkline.css +24 -0
  203. package/dist/styles/steps.css +27 -0
  204. package/dist/styles/tabs.css +79 -0
  205. package/dist/styles/tag-input.css +213 -0
  206. package/dist/styles/time-picker.css +66 -0
  207. package/dist/styles/timeline.css +24 -0
  208. package/dist/styles/toast.css +160 -0
  209. package/dist/styles/toggle-group.css +107 -0
  210. package/dist/styles/tooltip.css +78 -0
  211. package/dist/styles/transfer.css +148 -0
  212. package/dist/styles/tree-select.css +131 -0
  213. package/dist/styles/tree.css +59 -0
  214. package/dist/styles/typewriter.css +62 -0
  215. package/dist/styles/upload.css +60 -0
  216. package/dist/styles/virtual-list.css +112 -0
  217. package/dist/styles.css +2241 -443
  218. package/dist/tabs/index.d.ts +9 -0
  219. package/dist/tabs/index.js +66 -16
  220. package/dist/tabs/useOverflow.d.ts +20 -0
  221. package/dist/tabs/useOverflow.js +145 -0
  222. package/dist/tag-input/index.d.ts +7 -2
  223. package/dist/tag-input/index.js +70 -26
  224. package/dist/time-picker/index.js +38 -20
  225. package/dist/toast/index.d.ts +19 -11
  226. package/dist/toast/index.js +48 -8
  227. package/dist/toast/manager.d.ts +42 -0
  228. package/dist/toast/manager.js +85 -0
  229. package/dist/toast/useToastNavigation.d.ts +3 -0
  230. package/dist/toast/useToastNavigation.js +75 -0
  231. package/dist/tokens.css +4 -0
  232. package/dist/tooltip/Tooltip.d.ts +1 -1
  233. package/dist/tooltip/Tooltip.js +6 -5
  234. package/dist/transfer/index.d.ts +5 -0
  235. package/dist/transfer/index.js +93 -29
  236. package/dist/transfer/model.d.ts +4 -0
  237. package/dist/transfer/model.js +37 -11
  238. package/dist/tree/index.d.ts +5 -1
  239. package/dist/tree/index.js +5 -5
  240. package/dist/tree-select/index.js +67 -30
  241. package/dist/tree-select/model.d.ts +11 -5
  242. package/dist/tree-select/model.js +111 -83
  243. package/dist/typewriter/index.js +10 -10
  244. package/dist/upload/index.js +30 -16
  245. package/dist/virtual-list/index.js +10 -7
  246. package/package.json +416 -5
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Inkra
2
2
 
3
- Inkra is a pencil-styled React component library. It includes action, form, overlay, navigation, content and feedback components, plus Calendar, DayAgenda, JourneyMap and deterministic drawing primitives. Version `0.1.0` is published; the component expansion described below is available only in the current, unpublished source candidate.
3
+ Inkra is a pencil-styled React component library. It includes action, form, overlay, navigation, content and feedback components, plus Calendar, DayAgenda, JourneyMap and deterministic drawing primitives. This document describes the APIs shipped with this package; see the repository changelog and npm registry for release status.
4
4
 
5
5
  Source and maintenance: [inkrajs/inkra](https://github.com/inkrajs/inkra).
6
6
 
@@ -80,6 +80,18 @@ All standard `button` props, including `disabled`, `name`, `value`, `form`, `ari
80
80
 
81
81
  `InputNumber` places decrease and increase buttons on either side of a centered numeric input, with no clear button. Keyboard deletion produces `null`; decimal stepping and out-of-range validation remain native. `Slider` supports single/range values, horizontal/vertical layout and `standard` or `inset` appearance.
82
82
 
83
+ `InputNumber` supports Shift-click and Shift-arrow increments of 10 by default.
84
+ Use `stepModifiers={[{ shiftKey: true, step: 10 }, { altKey: true, step: 0.1 }]}`
85
+ to configure exact modifier chords with absolute increments, or `[]` to disable
86
+ overrides. The first valid matching rule wins; unspecified modifiers must be
87
+ released. Values remain bounded by `min`/`max`; `step="any"` disables stepping.
88
+ `pressAndHold` defaults to true, with `repeatDelay={400}` and
89
+ `repeatInterval={80}` in milliseconds. Release, cancellation, blur, backgrounding,
90
+ disabling and value boundaries stop repetition. Delay/interval are bounded to
91
+ 150-60000/32-60000 ms. Numeric values use pencil lettering at rest and during
92
+ button stepping. Direct text entry deliberately stays native until blur, keeping
93
+ browser-owned incomplete drafts such as `-`, `1.` and `1e` readable.
94
+
83
95
  `Select.Root` uses string values for single selection and string arrays with `multiple`. Multi-select remains open while toggling items and submits repeated values under the same field name. `Select.Clear` is a separate button; never nest it inside the trigger.
84
96
 
85
97
  `Select.Trigger` supports `startIcon`, `endIcon`, `hideValue`, `hideIndicator`, `iconOnly`, `fullWidth` and `textAlign`. The existing `icon` prop replaces the dropdown indicator. Icon-only mode keeps a square hit target of at least 44px and retains selected text as an accessible description. Provide a Field/ARIA name and Tooltip for icon-only triggers. Decorative slots must not contain interactive controls.
@@ -195,10 +207,9 @@ Content layout options preserve existing defaults:
195
207
  - `Avatar.Root.radius` clamps to 0-8px, including pencil and image corners; `bordered={false}` removes the outline. `Avatar.Image.fit` accepts `cover | contain` (default cover).
196
208
  - `Table.Root.striped`, `hoverable` and `stickyHeader` default to false. Constrain `Table.Container` with `style={{ maxHeight: 240 }}` for sticky scrolling. `captionSide` accepts `top | bottom`. `Head.align` and `Cell.align` accept logical `start | center | end`, retaining native alignment values for compatibility. Hover feedback does not imply row actions.
197
209
 
198
- ## Component Expansion (Unreleased)
210
+ ## Additional Components
199
211
 
200
- These exports are not present in the published 0.1.0 archive. Build, pack and
201
- install the local candidate to use them. The documentation playground includes
212
+ These exports extend the original core component set. The documentation playground includes
202
213
  working examples, live property tabs and per-component API/accessibility notes.
203
214
 
204
215
  | Component | Contract |
@@ -239,6 +250,77 @@ motion and SSR show deterministic final values. Intermediate frames are visual;
239
250
  the polite announcement updates only at the final value. Use `getValueLabel`
240
251
  when units or custom rendering need a complete accessible phrase.
241
252
 
253
+ Set `compact` to follow `locale` (Chinese units for `zh`, English units otherwise),
254
+ or use `compact="en"` / `compact="zh"` to select a unit table explicitly.
255
+ English uses this library's short-scale suffix convention: K, M, B, T, Qa, Qi,
256
+ Sx, Sp, Oc, No, Dc (10^3 through 10^33, increasing by 10^3).
257
+ Chinese uses 千 (10^3), then 万, 亿, 兆, 京, 垓, 秭, 穰, 沟, 涧, 正, 载, 极,
258
+ 恒河沙, 阿僧祇, 那由他, 不可思议, 无量大数 (10^4 through 10^68, increasing by 10^4).
259
+ `numberTickerUnits` exports both tables. A space separates numbers and units by default.
260
+ The default is `false`, preserving existing `formatOptions` behavior. Compact mode
261
+ uses at most one fractional digit by default; customize precision with
262
+ `formatOptions`. Explicit scientific/engineering notation takes priority over compact.
263
+ Currency/percent affixes are preserved. By default rounding can promote the unit
264
+ (999,950 becomes 1 M). Deltas use the same unit
265
+ system with their own precision; announcements and `getValueLabel` receive the
266
+ unabbreviated formatted value. `renderValue` receives the abbreviated value in
267
+ `details.formattedValue`. NumberTicker never changes the supplied numeric value.
268
+
269
+ `compactOptions` supports:
270
+
271
+ - `minUnit` / `maxUnit`: allowed unit labels; equal bounds fix the unit. An empty
272
+ label denotes the unitless base. For example `maxUnit: '万'` keeps 10^8 as `10,000 万`.
273
+ - `separator`: defaults to one space; `''` removes it.
274
+ - `promotionThreshold`: positive multiplier on each unit's factor (default 1).
275
+ Set 10 to wait until 10,000 before entering K. `promoteAfterRounding: false`
276
+ compares raw magnitudes instead of rounded current-unit values.
277
+ - `units`: replace the table with `{ factor, label, threshold? }` entries.
278
+ `threshold` overrides the multiplier with an absolute display-magnitude trigger;
279
+ e.g. `{ factor: 10000, label: '万', threshold: 100000 }`. Binary factors work too.
280
+ Entries are sorted without mutation; labels/factors must be unique, factors > 1,
281
+ and thresholds positive, finite and strictly increasing. Invalid configurations
282
+ and unknown/reversed bounds throw RangeError. The unitless base is implicit.
283
+ - `overflow`: `unit` keeps the largest permitted unit; `scientific` switches at
284
+ the next unit's threshold (or extrapolates the last step at the table end).
285
+ Explicit `maxUnit` defaults to `unit`; otherwise the default is `scientific`.
286
+
287
+ Use `formatOptions.notation: 'scientific' | 'engineering'` for exponent notation.
288
+ `maximumFractionDigits` controls decimals, `minimumFractionDigits` pads zeros,
289
+ and `roundingMode: 'trunc'` truncates instead of rounding (`halfExpand` is default).
290
+ Other Intl rounding modes are supported when provided by the runtime; unsupported
291
+ nondefault rounding modes throw rather than silently round differently.
292
+ `value`, unit `factor` / `threshold`, and `promotionThreshold` accept
293
+ `number | bigint | string`. Use bigint for exact integers and decimal strings for
294
+ exact fractions or exponents beyond JavaScript's numeric range. Already-rounded
295
+ `number` inputs cannot regain lost digits. Decimal arithmetic uses an isolated
296
+ `decimal.js` context without modifying the application's Decimal settings.
297
+
298
+ ```tsx
299
+ <NumberTicker value={9007199254740993n} showDelta />
300
+ <NumberTicker value="12345678901234567890.123456" formatOptions={{ maximumFractionDigits: 6 }} />
301
+ <NumberTicker value="1.23456789e1000" formatOptions={{ notation: 'scientific' }} />
302
+ ```
303
+
304
+ Strings accept signs, decimal points, e/E exponents and outer whitespace, but
305
+ not grouping separators, hex, empty strings or non-finite spellings. Inputs are
306
+ bounded to 10,000 coefficient digits and written/normalized exponents within
307
+ -10,000..10,000; invalid or oversized exact inputs throw `RangeError`. Decimal
308
+ fractions require exact Intl decimal-string support in the runtime; unsupported
309
+ engines throw rather than silently lose precision. Native number NaN/Infinity
310
+ formatting is unchanged.
311
+
312
+ `renderValue` and `getValueLabel` retain the input type; prefer the supplied
313
+ formatted text when custom rendering. Deltas between bigints are bigint, between
314
+ numbers are number, and mixed/string deltas are exact decimal strings. Bigint
315
+ animation frames stay integral. Input-type changes or exact arithmetic spanning
316
+ more than 512 digits settle immediately, as do reduced-motion updates.
317
+ `createNumberFormatter` remains a native Intl formatter; use
318
+ `formatTickerValue(formatter, value)` for precise numeric-string formatting.
319
+
320
+ `prefix` and `suffix` accept icons, text or fragments. Pass `PencilIcon` to keep
321
+ the library material, and `getValueLabel` to describe icon meanings to assistive
322
+ technology; decorative affixes are not announced twice.
323
+
242
324
  `Log` renders caller-ordered records with stable ids, optional level filters and
243
325
  pause/follow controls. It follows only append updates while the viewport remains
244
326
  near the bottom. The named scroll region supports Home/End and intentionally is
@@ -266,7 +348,18 @@ All examples use public sample data.
266
348
 
267
349
  `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.
268
350
 
269
- `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.
351
+ `PencilIcon` accepts familiar SVG path data and deposits pigment along sampled geometry.
352
+ Use `size` for the displayed canvas size (default 40px), `strokeWidth` (0.3-3,
353
+ default 1.5), `intensity` (0-1, default 1), `breaks` (0-1, default 0.18),
354
+ `pressureVariation` (0-1, default 0.82), and `breakCount` (integer 0-12, default 1).
355
+ The gap count is a maximum: short displayed paths use fewer or no gaps, while
356
+ larger icons can use more up to that limit. Device pixel ratio does not change
357
+ the count. Zero breaks or zero breakCount removes intentional gaps; zero pressure
358
+ variation keeps constant pressure without removing the grain or endpoint taper.
359
+ The matching inherited tokens are `--inkra-icon-stroke-width`,
360
+ `--inkra-icon-intensity`, `--inkra-icon-breaks`, `--inkra-icon-pressure-variation`
361
+ and `--inkra-icon-break-count`. Finite explicit props override those tokens.
362
+ See `docs/pencil-contract.md` in the workspace for the material contract.
270
363
 
271
364
  `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:
272
365
 
@@ -285,8 +378,9 @@ text within that scope without changing layout, selectable text, or labels.
285
378
 
286
379
  `PencilLine` reads `--inkra-stroke-width` (0.3-3) and
287
380
  `--inkra-stroke-amplitude` (0-4) when `weight` or `roughness` is omitted.
288
- Explicit props take precedence. These tokens also scale surface outlines and icon
289
- strokes relative to their original presets, including button/card/input borders.
381
+ Explicit props take precedence. These tokens also scale surface outlines,
382
+ including button/card/input borders. Icon weight uses its independent token;
383
+ the general amplitude token still controls icon roughness.
290
384
  The default values are 2 and 1.5. They do not change layout or text lettering.
291
385
 
292
386
  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,
@@ -11,7 +11,7 @@ export declare function AlertDialogRoot({ defaultOpen, onOpenChange, open, pendi
11
11
  export type AlertDialogTriggerProps = DialogPrimitive.DialogTriggerProps;
12
12
  export declare const AlertDialogTrigger: import("react").ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
13
13
  export type AlertDialogPortalProps = DialogPrimitive.DialogPortalProps;
14
- export declare function AlertDialogPortal({ children, ...props }: AlertDialogPortalProps): import("react").JSX.Element;
14
+ export declare function AlertDialogPortal({ children, ...props }: AlertDialogPortalProps): ReactNode;
15
15
  export type AlertDialogBackdropProps = DialogPrimitive.DialogOverlayProps;
16
16
  export declare const AlertDialogBackdrop: import("react").ForwardRefExoticComponent<DialogPrimitive.DialogOverlayProps & import("react").RefAttributes<HTMLDivElement>>;
17
17
  export interface AlertDialogContentProps extends DialogPrimitive.DialogContentProps {