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():