@a4ui/core 0.33.0 → 0.35.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.
Files changed (58) hide show
  1. package/dist/elements.css +299 -0
  2. package/dist/full.css +299 -0
  3. package/dist/index.d.ts +27 -1
  4. package/dist/index.js +9140 -5065
  5. package/dist/ui/ActivityFeed.d.ts +49 -0
  6. package/dist/ui/AudioWaveform.d.ts +22 -0
  7. package/dist/ui/AvailabilityPicker.d.ts +35 -0
  8. package/dist/ui/CodeEditor.d.ts +25 -0
  9. package/dist/ui/CouponField.d.ts +33 -0
  10. package/dist/ui/EmojiPicker.d.ts +18 -0
  11. package/dist/ui/EventScheduler.d.ts +41 -0
  12. package/dist/ui/FollowingPointer.d.ts +24 -0
  13. package/dist/ui/GanttChart.d.ts +37 -0
  14. package/dist/ui/InteractiveMap.d.ts +50 -0
  15. package/dist/ui/JsonViewer.d.ts +24 -0
  16. package/dist/ui/Kanban.d.ts +49 -0
  17. package/dist/ui/Lamp.d.ts +22 -0
  18. package/dist/ui/Lightbox.d.ts +41 -0
  19. package/dist/ui/LocationPicker.d.ts +25 -0
  20. package/dist/ui/MaskedInput.d.ts +26 -0
  21. package/dist/ui/OnboardingChecklist.d.ts +51 -0
  22. package/dist/ui/OtpInput.d.ts +29 -0
  23. package/dist/ui/PivotTable.d.ts +37 -0
  24. package/dist/ui/PresenceAvatars.d.ts +54 -0
  25. package/dist/ui/QueryBuilder.d.ts +56 -0
  26. package/dist/ui/SheetSnap.d.ts +28 -0
  27. package/dist/ui/SignaturePad.d.ts +19 -0
  28. package/dist/ui/SpreadsheetGrid.d.ts +27 -0
  29. package/dist/ui/TreeTable.d.ts +52 -0
  30. package/dist/ui/VideoPlayerShell.d.ts +19 -0
  31. package/package.json +1 -1
  32. package/src/index.ts +60 -1
  33. package/src/ui/ActivityFeed.tsx +178 -0
  34. package/src/ui/AudioWaveform.tsx +190 -0
  35. package/src/ui/AvailabilityPicker.tsx +163 -0
  36. package/src/ui/CodeEditor.tsx +277 -0
  37. package/src/ui/CouponField.tsx +120 -0
  38. package/src/ui/EmojiPicker.tsx +424 -0
  39. package/src/ui/EventScheduler.tsx +280 -0
  40. package/src/ui/FollowingPointer.tsx +112 -0
  41. package/src/ui/GanttChart.tsx +283 -0
  42. package/src/ui/InteractiveMap.tsx +349 -0
  43. package/src/ui/JsonViewer.tsx +189 -0
  44. package/src/ui/Kanban.tsx +250 -0
  45. package/src/ui/Lamp.tsx +88 -0
  46. package/src/ui/Lightbox.tsx +261 -0
  47. package/src/ui/LocationPicker.tsx +96 -0
  48. package/src/ui/MaskedInput.tsx +108 -0
  49. package/src/ui/OnboardingChecklist.tsx +163 -0
  50. package/src/ui/OtpInput.tsx +153 -0
  51. package/src/ui/PivotTable.tsx +0 -0
  52. package/src/ui/PresenceAvatars.tsx +142 -0
  53. package/src/ui/QueryBuilder.tsx +280 -0
  54. package/src/ui/SheetSnap.tsx +264 -0
  55. package/src/ui/SignaturePad.tsx +221 -0
  56. package/src/ui/SpreadsheetGrid.tsx +304 -0
  57. package/src/ui/TreeTable.tsx +172 -0
  58. package/src/ui/VideoPlayerShell.tsx +282 -0
