Files
FabledGrimoire/frontend/src/assets/components.css
T
bvandeusenandClaude Opus 5.5 699a4cd986
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m10s
CI & Build / Python tests (push) Successful in 1m54s
CI & Build / Build & push image (push) Successful in 52s
docs(6208): ProjectView, KnowledgeView, SnippetListView and components.css comments drop citations and stories
components.css says why shared recipes live there without the five-copy
.btn-primary count; KnowledgeView's editor-routing and chip comments state
the present reason instead of the bug that once routed snippets wrong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:10:45 -04:00

375 lines
14 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Shared component styles — the house recipes, in one place.
* ===========================================================================
*
* WHY THIS FILE EXISTS
*
* A recipe copied into several scoped stylesheets drifts — paddings, font
* sizes, disabled states — and nothing detects it, 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. Shared recipes live here instead.
*
* 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. */
.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) ───────────────────────────────────────────────
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). */
.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: families that are 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); }
/* --- usage badge ----------------------------------------------------------
"surfaced N×, opened M×" on a list row, for any record kind the retrieval
surfaces can choose: snippets and notes from note_usage_events, rules from
rule_usage_events. Shared here because a second scoped copy is how a
duplicated CSS family starts.
Geometry and colour only. A view keeps its own spacing as a scoped
remainder, the way it does for every other recipe in this file. */
.usage-tag {
font-size: 0.7rem;
padding: 0.1rem 0.4rem;
border-radius: 4px;
white-space: nowrap;
font-variant-numeric: tabular-nums;
background: color-mix(in srgb, var(--fs-text-tertiary) 15%, transparent);
color: var(--fs-text-tertiary-fg);
}
/* Dead weight is a nudge, not an error — it warns in the warning colour rather
than the danger one, because the record isn't broken, just unearned. */
.usage-tag.usage-dead {
background: color-mix(in srgb, var(--fs-warning) 18%, transparent);
color: var(--fs-warning-fg);
}