@checkcourt/sdk 0.9.0 → 0.11.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/README.md +55 -0
- package/dist/extensions.d.ts +9 -1
- package/dist/iframe.d.ts +1 -1
- package/dist/iframe.js +3 -0
- package/dist/manifest.d.ts +26 -0
- package/dist/manifest.js +6 -0
- package/dist/ui.d.ts +53 -6
- package/dist/ui.js +101 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -193,6 +193,36 @@ ui.form({
|
|
|
193
193
|
});
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
+
Blocks can carry an `icon` from `UI_ICONS` (text, heading, stat, badge, list items), a `stat`
|
|
197
|
+
can be `size: "lg"`, badges have a soft amber `"warning"` variant, `ui.columns` lays out 2
|
|
198
|
+
to 6 children in equal widths, and `ui.row(children, { justify: "between" })` spreads a row
|
|
199
|
+
across the full width (first child left, last child right). A weather widget:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const days = [
|
|
203
|
+
["Heute", "sun", "24°"],
|
|
204
|
+
["Do", "cloud-sun", "21°"],
|
|
205
|
+
["Fr", "cloud-rain", "17°"],
|
|
206
|
+
["Sa", "cloud-drizzle", "18°"],
|
|
207
|
+
["So", "sun", "23°"],
|
|
208
|
+
] as const;
|
|
209
|
+
|
|
210
|
+
ui.doc([
|
|
211
|
+
ui.row(
|
|
212
|
+
[
|
|
213
|
+
ui.stat("Jetzt", "24°", { icon: "sun", size: "lg", hint: "Sonnig" }),
|
|
214
|
+
ui.badge("Regen möglich", { variant: "warning", icon: "droplets" }),
|
|
215
|
+
],
|
|
216
|
+
{ justify: "between" },
|
|
217
|
+
),
|
|
218
|
+
ui.divider(),
|
|
219
|
+
ui.columns(
|
|
220
|
+
days.map(([day, icon, temp]) => ui.stat(day, temp, { icon })),
|
|
221
|
+
{ dividers: true, align: "center" },
|
|
222
|
+
),
|
|
223
|
+
]);
|
|
224
|
+
```
|
|
225
|
+
|
|
196
226
|
Compare `context.installation_id` and `context.tenant_id` with what you stored from
|
|
197
227
|
`app.installed` before you act on a request. For the context token alone (for example in
|
|
198
228
|
the backend of an iframe extension), use `verifyExtensionContext(token, secret)`.
|
|
@@ -246,6 +276,31 @@ if (ext.point === "nav.page") {
|
|
|
246
276
|
}
|
|
247
277
|
```
|
|
248
278
|
|
|
279
|
+
#### Permissions for the club's roles
|
|
280
|
+
|
|
281
|
+
A club app can offer its own rights (at most `MAX_APP_PERMISSIONS`, keys matching
|
|
282
|
+
`APP_PERMISSION_KEY_PATTERN`). The club decides in its role editor which roles hold them; only
|
|
283
|
+
administrators hold them from the start. Every extension request carries the viewer's flags
|
|
284
|
+
for all declared keys in `viewer.capabilities.permissions`, and a `nav.page` with `requires`
|
|
285
|
+
is only listed and opened for holders:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
defineManifest({
|
|
289
|
+
installTargets: ["tenant"],
|
|
290
|
+
permissions: [{ key: "manage_ladder", label: "Rangliste verwalten", description: "Darf Ergebnisse korrigieren" }],
|
|
291
|
+
extensions: [
|
|
292
|
+
{ point: "nav.page", kind: "declarative", url: "https://ladder.example.de/ext/admin", label: "Rangliste", icon: "trophy", requires: "manage_ladder" },
|
|
293
|
+
{ point: "dashboard.widget", kind: "declarative", url: "https://ladder.example.de/ext/widget" },
|
|
294
|
+
],
|
|
295
|
+
dataProcessing: { categories: ["Spielergebnisse"], purpose: "Führt die Rangliste", storageLocation: "EU", avvRequired: false },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const { context } = ext; // from verifyExtensionRequest, as above
|
|
299
|
+
if (context.viewer.capabilities.permissions?.manage_ladder) {
|
|
300
|
+
// show the edit buttons
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
249
304
|
### OAuth with PKCE (member apps)
|
|
250
305
|
|
|
251
306
|
```ts
|
package/dist/extensions.d.ts
CHANGED
|
@@ -55,6 +55,14 @@ export interface PointCapabilities {
|
|
|
55
55
|
"sidebar.action": Record<string, never>;
|
|
56
56
|
"nav.page": Record<string, never>;
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Present at every point when the app's manifest declares `permissions`: each declared key with
|
|
60
|
+
* whether the viewer holds it (through a club role; administrators hold all). Absent otherwise.
|
|
61
|
+
* `false` at the kiosk, where nobody is signed in.
|
|
62
|
+
*/
|
|
63
|
+
export interface AppPermissionCapabilities {
|
|
64
|
+
permissions?: Record<string, boolean>;
|
|
65
|
+
}
|
|
58
66
|
export interface PointSubject {
|
|
59
67
|
"app.settings": {
|
|
60
68
|
type: "installation";
|
|
@@ -98,7 +106,7 @@ interface ContextClaimsOf<P extends ExtensionPoint> {
|
|
|
98
106
|
viewer: {
|
|
99
107
|
/** null on the kiosk; a stable `psn_…` pseudonym unless the installation holds `members:read`. */
|
|
100
108
|
user_id: string | null;
|
|
101
|
-
capabilities: PointCapabilities[P];
|
|
109
|
+
capabilities: PointCapabilities[P] & AppPermissionCapabilities;
|
|
102
110
|
};
|
|
103
111
|
iat: number;
|
|
104
112
|
exp: number;
|
package/dist/iframe.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare const MAX_TOAST_LENGTH = 200;
|
|
|
6
6
|
/** Top-level areas `navigate()` may target; anything else is dropped by CheckCourt. */
|
|
7
7
|
export declare const NAVIGABLE_SECTIONS: readonly ["dashboard", "booking", "my-bookings", "club", "team", "trainer", "fees", "arbeitsstunden", "settings", "admin"];
|
|
8
8
|
/** CSS variables CheckCourt sends with every theme message. */
|
|
9
|
-
export declare const THEME_TOKEN_NAMES: readonly ["--background", "--foreground", "--card", "--card-foreground", "--muted", "--muted-foreground", "--primary", "--primary-foreground", "--secondary", "--secondary-foreground", "--accent", "--accent-foreground", "--destructive", "--border", "--input", "--ring", "--radius"];
|
|
9
|
+
export declare const THEME_TOKEN_NAMES: readonly ["--background", "--foreground", "--card", "--card-foreground", "--muted", "--muted-foreground", "--primary", "--primary-foreground", "--secondary", "--secondary-foreground", "--accent", "--accent-foreground", "--destructive", "--warning", "--warning-foreground", "--warning-muted", "--border", "--input", "--ring", "--radius"];
|
|
10
10
|
export type ThemeTokenName = (typeof THEME_TOKEN_NAMES)[number];
|
|
11
11
|
export type FrameMessage = {
|
|
12
12
|
type: "checkcourt:resize";
|
package/dist/iframe.js
CHANGED
package/dist/manifest.d.ts
CHANGED
|
@@ -86,6 +86,25 @@ export declare const STATIC_ACTION_LABEL_MAX = 24;
|
|
|
86
86
|
/** Bounds of an iframe extension's initial `height` in pixels. */
|
|
87
87
|
export declare const IFRAME_EXTENSION_HEIGHT_MIN = 120;
|
|
88
88
|
export declare const IFRAME_EXTENSION_HEIGHT_MAX = 2000;
|
|
89
|
+
/** Keys of app permissions: lowercase letters, digits and `_`, starting with a letter, at most 40 characters. */
|
|
90
|
+
export declare const APP_PERMISSION_KEY_PATTERN: RegExp;
|
|
91
|
+
/** Most permissions one manifest may declare. */
|
|
92
|
+
export declare const MAX_APP_PERMISSIONS = 10;
|
|
93
|
+
export declare const APP_PERMISSION_LABEL_MAX = 60;
|
|
94
|
+
export declare const APP_PERMISSION_DESCRIPTION_MAX = 200;
|
|
95
|
+
/**
|
|
96
|
+
* A right the app offers the club's roles, e.g. who may manage its ladder. The club decides in
|
|
97
|
+
* its role editor which roles hold it; only administrators hold it from the start. Viewers' flags
|
|
98
|
+
* arrive as `viewer.capabilities.permissions` in the extension context.
|
|
99
|
+
*/
|
|
100
|
+
export interface AppPermission {
|
|
101
|
+
/** Matches `APP_PERMISSION_KEY_PATTERN`, unique within the manifest. */
|
|
102
|
+
key: string;
|
|
103
|
+
/** Shown in the role editor, German, 1 to 60 characters, e.g. "Rangliste verwalten". */
|
|
104
|
+
label: string;
|
|
105
|
+
/** Up to 200 characters. */
|
|
106
|
+
description?: string;
|
|
107
|
+
}
|
|
89
108
|
/** lucide icon names a static action or `nav.page` may use as `icon`. */
|
|
90
109
|
export declare const APP_ACTION_ICONS: readonly ["bell", "calendar", "calendar-check", "camera", "chart-column", "circle-help", "clipboard-list", "clock", "cloud-rain", "door-open", "file-text", "flag", "heart-pulse", "info", "key-round", "lightbulb", "link", "list-checks", "lock-open", "map-pin", "megaphone", "message-square", "receipt", "send", "sparkles", "star", "sun", "thermometer", "ticket", "triangle-alert", "trophy", "user-round", "users", "wallet", "wrench"];
|
|
91
110
|
export type AppActionIcon = (typeof APP_ACTION_ICONS)[number];
|
|
@@ -115,6 +134,11 @@ type ExtensionOf<P extends ExtensionPoint> = {
|
|
|
115
134
|
/** Navigation entry and page title, 1 to 24 characters (`STATIC_ACTION_LABEL_MAX`). */
|
|
116
135
|
label: string;
|
|
117
136
|
icon: AppActionIcon;
|
|
137
|
+
/**
|
|
138
|
+
* Key of one of the manifest's `permissions`: only holders see the entry and may open
|
|
139
|
+
* the page; CheckCourt enforces it for the page and its actions.
|
|
140
|
+
*/
|
|
141
|
+
requires?: string;
|
|
118
142
|
} : P extends "booking.action" ? {
|
|
119
143
|
label: string;
|
|
120
144
|
icon?: never;
|
|
@@ -247,6 +271,8 @@ export interface Manifest {
|
|
|
247
271
|
/** At most 20. */
|
|
248
272
|
extensions?: readonly ManifestExtension[];
|
|
249
273
|
settingsSchema?: SettingsSchema;
|
|
274
|
+
/** Rights the app offers the club's roles (at most 10). Club installations only. */
|
|
275
|
+
permissions?: readonly AppPermission[];
|
|
250
276
|
dataProcessing: DataProcessing;
|
|
251
277
|
/** Metadata other apps may read (at most 20). Club installations only. */
|
|
252
278
|
shares?: {
|
package/dist/manifest.js
CHANGED
|
@@ -41,6 +41,12 @@ export const STATIC_ACTION_LABEL_MAX = 24;
|
|
|
41
41
|
/** Bounds of an iframe extension's initial `height` in pixels. */
|
|
42
42
|
export const IFRAME_EXTENSION_HEIGHT_MIN = 120;
|
|
43
43
|
export const IFRAME_EXTENSION_HEIGHT_MAX = 2000;
|
|
44
|
+
/** Keys of app permissions: lowercase letters, digits and `_`, starting with a letter, at most 40 characters. */
|
|
45
|
+
export const APP_PERMISSION_KEY_PATTERN = /^[a-z][a-z0-9_]{0,39}$/;
|
|
46
|
+
/** Most permissions one manifest may declare. */
|
|
47
|
+
export const MAX_APP_PERMISSIONS = 10;
|
|
48
|
+
export const APP_PERMISSION_LABEL_MAX = 60;
|
|
49
|
+
export const APP_PERMISSION_DESCRIPTION_MAX = 200;
|
|
44
50
|
/** lucide icon names a static action or `nav.page` may use as `icon`. */
|
|
45
51
|
export const APP_ACTION_ICONS = [
|
|
46
52
|
"bell",
|
package/dist/ui.d.ts
CHANGED
|
@@ -2,34 +2,55 @@ export declare const UI_VERSION = "v1";
|
|
|
2
2
|
export declare const MAX_UI_BLOCKS = 50;
|
|
3
3
|
/** Top-level blocks sit at depth 1; a container's children one deeper. */
|
|
4
4
|
export declare const MAX_UI_DEPTH = 3;
|
|
5
|
-
export declare const BADGE_VARIANTS: readonly ["default", "secondary", "outline", "destructive"];
|
|
5
|
+
export declare const BADGE_VARIANTS: readonly ["default", "secondary", "outline", "destructive", "warning"];
|
|
6
6
|
export declare const BUTTON_VARIANTS: readonly ["default", "secondary", "outline", "destructive"];
|
|
7
7
|
export type BadgeVariant = (typeof BADGE_VARIANTS)[number];
|
|
8
8
|
export type ButtonVariant = (typeof BUTTON_VARIANTS)[number];
|
|
9
|
+
/** Icons a block may show, by lucide name; CheckCourt draws them in the text color at text size. */
|
|
10
|
+
export declare const UI_ICONS: readonly ["sun", "moon", "cloud", "cloud-sun", "cloud-moon", "cloud-sun-rain", "cloud-rain", "cloud-drizzle", "cloud-lightning", "cloud-snow", "cloud-fog", "snowflake", "wind", "droplet", "droplets", "umbrella", "thermometer", "sunrise", "sunset", "check", "x", "info", "alert-triangle", "triangle-alert", "alert-circle", "circle-alert", "clock", "calendar", "map-pin", "trophy", "users", "user", "lock", "unlock", "lock-open", "lightbulb", "zap", "euro", "star", "heart", "bell", "flag", "activity", "timer", "ticket", "door-open"];
|
|
11
|
+
export type UiIcon = (typeof UI_ICONS)[number];
|
|
12
|
+
/** `lg` shows the value as a large display figure, `md` (default) as a regular stat. */
|
|
13
|
+
export declare const STAT_SIZES: readonly ["md", "lg"];
|
|
14
|
+
export type StatSize = (typeof STAT_SIZES)[number];
|
|
15
|
+
export declare const COLUMNS_ALIGNMENTS: readonly ["start", "center"];
|
|
16
|
+
export type ColumnsAlignment = (typeof COLUMNS_ALIGNMENTS)[number];
|
|
17
|
+
/** `between` spreads a row's children across the full width, first at the left edge, last at the right. */
|
|
18
|
+
export declare const ROW_JUSTIFICATIONS: readonly ["start", "between"];
|
|
19
|
+
export type RowJustify = (typeof ROW_JUSTIFICATIONS)[number];
|
|
20
|
+
/** A `columns` block holds 2 to 6 children. */
|
|
21
|
+
export declare const MIN_COLUMNS = 2;
|
|
22
|
+
export declare const MAX_COLUMNS = 6;
|
|
9
23
|
export type UiTextBlock = {
|
|
10
24
|
type: "text";
|
|
11
25
|
text: string;
|
|
12
26
|
tone?: "muted";
|
|
27
|
+
icon?: UiIcon;
|
|
13
28
|
};
|
|
14
29
|
export type UiHeadingBlock = {
|
|
15
30
|
type: "heading";
|
|
16
31
|
text: string;
|
|
17
32
|
level: 2 | 3;
|
|
33
|
+
icon?: UiIcon;
|
|
18
34
|
};
|
|
35
|
+
/** With `icon`, the icon sits before the value. */
|
|
19
36
|
export type UiStatBlock = {
|
|
20
37
|
type: "stat";
|
|
21
38
|
label: string;
|
|
22
39
|
value: string;
|
|
23
40
|
hint?: string;
|
|
41
|
+
icon?: UiIcon;
|
|
42
|
+
size?: StatSize;
|
|
24
43
|
};
|
|
25
44
|
export type UiBadgeBlock = {
|
|
26
45
|
type: "badge";
|
|
27
46
|
label: string;
|
|
28
47
|
variant?: BadgeVariant;
|
|
48
|
+
icon?: UiIcon;
|
|
29
49
|
};
|
|
30
50
|
export type UiListItem = {
|
|
31
51
|
title: string;
|
|
32
52
|
description?: string;
|
|
53
|
+
icon?: UiIcon;
|
|
33
54
|
};
|
|
34
55
|
export type UiListBlock = {
|
|
35
56
|
type: "list";
|
|
@@ -136,8 +157,16 @@ export type UiStackBlock = {
|
|
|
136
157
|
export type UiRowBlock = {
|
|
137
158
|
type: "row";
|
|
138
159
|
children: UiBlock[];
|
|
160
|
+
justify?: RowJustify;
|
|
139
161
|
};
|
|
140
|
-
|
|
162
|
+
/** 2 to 6 children side by side in equal widths; on narrow cards 4 wrap to 2 per row, 5 and 6 to 3. */
|
|
163
|
+
export type UiColumnsBlock = {
|
|
164
|
+
type: "columns";
|
|
165
|
+
children: UiBlock[];
|
|
166
|
+
dividers?: boolean;
|
|
167
|
+
align?: ColumnsAlignment;
|
|
168
|
+
};
|
|
169
|
+
export type UiBlock = UiTextBlock | UiHeadingBlock | UiStatBlock | UiBadgeBlock | UiListBlock | UiKeyValueBlock | UiLinkBlock | UiButtonBlock | UiFormBlock | UiDividerBlock | UiStackBlock | UiRowBlock | UiColumnsBlock;
|
|
141
170
|
export type UiToast = {
|
|
142
171
|
kind: "success" | "error";
|
|
143
172
|
message: string;
|
|
@@ -265,12 +294,23 @@ export declare const ui: {
|
|
|
265
294
|
readonly hidden: (options?: UiDocumentOptions) => UiHiddenDocument;
|
|
266
295
|
readonly text: (text: string, options?: {
|
|
267
296
|
tone?: "muted";
|
|
297
|
+
icon?: UiIcon;
|
|
268
298
|
}) => UiTextBlock;
|
|
269
|
-
|
|
299
|
+
/** `ui.heading(text, 3)` or `ui.heading(text, { level: 3, icon: "trophy" })`; level defaults to 2. */
|
|
300
|
+
readonly heading: (text: string, levelOrOptions?: 2 | 3 | {
|
|
301
|
+
level?: 2 | 3;
|
|
302
|
+
icon?: UiIcon;
|
|
303
|
+
}) => UiHeadingBlock;
|
|
270
304
|
readonly stat: (label: string, value: string, options?: {
|
|
271
305
|
hint?: string;
|
|
306
|
+
icon?: UiIcon;
|
|
307
|
+
size?: StatSize;
|
|
272
308
|
}) => UiStatBlock;
|
|
273
|
-
|
|
309
|
+
/** `ui.badge(label, "warning")` or `ui.badge(label, { variant: "warning", icon: "droplets" })`. */
|
|
310
|
+
readonly badge: (label: string, variantOrOptions?: BadgeVariant | {
|
|
311
|
+
variant?: BadgeVariant;
|
|
312
|
+
icon?: UiIcon;
|
|
313
|
+
}) => UiBadgeBlock;
|
|
274
314
|
readonly list: (items: UiListItem[]) => UiListBlock;
|
|
275
315
|
/** Pairs keep their order; a plain object is turned into pairs in key order. */
|
|
276
316
|
readonly keyValue: (pairs: UiKeyValuePair[] | Record<string, string>) => UiKeyValueBlock;
|
|
@@ -294,8 +334,15 @@ export declare const ui: {
|
|
|
294
334
|
};
|
|
295
335
|
readonly divider: () => UiDividerBlock;
|
|
296
336
|
readonly stack: (children: UiBlock[]) => UiStackBlock;
|
|
297
|
-
/** Horizontal, wraps on narrow screens. */
|
|
298
|
-
readonly row: (children: UiBlock[]
|
|
337
|
+
/** Horizontal, wraps on narrow screens; `justify: "between"` pushes the last child to the right edge. */
|
|
338
|
+
readonly row: (children: UiBlock[], options?: {
|
|
339
|
+
justify?: RowJustify;
|
|
340
|
+
}) => UiRowBlock;
|
|
341
|
+
/** 2 to 6 equal-width columns, optionally with vertical dividers; throws on any other count. */
|
|
342
|
+
readonly columns: (children: UiBlock[], options?: {
|
|
343
|
+
dividers?: boolean;
|
|
344
|
+
align?: ColumnsAlignment;
|
|
345
|
+
}) => UiColumnsBlock;
|
|
299
346
|
};
|
|
300
347
|
/** Toasts for `ui.doc(blocks, { toast })`, at most 200 characters. */
|
|
301
348
|
export declare const toast: {
|
package/dist/ui.js
CHANGED
|
@@ -2,8 +2,65 @@ export const UI_VERSION = "v1";
|
|
|
2
2
|
export const MAX_UI_BLOCKS = 50;
|
|
3
3
|
/** Top-level blocks sit at depth 1; a container's children one deeper. */
|
|
4
4
|
export const MAX_UI_DEPTH = 3;
|
|
5
|
-
export const BADGE_VARIANTS = ["default", "secondary", "outline", "destructive"];
|
|
5
|
+
export const BADGE_VARIANTS = ["default", "secondary", "outline", "destructive", "warning"];
|
|
6
6
|
export const BUTTON_VARIANTS = ["default", "secondary", "outline", "destructive"];
|
|
7
|
+
/** Icons a block may show, by lucide name; CheckCourt draws them in the text color at text size. */
|
|
8
|
+
export const UI_ICONS = [
|
|
9
|
+
"sun",
|
|
10
|
+
"moon",
|
|
11
|
+
"cloud",
|
|
12
|
+
"cloud-sun",
|
|
13
|
+
"cloud-moon",
|
|
14
|
+
"cloud-sun-rain",
|
|
15
|
+
"cloud-rain",
|
|
16
|
+
"cloud-drizzle",
|
|
17
|
+
"cloud-lightning",
|
|
18
|
+
"cloud-snow",
|
|
19
|
+
"cloud-fog",
|
|
20
|
+
"snowflake",
|
|
21
|
+
"wind",
|
|
22
|
+
"droplet",
|
|
23
|
+
"droplets",
|
|
24
|
+
"umbrella",
|
|
25
|
+
"thermometer",
|
|
26
|
+
"sunrise",
|
|
27
|
+
"sunset",
|
|
28
|
+
"check",
|
|
29
|
+
"x",
|
|
30
|
+
"info",
|
|
31
|
+
"alert-triangle",
|
|
32
|
+
"triangle-alert",
|
|
33
|
+
"alert-circle",
|
|
34
|
+
"circle-alert",
|
|
35
|
+
"clock",
|
|
36
|
+
"calendar",
|
|
37
|
+
"map-pin",
|
|
38
|
+
"trophy",
|
|
39
|
+
"users",
|
|
40
|
+
"user",
|
|
41
|
+
"lock",
|
|
42
|
+
"unlock",
|
|
43
|
+
"lock-open",
|
|
44
|
+
"lightbulb",
|
|
45
|
+
"zap",
|
|
46
|
+
"euro",
|
|
47
|
+
"star",
|
|
48
|
+
"heart",
|
|
49
|
+
"bell",
|
|
50
|
+
"flag",
|
|
51
|
+
"activity",
|
|
52
|
+
"timer",
|
|
53
|
+
"ticket",
|
|
54
|
+
"door-open",
|
|
55
|
+
];
|
|
56
|
+
/** `lg` shows the value as a large display figure, `md` (default) as a regular stat. */
|
|
57
|
+
export const STAT_SIZES = ["md", "lg"];
|
|
58
|
+
export const COLUMNS_ALIGNMENTS = ["start", "center"];
|
|
59
|
+
/** `between` spreads a row's children across the full width, first at the left edge, last at the right. */
|
|
60
|
+
export const ROW_JUSTIFICATIONS = ["start", "between"];
|
|
61
|
+
/** A `columns` block holds 2 to 6 children. */
|
|
62
|
+
export const MIN_COLUMNS = 2;
|
|
63
|
+
export const MAX_COLUMNS = 6;
|
|
7
64
|
/** CheckCourt caps every render lifetime at this many seconds. */
|
|
8
65
|
export const UI_CACHE_MAX_AGE_LIMIT = 300;
|
|
9
66
|
/** Longest badge label of a `court.annotation`. */
|
|
@@ -75,6 +132,14 @@ function variantOf(variant, what) {
|
|
|
75
132
|
}
|
|
76
133
|
return variant;
|
|
77
134
|
}
|
|
135
|
+
function iconOf(icon) {
|
|
136
|
+
if (icon === undefined)
|
|
137
|
+
return undefined;
|
|
138
|
+
if (!UI_ICONS.includes(icon)) {
|
|
139
|
+
throw new TypeError(`Unknown icon ${String(icon)}; use one of UI_ICONS`);
|
|
140
|
+
}
|
|
141
|
+
return icon;
|
|
142
|
+
}
|
|
78
143
|
function entriesOf(list, what) {
|
|
79
144
|
if (!Array.isArray(list))
|
|
80
145
|
throw new TypeError(`${what} must be an array`);
|
|
@@ -157,19 +222,31 @@ export const ui = {
|
|
|
157
222
|
return compact({ ui: UI_VERSION, hidden: true, toast: options.toast, cache: cacheOf(options.maxAge) });
|
|
158
223
|
},
|
|
159
224
|
text(text, options = {}) {
|
|
160
|
-
return compact({ type: "text", text, tone: options.tone });
|
|
225
|
+
return compact({ type: "text", text, tone: options.tone, icon: iconOf(options.icon) });
|
|
161
226
|
},
|
|
162
|
-
heading(text, level
|
|
163
|
-
|
|
227
|
+
/** `ui.heading(text, 3)` or `ui.heading(text, { level: 3, icon: "trophy" })`; level defaults to 2. */
|
|
228
|
+
heading(text, levelOrOptions = 2) {
|
|
229
|
+
const options = typeof levelOrOptions === "object" ? levelOrOptions : { level: levelOrOptions };
|
|
230
|
+
return compact({ type: "heading", text, level: options.level ?? 2, icon: iconOf(options.icon) });
|
|
164
231
|
},
|
|
165
232
|
stat(label, value, options = {}) {
|
|
166
|
-
|
|
233
|
+
if (options.size !== undefined && !STAT_SIZES.includes(options.size)) {
|
|
234
|
+
throw new TypeError(`stat size must be one of ${STAT_SIZES.join(", ")}`);
|
|
235
|
+
}
|
|
236
|
+
return compact({ type: "stat", label, value, hint: options.hint, icon: iconOf(options.icon), size: options.size });
|
|
167
237
|
},
|
|
168
|
-
badge(label, variant)
|
|
169
|
-
|
|
238
|
+
/** `ui.badge(label, "warning")` or `ui.badge(label, { variant: "warning", icon: "droplets" })`. */
|
|
239
|
+
badge(label, variantOrOptions) {
|
|
240
|
+
const options = typeof variantOrOptions === "object" ? variantOrOptions : { variant: variantOrOptions };
|
|
241
|
+
return compact({
|
|
242
|
+
type: "badge",
|
|
243
|
+
label,
|
|
244
|
+
variant: variantOf(options.variant, "badge variant"),
|
|
245
|
+
icon: iconOf(options.icon),
|
|
246
|
+
});
|
|
170
247
|
},
|
|
171
248
|
list(items) {
|
|
172
|
-
return { type: "list", items: items.map((item) => compact({ ...item })) };
|
|
249
|
+
return { type: "list", items: items.map((item) => compact({ ...item, icon: iconOf(item.icon) })) };
|
|
173
250
|
},
|
|
174
251
|
/** Pairs keep their order; a plain object is turned into pairs in key order. */
|
|
175
252
|
keyValue(pairs) {
|
|
@@ -198,9 +275,22 @@ export const ui = {
|
|
|
198
275
|
stack(children) {
|
|
199
276
|
return { type: "stack", children };
|
|
200
277
|
},
|
|
201
|
-
/** Horizontal, wraps on narrow screens. */
|
|
202
|
-
row(children) {
|
|
203
|
-
|
|
278
|
+
/** Horizontal, wraps on narrow screens; `justify: "between"` pushes the last child to the right edge. */
|
|
279
|
+
row(children, options = {}) {
|
|
280
|
+
if (options.justify !== undefined && !ROW_JUSTIFICATIONS.includes(options.justify)) {
|
|
281
|
+
throw new TypeError(`row justify must be one of ${ROW_JUSTIFICATIONS.join(", ")}`);
|
|
282
|
+
}
|
|
283
|
+
return compact({ type: "row", children, justify: options.justify });
|
|
284
|
+
},
|
|
285
|
+
/** 2 to 6 equal-width columns, optionally with vertical dividers; throws on any other count. */
|
|
286
|
+
columns(children, options = {}) {
|
|
287
|
+
if (!Array.isArray(children) || children.length < MIN_COLUMNS || children.length > MAX_COLUMNS) {
|
|
288
|
+
throw new RangeError(`columns takes ${MIN_COLUMNS} to ${MAX_COLUMNS} children`);
|
|
289
|
+
}
|
|
290
|
+
if (options.align !== undefined && !COLUMNS_ALIGNMENTS.includes(options.align)) {
|
|
291
|
+
throw new TypeError(`columns align must be one of ${COLUMNS_ALIGNMENTS.join(", ")}`);
|
|
292
|
+
}
|
|
293
|
+
return compact({ type: "columns", children, dividers: options.dividers, align: options.align });
|
|
204
294
|
},
|
|
205
295
|
};
|
|
206
296
|
/** Toasts for `ui.doc(blocks, { toast })`, at most 200 characters. */
|