@a4ui/core 0.14.1 → 0.16.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 +23 -23
- package/dist/Checkbox-B5Gb3h5J.js +400 -0
- package/dist/commerce.js +1 -1
- package/dist/elements.css +6 -0
- package/dist/elements.iife.js +2 -2
- package/dist/elements.js +2919 -2848
- package/dist/full.css +6 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +2287 -2359
- package/dist/ui/Button.d.ts +7 -0
- package/dist/ui/Card.d.ts +15 -1
- package/dist/ui/GradientText.d.ts +25 -0
- package/dist/ui/Magnetic.d.ts +22 -0
- package/dist/ui/Ripple.d.ts +14 -1
- package/dist/ui/ScrollProgress.d.ts +20 -0
- package/dist/ui/Spotlight.d.ts +36 -0
- package/dist/ui/TiltCard.d.ts +34 -0
- package/package.json +2 -1
- package/src/index.ts +7 -2
- package/src/ui/Button.tsx +24 -2
- package/src/ui/Card.tsx +32 -3
- package/src/ui/GradientText.tsx +69 -0
- package/src/ui/Magnetic.tsx +73 -0
- package/src/ui/Ripple.tsx +64 -75
- package/src/ui/ScrollProgress.tsx +61 -0
- package/src/ui/Spotlight.tsx +94 -0
- package/src/ui/TiltCard.tsx +89 -0
- package/dist/Checkbox-DQcDOBLd.js +0 -168
package/dist/ui/Button.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ interface ButtonProps extends ParentProps, Omit<JSX.ButtonHTMLAttributes<HTMLBut
|
|
|
7
7
|
class?: string;
|
|
8
8
|
/** Defaults to "button" so action buttons inside a form never submit it by accident. */
|
|
9
9
|
type?: 'button' | 'submit' | 'reset';
|
|
10
|
+
/**
|
|
11
|
+
* Material-style click ripple from the press position. Engine-free (Web
|
|
12
|
+
* Animations API, shared with {@link spawnRipple}) and reduced-motion aware —
|
|
13
|
+
* costs nothing when off.
|
|
14
|
+
*/
|
|
15
|
+
ripple?: boolean;
|
|
10
16
|
}
|
|
11
17
|
/**
|
|
12
18
|
* Base button primitive: a plain `<button>` with A4ui's variants, focus ring,
|
|
@@ -15,6 +21,7 @@ interface ButtonProps extends ParentProps, Omit<JSX.ButtonHTMLAttributes<HTMLBut
|
|
|
15
21
|
* @example
|
|
16
22
|
* ```tsx
|
|
17
23
|
* <Button variant="outline" onClick={() => save()}>Save</Button>
|
|
24
|
+
* <Button ripple onClick={() => save()}>Save</Button> // with a click ripple
|
|
18
25
|
* ```
|
|
19
26
|
*/
|
|
20
27
|
export declare function Button(props: ButtonProps): JSX.Element;
|
package/dist/ui/Card.d.ts
CHANGED
|
@@ -10,15 +10,29 @@ interface CardProps extends DivProps {
|
|
|
10
10
|
* for glass cards so the look is uniform; pass `glow={false}` to opt out.
|
|
11
11
|
*/
|
|
12
12
|
glow?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* 3D tilt toward the cursor on hover — the same primitive as `<TiltCard>`,
|
|
15
|
+
* baked in ({@link attachTilt}). Engine-free and reduced-motion aware.
|
|
16
|
+
*/
|
|
17
|
+
tilt?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Soft radial glow following the cursor inside the card — the same primitive
|
|
20
|
+
* as `<Spotlight>`, baked in ({@link attachSpotlight}). Engine-free and
|
|
21
|
+
* reduced-motion aware.
|
|
22
|
+
*/
|
|
23
|
+
spotlight?: boolean;
|
|
13
24
|
}
|
|
14
25
|
/**
|
|
15
26
|
* Container surface for grouping related content, with an optional frosted
|
|
16
27
|
* "space glass" look. Compose with {@link CardHeader}, {@link CardTitle}, and
|
|
17
28
|
* {@link CardContent}.
|
|
18
29
|
*
|
|
30
|
+
* `tilt` and `spotlight` bake in the cursor interactions from the Motion
|
|
31
|
+
* category — no wrapper components needed.
|
|
32
|
+
*
|
|
19
33
|
* @example
|
|
20
34
|
* ```tsx
|
|
21
|
-
* <Card glass>
|
|
35
|
+
* <Card glass tilt spotlight>
|
|
22
36
|
* <CardHeader>
|
|
23
37
|
* <CardTitle>Usage</CardTitle>
|
|
24
38
|
* </CardHeader>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface GradientTextProps {
|
|
3
|
+
children: JSX.Element;
|
|
4
|
+
/** CSS gradient color stop at 0%. @default 'hsl(var(--primary))' */
|
|
5
|
+
from?: string;
|
|
6
|
+
/** CSS gradient color stop at 50%. @default 'hsl(var(--accent))' */
|
|
7
|
+
via?: string;
|
|
8
|
+
/** CSS gradient color stop at 100%. @default 'hsl(var(--primary))' */
|
|
9
|
+
to?: string;
|
|
10
|
+
/** Seconds per loop. @default 4 */
|
|
11
|
+
duration?: number;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Renders `children` clipped to a horizontal gradient whose position sweeps
|
|
16
|
+
* back and forth in a loop, for eye-catching headings. Respects
|
|
17
|
+
* `prefers-reduced-motion` by rendering a static gradient instead of
|
|
18
|
+
* animating.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <GradientText duration={6}>Sonora Precision</GradientText>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function GradientText(props: GradientTextProps): JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface MagneticProps {
|
|
3
|
+
children: JSX.Element;
|
|
4
|
+
/** Max px the element travels toward the cursor. @default 12 */
|
|
5
|
+
strength?: number;
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Wraps `children` in an element that's magnetically drawn toward the
|
|
10
|
+
* cursor: as the pointer moves within its bounds, the element translates a
|
|
11
|
+
* fraction of the way toward it (clamped to `strength` px), springing back
|
|
12
|
+
* to rest on pointerleave. Respects `prefers-reduced-motion` (renders
|
|
13
|
+
* static).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <Magnetic strength={16}>
|
|
18
|
+
* <button class="rounded-full bg-primary px-6 py-3 text-primary-foreground">Hover me</button>
|
|
19
|
+
* </Magnetic>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function Magnetic(props: MagneticProps): JSX.Element;
|
package/dist/ui/Ripple.d.ts
CHANGED
|
@@ -7,6 +7,17 @@ export interface RippleProps {
|
|
|
7
7
|
opacity?: number;
|
|
8
8
|
class?: string;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Spawns one Material-style ripple inside `container` at the pointer's
|
|
12
|
+
* position, expanding to cover the container and fading out, then removes
|
|
13
|
+
* itself. The container must be `position: relative; overflow: hidden`.
|
|
14
|
+
* Engine-free (Web Animations API); no-op under reduced motion. This is the
|
|
15
|
+
* primitive behind {@link Ripple} and `<Button ripple>`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function spawnRipple(container: HTMLElement, event: PointerEvent, opts?: {
|
|
18
|
+
color?: string;
|
|
19
|
+
opacity?: number;
|
|
20
|
+
}): void;
|
|
10
21
|
/**
|
|
11
22
|
* Wraps `children` in a `position: relative; overflow: hidden` layer that
|
|
12
23
|
* spawns a Material-style ripple — a circle centered on the pointer's down
|
|
@@ -15,10 +26,12 @@ export interface RippleProps {
|
|
|
15
26
|
* itself once its animation finishes. A no-op under reduced motion: children
|
|
16
27
|
* still render, but no ripple ever spawns.
|
|
17
28
|
*
|
|
29
|
+
* Buttons don't need the wrapper — use `<Button ripple>` instead.
|
|
30
|
+
*
|
|
18
31
|
* @example
|
|
19
32
|
* ```tsx
|
|
20
33
|
* <Ripple color="white" opacity={0.25}>
|
|
21
|
-
* <
|
|
34
|
+
* <div class="rounded-lg bg-primary px-4 py-2 text-primary-foreground">Save</div>
|
|
22
35
|
* </Ripple>
|
|
23
36
|
* ```
|
|
24
37
|
*/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface ScrollProgressProps {
|
|
3
|
+
/** Bar height in px. @default 3 */
|
|
4
|
+
height?: number;
|
|
5
|
+
/** Bar color (CSS color). @default 'hsl(var(--primary))' */
|
|
6
|
+
color?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Renders a fixed bar at the top of the viewport whose fill tracks how far
|
|
11
|
+
* the page has been scrolled. Subscribes to Motion's `scroll` on mount and
|
|
12
|
+
* scales the inner bar horizontally; respects `prefers-reduced-motion`
|
|
13
|
+
* (renders a static, empty bar instead).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <ScrollProgress height={4} color="hsl(var(--accent))" />
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function ScrollProgress(props: ScrollProgressProps): JSX.Element;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface SpotlightProps {
|
|
3
|
+
children: JSX.Element;
|
|
4
|
+
/** Glow color (CSS color). @default 'hsl(var(--primary))' */
|
|
5
|
+
color?: string;
|
|
6
|
+
/** Glow radius in px. @default 180 */
|
|
7
|
+
size?: number;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Appends a cursor-following radial-glow overlay inside `el` (which must be
|
|
12
|
+
* `position: relative; overflow: hidden`) and binds the pointer handlers.
|
|
13
|
+
* Returns a cleanup that unbinds and removes the overlay. Engine-free; no-op
|
|
14
|
+
* under reduced motion. This is the primitive behind {@link Spotlight} and
|
|
15
|
+
* `<Card spotlight>`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function attachSpotlight(el: HTMLElement, opts?: {
|
|
18
|
+
color?: string;
|
|
19
|
+
size?: number;
|
|
20
|
+
}): () => void;
|
|
21
|
+
/**
|
|
22
|
+
* Wraps `children` in a `position: relative; overflow: hidden` layer with a
|
|
23
|
+
* radial glow that tracks the cursor, fading in on pointerenter and out on
|
|
24
|
+
* pointerleave. Children render below the (pointer-transparent) glow.
|
|
25
|
+
* Respects `prefers-reduced-motion` (renders children with no glow at all).
|
|
26
|
+
*
|
|
27
|
+
* A4ui's own `Card` can do this without the wrapper — `<Card spotlight>`.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <Spotlight color="hsl(var(--primary))" size={220}>
|
|
32
|
+
* <div class="rounded-2xl border border-border p-8">Hover for glow</div>
|
|
33
|
+
* </Spotlight>
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function Spotlight(props: SpotlightProps): JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface TiltCardProps {
|
|
3
|
+
children: JSX.Element;
|
|
4
|
+
/** Max tilt in degrees. @default 10 */
|
|
5
|
+
max?: number;
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Binds a cursor-following 3D tilt: pointer moves over `listenEl` rotate
|
|
10
|
+
* `animEl` toward the cursor (clamped to `max` degrees) with a slight lift,
|
|
11
|
+
* easing back flat on pointerleave. Returns a cleanup that unbinds and clears
|
|
12
|
+
* the transform. Engine-free (CSS transitions); no-op under reduced motion.
|
|
13
|
+
* This is the primitive behind {@link TiltCard} and `<Card tilt>` — pass the
|
|
14
|
+
* same element twice to tilt the element the pointer is over.
|
|
15
|
+
*/
|
|
16
|
+
export declare function attachTilt(listenEl: HTMLElement, animEl: HTMLElement, opts?: {
|
|
17
|
+
max?: number;
|
|
18
|
+
}): () => void;
|
|
19
|
+
/**
|
|
20
|
+
* Wraps `children` in a card that tilts in 3D toward the cursor: as the
|
|
21
|
+
* pointer moves within its bounds, the card rotates on both axes (clamped to
|
|
22
|
+
* `max` degrees) and lifts slightly, easing back flat on pointerleave.
|
|
23
|
+
* Respects `prefers-reduced-motion` (renders static).
|
|
24
|
+
*
|
|
25
|
+
* A4ui's own `Card` can do this without the wrapper — `<Card tilt>`.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* <TiltCard max={12}>
|
|
30
|
+
* <div class="rounded-2xl border border-border bg-card p-6">Card content</div>
|
|
31
|
+
* </TiltCard>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function TiltCard(props: TiltCardProps): JSX.Element;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a4ui/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "A4ui — Spatial Glass design system & component library for SolidJS (Kobalte behavior + Tailwind glass tokens + motion).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
"husky": "^9.1.7",
|
|
139
139
|
"jsdom": "^29.1.1",
|
|
140
140
|
"lint-staged": "^16.4.0",
|
|
141
|
+
"marked": "^18.0.6",
|
|
141
142
|
"playwright-core": "^1.61.1",
|
|
142
143
|
"prettier": "^3.9.5",
|
|
143
144
|
"solid-element": "^1.9.2",
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// import '@a4ui/core/styles.css'
|
|
9
9
|
// import { Button, Card, Modal } from '@a4ui/core'
|
|
10
10
|
|
|
11
|
-
export const A4UI_VERSION = '0.
|
|
11
|
+
export const A4UI_VERSION = '0.16.0'
|
|
12
12
|
|
|
13
13
|
// Helpers (src/lib) — generic, framework-level utilities.
|
|
14
14
|
export { cn } from './lib/cn'
|
|
@@ -150,7 +150,12 @@ export { MultiStateBadge, type MultiStateBadgeProps, type BadgeState } from './u
|
|
|
150
150
|
export { NowPlaying, type NowPlayingProps } from './ui/NowPlaying'
|
|
151
151
|
export { Expandable, type ExpandableProps } from './ui/Expandable'
|
|
152
152
|
export { Typewriter, type TypewriterProps } from './ui/Typewriter'
|
|
153
|
-
export { Ripple, type RippleProps } from './ui/Ripple'
|
|
153
|
+
export { Ripple, spawnRipple, type RippleProps } from './ui/Ripple'
|
|
154
|
+
export { Magnetic, type MagneticProps } from './ui/Magnetic'
|
|
155
|
+
export { TiltCard, attachTilt, type TiltCardProps } from './ui/TiltCard'
|
|
156
|
+
export { Spotlight, attachSpotlight, type SpotlightProps } from './ui/Spotlight'
|
|
157
|
+
export { ScrollProgress, type ScrollProgressProps } from './ui/ScrollProgress'
|
|
158
|
+
export { GradientText, type GradientTextProps } from './ui/GradientText'
|
|
154
159
|
export { flyToCart, type FlyToCartOptions } from './lib/flyToCart'
|
|
155
160
|
|
|
156
161
|
// Layout (src/layout) — generic shell + backdrop + toggles. App-coupled pieces
|
package/src/ui/Button.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import type { JSX, ParentProps } from 'solid-js'
|
|
|
2
2
|
import { splitProps } from 'solid-js'
|
|
3
3
|
|
|
4
4
|
import { cn } from '../lib/cn'
|
|
5
|
+
import { spawnRipple } from './Ripple'
|
|
5
6
|
|
|
6
7
|
/** Visual style of a {@link Button}. Defaults to `'primary'`. */
|
|
7
8
|
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost'
|
|
@@ -24,6 +25,12 @@ interface ButtonProps extends ParentProps, Omit<JSX.ButtonHTMLAttributes<HTMLBut
|
|
|
24
25
|
class?: string
|
|
25
26
|
/** Defaults to "button" so action buttons inside a form never submit it by accident. */
|
|
26
27
|
type?: 'button' | 'submit' | 'reset'
|
|
28
|
+
/**
|
|
29
|
+
* Material-style click ripple from the press position. Engine-free (Web
|
|
30
|
+
* Animations API, shared with {@link spawnRipple}) and reduced-motion aware —
|
|
31
|
+
* costs nothing when off.
|
|
32
|
+
*/
|
|
33
|
+
ripple?: boolean
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
/**
|
|
@@ -33,14 +40,29 @@ interface ButtonProps extends ParentProps, Omit<JSX.ButtonHTMLAttributes<HTMLBut
|
|
|
33
40
|
* @example
|
|
34
41
|
* ```tsx
|
|
35
42
|
* <Button variant="outline" onClick={() => save()}>Save</Button>
|
|
43
|
+
* <Button ripple onClick={() => save()}>Save</Button> // with a click ripple
|
|
36
44
|
* ```
|
|
37
45
|
*/
|
|
38
46
|
export function Button(props: ButtonProps): JSX.Element {
|
|
39
|
-
const [local, rest] = splitProps(props, ['variant', 'class', 'type', 'children'])
|
|
47
|
+
const [local, rest] = splitProps(props, ['variant', 'class', 'type', 'children', 'ripple', 'onPointerDown'])
|
|
48
|
+
|
|
49
|
+
const handlePointerDown: JSX.EventHandler<HTMLButtonElement, PointerEvent> = (event) => {
|
|
50
|
+
if (local.ripple) spawnRipple(event.currentTarget, event, { opacity: 0.35 })
|
|
51
|
+
const user = local.onPointerDown
|
|
52
|
+
if (typeof user === 'function') user(event)
|
|
53
|
+
else if (user) user[0](user[1], event)
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
return (
|
|
41
57
|
<button
|
|
42
58
|
type={local.type ?? 'button'}
|
|
43
|
-
class={cn(
|
|
59
|
+
class={cn(
|
|
60
|
+
BUTTON_BASE,
|
|
61
|
+
VARIANT_CLASSES[local.variant ?? 'primary'],
|
|
62
|
+
local.ripple && 'relative overflow-hidden',
|
|
63
|
+
local.class,
|
|
64
|
+
)}
|
|
65
|
+
onPointerDown={handlePointerDown}
|
|
44
66
|
{...rest}
|
|
45
67
|
>
|
|
46
68
|
{local.children}
|
package/src/ui/Card.tsx
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Card + 4 sub-parts (no CardFooter/CardDescription — add if needed).
|
|
2
2
|
import type { JSX, ParentProps } from 'solid-js'
|
|
3
|
-
import { splitProps } from 'solid-js'
|
|
3
|
+
import { onCleanup, onMount, splitProps } from 'solid-js'
|
|
4
4
|
|
|
5
5
|
import { cn } from '../lib/cn'
|
|
6
|
+
import { attachSpotlight } from './Spotlight'
|
|
7
|
+
import { attachTilt } from './TiltCard'
|
|
6
8
|
|
|
7
9
|
interface DivProps extends ParentProps {
|
|
8
10
|
class?: string
|
|
@@ -16,6 +18,17 @@ interface CardProps extends DivProps {
|
|
|
16
18
|
* for glass cards so the look is uniform; pass `glow={false}` to opt out.
|
|
17
19
|
*/
|
|
18
20
|
glow?: boolean
|
|
21
|
+
/**
|
|
22
|
+
* 3D tilt toward the cursor on hover — the same primitive as `<TiltCard>`,
|
|
23
|
+
* baked in ({@link attachTilt}). Engine-free and reduced-motion aware.
|
|
24
|
+
*/
|
|
25
|
+
tilt?: boolean
|
|
26
|
+
/**
|
|
27
|
+
* Soft radial glow following the cursor inside the card — the same primitive
|
|
28
|
+
* as `<Spotlight>`, baked in ({@link attachSpotlight}). Engine-free and
|
|
29
|
+
* reduced-motion aware.
|
|
30
|
+
*/
|
|
31
|
+
spotlight?: boolean
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
/**
|
|
@@ -23,9 +36,12 @@ interface CardProps extends DivProps {
|
|
|
23
36
|
* "space glass" look. Compose with {@link CardHeader}, {@link CardTitle}, and
|
|
24
37
|
* {@link CardContent}.
|
|
25
38
|
*
|
|
39
|
+
* `tilt` and `spotlight` bake in the cursor interactions from the Motion
|
|
40
|
+
* category — no wrapper components needed.
|
|
41
|
+
*
|
|
26
42
|
* @example
|
|
27
43
|
* ```tsx
|
|
28
|
-
* <Card glass>
|
|
44
|
+
* <Card glass tilt spotlight>
|
|
29
45
|
* <CardHeader>
|
|
30
46
|
* <CardTitle>Usage</CardTitle>
|
|
31
47
|
* </CardHeader>
|
|
@@ -34,16 +50,29 @@ interface CardProps extends DivProps {
|
|
|
34
50
|
* ```
|
|
35
51
|
*/
|
|
36
52
|
export function Card(props: CardProps): JSX.Element {
|
|
37
|
-
const [local, rest] = splitProps(props, ['class', 'children', 'glass', 'glow'])
|
|
53
|
+
const [local, rest] = splitProps(props, ['class', 'children', 'glass', 'glow', 'tilt', 'spotlight'])
|
|
38
54
|
// Glow defaults to the card's glass state when not explicitly set.
|
|
39
55
|
const showGlow = () => (local.glow ?? local.glass) === true
|
|
56
|
+
|
|
57
|
+
let el: HTMLDivElement | undefined
|
|
58
|
+
onMount(() => {
|
|
59
|
+
const cleanups = [
|
|
60
|
+
local.tilt && el ? attachTilt(el, el) : null,
|
|
61
|
+
local.spotlight && el ? attachSpotlight(el) : null,
|
|
62
|
+
].filter((f): f is () => void => f !== null)
|
|
63
|
+
onCleanup(() => cleanups.forEach((f) => f()))
|
|
64
|
+
})
|
|
65
|
+
|
|
40
66
|
return (
|
|
41
67
|
<div
|
|
68
|
+
ref={el}
|
|
42
69
|
class={cn(
|
|
43
70
|
local.glass
|
|
44
71
|
? 'card rounded-xl text-card-foreground'
|
|
45
72
|
: 'rounded-xl border border-border bg-card text-card-foreground shadow-sm',
|
|
46
73
|
showGlow() && 'glow-edge',
|
|
74
|
+
local.spotlight && 'relative overflow-hidden',
|
|
75
|
+
local.tilt && 'will-change-transform',
|
|
47
76
|
local.class,
|
|
48
77
|
)}
|
|
49
78
|
{...rest}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// GradientText — text with a smoothly animated multi-color gradient sweeping
|
|
2
|
+
// across the glyphs on loop. The sweep is a background-position animation
|
|
3
|
+
// clipped to the glyphs via `background-clip: text`, driven by Motion's
|
|
4
|
+
// `animate`. Falls back to a static (non-animated) gradient under reduced
|
|
5
|
+
// motion — still colorful, just not moving.
|
|
6
|
+
import { onCleanup, onMount, type JSX } from 'solid-js'
|
|
7
|
+
|
|
8
|
+
import { cn } from '../lib/cn'
|
|
9
|
+
import { animate, motionReduced } from '../lib/motion'
|
|
10
|
+
|
|
11
|
+
export interface GradientTextProps {
|
|
12
|
+
children: JSX.Element
|
|
13
|
+
/** CSS gradient color stop at 0%. @default 'hsl(var(--primary))' */
|
|
14
|
+
from?: string
|
|
15
|
+
/** CSS gradient color stop at 50%. @default 'hsl(var(--accent))' */
|
|
16
|
+
via?: string
|
|
17
|
+
/** CSS gradient color stop at 100%. @default 'hsl(var(--primary))' */
|
|
18
|
+
to?: string
|
|
19
|
+
/** Seconds per loop. @default 4 */
|
|
20
|
+
duration?: number
|
|
21
|
+
class?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Renders `children` clipped to a horizontal gradient whose position sweeps
|
|
26
|
+
* back and forth in a loop, for eye-catching headings. Respects
|
|
27
|
+
* `prefers-reduced-motion` by rendering a static gradient instead of
|
|
28
|
+
* animating.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* <GradientText duration={6}>Sonora Precision</GradientText>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function GradientText(props: GradientTextProps): JSX.Element {
|
|
36
|
+
let el!: HTMLSpanElement
|
|
37
|
+
let controls: { stop: () => void } | undefined
|
|
38
|
+
|
|
39
|
+
onMount(() => {
|
|
40
|
+
if (motionReduced()) return
|
|
41
|
+
|
|
42
|
+
controls = animate(
|
|
43
|
+
el,
|
|
44
|
+
{ backgroundPosition: ['0% 0%', '200% 0%'] },
|
|
45
|
+
{ duration: props.duration ?? 4, ease: 'linear', repeat: Infinity },
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
onCleanup(() => controls?.stop())
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<span
|
|
53
|
+
ref={el}
|
|
54
|
+
class={cn('inline-block font-bold', props.class)}
|
|
55
|
+
style={{
|
|
56
|
+
'background-image': `linear-gradient(90deg, ${props.from ?? 'hsl(var(--primary))'} 0%, ${
|
|
57
|
+
props.via ?? 'hsl(var(--accent))'
|
|
58
|
+
} 50%, ${props.to ?? 'hsl(var(--primary))'} 100%)`,
|
|
59
|
+
'background-size': '200% 100%',
|
|
60
|
+
'-webkit-background-clip': 'text',
|
|
61
|
+
'background-clip': 'text',
|
|
62
|
+
'-webkit-text-fill-color': 'transparent',
|
|
63
|
+
color: 'transparent',
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
{props.children}
|
|
67
|
+
</span>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Magnetic — pulls its wrapped content toward the cursor while the pointer
|
|
2
|
+
// hovers over it, and springs back to rest on pointerleave. Motion's `animate`
|
|
3
|
+
// drives the translate (a spring, so repeated pointermoves smoothly retarget
|
|
4
|
+
// mid-flight rather than snapping). No-op (static) under reduced motion.
|
|
5
|
+
import { onCleanup, onMount, type JSX } from 'solid-js'
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/cn'
|
|
8
|
+
import { animate, motionReduced } from '../lib/motion'
|
|
9
|
+
|
|
10
|
+
export interface MagneticProps {
|
|
11
|
+
children: JSX.Element
|
|
12
|
+
/** Max px the element travels toward the cursor. @default 12 */
|
|
13
|
+
strength?: number
|
|
14
|
+
class?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wraps `children` in an element that's magnetically drawn toward the
|
|
19
|
+
* cursor: as the pointer moves within its bounds, the element translates a
|
|
20
|
+
* fraction of the way toward it (clamped to `strength` px), springing back
|
|
21
|
+
* to rest on pointerleave. Respects `prefers-reduced-motion` (renders
|
|
22
|
+
* static).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <Magnetic strength={16}>
|
|
27
|
+
* <button class="rounded-full bg-primary px-6 py-3 text-primary-foreground">Hover me</button>
|
|
28
|
+
* </Magnetic>
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function Magnetic(props: MagneticProps): JSX.Element {
|
|
32
|
+
let root!: HTMLDivElement
|
|
33
|
+
|
|
34
|
+
onMount(() => {
|
|
35
|
+
if (motionReduced()) return
|
|
36
|
+
|
|
37
|
+
let controls: ReturnType<typeof animate> | undefined
|
|
38
|
+
|
|
39
|
+
const handlePointerMove = (event: PointerEvent): void => {
|
|
40
|
+
const rect = root.getBoundingClientRect()
|
|
41
|
+
const nx = (event.clientX - (rect.left + rect.width / 2)) / (rect.width / 2)
|
|
42
|
+
const ny = (event.clientY - (rect.top + rect.height / 2)) / (rect.height / 2)
|
|
43
|
+
const strength = props.strength ?? 12
|
|
44
|
+
|
|
45
|
+
controls?.stop()
|
|
46
|
+
controls = animate(
|
|
47
|
+
root,
|
|
48
|
+
{ x: nx * strength, y: ny * strength },
|
|
49
|
+
{ type: 'spring', stiffness: 300, damping: 20 },
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const handlePointerLeave = (): void => {
|
|
54
|
+
controls?.stop()
|
|
55
|
+
controls = animate(root, { x: 0, y: 0 }, { type: 'spring', stiffness: 300, damping: 20 })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
root.addEventListener('pointermove', handlePointerMove)
|
|
59
|
+
root.addEventListener('pointerleave', handlePointerLeave)
|
|
60
|
+
|
|
61
|
+
onCleanup(() => {
|
|
62
|
+
root.removeEventListener('pointermove', handlePointerMove)
|
|
63
|
+
root.removeEventListener('pointerleave', handlePointerLeave)
|
|
64
|
+
controls?.stop()
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div ref={root} class={cn('inline-block will-change-transform', props.class)}>
|
|
70
|
+
{props.children}
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|