@trypattern/sdk 0.1.9 → 0.1.11

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 (77) hide show
  1. package/README.md +37 -8
  2. package/dist/app.d.ts +1 -0
  3. package/dist/app.js +4 -3
  4. package/dist/cli/app.js +119 -6
  5. package/dist/cli/publishing.js +2 -0
  6. package/dist/components/AppFrame.d.ts +4 -1
  7. package/dist/components/AppFrame.js +25 -7
  8. package/dist/components/Card.d.ts +7 -2
  9. package/dist/components/Card.js +59 -3
  10. package/dist/components/Checkbox.d.ts +10 -0
  11. package/dist/components/Checkbox.js +59 -0
  12. package/dist/components/Choice.d.ts +15 -0
  13. package/dist/components/Choice.js +80 -0
  14. package/dist/components/ChoiceGroup.d.ts +11 -0
  15. package/dist/components/ChoiceGroup.js +44 -0
  16. package/dist/components/Icon.d.ts +2 -1
  17. package/dist/components/Icon.js +2 -2
  18. package/dist/components/Image.d.ts +11 -0
  19. package/dist/components/Image.js +76 -0
  20. package/dist/components/ListRow.d.ts +3 -1
  21. package/dist/components/ListRow.js +18 -4
  22. package/dist/components/PlainScreen.d.ts +16 -0
  23. package/dist/components/PlainScreen.js +80 -0
  24. package/dist/components/PlainSidebar.d.ts +15 -0
  25. package/dist/components/PlainSidebar.js +179 -0
  26. package/dist/components/Screen.d.ts +3 -1
  27. package/dist/components/Screen.js +5 -1
  28. package/dist/components/ScrollArea.d.ts +2 -1
  29. package/dist/components/ScrollArea.js +7 -2
  30. package/dist/components/Text.d.ts +1 -1
  31. package/dist/components/Text.js +10 -4
  32. package/dist/components/TextField.d.ts +5 -1
  33. package/dist/components/TextField.js +47 -9
  34. package/dist/components/choiceContext.d.ts +4 -0
  35. package/dist/components/choiceContext.js +7 -0
  36. package/dist/components/frame.d.ts +2 -0
  37. package/dist/components/icons.d.ts +3 -0
  38. package/dist/components/icons.js +4 -1
  39. package/dist/components/interaction.d.ts +1 -1
  40. package/dist/entityStore.d.ts +1 -0
  41. package/dist/entityStore.js +1 -1
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.js +1 -0
  44. package/dist/media.d.ts +36 -0
  45. package/dist/media.js +74 -0
  46. package/dist/publishWire.d.ts +2 -0
  47. package/dist/publishWire.js +21 -1
  48. package/dist/react.d.ts +5 -0
  49. package/dist/react.js +4 -0
  50. package/dist/rules/checkApiRouting.js +1 -1
  51. package/dist/rules/lintPlugin.js +13 -4
  52. package/dist/rules/messages.d.ts +1 -0
  53. package/dist/rules/messages.js +1 -0
  54. package/dist/schema.d.ts +2 -0
  55. package/dist/schema.js +1 -0
  56. package/dist/server/createServer.d.ts +5 -3
  57. package/dist/server/createServer.js +32 -2
  58. package/dist/server/env.d.ts +3 -1
  59. package/dist/server/imageHeader.d.ts +10 -0
  60. package/dist/server/imageHeader.js +159 -0
  61. package/dist/server/index.d.ts +6 -1
  62. package/dist/server/index.js +3 -0
  63. package/dist/server/jsonLd.d.ts +45 -0
  64. package/dist/server/jsonLd.js +88 -0
  65. package/dist/server/media.d.ts +16 -0
  66. package/dist/server/media.js +135 -0
  67. package/dist/server/pages.d.ts +72 -0
  68. package/dist/server/pages.js +301 -0
  69. package/dist/server/session.js +1 -1
  70. package/dist/server/tables.d.ts +3 -0
  71. package/dist/server/tables.js +23 -8
  72. package/dist/start.js +30 -2
  73. package/dist/table.d.ts +1 -0
  74. package/dist/table.js +8 -1
  75. package/dist/tables.d.ts +2 -1
  76. package/dist/tables.js +10 -1
  77. package/package.json +1 -1
package/README.md CHANGED
@@ -80,7 +80,11 @@ export default createServer({ tables, admins })
80
80
  `id` and `createdAt` that Pattern sets. Every table declares `access` (`anyone`, `signedIn`,
81
81
  `owner`, `admin` or `server` for read, create, update and delete) and `writes` (`optimistic` or
82
82
  `confirmed`), and may add `owned: true` (an `ownerId` the server fills), `public` columns (a
83
- read-only `public<Table>` store) and `unique` column sets (a 409 `conflict`). `src/access.ts` is
83
+ read-only `public<Table>` store) and `unique` column sets (a 409 `conflict`). `media` is Pattern's
84
+ own table of photos (`key`, `width`, `height`, `placeholder`, `alt`, `type`, `bytes`): an app
85
+ that shows photos adds it to its tables (`{ ...defineTables({ … }), media }`), binds an R2
86
+ bucket as `MEDIA` in `wrangler.jsonc` with `"/media/*"` in `run_worker_first`, and stores the
87
+ media row's `id` in its own columns. Anyone reads it; only the server creates rows. `src/access.ts` is
84
88
  the one rule engine; the Worker and the flow runner's memory API both use it, and
