Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31158cdb1a | ||
|
|
7b04575ed5 | ||
|
|
80af2e6816 |
+6
-2
@@ -37,6 +37,10 @@ export function requestOpenSpawn(): void {
|
||||
* connection pool. */
|
||||
export const FOCUS_PANE_EVENT = "catalog-focus-pane";
|
||||
|
||||
/** Ask the shell to open a draft pane seeded from an existing session
|
||||
* ("Spawn similar"). Detail is the session being cloned. */
|
||||
export const SPAWN_SIMILAR_EVENT = "catalog-spawn-similar";
|
||||
|
||||
/** The rows a keyboard walk stops on, in visual (document) order. Expanded
|
||||
* folder headers are skipped: a directory is not something to focus, and the
|
||||
* cards under it are one row away anyway. A *collapsed* folder still counts —
|
||||
@@ -169,7 +173,7 @@ function renderNode(node: TreeNode, depth: number): HTMLElement {
|
||||
similarBtn.addEventListener("click", (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
document.dispatchEvent(new CustomEvent("catalog-spawn-similar", { detail: s }));
|
||||
document.dispatchEvent(new CustomEvent(SPAWN_SIMILAR_EVENT, { detail: s }));
|
||||
});
|
||||
const killBtn = row.querySelector(".leaf-kill") as HTMLButtonElement;
|
||||
killBtn.addEventListener("click", (ev) => {
|
||||
@@ -185,7 +189,7 @@ function renderNode(node: TreeNode, depth: number): HTMLElement {
|
||||
{
|
||||
label: "Spawn similar",
|
||||
onSelect: () =>
|
||||
document.dispatchEvent(new CustomEvent("catalog-spawn-similar", { detail: s })),
|
||||
document.dispatchEvent(new CustomEvent(SPAWN_SIMILAR_EVENT, { detail: s })),
|
||||
},
|
||||
{ label: "Kill session", danger: true, onSelect: () => void confirmAndKill(s, name) },
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { OPEN_SPAWN_EVENT, SPAWN_SIMILAR_EVENT } from "./catalog.js";
|
||||
import { installDrawer, type Drawer } from "./drawer.js";
|
||||
|
||||
function mount(): { shell: HTMLElement; sidebar: HTMLElement; drawer: Drawer } {
|
||||
const shell = document.createElement("div");
|
||||
const sidebar = document.createElement("aside");
|
||||
const backdrop = document.createElement("div");
|
||||
const row = document.createElement("div");
|
||||
row.className = "tree-leaf";
|
||||
sidebar.appendChild(row);
|
||||
shell.append(sidebar, backdrop);
|
||||
document.body.replaceChildren(shell);
|
||||
const drawer = installDrawer(shell, sidebar, backdrop);
|
||||
drawer.open();
|
||||
return { shell, sidebar, drawer };
|
||||
}
|
||||
|
||||
describe("mobile drawer", () => {
|
||||
beforeEach(() => document.body.replaceChildren());
|
||||
|
||||
it("closes when a session row is tapped", () => {
|
||||
const { shell, sidebar } = mount();
|
||||
(sidebar.querySelector(".tree-leaf") as HTMLElement).click();
|
||||
expect(shell.classList.contains("drawer-open")).toBe(false);
|
||||
});
|
||||
|
||||
it("closes when a spawn is requested", () => {
|
||||
const { shell } = mount();
|
||||
document.dispatchEvent(new CustomEvent(OPEN_SPAWN_EVENT));
|
||||
expect(shell.classList.contains("drawer-open")).toBe(false);
|
||||
});
|
||||
|
||||
it("closes when spawn similar is requested", () => {
|
||||
const { shell } = mount();
|
||||
document.dispatchEvent(new CustomEvent(SPAWN_SIMILAR_EVENT, { detail: {} }));
|
||||
expect(shell.classList.contains("drawer-open")).toBe(false);
|
||||
});
|
||||
|
||||
it("leaves the backdrop click closing it", () => {
|
||||
const { shell } = mount();
|
||||
(shell.lastElementChild as HTMLElement).click();
|
||||
expect(shell.classList.contains("drawer-open")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
// Mobile off-canvas sidebar (issue #35); on desktop the `drawer-open` class is
|
||||
// inert (CSS gates the drawer styling).
|
||||
//
|
||||
// Anything that puts a new view in the content area must close the drawer, or
|
||||
// it renders behind the sidebar and the tap reads as a dead button. "Spawn
|
||||
// similar" sits inside a session row but stops propagation, so it never reaches
|
||||
// the row's own close — both spawn paths arrive here via document events.
|
||||
|
||||
import { OPEN_SPAWN_EVENT, SPAWN_SIMILAR_EVENT } from "./catalog.js";
|
||||
|
||||
export interface Drawer {
|
||||
open(): void;
|
||||
close(): void;
|
||||
toggle(): void;
|
||||
isOpen(): boolean;
|
||||
}
|
||||
|
||||
export function installDrawer(
|
||||
shellEl: HTMLElement,
|
||||
sidebarEl: HTMLElement,
|
||||
backdropEl: HTMLElement,
|
||||
): Drawer {
|
||||
const drawer: Drawer = {
|
||||
open: () => shellEl.classList.add("drawer-open"),
|
||||
close: () => shellEl.classList.remove("drawer-open"),
|
||||
toggle: () => shellEl.classList.toggle("drawer-open"),
|
||||
isOpen: () => shellEl.classList.contains("drawer-open"),
|
||||
};
|
||||
|
||||
backdropEl.addEventListener("click", () => drawer.close());
|
||||
sidebarEl.addEventListener("click", (e) => {
|
||||
if ((e.target as HTMLElement).closest(".tree-leaf")) drawer.close();
|
||||
});
|
||||
document.addEventListener(OPEN_SPAWN_EVENT, () => drawer.close());
|
||||
document.addEventListener(SPAWN_SIMILAR_EVENT, () => drawer.close());
|
||||
|
||||
return drawer;
|
||||
}
|
||||
|
||||
/** Horizontal swipes drive the drawer (right opens, left closes) rather than
|
||||
* the browser's back/forward navigation. Only a clearly horizontal gesture past
|
||||
* a threshold counts, so vertical terminal scrolling (which owns its own touch
|
||||
* handler) is untouched. */
|
||||
export function installDrawerSwipe(drawer: Drawer): void {
|
||||
const mobileMq = window.matchMedia("(max-width: 859px)");
|
||||
let sx = 0;
|
||||
let sy = 0;
|
||||
let tracking = false;
|
||||
document.addEventListener(
|
||||
"touchstart",
|
||||
(e) => {
|
||||
tracking = mobileMq.matches && e.touches.length === 1;
|
||||
if (tracking) {
|
||||
sx = e.touches[0].clientX;
|
||||
sy = e.touches[0].clientY;
|
||||
}
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
document.addEventListener(
|
||||
"touchend",
|
||||
(e) => {
|
||||
if (!tracking) return;
|
||||
tracking = false;
|
||||
const t = e.changedTouches[0];
|
||||
const dx = t.clientX - sx;
|
||||
const dy = t.clientY - sy;
|
||||
if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy) * 1.5) return;
|
||||
if (dx > 0 && !drawer.isOpen()) drawer.open();
|
||||
else if (dx < 0 && drawer.isOpen()) drawer.close();
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
}
|
||||
+9
-55
@@ -4,6 +4,7 @@ import "@fontsource/jetbrains-mono/600.css";
|
||||
import "@fontsource/jetbrains-mono/700.css";
|
||||
import { renderSidebar, renderTopbar } from "./catalog.js";
|
||||
import { ConnectionPool } from "./connections.js";
|
||||
import { installDrawer, installDrawerSwipe } from "./drawer.js";
|
||||
import { installHudKeybind } from "./debug.js";
|
||||
import { installHelpKeybind, isHelpOpen, isTypingTarget } from "./help.js";
|
||||
import { CatalogStore } from "./events.js";
|
||||
@@ -25,7 +26,7 @@ import { mountHud, type Hud } from "./hud.js";
|
||||
import { emptyLayout, type Layout, openSessions } from "./layout.js";
|
||||
import { createMultiView, type MultiView } from "./multiview.js";
|
||||
import { loadLayout, saveLayout } from "./persistence.js";
|
||||
import { FOCUS_PANE_EVENT, OPEN_SPAWN_EVENT } from "./catalog.js";
|
||||
import { FOCUS_PANE_EVENT, OPEN_SPAWN_EVENT, SPAWN_SIMILAR_EVENT } from "./catalog.js";
|
||||
import { initTheme } from "./theme.js";
|
||||
import { sessionName, type Project, type Session, type SpawnPreset } from "./types.js";
|
||||
import { installViewportSizing } from "./viewport.js";
|
||||
@@ -92,60 +93,13 @@ const content = document.createElement("section");
|
||||
content.className = "content";
|
||||
|
||||
// Mobile (issue #35): the sidebar is an off-canvas drawer toggled by the ☰ in
|
||||
// the header; a backdrop covers the content while it's open, and tapping a
|
||||
// session closes it. On desktop these are inert (CSS gates the drawer styling).
|
||||
// the header. `installDrawer` owns the close rules (session tap, Spawn, Spawn
|
||||
// similar, backdrop); on desktop the class it toggles is inert.
|
||||
const backdrop = document.createElement("div");
|
||||
backdrop.className = "mv-backdrop";
|
||||
function closeDrawer(): void {
|
||||
shellEl.classList.remove("drawer-open");
|
||||
}
|
||||
function openDrawer(): void {
|
||||
shellEl.classList.add("drawer-open");
|
||||
}
|
||||
function toggleDrawer(): void {
|
||||
shellEl.classList.toggle("drawer-open");
|
||||
}
|
||||
backdrop.addEventListener("click", closeDrawer);
|
||||
sidebar.el.addEventListener("click", (e) => {
|
||||
if ((e.target as HTMLElement).closest(".tree-leaf")) closeDrawer();
|
||||
});
|
||||
|
||||
// Mobile: horizontal swipes drive the drawer (right opens, left closes) rather
|
||||
// than the browser's back/forward navigation. We only act on a clearly
|
||||
// horizontal gesture past a threshold, so vertical terminal scrolling (which
|
||||
// owns its own touch handler) is untouched.
|
||||
{
|
||||
const mobileMq = window.matchMedia("(max-width: 859px)");
|
||||
let sx = 0;
|
||||
let sy = 0;
|
||||
let tracking = false;
|
||||
document.addEventListener(
|
||||
"touchstart",
|
||||
(e) => {
|
||||
tracking = mobileMq.matches && e.touches.length === 1;
|
||||
if (tracking) {
|
||||
sx = e.touches[0].clientX;
|
||||
sy = e.touches[0].clientY;
|
||||
}
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
document.addEventListener(
|
||||
"touchend",
|
||||
(e) => {
|
||||
if (!tracking) return;
|
||||
tracking = false;
|
||||
const t = e.changedTouches[0];
|
||||
const dx = t.clientX - sx;
|
||||
const dy = t.clientY - sy;
|
||||
if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy) * 1.5) return;
|
||||
const open = shellEl.classList.contains("drawer-open");
|
||||
if (dx > 0 && !open) openDrawer();
|
||||
else if (dx < 0 && open) closeDrawer();
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
}
|
||||
const drawer = installDrawer(shellEl, sidebar.el, backdrop);
|
||||
installDrawerSwipe(drawer);
|
||||
const toggleDrawer = () => drawer.toggle();
|
||||
|
||||
shellEl.append(sidebar.el, content, backdrop);
|
||||
root.replaceChildren(renderTopbar(store, toggleDrawer), shellEl);
|
||||
@@ -284,7 +238,7 @@ function requestOpenSpawn(): void {
|
||||
mv.openDraft(undefined, { newTab: true });
|
||||
}
|
||||
document.addEventListener(OPEN_SPAWN_EVENT, () => requestOpenSpawn());
|
||||
document.addEventListener("catalog-spawn-similar", (ev) => {
|
||||
document.addEventListener(SPAWN_SIMILAR_EVENT, (ev) => {
|
||||
const s = (ev as CustomEvent<Session>).detail;
|
||||
showMultiView();
|
||||
mv.openDraft(spawnSimilarSeed(s));
|
||||
@@ -358,7 +312,7 @@ window.addEventListener("keydown", (e) => {
|
||||
if (sidebar.hasListFocus()) {
|
||||
pool.focusTerminal();
|
||||
} else {
|
||||
openDrawer(); // no-op on desktop; on mobile the list is off-canvas
|
||||
drawer.open(); // no-op on desktop; on mobile the list is off-canvas
|
||||
sidebar.focusList();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -432,3 +432,94 @@ describe("multiview closeSession tab cleanup (#137)", () => {
|
||||
expect(layout.activeTabId).toBe(tab.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("multiview mobile single pane", () => {
|
||||
/** Force the mobile media query on, whatever happy-dom's viewport says. */
|
||||
function forceMobile(): void {
|
||||
vi.spyOn(window, "matchMedia").mockImplementation(
|
||||
(query: string) =>
|
||||
({
|
||||
matches: query.includes("859"),
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
addListener() {},
|
||||
removeListener() {},
|
||||
dispatchEvent: () => false,
|
||||
}) as unknown as MediaQueryList,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
stubFetch();
|
||||
forceMobile();
|
||||
document.body.replaceChildren();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("replaces the focused pane instead of splitting beside it", () => {
|
||||
const layout = emptyLayout();
|
||||
const tab = layout.tabs[0];
|
||||
tab.focusedLeafId = splitLeaf(tab, "", "s1", "row", true);
|
||||
const mv = makeMv(layout);
|
||||
document.body.appendChild(mv.el);
|
||||
mv.render();
|
||||
|
||||
mv.openSessionBeside("s2");
|
||||
|
||||
const ls = leaves(tab.root);
|
||||
expect(ls).toHaveLength(1);
|
||||
expect(ls[0].sessionId).toBe("s2");
|
||||
expect(layout.tabs).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("keeps a spawn draft in the current pane rather than a new tab", async () => {
|
||||
const layout = emptyLayout();
|
||||
const tab = layout.tabs[0];
|
||||
tab.focusedLeafId = splitLeaf(tab, "", "s1", "row", true);
|
||||
const mv = makeMv(layout);
|
||||
document.body.appendChild(mv.el);
|
||||
mv.render();
|
||||
|
||||
mv.openDraft(undefined, { newTab: true });
|
||||
await flush();
|
||||
|
||||
expect(layout.tabs).toHaveLength(1);
|
||||
const ls = leaves(tab.root);
|
||||
expect(ls).toHaveLength(1);
|
||||
expect(ls[0].sessionId).toBeNull();
|
||||
expect(ls[0].draft).not.toBeNull();
|
||||
});
|
||||
|
||||
it("renders one pane and no pane pills for a multi-leaf tab", () => {
|
||||
const layout = emptyLayout();
|
||||
const tab = layout.tabs[0];
|
||||
const first = splitLeaf(tab, "", "s1", "row", true);
|
||||
tab.focusedLeafId = splitLeaf(tab, first, "s2", "row", true);
|
||||
const mv = makeMv(layout);
|
||||
document.body.appendChild(mv.el);
|
||||
mv.render();
|
||||
|
||||
expect(mv.el.querySelectorAll(".mv-pane")).toHaveLength(1);
|
||||
expect(mv.el.querySelector(".mv-panebar")).toBeNull();
|
||||
expect(mv.el.querySelector(".mv-pill")).toBeNull();
|
||||
});
|
||||
|
||||
it("collapses the tab bar even with several tabs", () => {
|
||||
const layout = emptyLayout();
|
||||
const first = layout.tabs[0];
|
||||
first.focusedLeafId = splitLeaf(first, "", "s1", "row", true);
|
||||
const second = addTab(layout);
|
||||
second.focusedLeafId = splitLeaf(second, "", "s2", "row", true);
|
||||
const mv = makeMv(layout);
|
||||
document.body.appendChild(mv.el);
|
||||
mv.render();
|
||||
|
||||
expect(mv.el.classList.contains("mv-mobile-solo")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
+19
-27
@@ -84,9 +84,11 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
||||
canvas.className = "mv-canvas";
|
||||
el.append(tabBar, canvas);
|
||||
|
||||
// Mobile (issue #35): below 860px the tree projects to a one-pane-at-a-time
|
||||
// strip of pills — splits/drag/resize are desktop-only. The tree is untouched,
|
||||
// so widening restores the splits.
|
||||
// Mobile (issue #35): below 860px the layout collapses to exactly one visible
|
||||
// pane and no tab bar — splits/drag/resize are desktop-only, and opening or
|
||||
// spawning replaces the pane rather than adding a second thing to navigate.
|
||||
// The sidebar drawer is the only navigation. The tree is untouched, so
|
||||
// widening restores whatever splits and tabs a desktop session built.
|
||||
const mq = window.matchMedia("(max-width: 859px)");
|
||||
let isMobile = mq.matches;
|
||||
mq.addEventListener("change", (e) => {
|
||||
@@ -391,7 +393,9 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
||||
return;
|
||||
}
|
||||
const target = findLeaf(tab.root, tab.focusedLeafId)!;
|
||||
if (!target.sessionId) {
|
||||
// Mobile shows one pane, so there is no "beside": take over the pane. The
|
||||
// evicted session keeps running and stays a tap away in the sidebar.
|
||||
if (!target.sessionId || isMobile) {
|
||||
target.sessionId = sessionId;
|
||||
target.draft = null;
|
||||
} else {
|
||||
@@ -403,7 +407,9 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
||||
|
||||
function openDraft(seed?: Partial<Draft>, opts?: { newTab?: boolean }): void {
|
||||
const draft = (): Draft => ({ command: "", cwd: "", name: "", ...seed });
|
||||
if (opts?.newTab) {
|
||||
// Mobile keeps a single tab and a single pane: the spawn form takes over the
|
||||
// current pane instead of opening a tab or a split.
|
||||
if (opts?.newTab && !isMobile) {
|
||||
// Spawn opens its own tab rather than halving whatever you were reading
|
||||
// (#100). The empty-pane reuse below still applies within a tab.
|
||||
const fresh = addTab(layout);
|
||||
@@ -421,9 +427,10 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
||||
return;
|
||||
}
|
||||
const target = findLeaf(tab.root, tab.focusedLeafId)!;
|
||||
if (!target.sessionId) {
|
||||
if (!target.sessionId || isMobile) {
|
||||
// Empty = no session, not `!draft` (#72): the mounted form auto-syncs
|
||||
// picked defaults into `draft` once ready, so that's never reliably null.
|
||||
target.sessionId = null;
|
||||
target.draft = draft();
|
||||
const stale = draftForms.get(target.id);
|
||||
if (stale) {
|
||||
@@ -462,9 +469,9 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
||||
renderTabs();
|
||||
canvas.replaceChildren();
|
||||
const tab = activeTab(layout);
|
||||
// Mobile single-tab: the tab bar carries no information (one tab, one pane),
|
||||
// so CSS collapses it and we float a ☰ instead (rendered in renderMobile).
|
||||
el.classList.toggle("mv-mobile-solo", isMobile && layout.tabs.length === 1);
|
||||
// Mobile: the tab bar is a second navigation surface we don't want, so CSS
|
||||
// collapses it; the header ☰ keeps the sidebar drawer reachable.
|
||||
el.classList.toggle("mv-mobile-solo", isMobile);
|
||||
|
||||
if (!tab.root) {
|
||||
pruneForms(tab);
|
||||
@@ -496,7 +503,9 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
||||
persist();
|
||||
}
|
||||
|
||||
// Mobile: a pill strip of the active tab's leaves + one full-screen pane.
|
||||
// Mobile: the focused leaf, full screen, alone. Panes a desktop session left
|
||||
// behind stay in the tree but are reached only by tapping the session in the
|
||||
// sidebar drawer — mobile shows no pane switcher of its own.
|
||||
function renderMobile(tab: { root: Node | null; focusedLeafId: string | null }): void {
|
||||
const ls = leaves(tab.root);
|
||||
const focused = (tab.focusedLeafId && findLeaf(tab.root, tab.focusedLeafId)) || ls[0];
|
||||
@@ -504,23 +513,6 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "mv-mobile";
|
||||
|
||||
if (ls.length > 1) {
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "mv-panebar";
|
||||
for (const l of ls) {
|
||||
const pill = document.createElement("button");
|
||||
pill.type = "button";
|
||||
pill.className = "mv-pill" + (l.id === focused.id ? " is-active" : "");
|
||||
pill.textContent = l.sessionId ? (infoFor(l.sessionId)?.name ?? l.sessionId.slice(0, 8)) : "empty";
|
||||
pill.addEventListener("click", () => {
|
||||
tab.focusedLeafId = l.id;
|
||||
render();
|
||||
});
|
||||
bar.appendChild(pill);
|
||||
}
|
||||
wrap.appendChild(bar);
|
||||
}
|
||||
|
||||
const pane = buildPane(focused);
|
||||
pane.el.classList.add("is-focused"); // the only pane — never dimmed
|
||||
wrap.appendChild(pane.el);
|
||||
|
||||
+4
-30
@@ -2202,8 +2202,8 @@ input.leaf-rename:focus {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
/* Mobile, single tab: the tab bar is pure chrome — collapse it (a floating ☰
|
||||
inside the pane keeps the sidebar drawer reachable). */
|
||||
/* Mobile: the tab bar is a second navigation surface — collapse it (a floating
|
||||
☰ inside the pane keeps the sidebar drawer reachable). */
|
||||
.mv-mobile-solo > .mv-tabs {
|
||||
display: none;
|
||||
}
|
||||
@@ -2482,8 +2482,8 @@ input.leaf-rename:focus {
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
multi-view mobile (#35): below 860px the split tree projects to a pill strip
|
||||
with one pane visible; the sidebar becomes an off-canvas drawer (☰).
|
||||
multi-view mobile (#35): below 860px only the focused pane is shown — no tab
|
||||
bar, no pane switcher; the sidebar drawer (☰) is the navigation.
|
||||
========================================================================= */
|
||||
.mv-backdrop {
|
||||
display: none;
|
||||
@@ -2503,32 +2503,6 @@ input.leaf-rename:focus {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.mv-panebar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: var(--sp-1) var(--sp-2);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
flex: none;
|
||||
}
|
||||
.mv-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
padding: 7px var(--sp-3);
|
||||
border-radius: var(--r-1);
|
||||
background: var(--surface-2);
|
||||
color: var(--muted);
|
||||
font: 600 var(--fs-12) / 1 var(--font-mono);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.mv-pill.is-active {
|
||||
background: var(--accent-tint);
|
||||
color: var(--accent-strong);
|
||||
border-color: var(--accent-ring);
|
||||
}
|
||||
.mv-mobile > .mv-pane {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
Reference in New Issue
Block a user