Public: anyone can read this; the crew can change it.
The studio's home and app pages lay main and rail with `.split`, and `.dash` leaves the stylesheet #4775
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/4748-the-studio-s-home-and-app-pages-lay-main → main 2 commits fork
6 files
changed +28
−11
docs/DESIGN.md +2 −1
| @@ -393,7 +393,8 @@ gone, so this list only shrinks. | ||
| 393 | 393 | |
| 394 | 394 | - **`style/base.ts`** — the page shell (topbar, main, footer) and the layout and text utilities every page leans on: `.bar` `.bare` `.foot` `.grow` `.hide-lg` `.hide-sm` `.js-only` `.mark` `.mb` `.mt` `.muted` `.narrow` `.narrow-wide` `.num` `.reduced` `.shell` `.shell-wide` `.skip` `.small` `.spacer` `.sr-only` `.stack` `.stack-tight` `.topbar` `.topbar-tools` `.truncate` `.wordmark` |
| 395 | 395 | A `.field` directly inside a `.bar` drops its bottom margin and the bar aligns to the end, so a labelled input and its button share a baseline |
| 396 | - **`style/components.ts`** — parts of the §5 components that the table does not spell out — the palette's list, the menu's items, a dialog's head and foot, comments, the theme picker — and a few small helpers beside them: `.cat` `.comment` `.comment-body` `.comment-form` `.dash` `.dialog-body` `.dialog-foot` `.dialog-head` `.empty-title` `.error-note` `.field-row` `.form-actions` `.hex` `.ico` `.ico-loader` `.list` `.menu-item` `.menu-panel` `.menu-sep` `.menu-title` `.palette-empty` `.palette-foot` `.palette-group` `.palette-heading` `.palette-input` `.palette-item` `.palette-list` `.preview` `.sep` `.split` `.sprite` `.swatch` `.table-scroll` `.tell-here` `.theme-apply` `.theme-option` `.tick` `.timeline` `.tl-dot` `.toast-body` `.topbar-search` | |
| 396 | - **`style/components.ts`** — parts of the §5 components that the table does not spell out — the palette's list, the menu's items, a dialog's head and foot, comments, the theme picker — and a few small helpers beside them: `.cat` `.comment` `.comment-body` `.comment-form` `.dialog-body` `.dialog-foot` `.dialog-head` `.empty-title` `.error-note` `.field-row` `.form-actions` `.hex` `.ico` `.ico-loader` `.list` `.menu-item` `.menu-panel` `.menu-sep` `.menu-title` `.palette-empty` `.palette-foot` `.palette-group` `.palette-heading` `.palette-input` `.palette-item` `.palette-list` `.preview` `.sep` `.split` `.sprite` `.swatch` `.table-scroll` `.tell-here` `.theme-apply` `.theme-option` `.tick` `.timeline` `.tl-dot` `.toast-body` `.topbar-search` | |
| 397 | `.split` is the one two-column page: main, then an `<aside>` rail of 260px that sticks from 960px. `data-rail="wide"` on it (the studio's home and an app's page) widens the rail to 320px from 900px, drops the top margin, and lets the rail scroll with the page, since a column of cards runs taller than a screen | |
| 397 | 398 | - **`style/components.ts`, settings rows** — a list whose rows are settings (limits, capacity): the `<ul class="list setting-list">` lets title and meta wrap instead of ellipsizing, and a row's controls take their own line under 640px. `.row-fields` is the form in that row's trail (input, Save, Default): always visible, never hover-gated, wrapping under the text before it would clip. Use both for any page whose rows show a name, a current value, and a control to change it; use `.row-actions` only for secondary actions (delete, wake, pin, revoke). Flags, telemetry, cloud, people, crew roles and subscription headroom/budget use `.row-fields`. Classes: `.row-fields` `.setting-list` |
| 398 | 399 | - **`style/content.ts`, page furniture** — what frames a page's content: page and repo heads, the front page and its tiles, explore, backups, boards, the fleet chart's series colours, lineage: `.bform` `.bgrid` `.bmanage` `.bp-h2` `.bp-h3` `.bp-w2` `.bp-w3` `.break` `.bt-bar` `.bt-grid` `.bt-group` `.bt-hist` `.bt-name` `.bt-procs` `.bt-row` `.bt-val` `.commit-body` `.commit-head` `.domain-records` `.fg-s0` `.fg-s1` `.fg-s2` `.fg-s3` `.fg-s4` `.fg-s5` `.fg-s6` `.fg-s7` `.filters` `.flag-form` `.front-admin` `.front-controls` `.front-crew` `.front-featured` `.front-featured-body` `.front-featured-look` `.front-issues` `.front-live` `.front-more` `.front-pick` `.headroom` `.issue-head` `.issue-meta` `.mirror-form` `.number` `.origin-mark` `.origin-name` `.origin-row` `.owner` `.page-head` `.policy-line` `.qf` `.repo-actions` `.repo-audience` `.repo-desc` `.repo-layout` `.repo-lineage` `.repo-title` `.search` `.slash` `.sub` `.sys-pair` `.table-wrap` `.tok` `.where-sep` |
| 399 | 400 | - **`style/content.ts`, code view** — the source view and the syntax-highlight token classes `highlight-html.ts` builds as `tok-${kind}`: `.blob-view` `.ln` `.tok-attribute` `.tok-comment` `.tok-constant` `.tok-decorator` `.tok-deleted` `.tok-emphasis` `.tok-function` `.tok-heading` `.tok-inserted` `.tok-keyword` `.tok-link` `.tok-meta` `.tok-number` `.tok-operator` `.tok-plain` `.tok-property` `.tok-punctuation` `.tok-regex` `.tok-string` `.tok-tag` `.tok-type` `.tok-variable` |
src/web/studio/app.ts +3 −3
| @@ -494,14 +494,14 @@ export async function studioApp( | ||
| 494 | 494 | egressSetting(ctx.app.db), |
| 495 | 495 | ) |
| 496 | 496 | : null} |
| 497 | <div class="dash"> | |
| 498 | <div class="dash-main stack"> | |
| 497 | <div class="split" data-rail="wide"> | |
| 498 | <div class="stack"> | |
| 499 | 499 | ${tell} |
| 500 | 500 | ${ownerFacts && ctx.auth.user ? answersRegion(ctx, repo, ctx.auth.user) : null} |
| 501 | 501 | <div id="app-previews">${previews}</div> |
| 502 | 502 | ${nowRegion(repo, now)} |
| 503 | 503 | </div> |
| 504 | <aside class="dash-rail stack"> | |
| 504 | <aside class="stack"> | |
| 505 | 505 | ${crewCard} ${weekCard} ${ownCard} ${more} |
| 506 | 506 | </aside> |
| 507 | 507 | </div> |
src/web/studio/home.ts +3 −3
| @@ -261,8 +261,8 @@ export async function studioHome( | ||
| 261 | 261 | </p>`, |
| 262 | 262 | )} |
| 263 | 263 | ${homeTabs("feed")} |
| 264 | <div class="dash"> | |
| 265 | <div class="dash-main stack"> | |
| 264 | <div class="split" data-rail="wide"> | |
| 265 | <div class="stack"> | |
| 266 | 266 | ${composer(ctx, user)} |
| 267 | 267 | <div class="bar feed-head"> |
| 268 | 268 | <span class="spacer"></span> |
| @@ -272,7 +272,7 @@ export async function studioHome( | ||
| 272 | 272 | ${feedNote()} |
| 273 | 273 | ${timePhase(ctx.request, "feed", () => feedRegion(user, ctx), () => feedCandidateCache(ctx.app.db))} |
| 274 | 274 | </div> |
| 275 | <aside class="dash-rail stack"> | |
| 275 | <aside class="stack"> | |
| 276 | 276 | ${ |
| 277 | 277 | // The crew's missing key is the owner's to fix, not the first |
| 278 | 278 | // thing above every post: beside the timeline on a desk, after it |
src/web/style/components.ts +1 −1
| @@ -337,7 +337,7 @@ a.sha:hover { color: var(--accent); } | ||
| 337 | 337 | /* Cards stacked in normal flow space themselves. Inside a container that |
| 338 | 338 | already manages its own spacing with gap, that margin would double it. */ |
| 339 | 339 | .card + .card { margin-top: 12px; } |
| 340 | :is(.stack, .dash, .split, .timeline) > .card + .card { margin-top: 0; } | |
| 340 | :is(.stack, .split, .timeline) > .card + .card { margin-top: 0; } | |
| 341 | 341 | .card-head { |
| 342 | 342 | display: flex; align-items: center; gap: 8px; flex-wrap: wrap; |
| 343 | 343 | padding: 10px 14px; border-bottom: 1px solid var(--border); background: var(--muted); |
src/web/style/content.ts +9 −3
| @@ -339,7 +339,7 @@ form.inline { display: inline; margin-left: auto; } | ||
| 339 | 339 | *content*, so a column with a long unbreakable line pushes the page wider |
| 340 | 340 | than the phone rather than letting its own ellipsis do the work. A zero |
| 341 | 341 | min-width is what makes truncation inside a grid actually truncate. */ |
| 342 | .dash > *, .split > *, .repo-layout > * { min-width: 0; } | |
| 342 | .split > *, .repo-layout > * { min-width: 0; } | |
| 343 | 343 | |
| 344 | 344 | /* The generic two-column page: content, then a sidebar that sticks. */ |
| 345 | 345 | .split { display: grid; gap: 16px; margin-top: 16px; align-items: start; } |
| @@ -348,8 +348,14 @@ form.inline { display: inline; margin-left: auto; } | ||
| 348 | 348 | .split > aside { position: sticky; top: calc(var(--topbar-h) + 16px); } |
| 349 | 349 | } |
| 350 | 350 | |
| 351 | .dash { display: grid; gap: 16px; align-items: start; } | |
| 352 | @media (min-width: 900px) { .dash { grid-template-columns: minmax(0, 1fr) 320px; gap: 24px; } } | |
| 351 | /* The studio's wide rail (home, an app): a column of cards that runs taller | |
| 352 | than a screen, so it scrolls with the page instead of sticking, sits flush | |
| 353 | under the tabs or header, and takes its two columns from 900px. */ | |
| 354 | .split[data-rail="wide"] { margin-top: 0; } | |
| 355 | @media (min-width: 900px) { | |
| 356 | .split[data-rail="wide"] { grid-template-columns: minmax(0, 1fr) 320px; gap: 24px; } | |
| 357 | .split[data-rail="wide"] > aside { position: static; } | |
| 358 | } | |
| 353 | 359 | |
| 354 | 360 | /* ---- code view ---- */ |
| 355 | 361 | .code { overflow-x: auto; scrollbar-width: thin; } |
test/studio.test.ts +10
| @@ -272,6 +272,16 @@ describe("the studio", () => { | ||
| 272 | 272 | expect(studio).toContain('class="card comment-form"'); |
| 273 | 273 | }); |
| 274 | 274 | |
| 275 | test("the home and an app's page lay main and rail with .split's wide rail; no dash class is left (#4748)", async () => { | |
| 276 | for (const path of ["/", "/alice/notes"]) { | |
| 277 | const page = await (await get(path)).text(); | |
| 278 | expect(page).not.toMatch(/class="[^"]*\bdash(?:-main|-rail)?\b/); | |
| 279 | expect(page).toContain('<div class="split" data-rail="wide">'); | |
| 280 | } | |
| 281 | const css = await (await fetch(`${base}${stylesheetAsset().path}`)).text(); | |
| 282 | expect(css).not.toMatch(/\.dash\b/); | |
| 283 | }); | |
| 284 | ||
| 275 | 285 | test("telling the crew opens an issue and hands it to a builder", async () => { |
| 276 | 286 | const told = await post("/alice/notes/tell", { |
| 277 | 287 | text: "Make the notes page load in under a second on my phone.\n\nIt takes three seconds now.", |
new above
offline