@tiniestcloud/create 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiniestcloud/create",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Start an app or website for Tiniest Cloud: React, TypeScript, Vite, Tailwind CSS and shadcn/ui, or Astro.",
5
5
  "keywords": [
6
6
  "tiniest-cloud",
@@ -327,6 +327,8 @@ apps never come out the same. Every font here is on npm as `@fontsource/<name>`
327
327
  background; a long list's search (and filters, where there's room) stays pinned under it.
328
328
  - The header's logo and last action line up with the page's edges, and nothing in it squeezes
329
329
  at a tablet's width (768px).
330
+ - Screens are built from components (the starter's and the app's own), with no bare
331
+ `<button>` or `<input>` and no hand-written stylesheet for them.
330
332
  - No control the browser draws: dates use the calendar, dropdowns the select, hints the tooltip,
331
333
  questions a dialog — all styled to the theme.
332
334
  - None of the giveaways (the design guide's "What gives generated UI away").
@@ -38,6 +38,15 @@ phone-width column for the screens that are one task; anywhere else it reads as
38
38
  and the header shrinks to one row. Keep the app's name or logo, the theme switch and the
39
39
  account at the edges. The header stays pinned at the top while the page scrolls
40
40
  (`sticky top-0`, solid background).
41
+ - **Built from components.** A screen is composed, not written out: the starter's Button,
42
+ Input and Card (src/components/ui), its AppHeader, SearchField and
43
+ FilterChips (src/components), and a component of the app's own for anything drawn more than
44
+ once (a product card, a list row, a stat tile). Styled with Tailwind classes and the theme's
45
+ colours on those components, never a stylesheet of `.card` and `.hero` rules. Anything
46
+ missing (a badge, tabs, a dialog, a select, a table, a sheet, a menu) comes from shadcn/ui
47
+ (`npx shadcn@latest add <name>`), then takes the app's style in its own file: shadcn first,
48
+ the app's style on top, nothing written by hand that shadcn has. The person will never ask
49
+ for this, and every later change depends on it.
41
50
  - **Lists are the heart.** A row per item: the main text strong, a secondary line muted,
42
51
  status as a small consistent badge, meta (date, amount) right-aligned in tabular figures.
43
52
  Search, filters and sort sit above the list and run on the server (`where`, `order`, `limit`);
@@ -19,6 +19,8 @@ React, TypeScript, Vite, Tailwind CSS and shadcn/ui, deployed to Tiniest Cloud a
19
19
  platform answers every path with index.html). Keep Vite's default `base`.
20
20
  Tiniest Cloud refuses a first deploy whose screens are anywhere else (all in App.tsx, say).
21
21
  - `src/components/` — pieces the screens share; `src/components/ui/` for shadcn components.
22
+ Anything drawn more than once is a component here with typed props (a product card, a row,
23
+ a stat tile, a page heading), never markup copied between screens.
22
24
  - `src/hooks/` — the app's own `useSomething` hooks. The SDK's hooks stay in src/lib/tiny.tsx.
23
25
  - `src/lib/` — plain code: helpers, formatting, types (`types.ts`), calculations.
24
26
  - `public/` — files served as they are (images, icons), plus `_functions/`, `tiny.rules.json`
@@ -26,19 +28,29 @@ React, TypeScript, Vite, Tailwind CSS and shadcn/ui, deployed to Tiniest Cloud a
26
28
  - `public/icon.svg` — the app's mark (the logo in its header), square, on its own background
27
29
  colour. Tiniest Cloud makes it the app's icon: browser tab, home screen, sign-in page, dashboard.
28
30
  - A file past about 300 lines is doing too much: move its parts into components and hooks.