85
89
  `accessFlows({ App })` from `@trypattern/sdk/flows` checks every table's rules against the seed.
86
90
  Store writes return `'saved' | 'conflict' | 'forbidden' | 'missing' | 'failed'`. Safe to import
@@ -89,16 +93,41 @@ export default createServer({ tables, admins })
89
93
  auth.trypattern.ai (Google, and emailed 6-digit codes; locally the code prints in the dev log),
90
94
  dev sign-in, sessions, `/api/me` and every table under `/api/tables`. A deployed app holds one
91
95
  `AUTH_SECRET` that Pattern's deploy sets, and no Google secrets. Also `users`, `sessions`,
96
+ With `media` in its tables it also serves `POST /api/media` (admins only, a form with `file`
97
+ and `alt`, up to 10 MB of JPEG, PNG, WebP, GIF or AVIF; HEIC is refused): it reads the size from
98
+ the file's header (EXIF rotation included), stores it in `MEDIA` as `images/<sha256-16>.<ext>`,
99
+ asks the zone's image transformations for a 24-pixel JPEG placeholder (skipped in dev, so it is
100
+ `null` locally) and adds the row; and `GET /media/<key>`, immutable with an ETag. With
101
+ `pages` (`definePages(tables, { '/': head, '/items/:itemId': { head, paths } })` in
102
+ `server/pages.ts`) it renders the `<head>` of public pages: each `head` reads tables anyone may
103
+ read (`rows`, `row`, `photoUrl`) and returns `{ title, description, image, hero, jsonLd, data }`
104
+ (or `null` for a 404). The Worker puts the title, description, canonical link, `og:*` (the image
105
+ at 1200×630 with `fit=cover`), `twitter:card`, JSON-LD, a preload of the `hero` with `Image`'s
106
+ srcset, and `data` as `<script type="application/json" id="pattern-data">` into the built
107
+ `index.html`, and the table stores start from those rows; `sitemap.xml` (with `<image:image>`
108
+ and the alt as caption) and `robots.txt` come from the same list (`paths` lists a pattern's
109
+ params). It needs `"binding": "ASSETS"` and the page paths, `/sitemap.xml` and `/robots.txt` in
110
+ `run_worker_first`; other paths still get the built files. `localBusiness({ … })` and
111
+ `product({ … })` write the JSON-LD. Also `users`, `sessions`,
92
112
  `sqlTables`, `seedSql`, the sign-in wire schemas (`verifiedIdentity`, `exchangeRequest`,
93
113
  `exchangeReply`, `sendCodeRequest`, `sendCodeReply`, `verifyCodeRequest`, `verifyCodeReply`) and
94
- the Worker type `D1Database`.
114
+ the Worker types `D1Database` and `R2Bucket`.
95
115
  - `@trypattern/sdk/react`: `AppProvider` and `useModel`, and the primitives every app draws
96
116
  with. The frame: `AppFrame`, `Screen`, `SignInScreen`, `CommandPalette`, `Toaster`,
97
117
  `CommandMenu`. Layout:
98
- `Content`, `Stack`, `Row`, `Card`, `Divider`, `SettingsRow`, `PropertyRow`, `ListRow`, `LoadingRows`, `EmptyState`, `Form`.
99
- Words: `Text` and `Link`. Controls: `Button` (or a link to a page with `to`), `AppearancePicker`, `IconButton`, `TextField`, `Select`, `Switch`,
100
- `SegmentedControl`, `Menu`, `ContextMenu`, `Dialog`, `Tooltip`, `Kbd`, `ScrollArea`, `Avatar`,
101
- `Badge`, `Skeleton`, `Spinner`, `Icon`. Types: `IconName`, `MenuEntry`, `SidebarLink`,
118
+ `Content`, `Stack`, `Row`, `Card` (with `to`, `href` or `onClick` the whole card is a link or a
119
+ button, with the focus ring and a raised shadow on hover), `Divider`, `SettingsRow`, `PropertyRow`, `ListRow`, `LoadingRows`, `EmptyState`, `Form`.
120
+ Words: `Text` and `Link`. Controls: `Button` (or a link to a page with `to`), `AppearancePicker`, `IconButton`, `TextField`
121
+ (`multiline` with `rows` for a few lines), `Select`, `Switch`,
122
+ `SegmentedControl`, `ChoiceGroup` (a wrapping radio group of `Choice` children, each with a
123
+ `title`, `subline`, `isDisabled` or any children such as an `Image` with a `label`; the chosen
124
+ one is ringed, the others fade, arrow keys move between them), `Menu`, `ContextMenu`, `Dialog`, `Tooltip`, `Kbd`, `ScrollArea`, `Avatar`,
125
+ `Badge`, `Skeleton`, `Spinner`, `Icon`. Photos: `Image` (`image` is a media row, `sizes` is
126
+ required, `priority` for the first photo on a page only, `aspect` crops with `fit=cover`): it
127
+ reserves the size from the row's `width` and `height`, offers 320 to 1920 pixel widths (never
128
+ wider than the original) through `/cdn-cgi/image/…` (plain `/media/<key>` on localhost), lazy-loads
129
+ unless `priority`, shows the blurred placeholder and fades the photo in over 200 ms unless it was
130
+ already loaded or motion is reduced, and shows a neutral block when the photo is missing or fails. Types: `IconName`, `MenuEntry`, `SidebarLink`,
102
131
  `Toast`, `SelectOption`, `SegmentedOption`, `CommandMenuItem`, `SpaceName` and `Placement`.
