mirror of
https://github.com/Nexoniarz/Niveus.git
synced 2026-11-03 04:34:26 +00:00
Fix Firefox: wheels, dark shadows, theme glass, smooth jumps, tri-state
Found by driving Firefox 158 with real pointer, wheel and key input. - Time picker wheels used padding to reach the last rows. Firefox leaves bottom padding out of the scrollable area, so the last rows were unreachable and the 24H/AM/PM wheel could not scroll at all. Spacer pseudo-elements replace the padding. - Wheels can now be dragged with a mouse or pen, with inertia, a rubber-band at the ends and a spring back. Touch is left to the platform. - Theme changes no longer use a page-wide view transition. The snapshot could not blur what sits behind frosted glass, so every frosted surface went clear for the length of the cross-fade. - Dark shadows were invisible: black on a near-black canvas, with a deep negative spread tucking the blur under the element. The dark canvas is lifted, the shadows are stronger and the spreads milder. - Same-page links now travel (scroll-behavior: smooth, motion permitting). - data-nv-tristate: a checkbox that cycles unchecked, checked, partial; the browser can display a partial state but a click can never choose it. - Button groups show their pressed segment. GUIDELINES.md gains the cross-engine rules these defects taught, and makes Firefox the first browser to verify in. 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
06fca1c385
commit
65d81481bd
9 changed files with 253 additions and 48 deletions
|
|
@ -533,6 +533,46 @@ tune. This is a first-class goal, bounded by sections 3.1 and 11.
|
|||
5. **One listener, one write per frame** for pointer-driven effects.
|
||||
6. **ES5-compatible style** in `niveus.js`, to match the file.
|
||||
|
||||
### 14.4 Cross-engine rules
|
||||
|
||||
Each of these was a real defect, found by driving Firefox with real input.
|
||||
|
||||
1. **Never wrap an appearance change in a page-wide view transition.** The
|
||||
snapshot cannot blur what sits behind frosted glass, so for its whole
|
||||
duration every frosted bar, menu and Glaze pane goes clear and shows the page
|
||||
sharply through itself. Let surfaces transition their own colours. Selection
|
||||
markers still use a short view transition; keep it brief, and prefer a
|
||||
per-element animation for anything inside frosted chrome.
|
||||
2. **Do not use padding to make a scroll container's last item reachable.**
|
||||
Firefox leaves the container's bottom padding out of its scrollable area, so
|
||||
the final rows were unreachable and a short list could not scroll at all. Use
|
||||
real boxes — `::before` / `::after` spacers — which every engine counts.
|
||||
3. **Normalise wheel deltas.** Chromium reports a notch in pixels (~100);
|
||||
Firefox reports it in lines (~3). Convert with `deltaMode` before using
|
||||
`deltaY`, or an effect that works in one engine is invisible in the other.
|
||||
4. **A scrollable list is not draggable on desktop.** Touch scrolls natively; a
|
||||
mouse does not. Anything that is meant to feel like a phone control (a wheel,
|
||||
a carousel) needs pointer-drag handling, with inertia, an edge that gives, and
|
||||
a click that still works when the pointer did not move.
|
||||
5. **Shadows need something to darken.** A black shadow on a near-black canvas
|
||||
is invisible, however correct its computed value. The dark canvas is
|
||||
deliberately lighter than the shadow colour, and soft layers use only a mild
|
||||
negative spread so the blur reaches outside the element.
|
||||
6. **Where a CSS feature is missing, supply the same curve in script** (scroll-
|
||||
driven animation and `interpolate-size` are absent in Firefox), and keep the
|
||||
plain state correct without either.
|
||||
7. **State that the browser only half-supports needs an attribute.** A checkbox
|
||||
can *show* a partial state but a click never *chooses* it;
|
||||
`data-nv-tristate` makes the cycle explicit.
|
||||
8. **Same-page links travel.** `scroll-behavior: smooth` under
|
||||
`prefers-reduced-motion: no-preference`; a jump to a section is the textbook
|
||||
change a user could have watched.
|
||||
9. **Know your test rig.** A headless browser without a GPU does not render
|
||||
`backdrop-filter`, reports no hover capability (so hover-only controls such
|
||||
as scroller arrows disappear), and may not deliver scroll events reliably.
|
||||
Treat those as limits of the rig, not of the system, and confirm them on a
|
||||
real display.
|
||||
|
||||
### 14.3 Documentation
|
||||
|
||||
Every new class, token, attribute, function or event is added to `REFERENCE.md`
|
||||
|
|
@ -604,9 +644,13 @@ Niveus code, follow it literally.
|
|||
python3 tools/build.py --check # dist/ matches sources
|
||||
```
|
||||
|
||||
7. **Look at it.** Render both appearances in a real browser at a realistic size
|
||||
and inspect the result. Passing checks prove the rules; they do not prove it
|
||||
looks right or works. Exercise interactive components, not just their markup.
|
||||
7. **Look at it, in Firefox first.** Chromium implements nearly everything
|
||||
Niveus uses, so passing there proves little; Firefox is the stricter test and
|
||||
is where regressions have actually appeared. Render both appearances at a
|
||||
realistic size and exercise the interactive parts with *real input* — pointer
|
||||
drags, wheel notches, key presses — not only synthetic events or static
|
||||
screenshots. Passing checks prove the rules; they do not prove it looks right
|
||||
or works. Then confirm in Chromium. See 14.4 for what differs.
|
||||
8. **Report faithfully.** State what you verified, what you could not, and what
|
||||
failed. A test that failed because of the test harness is still reported.
|
||||
|
||||
|
|
@ -651,6 +695,7 @@ A change is done when:
|
|||
- [ ] It is documented in `REFERENCE.md`; `README.md` and this document are
|
||||
updated if a principle or the component list changed.
|
||||
- [ ] It renders correctly in **light and dark**, with and without `niveus.js`.
|
||||
- [ ] It was seen and exercised in **Firefox** with real input, then in Chromium.
|
||||
- [ ] Every interaction state exists and is reachable by keyboard.
|
||||
- [ ] Translucent surfaces have a solid fallback.
|
||||
- [ ] It respects reduced motion and reduced transparency.
|
||||
|
|
@ -714,7 +759,7 @@ Use this for review, human or automated. Every line is a yes/no.
|
|||
- [ ] No `!important`; application CSS is unlayered.
|
||||
- [ ] `REFERENCE.md` updated; `dist/` rebuilt, not hand-edited.
|
||||
- [ ] `build.py`, `check.py`, `build.py --check` pass.
|
||||
- [ ] Seen in a browser; interactive parts exercised.
|
||||
- [ ] Seen and exercised in Firefox with real input, then Chromium.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -527,6 +527,7 @@ everything still renders — state changes simply arrive instead of travelling.
|
|||
| `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 wheel). |
|
||||
| `data-nv-picker-label` | child of a picker trigger | Receives the chosen option's text; default is the trigger itself. |
|
||||
| `data-nv-tristate` | `input[type=checkbox]` | Click cycles unchecked → checked → partial. `="partial"` starts partial. Exposes `data-nv-state` (`off`/`on`/`partial`). |
|
||||
| `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-nv-direction` | `<html>` | Set during a transition by `nv.transition`. |
|
||||
|
|
|
|||
57
dist/niveus.css
vendored
57
dist/niveus.css
vendored
|
|
@ -527,6 +527,12 @@
|
|||
consistent between appearances: an ambient contact shadow that keeps the
|
||||
edge crisp, a soft penumbra for the spread, and an umbra that only appears
|
||||
once a surface is genuinely far from the canvas.
|
||||
|
||||
Two things keep a shadow visible, and both have failed before. The soft
|
||||
layers use only a mild negative spread: a deep one tucks the blur under the
|
||||
element and leaves almost nothing outside it. And the canvas has to be
|
||||
lighter than the shadow colour — black cannot darken black — which is why the
|
||||
dark canvas is not the darkest value it could be.
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
|
|
@ -535,27 +541,27 @@
|
|||
--nv-elevation-1:
|
||||
0 1px 1px var(--nv-color-shadow-ambient),
|
||||
0 2px 4px -1px var(--nv-color-shadow-penumbra),
|
||||
0 6px 14px -8px var(--nv-color-shadow-umbra);
|
||||
0 6px 14px -5px var(--nv-color-shadow-umbra);
|
||||
|
||||
--nv-elevation-2:
|
||||
0 1px 1px var(--nv-color-shadow-ambient),
|
||||
0 3px 6px -2px var(--nv-color-shadow-penumbra),
|
||||
0 10px 20px -10px var(--nv-color-shadow-umbra);
|
||||
0 10px 20px -7px var(--nv-color-shadow-umbra);
|
||||
|
||||
--nv-elevation-3:
|
||||
0 1px 2px var(--nv-color-shadow-ambient),
|
||||
0 6px 12px -4px var(--nv-color-shadow-penumbra),
|
||||
0 18px 36px -12px var(--nv-color-shadow-umbra);
|
||||
0 18px 36px -9px var(--nv-color-shadow-umbra);
|
||||
|
||||
--nv-elevation-4:
|
||||
0 1px 2px var(--nv-color-shadow-ambient),
|
||||
0 10px 20px -6px var(--nv-color-shadow-penumbra),
|
||||
0 30px 60px -16px var(--nv-color-shadow-umbra);
|
||||
0 30px 60px -12px var(--nv-color-shadow-umbra);
|
||||
|
||||
--nv-elevation-5:
|
||||
0 2px 3px var(--nv-color-shadow-ambient),
|
||||
0 16px 32px -8px var(--nv-color-shadow-penumbra),
|
||||
0 44px 88px -20px var(--nv-color-shadow-umbra);
|
||||
0 44px 88px -16px var(--nv-color-shadow-umbra);
|
||||
|
||||
/* --- Inset ---------------------------------------------------------------
|
||||
Recessed surfaces: fields, wells, tracks, pressed controls. */
|
||||
|
|
@ -1093,10 +1099,10 @@
|
|||
:root,
|
||||
[data-nv-brand] {
|
||||
/* --- Canvas and surfaces ----------------------------------------------- */
|
||||
--nv-dark-background: color-mix(in oklab, #0f0f12, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-background-subtle: color-mix(in oklab, #18181b, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface: color-mix(in oklab, #1f1f22, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface-elevated: color-mix(in oklab, #2a2a2e, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-background: color-mix(in oklab, #17171b, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-background-subtle: color-mix(in oklab, #1d1d21, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface: color-mix(in oklab, #232327, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface-elevated: color-mix(in oklab, #2d2d32, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface-inset: color-mix(in oklab, #111113, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface-hover: color-mix(in oklab, #313136, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface-active: color-mix(in oklab, #39393f, var(--nv-brand) var(--nv-brand-tint));
|
||||
|
|
@ -1183,9 +1189,9 @@
|
|||
calc(c * 0.2) h);
|
||||
|
||||
/* --- Depth and material -------------------------------------------------- */
|
||||
--nv-dark-shadow-ambient: rgb(0 0 0 / 0.5);
|
||||
--nv-dark-shadow-penumbra: rgb(0 0 0 / 0.5);
|
||||
--nv-dark-shadow-umbra: rgb(0 0 0 / 0.55);
|
||||
--nv-dark-shadow-ambient: rgb(0 0 0 / 0.6);
|
||||
--nv-dark-shadow-penumbra: rgb(0 0 0 / 0.62);
|
||||
--nv-dark-shadow-umbra: rgb(0 0 0 / 0.72);
|
||||
--nv-dark-shadow-inset: rgb(0 0 0 / 0.65);
|
||||
--nv-dark-highlight: rgb(255 255 255 / 0.15);
|
||||
--nv-dark-highlight-strong: rgb(255 255 255 / 0.24);
|
||||
|
|
@ -1374,6 +1380,13 @@
|
|||
border-radius: var(--nv-radius-xs);
|
||||
}
|
||||
|
||||
/* A link to a place on the same page travels there instead of cutting. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
|
|
@ -4119,8 +4132,11 @@
|
|||
inline-size: 3.5rem;
|
||||
block-size: calc(var(--_row) * var(--_rows));
|
||||
margin: 0;
|
||||
padding: calc(var(--_row) * (var(--_rows) - 1) / 2) 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
touch-action: pan-y;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scroll-snap-type: y mandatory;
|
||||
|
|
@ -4140,6 +4156,21 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
/* Spacers, not padding: they let the first and last rows reach the centre.
|
||||
Firefox leaves the bottom padding of a scroll container out of its
|
||||
scrollable area, which stranded the last rows (and made a three-row wheel
|
||||
unscrollable); real boxes always count. */
|
||||
.nv-timepicker__column::before,
|
||||
.nv-timepicker__column::after {
|
||||
content: "";
|
||||
display: block;
|
||||
block-size: calc(var(--_row) * (var(--_rows) - 1) / 2);
|
||||
}
|
||||
|
||||
.nv-timepicker__column[data-nv-dragging] {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.nv-timepicker__column:focus-visible {
|
||||
outline: none;
|
||||
border-radius: var(--nv-radius-md);
|
||||
|
|
|
|||
|
|
@ -768,7 +768,7 @@
|
|||
Weekly digest
|
||||
</label>
|
||||
<label class="nv-choice">
|
||||
<input class="nv-checkbox" type="checkbox" id="f-indeterminate">
|
||||
<input class="nv-checkbox" type="checkbox" id="f-indeterminate" data-nv-tristate="partial">
|
||||
Partial selection
|
||||
</label>
|
||||
<label class="nv-choice">
|
||||
|
|
@ -1527,7 +1527,6 @@
|
|||
addEventListener("scroll", onScroll, { passive: true });
|
||||
onScroll();
|
||||
|
||||
document.getElementById("f-indeterminate").indeterminate = true;
|
||||
|
||||
/* Build the semantic color list from the tokens themselves, so the page
|
||||
cannot show a role the system does not actually define. */
|
||||
|
|
|
|||
|
|
@ -134,6 +134,13 @@ textarea {
|
|||
border-radius: var(--nv-radius-xs);
|
||||
}
|
||||
|
||||
/* A link to a place on the same page travels there instead of cutting. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
|
|
|
|||
|
|
@ -65,8 +65,11 @@
|
|||
inline-size: 3.5rem;
|
||||
block-size: calc(var(--_row) * var(--_rows));
|
||||
margin: 0;
|
||||
padding: calc(var(--_row) * (var(--_rows) - 1) / 2) 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
touch-action: pan-y;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scroll-snap-type: y mandatory;
|
||||
|
|
@ -86,6 +89,21 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
/* Spacers, not padding: they let the first and last rows reach the centre.
|
||||
Firefox leaves the bottom padding of a scroll container out of its
|
||||
scrollable area, which stranded the last rows (and made a three-row wheel
|
||||
unscrollable); real boxes always count. */
|
||||
.nv-timepicker__column::before,
|
||||
.nv-timepicker__column::after {
|
||||
content: "";
|
||||
display: block;
|
||||
block-size: calc(var(--_row) * (var(--_rows) - 1) / 2);
|
||||
}
|
||||
|
||||
.nv-timepicker__column[data-nv-dragging] {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.nv-timepicker__column:focus-visible {
|
||||
outline: none;
|
||||
border-radius: var(--nv-radius-md);
|
||||
|
|
|
|||
118
niveus/niveus.js
118
niveus/niveus.js
|
|
@ -96,17 +96,19 @@
|
|||
|
||||
/** "light" | "dark" | "system" — system means: follow the environment. */
|
||||
function setTheme(theme) {
|
||||
var done = transition(function () {
|
||||
// Deliberately not a view transition. A page-wide snapshot cannot blur what
|
||||
// sits behind frosted glass, so for the length of the cross-fade every
|
||||
// frosted bar, menu and glaze pane went clear and showed the page sharply
|
||||
// through itself. Surfaces transition their own colours instead (see
|
||||
// --nv-transition-surface), and the glass stays live throughout.
|
||||
if (theme === "system") {
|
||||
root.removeAttribute("data-theme");
|
||||
} else {
|
||||
root.setAttribute("data-theme", theme);
|
||||
}
|
||||
syncThemeControls(theme);
|
||||
}, { kind: "theme" });
|
||||
|
||||
root.dispatchEvent(new CustomEvent("nv:theme", { detail: { theme: theme } }));
|
||||
return done;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function syncThemeControls(theme) {
|
||||
|
|
@ -798,18 +800,87 @@
|
|||
var settle = 0;
|
||||
var limit = 36;
|
||||
|
||||
function setPull(value) {
|
||||
function clearPull() {
|
||||
pull = 0;
|
||||
delete list.dataset.nvPulling;
|
||||
list.style.removeProperty("--nv-pull");
|
||||
}
|
||||
|
||||
// `hold` keeps the pull until clearPull() — a finger or button is still
|
||||
// down — instead of releasing it after the wheel goes quiet.
|
||||
function setPull(value, hold) {
|
||||
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);
|
||||
if (!hold) settle = global.setTimeout(clearPull, 90);
|
||||
}
|
||||
|
||||
// Dragging with a mouse or pen. Touch scrolls natively; a desktop
|
||||
// pointer has no such thing, so without this a wheel cannot be grabbed.
|
||||
// Snapping is off while the pointer is down so the list follows it, the
|
||||
// end gives with resistance, and on release the list coasts to the row
|
||||
// the motion was heading for.
|
||||
var drag = null;
|
||||
|
||||
list.addEventListener("pointerdown", function (event) {
|
||||
if (event.pointerType === "touch" || event.button !== 0) return;
|
||||
drag = {
|
||||
id: event.pointerId, y: event.clientY, top: list.scrollTop,
|
||||
lastY: event.clientY, lastT: event.timeStamp, velocity: 0, moved: false,
|
||||
};
|
||||
});
|
||||
|
||||
list.addEventListener("pointermove", function (event) {
|
||||
if (!drag || event.pointerId !== drag.id) return;
|
||||
var dy = event.clientY - drag.y;
|
||||
if (!drag.moved) {
|
||||
if (Math.abs(dy) < 4) return;
|
||||
// Only now is it a drag and not a click, so only now is the pointer
|
||||
// captured; capturing earlier would redirect the click to the list.
|
||||
drag.moved = true;
|
||||
list.setPointerCapture(event.pointerId);
|
||||
list.style.scrollSnapType = "none";
|
||||
list.dataset.nvDragging = "";
|
||||
}
|
||||
var elapsed = event.timeStamp - drag.lastT;
|
||||
if (elapsed > 0) drag.velocity = (event.clientY - drag.lastY) / elapsed;
|
||||
drag.lastY = event.clientY;
|
||||
drag.lastT = event.timeStamp;
|
||||
|
||||
var max = list.scrollHeight - list.clientHeight;
|
||||
var target = drag.top - dy;
|
||||
if (target < 0 || target > max) {
|
||||
list.scrollTop = target < 0 ? 0 : max;
|
||||
var over = target < 0 ? -target : target - max;
|
||||
if (!prefersReducedMotion()) {
|
||||
setPull((target < 0 ? 1 : -1) * Math.min(limit, over * 0.35), true);
|
||||
}
|
||||
} else {
|
||||
list.scrollTop = target;
|
||||
if (pull) clearPull();
|
||||
}
|
||||
});
|
||||
|
||||
function endDrag(event) {
|
||||
if (!drag || event.pointerId !== drag.id) return;
|
||||
var moved = drag.moved;
|
||||
var velocity = drag.velocity;
|
||||
drag = null;
|
||||
if (!moved) return;
|
||||
delete list.dataset.nvDragging;
|
||||
list.style.scrollSnapType = "";
|
||||
clearPull();
|
||||
// Coast: carry the release speed a little way on, then settle on a row.
|
||||
var row = list.firstElementChild.offsetHeight;
|
||||
var ahead = list.scrollTop - velocity * 160;
|
||||
var index = Math.max(0, Math.min(list.children.length - 1, Math.round(ahead / row)));
|
||||
scrollTo(list, index, true);
|
||||
}
|
||||
|
||||
list.addEventListener("pointerup", endDrag);
|
||||
list.addEventListener("pointercancel", endDrag);
|
||||
|
||||
list.addEventListener("wheel", function (event) {
|
||||
if (prefersReducedMotion()) return;
|
||||
var atTop = list.scrollTop <= 0;
|
||||
|
|
@ -820,6 +891,7 @@
|
|||
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;
|
||||
if (drag && drag.moved) return;
|
||||
event.preventDefault();
|
||||
var next = pull - delta * 0.12 * (1 - Math.abs(pull) / limit);
|
||||
setPull(Math.max(-limit, Math.min(limit, next)));
|
||||
|
|
@ -1089,6 +1161,31 @@
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A checkbox with data-nv-tristate cycles unchecked → checked → partial →
|
||||
* unchecked. The browser only offers the first two by clicking, and the third
|
||||
* only from script, so a partial state could be shown but never chosen.
|
||||
* data-nv-tristate="partial" starts it partial.
|
||||
*/
|
||||
function initTristate(box) {
|
||||
if (box.dataset.nvTristateWired) return;
|
||||
box.dataset.nvTristateWired = "";
|
||||
var states = ["off", "on", "partial"];
|
||||
var state = box.dataset.nvTristate === "partial" ? 2 : box.checked ? 1 : 0;
|
||||
|
||||
function apply() {
|
||||
box.checked = state === 1;
|
||||
box.indeterminate = state === 2;
|
||||
box.dataset.nvState = states[state];
|
||||
}
|
||||
|
||||
box.addEventListener("click", function () {
|
||||
state = (state + 1) % 3;
|
||||
apply();
|
||||
});
|
||||
apply();
|
||||
}
|
||||
|
||||
function wire(scope) {
|
||||
scope = scope || document;
|
||||
|
||||
|
|
@ -1107,6 +1204,7 @@
|
|||
scope.querySelectorAll("[data-nv-tabs]").forEach(initTabs);
|
||||
scope.querySelectorAll("[data-nv-scroller]").forEach(initScroller);
|
||||
scope.querySelectorAll(".nv-slider").forEach(initSlider);
|
||||
scope.querySelectorAll("input[data-nv-tristate]").forEach(initTristate);
|
||||
scope.querySelectorAll("[data-nv-calendar]").forEach(initCalendar);
|
||||
scope.querySelectorAll("[data-nv-timepicker]").forEach(initTimePicker);
|
||||
scope.querySelectorAll(".nv-menu--listbox").forEach(initListbox);
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@
|
|||
:root,
|
||||
[data-nv-brand] {
|
||||
/* --- Canvas and surfaces ----------------------------------------------- */
|
||||
--nv-dark-background: color-mix(in oklab, #0f0f12, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-background-subtle: color-mix(in oklab, #18181b, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface: color-mix(in oklab, #1f1f22, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface-elevated: color-mix(in oklab, #2a2a2e, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-background: color-mix(in oklab, #17171b, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-background-subtle: color-mix(in oklab, #1d1d21, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface: color-mix(in oklab, #232327, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface-elevated: color-mix(in oklab, #2d2d32, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface-inset: color-mix(in oklab, #111113, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface-hover: color-mix(in oklab, #313136, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface-active: color-mix(in oklab, #39393f, var(--nv-brand) var(--nv-brand-tint));
|
||||
|
|
@ -111,9 +111,9 @@
|
|||
calc(c * 0.2) h);
|
||||
|
||||
/* --- Depth and material -------------------------------------------------- */
|
||||
--nv-dark-shadow-ambient: rgb(0 0 0 / 0.5);
|
||||
--nv-dark-shadow-penumbra: rgb(0 0 0 / 0.5);
|
||||
--nv-dark-shadow-umbra: rgb(0 0 0 / 0.55);
|
||||
--nv-dark-shadow-ambient: rgb(0 0 0 / 0.6);
|
||||
--nv-dark-shadow-penumbra: rgb(0 0 0 / 0.62);
|
||||
--nv-dark-shadow-umbra: rgb(0 0 0 / 0.72);
|
||||
--nv-dark-shadow-inset: rgb(0 0 0 / 0.65);
|
||||
--nv-dark-highlight: rgb(255 255 255 / 0.15);
|
||||
--nv-dark-highlight-strong: rgb(255 255 255 / 0.24);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@
|
|||
consistent between appearances: an ambient contact shadow that keeps the
|
||||
edge crisp, a soft penumbra for the spread, and an umbra that only appears
|
||||
once a surface is genuinely far from the canvas.
|
||||
|
||||
Two things keep a shadow visible, and both have failed before. The soft
|
||||
layers use only a mild negative spread: a deep one tucks the blur under the
|
||||
element and leaves almost nothing outside it. And the canvas has to be
|
||||
lighter than the shadow colour — black cannot darken black — which is why the
|
||||
dark canvas is not the darkest value it could be.
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
|
|
@ -23,27 +29,27 @@
|
|||
--nv-elevation-1:
|
||||
0 1px 1px var(--nv-color-shadow-ambient),
|
||||
0 2px 4px -1px var(--nv-color-shadow-penumbra),
|
||||
0 6px 14px -8px var(--nv-color-shadow-umbra);
|
||||
0 6px 14px -5px var(--nv-color-shadow-umbra);
|
||||
|
||||
--nv-elevation-2:
|
||||
0 1px 1px var(--nv-color-shadow-ambient),
|
||||
0 3px 6px -2px var(--nv-color-shadow-penumbra),
|
||||
0 10px 20px -10px var(--nv-color-shadow-umbra);
|
||||
0 10px 20px -7px var(--nv-color-shadow-umbra);
|
||||
|
||||
--nv-elevation-3:
|
||||
0 1px 2px var(--nv-color-shadow-ambient),
|
||||
0 6px 12px -4px var(--nv-color-shadow-penumbra),
|
||||
0 18px 36px -12px var(--nv-color-shadow-umbra);
|
||||
0 18px 36px -9px var(--nv-color-shadow-umbra);
|
||||
|
||||
--nv-elevation-4:
|
||||
0 1px 2px var(--nv-color-shadow-ambient),
|
||||
0 10px 20px -6px var(--nv-color-shadow-penumbra),
|
||||
0 30px 60px -16px var(--nv-color-shadow-umbra);
|
||||
0 30px 60px -12px var(--nv-color-shadow-umbra);
|
||||
|
||||
--nv-elevation-5:
|
||||
0 2px 3px var(--nv-color-shadow-ambient),
|
||||
0 16px 32px -8px var(--nv-color-shadow-penumbra),
|
||||
0 44px 88px -20px var(--nv-color-shadow-umbra);
|
||||
0 44px 88px -16px var(--nv-color-shadow-umbra);
|
||||
|
||||
/* --- Inset ---------------------------------------------------------------
|
||||
Recessed surfaces: fields, wells, tracks, pressed controls. */
|
||||
|
|
|
|||
Loading…
Reference in a new issue