@a4ui/core 0.14.0 → 0.15.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.
@@ -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;
@@ -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,23 @@
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
+ * Wraps `children` in a `position: relative; overflow: hidden` layer with a
12
+ * radial glow that tracks the cursor, fading in on pointerenter and out on
13
+ * pointerleave. Children render above the glow. Respects
14
+ * `prefers-reduced-motion` (renders children with no glow at all).
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * <Spotlight color="hsl(var(--primary))" size={220}>
19
+ * <div class="rounded-2xl border border-border p-8">Hover for glow</div>
20
+ * </Spotlight>
21
+ * ```
22
+ */
23
+ export declare function Spotlight(props: SpotlightProps): JSX.Element;
@@ -0,0 +1,21 @@
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
+ * Wraps `children` in a card that tilts in 3D toward the cursor: as the
10
+ * pointer moves within its bounds, the card rotates on both axes (clamped to
11
+ * `max` degrees) and lifts slightly, springing back flat on pointerleave.
12
+ * Respects `prefers-reduced-motion` (renders static).
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <TiltCard max={12}>
17
+ * <div class="rounded-2xl border border-border bg-card p-6">Card content</div>
18
+ * </TiltCard>
19
+ * ```
20
+ */
21
+ 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.14.0",
3
+ "version": "0.15.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",
@@ -98,6 +98,7 @@
98
98
  "test": "playwright test",
99
99
  "test:ui": "playwright test --ui",
100
100
  "test:a11y": "playwright test a11y --project=desktop",
101
+ "test:visual": "playwright test --config playwright.visual.config.ts",
101
102
  "test:report": "playwright show-report",
102
103
  "test:package": "node scripts/test-package.mjs",
103
104
  "validate": "npm run typecheck && npm run lint && npm run format:check && npm run test:unit && npm run build && npm run test:package",
@@ -137,6 +138,7 @@
137
138
  "husky": "^9.1.7",
138
139
  "jsdom": "^29.1.1",
139
140
  "lint-staged": "^16.4.0",
141
+ "marked": "^18.0.6",
140
142
  "playwright-core": "^1.61.1",
141
143
  "prettier": "^3.9.5",
142
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.14.0'
11
+ export const A4UI_VERSION = '0.15.0'
12
12
 
13
13
  // Helpers (src/lib) — generic, framework-level utilities.
14
14
  export { cn } from './lib/cn'
@@ -151,6 +151,11 @@ 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
153
  export { Ripple, type RippleProps } from './ui/Ripple'