103
132
  Everything takes closed choices (`variant`, `tone`, `size`, `gap`, `width`) and a `style`
104
133
  limited to placement, and every interactive primitive reports to `startApp`'s `onInteraction`.
@@ -121,8 +150,8 @@ export default createServer({ tables, admins })
121
150
  `size` and layout choices it is used with, the app's own drawn pieces and the tokens they read,
122
151
  and its icons. Pattern's dashboard renders the design system from it.
123
152
  - `@trypattern/sdk/tsconfig/app.json` and `/tsconfig/server.json`: the presets project tsconfigs extend.
124
- - The `app` command: start, check, fix, reset, migration, build, send, screenshot, test, sign-in,
125
- setup.
153
+ - The `app` command: start, check, fix, reset, migration, media (`app media add photo.jpg --alt "…"`
154
+ uploads to the dev server as the first admin), build, send, screenshot, test, sign-in, setup.
126
155
 
127
156
  Published to npm as `@trypattern/sdk`, compiled to `dist/`; in this repo the blueprint runs the
128
157
  source directly. Still to come: payments, workflows and blocks.
package/dist/app.d.ts CHANGED
@@ -44,6 +44,7 @@ export type AppDependencies<Api> = {
44
44
  cache: Cache;
45
45
  gate: Gate;
46
46
  clock?: Clock;
47
+ inline?: Readonly<Record<string, unknown>> | undefined;
47
48
  };
