@trypattern/sdk 0.1.38 → 0.1.39

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/docs/look.md ADDED
@@ -0,0 +1,147 @@
1
+ # The look: primitives, theme, tokens and icons
2
+
3
+ The look is Pattern's, and everything on screen is drawn by its primitives from
4
+ `@trypattern/sdk/react`. An app changes the look through its theme, never by restyling a primitive.
5
+
6
+ ## Frame
7
+
8
+ - `AppFrame`: the sidebar, phone drawer and page panel, for a signed-in area, from `links` and the
9
+ layout model's commands (it adds Log out). A link's `count` shows beside it, and
10
+ `countTone: 'attention'` makes it a solid blue badge for something waiting, whatever the brand
11
+ colour. Following a sidebar link closes the phone drawer by itself.
12
+ - `look="plain"`: a white sidebar with the app's name, its links and the signed-in person at the
13
+ bottom, whose menu is `accountMenu`.
14
+ - `look="shell"` (the default): a grey frame with the page inset, a top bar and a `workspaceMenu`
15
+ at the top.
16
+ - `Screen` follows the frame's look; in the plain look it is a heading with `title`, an optional
17
+ `eyebrow` line above it or a `parent` link, `actions` beside it, and the page below in a column
18
+ `width` `xs` to `lg` or `full` (`md` by default). On a phone it shows the button that opens the
19
+ drawer.
20
+ - Also `SignInScreen` (a ready-made sign-in page), `CommandPalette` and `Toaster`.
21
+
22
+ ## Layout
23
+
24
+ - `Content` (the centred page column, `width` `xs` to `lg` or `full`), `Stack` (a column) and `Row`
25
+ (side by side), each with `gap` and `padding` named from `space` (`gap="md"`).
26
+ - `Card`: with `to` the whole card is a link, and then `hover="zoom"` gently zooms its photo,
27
+ `hover="outline"` darkens its edge and `isPressable` presses it in, all still for people who turn
28
+ motion off; `surface="none"` for one with no background, like a photo tile.
29
+ - `Divider` (with `label="or"` for a labelled one), `SettingsRow` (a title and description beside a
30
+ control, stacked on phones), `PropertyRow` (a label and its value, side by side on every screen).
31
+ - `ListRow`: a row that opens a page with `to`, another site with `href`, or runs `onClick`;
32
+ `leading`, `trailing`, `size` `sm` (a list beside the page, which shades on hover), `md` or `lg`
33
+ (rows in a card, which stay still), and `isCurrent` on the one showing.
34
+ - `TableRow`: a row of a table that opens something: `label`, `isCurrent` on the open one, `onClick`,
35
+ and `size="lg"` for two-line rows.
36
+ - `EmptyState` (a message and an action, centred), `LoadingRows` (`count` placeholder rows while a
37
+ list loads) and `Form` (hands `onSubmit` the fields' values).
38
+ - `Header` wraps a site's top bar so it stays at the top of the page, clear at the top and frosted
39
+ with a hairline once scrolled, above the page and below menus and dialogs.
40
+
41
+ ## Words
42
+
43
+ - `Text` with a `variant`:
44
+ - for a website: `hero` (the home page's big line), `display` (a page's title), `section` (a
45
+ section's heading), `intro` (the line under the hero) and `lead` (the site's 15px body text);
46
+ - everywhere: `heading`, `subheading`, `title`, `label`, `body` (small print), `caption` and
47
+ `code`.
48
+
49
+ Also a `tone` (`primary`, `secondary`, `tertiary`, `brand`, `danger`, `positive`, `warning`, or
50
+ `inherit`), `weight` (`normal`, `medium`, `semibold`, `bold`, `heavy`) for emphasis, `as` (`p`,
51
+ `h1`, `h2`, `h3`) for screen readers, `align`, `isTruncated` and `isSelectable`.
52
+
53
+ - `Link` is words that go somewhere: `to="/flowers"` for a page here, `href="https://…"` for
54
+ another site (it opens in a new tab), and `Button` takes either too. `Link` and `Badge` take
55
+ `tone="positive"` for something live or done.
56
+ - `Appear` wraps something that shows up after the page opens (a button once there is something to
57
+ act on): it fades in without moving what is already there.
58
+
59
+ ## Controls
60
+
61
+ - `Button` (with `to` it opens a page). A full-width `Button` (`isFullWidth`) centres its label;
62
+ `align="start"` puts it at the start.
63
+ - `ContinueWithGoogle` (the Google sign-in button, with Google's logo), `AppearancePicker` (Light,
64
+ Dark or System; apps start light).
65
+ - `IconButton` (`variant="primary"` for a brand-coloured one, `shape="round"`).
66
+ - `Calendar`: a month of `open`, `closed` and `full` (crossed-out) days; `weekStart="sunday"` starts
67
+ its weeks on Sunday. It fills the width it is given.
68
+ - `TextField`, `Select` (its `label` is read aloud only; `isLabelHidden={false}` shows it above,
69
+ like a `TextField`'s), `Switch`.
70
+ - `Checkbox` (a round tick, or `shape="square"`; its `label` is for screen readers).
71
+ - `SegmentedControl` (an option's `count` shows beside its label).
72
+ - `RadioGroup`: a card of choices, each with a `label`, optional `description` (a second line),
73
+ `badge`, `content` and `trailing`, and `isDisabled`; `size="lg"` for a roomier one, like an item's
74
+ sizes.
75
+ - `ChoiceGroup`: a row of `Choice`s, words with a `title` and `subline`, or photos, like an item's
76
+ thumbnails.
77
+ - `CopyButton` (copies `text` and says Copied).
78
+ - `Menu` and `ContextMenu`: entries run `onSelect` or open a page with `to`, with an `icon` or a
79
+ `mark` (the first letter of a name in a small brand tile, as for switching between businesses).
80
+ - `Dialog`: `placement="sheet"` is a bottom sheet on phones, with a grab handle to drag it closed,
81
+ and a centred dialog on a computer; keep a close button in it. Its content scrolls by itself when
82
+ taller than the screen; to keep a header and footer in place, put the middle in a `ScrollArea`
83
+ between them.
84
+ - `Tooltip` and `Kbd`.
85
+ - `Image` (a photo from the `media` table; `origin` shows one from another site).
86
+ - `Gallery`: several photos one at a time, swipe on phones, arrows on hover on a computer, dots and
87
+ an "N of M" count. `value`/`onChange` take the shown photo's id, and on phones it runs edge to edge
88
+ when given negative `marginInline` equal to the page gutter.
89
+ - `Avatar`, `Badge`, `Icon` (with a `tone`, and `weight="light"` for a thinner line), `Spinner`,
90
+ `ScrollArea`.
91
+ - `Skeleton`: `text="heading"`, `"lead"`, `"label"` or another `Text` variant takes exactly one line
92
+ of that text, so a page that is loading has the same layout as once it has loaded. Draw loading in
93
+ the final layout, never a separate loader first.
94
+ - `FilePicker`: a drop zone with a button that picks a file (`title`, `hint`, `buttonLabel`,
95
+ `accept`, the file types); `onPick` hands a command the file as plain data, `{ id, name, type, size }`,
96
+ so the model checks its type and size and a server call sends it. `FilePreview` shows a picked
97
+ photo straight away, before it uploads (`file`, `alt`). `ProgressBar` (`label` and `value` from 0
98
+ to 1) shows an upload.
99
+
100
+ ## Rules for controls and styles
101
+
102
+ - Controls take closed choices: `variant` (`primary`, `secondary`, `tertiary`, `transparent`),
103
+ `tone` (`neutral`, `brand`, `danger`) and `size` (`xs` to `xl`). A `primary` button is the brand
104
+ colour unless it says `tone="neutral"`, so a brand colour shows everywhere at once; the same `size`
105
+ is the same height for a button, a field and a select, so a row lines up.
106
+ - A primitive's `style` prop takes placement only (`alignSelf`, `flexGrow`, margins, `minWidth`, and
107
+ `display: 'none'` to hide it at a breakpoint). Its colors, sizes, states and motion are not the
108
+ app's to change.
109
+ - A screen has no styles, no tokens and no plain elements: it arranges primitives and the app's
110
+ pieces.
111
+ - A piece in `src/components/<Name>/` may draw a shape no primitive gives (a booking calendar, a
112
+ photo grid) with `stylex.create`, where every value is a token from
113
+ `@trypattern/sdk/tokens.stylex`: `space.*` for paddings, gaps and margins, and `controls.*`,
114
+ `iconSizes.*` or `widths.*` for sizes. Words are `<Text>`, anything clickable is a primitive, and
115
+ nothing changes on hover (hover effects are the primitives' own, like `Card`'s `hover`). Such a
116
+ piece is listed as the app's own in its design system.
117
+ - Never write `<button>`, `<input>`, `<p>`, `<h1>`, words loose in a `<div>`, or `onClick` on a
118
+ `<div>`: use the primitive, and if none fits, the gap is in Pattern.
119
+ - There is one breakpoint, `breakpoints.compact` (phone width), and no hand cursor: both are
120
+ Pattern's choice.
121
+
122
+ ## Theme
123
+
124
+ An app changes the look in two files, and `src/main.tsx` hands both to
125
+ `startApp(App, { theme, icons })`.
126
+
127
+ - `src/theme/theme.ts` re-values tokens with `stylex.createTheme`:
128
+ - `colors`, each as `light-dark(light, dark)`. This is the only file where raw colors are allowed.
129
+ - `radii`, and `corners`: `buttonMd`, `fieldMd` and the other sizes are buttons' and fields'
130
+ corners, which follow `radii` until set, so pill buttons can sit beside squarer fields;
131
+ `checkbox` is a square `Checkbox`'s corner, 4px whatever `radii` says.
132
+ - `fonts`: `sans` for words, `heading` for the hero, display, section, heading and subheading
133
+ variants, `mono` for code. Load a font by adding its `@fontsource` package and importing it in
134
+ `src/index.css`.
135
+ - `fontWeights` (`heading` is the headings' weight) and `trackings` (letter spacing).
136
+ - `shadows`, and the size scales `space`, `fontSizes`, `lineHeights`, `controls`, `iconSizes`,
137
+ `borders` and `widths` to make the whole app roomier or tighter; every `Text` variant and
138
+ control moves with them. `fontSizes.bodyCompact` and `lineHeights.bodyCompact` are the body and
139
+ label size on phones; set them to the desktop size to keep text from growing there.
140
+ - Sizes are whole pixels, and `app check` refuses a theme where text would clip or a scale
141
+ shrinks.
142
+ - The business's own named colours go in `src/theme/colors.stylex.ts`,
143
+ `export const brand = stylex.defineVars({ sage: 'light-dark(#e3ebe0, #1f2a1d)' })`, and a piece
144
+ uses `brand.sage`; keep `colors.*` for what they mean (`warning` stays a warning).
145
+ - `src/theme/icons.ts` adds the app's icons with `defineIcons({ name: 'M5 12h14' })`: a path is
146
+ drawn as a 2px line, and `{ fill: '...' }` is filled. Several paths are drawn as one shape, so
147
+ lines don't darken where they meet.
@@ -0,0 +1,115 @@
1
+ # Requests: forms, bookings and owner emails
2
+
3
+ Requests are what people send for the owner to answer: bookings, forms (orders, quotes, contact messages, early-access requests) and anything your own server code takes in, like a custom checkout. Each kind is a request source. For every one, Pattern saves the request, emails the owner straight away (with the visitor as reply-to and a link to it), keeps trying until that email goes through, and lists it on onpattern.app under Requests. There the owner accepts or declines a booking, replies, marks it done or runs one of your source's own actions. Your part is to declare the source and build the visitor's side. Never build an admin page for requests, and never write an "email the owner" step.
4
+
5
+ ## Forms: `inquiry()`
6
+
7
+ - A form's rows live in the app's own table, whose `create` access is `'server'` and which has no status column: whether it's answered is Pattern's.
8
+ - Declare it in `schema.ts`: `export const orders = inquiry(tables.requests, { source: 'orders', title: 'Flower order', form: 'Order form', name: 'name', email: 'email', fields: { date: 'Wanted by', budget: 'Budget' }, summary: 'budget', message: 'message', when: { field: 'date', detail: 'Delivery, any time' } })`.
9
+ - `title` names one request and `form` the form it came from.
10
+ - `fields` are the columns the owner sees, with their labels, and `summary` the one shown in the list.
11
+ - `when` is the date column, if there is one.
12
+ - Serve it with `inquiries([orders])` in `server/worker.ts`'s `blocks`, and send it from a model with `await this.stores.inquiries.send('orders', { name, email, … })`, which returns `'saved'` or another outcome to show a toast for.
13
+ - A second form is a second table and a second `inquiry()`, listed in the same `inquiries([...])`.
14
+ - A site with forms and no bookings adds `...requestTables` from `@trypattern/sdk/schema` to `tables` (bookings already include them); `app migration` then creates them.
15
+ - Flows fake sending: `world.inquiries.sent` lists what was sent, and `stores.inquiries.sent` is the last one.
16
+
17
+ ## Bookings
18
+
19
+ The bookings block is a request source of its own: each booking request reaches the owner like any other request, with Accept and Decline.
20
+
21
+ - Wire it in three places: `blocks: [bookings]` in `src/App.ts`, `bookings({ timezone })` in `server/worker.ts` and `...bookingTables` in `schema.ts`.
22
+ - The block decides what is available; the app's components decide what it looks like. `this.stores.bookings` has `loadDays(month)`, `loadWindows(date)` and `request(fields)`, and `state.days` and `state.windows` hold the answers. Show them with the `Calendar` primitive, a list of days or a single date field, but never work out availability yourself.
23
+ - Opening hours are the `bookingRules` row. Flows fake the block with `world.bookings`.
24
+
25
+ ## Drafts from Claude
26
+
27
+ `app requests` lists the live site's requests, and `app requests draft <source/id> --text '…' --asked '<their words>'` writes a reply for the owner, who reads it on onpattern.app and presses Send. Nothing reaches the visitor until they do. Deciding, marking done and running actions are the owner's, on onpattern.app.
28
+
29
+ ## Your own server code
30
+
31
+ When your own server code takes something in that the owner should answer (a custom checkout, a quote calculator), declare it as a request source and record each request in the same batch as its row.
32
+
33
+ ### 1. Declare the source
34
+
35
+ In `schema.ts`, next to the table that holds the rows:
36
+
37
+ ```ts
38
+ import { requestSource } from '@trypattern/sdk/server'
39
+
40
+ export const rentals = requestSource(tables.rentals, {
41
+ source: 'rentals',
42
+ kind: 'booking',
43
+ title: 'Rental booking',
44
+ form: 'Checkout',
45
+ name: 'name',
46
+ email: 'email',
47
+ fields: { chairs: 'Chairs', eventDate: 'Event day' },
48
+ summary: 'chairs',
49
+ when: { field: 'eventDate', detail: 'Pickup the day before' },
50
+ actions: {
51
+ markFulfilled: {
52
+ label: 'Mark fulfilled',
53
+ resolves: true,
54
+ can: (row) => row['stage'] === 'paid',
55
+ run: ({ d1, id }) => [
56
+ d1
57
+ .prepare(
58
+ "UPDATE rentals SET stage = 'fulfilled' WHERE id = ?1 AND stage = 'paid'"
59
+ )
60
+ .bind(id)
61
+ ]
62
+ }
63
+ }
64
+ })
65
+ ```
66
+
67
+ - `source` is a stable camelCase id. Never rename it once the site is live.
68
+ - The table's `create` access is `'server'`: only your server code adds rows, so every row is also a request.
69
+ - `title`, `form`, `name`, `email`, `fields`, `summary`, `message` and `when` say how the request reads in the list and the side sheet, as for `inquiry()`.
70
+ - Every request can always be answered with **Reply** (an email from the dashboard) and **Mark done**. Don't declare those.
71
+ - `actions` adds the decisions only your business has:
72
+ - `accept` and `decline` keep their usual buttons. Pattern emails the customer that the owner confirmed or declined.
73
+ - Any other camelCase name is a custom action: `label` is the words on its button, and `resolves` says whether it finishes the request (`true`) or leaves it open (`false`). Custom actions send no email.
74
+ - `can(row)` says whether the button shows for this row right now. Pattern asks again just before running it.
75
+ - `run({ d1, id, row })` returns the D1 statements that make the change. Each statement must change at least one row, or the whole action is undone and the owner sees "That can't be done to this request now". Write the business check into the statement itself (`WHERE stage = 'paid'`), so a change that happened in between stops it.
76
+
77
+ ### 2. Serve it
78
+
79
+ In `server/worker.ts`, add the block, and add `...requestTables` to `tables` in `schema.ts` if it isn't there yet (then run `app migration`):
80
+
81
+ ```ts
82
+ import { createServer, requestSources } from '@trypattern/sdk/server'
83
+ import { rentals } from '../schema.ts'
84
+
85
+ export default createServer({
86
+ tables,
87
+ admins,
88
+ blocks: [requestSources([rentals])]
89
+ })
90
+ ```
91
+
92
+ ### 3. Record each request in the same batch as its row
93
+
94
+ ```ts
95
+ import { deliverRequestNotices, recordRequest } from '@trypattern/sdk/server'
96
+
97
+ const id = `rental-${checkoutKey}`
98
+ const row = { id, name, email, chairs, eventDate, stage: 'paid' }
99
+ await c.env.DB.batch([
100
+ c.env.DB.prepare(
101
+ 'INSERT INTO rentals (id, name, email, chairs, event_date, stage) VALUES (?1, ?2, ?3, ?4, ?5, ?6)'
102
+ ).bind(id, name, email, chairs, eventDate, 'paid'),
103
+ ...recordRequest(c, rentals, row)
104
+ ])
105
+ await deliverRequestNotices(c)
106
+ ```
107
+
108
+ - The row, the request, its history and the owner's email are saved together: if the batch fails, none of them exist.
109
+ - The row's `id` is the idempotency key. Derive it from something stable, like the checkout's own key, so a retried checkout records one request and sends one email.
110
+ - `deliverRequestNotices(c)` sends the owner's email after the response. If it can't be sent, Pattern keeps trying on its own, without waiting for another visit, and the dashboard shows the failure.
111
+
112
+ ### What Claude may and may not do
113
+
114
+ - Claude reads requests and drafts replies with `app requests`. Only the owner, signed in on onpattern.app, can send a reply or run an action.
115
+ - An action with an effect outside the database, like refunding a Stripe payment, needs a durable job with its own pending, succeeded and failed states. It can't happen inside `run`, because a database batch can't include Stripe. Don't present a refund as `decline`. Payment state comes only from verified Stripe events.
package/docs/state.md ADDED
@@ -0,0 +1,142 @@
1
+ # State: stores, models, actions, screens and flows
2
+
3
+ A Pattern app is a React + TypeScript single-page app (Vite, React Router in data mode, StyleX and
4
+ the components from `@trypattern/sdk/react`). State flows through four layers, and each reads only
5
+ the one below it:
6
+
7
+ ```
8
+ network → service → STORE → MODEL → SCREEN
9
+ (Pattern) data, state, reads one model
10
+ cached answers,
11
+ commands
12
+ ↑
13
+ ACTIONS menu entries and shortcuts that send commands
14
+ flows, ⌘K, shortcuts and `app send` change things only through commands on models
15
+ ```
16
+
17
+ ## Code conventions
18
+
19
+ - Import project files by their path from the project root: `@/src/models/Item.ts`,
20
+ `@/schema.ts`. Only a file in the same folder may be `./File.ts`. Export only what another file
21
+ imports, and import things from where they are defined, never through a file that re-exports
22
+ them.
23
+ - Let TypeScript infer return types. Write one only on functions exported from `src/lib/`, which
24
+ several files rely on, and where TypeScript asks for it (a function that calls itself).
25
+
26
+ ## Stores
27
+
28
+ `src/stores/<Name>.ts`, listed in `src/stores/index.ts`: state several models share.
29
+
30
+ - Pattern gives every table in `schema.ts` a store of the same name (`items`: `items`, `status`,
31
+ `pending`; `load()`, `create()`, `update()`, `remove()`, cached in the browser per person), and a
32
+ read-only `public<Table>` store for a table with `public` columns.
33
+ - Built-in stores: `session` (the signed-in user), `actions` (everything the command menu and
34
+ keyboard shortcuts can do right now, worked out by the files in `src/actions/`), `toasts` (messages
35
+ on screen: `this.stores.toasts.show({ tone: 'error', title: 'Couldn’t save' })`, with `neutral`,
36
+ `success` or `error`), `page` (the open page and its address values) and `device`
37
+ (`state.today`, today's date as `'2026-10-18'`, kept current past midnight, and
38
+ `state.isCompact`, whether the screen is phone width).
39
+ - Your own are `class X extends Store({ state, persist })` with plain methods. A store calls the
40
+ app's own server calls through `this.services`, typed for this app.
41
+
42
+ ## Models
43
+
44
+ `src/models/<Name>.ts`, listed in `src/models/index.ts` under its page's route id.
45
+
46
+ - `class X extends Model({ state, persist })`, or `Model()` with neither. `state` fields are zod
47
+ schemas with `.default()`; `persist` lists fields kept in this browser across reloads (never on the
48
+ server). Every store is there as `this.stores.<name>`.
49
+ - A model shows three kinds of public member: getters are its answers, worked out from its state and
50
+ stores (`public get hasNext()`); `command()` fields are what it can do
51
+ (`public openNext = command(() => ...)`, or `command({ input: { ... } }, (input) => ...)`); and
52
+ `onPageOpen(params)` runs when its page opens, which can happen twice for one visit (signed out,
53
+ then again after sign-in), so it only ever loads and never assumes it ran once.
54
+ - `this.params` holds the open page's address values (`this.params['itemId']` on
55
+ `/flowers/:itemId`), so a detail page answers `get itemId()` from it rather than copying it into
56
+ state. A model behind sign-in reads `this.stores.session.state.user` rather than assuming someone
57
+ is signed in.
58
+ - Everything else is `private`, and a helper is a private method, never a getter.
59
+ - A getter is pure: it works its answer out from `this.state` and `this.stores.<name>.state` only,
60
+ returns plain JSON, and never changes anything, waits, or reads the clock, randomness or a private
61
+ field. Pattern caches each getter until something it read changes, so an answer several getters
62
+ need is its own getter (`public get neighbours()`). Today's date and phone width come from the
63
+ `device` store, so getters can use both.
64
+ - Every model can call `this.app.navigate(path)`. Models never call or read other models: share
65
+ state through a store, and share work through a function in `src/lib/`. A model knows nothing
66
+ about menus or which page is open.
67
+
68
+ ## Actions
69
+
70
+ `src/actions/<thing>.ts`, listed in `src/actions/index.ts`. If a person can see it in a menu or
71
+ press a key for it, it is an action; what it does is a command.
72
+
73
+ - One file per thing the actions act on (`navigation.ts`), each
74
+ `export const navigationActions = defineActions(({ page, models }) => [...])` returning entries
75
+ built with `action()` from `@trypattern/sdk`: an id, a label, a group, an icon, optional keys and
76
+ the commands it sends.
77
+ - `page` is the open page's route id and `models.<name>` is that model's state and answers, so an
78
+ entry shows only when it applies (`models.item.hasNext && action(...)`). Actions that belong to
79
+ one page say so, `defineActions({ page: 'item' }, ({ models }) => [...])`, and Pattern offers them
80
+ only while that page is open.
81
+ - An actions file is pure like a getter, and Pattern reruns it only when something it read changes.
82
+ The ⌘K menu orders entries by the `actionGroups` list in `src/actions/index.ts`, and a group not in
83
+ that list is a type error.
84
+ - Pattern listens for the keys itself: an action's `keys` are a single key (`['c']`), a sequence
85
+ (`['g', 'i']`) or a combo with ⌘ on a Mac and Ctrl elsewhere (`['mod+k']`), and `isHidden: true`
86
+ keeps a shortcut-only action out of the menu.
87
+
88
+ ## Screens and layouts
89
+
90
+ - A screen (`src/screens/<Name>Screen.tsx`) reads one model, `const item = useModel('item')` from
91
+ `@trypattern/sdk/react`, shows `item.sizes` and calls `item.chooseSize()`. It never reads a store;
92
+ if it needs something, add a getter to the model. A screen arranges primitives and has no styles
93
+ of its own (see [the look](look.md)).
94
+ - Pieces only one screen uses go in `src/components/<Name>/` and take props; only screens and layouts
95
+ call `useModel`. Pass a model's commands straight to list rows (`onDelete={list.deleteItem}`), not
96
+ a new arrow function, so rows only redraw when their own data changes.
97
+ - Layouts (`src/layouts/<Name>Layout.tsx`) are the frames pages render inside, and they are the
98
+ app's own: any layout it likes, built from primitives. Every route sits under `AppLayout`; an area
99
+ that needs its own frame or sign-in is a nested layout route (a `Component` with `children` and no
100
+ `path`). `AppLayout` renders `<Toaster />` once, which `app check` requires.
101
+ - `src/App.ts` lists the `tables`, `stores`, `models`, `routes`, `actions` and `actionGroups`; a
102
+ route's `id` names the page (`page: 'item'` in the snapshot) and opens the model with the same
103
+ name.
104
+ - Pattern reads the app's types from `src/models/index.ts`, `schema.ts`, `src/stores/index.ts`,
105
+ `src/routes.tsx`, `src/actions/index.ts` (the action groups) and `src/theme/icons.ts` (the icon
106
+ names), so `Model`, `defineActions`, `action`, `AppStores`, `AppSnapshot`, `AppPath` (every path
107
+ the routes allow), `IconName` and `useModel` are typed for this app with nothing to declare.
108
+ - A new page: add `src/screens/<Name>Screen.tsx`, a route with an `id` in `src/routes.tsx`, and
109
+ `src/models/<Name>.ts` registered under that `id` in `src/models/index.ts`.
110
+
111
+ ## Built in
112
+
113
+ Every app gets four models and the stores above without listing them, and may not reuse their
114
+ names:
115
+
116
+ - `account` (`user`, `appearance`; `setAppearance`, `signOut`);
117
+ - `appSettings` (`title`, `canRename`, `isRenaming`; `rename`);
118
+ - `palette`, the ⌘K command menu (`isOpen`, `query`, `results`; `open`, `toggle`, `setQuery`, `run`);
119
+ - `signIn`, the sign-in steps (`continueWithGoogle`, the email code steps and the dev-only
120
+ `signInAsDevUser`).
121
+
122
+ Pattern applies `account.appearance` to the page and offers the appearance switches, Log out and ⌘K
123
+ as actions in the `Preferences` and `Account` groups. The app's name is data, not code: `title` in
124
+ the `settings` table of the app's own database, served at `/api/app` and shown by the sidebar, the
125
+ sign-in screen and the browser tab. Admins rename the app in Settings, which needs no publish.
126
+
127
+ ## Flows
128
+
129
+ - Behaviour is tested with flows, not a browser: `flows/<Name>.flow.ts` sends commands (as
130
+ `{ type: 'item.chooseSize', id: 'bouquets-large' }`, or the built-in
131
+ `{ type: 'app.navigate', to: '/flowers' }`) and checks the app snapshot:
132
+ `{ page, path, models: { <model>: state and getters }, stores: { ... } }`.
133
+ - `app test` runs them in about a second. A new feature gets a command and a flow. `flows/flow.ts`
134
+ hands the flow runner the app; it fakes the server from `schema.ts` and `seed.ts`.
135
+ - Flows sign in as ada@acme.test by default (`{ signedInAs: null }` for a signed-out flow), and the
136
+ emailed sign-in code is always 123456.
137
+ - `{ hold: '<service call>' }` and `{ release: '…' }` pause a server call, so a flow can check the
138
+ loading state; `world` fakes what the server knows.
139
+ - Flows check what actions are offered with `expect: { stores: { actions: { items: ... } } }`, and
140
+ press a key with `{ send: { type: 'app.pressKey', key: 'j' } }`.
141
+ - Never expect the app's own name in a flow: every site has its own title, so a flow that renames
142
+ it checks the new name instead.
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@trypattern/sdk",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
7
+ "docs",
7
8
  "presets"
8
9
  ],
9
10
  "bin": {
@@ -95,11 +96,11 @@
95
96
  "trailingComma": "none"
96
97
  },
97
98
  "scripts": {
98
- "check": "tsc -p . && oxlint -c oxlint.sdk.json src/components src/hooks src/theme && node tests/check-types.ts && vitest run --root tests/server && prettier --check src tests presets *.json",
99
+ "check": "tsc -p . && oxlint -c oxlint.sdk.json src/components src/hooks src/theme && node tests/check-types.ts && vitest run --root tests/server && prettier --check src tests presets docs *.json",
99
100
  "typecheck": "tsc -p .",
100
101
  "test:types": "node tests/check-types.ts",
101
102
  "test:server": "vitest run --root tests/server",
102
- "format": "prettier --write src tests presets *.json",
103
+ "format": "prettier --write src tests presets docs *.json",
103
104
  "lint": "oxlint -c oxlint.sdk.json src/components src/hooks src/theme",
104
105
  "build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/theme/reset.css dist/theme/reset.css"
105
106
  }