@trypattern/sdk 0.1.25 → 0.1.27

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/app.d.ts CHANGED
@@ -10,6 +10,7 @@ import type { Clock } from './clock.ts';
10
10
  import type { Gate } from './gate.ts';
11
11
  import { ActionsStore } from './actions.ts';
12
12
  import type { Action, ActionsBuilder } from './actions.ts';
13
+ import { Device } from './builtins/Device.ts';
13
14
  import { Toasts } from './builtins/Toasts.ts';
14
15
  import { PageStore } from './page.ts';
15
16
  import type { BuiltInModels } from './registered.ts';
@@ -83,6 +84,7 @@ export type StoresOf<Group extends Tables, Custom, Item extends Action = Action>
83
84
  app: AppStore;
84
85
  actions: ActionsStore<Item>;
85
86
  toasts: Toasts;
87
+ device: Device;
86
88
  page: PageStore;
87
89
  } & TableStores<Group> & PublicStores<Group> & BlockStores<Group> & Custom;
88
90
  type BlockTablesMissing<Name extends string> = StaticTypeError<`The ${Name} block keeps its rows in Pattern's own tables: add them to tables in schema.ts (for bookings, ...bookingTables from '@trypattern/sdk/schema'), then run app migration add_${Name}`>;
package/dist/app.js CHANGED
@@ -10,6 +10,7 @@ import { AppSettings } from "./builtins/AppSettings.js";
10
10
  import { BUILT_IN_GROUPS, builtInActions } from "./builtins/actions.js";
11
11
  import { Palette } from "./builtins/Palette.js";
12
12
  import { SignIn } from "./builtins/SignIn.js";
13
+ import { Device } from "./builtins/Device.js";
13
14
  import { Toasts } from "./builtins/Toasts.js";
14
15
  import { PageStore } from "./page.js";
15
16
  import { assertNotBuilding, snapshotsOf } from "./store.js";
@@ -126,15 +127,16 @@ export function defineApp(definition) {
126
127
  new block.Store(cache.persisted(`stores.${block.name}`), api.services)
127
128
  ]));
128
129
  const toasts = new Toasts(cache.persisted('stores.toasts'), {});
130
+ const device = new Device(cache.persisted('stores.device'), {});
129
131
  const clash = clashOf([
130
- ['session', 'app', 'actions', 'toasts', 'page'],
132
+ ['session', 'app', 'actions', 'toasts', 'device', 'page'],
131
133
  Object.keys(data.tables),
132
134
  Object.keys(data.publicViews),
133
135
  Object.keys(blockStores),
134
136
  Object.keys(custom)
135
137
  ]);
136
138
  if (clash !== undefined) {
137
- throw new Error(`Two stores are called "${clash}". The built-in session, app, actions, toasts and page stores, each table, each table's public view, each block and each store in src/stores/index.ts need their own name.`);
139
+ throw new Error(`Two stores are called "${clash}". The built-in session, app, actions, toasts, device and page stores, each table, each table's public view, each block and each store in src/stores/index.ts need their own name.`);
138
140
  }
