@inkra/react 0.1.0 → 0.2.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 (160) hide show
  1. package/README.md +104 -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 +109 -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 +23 -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 +240 -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 +86 -0
  35. package/dist/editable/index.d.ts +53 -0
  36. package/dist/editable/index.js +162 -0
  37. package/dist/empty/index.d.ts +21 -0
  38. package/dist/empty/index.js +23 -0
  39. package/dist/flip/index.d.ts +49 -0
  40. package/dist/flip/index.js +138 -0
  41. package/dist/form/EditablePencilText.js +157 -146
  42. package/dist/form/Textarea.d.ts +12 -0
  43. package/dist/form/Textarea.js +83 -4
  44. package/dist/form/index.d.ts +1 -1
  45. package/dist/heatmap/index.d.ts +59 -0
  46. package/dist/heatmap/index.js +248 -0
  47. package/dist/hotkey/index.d.ts +37 -0
  48. package/dist/hotkey/index.js +130 -0
  49. package/dist/hotkey/model.d.ts +27 -0
  50. package/dist/hotkey/model.js +97 -0
  51. package/dist/hover-card/index.d.ts +33 -0
  52. package/dist/hover-card/index.js +141 -0
  53. package/dist/image-cropper/index.d.ts +82 -0
  54. package/dist/image-cropper/index.js +261 -0
  55. package/dist/image-cropper/model.d.ts +42 -0
  56. package/dist/image-cropper/model.js +94 -0
  57. package/dist/image-viewer/index.d.ts +79 -0
  58. package/dist/image-viewer/index.js +220 -0
  59. package/dist/image-viewer/model.d.ts +9 -0
  60. package/dist/image-viewer/model.js +18 -0
  61. package/dist/index.d.ts +47 -0
  62. package/dist/index.js +47 -0
  63. package/dist/internal/cache.d.ts +1 -1
  64. package/dist/internal/cache.js +1 -1
  65. package/dist/internal/renderTokens.d.ts +10 -0
  66. package/dist/internal/renderTokens.js +13 -0
  67. package/dist/internal/surfaceRenderer.d.ts +14 -6
  68. package/dist/internal/surfaceRenderer.js +15 -12
  69. package/dist/internal/textRenderer.d.ts +1 -0
  70. package/dist/internal/textRenderer.js +33 -7
  71. package/dist/internal/usePencilSurface.js +16 -8
  72. package/dist/internal/useSelectionDrawing.js +15 -11
  73. package/dist/journey-map/MapScale.js +3 -1
  74. package/dist/journey-map/index.js +3 -1
  75. package/dist/keypad/index.d.ts +23 -0
  76. package/dist/keypad/index.js +63 -0
  77. package/dist/keypad/model.d.ts +20 -0
  78. package/dist/keypad/model.js +33 -0
  79. package/dist/layout/index.d.ts +63 -0
  80. package/dist/layout/index.js +62 -0
  81. package/dist/log/index.d.ts +45 -0
  82. package/dist/log/index.js +159 -0
  83. package/dist/log/model.d.ts +16 -0
  84. package/dist/log/model.js +13 -0
  85. package/dist/node-graph/index.d.ts +123 -0
  86. package/dist/node-graph/index.js +304 -0
  87. package/dist/number-ticker/index.d.ts +29 -0
  88. package/dist/number-ticker/index.js +132 -0
  89. package/dist/number-ticker/model.d.ts +8 -0
  90. package/dist/number-ticker/model.js +34 -0
  91. package/dist/overflow-list/index.d.ts +39 -0
  92. package/dist/overflow-list/index.js +230 -0
  93. package/dist/overflow-list/layout.d.ts +19 -0
  94. package/dist/overflow-list/layout.js +44 -0
  95. package/dist/pagination/index.d.ts +23 -0
  96. package/dist/pagination/index.js +41 -0
  97. package/dist/pagination/model.d.ts +4 -0
  98. package/dist/pagination/model.js +42 -0
  99. package/dist/pan-zoom/index.d.ts +71 -0
  100. package/dist/pan-zoom/index.js +175 -0
  101. package/dist/radar/index.d.ts +54 -0
  102. package/dist/radar/index.js +220 -0
  103. package/dist/range-slider/index.d.ts +16 -0
  104. package/dist/range-slider/index.js +22 -0
  105. package/dist/rating/index.d.ts +28 -0
  106. package/dist/rating/index.js +127 -0
  107. package/dist/resizable/index.d.ts +32 -0
  108. package/dist/resizable/index.js +50 -0
  109. package/dist/scroll-area/index.d.ts +36 -0
  110. package/dist/scroll-area/index.js +44 -0
  111. package/dist/segmented-progress/SegmentedProgress.d.ts +17 -0
  112. package/dist/segmented-progress/SegmentedProgress.js +42 -0
  113. package/dist/slider/index.js +1 -1
  114. package/dist/slot/index.d.ts +24 -0
  115. package/dist/slot/index.js +29 -0
  116. package/dist/sortable/index.d.ts +50 -0
  117. package/dist/sortable/index.js +278 -0
  118. package/dist/sortable/model.d.ts +30 -0
  119. package/dist/sortable/model.js +64 -0
  120. package/dist/sparkline/index.d.ts +19 -0
  121. package/dist/sparkline/index.js +240 -0
  122. package/dist/sparkline/model.d.ts +16 -0
  123. package/dist/sparkline/model.js +28 -0
  124. package/dist/steps/index.d.ts +16 -0
  125. package/dist/steps/index.js +37 -0
  126. package/dist/styles.css +4850 -287
  127. package/dist/tag-input/index.d.ts +69 -0
  128. package/dist/tag-input/index.js +325 -0
  129. package/dist/tag-input/model.d.ts +14 -0
  130. package/dist/tag-input/model.js +38 -0
  131. package/dist/time-picker/index.d.ts +15 -0
  132. package/dist/time-picker/index.js +168 -0
  133. package/dist/timeline/index.d.ts +14 -0
  134. package/dist/timeline/index.js +16 -0
  135. package/dist/toast/index.d.ts +27 -0
  136. package/dist/toast/index.js +36 -0
  137. package/dist/toggle-group/index.d.ts +17 -0
  138. package/dist/toggle-group/index.js +26 -0
  139. package/dist/tokens.css +10 -9
  140. package/dist/transfer/index.d.ts +64 -0
  141. package/dist/transfer/index.js +174 -0
  142. package/dist/transfer/model.d.ts +12 -0
  143. package/dist/transfer/model.js +33 -0
  144. package/dist/tree/index.d.ts +39 -0
  145. package/dist/tree/index.js +36 -0
  146. package/dist/tree-select/index.d.ts +59 -0
  147. package/dist/tree-select/index.js +142 -0
  148. package/dist/tree-select/model.d.ts +31 -0
  149. package/dist/tree-select/model.js +135 -0
  150. package/dist/typewriter/index.d.ts +21 -0
  151. package/dist/typewriter/index.js +101 -0
  152. package/dist/typewriter/model.d.ts +7 -0
  153. package/dist/typewriter/model.js +45 -0
  154. package/dist/upload/index.d.ts +59 -0
  155. package/dist/upload/index.js +230 -0
  156. package/dist/virtual-list/index.d.ts +44 -0
  157. package/dist/virtual-list/index.js +128 -0
  158. package/dist/virtual-list/model.d.ts +7 -0
  159. package/dist/virtual-list/model.js +27 -0
  160. package/package.json +22 -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. Version `0.1.0` is published; the component expansion described below is available only in the current, unpublished source candidate.
