@mccaskie-labs/workbench-config 0.2.4 → 0.3.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @mccaskie-labs/workbench-config
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Every component now ships its client questions: email (which emails, where replies go, drafts of the wording), inbound email (which addresses, attachments), strings (other languages) and the database (what to store and who sees it). `OFFER_PENDING` is removed, and so is `delivery.when`: a component's `offer.when` says when its tool applies, and a manifest with neither `offer` nor `no_offer` fails the checks.
8
+
9
+ ## 0.2.5
10
+
11
+ ### Patch Changes
12
+
13
+ - Client questions for country formats (where the readers are, the business's time zone, the currency), visitor counts (page views, and which actions to count) and admin pages (which pages, and who sees each).
14
+
3
15
  ## 0.2.4
4
16
 
5
17
  ### Patch Changes
@@ -7,8 +7,7 @@
7
7
  "status": "built",
8
8
  "delivery": {
9
9
  "choice": "optional",
10
- "intro": "Counting page views and the things visitors do on the site.",
11
- "when": "The client wants to know how many people visit, which pages they open, or to count an action such as a sign-up or an order."
10
+ "intro": "Counting page views and the things visitors do on the site."
12
11
  },
13
12
  "surfaces": {
14
13
  "package": [
@@ -99,5 +98,50 @@
99
98
  "cost_hint_usd": 0,
100
99
  "note": "Create the site, drive page views, query them back (cf_analytics_query, later)"
101
100
  }
102
- ]
101
+ ],
102
+ "offer": {
103
+ "pattern": "switch_on",
104
+ "when": "The client wants to know how many people visit, which pages they open, or to count an action such as a sign-up or an order.",
105
+ "say": "The project already has Workbench visitor counts: page views counted by Cloudflare without cookies, and the actions you choose counted in the app's own database. I'll switch on what you want.",
106
+ "answers_file": "workbench/analytics.json",
107
+ "answers_format": "`{ \"format\": 1, \"page_views\": true, \"events\": [{ \"name\": \"order.placed\", \"when\": \"a customer sends an order\", \"details\": [\"items\", \"total_band\"] }] }`. For page views, create the Web Analytics site with Workbench Control (`cf_web_analytics_site_create`), write its beacon token onto staging and production (`cf_worker_secret_put source=web_analytics`), and put `beacon(env)` on every page customers see. For each event, call `eventsFor(db).track(name, details)` where the action happens.",
108
+ "questions": [
109
+ {
110
+ "id": "analytics.page-views",
111
+ "sets": "page_views",
112
+ "ask": "Should the site count its page views?",
113
+ "style": "cards",
114
+ "answers": [
115
+ {
116
+ "label": "Yes",
117
+ "value": true,
118
+ "means": "How many people visit and which pages they open, counted by Cloudflare. No cookies, and no personal details kept."
119
+ },
120
+ {
121
+ "label": "No",
122
+ "value": false,
123
+ "means": "Nothing is counted on the site's pages."
124
+ }
125
+ ],
126
+ "note": "Only pages customers see; never the owner's or admins' pages."
127
+ },
128
+ {
129
+ "id": "analytics.actions",
130
+ "sets": "events",
131
+ "ask": "Which actions should be counted?",
132
+ "style": "table",
133
+ "note": "Propose the few actions that show whether the site is working for the business (a sign-up, an order, a booking, a form sent), each with a name (`thing.past_verb`, lower case: `order.placed`), when it happens in the client's words, and its details. Details are counts, choices and bands (`items`, `plan`, `total_band`), never an email address, a name, a phone number or free text someone typed. The client accepts, removes or adds; an empty list is fine."
134
+ }
135
+ ],
136
+ "not_asked": [
137
+ {
138
+ "sets": "consent",
139
+ "why": "The page-view counter sets no cookies and keeps no personal details, and events hold none either, so they add nothing that a cookie notice would need to cover."
140
+ },
141
+ {
142
+ "sets": "reports",
143
+ "why": "Page views are seen in Cloudflare's Web Analytics, and events are rows in the app's own `events` table; ask what the owner wants to see when they ask for a report."
144
+ }
145
+ ]
146
+ }
103
147
  }
