@a4ui/core 0.12.0 → 0.13.1

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
+ export interface FlyToCartOptions {
2
+ /** Image URL to fly; if omitted, the source element is cloned. */
3
+ image?: string;
4
+ /** Flight duration in seconds. @default 0.6 */
5
+ duration?: number;
6
+ /** Called when the flight finishes (e.g. to increment the cart count). */
7
+ onArrive?: () => void;
8
+ }
9
+ /**
10
+ * Animate a small ghost from `source` to `target` (e.g. product → cart icon),
11
+ * then a bump on the target. No-op under reduced motion (still calls onArrive).
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * function onAddToCart(e: MouseEvent) {
16
+ * const button = e.currentTarget as HTMLElement
17
+ * const cartIcon = document.getElementById('cart-icon')!
18
+ * flyToCart(button, cartIcon, {
19
+ * image: product.thumbnailUrl,
20
+ * onArrive: () => setCartCount((n) => n + 1),
21
+ * })
22
+ * }
23
+ * ```
24
+ */
25
+ export declare function flyToCart(source: Element, target: Element, opts?: FlyToCartOptions): void;
@@ -0,0 +1,44 @@
1
+ import { JSX } from 'solid-js';
2
+ export type CurtainVariant = 'fade' | 'doors' | 'blinds' | 'iris' | 'shutter' | 'clip' | 'pixels';
3
+ export interface CurtainProps {
4
+ /** true = cover the screen; false = uncover (reveal content). */
5
+ show: boolean;
6
+ /** Which wipe style. @default 'fade' */
7
+ variant?: CurtainVariant;
8
+ /** Overlay color (CSS color / var). @default 'hsl(var(--background))' */
9
+ color?: string;
10
+ /** Seconds for one direction. @default 0.6 */
11
+ duration?: number;
12
+ /** Called once the screen is fully covered (show went true→covered). */
13
+ onCovered?: () => void;
14
+ /** Called once fully uncovered (show went false→revealed). */
15
+ onRevealed?: () => void;
16
+ class?: string;
17
+ }
18
+ /**
19
+ * A controlled full-screen curtain for page/route transitions. Set `show` to
20
+ * `true` to cover the viewport, `false` to uncover it, and swap your route
21
+ * content while covered. `variant` picks the wipe style — fade, sliding
22
+ * doors, blinds, shutter (vertical blinds), iris/circle wipe, a left-to-right
23
+ * clip wipe, or a pixel-grid cascade —
24
+ * `onCovered` / `onRevealed` fire once each transition finishes so you can
25
+ * swap content at exactly the right moment. Skips the animation (and still
26
+ * fires the callback on the next microtask) under reduced motion.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * const [covered, setCovered] = createSignal(false)
31
+ * const goTo = (path: string) => setCovered(true)
32
+ * return (
33
+ * <>
34
+ * <Curtain
35
+ * show={covered()}
36
+ * variant="iris"
37
+ * onCovered={() => { loadRoute(); setCovered(false) }}
38
+ * />
39
+ * <Router />
40
+ * </>
41
+ * )
42
+ * ```
43
+ */
44
+ export declare function Curtain(props: CurtainProps): JSX.Element;
@@ -0,0 +1,34 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface ExpandableProps {
3
+ /** Collapsed content — the card users click to expand. */
4
+ trigger: JSX.Element;
5
+ /** Expanded panel content. */
6
+ children: JSX.Element;
7
+ /** `dialog` = centered panel capped at `maxWidth`; `full` = near-fullscreen. @default 'dialog' */
8
+ size?: 'dialog' | 'full';
9
+ /** Max width (px) of the expanded panel when `size='dialog'`. @default 640 */
10
+ maxWidth?: number;
11
+ /** Notified when the panel opens/closes. */
12
+ onOpenChange?: (open: boolean) => void;
13
+ /** Class for the inline trigger wrapper. */
14
+ class?: string;
15
+ /** Class for the expanded panel surface. */
16
+ panelClass?: string;
17
+ }
18
+ /**
19
+ * Expands a card into an overlay panel with a shared-element (FLIP) transition —
20
+ * the card appears to grow into the dialog and shrink back on close. Animates
21
+ * position + size (never scale) so content stays crisp. Falls back to a plain
22
+ * show/hide under reduced motion. Close with the ✕, a backdrop click, or Escape.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <Expandable
27
+ * trigger={<Card class="cursor-pointer">Tap to expand</Card>}
28
+ * size="dialog"
29
+ * >
30
+ * <div class="p-6">Full details here…</div>
31
+ * </Expandable>
32
+ * ```
33
+ */
34
+ export declare function Expandable(props: ExpandableProps): JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface FillTextProps {
3
+ text: string;
4
+ /** Seconds per sweep. @default 1.6 */
5
+ duration?: number;
6
+ class?: string;
7
+ }
8
+ /**
9
+ * Renders `text` with a bright gradient band that sweeps across the glyphs
10
+ * repeatedly, useful as a lightweight loading indicator. Respects
11
+ * `prefers-reduced-motion` by rendering plain muted text instead of animating.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <FillText text="Loading results…" />
16
+ * ```
17
+ */
18
+ export declare function FillText(props: FillTextProps): JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface HoldToConfirmProps {
3
+ onConfirm: () => void;
4
+ /** Button label. @default 'Hold to confirm' */
5
+ label?: string;
6
+ /** How long to hold, in ms. @default 1200 */
7
+ holdMs?: number;
8
+ disabled?: boolean;
9
+ class?: string;
10
+ }
11
+ /**
12
+ * Press-and-hold button for destructive/important actions: `onConfirm` only
13
+ * fires once the pointer (or Space/Enter) has been held down for `holdMs`.
14
+ * A fill layer sweeps left-to-right as visual progress; releasing early stops
15
+ * and resets it. Under reduced motion the hold is still required (gated by a
16
+ * plain timer instead of the tracked animation), but the fill jumps instead
17
+ * of sweeping and the success flourish is skipped.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <HoldToConfirm label="Hold to delete" holdMs={1500} onConfirm={() => deleteItem(id)} />
22
+ * ```
23
+ */
24
+ export declare function HoldToConfirm(props: HoldToConfirmProps): JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface LoadingDotsProps {
3
+ /** Dot diameter in px. @default 8 */
4
+ size?: number;
5
+ /** Optional accessible label. @default 'Loading' */
6
+ label?: string;
7
+ class?: string;
8
+ }
9
+ /**
10
+ * Three dots bouncing in a staggered wave — an inline, indeterminate loading
11
+ * indicator. Dots use `bg-current`, so color/size follow the ancestor's text
12
+ * color; diameter is set via `size`. Falls back to a gentle opacity pulse
13
+ * under reduced motion.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * <LoadingDots label="Loading results" class="text-primary" />
18
+ * ```
19
+ */
20
+ export declare function LoadingDots(props: LoadingDotsProps): JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { JSX } from 'solid-js';
2
+ /** The lifecycle state a {@link MultiStateBadge} can be in. */
3
+ export type BadgeState = 'idle' | 'loading' | 'success' | 'error';
4
+ export interface MultiStateBadgeProps {
5
+ /** Current lifecycle state; drives color, icon, and label. */
6
+ state: BadgeState;
7
+ /** Override the per-state text. Defaults: idle→'Ready', loading→'Working…', success→'Done', error→'Failed'. */
8
+ labels?: Partial<Record<BadgeState, string>>;
9
+ class?: string;
10
+ }
11
+ /**
12
+ * Status badge that animates between `idle` → `loading` → `success` → `error`,
13
+ * morphing its color and swapping its icon/label on each transition. Colors
14
+ * tween via a CSS `transition-colors` class; the pill also gets a spring
15
+ * "pop" and the icon/label cross-fade via Motion's `animate`, both skipped
16
+ * when {@link motionReduced} is true.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * <MultiStateBadge state={saveState()} labels={{ error: 'Save failed' }} />
21
+ * ```
22
+ */
23
+ export declare function MultiStateBadge(props: MultiStateBadgeProps): JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface StackNotification {
3
+ id: string | number;
4
+ title: string;
5
+ description?: string;
6
+ icon?: JSX.Element;
7
+ }
8
+ export interface NotificationStackProps {
9
+ /** Newest first. The parent owns this array. */
10
+ items: StackNotification[];
11
+ /** How many cards peek when collapsed. @default 3 */
12
+ max?: number;
13
+ /** Fired when a card is dismissed; the parent should drop it from `items`. */
14
+ onDismiss?: (id: string | number) => void;
15
+ class?: string;
16
+ }
17
+ /**
18
+ * Stacked notifications. Collapsed, cards behind the front one are offset down
19
+ * and scaled so they peek from the bottom edge; click the stack to expand every
20
+ * notification into a scrollable list, and "Show less" to collapse. Removing the
21
+ * front card promotes the rest; new cards fade/slide in via `animateIn`.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * const [items, setItems] = createSignal<StackNotification[]>([])
26
+ * <NotificationStack
27
+ * items={items()}
28
+ * onDismiss={(id) => setItems((xs) => xs.filter((x) => x.id !== id))}
29
+ * />
30
+ * ```
31
+ */
32
+ export declare function NotificationStack(props: NotificationStackProps): JSX.Element;
@@ -0,0 +1,21 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface NowPlayingProps {
3
+ title: string;
4
+ artist?: string;
5
+ /** Cover art URL. */
6
+ cover?: string;
7
+ /** Whether the equalizer bars are animating. @default true */
8
+ playing?: boolean;
9
+ class?: string;
10
+ }
11
+ /**
12
+ * Compact "now playing" music widget with an animated equalizer. Bars bounce
13
+ * via CSS keyframes while `playing` is true; they freeze to a low static
14
+ * height when paused or under reduced motion.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * <NowPlaying title="Random Access Memories" artist="Daft Punk" cover="/covers/ram.jpg" playing />
19
+ * ```
20
+ */
21
+ export declare function NowPlaying(props: NowPlayingProps): JSX.Element;
@@ -0,0 +1,21 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface ParallaxProps {
3
+ children: JSX.Element;
4
+ /** Parallax strength; positive moves slower/opposite. Pixels of travel across the scroll range. @default 80 */
5
+ amount?: number;
6
+ class?: string;
7
+ }
8
+ /**
9
+ * Wraps `children` in an element that drifts vertically as the page scrolls,
10
+ * creating a depth effect. Tracks scroll progress of the element itself
11
+ * against the viewport via Motion's `scroll`; respects `prefers-reduced-motion`
12
+ * (renders static).
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <Parallax amount={120}>
17
+ * <img src="/hero.png" alt="" />
18
+ * </Parallax>
19
+ * ```
20
+ */
21
+ export declare function Parallax(props: ParallaxProps): JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface ScrambleTextProps {
3
+ text: string;
4
+ /** When to run: on mount, or on hover. @default 'mount' */
5
+ trigger?: 'mount' | 'hover';
6
+ /** Total scramble duration in ms. @default 800 */
7
+ duration?: number;
8
+ class?: string;
9
+ }
10
+ /**
11
+ * Text that scrambles through random glyphs before "decoding" into the final
12
+ * string, left to right, over `duration` ms. Runs once on mount by default;
13
+ * pass `trigger="hover"` to (re)run it — cleanly restarting if it's still
14
+ * mid-scramble — on pointer hover instead. Renders the final text as-is, with
15
+ * no scramble, when the user prefers reduced motion.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <ScrambleText text="ACCESS GRANTED" trigger="hover" duration={600} />
20
+ * ```
21
+ */
22
+ export declare function ScrambleText(props: ScrambleTextProps): JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface TextRevealProps {
3
+ text: string;
4
+ /** Split unit. @default 'word' */
5
+ by?: 'word' | 'char';
6
+ /** Reveal when scrolled into view instead of on mount. @default false */
7
+ onView?: boolean;
8
+ class?: string;
9
+ }
10
+ /**
11
+ * Reveals `text`, split into words or characters, with a staggered
12
+ * fade + slide-up (via Motion's `animate` + `stagger`). Runs once on mount by
13
+ * default; pass `onView` to instead reveal the first time the component
14
+ * scrolls into view. Whitespace between words renders as plain text, not an
15
+ * animated unit. Under reduced motion, everything stays visible and no
16
+ * animation runs.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * <TextReveal text="Ship it." by="char" onView />
21
+ * ```
22
+ */
23
+ export declare function TextReveal(props: TextRevealProps): JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a4ui/core",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
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",
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.12.0'
11
+ export const A4UI_VERSION = '0.13.1'
12
12
 
