@trypattern/sdk 0.0.0-stage → 0.1.1
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/README.md +127 -2
- package/dist/access.d.ts +31 -0
- package/dist/access.js +72 -0
- package/dist/actions.d.ts +57 -0
- package/dist/actions.js +198 -0
- package/dist/api.d.ts +30 -0
- package/dist/api.js +60 -0
- package/dist/app.d.ts +115 -0
- package/dist/app.js +266 -0
- package/dist/appInfo.d.ts +35 -0
- package/dist/appInfo.js +67 -0
- package/dist/appTitle.d.ts +6 -0
- package/dist/appTitle.js +3 -0
- package/dist/appearance.d.ts +7 -0
- package/dist/appearance.js +2 -0
- package/dist/auth/sessionApi.d.ts +41 -0
- package/dist/auth/sessionApi.js +158 -0
- package/dist/auth/sessionStore.d.ts +20 -0
- package/dist/auth/sessionStore.js +40 -0
- package/dist/auth/user.d.ts +11 -0
- package/dist/auth/user.js +9 -0
- package/dist/builtins/Account.d.ts +25 -0
- package/dist/builtins/Account.js +15 -0
- package/dist/builtins/AppSettings.d.ts +12 -0
- package/dist/builtins/AppSettings.js +31 -0
- package/dist/builtins/Palette.d.ts +29 -0
- package/dist/builtins/Palette.js +63 -0
- package/dist/builtins/SignIn.d.ts +28 -0
- package/dist/builtins/SignIn.js +60 -0
- package/dist/builtins/Toasts.d.ts +32 -0
- package/dist/builtins/Toasts.js +25 -0
- package/dist/builtins/actions.d.ts +2 -0
- package/dist/builtins/actions.js +45 -0
- package/dist/cache.d.ts +24 -0
- package/dist/cache.js +112 -0
- package/dist/cli/app.d.ts +2 -0
- package/dist/cli/app.js +1406 -0
- package/dist/cli/publishing.d.ts +21 -0
- package/dist/cli/publishing.js +493 -0
- package/dist/cli/screenshot.d.ts +1 -0
- package/dist/cli/screenshot.js +96 -0
- package/dist/cli/tools.d.ts +12 -0
- package/dist/cli/tools.js +35 -0
- package/dist/clock.d.ts +6 -0
- package/dist/clock.js +31 -0
- package/dist/components/AppFrame.d.ts +14 -0
- package/dist/components/AppFrame.js +64 -0
- package/dist/components/AppearancePicker.d.ts +1 -0
- package/dist/components/AppearancePicker.js +12 -0
- package/dist/components/Avatar.d.ts +9 -0
- package/dist/components/Avatar.js +63 -0
- package/dist/components/Badge.d.ts +11 -0
- package/dist/components/Badge.js +87 -0
- package/dist/components/Button.d.ts +24 -0
- package/dist/components/Button.js +252 -0
- package/dist/components/Card.d.ts +12 -0
- package/dist/components/Card.js +38 -0
- package/dist/components/CommandMenu.d.ts +23 -0
- package/dist/components/CommandMenu.js +214 -0
- package/dist/components/CommandPalette.d.ts +1 -0
- package/dist/components/CommandPalette.js +7 -0
- package/dist/components/Content.d.ts +7 -0
- package/dist/components/Content.js +24 -0
- package/dist/components/ContextMenu.d.ts +8 -0
- package/dist/components/ContextMenu.js +23 -0
- package/dist/components/CopyButton.d.ts +11 -0
- package/dist/components/CopyButton.js +16 -0
- package/dist/components/Dialog.d.ts +11 -0
- package/dist/components/Dialog.js +81 -0
- package/dist/components/Divider.d.ts +7 -0
- package/dist/components/Divider.js +30 -0
- package/dist/components/EmptyState.d.ts +8 -0
- package/dist/components/EmptyState.js +24 -0
- package/dist/components/Form.d.ts +9 -0
- package/dist/components/Form.js +23 -0
- package/dist/components/Icon.d.ts +10 -0
- package/dist/components/Icon.js +22 -0
- package/dist/components/IconButton.d.ts +14 -0
- package/dist/components/IconButton.js +53 -0
- package/dist/components/Kbd.d.ts +6 -0
- package/dist/components/Kbd.js +67 -0
- package/dist/components/Link.d.ts +14 -0
- package/dist/components/Link.js +44 -0
- package/dist/components/ListRow.d.ts +12 -0
- package/dist/components/ListRow.js +62 -0
- package/dist/components/LoadingRows.d.ts +5 -0
- package/dist/components/LoadingRows.js +6 -0
- package/dist/components/Menu.d.ts +12 -0
- package/dist/components/Menu.js +33 -0
- package/dist/components/MenuItems.d.ts +21 -0
- package/dist/components/MenuItems.js +32 -0
- package/dist/components/Placement.d.ts +21 -0
- package/dist/components/Placement.js +1 -0
- package/dist/components/Popup.d.ts +8 -0
- package/dist/components/Popup.js +43 -0
- package/dist/components/PopupItem.d.ts +8 -0
- package/dist/components/PopupItem.js +51 -0
- package/dist/components/PropertyRow.d.ts +7 -0
- package/dist/components/PropertyRow.js +23 -0
- package/dist/components/RadioGroup.d.ts +18 -0
- package/dist/components/RadioGroup.js +85 -0
- package/dist/components/Row.d.ts +16 -0
- package/dist/components/Row.js +27 -0
- package/dist/components/Screen.d.ts +13 -0
- package/dist/components/Screen.js +56 -0
- package/dist/components/Scrim.d.ts +8 -0
- package/dist/components/Scrim.js +40 -0
- package/dist/components/ScrollArea.d.ts +15 -0
- package/dist/components/ScrollArea.js +13 -0
- package/dist/components/SegmentedControl.d.ts +17 -0
- package/dist/components/SegmentedControl.js +68 -0
- package/dist/components/Select.d.ts +17 -0
- package/dist/components/Select.js +107 -0
- package/dist/components/SettingsRow.d.ts +8 -0
- package/dist/components/SettingsRow.js +32 -0
- package/dist/components/Sidebar.d.ts +20 -0
- package/dist/components/Sidebar.js +137 -0
- package/dist/components/SignInScreen.d.ts +1 -0
- package/dist/components/SignInScreen.js +40 -0
- package/dist/components/Size.d.ts +1 -0
- package/dist/components/Size.js +1 -0
- package/dist/components/Skeleton.d.ts +8 -0
- package/dist/components/Skeleton.js +48 -0
- package/dist/components/Spinner.d.ts +6 -0
- package/dist/components/Spinner.js +54 -0
- package/dist/components/Stack.d.ts +14 -0
- package/dist/components/Stack.js +19 -0
- package/dist/components/Switch.d.ts +10 -0
- package/dist/components/Switch.js +72 -0
- package/dist/components/Text.d.ts +21 -0
- package/dist/components/Text.js +77 -0
- package/dist/components/TextField.d.ts +25 -0
- package/dist/components/TextField.js +68 -0
- package/dist/components/Toaster.d.ts +1 -0
- package/dist/components/Toaster.js +83 -0
- package/dist/components/Tooltip.d.ts +8 -0
- package/dist/components/Tooltip.js +43 -0
- package/dist/components/controlLook.d.ts +70 -0
- package/dist/components/controlLook.js +80 -0
- package/dist/components/frame.d.ts +4 -0
- package/dist/components/frame.js +2 -0
- package/dist/components/icons.d.ts +59 -0
- package/dist/components/icons.js +111 -0
- package/dist/components/interaction.d.ts +12 -0
- package/dist/components/interaction.js +22 -0
- package/dist/components/spacing.d.ts +118 -0
- package/dist/components/spacing.js +46 -0
- package/dist/components/tones.d.ts +28 -0
- package/dist/components/tones.js +12 -0
- package/dist/entityStore.d.ts +46 -0
- package/dist/entityStore.js +160 -0
- package/dist/flows/accessFlows.d.ts +9 -0
- package/dist/flows/accessFlows.js +167 -0
- package/dist/flows/browser.d.ts +34 -0
- package/dist/flows/browser.js +167 -0
- package/dist/flows/contactSheet.d.ts +1 -0
- package/dist/flows/contactSheet.js +63 -0
- package/dist/flows/flowBrowser.d.ts +25 -0
- package/dist/flows/flowBrowser.js +23 -0
- package/dist/flows/flowSetup.d.ts +3 -0
- package/dist/flows/flowSetup.js +35 -0
- package/dist/flows/flows.d.ts +47 -0
- package/dist/flows/flows.js +175 -0
- package/dist/flows/screenshotName.d.ts +1 -0
- package/dist/flows/screenshotName.js +6 -0
- package/dist/gate.d.ts +8 -0
- package/dist/gate.js +52 -0
- package/dist/hooks/appContext.d.ts +15 -0
- package/dist/hooks/appContext.js +5 -0
- package/dist/hooks/appHooks.d.ts +21 -0
- package/dist/hooks/appHooks.js +24 -0
- package/dist/hooks/useAnimatesOnChange.d.ts +1 -0
- package/dist/hooks/useAnimatesOnChange.js +14 -0
- package/dist/hooks/useStoreState.d.ts +6 -0
- package/dist/hooks/useStoreState.js +5 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +21 -0
- package/dist/memoryApi.d.ts +19 -0
- package/dist/memoryApi.js +42 -0
- package/dist/model.d.ts +94 -0
- package/dist/model.js +243 -0
- package/dist/page.d.ts +11 -0
- package/dist/page.js +9 -0
- package/dist/platform.d.ts +1 -0
- package/dist/platform.js +5 -0
- package/dist/publishWire.d.ts +271 -0
- package/dist/publishWire.js +168 -0
- package/dist/react.d.ts +49 -0
- package/dist/react.js +40 -0
- package/dist/registered.d.ts +60 -0
- package/dist/registered.js +19 -0
- package/dist/routes.d.ts +38 -0
- package/dist/routes.js +21 -0
- package/dist/rules/checkAllFiles.d.ts +1 -0
- package/dist/rules/checkAllFiles.js +749 -0
- package/dist/rules/checkApiRouting.d.ts +1 -0
- package/dist/rules/checkApiRouting.js +112 -0
- package/dist/rules/lintPlugin.d.ts +252 -0
- package/dist/rules/lintPlugin.js +1159 -0
- package/dist/rules/messages.d.ts +48 -0
- package/dist/rules/messages.js +48 -0
- package/dist/rules/themeTokens.d.ts +6 -0
- package/dist/rules/themeTokens.js +64 -0
- package/dist/schema.d.ts +4 -0
- package/dist/schema.js +2 -0
- package/dist/server/auth.d.ts +115 -0
- package/dist/server/auth.js +134 -0
- package/dist/server/authService.d.ts +9 -0
- package/dist/server/authService.js +50 -0
- package/dist/server/createServer.d.ts +402 -0
- package/dist/server/createServer.js +82 -0
- package/dist/server/env.d.ts +15 -0
- package/dist/server/env.js +1 -0
- package/dist/server/index.d.ts +13 -0
- package/dist/server/index.js +9 -0
- package/dist/server/services.d.ts +9 -0
- package/dist/server/services.js +47 -0
- package/dist/server/session.d.ts +29 -0
- package/dist/server/session.js +65 -0
- package/dist/server/sessions.d.ts +92 -0
- package/dist/server/sessions.js +47 -0
- package/dist/server/settings.d.ts +49 -0
- package/dist/server/settings.js +25 -0
- package/dist/server/signInWire.d.ts +53 -0
- package/dist/server/signInWire.js +23 -0
- package/dist/server/tables.d.ts +242 -0
- package/dist/server/tables.js +246 -0
- package/dist/server/users.d.ts +189 -0
- package/dist/server/users.js +49 -0
- package/dist/serviceClients.d.ts +5 -0
- package/dist/serviceClients.js +70 -0
- package/dist/services.d.ts +53 -0
- package/dist/services.js +30 -0
- package/dist/shortcuts.d.ts +3 -0
- package/dist/shortcuts.js +40 -0
- package/dist/start.d.ts +20 -0
- package/dist/start.js +145 -0
- package/dist/state.d.ts +14 -0
- package/dist/state.js +17 -0
- package/dist/store.d.ts +45 -0
- package/dist/store.js +208 -0
- package/dist/subscribers.d.ts +5 -0
- package/dist/subscribers.js +13 -0
- package/dist/table.d.ts +143 -0
- package/dist/table.js +91 -0
- package/dist/tables.d.ts +77 -0
- package/dist/tables.js +216 -0
- package/dist/theme/reset.css +84 -0
- package/dist/theme/scales.d.ts +76 -0
- package/dist/theme/scales.js +82 -0
- package/dist/theme/tokens.stylex.d.ts +218 -0
- package/dist/theme/tokens.stylex.js +233 -0
- package/dist/vite/commandBridge.d.ts +2 -0
- package/dist/vite/commandBridge.js +46 -0
- package/dist/vite/designManifest.d.ts +25 -0
- package/dist/vite/designManifest.js +191 -0
- package/dist/vite/localWorker.d.ts +4 -0
- package/dist/vite/localWorker.js +60 -0
- package/dist/vite/projectFile.d.ts +9 -0
- package/dist/vite/projectFile.js +17 -0
- package/dist/vite/rootAlias.d.ts +2 -0
- package/dist/vite/rootAlias.js +15 -0
- package/dist/vite/vite.d.ts +8 -0
- package/dist/vite/vite.js +95 -0
- package/dist/wire.d.ts +7 -0
- package/dist/wire.js +1 -0
- package/package.json +103 -3
- package/presets/lintPlugin.js +7 -0
- package/presets/oxlint.json +167 -0
- package/presets/register.d.ts +21 -0
- package/presets/server.d.ts +2 -0
- package/presets/tsconfig.app.json +29 -0
- package/presets/tsconfig.server.json +27 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export declare const SRC_STRUCTURE_MESSAGE = "src/ follows the four layers, one folder each, and each folder holds only its kind. Data comes from the server through stores: Pattern gives every table in schema.ts a store, and stores/ holds the others as <Name>.ts with a class extending Store({ state, persist }) (stores/Toasts.ts, stores/IssuesView.ts), listed in `stores` in stores/index.ts; a store reaches the app\u2019s own server calls as this.services.<group>.<call>(). services/ holds the app\u2019s own server calls, declared once with their inputs, replies and the memory fake flows run against: services/<group>.ts holds defineServices({ <group>: { ... } }) and its memory handlers, and services/index.ts exports `services` and `memory` for App.ts (server/<group>.ts serves them with serveServices). Stores call them; models never do. models/ holds each model, the state, answers and commands behind a screen or app chrome, as <Name>.ts with a class extending Model({ state, persist }) (models/IssueList.ts holds class IssueList), listed in `models` in models/index.ts under the route id of its page. actions/ holds the menu entries and shortcuts, one file per thing they act on (actions/issues.ts, actions/appearance.ts), each `export const issueActions = defineActions(({ page, models }) => [...])`, listed in `actions` in actions/index.ts. screens/ holds the pages you see, one file each, named <Name>Screen.tsx (IssueListScreen.tsx), each reading its model with useModel and arranging primitives, with no styles of its own. components/<Name>/ groups the pieces this app adds, and every file in it starts with the folder name (components/IssueList/IssueListRow.tsx); they take props, and only screens and layouts may call useModel. layouts/ holds <Name>Layout.tsx files (layouts/AppLayout.tsx), the frames pages render inside: which pages need sign-in, the sidebar or top bar, and where the Toaster and CommandPalette go. A piece may draw a shape no primitive gives, with tokens for every value, <Text> for its words and primitives for anything clickable. App.ts wires the app together from tables, services, stores, models, routes and actions. hooks/ holds hooks (use<Name>.ts); lib/ holds non-UI helpers and data (.ts only); theme/ holds theme.ts (the tokens this app re-values) and icons.ts (the icons this app adds). Primitives (Screen, Stack, Text, Button, Menu, Icon) come from @trypattern/sdk/react and are not in this project. Styles live only at the bottom of the component file that owns them; there are no style files. To reuse a look, reuse the component. Move the file where it belongs.";
|
|
2
|
+
export declare const SCRIPTS_WIRED_MESSAGE = "A project script lives in a labelled folder under scripts/ (a folder named for what it does) and is used by something: package.json, the app launcher, vite.config.ts, flows/, or another script. Delete scripts nothing uses; never leave one-off scripts behind. Checks and tools every Pattern app needs belong in @trypattern/sdk, not here.";
|
|
3
|
+
export declare const NO_COMMENTS_MESSAGE = "Comments are not allowed in Pattern, including directives like @ts-expect-error or oxlint-disable: there are no escape hatches, fix the code. Express intent with names, types and structure; put lasting rationale in the error message of the rule that enforces it, or in the commit message.";
|
|
4
|
+
export declare const NO_POINTER_CURSOR_MESSAGE = "cursor: pointer is not allowed in Pattern. Pattern apps feel native, and native apps keep the default arrow over links and buttons. Remove it; the reset Pattern ships already sets cursor: default.";
|
|
5
|
+
export declare const USE_COLOR_TOKEN_MESSAGE = "Raw colors are not allowed in styles. Use a token from @trypattern/sdk/tokens.stylex (colors.*). To change what a token looks like in this app, re-value it in src/theme/theme.ts, the one file where raw colors are allowed. Keywords like transparent and currentColor are fine.";
|
|
6
|
+
export declare const USE_FONT_TOKEN_MESSAGE = "Raw font sizes, families and weights are not allowed in styles. Use fontSizes.*, fonts.* or fontWeights.* from @trypattern/sdk/tokens.stylex; to change a font or a size for the whole app, re-value it in src/theme/theme.ts. Text inherits Inter 13px/20px (15px/24px on phones) from <body>, so most styles need no font properties at all.";
|
|
7
|
+
export declare const NO_INTERACTIVE_TRANSITION_MESSAGE = "Hover and press transitions use the motion tokens, as Interfere does: motion.fast with motion.easeOut for buttons, motion.instant for fields, motion.base for segmented controls. Never a raw duration or easing, so every control responds at the same speed. Navigation items change instantly with no transition.";
|
|
8
|
+
export declare const HOVER_USES_HIGHLIGHT_MESSAGE = "Hover, active and selected backgrounds must be colors.transparentHover (with radii.md), so hovering then selecting an item never flashes a second color. Styles named *Active, *Selected or *Current count as selected. Buttons and switches, which have no selected state, may instead use their own colors.*Hover and colors.*Active tokens.";
|
|
9
|
+
export declare const SCROLL_VIEW_ONLY_MESSAGE = "Only ScrollArea may scroll (overflow auto/scroll). The page itself never scrolls or bounces in Pattern; wrap scrollable content in <ScrollArea> from @trypattern/sdk/react.";
|
|
10
|
+
export declare const SELECTABLE_ONLY_MESSAGE = "Do not set userSelect. Text is not selectable by default in Pattern (inputs and editable fields are). For words people need to copy, write <Text isSelectable> from @trypattern/sdk/react.";
|
|
11
|
+
export declare const USE_BREAKPOINTS_MESSAGE = "Media queries come from breakpoints in @trypattern/sdk/tokens.stylex, e.g. [breakpoints.compact]: value, so every screen adapts at the same sizes.";
|
|
12
|
+
export declare const DARK_MODE_IN_TOKENS_MESSAGE = "Dark mode lives only in the color tokens: each is light-dark(lightValue, darkValue), in @trypattern/sdk/tokens.stylex and in the overrides in src/theme/theme.ts. The Appearance setting sets color-scheme on <html>, which picks the side. Components never branch on the color scheme.";
|
|
13
|
+
export declare const NO_CLASSNAME_STRINGS_MESSAGE = "Hand-written class names are not allowed. Style with StyleX: {...stylex.props(styles.x)}, or className={stylex.props(...).className} where a component needs a className.";
|
|
14
|
+
export declare const TYPED_STYLE_PROP_MESSAGE = "A `style` prop must be StyleXStyles<{ ...allowed properties }> (directly or via a type alias), so callers can only override what the component allows and anything else is a type error. Merge it last: {...stylex.props(styles.x, style)}.";
|
|
15
|
+
export declare const COMPONENT_FILE_NAME_MESSAGE = "Each .tsx file under src/components and src/screens holds one component, exported as a named function with the same name as the file (Sidebar.tsx exports function Sidebar).";
|
|
16
|
+
export declare const COMPONENT_FIRST_MESSAGE = "A component file reads top to bottom: imports, its props type, then the component, so the first thing a reader sees is what the file draws. Constants and helper functions it uses (sortOptions, LOADING_ROWS, sectionsOf) go below the component, above `styles`.";
|
|
17
|
+
export declare const ROUTES_UNDER_APP_LAYOUT_MESSAGE = "Every route is a child of AppLayout (src/layouts/AppLayout.tsx) in `routes = defineRoutes([...])` in src/routes.tsx, directly or through another layout from src/layouts/ (a route with a Component and children but no path), so every page sits inside a layout the app chose. Add a page as { id, path, Component } inside the children of the layout it belongs to, above the * route. Its id is the page name the snapshot reports (page: 'issues'), and opening it calls onPageOpen() on the model with the same name in src/models/index.ts, if there is one. A sidebar entry goes in the `navigation` answer of the layout model in src/models/Layout.ts.";
|
|
18
|
+
export declare const NO_ROUTE_DATA_APIS_MESSAGE = "React Router is used in data mode without loaders, actions or lazy routes: models own the state, so router-managed data would compete with it. Load data in the model's onPageOpen() instead.";
|
|
19
|
+
export declare const USE_LINK_MESSAGE = "Use <Link to=\"...\"> from @trypattern/sdk/react for in-app navigation, not <a href=\"/...\">, so navigation stays client-side and looks like every other link. A whole row that opens a page is a <ListRow to=\"...\">.";
|
|
20
|
+
export declare const STORES_IN_APP_MESSAGE = "Only src/stores/index.ts (the `stores` list) and src/App.ts may import values from src/stores/. Every model reads every store as this.stores.<name>; a screen reads its model with useModel('issueList'). Import types from stores freely (import type).";
|
|
21
|
+
export declare const DEV_ONLY_SIGN_IN_MESSAGE = "The server has exactly one dev-only switch, inside @trypattern/sdk: createServer mounts /api/auth/dev-sign-in only in the dev server. Nothing in server/ may branch on dev mode, so there is never an \"if dev, skip auth\" path. To test as a signed-in user, run `app sign-in <email>` (or a flow, which signs in as ada@acme.test by default): it sets the real session cookie, so the real auth code runs.";
|
|
22
|
+
export declare const STATE_VIA_HOOKS_MESSAGE = "A screen reads only its model: const issueList = useModel('issueList'), then issueList.issues, issueList.isLoading and issueList.createIssue(). Reading .state, .models, .stores or getSnapshot() directly does not subscribe, so the screen would show stale data. If the screen needs something from a store, add a getter for it to the model.";
|
|
23
|
+
export declare const CLASSES_NOT_FACTORIES_MESSAGE = "A function that returns an object of methods is a class in disguise: write it as a class, with private fields for its state and methods for its behaviour. One shape for everything that has state keeps the codebase predictable. Plain functions are for pure helpers that compute a value (sortProjects, newClientSideId).";
|
|
24
|
+
export declare const ACCESS_FLOWS_MESSAGE = "flows/Access.flow.ts runs Pattern's generated access flows, which prove every table's access rules against the seed on each app test. Keep it: `import { accessFlows } from '@trypattern/sdk/flows'` and `accessFlows({ App })`.";
|
|
25
|
+
export declare const SCHEMA_TABLES_MESSAGE = "schema.ts defines every saved table in one place and says who may do what: `export const tables = defineTables({ issues: { columns: { title: z.string() }, access: { read: 'signedIn', create: 'signedIn', update: 'signedIn', delete: 'signedIn' }, writes: 'optimistic' } })`. It exports a row type for each table, named for one row (`export type Issue = Row<typeof tables.issues>`), and the emails of the people who run the app, `export const admins: string[] = []`, even when there are none yet, so the question is answered on purpose. Add the line shown.";
|
|
26
|
+
export declare const NO_UNUSED_EXPORTS_MESSAGE = "Export only what another file imports. Nothing imports this, so remove `export` (and the code itself if this file does not use it either). Unused exports hide dead code and make a file look like it offers more than it does. Pattern reads `tables` from schema.ts and `seed` from seed.ts, and vite.config.ts and server/worker.ts default-export their config, so those stay.";
|
|
27
|
+
export declare const NO_RE_EXPORTS_MESSAGE = "Do not pass imports through: a file exports only what it defines. Import the thing from where it lives instead (command from @trypattern/sdk, a component from its own file), so every import names the real source. server/worker.ts is the one exception: Cloudflare runs the Workflow and Durable Object classes its Worker exports, so it may export them from where they live.";
|
|
28
|
+
export declare const EXPLICIT_ACCESS_MESSAGE = "Mark every class member public or private (or protected, to override a base class member), so a reader sees at a glance what other code may call: `public setSort = command(...)`, `public override onPageOpen()`, `private loadIssues()`. Use the word private, never #name, so there is one way to say it. Constructors need neither.";
|
|
29
|
+
export declare const IMPORT_FROM_ROOT_MESSAGE = "Import project files by their path from the project root, the way errors and docs name them: `@/src/models/Issues.ts`, `@/schema.ts`. `./File.ts` is fine for a file in the same folder; never `../` or `./folder/File.ts`, so every import says exactly where the file is and moving a file never breaks the files beside it.";
|
|
30
|
+
export declare const MODEL_MEMBERS_MESSAGE = "A model shows the outside world three kinds of public member, so a reader can tell them apart at a glance: getters are its answers (public get hasNext()), command() fields are what it can do (public openNext = command(() => ...)), and onPageOpen() runs when its page opens. Everything else is private, and a private helper that works something out is a method (private neighbours()), never a getter, because Pattern publishes every getter to the screen and the snapshot. A model has no `actions`: menu entries and shortcuts live in src/actions/.";
|
|
31
|
+
export declare const MODEL_BY_NAME_MESSAGE = "Keep the model whole and read it by name: const issueList = useModel('issueList'), then issueList.isLoading and onClick={issueList.createIssue}. Destructuring it hides where each value comes from and leaves loose names a reader has to trace back.";
|
|
32
|
+
export declare const USE_MODEL_WHERE_MESSAGE = "useModel is for screens (src/screens/) and layouts (src/layouts/). Other components take what they show as props and the commands they call as callbacks, so the screen decides what they read: <IssueListRow issue={issue} onDelete={issueList.deleteIssue} />.";
|
|
33
|
+
export declare const PURE_GETTERS_MESSAGE = "A getter works an answer out from this.state and this.stores.<name>.state and nothing else. It never changes anything (this.set, a store method, this.app.navigate), never waits, and never reads the clock, randomness or a private field, because Pattern caches each getter until the state it read changes, and flows compare its value. Put the change in a command.";
|
|
34
|
+
export declare const ACTIONS_WHERE_MESSAGE = "Menu entries and shortcuts are actions, and actions live only in src/actions/: one file per thing they act on, `export const issueActions = defineActions(({ page, models }) => [action({ id: 'issues.next', label: 'Next issue', keys: ['j'], send: [{ type: 'issueDetail.openNext' }] })])`, listed in src/actions/index.ts. If a person can see it in a menu or press a key for it, it is an action; what it does is a command on a model, and an action only sends commands. Models never build actions.";
|
|
35
|
+
export declare const PURE_ACTIONS_MESSAGE = "An actions file works its entries out from `page` and `models` (each model's state and answers) and nothing else, because Pattern reruns it only when something it read changes, and flows compare the result. It never waits, never uses `this`, never reads the clock or randomness, and imports only types from src/models/, src/stores/ and src/components/. If an entry needs something a model does not answer yet, add a getter to that model.";
|
|
36
|
+
export declare const USE_PRIMITIVES_MESSAGE = "Write <Button>, <IconButton>, <TextField>, <Select>, <Switch> or <Dialog> from @trypattern/sdk/react instead of this element. The primitives carry the focus, keyboard, disabled, pending and error behaviour and the design system's look, so a screen only says what the control does. If no primitive fits, the gap is in the SDK; do not build around it here.";
|
|
37
|
+
export declare const NO_WIDGET_ROLES_MESSAGE = "This role rebuilds a primitive by hand. Use the component from @trypattern/sdk/react with that behaviour (Button, Switch, Menu, Tooltip, Dialog, Divider, CommandMenu): it already has the role, the keyboard handling and the focus management, and it will keep working when the SDK is upgraded.";
|
|
38
|
+
export declare const NO_CLICK_ON_STATIC_MESSAGE = "A click on a <div> or <span> cannot be reached from the keyboard or by a screen reader. Make it a <Button variant=\"transparent\"> or an <IconButton> from @trypattern/sdk/react, or a <Link> if it goes somewhere.";
|
|
39
|
+
export declare const PRIMITIVE_NAMES_MESSAGE = "{name} is a primitive in @trypattern/sdk/react; a second one with the same name would drift from it. Import the SDK's, and if yours adds something (a fixed icon, a confirm step) name it for what it does, like DeleteIssue{name}, and render the SDK {name} inside.";
|
|
40
|
+
export declare const USE_SPACE_TOKENS_MESSAGE = "Paddings, gaps and margins come from space in @trypattern/sdk/tokens.stylex (space.sm, space.lg), never a number, so one theme can make the whole app roomier or tighter. Write 0 or auto for none, and calc() over tokens for a negative margin: `calc(-1 * ${space.sm})`.";
|
|
41
|
+
export declare const USE_SIZE_TOKENS_MESSAGE = "Heights, widths, font sizes and line heights read a token (controls.heightMd, iconSizes.md, widths.contentMd, lineHeights.bodySm), or are a percentage, auto or calc() over tokens, so re-valuing a scale in the app's theme moves every piece together and rows still line up.";
|
|
42
|
+
export declare const THEME_SCALES_MESSAGE = "src/theme/theme.ts may re-value the size scales (space, fontSizes, lineHeights, controls, iconSizes, borders, widths) as well as colors, radii, fonts and shadows, and Pattern checks the result against the SDK defaults it leaves alone: every value is whole pixels, each scale only grows from one step to the next, a control is at least as tall as the text and icon inside it, and a line height is at least its font size. Each finding names the two values that disagree; change one of them.";
|
|
43
|
+
export declare const TEXT_VIA_TEXT_MESSAGE = "Words are drawn with <Text variant=\"label\"> from @trypattern/sdk/react, never with font, line height, alignment or wrapping properties in a style, so every app has one set of text styles and the theme can resize them all at once. Pick the variant closest to what you meant (heading, subheading, title, label, body, caption, code), its tone for the colour, and align, isTruncated or isSelectable for the rest. If none fits, that is an SDK change.";
|
|
44
|
+
export declare const TEXT_ELEMENTS_MESSAGE = "Write <Text as=\"h1\" variant=\"heading\"> or <Text as=\"p\"> from @trypattern/sdk/react instead of this element. `as` says what the words are for screen readers and `variant` how they look, so a heading never needs a style of its own. A form label comes from the field: <TextField label=\"Email\">.";
|
|
45
|
+
export declare const LOOSE_TEXT_MESSAGE = "Words inside a plain element are drawn with whatever style happens to surround them. Wrap them in <Text> from @trypattern/sdk/react (<Text tone=\"secondary\">Unassigned</Text>), or pass them to the primitive that shows them (<Button>Save</Button>, <Badge>3</Badge>).";
|
|
46
|
+
export declare const COMPOSE_IN_SCREENS_MESSAGE = "A screen or layout arranges primitives (Screen, Content, Stack, Row, Card, Text, Button, AppFrame, Toaster and the rest from @trypattern/sdk/react) and the pieces in src/components; it draws nothing itself, so it has no styles, no tokens and no plain elements like <div>. If a screen needs a shape no primitive gives it, make it a piece in src/components/<Name>/ and use that here, so the design system can list it.";
|
|
47
|
+
export declare const NO_HOVER_IN_APP_MESSAGE = "Only primitives react to the pointer and to focus. A thing that highlights on hover is a Button, IconButton, ListRow or Link from @trypattern/sdk/react, which already have the hover, press and focus looks, so every clickable thing in the app feels the same.";
|
|
48
|
+
export declare const TOASTER_ONCE_MESSAGE = "Toasts need exactly one place to appear: render <Toaster /> from @trypattern/sdk/react once, in the layout that wraps every page (src/layouts/AppLayout.tsx), so a toast shows whichever page is open and never twice.";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const SRC_STRUCTURE_MESSAGE = 'src/ follows the four layers, one folder each, and each folder holds only its kind. Data comes from the server through stores: Pattern gives every table in schema.ts a store, and stores/ holds the others as <Name>.ts with a class extending Store({ state, persist }) (stores/Toasts.ts, stores/IssuesView.ts), listed in `stores` in stores/index.ts; a store reaches the app’s own server calls as this.services.<group>.<call>(). services/ holds the app’s own server calls, declared once with their inputs, replies and the memory fake flows run against: services/<group>.ts holds defineServices({ <group>: { ... } }) and its memory handlers, and services/index.ts exports `services` and `memory` for App.ts (server/<group>.ts serves them with serveServices). Stores call them; models never do. models/ holds each model, the state, answers and commands behind a screen or app chrome, as <Name>.ts with a class extending Model({ state, persist }) (models/IssueList.ts holds class IssueList), listed in `models` in models/index.ts under the route id of its page. actions/ holds the menu entries and shortcuts, one file per thing they act on (actions/issues.ts, actions/appearance.ts), each `export const issueActions = defineActions(({ page, models }) => [...])`, listed in `actions` in actions/index.ts. screens/ holds the pages you see, one file each, named <Name>Screen.tsx (IssueListScreen.tsx), each reading its model with useModel and arranging primitives, with no styles of its own. components/<Name>/ groups the pieces this app adds, and every file in it starts with the folder name (components/IssueList/IssueListRow.tsx); they take props, and only screens and layouts may call useModel. layouts/ holds <Name>Layout.tsx files (layouts/AppLayout.tsx), the frames pages render inside: which pages need sign-in, the sidebar or top bar, and where the Toaster and CommandPalette go. A piece may draw a shape no primitive gives, with tokens for every value, <Text> for its words and primitives for anything clickable. App.ts wires the app together from tables, services, stores, models, routes and actions. hooks/ holds hooks (use<Name>.ts); lib/ holds non-UI helpers and data (.ts only); theme/ holds theme.ts (the tokens this app re-values) and icons.ts (the icons this app adds). Primitives (Screen, Stack, Text, Button, Menu, Icon) come from @trypattern/sdk/react and are not in this project. Styles live only at the bottom of the component file that owns them; there are no style files. To reuse a look, reuse the component. Move the file where it belongs.';
|
|
2
|
+
export const SCRIPTS_WIRED_MESSAGE = 'A project script lives in a labelled folder under scripts/ (a folder named for what it does) and is used by something: package.json, the app launcher, vite.config.ts, flows/, or another script. Delete scripts nothing uses; never leave one-off scripts behind. Checks and tools every Pattern app needs belong in @trypattern/sdk, not here.';
|
|
3
|
+
export const NO_COMMENTS_MESSAGE = 'Comments are not allowed in Pattern, including directives like @ts-expect-error or oxlint-disable: there are no escape hatches, fix the code. Express intent with names, types and structure; put lasting rationale in the error message of the rule that enforces it, or in the commit message.';
|
|
4
|
+
export const NO_POINTER_CURSOR_MESSAGE = 'cursor: pointer is not allowed in Pattern. Pattern apps feel native, and native apps keep the default arrow over links and buttons. Remove it; the reset Pattern ships already sets cursor: default.';
|
|
5
|
+
export const USE_COLOR_TOKEN_MESSAGE = 'Raw colors are not allowed in styles. Use a token from @trypattern/sdk/tokens.stylex (colors.*). To change what a token looks like in this app, re-value it in src/theme/theme.ts, the one file where raw colors are allowed. Keywords like transparent and currentColor are fine.';
|
|
6
|
+
export const USE_FONT_TOKEN_MESSAGE = 'Raw font sizes, families and weights are not allowed in styles. Use fontSizes.*, fonts.* or fontWeights.* from @trypattern/sdk/tokens.stylex; to change a font or a size for the whole app, re-value it in src/theme/theme.ts. Text inherits Inter 13px/20px (15px/24px on phones) from <body>, so most styles need no font properties at all.';
|
|
7
|
+
export const NO_INTERACTIVE_TRANSITION_MESSAGE = 'Hover and press transitions use the motion tokens, as Interfere does: motion.fast with motion.easeOut for buttons, motion.instant for fields, motion.base for segmented controls. Never a raw duration or easing, so every control responds at the same speed. Navigation items change instantly with no transition.';
|
|
8
|
+
export const HOVER_USES_HIGHLIGHT_MESSAGE = 'Hover, active and selected backgrounds must be colors.transparentHover (with radii.md), so hovering then selecting an item never flashes a second color. Styles named *Active, *Selected or *Current count as selected. Buttons and switches, which have no selected state, may instead use their own colors.*Hover and colors.*Active tokens.';
|
|
9
|
+
export const SCROLL_VIEW_ONLY_MESSAGE = 'Only ScrollArea may scroll (overflow auto/scroll). The page itself never scrolls or bounces in Pattern; wrap scrollable content in <ScrollArea> from @trypattern/sdk/react.';
|
|
10
|
+
export const SELECTABLE_ONLY_MESSAGE = 'Do not set userSelect. Text is not selectable by default in Pattern (inputs and editable fields are). For words people need to copy, write <Text isSelectable> from @trypattern/sdk/react.';
|
|
11
|
+
export const USE_BREAKPOINTS_MESSAGE = 'Media queries come from breakpoints in @trypattern/sdk/tokens.stylex, e.g. [breakpoints.compact]: value, so every screen adapts at the same sizes.';
|
|
12
|
+
export const DARK_MODE_IN_TOKENS_MESSAGE = 'Dark mode lives only in the color tokens: each is light-dark(lightValue, darkValue), in @trypattern/sdk/tokens.stylex and in the overrides in src/theme/theme.ts. The Appearance setting sets color-scheme on <html>, which picks the side. Components never branch on the color scheme.';
|
|
13
|
+
export const NO_CLASSNAME_STRINGS_MESSAGE = 'Hand-written class names are not allowed. Style with StyleX: {...stylex.props(styles.x)}, or className={stylex.props(...).className} where a component needs a className.';
|
|
14
|
+
export const TYPED_STYLE_PROP_MESSAGE = 'A `style` prop must be StyleXStyles<{ ...allowed properties }> (directly or via a type alias), so callers can only override what the component allows and anything else is a type error. Merge it last: {...stylex.props(styles.x, style)}.';
|
|
15
|
+
export const COMPONENT_FILE_NAME_MESSAGE = 'Each .tsx file under src/components and src/screens holds one component, exported as a named function with the same name as the file (Sidebar.tsx exports function Sidebar).';
|
|
16
|
+
export const COMPONENT_FIRST_MESSAGE = 'A component file reads top to bottom: imports, its props type, then the component, so the first thing a reader sees is what the file draws. Constants and helper functions it uses (sortOptions, LOADING_ROWS, sectionsOf) go below the component, above `styles`.';
|
|
17
|
+
export const ROUTES_UNDER_APP_LAYOUT_MESSAGE = "Every route is a child of AppLayout (src/layouts/AppLayout.tsx) in `routes = defineRoutes([...])` in src/routes.tsx, directly or through another layout from src/layouts/ (a route with a Component and children but no path), so every page sits inside a layout the app chose. Add a page as { id, path, Component } inside the children of the layout it belongs to, above the * route. Its id is the page name the snapshot reports (page: 'issues'), and opening it calls onPageOpen() on the model with the same name in src/models/index.ts, if there is one. A sidebar entry goes in the `navigation` answer of the layout model in src/models/Layout.ts.";
|
|
18
|
+
export const NO_ROUTE_DATA_APIS_MESSAGE = "React Router is used in data mode without loaders, actions or lazy routes: models own the state, so router-managed data would compete with it. Load data in the model's onPageOpen() instead.";
|
|
19
|
+
export const USE_LINK_MESSAGE = 'Use <Link to="..."> from @trypattern/sdk/react for in-app navigation, not <a href="/...">, so navigation stays client-side and looks like every other link. A whole row that opens a page is a <ListRow to="...">.';
|
|
20
|
+
export const STORES_IN_APP_MESSAGE = "Only src/stores/index.ts (the `stores` list) and src/App.ts may import values from src/stores/. Every model reads every store as this.stores.<name>; a screen reads its model with useModel('issueList'). Import types from stores freely (import type).";
|
|
21
|
+
export const DEV_ONLY_SIGN_IN_MESSAGE = 'The server has exactly one dev-only switch, inside @trypattern/sdk: createServer mounts /api/auth/dev-sign-in only in the dev server. Nothing in server/ may branch on dev mode, so there is never an "if dev, skip auth" path. To test as a signed-in user, run `app sign-in <email>` (or a flow, which signs in as ada@acme.test by default): it sets the real session cookie, so the real auth code runs.';
|
|
22
|
+
export const STATE_VIA_HOOKS_MESSAGE = "A screen reads only its model: const issueList = useModel('issueList'), then issueList.issues, issueList.isLoading and issueList.createIssue(). Reading .state, .models, .stores or getSnapshot() directly does not subscribe, so the screen would show stale data. If the screen needs something from a store, add a getter for it to the model.";
|
|
23
|
+
export const CLASSES_NOT_FACTORIES_MESSAGE = 'A function that returns an object of methods is a class in disguise: write it as a class, with private fields for its state and methods for its behaviour. One shape for everything that has state keeps the codebase predictable. Plain functions are for pure helpers that compute a value (sortProjects, newClientSideId).';
|
|
24
|
+
export const ACCESS_FLOWS_MESSAGE = "flows/Access.flow.ts runs Pattern's generated access flows, which prove every table's access rules against the seed on each app test. Keep it: `import { accessFlows } from '@trypattern/sdk/flows'` and `accessFlows({ App })`.";
|
|
25
|
+
export const SCHEMA_TABLES_MESSAGE = "schema.ts defines every saved table in one place and says who may do what: `export const tables = defineTables({ issues: { columns: { title: z.string() }, access: { read: 'signedIn', create: 'signedIn', update: 'signedIn', delete: 'signedIn' }, writes: 'optimistic' } })`. It exports a row type for each table, named for one row (`export type Issue = Row<typeof tables.issues>`), and the emails of the people who run the app, `export const admins: string[] = []`, even when there are none yet, so the question is answered on purpose. Add the line shown.";
|
|
26
|
+
export const NO_UNUSED_EXPORTS_MESSAGE = 'Export only what another file imports. Nothing imports this, so remove `export` (and the code itself if this file does not use it either). Unused exports hide dead code and make a file look like it offers more than it does. Pattern reads `tables` from schema.ts and `seed` from seed.ts, and vite.config.ts and server/worker.ts default-export their config, so those stay.';
|
|
27
|
+
export const NO_RE_EXPORTS_MESSAGE = 'Do not pass imports through: a file exports only what it defines. Import the thing from where it lives instead (command from @trypattern/sdk, a component from its own file), so every import names the real source. server/worker.ts is the one exception: Cloudflare runs the Workflow and Durable Object classes its Worker exports, so it may export them from where they live.';
|
|
28
|
+
export const EXPLICIT_ACCESS_MESSAGE = 'Mark every class member public or private (or protected, to override a base class member), so a reader sees at a glance what other code may call: `public setSort = command(...)`, `public override onPageOpen()`, `private loadIssues()`. Use the word private, never #name, so there is one way to say it. Constructors need neither.';
|
|
29
|
+
export const IMPORT_FROM_ROOT_MESSAGE = 'Import project files by their path from the project root, the way errors and docs name them: `@/src/models/Issues.ts`, `@/schema.ts`. `./File.ts` is fine for a file in the same folder; never `../` or `./folder/File.ts`, so every import says exactly where the file is and moving a file never breaks the files beside it.';
|
|
30
|
+
export const MODEL_MEMBERS_MESSAGE = 'A model shows the outside world three kinds of public member, so a reader can tell them apart at a glance: getters are its answers (public get hasNext()), command() fields are what it can do (public openNext = command(() => ...)), and onPageOpen() runs when its page opens. Everything else is private, and a private helper that works something out is a method (private neighbours()), never a getter, because Pattern publishes every getter to the screen and the snapshot. A model has no `actions`: menu entries and shortcuts live in src/actions/.';
|
|
31
|
+
export const MODEL_BY_NAME_MESSAGE = "Keep the model whole and read it by name: const issueList = useModel('issueList'), then issueList.isLoading and onClick={issueList.createIssue}. Destructuring it hides where each value comes from and leaves loose names a reader has to trace back.";
|
|
32
|
+
export const USE_MODEL_WHERE_MESSAGE = 'useModel is for screens (src/screens/) and layouts (src/layouts/). Other components take what they show as props and the commands they call as callbacks, so the screen decides what they read: <IssueListRow issue={issue} onDelete={issueList.deleteIssue} />.';
|
|
33
|
+
export const PURE_GETTERS_MESSAGE = 'A getter works an answer out from this.state and this.stores.<name>.state and nothing else. It never changes anything (this.set, a store method, this.app.navigate), never waits, and never reads the clock, randomness or a private field, because Pattern caches each getter until the state it read changes, and flows compare its value. Put the change in a command.';
|
|
34
|
+
export const ACTIONS_WHERE_MESSAGE = "Menu entries and shortcuts are actions, and actions live only in src/actions/: one file per thing they act on, `export const issueActions = defineActions(({ page, models }) => [action({ id: 'issues.next', label: 'Next issue', keys: ['j'], send: [{ type: 'issueDetail.openNext' }] })])`, listed in src/actions/index.ts. If a person can see it in a menu or press a key for it, it is an action; what it does is a command on a model, and an action only sends commands. Models never build actions.";
|
|
35
|
+
export const PURE_ACTIONS_MESSAGE = "An actions file works its entries out from `page` and `models` (each model's state and answers) and nothing else, because Pattern reruns it only when something it read changes, and flows compare the result. It never waits, never uses `this`, never reads the clock or randomness, and imports only types from src/models/, src/stores/ and src/components/. If an entry needs something a model does not answer yet, add a getter to that model.";
|
|
36
|
+
export const USE_PRIMITIVES_MESSAGE = "Write <Button>, <IconButton>, <TextField>, <Select>, <Switch> or <Dialog> from @trypattern/sdk/react instead of this element. The primitives carry the focus, keyboard, disabled, pending and error behaviour and the design system's look, so a screen only says what the control does. If no primitive fits, the gap is in the SDK; do not build around it here.";
|
|
37
|
+
export const NO_WIDGET_ROLES_MESSAGE = 'This role rebuilds a primitive by hand. Use the component from @trypattern/sdk/react with that behaviour (Button, Switch, Menu, Tooltip, Dialog, Divider, CommandMenu): it already has the role, the keyboard handling and the focus management, and it will keep working when the SDK is upgraded.';
|
|
38
|
+
export const NO_CLICK_ON_STATIC_MESSAGE = 'A click on a <div> or <span> cannot be reached from the keyboard or by a screen reader. Make it a <Button variant="transparent"> or an <IconButton> from @trypattern/sdk/react, or a <Link> if it goes somewhere.';
|
|
39
|
+
export const PRIMITIVE_NAMES_MESSAGE = "{name} is a primitive in @trypattern/sdk/react; a second one with the same name would drift from it. Import the SDK's, and if yours adds something (a fixed icon, a confirm step) name it for what it does, like DeleteIssue{name}, and render the SDK {name} inside.";
|
|
40
|
+
export const USE_SPACE_TOKENS_MESSAGE = 'Paddings, gaps and margins come from space in @trypattern/sdk/tokens.stylex (space.sm, space.lg), never a number, so one theme can make the whole app roomier or tighter. Write 0 or auto for none, and calc() over tokens for a negative margin: `calc(-1 * ${space.sm})`.';
|
|
41
|
+
export const USE_SIZE_TOKENS_MESSAGE = "Heights, widths, font sizes and line heights read a token (controls.heightMd, iconSizes.md, widths.contentMd, lineHeights.bodySm), or are a percentage, auto or calc() over tokens, so re-valuing a scale in the app's theme moves every piece together and rows still line up.";
|
|
42
|
+
export const THEME_SCALES_MESSAGE = 'src/theme/theme.ts may re-value the size scales (space, fontSizes, lineHeights, controls, iconSizes, borders, widths) as well as colors, radii, fonts and shadows, and Pattern checks the result against the SDK defaults it leaves alone: every value is whole pixels, each scale only grows from one step to the next, a control is at least as tall as the text and icon inside it, and a line height is at least its font size. Each finding names the two values that disagree; change one of them.';
|
|
43
|
+
export const TEXT_VIA_TEXT_MESSAGE = 'Words are drawn with <Text variant="label"> from @trypattern/sdk/react, never with font, line height, alignment or wrapping properties in a style, so every app has one set of text styles and the theme can resize them all at once. Pick the variant closest to what you meant (heading, subheading, title, label, body, caption, code), its tone for the colour, and align, isTruncated or isSelectable for the rest. If none fits, that is an SDK change.';
|
|
44
|
+
export const TEXT_ELEMENTS_MESSAGE = 'Write <Text as="h1" variant="heading"> or <Text as="p"> from @trypattern/sdk/react instead of this element. `as` says what the words are for screen readers and `variant` how they look, so a heading never needs a style of its own. A form label comes from the field: <TextField label="Email">.';
|
|
45
|
+
export const LOOSE_TEXT_MESSAGE = 'Words inside a plain element are drawn with whatever style happens to surround them. Wrap them in <Text> from @trypattern/sdk/react (<Text tone="secondary">Unassigned</Text>), or pass them to the primitive that shows them (<Button>Save</Button>, <Badge>3</Badge>).';
|
|
46
|
+
export const COMPOSE_IN_SCREENS_MESSAGE = 'A screen or layout arranges primitives (Screen, Content, Stack, Row, Card, Text, Button, AppFrame, Toaster and the rest from @trypattern/sdk/react) and the pieces in src/components; it draws nothing itself, so it has no styles, no tokens and no plain elements like <div>. If a screen needs a shape no primitive gives it, make it a piece in src/components/<Name>/ and use that here, so the design system can list it.';
|
|
47
|
+
export const NO_HOVER_IN_APP_MESSAGE = 'Only primitives react to the pointer and to focus. A thing that highlights on hover is a Button, IconButton, ListRow or Link from @trypattern/sdk/react, which already have the hover, press and focus looks, so every clickable thing in the app feels the same.';
|
|
48
|
+
export const TOASTER_ONCE_MESSAGE = 'Toasts need exactly one place to appear: render <Toaster /> from @trypattern/sdk/react once, in the layout that wraps every page (src/layouts/AppLayout.tsx), so a toast shows whichever page is open and never twice.';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { extname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
const SDK_TOKENS_FILE = fileURLToPath(new URL(`../theme/tokens.stylex${extname(import.meta.filename)}`, import.meta.url));
|
|
6
|
+
export function themeFileOf(root) {
|
|
7
|
+
return join(root, 'src', 'theme', 'theme.ts');
|
|
8
|
+
}
|
|
9
|
+
function objectEntries(literal) {
|
|
10
|
+
return literal.properties.flatMap((property) => ts.isPropertyAssignment(property) &&
|
|
11
|
+
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) &&
|
|
12
|
+
ts.isStringLiteralLike(property.initializer)
|
|
13
|
+
? [
|
|
14
|
+
{
|
|
15
|
+
key: property.name.text,
|
|
16
|
+
value: property.initializer.text,
|
|
17
|
+
offset: property.getStart()
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
: []);
|
|
21
|
+
}
|
|
22
|
+
function stylexCalls(path, name) {
|
|
23
|
+
const source = ts.createSourceFile(path, readFileSync(path, 'utf8'), ts.ScriptTarget.Latest, true);
|
|
24
|
+
const calls = [];
|
|
25
|
+
const visit = (node) => {
|
|
26
|
+
if (ts.isCallExpression(node) &&
|
|
27
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
28
|
+
node.expression.name.text === name) {
|
|
29
|
+
const [first, second] = node.arguments;
|
|
30
|
+
const parent = node.parent;
|
|
31
|
+
const group = name === 'defineVars' &&
|
|
32
|
+
ts.isVariableDeclaration(parent) &&
|
|
33
|
+
ts.isIdentifier(parent.name)
|
|
34
|
+
? parent.name.text
|
|
35
|
+
: first !== undefined && ts.isIdentifier(first)
|
|
36
|
+
? first.text
|
|
37
|
+
: '';
|
|
38
|
+
const literal = name === 'defineVars' ? first : second;
|
|
39
|
+
if (literal !== undefined && ts.isObjectLiteralExpression(literal)) {
|
|
40
|
+
calls.push({ group, entries: objectEntries(literal) });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
ts.forEachChild(node, visit);
|
|
44
|
+
};
|
|
45
|
+
visit(source);
|
|
46
|
+
return calls;
|
|
47
|
+
}
|
|
48
|
+
export function themeTokens(root) {
|
|
49
|
+
const tokens = new Map();
|
|
50
|
+
for (const { group, entries } of stylexCalls(SDK_TOKENS_FILE, 'defineVars')) {
|
|
51
|
+
for (const { key, value } of entries) {
|
|
52
|
+
tokens.set(`${group}.${key}`, { value, offset: undefined });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const themeFile = themeFileOf(root);
|
|
56
|
+
if (!existsSync(themeFile))
|
|
57
|
+
return tokens;
|
|
58
|
+
for (const { group, entries } of stylexCalls(themeFile, 'createTheme')) {
|
|
59
|
+
for (const { key, value, offset } of entries) {
|
|
60
|
+
tokens.set(`${group}.${key}`, { value, offset });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return tokens;
|
|
64
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { call, defineServices } from './services.ts';
|
|
2
|
+
export type { CallInput, CallReply, ServiceCall, ServiceMemory, Services } from './services.ts';
|
|
3
|
+
export { defineTables } from './table.ts';
|
|
4
|
+
export type { Access, Columns, Fields, Level, PublicRow, Row, Seed, Table, TableDefinition, TableOf, Tables, Value, Writes } from './table.ts';
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { ServerEnv } from './env.ts';
|
|
2
|
+
export declare const auth: import("hono/hono-base").HonoBase<ServerEnv, {
|
|
3
|
+
"/google": {
|
|
4
|
+
$get: {
|
|
5
|
+
input: {};
|
|
6
|
+
output: {
|
|
7
|
+
error: string;
|
|
8
|
+
};
|
|
9
|
+
outputFormat: "json";
|
|
10
|
+
status: 503;
|
|
11
|
+
} | {
|
|
12
|
+
input: {};
|
|
13
|
+
output: undefined;
|
|
14
|
+
outputFormat: "redirect";
|
|
15
|
+
status: 302;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
} & {
|
|
19
|
+
"/finish": {
|
|
20
|
+
$get: {
|
|
21
|
+
input: {};
|
|
22
|
+
output: undefined;
|
|
23
|
+
outputFormat: "redirect";
|
|
24
|
+
status: 302;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
} & {
|
|
28
|
+
"/email": {
|
|
29
|
+
$post: {
|
|
30
|
+
input: {};
|
|
31
|
+
output: {
|
|
32
|
+
error: string;
|
|
33
|
+
};
|
|
34
|
+
outputFormat: "json";
|
|
35
|
+
status: 400;
|
|
36
|
+
} | {
|
|
37
|
+
input: {};
|
|
38
|
+
output: {
|
|
39
|
+
error: string;
|
|
40
|
+
};
|
|
41
|
+
outputFormat: "json";
|
|
42
|
+
status: 503;
|
|
43
|
+
} | {
|
|
44
|
+
input: {};
|
|
45
|
+
output: {
|
|
46
|
+
error: string;
|
|
47
|
+
};
|
|
48
|
+
outputFormat: "json";
|
|
49
|
+
status: 429;
|
|
50
|
+
} | {
|
|
51
|
+
input: {};
|
|
52
|
+
output: {
|
|
53
|
+
ok: true;
|
|
54
|
+
};
|
|
55
|
+
outputFormat: "json";
|
|
56
|
+
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
} & {
|
|
60
|
+
"/email/verify": {
|
|
61
|
+
$post: {
|
|
62
|
+
input: {};
|
|
63
|
+
output: {
|
|
64
|
+
isAdmin: boolean;
|
|
65
|
+
id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
email: string;
|
|
68
|
+
avatarUrl: string | null;
|
|
69
|
+
};
|
|
70
|
+
outputFormat: "json";
|
|
71
|
+
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
72
|
+
} | {
|
|
73
|
+
input: {};
|
|
74
|
+
output: {
|
|
75
|
+
error: string;
|
|
76
|
+
};
|
|
77
|
+
outputFormat: "json";
|
|
78
|
+
status: 401;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
} & {
|
|
82
|
+
"/sign-out": {
|
|
83
|
+
$post: {
|
|
84
|
+
input: {};
|
|
85
|
+
output: {
|
|
86
|
+
ok: true;
|
|
87
|
+
};
|
|
88
|
+
outputFormat: "json";
|
|
89
|
+
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
}, "/", "/sign-out">;
|
|
93
|
+
export declare const devSignIn: import("hono/hono-base").HonoBase<ServerEnv, {
|
|
94
|
+
"/dev-sign-in": {
|
|
95
|
+
$post: {
|
|
96
|
+
input: {};
|
|
97
|
+
output: {
|
|
98
|
+
isAdmin: boolean;
|
|
99
|
+
id: string;
|
|
100
|
+
name: string;
|
|
101
|
+
email: string;
|
|
102
|
+
avatarUrl: string | null;
|
|
103
|
+
};
|
|
104
|
+
outputFormat: "json";
|
|
105
|
+
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
106
|
+
} | {
|
|
107
|
+
input: {};
|
|
108
|
+
output: {
|
|
109
|
+
error: string;
|
|
110
|
+
};
|
|
111
|
+
outputFormat: "json";
|
|
112
|
+
status: 400;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
}, "/", "/dev-sign-in">;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { deleteCookie, getCookie, setCookie } from 'hono/cookie';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { AUTH_URL, authServiceFor } from "./authService.js";
|
|
5
|
+
import { forgetSession, readPass, startSession } from "./session.js";
|
|
6
|
+
import { readTitle } from "./settings.js";
|
|
7
|
+
import { deleteSession } from "./sessions.js";
|
|
8
|
+
import { saveUser } from "./users.js";
|
|
9
|
+
const GOOGLE_COOKIE = 'pattern_google';
|
|
10
|
+
const EMAIL_COOKIE = 'pattern_email';
|
|
11
|
+
const signInCookie = {
|
|
12
|
+
path: '/api/auth',
|
|
13
|
+
httpOnly: true,
|
|
14
|
+
secure: true,
|
|
15
|
+
sameSite: 'Lax',
|
|
16
|
+
maxAge: 600
|
|
17
|
+
};
|
|
18
|
+
const NOT_SET_UP = 'Sign-in is not set up for this app: it has no AUTH_SECRET. Pattern sets it when it deploys the app.';
|
|
19
|
+
const GOOGLE_ONLY_DEPLOYED = 'Google sign-in goes through auth.trypattern.ai, which only knows deployed apps. Locally, sign in with an email code or run `app sign-in <email>`.';
|
|
20
|
+
const googleFlow = z.object({ nonce: z.string(), next: z.string() });
|
|
21
|
+
const emailBody = z.object({ email: z.email() });
|
|
22
|
+
const codeBody = z.object({ code: z.string().regex(/^\d{6}$/) });
|
|
23
|
+
const devSignInBody = z.object({ email: z.email() });
|
|
24
|
+
function randomToken() {
|
|
25
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
26
|
+
return btoa(String.fromCharCode(...bytes))
|
|
27
|
+
.replaceAll('+', '-')
|
|
28
|
+
.replaceAll('/', '_')
|
|
29
|
+
.replaceAll('=', '');
|
|
30
|
+
}
|
|
31
|
+
function safePath(path) {
|
|
32
|
+
return path !== undefined && path.startsWith('/') && !path.startsWith('//')
|
|
33
|
+
? path
|
|
34
|
+
: '/';
|
|
35
|
+
}
|
|
36
|
+
function readGoogleFlow(c) {
|
|
37
|
+
const value = getCookie(c, GOOGLE_COOKIE);
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = googleFlow.safeParse(JSON.parse(value));
|
|
42
|
+
return parsed.success ? parsed.data : undefined;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function hostnameOf(c) {
|
|
49
|
+
return new URL(c.req.url).hostname;
|
|
50
|
+
}
|
|
51
|
+
async function signIn(c, person) {
|
|
52
|
+
const user = await saveUser(c.env.DB, person);
|
|
53
|
+
await startSession(c, user);
|
|
54
|
+
return { ...user, isAdmin: c.get('admins').has(user.email) };
|
|
55
|
+
}
|
|
56
|
+
export const auth = new Hono()
|
|
57
|
+
.get('/google', (c) => {
|
|
58
|
+
if (c.env.AUTH_SECRET === undefined) {
|
|
59
|
+
return c.json({ error: GOOGLE_ONLY_DEPLOYED }, 503);
|
|
60
|
+
}
|
|
61
|
+
const nonce = randomToken();
|
|
62
|
+
const next = safePath(c.req.query('next'));
|
|
63
|
+
setCookie(c, GOOGLE_COOKIE, JSON.stringify({ nonce, next }), signInCookie);
|
|
64
|
+
const start = new URL('/google', AUTH_URL);
|
|
65
|
+
start.searchParams.set('hostname', hostnameOf(c));
|
|
66
|
+
start.searchParams.set('state', nonce);
|
|
67
|
+
return c.redirect(start.href);
|
|
68
|
+
})
|
|
69
|
+
.get('/finish', async (c) => {
|
|
70
|
+
const flow = readGoogleFlow(c);
|
|
71
|
+
deleteCookie(c, GOOGLE_COOKIE, signInCookie);
|
|
72
|
+
const { code, state } = c.req.query();
|
|
73
|
+
const service = authServiceFor(c.env);
|
|
74
|
+
if (flow === undefined ||
|
|
75
|
+
service === undefined ||
|
|
76
|
+
code === undefined ||
|
|
77
|
+
state !== flow.nonce) {
|
|
78
|
+
return c.redirect('/');
|
|
79
|
+
}
|
|
80
|
+
const found = await service.exchange(code);
|
|
81
|
+
if (found === undefined || found.hostname !== hostnameOf(c)) {
|
|
82
|
+
return c.redirect('/');
|
|
83
|
+
}
|
|
84
|
+
await signIn(c, found.identity);
|
|
85
|
+
return c.redirect(flow.next);
|
|
86
|
+
})
|
|
87
|
+
.post('/email', async (c) => {
|
|
88
|
+
const parsed = emailBody.safeParse(await c.req.json());
|
|
89
|
+
if (!parsed.success) {
|
|
90
|
+
return c.json({ error: 'Enter a valid email address.' }, 400);
|
|
91
|
+
}
|
|
92
|
+
const service = authServiceFor(c.env);
|
|
93
|
+
if (service === undefined)
|
|
94
|
+
return c.json({ error: NOT_SET_UP }, 503);
|
|
95
|
+
const appName = await readTitle(c.env.DB);
|
|
96
|
+
const sent = await service.sendCode({
|
|
97
|
+
email: parsed.data.email.toLowerCase(),
|
|
98
|
+
hostname: hostnameOf(c),
|
|
99
|
+
...(appName === '' ? {} : { appName })
|
|
100
|
+
});
|
|
101
|
+
if (sent === undefined) {
|
|
102
|
+
return c.json({ error: 'Couldn’t send a code to that address.' }, 429);
|
|
103
|
+
}
|
|
104
|
+
setCookie(c, EMAIL_COOKIE, sent.challenge, signInCookie);
|
|
105
|
+
return c.json({ ok: true });
|
|
106
|
+
})
|
|
107
|
+
.post('/email/verify', async (c) => {
|
|
108
|
+
const parsed = codeBody.safeParse(await c.req.json());
|
|
109
|
+
const challenge = getCookie(c, EMAIL_COOKIE);
|
|
110
|
+
const service = authServiceFor(c.env);
|
|
111
|
+
const identity = !parsed.success || challenge === undefined || service === undefined
|
|
112
|
+
? undefined
|
|
113
|
+
: await service.verifyCode({ challenge, code: parsed.data.code });
|
|
114
|
+
if (identity === undefined) {
|
|
115
|
+
return c.json({ error: 'That code is wrong or has expired.' }, 401);
|
|
116
|
+
}
|
|
117
|
+
deleteCookie(c, EMAIL_COOKIE, signInCookie);
|
|
118
|
+
return c.json(await signIn(c, identity));
|
|
119
|
+
})
|
|
120
|
+
.post('/sign-out', async (c) => {
|
|
121
|
+
const pass = await readPass(c);
|
|
122
|
+
if (pass !== undefined)
|
|
123
|
+
await deleteSession(c.env.DB, pass.sessionId);
|
|
124
|
+
forgetSession(c);
|
|
125
|
+
return c.json({ ok: true });
|
|
126
|
+
});
|
|
127
|
+
export const devSignIn = new Hono().post('/dev-sign-in', async (c) => {
|
|
128
|
+
const parsed = devSignInBody.safeParse(await c.req.json());
|
|
129
|
+
if (!parsed.success) {
|
|
130
|
+
return c.json({ error: z.prettifyError(parsed.error) }, 400);
|
|
131
|
+
}
|
|
132
|
+
const { email } = parsed.data;
|
|
133
|
+
return c.json(await signIn(c, { email, name: null, avatarUrl: null }));
|
|
134
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ServerBindings } from './env.ts';
|
|
2
|
+
import type { ExchangeReply, SendCodeReply, SendCodeRequest, VerifiedIdentity, VerifyCodeRequest } from './signInWire.ts';
|
|
3
|
+
export declare const AUTH_URL = "https://auth.trypattern.ai";
|
|
4
|
+
export type AuthService = {
|
|
5
|
+
exchange: (code: string) => Promise<ExchangeReply | undefined>;
|
|
6
|
+
sendCode: (request: SendCodeRequest) => Promise<SendCodeReply | undefined>;
|
|
7
|
+
verifyCode: (request: VerifyCodeRequest) => Promise<VerifiedIdentity | undefined>;
|
|
8
|
+
};
|
|
9
|
+
export declare function authServiceFor(env: ServerBindings): AuthService | undefined;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { exchangeReply, sendCodeReply, verifyCodeReply } from "./signInWire.js";
|
|
2
|
+
export const AUTH_URL = 'https://auth.trypattern.ai';
|
|
3
|
+
function httpAuthService(secret) {
|
|
4
|
+
async function post(path, body, reply) {
|
|
5
|
+
const response = await fetch(`${AUTH_URL}${path}`, {
|
|
6
|
+
method: 'POST',
|
|
7
|
+
headers: {
|
|
8
|
+
authorization: `Bearer ${secret}`,
|
|
9
|
+
'content-type': 'application/json'
|
|
10
|
+
},
|
|
11
|
+
body: JSON.stringify(body)
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok)
|
|
14
|
+
return undefined;
|
|
15
|
+
const parsed = reply.safeParse(await response.json());
|
|
16
|
+
return parsed.success ? parsed.data : undefined;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
exchange: (code) => post('/exchange', { code }, exchangeReply),
|
|
20
|
+
sendCode: (request) => post('/email/send', request, sendCodeReply),
|
|
21
|
+
verifyCode: async (request) => (await post('/email/verify', request, verifyCodeReply))?.identity
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const devCodes = new Map();
|
|
25
|
+
function sixDigits() {
|
|
26
|
+
const [value = 0] = crypto.getRandomValues(new Uint32Array(1));
|
|
27
|
+
return String(value % 1_000_000).padStart(6, '0');
|
|
28
|
+
}
|
|
29
|
+
const devAuthService = {
|
|
30
|
+
exchange: async () => undefined,
|
|
31
|
+
sendCode: async ({ email }) => {
|
|
32
|
+
const challenge = crypto.randomUUID();
|
|
33
|
+
const code = sixDigits();
|
|
34
|
+
devCodes.set(challenge, { email, code });
|
|
35
|
+
console.log(`Sign-in code for ${email}: ${code}`);
|
|
36
|
+
return { challenge };
|
|
37
|
+
},
|
|
38
|
+
verifyCode: async ({ challenge, code }) => {
|
|
39
|
+
const sent = devCodes.get(challenge);
|
|
40
|
+
if (sent === undefined || sent.code !== code)
|
|
41
|
+
return undefined;
|
|
42
|
+
devCodes.delete(challenge);
|
|
43
|
+
return { email: sent.email, name: null, avatarUrl: null, via: 'email' };
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
export function authServiceFor(env) {
|
|
47
|
+
if (env.AUTH_SECRET !== undefined)
|
|
48
|
+
return httpAuthService(env.AUTH_SECRET);
|
|
49
|
+
return import.meta.env.DEV ? devAuthService : undefined;
|
|
50
|
+
}
|