@@ -7,8 +7,7 @@
7
7
  "status": "built",
8
8
  "delivery": {
9
9
  "choice": "optional",
10
- "intro": "Dates, times, numbers and money written the way readers in your country expect.",
11
- "when": "The app shows a date, a time, a number, a phone number or an amount of money to its readers, or the client says where their readers are."
10
+ "intro": "Dates, times, numbers and money written the way readers in your country expect."
12
11
  },
13
12
  "surfaces": {
14
13
  "package": [
@@ -84,5 +83,163 @@
84
83
  "cost_hint_usd": 0,
85
84
  "note": "Every pack's spelling is one the Gateway accepts (en-PH, en-AU, en-GB, en-US), checked against the ai manifest's spellings; no Gateway call"
86
85
  }
87
- ]
86
+ ],
87
+ "offer": {
88
+ "pattern": "project",
89
+ "when": "The app shows a date, a time, a number, a phone number or an amount of money to its readers, or the client says where their readers are.",
90
+ "say": "I'll write dates, times and money with Workbench country formats, the way your readers expect them. That needs to know where they are, once for the whole site.",
91
+ "answers_file": "workbench/country.json",
92
+ "answers_format": "`{ \"format\": 1, \"country\": \"AU\", \"time_zone\": null, \"currency\": null }`: the country code (or `ZZ`, the package's WORLDWIDE, for readers everywhere), and an IANA time zone or `{ \"code\", \"symbol\" }` currency only when they differ from the country's own (the Australian and US time questions both fill `time_zone`). Then set `READERS` in `src/lib/country.ts` to match: `audiencePack({ country, timeZone, currency })`, leaving out what is null. Every page, email, PDF and form reads `READERS`, so nothing else changes.",
93
+ "questions": [
94
+ {
95
+ "id": "country.where",
96
+ "sets": "audience.country",
97
+ "ask": "Where are most of the site's readers?",
98
+ "style": "cards",
99
+ "answers": [
100
+ {
101
+ "label": "Australia",
102
+ "value": "AU",
103
+ "means": "26 September 2026, A$1,400, Sydney time, Australian spelling."
104
+ },
105
+ {
106
+ "label": "The Philippines",
107
+ "value": "PH",
108
+ "means": "September 26, 2026, ₱1,400, Manila time."
109
+ },
110
+ {
111
+ "label": "The United Kingdom",
112
+ "value": "GB",
113
+ "means": "26 September 2026, £1,400, UK time, British spelling."
114
+ },
115
+ {
116
+ "label": "The United States",
117
+ "value": "US",
118
+ "means": "September 26, 2026, US$1,400, Eastern time, American spelling."
119
+ }
120
+ ],
121
+ "other": {
122
+ "kind": "text",
123
+ "min": 2,
124
+ "max": 60,
125
+ "description": "Another country, or readers all over the world."
126
+ },
127
+ "note": "Recommend Australia unless the client's words say otherwise. A country with no pack yet, or readers everywhere, gets the neutral one (`ZZ`: 26 September 2026, 24-hour clock, UTC, US$): say so, and ask for the currency."
128
+ },
129
+ {
130
+ "id": "country.au-time",
131
+ "sets": "audience.time_zone.au",
132
+ "ask": "Which Australian time should the site show?",
133
+ "style": "cards",
134
+ "answers": [
135
+ {
136
+ "label": "Sydney, Melbourne, Canberra",
137
+ "value": null,
138
+ "means": "The pack's own (Australia/Sydney), with daylight saving."
139
+ },
140
+ {
141
+ "label": "Brisbane",
142
+ "value": "Australia/Brisbane",
143
+ "means": "Queensland, no daylight saving."
144
+ },
145
+ {
146
+ "label": "Adelaide",
147
+ "value": "Australia/Adelaide",
148
+ "means": "South Australia, half an hour behind Sydney."
149
+ },
150
+ {
151
+ "label": "Perth",
152
+ "value": "Australia/Perth",
153
+ "means": "Western Australia."
154
+ }
155
+ ],
156
+ "other": {
157
+ "kind": "text",
158
+ "min": 5,
159
+ "max": 40,
160
+ "description": "Another Australian time zone, such as Australia/Darwin or Australia/Hobart."
161
+ },
162
+ "when": {
163
+ "id": "country.where",
164
+ "is": [
165
+ "AU"
166
+ ]
167
+ },
168
+ "note": "The time zone of the business, where bookings and opening hours happen. Hobart follows Sydney; Darwin is Australia/Darwin."
169
+ },
170
+ {
171
+ "id": "country.us-time",
172
+ "sets": "audience.time_zone.us",
173
+ "ask": "Which US time should the site show?",
174
+ "style": "cards",
175
+ "answers": [
176
+ {
177
+ "label": "Eastern",
178
+ "value": null,
179
+ "means": "The pack's own (America/New_York)."
180
+ },
181
+ {
182
+ "label": "Central",
183
+ "value": "America/Chicago"
184
+ },
185
+ {
186
+ "label": "Mountain",
187
+ "value": "America/Denver"
188
+ },
189
+ {
190
+ "label": "Pacific",
191
+ "value": "America/Los_Angeles"
192
+ }
193
+ ],
194
+ "other": {
195
+ "kind": "text",
196
+ "min": 5,
197
+ "max": 40,
198
+ "description": "Another US time zone, such as America/Phoenix or Pacific/Honolulu."
199
+ },
200
+ "when": {
201
+ "id": "country.where",
202
+ "is": [
203
+ "US"
204
+ ]
205
+ },
206
+ "note": "The time zone of the business."
207
+ },
208
+ {
209
+ "id": "country.currency",
210
+ "sets": "audience.currency",
211
+ "ask": "Which money are prices in?",
212
+ "style": "cards",
213
+ "answers": [
214
+ {
215
+ "label": "The country's own",
216
+ "value": null,
217
+ "means": "A$ in Australia, ₱ in the Philippines, £ in the UK, US$ in the US."
218
+ },
219
+ {
220
+ "label": "US dollars",
221
+ "value": "USD",
222
+ "means": "Shown as US$1,400."
223
+ }
224
+ ],
225
+ "other": {
226
+ "kind": "text",
227
+ "min": 3,
228
+ "max": 3,
229
+ "description": "Another currency's three-letter code, such as NZD or EUR."
230
+ },
231
+ "note": "Write a currency other than the country's as `{ \"code\", \"symbol\" }` (NZD is NZ$). Ask it only when the client sells something; otherwise record null."
232
+ }
233
+ ],
234
+ "not_asked": [
235
+ {
236
+ "sets": "audience.format",
237
+ "why": "Date order, the clock, number separators, spelling and phone number shapes all come from the country's pack, so they match what readers expect without asking."
238
+ },
239
+ {
240
+ "sets": "audience.owner_pages",
241
+ "why": "Pages only the owner sees use workbench-ui's own times and money, not the readers'."
242
+ }
243
+ ]
244
+ }
88
245
  }
