diff --git a/apps/extension/package.json b/apps/extension/package.json index a23dd32..86f3174 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -12,7 +12,7 @@ "typecheck": "tsc -p tsconfig.json" }, "dependencies": { - "@mdbase-dev/ui": "0.1.0-beta.123", + "@mdbase-dev/ui": "0.1.0-beta.124", "@mdbase-reader/connect": "workspace:*", "@mdbase-reader/core": "workspace:*", "@mdbase-reader/platform": "workspace:*", diff --git a/apps/reader/package.json b/apps/reader/package.json index 2e257b8..5431452 100644 --- a/apps/reader/package.json +++ b/apps/reader/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@citation-js/plugin-csl": "0.8.2", - "@mdbase-dev/ui": "0.1.0-beta.123", + "@mdbase-dev/ui": "0.1.0-beta.124", "@mdbase-reader/connect": "workspace:*", "@mdbase-reader/core": "workspace:*", "@mdbase-reader/markdown-editor": "workspace:*", diff --git a/apps/reader/src/ReaderHeader.tsx b/apps/reader/src/ReaderHeader.tsx index 351cbc8..d478602 100644 --- a/apps/reader/src/ReaderHeader.tsx +++ b/apps/reader/src/ReaderHeader.tsx @@ -1,5 +1,6 @@ import { AppSwitcher } from "@mdbase-dev/ui/app-switcher"; import { shortcutLabel } from "@mdbase-dev/ui/command-palette"; +import { useMdbaseMarkBusy } from "@mdbase-dev/ui/mark-activity"; import { CollectionPicker } from "./CollectionPicker.js"; import { DisplayMenu } from "./DisplayMenu.js"; @@ -204,6 +205,7 @@ function ConnectionState({ readonly directAccess: ReaderDirectAccessState; }): JSX.Element { const { snapshot, working, problem, request } = directAccess; + useMdbaseMarkBusy(state === "syncing" && "stream"); if (snapshot?.authority !== "connector" || state !== "connected") { return ; } diff --git a/apps/reader/src/import-controller.ts b/apps/reader/src/import-controller.ts index 3fd1259..e732ca4 100644 --- a/apps/reader/src/import-controller.ts +++ b/apps/reader/src/import-controller.ts @@ -10,6 +10,8 @@ import { type MigrationResult, } from "@mdbase-reader/migration"; +import { markFraction, withMarkProgress } from "./mark-activity.js"; + import type { ReaderApplicationSession } from "@mdbase-reader/connect"; export interface ImportState { plan: MigrationPlan | null; @@ -150,10 +152,17 @@ export class ImportController { this.update({ boundTarget: target.collectionId }); void this.task(async (signal) => { this.update({ result: null }); - const result = await runMigration(plan, target, signal, (progress) => { - this.update({ progress, message: `${progress.phase} — ${name}` }); - saveProgress(plan.service, progress); - }); + // The mark fills with the import and plays saved or error when it ends; a stop is quiet. + const result = await withMarkProgress( + (mark) => + runMigration(plan, target, signal, (progress) => { + mark(markFraction(progress.completed, progress.total)); + this.update({ progress, message: `${progress.phase} — ${name}` }); + saveProgress(plan.service, progress); + }), + undefined, + signal, + ); this.client?.forget(); this.update({ result, diff --git a/apps/reader/src/mark-activity.test.tsx b/apps/reader/src/mark-activity.test.tsx new file mode 100644 index 0000000..f9da385 --- /dev/null +++ b/apps/reader/src/mark-activity.test.tsx @@ -0,0 +1,132 @@ +/* eslint-disable @typescript-eslint/require-await -- the work under test is async by contract */ +import { getMdbaseMarkActivity, resetMdbaseMarkActivity } from "@mdbase-dev/ui/mark-activity"; +import { sourceId, type SourceSummary } from "@mdbase-reader/core"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { markFraction, withMarkProgress } from "./mark-activity.js"; +import { eachSource } from "./use-library-writes.js"; + +import type { BulkStatusProgress } from "./LibraryBulkBar.js"; + +const mark = (): ReturnType => getMdbaseMarkActivity(); + +beforeEach(() => resetMdbaseMarkActivity()); +afterEach(() => resetMdbaseMarkActivity()); + +describe("withMarkProgress", () => { + it("fills the mark while the work reports progress, then plays saved", async () => { + let seen: number | null = null; + const result = await withMarkProgress(async (update) => { + update(0.4); + seen = mark().progress; + return "done"; + }); + expect(result).toBe("done"); + expect(seen).toBe(0.4); + expect(mark().progress).toBeNull(); + expect(mark().signal?.kind).toBe("saved"); + }); + + it("ends the way the result says", async () => { + await withMarkProgress( + async () => null, + () => "fail", + ); + expect(mark()).toMatchObject({ progress: null, signal: { kind: "error" } }); + resetMdbaseMarkActivity(); + await withMarkProgress( + async () => null, + () => "cancel", + ); + expect(mark()).toMatchObject({ progress: null, signal: null }); + }); + + it("plays error when the work throws, and rethrows", async () => { + await expect( + withMarkProgress(async () => { + throw new Error("offline"); + }), + ).rejects.toThrow("offline"); + expect(mark()).toMatchObject({ progress: null, signal: { kind: "error" } }); + }); + + it("stops quietly when the work was aborted", async () => { + const controller = new AbortController(); + await expect( + withMarkProgress( + async () => { + controller.abort(); + throw new DOMException("Stopped", "AbortError"); + }, + undefined, + controller.signal, + ), + ).rejects.toThrow("Stopped"); + expect(mark()).toMatchObject({ progress: null, signal: null }); + }); + + it("never leaves the mark filling when deciding the outcome throws", async () => { + await expect( + withMarkProgress( + async () => 1, + () => { + throw new Error("bad outcome"); + }, + ), + ).rejects.toThrow("bad outcome"); + expect(mark().progress).toBeNull(); + }); +}); + +describe("markFraction", () => { + it("is the share done, and nothing of nothing", () => { + expect(markFraction(3, 4)).toBe(0.75); + expect(markFraction(0, 0)).toBe(0); + }); +}); + +describe("a bulk library update on the mark", () => { + const sources = ["a", "b", "c", "d"].map( + (id) => ({ id: sourceId(id) }) as unknown as SourceSummary, + ); + + it("fills with each source written and plays saved when all succeed", async () => { + const fills: (number | null)[] = []; + const reports: BulkStatusProgress[] = []; + await eachSource( + sources, + (value) => { + reports.push(value); + fills.push(mark().progress); + }, + async () => undefined, + ); + expect(reports.at(-1)).toEqual({ done: 4, total: 4, failed: 0 }); + expect(fills).toEqual([0.25, 0.5, 0.75, 1]); + expect(mark()).toMatchObject({ progress: null, signal: { kind: "saved" } }); + }); + + it("plays error when any source could not be updated", async () => { + const reports: BulkStatusProgress[] = []; + await eachSource( + sources, + (value) => reports.push(value), + async (source) => { + if (source.id === sourceId("c")) { + throw new Error("conflict"); + } + }, + ); + expect(reports.at(-1)).toEqual({ done: 4, total: 4, failed: 1 }); + expect(mark()).toMatchObject({ progress: null, signal: { kind: "error" } }); + }); + + it("leaves the mark alone for an empty selection", async () => { + await eachSource( + [], + () => undefined, + async () => undefined, + ); + expect(mark()).toMatchObject({ progress: null, signal: null }); + }); +}); diff --git a/apps/reader/src/mark-activity.ts b/apps/reader/src/mark-activity.ts new file mode 100644 index 0000000..5777cff --- /dev/null +++ b/apps/reader/src/mark-activity.ts @@ -0,0 +1,37 @@ +import { trackMdbaseMarkProgress } from "@mdbase-dev/ui/mark-activity"; + +/** How a tracked operation ends on the mark: `finish` plays saved, `fail` plays error. */ +export type MarkProgressOutcome = "finish" | "fail" | "cancel"; + +/** + * Shows `work`'s known progress on the app mark and always ends it. A result is ended by + * `outcome` (finished by default); a throw fails it, or cancels it quietly once `signal` is + * aborted, so a stopped operation never shakes the mark. + */ +export async function withMarkProgress( + work: (update: (fraction: number) => void) => Promise, + outcome: (result: T) => MarkProgressOutcome = () => "finish", + signal?: AbortSignal, +): Promise { + const progress = trackMdbaseMarkProgress(); + try { + const result = await work(progress.update); + progress[outcome(result)](); + return result; + } catch (reason) { + if (signal?.aborted) { + progress.cancel(); + } else { + progress.fail(); + } + throw reason; + } finally { + // Already ended above; this only guards against an `outcome` that throws. + progress.cancel(); + } +} + +/** `done` of `total` as a 0 to 1 fraction; nothing is done of nothing. */ +export function markFraction(done: number, total: number): number { + return total > 0 ? done / total : 0; +} diff --git a/apps/reader/src/use-annotation-edit.ts b/apps/reader/src/use-annotation-edit.ts index 3682c25..1d4c3f5 100644 --- a/apps/reader/src/use-annotation-edit.ts +++ b/apps/reader/src/use-annotation-edit.ts @@ -1,3 +1,4 @@ +import { signalMdbaseMark } from "@mdbase-dev/ui/mark-activity"; import { useEffect, useMemo, useState, useSyncExternalStore } from "react"; import { confirmAnnotationDiscard } from "./annotation-draft-actions.js"; @@ -79,8 +80,16 @@ export function useAnnotationEdit(props: AnnotationEditProps): AnnotationEditCon if (locked || !session.ownsEditor(editorOwner) || !ready) { return; } + // Saving an already-saved annotation writes nothing, so the mark stays still. + const wrote = session.getSnapshot().status !== "saved"; void session.save().then(() => { - if (session.getSnapshot().status === "saved" && deletion.isMounted()) { + const after = session.getSnapshot(); + if (wrote && after.status === "saved") { + signalMdbaseMark("saved"); + } else if (wrote && (after.status === "error" || after.conflict)) { + signalMdbaseMark("error"); + } + if (after.status === "saved" && deletion.isMounted()) { onCancel(); } }); diff --git a/apps/reader/src/use-bibliography-export.ts b/apps/reader/src/use-bibliography-export.ts index 41426bb..0a3082d 100644 --- a/apps/reader/src/use-bibliography-export.ts +++ b/apps/reader/src/use-bibliography-export.ts @@ -1,3 +1,4 @@ +import { signalMdbaseMark } from "@mdbase-dev/ui/mark-activity"; import { buildCslBibliography, serializeCslBibliography } from "@mdbase-reader/core"; import { useCallback, useMemo, useState } from "react"; @@ -27,33 +28,35 @@ export function useBibliographyExport( readonly status: BibliographyExportStatus; readonly message: string | null; }>({ status: "idle", message: null }); + const fail = useCallback((message: string): void => { + signalMdbaseMark("error"); + setState({ status: "error", message }); + }, []); const save = useCallback( (items: ReturnType["items"]): void => { if (!saveFile) { - setState({ status: "error", message: "File export is unavailable in this build." }); + fail("File export is unavailable in this build."); return; } if (items.length === 0) { - setState({ status: "error", message: "No valid citations are ready to export." }); + fail("No valid citations are ready to export."); return; } setState({ status: "exporting", message: null }); const contents = serializeCslBibliography(items); void saveFile("references.json", new Blob([contents], { type: "application/json" })) - .then(() => + .then(() => { + signalMdbaseMark("saved"); setState({ status: "success", message: `Exported ${countLabel(items.length, "citation")}.`, - }), - ) + }); + }) .catch((reason: unknown) => - setState({ - status: "error", - message: readerErrorMessage(reason, "Reader could not export the bibliography."), - }), + fail(readerErrorMessage(reason, "Reader could not export the bibliography.")), ); }, - [saveFile], + [fail, saveFile], ); const run = useCallback((): void => save(bibliography.items), [bibliography, save]); const runFor = useCallback( diff --git a/apps/reader/src/use-citation-editor.ts b/apps/reader/src/use-citation-editor.ts index 85784cb..7b37cd1 100644 --- a/apps/reader/src/use-citation-editor.ts +++ b/apps/reader/src/use-citation-editor.ts @@ -1,3 +1,4 @@ +import { signalMdbaseMark } from "@mdbase-dev/ui/mark-activity"; import { citationCompletenessWarnings, citekeyForCitation, @@ -97,8 +98,12 @@ export function useCitationEditor(input: { .then((updated) => { input.onSaved(updated); setStatusState({ sourceId: updated.id, value: "saved" }); + // The editor's SaveNotice usually plays this too; the mark plays it once. + signalMdbaseMark("saved"); }) .catch((reason: unknown) => { + // The editor's SaveNotice never shows attention; the error is reported below the form. + signalMdbaseMark("error"); setStatusState({ sourceId: source.id, value: "idle" }); setErrorState({ sourceId: source.id, diff --git a/apps/reader/src/use-creation-saving.ts b/apps/reader/src/use-creation-saving.ts index ccbe61d..43e633a 100644 --- a/apps/reader/src/use-creation-saving.ts +++ b/apps/reader/src/use-creation-saving.ts @@ -1,3 +1,4 @@ +import { signalMdbaseMark } from "@mdbase-dev/ui/mark-activity"; import { useRef, useState, type RefObject } from "react"; import { saveAnnotationDraftToCollection } from "./annotation-draft-actions.js"; @@ -46,6 +47,12 @@ export function useCreationSaving(input: { busy.current = false; setSaving(false); }; + // The app mark confirms each annotation the person saves and shakes when one could not be. + const saved = (): void => signalMdbaseMark("saved"); + const failed = (value: NonNullable): void => { + signalMdbaseMark("error"); + setProblem(value); + }; return { busy, saving, @@ -72,8 +79,8 @@ export function useCreationSaving(input: { }, value, "", - () => undefined, - setProblem, + saved, + failed, () => undefined, ); }, @@ -83,12 +90,15 @@ export function useCreationSaving(input: { return; } void saveAnnotationDraftToCollection( - () => buffer.clearIf(value), + () => { + buffer.clearIf(value); + saved(); + }, value, source, surface, create, - setProblem, + failed, finish, ); }, diff --git a/apps/reader/src/use-library-writes.ts b/apps/reader/src/use-library-writes.ts index e42ce7b..0723820 100644 --- a/apps/reader/src/use-library-writes.ts +++ b/apps/reader/src/use-library-writes.ts @@ -1,7 +1,9 @@ +import { signalMdbaseMark } from "@mdbase-dev/ui/mark-activity"; import { useCallback, useMemo } from "react"; import { propertyValue } from "./library-columns.js"; import { parseFieldInput } from "./library-field-edit.js"; +import { markFraction, withMarkProgress } from "./mark-activity.js"; import type { FieldShape } from "./library-conditions.js"; import type { BulkStatusProgress } from "./LibraryBulkBar.js"; @@ -61,9 +63,16 @@ export function useLibraryWrites( ); const saveField = useCallback( async (source: SourceSummary, key: string, text: string): Promise => { - if (saveFields) { + if (!saveFields) { + return; + } + try { onSourceChanged?.(await saveFields(source.id, { [key]: fieldValue(source, key, text) })); + } catch (reason) { + signalMdbaseMark("error"); + throw reason; } + signalMdbaseMark("saved"); }, [fieldValue, onSourceChanged, saveFields], ); @@ -76,24 +85,34 @@ export function useLibraryWrites( ); } -async function eachSource( +/** Exported for tests. Shows the run on the app mark, which shakes if any source failed. */ +export async function eachSource( sources: readonly SourceSummary[], progress: Progress, write: (source: SourceSummary) => Promise, ): Promise { + if (sources.length === 0) { + return; + } let done = 0; let failed = 0; const queue = [...sources]; - const worker = async (): Promise => { - for (let source = queue.shift(); source; source = queue.shift()) { - try { - await write(source); - } catch { - failed += 1; - } - done += 1; - progress({ done, total: sources.length, failed }); - } - }; - await Promise.all(Array.from({ length: Math.min(4, sources.length) }, worker)); + await withMarkProgress( + async (mark) => { + const worker = async (): Promise => { + for (let source = queue.shift(); source; source = queue.shift()) { + try { + await write(source); + } catch { + failed += 1; + } + done += 1; + mark(markFraction(done, sources.length)); + progress({ done, total: sources.length, failed }); + } + }; + await Promise.all(Array.from({ length: Math.min(4, sources.length) }, worker)); + }, + () => (failed > 0 ? "fail" : "finish"), + ); } diff --git a/apps/reader/src/use-source-fields.ts b/apps/reader/src/use-source-fields.ts index aeb6f5f..64bdfcc 100644 --- a/apps/reader/src/use-source-fields.ts +++ b/apps/reader/src/use-source-fields.ts @@ -1,3 +1,4 @@ +import { signalMdbaseMark } from "@mdbase-dev/ui/mark-activity"; import { useCallback, useState } from "react"; import { readerErrorMessage } from "./errors.js"; @@ -37,7 +38,9 @@ export function useSourceFields(input: { setErrorState({ sourceId, value: null }); try { onSaved(await saveFields(sourceId, fields)); + signalMdbaseMark("saved"); } catch (reason) { + signalMdbaseMark("error"); setErrorState({ sourceId, value: readerErrorMessage(reason, "Reader could not save these details."), diff --git a/apps/reader/src/use-source-import.ts b/apps/reader/src/use-source-import.ts index fb65e55..5a6c3e2 100644 --- a/apps/reader/src/use-source-import.ts +++ b/apps/reader/src/use-source-import.ts @@ -1,5 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; +import { markFraction, withMarkProgress } from "./mark-activity.js"; + import type { FileImportSuggestion } from "./file-import-suggestion.js"; import type { ReaderWorkspaceController } from "./use-reader-workspace.js"; import type { Source, SourceId, SourceImportProgress } from "@mdbase-reader/core"; @@ -73,11 +75,20 @@ export function useSourceImport( importController.current = controller; setProgress(null); try { - const imported = await workspace.importSourceFile(importRequest(file, title, found), { - signal: controller.signal, - onProgress: setProgress, - ...(recoveryFile.current === file ? { recoverExistingFiles: true } : {}), - }); + // The workspace reports failures and returns null; a stopped import ends quietly. + const imported = await withMarkProgress( + (mark) => + workspace.importSourceFile(importRequest(file, title, found), { + signal: controller.signal, + onProgress: (value) => { + mark(markFraction(value.completedBytes, value.totalBytes)); + setProgress(value); + }, + ...(recoveryFile.current === file ? { recoverExistingFiles: true } : {}), + }), + (result) => (result ? "finish" : controller.signal.aborted ? "cancel" : "fail"), + controller.signal, + ); if (imported) { recoveryFile.current = null; if (useCitation) { diff --git a/apps/reader/src/use-web-capture.ts b/apps/reader/src/use-web-capture.ts index 2ef4596..affa665 100644 --- a/apps/reader/src/use-web-capture.ts +++ b/apps/reader/src/use-web-capture.ts @@ -1,3 +1,4 @@ +import { signalMdbaseMark, useMdbaseMarkBusy } from "@mdbase-dev/ui/mark-activity"; import { useCallback, useRef, useState } from "react"; import { readerErrorMessage } from "./errors.js"; @@ -37,6 +38,8 @@ export function useWebCapture( } | null>(null); const [notice, setNotice] = useState(null); const recoveryInput = useRef(null); + // Fetching, reading and looking up a page has no measurable progress. + useMdbaseMarkBusy(status === "capturing" && "scan"); const run = useCallback( async ( @@ -69,8 +72,11 @@ export function useWebCapture( if (outcome.kind === "citation-only") { setOffer({ candidate: outcome.candidate, url: input, reason: outcome.reason }); } else if (outcome.kind === "not-added") { + // The workspace reports why; the input is kept to recover the upload on retry. + signalMdbaseMark("error"); recoveryInput.current = input; } else { + signalMdbaseMark("saved"); recoveryInput.current = null; if (outcome.notices.length) { setNotice({ message: outcome.notices.join(" "), sourceId: outcome.source.id }); @@ -79,6 +85,7 @@ export function useWebCapture( } } } catch (reason) { + signalMdbaseMark("error"); setError(readerErrorMessage(reason, "Reader could not add that source.")); } finally { setStatus("idle"); diff --git a/packages/markdown-editor/package.json b/packages/markdown-editor/package.json index cf8aa1c..95a67df 100644 --- a/packages/markdown-editor/package.json +++ b/packages/markdown-editor/package.json @@ -21,7 +21,7 @@ "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.43.8", "@lezer/highlight": "^1.2.3", - "@mdbase-dev/ui": "0.1.0-beta.123", + "@mdbase-dev/ui": "0.1.0-beta.124", "@mdbase-reader/core": "workspace:*", "react": "^19.2.8" }, diff --git a/packages/ui/package.json b/packages/ui/package.json index a486888..4e8a581 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -14,7 +14,7 @@ "dependencies": { "@fontsource/atkinson-hyperlegible": "^5.3.0", "@fontsource/azeret-mono": "^5.3.0", - "@mdbase-dev/ui": "0.1.0-beta.123", + "@mdbase-dev/ui": "0.1.0-beta.124", "@phosphor-icons/react": "^2.1.10", "react": "^19.2.8" }, diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css index 029ea37..093e273 100644 --- a/packages/ui/src/styles.css +++ b/packages/ui/src/styles.css @@ -1,6 +1,7 @@ @import "@mdbase-dev/ui/fonts.css"; @import "@mdbase-dev/ui/tokens.css"; @import "@mdbase-dev/ui/brand.css"; +@import "@mdbase-dev/ui/motion.css"; @import "@mdbase-dev/ui/controls.css"; @import "@mdbase-dev/ui/screens.css"; @import "@mdbase-dev/ui/palette.css"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ff42e9..64e7cee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,8 +93,8 @@ importers: apps/extension: dependencies: '@mdbase-dev/ui': - specifier: 0.1.0-beta.123 - version: 0.1.0-beta.123(@codemirror/view@6.43.8)(react@19.2.8) + specifier: 0.1.0-beta.124 + version: 0.1.0-beta.124(@codemirror/view@6.43.8)(react@19.2.8) '@mdbase-reader/connect': specifier: workspace:* version: link:../../packages/connect @@ -139,8 +139,8 @@ importers: specifier: 0.8.2 version: 0.8.2(@citation-js/core@0.8.2) '@mdbase-dev/ui': - specifier: 0.1.0-beta.123 - version: 0.1.0-beta.123(@codemirror/view@6.43.8)(react@19.2.8) + specifier: 0.1.0-beta.124 + version: 0.1.0-beta.124(@codemirror/view@6.43.8)(react@19.2.8) '@mdbase-reader/connect': specifier: workspace:* version: link:../../packages/connect @@ -276,8 +276,8 @@ importers: specifier: ^1.2.3 version: 1.2.3 '@mdbase-dev/ui': - specifier: 0.1.0-beta.123 - version: 0.1.0-beta.123(@codemirror/view@6.43.8)(react@19.2.8) + specifier: 0.1.0-beta.124 + version: 0.1.0-beta.124(@codemirror/view@6.43.8)(react@19.2.8) '@mdbase-reader/core': specifier: workspace:* version: link:../core @@ -416,8 +416,8 @@ importers: specifier: ^5.3.0 version: 5.3.0 '@mdbase-dev/ui': - specifier: 0.1.0-beta.123 - version: 0.1.0-beta.123(@codemirror/view@6.43.8)(react@19.2.8) + specifier: 0.1.0-beta.124 + version: 0.1.0-beta.124(@codemirror/view@6.43.8)(react@19.2.8) '@phosphor-icons/react': specifier: ^2.1.10 version: 2.1.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -1151,8 +1151,8 @@ packages: '@mdbase-dev/connect@0.1.0-beta.123': resolution: {integrity: sha512-LquiTAhumNaE2E96RimFPaxMcClH28cgYPtGPAAWD5L+PQjcv6aR8m/h7s0k933W0sJjY0BMkRjtX+aFu4fo5g==} - '@mdbase-dev/ui@0.1.0-beta.123': - resolution: {integrity: sha512-KaHQfUelNoXWtGgvde6li1IWtImckBML9FQ4cJ3DFXlHD70hedYF8ly7mz3U4TRu98RNCPCVWmugXaP3pE/LAw==} + '@mdbase-dev/ui@0.1.0-beta.124': + resolution: {integrity: sha512-75xiQ9PhKjVyk9R+xaE0APpNn5MprfrkXwXhVQM5eT3uTCCZ0YwFCTeIAFsjEB6MNItp4pujoyebSIPQKwXg7w==} peerDependencies: '@codemirror/view': ^6.0.0 react: ^19.2.0 @@ -4327,7 +4327,7 @@ snapshots: dependencies: '@mdbase-dev/connect-protocol': 0.1.0-beta.123 - '@mdbase-dev/ui@0.1.0-beta.123(@codemirror/view@6.43.8)(react@19.2.8)': + '@mdbase-dev/ui@0.1.0-beta.124(@codemirror/view@6.43.8)(react@19.2.8)': dependencies: '@fontsource/atkinson-hyperlegible': 5.3.0 '@fontsource/azeret-mono': 5.3.0