Mike0021 commited on
Commit
dd98527
·
verified ·
1 Parent(s): 9ba3ca6

Dropdowns: compact (rows 24, inset 4, surface 10)

Browse files
Files changed (1) hide show
  1. piweb/hf-design.patch +79 -49
piweb/hf-design.patch CHANGED
@@ -10787,10 +10787,10 @@ index 0b8f66a..9a60b64 100644
10787
  .sidebar-tool-button.is-active,
10788
  diff --git a/app/ui.css b/app/ui.css
10789
  new file mode 100644
10790
- index 0000000..32e2abf
10791
  --- /dev/null
10792
  +++ b/app/ui.css
10793
- @@ -0,0 +1,935 @@
10794
  +/* The shared components on Base UI (components/ui/*): menus, tooltips,
10795
  + popovers and the composer's suggestion list. Popups are portaled to <body>,
10796
  + so they stack by the positioner's z-index, above every pane. */
@@ -10804,37 +10804,43 @@ index 0000000..32e2abf
10804
  +
10805
  +/* ---------------------------------------------------------------- dropdowns
10806
  + Every dropdown (menus, the composer's list, popovers, the sidebar's menus,
10807
- + the model field, the header's panels) keeps one set of proportions, measured
10808
- + on design/dropdown-spacing-reference.jpg and design/model-dropdown-reference.jpg
10809
- + (phone captures at 2.81x: 15.7px text, rows 32.7px, rows inset 9.3px and
10810
- + rounded 8px, surfaces rounded 14-18px, icons 16.7px and text 44.5px from the
10811
- + edge) and scaled to Pi Web's 13px:
10812
- + - a 14px-rounded surface, set off by a hairline and a soft shadow;
10813
- + - rows inset 7px from its edge and rounded 7px, so the two curves run
10814
  + parallel (the surface's radius is the inset plus the row's);
10815
- + - one icon column 14px from the edge and one text column at 37px; whatever
10816
  + has no icon starts on the icon column, as the references' section labels
10817
  + and search line do: section labels, filters, headers, and rows without an
10818
  + icon (the effort levels), never an empty icon's room before a label;
10819
- + - rows 28px, back to back; a section label at 12px in --text-dim, in
10820
  + sentence case; a separator edge to edge with the inset above and below;
10821
  + - a row's description inline after its label, the same size, in --text-dim.
10822
- + A touch screen takes the reference's own scale (15px text, 32px rows).
 
 
10823
  + A dropdown that scrolls keeps its rows centred: its scrollbar is as wide as
10824
  + the inset and stands in the right inset's place (`--popup-inset-end`, set by
10825
  + components/ui/scrollbar-inset.ts while a bar takes room). */
10826
  +:root {
10827
  + --popup-font: 13px;
10828
  + --popup-label-font: 12px;
10829
- + --popup-radius: 14px;
10830
- + --popup-inset: 7px;
10831
- + --popup-row-radius: 7px;
10832
- + --popup-row-height: 28px;
10833
- + --popup-row-pad: 7px;
10834
  + --popup-icon: 14px;
10835
- + --popup-gap: 9px;
10836
- + --popup-text-gap: 10px;
10837
  + --popup-edge: calc(var(--popup-inset) + var(--popup-row-pad));
 
 
 
 
 
10838
  + /* The right inset: the scrollbar's place while one takes room. */
10839
  + --popup-inset-end: var(--popup-inset);
10840
  + --popup-hairline: color-mix(in srgb, var(--text-strong) 10%, transparent);
@@ -10891,7 +10897,8 @@ index 0000000..32e2abf
10891
  + background: var(--line);
10892
  +}
10893
  +/* Scrolling: the bar takes the right inset (components/ui/scrollbar-inset.ts
10894
- + measures it), as wide as the inset in Chromium and Safari, thin in Firefox. */
 
10895
  +.ui-menu[data-scrollbar],
10896
  +.ui-composer-list-scroll[data-scrollbar],
10897
  +.ui-dropdown-scroll[data-scrollbar],
@@ -10901,14 +10908,18 @@ index 0000000..32e2abf
10901
  +.ui-menu::-webkit-scrollbar,
10902
  +.ui-composer-list-scroll::-webkit-scrollbar,
10903
  +.ui-dropdown-scroll::-webkit-scrollbar,
10904
- +.sidebar-menu::-webkit-scrollbar { width: 7px; }
10905
  +.ui-menu::-webkit-scrollbar-thumb,
10906
  +.ui-composer-list-scroll::-webkit-scrollbar-thumb,
10907
  +.ui-dropdown-scroll::-webkit-scrollbar-thumb,
10908
- +.sidebar-menu::-webkit-scrollbar-thumb { border-width: 1.5px; }
 
 
 
 
10909
  +/* Where the surface itself scrolls, the thumb keeps off its rounded corners. */
10910
  +.ui-menu::-webkit-scrollbar-track,