139
141
  const actions = new ActionsStore(this.#page, [...(definition.actions ?? []), ...builtInActions], [
140
142
  ...(definition.actionGroups ?? []),
@@ -145,6 +147,7 @@ export function defineApp(definition) {
145
147
  app,
146
148
  actions,
147
149
  toasts,
150
+ device,
148
151
  page: this.#page,
149
152
  ...data.tables,
150
153
  ...data.publicViews,
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+ import type { Persisted } from '../state.ts';
3
+ declare const Device_base: import("../store.ts").StoreClass<{
4
+ today: z.ZodDefault<z.ZodString>;
5
+ isCompact: z.ZodDefault<z.ZodBoolean>;
6
+ }, [], Record<never, never>>;
7
+ export declare class Device extends Device_base {
8
+ #private;
9
+ constructor(persisted: Persisted, services: Record<never, never>);
10
+ protected initialState(): {
11
+ today: string;
12
+ isCompact: boolean;
13
+ };
14
+ }
15
+ export {};
@@ -0,0 +1,52 @@
1
+ import { z } from 'zod';
2
+ import { defineStore } from "../store.js";
3
+ const Store = defineStore();
4
+ const COMPACT = '(max-width: 767px)';
5
+ function dayOf(at) {
6
+ const month = String(at.getMonth() + 1).padStart(2, '0');
7
+ const day = String(at.getDate()).padStart(2, '0');
8
+ return `${at.getFullYear()}-${month}-${day}`;
9
+ }
10
+ function msUntilTomorrow(at) {
11
+ const tomorrow = new Date(at);
12
+ tomorrow.setHours(24, 0, 1, 0);
13
+ return tomorrow.getTime() - at.getTime();
14
+ }
15
+ function compactQuery() {
16
+ return typeof window === 'undefined' || !('matchMedia' in window)
17
+ ? undefined
18
+ : window.matchMedia(COMPACT);
19
+ }
20
+ export class Device extends Store({
21
+ state: {
22
+ today: z.string().default(''),
23
+ isCompact: z.boolean().default(false)
24
+ }
25
+ }) {
26
+ #midnight;
27
+ constructor(persisted, services) {
28
+ super(persisted, services);
29
+ const query = compactQuery();
30
+ if (query === undefined)
31
+ return;
32
+ query.addEventListener('change', () => this.set({ isCompact: query.matches }));
33
+ const refresh = () => this.#refreshToday();
34
+ window.addEventListener('focus', refresh);
35
+ document.addEventListener('visibilitychange', refresh);
36
+ this.#waitForMidnight();
37
+ }
38
+ initialState() {
39
+ return {
40
+ today: dayOf(new Date()),
41
+ isCompact: compactQuery()?.matches ?? false
42
+ };
43
+ }
44
+ #refreshToday() {
45
+ this.set({ today: dayOf(new Date()) });
46
+ this.#waitForMidnight();
47
+ }
48
+ #waitForMidnight() {
49
+ clearTimeout(this.#midnight);
50
+ this.#midnight = setTimeout(() => this.#refreshToday(), msUntilTomorrow(new Date()));
51
+ }
52
+ }
@@ -5,13 +5,14 @@ type TextProps = {
5
5
  children: ReactNode;
6
6
  variant?: 'hero' | 'display' | 'section' | 'heading' | 'subheading' | 'title' | 'lead' | 'intro' | 'label' | 'body' | 'caption' | 'code';
7
7
  tone?: Tone;
8
+ weight?: 'normal' | 'medium' | 'semibold' | 'bold' | 'heavy';
8
9
  align?: 'start' | 'center' | 'end';
9
10
  as?: 'span' | 'p' | 'h1' | 'h2' | 'h3';
10
11
  isTruncated?: boolean;
11
12
  isSelectable?: boolean;
12
13
  style?: Placement;
13
14
  };
14
- export declare function Text({ children, variant, tone, align, as, isTruncated, isSelectable, style }: TextProps): import("react").DetailedReactHTMLElement<Readonly<{
15
+ export declare function Text({ children, variant, tone, weight, align, as, isTruncated, isSelectable, style }: TextProps): import("react").DetailedReactHTMLElement<Readonly<{
15
16
  className?: string;
16
17
  'data-style-src'?: string;
17
18
  style?: Readonly<{
@@ -2,8 +2,8 @@ import * as stylex from '@stylexjs/stylex';
2
2
  import { createElement } from 'react';
3
3
  import { breakpoints, fontSizes, fontWeights, fonts, lineHeights, trackings } from "../theme/tokens.stylex.js";
4
4
  import { tones } from "./tones.js";
5
- export function Text({ children, variant = 'body', tone = 'inherit', align, as = 'span', isTruncated = false, isSelectable = false, style }) {
6
- return createElement(as, stylex.props(as !== 'span' && styles.block, variants[variant], tones[tone], align !== undefined && alignments[align], isTruncated && styles.truncated, isSelectable && styles.selectable, style), children);
5
+ export function Text({ children, variant = 'body', tone = 'inherit', weight, align, as = 'span', isTruncated = false, isSelectable = false, style }) {
6
+ return createElement(as, stylex.props(as !== 'span' && styles.block, variants[variant], tones[tone], weight !== undefined && weights[weight], align !== undefined && alignments[align], isTruncated && styles.truncated, isSelectable && styles.selectable, style), children);
7
7
  }
8
8
  const styles = stylex.create({
9
9
  block: {
@@ -22,11 +22,12 @@ const styles = stylex.create({
22
22
  });
23
23
  const variants = stylex.create({
24
24
  hero: {
25
+ fontFamily: fonts.heading,
25
26
  fontSize: {
26
27
  default: fontSizes.heading1,
27
28
  [breakpoints.compact]: fontSizes.heading2
28
29
  },
29
- fontWeight: fontWeights.medium,
30
+ fontWeight: fontWeights.heading,
30
31
  lineHeight: {
31
32
  default: lineHeights.heading1,
32
33
  [breakpoints.compact]: lineHeights.heading2
@@ -34,11 +35,12 @@ const variants = stylex.create({
34
35
  letterSpacing: trackings.hero
35
36
  },
36
37
  display: {
38
+ fontFamily: fonts.heading,
37
39
  fontSize: {
38
40
  default: fontSizes.heading2,
39
41
  [breakpoints.compact]: fontSizes.heading3
40
42
  },
41
- fontWeight: fontWeights.medium,
43
+ fontWeight: fontWeights.heading,
42
44
  lineHeight: {
43
45
  default: lineHeights.heading2,
44
46
  [breakpoints.compact]: lineHeights.heading3
@@ -46,11 +48,12 @@ const variants = stylex.create({
46
48
  letterSpacing: trackings.display
47
49
  },
48
50
  section: {
51
+ fontFamily: fonts.heading,
49
52
  fontSize: {
50
53
  default: fontSizes.heading3,
51
54
  [breakpoints.compact]: fontSizes.heading5
52
55
  },
53
- fontWeight: fontWeights.medium,
56
+ fontWeight: fontWeights.heading,
54
57
  lineHeight: {
55
58
  default: lineHeights.heading3,
56
59
  [breakpoints.compact]: lineHeights.heading5
@@ -58,14 +61,16 @@ const variants = stylex.create({
58
61
  letterSpacing: trackings.section
59
62
  },
60
63
  heading: {
64
+ fontFamily: fonts.heading,
61
65
  fontSize: fontSizes.heading5,
62
- fontWeight: fontWeights.medium,
66
+ fontWeight: fontWeights.heading,
63
67
  lineHeight: lineHeights.heading5,
64
68
  letterSpacing: trackings.heading
65
69
  },
66
70
  subheading: {
71
+ fontFamily: fonts.heading,
67
72
  fontSize: fontSizes.heading6,
68
- fontWeight: fontWeights.medium,
73
+ fontWeight: fontWeights.heading,
69
74
  lineHeight: lineHeights.heading6
70
75
  },
71
76
  title: {
@@ -86,23 +91,23 @@ const variants = stylex.create({
86
91
  label: {
87
92
  fontSize: {
88
93
  default: fontSizes.bodySm,
89
- [breakpoints.compact]: fontSizes.bodyBase
94
+ [breakpoints.compact]: fontSizes.bodyCompact
90
95
  },
91
96
  fontWeight: fontWeights.medium,
92
97
  lineHeight: {
93
98
  default: lineHeights.bodySm,
94
- [breakpoints.compact]: lineHeights.body
99
+ [breakpoints.compact]: lineHeights.bodyCompact
95
100
  }
96
101
  },
97
102
  body: {
98
103
  fontSize: {
99
104
  default: fontSizes.bodySm,
100
- [breakpoints.compact]: fontSizes.bodyBase
105
+ [breakpoints.compact]: fontSizes.bodyCompact
101
106
  },
102
107
  fontWeight: fontWeights.normal,
103
108
  lineHeight: {
104
109
  default: lineHeights.bodySm,
105
- [breakpoints.compact]: lineHeights.body
110
+ [breakpoints.compact]: lineHeights.bodyCompact
106
111
  }
107
112
  },
108
113
  caption: {
@@ -117,6 +122,13 @@ const variants = stylex.create({
117
122
  lineHeight: lineHeights.bodySm
118
123
  }
119
124
  });
125
+ const weights = stylex.create({
126
+ normal: { fontWeight: fontWeights.normal },
127
+ medium: { fontWeight: fontWeights.medium },
128
+ semibold: { fontWeight: fontWeights.semibold },
129
+ bold: { fontWeight: fontWeights.bold },
130
+ heavy: { fontWeight: fontWeights.heavy }
131
+ });
120
132
  const alignments = stylex.create({
121
133
  start: { textAlign: 'start' },
122
134
  center: { textAlign: 'center' },
@@ -2,6 +2,7 @@ import type { AppSnapshotShape, ModelServicesOf, NavigateCommand, PressKeyComman
2
2
  import { z } from 'zod';
3
3
  import type { Action, DefineActions } from './actions.ts';
4
4
  import type { ServiceClient, Services } from './services.ts';
5
+ import type { StoreFactory } from './store.ts';
5
6
  import type { Instances, MessagesOf, ModelClassLike, ModelSnapshots } from './model.ts';
6
7
  import type { BUILT_IN_GROUPS } from './builtins/actions.ts';
7
8
  import type { Account } from './builtins/Account.ts';
@@ -48,7 +49,7 @@ export type ActionContext = {
48
49
  };
49
50
  export type ModelServices = ModelServicesOf<AppPath>;
50
51
  export declare const Model: <S extends import("./state.ts").StateSpec = Record<never, never>, const P extends readonly (keyof S & string)[] = []>(config?: import("./model.ts").ModelConfig<S, P>) => import("./model.ts").ModelClass<AppStores, ModelServices, S, P>;
51
- export declare const Store: <S extends import("./state.ts").StateSpec = Record<never, never>, const P extends readonly (keyof S & string)[] = []>(config: import("./store.ts").StoreConfig<S, P>) => import("./store.ts").StoreClass<S, P, ServiceClient<Empty>>;
52
+ export declare const Store: StoreFactory<AppServices>;
52
53
  export declare const defineActions: DefineActions<ActionContext, RegisteredAction, AppCommand>;
53
54
  export declare const appPath: z.ZodCustom<AppPath, AppPath>;
54
55
  type ActionInput = Pick<AppAction, 'id' | 'label' | 'group' | 'icon'> & Partial<Pick<AppAction, 'keys' | 'keywords' | 'isDanger' | 'isSearchOnly' | 'isHidden'>> & {
@@ -378,14 +378,6 @@ function misplacedSourceFiles() {
378
378
  return ownedFiles(SRC_DIRECTORY).filter((path) => !isWhereItBelongs(relative(SRC_DIRECTORY, path)) || !isInItsFolder(path));
379
379
  }
380
380
  const SCHEMA_FILE = join(ROOT, 'schema.ts');
381
- function rowTypeName(table) {
382
- const one = table.endsWith('ies')
383
- ? `${table.slice(0, -3)}y`
384
- : table.endsWith('s')
385
- ? table.slice(0, -1)
386
- : table;
387
- return `${one.charAt(0).toUpperCase()}${one.slice(1)}`;
388
- }
389
381
  function isExported(node) {
390
382
  return (ts.canHaveModifiers(node) &&
391
383
  (ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword));
@@ -403,8 +395,6 @@ function schemaFindings() {
403
395
  return [];
404
396
  const text = readFileSync(SCHEMA_FILE, 'utf8');
405
397
  const source = ts.createSourceFile(SCHEMA_FILE, text, ts.ScriptTarget.Latest, true);
406
- const tables = [];
407
- const rowTypes = new Set();
408
398
  let exportsTables = false;
409
399
  let exportsAdmins = false;
410
400
  const visit = (node) => {
@@ -422,29 +412,6 @@ function schemaFindings() {
422
412
  definesTables(declaration.initializer, source))) {
423
413
  exportsTables = true;
424
414
  }
425
- if (ts.isCallExpression(node) &&
426
- node.expression.getText(source) === 'defineTables') {
427
- const [definitions] = node.arguments;
428
- if (definitions !== undefined &&
429
- ts.isObjectLiteralExpression(definitions)) {
430
- for (const property of definitions.properties) {
431
- const name = property.name;
432
- if (name !== undefined &&
433
- (ts.isIdentifier(name) || ts.isStringLiteral(name))) {
434
- tables.push({ key: name.text, offset: property.getStart(source) });
435
- }
436
- }
437
- }
438
- }
439
- if (ts.isTypeAliasDeclaration(node) &&
440
- isExported(node) &&
441
- ts.isTypeReferenceNode(node.type) &&
442
- node.type.typeName.getText(source) === 'Row') {
443
- const [argument] = node.type.typeArguments ?? [];
444
- if (argument !== undefined && ts.isTypeQueryNode(argument)) {
445
- rowTypes.add(argument.exprName.getText(source));
446
- }
447
- }
448
415
  ts.forEachChild(node, visit);
449
416
  };
450
417
  visit(source);
@@ -456,18 +423,9 @@ function schemaFindings() {
456
423
  }
457
424
  ];
458
425
  }
459
- const missingAdmins = exportsAdmins
426
+ return exportsAdmins
460
427
  ? []
461
428
  : [{ offset: 0, fix: 'export const admins: string[] = []' }];
462
- return [
463
- ...missingAdmins,
464
- ...tables
465
- .filter(({ key }) => !rowTypes.has(`tables.${key}`))
466
- .map(({ key, offset }) => ({
467
- offset,
468
- fix: `export type ${rowTypeName(key)} = Row<typeof tables.${key}>`
469
- }))
470
- ];
471
429
  }
472
430
  const ACCESS_FLOWS_FILE = join(ROOT, 'flows', 'Access.flow.ts');
473
431
  function toasterPlacements() {
@@ -22,7 +22,7 @@ export declare const DEV_ONLY_SIGN_IN_MESSAGE = "The server has exactly one dev-
22
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
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
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.";
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' } })`. When another file needs a table's row type, schema.ts exports it, named for one row (`export type Issue = Row<typeof tables.issues>`). It also exports 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
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
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
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.";
@@ -30,7 +30,7 @@ export declare const IMPORT_FROM_ROOT_MESSAGE = "Import project files by their p
30
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
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
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.";
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. For the date today read this.stores.device.state.today (YYYY-MM-DD on this device, kept current past midnight), and for a phone-width screen this.stores.device.state.isCompact.";
34
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
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
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.";
@@ -22,7 +22,7 @@ export const DEV_ONLY_SIGN_IN_MESSAGE = 'The server has exactly one dev-only swi
22
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
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
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.";
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' } })`. When another file needs a table's row type, schema.ts exports it, named for one row (`export type Issue = Row<typeof tables.issues>`). It also exports 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
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
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
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.';
@@ -30,7 +30,7 @@ export const IMPORT_FROM_ROOT_MESSAGE = 'Import project files by their path from
30
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
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
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.';
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. For the date today read this.stores.device.state.today (YYYY-MM-DD on this device, kept current past midnight), and for a phone-width screen this.stores.device.state.isCompact.';
34
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
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
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.";
package/dist/start.d.ts CHANGED
@@ -5,8 +5,8 @@ import type { IconDrawing } from './components/icons.ts';
5
5
  import type { Interaction } from './components/interaction.ts';
6
6
  import type { ReactApp } from './hooks/appContext.ts';
7
7
  import type { Tables } from './table.ts';
8
- import { borders, colors, controls, fontSizes, fonts, iconSizes, lineHeights, radii, shadows, space, widths } from './theme/tokens.stylex.ts';
9
- type Themeable = typeof colors | typeof radii | typeof fonts | typeof shadows | typeof space | typeof fontSizes | typeof lineHeights | typeof controls | typeof iconSizes | typeof borders | typeof widths;
8
+ import { borders, colors, controls, fontSizes, fontWeights, fonts, iconSizes, lineHeights, radii, shadows, space, trackings, widths } from './theme/tokens.stylex.ts';
9
+ type Themeable = typeof colors | typeof radii | typeof fonts | typeof shadows | typeof space | typeof fontSizes | typeof fontWeights | typeof lineHeights | typeof trackings | typeof controls | typeof iconSizes | typeof borders | typeof widths;
10
10
  export type StartOptions = {
11
11
  theme?: readonly Theme<Themeable>[];
12
12
  icons?: Readonly<Record<string, IconDrawing>>;
package/dist/start.js CHANGED
@@ -12,7 +12,7 @@ import { manualClock, realClock } from "./clock.js";
12
12
  import { Gate } from "./gate.js";
13
13
  import { AppProvider } from "./hooks/appContext.js";
14
14
  import { routeObjects } from "./routes.js";
15
- import { borders, breakpoints, colors, controls, fontFeatures, fontSizes, fontWeights, fonts, iconSizes, lineHeights, radii, shadows, space, widths } from "./theme/tokens.stylex.js";
15
+ import { borders, breakpoints, colors, controls, fontFeatures, fontSizes, fontWeights, fonts, iconSizes, lineHeights, radii, shadows, space, trackings, widths } from "./theme/tokens.stylex.js";
16
16
  const TEST_SEED = 'patternSeed';
17
17
  const INLINE_DATA_ID = 'pattern-data';
18
18
  const TEST_ADMINS = 'patternAdmins';
package/dist/store.d.ts CHANGED
@@ -34,7 +34,8 @@ export declare abstract class DeclaredStore<State extends object, Client = Recor
34
34
  export type StoreClass<S extends StateSpec, P extends readonly (keyof S & string)[], Client = Record<never, never>> = (abstract new (persisted: Persisted, services: Client) => DeclaredStore<StateOf<S>, Client>) & {
35
35
  readonly config: StoreConfig<S, P>;
36
36
  };
37
- export declare function defineStore<Client>(): <S extends StateSpec = Record<never, never>, const P extends readonly (keyof S & string)[] = []>(config: StoreConfig<S, P>) => StoreClass<S, P, Client>;
37
+ export type StoreFactory<Client> = <S extends StateSpec = Record<never, never>, const P extends readonly (keyof S & string)[] = []>(config: StoreConfig<S, P>) => StoreClass<S, P, Client>;
38
+ export declare function defineStore<Client>(): StoreFactory<Client>;
38
39
  export type Snapshots<Group> = {
39
40
  [Key in keyof Group]: Group[Key] extends {
40
41
  readonly state: infer State;
@@ -174,6 +174,7 @@ export declare const fontSizes: stylex.VarGroup<Readonly<{
174
174
  bodyMd: string;
175
175
  bodyBase: string;
176
176
  bodyLg: string;
177
+ bodyCompact: string;
177
178
  heading6: string;
178
179
  heading5: string;
179
180
  heading4: string;
@@ -194,6 +195,7 @@ export declare const lineHeights: stylex.VarGroup<Readonly<{
194
195
  bodySm: string;
195
196
  body: string;
196
197
  bodyLg: string;
198
+ bodyCompact: string;
197
199
  heading6: string;
198
200
  heading5: string;
199
201
  heading4: string;
@@ -203,6 +205,7 @@ export declare const lineHeights: stylex.VarGroup<Readonly<{
203
205
  }>, symbol>;
204
206
  export declare const fonts: stylex.VarGroup<Readonly<{
205
207
  sans: string;
208
+ heading: string;
206
209
  mono: string;
207
210
  }>, symbol>;
208
211
  export declare const fontFeatures: stylex.VarGroup<Readonly<{
@@ -215,6 +218,8 @@ export declare const fontWeights: stylex.VarGroup<Readonly<{
215
218
  medium: string;
216
219
  semibold: string;
217
220
  bold: string;
221
+ heavy: string;
222
+ heading: string;
218
223
  }>, symbol>;
219
224
  export declare const shadows: stylex.VarGroup<Readonly<{
220
225
  border: string;
@@ -174,6 +174,7 @@ export const fontSizes = stylex.defineVars({
174
174
  bodyMd: '14px',
175
175
  bodyBase: '15px',
176
176
  bodyLg: '18px',
177
+ bodyCompact: '15px',
177
178
  heading6: '20px',
178
179
  heading5: '24px',
179
180
  heading4: '28px',
@@ -194,6 +195,7 @@ export const lineHeights = stylex.defineVars({
194
195
  bodySm: '20px',
195
196
  body: '24px',
196
197
  bodyLg: '28px',
198
+ bodyCompact: '24px',
197
199
  heading6: '24px',
198
200
  heading5: '32px',
199
201
  heading4: '36px',
@@ -203,6 +205,7 @@ export const lineHeights = stylex.defineVars({
203
205
  });
204
206
  export const fonts = stylex.defineVars({
205
207
  sans: '"Inter Variable", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
208
+ heading: '"Inter Variable", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
206
209
  mono: 'ui-monospace, SFMono-Regular, Menlo, monospace'
207
210
  });
208
211
  export const fontFeatures = stylex.defineVars({
@@ -214,7 +217,9 @@ export const fontWeights = stylex.defineVars({
214
217
  semimedium: '450',
215
218
  medium: '500',
216
219
  semibold: '600',
217
- bold: '700'
220
+ bold: '700',
221
+ heavy: '800',
222
+ heading: '500'
218
223
  });
219
224
  export const shadows = stylex.defineVars({
220
225
  border: '0 0 0 1px light-dark(#00000012, #ffffff12)',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trypattern/sdk",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",