154
+ export { Magnetic, type MagneticProps } from './ui/Magnetic'
155
+ export { TiltCard, type TiltCardProps } from './ui/TiltCard'
156
+ export { Spotlight, 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
@@ -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
+ }
@@ -0,0 +1,61 @@
1
+ // ScrollProgress — a thin bar pinned to the top of the viewport that fills
2
+ // left-to-right as the page scrolls, a common reading-progress indicator.
3
+ // Driven by Motion's `scroll` progress callback (0..1). Renders a static,
4
+ // unfilled bar under reduced motion (no scroll subscription).
5
+ import { onCleanup, onMount, type JSX } from 'solid-js'
6
+
7
+ import { cn } from '../lib/cn'
8
+ import { motionReduced, scroll } from '../lib/motion'
9
+
10
+ export interface ScrollProgressProps {
11
+ /** Bar height in px. @default 3 */
12
+ height?: number
13
+ /** Bar color (CSS color). @default 'hsl(var(--primary))' */
14
+ color?: string
15
+ class?: string
16
+ }
17
+
18
+ /**
19
+ * Renders a fixed bar at the top of the viewport whose fill tracks how far
20
+ * the page has been scrolled. Subscribes to Motion's `scroll` on mount and
21
+ * scales the inner bar horizontally; respects `prefers-reduced-motion`
22
+ * (renders a static, empty bar instead).
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <ScrollProgress height={4} color="hsl(var(--accent))" />
27
+ * ```
28
+ */
29
+ export function ScrollProgress(props: ScrollProgressProps): JSX.Element {
30
+ let bar!: HTMLDivElement
31
+
32
+ onMount(() => {
33
+ bar.style.transform = 'scaleX(0)'
34
+
35
+ if (motionReduced()) return
36
+
37
+ const stop = scroll((progress: number) => {
38
+ bar.style.transform = `scaleX(${progress})`
39
+ })
40
+
41
+ onCleanup(() => stop())
42
+ })
43
+
44
+ return (
45
+ <div
46
+ aria-hidden="true"
47
+ class={cn('fixed top-0 left-0 z-[9999] w-full', props.class)}
48
+ style={{ height: `${props.height ?? 3}px` }}
49
+ >
50
+ <div
51
+ ref={bar}
52
+ class="origin-left will-change-transform"
53
+ style={{
54
+ height: '100%',
55
+ width: '100%',
56
+ 'background-color': props.color ?? 'hsl(var(--primary))',
57
+ }}
58
+ />
59
+ </div>
60
+ )
61
+ }
@@ -0,0 +1,83 @@
1
+ // Spotlight — a soft radial glow that follows the cursor inside the wrapped
2
+ // element, like a light hovering over the surface. This is a plain CSS
3
+ // follow (the overlay's `background` is written directly on pointermove)
4
+ // rather than a Motion tween: a radial-gradient position update has no
5
+ // transform to spring, so there's nothing for Motion to add here. Only the
6
+ // fade in/out on enter/leave uses a CSS `transition: opacity`. No-op (no
7
+ // glow, children still render) under reduced motion.
8
+ import { onCleanup, onMount, type JSX } from 'solid-js'
9
+
10
+ import { cn } from '../lib/cn'
11
+ import { motionReduced } from '../lib/motion'
12
+
13
+ export interface SpotlightProps {
14
+ children: JSX.Element
15
+ /** Glow color (CSS color). @default 'hsl(var(--primary))' */
16
+ color?: string
17
+ /** Glow radius in px. @default 180 */
18
+ size?: number
19
+ class?: string
20
+ }
21
+
22
+ /**
23
+ * Wraps `children` in a `position: relative; overflow: hidden` layer with a
24
+ * radial glow that tracks the cursor, fading in on pointerenter and out on
25
+ * pointerleave. Children render above the glow. Respects
26
+ * `prefers-reduced-motion` (renders children with no glow at all).
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * <Spotlight color="hsl(var(--primary))" size={220}>
31
+ * <div class="rounded-2xl border border-border p-8">Hover for glow</div>
32
+ * </Spotlight>
33
+ * ```
34
+ */
35
+ export function Spotlight(props: SpotlightProps): JSX.Element {
36
+ let root!: HTMLDivElement
37
+ let overlay!: HTMLSpanElement
38
+
39
+ onMount(() => {
40
+ if (motionReduced()) return
41
+
42
+ const size = props.size ?? 180
43
+ const color = props.color ?? 'hsl(var(--primary))'
44
+
45
+ const paint = (x: number, y: number): void => {
46
+ overlay.style.background = `radial-gradient(${size}px circle at ${x}px ${y}px, color-mix(in srgb, ${color} 30%, transparent), transparent 70%)`
47
+ }
48
+
49
+ const handlePointerMove = (event: PointerEvent): void => {
50
+ const rect = root.getBoundingClientRect()
51
+ paint(event.clientX - rect.left, event.clientY - rect.top)
52
+ }
53
+
54
+ const handlePointerEnter = (): void => {
55
+ overlay.style.opacity = '1'
56
+ }
57
+
58
+ const handlePointerLeave = (): void => {
59
+ overlay.style.opacity = '0'
60
+ }
61
+
62
+ root.addEventListener('pointermove', handlePointerMove)
63
+ root.addEventListener('pointerenter', handlePointerEnter)
64
+ root.addEventListener('pointerleave', handlePointerLeave)
65
+
66
+ onCleanup(() => {
67
+ root.removeEventListener('pointermove', handlePointerMove)
68
+ root.removeEventListener('pointerenter', handlePointerEnter)
69
+ root.removeEventListener('pointerleave', handlePointerLeave)
70
+ })
71
+ })
72
+
73
+ return (
74
+ <div ref={root} class={cn('relative overflow-hidden', props.class)}>
75
+ {props.children}
76
+ <span
77
+ ref={overlay}
78
+ aria-hidden="true"
79
+ class="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300"
80
+ />
81
+ </div>
82
+ )
83
+ }
@@ -0,0 +1,80 @@
1
+ // TiltCard — tilts its wrapped content in 3D toward the cursor on hover, like
2
+ // a card catching the light. The outer element sets the CSS perspective; the
3
+ // inner element (which actually rotates) gets `transform-style: preserve-3d`
4
+ // so nested content keeps its own depth. Motion's `animate` drives the
5
+ // rotate/scale as a spring. No-op (static) under reduced motion.
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 TiltCardProps {
12
+ children: JSX.Element
13
+ /** Max tilt in degrees. @default 10 */
14
+ max?: number
15
+ class?: string
16
+ }
17
+
18
+ /**
19
+ * Wraps `children` in a card that tilts in 3D toward the cursor: as the
20
+ * pointer moves within its bounds, the card rotates on both axes (clamped to
21
+ * `max` degrees) and lifts slightly, springing back flat on pointerleave.
22
+ * Respects `prefers-reduced-motion` (renders static).
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <TiltCard max={12}>
27
+ * <div class="rounded-2xl border border-border bg-card p-6">Card content</div>
28
+ * </TiltCard>
29
+ * ```
30
+ */
31
+ export function TiltCard(props: TiltCardProps): JSX.Element {
32
+ let root!: HTMLDivElement
33
+ let inner!: HTMLDivElement
34
+
35
+ onMount(() => {
36
+ if (motionReduced()) return
37
+
38
+ let controls: ReturnType<typeof animate> | undefined
39
+
40
+ const handlePointerMove = (event: PointerEvent): void => {
41
+ const rect = root.getBoundingClientRect()
42
+ const nx = (event.clientX - rect.left) / rect.width - 0.5
43
+ const ny = (event.clientY - rect.top) / rect.height - 0.5
44
+ const max = props.max ?? 10
45
+
46
+ controls?.stop()
47
+ controls = animate(
48
+ inner,
49
+ { rotateX: ny * -2 * max, rotateY: nx * 2 * max, scale: 1.02 },
50
+ { type: 'spring', stiffness: 300, damping: 20 },
51
+ )
52
+ }
53
+
54
+ const handlePointerLeave = (): void => {
55
+ controls?.stop()
56
+ controls = animate(
57
+ inner,
58
+ { rotateX: 0, rotateY: 0, scale: 1 },
59
+ { type: 'spring', stiffness: 300, damping: 20 },
60
+ )
61
+ }
62
+
63
+ root.addEventListener('pointermove', handlePointerMove)
64
+ root.addEventListener('pointerleave', handlePointerLeave)
65
+
66
+ onCleanup(() => {
67
+ root.removeEventListener('pointermove', handlePointerMove)
68
+ root.removeEventListener('pointerleave', handlePointerLeave)
69
+ controls?.stop()
70
+ })
71
+ })
72
+
73
+ return (
74
+ <div ref={root} class={cn('inline-block', props.class)} style={{ perspective: '800px' }}>
75
+ <div ref={inner} class="will-change-transform" style={{ 'transform-style': 'preserve-3d' }}>
76
+ {props.children}
77
+ </div>
78
+ </div>
79
+ )
80
+ }