mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 17:32:41 -10:00
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:
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>
|
||||
)}
|
||||
|
||||
@@ -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" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user