@a4ui/core 0.33.0 → 0.34.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/elements.css +183 -0
- package/dist/full.css +183 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +5857 -4509
- package/dist/ui/AudioWaveform.d.ts +22 -0
- package/dist/ui/CouponField.d.ts +33 -0
- package/dist/ui/FollowingPointer.d.ts +24 -0
- package/dist/ui/GanttChart.d.ts +37 -0
- package/dist/ui/Kanban.d.ts +49 -0
- package/dist/ui/Lamp.d.ts +22 -0
- package/dist/ui/Lightbox.d.ts +41 -0
- package/dist/ui/OnboardingChecklist.d.ts +51 -0
- package/dist/ui/PivotTable.d.ts +37 -0
- package/dist/ui/SheetSnap.d.ts +28 -0
- package/dist/ui/TreeTable.d.ts +52 -0
- package/dist/ui/VideoPlayerShell.d.ts +19 -0
- package/package.json +1 -1
- package/src/index.ts +25 -1
- package/src/ui/AudioWaveform.tsx +190 -0
- package/src/ui/CouponField.tsx +120 -0
- package/src/ui/FollowingPointer.tsx +112 -0
- package/src/ui/GanttChart.tsx +283 -0
- package/src/ui/Kanban.tsx +250 -0
- package/src/ui/Lamp.tsx +88 -0
- package/src/ui/Lightbox.tsx +261 -0
- package/src/ui/OnboardingChecklist.tsx +163 -0
- package/src/ui/PivotTable.tsx +0 -0
- package/src/ui/SheetSnap.tsx +264 -0
- package/src/ui/TreeTable.tsx +172 -0
- package/src/ui/VideoPlayerShell.tsx +282 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface AudioWaveformProps {
|
|
3
|
+
src?: string;
|
|
4
|
+
peaks?: number[];
|
|
5
|
+
/** Bar row height in pixels. @default 64 */
|
|
6
|
+
height?: number;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Waveform of vertical bars driven by an optional hidden `<audio>` element.
|
|
11
|
+
* Pass `peaks` (numbers in `0..1`) for a real waveform, or omit it for a
|
|
12
|
+
* deterministic placeholder shape. The played portion (up to
|
|
13
|
+
* `currentTime / duration`) is highlighted; clicking a bar seeks there. With
|
|
14
|
+
* no `src`, playback is simulated on a timer for demo purposes.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <AudioWaveform src="/media/track.mp3" />
|
|
19
|
+
* <AudioWaveform peaks={[0.2, 0.8, 0.4, 0.9, 0.3]} height={48} />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function AudioWaveform(props: AudioWaveformProps): JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface CouponFieldProps {
|
|
3
|
+
/** Validate/apply a coupon code. Resolve with `{ ok: false }` for a rejected
|
|
4
|
+
* code — do not throw for expected rejections, only for unexpected failures. */
|
|
5
|
+
onApply: (code: string) => Promise<{
|
|
6
|
+
ok: boolean;
|
|
7
|
+
message?: string;
|
|
8
|
+
discount?: string;
|
|
9
|
+
}>;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Coupon/promo code input with an "Apply" button. Tracks explicit
|
|
15
|
+
* idle/loading/success/error state: loading shows a spinner and disables the
|
|
16
|
+
* input, success shows the applied discount with a way to remove it and try
|
|
17
|
+
* another code, error shows the failure message inline and lets the user retry.
|
|
18
|
+
* Rejections are read from the resolved `{ ok: false }` result, not caught
|
|
19
|
+
* exceptions — an unexpected throw from `onApply` propagates uncaught.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <CouponField
|
|
24
|
+
* onApply={async (code) => {
|
|
25
|
+
* const res = await api.applyCoupon(code)
|
|
26
|
+
* return res.valid
|
|
27
|
+
* ? { ok: true, discount: '-$10' }
|
|
28
|
+
* : { ok: false, message: 'That code has expired.' }
|
|
29
|
+
* }}
|
|
30
|
+
* />
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function CouponField(props: CouponFieldProps): JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface FollowingPointerProps {
|
|
3
|
+
label?: JSX.Element;
|
|
4
|
+
/** Tone of the custom cursor + label chip. @default 'primary' */
|
|
5
|
+
color?: 'primary' | 'accent';
|
|
6
|
+
children: JSX.Element;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Wraps `children` in a `relative` container that hides the native cursor
|
|
11
|
+
* and renders a custom pointer — a small arrow glyph plus a rounded label
|
|
12
|
+
* chip in the given tone — that follows the mouse within the container.
|
|
13
|
+
* Hidden until the pointer enters, hidden again on leave. Under
|
|
14
|
+
* {@link motionReduced}, the native cursor is kept and nothing custom is
|
|
15
|
+
* rendered. Listeners are cleaned up on unmount.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <FollowingPointer label="Alex" color="accent">
|
|
20
|
+
* <div class="grid h-48 place-items-center rounded-lg border">Hover me</div>
|
|
21
|
+
* </FollowingPointer>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function FollowingPointer(props: FollowingPointerProps): JSX.Element;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
/** A single bar rendered by {@link GanttChart}. */
|
|
3
|
+
export interface GanttTask {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
/** ISO date, `'YYYY-MM-DD'`. */
|
|
7
|
+
start: string;
|
|
8
|
+
/** ISO date, `'YYYY-MM-DD'`. */
|
|
9
|
+
end: string;
|
|
10
|
+
/** Ids of tasks that must finish before this one starts; drawn as elbow connectors. */
|
|
11
|
+
dependencies?: string[];
|
|
12
|
+
/** Bar color. Defaults to `'primary'`. */
|
|
13
|
+
tone?: 'primary' | 'accent';
|
|
14
|
+
}
|
|
15
|
+
export interface GanttChartProps {
|
|
16
|
+
tasks: GanttTask[];
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Read-only Gantt chart: a header row of date ticks over a proportional time
|
|
21
|
+
* axis, one row per task with a colored bar spanning `start`–`end`, elbow
|
|
22
|
+
* connectors for `dependencies`, and a "today" marker when today falls
|
|
23
|
+
* within the overall range. Horizontal scroll kicks in when the range is
|
|
24
|
+
* wide relative to the container.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <GanttChart
|
|
29
|
+
* tasks={[
|
|
30
|
+
* { id: 'design', name: 'Design', start: '2026-01-05', end: '2026-01-16', tone: 'accent' },
|
|
31
|
+
* { id: 'build', name: 'Build', start: '2026-01-19', end: '2026-02-06', dependencies: ['design'] },
|
|
32
|
+
* { id: 'launch', name: 'Launch', start: '2026-02-09', end: '2026-02-10', dependencies: ['build'] },
|
|
33
|
+
* ]}
|
|
34
|
+
* />
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function GanttChart(props: GanttChartProps): JSX.Element;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
/** A single card on a {@link Kanban} board. */
|
|
3
|
+
export interface KanbanCard {
|
|
4
|
+
/** Stable unique id (used for drag tracking + list keying). */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Card body — usually a title/label. */
|
|
7
|
+
title: JSX.Element;
|
|
8
|
+
/** Optional trailing badge (e.g. priority, assignee initials). */
|
|
9
|
+
badge?: JSX.Element;
|
|
10
|
+
}
|
|
11
|
+
/** A column of a {@link Kanban} board. */
|
|
12
|
+
export interface KanbanColumn {
|
|
13
|
+
/** Stable unique id. */
|
|
14
|
+
id: string;
|
|
15
|
+
/** Column heading. */
|
|
16
|
+
title: string;
|
|
17
|
+
/** Cards in the column, top to bottom. */
|
|
18
|
+
cards: KanbanCard[];
|
|
19
|
+
/** Optional WIP limit — the count badge switches to a warning tone past it. */
|
|
20
|
+
limit?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface KanbanProps {
|
|
23
|
+
/** Columns, in display order left to right. */
|
|
24
|
+
columns: KanbanColumn[];
|
|
25
|
+
/** Called with the full new columns array after any drag (reorder or move). */
|
|
26
|
+
onChange?: (columns: KanbanColumn[]) => void;
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Horizontal Kanban board: glass column panels holding vertical lists of
|
|
31
|
+
* draggable cards. Drag a card by its grip handle to reorder it within a
|
|
32
|
+
* column or drop it into another column — a dashed placeholder marks the
|
|
33
|
+
* drop slot and a floating clone follows the pointer, the same pointer-events
|
|
34
|
+
* idiom as {@link Sortable} extended to track which column's list the
|
|
35
|
+
* pointer is currently over. Works controlled (pass `columns` sourced from
|
|
36
|
+
* your own state + `onChange` to write it back) or uncontrolled (pass
|
|
37
|
+
* `columns` once and just read the final layout from `onChange`).
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* const [columns, setColumns] = createSignal<KanbanColumn[]>([
|
|
42
|
+
* { id: 'todo', title: 'To do', cards: [{ id: '1', title: 'Write spec' }] },
|
|
43
|
+
* { id: 'doing', title: 'Doing', cards: [], limit: 2 },
|
|
44
|
+
* { id: 'done', title: 'Done', cards: [] },
|
|
45
|
+
* ])
|
|
46
|
+
* <Kanban columns={columns()} onChange={setColumns} />
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function Kanban(props: KanbanProps): JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
export interface LampProps {
|
|
3
|
+
children?: JSX.Element;
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Hero/section backdrop: a spotlight beam fanning down from the top center —
|
|
8
|
+
* two symmetric blurred gradient cones (`--primary` / `--accent`) plus a
|
|
9
|
+
* bright thin source line — over a dark base, with `children` centered in
|
|
10
|
+
* the illuminated area below. Grows/brightens in on mount via Motion
|
|
11
|
+
* (opacity + scale); static under {@link motionReduced}. Self-contained
|
|
12
|
+
* (`relative overflow-hidden`); size it with `class` (e.g. `min-h-[28rem]`).
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <Lamp class="min-h-[28rem]">
|
|
17
|
+
* <h1 class="text-4xl font-semibold text-foreground">Build in the light</h1>
|
|
18
|
+
* <p class="mt-4 text-muted-foreground">A hero backdrop with a spotlight glow.</p>
|
|
19
|
+
* </Lamp>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function Lamp(props: LampProps): JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
/** One image in a {@link Lightbox}. */
|
|
3
|
+
export interface LightboxImage {
|
|
4
|
+
src: string;
|
|
5
|
+
alt?: string;
|
|
6
|
+
/** Smaller image used in the thumbnail grid/strip; falls back to `src`. */
|
|
7
|
+
thumb?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LightboxProps {
|
|
10
|
+
images: LightboxImage[];
|
|
11
|
+
/** Controlled overlay open state. When set, wins over internal state — pair with `onOpenChange`. */
|
|
12
|
+
open?: boolean;
|
|
13
|
+
/** Controlled active image index (clamped to the images array). When set, wins over internal state — pair with `onIndexChange`. */
|
|
14
|
+
index?: number;
|
|
15
|
+
onOpenChange?: (open: boolean) => void;
|
|
16
|
+
onIndexChange?: (index: number) => void;
|
|
17
|
+
/** Show the thumbnail strip at the bottom of the overlay. @default true */
|
|
18
|
+
showThumbnails?: boolean;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Responsive thumbnail grid (`thumb || src`) that opens into a full-screen
|
|
23
|
+
* viewer on click: the active image centered, prev/next chevrons, a close
|
|
24
|
+
* button, an alt-text caption, a zoom toggle, and (by default) a thumbnail
|
|
25
|
+
* strip with the active image highlighted. Navigate with the arrow keys or
|
|
26
|
+
* Escape, click the backdrop to close. Works controlled
|
|
27
|
+
* (`open`/`index` + `onOpenChange`/`onIndexChange`) or uncontrolled. Body
|
|
28
|
+
* scroll locks while open, like `Modal`; the zoom transition is skipped
|
|
29
|
+
* under `prefers-reduced-motion` but stays fully functional.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* <Lightbox
|
|
34
|
+
* images={[
|
|
35
|
+
* { src: '/photos/1-full.jpg', thumb: '/photos/1-thumb.jpg', alt: 'Sunset over the bay' },
|
|
36
|
+
* { src: '/photos/2-full.jpg', thumb: '/photos/2-thumb.jpg', alt: 'Mountain trail' },
|
|
37
|
+
* ]}
|
|
38
|
+
* />
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function Lightbox(props: LightboxProps): JSX.Element;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
/** A single step in an {@link OnboardingChecklist}. */
|
|
3
|
+
export interface OnboardingStep {
|
|
4
|
+
/** Stable identifier, used as the toggle key and `For` item key. */
|
|
5
|
+
id: string;
|
|
6
|
+
title: JSX.Element;
|
|
7
|
+
/** Optional detail shown when the step is expanded. */
|
|
8
|
+
description?: JSX.Element;
|
|
9
|
+
/** Whether the step is complete. */
|
|
10
|
+
done?: boolean;
|
|
11
|
+
/** Optional call-to-action rendered alongside the description. */
|
|
12
|
+
action?: {
|
|
13
|
+
label: string;
|
|
14
|
+
onClick: () => void;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface OnboardingChecklistProps {
|
|
18
|
+
steps: OnboardingStep[];
|
|
19
|
+
/** Called with the step id and its next `done` state when the indicator is toggled. */
|
|
20
|
+
onToggle?: (id: string, done: boolean) => void;
|
|
21
|
+
/** Heading shown above the progress summary. Defaults to `'Getting started'`. */
|
|
22
|
+
title?: JSX.Element;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Card-shaped onboarding/setup checklist: a header with a completion ring and
|
|
27
|
+
* "{done} of {total} complete" summary, followed by expandable steps. Each
|
|
28
|
+
* step has a clickable check indicator (toggles `done` via `onToggle`), a
|
|
29
|
+
* title, and an optional description + CTA revealed on expand. Completion is
|
|
30
|
+
* controlled by the caller; expansion is local UI state (one step open at a time).
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* const [steps, setSteps] = createSignal<OnboardingStep[]>([
|
|
35
|
+
* { id: 'profile', title: 'Complete your profile', done: true },
|
|
36
|
+
* {
|
|
37
|
+
* id: 'invite',
|
|
38
|
+
* title: 'Invite a teammate',
|
|
39
|
+
* description: 'Collaborate faster by adding at least one teammate.',
|
|
40
|
+
* action: { label: 'Invite', onClick: () => openInviteDialog() },
|
|
41
|
+
* },
|
|
42
|
+
* ])
|
|
43
|
+
* <OnboardingChecklist
|
|
44
|
+
* steps={steps()}
|
|
45
|
+
* onToggle={(id, done) =>
|
|
46
|
+
* setSteps((prev) => prev.map((s) => (s.id === id ? { ...s, done } : s)))
|
|
47
|
+
* }
|
|
48
|
+
* />
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function OnboardingChecklist(props: OnboardingChecklistProps): JSX.Element;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
/** A single record fed into {@link PivotTable}. */
|
|
3
|
+
export interface PivotDatum {
|
|
4
|
+
[key: string]: string | number;
|
|
5
|
+
}
|
|
6
|
+
export interface PivotTableProps {
|
|
7
|
+
data: PivotDatum[];
|
|
8
|
+
/** Field whose unique values become row headers. */
|
|
9
|
+
rowField: string;
|
|
10
|
+
/** Field whose unique values become column headers. */
|
|
11
|
+
columnField: string;
|
|
12
|
+
/** Numeric field aggregated into each cell. */
|
|
13
|
+
valueField: string;
|
|
14
|
+
/** Aggregation applied per (row, column) bucket. Defaults to 'sum'. */
|
|
15
|
+
aggregate?: 'sum' | 'count' | 'avg';
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Cross-tab table: crosses the unique values of `rowField` and `columnField`,
|
|
20
|
+
* aggregating `valueField` into each cell (default `'sum'`), plus row/column
|
|
21
|
+
* totals.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <PivotTable
|
|
26
|
+
* data={[
|
|
27
|
+
* { region: 'West', product: 'Widget', revenue: 100 },
|
|
28
|
+
* { region: 'West', product: 'Gadget', revenue: 50 },
|
|
29
|
+
* { region: 'East', product: 'Widget', revenue: 75 },
|
|
30
|
+
* ]}
|
|
31
|
+
* rowField="region"
|
|
32
|
+
* columnField="product"
|
|
33
|
+
* valueField="revenue"
|
|
34
|
+
* />
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function PivotTable(props: PivotTableProps): 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,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
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.
|
|
11
|
+
export const A4UI_VERSION = '0.34.0'
|
|
12
12
|
|
|
13
13
|
// Helpers (src/lib) — generic, framework-level utilities.
|
|
14
14
|
export { cn } from './lib/cn'
|
|
@@ -209,6 +209,30 @@ 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'
|
|
212
236
|
export {
|
|
213
237
|
CopyButton,
|
|
214
238
|
type CopyButtonProps,
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Bar-style audio waveform over a hidden native <audio> element — no media
|
|
2
|
+
// library. When no real `peaks` are supplied, a deterministic placeholder
|
|
3
|
+
// waveform is derived from the bar index (abs(sin(...)) blend) so the shape
|
|
4
|
+
// is stable across renders instead of reshuffling on every re-render like
|
|
5
|
+
// Math.random() would.
|
|
6
|
+
import { Pause, Play } from 'lucide-solid'
|
|
7
|
+
import type { JSX } from 'solid-js'
|
|
8
|
+
import { createMemo, createSignal, For, onCleanup, onMount, Show } from 'solid-js'
|
|
9
|
+
|
|
10
|
+
import { cn } from '../lib/cn'
|
|
11
|
+
import { motionReduced } from '../lib/motion'
|
|
12
|
+
|
|
13
|
+
export interface AudioWaveformProps {
|
|
14
|
+
src?: string
|
|
15
|
+
peaks?: number[]
|
|
16
|
+
/** Bar row height in pixels. @default 64 */
|
|
17
|
+
height?: number
|
|
18
|
+
class?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PLACEHOLDER_BAR_COUNT = 48
|
|
22
|
+
// Demo progress rate (fraction of the fake "duration" per animation frame
|
|
23
|
+
// tick) used only when there's no `src` to drive real audio playback.
|
|
24
|
+
const DEMO_TICK_MS = 100
|
|
25
|
+
const DEMO_STEP = 0.01
|
|
26
|
+
|
|
27
|
+
/** Deterministic placeholder bar heights (0..1), no Math.random. */
|
|
28
|
+
function placeholderPeaks(count: number): number[] {
|
|
29
|
+
return Array.from({ length: count }, (_, i) => {
|
|
30
|
+
const a = Math.abs(Math.sin(i * 0.35))
|
|
31
|
+
const b = Math.abs(Math.sin(i * 0.09 + 1.2))
|
|
32
|
+
return 0.15 + 0.85 * (0.65 * a + 0.35 * b)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Waveform of vertical bars driven by an optional hidden `<audio>` element.
|
|
38
|
+
* Pass `peaks` (numbers in `0..1`) for a real waveform, or omit it for a
|
|
39
|
+
* deterministic placeholder shape. The played portion (up to
|
|
40
|
+
* `currentTime / duration`) is highlighted; clicking a bar seeks there. With
|
|
41
|
+
* no `src`, playback is simulated on a timer for demo purposes.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <AudioWaveform src="/media/track.mp3" />
|
|
46
|
+
* <AudioWaveform peaks={[0.2, 0.8, 0.4, 0.9, 0.3]} height={48} />
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function AudioWaveform(props: AudioWaveformProps): JSX.Element {
|
|
50
|
+
let audioRef: HTMLAudioElement | undefined
|
|
51
|
+
|
|
52
|
+
const [playing, setPlaying] = createSignal(false)
|
|
53
|
+
const [currentTime, setCurrentTime] = createSignal(0)
|
|
54
|
+
const [duration, setDuration] = createSignal(0)
|
|
55
|
+
|
|
56
|
+
const bars = createMemo(() => props.peaks ?? placeholderPeaks(PLACEHOLDER_BAR_COUNT))
|
|
57
|
+
const progress = createMemo(() => (duration() > 0 ? currentTime() / duration() : 0))
|
|
58
|
+
const barHeight = () => props.height ?? 64
|
|
59
|
+
|
|
60
|
+
const togglePlay = (): void => {
|
|
61
|
+
if (props.src) {
|
|
62
|
+
const audio = audioRef
|
|
63
|
+
if (!audio) return
|
|
64
|
+
if (audio.paused) void audio.play()
|
|
65
|
+
else audio.pause()
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
// No source: drive a fake demo progress instead of real playback.
|
|
69
|
+
setPlaying((p) => !p)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const seekToRatio = (ratio: number): void => {
|
|
73
|
+
const clamped = Math.min(Math.max(ratio, 0), 1)
|
|
74
|
+
if (props.src) {
|
|
75
|
+
const audio = audioRef
|
|
76
|
+
if (!audio || !duration()) return
|
|
77
|
+
audio.currentTime = clamped * duration()
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
setCurrentTime(clamped * (duration() || 1))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const handleBarClick = (index: number): void => {
|
|
84
|
+
seekToRatio((index + 0.5) / bars().length)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onMount(() => {
|
|
88
|
+
if (props.src) {
|
|
89
|
+
const audio = audioRef
|
|
90
|
+
if (!audio) return
|
|
91
|
+
|
|
92
|
+
const onPlay = (): void => {
|
|
93
|
+
setPlaying(true)
|
|
94
|
+
}
|
|
95
|
+
const onPause = (): void => {
|
|
96
|
+
setPlaying(false)
|
|
97
|
+
}
|
|
98
|
+
const onTimeUpdate = (): void => {
|
|
99
|
+
setCurrentTime(audio.currentTime)
|
|
100
|
+
}
|
|
101
|
+
const onLoadedMetadata = (): void => {
|
|
102
|
+
setDuration(audio.duration || 0)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
audio.addEventListener('play', onPlay)
|
|
106
|
+
audio.addEventListener('pause', onPause)
|
|
107
|
+
audio.addEventListener('timeupdate', onTimeUpdate)
|
|
108
|
+
audio.addEventListener('loadedmetadata', onLoadedMetadata)
|
|
109
|
+
|
|
110
|
+
onCleanup(() => {
|
|
111
|
+
audio.removeEventListener('play', onPlay)
|
|
112
|
+
audio.removeEventListener('pause', onPause)
|
|
113
|
+
audio.removeEventListener('timeupdate', onTimeUpdate)
|
|
114
|
+
audio.removeEventListener('loadedmetadata', onLoadedMetadata)
|
|
115
|
+
})
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Demo mode: fake a duration and advance currentTime on an interval while playing.
|
|
120
|
+
setDuration(30)
|
|
121
|
+
const interval = setInterval(() => {
|
|
122
|
+
if (!playing() || motionReduced()) return
|
|
123
|
+
setCurrentTime((t) => {
|
|
124
|
+
const next = t + DEMO_STEP * duration()
|
|
125
|
+
if (next >= duration()) {
|
|
126
|
+
setPlaying(false)
|
|
127
|
+
return 0
|
|
128
|
+
}
|
|
129
|
+
return next
|
|
130
|
+
})
|
|
131
|
+
}, DEMO_TICK_MS)
|
|
132
|
+
onCleanup(() => clearInterval(interval))
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div class={cn('card flex items-center gap-3 p-3', props.class)}>
|
|
137
|
+
<Show when={props.src}>
|
|
138
|
+
<audio ref={audioRef} src={props.src} />
|
|
139
|
+
</Show>
|
|
140
|
+
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
aria-label={playing() ? 'Pause' : 'Play'}
|
|
144
|
+
onClick={togglePlay}
|
|
145
|
+
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground transition-transform duration-150 hover:scale-105 active:scale-95"
|
|
146
|
+
>
|
|
147
|
+
<Show when={playing()} fallback={<Play class="h-4 w-4 translate-x-0.5" fill="currentColor" />}>
|
|
148
|
+
<Pause class="h-4 w-4" fill="currentColor" />
|
|
149
|
+
</Show>
|
|
150
|
+
</button>
|
|
151
|
+
|
|
152
|
+
<div
|
|
153
|
+
role="slider"
|
|
154
|
+
aria-label="Waveform position"
|
|
155
|
+
aria-valuemin={0}
|
|
156
|
+
aria-valuemax={100}
|
|
157
|
+
aria-valuenow={Math.round(progress() * 100)}
|
|
158
|
+
tabIndex={0}
|
|
159
|
+
class="flex flex-1 cursor-pointer items-center gap-[2px]"
|
|
160
|
+
style={{ height: `${barHeight()}px` }}
|
|
161
|
+
onClick={(event) => {
|
|
162
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
163
|
+
seekToRatio((event.clientX - rect.left) / rect.width)
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<For each={bars()}>
|
|
167
|
+
{(peak, index) => {
|
|
168
|
+
const played = createMemo(() => index() / bars().length <= progress())
|
|
169
|
+
return (
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
aria-hidden="true"
|
|
173
|
+
tabIndex={-1}
|
|
174
|
+
onClick={(event) => {
|
|
175
|
+
event.stopPropagation()
|
|
176
|
+
handleBarClick(index())
|
|
177
|
+
}}
|
|
178
|
+
class={cn(
|
|
179
|
+
'w-full min-w-[2px] flex-1 rounded-full transition-colors duration-150',
|
|
180
|
+
played() ? 'bg-primary' : 'bg-muted',
|
|
181
|
+
)}
|
|
182
|
+
style={{ height: `${Math.max(peak, 0.06) * 100}%` }}
|
|
183
|
+
/>
|
|
184
|
+
)
|
|
185
|
+
}}
|
|
186
|
+
</For>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
)
|
|
190
|
+
}
|