@trypattern/sdk 0.1.13 → 0.1.15

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 CHANGED
@@ -117,7 +117,7 @@ email, phone, address, notes, status }` with `status` `requested`, `accepted`, `
117
117
  shows the live photos without storing them. With
118
118
  `pages` (`definePages(tables, { '/': head, '/items/:itemId': { head, paths } })` in
119
119
  `server/pages.ts`) it renders the `<head>` of public pages: each `head` reads tables anyone may
120
- read (`rows`, `row`, `photoUrl`) and returns `{ title, description, image, hero, jsonLd, data }`
120
+ read (`rows`, `row`, `photoUrl`), and `appTitle()` gives the site's name as the owner set it, and returns `{ title, description, image, hero, jsonLd, data }`
121
121
  (or `null` for a 404). The Worker puts the title, description, canonical link, `og:*` (the image
122
122
  at 1200×630 with `fit=cover`), `twitter:card`, JSON-LD, a preload of the `hero` with `Image`'s
123
123
  srcset, and `data` as `<script type="application/json" id="pattern-data">` into the built
package/dist/cli/app.js CHANGED
@@ -330,7 +330,8 @@ moves forward with any new migrations; nothing is ever deleted from it. If a ste
330
330
  keeps running the previous version and the error says why. Run app check first.
