fix(ui): close wave 3b QA coverage gaps

Restore the Files workspace route, make the slice Docs browser stack on phone widths, lower Storytelling card vellum opacity, and hide slice-derived feed cards for completed missions.

Tests: npm --workspace packages/ui test -- slice-20-mobile-density.test.tsx slice-16-typography-vellum.test.tsx storytelling-mark-complete.test.tsx storytelling-cards.test.tsx

Tests: npm --workspace packages/ui run build

SC-29: NONE
This commit is contained in:
mvschwarz committed 2026-05-11 15:17:33 -07:00
1 parent 9757bceb93
commit 8d9d86f53f
6 files changed
+130 -11

No files matched your search

@@ -56,7 +56,7 @@ export function CardShell({ testId, kind, title, oneLiner, accent, expanded, dri
data-testid={testId}
data-card-kind={kind}
data-expanded={open}
className={`border border-outline-variant border-l-4 ${accent.stripe} bg-white/85 hard-shadow`}
className={`border border-outline-variant border-l-4 ${accent.stripe} bg-white/35 hard-shadow`}
>
<div
role="button"
@@ -78,7 +78,7 @@ export function CardShell({ testId, kind, title, oneLiner, accent, expanded, dri
{leadingAccessory}
<span className="truncate">{title}</span>
</h3>
<p data-testid={`${testId}-one-liner`} className="mt-0.5 text-[11px] leading-relaxed text-stone-700 line-clamp-2">
<p data-testid={`${testId}-one-liner`} className="mt-0.5 font-body text-[11px] leading-relaxed text-stone-700 line-clamp-2">
{oneLiner}
</p>
</div>
@@ -343,7 +343,7 @@ export function ApprovalCard({ source }: { source: ApprovalCardSource }) {
}
expanded={
source.bodyPreview ? (
<pre data-testid={`feed-card-approval-${source.qitemId}-body`} className="overflow-x-auto whitespace-pre-wrap break-words bg-stone-50 p-2 font-mono text-[10px] text-stone-800">
<pre data-testid={`feed-card-approval-${source.qitemId}-body`} className="overflow-x-auto whitespace-pre-wrap break-words bg-stone-50 p-2 font-body text-[11px] leading-relaxed text-stone-800">
{source.bodyPreview}
</pre>
) : (
@@ -437,6 +437,7 @@ export interface AdapterMissionRow {
}
export interface AdapterSliceRow {
name: string;
missionId?: string | null;
displayName?: string;
status?: string | null;
lastActivityAt?: string | null;
@@ -458,10 +459,14 @@ export function buildStorytellingFeedItems(
// GET /api/missions/:id and threaded onto the row). The status
// filter is the source-of-truth survival across browser/localStorage
// reset; the local set is the instant-feedback mirror.
const hiddenMissionIds = new Set(completedMissionIds ? Array.from(completedMissionIds) : []);
for (const mission of missions ?? []) {
if ((mission.status ?? "").toLowerCase() === "complete") {
hiddenMissionIds.add(mission.name);
}
}
const eligibleMissions = (missions ?? []).filter((m) => {
if (m.status === "complete") return false;
if (completedMissionIds && completedMissionIds.has(m.name)) return false;
return true;
return !hiddenMissionIds.has(m.name);
});
for (const mission of eligibleMissions.slice(0, 2)) {
items.push({
@@ -475,7 +480,10 @@ export function buildStorytellingFeedItems(
},
});
}
for (const slice of (slices ?? []).slice(0, 3)) {
const eligibleSlices = (slices ?? []).filter((slice) => {
return !slice.missionId || !hiddenMissionIds.has(slice.missionId);
});
for (const slice of eligibleSlices.slice(0, 3)) {
const oneLiner = slice.lastActivityAt
? `Last activity ${slice.lastActivityAt}`
: `Slice in ${slice.status ?? "unknown"} state`;
@@ -507,7 +515,7 @@ export function StorytellingFeed({
}) {
if (items.length === 0) {
return (
<div data-testid="storytelling-feed-empty" className="border border-dashed border-outline-variant bg-white/35 p-4 font-mono text-[10px] text-stone-500">
<div data-testid="storytelling-feed-empty" className="border border-dashed border-outline-variant bg-white/35 p-4 font-body text-[11px] text-stone-500">
No items in the feed.
</div>
);
@@ -29,8 +29,8 @@ export function DocsTab({ sliceName, tree }: { sliceName: string; tree: DocsTree
const doc = useSliceDoc(sliceName, selected);
return (
<div data-testid="docs-tab" className="flex h-full">
<aside className="w-56 shrink-0 overflow-y-auto border-r border-stone-200 bg-stone-50 p-2" data-testid="docs-tree">
<div data-testid="docs-tab" className="flex h-full flex-col sm:flex-row">
<aside className="w-full max-h-48 shrink-0 overflow-y-auto border-b border-stone-200 bg-stone-50 p-2 sm:w-56 sm:max-h-none sm:border-b-0 sm:border-r" data-testid="docs-tree">
{tree.length === 0 && (
<div className="font-mono text-[10px] text-stone-400">Empty slice folder.</div>
)}
+8
View File
@@ -45,6 +45,7 @@ import { SkillsIndexPage } from "./components/specs/SkillsIndexPage.js";
import { PluginsIndexPage } from "./components/specs/PluginsIndexPage.js";
// Phase 3a slice 3.3 — plugin detail page route.
import { PluginDetailPage } from "./components/specs/PluginDetailPage.js";
import { FilesWorkspace } from "./components/files/FilesWorkspace.js";
import { SettingsCenter } from "./components/system/SettingsCenter.js";
import {
HostScopePage,
@@ -179,6 +180,12 @@ const specsPluginsIndexRoute = createRoute({
component: PluginsIndexPage,
});
const filesRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/files",
component: FilesWorkspace,
});
// Phase 3a slice 3.3 — Plugin detail page mounted at /plugins/:pluginId.
// Library Explorer's Plugins section links here; AgentSpec Plugins block
// (Batch 1) navigates here too via the "view in library" affordance.
@@ -428,6 +435,7 @@ const routeTree = rootRoute.addChildren([
specsSkillRoute,
specsSkillFileRoute,
pluginDetailRoute,
filesRoute,
specsKindRoute,
settingsRoute,
searchRoute,
@@ -58,6 +58,22 @@ describe("slice 16: FeedCard typography + vellum density", () => {
});
});
describe("slice 16: Storytelling preview typography + vellum density", () => {
it("Storytelling CardShell vellum opacity is in DESIGN §Vellum range", async () => {
const source = await readSource("src/components/feed/cards/storytelling-cards.tsx");
expect(source).toMatch(/bg-white\/35 hard-shadow/);
expect(source).not.toMatch(/bg-white\/85 hard-shadow/);
});
it("Storytelling prose preview uses font-body while metadata labels stay font-mono", async () => {
const source = await readSource("src/components/feed/cards/storytelling-cards.tsx");
expect(source).toMatch(/font-body text-\[11px\] leading-relaxed text-stone-700 line-clamp-2/);
expect(source).toMatch(/inline-block border px-2 py-0\.5 font-mono text-\[8px\] uppercase/);
expect(source).toMatch(/whitespace-pre-wrap break-words bg-stone-50 p-2 font-body text-\[11px\] leading-relaxed/);
expect(source).not.toMatch(/whitespace-pre-wrap break-words bg-stone-50 p-2 font-mono text-\[10px\] text-stone-800/);
});
});
describe("slice 16: TimelineTab event body is prose font-body", () => {
it("event body <pre> uses font-body (preserves whitespace via whitespace-pre-wrap, font is body)", async () => {
const source = await readSource("src/components/slices/tabs/TimelineTab.tsx");
@@ -1,11 +1,13 @@
// Slice 20 mobile regression tests.
//
// Two surfaces fixed:
// Three surfaces fixed:
// (a) FilesWorkspace two-pane shape stacks vertically on narrow
// viewports (< sm breakpoint) so the document panel claims full
// width on a phone.
// (b) AppShell mobile slide-over Rail renders vertical (already
// covered by app-shell.test.tsx slice-20 cases).
// (c) Slice Artifacts Docs Browser uses the same responsive two-pane
// shape; this is the actual rendered path under /project/slice/:id.
//
// (a) is asserted here via source-level scan (the component fetches
// data via hooks; full DOM render would require deep mocking). Source
@@ -26,6 +28,12 @@ async function readSource(packageRelPath: string): Promise<string> {
}
describe("slice 20: FilesWorkspace responsive two-pane shape", () => {
it("/files route mounts FilesWorkspace instead of falling through to Not Found", async () => {
const source = await readSource("src/routes.tsx");
expect(source).toMatch(/import \{ FilesWorkspace \} from "\.\/components\/files\/FilesWorkspace\.js";/);
expect(source).toMatch(/path: "\/files",\n\s+component: FilesWorkspace/);
});
it("outer container uses flex-col on mobile + sm:flex-row on desktop", async () => {
const source = await readSource("src/components/files/FilesWorkspace.tsx");
// The outer flex parent that owns the two-pane shape now stacks
@@ -47,6 +55,22 @@ describe("slice 20: FilesWorkspace responsive two-pane shape", () => {
});
});
describe("slice 20: DocsTab responsive two-pane shape", () => {
it("slice Artifacts Docs Browser stacks vertically on mobile + restores row at sm:", async () => {
const source = await readSource("src/components/slices/tabs/DocsTab.tsx");
expect(source).toMatch(/className="flex h-full flex-col sm:flex-row"/);
});
it("docs tree pane is full-width on mobile + capped + scrollable; restores w-56 at sm:", async () => {
const source = await readSource("src/components/slices/tabs/DocsTab.tsx");
expect(source).toMatch(/w-full max-h-48 shrink-0 overflow-y-auto/);
expect(source).toMatch(/sm:w-56/);
expect(source).toMatch(/sm:max-h-none/);
expect(source).toMatch(/border-b border-stone-200/);
expect(source).toMatch(/sm:border-b-0 sm:border-r/);
});
});
describe("slice 20: AppShell mobile slide-over hamburger orientation", () => {
it("Rail invocation inside mobile slide-over uses `vertical` (not vertical={false})", async () => {
const source = await readSource("src/components/AppShell.tsx");
@@ -14,6 +14,7 @@ interface MissionRow {
}
interface SliceRow {
name: string;
missionId?: string | null;
displayName?: string;
status?: string;
lastActivityAt?: string;
@@ -138,5 +139,67 @@ describe("buildStorytellingFeedItems — filter completed missions (slice 18 Che
(progressItems[0] as { source: { missionId: string } }).source.missionId,
).toBe("m3");
});
it("filters slice-derived cards from missions with status === 'complete'", () => {
const withStatus: MissionRow[] = [
{ name: "getting-started", path: "/m/getting-started", status: "complete" },
{ name: "release-0-3-1", path: "/m/release-0-3-1", status: "active" },
];
const slicesWithMissions: SliceRow[] = [
{
name: "first-conveyor-run",
missionId: "getting-started",
displayName: "First Conveyor Run",
status: "blocked",
},
{
name: "slice-16-typography",
missionId: "release-0-3-1",
displayName: "Slice 16 Typography",
status: "done",
},
];
const items = buildStorytellingFeedItems(withStatus, slicesWithMissions);
expect(items).not.toContainEqual(
expect.objectContaining({
source: expect.objectContaining({ sliceId: "first-conveyor-run" }),
}),
);
expect(items).toContainEqual(
expect.objectContaining({
source: expect.objectContaining({ sliceId: "slice-16-typography" }),
}),
);
});
it("filters slice-derived cards from locally completed missions before durable status loads", () => {
const activeMissions: MissionRow[] = [
{ name: "getting-started", path: "/m/getting-started", status: "active" },
{ name: "release-0-3-1", path: "/m/release-0-3-1", status: "active" },
];
const slicesWithMissions: SliceRow[] = [
{ name: "intro", missionId: "getting-started", status: "active" },
{ name: "ship", missionId: "release-0-3-1", status: "shipped" },
];
const items = buildStorytellingFeedItems(
activeMissions,
slicesWithMissions,
new Set(["getting-started"]),
);
expect(items).not.toContainEqual(
expect.objectContaining({
source: expect.objectContaining({ sliceId: "intro" }),
}),
);
expect(items).toContainEqual(
expect.objectContaining({
source: expect.objectContaining({ sliceId: "ship" }),
}),
);
});
});
});