mirror of
https://github.com/Nexoniarz/Niveus.git
synced 2026-11-03 04:34:26 +00:00
Rework light theme, add format wheel and motion, fix Firefox paths
Theme: light is a dimmer brand-tinted stone with deeper shade, so it reads as the same material as dark; every neutral now carries --nv-brand in both appearances. Native controls (accent-color, caret-color) take the brand. Time picker: 24-hour by default; data-nv-format-toggle adds a third wheel of 24H / AM / PM. Swiping it rebuilds the hour wheel for the new format. The selection lens is a raised glass bar, not a flat band. Motion: drum effect on the wheels as a scroll-driven animation, with a script fallback for engines without it (Firefox); rubber-band at both ends of a wheel that springs back; calendar months slide locally instead of via a page-wide transition; a chosen day springs into place. Firefox: wheel notches arrive in lines, not pixels, which made the rubber-band invisible; deltaMode is now normalised. The selected-day pop no longer plays on page load. Also: hover only inside @media (hover: hover) on the slider, the Glaze inner glow is a token, and the focus note in GUIDELINES.md is corrected (popovers do not trap focus). SPDX identifier added to niveus.js. check.py, build.py and build.py --check pass. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
369d1cd8f7
commit
5d435c4f49
12 changed files with 410 additions and 157 deletions
|
|
@ -146,6 +146,16 @@ freely.
|
||||||
|
|
||||||
### 4.1 Rules
|
### 4.1 Rules
|
||||||
|
|
||||||
|
0. **One dimensionality per interface.** Niveus is skeuomorphic *all the way
|
||||||
|
through*. A raised key, a recessed well and a lit lens are physical objects;
|
||||||
|
a flat colour block beside them reads as a different system. Every
|
||||||
|
interactive or selected element MUST take its shape from the depth
|
||||||
|
vocabulary — bevel, sheen, inset, ledge, outline, elevation — and MUST NOT be
|
||||||
|
a flat fill. If a state seems to need a flat wash (a hover, a selection band,
|
||||||
|
a pressed toggle), give it depth instead: press it *into* the surface, or
|
||||||
|
lay a lit lens over it. Mixing flat and physical is the single most common
|
||||||
|
way a Niveus interface starts to look unfinished.
|
||||||
|
|
||||||
1. **Solid by default.** Anything that *holds content* — windows, cards, dialogs,
|
1. **Solid by default.** Anything that *holds content* — windows, cards, dialogs,
|
||||||
tables, forms — is solid. Translucency is reserved for what *floats over*
|
tables, forms — is solid. Translucency is reserved for what *floats over*
|
||||||
content.
|
content.
|
||||||
|
|
@ -265,7 +275,17 @@ place to define a colour.
|
||||||
attribute change (see 11).
|
attribute change (see 11).
|
||||||
5. **Semantic colours mean one thing.** `success`, `warning`, `danger` carry
|
5. **Semantic colours mean one thing.** `success`, `warning`, `danger` carry
|
||||||
meaning; they are not accents. Use `accent` for emphasis and brand.
|
meaning; they are not accents. Use `accent` for emphasis and brand.
|
||||||
6. **Dark mode is designed, not generated.** When a new surface is added, look at
|
6. **Every neutral carries the brand.** Surfaces, canvases, glass and shadows
|
||||||
|
are mixed with `--nv-brand` in *both* appearances, so changing one value
|
||||||
|
re-colours the whole interface and light and dark read as one material. A
|
||||||
|
neutral that ignores the brand — a literal grey or a bright white field — is
|
||||||
|
a defect. Light is a soft stone, not paper-white: a glaring field flattens
|
||||||
|
the bevels meant to shape it.
|
||||||
|
7. **No native colour survives.** Anything the browser draws itself (checkbox
|
||||||
|
ticks, range and progress fills, the caret, selection, date glyphs) takes the
|
||||||
|
brand through `accent-color`, `caret-color` and `::selection`. Do not leave a
|
||||||
|
control in its default blue.
|
||||||
|
8. **Dark mode is designed, not generated.** When a new surface is added, look at
|
||||||
it in dark. If it only works on white, it is not finished.
|
it in dark. If it only works on white, it is not finished.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -320,7 +340,18 @@ have watched it reach.**
|
||||||
away from the control that opened them.
|
away from the control that opened them.
|
||||||
7. **No JavaScript, no breakage.** Continuity enhances; it MUST NOT be required
|
7. **No JavaScript, no breakage.** Continuity enhances; it MUST NOT be required
|
||||||
for function.
|
for function.
|
||||||
8. **Restraint wins ties.** When continuity and restraint conflict, animate less.
|
8. **Edges give.** A scroller that reaches its end yields and springs back
|
||||||
|
(rubber-band) instead of stopping dead, the way a phone's list does. Use the
|
||||||
|
spring easing token, cap the stretch, and leave touch to the platform where
|
||||||
|
it already does this. The wheels in the time picker are the reference.
|
||||||
|
9. **Let scroll drive depth.** Where an effect is a function of scroll
|
||||||
|
position — a drum's rows fading and tilting away from the centre — express it
|
||||||
|
as a scroll-driven animation in CSS, not as script, so it tracks a finger
|
||||||
|
exactly and costs no frame budget. Provide a fallback where the feature is missing — either the plain state, or the same curve from script (as the wheels do for Firefox) — and test input differences between engines: wheel notches arrive in pixels in Chromium and in *lines* in Firefox, so normalise `deltaMode` before using `deltaY`.
|
||||||
|
10. **Local motion for local change.** Changing a month moves the month, not the
|
||||||
|
page. Reserve page-wide view transitions for changes that are about the
|
||||||
|
page: appearance, route, a selection travelling between regions.
|
||||||
|
11. **Restraint wins ties.** When continuity and restraint conflict, animate less.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -368,8 +399,10 @@ Niveus provides the base; the author is responsible for the rest.
|
||||||
visible focus order that follows reading order.
|
visible focus order that follows reading order.
|
||||||
4. **Labels.** Every field has a programmatic label. Errors pair
|
4. **Labels.** Every field has a programmatic label. Errors pair
|
||||||
`aria-invalid="true"` with `aria-describedby` pointing at the message.
|
`aria-invalid="true"` with `aria-describedby` pointing at the message.
|
||||||
5. **Dialogs and popovers** trap or restore focus (native `<dialog>` and
|
5. **Focus management.** A modal `<dialog>` opened with `showModal()` traps focus
|
||||||
`popover` do; custom ones MUST).
|
and restores it on close. A `popover` does **not** trap focus: move focus into
|
||||||
|
it on open where it holds controls, and return it to the trigger on close, as
|
||||||
|
the popup list does. Custom overlays MUST do both.
|
||||||
6. **Live regions** (`aria-live`) for content that changes without focus —
|
6. **Live regions** (`aria-live`) for content that changes without focus —
|
||||||
a calendar's month title is one.
|
a calendar's month title is one.
|
||||||
7. **Test the extremes.** Verify text on translucent materials at the
|
7. **Test the extremes.** Verify text on translucent materials at the
|
||||||
|
|
@ -396,8 +429,8 @@ The complete list and API live in `REFERENCE.md`. These are the rules for
|
||||||
month so it never changes height. The grid and states are plain markup;
|
month so it never changes height. The grid and states are plain markup;
|
||||||
`niveus.js` only builds days, steps months and provides keyboard movement.
|
`niveus.js` only builds days, steps months and provides keyboard movement.
|
||||||
- **Time picker** (`.nv-timepicker`): scroll-snap wheels. **24-hour is the
|
- **Time picker** (`.nv-timepicker`): scroll-snap wheels. **24-hour is the
|
||||||
default**; `data-nv-format-toggle` adds a 24H / AM/PM switch beneath, and
|
default**; `data-nv-format-toggle` adds a third wheel of 24H / AM / PM: swiping it
|
||||||
AM/PM adds a third wheel. The value is always 24-hour (`HH:MM`), so a form
|
changes the format, and the hour wheel follows. The value is always 24-hour (`HH:MM`), so a form
|
||||||
never needs to know which the person used. For a no-script fallback use
|
never needs to know which the person used. For a no-script fallback use
|
||||||
`<input type="time" class="nv-input">`; the wheels are an enhancement, not
|
`<input type="time" class="nv-input">`; the wheels are an enhancement, not
|
||||||
the only path.
|
the only path.
|
||||||
|
|
|
||||||
|
|
@ -494,6 +494,7 @@ to the state change simply happening rather than travelling:
|
||||||
| `interpolate-size` | disclosure height | content appears at full height (`.nv-collapse` still animates) |
|
| `interpolate-size` | disclosure height | content appears at full height (`.nv-collapse` still animates) |
|
||||||
| Anchor positioning | menus tied to their trigger | the menu centres itself |
|
| Anchor positioning | menus tied to their trigger | the menu centres itself |
|
||||||
| `popover` | menus, light dismiss | needs the application's own toggle |
|
| `popover` | menus, light dismiss | needs the application's own toggle |
|
||||||
|
| Scroll-driven animation | the time picker's drum effect | `niveus.js` applies the same curve from the scroll position (Firefox) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,8 @@ States come from the element: `disabled`, `aria-disabled`, `aria-busy="true"`
|
||||||
| `.nv-calendar__grid` | day grid | `role="grid"`; built by `niveus.js` or written by hand. |
|
| `.nv-calendar__grid` | day grid | `role="grid"`; built by `niveus.js` or written by hand. |
|
||||||
| `.nv-calendar__weekday` | weekday column header | |
|
| `.nv-calendar__weekday` | weekday column header | |
|
||||||
| `.nv-calendar__day` | day cell | `aria-selected="true"` chosen · `aria-current="date"` today · `data-outside` neighbouring month · `disabled` out of range. |
|
| `.nv-calendar__day` | day cell | `aria-selected="true"` chosen · `aria-current="date"` today · `data-outside` neighbouring month · `disabled` out of range. |
|
||||||
| `.nv-timepicker` | time picker, wheel picker | Scroll-snap wheels built from `data-nv-timepicker` plus `data-nv-value="HH:MM"` / `-hours` (`24` default, `12` adds an AM/PM wheel) / `-step` / `-name` / `-format-toggle` (a 24H / AM/PM switch under the wheels). The value is always 24-hour. Fires `nv:time`. Empty without the script; use `<input type="time" class="nv-input">` as the fallback. |
|
| `.nv-timepicker` | time picker, wheel picker | Scroll-snap wheels built from `data-nv-timepicker` plus `data-nv-value="HH:MM"` / `-hours` (`24` default, `12` adds an AM/PM wheel) / `-step` / `-name` / `-format-toggle` (a third wheel of 24H / AM / PM; swipe it to change the format). The value is always 24-hour. Fires `nv:time`. Empty without the script; use `<input type="time" class="nv-input">` as the fallback. |
|
||||||
| `.nv-timepicker__wheels` | wheel row, picker body | Holds the columns and the selection lens. |
|
| `.nv-timepicker__wheels` | wheel row, picker body | Holds the columns and the selection lens. |
|
||||||
| `.nv-timepicker__format` | 24H / AM/PM switch, time format toggle | Button group under the wheels; added by `data-nv-format-toggle`. |
|
|
||||||
| `.nv-timepicker__column` | wheel, picker column | One scrolling list (`role="listbox"`). |
|
| `.nv-timepicker__column` | wheel, picker column | One scrolling list (`role="listbox"`). |
|
||||||
| `.nv-timepicker__option` | wheel row | `aria-selected="true"` is the row in the lens. |
|
| `.nv-timepicker__option` | wheel row | `aria-selected="true"` is the row in the lens. |
|
||||||
| `.nv-timepicker__separator` | colon between wheels | |
|
| `.nv-timepicker__separator` | colon between wheels | |
|
||||||
|
|
@ -472,7 +471,7 @@ level. Components never opt in individually.
|
||||||
subtree) · `--nv-glaze-blur-max` · `--nv-glaze-tint-min` ·
|
subtree) · `--nv-glaze-blur-max` · `--nv-glaze-tint-min` ·
|
||||||
`--nv-glaze-tint-max` · `--nv-glaze-rim` · `--nv-light-x` / `--nv-light-y` /
|
`--nv-glaze-tint-max` · `--nv-glaze-rim` · `--nv-light-x` / `--nv-light-y` /
|
||||||
`--nv-light-angle` (set by `niveus.js` on each glaze surface) ·
|
`--nv-light-angle` (set by `niveus.js` on each glaze surface) ·
|
||||||
`--nv-slider-pos` (set by `niveus.js`) · `--nv-focus-ring` · `--nv-focus-ring-width` · `--nv-border-width` ·
|
`--nv-slider-pos` (set by `niveus.js`) · `--nv-pull` (set by `niveus.js` on a time-picker wheel pulled past its end) · `--nv-focus-ring` · `--nv-focus-ring-width` · `--nv-border-width` ·
|
||||||
`--nv-border-width-strong` · `--nv-target-min` · `--nv-layout-gutter` ·
|
`--nv-border-width-strong` · `--nv-target-min` · `--nv-layout-gutter` ·
|
||||||
`--nv-layout-max-width` · `--nv-selection-name` · `--nv-anchor` ·
|
`--nv-layout-max-width` · `--nv-selection-name` · `--nv-anchor` ·
|
||||||
`--nv-button-offset` · `--nv-bed-tint` · `--nv-scroller-item` ·
|
`--nv-button-offset` · `--nv-bed-tint` · `--nv-scroller-item` ·
|
||||||
|
|
@ -526,7 +525,7 @@ everything still renders — state changes simply arrive instead of travelling.
|
||||||
| `data-nv-var-target` | `.nv-slider` | Selector of the element that receives the property. Default: `<html>`. |
|
| `data-nv-var-target` | `.nv-slider` | Selector of the element that receives the property. Default: `<html>`. |
|
||||||
| `data-nv-calendar` | `.nv-calendar` | Marks a calendar for `niveus.js`. Also `data-nv-value`, `-min`, `-max` (`YYYY-MM-DD`), `-locale`, `-week-start` (0 = Sunday). |
|
| `data-nv-calendar` | `.nv-calendar` | Marks a calendar for `niveus.js`. Also `data-nv-value`, `-min`, `-max` (`YYYY-MM-DD`), `-locale`, `-week-start` (0 = Sunday). |
|
||||||
| `data-nv-calendar-step` | button in a calendar | `-1` or `1`: months to move. |
|
| `data-nv-calendar-step` | button in a calendar | `-1` or `1`: months to move. |
|
||||||
| `data-nv-timepicker` | `.nv-timepicker` | Marks a time picker. Also `data-nv-value` (`HH:MM`), `-hours` (`24` default / `12`), `-step` (minutes), `-name` (hidden form field), `-format-toggle` (adds the 24H / AM/PM switch). |
|
| `data-nv-timepicker` | `.nv-timepicker` | Marks a time picker. Also `data-nv-value` (`HH:MM`), `-hours` (`24` default / `12`), `-step` (minutes), `-name` (hidden form field), `-format-toggle` (adds the 24H / AM / PM wheel). |
|
||||||
| `data-nv-picker-label` | child of a picker trigger | Receives the chosen option's text; default is the trigger itself. |
|
| `data-nv-picker-label` | child of a picker trigger | Receives the chosen option's text; default is the trigger itself. |
|
||||||
| `data-nv-scroller` | `.nv-scroller-frame` | Wires its `[data-nv-scroll="prev"\|"next"]` controls. |
|
| `data-nv-scroller` | `.nv-scroller-frame` | Wires its `[data-nv-scroll="prev"\|"next"]` controls. |
|
||||||
| `data-scrolled` | `.nv-nav` | Set by the application on scroll; gives the bar its shadow. |
|
| `data-scrolled` | `.nv-nav` | Set by the application on scroll; gives the bar its shadow. |
|
||||||
|
|
|
||||||
164
dist/niveus.css
vendored
164
dist/niveus.css
vendored
|
|
@ -811,6 +811,9 @@
|
||||||
--nv-glaze-tint-min: 4%; /* tint opacity at frost 0 */
|
--nv-glaze-tint-min: 4%; /* tint opacity at frost 0 */
|
||||||
--nv-glaze-tint-max: 78%; /* tint opacity at frost 1 */
|
--nv-glaze-tint-max: 78%; /* tint opacity at frost 1 */
|
||||||
--nv-glaze-rim: 1.5px;
|
--nv-glaze-rim: 1.5px;
|
||||||
|
/* The light a pane holds just inside its edge, so it reads as a thick lens
|
||||||
|
rather than a flat film. */
|
||||||
|
--nv-glaze-glow: inset 0 0 1.5rem -0.5rem var(--nv-color-glaze-specular);
|
||||||
|
|
||||||
/* --- Sheen ------------------------------------------------------------------
|
/* --- Sheen ------------------------------------------------------------------
|
||||||
raised neutral controls: buttons, the selected tab, a titlebar
|
raised neutral controls: buttons, the selected tab, a titlebar
|
||||||
|
|
@ -907,16 +910,18 @@
|
||||||
:root,
|
:root,
|
||||||
[data-nv-brand] {
|
[data-nv-brand] {
|
||||||
/* --- Canvas and surfaces -----------------------------------------------
|
/* --- Canvas and surfaces -----------------------------------------------
|
||||||
Paper stays paper: the two surfaces that hold content are left untinted so
|
Every neutral carries a trace of the brand, content surfaces included, the
|
||||||
text keeps its contrast. Everything the surfaces sit *in* picks up a trace
|
way dark does: that is what stops an accent from looking stuck on, and what
|
||||||
of the brand, which is what stops an accent from looking stuck on. */
|
makes the two appearances read as one material. The paper is a soft
|
||||||
--nv-light-background: color-mix(in oklab, #e8e5de, var(--nv-brand) var(--nv-brand-tint));
|
grey-stone, not white — a bright field glares, and flattens the bevels that
|
||||||
--nv-light-background-subtle: color-mix(in oklab, #eeece6, var(--nv-brand) var(--nv-brand-tint));
|
are meant to shape it. Text keeps its contrast: the primary is neutral. */
|
||||||
--nv-light-surface: #fbfbfa;
|
--nv-light-background: color-mix(in oklab, #d8d5ce, var(--nv-brand) calc(var(--nv-brand-tint) * 2));
|
||||||
--nv-light-surface-elevated: #f8f8f6;
|
--nv-light-background-subtle: color-mix(in oklab, #dedbd4, var(--nv-brand) calc(var(--nv-brand-tint) * 2));
|
||||||
--nv-light-surface-inset: color-mix(in oklab, #eeede8, var(--nv-brand) var(--nv-brand-tint));
|
--nv-light-surface: color-mix(in oklab, #eeede9, var(--nv-brand) var(--nv-brand-tint));
|
||||||
--nv-light-surface-hover: color-mix(in oklab, #f1f0ec, var(--nv-brand) var(--nv-brand-tint));
|
--nv-light-surface-elevated: color-mix(in oklab, #f2f1ed, var(--nv-brand) var(--nv-brand-tint));
|
||||||
--nv-light-surface-active: color-mix(in oklab, #e6e5e0, var(--nv-brand) var(--nv-brand-tint));
|
--nv-light-surface-inset: color-mix(in oklab, #dddbd4, var(--nv-brand) calc(var(--nv-brand-tint) * 2));
|
||||||
|
--nv-light-surface-hover: color-mix(in oklab, #e6e4de, var(--nv-brand) var(--nv-brand-tint));
|
||||||
|
--nv-light-surface-active: color-mix(in oklab, #dad8d1, var(--nv-brand) var(--nv-brand-tint));
|
||||||
--nv-light-scrim: rgb(28 24 18 / 0.34);
|
--nv-light-scrim: rgb(28 24 18 / 0.34);
|
||||||
|
|
||||||
/* --- Text ---------------------------------------------------------------
|
/* --- Text ---------------------------------------------------------------
|
||||||
|
|
@ -925,7 +930,7 @@
|
||||||
loss of legibility. */
|
loss of legibility. */
|
||||||
--nv-light-text-primary: var(--nv-palette-neutral-900);
|
--nv-light-text-primary: var(--nv-palette-neutral-900);
|
||||||
--nv-light-text-secondary: color-mix(in oklab, var(--nv-palette-neutral-600), var(--nv-brand) 8%);
|
--nv-light-text-secondary: color-mix(in oklab, var(--nv-palette-neutral-600), var(--nv-brand) 8%);
|
||||||
--nv-light-text-tertiary: color-mix(in oklab, var(--nv-palette-neutral-500), var(--nv-brand) 8%);
|
--nv-light-text-tertiary: color-mix(in oklab, color-mix(in oklab, var(--nv-palette-neutral-500), var(--nv-palette-neutral-600) 40%), var(--nv-brand) 8%);
|
||||||
--nv-light-text-disabled: var(--nv-palette-neutral-400);
|
--nv-light-text-disabled: var(--nv-palette-neutral-400);
|
||||||
--nv-light-text-inverted: var(--nv-palette-neutral-0);
|
--nv-light-text-inverted: var(--nv-palette-neutral-0);
|
||||||
|
|
||||||
|
|
@ -1010,20 +1015,20 @@
|
||||||
/* --- Depth and material -------------------------------------------------
|
/* --- Depth and material -------------------------------------------------
|
||||||
Three shadow colors build every elevation level: an ambient contact
|
Three shadow colors build every elevation level: an ambient contact
|
||||||
shadow, a soft penumbra, and a deeper umbra for high elevations. */
|
shadow, a soft penumbra, and a deeper umbra for high elevations. */
|
||||||
--nv-light-shadow-ambient: rgb(40 32 20 / 0.08);
|
--nv-light-shadow-ambient: rgb(32 28 22 / 0.12);
|
||||||
--nv-light-shadow-penumbra: rgb(40 32 20 / 0.1);
|
--nv-light-shadow-penumbra: rgb(32 28 22 / 0.15);
|
||||||
--nv-light-shadow-umbra: rgb(40 32 20 / 0.16);
|
--nv-light-shadow-umbra: rgb(32 28 22 / 0.24);
|
||||||
--nv-light-shadow-inset: rgb(40 32 20 / 0.14);
|
--nv-light-shadow-inset: rgb(32 28 22 / 0.2);
|
||||||
--nv-light-highlight: rgb(255 255 255 / 0.9);
|
--nv-light-highlight: rgb(255 255 255 / 0.7);
|
||||||
--nv-light-highlight-strong: rgb(255 255 255 / 1);
|
--nv-light-highlight-strong: rgb(255 255 255 / 0.85);
|
||||||
|
|
||||||
/* Paper already has a border doing the contact work, so the outline only
|
/* Paper already has a border doing the contact work, so the outline only
|
||||||
softens it; the bottom shade is faint for the same reason. */
|
softens it; the bottom shade is faint for the same reason. */
|
||||||
--nv-light-lowlight: rgb(40 32 20 / 0.09);
|
--nv-light-lowlight: rgb(32 28 22 / 0.14);
|
||||||
--nv-light-outline: rgb(40 32 20 / 0.05);
|
--nv-light-outline: rgb(32 28 22 / 0.08);
|
||||||
|
|
||||||
--nv-light-sheen-top: rgb(255 255 255 / 0.9);
|
--nv-light-sheen-top: rgb(255 255 255 / 0.55);
|
||||||
--nv-light-sheen-bottom: rgb(40 32 20 / 0.07);
|
--nv-light-sheen-bottom: rgb(32 28 22 / 0.11);
|
||||||
--nv-light-sheen-strong-top: rgb(255 255 255 / 0.32);
|
--nv-light-sheen-strong-top: rgb(255 255 255 / 0.32);
|
||||||
--nv-light-sheen-strong-bottom: rgb(20 16 10 / 0.16);
|
--nv-light-sheen-strong-bottom: rgb(20 16 10 / 0.16);
|
||||||
--nv-light-glow: oklch(from var(--nv-light-accent) l c h / 0.3);
|
--nv-light-glow: oklch(from var(--nv-light-accent) l c h / 0.3);
|
||||||
|
|
@ -1039,16 +1044,16 @@
|
||||||
--nv-light-elevation-tint-4: transparent;
|
--nv-light-elevation-tint-4: transparent;
|
||||||
--nv-light-elevation-tint-5: transparent;
|
--nv-light-elevation-tint-5: transparent;
|
||||||
|
|
||||||
--nv-light-material-frosted: rgb(252 252 250 / 0.56);
|
--nv-light-material-frosted: color-mix(in oklab, rgb(228 227 222 / 0.6), var(--nv-brand) var(--nv-brand-tint));
|
||||||
--nv-light-material-glass: rgb(252 252 250 / 0.4);
|
--nv-light-material-glass: color-mix(in oklab, rgb(228 227 222 / 0.44), var(--nv-brand) var(--nv-brand-tint));
|
||||||
--nv-light-material-border: rgb(255 255 255 / 0.72);
|
--nv-light-material-border: rgb(255 255 255 / 0.5);
|
||||||
|
|
||||||
/* Glaze: the tint it is made of, the rim that catches light on the side
|
/* Glaze: the tint it is made of, the rim that catches light on the side
|
||||||
facing the pointer, the rim on the far side, and the specular bloom. */
|
facing the pointer, the rim on the far side, and the specular bloom. */
|
||||||
--nv-light-material-glaze: rgb(255 255 255 / 1);
|
--nv-light-material-glaze: color-mix(in oklab, rgb(232 231 226 / 1), var(--nv-brand) calc(var(--nv-brand-tint) * 2));
|
||||||
--nv-light-glaze-rim-lit: rgb(255 255 255 / 1);
|
--nv-light-glaze-rim-lit: rgb(255 255 255 / 0.9);
|
||||||
--nv-light-glaze-rim-far: rgb(255 255 255 / 0.5);
|
--nv-light-glaze-rim-far: rgb(255 255 255 / 0.4);
|
||||||
--nv-light-glaze-specular: rgb(255 255 255 / 0.6);
|
--nv-light-glaze-specular: rgb(255 255 255 / 0.4);
|
||||||
|
|
||||||
/* --- Window controls --------------------------------------------------------- */
|
/* --- Window controls --------------------------------------------------------- */
|
||||||
--nv-light-window-close: var(--nv-palette-window-close);
|
--nv-light-window-close: var(--nv-palette-window-close);
|
||||||
|
|
@ -1369,6 +1374,14 @@
|
||||||
border-radius: var(--nv-radius-xs);
|
border-radius: var(--nv-radius-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Whatever the browser draws itself — a bare checkbox, a progress bar, the
|
||||||
|
text caret, a date field's glyphs — takes the brand, so no native colour
|
||||||
|
survives next to the system's own. */
|
||||||
|
:root {
|
||||||
|
accent-color: var(--nv-color-accent);
|
||||||
|
caret-color: var(--nv-color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background-color: var(--nv-color-selection);
|
background-color: var(--nv-color-selection);
|
||||||
color: var(--nv-color-text-primary);
|
color: var(--nv-color-text-primary);
|
||||||
|
|
@ -1840,7 +1853,7 @@
|
||||||
border: var(--nv-border-width) solid transparent;
|
border: var(--nv-border-width) solid transparent;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
var(--nv-highlight-top-strong),
|
var(--nv-highlight-top-strong),
|
||||||
inset 0 0 1.5rem -0.5rem var(--nv-color-glaze-specular),
|
var(--nv-glaze-glow),
|
||||||
var(--nv-outline),
|
var(--nv-outline),
|
||||||
var(--nv-elevation-4);
|
var(--nv-elevation-4);
|
||||||
backdrop-filter:
|
backdrop-filter:
|
||||||
|
|
@ -2772,7 +2785,7 @@
|
||||||
box-shadow: var(--nv-outline), var(--nv-elevation-2);
|
box-shadow: var(--nv-outline), var(--nv-elevation-2);
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
transition: scale var(--nv-duration-fast) var(--nv-ease-standard);
|
transition: scale var(--nv-duration-quick) var(--nv-ease-spring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nv-slider::-moz-range-thumb {
|
.nv-slider::-moz-range-thumb {
|
||||||
|
|
@ -2784,12 +2797,17 @@
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--nv-radius-full);
|
border-radius: var(--nv-radius-full);
|
||||||
box-shadow: var(--nv-outline), var(--nv-elevation-2);
|
box-shadow: var(--nv-outline), var(--nv-elevation-2);
|
||||||
transition: scale var(--nv-duration-fast) var(--nv-ease-standard);
|
transition: scale var(--nv-duration-quick) var(--nv-ease-spring);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover only where there is a pointer to hover with; touch would leave the
|
||||||
|
thumb enlarged after a tap. */
|
||||||
|
@media (hover: hover) {
|
||||||
|
.nv-slider:hover::-webkit-slider-thumb { scale: 1.06; }
|
||||||
|
.nv-slider:hover::-moz-range-thumb { scale: 1.06; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.nv-slider:hover::-webkit-slider-thumb { scale: 1.06; }
|
|
||||||
.nv-slider:active::-webkit-slider-thumb { scale: 0.96; }
|
.nv-slider:active::-webkit-slider-thumb { scale: 0.96; }
|
||||||
.nv-slider:hover::-moz-range-thumb { scale: 1.06; }
|
|
||||||
.nv-slider:active::-moz-range-thumb { scale: 0.96; }
|
.nv-slider:active::-moz-range-thumb { scale: 0.96; }
|
||||||
|
|
||||||
.nv-slider:focus-visible::-webkit-slider-thumb {
|
.nv-slider:focus-visible::-webkit-slider-thumb {
|
||||||
|
|
@ -3971,8 +3989,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
|
/* Hover presses the day slightly into the surface; it is never a flat wash
|
||||||
|
of colour, because the chosen day is a raised key and the two must read as
|
||||||
|
the same material. */
|
||||||
.nv-calendar__day:hover:not(:disabled, [aria-selected="true"]) {
|
.nv-calendar__day:hover:not(:disabled, [aria-selected="true"]) {
|
||||||
background-color: var(--nv-color-surface-hover);
|
background-color: var(--nv-color-surface-hover);
|
||||||
|
box-shadow: var(--nv-elevation-inset-sm), var(--nv-ledge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3983,7 +4005,7 @@
|
||||||
/* Today is ringed, not filled: filled means chosen. */
|
/* Today is ringed, not filled: filled means chosen. */
|
||||||
.nv-calendar__day[aria-current="date"] {
|
.nv-calendar__day[aria-current="date"] {
|
||||||
color: var(--nv-color-accent-text);
|
color: var(--nv-color-accent-text);
|
||||||
box-shadow: inset 0 0 0 var(--nv-border-width-strong) var(--nv-color-accent);
|
box-shadow: inset 0 0 0 var(--nv-border-width-strong) var(--nv-color-accent), var(--nv-elevation-inset-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The chosen day is a small raised key, lit like the filled buttons. */
|
/* The chosen day is a small raised key, lit like the filled buttons. */
|
||||||
|
|
@ -4007,6 +4029,17 @@
|
||||||
color: var(--nv-color-text-disabled);
|
color: var(--nv-color-text-disabled);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A day pressed into place springs up to full size. Only when it was just
|
||||||
|
chosen: a selection that is merely *there* on load stays still. */
|
||||||
|
.nv-calendar__day[aria-selected="true"][data-nv-pop] {
|
||||||
|
animation: nv-day-pop var(--nv-duration-quick) var(--nv-ease-spring);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes nv-day-pop {
|
||||||
|
from { scale: 0.72; }
|
||||||
|
to { scale: 1; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@layer niveus.components {
|
@layer niveus.components {
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
|
|
@ -4026,9 +4059,9 @@
|
||||||
Glaze with something moving behind it.
|
Glaze with something moving behind it.
|
||||||
|
|
||||||
The format is 24-hour unless told otherwise. With data-nv-format-toggle a
|
The format is 24-hour unless told otherwise. With data-nv-format-toggle a
|
||||||
24H / AM/PM switch sits under the wheels and adds an AM/PM wheel when chosen;
|
third wheel reads 24H / AM / PM: swipe it to AM or PM and the hour wheel
|
||||||
the value stays 24-hour either way, so a form never has to know which the
|
becomes 1–12; swipe back to 24H and it returns to 0–23. The value stays
|
||||||
person used.
|
24-hour either way, so a form never has to know which the person used.
|
||||||
|
|
||||||
Without niveus.js the element is empty. For a no-script fallback, use a
|
Without niveus.js the element is empty. For a no-script fallback, use a
|
||||||
native <input type="time" class="nv-input">.
|
native <input type="time" class="nv-input">.
|
||||||
|
|
@ -4042,7 +4075,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--nv-space-2);
|
gap: var(--nv-space-2);
|
||||||
padding: var(--nv-space-2) var(--nv-space-3) var(--nv-space-3);
|
padding: var(--nv-space-2) var(--nv-space-3);
|
||||||
border-radius: var(--nv-radius-xl);
|
border-radius: var(--nv-radius-xl);
|
||||||
color: var(--nv-color-text-primary);
|
color: var(--nv-color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
@ -4053,8 +4086,10 @@
|
||||||
gap: var(--nv-space-1);
|
gap: var(--nv-space-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The lens. It belongs to the wheels, not the picker, so the switch beneath
|
/* The lens: a bar of tinted glass lying across the centre row. It is raised
|
||||||
them never moves the centre. */
|
like the keys elsewhere in the system — lit on top, shaded below, with a
|
||||||
|
contact line — not a flat band of colour, so it reads as a thing the wheels
|
||||||
|
pass under. */
|
||||||
.nv-timepicker__wheels::before {
|
.nv-timepicker__wheels::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -4063,8 +4098,9 @@
|
||||||
block-size: var(--_row);
|
block-size: var(--_row);
|
||||||
translate: 0 -50%;
|
translate: 0 -50%;
|
||||||
background-color: var(--nv-color-selection);
|
background-color: var(--nv-color-selection);
|
||||||
|
background-image: var(--nv-sheen-raised);
|
||||||
border-radius: var(--nv-radius-md);
|
border-radius: var(--nv-radius-md);
|
||||||
box-shadow: var(--nv-highlight-top);
|
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4085,6 +4121,9 @@
|
||||||
currentcolor 72%,
|
currentcolor 72%,
|
||||||
transparent
|
transparent
|
||||||
);
|
);
|
||||||
|
/* Depth for the drum effect below: rows turn away from the viewer as they
|
||||||
|
leave the centre. */
|
||||||
|
perspective: 14rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nv-timepicker__column::-webkit-scrollbar {
|
.nv-timepicker__column::-webkit-scrollbar {
|
||||||
|
|
@ -4107,28 +4146,43 @@
|
||||||
font-size: var(--nv-font-size-lg);
|
font-size: var(--nv-font-size-lg);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
/* --nv-pull is how far the wheel has been dragged past its end. Letting go
|
||||||
|
removes it, and the spring easing carries the rows back with a small
|
||||||
|
overshoot — the way a phone's list gives at its limit and settles. */
|
||||||
|
translate: 0 var(--nv-pull, 0);
|
||||||
|
transition:
|
||||||
|
var(--nv-transition-colors),
|
||||||
|
translate var(--nv-duration-slower) var(--nv-ease-spring);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nv-timepicker__column[data-nv-pulling] .nv-timepicker__option {
|
||||||
transition: var(--nv-transition-colors);
|
transition: var(--nv-transition-colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A drum: each row shrinks, fades and tilts away in proportion to how far it
|
||||||
|
is from the lens. Driven by the scroll position itself, so it needs no
|
||||||
|
script and tracks a finger exactly. Where scroll-driven animation is missing
|
||||||
|
the rows are simply flat, which is still correct. */
|
||||||
|
@supports (animation-timeline: view()) {
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.nv-timepicker__option {
|
||||||
|
animation: nv-wheel-drum linear both;
|
||||||
|
animation-timeline: view(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes nv-wheel-drum {
|
||||||
|
0% { scale: 0.78; opacity: 0.25; rotate: x 52deg; }
|
||||||
|
50% { scale: 1; opacity: 1; rotate: x 0deg; }
|
||||||
|
100% { scale: 0.78; opacity: 0.25; rotate: x -52deg; }
|
||||||
|
}
|
||||||
|
|
||||||
.nv-timepicker__option[aria-selected="true"] {
|
.nv-timepicker__option[aria-selected="true"] {
|
||||||
color: var(--nv-color-text-primary);
|
color: var(--nv-color-text-primary);
|
||||||
font-weight: var(--nv-font-weight-semibold);
|
font-weight: var(--nv-font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 24H / AM/PM. A button group under the wheels; the pressed one is the format
|
|
||||||
in use. 24-hour is the default, so it starts pressed. */
|
|
||||||
.nv-timepicker__format {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pressed reads as pushed in: the format in use is a key held down. */
|
|
||||||
.nv-timepicker__format .nv-button[aria-pressed="true"] {
|
|
||||||
background-color: var(--nv-color-accent-subtle);
|
|
||||||
background-image: none;
|
|
||||||
color: var(--nv-color-accent-text);
|
|
||||||
box-shadow: var(--nv-elevation-inset-sm), var(--nv-ledge);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nv-timepicker__separator {
|
.nv-timepicker__separator {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,14 @@ textarea {
|
||||||
border-radius: var(--nv-radius-xs);
|
border-radius: var(--nv-radius-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Whatever the browser draws itself — a bare checkbox, a progress bar, the
|
||||||
|
text caret, a date field's glyphs — takes the brand, so no native colour
|
||||||
|
survives next to the system's own. */
|
||||||
|
:root {
|
||||||
|
accent-color: var(--nv-color-accent);
|
||||||
|
caret-color: var(--nv-color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background-color: var(--nv-color-selection);
|
background-color: var(--nv-color-selection);
|
||||||
color: var(--nv-color-text-primary);
|
color: var(--nv-color-text-primary);
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
|
/* Hover presses the day slightly into the surface; it is never a flat wash
|
||||||
|
of colour, because the chosen day is a raised key and the two must read as
|
||||||
|
the same material. */
|
||||||
.nv-calendar__day:hover:not(:disabled, [aria-selected="true"]) {
|
.nv-calendar__day:hover:not(:disabled, [aria-selected="true"]) {
|
||||||
background-color: var(--nv-color-surface-hover);
|
background-color: var(--nv-color-surface-hover);
|
||||||
|
box-shadow: var(--nv-elevation-inset-sm), var(--nv-ledge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,7 +109,7 @@
|
||||||
/* Today is ringed, not filled: filled means chosen. */
|
/* Today is ringed, not filled: filled means chosen. */
|
||||||
.nv-calendar__day[aria-current="date"] {
|
.nv-calendar__day[aria-current="date"] {
|
||||||
color: var(--nv-color-accent-text);
|
color: var(--nv-color-accent-text);
|
||||||
box-shadow: inset 0 0 0 var(--nv-border-width-strong) var(--nv-color-accent);
|
box-shadow: inset 0 0 0 var(--nv-border-width-strong) var(--nv-color-accent), var(--nv-elevation-inset-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The chosen day is a small raised key, lit like the filled buttons. */
|
/* The chosen day is a small raised key, lit like the filled buttons. */
|
||||||
|
|
@ -129,3 +133,14 @@
|
||||||
color: var(--nv-color-text-disabled);
|
color: var(--nv-color-text-disabled);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A day pressed into place springs up to full size. Only when it was just
|
||||||
|
chosen: a selection that is merely *there* on load stays still. */
|
||||||
|
.nv-calendar__day[aria-selected="true"][data-nv-pop] {
|
||||||
|
animation: nv-day-pop var(--nv-duration-quick) var(--nv-ease-spring);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes nv-day-pop {
|
||||||
|
from { scale: 0.72; }
|
||||||
|
to { scale: 1; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
box-shadow: var(--nv-outline), var(--nv-elevation-2);
|
box-shadow: var(--nv-outline), var(--nv-elevation-2);
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
transition: scale var(--nv-duration-fast) var(--nv-ease-standard);
|
transition: scale var(--nv-duration-quick) var(--nv-ease-spring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nv-slider::-moz-range-thumb {
|
.nv-slider::-moz-range-thumb {
|
||||||
|
|
@ -87,12 +87,17 @@
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--nv-radius-full);
|
border-radius: var(--nv-radius-full);
|
||||||
box-shadow: var(--nv-outline), var(--nv-elevation-2);
|
box-shadow: var(--nv-outline), var(--nv-elevation-2);
|
||||||
transition: scale var(--nv-duration-fast) var(--nv-ease-standard);
|
transition: scale var(--nv-duration-quick) var(--nv-ease-spring);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover only where there is a pointer to hover with; touch would leave the
|
||||||
|
thumb enlarged after a tap. */
|
||||||
|
@media (hover: hover) {
|
||||||
|
.nv-slider:hover::-webkit-slider-thumb { scale: 1.06; }
|
||||||
|
.nv-slider:hover::-moz-range-thumb { scale: 1.06; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.nv-slider:hover::-webkit-slider-thumb { scale: 1.06; }
|
|
||||||
.nv-slider:active::-webkit-slider-thumb { scale: 0.96; }
|
.nv-slider:active::-webkit-slider-thumb { scale: 0.96; }
|
||||||
.nv-slider:hover::-moz-range-thumb { scale: 1.06; }
|
|
||||||
.nv-slider:active::-moz-range-thumb { scale: 0.96; }
|
.nv-slider:active::-moz-range-thumb { scale: 0.96; }
|
||||||
|
|
||||||
.nv-slider:focus-visible::-webkit-slider-thumb {
|
.nv-slider:focus-visible::-webkit-slider-thumb {
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@
|
||||||
border: var(--nv-border-width) solid transparent;
|
border: var(--nv-border-width) solid transparent;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
var(--nv-highlight-top-strong),
|
var(--nv-highlight-top-strong),
|
||||||
inset 0 0 1.5rem -0.5rem var(--nv-color-glaze-specular),
|
var(--nv-glaze-glow),
|
||||||
var(--nv-outline),
|
var(--nv-outline),
|
||||||
var(--nv-elevation-4);
|
var(--nv-elevation-4);
|
||||||
backdrop-filter:
|
backdrop-filter:
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@
|
||||||
Glaze with something moving behind it.
|
Glaze with something moving behind it.
|
||||||
|
|
||||||
The format is 24-hour unless told otherwise. With data-nv-format-toggle a
|
The format is 24-hour unless told otherwise. With data-nv-format-toggle a
|
||||||
24H / AM/PM switch sits under the wheels and adds an AM/PM wheel when chosen;
|
third wheel reads 24H / AM / PM: swipe it to AM or PM and the hour wheel
|
||||||
the value stays 24-hour either way, so a form never has to know which the
|
becomes 1–12; swipe back to 24H and it returns to 0–23. The value stays
|
||||||
person used.
|
24-hour either way, so a form never has to know which the person used.
|
||||||
|
|
||||||
Without niveus.js the element is empty. For a no-script fallback, use a
|
Without niveus.js the element is empty. For a no-script fallback, use a
|
||||||
native <input type="time" class="nv-input">.
|
native <input type="time" class="nv-input">.
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--nv-space-2);
|
gap: var(--nv-space-2);
|
||||||
padding: var(--nv-space-2) var(--nv-space-3) var(--nv-space-3);
|
padding: var(--nv-space-2) var(--nv-space-3);
|
||||||
border-radius: var(--nv-radius-xl);
|
border-radius: var(--nv-radius-xl);
|
||||||
color: var(--nv-color-text-primary);
|
color: var(--nv-color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
@ -42,8 +42,10 @@
|
||||||
gap: var(--nv-space-1);
|
gap: var(--nv-space-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The lens. It belongs to the wheels, not the picker, so the switch beneath
|
/* The lens: a bar of tinted glass lying across the centre row. It is raised
|
||||||
them never moves the centre. */
|
like the keys elsewhere in the system — lit on top, shaded below, with a
|
||||||
|
contact line — not a flat band of colour, so it reads as a thing the wheels
|
||||||
|
pass under. */
|
||||||
.nv-timepicker__wheels::before {
|
.nv-timepicker__wheels::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -52,8 +54,9 @@
|
||||||
block-size: var(--_row);
|
block-size: var(--_row);
|
||||||
translate: 0 -50%;
|
translate: 0 -50%;
|
||||||
background-color: var(--nv-color-selection);
|
background-color: var(--nv-color-selection);
|
||||||
|
background-image: var(--nv-sheen-raised);
|
||||||
border-radius: var(--nv-radius-md);
|
border-radius: var(--nv-radius-md);
|
||||||
box-shadow: var(--nv-highlight-top);
|
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,6 +77,9 @@
|
||||||
currentcolor 72%,
|
currentcolor 72%,
|
||||||
transparent
|
transparent
|
||||||
);
|
);
|
||||||
|
/* Depth for the drum effect below: rows turn away from the viewer as they
|
||||||
|
leave the centre. */
|
||||||
|
perspective: 14rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nv-timepicker__column::-webkit-scrollbar {
|
.nv-timepicker__column::-webkit-scrollbar {
|
||||||
|
|
@ -96,28 +102,43 @@
|
||||||
font-size: var(--nv-font-size-lg);
|
font-size: var(--nv-font-size-lg);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
/* --nv-pull is how far the wheel has been dragged past its end. Letting go
|
||||||
|
removes it, and the spring easing carries the rows back with a small
|
||||||
|
overshoot — the way a phone's list gives at its limit and settles. */
|
||||||
|
translate: 0 var(--nv-pull, 0);
|
||||||
|
transition:
|
||||||
|
var(--nv-transition-colors),
|
||||||
|
translate var(--nv-duration-slower) var(--nv-ease-spring);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nv-timepicker__column[data-nv-pulling] .nv-timepicker__option {
|
||||||
transition: var(--nv-transition-colors);
|
transition: var(--nv-transition-colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A drum: each row shrinks, fades and tilts away in proportion to how far it
|
||||||
|
is from the lens. Driven by the scroll position itself, so it needs no
|
||||||
|
script and tracks a finger exactly. Where scroll-driven animation is missing
|
||||||
|
the rows are simply flat, which is still correct. */
|
||||||
|
@supports (animation-timeline: view()) {
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.nv-timepicker__option {
|
||||||
|
animation: nv-wheel-drum linear both;
|
||||||
|
animation-timeline: view(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes nv-wheel-drum {
|
||||||
|
0% { scale: 0.78; opacity: 0.25; rotate: x 52deg; }
|
||||||
|
50% { scale: 1; opacity: 1; rotate: x 0deg; }
|
||||||
|
100% { scale: 0.78; opacity: 0.25; rotate: x -52deg; }
|
||||||
|
}
|
||||||
|
|
||||||
.nv-timepicker__option[aria-selected="true"] {
|
.nv-timepicker__option[aria-selected="true"] {
|
||||||
color: var(--nv-color-text-primary);
|
color: var(--nv-color-text-primary);
|
||||||
font-weight: var(--nv-font-weight-semibold);
|
font-weight: var(--nv-font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 24H / AM/PM. A button group under the wheels; the pressed one is the format
|
|
||||||
in use. 24-hour is the default, so it starts pressed. */
|
|
||||||
.nv-timepicker__format {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pressed reads as pushed in: the format in use is a key held down. */
|
|
||||||
.nv-timepicker__format .nv-button[aria-pressed="true"] {
|
|
||||||
background-color: var(--nv-color-accent-subtle);
|
|
||||||
background-image: none;
|
|
||||||
color: var(--nv-color-accent-text);
|
|
||||||
box-shadow: var(--nv-elevation-inset-sm), var(--nv-ledge);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nv-timepicker__separator {
|
.nv-timepicker__separator {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
|
|
||||||
190
niveus/niveus.js
190
niveus/niveus.js
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* SPDX-License-Identifier: Apache-2.0 */
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
Niveus — continuity helpers
|
Niveus — continuity helpers
|
||||||
--------------------------------------------------------------------------
|
--------------------------------------------------------------------------
|
||||||
|
|
@ -492,6 +493,8 @@
|
||||||
grid.setAttribute("aria-labelledby", title.id);
|
grid.setAttribute("aria-labelledby", title.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pop = false;
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
if (title) title.textContent = view.toLocaleDateString(locale, { month: "long", year: "numeric" });
|
if (title) title.textContent = view.toLocaleDateString(locale, { month: "long", year: "numeric" });
|
||||||
grid.textContent = "";
|
grid.textContent = "";
|
||||||
|
|
@ -525,6 +528,7 @@
|
||||||
}
|
}
|
||||||
var iso = isoDate(cursor);
|
var iso = isoDate(cursor);
|
||||||
var day = document.createElement("button");
|
var day = document.createElement("button");
|
||||||
|
if (pop && iso === selectedIso) day.dataset.nvPop = "";
|
||||||
day.type = "button";
|
day.type = "button";
|
||||||
day.className = "nv-calendar__day";
|
day.className = "nv-calendar__day";
|
||||||
day.setAttribute("role", "gridcell");
|
day.setAttribute("role", "gridcell");
|
||||||
|
|
@ -558,10 +562,36 @@
|
||||||
var update = function () {
|
var update = function () {
|
||||||
view = new Date(date.getFullYear(), date.getMonth(), 1);
|
view = new Date(date.getFullYear(), date.getMonth(), 1);
|
||||||
render();
|
render();
|
||||||
|
pop = false;
|
||||||
if (focus) focusActive();
|
if (focus) focusActive();
|
||||||
};
|
};
|
||||||
if (monthChanged) transition(update, { direction: direction });
|
update();
|
||||||
else update();
|
if (monthChanged) slide(direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The month slides in from the side it lies on, with the easing and
|
||||||
|
// duration the rest of the system uses, so it collapses with reduced
|
||||||
|
// motion like everything else. Only the grid moves; the page does not.
|
||||||
|
function slide(direction) {
|
||||||
|
if (prefersReducedMotion() || !grid.animate) return;
|
||||||
|
var root = getComputedStyle(document.documentElement);
|
||||||
|
var duration = parseFloat(root.getPropertyValue("--nv-duration-expand"));
|
||||||
|
if (!duration) return;
|
||||||
|
var easing = root.getPropertyValue("--nv-ease-emphasized").trim() || "ease-out";
|
||||||
|
var shift = (direction === "forward" ? 1 : -1) * 28;
|
||||||
|
grid.animate(
|
||||||
|
[
|
||||||
|
{ opacity: 0, translate: shift + "px 0", filter: "blur(3px)" },
|
||||||
|
{ opacity: 1, translate: "0 0", filter: "blur(0)" },
|
||||||
|
],
|
||||||
|
{ duration: duration, easing: easing }
|
||||||
|
);
|
||||||
|
if (title) {
|
||||||
|
title.animate(
|
||||||
|
[{ opacity: 0, translate: shift / 2 + "px 0" }, { opacity: 1, translate: "0 0" }],
|
||||||
|
{ duration: duration, easing: easing }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stepMonth(count) {
|
function stepMonth(count) {
|
||||||
|
|
@ -574,14 +604,18 @@
|
||||||
function choose(button) {
|
function choose(button) {
|
||||||
selected = parseDate(button.dataset.date);
|
selected = parseDate(button.dataset.date);
|
||||||
active = selected;
|
active = selected;
|
||||||
|
pop = true;
|
||||||
calendar.dataset.nvValue = button.dataset.date;
|
calendar.dataset.nvValue = button.dataset.date;
|
||||||
if (button.dataset.outside !== undefined) {
|
if (button.dataset.outside !== undefined) {
|
||||||
show(selected, selected > view ? "forward" : "backward", true);
|
show(selected, selected > view ? "forward" : "backward", true);
|
||||||
} else {
|
} else {
|
||||||
grid.querySelectorAll('[aria-selected="true"]').forEach(function (other) {
|
grid.querySelectorAll('[aria-selected="true"]').forEach(function (other) {
|
||||||
other.removeAttribute("aria-selected");
|
other.removeAttribute("aria-selected");
|
||||||
|
delete other.dataset.nvPop;
|
||||||
});
|
});
|
||||||
button.setAttribute("aria-selected", "true");
|
button.setAttribute("aria-selected", "true");
|
||||||
|
button.dataset.nvPop = "";
|
||||||
|
pop = false;
|
||||||
}
|
}
|
||||||
calendar.dispatchEvent(new CustomEvent("nv:date", {
|
calendar.dispatchEvent(new CustomEvent("nv:date", {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
|
|
@ -632,7 +666,8 @@
|
||||||
* Builds and drives one .nv-timepicker from data-*: nv-value (HH:MM, 24h),
|
* Builds and drives one .nv-timepicker from data-*: nv-value (HH:MM, 24h),
|
||||||
* nv-hours ("24", the default, or "12" for an AM/PM wheel), nv-step (minute
|
* nv-hours ("24", the default, or "12" for an AM/PM wheel), nv-step (minute
|
||||||
* interval), nv-name (adds a hidden input for forms) and nv-format-toggle
|
* interval), nv-name (adds a hidden input for forms) and nv-format-toggle
|
||||||
* (adds a 24H / AM/PM switch under the wheels). The value is always 24-hour,
|
* (adds a third wheel of 24H / AM / PM: swiping it changes the format). The
|
||||||
|
* value is always 24-hour,
|
||||||
* whatever the wheels show. Fires `nv:time` with { value } as the wheels
|
* whatever the wheels show. Fires `nv:time` with { value } as the wheels
|
||||||
* settle on a new row.
|
* settle on a new row.
|
||||||
*/
|
*/
|
||||||
|
|
@ -652,9 +687,10 @@
|
||||||
|
|
||||||
var wheels = document.createElement("div");
|
var wheels = document.createElement("div");
|
||||||
wheels.className = "nv-timepicker__wheels";
|
wheels.className = "nv-timepicker__wheels";
|
||||||
var hourColumn, minuteColumn, periodColumn;
|
var hourColumn, minuteColumn, periodColumn, formatColumn;
|
||||||
var field = null;
|
var field = null;
|
||||||
var formatButtons = [];
|
var modeTimer = 0;
|
||||||
|
var withFormat = picker.hasAttribute("data-nv-format-toggle");
|
||||||
var placed = false;
|
var placed = false;
|
||||||
var last = "";
|
var last = "";
|
||||||
var frame = 0;
|
var frame = 0;
|
||||||
|
|
@ -680,10 +716,22 @@
|
||||||
var hi = indexOf(hourColumn);
|
var hi = indexOf(hourColumn);
|
||||||
var mi = indexOf(minuteColumn);
|
var mi = indexOf(minuteColumn);
|
||||||
if (hi < 0 || mi < 0) return;
|
if (hi < 0 || mi < 0) return;
|
||||||
var pm = periodColumn ? indexOf(periodColumn) === 1 : false;
|
var fi = formatColumn ? indexOf(formatColumn) : -1;
|
||||||
|
if (formatColumn && fi < 0) return;
|
||||||
|
|
||||||
|
// The format wheel disagrees with the hour wheel: a new format was
|
||||||
|
// swiped to. Wait for it to settle, then rebuild the hours for it.
|
||||||
|
if (formatColumn && (fi > 0) !== twelve) {
|
||||||
|
global.clearTimeout(modeTimer);
|
||||||
|
modeTimer = global.setTimeout(switchFormat, 160);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pm = formatColumn ? fi === 2 : periodColumn ? indexOf(periodColumn) === 1 : false;
|
||||||
mark(hourColumn, hi);
|
mark(hourColumn, hi);
|
||||||
mark(minuteColumn, mi);
|
mark(minuteColumn, mi);
|
||||||
if (periodColumn) mark(periodColumn, pm ? 1 : 0);
|
if (formatColumn) mark(formatColumn, fi);
|
||||||
|
else if (periodColumn) mark(periodColumn, pm ? 1 : 0);
|
||||||
|
|
||||||
var hour = twelve ? ((hi + 1) % 12) + (pm ? 12 : 0) : hi;
|
var hour = twelve ? ((hi + 1) % 12) + (pm ? 12 : 0) : hi;
|
||||||
var value = pad(hour) + ":" + minuteItems[mi];
|
var value = pad(hour) + ":" + minuteItems[mi];
|
||||||
|
|
@ -708,10 +756,24 @@
|
||||||
var mi = Math.round(minute / step) % minuteItems.length;
|
var mi = Math.round(minute / step) % minuteItems.length;
|
||||||
scrollTo(hourColumn, hi, false);
|
scrollTo(hourColumn, hi, false);
|
||||||
scrollTo(minuteColumn, mi, false);
|
scrollTo(minuteColumn, mi, false);
|
||||||
if (periodColumn) scrollTo(periodColumn, h24 >= 12 ? 1 : 0, false);
|
if (formatColumn) scrollTo(formatColumn, twelve ? (h24 >= 12 ? 2 : 1) : 0, false);
|
||||||
|
else if (periodColumn) scrollTo(periodColumn, h24 >= 12 ? 1 : 0, false);
|
||||||
read();
|
read();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Going to 12-hour takes the half of the day that was swiped to; going back
|
||||||
|
// to 24-hour keeps the moment, which the hour wheel has been tracking.
|
||||||
|
function switchFormat() {
|
||||||
|
var fi = indexOf(formatColumn);
|
||||||
|
if (fi < 0) return;
|
||||||
|
var wants = fi > 0;
|
||||||
|
if (wants === twelve) { read(); return; }
|
||||||
|
if (wants) h24 = (h24 % 12) + (fi === 2 ? 12 : 0);
|
||||||
|
twelve = wants;
|
||||||
|
picker.dataset.nvHours = twelve ? "12" : "24";
|
||||||
|
build();
|
||||||
|
}
|
||||||
|
|
||||||
function buildColumn(label, items) {
|
function buildColumn(label, items) {
|
||||||
var list = document.createElement("ul");
|
var list = document.createElement("ul");
|
||||||
list.className = "nv-timepicker__column";
|
list.className = "nv-timepicker__column";
|
||||||
|
|
@ -726,9 +788,51 @@
|
||||||
list.appendChild(option);
|
list.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Past either end the wheel gives, as a phone's list does, then springs
|
||||||
|
// back. Touch is left to the platform (iOS bounces, Android stretches);
|
||||||
|
// overscroll-behavior keeps that inside the wheel. Mouse wheels and
|
||||||
|
// trackpads, which have no such effect on a nested scroller, are handled
|
||||||
|
// here. The distance has resistance: the further it is pulled, the less
|
||||||
|
// each notch moves it.
|
||||||
|
var pull = 0;
|
||||||
|
var settle = 0;
|
||||||
|
var limit = 36;
|
||||||
|
|
||||||
|
function setPull(value) {
|
||||||
|
pull = value;
|
||||||
|
list.style.setProperty("--nv-pull", value.toFixed(1) + "px");
|
||||||
|
list.dataset.nvPulling = "";
|
||||||
|
global.clearTimeout(settle);
|
||||||
|
settle = global.setTimeout(function () {
|
||||||
|
pull = 0;
|
||||||
|
delete list.dataset.nvPulling;
|
||||||
|
list.style.removeProperty("--nv-pull");
|
||||||
|
}, 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.addEventListener("wheel", function (event) {
|
||||||
|
if (prefersReducedMotion()) return;
|
||||||
|
var atTop = list.scrollTop <= 0;
|
||||||
|
var atBottom = list.scrollTop >= list.scrollHeight - list.clientHeight - 1;
|
||||||
|
// Chromium reports a wheel notch in pixels (~100); Firefox reports it
|
||||||
|
// in lines (~3). Both are brought to pixels, or the pull would be
|
||||||
|
// invisible in one of them.
|
||||||
|
var delta = event.deltaY * (event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? list.clientHeight : 1);
|
||||||
|
var pushing = (atTop && delta < 0) || (atBottom && delta > 0);
|
||||||
|
if (!pushing && !pull) return;
|
||||||
|
event.preventDefault();
|
||||||
|
var next = pull - delta * 0.12 * (1 - Math.abs(pull) / limit);
|
||||||
|
setPull(Math.max(-limit, Math.min(limit, next)));
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
// A key pressed at the end nudges the wheel instead of doing nothing.
|
||||||
|
list.nvNudge = function (direction) {
|
||||||
|
if (!prefersReducedMotion()) setPull(direction * 12);
|
||||||
|
};
|
||||||
|
|
||||||
list.addEventListener("scroll", function () {
|
list.addEventListener("scroll", function () {
|
||||||
if (frame) return;
|
if (frame) return;
|
||||||
frame = global.requestAnimationFrame(function () { frame = 0; read(); });
|
frame = global.requestAnimationFrame(function () { frame = 0; read(); drum(list); });
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
list.addEventListener("click", function (event) {
|
list.addEventListener("click", function (event) {
|
||||||
|
|
@ -750,11 +854,41 @@
|
||||||
default: return;
|
default: return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
scrollTo(list, Math.max(0, Math.min(end, to)), true);
|
var clamped = Math.max(0, Math.min(end, to));
|
||||||
|
if (clamped === index) list.nvNudge(to < index ? 1 : -1);
|
||||||
|
scrollTo(list, clamped, true);
|
||||||
});
|
});
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The drum effect is a CSS scroll-driven animation (see timepicker.css).
|
||||||
|
// Where that does not exist — Firefox — the same curve is applied from the
|
||||||
|
// scroll position here, so the wheels look the same in both. Only the few
|
||||||
|
// rows near the lens are touched.
|
||||||
|
var scrollDriven = global.CSS && global.CSS.supports && global.CSS.supports("animation-timeline", "view()");
|
||||||
|
|
||||||
|
function drum(list) {
|
||||||
|
if (scrollDriven || prefersReducedMotion()) return;
|
||||||
|
var first = list.firstElementChild;
|
||||||
|
var row = first ? first.offsetHeight : 0;
|
||||||
|
if (!row) return;
|
||||||
|
var centre = list.scrollTop / row;
|
||||||
|
var reach = list.clientHeight + row;
|
||||||
|
for (var i = 0; i < list.children.length; i++) {
|
||||||
|
var away = i - centre;
|
||||||
|
var option = list.children[i];
|
||||||
|
if (Math.abs(away) > 3) {
|
||||||
|
option.style.opacity = "0.25";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 1 at the lens, 0 at the edge of the wheel; the CSS keyframes, spelt out.
|
||||||
|
var near = 1 - Math.min(1, Math.abs(away) * row / reach * 2);
|
||||||
|
option.style.scale = String(0.78 + 0.22 * near);
|
||||||
|
option.style.opacity = String(0.25 + 0.75 * near);
|
||||||
|
option.style.rotate = "x " + ((away < 0 ? 1 : -1) * 52 * (1 - near)).toFixed(1) + "deg";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function colon() {
|
function colon() {
|
||||||
var mark = document.createElement("span");
|
var mark = document.createElement("span");
|
||||||
mark.className = "nv-timepicker__separator";
|
mark.className = "nv-timepicker__separator";
|
||||||
|
|
@ -773,17 +907,19 @@
|
||||||
wheels.textContent = "";
|
wheels.textContent = "";
|
||||||
hourColumn = buildColumn("Hours", hourItems);
|
hourColumn = buildColumn("Hours", hourItems);
|
||||||
minuteColumn = buildColumn("Minutes", minuteItems);
|
minuteColumn = buildColumn("Minutes", minuteItems);
|
||||||
periodColumn = twelve ? buildColumn("AM or PM", ["AM", "PM"]) : null;
|
formatColumn = withFormat ? buildColumn("Time format", ["24H", "AM", "PM"]) : null;
|
||||||
|
periodColumn = twelve && !withFormat ? buildColumn("AM or PM", ["AM", "PM"]) : null;
|
||||||
wheels.appendChild(hourColumn);
|
wheels.appendChild(hourColumn);
|
||||||
wheels.appendChild(colon());
|
wheels.appendChild(colon());
|
||||||
wheels.appendChild(minuteColumn);
|
wheels.appendChild(minuteColumn);
|
||||||
if (periodColumn) wheels.appendChild(periodColumn);
|
if (formatColumn) wheels.appendChild(formatColumn);
|
||||||
|
else if (periodColumn) wheels.appendChild(periodColumn);
|
||||||
|
|
||||||
formatButtons.forEach(function (button) {
|
|
||||||
button.setAttribute("aria-pressed", String((button.dataset.hours === "12") === twelve));
|
|
||||||
});
|
|
||||||
placed = false;
|
placed = false;
|
||||||
place();
|
place();
|
||||||
|
[hourColumn, minuteColumn, formatColumn || periodColumn].forEach(function (list) {
|
||||||
|
if (list) drum(list);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
picker.textContent = "";
|
picker.textContent = "";
|
||||||
|
|
@ -793,30 +929,6 @@
|
||||||
}
|
}
|
||||||
picker.appendChild(wheels);
|
picker.appendChild(wheels);
|
||||||
|
|
||||||
if (picker.hasAttribute("data-nv-format-toggle")) {
|
|
||||||
var format = document.createElement("div");
|
|
||||||
format.className = "nv-timepicker__format nv-button-group";
|
|
||||||
format.setAttribute("role", "group");
|
|
||||||
format.setAttribute("aria-label", "Time format");
|
|
||||||
[["24", "24H"], ["12", "AM/PM"]].forEach(function (option) {
|
|
||||||
var button = document.createElement("button");
|
|
||||||
button.type = "button";
|
|
||||||
button.className = "nv-button nv-button--sm";
|
|
||||||
button.dataset.hours = option[0];
|
|
||||||
button.textContent = option[1];
|
|
||||||
button.addEventListener("click", function () {
|
|
||||||
var wants = option[0] === "12";
|
|
||||||
if (wants === twelve) return;
|
|
||||||
twelve = wants;
|
|
||||||
picker.dataset.nvHours = option[0];
|
|
||||||
build();
|
|
||||||
});
|
|
||||||
formatButtons.push(button);
|
|
||||||
format.appendChild(button);
|
|
||||||
});
|
|
||||||
picker.appendChild(format);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (picker.dataset.nvName) {
|
if (picker.dataset.nvName) {
|
||||||
field = document.createElement("input");
|
field = document.createElement("input");
|
||||||
field.type = "hidden";
|
field.type = "hidden";
|
||||||
|
|
|
||||||
|
|
@ -32,16 +32,18 @@
|
||||||
:root,
|
:root,
|
||||||
[data-nv-brand] {
|
[data-nv-brand] {
|
||||||
/* --- Canvas and surfaces -----------------------------------------------
|
/* --- Canvas and surfaces -----------------------------------------------
|
||||||
Paper stays paper: the two surfaces that hold content are left untinted so
|
Every neutral carries a trace of the brand, content surfaces included, the
|
||||||
text keeps its contrast. Everything the surfaces sit *in* picks up a trace
|
way dark does: that is what stops an accent from looking stuck on, and what
|
||||||
of the brand, which is what stops an accent from looking stuck on. */
|
makes the two appearances read as one material. The paper is a soft
|
||||||
--nv-light-background: color-mix(in oklab, #e8e5de, var(--nv-brand) var(--nv-brand-tint));
|
grey-stone, not white — a bright field glares, and flattens the bevels that
|
||||||
--nv-light-background-subtle: color-mix(in oklab, #eeece6, var(--nv-brand) var(--nv-brand-tint));
|
are meant to shape it. Text keeps its contrast: the primary is neutral. */
|
||||||
--nv-light-surface: #fbfbfa;
|
--nv-light-background: color-mix(in oklab, #d8d5ce, var(--nv-brand) calc(var(--nv-brand-tint) * 2));
|
||||||
--nv-light-surface-elevated: #f8f8f6;
|
--nv-light-background-subtle: color-mix(in oklab, #dedbd4, var(--nv-brand) calc(var(--nv-brand-tint) * 2));
|
||||||
--nv-light-surface-inset: color-mix(in oklab, #eeede8, var(--nv-brand) var(--nv-brand-tint));
|
--nv-light-surface: color-mix(in oklab, #eeede9, var(--nv-brand) var(--nv-brand-tint));
|
||||||
--nv-light-surface-hover: color-mix(in oklab, #f1f0ec, var(--nv-brand) var(--nv-brand-tint));
|
--nv-light-surface-elevated: color-mix(in oklab, #f2f1ed, var(--nv-brand) var(--nv-brand-tint));
|
||||||
--nv-light-surface-active: color-mix(in oklab, #e6e5e0, var(--nv-brand) var(--nv-brand-tint));
|
--nv-light-surface-inset: color-mix(in oklab, #dddbd4, var(--nv-brand) calc(var(--nv-brand-tint) * 2));
|
||||||
|
--nv-light-surface-hover: color-mix(in oklab, #e6e4de, var(--nv-brand) var(--nv-brand-tint));
|
||||||
|
--nv-light-surface-active: color-mix(in oklab, #dad8d1, var(--nv-brand) var(--nv-brand-tint));
|
||||||
--nv-light-scrim: rgb(28 24 18 / 0.34);
|
--nv-light-scrim: rgb(28 24 18 / 0.34);
|
||||||
|
|
||||||
/* --- Text ---------------------------------------------------------------
|
/* --- Text ---------------------------------------------------------------
|
||||||
|
|
@ -50,7 +52,7 @@
|
||||||
loss of legibility. */
|
loss of legibility. */
|
||||||
--nv-light-text-primary: var(--nv-palette-neutral-900);
|
--nv-light-text-primary: var(--nv-palette-neutral-900);
|
||||||
--nv-light-text-secondary: color-mix(in oklab, var(--nv-palette-neutral-600), var(--nv-brand) 8%);
|
--nv-light-text-secondary: color-mix(in oklab, var(--nv-palette-neutral-600), var(--nv-brand) 8%);
|
||||||
--nv-light-text-tertiary: color-mix(in oklab, var(--nv-palette-neutral-500), var(--nv-brand) 8%);
|
--nv-light-text-tertiary: color-mix(in oklab, color-mix(in oklab, var(--nv-palette-neutral-500), var(--nv-palette-neutral-600) 40%), var(--nv-brand) 8%);
|
||||||
--nv-light-text-disabled: var(--nv-palette-neutral-400);
|
--nv-light-text-disabled: var(--nv-palette-neutral-400);
|
||||||
--nv-light-text-inverted: var(--nv-palette-neutral-0);
|
--nv-light-text-inverted: var(--nv-palette-neutral-0);
|
||||||
|
|
||||||
|
|
@ -135,20 +137,20 @@
|
||||||
/* --- Depth and material -------------------------------------------------
|
/* --- Depth and material -------------------------------------------------
|
||||||
Three shadow colors build every elevation level: an ambient contact
|
Three shadow colors build every elevation level: an ambient contact
|
||||||
shadow, a soft penumbra, and a deeper umbra for high elevations. */
|
shadow, a soft penumbra, and a deeper umbra for high elevations. */
|
||||||
--nv-light-shadow-ambient: rgb(40 32 20 / 0.08);
|
--nv-light-shadow-ambient: rgb(32 28 22 / 0.12);
|
||||||
--nv-light-shadow-penumbra: rgb(40 32 20 / 0.1);
|
--nv-light-shadow-penumbra: rgb(32 28 22 / 0.15);
|
||||||
--nv-light-shadow-umbra: rgb(40 32 20 / 0.16);
|
--nv-light-shadow-umbra: rgb(32 28 22 / 0.24);
|
||||||
--nv-light-shadow-inset: rgb(40 32 20 / 0.14);
|
--nv-light-shadow-inset: rgb(32 28 22 / 0.2);
|
||||||
--nv-light-highlight: rgb(255 255 255 / 0.9);
|
--nv-light-highlight: rgb(255 255 255 / 0.7);
|
||||||
--nv-light-highlight-strong: rgb(255 255 255 / 1);
|
--nv-light-highlight-strong: rgb(255 255 255 / 0.85);
|
||||||
|
|
||||||
/* Paper already has a border doing the contact work, so the outline only
|
/* Paper already has a border doing the contact work, so the outline only
|
||||||
softens it; the bottom shade is faint for the same reason. */
|
softens it; the bottom shade is faint for the same reason. */
|
||||||
--nv-light-lowlight: rgb(40 32 20 / 0.09);
|
--nv-light-lowlight: rgb(32 28 22 / 0.14);
|
||||||
--nv-light-outline: rgb(40 32 20 / 0.05);
|
--nv-light-outline: rgb(32 28 22 / 0.08);
|
||||||
|
|
||||||
--nv-light-sheen-top: rgb(255 255 255 / 0.9);
|
--nv-light-sheen-top: rgb(255 255 255 / 0.55);
|
||||||
--nv-light-sheen-bottom: rgb(40 32 20 / 0.07);
|
--nv-light-sheen-bottom: rgb(32 28 22 / 0.11);
|
||||||
--nv-light-sheen-strong-top: rgb(255 255 255 / 0.32);
|
--nv-light-sheen-strong-top: rgb(255 255 255 / 0.32);
|
||||||
--nv-light-sheen-strong-bottom: rgb(20 16 10 / 0.16);
|
--nv-light-sheen-strong-bottom: rgb(20 16 10 / 0.16);
|
||||||
--nv-light-glow: oklch(from var(--nv-light-accent) l c h / 0.3);
|
--nv-light-glow: oklch(from var(--nv-light-accent) l c h / 0.3);
|
||||||
|
|
@ -164,16 +166,16 @@
|
||||||
--nv-light-elevation-tint-4: transparent;
|
--nv-light-elevation-tint-4: transparent;
|
||||||
--nv-light-elevation-tint-5: transparent;
|
--nv-light-elevation-tint-5: transparent;
|
||||||
|
|
||||||
--nv-light-material-frosted: rgb(252 252 250 / 0.56);
|
--nv-light-material-frosted: color-mix(in oklab, rgb(228 227 222 / 0.6), var(--nv-brand) var(--nv-brand-tint));
|
||||||
--nv-light-material-glass: rgb(252 252 250 / 0.4);
|
--nv-light-material-glass: color-mix(in oklab, rgb(228 227 222 / 0.44), var(--nv-brand) var(--nv-brand-tint));
|
||||||
--nv-light-material-border: rgb(255 255 255 / 0.72);
|
--nv-light-material-border: rgb(255 255 255 / 0.5);
|
||||||
|
|
||||||
/* Glaze: the tint it is made of, the rim that catches light on the side
|
/* Glaze: the tint it is made of, the rim that catches light on the side
|
||||||
facing the pointer, the rim on the far side, and the specular bloom. */
|
facing the pointer, the rim on the far side, and the specular bloom. */
|
||||||
--nv-light-material-glaze: rgb(255 255 255 / 1);
|
--nv-light-material-glaze: color-mix(in oklab, rgb(232 231 226 / 1), var(--nv-brand) calc(var(--nv-brand-tint) * 2));
|
||||||
--nv-light-glaze-rim-lit: rgb(255 255 255 / 1);
|
--nv-light-glaze-rim-lit: rgb(255 255 255 / 0.9);
|
||||||
--nv-light-glaze-rim-far: rgb(255 255 255 / 0.5);
|
--nv-light-glaze-rim-far: rgb(255 255 255 / 0.4);
|
||||||
--nv-light-glaze-specular: rgb(255 255 255 / 0.6);
|
--nv-light-glaze-specular: rgb(255 255 255 / 0.4);
|
||||||
|
|
||||||
/* --- Window controls --------------------------------------------------------- */
|
/* --- Window controls --------------------------------------------------------- */
|
||||||
--nv-light-window-close: var(--nv-palette-window-close);
|
--nv-light-window-close: var(--nv-palette-window-close);
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,9 @@
|
||||||
--nv-glaze-tint-min: 4%; /* tint opacity at frost 0 */
|
--nv-glaze-tint-min: 4%; /* tint opacity at frost 0 */
|
||||||
--nv-glaze-tint-max: 78%; /* tint opacity at frost 1 */
|
--nv-glaze-tint-max: 78%; /* tint opacity at frost 1 */
|
||||||
--nv-glaze-rim: 1.5px;
|
--nv-glaze-rim: 1.5px;
|
||||||
|
/* The light a pane holds just inside its edge, so it reads as a thick lens
|
||||||
|
rather than a flat film. */
|
||||||
|
--nv-glaze-glow: inset 0 0 1.5rem -0.5rem var(--nv-color-glaze-specular);
|
||||||
|
|
||||||
/* --- Sheen ------------------------------------------------------------------
|
/* --- Sheen ------------------------------------------------------------------
|
||||||
raised neutral controls: buttons, the selected tab, a titlebar
|
raised neutral controls: buttons, the selected tab, a titlebar
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue