@trypattern/sdk 0.1.33 → 0.1.35

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 (46) hide show
  1. package/dist/app.d.ts +6 -2
  2. package/dist/app.js +6 -1
  3. package/dist/blocks.js +11 -3
  4. package/dist/bookings.d.ts +78 -0
  5. package/dist/bookings.js +7 -1
  6. package/dist/bookingsStore.d.ts +78 -0
  7. package/dist/cli/app.js +17 -2
  8. package/dist/cli/requests.d.ts +5 -0
  9. package/dist/cli/requests.js +115 -0
  10. package/dist/components/AppFrame.js +12 -2
  11. package/dist/components/Icon.js +1 -1
  12. package/dist/components/MenuItems.d.ts +1 -0
  13. package/dist/components/MenuItems.js +13 -2
  14. package/dist/components/SegmentedControl.d.ts +1 -0
  15. package/dist/components/SegmentedControl.js +9 -2
  16. package/dist/components/Sidebar.d.ts +1 -0
  17. package/dist/components/Sidebar.js +18 -2
  18. package/dist/components/TableRow.d.ts +2 -1
  19. package/dist/components/TableRow.js +5 -2
  20. package/dist/components/icons.js +1 -2
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.js +1 -0
  23. package/dist/inquiriesStore.d.ts +134 -0
  24. package/dist/inquiriesStore.js +64 -0
  25. package/dist/patternAccess.d.ts +8 -2
  26. package/dist/patternAccess.js +17 -5
  27. package/dist/publishWire.d.ts +866 -0
  28. package/dist/publishWire.js +78 -0
  29. package/dist/requests.d.ts +361 -0
  30. package/dist/requests.js +180 -0
  31. package/dist/schema.d.ts +2 -0
  32. package/dist/schema.js +1 -0
  33. package/dist/server/authService.d.ts +1 -0
  34. package/dist/server/bookings.d.ts +2 -9
  35. package/dist/server/bookings.js +149 -31
  36. package/dist/server/createServer.js +4 -0
  37. package/dist/server/env.d.ts +3 -0
  38. package/dist/server/index.d.ts +1 -0
  39. package/dist/server/index.js +1 -0
  40. package/dist/server/inquiries.d.ts +9 -0
  41. package/dist/server/inquiries.js +150 -0
  42. package/dist/server/patternViewer.js +7 -1
  43. package/dist/server/requests.d.ts +76 -0
  44. package/dist/server/requests.js +328 -0
  45. package/dist/server/services.js +9 -1
  46. package/package.json +1 -1
package/dist/app.d.ts CHANGED
@@ -4,6 +4,8 @@ import { AppStore } from './appInfo.ts';
4
4
  import type { AppBlock } from './blocks.ts';
5
5
  import type { bookingRequests } from './bookings.ts';
6
6
  import type { Bookings } from './bookingsStore.ts';
7
+ import type { Inquiries } from './inquiriesStore.ts';
8
+ import { requestOperations } from './requests.ts';
7
9
  import { SessionStore } from './auth/sessionStore.ts';
8
10
  import type { Cache } from './cache.ts';
9
11
  import type { Clock } from './clock.ts';
@@ -74,11 +76,13 @@ export type PublicStores<Group extends Tables> = {
74
76
  never
75
77
  ] ? never : `public${Capitalize<Key>}`]: PublicTableStore<Group[Key]>;
76
78
  };
