@a4ui/core 0.27.0 → 0.29.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/README.md +11 -11
- package/dist/elements.css +66 -0
- package/dist/full.css +66 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +3033 -2517
- package/dist/ui/BalanceCard.d.ts +24 -0
- package/dist/ui/BlockEditor.d.ts +42 -0
- package/dist/ui/KpiBlock.d.ts +22 -0
- package/dist/ui/MoneyActionButton.d.ts +22 -0
- package/dist/ui/ScrollScene.d.ts +25 -0
- package/dist/ui/SideRail.d.ts +39 -0
- package/dist/ui/Spaces.d.ts +31 -0
- package/dist/ui/StickyReveal.d.ts +24 -0
- package/dist/ui/TransactionFeed.d.ts +40 -0
- package/package.json +1 -1
- package/src/index.ts +12 -1
- package/src/ui/BalanceCard.tsx +68 -0
- package/src/ui/BlockEditor.tsx +139 -0
- package/src/ui/KpiBlock.tsx +58 -0
- package/src/ui/MoneyActionButton.tsx +54 -0
- package/src/ui/ScrollScene.tsx +70 -0
- package/src/ui/SideRail.tsx +120 -0
- package/src/ui/Spaces.tsx +139 -0
- package/src/ui/StickyReveal.tsx +103 -0
- package/src/ui/TransactionFeed.tsx +130 -0
|
@@ -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,42 @@
|
|
|
1
|
+
import { JSX } from 'solid-js';
|
|
2
|
+
/** A single block in a {@link BlockEditor}. Content is supplied by the caller. */
|
|
3
|
+
export interface EditorBlock {
|
|
4
|
+
id: string;
|
|
5
|
+
type: string;
|
|
6
|
+
content: JSX.Element;
|
|
7
|
+
}
|
|
8
|
+
/** An option offered by the "add block" menu. */
|
|
9
|
+
export interface BlockTypeOption {
|
|
10
|
+
value: string;
|
|
11
|
+
label: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
icon?: JSX.Element;
|
|
14
|
+
}
|
|
15
|
+
export interface BlockEditorProps {
|
|
16
|
+
blocks: EditorBlock[];
|
|
17
|
+
/** Called on reorder and on delete, with the new array. */
|
|
18
|
+
onChange: (blocks: EditorBlock[]) => void;
|
|
19
|
+
/** Block types offered by the "add" menu. */
|
|
20
|
+
blockTypes?: BlockTypeOption[];
|
|
21
|
+
/** Called when the user picks a type from the add menu; the caller creates + inserts the block. */
|
|
22
|
+
onAddBlock?: (type: string) => void;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Reorderable, deletable stack of content blocks with a slash-style insert
|
|
27
|
+
* menu — the block-primitives layer of a Notion-style editor, not a
|
|
28
|
+
* rich-text engine. Reordering and the drag handle come from {@link Sortable};
|
|
29
|
+
* the "add block" picker reuses {@link SlashMenu} filtered by a local search
|
|
30
|
+
* input.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <BlockEditor
|
|
35
|
+
* blocks={blocks()}
|
|
36
|
+
* onChange={setBlocks}
|
|
37
|
+
* blockTypes={[{ value: 'text', label: 'Text' }]}
|
|
38
|
+
* onAddBlock={(type) => setBlocks([...blocks(), { id: crypto.randomUUID(), type, content: <p>New</p> }])}
|
|
39
|
+
* />
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function BlockEditor(props: BlockEditorProps): 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,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
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.29.0'
|
|
12
12
|
|
|
13
13
|
// Helpers (src/lib) — generic, framework-level utilities.
|
|
14
14
|
export { cn } from './lib/cn'
|
|
@@ -168,6 +168,17 @@ export { MasterDetail, type MasterDetailProps, type MasterDetailItem } from './u
|
|
|
168
168
|
export { SlashMenu, type SlashMenuProps, type SlashItem } from './ui/SlashMenu'
|
|
169
169
|
export { DataView, type DataViewProps, type DataViewColumn, type DataViewMode } from './ui/DataView'
|
|
170
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'
|
|
180
|
+
export { BlockEditor, type BlockEditorProps, type EditorBlock, type BlockTypeOption } from './ui/BlockEditor'
|
|
181
|
+
|
|
171
182
|
// Motion components (src/ui) — animation primitives built on the `motion` engine
|
|
172
183
|
// (adapted from motion.dev examples). All tree-shakeable and reduced-motion aware;
|
|
173
184
|
// `motion` is external, so importing one of these is the only thing that pulls it.
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// BlockEditor — block-primitives shell for a Notion-style editor: a
|
|
2
|
+
// reorderable, deletable stack of blocks plus a slash-style "add block" menu.
|
|
3
|
+
// It does not know how to render or edit block content (that's the caller's
|
|
4
|
+
// job via `EditorBlock.content`) — it only owns ordering, removal, and
|
|
5
|
+
// insertion UI. Built entirely from existing primitives: Sortable (drag
|
|
6
|
+
// handle + reorder) and SlashMenu (filterable insert picker).
|
|
7
|
+
import { createSignal, Show, type JSX } from 'solid-js'
|
|
8
|
+
import { Plus, Trash2 } from 'lucide-solid'
|
|
9
|
+
|
|
10
|
+
import { cn } from '../lib/cn'
|
|
11
|
+
import { Button } from './Button'
|
|
12
|
+
import { Sortable } from './Sortable'
|
|
13
|
+
import { SlashMenu } from './SlashMenu'
|
|
14
|
+
|
|
15
|
+
/** A single block in a {@link BlockEditor}. Content is supplied by the caller. */
|
|
16
|
+
export interface EditorBlock {
|
|
17
|
+
id: string
|
|
18
|
+
type: string
|
|
19
|
+
content: JSX.Element
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** An option offered by the "add block" menu. */
|
|
23
|
+
export interface BlockTypeOption {
|
|
24
|
+
value: string
|
|
25
|
+
label: string
|
|
26
|
+
description?: string
|
|
27
|
+
icon?: JSX.Element
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BlockEditorProps {
|
|
31
|
+
blocks: EditorBlock[]
|
|
32
|
+
/** Called on reorder and on delete, with the new array. */
|
|
33
|
+
onChange: (blocks: EditorBlock[]) => void
|
|
34
|
+
/** Block types offered by the "add" menu. */
|
|
35
|
+
blockTypes?: BlockTypeOption[]
|
|
36
|
+
/** Called when the user picks a type from the add menu; the caller creates + inserts the block. */
|
|
37
|
+
onAddBlock?: (type: string) => void
|
|
38
|
+
class?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Reorderable, deletable stack of content blocks with a slash-style insert
|
|
43
|
+
* menu — the block-primitives layer of a Notion-style editor, not a
|
|
44
|
+
* rich-text engine. Reordering and the drag handle come from {@link Sortable};
|
|
45
|
+
* the "add block" picker reuses {@link SlashMenu} filtered by a local search
|
|
46
|
+
* input.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* <BlockEditor
|
|
51
|
+
* blocks={blocks()}
|
|
52
|
+
* onChange={setBlocks}
|
|
53
|
+
* blockTypes={[{ value: 'text', label: 'Text' }]}
|
|
54
|
+
* onAddBlock={(type) => setBlocks([...blocks(), { id: crypto.randomUUID(), type, content: <p>New</p> }])}
|
|
55
|
+
* />
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function BlockEditor(props: BlockEditorProps): JSX.Element {
|
|
59
|
+
const [menuOpen, setMenuOpen] = createSignal(false)
|
|
60
|
+
const [query, setQuery] = createSignal('')
|
|
61
|
+
|
|
62
|
+
const closeMenu = () => {
|
|
63
|
+
setMenuOpen(false)
|
|
64
|
+
setQuery('')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const handleDelete = (id: string) => {
|
|
68
|
+
props.onChange(props.blocks.filter((block) => block.id !== id))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const handleSelect = (type: string) => {
|
|
72
|
+
props.onAddBlock?.(type)
|
|
73
|
+
closeMenu()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div class={cn('flex flex-col gap-3', props.class)}>
|
|
78
|
+
<Show
|
|
79
|
+
when={props.blocks.length}
|
|
80
|
+
fallback={
|
|
81
|
+
<div class="rounded-lg border border-dashed border-border p-6 text-center text-sm text-muted-foreground">
|
|
82
|
+
No blocks yet
|
|
83
|
+
</div>
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
<Sortable
|
|
87
|
+
items={props.blocks}
|
|
88
|
+
itemKey={(block) => block.id}
|
|
89
|
+
onReorder={(items) => props.onChange(items)}
|
|
90
|
+
>
|
|
91
|
+
{(block) => (
|
|
92
|
+
<div class="group/block flex w-full items-center justify-between gap-2">
|
|
93
|
+
<div class="min-w-0 flex-1">{block.content}</div>
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={() => handleDelete(block.id)}
|
|
97
|
+
class="shrink-0 rounded-md p-1 text-muted-foreground opacity-0 transition-opacity hover:bg-muted hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring group-hover/block:opacity-100"
|
|
98
|
+
aria-label="Delete block"
|
|
99
|
+
>
|
|
100
|
+
<Trash2 class="h-4 w-4" />
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</Sortable>
|
|
105
|
+
</Show>
|
|
106
|
+
|
|
107
|
+
<div
|
|
108
|
+
onKeyDown={(e) => {
|
|
109
|
+
if (e.key === 'Escape') closeMenu()
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<Button
|
|
113
|
+
type="button"
|
|
114
|
+
variant="outline"
|
|
115
|
+
onClick={() => setMenuOpen((open) => !open)}
|
|
116
|
+
aria-expanded={menuOpen()}
|
|
117
|
+
>
|
|
118
|
+
<Plus class="mr-1 h-4 w-4" />
|
|
119
|
+
Add block
|
|
120
|
+
</Button>
|
|
121
|
+
|
|
122
|
+
<Show when={menuOpen()}>
|
|
123
|
+
<div class="mt-2 flex flex-col gap-2">
|
|
124
|
+
<input
|
|
125
|
+
type="text"
|
|
126
|
+
autofocus
|
|
127
|
+
value={query()}
|
|
128
|
+
onInput={(e) => setQuery(e.currentTarget.value)}
|
|
129
|
+
placeholder="Search block types..."
|
|
130
|
+
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
|
131
|
+
aria-label="Filter block types"
|
|
132
|
+
/>
|
|
133
|
+
<SlashMenu items={props.blockTypes ?? []} query={query()} onSelect={handleSelect} />
|
|
134
|
+
</div>
|
|
135
|
+
</Show>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Labelled metric block for dashboards: value + a signed delta chip, with an
|
|
2
|
+
// optional inline chart slot (e.g. a Sparkline from @a4ui/core/charts) passed
|
|
3
|
+
// in by the consumer — this component never imports the charts package.
|
|
4
|
+
import { ArrowDown, ArrowUp } from 'lucide-solid'
|
|
5
|
+
import { Show, type JSX } from 'solid-js'
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/cn'
|
|
8
|
+
import { Badge } from './Badge'
|
|
9
|
+
import { Card, CardContent } from './Card'
|
|
10
|
+
|
|
11
|
+
export interface KpiBlockProps {
|
|
12
|
+
label: string
|
|
13
|
+
value: string | number
|
|
14
|
+
/** Change as a fraction (0.128 = +12.8%). Shows a signed up/down chip. */
|
|
15
|
+
delta?: number
|
|
16
|
+
/** Optional chart node (e.g. a <Sparkline/> from @a4ui/core/charts) shown at the bottom. */
|
|
17
|
+
chart?: JSX.Element
|
|
18
|
+
class?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const formatDelta = (delta: number) => `${delta >= 0 ? '+' : ''}${(delta * 100).toFixed(1)}%`
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Labelled metric tile for dashboards: a large value with a signed
|
|
25
|
+
* up/down delta chip and an optional chart slot at the bottom. The chart is
|
|
26
|
+
* consumer-supplied (e.g. a `<Sparkline/>` from `@a4ui/core/charts`) — this
|
|
27
|
+
* component takes it as a prop rather than importing the charts package.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <KpiBlock label="Monthly revenue" value="$48,204" delta={0.128} />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function KpiBlock(props: KpiBlockProps): JSX.Element {
|
|
35
|
+
return (
|
|
36
|
+
<Card glass class={props.class}>
|
|
37
|
+
<CardContent class="p-5">
|
|
38
|
+
<p class="text-sm text-muted-foreground">{props.label}</p>
|
|
39
|
+
<div class="mt-1 flex items-center gap-2">
|
|
40
|
+
<span class="text-2xl font-semibold text-foreground">{props.value}</span>
|
|
41
|
+
<Show when={props.delta !== undefined}>
|
|
42
|
+
<Badge tone={(props.delta as number) >= 0 ? 'success' : 'danger'}>
|
|
43
|
+
{(props.delta as number) >= 0 ? (
|
|
44
|
+
<ArrowUp class="h-3 w-3" aria-hidden="true" />
|
|
45
|
+
) : (
|
|
46
|
+
<ArrowDown class="h-3 w-3" aria-hidden="true" />
|
|
47
|
+
)}
|
|
48
|
+
{formatDelta(props.delta as number)}
|
|
49
|
+
</Badge>
|
|
50
|
+
</Show>
|
|
51
|
+
</div>
|
|
52
|
+
<Show when={props.chart}>
|
|
53
|
+
<div class={cn('mt-3')}>{props.chart}</div>
|
|
54
|
+
</Show>
|
|
55
|
+
</CardContent>
|
|
56
|
+
</Card>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Prominent CTA reserved for value-moving actions (send / request / pay /
|
|
2
|
+
// withdraw). One accent, tightly scoped to money movement — deliberately
|
|
3
|
+
// bolder than the generic Button so it reads as "this moves money" at a
|
|
4
|
+
// glance, distinct from ordinary actions elsewhere in the UI.
|
|
5
|
+
import { ArrowDownLeft, ArrowUpRight, Banknote, CreditCard } from 'lucide-solid'
|
|
6
|
+
import type { Component, JSX } from 'solid-js'
|
|
7
|
+
import { splitProps } from 'solid-js'
|
|
8
|
+
import { Dynamic } from 'solid-js/web'
|
|
9
|
+
|
|
10
|
+
import { cn } from '../lib/cn'
|
|
11
|
+
|
|
12
|
+
/** Money-movement action a {@link MoneyActionButton} represents; picks the leading icon. */
|
|
13
|
+
export type MoneyActionKind = 'send' | 'request' | 'pay' | 'withdraw'
|
|
14
|
+
|
|
15
|
+
const KIND_ICON: Record<MoneyActionKind, Component<{ class?: string }>> = {
|
|
16
|
+
send: ArrowUpRight,
|
|
17
|
+
request: ArrowDownLeft,
|
|
18
|
+
pay: CreditCard,
|
|
19
|
+
withdraw: Banknote,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MoneyActionButtonProps {
|
|
23
|
+
children: JSX.Element
|
|
24
|
+
/** Which money-movement action this triggers. Defaults to `'send'`. */
|
|
25
|
+
kind?: MoneyActionKind
|
|
26
|
+
onClick?: () => void
|
|
27
|
+
disabled?: boolean
|
|
28
|
+
class?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const BASE =
|
|
32
|
+
'inline-flex h-full items-center justify-center gap-2 rounded-xl px-5 py-3 text-sm font-semibold bg-primary text-primary-foreground transition-[background-color,transform] duration-150 motion-reduce:transition-none hover:bg-primary/90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-ring disabled:pointer-events-none disabled:opacity-50'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Bold, full-height pill CTA reserved for value-moving actions — send,
|
|
36
|
+
* request, pay, withdraw. Uses the app's single accent (`bg-primary`) so it
|
|
37
|
+
* stands out from ordinary buttons, with a leading icon chosen by `kind`.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* <MoneyActionButton kind="send" onClick={() => sendMoney()}>Send</MoneyActionButton>
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function MoneyActionButton(props: MoneyActionButtonProps): JSX.Element {
|
|
45
|
+
const [local, rest] = splitProps(props, ['children', 'kind', 'class'])
|
|
46
|
+
const Icon = () => KIND_ICON[local.kind ?? 'send']
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<button type="button" class={cn(BASE, local.class)} {...rest}>
|
|
50
|
+
<Dynamic component={Icon()} class="h-4 w-4 shrink-0" aria-hidden="true" />
|
|
51
|
+
{local.children}
|
|
52
|
+
</button>
|
|
53
|
+
)
|
|
54
|
+
}
|