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