13
13
  // Helpers (src/lib) — generic, framework-level utilities.
14
14
  export { cn } from './lib/cn'
@@ -131,6 +131,26 @@ export { Portal, type PortalProps } from './ui/Portal'
131
131
  export { Sortable, type SortableProps } from './ui/Sortable'
132
132
  export { Clock, type ClockProps } from './ui/Clock'
133
133
 
134
+ // Motion components (src/ui) — animation primitives built on the `motion` engine
135
+ // (adapted from motion.dev examples). All tree-shakeable and reduced-motion aware;
136
+ // `motion` is external, so importing one of these is the only thing that pulls it.
137
+ export { ScrambleText, type ScrambleTextProps } from './ui/ScrambleText'
138
+ export { TextReveal, type TextRevealProps } from './ui/TextReveal'
139
+ export { HoldToConfirm, type HoldToConfirmProps } from './ui/HoldToConfirm'
140
+ export { LoadingDots, type LoadingDotsProps } from './ui/LoadingDots'
141
+ export { Curtain, type CurtainProps, type CurtainVariant } from './ui/Curtain'
142
+ export { Parallax, type ParallaxProps } from './ui/Parallax'
143
+ export { FillText, type FillTextProps } from './ui/FillText'
144
+ export {
145
+ NotificationStack,
146
+ type NotificationStackProps,
147
+ type StackNotification,
148
+ } from './ui/NotificationStack'
149
+ export { MultiStateBadge, type MultiStateBadgeProps, type BadgeState } from './ui/MultiStateBadge'
150
+ export { NowPlaying, type NowPlayingProps } from './ui/NowPlaying'
151
+ export { Expandable, type ExpandableProps } from './ui/Expandable'
152
+ export { flyToCart, type FlyToCartOptions } from './lib/flyToCart'
153
+
134
154
  // Layout (src/layout) — generic shell + backdrop + toggles. App-coupled pieces
