-
-
-
-
+
+ {paragraphs.map((lines, paragraph) => (
+ // biome-ignore lint/suspicious/noArrayIndexKey: the paragraphs are a fixed set, in order.
+
{lines.map((width, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: the lines are a fixed set, in order.
))}
-
+ ))}
)
}
diff --git a/apps/web/src/features/conversation/components/HeaderFrame.tsx b/apps/web/src/features/conversation/components/HeaderFrame.tsx
new file mode 100644
index 0000000..f62d332
--- /dev/null
+++ b/apps/web/src/features/conversation/components/HeaderFrame.tsx
@@ -0,0 +1,32 @@
+import { cn } from '../../../shared/cn.ts'
+import { SkeletonText } from '../../../shared/ui/Skeleton.tsx'
+
+/** The conversation header's row, shared with its frame so the two sit in the same place. */
+export const headerBar = cn(
+ // Out of the centered column to the panel's edges.
+ 'group/header sticky top-0 isolate z-1 mx-[calc((100%-100cqi)/2)] flex min-h-bar items-center gap-4 px-2.75 transition-[padding-inline-start] duration-(--dur-slide) ease-slide before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:-bottom-6 before:-z-1 before:bg-panel motion-reduce:transition-none before:[mask-image:linear-gradient(black_calc(100%-(--spacing(10))),transparent)]',
+ // The title moves aside for the sidebar's toggle as the sidebar slides away, and back, and
+ // stays clear of it on a narrow screen, where the sidebar is a drawer.
+ 'in-data-sidebar-collapsed:ps-[calc(--spacing(2.75)+var(--spacing-toggle)+--spacing(1.5))] max-wide:ps-[calc(--spacing(2.75)+var(--spacing-toggle)+--spacing(1.5))]',
+)
+
+/** The title on its way, a line where the title's text will be. */
+export function TitleSkeleton() {
+ return (
+
+
+
+ )
+}
+
+/**
+ * The header while the conversation's code loads: its row with the title on its way, which keeps
+ * the messages' skeleton clear of the sidebar's toggle, where the header will be.
+ */
+export function HeaderFrame() {
+ return (
+
+ )
+}
diff --git a/apps/web/src/features/sharing/pages/SharedStates.tsx b/apps/web/src/features/sharing/pages/SharedStates.tsx
index 34088e1..6543f6e 100644
--- a/apps/web/src/features/sharing/pages/SharedStates.tsx
+++ b/apps/web/src/features/sharing/pages/SharedStates.tsx
@@ -2,7 +2,7 @@ import type { ErrorComponentProps } from '@tanstack/react-router'
import type { ReactNode } from 'react'
import { RouteError } from '../../../shared/route-states.tsx'
import { Loading, SkeletonText } from '../../../shared/ui/Skeleton.tsx'
-import { TurnSkeleton } from '../../conversation/components/ConversationSkeleton.tsx'
+import { MessagesSkeleton } from '../../conversation/components/ConversationSkeleton.tsx'
/*
* The shared page's frame, its skeleton, and its error page. The route names its pending and error
@@ -26,9 +26,9 @@ export function SharedColumn(props: ColumnProps) {
}
/**
- * The shared chat on its way: its title and who shared it, over its first turn. It is the route's
- * pending page, which the server sends and the router otherwise holds back until loading takes a
- * while, so it shows at once.
+ * The shared chat on its way: its title and who shared it, over a few paragraphs of text. It is
+ * the route's pending page, which the server sends and the router otherwise holds back until
+ * loading takes a while, so it shows at once.
*/
export function SharedPageSkeleton() {
return (
@@ -38,7 +38,7 @@ export function SharedPageSkeleton() {
-
+
)
diff --git a/specs/web-ui.md b/specs/web-ui.md
index f25ef52..276417d 100644
--- a/specs/web-ui.md
+++ b/specs/web-ui.md
@@ -99,10 +99,10 @@ The root route's `beforeLoad` asks `/api/me` who is signed in, through the query
- Plugins: one form with a card per plugin and a single Save, which saves every plugin's settings and any tool switches that changed. A card has a switch for each tool the user may switch, labeled "Enabled" when it is the plugin's only one and by tool name otherwise, with the tool's description, then the fields generated from the plugin's JSON schema. When no plugin has settings or switches for the user, the section says so rather than showing nothing.
- Sync: a note saying where the user's chats are stored, their PDS or this server, then the background sync switch, shown only when the admin allows users to opt out, and a button that deletes and rebuilds the browser store. The switch moves as soon as it is clicked.
- **Loading.** Content on its way shows a skeleton in its own shape, which fades in only after a quiet wait, rather than the words "Loading…" or an empty space:
- - The page's frame, which the server sends for the chat routes, has the sidebar's logo and toggle over skeletons of New chat, the chats, and the account card, the composer's frame at the foot of the panel, which takes typing at once, and an open chat's turn above it, and the chat list keeps the rows' skeleton until the local copy answers, or, when it has no chats yet, until the chat index first syncs. While another tab holds the copy, the banner says why instead.
+ - The page's frame, which the server sends for the chat routes, has the sidebar's logo and toggle over skeletons of New chat, the chats, and the account card, the composer's frame at the foot of the panel, which takes typing at once, and an open chat's header, with a line for its title, over its paragraphs, which keeps them clear of the sidebar's toggle on a narrow screen, and the chat list keeps the rows' skeleton until the local copy answers, or, when it has no chats yet, until the chat index first syncs. While another tab holds the copy, the banner says why instead.
- A search shows result rows until it first answers, so it never says that nothing matched before it has looked.
- The account card shows its avatar's tile and a line for the name until the profile answers.
- - A conversation shows a line for its title and the shape of its first turn until the local copy answers, or, when it has no messages, until the PDS has, so a titled chat never flashes "New chat". On a reload, the turn in the page's frame shows at once, and the conversation's column and the conversation carry it on in turn, so it never vanishes to fade in again.
+ - A conversation shows a line for its title and a few paragraphs of text lines, with no bubble or name, until the local copy answers, or, when it has no messages, until the PDS has, so a titled chat never flashes "New chat". On a reload, the paragraphs in the page's frame show at once, and the conversation's column and the conversation carry it on in turn, so it never vanishes to fade in again.
- The composer's and Preferences' model selects hold a line in place of the name until the models load.
- The sharing panel, the shared page, which the server also sends as its skeleton, settings and admin pages, the users list, and the Sync page's background switch each show their own shapes.