@a4ui/core 0.31.0 → 0.32.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,119 @@
1
+ import { clsx as M } from "clsx";
2
+ import { twMerge as h } from "tailwind-merge";
3
+ import { animate as a, inView as I } from "motion";
4
+ import { createSignal as i, createEffect as R, untrack as m, onCleanup as C } from "solid-js";
5
+ function N(...e) {
6
+ return h(M(e));
7
+ }
8
+ const l = "a4ui-effects";
9
+ function p() {
10
+ try {
11
+ return window.localStorage;
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+ function F() {
17
+ var t;
18
+ try {
19
+ if (new URLSearchParams(window.location.search).get("calm") === "1") return !1;
20
+ } catch {
21
+ }
22
+ const e = (t = p()) == null ? void 0 : t.getItem(l);
23
+ return e === null ? !0 : e === "1";
24
+ }
25
+ function y(e) {
26
+ typeof document > "u" || document.documentElement.classList.toggle("calm", !e);
27
+ }
28
+ const w = F();
29
+ y(w);
30
+ const [g, U] = i(w);
31
+ function V() {
32
+ return g;
33
+ }
34
+ function _() {
35
+ return !g();
36
+ }
37
+ function b(e) {
38
+ var t;
39
+ (t = p()) == null || t.setItem(l, e ? "1" : "0"), y(e), U(e);
40
+ }
41
+ const L = "(prefers-reduced-motion: reduce)";
42
+ function T() {
43
+ return typeof window > "u" || typeof window.matchMedia != "function" ? !1 : window.matchMedia(L).matches;
44
+ }
45
+ const E = "a4ui-motion-forced";
46
+ function O() {
47
+ try {
48
+ return window.localStorage;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ var d;
54
+ const [u, Y] = i(((d = O()) == null ? void 0 : d.getItem(E)) === "1");
55
+ typeof document < "u" && document.documentElement.classList.toggle("force-motion", m(u));
56
+ function k() {
57
+ return u;
58
+ }
59
+ function A(e) {
60
+ var t;
61
+ (t = O()) == null || t.setItem(E, e ? "1" : "0"), typeof document < "u" && document.documentElement.classList.toggle("force-motion", e), Y(e);
62
+ }
63
+ function c() {
64
+ return _() || T() && !u();
65
+ }
66
+ function G(e, t = 600) {
67
+ const [o, f] = i(0);
68
+ let n;
69
+ return R(() => {
70
+ const r = e();
71
+ if (n == null || n.stop(), c()) {
72
+ f(r);
73
+ return;
74
+ }
75
+ const s = m(o);
76
+ s !== r && (n = a(s, r, {
77
+ duration: t / 1e3,
78
+ ease: "easeOut",
79
+ onUpdate: (S) => f(S)
80
+ }));
81
+ }), C(() => n == null ? void 0 : n.stop()), o;
82
+ }
83
+ function P(e, t = {}) {
84
+ c() || a(
85
+ e,
86
+ { opacity: [0, 1], y: [t.y ?? 8, 0] },
87
+ { duration: t.duration ?? 0.32, delay: t.delay ?? 0, ease: "easeOut" }
88
+ );
89
+ }
90
+ function Q(e, t = {}) {
91
+ if (c()) return () => {
92
+ };
93
+ e.style.opacity = "0";
94
+ let o = !1;
95
+ return I(
96
+ e,
97
+ () => {
98
+ o || (o = !0, a(
99
+ e,
100
+ { opacity: [0, 1], y: [t.y ?? 16, 0] },
101
+ { duration: t.duration ?? 0.5, ease: "easeOut" }
102
+ ));
103
+ },
104
+ { amount: t.amount ?? 0.2 }
105
+ );
106
+ }
107
+ export {
108
+ G as a,
109
+ P as b,
110
+ N as c,
111
+ A as d,
112
+ k as e,
113
+ _ as i,
114
+ c as m,
115
+ T as p,
116
+ Q as r,
117
+ b as s,
118
+ V as u
119
+ };
@@ -0,0 +1,34 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface AnimatedBeamProps {
3
+ containerRef: HTMLElement | undefined;
4
+ fromRef: HTMLElement | undefined;
5
+ toRef: HTMLElement | undefined;
6
+ /** Px the curve bows away from a straight line. @default 0 (straight) */
7
+ curvature?: number;
8
+ /** Travel from `toRef` to `fromRef` instead of the default direction. */
9
+ reverse?: boolean;
10
+ /** Seconds for one travel of the gradient segment. @default 3 */
11
+ duration?: number;
12
+ class?: string;
13
+ }
14
+ /**
15
+ * Renders an animated SVG "beam" connecting two elements: a faint static
16
+ * base stroke under a traveling gradient segment that loops from `fromRef`
17
+ * to `toRef` (or the reverse, with `reverse`). Absolutely positioned over
18
+ * `containerRef`, whose bounds establish the coordinate space both
19
+ * endpoints are measured against. Static (no travel) under reduced motion.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * let container: HTMLDivElement | undefined
24
+ * let fromEl: HTMLDivElement | undefined
25
+ * let toEl: HTMLDivElement | undefined
26
+ *
27
+ * <div ref={container} class="relative flex items-center justify-between p-8">
28
+ * <div ref={fromEl} class="size-10 rounded-full bg-card" />
29
+ * <div ref={toEl} class="size-10 rounded-full bg-card" />
30
+ * <AnimatedBeam containerRef={container} fromRef={fromEl} toRef={toEl} curvature={40} />
31
+ * </div>
32
+ * ```
33
+ */
34
+ export declare function AnimatedBeam(props: AnimatedBeamProps): JSX.Element;
@@ -0,0 +1,39 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface BentoGridProps {
3
+ children: JSX.Element;
4
+ class?: string;
5
+ }
6
+ export interface BentoCardProps {
7
+ children: JSX.Element;
8
+ /** Number of grid columns to span at `sm`+ (2) and `lg`+ (3). Defaults to 1. */
9
+ colSpan?: 1 | 2 | 3;
10
+ /** Number of grid rows to span. Defaults to 1. */
11
+ rowSpan?: 1 | 2;
12
+ class?: string;
13
+ }
14
+ /**
15
+ * Responsive CSS grid for bento-style layouts — 1 column on mobile, 2 at
16
+ * `sm`, 3 at `lg`. Compose with {@link BentoCard} tiles, using `colSpan`/
17
+ * `rowSpan` on each tile to create the varied-size bento look.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <BentoGrid>
22
+ * <BentoCard colSpan={2} rowSpan={2}>
23
+ * <h3 class="text-lg font-semibold">Overview</h3>
24
+ * </BentoCard>
25
+ * <BentoCard>
26
+ * <h3 class="text-lg font-semibold">Uptime</h3>
27
+ * </BentoCard>
28
+ * <BentoCard>
29
+ * <h3 class="text-lg font-semibold">Latency</h3>
30
+ * </BentoCard>
31
+ * <BentoCard colSpan={3}>
32
+ * <h3 class="text-lg font-semibold">Recent activity</h3>
33
+ * </BentoCard>
34
+ * </BentoGrid>
35
+ * ```
36
+ */
37
+ export declare function BentoGrid(props: BentoGridProps): JSX.Element;
38
+ /** Glass tile for a {@link BentoGrid}, spanning columns/rows via `colSpan`/`rowSpan`. */
39
+ export declare function BentoCard(props: BentoCardProps): JSX.Element;
@@ -0,0 +1,27 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface BorderBeamProps {
3
+ /** Length of the traveling gradient segment, in px. @default 60 */
4
+ size?: number;
5
+ /** Full trip duration around the border, in seconds. @default 6 */
6
+ duration?: number;
7
+ /** Animation start offset, in seconds. @default 0 */
8
+ delay?: number;
9
+ class?: string;
10
+ }
11
+ /**
12
+ * Decorative light segment that travels continuously around the border of
13
+ * its parent. The parent must be `position: relative` (or similar) — this
14
+ * renders an absolutely-positioned, `pointer-events-none` layer (`inset-0`,
15
+ * `rounded-[inherit]`) tracking the parent's own box. Purely cosmetic
16
+ * (`aria-hidden`); under reduced motion the moving beam is replaced with a
17
+ * static edge glow instead of animating.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <div class="relative overflow-hidden rounded-2xl border border-border p-6">
22
+ * <BorderBeam />
23
+ * <p>Card content</p>
24
+ * </div>
25
+ * ```
26
+ */
27
+ export declare function BorderBeam(props: BorderBeamProps): JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface DockItem {
3
+ icon: JSX.Element;
4
+ label?: string;
5
+ onClick?: (e: MouseEvent) => void;
6
+ href?: string;
7
+ }
8
+ export interface DockProps {
9
+ items: DockItem[];
10
+ class?: string;
11
+ }
12
+ /**
13
+ * macOS-style dock: a glass pill of icon buttons that magnify based on
14
+ * horizontal distance to the cursor. Scale-only transform (`transform-origin:
15
+ * bottom`), reset to 1× on pointerleave. Respects `prefers-reduced-motion`
16
+ * (renders static, still fully clickable/keyboard-focusable).
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * import { Home, Search, Settings } from 'lucide-solid'
21
+ *
22
+ * <Dock
23
+ * items={[
24
+ * { icon: <Home size={22} />, label: 'Home', onClick: () => {} },
25
+ * { icon: <Search size={22} />, label: 'Search', onClick: () => {} },
26
+ * { icon: <Settings size={22} />, label: 'Settings', href: '/settings' },
27
+ * ]}
28
+ * />
29
+ * ```
30
+ */
31
+ export declare function Dock(props: DockProps): JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface MeteorsProps {
3
+ /** Number of meteor streaks to render. @default 20 */
4
+ count?: number;
5
+ class?: string;
6
+ }
7
+ /**
8
+ * Decorative full-cover meteor shower: thin diagonal streaks that fall
9
+ * down-left and fade, looping with staggered delays and varied start
10
+ * positions/durations. Purely visual — absolute, `pointer-events-none`,
11
+ * `aria-hidden` — so it layers behind/inside a `relative overflow-hidden`
12
+ * container without affecting layout or a11y. Under {@link motionReduced} it
13
+ * swaps the falling streaks for a handful of static, faint dots.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * <div class="relative overflow-hidden rounded-2xl border border-border p-8">
18
+ * <Meteors count={20} />
19
+ * <p class="relative">Content sits above the meteors.</p>
20
+ * </div>
21
+ * ```
22
+ */
23
+ export declare function Meteors(props: MeteorsProps): JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a4ui/core",
3
- "version": "0.31.0",
3
+ "version": "0.32.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",
@@ -0,0 +1,175 @@
1
+ // GaugeChart — a native SVG radial gauge (no charting library). A single
2
+ // 270° arc (from -135° to +135°, leaving a 90° gap at the bottom) is drawn
3
+ // twice: a static background track and a value arc revealed proportionally
4
+ // to `value` within `[min, max]` via the classic stroke-dasharray/
5
+ // stroke-dashoffset trick (dash length == full path length, offset shifts
6
+ // how much of it is "on" from the arc's start). The value arc's color comes
7
+ // from `tone`, or from the highest `threshold` the value has reached. The
8
+ // sweep animates on mount and on value change (CSS transition on
9
+ // stroke-dashoffset), jumping instantly under `motionReduced()`.
10
+ import { createEffect, createSignal, on, onMount, Show, type JSX } from 'solid-js'
11
+
12
+ import { cn } from '../lib/cn'
13
+ import { motionReduced } from '../lib/motion'
14
+
15
+ export interface GaugeThreshold {
16
+ value: number
17
+ tone: 'primary' | 'accent' | 'destructive' | 'muted'
18
+ }
19
+
20
+ export interface GaugeChartProps {
21
+ value: number
22
+ /** Value mapped to the arc's start (-135°). Default 0. */
23
+ min?: number
24
+ /** Value mapped to the arc's end (+135°). Default 100. */
25
+ max?: number
26
+ label?: JSX.Element
27
+ unit?: string
28
+ /** Overall SVG width/height in px (square). Default 160. */
29
+ size?: number
30
+ /** Value arc color when `thresholds` are omitted. Default 'primary'. */
31
+ tone?: 'primary' | 'accent'
32
+ /** Recolor the value arc once `value` reaches a threshold's `value` (highest match wins). */
33
+ thresholds?: GaugeThreshold[]
34
+ class?: string
35
+ }
36
+
37
+ // Explicit tone -> CSS custom property name.
38
+ const TONE_TOKENS: Record<GaugeThreshold['tone'], string> = {
39
+ primary: '--primary',
40
+ accent: '--accent',
41
+ destructive: '--destructive',
42
+ muted: '--muted',
43
+ }
44
+
45
+ const DEG_TO_RAD = Math.PI / 180
46
+ const START_ANGLE = -135
47
+ const END_ANGLE = 135
48
+ const SWEEP_ANGLE = END_ANGLE - START_ANGLE // 270°
49
+
50
+ /** Cartesian point on a circle of radius `r` centered at `(cx, cy)`, `angleDeg` measured clockwise from 12 o'clock. */
51
+ function pointOnArc(cx: number, cy: number, r: number, angleDeg: number): { x: number; y: number } {
52
+ const rad = angleDeg * DEG_TO_RAD
53
+ return { x: cx + r * Math.sin(rad), y: cy - r * Math.cos(rad) }
54
+ }
55
+
56
+ /** SVG path `d` for the fixed 270° arc (shared by the track and the value arc). */
57
+ function gaugeArcPath(cx: number, cy: number, r: number): string {
58
+ const start = pointOnArc(cx, cy, r, START_ANGLE)
59
+ const end = pointOnArc(cx, cy, r, END_ANGLE)
60
+ return `M ${start.x} ${start.y} A ${r} ${r} 0 1 1 ${end.x} ${end.y}`
61
+ }
62
+
63
+ /** Highest threshold whose `value` the gauge has reached, falling back to `tone` when none match. */
64
+ function resolveTone(value: number, tone: 'primary' | 'accent', thresholds?: GaugeThreshold[]): string {
65
+ if (!thresholds || thresholds.length === 0) return TONE_TOKENS[tone]
66
+ let resolved = TONE_TOKENS[tone]
67
+ for (const threshold of [...thresholds].sort((a, b) => a.value - b.value)) {
68
+ if (value >= threshold.value) resolved = TONE_TOKENS[threshold.tone]
69
+ }
70
+ return resolved
71
+ }
72
+
73
+ /**
74
+ * SVG radial gauge: a 270° arc (from -135° to +135°) with a background track
75
+ * and a value arc filled proportionally to `value` within `[min, max]`,
76
+ * colored by `tone` or by the highest `threshold` reached. The center shows
77
+ * the value plus optional `unit`/`label`. The sweep animates on mount and on
78
+ * value change, jumping instantly under reduced motion.
79
+ *
80
+ * @example
81
+ * ```tsx
82
+ * <GaugeChart
83
+ * value={72}
84
+ * unit="%"
85
+ * label="CPU load"
86
+ * thresholds={[
87
+ * { value: 0, tone: 'primary' },
88
+ * { value: 60, tone: 'accent' },
89
+ * { value: 85, tone: 'destructive' },
90
+ * ]}
91
+ * />
92
+ * ```
93
+ */
94
+ export function GaugeChart(props: GaugeChartProps): JSX.Element {
95
+ const size = () => props.size ?? 160
96
+ const min = () => props.min ?? 0
97
+ const max = () => props.max ?? 100
98
+ const tone = () => props.tone ?? 'primary'
99
+ const thickness = () => size() / 8
100
+ const center = () => size() / 2
101
+ const radius = () => Math.max(size() / 2 - thickness() / 2, 0)
102
+ const totalLength = () => radius() * (SWEEP_ANGLE * DEG_TO_RAD)
103
+ const arcPath = () => gaugeArcPath(center(), center(), radius())
104
+
105
+ const clampedValue = () => Math.min(Math.max(props.value, min()), max())
106
+ const fraction = () => {
107
+ const range = max() - min()
108
+ return range > 0 ? (clampedValue() - min()) / range : 0
109
+ }
110
+ const strokeToken = () => resolveTone(clampedValue(), tone(), props.thresholds)
111
+
112
+ // Drives the visible sweep; starts at 0 so the mount animation reveals the
113
+ // arc from empty, then tracks `fraction()` (deferred, so mount handles the
114
+ // initial reveal and this only reacts to later value changes).
115
+ const [displayedFraction, setDisplayedFraction] = createSignal(0)
116
+
117
+ onMount(() => {
118
+ if (motionReduced()) {
119
+ setDisplayedFraction(fraction())
120
+ return
121
+ }
122
+ requestAnimationFrame(() => setDisplayedFraction(fraction()))
123
+ })
124
+
125
+ createEffect(on(fraction, (f) => setDisplayedFraction(f), { defer: true }))
126
+
127
+ const dashOffset = () => totalLength() * (1 - displayedFraction())
128
+ const transition = () => (motionReduced() ? 'none' : 'stroke-dashoffset 700ms ease-out')
129
+
130
+ const displayValue = () => Math.round(clampedValue())
131
+ const ariaLabel = () => `${displayValue()}${props.unit ?? ''}`
132
+
133
+ return (
134
+ <div
135
+ class={cn('relative inline-flex items-center justify-center', props.class)}
136
+ style={{ width: `${size()}px`, height: `${size()}px` }}
137
+ role="meter"
138
+ aria-valuenow={clampedValue()}
139
+ aria-valuemin={min()}
140
+ aria-valuemax={max()}
141
+ aria-label={ariaLabel()}
142
+ >
143
+ <svg width={size()} height={size()} viewBox={`0 0 ${size()} ${size()}`} role="img" aria-hidden="true">
144
+ <path
145
+ d={arcPath()}
146
+ fill="none"
147
+ stroke="hsl(var(--muted))"
148
+ stroke-width={thickness()}
149
+ stroke-linecap="round"
150
+ />
151
+ <path
152
+ d={arcPath()}
153
+ fill="none"
154
+ stroke={`hsl(var(${strokeToken()}))`}
155
+ stroke-width={thickness()}
156
+ stroke-linecap="round"
157
+ stroke-dasharray={`${totalLength()} ${totalLength()}`}
158
+ stroke-dashoffset={dashOffset()}
159
+ style={{ transition: transition() }}
160
+ />
161
+ </svg>
162
+ <div class="absolute inset-0 flex flex-col items-center justify-center gap-0.5 px-4 text-center">
163
+ <span class="text-2xl font-semibold tabular-nums text-foreground">
164
+ {displayValue()}
165
+ <Show when={props.unit}>
166
+ <span class="text-base font-medium">{props.unit}</span>
167
+ </Show>
168
+ </span>
169
+ <Show when={props.label}>
170
+ <span class="text-xs text-muted-foreground">{props.label}</span>
171
+ </Show>
172
+ </div>
173
+ </div>
174
+ )
175
+ }