feat(maestro): colour the previewed pane and its sidebar row
A preview was distinguished only by the chip and a dashed ring, both easy to miss, and the sidebar's hover reused the neutral surface — so a hovered row said nothing about what the content pane was showing. Adds a `--preview` token family on a hue of its own, deliberately not the accent: the accent means "this is the one you picked", which a preview is not. The previewed pane's titlebar, the chip, the throwaway tab, the previewed sidebar row and hover all carry it. Palettes whose accent is already violet (periwinkle, orchid) move the hue via `--preview-h`.
This commit is contained in:
@@ -32,6 +32,29 @@ The throwaway tab never counts as an open session: the tab bar does not appear
|
|||||||
because of it, the mobile shell does not switch away from the session list for
|
because of it, the mobile shell does not switch away from the session list for
|
||||||
it, and the sidebar does not mark the session as open.
|
it, and the sidebar does not mark the session as open.
|
||||||
|
|
||||||
|
## How a preview reads
|
||||||
|
|
||||||
|
A preview is provisional, so it never wears the accent — the accent is what
|
||||||
|
says "this is the one you picked". It has a hue of its own, and every surface
|
||||||
|
involved in the preview carries that same hue, so the pane and the row it came
|
||||||
|
from read as one thing rather than two unrelated highlights.
|
||||||
|
|
||||||
|
Web client (`--preview` and friends in `web/src/styles.css`):
|
||||||
|
|
||||||
|
| Surface | While previewing |
|
||||||
|
|---|---|
|
||||||
|
| The previewed pane's titlebar | Tinted in the preview hue, at full strength, in place of the focused pane's accent tint. |
|
||||||
|
| The canvas | A `preview` chip, top right, and a dashed inset ring. |
|
||||||
|
| A throwaway tab | Dashed border, italic label. |
|
||||||
|
| The sidebar row being previewed | Preview tint + ring, over whatever open/focused state the row already had. |
|
||||||
|
| A hovered sidebar row | Washed with the preview hue, because hovering is what previews it. |
|
||||||
|
|
||||||
|
The hue is a palette token, not a fixed colour: a palette whose accent already
|
||||||
|
sits on it moves it (`--preview-h`), so "previewing" and "picked" never collide.
|
||||||
|
|
||||||
|
The TUI marks a preview in text only — the title bar reads `<name> · preview`
|
||||||
|
— and has no preview colour role yet.
|
||||||
|
|
||||||
## What ends a preview
|
## What ends a preview
|
||||||
|
|
||||||
- The highlight moves to another session — that session is previewed instead.
|
- The highlight moves to another session — that session is previewed instead.
|
||||||
|
|||||||
@@ -342,6 +342,9 @@ export interface SidebarHandle {
|
|||||||
setActive(id: string | null): void;
|
setActive(id: string | null): void;
|
||||||
/** Apply the full open/focused highlight state. */
|
/** Apply the full open/focused highlight state. */
|
||||||
setHighlights(h: SidebarHighlights): void;
|
setHighlights(h: SidebarHighlights): void;
|
||||||
|
/** Mark the row the content pane is previewing (docs/PREVIEW.md), or clear it
|
||||||
|
* with `null`. A preview is a view, not state, so this only paints. */
|
||||||
|
setPreview(sessionId: string | null): void;
|
||||||
/** Hand the keyboard to the session list, highlighting a row to walk from. */
|
/** Hand the keyboard to the session list, highlighting a row to walk from. */
|
||||||
focusList(): void;
|
focusList(): void;
|
||||||
/** Whether the session list currently holds DOM focus. */
|
/** Whether the session list currently holds DOM focus. */
|
||||||
@@ -501,6 +504,10 @@ export function renderSidebar(store: CatalogStore, opts: SidebarOptions = {}): S
|
|||||||
});
|
});
|
||||||
|
|
||||||
let highlights: SidebarHighlights = { open: new Set(), focused: null };
|
let highlights: SidebarHighlights = { open: new Set(), focused: null };
|
||||||
|
// The previewed row is painted alongside the open/focused tiers so a
|
||||||
|
// re-render can't drop it, but it is never part of `highlights`: a preview
|
||||||
|
// is a view, and nothing about it is durable (docs/PREVIEW.md).
|
||||||
|
let previewing: string | null = null;
|
||||||
const applyHighlights = () => {
|
const applyHighlights = () => {
|
||||||
for (const n of treeHost.querySelectorAll<HTMLElement>(".tree-leaf")) {
|
for (const n of treeHost.querySelectorAll<HTMLElement>(".tree-leaf")) {
|
||||||
const id = n.dataset.sessionId;
|
const id = n.dataset.sessionId;
|
||||||
@@ -509,6 +516,7 @@ export function renderSidebar(store: CatalogStore, opts: SidebarOptions = {}): S
|
|||||||
const open = highlights.open.has(id);
|
const open = highlights.open.has(id);
|
||||||
n.classList.toggle("is-active", focused);
|
n.classList.toggle("is-active", focused);
|
||||||
n.classList.toggle("is-open", open && !focused);
|
n.classList.toggle("is-open", open && !focused);
|
||||||
|
n.classList.toggle("is-previewing", previewing === id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -692,6 +700,10 @@ export function renderSidebar(store: CatalogStore, opts: SidebarOptions = {}): S
|
|||||||
highlights = h;
|
highlights = h;
|
||||||
applyHighlights();
|
applyHighlights();
|
||||||
},
|
},
|
||||||
|
setPreview(sessionId: string | null) {
|
||||||
|
previewing = sessionId;
|
||||||
|
applyHighlights();
|
||||||
|
},
|
||||||
focusList() {
|
focusList() {
|
||||||
// Start from the session on screen — that is where the user's attention
|
// Start from the session on screen — that is where the user's attention
|
||||||
// already is — falling back to the standing highlight, then the top row.
|
// already is — falling back to the standing highlight, then the top row.
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ export function renderKit(): HTMLElement {
|
|||||||
"--muted",
|
"--muted",
|
||||||
"--faint",
|
"--faint",
|
||||||
"--accent",
|
"--accent",
|
||||||
|
"--preview",
|
||||||
"--danger",
|
"--danger",
|
||||||
"--warn",
|
"--warn",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ function fakeSidebar(): SidebarHandle {
|
|||||||
el: document.createElement("aside"),
|
el: document.createElement("aside"),
|
||||||
setActive() {},
|
setActive() {},
|
||||||
setHighlights() {},
|
setHighlights() {},
|
||||||
|
setPreview() {},
|
||||||
focusList() {},
|
focusList() {},
|
||||||
hasListFocus() {
|
hasListFocus() {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -221,6 +221,12 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
|||||||
const tab = activeTab(layout);
|
const tab = activeTab(layout);
|
||||||
for (const pane of canvas.querySelectorAll<HTMLElement>(".mv-pane")) {
|
for (const pane of canvas.querySelectorAll<HTMLElement>(".mv-pane")) {
|
||||||
pane.classList.toggle("is-focused", pane.dataset.leafId === tab.focusedLeafId);
|
pane.classList.toggle("is-focused", pane.dataset.leafId === tab.focusedLeafId);
|
||||||
|
// The previewed pane's titlebar wears the preview colour instead of the
|
||||||
|
// accent tint a focused pane gets (docs/PREVIEW.md).
|
||||||
|
pane.classList.toggle(
|
||||||
|
"is-preview",
|
||||||
|
preview !== null && pane.dataset.sessionId === preview.sessionId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// A preview is not a focused pane: reporting it would clear the session's
|
// A preview is not a focused pane: reporting it would clear the session's
|
||||||
// `AwaitingInput` glow just because the operator scrolled past it.
|
// `AwaitingInput` glow just because the operator scrolled past it.
|
||||||
@@ -229,6 +235,9 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
|||||||
open: realOpenSessions(),
|
open: realOpenSessions(),
|
||||||
focused: focusedLeafSession(),
|
focused: focusedLeafSession(),
|
||||||
});
|
});
|
||||||
|
// The row the preview came from wears the same colour, so the sidebar and
|
||||||
|
// the pane read as one thing rather than two unrelated highlights.
|
||||||
|
sidebar.setPreview(preview?.sessionId ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusLeaf(leafId: string): void {
|
function focusLeaf(leafId: string): void {
|
||||||
@@ -549,6 +558,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
|||||||
if (!preview) pool.setFocus(null);
|
if (!preview) pool.setFocus(null);
|
||||||
canvas.appendChild(renderWelcome());
|
canvas.appendChild(renderWelcome());
|
||||||
sidebar.setHighlights({ open: realOpenSessions(), focused: null });
|
sidebar.setHighlights({ open: realOpenSessions(), focused: null });
|
||||||
|
sidebar.setPreview(preview?.sessionId ?? null);
|
||||||
persist();
|
persist();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -588,6 +598,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
|||||||
|
|
||||||
const pane = buildPane(focused);
|
const pane = buildPane(focused);
|
||||||
pane.el.classList.add("is-focused"); // the only pane — never dimmed
|
pane.el.classList.add("is-focused"); // the only pane — never dimmed
|
||||||
|
if (preview && focused.sessionId === preview.sessionId) pane.el.classList.add("is-preview");
|
||||||
wrap.appendChild(pane.el);
|
wrap.appendChild(pane.el);
|
||||||
canvas.appendChild(wrap);
|
canvas.appendChild(wrap);
|
||||||
|
|
||||||
@@ -598,6 +609,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
|
|||||||
);
|
);
|
||||||
if (!preview) pool.setFocus(focused.sessionId ?? null);
|
if (!preview) pool.setFocus(focused.sessionId ?? null);
|
||||||
sidebar.setHighlights({ open: realOpenSessions(), focused: focused.sessionId ?? null });
|
sidebar.setHighlights({ open: realOpenSessions(), focused: focused.sessionId ?? null });
|
||||||
|
sidebar.setPreview(preview?.sessionId ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSession(sessionId: string): void {
|
function closeSession(sessionId: string): void {
|
||||||
|
|||||||
@@ -45,11 +45,17 @@ function fakePool(): Spy {
|
|||||||
return { pool, visible: () => visible, focusCalls, mountedWithFocus };
|
return { pool, visible: () => visible, focusCalls, mountedWithFocus };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Last value the controller pushed to `SidebarHandle.setPreview`. */
|
||||||
|
const previewRow: { id: string | null } = { id: null };
|
||||||
|
|
||||||
function fakeSidebar(): SidebarHandle {
|
function fakeSidebar(): SidebarHandle {
|
||||||
return {
|
return {
|
||||||
el: document.createElement("aside"),
|
el: document.createElement("aside"),
|
||||||
setActive() {},
|
setActive() {},
|
||||||
setHighlights() {},
|
setHighlights() {},
|
||||||
|
setPreview(id: string | null) {
|
||||||
|
previewRow.id = id;
|
||||||
|
},
|
||||||
focusList() {},
|
focusList() {},
|
||||||
hasListFocus() {
|
hasListFocus() {
|
||||||
return false;
|
return false;
|
||||||
@@ -88,6 +94,7 @@ describe("sidebar preview", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
stubFetch();
|
stubFetch();
|
||||||
document.body.replaceChildren();
|
document.body.replaceChildren();
|
||||||
|
previewRow.id = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -110,6 +117,23 @@ describe("sidebar preview", () => {
|
|||||||
expect(serializeLayout(layout)).toEqual(before);
|
expect(serializeLayout(layout)).toEqual(before);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("marks the previewed pane and its sidebar row, and unmarks them after", () => {
|
||||||
|
const layout = twoTabs();
|
||||||
|
const spy = fakePool();
|
||||||
|
const mv = makeMv(layout, spy.pool);
|
||||||
|
|
||||||
|
mv.previewSession("s1");
|
||||||
|
expect(previewRow.id).toBe("s1");
|
||||||
|
expect(mv.el.querySelectorAll(".mv-pane.is-preview")).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
(mv.el.querySelector(".mv-pane.is-preview") as HTMLElement).dataset.sessionId,
|
||||||
|
).toBe("s1");
|
||||||
|
|
||||||
|
mv.previewSession(null);
|
||||||
|
expect(previewRow.id).toBeNull();
|
||||||
|
expect(mv.el.querySelectorAll(".mv-pane.is-preview")).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("shows a session no pane holds in a throwaway tab that leaves no trace", () => {
|
it("shows a session no pane holds in a throwaway tab that leaves no trace", () => {
|
||||||
const layout = twoTabs();
|
const layout = twoTabs();
|
||||||
const before = serializeLayout(layout);
|
const before = serializeLayout(layout);
|
||||||
|
|||||||
+64
-8
@@ -87,6 +87,29 @@
|
|||||||
--accent-tint: oklch(0.92 0.005 256 / 0.1);
|
--accent-tint: oklch(0.92 0.005 256 / 0.1);
|
||||||
--accent-ring: oklch(0.92 0.005 256 / 0.5);
|
--accent-ring: oklch(0.92 0.005 256 / 0.5);
|
||||||
|
|
||||||
|
/* preview (docs/PREVIEW.md): a provisional view needs a colour that is not
|
||||||
|
the accent, because the accent is what says "this is the one you picked".
|
||||||
|
Defined once, off a single hue variable, so a palette whose accent already
|
||||||
|
sits on that hue can move it (see periwinkle/orchid below); `light-dark()`
|
||||||
|
keeps the light pair here instead of in all eighteen theme blocks. */
|
||||||
|
--preview-h: 305;
|
||||||
|
--preview: light-dark(
|
||||||
|
oklch(0.55 0.16 var(--preview-h)),
|
||||||
|
oklch(0.78 0.14 var(--preview-h))
|
||||||
|
);
|
||||||
|
--preview-ink: light-dark(
|
||||||
|
oklch(0.99 0.005 var(--preview-h)),
|
||||||
|
oklch(0.19 0.045 var(--preview-h))
|
||||||
|
);
|
||||||
|
--preview-tint: light-dark(
|
||||||
|
oklch(0.55 0.16 var(--preview-h) / 0.12),
|
||||||
|
oklch(0.78 0.14 var(--preview-h) / 0.16)
|
||||||
|
);
|
||||||
|
--preview-ring: light-dark(
|
||||||
|
oklch(0.55 0.16 var(--preview-h) / 0.5),
|
||||||
|
oklch(0.78 0.14 var(--preview-h) / 0.6)
|
||||||
|
);
|
||||||
|
|
||||||
--live: oklch(0.84 0.17 152);
|
--live: oklch(0.84 0.17 152);
|
||||||
--live-glow: oklch(0.84 0.17 152 / 0.55);
|
--live-glow: oklch(0.84 0.17 152 / 0.55);
|
||||||
|
|
||||||
@@ -256,6 +279,9 @@
|
|||||||
--accent-ink: oklch(0.18 0.04 278);
|
--accent-ink: oklch(0.18 0.04 278);
|
||||||
--accent-tint: oklch(0.75 0.125 275 / 0.14);
|
--accent-tint: oklch(0.75 0.125 275 / 0.14);
|
||||||
--accent-ring: oklch(0.75 0.125 275 / 0.55);
|
--accent-ring: oklch(0.75 0.125 275 / 0.55);
|
||||||
|
/* This accent is already violet, so preview moves to cyan to stay legible
|
||||||
|
as "not the accent". Set once here: the light blocks below don't touch it. */
|
||||||
|
--preview-h: 195;
|
||||||
--term-bg: oklch(0.155 0.018 278);
|
--term-bg: oklch(0.155 0.018 278);
|
||||||
}
|
}
|
||||||
:root[data-palette="periwinkle"][data-theme="light"],
|
:root[data-palette="periwinkle"][data-theme="light"],
|
||||||
@@ -437,6 +463,8 @@
|
|||||||
--accent-ink: oklch(0.22 0.05 310);
|
--accent-ink: oklch(0.22 0.05 310);
|
||||||
--accent-tint: oklch(0.78 0.13 310 / 0.14);
|
--accent-tint: oklch(0.78 0.13 310 / 0.14);
|
||||||
--accent-ring: oklch(0.78 0.13 310 / 0.55);
|
--accent-ring: oklch(0.78 0.13 310 / 0.55);
|
||||||
|
/* Same collision as periwinkle: an orchid accent needs a non-purple preview. */
|
||||||
|
--preview-h: 195;
|
||||||
--term-bg: oklch(0.155 0.014 305);
|
--term-bg: oklch(0.155 0.014 305);
|
||||||
}
|
}
|
||||||
:root[data-palette="orchid"][data-theme="light"],
|
:root[data-palette="orchid"][data-theme="light"],
|
||||||
@@ -1125,12 +1153,14 @@ textarea {
|
|||||||
background: color-mix(in oklab, var(--accent) 6%, transparent);
|
background: color-mix(in oklab, var(--accent) 6%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hovering a row is what previews it (docs/PREVIEW.md), so hover reads in the
|
||||||
|
preview hue — never the accent, which belongs to the row actually open. */
|
||||||
.tree-leaf:hover {
|
.tree-leaf:hover {
|
||||||
background: var(--surface-2);
|
background: color-mix(in oklab, var(--preview) 12%, var(--surface-2));
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-leaf[data-status="running"]:hover {
|
.tree-leaf[data-status="running"]:hover {
|
||||||
background: color-mix(in oklab, var(--accent) 13%, var(--surface-2));
|
background: color-mix(in oklab, var(--preview) 18%, var(--surface-2));
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-leaf:focus-visible {
|
.tree-leaf:focus-visible {
|
||||||
@@ -1141,7 +1171,10 @@ textarea {
|
|||||||
/* Where the keyboard walk (j/k) is standing. Distinct from `.is-active`: this
|
/* Where the keyboard walk (j/k) is standing. Distinct from `.is-active`: this
|
||||||
is a cursor, not a statement about what is mounted, so it reads as a ring
|
is a cursor, not a statement about what is mounted, so it reads as a ring
|
||||||
rather than a fill and the two can sit on the same row. */
|
rather than a fill and the two can sit on the same row. */
|
||||||
.tree-leaf.is-highlighted,
|
.tree-leaf.is-highlighted {
|
||||||
|
box-shadow: inset 0 0 0 2px var(--preview-ring);
|
||||||
|
}
|
||||||
|
|
||||||
.tree-folder-header.is-highlighted {
|
.tree-folder-header.is-highlighted {
|
||||||
box-shadow: inset 0 0 0 2px var(--accent-ring);
|
box-shadow: inset 0 0 0 2px var(--accent-ring);
|
||||||
}
|
}
|
||||||
@@ -1161,6 +1194,15 @@ textarea {
|
|||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The row the content pane is previewing right now — after `.is-active` so a
|
||||||
|
preview of an already-open session still reads as provisional. */
|
||||||
|
.tree-leaf.is-previewing,
|
||||||
|
.tree-leaf.is-previewing[data-status="running"],
|
||||||
|
.tree-leaf.is-previewing.is-active {
|
||||||
|
background: var(--preview-tint);
|
||||||
|
box-shadow: inset 0 0 0 2px var(--preview-ring);
|
||||||
|
}
|
||||||
|
|
||||||
/* Exited sessions recede: dimmed name + an "exited" badge carry status, so it's
|
/* Exited sessions recede: dimmed name + an "exited" badge carry status, so it's
|
||||||
never the running tint's absence alone doing the work. */
|
never the running tint's absence alone doing the work. */
|
||||||
.tree-leaf[data-status="dead"] {
|
.tree-leaf[data-status="dead"] {
|
||||||
@@ -2884,10 +2926,14 @@ input.leaf-rename:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Preview (docs/PREVIEW.md): the content pane is showing a session the
|
/* Preview (docs/PREVIEW.md): the content pane is showing a session the
|
||||||
operator only highlighted, so it reads as provisional — a dashed tab and a
|
operator only highlighted, so it reads as provisional — the preview hue on
|
||||||
dashed inset ring on the canvas — and reverts when the highlight moves. */
|
the pane's titlebar, a dashed tab, a dashed inset ring on the canvas — and
|
||||||
|
reverts when the highlight moves. The hue is deliberately not the accent:
|
||||||
|
the accent means "this is what you picked", which a preview is not. */
|
||||||
.mv-tab.is-preview {
|
.mv-tab.is-preview {
|
||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
|
border-color: var(--preview-ring);
|
||||||
|
color: var(--preview);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
.mv.mv-previewing .mv-canvas::before {
|
.mv.mv-previewing .mv-canvas::before {
|
||||||
@@ -2900,8 +2946,8 @@ input.leaf-rename:focus {
|
|||||||
border-radius: var(--r-1);
|
border-radius: var(--r-1);
|
||||||
font: 500 var(--fs-11) / 1.5 var(--font-mono);
|
font: 500 var(--fs-11) / 1.5 var(--font-mono);
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
color: var(--accent-ink);
|
color: var(--preview-ink);
|
||||||
background: var(--accent);
|
background: var(--preview);
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -2910,7 +2956,7 @@ input.leaf-rename:focus {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
border: 1px dashed var(--accent-ring);
|
border: 1px dashed var(--preview-ring);
|
||||||
border-radius: var(--r-1);
|
border-radius: var(--r-1);
|
||||||
z-index: var(--z-sticky);
|
z-index: var(--z-sticky);
|
||||||
}
|
}
|
||||||
@@ -3028,6 +3074,16 @@ input.leaf-rename:focus {
|
|||||||
.mv-pane:not(.is-focused) .attach-header {
|
.mv-pane:not(.is-focused) .attach-header {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
/* The previewed pane's titlebar carries the preview hue, so the chip is not the
|
||||||
|
only thing saying the pane is provisional — and so it is never mistaken for
|
||||||
|
the accent tint of the pane the operator actually focused. It keeps full
|
||||||
|
strength even though a preview never takes focus. */
|
||||||
|
.mv-pane.is-preview .attach-header,
|
||||||
|
.mv-pane.is-preview.is-focused .attach-header {
|
||||||
|
background: color-mix(in oklab, var(--preview) 34%, var(--surface));
|
||||||
|
box-shadow: inset 0 -1px 0 var(--preview-ring);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
/* A pane with no session yet: a quiet hint rather than blank terminal black. */
|
/* A pane with no session yet: a quiet hint rather than blank terminal black. */
|
||||||
.mv-pane-empty {
|
.mv-pane-empty {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user