mirror of
https://github.com/Nexoniarz/Niveus.git
synced 2026-11-03 04:34:26 +00:00
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:
commit
23b8dd376d
51 changed files with 15664 additions and 0 deletions
103
niveus/components/badge.css
Normal file
103
niveus/components/badge.css
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Badge
|
||||
--------------------------------------------------------------------------
|
||||
<span class="nv-badge nv-badge--success">In stock</span>
|
||||
<span class="nv-badge nv-badge--danger nv-badge--solid">3 failing</span>
|
||||
<span class="nv-badge"><span class="nv-badge__dot"></span>Draft</span>
|
||||
|
||||
A small, non-interactive statement of state. Subtle by default, because a
|
||||
badge is almost always beside something more important than itself; --solid
|
||||
is for the one badge in a view that must be seen before anything else.
|
||||
|
||||
If it can be clicked it is not a badge — that is a button, probably
|
||||
.nv-button--pill --sm.
|
||||
========================================================================== */
|
||||
|
||||
.nv-badge {
|
||||
--_bg: var(--nv-color-surface-inset);
|
||||
--_fg: var(--nv-color-text-secondary);
|
||||
--_border: var(--nv-color-border);
|
||||
--_solid-bg: var(--nv-color-text-secondary);
|
||||
--_solid-fg: var(--nv-color-surface);
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--nv-space-1-5);
|
||||
|
||||
block-size: var(--nv-space-5);
|
||||
padding-inline: var(--nv-space-2);
|
||||
|
||||
background-color: var(--_bg);
|
||||
color: var(--_fg);
|
||||
border: var(--nv-border-width) solid var(--_border);
|
||||
border-radius: var(--nv-radius-full);
|
||||
|
||||
font: var(--nv-font-caption);
|
||||
font-weight: var(--nv-font-weight-semibold);
|
||||
white-space: nowrap;
|
||||
transition: var(--nv-transition-colors);
|
||||
}
|
||||
|
||||
.nv-badge--accent {
|
||||
--_bg: var(--nv-color-accent-subtle);
|
||||
--_fg: var(--nv-color-accent-text);
|
||||
--_border: var(--nv-color-accent-border);
|
||||
--_solid-bg: var(--nv-color-accent);
|
||||
--_solid-fg: var(--nv-color-on-accent);
|
||||
}
|
||||
|
||||
.nv-badge--success {
|
||||
--_bg: var(--nv-color-success-subtle);
|
||||
--_fg: var(--nv-color-success-text);
|
||||
--_border: var(--nv-color-success-border);
|
||||
--_solid-bg: var(--nv-color-success);
|
||||
--_solid-fg: var(--nv-color-on-success);
|
||||
}
|
||||
|
||||
.nv-badge--warning {
|
||||
--_bg: var(--nv-color-warning-subtle);
|
||||
--_fg: var(--nv-color-warning-text);
|
||||
--_border: var(--nv-color-warning-border);
|
||||
--_solid-bg: var(--nv-color-warning);
|
||||
--_solid-fg: var(--nv-color-on-warning);
|
||||
}
|
||||
|
||||
.nv-badge--danger {
|
||||
--_bg: var(--nv-color-danger-subtle);
|
||||
--_fg: var(--nv-color-danger-text);
|
||||
--_border: var(--nv-color-danger-border);
|
||||
--_solid-bg: var(--nv-color-danger);
|
||||
--_solid-fg: var(--nv-color-on-danger);
|
||||
}
|
||||
|
||||
/* Declared after the colors so it wins for --_bg; the color modifier is still
|
||||
what decides which solid pair it reads. */
|
||||
.nv-badge--solid {
|
||||
--_bg: var(--_solid-bg);
|
||||
--_fg: var(--_solid-fg);
|
||||
--_border: transparent;
|
||||
background-image: var(--nv-sheen-filled);
|
||||
box-shadow: var(--nv-highlight-top);
|
||||
}
|
||||
|
||||
.nv-badge--square {
|
||||
border-radius: var(--nv-radius-xs);
|
||||
}
|
||||
|
||||
/* Counts read better without a border, and with room for two digits. */
|
||||
.nv-badge--count {
|
||||
min-inline-size: var(--nv-space-5);
|
||||
padding-inline: var(--nv-space-1-5);
|
||||
justify-content: center;
|
||||
border-color: transparent;
|
||||
font-size: var(--nv-font-size-2xs);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.nv-badge__dot {
|
||||
inline-size: var(--nv-space-1-5);
|
||||
block-size: var(--nv-space-1-5);
|
||||
border-radius: var(--nv-radius-full);
|
||||
background-color: currentColor;
|
||||
flex: none;
|
||||
}
|
||||
101
niveus/components/bed.css
Normal file
101
niveus/components/bed.css
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Bed
|
||||
--------------------------------------------------------------------------
|
||||
<div class="nv-bed"> … </div>
|
||||
|
||||
A bed is ground that is visibly *not* ordinary background. Where a card says
|
||||
"this content belongs together", a bed says "what sits here is different in
|
||||
kind" — a feature being shown off, a preview, a sandbox, an empty state, a
|
||||
region under explanation.
|
||||
|
||||
It reads that way because of what it does to the materials on top of it: the
|
||||
hatch and the soft color blooms give translucent surfaces something to be
|
||||
translucent *against*, and give shadows something to fall on. A frosted panel
|
||||
over a flat grey proves nothing; over a bed it is obvious.
|
||||
|
||||
That is also the rule for using one. A bed is for regions the user should
|
||||
look *at*. Put content they need to work *through* on a plain surface —
|
||||
patterning behind a form or a table is noise, and the calm is the point.
|
||||
|
||||
Everything it is made of derives from the accent, so it follows the brand.
|
||||
Set --nv-bed-tint to lead with a different color.
|
||||
========================================================================== */
|
||||
|
||||
.nv-bed {
|
||||
--nv-bed-tint: var(--nv-color-accent);
|
||||
--_bloom: 16%;
|
||||
--_hatch: var(--nv-color-border-subtle);
|
||||
|
||||
position: relative;
|
||||
padding: var(--nv-space-6);
|
||||
|
||||
background-color: var(--nv-color-background-subtle);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
border-radius: var(--nv-radius-xl);
|
||||
|
||||
/* The hatch sits over the blooms, and both sit under the content: three
|
||||
background layers rather than a pseudo-element, so nothing has to be
|
||||
lifted out of the way. */
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
45deg,
|
||||
var(--_hatch) 0 2px,
|
||||
transparent 2px 14px
|
||||
),
|
||||
radial-gradient(
|
||||
60% 80% at 15% 20%,
|
||||
color-mix(in oklab, var(--nv-bed-tint) var(--_bloom), transparent),
|
||||
transparent 70%
|
||||
),
|
||||
radial-gradient(
|
||||
50% 70% at 85% 25%,
|
||||
color-mix(in oklab, var(--nv-color-danger) calc(var(--_bloom) * 0.7), transparent),
|
||||
transparent 70%
|
||||
),
|
||||
radial-gradient(
|
||||
70% 90% at 50% 100%,
|
||||
color-mix(in oklab, var(--nv-color-success) calc(var(--_bloom) * 0.7), transparent),
|
||||
transparent 70%
|
||||
);
|
||||
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
transition: var(--nv-transition-surface);
|
||||
}
|
||||
|
||||
/* Just the ground, no weather: for grouping a region without drawing the eye
|
||||
to it. */
|
||||
.nv-bed--quiet {
|
||||
--_bloom: 8%;
|
||||
--_hatch: transparent;
|
||||
}
|
||||
|
||||
.nv-bed--vivid {
|
||||
--_bloom: 26%;
|
||||
}
|
||||
|
||||
/* One color instead of three — a bed that belongs to a single state. */
|
||||
.nv-bed--single {
|
||||
background-image:
|
||||
repeating-linear-gradient(45deg, var(--_hatch) 0 2px, transparent 2px 14px),
|
||||
radial-gradient(
|
||||
80% 100% at 30% 0%,
|
||||
color-mix(in oklab, var(--nv-bed-tint) var(--_bloom), transparent),
|
||||
transparent 75%
|
||||
);
|
||||
}
|
||||
|
||||
.nv-bed--flush {
|
||||
border-radius: 0;
|
||||
border-inline: 0;
|
||||
}
|
||||
|
||||
.nv-bed__label {
|
||||
display: block;
|
||||
margin-block-end: var(--nv-space-4);
|
||||
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);
|
||||
}
|
||||
296
niveus/components/button.css
Normal file
296
niveus/components/button.css
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Button
|
||||
--------------------------------------------------------------------------
|
||||
.nv-button secondary by default — a raised surface
|
||||
.nv-button--primary the single affirmative action in a view
|
||||
.nv-button--ghost low-emphasis, no material until hovered
|
||||
.nv-button--danger destructive, and only when it is
|
||||
.nv-button--frosted for use over imagery or frosted chrome
|
||||
.nv-button--sm | --lg 36px by default
|
||||
.nv-button--icon square, for a lone icon (needs a label)
|
||||
.nv-button--block fills its container
|
||||
|
||||
A button is a key: a bevelled face with light falling across it, a contact
|
||||
line where it meets the surface, and a press that sinks it into an inset.
|
||||
|
||||
Every variant is defined purely by reassigning the private --_* properties
|
||||
below. Interaction — the lift on hover, the sink on press, the focus ring —
|
||||
lives once in the base rule, so a new variant can never drift from the way
|
||||
the rest of the system responds to a pointer.
|
||||
========================================================================== */
|
||||
|
||||
.nv-button {
|
||||
/* Variant surface, overridden per modifier. */
|
||||
--_bg: var(--nv-color-surface-elevated);
|
||||
--_bg-hover: var(--nv-color-surface-hover);
|
||||
--_bg-active: var(--nv-color-surface-active);
|
||||
--_fg: var(--nv-color-text-primary);
|
||||
--_border: var(--nv-color-border);
|
||||
--_sheen: var(--nv-sheen-raised);
|
||||
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2);
|
||||
--_shadow-hover: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3);
|
||||
--_shadow-active: var(--nv-elevation-inset-sm), var(--nv-outline);
|
||||
|
||||
/* Size, overridden per size modifier. */
|
||||
--_height: var(--nv-control-height-md);
|
||||
--_padding: var(--nv-control-padding-md);
|
||||
--_gap: var(--nv-control-gap-md);
|
||||
--_font-size: var(--nv-font-size-sm);
|
||||
--_radius: var(--nv-radius-md);
|
||||
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--_gap);
|
||||
|
||||
block-size: var(--_height);
|
||||
padding-inline: var(--_padding);
|
||||
border: var(--nv-border-width) solid var(--_border);
|
||||
border-radius: var(--_radius);
|
||||
|
||||
background-color: var(--_bg);
|
||||
background-image: var(--_sheen);
|
||||
color: var(--_fg);
|
||||
box-shadow: var(--_shadow);
|
||||
|
||||
font-family: var(--nv-font-family-sans);
|
||||
font-size: var(--_font-size);
|
||||
font-weight: var(--nv-font-weight-medium);
|
||||
line-height: var(--nv-line-height-snug);
|
||||
letter-spacing: var(--nv-letter-spacing-normal);
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: var(--nv-transition-control);
|
||||
}
|
||||
|
||||
/* A control smaller than the minimum target still gets a full-size hit area:
|
||||
the pseudo-element grows outward without changing the visual size. */
|
||||
.nv-button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-block-start: 50%;
|
||||
inset-inline-start: 50%;
|
||||
translate: -50% -50%;
|
||||
inline-size: max(100%, var(--nv-target-min));
|
||||
block-size: max(100%, var(--nv-target-min));
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.nv-button:hover:not(:disabled, [aria-disabled="true"]) {
|
||||
background-color: var(--_bg-hover);
|
||||
box-shadow: var(--_shadow-hover);
|
||||
translate: 0 var(--nv-lift-hover);
|
||||
}
|
||||
}
|
||||
|
||||
/* Pressed, the face is pushed below the light: the sheen goes and the inset
|
||||
takes over. */
|
||||
.nv-button:active:not(:disabled, [aria-disabled="true"]) {
|
||||
background-color: var(--_bg-active);
|
||||
background-image: none;
|
||||
box-shadow: var(--_shadow-active);
|
||||
translate: 0 var(--nv-press-depth);
|
||||
transition-duration: var(--nv-duration-instant);
|
||||
}
|
||||
|
||||
.nv-button:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--_shadow), var(--nv-focus-ring);
|
||||
}
|
||||
|
||||
.nv-button:disabled,
|
||||
.nv-button[aria-disabled="true"] {
|
||||
background-color: var(--nv-color-surface-inset);
|
||||
background-image: none;
|
||||
color: var(--nv-color-text-disabled);
|
||||
border-color: var(--nv-color-border-subtle);
|
||||
box-shadow: none;
|
||||
translate: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* --- Variants --------------------------------------------------------------- */
|
||||
|
||||
.nv-button--primary {
|
||||
--_bg: var(--nv-color-accent);
|
||||
--_bg-hover: var(--nv-color-accent-hover);
|
||||
--_bg-active: var(--nv-color-accent-active);
|
||||
--_fg: var(--nv-color-on-accent);
|
||||
--_border: transparent;
|
||||
--_sheen: var(--nv-sheen-filled);
|
||||
font-weight: var(--nv-font-weight-semibold);
|
||||
}
|
||||
|
||||
.nv-button--danger {
|
||||
--_bg: var(--nv-color-danger);
|
||||
--_bg-hover: var(--nv-color-danger-hover);
|
||||
--_bg-active: var(--nv-color-danger-active);
|
||||
--_fg: var(--nv-color-on-danger);
|
||||
--_border: transparent;
|
||||
--_sheen: var(--nv-sheen-filled);
|
||||
--nv-color-focus-ring: var(--nv-color-danger-border);
|
||||
font-weight: var(--nv-font-weight-semibold);
|
||||
}
|
||||
|
||||
.nv-button--ghost {
|
||||
--_bg: transparent;
|
||||
--_bg-hover: var(--nv-color-surface-hover);
|
||||
--_bg-active: var(--nv-color-surface-active);
|
||||
--_fg: var(--nv-color-text-secondary);
|
||||
--_border: transparent;
|
||||
--_sheen: none;
|
||||
--_shadow: none;
|
||||
--_shadow-hover: none;
|
||||
--_shadow-active: var(--nv-elevation-inset-sm);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.nv-button--ghost:hover:not(:disabled, [aria-disabled="true"]) {
|
||||
color: var(--nv-color-text-primary);
|
||||
translate: none; /* nothing to lift — a ghost has no material to raise */
|
||||
}
|
||||
}
|
||||
|
||||
.nv-button--subtle {
|
||||
--_bg: var(--nv-color-accent-subtle);
|
||||
--_bg-hover: var(--nv-color-accent-subtle-hover);
|
||||
--_bg-active: var(--nv-color-accent-subtle-hover);
|
||||
--_fg: var(--nv-color-accent-text);
|
||||
--_border: var(--nv-color-accent-border);
|
||||
--_sheen: none;
|
||||
--_shadow: none;
|
||||
--_shadow-hover: var(--nv-elevation-1);
|
||||
}
|
||||
|
||||
.nv-button--frosted {
|
||||
--_bg: var(--nv-material-glass-bg);
|
||||
--_bg-hover: var(--nv-material-frosted-bg);
|
||||
--_bg-active: var(--nv-material-frosted-bg);
|
||||
--_border: var(--nv-material-glass-border);
|
||||
--_sheen: none;
|
||||
backdrop-filter: var(--nv-backdrop-glass);
|
||||
-webkit-backdrop-filter: var(--nv-backdrop-glass);
|
||||
}
|
||||
|
||||
/* --- Sizes ------------------------------------------------------------------- */
|
||||
|
||||
.nv-button--sm {
|
||||
--_height: var(--nv-control-height-sm);
|
||||
--_padding: var(--nv-control-padding-sm);
|
||||
--_gap: var(--nv-control-gap-sm);
|
||||
--_font-size: var(--nv-font-size-xs);
|
||||
--_radius: var(--nv-radius-sm);
|
||||
}
|
||||
|
||||
.nv-button--lg {
|
||||
--_height: var(--nv-control-height-lg);
|
||||
--_padding: var(--nv-control-padding-lg);
|
||||
--_gap: var(--nv-control-gap-lg);
|
||||
--_font-size: var(--nv-font-size-md);
|
||||
}
|
||||
|
||||
.nv-button--icon {
|
||||
--_padding: 0;
|
||||
inline-size: var(--_height);
|
||||
}
|
||||
|
||||
.nv-button--pill {
|
||||
--_radius: var(--nv-radius-full);
|
||||
}
|
||||
|
||||
.nv-button--block {
|
||||
display: flex;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
/* --- Floating ------------------------------------------------------------------
|
||||
A control that sits *over* content rather than in the flow: a carousel arrow,
|
||||
a floating action. Two things change.
|
||||
|
||||
It is already off the surface, so there is nothing to lift it from — it grows
|
||||
instead. And it is almost always positioned with `translate`, which the hover
|
||||
lift would overwrite, throwing the button half its own height down the page
|
||||
the moment a pointer touched it. --nv-button-offset carries that positioning
|
||||
through every state. */
|
||||
|
||||
.nv-button--float {
|
||||
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3);
|
||||
--_shadow-hover: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-4);
|
||||
--_shadow-active: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2);
|
||||
|
||||
translate: var(--nv-button-offset, 0 0);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.nv-button--float:hover:not(:disabled, [aria-disabled="true"]) {
|
||||
translate: var(--nv-button-offset, 0 0);
|
||||
scale: 1.06;
|
||||
}
|
||||
}
|
||||
|
||||
.nv-button--float:active:not(:disabled, [aria-disabled="true"]) {
|
||||
translate: var(--nv-button-offset, 0 0);
|
||||
scale: 0.97;
|
||||
}
|
||||
|
||||
/* --- Parts --------------------------------------------------------------------- */
|
||||
|
||||
.nv-button__icon {
|
||||
inline-size: 1em;
|
||||
block-size: 1em;
|
||||
font-size: 1.15em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Busy: the label stays in place to keep the button from resizing mid-action,
|
||||
and the spinner replaces it. */
|
||||
.nv-button[aria-busy="true"] {
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nv-button[aria-busy="true"]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inline-size: 1em;
|
||||
block-size: 1em;
|
||||
border: 2px solid var(--_fg);
|
||||
border-block-start-color: transparent;
|
||||
border-radius: var(--nv-radius-full);
|
||||
animation: nv-spin var(--nv-duration-slower) var(--nv-ease-linear) infinite;
|
||||
}
|
||||
|
||||
@keyframes nv-spin {
|
||||
to { rotate: 1turn; }
|
||||
}
|
||||
|
||||
/* --- Group ---------------------------------------------------------------------
|
||||
Segmented controls: buttons share an edge, so only the outer corners round
|
||||
and the shared border is not drawn twice. */
|
||||
|
||||
.nv-button-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nv-button-group > .nv-button:not(:first-child) {
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
margin-inline-start: calc(var(--nv-border-width) * -1);
|
||||
}
|
||||
|
||||
.nv-button-group > .nv-button:not(:last-child) {
|
||||
border-start-end-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
.nv-button-group > .nv-button:hover,
|
||||
.nv-button-group > .nv-button:focus-visible {
|
||||
z-index: 1;
|
||||
translate: none;
|
||||
}
|
||||
175
niveus/components/card.css
Normal file
175
niveus/components/card.css
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Card
|
||||
--------------------------------------------------------------------------
|
||||
.nv-card resting surface, elevation 1
|
||||
.nv-card--elevated separated from its context, elevation 2
|
||||
.nv-card--inset recessed into its parent
|
||||
.nv-card--frosted over imagery or scrolling content
|
||||
.nv-card--interactive the whole card is one target
|
||||
.nv-card__header / __body / __footer / __media
|
||||
|
||||
A card is a container, not a look. If a card needs a shadow that is not on
|
||||
the elevation scale, the answer is a different elevation — not a different
|
||||
shadow.
|
||||
========================================================================== */
|
||||
|
||||
.nv-card {
|
||||
--_bg: var(--nv-color-surface);
|
||||
--_border: var(--nv-color-border-subtle);
|
||||
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
|
||||
--_sheen: var(--nv-sheen-surface);
|
||||
--_padding: var(--nv-space-5);
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-inline-size: 0;
|
||||
|
||||
background-color: var(--_bg);
|
||||
background-image: var(--_sheen);
|
||||
color: var(--nv-color-text-primary);
|
||||
border: var(--nv-border-width) solid var(--_border);
|
||||
border-radius: var(--nv-radius-lg);
|
||||
box-shadow: var(--_shadow);
|
||||
|
||||
/* Children are clipped to the corner radius so media and inset regions
|
||||
cannot square off the card. */
|
||||
overflow: hidden;
|
||||
transition: var(--nv-transition-surface);
|
||||
}
|
||||
|
||||
.nv-card--elevated {
|
||||
--_bg: var(--nv-color-surface-elevated);
|
||||
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2);
|
||||
}
|
||||
|
||||
.nv-card--floating {
|
||||
--_bg: var(--nv-color-surface-elevated);
|
||||
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3);
|
||||
}
|
||||
|
||||
.nv-card--inset {
|
||||
--_bg: var(--nv-color-surface-inset);
|
||||
--_shadow: var(--nv-elevation-inset-md), var(--nv-ledge);
|
||||
--_sheen: none;
|
||||
--_border: var(--nv-color-border-subtle);
|
||||
}
|
||||
|
||||
.nv-card--frosted {
|
||||
--_bg: var(--nv-material-frosted-bg);
|
||||
--_border: var(--nv-material-frosted-border);
|
||||
--_shadow: var(--nv-material-frosted-shadow);
|
||||
--_sheen: none;
|
||||
backdrop-filter: var(--nv-backdrop-frosted);
|
||||
-webkit-backdrop-filter: var(--nv-backdrop-frosted);
|
||||
}
|
||||
|
||||
.nv-card--flush {
|
||||
--_padding: 0;
|
||||
}
|
||||
|
||||
/* --- Interactive ------------------------------------------------------------
|
||||
Rises on hover and sinks on press, exactly like a button — one physical
|
||||
language for anything that responds to a pointer. */
|
||||
|
||||
.nv-card--interactive {
|
||||
cursor: pointer;
|
||||
text-align: start;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.nv-card--interactive:hover {
|
||||
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3);
|
||||
translate: 0 calc(var(--nv-lift-hover) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
.nv-card--interactive:active {
|
||||
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
|
||||
translate: 0 var(--nv-press-depth);
|
||||
transition-duration: var(--nv-duration-instant);
|
||||
}
|
||||
|
||||
.nv-card--interactive:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--_shadow), var(--nv-focus-ring);
|
||||
}
|
||||
|
||||
/* Cards whose whole area is clickable via a link inside: the link's target is
|
||||
stretched over the card, so the markup stays semantic and only the link text
|
||||
is announced. */
|
||||
.nv-card__link::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
/* --- Parts ------------------------------------------------------------------- */
|
||||
|
||||
.nv-card__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--nv-space-4);
|
||||
padding: var(--_padding);
|
||||
padding-block-end: var(--nv-space-3);
|
||||
}
|
||||
|
||||
.nv-card__title {
|
||||
font: var(--nv-font-title);
|
||||
letter-spacing: var(--nv-letter-spacing-tight);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nv-card__subtitle {
|
||||
font: var(--nv-font-caption);
|
||||
color: var(--nv-color-text-secondary);
|
||||
margin-block-start: var(--nv-space-1);
|
||||
}
|
||||
|
||||
.nv-card__body {
|
||||
flex: 1;
|
||||
padding: var(--_padding);
|
||||
color: var(--nv-color-text-secondary);
|
||||
font: var(--nv-font-body);
|
||||
}
|
||||
|
||||
/* A header already opened the block, so the body should not open it again. */
|
||||
.nv-card__header + .nv-card__body {
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
.nv-card__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--nv-space-2);
|
||||
padding: var(--_padding);
|
||||
padding-block-start: var(--nv-space-3);
|
||||
border-block-start: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
margin-block-start: auto;
|
||||
}
|
||||
|
||||
.nv-card__footer--plain {
|
||||
border-block-start: 0;
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
.nv-card__media {
|
||||
display: block;
|
||||
inline-size: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
object-fit: cover;
|
||||
background-color: var(--nv-color-surface-inset);
|
||||
}
|
||||
|
||||
/* --- Group ---------------------------------------------------------------------
|
||||
Equal-height cards on one rhythm, collapsing to a single column without a
|
||||
media query. */
|
||||
|
||||
.nv-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
|
||||
gap: var(--nv-space-4);
|
||||
align-items: stretch;
|
||||
}
|
||||
191
niveus/components/dialog.css
Normal file
191
niveus/components/dialog.css
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Dialog
|
||||
--------------------------------------------------------------------------
|
||||
Built on the native <dialog> element, so focus trapping, the top layer,
|
||||
Escape-to-close and inertness come from the platform rather than from
|
||||
script.
|
||||
|
||||
<dialog class="nv-dialog"> … </dialog> opened with .showModal()
|
||||
.nv-dialog--sheet enters from the block end
|
||||
.nv-dialog--wide | --compact width
|
||||
.nv-dialog__header / __body / __footer / __close
|
||||
|
||||
The dialog is the system's highest material: elevation 4 over a blurred
|
||||
scrim. It enters by rising and settling, and leaves the way it came — the
|
||||
motion is what tells the user the surface was always there, above the page.
|
||||
========================================================================== */
|
||||
|
||||
.nv-dialog {
|
||||
--_width: 30rem;
|
||||
--_padding: var(--nv-space-6);
|
||||
|
||||
inline-size: min(var(--_width), calc(100vw - var(--nv-space-8)));
|
||||
max-block-size: min(42rem, calc(100svh - var(--nv-space-12)));
|
||||
padding: 0;
|
||||
|
||||
background-color: var(--nv-color-surface-elevated);
|
||||
background-image: var(--nv-sheen-surface);
|
||||
color: var(--nv-color-text-primary);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
border-radius: var(--nv-radius-xl);
|
||||
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-4);
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
/* Closed state. [open] below is the resting state; @starting-style replays
|
||||
these values for the entrance. */
|
||||
/* A dialog should look as though it came from the control that opened it.
|
||||
nv.openDialog() sets --nv-origin to that control's position relative to
|
||||
the dialog, so the entrance scale expands away from the trigger rather
|
||||
than from the middle of the screen. Centred when nothing sets it. */
|
||||
transform-origin: var(--nv-origin, 50% 50%);
|
||||
|
||||
opacity: 0;
|
||||
scale: 0.97;
|
||||
translate: 0 var(--nv-travel-md);
|
||||
|
||||
transition:
|
||||
opacity var(--nv-duration-normal) var(--nv-ease-standard),
|
||||
scale var(--nv-duration-normal) var(--nv-ease-standard),
|
||||
translate var(--nv-duration-normal) var(--nv-ease-standard),
|
||||
display var(--nv-duration-normal) allow-discrete,
|
||||
overlay var(--nv-duration-normal) allow-discrete;
|
||||
}
|
||||
|
||||
.nv-dialog[open] {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
translate: 0;
|
||||
transition-timing-function: var(--nv-ease-entrance);
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
.nv-dialog[open] {
|
||||
opacity: 0;
|
||||
scale: 0.97;
|
||||
translate: 0 var(--nv-travel-md);
|
||||
}
|
||||
}
|
||||
|
||||
.nv-dialog::backdrop {
|
||||
background-color: var(--nv-color-scrim);
|
||||
backdrop-filter: var(--nv-backdrop-scrim);
|
||||
-webkit-backdrop-filter: var(--nv-backdrop-scrim);
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity var(--nv-duration-normal) var(--nv-ease-standard),
|
||||
display var(--nv-duration-normal) allow-discrete,
|
||||
overlay var(--nv-duration-normal) allow-discrete;
|
||||
}
|
||||
|
||||
.nv-dialog[open]::backdrop {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
.nv-dialog[open]::backdrop {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* showModal() does not stop the page behind from scrolling. */
|
||||
html:has(.nv-dialog:modal) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* --- Widths -------------------------------------------------------------------- */
|
||||
|
||||
.nv-dialog--compact { --_width: 24rem; }
|
||||
.nv-dialog--wide { --_width: 44rem; }
|
||||
|
||||
/* --- Sheet ----------------------------------------------------------------------
|
||||
Anchored to the block end and full width. On small screens this is usually
|
||||
the right form for anything with more than two actions. */
|
||||
|
||||
.nv-dialog--sheet {
|
||||
--_width: 34rem;
|
||||
|
||||
/* Flush to the viewport edges — a sheet with side gaps and square bottom
|
||||
corners reads as a mistake rather than as a sheet. */
|
||||
inline-size: min(var(--_width), 100vw);
|
||||
max-block-size: min(46rem, calc(100svh - var(--nv-space-10)));
|
||||
margin-block-end: 0;
|
||||
margin-block-start: auto;
|
||||
border-start-start-radius: var(--nv-radius-xl);
|
||||
border-start-end-radius: var(--nv-radius-xl);
|
||||
border-end-start-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
scale: 1;
|
||||
translate: 0 var(--nv-travel-lg);
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
.nv-dialog--sheet[open] {
|
||||
scale: 1;
|
||||
translate: 0 var(--nv-travel-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Parts ------------------------------------------------------------------------ */
|
||||
|
||||
.nv-dialog__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--nv-space-4);
|
||||
padding: var(--_padding);
|
||||
padding-block-end: var(--nv-space-3);
|
||||
}
|
||||
|
||||
.nv-dialog__title {
|
||||
font: var(--nv-font-heading);
|
||||
font-size: var(--nv-font-size-xl);
|
||||
letter-spacing: var(--nv-letter-spacing-tight);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nv-dialog__description {
|
||||
font: var(--nv-font-body);
|
||||
color: var(--nv-color-text-secondary);
|
||||
margin-block-start: var(--nv-space-2);
|
||||
}
|
||||
|
||||
.nv-dialog__body {
|
||||
padding: var(--_padding);
|
||||
padding-block: var(--nv-space-2);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
color: var(--nv-color-text-secondary);
|
||||
}
|
||||
|
||||
.nv-dialog__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--nv-space-2);
|
||||
padding: var(--_padding);
|
||||
padding-block-start: var(--nv-space-4);
|
||||
}
|
||||
|
||||
/* A footer over scrollable content needs its own ground: the inset tint and
|
||||
hairline tell the user there is more above it. */
|
||||
.nv-dialog__footer--divided {
|
||||
background-color: var(--nv-color-background-subtle);
|
||||
border-block-start: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
}
|
||||
|
||||
.nv-dialog__close {
|
||||
flex: none;
|
||||
margin: calc(var(--nv-space-1) * -1);
|
||||
}
|
||||
|
||||
/* A dialog laid out as a column keeps its header and footer fixed while only
|
||||
the body scrolls. */
|
||||
.nv-dialog:has(.nv-dialog__body) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nv-dialog:not([open]) {
|
||||
display: none;
|
||||
}
|
||||
131
niveus/components/disclosure.css
Normal file
131
niveus/components/disclosure.css
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Disclosure
|
||||
--------------------------------------------------------------------------
|
||||
<details class="nv-disclosure">
|
||||
<summary class="nv-disclosure__summary"> … </summary>
|
||||
<div class="nv-disclosure__body"> … </div>
|
||||
</details>
|
||||
|
||||
Content expands out of the row that revealed it, at the position it already
|
||||
occupies, rather than appearing at full height. The height animates because
|
||||
the continuity layer turns on `interpolate-size`, which is what lets
|
||||
block-size: auto take part in a transition — no measurement, no script, and
|
||||
no wrapper whose height has to be kept in sync.
|
||||
|
||||
.nv-collapse in continuity.css covers the same relationship for markup that
|
||||
cannot be a <details>.
|
||||
========================================================================== */
|
||||
|
||||
.nv-disclosure {
|
||||
--_padding: var(--nv-space-4);
|
||||
|
||||
background-color: var(--nv-color-surface);
|
||||
background-image: var(--nv-sheen-surface);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
border-radius: var(--nv-radius-lg);
|
||||
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
|
||||
transition: var(--nv-transition-surface);
|
||||
}
|
||||
|
||||
.nv-disclosure--plain {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.nv-disclosure__summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--nv-space-3);
|
||||
padding: var(--nv-space-3) var(--_padding);
|
||||
|
||||
font: var(--nv-font-label);
|
||||
color: var(--nv-color-text-primary);
|
||||
border-radius: inherit;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
list-style: none;
|
||||
transition: var(--nv-transition-colors);
|
||||
}
|
||||
|
||||
/* Both spellings are needed to suppress the native marker. */
|
||||
.nv-disclosure__summary::marker,
|
||||
.nv-disclosure__summary::-webkit-details-marker {
|
||||
display: none;
|
||||
content: "";
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.nv-disclosure__summary:hover {
|
||||
background-color: var(--nv-color-surface-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.nv-disclosure__summary:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--nv-focus-ring);
|
||||
}
|
||||
|
||||
/* The chevron turns rather than swapping to a different glyph — the rotation
|
||||
is what says "this is the same control in its other state". */
|
||||
.nv-disclosure__chevron {
|
||||
inline-size: 0.5em;
|
||||
block-size: 0.5em;
|
||||
margin-inline-start: auto;
|
||||
margin-block-start: -0.2em;
|
||||
flex: none;
|
||||
|
||||
border-inline-end: 1.5px solid var(--nv-color-text-tertiary);
|
||||
border-block-end: 1.5px solid var(--nv-color-text-tertiary);
|
||||
rotate: 45deg;
|
||||
transition: rotate var(--nv-duration-normal) var(--nv-ease-standard);
|
||||
}
|
||||
|
||||
.nv-disclosure[open] .nv-disclosure__chevron {
|
||||
rotate: 225deg;
|
||||
margin-block-start: 0.15em;
|
||||
}
|
||||
|
||||
.nv-disclosure__body {
|
||||
padding: 0 var(--_padding) var(--_padding);
|
||||
color: var(--nv-color-text-secondary);
|
||||
font: var(--nv-font-body);
|
||||
}
|
||||
|
||||
/* The element the browser creates for a <details> body. Transitioning
|
||||
content-visibility as a discrete property is what keeps the content
|
||||
rendered while it collapses, instead of vanishing on the first frame. */
|
||||
.nv-disclosure::details-content {
|
||||
block-size: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition:
|
||||
block-size var(--nv-duration-expand) var(--nv-ease-standard),
|
||||
opacity var(--nv-duration-expand) var(--nv-ease-standard),
|
||||
content-visibility var(--nv-duration-expand) allow-discrete;
|
||||
}
|
||||
|
||||
.nv-disclosure[open]::details-content {
|
||||
block-size: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Stacked disclosures read as one control surface, so only the outer corners
|
||||
round and neighbours share a single hairline. */
|
||||
.nv-disclosure-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* The negative margin collapses each pair of adjacent borders into a single
|
||||
hairline, so neighbours are separated once rather than twice. */
|
||||
.nv-disclosure-group > .nv-disclosure:not(:first-child) {
|
||||
border-start-start-radius: 0;
|
||||
border-start-end-radius: 0;
|
||||
margin-block-start: calc(var(--nv-border-width) * -1);
|
||||
}
|
||||
|
||||
.nv-disclosure-group > .nv-disclosure:not(:last-child) {
|
||||
border-end-start-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
407
niveus/components/input.css
Normal file
407
niveus/components/input.css
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Input
|
||||
--------------------------------------------------------------------------
|
||||
.nv-field label + control + hint, correctly spaced
|
||||
.nv-input text input, textarea and select
|
||||
.nv-input-group control with a leading or trailing affix
|
||||
.nv-select wrapper that supplies the chevron
|
||||
.nv-checkbox / .nv-radio custom marks on the native controls
|
||||
.nv-switch binary control with an inset track
|
||||
|
||||
Inputs are the system's inset material: they read as wells cut into the
|
||||
surface that holds them — shaded inside, lit along the lower lip — which is
|
||||
what distinguishes a place to type from a thing to press. On focus the recess stays — only the ring and border change,
|
||||
so the field never appears to jump forward.
|
||||
========================================================================== */
|
||||
|
||||
.nv-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--nv-space-1-5);
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.nv-field__label {
|
||||
font: var(--nv-font-label);
|
||||
color: var(--nv-color-text-primary);
|
||||
}
|
||||
|
||||
.nv-field__label[data-optional]::after {
|
||||
content: " " attr(data-optional);
|
||||
color: var(--nv-color-text-tertiary);
|
||||
font-weight: var(--nv-font-weight-regular);
|
||||
}
|
||||
|
||||
.nv-field__hint {
|
||||
font: var(--nv-font-caption);
|
||||
color: var(--nv-color-text-secondary);
|
||||
}
|
||||
|
||||
.nv-field__error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--nv-space-1-5);
|
||||
font: var(--nv-font-caption);
|
||||
font-weight: var(--nv-font-weight-medium);
|
||||
color: var(--nv-color-danger-text);
|
||||
}
|
||||
|
||||
/* --- Control ----------------------------------------------------------------- */
|
||||
|
||||
.nv-input {
|
||||
--_height: var(--nv-control-height-md);
|
||||
--_padding: var(--nv-control-padding-md);
|
||||
--_font-size: var(--nv-font-size-md);
|
||||
--_radius: var(--nv-radius-md);
|
||||
--_border: var(--nv-color-border);
|
||||
|
||||
inline-size: 100%;
|
||||
min-inline-size: 0;
|
||||
block-size: var(--_height);
|
||||
padding-inline: var(--_padding);
|
||||
|
||||
background-color: var(--nv-color-surface-inset);
|
||||
color: var(--nv-color-text-primary);
|
||||
border: var(--nv-border-width) solid var(--_border);
|
||||
border-radius: var(--_radius);
|
||||
box-shadow: var(--nv-elevation-inset-md), var(--nv-ledge);
|
||||
|
||||
font-family: var(--nv-font-family-sans);
|
||||
font-size: var(--_font-size);
|
||||
line-height: var(--nv-line-height-snug);
|
||||
transition: var(--nv-transition-control);
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.nv-input::placeholder {
|
||||
color: var(--nv-color-text-tertiary);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.nv-input:hover:not(:disabled, [readonly]) {
|
||||
--_border: var(--nv-color-border-strong);
|
||||
}
|
||||
}
|
||||
|
||||
.nv-input:focus-visible,
|
||||
.nv-input:focus {
|
||||
outline: none;
|
||||
--_border: var(--nv-color-accent);
|
||||
box-shadow: var(--nv-elevation-inset-md), var(--nv-focus-ring);
|
||||
}
|
||||
|
||||
.nv-input[aria-invalid="true"] {
|
||||
--_border: var(--nv-color-danger);
|
||||
--nv-color-focus-ring: var(--nv-color-danger-border);
|
||||
}
|
||||
|
||||
.nv-input:disabled {
|
||||
background-color: var(--nv-color-background-subtle);
|
||||
color: var(--nv-color-text-disabled);
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.nv-input[readonly] {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Multi-line controls grow, so height becomes a minimum and the block padding
|
||||
has to be stated explicitly. */
|
||||
textarea.nv-input {
|
||||
block-size: auto;
|
||||
min-block-size: calc(var(--nv-control-height-lg) * 2);
|
||||
padding-block: var(--nv-space-2-5);
|
||||
line-height: var(--nv-line-height-normal);
|
||||
}
|
||||
|
||||
.nv-input--sm {
|
||||
--_height: var(--nv-control-height-sm);
|
||||
--_padding: var(--nv-control-padding-sm);
|
||||
--_font-size: var(--nv-font-size-sm);
|
||||
--_radius: var(--nv-radius-sm);
|
||||
}
|
||||
|
||||
/* A color well: the swatch fills the control, so the value is the control. */
|
||||
.nv-input--color {
|
||||
inline-size: var(--nv-control-height-md);
|
||||
padding: var(--nv-space-1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nv-input--color::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nv-input--color::-webkit-color-swatch,
|
||||
.nv-input--color::-moz-color-swatch {
|
||||
border: var(--nv-border-width) solid var(--nv-color-border);
|
||||
border-radius: calc(var(--nv-radius-md) - var(--nv-space-1));
|
||||
}
|
||||
|
||||
.nv-input--lg {
|
||||
--_height: var(--nv-control-height-lg);
|
||||
--_padding: var(--nv-control-padding-lg);
|
||||
--_font-size: var(--nv-font-size-lg);
|
||||
}
|
||||
|
||||
/* --- Affixes -------------------------------------------------------------------
|
||||
<div class="nv-input-group">
|
||||
<span class="nv-input-group__affix">@</span>
|
||||
<input class="nv-input">
|
||||
</div>
|
||||
The affix sits inside the field's recess rather than beside it, so the group
|
||||
still reads as a single surface. */
|
||||
|
||||
.nv-input-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.nv-input-group__affix {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-inline: var(--nv-control-padding-md);
|
||||
color: var(--nv-color-text-tertiary);
|
||||
font-size: var(--nv-font-size-sm);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nv-input-group__affix:first-child {
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
|
||||
.nv-input-group__affix:last-child {
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
.nv-input-group__affix:first-child ~ .nv-input {
|
||||
padding-inline-start: calc(var(--nv-control-padding-md) * 2 + 1em);
|
||||
}
|
||||
|
||||
.nv-input:has(~ .nv-input-group__affix:last-child) {
|
||||
padding-inline-end: calc(var(--nv-control-padding-md) * 2 + 1em);
|
||||
}
|
||||
|
||||
/* Interactive affixes — a clear button, a reveal toggle — opt back in. */
|
||||
.nv-input-group__affix > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* --- Select ----------------------------------------------------------------------
|
||||
<div class="nv-select"><select class="nv-input">…</select></div> */
|
||||
|
||||
.nv-select {
|
||||
position: relative;
|
||||
display: flex;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.nv-select > .nv-input {
|
||||
padding-inline-end: calc(var(--nv-control-padding-md) * 2 + 0.5em);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nv-select::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-inline-end: var(--nv-control-padding-md);
|
||||
inset-block-start: 50%;
|
||||
inline-size: 0.45em;
|
||||
block-size: 0.45em;
|
||||
margin-block-start: -0.28em;
|
||||
border-inline-end: 1.5px solid var(--nv-color-text-tertiary);
|
||||
border-block-end: 1.5px solid var(--nv-color-text-tertiary);
|
||||
rotate: 45deg;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* --- Checkbox and radio ------------------------------------------------------------ */
|
||||
|
||||
.nv-checkbox,
|
||||
.nv-radio {
|
||||
position: relative;
|
||||
flex: none;
|
||||
inline-size: var(--nv-space-5);
|
||||
block-size: var(--nv-space-5);
|
||||
margin: 0;
|
||||
background-color: var(--nv-color-surface-inset);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border-strong);
|
||||
border-radius: var(--nv-radius-xs);
|
||||
box-shadow: var(--nv-elevation-inset-sm), var(--nv-ledge);
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
transition: var(--nv-transition-control);
|
||||
}
|
||||
|
||||
.nv-radio {
|
||||
border-radius: var(--nv-radius-full);
|
||||
}
|
||||
|
||||
.nv-checkbox:checked,
|
||||
.nv-radio:checked,
|
||||
.nv-checkbox:indeterminate {
|
||||
background-color: var(--nv-color-accent);
|
||||
background-image: var(--nv-sheen-filled);
|
||||
border-color: transparent;
|
||||
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
|
||||
}
|
||||
|
||||
/* The marks are always present, at zero size and zero opacity, so checking a
|
||||
box grows the tick out of its centre instead of stamping it there. A mark
|
||||
that only exists in the :checked state has nothing to animate from. */
|
||||
|
||||
.nv-checkbox::after,
|
||||
.nv-radio::after,
|
||||
.nv-checkbox::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
scale: 0;
|
||||
transition:
|
||||
opacity var(--nv-duration-fast) var(--nv-ease-standard),
|
||||
scale var(--nv-duration-quick) var(--nv-ease-spring);
|
||||
}
|
||||
|
||||
/* The tick is drawn from two borders so it scales with the control and needs
|
||||
no asset. */
|
||||
.nv-checkbox::after {
|
||||
inset-block-start: 45%;
|
||||
inset-inline-start: 50%;
|
||||
inline-size: 0.28em;
|
||||
block-size: 0.55em;
|
||||
translate: -50% -50%;
|
||||
rotate: 45deg;
|
||||
border-inline-end: 2px solid var(--nv-color-on-accent);
|
||||
border-block-end: 2px solid var(--nv-color-on-accent);
|
||||
}
|
||||
|
||||
.nv-checkbox:checked:not(:indeterminate)::after {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
/* Indeterminate is its own mark rather than a variation of the tick: the two
|
||||
have to be able to cross over each other when the state changes. */
|
||||
.nv-checkbox::before {
|
||||
inset: 50% 25% auto;
|
||||
translate: 0 -50%;
|
||||
block-size: 2px;
|
||||
background-color: var(--nv-color-on-accent);
|
||||
border-radius: var(--nv-radius-full);
|
||||
}
|
||||
|
||||
.nv-checkbox:indeterminate::before {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
.nv-radio::after {
|
||||
inset: 50% auto auto 50%;
|
||||
translate: -50% -50%;
|
||||
inline-size: 0.4em;
|
||||
block-size: 0.4em;
|
||||
background-color: var(--nv-color-on-accent);
|
||||
border-radius: var(--nv-radius-full);
|
||||
}
|
||||
|
||||
.nv-radio:checked::after {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
|
||||
.nv-checkbox:disabled,
|
||||
.nv-radio:disabled {
|
||||
background-color: var(--nv-color-background-subtle);
|
||||
border-color: var(--nv-color-border);
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* --- Switch --------------------------------------------------------------------------
|
||||
The track is a slot cut into the surface and the thumb is a knob riding in
|
||||
it: the physical relationship does the explaining, so the control reads as
|
||||
on or off without a label. The slot stays recessed when it fills with color
|
||||
— switching on lights the slot, it does not raise it. */
|
||||
|
||||
.nv-switch {
|
||||
--_track-inline: calc(var(--nv-space-6) * 1.5);
|
||||
--_track-block: var(--nv-space-6);
|
||||
--_thumb: calc(var(--_track-block) - 6px);
|
||||
--_inset: 2px;
|
||||
--_travel: calc(
|
||||
var(--_track-inline) - var(--_thumb) - var(--_inset) * 2 -
|
||||
var(--nv-border-width) * 2
|
||||
);
|
||||
|
||||
position: relative;
|
||||
flex: none;
|
||||
inline-size: var(--_track-inline);
|
||||
block-size: var(--_track-block);
|
||||
margin: 0;
|
||||
background-color: var(--nv-color-surface-inset);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border);
|
||||
border-radius: var(--nv-radius-full);
|
||||
box-shadow: var(--nv-elevation-inset-md), var(--nv-ledge);
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
transition: var(--nv-transition-control);
|
||||
}
|
||||
|
||||
.nv-switch::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-block-start: 50%;
|
||||
inset-inline-start: var(--_inset);
|
||||
inline-size: var(--_thumb);
|
||||
block-size: var(--_thumb);
|
||||
translate: 0 -50%;
|
||||
background-color: var(--nv-color-knob);
|
||||
background-image: var(--nv-sheen-knob);
|
||||
border-radius: var(--nv-radius-full);
|
||||
box-shadow: var(--nv-outline), var(--nv-elevation-2);
|
||||
transition:
|
||||
translate var(--nv-duration-travel) var(--nv-ease-emphasized),
|
||||
background-color var(--nv-duration-fast) var(--nv-ease-standard);
|
||||
}
|
||||
|
||||
.nv-switch:checked {
|
||||
background-color: var(--nv-color-accent);
|
||||
border-color: transparent;
|
||||
box-shadow: var(--nv-elevation-inset-md), var(--nv-outline);
|
||||
}
|
||||
|
||||
.nv-switch:checked::after {
|
||||
translate: var(--_travel) -50%;
|
||||
}
|
||||
|
||||
.nv-switch:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* --- Inline control + label --------------------------------------------------------- */
|
||||
|
||||
.nv-choice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--nv-space-2-5);
|
||||
font: var(--nv-font-body);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nv-choice:has(:disabled) {
|
||||
color: var(--nv-color-text-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* translate has no logical equivalent, so the thumb's direction is flipped
|
||||
explicitly rather than travelling the wrong way in RTL. */
|
||||
.nv-switch:checked:dir(rtl)::after {
|
||||
translate: calc(var(--_travel) * -1) -50%;
|
||||
}
|
||||
175
niveus/components/menu.css
Normal file
175
niveus/components/menu.css
Normal 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);
|
||||
}
|
||||
358
niveus/components/navigation.css
Normal file
358
niveus/components/navigation.css
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
/* ==========================================================================
|
||||
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;
|
||||
gap: 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);
|
||||
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);
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: var(--nv-space-4) var(--nv-space-3);
|
||||
|
||||
background-color: var(--nv-color-background-subtle);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
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;
|
||||
gap: var(--nv-space-0-5);
|
||||
padding: var(--nv-space-1);
|
||||
|
||||
background-color: var(--nv-color-surface-inset);
|
||||
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);
|
||||
}
|
||||
131
niveus/components/scroller.css
Normal file
131
niveus/components/scroller.css
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Scroller
|
||||
--------------------------------------------------------------------------
|
||||
<div class="nv-scroller-frame" data-nv-scroller>
|
||||
<div class="nv-scroller"> …cards… </div>
|
||||
<button class="nv-button nv-button--icon" data-nv-scroll="prev">…</button>
|
||||
<button class="nv-button nv-button--icon" data-nv-scroll="next">…</button>
|
||||
</div>
|
||||
|
||||
A row of content that continues past its frame. Two things make that
|
||||
readable rather than merely clipped: the edges fade, so the row visibly runs
|
||||
under them instead of being cut, and snapping lands each item at the same
|
||||
place every time — so moving left and right returns you to positions you
|
||||
have already seen rather than to arbitrary offsets.
|
||||
|
||||
Scrolling works with no script at all: wheel, trackpad, touch and keyboard
|
||||
are the browser's. The buttons are an affordance for pointers that have no
|
||||
horizontal axis, and niveus.js wires them.
|
||||
========================================================================== */
|
||||
|
||||
.nv-scroller-frame {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nv-scroller {
|
||||
--nv-scroller-item: 18rem;
|
||||
--nv-scroller-fade: var(--nv-space-8);
|
||||
|
||||
display: flex;
|
||||
gap: var(--nv-space-4);
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-inline: contain;
|
||||
|
||||
/* Room for items to lift on hover without being clipped by the scroll box. */
|
||||
padding-block: var(--nv-space-2);
|
||||
|
||||
scroll-snap-type: inline mandatory;
|
||||
scroll-padding-inline: var(--nv-space-1);
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
|
||||
/* The row runs under its own edges rather than stopping at them — but only
|
||||
on the side where there is actually more to see. A fade at an end the row
|
||||
has already reached is a lie about the content. niveus.js marks the ends;
|
||||
with no script both sides fade, which is the safe default. */
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
black var(--nv-scroller-fade),
|
||||
black calc(100% - var(--nv-scroller-fade)),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.nv-scroller-frame[data-nv-at-start] .nv-scroller {
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
black calc(100% - var(--nv-scroller-fade)),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.nv-scroller-frame[data-nv-at-end] .nv-scroller {
|
||||
mask-image: linear-gradient(to right, transparent, black var(--nv-scroller-fade));
|
||||
}
|
||||
|
||||
.nv-scroller-frame[data-nv-at-start][data-nv-at-end] .nv-scroller {
|
||||
mask-image: none;
|
||||
}
|
||||
|
||||
.nv-scroller::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nv-scroller > * {
|
||||
flex: 0 0 auto;
|
||||
inline-size: min(var(--nv-scroller-item), 78%);
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
/* Items that should size to their content rather than to the track. */
|
||||
.nv-scroller--auto > * {
|
||||
inline-size: auto;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.nv-scroller {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Controls ------------------------------------------------------------
|
||||
Centred on the row's block axis and pulled half outside it, so they read as
|
||||
belonging to the frame rather than floating over the content. */
|
||||
|
||||
.nv-scroller-frame [data-nv-scroll] {
|
||||
position: absolute;
|
||||
inset-block-start: 50%;
|
||||
z-index: 1;
|
||||
|
||||
/* Read by .nv-button--float, which keeps it through hover and press. */
|
||||
--nv-button-offset: 0 -50%;
|
||||
}
|
||||
|
||||
/* Belt and braces: the frame knows these controls float, so their centring
|
||||
survives even if the markup forgets --float. */
|
||||
.nv-scroller-frame [data-nv-scroll],
|
||||
.nv-scroller-frame [data-nv-scroll]:hover,
|
||||
.nv-scroller-frame [data-nv-scroll]:active {
|
||||
translate: var(--nv-button-offset);
|
||||
}
|
||||
|
||||
.nv-scroller-frame [data-nv-scroll="prev"] {
|
||||
inset-inline-start: calc(var(--nv-space-4) * -1);
|
||||
}
|
||||
|
||||
.nv-scroller-frame [data-nv-scroll="next"] {
|
||||
inset-inline-end: calc(var(--nv-space-4) * -1);
|
||||
}
|
||||
|
||||
/* At either end the control has nothing left to do, and says so. */
|
||||
.nv-scroller-frame [data-nv-scroll]:disabled {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.nv-scroller-frame [data-nv-scroll] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
116
niveus/components/surface.css
Normal file
116
niveus/components/surface.css
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/* ==========================================================================
|
||||
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);
|
||||
}
|
||||
|
||||
/* --- 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;
|
||||
}
|
||||
95
niveus/components/table.css
Normal file
95
niveus/components/table.css
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Table
|
||||
--------------------------------------------------------------------------
|
||||
<div class="nv-table-frame">
|
||||
<table class="nv-table"> … </table>
|
||||
</div>
|
||||
|
||||
Rows of the same kind of thing. The frame handles the overflow, because a
|
||||
table is the one component that legitimately refuses to get narrower — it
|
||||
scrolls sideways inside its frame rather than forcing the page to.
|
||||
|
||||
Numbers belong in .nv-table__number: aligned to the end and set in tabular
|
||||
figures, so columns of digits line up and can be compared by eye.
|
||||
========================================================================== */
|
||||
|
||||
.nv-table-frame {
|
||||
inline-size: 100%;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-inline: contain;
|
||||
}
|
||||
|
||||
.nv-table {
|
||||
inline-size: 100%;
|
||||
border-collapse: collapse;
|
||||
font: var(--nv-font-body);
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.nv-table th {
|
||||
padding: var(--nv-space-2-5) var(--nv-space-4);
|
||||
font: var(--nv-font-caption);
|
||||
font-weight: var(--nv-font-weight-semibold);
|
||||
letter-spacing: var(--nv-letter-spacing-wide);
|
||||
color: var(--nv-color-text-tertiary);
|
||||
text-align: start;
|
||||
white-space: nowrap;
|
||||
border-block-end: var(--nv-border-width) solid var(--nv-color-border);
|
||||
}
|
||||
|
||||
.nv-table td {
|
||||
padding: var(--nv-space-3) var(--nv-space-4);
|
||||
color: var(--nv-color-text-secondary);
|
||||
border-block-end: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* The first cell in a row names it, so it carries the reading weight. */
|
||||
.nv-table td:first-child {
|
||||
color: var(--nv-color-text-primary);
|
||||
font-weight: var(--nv-font-weight-medium);
|
||||
}
|
||||
|
||||
.nv-table tbody tr:last-child td {
|
||||
border-block-end: 0;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.nv-table tbody tr {
|
||||
transition: var(--nv-transition-colors);
|
||||
}
|
||||
|
||||
.nv-table tbody tr:hover {
|
||||
background-color: var(--nv-color-surface-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.nv-table__number {
|
||||
text-align: end;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.nv-table__actions {
|
||||
inline-size: 1%;
|
||||
white-space: nowrap;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.nv-table--compact th {
|
||||
padding-block: var(--nv-space-2);
|
||||
}
|
||||
|
||||
.nv-table--compact td {
|
||||
padding-block: var(--nv-space-2);
|
||||
}
|
||||
|
||||
/* Rows that lead somewhere. Same physical language as an interactive card:
|
||||
it responds to a pointer, so it must look like it can be pressed. */
|
||||
.nv-table--interactive tbody tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nv-table--interactive tbody tr:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--nv-focus-ring);
|
||||
}
|
||||
189
niveus/components/window.css
Normal file
189
niveus/components/window.css
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Window
|
||||
--------------------------------------------------------------------------
|
||||
.nv-window a framed application window
|
||||
.nv-window--inactive a window that does not have focus
|
||||
.nv-window__titlebar controls · title · trailing actions
|
||||
.nv-window__controls the three lights, at the start of the bar
|
||||
.nv-window__control one light: --close, --minimize or --zoom
|
||||
.nv-window__title the window's name, centered on the bar
|
||||
.nv-window__actions trailing slot for a few ghost buttons
|
||||
.nv-window__body the content area
|
||||
|
||||
<div class="nv-window">
|
||||
<div class="nv-window__titlebar">
|
||||
<div class="nv-window__controls">
|
||||
<button class="nv-window__control nv-window__control--close" aria-label="Close"></button>
|
||||
<button class="nv-window__control nv-window__control--minimize" aria-label="Minimize"></button>
|
||||
<button class="nv-window__control nv-window__control--zoom" aria-label="Zoom"></button>
|
||||
</div>
|
||||
<span class="nv-window__title">Settings</span>
|
||||
</div>
|
||||
<div class="nv-window__body">…</div>
|
||||
</div>
|
||||
|
||||
A window is a solid object: the content it holds never sits on glass. Its
|
||||
titlebar is a raised strip lit like a control, and the controls are three
|
||||
lights at its start — close, minimize, zoom — always in that order and in
|
||||
those colors, because people find them by position and color before they
|
||||
read anything. Their glyphs appear only while the pointer is over the
|
||||
group. An inactive window greys its lights and steps back one elevation,
|
||||
and the lights regain their color the moment they are hovered.
|
||||
========================================================================== */
|
||||
|
||||
.nv-window {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-inline-size: 0;
|
||||
|
||||
background-color: var(--nv-color-surface);
|
||||
color: var(--nv-color-text-primary);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
border-radius: var(--nv-radius-xl);
|
||||
box-shadow: var(--nv-highlight-top), var(--nv-outline), var(--nv-elevation-4);
|
||||
|
||||
overflow: hidden;
|
||||
transition: var(--nv-transition-surface);
|
||||
}
|
||||
|
||||
.nv-window--inactive {
|
||||
box-shadow: var(--nv-highlight-top), var(--nv-outline), var(--nv-elevation-3);
|
||||
}
|
||||
|
||||
/* --- Titlebar ---------------------------------------------------------------
|
||||
Three columns with the outer two sharing the slack, so the title stays on
|
||||
the window's center line whatever the controls and actions measure. */
|
||||
|
||||
.nv-window__titlebar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: var(--nv-space-3);
|
||||
min-block-size: var(--nv-control-height-lg);
|
||||
padding-inline: var(--nv-space-4);
|
||||
|
||||
background-color: var(--nv-color-surface-elevated);
|
||||
background-image: var(--nv-sheen-raised);
|
||||
border-block-end: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
box-shadow: var(--nv-highlight-top);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nv-window__title {
|
||||
grid-column: 2;
|
||||
min-inline-size: 0;
|
||||
overflow: hidden;
|
||||
font: var(--nv-font-label);
|
||||
font-weight: var(--nv-font-weight-semibold);
|
||||
color: var(--nv-color-text-primary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nv-window--inactive .nv-window__title {
|
||||
color: var(--nv-color-text-tertiary);
|
||||
}
|
||||
|
||||
.nv-window__actions {
|
||||
grid-column: 3;
|
||||
justify-self: end;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--nv-space-1);
|
||||
}
|
||||
|
||||
/* --- Controls ----------------------------------------------------------------- */
|
||||
|
||||
.nv-window__controls {
|
||||
grid-column: 1;
|
||||
justify-self: start;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--nv-space-2);
|
||||
}
|
||||
|
||||
.nv-window__control {
|
||||
--_light: var(--nv-color-window-close);
|
||||
--_c: var(--_light);
|
||||
|
||||
position: relative;
|
||||
flex: none;
|
||||
inline-size: var(--nv-space-3);
|
||||
block-size: var(--nv-space-3);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
background-color: var(--_c);
|
||||
background-image: var(--nv-sheen-filled);
|
||||
border: 0;
|
||||
border-radius: var(--nv-radius-full);
|
||||
box-shadow: inset 0 0 0 var(--nv-border-width) var(--nv-color-window-control-ring);
|
||||
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: var(--nv-transition-colors);
|
||||
}
|
||||
|
||||
.nv-window__control--minimize { --_light: var(--nv-color-window-minimize); }
|
||||
.nv-window__control--zoom { --_light: var(--nv-color-window-zoom); }
|
||||
|
||||
/* The glyph is two bars drawn from the light's center: crossed for close, one
|
||||
for minimize, a plus for zoom. They are always present and fade in, so the
|
||||
whole group reveals at once instead of one glyph at a time. */
|
||||
.nv-window__control::before,
|
||||
.nv-window__control::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-block-start: 50%;
|
||||
inset-inline-start: 50%;
|
||||
inline-size: 55%;
|
||||
block-size: calc(var(--nv-border-width) * 1.5);
|
||||
translate: -50% -50%;
|
||||
background-color: var(--nv-color-window-glyph);
|
||||
border-radius: var(--nv-radius-full);
|
||||
opacity: 0;
|
||||
transition: opacity var(--nv-duration-fast) var(--nv-ease-standard);
|
||||
}
|
||||
|
||||
.nv-window__control--close::before { rotate: 45deg; }
|
||||
.nv-window__control--close::after { rotate: -45deg; }
|
||||
.nv-window__control--minimize::after { display: none; }
|
||||
.nv-window__control--zoom::after { rotate: 90deg; }
|
||||
|
||||
.nv-window__controls:hover .nv-window__control::before,
|
||||
.nv-window__controls:hover .nv-window__control::after,
|
||||
.nv-window__controls:focus-within .nv-window__control::before,
|
||||
.nv-window__controls:focus-within .nv-window__control::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nv-window__control:active {
|
||||
background-image: none;
|
||||
box-shadow:
|
||||
inset 0 0 0 var(--nv-border-width) var(--nv-color-window-control-ring),
|
||||
var(--nv-elevation-inset-sm);
|
||||
}
|
||||
|
||||
.nv-window__control:focus-visible {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
inset 0 0 0 var(--nv-border-width) var(--nv-color-window-control-ring),
|
||||
var(--nv-focus-ring);
|
||||
}
|
||||
|
||||
.nv-window--inactive .nv-window__control {
|
||||
--_c: var(--nv-color-window-inactive);
|
||||
}
|
||||
|
||||
.nv-window--inactive .nv-window__controls:hover .nv-window__control {
|
||||
--_c: var(--_light);
|
||||
}
|
||||
|
||||
/* --- Body --------------------------------------------------------------------- */
|
||||
|
||||
.nv-window__body {
|
||||
flex: 1;
|
||||
min-block-size: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
Loading…
Reference in a new issue