Flock as list tiles with latest updates
- Each crow is a tile: live avatar, name, latest update, time; the most recently updated tile moves to the top - Red "N waiting" count per tile tracks open questions separately from the latest update, so a newer update never hides one - Phones: no crow strip; two tabs (# todo, Flock) with a count of crows waiting on you Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
This commit is contained in:
3 files changed
+146
-40
No files matched your search
+6
-1
@@ -9,7 +9,12 @@
|
|||||||
<link href="https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,500;0,700;1,500&family=IM+Fell+English+SC&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,500;0,700;1,500&family=IM+Fell+English+SC&display=swap" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app" id="app" data-view="room">
|
||||||
|
<nav class="tabs" aria-label="Views">
|
||||||
|
<span class="brand">So Many Crows</span>
|
||||||
|
<button class="tab" id="tab-room" type="button" aria-pressed="true"># todo</button>
|
||||||
|
<button class="tab" id="tab-flock" type="button" aria-pressed="false">Flock</button>
|
||||||
|
</nav>
|
||||||
<aside class="side" aria-label="The flock">
|
<aside class="side" aria-label="The flock">
|
||||||
<div class="brand">So Many Crows</div>
|
<div class="brand">So Many Crows</div>
|
||||||
<h2>The flock</h2>
|
<h2>The flock</h2>
|
||||||
|
|||||||
+92
-11
@@ -29,24 +29,71 @@ function el<K extends keyof HTMLElementTagNameMap>(tag: K, cls?: string, text?:
|
|||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- the roster: live, animated crows ----------
|
// ---------- the roster: one list tile per crow, latest update first ----------
|
||||||
const crows = new Map<string, CrowView>();
|
type Tile = { view: CrowView; el: HTMLDivElement; update: HTMLParagraphElement; time: HTMLTimeElement; count: HTMLSpanElement; pending: Set<string> };
|
||||||
|
const tiles = new Map<string, Tile>();
|
||||||
let trust: TrustLevels = new Map();
|
let trust: TrustLevels = new Map();
|
||||||
|
let seq = 0;
|
||||||
|
|
||||||
function roleOf(name: string): Role {
|
function roleOf(name: string): Role {
|
||||||
return name === "Sentry" ? "sentry" : name === "Elder" ? "elder" : "crow";
|
return name === "Sentry" ? "sentry" : name === "Elder" ? "elder" : "crow";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tile(name: string): Tile {
|
||||||
|
let t = tiles.get(name);
|
||||||
|
if (!t) {
|
||||||
|
const view = svgCrow(name, roleOf(name)); // the live, animated crow is the tile's avatar
|
||||||
|
const box = el("div", `tile role-${roleOf(name)}`);
|
||||||
|
const text = el("div", "tile-text");
|
||||||
|
const update = el("p", "update", name === "Sentry" ? "On watch" : "Perched");
|
||||||
|
text.append(el("p", "tile-name", name), update);
|
||||||
|
const meta = el("div", "tile-meta");
|
||||||
|
const time = el("time", "tile-time");
|
||||||
|
const count = el("span", "tile-count");
|
||||||
|
meta.append(time, count);
|
||||||
|
box.append(view.el, text, meta);
|
||||||
|
roster.append(box);
|
||||||
|
t = { view, el: box, update, time, count, pending: new Set() };
|
||||||
|
tiles.set(name, t);
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
function crow(name: string): CrowView {
|
function crow(name: string): CrowView {
|
||||||
let c = crows.get(name);
|
return tile(name).view;
|
||||||
if (!c) {
|
|
||||||
c = svgCrow(name, roleOf(name));
|
|
||||||
crows.set(name, c);
|
|
||||||
roster.append(c.el);
|
|
||||||
}
|
}
|
||||||
return c;
|
|
||||||
|
// Each crow's latest update; the most recently updated tile moves to the top.
|
||||||
|
type Kind = "" | "waiting" | "good" | "bad";
|
||||||
|
function update(name: string, text: string, ts?: string, kind: Kind = "") {
|
||||||
|
const t = tile(name);
|
||||||
|
t.update.textContent = text;
|
||||||
|
t.update.className = `update ${kind}`.trim();
|
||||||
|
t.time.textContent = fmtTime(ts);
|
||||||
|
t.el.style.order = String(-(++seq));
|
||||||
|
renderWaitingCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What each crow still owes a verdict on. Independent of its latest update,
|
||||||
|
// so a newer update never hides an open question.
|
||||||
|
function pending(name: string, key: string, open: boolean) {
|
||||||
|
const t = tile(name);
|
||||||
|
if (open) t.pending.add(key); else t.pending.delete(key);
|
||||||
|
const n = t.pending.size;
|
||||||
|
t.count.textContent = n ? `${n} waiting` : "";
|
||||||
|
t.el.classList.toggle("has-pending", n > 0);
|
||||||
|
renderWaitingCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phone tab badge: how many crows are waiting on you.
|
||||||
|
const flockTab = document.getElementById("tab-flock");
|
||||||
|
function renderWaitingCount() {
|
||||||
|
const n = [...tiles.values()].filter((t) => t.pending.size > 0).length;
|
||||||
|
if (flockTab) flockTab.dataset.count = n ? String(n) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = (s: string) => `“${s}”`;
|
||||||
|
|
||||||
// Badges show trust for the task type the crow is working on now.
|
// Badges show trust for the task type the crow is working on now.
|
||||||
function showTrust(name: string, taskType: string) {
|
function showTrust(name: string, taskType: string) {
|
||||||
const key = name === "Sentry" ? `Sentry|triage/${taskType}` : `${name}|${taskType}`;
|
const key = name === "Sentry" ? `Sentry|triage/${taskType}` : `${name}|${taskType}`;
|
||||||
@@ -163,6 +210,8 @@ const handlers: Record<string, (d: Payload) => void> = {
|
|||||||
}
|
}
|
||||||
post("you", [ol], d.ts);
|
post("you", [ol], d.ts);
|
||||||
crow("Sentry").setPose("perched");
|
crow("Sentry").setPose("perched");
|
||||||
|
const n = (d.items ?? []).length;
|
||||||
|
update("Sentry", `Got a list of ${n} item${n === 1 ? "" : "s"}`, d.ts);
|
||||||
},
|
},
|
||||||
|
|
||||||
"item.triaged": (d) => {
|
"item.triaged": (d) => {
|
||||||
@@ -181,6 +230,10 @@ const handlers: Record<string, (d: Payload) => void> = {
|
|||||||
if (d.needs_approval) {
|
if (d.needs_approval) {
|
||||||
crow("Sentry").setPose("waiting");
|
crow("Sentry").setPose("waiting");
|
||||||
ask(it.triage, d.item_id, "triage");
|
ask(it.triage, d.item_id, "triage");
|
||||||
|
update("Sentry", `Waiting on you: plan for ${q(it.text)}`, d.ts, "waiting");
|
||||||
|
pending("Sentry", `${d.item_id}/triage`, true);
|
||||||
|
} else {
|
||||||
|
update("Sentry", `Sorted ${q(it.text)}`, d.ts);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -188,12 +241,19 @@ const handlers: Record<string, (d: Payload) => void> = {
|
|||||||
const it = items.get(d.item_id);
|
const it = items.get(d.item_id);
|
||||||
if (it && d.crow !== "Sentry") showTrust(d.crow, it.taskType);
|
if (it && d.crow !== "Sentry") showTrust(d.crow, it.taskType);
|
||||||
crow(d.crow).setPose("flying");
|
crow(d.crow).setPose("flying");
|
||||||
|
if (it) {
|
||||||
|
const doing = d.crow === "Sentry" ? "Sorting" : d.crow === "Elder" ? "Merging reports on" : "Flying";
|
||||||
|
update(d.crow, `${doing}${doing === "Flying" ? ":" : ""} ${q(it.text)}`, d.ts);
|
||||||
|
}
|
||||||
flying.add(d.crow);
|
flying.add(d.crow);
|
||||||
renderTyping();
|
renderTyping();
|
||||||
},
|
},
|
||||||
|
|
||||||
"crow.landed": (d) => {
|
"crow.landed": (d) => {
|
||||||
crow(d.crow).setPose(d.ok ? "perched" : "grounded");
|
crow(d.crow).setPose(d.ok ? "perched" : "grounded");
|
||||||
|
const it = items.get(d.item_id);
|
||||||
|
if (it && !d.ok) update(d.crow, `Grounded on ${q(it.text)}`, d.ts, "bad");
|
||||||
|
else if (it && d.crow !== "Sentry" && d.crow !== "Elder") update(d.crow, `Reported back on ${q(it.text)}`, d.ts);
|
||||||
flying.delete(d.crow);
|
flying.delete(d.crow);
|
||||||
renderTyping();
|
renderTyping();
|
||||||
},
|
},
|
||||||
@@ -208,7 +268,11 @@ const handlers: Record<string, (d: Payload) => void> = {
|
|||||||
if (author === "Elder" && helpers.length) parts.push(el("p", "with", `from ${helpers.join(", ")}`));
|
if (author === "Elder" && helpers.length) parts.push(el("p", "with", `from ${helpers.join(", ")}`));
|
||||||
it.answer = post(author, parts, d.ts);
|
it.answer = post(author, parts, d.ts);
|
||||||
if (d.needs_approval) {
|
if (d.needs_approval) {
|
||||||
for (const n of it.crows) crow(n).setPose("waiting");
|
for (const n of it.crows) {
|
||||||
|
crow(n).setPose("waiting");
|
||||||
|
update(n, `Waiting on you: ${q(it.text)}`, d.ts, "waiting");
|
||||||
|
pending(n, `${d.item_id}/output`, true);
|
||||||
|
}
|
||||||
ask(it.answer, d.item_id, "output");
|
ask(it.answer, d.item_id, "output");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -216,6 +280,7 @@ const handlers: Record<string, (d: Payload) => void> = {
|
|||||||
verdict: (d) => {
|
verdict: (d) => {
|
||||||
const it = items.get(d.item_id);
|
const it = items.get(d.item_id);
|
||||||
if (!it) return;
|
if (!it) return;
|
||||||
|
for (const n of d.target === "triage" ? ["Sentry"] : it.crows) pending(n, `${d.item_id}/${d.target}`, false);
|
||||||
const m = d.target === "triage" ? it.triage : it.answer;
|
const m = d.target === "triage" ? it.triage : it.answer;
|
||||||
const good = d.verdict === "approved" || d.verdict === "auto";
|
const good = d.verdict === "approved" || d.verdict === "auto";
|
||||||
if (m) {
|
if (m) {
|
||||||
@@ -229,9 +294,12 @@ const handlers: Record<string, (d: Payload) => void> = {
|
|||||||
react(m, text, kind);
|
react(m, text, kind);
|
||||||
}
|
}
|
||||||
if (d.target === "triage" && d.verdict === "auto") return; // a trusted Sentry just keeps going
|
if (d.target === "triage" && d.verdict === "auto") return; // a trusted Sentry just keeps going
|
||||||
|
const word: Record<string, string> = { approved: "Approved", rejected: "Rejected", auto: "Accepted (trusted)", timeout: "Timed out" };
|
||||||
for (const n of d.target === "triage" ? ["Sentry"] : it.crows) {
|
for (const n of d.target === "triage" ? ["Sentry"] : it.crows) {
|
||||||
crow(n).fire(good ? "cheer" : "sulk");
|
crow(n).fire(good ? "cheer" : "sulk");
|
||||||
crow(n).setPose("perched");
|
crow(n).setPose(tile(n).pending.size ? "waiting" : "perched");
|
||||||
|
const what = d.target === "triage" ? `plan for ${q(it.text)}` : q(it.text);
|
||||||
|
update(n, `${word[d.verdict] ?? d.verdict}: ${what}`, d.ts, good ? "good" : "bad");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -246,6 +314,7 @@ const handlers: Record<string, (d: Payload) => void> = {
|
|||||||
const c = crow(d.holder);
|
const c = crow(d.holder);
|
||||||
c.setTrust(d.to);
|
c.setTrust(d.to);
|
||||||
c.fire(up ? "levelUp" : "levelDown");
|
c.fire(up ? "levelUp" : "levelDown");
|
||||||
|
update(d.holder, `${up ? "Leveled up to" : "Dropped to"} ${LEVELS[d.to]} on ${what}`, d.ts, up ? "good" : "bad");
|
||||||
note(`${d.holder} ${up ? "leveled up to" : "dropped to"} “${LEVELS[d.to]}” on ${what}.`, up ? "sparkle" : "");
|
note(`${d.holder} ${up ? "leveled up to" : "dropped to"} “${LEVELS[d.to]}” on ${what}.`, up ? "sparkle" : "");
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -253,12 +322,13 @@ const handlers: Record<string, (d: Payload) => void> = {
|
|||||||
const it = items.get(d.item_id);
|
const it = items.get(d.item_id);
|
||||||
note(`Skipped “${it?.text ?? d.item_id}”: ${d.reason}.`);
|
note(`Skipped “${it?.text ?? d.item_id}”: ${d.reason}.`);
|
||||||
for (const n of it?.crows ?? []) crow(n).setPose("perched");
|
for (const n of it?.crows ?? []) crow(n).setPose("perched");
|
||||||
|
update("Sentry", `Skipped ${q(it?.text ?? d.item_id)}`, d.ts, "bad");
|
||||||
},
|
},
|
||||||
|
|
||||||
"requester.pranked": (d) => prank(d.message, d.cooldown_s),
|
"requester.pranked": (d) => prank(d.message, d.cooldown_s),
|
||||||
|
|
||||||
"list.finished": (d) => {
|
"list.finished": (d) => {
|
||||||
for (const c of crows.values()) { c.setPose("perched"); c.fire("cheer"); }
|
for (const t of tiles.values()) { t.view.setPose("perched"); t.view.fire("cheer"); }
|
||||||
flying.clear();
|
flying.clear();
|
||||||
renderTyping();
|
renderTyping();
|
||||||
note(`List done. The flock used ${Number(d.tokens_used).toLocaleString()} of ${Number(d.token_budget).toLocaleString()} tokens.`);
|
note(`List done. The flock used ${Number(d.tokens_used).toLocaleString()} of ${Number(d.token_budget).toLocaleString()} tokens.`);
|
||||||
@@ -274,6 +344,7 @@ const sendLabel = sendBtn.textContent ?? "Send";
|
|||||||
function prank(message: string, cooldownS: number) {
|
function prank(message: string, cooldownS: number) {
|
||||||
crow("Sentry").fire("prank");
|
crow("Sentry").fire("prank");
|
||||||
post("Sentry", [el("p", "", message)]);
|
post("Sentry", [el("p", "", message)]);
|
||||||
|
update("Sentry", "Left a gift on someone's windshield", undefined, "bad");
|
||||||
let left = Math.max(0, Math.round(cooldownS));
|
let left = Math.max(0, Math.round(cooldownS));
|
||||||
clearInterval(cooldownTimer);
|
clearInterval(cooldownTimer);
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
@@ -321,6 +392,16 @@ form.addEventListener("submit", async (e) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Phone tabs: the channel or the flock.
|
||||||
|
const app = document.getElementById("app")!;
|
||||||
|
for (const [id, view] of [["tab-room", "room"], ["tab-flock", "flock"]] as const) {
|
||||||
|
document.getElementById(id)!.addEventListener("click", () => {
|
||||||
|
app.dataset.view = view;
|
||||||
|
for (const t of document.querySelectorAll<HTMLButtonElement>(".tab")) t.setAttribute("aria-pressed", String(t.id === id));
|
||||||
|
if (view === "room") feed.scrollTop = feed.scrollHeight;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// The Sentry is always on watch, and says hello.
|
// The Sentry is always on watch, and says hello.
|
||||||
crow("Sentry");
|
crow("Sentry");
|
||||||
post("Sentry", [el("p", "", "Caw. Send me a todo list, one item per line. I'll sort it, the flock will fly it, and you judge the results.")]);
|
post("Sentry", [el("p", "", "Caw. Send me a todo list, one item per line. I'll sort it, the flock will fly it, and you judge the results.")]);
|
||||||
+48
-28
@@ -54,7 +54,7 @@ body { margin: 0; background: var(--paper); color: var(--ink); font: 500 17px/1.
|
|||||||
.btn-no::before { background: var(--paper); }
|
.btn-no::before { background: var(--paper); }
|
||||||
|
|
||||||
/* ---------- app frame: sidebar + room ---------- */
|
/* ---------- app frame: sidebar + room ---------- */
|
||||||
.app { display: grid; grid-template-columns: 248px minmax(0, 1fr); height: 100vh; height: 100dvh; }
|
.app { display: grid; grid-template-columns: 300px minmax(0, 1fr); height: 100vh; height: 100dvh; }
|
||||||
|
|
||||||
.side {
|
.side {
|
||||||
position: relative; z-index: 1; display: flex; flex-direction: column; gap: 12px;
|
position: relative; z-index: 1; display: flex; flex-direction: column; gap: 12px;
|
||||||
@@ -73,21 +73,31 @@ body { margin: 0; background: var(--paper); color: var(--ink); font: 500 17px/1.
|
|||||||
}
|
}
|
||||||
.who input:focus { outline: none; border-bottom-width: 3px; }
|
.who input:focus { outline: none; border-bottom-width: 3px; }
|
||||||
|
|
||||||
/* ---------- roster: live crows ---------- */
|
/* ---------- roster: one list tile per crow, newest update on top ---------- */
|
||||||
.roster { display: flex; flex-direction: column; gap: 2px; }
|
.roster { display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
.tile {
|
||||||
|
display: grid; grid-template-columns: 40px minmax(0, 1fr) auto; align-items: center; gap: 10px;
|
||||||
|
padding: 7px 8px; border-radius: var(--corner);
|
||||||
|
}
|
||||||
|
.tile.has-pending { background: rgb(155 47 34 / 0.06); }
|
||||||
|
.tile-text { min-width: 0; }
|
||||||
|
.tile-name { margin: 0; font: 400 16px/1.15 var(--display); }
|
||||||
|
.update {
|
||||||
|
margin: 1px 0 0; font: italic 500 14px/1.3 var(--body); color: var(--muted);
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.update.waiting { color: var(--red); }
|
||||||
|
.update.good { color: var(--ink); }
|
||||||
|
.tile-meta { align-self: stretch; display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; }
|
||||||
|
.tile-time { margin-top: 2px; font-size: 12px; color: var(--muted); white-space: nowrap; }
|
||||||
|
.tile-count:not(:empty) {
|
||||||
|
padding: 0 7px; border-radius: 999px; background: var(--red); color: var(--paper);
|
||||||
|
font: 700 12px/1.4 var(--body); white-space: nowrap;
|
||||||
|
}
|
||||||
.crow { margin: 0; position: relative; }
|
.crow { margin: 0; position: relative; }
|
||||||
.crow.role-sentry { order: -1; }
|
.crow figcaption { display: none; } /* the tile shows the name */
|
||||||
.crow.role-elder { order: 99; }
|
|
||||||
.roster .crow { display: grid; grid-template-columns: 40px 1fr 16px; align-items: center; gap: 10px; padding: 5px 6px; }
|
|
||||||
.roster .crow figcaption { order: 2; }
|
|
||||||
.roster .crow .badge { order: 3; }
|
|
||||||
.crow-body { width: 40px; height: 37px; }
|
.crow-body { width: 40px; height: 37px; }
|
||||||
.crow-body svg { display: block; width: 100%; height: 100%; overflow: visible; }
|
.crow-body svg { display: block; width: 100%; height: 100%; overflow: visible; }
|
||||||
.crow figcaption { font: 400 16px/1.15 var(--display); min-width: 0; }
|
|
||||||
.crow figcaption::after { display: block; font: italic 500 13px/1.3 var(--body); color: var(--muted); content: "perched"; }
|
|
||||||
.pose-flying figcaption::after { content: "flying"; }
|
|
||||||
.pose-waiting figcaption::after { content: "waiting on you"; color: var(--red); }
|
|
||||||
.pose-grounded figcaption::after { content: "grounded"; }
|
|
||||||
|
|
||||||
.crow-art .disc, .crow .disc { fill: #b7e4c7; }
|
.crow-art .disc, .crow .disc { fill: #b7e4c7; }
|
||||||
.role-sentry .disc { fill: #ffe08a; }
|
.role-sentry .disc { fill: #ffe08a; }
|
||||||
@@ -96,7 +106,7 @@ body { margin: 0; background: var(--paper); color: var(--ink); font: 500 17px/1.
|
|||||||
.crow .jaw { transform-origin: 211px 182px; transition: transform 150ms; }
|
.crow .jaw { transform-origin: 211px 182px; transition: transform 150ms; }
|
||||||
|
|
||||||
.crow .badge {
|
.crow .badge {
|
||||||
width: 16px; height: 16px; fill: var(--card); stroke: var(--ink); stroke-width: 1.8; stroke-linejoin: round;
|
position: absolute; top: -4px; right: -6px; width: 14px; height: 14px; fill: var(--card); stroke: var(--ink); stroke-width: 1.8; stroke-linejoin: round;
|
||||||
opacity: 0; transition: opacity 150ms;
|
opacity: 0; transition: opacity 150ms;
|
||||||
}
|
}
|
||||||
.crow[data-trust="1"] .badge { opacity: 1; }
|
.crow[data-trust="1"] .badge { opacity: 1; }
|
||||||
@@ -124,6 +134,8 @@ body { margin: 0; background: var(--paper); color: var(--ink); font: 500 17px/1.
|
|||||||
.crow *, .crow::before, .crow::after { animation: none !important; transition: none !important; }
|
.crow *, .crow::before, .crow::after { animation: none !important; transition: none !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabs { display: none; }
|
||||||
|
|
||||||
/* ---------- room: header, feed, composer ---------- */
|
/* ---------- room: header, feed, composer ---------- */
|
||||||
.room { display: grid; grid-template-rows: auto 1fr auto auto; min-width: 0; min-height: 0; }
|
.room { display: grid; grid-template-rows: auto 1fr auto auto; min-width: 0; min-height: 0; }
|
||||||
.room-head { position: relative; padding: 16px 24px 12px; }
|
.room-head { position: relative; padding: 16px 24px 12px; }
|
||||||
@@ -188,23 +200,31 @@ body { margin: 0; background: var(--paper); color: var(--ink); font: 500 17px/1.
|
|||||||
.composer .field:focus-within::after { -webkit-mask-image: none; mask-image: none; }
|
.composer .field:focus-within::after { -webkit-mask-image: none; mask-image: none; }
|
||||||
.hint { grid-column: 1 / -1; margin: 0; color: var(--muted); font-style: italic; font-size: 13px; }
|
.hint { grid-column: 1 / -1; margin: 0; color: var(--muted); font-style: italic; font-size: 13px; }
|
||||||
|
|
||||||
/* ---------- phones: sidebar becomes a top bar with a scrolling flock ---------- */
|
/* ---------- phones: two tabs, the channel or the flock ---------- */
|
||||||
@media (max-width: 759px) {
|
@media (max-width: 759px) {
|
||||||
.app { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
|
.app { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
|
||||||
.side { min-width: 0; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 6px 12px; padding: 10px 12px 6px; overflow: visible; }
|
.tabs {
|
||||||
.side::after { top: auto; bottom: -8px; left: 0; right: 0; width: auto; height: 8px;
|
position: relative; z-index: 2; display: flex; align-items: center; gap: 2px; padding: 10px 10px 8px 14px; background: var(--card);
|
||||||
-webkit-mask-image: linear-gradient(rgb(0 0 0 / 0.7), transparent); mask-image: linear-gradient(rgb(0 0 0 / 0.7), transparent); }
|
}
|
||||||
.side h2 { display: none; }
|
.tabs::after {
|
||||||
.brand { font-size: 20px; padding: 0; }
|
content: ""; position: absolute; left: 0; right: 0; bottom: -8px; height: 8px; background: var(--stipple); pointer-events: none;
|
||||||
.who { order: 2; margin: 0 0 0 auto; padding: 0; font-size: 14px; }
|
-webkit-mask-image: linear-gradient(rgb(0 0 0 / 0.7), transparent); mask-image: linear-gradient(rgb(0 0 0 / 0.7), transparent);
|
||||||
.who input { display: inline-block; width: 96px; margin: 0 0 0 4px; font-size: 15px; }
|
}
|
||||||
.roster { order: 3; width: 100%; min-width: 0; flex-direction: row; overflow-x: auto; gap: 2px; padding-top: 10px; }
|
.tabs .brand { margin-right: auto; font-size: 19px; padding: 0; }
|
||||||
.roster .crow { display: flex; flex-direction: column; align-items: center; flex: none; width: 54px; padding: 0 2px; }
|
.tab { padding: 6px 8px; font: 400 16px/1 var(--display); color: var(--muted); background: none; border: 0; cursor: pointer; }
|
||||||
.roster .crow-body { width: 38px; height: 35px; }
|
.tab[aria-pressed="true"] { color: var(--ink); text-decoration: underline 2px; text-underline-offset: 6px; }
|
||||||
.roster .crow figcaption { font-size: 12px; }
|
.tab:focus-visible { outline: none; text-decoration: underline 3px; text-underline-offset: 6px; }
|
||||||
.roster .crow figcaption::after { display: none; }
|
.tab[data-count]:not([data-count=""])::after {
|
||||||
.roster .crow .badge { position: absolute; top: -4px; right: 3px; width: 13px; height: 13px; }
|
content: attr(data-count); display: inline-block; margin-left: 5px; padding: 1px 7px; border-radius: 999px;
|
||||||
.roster .pose-waiting::after { content: "?"; position: absolute; top: -10px; left: 4px; font: 400 15px/1 var(--display); color: var(--red); }
|
background: var(--red); color: var(--paper); font: 700 12px/1.3 var(--body); vertical-align: 2px;
|
||||||
|
}
|
||||||
|
.side, .room { grid-row: 2; min-width: 0; }
|
||||||
|
.app[data-view="room"] .side { display: none; }
|
||||||
|
.app[data-view="flock"] .room { display: none; }
|
||||||
|
.side { padding: 14px 6px 16px; }
|
||||||
|
.side::after, .side .brand { display: none; }
|
||||||
|
.side h2 { margin: 4px 10px 6px; }
|
||||||
|
.who { padding: 0 10px; }
|
||||||
.room-head { padding: 12px 14px 10px; }
|
.room-head { padding: 12px 14px 10px; }
|
||||||
.room-head h1 { font-size: 19px; }
|
.room-head h1 { font-size: 19px; }
|
||||||
.room-sub { display: none; }
|
.room-sub { display: none; }
|
||||||
|
|||||||
Reference in new issue
Block a user