mirror of
https://github.com/Nexoniarz/Niveus.git
synced 2026-11-03 12:44:26 +00:00
Niveus read flat: thin sheens, short shadows, opaque grey chrome and a painted canvas. This retunes values and component internals only; every class and token name is unchanged, so existing pages pick it up as is. - Canvas: a faint accent light falls across the page, so translucent layers have something to show through. - Surfaces: the surface sheen runs the full height (lit top, shaded foot); raised and filled sheens, highlights and bevels are stronger; the elevation scale is deeper and softer. - Translucency: cards, dialogs, the sidebar and tab tracks let some of the canvas through, with blur so busy grounds never read as texture; frosted and glass materials are clearer with more blur. - Filled buttons give off a soft glow of their own color. - Light fields are paper instead of grey wells that looked disabled. - Fixes: navigation bars wrap and lists scroll inside them instead of overflowing the page on phones; choice labels sit at control size; disabled ghost buttons no longer grow a filled well; the color-swatch rule is split so neither engine drops it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iMUTHrD8K9CSuHqsanje9
421 lines
12 KiB
CSS
421 lines
12 KiB
CSS
/* ==========================================================================
|
|
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);
|
|
/* A well in dark, where recess reads as shade; paper in light, where a grey
|
|
well reads as a field that has been switched off. The inset shadow and
|
|
ledge carry the recess either way. */
|
|
background-color: light-dark(var(--nv-color-surface), 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 {
|
|
border: var(--nv-border-width) solid var(--nv-color-border);
|
|
border-radius: calc(var(--nv-radius-md) - var(--nv-space-1));
|
|
}
|
|
|
|
/* Separate rules on purpose: a selector list that names another engine's
|
|
pseudo-element is invalid as a whole, and each browser would drop both. */
|
|
.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;
|
|
/* The marks are drawn in em; pinning the size keeps them the same whatever
|
|
the label around them is set in. */
|
|
font-size: var(--nv-font-size-md);
|
|
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);
|
|
/* Labelled at control size, so a row of switches and checkboxes sits level
|
|
with the buttons and fields beside it. */
|
|
font: var(--nv-font-body);
|
|
font-size: var(--nv-font-size-sm);
|
|
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%;
|
|
}
|