4
4
 
5
5
  Source and maintenance: [inkrajs/inkra](https://github.com/inkrajs/inkra).
6
6
 
@@ -150,12 +150,31 @@ outside interaction, modal focus trapping and focus return.
150
150
  <Skeleton style={{ width: 200, height: 20 }} />
151
151
  <Progress label="Uploading" value={60} />
152
152
  <Progress aria-label="Loading" value={null} />
153
+ <Progress shape="circular" value={72} diameter={128} thickness={6} aria-label="Download">
154
+ {({ percent }) => `${Math.round(percent ?? 0)}%`}
155
+ </Progress>
156
+ <Progress shape="circular" value={3} max={5} contentPosition="end" label="Files">
157
+ <span>3 / 5 files</span>
158
+ </Progress>
153
159
  ```
154
160
 
155
161
  `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
162
 
157
163
  `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
164
 
165
+ Circular Progress uses a grainy pencil arc with naturally tapered ends and an
166
+ empty gap for the remaining progress.
167
+ `size` selects a 64/96/128px diameter; `diameter` overrides it (32-512px), and
168
+ `thickness` sets the base stroke width (1-diameter/8px, affected by global stroke
169
+ settings). `children` accepts non-interactive icons/text or a render function
170
+ receiving normalized `{ value, max, percent }`; unknown value/percent are null.
171
+ `contentPosition` supports `center`, `top`, `bottom`, `start`, and `end`; logical
172
+ sides follow RTL. Circular defaults to center, linear to end (including explicit
173
+ center). Keep center content short; longer descriptions belong outside the ring.
174
+ Always supply a label or ARIA name, even for icon content. Progress updates reveal
175
+ the existing texture; only unknown progress rotates, respecting reduced motion.
176
+ Canvas failures, forced colors and printing retain an SVG progress fallback.
177
+
159
178
  ## Menus And Content
160
179
 
161
180
  `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 +195,63 @@ Content layout options preserve existing defaults:
176
195
  - `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
196
  - `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
197
 
198
+ ## Component Expansion (Unreleased)
199
+
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
202
+ working examples, live property tabs and per-component API/accessibility notes.
203
+
204
+ | Component | Contract |
205
+ | --- | --- |
206
+ | Toast | `Provider`, `useToastManager`, `createToastManager`; add/update/close/promise notifications, named actions and timer pause managed by Base UI |
207
+ | 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}` |
208
+ | Drawer | Dialog parts plus section layout, four `Content.direction` values and token/custom `size`; focus trap, Escape and return focus, no swipe gesture API |
209
+ | DatePicker | Calendar/Popover composition; single/range local dates, controlled/default values, clear, form fields and localized labels; no UTC conversion |
210
+ | TimePicker | Native time input with clear, disabled/readOnly, min/max/step and optional seconds; browser/OS controls the native presentation |
211
+ | Pagination | Page count/current page, boundary actions and ellipses; buttons change local state rather than pretending to navigate URLs |
212
+ | Empty | Optional `Root`, `Icon`, `Title`, `Description`, `Actions`; no mandatory card or live-region role |
213
+ | ToggleGroup | `Root` and `Item`; array values for single/multiple toggles, roving focus and `aria-pressed`, not tabs |
214
+ | Command | cmdk-backed search, grouped commands, empty/disabled states and optional modal composition |
215
+ | Upload | File selection/drop, type/size/count rejection, removal, progress, cancel and retry; application transport receives `AbortSignal` and `reportProgress` |
216
+ | DataTable | Typed columns, required stable `getRowId`, sorting, filtering, pagination, selection, empty/loading state; native table semantics |
217
+ | Tree | `Root`, `Item`, `ItemContent`, `ItemTitle`, `ItemDescription`; stable keys, expansion and selection via React Aria; no drag/drop |
218
+ | Steps | Ordered `items`, `current`, orientation and optional `onStepChange`; status and disabled steps |
219
+ | Timeline | Ordered `items` with date/title/description/content, semantic `dateTime` and horizontal/vertical orientation |
220
+
221
+ ## Utility Components (Unreleased)
222
+
223
+ `Slot` is a fixed square visual container for arbitrary icons or content. Static
224
+ slots render a `div`; `interactive` slots render one native button with
225
+ `aria-pressed` selection and native Enter/Space behavior. `empty` preserves
226
+ geometry, `count` reuses Badge, and `renderCount` keeps custom rendering at the
227
+ logical corner. Keep nested controls out of interactive slots. Slot does not
228
+ implement equipment rules, rarity, sorting or drag and drop.
229
+
230
+ `Hotkey` formats shortcut values for Mac or Windows. `HotkeyInput` records only
231
+ while its native input is focused, supports controlled/uncontrolled values,
232
+ clear, Escape cancellation, read-only/disabled states, IME safeguards and
233
+ caller-owned conflict validation. It does not register global shortcut listeners
234
+ or suppress browser shortcuts.
235
+
236
+ `NumberTicker` formats values with `Intl.NumberFormat`, optionally animates from
237
+ the currently displayed frame, and can show a temporary signed delta. Reduced
238
+ motion and SSR show deterministic final values. Intermediate frames are visual;
239
+ the polite announcement updates only at the final value. Use `getValueLabel`
240
+ when units or custom rendering need a complete accessible phrase.
241
+
242
+ `Log` renders caller-ordered records with stable ids, optional level filters and
243
+ pause/follow controls. It follows only append updates while the viewport remains
244
+ near the bottom. The named scroll region supports Home/End and intentionally is
245
+ not a live region, so a long stream is not repeatedly announced. Log does not
246
+ provide persistence, virtualization or transport.
247
+
248
+ Upload does not send files anywhere unless the application supplies `onUpload`.
249
+ Without a transport, files remain selected, not successfully uploaded. Client
250
+ validation is a convenience; the receiving server must validate files itself.
251
+ DataTable does not replace `Table` and does not promise virtualization, cell
252
+ editing or server-side transport. Tree is not a file-system permission boundary.
253
+ All examples use public sample data.
254
+
179
255
  ## Travel Components
180
256
 
181
257
  `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 +275,33 @@ Content layout options preserve existing defaults:
