@trypattern/sdk 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -8
- package/dist/app.d.ts +1 -0
- package/dist/app.js +4 -3
- package/dist/cli/app.js +119 -6
- package/dist/cli/publishing.js +2 -0
- package/dist/components/AppFrame.d.ts +4 -1
- package/dist/components/AppFrame.js +25 -7
- package/dist/components/Card.d.ts +7 -2
- package/dist/components/Card.js +59 -3
- package/dist/components/Checkbox.d.ts +10 -0
- package/dist/components/Checkbox.js +59 -0
- package/dist/components/Choice.d.ts +15 -0
- package/dist/components/Choice.js +80 -0
- package/dist/components/ChoiceGroup.d.ts +11 -0
- package/dist/components/ChoiceGroup.js +44 -0
- package/dist/components/Icon.d.ts +2 -1
- package/dist/components/Icon.js +2 -2
- package/dist/components/Image.d.ts +11 -0
- package/dist/components/Image.js +76 -0
- package/dist/components/PlainScreen.d.ts +16 -0
- package/dist/components/PlainScreen.js +80 -0
- package/dist/components/PlainSidebar.d.ts +15 -0
- package/dist/components/PlainSidebar.js +179 -0
- package/dist/components/Screen.d.ts +3 -1
- package/dist/components/Screen.js +5 -1
- package/dist/components/TextField.d.ts +3 -1
- package/dist/components/TextField.js +25 -4
- package/dist/components/choiceContext.d.ts +4 -0
- package/dist/components/choiceContext.js +7 -0
- package/dist/components/frame.d.ts +2 -0
- package/dist/components/icons.d.ts +3 -0
- package/dist/components/icons.js +4 -1
- package/dist/components/interaction.d.ts +1 -1
- package/dist/entityStore.d.ts +1 -0
- package/dist/entityStore.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/media.d.ts +36 -0
- package/dist/media.js +74 -0
- package/dist/publishWire.d.ts +2 -0
- package/dist/publishWire.js +21 -1
- package/dist/react.d.ts +5 -0
- package/dist/react.js +4 -0
- package/dist/rules/checkApiRouting.js +1 -1
- package/dist/rules/lintPlugin.js +13 -4
- package/dist/rules/messages.d.ts +1 -0
- package/dist/rules/messages.js +1 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +1 -0
- package/dist/server/createServer.d.ts +5 -3
- package/dist/server/createServer.js +32 -2
- package/dist/server/env.d.ts +3 -1
- package/dist/server/imageHeader.d.ts +10 -0
- package/dist/server/imageHeader.js +159 -0
- package/dist/server/index.d.ts +6 -1
- package/dist/server/index.js +3 -0
- package/dist/server/jsonLd.d.ts +45 -0
- package/dist/server/jsonLd.js +88 -0
- package/dist/server/media.d.ts +16 -0
- package/dist/server/media.js +135 -0
- package/dist/server/pages.d.ts +72 -0
- package/dist/server/pages.js +301 -0
- package/dist/server/tables.d.ts +3 -0
- package/dist/server/tables.js +23 -8
- package/dist/start.js +26 -1
- package/dist/table.d.ts +1 -0
- package/dist/table.js +8 -1
- package/dist/tables.d.ts +2 -1
- package/dist/tables.js +10 -1
- 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`). `
|
|
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
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
`
|
|
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,
|
|
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
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 ?
|
|
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 = {
|
package/dist/cli/publishing.js
CHANGED
|
@@ -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:
|
|
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?:
|
|
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 {};
|
package/dist/components/Card.js
CHANGED
|
@@ -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 {
|
|
4
|
-
|
|
5
|
-
|
|
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 {};
|