@inkra/react 0.1.0 → 0.3.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 (177) hide show
  1. package/README.md +186 -1
  2. package/dist/PencilIcon.js +14 -16
  3. package/dist/PencilLine.js +18 -12
  4. package/dist/PencilText.js +192 -59
  5. package/dist/alert-dialog/index.d.ts +64 -0
  6. package/dist/alert-dialog/index.js +111 -0
  7. package/dist/audio-player/index.d.ts +60 -0
  8. package/dist/audio-player/index.js +101 -0
  9. package/dist/carousel/index.d.ts +51 -0
  10. package/dist/carousel/index.js +290 -0
  11. package/dist/choice-grid/index.d.ts +38 -0
  12. package/dist/choice-grid/index.js +85 -0
  13. package/dist/color-picker/color.d.ts +15 -0
  14. package/dist/color-picker/color.js +55 -0
  15. package/dist/color-picker/index.d.ts +31 -0
  16. package/dist/color-picker/index.js +69 -0
  17. package/dist/command/index.d.ts +45 -0
  18. package/dist/command/index.js +39 -0
  19. package/dist/context-menu/index.d.ts +76 -0
  20. package/dist/context-menu/index.js +241 -0
  21. package/dist/data-table/index.d.ts +47 -0
  22. package/dist/data-table/index.js +91 -0
  23. package/dist/date-picker/index.d.ts +50 -0
  24. package/dist/date-picker/index.js +180 -0
  25. package/dist/display/ProgressRing.d.ts +6 -0
  26. package/dist/display/ProgressRing.js +146 -0
  27. package/dist/display/index.d.ts +27 -0
  28. package/dist/display/index.js +18 -2
  29. package/dist/dockable-panel/index.d.ts +41 -0
  30. package/dist/dockable-panel/index.js +253 -0
  31. package/dist/dockable-panel/model.d.ts +21 -0
  32. package/dist/dockable-panel/model.js +61 -0
  33. package/dist/drawer/index.d.ts +63 -0
  34. package/dist/drawer/index.js +88 -0
  35. package/dist/dropdown-menu/index.d.ts +1 -1
  36. package/dist/dropdown-menu/index.js +3 -2
  37. package/dist/editable/index.d.ts +53 -0
  38. package/dist/editable/index.js +162 -0
  39. package/dist/empty/index.d.ts +21 -0
  40. package/dist/empty/index.js +23 -0
  41. package/dist/flip/index.d.ts +49 -0
  42. package/dist/flip/index.js +138 -0
  43. package/dist/form/EditablePencilText.js +188 -143
  44. package/dist/form/Input.js +1 -1
  45. package/dist/form/Textarea.d.ts +12 -0
  46. package/dist/form/Textarea.js +83 -4
  47. package/dist/form/index.d.ts +1 -1
  48. package/dist/heatmap/index.d.ts +59 -0
  49. package/dist/heatmap/index.js +248 -0
  50. package/dist/hotkey/index.d.ts +37 -0
  51. package/dist/hotkey/index.js +130 -0
  52. package/dist/hotkey/model.d.ts +27 -0
  53. package/dist/hotkey/model.js +97 -0
  54. package/dist/hover-card/index.d.ts +33 -0
  55. package/dist/hover-card/index.js +141 -0
  56. package/dist/image-cropper/index.d.ts +82 -0
  57. package/dist/image-cropper/index.js +261 -0
  58. package/dist/image-cropper/model.d.ts +42 -0
  59. package/dist/image-cropper/model.js +94 -0
  60. package/dist/image-viewer/index.d.ts +79 -0
  61. package/dist/image-viewer/index.js +220 -0
  62. package/dist/image-viewer/model.d.ts +9 -0
  63. package/dist/image-viewer/model.js +18 -0
  64. package/dist/index.d.ts +47 -0
  65. package/dist/index.js +47 -0
  66. package/dist/input-number/index.d.ts +6 -0
  67. package/dist/input-number/index.js +126 -15
  68. package/dist/input-number/model.d.ts +13 -0
  69. package/dist/input-number/model.js +50 -0
  70. package/dist/internal/cache.d.ts +1 -1
  71. package/dist/internal/cache.js +1 -1
  72. package/dist/internal/renderTokens.d.ts +10 -0
  73. package/dist/internal/renderTokens.js +13 -0
  74. package/dist/internal/surfaceRenderer.d.ts +14 -6
  75. package/dist/internal/surfaceRenderer.js +15 -12
  76. package/dist/internal/textRenderer.d.ts +1 -0
  77. package/dist/internal/textRenderer.js +33 -7
  78. package/dist/internal/usePencilSurface.js +16 -8
  79. package/dist/internal/useSelectionDrawing.js +15 -11
  80. package/dist/journey-map/MapScale.js +3 -1
  81. package/dist/journey-map/index.js +3 -1
  82. package/dist/keypad/index.d.ts +23 -0
  83. package/dist/keypad/index.js +63 -0
  84. package/dist/keypad/model.d.ts +20 -0
  85. package/dist/keypad/model.js +33 -0
  86. package/dist/layout/index.d.ts +63 -0
  87. package/dist/layout/index.js +62 -0
  88. package/dist/log/index.d.ts +45 -0
  89. package/dist/log/index.js +159 -0
  90. package/dist/log/model.d.ts +16 -0
  91. package/dist/log/model.js +13 -0
  92. package/dist/node-graph/index.d.ts +123 -0
  93. package/dist/node-graph/index.js +304 -0
  94. package/dist/number-ticker/index.d.ts +31 -0
  95. package/dist/number-ticker/index.js +135 -0
  96. package/dist/number-ticker/model.d.ts +41 -0
  97. package/dist/number-ticker/model.js +163 -0
  98. package/dist/number-ticker/precision.d.ts +7 -0
  99. package/dist/number-ticker/precision.js +134 -0
  100. package/dist/overflow-list/index.d.ts +39 -0
  101. package/dist/overflow-list/index.js +230 -0
  102. package/dist/overflow-list/layout.d.ts +19 -0
  103. package/dist/overflow-list/layout.js +44 -0
  104. package/dist/overlay/Overlay.d.ts +1 -1
  105. package/dist/overlay/Overlay.js +4 -2
  106. package/dist/overlay/Popover.d.ts +1 -1
  107. package/dist/overlay/Popover.js +3 -2
  108. package/dist/overlay/themeBridge.d.ts +12 -1
  109. package/dist/overlay/themeBridge.js +27 -1
  110. package/dist/pagination/index.d.ts +23 -0
  111. package/dist/pagination/index.js +41 -0
  112. package/dist/pagination/model.d.ts +4 -0
  113. package/dist/pagination/model.js +42 -0
  114. package/dist/pan-zoom/index.d.ts +71 -0
  115. package/dist/pan-zoom/index.js +175 -0
  116. package/dist/radar/index.d.ts +56 -0
  117. package/dist/radar/index.js +220 -0
  118. package/dist/range-slider/index.d.ts +16 -0
  119. package/dist/range-slider/index.js +22 -0
  120. package/dist/rating/index.d.ts +28 -0
  121. package/dist/rating/index.js +127 -0
  122. package/dist/resizable/index.d.ts +32 -0
  123. package/dist/resizable/index.js +50 -0
  124. package/dist/scroll-area/index.d.ts +36 -0
  125. package/dist/scroll-area/index.js +44 -0
  126. package/dist/segmented-progress/SegmentedProgress.d.ts +17 -0
  127. package/dist/segmented-progress/SegmentedProgress.js +42 -0
  128. package/dist/slider/index.js +1 -1
  129. package/dist/slot/index.d.ts +24 -0
  130. package/dist/slot/index.js +29 -0
  131. package/dist/sortable/index.d.ts +50 -0
  132. package/dist/sortable/index.js +283 -0
  133. package/dist/sortable/model.d.ts +30 -0
  134. package/dist/sortable/model.js +64 -0
  135. package/dist/sparkline/index.d.ts +19 -0
  136. package/dist/sparkline/index.js +240 -0
  137. package/dist/sparkline/model.d.ts +16 -0
  138. package/dist/sparkline/model.js +28 -0
  139. package/dist/steps/index.d.ts +16 -0
  140. package/dist/steps/index.js +37 -0
  141. package/dist/styles.css +4957 -269
  142. package/dist/tag-input/index.d.ts +69 -0
  143. package/dist/tag-input/index.js +325 -0
  144. package/dist/tag-input/model.d.ts +14 -0
  145. package/dist/tag-input/model.js +38 -0
  146. package/dist/time-picker/index.d.ts +15 -0
  147. package/dist/time-picker/index.js +168 -0
  148. package/dist/timeline/index.d.ts +14 -0
  149. package/dist/timeline/index.js +16 -0
  150. package/dist/toast/index.d.ts +27 -0
  151. package/dist/toast/index.js +36 -0
  152. package/dist/toggle-group/index.d.ts +17 -0
  153. package/dist/toggle-group/index.js +26 -0
  154. package/dist/tokens.css +10 -9
  155. package/dist/tooltip/Tooltip.d.ts +1 -1
  156. package/dist/tooltip/Tooltip.js +3 -2
  157. package/dist/transfer/index.d.ts +64 -0
  158. package/dist/transfer/index.js +174 -0
  159. package/dist/transfer/model.d.ts +12 -0
  160. package/dist/transfer/model.js +33 -0
  161. package/dist/tree/index.d.ts +39 -0
  162. package/dist/tree/index.js +36 -0
  163. package/dist/tree-select/index.d.ts +59 -0
  164. package/dist/tree-select/index.js +142 -0
  165. package/dist/tree-select/model.d.ts +31 -0
  166. package/dist/tree-select/model.js +135 -0
  167. package/dist/typewriter/index.d.ts +21 -0
  168. package/dist/typewriter/index.js +101 -0
  169. package/dist/typewriter/model.d.ts +7 -0
  170. package/dist/typewriter/model.js +45 -0
  171. package/dist/upload/index.d.ts +59 -0
  172. package/dist/upload/index.js +230 -0
  173. package/dist/virtual-list/index.d.ts +44 -0
  174. package/dist/virtual-list/index.js +129 -0
  175. package/dist/virtual-list/model.d.ts +7 -0
  176. package/dist/virtual-list/model.js +27 -0
  177. package/package.json +23 -3
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. The first `0.1.0` release is being prepared; this package is still an unpublished candidate unless a corresponding registry release has been announced.
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.
@@ -150,12 +162,31 @@ outside interaction, modal focus trapping and focus return.
150
162
  <Skeleton style={{ width: 200, height: 20 }} />