135
155
  // (Sidebar, Topbar, CompanySwitcher, DemoBanner, CommandPalette) stay in the app.
136
156
  export { AppShell } from './layout/AppShell'
@@ -0,0 +1,103 @@
1
+ // The classic "add to basket" fly animation: a ghost of the product image
2
+ // arcs from a source element (button/card) to a target element (the cart
3
+ // icon), then the target bumps to acknowledge the drop. Pure JS/DOM (a
4
+ // throwaway ghost node driven by Motion's `animate`), so it works from any
5
+ // click handler without touching component state. Reduced motion skips the
6
+ // flight entirely but still fires `onArrive` so cart counts stay correct.
7
+ import { animate, motionReduced } from './motion'
8
+
9
+ export interface FlyToCartOptions {
10
+ /** Image URL to fly; if omitted, the source element is cloned. */
11
+ image?: string
12
+ /** Flight duration in seconds. @default 0.6 */
13
+ duration?: number
14
+ /** Called when the flight finishes (e.g. to increment the cart count). */
15
+ onArrive?: () => void
16
+ }
17
+
18
+ /**
19
+ * Animate a small ghost from `source` to `target` (e.g. product → cart icon),
20
+ * then a bump on the target. No-op under reduced motion (still calls onArrive).
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * function onAddToCart(e: MouseEvent) {
25
+ * const button = e.currentTarget as HTMLElement
26
+ * const cartIcon = document.getElementById('cart-icon')!
27
+ * flyToCart(button, cartIcon, {
28
+ * image: product.thumbnailUrl,
29
+ * onArrive: () => setCartCount((n) => n + 1),
30
+ * })
31
+ * }
32
+ * ```
33
+ */
34
+ export function flyToCart(source: Element, target: Element, opts?: FlyToCartOptions): void {
35
+ if (typeof document === 'undefined') {
36
+ opts?.onArrive?.()
37
+ return
38
+ }
39
+
40
+ if (motionReduced()) {
41
+ opts?.onArrive?.()
42
+ return
43
+ }
44
+
45
+ try {
46
+ const sourceRect = source.getBoundingClientRect()
47
+ const targetRect = target.getBoundingClientRect()
48
+
49
+ const size = 48
50
+ const sourceCenterX = sourceRect.left + sourceRect.width / 2
51
+ const sourceCenterY = sourceRect.top + sourceRect.height / 2
52
+ const targetCenterX = targetRect.left + targetRect.width / 2
53
+ const targetCenterY = targetRect.top + targetRect.height / 2
54
+
55
+ const dx = targetCenterX - sourceCenterX
56
+ const dy = targetCenterY - sourceCenterY
57
+
58
+ const ghost: HTMLElement = opts?.image ? document.createElement('img') : document.createElement('div')
59
+ if (opts?.image) (ghost as HTMLImageElement).src = opts.image
60
+
61
+ ghost.style.position = 'fixed'
62
+ ghost.style.left = `${sourceCenterX - size / 2}px`
63
+ ghost.style.top = `${sourceCenterY - size / 2}px`
64
+ ghost.style.width = `${size}px`
65
+ ghost.style.height = `${size}px`
66
+ ghost.style.borderRadius = '9999px'
67
+ ghost.style.objectFit = 'cover'
68
+ ghost.style.pointerEvents = 'none'
69
+ ghost.style.zIndex = '9999'
70
+ if (!opts?.image) ghost.style.background = 'var(--a4-primary, #6366f1)'
71
+
72
+ document.body.appendChild(ghost)
73
+
74
+ const duration = opts?.duration ?? 0.6
75
+
76
+ const controls = animate(
77
+ ghost,
78
+ { x: [0, dx], y: [0, dy], scale: [1, 0.3], opacity: [1, 0.5] },
79
+ { duration, ease: 'easeInOut' },
80
+ )
81
+
82
+ controls.finished
83
+ .catch(() => {
84
+ /* stopped early — still clean up and settle below */
85
+ })
86
+ .then(() => {
87
+ ghost.remove()
88
+ opts?.onArrive?.()
89
+ try {
90
+ animate(target, { scale: [1, 1.25, 1] }, { duration: 0.3, ease: 'easeOut' })
91
+ } catch {
92
+ /* target no longer animatable — ignore */
93
+ }
94
+ })
95
+ .catch(() => {
96
+ /* onArrive threw — nothing more we can do here */
97
+ })
98
+ } catch {
99
+ // Rect reads / DOM writes can fail (detached nodes, hostile environments) —
100
+ // bail gracefully rather than throwing out of an event handler.
101
+ opts?.onArrive?.()
102
+ }
103
+ }