@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.
- package/LICENSE +21 -0
- package/README.md +1095 -2
- package/dist/actions/actionCalls.d.ts +50 -0
- package/dist/actions/actionRegistry.d.ts +78 -0
- package/dist/components/Adaptive.d.ts +116 -0
- package/dist/components/AdaptiveAction.d.ts +4 -0
- package/dist/components/AdaptiveProvider.d.ts +38 -0
- package/dist/components/AdaptiveRegion.d.ts +60 -0
- package/dist/components/ClicklessUiErrorBoundary.d.ts +24 -0
- package/dist/components/OriginalToolbar.d.ts +21 -0
- package/dist/components/PromptBar.d.ts +21 -0
- package/dist/components/PromptPanel.d.ts +103 -0
- package/dist/components/WorkingDots.d.ts +4 -0
- package/dist/components/focusAfterPromptAction.d.ts +15 -0
- package/dist/components/hostColorScheme.d.ts +35 -0
- package/dist/components/promptPanelStyles.d.ts +14 -0
- package/dist/core/applyOperations.d.ts +21 -0
- package/dist/core/colorContrast.d.ts +22 -0
- package/dist/core/defaultLayout.d.ts +12 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/ownProperties.d.ts +11 -0
- package/dist/core/propValues.d.ts +8 -0
- package/dist/core/reconcileLayout.d.ts +59 -0
- package/dist/core/schemas.gen.d.ts +427 -0
- package/dist/core/validateActionCalls.d.ts +28 -0
- package/dist/core/validateManifest.d.ts +11 -0
- package/dist/core/validateOperations.d.ts +15 -0
- package/dist/engine/engineClient.d.ts +231 -0
- package/dist/engine/engineErrorMessages.d.ts +13 -0
- package/dist/engine/engineResponses.d.ts +201 -0
- package/dist/engine/plainReplies.d.ts +2 -0
- package/dist/hooks/useActionRunHistory.d.ts +12 -0
- package/dist/hooks/useAdaptiveAction.d.ts +11 -0
- package/dist/hooks/useAdaptiveRegion.d.ts +34 -0
- package/dist/hooks/useChangeHistory.d.ts +32 -0
- package/dist/hooks/useDictation.d.ts +50 -0
- package/dist/hooks/useEngineReachable.d.ts +5 -0
- package/dist/hooks/usePromptBar.d.ts +163 -0
- package/dist/hooks/useRegionGroup.d.ts +15 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +6600 -0
- package/dist/presets/mergePresets.d.ts +20 -0
- package/dist/presets/propPresets.d.ts +305 -0
- package/dist/region/changedComponentIds.d.ts +13 -0
- package/dist/region/firstPaintOrder.d.ts +19 -0
- package/dist/region/previewRequests.d.ts +21 -0
- package/dist/region/regionContext.d.ts +96 -0
- package/dist/region/regionGroup.d.ts +189 -0
- package/dist/region/regionSnapshot.d.ts +52 -0
- package/dist/region/regionStore.d.ts +361 -0
- package/dist/region/responsiveColumns.d.ts +35 -0
- package/dist/region/retrier.d.ts +22 -0
- package/dist/region/savedLayoutParsing.d.ts +7 -0
- package/dist/region/settleIntoSavedLayout.d.ts +14 -0
- package/dist/region/textDirection.d.ts +12 -0
- package/dist/region/unsavedChangeSaver.d.ts +45 -0
- package/dist/region/unsavedChanges.d.ts +78 -0
- package/dist/region/userFacingTitle.d.ts +7 -0
- package/package.json +59 -3
package/README.md
CHANGED
|
@@ -1,3 +1,1096 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @clickless/react
|
|
2
2
|
|
|
3
|
-
|
|
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.
|