@a4ui/core 0.31.1 → 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.
- package/dist/NumberInput-Bnh2u7pp.js +381 -0
- package/dist/charts/GaugeChart.d.ts +43 -0
- package/dist/charts/LineChart.d.ts +39 -0
- package/dist/charts/RadarChart.d.ts +34 -0
- package/dist/charts/index.d.ts +3 -0
- package/dist/charts.js +563 -100
- package/dist/commerce.js +2 -2
- package/dist/elements.css +111 -0
- package/dist/full.css +111 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3579 -3288
- package/dist/motion-jYMWmZqB.js +119 -0
- package/dist/ui/AnimatedBeam.d.ts +34 -0
- package/dist/ui/BentoGrid.d.ts +39 -0
- package/dist/ui/BorderBeam.d.ts +27 -0
- package/dist/ui/Dock.d.ts +31 -0
- package/dist/ui/Meteors.d.ts +23 -0
- package/package.json +1 -1
- package/src/charts/GaugeChart.tsx +175 -0
- package/src/charts/LineChart.tsx +264 -0
- package/src/charts/RadarChart.tsx +215 -0
- package/src/charts/index.ts +3 -0
- package/src/index.ts +8 -1
- package/src/ui/AnimatedBeam.tsx +187 -0
- package/src/ui/BentoGrid.tsx +89 -0
- package/src/ui/BorderBeam.tsx +96 -0
- package/src/ui/Dock.tsx +124 -0
- package/src/ui/Meteors.tsx +109 -0
- package/dist/NumberInput-BQhVucw-.js +0 -491
- package/dist/cn-B6yFEsav.js +0 -8
|
@@ -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
|
@@ -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
|
+
}
|