@clickless/react 0.0.0-stage → 0.1.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1095 -2
  3. package/dist/actions/actionCalls.d.ts +50 -0
  4. package/dist/actions/actionRegistry.d.ts +78 -0
  5. package/dist/components/Adaptive.d.ts +116 -0
  6. package/dist/components/AdaptiveAction.d.ts +4 -0
  7. package/dist/components/AdaptiveProvider.d.ts +38 -0
  8. package/dist/components/AdaptiveRegion.d.ts +60 -0
  9. package/dist/components/ClicklessUiErrorBoundary.d.ts +24 -0
  10. package/dist/components/OriginalToolbar.d.ts +21 -0
  11. package/dist/components/PromptBar.d.ts +21 -0
  12. package/dist/components/PromptPanel.d.ts +103 -0
  13. package/dist/components/WorkingDots.d.ts +4 -0
  14. package/dist/components/focusAfterPromptAction.d.ts +15 -0
  15. package/dist/components/hostColorScheme.d.ts +35 -0
  16. package/dist/components/promptPanelStyles.d.ts +14 -0
  17. package/dist/core/applyOperations.d.ts +21 -0
  18. package/dist/core/colorContrast.d.ts +22 -0
  19. package/dist/core/defaultLayout.d.ts +12 -0
  20. package/dist/core/index.d.ts +8 -0
  21. package/dist/core/ownProperties.d.ts +11 -0
  22. package/dist/core/propValues.d.ts +8 -0
  23. package/dist/core/reconcileLayout.d.ts +59 -0
  24. package/dist/core/schemas.gen.d.ts +427 -0
  25. package/dist/core/validateActionCalls.d.ts +28 -0
  26. package/dist/core/validateManifest.d.ts +11 -0
  27. package/dist/core/validateOperations.d.ts +15 -0
  28. package/dist/engine/engineClient.d.ts +231 -0
  29. package/dist/engine/engineErrorMessages.d.ts +13 -0
  30. package/dist/engine/engineResponses.d.ts +201 -0
  31. package/dist/engine/plainReplies.d.ts +2 -0
  32. package/dist/hooks/useActionRunHistory.d.ts +12 -0
  33. package/dist/hooks/useAdaptiveAction.d.ts +11 -0
  34. package/dist/hooks/useAdaptiveRegion.d.ts +34 -0
  35. package/dist/hooks/useChangeHistory.d.ts +32 -0
  36. package/dist/hooks/useDictation.d.ts +50 -0
  37. package/dist/hooks/useEngineReachable.d.ts +5 -0
  38. package/dist/hooks/usePromptBar.d.ts +163 -0
  39. package/dist/hooks/useRegionGroup.d.ts +15 -0
  40. package/dist/index.d.ts +21 -0
  41. package/dist/index.js +6600 -0
  42. package/dist/presets/mergePresets.d.ts +20 -0
  43. package/dist/presets/propPresets.d.ts +305 -0
  44. package/dist/region/changedComponentIds.d.ts +13 -0
  45. package/dist/region/firstPaintOrder.d.ts +19 -0
  46. package/dist/region/previewRequests.d.ts +21 -0
  47. package/dist/region/regionContext.d.ts +96 -0
  48. package/dist/region/regionGroup.d.ts +189 -0
  49. package/dist/region/regionSnapshot.d.ts +52 -0
  50. package/dist/region/regionStore.d.ts +361 -0
  51. package/dist/region/responsiveColumns.d.ts +35 -0
  52. package/dist/region/retrier.d.ts +22 -0
  53. package/dist/region/savedLayoutParsing.d.ts +7 -0
  54. package/dist/region/settleIntoSavedLayout.d.ts +14 -0
  55. package/dist/region/textDirection.d.ts +12 -0
  56. package/dist/region/unsavedChangeSaver.d.ts +45 -0
  57. package/dist/region/unsavedChanges.d.ts +78 -0
  58. package/dist/region/userFacingTitle.d.ts +7 -0
  59. package/package.json +59 -3