151
163
  <Progress label="Uploading" value={60} />
152
164
  <Progress aria-label="Loading" value={null} />
165
+ <Progress shape="circular" value={72} diameter={128} thickness={6} aria-label="Download">
166
+ {({ percent }) => `${Math.round(percent ?? 0)}%`}
167
+ </Progress>
168
+ <Progress shape="circular" value={3} max={5} contentPosition="end" label="Files">
169
+ <span>3 / 5 files</span>
170
+ </Progress>
153
171
  ```
154
172
 
155
173
  `Badge` variants are `neutral`, `accent`, `success`, `warning`, `danger` and `info`. `Separator` has horizontal/vertical orientation and a decorative opt-out. `Skeleton` is a static, aria-hidden placeholder; mark its loading container `aria-busy`. `Progress` clamps numeric values to `0..max` (default 100); `null` is indeterminate without a fabricated percentage. Always provide a progress label.
156
174
 
157
175
  `Badge.size` accepts `sm | md | lg` with optional `startIcon` and `endIcon`. `Separator.weight` clamps to 0.3–2 (default 0.9). `Skeleton.radius` uses the existing 0–8px pencil corner contract. `Progress.size` selects a 12/16/24px track (`sm/md/lg`); `dir`, `aria-valuetext`, refs and native DOM attributes remain available. Tooltip exposes the underlying delay, alignment, collision and hoverable-content controls alongside `Content.surfaceRadius`.
158
176
 
177
+ Circular Progress uses a grainy pencil arc with naturally tapered ends and an
178
+ empty gap for the remaining progress.
179
+ `size` selects a 64/96/128px diameter; `diameter` overrides it (32-512px), and
180
+ `thickness` sets the base stroke width (1-diameter/8px, affected by global stroke
181
+ settings). `children` accepts non-interactive icons/text or a render function
182
+ receiving normalized `{ value, max, percent }`; unknown value/percent are null.
183
+ `contentPosition` supports `center`, `top`, `bottom`, `start`, and `end`; logical
184
+ sides follow RTL. Circular defaults to center, linear to end (including explicit
185
+ center). Keep center content short; longer descriptions belong outside the ring.
186
+ Always supply a label or ARIA name, even for icon content. Progress updates reveal
187
+ the existing texture; only unknown progress rotates, respecting reduced motion.
188
+ Canvas failures, forced colors and printing retain an SVG progress fallback.
189
+
159
190
  ## Menus And Content
160
191
 
161
192
  `DropdownMenu` exposes `Root`, `Trigger`, `Portal`, `Content`, `Item`, `CheckboxItem`, `RadioGroup`, `RadioItem`, `Group`, `Label` and `Separator`. It supports controlled open state, placement/collision props, disabled items, keyboard navigation and focus return. Use `Trigger asChild` with a Button; use `Item.destructive` for dangerous actions. Portal content inherits the trigger theme. Submenus are not included yet.
@@ -176,6 +207,133 @@ Content layout options preserve existing defaults:
176
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).
177
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.
178
209
 
210
+ ## Additional Components
211
+
212
+ These exports extend the original core component set. The documentation playground includes
213
+ working examples, live property tabs and per-component API/accessibility notes.
214
+
215
+ | Component | Contract |
216
+ | --- | --- |
217
+ | Toast | `Provider`, `useToastManager`, `createToastManager`; add/update/close/promise notifications, named actions and timer pause managed by Base UI |
218
+ | AlertDialog | Always-modal confirmation; `Root`, `Trigger`, `Portal`, `Backdrop`, `Content`, `Header`, `Body`, `Footer`, `Title`, `Description`, `Cancel`, `Action`; pending blocks user dismissal, async actions use `closeOnAction={false}` |
219
+ | Drawer | Dialog parts plus section layout, four `Content.direction` values and token/custom `size`; focus trap, Escape and return focus, no swipe gesture API |
220
+ | DatePicker | Calendar/Popover composition; single/range local dates, controlled/default values, clear, form fields and localized labels; no UTC conversion |
221
+ | TimePicker | Native time input with clear, disabled/readOnly, min/max/step and optional seconds; browser/OS controls the native presentation |
222
+ | Pagination | Page count/current page, boundary actions and ellipses; buttons change local state rather than pretending to navigate URLs |
223
+ | Empty | Optional `Root`, `Icon`, `Title`, `Description`, `Actions`; no mandatory card or live-region role |
224
+ | ToggleGroup | `Root` and `Item`; array values for single/multiple toggles, roving focus and `aria-pressed`, not tabs |
225
+ | Command | cmdk-backed search, grouped commands, empty/disabled states and optional modal composition |
226
+ | Upload | File selection/drop, type/size/count rejection, removal, progress, cancel and retry; application transport receives `AbortSignal` and `reportProgress` |
227
+ | DataTable | Typed columns, required stable `getRowId`, sorting, filtering, pagination, selection, empty/loading state; native table semantics |
228
+ | Tree | `Root`, `Item`, `ItemContent`, `ItemTitle`, `ItemDescription`; stable keys, expansion and selection via React Aria; no drag/drop |
229
+ | Steps | Ordered `items`, `current`, orientation and optional `onStepChange`; status and disabled steps |
230
+ | Timeline | Ordered `items` with date/title/description/content, semantic `dateTime` and horizontal/vertical orientation |
231
+
232
+ ## Utility Components (Unreleased)
233
+
234
+ `Slot` is a fixed square visual container for arbitrary icons or content. Static
235
+ slots render a `div`; `interactive` slots render one native button with
236
+ `aria-pressed` selection and native Enter/Space behavior. `empty` preserves
237
+ geometry, `count` reuses Badge, and `renderCount` keeps custom rendering at the
238
+ logical corner. Keep nested controls out of interactive slots. Slot does not
239
+ implement equipment rules, rarity, sorting or drag and drop.
240
+
241
+ `Hotkey` formats shortcut values for Mac or Windows. `HotkeyInput` records only
242
+ while its native input is focused, supports controlled/uncontrolled values,
243
+ clear, Escape cancellation, read-only/disabled states, IME safeguards and
244
+ caller-owned conflict validation. It does not register global shortcut listeners
245
+ or suppress browser shortcuts.
246
+
247
+ `NumberTicker` formats values with `Intl.NumberFormat`, optionally animates from
248
+ the currently displayed frame, and can show a temporary signed delta. Reduced
249
+ motion and SSR show deterministic final values. Intermediate frames are visual;
250
+ the polite announcement updates only at the final value. Use `getValueLabel`
251
+ when units or custom rendering need a complete accessible phrase.
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
+
324
+ `Log` renders caller-ordered records with stable ids, optional level filters and
325
+ pause/follow controls. It follows only append updates while the viewport remains
326
+ near the bottom. The named scroll region supports Home/End and intentionally is
327
+ not a live region, so a long stream is not repeatedly announced. Log does not
328
+ provide persistence, virtualization or transport.
329
+
330
+ Upload does not send files anywhere unless the application supplies `onUpload`.
331
+ Without a transport, files remain selected, not successfully uploaded. Client
332
+ validation is a convenience; the receiving server must validate files itself.
333
+ DataTable does not replace `Table` and does not promise virtualization, cell
334
+ editing or server-side transport. Tree is not a file-system permission boundary.
335
+ All examples use public sample data.
336
+
179
337
  ## Travel Components
180
338
 
181
339
  `Calendar` wraps React DayPicker with default `appearance="itinerary"` day cards and optional `appearance="picker"`. It retains single/multiple/range selection, month control, disabled matchers, locale and keyboard navigation. Supply `selected` and `onSelect` for controlled selection. Events have independent `onEventSelect` handling; `displayRange` and `renderDayAccessory` customize the itinerary. Narrow itinerary grids retain seven columns and hide event text while preserving accessible names; picker targets remain at least 44px.
@@ -199,6 +357,33 @@ Content layout options preserve existing defaults:
199
357
  <PencilLine orientation="vertical" seed="sidebar-divider" className="sidebar-divider" />
200
358
  ```
