asw/assets/css/layers/03-components.css
Vigilio Desto 376bfb2364
cleanup: replace 37 hardcoded values with token references
Across 6 layer files: 1px→var(--border-width), 1rem→var(--space-4),
2rem→var(--space-6), etc. Intentionally left: 3px accent borders,
graduated chart spacing, em-based sub/sup ratios.
2026-04-10 19:27:46 +02:00

955 lines
25 KiB
CSS

/**
* 03-components.css
* UI component patterns (buttons, forms, nav, dialog, details)
* Part of: Agentic Semantic Web
*
* Ported from: Pico CSS v2.1.1
* License: MIT
*
* Modernizations:
* - Uses `accent-color` for checkbox/radio (simpler than background-image)
* - Drops class-based button variants (.secondary, .contrast, .outline)
*/
/* ── Buttons ───────────────────────────────────────────────────────────*/
button {
margin: 0;
overflow: visible;
font-family: inherit;
text-transform: none;
}
button,
[type=submit],
[type=reset],
[type=button],
[role=button] {
display: inline-block;
padding: var(--input-py) var(--input-px);
border: var(--border-width) solid var(--border);
border-radius: var(--radius-md);
outline: none;
background-color: var(--surface);
color: var(--text);
font-weight: var(--font-weight-4);
font-size: var(--text-base);
line-height: var(--leading);
text-align: center;
text-decoration: none;
cursor: pointer;
user-select: none;
transition: background-color var(--ease),
border-color var(--ease),
color var(--ease);
}
button:is(:hover, :active, :focus-visible),
[type=submit]:is(:hover, :active, :focus-visible),
[type=reset]:is(:hover, :active, :focus-visible),
[type=button]:is(:hover, :active, :focus-visible),
[role=button]:is(:hover, :active, :focus-visible) {
background-color: var(--surface-hover);
border-color: var(--border);
color: var(--text);
}
button:focus-visible,
[type=submit]:focus-visible,
[type=reset]:focus-visible,
[type=button]:focus-visible,
[role=button]:focus-visible {
box-shadow: 0 0 0 var(--outline-width) var(--accent-focus);
}
button[disabled],
[type=submit][disabled],
[type=reset][disabled],
[type=button][disabled],
[role=button][disabled] {
opacity: 0.5;
pointer-events: none;
}
/* ── Form Elements ─────────────────────────────────────────────────────*/
input,
optgroup,
select,
textarea {
margin: 0;
font-size: var(--text-base);
line-height: var(--leading);
font-family: inherit;
letter-spacing: inherit;
}
fieldset {
width: 100%;
margin: 0;
margin-bottom: var(--space-4);
padding: 0;
border: 0;
}
fieldset legend,
label {
display: block;
margin-bottom: calc(var(--space-4) * 0.375);
color: var(--text);
font-weight: var(--font-weight-4);
}
input:not([type=checkbox], [type=radio], [type=range], [type=file]),
select,
textarea {
width: 100%;
padding: var(--input-py) var(--input-px);
border: var(--border-width) solid var(--input-border);
border-radius: var(--radius-md);
outline: none;
background-color: var(--input-bg);
color: var(--text);
font-weight: var(--font-weight-4);
transition: background-color var(--ease),
border-color var(--ease),
color var(--ease);
}
input:not([type=checkbox], [type=radio], [type=range], [type=file], [readonly]):is(:active, :focus-visible),
select:not([readonly]):is(:active, :focus-visible),
textarea:not([readonly]):is(:active, :focus-visible) {
border-color: var(--accent);
background-color: var(--input-active-bg);
}
input:not([type=checkbox], [type=radio], [type=range], [type=file], [readonly]):focus-visible,
select:not([readonly]):focus-visible,
textarea:not([readonly]):focus-visible {
box-shadow: 0 0 0 var(--outline-width) var(--accent);
}
input:not([type=checkbox], [type=radio], [type=range], [type=file])[disabled],
select[disabled],
textarea[disabled] {
opacity: var(--disabled-opacity);
pointer-events: none;
}
input::placeholder,
textarea::placeholder,
select:invalid {
color: var(--text-3);
opacity: 1;
}
input:not([type=checkbox], [type=radio]),
select,
textarea {
margin-bottom: var(--space-4);
}
/* ── Select Dropdown ───────────────────────────────────────────────────*/
select:not([multiple], [size]) {
padding-right: calc(var(--input-px) + 1.5rem);
background-image: var(--icon-chevron);
background-position: center right 0.75rem;
background-size: 1rem auto;
background-repeat: no-repeat;
}
select[multiple] option:checked {
background: var(--input-selected);
color: var(--text);
}
/* ── Textarea ──────────────────────────────────────────────────────────*/
textarea {
display: block;
resize: vertical;
}
/* ── Checkboxes & Radios (Modern CSS) ──────────────────────────────────*/
label:has([type=checkbox], [type=radio]) {
width: fit-content;
cursor: pointer;
}
[type=checkbox],
[type=radio] {
width: 1.25em;
height: 1.25em;
margin-top: -0.125em;
margin-right: 0.5em;
vertical-align: middle;
cursor: pointer;
/* Modern CSS: use browser's native styling with our accent color */
accent-color: var(--accent);
}
[type=checkbox] ~ label,
[type=radio] ~ label {
display: inline-block;
margin-bottom: 0;
cursor: pointer;
}
[type=checkbox] ~ label:not(:last-of-type),
[type=radio] ~ label:not(:last-of-type) {
margin-right: 1em;
}
/* ── Validation States ─────────────────────────────────────────────────*/
input[aria-invalid=false],
select[aria-invalid=false],
textarea[aria-invalid=false] {
border-color: var(--accent);
}
input[aria-invalid=false]:is(:active, :focus-visible),
select[aria-invalid=false]:is(:active, :focus-visible),
textarea[aria-invalid=false]:is(:active, :focus-visible) {
border-color: var(--accent-hover);
box-shadow: 0 0 0 var(--outline-width) var(--accent-focus) !important;
}
input[aria-invalid=true],
select[aria-invalid=true],
textarea[aria-invalid=true] {
border-color: var(--error);
}
input[aria-invalid=true]:is(:active, :focus-visible),
select[aria-invalid=true]:is(:active, :focus-visible),
textarea[aria-invalid=true]:is(:active, :focus-visible) {
border-color: var(--error-active);
box-shadow: 0 0 0 var(--outline-width) var(--error-focus) !important;
}
/* ── Helper Text ───────────────────────────────────────────────────────*/
:where(input, select, textarea, fieldset) + small {
display: block;
width: 100%;
margin-top: calc(var(--space-4) * -0.75);
margin-bottom: var(--space-4);
color: var(--text-3);
}
:where(input, select, textarea, fieldset)[aria-invalid=false] + small {
color: var(--accent);
}
:where(input, select, textarea, fieldset)[aria-invalid=true] + small {
color: var(--accent-red);
}
label > :where(input, select, textarea) {
margin-top: calc(var(--space-4) * 0.25);
}
/* ── Navigation ────────────────────────────────────────────────────────*/
/* Semantic nav: <nav><strong>Brand</strong><ul><li><a>...</a></li></ul></nav> */
body > nav {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: var(--space-5);
padding-bottom: var(--space-5);
margin-bottom: var(--space-6);
border-bottom: var(--border-width) solid var(--border);
}
body > nav strong {
font-family: var(--font-mono);
font-weight: 700;
font-size: var(--text-base);
letter-spacing: -0.03em;
}
body > nav ul {
list-style: none;
display: flex;
gap: 0;
margin: 0;
padding: 0;
font-family: var(--font-mono);
font-size: var(--text-sm);
}
body > nav ul li {
list-style: none;
margin: 0;
padding: 0;
}
body > nav ul li + li::before {
content: "|";
color: var(--text-dim);
margin: 0 0.75rem;
}
body > nav a {
color: var(--text-2);
text-decoration: none;
transition: color var(--ease);
}
body > nav a:hover,
body > nav a[aria-current="page"] {
color: var(--text);
}
/* Medium screens: allow links to wrap */
@media (--nav-compact) {
body > nav ul {
flex-wrap: wrap;
gap: 0.25rem 0;
}
}
/* Small screens: stack brand above links */
@media (--md-n-below) {
body > nav {
flex-direction: column;
align-items: flex-start;
gap: var(--space-2);
}
body > nav ul:last-child {
flex-wrap: wrap;
}
body > nav ul:last-child li + li::before {
display: none;
}
}
/* ── Nav Dropdown ──────────────────────────────────────────────────────*/
/* <details> inside <nav> becomes a dropdown menu. No classes needed.
Usage: <nav><ul><li><details><summary>Menu</summary><ul><li>...</li></ul></details></li></ul></nav> */
body > nav li:has(details) {
display: flex;
align-items: center;
}
body > nav details {
position: relative;
margin: 0;
}
body > nav details summary {
color: var(--text-2);
font-family: var(--font-mono);
font-size: var(--text-sm);
list-style: none;
cursor: pointer;
transition: color var(--ease);
}
body > nav details summary:hover {
color: var(--text);
}
/* Override accordion chevron in nav context */
body > nav details summary::after {
content: "▾";
float: none;
margin-inline-start: 0.25rem;
transform: none;
font-size: var(--text-xs);
}
body > nav details[open] > summary::after {
content: "▴";
transform: none;
}
/* Dropdown panel */
body > nav details > ul,
body > nav details > div {
position: absolute;
top: calc(100% + 0.5rem);
left: 0;
min-width: var(--size-px-12);
padding: 0.5rem 0;
margin: 0;
background: var(--surface-1);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-2);
z-index: 20;
list-style: none;
display: flex;
flex-direction: column;
}
body > nav details > ul li {
margin: 0;
padding: 0;
}
/* Remove pipe separator in dropdown items */
body > nav details > ul li + li::before {
display: none;
}
body > nav details > ul li a {
display: block;
padding: 0.35rem 1rem;
color: var(--text-2);
text-decoration: none;
font-family: var(--font-mono);
font-size: var(--text-sm);
transition: background-color var(--ease-fast), color var(--ease-fast);
}
body > nav details > ul li a:hover {
background: var(--border-subtle);
color: var(--text);
}
/* Close dropdown when clicking outside (CSS-only via :focus-within) */
nav details:not(:focus-within) > ul,
nav details:not(:focus-within) > div {
/* Allow browser default open/close behavior —
no forced hiding. Agent can add JS for click-outside. */
}
/* Mobile: dropdown becomes full-width */
@media (--md-n-below) {
nav details > ul,
nav details > div {
position: static;
box-shadow: none;
border: none;
border-left: 2px solid var(--border);
margin-left: 0.5rem;
padding: 0.25rem 0 0.25rem 0.5rem;
background: transparent;
}
}
/* ── Articles & Cards ──────────────────────────────────────────────────*/
/* Semantic article: <article><header><h3>Title</h3></header>Content</article> */
/* Container query: layout adapts to article's own width, not viewport.
An article in a sidebar shrinks gracefully; at full width it expands. */
article {
container-type: inline-size;
container-name: article;
background: transparent;
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 1rem 1.25rem;
margin: var(--space-3) 0;
}
article > header {
margin: 0 0 var(--space-2) 0;
padding: 0 0 0.4rem 0;
border-bottom: 1px solid var(--border-subtle);
border-top: none;
border-radius: 0;
background-color: transparent;
}
article header h3 {
margin: 0;
padding: 0;
font-family: var(--font-mono);
font-size: var(--text-sm);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-3);
}
/* Narrow container: compact card (sidebar, grid cell) */
@container article (max-width: 300px) {
article > header {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 0.25rem;
}
article header h3 {
font-size: var(--text-xs);
}
article > :is(p, dl, ul, ol) {
font-size: var(--text-sm);
}
}
/* Wide container: spacious layout */
@container article (min-width: 600px) {
article {
padding: var(--space-5) var(--space-6);
}
article > header {
margin-bottom: var(--space-3);
padding-bottom: var(--space-2);
}
}
/* ── Definition Lists ──────────────────────────────────────────────────*/
/* Monospace data display for dt/dd pairs */
dt {
font-family: var(--font-mono);
font-size: var(--text-sm);
font-weight: 500;
color: var(--text-2);
margin-top: var(--space-3);
}
dd {
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--text);
margin-left: 0;
margin-top: 0.15rem;
}
article dt:first-of-type {
margin-top: 0;
}
/* ── Sections ──────────────────────────────────────────────────────────*/
section + section {
padding-top: var(--space-5);
border-top: var(--border-width) solid var(--border-subtle);
}
hgroup p {
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--text-3);
margin-top: var(--space-1);
}
/* Section intro: hgroup as centered subtitle block */
section > hgroup:first-child {
text-align: center;
margin-bottom: var(--space-6);
}
/* Card variant: navigation cards use UI font h3, not session-log monospace */
article[data-role="card"] header h3 {
font-family: var(--font-ui);
font-size: var(--h3-size);
font-weight: var(--h3-weight);
text-transform: none;
letter-spacing: normal;
color: var(--text);
}
/* ── Footer ────────────────────────────────────────────────────────────*/
body > footer,
footer:last-child {
margin-top: 3rem;
padding-top: var(--space-5);
padding-bottom: var(--space-6);
border-top: var(--border-width) solid var(--border);
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--text-3);
}
/* ── Accordion / Disclosure ────────────────────────────────────────────*/
/* Standalone <details>/<summary> — no JS needed.
Nav dropdown variant lives in the Nav Dropdown section above.
Usage:
<details>
<summary>Title</summary>
<p>Content</p>
</details>
Grouped variant:
<div data-role="accordion">
<details>…</details>
<details>…</details>
</div>
*/
details:not(nav details) {
border: var(--border-width) solid var(--border);
border-radius: var(--radius-md);
margin-bottom: var(--space-3);
background: var(--surface-1);
overflow: hidden;
}
details:not(nav details) > summary {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-4);
font-family: var(--font-ui);
font-size: var(--text-base);
font-weight: var(--font-weight-5, 500);
color: var(--text);
cursor: pointer;
list-style: none;
user-select: none;
transition: background-color var(--ease), color var(--ease);
}
details:not(nav details) > summary::-webkit-details-marker {
display: none;
}
/* Chevron indicator */
details:not(nav details) > summary::after {
content: "▾";
font-size: var(--text-sm);
color: var(--text-3);
transition: transform var(--ease);
flex-shrink: 0;
margin-inline-start: var(--space-3);
}
details:not(nav details)[open] > summary::after {
transform: rotate(180deg);
}
details:not(nav details) > summary:hover {
background-color: var(--surface-hover);
color: var(--text);
}
details:not(nav details) > summary:focus-visible {
outline: var(--outline-width) solid var(--accent-focus);
outline-offset: -2px;
}
/* Body content */
details:not(nav details) > :not(summary) {
padding: var(--space-3) var(--space-4) var(--space-4);
border-top: var(--border-width) solid var(--border);
}
details:not(nav details) > :not(summary):last-child {
margin-bottom: 0;
}
/* Grouped accordion: flush borders between items */
[data-role="accordion"] > details:not(nav details) {
margin-bottom: 0;
border-radius: 0;
border-bottom-width: 0;
}
[data-role="accordion"] > details:not(nav details):first-child {
border-radius: var(--radius-md) var(--radius-md) 0 0;
}
[data-role="accordion"] > details:not(nav details):last-child {
border-radius: 0 0 var(--radius-md) var(--radius-md);
border-bottom-width: var(--border-width);
}
[data-role="accordion"] > details:not(nav details):only-child {
border-radius: var(--radius-md);
border-bottom-width: var(--border-width);
}
/* ── Dialog / Modal ────────────────────────────────────────────────────*/
/* Native <dialog> element. Works with dialog.showModal() / dialog.close().
Usage:
<dialog id="my-dialog">
<header><h2>Title</h2></header>
<p>Body content.</p>
<footer><button>Close</button></footer>
</dialog>
*/
dialog {
position: fixed;
inset: 0;
margin: auto;
padding: 0;
border: var(--border-width) solid var(--border);
border-radius: var(--radius-md);
background: var(--surface-1);
color: var(--text);
box-shadow: var(--shadow-4);
z-index: var(--layer-4);
max-width: min(90vw, 42rem);
max-height: min(90vh, 40rem);
overflow: auto;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
dialog:not([open]) {
display: none;
}
/* Backdrop (modal mode only — showModal()) */
dialog::backdrop {
background: color-mix(in oklch, var(--gray-15) 70%, transparent);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
/* Internal layout */
dialog > header {
padding: var(--space-4) var(--space-5);
border-bottom: var(--border-width) solid var(--border);
background: transparent;
border-top: none;
border-radius: 0;
}
dialog > header h1,
dialog > header h2,
dialog > header h3 {
margin: 0;
font-size: var(--text-2xl);
color: var(--text);
}
dialog > :not(header):not(footer) {
padding: var(--space-5);
}
dialog > footer {
padding: var(--space-3) var(--space-5);
border-top: var(--border-width) solid var(--border);
display: flex;
justify-content: flex-end;
gap: var(--space-3);
background: var(--surface);
border-radius: 0 0 var(--radius-md) var(--radius-md);
}
/* ══════════════════════════════════════════════════════════════════════════
Breadcrumb — <nav data-role="breadcrumb" aria-label="breadcrumb">
Usage:
<nav data-role="breadcrumb" aria-label="breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/docs/">Docs</a></li>
<li aria-current="page">Token System</li>
</ol>
</nav>
══════════════════════════════════════════════════════════════════════════ */
[data-role="breadcrumb"] {
font-family: var(--font-ui);
font-size: var(--text-sm);
}
[data-role="breadcrumb"] ol {
display: flex;
flex-wrap: wrap;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
gap: 0;
}
[data-role="breadcrumb"] li {
display: flex;
align-items: center;
margin: 0;
padding: 0;
}
/* Separator: slash before every item after the first */
[data-role="breadcrumb"] li + li::before {
content: "/";
color: var(--text-3);
padding-inline: var(--space-2);
user-select: none;
font-weight: var(--font-weight-4);
}
[data-role="breadcrumb"] a {
color: var(--text-2);
text-decoration: none;
transition: color var(--ease);
}
[data-role="breadcrumb"] a:hover {
color: var(--accent);
}
/* Current page: plain text, full colour, no link underline */
[data-role="breadcrumb"] [aria-current="page"] {
color: var(--text);
font-weight: var(--font-weight-5);
}
/* ══════════════════════════════════════════════════════════════════════════
Steps — <ol data-role="steps"> with data-status on each <li>
Usage:
<ol data-role="steps">
<li data-status="complete"><span>Plan</span></li>
<li data-status="active"><span>Build</span></li>
<li data-status="pending"><span>Deploy</span></li>
</ol>
data-status values: complete / active / pending
══════════════════════════════════════════════════════════════════════════ */
[data-role="steps"] {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
list-style: none;
margin: var(--space-5) 0;
padding: 0;
gap: 0;
counter-reset: steps-counter;
}
[data-role="steps"] > li {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
min-width: 0;
position: relative;
counter-increment: steps-counter;
padding-top: calc(var(--space-5) + var(--space-3)); /* room for the node circle */
text-align: center;
}
/* Connector line between steps */
[data-role="steps"] > li + li::before {
content: "";
position: absolute;
top: calc(var(--space-3) / 2 + 0.75rem); /* vertically centred on the node */
left: calc(-50% + 1.25rem);
right: calc(50% + 1.25rem);
height: var(--border-width);
background: var(--border);
z-index: 0;
}
/* Step node circle — drawn via ::after on the li */
[data-role="steps"] > li::after {
content: counter(steps-counter);
position: absolute;
top: var(--space-3);
left: 50%;
transform: translateX(-50%);
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: var(--font-weight-6);
z-index: 1;
/* default (pending) colours — overridden below */
background: var(--surface-card);
border: var(--border-width) solid var(--border);
color: var(--text-3);
transition: background var(--ease), border-color var(--ease), color var(--ease);
}
/* Step label text */
[data-role="steps"] > li > span {
font-family: var(--font-ui);
font-size: var(--text-sm);
color: var(--text-3);
transition: color var(--ease);
padding-inline: var(--space-2);
word-break: break-word;
}
/* ── Status variants ────────────────────────────────────────────── */
[data-role="steps"] > [data-status="complete"]::after {
content: "✓";
background: var(--accent-subtle);
border-color: var(--accent);
color: var(--accent);
}
[data-role="steps"] > [data-status="complete"] > span {
color: var(--text-2);
}
/* Connector line from a completed step is accented */
[data-role="steps"] > [data-status="complete"] + li::before {
background: var(--accent);
}
[data-role="steps"] > [data-status="active"]::after {
background: var(--accent);
border-color: var(--accent);
color: var(--on-accent);
box-shadow: 0 0 0 var(--border-size-2) var(--accent-focus);
}
[data-role="steps"] > [data-status="active"] > span {
color: var(--text);
font-weight: var(--font-weight-5);
}
/* pending is the default — no additional rules needed */
/* ── Vertical variant ───────────────────────────────────────────── */
/* Add data-layout="vertical" to the ol for a top-down flow */
[data-role="steps"][data-layout="vertical"] {
flex-direction: column;
gap: var(--space-5);
}
[data-role="steps"][data-layout="vertical"] > li {
flex-direction: row;
align-items: center;
text-align: left;
padding-top: 0;
padding-left: calc(1.5rem + var(--space-4)); /* room for the node */
gap: var(--space-4);
}
[data-role="steps"][data-layout="vertical"] > li::after {
top: 50%;
left: 0;
transform: translateY(-50%);
}
/* Vertical connector: vertical line */
[data-role="steps"][data-layout="vertical"] > li + li::before {
content: "";
position: absolute;
top: calc(-1 * var(--space-5));
left: 0.675rem; /* centred on the 1.5rem node */
width: var(--border-width);
height: var(--space-5);
right: auto;
background: var(--border);
}
[data-role="steps"][data-layout="vertical"] > [data-status="complete"] + li::before {
background: var(--accent);
}
[data-role="steps"][data-layout="vertical"] > li > span {
padding-inline: 0;
}