@@ -7,8 +7,7 @@
7
7
  "status": "built",
8
8
  "delivery": {
9
9
  "choice": "built_in",
10
- "intro": "The app's own database, one for testing and one for the live site.",
11
- "when": "The app needs to store or read data, or the client asks for a change to what is stored: a new table, column, rule or migration."
10
+ "intro": "The app's own database, one for testing and one for the live site."
12
11
  },
13
12
  "surfaces": {
14
13
  "package": [
@@ -237,5 +236,35 @@
237
236
  "cost_hint_usd": 0,
238
237
  "note": "On a project's staging: cf_turnstile_widget_create and the turnstile_sitekey and turnstile_secret sources; the sign-in page shows the check, /auth/code without a passed token sends no code, and with one it does"
239
238
  }
240
- ]
239
+ ],
240
+ "offer": {
241
+ "pattern": "settings",
242
+ "when": "The app needs to store or read data, or the client asks for a change to what is stored: a new table, column, rule or migration.",
243
+ "say": "The app's data goes in its own Workbench database, one for testing and one for the live site. I'll propose what to store and who can see it, and change it with a migration you can undo.",
244
+ "answers_file": "workbench/database.json",
245
+ "answers_format": "`{ \"format\": 1, \"status\": \"draft\", \"tables\": [{ \"name\": \"orders\", \"row\": \"one order\", \"columns\": [{ \"name\": \"total\", \"kind\": \"money\", \"required\": true }], \"read\": \"own\", \"change\": \"own\" }] }`. Then write one migration (`supabase/migrations/<14 digits>_<name>.sql`) with its down file, turning on row-level security with one policy per answer, apply it on staging with Workbench Control (`sb_migration_apply`), and refresh `supabase/staging-columns.json`.",
246
+ "questions": [
247
+ {
248
+ "id": "database.tables",
249
+ "sets": "tables",
250
+ "ask": "What should the app store, and who can see and change it?",
251
+ "style": "table",
252
+ "note": "Propose the tables the request needs, each with what one row is, its columns (name, kind such as text, number, money, date, yes/no, a link to another table; and whether it is required), and who may read and who may change its rows: `own` (the person who made the row), `admins`, `signed_in` (anyone signed in) or `public` (anyone, read only). Recommend `own` for anything about a person. Show money as whole cents, never a fraction. The client accepts, removes or adds tables and columns."
253
+ }
254
+ ],
255
+ "not_asked": [
256
+ {
257
+ "sets": "keys",
258
+ "why": "The app reaches the database as the signed-in person (`dbAs`), so row-level security decides what each person sees; the template holds no secret key."
259
+ },
260
+ {
261
+ "sets": "hosting",
262
+ "why": "Each project has its own staging and production databases, made by Workbench Control in the region set for the install."
263
+ },
264
+ {
265
+ "sets": "sign_in_settings",
266
+ "why": "The database's sign-in settings follow the Workbench baseline (the workbench-sign-in skill)."
267
+ }
268
+ ]
269
+ }
241
270
  }