10911
- +.sidebar-menu::-webkit-scrollbar-track { margin: 7px 0; }
10912
  +@media (pointer: coarse) {
10913
  + .ui-menu::-webkit-scrollbar,
10914
  + .ui-composer-list-scroll::-webkit-scrollbar,
@@ -10918,8 +10929,6 @@ index 0000000..32e2abf
10918
  + .ui-composer-list-scroll::-webkit-scrollbar-thumb,
10919
  + .ui-dropdown-scroll::-webkit-scrollbar-thumb,
10920
  + .sidebar-menu::-webkit-scrollbar-thumb { border-width: 2px; }
10921
- + .ui-menu::-webkit-scrollbar-track,
10922
- + .sidebar-menu::-webkit-scrollbar-track { margin: 8px 0; }
10923
  +}
10924
  +@supports not selector(::-webkit-scrollbar) {
10925
  + .ui-menu,
@@ -11015,19 +11024,19 @@ index 0000000..32e2abf
11015
  +}
11016
  +.ui-dropdown-row:focus-visible { outline: none; box-shadow: none; }
11017
  +.ui-dropdown-row[aria-selected="true"] { color: var(--text-strong); }
11018
- +.ui-dropdown-row.has-gutter { padding-right: calc(var(--popup-row-pad) + 28px); }
11019
  +.ui-dropdown-icon { display: inline-flex; flex: none; align-items: center; justify-content: center; width: var(--popup-icon); height: var(--popup-icon); color: var(--text-muted); }
11020
  +.ui-dropdown-row-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
11021
  +.ui-dropdown-check { display: flex; flex: none; width: 14px; color: var(--text-muted); }
11022
  +.ui-dropdown-star {
11023
  + position: absolute;
11024
  + top: 50%;
11025
- + right: 4px;
11026
  + display: flex;
11027
  + align-items: center;
11028
  + justify-content: center;
11029
- + width: 24px;
11030
- + height: 24px;
11031
  + padding: 0;
11032
  + border: 0;
11033
  + border-radius: calc(var(--popup-row-radius) - 2px);
@@ -11450,7 +11459,7 @@ index 0000000..32e2abf
11450
  + width: min(640px, calc(100vw - 32px));
11451
  + max-height: min(560px, calc(100dvh - max(12vh, 56px) - 24px));
11452
  + overflow: hidden;
11453
- + border-radius: var(--popup-radius);
11454
  + background: var(--bg-elevated);
11455
  + box-shadow: var(--popup-shadow);
11456
  + color: var(--text);
@@ -11503,7 +11512,7 @@ index 0000000..32e2abf
11503
  + display: flex;
11504
  + flex-shrink: 0;
11505
  + gap: 8px;
11506
- + padding: 10px calc(var(--popup-inset) + var(--popup-row-pad)) 4px;
11507
  + color: var(--text-dim);
11508
  + font-size: var(--popup-label-font);
11509
  +}
@@ -11511,7 +11520,7 @@ index 0000000..32e2abf
11511
  +.command-bar-list {
11512
  + flex: 1 1 auto;
11513
  + min-height: 0;
11514
- + padding: 0 var(--popup-inset) var(--popup-inset);
11515
  + overflow-y: auto;
11516
  + overscroll-behavior: contain;
11517
  +}
@@ -11566,7 +11575,7 @@ index 0000000..32e2abf
11566
  + components/DirectoryPicker.tsx: the command bar's surface (a dropdown's, larger), a
11567
  + 15px title, a path row of 32px controls (up, the path as code, Go), the folders as
11568
  + rows in the menus' proportions, and the footer's actions in their hierarchy. Title,
11569
- + folder icons and New folder's icon share the menus' edge (--popup-edge); on a touch
11570
  + screen the popup tokens take the references' larger scale. */
11571
  +.directory-picker-backdrop {
11572
  + position: fixed;
@@ -11585,7 +11594,7 @@ index 0000000..32e2abf
11585
  + max-width: 100%;
11586
  + height: min(560px, 100%);
11587
  + overflow: hidden;
11588
- + border-radius: var(--popup-radius);
11589
  + background: var(--bg-elevated);
11590
  + box-shadow: var(--popup-shadow);
11591
  + color: var(--text);
@@ -11596,7 +11605,7 @@ index 0000000..32e2abf
11596
  + align-items: center;
11597
  + gap: 8px;
11598
  + height: 52px;
11599
- + padding: 0 var(--popup-inset) 0 var(--popup-edge);
11600
  +}
11601
  +.directory-picker-title {
11602
  + flex: 1 1 auto;
@@ -11614,7 +11623,7 @@ index 0000000..32e2abf
11614
  + flex-shrink: 0;
11615
  + align-items: center;
11616
  + gap: 6px;
11617
- + padding: 0 var(--popup-inset) var(--popup-inset);
11618
  + border-bottom: 1px solid var(--popup-hairline);
11619
  +}
