Spaces:
Paused
Paused
Dropdowns: compact (rows 24, inset 4, surface 10)
Browse files- 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..
|
| 10791 |
--- /dev/null
|
| 10792 |
+++ b/app/ui.css
|
| 10793 |
-
@@ -0,0 +1,
|
| 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
|
| 10808 |
-
+
|
| 10809 |
-
+
|
| 10810 |
-
+
|
| 10811 |
-
+
|
| 10812 |
-
+ -
|
| 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
|
| 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
|
| 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
|
|
|
|
|
|
|
| 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:
|
| 10830 |
-
+ --popup-inset:
|
| 10831 |
-
+ --popup-row-radius:
|
| 10832 |
-
+ --popup-row-height:
|
| 10833 |
-
+ --popup-row-pad:
|
| 10834 |
+ --popup-icon: 14px;
|
| 10835 |
-
+ --popup-gap:
|
| 10836 |
-
+ --popup-text-gap:
|
| 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:
|
| 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:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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) +
|
| 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:
|
| 11026 |
+ display: flex;
|
| 11027 |
+ align-items: center;
|
| 11028 |
+ justify-content: center;
|
| 11029 |
-
+ width:
|
| 11030 |
-
+ height:
|
| 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(--
|
| 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
|
| 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(--
|
| 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
|
| 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(--
|
| 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(--
|
| 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(--
|
| 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(--
|
| 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(--
|
| 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..
|
| 32277 |
--- /dev/null
|
| 32278 |
+++ b/components/ui/dropdowns.test.mjs
|
| 32279 |
-
@@ -0,0 +1,
|
| 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(
|
| 32302 |
-
+
|
| 32303 |
-
+
|
| 32304 |
-
+ assert.ok(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32305 |
+ }
|
| 32306 |
-
+ assert.
|
|
|
|
| 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..
|
| 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**,
|
| 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`, `--
|
| 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
|