@@ -7,8 +7,7 @@
7
7
  "status": "built",
8
8
  "delivery": {
9
9
  "choice": "optional",
10
- "intro": "Receiving email at the project's own addresses, for the app to act on.",
11
- "when": "The client wants the app to receive email: replies to its messages, or an address such as hello@ that the app handles."
10
+ "intro": "Receiving email at the project's own addresses, for the app to act on."
12
11
  },
13
12
  "surfaces": {
14
13
  "tools": [
@@ -129,5 +128,50 @@
129
128
  "two_step": true,
130
129
  "note": "cf_email_routing_rule_delete returns a confirmation and changes nothing until Robert approves"
131
130
  }
132
- ]
131
+ ],
132
+ "offer": {
133
+ "pattern": "switch_on",
134
+ "when": "The client wants the app to receive email: replies to its messages, or an address such as hello@ that the app handles.",
135
+ "say": "The project can already receive email with Workbench inbound email: each address goes straight to the app, which decides what to do with every message. I'll switch on the addresses you want.",
136
+ "answers_file": "workbench/email-inbound.json",
137
+ "answers_format": "`{ \"format\": 1, \"addresses\": [{ \"name\": \"hello\", \"does\": \"saves it as a message the admins see\" }] }`. Then set `INBOUND_EMAIL` to `\"on\"` in `wrangler.jsonc`, write `handleEmail` in `src/app.ts` to do what each address `does`, and make the addresses with Workbench Control: `cf_email_routing_enable`, then `cf_email_routing_rule_create` for each address on staging first (the capabilities guide below has the steps).",
138
+ "questions": [
139
+ {
140
+ "id": "email-inbound.addresses",
141
+ "sets": "addresses",
142
+ "ask": "Which addresses should the app receive, and what should it do with each message?",
143
+ "style": "table",
144
+ "note": "Propose the addresses the request needs, each with its name (the part before @, lower case: `hello`, `bookings`) and what the app does with a message, in the client's words: save it for the admins to read, add it to a booking, answer it automatically. One address, one job. Say plainly that the app reads the mail itself: nothing is forwarded to anyone's inbox."
145
+ },
146
+ {
147
+ "id": "email-inbound.attachments",
148
+ "sets": "attachments",
149
+ "ask": "What should happen to files attached to these emails?",
150
+ "style": "cards",
151
+ "answers": [
152
+ {
153
+ "label": "Leave them out",
154
+ "value": "drop",
155
+ "means": "The app keeps the words of each email, not its files. Nothing extra to store or check."
156
+ },
157
+ {
158
+ "label": "Keep them",
159
+ "value": "keep",
160
+ "means": "The app stores each file for the admins. Files can be large or unsafe, so keep only the types the job needs."
161
+ }
162
+ ],
163
+ "note": "With keep, ask which types (photos, PDFs) and store them outside the Worker, in file storage the app already has, or ask the owner before adding some; refuse other types with `reject`."
164
+ }
165
+ ],
166
+ "not_asked": [
167
+ {
168
+ "sets": "domain",
169
+ "why": "Addresses are on the project's own email domain: its staging name first, and the client's own domain once it is attached for production (`cf_zone_attach`)."
170
+ },
171
+ {
172
+ "sets": "forwarding",
173
+ "why": "Workbench never forwards mail outside the project or catches every address: each literal address goes to the app."
174
+ }
175
+ ]
176
+ }
133
177
  }
