@trypattern/sdk 0.1.21 → 0.1.22

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/dist/cli/app.js CHANGED
@@ -20,6 +20,8 @@ import { putLocalPhotos, resetLocalDatabase } from "../vite/localWorker.js";
20
20
  import { readProjectFile } from "../vite/projectFile.js";
21
21
  import { readStarter, starterContentOf, withStarter } from "./starter.js";
22
22
  import { addLiveMedia, login, publish, pull, restore, versions as listVersions } from "./publishing.js";
23
+ import { CHANGE_SET_ROWS } from "../publishWire.js";
24
+ import { DATA_USAGE, data } from "./data.js";
23
25
  import { toolBin } from "./tools.js";
24
26
  function dependsOnPattern(directory) {
25
27
  try {
@@ -84,7 +86,11 @@ const VALUE_FLAGS = new Set([
84
86
  '--as',
85
87
  '--code',
86
88
  '--message',
87
- '--alt'
89
+ '--alt',
90
+ '--row',
91
+ '--set',
92
+ '--file',
93
+ '--asked'
88
94
  ]);
89
95
  const PHOTO_TYPES = {
90
96
  '.jpg': 'image/jpeg',
@@ -349,6 +355,24 @@ own POST /api/media, then the rows. Later publishes never touch them; they are t
349
355
  },
350
356
  flags: ['--starter', '--message']
351
357
  },
358
+ data: {
359
+ summary: 'Change the live site’s data, with the owner’s approval (app data --help)',
360
+ details: `${DATA_USAGE}
361
+ The live database belongs to the owner: changes to it go to them for approval, never straight in.
362
+ tables and list read the live data (columns, ids and values) through onpattern.app. add, update
363
+ and remove only collect changes on this computer (.dev/data-changes.json); nothing live changes.
364
+ Values use the table's own columns and types, as tables shows them: a price stored in cents is 1400.
365
+ send puts every collected change in front of the owner as one request on onpattern.app (Database)
366
+ and emails them. --asked is required: their request in their own words, quoted, which they see
367
+ beside Approve. Only the owner can approve, in their browser; send then waits for their decision
368
+ and prints what went live. A row the owner changed after you read it is skipped, never
369
+ overwritten: list it again and send a new change. The live site must run a Pattern with app data:
370
+ after updating @trypattern/sdk, app publish once first. status waits on the latest request; clear drops
371
+ collected changes. Up to ${CHANGE_SET_ROWS} changes per request. For this computer's data, edit seed.ts and run app reset.`,
372
+ run: (_argument, options, rest) => data(publishingIo(), ROOT, rest, options),
373
+ argumentCount: 3,
374
+ flags: ['--row', '--set', '--file', '--asked']
375
+ },
352
376
  versions: {
353
377
  summary: 'List the versions that went live, newest first',
354
378
  details: 'Each version with its number, whether it went live, when, and who published it. app restore <number> puts one back.',
@@ -0,0 +1,5 @@
1
+ import type { Outcome, PublishingIo } from './publishing.ts';
2
+ export declare const DATA_USAGE = "app data tables\napp data list <table>\napp data add <table> --row '{\"name\":\"Gold chair\",\"price\":8}' (or --file rows.json, a JSON array)\napp data update <table> <id> --set '{\"price\":14}'\napp data remove <table> <id>\napp data send --asked \"<the owner's request, in their words>\"\napp data status\napp data clear";
3
+ type DataOptions = ReadonlyMap<string, string | true>;
4
+ export declare function data(io: PublishingIo, root: string, [action, table, id]: readonly (string | undefined)[], options: DataOptions): Promise<Outcome>;
5
+ export {};
@@ -0,0 +1,273 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { CHANGE_SET_ROWS, DASHBOARD_URL, changeSet, dataChange, databaseRow, databaseRows, databaseSchema } from "../publishWire.js";
5
+ import { readProjectFile } from "../vite/projectFile.js";
6
+ import { callJson, failure, isOutcome, sayHost, signedIn, sleep } from "./publishing.js";
7
+ const STAGED = join('.dev', 'data-changes.json');
8
+ const WAIT_MS = 90_000;
9
+ const POLL_MS = 3000;
10
+ const MAX_PAGES = 50;
11
+ const staged = z.array(dataChange);
12
+ export const DATA_USAGE = `app data tables
13
+ app data list <table>
14
+ app data add <table> --row '{"name":"Gold chair","price":8}' (or --file rows.json, a JSON array)
15
+ app data update <table> <id> --set '{"price":14}'
16
+ app data remove <table> <id>
17
+ app data send --asked "<the owner's request, in their words>"
18
+ app data status
19
+ app data clear`;
20
+ function stagedPath(root) {
21
+ return join(root, STAGED);
22
+ }
23
+ function readStaged(root) {
24
+ const path = stagedPath(root);
25
+ if (!existsSync(path))
26
+ return [];
27
+ const parsed = staged.safeParse(JSON.parse(readFileSync(path, 'utf8')));
28
+ return parsed.success ? parsed.data : [];
29
+ }
30
+ function writeStaged(root, changes) {
31
+ const path = stagedPath(root);
32
+ mkdirSync(dirname(path), { recursive: true });
33
+ writeFileSync(path, `${JSON.stringify(changes, null, 2)}\n`);
34
+ }
35
+ function refusal(text, code = 1) {
36
+ return { code, data: { error: text }, text: `✗ ${text}` };
37
+ }
38
+ function valueOf(options, flag) {
39
+ const value = options.get(flag);
40
+ return typeof value === 'string' ? value : undefined;
41
+ }
42
+ function jsonOf(text, what) {
43
+ try {
44
+ return JSON.parse(text);
45
+ }
46
+ catch {
47
+ throw new Error(`${what} is not JSON: ${text.slice(0, 80)}`);
48
+ }
49
+ }
50
+ function rowsGiven(root, options) {
51
+ const row = valueOf(options, '--row');
52
+ const file = valueOf(options, '--file');
53
+ if (row !== undefined)
54
+ return [databaseRow.parse(jsonOf(row, '--row'))];
55
+ if (file === undefined)
56
+ throw new Error('Give the row with --row or --file');
57
+ const path = resolve(root, file);
58
+ if (!existsSync(path))
59
+ throw new Error(`There is no file at ${file}`);
60
+ return z.array(databaseRow).parse(jsonOf(readFileSync(path, 'utf8'), file));
61
+ }
62
+ function pendingLines(changes) {
63
+ return changes.map((change) => {
64
+ if (change.op === 'add')
65
+ return ` + ${change.table} ${JSON.stringify(change.row)}`;
66
+ if (change.op === 'remove')
67
+ return ` − ${change.table} ${change.id}`;
68
+ return ` ~ ${change.table} ${change.id} ${JSON.stringify(change.after)}`;
69
+ });
70
+ }
71
+ async function liveRows(io, name, table) {
72
+ const rows = [];
73
+ let after = null;
74
+ for (let page = 0; page < MAX_PAGES; page += 1) {
75
+ const query = after === null ? '' : `?after=${encodeURIComponent(after)}`;
76
+ const result = await signedIn(io, `app data list ${table}`, () => callJson(`/api/apps/${name}/database/${table}${query}`, databaseRows));
77
+ if (isOutcome(result))
78
+ return result;
79
+ rows.push(...result.rows);
80
+ after = result.next;
81
+ if (after === null)
82
+ break;
83
+ }
84
+ return rows;
85
+ }
86
+ async function tables(io, name) {
87
+ const result = await signedIn(io, 'app data tables', () => callJson(`/api/apps/${name}/database`, databaseSchema));
88
+ if (isOutcome(result))
89
+ return result;
90
+ if (result.status === 'outdated') {
91
+ return refusal('Publish once with the latest Pattern to see the live data');
92
+ }
93
+ const lines = result.tables
94
+ .filter((table) => !table.isBuiltIn)
95
+ .map((table) => {
96
+ const properties = Object.keys(Object(Reflect.get(table.schema, 'properties')));
97
+ return ` ${table.name} ${table.rowCount} rows columns: ${properties.join(', ')}`;
98
+ });
99
+ return {
100
+ code: 0,
101
+ data: { name, tables: result.tables },
102
+ text: [`Live tables of ${name}`, ...lines].join('\n')
103
+ };
104
+ }
105
+ async function list(io, name, table) {
106
+ const rows = await liveRows(io, name, table);
107
+ if (isOutcome(rows))
108
+ return rows;
109
+ return {
110
+ code: 0,
111
+ data: { name, table, rows },
112
+ text: [
113
+ `${rows.length} live ${table} rows on ${name}`,
114
+ ...rows.map((row) => ` ${JSON.stringify(row)}`)
115
+ ].join('\n')
116
+ };
117
+ }
118
+ function stage(root, added) {
119
+ const changes = [...readStaged(root), ...added];
120
+ if (changes.length > CHANGE_SET_ROWS) {
121
+ return refusal(`That's more than ${CHANGE_SET_ROWS} changes. Send these first with app data send, then add the rest.`);
122
+ }
123
+ writeStaged(root, changes);
124
+ return {
125
+ code: 0,
126
+ data: { staged: changes },
127
+ text: [
128
+ `✓ ${changes.length} change${changes.length === 1 ? '' : 's'} ready to send. Nothing on the live site has changed.`,
129
+ ...pendingLines(changes),
130
+ ' next add any other changes, then app data send --asked "<their request, in their words>"'
131
+ ].join('\n')
132
+ };
133
+ }
134
+ async function before(io, name, table, id) {
135
+ const rows = await liveRows(io, name, table);
136
+ if (isOutcome(rows))
137
+ return rows;
138
+ const row = rows.find((candidate) => candidate['id'] === id);
139
+ if (row === undefined)
140
+ throw new Error(`There is no ${table} row with id ${id}`);
141
+ return row;
142
+ }
143
+ function decisionText(set, link) {
144
+ if (set.status === 'waiting') {
145
+ return {
146
+ code: 3,
147
+ data: { changeSet: set },
148
+ text: `… Still waiting for the owner to approve "${set.title}" at ${link}.\n next run app data status again: it waits for the decision`
149
+ };
150
+ }
151
+ if (set.status === 'applied') {
152
+ const outcomes = set.outcomes ?? [];
153
+ const missed = outcomes
154
+ .map((outcome, index) => ({ outcome, change: set.changes[index] }))
155
+ .filter(({ outcome }) => outcome.status !== 'applied');
156
+ return {
157
+ code: missed.length === 0 ? 0 : 1,
158
+ data: { changeSet: set },
159
+ text: [
160
+ `✓ Approved: ${set.title}. ${outcomes.length - missed.length} of ${outcomes.length} changes are live.`,
161
+ ...missed.map(({ outcome, change }) => ` ${outcome.status} ${change?.table ?? ''} ${change?.op ?? ''}: ${outcome.reason ?? ''}`)
162
+ ].join('\n')
163
+ };
164
+ }
165
+ const words = {
166
+ declined: 'The owner chose Not yet. Nothing changed on the live site.',
167
+ expired: 'This request expired or a newer one replaced it. Nothing changed.',
168
+ undone: 'The owner approved this, then undid it.'
169
+ }[set.status];
170
+ return { code: 1, data: { changeSet: set }, text: `✗ ${words}` };
171
+ }
172
+ async function waitForDecision(io, name, id) {
173
+ const link = `${DASHBOARD_URL}/sites/${name}/database`;
174
+ const deadline = Date.now() + WAIT_MS;
175
+ for (;;) {
176
+ const set = await signedIn(io, 'app data status', () => callJson(`/api/apps/${name}/changes/${id}`, changeSet));
177
+ if (isOutcome(set))
178
+ return set;
179
+ if (set.status !== 'waiting' || Date.now() > deadline) {
180
+ return decisionText(set, link);
181
+ }
182
+ await sleep(POLL_MS);
183
+ }
184
+ }
185
+ async function send(io, root, name, asked) {
186
+ const changes = readStaged(root);
187
+ if (changes.length === 0) {
188
+ return refusal('There are no changes to send. Use app data add, update or remove first.');
189
+ }
190
+ if (asked === undefined || asked.trim() === '') {
191
+ return refusal('Say what the owner asked for with --asked "<their words>". They see it beside Approve.', 2);
192
+ }
193
+ const set = await signedIn(io, 'app data send', () => callJson(`/api/apps/${name}/changes`, changeSet, {
194
+ method: 'POST',
195
+ headers: { 'content-type': 'application/json' },
196
+ body: JSON.stringify({ asked: asked.trim(), changes })
197
+ }));
198
+ if (isOutcome(set))
199
+ return set;
200
+ rmSync(stagedPath(root), { force: true });
201
+ const link = `${DASHBOARD_URL}/sites/${name}/database`;
202
+ io.say(`✓ Sent "${set.title}" to the owner. They also got an email.`);
203
+ io.say(` next tell them: "I've sent the changes to your Pattern dashboard. Open ${link} and press Approve." This waits for their decision.`);
204
+ return waitForDecision(io, name, set.id);
205
+ }
206
+ async function status(io, root, name) {
207
+ const sets = await signedIn(io, 'app data status', () => callJson(`/api/apps/${name}/changes`, z.array(changeSet)));
208
+ if (isOutcome(sets))
209
+ return sets;
210
+ const latest = sets[0];
211
+ const pending = readStaged(root);
212
+ if (latest === undefined) {
213
+ return {
214
+ code: 0,
215
+ data: { staged: pending },
216
+ text: pending.length === 0
217
+ ? 'Nothing has been sent for approval yet.'
218
+ : [
219
+ `${pending.length} changes ready to send:`,
220
+ ...pendingLines(pending)
221
+ ].join('\n')
222
+ };
223
+ }
224
+ return latest.status === 'waiting'
225
+ ? waitForDecision(io, name, latest.id)
226
+ : decisionText(latest, `${DASHBOARD_URL}/sites/${name}/database`);
227
+ }
228
+ export async function data(io, root, [action, table, id], options) {
229
+ try {
230
+ const { name } = readProjectFile(root);
231
+ if (action === 'clear') {
232
+ rmSync(stagedPath(root), { force: true });
233
+ return {
234
+ code: 0,
235
+ data: { staged: [] },
236
+ text: '✓ Cleared the changes that were ready to send'
237
+ };
238
+ }
239
+ sayHost(io);
240
+ if (action === 'tables')
241
+ return await tables(io, name);
242
+ if (action === 'send')
243
+ return await send(io, root, name, valueOf(options, '--asked'));
244
+ if (action === 'status')
245
+ return await status(io, root, name);
246
+ if (table === undefined)
247
+ return refusal(`Say which table\n${DATA_USAGE}`, 2);
248
+ if (action === 'list')
249
+ return await list(io, name, table);
250
+ if (action === 'add') {
251
+ return stage(root, rowsGiven(root, options).map((row) => ({ op: 'add', table, row })));
252
+ }
253
+ if (id === undefined)
254
+ return refusal(`Say which row, by its id\n${DATA_USAGE}`, 2);
255
+ const current = await before(io, name, table, id);
256
+ if (isOutcome(current))
257
+ return current;
258
+ if (action === 'remove') {
259
+ return stage(root, [{ op: 'remove', table, id, before: current }]);
260
+ }
261
+ if (action === 'update') {
262
+ const set = valueOf(options, '--set');
263
+ if (set === undefined)
264
+ return refusal(`Give the new values with --set '{...}'`, 2);
265
+ const after = databaseRow.parse(jsonOf(set, '--set'));
266
+ return stage(root, [{ op: 'update', table, id, before: current, after }]);
267
+ }
268
+ return refusal(`Unknown data command: ${action ?? ''}\n${DATA_USAGE}`, 2);
269
+ }
270
+ catch (error) {
271
+ return failure(error);
272
+ }
273
+ }
@@ -1,5 +1,6 @@
1
+ import { z } from 'zod';
1
2
  import type { StarterContent } from '../publishWire.ts';
2
- type Outcome = {
3
+ export type Outcome = {
3
4
  code: number;
4
5
  data: Record<string, unknown>;
5
6
  text: string;
@@ -14,10 +15,16 @@ export type PublishingIo = {
14
15
  build: () => Promise<Outcome>;
15
16
  sdkVersion: string;
16
17
  };
18
+ export declare function dashboard(): string;
19
+ export declare function sayHost(io: PublishingIo): void;
20
+ export declare function callJson<Value>(path: string, schema: z.ZodType<Value>, init?: RequestInit): Promise<Value>;
21
+ export declare function sleep(ms: number): Promise<unknown>;
22
+ export declare function signedIn<Result>(io: PublishingIo, again: string, work: () => Promise<Result>): Promise<Result | Outcome>;
23
+ export declare function isOutcome(value: unknown): value is Outcome;
24
+ export declare function failure(error: unknown, data?: Record<string, unknown>): Outcome;
17
25
  export declare function login(io: PublishingIo): Promise<Outcome>;
18
26
  export declare function pull(io: PublishingIo, name: string | undefined, cwd: string, setupCode: string | undefined): Promise<Outcome>;
19
27
  export declare function publish(io: PublishingIo, root: string, isStarter: boolean, message: string | undefined, starter: StarterContent | undefined): Promise<Outcome>;
20
28
  export declare function addLiveMedia(io: PublishingIo, root: string, form: FormData, again: string): Promise<Outcome>;
21
29
  export declare function versions(io: PublishingIo, root: string): Promise<Outcome>;
22
30
  export declare function restore(io: PublishingIo, root: string, number: string | undefined): Promise<Outcome>;
23
- export {};
@@ -48,10 +48,10 @@ const STEP_WORDS = {
48
48
  'sign-in': 'Turning on sign-in',
49
49
  publish: 'Publishing the site'
50
50
  };
51
- function dashboard() {
51
+ export function dashboard() {
52
52
  return (process.env['PATTERN_DASHBOARD_URL'] ?? DASHBOARD_URL).replace(/\/$/, '');
53
53
  }
54
- function sayHost(io) {
54
+ export function sayHost(io) {
55
55
  io.say(` via ${new URL(dashboard()).host}`);
56
56
  }
57
57
  const credentials = z.record(z.string(), z.object({ cookie: z.string() }));
@@ -114,7 +114,7 @@ async function call(path, init = {}) {
114
114
  }
115
115
  return response;
116
116
  }
117
- async function callJson(path, schema, init = {}) {
117
+ export async function callJson(path, schema, init = {}) {
118
118
  const response = await call(path, init);
119
119
  const body = await response.json().catch(() => null);
120
120
  if (!response.ok) {
@@ -130,7 +130,7 @@ function labelOfThisMachine() {
130
130
  const name = named.status === 0 ? named.stdout.trim() : '';
131
131
  return (name === '' ? 'this computer' : name).slice(0, 80);
132
132
  }
133
- function sleep(ms) {
133
+ export function sleep(ms) {
134
134
  return new Promise((done) => setTimeout(done, ms));
135
135
  }
136
136
  async function waitForApproval(secret) {
@@ -195,7 +195,7 @@ async function useSetupCode(io, code) {
195
195
  }
196
196
  io.say('✗ That setup code has expired or was already used');
197
197
  }
198
- async function signedIn(io, again, work) {
198
+ export async function signedIn(io, again, work) {
199
199
  if (savedCookie() === undefined) {
200
200
  const outcome = await signIn(io, again);
201
201
  if (outcome.code !== 0)
@@ -213,10 +213,10 @@ async function signedIn(io, again, work) {
213
213
  return work();
214
214
  }
215
215
  }
216
- function isOutcome(value) {
216
+ export function isOutcome(value) {
217
217
  return typeof value === 'object' && value !== null && 'text' in value;
218
218
  }
219
- function failure(error, data = {}) {
219
+ export function failure(error, data = {}) {
220
220
  const message = error instanceof TypeError && error.message === 'fetch failed'
221
221
  ? `Couldn’t reach ${dashboard()}. Check the internet connection and try again.`
222
222
  : error instanceof Error
@@ -3,7 +3,7 @@ import type { Placement } from './Placement.ts';
3
3
  type BadgeProps = {
4
4
  children: ReactNode;
5
5
  variant?: 'primary' | 'tertiary' | 'outline' | 'transparent';
6
- tone?: 'neutral' | 'brand' | 'positive' | 'danger';
6
+ tone?: 'neutral' | 'brand' | 'positive' | 'warning' | 'danger';
7
7
  size?: 'sm' | 'md';
8
8
  style?: Placement;
9
9
  };
@@ -8,6 +8,7 @@ const toneKeys = {
8
8
  neutral: 'Neutral',
9
9
  brand: 'Brand',
10
10
  positive: 'Positive',
11
+ warning: 'Warning',
11
12
  danger: 'Danger'
12
13
  };
13
14
  const styles = stylex.create({
@@ -46,6 +47,10 @@ const looks = stylex.create({
46
47
  color: colors.onPositiveSolidForeground,
47
48
  backgroundColor: colors.positiveSolid
48
49
  },
50
+ primaryWarning: {
51
+ color: colors.onWarningSolidForeground,
52
+ backgroundColor: colors.warningSolid
53
+ },
49
54
  primaryDanger: {
50
55
  color: colors.onDangerSolidForeground,
51
56
  backgroundColor: colors.dangerSolid
@@ -62,6 +67,10 @@ const looks = stylex.create({
62
67
  color: colors.onPositiveSubtleForeground,
63
68
  backgroundColor: colors.positiveSubtle
64
69
  },
70
+ tertiaryWarning: {
71
+ color: colors.onWarningSubtleForeground,
72
+ backgroundColor: colors.warningSubtle
73
+ },
65
74
  tertiaryDanger: {
66
75
  color: colors.dangerText,
67
76
  backgroundColor: colors.dangerSubtleHover
@@ -81,6 +90,11 @@ const looks = stylex.create({
81
90
  backgroundColor: 'transparent',
82
91
  boxShadow: `inset 0 0 0 ${borders.thin} ${colors.positiveBorderDefault}`
83
92
  },
93
+ outlineWarning: {
94
+ color: colors.warningForeground,
95
+ backgroundColor: 'transparent',
96
+ boxShadow: `inset 0 0 0 ${borders.thin} ${colors.warningBorderDefault}`
97
+ },
84
98
  outlineDanger: {
85
99
  color: colors.dangerForeground,
86
100
  backgroundColor: 'transparent',
@@ -98,6 +112,10 @@ const looks = stylex.create({
98
112
  color: colors.positiveForeground,
99
113
  backgroundColor: 'transparent'
100
114
  },
115
+ transparentWarning: {
116
+ color: colors.warningForeground,
117
+ backgroundColor: 'transparent'
118
+ },
101
119
  transparentDanger: {
102
120
  color: colors.dangerForeground,
103
121
  backgroundColor: 'transparent'
@@ -6,7 +6,7 @@ import type { Size } from './Size.ts';
6
6
  export type ButtonProps = {
7
7
  children: ReactNode;
8
8
  variant?: 'primary' | 'secondary' | 'tertiary' | 'transparent';
9
- tone?: 'neutral' | 'brand' | 'danger';
9
+ tone?: 'neutral' | 'brand' | 'positive' | 'danger';
10
10
  size?: Size;
11
11
  to?: AppPath;
12
12
  href?: string;
@@ -27,6 +27,7 @@ export function ButtonBody({ children, variant = 'primary', tone = variant === '
27
27
  const toneKeys = {
28
28
  neutral: 'Neutral',
29
29
  brand: 'Brand',
30
+ positive: 'Positive',
30
31
  danger: 'Danger'
31
32
  };
32
33
  const styles = stylex.create({
@@ -130,6 +131,10 @@ const looks = stylex.create({
130
131
  color: colors.onBrandSolidForeground,
131
132
  backgroundColor: colors.brandSolid
132
133
  },
134
+ primaryPositive: {
135
+ color: colors.onPositiveSolidForeground,
136
+ backgroundColor: colors.positiveSolid
137
+ },
133
138
  primaryDanger: {
134
139
  color: colors.onDangerSolidForeground,
135
140
  backgroundColor: colors.dangerSolid
@@ -144,6 +149,11 @@ const looks = stylex.create({
144
149
  backgroundColor: colors.componentOnContainerStrong,
145
150
  boxShadow: shadows.default
146
151
  },
152
+ secondaryPositive: {
153
+ color: colors.positiveForeground,
154
+ backgroundColor: colors.componentOnContainerStrong,
155
+ boxShadow: shadows.default
156
+ },
147
157
  secondaryDanger: {
148
158
  color: colors.dangerForeground,
149
159
  backgroundColor: colors.componentOnContainerStrong,
@@ -157,6 +167,10 @@ const looks = stylex.create({
157
167
  color: colors.onBrandSubtleForeground,
158
168
  backgroundColor: colors.brandSubtle
159
169
  },
170
+ tertiaryPositive: {
171
+ color: colors.onPositiveSubtleForeground,
172
+ backgroundColor: colors.positiveSubtle
173
+ },
160
174
  tertiaryDanger: {
161
175
  color: colors.dangerForeground,
162
176
  backgroundColor: colors.dangerSubtle
@@ -169,6 +183,10 @@ const looks = stylex.create({
169
183
  color: colors.brandForeground,
170
184
  backgroundColor: 'transparent'
171
185
  },
186
+ transparentPositive: {
187
+ color: colors.positiveForeground,
188
+ backgroundColor: 'transparent'
189
+ },
172
190
  transparentDanger: {
173
191
  color: colors.dangerForeground,
174
192
  backgroundColor: 'transparent'
@@ -189,6 +207,13 @@ const hovers = stylex.create({
189
207
  ':active': colors.brandSolidActive
190
208
  }
191
209
  },
210
+ primaryPositive: {
211
+ backgroundColor: {
212
+ default: colors.positiveSolid,
213
+ ':hover': colors.positiveSolidHover,
214
+ ':active': colors.positiveSolidHover
215
+ }
216
+ },
192
217
  primaryDanger: {
193
218
  backgroundColor: {
194
219
  default: colors.dangerSolid,
@@ -208,6 +233,12 @@ const hovers = stylex.create({
208
233
  ':hover': colors.componentOnContainerStrongHover
209
234
  }
210
235
  },
236
+ secondaryPositive: {
237
+ backgroundColor: {
238
+ default: colors.componentOnContainerStrong,
239
+ ':hover': colors.componentOnContainerStrongHover
240
+ }
241
+ },
211
242
  secondaryDanger: {
212
243
  backgroundColor: {
213
244
  default: colors.componentOnContainerStrong,
@@ -227,6 +258,12 @@ const hovers = stylex.create({
227
258
  ':hover': colors.brandSubtleHover
228
259
  }
229
260
  },
261
+ tertiaryPositive: {
262
+ backgroundColor: {
263
+ default: colors.positiveSubtle,
264
+ ':hover': colors.positiveSubtleHover
265
+ }
266
+ },
230
267
  tertiaryDanger: {
231
268
  backgroundColor: {
232
269
  default: colors.dangerSubtle,
@@ -250,6 +287,12 @@ const hovers = stylex.create({
250
287
  ':hover': colors.transparentHover
251
288
  }
252
289
  },
290
+ transparentPositive: {
291
+ backgroundColor: {
292
+ default: 'transparent',
293
+ ':hover': colors.positiveSubtleHover
294
+ }
295
+ },
253
296
  transparentDanger: {
254
297
  backgroundColor: {
255
298
  default: 'transparent',
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  export declare const TABLES_READ = "tables:read";
3
3
  export declare const MEDIA_WRITE = "media:write";
4
+ export declare const TABLES_WRITE = "tables:write";
4
5
  export declare const ASSERTION_SECONDS = 60;
5
6
  export declare const NEXT_PAGE_HEADER = "x-pattern-next";
6
7
  export declare const assertionKey: z.ZodObject<{
@@ -14,6 +15,7 @@ export type AssertionKey = z.output<typeof assertionKey>;
14
15
  declare const assertionScope: z.ZodEnum<{
15
16
  "tables:read": "tables:read";
16
17
  "media:write": "media:write";
18
+ "tables:write": "tables:write";
17
19
  }>;
18
20
  export type AssertionScope = z.output<typeof assertionScope>;
19
21
  export declare function signAssertion(key: AssertionKey, { sub, aud, scope }: {
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  export const TABLES_READ = 'tables:read';
3
3
  export const MEDIA_WRITE = 'media:write';
4
+ export const TABLES_WRITE = 'tables:write';
4
5
  export const ASSERTION_SECONDS = 60;
5
6
  export const NEXT_PAGE_HEADER = 'x-pattern-next';
6
7
  const CLOCK_SKEW_SECONDS = 30;
@@ -12,7 +13,7 @@ export const assertionKey = z.object({
12
13
  x: z.string(),
13
14
  d: z.string()
14
15
  });
15
- const assertionScope = z.enum([TABLES_READ, MEDIA_WRITE]);
16
+ const assertionScope = z.enum([TABLES_READ, TABLES_WRITE, MEDIA_WRITE]);
16
17
  const assertionHeader = z.object({
17
18
  alg: z.literal('EdDSA'),
18
19
  typ: z.literal('JWT'),