Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions client/src/components/ui/PageSkeleton.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,29 @@ describe('PageSkeleton', () => {
expect(container.innerHTML).not.toContain('px-3 py-2 sm:px-4 sm:py-3');
});

it('owns the height on a fullHeight bar page and gives the body the scroll', () => {
// The bar branch keeps the shell `h-full` and puts `overflow-y-auto` on the
// BODY, not the root — a full-bleed page's header bar must not scroll away.
const { container } = render(<PageSkeleton header="bar" fullHeight padded bodyClassName="p-4" />);
expect(status().className).toContain('h-full');
expect(status().className).not.toContain('overflow-y-auto');
const bodyRegion = container.querySelector('.flex-1.min-h-0');
expect(bodyRegion.className).toContain('overflow-y-auto');
expect(bodyRegion.className).toContain('p-4');
});

it('reserves a card grid under a page-supplied header for layout="grid" + header="none"', () => {
const { container } = render(
<PageSkeleton header="none" layout="grid" gridColsClass="lg:grid-cols-3" cards={3} />
);
// No title/action placeholders — the page already painted its own chrome.
expect(cardCount(container)).toBe(3);
expect(container.innerHTML).toContain('lg:grid-cols-3');
expect(container.innerHTML).not.toContain('lg:grid-cols-[1fr_360px]');
// 3 cards x (1 title + 2 body lines) and nothing else.
expect(container.querySelectorAll('.animate-pulse')).toHaveLength(9);
});