@@ -7,8 +7,7 @@
7
7
  "status": "built",
8
8
  "delivery": {
9
9
  "choice": "built_in",
10
- "intro": "The emails the app sends, such as confirmations, receipts and sign-in codes.",
11
- "when": "The app needs to send an email: a confirmation, receipt, notification, invitation or sign-in code."
10
+ "intro": "The emails the app sends, such as confirmations, receipts and sign-in codes."
12
11
  },
13
12
  "surfaces": {
14
13
  "gateway": ["POST /v1/email/send"],
@@ -71,5 +70,70 @@
71
70
  ],
72
71
  "styles": {
73
72
  "prefix": "wb-email-"
73
+ },
74
+ "offer": {
75
+ "pattern": "settings",
76
+ "when": "The app needs to send an email: a confirmation, receipt, notification, invitation or sign-in code.",
77
+ "say": "I'll send these with Workbench email, which sends from the app's own address in your brand, with no email key in the app. The sign-in codes are already there; I'll add yours beside them.",
78
+ "answers_file": "workbench/email.json",
79
+ "answers_format": "`{ \"format\": 1, \"status\": \"draft\", \"reply_to\": null, \"emails\": [{ \"id\": \"order_confirmed\", \"when\": \"a customer sends an order\", \"to\": \"the customer\", \"parts\": [\"heading\", \"paragraph\", \"button\"], \"link_changes_something\": false }] }`. Declare each email in `src/lib/emails.ts` with `defineEmails` (stream `transactional`, shell `account`, `decidedBy` naming this answers file), put its words in the strings under `email.<id>.<key>`, and send it with `mailerFor(env)` and an `idempotencyKey` whenever a retry could repeat it.",
80
+ "questions": [
81
+ {
82
+ "id": "email.emails",
83
+ "sets": "emails",
84
+ "ask": "Which emails should the app send?",
85
+ "style": "table",
86
+ "note": "Propose the emails the request needs, each with an id (snake_case: `order_confirmed`), when it is sent, who gets it, its parts in order (heading, paragraphs, a button and its link, small print) and whether its link changes something (then the link lands on a confirmation page first). Only emails a person's own action or account causes: receipts, confirmations, notices. The client accepts, removes or adds. The wording is drafted afterwards, one email at a time."
87
+ },
88
+ {
89
+ "id": "email.reply-to",
90
+ "sets": "reply_to",
91
+ "ask": "Where should a reply to these emails go?",
92
+ "style": "cards",
93
+ "answers": [
94
+ {
95
+ "label": "Nowhere",
96
+ "value": null,
97
+ "means": "The emails come from an address that isn't read. Say so in the small print, with where to get help instead."
98
+ },
99
+ {
100
+ "label": "To the app",
101
+ "value": "app",
102
+ "means": "Replies go to an address the app reads itself (Workbench inbound email)."
103
+ }
104
+ ],
105
+ "other": {
106
+ "kind": "text",
107
+ "min": 6,
108
+ "max": 254,
109
+ "description": "An email address someone at the business reads."
110
+ },
111
+ "must_ask": true,
112
+ "note": "A typed address goes in each send as `replyTo`; check it looks like an email address. With the app, the workbench-email-inbound skill sets up the address."
113
+ }
114
+ ],
115
+ "not_asked": [
116
+ {
117
+ "sets": "sender",
118
+ "why": "Emails come from the project's own sending address (`FROM` in `src/lib/email.ts`) on its own domain, which Workbench Control sets up."
119
+ },
120
+ {
121
+ "sets": "marketing",
122
+ "why": "Only transactional email exists: newsletters and other bulk mail are refused until Workbench has a sender for them, so offer none."
123
+ },
124
+ {
125
+ "sets": "look",
126
+ "why": "Emails take the brand's name, colour and logo, and the project's `project.css` (see Restyling)."
127
+ }
128
+ ],
129
+ "drafts": {
130
+ "of": "wording for one email",
131
+ "surface": "ai",
132
+ "alias": "claude-default",
133
+ "count": 3,
134
+ "max_rounds": 3,
135
+ "job": "ai.drafts",
136
+ "command": "npx workbench-ai drafts text email_<id> --about \"<what the email is for, who gets it and its parts>\""
137
+ }
74
138
  }
