weasel-icons 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 +13 -4
- package/THIRD_PARTY_NOTICES.md +14 -4
- package/dist/icons/alert/alert-icon.d.ts +6 -0
- package/dist/icons/alert/alert-icon.js +22 -0
- package/dist/icons/alert/alert-rig.d.ts +12 -0
- package/dist/icons/alert/alert-rig.js +25 -0
- package/dist/icons/badge-check/badge-check-icon.d.ts +5 -0
- package/dist/icons/badge-check/badge-check-icon.js +33 -0
- package/dist/icons/badge-check/badge-check-rig.d.ts +14 -0
- package/dist/icons/badge-check/badge-check-rig.js +36 -0
- package/dist/icons/bell/bell-icon.js +1 -1
- package/dist/icons/bulb/bulb-icon.d.ts +5 -0
- package/dist/icons/bulb/bulb-icon.js +25 -0
- package/dist/icons/bulb/bulb-rig.d.ts +13 -0
- package/dist/icons/bulb/bulb-rig.js +37 -0
- package/dist/icons/check/check-icon.d.ts +5 -0
- package/dist/icons/check/check-icon.js +26 -0
- package/dist/icons/check/check-rig.d.ts +6 -0
- package/dist/icons/check/check-rig.js +38 -0
- package/dist/icons/clapperboard/clapperboard-icon.d.ts +5 -0
- package/dist/icons/clapperboard/clapperboard-icon.js +18 -0
- package/dist/icons/clapperboard/clapperboard-rig.d.ts +12 -0
- package/dist/icons/clapperboard/clapperboard-rig.js +25 -0
- package/dist/icons/close/close-icon.d.ts +5 -0
- package/dist/icons/close/close-icon.js +27 -0
- package/dist/icons/close/close-rig.d.ts +10 -0
- package/dist/icons/close/close-rig.js +31 -0
- package/dist/icons/copy/copy-icon.d.ts +5 -0
- package/dist/icons/copy/copy-icon.js +31 -0
- package/dist/icons/copy/copy-rig.d.ts +13 -0
- package/dist/icons/copy/copy-rig.js +44 -0
- package/dist/icons/cursor/cursor-icon.d.ts +5 -0
- package/dist/icons/cursor/cursor-icon.js +32 -0
- package/dist/icons/cursor/cursor-rig.d.ts +21 -0
- package/dist/icons/cursor/cursor-rig.js +63 -0
- package/dist/icons/delete/delete-icon.d.ts +5 -0
- package/dist/icons/delete/delete-icon.js +25 -0
- package/dist/icons/delete/delete-rig.d.ts +15 -0
- package/dist/icons/delete/delete-rig.js +33 -0
- package/dist/icons/dog/dog-icon.js +1 -1
- package/dist/icons/download/download-icon.js +1 -1
- package/dist/icons/eye/eye-icon.d.ts +5 -0
- package/dist/icons/eye/eye-icon.js +27 -0
- package/dist/icons/eye/eye-rig.d.ts +9 -0
- package/dist/icons/eye/eye-rig.js +37 -0
- package/dist/icons/heart/heart-icon.js +1 -1
- package/dist/icons/heart/heart-rig.js +1 -1
- package/dist/icons/link/link-icon.d.ts +5 -0
- package/dist/icons/link/link-icon.js +30 -0
- package/dist/icons/link/link-rig.d.ts +13 -0
- package/dist/icons/link/link-rig.js +37 -0
- package/dist/icons/monkey/monkey-icon.js +1 -1
- package/dist/icons/plug/plug-icon.d.ts +5 -0
- package/dist/icons/plug/plug-icon.js +21 -0
- package/dist/icons/plug/plug-rig.d.ts +16 -0
- package/dist/icons/plug/plug-rig.js +29 -0
- package/dist/icons/refresh/refresh-icon.d.ts +5 -0
- package/dist/icons/refresh/refresh-icon.js +40 -0
- package/dist/icons/refresh/refresh-rig.d.ts +11 -0
- package/dist/icons/refresh/refresh-rig.js +30 -0
- package/dist/icons/replace/replace-icon.d.ts +5 -0
- package/dist/icons/replace/replace-icon.js +25 -0
- package/dist/icons/replace/replace-rig.d.ts +14 -0
- package/dist/icons/replace/replace-rig.js +24 -0
- package/dist/icons/rocket/rocket-icon.js +1 -1
- package/dist/icons/search/search-icon.js +1 -1
- package/dist/icons/shield/shield-icon.d.ts +5 -0
- package/dist/icons/shield/shield-icon.js +25 -0
- package/dist/icons/shield/shield-rig.d.ts +23 -0
- package/dist/icons/shield/shield-rig.js +42 -0
- package/dist/icons/ship/ship-icon.js +1 -1
- package/dist/icons/sparkle/sparkle-icon.d.ts +5 -0
- package/dist/icons/sparkle/sparkle-icon.js +27 -0
- package/dist/icons/sparkle/sparkle-rig.d.ts +8 -0
- package/dist/icons/sparkle/sparkle-rig.js +6 -0
- package/dist/icons/sparkles/sparkles-icon.d.ts +5 -0
- package/dist/icons/sparkles/sparkles-icon.js +27 -0
- package/dist/icons/sparkles/sparkles-rig.d.ts +14 -0
- package/dist/icons/sparkles/sparkles-rig.js +24 -0
- package/dist/icons/star/star-icon.d.ts +5 -0
- package/dist/icons/star/star-icon.js +38 -0
- package/dist/icons/star/star-rig.d.ts +20 -0
- package/dist/icons/star/star-rig.js +61 -0
- package/dist/icons/swap/swap-icon.d.ts +5 -0
- package/dist/icons/swap/swap-icon.js +26 -0
- package/dist/icons/swap/swap-rig.d.ts +10 -0
- package/dist/icons/swap/swap-rig.js +24 -0
- package/dist/icons/wave/wave-icon.d.ts +5 -0
- package/dist/icons/wave/wave-icon.js +18 -0
- package/dist/icons/wave/wave-rig.d.ts +8 -0
- package/dist/icons/wave/wave-rig.js +39 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.js +20 -1
- package/dist/shared/icon-colors.d.ts +23 -4
- package/dist/shared/icon-types.d.ts +1 -0
- package/dist/shared/sparkle-motion.d.ts +12 -0
- package/dist/shared/sparkle-motion.js +38 -0
- package/dist/shared/use-icon-motion.d.ts +4 -0
- package/dist/shared/use-icon-motion.js +26 -0
- package/package.json +104 -9
- package/dist/icons/leaf/leaf-icon.d.ts +0 -6
- package/dist/icons/leaf/leaf-icon.js +0 -29
- package/dist/icons/leaf/leaf-rig.d.ts +0 -25
- package/dist/icons/leaf/leaf-rig.js +0 -201
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Weasel Icons
|
|
2
2
|
|
|
3
|
-
Icons that overreact beautifully.
|
|
3
|
+
Icons that overreact beautifully. Stateful React SVG icons with smooth transitions and optional continuous behavior.
|
|
4
|
+
|
|
5
|
+
Version `0.2.0` contains 28 icons. See the repository design docs for supported states and verification evidence.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
@@ -10,7 +12,7 @@ npm install weasel-icons
|
|
|
10
12
|
pnpm add weasel-icons
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
Supports React / React DOM 18.3 or 19. Motion is installed as a dependency. No stylesheet, provider, Tailwind, or Next.js dependency is required. The package is ESM with TypeScript declarations and per-icon exports.
|
|
14
16
|
|
|
15
17
|
## Use
|
|
16
18
|
|
|
@@ -47,14 +49,21 @@ The existing SVG smoothly transitions when `state` changes, including during an
|
|
|
47
49
|
|
|
48
50
|
## Icons and props
|
|
49
51
|
|
|
50
|
-
Exports: `DogIcon`, `MonkeyIcon`, `HeartIcon`, `ShipIcon`, `RocketIcon`, `
|
|
52
|
+
Exports in the current local build: `DogIcon`, `MonkeyIcon`, `HeartIcon`, `ShipIcon`, `RocketIcon`, `SearchIcon`, `BellIcon`, `DownloadIcon`, `ShieldIcon`, `CursorIcon`, `ClapperboardIcon`, `PlugIcon`, `BulbIcon`, `AlertIcon`, `BadgeCheckIcon`, `ReplaceIcon`, `DeleteIcon`, `SwapIcon`, `SparklesIcon`, `SparkleIcon`, `StarIcon`, `WaveIcon`, `RefreshIcon`, `CopyIcon`, `EyeIcon`, `LinkIcon`, `CheckIcon`, and `CloseIcon`. Each icon exports its own state type and list, for example `DogState` and `dogStates`, or `BadgeCheckState` and `badgeCheckStates`. TypeScript autocompletion shows the valid states for each icon.
|
|
53
|
+
|
|
54
|
+
### Upgrading from 0.1.0
|
|
55
|
+
|
|
56
|
+
Version `0.2.0` also includes `SparklesIcon` and the single-contour `SparkleIcon` (both `idle`, `shimmering`, `blink`, `dimmed`), `StarIcon` (`idle`, `shooting`, `starred`, `loading`) and `WaveIcon` (`idle`, `flowing`, `energetic`, `flat`), with direct exports at `/sparkles`, `/sparkle`, `/star` and `/wave`. Sparkles uses `blink` and Star uses `shooting`. `RefreshIcon`, `CopyIcon` and `LinkIcon` have four states; `EyeIcon`, `CheckIcon` and `CloseIcon` intentionally have three.
|
|
57
|
+
|
|
58
|
+
`0.2.0` adds Shield, Cursor, Clapperboard, Plug, Bulb, Alert, Badge Check, Replace, Delete and Swap, with the latest state feedback fixes. Leaf and its `weasel-icons/leaf` export have been removed; remove or replace any Leaf imports before upgrading. Existing retained icon imports continue to work. The new motions have geometry and lifecycle regression coverage, but are not newly browser-profiled performance guarantees.
|
|
51
59
|
|
|
52
60
|
Common optional props: `state`, `size`, `color`, `accentColor`, `strokeWidth`, `speed`, `alive`, `className`.
|
|
53
61
|
|
|
54
62
|
- `alive={false}` (default) transitions to the state, then settles.
|
|
63
|
+
- `strokeWidth` defaults to `1.65` SVG units; pass a different width to override it.
|
|
55
64
|
- `alive` adds varied continuous in-state behavior. System reduced-motion preferences take precedence.
|
|
56
65
|
- `color` and `accentColor` default to `currentColor`.
|
|
57
|
-
- Dog, monkey, heart, ship
|
|
66
|
+
- Dog, monkey, heart, ship and alert accept small icon-specific `colors` objects, with exported types such as `DogColors`. Alert's critical punctuation defaults to red; use `colors={{ critical: 'your-colour' }}` to override it without recolouring the triangle.
|
|
58
67
|
- Dog and ship have neutral interiors using `--weasel-surface` (white by default). Set this CSS variable on their parent for other backgrounds. Monkey supports `colors.face`.
|
|
59
68
|
|
|
60
69
|
SVGs are decorative and hidden from screen readers. Label the containing control or adjacent status text. Do not communicate state using only color or animation.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -4,9 +4,15 @@ The animated components in this project use original Weasel motion design on top
|
|
|
4
4
|
|
|
5
5
|
## Lucide
|
|
6
6
|
|
|
7
|
-
Rocket,
|
|
7
|
+
Rocket, Dog, Heart, Ship, Bell, Search, Download, Shield, Cursor, Clapperboard, and Plug base SVG geometry is adapted from [Lucide](https://github.com/lucide-icons/lucide), licensed under the ISC License, with the additional Feather notice below for Search and Download. The current front-facing ship replaces the earlier Fluent side-profile implementation. Source SVGs for Bell, Search, Download and their inspected state references are retained in `docs/references/lucide-*.svg`. Shield uses `shield`; Cursor uses `mouse-pointer-2`; Clapperboard adapts `clapperboard` into a left-hinged rig; Plug articulates the two halves of `unplug`. Their source links and design decisions are recorded in `docs/TRACE_ICONS.md`.
|
|
8
|
+
|
|
9
|
+
Bulb, Alert, Badge Check, Replace, Delete and Swap additionally adapt Lucide's `lightbulb`, `triangle-alert`, `badge-check`, `replace`, `delete` and `arrow-left-right` assets. Their motion, rays, filament and status details are original to this project. No `lucide-react` runtime dependency is installed or redistributed.
|
|
10
|
+
|
|
11
|
+
Refresh, Copy, Eye, Link, Check and Close additionally adapt `refresh-cw`, `copy`, `eye` / `eye-off` landmarks, `link` (previously `link-2`), `check` and `x`. Eye uses a compatible cubic eyelid rather than swapping complete SVGs. Reference geometry and state-specific choreography are recorded in `docs/TRACE_UTILITY_ICONS.md`. Check, Link and Close retain the additional Feather MIT notice below.
|
|
12
|
+
|
|
13
|
+
The retained Lucide geometry carries the following [Lucide ISC notice](https://lucide.dev/license):
|
|
14
|
+
Star, Sparkles and Sparkle additionally adapt the current `star` and central `sparkles` contours. Sparkle is the single-contour composition; Sparkles' satellite diamonds and the single-line Wave geometry are original to Weasel. Wave follows the user's visual reference, not Lucide's stacked wave glyph.
|
|
8
15
|
|
|
9
|
-
The retained Dog geometry carries the following [Lucide ISC notice](https://lucide.dev/license):
|
|
10
16
|
|
|
11
17
|
```text
|
|
12
18
|
ISC License
|
|
@@ -28,7 +34,7 @@ PERFORMANCE OF THIS SOFTWARE.
|
|
|
28
34
|
|
|
29
35
|
## Feather-derived Lucide icons
|
|
30
36
|
|
|
31
|
-
Search and
|
|
37
|
+
Search, Download, Alert (listed upstream under its former `alert-triangle` name), Check, Link (`link-2`) and Close (`x`) are listed by Lucide as derived from Feather. Their additional notice is retained from the [upstream license](https://github.com/lucide-icons/lucide/blob/main/LICENSE):
|
|
32
38
|
|
|
33
39
|
```text
|
|
34
40
|
The MIT License (MIT)
|
|
@@ -82,7 +88,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
82
88
|
SOFTWARE
|
|
83
89
|
```
|
|
84
90
|
|
|
85
|
-
|
|
91
|
+
The animation choreography, state behavior and state effects are independently authored for this project. This does not grant rights to third-party artwork displayed by the website.
|
|
92
|
+
|
|
93
|
+
## Website-only illustration references and brand draft
|
|
94
|
+
|
|
95
|
+
The red-panda sheet under `public/art` and its reference-derived raster cutout match [Madi / madivoid's Red Panda Spread Art Print](https://www.etsy.com/listing/1624097837/red-panda-spread-art-print-animal-art), visually checked on October 24, 2026. Reuse permission has not been obtained; the physical-print listing is not a commercial website or branding licence. The website currently uses six poses from this sheet. Older dog/cat sheets remain local historical files but are not rendered; the animated SVG box-cat component has been removed. The eyes/nose/apple logo in `public/weasel-mark.svg` has newly drawn paths, but has not received copyright or trademark clearance. The requested red-panda head crop has not replaced it. See `docs/reference-artwork.md` for provenance, source-research limits and required pre-launch checks. Keep these website design assets local; they are not part of the npm icon package, and its MIT licence does not license the supplied illustrations.
|
|
86
96
|
|
|
87
97
|
## Motion study reference
|
|
88
98
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StatefulIconProps } from "../../shared/icon-types.js";
|
|
2
|
+
import { type AlertState } from "./alert-rig.js";
|
|
3
|
+
import type { AlertColors } from "../../shared/icon-colors.js";
|
|
4
|
+
export { alertStates, type AlertState } from "./alert-rig.js";
|
|
5
|
+
export type AlertIconProps = StatefulIconProps<AlertState, AlertColors>;
|
|
6
|
+
export declare const AlertIcon: import("react").NamedExoticComponent<AlertIconProps>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { memo, useRef } from 'react';
|
|
4
|
+
import { motion, useTransform } from 'motion/react';
|
|
5
|
+
import { usePoseWeight, useBehaviorClock, useActionProgress } from "../../shared/use-icon-motion.js";
|
|
6
|
+
import { useMotionPreference } from "../../shared/use-motion-preference.js";
|
|
7
|
+
import { ALERT_SHELL, sampleAlert } from "./alert-rig.js";
|
|
8
|
+
export { alertStates } from "./alert-rig.js";
|
|
9
|
+
const ORIGIN = { originX: '0px', originY: '0px', transformBox: 'view-box' };
|
|
10
|
+
export const AlertIcon = memo(function AlertIcon({ state = 'idle', size = 24, color = 'currentColor', colors, strokeWidth = 1.65, speed = 1, alive = false, className, }) {
|
|
11
|
+
const reduced = useMotionPreference(), element = useRef(null);
|
|
12
|
+
const pace = Number.isFinite(speed) ? Math.min(4, Math.max(0.1, speed)) : 1;
|
|
13
|
+
const idle = usePoseWeight(state === 'idle', pace, reduced), warning = usePoseWeight(state === 'warning', pace, reduced), critical = usePoseWeight(state === 'critical', pace, reduced), resolved = usePoseWeight(state === 'resolved', pace, reduced);
|
|
14
|
+
const performs = alive && !reduced && state !== 'idle', life = usePoseWeight(performs, pace, reduced), time = useBehaviorClock(element, performs, pace, life);
|
|
15
|
+
const warningEntry = useActionProgress(state === 'warning', pace, reduced, 1.4);
|
|
16
|
+
const criticalEntry = useActionProgress(state === 'critical', pace, reduced, 1.4);
|
|
17
|
+
const rig = useTransform(() => sampleAlert([idle.get(), warning.get(), critical.get(), resolved.get()], time.get(), life.get(), warningEntry.get(), criticalEntry.get()));
|
|
18
|
+
const criticalColor = colors?.critical ?? '#ef4444';
|
|
19
|
+
const blendedColor = useTransform(critical, [0, 1], [color, criticalColor]);
|
|
20
|
+
const body = useTransform(rig, (r) => r.body), shell = useTransform(rig, (r) => r.shell), mark = useTransform(rig, (r) => r.mark), markOpacity = useTransform(rig, (r) => r.markOpacity), dot = useTransform(rig, (r) => r.dot * r.markOpacity), dotY = useTransform(rig, (r) => r.dotY), markColor = useTransform(() => critical.get() === 0 ? color : critical.get() === 1 ? criticalColor : blendedColor.get());
|
|
21
|
+
return (_jsx("svg", { ref: element, className: className, width: size, height: size, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", pointerEvents: "none", style: { display: 'block', overflow: 'visible' }, children: _jsxs(motion.g, { "data-layer": "alert-body", transform: body, style: ORIGIN, children: [_jsx(motion.path, { "data-part": "shell", "data-layer": "alert-shell", d: ALERT_SHELL, transform: shell, style: ORIGIN, stroke: color, strokeWidth: strokeWidth }), _jsx(motion.path, { "data-layer": "alert-mark", d: mark, opacity: markOpacity, stroke: markColor, strokeWidth: Math.min(strokeWidth, 1.7) }), _jsx(motion.circle, { "data-layer": "alert-dot", cx: "12", cy: dotY, r: Math.min(strokeWidth * 0.55, 1.1), opacity: dot, fill: markColor })] }) }));
|
|
22
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const alertStates: readonly ["idle", "warning", "critical", "resolved"];
|
|
2
|
+
export type AlertState = (typeof alertStates)[number];
|
|
3
|
+
export declare const ALERT_SHELL = "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3";
|
|
4
|
+
export declare function sampleAlert(weights: readonly number[], time?: number, alive?: number, entry?: number, criticalEntry?: number): {
|
|
5
|
+
body: string;
|
|
6
|
+
shell: string;
|
|
7
|
+
mark: string;
|
|
8
|
+
markOpacity: number;
|
|
9
|
+
dot: number;
|
|
10
|
+
dotY: number;
|
|
11
|
+
resolved: number;
|
|
12
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { behaviorBeat } from "../../shared/behavior-curves.js";
|
|
2
|
+
export const alertStates = ['idle', 'warning', 'critical', 'resolved'];
|
|
3
|
+
export const ALERT_SHELL = 'm21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3';
|
|
4
|
+
export function sampleAlert(weights, time = 0, alive = 0, entry = 1, criticalEntry = entry) {
|
|
5
|
+
const clamp = (n) => Math.max(0, Math.min(1, n));
|
|
6
|
+
const [, warning, critical, resolved] = weights.map(clamp), life = clamp(alive);
|
|
7
|
+
const actionTime = clamp(entry) * 1.4;
|
|
8
|
+
const envelope = entry >= 1 ? 0 : behaviorBeat(actionTime, 0, 0.15, 0.8, 0.45);
|
|
9
|
+
const blink = Math.max((envelope * (1 - Math.cos(actionTime * Math.PI * 4))) / 2, (life * (1 - Math.cos((time * Math.PI * 2) / 1.4))) / 2);
|
|
10
|
+
const criticalTime = clamp(criticalEntry) * 1.4;
|
|
11
|
+
const criticalEnvelope = criticalEntry >= 1 ? 0 : behaviorBeat(criticalTime, 0, 0.15, 0.8, 0.45);
|
|
12
|
+
const vibration = critical *
|
|
13
|
+
((1 - life) * criticalEnvelope * Math.sin(criticalTime * Math.PI * 10) +
|
|
14
|
+
life * Math.sin(time * Math.PI * 10));
|
|
15
|
+
return {
|
|
16
|
+
body: 'translate(12px, 12px) scale(0.8) translate(-12px, -12px)',
|
|
17
|
+
shell: `translate(12px, 13px) rotate(${vibration * 3.2}deg) translate(-12px, -13px)`,
|
|
18
|
+
// Corresponding three landmarks fold the upright stem into one interior check.
|
|
19
|
+
mark: `M${12 - 2.6 * resolved} ${9 + 5 * resolved}L${12 - 0.7 * resolved} ${11 + 4.9 * resolved}L${12 + 2.1 * resolved} ${13 - 0.1 * resolved}`,
|
|
20
|
+
markOpacity: 1 - warning * blink * 0.85,
|
|
21
|
+
dot: 1 - resolved,
|
|
22
|
+
dotY: 17,
|
|
23
|
+
resolved,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { StatefulIconProps } from "../../shared/icon-types.js";
|
|
2
|
+
import { type BadgeCheckState } from "./badge-check-rig.js";
|
|
3
|
+
export { badgeCheckStates, type BadgeCheckState } from "./badge-check-rig.js";
|
|
4
|
+
export type BadgeCheckIconProps = StatefulIconProps<BadgeCheckState>;
|
|
5
|
+
export declare const BadgeCheckIcon: import("react").NamedExoticComponent<BadgeCheckIconProps>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { memo, useCallback, useRef } from 'react';
|
|
4
|
+
import { motion, useTransform } from 'motion/react';
|
|
5
|
+
import { usePoseWeight, useBehaviorClock, useActionProgress } from "../../shared/use-icon-motion.js";
|
|
6
|
+
import { useMotionPreference } from "../../shared/use-motion-preference.js";
|
|
7
|
+
import { BADGE_SHELL, sampleBadgeCheck } from "./badge-check-rig.js";
|
|
8
|
+
export { badgeCheckStates } from "./badge-check-rig.js";
|
|
9
|
+
const ORIGIN = { originX: '0px', originY: '0px', transformBox: 'view-box' };
|
|
10
|
+
export const BadgeCheckIcon = memo(function BadgeCheckIcon({ state = 'idle', size = 24, color = 'currentColor', accentColor = 'currentColor', strokeWidth = 1.65, speed = 1, alive = false, className, }) {
|
|
11
|
+
const reduced = useMotionPreference(), element = useRef(null);
|
|
12
|
+
const pace = Number.isFinite(speed) ? Math.min(4, Math.max(0.1, speed)) : 1;
|
|
13
|
+
const idle = usePoseWeight(state === 'idle', pace, reduced), checking = usePoseWeight(state === 'checking', pace, reduced), verified = usePoseWeight(state === 'verified', pace, reduced), rejected = usePoseWeight(state === 'rejected', pace, reduced);
|
|
14
|
+
const performs = alive && !reduced && state !== 'idle', life = usePoseWeight(performs, pace, reduced), time = useBehaviorClock(element, performs, pace, life);
|
|
15
|
+
// Mount an already verified value as a still; replay only on a later entry.
|
|
16
|
+
const first = useRef(state !== 'verified');
|
|
17
|
+
const canRestart = useCallback(() => {
|
|
18
|
+
const invisible = first.current || verified.get() < 0.001;
|
|
19
|
+
first.current = false;
|
|
20
|
+
return invisible;
|
|
21
|
+
}, [verified]);
|
|
22
|
+
const entry = useActionProgress(state === 'verified', pace, reduced, 0.9, canRestart);
|
|
23
|
+
const firstRejection = useRef(state !== 'rejected');
|
|
24
|
+
const canRestartRejection = useCallback(() => {
|
|
25
|
+
const invisible = firstRejection.current || rejected.get() < 0.001;
|
|
26
|
+
firstRejection.current = false;
|
|
27
|
+
return invisible;
|
|
28
|
+
}, [rejected]);
|
|
29
|
+
const rejectionEntry = useActionProgress(state === 'rejected', pace, reduced, 0.8, canRestartRejection);
|
|
30
|
+
const rig = useTransform(() => sampleBadgeCheck([idle.get(), checking.get(), verified.get(), rejected.get()], time.get(), life.get(), entry.get(), rejectionEntry.get()));
|
|
31
|
+
const body = useTransform(rig, (r) => r.body), mark = useTransform(rig, (r) => r.mark), markOpacity = useTransform(rig, (r) => r.markOpacity), markOffset = useTransform(rig, (r) => r.markOffset), reverse = useTransform(rig, (r) => r.reverse), cross = useTransform(rig, (r) => r.cross), progress = useTransform(rig, (r) => r.progress), progressOffset = useTransform(rig, (r) => r.progressOffset), shine = useTransform(rig, (r) => r.shine);
|
|
32
|
+
return (_jsx("svg", { ref: element, className: className, width: size, height: size, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", pointerEvents: "none", style: { display: 'block', overflow: 'visible' }, children: _jsxs(motion.g, { "data-layer": "badge-body", transform: body, style: ORIGIN, children: [_jsx("path", { "data-part": "shell", d: BADGE_SHELL, stroke: color, strokeWidth: strokeWidth }), _jsx(motion.path, { "data-layer": "badge-progress", d: "M12 7.5a4.5 4.5 0 1 1 0 9a4.5 4.5 0 1 1 0-9", opacity: progress, stroke: accentColor, strokeWidth: Math.min(strokeWidth * 0.8, 1.5), strokeDasharray: "7 21.274334", strokeDashoffset: progressOffset }), _jsx(motion.path, { "data-layer": "badge-mark", d: mark, opacity: markOpacity, stroke: color, strokeWidth: strokeWidth, strokeDasharray: "12 12", strokeDashoffset: markOffset }), _jsx(motion.path, { "data-layer": "badge-reverse", d: cross, opacity: reverse, stroke: color, strokeWidth: strokeWidth }), _jsx(motion.path, { "data-layer": "badge-shine", d: "M7.2 6.3a2 2 0 0 1 1.7-.3", opacity: shine, stroke: accentColor, strokeWidth: strokeWidth * 0.65 })] }) }));
|
|
33
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const badgeCheckStates: readonly ["idle", "checking", "verified", "rejected"];
|
|
2
|
+
export type BadgeCheckState = (typeof badgeCheckStates)[number];
|
|
3
|
+
export declare const BADGE_SHELL = "M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z";
|
|
4
|
+
export declare function sampleBadgeCheck(weights: readonly number[], time?: number, alive?: number, entry?: number, rejectionEntry?: number): {
|
|
5
|
+
body: string;
|
|
6
|
+
mark: string;
|
|
7
|
+
markOpacity: number;
|
|
8
|
+
markOffset: number;
|
|
9
|
+
reverse: number;
|
|
10
|
+
cross: string;
|
|
11
|
+
progress: number;
|
|
12
|
+
progressOffset: number;
|
|
13
|
+
shine: number;
|
|
14
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { behaviorBeat } from "../../shared/behavior-curves.js";
|
|
2
|
+
export const badgeCheckStates = ['idle', 'checking', 'verified', 'rejected'];
|
|
3
|
+
export const BADGE_SHELL = 'M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z';
|
|
4
|
+
export function sampleBadgeCheck(weights, time = 0, alive = 0, entry = 1, rejectionEntry = 1) {
|
|
5
|
+
const clamp = (n) => Math.max(0, Math.min(1, n));
|
|
6
|
+
const smooth = (n) => {
|
|
7
|
+
const t = clamp(n);
|
|
8
|
+
return t * t * t * (t * (t * 6 - 15) + 10);
|
|
9
|
+
};
|
|
10
|
+
const [, checking, verified, rejected] = weights.map(clamp), life = clamp(alive);
|
|
11
|
+
const t = time % 7.2;
|
|
12
|
+
const stamp = Math.max(behaviorBeat(entry * 0.9, 0.3, 0.18, 0.06, 0.36), life * behaviorBeat(t, 0.5, 0.18, 0.08, 0.55));
|
|
13
|
+
const shine = behaviorBeat(t, 3.9, 0.45, 0.1, 0.7);
|
|
14
|
+
const refuse = life * rejected * behaviorBeat(t, 0.35, 0.35, 0.12, 0.7);
|
|
15
|
+
const draw = smooth(entry / 0.68);
|
|
16
|
+
const crossDraw = smooth((rejectionEntry - 0.1) / 0.72);
|
|
17
|
+
const cross = [
|
|
18
|
+
[8.5, 8.5],
|
|
19
|
+
[15.5, 8.5],
|
|
20
|
+
[15.5, 15.5],
|
|
21
|
+
[8.5, 15.5],
|
|
22
|
+
]
|
|
23
|
+
.map(([x, y]) => `M${x} ${y}L${x + (12 - x) * crossDraw} ${y + (12 - y) * crossDraw}`)
|
|
24
|
+
.join('');
|
|
25
|
+
return {
|
|
26
|
+
body: `translate(12px, 12px) rotate(${-4 * refuse}deg) scale(${0.84 + verified * stamp * 0.027}) translate(-12px, -12px)`,
|
|
27
|
+
mark: `M${8 + 0.5 * rejected} ${12 - 3.5 * rejected}L${10.5 + 1.5 * rejected} ${14.5 - 2.5 * rejected}L${16 - 0.5 * rejected} ${9 + 6.5 * rejected}`,
|
|
28
|
+
markOpacity: (1 - checking) * (1 - rejected) * (1 - verified + verified * smooth(draw / 0.025)),
|
|
29
|
+
markOffset: verified * (1 - draw) * 12,
|
|
30
|
+
reverse: rejected * smooth(crossDraw / 0.04),
|
|
31
|
+
cross,
|
|
32
|
+
progress: checking,
|
|
33
|
+
progressOffset: -Math.PI * 9 * (time / 1.5) * life,
|
|
34
|
+
shine: verified * life * shine,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -7,7 +7,7 @@ import { usePoseWeight, useBehaviorClock } from "../../shared/use-icon-motion.js
|
|
|
7
7
|
import { sampleBell, BELL_SHELL, BELL_CLAPPER } from "./bell-rig.js";
|
|
8
8
|
export { bellStates } from "./bell-rig.js";
|
|
9
9
|
const ORIGIN = { originX: 0, originY: 0, transformBox: 'view-box' };
|
|
10
|
-
export const BellIcon = memo(function BellIcon({ state = 'idle', size = 24, color = 'currentColor', accentColor = 'currentColor', strokeWidth = 1.
|
|
10
|
+
export const BellIcon = memo(function BellIcon({ state = 'idle', size = 24, color = 'currentColor', accentColor = 'currentColor', strokeWidth = 1.65, speed = 1, alive = false, className, }) {
|
|
11
11
|
const reduced = useMotionPreference(), element = useRef(null), id = useId();
|
|
12
12
|
const pace = Number.isFinite(speed) ? Math.min(4, Math.max(0.1, speed)) : 1;
|
|
13
13
|
const idle = usePoseWeight(state === 'idle', pace, reduced), ringing = usePoseWeight(state === 'ringing', pace, reduced);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { StatefulIconProps } from "../../shared/icon-types.js";
|
|
2
|
+
import { type BulbState } from "./bulb-rig.js";
|
|
3
|
+
export { bulbStates, type BulbState } from "./bulb-rig.js";
|
|
4
|
+
export type BulbIconProps = StatefulIconProps<BulbState>;
|
|
5
|
+
export declare const BulbIcon: import("react").NamedExoticComponent<BulbIconProps>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { memo, useCallback, useRef } from 'react';
|
|
4
|
+
import { motion, useTransform } from 'motion/react';
|
|
5
|
+
import { usePoseWeight, useBehaviorClock, useActionProgress } from "../../shared/use-icon-motion.js";
|
|
6
|
+
import { useMotionPreference } from "../../shared/use-motion-preference.js";
|
|
7
|
+
import { BULB_SHELL, sampleBulb } from "./bulb-rig.js";
|
|
8
|
+
export { bulbStates } from "./bulb-rig.js";
|
|
9
|
+
const ORIGIN = { originX: '0px', originY: '0px', transformBox: 'view-box' };
|
|
10
|
+
export const BulbIcon = memo(function BulbIcon({ state = 'off', size = 24, color = 'currentColor', accentColor = 'currentColor', strokeWidth = 1.65, speed = 1, alive = false, className, }) {
|
|
11
|
+
const reduced = useMotionPreference(), element = useRef(null);
|
|
12
|
+
const pace = Number.isFinite(speed) ? Math.min(4, Math.max(0.1, speed)) : 1;
|
|
13
|
+
const off = usePoseWeight(state === 'off', pace, reduced), thinking = usePoseWeight(state === 'thinking', pace, reduced), on = usePoseWeight(state === 'on', pace, reduced), fault = usePoseWeight(state === 'fault', pace, reduced);
|
|
14
|
+
const performs = alive && !reduced && state !== 'off', life = usePoseWeight(performs, pace, reduced), time = useBehaviorClock(element, performs, pace, life);
|
|
15
|
+
const first = useRef(state !== 'on');
|
|
16
|
+
const canRestart = useCallback(() => {
|
|
17
|
+
const invisible = first.current || on.get() < 0.001;
|
|
18
|
+
first.current = false;
|
|
19
|
+
return invisible;
|
|
20
|
+
}, [on]);
|
|
21
|
+
const entry = useActionProgress(state === 'on', pace, reduced, 1, canRestart);
|
|
22
|
+
const rig = useTransform(() => sampleBulb([off.get(), thinking.get(), on.get(), fault.get()], time.get(), life.get(), entry.get()));
|
|
23
|
+
const body = useTransform(rig, (r) => r.body), filament = useTransform(rig, (r) => r.filament), filamentOpacity = useTransform(rig, (r) => r.filamentOpacity), light = useTransform(rig, (r) => r.light), rays = useTransform(rig, (r) => r.rays), failure = useTransform(rig, (r) => r.fault), shine = useTransform(rig, (r) => r.shine);
|
|
24
|
+
return (_jsx("svg", { ref: element, className: className, width: size, height: size, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", pointerEvents: "none", style: { display: 'block', overflow: 'visible' }, children: _jsxs(motion.g, { "data-layer": "bulb-body", transform: body, style: ORIGIN, children: [_jsx(motion.path, { "data-layer": "bulb-rays", d: rays, opacity: light, stroke: accentColor, strokeWidth: Math.min(strokeWidth * 0.75, 1.4) }), _jsx("path", { "data-part": "shell", d: BULB_SHELL, stroke: color, strokeWidth: strokeWidth }), _jsx("path", { "data-part": "base", d: "M9 18h6M10 22h4", stroke: color, strokeWidth: strokeWidth }), _jsx(motion.path, { "data-layer": "bulb-filament", d: filament, opacity: filamentOpacity, stroke: accentColor, strokeWidth: Math.min(strokeWidth * 0.75, 1.4) }), _jsx(motion.path, { "data-layer": "bulb-fault", d: "m10 8 4 4m0-4-4 4", opacity: failure, stroke: color, strokeWidth: Math.min(strokeWidth * 0.75, 1.4) }), _jsx(motion.path, { "data-layer": "bulb-shine", d: "M8.2 7.2a4 4 0 0 1 1.5-2.4", opacity: shine, stroke: accentColor, strokeWidth: strokeWidth * 0.6 })] }) }));
|
|
25
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const bulbStates: readonly ["off", "thinking", "on", "fault"];
|
|
2
|
+
export type BulbState = (typeof bulbStates)[number];
|
|
3
|
+
/** Lucide lightbulb contour. Rays and filament are original supporting details. */
|
|
4
|
+
export declare const BULB_SHELL = "M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5";
|
|
5
|
+
export declare function sampleBulb(weights: readonly number[], time?: number, alive?: number, entry?: number): {
|
|
6
|
+
body: string;
|
|
7
|
+
filament: string;
|
|
8
|
+
light: number;
|
|
9
|
+
filamentOpacity: number;
|
|
10
|
+
rays: string;
|
|
11
|
+
fault: number;
|
|
12
|
+
shine: number;
|
|
13
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { behaviorBeat } from "../../shared/behavior-curves.js";
|
|
2
|
+
export const bulbStates = ['off', 'thinking', 'on', 'fault'];
|
|
3
|
+
/** Lucide lightbulb contour. Rays and filament are original supporting details. */
|
|
4
|
+
export const BULB_SHELL = 'M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5';
|
|
5
|
+
export function sampleBulb(weights, time = 0, alive = 0, entry = 1) {
|
|
6
|
+
const clamp = (n) => Math.max(0, Math.min(1, n));
|
|
7
|
+
const [, thinking, on, fault] = weights.map(clamp), life = clamp(alive);
|
|
8
|
+
const t = time % 8;
|
|
9
|
+
// A tentative filament warms twice, then considers; never falsely signals ON.
|
|
10
|
+
const warm = behaviorBeat(t, 0.2, 0.6, 0.1, 0.55) +
|
|
11
|
+
0.65 * behaviorBeat(t, 2.05, 0.3, 0.15, 0.6) +
|
|
12
|
+
0.8 * behaviorBeat(t, 4.9, 0.7, 0.25, 0.7);
|
|
13
|
+
const glint = behaviorBeat(t, 0.8, 0.35, 0.1, 0.9) + 0.65 * behaviorBeat(t, 5, 0.35, 0.1, 0.8);
|
|
14
|
+
const bloom = behaviorBeat(clamp(entry), 0.08, 0.23, 0.12, 0.5);
|
|
15
|
+
const emission = clamp(Math.max(bloom, life * glint));
|
|
16
|
+
// Five rays grow outwards together; the invisible gap separates each repeat.
|
|
17
|
+
const reach = 0.2 + emission * 0.8;
|
|
18
|
+
const rays = [Math.PI, Math.PI * 1.25, Math.PI * 1.5, Math.PI * 1.75, 0]
|
|
19
|
+
.map((angle) => {
|
|
20
|
+
const radius = 9.4 + emission * 0.45;
|
|
21
|
+
const x = 12 + Math.cos(angle) * radius;
|
|
22
|
+
const y = 8 + Math.sin(angle) * radius;
|
|
23
|
+
return `M${x} ${y}L${x + Math.cos(angle) * reach} ${y + Math.sin(angle) * reach}`;
|
|
24
|
+
})
|
|
25
|
+
.join('');
|
|
26
|
+
const retry = behaviorBeat(t, 0.35, 0.25, 0.1, 0.5) - 0.65 * behaviorBeat(t, 3.8, 0.3, 0.1, 0.6);
|
|
27
|
+
const scale = 0.78 + on * 0.015 + thinking * life * warm * 0.014;
|
|
28
|
+
return {
|
|
29
|
+
body: `translate(12px, 18px) rotate(${fault * life * retry * -5}deg) scale(${scale}) translate(-12px, -18px)`,
|
|
30
|
+
filament: 'M10 10.5L12 12.5L14 10.5M12 12.5V15',
|
|
31
|
+
light: on * emission,
|
|
32
|
+
filamentOpacity: thinking * (0.3 + life * warm * 0.65) + on,
|
|
33
|
+
rays,
|
|
34
|
+
fault,
|
|
35
|
+
shine: on * emission,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { StatefulIconProps } from "../../shared/icon-types.js";
|
|
2
|
+
import { type CheckState } from "./check-rig.js";
|
|
3
|
+
export { checkStates, type CheckState } from "./check-rig.js";
|
|
4
|
+
export type CheckIconProps = StatefulIconProps<CheckState>;
|
|
5
|
+
export declare const CheckIcon: import("react").NamedExoticComponent<CheckIconProps>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { memo, useRef, useCallback } from 'react';
|
|
4
|
+
import { motion, useTransform } from 'motion/react';
|
|
5
|
+
import { usePoseWeight, useBehaviorClock, useActionProgress } from "../../shared/use-icon-motion.js";
|
|
6
|
+
import { useMotionPreference } from "../../shared/use-motion-preference.js";
|
|
7
|
+
import { sampleCheck } from "./check-rig.js";
|
|
8
|
+
export { checkStates } from "./check-rig.js";
|
|
9
|
+
const ORIGIN = { originX: '0px', originY: '0px', transformBox: 'view-box' };
|
|
10
|
+
export const CheckIcon = memo(function CheckIcon({ state = 'checked', size = 24, color = 'currentColor', strokeWidth = 1.65, speed = 1, alive = false, className, }) {
|
|
11
|
+
const reduced = useMotionPreference(), element = useRef(null);
|
|
12
|
+
const pace = Number.isFinite(speed) ? Math.min(4, Math.max(0.1, speed)) : 1;
|
|
13
|
+
const w0 = usePoseWeight(state === 'checked', pace, reduced);
|
|
14
|
+
const w1 = usePoseWeight(state === 'checking', pace, reduced);
|
|
15
|
+
const w2 = usePoseWeight(state === 'unchecked', pace, reduced);
|
|
16
|
+
const performs = alive && !reduced && state === 'checking';
|
|
17
|
+
const life = usePoseWeight(performs, pace, reduced), time = useBehaviorClock(element, performs, pace, life);
|
|
18
|
+
const can0 = useCallback(() => w1.get() < 0.001, [w1]);
|
|
19
|
+
const e0 = useActionProgress(state === 'checking', pace, reduced, 1.1, can0);
|
|
20
|
+
const can1 = useCallback(() => w0.get() < 0.001, [w0]);
|
|
21
|
+
const e1 = useActionProgress(state === 'checked', pace, reduced, 0.85, can1);
|
|
22
|
+
const rig = useTransform(() => sampleCheck([w0.get(), w1.get(), w2.get()], time.get(), life.get(), e0.get(), e1.get()));
|
|
23
|
+
const mark = useTransform(rig, (r) => r.mark);
|
|
24
|
+
const confirm = useTransform(rig, (r) => r.confirm);
|
|
25
|
+
return (_jsx("svg", { ref: element, className: className, width: size, height: size, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", fill: "none", stroke: color, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", pointerEvents: "none", style: { display: 'block' }, children: _jsx("g", { "data-layer": "check-body", transform: "translate(12 12) scale(.84) translate(-12 -12)", children: _jsx(motion.path, { "data-layer": "check-mark", "data-part": "mark", d: mark, transform: confirm, style: ORIGIN }) }) }));
|
|
26
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const checkStates: readonly ["checked", "checking", "unchecked"];
|
|
2
|
+
export type CheckState = (typeof checkStates)[number];
|
|
3
|
+
export declare function sampleCheck(weights: readonly number[], time?: number, alive?: number, entry?: number, successEntry?: number): {
|
|
4
|
+
mark: string;
|
|
5
|
+
confirm: string;
|
|
6
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { behaviorBeat } from "../../shared/behavior-curves.js";
|
|
2
|
+
const clamp = (n) => Math.max(0, Math.min(1, Number.isFinite(n) ? n : 0));
|
|
3
|
+
const normalized = (values) => {
|
|
4
|
+
const w = values.map(clamp), sum = w.reduce((a, b) => a + b, 0) || 1;
|
|
5
|
+
return w.map((v) => v / sum);
|
|
6
|
+
};
|
|
7
|
+
export const checkStates = ['checked', 'checking', 'unchecked'];
|
|
8
|
+
export function sampleCheck(weights, time = 0, alive = 0, entry = 1, successEntry = 1) {
|
|
9
|
+
const [checked, checking] = normalized(weights), life = clamp(alive);
|
|
10
|
+
const entryWave = behaviorBeat(clamp(entry) * 1.1, 0.08, 0.22, 0.05, 0.65);
|
|
11
|
+
const wave = checking * ((1 - life) * entryWave * 1.2 + life * Math.sin(time * 4.2) * 1.4);
|
|
12
|
+
const x0 = 8 - checked * 4 - checking * 2, x1 = 12 - checked * 3, x2 = 16 + checked * 4 + checking * 2;
|
|
13
|
+
const y0 = 12 + checking * life * Math.sin(time * 4.2) * 0.35, crest = checking * 2.6 + wave, knee = 12 + checked * 5;
|
|
14
|
+
const pop = checked * behaviorBeat(clamp(successEntry) * 0.85, 0.12, 0.15, 0.03, 0.48);
|
|
15
|
+
return {
|
|
16
|
+
mark: 'M' +
|
|
17
|
+
x0 +
|
|
18
|
+
' ' +
|
|
19
|
+
y0 +
|
|
20
|
+
'Q' +
|
|
21
|
+
(x0 + x1) / 2 +
|
|
22
|
+
' ' +
|
|
23
|
+
(12 + checked * 2.5 - crest) +
|
|
24
|
+
' ' +
|
|
25
|
+
x1 +
|
|
26
|
+
' ' +
|
|
27
|
+
knee +
|
|
28
|
+
'Q' +
|
|
29
|
+
(x1 + x2) / 2 +
|
|
30
|
+
' ' +
|
|
31
|
+
(12 - checked * 0.5 + crest) +
|
|
32
|
+
' ' +
|
|
33
|
+
x2 +
|
|
34
|
+
' ' +
|
|
35
|
+
(12 - checked * 6),
|
|
36
|
+
confirm: 'translate(9px, 17px) scale(' + (1 + pop * 0.07) + ') translate(-9px, -17px)',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { StatefulIconProps } from "../../shared/icon-types.js";
|
|
2
|
+
import { type ClapperboardState } from "./clapperboard-rig.js";
|
|
3
|
+
export { clapperboardStates, type ClapperboardState } from "./clapperboard-rig.js";
|
|
4
|
+
export type ClapperboardIconProps = StatefulIconProps<ClapperboardState>;
|
|
5
|
+
export declare const ClapperboardIcon: import("react").NamedExoticComponent<ClapperboardIconProps>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { memo, useRef } from 'react';
|
|
4
|
+
import { motion, useTransform } from 'motion/react';
|
|
5
|
+
import { usePoseWeight, useBehaviorClock } from "../../shared/use-icon-motion.js";
|
|
6
|
+
import { useMotionPreference } from "../../shared/use-motion-preference.js";
|
|
7
|
+
import { CLAPPERBOARD_BASE, CLAPPERBOARD_ARM, sampleClapperboard, } from "./clapperboard-rig.js";
|
|
8
|
+
export { clapperboardStates } from "./clapperboard-rig.js";
|
|
9
|
+
const ORIGIN = { originX: '0px', originY: '0px', transformBox: 'view-box' };
|
|
10
|
+
export const ClapperboardIcon = memo(function ClapperboardIcon({ state = 'idle', size = 24, color = 'currentColor', accentColor = 'currentColor', strokeWidth = 1.65, speed = 1, alive = false, className, }) {
|
|
11
|
+
const reduced = useMotionPreference(), element = useRef(null);
|
|
12
|
+
const pace = Number.isFinite(speed) ? Math.min(4, Math.max(0.1, speed)) : 1;
|
|
13
|
+
const idle = usePoseWeight(state === 'idle', pace, reduced), clapping = usePoseWeight(state === 'clapping', pace, reduced), recording = usePoseWeight(state === 'recording', pace, reduced), paused = usePoseWeight(state === 'paused', pace, reduced);
|
|
14
|
+
const performs = alive && !reduced && (state === 'clapping' || state === 'recording'), life = usePoseWeight(performs, pace, reduced), time = useBehaviorClock(element, performs, pace, life);
|
|
15
|
+
const rig = useTransform(() => sampleClapperboard([idle.get(), clapping.get(), recording.get(), paused.get()], time.get(), life.get()));
|
|
16
|
+
const body = useTransform(rig, (r) => r.body), arm = useTransform(rig, (r) => r.arm), contact = useTransform(rig, (r) => r.contact), record = useTransform(rig, (r) => r.recording), radius = useTransform(rig, (r) => r.recordRadius), pause = useTransform(rig, (r) => r.paused);
|
|
17
|
+
return (_jsx("svg", { ref: element, className: className, width: size, height: size, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", pointerEvents: "none", style: { display: 'block', overflow: 'visible' }, children: _jsxs(motion.g, { "data-layer": "clapperboard-body", transform: body, style: ORIGIN, children: [_jsx("path", { "data-part": "base", d: CLAPPERBOARD_BASE, stroke: color, strokeWidth: strokeWidth }), _jsxs(motion.g, { "data-layer": "clapperboard-arm", transform: arm, style: ORIGIN, stroke: color, strokeWidth: strokeWidth, children: [_jsx("path", { "data-part": "arm", d: CLAPPERBOARD_ARM }), _jsx("path", { "data-part": "stripes", d: "m7 9 2 3m4-3 2 3" })] }), _jsx(motion.path, { "data-layer": "clapperboard-contact", d: "M23.95 10l.7-.7M23.95 12h.7", stroke: accentColor, strokeWidth: strokeWidth * 0.4, opacity: contact }), _jsx(motion.circle, { "data-layer": "clapperboard-record", cx: "12", cy: "16.8", r: radius, fill: accentColor, opacity: record }), _jsx(motion.path, { "data-layer": "clapperboard-pause", d: "M10 15.3v3M14 15.3v3", stroke: color, strokeWidth: strokeWidth, opacity: pause })] }) }));
|
|
18
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const clapperboardStates: readonly ["idle", "clapping", "recording", "paused"];
|
|
2
|
+
export type ClapperboardState = (typeof clapperboardStates)[number];
|
|
3
|
+
export declare const CLAPPERBOARD_BASE = "M3 12h18v8a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1Z";
|
|
4
|
+
export declare const CLAPPERBOARD_ARM = "M3 9h17a1 1 0 0 1 1 1v2H3Z";
|
|
5
|
+
export declare function sampleClapperboard(weights: readonly number[], time?: number, alive?: number): {
|
|
6
|
+
body: string;
|
|
7
|
+
arm: string;
|
|
8
|
+
contact: number;
|
|
9
|
+
recording: number;
|
|
10
|
+
recordRadius: number;
|
|
11
|
+
paused: number;
|
|
12
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { behaviorBeat } from "../../shared/behavior-curves.js";
|
|
2
|
+
export const clapperboardStates = ['idle', 'clapping', 'recording', 'paused'];
|
|
3
|
+
// Lucide slate proportions; the straightened top is articulated at the left hinge.
|
|
4
|
+
export const CLAPPERBOARD_BASE = 'M3 12h18v8a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1Z';
|
|
5
|
+
export const CLAPPERBOARD_ARM = 'M3 9h17a1 1 0 0 1 1 1v2H3Z';
|
|
6
|
+
const clamp = (n) => Math.max(0, Math.min(1, n));
|
|
7
|
+
export function sampleClapperboard(weights, time = 0, alive = 0) {
|
|
8
|
+
const [idle, c, r, p] = weights.map(clamp), life = clamp(alive), t = time % 7.6;
|
|
9
|
+
const lift = behaviorBeat(t, 0.12, 0.45, 0.08, 0.22) + 0.7 * behaviorBeat(t, 4.2, 0.35, 0.08, 0.2);
|
|
10
|
+
const close = behaviorBeat(t, 0.65, 0.18, 0.08, 0.7) + behaviorBeat(t, 4.63, 0.16, 0.08, 0.65);
|
|
11
|
+
const impact = behaviorBeat(t, 0.83, 0.07, 0.02, 0.35) + 0.7 * behaviorBeat(t, 4.79, 0.07, 0.02, 0.3);
|
|
12
|
+
const record = behaviorBeat(t, 0.2, 0.45, 0.3, 0.65) + 0.65 * behaviorBeat(t, 3.5, 0.4, 0.3, 0.65);
|
|
13
|
+
// A non-living clap closes once as the state weight arrives. Living behavior
|
|
14
|
+
// opens it again for authored takes, without replacing the arm or resetting time.
|
|
15
|
+
const anticipation = Math.sin(Math.PI * c) * 16 * (1 - life) * (idle + p);
|
|
16
|
+
const angle = Math.min(0, -16 * (idle + p) - p * 5 - anticipation + c * life * (-16 - 12 * lift + 16 * close));
|
|
17
|
+
return {
|
|
18
|
+
body: `translate(12px, 12px) scale(.84) translate(-12px, -12px) translate(0px, ${c * life * impact * 0.35}px)`,
|
|
19
|
+
arm: `translate(3px, 12px) rotate(${angle}deg) translate(-3px, -12px)`,
|
|
20
|
+
contact: c * life * impact,
|
|
21
|
+
recording: r,
|
|
22
|
+
recordRadius: 1.5 + r * life * record * 0.16,
|
|
23
|
+
paused: p,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { StatefulIconProps } from "../../shared/icon-types.js";
|
|
2
|
+
import { type CloseState } from "./close-rig.js";
|
|
3
|
+
export { closeStates, type CloseState } from "./close-rig.js";
|
|
4
|
+
export type CloseIconProps = StatefulIconProps<CloseState>;
|
|
5
|
+
export declare const CloseIcon: import("react").NamedExoticComponent<CloseIconProps>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { memo, useRef, useCallback } from 'react';
|
|
4
|
+
import { motion, useTransform } from 'motion/react';
|
|
5
|
+
import { usePoseWeight, useBehaviorClock, useActionProgress } from "../../shared/use-icon-motion.js";
|
|
6
|
+
import { useMotionPreference } from "../../shared/use-motion-preference.js";
|
|
7
|
+
import { CLOSE_FIRST, CLOSE_SECOND, sampleClose } from "./close-rig.js";
|
|
8
|
+
export { closeStates } from "./close-rig.js";
|
|
9
|
+
export const CloseIcon = memo(function CloseIcon({ state = 'ready', size = 24, color = 'currentColor', accentColor = 'currentColor', strokeWidth = 1.65, speed = 1, alive = false, className, }) {
|
|
10
|
+
const reduced = useMotionPreference(), element = useRef(null);
|
|
11
|
+
const pace = Number.isFinite(speed) ? Math.min(4, Math.max(0.1, speed)) : 1;
|
|
12
|
+
const w0 = usePoseWeight(state === 'ready', pace, reduced);
|
|
13
|
+
const w1 = usePoseWeight(state === 'closing', pace, reduced);
|
|
14
|
+
const w2 = usePoseWeight(state === 'blocked', pace, reduced);
|
|
15
|
+
const performs = alive && !reduced && (state === 'closing' || state === 'blocked');
|
|
16
|
+
const life = usePoseWeight(performs, pace, reduced), time = useBehaviorClock(element, performs, pace, life);
|
|
17
|
+
const can0 = useCallback(() => w1.get() < 0.001, [w1]);
|
|
18
|
+
const e0 = useActionProgress(state === 'closing', pace, reduced, 1.15, can0);
|
|
19
|
+
const can1 = useCallback(() => w2.get() < 0.001, [w2]);
|
|
20
|
+
const e1 = useActionProgress(state === 'blocked', pace, reduced, 1, can1);
|
|
21
|
+
const rig = useTransform(() => sampleClose([w0.get(), w1.get(), w2.get()], time.get(), life.get(), e0.get(), e1.get()));
|
|
22
|
+
const firstDash = useTransform(rig, (r) => r.firstDash);
|
|
23
|
+
const secondDash = useTransform(rig, (r) => r.secondDash);
|
|
24
|
+
const stop = useTransform(rig, (r) => r.stop);
|
|
25
|
+
const stopOpacity = useTransform(rig, (r) => r.stopOpacity);
|
|
26
|
+
return (_jsx("svg", { ref: element, className: className, width: size, height: size, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", fill: "none", stroke: color, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", pointerEvents: "none", style: { display: 'block' }, children: _jsxs("g", { "data-layer": "close-body", transform: "translate(12 12) scale(.84) translate(-12 -12)", children: [_jsxs("g", { "data-layer": "close-arms", children: [_jsx(motion.path, { "data-part": "first", d: CLOSE_FIRST, strokeDasharray: firstDash }), _jsx(motion.path, { "data-part": "second", d: CLOSE_SECOND, strokeDasharray: secondDash })] }), _jsx(motion.path, { "data-layer": "close-stop", d: stop, opacity: stopOpacity, stroke: accentColor, strokeWidth: Math.min(strokeWidth, 1.6) })] }) }));
|
|
27
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const closeStates: readonly ["ready", "closing", "blocked"];
|
|
2
|
+
export type CloseState = (typeof closeStates)[number];
|
|
3
|
+
export declare const CLOSE_FIRST = "M6 6L18 18";
|
|
4
|
+
export declare const CLOSE_SECOND = "M18 6L6 18";
|
|
5
|
+
export declare function sampleClose(weights: readonly number[], time?: number, alive?: number, entry?: number, successEntry?: number): {
|
|
6
|
+
firstDash: string;
|
|
7
|
+
secondDash: string;
|
|
8
|
+
stop: string;
|
|
9
|
+
stopOpacity: number;
|
|
10
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { behaviorBeat } from "../../shared/behavior-curves.js";
|
|
2
|
+
const clamp = (n) => Math.max(0, Math.min(1, Number.isFinite(n) ? n : 0));
|
|
3
|
+
const normalized = (values) => {
|
|
4
|
+
const w = values.map(clamp), sum = w.reduce((a, b) => a + b, 0) || 1;
|
|
5
|
+
return w.map((v) => v / sum);
|
|
6
|
+
};
|
|
7
|
+
export const closeStates = ['ready', 'closing', 'blocked'];
|
|
8
|
+
export const CLOSE_FIRST = 'M6 6L18 18';
|
|
9
|
+
export const CLOSE_SECOND = 'M18 6L6 18';
|
|
10
|
+
const LENGTH = Math.hypot(12, 12);
|
|
11
|
+
export function sampleClose(weights, time = 0, alive = 0, entry = 1, successEntry = 1) {
|
|
12
|
+
const [, closing, blocked] = normalized(weights), life = clamp(alive), t = time % 4.4;
|
|
13
|
+
// Ink retracts towards the start, then crosses the diagonal again. The
|
|
14
|
+
// second stroke follows only after the first is substantially written.
|
|
15
|
+
const rewrite = (s, delay) => behaviorBeat(s, 0.02 + delay, 0.14, 0.02, 0.38);
|
|
16
|
+
const ink = (delay) => 1 -
|
|
17
|
+
0.84 * closing * ((1 - life) * rewrite(clamp(entry) * 1.15, delay) + life * rewrite(t, delay));
|
|
18
|
+
const refusal = (s) => behaviorBeat(s, 0.05, 0.18, 0.03, 0.35);
|
|
19
|
+
const stopAttempt = blocked * ((1 - life) * refusal(clamp(successEntry)) + life * refusal(t));
|
|
20
|
+
const gap = blocked * 0.45;
|
|
21
|
+
const dash = (amount) => {
|
|
22
|
+
const arm = (LENGTH * amount * (1 - gap)) / 2;
|
|
23
|
+
return [arm, LENGTH * gap, arm, LENGTH * 2 - arm * 2 - LENGTH * gap].join(' ');
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
firstDash: dash(ink(0) * (1 - stopAttempt * 0.16)),
|
|
27
|
+
secondDash: dash(ink(0.29) * (1 - stopAttempt * 0.16)),
|
|
28
|
+
stop: 'M10.5 12H13.5',
|
|
29
|
+
stopOpacity: blocked,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { StatefulIconProps } from "../../shared/icon-types.js";
|
|
2
|
+
import { type CopyState } from "./copy-rig.js";
|
|
3
|
+
export { copyStates, type CopyState } from "./copy-rig.js";
|
|
4
|
+
export type CopyIconProps = StatefulIconProps<CopyState>;
|
|
5
|
+
export declare const CopyIcon: import("react").NamedExoticComponent<CopyIconProps>;
|