From a72605de8fa1ce36a09051564c91af9099407276 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 22 Aug 2026 14:55:28 -0400 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20the=20last=20pay-down,=20?= =?UTF-8?q?part=201=20=E2=80=94=20three=20dead=20views=20deleted,=20editor?= =?UTF-8?q?=20rules=20shared,=20.page-container=20+=20.fs-input=20canon,?= =?UTF-8?q?=20rules-shared.css;=20a=20one-declaration=20CSS=20body=20is=20?= =?UTF-8?q?not=20a=20shape=20(#2903,=20milestone=20299=20step=205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The derive queue said the biggest duplicate families were whole views: TaskViewerView, UserManagementView and LogsView were imported nowhere — left behind when tasks moved to the editor and users/logs became SettingsView tabs. Deleted (rule 22). Note/TaskEditorView carried six identical scoped rules -> editor-shared.css (the .tag-suggest-row gap the scoped copies actually rendered wins). Three views wrapped the page under three names -> .page-container in components.css. Three scoped input recipes -> the design system fs-input recipe (snippet #2336), verbatim, in components.css; width/ box-sizing stay with the caller. The three rules panes share .pane and the pane heading via rules-shared.css (the auth-shared pattern, #2852). Ledger: a single-declaration CSS rule keeps its selector in its fingerprint, so `color: var(--fs-text-tertiary)` under five different names is no longer a five-file "identical body" family — the first pay-down found that most of the 148 dup families were exactly this, and nobody would consolidate them. Co-Authored-By: Claude Fable 5 --- frontend/src/assets/components.css | 33 + frontend/src/assets/editor-shared.css | 35 +- frontend/src/assets/rules-shared.css | 14 + frontend/src/components/MilestoneSelector.vue | 19 +- frontend/src/components/SystemsSection.vue | 23 +- .../src/components/rules/RuleListPane.vue | 3 +- .../components/rules/RulebookDetailPane.vue | 3 +- .../src/components/rules/RulebookListPane.vue | 3 +- frontend/src/views/LogsView.vue | 485 ----------- frontend/src/views/NoteEditorView.vue | 43 - frontend/src/views/ProjectListView.vue | 9 +- frontend/src/views/ProjectView.vue | 34 +- frontend/src/views/SettingsView.vue | 2 +- frontend/src/views/SnippetListView.vue | 9 +- frontend/src/views/TaskEditorView.vue | 41 - frontend/src/views/TaskViewerView.vue | 767 ------------------ frontend/src/views/UserManagementView.vue | 441 ---------- src/scribe/services/coverage.py | 15 + tests/test_pattern_coverage.py | 12 +- 19 files changed, 130 insertions(+), 1861 deletions(-) create mode 100644 frontend/src/assets/rules-shared.css delete mode 100644 frontend/src/views/LogsView.vue delete mode 100644 frontend/src/views/TaskViewerView.vue delete mode 100644 frontend/src/views/UserManagementView.vue diff --git a/frontend/src/assets/components.css b/frontend/src/assets/components.css index 1b412ef..fa8b549 100644 --- a/frontend/src/assets/components.css +++ b/frontend/src/assets/components.css @@ -297,3 +297,36 @@ background: var(--fs-action-destructive-hover); border-color: var(--fs-action-destructive-hover); } + +/* ── Page container ───────────────────────────────────────────────────────── + The one wrapper a top-level view sits in: page width from the layout + tokens, centred, clipped horizontally so a wide child (a kanban, a table) + scrolls inside itself instead of the page. ProjectListView, ProjectView and + SnippetListView each carried this rule under their own name until #2903 + (milestone 299). */ +.page-container { + max-width: var(--fs-layout-page-max); + margin: 2rem auto; + padding: 0 var(--fs-layout-page-pad); + overflow-x: clip; +} + +/* ── Form input (fs-surfaces, snippet #2336) ──────────────────────────────── + Inputs sit DARKER than the page they're on — an inset well rather than a + raised panel; that inversion is what makes a field read as writable. The + design system's recipe, verbatim; width/box-sizing stay the caller's + (an inline select and a full-width textarea differ there). Three scoped + copies of an older input recipe were folded into this in #2903. */ +.fs-input { + background: var(--fs-surface-page); + border: var(--fs-border); + border-radius: var(--fs-radius-md); + padding: var(--fs-space-2) var(--fs-space-3); /* 8px 12px */ + color: var(--fs-text-primary); + font-family: var(--fs-font-body); + font-size: var(--fs-size-body); + transition: box-shadow var(--fs-dur-fast) var(--fs-ease); +} +.fs-input::placeholder { color: var(--fs-text-tertiary); } +.fs-input:focus { outline: none; box-shadow: var(--fs-focus-ring); } +.fs-input:disabled { opacity: var(--fs-disabled-opacity); cursor: not-allowed; } diff --git a/frontend/src/assets/editor-shared.css b/frontend/src/assets/editor-shared.css index df857d9..50cd8dc 100644 --- a/frontend/src/assets/editor-shared.css +++ b/frontend/src/assets/editor-shared.css @@ -78,7 +78,7 @@ display: flex; flex-wrap: wrap; align-items: center; - gap: 0.4rem; + gap: 0.3rem; } .tag-pill { display: inline-flex; @@ -508,3 +508,36 @@ opacity: var(--fs-disabled-opacity); cursor: not-allowed; } + +/* Shared by NoteEditorView and TaskEditorView — both carried identical scoped + copies of these until #2903 (milestone 299); one source here. */ +.body-tabs-row { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--fs-border-color); +} +.body-editor-wrap { + min-height: 200px; +} +.stream-preview { + border: 1px solid var(--fs-border-color); + border-radius: var(--fs-radius-sm); + padding: 0.75rem; + background: var(--fs-surface-raised); + min-height: 200px; +} +.main-diff { + flex: 1; + min-height: 0; +} +.assist-section-title { + font-size: 0.78rem; + font-weight: 500; + color: var(--fs-text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} diff --git a/frontend/src/assets/rules-shared.css b/frontend/src/assets/rules-shared.css new file mode 100644 index 0000000..963a32a --- /dev/null +++ b/frontend/src/assets/rules-shared.css @@ -0,0 +1,14 @@ +/* Shared by the three rules panes (RulebookListPane, RuleListPane, + RulebookDetailPane): the pane surface and its heading. Load with + diff --git a/frontend/src/components/SystemsSection.vue b/frontend/src/components/SystemsSection.vue index f1ef37b..afb4897 100644 --- a/frontend/src/components/SystemsSection.vue +++ b/frontend/src/components/SystemsSection.vue @@ -176,7 +176,7 @@ async function confirmDelete() {
+
- +
- @@ -891,7 +891,7 @@ async function confirmDelete() {
- @@ -1202,13 +1202,6 @@ async function confirmDelete() { diff --git a/frontend/src/views/UserManagementView.vue b/frontend/src/views/UserManagementView.vue deleted file mode 100644 index d4fce94..0000000 --- a/frontend/src/views/UserManagementView.vue +++ /dev/null @@ -1,441 +0,0 @@ - - - - - diff --git a/src/scribe/services/coverage.py b/src/scribe/services/coverage.py index 6dfd381..eabda24 100644 --- a/src/scribe/services/coverage.py +++ b/src/scribe/services/coverage.py @@ -156,6 +156,12 @@ def _block_sha(lines: list[str]) -> str: return hashlib.sha1("\n".join(kept).encode("utf-8")).hexdigest()[:16] +def _declaration_count(lines: list[str]) -> int: + """How many `prop: value` declarations a CSS block body carries.""" + body = " ".join(lines) + return sum(1 for part in body.replace("}", "").split(";") if ":" in part) + + def extract_definitions(text: str) -> list[Definition]: """Every definition this text makes, with signature + fingerprint. @@ -202,6 +208,15 @@ def extract_definitions(text: str) -> list[Definition]: hashed = head + block[1:] if not any(x.strip() for x in hashed): hashed = block + # A SINGLE declaration is not a shape (#2903): `color: var(--fs- + # text-tertiary)` under .text-muted, .task-mark and .pin-badge-auto + # is three meanings sharing one line, not three copies of one + # rule — the first pay-down found 5-file "families" of exactly + # this and nobody would consolidate them. Keep the selector in the + # hash for one-liners, so they group only with same-name copies; + # two declarations and up stay selector-agnostic. + elif _declaration_count(hashed) < 2: + hashed = block else: hashed = block out.append(Definition( diff --git a/tests/test_pattern_coverage.py b/tests/test_pattern_coverage.py index ab70205..c2a6ae4 100644 --- a/tests/test_pattern_coverage.py +++ b/tests/test_pattern_coverage.py @@ -484,12 +484,18 @@ def test_extract_definitions_fingerprints_each_block(): d = {x.name: x for x in extract_definitions(css)} assert d["closed-msg"].body_sha == d["error-block"].body_sha != d["other"].body_sha # One-line rules hash their own declarations — never the empty string - # (first deploy grouped 68 unrelated one-liners as one copy). - one = ".a { color: red; }\n\n.b { color: red; }\n\n.c { color: blue; }\n\n.d {\n color: red;\n}\n" + # (first deploy grouped 68 unrelated one-liners as one copy) — and a + # SINGLE declaration is not a shape (#2903): it keeps its selector in the + # hash, so `.a { color: red }` groups only with another `.a`, never with + # `.b { color: red }`. Two declarations and up stay selector-agnostic. + one = ".a { color: red; }\n\n.b { color: red; }\n\n.c { color: blue; }\n\n.a {\n color: red;\n}\n" e = {x.name: x for x in extract_definitions(one)} import hashlib - assert e["a"].body_sha == e["b"].body_sha != e["c"].body_sha + assert e["a"].body_sha != e["b"].body_sha != e["c"].body_sha assert e["a"].body_sha != hashlib.sha1(b"").hexdigest()[:16] + two = ".a { color: red; margin: 0; }\n.b {\n color: red;\n margin: 0;\n}\n" + f = {x.name: x for x in extract_definitions(two)} + assert f["a"].body_sha == f["b"].body_sha def test_coverage_line_names_the_proposers_standing():