feat(web): settings rows on one line with icon actions
Each root and policy row is one line: identity left, chips at the right edge, then pencil/trash icon controls — the episode-row idiom instead of a two-line group. Every icon action's aria-label names the action and the row (edit root /mnt/media/tv/main). Delete keeps the arm-then-confirm behaviour via the shared armedDeleteIcon; the local text-button armedDelete is gone with it. Under 46rem the row wraps like an episode row: identity, chips full-width, actions keeping the right edge. Closes #231 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+21
-55
@@ -2,6 +2,7 @@
|
|||||||
// plus the settings view over them — same reasoning as search.ts: the
|
// plus the settings view over them — same reasoning as search.ts: the
|
||||||
// generated client (src/api/) is uncommitted, so CI's tsc cannot see it.
|
// generated client (src/api/) is uncommitted, so CI's tsc cannot see it.
|
||||||
|
|
||||||
|
import { armedDeleteIcon, icon } from "./icons";
|
||||||
import { navigate } from "./router";
|
import { navigate } from "./router";
|
||||||
import type { Root } from "./search";
|
import type { Root } from "./search";
|
||||||
|
|
||||||
@@ -185,40 +186,14 @@ function listValue(input: HTMLInputElement): string[] {
|
|||||||
.filter((part) => part !== "");
|
.filter((part) => part !== "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Icon-only edit toggle for one row; the label names the row it opens. */
|
||||||
* First click arms the destructive action, second confirms. The armed state
|
function editIcon(label: string): HTMLButtonElement {
|
||||||
* clears on blur or after a few seconds, so an accidental double click
|
const button = el("button", "control control-quiet control-icon");
|
||||||
* never deletes. Shared with the series detail rows — one confirmation
|
|
||||||
* idiom for row-level destruction, not one per page.
|
|
||||||
*/
|
|
||||||
export function armedDelete(label: string, execute: () => void): HTMLButtonElement {
|
|
||||||
const button = el("button", "control control-quiet readout", label);
|
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
let armed = false;
|
button.setAttribute("aria-label", label);
|
||||||
let resetTimer: number | undefined;
|
button.title = label;
|
||||||
const disarm = () => {
|
button.setAttribute("aria-expanded", "false");
|
||||||
armed = false;
|
button.append(icon("pencil"));
|
||||||
window.clearTimeout(resetTimer);
|
|
||||||
delete button.dataset.armed;
|
|
||||||
button.textContent = label;
|
|
||||||
};
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
if (armed) {
|
|
||||||
disarm();
|
|
||||||
button.disabled = true;
|
|
||||||
execute();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
armed = true;
|
|
||||||
button.dataset.armed = "true";
|
|
||||||
button.textContent = `confirm ${label}`;
|
|
||||||
resetTimer = window.setTimeout(disarm, 4000);
|
|
||||||
});
|
|
||||||
button.addEventListener("blur", () => {
|
|
||||||
if (armed) {
|
|
||||||
disarm();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,31 +275,29 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
|||||||
|
|
||||||
/* -- roots -------------------------------------------------------------- */
|
/* -- roots -------------------------------------------------------------- */
|
||||||
|
|
||||||
/** A delete/edit action pair sharing the row's feedback line. */
|
/** The row's line: identity, chips right, the icon action pair, feedback. */
|
||||||
function rowActions(
|
function rowLine(
|
||||||
item: HTMLLIElement,
|
item: HTMLLIElement,
|
||||||
|
title: string,
|
||||||
|
chips: HTMLSpanElement,
|
||||||
del: HTMLButtonElement,
|
del: HTMLButtonElement,
|
||||||
edit: HTMLButtonElement,
|
edit: HTMLButtonElement,
|
||||||
): HTMLParagraphElement {
|
): HTMLParagraphElement {
|
||||||
const actions = el("div", "settings-row-actions");
|
const actions = el("div", "settings-row-actions");
|
||||||
actions.append(edit, del);
|
actions.append(edit, del);
|
||||||
item.append(actions);
|
|
||||||
const note = el("p", "add-note readout");
|
const note = el("p", "add-note readout");
|
||||||
note.setAttribute("role", "status");
|
note.setAttribute("role", "status");
|
||||||
note.hidden = true;
|
note.hidden = true;
|
||||||
item.append(note);
|
item.append(rowTitle(title), chips, actions, note);
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rootRow(root: Root): HTMLLIElement {
|
function rootRow(root: Root): HTMLLIElement {
|
||||||
const item = el("li", "queue-item");
|
const item = el("li", "queue-item settings-row");
|
||||||
const row = el("div", "row");
|
|
||||||
const chips = el("span", "row-chips");
|
const chips = el("span", "row-chips");
|
||||||
chips.append(chip(root.kind), chip(root.audience), chip(root.policy_name));
|
chips.append(chip(root.kind), chip(root.audience), chip(root.policy_name));
|
||||||
row.append(rowTitle(root.path), chips);
|
|
||||||
item.append(row);
|
|
||||||
|
|
||||||
const del = armedDelete("delete", () => {
|
const del = armedDeleteIcon(`delete root ${root.path}`, () => {
|
||||||
void deleteRow("roots", root.id).then((detail) => {
|
void deleteRow("roots", root.id).then((detail) => {
|
||||||
if (detail !== null) {
|
if (detail !== null) {
|
||||||
note.hidden = false;
|
note.hidden = false;
|
||||||
@@ -336,9 +309,7 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const edit = el("button", "control control-quiet readout", "edit");
|
const edit = editIcon(`edit root ${root.path}`);
|
||||||
edit.type = "button";
|
|
||||||
edit.setAttribute("aria-expanded", "false");
|
|
||||||
edit.addEventListener("click", () => {
|
edit.addEventListener("click", () => {
|
||||||
const existing = item.querySelector(".edit-panel");
|
const existing = item.querySelector(".edit-panel");
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -350,7 +321,7 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
|||||||
edit.setAttribute("aria-expanded", "true");
|
edit.setAttribute("aria-expanded", "true");
|
||||||
});
|
});
|
||||||
|
|
||||||
const note = rowActions(item, del, edit);
|
const note = rowLine(item, root.path, chips, del, edit);
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,17 +403,14 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
|||||||
}
|
}
|
||||||
|
|
||||||
function policyRow(policy: PolicyDoc): HTMLLIElement {
|
function policyRow(policy: PolicyDoc): HTMLLIElement {
|
||||||
const item = el("li", "queue-item");
|
const item = el("li", "queue-item settings-row");
|
||||||
const row = el("div", "row");
|
|
||||||
const chips = el("span", "row-chips");
|
const chips = el("span", "row-chips");
|
||||||
chips.append(chip(policy.resolution_pref.join(" › ") || "no resolutions"));
|
chips.append(chip(policy.resolution_pref.join(" › ") || "no resolutions"));
|
||||||
chips.append(chip(requiredAudioLabel(policy)));
|
chips.append(chip(requiredAudioLabel(policy)));
|
||||||
const bandCount = Object.keys(policy.size_bands).length;
|
const bandCount = Object.keys(policy.size_bands).length;
|
||||||
chips.append(chip(`${bandCount} band${bandCount === 1 ? "" : "s"}`));
|
chips.append(chip(`${bandCount} band${bandCount === 1 ? "" : "s"}`));
|
||||||
row.append(rowTitle(policy.name), chips);
|
|
||||||
item.append(row);
|
|
||||||
|
|
||||||
const del = armedDelete("delete", () => {
|
const del = armedDeleteIcon(`delete policy ${policy.name}`, () => {
|
||||||
void deleteRow("policies", policy.id).then((detail) => {
|
void deleteRow("policies", policy.id).then((detail) => {
|
||||||
if (detail !== null) {
|
if (detail !== null) {
|
||||||
note.hidden = false;
|
note.hidden = false;
|
||||||
@@ -454,9 +422,7 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const edit = el("button", "control control-quiet readout", "edit");
|
const edit = editIcon(`edit policy ${policy.name}`);
|
||||||
edit.type = "button";
|
|
||||||
edit.setAttribute("aria-expanded", "false");
|
|
||||||
edit.addEventListener("click", () => {
|
edit.addEventListener("click", () => {
|
||||||
const existing = item.querySelector(".edit-panel");
|
const existing = item.querySelector(".edit-panel");
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -468,7 +434,7 @@ export function settingsMain(views: { hide: () => void }[], goHome: () => void):
|
|||||||
edit.setAttribute("aria-expanded", "true");
|
edit.setAttribute("aria-expanded", "true");
|
||||||
});
|
});
|
||||||
|
|
||||||
const note = rowActions(item, del, edit);
|
const note = rowLine(item, policy.name, chips, del, edit);
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-2
@@ -1957,11 +1957,32 @@ body {
|
|||||||
|
|
||||||
/* ---- settings (issue #116) -------------------------------------------- */
|
/* ---- settings (issue #116) -------------------------------------------- */
|
||||||
|
|
||||||
/* rows follow the queues pattern: line, then an action pair */
|
/* one machined line per row (#231): identity, chips at the right edge,
|
||||||
|
then the icon action pair — the episode-row idiom, not a two-line group */
|
||||||
.settings-row-actions {
|
.settings-row-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
margin: var(--space-1) 0 var(--space-2);
|
margin-left: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row .row-title {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* feedback and the edit panel drop below the line, full width */
|
||||||
|
.settings-row .add-note,
|
||||||
|
.settings-row .edit-panel {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* narrow, the row wraps like an episode row: identity first, chips
|
||||||
|
full-width (the shared .row-chips rule), the icon pair keeps the right
|
||||||
|
edge. Lives here, after the base rule, so the margin actually flips. */
|
||||||
|
@media (max-width: 46rem) {
|
||||||
|
.settings-row-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-grid {
|
.form-grid {
|
||||||
|
|||||||
Reference in New Issue
Block a user