strata-react 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +108 -2
- package/dist/components/app-shell.d.ts +29 -0
- package/dist/components/async-button.d.ts +17 -0
- package/dist/components/button.d.ts +16 -0
- package/dist/components/data-table.d.ts +38 -0
- package/dist/components/data-view.d.ts +30 -0
- package/dist/components/done.d.ts +13 -0
- package/dist/components/field.d.ts +29 -0
- package/dist/components/line-chart.d.ts +35 -0
- package/dist/components/metric.d.ts +15 -0
- package/dist/components/page-header.d.ts +13 -0
- package/dist/components/status.d.ts +8 -0
- package/dist/components/tabs.d.ts +15 -0
- package/dist/components/undo.d.ts +18 -0
- package/dist/components/virtual-list.d.ts +11 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1085 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/format.d.ts +28 -0
- package/dist/lib/icon.d.ts +6 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/query/index.d.ts +24 -0
- package/dist/query-BXH-Gfj7.js +69 -0
- package/dist/query-BXH-Gfj7.js.map +1 -0
- package/dist/strata.css +181 -0
- package/dist/testing/index.d.ts +64 -0
- package/dist/testing.js +32 -0
- package/dist/testing.js.map +1 -0
- package/package.json +124 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["_Fragment"],"sources":["../src/lib/utils.ts","../src/lib/format.ts","../src/components/button.tsx","../src/components/async-button.tsx","../src/components/field.tsx","../src/components/status.tsx","../src/components/metric.tsx","../src/components/data-view.tsx","../src/components/data-table.tsx","../src/components/virtual-list.tsx","../src/components/line-chart.tsx","../src/components/page-header.tsx","../src/components/undo.tsx","../src/components/done.tsx","../src/components/app-shell.tsx","../src/components/tabs.tsx"],"sourcesContent":["import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n// Teach tailwind-merge that `type-*` utilities are font sizes. Without this, cn() cannot tell\n// `type-label` from a colour and may drop one of two classes that both survive in CSS; with stock\n// shadcn the same bug drops `text-muted-foreground` next to a custom `text-*` size.\nexport const typeSteps = [\"caption\", \"label\", \"body-sm\", \"body\", \"heading\", \"title\", \"figure\", \"display\"] as const;\n\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n \"font-size\": [{ type: [...typeSteps] }],\n },\n },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","// Every number and date on screen goes through Intl, once, here. Units are joined with a\n// non-breaking space so \"212 °C\" never wraps between figure and unit. A product that is not\n// en-GB calls createFormat with its locale once and exports the result from its own lib.\n\nexport const NBSP = \" \";\n\nexport function createFormat(locale: string) {\n const integer = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 });\n const decimal = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 });\n const percent = new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 0 });\n const clock = new Intl.DateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" });\n const day = new Intl.DateTimeFormat(locale, { weekday: \"long\", day: \"numeric\", month: \"long\" });\n const shortDay = new Intl.DateTimeFormat(locale, { weekday: \"short\", hour: \"2-digit\", minute: \"2-digit\" });\n const withUnit = (value: number, unit: string, fmt: Intl.NumberFormat = integer) =>\n `${fmt.format(value)}${NBSP}${unit}`;\n return {\n locale,\n integer,\n decimal,\n percent,\n clock,\n day,\n shortDay,\n withUnit,\n celsius: (value: number) => withUnit(value, \"°C\"),\n /** \"1 piece\", \"6 pieces\": the count and its noun, so copy never reads \"1 pieces\". */\n count: (value: number, one: string, many = `${one}s`) =>\n `${integer.format(value)}${NBSP}${new Intl.PluralRules(locale).select(value) === \"one\" ? one : many}`,\n };\n}\n\nexport type Format = ReturnType<typeof createFormat>;\n\nexport const format = createFormat(\"en-GB\");\n","import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type { IconComponent } from \"../lib/icon\";\nimport { cn } from \"../lib/utils\";\n\n// Variant names are shadcn's, so copied code and agents' prior knowledge still work. Two things\n// differ on purpose:\n// 1. The default variant is `outline`, not the filled accent. A bare <Button> is the restrained\n// one; the accent (`variant=\"default\"`) has to be asked for, once per view. Generated app screens\n// put the accent on every button; 12 of 23 real product screens had no filled primary at all.\n// 2. Heights come from --control-h, shared with Input and NativeSelect, so density changes move\n// every control together.\n// 3. `press`: the button gives 2% under a click or a finger, over 100ms. Buttons are used tens of\n// times a day, so that is all the motion they get (character.md, the frequency budget).\nexport const buttonVariants = cva(\n \"inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-control type-label transition-state press disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary-10\",\n outline: \"border border-input bg-background text-foreground hover:bg-muted\",\n secondary: \"bg-muted text-foreground hover:bg-neutral-4\",\n ghost: \"text-foreground hover:bg-muted\",\n destructive: \"bg-destructive text-destructive-foreground hover:bg-danger\",\n link: \"h-auto px-0 text-primary-11 underline underline-offset-4 hover:text-primary-12\",\n },\n size: {\n default: \"h-(--control-h) px-4\",\n sm: \"h-(--control-h-sm) px-3\",\n icon: \"size-(--control-h)\",\n },\n },\n defaultVariants: { variant: \"outline\", size: \"default\" },\n },\n);\n\nexport type ButtonProps = React.ComponentProps<\"button\"> &\n VariantProps<typeof buttonVariants> & { asChild?: boolean };\n\nexport function Button({ className, variant, size, asChild = false, type = \"button\", ...props }: ButtonProps) {\n const Comp = asChild ? Slot : \"button\";\n return <Comp type={asChild ? undefined : type} className={cn(buttonVariants({ variant, size }), className)} {...props} />;\n}\n\n// An icon-only button cannot be written without a name: `label` is required by the type, becomes\n// the accessible name, and doubles as the native tooltip. This makes the most common generated\n// accessibility defect (an unlabelled icon button) a compile error instead of an audit finding.\nexport type IconButtonProps = Omit<ButtonProps, \"size\" | \"children\" | \"aria-label\"> & {\n icon: IconComponent;\n label: string;\n};\n\nexport function IconButton({ icon: Icon, label, variant = \"ghost\", ...props }: IconButtonProps) {\n return (\n <Button size=\"icon\" variant={variant} aria-label={label} title={label} {...props}>\n <Icon aria-hidden />\n </Button>\n );\n}\n","import * as React from \"react\";\nimport { Check, CircleAlert, LoaderCircle } from \"lucide-react\";\nimport { Button, type ButtonProps } from \"./button\";\n\n// One object through its states: \"Book firing\" becomes \"Booking…\" becomes \"Booked\", in the same\n// button, rather than the button being swapped for a spinner. The labels are stacked in one grid\n// cell so the button is always as wide as its longest label and nothing around it shifts. The\n// button stays enabled until the request starts (so a person can always try), and the outcome is\n// announced politely for screen readers.\n\nexport type Phase = \"idle\" | \"pending\" | \"done\" | \"error\";\n\nexport type AsyncLabels = {\n idle: string;\n /** Ends in an ellipsis: \"Saving…\", \"Booking…\". */\n pending: string;\n /** The same verb, finished: \"Saved\", \"Booked\". */\n done: string;\n error?: string;\n};\n\nexport function AsyncButton({\n phase,\n labels,\n disabled,\n ...props\n}: Omit<ButtonProps, \"children\"> & { phase: Phase; labels: AsyncLabels }) {\n const errorLabel = labels.error ?? \"Try again\";\n const current = { idle: labels.idle, pending: labels.pending, done: labels.done, error: errorLabel }[phase];\n const Icon = { idle: null, pending: LoaderCircle, done: Check, error: CircleAlert }[phase];\n\n return (\n <>\n <Button {...props} disabled={disabled || phase === \"pending\"} aria-disabled={phase === \"pending\" || undefined}>\n {Icon && <Icon aria-hidden className={phase === \"pending\" ? \"motion-safe:animate-spin\" : undefined} />}\n <span className=\"grid\">\n {[labels.idle, labels.pending, labels.done, errorLabel].map((l) => (\n <span key={l} aria-hidden={l !== current} className={l === current ? \"[grid-area:1/1]\" : \"invisible [grid-area:1/1]\"}>\n {l}\n </span>\n ))}\n </span>\n </Button>\n <span role=\"status\" className=\"sr-only\">\n {phase === \"done\" ? labels.done : phase === \"error\" ? errorLabel : \"\"}\n </span>\n </>\n );\n}\n\n/** Runs an async action and reports its phase. `done` returns to `idle` after `settleMs`. */\nexport function useAsyncAction<A extends unknown[]>(action: (...args: A) => Promise<void>, settleMs = 2000) {\n const [phase, setPhase] = React.useState<Phase>(\"idle\");\n const timer = React.useRef<number | undefined>(undefined);\n React.useEffect(() => () => window.clearTimeout(timer.current), []);\n const run = React.useCallback(\n async (...args: A) => {\n window.clearTimeout(timer.current);\n setPhase(\"pending\");\n try {\n await action(...args);\n setPhase(\"done\");\n timer.current = window.setTimeout(() => setPhase(\"idle\"), settleMs);\n } catch {\n setPhase(\"error\");\n }\n },\n [action, settleMs],\n );\n return [phase, run] as const;\n}\n","import * as React from \"react\";\nimport { CircleAlert } from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\n\n// Field owns the wiring a generated form usually forgets: the label's htmlFor, aria-describedby\n// pointing at the hint and the error, and aria-invalid. Controls read it from context, so a control\n// inside a Field cannot end up unlabelled or silently invalid.\n\ntype FieldState = { id: string; describedBy?: string; invalid: boolean };\nconst FieldContext = React.createContext<FieldState | null>(null);\n\nfunction useField(id?: string) {\n const field = React.useContext(FieldContext);\n return {\n // Inside a Field the Field's id wins, so the label always points at the control. To choose the\n // id yourself, pass it to Field.\n id: field?.id ?? id,\n \"aria-describedby\": field?.describedBy,\n \"aria-invalid\": field?.invalid || undefined,\n };\n}\n\nexport type FieldProps = {\n label: string;\n /** The control's id, when the page needs a known one (an anchor, a test). Generated otherwise. */\n id?: string;\n /** What the person needs to know before typing, such as limits or units. */\n hint?: React.ReactNode;\n /** What went wrong and how to fix it, in one sentence. Shown under the control, never in a toast. */\n error?: string;\n children: React.ReactNode;\n className?: string;\n};\n\nexport function Field({ label, id: idProp, hint, error, children, className }: FieldProps) {\n const generated = React.useId();\n const id = idProp ?? generated;\n const hintId = hint ? `${id}-hint` : undefined;\n const errorId = error ? `${id}-error` : undefined;\n const describedBy = [errorId, hintId].filter(Boolean).join(\" \") || undefined;\n\n return (\n <FieldContext.Provider value={{ id, describedBy, invalid: Boolean(error) }}>\n <div className={cn(\"grid min-w-0 content-start gap-1.5\", className)}>\n <label htmlFor={id} className=\"type-label text-foreground\">\n {label}\n </label>\n {children}\n {error && (\n <p id={errorId} className=\"flex items-start gap-1.5 type-body-sm text-danger\">\n <CircleAlert aria-hidden className=\"mt-0.5 size-4 shrink-0\" />\n {error}\n </p>\n )}\n {hint && (\n <p id={hintId} className=\"type-body-sm text-muted-foreground\">\n {hint}\n </p>\n )}\n </div>\n </FieldContext.Provider>\n );\n}\n\nconst control =\n \"w-full min-w-0 rounded-control border border-input bg-background px-3 type-body text-foreground transition-state placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:bg-muted disabled:opacity-70 aria-invalid:border-danger\";\n\nexport function Input({ className, id, ...props }: React.ComponentProps<\"input\">) {\n return <input {...useField(id)} className={cn(control, \"h-(--control-h)\", className)} {...props} />;\n}\n\nexport function Textarea({ className, id, ...props }: React.ComponentProps<\"textarea\">) {\n return <textarea {...useField(id)} className={cn(control, \"min-h-24 py-2\", className)} {...props} />;\n}\n\n// A native select, styled. For short option lists on a touch screen it beats a custom listbox:\n// the platform picker, typeahead and form autofill all work with no code.\nexport function NativeSelect({ className, id, ...props }: React.ComponentProps<\"select\">) {\n return <select {...useField(id)} className={cn(control, \"h-(--control-h) pr-8\", className)} {...props} />;\n}\n\n// Radio choices: a fieldset with a legend, and each label wrapping its control so the whole row is\n// one hit target. accent-color carries the theme into the native control.\nexport type Choice = { value: string; label: string; detail?: string };\n\nexport function ChoiceGroup({\n legend,\n name,\n choices,\n value,\n onChange,\n error,\n}: {\n legend: string;\n name: string;\n choices: Choice[];\n value: string;\n onChange: (value: string) => void;\n error?: string;\n}) {\n const errorId = React.useId();\n return (\n <fieldset className=\"grid gap-2\" aria-describedby={error ? errorId : undefined}>\n <legend className=\"mb-1.5 type-label text-foreground\">{legend}</legend>\n {choices.map((c) => (\n <label key={c.value} className=\"flex min-h-(--control-h-sm) cursor-pointer items-start gap-3 py-1\">\n <input\n type=\"radio\"\n name={name}\n value={c.value}\n checked={value === c.value}\n onChange={() => onChange(c.value)}\n className=\"mt-1 size-4 shrink-0 accent-primary\"\n />\n <span className=\"grid\">\n <span className=\"type-body\">{c.label}</span>\n {c.detail && <span className=\"type-body-sm text-muted-foreground\">{c.detail}</span>}\n </span>\n </label>\n ))}\n {error && (\n <p id={errorId} className=\"flex items-start gap-1.5 type-body-sm text-danger\">\n <CircleAlert aria-hidden className=\"mt-0.5 size-4 shrink-0\" />\n {error}\n </p>\n )}\n </fieldset>\n );\n}\n","import type { IconComponent } from \"../lib/icon\";\nimport { cn } from \"../lib/utils\";\n\n// Status is a word first. `children` is required and must be a string, so a status can never be a\n// bare coloured dot. The tint and ink come from the fixed status roles, which are kept apart from\n// the brand accent: pale tint, dark ink of the same hue, never a saturated chip with white text.\n// No pulsing variant exists; nothing on a status mark moves unless the data behind it is live.\n\nexport type Tone = \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\";\n\nconst tones: Record<Tone, string> = {\n neutral: \"bg-muted text-muted-foreground\",\n info: \"bg-info-subtle text-info\",\n success: \"bg-success-subtle text-success\",\n warning: \"bg-warning-subtle text-warning\",\n danger: \"bg-danger-subtle text-danger\",\n};\n\nexport function Status({\n tone,\n icon: Icon,\n children,\n className,\n}: {\n tone: Tone;\n icon?: IconComponent;\n children: string;\n className?: string;\n}) {\n return (\n <span\n className={cn(\n \"inline-flex items-center gap-1.5 whitespace-nowrap rounded-pill py-0.5 pr-2.5 pl-2 type-caption font-medium\",\n tones[tone],\n className,\n )}\n >\n {Icon && <Icon aria-hidden className=\"size-3.5 shrink-0\" />}\n {children}\n </span>\n );\n}\n","import * as React from \"react\";\nimport { cn } from \"../lib/utils\";\n\n// Headline figures as one container split by hairlines, label over value: what real product screens\n// do, instead of four cards each with an icon in a tinted circle. There is deliberately no `icon`,\n// `sparkline` or `trend` prop. A change is written in words with its window (\"4 more than last\n// Thursday\"), because a green \"+12.5%\" pill says neither what changed nor since when, and the trend\n// gets its own chart.\n\nexport function MetricStrip({ label, children, className }: { label: string; children: React.ReactNode; className?: string }) {\n return (\n <dl\n aria-label={label}\n className={cn(\n \"grid grid-cols-1 border-y border-border sm:auto-cols-fr sm:grid-flow-col sm:divide-x sm:divide-border\",\n className,\n )}\n >\n {children}\n </dl>\n );\n}\n\nexport type MetricProps = {\n label: string;\n /** Already formatted. Ink colour, 28px, tabular figures: never the brand colour, never 48px. */\n value: string;\n unit?: string;\n /** One sentence: the change in words with its window, or what the figure means right now. */\n note?: React.ReactNode;\n};\n\nexport function Metric({ label, value, unit, note }: MetricProps) {\n return (\n <div className=\"grid content-start gap-0.5 border-border py-3 not-first:border-t sm:px-5 sm:not-first:border-t-0 sm:first:pl-0\">\n <dt className=\"type-label text-muted-foreground\">{label}</dt>\n <dd className=\"type-figure text-foreground\">\n {value}\n {unit && <span className=\"type-body text-muted-foreground\">{\" \" + unit}</span>}\n </dd>\n {note && <dd className=\"type-body-sm text-muted-foreground\">{note}</dd>}\n </div>\n );\n}\n","import * as React from \"react\";\nimport { CircleAlert, RotateCcw } from \"lucide-react\";\nimport { Button } from \"./button\";\nimport { cn } from \"../lib/utils\";\n\n// Every view of remote data goes through DataView, and DataView will not type-check without a\n// skeleton and an empty state. Generated UI renders the happy path; this makes the other three\n// states part of the component's signature rather than a review comment.\n\nexport type Load<T> =\n | { status: \"loading\" }\n | { status: \"error\"; message: string; retry: () => void }\n | { status: \"ready\"; data: T };\n\nexport function DataView<T>({\n state,\n isEmpty,\n skeleton,\n empty,\n children,\n className,\n}: {\n state: Load<T>;\n /** When the loaded data counts as empty. For arrays, `(rows) => rows.length === 0`. */\n isEmpty: (data: T) => boolean;\n /** Mirrors the final layout, so nothing jumps when data arrives. Not a centred spinner. */\n skeleton: React.ReactNode;\n /** What goes here and how to add it. An <EmptyState>, not an illustration. */\n empty: React.ReactNode;\n children: (data: T) => React.ReactNode;\n className?: string;\n}) {\n return (\n <div className={cn(\"min-w-0\", className)} aria-busy={state.status === \"loading\" || undefined}>\n {state.status === \"loading\" && skeleton}\n {state.status === \"error\" && (\n <div role=\"alert\" className=\"flex flex-wrap items-start gap-3 rounded-surface bg-danger-subtle p-4 text-danger\">\n <CircleAlert aria-hidden className=\"mt-0.5 size-5 shrink-0\" />\n <p className=\"min-w-0 flex-1 type-body\">{state.message}</p>\n <Button size=\"sm\" onClick={state.retry}>\n <RotateCcw aria-hidden />\n Try again\n </Button>\n </div>\n )}\n {state.status === \"ready\" && (isEmpty(state.data) ? empty : children(state.data))}\n </div>\n );\n}\n\n// An empty state teaches the next action. No illustration slot exists, on purpose.\nexport function EmptyState({ title, children, action }: { title: string; children?: React.ReactNode; action?: React.ReactNode }) {\n return (\n <div className=\"grid justify-items-start gap-2 border-y border-border py-6\">\n <p className=\"type-heading\">{title}</p>\n {children && <p className=\"max-w-prose type-body text-muted-foreground\">{children}</p>}\n {action && <div className=\"mt-2\">{action}</div>}\n </div>\n );\n}\n\nexport function Skeleton({ className }: { className?: string }) {\n return <div aria-hidden className={cn(\"rounded-control bg-muted motion-safe:animate-pulse\", className)} />;\n}\n","import * as React from \"react\";\nimport { ArrowDown, ArrowUp, ArrowUpDown } from \"lucide-react\";\nimport {\n flexRender,\n getCoreRowModel,\n getSortedRowModel,\n useReactTable,\n type ColumnDef,\n type SortingState,\n} from \"@tanstack/react-table\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { useSearchParams } from \"react-router\";\nimport { cn } from \"../lib/utils\";\n\n// The table real product screens use: 12px muted sentence-case headers, row rules (no zebra),\n// numbers right-aligned in tabular figures, one status mark and at most one action per row.\n// Sortable headers are buttons and look sortable; aria-sort says which way. The scroll container is\n// a labelled, focusable region so keyboard users can scroll a wide table.\n//\n// Underneath: TanStack Table for the row model and sorting, TanStack Virtual once a table passes\n// `virtualizeFrom` rows (only the visible rows are in the DOM; the header sticks). The column API\n// stays small on purpose (key, header, cell, numeric, sort), so a screen reads as a list of\n// decisions about columns, not as table plumbing. `sortParam` keeps the sort in the URL\n// (?sort=when.desc) so a sorted view can be linked and survives a reload.\n\nexport type Column<T> = {\n key: string;\n header: string;\n cell: (row: T) => React.ReactNode;\n /** Right-aligns and sets tabular figures. Use for every quantity. */\n numeric?: boolean;\n sort?: (a: T, b: T) => number;\n /** Cells that may hold long text get truncated rather than stretching the table. */\n truncate?: boolean;\n className?: string;\n};\n\nexport type Sort = { key: string; dir: \"ascending\" | \"descending\" };\n\nexport type DataTableProps<T> = {\n /** Names the table for screen readers and labels the scroll region. */\n caption: string;\n columns: Column<T>[];\n rows: T[];\n rowKey: (row: T) => string;\n initialSort?: Sort;\n /** Keep the sort in this URL search parameter. Needs a React Router context. */\n sortParam?: string;\n /** Row count from which only the visible rows render. Default 100. */\n virtualizeFrom?: number;\n /** Height of the scroll region once virtualised, in px. Default 480. */\n maxHeight?: number;\n className?: string;\n};\n\ntype SortState = [Sort | undefined, (next: Sort) => void];\n\nfunction useLocalSort(initial?: Sort): SortState {\n const [sort, setSort] = React.useState(initial);\n return [sort, setSort];\n}\n\nexport function parseSort(value: string | null): Sort | undefined {\n if (!value) return undefined;\n const [key, dir] = value.split(\".\");\n if (!key) return undefined;\n return { key, dir: dir === \"desc\" ? \"descending\" : \"ascending\" };\n}\n\nexport function serialiseSort(sort: Sort) {\n return `${sort.key}.${sort.dir === \"descending\" ? \"desc\" : \"asc\"}`;\n}\n\nfunction useUrlSort(param: string, initial?: Sort): SortState {\n const [params, setParams] = useSearchParams();\n const sort = parseSort(params.get(param)) ?? initial;\n const set = React.useCallback(\n (next: Sort) =>\n setParams(\n (p) => {\n const out = new URLSearchParams(p);\n out.set(param, serialiseSort(next));\n return out;\n },\n { replace: true },\n ),\n [param, setParams],\n );\n return [sort, set];\n}\n\nexport function DataTable<T>(props: DataTableProps<T>) {\n // Two thin wrappers so each calls its hooks unconditionally.\n return props.sortParam ? <UrlSortedTable {...props} sortParam={props.sortParam} /> : <LocalSortedTable {...props} />;\n}\n\nfunction LocalSortedTable<T>(props: DataTableProps<T>) {\n return <Table {...props} sortState={useLocalSort(props.initialSort)} />;\n}\n\nfunction UrlSortedTable<T>(props: DataTableProps<T> & { sortParam: string }) {\n return <Table {...props} sortState={useUrlSort(props.sortParam, props.initialSort)} />;\n}\n\ntype Meta = { numeric?: boolean; truncate?: boolean; className?: string };\n\nfunction Table<T>({\n caption,\n columns,\n rows,\n rowKey,\n sortState: [sort, setSort],\n virtualizeFrom = 100,\n maxHeight = 480,\n className,\n}: DataTableProps<T> & { sortState: SortState }) {\n const defs = React.useMemo<ColumnDef<T>[]>(\n () =>\n columns.map((c) => ({\n id: c.key,\n // TanStack only sorts columns with an accessor; the row itself is enough, since the\n // column's own comparator does the work.\n accessorFn: (row) => row,\n header: c.header,\n cell: (info) => c.cell(info.row.original),\n enableSorting: Boolean(c.sort),\n sortingFn: c.sort ? (a, b) => c.sort!(a.original, b.original) : undefined,\n meta: { numeric: c.numeric, truncate: c.truncate, className: c.className } satisfies Meta,\n })),\n [columns],\n );\n\n const sorting: SortingState = sort && columns.some((c) => c.key === sort.key && c.sort)\n ? [{ id: sort.key, desc: sort.dir === \"descending\" }]\n : [];\n\n const table = useReactTable({\n data: rows,\n columns: defs,\n state: { sorting },\n getRowId: (row) => rowKey(row),\n getCoreRowModel: getCoreRowModel(),\n getSortedRowModel: getSortedRowModel(),\n });\n\n const toggle = (key: string) =>\n setSort({ key, dir: sort?.key === key && sort.dir === \"ascending\" ? \"descending\" : \"ascending\" });\n\n const modelRows = table.getRowModel().rows;\n const virtual = modelRows.length >= virtualizeFrom;\n const scroller = React.useRef<HTMLDivElement>(null);\n const virtualizer = useVirtualizer({\n count: virtual ? modelRows.length : 0,\n getScrollElement: () => scroller.current,\n estimateSize: () => 41,\n overscan: 8,\n // Until the region is measured, assume it is full height: the first paint (and a test DOM\n // with no layout) shows the first page of rows instead of none.\n initialRect: { width: 0, height: maxHeight },\n });\n const items = virtual ? virtualizer.getVirtualItems() : [];\n const padTop = items.length ? items[0].start : 0;\n const padBottom = items.length ? virtualizer.getTotalSize() - items[items.length - 1].end : 0;\n const visible = virtual ? items.map((i) => modelRows[i.index]) : modelRows;\n\n return (\n <div\n ref={scroller}\n role=\"region\"\n aria-label={caption}\n tabIndex={0}\n className={cn(\"min-w-0 overflow-x-auto\", virtual && \"overflow-y-auto\", className)}\n style={virtual ? { maxHeight } : undefined}\n >\n <table className=\"w-full border-collapse type-body-sm\" aria-rowcount={virtual ? modelRows.length + 1 : undefined}>\n <caption className=\"sr-only\">{caption}</caption>\n <thead className={cn(virtual && \"sticky top-0 z-10 bg-background\")}>\n {table.getHeaderGroups().map((group) => (\n <tr key={group.id}>\n {group.headers.map((header) => {\n const meta = header.column.columnDef.meta as Meta | undefined;\n const active = sort?.key === header.id && header.column.getCanSort() ? sort.dir : undefined;\n const Icon = active === \"ascending\" ? ArrowUp : active === \"descending\" ? ArrowDown : ArrowUpDown;\n const label = flexRender(header.column.columnDef.header, header.getContext());\n return (\n <th\n key={header.id}\n scope=\"col\"\n aria-sort={active}\n className={cn(\n \"py-2 pr-4 text-left align-bottom type-caption text-muted-foreground last:pr-0\",\n meta?.numeric && \"text-right\",\n )}\n >\n {header.column.getCanSort() ? (\n <button\n type=\"button\"\n onClick={() => toggle(header.id)}\n className={cn(\n \"-mx-1 inline-flex cursor-pointer items-center gap-1 rounded-control px-1 transition-state hover:text-foreground\",\n meta?.numeric && \"flex-row-reverse\",\n active && \"text-foreground\",\n )}\n >\n {label}\n <Icon aria-hidden className={cn(\"size-3.5\", !active && \"opacity-50\")} />\n </button>\n ) : (\n label\n )}\n </th>\n );\n })}\n </tr>\n ))}\n </thead>\n <tbody>\n {padTop > 0 && <tr aria-hidden style={{ height: padTop }} />}\n {visible.map((row, i) => (\n <tr\n key={row.id}\n className=\"border-t border-border\"\n aria-rowindex={virtual ? (items[i]?.index ?? 0) + 2 : undefined}\n >\n {row.getVisibleCells().map((cell) => {\n const meta = cell.column.columnDef.meta as Meta | undefined;\n return (\n <td\n key={cell.id}\n className={cn(\n \"py-2.5 pr-4 align-middle last:pr-0\",\n meta?.numeric && \"text-right tabular-nums\",\n meta?.truncate && \"max-w-48 truncate\",\n meta?.className,\n )}\n >\n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n </td>\n );\n })}\n </tr>\n ))}\n {padBottom > 0 && <tr aria-hidden style={{ height: padBottom }} />}\n </tbody>\n </table>\n </div>\n );\n}\n\nexport function TableSkeleton({ rows = 4, columns = 5 }: { rows?: number; columns?: number }) {\n return (\n <div aria-hidden className=\"grid gap-0\">\n <div className=\"flex gap-4 py-2\">\n {Array.from({ length: columns }, (_, i) => (\n <div key={i} className=\"h-3 flex-1 rounded-control bg-muted\" />\n ))}\n </div>\n {Array.from({ length: rows }, (_, r) => (\n <div key={r} className=\"flex gap-4 border-t border-border py-3.5\">\n {Array.from({ length: columns }, (_, i) => (\n <div key={i} className=\"h-4 flex-1 rounded-control bg-muted motion-safe:animate-pulse\" />\n ))}\n </div>\n ))}\n </div>\n );\n}\n","import * as React from \"react\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { cn } from \"../lib/utils\";\n\n// A long list that keeps only its visible rows in the DOM (TanStack Virtual), for feeds, logs and\n// pickers with hundreds of items. Rows are measured after render, so variable heights work; the\n// estimate only sets the first paint. The list is a labelled, focusable region so it can be\n// scrolled from the keyboard, and keeps list semantics with aria-setsize/aria-posinset so screen\n// readers still hear \"12 of 480\". Under `virtualizeFrom` items it renders a plain list.\n\nexport function VirtualList<T>({\n label,\n items,\n itemKey,\n children,\n estimateSize = 48,\n height = 480,\n virtualizeFrom = 100,\n className,\n}: {\n label: string;\n items: T[];\n itemKey: (item: T) => string;\n children: (item: T, index: number) => React.ReactNode;\n estimateSize?: number;\n height?: number;\n virtualizeFrom?: number;\n className?: string;\n}) {\n const scroller = React.useRef<HTMLDivElement>(null);\n const virtual = items.length >= virtualizeFrom;\n const virtualizer = useVirtualizer({\n count: virtual ? items.length : 0,\n getScrollElement: () => scroller.current,\n estimateSize: () => estimateSize,\n overscan: 6,\n initialRect: { width: 0, height },\n });\n\n if (!virtual) {\n return (\n <ul aria-label={label} className={cn(\"grid min-w-0\", className)}>\n {items.map((item, i) => (\n <li key={itemKey(item)} className=\"border-t border-border first:border-t-0\">\n {children(item, i)}\n </li>\n ))}\n </ul>\n );\n }\n\n return (\n <div\n ref={scroller}\n role=\"region\"\n aria-label={label}\n tabIndex={0}\n className={cn(\"min-w-0 overflow-y-auto\", className)}\n style={{ height }}\n >\n <ul aria-label={label} className=\"relative w-full\" style={{ height: virtualizer.getTotalSize() }}>\n {virtualizer.getVirtualItems().map((v) => {\n const item = items[v.index];\n return (\n <li\n key={itemKey(item)}\n ref={virtualizer.measureElement}\n data-index={v.index}\n aria-setsize={items.length}\n aria-posinset={v.index + 1}\n className={cn(\"absolute inset-x-0 top-0\", v.index > 0 && \"border-t border-border\")}\n style={{ translate: `0 ${v.start}px` }}\n >\n {children(item, v.index)}\n </li>\n );\n })}\n </ul>\n </div>\n );\n}\n","import * as React from \"react\";\nimport { cn } from \"../lib/utils\";\n\n// A line chart with the defaults real product screens use: one measured series in one hue, a 2px\n// straight line with no fill and no markers, 3 to 5 faint gridlines, no y-axis line, no legend for a\n// single series (the series is labelled where it ends). Values never live only in a hover tooltip:\n// the latest and peak readings are labelled on the chart and every reading is in a table for screen\n// readers. Optional extras exist only where they answer a question: a dashed `planned` series, a\n// horizontal `band` (a threshold that matters), and a `now` marker.\n//\n//\n// `colourScale` colours the measured line by its own value, for subjects where that colour is a\n// fact rather than decoration: a kiln's glow by temperature, water by depth. The y position still\n// carries the value, so the colour is never the only cue. Without a reason in DESIGN.md, use one\n// hue.\n//\n// It draws in real pixels (measured with ResizeObserver) rather than scaling a fixed viewBox, so\n// the 12px labels stay 12px on a phone.\n\nexport type Point = { x: number; y: number };\n\nexport type LineChartProps = {\n /** What the chart shows, as a sentence. Used as the accessible name and the table caption. */\n label: string;\n series: Point[];\n seriesLabel: string;\n planned?: Point[];\n plannedLabel?: string;\n band?: { below: number; label: string };\n formatX: (x: number) => string;\n formatY: (y: number) => string;\n xTicks: number[];\n /** CSS colour for the measured series. Defaults to the first chart token. */\n colour?: string;\n /** Colour the measured series by value: each stop applies from `at` upwards, blending to the\n * next. Only where the colour means something in the subject; record the reason in DESIGN.md. */\n colourScale?: { at: number; colour: string }[];\n /** Says what the colours mean, shown beside the legend swatch when `colourScale` is set. */\n colourScaleLabel?: string;\n height?: number;\n className?: string;\n};\n\nfunction useWidth<T extends HTMLElement>() {\n const ref = React.useRef<T>(null);\n const [width, setWidth] = React.useState(0);\n React.useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n setWidth(el.getBoundingClientRect().width);\n const ro = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width));\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n return [ref, width] as const;\n}\n\n/** Four gridline steps of a size people write on a scale (1, 2, 2.5, 4 or 5 × 10ⁿ), ending above the max. */\nexport function niceMax(max: number) {\n // All-zero (or empty) data still needs a positive extent, or every coordinate divides by zero.\n if (!(max > 0)) return 4;\n const raw = max / 4;\n const mag = 10 ** Math.floor(Math.log10(raw));\n const step = [1, 2, 2.5, 4, 5, 10].map((m) => m * mag).find((s) => s >= raw) ?? 10 * mag;\n return step * 4;\n}\n\nexport function LineChart({\n label,\n series,\n seriesLabel,\n planned,\n plannedLabel,\n band,\n formatX,\n formatY,\n xTicks,\n colour = \"var(--chart-1)\",\n colourScale,\n colourScaleLabel,\n height = 280,\n className,\n}: LineChartProps) {\n const [ref, width] = useWidth<HTMLDivElement>();\n const gradientId = `heat-${React.useId().replace(/[^\\w-]/g, \"\")}`;\n const all = [...series, ...(planned ?? [])];\n const xMin = Math.min(...all.map((p) => p.x));\n const xMax = Math.max(...all.map((p) => p.x));\n const yMax = niceMax(Math.max(...all.map((p) => p.y)));\n const yTicks = [0, 0.25, 0.5, 0.75, 1].map((f) => f * yMax);\n\n const pad = { top: 16, right: 16, bottom: 28, left: 56 };\n const w = Math.max(0, width - pad.left - pad.right);\n const h = height - pad.top - pad.bottom;\n const sx = (x: number) => pad.left + ((x - xMin) / (xMax - xMin || 1)) * w;\n const sy = (y: number) => pad.top + h - (y / yMax) * h;\n const path = (pts: Point[]) => pts.map((p, i) => `${i ? \"L\" : \"M\"}${sx(p.x).toFixed(1)},${sy(p.y).toFixed(1)}`).join(\"\");\n\n const last = series[series.length - 1];\n const peak = series.reduce((a, b) => (b.y > a.y ? b : a), series[0]);\n const stops = colourScale ? [...colourScale].sort((a, b) => a.at - b.at) : [];\n /** The colour a value lands on: the highest stop at or below it. Used for the labelled points. */\n const colourAt = (y: number) => (stops.length ? [...stops].reverse().find((s) => s.at <= y)?.colour ?? stops[0].colour : colour);\n const stroke = stops.length ? `url(#${gradientId})` : colour;\n\n return (\n <figure className={cn(\"grid min-w-0 gap-2\", className)}>\n <div ref={ref} className=\"min-w-0\">\n {width > 0 && (\n <svg width={width} height={height} role=\"img\" aria-label={label} className=\"block overflow-visible\">\n {stops.length > 0 && (\n // A vertical gradient in chart space, so a colour sits at a temperature, not at a\n // point along the line: the line is the same red wherever it crosses 600 °C.\n <defs>\n <linearGradient id={gradientId} gradientUnits=\"userSpaceOnUse\" x1={0} x2={0} y1={sy(0)} y2={sy(yMax)}>\n {stops.map((s) => (\n <stop key={s.at} offset={Math.min(1, Math.max(0, s.at / yMax))} stopColor={s.colour} />\n ))}\n </linearGradient>\n </defs>\n )}\n {band && (\n <g>\n <rect x={pad.left} y={sy(band.below)} width={w} height={sy(0) - sy(band.below)} fill=\"var(--success-subtle)\" />\n <text x={pad.left + 8} y={sy(band.below) + 16} className=\"fill-success type-caption\">\n {band.label}\n </text>\n </g>\n )}\n {yTicks.map((t) => (\n <g key={t}>\n <line x1={pad.left} x2={pad.left + w} y1={sy(t)} y2={sy(t)} stroke=\"var(--border)\" strokeWidth={1} />\n <text x={pad.left - 8} y={sy(t)} dy=\"0.32em\" textAnchor=\"end\" className=\"fill-muted-foreground type-caption tabular-nums\">\n {formatY(t)}\n </text>\n </g>\n ))}\n {xTicks.map((t) => (\n <text key={t} x={sx(t)} y={height - 8} textAnchor=\"middle\" className=\"fill-muted-foreground type-caption tabular-nums\">\n {formatX(t)}\n </text>\n ))}\n {planned && (\n <path d={path(planned)} fill=\"none\" stroke=\"var(--muted-foreground)\" strokeWidth={1.5} strokeDasharray=\"4 4\" />\n )}\n <path d={path(series)} fill=\"none\" stroke={stroke} strokeWidth={stops.length ? 2.5 : 2} strokeLinejoin=\"round\" strokeLinecap=\"round\" />\n {/* Direct labels instead of a legend and a tooltip. */}\n <circle cx={sx(peak.x)} cy={sy(peak.y)} r={3} fill={colourAt(peak.y)} />\n <text x={sx(peak.x) + 8} y={sy(peak.y) - 8} className=\"fill-foreground type-caption tabular-nums\">\n {formatY(peak.y)} peak\n </text>\n <line x1={sx(last.x)} x2={sx(last.x)} y1={pad.top} y2={pad.top + h} stroke=\"var(--foreground)\" strokeWidth={1} />\n <circle cx={sx(last.x)} cy={sy(last.y)} r={3.5} fill={colourAt(last.y)} />\n <text\n x={sx(last.x) - 8}\n y={sy(last.y) - 10}\n textAnchor=\"end\"\n className=\"fill-foreground type-label tabular-nums\"\n >\n {formatY(last.y)} now\n </text>\n </svg>\n )}\n </div>\n <figcaption className=\"flex flex-wrap gap-x-5 gap-y-1 type-caption text-muted-foreground\">\n <span className=\"inline-flex items-center gap-2\">\n {stops.length ? (\n <svg width={stops.length * 8} height=\"3\" aria-hidden>\n {stops.map((s, i) => (\n <line key={s.at} x1={i * 8} x2={i * 8 + 8} y1=\"1.5\" y2=\"1.5\" stroke={s.colour} strokeWidth={3} />\n ))}\n </svg>\n ) : (\n <svg width=\"16\" height=\"2\" aria-hidden>\n <line x1=\"0\" x2=\"16\" y1=\"1\" y2=\"1\" stroke={colour} strokeWidth={2} />\n </svg>\n )}\n {seriesLabel}\n {stops.length > 0 && colourScaleLabel && <span>({colourScaleLabel})</span>}\n </span>\n {planned && plannedLabel && (\n <span className=\"inline-flex items-center gap-2\">\n <svg width=\"16\" height=\"2\" aria-hidden>\n <line x1=\"0\" x2=\"16\" y1=\"1\" y2=\"1\" stroke=\"var(--muted-foreground)\" strokeWidth={1.5} strokeDasharray=\"4 4\" />\n </svg>\n {plannedLabel}\n </span>\n )}\n </figcaption>\n {/* sr-only on a wrapper: a table ignores width: 1px and would widen the page. */}\n <div className=\"sr-only\">\n <table>\n <caption>{label}</caption>\n <thead>\n <tr>\n <th scope=\"col\">Time</th>\n <th scope=\"col\">{seriesLabel}</th>\n </tr>\n </thead>\n <tbody>\n {series.map((p) => (\n <tr key={p.x}>\n <td>{formatX(p.x)}</td>\n <td>{formatY(p.y)}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </figure>\n );\n}\n","import * as React from \"react\";\n\n// The top of every Operate screen: a noun title at 24px (never \"Welcome back\"), its scope, the\n// screen's main action, and the finding. The finding is the one fact in today's data that most\n// needs attention; it sits where it is read first, beside the action it drives.\n\nexport function PageHeader({\n title,\n scope,\n actions,\n finding,\n as: Heading = \"h1\",\n}: {\n title: string;\n /** The title's element: h1 on a screen, h2 where the header is shown inside another page. */\n as?: \"h1\" | \"h2\";\n /** What the screen covers: a date, a range, a filter. */\n scope?: React.ReactNode;\n actions?: React.ReactNode;\n finding?: { text: React.ReactNode; action?: React.ReactNode };\n}) {\n return (\n <header className=\"grid gap-4\">\n <div className=\"flex flex-wrap items-end justify-between gap-x-6 gap-y-3\">\n <div className=\"grid min-w-0 gap-0.5\">\n <Heading className=\"type-title text-balance\">{title}</Heading>\n {scope && <p className=\"type-body-sm text-muted-foreground\">{scope}</p>}\n </div>\n {actions && <div className=\"flex flex-wrap gap-2\">{actions}</div>}\n </div>\n {finding && (\n <div className=\"flex flex-wrap items-center justify-between gap-x-6 gap-y-3 border-y border-border py-3\">\n <p className=\"max-w-prose min-w-0 type-body text-pretty\">{finding.text}</p>\n {finding.action}\n </div>\n )}\n </header>\n );\n}\n","import * as React from \"react\";\nimport { Button } from \"./button\";\n\n// Destructive actions take effect visibly at once and commit after a window, with Undo in reach.\n// For reversible removals this beats a confirm dialog: the dialog interrupts every time to protect\n// against the rare mistake, and people learn to click through it. Keep a confirm dialog for\n// actions that cannot be undone (deleting an account, firing a kiln).\n\ntype Pending = { id: string; message: string; commit: () => void };\n\nexport function useUndoable(windowMs = 8000) {\n const [pending, setPending] = React.useState<Pending | null>(null);\n const current = React.useRef<Pending | null>(null);\n const timer = React.useRef<number | undefined>(undefined);\n\n const set = React.useCallback((p: Pending | null) => {\n current.current = p;\n setPending(p);\n }, []);\n\n const schedule = React.useCallback(\n (next: Pending) => {\n window.clearTimeout(timer.current);\n current.current?.commit(); // a second removal commits the first\n set(next);\n timer.current = window.setTimeout(() => {\n current.current?.commit();\n set(null);\n }, windowMs);\n },\n [set, windowMs],\n );\n\n const undo = React.useCallback(() => {\n window.clearTimeout(timer.current);\n set(null);\n }, [set]);\n\n // Leaving the screen is not Undo: if the owner unmounts inside the window (the person navigated\n // away), commit what was scheduled rather than silently dropping it.\n React.useEffect(\n () => () => {\n window.clearTimeout(timer.current);\n current.current?.commit();\n current.current = null;\n },\n [],\n );\n return { pending, schedule, undo };\n}\n\n// The bar lives in a polite live region that is always mounted, so screen readers hear the message\n// when it appears. It is fixed to the bottom of the viewport; a page that uses it keeps that much\n// bottom padding so the bar never covers the focused row.\nexport function UndoBar({\n pending,\n onUndo,\n inline = false,\n}: {\n pending: Pending | null;\n onUndo: () => void;\n /** Render in the flow instead of fixed to the viewport; for the state-matrix page. */\n inline?: boolean;\n}) {\n return (\n <div\n aria-live=\"polite\"\n className={\n inline ? \"flex\" : \"pointer-events-none fixed inset-x-0 bottom-4 z-50 flex justify-center px-4\"\n }\n >\n {pending && (\n <div className=\"pointer-events-auto flex max-w-lg items-center gap-4 rounded-overlay bg-foreground py-2 pr-2 pl-4 text-background shadow-overlay\">\n <p className=\"min-w-0 type-body-sm\">{pending.message}</p>\n <Button size=\"sm\" variant=\"ghost\" className=\"text-background hover:bg-neutral-11\" onClick={onUndo}>\n Undo\n </Button>\n </div>\n )}\n </div>\n );\n}\n","import * as React from \"react\";\nimport { Check } from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\n\n// The completion moment: what a person sees after finishing something that took effort (a\n// booking, a submission, an import). It is the one place on an Operate product where the library\n// spends its delight budget (character.md §3), so it is a component with rules rather than a\n// confetti call:\n// - `title` says what happened, in the past tense, with the object: \"Booked: 6 pieces in kiln 2\".\n// - `next` is required. A finish that does not say what happens now is a dead end, however\n// pleasant.\n// - `figure` is optional and must be drawn from the person's own data in the subject's terms\n// (their pieces on a shelf, their route on a map), never a stock illustration.\n// - The part of the figure that is the person's result enters once, with the `enter-moment`\n// utility (500ms, hard ease-out, no overshoot): their pieces set down on the shelf. Reduced\n// motion makes it instant. Nothing loops and nothing plays a sound.\n// - Focus moves to the heading on mount, so keyboard and screen-reader users land on the result.\n\nexport function Done({\n title,\n next,\n figure,\n actions,\n focusOnMount = true,\n className,\n}: {\n /** What happened, past tense, with its object. */\n title: string;\n /** What the person does now, or what happens next and when. */\n next: React.ReactNode;\n /** A drawing of the result made from the person's data in the subject's own terms. */\n figure?: React.ReactNode;\n actions?: React.ReactNode;\n /** Off only where the result is not the page's subject (the state-matrix page). */\n focusOnMount?: boolean;\n className?: string;\n}) {\n const heading = React.useRef<HTMLHeadingElement>(null);\n const titleId = React.useId();\n React.useEffect(() => {\n if (focusOnMount) heading.current?.focus();\n }, [focusOnMount]);\n return (\n <section aria-labelledby={titleId} className={cn(\"grid gap-5\", className)}>\n <div className=\"grid gap-2\">\n <h1 id={titleId} ref={heading} tabIndex={-1} className=\"flex items-start gap-2 type-title\">\n <Check aria-hidden className=\"mt-1 size-6 shrink-0 text-success\" />\n {title}\n </h1>\n <div className=\"max-w-prose type-body text-muted-foreground\">{next}</div>\n </div>\n {figure}\n {actions && <div className=\"flex flex-wrap items-center gap-3\">{actions}</div>}\n </section>\n );\n}\n","import * as React from \"react\";\nimport { Link, NavLink } from \"react-router\";\nimport { cn } from \"../lib/utils\";\nimport type { IconComponent } from \"../lib/icon\";\n\n// The frame of an app: a side nav on the page ground (not a navy or tinted sidebar), the current\n// item as a neutral pill (no left bar, no accent), and the page beside it. A top bar exists only\n// when it holds something used on every screen, so it is a slot, not a default. Built on React\n// Router's NavLink, so \"current\" is the router's answer and aria-current is set for free. On\n// phones the nav becomes a horizontal, scrollable row above the page rather than a hamburger, so\n// every destination stays one tap away.\n\nexport type NavItem = {\n to: string;\n label: string;\n icon?: IconComponent;\n /** Match only this exact path, for an index route such as \"/\". */\n end?: boolean;\n};\n\nexport function AppShell({\n product,\n nav,\n navLabel = \"Main\",\n topBar,\n children,\n className,\n}: {\n /** The product's name or mark, linking home. */\n product: React.ReactNode;\n nav: NavItem[];\n navLabel?: string;\n topBar?: React.ReactNode;\n children: React.ReactNode;\n className?: string;\n}) {\n return (\n <div className={cn(\"min-h-dvh md:grid md:grid-cols-[14rem_minmax(0,1fr)]\", className)}>\n <aside className=\"border-b border-border md:border-r md:border-b-0\">\n <div className=\"flex items-center gap-4 px-4 py-3 md:block md:px-3 md:py-4\">\n <Link to=\"/\" className=\"shrink-0 rounded-control px-2 type-heading text-foreground no-underline\">\n {product}\n </Link>\n <SideNav items={nav} label={navLabel} className=\"min-w-0 flex-1 md:mt-4\" />\n </div>\n </aside>\n <div className=\"min-w-0\">\n {topBar && <div className=\"flex items-center gap-3 border-b border-border px-4 py-2 md:px-8\">{topBar}</div>}\n <main className=\"px-4 py-6 md:px-8 md:py-8\">{children}</main>\n </div>\n </div>\n );\n}\n\nexport function SideNav({ items, label, className }: { items: NavItem[]; label: string; className?: string }) {\n return (\n <nav aria-label={label} className={cn(\"min-w-0\", className)}>\n <ul className=\"flex gap-1 overflow-x-auto md:grid md:overflow-visible\">\n {items.map(({ to, label, icon: Icon, end }) => (\n <li key={to} className=\"shrink-0\">\n <NavLink\n to={to}\n end={end}\n className={({ isActive }) =>\n cn(\n \"flex h-(--control-h-sm) items-center gap-2 rounded-control px-2 type-label whitespace-nowrap no-underline transition-state\",\n isActive ? \"bg-muted text-foreground\" : \"text-muted-foreground hover:bg-muted hover:text-foreground\",\n )\n }\n >\n {Icon && <Icon aria-hidden className=\"size-4 shrink-0\" />}\n {label}\n </NavLink>\n </li>\n ))}\n </ul>\n </nav>\n );\n}\n\n// Plain links with a \"/\" separator in muted ink. The last crumb is the current page: text, not a\n// link, with aria-current.\nexport function Breadcrumb({ items }: { items: { label: string; to?: string }[] }) {\n return (\n <nav aria-label=\"Breadcrumb\">\n <ol className=\"flex flex-wrap items-center gap-x-2 type-body-sm text-muted-foreground\">\n {items.map((item, i) => {\n const last = i === items.length - 1;\n return (\n <li key={`${item.label}-${i}`} className=\"flex items-center gap-2\">\n {item.to && !last ? (\n <Link to={item.to} className=\"text-muted-foreground underline hover:text-foreground\">\n {item.label}\n </Link>\n ) : (\n <span aria-current={last ? \"page\" : undefined} className={cn(last && \"text-foreground\")}>\n {item.label}\n </span>\n )}\n {!last && <span aria-hidden>/</span>}\n </li>\n );\n })}\n </ol>\n </nav>\n );\n}\n","import * as React from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { cn } from \"../lib/utils\";\n\n// Tabs whose state lives in the URL (?view=readings), so a tab can be linked, survives reload and\n// works with Back. ARIA tabs pattern: arrow keys move between tabs and select them, Home and End\n// jump, only the selected tab is in the tab order. The selected tab is marked by weight and a\n// 2px rule in ink, not by colour alone. The first tab is the default and is not written to the URL.\n\nexport type TabItem = { value: string; label: string };\n\nexport function Tabs({\n param,\n items,\n label,\n children,\n className,\n}: {\n /** The search parameter that holds the selected tab. */\n param: string;\n items: TabItem[];\n /** Names the tab list for screen readers. */\n label: string;\n /** Renders the selected panel. */\n children: (value: string) => React.ReactNode;\n className?: string;\n}) {\n const [params, setParams] = useSearchParams();\n const fallback = items[0]?.value ?? \"\";\n const raw = params.get(param);\n const value = items.some((i) => i.value === raw) ? raw! : fallback;\n const baseId = React.useId();\n const refs = React.useRef<(HTMLButtonElement | null)[]>([]);\n\n const select = (next: string, focus = false) => {\n setParams(\n (p) => {\n const out = new URLSearchParams(p);\n if (next === fallback) out.delete(param);\n else out.set(param, next);\n return out;\n },\n { replace: true },\n );\n if (focus) refs.current[items.findIndex((i) => i.value === next)]?.focus();\n };\n\n const onKeyDown = (e: React.KeyboardEvent, index: number) => {\n const last = items.length - 1;\n const to =\n e.key === \"ArrowRight\" ? (index === last ? 0 : index + 1)\n : e.key === \"ArrowLeft\" ? (index === 0 ? last : index - 1)\n : e.key === \"Home\" ? 0\n : e.key === \"End\" ? last\n : undefined;\n if (to === undefined) return;\n e.preventDefault();\n select(items[to].value, true);\n };\n\n return (\n <div className={cn(\"grid gap-4\", className)}>\n <div role=\"tablist\" aria-label={label} className=\"flex gap-4 overflow-x-auto border-b border-border\">\n {items.map((item, i) => {\n const selected = item.value === value;\n return (\n <button\n key={item.value}\n ref={(el) => {\n refs.current[i] = el;\n }}\n type=\"button\"\n role=\"tab\"\n id={`${baseId}-tab-${item.value}`}\n aria-selected={selected}\n aria-controls={`${baseId}-panel`}\n tabIndex={selected ? 0 : -1}\n onClick={() => select(item.value)}\n onKeyDown={(e) => onKeyDown(e, i)}\n className={cn(\n \"-mb-px cursor-pointer border-b-2 py-2 type-label whitespace-nowrap transition-state\",\n selected\n ? \"border-foreground font-semibold text-foreground\"\n : \"border-transparent text-muted-foreground hover:text-foreground\",\n )}\n >\n {item.label}\n </button>\n );\n })}\n </div>\n <div role=\"tabpanel\" id={`${baseId}-panel`} aria-labelledby={`${baseId}-tab-${value}`} tabIndex={0}>\n {children(value)}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const NBSP = "\u00A0";
|
|
2
|
+
export declare function createFormat(locale: string): {
|
|
3
|
+
locale: string;
|
|
4
|
+
integer: Intl.NumberFormat;
|
|
5
|
+
decimal: Intl.NumberFormat;
|
|
6
|
+
percent: Intl.NumberFormat;
|
|
7
|
+
clock: Intl.DateTimeFormat;
|
|
8
|
+
day: Intl.DateTimeFormat;
|
|
9
|
+
shortDay: Intl.DateTimeFormat;
|
|
10
|
+
withUnit: (value: number, unit: string, fmt?: Intl.NumberFormat) => string;
|
|
11
|
+
celsius: (value: number) => string;
|
|
12
|
+
/** "1 piece", "6 pieces": the count and its noun, so copy never reads "1 pieces". */
|
|
13
|
+
count: (value: number, one: string, many?: string) => string;
|
|
14
|
+
};
|
|
15
|
+
export type Format = ReturnType<typeof createFormat>;
|
|
16
|
+
export declare const format: {
|
|
17
|
+
locale: string;
|
|
18
|
+
integer: Intl.NumberFormat;
|
|
19
|
+
decimal: Intl.NumberFormat;
|
|
20
|
+
percent: Intl.NumberFormat;
|
|
21
|
+
clock: Intl.DateTimeFormat;
|
|
22
|
+
day: Intl.DateTimeFormat;
|
|
23
|
+
shortDay: Intl.DateTimeFormat;
|
|
24
|
+
withUnit: (value: number, unit: string, fmt?: Intl.NumberFormat) => string;
|
|
25
|
+
celsius: (value: number) => string;
|
|
26
|
+
/** "1 piece", "6 pieces": the count and its noun, so copy never reads "1 pieces". */
|
|
27
|
+
count: (value: number, one: string, many?: string) => string;
|
|
28
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { QueryClient, QueryKey, UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
|
|
2
|
+
import { Load } from '../components/data-view';
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
export declare function createQueryClient(overrides?: ConstructorParameters<typeof QueryClient>[0]): QueryClient;
|
|
5
|
+
export declare function StrataQueryProvider({ client, children }: {
|
|
6
|
+
client?: QueryClient;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}): React.JSX.Element;
|
|
9
|
+
/** Writes an error for a person: what happened, in a sentence. A product passes its own words. */
|
|
10
|
+
export type DescribeError = (error: unknown) => string;
|
|
11
|
+
export declare const describeHttpError: DescribeError;
|
|
12
|
+
export declare function toLoad<T>(query: UseQueryResult<T>, describe?: DescribeError): Load<T>;
|
|
13
|
+
/** useQuery, returned as a Load<T> for DataView. */
|
|
14
|
+
export declare function useLoad<T, K extends QueryKey = QueryKey>(options: UseQueryOptions<T, Error, T, K> & {
|
|
15
|
+
describeError?: DescribeError;
|
|
16
|
+
}): Load<T>;
|
|
17
|
+
export declare class HttpError extends Error {
|
|
18
|
+
readonly status: number;
|
|
19
|
+
readonly statusText: string;
|
|
20
|
+
readonly url: string;
|
|
21
|
+
constructor(status: number, statusText: string, url: string);
|
|
22
|
+
}
|
|
23
|
+
/** fetch() that throws an HttpError on a non-2xx response and parses JSON: the queryFn default. */
|
|
24
|
+
export declare function fetchJson<T>(input: string | URL, init?: RequestInit): Promise<T>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
|
|
4
|
+
// src/query/index.tsx
|
|
5
|
+
function createQueryClient(overrides) {
|
|
6
|
+
return new QueryClient({
|
|
7
|
+
...overrides,
|
|
8
|
+
defaultOptions: {
|
|
9
|
+
...overrides?.defaultOptions,
|
|
10
|
+
queries: {
|
|
11
|
+
retry: 1,
|
|
12
|
+
staleTime: 3e4,
|
|
13
|
+
refetchOnWindowFocus: false,
|
|
14
|
+
...overrides?.defaultOptions?.queries
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function StrataQueryProvider({ client, children }) {
|
|
20
|
+
const [fallback] = React.useState(() => client ?? createQueryClient());
|
|
21
|
+
return /* @__PURE__ */ jsx(QueryClientProvider, {
|
|
22
|
+
client: client ?? fallback,
|
|
23
|
+
children
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
var describeHttpError = (error) => error instanceof HttpError ? error.status >= 500 ? `The server could not answer (${error.status}). Nothing was changed.` : `The request was refused (${error.status}): ${error.statusText || "no reason given"}.` : error instanceof Error && error.message ? error.message : "The data could not be loaded. Nothing was changed.";
|
|
27
|
+
function toLoad(query, describe = describeHttpError) {
|
|
28
|
+
if (query.status === "success") return {
|
|
29
|
+
status: "ready",
|
|
30
|
+
data: query.data
|
|
31
|
+
};
|
|
32
|
+
if (query.status === "error") return {
|
|
33
|
+
status: "error",
|
|
34
|
+
message: describe(query.error),
|
|
35
|
+
retry: () => void query.refetch()
|
|
36
|
+
};
|
|
37
|
+
return { status: "loading" };
|
|
38
|
+
}
|
|
39
|
+
/** useQuery, returned as a Load<T> for DataView. */
|
|
40
|
+
function useLoad(options) {
|
|
41
|
+
const { describeError, ...rest } = options;
|
|
42
|
+
return toLoad(useQuery(rest), describeError);
|
|
43
|
+
}
|
|
44
|
+
var HttpError = class extends Error {
|
|
45
|
+
status;
|
|
46
|
+
statusText;
|
|
47
|
+
url;
|
|
48
|
+
constructor(status, statusText, url) {
|
|
49
|
+
super(`${status} ${statusText} from ${url}`);
|
|
50
|
+
this.status = status;
|
|
51
|
+
this.statusText = statusText;
|
|
52
|
+
this.url = url;
|
|
53
|
+
this.name = "HttpError";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/** fetch() that throws an HttpError on a non-2xx response and parses JSON: the queryFn default. */
|
|
57
|
+
async function fetchJson(input, init) {
|
|
58
|
+
const headers = new Headers(init?.headers);
|
|
59
|
+
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
60
|
+
const res = await fetch(input, {
|
|
61
|
+
...init,
|
|
62
|
+
headers
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) throw new HttpError(res.status, res.statusText, String(input));
|
|
65
|
+
return await res.json();
|
|
66
|
+
}
|
|
67
|
+
export { fetchJson as a, describeHttpError as i, StrataQueryProvider as n, toLoad as o, createQueryClient as r, useLoad as s, HttpError as t };
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=query-BXH-Gfj7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-BXH-Gfj7.js","names":[],"sources":["../src/query/index.tsx"],"sourcesContent":["import * as React from \"react\";\nimport {\n QueryClient,\n QueryClientProvider,\n useQuery,\n type QueryKey,\n type UseQueryOptions,\n type UseQueryResult,\n} from \"@tanstack/react-query\";\nimport type { Load } from \"../components/data-view\";\n\n// Data loading, the Strata way: TanStack Query fetches and caches, and every view of the result\n// goes through DataView, which takes a Load<T>. `toLoad` is the one bridge between them, so a\n// screen never re-derives loading/error/ready from a query's flags and never forgets one.\n//\n// Opinions in createQueryClient: one retry (a second failure is real and the person should see\n// it with a \"Try again\"), 30 s stale time (dashboards are read many times a minute but change\n// over minutes), and no refetch on window focus (a table that reorders itself while someone reads\n// it is worse than one that is 30 s old). Override per query when the data says otherwise.\n\nexport function createQueryClient(overrides?: ConstructorParameters<typeof QueryClient>[0]) {\n return new QueryClient({\n ...overrides,\n defaultOptions: {\n ...overrides?.defaultOptions,\n queries: { retry: 1, staleTime: 30_000, refetchOnWindowFocus: false, ...overrides?.defaultOptions?.queries },\n },\n });\n}\n\nexport function StrataQueryProvider({ client, children }: { client?: QueryClient; children: React.ReactNode }) {\n const [fallback] = React.useState(() => client ?? createQueryClient());\n return <QueryClientProvider client={client ?? fallback}>{children}</QueryClientProvider>;\n}\n\n/** Writes an error for a person: what happened, in a sentence. A product passes its own words. */\nexport type DescribeError = (error: unknown) => string;\n\nexport const describeHttpError: DescribeError = (error) =>\n error instanceof HttpError\n ? error.status >= 500\n ? `The server could not answer (${error.status}). Nothing was changed.`\n : `The request was refused (${error.status}): ${error.statusText || \"no reason given\"}.`\n : error instanceof Error && error.message\n ? error.message\n : \"The data could not be loaded. Nothing was changed.\";\n\nexport function toLoad<T>(query: UseQueryResult<T>, describe: DescribeError = describeHttpError): Load<T> {\n if (query.status === \"success\") return { status: \"ready\", data: query.data };\n if (query.status === \"error\") return { status: \"error\", message: describe(query.error), retry: () => void query.refetch() };\n return { status: \"loading\" };\n}\n\n/** useQuery, returned as a Load<T> for DataView. */\nexport function useLoad<T, K extends QueryKey = QueryKey>(\n options: UseQueryOptions<T, Error, T, K> & { describeError?: DescribeError },\n): Load<T> {\n const { describeError, ...rest } = options;\n return toLoad(useQuery(rest), describeError);\n}\n\nexport class HttpError extends Error {\n constructor(\n readonly status: number,\n readonly statusText: string,\n readonly url: string,\n ) {\n super(`${status} ${statusText} from ${url}`);\n this.name = \"HttpError\";\n }\n}\n\n/** fetch() that throws an HttpError on a non-2xx response and parses JSON: the queryFn default. */\nexport async function fetchJson<T>(input: string | URL, init?: RequestInit): Promise<T> {\n // Headers, tuple arrays and plain objects are all valid HeadersInit; normalise before adding the\n // default so a caller's Authorization (or their own Accept) is never dropped.\n const headers = new Headers(init?.headers);\n if (!headers.has(\"Accept\")) headers.set(\"Accept\", \"application/json\");\n const res = await fetch(input, { ...init, headers });\n if (!res.ok) throw new HttpError(res.status, res.statusText, String(input));\n return (await res.json()) as T;\n}\n"],"mappings":";;;;AAoBA,SAAgB,kBAAkB,WAA0D;CAC1F,OAAO,IAAI,YAAY;EACrB,GAAG;EACH,gBAAgB;GACd,GAAG,WAAW;GACd,SAAS;IAAE,OAAO;IAAG,WAAW;IAAQ,sBAAsB;IAAO,GAAG,WAAW,gBAAgB;GAAQ;EAC7G;CACF,CAAC;AACH;AAEA,SAAgB,oBAAoB,EAAE,QAAQ,YAAiE;CAC7G,MAAM,CAAC,YAAY,MAAM,eAAe,UAAU,kBAAkB,CAAC;CACrE,OAAO,oBAAC,qBAAD;EAAqB,QAAQ,UAAU;EAAW;CAA8B,CAAA;AACzF;AAKA,IAAa,qBAAoC,UAC/C,iBAAiB,YACb,MAAM,UAAU,MACd,gCAAgC,MAAM,OAAO,2BAC7C,4BAA4B,MAAM,OAAO,KAAK,MAAM,cAAc,kBAAkB,KACtF,iBAAiB,SAAS,MAAM,UAC9B,MAAM,UACN;AAER,SAAgB,OAAU,OAA0B,WAA0B,mBAA4B;CACxG,IAAI,MAAM,WAAW,WAAW,OAAO;EAAE,QAAQ;EAAS,MAAM,MAAM;CAAK;CAC3E,IAAI,MAAM,WAAW,SAAS,OAAO;EAAE,QAAQ;EAAS,SAAS,SAAS,MAAM,KAAK;EAAG,aAAa,KAAK,MAAM,QAAQ;CAAE;CAC1H,OAAO,EAAE,QAAQ,UAAU;AAC7B;;AAGA,SAAgB,QACd,SACS;CACT,MAAM,EAAE,eAAe,GAAG,SAAS;CACnC,OAAO,OAAO,SAAS,IAAI,GAAG,aAAa;AAC7C;AAEA,IAAa,YAAb,cAA+B,MAAM;CAExB;CACA;CACA;CAHX,YACE,QACA,YACA,KACA;EACA,MAAM,GAAG,OAAO,GAAG,WAAW,QAAQ,KAAK;EAJlC,KAAA,SAAA;EACA,KAAA,aAAA;EACA,KAAA,MAAA;EAGT,KAAK,OAAO;CACd;AACF;;AAGA,eAAsB,UAAa,OAAqB,MAAgC;CAGtF,MAAM,UAAU,IAAI,QAAQ,MAAM,OAAO;CACzC,IAAI,CAAC,QAAQ,IAAI,QAAQ,GAAG,QAAQ,IAAI,UAAU,kBAAkB;CACpE,MAAM,MAAM,MAAM,MAAM,OAAO;EAAE,GAAG;EAAM;CAAQ,CAAC;CACnD,IAAI,CAAC,IAAI,IAAI,MAAM,IAAI,UAAU,IAAI,QAAQ,IAAI,YAAY,OAAO,KAAK,CAAC;CAC1E,OAAQ,MAAM,IAAI,KAAK;AACzB"}
|
package/dist/strata.css
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/* strata-react: the token contract, the namespace reset, the type steps, motion utilities and the
|
|
2
|
+
base layer. It ships no colours. A product imports it between Tailwind and its own theme:
|
|
3
|
+
|
|
4
|
+
@import "tailwindcss";
|
|
5
|
+
@import "strata-react/strata.css";
|
|
6
|
+
@import "./theme.generated.css"; (palette.mjs output: shadcn roles + 12-step ramps)
|
|
7
|
+
@import "./theme.css"; (status roles, fonts, any subject colours, with reasons)
|
|
8
|
+
|
|
9
|
+
The research behind every value is in jburnhams/ux-research, ui-library/tokens.md. */
|
|
10
|
+
|
|
11
|
+
/* Clear Tailwind's stock namespaces right after Tailwind's own theme loads. With these gone,
|
|
12
|
+
`bg-indigo-500`, `text-sm`, `rounded-xl` and `shadow-lg` do not compile at all: the only colours,
|
|
13
|
+
type steps, radii and shadows that exist are the ones the product's theme names. That is the
|
|
14
|
+
library's strongest anti-drift rule, and it costs nothing at runtime. */
|
|
15
|
+
@theme {
|
|
16
|
+
--color-*: initial;
|
|
17
|
+
--text-*: initial;
|
|
18
|
+
--radius-*: initial;
|
|
19
|
+
--shadow-*: initial;
|
|
20
|
+
--font-*: initial;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@custom-variant dark (&:is(.dark *));
|
|
24
|
+
|
|
25
|
+
/* Status roles are part of the contract but not of the generated palette: the product solves them
|
|
26
|
+
(pale tint + dark ink of one hue, ink >= 6:1 on its tint and on the ground) and sets the
|
|
27
|
+
variables in its theme. Strata only names them. Never the accent. */
|
|
28
|
+
@theme inline {
|
|
29
|
+
--color-success: var(--success);
|
|
30
|
+
--color-success-subtle: var(--success-subtle);
|
|
31
|
+
--color-warning: var(--warning);
|
|
32
|
+
--color-warning-subtle: var(--warning-subtle);
|
|
33
|
+
--color-danger: var(--danger);
|
|
34
|
+
--color-danger-subtle: var(--danger-subtle);
|
|
35
|
+
--color-info: var(--info);
|
|
36
|
+
--color-info-subtle: var(--info-subtle);
|
|
37
|
+
--color-surface-sunken: var(--surface-sunken);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@theme {
|
|
41
|
+
/* System faces until the product names its own (a later @theme block wins). One family on an
|
|
42
|
+
Operate surface; --font-display is for page titles only. */
|
|
43
|
+
--font-sans: ui-sans-serif, system-ui, sans-serif;
|
|
44
|
+
--font-mono: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
|
45
|
+
--font-display: var(--font-sans);
|
|
46
|
+
|
|
47
|
+
/* Radius by role. Optical nesting: inner = outer - padding, so a control inside an 8px overlay
|
|
48
|
+
with 4px padding is 4px, never the same value as its container. */
|
|
49
|
+
--radius-control: 4px;
|
|
50
|
+
--radius-surface: 6px;
|
|
51
|
+
--radius-overlay: 8px;
|
|
52
|
+
--radius-pill: 9999px;
|
|
53
|
+
|
|
54
|
+
/* One elevation channel per surface: hairlines for things on the page, a shadow only for what
|
|
55
|
+
floats above it. */
|
|
56
|
+
--shadow-overlay: 0 1px 2px oklch(0% 0 0 / 0.08), 0 8px 24px oklch(0% 0 0 / 0.12);
|
|
57
|
+
|
|
58
|
+
/* Effects (colour, opacity) never overshoot; spatial gets Material Standard's barely visible
|
|
59
|
+
settle. Expressive springs are a product allowance, recorded in its DESIGN.md. */
|
|
60
|
+
--ease-effects: cubic-bezier(0.34, 0.8, 0.34, 1);
|
|
61
|
+
--ease-spatial: cubic-bezier(0.27, 1.06, 0.18, 1);
|
|
62
|
+
--ease-moment: cubic-bezier(0.23, 1, 0.32, 1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Non-colour defaults in the base layer, so any unlayered rule in the product's theme wins
|
|
66
|
+
regardless of import order. */
|
|
67
|
+
@layer base {
|
|
68
|
+
:root {
|
|
69
|
+
--surface-sunken: var(--neutral-2);
|
|
70
|
+
|
|
71
|
+
/* Density: control heights move together (button, input, select). */
|
|
72
|
+
--control-h: 2.5rem;
|
|
73
|
+
--control-h-sm: 2rem;
|
|
74
|
+
|
|
75
|
+
--motion-effects: 200ms;
|
|
76
|
+
--motion-spatial: 350ms;
|
|
77
|
+
/* Press feedback is the one motion on a frequent action: 100ms, 2%, felt more than seen. A
|
|
78
|
+
moment is the rare, earned one; slower, hard ease-out, no overshoot. */
|
|
79
|
+
--motion-press: 100ms;
|
|
80
|
+
--motion-moment: 500ms;
|
|
81
|
+
|
|
82
|
+
/* The page title's voice. A product with a display face sets these with it. */
|
|
83
|
+
--title-weight: 650;
|
|
84
|
+
--title-stretch: 100%;
|
|
85
|
+
--title-tracking: -0.01em;
|
|
86
|
+
}
|
|
87
|
+
.dark {
|
|
88
|
+
--surface-sunken: var(--neutral-1);
|
|
89
|
+
}
|
|
90
|
+
[data-density="compact"] {
|
|
91
|
+
--control-h: 2rem;
|
|
92
|
+
--control-h-sm: 1.75rem;
|
|
93
|
+
}
|
|
94
|
+
/* After the compact rule, so a coarse pointer always wins: every control stays a 44px target. */
|
|
95
|
+
@media (pointer: coarse) {
|
|
96
|
+
:root,
|
|
97
|
+
[data-density="compact"] {
|
|
98
|
+
--control-h: 2.75rem;
|
|
99
|
+
--control-h-sm: 2.75rem;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Type steps as utilities, not --text-* keys: a compound of size, line height and weight that
|
|
105
|
+
cn() can tell apart from text colours. Operate scale, ratio about 1.2, fixed rem; nothing above
|
|
106
|
+
28px except `type-display`, which is for Persuade surfaces only. */
|
|
107
|
+
@utility type-caption { font-size: 0.75rem; line-height: 1rem; font-weight: 400; }
|
|
108
|
+
@utility type-label { font-size: 0.875rem; line-height: 1.25rem; font-weight: 500; }
|
|
109
|
+
@utility type-body-sm { font-size: 0.875rem; line-height: 1.25rem; font-weight: 400; }
|
|
110
|
+
@utility type-body { font-size: 1rem; line-height: 1.5rem; font-weight: 400; }
|
|
111
|
+
@utility type-heading { font-size: 1.125rem; line-height: 1.5rem; font-weight: 600; }
|
|
112
|
+
@utility type-title {
|
|
113
|
+
font-family: var(--font-display);
|
|
114
|
+
font-size: 1.75rem;
|
|
115
|
+
line-height: 2.25rem;
|
|
116
|
+
font-weight: var(--title-weight);
|
|
117
|
+
font-stretch: var(--title-stretch);
|
|
118
|
+
letter-spacing: var(--title-tracking);
|
|
119
|
+
}
|
|
120
|
+
@utility type-figure { font-size: 1.75rem; line-height: 2.25rem; font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
121
|
+
@utility type-display {
|
|
122
|
+
font-family: var(--font-display);
|
|
123
|
+
font-size: clamp(2.25rem, 1.5rem + 3vw, 4rem);
|
|
124
|
+
line-height: 1.05;
|
|
125
|
+
font-weight: var(--title-weight);
|
|
126
|
+
font-stretch: var(--title-stretch);
|
|
127
|
+
letter-spacing: -0.02em;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* State transitions: named properties only (never `transition: all`). `scale` is listed for the
|
|
131
|
+
press feedback and runs at the shorter press duration. */
|
|
132
|
+
@utility transition-state {
|
|
133
|
+
transition-property: color, background-color, border-color, opacity, box-shadow, scale;
|
|
134
|
+
transition-duration: var(--motion-effects), var(--motion-effects), var(--motion-effects), var(--motion-effects),
|
|
135
|
+
var(--motion-effects), var(--motion-press);
|
|
136
|
+
transition-timing-function: var(--ease-effects);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Press feedback: 2% is the most a control used tens of times a day can move without feeling slow. */
|
|
140
|
+
@utility press {
|
|
141
|
+
&:active:not(:disabled) { scale: 0.98; }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* A moment's entrance: CSS only, runs once when the element mounts. Reduced motion collapses it. */
|
|
145
|
+
@utility enter-moment {
|
|
146
|
+
transition-property: opacity, translate;
|
|
147
|
+
transition-duration: var(--motion-moment);
|
|
148
|
+
transition-timing-function: var(--ease-moment);
|
|
149
|
+
@starting-style { opacity: 0; translate: 0 -0.75rem; }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@layer base {
|
|
153
|
+
html { color-scheme: light; }
|
|
154
|
+
.dark { color-scheme: dark; }
|
|
155
|
+
body {
|
|
156
|
+
background-color: var(--background);
|
|
157
|
+
color: var(--foreground);
|
|
158
|
+
font-family: var(--font-sans);
|
|
159
|
+
font-size: 1rem;
|
|
160
|
+
line-height: 1.5rem;
|
|
161
|
+
-webkit-font-smoothing: antialiased;
|
|
162
|
+
}
|
|
163
|
+
/* One focus treatment for everything focusable, so no component can forget it. */
|
|
164
|
+
:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
|
|
165
|
+
/* Except where a script moved focus to a heading to announce a result (Done): the heading is not
|
|
166
|
+
a control, so a ring around it says "press me" to nobody. */
|
|
167
|
+
h1[tabindex="-1"]:focus-visible, h2[tabindex="-1"]:focus-visible { outline: none; }
|
|
168
|
+
::selection { background-color: var(--primary-5); color: var(--foreground); }
|
|
169
|
+
a { text-underline-offset: 0.2em; }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@media (prefers-reduced-motion: reduce) {
|
|
173
|
+
*, *::before, *::after {
|
|
174
|
+
animation-duration: 0.01ms !important;
|
|
175
|
+
animation-iteration-count: 1 !important;
|
|
176
|
+
transition-duration: 0.01ms !important;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* The built components, so their classes are generated. */
|
|
181
|
+
@source "./*.js";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { RenderOptions } from '@testing-library/react';
|
|
2
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
export declare function createTestQueryClient(): QueryClient;
|
|
5
|
+
export declare function renderWithStrata(ui: React.ReactElement, { route, client, ...options }?: RenderOptions & {
|
|
6
|
+
route?: string;
|
|
7
|
+
client?: QueryClient;
|
|
8
|
+
}): {
|
|
9
|
+
container: HTMLElement;
|
|
10
|
+
baseElement: HTMLElement;
|
|
11
|
+
debug: (baseElement?: import('react-dom/client').Container | (Element | Document) | Array<import('react-dom/client').Container | (Element | Document)> | undefined, maxLength?: number | undefined, options?: import('pretty-format').OptionsReceived | undefined) => void;
|
|
12
|
+
rerender: (ui: React.ReactNode) => void;
|
|
13
|
+
unmount: () => void;
|
|
14
|
+
asFragment: () => DocumentFragment;
|
|
15
|
+
getByLabelText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').SelectorMatcherOptions | undefined) => HTMLElement;
|
|
16
|
+
getAllByLabelText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').SelectorMatcherOptions | undefined) => HTMLElement[];
|
|
17
|
+
queryByLabelText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').SelectorMatcherOptions | undefined) => HTMLElement | null;
|
|
18
|
+
queryAllByLabelText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').SelectorMatcherOptions | undefined) => HTMLElement[];
|
|
19
|
+
findByLabelText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').SelectorMatcherOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement>;
|
|
20
|
+
findAllByLabelText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').SelectorMatcherOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement[]>;
|
|
21
|
+
getByPlaceholderText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement;
|
|
22
|
+
getAllByPlaceholderText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement[];
|
|
23
|
+
queryByPlaceholderText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement | null;
|
|
24
|
+
queryAllByPlaceholderText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement[];
|
|
25
|
+
findByPlaceholderText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement>;
|
|
26
|
+
findAllByPlaceholderText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement[]>;
|
|
27
|
+
getByText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').SelectorMatcherOptions | undefined) => HTMLElement;
|
|
28
|
+
getAllByText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').SelectorMatcherOptions | undefined) => HTMLElement[];
|
|
29
|
+
queryByText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').SelectorMatcherOptions | undefined) => HTMLElement | null;
|
|
30
|
+
queryAllByText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').SelectorMatcherOptions | undefined) => HTMLElement[];
|
|
31
|
+
findByText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').SelectorMatcherOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement>;
|
|
32
|
+
findAllByText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').SelectorMatcherOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement[]>;
|
|
33
|
+
getByAltText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement;
|
|
34
|
+
getAllByAltText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement[];
|
|
35
|
+
queryByAltText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement | null;
|
|
36
|
+
queryAllByAltText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement[];
|
|
37
|
+
findByAltText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement>;
|
|
38
|
+
findAllByAltText: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement[]>;
|
|
39
|
+
getByTitle: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement;
|
|
40
|
+
getAllByTitle: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement[];
|
|
41
|
+
queryByTitle: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement | null;
|
|
42
|
+
queryAllByTitle: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement[];
|
|
43
|
+
findByTitle: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement>;
|
|
44
|
+
findAllByTitle: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement[]>;
|
|
45
|
+
getByDisplayValue: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement;
|
|
46
|
+
getAllByDisplayValue: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement[];
|
|
47
|
+
queryByDisplayValue: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement | null;
|
|
48
|
+
queryAllByDisplayValue: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement[];
|
|
49
|
+
findByDisplayValue: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement>;
|
|
50
|
+
findAllByDisplayValue: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement[]>;
|
|
51
|
+
getByRole: (role: import('@testing-library/dom').ByRoleMatcher, options?: import('@testing-library/dom').ByRoleOptions | undefined) => HTMLElement;
|
|
52
|
+
getAllByRole: (role: import('@testing-library/dom').ByRoleMatcher, options?: import('@testing-library/dom').ByRoleOptions | undefined) => HTMLElement[];
|
|
53
|
+
queryByRole: (role: import('@testing-library/dom').ByRoleMatcher, options?: import('@testing-library/dom').ByRoleOptions | undefined) => HTMLElement | null;
|
|
54
|
+
queryAllByRole: (role: import('@testing-library/dom').ByRoleMatcher, options?: import('@testing-library/dom').ByRoleOptions | undefined) => HTMLElement[];
|
|
55
|
+
findByRole: (role: import('@testing-library/dom').ByRoleMatcher, options?: import('@testing-library/dom').ByRoleOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement>;
|
|
56
|
+
findAllByRole: (role: import('@testing-library/dom').ByRoleMatcher, options?: import('@testing-library/dom').ByRoleOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement[]>;
|
|
57
|
+
getByTestId: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement;
|
|
58
|
+
getAllByTestId: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement[];
|
|
59
|
+
queryByTestId: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement | null;
|
|
60
|
+
queryAllByTestId: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined) => HTMLElement[];
|
|
61
|
+
findByTestId: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement>;
|
|
62
|
+
findAllByTestId: (id: import('@testing-library/dom').Matcher, options?: import('@testing-library/dom').MatcherOptions | undefined, waitForElementOptions?: import('@testing-library/dom').waitForOptions | undefined) => Promise<HTMLElement[]>;
|
|
63
|
+
client: QueryClient;
|
|
64
|
+
};
|