201
359
 
360
+ ### Rendering tokens
361
+
362
+ `PencilText` reads `--inkra-letter-variation`, `--inkra-letter-breaks`,
363
+ `--inkra-letter-outline`, `--inkra-letter-fill`, and `--inkra-letter-grain` from
364
+ its computed style. Values are clamped to 0-1; missing or invalid values use the
365
+ default lettering preset (0.20, 0.50, 0.90, 0.90, 0.60 respectively). Apply these tokens to an ancestor to customize all
366
+ text within that scope without changing layout, selectable text, or labels.
367
+
368
+ `PencilLine` reads `--inkra-stroke-width` (0.3-3) and
369
+ `--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.
372
+ The default values are 2 and 1.5. They do not change layout or text lettering.
373
+
374
+ Surface fills read `--inkra-wash-strength` (0-1, default 0.8),
375
+ `--inkra-wash-spacing` (1-6, default 2), `--inkra-wash-roughness` (0-6,
376
+ default 3), and `--inkra-wash-angle` (-85 to 85 degrees, default -45).
377
+ Strength scales each variant's original light/dark pigment intensity relative to
378
+ the original 0.48 reference. These settings affect button fills, selections
379
+ and swatches, not paper or lettering. Selection reveals follow the chosen angle.
380
+
381
+ Only self/ancestor appearance changes invalidate decoration, not unrelated theme
382
+ subtrees. Unchanged computed paint settings are reused, including when a dialog
383
+ changes body pointer events or scroll locks. Rendering settings are part of the
384
+ bounded texture cache keys. No page remount is needed. Forced-color, print, and
385
+ unavailable-canvas fallbacks remain intact.
386
+
202
387
  ## Font license
203
388
 
204
389
  The package includes `Trip Map Hand`, a modified web subset of Yozai Light v0.868, under the SIL Open Font License 1.1. The font is not covered by Inkra's MIT software license. See `THIRD_PARTY_NOTICES.md` and `Yozai-OFL.txt` in the package.
@@ -4,6 +4,7 @@ import { BoundedCache, PENCIL_RENDERER_VERSION, canvasByteCost, canvasIsUsable,
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
+ import { maxStrokeRoughness, pencilRenderDefaults, readRenderToken } from './internal/renderTokens.js';
7
8
  const geometryCache = new BoundedCache(256, 180000);
8
9
  const imageCache = new BoundedCache(128, 24 * 1024 * 1024);
9
10
  function cachedPathParts(data) {
@@ -45,6 +46,7 @@ function drawFallbackPath(context, data, color, mode, weightScale = 1) {
45
46
  function renderIcon(paths, options) {
46
47
  const { seed, size, sourceSize, active, color, dpr } = options;
47
48
  const weightScale = options.weightScale ?? 1;
49
+ const roughnessScale = options.roughnessScale ?? 1;
48
50
  const canvas = document.createElement('canvas');
49
51
  canvas.width = Math.round(size * dpr);
50
52
  canvas.height = Math.round(size * dpr);
@@ -76,7 +78,7 @@ function renderIcon(paths, options) {
76
78
  try {
77
79
  const parts = cachedPathParts(path.d);
78
80
  for (const points of parts) {
79
- pencilStroke(context, points, color, (mode === 'scuff' ? 0.55 : 1.65) * weightScale, pathSeed, 0.3, mode === 'scuff' ? 1 : 3, false, {
81
+ pencilStroke(context, points, color, (mode === 'scuff' ? 0.55 : 1.65) * weightScale, pathSeed, 0.3 * roughnessScale, mode === 'scuff' ? 1 : 3, false, {
80
82
  variation: 0.82,
81
83
  breaks: 0.18,
82
84
  grain: 0.68,
@@ -91,7 +93,7 @@ function renderIcon(paths, options) {
91
93
  }
92
94
  });
93
95
  if (active) {
94
- pencilStroke(context, [[sourceSize * 0.2, sourceSize - 2], [sourceSize * 0.5, sourceSize - 1.5], [sourceSize * 0.8, sourceSize - 2.2]], color, 1.1, seed + 977, 0.25, 3, false, { variation: 0.8, breaks: 0.25, grain: 0.7, gain: 2.8, step: 0.35 });
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 });
95
97
  }
96
98
  if (options.badge?.paths.length) {
97
99
  const badge = options.badge;
@@ -104,7 +106,7 @@ function renderIcon(paths, options) {
104
106
  const y = position.startsWith('top') ? 0 : 40 - extent;
105
107
  const badgeImage = renderIcon(badge.paths, {
106
108
  seed: seed + 1543, size: size * scale, sourceSize: badgeSourceSize, active: false, color, dpr,
107
- weightScale: 1 / scale,
109
+ weightScale: weightScale / scale, roughnessScale,
108
110
  });
109
111
  const mask = document.createElement('canvas');
110
112
  mask.width = badgeImage.width;
@@ -135,6 +137,7 @@ function renderIcon(paths, options) {
135
137
  }
136
138
  export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge, className = '', decorative = true, paths, seed, size = 40, sourceSize = 32, ...props }, forwardedRef) {
137
139
  const canvasRef = useRef(null);
140
+ const lastPaintRef = useRef('');
138
141
  useImperativeHandle(forwardedRef, () => canvasRef.current);
139
142
  const geometryKey = useMemo(() => JSON.stringify(paths), [paths]);
140
143
  const badgeKey = useMemo(() => badge ? JSON.stringify(badge) : '', [badge]);
@@ -144,7 +147,6 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
144
147
  if (!canvas)
145
148
  return undefined;
146
149
  let disposed = false;
147
- let lastPaint = '';
148
150
  const paint = () => {
149
151
  if (disposed)
150
152
  return;
@@ -152,6 +154,8 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
152
154
  const computed = getComputedStyle(canvas);
153
155
  const color = computed.color;
154
156
  const rtl = computed.direction === 'rtl';
157
+ const weightScale = readRenderToken(computed, '--inkra-stroke-width', pencilRenderDefaults.strokeWidth, 0.3, 3) / 1.2;
158
+ const roughnessScale = readRenderToken(computed, '--inkra-stroke-amplitude', pencilRenderDefaults.strokeRoughness, 0, maxStrokeRoughness) / 0.6;
155
159
  const key = JSON.stringify([
156
160
  PENCIL_RENDERER_VERSION,
157
161
  geometryKey,
@@ -163,8 +167,9 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
163
167
  active,
164
168
  color,
165
169
  dpr,
170
+ weightScale, roughnessScale,
166
171
  ]);
167
- if (key === lastPaint && canvasIsUsable(canvas))
172
+ if (key === lastPaintRef.current && canvasIsUsable(canvas))
168
173
  return;
169
174
  let image = imageCache.get(key);
170
175
  if (image && !canvasIsUsable(image)) {
@@ -172,7 +177,7 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
172
177
  image = undefined;
173
178
  }
174
179
  if (!image) {
175
- image = renderIcon(paths, { seed: resolvedSeed, size, sourceSize, active, color, dpr, badge, rtl });
180
+ image = renderIcon(paths, { seed: resolvedSeed, size, sourceSize, active, color, dpr, badge, rtl, weightScale, roughnessScale });
176
181
  imageCache.set(key, image, canvasByteCost(image));
177
182
  }
178
183
  canvas.width = image.width;
@@ -181,26 +186,19 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
181
186
  if (!context || context.isContextLost?.())
182
187
  return;
183
188
  context.drawImage(image, 0, 0);
184
- lastPaint = key;
189
+ lastPaintRef.current = key;
185
190
  };
186
- const appearance = new MutationObserver(paint);
187
- appearance.observe(document.documentElement, {
188
- attributes: true,
189
- subtree: true,
190
- attributeFilter: ['data-theme', 'data-inkra-theme'],
191
- });
192
191
  const inheritedColor = new MutationObserver(paint);
193
192
  for (let element = canvas; element; element = element.parentElement) {
194
- inheritedColor.observe(element, { attributes: true, attributeFilter: ['style', 'class', 'dir'] });
193
+ inheritedColor.observe(element, { attributes: true, attributeFilter: ['style', 'class', 'dir', 'data-theme', 'data-inkra-theme'] });
195
194
  }
196
195
  const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
197
196
  colorScheme.addEventListener('change', paint);
198
- const stopRecovery = observeCanvasRecovery(() => { lastPaint = ''; paint(); });
197
+ const stopRecovery = observeCanvasRecovery(() => { lastPaintRef.current = ''; paint(); });
199
198
  paint();
200
199
  return () => {
201
200
  disposed = true;
202
201
  stopRecovery();
203
- appearance.disconnect();
204
202
  inheritedColor.disconnect();
205
203
  colorScheme.removeEventListener('change', paint);
206
204
  };
@@ -5,6 +5,7 @@ import { observeCanvasRecovery } from './internal/recovery.js';
5
5
  import { seedToNumber } from './internal/random.js';
6
6
  import { createPencilLinePoints, pencilLineHasDrawableSize } from './internal/line.js';
7
7
  import { pencilStroke } from './internal/stroke.js';
8
+ import { maxStrokeRoughness, pencilRenderDefaults, readRenderToken } from './internal/renderTokens.js';
8
9
  const lineCache = new BoundedCache(96, 12 * 1024 * 1024);
9
10
  function renderLine(width, height, color, seed, weight, roughness, orientation, dpr) {
10
11
  const canvas = document.createElement('canvas');
@@ -17,7 +18,7 @@ function renderLine(width, height, color, seed, weight, roughness, orientation,
17
18
  pencilStroke(context, createPencilLinePoints(width, height, orientation, roughness), color, weight, seed, roughness, 3, false, { variation: 0.82, breaks: 0.18, grain: 0.68, gain: 2.6, step: 0.5, taperLength: 1.4 });
18
19
  return canvas;
19
20
  }
20
- export const PencilLine = forwardRef(function PencilLine({ className = '', orientation = 'horizontal', roughness = 0.6, seed = 'inkra-pencil-line', weight = 1.2, ...props }, forwardedRef) {
21
+ export const PencilLine = forwardRef(function PencilLine({ className = '', orientation = 'horizontal', roughness, seed = 'inkra-pencil-line', weight, ...props }, forwardedRef) {
21
22
  const canvasRef = useRef(null);
22
23
  useImperativeHandle(forwardedRef, () => canvasRef.current);
23
24
  useEffect(() => {
@@ -26,6 +27,7 @@ export const PencilLine = forwardRef(function PencilLine({ className = '', orien
26
27
  return undefined;
27
28
  let frame = 0;
28
29
  let disposed = false;
30
+ let lastPaint = '';
29
31
  const paint = () => {
30
32
  frame = 0;
31
33
  if (disposed)
@@ -35,18 +37,23 @@ export const PencilLine = forwardRef(function PencilLine({ className = '', orien
35
37
  if (!pencilLineHasDrawableSize(width, height, orientation))
36
38
  return;
37
39
  const dpr = effectiveDpr(2);
38
- const color = getComputedStyle(canvas).color;
40
+ const style = getComputedStyle(canvas);
41
+ const color = style.color;
42
+ const resolvedWeight = weight ?? readRenderToken(style, '--inkra-stroke-width', pencilRenderDefaults.strokeWidth, 0.3, 3);
43
+ const resolvedRoughness = roughness ?? readRenderToken(style, '--inkra-stroke-amplitude', pencilRenderDefaults.strokeRoughness, 0, maxStrokeRoughness);
39
44
  const numericSeed = seedToNumber(seed);
40
45
  const key = JSON.stringify([
41
- PENCIL_RENDERER_VERSION, orientation, width, height, color, numericSeed, weight, roughness, dpr,
46
+ PENCIL_RENDERER_VERSION, orientation, width, height, color, numericSeed, resolvedWeight, resolvedRoughness, dpr,
42
47
  ]);
48
+ if (key === lastPaint && canvasIsUsable(canvas))
49
+ return;
43
50
  let image = lineCache.get(key);
44
51
  if (image && !canvasIsUsable(image)) {
45
52
  lineCache.delete(key);
46
53
  image = undefined;
47
54
  }
48
55
  if (!image) {
49
- image = renderLine(width, height, color, numericSeed, weight, roughness, orientation, dpr);
56
+ image = renderLine(width, height, color, numericSeed, resolvedWeight, resolvedRoughness, orientation, dpr);
50
57
  lineCache.set(key, image, canvasByteCost(image));
51
58
  }
52
59
  canvas.width = image.width;
@@ -55,6 +62,7 @@ export const PencilLine = forwardRef(function PencilLine({ className = '', orien
55
62
  if (!context || context.isContextLost?.())
56
63
  return;
57
64
  context.drawImage(image, 0, 0);
65
+ lastPaint = key;
58
66
  };
59
67
  const schedule = () => {
60
68
  if (!frame)
@@ -62,21 +70,19 @@ export const PencilLine = forwardRef(function PencilLine({ className = '', orien
62
70
  };
63
71
  const resize = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
64
72
  resize?.observe(canvas);
65
- const appearance = new MutationObserver(schedule);
66
- appearance.observe(document.documentElement, {
67
- attributes: true,
68
- subtree: true,
69
- attributeFilter: ['data-theme', 'data-inkra-theme'],
70
- });
71
73
  const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
74
+ const inheritedStyle = new MutationObserver(schedule);
75
+ for (let element = canvas; element; element = element.parentElement) {
76
+ inheritedStyle.observe(element, { attributes: true, attributeFilter: ['style', 'class', 'data-theme', 'data-inkra-theme'] });
77
+ }
72
78
  colorScheme.addEventListener('change', schedule);
73
- const stopRecovery = observeCanvasRecovery(schedule);
79
+ const stopRecovery = observeCanvasRecovery(() => { lastPaint = ''; schedule(); });
74
80
  paint();
75
81
  return () => {
76
82
  disposed = true;
77
83
  stopRecovery();
78
84
  resize?.disconnect();
79
- appearance.disconnect();
85
+ inheritedStyle.disconnect();
80
86
  colorScheme.removeEventListener('change', schedule);
81
87
  cancelAnimationFrame(frame);
82
88
  };