@mdbase-dev/ui 0.1.0-beta.126 → 0.1.0-beta.148
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 +168 -0
- package/css/collections.css +978 -0
- package/css/controls.css +33 -1
- package/css/screens.css +44 -2
- package/dist/AppSwitcher.js +187 -33
- package/dist/Checkbox.d.ts +14 -0
- package/dist/Checkbox.js +32 -0
- package/dist/CollectionModeControl.d.ts +11 -0
- package/dist/CollectionModeControl.js +290 -0
- package/dist/CommandPalette.d.ts +4 -2
- package/dist/CommandPalette.js +275 -103
- package/dist/Dialog.js +72 -25
- package/dist/Feedback.js +763 -229
- package/dist/FeedbackMarkup.d.ts +1 -1
- package/dist/FeedbackMarkup.js +326 -125
- package/dist/FeedbackVerification.js +115 -68
- package/dist/SaveNotice.js +71 -28
- package/dist/Select.js +183 -135
- package/dist/SelectList.js +59 -14
- package/dist/ThemeSelect.js +55 -4
- package/dist/apps.d.ts +1 -0
- package/dist/apps.js +50 -29
- package/dist/brand.js +251 -68
- package/dist/codemirror.js +116 -87
- package/dist/collections/CollectionGate.d.ts +52 -0
- package/dist/collections/CollectionGate.js +456 -0
- package/dist/collections/CollectionSaveWarning.d.ts +10 -0
- package/dist/collections/CollectionSaveWarning.js +28 -0
- package/dist/collections/CollectionSwitcher.d.ts +51 -0
- package/dist/collections/CollectionSwitcher.js +598 -0
- package/dist/collections/SetupReview.d.ts +9 -0
- package/dist/collections/SetupReview.js +216 -0
- package/dist/collections/copy.d.ts +100 -0
- package/dist/collections/copy.js +937 -0
- package/dist/collections/dialogs.d.ts +27 -0
- package/dist/collections/dialogs.js +169 -0
- package/dist/collections/model.d.ts +146 -0
- package/dist/collections/model.js +337 -0
- package/dist/collections/mount.d.ts +19 -0
- package/dist/collections/mount.js +56 -0
- package/dist/collections/parts.d.ts +86 -0
- package/dist/collections/parts.js +439 -0
- package/dist/collections/testing.d.ts +19 -0
- package/dist/collections/testing.js +39 -0
- package/dist/collections/use-collection-ui.d.ts +140 -0
- package/dist/collections/use-collection-ui.js +236 -0
- package/dist/collections.d.ts +19 -0
- package/dist/collections.js +13 -0
- package/dist/feedback-data.d.ts +13 -12
- package/dist/feedback-data.js +365 -168
- package/dist/flock.d.ts +63 -0
- package/dist/flock.js +140 -0
- package/dist/i18n.d.ts +6 -0
- package/dist/i18n.js +71 -0
- package/dist/locales/de/messages.json +1 -0
- package/dist/locales/en/messages.json +1 -0
- package/dist/locales/es/messages.json +1 -0
- package/dist/locales/fr/messages.json +1 -0
- package/dist/locales/it/messages.json +1 -0
- package/dist/locales/ja/messages.json +1 -0
- package/dist/locales/ko/messages.json +1 -0
- package/dist/locales/pt/messages.json +1 -0
- package/dist/locales/ru/messages.json +1 -0
- package/dist/locales/zh/messages.json +1 -0
- package/dist/mark-activity.js +82 -75
- package/dist/permission-selection.js +27 -17
- package/dist/popover.js +138 -130
- package/dist/screens.d.ts +20 -3
- package/dist/screens.js +275 -6
- package/dist/select-model.js +34 -28
- package/dist/text-diff.d.ts +1 -0
- package/dist/text-diff.js +106 -74
- package/dist/theme.js +34 -40
- package/package.json +56 -7
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { CollectionPanel, CollectionProblem } from "./CollectionGate.js";
|
|
4
|
+
import { CollectionSwitcher } from "./CollectionSwitcher.js";
|
|
5
|
+
/** Render a shared collection component into an element, for apps that don't render with React. */
|
|
6
|
+
export function mountCollectionUi(element, options) {
|
|
7
|
+
const root = createRoot(element);
|
|
8
|
+
let current = options;
|
|
9
|
+
const render = () => root.render(_jsx(Part, {
|
|
10
|
+
...current
|
|
11
|
+
}));
|
|
12
|
+
render();
|
|
13
|
+
return {
|
|
14
|
+
update(next) {
|
|
15
|
+
current = {
|
|
16
|
+
...current,
|
|
17
|
+
...next
|
|
18
|
+
};
|
|
19
|
+
render();
|
|
20
|
+
},
|
|
21
|
+
unmount() {
|
|
22
|
+
root.unmount();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function Part({
|
|
27
|
+
part,
|
|
28
|
+
variant,
|
|
29
|
+
icon,
|
|
30
|
+
autoStart,
|
|
31
|
+
...options
|
|
32
|
+
}) {
|
|
33
|
+
switch (part) {
|
|
34
|
+
case "panel":
|
|
35
|
+
return _jsx(CollectionPanel, {
|
|
36
|
+
...options,
|
|
37
|
+
...(autoStart === undefined ? {} : {
|
|
38
|
+
autoStart
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
case "switcher":
|
|
42
|
+
return _jsx(CollectionSwitcher, {
|
|
43
|
+
...options,
|
|
44
|
+
...(variant ? {
|
|
45
|
+
variant
|
|
46
|
+
} : {}),
|
|
47
|
+
...(icon === undefined ? {} : {
|
|
48
|
+
icon
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
case "problem":
|
|
52
|
+
return _jsx(CollectionProblem, {
|
|
53
|
+
...options
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type JSX, type ReactNode } from "react";
|
|
2
|
+
import type { CollectionNotice, CollectionRow } from "./model.js";
|
|
3
|
+
import type { CollectionSectionItem, CollectionUi, DeleteCollectionOutcome } from "./use-collection-ui.js";
|
|
4
|
+
/** Which row dialog is open, shared by the gate and the switcher. `onClose` hears a delete's outcome. */
|
|
5
|
+
export type RowDialog = {
|
|
6
|
+
readonly kind: "forget" | "delete";
|
|
7
|
+
readonly row: Pick<CollectionRow, "id" | "name">;
|
|
8
|
+
readonly onClose?: ((outcome: DeleteCollectionOutcome) => void) | undefined;
|
|
9
|
+
} | null;
|
|
10
|
+
export type ButtonTone = "primary" | "secondary" | "quiet" | "danger";
|
|
11
|
+
/**
|
|
12
|
+
* A text button. While another action runs it stays focusable but inert
|
|
13
|
+
* (`aria-disabled`), and the running one shows its progress label.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ActionButton({ tone, inert, progress, onClick, children, className, ...rest }: {
|
|
16
|
+
readonly tone?: ButtonTone;
|
|
17
|
+
readonly inert?: boolean;
|
|
18
|
+
/** Shown instead of the label while this button's own action runs. */
|
|
19
|
+
readonly progress?: string | null | undefined;
|
|
20
|
+
readonly onClick: () => void;
|
|
21
|
+
readonly children: ReactNode;
|
|
22
|
+
readonly className?: string;
|
|
23
|
+
readonly "aria-describedby"?: string | undefined;
|
|
24
|
+
}): JSX.Element;
|
|
25
|
+
/** One problem with its own fix: SDK title and message, then the SDK action as a button inside the block. */
|
|
26
|
+
export declare function NoticeBlock({ notice, ui, onSetUp, variant }: {
|
|
27
|
+
readonly notice: CollectionNotice;
|
|
28
|
+
readonly ui: CollectionUi;
|
|
29
|
+
readonly onSetUp?: (() => void) | undefined;
|
|
30
|
+
readonly variant?: "block" | "banner" | "inline";
|
|
31
|
+
}): JSX.Element;
|
|
32
|
+
/** Initials on a hue that stays the same for a collection across sessions. */
|
|
33
|
+
export declare function CollectionMark({ row }: {
|
|
34
|
+
readonly row: Pick<CollectionRow, "id" | "name">;
|
|
35
|
+
}): JSX.Element;
|
|
36
|
+
/** The name (wrapping, never truncated), the app's optional second line, then one line of role and availability. */
|
|
37
|
+
export declare function RowCopy({ row, ui, detailId, current }: {
|
|
38
|
+
readonly row: CollectionRow;
|
|
39
|
+
readonly ui: CollectionUi;
|
|
40
|
+
readonly detailId?: string;
|
|
41
|
+
/** Overrides `row.current` (an app section item is open instead). */
|
|
42
|
+
readonly current?: boolean;
|
|
43
|
+
}): JSX.Element;
|
|
44
|
+
/** Marks a Private collection: a visible label, read with the row's name. */
|
|
45
|
+
export declare function PrivateMark({ ui }: {
|
|
46
|
+
readonly ui: CollectionUi;
|
|
47
|
+
}): JSX.Element;
|
|
48
|
+
/** An app section item: its name and optional second line. */
|
|
49
|
+
export declare function ItemCopy({ item, ui }: {
|
|
50
|
+
readonly item: CollectionSectionItem;
|
|
51
|
+
readonly ui: CollectionUi;
|
|
52
|
+
}): JSX.Element;
|
|
53
|
+
/** "Remove from this list" and, for owners when allowed, "Delete collection…". */
|
|
54
|
+
export declare function RowMore({ row, ui, onDialog, tabbable }: {
|
|
55
|
+
readonly row: CollectionRow;
|
|
56
|
+
readonly ui: CollectionUi;
|
|
57
|
+
readonly onDialog: (dialog: NonNullable<RowDialog>) => void;
|
|
58
|
+
readonly tabbable?: boolean;
|
|
59
|
+
}): JSX.Element;
|
|
60
|
+
/** An app section item's own actions, in the same row menu as an SDK collection's. */
|
|
61
|
+
export declare function ItemMore({ item, ui, onDialog, tabbable }: {
|
|
62
|
+
readonly item: CollectionSectionItem;
|
|
63
|
+
readonly ui: CollectionUi;
|
|
64
|
+
readonly onDialog: (dialog: NonNullable<RowDialog>) => void;
|
|
65
|
+
readonly tabbable?: boolean;
|
|
66
|
+
}): JSX.Element | null;
|
|
67
|
+
/** The app's actions, Choose a different collection, Manage access, the account and Sign out: never under an error. */
|
|
68
|
+
export declare function GateFooter({ ui, chooseAnother, extra }: {
|
|
69
|
+
readonly ui: CollectionUi;
|
|
70
|
+
readonly chooseAnother: boolean;
|
|
71
|
+
readonly extra?: ReactNode;
|
|
72
|
+
}): JSX.Element | null;
|
|
73
|
+
export declare function AccountLine({ ui }: {
|
|
74
|
+
readonly ui: CollectionUi;
|
|
75
|
+
}): JSX.Element;
|
|
76
|
+
/** A native modal <dialog>: the browser traps focus and makes the page inert; Escape cancels. */
|
|
77
|
+
export declare function CollectionDialog({ title, alert, busy, onClose, children, footer }: {
|
|
78
|
+
readonly title: string;
|
|
79
|
+
readonly alert?: boolean;
|
|
80
|
+
readonly busy?: boolean;
|
|
81
|
+
readonly onClose: () => void;
|
|
82
|
+
readonly children: ReactNode;
|
|
83
|
+
readonly footer: ReactNode;
|
|
84
|
+
}): JSX.Element;
|
|
85
|
+
export declare function initials(name: string): string;
|
|
86
|
+
export declare function hueOf(id: string): number;
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { moveMenuFocus, useMenuPopover } from "../popover.js";
|
|
4
|
+
/**
|
|
5
|
+
* A text button. While another action runs it stays focusable but inert
|
|
6
|
+
* (`aria-disabled`), and the running one shows its progress label.
|
|
7
|
+
*/
|
|
8
|
+
export function ActionButton({
|
|
9
|
+
tone = "secondary",
|
|
10
|
+
inert = false,
|
|
11
|
+
progress,
|
|
12
|
+
onClick,
|
|
13
|
+
children,
|
|
14
|
+
className,
|
|
15
|
+
...rest
|
|
16
|
+
}) {
|
|
17
|
+
const label = progress ?? children;
|
|
18
|
+
return _jsx("button", {
|
|
19
|
+
type: "button",
|
|
20
|
+
className: ["mdbase-collections-button", `is-${tone}`, className].filter(Boolean).join(" "),
|
|
21
|
+
"aria-disabled": inert || progress ? true : undefined,
|
|
22
|
+
onClick: () => {
|
|
23
|
+
if (!inert && !progress) onClick();
|
|
24
|
+
},
|
|
25
|
+
...rest,
|
|
26
|
+
children: label
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** One problem with its own fix: SDK title and message, then the SDK action as a button inside the block. */
|
|
30
|
+
export function NoticeBlock({
|
|
31
|
+
notice,
|
|
32
|
+
ui,
|
|
33
|
+
onSetUp,
|
|
34
|
+
variant = "block"
|
|
35
|
+
}) {
|
|
36
|
+
const control = notice.control && (notice.control.kind !== "set_up" || onSetUp) ? notice.control : null;
|
|
37
|
+
const urgent = !notice.transient && notice.tone !== "info";
|
|
38
|
+
return _jsxs("div", {
|
|
39
|
+
className: `mdbase-collections-notice is-${variant}`,
|
|
40
|
+
"data-tone": notice.tone,
|
|
41
|
+
role: urgent ? "alert" : "status",
|
|
42
|
+
children: [_jsxs("div", {
|
|
43
|
+
className: "mdbase-collections-notice-copy",
|
|
44
|
+
children: [notice.title && _jsx("p", {
|
|
45
|
+
className: "mdbase-collections-notice-title",
|
|
46
|
+
children: notice.title
|
|
47
|
+
}), _jsx("p", {
|
|
48
|
+
className: "mdbase-collections-notice-message",
|
|
49
|
+
children: notice.message
|
|
50
|
+
})]
|
|
51
|
+
}), control && _jsx("div", {
|
|
52
|
+
className: "mdbase-collections-notice-actions",
|
|
53
|
+
children: _jsx(ControlButton, {
|
|
54
|
+
control: control,
|
|
55
|
+
ui: ui,
|
|
56
|
+
onSetUp: onSetUp
|
|
57
|
+
})
|
|
58
|
+
})]
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function ControlButton({
|
|
62
|
+
control,
|
|
63
|
+
ui,
|
|
64
|
+
onSetUp
|
|
65
|
+
}) {
|
|
66
|
+
if (control.kind === "link") {
|
|
67
|
+
return _jsx("a", {
|
|
68
|
+
className: "mdbase-collections-button is-primary",
|
|
69
|
+
href: control.href,
|
|
70
|
+
target: "_blank",
|
|
71
|
+
rel: "noopener noreferrer",
|
|
72
|
+
children: control.label
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return _jsx(ActionButton, {
|
|
76
|
+
tone: "primary",
|
|
77
|
+
inert: ui.busy !== null,
|
|
78
|
+
onClick: () => ui.actions.perform(control, onSetUp),
|
|
79
|
+
children: control.label
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Initials on a hue that stays the same for a collection across sessions. */
|
|
83
|
+
export function CollectionMark({
|
|
84
|
+
row
|
|
85
|
+
}) {
|
|
86
|
+
return _jsx("span", {
|
|
87
|
+
className: "mdbase-collections-mark",
|
|
88
|
+
"aria-hidden": "true",
|
|
89
|
+
style: {
|
|
90
|
+
"--mdbase-collections-hue": hueOf(row.id)
|
|
91
|
+
},
|
|
92
|
+
children: initials(row.name)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** The name (wrapping, never truncated), the app's optional second line, then one line of role and availability. */
|
|
96
|
+
export function RowCopy({
|
|
97
|
+
row,
|
|
98
|
+
ui,
|
|
99
|
+
detailId,
|
|
100
|
+
current = row.current
|
|
101
|
+
}) {
|
|
102
|
+
const meta = [current ? ui.copy.openNow : null, row.roleLabel, row.status].filter(Boolean).join(" · ");
|
|
103
|
+
const secondary = ui.options.rowDetail?.(row);
|
|
104
|
+
return _jsxs("span", {
|
|
105
|
+
className: "mdbase-collections-row-copy",
|
|
106
|
+
children: [_jsxs("span", {
|
|
107
|
+
className: "mdbase-collections-row-name",
|
|
108
|
+
children: [row.name, row.disambiguator && _jsxs("span", {
|
|
109
|
+
className: "mdbase-collections-row-hint",
|
|
110
|
+
children: [" ", row.disambiguator]
|
|
111
|
+
}), row.private && _jsxs(_Fragment, {
|
|
112
|
+
children: [" ", _jsx(PrivateMark, {
|
|
113
|
+
ui: ui
|
|
114
|
+
})]
|
|
115
|
+
})]
|
|
116
|
+
}), secondary && _jsx("span", {
|
|
117
|
+
className: "mdbase-collections-row-secondary",
|
|
118
|
+
children: secondary
|
|
119
|
+
}), _jsx("span", {
|
|
120
|
+
className: "mdbase-collections-row-meta",
|
|
121
|
+
children: meta
|
|
122
|
+
}), row.statusDetail && _jsx("span", {
|
|
123
|
+
className: "mdbase-collections-row-detail",
|
|
124
|
+
id: detailId,
|
|
125
|
+
children: row.statusDetail
|
|
126
|
+
})]
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** Marks a Private collection: a visible label, read with the row's name. */
|
|
130
|
+
export function PrivateMark({
|
|
131
|
+
ui
|
|
132
|
+
}) {
|
|
133
|
+
return _jsxs("span", {
|
|
134
|
+
className: "mdbase-collections-chip mdbase-collections-private",
|
|
135
|
+
title: ui.copy.privateCollectionHint,
|
|
136
|
+
children: [_jsx("svg", {
|
|
137
|
+
viewBox: "0 0 16 16",
|
|
138
|
+
"aria-hidden": "true",
|
|
139
|
+
children: _jsx("path", {
|
|
140
|
+
d: "M4.5 7.5h7v5.5h-7zM6 7.5V5.5a2 2 0 0 1 4 0v2"
|
|
141
|
+
})
|
|
142
|
+
}), ui.copy.privateCollection]
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** An app section item: its name and optional second line. */
|
|
146
|
+
export function ItemCopy({
|
|
147
|
+
item,
|
|
148
|
+
ui
|
|
149
|
+
}) {
|
|
150
|
+
return _jsxs("span", {
|
|
151
|
+
className: "mdbase-collections-row-copy",
|
|
152
|
+
children: [_jsx("span", {
|
|
153
|
+
className: "mdbase-collections-row-name",
|
|
154
|
+
children: item.name
|
|
155
|
+
}), item.detail && _jsx("span", {
|
|
156
|
+
className: "mdbase-collections-row-secondary",
|
|
157
|
+
children: item.detail
|
|
158
|
+
}), item.current && _jsx("span", {
|
|
159
|
+
className: "mdbase-collections-row-meta",
|
|
160
|
+
children: ui.copy.openNow
|
|
161
|
+
})]
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** "Remove from this list" and, for owners when allowed, "Delete collection…". */
|
|
165
|
+
export function RowMore({
|
|
166
|
+
row,
|
|
167
|
+
ui,
|
|
168
|
+
onDialog,
|
|
169
|
+
tabbable = true
|
|
170
|
+
}) {
|
|
171
|
+
const canDelete = ui.options.allowDelete === true && row.role === "owner";
|
|
172
|
+
const entries = [{
|
|
173
|
+
id: "forget",
|
|
174
|
+
label: ui.copy.removeFromList,
|
|
175
|
+
onSelect: () => onDialog({
|
|
176
|
+
kind: "forget",
|
|
177
|
+
row
|
|
178
|
+
})
|
|
179
|
+
}];
|
|
180
|
+
if (canDelete) entries.push({
|
|
181
|
+
id: "delete",
|
|
182
|
+
label: ui.copy.deleteCollection,
|
|
183
|
+
danger: true,
|
|
184
|
+
onSelect: () => onDialog({
|
|
185
|
+
kind: "delete",
|
|
186
|
+
row
|
|
187
|
+
})
|
|
188
|
+
});
|
|
189
|
+
return _jsx(RowMenuButton, {
|
|
190
|
+
label: ui.say("moreFor", {
|
|
191
|
+
collection: row.name
|
|
192
|
+
}),
|
|
193
|
+
entries: entries,
|
|
194
|
+
tabbable: tabbable
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/** An app section item's own actions, in the same row menu as an SDK collection's. */
|
|
198
|
+
export function ItemMore({
|
|
199
|
+
item,
|
|
200
|
+
ui,
|
|
201
|
+
onDialog,
|
|
202
|
+
tabbable = true
|
|
203
|
+
}) {
|
|
204
|
+
const actions = item.actions ?? [];
|
|
205
|
+
if (actions.length === 0) return null;
|
|
206
|
+
const context = {
|
|
207
|
+
confirmDelete: onClose => onDialog({
|
|
208
|
+
kind: "delete",
|
|
209
|
+
row: item,
|
|
210
|
+
onClose
|
|
211
|
+
})
|
|
212
|
+
};
|
|
213
|
+
const entries = actions.map(action => ({
|
|
214
|
+
id: action.id,
|
|
215
|
+
label: action.label,
|
|
216
|
+
danger: action.danger,
|
|
217
|
+
disabled: action.disabled,
|
|
218
|
+
onSelect: () => action.onSelect(item, context)
|
|
219
|
+
}));
|
|
220
|
+
return _jsx(RowMenuButton, {
|
|
221
|
+
label: ui.say("moreFor", {
|
|
222
|
+
collection: item.name
|
|
223
|
+
}),
|
|
224
|
+
entries: entries,
|
|
225
|
+
tabbable: tabbable,
|
|
226
|
+
refocus: true
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A row's "…" button and its menu. Arrow keys move between enabled entries,
|
|
231
|
+
* Escape closes it and returns focus to the button; with `refocus`, choosing an
|
|
232
|
+
* entry returns focus there too, so a dialog it opens hands focus back to it.
|
|
233
|
+
*/
|
|
234
|
+
function RowMenuButton({
|
|
235
|
+
label,
|
|
236
|
+
entries,
|
|
237
|
+
tabbable,
|
|
238
|
+
refocus = false
|
|
239
|
+
}) {
|
|
240
|
+
const [open, setOpen] = useState(false);
|
|
241
|
+
const trigger = useRef(null);
|
|
242
|
+
const menuId = useId();
|
|
243
|
+
const close = focus => {
|
|
244
|
+
setOpen(false);
|
|
245
|
+
if (focus) trigger.current?.focus();
|
|
246
|
+
};
|
|
247
|
+
return _jsxs(_Fragment, {
|
|
248
|
+
children: [_jsx("button", {
|
|
249
|
+
ref: trigger,
|
|
250
|
+
type: "button",
|
|
251
|
+
className: "mdbase-collections-more",
|
|
252
|
+
"aria-label": label,
|
|
253
|
+
"aria-haspopup": "menu",
|
|
254
|
+
"aria-expanded": open,
|
|
255
|
+
"aria-controls": open ? menuId : undefined,
|
|
256
|
+
tabIndex: tabbable ? 0 : -1,
|
|
257
|
+
onClick: () => setOpen(value => !value),
|
|
258
|
+
children: _jsxs("svg", {
|
|
259
|
+
viewBox: "0 0 16 16",
|
|
260
|
+
"aria-hidden": "true",
|
|
261
|
+
children: [_jsx("circle", {
|
|
262
|
+
cx: "3.5",
|
|
263
|
+
cy: "8",
|
|
264
|
+
r: "1.3"
|
|
265
|
+
}), _jsx("circle", {
|
|
266
|
+
cx: "8",
|
|
267
|
+
cy: "8",
|
|
268
|
+
r: "1.3"
|
|
269
|
+
}), _jsx("circle", {
|
|
270
|
+
cx: "12.5",
|
|
271
|
+
cy: "8",
|
|
272
|
+
r: "1.3"
|
|
273
|
+
})]
|
|
274
|
+
})
|
|
275
|
+
}), open && _jsx(RowMenu, {
|
|
276
|
+
id: menuId,
|
|
277
|
+
triggerRef: trigger,
|
|
278
|
+
onClose: close,
|
|
279
|
+
children: entries.map(entry => _jsx("button", {
|
|
280
|
+
type: "button",
|
|
281
|
+
role: "menuitem",
|
|
282
|
+
disabled: entry.disabled,
|
|
283
|
+
className: entry.danger ? "mdbase-collections-menu-item is-danger" : "mdbase-collections-menu-item",
|
|
284
|
+
onClick: () => {
|
|
285
|
+
if (entry.disabled) return;
|
|
286
|
+
close(refocus);
|
|
287
|
+
entry.onSelect();
|
|
288
|
+
},
|
|
289
|
+
children: entry.label
|
|
290
|
+
}, entry.id))
|
|
291
|
+
})]
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function RowMenu({
|
|
295
|
+
id,
|
|
296
|
+
triggerRef,
|
|
297
|
+
onClose,
|
|
298
|
+
children
|
|
299
|
+
}) {
|
|
300
|
+
const menu = useRef(null);
|
|
301
|
+
useMenuPopover(menu, triggerRef, onClose, {
|
|
302
|
+
align: "end",
|
|
303
|
+
focus: '[role="menuitem"]'
|
|
304
|
+
});
|
|
305
|
+
return _jsx("div", {
|
|
306
|
+
ref: menu,
|
|
307
|
+
id: id,
|
|
308
|
+
className: "mdbase-collections-menu",
|
|
309
|
+
popover: "manual",
|
|
310
|
+
role: "menu",
|
|
311
|
+
tabIndex: -1,
|
|
312
|
+
onKeyDown: event => moveMenuFocus(event, menu.current),
|
|
313
|
+
children: children
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/** The app's actions, Choose a different collection, Manage access, the account and Sign out: never under an error. */
|
|
317
|
+
export function GateFooter({
|
|
318
|
+
ui,
|
|
319
|
+
chooseAnother,
|
|
320
|
+
extra
|
|
321
|
+
}) {
|
|
322
|
+
const {
|
|
323
|
+
view,
|
|
324
|
+
copy,
|
|
325
|
+
busy,
|
|
326
|
+
actions
|
|
327
|
+
} = ui;
|
|
328
|
+
const hasExtra = extra !== undefined && extra !== null && extra !== false;
|
|
329
|
+
if (!view.signedIn && !chooseAnother && !hasExtra) return null;
|
|
330
|
+
return _jsxs("footer", {
|
|
331
|
+
className: "mdbase-collections-footer",
|
|
332
|
+
children: [hasExtra && _jsx("span", {
|
|
333
|
+
className: "mdbase-collections-footer-extra",
|
|
334
|
+
children: extra
|
|
335
|
+
}), chooseAnother && _jsx(ActionButton, {
|
|
336
|
+
tone: "quiet",
|
|
337
|
+
inert: busy !== null,
|
|
338
|
+
progress: busy === "choose_another" ? copy.chooseAnother : null,
|
|
339
|
+
onClick: actions.chooseAnother,
|
|
340
|
+
children: copy.chooseAnother
|
|
341
|
+
}), view.signedIn && view.links && _jsx("a", {
|
|
342
|
+
className: "mdbase-collections-link",
|
|
343
|
+
href: view.links.manageAccess,
|
|
344
|
+
target: "_blank",
|
|
345
|
+
rel: "noopener noreferrer",
|
|
346
|
+
children: copy.manageAccess
|
|
347
|
+
}), view.signedIn && _jsx(AccountLine, {
|
|
348
|
+
ui: ui
|
|
349
|
+
})]
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
export function AccountLine({
|
|
353
|
+
ui
|
|
354
|
+
}) {
|
|
355
|
+
const {
|
|
356
|
+
view,
|
|
357
|
+
copy,
|
|
358
|
+
busy,
|
|
359
|
+
actions,
|
|
360
|
+
say
|
|
361
|
+
} = ui;
|
|
362
|
+
return _jsxs("span", {
|
|
363
|
+
className: "mdbase-collections-account",
|
|
364
|
+
children: [view.account && _jsx("span", {
|
|
365
|
+
className: "mdbase-collections-account-label",
|
|
366
|
+
children: say("signedInAs", {
|
|
367
|
+
account: view.account
|
|
368
|
+
})
|
|
369
|
+
}), _jsx(ActionButton, {
|
|
370
|
+
tone: "quiet",
|
|
371
|
+
inert: busy !== null,
|
|
372
|
+
progress: busy === "sign_out" ? copy.signingOut : null,
|
|
373
|
+
onClick: actions.signOut,
|
|
374
|
+
children: copy.signOut
|
|
375
|
+
})]
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
/** A native modal <dialog>: the browser traps focus and makes the page inert; Escape cancels. */
|
|
379
|
+
export function CollectionDialog({
|
|
380
|
+
title,
|
|
381
|
+
alert = false,
|
|
382
|
+
busy = false,
|
|
383
|
+
onClose,
|
|
384
|
+
children,
|
|
385
|
+
footer
|
|
386
|
+
}) {
|
|
387
|
+
const ref = useRef(null);
|
|
388
|
+
const titleId = useId();
|
|
389
|
+
useLayoutEffect(() => {
|
|
390
|
+
const dialog = ref.current;
|
|
391
|
+
const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
392
|
+
if (dialog && !dialog.open) {
|
|
393
|
+
if (typeof dialog.showModal === "function") dialog.showModal();else dialog.setAttribute("open", "");
|
|
394
|
+
}
|
|
395
|
+
(dialog?.querySelector("[data-autofocus]") ?? dialog?.querySelector("button"))?.focus();
|
|
396
|
+
return () => {
|
|
397
|
+
if (dialog?.open && typeof dialog.close === "function") dialog.close();
|
|
398
|
+
if (previous?.isConnected) previous.focus();
|
|
399
|
+
};
|
|
400
|
+
}, []);
|
|
401
|
+
return _jsxs("dialog", {
|
|
402
|
+
ref: ref,
|
|
403
|
+
className: "mdbase-collections mdbase-collections-dialog",
|
|
404
|
+
role: alert ? "alertdialog" : undefined,
|
|
405
|
+
"aria-labelledby": titleId,
|
|
406
|
+
onKeyDown: event => {
|
|
407
|
+
event.stopPropagation();
|
|
408
|
+
if (event.key === "Escape") {
|
|
409
|
+
event.preventDefault();
|
|
410
|
+
if (!busy) onClose();
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
onCancel: event => {
|
|
414
|
+
event.preventDefault();
|
|
415
|
+
if (!busy) onClose();
|
|
416
|
+
},
|
|
417
|
+
children: [_jsx("h2", {
|
|
418
|
+
id: titleId,
|
|
419
|
+
className: "mdbase-collections-dialog-title",
|
|
420
|
+
children: title
|
|
421
|
+
}), _jsx("div", {
|
|
422
|
+
className: "mdbase-collections-dialog-body",
|
|
423
|
+
children: children
|
|
424
|
+
}), _jsx("div", {
|
|
425
|
+
className: "mdbase-collections-dialog-actions",
|
|
426
|
+
children: footer
|
|
427
|
+
})]
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
export function initials(name) {
|
|
431
|
+
const words = name.replace(/[^\p{L}\p{N}\s]/gu, " ").trim().split(/\s+/u).filter(Boolean);
|
|
432
|
+
const letters = words.slice(0, 2).map(word => String.fromCodePoint(word.codePointAt(0) ?? 63));
|
|
433
|
+
return (letters.join("") || "?").toLocaleUpperCase();
|
|
434
|
+
}
|
|
435
|
+
export function hueOf(id) {
|
|
436
|
+
let hash = 0;
|
|
437
|
+
for (const code of id) hash = (hash * 31 + (code.codePointAt(0) ?? 0)) % 360;
|
|
438
|
+
return hash;
|
|
439
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@mdbase-dev/ui/collections/testing`: for Node test runners that render the
|
|
3
|
+
* collection components without a DOM environment per file.
|
|
4
|
+
*
|
|
5
|
+
* The components' focus and popover code reads the global `window` and
|
|
6
|
+
* `document` (not the rendered element's `ownerDocument`), so the test's
|
|
7
|
+
* window must also be the global one.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Make `window` (a jsdom or happy-dom window) the global window and document
|
|
11
|
+
* for the collection components. Returns a function that restores the
|
|
12
|
+
* previous globals.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* const restore = shareTestDocument(new JSDOM("<!doctype html><body></body>").window);
|
|
16
|
+
* afterAll(restore);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function shareTestDocument(window: object): () => void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@mdbase-dev/ui/collections/testing`: for Node test runners that render the
|
|
3
|
+
* collection components without a DOM environment per file.
|
|
4
|
+
*
|
|
5
|
+
* The components' focus and popover code reads the global `window` and
|
|
6
|
+
* `document` (not the rendered element's `ownerDocument`), so the test's
|
|
7
|
+
* window must also be the global one.
|
|
8
|
+
*/
|
|
9
|
+
const shared = ["window", "document", "navigator", "Node", "Element", "HTMLElement", "HTMLButtonElement", "HTMLDialogElement", "HTMLInputElement", "Event", "FocusEvent", "KeyboardEvent", "MouseEvent", "PointerEvent", "getComputedStyle", "matchMedia", "requestAnimationFrame", "cancelAnimationFrame"];
|
|
10
|
+
/**
|
|
11
|
+
* Make `window` (a jsdom or happy-dom window) the global window and document
|
|
12
|
+
* for the collection components. Returns a function that restores the
|
|
13
|
+
* previous globals.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const restore = shareTestDocument(new JSDOM("<!doctype html><body></body>").window);
|
|
17
|
+
* afterAll(restore);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function shareTestDocument(window) {
|
|
21
|
+
const global = globalThis;
|
|
22
|
+
const source = window;
|
|
23
|
+
const previous = new Map();
|
|
24
|
+
for (const name of shared) {
|
|
25
|
+
const value = name === "window" ? window : source[name];
|
|
26
|
+
if (value === undefined) continue;
|
|
27
|
+
previous.set(name, Object.getOwnPropertyDescriptor(globalThis, name));
|
|
28
|
+
Object.defineProperty(globalThis, name, {
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true,
|
|
31
|
+
value: typeof value === "function" && /^[a-z]/.test(name) ? value.bind(window) : value
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return () => {
|
|
35
|
+
for (const [name, descriptor] of previous) {
|
|
36
|
+
if (descriptor) Object.defineProperty(globalThis, name, descriptor);else delete global[name];
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|