75
139
  }
@@ -745,10 +745,6 @@
745
745
  "type": "string",
746
746
  "minLength": 1,
747
747
  "maxLength": 160
748
- },
749
- "when": {
750
- "type": "string",
751
- "minLength": 1
752
748
  }
753
749
  },
754
750
  "required": [
@@ -7,8 +7,7 @@
7
7
  "status": "built",
8
8
  "delivery": {
9
9
  "choice": "built_in",
10
- "intro": "The words on the app's screens and in its emails, kept in one place for each language.",
11
- "when": "The client wants to add or change wording on a page or in an email, or asks about showing the app in another language."
10
+ "intro": "The words on the app's screens and in its emails, kept in one place for each language."
12
11
  },
13
12
  "surfaces": {
14
13
  "package": [
@@ -132,5 +131,49 @@
132
131
  "cost_hint_usd": 0,
133
132
  "note": "The app's own checkStrings run passes in its tests, and npx workbench-strings check passes in CI"
134
133
  }
135
- ]
134
+ ],
135
+ "offer": {
136
+ "pattern": "project",
137
+ "when": "The client wants to add or change wording on a page or in an email, or asks about showing the app in another language.",
138
+ "say": "Every word the app shows is kept in one place with Workbench strings, so changing wording never means hunting through code, and other languages can be added later.",
139
+ "answers_file": "workbench/strings.json",
140
+ "answers_format": "`{ \"format\": 1, \"languages\": [\"en-AU\"], \"later\": [\"fil\"] }`: the language the site is written in first, and any the client wants later. New wording goes into `strings/strings.<base>.json` with one key per sentence, and `npm run check:strings` checks it.",
141
+ "questions": [
142
+ {
143
+ "id": "strings.languages",
144
+ "sets": "languages",
145
+ "ask": "Does the site need to be in any language besides English?",
146
+ "style": "cards",
147
+ "answers": [
148
+ {
149
+ "label": "English only",
150
+ "value": "none",
151
+ "means": "Every page and email is in English."
152
+ },
153
+ {
154
+ "label": "Another language later",
155
+ "value": "later",
156
+ "means": "English now. The words are kept so a translation can be added, checked by a native speaker, without changing the code."
157
+ }
158
+ ],
159
+ "other": {
160
+ "kind": "text",
161
+ "min": 2,
162
+ "max": 120,
163
+ "description": "The languages, such as Filipino or Spanish."
164
+ },
165
+ "note": "Translations come with a Workbench tool that isn't built yet: record the languages under `later`, keep every word in the catalogue, and tell the client a translation goes live only once a named native speaker has checked it."
166
+ }
167
+ ],
168
+ "not_asked": [
169
+ {
170
+ "sets": "base",
171
+ "why": "The site is written first in English, in the readers' spelling from the country answers, so it isn't asked again."
172
+ },
173
+ {
174
+ "sets": "wording",
175
+ "why": "Wording is content, not a setting: the client says what they want in their own words, or asks for drafts (the workbench-ai skill), and it goes into the catalogue."
176
+ }
177
+ ]
178
+ }
136
179
  }