11620
  +.directory-picker-path {
@@ -11636,7 +11645,7 @@ index 0000000..32e2abf
11636
  + flex: 1 1 auto;
11637
  + flex-direction: column;
11638
  + min-height: 0;
11639
- + padding: var(--popup-inset);
11640
  + overflow-y: auto;
11641
  + overscroll-behavior: contain;
11642
  +}
@@ -11687,7 +11696,7 @@ index 0000000..32e2abf
11687
  + align-items: center;
11688
  + justify-content: flex-end;
11689
  + gap: 8px;
11690
- + padding: 10px var(--popup-inset);
11691
  + border-top: 1px solid var(--popup-hairline);
11692
  +}
11693
  +/* Quiet, on the left, its icon on the folders' column. */
@@ -32273,10 +32282,10 @@ index 0000000..c9e3f78
32273
  +}
32274
  diff --git a/components/ui/dropdowns.test.mjs b/components/ui/dropdowns.test.mjs
32275
  new file mode 100644
32276
- index 0000000..d9595de
32277
  --- /dev/null
32278
  +++ b/components/ui/dropdowns.test.mjs
32279
- @@ -0,0 +1,122 @@
32280
  +import assert from "node:assert/strict";
32281
  +import { readFileSync, readdirSync } from "node:fs";
32282
  +import test from "node:test";
@@ -32293,17 +32302,38 @@ index 0000000..d9595de
32293
  +test("every dropdown's surface radius is its inset plus its rows' radius, at both scales", () => {
32294
  + const desktop = tokens(css.slice(css.indexOf(":root {\n --popup-font"), css.indexOf("html.dark {\n --popup-shadow")));
32295
  + const touch = { ...desktop, ...tokens(css.slice(css.indexOf("@media (pointer: coarse)"), css.indexOf("/* The surface, shared by every dropdown. */"))) };
 
 
 
 
 
 
 
32296
  + for (const [name, scale] of [["desktop", desktop], ["touch", touch]]) {
32297
  + const px = (key) => Number.parseFloat(scale[key]);
 
 
32298
  + assert.equal(px("popup-radius"), px("popup-inset") + px("popup-row-radius"), `${name}: concentric corners`);
32299
- + // The reference (at 2.81x): 15.7px text, text column 44.5px from the edge, rows 32.7px.
32300
  + const textColumn = px("popup-inset") + px("popup-row-pad") + px("popup-icon") + px("popup-gap");
32301
- + assert.ok(Math.abs(textColumn / px("popup-font") - 44.5 / 15.7) < 0.12, `${name}: text column ${textColumn}px for ${scale["popup-font"]}`);
32302
- + assert.ok(Math.abs(px("popup-row-height") / px("popup-font") - 32.7 / 15.7) < 0.1, `${name}: row height`);
32303
- + // Rows inset about 0.6 of the text size (9.3px at 15.7px).
32304
- + assert.ok(Math.abs(px("popup-inset") / px("popup-font") - 9.3 / 15.7) < 0.08, `${name}: inset`);
 
 
 
 
 
 
 
 
 
 
 
 
32305
  + }
32306
- + assert.equal(touch["popup-font"], "15px", "a touch screen takes the reference's own scale");
 
32307
  +});
32308
  +
