CI & Build / Python lint (push) Failing after 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 23s
CI & Build / integration (push) Successful in 27s
CI & Build / Python tests (push) Canceled after 38s
CI & Build / Build & push image (push) Canceled after 0s
Operator reported four things looking wrong. Two were the same bug, and it is
not a design drift — it is deleted CSS.
Removing a rule from a scoped stylesheet leaves its modifiers behind. The
selector still exists, so nothing reads as unused, and the element renders with
no base styling at all:
.btn-workspace base gone, :hover survived — the Workspace link rendered
as raw browser blue, underlined
.milestone-header base gone, .clickable and :hover survived. Every child is
written for a flex ROW (.ms-name { flex: 1 }, the progress
track, .ms-pct), so without the parent they stacked and a
one-line milestone became five. That is the "projects
section uses space poorly" — a deletion, not a redesign.
.milestone-group no rule at all; the card around each milestone
.ds-header only its h1 descendant survived
vue-tsc cannot see any of it. A dead style typechecks perfectly.
scripts/check_dangling_styles.py finds the shape: an element whose every static
class has no base rule anywhere, while at least one carries modifier rules. It
reports 11 more. Reported and not gated, because a genuinely bare wrapper is
legitimate — the signal is the count growing. Runs in the lint lane, stdlib
only, and knows no class name or convention (rule #115).
Also from the same report:
- The header pill bar was `position: absolute; left: 50%`, so it did not
participate in layout: out of room, it OVERLAPPED the brand and the utility
cluster instead of pushing them. A sixth link reached that at ~1270px, an
ordinary window. Now `1fr auto 1fr` — a 1fr track has an auto minimum, so
neither side can be squeezed under its content and the two stay equal, which
is what keeps the bar centred in the viewport rather than in the leftover
space. Overflow becomes the header growing, not two things sharing pixels.
- The token preview put its checkerboard on the whole specimen stage, so every
swatch sat in a frame of checks and the pattern read as the loudest thing on
the page. The checks now sit UNDER the colour as a second background layer:
an opaque value hides them, a 15% tint shows exactly as much as it should.
Text-bearing specimens lose the box entirely, and name/value/purpose are one
line each with the full text on hover — they wrapped freely before, so a card
was two lines tall or five depending on how long its color-mix() happened to
be, and the grid had no rhythm.
- .btn-cta joins the shared button family: the gradient-and-glow brand moment
the system carries tokens for, which had been living in one view's scoped
block. That is what made it deletable. The header actions are now one size
and one family instead of four sizes and two.
- The shared button shape gained inline-flex + gap, so a button carrying an
icon centres it without each caller rebuilding the row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
224 lines
8.8 KiB
CSS
224 lines
8.8 KiB
CSS
/* Shared component styles — the house recipes, in one place.
|
||
* ===========================================================================
|
||
*
|
||
* WHY THIS FILE EXISTS
|
||
*
|
||
* `.btn-primary` was defined five times, in five scoped stylesheets, and all
|
||
* five had drifted: three paddings, three font sizes, three disabled opacities,
|
||
* and one view with no disabled style at all (#2273). Nothing detected that,
|
||
* because a scoped duplicate is invisible to every tool — it isn't a rule
|
||
* violation, isn't a broken reference, and isn't a recorded snippet.
|
||
*
|
||
* GEOMETRY LIVES HERE. Every value is a design-system token, so a palette or
|
||
* scale change moves the buttons rather than stranding a copy that no longer
|
||
* matches.
|
||
*
|
||
* A button is `variant + size`, composed in the template:
|
||
* btn-primary a page action
|
||
* btn-primary btn-compact a row action
|
||
* btn-ghost btn-inline an affordance inside a card
|
||
* btn-primary btn-block a form's single submitting action
|
||
* Semantic per-view names (.btn-save, .btn-delete-task, …) were the thing that
|
||
* drifted, because a name says what a button is FOR and nothing about what it
|
||
* should look like — so two buttons doing the same job in two views had no
|
||
* reason to match, and didn't.
|
||
*
|
||
* MIGRATION NOTE — this file is deliberately safe to land ahead of the removals.
|
||
* These are plain selectors (specificity 0,1,0); a Vue `<style scoped>` rule
|
||
* compiles to `.btn-primary[data-v-…]` (0,2,0) and therefore WINS. So a view
|
||
* still carrying its own copy is unaffected until that copy is deleted, and
|
||
* every intermediate state of the migration is coherent.
|
||
*/
|
||
|
||
/* --- the shared shape ---------------------------------------------------- */
|
||
|
||
.btn-primary,
|
||
.btn-secondary,
|
||
.btn-ghost,
|
||
.btn-danger,
|
||
.btn-danger-outline,
|
||
.btn-cta {
|
||
padding: var(--fs-space-2) var(--fs-space-4); /* 8px 16px */
|
||
border: none;
|
||
border-radius: var(--fs-radius-md); /* 8px — the system's button radius */
|
||
font-family: var(--fs-font-body);
|
||
font-size: var(--fs-size-label); /* 12px */
|
||
font-weight: var(--fs-weight-medium); /* 500 — the heaviest the system goes */
|
||
line-height: var(--fs-leading-body);
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
/* So a button carrying an icon centres it against the label without each
|
||
caller re-inventing the flex row — the shape they all reached for
|
||
separately, and the reason icon buttons sat a pixel or two off. */
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: var(--fs-space-2);
|
||
text-decoration: none;
|
||
transition: background var(--fs-dur-fast) var(--fs-ease),
|
||
border-color var(--fs-dur-fast) var(--fs-ease),
|
||
color var(--fs-dur-fast) var(--fs-ease);
|
||
}
|
||
|
||
/* One rule, so a disabled button can never look enabled in one view and
|
||
* disabled in another — which is exactly what ProjectListView shipped, having
|
||
* no disabled style at all. */
|
||
.btn-primary:disabled,
|
||
.btn-secondary:disabled,
|
||
.btn-ghost:disabled,
|
||
.btn-danger:disabled,
|
||
.btn-danger-outline:disabled,
|
||
.btn-cta:disabled {
|
||
opacity: var(--fs-disabled-opacity);
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.btn-primary:focus-visible,
|
||
.btn-secondary:focus-visible,
|
||
.btn-ghost:focus-visible,
|
||
.btn-danger:focus-visible,
|
||
.btn-danger-outline:focus-visible,
|
||
.btn-cta:focus-visible {
|
||
outline: none;
|
||
box-shadow: var(--fs-focus-ring);
|
||
}
|
||
|
||
/* --- variants ------------------------------------------------------------ */
|
||
|
||
/* The accent is deliberately ABSENT from every filled variant. Action colours
|
||
* are universal across the family so a Save button looks identical in every
|
||
* app — the accent is identity, not action. */
|
||
.btn-primary {
|
||
background: var(--color-action-primary);
|
||
color: var(--fs-text-on-action);
|
||
}
|
||
.btn-primary:not(:disabled):hover {
|
||
background: var(--color-action-primary-hover);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: var(--color-action-secondary);
|
||
color: var(--fs-text-on-action);
|
||
}
|
||
.btn-secondary:not(:disabled):hover {
|
||
background: var(--color-action-secondary-hover);
|
||
}
|
||
|
||
/* Ghost is an OUTLINE, which is why its border and the tertiary action colour
|
||
* are the same token rather than two values that happen to match. Hover moves
|
||
* the BORDER, not the text to the accent — SnippetDetailView tinted the label
|
||
* with the accent on hover, which the house style reserves for identity and
|
||
* active state, not for general chrome. */
|
||
.btn-ghost {
|
||
background: none;
|
||
border: var(--fs-border);
|
||
color: var(--color-text);
|
||
}
|
||
.btn-ghost:not(:disabled):hover {
|
||
border: var(--fs-border-hover);
|
||
background: var(--color-hover);
|
||
}
|
||
|
||
/* Destructive is NOT the error colour: an error is a failure that happened, a
|
||
* destructive action is one about to happen. Pair with an icon. */
|
||
.btn-danger {
|
||
background: var(--color-action-destructive);
|
||
color: var(--fs-text-on-action);
|
||
}
|
||
.btn-danger:not(:disabled):hover {
|
||
background: var(--color-action-destructive-hover);
|
||
}
|
||
|
||
/* A bare text button: no fill, no border. The most common shape in the dense
|
||
* surfaces — a dismiss, a cancel next to a confirm, a clear-search — where a
|
||
* border would draw a box around something that should read as an action on
|
||
* the text beside it. Distinct from ghost, which IS a box. */
|
||
.btn-text {
|
||
background: none;
|
||
border: none;
|
||
color: var(--color-text-muted);
|
||
padding: var(--fs-space-1) var(--fs-space-2);
|
||
font-family: var(--fs-font-body);
|
||
font-size: var(--fs-size-tiny);
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
transition: color var(--fs-dur-fast) var(--fs-ease);
|
||
}
|
||
.btn-text:not(:disabled):hover { color: var(--color-text); }
|
||
.btn-text:disabled { opacity: var(--fs-disabled-opacity); cursor: not-allowed; }
|
||
.btn-text:focus-visible { outline: none; box-shadow: var(--fs-focus-ring); }
|
||
|
||
/* Destructive, outlined — fills on hover. Already existed independently in
|
||
* three views before this sheet, which is what makes it a variant rather than
|
||
* a one-off: it is what a delete looks like when it must not shout. */
|
||
.btn-danger-outline {
|
||
background: none;
|
||
border: 1px solid var(--color-action-destructive);
|
||
color: var(--color-action-destructive);
|
||
}
|
||
.btn-danger-outline:not(:disabled):hover {
|
||
background: var(--color-action-destructive);
|
||
color: var(--fs-text-on-action);
|
||
}
|
||
|
||
/* The one place the accent is allowed on a button: a deliberate brand moment,
|
||
* never an ordinary action. The system carries `--fs-gradient-cta` and
|
||
* `--fs-glow-cta` for exactly this and nothing else was using them.
|
||
*
|
||
* It exists because ProjectView's Workspace link WAS this button, defined in a
|
||
* scoped block that the migration deleted — leaving a `:hover` rule with no
|
||
* base and a link that rendered as raw browser blue. A variant living in one
|
||
* view is a variant waiting to be deleted by someone tidying another; this is
|
||
* the shared home so the next sweep can't strand it. */
|
||
.btn-cta {
|
||
background: var(--fs-gradient-cta);
|
||
color: var(--fs-text-on-action);
|
||
box-shadow: var(--fs-glow-cta);
|
||
text-decoration: none;
|
||
}
|
||
.btn-cta:not(:disabled):hover {
|
||
box-shadow: var(--fs-glow-cta-hover);
|
||
}
|
||
|
||
/* --- size modifiers ------------------------------------------------------
|
||
*
|
||
* THREE sizes, because the app genuinely has three. Measured across the ~100
|
||
* bespoke button rules before this scale existed, vertical padding fell into
|
||
* clusters rather than a spread: ~27 at 0.4–0.45rem, ~28 at 0.25–0.3rem, ~23
|
||
* at 0.1–0.15rem. Those are three different components — a page action, a row
|
||
* action, and an affordance living inside a card — that happen to share a name
|
||
* prefix. Collapsing them to one size would visibly break the card layouts.
|
||
*
|
||
* A button carries its size modifier; the DEFAULT (no modifier) is the page
|
||
* action, which is the one the house style specifies.
|
||
*/
|
||
|
||
/* Row actions: a toolbar, a table row, a list item's controls. */
|
||
.btn-compact,
|
||
.btn-small, /* pre-existing spellings, kept so no template churns */
|
||
.btn-sm {
|
||
padding: var(--fs-space-1) var(--fs-space-3); /* 4px 12px */
|
||
font-size: var(--fs-size-tiny);
|
||
}
|
||
|
||
/* Inline affordances: a dismiss ×, a confirm tick, an add-chip — things that
|
||
* sit INSIDE a line of text or a card and must not disturb its rhythm. Below
|
||
* the spacing scale's first step on the vertical axis by necessity: 4px of
|
||
* padding on a 11px label already exceeds the line box these live in. */
|
||
.btn-inline {
|
||
padding: 2px var(--fs-space-1); /* 2px 4px */
|
||
font-size: var(--fs-size-tiny);
|
||
line-height: 1;
|
||
}
|
||
|
||
/* Full width, for a form's single submitting action — the auth screens. Width
|
||
* is orthogonal to size, so it composes: `btn-primary btn-block`. */
|
||
.btn-block {
|
||
display: flex; /* not `block` — the shared shape centres with flex */
|
||
width: 100%;
|
||
padding: var(--fs-space-3) var(--fs-space-4); /* 12px 16px — a touch taller,
|
||
because a full-width button
|
||
is the page's main action */
|
||
font-size: var(--fs-size-body-sm);
|
||
}
|