29
- - Components: `npx shadcn@latest add <name>` adds one to src/components/ui. They are a starting
30
- point: restyle them to fit this app (radius, size, colour, spacing) in src/components/ui.
31
+ - Components: build every screen from them. Here already: `Button`, `Input`, `Card`, `Tooltip`
32
+ (src/components/ui); `AppHeader`, `SearchField`, `FilterChips`, `ThemeToggle` (src/components).
33
+ No bare `<button>`, `<input>` or `<select>` in a screen.
34
+ - Anything else (a badge, tabs, a dialog, a select, a table, a sheet, a menu, a toast) comes from
35
+ shadcn/ui, never written by hand: `npx shadcn@latest add <name>`, or without a shell the file
36
+ as shadcn publishes it, in src/components/ui. Then style it for this app in its own file.
37
+ shadcn first, the app's style on top.
38
+ - Styling: Tailwind classes on components, colours from the theme tokens (`bg-background`,
39
+ `text-muted-foreground`). src/index.css holds the palette and fonts, not `.card` or `.hero`
40
+ rules for screens.
41
+ - Header: keep `AppHeader` and its `PAGE` container, which the content shares, so the logo and
42
+ the last action line up with the page. Give it the app's name and logo; restyle, don't rewrite.
43
+ - Theme: switch only with `setTheme` (src/lib/theme.ts), which crossfades the page. Never
44
+ toggle the `dark` class yourself.
31
45
  - Design: before the first screen, read the tiny-design skill — `.agents/skills/tiny-design/`
32
46
  (the same files are in `.claude/skills/` for Claude Code): SKILL.md, then app.md. Where that
33
47
  folder is missing, `tiny_guide` with topic `app` returns the same. Write what you decide in
34
48
  DESIGN.md.
35
49
  - Look: give the app its own palette in src/index.css (the `:root` and `.dark` blocks) that suits
36
50
  what it is and who uses it — never ship the default grey. Lay each screen out for what it
37
- holds (a sidebar, a grid of cards, a table, a list beside its detail); the frame in App.tsx and
38
- the first screen in src/pages/home.tsx are placeholders, and a narrow centred column suits only
39
- one focused task. Light, dark and "match
40
- device" are chosen with `ThemeToggle` (src/components/theme-toggle.tsx); keep both themes
41
- looking right.
51
+ holds (a sidebar, a grid of cards, a table, a list beside its detail); the first screen in
52
+ src/pages/home.tsx is a placeholder, and a narrow centred column suits only one focused task.
53
+ Light, dark and "match device" are chosen with `ThemeToggle`; keep both themes looking right.
42
54
  - Their own components or brand: if the person points at a folder, a style guide or another app,
43
55
  that is the design — reuse their components and bring their colours, fonts and logo into this
44
56
  project (copied in, never linked to a path on their computer).
@@ -17,6 +17,7 @@
17
17
  "radix-ui": "^1.6.7",
18
18
  "react": "^19.2.8",
19
19
  "react-dom": "^19.2.8",
20
+ "react-router": "^8.5.1",
20
21
  "shadcn": "^4.21.0",
21
22
  "tailwindcss": "^4",
22
23
  "tw-animate-css": "^1.4.0"
@@ -1,39 +1,34 @@
1
- import { Button } from "@/components/ui/button"
2
- import { ThemeToggle } from "@/components/theme-toggle"
1
+ import { Home as HomeIcon } from "lucide-react"
2
+ import { BrowserRouter, Route, Routes } from "react-router"
3
+
4
+ import { AppHeader, PAGE, type Screen } from "@/components/app-header"
3
5
  import { useMe } from "@/lib/tiny"
6
+ import { cn } from "@/lib/utils"
4
7
  import Home from "@/pages/home"
5
8
 
9
+ /**
10
+ * The app's screens, one line each: a file in src/pages, its address, and its name and icon in
11
+ * the navigation. The header and the routes below are both drawn from this list. A detail page
12
+ * (`/orders/:id`) is a screen too, with `hidden: true` to keep it out of the navigation.
13
+ */
14
+ const screens: Screen[] = [{ path: "/", label: "Home", icon: HomeIcon, element: <Home /> }]
15
+
6
16
  export default function App() {
7
17
  const me = useMe()
8
18
  if (me === undefined) return null
9
19
 
10
20
  return (
11
- <div className="flex min-h-svh flex-col">
12
- <header className="sticky top-0 z-40 border-b bg-background">
13
- <div className="mx-auto flex h-14 w-full max-w-7xl items-center justify-between gap-4 px-4 sm:px-6">
14
- <h1 className="text-base font-medium">App</h1>
15
- <div className="flex items-center gap-3">
16
- <ThemeToggle />
17
- {me ? (
18
- <div className="flex min-w-0 items-center gap-3 text-sm text-muted-foreground">
19
- <span className="hidden truncate sm:inline">{me.email}</span>
20
- <Button variant="outline" size="sm" onClick={() => tiny.logout()}>
21
- Sign out
22
- </Button>
23
- </div>
24
- ) : (
25
- <Button size="sm" onClick={() => tiny.login()}>
26
- Sign in
27
- </Button>
28
- )}
29
- </div>
30
- </div>
31
- </header>
32
- {/* A placeholder frame around the screens in src/pages. With more than one screen, add
33
- React Router here: a route per page, the header shared by all of them. */}
34
- <main className="mx-auto w-full max-w-7xl flex-1 px-4 py-6 sm:px-6">
35
- <Home />
36
- </main>
37
- </div>
21
+ <BrowserRouter>
22
+ <div className="flex min-h-svh flex-col">
23
+ <AppHeader name="App" screens={screens} me={me} />
24
+ <main className={cn(PAGE, "flex-1 pt-6 pb-[calc(var(--nav-h)+1.5rem)]")}>
25
+ <Routes>
26
+ {screens.map((screen) => (
27
+ <Route key={screen.path} path={screen.path} element={screen.element} />
28
+ ))}
29
+ </Routes>
30
+ </main>
31
+ </div>
32
+ </BrowserRouter>
38
33
  )
39
34
  }