package/README.md CHANGED
@@ -1,3 +1,1096 @@
1
- # Temporary Holding Version
1
+ # @clickless/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Lets the users of a React application shape its interface and use its
4
+ features by asking in plain words, typed or spoken:
5
+
6
+ - **Reshape the interface.** Users rearrange dashboards, hide sections, choose
7
+ which table columns show, change colours or adjust text size. Each change is
8
+ previewed before it is kept, can be undone, and is saved for that user alone.
9
+ - **Carry out actions.** Users ask for what your application already offers:
10
+ "show this week's unpaid invoices", "change my name to Joshua". Actions you
11
+ mark as only changing what is shown happen at once; actions that change data
12
+ wait for the user to confirm.
13
+
14
+ You mark which parts of a screen may change and how, and which actions users
15
+ may run. The model only ever proposes what you declared, and the library checks
16
+ every proposal against your declarations before anything is shown, saved or
17
+ run. If the engine can't be reached, your application keeps working as you
18
+ built it.
19
+
20
+ - [Install](#install)
21
+ - [Quick start](#quick-start)
22
+ - [Choosing what users can change](#choosing-what-users-can-change)
23
+ - [Doing things for the user: actions](#doing-things-for-the-user-actions)
24
+ - [Connecting to the engine](#connecting-to-the-engine)
25
+ - [Fitting a region to the page's own layout](#fitting-a-region-to-the-pages-own-layout)
26
+ - [A screen made of several regions](#a-screen-made-of-several-regions)
27
+ - [The prompt panel](#the-prompt-panel)
28
+ - [Speaking a request](#speaking-a-request)
29
+ - [Building your own controls](#building-your-own-controls)
30
+ - [When your screens change](#when-your-screens-change)
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ npm install @clickless/react
36
+ ```
37
+
38
+ This one package is all you need: it carries `@clickless/core`'s code and
39
+ types, and re-exports the format's types (`Layout`, `Operation`,
40
+ `PropCapabilities` and the rest). React 18 or later is required, and its only
41
+ other dependency is `zod`.
42
+
43
+ Until version 1.0, a minor version (0.1 to 0.2) may change the API, and the
44
+ release notes say what to change.
45
+
46
+ ## Quick start
47
+
48
+ ```tsx
49
+ import { Adaptive, AdaptiveProvider, AdaptiveRegion, PromptPanel, propPresets } from "@clickless/react";
50
+
51
+ <AdaptiveProvider userId={currentUser.id} engine={{ url: "https://clickless.example.com", getToken }}>
52
+ <AdaptiveRegion id="dashboard" version="1">
53
+ <Adaptive
54
+ id="revenue"
55
+ kind="chart"
56
+ title="Revenue by month"
57
+ defaults={{ w: 6, h: 3 }}
58
+ presets={{
59
+ months: propPresets.count({ max: 12, initial: 6 }),
60
+ chartType: propPresets.choice({ values: ["bar", "line"], initial: "bar", means: "the kind of chart" }),
61
+ }}
62
+ >
63
+ {(props, adaptiveControls, presetValues) => <RevenueChart type={presetValues.chartType} months={presetValues.months} />}
64
+ </Adaptive>
65
+ <Adaptive id="txns" kind="table" title="Recent transactions" defaults={{ w: 6, h: 3 }}>
66
+ <TransactionsTable />
67
+ </Adaptive>
68
+ </AdaptiveRegion>
69
+ <PromptPanel regionId="dashboard" />
70
+ </AdaptiveProvider>
71
+ ```
72
+
73
+ - **`AdaptiveProvider`** connects the page to the ClickLess engine, which stores
74
+ each user's layout and plans changes from prompts. `userId` is whoever is
75
+ signed in: the user id your backend puts in the token's `sub`. When it
76
+ changes, as when someone signs out and another signs in on a shared
77
+ computer, everything inside the provider is mounted afresh, so the next user
78
+ never sees what the last one typed. A request is sent only with a token for
79
+ `userId`, so nothing the last user left unsaved is saved to the next one's
80
+ account, wherever in your application they land. The same holds for the
81
+ tenant of the first token the provider sends: where someone can switch
82
+ tenant without a reload, give the provider the tenant as its `key`. A
83
+ `userId` that is not the token's `sub` sends nothing at all, and says so in
84
+ the browser console. A region shows its default
85
+ until its saved layout arrives; list in `preloadRegions` any that mount late,
86
+ such as a sidebar a phone draws only once its menu opens, and they are read
87
+ up front so they show the saved layout at once. Regions, sections,
88
+ `AdaptiveAction`, `useAdaptiveRegion` and `useAdaptiveAction` also work
89
+ outside the provider,
90
+ as in your own tests or stories: regions show their defaults, nothing is
91
+ saved, no action is offered, and the browser console says so once.
92
+ `useAdaptiveRegion` then reads the region it sits in; outside that region
93
+ too, it reads an empty one of its own. `PromptPanel`,
94
+ `PromptBar`, `usePromptBar`, `useChangeHistory` and `useDictation` still
95
+ need the provider, since they have nothing to offer without it.
96
+ - **`AdaptiveRegion`** is one part of a screen whose sections can be rearranged.
97
+ Its `id` is what each user's layout is saved under.
98
+ - **`Adaptive`** wraps one section. `title` is what users call it; the model
99
+ uses it to know which section "the revenue chart" means, and the panel uses it
100
+ in what it says. Without one the model is given the `id`, and the panel,
101
+ which never shows users an id, speaks of "a section".
102
+ - **`PromptPanel`** is the button and panel where users ask for changes.
103
+
104
+ ## Choosing what users can change
105
+
106
+ Everything a user can change about a section is declared on its `Adaptive`.
107
+ What is not declared cannot be changed: a request for it is refused, and the
108
+ panel's reply says, in plain words, that it cannot be done.
109
+
110
+ | To let users | Declare | On by default |
111
+ |---|---|---|
112
+ | Move it among the other sections | `capabilities={{ move: true }}` | yes |
113
+ | Hide it and show it again | `capabilities={{ hide: true }}` | yes |
114
+ | Make it wider, narrower, taller or shorter | `capabilities={{ resize: { minW, maxW, minH, maxH } }}` | no |
115
+ | Change a setting of it, such as a colour | `presets={{ … }}` ([below](#settings-built-in-and-custom)) | no |
116
+
117
+ Moving and hiding are on unless you turn them off, so an `Adaptive` with no
118
+ `capabilities` at all can already be moved and hidden. (In a region drawn with
119
+ `grid={false}`, only sections in a `group` can be moved; see
120
+ [below](#fitting-a-region-to-the-pages-own-layout).) To stop either, say so:
121
+
122
+ ```tsx
123
+ <Adaptive id="balance" title="Account balance" capabilities={{ move: false, hide: false }}>
124
+ ```
125
+
126
+ `locked` turns off every change at once. Showing is not a separate capability:
127
+ a hidden section can always be shown again.
128
+
129
+ ### Resizing
130
+
131
+ Sizes are counted in the region's grid: `w` in columns (12 across by default)
132
+ and `h` in rows. Give the starting size in `defaults` and the limits in
133
+ `resize`:
134
+
135
+ ```tsx
136
+ <Adaptive
137
+ id="pipeline"
138
+ title="Sales pipeline"
139
+ capabilities={{ resize: { minW: 4, maxW: 12, minH: 2, maxH: 6 } }}
140
+ defaults={{ w: 8, h: 4 }}
141
+ >
142
+ ```
143
+
144
+ ### Settings: built-in and custom
145
+
146
+ A setting is anything about a section you are willing to let users choose: a
147
+ chart type, the columns a table shows, an accent colour, a font size. You
148
+ declare each one in `presets`, under a name of your choosing, and read its
149
+ current value from the render function's third argument, under the same name
150
+ and already of the right type. ClickLess stores the value and hands it to you;
151
+ **what it changes on screen is up to you**, since ClickLess cannot reach inside
152
+ your component.
153
+
154
+ Each setting brings its starting value and a sentence telling the model what it
155
+ means, so a request in the user's own words ("tighter rows", "show 6 months")
156
+ lands on the right setting.
157
+
158
+ **Use a built-in setting when one fits.** Its name, values and wording are the
159
+ same in every application, and tested:
160
+
161
+ | Setting | Lets users change | You receive |
162
+ |---|---|---|
163
+ | `count({ max, initial, min?, of? })` | how many entries show | a number |
164
+ | `highlight({ top?, above?, below?, kinds?, entries?, colour?, suggested? })` | which entries stand out: the top few (such as the top 3), those above or below an amount (such as over £1,000, or due before 2 pm), or those of chosen kinds (such as cancelled orders) | `styleFor(position, value)`, a style for each entry; also `isHighlighted`, `colour`, `textColour` |
165
+ | `density({ initial? })` | how tightly rows sit | `"comfortable"` or `"compact"` |
166
+ | `stripes({ suggested?, textOn?, against? })` or `stripes({ palette })` | colouring every other row | `{ colour, textColour }`, or `null` when off |
167
+ | `colours({ background?, textColour?, suggested? })` | the background and text colour | `style`, both as a style; also `background`, `textColour`, `chosenTextColour` |
168
+ | `textSize({ min?, max?, initial? })` | the size of the text, in pixels | a number |
169
+ | `textStyle({ initial? })` | bold, italic, both or neither | `{ bold, italic }` |
170
+ | `accent({ marks, colour?, suggested?, textOn?, against? })` | the colour that marks what stands out, such as today or the primary buttons | `{ colour, textColour }` |
171
+
172
+ ```tsx
173
+ <Adaptive
174
+ id="txns"
175
+ title="Recent transactions"
176
+ defaults={{ w: 12, h: 4 }}
177
+ presets={{
178
+ count: propPresets.count({ min: 3, max: 25, initial: 8 }),
179
+ rows: propPresets.density(),
180
+ highlight: propPresets.highlight({ above: { measure: "amount in pounds" } }),
181
+ }}
182
+ >
183
+ {(props, adaptiveControls, presetValues) => (
184
+ <TransactionsTable
185
+ rowCount={presetValues.count}
186
+ compact={presetValues.rows === "compact"}
187
+ rowStyle={(rowIndex, transaction) => presetValues.highlight.styleFor(rowIndex, transaction.amount)}
188
+ />
189
+ )}
190
+ </Adaptive>
191
+ ```
192
+
193
+ `count`'s `of` and `highlight`'s `entries` name what is counted or highlighted,
194
+ in the plural, where the section's `title` does not make it plain. `highlight`'s
195
+ `above.measure` and `below.measure` say what is compared, with its unit and
196
+ without "the", since "over 1,000" means nothing on its own. A time of day is a
197
+ number too: "due time in hours from midnight, such as 13.5 for 1:30 pm", with
198
+ `max: 24`, lets users colour what is due before 2 pm. `above` and `below`
199
+ compare the same value, so given together they name the same measure. No
200
+ highlight has an upper limit unless you give `max`, so it suits a list whose
201
+ contents change.
202
+
203
+ `highlight`'s `kinds` colours the entries of the kinds users choose, from a
204
+ fixed set you name: `kinds: { of: "statuses", values: ["placed", "shipped",
205
+ "cancelled"] }`. Give each entry's kind where you would give its amount,
206
+ `styleFor(rowIndex, { kind: order.status })`, or both,
207
+ `styleFor(rowIndex, { measure: hours, kind: order.status })`; an entry is
208
+ coloured when any rule in use holds. The values are kinds an entry falls into,
209
+ such as a status, a priority or a category, never the entries themselves: a
210
+ person's or a customer's name is never one, so no one's name is sent to the
211
+ engine or kept in a layout.
212
+
213
+ A built-in setting's props keep their shared names (`density`, `stripes`)
214
+ whatever you file it under.
215
+
216
+ **Start with the props your components already have.** Most settings are one
217
+ value for the whole section, a number, a word or a colour, and go straight into a
218
+ prop the component already takes (`type`, `variant`, `pageSize`) or into `style`,
219
+ so the component itself does not change. Where a setting decides something for
220
+ each entry, as `highlight` does, it hands you a ready `style` for each one, to
221
+ pass to the entry's own `style`:
222
+
223
+ ```tsx
224
+ <tr style={presetValues.highlight.styleFor(rowIndex, transaction.amount)}>
225
+ ```
226
+
227
+ The value you pass, here `transaction.amount`, is the one `measure` describes:
228
+ the setting stores only the amount the user asked for, and compares it with what
229
+ you give it.
230
+
231
+ **Otherwise, a custom setting**, for what only your application knows: its own
232
+ views, its columns, its colours. You give the values and a few plain words saying
233
+ what the setting means, in `means` (or `shows`), and the setting takes the name
234
+ you file it under.
235
+
236
+ **On or off**, for showing or hiding one part:
237
+
238
+ ```tsx
239
+ presets={{ trend: propPresets.toggle({ shows: "the line comparing it with last month" }) }}
240
+ // presetValues.trend is true or false
241
+ ```
242
+
243
+ **One choice from a list**, such as a chart type:
244
+
245
+ ```tsx
246
+ presets={{
247
+ chartType: propPresets.choice({
248
+ values: ["bar", "line", "area"],
249
+ initial: "bar",
250
+ means: "the kind of chart",
251
+ }),
252
+ }}
253
+ // presetValues.chartType is "bar" | "line" | "area"
254
+ ```
255
+
256
+ A request for something that is not listed ("make it a pie chart") is answered
257
+ with the closest listed option, where one is a sensible stand-in, and the reply
258
+ says which was used. Where none is, the reply names the options there are.
259
+
260
+ **Several entries, in order**, such as the columns a table shows. `min` is the
261
+ fewest entries allowed, and `initial` the ones that show to begin with (all of
262
+ them unless given):
263
+
264
+ ```tsx
265
+ presets={{
266
+ columns: propPresets.visibleList({
267
+ values: ["date", "payee", "category", "amount"],
268
+ initial: ["date", "payee", "amount"],
269
+ min: 1,
270
+ means: "the columns that show, left to right",
271
+ }),
272
+ }}
273
+ // presetValues.columns is the list, in the order to show it
274
+ ```
275
+
276
+ The value is the list, in the order to show it, so hiding, showing and
277
+ reordering columns are all one kind of change. A column you add to `values` in
278
+ a later release, and include in `initial`, appears for existing users too; one
279
+ a user removed stays removed.
280
+
281
+ `required` lists entries the list must always hold. They can be moved but never
282
+ left out, and one missing from a saved layout goes back in beside its neighbour
283
+ in `initial`, or at the end if `initial` does not list it. The value may change
284
+ from render to render, such as keeping the tab of the page that is open, so the
285
+ user always has a way back to it:
286
+
287
+ ```tsx
288
+ pages: propPresets.visibleList({
289
+ values: Object.keys(PAGES),
290
+ min: 1,
291
+ required: [currentPageKey],
292
+ means: "the tabs that show, left to right",
293
+ }),
294
+ ```
295
+
296
+ Say why in `description`, so a request to hide it is declined with a reason.
297
+
298
+ **A colour.** Any colour is accepted, written as `#rrggbb`. `suggested` lists
299
+ your own brand colours, which the model prefers when the user names none ("use
300
+ one of our colours"); it does not restrict the user:
301
+
302
+ ```tsx
303
+ presets={{
304
+ barColour: propPresets.colour({
305
+ initial: "#1c5cab",
306
+ suggested: ["#1c5cab", "#0f766e", "#b45309"],
307
+ means: "the colour of the bars",
308
+ }),
309
+ }}
310
+ ```
311
+
312
+ **Text drawn on a chosen colour.** Every setting that hands you a chosen colour
313
+ also hands you the text colour to draw on it, worked out for you: `accent`,
314
+ `colour` and `stripes` read as `{ colour, textColour }`, where `textColour` is
315
+ black or white, whichever reads better on `colour`. `highlight` gives
316
+ `textColour` too, and `colours` keeps the text colour the user chose where it
317
+ reads on the background and turns it black or white where it does not.
318
+
319
+ Give `textOn: true` where you draw text on the colour in `textColour`, such as
320
+ a button's label. The model is then told the text stays readable, and when a
321
+ chosen colour turns it from white to black or back, the reply says so: "White
322
+ text is hard to read on orange, so I've made it black." Give `against` for the
323
+ colour behind it, such as the page's, and the reply says when a chosen colour
324
+ would be hard to see there: white buttons on a white page, or a yellow link
325
+ colour on white.
326
+
327
+ ```tsx
328
+ <Adaptive id="primary-buttons" title="Primary buttons"
329
+ presets={{ accent: propPresets.accent({ marks: "the primary buttons", textOn: true }) }}>
330
+ {(props, adaptiveControls, { accent }) => (
331
+ <Button style={{ background: accent.colour, color: accent.textColour }}>Save</Button>
332
+ )}
333
+ </Adaptive>
334
+ ```
335
+
336
+ For a colour you pair with text yourself, `readableTextColor(background)`
337
+ returns black or white for it, and `ensureReadableTextColor(text, background)`
338
+ keeps a text colour the user chose only where it is readable on that background,
339
+ falling back to black or white.
340
+
341
+ **A number in a range**, such as a corner radius. Numbers are whole unless you
342
+ pass `whole: false`. A request past either end is brought to that end, and the
343
+ reply says so:
344
+
345
+ ```tsx
346
+ presets={{
347
+ corners: propPresets.number({ min: 0, max: 24, initial: 6, means: "the corner radius in pixels" }),
348
+ }}
349
+ ```
350
+
351
+ If a custom setting you need keeps coming up, tell us: settings that recur
352
+ across applications are made built-in.
353
+
354
+ ### What gets refused
355
+
356
+ Every change is checked against the declarations before it is previewed or
357
+ saved, by the library in the browser and again by the engine:
358
+
359
+ | A request to… | Is refused because |
360
+ |---|---|
361
+ | change a setting the section never declared | no setting in `presets` has that name |
362
+ | set a value the setting does not allow (an unlisted option, a number out of range, a colour not written `#rrggbb`) | it is checked against the setting's kind |
363
+ | resize a section with no `resize` limits, or past them | resizing was not declared, or the size is out of range |
364
+ | move or hide a section with `move: false` or `hide: false` | that capability is off |
365
+ | change a `locked` section in any way | it is locked |
366
+ | hide the last section showing in a region | a region always keeps something showing |
367
+
368
+ Users never see these rules or their codes. The model plans only within what
369
+ you declared, and when part of a request is outside it, the reply says what
370
+ could not be done and the rest goes ahead. A request that could not be carried
371
+ out is also recorded for your team, so you can
372
+ see which settings users ask for that you do not offer yet.
373
+
374
+ A region always keeps at least one section showing, so no request empties a
375
+ page. Where a region has sections with a size, only those count: buttons, menus
376
+ and tabs, which the page sizes itself, do not keep a page of cards and charts
377
+ from looking empty, though the last section of any kind stays too. "Hide
378
+ everything on this page" hides all but the first of them and the reply says
379
+ which one stayed and that "show everything" brings the rest back; with a sidebar
380
+ on the same screen, each region keeps one. The same change through
381
+ `applyOperations` is refused with the code `empty_region`. Nothing has to be
382
+ declared for this. A region that shows nothing by default may be left showing
383
+ nothing. The prompt panel is not a section, so it stays on screen to bring
384
+ everything back.
385
+
386
+ Declarations are what users may change, not a security boundary: the engine
387
+ checks a change against the manifest sent with it, which comes from the
388
+ browser.
389
+
390
+ ## Doing things for the user: actions
391
+
392
+ Besides reshaping a screen, the panel can do things for the user that your
393
+ application already does: change their name, upload a profile picture, open a
394
+ customer's record. You offer each one as an action, with a function that does it
395
+ the way your own screens would:
396
+
397
+ ```tsx
398
+ import { ActionError, useAdaptiveAction } from "@clickless/react";
399
+
400
+ useAdaptiveAction({
401
+ name: "updateName",
402
+ title: "Change your name",
403
+ description: "Changes the signed-in user's first name, last name, or both. Give only what changes.",
404
+ parameters: {
405
+ firstName: { type: "text", description: "First name", required: false },
406
+ lastName: { type: "text", description: "Last name", required: false },
407
+ },
408
+ confirm: ({ firstName, lastName }) => `Change your name to ${firstName} ${lastName}?`,
409
+ run: async (changes) => {
410
+ const response = await fetch("/api/me", { method: "PATCH", body: JSON.stringify(changes) });
411
+ if (!response.ok) throw new ActionError("Your name couldn't be changed just now.");
412
+ return "Your name has been changed.";
413
+ },
414
+ });
415
+ ```
416
+
417
+ A user then asks, in words or by voice: "update my first name to Joshua and my
418
+ last name to King".
419
+
420
+ - **`run` makes the request, in the browser.** It goes out with the user's own
421
+ sign-in, so your backend's permissions and rules apply exactly as they do for
422
+ your own forms. ClickLess never holds credentials for your API and never
423
+ calls it.
424
+ - **Changes are confirmed first.** An action is a `write` unless you say
425
+ otherwise: the panel shows what will happen (your `confirm` sentence, or the
426
+ action's title with each value given) and runs it only when the user presses
427
+ Confirm. `mode: "read"` is for actions that only change what is shown, such as
428
+ opening a page or a tab; they run at once. A read action must not change data.
429
+ When the same request also previews a layout change, the read action waits
430
+ ("Then: …" under Keep and Discard): Keep runs it, Discard turns it down with
431
+ the preview and says so under the reply. A change waiting to be confirmed, or
432
+ to follow a preview, is cancelled when the user sends another request that
433
+ replaces the preview, or when no preview shows, and the conversation says so.
434
+ A follow-up that only adds to the preview, or a question about it, leaves it
435
+ waiting.
436
+ - **Missing details are asked for.** If the user leaves something out ("change
437
+ my last name"), the panel asks for it, and their answer is understood as the
438
+ reply to that question. The panel keeps the last few requests on a screen for
439
+ this.
440
+ - **What a change's `run` resolves to is shown to the user.** Return a
441
+ sentence, or nothing for "Done.". Throw an `ActionError` to tell the user why
442
+ it could not be done; any other error shows a general "That couldn't be done"
443
+ so nothing technical reaches them. A read action is quiet when it works, since
444
+ the planner's reply already says what it is showing; set `showResult: true`
445
+ on one whose answer the user needs before deciding, such as the new times a
446
+ change later in the same request will use, and its sentence shows under the
447
+ reply, above the confirmation.
448
+ - **The planner is told only how it ended.** Later requests tell the planner,
449
+ the AI model behind the panel, which action ran and whether it was done,
450
+ failed or was cancelled ("Mark an invoice paid — done."), never the sentence
451
+ `run` returned or the `ActionError` message. So "Invoice INV-2041 for Acme Ltd, £12,400" reaches the user's
452
+ screen and goes no further. To let a follow-up build on what an action found
453
+ ("now email that invoice"), set `shareResultWithPlanner: true` on it: the
454
+ sentence and the failure message then also go to the AI model (Anthropic)
455
+ with the user's next few requests.
456
+ - **Only the screen's actions are offered.** An action is offered while the
457
+ component that declares it is mounted, so a profile page's actions exist only
458
+ on that page, and one declared near the top of your app exists everywhere.
459
+ `<AdaptiveAction … />` does the same as the hook, from JSX.
460
+ - **Screens showing what an action changed must load it again.** ClickLess
461
+ runs your `run` and redraws nothing of your app. An action declared on the
462
+ page it changes can load that page's data again at the end of `run`. One
463
+ declared near the top of your app, and so offered everywhere, changes data
464
+ that another page may be showing; that page keeps the copy it loaded until
465
+ told otherwise. Have it listen for changes the way your app already shares
466
+ them: refresh the cached data when you use a data library (TanStack Query's
467
+ `invalidateQueries`, SWR's `mutate`), or subscribe to your data client's
468
+ own change events.
469
+ - **Undo is for changes to the screen.** An action's effect is your
470
+ application's, which ClickLess cannot take back, so once an action has run,
471
+ read or write, the prompt panel and bar do not offer Undo until the screen is
472
+ changed again: it would otherwise take back an older change the user is not
473
+ thinking of. This holds for the rest of the visit to the page; after a
474
+ reload, Undo takes back the latest change to the screen again. To reverse an
475
+ action, the user asks for it ("switch back to light mode").
476
+ `useAdaptiveRegion(id).undoLastChange` always takes back the latest change
477
+ to the screen.
478
+ - **Past actions stay listed.** The engine keeps a record of each action run,
479
+ or turned down: the action, its outcome and the names of the fields it was
480
+ given, never their values. The panel lists each past action by its title
481
+ and how it ended ("Change your contact details", Done) after the page is
482
+ loaded again. The request and the reply are not kept.
483
+ - **What no action covers is recorded.** "Delete my account", with no action for
484
+ it, is answered with what can be done instead, and recorded as a missed
485
+ request for your team, as layout requests are.
486
+
487
+ Parameters come in four kinds: `{ type: "text", values?, maxLength? }`,
488
+ `{ type: "number", min?, max?, integer? }`, `{ type: "boolean" }` and
489
+ `{ type: "file", accept? }`. Each is required unless `required: false`, and its
490
+ `description` is what the planner and the confirmation call it. Prefer optional
491
+ parameters for changes, so a user can change one thing without restating the
492
+ rest.
493
+
494
+ A **file** parameter takes a file the user attached: the panel shows an attach
495
+ button when an action on the screen takes one, and a picture pasted into the
496
+ box is attached too. The file stays in the browser and is handed to `run`. Only
497
+ its name and type are sent to the engine.
498
+
499
+ ```tsx
500
+ useAdaptiveAction({
501
+ name: "updatePhoto",
502
+ title: "Change your profile picture",
503
+ parameters: { photo: { type: "file", description: "Picture", accept: ["image/*"] } },
504
+ run: ({ photo }) => fetch("/api/me/photo", { method: "PUT", body: photo as File }),
505
+ });
506
+ ```
507
+
508
+ A screen can offer actions with no region on it at all: give the panel an id
509
+ for the screen (`<PromptPanel regionId="profile" />`), which is what a missed
510
+ request on it is recorded under.
511
+
512
+ ## What the AI model sees
513
+
514
+ Each request a user makes goes to the engine, which asks an AI model (Anthropic's
515
+ Claude) for the change. With it go:
516
+
517
+ - What the user typed, or what they said once it is turned into text. Where the
518
+ panel covers several regions or offers actions, the last six requests and
519
+ replies in it go too.
520
+ - For each region the request is about: its id and `title`, and each section's
521
+ `id`, `kind`, `title`, `description` and `group`, with the settings it may
522
+ have, their allowed values and the sentence each preset gives the model.
523
+ - How the region is laid out now: the order, sizes, what is hidden and each
524
+ setting's value.
525
+ - The actions on the page: their names, titles and descriptions, and each
526
+ parameter's name, description and allowed values. For one that has run,
527
+ which action it was and whether it was done, failed or cancelled, plus what
528
+ it returned, or the `ActionError` message it failed with, only where
529
+ `shareResultWithPlanner` is set.
530
+ - The name and type of a file the user attached, never its contents.
531
+
532
+ What your sections draw, such as the rows of a table or the numbers on a card,
533
+ is never sent: ClickLess sees only what you declare. When dictation is on, the
534
+ section titles also go with each recording to OpenAI, as spelling hints.
535
+
536
+ So everything you declare is sent as you write it, to every user's requests.
537
+ Write every string you declare as a label that is the same for everyone, never
538
+ built from a user's or a record's data: ids and names as well as titles and
539
+ descriptions, and the words you give a preset (`means`, `shows`, `of`,
540
+ `entries`, `above.measure`) as well as its values:
541
+
542
+ ```tsx
543
+ <Adaptive id="orders" title="Recent orders" … /> // sent as is
544
+ <Adaptive id="orders" title={`${customer.name}'s orders`} … /> // sends the customer's name
545
+ <Adaptive id={`orders-${customer.id}`} title="Recent orders" … /> // sends the customer's id
546
+ ```
547
+
548
+ The same goes for an action's `title` and `description`, and a parameter's
549
+ `description` and `values`: "Open a customer's record", not one built from the
550
+ customer on screen.
551
+
552
+ ## Connecting to the engine
553
+
554
+ `getToken` returns a token your own backend signs for the signed-in user: an
555
+ HS256 JWT with the claims `sub` (user id), `tenant` and `exp`, signed with your
556
+ tenant's secret, the one the engine holds for that tenant. The secret stays on
557
+ your backend. It is called
558
+ before every request, so it can refresh an expired token. Signing one in
559
+ Node.js takes no library:
560
+
561
+ ```ts
562
+ import { createHmac } from "node:crypto";
563
+
564
+ function signClicklessToken(userId: string, tenantId: string, tenantSecret: string) {
565
+ const encode = (part: object) => Buffer.from(JSON.stringify(part)).toString("base64url");
566
+ const unsignedToken = `${encode({ alg: "HS256", typ: "JWT" })}.${encode({
567
+ sub: userId,
568
+ tenant: tenantId,
569
+ exp: Math.floor(Date.now() / 1000) + 300, // five minutes
570
+ })}`;
571
+ const signature = createHmac("sha256", tenantSecret).update(unsignedToken).digest("base64url");
572
+ return `${unsignedToken}.${signature}`;
573
+ }
574
+ ```
575
+
576
+ Since `getToken` is called before every request, keep the token in memory and
577
+ fetch a new one only when it is close to expiring, or your backend is asked for
578
+ one each time. Have `getToken` read the session as it is when called, not a
579
+ token held from an earlier render:
580
+
581
+ ```ts
582
+ let cachedToken: { token: string; expiresAt: number } | undefined;
583
+
584
+ async function getToken() {
585
+ if (cachedToken && cachedToken.expiresAt > Date.now() + 60_000) return cachedToken.token;
586
+ const response = await fetch("/api/clickless-token", { credentials: "include" });
587
+ cachedToken = await response.json(); // { token, expiresAt }, from your own backend
588
+ return cachedToken.token;
589
+ }
590
+ ```
591
+
592
+ A page reload empties the cache, so the first request after one fetches a new
593
+ token, signed for the session your own sign-in keeps. Keep the token out of
594
+ `localStorage` and cookies: it is short-lived, and the engine reads it only
595
+ from the request's `Authorization` header.
596
+
597
+ `engine` is read once for each signed-in user. Changing its `url` or `getToken`
598
+ later has no effect until `userId` changes and the provider mounts afresh, so
599
+ pass a `getToken` that stays correct for as long as that user is signed in.
600
+ `engine` also takes an optional `fetch`, used in place of the browser's own for
601
+ every request to the engine, such as one your tests replace or one that adds
602
+ headers your network needs.
603
+
604
+ The engine's `url` must start with `https://`, so that tokens and requests
605
+ cannot be read on the way, or be an address on your own site, such as
606
+ `/clickless`, on a page served over HTTPS. Plain `http://` is accepted only for
607
+ this machine (`localhost`, `127.0.0.1`, `[::1]` or a `.localhost` name), as in
608
+ local development. At any other address nothing is sent, the browser console says
609
+ why, and the page behaves as when the engine is unavailable.
610
+
611
+ The browser calls the engine directly, so the engine must allow your
612
+ application's origin, or the browser refuses every answer and the panel only
613
+ says personalising isn't available right now. The engine's `CLICKLESS_CORS_ORIGINS`
614
+ must list it, such as `["https://app.example.com"]`; when the engine refuses an
615
+ origin, its log says which one. The SDK sends its version with every request,
616
+ and the engine's log names it when it refuses one, so a build older or newer
617
+ than the engine reads is easy to spot.
618
+
619
+ **Nothing waits for the engine.** A region draws its default layout straight
620
+ away. When the user's saved layout arrives, each section glides from where it
621
+ was drawn to its saved place and one that was not drawn fades in, so the screen
622
+ settles rather than jumps; users who ask for less motion get the change at once.
623
+ While the engine is slow, the default layout is what shows. If it is down or
624
+ cannot be reached, see "If ClickLess is unavailable" below.
625
+
626
+ `initialLayout` on a region is shown until the engine's copy has loaded, for
627
+ example a layout fetched while rendering on the server. An `initialLayout` that
628
+ cannot be read is ignored.
629
+
630
+ Without `engine`, layouts live in memory: pass `initialLayout` and save what
631
+ `onLayoutChange` gives you yourself. Prompts need the engine. Save a layout as
632
+ it is given to you, `formatVersion` included: it says which format the layout
633
+ is in, and one in a format this version of the SDK does not read is ignored, so
634
+ the default shows.
635
+
636
+ ### If ClickLess is unavailable
637
+
638
+ Your application never depends on the engine to work. If the engine is down,
639
+ slow (past the SDK's time limit) or unreachable, its `url` is one it may not be
640
+ called at (see "Connecting to the engine"), or `getToken` throws:
641
+
642
+ - Every region draws straight away, in the layout you wrote (or the
643
+ `initialLayout` you passed), as if the user had never personalised it.
644
+ Nothing is blank, nothing waits, and nothing throws into your page. The SDK
645
+ tries reading the user's saved layout again on its own, a few seconds later
646
+ and then less often, and at once when the browser comes back online or any
647
+ other request gets through, and shows it when it arrives.
648
+ - Your own controls keep working. A change through `adaptiveControls.setProp`, or
649
+ through `applyOperations` without a prompt, shows at once, can be undone for
650
+ the rest of the visit, and its promise resolves at once rather than waiting
651
+ or rejecting. It is saved when the engine answers again: the SDK tries again
652
+ on its own, a few seconds later and then less often, and at once when the
653
+ browser comes back online or any other request gets through. A change is
654
+ saved only on top of what it was made on: if the user's saved layout could
655
+ not be read when they made it, or was changed elsewhere meanwhile, and it
656
+ holds something else for the setting the change touched, the change stays on
657
+ screen for the rest of the visit but is not saved, so the next visit shows
658
+ what they had saved. So does a change the engine refuses for good. Such a
659
+ change gives way to the next one that is saved, whether made with your own
660
+ controls, a prompt or a Restore; an Undo of a later change leaves it.
661
+ Changes reach the engine in the order they were made: a prompt's change, or
662
+ a Restore, waits for older ones to be saved first, and while they cannot be
663
+ is refused with a plain message, the one a prompt would get for the same
664
+ cause. A change to a section that has since left the screen, such as one on
665
+ another tab, is saved with the screen it was made on.
666
+ - Only prompting needs the engine. `PromptPanel` and `PromptBar` say plainly
667
+ that personalising isn't available right now and, when it applies, that the
668
+ screen may not show the user's saved changes, or that changes made meanwhile
669
+ will be saved when it's back. The panel's button is dimmed
670
+ (`--clickless-unavailable-opacity`) but still opens, to say so and to offer
671
+ Undo. A prompt box of your own gets the same sentence from
672
+ `usePromptBar(id).notice` (put away with `dismissNotice`); for anything else,
673
+ such as dimming your own button, use `usePromptBar(id).available`, and
674
+ `loadFailed` and `hasUnsavedChanges` from `useAdaptiveRegion(id)`.
675
+ - A fault in your `onLayoutChange` is reported to the browser and does not
676
+ stop the region.
677
+
678
+ ## Fitting a region to the page's own layout
679
+
680
+ By default a region is a box of its own: a 12-column grid with rows 80px tall,
681
+ in which every section is placed by its `w` and `h`. A section with a height
682
+ fills its cell, and when its content is a single element, such as a card, so
683
+ does that element. Content that sets its own height in pixels, such as a chart
684
+ given `height={350}`, keeps that height whatever the section's size, so give it
685
+ `100%` instead for resizing to show.
686
+
687
+ For fewer columns on narrow screens, give `columns` the columns from each
688
+ screen width up, in pixels:
689
+
690
+ ```tsx
691
+ <AdaptiveRegion id="dashboard" version="1" columns={{ 0: 1, 768: 12 }}>…</AdaptiveRegion>
692
+ ```
693
+
694
+ Users arrange the widest grid, and that is the layout saved and planned. A
695
+ narrower screen draws the same arrangement in its own columns, a row at a time,
696
+ so each section keeps its share of the width, and a change made on a phone does
697
+ not undo what was set for a wide screen. The planner is told how many columns
698
+ the user's screen draws, so its replies describe what they see: on a phone,
699
+ sections one above the other rather than side by side. It is told too when the
700
+ page reads right to left, from its `dir` attribute or a stylesheet's
701
+ `direction` (a `lang` attribute alone does not set it), so "left" and
702
+ "right" mean what the user sees, and each reply in the panel runs in the
703
+ direction of its own words.
704
+
705
+ `rowHeight="auto"` makes the rows as tall as their content, for something whose
706
+ height follows what is in it, such as a table:
707
+
708
+ ```tsx
709
+ <AdaptiveRegion id="tasks" version="1" rowHeight="auto">…</AdaptiveRegion>
710
+ ```
711
+
712
+ `grid={false}` makes the region draw nothing at all. Each section stays in
713
+ whichever of your containers it is written in, so one region can cover a
714
+ header, a row of tabs, your own card grids and a menu without any of them
715
+ changing. Give the sections that share a container the same `group`:
716
+
717
+ ```tsx
718
+ <AdaptiveRegion id="dashboard" version="1" grid={false}>
719
+ <header className="flex gap-2">
720
+ <Adaptive id="search" title="Search" group="header"><Search /></Adaptive>
721
+ <Adaptive id="profile" title="Profile menu" group="header"><ProfileMenu /></Adaptive>
722
+ </header>
723
+ <Adaptive id="download" title="Download" presets={{ look: propPresets.choice({ … }) }}>
724
+ {(props, adaptiveControls, presetValues) => <Button variant={presetValues.look}>Download</Button>}
725
+ </Adaptive>
726
+ <div className="grid grid-cols-12 gap-4">
727
+ <Adaptive id="revenue" title="Total Revenue" group="overview-cards"
728
+ capabilities={{ resize: { minW: 2, maxW: 12, minH: 1, maxH: 3 } }} defaults={{ w: 3, h: 2 }}>
729
+ <RevenueCard />
730
+ </Adaptive>
731
+ <Adaptive id="sales" title="Sales" group="overview-cards" defaults={{ w: 3, h: 2 }}>
732
+ <SalesCard />
733
+ </Adaptive>
734
+ </div>
735
+ </AdaptiveRegion>
736
+ ```
737
+
738
+ - **Groups.** Sections are put in order only among the others of their `group`:
739
+ "move to the start" means the start of that group, and a request to move a
740
+ card to another tab or into the header is refused, with a reply saying it can
741
+ only be moved among the things next to it. In a region with `grid={false}`, a
742
+ section without a group stays where it is written: it can be hidden or
743
+ changed, but not moved. Groups work the same way in a grid region, to keep
744
+ some of its sections apart from the rest.
745
+ - **Sizes.** `w` and `h` are optional with `grid={false}`. A section without
746
+ them, such as a button or a menu, is sized by the page and is never offered
747
+ for resizing. One with them is given `grid-column` and `grid-row` spans, which
748
+ a grid of your own, such as the card grid above, then uses; `columns` says how
749
+ many columns that grid has, and so how wide a section can be made. In a
750
+ region's own grid both are needed.
751
+ - **Order.** Moves are drawn with CSS `order`, counted within the group, so a
752
+ component's state survives a move. A section is given an `order` only while
753
+ its group is shown in an order other than the one written, so a group that
754
+ has never been moved leaves your own elements beside it alone. Once a group
755
+ has been moved, your own elements in the same container come before its
756
+ sections; give a movable group a container to itself, or put your own
757
+ elements first in it.
758
+ - **Keyboard and screen readers.** So that the Tab key and screen readers
759
+ follow what is shown, a region sets `reading-flow: grid-order`, which
760
+ browsers that support it use for both, and once a user has moved a section it
761
+ lists its sections in the shown order with `aria-owns`, which screen readers
762
+ use. In a browser without `reading-flow`, screen readers still read them in
763
+ the shown order, but the Tab key follows the order written in the source. A
764
+ moved group's container does the same, when it holds nothing but that group.
765
+ - **Finding a section.** Each section's element has an `id` that starts with
766
+ `clickless-` and is unique on the page; to find one from your own code or
767
+ styles, use its `data-clickless-item` attribute instead. With `grid={false}`,
768
+ `className` and `style` on the region are not used, since there is no element
769
+ to put them on.
770
+
771
+ ## A screen made of several regions
772
+
773
+ A sidebar, a header or a row of tabs that every page shares has to be a region
774
+ of its own: inside each page's region its layout would be saved once per page.
775
+ Leave the regions as they are and give the prompt panel all of their ids:
776
+
777
+ ```tsx
778
+ <AdaptiveRegion id="sidebar" version="1" title="Sidebar">…</AdaptiveRegion>
779
+ <AdaptiveRegion id="dashboard" version="1">…</AdaptiveRegion>
780
+
781
+ <PromptPanel regionId={["dashboard", "sidebar"]} />
782
+ ```
783
+
784
+ `PromptBar`, `usePromptBar` and `useChangeHistory` take a list in the same way,
785
+ and a single id means what it always did. With a list:
786
+
787
+ - One prompt, such as "hide Chats in the sidebar and move Sales first", is
788
+ planned for all the regions in one request, so the model sees everything on
789
+ the screen.
790
+ - The preview shows in every region it changes, and Keep and Discard act on all
791
+ of it. Each region's part is saved to that region, so the sidebar's layout is
792
+ still the same one on every page.
793
+ - Keep saves every part or none. If one region was changed somewhere else in
794
+ the meantime, nothing is saved, that region is refreshed, and the user is
795
+ asked to try again.
796
+ - Undo takes back the last change in one go, in every region it reached, even
797
+ one on a page that is not open, and the reply names it: the request in the
798
+ user's words, or what the change did. A region that has had a later change
799
+ since keeps its part, and the reply says that part stays in place elsewhere. The list of past changes is one conversation for all the regions, and
800
+ "Restore to this point" returns all of them to how they were then.
801
+ - When part of a request cannot be done, the rest is still previewed and the
802
+ reply says which part was left out.
803
+ - A region in the list that is not on the screen is left out until it is, so
804
+ the same list can be used on pages that lack one of them.
805
+
806
+ Several regions in one panel need the engine, and an engine recent enough to
807
+ have its `/v1/changes` endpoints. Put the page first in the list, which may
808
+ name up to 8 regions. Give each
809
+ region a `title`, what users call that part of the screen: it is how the model
810
+ knows which one "in the sidebar" means, and what its replies call it.
811
+
812
+ ## The prompt panel
813
+
814
+ `PromptPanel` is a floating button that opens a panel for asking for changes.
815
+ Users see a preview of each change before anything is saved, and choose Keep or
816
+ Discard. The panel lists their past changes, a few at a time, each with the
817
+ request, the reply, a way to reuse the request, and "Restore to this point".
818
+ It can be minimised to show the whole page; while a preview is waiting, the
819
+ button that reopens it reads "Keep or discard?". When a preview appears, the
820
+ page scrolls to what changed. `PromptBar` is the same thing as an inline bar.
821
+
822
+ The button and the panel's title both read "Ask ClickLess" unless you name them
823
+ yourself, in words that fit your application:
824
+
825
+ ```tsx
826
+ <PromptPanel regionId="dashboard" label="Customise" title="Customise this screen" />
827
+ ```
828
+
829
+ What a preview changes is outlined with a dashed line in the panel's accent
830
+ colour, so users see what Keep will save before they choose; a section the
831
+ preview hides is simply gone, and the reply says so. The same outline marks each
832
+ section drawn differently from the user's version while "See original" is
833
+ showing. Restyle it with `--clickless-preview-outline`, a whole outline such as
834
+ `2px dashed #0f766e` (`none` turns it off), and
835
+ `--clickless-preview-outline-offset` (`4px` by default). A section's own `style`
836
+ changes it for that section. Each outlined section also carries
837
+ `data-clickless-preview="changed"` for anything else a stylesheet should do to
838
+ it; the outline itself is set by the variables, which take precedence over a
839
+ stylesheet's `outline`. A section that draws nothing of its own, such as one
840
+ that only hands settings to a component drawn elsewhere, has nothing to outline:
841
+ give that component a rule of its own on the attribute, such as
842
+ `body:has([data-clickless-item="menu"][data-clickless-preview="changed"]) nav`.
843
+
844
+ A section that holds several things, such as a row of counts above a table, is
845
+ better outlined where the change is than all round. Its render function's
846
+ controls say what changed: `isChanged(name)` and `changeOutline(name)` take a
847
+ prop's name, or the name of a preset in `presets` for all of its props, and,
848
+ for a list such as the columns, an entry, which counts as changed when it is new
849
+ or had to move. `changeOutline` gives the same outline as a style, or nothing
850
+ where the part is drawn the same; put it on that part. Set `marksChanges` on the
851
+ section, and its own outline is kept for hiding, moving or resizing it:
852
+
853
+ ```tsx
854
+ <Adaptive id="txns" title="Transactions" marksChanges presets={{ columns, rows: propPresets.density() }}>
855
+ {(props, adaptiveControls, presetValues) => (
856
+ <table style={adaptiveControls.changeOutline("rows")}>
857
+ <tr>
858
+ {presetValues.columns.map((column) => (
859
+ <th key={column} style={adaptiveControls.changeOutline("columns", column)}>{column}</th>
860
+ ))}
861
+ </tr>
862
+ </table>
863
+ )}
864
+ </Adaptive>
865
+ ```
866
+
867
+ While "See original" shows, a column the user removed is there again, and
868
+ outlined.
869
+
870
+ A request leaves the box once it is sent; the conversation still shows the one
871
+ sent. A request that could not be carried out, as when the engine cannot be
872
+ reached, is put back in the box to send again, unless something else was typed
873
+ meanwhile.
874
+
875
+ A request typed while a preview is showing builds on it: "make Revenue wider",
876
+ then "wider again", widens it from the preview, and "not that one, the other
877
+ card" takes back that part of it. Its change is added to the preview, Keep
878
+ saves the whole as one change, listed in the history with its requests
879
+ together, and Discard goes back to the saved layout. A reply with nothing to
880
+ change, such as an answer to a question, leaves the preview as it is, and with
881
+ it anything the request that made the preview asked to do once it is decided
882
+ on. If the screen was changed somewhere else meanwhile, the preview is taken
883
+ off, the user is told, and the request is put back in the box to send again.
884
+
885
+ Leaving a page does not lose what the user typed. A request typed and not sent
886
+ is kept for the regions the panel speaks for, and is back in the box when the
887
+ user returns to them, for as long as the `AdaptiveProvider` stays mounted and
888
+ the same user is signed in. A
889
+ panel for `"dashboard"` and one for `["dashboard", "sidebar"]` keep their own.
890
+ When Keep is refused because the screen was changed somewhere else, such as in
891
+ another browser tab, the request is put back in the box, ready to send again.
892
+ While a preview waits for Keep or Discard, or is being saved, closing or
893
+ reloading the browser tab makes the browser ask first; at any other time it
894
+ does not. A change being saved when the tab closes anyway is still sent, and
895
+ saved, as long as it is no larger than browsers carry on sending (about 60 KB).
896
+ A reload straight after may show the page before that save has landed; the
897
+ change is there on the next read. Going to another page inside your
898
+ application is not held up: the preview is taken off and recorded as abandoned.
899
+
900
+ Once the screen differs from how it first looked, two buttons stay pinned under
901
+ the panel's title. "See original" shows the screen as it first looked, in place
902
+ and without saving anything, so users can spot a section they removed; each
903
+ section drawn differently from their version is marked as in a preview. From
904
+ there they choose "Back to my version" or "Keep the original". "Revert to original" puts
905
+ the original back at once, and can be undone. A request, Undo or any other
906
+ change ends the look at the original, as does leaving the page the panel is on.
907
+ Closing or minimising the panel does not: its button then reads "Seeing
908
+ original", and opening it offers the way back.
909
+
910
+ The button sits in the bottom-right corner unless you say otherwise:
911
+ `position` takes `"bottom-right"`, `"bottom-left"`, `"top-right"` or
912
+ `"top-left"`, and the panel opens from the same corner. To move the button in
913
+ from the edges, for example clear of a tab bar or a chat bubble, set
914
+ `--clickless-launcher-offset-x` and `--clickless-launcher-offset-y` (16px each
915
+ by default), each with its unit (`0px`, not `0`). The button and panel sit in
916
+ front of the page at a z-index of 1000; to put them in front of, or behind,
917
+ something of your own, such as a modal, set `--clickless-panel-z-index`.
918
+
919
+ Both components are themed with CSS custom properties, so they need no
920
+ stylesheet and no class names from you:
921
+
922
+ ```css
923
+ :root {
924
+ --clickless-accent: #0f766e; /* buttons and links */
925
+ --clickless-accent-text: #ffffff; /* the writing on those buttons */
926
+ --clickless-user-bubble: #0f766e; /* the user's requests */
927
+ --clickless-user-text: #ffffff;
928
+ --clickless-reply-bubble: #f0fdfa; /* the replies */
929
+ --clickless-reply-border: #0f766e;
930
+ --clickless-bubble-radius: 6px;
931
+ }
932
+ ```
933
+
934
+ Also available: `--clickless-surface`, `--clickless-text`, `--clickless-muted`,
935
+ `--clickless-border`, `--clickless-danger` and `--clickless-reply-text`. A page
936
+ that sets none of them gets a panel that follows it: it turns dark when the
937
+ page's background is dark, however the page switches theme, and back again. A
938
+ page that sets any of them on its root or body has chosen its own colours, so
939
+ the rest stay light rather than mix with them; such a page looks best mapping
940
+ the variables to its own colours, as shadcn's `--card`, `--foreground`,
941
+ `--primary` and the like, which then follow its light and dark themes with it.
942
+ A page that paints no background of its own on `<html>` or `<body>` is followed
943
+ by its theme attribute or class there, its `color-scheme`, or the background
944
+ in the middle of the screen. To choose instead, as for an app that keeps its
945
+ theme in its own state, pass `colorScheme="light"`, `"dark"` or `"auto"` (the
946
+ default) to `PromptPanel` or `PromptBar`, or once to `AdaptiveProvider`.
947
+ Bubbles carry `data-clickless-bubble="user"` or `"reply"` for anything the
948
+ variables do not cover. A request shows as soon as it is sent: in
949
+ `PromptPanel` as the user's bubble with a `"working"` one under it until the
950
+ answer comes, the panel scrolling so the newest of these stays in view, and in
951
+ `PromptBar` as one `"working"` line holding the sign and the request. While the screen shows how it first looked, the bar saying so
952
+ carries `data-clickless-original="showing"`.
953
+
954
+ ## Speaking a request
955
+
956
+ `<PromptPanel dictation />` shows a microphone button, when the engine is set
957
+ up for it and the browser can record. The user speaks, presses Done, and what
958
+ they said is added to the box for them to read, change and send; nothing is sent for them.
959
+ If it seems to hold a password, card number or other private detail, nothing is
960
+ added to the box and the user is told why.
961
+ While they speak, bars move with their voice, and a recording in which nothing
962
+ was said is never sent. A recording ends by itself after a minute.
963
+ Nothing is recorded until the user presses the button.
964
+
965
+ It is off unless you turn it on, because what users say leaves your
966
+ application: the recording goes to the engine, which passes it to OpenAI's
967
+ speech-to-text service to be turned into text. Turn it on where your
968
+ agreements with your users and customers cover that.
969
+
970
+ Three things on your side decide whether it works:
971
+
972
+ - The page is served over HTTPS (or from `localhost`). Browsers give no
973
+ microphone to a page that is not.
974
+ - If you send a `Permissions-Policy` header, it allows the microphone for your
975
+ own origin: `microphone=(self)`.
976
+ - If the application runs inside an `<iframe>`, the frame has `allow="microphone"`.
977
+
978
+ The engine keeps neither the recording nor its text; what OpenAI keeps is set by
979
+ its terms with whoever runs the engine. The titles of the sections on the
980
+ screen are sent with the recording as hints, so that their names are spelt as
981
+ you spell them.
982
+
983
+ The button carries `data-clickless-dictation="start"` for a stylesheet to
984
+ target. `dictationLabels` replaces any of its wording (`speak`, `starting`,
985
+ `listening`, `transcribing`, `done`, `cancel`), and `renderDictationButton`
986
+ draws a button of your own in its place, still only with `dictation`:
987
+
988
+ ```tsx
989
+ <PromptPanel
990
+ regionId="dashboard"
991
+ dictation
992
+ dictationLabels={{ speak: "Dictate", done: "Finished" }}
993
+ renderDictationButton={({ start, disabled, label }) => (
994
+ <button type="button" aria-label={label} disabled={disabled} onClick={start}>
995
+ <MyMicIcon />
996
+ </button>
997
+ )}
998
+ />
999
+ ```
1000
+
1001
+ The bars read `--clickless-recording` for their colour. What replaces the
1002
+ button while the user speaks carries `data-clickless-dictation` too, as
1003
+ `"starting"`, `"recording"` or `"transcribing"`. While it is `"recording"`,
1004
+ `--clickless-dictation-level` is set on it several times a second: a number
1005
+ from 0 (silence) to 1 (loud).
1006
+
1007
+ ## Building your own controls
1008
+
1009
+ For controls you build yourself, such as a column picker, the render function
1010
+ also receives `adaptiveControls.setProp(propName, propValue)`. It goes through the same checks,
1011
+ storage and undo as a request made in words, but takes effect at once, without
1012
+ waiting for the engine, and its promise resolves at once and never rejects
1013
+ because the engine is unavailable (see "If ClickLess is unavailable").
1014
+ Resolving means the change is shown, not that it is saved; `hasUnsavedChanges`
1015
+ from `useAdaptiveRegion(id)` says whether any change is still waiting to be saved.
1016
+ It resolves to `{ applied: true, layout }`, or to `{ applied: false, errors }`
1017
+ when the manifest does not allow the change, such as a value the prop does not
1018
+ offer, and then nothing changes on screen.
1019
+
1020
+ ```tsx
1021
+ {(props, adaptiveControls, presetValues) => (
1022
+ <ColumnPicker
1023
+ visible={presetValues.columns}
1024
+ onChange={(shownColumns) => adaptiveControls.setProp("columns", shownColumns)}
1025
+ />
1026
+ )}
1027
+ ```
1028
+
1029
+ To build your own panel or bar, the hooks behind the built-in ones are
1030
+ exported:
1031
+
1032
+ - `usePromptBar(id)`: the request, the preview, Keep, Discard, Undo and restore.
1033
+ `requestInProgress` holds a sent request until its answer comes, and is null
1034
+ otherwise, so a custom panel can show it at once.
1035
+ `differsFromOriginal` says when there is an original to offer,
1036
+ `showOriginal(true)` shows the screen as it first looked, `showingOriginal`
1037
+ says it is showing, and `showOriginal(false)` goes back to the user's version.
1038
+ Call `showOriginal(false)` when your panel leaves the page, or the screen
1039
+ stays on the original. `prompt` and `setPrompt` keep an unsent request per
1040
+ set of regions, and the browser asks before the tab closes while `previewing`,
1041
+ as in the built-in panel.
1042
+ - `useChangeHistory(id)`: a user's past changes, a page at a time.
1043
+ - `useDictation({ onTranscript })`: the microphone, whose recordings go to
1044
+ OpenAI as described in [Speaking a request](#speaking-a-request). It returns `available`,
1045
+ `status`, `elapsedSeconds`, `error`, `clearError`, `start`, `stop`, `cancel`
1046
+ and a `soundLevelRef` for the element that should carry the sound level.
1047
+ When what was said seems to hold a private detail, `onTranscript` is not
1048
+ called and `error` says why.
1049
+ - `useAdaptiveAction(definition)`: offers an action, as described above.
1050
+ - `useAdaptiveRegion(id)`: one region's manifest and layout, with
1051
+ `applyOperations`, `previewOperations`, `commitPreview`, `cancelPreview`,
1052
+ `discardPreview`, `undoLastChange` and `planFromPrompt`. `undoLastChange`
1053
+ resolves to `{ keptElsewhere, undoneChange }`: `keptElsewhere` is true when
1054
+ part of the change stays in a region that changed again since, and
1055
+ `undoneChange` is the change taken back (its `prompt`, `kind` and
1056
+ `operations`), or null when the engine does not say. `describeHistoryEntry`
1057
+ turns it into words.
1058
+
1059
+ Call `discardPreview` when the user turns a preview down, and `cancelPreview`
1060
+ when your application takes one off the screen for a reason of its own: only
1061
+ the first is recorded as a missed request. `planFromPrompt` plans from the
1062
+ preview showing, if there is one, and adds its answer to it: the `operations`
1063
+ it resolves to are what the request changes from the screen shown. One that
1064
+ takes the whole preview back records it as discarded. With no preview showing,
1065
+ any preview that appears while it waits is taken off when its answer comes,
1066
+ and recorded as discarded if it came from a request. A preview from a request that
1067
+ the screen takes off before the user chose, because a region it changed
1068
+ unmounted or its components changed, as on going to another page, is recorded
1069
+ as abandoned instead. Closing a prompt bar leaves its preview waiting.
1070
+
1071
+ ## When your screens change
1072
+
1073
+ Each user's layout is saved against what a region contained at the time. When a
1074
+ release of your application adds, removes or changes sections, a saved layout
1075
+ is fitted to the region as it is now rather than thrown away:
1076
+
1077
+ - A new section is placed after its nearest neighbour.
1078
+ - A setting is reset to its default only where your new declaration no longer
1079
+ allows the saved value.
1080
+ - A section that is not on screen, such as one in a closed tab, keeps what the
1081
+ user set on it, and gets it back when it is shown again. Nothing has to be
1082
+ declared for this, and tabs do not have to keep their content mounted.
1083
+
1084
+ What a user set on a section is saved under its `id`, so keep ids the same
1085
+ through redesigns and refactors. Restyling a section or rewriting the component
1086
+ inside it keeps everything the user set. A section whose `id` changes is a new
1087
+ section to ClickLess: it starts from its defaults, and what the user set under
1088
+ the old id is not carried over.
1089
+
1090
+ Bump a region's `version` when a release adds, removes or renames sections, not
1091
+ when they come and go on screen. The panel tells a returning user, in plain
1092
+ words, what was added or reset since they arranged the screen. Without the bump
1093
+ new sections are not mentioned: a section the saved layout has not met is taken
1094
+ to have been off screen when it was saved, and it quietly shows its defaults.
1095
+ Settings reset because your declaration no longer allows them are mentioned
1096
+ either way.