@trypattern/sdk 0.1.37 → 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/dist/bookings.d.ts +1 -7
- package/dist/bookingsStore.d.ts +1 -7
- package/dist/cli/app.js +71 -2
- package/dist/cli/guidance.d.ts +8 -0
- package/dist/cli/guidance.js +121 -0
- package/dist/cli/instructions.d.ts +21 -0
- package/dist/cli/instructions.js +206 -0
- package/dist/components/Calendar.d.ts +3 -1
- package/dist/components/Calendar.js +5 -5
- package/dist/components/Checkbox.js +2 -2
- package/dist/components/Dialog.js +10 -1
- package/dist/components/ScrollArea.js +4 -2
- package/dist/components/Select.d.ts +2 -1
- package/dist/components/Select.js +14 -3
- package/dist/components/calendarDates.d.ts +4 -3
- package/dist/components/calendarDates.js +8 -7
- package/dist/inquiriesStore.d.ts +1 -7
- package/dist/publishWire.d.ts +37 -59
- package/dist/publishWire.js +4 -2
- package/dist/requests.d.ts +14 -32
- package/dist/requests.js +12 -5
- package/dist/rules/lintPlugin.js +4 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +1 -0
- package/dist/server/inquiries.d.ts +7 -2
- package/dist/server/inquiries.js +2 -2
- package/dist/server/requestSources.d.ts +41 -0
- package/dist/server/requestSources.js +157 -0
- package/dist/server/requests.d.ts +7 -3
- package/dist/server/requests.js +130 -62
- package/dist/start.js +14 -0
- package/dist/theme/tokens.stylex.d.ts +1 -0
- package/dist/theme/tokens.stylex.js +2 -1
- package/docs/CHANGELOG.json +331 -0
- package/docs/PATTERN.md +87 -0
- package/docs/app.md +74 -0
- package/docs/data.md +109 -0
- package/docs/history/agents.json +1141 -0
- package/docs/look.md +147 -0
- package/docs/requests.md +115 -0
- package/docs/state.md +142 -0
- package/package.json +4 -3
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.
|
package/docs/requests.md
ADDED
|
@@ -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.
|
|
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
|
}
|