@reventlessdev/reventless-ui-slots 3.0.0-alpha.1 → 3.0.0-alpha.2
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 +22 -0
- package/package.json +5 -3
- package/src/ReventlessSlots.res +152 -7
- package/src/ReventlessSlots.res.mjs +151 -2
- package/src/ReventlessSlots.resi +91 -0
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.
|
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.2",
|
|
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": "6d74b362ba047678919b27c22f84e698dea48741"
|
|
56
58
|
}
|
package/src/ReventlessSlots.res
CHANGED
|
@@ -32,14 +32,159 @@ type slots = {
|
|
|
32
32
|
|
|
33
33
|
type registerArg = {h: hyperscript, slots: slots}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
/* `React.createElement(tag, props, ...children)` against the function the shell
|
|
36
|
+
passed in.
|
|
37
|
+
|
|
38
|
+
`@variadic` cannot express this: it binds a module member, and the callee here
|
|
39
|
+
is a value that arrived as an argument. So the call goes through
|
|
40
|
+
`Function.prototype.apply`, which is exactly "call this value with these
|
|
41
|
+
arguments" and is the one thing in JavaScript that takes an argument list as
|
|
42
|
+
data.
|
|
43
|
+
|
|
44
|
+
`apply` rather than handing React the array as a single child, which would
|
|
45
|
+
also compile: React reads an array child as a keyed list and warns about
|
|
46
|
+
missing keys, where positional children carry no such expectation. The
|
|
47
|
+
distinction is invisible until a slot author's console fills up. */
|
|
48
|
+
|
|
49
|
+
// One element of that argument list. Heterogeneous by nature — a tag, then
|
|
50
|
+
// props, then elements — so the three are widened to a common type on the way
|
|
51
|
+
// in. Nothing reads an `arg` back out; it exists only to be passed on.
|
|
52
|
+
//
|
|
53
|
+
// `args` widens the children array in one step rather than per element: mapping
|
|
54
|
+
// `arg` over it would compile to an identity `.map`, allocating a second array
|
|
55
|
+
// on every element built.
|
|
56
|
+
type arg
|
|
57
|
+
external arg: 'a => arg = "%identity"
|
|
58
|
+
external args: array<React.element> => array<arg> = "%identity"
|
|
59
|
+
|
|
60
|
+
@send
|
|
61
|
+
external applyTo: (hyperscript, Nullable.t<unit>, array<arg>) => React.element = "apply"
|
|
62
|
+
|
|
63
|
+
let h = (hyperscript: hyperscript, tag: 'tag, props: 'props, children: array<React.element>) =>
|
|
64
|
+
hyperscript->applyTo(Nullable.null, [arg(tag), arg(props)]->Array.concat(args(children)))
|
|
65
|
+
|
|
66
|
+
// Reading the row's JSON. One right answer per field type, so it lives here
|
|
67
|
+
// rather than being rewritten — subtly differently — per deployment.
|
|
68
|
+
module Row = {
|
|
69
|
+
let field = (row: JSON.t, name: string): option<JSON.t> =>
|
|
70
|
+
switch row {
|
|
71
|
+
| Object(fields) => fields->Dict.get(name)
|
|
72
|
+
| _ => None
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let string = (row: JSON.t, name: string): option<string> =>
|
|
76
|
+
switch field(row, name) {
|
|
77
|
+
| Some(String(value)) => Some(value)
|
|
78
|
+
| _ => None
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let text = (row: JSON.t, name: string): option<string> =>
|
|
82
|
+
switch string(row, name) {
|
|
83
|
+
| Some("") | None => None
|
|
84
|
+
| Some(value) => Some(value)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let float = (row: JSON.t, name: string): option<float> =>
|
|
88
|
+
switch field(row, name) {
|
|
89
|
+
| Some(Number(value)) => Some(value)
|
|
90
|
+
| _ => None
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let bool = (row: JSON.t, name: string): option<bool> =>
|
|
94
|
+
switch field(row, name) {
|
|
95
|
+
| Some(Boolean(value)) => Some(value)
|
|
96
|
+
| _ => None
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let array = (row: JSON.t, name: string): option<array<JSON.t>> =>
|
|
100
|
+
switch field(row, name) {
|
|
101
|
+
| Some(Array(values)) => Some(values)
|
|
102
|
+
| _ => None
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let money = (row: JSON.t, name: string): option<(float, string)> =>
|
|
106
|
+
switch field(row, name) {
|
|
107
|
+
| Some(Object(value)) =>
|
|
108
|
+
switch (value->Dict.get("amount"), value->Dict.get("currency")) {
|
|
109
|
+
| (Some(Number(amount)), Some(String(currency))) => Some((amount, currency))
|
|
110
|
+
| _ => None
|
|
111
|
+
}
|
|
112
|
+
| _ => None
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let firstAttachment = (row: JSON.t, name: string): option<(option<string>, option<string>)> => {
|
|
116
|
+
let nonEmpty = (member: Dict.t<JSON.t>, key: string) =>
|
|
117
|
+
switch member->Dict.get(key) {
|
|
118
|
+
| Some(String("")) | None => None
|
|
119
|
+
| Some(String(value)) => Some(value)
|
|
120
|
+
| Some(_) => None
|
|
121
|
+
}
|
|
122
|
+
switch field(row, name) {
|
|
123
|
+
| Some(Array(members)) =>
|
|
124
|
+
switch members->Array.get(0) {
|
|
125
|
+
| Some(JSON.Object(first)) => Some((nonEmpty(first, "altText"), nonEmpty(first, "caption")))
|
|
126
|
+
| _ => None
|
|
127
|
+
}
|
|
128
|
+
| _ => None
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module Format = {
|
|
134
|
+
let money = ((amount, currency): (float, string)): string => {
|
|
135
|
+
let format = Intl.NumberFormat.make(~options={style: #currency, currency})
|
|
136
|
+
let digits = switch Intl.NumberFormat.resolvedOptions(format).maximumFractionDigits {
|
|
137
|
+
| Some(digits) => (digits :> int)
|
|
138
|
+
| None => 2
|
|
139
|
+
}
|
|
140
|
+
Intl.NumberFormat.format(format, amount /. Math.pow(10.0, ~exp=Int.toFloat(digits)))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let day = (iso: string): string => {
|
|
144
|
+
let at = Date.fromString(iso)
|
|
145
|
+
Float.isNaN(Date.getTime(at)) ? "" : Date.toLocaleDateString(at)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let titleOf = (payload: rowPayload): string =>
|
|
150
|
+
switch payload.label {
|
|
151
|
+
| Some(label) => label
|
|
152
|
+
| None => Row.string(payload.row, "name")->Option.getOr("")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// The DOM calls `ensureStyles` makes. Bound here rather than pulled from a DOM
|
|
156
|
+
// binding package, so this package keeps its single dependency.
|
|
157
|
+
//
|
|
158
|
+
// `document` is reached through `globalThis` rather than as a bare global, and
|
|
159
|
+
// that is the load-bearing detail: referencing an undeclared `document` throws a
|
|
160
|
+
// ReferenceError on Node, while `globalThis.document` is simply `undefined`
|
|
161
|
+
// there. It is what lets a slot module be imported and driven by a test.
|
|
162
|
+
module Dom = {
|
|
163
|
+
type document
|
|
164
|
+
type element
|
|
165
|
+
|
|
166
|
+
@val @scope("globalThis") external document: Nullable.t<document> = "document"
|
|
167
|
+
@send external getElementById: (document, string) => Nullable.t<element> = "getElementById"
|
|
168
|
+
@send external createElement: (document, string) => element = "createElement"
|
|
169
|
+
@get external head: document => element = "head"
|
|
170
|
+
@set external setId: (element, string) => unit = "id"
|
|
171
|
+
@set external setTextContent: (element, string) => unit = "textContent"
|
|
172
|
+
@send external appendChild: (element, element) => unit = "appendChild"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let ensureStyles = (~id: string, css: string): unit =>
|
|
176
|
+
switch Nullable.toOption(Dom.document) {
|
|
177
|
+
| None => ()
|
|
178
|
+
| Some(doc) =>
|
|
179
|
+
switch Nullable.toOption(doc->Dom.getElementById(id)) {
|
|
180
|
+
| Some(_) => ()
|
|
181
|
+
| None =>
|
|
182
|
+
let style = doc->Dom.createElement("style")
|
|
183
|
+
style->Dom.setId(id)
|
|
184
|
+
style->Dom.setTextContent(css)
|
|
185
|
+
doc->Dom.head->Dom.appendChild(style)
|
|
186
|
+
}
|
|
41
187
|
}
|
|
42
|
-
`)
|
|
43
188
|
|
|
44
189
|
module RowSlot = {
|
|
45
190
|
let cardsFace = "cards.face"
|
|
@@ -1,9 +1,154 @@
|
|
|
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
|
+
let Format = {
|
|
125
|
+
money: money$1,
|
|
126
|
+
day: day
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function titleOf(payload) {
|
|
130
|
+
let label = payload.label;
|
|
131
|
+
if (label !== undefined) {
|
|
132
|
+
return label;
|
|
133
|
+
} else {
|
|
134
|
+
return Stdlib_Option.getOr(string(payload.row, "name"), "");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function ensureStyles(id, css) {
|
|
139
|
+
let doc = globalThis.document;
|
|
140
|
+
if (doc == null) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
let match = doc.getElementById(id);
|
|
144
|
+
if (!(match == null)) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
let style = doc.createElement("style");
|
|
148
|
+
style.id = id;
|
|
149
|
+
style.textContent = css;
|
|
150
|
+
doc.head.appendChild(style);
|
|
151
|
+
}
|
|
7
152
|
|
|
8
153
|
let cardsFace = "cards.face";
|
|
9
154
|
|
|
@@ -58,6 +203,10 @@ let ViewSlot = {
|
|
|
58
203
|
export {
|
|
59
204
|
h,
|
|
60
205
|
RowSlot,
|
|
206
|
+
Row,
|
|
207
|
+
Format,
|
|
208
|
+
titleOf,
|
|
209
|
+
ensureStyles,
|
|
61
210
|
ViewSlot,
|
|
62
211
|
}
|
|
63
212
|
/* No side effect */
|
package/src/ReventlessSlots.resi
CHANGED
|
@@ -119,6 +119,97 @@ module RowSlot: {
|
|
|
119
119
|
let all: array<string>
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/* ── Helpers ────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
Everything below is runtime code, and it is ReScript-only on purpose rather
|
|
125
|
+
than by omission.
|
|
126
|
+
|
|
127
|
+
A ReScript slot module is bundled before it is served, so importing a helper
|
|
128
|
+
costs it nothing. A JavaScript one is served as written — no bundler, no
|
|
129
|
+
dependency, which is that path's whole point — and anything it imported from
|
|
130
|
+
this package would take that away. `index.js` therefore stays what it is: one
|
|
131
|
+
identity function for the type checker, and no runtime to depend on.
|
|
132
|
+
|
|
133
|
+
The JS contract is the same contract; it just reads its own fields. */
|
|
134
|
+
|
|
135
|
+
/** Reading the row you were handed.
|
|
136
|
+
|
|
137
|
+
`row` is the read model's own JSON, so every renderer that shows a field has
|
|
138
|
+
to get it out of a `JSON.t` — and there is one right answer per field type,
|
|
139
|
+
which is why these are here rather than rewritten per deployment. None of
|
|
140
|
+
them reaches past the payload: they read what `rowPayload` already carries. */
|
|
141
|
+
module Row: {
|
|
142
|
+
/** One field, whatever its type. */
|
|
143
|
+
let field: (JSON.t, string) => option<JSON.t>
|
|
144
|
+
|
|
145
|
+
/** A string field. `None` when absent or not a string. */
|
|
146
|
+
let string: (JSON.t, string) => option<string>
|
|
147
|
+
|
|
148
|
+
/** A string field that actually says something.
|
|
149
|
+
|
|
150
|
+
Distinct from `string` because a view writes `""` for "not yet" — an order
|
|
151
|
+
that has not shipped carries `shippedAt: ""` — and a blank is not a value
|
|
152
|
+
to render. Take this one wherever an empty string should read as absent. */
|
|
153
|
+
let text: (JSON.t, string) => option<string>
|
|
154
|
+
|
|
155
|
+
/** A number field. */
|
|
156
|
+
let float: (JSON.t, string) => option<float>
|
|
157
|
+
|
|
158
|
+
/** A boolean field. */
|
|
159
|
+
let bool: (JSON.t, string) => option<bool>
|
|
160
|
+
|
|
161
|
+
/** An array field, as raw elements. */
|
|
162
|
+
let array: (JSON.t, string) => option<array<JSON.t>>
|
|
163
|
+
|
|
164
|
+
/** A `Money` field: whole minor units and the currency they are in.
|
|
165
|
+
|
|
166
|
+
Reventless serialises money as `{amount, currency}` where `amount` is
|
|
167
|
+
**minor units** — 1000 is €10.00, ¥1000 or 1.000 TND depending on the
|
|
168
|
+
currency. Pair it with `Format.money`, which asks `Intl` for the scale
|
|
169
|
+
rather than assuming a hundred. */
|
|
170
|
+
let money: (JSON.t, string) => option<(float, string)>
|
|
171
|
+
|
|
172
|
+
/** The first member of a captioned attachment set, as `(altText, caption)`.
|
|
173
|
+
|
|
174
|
+
Reventless serialises these as `{ref, altText?, caption?}`, where `ref` is
|
|
175
|
+
a **storage ref, not a URL**: only `rowPayload.image` is rebased for this
|
|
176
|
+
deployment's asset origins. So this reads the *text* of a member, never its
|
|
177
|
+
picture — resolving one is the producer's job. */
|
|
178
|
+
let firstAttachment: (JSON.t, string) => option<(option<string>, option<string>)>
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Turning values into the strings a region shows. */
|
|
182
|
+
module Format: {
|
|
183
|
+
/** Money, at the scale its currency actually uses.
|
|
184
|
+
|
|
185
|
+
Takes what `Row.money` returns. The number of minor units per unit comes
|
|
186
|
+
from `Intl` rather than being assumed to be a hundred, which is right for
|
|
187
|
+
most currencies and quietly wrong for JPY and TND. */
|
|
188
|
+
let money: ((float, string)) => string
|
|
189
|
+
|
|
190
|
+
/** An ISO instant as a short local date. `""` when it does not parse. */
|
|
191
|
+
let day: string => string
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** What the mode would have titled this row, else the row's own `name`.
|
|
195
|
+
|
|
196
|
+
The precedence is the contract's, not the deployment's: `label` is what the
|
|
197
|
+
producer resolved, and a renderer that preferred the raw field would ignore
|
|
198
|
+
it. */
|
|
199
|
+
let titleOf: rowPayload => string
|
|
200
|
+
|
|
201
|
+
/** Put a stylesheet in the page, once, under an id of your choosing.
|
|
202
|
+
|
|
203
|
+
Slot modules ship their own CSS and there is nowhere else to put it. Doing it
|
|
204
|
+
by hand is easy to get subtly wrong: the shell re-imports a module on a hot
|
|
205
|
+
reload, so an unguarded injection appends the same rules again on every save.
|
|
206
|
+
This no-ops when the id is already present, and no-ops entirely outside a
|
|
207
|
+
browser so a module stays loadable by a test on Node.
|
|
208
|
+
|
|
209
|
+
Call it from `register` rather than at module scope, so importing the module
|
|
210
|
+
has no side effect. */
|
|
211
|
+
let ensureStyles: (~id: string, string) => unit
|
|
212
|
+
|
|
122
213
|
/** Per-view slots the shipped modes offer. */
|
|
123
214
|
module ViewSlot: {
|
|
124
215
|
/** The card grid when the view has no rows. */
|