77
- export type BlockStores<Group extends Tables> = [
79
+ export type BlockStores<Group extends Tables> = ([
78
80
  typeof bookingRequests
79
81
  ] extends [Group[keyof Group]] ? {
80
82
  bookings: Bookings;
81
- } : Record<never, never>;
83
+ } : Record<never, never>) & ([typeof requestOperations] extends [Group[keyof Group]] ? {
84
+ inquiries: Inquiries;
85
+ } : Record<never, never>);
82
86
  export type StoresOf<Group extends Tables, Custom, Item extends Action = Action> = {
83
87
  session: SessionStore;
84
88
  app: AppStore;
package/dist/app.js CHANGED
@@ -2,6 +2,8 @@ import { z } from 'zod';
2
2
  import { viewerOf } from "./access.js";
3
3
  import { AppStore } from "./appInfo.js";
4
4
  import { blockWiringProblem } from "./blocks.js";
5
+ import { inquiries } from "./inquiriesStore.js";
6
+ import { requestOperations } from "./requests.js";
5
7
  import { SessionStore } from "./auth/sessionStore.js";
6
8
  import { realClock } from "./clock.js";
7
9
  import { ActionsStore } from "./actions.js";
@@ -75,7 +77,10 @@ function withBlocks(definition, blocks) {
75
77
  return { ...definition, services, memory };
76
78
  }
77
79
  export function defineApp(definition) {
78
- const blocks = definition.blocks ?? [];
80
+ const listed = definition.blocks ?? [];
81
+ const takesRequests = Object.values(definition.tables).includes(requestOperations) &&
82
+ !listed.some((block) => block.name === inquiries.name);
83
+ const blocks = takesRequests ? [...listed, inquiries] : listed;
79
84
  const wiring = blockWiringProblem(definition.tables, blocks, 'app');
80
85
  if (wiring !== undefined)
81
86
  throw new Error(wiring);
package/dist/blocks.js CHANGED
@@ -1,10 +1,17 @@
1
- import { bookingTables } from "./bookings.js";
1
+ import { bookingBlackouts, bookingRequests, bookingRules } from "./bookings.js";
2
+ import { requestTables } from "./requests.js";
2
3
  const KNOWN_BLOCKS = [
3
4
  {
4
5
  name: 'bookings',
5
- tables: bookingTables,
6
+ tables: { bookingRules, bookingBlackouts, bookingRequests },
6
7
  tablesExport: 'bookingTables',
7
8
  server: "bookings({ timezone: 'America/Toronto' })"
9
+ },
10
+ {
11
+ name: 'inquiries',
12
+ tables: requestTables,
13
+ tablesExport: 'requestTables',
14
+ server: 'inquiries([orders])'
8
15
  }
9
16
  ];
10
17
  function includesAll(tables, wanted) {
@@ -26,8 +33,9 @@ export function blockWiringProblem(tables, blocks, side) {
26
33
  for (const known of KNOWN_BLOCKS) {
27
34
  if (!includesAny(tables, known.tables))
28
35
  continue;
29
- if (blocks.some((block) => block.name === known.name))
36
+ if (blocks.some((block) => includesAll(block.tables, known.tables))) {
30
37
  continue;
38
+ }
31
39
  return side === 'server'
32
40
  ? `tables has the ${known.name} block's tables, so the server needs the block: add blocks: [${known.server}] from '@trypattern/sdk/server' to createServer in server/worker.ts`
33
41
  : `tables has the ${known.name} block's tables, so the app needs the block: add blocks: [${known.name}] from '@trypattern/sdk' to defineApp in src/App.ts`;
@@ -64,6 +64,84 @@ export declare const bookingRequests: import("./table.ts").TableOf<"bookingReque
64
64
  readonly writes: "confirmed";
65
65
  }>;
66
66
  export declare const bookingTables: {
67
+ requestStates: import("./table.ts").TableOf<"requestStates", {
68
+ readonly columns: {
69
+ readonly source: z.ZodString;
70
+ readonly requestId: z.ZodString;
71
+ readonly state: z.ZodEnum<{
72
+ done: "done";
73
+ open: "open";
74
+ }>;
75
+ readonly revision: z.ZodNumber;
76
+ readonly sequence: z.ZodNumber;
77
+ readonly updatedAt: z.ZodString;
78
+ };
79
+ readonly access: {
80
+ readonly read: "server";
81
+ readonly create: "server";
82
+ readonly update: "server";
83
+ readonly delete: "server";
84
+ };
85
+ readonly writes: "confirmed";
86
+ }>;
87
+ requestEvents: import("./table.ts").TableOf<"requestEvents", {
88
+ readonly columns: {
89
+ readonly source: z.ZodString;
90
+ readonly requestId: z.ZodString;
91
+ readonly kind: z.ZodEnum<{
92
+ done: "done";
93
+ accepted: "accepted";
94
+ declined: "declined";
95
+ replied: "replied";
96
+ received: "received";
97
+ }>;
98
+ };
99
+ readonly access: {
100
+ readonly read: "server";
101
+ readonly create: "server";
102
+ readonly update: "server";
103
+ readonly delete: "server";
104
+ };
105
+ readonly writes: "confirmed";
106
+ }>;
107
+ requestOutbox: import("./table.ts").TableOf<"requestOutbox", {
108
+ readonly columns: {
109
+ readonly source: z.ZodString;
110
+ readonly requestId: z.ZodString;
111
+ readonly subject: z.ZodString;
112
+ readonly text: z.ZodString;
113
+ readonly replyTo: z.ZodString;
114
+ readonly status: z.ZodEnum<{
115
+ failed: "failed";
116
+ pending: "pending";
117
+ sent: "sent";
118
+ sending: "sending";
119
+ }>;
120
+ readonly attempts: z.ZodNumber;
121
+ readonly nextAt: z.ZodNullable<z.ZodString>;
122
+ readonly sentAt: z.ZodNullable<z.ZodString>;
123
+ readonly claimedAt: z.ZodNullable<z.ZodString>;
124
+ };
125
+ readonly access: {
126
+ readonly read: "server";
127
+ readonly create: "server";
128
+ readonly update: "server";
129
+ readonly delete: "server";
130
+ };
131
+ readonly writes: "confirmed";
132
+ }>;
133
+ requestOperations: import("./table.ts").TableOf<"requestOperations", {
134
+ readonly columns: {
135
+ readonly result: z.ZodString;
136
+ };
137
+ readonly access: {
138
+ readonly read: "server";
139
+ readonly create: "server";
140
+ readonly update: "server";
141
+ readonly delete: "server";
142
+ };
143
+ readonly writes: "confirmed";
144
+ }>;
67
145
  bookingRules: import("./table.ts").TableOf<"bookingRules", {
68
146
  readonly columns: {
69
147
  readonly windows: z.ZodString;
package/dist/bookings.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { requestTables } from "./requests.js";
2
3
  import { call, defineServices } from "./services.js";
3
4
  import { patternTable } from "./table.js";
4
5
  const DATE = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
@@ -78,7 +79,12 @@ export const bookingRequests = patternTable('bookingRequests', {
78
79
  },
79
80
  writes: 'confirmed'
80
81
  });
81
- export const bookingTables = { bookingRules, bookingBlackouts, bookingRequests };
82
+ export const bookingTables = {
83
+ bookingRules,
84
+ bookingBlackouts,
85
+ bookingRequests,
86
+ ...requestTables
87
+ };
82
88
  export const bookingWindow = z.object({
83
89
  weekday: z.enum(WEEKDAYS),
84
90
  label: z.string().min(1).max(60),
@@ -281,6 +281,84 @@ export declare class Bookings extends Bookings_base {
281
281
  export declare const bookings: {
282
282
  readonly name: "bookings";
283
283
  readonly tables: {
284
+ requestStates: import("./table.ts").TableOf<"requestStates", {
285
+ readonly columns: {
286
+ readonly source: z.ZodString;
287
+ readonly requestId: z.ZodString;
288
+ readonly state: z.ZodEnum<{
289
+ done: "done";
290
+ open: "open";
291
+ }>;
292
+ readonly revision: z.ZodNumber;
293
+ readonly sequence: z.ZodNumber;
294
+ readonly updatedAt: z.ZodString;
295
+ };
296
+ readonly access: {
297
+ readonly read: "server";
298
+ readonly create: "server";
299
+ readonly update: "server";
300
+ readonly delete: "server";
301
+ };
302
+ readonly writes: "confirmed";
303
+ }>;
304
+ requestEvents: import("./table.ts").TableOf<"requestEvents", {
305
+ readonly columns: {
306
+ readonly source: z.ZodString;
307
+ readonly requestId: z.ZodString;
308
+ readonly kind: z.ZodEnum<{
309
+ done: "done";
310
+ accepted: "accepted";
311
+ declined: "declined";
312
+ replied: "replied";
313
+ received: "received";
314
+ }>;
315
+ };
316
+ readonly access: {
317
+ readonly read: "server";
318
+ readonly create: "server";
319
+ readonly update: "server";
320
+ readonly delete: "server";
321
+ };
322
+ readonly writes: "confirmed";
323
+ }>;
324
+ requestOutbox: import("./table.ts").TableOf<"requestOutbox", {
325
+ readonly columns: {
326
+ readonly source: z.ZodString;
327
+ readonly requestId: z.ZodString;
328
+ readonly subject: z.ZodString;
329
+ readonly text: z.ZodString;
330
+ readonly replyTo: z.ZodString;
331
+ readonly status: z.ZodEnum<{
332
+ failed: "failed";
333
+ pending: "pending";
334
+ sent: "sent";
335
+ sending: "sending";
336
+ }>;
337
+ readonly attempts: z.ZodNumber;
338
+ readonly nextAt: z.ZodNullable<z.ZodString>;
339
+ readonly sentAt: z.ZodNullable<z.ZodString>;
340
+ readonly claimedAt: z.ZodNullable<z.ZodString>;
341
+ };
342
+ readonly access: {
343
+ readonly read: "server";
344
+ readonly create: "server";
345
+ readonly update: "server";
346
+ readonly delete: "server";
347
+ };
348
+ readonly writes: "confirmed";
349
+ }>;
350
+ requestOperations: import("./table.ts").TableOf<"requestOperations", {
351
+ readonly columns: {
352
+ readonly result: z.ZodString;
353
+ };
354
+ readonly access: {
355
+ readonly read: "server";
356
+ readonly create: "server";
357
+ readonly update: "server";
358
+ readonly delete: "server";
359
+ };
360
+ readonly writes: "confirmed";
361
+ }>;
284
362
  bookingRules: import("./table.ts").TableOf<"bookingRules", {
285
363
  readonly columns: {
286
364
  readonly windows: z.ZodString;
package/dist/cli/app.js CHANGED
@@ -22,6 +22,7 @@ import { readStarter, starterContentOf, withStarter } from "./starter.js";
22
22
  import { addLiveMedia, login, publish, pull, restore, versions as listVersions } from "./publishing.js";
23
23
  import { CHANGE_SET_ROWS } from "../publishWire.js";
24
24
  import { DATA_USAGE, data } from "./data.js";
25
+ import { REQUESTS_USAGE, requests } from "./requests.js";
25
26
  import { withStartingValues } from "./startingValues.js";
26
27
  import { toolBin } from "./tools.js";
27
28
  import { isNewer, latestPattern, newerPattern, updateNote } from "./updates.js";
@@ -93,7 +94,8 @@ const VALUE_FLAGS = new Set([
93
94
  '--row',
94
95
  '--set',
95
96
  '--file',
96
- '--asked'
97
+ '--asked',
98
+ '--text'
97
99
  ]);
98
100
  const PHOTO_TYPES = {
99
101
  '.jpg': 'image/jpeg',
@@ -379,6 +381,19 @@ collected changes. Up to ${CHANGE_SET_ROWS} changes per request. For this comput
379
381
  argumentCount: 3,
380
382
  flags: ['--row', '--set', '--file', '--asked']
381
383
  },
384
+ requests: {
385
+ summary: 'Read the live site’s requests and write replies for the owner (app requests --help)',
386
+ details: `${REQUESTS_USAGE}
387
+ Requests are what visitors send from the live site through a request source: bookings, and each
388
+ inquiry() form. The owner reads and answers them on onpattern.app (Requests), and gets an email for
389
+ each one. With no command this lists them as source/id; show prints one. draft writes a reply for the
390
+ owner to read on onpattern.app: it waits there with --asked (their request, in their words) beside
391
+ it, and nothing reaches the customer until the owner presses Send there. Accepting or declining a
392
+ booking, and marking a request done, are the owner's to do on onpattern.app.`,
393
+ run: (_argument, options, rest) => requests(publishingIo(), ROOT, rest, options),
394
+ argumentCount: 2,
395
+ flags: ['--text', '--file', '--asked']
396
+ },
382
397
  update: {
383
398
  summary: 'Bring this project to the newest Pattern',
384
399
  details: `Installs the newest @trypattern/sdk from npm into this project (pnpm add, exact version).
@@ -1617,7 +1632,7 @@ async function update() {
1617
1632
  return {
1618
1633
  code: 0,
1619
1634
  data: { current, latest, updated: true },
1620
- text: `✓ Updated Pattern ${current} → ${latest}\n next run app check, then app publish --message 'Updated Pattern' (straight away if nothing else is in progress) so the live site gets it`
1635
+ text: `✓ Updated Pattern ${current} → ${latest}\n next run app migration pattern_${latest.replaceAll('.', '_')} (it writes nothing when Pattern's own tables didn't change), then app check, then app publish --message 'Updated Pattern' (straight away if nothing else is in progress) so the live site gets it`
1621
1636
  };
1622
1637
  }
1623
1638
  async function setup() {
@@ -0,0 +1,5 @@
1
+ import type { Outcome, PublishingIo } from './publishing.ts';
2
+ export declare const REQUESTS_USAGE = "app requests\napp requests show <source/id>\napp requests draft <source/id> --text \"<the reply>\" --asked \"<the owner's request, in their words>\"\napp requests draft <source/id> --file reply.txt --asked \"\u2026\"";
3
+ type RequestOptions = ReadonlyMap<string, string | true>;
4
+ export declare function requests(io: PublishingIo, root: string, [action, key]: readonly (string | undefined)[], options: RequestOptions): Promise<Outcome>;
5
+ export {};
@@ -0,0 +1,115 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { DASHBOARD_URL, requestDraft, requestList } from "../publishWire.js";
4
+ import { readProjectFile } from "../vite/projectFile.js";
5
+ import { callJson, failure, isOutcome, sayHost, signedIn } from "./publishing.js";
6
+ export const REQUESTS_USAGE = `app requests
7
+ app requests show <source/id>
8
+ app requests draft <source/id> --text "<the reply>" --asked "<the owner's request, in their words>"
9
+ app requests draft <source/id> --file reply.txt --asked "…"`;
10
+ function refusal(text, code = 1) {
11
+ return { code, data: { error: text }, text: `✗ ${text}` };
12
+ }
13
+ function valueOf(options, flag) {
14
+ const value = options.get(flag);
15
+ return typeof value === 'string' ? value : undefined;
16
+ }
17
+ function lineOf(entry) {
18
+ const when = entry.when === null ? '' : ` ${entry.when}`;
19
+ return ` ${entry.source}/${entry.id} ${entry.status} ${entry.name} <${entry.email}> ${entry.label}${entry.summary === '' ? '' : ` · ${entry.summary}`}${when}`;
20
+ }
21
+ function detailOf(entry) {
22
+ return [
23
+ `${entry.title} from ${entry.name} <${entry.email}> (${entry.status})`,
24
+ ...entry.fields.map((field) => ` ${field.label}: ${field.value}`),
25
+ ...entry.contact.map((field) => ` ${field.label}: ${field.value}`),
26
+ ...(entry.message === '' ? [] : ['', entry.message]),
27
+ ...entry.messages.map((message) => ` ${message.kind} (${message.status}): ${message.text}`),
28
+ ...(entry.draft === null
29
+ ? []
30
+ : [` draft waiting for the owner: ${entry.draft.text}`])
31
+ ];
32
+ }
33
+ async function listed(io, name) {
34
+ return signedIn(io, 'app requests', () => callJson(`/api/apps/${name}/requests`, requestList));
35
+ }
36
+ function textGiven(root, options) {
37
+ const text = valueOf(options, '--text');
38
+ if (text !== undefined)
39
+ return text;
40
+ const file = valueOf(options, '--file');
41
+ if (file === undefined)
42
+ return undefined;
43
+ const path = resolve(root, file);
44
+ if (!existsSync(path))
45
+ throw new Error(`There is no file at ${file}`);
46
+ return readFileSync(path, 'utf8');
47
+ }
48
+ export async function requests(io, root, [action, key], options) {
49
+ try {
50
+ const { name } = readProjectFile(root);
51
+ sayHost(io);
52
+ const list = await listed(io, name);
53
+ if (isOutcome(list))
54
+ return list;
55
+ if (list.status === 'outdated') {
56
+ return refusal('Publish once with the latest Pattern so its requests reach onpattern.app');
57
+ }
58
+ if (action === undefined) {
59
+ const open = list.requests.filter((entry) => entry.needsAnswer);
60
+ return {
61
+ code: 0,
62
+ data: { requests: list.requests },
63
+ text: [
64
+ `${open.length} of ${list.requests.length} requests on ${name} need an answer`,
65
+ ...list.requests.map(lineOf)
66
+ ].join('\n')
67
+ };
68
+ }
69
+ const [source = '', id = ''] = (key ?? '').split('/');
70
+ const entry = list.requests.find((item) => item.source === source && item.id === id);
71
+ if (entry === undefined) {
72
+ return refusal(`Say which request as source/id, as app requests lists it\n${REQUESTS_USAGE}`, 2);
73
+ }
74
+ if (action === 'show') {
75
+ return {
76
+ code: 0,
77
+ data: { request: entry },
78
+ text: detailOf(entry).join('\n')
79
+ };
80
+ }
81
+ if (action !== 'draft') {
82
+ return refusal(`Unknown requests command: ${action}\n${REQUESTS_USAGE}`, 2);
83
+ }
84
+ if (!entry.actions.includes('reply')) {
85
+ return refusal(`${entry.title} can’t take a written reply: the owner answers it with ${entry.actions.join(' or ') || 'nothing now'} on onpattern.app`);
86
+ }
87
+ const text = textGiven(root, options);
88
+ const asked = valueOf(options, '--asked');
89
+ if (text === undefined || text.trim() === '') {
90
+ return refusal('Give the reply with --text "…" or --file reply.txt', 2);
91
+ }
92
+ if (asked === undefined || asked.trim() === '') {
93
+ return refusal('Say what the owner asked for with --asked "<their words>". They see it beside Send.', 2);
94
+ }
95
+ const draft = await signedIn(io, 'app requests draft', () => callJson(`/api/apps/${name}/requests/${source}/${id}/drafts`, requestDraft, {
96
+ method: 'POST',
97
+ headers: { 'content-type': 'application/json' },
98
+ body: JSON.stringify({ text: text.trim(), asked: asked.trim() })
99
+ }));
100
+ if (isOutcome(draft))
101
+ return draft;
102
+ const link = `${DASHBOARD_URL}/sites/${name}/requests`;
103
+ return {
104
+ code: 0,
105
+ data: { draft },
106
+ text: [
107
+ `✓ Your reply to ${entry.name} is waiting for the owner. Nothing was sent.`,
108
+ ` next tell them: "I've written a reply to ${entry.name}. Open ${link}, read it, change anything you like, then press Send."`
109
+ ].join('\n')
110
+ };
111
+ }
112
+ catch (error) {
113
+ return failure(error);
114
+ }
115
+ }
@@ -15,12 +15,22 @@ export function AppFrame({ look = 'shell', workspace, links, workspaceMenu = [],
15
15
  : [
16
16
  ...workspaceMenu,
17
17
  ...(workspaceMenu.length > 0 ? ['separator'] : []),
18
- { id: 'signOut', label: 'Log out', onSelect: account.signOut }
18
+ {
19
+ id: 'signOut',
20
+ label: 'Log out',
21
+ icon: 'logOut',
22
+ onSelect: account.signOut
23
+ }
19
24
  ];
20
25
  const accountEntries = [
21
26
  ...accountMenu,
22
27
  ...(accountMenu.length > 0 ? ['separator'] : []),
23
- { id: 'signOut', label: 'Log out', onSelect: account.signOut }
28
+ {
29
+ id: 'signOut',
30
+ label: 'Log out',
31
+ icon: 'logOut',
32
+ onSelect: account.signOut
33
+ }
24
34
  ];
25
35
  const search = onSearch === undefined
26
36
  ? undefined
@@ -5,7 +5,7 @@ import { shapeOf } from "./icons.js";
5
5
  import { tones } from "./tones.js";
6
6
  export function Icon({ name, size = 'md', tone = 'inherit', weight = 'regular' }) {
7
7
  const shape = shapeOf(name);
8
- return (_jsx("svg", { viewBox: "0 0 24 24", fill: shape.isFilled ? 'currentColor' : 'none', fillRule: shape.rule, stroke: shape.isFilled ? 'none' : 'currentColor', strokeWidth: weight === 'light' ? '1.5' : '2', strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", ...stylex.props(styles.icon, sizes[size], tones[tone]), children: shape.paths.map((path, index) => (_jsx("path", { d: path, fill: shape.colors?.[index] }, path))) }));
8
+ return (_jsx("svg", { viewBox: "0 0 24 24", fill: shape.isFilled ? 'currentColor' : 'none', fillRule: shape.rule, stroke: shape.isFilled ? 'none' : 'currentColor', strokeWidth: weight === 'light' ? '1.5' : '2', strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", ...stylex.props(styles.icon, sizes[size], tones[tone]), children: shape.colors === undefined ? (_jsx("path", { d: shape.paths.join(' ') })) : (shape.paths.map((path, index) => (_jsx("path", { d: path, fill: shape.colors?.[index] }, path)))) }));
9
9
  }
10
10
  const styles = stylex.create({
11
11
  icon: {
@@ -4,6 +4,7 @@ type MenuAction = {
4
4
  id: string;
5
5
  label: string;
6
6
  icon?: IconName;
7
+ mark?: string;
7
8
  isDanger?: boolean;
8
9
  } & ({
9
10
  onSelect: () => void;
@@ -2,11 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Menu as BaseMenu } from '@base-ui/react/menu';
3
3
  import { useNavigate } from 'react-router';
4
4
  import * as stylex from '@stylexjs/stylex';
5
- import { colors, space } from "../theme/tokens.stylex.js";
5
+ import { colors, iconSizes, radii, space } from "../theme/tokens.stylex.js";
6
6
  import { Divider } from "./Divider.js";
7
7
  import { Icon } from "./Icon.js";
8
8
  import { interact } from "./interaction.js";
9
9
  import { PopupItem } from "./PopupItem.js";
10
+ import { Text } from "./Text.js";
10
11
  export function MenuItems({ items, primitive }) {
11
12
  const navigate = useNavigate();
12
13
  return items.map((entry, index) => entry === 'separator' ? (_jsx(Divider, { style: styles.separator }, `separator-${index}`)) : (_jsxs(BaseMenu.Item, { onClick: () => {
@@ -15,7 +16,7 @@ export function MenuItems({ items, primitive }) {
15
16
  entry.onSelect();
16
17
  else
17
18
  void navigate(entry.to);
18
- }, render: (props, state) => (_jsx(PopupItem, { ...props, kind: "menu", isHighlighted: state.highlighted, isDanger: entry.isDanger ?? false })), children: [entry.icon === undefined ? null : (_jsx("span", { ...stylex.props(styles.icon, entry.isDanger && styles.dangerIcon), children: _jsx(Icon, { name: entry.icon }) })), entry.label] }, entry.id)));
19
+ }, render: (props, state) => (_jsx(PopupItem, { ...props, kind: "menu", isHighlighted: state.highlighted, isDanger: entry.isDanger ?? false })), children: [entry.mark === undefined ? null : (_jsx("span", { ...stylex.props(styles.mark), children: _jsx(Text, { variant: "caption", children: entry.mark.charAt(0) }) })), entry.icon === undefined ? null : (_jsx("span", { ...stylex.props(styles.icon, entry.isDanger && styles.dangerIcon), children: _jsx(Icon, { name: entry.icon }) })), entry.label] }, entry.id)));
19
20
  }
20
21
  const styles = stylex.create({
21
22
  separator: {
@@ -26,6 +27,16 @@ const styles = stylex.create({
26
27
  display: 'flex',
27
28
  color: colors.secondaryForeground
28
29
  },
30
+ mark: {
31
+ display: 'grid',
32
+ flexShrink: 0,
33
+ placeItems: 'center',
34
+ width: iconSizes.md,
35
+ height: iconSizes.md,
36
+ color: colors.onBrandSolidForeground,
37
+ backgroundColor: colors.brandSolid,
38
+ borderRadius: radii.sm
39
+ },
29
40
  dangerIcon: {
30
41
  color: 'inherit'
31
42
  }
@@ -3,6 +3,7 @@ import type { Size } from './Size.ts';
3
3
  export type SegmentedOption<Value extends string> = {
4
4
  value: Value;
5
5
  label: string;
6
+ count?: number;
6
7
  };
7
8
  type SegmentedControlProps<Value extends string> = {
8
9
  label: string;
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Radio } from '@base-ui/react/radio';
3
3
  import { RadioGroup } from '@base-ui/react/radio-group';
4
4
  import * as stylex from '@stylexjs/stylex';
5
- import { borders, colors, fontWeights, motion, radii } from "../theme/tokens.stylex.js";
5
+ import { borders, colors, fontSizes, fontWeights, lineHeights, motion, radii, space } from "../theme/tokens.stylex.js";
6
6
  import { interact } from "./interaction.js";
7
7
  import { buttonCorners, controlSizes, focusRing } from "./controlLook.js";
8
8
  export function SegmentedControl({ label, value, options, size = 'md', disabled = false, onChange, style }) {
@@ -10,7 +10,7 @@ export function SegmentedControl({ label, value, options, size = 'md', disabled
10
10
  return (_jsxs(RadioGroup, { "aria-label": label, value: value, disabled: disabled, onValueChange: (next) => {
11
11
  interact({ primitive: 'SegmentedControl', event: 'change', label });
12
12
  onChange(next);
13
- }, className: stylex.props(styles.group, style).className, children: [_jsx("span", { ...stylex.props(styles.indicator(index, options.length)) }), options.map((option) => (_jsx(Radio.Root, { value: option.value, className: (state) => stylex.props(styles.option, controlSizes[size], buttonCorners[size], !disabled && styles.hoverable, state.checked && styles.optionChecked, disabled && styles.disabled, focusRing.visible).className, children: option.label }, option.value)))] }));
13
+ }, className: stylex.props(styles.group, style).className, children: [_jsx("span", { ...stylex.props(styles.indicator(index, options.length)) }), options.map((option) => (_jsxs(Radio.Root, { value: option.value, className: (state) => stylex.props(styles.option, controlSizes[size], buttonCorners[size], !disabled && styles.hoverable, state.checked && styles.optionChecked, disabled && styles.disabled, focusRing.visible).className, children: [option.label, option.count === undefined ? null : (_jsx("span", { ...stylex.props(styles.count), children: option.count }))] }, option.value)))] }));
14
14
  }
15
15
  const styles = stylex.create({
16
16
  group: {
@@ -44,6 +44,7 @@ const styles = stylex.create({
44
44
  position: 'relative',
45
45
  zIndex: 1,
46
46
  display: 'inline-flex',
47
+ gap: `calc(${space.xs} + ${space.xxs})`,
47
48
  alignItems: 'center',
48
49
  justifyContent: 'center',
49
50
  color: colors.secondaryForeground,
@@ -64,5 +65,11 @@ const styles = stylex.create({
64
65
  },
65
66
  disabled: {
66
67
  color: colors.disabledForeground
68
+ },
69
+ count: {
70
+ fontSize: fontSizes.captionLg,
71
+ fontWeight: fontWeights.normal,
72
+ lineHeight: lineHeights.caption,
73
+ color: colors.tertiaryForeground
67
74
  }
68
75
  });
@@ -6,6 +6,7 @@ export type SidebarLink = {
6
6
  to: AppPath;
7
7
  icon: IconName;
8
8
  count?: number | null;
9
+ countTone?: 'quiet' | 'brand';
9
10
  };
10
11
  type SidebarProps = {
11
12
  workspace: string;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as stylex from '@stylexjs/stylex';
3
3
  import { NavLink } from 'react-router';
4
- import { breakpoints, colors, controls, iconSizes, layers, motion, radii, shadows, space, widths } from "../theme/tokens.stylex.js";
4
+ import { breakpoints, colors, controls, fontSizes, fontWeights, iconSizes, layers, lineHeights, motion, radii, shadows, space, widths } from "../theme/tokens.stylex.js";
5
5
  import { Badge } from "./Badge.js";
6
6
  import { focusRing } from "./controlLook.js";
7
7
  import { Button } from "./Button.js";
@@ -16,7 +16,8 @@ export function Sidebar({ workspace, workspaceMenu, links, isToggled, isAnimatin
16
16
  onFollowLink();
17
17
  };
18
18
  return (_jsxs("nav", { ...stylex.props(styles.sidebar, isToggled && styles.sidebarToggled, isAnimating && styles.sidebarAnimating), children: [_jsx("div", { ...stylex.props(styles.header), children: _jsx(Menu, { trigger: _jsxs(_Fragment, { children: [_jsx("span", { ...stylex.props(styles.logo), children: _jsx(Text, { variant: "caption", children: workspace.charAt(0) }) }), _jsx(Text, { variant: "label", isTruncated: true, children: workspace }), _jsx(Icon, { name: "chevronDown" })] }), items: workspaceMenu }) }), onSearch === undefined ? null : (_jsx(Button, { variant: "secondary", icon: "search", shortcut: ['mod', 'k'], isFullWidth: true, align: "start", onClick: onSearch, children: "Search" })), _jsx("div", { ...stylex.props(styles.body), children: _jsx(ScrollArea, { children: _jsxs("div", { ...stylex.props(styles.group), children: [_jsx("span", { ...stylex.props(styles.groupLabel), children: _jsx(Text, { variant: "label", tone: "tertiary", children: "Workspace" }) }), links.map((item) => (_jsxs(NavLink, { onClick: () => followLink(item), to: item.to, end: item.to === '/' ||
19
- links.some((other) => String(other.to).startsWith(`${String(item.to)}/`)), className: ({ isActive }) => stylex.props(styles.item, isActive && styles.itemCurrent, focusRing.visible, focusRing.inside).className ?? '', children: [_jsx(Icon, { name: item.icon }), _jsx(Text, { variant: "label", isTruncated: true, style: styles.itemLabel, children: item.label }), typeof item.count !== 'number' ? null : (_jsx(Badge, { variant: "transparent", children: item.count }))] }, item.to)))] }) }) })] }));
19
+ links.some((other) => String(other.to).startsWith(`${String(item.to)}/`)), className: ({ isActive }) => stylex.props(styles.item, isActive && styles.itemCurrent, focusRing.visible, focusRing.inside).className ?? '', children: [_jsx(Icon, { name: item.icon }), _jsx(Text, { variant: "label", isTruncated: true, style: styles.itemLabel, children: item.label }), typeof item.count !== 'number' ? null : item.countTone ===
20
+ 'brand' ? (_jsx("span", { ...stylex.props(styles.count), children: item.count })) : (_jsx(Badge, { variant: "transparent", children: item.count }))] }, item.to)))] }) }) })] }));
20
21
  }
21
22
  const styles = stylex.create({
22
23
  sidebar: {
@@ -132,6 +133,21 @@ const styles = stylex.create({
132
133
  color: colors.primaryForeground,
133
134
  backgroundColor: colors.transparentHover
134
135
  },
136
+ count: {
137
+ display: 'inline-flex',
138
+ flexShrink: 0,
139
+ alignItems: 'center',
140
+ justifyContent: 'center',
141
+ minWidth: `calc(${space.lg} + ${space.xxs})`,
142
+ height: `calc(${space.lg} + ${space.xxs})`,
143
+ paddingInline: `calc(${space.xs} + ${space.xxs} / 2)`,
144
+ fontSize: fontSizes.captionSm,
145
+ fontWeight: fontWeights.medium,
146
+ lineHeight: lineHeights.caption,
147
+ color: colors.onBrandSolidForeground,
148
+ backgroundColor: colors.brandSolid,
149
+ borderRadius: radii.full
150
+ },
135
151
  itemLabel: {
136
152
  flexGrow: 1
137
153
  }
@@ -2,8 +2,9 @@ import type { ReactNode } from 'react';
2
2
  type TableRowProps = {
3
3
  label: string;
4
4
  isCurrent?: boolean;
5
+ size?: 'md' | 'lg';
5
6
  onClick: () => void;
6
7
  children: ReactNode;
7
8
  };
8
- export declare function TableRow({ label, isCurrent, onClick, children }: TableRowProps): import("react").JSX.Element;
9
+ export declare function TableRow({ label, isCurrent, size, onClick, children }: TableRowProps): import("react").JSX.Element;
9
10
  export {};