@trypattern/sdk 0.0.0-stage → 0.1.0

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.
Files changed (247) hide show
  1. package/README.md +127 -2
  2. package/dist/access.d.ts +31 -0
  3. package/dist/access.js +72 -0
  4. package/dist/actions.d.ts +57 -0
  5. package/dist/actions.js +198 -0
  6. package/dist/api.d.ts +20 -0
  7. package/dist/api.js +34 -0
  8. package/dist/app.d.ts +106 -0
  9. package/dist/app.js +224 -0
  10. package/dist/appearance.d.ts +7 -0
  11. package/dist/appearance.js +2 -0
  12. package/dist/auth/sessionApi.d.ts +41 -0
  13. package/dist/auth/sessionApi.js +158 -0
  14. package/dist/auth/sessionStore.d.ts +20 -0
  15. package/dist/auth/sessionStore.js +40 -0
  16. package/dist/auth/user.d.ts +11 -0
  17. package/dist/auth/user.js +9 -0
  18. package/dist/builtins/Account.d.ts +25 -0
  19. package/dist/builtins/Account.js +15 -0
  20. package/dist/builtins/Palette.d.ts +29 -0
  21. package/dist/builtins/Palette.js +63 -0
  22. package/dist/builtins/SignIn.d.ts +28 -0
  23. package/dist/builtins/SignIn.js +60 -0
  24. package/dist/builtins/Toasts.d.ts +32 -0
  25. package/dist/builtins/Toasts.js +24 -0
  26. package/dist/builtins/actions.d.ts +2 -0
  27. package/dist/builtins/actions.js +45 -0
  28. package/dist/cache.d.ts +24 -0
  29. package/dist/cache.js +112 -0
  30. package/dist/cli/app.d.ts +2 -0
  31. package/dist/cli/app.js +1329 -0
  32. package/dist/cli/screenshot.d.ts +1 -0
  33. package/dist/cli/screenshot.js +96 -0
  34. package/dist/cli/tools.d.ts +12 -0
  35. package/dist/cli/tools.js +35 -0
  36. package/dist/components/AppFrame.d.ts +14 -0
  37. package/dist/components/AppFrame.js +64 -0
  38. package/dist/components/AppearancePicker.d.ts +1 -0
  39. package/dist/components/AppearancePicker.js +12 -0
  40. package/dist/components/Avatar.d.ts +9 -0
  41. package/dist/components/Avatar.js +63 -0
  42. package/dist/components/Badge.d.ts +11 -0
  43. package/dist/components/Badge.js +87 -0
  44. package/dist/components/Button.d.ts +23 -0
  45. package/dist/components/Button.js +249 -0
  46. package/dist/components/Card.d.ts +12 -0
  47. package/dist/components/Card.js +38 -0
  48. package/dist/components/CommandMenu.d.ts +23 -0
  49. package/dist/components/CommandMenu.js +214 -0
  50. package/dist/components/CommandPalette.d.ts +1 -0
  51. package/dist/components/CommandPalette.js +7 -0
  52. package/dist/components/Content.d.ts +7 -0
  53. package/dist/components/Content.js +24 -0
  54. package/dist/components/ContextMenu.d.ts +8 -0
  55. package/dist/components/ContextMenu.js +23 -0
  56. package/dist/components/Dialog.d.ts +11 -0
  57. package/dist/components/Dialog.js +81 -0
  58. package/dist/components/Divider.d.ts +6 -0
  59. package/dist/components/Divider.js +15 -0
  60. package/dist/components/EmptyState.d.ts +8 -0
  61. package/dist/components/EmptyState.js +24 -0
  62. package/dist/components/Form.d.ts +9 -0
  63. package/dist/components/Form.js +23 -0
  64. package/dist/components/Icon.d.ts +10 -0
  65. package/dist/components/Icon.js +22 -0
  66. package/dist/components/IconButton.d.ts +14 -0
  67. package/dist/components/IconButton.js +53 -0
  68. package/dist/components/Kbd.d.ts +6 -0
  69. package/dist/components/Kbd.js +67 -0
  70. package/dist/components/Link.d.ts +9 -0
  71. package/dist/components/Link.js +35 -0
  72. package/dist/components/ListRow.d.ts +12 -0
  73. package/dist/components/ListRow.js +62 -0
  74. package/dist/components/LoadingRows.d.ts +5 -0
  75. package/dist/components/LoadingRows.js +6 -0
  76. package/dist/components/Menu.d.ts +12 -0
  77. package/dist/components/Menu.js +33 -0
  78. package/dist/components/MenuItems.d.ts +21 -0
  79. package/dist/components/MenuItems.js +32 -0
  80. package/dist/components/Placement.d.ts +21 -0
  81. package/dist/components/Placement.js +1 -0
  82. package/dist/components/Popup.d.ts +8 -0
  83. package/dist/components/Popup.js +43 -0
  84. package/dist/components/PopupItem.d.ts +8 -0
  85. package/dist/components/PopupItem.js +51 -0
  86. package/dist/components/PropertyRow.d.ts +7 -0
  87. package/dist/components/PropertyRow.js +23 -0
  88. package/dist/components/Row.d.ts +16 -0
  89. package/dist/components/Row.js +27 -0
  90. package/dist/components/Screen.d.ts +13 -0
  91. package/dist/components/Screen.js +56 -0
  92. package/dist/components/Scrim.d.ts +8 -0
  93. package/dist/components/Scrim.js +40 -0
  94. package/dist/components/ScrollArea.d.ts +15 -0
  95. package/dist/components/ScrollArea.js +13 -0
  96. package/dist/components/SegmentedControl.d.ts +17 -0
  97. package/dist/components/SegmentedControl.js +68 -0
  98. package/dist/components/Select.d.ts +17 -0
  99. package/dist/components/Select.js +107 -0
  100. package/dist/components/SettingsRow.d.ts +8 -0
  101. package/dist/components/SettingsRow.js +32 -0
  102. package/dist/components/Sidebar.d.ts +20 -0
  103. package/dist/components/Sidebar.js +138 -0
  104. package/dist/components/SignInScreen.d.ts +1 -0
  105. package/dist/components/SignInScreen.js +40 -0
  106. package/dist/components/Size.d.ts +1 -0
  107. package/dist/components/Size.js +1 -0
  108. package/dist/components/Skeleton.d.ts +8 -0
  109. package/dist/components/Skeleton.js +48 -0
  110. package/dist/components/Spinner.d.ts +6 -0
  111. package/dist/components/Spinner.js +54 -0
  112. package/dist/components/Stack.d.ts +14 -0
  113. package/dist/components/Stack.js +19 -0
  114. package/dist/components/Switch.d.ts +10 -0
  115. package/dist/components/Switch.js +71 -0
  116. package/dist/components/Text.d.ts +21 -0
  117. package/dist/components/Text.js +77 -0
  118. package/dist/components/TextField.d.ts +25 -0
  119. package/dist/components/TextField.js +68 -0
  120. package/dist/components/Toaster.d.ts +1 -0
  121. package/dist/components/Toaster.js +83 -0
  122. package/dist/components/Tooltip.d.ts +8 -0
  123. package/dist/components/Tooltip.js +43 -0
  124. package/dist/components/controlLook.d.ts +60 -0
  125. package/dist/components/controlLook.js +67 -0
  126. package/dist/components/frame.d.ts +4 -0
  127. package/dist/components/frame.js +2 -0
  128. package/dist/components/icons.d.ts +58 -0
  129. package/dist/components/icons.js +105 -0
  130. package/dist/components/interaction.d.ts +12 -0
  131. package/dist/components/interaction.js +22 -0
  132. package/dist/components/spacing.d.ts +118 -0
  133. package/dist/components/spacing.js +46 -0
  134. package/dist/components/tones.d.ts +28 -0
  135. package/dist/components/tones.js +12 -0
  136. package/dist/entityStore.d.ts +46 -0
  137. package/dist/entityStore.js +160 -0
  138. package/dist/flows/accessFlows.d.ts +9 -0
  139. package/dist/flows/accessFlows.js +151 -0
  140. package/dist/flows/browser.d.ts +32 -0
  141. package/dist/flows/browser.js +164 -0
  142. package/dist/flows/contactSheet.d.ts +1 -0
  143. package/dist/flows/contactSheet.js +63 -0
  144. package/dist/flows/flowBrowser.d.ts +23 -0
  145. package/dist/flows/flowBrowser.js +21 -0
  146. package/dist/flows/flowSetup.d.ts +3 -0
  147. package/dist/flows/flowSetup.js +35 -0
  148. package/dist/flows/flows.d.ts +41 -0
  149. package/dist/flows/flows.js +148 -0
  150. package/dist/flows/screenshotName.d.ts +1 -0
  151. package/dist/flows/screenshotName.js +6 -0
  152. package/dist/gate.d.ts +8 -0
  153. package/dist/gate.js +52 -0
  154. package/dist/hooks/appContext.d.ts +14 -0
  155. package/dist/hooks/appContext.js +5 -0
  156. package/dist/hooks/appHooks.d.ts +21 -0
  157. package/dist/hooks/appHooks.js +24 -0
  158. package/dist/hooks/useAnimatesOnChange.d.ts +1 -0
  159. package/dist/hooks/useAnimatesOnChange.js +14 -0
  160. package/dist/hooks/useStoreState.d.ts +6 -0
  161. package/dist/hooks/useStoreState.js +5 -0
  162. package/dist/index.d.ts +40 -0
  163. package/dist/index.js +19 -0
  164. package/dist/memoryApi.d.ts +19 -0
  165. package/dist/memoryApi.js +42 -0
  166. package/dist/model.d.ts +94 -0
  167. package/dist/model.js +243 -0
  168. package/dist/page.d.ts +11 -0
  169. package/dist/page.js +9 -0
  170. package/dist/platform.d.ts +1 -0
  171. package/dist/platform.js +5 -0
  172. package/dist/react.d.ts +46 -0
  173. package/dist/react.js +38 -0
  174. package/dist/registered.d.ts +54 -0
  175. package/dist/registered.js +17 -0
  176. package/dist/routes.d.ts +38 -0
  177. package/dist/routes.js +21 -0
  178. package/dist/rules/checkAllFiles.d.ts +1 -0
  179. package/dist/rules/checkAllFiles.js +741 -0
  180. package/dist/rules/checkApiRouting.d.ts +1 -0
  181. package/dist/rules/checkApiRouting.js +112 -0
  182. package/dist/rules/lintPlugin.d.ts +252 -0
  183. package/dist/rules/lintPlugin.js +1157 -0
  184. package/dist/rules/messages.d.ts +48 -0
  185. package/dist/rules/messages.js +48 -0
  186. package/dist/rules/themeTokens.d.ts +6 -0
  187. package/dist/rules/themeTokens.js +64 -0
  188. package/dist/schema.d.ts +2 -0
  189. package/dist/schema.js +1 -0
  190. package/dist/server/auth.d.ts +115 -0
  191. package/dist/server/auth.js +131 -0
  192. package/dist/server/authService.d.ts +9 -0
  193. package/dist/server/authService.js +50 -0
  194. package/dist/server/createServer.d.ts +357 -0
  195. package/dist/server/createServer.js +62 -0
  196. package/dist/server/env.d.ts +15 -0
  197. package/dist/server/env.js +1 -0
  198. package/dist/server/index.d.ts +8 -0
  199. package/dist/server/index.js +5 -0
  200. package/dist/server/session.d.ts +28 -0
  201. package/dist/server/session.js +54 -0
  202. package/dist/server/sessions.d.ts +92 -0
  203. package/dist/server/sessions.js +47 -0
  204. package/dist/server/signInWire.d.ts +52 -0
  205. package/dist/server/signInWire.js +22 -0
  206. package/dist/server/tables.d.ts +242 -0
  207. package/dist/server/tables.js +246 -0
  208. package/dist/server/users.d.ts +189 -0
  209. package/dist/server/users.js +49 -0
  210. package/dist/shortcuts.d.ts +3 -0
  211. package/dist/shortcuts.js +40 -0
  212. package/dist/start.d.ts +20 -0
  213. package/dist/start.js +121 -0
  214. package/dist/state.d.ts +14 -0
  215. package/dist/state.js +17 -0
  216. package/dist/store.d.ts +43 -0
  217. package/dist/store.js +198 -0
  218. package/dist/subscribers.d.ts +5 -0
  219. package/dist/subscribers.js +13 -0
  220. package/dist/table.d.ts +143 -0
  221. package/dist/table.js +91 -0
  222. package/dist/tables.d.ts +77 -0
  223. package/dist/tables.js +216 -0
  224. package/dist/theme/reset.css +84 -0
  225. package/dist/theme/scales.d.ts +76 -0
  226. package/dist/theme/scales.js +82 -0
  227. package/dist/theme/tokens.stylex.d.ts +218 -0
  228. package/dist/theme/tokens.stylex.js +233 -0
  229. package/dist/vite/commandBridge.d.ts +2 -0
  230. package/dist/vite/commandBridge.js +46 -0
  231. package/dist/vite/designManifest.d.ts +25 -0
  232. package/dist/vite/designManifest.js +191 -0
  233. package/dist/vite/localWorker.d.ts +4 -0
  234. package/dist/vite/localWorker.js +59 -0
  235. package/dist/vite/rootAlias.d.ts +2 -0
  236. package/dist/vite/rootAlias.js +15 -0
  237. package/dist/vite/vite.d.ts +7 -0
  238. package/dist/vite/vite.js +88 -0
  239. package/dist/wire.d.ts +7 -0
  240. package/dist/wire.js +1 -0
  241. package/package.json +99 -3
  242. package/presets/lintPlugin.js +7 -0
  243. package/presets/oxlint.json +167 -0
  244. package/presets/register.d.ts +19 -0
  245. package/presets/server.d.ts +2 -0
  246. package/presets/tsconfig.app.json +29 -0
  247. package/presets/tsconfig.server.json +27 -0
