@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 +34 -5
- package/package.json +5 -3
- package/src/ReventlessSlots.res +168 -8
- package/src/ReventlessSlots.res.mjs +174 -3
- package/src/ReventlessSlots.resi +121 -5
- package/src/index.d.ts +16 -8
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`
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
a
|
|
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.
|
|
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": "
|
|
57
|
+
"gitHead": "d2a7156f66c7f5f42837c6d5c13faf6d33a6df7e"
|
|
56
58
|
}
|
package/src/ReventlessSlots.res
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
5
|
-
|
|
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 */
|
package/src/ReventlessSlots.resi
CHANGED
|
@@ -42,17 +42,21 @@ type rowPayload = {
|
|
|
42
42
|
actions?: array<action>,
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/** What a per-view region is handed.
|
|
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
|
|
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
|
|
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.
|