@@ -0,0 +1,54 @@
1
+ import { JSX } from 'solid-js';
2
+ /** A user currently present/active in a collaborative session. */
3
+ export interface PresenceUser {
4
+ id: string;
5
+ name: string;
6
+ avatar?: string;
7
+ /** Accent for this user's remote cursor label. Falls back to the primary token. */
8
+ color?: string;
9
+ }
10
+ /** A remote user's pointer position, as fractions of the shared canvas. */
11
+ export interface RemoteCursor {
12
+ userId: string;
13
+ /** 0..1 fraction of the container's width. */
14
+ x: number;
15
+ /** 0..1 fraction of the container's height. */
16
+ y: number;
17
+ }
18
+ export interface PresenceAvatarsProps {
19
+ users: PresenceUser[];
20
+ /** Max avatars shown before collapsing the rest into `+N`. @default 4 */
21
+ max?: number;
22
+ /**
23
+ * Remote cursors to render. Positioned absolutely against the nearest
24
+ * `relative` ancestor, so the caller wraps its canvas/surface with
25
+ * `class="relative"` for the cursors to anchor to — this component itself
26
+ * does not create that positioning context.
27
+ */
28
+ cursors?: RemoteCursor[];
29
+ class?: string;
30
+ }
31
+ /**
32
+ * Stacked avatar group of users active in a collaborative session, each with
33
+ * a small online dot and a name tooltip; overflow past `max` (default 4)
34
+ * collapses into a trailing `+N` chip, mirroring {@link AvatarGroup}. When
35
+ * `cursors` is given, also renders labeled remote cursors — a small arrow
36
+ * plus a name chip tinted with the user's `color` — positioned at `x`/`y`
37
+ * fractions of the nearest `relative` ancestor and animated smoothly as
38
+ * positions update.
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * <div class="relative h-64 w-full overflow-hidden rounded-lg border">
43
+ * <PresenceAvatars
44
+ * class="absolute right-3 top-3"
45
+ * users={[
46
+ * { id: 'u1', name: 'Ada Lovelace' },
47
+ * { id: 'u2', name: 'Grace Hopper', color: 'hsl(280 80% 60%)' },
48
+ * ]}
49
+ * cursors={[{ userId: 'u2', x: 0.42, y: 0.6 }]}
50
+ * />
51
+ * </div>
52
+ * ```
53
+ */
54
+ export declare function PresenceAvatars(props: PresenceAvatarsProps): JSX.Element;
@@ -0,0 +1,56 @@
1
+ import { JSX } from 'solid-js';
2
+ /** A field the query builder can filter on. */
3
+ export interface QueryField {
4
+ /** Value stored in a rule's `field`; must be unique across `fields`. */
5
+ name: string;
6
+ /** Label shown in the field `<Select>`. */
7
+ label: string;
8
+ /** Determines which operators are offered and how the value is entered. Defaults to `'text'`. */
9
+ type?: 'text' | 'number' | 'select';
10
+ /** Options for the value `<Select>` when `type` is `'select'`. */
11
+ options?: string[];
12
+ }
13
+ /** A single leaf condition: `field operator value`. */
14
+ export type QueryRule = {
15
+ field: string;
16
+ operator: string;
17
+ value: string;
18
+ };
19
+ /** A group of rules and/or nested groups, combined with `and`/`or`. */
20
+ export type QueryGroup = {
21
+ combinator: 'and' | 'or';
22
+ rules: (QueryRule | QueryGroup)[];
23
+ };
24
+ export interface QueryBuilderProps {
25
+ /** Fields available to filter on; drives the per-field operator and value-input set. */
26
+ fields: QueryField[];
27
+ /** Controlled tree. Omit to manage state internally, seeded with one empty rule. */
28
+ value?: QueryGroup;
29
+ /** Called with the full, updated tree on every edit. */
30
+ onChange?: (group: QueryGroup) => void;
31
+ class?: string;
32
+ }
33
+ /**
34
+ * Nested AND/OR rule builder: each group has a combinator toggle plus "+ Rule"
35
+ * / "+ Group" buttons, and rules/groups can be removed individually. Groups
36
+ * nest recursively, indented with a left border. Controlled via `value` +
37
+ * `onChange`, or uncontrolled (seeded with one empty rule) when `value` is
38
+ * omitted.
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * const [query, setQuery] = createSignal<QueryGroup>({
43
+ * combinator: 'and',
44
+ * rules: [{ field: 'status', operator: 'is', value: 'open' }],
45
+ * })
46
+ * <QueryBuilder
47
+ * fields={[
48
+ * { name: 'status', label: 'Status', type: 'select', options: ['open', 'closed'] },
49
+ * { name: 'age', label: 'Age', type: 'number' },
50
+ * ]}
51
+ * value={query()}
52
+ * onChange={setQuery}
53
+ * />
54
+ * ```
55
+ */
56
+ export declare function QueryBuilder(props: QueryBuilderProps): JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface SheetSnapProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ /** Fractions of viewport height, ascending, e.g. `[0.4, 0.9]`. @default [0.5, 0.9] */
6
+ snapPoints?: number[];
7
+ /** Index into `snapPoints` to open at. @default 0 */
8
+ defaultSnap?: number;
9
+ children: JSX.Element;
10
+ class?: string;
11
+ }
12
+ /**
13
+ * A draggable bottom sheet that rests at one of several height "snap points"
14
+ * (fractions of viewport height) instead of just open/closed. Drag the handle
15
+ * to resize with the finger; releasing snaps to the nearest point, biased by
16
+ * flick velocity (a fast swipe jumps a level), and a hard downward flick/drag
17
+ * past the lowest point dismisses. Falls back to an instant show/hide at
18
+ * `defaultSnap` — no drag physics — under reduced motion.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * const [open, setOpen] = createSignal(false)
23
+ * <SheetSnap open={open()} onOpenChange={setOpen} snapPoints={[0.4, 0.9]}>
24
+ * <p>Sheet content…</p>
25
+ * </SheetSnap>
26
+ * ```
27
+ */
28
+ export declare function SheetSnap(props: SheetSnapProps): JSX.Element;
@@ -0,0 +1,19 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface SignaturePadProps {
3
+ /** Called with a PNG data URL after each completed stroke, or `null` once the pad is empty (cleared/undone to nothing). */
4
+ onChange?: (dataUrl: string | null) => void;
5
+ /** Canvas height in CSS pixels. @default 200 */
6
+ height?: number;
7
+ class?: string;
8
+ }
9
+ /**
10
+ * A signature capture pad: draw with mouse, pen, or touch on a DPR-aware
11
+ * `<canvas>`, with Clear and Undo controls. Strokes are kept as point lists so
12
+ * Undo can fully redraw the remaining ones rather than trying to erase pixels.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <SignaturePad height={220} onChange={(dataUrl) => setSignature(dataUrl)} />
17
+ * ```
18
+ */
19
+ export declare function SignaturePad(props: SignaturePadProps): JSX.Element;
@@ -0,0 +1,27 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface SpreadsheetGridProps {
3
+ /** Number of rows in the grid (fixed for the component's lifetime). */
4
+ rows: number;
5
+ /** Number of columns in the grid (fixed for the component's lifetime). */
6
+ cols: number;
7
+ /** Initial cell values, seeded once on mount (missing cells default to `''`). */
8
+ data?: string[][];
9
+ /** Called with the full `rows×cols` matrix after any edit or paste. */
10
+ onChange?: (data: string[][]) => void;
11
+ /** Column header labels; defaults to `A, B, C, …, Z, AA, AB, …`. */
12
+ columnHeaders?: string[];
13
+ class?: string;
14
+ }
15
+ /**
16
+ * Excel-like editable grid. Arrow keys move the active cell; typing or Enter
17
+ * opens an inline editor that commits on Enter/blur and cancels on Escape.
18
+ * Shift+click / Shift+arrow extend a rectangular selection (Ctrl/Cmd+C copies
19
+ * it as TSV, Ctrl/Cmd+V pastes TSV starting at the active cell).
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * const [sheet, setSheet] = createSignal<string[][]>([])
24
+ * <SpreadsheetGrid rows={10} cols={6} onChange={setSheet} />
25
+ * ```
26
+ */
27
+ export declare function SpreadsheetGrid(props: SpreadsheetGridProps): JSX.Element;
@@ -0,0 +1,52 @@
1
+ import { JSX } from 'solid-js';
2
+ /** A column definition for {@link TreeTable}. */
3
+ export interface TreeTableColumn<T> {
4
+ /** Row data property this column reads when `cell` is omitted. */
5
+ key: string;
6
+ /** Column heading content. */
7
+ header: JSX.Element;
8
+ /** Custom cell renderer; falls back to `String(row[key])` when omitted. */
9
+ cell?: (row: T) => JSX.Element;
10
+ /** Right-align the column (numbers, totals). */
11
+ align?: 'left' | 'right';
12
+ }
13
+ /** A single node in a {@link TreeTable}; nodes may nest via `children`. */
14
+ export interface TreeTableRow<T> {
15
+ /** Unique identifier; used to track expanded state. */
16
+ id: string;
17
+ /** Row payload passed to each column's `cell` renderer. */
18
+ data: T;
19
+ /** Child rows revealed when this row is expanded. */
20
+ children?: TreeTableRow<T>[];
21
+ }
22
+ export interface TreeTableProps<T> {
23
+ columns: TreeTableColumn<T>[];
24
+ rows: TreeTableRow<T>[];
25
+ /** Expand every row with children on first render. Defaults to false. */
26
+ defaultExpanded?: boolean;
27
+ class?: string;
28
+ }
29
+ /**
30
+ * Hierarchical table: rows may carry `children`, revealed by clicking the
31
+ * chevron in the first column. Depth is shown via indentation; expanded state
32
+ * is tracked internally, seeded fully expanded when `defaultExpanded` is set.
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * <TreeTable
37
+ * defaultExpanded
38
+ * columns={[
39
+ * { key: 'name', header: 'Name' },
40
+ * { key: 'size', header: 'Size', align: 'right', cell: (row) => `${row.size} KB` },
41
+ * ]}
42
+ * rows={[
43
+ * {
44
+ * id: 'src',
45
+ * data: { name: 'src', size: 0 },
46
+ * children: [{ id: 'index', data: { name: 'index.ts', size: 2 } }],
47
+ * },
48
+ * ]}
49
+ * />
50
+ * ```
51
+ */
52
+ export declare function TreeTable<T>(props: TreeTableProps<T>): JSX.Element;
@@ -0,0 +1,19 @@
1
+ import { JSX } from 'solid-js';
2
+ export interface VideoPlayerShellProps {
3
+ src: string;
4
+ poster?: string;
5
+ class?: string;
6
+ }
7
+ /**
8
+ * Native `<video>` wrapped in a glass shell with fully custom controls: a big
9
+ * center play/pause, a bottom bar (play/pause, elapsed/duration, a scrub
10
+ * range, mute toggle, fullscreen, Picture-in-Picture), and a keyboard map
11
+ * (Space = play/pause, ArrowLeft/Right = seek ±5s). Controls auto-hide while
12
+ * playing and reappear on pointer movement.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <VideoPlayerShell src="/media/demo.mp4" poster="/media/demo-poster.jpg" />
17
+ * ```
18
+ */
19
+ export declare function VideoPlayerShell(props: VideoPlayerShellProps): JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a4ui/core",
3
- "version": "0.33.0",
3
+ "version": "0.35.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/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.33.0'
11
+ export const A4UI_VERSION = '0.35.0'
12
12
 
