@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.
Files changed (74) hide show
  1. package/README.md +168 -0
  2. package/css/collections.css +978 -0
  3. package/css/controls.css +33 -1
  4. package/css/screens.css +44 -2
  5. package/dist/AppSwitcher.js +187 -33
  6. package/dist/Checkbox.d.ts +14 -0
  7. package/dist/Checkbox.js +32 -0
  8. package/dist/CollectionModeControl.d.ts +11 -0
  9. package/dist/CollectionModeControl.js +290 -0
  10. package/dist/CommandPalette.d.ts +4 -2
  11. package/dist/CommandPalette.js +275 -103
  12. package/dist/Dialog.js +72 -25
  13. package/dist/Feedback.js +763 -229
  14. package/dist/FeedbackMarkup.d.ts +1 -1
  15. package/dist/FeedbackMarkup.js +326 -125
  16. package/dist/FeedbackVerification.js +115 -68
  17. package/dist/SaveNotice.js +71 -28
  18. package/dist/Select.js +183 -135
  19. package/dist/SelectList.js +59 -14
  20. package/dist/ThemeSelect.js +55 -4
  21. package/dist/apps.d.ts +1 -0
  22. package/dist/apps.js +50 -29
  23. package/dist/brand.js +251 -68
  24. package/dist/codemirror.js +116 -87
  25. package/dist/collections/CollectionGate.d.ts +52 -0
  26. package/dist/collections/CollectionGate.js +456 -0
  27. package/dist/collections/CollectionSaveWarning.d.ts +10 -0
  28. package/dist/collections/CollectionSaveWarning.js +28 -0
  29. package/dist/collections/CollectionSwitcher.d.ts +51 -0
  30. package/dist/collections/CollectionSwitcher.js +598 -0
  31. package/dist/collections/SetupReview.d.ts +9 -0
  32. package/dist/collections/SetupReview.js +216 -0
  33. package/dist/collections/copy.d.ts +100 -0
  34. package/dist/collections/copy.js +937 -0
  35. package/dist/collections/dialogs.d.ts +27 -0
  36. package/dist/collections/dialogs.js +169 -0
  37. package/dist/collections/model.d.ts +146 -0
  38. package/dist/collections/model.js +337 -0
  39. package/dist/collections/mount.d.ts +19 -0
  40. package/dist/collections/mount.js +56 -0
  41. package/dist/collections/parts.d.ts +86 -0
  42. package/dist/collections/parts.js +439 -0
  43. package/dist/collections/testing.d.ts +19 -0
  44. package/dist/collections/testing.js +39 -0
  45. package/dist/collections/use-collection-ui.d.ts +140 -0
  46. package/dist/collections/use-collection-ui.js +236 -0
  47. package/dist/collections.d.ts +19 -0
  48. package/dist/collections.js +13 -0
  49. package/dist/feedback-data.d.ts +13 -12
  50. package/dist/feedback-data.js +365 -168
  51. package/dist/flock.d.ts +63 -0
  52. package/dist/flock.js +140 -0
  53. package/dist/i18n.d.ts +6 -0
  54. package/dist/i18n.js +71 -0
  55. package/dist/locales/de/messages.json +1 -0
  56. package/dist/locales/en/messages.json +1 -0
  57. package/dist/locales/es/messages.json +1 -0
  58. package/dist/locales/fr/messages.json +1 -0
  59. package/dist/locales/it/messages.json +1 -0
  60. package/dist/locales/ja/messages.json +1 -0
  61. package/dist/locales/ko/messages.json +1 -0
  62. package/dist/locales/pt/messages.json +1 -0
  63. package/dist/locales/ru/messages.json +1 -0
  64. package/dist/locales/zh/messages.json +1 -0
  65. package/dist/mark-activity.js +82 -75
  66. package/dist/permission-selection.js +27 -17
  67. package/dist/popover.js +138 -130
  68. package/dist/screens.d.ts +20 -3
  69. package/dist/screens.js +275 -6
  70. package/dist/select-model.js +34 -28
  71. package/dist/text-diff.d.ts +1 -0
  72. package/dist/text-diff.js +106 -74
  73. package/dist/theme.js +34 -40
  74. package/package.json +56 -7