it('omits body padding on a full-bleed tab even in bar mode', () => {
const { container } = render(<PageSkeleton header="bar" padded={false} bodyClassName="p-4" />);
const bodyRegion = container.querySelector('.flex-1.min-h-0');
Expand Down
5 changes: 3 additions & 2 deletions client/src/pages/AIProviders.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from '../utils/formatters';
import SettingsTabsHeader from '../components/settings/SettingsTabsHeader';
import PageHeader from '../components/PageHeader';
import PageSkeleton from '../components/ui/PageSkeleton';
import OverflowMenu from '../components/ui/OverflowMenu';
import EffortSelect from '../components/cos/EffortSelect';
import Drawer from '../components/Drawer';
Expand Down Expand Up @@ -692,8 +693,8 @@ export default function AIProviders() {
<div className="flex flex-col h-full">
<PageHeader icon={Bot} title="AI Providers" />
<SettingsTabsHeader activeTab="providers" />
<div className="flex-1 flex items-center justify-center">
<div className="text-gray-400">Loading providers...</div>
<div className="flex-1 overflow-auto p-4">
<PageSkeleton header="none" label="Loading providers" layout="grid" cards={4} />
</div>
</div>
);
Expand Down
11 changes: 10 additions & 1 deletion client/src/pages/AgentsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { RefreshCw, Activity, XCircle, Cpu, MemoryStick, Terminal } from 'lucide
import * as api from '../services/api';
import { useAutoRefetch } from '../hooks/useAutoRefetch';
import { formatDateTime } from '../utils/formatters';
import PageSkeleton from '../components/ui/PageSkeleton';

export function AgentsPage() {
const [killing, setKilling] = useState({});
Expand Down Expand Up @@ -32,7 +33,15 @@ export function AgentsPage() {
const totalMemory = agents.reduce((sum, a) => sum + (a.memory || 0), 0);

if (loading) {
return <div className="text-center py-8 text-gray-400">Scanning for AI agents...</div>;
return (
<PageSkeleton
label="Scanning for AI agents"
headerRowClass="flex flex-col sm:flex-row sm:items-center justify-between gap-3"
titleWidthClass="w-56"
cards={3}
sidebar={false}
/>
);
}

return (
Expand Down
10 changes: 8 additions & 2 deletions client/src/pages/BrainScanReport.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Link, useParams } from 'react-router';
import { ArrowLeft, FileText, RefreshCw, ShieldAlert, ShieldCheck, Skull } from 'lucide-react';
import * as api from '../services/api';
import MarkdownOutput from '../components/cos/MarkdownOutput';
import BrailleSpinner from '../components/BrailleSpinner';
import PageSkeleton from '../components/ui/PageSkeleton';
import { useAutoRefetch } from '../hooks/useAutoRefetch';

const VERDICT_STYLES = {
Expand Down Expand Up @@ -36,7 +36,13 @@ export default function BrainScanReport() {
if (loading) {
return (
<Shell>
<div className="flex justify-center py-16"><BrailleSpinner text="Loading scan report" /></div>
<PageSkeleton
label="Loading scan report"
headerRowClass="flex flex-wrap items-start justify-between gap-3"
titleWidthClass="w-64"
cards={1}
sidebar={false}
/>
</Shell>
);
}
Expand Down
12 changes: 9 additions & 3 deletions client/src/pages/CatalogIngredient.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Modal from '../components/ui/Modal.jsx';
import ConfirmButtonPair from '../components/ui/ConfirmButtonPair.jsx';
import UnsavedChangesConfirm from '../components/ui/UnsavedChangesConfirm.jsx';
import AutoSizeTextarea from '../components/ui/AutoSizeTextarea';
import PageSkeleton from '../components/ui/PageSkeleton';
import {
getCatalogIngredientDetails,
updateCatalogIngredient,
Expand Down Expand Up @@ -420,9 +421,14 @@ export default function CatalogIngredient() {

if (loading || !record) {
return (
<section className="h-full overflow-y-auto p-4 md:p-6">
<div className="max-w-4xl mx-auto text-sm text-gray-400">Loading ingredient…</div>
</section>
<PageSkeleton
header="none"
label="Loading ingredient"
padded
fullHeight
cards={3}
sidebar={false}
/>
);
}

Expand Down
16 changes: 15 additions & 1 deletion client/src/pages/CreativeDirector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { listUniverses } from '../services/apiUniverseBuilder.js';
import { listPipelineSeries } from '../services/apiPipeline.js';
import ModelSelect from '../components/ModelSelect';
import PageHeader from '../components/PageHeader';
import PageSkeleton from '../components/ui/PageSkeleton';
import Drawer from '../components/Drawer';
import DirectiveComposer from '../components/creative-director/DirectiveComposer.jsx';
import CreativeDirectorModelsDrawer from '../components/creative-director/CreativeDirectorModelsDrawer.jsx';
Expand Down Expand Up @@ -259,7 +260,20 @@ export default function CreativeDirector() {
};

if (loading) {
return <div className="p-6 text-port-text-muted">Loading projects…</div>;
return (
<PageSkeleton
header="bar"
label="Loading Creative Director projects"
fullHeight
padded
showSubtitle
titleWidthClass="w-56"
layout="grid"
gridColsClass="md:grid-cols-2 lg:grid-cols-3"
cards={6}
bodyClassName="p-6"
/>
);
}

return (
Expand Down
28 changes: 27 additions & 1 deletion client/src/pages/Game.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { ArrowLeft, Boxes, Gamepad2, Images, MessageSquare, Plus } from 'lucide-react';
import { Link, useNavigate, useParams } from 'react-router';
import PageSkeleton from '../components/ui/PageSkeleton';
import toast from '../components/ui/Toast';
import AppContextPicker from '../components/AppContextPicker.jsx';
import GameBindings from '../components/games/GameBindings.jsx';
Expand Down Expand Up @@ -259,7 +260,32 @@ export default function Game() {
};

if (loading) {
return <div className="py-12 text-center text-sm text-gray-400">Loading Game studio…</div>;
// The detail workspace is a full-bleed h-full shell with its own bordered
// bar; the index is a plain padded page. `id` is known before the fetch
// settles, so each reserves the chrome its own loaded state renders.
return id ? (
<PageSkeleton
header="bar"
label="Loading Game workspace"
fullHeight
padded
barClassName="px-4 py-3"
bodyClassName="p-4"
headerRowClass="flex flex-col justify-between gap-2 sm:flex-row sm:items-center"
titleWidthClass="w-48"
cards={3}
sidebar={false}
/>
) : (
<PageSkeleton
label="Loading Game studio"
titleWidthClass="w-32"
showSubtitle
showAction={false}
cards={3}
sidebar={false}
/>
);
}

if (id && !game) {
Expand Down
24 changes: 8 additions & 16 deletions client/src/pages/Insights.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import GoalScorecardTab from '../components/insights/GoalScorecardTab';
import ConfidenceBadge from '../components/insights/ConfidenceBadge';
import PageHeader from '../components/PageHeader';
import TabPills from '../components/ui/TabPills';
import PageSkeleton from '../components/ui/PageSkeleton';
import { timeAgo } from '../utils/formatters';

// Exported for the nav-manifest tab-coverage guard (server/lib/navManifest.test.js).
Expand All @@ -28,17 +29,6 @@ export const TABS = [
{ id: 'goal-scorecard', label: 'Goal Scorecard', icon: Target }
];

function SummaryCardSkeleton() {
return (
<div className="bg-port-card border border-port-border rounded-lg p-6 animate-pulse">
<div className="h-5 bg-gray-700 rounded w-2/3 mb-3" />
<div className="h-8 bg-gray-800 rounded w-1/2 mb-2" />
<div className="h-3 bg-gray-800 rounded w-full mb-1" />
<div className="h-3 bg-gray-800 rounded w-4/5" />
</div>
);
}

export function OverviewTab() {
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
Expand Down Expand Up @@ -72,11 +62,13 @@ export function OverviewTab() {

if (loading) {
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<SummaryCardSkeleton />
<SummaryCardSkeleton />
<SummaryCardSkeleton />
</div>
<PageSkeleton
header="none"
label="Loading insights overview"
layout="grid"
gridColsClass="lg:grid-cols-3"
cards={3}
/>
);
}

Expand Down
13 changes: 10 additions & 3 deletions client/src/pages/Instances.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import BrainParitySchedule from '../components/instances/BrainParitySchedule';
import TailnetHelpBanner from '../components/instances/TailnetHelpBanner';
import { timeAgo, timeUntil } from '../utils/formatters';
import { directionalCounts, describeDirectional } from '../lib/syncCounts';
import PageSkeleton from '../components/ui/PageSkeleton';

const STATUS_COLORS = {
online: 'text-port-success',
Expand Down Expand Up @@ -1295,9 +1296,15 @@ export default function Instances() {

if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading instances...</div>
</div>
<PageSkeleton
label="Loading instances"
headerRowClass="flex items-center gap-3"
titleWidthClass="w-40"
showAction={false}
layout="grid"
gridColsClass="md:grid-cols-2"
cards={4}
/>
);
}

Expand Down
11 changes: 9 additions & 2 deletions client/src/pages/Media3DDetail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ImageTo3dRenderOptions from '../components/media/ImageTo3dRenderOptions';
import { fieldsFromRun, renderOptionsBody, runWantsTransparency } from '../lib/imageTo3dRenderOptions';
import { imageTo3dStatusMeta } from '../components/media/imageTo3dStatus';
import toast from '../components/ui/Toast';
import PageSkeleton from '../components/ui/PageSkeleton';

// Poll cadence while a render is in flight (a real TRELLIS.2 render is multi-minute).
const POLL_INTERVAL_MS = 2500;
Expand Down Expand Up @@ -107,8 +108,14 @@ export default function Media3DDetail() {

if (loading) {
return (
<div className="mx-auto flex max-w-4xl items-center gap-2 py-16 text-sm text-gray-400">
<Loader2 className="h-4 w-4 animate-spin" /> Loading 3D model…
<div className="mx-auto max-w-4xl">
<PageSkeleton
label="Loading 3D model"
headerRowClass="flex flex-wrap items-center justify-between gap-3"
titleWidthClass="w-56"
cards={2}
sidebar={false}
/>
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/Models.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export default function Models() {
<div className="flex-1 min-w-0 overflow-auto p-4">
{/* Local boundary rather than the App-level one: a lazy tab must not blank
out the section header and tab bar while its chunk loads. */}
<Suspense fallback={<PageSkeleton />}>
<Suspense fallback={<PageSkeleton header="none" label="Loading models section" cards={3} sidebar={false} />}>
{DetailContent ? <DetailContent recordId={recordId} /> : <TabContent view={recordId} />}
</Suspense>
</div>
Expand Down
14 changes: 11 additions & 3 deletions client/src/pages/PipelineContinuityBible.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useEffect, useRef, useState } from 'react';
import { Link, useParams, useNavigate } from 'react-router';
import { Loader2, RefreshCw, X, ArrowLeft, BookOpen, AlertTriangle, BookMarked, Lock } from 'lucide-react';
import toast from '../components/ui/Toast';
import PageSkeleton from '../components/ui/PageSkeleton';
import {
getPipelineSeries,
getContinuityBible,
Expand Down Expand Up @@ -112,9 +113,16 @@ export default function PipelineContinuityBible() {

if (loading) {
return (
<div className="h-full flex items-center justify-center text-gray-400">
<Loader2 className="animate-spin" size={20} />
</div>
<PageSkeleton
label="Loading series continuity"
fullHeight
padded
headerRowClass="flex flex-wrap items-center gap-2"
titleWidthClass="w-56"
showAction={false}
cards={4}
sidebar={false}
/>
);
}

Expand Down
15 changes: 12 additions & 3 deletions client/src/pages/PipelineExport.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useEffect, useMemo, useState } from 'react';
import { Link, useParams, useNavigate } from 'react-router';
import { Loader2, ArrowLeft, Download, Save, BookText, FileText, FileType } from 'lucide-react';
import toast from '../components/ui/Toast';
import PageSkeleton from '../components/ui/PageSkeleton';
import {
getPipelineSeries,
updatePipelineSeries,
Expand Down Expand Up @@ -109,9 +110,17 @@ export default function PipelineExport() {

if (loading) {
return (
<div className="h-full flex items-center justify-center text-gray-400">
<Loader2 className="animate-spin" size={20} />
</div>
<PageSkeleton
label="Loading export options"
fullHeight
padded
headerRowClass="flex flex-wrap items-center gap-2"
titleWidthClass="w-56"
showAction={false}
layout="grid"
gridColsClass="lg:grid-cols-2"
cards={2}
/>
);
}

Expand Down
19 changes: 18 additions & 1 deletion client/src/pages/PipelineIssue.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
LayoutGrid, Image as ImageIcon, Clapperboard, Users, Settings, Mic, Lock, Unlock,
} from 'lucide-react';
import toast from '../components/ui/Toast';
import PageSkeleton from '../components/ui/PageSkeleton';
import Modal from '../components/ui/Modal';
import TabPills from '../components/ui/TabPills';
import {
Expand Down Expand Up @@ -287,7 +288,23 @@ export default function PipelineIssue() {
? `${PIPELINE_STAGE_LABELS[stageId]} stage is locked — unlock it to regenerate`
: ambientLockHint;

if (loading) return <div className="p-6 text-gray-500 text-sm">Loading issue…</div>;
if (loading) {
return (
<PageSkeleton
header="bar"
label="Loading pipeline issue"
fullHeight
padded
barClassName="p-4 md:p-6"
bodyClassName="p-4 md:p-6"
headerRowClass="flex items-center justify-between gap-3 flex-wrap"
titleWidthClass="w-64"
tabs={stageTabs.length}
cards={2}
sidebar={false}
/>
);
}
if (!issue) return null;

const StageComponent = STAGE_COMPONENTS[stageId];
Expand Down
Loading