@mdbase-dev/ui 0.1.0-beta.126 → 0.1.0-beta.147

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 (72) hide show
  1. package/README.md +159 -0
  2. package/css/collections.css +978 -0
  3. package/css/controls.css +26 -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/CommandPalette.d.ts +4 -2
  9. package/dist/CommandPalette.js +275 -103
  10. package/dist/Dialog.js +72 -25
  11. package/dist/Feedback.js +763 -229
  12. package/dist/FeedbackMarkup.d.ts +1 -1
  13. package/dist/FeedbackMarkup.js +326 -125
  14. package/dist/FeedbackVerification.js +115 -68
  15. package/dist/SaveNotice.js +71 -28
  16. package/dist/Select.js +183 -135
  17. package/dist/SelectList.js +59 -14
  18. package/dist/ThemeSelect.js +55 -4
  19. package/dist/apps.d.ts +1 -0
  20. package/dist/apps.js +50 -29
  21. package/dist/brand.js +251 -68
  22. package/dist/codemirror.js +116 -87
  23. package/dist/collections/CollectionGate.d.ts +52 -0
  24. package/dist/collections/CollectionGate.js +456 -0
  25. package/dist/collections/CollectionSaveWarning.d.ts +10 -0
  26. package/dist/collections/CollectionSaveWarning.js +28 -0
  27. package/dist/collections/CollectionSwitcher.d.ts +26 -0
  28. package/dist/collections/CollectionSwitcher.js +579 -0
  29. package/dist/collections/SetupReview.d.ts +9 -0
  30. package/dist/collections/SetupReview.js +175 -0
  31. package/dist/collections/copy.d.ts +100 -0
  32. package/dist/collections/copy.js +937 -0
  33. package/dist/collections/dialogs.d.ts +27 -0
  34. package/dist/collections/dialogs.js +169 -0
  35. package/dist/collections/model.d.ts +146 -0
  36. package/dist/collections/model.js +337 -0
  37. package/dist/collections/mount.d.ts +18 -0
  38. package/dist/collections/mount.js +56 -0
  39. package/dist/collections/parts.d.ts +86 -0
  40. package/dist/collections/parts.js +439 -0
  41. package/dist/collections/testing.d.ts +19 -0
  42. package/dist/collections/testing.js +39 -0
  43. package/dist/collections/use-collection-ui.d.ts +140 -0
  44. package/dist/collections/use-collection-ui.js +236 -0
  45. package/dist/collections.d.ts +19 -0
  46. package/dist/collections.js +13 -0
  47. package/dist/feedback-data.d.ts +13 -12
  48. package/dist/feedback-data.js +365 -168
  49. package/dist/flock.d.ts +63 -0
  50. package/dist/flock.js +140 -0
  51. package/dist/i18n.d.ts +6 -0
  52. package/dist/i18n.js +71 -0
  53. package/dist/locales/de/messages.json +1 -0
  54. package/dist/locales/en/messages.json +1 -0
  55. package/dist/locales/es/messages.json +1 -0
  56. package/dist/locales/fr/messages.json +1 -0
  57. package/dist/locales/it/messages.json +1 -0
  58. package/dist/locales/ja/messages.json +1 -0
  59. package/dist/locales/ko/messages.json +1 -0
  60. package/dist/locales/pt/messages.json +1 -0
  61. package/dist/locales/ru/messages.json +1 -0
  62. package/dist/locales/zh/messages.json +1 -0
  63. package/dist/mark-activity.js +82 -75
  64. package/dist/permission-selection.js +27 -17
  65. package/dist/popover.js +138 -130
  66. package/dist/screens.d.ts +20 -3
  67. package/dist/screens.js +275 -6
  68. package/dist/select-model.js +34 -28
  69. package/dist/text-diff.d.ts +1 -0
  70. package/dist/text-diff.js +106 -74
  71. package/dist/theme.js +34 -40
  72. package/package.json +52 -7
