@inkra/react 0.8.0 → 0.10.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 +137 -2
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/Xiaolai-OFL.txt +94 -0
- package/dist/PencilIcon.d.ts +14 -1
- package/dist/PencilIcon.js +88 -52
- package/dist/PencilText.js +4 -1
- package/dist/assets/ui-hand-xiaolai-extended.woff2 +0 -0
- package/dist/assets/ui-hand-xiaolai.woff2 +0 -0
- package/dist/assets/xiaolai.css +17 -0
- package/dist/form/EditablePencilText.js +4 -2
- package/dist/guide/index.d.ts +77 -0
- package/dist/guide/index.js +379 -0
- package/dist/i18n/catalog.d.ts +1 -1
- package/dist/i18n/catalog.js +8 -0
- package/dist/image-viewer/index.d.ts +2 -0
- package/dist/image-viewer/index.js +28 -28
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/infinite-scroll/gesture.d.ts +19 -0
- package/dist/infinite-scroll/gesture.js +218 -0
- package/dist/infinite-scroll/index.d.ts +55 -0
- package/dist/infinite-scroll/index.js +543 -0
- package/dist/infinite-scroll/model.d.ts +33 -0
- package/dist/infinite-scroll/model.js +168 -0
- package/dist/internal/iconFill.d.ts +16 -0
- package/dist/internal/iconFill.js +275 -0
- package/dist/internal/surfaceRenderer.d.ts +4 -4
- package/dist/internal/surfaceRenderer.js +2 -2
- package/dist/internal/textRenderer.d.ts +1 -1
- package/dist/internal/textRenderer.js +23 -11
- package/dist/inventory-grid/index.js +13 -3
- package/dist/inventory-grid/outline.d.ts +9 -0
- package/dist/inventory-grid/outline.js +54 -0
- package/dist/loading/index.d.ts +58 -0
- package/dist/loading/index.js +188 -0
- package/dist/loading/locales.d.ts +15 -0
- package/dist/loading/locales.js +34 -0
- package/dist/loading/model.d.ts +6 -0
- package/dist/loading/model.js +29 -0
- package/dist/masonry/index.d.ts +25 -0
- package/dist/masonry/index.js +227 -0
- package/dist/masonry/layout.d.ts +35 -0
- package/dist/masonry/layout.js +67 -0
- package/dist/number-ticker/RollingValue.d.ts +6 -0
- package/dist/number-ticker/RollingValue.js +10 -0
- package/dist/number-ticker/index.d.ts +4 -0
- package/dist/number-ticker/index.js +55 -7
- package/dist/number-ticker/rolling.d.ts +35 -0
- package/dist/number-ticker/rolling.js +87 -0
- package/dist/selection/CircularSwitchVisual.d.ts +8 -0
- package/dist/selection/CircularSwitchVisual.js +82 -0
- package/dist/selection/Switch.d.ts +7 -0
- package/dist/selection/Switch.js +15 -3
- package/dist/slider/circular.d.ts +35 -0
- package/dist/slider/circular.js +196 -0
- package/dist/slider/circularMath.d.ts +10 -0
- package/dist/slider/circularMath.js +34 -0
- package/dist/slider/index.d.ts +5 -0
- package/dist/slider/index.js +5 -3
- package/dist/styles/base.css +4 -2
- package/dist/styles/breadcrumb.css +1 -1
- package/dist/styles/guide.css +202 -0
- package/dist/styles/image-viewer.css +20 -0
- package/dist/styles/infinite-scroll.css +171 -0
- package/dist/styles/inventory-grid.css +20 -8
- package/dist/styles/loading.css +348 -0
- package/dist/styles/markdown.css +1 -1
- package/dist/styles/masonry.css +68 -0
- package/dist/styles/number-ticker.css +47 -1
- package/dist/styles/selection.css +50 -0
- package/dist/styles/slider.css +65 -0
- package/dist/styles/slot.css +1 -1
- package/dist/styles.css +1180 -192
- package/dist/toast/manager.d.ts +4 -4
- package/dist/tokens.css +13 -10
- package/package.json +28 -2
package/README.md
CHANGED
|
@@ -17,6 +17,52 @@ Inkra components require a React client boundary. Next.js and React Server Compo
|
|
|
17
17
|
|
|
18
18
|
## Usage
|
|
19
19
|
|
|
20
|
+
### Guided And Asynchronous Experiences
|
|
21
|
+
|
|
22
|
+
`Guide.Root`, `Guide.Trigger` and `Guide.Step` compose a targeted walkthrough.
|
|
23
|
+
`showSkip` exposes skipping; `onSkip` is separate from `onFinish`, so leaving
|
|
24
|
+
early never reports completion. Controlled `open` and `step`, target resolution,
|
|
25
|
+
focus restoration and localized labels are available.
|
|
26
|
+
|
|
27
|
+
`Loading` supports `inline`, `region` and `fullscreen` carriers with six layout
|
|
28
|
+
presets: `brand-center`, `scene-bottom`, `intel`, `staged`, `edge` and `skeleton`.
|
|
29
|
+
Supply real progress or `null` for indeterminate progress. The application owns
|
|
30
|
+
`status` and the cancel/retry/continue actions. Fullscreen blocking and scroll
|
|
31
|
+
locking can be configured independently; the edge preset is non-blocking by
|
|
32
|
+
default. Backgrounds, logos, stages, six logical content slots and rotating
|
|
33
|
+
ReactNode tips share the same component. Tips support manual navigation, pause,
|
|
34
|
+
controlled indices and sequential or non-repeating random rotation.
|
|
35
|
+
|
|
36
|
+
`InfiniteScroll` loads at `top`, `bottom`, `left` or `right` boundaries. Configure
|
|
37
|
+
automatic boundary loading and touch pull-and-release separately per direction;
|
|
38
|
+
buttons remain available for keyboard use. `onLoad` receives an abort signal,
|
|
39
|
+
direction and source. Stable item identities support anchored prepends, while
|
|
40
|
+
errors, retry, exhausted directions and in-flight requests remain explicit.
|
|
41
|
+
|
|
42
|
+
`Masonry` arranges measured items in shortest-column or sequential order while
|
|
43
|
+
retaining DOM order. It supports adaptive or fixed columns, independent gaps,
|
|
44
|
+
RTL and dynamic item sizes. Supply unique stable keys, image alternatives and
|
|
45
|
+
your own data fetching, optionally through `InfiniteScroll`:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<InfiniteScroll
|
|
49
|
+
directions="bottom"
|
|
50
|
+
hasMore={hasMore}
|
|
51
|
+
onLoad={({ signal }) => fetchNextPage(signal)}
|
|
52
|
+
style={{ maxHeight: 640, overflow: 'auto' }}
|
|
53
|
+
>
|
|
54
|
+
<Masonry
|
|
55
|
+
items={items}
|
|
56
|
+
getItemKey={item => item.id}
|
|
57
|
+
minColumnWidth={220}
|
|
58
|
+
renderItem={({ item }) => <article>{item.content}</article>}
|
|
59
|
+
/>
|
|
60
|
+
</InfiniteScroll>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
These APIs are included in this source candidate. Local browser checks are not
|
|
64
|
+
real-device certification or a claim that this candidate has been published.
|
|
65
|
+
|
|
20
66
|
Import the component styles once near the application entry point:
|
|
21
67
|
|
|
22
68
|
```tsx
|
|
@@ -435,6 +481,22 @@ motion and SSR show deterministic final values. Intermediate frames are visual;
|
|
|
435
481
|
the polite announcement updates only at the final value. Use `getValueLabel`
|
|
436
482
|
when units or custom rendering need a complete accessible phrase.
|
|
437
483
|
|
|
484
|
+
Use `animation="roll"` for vertical, place-aligned digit reels. Integers align
|
|
485
|
+
from the right and fractions from the decimal point; carry, multiplication,
|
|
486
|
+
shrinking values and sign changes expand or collapse the affected columns.
|
|
487
|
+
Grouping, signs, currency and compact units remain formatted text, not digit
|
|
488
|
+
reels. `rollDirection="auto"` rolls up for increasing values and down for
|
|
489
|
+
decreasing values; `up` or `down` fixes the direction. `duration` and `easing`
|
|
490
|
+
apply to both modes. The default `animation="count"` retains value interpolation.
|
|
491
|
+
`renderValue` takes precedence over the built-in reels. Changing the formatting
|
|
492
|
+
or animation mode settles immediately, as do disabled/reduced motion, invalid
|
|
493
|
+
durations, and transitions with more than 128 formatted tokens. Exact strings
|
|
494
|
+
and bigint values retain their original precision in either mode.
|
|
495
|
+
|
|
496
|
+
```tsx
|
|
497
|
+
<NumberTicker value={amount} animation="roll" rollDirection="auto" duration={650} />
|
|
498
|
+
```
|
|
499
|
+
|
|
438
500
|
Set `compact` to follow `locale` (Chinese units for `zh`, English units otherwise),
|
|
439
501
|
or use `compact="en"` / `compact="zh"` to select a unit table explicitly.
|
|
440
502
|
English uses this library's short-scale suffix convention: K, M, B, T, Qa, Qi,
|
|
@@ -546,6 +608,33 @@ The matching inherited tokens are `--inkra-icon-stroke-width`,
|
|
|
546
608
|
and `--inkra-icon-break-count`. Finite explicit props override those tokens.
|
|
547
609
|
See `docs/pencil-contract.md` in the workspace for the material contract.
|
|
548
610
|
|
|
611
|
+
Icons support `variant="outline"` (default) and `variant="filled"`. Filled icons
|
|
612
|
+
keep holes and internal marks transparent, join overlapping silhouettes and do
|
|
613
|
+
not close open arrows or checks into polygons. Set `fillColor` independently of
|
|
614
|
+
the contour's CSS `color`; it defaults to `currentColor` and accepts CSS variables.
|
|
615
|
+
The silhouette stays continuous while stroke controls affect its exterior.
|
|
616
|
+
|
|
617
|
+
```tsx
|
|
618
|
+
<PencilIcon paths={folderPaths} variant="filled" fillColor="var(--folder-color)"
|
|
619
|
+
sourceSize={24} size={32} style={{ color: 'var(--inkra-ink)' }} />
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Supply `filledPaths` when a shape needs a dedicated filled silhouette. Each path
|
|
623
|
+
can use `mode: 'fill'` with an optional `color` for independently colored regions,
|
|
624
|
+
or `mode: 'cutout'` for transparent details. Closed cutouts remove an area; open
|
|
625
|
+
cutouts remove a stroke without implicitly closing it. Explicit fills accept
|
|
626
|
+
`fillRule: 'nonzero' | 'evenodd'` (default `nonzero`). These authored regions do
|
|
627
|
+
not add their own contour stroke. For lighter colored-pencil regions, use
|
|
628
|
+
`mode: 'wash'` with the same optional `color` and `fillRule`: paper remains visible
|
|
629
|
+
between pigment strokes, with a light pencil edge in the icon's CSS `color`.
|
|
630
|
+
Dense `fill` regions are unchanged.
|
|
631
|
+
Explicit `mode: 'stroke'` keeps a line symbol
|
|
632
|
+
even when its path closes; omitted mode detects filled silhouettes automatically.
|
|
633
|
+
An empty `filledPaths` intentionally hides the
|
|
634
|
+
base shape in filled mode. `badge` accepts the same three props and inherits the
|
|
635
|
+
base variant and fill color unless overridden. Intensity applies once to the
|
|
636
|
+
completed image, including its fills, exterior strokes and badge.
|
|
637
|
+
|
|
549
638
|
`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:
|
|
550
639
|
|
|
551
640
|
```tsx
|
|
@@ -555,10 +644,28 @@ See `docs/pencil-contract.md` in the workspace for the material contract.
|
|
|
555
644
|
|
|
556
645
|
### Rendering tokens
|
|
557
646
|
|
|
647
|
+
`ThemeProvider` accepts typography through `tokens`. Set
|
|
648
|
+
`--inkra-font-hand`, `--inkra-font-body` and `--inkra-font-display` to CSS font
|
|
649
|
+
stacks (load your chosen font separately). Set `--inkra-letter-effect: 0` to
|
|
650
|
+
display plain selectable text in that font, without the pencil canvas.
|
|
651
|
+
The default is 1 (enabled). This inherited toggle preserves all lettering
|
|
652
|
+
parameters and does not affect icons, surface fills or borders.
|
|
653
|
+
|
|
654
|
+
```tsx
|
|
655
|
+
<ThemeProvider tokens={{
|
|
656
|
+
'--inkra-font-hand': '"Inkra Xiaolai", cursive',
|
|
657
|
+
'--inkra-font-body': '"Inkra Xiaolai", cursive',
|
|
658
|
+
'--inkra-font-display': '"Inkra Xiaolai", cursive',
|
|
659
|
+
'--inkra-letter-effect': 0,
|
|
660
|
+
}}>
|
|
661
|
+
<Button>普通字体效果</Button>
|
|
662
|
+
</ThemeProvider>
|
|
663
|
+
```
|
|
664
|
+
|
|
558
665
|
`PencilText` reads `--inkra-letter-variation`, `--inkra-letter-breaks`,
|
|
559
666
|
`--inkra-letter-outline`, `--inkra-letter-fill`, and `--inkra-letter-grain` from
|
|
560
667
|
its computed style. Values are clamped to 0-1; missing or invalid values use the
|
|
561
|
-
default lettering preset (0.
|
|
668
|
+
default Xiaolai lettering preset (0.10, 0.31, 0.44, 0.85, 0.40 respectively, revised 2026-10-28). Apply these tokens to an ancestor to customize all
|
|
562
669
|
text within that scope without changing layout, selectable text, or labels.
|
|
563
670
|
|
|
564
671
|
`PencilLine` reads `--inkra-stroke-width` (0.3-3) and
|
|
@@ -583,7 +690,13 @@ unavailable-canvas fallbacks remain intact.
|
|
|
583
690
|
|
|
584
691
|
## Font license
|
|
585
692
|
|
|
586
|
-
The
|
|
693
|
+
The default font is Xiaolai SC Regular v3.120, registered as `Inkra Xiaolai`.
|
|
694
|
+
Common and extended characters are separate WOFF2 resources loaded through
|
|
695
|
+
`unicode-range`. Override `--inkra-font-hand` to choose another font; the source
|
|
696
|
+
text and canvas use the same computed family. The package also retains
|
|
697
|
+
`Trip Map Hand`, a web subset of Yozai Light v0.868, for compatibility.
|
|
698
|
+
Both fonts use SIL OFL 1.1, separately from Inkra's MIT software license.
|
|
699
|
+
See `THIRD_PARTY_NOTICES.md`, `Xiaolai-OFL.txt` and `Yozai-OFL.txt`.
|
|
587
700
|
|
|
588
701
|
## Development
|
|
589
702
|
|
|
@@ -594,3 +707,25 @@ npm run pack:check
|
|
|
594
707
|
```
|
|
595
708
|
|
|
596
709
|
`npm test` builds the package before running the SSR and determinism tests. `pack:check` prints the files that would be included without publishing anything.
|
|
710
|
+
|
|
711
|
+
## Slider endpoints and circular dragging
|
|
712
|
+
|
|
713
|
+
Slider and RangeSlider use `thumbAlignment="edge"` by default: the thumb center
|
|
714
|
+
reaches either end of the track. Set `thumbAlignment="contain"` to keep the
|
|
715
|
+
previous inset positioning. This setting is independent of the visual variant.
|
|
716
|
+
|
|
717
|
+
`CircularSlider` is an optional single-value control exported from
|
|
718
|
+
`@inkra/react` and `@inkra/react/slider`. It supports controlled and uncontrolled
|
|
719
|
+
values, min/max/step, pointer and touch dragging, keyboard controls, disabled
|
|
720
|
+
and read-only states, form submission/reset, and a configurable arc. Angles are
|
|
721
|
+
measured clockwise from the top; `startAngle={0} sweepAngle={360}` draws a full
|
|
722
|
+
ring. `clockwise` controls the value direction. Arrow Right/Up increase,
|
|
723
|
+
Arrow Left/Down decrease, Home/End reach bounds and Page keys take larger steps.
|
|
724
|
+
Give the control an accessible name and use `getValueLabel` for units.
|
|
725
|
+
|
|
726
|
+
Use `centerIcon` for a decorative icon. The default center displays the value;
|
|
727
|
+
set `showValue={false}` for an icon alone. `centerContent` accepts a React node
|
|
728
|
+
or `(value: number) => ReactNode`, overriding the default value display when
|
|
729
|
+
provided; `null` hides the text. Strings and numbers retain the pencil text
|
|
730
|
+
appearance. Center content is inert and hidden from accessibility APIs;
|
|
731
|
+
keep the slider's `aria-label` and `getValueLabel` meaningful independently.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Third-Party Notices
|
|
2
2
|
|
|
3
|
+
## Xiaolai SC
|
|
4
|
+
|
|
5
|
+
`dist/assets/ui-hand-xiaolai*.woff2` contains Xiaolai SC Regular v3.120 by LXGW,
|
|
6
|
+
converted to WOFF2 and split into common and extended character sets without
|
|
7
|
+
dropping mapped characters. It is registered as `Inkra Xiaolai` in CSS.
|
|
8
|
+
Source: https://github.com/lxgw/kose-font/releases/tag/v3.120
|
|
9
|
+
License: SIL Open Font License 1.1, reproduced in `Xiaolai-OFL.txt`.
|
|
10
|
+
This font is not covered by Inkra's MIT software license.
|
|
11
|
+
|
|
3
12
|
## Trip Map Hand font subset
|
|
4
13
|
|
|
5
14
|
`dist/assets/ui-hand-yozai.woff2` is a web subset of Yozai Light v0.868 by LXGW, derived from work by Y.Oz. The subset uses the modified family name `Trip Map Hand` and is included to provide the handwriting geometry used by Inkra's pencil-text renderer.
|
package/Xiaolai-OFL.txt
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Copyright 2020-2024 LXGW (https://github.com/lxgw/kose-font)
|
|
2
|
+
Copyright 2014 Nozomi Seto (https://ja.osdn.net/projects/setofont/)
|
|
3
|
+
|
|
4
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
5
|
+
This license is copied below, and is also available with a FAQ at:
|
|
6
|
+
http://scripts.sil.org/OFL
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
-----------------------------------------------------------
|
|
10
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
11
|
+
-----------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
PREAMBLE
|
|
14
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
15
|
+
development of collaborative font projects, to support the font creation
|
|
16
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
17
|
+
open framework in which fonts may be shared and improved in partnership
|
|
18
|
+
with others.
|
|
19
|
+
|
|
20
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
21
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
22
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
23
|
+
redistributed and/or sold with any software provided that any reserved
|
|
24
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
25
|
+
however, cannot be released under any other type of license. The
|
|
26
|
+
requirement for fonts to remain under this license does not apply
|
|
27
|
+
to any document created using the fonts or their derivatives.
|
|
28
|
+
|
|
29
|
+
DEFINITIONS
|
|
30
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
31
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
32
|
+
include source files, build scripts and documentation.
|
|
33
|
+
|
|
34
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
35
|
+
copyright statement(s).
|
|
36
|
+
|
|
37
|
+
"Original Version" refers to the collection of Font Software components as
|
|
38
|
+
distributed by the Copyright Holder(s).
|
|
39
|
+
|
|
40
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
41
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
42
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
43
|
+
new environment.
|
|
44
|
+
|
|
45
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
46
|
+
writer or other person who contributed to the Font Software.
|
|
47
|
+
|
|
48
|
+
PERMISSION & CONDITIONS
|
|
49
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
50
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
51
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
52
|
+
Software, subject to the following conditions:
|
|
53
|
+
|
|
54
|
+
1) Neither the Font Software nor any of its individual components,
|
|
55
|
+
in Original or Modified Versions, may be sold by itself.
|
|
56
|
+
|
|
57
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
58
|
+
redistributed and/or sold with any software, provided that each copy
|
|
59
|
+
contains the above copyright notice and this license. These can be
|
|
60
|
+
included either as stand-alone text files, human-readable headers or
|
|
61
|
+
in the appropriate machine-readable metadata fields within text or
|
|
62
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
63
|
+
|
|
64
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
65
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
66
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
67
|
+
presented to the users.
|
|
68
|
+
|
|
69
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
70
|
+
Software shall not be used to promote, endorse or advertise any
|
|
71
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
72
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
73
|
+
permission.
|
|
74
|
+
|
|
75
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
76
|
+
must be distributed entirely under this license, and must not be
|
|
77
|
+
distributed under any other license. The requirement for fonts to
|
|
78
|
+
remain under this license does not apply to any document created
|
|
79
|
+
using the Font Software.
|
|
80
|
+
|
|
81
|
+
TERMINATION
|
|
82
|
+
This license becomes null and void if any of the above conditions are
|
|
83
|
+
not met.
|
|
84
|
+
|
|
85
|
+
DISCLAIMER
|
|
86
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
87
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
88
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
89
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
90
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
91
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
92
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
93
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
94
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
package/dist/PencilIcon.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { CanvasHTMLAttributes } from 'react';
|
|
2
2
|
export interface PencilIconPath {
|
|
3
3
|
d: string;
|
|
4
|
-
|
|
4
|
+
/** Filled icons: stroke stays a line, wash exposes paper, fill is dense; omitted mode detects closed silhouettes. */
|
|
5
|
+
mode?: 'stroke' | 'wash' | 'scuff' | 'fill' | 'cutout';
|
|
6
|
+
/** Optional pigment for an explicit fill region, including CSS custom properties. */
|
|
7
|
+
color?: string;
|
|
8
|
+
fillRule?: CanvasFillRule;
|
|
5
9
|
}
|
|
6
10
|
export interface PencilIconBadge {
|
|
7
11
|
paths: readonly PencilIconPath[];
|
|
12
|
+
filledPaths?: readonly PencilIconPath[];
|
|
13
|
+
variant?: 'outline' | 'filled';
|
|
14
|
+
fillColor?: string;
|
|
8
15
|
sourceSize?: number;
|
|
9
16
|
position?: 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
|
|
10
17
|
/** Relative canvas size, clamped to 0.35-0.65. Defaults to 0.55. */
|
|
@@ -12,6 +19,12 @@ export interface PencilIconBadge {
|
|
|
12
19
|
}
|
|
13
20
|
export interface PencilIconProps extends Omit<CanvasHTMLAttributes<HTMLCanvasElement>, 'children' | 'height' | 'width'> {
|
|
14
21
|
paths: readonly PencilIconPath[];
|
|
22
|
+
/** Filled icons retain open symbols and use negative space for interior details. */
|
|
23
|
+
variant?: 'outline' | 'filled';
|
|
24
|
+
/** Optional authored geometry for shapes whose filled silhouette differs from the outline. */
|
|
25
|
+
filledPaths?: readonly PencilIconPath[];
|
|
26
|
+
/** Body pigment, independent of the CSS color used for the contour. Defaults to currentColor. */
|
|
27
|
+
fillColor?: string;
|
|
15
28
|
badge?: PencilIconBadge;
|
|
16
29
|
seed?: string | number;
|
|
17
30
|
size?: number;
|
package/dist/PencilIcon.js
CHANGED
|
@@ -6,6 +6,7 @@ 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
|
+
import { paintFilledIcon } from './internal/iconFill.js';
|
|
9
10
|
const imageCache = new BoundedCache(128, 24 * 1024 * 1024);
|
|
10
11
|
function drawFallbackPath(context, data, color, mode, weightScale = 1) {
|
|
11
12
|
try {
|
|
@@ -47,56 +48,62 @@ function renderIcon(paths, options) {
|
|
|
47
48
|
const offset = (40 - sourceSize) / 2;
|
|
48
49
|
context.translate(offset, offset);
|
|
49
50
|
let sampledPoints = 0;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
if (options.variant === 'filled') {
|
|
52
|
+
paintFilledIcon(context, paths, { color, fillColor: options.fillColor ?? color, sourceSize, seed, weightScale, roughnessScale, breaks, breakCount, pressureVariation, coordinateScale });
|
|
53
|
+
}
|
|
54
|
+
else
|
|
55
|
+
paths.forEach((path, index) => {
|
|
56
|
+
if (path.d.length > 65536)
|
|
57
|
+
return;
|
|
58
|
+
const mode = path.mode ?? 'stroke';
|
|
59
|
+
if (mode === 'cutout')
|
|
60
|
+
return;
|
|
61
|
+
const pathSeed = seed + index * 137;
|
|
62
|
+
if (mode === 'wash' || mode === 'fill') {
|
|
63
|
+
try {
|
|
64
|
+
context.save();
|
|
65
|
+
context.clip(new Path2D(path.d));
|
|
66
|
+
context.globalAlpha = active ? 0.76 : 0.34;
|
|
67
|
+
for (let y = sourceSize * 0.18; y < sourceSize; y += 1.6) {
|
|
68
|
+
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 });
|
|
69
|
+
}
|
|
70
|
+
context.restore();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
drawFallbackPath(context, path.d, color, mode, weightScale);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
56
77
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
if (sampledPoints >= 65536) {
|
|
79
|
+
drawFallbackPath(context, path.d, color, mode, weightScale);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const parts = cachedPathParts(path.d);
|
|
83
|
+
sampledPoints += parts.reduce((total, part) => total + part.length, 0);
|
|
84
|
+
if (sampledPoints > 65536) {
|
|
85
|
+
drawFallbackPath(context, path.d, color, mode, weightScale);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const points of parts) {
|
|
89
|
+
pencilStroke(context, points, color, (mode === 'scuff' ? 0.55 : 1.65) * weightScale, pathSeed, 0.3 * roughnessScale, mode === 'scuff' ? 1 : 3, false, {
|
|
90
|
+
variation: pressureVariation,
|
|
91
|
+
fitPressure: true,
|
|
92
|
+
breaks,
|
|
93
|
+
breakCount,
|
|
94
|
+
breakScale: coordinateScale,
|
|
95
|
+
continuousBreaks: true,
|
|
96
|
+
grain: 0.68,
|
|
97
|
+
gain: mode === 'scuff' ? 1.5 : 3,
|
|
98
|
+
step: 0.35,
|
|
99
|
+
taperLength: 0.65,
|
|
100
|
+
});
|
|
62
101
|
}
|
|
63
|
-
context.restore();
|
|
64
102
|
}
|
|
65
103
|
catch {
|
|
66
104
|
drawFallbackPath(context, path.d, color, mode, weightScale);
|
|
67
105
|
}
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
try {
|
|
71
|
-
if (sampledPoints >= 65536) {
|
|
72
|
-
drawFallbackPath(context, path.d, color, mode, weightScale);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
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
|
-
}
|
|
81
|
-
for (const points of parts) {
|
|
82
|
-
pencilStroke(context, points, color, (mode === 'scuff' ? 0.55 : 1.65) * weightScale, pathSeed, 0.3 * roughnessScale, mode === 'scuff' ? 1 : 3, false, {
|
|
83
|
-
variation: pressureVariation,
|
|
84
|
-
fitPressure: true,
|
|
85
|
-
breaks,
|
|
86
|
-
breakCount,
|
|
87
|
-
breakScale: coordinateScale,
|
|
88
|
-
continuousBreaks: true,
|
|
89
|
-
grain: 0.68,
|
|
90
|
-
gain: mode === 'scuff' ? 1.5 : 3,
|
|
91
|
-
step: 0.35,
|
|
92
|
-
taperLength: 0.65,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
drawFallbackPath(context, path.d, color, mode, weightScale);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
106
|
+
});
|
|
100
107
|
if (active) {
|
|
101
108
|
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 });
|
|
102
109
|
}
|
|
@@ -109,8 +116,10 @@ function renderIcon(paths, options) {
|
|
|
109
116
|
const extent = 40 * scale;
|
|
110
117
|
const x = onLeft ? 0 : 40 - extent;
|
|
111
118
|
const y = position.startsWith('top') ? 0 : 40 - extent;
|
|
112
|
-
const
|
|
119
|
+
const badgeVariant = badge.variant ?? options.variant;
|
|
120
|
+
const badgeImage = renderIcon(badgeVariant === 'filled' ? badge.filledPaths ?? badge.paths : badge.paths, {
|
|
113
121
|
seed: seed + 1543, size: size * scale, sourceSize: badgeSourceSize, active: false, color, dpr,
|
|
122
|
+
variant: badgeVariant, fillColor: badge.fillColor ?? options.fillColor,
|
|
114
123
|
weightScale: weightScale / scale, roughnessScale, breaks, breakCount, pressureVariation,
|
|
115
124
|
});
|
|
116
125
|
const mask = document.createElement('canvas');
|
|
@@ -140,17 +149,18 @@ function renderIcon(paths, options) {
|
|
|
140
149
|
}
|
|
141
150
|
return canvas;
|
|
142
151
|
}
|
|
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) {
|
|
152
|
+
export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge, className = '', decorative = true, paths, variant = 'outline', filledPaths, fillColor, seed, size = 40, sourceSize = 32, strokeWidth, intensity, breaks, breakCount, pressureVariation, ...props }, forwardedRef) {
|
|
144
153
|
const canvasRef = useRef(null);
|
|
145
154
|
const lastPaintRef = useRef('');
|
|
146
155
|
useImperativeHandle(forwardedRef, () => canvasRef.current);
|
|
147
156
|
// Compare values, not caller object identity. Serializing each render also
|
|
148
157
|
// detects in-place edits; the effect receives a stable snapshot of that value.
|
|
149
|
-
const
|
|
158
|
+
const originalGeometryKey = JSON.stringify(paths);
|
|
159
|
+
const geometryKey = variant === 'filled' && filledPaths ? JSON.stringify(filledPaths) : originalGeometryKey;
|
|
150
160
|
const badgeKey = badge ? JSON.stringify(badge) : '';
|
|
151
161
|
const stablePaths = useMemo(() => JSON.parse(geometryKey), [geometryKey]);
|
|
152
162
|
const stableBadge = useMemo(() => badgeKey ? JSON.parse(badgeKey) : undefined, [badgeKey]);
|
|
153
|
-
const resolvedSeed = seedToNumber(seed ??
|
|
163
|
+
const resolvedSeed = seedToNumber(seed ?? originalGeometryKey);
|
|
154
164
|
useEffect(() => {
|
|
155
165
|
const canvas = canvasRef.current;
|
|
156
166
|
if (!canvas)
|
|
@@ -162,6 +172,28 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
|
|
|
162
172
|
const dpr = effectiveDpr(3);
|
|
163
173
|
const computed = getComputedStyle(canvas);
|
|
164
174
|
const color = computed.color;
|
|
175
|
+
const resolveColor = (value) => {
|
|
176
|
+
if (!value || value === 'currentColor')
|
|
177
|
+
return color;
|
|
178
|
+
const probe = document.createElement('span');
|
|
179
|
+
probe.style.color = value;
|
|
180
|
+
// Canvas fallback children inherit its own color and custom properties, not just its parent's.
|
|
181
|
+
canvas.appendChild(probe);
|
|
182
|
+
try {
|
|
183
|
+
return getComputedStyle(probe).color || color;
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
probe.remove();
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const resolvedFill = variant === 'filled' ? resolveColor(fillColor) : color;
|
|
190
|
+
const resolvePaths = (items) => items.map(path => path.color ? { ...path, color: resolveColor(path.color) } : path);
|
|
191
|
+
const renderedPaths = variant === 'filled' ? resolvePaths(stablePaths) : stablePaths;
|
|
192
|
+
const renderedBadge = stableBadge ? { ...stableBadge,
|
|
193
|
+
fillColor: resolveColor(stableBadge.fillColor ?? fillColor),
|
|
194
|
+
paths: resolvePaths(stableBadge.paths),
|
|
195
|
+
filledPaths: stableBadge.filledPaths ? resolvePaths(stableBadge.filledPaths) : undefined,
|
|
196
|
+
} : undefined;
|
|
165
197
|
const rtl = computed.direction === 'rtl';
|
|
166
198
|
const iconWeight = readRenderToken(computed, '--inkra-icon-stroke-width', pencilRenderDefaults.iconStrokeWidth, 0.3, 3);
|
|
167
199
|
const weightScale = (typeof strokeWidth === 'number' && Number.isFinite(strokeWidth) ? Math.min(3, Math.max(0.3, strokeWidth)) : iconWeight) / 1.2;
|
|
@@ -180,9 +212,10 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
|
|
|
180
212
|
: readRenderToken(computed, '--inkra-icon-break-count', pencilRenderDefaults.iconBreakCount, 0, 12));
|
|
181
213
|
const key = JSON.stringify([
|
|
182
214
|
PENCIL_RENDERER_VERSION,
|
|
183
|
-
'icon-path-
|
|
215
|
+
'icon-path-paper-wash-v8',
|
|
184
216
|
geometryKey,
|
|
185
217
|
badgeKey,
|
|
218
|
+
variant, resolvedFill, renderedPaths, renderedBadge,
|
|
186
219
|
rtl,
|
|
187
220
|
resolvedSeed,
|
|
188
221
|
size,
|
|
@@ -202,7 +235,7 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
|
|
|
202
235
|
image = undefined;
|
|
203
236
|
}
|
|
204
237
|
if (!image) {
|
|
205
|
-
image = renderIcon(
|
|
238
|
+
image = renderIcon(renderedPaths, { seed: resolvedSeed, size, sourceSize, active, color, dpr, variant, fillColor: resolvedFill, badge: renderedBadge, rtl, weightScale, roughnessScale, breaks: strokeBreaks, breakCount: strokeBreakCount, pressureVariation: strokeVariation });
|
|
206
239
|
imageCache.set(key, image, canvasByteCost(image));
|
|
207
240
|
}
|
|
208
241
|
canvas.width = image.width;
|
|
@@ -236,7 +269,9 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
|
|
|
236
269
|
inheritedColor.observe(element, { attributes: true, attributeFilter: ['style', 'class', 'dir', 'data-theme', 'data-inkra-theme'] });
|
|
237
270
|
}
|
|
238
271
|
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
|
272
|
+
const forcedColors = window.matchMedia('(forced-colors: active)');
|
|
239
273
|
colorScheme.addEventListener('change', paint);
|
|
274
|
+
forcedColors.addEventListener('change', paint);
|
|
240
275
|
const stopRecovery = observeCanvasRecovery(() => { lastPaintRef.current = ''; paint(); });
|
|
241
276
|
paint();
|
|
242
277
|
return () => {
|
|
@@ -245,10 +280,11 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
|
|
|
245
280
|
stopRecovery();
|
|
246
281
|
inheritedColor.disconnect();
|
|
247
282
|
colorScheme.removeEventListener('change', paint);
|
|
283
|
+
forcedColors.removeEventListener('change', paint);
|
|
248
284
|
};
|
|
249
|
-
}, [active, stableBadge, badgeKey, geometryKey, stablePaths, resolvedSeed, size, sourceSize, strokeWidth, intensity, breaks, breakCount, pressureVariation]);
|
|
285
|
+
}, [active, stableBadge, badgeKey, geometryKey, stablePaths, resolvedSeed, size, sourceSize, strokeWidth, intensity, breaks, breakCount, pressureVariation, variant, fillColor]);
|
|
250
286
|
const accessibilityProps = decorative
|
|
251
287
|
? { 'aria-hidden': true }
|
|
252
288
|
: { role: props.role ?? 'img' };
|
|
253
|
-
return (_jsx("canvas", { ...props, ...accessibilityProps, className: ['inkra-pencil-icon', className].filter(Boolean).join(' '), "data-active": active || undefined, "data-composite": Boolean(badge?.paths.length) || undefined, "data-renderer": "inkra-pressure-pencil", ref: canvasRef, style: { width: size, height: size, ...props.style } }));
|
|
289
|
+
return (_jsx("canvas", { ...props, ...accessibilityProps, className: ['inkra-pencil-icon', className].filter(Boolean).join(' '), "data-active": active || undefined, "data-icon-variant": variant, "data-composite": Boolean(badge?.paths.length) || undefined, "data-renderer": "inkra-pressure-pencil", ref: canvasRef, style: { width: size, height: size, ...props.style } }));
|
|
254
290
|
});
|
package/dist/PencilText.js
CHANGED
|
@@ -97,6 +97,9 @@ function measureLocalTextLines(element, liveSource, text, width, height) {
|
|
|
97
97
|
}
|
|
98
98
|
function paintText(element, source, canvas, text, seed, glyphSeed) {
|
|
99
99
|
const style = getComputedStyle(element);
|
|
100
|
+
// Plain mode retains the selectable source and font metrics, without painting.
|
|
101
|
+
if (readRenderToken(style, '--inkra-letter-effect', 1, 0, 1) === 0)
|
|
102
|
+
return false;
|
|
100
103
|
const { width: localWidth, height: localHeight } = localBorderBox(element, style);
|
|
101
104
|
if (!localWidth || !localHeight || text.length > 4096 || !source.firstChild)
|
|
102
105
|
return false;
|
|
@@ -255,7 +258,7 @@ const PencilTextImplementation = forwardRef(function PencilText({ as: Element =
|
|
|
255
258
|
...['color', 'font-family', 'font-size', 'font-weight', 'font-style', 'line-height', 'text-align', 'text-indent',
|
|
256
259
|
'letter-spacing', 'white-space', 'word-break', 'overflow-wrap', 'direction',
|
|
257
260
|
'padding-top', 'padding-left', 'padding-right', 'padding-bottom',
|
|
258
|
-
'--inkra-letter-variation', '--inkra-letter-breaks', '--inkra-letter-outline',
|
|
261
|
+
'--inkra-letter-effect', '--inkra-letter-variation', '--inkra-letter-breaks', '--inkra-letter-outline',
|
|
259
262
|
'--inkra-letter-fill', '--inkra-letter-grain'].map(name => style.getPropertyValue(name)),
|
|
260
263
|
]);
|
|
261
264
|
};
|
|
Binary file
|
|
Binary file
|