32309
  +test("every popup surface shares the dropdown rules", () => {
@@ -35276,7 +35306,7 @@ index 7fb1c0c..9c6aa29 100644
35276
 
35277
  diff --git a/docs/agents/desktop-shell.md b/docs/agents/desktop-shell.md
35278
  new file mode 100644
35279
- index 0000000..0f1c98c
35280
  --- /dev/null
35281
  +++ b/docs/agents/desktop-shell.md
35282
  @@ -0,0 +1,75 @@
@@ -35285,7 +35315,7 @@ index 0000000..0f1c98c
35285
  +Pi Web's frame follows Claude Code Desktop's Code tab: a sidebar of sessions, a header, then the conversation and the panes it opened as tiles. Pi's power stays reachable through "/", the composer's "+" menu, the title's session menu and Settings rather than permanent chrome. Popups are Base UI (`@base-ui/react` 1.8.0) through the wrappers in `components/ui/`, styled in `app/ui.css`; the header and tiles in `app/shell.css`. The Code tab is claude.ai web code (its chunks are on assets-proxy.anthropic.com, no Cloudflare there); the tile behaviour below was read from its tile engine and side-pane store, not from screenshots. The conversation outline (upstream's ChatMinimap) is removed.
35286
  +
35287
  +## Dropdowns (`app/ui.css` "dropdowns", `components/ui/menu.tsx`, `app/sidebar-menu.css`)
35288
- +- **One set of proportions for every dropdown**, measured on `design/dropdown-spacing-reference.jpg` and `design/model-dropdown-reference.jpg` (phone captures at 2.81x: 15.7px text, rows 32.7px, rows inset 9.3px and rounded about 8px, surfaces rounded 14-18px, icons 16.7px and text 44.5px from the edge) and kept as `--popup-*` tokens: at 13px, a 14px surface (hairline + soft shadow, `--popup-shadow`), rows 28px inset 7px and rounded 7px (the surface's radius is the inset plus the row's), icons 14px from the edge, text at 37px, section labels 12px `--text-dim` in sentence case on the icon column, separators edge to edge with the inset above and below, a row's description inline after its label. Touch screens (`pointer: coarse`) take the reference's own scale: 15px, 32px rows, 8px inset, 16px surface. A dropdown that scrolls keeps its rows centred: Pi Web's Chromium scrollbars take room (10px, `app/globals.css`), which held a scrolling menu's rows 7px from the left and 17px from the right; dropdown scrollers take a scrollbar as wide as the inset, and `components/ui/scrollbar-inset.ts` marks the scroller (`data-scrollbar`, `--popup-scrollbar`) while one shows so the right inset (`--popup-inset-end`) gives way to it. Separators then end at the bar. Headless Chromium hides scrollbars: tests that check this launch without `--hide-scrollbars`. `components/ui/dropdowns.test.mjs` checks the ratios against the reference and that the sidebar's menus use the tokens.
35289
  +- Who follows it: Base UI menus (`.ui-menu`), the "/" "@" and history list (`.ui-composer-list`: its scroller holds the side inset so a group's line can reach the edges), popovers (content on the icon column), the sidebar's menus and their filter (a plain line like the reference's "Search here…"; the files tab's `classic` look is gone, only paths keep code type), the phone sheet (the same inset rounded rows, 44px), the model field (`ModelSelector`/`SelectorRow`: `.ui-dropdown-*`, check on the right as in the menus), and the header's panels (`.header-dropdown`: session info, system prompt, tools, subagents, branches, inset 8px under the header).
35290
  +- **One icon column**: model rows carry their maker's mark (`components/ModelMark.tsx`: the repo's organisation on a router such as Hugging Face, else the provider; brand colours in grays), whatever has no icon starts on the icon column as the references' section labels and search line do (the effort levels, the model field's clear row: never an empty icon's room before a label), "/" rows show their kind (the "+" menu's icons: commands, connectors, a page for prompts, skills) and history rows a clock.
35291
  +- **`MenuSection`** is the only way to label a group: Base UI throws on a `GroupLabel` outside a `Group`, and the Browser pane's ⋯ menu did that, which took the page down.