@@ -0,0 +1,27 @@
1
+ import { type JSX } from "react";
2
+ import type { CollectionRow } from "./model.js";
3
+ import { type RowDialog } from "./parts.js";
4
+ import type { CollectionUi, DeleteCollectionOutcome } from "./use-collection-ui.js";
5
+ type Target = Pick<CollectionRow, "id" | "name">;
6
+ /** Hide a collection on this device. Nothing is deleted and access stays. */
7
+ export declare function ForgetCollectionDialog({ ui, collection, onClose }: {
8
+ readonly ui: CollectionUi;
9
+ readonly collection: Target;
10
+ readonly onClose: () => void;
11
+ }): JSX.Element;
12
+ /** Owner only: type the name to delete a collection for everyone. The SDK and server check both again. */
13
+ export declare function DeleteCollectionDialog({ ui, collection, onClose }: {
14
+ readonly ui: CollectionUi;
15
+ readonly collection: Target;
16
+ readonly onClose: (outcome: DeleteCollectionOutcome) => void;
17
+ }): JSX.Element;
18
+ /** Sign-out found changes that haven't reached mdbase: wait, or sign out and lose them here. */
19
+ export declare function SignOutDialog({ ui }: {
20
+ readonly ui: CollectionUi;
21
+ }): JSX.Element | null;
22
+ export declare function RowDialogs({ ui, dialog, onClose }: {
23
+ readonly ui: CollectionUi;
24
+ readonly dialog: RowDialog;
25
+ readonly onClose: () => void;
26
+ }): JSX.Element | null;
27
+ export {};
@@ -0,0 +1,169 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describeError } from "@mdbase-dev/sdk/describe";
3
+ import { useId, useState } from "react";
4
+ import { ActionButton, CollectionDialog } from "./parts.js";
5
+ /** Hide a collection on this device. Nothing is deleted and access stays. */
6
+ export function ForgetCollectionDialog({
7
+ ui,
8
+ collection,
9
+ onClose
10
+ }) {
11
+ const [busy, setBusy] = useState(false);
12
+ const [error, setError] = useState(null);
13
+ const forget = () => {
14
+ setBusy(true);
15
+ setError(null);
16
+ ui.options.mdbase.forget(collection.id).then(onClose, cause => {
17
+ setBusy(false);
18
+ setError(describeError(cause)?.message ?? null);
19
+ });
20
+ };
21
+ return _jsxs(CollectionDialog, {
22
+ title: ui.say("forgetTitle", {
23
+ collection: collection.name
24
+ }),
25
+ busy: busy,
26
+ onClose: onClose,
27
+ footer: _jsxs(_Fragment, {
28
+ children: [_jsx(ActionButton, {
29
+ tone: "quiet",
30
+ inert: busy,
31
+ onClick: onClose,
32
+ children: ui.copy.cancel
33
+ }), _jsx(ActionButton, {
34
+ tone: "primary",
35
+ progress: busy ? ui.copy.forgetConfirm : null,
36
+ onClick: forget,
37
+ children: ui.copy.forgetConfirm
38
+ })]
39
+ }),
40
+ children: [_jsx("p", {
41
+ children: ui.say("forgetBody")
42
+ }), error && _jsx("p", {
43
+ className: "mdbase-collections-dialog-error",
44
+ role: "alert",
45
+ children: error
46
+ })]
47
+ });
48
+ }
49
+ /** Owner only: type the name to delete a collection for everyone. The SDK and server check both again. */
50
+ export function DeleteCollectionDialog({
51
+ ui,
52
+ collection,
53
+ onClose
54
+ }) {
55
+ const id = useId();
56
+ const [typed, setTyped] = useState("");
57
+ const [busy, setBusy] = useState(false);
58
+ const [error, setError] = useState(null);
59
+ const [failed, setFailed] = useState(false);
60
+ const matches = typed.trim() === collection.name.trim();
61
+ const dismiss = () => onClose(failed ? "failed" : "cancelled");
62
+ const remove = () => {
63
+ if (!matches || busy) return;
64
+ setBusy(true);
65
+ setError(null);
66
+ ui.options.mdbase.deleteCollection(collection.id, {
67
+ confirmName: typed
68
+ }).then(() => onClose("deleted"), cause => {
69
+ setBusy(false);
70
+ setFailed(true);
71
+ setError(describeError(cause)?.message ?? null);
72
+ });
73
+ };
74
+ return _jsxs(CollectionDialog, {
75
+ alert: true,
76
+ title: ui.say("deleteTitle", {
77
+ collection: collection.name
78
+ }),
79
+ busy: busy,
80
+ onClose: dismiss,
81
+ footer: _jsxs(_Fragment, {
82
+ children: [_jsx(ActionButton, {
83
+ tone: "quiet",
84
+ inert: busy,
85
+ onClick: dismiss,
86
+ children: ui.copy.cancel
87
+ }), _jsx(ActionButton, {
88
+ tone: "danger",
89
+ inert: !matches,
90
+ progress: busy ? ui.copy.deleting : null,
91
+ onClick: remove,
92
+ children: ui.copy.deleteConfirm
93
+ })]
94
+ }),
95
+ children: [_jsx("p", {
96
+ children: ui.copy.deleteBody
97
+ }), _jsxs("form", {
98
+ onSubmit: event => {
99
+ event.preventDefault();
100
+ remove();
101
+ },
102
+ children: [_jsx("label", {
103
+ className: "mdbase-collections-field-label",
104
+ htmlFor: id,
105
+ children: ui.say("deleteTypeName", {
106
+ collection: collection.name
107
+ })
108
+ }), _jsx("input", {
109
+ id: id,
110
+ className: "mdbase-collections-field",
111
+ value: typed,
112
+ autoComplete: "off",
113
+ spellCheck: false,
114
+ disabled: busy,
115
+ onChange: event => setTyped(event.target.value),
116
+ "data-autofocus": true
117
+ })]
118
+ }), error && _jsx("p", {
119
+ className: "mdbase-collections-dialog-error",
120
+ role: "alert",
121
+ children: error
122
+ })]
123
+ });
124
+ }
125
+ /** Sign-out found changes that haven't reached mdbase: wait, or sign out and lose them here. */
126
+ export function SignOutDialog({
127
+ ui
128
+ }) {
129
+ const problem = ui.signOutPrompt;
130
+ if (!problem) return null;
131
+ return _jsx(CollectionDialog, {
132
+ alert: true,
133
+ title: problem.title,
134
+ onClose: ui.actions.dismissSignOut,
135
+ footer: _jsxs(_Fragment, {
136
+ children: [_jsx(ActionButton, {
137
+ tone: "primary",
138
+ onClick: ui.actions.dismissSignOut,
139
+ children: problem.actionLabel ?? ui.copy.cancel
140
+ }), _jsx(ActionButton, {
141
+ tone: "danger",
142
+ onClick: ui.actions.confirmSignOut,
143
+ children: ui.copy.signOutAnyway
144
+ })]
145
+ }),
146
+ children: _jsx("p", {
147
+ children: problem.message
148
+ })
149
+ });
150
+ }
151
+ export function RowDialogs({
152
+ ui,
153
+ dialog,
154
+ onClose
155
+ }) {
156
+ if (!dialog) return null;
157
+ return dialog.kind === "forget" ? _jsx(ForgetCollectionDialog, {
158
+ ui: ui,
159
+ collection: dialog.row,
160
+ onClose: onClose
161
+ }) : _jsx(DeleteCollectionDialog, {
162
+ ui: ui,
163
+ collection: dialog.row,
164
+ onClose: outcome => {
165
+ onClose();
166
+ dialog.onClose?.(outcome);
167
+ }
168
+ });
169
+ }
@@ -0,0 +1,146 @@
1
+ import "../i18n.js";
2
+ /**
3
+ * The headless collection view: one pure mapping from the SDK snapshot (whose
4
+ * `phase` already says what state the app is in) to what the shared screens
5
+ * show. No React, DOM or IO; the SDK owns state and problem copy, this module
6
+ * only chooses headings, lists and which SDK call each control makes.
7
+ */
8
+ import type { MdbaseLinks, MdbasePhase, MdbaseProblem, MdbaseSnapshot } from "@mdbase-dev/sdk";
9
+ import { type CollectionCopy } from "./copy.js";
10
+ export type CollectionRole = "owner" | "editor" | "viewer";
11
+ /** One approved collection, ready to render as a list row. */
12
+ export interface CollectionRow {
13
+ readonly id: string;
14
+ readonly name: string;
15
+ /** Set when another listed collection has the same name. */
16
+ readonly disambiguator: string | null;
17
+ readonly role: CollectionRole;
18
+ readonly roleLabel: string;
19
+ /** A Private collection: kept on the person's computers and reached through
20
+ * the relay (the SDK entry's `kind: "private"`), not a hosted one. */
21
+ readonly private: boolean;
22
+ /** The collection that is open, or opening. */
23
+ readonly current: boolean;
24
+ readonly available: boolean;
25
+ /** Availability in a few words ("Your computer isn't online"), or null when available. */
26
+ readonly status: string | null;
27
+ /** The longer explanation for an unavailable collection. */
28
+ readonly statusDetail: string | null;
29
+ /** The SDK's entry, for app-derived details (see `rowDetail`). */
30
+ readonly entry: MdbaseSnapshot["collections"][number];
31
+ }
32
+ /** What a problem's own button does. Every SDK action maps to one control or none. */
33
+ export type ProblemControl = {
34
+ readonly kind: "sign_in";
35
+ readonly label: string;
36
+ } | {
37
+ readonly kind: "approve";
38
+ readonly label: string;
39
+ readonly collectionId: string | null;
40
+ } | {
41
+ readonly kind: "choose_another";
42
+ readonly label: string;
43
+ } | {
44
+ readonly kind: "set_up";
45
+ readonly label: string;
46
+ } | {
47
+ readonly kind: "assess";
48
+ readonly label: string;
49
+ } | {
50
+ readonly kind: "start";
51
+ readonly label: string;
52
+ } | {
53
+ readonly kind: "reload";
54
+ readonly label: string;
55
+ } | {
56
+ readonly kind: "link";
57
+ readonly label: string;
58
+ readonly href: string;
59
+ };
60
+ /** A described problem plus its control. `title`/`message` are the SDK's copy. */
61
+ export interface CollectionNotice {
62
+ readonly key: string;
63
+ readonly tone: "info" | "warning" | "error";
64
+ readonly title: string | null;
65
+ readonly message: string;
66
+ readonly control: ProblemControl | null;
67
+ /** The SDK clears it by itself (busy, migrating): show it quietly. */
68
+ readonly transient: boolean;
69
+ }
70
+ interface Common {
71
+ /** The gate's heading for this phase (never an SDK problem title). */
72
+ readonly heading: string;
73
+ readonly lead: string | null;
74
+ /** Signed in to mdbase on this device. */
75
+ readonly signedIn: boolean;
76
+ /** "Signed in as …" label, or null when not signed in (or the account has no label). */
77
+ readonly account: string | null;
78
+ /** Every approved collection, current first then by name. */
79
+ readonly collections: readonly CollectionRow[];
80
+ readonly current: CollectionRow | null;
81
+ readonly links: MdbaseLinks | null;
82
+ /** True when "Choose a different collection" leaves something open or requested. */
83
+ readonly canChooseAnother: boolean;
84
+ }
85
+ export type CollectionView = Common & ({
86
+ readonly kind: "starting";
87
+ } | {
88
+ readonly kind: "signed_out";
89
+ readonly notice: CollectionNotice | null;
90
+ } | {
91
+ readonly kind: "authorizing";
92
+ readonly purpose: "sign_in" | "add" | "approve";
93
+ readonly collectionId: string | null;
94
+ } | {
95
+ readonly kind: "choosing";
96
+ readonly notice: CollectionNotice | null;
97
+ } | {
98
+ readonly kind: "opening";
99
+ readonly collectionId: string;
100
+ readonly slow: boolean;
101
+ } | {
102
+ readonly kind: "needs_approval";
103
+ readonly collectionId: string;
104
+ readonly declined: boolean;
105
+ } | {
106
+ readonly kind: "needs_setup";
107
+ readonly collectionId: string;
108
+ } | {
109
+ readonly kind: "problem";
110
+ readonly collectionId: string | null;
111
+ readonly notice: CollectionNotice;
112
+ } | {
113
+ readonly kind: "ready";
114
+ readonly collectionId: string;
115
+ readonly phase: Extract<MdbasePhase, {
116
+ kind: "ready";
117
+ }>;
118
+ readonly notice: CollectionNotice | null;
119
+ } | {
120
+ readonly kind: "unavailable";
121
+ readonly notice: CollectionNotice;
122
+ } | {
123
+ readonly kind: "closed";
124
+ });
125
+ export type CollectionViewKind = CollectionView["kind"];
126
+ export interface CollectionViewOptions {
127
+ /** The app's name, as people know it ("TaskNotes"). */
128
+ readonly appName: string;
129
+ readonly copy?: Partial<CollectionCopy> | undefined;
130
+ }
131
+ type Snapshot = Pick<MdbaseSnapshot, "phase" | "collections" | "signIn" | "selection" | "links" | "readiness" | "status"> & Partial<Pick<MdbaseSnapshot, "signOutResult" | "selectedCollectionId">>;
132
+ /** Map an SDK snapshot to the screen to show. */
133
+ export declare function collectionView(snapshot: Snapshot, options: CollectionViewOptions): CollectionView;
134
+ /** The SDK's description of a failed action, as an inline notice (no control: the person clicks again). */
135
+ export declare function actionNotice(problem: MdbaseProblem | null): CollectionNotice | null;
136
+ /** One control per SDK action; `wait`, `none` and actions with nowhere to go get none. */
137
+ export declare function problemControl(problem: Pick<MdbaseProblem, "action" | "actionLabel" | "updateUrl">, context: {
138
+ collectionId: string | null;
139
+ signedIn: boolean;
140
+ open: boolean;
141
+ }): ProblemControl | null;
142
+ /** Rows for the approved collections: current first, then by name; equal names get an ID hint. */
143
+ export declare function collectionRows(collections: MdbaseSnapshot["collections"], currentId: string | null, copy?: CollectionCopy): CollectionRow[];
144
+ /** "someone@example.com", or "octocat on GitHub". */
145
+ export declare function accountLabel(signIn: MdbaseSnapshot["signIn"]): string | null;
146
+ export {};
@@ -0,0 +1,337 @@
1
+ import { i18n as _i18n } from "@lingui/core";
2
+ import "../i18n.js";
3
+ import { describeError } from "@mdbase-dev/sdk/describe";
4
+ import { defaultCollectionCopy, fill } from "./copy.js";
5
+ /** Map an SDK snapshot to the screen to show. */
6
+ export function collectionView(snapshot, options) {
7
+ const copy = {
8
+ ...defaultCollectionCopy,
9
+ ...options.copy
10
+ };
11
+ const app = options.appName;
12
+ const phase = snapshot.phase;
13
+ const currentId = currentCollectionId(phase);
14
+ const collections = collectionRows(snapshot.collections, currentId, copy);
15
+ const current = collections.find(row => row.current) ?? null;
16
+ const nameOf = (id, fallback = null) => id && collections.find(row => row.id === id)?.name || fallback;
17
+ const signedIn = snapshot.signIn?.state === "paired";
18
+ const say = (template, collection) => fill(template, {
19
+ app,
20
+ collection: collection ?? ""
21
+ });
22
+ const common = {
23
+ signedIn,
24
+ account: signedIn ? accountLabel(snapshot.signIn) : null,
25
+ collections,
26
+ current,
27
+ links: snapshot.links ?? null,
28
+ canChooseAnother: signedIn && (currentId !== null || (snapshot.selection.requested.collectionId ?? null) !== null)
29
+ };
30
+ switch (phase.kind) {
31
+ case "starting":
32
+ return {
33
+ ...common,
34
+ kind: "starting",
35
+ heading: say(copy.startingHeading),
36
+ lead: null
37
+ };
38
+ case "signed_out":
39
+ {
40
+ const signedOut = snapshot.signOutResult ?? null;
41
+ const notice = phase.notice ? quietNotice(phase.notice) : signedOut ? {
42
+ key: "signed_out",
43
+ tone: "info",
44
+ title: null,
45
+ control: null,
46
+ transient: false,
47
+ message: signedOut.server === "incomplete" ? `${say(copy.signedOutNotice)} ${copy.signedOutIncomplete}` : say(copy.signedOutNotice)
48
+ } : null;
49
+ return {
50
+ ...common,
51
+ kind: "signed_out",
52
+ heading: say(copy.signedOutHeading),
53
+ lead: say(copy.signedOutLead),
54
+ notice
55
+ };
56
+ }
57
+ case "authorizing":
58
+ {
59
+ const name = nameOf(phase.collectionId, snapshot.selection.requested.displayName) ?? copy.thisCollection;
60
+ const lead = phase.purpose === "sign_in" ? copy.authorizingSignIn : phase.purpose === "add" ? copy.authorizingAdd : copy.authorizingApprove;
61
+ return {
62
+ ...common,
63
+ kind: "authorizing",
64
+ purpose: phase.purpose,
65
+ collectionId: phase.collectionId,
66
+ heading: say(copy.authorizingHeading),
67
+ lead: say(lead, name)
68
+ };
69
+ }
70
+ case "choosing":
71
+ {
72
+ const empty = collections.length === 0;
73
+ return {
74
+ ...common,
75
+ kind: "choosing",
76
+ notice: approvalNotice(phase.lastApproval, copy),
77
+ heading: say(empty ? copy.emptyHeading : copy.chooseHeading),
78
+ lead: empty ? say(copy.emptyLead) : null
79
+ };
80
+ }
81
+ case "opening":
82
+ {
83
+ const name = nameOf(phase.collectionId, phase.displayName);
84
+ // Older SDKs omit progress. Only fixed SDK stages select localized copy;
85
+ // never render a remote string or infer progress from elapsed time.
86
+ const progressCopy = {
87
+ joining: copy.openingJoining,
88
+ opening_store: copy.openingStore,
89
+ connecting_log: copy.openingLog,
90
+ syncing: copy.openingSyncing,
91
+ loading_snapshot: copy.openingSnapshot,
92
+ replaying: copy.openingReplaying,
93
+ waiting_for_key: copy.openingKey,
94
+ verifying_read: copy.openingRead,
95
+ checking_setup: copy.openingSetup
96
+ };
97
+ const progress = "progress" in phase ? phase.progress : null;
98
+ const detail = typeof progress === "string" && Object.hasOwn(progressCopy, progress) ? progressCopy[progress] : null;
99
+ return {
100
+ ...common,
101
+ kind: "opening",
102
+ collectionId: phase.collectionId,
103
+ slow: phase.slow,
104
+ heading: name ? say(copy.openingHeading, name) : say(copy.openingHeadingUnnamed),
105
+ lead: detail ? [say(detail), ...(phase.slow ? [say(copy.openingSlow)] : [])].join("\n") : phase.slow ? say(copy.openingSlow) : null
106
+ };
107
+ }
108
+ case "needs_approval":
109
+ {
110
+ const name = nameOf(phase.collectionId, phase.displayName);
111
+ return {
112
+ ...common,
113
+ kind: "needs_approval",
114
+ collectionId: phase.collectionId,
115
+ declined: phase.declined,
116
+ heading: name ? say(copy.approvalHeading, name) : say(copy.approvalHeadingUnnamed),
117
+ lead: say(phase.declined ? copy.approvalDeclined : copy.approvalLead)
118
+ };
119
+ }
120
+ case "needs_setup":
121
+ {
122
+ const name = nameOf(phase.collectionId, phase.collection.displayName);
123
+ return {
124
+ ...common,
125
+ kind: "needs_setup",
126
+ collectionId: phase.collectionId,
127
+ heading: say(copy.setupHeading, name),
128
+ lead: say(copy.setupLead)
129
+ };
130
+ }
131
+ case "problem":
132
+ {
133
+ const name = nameOf(phase.collectionId, snapshot.selection.requested.displayName);
134
+ const notice = problemNotice(phase.problem, {
135
+ collectionId: phase.collectionId,
136
+ signedIn,
137
+ open: isOpen(snapshot)
138
+ });
139
+ return {
140
+ ...common,
141
+ kind: "problem",
142
+ collectionId: phase.collectionId,
143
+ notice,
144
+ heading: name ? say(copy.problemHeading, name) : say(copy.problemHeadingUnnamed),
145
+ lead: null
146
+ };
147
+ }
148
+ case "ready":
149
+ return {
150
+ ...common,
151
+ kind: "ready",
152
+ collectionId: phase.collectionId,
153
+ phase,
154
+ notice: phase.problem ? problemNotice(phase.problem, {
155
+ collectionId: phase.collectionId,
156
+ signedIn,
157
+ open: true
158
+ }) : null,
159
+ heading: phase.collection.displayName,
160
+ lead: null
161
+ };
162
+ case "unavailable":
163
+ return {
164
+ ...common,
165
+ kind: "unavailable",
166
+ notice: problemNotice(phase.problem, {
167
+ collectionId: null,
168
+ signedIn: false,
169
+ open: false
170
+ }),
171
+ heading: say(copy.unavailableHeading),
172
+ lead: null
173
+ };
174
+ case "closed":
175
+ return {
176
+ ...common,
177
+ kind: "closed",
178
+ heading: say(copy.closedHeading),
179
+ lead: say(copy.closedLead)
180
+ };
181
+ }
182
+ }
183
+ /** The SDK's description of a failed action, as an inline notice (no control: the person clicks again). */
184
+ export function actionNotice(problem) {
185
+ return problem ? quietNotice(problem) : null;
186
+ }
187
+ function quietNotice(problem) {
188
+ return {
189
+ key: problem.key,
190
+ tone: problem.severity,
191
+ title: problem.title,
192
+ message: problem.message,
193
+ control: null,
194
+ transient: problem.transient
195
+ };
196
+ }
197
+ function problemNotice(problem, context) {
198
+ return {
199
+ ...quietNotice(problem),
200
+ control: problemControl(problem, context)
201
+ };
202
+ }
203
+ /** One control per SDK action; `wait`, `none` and actions with nowhere to go get none. */
204
+ export function problemControl(problem, context) {
205
+ const label = problem.actionLabel;
206
+ if (!label) return null;
207
+ const action = problem.action;
208
+ switch (action) {
209
+ case "sign_in":
210
+ return {
211
+ kind: "sign_in",
212
+ label
213
+ };
214
+ case "approve":
215
+ return {
216
+ kind: "approve",
217
+ label,
218
+ collectionId: context.collectionId
219
+ };
220
+ case "choose_collection":
221
+ return context.signedIn ? {
222
+ kind: "choose_another",
223
+ label
224
+ } : null;
225
+ case "set_up":
226
+ return context.open ? {
227
+ kind: "set_up",
228
+ label
229
+ } : null;
230
+ case "update_app":
231
+ return problem.updateUrl ? {
232
+ kind: "link",
233
+ label,
234
+ href: problem.updateUrl
235
+ } : null;
236
+ case "retry":
237
+ case "check_connection":
238
+ return context.open ? {
239
+ kind: "assess",
240
+ label
241
+ } : {
242
+ kind: "start",
243
+ label
244
+ };
245
+ case "resolve_outcome":
246
+ return {
247
+ kind: "start",
248
+ label
249
+ };
250
+ case "reload":
251
+ return {
252
+ kind: "reload",
253
+ label
254
+ };
255
+ case "wait":
256
+ case "free_space":
257
+ case "contact_support":
258
+ case "none":
259
+ return null;
260
+ }
261
+ }
262
+ function approvalNotice(outcome, copy) {
263
+ if (!outcome || outcome.outcome === "added" || outcome.outcome === "unchanged") return null;
264
+ const message = outcome.kind === "add" ? copy.noCollectionAdded : copy.accessNotGiven;
265
+ return {
266
+ key: `approval_${outcome.outcome}`,
267
+ tone: "info",
268
+ title: null,
269
+ message,
270
+ control: null,
271
+ transient: false
272
+ };
273
+ }
274
+ function currentCollectionId(phase) {
275
+ switch (phase.kind) {
276
+ case "opening":
277
+ case "needs_setup":
278
+ case "ready":
279
+ return phase.collectionId;
280
+ case "problem":
281
+ return phase.collectionId;
282
+ default:
283
+ return null;
284
+ }
285
+ }
286
+ function isOpen(snapshot) {
287
+ return snapshot.status === "ready" || snapshot.status === "setup_required";
288
+ }
289
+ const roleLabels = copy => ({
290
+ owner: copy.roleOwner,
291
+ editor: copy.roleEditor,
292
+ viewer: copy.roleViewer
293
+ });
294
+ /** Rows for the approved collections: current first, then by name; equal names get an ID hint. */
295
+ export function collectionRows(collections, currentId, copy = defaultCollectionCopy) {
296
+ const counts = new Map();
297
+ for (const c of collections) counts.set(c.displayName.trim().toLocaleLowerCase(), (counts.get(c.displayName.trim().toLocaleLowerCase()) ?? 0) + 1);
298
+ const roles = roleLabels(copy);
299
+ const rows = collections.map(c => {
300
+ const availability = c.availability ?? {
301
+ state: "available"
302
+ };
303
+ const unavailable = availability.state === "unavailable";
304
+ const described = unavailable ? describeError(availability) : null;
305
+ return {
306
+ id: c.collectionId,
307
+ name: c.displayName,
308
+ disambiguator: (counts.get(c.displayName.trim().toLocaleLowerCase()) ?? 0) > 1 ? fill(copy.idHint, {
309
+ id: c.collectionId.slice(0, 8)
310
+ }) : null,
311
+ role: c.role,
312
+ roleLabel: roles[c.role],
313
+ private: c.kind === "private",
314
+ current: c.collectionId === currentId,
315
+ available: !unavailable,
316
+ status: availability.state === "opening" ? copy.rowOpening : described?.title ?? null,
317
+ statusDetail: described?.message ?? null,
318
+ entry: c
319
+ };
320
+ });
321
+ return rows.sort((a, b) => Number(b.current) - Number(a.current) || a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
322
+ }
323
+ /** "someone@example.com", or "octocat on GitHub". */
324
+ export function accountLabel(signIn) {
325
+ if (!signIn) return null;
326
+ if (signIn.accountEmail) return signIn.accountEmail;
327
+ if (signIn.accountProvider === "github" && signIn.accountLogin) return _i18n._(
328
+ /** i18n */
329
+ {
330
+ id: "yObBTM",
331
+ message: "{0} on GitHub",
332
+ values: {
333
+ 0: signIn.accountLogin
334
+ }
335
+ });
336
+ return null;
337
+ }
@@ -0,0 +1,19 @@
1
+ import { type CollectionSwitcherProps } from "./CollectionSwitcher.js";
2
+ import type { CollectionUiOptions } from "./use-collection-ui.js";
3
+ export type CollectionUiPart = "panel" | "switcher" | "problem";
4
+ export interface MountCollectionUiOptions extends CollectionUiOptions {
5
+ /** "panel": sign-in, choosing and the list (a settings dialog); "switcher": the header trigger; "problem": the current problem with its fix. */
6
+ readonly part: CollectionUiPart;
7
+ /** "switcher": the trigger's variant. "none" needs an app anchor, so it is not mountable. */
8
+ readonly variant?: Exclude<CollectionSwitcherProps["variant"], "none">;
9
+ /** "switcher": leading content in the trigger ("mark" for the collection's initials). */
10
+ readonly icon?: CollectionSwitcherProps["icon"];
11
+ /** "panel": call `mdbase.start()` when it hasn't started. Default true. */
12
+ readonly autoStart?: boolean;
13
+ }
14
+ export interface MountedCollectionUi {
15
+ update(next: Partial<MountCollectionUiOptions>): void;
16
+ unmount(): void;
17
+ }
18
+ /** Render a shared collection component into an element, for apps that don't render with React. */
19
+ export declare function mountCollectionUi(element: Element, options: MountCollectionUiOptions): MountedCollectionUi;