blob: d000c6c19a4cb0b410b697dfda600dc62fbb9fde [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;
}
/* Sidebar and canvas share one floor colour; the content plate is the only
step. Drop the theme sidenav hairline (generated maka.css) so the plate edge
is the single seam. Width 0: a transparent 1px border still reserved layout. */
.maka-shell-astryx .astryx-app-shell-sidenav {
background: var(--agents-layout-bg);
border-inline-end-color: transparent;
border-inline-end-width: 0;
}
/* 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.
It overrides the VARIABLE, not the wrapper's width, because the wrapper is no
longer the only reader: the titlebar starts its session breadcrumb at this
column's right edge. Setting the width here instead would leave the titlebar
reading the expanded value while the column sat collapsed. */
.appFrame[data-sidebar-state='collapsed'] {
--maka-sidenav-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;
align-items: center;
gap: var(--maka-titlebar-gap);
/* 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: var(--maka-titlebar-gutter-left);
padding-right: calc(var(--space-6) + var(--maka-titlebar-overlay-right-width));
-webkit-app-region: drag;
/* Three columns, not a flex row, because the middle one has to line up with
something OUTSIDE this strip. The strip spans the whole window while the
shell below it is two columns, and the session breadcrumb belongs to the
content column — laid out in flow it landed wherever the icon rail happened
to end, which was mid-sidebar, straddling the seam between the columns and
aligned with neither.
Column 1 stands in for the sidebar column, so its floor is that column's
width less everything already between it and the window's left edge: this
strip's resize inset, its left gutter, and the `gap` that follows the
column. Subtracting the gap is what makes column 2 OPEN at the seam — left
in, it pushed the breadcrumb a gap past the plate edge — one whole
`--maka-titlebar-gap` (8px) of overhang instead of the intended flush
start.
`minmax(..., auto)` rather than a fixed width for the collapsed state,
where the two icons are wider than the 48px rail: the column grows to the
icons and the gap then holds the breadcrumb off them, which is the same
clearance by a different measure.
Every child names its own column. Two of the three are conditional — the
breadcrumb only in a session, the workspace actions only in views that have
them — and with implicit placement the survivors slide into the empty slots,
which would park the workbar toggle in the middle of the window. */
display: grid;
grid-template-columns:
minmax(
calc(
var(--maka-sidenav-width) - var(--maka-window-resize-edge) -
var(--maka-titlebar-gutter-left) - var(--maka-titlebar-gap)
),
auto
)
minmax(0, 1fr)
auto;
/* Known, and deliberately not papered over: while the sidebar eases its width
over `--duration-large`, this track resolves to its final size on the first
frame, so the breadcrumb arrives before the column it is aligned to. Both
ways of easing it were measured in Electron's Chromium and neither
interpolates — `transition: grid-template-columns` and an `@property`
`<length>` on the width variable both jump on frame one. Closing it means
moving the sidebar's ease onto a property that does interpolate, which is a
change to the sidebar's motion, not to this strip. */
}
/* 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 {
grid-column: 1;
justify-self: start;
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);
}
/* The session's identity: left-aligned at the content column's left edge.
It sits in the titlebar's second grid column, which opens exactly where the
sidebar column ends, so the breadcrumb reads as the heading of the
conversation plate beneath it. Laid out in flow instead, it anchored to the
icon rail and landed mid-sidebar — straddling the seam, aligned to neither
column. Collapsing the sidebar narrows column 1 and the breadcrumb follows it
left — flush with the plate while the sidebar is wide enough to be the
binding measure, and held off the icon rail by the strip's own gap once the
icons are wider than the collapsed column. Both readings are one rule: the
column's floor is the sidebar, its ceiling the icons.
`padding-left` keeps the text off the seam itself. It matches the inset the
sidebar's own rows carry, so the two columns' text starts read as a pair
rather than one label welded to the divider.
Both plates below reserve the full `--h-titlebar` (`.mainColumn` via
`--maka-plate-titlebar-clearance`, `.maka-session-panel` directly), so this
strip is empty for its whole width and the label collides with nothing.
`min-width: 0` + `justify-self: start`: the column is `minmax(0, 1fr)`, so on
a narrow window this is what gives way, truncating its own segments instead
of squeezing the two action clusters that flank it. `no-drag` covers only
this box — the strip stays draggable on both sides of the text, exactly as it
already is around the icon clusters. */
.maka-titlebar-identity {
grid-column: 2;
justify-self: start;
min-width: 0;
/* Never the whole column: what is left of it is the strip's drag band. */
max-width: calc(100% - var(--maka-titlebar-drag-reserve));
display: flex;
padding-left: var(--space-3);
-webkit-app-region: no-drag;
}
/* Every box between the strip's column and the text has to agree to shrink, or
none of them do. `min-width: 0` on the outer box alone clamped only that box:
the nav, the crumb buttons and the segments kept `min-width: auto`, so with a
long name the text ran on past the clamp — measured 2408px inside a 1320px
window — painting over the workspace actions and, worse, spilling the visible
tail into the strip's DRAG region, where a click on the session name reaches
the OS as a window drag. The ellipsis never engaged, because nothing upstream
ever handed these elements a smaller size to ellipsize into. */
.maka-titlebar-identity__breadcrumbs {
min-width: 0;
overflow: hidden;
}
.maka-titlebar-identity__breadcrumbs > ol > li > button {
min-width: 0;
overflow: hidden;
}
.maka-titlebar-identity__breadcrumbs > ol {
min-width: 0;
flex-wrap: nowrap;
overflow: hidden;
}
.maka-titlebar-identity__breadcrumbs > ol > li {
min-width: 0;
overflow: hidden;
}
.maka-titlebar-identity__segment {
display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Astryx styles every non-current crumb as a link, and its hover is an
underline. In this app an underline means "this leaves for the open web"
(`.maka-web-search-preview a` is its only other use); these two crumbs open a
folder and start a rename. Both are in-app actions, so they take the hover
the rest of the app's controls take — the `--state-hover-bg` wash — and the
underline is declined.
Keyed on the button rather than the segment span so the wash covers the whole
hit target. What clears the StyleX atom is LAYER ORDER, not specificity:
product CSS imports into `components`, which cascade-layers.css declares
last, and Astryx's atoms carry `:not(#\#):not(#\#)` — on specificity alone
the atom wins. */
.maka-titlebar-identity__breadcrumbs button {
border-radius: var(--radius-control);
padding-inline: var(--space-1);
transition: background-color var(--duration-quick) var(--ease-out-strong);
}
.maka-titlebar-identity__breadcrumbs button:hover {
text-decoration: none;
background: var(--state-hover-bg);
}
.maka-titlebar-identity__breadcrumbs button:focus-visible {
/* Inset, not outline: the breadcrumb item ancestor clips 3px of an
outward ring (measured by the T5 geometry probe) — a ring that cannot
fully paint is worse than none. Same idiom as the workbar launcher. */
outline: none;
box-shadow: inset 0 0 0 var(--focus-ring-width) var(--focus-ring);
}
/* The session name carries the weight of the pair: it is what the window is
showing, and the project is the context it sits in. Astryx gives every
non-current crumb the same secondary ink, and this one has to be non-current
to stay clickable (see the `isCurrent={false}` note in the component), so the
emphasis is restored here rather than inherited.
The target is the sidebar's own selected row — same size, same weight, same
ink. One session should not have two different weights depending on which
surface is naming it, and the titlebar has no business being the lighter of
the two. */
.maka-titlebar-identity__segment--session {
font-weight: var(--font-weight-medium);
color: var(--foreground);
}
/* The rename field stands in for the session crumb ALONE — it renders inside
that crumb, so the project crumb and the separator keep their places — and it
matches the label it replaces exactly in size, weight and ink, because the
name must not shift when it becomes editable.
`field-sizing: content` is what keeps the row still: the field opens at the
width of the text it is editing and grows with what is typed. A fixed width
made starting a rename shove everything beside it, which is the same jump
from the other direction. `min-width` keeps an empty field clickable; the
`max-width` is the truncation ceiling the label itself has.
The field claims no box of its own beyond that: no border, and a focus line
drawn with `box-shadow`, which paints outside layout. The bottom border it
used to carry was 1px the crumb did not have, so the trail lost a pixel of
height the moment the field appeared. */
.maka-titlebar-identity__rename-input {
font: var(--maka-text-body);
font-weight: var(--font-weight-medium);
color: var(--foreground);
background: transparent;
border: 0;
outline: 0;
box-shadow: 0 1px 0 oklch(from var(--focus-ring) l c h / 0.4);
field-sizing: content;
padding-block: 0;
/* The crumb button's own inline padding, so the first glyph does not step
4px left the moment the label becomes a field. */
padding-inline: var(--space-1);
/* Small enough that a real name is always the binding measure — at `4rem`
the field opened wider than the word it replaced and nudged the trail. */
min-width: 2rem;
/* Bounded by the column, not by a guess: `20rem` alone overflowed the same
way the segments did once the window was narrow enough. */
max-width: min(20rem, 100%);
}
.maka-titlebar-identity__rename-input:focus {
box-shadow: 0 1px 0 var(--focus-ring);
}
.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. It is the strip's last grid column —
it used to be absolutely positioned against the detail panel, which is why
the chat header had to reserve its width by hand, and then a `margin-left:
auto` flex item, which only reached the right edge as long as it was the last
child. Naming the column keeps it there when the breadcrumb beside it is
absent. 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 {
grid-column: 3;
justify-self: end;
-webkit-app-region: no-drag;
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. */
/* Focus rings inside the titlebar strip go inset (T5 geometry probe): the
strip sits 4px under the window edge and body's overflow clips the top
pixel of an outward 2px+offset ring. An inset ring paints whole. */
.maka-window-titlebar :is(button, [role="button"]):focus-visible {
outline: none;
box-shadow: inset 0 0 0 var(--focus-ring-width) var(--focus-ring);
}