diff --git a/apps/web/DESIGN.md b/apps/web/DESIGN.md
index 38611e137..1055208f5 100644
--- a/apps/web/DESIGN.md
+++ b/apps/web/DESIGN.md
@@ -149,7 +149,7 @@ text only when people need it to act or recover.
## Layout
-The workspace places Chat on the left and the document on the right when space permits. Chat and its top bar (breadcrumb and document title) sit directly on the ground; the document is a raised sheet that rises to the top margin, and its tab row holds the expand control, the Document/Decisions/Build views and presence. Expanding the document hides Chat and the top bar; a Chat button in the same place restores them. Document, Decisions, and Build switch the main pane; Build is available on parent documents. Local investigation proposals, approval controls, and results appear in the document flow. The Projects sidebar slides over the document without resizing it; on narrower screens, panels take their own view. The document uses a maximum prose measure of 40.625rem (650px), with 3.75rem (60px) of top padding and at least 3.5rem (56px) of padding on each side. At browser widths of 600px or less, the horizontal padding becomes 1.5rem (24px). On a phone (a primary touch pointer with a width of 600px or less, or a height of 500px or less, so landscape phones count), the workspace drops its card and the raised sheet: panes run full-bleed with the same 1.5rem (24px) gutters, so margin comment markers keep their lane, under a translucent top bar that tucks away while reading down, above a floating bottom tab bar in the thumb zone. Landscape phones use a shorter top bar and a one-line tab bar. Destinations slide sideways in tab order, and crossfade under reduced motion. Images and diagrams can extend beyond the prose measure within those gutters; tables can grow toward the trailing gutter and retain their own scroll lane. On a phone (a coarse pointer on a small screen), when the document is narrower than 40rem, a wide table fades whichever side still hides columns, code wraps with a hanging indent instead of scrolling, and a Mermaid diagram below its readability floor offers Expand diagram under its scroll lane, which opens it in a full-screen sheet at its authored size. A narrow document alone does not trigger these: narrow desktop windows keep the scroll lanes.
+The workspace places Chat on the left and the document on the right when space permits. Chat sits directly on the ground with no bar of its own; the document is a raised sheet that rises to the top margin and carries two 40px bars. The title bar holds the expand control, the project and title with rename and the document actions menu, then status and presence, separated by full-height dividers. The views bar below it holds Document, Decisions, and Build on the paper, with no rule beneath. Expanding the document only widens the sheet: Chat collapses, the title stays, and a Chat button in the same place restores it. As the sheet narrows, the title truncates first, then status labels fold to glyphs, then presence keeps one face and a count. A child document zooms in place of its root: the title bar shows Back, the root as a crumb, and the child's own title and actions, and the views fold away because a child has none. Compact layouts and phones show the path in a slim stack bar instead. Document, Decisions, and Build switch the main pane; Build is available on parent documents. Local investigation proposals, approval controls, and results appear in the document flow. The Projects sidebar slides over the document without resizing it; on narrower screens, panels take their own view. The document uses a maximum prose measure of 40.625rem (650px), with 3.75rem (60px) of top padding and at least 3.5rem (56px) of padding on each side. At browser widths of 600px or less, the horizontal padding becomes 1.5rem (24px). On a phone (a primary touch pointer with a width of 600px or less, or a height of 500px or less, so landscape phones count), the workspace drops its card and the raised sheet: panes run full-bleed with the same 1.5rem (24px) gutters, so margin comment markers keep their lane, under a translucent top bar that tucks away while reading down, above a floating bottom tab bar in the thumb zone. Landscape phones use a shorter top bar and a one-line tab bar. Destinations slide sideways in tab order, and crossfade under reduced motion. Images and diagrams can extend beyond the prose measure within those gutters; tables can grow toward the trailing gutter and retain their own scroll lane. On a phone (a coarse pointer on a small screen), when the document is narrower than 40rem, a wide table fades whichever side still hides columns, code wraps with a hanging indent instead of scrolling, and a Mermaid diagram below its readability floor offers Expand diagram under its scroll lane, which opens it in a full-screen sheet at its authored size. A narrow document alone does not trigger these: narrow desktop windows keep the scroll lanes.
Spacing starts from a 4px unit and uses the measured steps shown in the frontmatter. App chrome is dense; prose has larger margins and trailing space so the caret remains comfortable near the end of a document. Layout responds to both viewport and document container width. Safe-area insets and larger coarse-pointer targets are accounted for in the web styles.
diff --git a/apps/web/src/anchored-child-surface.tsx b/apps/web/src/anchored-child-surface.tsx
index b71c8f4e9..638984db7 100644
--- a/apps/web/src/anchored-child-surface.tsx
+++ b/apps/web/src/anchored-child-surface.tsx
@@ -1,5 +1,4 @@
import { childDocumentPath, documentPath } from "@chopin/protocol/document-url";
-import { MagnifierIcon } from "@chopin/icons";
import { Fragment, useLayoutEffect, useRef } from "react";
import { closeChild, openChild, rememberOpener } from "./child-motion-driver";
@@ -129,12 +128,6 @@ export function AnchoredChildSurface(
tabIndex={-1}
>
- {motion === "lift" && (
-
-
- {entry.label}
-
- )}
{entry.node}
diff --git a/apps/web/src/child-motion-driver.ts b/apps/web/src/child-motion-driver.ts
index 3b8124d35..e78dfa8dc 100644
--- a/apps/web/src/child-motion-driver.ts
+++ b/apps/web/src/child-motion-driver.ts
@@ -17,7 +17,7 @@ export function readChildMotionStyle() {
try {
stored = localStorage.getItem(CHILD_MOTION_STORAGE_KEY);
} catch {
- // Storage can be unavailable; Lift is the default.
+ // Storage can be unavailable; Zoom is the default.
}
return childMotionStyle(toggleEnabled, stored);
}
diff --git a/apps/web/src/child-motion.test.ts b/apps/web/src/child-motion.test.ts
index 3aba89f16..1ea252b82 100644
--- a/apps/web/src/child-motion.test.ts
+++ b/apps/web/src/child-motion.test.ts
@@ -70,11 +70,11 @@ describe("child motion geometry", () => {
});
describe("child motion style", () => {
- it("honours Zoom only where the toggle is enabled", () => {
+ it("defaults to Zoom and honours Lift only where the toggle is enabled", () => {
expect(childMotionStyle(true, "zoom")).toBe("zoom");
expect(childMotionStyle(true, "lift")).toBe("lift");
- expect(childMotionStyle(false, "zoom")).toBe("lift");
- expect(childMotionStyle(true, null)).toBe("lift");
- expect(childMotionStyle(true, "wobble")).toBe("lift");
+ expect(childMotionStyle(false, "lift")).toBe("zoom");
+ expect(childMotionStyle(true, null)).toBe("zoom");
+ expect(childMotionStyle(true, "wobble")).toBe("zoom");
});
});
diff --git a/apps/web/src/child-motion.ts b/apps/web/src/child-motion.ts
index b99e3ba24..2d44fd2ff 100644
--- a/apps/web/src/child-motion.ts
+++ b/apps/web/src/child-motion.ts
@@ -7,8 +7,9 @@ export const CHILD_MOTION_STORAGE_KEY = "chopin:dev:child-motion";
/** Each covered card shrinks this much more than the one above it. */
const RECEDE_STEP = 0.03;
+/** Zoom is the default; Lift stays available behind the development toggle. */
export function childMotionStyle(enabled: boolean, stored: string | null): ChildMotionStyle {
- return enabled && stored === "zoom" ? "zoom" : "lift";
+ return enabled && stored === "lift" ? "lift" : "zoom";
}
function px(value: number): string {
diff --git a/apps/web/src/child-title.ts b/apps/web/src/child-title.ts
new file mode 100644
index 000000000..f90e5b75e
--- /dev/null
+++ b/apps/web/src/child-title.ts
@@ -0,0 +1,19 @@
+/**
+ * Where an open child's title goes in the split workspace.
+ *
+ * Under Zoom the child replaces its parent in place, so its title, rename and actions join the
+ * end of the parent's path in the parent's title bar. Under Lift the child's sheet carries a
+ * title bar of its own. Either way the child room owns its title, so it renders it here.
+ */
+
+import { createContext } from "react";
+
+import type { ChildMotionStyle } from "./child-motion";
+
+export type ChildTitle = {
+ motion: ChildMotionStyle;
+ /** The end of the parent's path under Zoom; null until it mounts, and under Lift. */
+ slot: HTMLElement | null;
+};
+
+export const ChildTitleContext = createContext(undefined);
diff --git a/apps/web/src/hosted.test.ts b/apps/web/src/hosted.test.ts
index ec1a00d0e..fe2a60bf0 100644
--- a/apps/web/src/hosted.test.ts
+++ b/apps/web/src/hosted.test.ts
@@ -68,7 +68,7 @@ function detail(
let toolbarOf = (markup: string) =>
markup.slice(
- markup.indexOf('data-document-toolbar="true"'),
+ markup.indexOf('data-document-title-bar="true"'),
markup.indexOf('data-document-view="plan"'),
);
@@ -287,9 +287,8 @@ describe("anchored child lifecycle", () => {
expect(lift).toContain('class="anchored-child-layer" data-child-motion="lift"');
expect(lift).toContain('style="--level:1;--depth:1"');
expect(lift).toContain('aria-label="Child document: Source review"');
- expect(lift).toContain('
');
- expect(lift).toContain(">Source review");
- expect(zoom).not.toContain("anchored-child-title");
+ // The child room draws its own title bar, so the surface adds none.
+ expect(lift).not.toContain("anchored-child-title");
expect(zoom).not.toContain("anchored-child-shadow");
expect(zoom).toContain('data-workspace-room="child-room"');
});
@@ -301,6 +300,7 @@ describe("anchored child lifecycle", () => {
decisions: createElement("div", null, "Decisions"),
header: createElement("header", null, "Room header"),
identity: "room",
+ title: createElement("div", null, "Room title"),
ids: workspaceIds,
mode: "split" as const,
onChatOpen() {},
@@ -333,17 +333,20 @@ describe("anchored child lifecycle", () => {
view,
}));
- it("Lift covers the root sheet below its tab row and leaves Chat and the top bar live", () => {
+ it("Lift covers the root sheet below its title bar and leaves Chat and the title bar live", () => {
let markup = withChild("lift");
let main = markup.match(/]*>/)?.[0];
let swap = markup.match(/
]*data-workspace-document-swap[^>]*>/)?.[0];
- let toolbar = markup.match(/
]*data-document-toolbar[^>]*>/)?.[0];
+ let titleBar = markup.match(/
]*data-document-title-bar[^>]*>/)?.[0];
+ let views = markup.match(/
]*data-document-toolbar[^>]*>/)?.[0];
let stack = markup.slice(markup.indexOf("workspace-document-stack"), markup.indexOf("