@@ -35353,7 +35383,7 @@ index 0000000..0f1c98c
35353
  +Settings › MCP is called Connectors. "Add Hugging Face" goes through the ordinary add route (pi syntax kept, so the header's `!command` stays as written): `Authorization` is printed on every connection from `HF_TOKEN` or the token the Space lends the machine (`~/.cache/huggingface/token`). pi resolves header values uncached (`resolveConfigValueUncached`), so a refreshed token is used at the next connection and a token taken back fails the connection instead of leaking a stale one.
35354
  +
35355
  +## Folder picker (`components/DirectoryPicker.tsx`, `app/ui.css` "folder picker")
35356
- +- The command bar's surface (`--bg-elevated`, `--popup-radius`, `--popup-shadow`), a 15px title, a path row of 32px controls (up, the path in mono on `--bg-surface`, Go as secondary), the folders as rows in the menus' proportions (`--popup-row-height`, `--popup-highlight`; a touch screen takes the references' scale) and a footer of `.ui-btn`s: New directory quiet on the left, Cancel secondary, Select this folder primary. Title, folder icons and New directory's icon share `--popup-edge`.
35357
  +- A new folder is named in a row at the top of the list (Enter creates it and opens it, so Select stays the only commit; Escape closes only the row; a refused name keeps the focus). After a navigation the path shows its end with the caret there, and focus that left with a replaced row returns to the path. Escape closes the picker from anywhere (window, capture phase, marked handled so the global Escape does not also stop a run); Tab stays inside (`keepTabInside`).
35358
  diff --git a/docs/agents/files-and-access.md b/docs/agents/files-and-access.md
35359
  index 71db8fb..eefcad7 100644
 
10787
  .sidebar-tool-button.is-active,
10788
  diff --git a/app/ui.css b/app/ui.css
10789
  new file mode 100644
10790
+ index 0000000..f4133ef
10791
  --- /dev/null
10792
  +++ b/app/ui.css
10793
+ @@ -0,0 +1,944 @@
10794
  +/* The shared components on Base UI (components/ui/*): menus, tooltips,
10795
  + popovers and the composer's suggestion list. Popups are portaled to <body>,
10796
  + so they stack by the positioner's z-index, above every pane. */
 
10804
  +
10805
  +/* ---------------------------------------------------------------- dropdowns
10806
  + Every dropdown (menus, the composer's list, popovers, the sidebar's menus,
10807
+ + the model field, the header's panels) keeps one set of proportions. On a
10808
+ + desktop they are design/dropdown-compact-reference.jpg's (a desktop capture
10809
+ + at 2x: rows 24px, inset 4px, surface rounded 10px, icons centred 19.5px and
10810
+ + text 34px from the edge, a separator 4px from the rows):
10811
+ + - a 10px-rounded surface, set off by a hairline and a soft shadow;
10812
+ + - rows inset 4px from its edge and rounded 6px, so the two curves run
 
10813
  + parallel (the surface's radius is the inset plus the row's);
10814
+ + - one icon column 12px from the edge and one text column at 34px; whatever
10815
  + has no icon starts on the icon column, as the references' section labels
10816
  + and search line do: section labels, filters, headers, and rows without an
10817
  + icon (the effort levels), never an empty icon's room before a label;
10818
+ + - rows 24px, back to back; a section label at 12px in --text-dim, in
10819
  + sentence case; a separator edge to edge with the inset above and below;
10820
  + - a row's description inline after its label, the same size, in --text-dim.
10821
+ + A touch screen keeps the phone captures' scale, rows a finger can take
10822
+ + (design/dropdown-spacing-reference.jpg and design/model-dropdown-reference.jpg
10823
+ + at 2.81x: 15px text, 32px rows inset 8px).
10824
  + A dropdown that scrolls keeps its rows centred: its scrollbar is as wide as
10825
  + the inset and stands in the right inset's place (`--popup-inset-end`, set by
10826
  + components/ui/scrollbar-inset.ts while a bar takes room). */
10827
  +:root {
10828
  + --popup-font: 13px;
10829
  + --popup-label-font: 12px;
10830
+ + --popup-radius: 10px;
10831
+ + --popup-inset: 4px;
10832
+ + --popup-row-radius: 6px;
10833
+ + --popup-row-height: 24px;
10834
+ + --popup-row-pad: 8px;
10835
  + --popup-icon: 14px;
10836
+ + --popup-gap: 8px;
10837
+ + --popup-text-gap: 8px;
10838
  + --popup-edge: calc(var(--popup-inset) + var(--popup-row-pad));
10839
+ + /* Dialogs on the same surface (the command bar, the folder picker) are larger than
10840
+ + a dropdown: their rows take the dropdowns' rows, inset further from the edge. */
10841
+ + --dialog-inset: 8px;
10842
+ + --dialog-radius: calc(var(--dialog-inset) + var(--popup-row-radius));
10843
+ + --dialog-edge: calc(var(--dialog-inset) + var(--popup-row-pad));
10844
  + /* The right inset: the scrollbar's place while one takes room. */
10845
  + --popup-inset-end: var(--popup-inset);
10846
  + --popup-hairline: color-mix(in srgb, var(--text-strong) 10%, transparent);
 
10897
  + background: var(--line);
10898
  +}
10899
  +/* Scrolling: the bar takes the right inset (components/ui/scrollbar-inset.ts
10900
+ + measures it), as wide as the inset in Chromium and Safari, thin in Firefox;
10901
+ + its thumb fills it while the pointer is on it. */
10902
  +.ui-menu[data-scrollbar],
10903
  +.ui-composer-list-scroll[data-scrollbar],
10904
  +.ui-dropdown-scroll[data-scrollbar],
 
10908
  +.ui-menu::-webkit-scrollbar,
10909
  +.ui-composer-list-scroll::-webkit-scrollbar,
10910
  +.ui-dropdown-scroll::-webkit-scrollbar,
10911
+ +.sidebar-menu::-webkit-scrollbar { width: 4px; }
10912
  +.ui-menu::-webkit-scrollbar-thumb,
10913
  +.ui-composer-list-scroll::-webkit-scrollbar-thumb,
10914
  +.ui-dropdown-scroll::-webkit-scrollbar-thumb,
10915
+ +.sidebar-menu::-webkit-scrollbar-thumb { border-width: 0.5px; }
10916
+ +.ui-menu::-webkit-scrollbar-thumb:hover,
10917
+ +.ui-composer-list-scroll::-webkit-scrollbar-thumb:hover,
10918
+ +.ui-dropdown-scroll::-webkit-scrollbar-thumb:hover,
10919
+ +.sidebar-menu::-webkit-scrollbar-thumb:hover { border-width: 0; }
10920
  +/* Where the surface itself scrolls, the thumb keeps off its rounded corners. */
10921
  +.ui-menu::-webkit-scrollbar-track,
10922
+ +.sidebar-menu::-webkit-scrollbar-track { margin: var(--popup-radius) 0; }
10923
  +@media (pointer: coarse) {
10924
  + .ui-menu::-webkit-scrollbar,
10925
  + .ui-composer-list-scroll::-webkit-scrollbar,
 
10929
  + .ui-composer-list-scroll::-webkit-scrollbar-thumb,
10930
  + .ui-dropdown-scroll::-webkit-scrollbar-thumb,
10931
  + .sidebar-menu::-webkit-scrollbar-thumb { border-width: 2px; }
 
 
10932
  +}
10933
  +@supports not selector(::-webkit-scrollbar) {
10934
  + .ui-menu,
 
11024
  +}
11025
  +.ui-dropdown-row:focus-visible { outline: none; box-shadow: none; }
11026
  +.ui-dropdown-row[aria-selected="true"] { color: var(--text-strong); }
11027
+ +.ui-dropdown-row.has-gutter { padding-right: calc(var(--popup-row-pad) + var(--popup-row-height)); }
11028
  +.ui-dropdown-icon { display: inline-flex; flex: none; align-items: center; justify-content: center; width: var(--popup-icon); height: var(--popup-icon); color: var(--text-muted); }
11029
  +.ui-dropdown-row-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
11030
  +.ui-dropdown-check { display: flex; flex: none; width: 14px; color: var(--text-muted); }
11031
  +.ui-dropdown-star {
11032
  + position: absolute;
11033
  + top: 50%;
11034
+ + right: 2px;
11035
  + display: flex;
11036
  + align-items: center;
11037
  + justify-content: center;
11038
+ + width: calc(var(--popup-row-height) - 4px);
11039
+ + height: calc(var(--popup-row-height) - 4px);
11040
  + padding: 0;
11041
  + border: 0;
11042
  + border-radius: calc(var(--popup-row-radius) - 2px);
 
11459
  + width: min(640px, calc(100vw - 32px));
11460
  + max-height: min(560px, calc(100dvh - max(12vh, 56px) - 24px));
11461
  + overflow: hidden;
11462
+ + border-radius: var(--dialog-radius);
11463
  + background: var(--bg-elevated);
11464
  + box-shadow: var(--popup-shadow);
11465
  + color: var(--text);
 
11512
  + display: flex;
11513
  + flex-shrink: 0;
11514
  + gap: 8px;
11515
+ + padding: 10px var(--dialog-edge) 4px;
11516
  + color: var(--text-dim);
11517
  + font-size: var(--popup-label-font);
11518
  +}
 
11520
  +.command-bar-list {
11521
  + flex: 1 1 auto;
11522
  + min-height: 0;
11523
+ + padding: 0 var(--dialog-inset) var(--dialog-inset);
11524
  + overflow-y: auto;
11525
  + overscroll-behavior: contain;
11526
  +}
 
11575
  + components/DirectoryPicker.tsx: the command bar's surface (a dropdown's, larger), a
11576
  + 15px title, a path row of 32px controls (up, the path as code, Go), the folders as
11577
  + rows in the menus' proportions, and the footer's actions in their hierarchy. Title,
11578
+ + folder icons and New folder's icon share the dialogs' edge (--dialog-edge); on a touch
11579
  + screen the popup tokens take the references' larger scale. */
11580
  +.directory-picker-backdrop {
11581
  + position: fixed;
 
11594
  + max-width: 100%;
11595
  + height: min(560px, 100%);
11596
  + overflow: hidden;
11597
+ + border-radius: var(--dialog-radius);
11598
  + background: var(--bg-elevated);
11599
  + box-shadow: var(--popup-shadow);
11600
  + color: var(--text);
 
11605
  + align-items: center;
11606
  + gap: 8px;
11607
  + height: 52px;
11608
+ + padding: 0 var(--dialog-inset) 0 var(--dialog-edge);
11609
  +}
11610
  +.directory-picker-title {
11611
  + flex: 1 1 auto;
 
11623
  + flex-shrink: 0;
11624
  + align-items: center;
11625
  + gap: 6px;
11626
+ + padding: 0 var(--dialog-inset) var(--dialog-inset);
11627
  + border-bottom: 1px solid var(--popup-hairline);
11628
  +}
11629
  +.directory-picker-path {
 
11645
  + flex: 1 1 auto;
11646
  + flex-direction: column;
11647
  + min-height: 0;
11648
+ + padding: var(--dialog-inset);
11649
  + overflow-y: auto;
11650
  + overscroll-behavior: contain;
11651
  +}
 
11696
  + align-items: center;
11697
  + justify-content: flex-end;
11698
  + gap: 8px;
11699
+ + padding: 10px var(--dialog-inset);
11700
  + border-top: 1px solid var(--popup-hairline);
11701
  +}
