diff --git a/.changeset/olive-spoons-shake.md b/.changeset/olive-spoons-shake.md
new file mode 100644
index 0000000..f6405b2
--- /dev/null
+++ b/.changeset/olive-spoons-shake.md
@@ -0,0 +1,8 @@
+---
+'@pdsjs/cloudflare': patch
+'@pdsjs/node': patch
+---
+
+The repository page draws skeleton placeholders while it loads. The file
+listing, a file's contents, and the commit history each get the shape of the
+content they are about to show, instead of a blank panel.
diff --git a/packages/account-ui/src/components/molecules/skeleton-row.jsx b/packages/account-ui/src/components/molecules/skeleton-row.jsx
new file mode 100644
index 0000000..44bf7d0
--- /dev/null
+++ b/packages/account-ui/src/components/molecules/skeleton-row.jsx
@@ -0,0 +1,28 @@
+import { Skeleton } from '#/components/atoms/skeleton.jsx';
+import { cn } from '#/lib/utils.js';
+
+/**
+ * A plain ListRow's geometry as placeholders: the media square, the title bar,
+ * an optional second line, and an optional trailing bar. Each width prop takes
+ * a Tailwind width class, so a list of rows can vary its widths and read as
+ * text rather than as a block.
+ */
+export function SkeletonRow({
+ media = 'size-4 rounded-md',
+ title = 'w-40',
+ description,
+ action,
+}) {
+ return (
+
+
+
+
+ {description && (
+
+ )}
+
+ {action &&
}
+
+ );
+}
diff --git a/packages/account-ui/src/components/organisms/commit-log-skeleton.jsx b/packages/account-ui/src/components/organisms/commit-log-skeleton.jsx
new file mode 100644
index 0000000..5706d10
--- /dev/null
+++ b/packages/account-ui/src/components/organisms/commit-log-skeleton.jsx
@@ -0,0 +1,27 @@
+import { SkeletonRow } from '#/components/molecules/skeleton-row.jsx';
+
+/** A commit subject varies in length; the author line and the short sha do
+ * not. */
+const ROWS = [
+ { key: 'a', title: 'w-56' },
+ { key: 'b', title: 'w-40' },
+ { key: 'c', title: 'w-64' },
+ { key: 'd', title: 'w-44' },
+ { key: 'e', title: 'w-52' },
+];
+
+/** The commit list, as placeholders, shown while the history loads. */
+export function CommitLogSkeleton() {
+ return (
+
+ {ROWS.map((row) => (
+
+ ))}
+
+ );
+}
diff --git a/packages/account-ui/src/components/organisms/file-content-skeleton.jsx b/packages/account-ui/src/components/organisms/file-content-skeleton.jsx
new file mode 100644
index 0000000..11c8a67
--- /dev/null
+++ b/packages/account-ui/src/components/organisms/file-content-skeleton.jsx
@@ -0,0 +1,36 @@
+import { Skeleton } from '#/components/atoms/skeleton.jsx';
+
+/** Indent and length per line, so the block reads as code. */
+const LINES = [
+ { key: 'a', width: 'w-[38%]', indent: '' },
+ { key: 'b', width: 'w-[52%]', indent: '' },
+ { key: 'c', width: 'w-[30%]', indent: 'ml-6' },
+ { key: 'd', width: 'w-[44%]', indent: 'ml-6' },
+ { key: 'e', width: 'w-[22%]', indent: 'ml-12' },
+ { key: 'f', width: 'w-[48%]', indent: 'ml-6' },
+ { key: 'g', width: 'w-[18%]', indent: '' },
+ { key: 'h', width: 'w-[56%]', indent: '' },
+ { key: 'i', width: 'w-[34%]', indent: 'ml-6' },
+ { key: 'j', width: 'w-[26%]', indent: 'ml-6' },
+];
+
+/** A file's header line and its text, as placeholders, shown while the file
+ * loads. */
+export function FileContentSkeleton() {
+ return (
+
+
+
+
+
+
+ {LINES.map((line) => (
+
+ ))}
+
+
+ );
+}
diff --git a/packages/account-ui/src/components/organisms/file-tree-skeleton.jsx b/packages/account-ui/src/components/organisms/file-tree-skeleton.jsx
new file mode 100644
index 0000000..8bf5125
--- /dev/null
+++ b/packages/account-ui/src/components/organisms/file-tree-skeleton.jsx
@@ -0,0 +1,24 @@
+import { SkeletonRow } from '#/components/molecules/skeleton-row.jsx';
+
+/** Directory rows come first and carry no size, so the top rows have no
+ * trailing bar. The widths run short to long to read as file names. */
+const ROWS = [
+ { key: 'a', title: 'w-24' },
+ { key: 'b', title: 'w-32' },
+ { key: 'c', title: 'w-20', action: 'w-10' },
+ { key: 'd', title: 'w-44', action: 'w-12' },
+ { key: 'e', title: 'w-28', action: 'w-8' },
+ { key: 'f', title: 'w-36', action: 'w-12' },
+];
+
+/** The repository listing at a path, as placeholders, shown while the tree
+ * loads. */
+export function FileTreeSkeleton() {
+ return (
+
+ {ROWS.map((row) => (
+
+ ))}
+
+ );
+}
diff --git a/packages/account-ui/src/components/organisms/repo-page-skeleton.jsx b/packages/account-ui/src/components/organisms/repo-page-skeleton.jsx
new file mode 100644
index 0000000..64a0898
--- /dev/null
+++ b/packages/account-ui/src/components/organisms/repo-page-skeleton.jsx
@@ -0,0 +1,24 @@
+import { Skeleton } from '#/components/atoms/skeleton.jsx';
+import { FileTreeSkeleton } from '#/components/organisms/file-tree-skeleton.jsx';
+
+/** The clone commands, the breadcrumb and ref bar, and the listing, as
+ * placeholders, shown while the repository loads. */
+export function RepoPageSkeleton() {
+ return (
+
+ );
+}
diff --git a/packages/account-ui/src/pages/git-repo-page.jsx b/packages/account-ui/src/pages/git-repo-page.jsx
index 97d1c26..dab99fa 100644
--- a/packages/account-ui/src/pages/git-repo-page.jsx
+++ b/packages/account-ui/src/pages/git-repo-page.jsx
@@ -18,6 +18,10 @@ import { Notice } from '#/components/atoms/notice.jsx';
import { ListRow } from '#/components/molecules/list-row.jsx';
import { SelectField } from '#/components/molecules/select-field.jsx';
import { TextField } from '#/components/molecules/text-field.jsx';
+import { CommitLogSkeleton } from '#/components/organisms/commit-log-skeleton.jsx';
+import { FileContentSkeleton } from '#/components/organisms/file-content-skeleton.jsx';
+import { FileTreeSkeleton } from '#/components/organisms/file-tree-skeleton.jsx';
+import { RepoPageSkeleton } from '#/components/organisms/repo-page-skeleton.jsx';
import { get, post } from '#/lib/api.js';
import { bytes, timeAgo } from '#/lib/format.js';
import { queryKeys, useApiMutation } from '#/lib/query.js';
@@ -42,7 +46,7 @@ function LogView({ name, refName, space }) {
),
});
if (error) return {error.message};
- if (!data) return null;
+ if (!data) return ;
if (data.enabled === false) {
return (
@@ -363,7 +367,7 @@ function FileView({ name, refName, path, space }) {
queryFn: () => get(`/git/file?${query}`),
});
if (error) return {error.message};
- if (!data) return null;
+ if (!data) return ;
if (data.mediaType) {
return (
@@ -439,7 +443,7 @@ export function GitRepoPage({
});
if (repoError) return
{repoError.message};
- if (!repoData) return null;
+ if (!repoData) return
;
if (!repo) {
return
No repository named "{name}".;
}
@@ -562,7 +566,9 @@ export function GitRepoPage({
) : treeError ? (
{treeError.message}
- ) : !tree ? null : (
+ ) : !tree ? (
+
+ ) : (
{path && (