13
13
  // Helpers (src/lib) — generic, framework-level utilities.
14
14
  export { cn } from './lib/cn'
@@ -209,6 +209,65 @@ export { Globe, type GlobeMarker, type GlobeArc, type GlobeProps } from './ui/Gl
209
209
  export { WorldMap, type WorldMapPoint, type WorldMapConnection, type WorldMapProps } from './ui/WorldMap'
210
210
  export { Confetti, fireConfetti, type ConfettiProps } from './ui/Confetti'
211
211
  export { CursorTrail, type CursorTrailProps } from './ui/CursorTrail'
212
+
213
+ // Productivity data views.
214
+ export { Kanban, type KanbanCard, type KanbanColumn, type KanbanProps } from './ui/Kanban'
215
+ export { GanttChart, type GanttTask, type GanttChartProps } from './ui/GanttChart'
216
+ export { TreeTable, type TreeTableColumn, type TreeTableRow, type TreeTableProps } from './ui/TreeTable'
217
+ export { PivotTable, type PivotDatum, type PivotTableProps } from './ui/PivotTable'
218
+
219
+ // Onboarding + commerce.
220
+ export {
221
+ OnboardingChecklist,
222
+ type OnboardingStep,
223
+ type OnboardingChecklistProps,
224
+ } from './ui/OnboardingChecklist'
225
+ export { CouponField, type CouponFieldProps } from './ui/CouponField'
226
+
227
+ // Media.
228
+ export { Lightbox, type LightboxImage, type LightboxProps } from './ui/Lightbox'
229
+ export { VideoPlayerShell, type VideoPlayerShellProps } from './ui/VideoPlayerShell'
230
+ export { AudioWaveform, type AudioWaveformProps } from './ui/AudioWaveform'
231
+
232
+ // Spatial + interaction.
233
+ export { Lamp, type LampProps } from './ui/Lamp'
234
+ export { FollowingPointer, type FollowingPointerProps } from './ui/FollowingPointer'
235
+ export { SheetSnap, type SheetSnapProps } from './ui/SheetSnap'
236
+
237
+ // Advanced form inputs.
238
+ export { OtpInput, type OtpInputProps } from './ui/OtpInput'
239
+ export { MaskedInput, type MaskedInputProps } from './ui/MaskedInput'
240
+ export { CodeEditor, type CodeEditorProps } from './ui/CodeEditor'
241
+ export { SignaturePad, type SignaturePadProps } from './ui/SignaturePad'
242
+ export { EmojiPicker, type EmojiPickerProps } from './ui/EmojiPicker'
243
+
244
+ // Scheduling.
245
+ export { EventScheduler, type SchedulerEvent, type EventSchedulerProps } from './ui/EventScheduler'
246
+ export { AvailabilityPicker, type AvailabilityPickerProps } from './ui/AvailabilityPicker'
247
+
248
+ // Geo.
249
+ export { InteractiveMap, type MapMarker, type InteractiveMapProps } from './ui/InteractiveMap'
250
+ export { LocationPicker, type LocationValue, type LocationPickerProps } from './ui/LocationPicker'
251
+
252
+ // Dev / data tooling.
253
+ export {
254
+ QueryBuilder,
255
+ type QueryField,
256
+ type QueryRule,
257
+ type QueryGroup,
258
+ type QueryBuilderProps,
259
+ } from './ui/QueryBuilder'
260
+ export { JsonViewer, type JsonViewerProps } from './ui/JsonViewer'
261
+ export { SpreadsheetGrid, type SpreadsheetGridProps } from './ui/SpreadsheetGrid'
262
+
263
+ // Collaboration.
264
+ export {
265
+ PresenceAvatars,
266
+ type PresenceUser,
267
+ type RemoteCursor,
268
+ type PresenceAvatarsProps,
269
+ } from './ui/PresenceAvatars'
270
+ export { ActivityFeed, type ActivityItem, type ActivityFeedProps } from './ui/ActivityFeed'
212
271
  export {
213
272
  CopyButton,
214
273
  type CopyButtonProps,
@@ -0,0 +1,178 @@
1
+ // Chronological audit-trail list, newest first, grouped by day with a
2
+ // Timeline-style connector line down the left. Distinct from
3
+ // TransactionFeed (dense finance rows, no day grouping/connector) — this is
4
+ // generic activity/audit history ("who did what, when").
5
+ import { Activity } from 'lucide-solid'
6
+ import type { JSX } from 'solid-js'
7
+ import { For, Show } from 'solid-js'
8
+
9
+ import { cn } from '../lib/cn'
10
+ import { Avatar } from './Avatar'
11
+
12
+ /** A single audit-trail entry rendered by {@link ActivityFeed}. */
13
+ export interface ActivityItem {
14
+ id: string
15
+ /** Name of the user/system that performed the action. */
16
+ actor: string
17
+ /** What happened, rendered right after `actor` (e.g. `"commented on Invoice #42"`). */
18
+ action: JSX.Element
19
+ /** Leading icon shown when `avatar` is omitted. Defaults to a generic activity icon. */
20
+ icon?: JSX.Element
21
+ /** ISO 8601 timestamp of the event. */
22
+ timestamp: string
23
+ avatar?: string
24
+ }
25
+
26
+ export interface ActivityFeedProps {
27
+ items: ActivityItem[]
28
+ class?: string
29
+ }
30
+
31
+ interface ActivityGroup {
32
+ label: string
33
+ items: ActivityItem[]
34
+ }
35
+
36
+ const DAY_MS = 86_400_000
37
+
38
+ function startOfDay(date: Date): number {
39
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()
40
+ }
41
+
42
+ /** `"Today"` / `"Yesterday"` / a locale date, based on calendar-day distance from now. */
43
+ function dayLabel(timestamp: string): string {
44
+ const date = new Date(timestamp)
45
+ const diffDays = Math.round((startOfDay(new Date()) - startOfDay(date)) / DAY_MS)
46
+ if (diffDays === 0) return 'Today'
47
+ if (diffDays === 1) return 'Yesterday'
48
+ return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })
49
+ }
50
+
51
+ /** Compact relative time (`"2h ago"`, `"just now"`) computed from an ISO timestamp. */
52
+ function relativeTime(timestamp: string): string {
53
+ const diffSeconds = Math.round((Date.now() - new Date(timestamp).getTime()) / 1000)
54
+ if (diffSeconds < 60) return 'just now'
55
+ const diffMinutes = Math.round(diffSeconds / 60)
56
+ if (diffMinutes < 60) return `${diffMinutes}m ago`
57
+ const diffHours = Math.round(diffMinutes / 60)
58
+ if (diffHours < 24) return `${diffHours}h ago`
59
+ const diffDays = Math.round(diffHours / 24)
60
+ if (diffDays < 7) return `${diffDays}d ago`
61
+ const diffWeeks = Math.round(diffDays / 7)
62
+ if (diffWeeks < 5) return `${diffWeeks}w ago`
63
+ const diffMonths = Math.round(diffDays / 30)
64
+ if (diffMonths < 12) return `${diffMonths}mo ago`
65
+ return `${Math.round(diffDays / 365)}y ago`
66
+ }
67
+
68
+ function initials(name: string): string {
69
+ return name
70
+ .split(' ')
71
+ .filter(Boolean)
72
+ .slice(0, 2)
73
+ .map((part) => part[0]?.toUpperCase())
74
+ .join('')
75
+ }
76
+
77
+ /** Groups items newest-first by calendar day, preserving first-seen group order. */
78
+ function groupByDay(items: ActivityItem[]): ActivityGroup[] {
79
+ const sorted = [...items].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
80
+ const groups: ActivityGroup[] = []
81
+ const byLabel = new Map<string, ActivityGroup>()
82
+ for (const item of sorted) {
83
+ const label = dayLabel(item.timestamp)
84
+ let group = byLabel.get(label)
85
+ if (!group) {
86
+ group = { label, items: [] }
87
+ byLabel.set(label, group)
88
+ groups.push(group)
89
+ }
90
+ group.items.push(item)
91
+ }
92
+ return groups
93
+ }
94
+
95
+ /**
96
+ * Chronological audit-trail list (newest first), grouped under subtle day
97
+ * headers (`"Today"` / `"Yesterday"` / a locale date) with a continuous
98
+ * connector line down the left, in the same spirit as {@link Timeline}. Each
99
+ * row shows an avatar (or a fallback icon), `"{actor} {action}"`, and a
100
+ * relative timestamp whose `title` carries the absolute time. Distinct from
101
+ * {@link TransactionFeed}: this is generic activity/audit history, not a
102
+ * financial ledger.
103
+ *
104
+ * @example
105
+ * ```tsx
106
+ * <ActivityFeed
107
+ * items={[
108
+ * {
109
+ * id: '1',
110
+ * actor: 'Ada Lovelace',
111
+ * action: <>commented on <strong>Invoice #42</strong></>,
112
+ * timestamp: '2026-07-21T09:15:00Z',
113
+ * avatar: ada.avatarUrl,
114
+ * },
115
+ * {
116
+ * id: '2',
117
+ * actor: 'System',
118
+ * action: 'archived the project',
119
+ * timestamp: '2026-07-20T18:02:00Z',
120
+ * },
121
+ * ]}
122
+ * />
123
+ * ```
124
+ */
125
+ export function ActivityFeed(props: ActivityFeedProps): JSX.Element {
126
+ const groups = () => groupByDay(props.items)
127
+
128
+ return (
129
+ <div class={cn('flex flex-col gap-6', props.class)}>
130
+ <For each={groups()}>
131
+ {(group) => (
132
+ <div>
133
+ <p class="mb-3 text-xs font-medium uppercase tracking-wide text-muted-foreground">
134
+ {group.label}
135
+ </p>
136
+ <ol class="relative flex flex-col gap-5">
137
+ <For each={group.items}>
138
+ {(item, index) => (
139
+ <li class="relative flex gap-3 pl-1">
140
+ <Show when={index() < group.items.length - 1}>
141
+ <span
142
+ aria-hidden="true"
143
+ class="absolute left-[1.375rem] top-9 -bottom-5 w-px -translate-x-1/2 bg-border"
144
+ />
145
+ </Show>
146
+ <Show
147
+ when={item.avatar}
148
+ fallback={
149
+ <span class="relative z-[1] grid h-9 w-9 shrink-0 place-items-center rounded-full bg-muted text-muted-foreground">
150
+ {item.icon ?? <Activity class="h-4 w-4" />}
151
+ </span>
152
+ }
153
+ >
154
+ <span class="relative z-[1] shrink-0">
155
+ <Avatar src={item.avatar} alt={item.actor} fallback={initials(item.actor)} />
156
+ </span>
157
+ </Show>
158
+ <div class="min-w-0 flex-1 pt-1.5">
159
+ <p class="text-sm text-foreground">
160
+ <span class="font-medium">{item.actor}</span> {item.action}
161
+ </p>
162
+ <p
163
+ class="mt-0.5 text-xs text-muted-foreground"
164
+ title={new Date(item.timestamp).toLocaleString()}
165
+ >
166
+ {relativeTime(item.timestamp)}
167
+ </p>
168
+ </div>
169
+ </li>
170
+ )}
171
+ </For>
172
+ </ol>
173
+ </div>
174
+ )}
175
+ </For>
176
+ </div>
177
+ )
178
+ }