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 && (