@reventlessdev/reventless-ui-slots 3.0.0-alpha.1 → 3.0.0-alpha.3

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/README.md CHANGED
@@ -93,6 +93,28 @@ let register = (arg: ReventlessSlots.registerArg) => {
93
93
  }
94
94
  ```
95
95
 
96
+ ### Helpers
97
+
98
+ The ReScript surface carries a few things every slot module otherwise rewrites,
99
+ and rewrites subtly wrong:
100
+
101
+ | | |
102
+ |---|---|
103
+ | `Row.string` / `text` / `float` / `bool` / `array` | read a field out of the row's JSON. `text` treats `""` as absent, which is what a view writes for "not yet" |
104
+ | `Row.money` + `Format.money` | `amount` is **minor units** at a scale the currency decides — 1000 is €10.00 but ¥1000 and 1.000 TND. `Format.money` asks `Intl` for the scale instead of assuming a hundred |
105
+ | `Row.firstAttachment` | the `altText` and `caption` of a captioned set's first member. Text only: members hold storage **refs**, not URLs, and only `payload.image` is rebased for the deployment's origins |
106
+ | `titleOf` | what the mode would have titled the row, falling back to its `name` |
107
+ | `ensureStyles(~id, css)` | put a stylesheet in the page once. The shell re-imports a module on hot reload, so an unguarded injection appends the same rules on every save |
108
+
109
+ They are ReScript-only, and deliberately: a ReScript module is bundled anyway, so
110
+ importing a helper costs it nothing, while a JavaScript one is served as written
111
+ and anything imported from this package would take away the no-bundler property
112
+ that is that path's whole point. `index.js` stays one identity function.
113
+
114
+ Call `ensureStyles` from inside `register`, not at module scope, so importing the
115
+ module has no side effect — which is also what keeps it loadable by a test on
116
+ Node, where there is no document.
117
+
96
118
  ### Why `h` rather than JSX
97
119
 
98
120
  Because React is the one thing a slot module must **not** bring its own copy of.
@@ -123,6 +145,11 @@ the question.
123
145
  | `tracker.summary` | row | the line beside one row's name; its summary fields |
124
146
  | `detail.media` | row | the media column of a `detailLayout: "media"` page; the record's picture |
125
147
  | `detail.aside` | row | above the field set on a `detailLayout: "media"` page; nothing by default |
148
+ | `list.selection` | view | the bar over a list whose rows the caller can pick out: what is picked, a way to empty it, and the one command that takes the lot |
149
+
150
+ `list.selection` is the one id that is not `<mode>.<region>`: it belongs to the
151
+ list itself rather than to a mode, which is what lets a picking made in the table
152
+ survive a switch to cards.
126
153
 
127
154
  An id nothing offers is reported to the console at boot — a renderer nothing will
128
155
  ever call is a mistake the running app would otherwise never mention.
@@ -134,7 +161,7 @@ ever call is a mistake the running app would otherwise never mention.
134
161
  { row, label?, image?, open?, actions? }
135
162
 
136
163
  // view slot
137
- { rows, total?, selection?, run? }
164
+ { rows, total?, selection?, picked?, run? }
138
165
  ```
139
166
 
140
167
  `row` is the read model's own JSON. `image` is already resolved for this
@@ -142,10 +169,12 @@ deployment's asset origins. `actions` are already resolved against the
142
169
  deployment's declarations, the caller's permissions and the row's lifecycle
143
170
  state. `open` drills into the row.
144
171
 
145
- `selection` and `run` are declared and filled by nothing yet — cross-row
146
- selection is its own piece of work. They are in the contract from the start
147
- because discovering that a region needs a second arity after publishing would be
148
- a breaking change.
172
+ `selection` is the picked row **ids**; `picked` is the rows behind them, each as
173
+ the same row-slot payload above. Both, and not just the ids, because a list's
174
+ window is replaced page by page: a row picked on page 1 is gone from `rows` by
175
+ the time the caller is on page 3, and a bar whose job is to name what is in it
176
+ cannot look one up. `run` takes the command's name and opens its form with the
177
+ picked ids already in place.
149
178
 
150
179
  ## What a slot may not do