@@ -7,8 +7,7 @@
7
7
  "status": "built",
8
8
  "delivery": {
9
9
  "choice": "optional",
10
- "intro": "A ready-made look for the pages only you and your admins see.",
11
- "when": "The client wants a page only the owner or the app's admins see: a dashboard, a list, a settings page or a status page."
10
+ "intro": "A ready-made look for the pages only you and your admins see."
12
11
  },
13
12
  "surfaces": {
14
13
  "package": [
@@ -95,5 +94,31 @@
95
94
  ],
96
95
  "styles": {
97
96
  "prefix": "wb-ui-"
97
+ },
98
+ "offer": {
99
+ "pattern": "switch_on",
100
+ "when": "The client wants a page only the owner or the app's admins see: a dashboard, a list, a settings page or a status page.",
101
+ "say": "I'll build this with the Workbench owner-page look, the same top bar, cards and pills as the other admin pages, so it's quick to build and consistent. Your customers never see it.",
102
+ "answers_file": "workbench/ui.json",
103
+ "answers_format": "`{ \"format\": 1, \"pages\": [{ \"path\": \"/admin/orders\", \"what\": \"every order this month, newest first\", \"who\": \"admins\" }] }`. Build each page with `shell`, `topBar`, `card` and `pill`. An `admins` page lives under `/admin/` and checks `requireUser` and `app_admins` (the workbench-sign-in skill); an `owner` page lives under `/owner/` and is behind Access in production.",
104
+ "questions": [
105
+ {
106
+ "id": "ui.pages",
107
+ "sets": "pages",
108
+ "ask": "Which admin pages should the app have?",
109
+ "style": "table",
110
+ "note": "Propose the pages the request needs, each with its path, what it shows or lets them change in the client's words, and who sees it: `admins` (the business's people, who sign in to the app) or `owner` (whoever runs the app's Workbench, through Access). Recommend `admins` unless the page is about running the app itself, such as a status page. The client accepts, removes or adds pages."
111
+ }
112
+ ],
113
+ "not_asked": [
114
+ {
115
+ "sets": "look",
116
+ "why": "Every admin page has the same Workbench look, so the business's people learn it once. Restyle it in project.css (see Restyling) if the client wants their colours."
117
+ },
118
+ {
119
+ "sets": "formats",
120
+ "why": "Times, amounts and durations on these pages come from workbench-ui's `when`, `money` and `duration`. What customers see uses the readers' formats (the workbench-country skill)."
121
+ }
122
+ ]
98
123
  }
99
124
  }