11702
  +/* Quiet, on the left, its icon on the folders' column. */
 
32282
  +}
32283
  diff --git a/components/ui/dropdowns.test.mjs b/components/ui/dropdowns.test.mjs
32284
  new file mode 100644
32285
+ index 0000000..2b08674
32286
  --- /dev/null
32287
  +++ b/components/ui/dropdowns.test.mjs
32288
+ @@ -0,0 +1,143 @@
32289
  +import assert from "node:assert/strict";
32290
  +import { readFileSync, readdirSync } from "node:fs";
32291
  +import test from "node:test";
 
32302
  +test("every dropdown's surface radius is its inset plus its rows' radius, at both scales", () => {
32303
  + const desktop = tokens(css.slice(css.indexOf(":root {\n --popup-font"), css.indexOf("html.dark {\n --popup-shadow")));
32304
  + const touch = { ...desktop, ...tokens(css.slice(css.indexOf("@media (pointer: coarse)"), css.indexOf("/* The surface, shared by every dropdown. */"))) };
32305
+ + // Each scale's reference, in its own pixels: design/dropdown-compact-reference.jpg on a desktop
32306
+ + // (2x: rows 48, inset 7-8, icons centred 39, text at 68, the text 20 tall for 13px), the phone
32307
+ + // captures on a touch screen (2.81x, given at 1x: 15.7px text, rows 32.7, inset 9.3, text at 44.5).
32308
+ + const references = {
32309
+ + desktop: { font: 13, row: 24, inset: 3.75, textColumn: 34, iconCenter: 19.5 },
32310
+ + touch: { font: 15.7, row: 32.7, inset: 9.3, textColumn: 44.5, iconCenter: 16.7 + 16.7 / 2 },
32311
+ + };
32312
  + for (const [name, scale] of [["desktop", desktop], ["touch", touch]]) {
32313
  + const px = (key) => Number.parseFloat(scale[key]);
32314
+ + const ref = references[name];
32315
+ + const relative = (value, target) => Math.abs(value / px("popup-font") - target / ref.font);
32316
  + assert.equal(px("popup-radius"), px("popup-inset") + px("popup-row-radius"), `${name}: concentric corners`);
 
32317
  + const textColumn = px("popup-inset") + px("popup-row-pad") + px("popup-icon") + px("popup-gap");
32318
+ + assert.ok(relative(textColumn, ref.textColumn) < 0.12, `${name}: text column ${textColumn}px for ${scale["popup-font"]}`);
32319
+ + const iconCenter = px("popup-inset") + px("popup-row-pad") + px("popup-icon") / 2;
32320
+ + assert.ok(relative(iconCenter, ref.iconCenter) < 0.12, `${name}: icon column centred at ${iconCenter}px`);
32321
+ + assert.ok(relative(px("popup-row-height"), ref.row) < 0.1, `${name}: row height`);
32322
+ + assert.ok(relative(px("popup-inset"), ref.inset) < 0.08, `${name}: inset`);
32323
+ + }
32324
+ + assert.equal(desktop["popup-row-height"], "24px", "a desktop takes the compact reference's rows");
32325
+ + assert.equal(touch["popup-font"], "15px", "a touch screen keeps the phone captures' scale");
32326
+ +});
32327
+ +
32328
+ +test("dialogs on the dropdowns' surface keep a dialog's inset, their rows the dropdowns'", () => {
32329
+ + const rules = (selector) => [...css.matchAll(new RegExp(`(?:^|\\n)${selector.replace(".", "\\.")}(?:-[a-z-]+)? \\{([^}]*)\\}`, "g"))].map((match) => match[1]).join("\n");
32330
+ + for (const dialog of [".command-bar", ".directory-picker"]) {
32331
+ + const body = rules(dialog);
32332
+ + assert.match(body, /border-radius: var\(--dialog-radius\)/, `${dialog}: a dialog's radius`);
32333
+ + assert.doesNotMatch(body, /var\(--popup-(inset|edge|radius)\)/, `${dialog}: no dropdown inset, edge or radius`);
32334
  + }
32335
+ + assert.match(rules(".directory-picker-entry"), /height: var\(--popup-row-height\)/, "the folders are dropdown rows");
32336
+ + assert.match(css, /--dialog-radius: calc\(var\(--dialog-inset\) \+ var\(--popup-row-radius\)\)/, "concentric corners");
32337
  +});
