mirror of
https://github.com/Nexoniarz/Niveus.git
synced 2026-11-03 04:34:26 +00:00
Niveus read flat: thin sheens, short shadows, opaque grey chrome and a painted canvas. This retunes values and component internals only; every class and token name is unchanged, so existing pages pick it up as is. - Canvas: a faint accent light falls across the page, so translucent layers have something to show through. - Surfaces: the surface sheen runs the full height (lit top, shaded foot); raised and filled sheens, highlights and bevels are stronger; the elevation scale is deeper and softer. - Translucency: cards, dialogs, the sidebar and tab tracks let some of the canvas through, with blur so busy grounds never read as texture; frosted and glass materials are clearer with more blur. - Filled buttons give off a soft glow of their own color. - Light fields are paper instead of grey wells that looked disabled. - Fixes: navigation bars wrap and lists scroll inside them instead of overflowing the page on phones; choice labels sit at control size; disabled ghost buttons no longer grow a filled well; the color-swatch rule is split so neither engine drops it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iMUTHrD8K9CSuHqsanje9
374 lines
11 KiB
CSS
374 lines
11 KiB
CSS
/* ==========================================================================
|
|
Niveus — Navigation
|
|
--------------------------------------------------------------------------
|
|
.nv-nav horizontal bar, frosted, sticky by default
|
|
.nv-nav--vertical sidebar
|
|
.nv-nav--plain solid instead of frosted
|
|
.nv-nav__brand / __list / __item / __section / __spacer
|
|
.nv-nav-tabs in-page tab strip with a sliding indicator
|
|
|
|
Navigation is chrome: it sits above content rather than in it. That is why
|
|
the bar is frosted and bevelled — the blur is not decoration, it is the
|
|
signal that content is passing underneath. A sidebar that scrolls with the page is not
|
|
chrome, so .nv-nav--vertical drops the translucency by default.
|
|
========================================================================== */
|
|
|
|
.nv-nav {
|
|
position: sticky;
|
|
inset-block-start: 0;
|
|
z-index: 10;
|
|
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: var(--nv-space-2) var(--nv-space-4);
|
|
padding: var(--nv-space-2) var(--nv-space-4);
|
|
min-block-size: var(--nv-control-height-lg);
|
|
background-color: var(--nv-material-frosted-bg);
|
|
background-image: var(--nv-sheen-raised);
|
|
border-block-end: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
|
|
|
/* The travelling selection marker sits behind its item at z-index -1; the
|
|
bar has to be its own stacking context or the marker would fall behind
|
|
the bar's background and disappear. */
|
|
isolation: isolate;
|
|
box-shadow: var(--nv-bevel);
|
|
backdrop-filter: var(--nv-backdrop-frosted);
|
|
-webkit-backdrop-filter: var(--nv-backdrop-frosted);
|
|
transition: var(--nv-transition-surface);
|
|
}
|
|
|
|
/* The bar only earns a shadow once something is behind it. Set data-scrolled
|
|
from the application on scroll. */
|
|
.nv-nav[data-scrolled] {
|
|
box-shadow: var(--nv-bevel), var(--nv-elevation-2);
|
|
transition: box-shadow var(--nv-duration-normal) var(--nv-ease-standard);
|
|
}
|
|
|
|
.nv-nav--plain {
|
|
background-color: var(--nv-color-surface);
|
|
background-image: var(--nv-sheen-raised);
|
|
backdrop-filter: none;
|
|
-webkit-backdrop-filter: none;
|
|
}
|
|
|
|
.nv-nav--static {
|
|
position: static;
|
|
}
|
|
|
|
/* A navigation group used inside content rather than as page chrome: no bar,
|
|
no border, no blur — just the items and their travelling selection. */
|
|
.nv-nav--inline {
|
|
position: static;
|
|
display: inline-flex;
|
|
min-block-size: 0;
|
|
padding: var(--nv-space-1);
|
|
|
|
background-color: transparent;
|
|
border-block-end: 0;
|
|
background-image: none;
|
|
box-shadow: none;
|
|
backdrop-filter: none;
|
|
-webkit-backdrop-filter: none;
|
|
}
|
|
|
|
.nv-nav__brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--nv-space-2);
|
|
font: var(--nv-font-title);
|
|
letter-spacing: var(--nv-letter-spacing-tight);
|
|
color: var(--nv-color-text-primary);
|
|
text-decoration: none;
|
|
padding-inline: var(--nv-space-1);
|
|
}
|
|
|
|
.nv-nav__list {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--nv-space-1);
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
min-inline-size: 0;
|
|
/* On a narrow screen a long list scrolls inside the bar instead of pushing
|
|
the page sideways. */
|
|
max-inline-size: 100%;
|
|
overflow-x: auto;
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
.nv-nav__list::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.nv-nav__spacer {
|
|
flex: 1;
|
|
}
|
|
|
|
/* --- Item --------------------------------------------------------------------
|
|
Items are flat until they are touched. The active item is the only one with
|
|
material, which keeps the bar quiet while still answering "where am I". */
|
|
|
|
.nv-nav__item {
|
|
/* Rides above a travelling marker once niveus.js has given it a name. */
|
|
view-transition-class: nv-carried;
|
|
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--nv-space-2);
|
|
padding: var(--nv-space-2) var(--nv-space-3);
|
|
min-block-size: var(--nv-control-height-md);
|
|
|
|
border: var(--nv-border-width) solid transparent;
|
|
border-radius: var(--nv-radius-md);
|
|
background-color: transparent;
|
|
|
|
color: var(--nv-color-text-secondary);
|
|
font: var(--nv-font-label);
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
cursor: pointer;
|
|
transition: var(--nv-transition-control);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.nv-nav__item:hover {
|
|
background-color: var(--nv-color-surface-hover);
|
|
color: var(--nv-color-text-primary);
|
|
}
|
|
}
|
|
|
|
.nv-nav__item:active {
|
|
background-color: var(--nv-color-surface-active);
|
|
box-shadow: var(--nv-elevation-inset-sm);
|
|
transition-duration: var(--nv-duration-instant);
|
|
}
|
|
|
|
.nv-nav__item:focus-visible {
|
|
outline: none;
|
|
box-shadow: var(--nv-focus-ring);
|
|
}
|
|
|
|
/* Selection is a material that *moves*. The emphasis lives in a pseudo-element
|
|
rather than on the item itself, and carries a view-transition-name: when the
|
|
selection changes inside a transition, the browser interpolates the marker's
|
|
old box into its new one, so the highlight slides from the previous item to
|
|
the next instead of blinking out and in somewhere else.
|
|
|
|
Only one item in a group may be selected at a time — that is what keeps the
|
|
name unique. A page with several selection groups must give each its own
|
|
--nv-selection-name, or the duplicate name aborts the transition. */
|
|
|
|
/* Selection changes color and gains material — but not weight. Bolding the
|
|
selected item re-measures its text, which reflows every item beside it on
|
|
each change, and makes the old and new labels cross-fade against each other
|
|
at different widths. The pill is the cue; the weight was costing a flicker
|
|
for nothing. */
|
|
.nv-nav__item[aria-current],
|
|
.nv-nav__item--active,
|
|
.nv-nav__item[aria-selected="true"] {
|
|
color: var(--nv-color-text-primary);
|
|
}
|
|
|
|
.nv-nav__item[aria-current]::before,
|
|
.nv-nav__item--active::before,
|
|
.nv-nav__item[aria-selected="true"]::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: -1;
|
|
|
|
background-color: var(--nv-color-surface-elevated);
|
|
background-image: var(--nv-sheen-raised);
|
|
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
|
border-radius: inherit;
|
|
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
|
|
view-transition-class: nv-travelling;
|
|
}
|
|
|
|
/* The marker is named only while its own group is changing (niveus.js sets
|
|
data-nv-travelling for the length of the transition). A permanent name
|
|
would lift every marker on the page out of the page snapshot on every
|
|
transition, theme changes included. */
|
|
[data-nv-travelling] .nv-nav__item[aria-current]::before,
|
|
[data-nv-travelling] .nv-nav__item--active::before,
|
|
[data-nv-travelling] .nv-nav__item[aria-selected="true"]::before {
|
|
view-transition-name: var(--nv-selection-name);
|
|
}
|
|
|
|
/* Hovering an already-selected item should not restate the emphasis. */
|
|
@media (hover: hover) {
|
|
.nv-nav__item[aria-current]:hover,
|
|
.nv-nav__item--active:hover,
|
|
.nv-nav__item[aria-selected="true"]:hover {
|
|
background-color: transparent;
|
|
}
|
|
}
|
|
|
|
.nv-nav__item[aria-disabled="true"] {
|
|
color: var(--nv-color-text-disabled);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.nv-nav__icon {
|
|
inline-size: 1.15em;
|
|
block-size: 1.15em;
|
|
flex: none;
|
|
opacity: 0.85;
|
|
}
|
|
|
|
.nv-nav__badge {
|
|
margin-inline-start: auto;
|
|
padding-inline: var(--nv-space-1-5);
|
|
min-inline-size: var(--nv-space-5);
|
|
block-size: var(--nv-space-5);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
|
|
background-color: var(--nv-color-accent-subtle);
|
|
color: var(--nv-color-accent-text);
|
|
border-radius: var(--nv-radius-full);
|
|
font: var(--nv-font-caption);
|
|
font-size: var(--nv-font-size-2xs);
|
|
font-weight: var(--nv-font-weight-semibold);
|
|
}
|
|
|
|
/* --- Vertical ------------------------------------------------------------------ */
|
|
|
|
.nv-nav--vertical {
|
|
position: sticky;
|
|
inset-block-start: 0;
|
|
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: var(--nv-space-1);
|
|
inline-size: 15rem;
|
|
block-size: 100svh;
|
|
flex-wrap: nowrap;
|
|
padding: var(--nv-space-4) var(--nv-space-3);
|
|
/* A sidebar is a pane of the canvas rather than chrome over content, so it
|
|
stays mostly opaque — but it lets the light of the canvas through, which
|
|
is what stops it reading as a grey slab beside the page. */
|
|
background-color: color-mix(in oklab, var(--nv-color-background-subtle) 72%, transparent);
|
|
background-image: var(--nv-sheen-surface);
|
|
backdrop-filter: var(--nv-backdrop-frosted);
|
|
-webkit-backdrop-filter: var(--nv-backdrop-frosted);
|
|
border-block-end: 0;
|
|
border-inline-end: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
|
box-shadow: none;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.nv-nav--vertical .nv-nav__list {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: var(--nv-space-0-5);
|
|
}
|
|
|
|
.nv-nav--vertical .nv-nav__item {
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.nv-nav--vertical .nv-nav__brand {
|
|
padding: var(--nv-space-2) var(--nv-space-3);
|
|
margin-block-end: var(--nv-space-3);
|
|
}
|
|
|
|
/* Engraved, like .nv-overline: a section marks a region, it is not read. */
|
|
.nv-nav__section {
|
|
padding: var(--nv-space-4) var(--nv-space-3) var(--nv-space-1-5);
|
|
font: var(--nv-font-engraved);
|
|
letter-spacing: var(--nv-letter-spacing-engraved);
|
|
text-transform: uppercase;
|
|
color: var(--nv-color-text-tertiary);
|
|
}
|
|
|
|
.nv-nav__section:first-child {
|
|
padding-block-start: 0;
|
|
}
|
|
|
|
/* --- Tabs ------------------------------------------------------------------------
|
|
For switching views inside a page, not for moving between pages. The strip
|
|
itself is inset, so the selected tab reads as lifted out of the track. */
|
|
|
|
.nv-nav-tabs {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
max-inline-size: 100%;
|
|
gap: var(--nv-space-0-5);
|
|
padding: var(--nv-space-1);
|
|
background-color: color-mix(in oklab, var(--nv-color-surface-inset) 80%, transparent);
|
|
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
|
border-radius: var(--nv-radius-md);
|
|
box-shadow: var(--nv-elevation-inset-md), var(--nv-ledge);
|
|
overflow-x: auto;
|
|
scrollbar-width: none;
|
|
isolation: isolate;
|
|
}
|
|
|
|
.nv-nav-tabs::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.nv-nav-tabs .nv-nav__item {
|
|
border-radius: var(--nv-radius-sm);
|
|
min-block-size: var(--nv-control-height-sm);
|
|
padding-block: var(--nv-space-1);
|
|
}
|
|
|
|
/* Tabs are a selection group of their own, so they get their own marker name;
|
|
sharing the navigation bar's would mean two visible elements with the same
|
|
view-transition-name. */
|
|
.nv-nav-tabs {
|
|
--nv-selection-name: nv-tabs-selection;
|
|
}
|
|
|
|
.nv-nav-tabs .nv-nav__item[aria-selected="true"]::before {
|
|
border-radius: var(--nv-radius-sm);
|
|
}
|
|
|
|
/* --- Panels ----------------------------------------------------------------------
|
|
Switching tabs should feel like moving through one surface rather than
|
|
swapping the interface. nv.selectTab() gives the outgoing and incoming
|
|
panels one shared name for the length of the change, so they hand over
|
|
inside the same box — and slide with the direction of travel. The class is
|
|
what picks up the content animation in continuity.css. */
|
|
.nv-tab-panel {
|
|
view-transition-class: nv-content;
|
|
}
|
|
|
|
/* --- Breadcrumbs -------------------------------------------------------------------- */
|
|
|
|
.nv-breadcrumbs {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--nv-space-2);
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
font: var(--nv-font-caption);
|
|
color: var(--nv-color-text-tertiary);
|
|
}
|
|
|
|
.nv-breadcrumbs li + li::before {
|
|
content: "/";
|
|
margin-inline-end: var(--nv-space-2);
|
|
color: var(--nv-color-border-strong);
|
|
}
|
|
|
|
.nv-breadcrumbs a {
|
|
color: var(--nv-color-text-secondary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.nv-breadcrumbs a:hover {
|
|
color: var(--nv-color-text-primary);
|
|
}
|
|
|
|
.nv-breadcrumbs [aria-current] {
|
|
color: var(--nv-color-text-primary);
|
|
font-weight: var(--nv-font-weight-medium);
|
|
}
|