W0 web foundation: Vite + React SPA under /app/, sync-pull ledger, auth, tokens, shared components
Build Kvitt images / build (push) Failing after 1h14m5s
Build Kvitt images / build (push) Failing after 1h14m5s
web/ is the browser client's scaffold: Vite 8, React 19, TypeScript 5.9 strict, React Router 7 (basename /app, every fixed route with a lazy placeholder page), TanStack Query 5 (refetch on focus and every 60 s), openapi-typescript types generated from contracts/openapi.yaml with openapi-fetch. Reads go through GET /sync (no REST reads exist for the ledger, SPEC §7): useLedger() pulls from cursor 0 and then incrementally, restarting on a new epoch or cursor_ahead. Login pairs the browser as "Web · <browser>" with an invite code (E-3); every call sends the SPEC §14.3.1 headers, a 401 signs out. kvitt-domain's split, balances, allocation and settlement rules are ported to web/src/lib and pass every case of contracts/test-vectors. Design tokens from design-spec §2 in tokens.css (light-dark()), Material Symbols Rounded (Apache-2.0) as generated inline SVG, sv/en catalogs generated from docs/ux/strings.csv plus web-strings.csv. Gate: cd web && npm run check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { Icon } from '../design/Icon';
|
||||
import { CATEGORY_ICON_TOKENS, type CategoryIconToken } from '../design/icons.generated';
|
||||
import styles from './CategoryIcon.module.css';
|
||||
|
||||
/** Colour tokens of SPEC §9.7 (14, `slate` and `amber` by D-054). */
|
||||
export const COLOR_TOKENS = [
|
||||
'green',
|
||||
'orange',
|
||||
'brown',
|
||||
'yellow',
|
||||
'blue',
|
||||
'teal',
|
||||
'purple',
|
||||
'red',
|
||||
'pink',
|
||||
'grey',
|
||||
'indigo',
|
||||
'lime',
|
||||
'slate',
|
||||
'amber',
|
||||
] as const;
|
||||
export type ColorToken = (typeof COLOR_TOKENS)[number];
|
||||
|
||||
export function isColorToken(value: string): value is ColorToken {
|
||||
return (COLOR_TOKENS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export function isCategoryIconToken(value: string): value is CategoryIconToken {
|
||||
return (CATEGORY_ICON_TOKENS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
/** A §9.7 icon token; an unknown token shows `category`. */
|
||||
icon: string;
|
||||
/** A §9.7 colour token; an unknown token shows `grey`. */
|
||||
color: string;
|
||||
/** px; design-spec §3.4 sizes 32 to 64 (40 in ledger rows). */
|
||||
size?: number;
|
||||
/** Solid: colour fill, on-cat icon. Container: tinted fill, coloured icon (unselected picker tiles, settings rows). */
|
||||
variant?: 'solid' | 'container';
|
||||
archived?: boolean;
|
||||
/** Announced name; decorative when omitted (the category name is printed beside it). */
|
||||
label?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** design-spec §3.4 `CategoryTile`: rounded tile (30 %), filled icon at 52 % of the tile. */
|
||||
export function CategoryIcon({ icon, color, size = 40, variant = 'solid', archived = false, label, className }: Props) {
|
||||
const token: CategoryIconToken = isCategoryIconToken(icon) ? icon : 'category';
|
||||
const colour: ColorToken = isColorToken(color) ? color : 'grey';
|
||||
const style =
|
||||
variant === 'solid'
|
||||
? { width: size, height: size, backgroundColor: `var(--cat-${colour})`, color: 'var(--on-cat)' }
|
||||
: { width: size, height: size, backgroundColor: `var(--cat-${colour}-container)`, color: `var(--cat-${colour})` };
|
||||
return (
|
||||
<span
|
||||
className={[styles.tile, archived ? styles.archived : undefined, className].filter(Boolean).join(' ')}
|
||||
style={style}
|
||||
data-color={colour}
|
||||
{...(label === undefined ? { 'aria-hidden': true } : { role: 'img', 'aria-label': label })}
|
||||
>
|
||||
<Icon name={token} filled size={Math.round(size * 0.52)} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user