48
49
  export type RunningApp<Command, Snapshot extends {
49
50
  readonly path: string;
package/dist/app.js CHANGED
@@ -15,7 +15,7 @@ import { assertNotBuilding, snapshotsOf } from "./store.js";
15
15
  import { ModelBase, command, createModels, modelSnapshotsOf, parseMessage, sendMessage } from "./model.js";
16
16
  import { isRoutedPath, pageIds, registerRoutes } from "./routes.js";
17
17
  import { PublicTableStore, TableStore, publicStoreName } from "./tables.js";
18
- function dataStores(tables, api, cache, viewer) {
18
+ function dataStores(tables, api, cache, viewer, inline = {}) {
19
19
  const stores = { tables: {}, publicViews: {} };
20
20
  for (const [key, table] of Object.entries(tables)) {
21
21
  const found = Reflect.get(api, key);
@@ -31,6 +31,7 @@ function dataStores(tables, api, cache, viewer) {
31
31
  api: tableApi,
32
32
  cache,
33
33
  viewer,
34
+ inline: inline[key],
34
35
  ...(publicView === undefined ? {} : { publicView })
35
36
  });
36
37
  }
@@ -81,7 +82,7 @@ export function defineApp(definition) {
81
82
  #failures = [];
82
83
  #path;
83
84
  #user;
84
- constructor(router, { api, cache, gate, clock = realClock() }) {
85
+ constructor(router, { api, cache, gate, clock = realClock(), inline }) {
85
86
  this.router = router;
86
87
  this.#cache = cache;
87
88
  this.#gate = gate;
@@ -91,7 +92,7 @@ export function defineApp(definition) {
91
92
  api: api.app,
92
93
  remembered: cache.scope('app.title', z.string())
93
94
  });
94
- const data = dataStores(definition.tables, api.tables, cache, () => viewerOf(session.state.user));
95
+ const data = dataStores(definition.tables, api.tables, cache, () => viewerOf(session.state.user), inline);
95
96
  const custom = Object.fromEntries(Object.entries(definition.stores ?? {}).map(([key, Store]) => [
96
97
  key,
97
98
  new Store(cache.persisted(`stores.${key}`), api.services)
package/dist/cli/app.js CHANGED
@@ -10,10 +10,12 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
10
10
  import { spawn, spawnSync } from 'node:child_process';
11
11
  import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
12
12
  import { homedir } from 'node:os';
13
- import { delimiter, dirname, extname, join, relative, resolve } from 'node:path';
13
+ import { basename, delimiter, dirname, extname, join, relative, resolve } from 'node:path';
14
14
  import { pathToFileURL } from 'node:url';
15
15
  import { createHash } from 'node:crypto';
16
16
  import { z } from 'zod';
17
+ import { MEDIA_LIMIT_BYTES } from "../media.js";
18
+ import { isHeic } from "../server/imageHeader.js";
17
19
  import { resetLocalDatabase } from "../vite/localWorker.js";
18
20
  import { login, publish, pull, restore, versions as listVersions } from "./publishing.js";
19
21
  import { toolBin } from "./tools.js";
@@ -79,8 +81,19 @@ const VALUE_FLAGS = new Set([
79
81
  '--name',
80
82
  '--as',
81
83
  '--code',
82
- '--message'
84
+ '--message',
85
+ '--alt'
83
86
  ]);
87
+ const PHOTO_TYPES = {
88
+ '.jpg': 'image/jpeg',
89
+ '.jpeg': 'image/jpeg',
90
+ '.png': 'image/png',
91
+ '.webp': 'image/webp',
92
+ '.gif': 'image/gif',
93
+ '.avif': 'image/avif'
94
+ };
95
+ const HEIC_EXTENSIONS = new Set(['.heic', '.heif']);
96
+ const MEDIA_USAGE = ' usage app media add photo.jpg --alt "Pink peonies in a glass vase"';
84
97
  const SHIM_SOURCE = `#!/bin/sh
85
98
  ${SHIM_MARKER_LINE}
86
99
  dir=$PWD
@@ -195,6 +208,22 @@ migrations to the local database. Prints "No schema changes" when the migrations
195
208
  run: migration,
196
209
  takesArgument: true
197
210
  },
211
+ media: {
212
+ summary: 'Add a photo to the media table: app media add photo.jpg --alt "…"',
213
+ details: `app media add photo.jpg --alt "Pink peonies in a glass vase"
214
+ Sends the photo to the dev server's POST /api/media, signed in as the first email in admins in
215
+ schema.ts (dev sign-in, so this adds to the local database and the local MEDIA bucket). The server
216
+ reads its width and height, stores it as images/<hash>.<ext> and adds a row to the media table;
217
+ this prints the row. Show it with <Image image={row} sizes="…" />.
218
+ --alt is required: describe the photo for someone who can't see it.
219
+ JPEG, PNG, WebP, GIF or AVIF, up to 10 MB. iPhone HEIC photos are refused: convert one first with
220
+ sips -s format jpeg photo.heic --out photo.jpg. The app's tables must include media from
221
+ @trypattern/sdk/schema, and wrangler.jsonc must bind an R2 bucket as MEDIA and keep "/media/*" in
222
+ run_worker_first. Uses the running dev server, or starts one and stops it afterwards.`,
223
+ run: (_argument, options, rest) => addMedia(rest, options),
224
+ argumentCount: 2,
225
+ flags: ['--alt']
226
+ },
198
227
  build: {
199
228
  summary: 'Build the app for production into dist/',
200
229
  details: 'Runs vite build: the browser app and the Cloudflare Worker that serves /api.',
@@ -324,7 +353,7 @@ existing ${SHIM_PATH} that was not installed by Pattern.`,
324
353
  },
325
354
  help: {
326
355
  summary: 'Show this help (`app <command> --help` for details)',
327
- details: 'Lists commands. Flags: --json (machine-readable stdout), --help, --no-open (start only), --visual (test only), --commands, --name, --as and --signed-out (screenshot only).',
356
+ details: 'Lists commands. Flags: --json (machine-readable stdout), --help, --no-open (start only), --visual (test only), --commands, --name, --as and --signed-out (screenshot only), --alt (media only).',
328
357
  run: help
329
358
  }
330
359
  };
@@ -1214,6 +1243,90 @@ function ownerBackfillWarning(name) {
1214
1243
  return undefined;
1215
1244
  return `${file} adds owner_id to ${tables.join(', ')}, which may already have rows. Backfill owner_id in this migration before the NOT NULL constraint, or reset the table.`;
1216
1245
  }
1246
+ function mediaRefusal(text, code = 1) {
1247
+ return { code, data: { error: text }, text: `✗ ${text}` };
1248
+ }
1249
+ function replyError(reply) {
1250
+ const error = Reflect.get(Object(reply), 'error');
1251
+ return typeof error === 'string' ? error : undefined;
1252
+ }
1253
+ async function firstAdmin() {
1254
+ const admins = await importExport('schema.ts', 'admins');
1255
+ const first = Array.isArray(admins) ? admins[0] : undefined;
1256
+ return typeof first === 'string' ? first : undefined;
1257
+ }
1258
+ async function addMedia(args, options) {
1259
+ const [action, file] = args;
1260
+ if (action !== 'add' || file === undefined) {
1261
+ return mediaRefusal(`Say which photo to add\n${MEDIA_USAGE}`, 2);
1262
+ }
1263
+ const alt = options.get('--alt');
1264
+ if (typeof alt !== 'string' || alt.trim() === '') {
1265
+ return mediaRefusal(`--alt is required: describe this photo for someone who can't see it\n${MEDIA_USAGE}`, 2);
1266
+ }
1267
+ const path = resolve(process.cwd(), file);
1268
+ if (!existsSync(path))
1269
+ return mediaRefusal(`There is no file at ${file}`);
1270
+ const bytes = readFileSync(path);
1271
+ const extension = extname(path).toLowerCase();
1272
+ if (HEIC_EXTENSIONS.has(extension) || isHeic(bytes)) {
1273
+ return mediaRefusal(`${file} is an iPhone HEIC photo, which browsers can't show. Convert it to JPEG, then add the JPEG:\n sips -s format jpeg ${file} --out ${file.replace(/\.[^.]+$/, '')}-converted.jpg`);
1274
+ }
1275
+ const type = PHOTO_TYPES[extension];
1276
+ if (type === undefined) {
1277
+ return mediaRefusal(`${file} is not a photo the web can show: use JPEG, PNG, WebP, GIF or AVIF`);
1278
+ }
1279
+ if (bytes.byteLength > MEDIA_LIMIT_BYTES) {
1280
+ return mediaRefusal(`${file} is over 10 MB: export it smaller first`);
1281
+ }
1282
+ const admin = await firstAdmin();
1283
+ if (admin === undefined) {
1284
+ return mediaRefusal("Only an admin can add photos: put the owner's email in admins in schema.ts");
1285
+ }
1286
+ return withDevServer(async (url) => {
1287
+ const origin = new URL(url).origin;
1288
+ const signedIn = await fetch(new URL('/api/auth/dev-sign-in', url), {
1289
+ method: 'POST',
1290
+ headers: { origin, 'content-type': 'application/json' },
1291
+ body: JSON.stringify({ email: admin })
1292
+ });
1293
+ if (!signedIn.ok) {
1294
+ return mediaRefusal(`Could not sign in as ${admin} (HTTP ${signedIn.status})`);
1295
+ }
1296
+ const cookie = signedIn.headers
1297
+ .getSetCookie()
1298
+ .map((line) => line.split(';')[0])
1299
+ .join('; ');
1300
+ const form = new FormData();
1301
+ form.set('file', new Blob([bytes], { type }), basename(path));
1302
+ form.set('alt', alt.trim());
1303
+ const response = await fetch(new URL('/api/media', url), {
1304
+ method: 'POST',
1305
+ headers: { origin, cookie },
1306
+ body: form
1307
+ });
1308
+ const reply = await response.json().catch(() => undefined);
1309
+ if (response.status === 404) {
1310
+ return mediaRefusal("This app has no media table: add media from '@trypattern/sdk/schema' to tables in schema.ts, run app migration add_media, and bind an R2 bucket as MEDIA in wrangler.jsonc");
1311
+ }
1312
+ if (!response.ok) {
1313
+ return mediaRefusal(`Could not add ${file}: ${replyError(reply) ?? `HTTP ${response.status}`}`);
1314
+ }
1315
+ const row = Object(reply);
1316
+ return {
1317
+ code: 0,
1318
+ data: { media: row },
1319
+ text: [
1320
+ `✓ Added ${file} to media`,
1321
+ ` id ${String(row['id'])}`,
1322
+ ` key ${String(row['key'])}`,
1323
+ ` size ${String(row['width'])}×${String(row['height'])}`,
1324
+ ` alt ${String(row['alt'])}`,
1325
+ ' show <Image image={row} sizes="…" /> with that media row'
1326
+ ].join('\n')
1327
+ };
1328
+ });
1329
+ }
1217
1330
  async function build() {
1218
1331
  let vite;
1219
1332
  try {
@@ -1328,7 +1441,7 @@ async function setup() {
1328
1441
  function help() {
1329
1442
  const width = Math.max(...Object.keys(COMMANDS).map((n) => n.length));
1330
1443
  const lines = Object.entries(COMMANDS).map(([name, cmd]) => `app ${name.padEnd(width)} ${cmd.summary}`);
1331
- lines.push('Flags: --json (machine-readable stdout), <command> --help (details and its own flags: start --no-open, test --visual, screenshot --commands --name --as --signed-out, pull --code, publish --message)');
1444
+ lines.push('Flags: --json (machine-readable stdout), <command> --help (details and its own flags: start --no-open, test --visual, screenshot --commands --name --as --signed-out, pull --code, publish --message, media --alt)');
1332
1445
  return {
1333
1446
  code: 0,
1334
1447
  data: {
@@ -1378,7 +1491,7 @@ async function main(argv) {
1378
1491
  const allowedFlags = [...GLOBAL_FLAGS, ...(command?.flags ?? [])];
1379
1492
  const unknownFlags = [...options.keys()].filter((f) => !allowedFlags.includes(f));
1380
1493
  let outcome;
1381
- const allowedPositionals = command?.takesArgument === true ? 2 : 1;
1494
+ const allowedPositionals = 1 + (command?.argumentCount ?? (command?.takesArgument === true ? 1 : 0));
1382
1495
  if (command === undefined ||
1383
1496
  positional.length > allowedPositionals ||
1384
1497
  unknownFlags.length > 0 ||
@@ -1412,7 +1525,7 @@ async function main(argv) {
1412
1525
  };
1413
1526
  }
1414
1527
  else {
1415
- outcome = await command.run(positional[1], options);
1528
+ outcome = await command.run(positional[1], options, positional.slice(1));
1416
1529
  }
1417
1530
  if (jsonMode) {
1418
1531
  const payload = {
@@ -412,6 +412,8 @@ function uploadForm(root, io, isStarter) {
412
412
  const form = new FormData();
413
413
  form.append(UPLOAD.compatibilityDate, String(Reflect.get(Object(wrangler), 'compatibility_date') ?? ''));
414
414
  form.append(UPLOAD.sdkVersion, io.sdkVersion);
415
+ const assets = Reflect.get(Object(wrangler), 'assets');
416
+ form.append(UPLOAD.runWorkerFirst, JSON.stringify(Reflect.get(Object(assets), 'run_worker_first') ?? []));
415
417
  for (const file of readdirSync(worker)) {
416
418
  if (!file.endsWith('.js') || isPrivatePath(file))
417
419
  continue;
@@ -1,15 +1,18 @@
1
1
  import type { ReactNode } from 'react';
2
+ import type { FrameLook } from './frame.ts';
2
3
  import type { MenuEntry } from './MenuItems.tsx';
3
4
  import type { SidebarLink } from './Sidebar.tsx';
4
5
  type AppFrameProps = {
6
+ look?: FrameLook;
5
7
  workspace?: string;
6
8
  links: readonly SidebarLink[];
7
9
  workspaceMenu?: readonly MenuEntry[];
10
+ accountMenu?: readonly MenuEntry[];
8
11
  isSidebarToggled: boolean;
9
12
  onToggleSidebar: () => void;
10
13
  onCloseSidebar: () => void;
11
14
  onSearch?: () => void;
12
15
  children: ReactNode;
13
16
  };
14
- export declare function AppFrame({ workspace, links, workspaceMenu, isSidebarToggled, onToggleSidebar, onCloseSidebar, onSearch, children }: AppFrameProps): import("react").JSX.Element;
17
+ export declare function AppFrame({ look, workspace, links, workspaceMenu, accountMenu, isSidebarToggled, onToggleSidebar, onCloseSidebar, onSearch, children }: AppFrameProps): import("react").JSX.Element;
15
18
  export {};
@@ -3,10 +3,11 @@ import * as stylex from '@stylexjs/stylex';
3
3
  import { breakpoints, colors, radii, shadows, space } from "../theme/tokens.stylex.js";
4
4
  import { useModel } from "../hooks/appContext.js";
5
5
  import { FrameContext } from "./frame.js";
6
+ import { PlainSidebar } from "./PlainSidebar.js";
6
7
  import { Scrim } from "./Scrim.js";
7
8
  import { Sidebar } from "./Sidebar.js";
8
9
  import { useAnimatesOnChange } from "../hooks/useAnimatesOnChange.js";
9
- export function AppFrame({ workspace, links, workspaceMenu = [], isSidebarToggled, onToggleSidebar, onCloseSidebar, onSearch, children }) {
10
+ export function AppFrame({ look = 'shell', workspace, links, workspaceMenu = [], accountMenu = [], isSidebarToggled, onToggleSidebar, onCloseSidebar, onSearch, children }) {
10
11
  const appSettings = useModel('appSettings');
11
12
  const account = useModel('account');
12
13
  const menu = account.user === null
@@ -16,13 +17,19 @@ export function AppFrame({ workspace, links, workspaceMenu = [], isSidebarToggle
16
17
  ...(workspaceMenu.length > 0 ? ['separator'] : []),
17
18
  { id: 'signOut', label: 'Log out', onSelect: account.signOut }
18
19
  ];
20
+ const accountEntries = [
21
+ ...accountMenu,
22
+ ...(accountMenu.length > 0 ? ['separator'] : []),
23
+ { id: 'signOut', label: 'Log out', onSelect: account.signOut }
24
+ ];
25
+ const search = onSearch === undefined
26
+ ? undefined
27
+ : () => {
28
+ onCloseSidebar();
29
+ onSearch();
30
+ };
19
31
  const isSidebarAnimating = useAnimatesOnChange(isSidebarToggled, SIDEBAR_ANIMATION_MS);
20
- return (_jsx(FrameContext, { value: { onToggleSidebar }, children: _jsxs("div", { ...stylex.props(styles.frame), children: [_jsx(Sidebar, { workspace: workspace ?? appSettings.title, workspaceMenu: menu, links: links, isToggled: isSidebarToggled, isAnimating: isSidebarAnimating, onFollowLink: onCloseSidebar, onSearch: onSearch === undefined
21
- ? undefined
22
- : () => {
23
- onCloseSidebar();
24
- onSearch();
25
- } }), _jsx(Scrim, { label: "Close sidebar", isOpen: isSidebarToggled, isAnimating: isSidebarAnimating, onClose: onCloseSidebar }), _jsx("main", { ...stylex.props(styles.panel, isSidebarToggled && styles.panelAlone), children: children })] }) }));
32
+ return (_jsx(FrameContext, { value: { look, onToggleSidebar }, children: _jsxs("div", { ...stylex.props(styles.frame, look === 'plain' && styles.plain), children: [look === 'plain' ? (_jsx(PlainSidebar, { title: workspace ?? appSettings.title, user: account.user, accountMenu: accountEntries, links: links, isToggled: isSidebarToggled, isAnimating: isSidebarAnimating, onFollowLink: onCloseSidebar, onSearch: search })) : (_jsx(Sidebar, { workspace: workspace ?? appSettings.title, workspaceMenu: menu, links: links, isToggled: isSidebarToggled, isAnimating: isSidebarAnimating, onFollowLink: onCloseSidebar, onSearch: search })), _jsx(Scrim, { label: "Close sidebar", isOpen: isSidebarToggled, isAnimating: isSidebarAnimating, onClose: onCloseSidebar }), _jsx("main", { ...stylex.props(look === 'plain' ? styles.plainPanel : styles.panel, look === 'shell' && isSidebarToggled && styles.panelAlone), children: children })] }) }));
26
33
  }
27
34
  const SIDEBAR_ANIMATION_MS = 400;
28
35
  const styles = stylex.create({
@@ -55,6 +62,17 @@ const styles = stylex.create({
55
62
  [breakpoints.compact]: null
56
63
  }
57
64
  },
65
+ plain: {
66
+ backgroundColor: colors.page
67
+ },
68
+ plainPanel: {
69
+ display: 'flex',
70
+ flexGrow: 1,
71
+ flexDirection: 'column',
72
+ minWidth: 0,
73
+ overflow: 'hidden',
74
+ backgroundColor: colors.page
75
+ },
58
76
  panelAlone: {
59
77
  marginInlineStart: {
60
78
  default: space.xs,
@@ -1,12 +1,17 @@
1
1
  import type { ReactNode } from 'react';
2
+ import type { AppPath } from '../registered.ts';
2
3
  import type { Placement } from './Placement.ts';
3
4
  type CardPadding = 'none' | 'sm' | 'md' | 'lg';
5
+ type CardSurface = 'subtle' | 'strong' | 'container';
4
6
  type CardProps = {
5
7
  children: ReactNode;
6
- surface?: 'subtle' | 'strong' | 'container';
8
+ surface?: CardSurface;
7
9
  padding?: CardPadding;
8
10
  paddingBlock?: CardPadding;
11
+ to?: AppPath;
12
+ href?: string;
13
+ onClick?: () => void;
9
14
  style?: Placement;
10
15
  };
11
- export declare function Card({ children, surface, padding, paddingBlock, style }: CardProps): import("react").JSX.Element;
16
+ export declare function Card({ children, surface, padding, paddingBlock, to, href, onClick, style }: CardProps): import("react").JSX.Element;
12
17
  export {};
@@ -1,8 +1,31 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Button as BaseButton } from '@base-ui/react/button';
2
3
  import * as stylex from '@stylexjs/stylex';
3
- import { colors, radii, shadows, space } from "../theme/tokens.stylex.js";
4
- export function Card({ children, surface = 'subtle', padding = 'lg', paddingBlock, style }) {
5
- return (_jsx("div", { ...stylex.props(styles.card, surfaces[surface], paddings[padding], paddingBlock !== undefined && paddingBlocks[paddingBlock], style), children: children }));
4
+ import { Link as RouterLink } from 'react-router';
5
+ import { colors, motion, radii, shadows, space } from "../theme/tokens.stylex.js";
6
+ import { focusRing } from "./controlLook.js";
7
+ import { interact, textOf } from "./interaction.js";
8
+ export function Card({ children, surface = 'subtle', padding = 'lg', paddingBlock, to, href, onClick, style }) {
9
+ const isInteractive = to !== undefined || href !== undefined || onClick !== undefined;
10
+ const className = stylex.props(styles.card, surfaces[surface], paddings[padding], paddingBlock !== undefined && paddingBlocks[paddingBlock], isInteractive && styles.interactive, isInteractive && hovers[surface], isInteractive && focusRing.visible, style).className;
11
+ const report = (event) => {
12
+ interact({
13
+ primitive: 'Card',
14
+ event,
15
+ label: textOf(children) || (href ?? to ?? '')
16
+ });
17
+ onClick?.();
18
+ };
19
+ if (href !== undefined) {
20
+ return (_jsx("a", { href: href, target: "_blank", rel: "noreferrer", onClick: () => report('open'), className: className, children: children }));
21
+ }
22
+ if (to !== undefined) {
23
+ return (_jsx(RouterLink, { to: to, onClick: () => report('navigate'), className: className, children: children }));
24
+ }
25
+ if (onClick !== undefined) {
26
+ return (_jsx(BaseButton, { nativeButton: false, render: _jsx("div", {}), onClick: () => report('click'), className: className, children: children }));
27
+ }
28
+ return _jsx("div", { className: className, children: children });
6
29
  }
7
30
  const styles = stylex.create({
8
31
  card: {
@@ -11,6 +34,19 @@ const styles = stylex.create({
11
34
  overflow: 'hidden',
12
35
  borderRadius: radii.xxl,
13
36
  boxShadow: shadows.default
37
+ },
38
+ interactive: {
39
+ color: 'inherit',
40
+ textAlign: 'start',
41
+ textDecoration: 'none',
42
+ outlineOffset: 0,
43
+ boxShadow: {
44
+ default: shadows.default,
45
+ ':hover': shadows.large
46
+ },
47
+ transitionTimingFunction: motion.easeOut,
48
+ transitionDuration: motion.fast,
49
+ transitionProperty: 'background-color, box-shadow'
14
50
  }
15
51
  });
16
52
  const surfaces = stylex.create({
@@ -24,6 +60,26 @@ const surfaces = stylex.create({
24
60
  backgroundColor: colors.container
25
61
  }
26
62
  });
63
+ const hovers = stylex.create({
64
+ subtle: {
65
+ backgroundColor: {
66
+ default: colors.componentOnContainerSubtle,
67
+ ':hover': colors.componentOnContainerSubtleHover
68
+ }
69
+ },
70
+ strong: {
71
+ backgroundColor: {
72
+ default: colors.componentOnContainerStrong,
73
+ ':hover': colors.componentOnContainerStrongHover
74
+ }
75
+ },
76
+ container: {
77
+ backgroundColor: {
78
+ default: colors.container,
79
+ ':hover': colors.componentOnContainerStrongHover
80
+ }
81
+ }
82
+ });
27
83
  const paddings = stylex.create({
28
84
  none: { padding: 0 },
29
85
  sm: { padding: space.sm },
@@ -0,0 +1,10 @@
1
+ import type { Placement } from './Placement.ts';
2
+ type CheckboxProps = {
3
+ label: string;
4
+ checked: boolean;
5
+ disabled?: boolean;
6
+ onChange: (checked: boolean) => void;
7
+ style?: Placement;
8
+ };
9
+ export declare function Checkbox({ label, checked, disabled, onChange, style }: CheckboxProps): import("react").JSX.Element;
10
+ export {};
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox';
3
+ import * as stylex from '@stylexjs/stylex';
4
+ import { borders, colors, iconSizes, motion, radii, space } from "../theme/tokens.stylex.js";
5
+ import { focusRing } from "./controlLook.js";
6
+ import { interact } from "./interaction.js";
7
+ export function Checkbox({ label, checked, disabled = false, onChange, style }) {
8
+ return (_jsx(BaseCheckbox.Root, { "aria-label": label, checked: checked, disabled: disabled, onCheckedChange: (next) => {
9
+ interact({ primitive: 'Checkbox', event: 'change', label });
10
+ onChange(next);
11
+ }, className: (state) => stylex.props(styles.box, state.checked ? styles.on : styles.off, disabled && styles.disabled, focusRing.visible, style).className, children: _jsx(BaseCheckbox.Indicator, { className: stylex.props(styles.indicator).className, children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3.25", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", ...stylex.props(styles.tick), children: _jsx("path", { d: "M5 12.5 9.5 17 19 7.5" }) }) }) }));
12
+ }
13
+ const styles = stylex.create({
14
+ box: {
15
+ position: 'relative',
16
+ display: 'grid',
17
+ flexShrink: 0,
18
+ placeItems: 'center',
19
+ width: iconSizes.md,
20
+ height: iconSizes.md,
21
+ padding: 0,
22
+ color: colors.onInvertedForeground,
23
+ outlineOffset: borders.thin,
24
+ borderRadius: radii.full,
25
+ transitionTimingFunction: motion.easeOut,
26
+ transitionDuration: motion.fast,
27
+ transitionProperty: 'background-color, box-shadow',
28
+ '::before': {
29
+ position: 'absolute',
30
+ inset: `calc(-1 * ${space.xs})`,
31
+ content: '""',
32
+ borderRadius: radii.full
33
+ }
34
+ },
35
+ off: {
36
+ backgroundColor: 'transparent',
37
+ boxShadow: `0 0 0 ${borders.thin} ${colors.borderStrong} inset, 0 ${borders.thin} ${borders.thin} ${colors.borderSubtle}`
38
+ },
39
+ on: {
40
+ backgroundColor: colors.invertedSolid,
41
+ backgroundImage: `linear-gradient(180deg, color-mix(in oklab, ${colors.whiteFill} 15%, transparent), transparent)`,
42
+ boxShadow: `0 ${borders.thin} 0 color-mix(in oklab, ${colors.whiteFill} 15%, transparent) inset, 0 ${borders.thin} 2px color-mix(in oklab, ${colors.blackFill} 20%, transparent), 0 0 0 ${borders.hairline} color-mix(in oklab, ${colors.blackFill} 40%, transparent)`
43
+ },
44
+ disabled: {
45
+ color: colors.disabledForeground,
46
+ backgroundColor: colors.disabled,
47
+ backgroundImage: 'none',
48
+ boxShadow: 'none'
49
+ },
50
+ indicator: {
51
+ display: 'grid',
52
+ placeItems: 'center'
53
+ },
54
+ tick: {
55
+ display: 'block',
56
+ width: `calc(${iconSizes.md} * 9 / 16)`,
57
+ height: `calc(${iconSizes.md} * 9 / 16)`
58
+ }
59
+ });
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from 'react';
2
+ type ChoiceProps = {
3
+ value: string;
4
+ subline?: string;
5
+ isDisabled?: boolean;
6
+ children?: ReactNode;
7
+ } & ({
8
+ title: string;
9
+ label?: string;
10
+ } | {
11
+ title?: never;
12
+ label: string;
13
+ });
14
+ export declare function Choice({ value, title, label, subline, isDisabled, children }: ChoiceProps): import("react").JSX.Element;
15
+ export {};