@a4ui/core 0.26.0 → 0.28.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.
@@ -9,6 +9,12 @@ export interface AuroraProps {
9
9
  * on any `.glow-edge` cards). Reduced-motion aware (off when reduced). @default true
10
10
  */
11
11
  pointerGlow?: boolean;
12
+ /**
13
+ * Backdrop style: `'blobs'` (soft blurred color blobs) or `'mesh'` (a
14
+ * multi-point gradient mesh). Both are theme-tinted and drift when `animated`.
15
+ * @default 'blobs'
16
+ */
17
+ variant?: 'blobs' | 'mesh';
12
18
  class?: string;
13
19
  }
14
20
  /**
@@ -0,0 +1,30 @@
1
+ export interface LinkedSelection<Id> {
2
+ hoveredId: () => Id | undefined;
3
+ selectedId: () => Id | undefined;
4
+ setHovered: (id: Id | undefined) => void;
5
+ setSelected: (id: Id | undefined) => void;
6
+ isHovered: (id: Id) => boolean;
7
+ isSelected: (id: Id) => boolean;
8
+ /** Convenience props to spread onto an element for a given id (onPointerEnter/Leave + onClick). */
9
+ itemProps: (id: Id) => {
10
+ onPointerEnter: () => void;
11
+ onPointerLeave: () => void;
12
+ onClick: () => void;
13
+ };
14
+ }
15
+ /**
16
+ * Shared hovered/selected state for two synchronized views of the same
17
+ * collection (a map and a list, a chart and a table, …): hover a list row to
18
+ * highlight the map pin, and vice versa.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * const selection = createLinkedSelection<string>()
23
+ * return (
24
+ * <For each={items}>
25
+ * {(item) => <ListRow class={selection.isSelected(item.id) ? 'active' : ''} {...selection.itemProps(item.id)} />}
26
+ * </For>
27
+ * )
28
+ * ```
29
+ */
30
+ export declare function createLinkedSelection<Id = string>(initialSelected?: Id): LinkedSelection<Id>;
@@ -0,0 +1,24 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface BalanceCardProps {
3
+ label: string;
4
+ amount: number;
5
+ /** ISO 4217 currency code. Defaults to `'USD'`. */
6
+ currency?: string;
7
+ /** BCP 47 locale used for formatting. Defaults to `'en-US'`. */
8
+ locale?: string;
9
+ /** Period-over-period change as a fraction (0.042 = +4.2%). Shows an up/down chip. */
10
+ delta?: number;
11
+ /** Small line under the balance (e.g. "Available" / account number). */
12
+ sub?: JSX.Element;
13
+ class?: string;
14
+ }
15
+ /**
16
+ * Balance-first summary card: label, large formatted amount, and an optional
17
+ * signed-percent delta chip (up/down colored via {@link Badge} tones).
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <BalanceCard label="Total balance" amount={128430.52} delta={0.042} />
22
+ * ```
23
+ */
24
+ export declare function BalanceCard(props: BalanceCardProps): JSX.Element;
@@ -0,0 +1,51 @@
1
+ import { JSX } from 'solid-js';
2
+ /** A single table column for {@link DataView}'s table presentation. */
3
+ export interface DataViewColumn<T> {
4
+ /** Row property this column reads. */
5
+ key: string;
6
+ /** Column heading text. */
7
+ header: string;
8
+ /** Custom cell renderer; falls back to `String(row[key])` when omitted. */
9
+ cell?: (row: T) => JSX.Element;
10
+ }
11
+ /** Presentation mode offered by {@link DataView}. */
12
+ export type DataViewMode = 'table' | 'board' | 'gallery';
13
+ /** Props for {@link DataView}. */
14
+ export interface DataViewProps<T> {
15
+ data: T[];
16
+ /** Table columns; also used as the source of truth in board/gallery fallbacks. */
17
+ columns: DataViewColumn<T>[];
18
+ /** Stable row identifier, used as a DOM hook for tests/a11y. */
19
+ getId: (row: T) => string;
20
+ /** For board view: group rows into columns by this key. If omitted, board mode is not offered. */
21
+ groupBy?: (row: T) => string;
22
+ /** Card renderer for board + gallery. Falls back to a simple key/value list. */
23
+ card?: (row: T) => JSX.Element;
24
+ /** Which views to offer (in order). Default: `['table', 'gallery']` plus `'board'` when `groupBy` is set. */
25
+ views?: DataViewMode[];
26
+ /** Controlled current view. Uncontrolled (internal signal) when omitted. */
27
+ view?: DataViewMode;
28
+ onViewChange?: (view: DataViewMode) => void;
29
+ class?: string;
30
+ }
31
+ /**
32
+ * One dataset, switchable between table, board (grouped columns), and
33
+ * gallery (card grid) presentations. Generic over the row type — pass
34
+ * `columns` for the table, an optional `groupBy` to enable the board view,
35
+ * and an optional `card` renderer reused by board + gallery.
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * <DataView
40
+ * data={tasks}
41
+ * getId={(t) => t.id}
42
+ * columns={[
43
+ * { key: 'title', header: 'Title' },
44
+ * { key: 'status', header: 'Status' },
45
+ * ]}
46
+ * groupBy={(t) => t.status}
47
+ * card={(t) => <span class="font-medium">{t.title}</span>}
48
+ * />
49
+ * ```
50
+ */
51
+ export declare function DataView<T>(props: DataViewProps<T>): JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface KpiBlockProps {
3
+ label: string;
4
+ value: string | number;
5
+ /** Change as a fraction (0.128 = +12.8%). Shows a signed up/down chip. */
6
+ delta?: number;
7
+ /** Optional chart node (e.g. a <Sparkline/> from @a4ui/core/charts) shown at the bottom. */
8
+ chart?: JSX.Element;
9
+ class?: string;
10
+ }
11
+ /**
12
+ * Labelled metric tile for dashboards: a large value with a signed
13
+ * up/down delta chip and an optional chart slot at the bottom. The chart is
14
+ * consumer-supplied (e.g. a `<Sparkline/>` from `@a4ui/core/charts`) — this
15
+ * component takes it as a prop rather than importing the charts package.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <KpiBlock label="Monthly revenue" value="$48,204" delta={0.128} />
20
+ * ```
21
+ */
22
+ export declare function KpiBlock(props: KpiBlockProps): JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { JSX } from 'solid-js';
2
+ /** Money-movement action a {@link MoneyActionButton} represents; picks the leading icon. */
3
+ export type MoneyActionKind = 'send' | 'request' | 'pay' | 'withdraw';
4
+ export interface MoneyActionButtonProps {
5
+ children: JSX.Element;
6
+ /** Which money-movement action this triggers. Defaults to `'send'`. */
7
+ kind?: MoneyActionKind;
8
+ onClick?: () => void;
9
+ disabled?: boolean;
10
+ class?: string;
11
+ }
12
+ /**
13
+ * Bold, full-height pill CTA reserved for value-moving actions — send,
14
+ * request, pay, withdraw. Uses the app's single accent (`bg-primary`) so it
15
+ * stands out from ordinary buttons, with a leading icon chosen by `kind`.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <MoneyActionButton kind="send" onClick={() => sendMoney()}>Send</MoneyActionButton>
20
+ * ```
21
+ */
22
+ export declare function MoneyActionButton(props: MoneyActionButtonProps): JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface ScrollSceneProps {
3
+ /** Render-prop receiving a reactive progress accessor (0..1). */
4
+ children: (progress: () => number) => JSX.Element;
5
+ class?: string;
6
+ }
7
+ /**
8
+ * Wraps `children` in an element that tracks its own scroll progress: 0 when
9
+ * its top edge reaches the bottom of the viewport, 1 when its bottom edge
10
+ * passes the top of the viewport. Progress is exposed as a reactive accessor
11
+ * via a render prop, so the caller drives whatever transform/animation it
12
+ * wants — this component only measures. Scroll position isn't itself motion,
13
+ * so it keeps measuring under `prefers-reduced-motion`; it just never
14
+ * animates anything on its own.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * <ScrollScene>
19
+ * {(progress) => (
20
+ * <div style={{ opacity: progress() }}>Reveal on scroll</div>
21
+ * )}
22
+ * </ScrollScene>
23
+ * ```
24
+ */
25
+ export declare function ScrollScene(props: ScrollSceneProps): JSX.Element;
@@ -0,0 +1,39 @@
1
+ import { JSX } from 'solid-js';
2
+ /** A single destination within a {@link SideRail}. */
3
+ export interface SideRailItem {
4
+ value: string;
5
+ label: string;
6
+ icon?: JSX.Element;
7
+ /** Optional badge (e.g. an unread count) rendered on the item. */
8
+ badge?: JSX.Element;
9
+ }
10
+ export interface SideRailProps {
11
+ items: SideRailItem[];
12
+ value: string;
13
+ onChange: (value: string) => void;
14
+ /** Show text labels under icons. When false, labels go in a tooltip. @default true */
15
+ labels?: boolean;
16
+ class?: string;
17
+ }
18
+ /**
19
+ * Narrow vertical column of stacked icon+label destinations — the vertical
20
+ * counterpart to {@link BottomNavigation} for side placement. The active item
21
+ * is highlighted with a left indicator bar and `aria-selected`. ArrowUp/
22
+ * ArrowDown move the selection between items (roving activation, matching the
23
+ * WAI-ARIA tabs pattern). Renders inline where placed — the consumer is
24
+ * responsible for positioning (e.g. `fixed inset-y-0 left-0`).
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <SideRail
29
+ * value={section()}
30
+ * onChange={setSection}
31
+ * items={[
32
+ * { value: 'home', label: 'Home', icon: <HomeIcon /> },
33
+ * { value: 'inbox', label: 'Inbox', icon: <InboxIcon />, badge: <Badge tone="info">3</Badge> },
34
+ * { value: 'settings', label: 'Settings', icon: <SettingsIcon /> },
35
+ * ]}
36
+ * />
37
+ * ```
38
+ */
39
+ export declare function SideRail(props: SideRailProps): JSX.Element;
@@ -0,0 +1,43 @@
1
+ import { JSX } from 'solid-js';
2
+ /** A single insertable item in a {@link SlashMenu}. */
3
+ export interface SlashItem {
4
+ /** Value passed to `onSelect` when this item is picked. */
5
+ value: string;
6
+ /** Primary text shown on the row. */
7
+ label: string;
8
+ /** Optional secondary text shown muted below the label. */
9
+ description?: string;
10
+ /** Optional leading icon. */
11
+ icon?: JSX.Element;
12
+ /** Extra terms matched against the query, in addition to label + description. */
13
+ keywords?: string[];
14
+ }
15
+ export interface SlashMenuProps {
16
+ items: SlashItem[];
17
+ /** Filter text — typically whatever the user typed after '/'. */
18
+ query: string;
19
+ /** Invoked with the picked item's `value` (click or Enter). */
20
+ onSelect: (value: string) => void;
21
+ /** Renders nothing when `false`. @default true */
22
+ open?: boolean;
23
+ class?: string;
24
+ }
25
+ /**
26
+ * Floating, keyboard-navigable menu for a "/"-triggered insert/command flow.
27
+ * Filters `items` against `query` (case-insensitive match over label +
28
+ * description + keywords), highlights an active row (↑/↓, wraps), and picks
29
+ * it on Enter or click. Purely presentational: the caller owns the '/'
30
+ * trigger, `open`, and `query` — this component only renders the filtered
31
+ * list and reports the pick.
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * const [query, setQuery] = createSignal('')
36
+ * <SlashMenu
37
+ * items={[{ value: 'heading', label: 'Heading', icon: <Heading size={16} /> }]}
38
+ * query={query()}
39
+ * onSelect={(value) => insertBlock(value)}
40
+ * />
41
+ * ```
42
+ */
43
+ export declare function SlashMenu(props: SlashMenuProps): JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface Space {
3
+ id: string;
4
+ label: string;
5
+ icon?: JSX.Element;
6
+ content: JSX.Element;
7
+ }
8
+ export interface SpacesProps {
9
+ spaces: Space[];
10
+ /** Controlled active space. Uncontrolled (internal signal, defaults to the first space) when omitted. */
11
+ activeId?: string;
12
+ onChange?: (id: string) => void;
13
+ class?: string;
14
+ }
15
+ /**
16
+ * Swipeable, switchable "context" containers — one space visible at a time,
17
+ * sliding horizontally between spaces, with a dot/pill indicator rail to jump
18
+ * directly to one. Drag/touch to swipe (the track follows the pointer and
19
+ * snaps on release past a threshold), same engine-free approach as Carousel.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * <Spaces
24
+ * spaces={[
25
+ * { id: 'personal', label: 'Personal', icon: <User class="h-4 w-4" />, content: <PersonalPane /> },
26
+ * { id: 'work', label: 'Work', icon: <Briefcase class="h-4 w-4" />, content: <WorkPane /> },
27
+ * ]}
28
+ * />
29
+ * ```
30
+ */
31
+ export declare function Spaces(props: SpacesProps): JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface StickyRevealProps {
3
+ children: JSX.Element;
4
+ /** Total scroll distance of the section (the taller it is, the longer the pin). CSS length, e.g. '200vh'. @default '180vh' */
5
+ height?: string;
6
+ class?: string;
7
+ }
8
+ /**
9
+ * A pinned hero/feature section: the outer element reserves `height` of
10
+ * scroll distance while the inner content sticks to the viewport and fades +
11
+ * rises in as the user scrolls through it. Needs real scroll room above/below
12
+ * to see the effect — drop it inside a page with enough surrounding content
13
+ * (or bump `height`) rather than in a short isolated container.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * <StickyReveal height="200vh">
18
+ * <Card class="p-10 text-center">
19
+ * <h2 class="text-3xl font-bold">Pinned moment</h2>
20
+ * </Card>
21
+ * </StickyReveal>
22
+ * ```
23
+ */
24
+ export declare function StickyReveal(props: StickyRevealProps): JSX.Element;
@@ -0,0 +1,40 @@
1
+ import { JSX } from 'solid-js';
2
+ /** A single row in a {@link TransactionFeed}. */
3
+ export interface Transaction {
4
+ id: string;
5
+ title: string;
6
+ subtitle?: string;
7
+ /** Negative = outgoing, positive (or zero) = incoming. */
8
+ amount: number;
9
+ /** ISO or display string. Required to group rows when `groupByDate` is set. */
10
+ date?: string;
11
+ icon?: JSX.Element;
12
+ }
13
+ export interface TransactionFeedProps {
14
+ transactions: Transaction[];
15
+ /** ISO 4217 currency code. Defaults to `'USD'`. */
16
+ currency?: string;
17
+ /** BCP 47 locale used for formatting. Defaults to `'en-US'`. */
18
+ locale?: string;
19
+ /** Group rows under date headers (uses each tx.date). @default false */
20
+ groupByDate?: boolean;
21
+ class?: string;
22
+ }
23
+ /**
24
+ * Dense vertical list of transactions/activity for finance dashboards. Each
25
+ * row shows an optional leading icon, a title + subtitle, and the amount
26
+ * formatted as currency — incoming (`amount >= 0`) in a success text tone,
27
+ * outgoing as `-$X` in the default foreground. Optionally groups rows under
28
+ * `text-xs uppercase` date headers via `groupByDate`.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * <TransactionFeed
33
+ * transactions={[
34
+ * { id: '1', title: 'Acme Corp', subtitle: 'Invoice #1042', amount: 1200, date: '2026-07-18' },
35
+ * { id: '2', title: 'Cloud Hosting', subtitle: 'Monthly plan', amount: -49.99, date: '2026-07-17' },
36
+ * ]}
37
+ * />
38
+ * ```
39
+ */
40
+ export declare function TransactionFeed(props: TransactionFeedProps): JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a4ui/core",
3
- "version": "0.26.0",
3
+ "version": "0.28.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",
package/preset.js CHANGED
@@ -48,6 +48,30 @@ const glass = plugin(({ addComponents, addVariant }) => {
48
48
  boxShadow: 'inset 0 1px 2px hsl(var(--shadow) / 0.18)',
49
49
  },
50
50
 
51
+ // ---- Refractive glass ----
52
+ // A heavier glass surface with a specular sheen + bright top edge, so it
53
+ // reads as light refracting through the material rather than a flat blur.
54
+ // Sits over the Aurora backdrop like `.card`; use for hero/feature panels.
55
+ '.glass-refractive': {
56
+ position: 'relative',
57
+ overflow: 'hidden',
58
+ background: 'hsl(var(--card) / 0.5)',
59
+ backdropFilter: 'blur(14px) saturate(180%)',
60
+ WebkitBackdropFilter: 'blur(14px) saturate(180%)',
61
+ border: '1px solid hsl(var(--foreground) / 0.14)',
62
+ borderRadius: 'var(--radius-xl, 1rem)',
63
+ boxShadow:
64
+ '0 1px 2px hsl(var(--shadow) / 0.06), 0 8px 24px hsl(var(--shadow) / 0.14), inset 0 1px 0 hsl(0 0% 100% / 0.18)',
65
+ '&::before': {
66
+ content: '""',
67
+ position: 'absolute',
68
+ inset: '0',
69
+ borderRadius: 'inherit',
70
+ background: 'linear-gradient(150deg, hsl(0 0% 100% / 0.16), transparent 42%)',
71
+ pointerEvents: 'none',
72
+ },
73
+ },
74
+
51
75
  // ---- Primary glass surface ----
52
76
  '.card': {
53
77
  position: 'relative',
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.26.0'
11
+ export const A4UI_VERSION = '0.28.0'
12
12
 
13
13
  // Helpers (src/lib) — generic, framework-level utilities.
14
14
  export { cn } from './lib/cn'
@@ -36,6 +36,7 @@ export { remeasureAfterLayout } from './lib/virtual'
36
36
  export { createOptimistic } from './lib/createOptimistic'
37
37
  export { startViewTransition } from './lib/viewTransition'
38
38
  export { EASE, SPRING, type EaseName, type SpringName } from './lib/easing'
39
+ export { createLinkedSelection, type LinkedSelection } from './lib/createLinkedSelection'
39
40
 
40
41
  // UI components (src/ui) — all 18 extracted. See CLAUDE.md.
41
42
  export { Accordion, type AccordionItem } from './ui/Accordion'
@@ -164,6 +165,18 @@ export { CodeTabs, type CodeTabsProps, type CodeTab } from './ui/CodeTabs'
164
165
  export { PillSearch, type PillSearchProps, type PillField } from './ui/PillSearch'
165
166
  export { CategoryStrip, type CategoryStripProps, type CategoryItem } from './ui/CategoryStrip'
166
167
  export { MasterDetail, type MasterDetailProps, type MasterDetailItem } from './ui/MasterDetail'
168
+ export { SlashMenu, type SlashMenuProps, type SlashItem } from './ui/SlashMenu'
169
+ export { DataView, type DataViewProps, type DataViewColumn, type DataViewMode } from './ui/DataView'
170
+
171
+ // Phase 3 — scroll storytelling, finance, spaces/rail.
172
+ export { ScrollScene, type ScrollSceneProps } from './ui/ScrollScene'
173
+ export { StickyReveal, type StickyRevealProps } from './ui/StickyReveal'
174
+ export { BalanceCard, type BalanceCardProps } from './ui/BalanceCard'
175
+ export { TransactionFeed, type TransactionFeedProps, type Transaction } from './ui/TransactionFeed'
176
+ export { KpiBlock, type KpiBlockProps } from './ui/KpiBlock'
177
+ export { MoneyActionButton, type MoneyActionButtonProps, type MoneyActionKind } from './ui/MoneyActionButton'
178
+ export { Spaces, type SpacesProps, type Space } from './ui/Spaces'
179
+ export { SideRail, type SideRailProps, type SideRailItem } from './ui/SideRail'
167
180
 
168
181
  // Motion components (src/ui) — animation primitives built on the `motion` engine
169
182
  // (adapted from motion.dev examples). All tree-shakeable and reduced-motion aware;
@@ -8,7 +8,7 @@
8
8
  // Usage: render it once at the top of your layout and keep the page root's
9
9
  // background transparent (don't put `bg-background` on the root) so the Aurora
10
10
  // shows through — the component paints the base background itself.
11
- import { For, onCleanup, onMount, type JSX } from 'solid-js'
11
+ import { For, onCleanup, onMount, Show, type JSX } from 'solid-js'
12
12
 
13
13
  import { cn } from '../lib/cn'
14
14
  import { motionReduced } from '../lib/motion'
@@ -24,9 +24,24 @@ export interface AuroraProps {
24
24
  * on any `.glow-edge` cards). Reduced-motion aware (off when reduced). @default true
25
25
  */
26
26
  pointerGlow?: boolean
27
+ /**
28
+ * Backdrop style: `'blobs'` (soft blurred color blobs) or `'mesh'` (a
29
+ * multi-point gradient mesh). Both are theme-tinted and drift when `animated`.
30
+ * @default 'blobs'
31
+ */
32
+ variant?: 'blobs' | 'mesh'
27
33
  class?: string
28
34
  }
29
35
 
36
+ // Mesh variant — several token-tinted radial gradients composited into one
37
+ // backdrop (positions chosen to fill the corners without a hard seam).
38
+ const MESH_STOPS = [
39
+ { at: '18% 22%', token: '--primary', a: 1 },
40
+ { at: '82% 26%', token: '--accent', a: 0.9 },
41
+ { at: '72% 82%', token: '--primary', a: 0.8 },
42
+ { at: '14% 78%', token: '--accent', a: 0.85 },
43
+ ] as const
44
+
30
45
  // Static class strings (scanned by Tailwind). token = which theme color; a =
31
46
  // per-blob opacity weight, multiplied by `intensity`.
32
47
  const BLOBS = [
@@ -53,6 +68,11 @@ const BLOBS = [
53
68
  */
54
69
  export function Aurora(props: AuroraProps): JSX.Element {
55
70
  const intensity = (): number => props.intensity ?? 0.45
71
+ const meshBg = (): string =>
72
+ MESH_STOPS.map(
73
+ (s) =>
74
+ `radial-gradient(45% 45% at ${s.at}, hsl(var(${s.token}) / ${(intensity() * s.a).toFixed(2)}), transparent 70%)`,
75
+ ).join(', ')
56
76
  let root: HTMLDivElement | undefined
57
77
 
58
78
  onMount(() => {
@@ -69,16 +89,26 @@ export function Aurora(props: AuroraProps): JSX.Element {
69
89
  aria-hidden="true"
70
90
  class={cn('pointer-events-none fixed inset-0 -z-10 overflow-hidden bg-background', props.class)}
71
91
  >
72
- <For each={BLOBS}>
73
- {(b) => (
74
- <div
75
- class={cn('absolute rounded-full blur-3xl', b.pos, b.size, props.animated && 'aurora-drift')}
76
- style={{
77
- background: `radial-gradient(circle, hsl(var(${b.token}) / ${(intensity() * b.a).toFixed(2)}), transparent 70%)`,
78
- }}
79
- />
80
- )}
81
- </For>
92
+ <Show
93
+ when={props.variant === 'mesh'}
94
+ fallback={
95
+ <For each={BLOBS}>
96
+ {(b) => (
97
+ <div
98
+ class={cn('absolute rounded-full blur-3xl', b.pos, b.size, props.animated && 'aurora-drift')}
99
+ style={{
100
+ background: `radial-gradient(circle, hsl(var(${b.token}) / ${(intensity() * b.a).toFixed(2)}), transparent 70%)`,
101
+ }}
102
+ />
103
+ )}
104
+ </For>
105
+ }
106
+ >
107
+ <div
108
+ class={cn('absolute -inset-[15%] blur-2xl', props.animated && 'aurora-drift')}
109
+ style={{ background: meshBg() }}
110
+ />
111
+ </Show>
82
112
  {/* Soft glow that follows the cursor across the backdrop (positioned by
83
113
  bindPointerFx via left/top on pointermove). */}
84
114
  <div
@@ -0,0 +1,60 @@
1
+ // Shared hover/selection state for two views of the same collection (e.g. a
2
+ // map and a list) so interacting with one highlights the other.
3
+ import { createSignal } from 'solid-js'
4
+
5
+ export interface LinkedSelection<Id> {
6
+ hoveredId: () => Id | undefined
7
+ selectedId: () => Id | undefined
8
+ setHovered: (id: Id | undefined) => void
9
+ setSelected: (id: Id | undefined) => void
10
+ isHovered: (id: Id) => boolean
11
+ isSelected: (id: Id) => boolean
12
+ /** Convenience props to spread onto an element for a given id (onPointerEnter/Leave + onClick). */
13
+ itemProps: (id: Id) => { onPointerEnter: () => void; onPointerLeave: () => void; onClick: () => void }
14
+ }
15
+
16
+ /**
17
+ * Shared hovered/selected state for two synchronized views of the same
18
+ * collection (a map and a list, a chart and a table, …): hover a list row to
19
+ * highlight the map pin, and vice versa.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * const selection = createLinkedSelection<string>()
24
+ * return (
25
+ * <For each={items}>
26
+ * {(item) => <ListRow class={selection.isSelected(item.id) ? 'active' : ''} {...selection.itemProps(item.id)} />}
27
+ * </For>
28
+ * )
29
+ * ```
30
+ */
31
+ export function createLinkedSelection<Id = string>(initialSelected?: Id): LinkedSelection<Id> {
32
+ const [hoveredId, setHoveredId] = createSignal<Id | undefined>(undefined)
33
+ const [selectedId, setSelectedId] = createSignal<Id | undefined>(initialSelected)
34
+
35
+ function setHovered(id: Id | undefined) {
36
+ setHoveredId(() => id)
37
+ }
38
+
39
+ function setSelected(id: Id | undefined) {
40
+ setSelectedId(() => id)
41
+ }
42
+
43
+ function isHovered(id: Id): boolean {
44
+ return hoveredId() === id
45
+ }
46
+
47
+ function isSelected(id: Id): boolean {
48
+ return selectedId() === id
49
+ }
50
+
51
+ function itemProps(id: Id) {
52
+ return {
53
+ onPointerEnter: () => setHovered(id),
54
+ onPointerLeave: () => setHovered(undefined),
55
+ onClick: () => setSelected(id),
56
+ }
57
+ }
58
+
59
+ return { hoveredId, selectedId, setHovered, setSelected, isHovered, isSelected, itemProps }
60
+ }
@@ -0,0 +1,68 @@
1
+ // Balance-first summary card for finance dashboards: a label, a large
2
+ // formatted balance, and an optional up/down delta chip.
3
+ import type { JSX } from 'solid-js'
4
+ import { Show } from 'solid-js'
5
+ import { TrendingDown, TrendingUp } from 'lucide-solid'
6
+
7
+ import { cn } from '../lib/cn'
8
+ import { Badge } from './Badge'
9
+ import { Card, CardContent } from './Card'
10
+
11
+ export interface BalanceCardProps {
12
+ label: string
13
+ amount: number
14
+ /** ISO 4217 currency code. Defaults to `'USD'`. */
15
+ currency?: string
16
+ /** BCP 47 locale used for formatting. Defaults to `'en-US'`. */
17
+ locale?: string
18
+ /** Period-over-period change as a fraction (0.042 = +4.2%). Shows an up/down chip. */
19
+ delta?: number
20
+ /** Small line under the balance (e.g. "Available" / account number). */
21
+ sub?: JSX.Element
22
+ class?: string
23
+ }
24
+
25
+ /**
26
+ * Balance-first summary card: label, large formatted amount, and an optional
27
+ * signed-percent delta chip (up/down colored via {@link Badge} tones).
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * <BalanceCard label="Total balance" amount={128430.52} delta={0.042} />
32
+ * ```
33
+ */
34
+ export function BalanceCard(props: BalanceCardProps): JSX.Element {
35
+ const format = () =>
36
+ new Intl.NumberFormat(props.locale ?? 'en-US', {
37
+ style: 'currency',
38
+ currency: props.currency ?? 'USD',
39
+ }).format(props.amount)
40
+
41
+ const isPositive = () => (props.delta ?? 0) >= 0
42
+
43
+ const deltaLabel = () => {
44
+ const pct = Math.abs(props.delta as number) * 100
45
+ const rounded = Math.round(pct * 10) / 10
46
+ return `${isPositive() ? '+' : '-'}${rounded}%`
47
+ }
48
+
49
+ return (
50
+ <Card glass class={cn('inline-block', props.class)}>
51
+ <CardContent class="flex flex-col gap-2">
52
+ <span class="text-sm text-muted-foreground">{props.label}</span>
53
+ <div class="flex items-center gap-3">
54
+ <span class="text-3xl font-bold text-foreground">{format()}</span>
55
+ <Show when={props.delta !== undefined}>
56
+ <Badge tone={isPositive() ? 'success' : 'danger'}>
57
+ <Show when={isPositive()} fallback={<TrendingDown class="h-3 w-3" aria-hidden="true" />}>
58
+ <TrendingUp class="h-3 w-3" aria-hidden="true" />
59
+ </Show>
60
+ {deltaLabel()}
61
+ </Badge>
62
+ </Show>
63
+ </div>
64
+ <Show when={props.sub}>{props.sub}</Show>
65
+ </CardContent>
66
+ </Card>
67
+ )
68
+ }