Fishies 0.8.2

This commit is contained in:
CRTkafa 2026-10-07 22:32:19 +03:00
parent 004fbdc97e
commit 4d52068149
14 changed files with 42 additions and 184 deletions

View file

@ -5,13 +5,9 @@ import FishiesImg from './FishiesImg'
import { ICON_SIZES, iconByName, iconUrl, type IconSize, type SystemIconName } from './icons'
import './kit.css'
/* fishies-ui components. Each export's first doc sentence is its Handbook entry (the build reads
them), so a component is described once, here, next to the code that draws it. */
const cx = (...names: (string | false | null | undefined)[]) => names.filter(Boolean).join(' ')
const px = (size: IconSize | number) => typeof size === 'number' ? size : ICON_SIZES[size]
/** A painted icon from the system set, by name, at one of the set's sizes. */
export function SystemIcon({ name, size = 'md', fallback, className }: { name: SystemIconName; size?: IconSize | number; fallback?: ReactNode; className?: string }) {
const [failed, setFailed] = useState(false)
const icon = iconByName(name)
@ -55,14 +51,12 @@ export function Chip({ tone, icon, selected, onClick, children, className }: { t
: <span className={cx('fx-chip', selected && 'is-on', className)} style={style}>{inner}</span>
}
/** A coral count bubble that sits on a tile, a dock item or a bell; a dot when the number does not matter. */
export function Badge({ count, label }: { count?: number; label?: string }) {
return count === undefined
? <span className="fx-badge is-dot" role="status" aria-label={label}/>
: <span className="fx-badge" role="status" aria-label={label}>{count > 99 ? '99+' : count}</span>
}
/** A gold-rimmed capsule for a small upper-case word beside a name (Beta, New, Admin). */
export function Tag({ children }: { children: ReactNode }) {
return <span className="fx-tag">{children}</span>
}
@ -76,7 +70,6 @@ export function Card({ eyebrow, title, children, className }: { eyebrow?: string
</article>
}
/** A gold-rimmed glass panel with a title row (icon, title, actions) and a body. */
export function Panel({ title, icon, actions, children, className, style }: { title: ReactNode; icon?: SystemIconName; actions?: ReactNode; children?: ReactNode; className?: string; style?: CSSProperties }) {
return <section className={cx('fx-panel', className)} style={style}>
<header className="fx-panel-head">{icon && <SystemIcon name={icon} size={30}/>}<h2>{title}</h2>{actions}</header>
@ -104,7 +97,6 @@ export function Window({ title, icon, onClose, onMinimize, closeLabel = 'Close',
export type TabItem = { id: string; label: string; icon?: SystemIconName }
/** A row of pill tabs in a sunken track; the chosen one is gold. */
export function Tabs({ items, value, onChange, label }: { items: readonly TabItem[]; value: string; onChange: (id: string) => void; label: string }) {
const move = (from: number, step: number) => onChange(items[(from + step + items.length) % items.length]!.id)
return <div className="fx-tabs" role="tablist" aria-label={label}>
@ -120,7 +112,6 @@ export function List({ children, label }: { children: ReactNode; label?: string
return <ul className="fx-list" aria-label={label}>{children}</ul>
}
/** One row of a list; a button when it does something, a link when it goes somewhere. */
export function ListRow({ icon, title, detail, end, onClick, href, selected }: { icon?: SystemIconName; title: ReactNode; detail?: ReactNode; end?: ReactNode; onClick?: () => void; href?: string; selected?: boolean }) {
const inner = <>{icon && <SystemIcon name={icon} size={36}/>}<span className="fx-row-text"><b>{title}</b>{detail && <small>{detail}</small>}</span>{end !== undefined && <span className="fx-row-end">{end}</span>}</>
return <li>{href ? <a className={cx('fx-row', selected && 'is-on')} href={href}>{inner}</a>
@ -128,7 +119,6 @@ export function ListRow({ icon, title, detail, end, onClick, href, selected }: {
: <div className={cx('fx-row', selected && 'is-on')}>{inner}</div>}</li>
}
/** A labelled text field in a sunken pill, with an optional hint under it. */
export function Field({ label, hint, className, ...input }: { label: string; hint?: string } & InputHTMLAttributes<HTMLInputElement>) {
const id = useId()
return <label className={cx('fx-field', className)} htmlFor={id}>
@ -138,7 +128,6 @@ export function Field({ label, hint, className, ...input }: { label: string; hin
</label>
}
/** An on and off switch with a pearl knob; gold when on. */
export function Toggle({ label, checked, onChange, disabled }: { label: ReactNode; checked: boolean; onChange: (checked: boolean) => void; disabled?: boolean }) {
return <label className="fx-toggle">
<input type="checkbox" role="switch" checked={checked} disabled={disabled} onChange={event => onChange(event.target.checked)}/>
@ -161,7 +150,6 @@ export function Tooltip({ title, icon, text, rows, children, open }: { title: Re
</span>
}
/** A notice that slides in at the edge: an icon in a coloured bubble, a title, a line and a close. */
export function Toast({ title, children, icon, tone, onClose, closeLabel = 'Close' }: { title: ReactNode; children?: ReactNode; icon?: SystemIconName; tone?: string; onClose?: () => void; closeLabel?: string }) {
return <div className="fx-toast" role="status" style={tone ? { '--tone': tone } as CSSProperties : undefined}>
{icon && <span className="fx-toast-art"><SystemIcon name={icon} size={32}/></span>}
@ -186,7 +174,6 @@ export function DockTile({ icon, title, hue, badge, open, onClick }: { icon: Sys
</button>
}
/** A dock: a gold-rimmed glass shelf of icons; the app in front wears a gold bar, open ones a dot. */
export function Dock({ children, label }: { children: ReactNode; label: string }) {
return <nav className="fx-dock" aria-label={label}>{children}</nav>
}