@@ -0,0 +1,23 @@
1
+ import type { ViewportName } from './browser.ts';
2
+ export type VisualFlowSettings = {
3
+ url: string;
4
+ wsEndpoint: string;
5
+ directory: string;
6
+ viewports: readonly ViewportName[];
7
+ };
8
+ declare module 'vitest' {
9
+ interface ProvidedContext {
10
+ patternVisualFlows?: VisualFlowSettings;
11
+ }
12
+ }
13
+ export declare function closeFlowBrowser(): Promise<void>;
14
+ export declare function openFlowPage(settings: VisualFlowSettings, viewport: ViewportName, startAt: string, signedInAs: string | null, seed: object, admins: readonly string[]): Promise<{
15
+ send: (command: unknown) => Promise<void>;
16
+ snapshot: () => Promise<unknown>;
17
+ hold: (call: string) => Promise<void>;
18
+ release: (call: string) => Promise<void>;
19
+ reload: (path: string) => Promise<void>;
20
+ screenshot: (name: string) => Promise<void>;
21
+ problems: string[];
22
+ close: () => Promise<void>;
23
+ }>;
@@ -0,0 +1,21 @@
1
+ import { connectChrome, openApp } from "./browser.js";
2
+ let browser;
3
+ export async function closeFlowBrowser() {
4
+ const connected = browser;
5
+ browser = undefined;
6
+ await (await connected)?.close();
7
+ }
8
+ export async function openFlowPage(settings, viewport, startAt, signedInAs, seed, admins) {
9
+ browser ??= connectChrome(settings.wsEndpoint);
10
+ const app = await openApp(await browser, settings.url, startAt, viewport, signedInAs, seed, admins);
11
+ return {
12
+ send: app.dispatch,
13
+ snapshot: app.snapshot,
14
+ hold: app.hold,
15
+ release: app.release,
16
+ reload: app.reload,
17
+ screenshot: (name) => app.screenshot(`${settings.directory}/${name}-${viewport}.png`),
18
+ problems: app.problems,
19
+ close: app.close
20
+ };
21
+ }
@@ -0,0 +1,3 @@
1
+ import type { TestProject } from 'vitest/node';
2
+ export declare function setup(project: TestProject): Promise<void>;
3
+ export declare function teardown(): Promise<void>;
@@ -0,0 +1,35 @@
1
+ import { mkdirSync, rmSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { launchChromeServer, viewportNames } from "./browser.js";
4
+ import { writeContactSheet } from "./contactSheet.js";
5
+ const ROOT = process.cwd();
6
+ const SCREENSHOTS_DIRECTORY = join(ROOT, '.dev', 'screenshots');
7
+ const FLOW_SCREENSHOTS_DIRECTORY = join(SCREENSHOTS_DIRECTORY, 'flows');
8
+ const CONTACT_SHEET = join(SCREENSHOTS_DIRECTORY, 'contact-sheet.png');
9
+ let server;
10
+ export async function setup(project) {
11
+ const url = process.env['PATTERN_VISUAL_URL'];
12
+ if (url === undefined) {
13
+ throw new Error('Visual flows need the dev server: run `app test --visual`');
14
+ }
15
+ rmSync(FLOW_SCREENSHOTS_DIRECTORY, { recursive: true, force: true });
16
+ rmSync(CONTACT_SHEET, { force: true });
17
+ mkdirSync(FLOW_SCREENSHOTS_DIRECTORY, { recursive: true });
18
+ server = await launchChromeServer();
19
+ project.provide('patternVisualFlows', {
20
+ url,
21
+ wsEndpoint: server.wsEndpoint(),
22
+ directory: FLOW_SCREENSHOTS_DIRECTORY,
23
+ viewports: viewportNames
24
+ });
25
+ }
26
+ export async function teardown() {
27
+ if (server === undefined)
28
+ return;
29
+ try {
30
+ await writeContactSheet(server.wsEndpoint(), FLOW_SCREENSHOTS_DIRECTORY, CONTACT_SHEET);
31
+ }
32
+ finally {
33
+ await server.close();
34
+ }
35
+ }
@@ -0,0 +1,41 @@
1
+ import { expect } from 'vitest';
2
+ import type { ApiCall } from '../api.ts';
3
+ import type { AppClass, RunningApp } from '../app.ts';
4
+ import type { Seed, Tables } from '../table.ts';
5
+ export { expect };
6
+ export { accessFlows } from './accessFlows.ts';
7
+ declare module 'vitest' {
8
+ interface ProvidedContext {
9
+ patternSeed?: Seed<Tables>;
10
+ patternAdmins?: readonly string[];
11
+ }
12
+ }
13
+ type Expected<Value> = Value extends readonly (infer Item)[] ? readonly Expected<Item>[] : Value extends object ? {
14
+ [Key in keyof Value]?: Expected<Value[Key]>;
15
+ } : Value;
16
+ export type FlowStep<Command, Snapshot, Call extends string> = {
17
+ send: Command;
18
+ } | {
19
+ expect: Expected<Snapshot> & object;
20
+ } | {
21
+ screenshot: string;
22
+ } | {
23
+ hold: Call;
24
+ } | {
25
+ release: Call;
26
+ } | {
27
+ restart: true;
28
+ };
29
+ export type FlowOptions<Call extends string> = {
30
+ startAt?: string;
31
+ failing?: readonly Call[];
32
+ signedInAs?: string | null;
33
+ };
34
+ export type FlowBinding<Group extends Tables, Command, Snapshot extends {
35
+ readonly path: string;
36
+ }> = {
37
+ App: AppClass<Group, RunningApp<Command, Snapshot>>;
38
+ };
39
+ export declare function defineFlow<Group extends Tables, Command, Snapshot extends {
40
+ readonly path: string;
41
+ }>({ App }: FlowBinding<Group, Command, Snapshot>): (name: string, steps: readonly FlowStep<Command, Snapshot, ApiCall<Group>>[], { startAt, failing, signedInAs }?: FlowOptions<ApiCall<Group>>) => void;
@@ -0,0 +1,148 @@
1
+ import { createMemoryRouter } from 'react-router';
2
+ import { afterAll, expect, inject, test } from 'vitest';
3
+ import { apiCalls, memoryApi } from "../api.js";
4
+ import { Cache } from "../cache.js";
5
+ import { Gate } from "../gate.js";
6
+ import { routeObjects } from "../routes.js";
7
+ import { screenshotNameProblem } from "./screenshotName.js";
8
+ export { expect };
9
+ export { accessFlows } from "./accessFlows.js";
10
+ const DEFAULT_USER = 'ada@acme.test';
11
+ const screenshotNamesInThisFile = new Set();
12
+ let closesBrowserAfterThisFile = false;
13
+ function checkSteps(steps, calls) {
14
+ for (const step of steps) {
15
+ for (const key of ['hold', 'release']) {
16
+ const call = Reflect.get(step, key);
17
+ if (typeof call === 'string' && !calls.includes(call)) {
18
+ throw new Error(`{ ${key}: '${call}' } names no server call. Calls: ${calls.join(', ')}`);
19
+ }
20
+ }
21
+ const name = Reflect.get(step, 'screenshot');
22
+ if (typeof name !== 'string')
23
+ continue;
24
+ const problem = screenshotNameProblem(name);
25
+ if (problem !== undefined)
26
+ throw new Error(problem);
27
+ if (screenshotNamesInThisFile.has(name)) {
28
+ throw new Error(`Screenshot name "${name}" is used twice; each screenshot step needs its own name.`);
29
+ }
30
+ screenshotNamesInThisFile.add(name);
31
+ }
32
+ }
33
+ async function runSteps(name, steps, run) {
34
+ for (const [index, step] of steps.entries()) {
35
+ if ('send' in step) {
36
+ await run.send(step.send);
37
+ }
38
+ else if ('screenshot' in step) {
39
+ await run.screenshot(step.screenshot);
40
+ }
41
+ else if ('hold' in step) {
42
+ await run.hold(step.hold);
43
+ }
44
+ else if ('release' in step) {
45
+ await run.release(step.release);
46
+ }
47
+ else if ('restart' in step) {
48
+ await run.restart();
49
+ }
50
+ else {
51
+ expect(await run.snapshot(), `step ${index + 1} of "${name}": expected ${JSON.stringify(step.expect)}`).toMatchObject(step.expect);
52
+ }
53
+ }
54
+ }
55
+ function withTestParams(path, failing, held) {
56
+ const params = new URLSearchParams([
57
+ ['memory', ''],
58
+ ...failing.map((call) => ['fail', call]),
59
+ ...[...held].map((call) => ['hold', call])
60
+ ]);
61
+ return `${path}?${params.toString()}`;
62
+ }
63
+ export function defineFlow({ App }) {
64
+ const { tables, routes } = App.definition;
65
+ const seed = (inject('patternSeed') ?? {});
66
+ const admins = inject('patternAdmins') ?? [];
67
+ const calls = apiCalls(tables);
68
+ const router = (path) => createMemoryRouter(routeObjects(routes), { initialEntries: [path] });
69
+ function headless(name, steps, startAt, failing, signedInAs) {
70
+ test(name, async () => {
71
+ const gate = new Gate(failing);
72
+ const deps = {
73
+ api: memoryApi(tables, seed, { gate, signedInAs, admins }),
74
+ cache: Cache.memory(),
75
+ gate
76
+ };
77
+ let app = new App(router(startAt), deps);
78
+ await runSteps(name, steps, {
79
+ send: async (command) => void app.send(command),
80
+ snapshot: async () => {
81
+ await app.whenIdle();
82
+ return app.getSnapshot();
83
+ },
84
+ screenshot: async () => undefined,
85
+ hold: async (call) => gate.hold(call),
86
+ release: async (call) => gate.release(call),
87
+ restart: async () => {
88
+ app = new App(router(app.getSnapshot().path), deps);
89
+ }
90
+ });
91
+ await app.whenIdle();
92
+ });
93
+ }
94
+ return function flow(name, steps, { startAt = '/', failing = [], signedInAs = DEFAULT_USER } = {}) {
95
+ checkSteps(steps, calls);
96
+ const visual = inject('patternVisualFlows');
97
+ if (visual === undefined) {
98
+ headless(name, steps, startAt, failing, signedInAs);
99
+ return;
100
+ }
101
+ if (!closesBrowserAfterThisFile) {
102
+ closesBrowserAfterThisFile = true;
103
+ afterAll(async () => {
104
+ const { closeFlowBrowser } = await import("./flowBrowser.js");
105
+ await closeFlowBrowser();
106
+ });
107
+ }
108
+ for (const viewport of visual.viewports) {
109
+ test(`${name} [${viewport}]`, async () => {
110
+ const { openFlowPage } = await import("./flowBrowser.js");
111
+ const held = new Set();
112
+ const page = await openFlowPage(visual, viewport, withTestParams(startAt, failing, held), signedInAs, seed, admins);
113
+ let stepFailure;
114
+ try {
115
+ await runSteps(name, steps, {
116
+ send: page.send,
117
+ snapshot: page.snapshot,
118
+ screenshot: page.screenshot,
119
+ hold: async (call) => {
120
+ held.add(call);
121
+ await page.hold(call);
122
+ },
123
+ release: async (call) => {
124
+ held.delete(call);
125
+ await page.release(call);
126
+ },
127
+ restart: async () => {
128
+ const snapshot = await page.snapshot();
129
+ const path = typeof snapshot === 'object' && snapshot !== null
130
+ ? Reflect.get(snapshot, 'path')
131
+ : undefined;
132
+ await page.reload(withTestParams(typeof path === 'string' ? path : startAt, failing, held));
133
+ }
134
+ });
135
+ }
136
+ catch (error) {
137
+ stepFailure = error;
138
+ }
139
+ finally {
140
+ await page.close();
141
+ }
142
+ expect(page.problems, `"${name}" [${viewport}] logged errors in the browser; fix them`).toEqual([]);
143
+ if (stepFailure !== undefined)
144
+ throw stepFailure;
145
+ });
146
+ }
147
+ };
148
+ }
@@ -0,0 +1 @@
1
+ export declare function screenshotNameProblem(name: string): string | undefined;
@@ -0,0 +1,6 @@
1
+ const KEBAB_CASE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
2
+ export function screenshotNameProblem(name) {
3
+ return KEBAB_CASE.test(name)
4
+ ? undefined
5
+ : `Screenshot name "${name}" must be kebab-case (lowercase letters, digits and dashes), like "settings" or "sidebar-drawer": it becomes the PNG file name.`;
6
+ }
package/dist/gate.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export declare class Gate {
2
+ #private;
3
+ constructor(failing?: readonly string[]);
4
+ pass<Value>(call: string, run: () => Promise<Value>): Promise<Value>;
5
+ hold(call: string): void;
6
+ release(call: string): void;
7
+ whenIdle(): Promise<void>;
8
+ }
package/dist/gate.js ADDED
@@ -0,0 +1,52 @@
1
+ function nextMacrotask() {
2
+ return new Promise((done) => setTimeout(done, 0));
3
+ }
4
+ export class Gate {
5
+ #failing;
6
+ #holds = new Map();
7
+ #active = 0;
8
+ #waiting = [];
9
+ constructor(failing = []) {
10
+ this.#failing = failing;
11
+ }
12
+ async pass(call, run) {
13
+ if (this.#failing.includes(call))
14
+ throw new Error(`${call} failed`);
15
+ this.#active += 1;
16
+ const value = await run().finally(() => {
17
+ this.#active -= 1;
18
+ this.#wakeIfIdle();
19
+ });
20
+ await this.#holds.get(call)?.opened;
21
+ return value;
22
+ }
23
+ hold(call) {
24
+ if (this.#holds.has(call))
25
+ return;
26
+ let open = () => { };
27
+ const opened = new Promise((resolve) => {
28
+ open = resolve;
29
+ });
30
+ this.#holds.set(call, { opened, open });
31
+ }
32
+ release(call) {
33
+ this.#holds.get(call)?.open();
34
+ this.#holds.delete(call);
35
+ }
36
+ async whenIdle() {
37
+ do {
38
+ if (this.#active > 0) {
39
+ await new Promise((wake) => this.#waiting.push(wake));
40
+ }
41
+ await nextMacrotask();
42
+ } while (this.#active > 0);
43
+ }
44
+ #wakeIfIdle() {
45
+ if (this.#active > 0)
46
+ return;
47
+ const woken = this.#waiting;
48
+ this.#waiting = [];
49
+ for (const wake of woken)
50
+ wake();
51
+ }
52
+ }
@@ -0,0 +1,14 @@
1
+ import type { Toasts } from '../builtins/Toasts.ts';
2
+ import type { AppModels } from '../registered.ts';
3
+ import type { AppProviderOf, UseModel } from './appHooks.ts';
4
+ export type ReactApp = {
5
+ readonly name: string;
6
+ readonly models: AppModels;
7
+ readonly stores: {
8
+ readonly toasts: Toasts;
9
+ };
10
+ pressKey(key: string): Promise<void> | undefined;
11
+ };
12
+ export declare const AppProvider: AppProviderOf<ReactApp>;
13
+ export declare const useApp: () => ReactApp;
14
+ export declare const useModel: UseModel<AppModels>;
@@ -0,0 +1,5 @@
1
+ import { createAppHooks } from "./appHooks.js";
2
+ const hooks = createAppHooks();
3
+ export const AppProvider = hooks.AppProvider;
4
+ export const useApp = hooks.useApp;
5
+ export const useModel = hooks.useModel;
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { PublishedOf } from '../model.ts';
3
+ type PublishingModel = {
4
+ readonly subscribe: (listener: () => void) => () => void;
5
+ readonly published: object;
6
+ };
7
+ type AppLike = {
8
+ readonly models: Readonly<Record<string, PublishingModel>>;
9
+ pressKey(key: string): Promise<void> | undefined;
10
+ };
11
+ export type UseModel<Models> = <Name extends keyof Models & string>(name: Name) => PublishedOf<Models[Name]>;
12
+ export type AppProviderOf<App> = (props: {
13
+ app: App;
14
+ children: ReactNode;
15
+ }) => ReactNode;
16
+ export declare function createAppHooks<App extends AppLike>(): {
17
+ AppProvider: AppProviderOf<App>;
18
+ useApp: () => App;
19
+ useModel: UseModel<App['models']>;
20
+ };
21
+ export {};
@@ -0,0 +1,24 @@
1
+ import { createContext, createElement, use, useSyncExternalStore } from 'react';
2
+ import { useShortcuts } from "../shortcuts.js";
3
+ export function createAppHooks() {
4
+ const AppContext = createContext(null);
5
+ function useApp() {
6
+ const app = use(AppContext);
7
+ if (app === null) {
8
+ throw new Error('Pattern hooks must be used inside <AppProvider>');
9
+ }
10
+ return app;
11
+ }
12
+ function useModel(name) {
13
+ const model = useApp().models[name];
14
+ if (model === undefined)
15
+ throw new Error(`There is no model named ${name}`);
16
+ const read = () => model.published;
17
+ return useSyncExternalStore(model.subscribe, read, read);
18
+ }
19
+ function AppProvider({ app, children }) {
20
+ useShortcuts((key) => app.pressKey(key));
21
+ return createElement(AppContext, { value: app }, children);
22
+ }
23
+ return { AppProvider, useApp, useModel };
24
+ }
@@ -0,0 +1 @@
1
+ export declare function useAnimatesOnChange(value: unknown, durationMs: number): boolean;
@@ -0,0 +1,14 @@
1
+ import { useEffect, useState } from 'react';
2
+ export function useAnimatesOnChange(value, durationMs) {
3
+ const [tracked, setTracked] = useState({ value, isAnimating: false });
4
+ if (tracked.value !== value) {
5
+ setTracked({ value, isAnimating: true });
6
+ }
7
+ useEffect(() => {
8
+ if (!tracked.isAnimating)
9
+ return undefined;
10
+ const timer = setTimeout(() => setTracked((current) => ({ ...current, isAnimating: false })), durationMs);
11
+ return () => clearTimeout(timer);
12
+ }, [tracked, durationMs]);
13
+ return tracked.isAnimating || tracked.value !== value;
14
+ }
@@ -0,0 +1,6 @@
1
+ type ObservedStore<State> = {
2
+ readonly currentState: State;
3
+ readonly subscribe: (listener: () => void) => () => void;
4
+ };
5
+ export declare function useStoreState<State>(store: ObservedStore<State>): State;
6
+ export {};
@@ -0,0 +1,5 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ export function useStoreState(store) {
3
+ const read = () => store.currentState;
4
+ return useSyncExternalStore(store.subscribe, read, read);
5
+ }
@@ -0,0 +1,40 @@
1
+ export { defineApp } from './app.ts';
2
+ export { ActionsStore, bindActions, rankActions } from './actions.ts';
3
+ export type { Action, ActionsBuilder } from './actions.ts';
4
+ export { startApp } from './start.ts';
5
+ export type { StartOptions } from './start.ts';
6
+ export { appearance } from './appearance.ts';
7
+ export type { Appearance } from './appearance.ts';
8
+ export { defineIcons } from './components/icons.ts';
9
+ export type { IconDrawing } from './components/icons.ts';
10
+ export type { Interaction } from './components/interaction.ts';
11
+ export { apiCalls, browserApi, httpApi, memoryApi } from './api.ts';
12
+ export type { ApiCall, AppApi } from './api.ts';
13
+ export { SessionStore } from './auth/sessionStore.ts';
14
+ export type { SessionState } from './auth/sessionStore.ts';
15
+ export { HttpSessionApi, MemorySessionApi } from './auth/sessionApi.ts';
16
+ export type { EmailCodeOutcome, SessionApi, SessionCall } from './auth/sessionApi.ts';
17
+ export { userSchema } from './auth/user.ts';
18
+ export type { User } from './auth/user.ts';
19
+ export type { AppClass, AppDefinition, AppDependencies, AppSnapshotShape, DataRouter, ModelServicesOf, NavigateCommand, PressKeyCommand, PublicStores, PlainJsonModel, RunningApp, StoreClasses, StoreInstances, StoresOf, TableStores } from './app.ts';
20
+ export { defineRoutes } from './routes.ts';
21
+ export type { PageOf, PathOf, RouteDefinition } from './routes.ts';
22
+ export { Model, action, appPath, defineActions } from './registered.ts';
23
+ export type { ActionContext, AppAction, AppActionGroup, AppCommand, AppModels, AppPage, AppPath, AppSnapshot, AppStores, ModelServices, Register } from './registered.ts';
24
+ export { Cache, Cached } from './cache.ts';
25
+ export type { CacheStorage } from './cache.ts';
26
+ export { EntityStore, WriteRefused, entityCalls } from './entityStore.ts';
27
+ export type { Entity, EntityApi, EntityCall, EntityDeps, EntitySnapshot, PendingWrite } from './entityStore.ts';
28
+ export { Gate } from './gate.ts';
29
+ export { MemoryEntityApi, MemoryServer } from './memoryApi.ts';
30
+ export type { MemoryApiOptions } from './memoryApi.ts';
31
+ export { Store, StoreBase, keepEqualParts, snapshotsOf } from './store.ts';
32
+ export type { Snapshots, StoreClass, StoreConfig } from './store.ts';
33
+ export type { Json, Persisted, StateOf, StateSpec } from './state.ts';
34
+ export { defineTables } from './table.ts';
35
+ export type { Access, Columns, Fields, Level, PublicRow, Row, Seed, Table, TableDefinition, TableOf, Tables, Value, Writes } from './table.ts';
36
+ export { HttpTableApi, MemoryTableApi, PublicTableStore, TableStore, httpTables, memoryTables, publicStoreName, tableCalls } from './tables.ts';
37
+ export type { PublicSnapshot, TableApi, TableCall, TablesApi } from './tables.ts';
38
+ export type { ReadScope, Viewer, WriteOutcome } from './access.ts';
39
+ export { ModelBase, command, commandsOf, createModels, modelSnapshotsOf, parseMessage, sendMessage, splitType } from './model.ts';
40
+ export type { Command, CommandKeys, CommandOptions, CommandResult, InputOf, Instances, Message, MessagesOf, ModelSnapshots, PublishedCommands, PublishedOf, SnapshotOf, DerivedOf, RouteParams, ModelClass, ModelClassLike, ModelConfig } from './model.ts';
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ export { defineApp } from "./app.js";
2
+ export { ActionsStore, bindActions, rankActions } from "./actions.js";
3
+ export { startApp } from "./start.js";
4
+ export { appearance } from "./appearance.js";
5
+ export { defineIcons } from "./components/icons.js";
6
+ export { apiCalls, browserApi, httpApi, memoryApi } from "./api.js";
7
+ export { SessionStore } from "./auth/sessionStore.js";
8
+ export { HttpSessionApi, MemorySessionApi } from "./auth/sessionApi.js";
9
+ export { userSchema } from "./auth/user.js";
10
+ export { defineRoutes } from "./routes.js";
11
+ export { Model, action, appPath, defineActions } from "./registered.js";
12
+ export { Cache, Cached } from "./cache.js";
13
+ export { EntityStore, WriteRefused, entityCalls } from "./entityStore.js";
14
+ export { Gate } from "./gate.js";
15
+ export { MemoryEntityApi, MemoryServer } from "./memoryApi.js";
16
+ export { Store, StoreBase, keepEqualParts, snapshotsOf } from "./store.js";
17
+ export { defineTables } from "./table.js";
18
+ export { HttpTableApi, MemoryTableApi, PublicTableStore, TableStore, httpTables, memoryTables, publicStoreName, tableCalls } from "./tables.js";
19
+ export { ModelBase, command, commandsOf, createModels, modelSnapshotsOf, parseMessage, sendMessage, splitType } from "./model.js";
@@ -0,0 +1,19 @@
1
+ import type { Entity, EntityApi } from './entityStore.ts';
2
+ import type { Gate } from './gate.ts';
3
+ export type MemoryApiOptions = {
4
+ latencyMs: number;
5
+ gate: Gate;
6
+ };
7
+ export declare class MemoryServer {
8
+ #private;
9
+ constructor(name: string, options: MemoryApiOptions);
10
+ reply<Value>(call: string, respond: () => Value): Promise<Value>;
11
+ }
12
+ export declare class MemoryEntityApi<T extends Entity> implements EntityApi<T> {
13
+ #private;
14
+ constructor(name: string, seed: readonly T[], options: MemoryApiOptions);
15
+ list(): Promise<readonly T[]>;
16
+ create(entity: T): Promise<T>;
17
+ update(entity: T): Promise<T>;
18
+ remove(id: string): Promise<void>;
19
+ }
@@ -0,0 +1,42 @@
1
+ export class MemoryServer {
2
+ #name;
3
+ #options;
4
+ constructor(name, options) {
5
+ this.#name = name;
6
+ this.#options = options;
7
+ }
8
+ reply(call, respond) {
9
+ return this.#options.gate.pass(`${this.#name}.${call}`, async () => {
10
+ await new Promise((done) => setTimeout(done, this.#options.latencyMs));
11
+ return respond();
12
+ });
13
+ }
14
+ }
15
+ export class MemoryEntityApi {
16
+ #server;
17
+ #saved;
18
+ constructor(name, seed, options) {
19
+ this.#server = new MemoryServer(name, options);
20
+ this.#saved = seed;
21
+ }
22
+ list() {
23
+ return this.#server.reply('list', () => this.#saved);
24
+ }
25
+ create(entity) {
26
+ return this.#server.reply('create', () => {
27
+ this.#saved = [...this.#saved, entity];
28
+ return entity;
29
+ });
30
+ }
31
+ update(entity) {
32
+ return this.#server.reply('update', () => {
33
+ this.#saved = this.#saved.map((item) => item.id === entity.id ? entity : item);
34
+ return entity;
35
+ });
36
+ }
37
+ remove(id) {
38
+ return this.#server.reply('remove', () => {
39
+ this.#saved = this.#saved.filter((item) => item.id !== id);
40
+ });
41
+ }
42
+ }