199
275
  <PencilLine orientation="vertical" seed="sidebar-divider" className="sidebar-divider" />
200
276
  ```
201
277
 
278
+ ### Rendering tokens
279
+
280
+ `PencilText` reads `--inkra-letter-variation`, `--inkra-letter-breaks`,
281
+ `--inkra-letter-outline`, `--inkra-letter-fill`, and `--inkra-letter-grain` from
282
+ its computed style. Values are clamped to 0-1; missing or invalid values use the
283
+ default lettering preset (0.20, 0.50, 0.90, 0.90, 0.60 respectively). Apply these tokens to an ancestor to customize all
284
+ text within that scope without changing layout, selectable text, or labels.
285
+
286
+ `PencilLine` reads `--inkra-stroke-width` (0.3-3) and
287
+ `--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.
290
+ The default values are 2 and 1.5. They do not change layout or text lettering.
291
+
292
+ Surface fills read `--inkra-wash-strength` (0-1, default 0.8),
293
+ `--inkra-wash-spacing` (1-6, default 2), `--inkra-wash-roughness` (0-6,
294
+ default 3), and `--inkra-wash-angle` (-85 to 85 degrees, default -45).
295
+ Strength scales each variant's original light/dark pigment intensity relative to
296
+ the original 0.48 reference. These settings affect button fills, selections
297
+ and swatches, not paper or lettering. Selection reveals follow the chosen angle.
298
+
299
+ Only self/ancestor appearance changes invalidate decoration, not unrelated theme
300
+ subtrees. Unchanged computed paint settings are reused, including when a dialog
301
+ changes body pointer events or scroll locks. Rendering settings are part of the
302
+ bounded texture cache keys. No page remount is needed. Forced-color, print, and
303
+ unavailable-canvas fallbacks remain intact.
304
+
202
305
  ## Font license
203
306
 
204
307
  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
  };