CI & Build / TypeScript typecheck (push) Failing after 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 24s
CI & Build / Python tests (push) Successful in 1m2s
CI & Build / Build & push image (push) Skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
354 lines
14 KiB
CSS
354 lines
14 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(--fs-action-primary);
|
||
color: var(--fs-text-on-action);
|
||
}
|
||
.btn-primary:not(:disabled):hover {
|
||
background: var(--fs-action-primary-hover);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: var(--fs-action-secondary);
|
||
color: var(--fs-text-on-action);
|
||
}
|
||
.btn-secondary:not(:disabled):hover {
|
||
background: var(--fs-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(--fs-text-primary);
|
||
}
|
||
.btn-ghost:not(:disabled):hover {
|
||
border: var(--fs-border-hover);
|
||
background: var(--fs-surface-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(--fs-action-destructive);
|
||
color: var(--fs-text-on-action);
|
||
}
|
||
.btn-danger:not(:disabled):hover {
|
||
background: var(--fs-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(--fs-text-tertiary);
|
||
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(--fs-text-primary); }
|
||
.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(--fs-action-destructive);
|
||
color: var(--fs-action-destructive);
|
||
}
|
||
.btn-danger-outline:not(:disabled):hover {
|
||
background: var(--fs-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);
|
||
}
|
||
|
||
|
||
/* ── Modal ─────────────────────────────────────────────────────────────────
|
||
The one overlay/card/button shape for every in-app dialog (ConfirmDialog,
|
||
the create-project / merge-snippet / systems dialogs, the editors' confirm
|
||
prompts). Global on purpose: ConfirmDialog teleports to <body> and has no
|
||
styles of its own, so these must be loaded with the app, not with whichever
|
||
view happens to be open. Views add only their own overrides (a wider card,
|
||
a form layout). Destructive = action-destructive per the Hybrid rule. */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: var(--fs-overlay);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 200;
|
||
}
|
||
.modal-card {
|
||
background: var(--fs-surface-raised);
|
||
border: 1px solid var(--fs-border-color);
|
||
border-radius: var(--fs-radius-lg);
|
||
padding: 1.5rem;
|
||
width: 100%;
|
||
max-width: 400px;
|
||
box-shadow: 0 8px 32px var(--color-shadow);
|
||
}
|
||
.modal-title {
|
||
margin: 0 0 0.75rem;
|
||
font-size: 1.05rem;
|
||
}
|
||
.modal-message {
|
||
font-size: 0.9rem;
|
||
color: var(--fs-text-secondary);
|
||
margin: 0 0 1.25rem;
|
||
line-height: 1.5;
|
||
}
|
||
.modal-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 0.5rem;
|
||
}
|
||
.modal-btn {
|
||
padding: 0.4rem 0.9rem;
|
||
border: 1px solid var(--fs-border-color);
|
||
background: var(--fs-surface-raised);
|
||
color: var(--fs-text-primary);
|
||
border-radius: var(--fs-radius-sm);
|
||
cursor: pointer;
|
||
font-size: 0.875rem;
|
||
font-family: inherit;
|
||
}
|
||
.modal-btn:hover {
|
||
background: var(--fs-surface-page);
|
||
}
|
||
.modal-btn-primary {
|
||
background: var(--fs-action-primary);
|
||
border-color: var(--fs-action-primary);
|
||
color: var(--fs-text-on-action);
|
||
}
|
||
.modal-btn-primary:hover:not(:disabled) {
|
||
background: var(--fs-action-primary-hover);
|
||
}
|
||
.modal-btn-primary:disabled {
|
||
opacity: 0.5;
|
||
cursor: default;
|
||
}
|
||
.modal-btn-danger {
|
||
background: var(--fs-action-destructive);
|
||
border-color: var(--fs-action-destructive);
|
||
color: var(--fs-text-on-action);
|
||
}
|
||
.modal-btn-danger:hover {
|
||
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; }
|
||
|
||
/* Page scaffold + feedback text recipes (milestone 302, note 2917): name
|
||
families the consumer map showed to be one recipe living in many views.
|
||
A view keeps only its deviation as a scoped remainder/override. */
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 1rem;
|
||
}
|
||
.page-header h1 { margin: 0; }
|
||
|
||
.error-msg { color: var(--fs-error); font-size: 0.9rem; }
|
||
.state-msg { color: var(--fs-text-tertiary); font-size: 0.9rem; }
|
||
.empty-msg { color: var(--fs-text-tertiary); font-size: 0.875rem; }
|
||
|
||
.empty-title { font-size: 1rem; font-weight: 500; color: var(--fs-text-secondary); margin: 0 0 0.35rem; }
|
||
.empty-sub { font-size: 0.85rem; color: var(--fs-text-tertiary); margin: 0 0 1rem; }
|
||
|
||
.required { color: var(--fs-error); }
|
||
.field-hint { margin: 0.3rem 0 0; font-size: 0.8rem; color: var(--fs-text-tertiary); }
|