blob: 762be2ac337c7af1f30d8def68436b02610fb2ea [file]
/* Shell layout — app frame, panels, topbar rail, main column, workspace top actions. */
.appFrame {
font: var(--maka-text-body);
position: relative;
height: 100dvh;
width: 100%;
padding: 0;
/* PR-APPFRAME-FILL-0: `#root` (index.html) is `display:flex;
align-items:center; justify-content:center` so the preload
skeleton (`.maka-preload`, rendered before React mounts) sits
centered. Once React mounts, `.appFrame` is that flex container's
only child, and without an explicit main-axis size a flex item
shrinks to its content width and gets centered — so on the
first-run hero (onboarding content max-content ~680px) the whole
app frame floated mid-window with large left/right gutters, while
long-message sessions filled the width. Pin `width:100%` so the
frame always spans the window regardless of inner content's
max-content. The preload centering is unaffected (it's the only
child at that point and still centers). */
/* The backplate under the preload skeleton and behind the shell before it
mounts. AppShell paints both columns over this, so it is not a shell
surface — it is what the window shows when there is no shell yet. */
background: var(--surface-canvas);
color: var(--foreground);
box-sizing: border-box;
}
.app {
height: 100%;
}
/* Astryx AppShell owns the column layout AND the column materials: on
`variant="elevated"` it paints the nav column --color-background-body and the
content column --color-background-surface, both of which makaTheme.ts points
back at the product palette. Window chrome is a transparent absolute
hit-test overlay on the frame — not an AppShell topNav row — so both columns
paint to the window top (Codex / Claude Desktop / Cursor). */
.maka-shell-astryx {
width: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
}
/* WAWQAQ msg `9f313715` (2026-08-03): the content column is a rounded floating
plate flush to the sidebar — its edge is the ONE boundary on that seam. The
theme's full-height sidenav hairline (`.astryx-app-shell-sidenav.elevated`
in the built maka.css) ran on the same seam, so beside the plate's top-left
corner radius both lines showed and read as a doubled border. Drop it in both
collapse states — column separation is tonal (the plate's fill against the
canvas), per DESIGN.md's One Working Plane rule. Override lives here because
maka.css is a generated artifact (`npm run astryx:theme`), not a hand-edited
file.
Continuous Collapse used to repaint the collapsed rail with
--color-background-surface here as well, so the traffic lights would sit on
one surface instead of straddling a column edge narrower than they are. That
was written while AppShell's own surface was still the content column's
material; `.astryx-layout-content` below now paints --agents-layout-bg
(== --surface-canvas == --color-background-body), so the two columns already
share one material and the repaint was the only thing breaking the band. In
light mode --color-background-surface happens to equal the floating plate's
fill, which hid it; in dark mode the plate lifts 0.035L off --background and
the rail became a THIRD tone (measured L 0.205 against the canvas's 0.18),
painting a 49px slab that ran to the window top past the plate's 4px gutter
and under the light cluster. The rail now wears its column's material in both
states, which is what "one surface" meant in the first place. */
.maka-shell-astryx .astryx-app-shell-sidenav {
border-inline-end-color: transparent;
}
/* The SIDENAV's resize handle is the one control positioned against the column
BOX rather than flowing inside it (Astryx gives it `position:absolute; top:0;
inset-block-end:0`), so the columns' `padding-top` clearance does not move it
and its top 36px lands under the chrome strip. The strip is the topmost hit
surface across the whole band, so that part of the handle cannot be grabbed at
all — pointer-down there reaches the OS as a window drag instead. Start the
handle below the strip; `inset-block-end: 0` still takes it to the bottom.
Keyed on the sidenav handle's own testid, not on the class: every Astryx
ResizeHandle carries that class, and a page's own inspector handle is a
`position: relative` flex item, so the same `top` shoved its divider 36px
down the page — it started below the header's rule instead of meeting it. */
.maka-shell-astryx [data-testid="astryx-sidenav-resize-handle"] {
top: var(--h-titlebar);
}
/* The canvas the floating plate sits on. Keyed on the AppShell's own
`#astryx-app-shell-main`, NOT on `.astryx-layout-content`: that class is on
EVERY Astryx LayoutContent, so as a descendant selector it also reached the
one a page renders inside the plate and painted the darker canvas back over
it, edge to edge. In light mode that was a 2.5% step nobody noticed; in dark
it repainted the whole page the sidebar's colour and the two columns fused.
The id is the vendor's own hook for this region and cannot be nested. */
.maka-shell-astryx #astryx-app-shell-main {
background: var(--agents-layout-bg);
}
.maka-shell-astryx .maka-panel-detail {
width: calc(100% - var(--agents-content-area-gap));
height: calc(100% - 2 * var(--agents-content-area-gap));
margin: var(--agents-content-area-gap) var(--agents-content-area-gap) var(--agents-content-area-gap) 0;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
/* This element frames; it does not paint. What the user reads as "the content
area" is the column inside it, and beside that column the workbar is a
second plate of the same kind. The canvas showing between them is the seam,
which is the same seam this frame's own margin draws on its other three
sides — the shell has exactly one way of separating surfaces, and it is a
gap, not a tone step and not a rule.
`clip`, not `hidden`: a frame is never a scrollport. Nothing overflows it
today, and the one thing that ever did — a column hung above this box to
reach its top edge — scrolled the whole frame up by the clearance the first
time anything inside took focus. */
overflow: clip;
/* How far each plate inside has to start below the absolute chrome strip.
Declared here because the frame is what the strip sits over, consumed by
every plate that stands in it — the conversation column and the workbar
both. One number, no plate reaching outside its own box to get it. */
--maka-plate-titlebar-clearance: calc(var(--h-titlebar) - var(--agents-content-area-gap));
box-sizing: border-box;
}
/* The conversation plate. Every view renders into this column, so this is where
the content surface actually is. */
.maka-shell-astryx .mainColumn {
border-radius: var(--radius-modal);
background: var(--agents-content-area-bg);
padding-top: var(--maka-plate-titlebar-clearance);
box-sizing: border-box;
}
/* Same top clear for the sidebar column so list/nav content sits below the
traffic-light / left chrome cluster while the transparent plate shows. */
.maka-shell-astryx .maka-session-panel {
padding-top: var(--h-titlebar);
box-sizing: border-box;
}
/* Collapse/expand easing. The declaration has to sit on an element that lives
across the toggle, and SideNav's own root is not one: expanded it wraps the
<nav> in a div for the overlay resize handle, collapsed it renders the bare
<nav>, so React unmounts and remounts that subtree. A transition on the nav
has nothing to interpolate from — measured live, the very first frame after
the click is already 48px across all 144 sampled frames. `.maka-sidenav-motion`
is the product's own wrapper outside SideNav, so it is the same node before
and after and its width really does animate.
The wrapper is therefore the width authority while the ease runs, and its
child must follow rather than assert its own. `width: 100% !important` is
load-bearing in the COLLAPSED direction: SideNav's `rootCollapsed` is a
StyleX atom padded to specificity 0-4-0 (`.xhgvbfk:not(#\#)` four times over)
against this rule's 0-2-0, so without the flag the nav would snap to 48px
while the wrapper was still easing toward it. Expanded, SideNav's inline
width sits on the <nav>, which is wrapped one level deeper by the resize
handle's container, so it is never what this rule is competing with.
Only a pointer drag suppresses the ease. A standing transition would catch the
width the resize handle feeds back on every pointer move, restarting the ease
each time so the rail trails the cursor. The gate asks the component that owns
the answer: Astryx marks its handle `data-resizing` while a drag is in flight
and clears it on pointer-up. The handle is a descendant of the wrapper (it is
the nav's sibling one level in), so plain `:has()` reads it.
Separator focus is deliberately NOT part of the gate — it was, and it left
the ease dead for every collapse after the handle had once been touched. */
.maka-shell-astryx .maka-sidenav-motion {
display: flex;
min-width: 0;
min-height: 0;
/* The nav sizes itself against a definite height (`.maka-session-panel`
is `height: 100%`); without one here the wrapper grows to the unclipped
content and the footer leaves the window — the geometry
e2e/sidebar-geometry.spec.ts locks. */
height: 100%;
width: var(--maka-sidenav-width);
transition: width var(--duration-large) var(--ease-out-strong);
}
/* Same source as Astryx's own `rootCollapsed` width, so the rail cannot drift
from the value SideNav lands on when the ease finishes. Keyed off the shell's
own collapsed flag — the wrapper carried a second copy of it until this rule
read the one `.appFrame` already publishes and the hairlines already use. */
.appFrame[data-sidebar-state='collapsed'] .maka-sidenav-motion {
width: var(--spacing-12);
}
.maka-shell-astryx .maka-sidenav-motion:has([data-resizing]) {
transition: none;
}
.maka-shell-astryx .maka-sidenav-motion > * {
/* !important: beats SideNav's inline width — see the note above. */
width: 100% !important;
min-width: 0;
}
.maka-panel {
min-width: 0;
min-height: 0;
overflow: hidden;
}
/* The window's only drag surface, and the only `-webkit-app-region: drag` in
the app. It is a transparent absolute overlay — pure hit-test chrome — so
column surfaces paint through it. Action clusters carve themselves out with
`no-drag`. Chromium builds drag regions in document order: this node must
precede the columns so later no-drag controls can subtract from it.
This replaced an arrangement where every container that happened to overlap
the titlebar declared its own drag region and then reserved room for its
neighbours with a hand-summed `margin-left`/`margin-right` ruler. Those
rulers encoded "how many titlebar buttons exist right now" — state-dependent
when a third titlebar button (新任务) only appeared while the sidebar was
collapsed (that button is gone now; 新任务 stays on the SideNav rail). The
sidebar's strip reserved room for two, the third button landed in the space
nobody reserved, and clicks on it reached the OS as window drags.
The inset keeps the drag rect off the window frame on the three sides it
touches, so the OS keeps its resize corridor there. It is DEFENSIVE, not
demonstrated: this area's other bug (`af681c1`, msg `5b85fdb1`) was a window
that could not be resized from its edges, and no automated check in this repo
can exercise the OS hit test to prove whether 0 would also be safe. 4px of a
36px titlebar is not a cost worth arguing about; a P0 nobody can reproduce in
CI is.
The gutters are applied as plain padding, NOT as `calc(gutter - margin)`. Both
are safe-AREA measurements, so the box's 4px inset just yields 4px more
clearance than the minimum. Subtracting it would restate the inset in two more
places to keep the clusters at an exact pixel — which is the habit this whole
change exists to remove. */
.maka-window-titlebar {
position: absolute;
z-index: var(--z-titlebar);
top: var(--maka-window-resize-edge);
left: var(--maka-window-resize-edge);
right: var(--maka-window-resize-edge);
height: calc(var(--h-titlebar) - var(--maka-window-resize-edge));
min-height: calc(var(--h-titlebar) - var(--maka-window-resize-edge));
box-sizing: border-box;
display: flex;
align-items: center;
gap: var(--space-2);
/* No paint: column surfaces under this strip own the color (canvas left,
--background right). Windows titleBarOverlay still samples --background
for the OS caption strip on the content side. */
background: transparent;
/* One gutter rule per side: our own `--space-6` of breathing room, or the
platform's native-control safe area when that is wider. Nothing here knows
which platform it is on or how wide any control is — `max()` picks whichever
the OS reports (macOS: traffic lights on the left; Windows: caption buttons
on the right; Linux: neither, so the design floor stands). */
padding-left: max(var(--space-6), var(--maka-titlebar-area-x));
padding-right: calc(var(--space-6) + var(--maka-titlebar-overlay-right-width));
-webkit-app-region: drag;
}
/* Drag regions are hit-tested from element rects, and the top layer is invisible
to that pass: a `showModal()` dialog paints over the titlebar, but its controls
inside the titlebar rect still reach the OS as window drags. */
dialog:modal {
-webkit-app-region: no-drag;
}
.maka-shell-topbar-rail {
display: flex;
align-items: center;
gap: var(--space-2);
opacity: 1;
pointer-events: auto;
transform: translateX(0);
transition:
opacity var(--duration-quick) var(--ease-out-strong),
transform var(--duration-base) var(--ease-out-strong);
-webkit-app-region: no-drag;
/* Match SideNav unselected icon ink (secondary), not ghost Button primary. */
color: var(--foreground-secondary);
}
.mainColumn {
min-width: 0;
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: transparent;
}
.mainColumn[data-home-surface="true"] {
align-content: stretch;
padding-bottom: 0;
}
/* Right-hand cluster of the titlebar row. `margin-left: auto` is the whole
positioning rule — it used to be absolutely positioned against the detail
panel, which is why the chat header had to reserve its width by hand. Which
views render it at all is decided in app-shell.tsx, not by a `display: none`
override reaching in from `[data-agents-view]`. */
.maka-workspace-top-actions {
-webkit-app-region: no-drag;
margin-left: auto;
display: inline-flex;
align-items: center;
gap: var(--space-1-5);
color: var(--foreground-secondary);
}
/* Titlebar chrome glyph ink is set where the glyph is: `ChromeIcon`
(app-shell-chrome-actions.tsx) renders Astryx Icon with `color="secondary"`,
and Icon puts `color: var(--color-icon-secondary)` on the svg ITSELF. A
button-level rule cannot reach past that — a child's own `color` always beats
an inherited one — so the ghost variant's primary text never applied to these
glyphs and the override that used to sit here corrected nothing. It also
over-reached onto button label text, which these buttons happen not to have.
`.maka-titlebar-action` needs no `no-drag` of its own: every one of these
buttons lives inside `.maka-shell-topbar-rail` or
`.maka-workspace-top-actions`, and each of those carves out a rect covering
its children. A per-button rule (plus a `*` descendant rule for the icons)
only duplicated that. */