@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 +2 -0
- package/dist/app.js +5 -2
- package/dist/builtins/Device.d.ts +15 -0
- package/dist/builtins/Device.js +52 -0
- package/dist/components/Text.d.ts +2 -1
- package/dist/components/Text.js +23 -11
- package/dist/registered.d.ts +2 -1
- package/dist/rules/checkAllFiles.js +1 -43
- package/dist/rules/messages.d.ts +2 -2
- package/dist/rules/messages.js +2 -2
- package/dist/start.d.ts +2 -2
- package/dist/start.js +1 -1
- package/dist/store.d.ts +2 -1
- package/dist/theme/tokens.stylex.d.ts +5 -0
- package/dist/theme/tokens.stylex.js +6 -1
- package/package.json +1 -1
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<{
|
package/dist/components/Text.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
94
|
+
[breakpoints.compact]: fontSizes.bodyCompact
|
|
90
95
|
},
|
|
91
96
|
fontWeight: fontWeights.medium,
|
|
92
97
|
lineHeight: {
|
|
93
98
|
default: lineHeights.bodySm,
|
|
94
|
-
[breakpoints.compact]: lineHeights.
|
|
99
|
+
[breakpoints.compact]: lineHeights.bodyCompact
|
|
95
100
|
}
|
|
96
101
|
},
|
|
97
102
|
body: {
|
|
98
103
|
fontSize: {
|
|
99
104
|
default: fontSizes.bodySm,
|
|
100
|
-
[breakpoints.compact]: fontSizes.
|
|
105
|
+
[breakpoints.compact]: fontSizes.bodyCompact
|
|
101
106
|
},
|
|
102
107
|
fontWeight: fontWeights.normal,
|
|
103
108
|
lineHeight: {
|
|
104
109
|
default: lineHeights.bodySm,
|
|
105
|
-
[breakpoints.compact]: lineHeights.
|
|
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' },
|
package/dist/registered.d.ts
CHANGED
|
@@ -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: <
|
|
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
|
-
|
|
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() {
|
package/dist/rules/messages.d.ts
CHANGED
|
@@ -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' } })`.
|
|
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.";
|
package/dist/rules/messages.js
CHANGED
|
@@ -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' } })`.
|
|
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
|
|
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)',
|