- name
- weft-frontend
- description
- Read when the user wants a page, app or site for the program, and before dispatching the frontend-builder: the verified scaffold commands, the default stack (pnpm, SvelteKit, PostgreSQL, BetterAuth, shadcn-svelte), calling the program's own routes and signal doors, one shared PostgreSQL, where the api token lives, pictures as links, the build, and the shape for a site whose people each bring their own accounts or settings (members: the manager routes, the member header and token, their settings page).
# The frontend
[the frontend] is the pages where a person starts a run, answers a parked
question, or reads what came out. It lives in `front/`, with its own
toolchain, as a client of [the program], never a second backend.
[the program] is the weft graph in `src/`, with its `Route` nodes (the
`weft-api` skill) and its signals (the `weft-consumers` skill). The `frontend-builder` specialist builds [the frontend] and reads
this skill; Tangle reads it whenever the request touches [the frontend].
## The default stack
If the user names a stack, that stack wins: you never override a technology
the user asked for. If they name nothing, you build with these and ask no
question about them:
- **pnpm**, the package manager, and the only one you use.
- **SvelteKit**, the framework (Svelte).
- **PostgreSQL**, the database.
- **BetterAuth**, the authentication.
- **shadcn-svelte**, the components.
You add a piece only when the request needs it.
## Starting the project
Every command below runs without a prompt; this exact chain has been run and
builds in under three seconds. You run it from the project root, one line at
a time, each with a timeout of at most 60 seconds:
```bash
pnpm dlx sv@0.17.0 create front --template minimal --types ts --no-add-ons --no-install
pnpm dlx sv@0.17.0 add tailwindcss="plugins:none" --no-install --cwd front --no-git-check --no-download-check
cd front && pnpm add clsx tailwind-merge && pnpm add -D tw-animate-css shadcn-svelte@1.6.1 && pnpm install
```
The Tailwind add-on writes `src/routes/layout.css`. `shadcn-svelte init`
cannot be made quiet (it asks before it touches the stylesheet whatever flags
it gets), so you never run it: you write the three files it would have
written, then add components with `--yes --overwrite` (without
`--overwrite`, `add` still asks "overwrite all existing files?" as soon as one
of its files is there):
`front/components.json`:
```json
{
"$schema": "https://shadcn-svelte.com/schema.json",
"tailwind": { "css": "src/routes/layout.css", "baseColor": "zinc" },
"aliases": { "components": "$lib/components", "utils": "$lib/utils", "ui": "$lib/components/ui", "hooks": "$lib/hooks", "lib": "$lib" },
"typescript": true,
"registry": "https://shadcn-svelte.com/registry"
}
```
`front/src/lib/utils.ts`:
```ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export type WithoutChild<T> = T extends { child?: unknown } ? Omit<T, 'child'> : T;
export type WithoutChildren<T> = T extends { children?: unknown } ? Omit<T, 'children'> : T;
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
```
`front/src/routes/layout.css`, replacing the one line the Tailwind add-on
wrote. These are the zinc colours `init` writes; the components' classes
(`bg-destructive`, `border-input`, `ring-ring`) name them, so without this
block a destructive button renders with no colour at all:
```css
@import 'tailwindcss';
@import "tw-animate-css";
@import "shadcn-svelte/tailwind.css";
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
}
@theme inline {
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
```
Then, still in `front/`:
```bash
pnpm dlx shadcn-svelte@1.6.1 add button card input --yes --overwrite --no-deps-install && pnpm install
pnpm run build
```
If any command asks a question anyway, you kill it and report what it asked;
you never answer it. A build takes seconds: you give it 30, and if it
overruns, you kill it and report the output.
## Reaching the program's own infrastructure
[the program] sometimes runs infrastructure of its own: a database, a cache, a
broker, whatever its author gave it. When [the frontend] needs to speak to one
of those directly, in that thing's own protocol, it goes through [the door].
[the door] is a piece of [the program]'s infrastructure that its node declared
reachable from this machine. You find them yourself:
````
weft infra list-doors
````
Each line is a node, one of its endpoints, and the address it answers on
(`db.sql 127.0.0.1:30080`). Nothing is listed unless the node that runs it
declared it reachable, so the listing answers "what is reachable from here".
Empty means nothing is, and that is not a thing you can change from here.
A door that is listed answers only once its infrastructure is running,
and nothing starts a piece the program itself never touches (a database only
the site's sign-in uses): if yours does not answer, ask the orchestrator to
run `weft infra start`.
**A door is an address, not a key.** Being reachable and being allowed in are
different questions, and the listing only answers the first: nearly everything
worth reaching still asks you to prove who you are. So a listed door with no
credential is not something to work around, it is the second half of the job,
and you ask the orchestrator for it rather than hunting for it yourself.
Where the credential comes from is the node's business, and the node says so
on its card in the graph: the readouts there are how a piece of infrastructure
hands out or resets what it needs, and the node's own description in the
catalog names what it offers. If you want values the node shows on its card (a
database's user, or a masked `secret` line like its password) in [the
frontend]'s server environment, one command writes them all, `weft infra env <node> --into <server env file> --set
DATABASE_USER=User --set DATABASE_PASSWORD=Password --set DATABASE_NAME=Database`, each `NAME=Label` taking
the card item under that label (`weft infra show <node>` lists the labels, and
`--as <NAME>` is the short form for the card's only secret). It prints the
plain values it wrote and never a secret's, so a secret never passes through
you. Plain values it cannot write (the door's host and port, from `weft infra
list-doors`) you add to the same file yourself. The file is server-side only,
never anywhere a browser can read.
A secret the node hands out once (a database's password) has usually been
taken by the time anyone looks, so expect `env` to say it was handed over
already. The flow is the same every time: read the card (`weft infra show
<node>` lists its items and its buttons), press the button that issues a new
one if the secret is gone (`weft infra press <node> <action>`), then write the
values with `weft infra env`. The button's warning (`weft infra show` prints it) says what a new secret
cuts off; if something already uses the old one, cutting it off is the user's
call, so you ask the orchestrator before pressing.
Run each of these as one plain command from the project folder, exactly as
written above: no `cd` in front, no `&&`, no pipe, and `--into front/.env`
for the path. The permission checker approves `weft infra ...` only when the
command is written that way; wrapped in anything else, it goes to an
automatic check that refuses writing a password into a file. If one is
refused anyway, you are a subagent: put the exact command in your report and
go on with the rest, and the orchestrator runs it and tells you when the file
is written. Nobody goes round a refusal by reading the secret or copying it
onto a command line; if the orchestrator is refused too, it hands the user
the exact command.
**If you need a door that is not listed, you report it and stop on that part.**
Whether a piece of infrastructure is reachable is part of what [the program]
is, written in the node that runs it, so it is the orchestrator's to change and
never yours. Say which infrastructure you need and why, and the orchestrator
either makes it reachable or builds what was missing and dispatches you again.
You never go round it. If you catch yourself running `kubectl`, reading a
node's source for a password, or opening a shell in a container, stop and write
verbatim "Wait. A door is listed or it does not exist." Then report and work on
something else meanwhile.
**You never stand up your own copy of something [the program] runs**, and you
never tell the user the two halves cannot share it. A second database beside
the program's, a second cache, a second broker: each is two sources of truth
在 GitHub 查看