32338
  +
32339
  +test("every popup surface shares the dropdown rules", () => {
 
35306
 
35307
  diff --git a/docs/agents/desktop-shell.md b/docs/agents/desktop-shell.md
35308
  new file mode 100644
35309
+ index 0000000..a9ccc50
35310
  --- /dev/null
35311
  +++ b/docs/agents/desktop-shell.md
35312
  @@ -0,0 +1,75 @@
 
35315
  +Pi Web's frame follows Claude Code Desktop's Code tab: a sidebar of sessions, a header, then the conversation and the panes it opened as tiles. Pi's power stays reachable through "/", the composer's "+" menu, the title's session menu and Settings rather than permanent chrome. Popups are Base UI (`@base-ui/react` 1.8.0) through the wrappers in `components/ui/`, styled in `app/ui.css`; the header and tiles in `app/shell.css`. The Code tab is claude.ai web code (its chunks are on assets-proxy.anthropic.com, no Cloudflare there); the tile behaviour below was read from its tile engine and side-pane store, not from screenshots. The conversation outline (upstream's ChatMinimap) is removed.
35316
  +
35317
  +## Dropdowns (`app/ui.css` "dropdowns", `components/ui/menu.tsx`, `app/sidebar-menu.css`)
35318
+ +- **One set of proportions for every dropdown**, kept as `--popup-*` tokens. On a desktop, `design/dropdown-compact-reference.jpg`'s (a desktop capture at 2x: rows 24px, inset about 4px, surface rounded 10px, icons centred 19.5px and text 34px from the edge): at 13px, a 10px surface (hairline + soft shadow, `--popup-shadow`), rows 24px inset 4px and rounded 6px (the surface's radius is the inset plus the row's), icons 12px from the edge (centred at 19px), text at 34px, section labels 12px `--text-dim` in sentence case on the icon column, separators edge to edge with the inset above and below, a row's description inline after its label. Touch screens (`pointer: coarse`) keep the phone captures' scale (`design/dropdown-spacing-reference.jpg`, `design/model-dropdown-reference.jpg`, 2.81x): 15px, 32px rows, 8px inset, 16px surface. A dropdown that scrolls keeps its rows centred: Pi Web's Chromium scrollbars take room (10px, `app/globals.css`), which held a scrolling menu's rows 7px from the left and 17px from the right; dropdown scrollers take a scrollbar as wide as the inset (4px, its thumb filling it under the pointer), and `components/ui/scrollbar-inset.ts` marks the scroller (`data-scrollbar`, `--popup-scrollbar`) while one shows so the right inset (`--popup-inset-end`) gives way to it. Separators then end at the bar. Headless Chromium hides scrollbars: tests that check this launch without `--hide-scrollbars`. `components/ui/dropdowns.test.mjs` checks each scale's ratios against its reference and that the sidebar's menus use the tokens.
35319
  +- Who follows it: Base UI menus (`.ui-menu`), the "/" "@" and history list (`.ui-composer-list`: its scroller holds the side inset so a group's line can reach the edges), popovers (content on the icon column), the sidebar's menus and their filter (a plain line like the reference's "Search here…"; the files tab's `classic` look is gone, only paths keep code type), the phone sheet (the same inset rounded rows, 44px), the model field (`ModelSelector`/`SelectorRow`: `.ui-dropdown-*`, check on the right as in the menus), and the header's panels (`.header-dropdown`: session info, system prompt, tools, subagents, branches, inset 8px under the header).
35320
  +- **One icon column**: model rows carry their maker's mark (`components/ModelMark.tsx`: the repo's organisation on a router such as Hugging Face, else the provider; brand colours in grays), whatever has no icon starts on the icon column as the references' section labels and search line do (the effort levels, the model field's clear row: never an empty icon's room before a label), "/" rows show their kind (the "+" menu's icons: commands, connectors, a page for prompts, skills) and history rows a clock.
