Niveus: a tactile, adaptive interface language

Bevelled controls with light falling across them, fields as wells, solid
windows, and glass kept for the chrome that floats over everything else.
Warm graphite in dark, soft paper in light, one brand color driving both.
Ships its own fonts (Instrument Sans, JetBrains Mono; OFL).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-03 17:47:03 +02:00
commit 23b8dd376d
51 changed files with 15664 additions and 0 deletions

175
niveus/components/menu.css Normal file
View file

@ -0,0 +1,175 @@
/* ==========================================================================
Niveus — Menu
--------------------------------------------------------------------------
<button class="nv-button nv-menu-trigger" popovertarget="actions"
style="--nv-anchor: --actions">Actions</button>
<div class="nv-menu" popover id="actions" style="--nv-anchor: --actions">
<button class="nv-menu__item">Rename</button>
<hr class="nv-menu__separator">
<button class="nv-menu__item nv-menu__item--danger">Delete</button>
</div>
A menu has a spatial relationship with the control that opened it: anchor
positioning ties it to the trigger, and it scales out of the trigger's edge
rather than appearing somewhere unrelated. Both sides name the same anchor
through --nv-anchor, so the pair stays obvious in the markup.
Light dismiss, Escape, and top-layer stacking come from the popover
attribute — none of it is scripted.
========================================================================== */
.nv-menu-trigger {
anchor-name: var(--nv-anchor);
}
.nv-menu {
position: fixed;
inset: auto;
margin: 0;
padding: var(--nv-space-1);
min-inline-size: 12rem;
max-inline-size: min(20rem, calc(100vw - var(--nv-space-8)));
max-block-size: min(24rem, calc(100svh - var(--nv-space-12)));
overflow-y: auto;
overscroll-behavior: contain;
/* Below the trigger, aligned to its inline-start edge, flipping to whichever
side has room. */
position-anchor: var(--nv-anchor);
position-area: block-end span-inline-end;
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
margin-block-start: var(--nv-space-2);
background-color: var(--nv-material-frosted-bg);
border: var(--nv-border-width) solid var(--nv-material-frosted-border);
border-radius: var(--nv-radius-lg);
box-shadow: var(--nv-material-frosted-shadow);
backdrop-filter: var(--nv-backdrop-frosted);
-webkit-backdrop-filter: var(--nv-backdrop-frosted);
color: var(--nv-color-text-primary);
/* Grows from the edge nearest the trigger. */
transform-origin: center block-start;
opacity: 0;
scale: 0.96;
translate: 0 calc(var(--nv-travel-sm) * -1);
transition:
opacity var(--nv-duration-quick) var(--nv-ease-standard),
scale var(--nv-duration-quick) var(--nv-ease-entrance),
translate var(--nv-duration-quick) var(--nv-ease-entrance),
display var(--nv-duration-quick) allow-discrete,
overlay var(--nv-duration-quick) allow-discrete;
}
.nv-menu:popover-open {
opacity: 1;
scale: 1;
translate: 0;
}
@starting-style {
.nv-menu:popover-open {
opacity: 0;
scale: 0.96;
translate: 0 calc(var(--nv-travel-sm) * -1);
}
}
/* Without anchor positioning the menu cannot know where its trigger is, so it
falls back to the popover's own centring rather than landing somewhere
arbitrary. The relationship is lost; the usability is not. */
@supports not (anchor-name: --nv-test) {
.nv-menu {
inset: 0;
margin: auto;
transform-origin: center;
}
}
/* --- Items -------------------------------------------------------------------- */
.nv-menu__item {
display: flex;
align-items: center;
gap: var(--nv-space-2-5);
inline-size: 100%;
padding: var(--nv-space-2) var(--nv-space-2-5);
background-color: transparent;
border: 0;
border-radius: var(--nv-radius-sm);
color: var(--nv-color-text-secondary);
font: var(--nv-font-label);
text-align: start;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
transition: var(--nv-transition-colors);
}
@media (hover: hover) {
.nv-menu__item:hover:not(:disabled, [aria-disabled="true"]) {
background-color: var(--nv-color-surface-hover);
color: var(--nv-color-text-primary);
}
}
.nv-menu__item:focus-visible {
outline: none;
background-color: var(--nv-color-surface-hover);
color: var(--nv-color-text-primary);
box-shadow: var(--nv-focus-ring);
}
.nv-menu__item--danger {
color: var(--nv-color-danger-text);
}
@media (hover: hover) {
.nv-menu__item--danger:hover:not(:disabled, [aria-disabled="true"]) {
background-color: var(--nv-color-danger-subtle);
color: var(--nv-color-danger-text);
}
}
.nv-menu__item:disabled,
.nv-menu__item[aria-disabled="true"] {
color: var(--nv-color-text-disabled);
cursor: not-allowed;
}
.nv-menu__item[aria-checked="true"]::before {
content: "";
inline-size: 0.28em;
block-size: 0.55em;
margin-inline-end: var(--nv-space-1);
translate: 0 -0.12em;
rotate: 45deg;
border-inline-end: 2px solid var(--nv-color-accent);
border-block-end: 2px solid var(--nv-color-accent);
}
.nv-menu__shortcut {
margin-inline-start: auto;
padding-inline-start: var(--nv-space-4);
color: var(--nv-color-text-tertiary);
font: var(--nv-font-caption);
}
.nv-menu__separator {
border: 0;
border-block-start: var(--nv-border-width) solid var(--nv-color-border-subtle);
margin: var(--nv-space-1) 0;
}
.nv-menu__label {
padding: var(--nv-space-2) var(--nv-space-2-5) var(--nv-space-1);
font: var(--nv-font-caption);
font-weight: var(--nv-font-weight-semibold);
letter-spacing: var(--nv-letter-spacing-wider);
text-transform: uppercase;
color: var(--nv-color-text-tertiary);
}