diff --git a/sideboard/src/index.css b/sideboard/src/index.css
index d90909f7..921509d1 100644
--- a/sideboard/src/index.css
+++ b/sideboard/src/index.css
@@ -378,6 +378,19 @@ button {
flex-direction: column;
}
+.document-list__error {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ padding: 0.75rem;
+ color: var(--document-list-danger);
+}
+
+.document-list__error span {
+ color: var(--document-list-muted);
+ overflow-wrap: anywhere;
+}
+
.document-list-item--loading {
pointer-events: none;
padding-left: calc(1.8rem * var(--depth) + var(--doclist-padding-x));
diff --git a/sideboard/src/sideboard/document-list-panel.test.tsx b/sideboard/src/sideboard/document-list-panel.test.tsx
new file mode 100644
index 00000000..2cdd7b01
--- /dev/null
+++ b/sideboard/src/sideboard/document-list-panel.test.tsx
@@ -0,0 +1,40 @@
+import { render } from "solid-js/web";
+import { afterEach, describe, expect, it } from "vitest";
+
+import { DocumentListError, isFolderDoc } from "./document-list-panel.tsx";
+
+let dispose: (() => void) | undefined;
+
+afterEach(() => {
+ dispose?.();
+ dispose = undefined;
+ document.body.replaceChildren();
+});
+
+describe("document-list panel errors", () => {
+ it("recognizes folder-shaped documents", () => {
+ expect(isFolderDoc({ title: "Root", docs: [] })).toBe(true);
+ expect(isFolderDoc({ title: "Not a folder" } as any)).toBe(false);
+ expect(isFolderDoc(undefined)).toBe(false);
+ });
+
+ it("renders visible error details", () => {
+ const root = document.createElement("div");
+ document.body.append(root);
+
+ dispose = render(
+ () => (
+
+ ),
+ root
+ );
+
+ expect(root.querySelector('[role="alert"]')?.textContent).toContain(
+ "Could not load root folder."
+ );
+ expect(root.textContent).toContain("automerge:missing");
+ });
+});
diff --git a/sideboard/src/sideboard/document-list-panel.tsx b/sideboard/src/sideboard/document-list-panel.tsx
index 0f18f73b..ad0b60bb 100644
--- a/sideboard/src/sideboard/document-list-panel.tsx
+++ b/sideboard/src/sideboard/document-list-panel.tsx
@@ -23,6 +23,24 @@ import { executeDrop } from "./dnd/operations.ts";
import { getDndPayload, hasDocumentDrag } from "./dnd/payload.ts";
import { createMarquee } from "./document-list/marquee.ts";
+export function isFolderDoc(doc: FolderDoc | undefined): doc is FolderDoc {
+ return !!doc && Array.isArray(doc.docs);
+}
+
+export function DocumentListError(props: {
+ message: string;
+ detail?: string;
+}) {
+ return (
+
+ {props.message}
+
+ {props.detail}
+
+
+ );
+}
+
/**
* The document-list panel: a sticky toolbar (new-doc button + filter) over a
* scrolling, recursive list of the folder's documents and subfolders. Renders a
@@ -46,6 +64,26 @@ export function DocumentListPanel(props: {
// handle happens to resolve synchronously.
const folderReady = () =>
folderHandle.state === "ready" || folderHandle.state === "refreshing";
+ const folderLoadError = () => {
+ if (folderHandle.state === "errored") {
+ const error = folderHandle.error;
+ return {
+ message: "Could not load root folder.",
+ detail:
+ error instanceof Error
+ ? error.message
+ : error
+ ? String(error)
+ : props.folderUrl,
+ };
+ }
+ if (folderReady() && !isFolderDoc(folder())) {
+ return {
+ message: "Root folder is not a folder.",
+ detail: props.folderUrl,
+ };
+ }
+ };
const selectedDocUrls = subscribe(
props.element,
@@ -189,43 +227,57 @@ export function DocumentListPanel(props: {
);
}}
>
-
- }>
- }>
- setFilter("")}
- />
-
+
+
+ }>
+ }>
+ setFilter("")}
+ />
+
+
+ >
+ }
+ >
+ {(error) => (
+
+ )}
diff --git a/sideboard/src/sideboard/sideboard.tsx b/sideboard/src/sideboard/sideboard.tsx
index b6d6487d..a70a3901 100644
--- a/sideboard/src/sideboard/sideboard.tsx
+++ b/sideboard/src/sideboard/sideboard.tsx
@@ -5,8 +5,7 @@ import type { DocHandle, Repo } from "@automerge/automerge-repo/slim";
import type { PatchworkViewElement } from "@inkandswitch/patchwork-elements";
import type { PatchworkToolProps, SideboardAccountDoc } from "../types.ts";
-import { DocumentListPanel } from "./document-list-panel.tsx";
-import { LoadingRows } from "./document-list/loading-row.tsx";
+import { DocumentListError, DocumentListPanel } from "./document-list-panel.tsx";
import { AccountBar } from "./account-bar.tsx";
/**
@@ -23,7 +22,7 @@ export function Sideboard(props: PatchworkToolProps) {
fallback={
}