@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.
- package/README.md +104 -1
- package/dist/PencilIcon.js +14 -16
- package/dist/PencilLine.js +18 -12
- package/dist/PencilText.js +192 -59
- package/dist/alert-dialog/index.d.ts +64 -0
- package/dist/alert-dialog/index.js +109 -0
- package/dist/audio-player/index.d.ts +60 -0
- package/dist/audio-player/index.js +101 -0
- package/dist/carousel/index.d.ts +51 -0
- package/dist/carousel/index.js +290 -0
- package/dist/choice-grid/index.d.ts +38 -0
- package/dist/choice-grid/index.js +85 -0
- package/dist/color-picker/color.d.ts +15 -0
- package/dist/color-picker/color.js +55 -0
- package/dist/color-picker/index.d.ts +31 -0
- package/dist/color-picker/index.js +69 -0
- package/dist/command/index.d.ts +23 -0
- package/dist/command/index.js +39 -0
- package/dist/context-menu/index.d.ts +76 -0
- package/dist/context-menu/index.js +240 -0
- package/dist/data-table/index.d.ts +47 -0
- package/dist/data-table/index.js +91 -0
- package/dist/date-picker/index.d.ts +50 -0
- package/dist/date-picker/index.js +180 -0
- package/dist/display/ProgressRing.d.ts +6 -0
- package/dist/display/ProgressRing.js +146 -0
- package/dist/display/index.d.ts +27 -0
- package/dist/display/index.js +18 -2
- package/dist/dockable-panel/index.d.ts +41 -0
- package/dist/dockable-panel/index.js +253 -0
- package/dist/dockable-panel/model.d.ts +21 -0
- package/dist/dockable-panel/model.js +61 -0
- package/dist/drawer/index.d.ts +63 -0
- package/dist/drawer/index.js +86 -0
- package/dist/editable/index.d.ts +53 -0
- package/dist/editable/index.js +162 -0
- package/dist/empty/index.d.ts +21 -0
- package/dist/empty/index.js +23 -0
- package/dist/flip/index.d.ts +49 -0
- package/dist/flip/index.js +138 -0
- package/dist/form/EditablePencilText.js +157 -146
- package/dist/form/Textarea.d.ts +12 -0
- package/dist/form/Textarea.js +83 -4
- package/dist/form/index.d.ts +1 -1
- package/dist/heatmap/index.d.ts +59 -0
- package/dist/heatmap/index.js +248 -0
- package/dist/hotkey/index.d.ts +37 -0
- package/dist/hotkey/index.js +130 -0
- package/dist/hotkey/model.d.ts +27 -0
- package/dist/hotkey/model.js +97 -0
- package/dist/hover-card/index.d.ts +33 -0
- package/dist/hover-card/index.js +141 -0
- package/dist/image-cropper/index.d.ts +82 -0
- package/dist/image-cropper/index.js +261 -0
- package/dist/image-cropper/model.d.ts +42 -0
- package/dist/image-cropper/model.js +94 -0
- package/dist/image-viewer/index.d.ts +79 -0
- package/dist/image-viewer/index.js +220 -0
- package/dist/image-viewer/model.d.ts +9 -0
- package/dist/image-viewer/model.js +18 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +47 -0
- package/dist/internal/cache.d.ts +1 -1
- package/dist/internal/cache.js +1 -1
- package/dist/internal/renderTokens.d.ts +10 -0
- package/dist/internal/renderTokens.js +13 -0
- package/dist/internal/surfaceRenderer.d.ts +14 -6
- package/dist/internal/surfaceRenderer.js +15 -12
- package/dist/internal/textRenderer.d.ts +1 -0
- package/dist/internal/textRenderer.js +33 -7
- package/dist/internal/usePencilSurface.js +16 -8
- package/dist/internal/useSelectionDrawing.js +15 -11
- package/dist/journey-map/MapScale.js +3 -1
- package/dist/journey-map/index.js +3 -1
- package/dist/keypad/index.d.ts +23 -0
- package/dist/keypad/index.js +63 -0
- package/dist/keypad/model.d.ts +20 -0
- package/dist/keypad/model.js +33 -0
- package/dist/layout/index.d.ts +63 -0
- package/dist/layout/index.js +62 -0
- package/dist/log/index.d.ts +45 -0
- package/dist/log/index.js +159 -0
- package/dist/log/model.d.ts +16 -0
- package/dist/log/model.js +13 -0
- package/dist/node-graph/index.d.ts +123 -0
- package/dist/node-graph/index.js +304 -0
- package/dist/number-ticker/index.d.ts +29 -0
- package/dist/number-ticker/index.js +132 -0
- package/dist/number-ticker/model.d.ts +8 -0
- package/dist/number-ticker/model.js +34 -0
- package/dist/overflow-list/index.d.ts +39 -0
- package/dist/overflow-list/index.js +230 -0
- package/dist/overflow-list/layout.d.ts +19 -0
- package/dist/overflow-list/layout.js +44 -0
- package/dist/pagination/index.d.ts +23 -0
- package/dist/pagination/index.js +41 -0
- package/dist/pagination/model.d.ts +4 -0
- package/dist/pagination/model.js +42 -0
- package/dist/pan-zoom/index.d.ts +71 -0
- package/dist/pan-zoom/index.js +175 -0
- package/dist/radar/index.d.ts +54 -0
- package/dist/radar/index.js +220 -0
- package/dist/range-slider/index.d.ts +16 -0
- package/dist/range-slider/index.js +22 -0
- package/dist/rating/index.d.ts +28 -0
- package/dist/rating/index.js +127 -0
- package/dist/resizable/index.d.ts +32 -0
- package/dist/resizable/index.js +50 -0
- package/dist/scroll-area/index.d.ts +36 -0
- package/dist/scroll-area/index.js +44 -0
- package/dist/segmented-progress/SegmentedProgress.d.ts +17 -0
- package/dist/segmented-progress/SegmentedProgress.js +42 -0
- package/dist/slider/index.js +1 -1
- package/dist/slot/index.d.ts +24 -0
- package/dist/slot/index.js +29 -0
- package/dist/sortable/index.d.ts +50 -0
- package/dist/sortable/index.js +278 -0
- package/dist/sortable/model.d.ts +30 -0
- package/dist/sortable/model.js +64 -0
- package/dist/sparkline/index.d.ts +19 -0
- package/dist/sparkline/index.js +240 -0
- package/dist/sparkline/model.d.ts +16 -0
- package/dist/sparkline/model.js +28 -0
- package/dist/steps/index.d.ts +16 -0
- package/dist/steps/index.js +37 -0
- package/dist/styles.css +4850 -287
- package/dist/tag-input/index.d.ts +69 -0
- package/dist/tag-input/index.js +325 -0
- package/dist/tag-input/model.d.ts +14 -0
- package/dist/tag-input/model.js +38 -0
- package/dist/time-picker/index.d.ts +15 -0
- package/dist/time-picker/index.js +168 -0
- package/dist/timeline/index.d.ts +14 -0
- package/dist/timeline/index.js +16 -0
- package/dist/toast/index.d.ts +27 -0
- package/dist/toast/index.js +36 -0
- package/dist/toggle-group/index.d.ts +17 -0
- package/dist/toggle-group/index.js +26 -0
- package/dist/tokens.css +10 -9
- package/dist/transfer/index.d.ts +64 -0
- package/dist/transfer/index.js +174 -0
- package/dist/transfer/model.d.ts +12 -0
- package/dist/transfer/model.js +33 -0
- package/dist/tree/index.d.ts +39 -0
- package/dist/tree/index.js +36 -0
- package/dist/tree-select/index.d.ts +59 -0
- package/dist/tree-select/index.js +142 -0
- package/dist/tree-select/model.d.ts +31 -0
- package/dist/tree-select/model.js +135 -0
- package/dist/typewriter/index.d.ts +21 -0
- package/dist/typewriter/index.js +101 -0
- package/dist/typewriter/model.d.ts +7 -0
- package/dist/typewriter/model.js +45 -0
- package/dist/upload/index.d.ts +59 -0
- package/dist/upload/index.js +230 -0
- package/dist/virtual-list/index.d.ts +44 -0
- package/dist/virtual-list/index.js +128 -0
- package/dist/virtual-list/model.d.ts +7 -0
- package/dist/virtual-list/model.js +27 -0
- 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.
|
|
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.
|
package/dist/PencilIcon.js
CHANGED
|
@@ -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:
|
|
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 ===
|
|
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
|
-
|
|
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(() => {
|
|
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
|
};
|
package/dist/PencilLine.js
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
85
|
+
inheritedStyle.disconnect();
|
|
80
86
|
colorScheme.removeEventListener('change', schedule);
|
|
81
87
|
cancelAnimationFrame(frame);
|
|
82
88
|
};
|