@@ -0,0 +1,121 @@
1
+ import type { ComponentType, ReactNode } from "react"
2
+ import { NavLink } from "react-router"
3
+
4
+ import { ThemeToggle } from "@/components/theme-toggle"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
7
+ import { cn } from "@/lib/utils"
8
+
9
+ /** A screen: a file in src/pages, listed in App.tsx. `hidden` keeps a detail page out of the navigation. */
10
+ export type Screen = {
11
+ path: string
12
+ label: string
13
+ icon: ComponentType<{ className?: string }>
14
+ element: ReactNode
15
+ hidden?: boolean
16
+ }
17
+
18
+ /** The page's width and side padding, shared by the header's row and the content so their edges line up. */
19
+ export const PAGE = "mx-auto w-full max-w-7xl px-4 sm:px-6"
20
+
21
+ /**
22
+ * The header and navigation: one line on a laptop, the screens as icons on a tablet, and a bar
23
+ * at the bottom on a phone. Restyle it to fit the app; keep the structure.
24
+ */
25
+ export function AppHeader({
26
+ name,
27
+ logo,
28
+ screens,
29
+ me,
30
+ }: {
31
+ name: string
32
+ /** `<img src="/icon.svg" alt="" className="size-7" />` */
33
+ logo?: ReactNode
34
+ screens: Screen[]
35
+ me: Tiny.User | null
36
+ }) {
37
+ const nav = screens.filter((screen) => !screen.hidden)
38
+ const many = nav.length > 1
39
+
40
+ return (
41
+ <>
42
+ <header className="sticky top-0 z-40 border-b bg-background">
43
+ <div className={cn(PAGE, "flex h-14 items-center gap-4")}>
44
+ <NavLink to="/" className="flex shrink-0 items-center gap-2 text-base font-medium">
45
+ {logo}
46
+ <span>{name}</span>
47
+ </NavLink>
48
+
49
+ {many && (
50
+ <TooltipProvider delayDuration={300}>
51
+ <nav aria-label="Screens" className="hidden min-w-0 items-center gap-1 sm:flex">
52
+ {nav.map(({ path, label, icon: Icon }) => (
53
+ <Tooltip key={path}>
54
+ <TooltipTrigger asChild>
55
+ <NavLink
56
+ to={path}
57
+ end={path === "/"}
58
+ aria-label={label}
59
+ // aria-current, not a className function: that would not survive the tooltip's trigger.
60
+ className={cn(
61
+ "flex h-9 shrink-0 items-center gap-2 rounded-full px-3 text-sm text-muted-foreground transition-colors duration-200",
62
+ "hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
63
+ "aria-[current=page]:bg-muted aria-[current=page]:text-foreground"
64
+ )}
65
+ >
66
+ <Icon className="size-4 shrink-0" />
67
+ <span className="hidden lg:inline">{label}</span>
68
+ </NavLink>
69
+ </TooltipTrigger>
70
+ <TooltipContent side="bottom" sideOffset={6} className="lg:hidden">
71
+ {label}
72
+ </TooltipContent>
73
+ </Tooltip>
74
+ ))}
75
+ </nav>
76
+ </TooltipProvider>
77
+ )}
78
+
79
+ <div className="ml-auto flex shrink-0 items-center gap-3">
80
+ <ThemeToggle className="shrink-0" />
81
+ {me ? (
82
+ <>
83
+ <span className="hidden max-w-48 truncate text-sm text-muted-foreground lg:inline">{me.email}</span>
84
+ <Button variant="outline" size="sm" className="shrink-0" onClick={() => tiny.logout()}>
85
+ Sign out
86
+ </Button>
87
+ </>
88
+ ) : (
89
+ <Button size="sm" className="shrink-0" onClick={() => tiny.login()}>
90
+ Sign in
91
+ </Button>
92
+ )}
93
+ </div>
94
+ </div>
95
+ </header>
96
+
97
+ {/* `data-bottom-nav` sets --nav-h (index.css): the page pads its bottom by it. */}
98
+ {many && (
99
+ <nav
100
+ aria-label="Screens"
101
+ data-bottom-nav
102
+ className="fixed inset-x-0 bottom-0 z-40 flex border-t bg-background pb-[env(safe-area-inset-bottom)] sm:hidden"
103
+ >
104
+ {nav.slice(0, 5).map(({ path, label, icon: Icon }) => (
105
+ <NavLink
106
+ key={path}
107
+ to={path}
108
+ end={path === "/"}
109
+ className="group flex h-16 min-w-0 flex-1 flex-col items-center justify-center gap-1 text-xs text-muted-foreground aria-[current=page]:font-medium aria-[current=page]:text-foreground"
110
+ >
111
+ <span className="flex h-7 w-14 items-center justify-center rounded-full transition-colors duration-200 group-aria-[current=page]:bg-muted">
112
+ <Icon className="size-5" />
113
+ </span>
114
+ <span className="max-w-full truncate px-1">{label}</span>
115
+ </NavLink>
116
+ ))}
117
+ </nav>
118
+ )}
119
+ </>
120
+ )
121
+ }
@@ -0,0 +1,45 @@
1
+ import { Button } from "@/components/ui/button"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ /**
5
+ * One choice from a short row of chips: a list's filter ("All · Open · Done"). Each chip is as
6
+ * wide as its words, and the row scrolls sideways when it no longer fits. Controlled: the screen
7
+ * keeps the choice (in the URL, when a link should open the same view).
8
+ */
9
+ export function FilterChips<T extends string>({
10
+ options,
11
+ value,
12
+ onChange,
13
+ label,
14
+ className,
15
+ }: {
16
+ options: ReadonlyArray<T | { value: T; label: string }>
17
+ value: T
18
+ onChange: (value: T) => void
19
+ /** What the row filters, for a screen reader: "Status". */
20
+ label: string
21
+ className?: string
22
+ }) {
23
+ return (
24
+ <div role="radiogroup" aria-label={label} className={cn("flex max-w-full gap-2 overflow-x-auto [scrollbar-width:none]", className)}>
25
+ {options.map((option) => {
26
+ const item = typeof option === "string" ? { value: option, label: option } : option
27
+ const selected = item.value === value
28
+ return (
29
+ <Button
30
+ key={item.value}
31
+ type="button"
32
+ role="radio"
33
+ aria-checked={selected}
34
+ variant={selected ? "secondary" : "outline"}
35
+ size="sm"
36
+ className="shrink-0 rounded-full px-3.5"
37
+ onClick={() => onChange(item.value)}
38
+ >
39
+ {item.label}
40
+ </Button>
41
+ )
42
+ })}
43
+ </div>
44
+ )
45
+ }
@@ -0,0 +1,45 @@
1
+ import { Search, X } from "lucide-react"
2
+
3
+ import { Input } from "@/components/ui/input"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ /** A search box: the field every list filters by. Controlled: the screen keeps the words. */
7
+ export function SearchField({
8
+ value,
9
+ onChange,
10
+ placeholder = "Search",
11
+ label = placeholder,
12
+ className,
13
+ }: {
14
+ value: string
15
+ onChange: (value: string) => void
16
+ placeholder?: string
17
+ /** What a screen reader calls it. */
18
+ label?: string
19
+ className?: string
20
+ }) {
21
+ return (
22
+ <div className={cn("relative w-full sm:w-64", className)}>
23
+ <Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
24
+ <Input
25
+ type="search"
26
+ inputMode="search"
27
+ aria-label={label}
28
+ placeholder={placeholder}
29
+ value={value}
30
+ onChange={(event) => onChange(event.target.value)}
31
+ className="h-9 rounded-full pr-9 pl-9 [&::-webkit-search-cancel-button]:hidden"
32
+ />
33
+ {value && (
34
+ <button
35
+ type="button"
36
+ aria-label="Clear search"
37
+ onClick={() => onChange("")}
38
+ className="absolute top-1/2 right-1.5 flex size-6 -translate-y-1/2 items-center justify-center rounded-full text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
39
+ >
40
+ <X className="size-3.5" />
41
+ </button>
42
+ )}
43
+ </div>
44
+ )
45
+ }
@@ -0,0 +1,102 @@
1
+ import * as React from "react"
2
+ import { cn } from "cn"
3
+
4
+ function Card({
5
+ className,
6
+ size = "default",
7
+ ...props
8
+ }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
9
+ return (
10
+ <div
11
+ data-slot="card"
12
+ data-size={size}
13
+ className={cn(
14
+ "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
23
+ return (
24
+ <div
25
+ data-slot="card-header"
26
+ className={cn(
27
+ "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
28
+ className
29
+ )}
30
+ {...props}
31
+ />
32
+ )
33
+ }
34
+
35
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
36
+ return (
37
+ <div
38
+ data-slot="card-title"
39
+ className={cn(
40
+ "font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
41
+ className
42
+ )}
43
+ {...props}
44
+ />
45
+ )
46
+ }
47
+
48
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
49
+ return (
50
+ <div
51
+ data-slot="card-description"
52
+ className={cn("text-sm text-muted-foreground", className)}
53
+ {...props}
54
+ />
55
+ )
56
+ }
57
+
58
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
59
+ return (
60
+ <div
61
+ data-slot="card-action"
62
+ className={cn(
63
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
64
+ className
65
+ )}
66
+ {...props}
67
+ />
68
+ )
69
+ }
70
+
71
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
72
+ return (
73
+ <div
74
+ data-slot="card-content"
75
+ className={cn("px-(--card-spacing)", className)}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
82
+ return (
83
+ <div
84
+ data-slot="card-footer"
85
+ className={cn(
86
+ "flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
87
+ className
88
+ )}
89
+ {...props}
90
+ />
91
+ )
92
+ }
93
+
94
+ export {
95
+ Card,
96
+ CardHeader,
97
+ CardFooter,
98
+ CardTitle,
99
+ CardAction,
100
+ CardDescription,
101
+ CardContent,
102
+ }
@@ -0,0 +1,18 @@
1
+ import * as React from "react"
2
+ import { cn } from "cn"
3
+
4
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
5
+ return (
6
+ <input
7
+ type={type}
8
+ data-slot="input"
9
+ className={cn(
10
+ "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export { Input }
@@ -8,19 +8,6 @@
8
8
  @theme inline {
9
9
  --font-heading: var(--font-sans);
10
10
  --font-sans: 'Geist Variable', sans-serif;
11
- --color-sidebar-ring: var(--sidebar-ring);
12
- --color-sidebar-border: var(--sidebar-border);
13
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
14
- --color-sidebar-accent: var(--sidebar-accent);
15
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
16
- --color-sidebar-primary: var(--sidebar-primary);
17
- --color-sidebar-foreground: var(--sidebar-foreground);
18
- --color-sidebar: var(--sidebar);
19
- --color-chart-5: var(--chart-5);
20
- --color-chart-4: var(--chart-4);
21
- --color-chart-3: var(--chart-3);
22
- --color-chart-2: var(--chart-2);
23
- --color-chart-1: var(--chart-1);
24
11
  --color-ring: var(--ring);
25
12
  --color-input: var(--input);
26
13
  --color-border: var(--border);
@@ -67,20 +54,7 @@
67
54
  --border: oklch(0.922 0 0);
68
55
  --input: oklch(0.922 0 0);
69
56
  --ring: oklch(0.708 0 0);
70
- --chart-1: oklch(0.87 0 0);
71
- --chart-2: oklch(0.556 0 0);
72
- --chart-3: oklch(0.439 0 0);
73
- --chart-4: oklch(0.371 0 0);
74
- --chart-5: oklch(0.269 0 0);
75
57
  --radius: 0.625rem;
76
- --sidebar: oklch(0.985 0 0);
77
- --sidebar-foreground: oklch(0.145 0 0);
78
- --sidebar-primary: oklch(0.205 0 0);
79
- --sidebar-primary-foreground: oklch(0.985 0 0);
80
- --sidebar-accent: oklch(0.97 0 0);
81
- --sidebar-accent-foreground: oklch(0.205 0 0);
82
- --sidebar-border: oklch(0.922 0 0);
83
- --sidebar-ring: oklch(0.708 0 0);
84
58
  }
85
59
 
86
60
  .dark {
@@ -102,19 +76,6 @@
102
76
  --border: oklch(1 0 0 / 10%);
103
77
  --input: oklch(1 0 0 / 15%);
104
78
  --ring: oklch(0.556 0 0);
105
- --chart-1: oklch(0.87 0 0);
106
- --chart-2: oklch(0.556 0 0);
107
- --chart-3: oklch(0.439 0 0);
108
- --chart-4: oklch(0.371 0 0);
109
- --chart-5: oklch(0.269 0 0);
110
- --sidebar: oklch(0.205 0 0);
111
- --sidebar-foreground: oklch(0.985 0 0);
112
- --sidebar-primary: oklch(0.488 0.243 264.376);
113
- --sidebar-primary-foreground: oklch(0.985 0 0);
114
- --sidebar-accent: oklch(0.269 0 0);
115
- --sidebar-accent-foreground: oklch(0.985 0 0);
116
- --sidebar-border: oklch(1 0 0 / 10%);
117
- --sidebar-ring: oklch(0.556 0 0);
118
79
  }
119
80
 
120
81
  @layer base {
@@ -129,6 +90,17 @@
129
90
  }
130
91
  }
131
92
 
93
+ /* The height of the phone's bottom navigation (src/components/app-header.tsx), 0 when there is
94
+ none: the page pads its bottom by it, and anything else pinned to the bottom sits above it. */
95
+ :root {
96
+ --nav-h: 0px;
97
+ }
98
+ @media (width < 40rem) {
99
+ :root:has([data-bottom-nav]) {
100
+ --nav-h: calc(4rem + env(safe-area-inset-bottom));
101
+ }
102
+ }
103
+
132
104
  /* The theme switch crossfades the page (src/lib/theme.ts). Short, slow at both ends and quick
133
105
  through the middle, so the halfway blend of two opposite themes never registers. */
134
106
  ::view-transition-old(root),
@@ -22,11 +22,8 @@ function apply(theme: Theme) {
22
22
  }
23
23
 
24
24
  /**
25
- * Switch the theme, with the page crossfading from one to the other (the timing is in
26
- * index.css, `::view-transition-*`). A snapshot fade rather than easing every colour: text and
27
- * background travel in opposite directions, and eased one by one they meet in the same grey
28
- * halfway. Where the browser has no view transitions, or the person asked for less motion, the
29
- * theme just changes.
25
+ * Switch the theme with a crossfade of the whole page (timed in index.css). Where the browser
26
+ * has no view transitions, or the person asked for less motion, the theme just changes.
30
27
  */
31
28
  export function setTheme(theme: Theme) {
32
29
  document.cookie =
@@ -1,9 +1,10 @@
1
1
  /**
2
- * The first screen. Every screen is a file in src/pages; App.tsx frames them and, once there is
3
- * more than one, routes to each (React Router). Lay this one out for what it holds: a sidebar for
4
- * several areas, a grid of cards for things people browse, a table for records, a list with its
5
- * detail beside it; a narrow centred column only for one focused task. Pieces it shares with
6
- * other screens go in src/components, its own hooks in src/hooks.
2
+ * The first screen. Every screen is a file in src/pages, listed in App.tsx (its address, its name
3
+ * and icon in the navigation). Lay this one out for what it holds: a grid of cards for things
4
+ * people browse, a table for records, a list with its detail beside it; a narrow centred column
5
+ * only for one focused task. Build it from components: Button, Input and Card in
6
+ * src/components/ui (more from shadcn), SearchField and FilterChips in src/components, and a
7
+ * component of this app's own for anything drawn more than once. Its hooks go in src/hooks.
7
8
  */
8
9
  export default function Home() {
9
10
  return <div />