Niveus/niveus/components/surface.css
Nexoniarz 369d1cd8f7 Add Glaze material, pickers, slider and design guidelines
Glaze is glass with a dial: --nv-glass-frost (0 clear .. 1 heavy) moves
backdrop blur, tint and saturation together, read where the surface is
drawn so any subtree or slider can change it. A masked rim and a specular
bloom follow the pointer via niveus.js; without it the light sits at its
default and the material still reads. Falls back to the elevated solid
under reduced transparency or without backdrop-filter.

New components: .nv-slider (binds to a token with data-nv-var),
.nv-calendar, .nv-timepicker (24-hour by default, optional 24H / AM/PM
switch, value always HH:MM), and .nv-picker with .nv-menu--listbox (search,
type-ahead, keys). All render without niveus.js except where noted.

GUIDELINES.md is the normative rulebook for people and AI agents.

check.py, build.py and build.py --check pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 09:24:24 +01:00

215 lines
7.6 KiB
CSS

/* ==========================================================================
Niveus — Surface
--------------------------------------------------------------------------
The material vocabulary, expressed as classes. Every other component is
built from these; applications use them directly for anything the system
does not ship as a named component.
.nv-surface a container using the solid material
.nv-material-{type} apply a material to any element
.nv-elevation-{0..5} apply a depth level to any element
A material and an elevation are independent: .nv-material-solid
.nv-elevation-3 is a legitimate combination. What is not legitimate is a
raw box-shadow that belongs to neither scale.
========================================================================== */
.nv-surface {
background-color: var(--nv-color-surface);
color: var(--nv-color-text-primary);
border: var(--nv-border-width) solid var(--nv-color-border);
border-radius: var(--nv-radius-lg);
transition: var(--nv-transition-surface);
}
/* --- Materials ------------------------------------------------------------- */
.nv-material-solid {
background-color: var(--nv-material-solid-bg);
border: var(--nv-border-width) solid var(--nv-material-solid-border);
box-shadow: var(--nv-material-solid-shadow);
backdrop-filter: none;
}
.nv-material-elevated {
background-color: var(--nv-material-elevated-bg);
border: var(--nv-border-width) solid var(--nv-material-elevated-border);
box-shadow: var(--nv-material-elevated-shadow);
backdrop-filter: none;
}
.nv-material-inset {
background-color: var(--nv-material-inset-bg);
border: var(--nv-border-width) solid var(--nv-material-inset-border);
box-shadow: var(--nv-material-inset-shadow);
backdrop-filter: none;
}
.nv-material-frosted {
background-color: var(--nv-material-frosted-bg);
border: var(--nv-border-width) solid var(--nv-material-frosted-border);
box-shadow: var(--nv-material-frosted-shadow);
backdrop-filter: var(--nv-backdrop-frosted);
-webkit-backdrop-filter: var(--nv-backdrop-frosted);
}
.nv-material-glass {
background-color: var(--nv-material-glass-bg);
border: var(--nv-border-width) solid var(--nv-material-glass-border);
box-shadow: var(--nv-material-glass-shadow);
backdrop-filter: var(--nv-backdrop-glass);
-webkit-backdrop-filter: var(--nv-backdrop-glass);
}
/* --- Glaze ------------------------------------------------------------------
Glass with a dial, and a rim that catches the light.
.nv-material-glaze the material
--nv-glass-frost: 0 … 1 clear glass … heavy frost (set anywhere)
Frost is read here, where the surface is drawn, not copied into a token
upstream: that is what lets a subtree (or a slider the visitor holds) change
it and have every surface inside follow. It moves three things together —
how far the backdrop is blurred, how much of the tint is laid over it, and
how much color the blur gives back — because a pane that changed only one
of them would stop looking like the same pane.
The rim is a 1.5px gradient border made with a masked ::before: bright on
the side facing the light, faint on the far side. ::after is the specular
bloom, a soft glow under the pointer. niveus.js sets --nv-light-x, -y and
-angle on the surface; without it both sit at their defaults (light from the
top left), so the material still reads as lit, just not as following you.
Zero specificity on position: a menu or dialog using it keeps the positioning
its own component gave it. */
:where(.nv-material-glaze) {
position: relative;
}
.nv-material-glaze {
--_frost: var(--nv-glass-frost);
isolation: isolate;
background-color: color-mix(
in oklab,
var(--nv-color-material-glaze)
calc(var(--nv-glaze-tint-min) + (var(--nv-glaze-tint-max) - var(--nv-glaze-tint-min)) * var(--_frost)),
transparent
);
border: var(--nv-border-width) solid transparent;
box-shadow:
var(--nv-highlight-top-strong),
inset 0 0 1.5rem -0.5rem var(--nv-color-glaze-specular),
var(--nv-outline),
var(--nv-elevation-4);
backdrop-filter:
blur(calc(4px + var(--_frost) * (var(--nv-glaze-blur-max) - 4px)))
saturate(calc(140% + var(--_frost) * 60%));
-webkit-backdrop-filter:
blur(calc(4px + var(--_frost) * (var(--nv-glaze-blur-max) - 4px)))
saturate(calc(140% + var(--_frost) * 60%));
}
.nv-material-glaze::before,
.nv-material-glaze::after {
content: "";
position: absolute;
inset: calc(var(--nv-border-width) * -1);
border-radius: inherit;
pointer-events: none;
}
.nv-material-glaze::before {
padding: var(--nv-glaze-rim);
background: linear-gradient(
var(--nv-light-angle, 135deg),
var(--nv-color-glaze-rim-lit) 0%,
transparent 38%,
transparent 62%,
var(--nv-color-glaze-rim-far) 100%
);
mask:
linear-gradient(currentcolor 0 0) content-box,
linear-gradient(currentcolor 0 0);
mask-composite: exclude;
-webkit-mask-composite: xor;
}
.nv-material-glaze::after {
z-index: -1;
background: radial-gradient(
circle 16rem at var(--nv-light-x, 15%) var(--nv-light-y, 0%),
var(--nv-color-glaze-specular),
transparent 70%
);
}
/* Where translucency cannot be honoured, glaze falls back to the elevated
solid, as the other glass materials do. */
@media (prefers-reduced-transparency: reduce) {
.nv-material-glaze {
background-color: var(--nv-color-surface-elevated);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.nv-material-glaze::after { display: none; }
}
@supports not (backdrop-filter: blur(1px)) {
.nv-material-glaze { background-color: var(--nv-color-surface-elevated); }
.nv-material-glaze::after { display: none; }
}
/* --- Elevation -------------------------------------------------------------
Elevation utilities carry three things at once, because a lifted surface is
never only a shadow: the shadow itself, the highlight along its top edge,
and — on a dark canvas, where a shadow has nothing left to darken — a tint
that lets more light reach the surface the higher it sits. The tint is
transparent in light appearance, so the two read as one scale. */
.nv-elevation-0 { box-shadow: var(--nv-elevation-0); }
.nv-elevation-1 {
box-shadow: var(--nv-highlight-top), var(--nv-elevation-1);
background-image: linear-gradient(var(--nv-color-elevation-tint-1) 0 0);
}
.nv-elevation-2 {
box-shadow: var(--nv-highlight-top), var(--nv-elevation-2);
background-image: linear-gradient(var(--nv-color-elevation-tint-2) 0 0);
}
.nv-elevation-3 {
box-shadow: var(--nv-highlight-top), var(--nv-elevation-3);
background-image: linear-gradient(var(--nv-color-elevation-tint-3) 0 0);
}
.nv-elevation-4 {
box-shadow: var(--nv-highlight-top), var(--nv-elevation-4);
background-image: linear-gradient(var(--nv-color-elevation-tint-4) 0 0);
}
.nv-elevation-5 {
box-shadow: var(--nv-highlight-top-strong), var(--nv-elevation-5);
background-image: linear-gradient(var(--nv-color-elevation-tint-5) 0 0);
}
.nv-elevation-inset-sm { box-shadow: var(--nv-elevation-inset-sm); }
.nv-elevation-inset-md { box-shadow: var(--nv-elevation-inset-md); }
.nv-elevation-inset-lg { box-shadow: var(--nv-elevation-inset-lg); }
/* --- Divider ---------------------------------------------------------------- */
.nv-divider {
border: 0;
border-top: var(--nv-border-width) solid var(--nv-color-border-subtle);
margin-block: var(--nv-space-4);
}
.nv-divider--vertical {
border-top: 0;
border-left: var(--nv-border-width) solid var(--nv-color-border-subtle);
margin-block: 0;
margin-inline: var(--nv-space-4);
align-self: stretch;
}