@a4ui/core 0.32.0 → 0.33.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,27 @@
1
+ import { JSX } from 'solid-js';
2
+ /** Semantic tone of a {@link Callout}; drives the accent border, tint, and default icon. */
3
+ export type CalloutTone = 'info' | 'success' | 'warning' | 'danger' | 'neutral';
4
+ export interface CalloutProps {
5
+ /** Visual/semantic tone. Defaults to `'info'`. */
6
+ tone?: 'info' | 'success' | 'warning' | 'danger' | 'neutral';
7
+ /** Optional bold heading shown above the body. */
8
+ title?: JSX.Element;
9
+ /** Overrides the tone's default icon. */
10
+ icon?: JSX.Element;
11
+ children: JSX.Element;
12
+ class?: string;
13
+ }
14
+ /**
15
+ * Inline highlighted block for embedding guidance/notes inside prose (docs,
16
+ * READMEs, long-form content) — a left accent border, a faint tone-tinted
17
+ * background, and an icon. Unlike {@link Alert} it isn't a dismissible
18
+ * notification; it's part of the content flow.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <Callout tone="warning" title="Before you continue">
23
+ * This action can't be undone once the migration starts.
24
+ * </Callout>
25
+ * ```
26
+ */
27
+ export declare function Callout(props: CalloutProps): JSX.Element;
@@ -0,0 +1,46 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface ConfettiProps {
3
+ /** Bump this (e.g. a counter) to fire a burst. Mount does not count. */
4
+ trigger?: number;
5
+ /** Number of pieces per burst. @default 80 */
6
+ count?: number;
7
+ class?: string;
8
+ }
9
+ /**
10
+ * Fires one burst of confetti pieces from the bottom-center of `container`,
11
+ * flying up and outward in a fan, rotating, then falling with gravity and
12
+ * fading out — each piece removes itself once its animation finishes. Angle,
13
+ * speed, fall distance and spin are derived deterministically from each
14
+ * piece's index (Math.sin), so the same `count` always produces the same
15
+ * fan. `container` should be `position: relative` (or already positioned)
16
+ * with `overflow: hidden` so pieces don't affect layout. No-op under
17
+ * {@link motionReduced}. This is the primitive behind {@link Confetti}.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * fireConfetti(cardEl, { count: 120 })
22
+ * ```
23
+ */
24
+ export declare function fireConfetti(container: HTMLElement, opts?: {
25
+ count?: number;
26
+ }): void;
27
+ /**
28
+ * Absolutely-positioned confetti layer: fires a burst of colored pieces from
29
+ * the bottom-center whenever `trigger` changes (the initial mount does not
30
+ * fire one). Purely decorative — `pointer-events-none` and `aria-hidden`.
31
+ * Place it inside a `position: relative` container; pair with a `count`
32
+ * prop, or reach for {@link fireConfetti} directly to fire into an
33
+ * arbitrary element outside Solid's render tree.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * const [bursts, setBursts] = createSignal(0)
38
+ * return (
39
+ * <div class="relative h-64">
40
+ * <Confetti trigger={bursts()} count={100} />
41
+ * <Button onClick={() => setBursts((n) => n + 1)}>Celebrate</Button>
42
+ * </div>
43
+ * )
44
+ * ```
45
+ */
46
+ export declare function Confetti(props: ConfettiProps): JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface CursorTrailProps {
3
+ /** Blob tone. @default 'primary' */
4
+ color?: 'primary' | 'accent';
5
+ /** Blob diameter in px. @default 24 */
6
+ size?: number;
7
+ class?: string;
8
+ }
9
+ /**
10
+ * Layer that spawns a soft, blurred blob at the cursor position on every
11
+ * throttled `pointermove` over its parent, scaling down and fading out as
12
+ * it removes itself — leaving a trail. Must sit inside a `position:
13
+ * relative` parent (the layer listens on `parentElement`, since it is
14
+ * itself `pointer-events-none`). Purely decorative — `pointer-events-none`
15
+ * and `aria-hidden`. No-op under {@link motionReduced}; listeners are
16
+ * cleaned up on unmount.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * <div class="relative h-48 rounded-lg border">
21
+ * <CursorTrail color="accent" size={32} />
22
+ * </div>
23
+ * ```
24
+ */
25
+ export declare function CursorTrail(props: CursorTrailProps): JSX.Element;
@@ -0,0 +1,33 @@
1
+ import { JSX } from 'solid-js';
2
+ /** A single rendered row of a diff. */
3
+ export interface DiffLine {
4
+ type: 'add' | 'remove' | 'context';
5
+ content: string;
6
+ /** Line number in the old file. Present for `'remove'` and `'context'` rows. */
7
+ oldNum?: number;
8
+ /** Line number in the new file. Present for `'add'` and `'context'` rows. */
9
+ newNum?: number;
10
+ }
11
+ export interface DiffViewerProps {
12
+ /** Unified diff text (as produced by `git diff`/`diff -u`). Ignored if `lines` is set. */
13
+ diff?: string;
14
+ /** Pre-parsed diff rows. Takes precedence over `diff`. */
15
+ lines?: DiffLine[];
16
+ /** Header label. If omitted, inferred from the `+++`/`---` headers of `diff`, when present. */
17
+ filename?: string;
18
+ class?: string;
19
+ }
20
+ /**
21
+ * Read-only diff block: renders added/removed/context lines with a line-number
22
+ * gutter and a +/-/space marker. Accepts either a raw unified-diff string
23
+ * (parsed internally) or pre-parsed {@link DiffLine} rows.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * <DiffViewer
28
+ * filename="src/lib/cn.ts"
29
+ * diff={`@@ -1,3 +1,3 @@\n-export const cn = (a) => a\n+export const cn = (a, b) => a + b\n context line`}
30
+ * />
31
+ * ```
32
+ */
33
+ export declare function DiffViewer(props: DiffViewerProps): JSX.Element;
@@ -0,0 +1,44 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface GlobeMarker {
3
+ lat: number;
4
+ lng: number;
5
+ label?: string;
6
+ }
7
+ export interface GlobeArc {
8
+ /** [lat, lng] */
9
+ from: [number, number];
10
+ /** [lat, lng] */
11
+ to: [number, number];
12
+ }
13
+ export interface GlobeProps {
14
+ markers?: GlobeMarker[];
15
+ arcs?: GlobeArc[];
16
+ /** Canvas size in CSS px (square). @default 400 */
17
+ size?: number;
18
+ /** Spin automatically. @default true (ignored under reduced motion). */
19
+ autoRotate?: boolean;
20
+ class?: string;
21
+ }
22
+ /**
23
+ * A dotted 3D globe drawn on `<canvas>` with plain Canvas 2D (no three.js,
24
+ * no WebGL): dots come from a deterministic Fibonacci-sphere distribution,
25
+ * rotate around the Y axis, and project orthographically with depth-scaled
26
+ * alpha/size. `markers` render as brighter glowing dots (title = label);
27
+ * `arcs` draw as outward-bulging curves between two front-facing markers,
28
+ * with a small traveling highlight. Auto-rotates unless `autoRotate={false}`;
29
+ * drag with the pointer to spin manually. Renders a single static frame
30
+ * under `motionReduced()` (no auto-rotation; drag still works).
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * <Globe
35
+ * size={360}
36
+ * markers={[
37
+ * { lat: 40.7, lng: -74.0, label: 'New York' },
38
+ * { lat: 35.7, lng: 139.7, label: 'Tokyo' },
39
+ * ]}
40
+ * arcs={[{ from: [40.7, -74.0], to: [35.7, 139.7] }]}
41
+ * />
42
+ * ```
43
+ */
44
+ export declare function Globe(props: GlobeProps): JSX.Element;
@@ -0,0 +1,38 @@
1
+ import { JSX } from 'solid-js';
2
+ /** A single selectable model in a {@link ModelPicker} menu. */
3
+ export interface ModelOption {
4
+ id: string;
5
+ name: string;
6
+ icon?: JSX.Element;
7
+ description?: string;
8
+ badge?: JSX.Element;
9
+ }
10
+ export interface ModelPickerProps {
11
+ models: ModelOption[];
12
+ /** Controlled selected model id. Omit and use `defaultValue` for uncontrolled use. */
13
+ value?: string;
14
+ /** Initial selected model id when uncontrolled. Ignored if `value` is passed. */
15
+ defaultValue?: string;
16
+ onChange?: (id: string) => void;
17
+ class?: string;
18
+ }
19
+ /**
20
+ * Dropdown trigger showing the selected model's icon + name + a chevron; the
21
+ * menu lists every model with icon, name, description, and optional badge,
22
+ * checkmarking the current selection. Controlled via `value`/`onChange` or
23
+ * uncontrolled via `defaultValue`, and fully keyboard-navigable (arrow keys,
24
+ * typeahead, Escape) via Kobalte's `DropdownMenu`.
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <ModelPicker
29
+ * models={[
30
+ * { id: 'sonnet', name: 'Claude Sonnet', description: 'Balanced speed and quality' },
31
+ * { id: 'opus', name: 'Claude Opus', description: 'Most capable', badge: <Badge tone="info">New</Badge> },
32
+ * ]}
33
+ * defaultValue="sonnet"
34
+ * onChange={(id) => console.log('selected', id)}
35
+ * />
36
+ * ```
37
+ */
38
+ export declare function ModelPicker(props: ModelPickerProps): JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface ReasoningTraceProps {
3
+ /** Reasoning body content; takes precedence over `text` when both are given. */
4
+ children?: JSX.Element;
5
+ /** Plain-text reasoning body, rendered with preserved whitespace. */
6
+ text?: string;
7
+ /** Header label. Defaults to `'Reasoning'`. */
8
+ label?: JSX.Element;
9
+ /** Whether the panel starts expanded. Defaults to `false`. */
10
+ defaultOpen?: boolean;
11
+ /** Show a pulsing dot in the header to indicate reasoning is still streaming. */
12
+ streaming?: boolean;
13
+ class?: string;
14
+ }
15
+ /**
16
+ * Collapsible panel for surfacing a model's intermediate "thinking"/reasoning
17
+ * trace. The header is a toggle button (brain icon + label + rotating
18
+ * chevron, plus a pulsing dot while `streaming`); the body is a muted,
19
+ * smaller-text block showing `text` or `children` with whitespace preserved.
20
+ * Collapsed by default unless `defaultOpen`. Height/opacity transition,
21
+ * instant under reduced motion.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * <ReasoningTrace streaming text={reasoningSoFar()} />
26
+ * ```
27
+ */
28
+ export declare function ReasoningTrace(props: ReasoningTraceProps): JSX.Element;
@@ -0,0 +1,19 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface SnippetProps {
3
+ /** Source text to display and copy. */
4
+ code: string;
5
+ /** Optional language tag shown as a small badge, e.g. `'tsx'`. Informational only, no highlighting. */
6
+ language?: string;
7
+ class?: string;
8
+ }
9
+ /**
10
+ * Single inline code block (mono, bordered, scrollable) with a copy button
11
+ * that morphs into a checkmark for 1.5s after copying. For one code sample —
12
+ * use {@link CodeTabs} instead when you need several tabs/languages.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <Snippet language="bash" code="pnpm add @a4ui/core" />
17
+ * ```
18
+ */
19
+ export declare function Snippet(props: SnippetProps): JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface SuggestionChipsProps {
3
+ suggestions: string[];
4
+ onSelect?: (s: string) => void;
5
+ onDismiss?: (s: string) => void;
6
+ class?: string;
7
+ }
8
+ /**
9
+ * Wrapping row of pill chips, e.g. AI-suggested follow-up prompts. Clicking a
10
+ * chip calls `onSelect`; if `onDismiss` is passed, each chip also shows a
11
+ * small "x" that removes it without triggering `onSelect`. The select and
12
+ * dismiss controls are real sibling `<button>`s (not nested — a `<button>`
13
+ * can't validly contain another interactive element), so both are reachable
14
+ * and operable independently via Tab/Enter/Space with no extra ARIA wiring.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * <SuggestionChips
19
+ * suggestions={['Summarize this', 'Explain like I\'m 5', 'Write tests']}
20
+ * onSelect={(s) => sendMessage(s)}
21
+ * onDismiss={(s) => setSuggestions((prev) => prev.filter((x) => x !== s))}
22
+ * />
23
+ * ```
24
+ */
25
+ export declare function SuggestionChips(props: SuggestionChipsProps): JSX.Element;
@@ -0,0 +1,34 @@
1
+ import { JSX } from 'solid-js';
2
+ /** Status of a single tool call rendered by {@link ToolCallTimeline}. */
3
+ export type ToolCallStatus = 'pending' | 'success' | 'error';
4
+ /** A single tool invocation rendered by {@link ToolCallTimeline}. */
5
+ export interface ToolCall {
6
+ /** Tool/function name, rendered in mono. */
7
+ name: string;
8
+ status: ToolCallStatus;
9
+ /** Arguments passed to the call. Shown in a collapsible details area when present. */
10
+ args?: JSX.Element | string;
11
+ /** Result returned by the call. Shown in a collapsible details area when present. */
12
+ result?: JSX.Element | string;
13
+ }
14
+ export interface ToolCallTimelineProps {
15
+ calls: ToolCall[];
16
+ class?: string;
17
+ }
18
+ /**
19
+ * Vertical timeline of AI tool calls: a connector line runs down the left with
20
+ * a status node per call (spinner while `pending`, check when `success`, x when
21
+ * `error`), the tool name in mono, and — when `args`/`result` are given — a
22
+ * collapsible details panel underneath.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <ToolCallTimeline
27
+ * calls={[
28
+ * { name: 'search_docs', status: 'success', args: 'query: "refunds"', result: '3 matches' },
29
+ * { name: 'update_ticket', status: 'pending' },
30
+ * ]}
31
+ * />
32
+ * ```
33
+ */
34
+ export declare function ToolCallTimeline(props: ToolCallTimelineProps): JSX.Element;
@@ -0,0 +1,26 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface UsageMeterProps {
3
+ used: number;
4
+ limit: number;
5
+ /** Header content shown before the "{used}/{limit}{unit}" count. */
6
+ label?: JSX.Element;
7
+ /** Appended to displayed numbers, e.g. `' tokens'` or `'%'`. */
8
+ unit?: string;
9
+ /** Fraction of `limit` (0–1) at which the fill switches to the warning tone. @default 0.85 */
10
+ warnAt?: number;
11
+ class?: string;
12
+ }
13
+ /**
14
+ * Labeled consumption bar: a header row with `label` and the raw
15
+ * "{used}/{limit}{unit}" count, a track whose fill width is `used / limit`
16
+ * (clamped to 0–100%) and switches to a warning tone once that ratio reaches
17
+ * `warnAt`, and a small "{remaining} left" line underneath. Built on Kobalte's
18
+ * `Meter` primitive, so it carries the same `role="meter"` a11y semantics as
19
+ * {@link Meter}.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * <UsageMeter used={92} limit={100} label="API requests" unit=" req" warnAt={0.9} />
24
+ * ```
25
+ */
26
+ export declare function UsageMeter(props: UsageMeterProps): JSX.Element;
@@ -0,0 +1,36 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface WorldMapPoint {
3
+ lat: number;
4
+ lng: number;
5
+ label?: string;
6
+ }
7
+ export interface WorldMapConnection {
8
+ from: WorldMapPoint;
9
+ to: WorldMapPoint;
10
+ }
11
+ export interface WorldMapProps {
12
+ /** Arcs to draw between projected points. @default [] */
13
+ connections?: WorldMapConnection[];
14
+ class?: string;
15
+ }
16
+ /**
17
+ * A decorative, dependency-free SVG "world map": an evenly spaced dotted-grid
18
+ * backdrop (a stylized map, not accurate coastlines) with connection arcs
19
+ * projected from lat/lng via equirectangular mapping. Each arc bulges upward
20
+ * and carries a traveling primary→accent gradient segment (reusing
21
+ * {@link AnimatedBeam}'s technique); arcs render static under reduced motion.
22
+ * Responsive — scales to its container via the 2:1 `viewBox`.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <WorldMap
27
+ * class="w-full"
28
+ * connections={[
29
+ * { from: { lat: 40.7128, lng: -74.006, label: 'New York' }, to: { lat: 51.5074, lng: -0.1278, label: 'London' } },
30
+ * { from: { lat: 51.5074, lng: -0.1278 }, to: { lat: 35.6762, lng: 139.6503, label: 'Tokyo' } },
31
+ * { from: { lat: 40.7128, lng: -74.006 }, to: { lat: -23.5505, lng: -46.6333, label: 'São Paulo' } },
32
+ * ]}
33
+ * />
34
+ * ```
35
+ */
36
+ export declare function WorldMap(props: WorldMapProps): JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a4ui/core",
3
- "version": "0.32.0",
3
+ "version": "0.33.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,83 @@
1
+ // BarList — a ranked horizontal bar list built from plain flex `div`s (no
2
+ // SVG, no charting library). Rows are sorted by value descending; each row
3
+ // is a full-width track with a low-opacity theme-toned fill sized to
4
+ // `value / max`, the `name` overlaid at the left (truncated, linked when
5
+ // `href` is set) and the formatted value at the right. Colors come from
6
+ // theme tokens only.
7
+ import { For, Show, type JSX } from 'solid-js'
8
+
9
+ import { cn } from '../lib/cn'
10
+
11
+ export interface BarListDatum {
12
+ name: string
13
+ value: number
14
+ href?: string
15
+ }
16
+
17
+ export interface BarListProps {
18
+ data: BarListDatum[]
19
+ /** Formats the raw value shown at the row's end. Default `String`. */
20
+ valueFormat?: (n: number) => string
21
+ /** Fill color token. Default 'primary'. */
22
+ tone?: 'primary' | 'accent'
23
+ class?: string
24
+ }
25
+
26
+ /**
27
+ * Ranked horizontal bar list: rows sorted by `value` descending, each a
28
+ * full-width track with a low-opacity fill sized to `value / max` of the
29
+ * tallest row, the `name` overlaid at the left (truncated, rendered as a
30
+ * link when `href` is set) and the formatted value at the right. Compact
31
+ * and reflows with its container.
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * <BarList
36
+ * data={[
37
+ * { name: '/dashboard', value: 842, href: '/pages/dashboard' },
38
+ * { name: '/settings', value: 231 },
39
+ * ]}
40
+ * tone="accent"
41
+ * />
42
+ * ```
43
+ */
44
+ export function BarList(props: BarListProps): JSX.Element {
45
+ const tone = () => props.tone ?? 'primary'
46
+ const format = () => props.valueFormat ?? ((n: number) => String(n))
47
+ const fillClass = () => (tone() === 'accent' ? 'bg-accent/15' : 'bg-primary/15')
48
+ const sorted = () => [...props.data].sort((a, b) => b.value - a.value)
49
+ const max = () => Math.max(...props.data.map((d) => d.value), 0) || 1
50
+
51
+ return (
52
+ <div class={cn('flex w-full flex-col gap-1.5', props.class)}>
53
+ <For each={sorted()}>
54
+ {(datum) => (
55
+ <div class="relative isolate flex h-8 w-full min-w-0 items-center overflow-hidden rounded-md bg-muted/40">
56
+ <div
57
+ class={cn('absolute inset-y-0 left-0 rounded-md transition-all', fillClass())}
58
+ style={{ width: `${Math.max((datum.value / max()) * 100, 0)}%` }}
59
+ />
60
+ <Show
61
+ when={datum.href}
62
+ fallback={
63
+ <span class="relative z-10 min-w-0 flex-1 truncate px-2.5 text-sm text-foreground">
64
+ {datum.name}
65
+ </span>
66
+ }
67
+ >
68
+ <a
69
+ href={datum.href}
70
+ class="relative z-10 min-w-0 flex-1 truncate px-2.5 text-sm text-foreground underline-offset-2 hover:underline"
71
+ >
72
+ {datum.name}
73
+ </a>
74
+ </Show>
75
+ <span class="relative z-10 shrink-0 px-2.5 text-sm tabular-nums text-muted-foreground">
76
+ {format()(datum.value)}
77
+ </span>
78
+ </div>
79
+ )}
80
+ </For>
81
+ </div>
82
+ )
83
+ }
@@ -0,0 +1,95 @@
1
+ // CategoryBar — a single horizontal segmented bar built from plain flex
2
+ // `div`s (no SVG, no charting library). Each segment's width is its share of
3
+ // the total (via CSS `flex-grow`), segments join seamlessly inside a
4
+ // rounded track, an optional caret marks a value along the total (pure-CSS
5
+ // border triangle), and cumulative boundary labels sit beneath. `success`/
6
+ // `warning` tones reuse `Alert`/`Badge`'s `emerald-500`/`amber-500` (the repo
7
+ // has no dedicated CSS token for them); `primary`/`accent`/`destructive` are
8
+ // the shared semantic tokens.
9
+ import { createMemo, For, Show, type JSX } from 'solid-js'
10
+
11
+ import { cn } from '../lib/cn'
12
+
13
+ export type CategoryBarTone = 'primary' | 'accent' | 'success' | 'warning' | 'destructive'
14
+
15
+ export interface CategoryBarProps {
16
+ values: number[]
17
+ /** A value along the cumulative total to mark with a caret above the bar. */
18
+ marker?: number
19
+ /** Per-segment color tokens; cycles the default order below when omitted or shorter than `values`. */
20
+ tones?: CategoryBarTone[]
21
+ class?: string
22
+ }
23
+
24
+ const DEFAULT_TONES: CategoryBarTone[] = ['primary', 'accent', 'success', 'warning', 'destructive']
25
+
26
+ const TONE_CLASSES: Record<CategoryBarTone, string> = {
27
+ primary: 'bg-primary',
28
+ accent: 'bg-accent',
29
+ success: 'bg-emerald-500',
30
+ warning: 'bg-amber-500',
31
+ destructive: 'bg-destructive',
32
+ }
33
+
34
+ /** Cumulative running sum of `values`, one boundary per entry (same length as `values`). */
35
+ function cumulativeBounds(values: number[]): number[] {
36
+ let running = 0
37
+ return values.map((v) => (running += v))
38
+ }
39
+
40
+ /**
41
+ * Single segmented bar: each `values[i]` sets a segment's flex-grow share of
42
+ * the total, colored by `tones[i]` (cycling a sensible default), joined
43
+ * seamlessly inside one rounded track. An optional `marker` renders a caret
44
+ * above the bar at that value's position along the total, and cumulative
45
+ * boundary labels sit beneath (muted, tabular-nums).
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * <CategoryBar values={[40, 25, 20, 15]} marker={62} />
50
+ * ```
51
+ */
52
+ export function CategoryBar(props: CategoryBarProps): JSX.Element {
53
+ const total = createMemo(() => props.values.reduce((sum, v) => sum + v, 0) || 1)
54
+ const bounds = createMemo(() => cumulativeBounds(props.values))
55
+ const tones = () => (props.tones && props.tones.length > 0 ? props.tones : DEFAULT_TONES)
56
+ const markerPct = createMemo(() =>
57
+ props.marker == null ? undefined : Math.min(Math.max((props.marker / total()) * 100, 0), 100),
58
+ )
59
+
60
+ return (
61
+ <div class={cn('w-full', props.class)}>
62
+ <div class="relative">
63
+ <Show when={markerPct() !== undefined}>
64
+ <div
65
+ class="absolute -top-1.5 h-0 w-0 -translate-x-1/2 border-x-4 border-t-4 border-x-transparent border-t-foreground"
66
+ style={{ left: `${markerPct()}%` }}
67
+ aria-hidden="true"
68
+ />
69
+ </Show>
70
+ <div class="flex h-2.5 w-full overflow-hidden rounded-full bg-muted">
71
+ <For each={props.values}>
72
+ {(value, i) => (
73
+ <div
74
+ class={cn('h-full', TONE_CLASSES[tones()[i() % tones().length] ?? 'primary'])}
75
+ style={{ flex: `${Math.max(value, 0)} 0 0%` }}
76
+ />
77
+ )}
78
+ </For>
79
+ </div>
80
+ </div>
81
+ <div class="relative mt-1 h-4 w-full text-xs tabular-nums text-muted-foreground">
82
+ <For each={bounds()}>
83
+ {(bound) => (
84
+ <span
85
+ class="absolute -translate-x-1/2 whitespace-nowrap"
86
+ style={{ left: `${Math.min(Math.max((bound / total()) * 100, 0), 100)}%` }}
87
+ >
88
+ {bound}
89
+ </span>
90
+ )}
91
+ </For>
92
+ </div>
93
+ </div>
94
+ )
95
+ }
@@ -0,0 +1,81 @@
1
+ // StatusTracker — a status-page style history bar built from plain flex
2
+ // `div`s (no SVG, no charting library). Each segment is a thin rounded bar
3
+ // colored by its status, with a native tooltip carrying the label. `ok`/
4
+ // `degraded` reuse the same success/warning tones as `Alert`/`Badge`
5
+ // (`emerald-500`/`amber-500` — the repo has no dedicated CSS token for
6
+ // them); `down` uses the `--destructive` semantic token.
7
+ import { For, Show, type JSX } from 'solid-js'
8
+
9
+ import { cn } from '../lib/cn'
10
+
11
+ export interface StatusSegment {
12
+ status: 'ok' | 'degraded' | 'down'
13
+ label?: string
14
+ }
15
+
16
+ export interface StatusTrackerProps {
17
+ segments: StatusSegment[]
18
+ class?: string
19
+ }
20
+
21
+ const STATUS_CLASSES: Record<StatusSegment['status'], string> = {
22
+ ok: 'bg-emerald-500',
23
+ degraded: 'bg-amber-500',
24
+ down: 'bg-destructive',
25
+ }
26
+
27
+ const STATUS_LABEL: Record<StatusSegment['status'], string> = {
28
+ ok: 'Operational',
29
+ degraded: 'Degraded',
30
+ down: 'Down',
31
+ }
32
+
33
+ /** Share of `segments` that are `'ok'`, formatted as e.g. `"98.7% uptime"`; `undefined` when empty. */
34
+ function uptimeSummary(segments: StatusSegment[]): string | undefined {
35
+ if (segments.length === 0) return undefined
36
+ const ok = segments.filter((s) => s.status === 'ok').length
37
+ return `${((ok / segments.length) * 100).toFixed(1)}% uptime`
38
+ }
39
+
40
+ /**
41
+ * Status-page style history bar: a row of thin rounded segments colored by
42
+ * `status` (ok/degraded/down), each with a native hover tooltip showing its
43
+ * label and status, plus a summary uptime line above.
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * <StatusTracker
48
+ * segments={[
49
+ * { status: 'ok', label: 'Jul 1' },
50
+ * { status: 'degraded', label: 'Jul 2' },
51
+ * { status: 'ok', label: 'Jul 3' },
52
+ * { status: 'down', label: 'Jul 4' },
53
+ * ]}
54
+ * />
55
+ * ```
56
+ */
57
+ export function StatusTracker(props: StatusTrackerProps): JSX.Element {
58
+ const summary = () => uptimeSummary(props.segments)
59
+
60
+ return (
61
+ <div class={cn('flex w-full flex-col gap-1.5', props.class)}>
62
+ <Show when={summary()}>
63
+ <span class="text-xs text-muted-foreground">{summary()}</span>
64
+ </Show>
65
+ <div class="flex w-full items-stretch gap-1">
66
+ <For each={props.segments}>
67
+ {(segment) => (
68
+ <div
69
+ title={
70
+ segment.label
71
+ ? `${segment.label}: ${STATUS_LABEL[segment.status]}`
72
+ : STATUS_LABEL[segment.status]
73
+ }
74
+ class={cn('h-6 flex-1 rounded-sm transition-colors', STATUS_CLASSES[segment.status])}
75
+ />
76
+ )}
77
+ </For>
78
+ </div>
79
+ </div>
80
+ )
81
+ }
@@ -6,3 +6,6 @@ export { DonutChart, type DonutSegment, type DonutChartProps } from './DonutChar
6
6
  export { LineChart, type LineSeries, type LineChartProps } from './LineChart'
7
7
  export { GaugeChart, type GaugeThreshold, type GaugeChartProps } from './GaugeChart'
8
8
  export { RadarChart, type RadarSeries, type RadarChartProps } from './RadarChart'
9
+ export { BarList, type BarListDatum, type BarListProps } from './BarList'
10
+ export { StatusTracker, type StatusSegment, type StatusTrackerProps } from './StatusTracker'
11
+ export { CategoryBar, type CategoryBarTone, type CategoryBarProps } from './CategoryBar'