You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
116 lines
3.4 KiB
TypeScript
116 lines
3.4 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
|
|
/** Wireframe kit: structure-first components shared by POS and back office. */
|
|
|
|
export function PageHeader(props: { title: string; desc?: string; badge?: string; actions?: ReactNode }) {
|
|
return (
|
|
<>
|
|
<div className="wf-page-head">
|
|
<h1>{props.title}</h1>
|
|
{props.badge !== undefined && <Badge tone="accent">{props.badge}</Badge>}
|
|
<div style={{ flex: 1 }} />
|
|
{props.actions}
|
|
</div>
|
|
{props.desc !== undefined && <p className="wf-page-desc">{props.desc}</p>}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export function Toolbar(props: { children: ReactNode }) {
|
|
return <div className="wf-toolbar">{props.children}</div>
|
|
}
|
|
|
|
export function Button(props: {
|
|
children: ReactNode
|
|
onClick?: () => void
|
|
tone?: 'default' | 'primary' | 'danger'
|
|
hotkey?: string
|
|
}) {
|
|
const cls = `wf${props.tone === 'primary' ? ' primary' : props.tone === 'danger' ? ' danger' : ''}`
|
|
return (
|
|
<button type="button" className={cls} onClick={props.onClick}>
|
|
{props.children}
|
|
{props.hotkey !== undefined && <kbd>{props.hotkey}</kbd>}
|
|
</button>
|
|
)
|
|
}
|
|
|
|
export function Badge(props: { children: ReactNode; tone?: 'ok' | 'warn' | 'err' | 'accent' }) {
|
|
return <span className={`wf-badge${props.tone !== undefined ? ` ${props.tone}` : ''}`}>{props.children}</span>
|
|
}
|
|
|
|
export interface Column {
|
|
key: string
|
|
label: string
|
|
numeric?: boolean
|
|
}
|
|
|
|
export function DataTable(props: {
|
|
columns: Column[]
|
|
rows: Record<string, ReactNode>[]
|
|
onRowClick?: (row: Record<string, ReactNode>, index: number) => void
|
|
}) {
|
|
return (
|
|
<table className="wf">
|
|
<thead>
|
|
<tr>
|
|
{props.columns.map((c) => (
|
|
<th key={c.key} className={c.numeric === true ? 'num' : undefined}>{c.label}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{props.rows.map((row, i) => (
|
|
<tr key={i} onClick={() => props.onRowClick?.(row, i)} style={props.onRowClick !== undefined ? { cursor: 'pointer' } : undefined}>
|
|
{props.columns.map((c) => (
|
|
<td key={c.key} className={c.numeric === true ? 'num' : undefined}>{row[c.key]}</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)
|
|
}
|
|
|
|
export function StatCard(props: { label: string; value: ReactNode; hint?: string }) {
|
|
return (
|
|
<div className="wf-stat">
|
|
<div className="label">{props.label}</div>
|
|
<div className="value num">{props.value}</div>
|
|
{props.hint !== undefined && <div className="hint">{props.hint}</div>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function Stats(props: { children: ReactNode }) {
|
|
return <div className="wf-stats">{props.children}</div>
|
|
}
|
|
|
|
export function EmptyState(props: { children: ReactNode }) {
|
|
return <div className="wf-empty">{props.children}</div>
|
|
}
|
|
|
|
export function Field(props: { label: string; children: ReactNode }) {
|
|
return (
|
|
<div className="wf-field">
|
|
<label>{props.label}</label>
|
|
{props.children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function Modal(props: { title: string; onClose: () => void; children: ReactNode }) {
|
|
return (
|
|
<div className="wf-modal-back" onClick={props.onClose}>
|
|
<div className="wf-modal" onClick={(e) => e.stopPropagation()}>
|
|
<h2>{props.title}</h2>
|
|
{props.children}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function Notice(props: { tone?: 'ok' | 'warn' | 'err'; children: ReactNode }) {
|
|
return <div className={`wf-notice${props.tone !== undefined ? ` ${props.tone}` : ''}`}>{props.children}</div>
|
|
}
|