| /* 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); |
| } |