@@ -0,0 +1,140 @@
1
+ import type { Mdbase, MdbaseProblem } from "@mdbase-dev/sdk";
2
+ import { type ReactNode } from "react";
3
+ import { type CollectionCopy } from "./copy.js";
4
+ import { type CollectionNotice, type CollectionRow, type CollectionView, type ProblemControl } from "./model.js";
5
+ /** The only app-specific input: what the app is called, and its mark. */
6
+ export interface CollectionAppIdentity {
7
+ readonly name: string;
8
+ readonly mark?: ReactNode;
9
+ }
10
+ export type LeaveIntent = "switch" | "add" | "choose_another" | "sign_out";
11
+ /** A collection the app lists itself (one the SDK doesn't), in an app section. */
12
+ export interface CollectionSectionItem {
13
+ readonly id: string;
14
+ readonly name: string;
15
+ /** A second line under the name: where it lives, or which account. */
16
+ readonly detail?: string | null | undefined;
17
+ /** Open in the app now. With `renderAppCollection`, the gate shows the app instead of its screens. */
18
+ readonly current?: boolean | undefined;
19
+ /** Listed but not selectable; say why in `detail`. */
20
+ readonly disabled?: boolean | undefined;
21
+ /** The row's menu, like an SDK collection's: shown when there is at least one. */
22
+ readonly actions?: readonly CollectionSectionItemAction[] | undefined;
23
+ }
24
+ /** An entry in an app section item's row menu. */
25
+ export interface CollectionSectionItemAction {
26
+ readonly id: string;
27
+ readonly label: string;
28
+ /** Runs in the click, after the menu closes and focus is back on the row's menu button. */
29
+ readonly onSelect: (item: CollectionSectionItem, context: CollectionSectionItemActionContext) => void;
30
+ /** Destructive: shown in the danger colour. Confirm before acting, for example with `confirmDelete`. */
31
+ readonly danger?: boolean | undefined;
32
+ readonly disabled?: boolean | undefined;
33
+ }
34
+ /**
35
+ * How the delete dialog ended: `deleted`; `cancelled` with no attempt; `failed`
36
+ * when the person closed it after a delete was refused.
37
+ */
38
+ export type DeleteCollectionOutcome = "deleted" | "cancelled" | "failed";
39
+ /** What a section item action can ask of the screen that shows its menu. */
40
+ export interface CollectionSectionItemActionContext {
41
+ /**
42
+ * Open `DeleteCollectionDialog` for the item: the person types its name, then
43
+ * the SDK deletes it by the item's id. `onClose` gets the outcome.
44
+ */
45
+ readonly confirmDelete: (onClose?: (outcome: DeleteCollectionOutcome) => void) => void;
46
+ }
47
+ /**
48
+ * A titled group of app-listed collections, shown after "Your collections" on
49
+ * the gate's choosing and signed-out screens and in the switcher.
50
+ */
51
+ export interface CollectionSection {
52
+ readonly id: string;
53
+ readonly title: string;
54
+ readonly items: readonly CollectionSectionItem[];
55
+ /** Open an item. Runs in the click, after `onBeforeLeave("switch")` when something else is open. */
56
+ readonly onSelect: (item: CollectionSectionItem) => void;
57
+ /** Close the open item: the person chose another collection or went back to the list. */
58
+ readonly onClose?: ((item: CollectionSectionItem) => void) | undefined;
59
+ /** Shown when there are no items. Without it an empty section is hidden. */
60
+ readonly empty?: string | undefined;
61
+ }
62
+ /** The app section item that is open, if any. */
63
+ export interface OpenSectionItem {
64
+ readonly section: CollectionSection;
65
+ readonly item: CollectionSectionItem;
66
+ }
67
+ export interface CollectionUiOptions {
68
+ /** The app's entry from `createMdbase`. */
69
+ readonly mdbase: Mdbase;
70
+ readonly app: CollectionAppIdentity;
71
+ /**
72
+ * Asked before anything that leaves the open collection. Return false to stay.
73
+ * Return a promise to ask first (an unsaved-work dialog): resolve it from that
74
+ * dialog's own click, and the shared UI continues in the same task, so a
75
+ * Connect window it opens still counts as the person's gesture.
76
+ */
77
+ readonly onBeforeLeave?: ((intent: LeaveIntent) => boolean | Promise<boolean>) | undefined;
78
+ /** @deprecated Presentation-only compatibility option; the single Add entry always uses the SDK chooser. */
79
+ readonly allowPrivate?: boolean | undefined;
80
+ /** Offer "Delete collection…" to owners. Default false. */
81
+ readonly allowDelete?: boolean | undefined;
82
+ /** Replacement or translated copy by key. */
83
+ readonly copy?: Partial<CollectionCopy> | undefined;
84
+ /** App-listed collections, in sections after the SDK's own list. */
85
+ readonly sections?: readonly CollectionSection[] | undefined;
86
+ /** A second line for an SDK collection row (where it lives, which account), from the row and its SDK entry. */
87
+ readonly rowDetail?: ((row: CollectionRow) => string | null | undefined) | undefined;
88
+ }
89
+ /** `add_private` remains available to legacy track callers; both add APIs now emit `add`. */
90
+ export type CollectionActionName = "sign_in" | "add" | "add_private" | "approve" | "select" | "choose_another" | "assess" | "start" | "sign_out" | "setup";
91
+ export interface CollectionUi {
92
+ readonly view: CollectionView;
93
+ readonly copy: CollectionCopy;
94
+ readonly say: (key: keyof CollectionCopy, values?: Readonly<Record<string, string>>) => string;
95
+ readonly options: CollectionUiOptions;
96
+ /** The running action, if any. Its controls stay focusable but inert. */
97
+ readonly busy: CollectionActionName | null;
98
+ /** The last failed action's SDK description, while the phase is unchanged. */
99
+ readonly error: CollectionNotice | null;
100
+ /** Set when sign-out found unsaved changes: the SDK's copy for the confirm. */
101
+ readonly signOutPrompt: MdbaseProblem | null;
102
+ /** The open app section item, which takes the place of the SDK's current collection. */
103
+ readonly appOpen: OpenSectionItem | null;
104
+ readonly actions: CollectionActions;
105
+ }
106
+ export interface CollectionActions {
107
+ signIn(): void;
108
+ addCollection(): void;
109
+ /** @deprecated Use addCollection. Both names open the same SDK chooser. */
110
+ addPrivateCollection(): void;
111
+ approve(collectionId: string | null): void;
112
+ select(collectionId: string): void;
113
+ /** Open an app section item, closing whatever app item was open. */
114
+ selectItem(section: CollectionSection, item: CollectionSectionItem): void;
115
+ /** Back to the list: closes the open app item, or the SDK's collection. */
116
+ chooseAnother(): void;
117
+ /** Open the Connect window again for the authorization in progress. */
118
+ reopen(): void;
119
+ /** Abandon the authorization this page started. */
120
+ cancel(): void;
121
+ assess(): void;
122
+ start(): void;
123
+ signOut(): void;
124
+ confirmSignOut(): void;
125
+ dismissSignOut(): void;
126
+ /** Run a problem's own control. `set_up` goes to `onSetUp`. */
127
+ perform(control: ProblemControl, onSetUp?: () => void): void;
128
+ /** Track an app-ui call that is not a button above (setup), so errors show the same way. */
129
+ track(name: CollectionActionName, call: () => Promise<unknown>): Promise<unknown>;
130
+ }
131
+ /** The first current item across the app's sections. */
132
+ export declare function appOpenItem(sections: readonly CollectionSection[] | undefined): OpenSectionItem | null;
133
+ /**
134
+ * Start the front door once it is rendered, while it has not started. `start()`
135
+ * is safe to call again; this also skips calls while one is in flight, so
136
+ * several gates and StrictMode's double effects make one call.
137
+ */
138
+ export declare function useAutoStart(mdbase: Mdbase, enabled?: boolean): void;
139
+ /** Subscribe to the app's mdbase entry and get its view plus gesture-safe actions. */
140
+ export declare function useCollectionUi(options: CollectionUiOptions): CollectionUi;
@@ -0,0 +1,236 @@
1
+ import { useLocaleRevision } from "../i18n.js";
2
+ import { describeError } from "@mdbase-dev/sdk/describe";
3
+ import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
+ import { defaultCollectionCopy, fill } from "./copy.js";
5
+ import { actionNotice, collectionView } from "./model.js";
6
+ const popupActions = new Set(["sign_in", "add", "add_private", "approve"]);
7
+ /** The first current item across the app's sections. */
8
+ export function appOpenItem(sections) {
9
+ for (const section of sections ?? []) {
10
+ const item = section.items.find(candidate => candidate.current);
11
+ if (item) return {
12
+ section,
13
+ item
14
+ };
15
+ }
16
+ return null;
17
+ }
18
+ const starting = new WeakSet();
19
+ /**
20
+ * Start the front door once it is rendered, while it has not started. `start()`
21
+ * is safe to call again; this also skips calls while one is in flight, so
22
+ * several gates and StrictMode's double effects make one call.
23
+ */
24
+ export function useAutoStart(mdbase, enabled = true) {
25
+ const notStarted = useSyncExternalStore(mdbase.subscribe, () => mdbase.getSnapshot().status === "not_started", () => false);
26
+ useEffect(() => {
27
+ if (!enabled || !notStarted || starting.has(mdbase)) return;
28
+ starting.add(mdbase);
29
+ const done = () => {
30
+ starting.delete(mdbase);
31
+ };
32
+ // A failed start shows as the snapshot's problem.
33
+ new Promise(resolve => {
34
+ resolve(mdbase.start());
35
+ }).then(done, done);
36
+ }, [mdbase, enabled, notStarted]);
37
+ }
38
+ /** Subscribe to the app's mdbase entry and get its view plus gesture-safe actions. */
39
+ export function useCollectionUi(options) {
40
+ const {
41
+ mdbase,
42
+ app
43
+ } = options;
44
+ const snapshot = useSyncExternalStore(mdbase.subscribe, mdbase.getSnapshot, mdbase.getSnapshot);
45
+ const localeRevision = useLocaleRevision();
46
+ const copy = useMemo(() => ({
47
+ ...defaultCollectionCopy,
48
+ ...options.copy
49
+ }), [options.copy, localeRevision]);
50
+ const view = useMemo(() => collectionView(snapshot, {
51
+ appName: app.name,
52
+ copy
53
+ }), [snapshot, app.name, copy]);
54
+ const [busy, setBusy] = useState(null);
55
+ const [error, setError] = useState(null);
56
+ const [signOutPrompt, setSignOutPrompt] = useState(null);
57
+ const running = useRef(null);
58
+ const latest = useRef({
59
+ options,
60
+ view
61
+ });
62
+ latest.current = {
63
+ options,
64
+ view
65
+ };
66
+ const actions = useMemo(() => {
67
+ /**
68
+ * Calls the SDK first, synchronously, then records state: Connect windows
69
+ * are reserved inside `call`, before anything here awaits.
70
+ */
71
+ const run = (name, call, again = false) => {
72
+ if (running.current && !again) return Promise.resolve();
73
+ const controller = new AbortController();
74
+ let pending;
75
+ try {
76
+ pending = call(controller.signal);
77
+ } catch (cause) {
78
+ pending = Promise.reject(cause);
79
+ }
80
+ const kind = latest.current.view.kind;
81
+ running.current = {
82
+ name,
83
+ controller
84
+ };
85
+ setBusy(name);
86
+ setError(null);
87
+ return pending.then(() => undefined, cause => {
88
+ const problem = describeError(cause);
89
+ if (problem) console.info("[mdbase collections]", problem.code, problem.developerMessage ?? "");
90
+ if (name === "sign_out" && problem?.key === "sign_out_pending_writes") setSignOutPrompt(problem);else if (!controller.signal.aborted) {
91
+ const notice = actionNotice(problem);
92
+ setError(notice ? {
93
+ kind,
94
+ notice
95
+ } : null);
96
+ }
97
+ }).finally(() => {
98
+ if (running.current?.controller !== controller) return;
99
+ running.current = null;
100
+ setBusy(null);
101
+ });
102
+ };
103
+ /** Ask the app first when leaving the open collection; continue in the same task when it agrees. */
104
+ const leave = (intent, go) => {
105
+ const {
106
+ options: current,
107
+ view: now
108
+ } = latest.current;
109
+ const open = now.current !== null || appOpenItem(current.sections) !== null;
110
+ const answer = open && current.onBeforeLeave ? current.onBeforeLeave(intent) : true;
111
+ if (answer === true) go();else if (answer !== false) void answer.then(ok => {
112
+ if (ok) go();
113
+ }, () => {});
114
+ };
115
+ const addCollection = () => leave("add", () => {
116
+ void run("add", signal => mdbase.addCollection({
117
+ signal
118
+ }));
119
+ });
120
+ const actions = {
121
+ signIn: () => {
122
+ void run("sign_in", signal => mdbase.signIn({
123
+ signal
124
+ }));
125
+ },
126
+ addCollection,
127
+ addPrivateCollection: addCollection,
128
+ approve: id => {
129
+ void run("approve", signal => mdbase.approve(id ?? undefined, {
130
+ signal
131
+ }));
132
+ },
133
+ select: id => {
134
+ const app = appOpenItem(latest.current.options.sections);
135
+ if (!app && latest.current.view.current?.id === id) return;
136
+ leave("switch", () => {
137
+ app?.section.onClose?.(app.item);
138
+ void run("select", signal => mdbase.select(id, {
139
+ signal
140
+ }));
141
+ });
142
+ },
143
+ selectItem: (section, item) => {
144
+ if (item.current || item.disabled) return;
145
+ leave("switch", () => {
146
+ const app = appOpenItem(latest.current.options.sections);
147
+ if (app && app.item.id !== item.id) app.section.onClose?.(app.item);
148
+ section.onSelect(item);
149
+ });
150
+ },
151
+ chooseAnother: () => leave("choose_another", () => {
152
+ const app = appOpenItem(latest.current.options.sections);
153
+ app?.section.onClose?.(app.item);
154
+ if (!app || latest.current.view.current) void run("choose_another", signal => mdbase.chooseAnother({
155
+ signal
156
+ }));
157
+ }),
158
+ reopen: () => {
159
+ const now = latest.current.view;
160
+ if (now.kind !== "authorizing") return;
161
+ const call = now.purpose === "sign_in" ? signal => mdbase.signIn({
162
+ signal
163
+ }) : now.purpose === "add" ? signal => mdbase.addCollection({
164
+ signal
165
+ }) : signal => mdbase.approve(now.collectionId ?? undefined, {
166
+ signal
167
+ });
168
+ void run(now.purpose === "sign_in" ? "sign_in" : now.purpose === "add" ? "add" : "approve", call, true);
169
+ },
170
+ cancel: () => {
171
+ const current = running.current;
172
+ if (current && popupActions.has(current.name)) current.controller.abort();
173
+ },
174
+ assess: () => {
175
+ void run("assess", signal => mdbase.assess({
176
+ signal
177
+ }));
178
+ },
179
+ start: () => {
180
+ void run("start", signal => mdbase.start({
181
+ signal
182
+ }));
183
+ },
184
+ signOut: () => leave("sign_out", () => {
185
+ void run("sign_out", signal => mdbase.signOut({
186
+ signal
187
+ }));
188
+ }),
189
+ confirmSignOut: () => {
190
+ setSignOutPrompt(null);
191
+ void run("sign_out", signal => mdbase.signOut({
192
+ signal,
193
+ discard: true
194
+ }), true);
195
+ },
196
+ dismissSignOut: () => setSignOutPrompt(null),
197
+ perform: (control, onSetUp) => {
198
+ switch (control.kind) {
199
+ case "sign_in":
200
+ return actions.signIn();
201
+ case "approve":
202
+ return actions.approve(control.collectionId);
203
+ case "choose_another":
204
+ return actions.chooseAnother();
205
+ case "set_up":
206
+ return onSetUp?.();
207
+ case "assess":
208
+ return actions.assess();
209
+ case "start":
210
+ return actions.start();
211
+ case "reload":
212
+ return globalThis.location?.reload();
213
+ case "link":
214
+ return undefined;
215
+ }
216
+ },
217
+ track: (name, call) => run(name, call, true)
218
+ };
219
+ return actions;
220
+ }, [mdbase]);
221
+ const say = useMemo(() => (key, values = {}) => fill(copy[key], {
222
+ app: app.name,
223
+ ...values
224
+ }), [copy, app.name]);
225
+ return {
226
+ view,
227
+ copy,
228
+ say,
229
+ options,
230
+ busy,
231
+ signOutPrompt,
232
+ actions,
233
+ appOpen: appOpenItem(options.sections),
234
+ error: error && error.kind === view.kind ? error.notice : null
235
+ };
236
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `@mdbase-dev/ui/collections`: the shared collection screens. Import
3
+ * `@mdbase-dev/ui/collections.css` once. The SDK (`createMdbase`) is a peer.
4
+ */
5
+ export { CollectionGate, CollectionPanel, CollectionProblem } from "./collections/CollectionGate.js";
6
+ export type { AppCollectionContext, CollectionExtraActions, CollectionGateOwnProps, CollectionGateProps, CollectionPanelProps, CollectionProblemProps } from "./collections/CollectionGate.js";
7
+ export { CollectionSaveWarning, useCollectionSaveWarning } from "./collections/CollectionSaveWarning.js";
8
+ export { CollectionSwitcher } from "./collections/CollectionSwitcher.js";
9
+ export type { CollectionSwitcherProps } from "./collections/CollectionSwitcher.js";
10
+ export { SetupReview as CollectionSetupReview } from "./collections/SetupReview.js";
11
+ export { DeleteCollectionDialog, ForgetCollectionDialog } from "./collections/dialogs.js";
12
+ export { useAutoStart, useCollectionUi } from "./collections/use-collection-ui.js";
13
+ export type { CollectionActionName, CollectionActions, CollectionAppIdentity, CollectionSection, CollectionSectionItem, CollectionSectionItemAction, CollectionSectionItemActionContext, CollectionUi, CollectionUiOptions, DeleteCollectionOutcome, LeaveIntent, OpenSectionItem, } from "./collections/use-collection-ui.js";
14
+ export { collectionView, collectionRows, problemControl, accountLabel } from "./collections/model.js";
15
+ export type { CollectionNotice, CollectionRole, CollectionRow, CollectionView, CollectionViewKind, CollectionViewOptions, ProblemControl } from "./collections/model.js";
16
+ export { defaultCollectionCopy } from "./collections/copy.js";
17
+ export type { CollectionCopy, CollectionCopyKey } from "./collections/copy.js";
18
+ export { mountCollectionUi } from "./collections/mount.js";
19
+ export type { CollectionUiPart, MountCollectionUiOptions, MountedCollectionUi } from "./collections/mount.js";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `@mdbase-dev/ui/collections`: the shared collection screens. Import
3
+ * `@mdbase-dev/ui/collections.css` once. The SDK (`createMdbase`) is a peer.
4
+ */
5
+ export { CollectionGate, CollectionPanel, CollectionProblem } from "./collections/CollectionGate.js";
6
+ export { CollectionSaveWarning, useCollectionSaveWarning } from "./collections/CollectionSaveWarning.js";
7
+ export { CollectionSwitcher } from "./collections/CollectionSwitcher.js";
8
+ export { SetupReview as CollectionSetupReview } from "./collections/SetupReview.js";
9
+ export { DeleteCollectionDialog, ForgetCollectionDialog } from "./collections/dialogs.js";
10
+ export { useAutoStart, useCollectionUi } from "./collections/use-collection-ui.js";
11
+ export { collectionView, collectionRows, problemControl, accountLabel } from "./collections/model.js";
12
+ export { defaultCollectionCopy } from "./collections/copy.js";
13
+ export { mountCollectionUi } from "./collections/mount.js";
@@ -1,3 +1,4 @@
1
+ import "./i18n.js";
1
2
  export declare const FEEDBACK_MAX_MESSAGE_LENGTH = 5000;
2
3
  export declare const FEEDBACK_MAX_SCREENSHOT_BYTES: number;
3
4
  export declare const FEEDBACK_ERROR_CODES: readonly ["cancelled", "http_error", "invalid_response", "outcome_unknown", "partial_failure", "timeout", "network_error", "unknown_error", "save_failed", "source_open_failed", "preview_failed"];
@@ -44,22 +45,22 @@ export interface FeedbackSubmission {
44
45
  }
45
46
  export declare const feedbackTopics: {
46
47
  problem: {
47
- label: string;
48
- prompt: string;
49
- placeholder: string;
50
- thanks: string;
48
+ readonly label: string;
49
+ readonly prompt: string;
50
+ readonly placeholder: string;
51
+ readonly thanks: string;
51
52
  };
52
53
  idea: {
53
- label: string;
54
- prompt: string;
55
- placeholder: string;
56
- thanks: string;
54
+ readonly label: string;
55
+ readonly prompt: string;
56
+ readonly placeholder: string;
57
+ readonly thanks: string;
57
58
  };
58
59
  appreciation: {
59
- label: string;
60
- prompt: string;
61
- placeholder: string;
62
- thanks: string;
60
+ readonly label: string;
61
+ readonly prompt: string;
62
+ readonly placeholder: string;
63
+ readonly thanks: string;
63
64
  };
64
65
  };
65
66
  /** Each provider owns its bounded, short-lived error buffer; never accepts messages or request data. */