151
180
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-ui-slots",
3
- "version": "3.0.0-alpha.1",
3
+ "version": "3.0.0-alpha.3",
4
4
  "description": "Types and one runtime helper for writing custom renderers into the regions of Reventless AutoUI's standard view modes.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -10,7 +10,9 @@
10
10
  ".": {
11
11
  "types": "./src/index.d.ts",
12
12
  "default": "./src/index.js"
13
- }
13
+ },
14
+ "./src/*": "./src/*",
15
+ "./package.json": "./package.json"
14
16
  },
15
17
  "files": [
16
18
  "src/index.js",
@@ -52,5 +54,5 @@
52
54
  "react-dom": "^18.3.1",
53
55
  "rescript": "^12.3.0"
54
56
  },
55
- "gitHead": "5877fd93c92d7f3d023e12a9f32e24832b639400"
57
+ "gitHead": "d2a7156f66c7f5f42837c6d5c13faf6d33a6df7e"
56
58
  }
@@ -20,6 +20,7 @@ type viewPayload = {
20
20
  rows: array<JSON.t>,
21
21
  total?: int,
22
22
  selection?: array<string>,
23
+ picked?: array<rowPayload>,
23
24
  run?: string => unit,
24
25
  }
25
26
 
@@ -32,14 +33,172 @@ type slots = {
32
33
 
33
34
  type registerArg = {h: hyperscript, slots: slots}
34
35
 
35
- // `React.createElement(tag, props, ...children)` against the function the shell
36
- // passed in. Written as `%raw` because the callee is a value rather than a
37
- // module member, which `@variadic` cannot express.
38
- let h: (hyperscript, 'tag, 'props, array<React.element>) => React.element = %raw(`
39
- function (h, tag, props, children) {
40
- return h(tag, props, ...children)
36
+ /* `React.createElement(tag, props, ...children)` against the function the shell
37
+ passed in.
38
+
39
+ `@variadic` cannot express this: it binds a module member, and the callee here
40
+ is a value that arrived as an argument. So the call goes through
41
+ `Function.prototype.apply`, which is exactly "call this value with these
42
+ arguments" and is the one thing in JavaScript that takes an argument list as
43
+ data.
44
+
45
+ `apply` rather than handing React the array as a single child, which would
46
+ also compile: React reads an array child as a keyed list and warns about
47
+ missing keys, where positional children carry no such expectation. The
48
+ distinction is invisible until a slot author's console fills up. */
49
+
50
+ // One element of that argument list. Heterogeneous by nature — a tag, then
51
+ // props, then elements — so the three are widened to a common type on the way
52
+ // in. Nothing reads an `arg` back out; it exists only to be passed on.
53
+ //
54
+ // `args` widens the children array in one step rather than per element: mapping
55
+ // `arg` over it would compile to an identity `.map`, allocating a second array
56
+ // on every element built.
57
+ type arg
58
+ external arg: 'a => arg = "%identity"
59
+ external args: array<React.element> => array<arg> = "%identity"
60
+
61
+ @send
62
+ external applyTo: (hyperscript, Nullable.t<unit>, array<arg>) => React.element = "apply"
63
+
64
+ let h = (hyperscript: hyperscript, tag: 'tag, props: 'props, children: array<React.element>) =>
65
+ hyperscript->applyTo(Nullable.null, [arg(tag), arg(props)]->Array.concat(args(children)))
66
+
67
+ // Reading the row's JSON. One right answer per field type, so it lives here
68
+ // rather than being rewritten — subtly differently — per deployment.
69
+ module Row = {
70
+ let field = (row: JSON.t, name: string): option<JSON.t> =>
71
+ switch row {
72
+ | Object(fields) => fields->Dict.get(name)
73
+ | _ => None
74
+ }
75
+
76
+ let string = (row: JSON.t, name: string): option<string> =>
77
+ switch field(row, name) {
78
+ | Some(String(value)) => Some(value)
79
+ | _ => None
80
+ }
81
+
82
+ let text = (row: JSON.t, name: string): option<string> =>
83
+ switch string(row, name) {
84
+ | Some("") | None => None
85
+ | Some(value) => Some(value)
86
+ }
87
+
88
+ let float = (row: JSON.t, name: string): option<float> =>
89
+ switch field(row, name) {
90
+ | Some(Number(value)) => Some(value)
91
+ | _ => None
92
+ }
93
+
94
+ let bool = (row: JSON.t, name: string): option<bool> =>
95
+ switch field(row, name) {
96
+ | Some(Boolean(value)) => Some(value)
97
+ | _ => None
98
+ }
99
+
100
+ let array = (row: JSON.t, name: string): option<array<JSON.t>> =>
101
+ switch field(row, name) {
102
+ | Some(Array(values)) => Some(values)
103
+ | _ => None
104
+ }
105
+
106
+ let money = (row: JSON.t, name: string): option<(float, string)> =>
107
+ switch field(row, name) {
108
+ | Some(Object(value)) =>
109
+ switch (value->Dict.get("amount"), value->Dict.get("currency")) {
110
+ | (Some(Number(amount)), Some(String(currency))) => Some((amount, currency))
111
+ | _ => None
112
+ }
113
+ | _ => None
114
+ }
115
+
116
+ let firstAttachment = (row: JSON.t, name: string): option<(option<string>, option<string>)> => {
117
+ let nonEmpty = (member: Dict.t<JSON.t>, key: string) =>
118
+ switch member->Dict.get(key) {
119
+ | Some(String("")) | None => None
120
+ | Some(String(value)) => Some(value)
121
+ | Some(_) => None
122
+ }
123
+ switch field(row, name) {
124
+ | Some(Array(members)) =>
125
+ switch members->Array.get(0) {
126
+ | Some(JSON.Object(first)) => Some((nonEmpty(first, "altText"), nonEmpty(first, "caption")))
127
+ | _ => None
128
+ }
129
+ | _ => None
130
+ }
131
+ }
132
+ }
133
+
134
+ module Format = {
135
+ let money = ((amount, currency): (float, string)): string => {
136
+ let format = Intl.NumberFormat.make(~options={style: #currency, currency})
137
+ let digits = switch Intl.NumberFormat.resolvedOptions(format).maximumFractionDigits {
138
+ | Some(digits) => (digits :> int)
139
+ | None => 2
140
+ }
141
+ Intl.NumberFormat.format(format, amount /. Math.pow(10.0, ~exp=Int.toFloat(digits)))
142
+ }
143
+
144
+ let day = (iso: string): string => {
145
+ let at = Date.fromString(iso)
146
+ Float.isNaN(Date.getTime(at)) ? "" : Date.toLocaleDateString(at)
147
+ }
148
+
149
+ let pad = (n: int): string => n < 10 ? "0" ++ Int.toString(n) : Int.toString(n)
150
+
151
+ let isoDay = (iso: string): string => {
152
+ let at = Date.fromString(iso)
153
+ Float.isNaN(Date.getTime(at))
154
+ ? ""
155
+ : Int.toString(Date.getFullYear(at)) ++
156
+ "-" ++
157
+ pad(Date.getMonth(at) + 1) ++
158
+ "-" ++
159
+ pad(Date.getDate(at))
160
+ }
161
+ }
162
+
163
+ let titleOf = (payload: rowPayload): string =>
164
+ switch payload.label {
165
+ | Some(label) => label
166
+ | None => Row.string(payload.row, "name")->Option.getOr("")
167
+ }
168
+
169
+ // The DOM calls `ensureStyles` makes. Bound here rather than pulled from a DOM
170
+ // binding package, so this package keeps its single dependency.
171
+ //
172
+ // `document` is reached through `globalThis` rather than as a bare global, and
173
+ // that is the load-bearing detail: referencing an undeclared `document` throws a
174
+ // ReferenceError on Node, while `globalThis.document` is simply `undefined`
175
+ // there. It is what lets a slot module be imported and driven by a test.
176
+ module Dom = {
177
+ type document
178
+ type element
179
+
180
+ @val @scope("globalThis") external document: Nullable.t<document> = "document"
181
+ @send external getElementById: (document, string) => Nullable.t<element> = "getElementById"
182
+ @send external createElement: (document, string) => element = "createElement"
183
+ @get external head: document => element = "head"
184
+ @set external setId: (element, string) => unit = "id"
185
+ @set external setTextContent: (element, string) => unit = "textContent"
186
+ @send external appendChild: (element, element) => unit = "appendChild"
187
+ }
188
+
189
+ let ensureStyles = (~id: string, css: string): unit =>
190
+ switch Nullable.toOption(Dom.document) {
191
+ | None => ()
192
+ | Some(doc) =>
193
+ switch Nullable.toOption(doc->Dom.getElementById(id)) {
194
+ | Some(_) => ()
195
+ | None =>
196
+ let style = doc->Dom.createElement("style")
197
+ style->Dom.setId(id)
198
+ style->Dom.setTextContent(css)
199
+ doc->Dom.head->Dom.appendChild(style)
200
+ }
41
201
  }
42
- `)
43
202
 
44
203
  module RowSlot = {
45
204
  let cardsFace = "cards.face"
@@ -63,5 +222,6 @@ module RowSlot = {
63
222
  module ViewSlot = {
64
223
  let cardsEmpty = "cards.empty"
65
224
  let galleryEmpty = "gallery.empty"
66
- let all = [cardsEmpty, galleryEmpty]
225
+ let listSelection = "list.selection"
226
+ let all = [cardsEmpty, galleryEmpty, listSelection]
67
227
  }
@@ -1,9 +1,166 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
3
4
 
4
- let h = (function (h, tag, props, children) {
5
- return h(tag, props, ...children)
5
+ function h(hyperscript, tag, props, children) {
6
+ return hyperscript.apply(null, [
7
+ tag,
8
+ props
9
+ ].concat(children));
10
+ }
11
+
12
+ function field(row, name) {
13
+ if (typeof row === "object" && row !== null && !Array.isArray(row)) {
14
+ return row[name];
15
+ }
16
+ }
17
+
18
+ function string(row, name) {
19
+ let match = field(row, name);
20
+ if (typeof match === "string") {
21
+ return match;
22
+ }
23
+ }
24
+
25
+ function text(row, name) {
26
+ let value = string(row, name);
27
+ if (value !== undefined && value !== "") {
28
+ return value;
29
+ }
30
+ }
31
+
32
+ function float(row, name) {
33
+ let match = field(row, name);
34
+ if (typeof match === "number") {
35
+ return match;
36
+ }
37
+ }
38
+
39
+ function bool(row, name) {
40
+ let match = field(row, name);
41
+ if (typeof match === "boolean") {
42
+ return match;
43
+ }
44
+ }
45
+
46
+ function array(row, name) {
47
+ let match = field(row, name);
48
+ if (Array.isArray(match)) {
49
+ return match;
50
+ }
51
+ }
52
+
53
+ function money(row, name) {
54
+ let match = field(row, name);
55
+ if (match === undefined) {
56
+ return;
57
+ }
58
+ if (typeof match !== "object" || match === null || Array.isArray(match)) {
59
+ return;
60
+ }
61
+ let match$1 = match["amount"];
62
+ let match$2 = match["currency"];
63
+ if (typeof match$1 === "number" && typeof match$2 === "string") {
64
+ return [
65
+ match$1,
66
+ match$2
67
+ ];
68
+ }
69
+ }
70
+
71
+ function firstAttachment(row, name) {
72
+ let nonEmpty = (member, key) => {
73
+ let match = member[key];
74
+ if (typeof match === "string" && match !== "") {
75
+ return match;
76
+ }
77
+ };
78
+ let match = field(row, name);
79
+ if (match === undefined) {
80
+ return;
81
+ }
82
+ if (!Array.isArray(match)) {
83
+ return;
84
+ }
85
+ let match$1 = match[0];
86
+ if (typeof match$1 === "object" && match$1 !== null && !Array.isArray(match$1)) {
87
+ return [
88
+ nonEmpty(match$1, "altText"),
89
+ nonEmpty(match$1, "caption")
90
+ ];
91
+ }
92
+ }
93
+
94
+ let Row = {
95
+ field: field,
96
+ string: string,
97
+ text: text,
98
+ float: float,
99
+ bool: bool,
100
+ array: array,
101
+ money: money,
102
+ firstAttachment: firstAttachment
103
+ };
104
+
105
+ function money$1(param) {
106
+ let format = new Intl.NumberFormat(undefined, {
107
+ currency: param[1],
108
+ style: "currency"
6
109
  });
110
+ let digits = format.resolvedOptions().maximumFractionDigits;
111
+ let digits$1 = digits !== undefined ? digits : 2;
112
+ return format.format(param[0] / Math.pow(10.0, digits$1));
113
+ }
114
+
115
+ function day(iso) {
116
+ let at = new Date(iso);
117
+ if (Number.isNaN(at.getTime())) {
118
+ return "";
119
+ } else {
120
+ return at.toLocaleDateString();
121
+ }
122
+ }
123
+
124
+ function pad(n) {
125
+ if (n < 10) {
126
+ return "0" + n.toString();
127
+ } else {
128
+ return n.toString();
129
+ }
130
+ }
131
+
132
+ function isoDay(iso) {
133
+ let at = new Date(iso);
134
+ if (Number.isNaN(at.getTime())) {
135
+ return "";
136
+ } else {
137
+ return at.getFullYear().toString() + "-" + pad(at.getMonth() + 1 | 0) + "-" + pad(at.getDate());
138
+ }
139
+ }
140
+
141
+ function titleOf(payload) {
142
+ let label = payload.label;
143
+ if (label !== undefined) {
144
+ return label;
145
+ } else {
146
+ return Stdlib_Option.getOr(string(payload.row, "name"), "");
147
+ }
148
+ }
149
+
150
+ function ensureStyles(id, css) {
151
+ let doc = globalThis.document;
152
+ if (doc == null) {
153
+ return;
154
+ }
155
+ let match = doc.getElementById(id);
156
+ if (!(match == null)) {
157
+ return;
158
+ }
159
+ let style = doc.createElement("style");
160
+ style.id = id;
161
+ style.textContent = css;
162
+ doc.head.appendChild(style);
163
+ }
7
164
 
8
165
  let cardsFace = "cards.face";
9
166
 
@@ -44,20 +201,34 @@ let cardsEmpty = "cards.empty";
44
201
 
45
202
  let galleryEmpty = "gallery.empty";
46
203
 
204
+ let listSelection = "list.selection";
205
+
47
206
  let all$1 = [
48
207
  cardsEmpty,
49
- galleryEmpty
208
+ galleryEmpty,
209
+ listSelection
50
210
  ];
51
211
 
52
212
  let ViewSlot = {
53
213
  cardsEmpty: cardsEmpty,
54
214
  galleryEmpty: galleryEmpty,
215
+ listSelection: listSelection,
55
216
  all: all$1
56
217
  };
57
218
 
219
+ let Format = {
220
+ money: money$1,
221
+ day: day,
222
+ isoDay: isoDay
223
+ };
224
+
58
225
  export {
59
226
  h,
60
227
  RowSlot,
228
+ Row,
229
+ Format,
230
+ titleOf,
231
+ ensureStyles,
61
232
  ViewSlot,
62
233
  }
63
234
  /* No side effect */
@@ -42,17 +42,21 @@ type rowPayload = {
42
42
  actions?: array<action>,
43
43
  }
44
44
 
45
- /** What a per-view region is handed. `selection` and `run` are declared and
46
- filled by nothing yet — cross-row selection is its own piece of work. They
47
- are in the contract from the start because discovering that a region needs a
48
- second arity after publishing would be a breaking change. */
45
+ /** What a per-view region is handed. */
49
46
  type viewPayload = {
50
47
  /** The rows the mode is currently drawing — its page window, not the table. */
51
48
  rows: array<JSON.t>,
52
49
  /** How many rows that window holds. */
53
50
  total?: int,
54
- /** Row ids the caller has picked out, where the mode offers picking. */
51
+ /** Row ids the caller has picked out, where the view offers picking. */
55
52
  selection?: array<string>,
53
+ /** The rows behind `selection`, as they were when picked.
54
+
55
+ A list's window is replaced page by page, so a row picked on page 1 is
56
+ gone from `rows` by the time the caller is on page 3 — and a bar whose job
57
+ is to name what is in it cannot look one up. So the rows travel, each as
58
+ the same `rowPayload` a per-row region is handed. */
59
+ picked?: array<rowPayload>,
56
60
  /** Start the region's one command, by name. */
57
61
  run?: string => unit,
58
62
  }
@@ -119,11 +123,123 @@ module RowSlot: {
119
123
  let all: array<string>
120
124
  }
121
125
 
126
+ /* ── Helpers ────────────────────────────────────────────────────────────────
127
+
128
+ Everything below is runtime code, and it is ReScript-only on purpose rather
129
+ than by omission.
130
+
131
+ A ReScript slot module is bundled before it is served, so importing a helper
132
+ costs it nothing. A JavaScript one is served as written — no bundler, no
133
+ dependency, which is that path's whole point — and anything it imported from
134
+ this package would take that away. `index.js` therefore stays what it is: one
135
+ identity function for the type checker, and no runtime to depend on.
136
+
137
+ The JS contract is the same contract; it just reads its own fields. */
138
+
139
+ /** Reading the row you were handed.
140
+
141
+ `row` is the read model's own JSON, so every renderer that shows a field has
142
+ to get it out of a `JSON.t` — and there is one right answer per field type,
143
+ which is why these are here rather than rewritten per deployment. None of
144
+ them reaches past the payload: they read what `rowPayload` already carries. */
145
+ module Row: {
146
+ /** One field, whatever its type. */
147
+ let field: (JSON.t, string) => option<JSON.t>
148
+
149
+ /** A string field. `None` when absent or not a string. */
150
+ let string: (JSON.t, string) => option<string>
151
+
152
+ /** A string field that actually says something.
153
+
154
+ Distinct from `string` because a view writes `""` for "not yet" — an order
155
+ that has not shipped carries `shippedAt: ""` — and a blank is not a value
156
+ to render. Take this one wherever an empty string should read as absent. */
157
+ let text: (JSON.t, string) => option<string>
158
+
159
+ /** A number field. */
160
+ let float: (JSON.t, string) => option<float>
161
+
162
+ /** A boolean field. */
163
+ let bool: (JSON.t, string) => option<bool>
164
+
165
+ /** An array field, as raw elements. */
166
+ let array: (JSON.t, string) => option<array<JSON.t>>
167
+
168
+ /** A `Money` field: whole minor units and the currency they are in.
169
+
170
+ Reventless serialises money as `{amount, currency}` where `amount` is
171
+ **minor units** — 1000 is €10.00, ¥1000 or 1.000 TND depending on the
172
+ currency. Pair it with `Format.money`, which asks `Intl` for the scale
173
+ rather than assuming a hundred. */
174
+ let money: (JSON.t, string) => option<(float, string)>
175
+
176
+ /** The first member of a captioned attachment set, as `(altText, caption)`.
177
+
178
+ Reventless serialises these as `{ref, altText?, caption?}`, where `ref` is
179
+ a **storage ref, not a URL**: only `rowPayload.image` is rebased for this
180
+ deployment's asset origins. So this reads the *text* of a member, never its
181
+ picture — resolving one is the producer's job. */
182
+ let firstAttachment: (JSON.t, string) => option<(option<string>, option<string>)>
183
+ }
184
+
185
+ /** Turning values into the strings a region shows. */
186
+ module Format: {
187
+ /** Money, at the scale its currency actually uses.
188
+
189
+ Takes what `Row.money` returns. The number of minor units per unit comes
190
+ from `Intl` rather than being assumed to be a hundred, which is right for
191
+ most currencies and quietly wrong for JPY and TND. */
192
+ let money: ((float, string)) => string
193
+
194
+ /** An ISO instant as a short date **in the viewer's own locale**.
195
+
196
+ Which format that is comes from the runtime, not from you or from the
197
+ deployment: the same order reads `9/4/2026` to one customer and `04/09/2026`
198
+ to another, and a screenshot never matches CI. That is right for a surface
199
+ whose readers are the public, and wrong wherever a date has to be
200
+ unambiguous or stable — take `isoDay` there. */
201
+ let day: string => string
202
+
203
+ /** An ISO instant as `YYYY-MM-DD`, the same everywhere.
204
+
205
+ Built from the **local** calendar parts rather than by slicing the string,
206
+ which would silently publish the UTC date and land a day out either side of
207
+ midnight for most of the world. */
208
+ let isoDay: string => string
209
+ }
210
+
211
+ /** What the mode would have titled this row, else the row's own `name`.
212
+
213
+ The precedence is the contract's, not the deployment's: `label` is what the
214
+ producer resolved, and a renderer that preferred the raw field would ignore
215
+ it. */
216
+ let titleOf: rowPayload => string
217
+
218
+ /** Put a stylesheet in the page, once, under an id of your choosing.
219
+
220
+ Slot modules ship their own CSS and there is nowhere else to put it. Doing it
221
+ by hand is easy to get subtly wrong: the shell re-imports a module on a hot
222
+ reload, so an unguarded injection appends the same rules again on every save.
223
+ This no-ops when the id is already present, and no-ops entirely outside a
224
+ browser so a module stays loadable by a test on Node.
225
+
226
+ Call it from `register` rather than at module scope, so importing the module
227
+ has no side effect. */
228
+ let ensureStyles: (~id: string, string) => unit
229
+
122
230
  /** Per-view slots the shipped modes offer. */
123
231
  module ViewSlot: {
124
232
  /** The card grid when the view has no rows. */
125
233
  let cardsEmpty: string
126
234
  /** The tile grid when the view has no rows. */
127
235
  let galleryEmpty: string
236
+ /** The bar over a list whose rows the caller can pick out: what is picked, a
237
+ way to empty it, and the one command that takes the lot.
238
+
239
+ Offered by the list producer rather than by a view mode — the first id
240
+ that is not `<mode>.<region>`. The list renders the mode inside its own
241
+ chrome and the bar is that chrome, so a picking made in the table survives
242
+ a switch to cards and the bar has to outlive both. */
243
+ let listSelection: string
128
244
  let all: array<string>
129
245
  }
package/src/index.d.ts CHANGED
@@ -48,19 +48,21 @@ export interface RowPayload<Row = Record<string, unknown>> {
48
48
  actions?: SlotAction[]
49
49
  }
50
50
 
51
- /** What a per-view region is handed.
52
- *
53
- * `selection` and `run` are declared and filled by nothing yet: cross-row
54
- * selection is its own piece of work. They are here from the start because
55
- * discovering that a region needs a second arity after the contract is
56
- * published would be a breaking change. */
51
+ /** What a per-view region is handed. */
57
52
  export interface ViewPayload<Row = Record<string, unknown>> {
58
53
  /** The rows the mode is currently drawing — its page window, not the table. */
59
54
  rows: Row[]
60
55
  /** How many rows that window holds. */
61
56
  total?: number
62
- /** Row ids the caller has picked out, where the mode offers picking. */
57
+ /** Row ids the caller has picked out, where the view offers picking. */
63
58
  selection?: string[]
59
+ /** The rows behind `selection`, as they were when picked.
60
+ *
61
+ * A list's window is replaced page by page, so a row picked on page 1 is gone
62
+ * from `rows` by the time the caller is on page 3 — and a bar whose job is to
63
+ * name what is in it cannot look one up. So the rows travel, each as the same
64
+ * `RowPayload` a per-row region is handed. */
65
+ picked?: RowPayload<Row>[]
64
66
  /** Start the region's one command, by name. */
65
67
  run?: (command: string) => void
66
68
  }
@@ -126,8 +128,14 @@ export type RowSlotId =
126
128
  * | --- | --- |
127
129
  * | `cards.empty` | the card grid when the view has no rows |
128
130
  * | `gallery.empty` | the tile grid when the view has no rows |
131
+ * | `list.selection` | the bar over a list whose rows the caller can pick out |
132
+ *
133
+ * `list.selection` is the one id that is not `<mode>.<region>`: it belongs to
134
+ * the list producer, not to a mode. The list renders the mode inside its own
135
+ * chrome and the bar is that chrome, so a picking made in the table survives a
136
+ * switch to cards and the bar has to outlive both.
129
137
  */
130
- export type ViewSlotId = 'cards.empty' | 'gallery.empty'
138
+ export type ViewSlotId = 'cards.empty' | 'gallery.empty' | 'list.selection'
131
139
 
132
140
  /**
133
141
  * Identity helper: type-checks a `register` function without changing it.