331
331
  --message '…' says what changed in a few plain words for the owner ("Pink brand colour and a welcome
332
332
  line"); onpattern.app and app versions show it beside the version.
333
- --starter (Pattern's admins only) publishes this project as the starter every new site begins from.
333
+ --starter (Pattern's admins only) publishes this project as the starter new sites begin from: the one
334
+ for its "blueprint" in pattern.json (website or web-app), which a new business picks when it starts.
334
335
  With a starter/ folder, its rows (starter/rows.ts) and photos (starter/photos) go with it, and a new
335
336
  site's first publish puts them in its own database and photo bucket: the photos through the site's
336
337
  own POST /api/media, then the rows. Later publishes never touch them; they are the owner's.`,
@@ -6,7 +6,7 @@ import { z } from 'zod';
6
6
  import { DASHBOARD_URL, UPLOAD, appSummary, isPrivatePath, loginPoll, loginStarted, mediaRow, nameProblem, publishMessage, publishSummary, setupCodeExchanged, sourcePathProblem } from "../publishWire.js";
7
7
  import { appInfo } from "../appTitle.js";
8
8
  import { SESSION_COOKIE } from "../auth/user.js";
9
- import { PROJECT_FILE, readProjectFile } from "../vite/projectFile.js";
9
+ import { PROJECT_FILE, blueprintOf, readProjectFile } from "../vite/projectFile.js";
10
10
  const CONFIG_DIR = join(homedir(), '.config', 'pattern');
11
11
  const CREDENTIALS_FILE = join(CONFIG_DIR, 'credentials.json');
12
12
  const PENDING_FILE = join(CONFIG_DIR, 'pending-login.json');
@@ -325,7 +325,8 @@ export async function pull(io, name, cwd, setupCode) {
325
325
  }
326
326
  if (existsSync(join(target, LAUNCHER)))
327
327
  chmodSync(join(target, LAUNCHER), 0o755);
328
- writeFileSync(join(target, PROJECT_FILE), `${JSON.stringify({ name, title: result.title }, null, 2)}\n`);
328
+ const blueprint = blueprintOf(target);
329
+ writeFileSync(join(target, PROJECT_FILE), `${JSON.stringify({ name, title: result.title, ...(blueprint === undefined ? {} : { blueprint }) }, null, 2)}\n`);
329
330
  io.say(`✓ Downloaded ${files} files of version ${result.number}`);
330
331
  if (!(await ensurePnpm(io, target))) {
331
332
  return {
@@ -492,12 +493,18 @@ async function followPublish(io, name, started) {
492
493
  export async function publish(io, root, isStarter, message, starter) {
493
494
  try {
494
495
  sayHost(io);
495
- const { name } = readProjectFile(root);
496
+ const { name, blueprint } = readProjectFile(root);
497
+ if (isStarter && blueprint === undefined) {
498
+ throw new Error(`${PROJECT_FILE} needs "blueprint" ("website" or "web-app") to publish the starter for it`);
499
+ }
496
500
  const built = await io.build();
497
501
  if (built.code !== 0)
498
502
  return built;
499
503
  io.say('✓ Built');
500
504
  const { form, sources } = uploadForm(root, io, isStarter, starter);
505
+ if (isStarter && blueprint !== undefined) {
506
+ form.append(UPLOAD.blueprint, blueprint);
507
+ }
501
508
  const said = publishMessage(message ?? '');
502
509
  if (said !== null)
503
510
  form.append(UPLOAD.message, said);
@@ -521,8 +528,8 @@ export async function publish(io, root, isStarter, message, starter) {
521
528
  if (result === undefined) {
522
529
  return {
523
530
  code: 0,
524
- data: { starter: io.sdkVersion },
525
- text: `✓ Published the starter for SDK ${io.sdkVersion}`
531
+ data: { starter: io.sdkVersion, blueprint },
532
+ text: `✓ Published the ${blueprint} starter for SDK ${io.sdkVersion}`
526
533
  };
527
534
  }
528
535
  return await followPublish(io, name, result);
@@ -0,0 +1,4 @@
1
+ import type { RadioOption } from './RadioGroup.tsx';
2
+ export declare function RadioDescribed<Value extends string>({ option }: {
3
+ option: RadioOption<Value>;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as stylex from '@stylexjs/stylex';
3
+ import { space } from "../theme/tokens.stylex.js";
4
+ import { Text } from "./Text.js";
5
+ export function RadioDescribed({ option }) {
6
+ const tone = option.isDisabled === true ? 'tertiary' : undefined;
7
+ return (_jsxs("span", { ...stylex.props(styles.described), children: [_jsxs("span", { ...stylex.props(styles.labelLine), children: [_jsx(Text, { variant: "label", tone: tone ?? 'primary', children: option.label }), option.badge] }), _jsx(Text, { variant: "body", tone: tone ?? 'secondary', children: option.description })] }));
8
+ }
9
+ const styles = stylex.create({
10
+ described: {
11
+ display: 'flex',
12
+ flexDirection: 'column',
13
+ gap: space.xxs
14
+ },
15
+ labelLine: {
16
+ display: 'flex',
17
+ gap: space.sm,
18
+ alignItems: 'center'
19
+ }
20
+ });
@@ -3,6 +3,8 @@ import type { Placement } from './Placement.ts';
3
3
  export type RadioOption<Value extends string> = {
4
4
  value: Value;
5
5
  label: string;
6
+ description?: string;
7
+ badge?: ReactNode;
6
8
  content?: ReactNode;
7
9
  trailing?: ReactNode;
8
10
  isDisabled?: boolean;
@@ -2,9 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Radio as BaseRadio } from '@base-ui/react/radio';
3
3
  import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group';
4
4
  import * as stylex from '@stylexjs/stylex';
5
- import { borders, colors, iconSizes, motion, radii, shadows, space } from "../theme/tokens.stylex.js";
5
+ import { borders, colors, iconSizes, lineHeights, motion, radii, shadows, space } from "../theme/tokens.stylex.js";
6
6
  import { focusRing } from "./controlLook.js";
7
7
  import { interact } from "./interaction.js";
8
+ import { RadioDescribed } from "./RadioDescribed.js";
8
9
  import { Text } from "./Text.js";
9
10
  export function RadioGroup({ label, value, options, onChange, size = 'md', style }) {
10
11
  return (_jsx(BaseRadioGroup, { "aria-label": label, value: value, onValueChange: (next) => {
@@ -18,7 +19,8 @@ export function RadioGroup({ label, value, options, onChange, size = 'md', style
18
19
  });
19
20
  onChange(chosen.value);
20
21
  }, className: stylex.props(styles.group, size === 'lg' && styles.groupLarge, style)
21
- .className, children: options.map((option) => (_jsxs("label", { ...stylex.props(styles.row, size === 'lg' && styles.rowLarge, option.isDisabled === true && styles.rowDisabled), children: [_jsx(BaseRadio.Root, { value: option.value, disabled: option.isDisabled === true, "aria-label": option.label, className: (state) => stylex.props(styles.dot, state.checked ? styles.dotOn : styles.dotOff, focusRing.visible).className, children: _jsx(BaseRadio.Indicator, { className: stylex.props(styles.mark).className }) }), _jsx("span", { ...stylex.props(styles.content), children: option.content ?? (_jsx(Text, { variant: size === 'lg' ? 'lead' : 'body', tone: option.isDisabled === true ? 'tertiary' : 'primary', isTruncated: true, children: option.label })) }), option.trailing === undefined ? null : (_jsx("span", { ...stylex.props(styles.trailing), children: option.trailing }))] }, option.value))) }));
22
+ .className, children: options.map((option) => (_jsxs("label", { ...stylex.props(styles.row, size === 'lg' && styles.rowLarge, option.description !== undefined && styles.rowDescribed, option.isDisabled === true && styles.rowDisabled), children: [_jsx("span", { ...stylex.props(styles.dotLine), children: _jsx(BaseRadio.Root, { value: option.value, disabled: option.isDisabled === true, "aria-label": option.label, className: (state) => stylex.props(styles.dot, state.checked ? styles.dotOn : styles.dotOff, option.isDisabled === true && styles.dotDisabled, focusRing.visible).className, children: _jsx(BaseRadio.Indicator, { className: stylex.props(styles.mark).className }) }) }), _jsx("span", { ...stylex.props(styles.content), children: option.content ??
23
+ (option.description === undefined ? (_jsx(Text, { variant: size === 'lg' ? 'lead' : 'body', tone: option.isDisabled === true ? 'tertiary' : 'primary', isTruncated: true, children: option.label })) : (_jsx(RadioDescribed, { option: option }))) }), option.trailing === undefined ? null : (_jsx("span", { ...stylex.props(styles.trailing), children: option.trailing }))] }, option.value))) }));
22
24
  }
23
25
  const styles = stylex.create({
24
26
  group: {
@@ -50,9 +52,20 @@ const styles = stylex.create({
50
52
  paddingBlock: space.lg,
51
53
  paddingInline: `calc(${space.md} + ${space.xxs})`
52
54
  },
55
+ rowDescribed: {
56
+ gap: `calc(${space.sm} + ${space.xxs})`,
57
+ alignItems: 'flex-start',
58
+ paddingBlock: `calc(${space.md} + ${space.xxs})`
59
+ },
53
60
  rowDisabled: {
54
61
  color: colors.tertiaryForeground
55
62
  },
63
+ dotLine: {
64
+ display: 'flex',
65
+ flexShrink: 0,
66
+ alignItems: 'center',
67
+ height: lineHeights.bodySm
68
+ },
56
69
  dot: {
57
70
  display: 'grid',
58
71
  flexShrink: 0,
@@ -71,6 +84,10 @@ const styles = stylex.create({
71
84
  backgroundColor: colors.componentOnContainerStrong,
72
85
  borderColor: colors.borderStrong
73
86
  },
87
+ dotDisabled: {
88
+ backgroundColor: colors.disabled,
89
+ borderColor: colors.borderDefault
90
+ },
74
91
  dotOn: {
75
92
  backgroundColor: colors.invertedSolid,
76
93
  borderColor: colors.invertedSolid
@@ -22,7 +22,14 @@ export declare const UPLOAD: {
22
22
  readonly message: "message";
23
23
  readonly runWorkerFirst: "runWorkerFirst";
24
24
  readonly starter: "starter";
25
+ readonly blueprint: "blueprint";
25
26
  };
27
+ export declare const BLUEPRINTS: readonly ["website", "web-app"];
28
+ export declare const blueprintName: z.ZodEnum<{
29
+ website: "website";
30
+ "web-app": "web-app";
31
+ }>;
32
+ export type BlueprintName = z.output<typeof blueprintName>;
26
33
  export declare const starterContent: z.ZodObject<{
27
34
  photos: z.ZodArray<z.ZodObject<{
28
35
  id: z.ZodString;
@@ -371,6 +378,10 @@ export declare const publishServices: {
371
378
  readonly create: NoInfer<import("./services.ts").ServiceCall<"/apps", {
372
379
  name: z.ZodString;
373
380
  title: z.ZodString;
381
+ blueprint: z.ZodEnum<{
382
+ website: "website";
383
+ "web-app": "web-app";
384
+ }>;
374
385
  }, z.ZodObject<{
375
386
  name: z.ZodString;
376
387
  title: z.ZodString;
@@ -93,8 +93,11 @@ export const UPLOAD = {
93
93
  sdkVersion: 'sdkVersion',
94
94
  message: 'message',
95
95
  runWorkerFirst: 'runWorkerFirst',
96
- starter: 'starter'
96
+ starter: 'starter',
97
+ blueprint: 'blueprint'
97
98
  };
99
+ export const BLUEPRINTS = ['website', 'web-app'];
100
+ export const blueprintName = z.enum(BLUEPRINTS);
98
101
  export const starterContent = z.object({
99
102
  photos: z.array(z.object({
100
103
  id: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/),
@@ -259,7 +262,7 @@ export const publishServices = defineServices({
259
262
  read: call({ get: '/apps/:name', reply: appSummary }),
260
263
  create: call({
261
264
  post: '/apps',
262
- input: { name: appName, title: appTitle },
265
+ input: { name: appName, title: appTitle, blueprint: blueprintName },
263
266
  reply: appSummary
264
267
  })
265
268
  },
@@ -14,6 +14,7 @@ export type PageRequest<Group extends Tables> = {
14
14
  rows: <Name extends ReadableName<Group>>(table: Name) => Promise<readonly Row<Group[Name]>[]>;
15
15
  row: <Name extends ReadableName<Group>>(table: Name, id: string) => Promise<Row<Group[Name]> | undefined>;
16
16
  photoUrl: (image: MediaImage) => string;
17
+ appTitle: () => Promise<string>;
17
18
  };
18
19
  export type PageHero = {
19
20
  image: MediaImage;
@@ -49,6 +50,7 @@ type AnyRequest = {
49
50
  rows: (table: string) => Promise<readonly unknown[]>;
50
51
  row: (table: string, id: string) => Promise<unknown>;
51
52
  photoUrl: (image: MediaImage) => string;
53
+ appTitle: () => Promise<string>;
52
54
  };
53
55
  type PageRoute = {
54
56
  pattern: string;
@@ -71,6 +71,7 @@ function readableTable(pages, name) {
71
71
  function requestFor(c, pages, params) {
72
72
  const url = new URL(c.req.url);
73
73
  const read = new Map();
74
+ let title;
74
75
  return {
75
76
  url,
76
77
  params,
@@ -81,7 +82,11 @@ function requestFor(c, pages, params) {
81
82
  return found;
82
83
  },
83
84
  row: (name, id) => readRow(c.env.DB, readableTable(pages, name), id),
84
- photoUrl: (image) => `${url.origin}${mediaPath(image.key)}`
85
+ photoUrl: (image) => `${url.origin}${mediaPath(image.key)}`,
86
+ appTitle: () => {
87
+ title ??= readTitle(c.env.DB);
88
+ return title;
89
+ }
85
90
  };
86
91
  }
87
92
  function escapeHtml(text) {
@@ -217,7 +222,7 @@ export async function renderPage(c, pages, route, params) {
217
222
  try {
218
223
  head = await route.head(request);
219
224
  if (head !== null) {
220
- tags = headTags(pages, request.url, await readTitle(c.env.DB), head);
225
+ tags = headTags(pages, request.url, await request.appTitle(), head);
221
226
  }
222
227
  }
223
228
  catch (error) {
@@ -3,7 +3,12 @@ export declare const PROJECT_FILE = "pattern.json";
3
3
  declare const projectFile: z.ZodObject<{
4
4
  name: z.ZodString;
5
5
  title: z.ZodString;
6
+ blueprint: z.ZodOptional<z.ZodEnum<{
7
+ website: "website";
8
+ "web-app": "web-app";
9
+ }>>;
6
10
  }, z.core.$strip>;
7
11
  export type ProjectFile = z.output<typeof projectFile>;
8
12
  export declare function readProjectFile(root: string): ProjectFile;
13
+ export declare function blueprintOf(root: string): "website" | "web-app" | undefined;
9
14
  export {};
@@ -2,8 +2,13 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { z } from 'zod';
4
4
  import { appTitle } from "../appTitle.js";
5
+ import { blueprintName } from "../publishWire.js";
5
6
  export const PROJECT_FILE = 'pattern.json';
6
- const projectFile = z.object({ name: z.string().min(1), title: appTitle });
7
+ const projectFile = z.object({
8
+ name: z.string().min(1),
9
+ title: appTitle,
10
+ blueprint: blueprintName.optional()
11
+ });
7
12
  export function readProjectFile(root) {
8
13
  const path = join(root, PROJECT_FILE);
9
14
  if (!existsSync(path)) {
@@ -15,3 +20,16 @@ export function readProjectFile(root) {
15
20
  }
16
21
  return parsed.data;
17
22
  }
23
+ export function blueprintOf(root) {
24
+ const path = join(root, PROJECT_FILE);
25
+ if (!existsSync(path))
26
+ return undefined;
27
+ try {
28
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
29
+ const parsed = blueprintName.safeParse(Reflect.get(Object(raw), 'blueprint'));
30
+ return parsed.success ? parsed.data : undefined;
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trypattern/sdk",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",