35321
  +- **`MenuSection`** is the only way to label a group: Base UI throws on a `GroupLabel` outside a `Group`, and the Browser pane's ⋯ menu did that, which took the page down.
 
35383
  +Settings › MCP is called Connectors. "Add Hugging Face" goes through the ordinary add route (pi syntax kept, so the header's `!command` stays as written): `Authorization` is printed on every connection from `HF_TOKEN` or the token the Space lends the machine (`~/.cache/huggingface/token`). pi resolves header values uncached (`resolveConfigValueUncached`), so a refreshed token is used at the next connection and a token taken back fails the connection instead of leaking a stale one.
35384
  +
35385
  +## Folder picker (`components/DirectoryPicker.tsx`, `app/ui.css` "folder picker")
35386
+ +- The command bar's surface (`--bg-elevated`, `--dialog-radius`, `--popup-shadow`; dialogs keep an 8px inset, `--dialog-inset`, where dropdowns have 4px), a 15px title, a path row of 32px controls (up, the path in mono on `--bg-surface`, Go as secondary), the folders as rows in the menus' proportions (`--popup-row-height`, `--popup-highlight`; a touch screen takes the references' scale) and a footer of `.ui-btn`s: New directory quiet on the left, Cancel secondary, Select this folder primary. Title, folder icons and New directory's icon share `--dialog-edge` (16px, the command bar field's).
35387
  +- A new folder is named in a row at the top of the list (Enter creates it and opens it, so Select stays the only commit; Escape closes only the row; a refused name keeps the focus). After a navigation the path shows its end with the caret there, and focus that left with a replaced row returns to the path. Escape closes the picker from anywhere (window, capture phase, marked handled so the global Escape does not also stop a run); Tab stays inside (`keepTabInside`).
35388
  diff --git a/docs/agents/files-and-access.md b/docs/agents/files-and-access.md
35389
  index 71db8fb..eefcad7 100644