@jsenv/navi 0.25.7 → 0.25.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import { isValidElement, createContext, h, options, toChildArray, render, cloneE
3
3
  import { useErrorBoundary, useLayoutEffect, useEffect, useContext, useMemo, useRef, useState, useCallback, useImperativeHandle, useId } from "preact/hooks";
4
4
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
5
5
  import { signal, effect, computed, batch, useSignal } from "@preact/signals";
6
- import { createIterableWeakSet, mergeOneStyle, stringifyStyle, createPubSub, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, findBefore, findAfter, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, createStyleController, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, resolveCSSSize, canInterceptKeys, activeElementSignal, resolveColorLuminance, contrastColor, hasCSSSizeUnit, initFocusGroup, elementIsFocusable, getScrollContainer, scrollIntoViewScoped, findFocusable, trapScrollInside, trapFocusInside, dragAfterThreshold, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
6
+ import { createIterableWeakSet, mergeOneStyle, stringifyStyle, createPubSub, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, findBefore, findAfter, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, createStyleController, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, resolveCSSSize, canInterceptKeys, activeElementSignal, hasCSSSizeUnit, resolveOklchLightness, contrastColor, initFocusGroup, elementIsFocusable, scrollIntoViewScoped, findFocusable, trapScrollInside, trapFocusInside, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
7
  export { contrastColor } from "@jsenv/dom";
8
8
  import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
9
9
  import { createValidity } from "@jsenv/validity";
@@ -6286,6 +6286,7 @@ const FLOW_PROPS = {
6286
6286
  block: () => {},
6287
6287
  flex: () => {},
6288
6288
  grid: () => {},
6289
+ gridTemplateColumns: PASS_THROUGH,
6289
6290
  row: () => {},
6290
6291
  column: () => {},
6291
6292
  };
@@ -6560,7 +6561,9 @@ const TYPO_PROPS = {
6560
6561
  underlineColor: applyOnCSSProp("textDecorationColor"),
6561
6562
  textShadow: PASS_THROUGH,
6562
6563
  lineHeight: PASS_THROUGH,
6563
- color: PASS_THROUGH,
6564
+ color: (value) => {
6565
+ return { color: resolveColorKeyword(value) };
6566
+ },
6564
6567
  noWrap: applyToCssPropWhenTruthy("whiteSpace", "nowrap", "normal"),
6565
6568
  pre: applyToCssPropWhenTruthy("whiteSpace", "pre", "normal"),
6566
6569
  preWrap: applyToCssPropWhenTruthy("whiteSpace", "pre-wrap", "normal"),
@@ -6840,6 +6843,16 @@ const resolveSpacingSize = (size, property = "padding") => {
6840
6843
  return stringifyStyle(SIZE_MAP[size] || size, property);
6841
6844
  };
6842
6845
 
6846
+ const COLOR_KEYWORD_MAP = {
6847
+ secondary: "var(--navi-color-secondary)",
6848
+ emphasis: "var(--navi-color-emphasis)",
6849
+ discrete: "var(--navi-color-discrete)",
6850
+ hint: "var(--navi-color-hint)",
6851
+ };
6852
+ const resolveColorKeyword = (value) => {
6853
+ return COLOR_KEYWORD_MAP[value] || value;
6854
+ };
6855
+
6843
6856
  const DEFAULT_DISPLAY_BY_TAG_NAME = {
6844
6857
  "inline": new Set([
6845
6858
  "a",
@@ -7202,212 +7215,361 @@ const listenInputValueChange = (input, callback) => {
7202
7215
  return teardown;
7203
7216
  };
7204
7217
 
7205
- const pressedElements = new WeakSet();
7218
+ /**
7219
+ * Navi uses three categories of custom events:
7220
+ *
7221
+ * 1. **Internal events** (`dispatchInternalCustomEvent`) — a component communicates
7222
+ * with other navi components internally. Not meant to be observed from outside.
7223
+ * They do not bubble so they stay contained within the subtree that handles them.
7224
+ * Names often reflect their internal nature (e.g. `navi_pseudo_state_request_check`).
7225
+ *
7226
+ * 2. **Public events** (`dispatchPublicCustomEvent`) — a component exposes information
7227
+ * about something that happened (e.g. `navi_list_select`). They bubble so any
7228
+ * ancestor can observe them. These are part of the public API and should be documented.
7229
+ *
7230
+ * 3. **Request events** (`dispatchCustomEvent`) — code *outside* a component asks it
7231
+ * to perform an action (e.g. `navi_list_request_open`). They are cancelable so the
7232
+ * component can signal whether it handled the request. Names are prefixed
7233
+ * with `request_` by convention.
7234
+ */
7206
7235
 
7207
- const PSEUDO_CLASSES = {
7208
- ":hover": {
7209
- attribute: "data-hover",
7210
- setup: (el, callback) => {
7211
- let onmouseenter = () => {
7236
+ /**
7237
+ * Dispatches an internal event on `el`.
7238
+ * Does not bubble — stays within the local subtree.
7239
+ */
7240
+ const dispatchInternalCustomEvent = (
7241
+ el,
7242
+ customEventName,
7243
+ customEventDetail,
7244
+ ) => {
7245
+ const customEvent = new CustomEvent(customEventName, {
7246
+ detail: customEventDetail,
7247
+ });
7248
+ return el.dispatchEvent(customEvent);
7249
+ };
7250
+
7251
+ /**
7252
+ * Dispatches a public event from `el`, announcing something that happened.
7253
+ * Bubbles so any ancestor can observe it.
7254
+ */
7255
+ const dispatchPublicCustomEvent = (
7256
+ el,
7257
+ customEventName,
7258
+ customEventDetail,
7259
+ ) => {
7260
+ const customEvent = new CustomEvent(customEventName, {
7261
+ detail: resolveEventDetail(customEventDetail),
7262
+ bubbles: true,
7263
+ });
7264
+ return el.dispatchEvent(customEvent);
7265
+ };
7266
+
7267
+ /**
7268
+ * Dispatches a request event *at* `el`, asking it to perform an action.
7269
+ * Cancelable — returns `false` if the component called `preventDefault()`,
7270
+ * indicating it did not (or could not) handle the request.
7271
+ * Names are conventionally prefixed with `request_` (e.g. `navi_list_request_open`).
7272
+ */
7273
+ const dispatchCustomEvent = (el, customEventName, customEventDetail) => {
7274
+ const customEvent = new CustomEvent(customEventName, {
7275
+ detail: resolveEventDetail(customEventDetail),
7276
+ cancelable: true,
7277
+ });
7278
+ const result = el.dispatchEvent(customEvent);
7279
+ return result;
7280
+ };
7281
+
7282
+ const resolveEventDetail = (customEventDetail) => {
7283
+ const { event, ...rest } = customEventDetail ?? {};
7284
+ let resolvedEvent;
7285
+ if (event?.detail?.event !== undefined) {
7286
+ resolvedEvent = event.detail.event;
7287
+ } else if (event !== undefined) {
7288
+ resolvedEvent = event;
7289
+ }
7290
+ return { ...rest, event: resolvedEvent };
7291
+ };
7292
+
7293
+ const requestPseudoStateCheck = (element, detail) => {
7294
+ dispatchInternalCustomEvent(
7295
+ element,
7296
+ "navi_pseudo_state_request_check",
7297
+ detail,
7298
+ );
7299
+ };
7300
+ const NAVI_PSEUDO_STATE_CUSTOM_EVENT = "navi_pseudo_state";
7301
+ const dispatchPseudoStateCustomEvent = (element, value, oldValue) => {
7302
+ dispatchInternalCustomEvent(element, NAVI_PSEUDO_STATE_CUSTOM_EVENT, {
7303
+ pseudoState: value,
7304
+ oldPseudoState: oldValue,
7305
+ });
7306
+ };
7307
+
7308
+ const PSEUDO_CLASSES = {};
7309
+ Object.assign(PSEUDO_CLASSES, {
7310
+ ":valid": {
7311
+ attribute: "data-valid",
7312
+ test: (el) => el.matches(":valid"),
7313
+ },
7314
+ ":invalid": {
7315
+ attribute: "data-invalid",
7316
+ test: (el) => el.matches(":invalid"),
7317
+ },
7318
+ ":visited": {
7319
+ attribute: "data-visited",
7320
+ },
7321
+ });
7322
+ const definePseudoClass = (pseudoClass, definition) => {
7323
+ PSEUDO_CLASSES[pseudoClass] = definition;
7324
+ };
7325
+
7326
+ definePseudoClass(":hover", {
7327
+ attribute: "data-hover",
7328
+ setup: (el, callback) => {
7329
+ let onmouseenter = () => {
7330
+ callback();
7331
+ };
7332
+ let onmouseleave = () => {
7333
+ callback();
7334
+ };
7335
+
7336
+ if (el.tagName === "LABEL") {
7337
+ // input.matches(":hover") is true when hovering the label
7338
+ // so when label is hovered/not hovered we need to recheck the input too
7339
+ const recheckInput = (e) => {
7340
+ if (el.htmlFor) {
7341
+ const input = document.getElementById(el.htmlFor);
7342
+ if (!input) {
7343
+ // cannot find the input for this label in the DOM
7344
+ return;
7345
+ }
7346
+ requestPseudoStateCheck(input, { event: e });
7347
+ return;
7348
+ }
7349
+ const input = el.querySelector("input, textarea, select");
7350
+ if (!input) {
7351
+ // label does not contain an input
7352
+ return;
7353
+ }
7354
+ requestPseudoStateCheck(input, { event: e });
7355
+ };
7356
+ onmouseenter = (e) => {
7212
7357
  callback();
7358
+ recheckInput(e);
7213
7359
  };
7214
- let onmouseleave = () => {
7360
+ onmouseleave = (e) => {
7215
7361
  callback();
7362
+ recheckInput(e);
7216
7363
  };
7364
+ }
7217
7365
 
7218
- if (el.tagName === "LABEL") {
7219
- // input.matches(":hover") is true when hovering the label
7220
- // so when label is hovered/not hovered we need to recheck the input too
7221
- const recheckInput = () => {
7222
- if (el.htmlFor) {
7223
- const input = document.getElementById(el.htmlFor);
7224
- if (!input) {
7225
- // cannot find the input for this label in the DOM
7226
- return;
7227
- }
7228
- input.dispatchEvent(
7229
- new CustomEvent(NAVI_CHECK_PSEUDO_STATE_CUSTOM_EVENT),
7230
- );
7231
- return;
7232
- }
7233
- const input = el.querySelector("input, textarea, select");
7234
- if (!input) {
7235
- // label does not contain an input
7236
- return;
7237
- }
7238
- input.dispatchEvent(
7239
- new CustomEvent(NAVI_CHECK_PSEUDO_STATE_CUSTOM_EVENT),
7240
- );
7241
- };
7242
- onmouseenter = () => {
7243
- callback();
7244
- recheckInput();
7245
- };
7246
- onmouseleave = () => {
7247
- callback();
7248
- recheckInput();
7249
- };
7366
+ el.addEventListener("mouseenter", onmouseenter);
7367
+ el.addEventListener("mouseleave", onmouseleave);
7368
+ return () => {
7369
+ el.removeEventListener("mouseenter", onmouseenter);
7370
+ el.removeEventListener("mouseleave", onmouseleave);
7371
+ };
7372
+ },
7373
+ test: (el) => el.matches(":hover"),
7374
+ });
7375
+ definePseudoClass(":disabled", {
7376
+ attribute: "data-disabled",
7377
+ add: (el) => {
7378
+ if (
7379
+ el.tagName === "BUTTON" ||
7380
+ el.tagName === "INPUT" ||
7381
+ el.tagName === "SELECT" ||
7382
+ el.tagName === "TEXTAREA"
7383
+ ) {
7384
+ el.disabled = true;
7385
+ }
7386
+ },
7387
+ remove: (el) => {
7388
+ if (
7389
+ el.tagName === "BUTTON" ||
7390
+ el.tagName === "INPUT" ||
7391
+ el.tagName === "SELECT" ||
7392
+ el.tagName === "TEXTAREA"
7393
+ ) {
7394
+ el.disabled = false;
7395
+ }
7396
+ },
7397
+ });
7398
+ definePseudoClass(":read-only", {
7399
+ attribute: "data-readonly",
7400
+ add: (el) => {
7401
+ if (
7402
+ el.tagName === "INPUT" ||
7403
+ el.tagName === "SELECT" ||
7404
+ el.tagName === "TEXTAREA"
7405
+ ) {
7406
+ if (el.type === "checkbox" || el.type === "radio") {
7407
+ // there is no readOnly for checkboxes/radios
7408
+ return;
7250
7409
  }
7251
-
7252
- el.addEventListener("mouseenter", onmouseenter);
7253
- el.addEventListener("mouseleave", onmouseleave);
7254
- return () => {
7255
- el.removeEventListener("mouseenter", onmouseenter);
7256
- el.removeEventListener("mouseleave", onmouseleave);
7257
- };
7258
- },
7259
- test: (el) => el.matches(":hover"),
7410
+ el.readOnly = true;
7411
+ }
7260
7412
  },
7261
- ":active": {
7262
- attribute: "data-active",
7263
- setup: (el, callback) => {
7264
- // It might be tempting to use el.setPointerCapture() here so that pointerup
7265
- // always fires on el regardless of where the pointer is released. However,
7266
- // pointer capture routes all subsequent pointer events to the capturing element,
7267
- // which means any other element in the tree that expects to receive pointerup,
7268
- // mouseup, click, etc. after a pointerdown will silently not get them.
7269
- // For example a <label> that reacts to mousedown + click, or a third-party
7270
- // library that attaches its own listeners, would break because an ancestor
7271
- // grabbed the pointer out from under them.
7272
- // To avoid forcing every such element to declare an opt-out attribute
7273
- // (e.g. navi-own-pointer-capture) we simply listen on document instead,
7274
- // which is safe and does not interfere with anyone else's event flow.
7275
- const onPointerDown = () => {
7276
- const onRelease = () => {
7277
- document.removeEventListener("pointercancel", onRelease, true);
7278
- document.removeEventListener("pointerup", onRelease, true);
7413
+ remove: (el) => {
7414
+ if (
7415
+ el.tagName === "INPUT" ||
7416
+ el.tagName === "SELECT" ||
7417
+ el.tagName === "TEXTAREA"
7418
+ ) {
7419
+ if (el.type === "checkbox" || el.type === "radio") {
7420
+ // there is no readOnly for checkboxes/radios
7421
+ return;
7422
+ }
7423
+ el.readOnly = false;
7424
+ }
7425
+ },
7426
+ });
7427
+ definePseudoClass(":checked", {
7428
+ attribute: "data-checked",
7429
+ setup: (el, callback) => {
7430
+ if (el.type === "checkbox") {
7431
+ // Listen to user interactions
7432
+ el.addEventListener("input", callback);
7433
+ // Intercept programmatic changes to .checked property
7434
+ const originalDescriptor = Object.getOwnPropertyDescriptor(
7435
+ HTMLInputElement.prototype,
7436
+ "checked",
7437
+ );
7438
+ Object.defineProperty(el, "checked", {
7439
+ get: originalDescriptor.get,
7440
+ set(value) {
7441
+ originalDescriptor.set.call(this, value);
7279
7442
  callback();
7280
- };
7281
- document.addEventListener("pointercancel", onRelease, true);
7282
- document.addEventListener("pointerup", onRelease, true);
7283
- callback();
7284
- };
7285
- el.addEventListener("pointerdown", onPointerDown);
7443
+ },
7444
+ configurable: true,
7445
+ });
7286
7446
  return () => {
7287
- el.removeEventListener("pointerdown", onPointerDown);
7447
+ // Restore original property descriptor
7448
+ Object.defineProperty(el, "checked", originalDescriptor);
7449
+ el.removeEventListener("input", callback);
7288
7450
  };
7289
- },
7290
- test: (el) => el.matches(":active"),
7291
- },
7292
- ":-navi-pressed": {
7293
- attribute: "data-pressed",
7294
- setup: (el, callback) => {
7295
- // Same reasoning as :active above: setPointerCapture is avoided because it
7296
- // hijacks all subsequent pointer events and can break elements that rely on
7297
- // receiving their own pointerup/mouseup/click after a pointerdown, including
7298
- // <label> elements and third-party code. Listening on document is the safe
7299
- // alternative.
7300
- const onPointerDown = (e) => {
7301
- if (e.button !== 0) {
7302
- // only left pointer (mouse left click, touch, pen)
7303
- return;
7304
- }
7305
- pressedElements.add(el);
7306
- const onRelease = () => {
7307
- pressedElements.delete(el);
7308
- document.removeEventListener("pointercancel", onRelease, true);
7309
- document.removeEventListener("pointerup", onRelease, true);
7310
- document.removeEventListener("contextmenu", onContextMenu, true);
7451
+ }
7452
+ if (el.type === "radio") {
7453
+ // Listen to changes on the radio group
7454
+ const radioSet =
7455
+ el.closest("[data-radio-list], fieldset, form") || document;
7456
+ radioSet.addEventListener("input", callback);
7457
+
7458
+ // Intercept programmatic changes to .checked property
7459
+ const originalDescriptor = Object.getOwnPropertyDescriptor(
7460
+ HTMLInputElement.prototype,
7461
+ "checked",
7462
+ );
7463
+ Object.defineProperty(el, "checked", {
7464
+ get: originalDescriptor.get,
7465
+ set(value) {
7466
+ originalDescriptor.set.call(this, value);
7311
7467
  callback();
7312
- };
7313
- const onContextMenu = (e) => {
7314
- // On touch devices, a long-press triggers the context menu.
7315
- // If the context menu is not prevented, it means it will open and the
7316
- // pointer events (pointerup, lostpointercapture) won't fire normally,
7317
- // leaving the element stuck in pressed state. We clear it manually.
7318
- // e.button === -1 means the event was synthesized from a long-press (not a real mouse click).
7319
- if (e.button === -1 && !e.defaultPrevented) {
7320
- pressedElements.delete(el);
7321
- document.removeEventListener("pointercancel", onRelease, true);
7322
- document.removeEventListener("pointerup", onRelease, true);
7323
- document.removeEventListener("contextmenu", onContextMenu, true);
7324
- callback();
7325
- }
7326
- };
7327
- document.addEventListener("pointercancel", onRelease, true);
7328
- document.addEventListener("pointerup", onRelease, true);
7329
- document.addEventListener("contextmenu", onContextMenu, true);
7330
- callback();
7468
+ },
7469
+ configurable: true,
7470
+ });
7471
+ return () => {
7472
+ radioSet.removeEventListener("input", callback);
7473
+ // Restore original property descriptor
7474
+ Object.defineProperty(el, "checked", originalDescriptor);
7331
7475
  };
7332
- el.addEventListener("pointerdown", onPointerDown);
7476
+ }
7477
+ if (el.tagName === "INPUT") {
7478
+ el.addEventListener("input", callback);
7333
7479
  return () => {
7334
- el.removeEventListener("pointerdown", onPointerDown);
7335
- pressedElements.delete(el);
7480
+ el.removeEventListener("input", callback);
7336
7481
  };
7337
- },
7338
- test: (el) => pressedElements.has(el),
7482
+ }
7483
+ return () => {};
7339
7484
  },
7340
- ":visited": {
7341
- attribute: "data-visited",
7485
+ test: (el) => el.matches(":checked"),
7486
+ });
7487
+ definePseudoClass(":active", {
7488
+ attribute: "data-active",
7489
+ setup: (el, callback) => {
7490
+ // I'ts recommended to use :-navi-pressed over :active for interactive elements.
7491
+ const onPointerDown = () => {
7492
+ const onRelease = () => {
7493
+ document.removeEventListener("pointercancel", onRelease, true);
7494
+ document.removeEventListener("pointerup", onRelease, true);
7495
+ callback();
7496
+ };
7497
+ document.addEventListener("pointercancel", onRelease, true);
7498
+ document.addEventListener("pointerup", onRelease, true);
7499
+ callback();
7500
+ };
7501
+ el.addEventListener("pointerdown", onPointerDown);
7502
+ return () => {
7503
+ el.removeEventListener("pointerdown", onPointerDown);
7504
+ };
7342
7505
  },
7343
- ":checked": {
7344
- attribute: "data-checked",
7345
- setup: (el, callback) => {
7346
- if (el.type === "checkbox") {
7347
- // Listen to user interactions
7348
- el.addEventListener("input", callback);
7349
- // Intercept programmatic changes to .checked property
7350
- const originalDescriptor = Object.getOwnPropertyDescriptor(
7351
- HTMLInputElement.prototype,
7352
- "checked",
7353
- );
7354
- Object.defineProperty(el, "checked", {
7355
- get: originalDescriptor.get,
7356
- set(value) {
7357
- originalDescriptor.set.call(this, value);
7358
- callback();
7359
- },
7360
- configurable: true,
7361
- });
7362
- return () => {
7363
- // Restore original property descriptor
7364
- Object.defineProperty(el, "checked", originalDescriptor);
7365
- el.removeEventListener("input", callback);
7366
- };
7506
+ test: (el) => el.matches(":active"),
7507
+ });
7508
+ {
7509
+ // We implement :focus and :focus-visible with enriched semantics:
7510
+ // an element is considered focused not only when it natively has focus, but also
7511
+ // when a "focus proxy" element has focus (e.g. a read-only range input delegates
7512
+ // focus to a sibling span) or when a controlling element has focus (e.g. a combobox
7513
+ // input with aria-controls pointing to a listbox — the listbox should appear focused
7514
+ // while the input is focused).
7515
+ //
7516
+ // We intentionally reuse the native :focus / :focus-visible names rather than
7517
+ // introducing new navi-specific pseudo-classes (e.g. :-navi-focus). This is a
7518
+ // deliberate exception: all existing CSS and code written as [data-focus] or
7519
+ // [data-focus-visible] automatically benefits from the enriched behavior without
7520
+ // any changes. A separate navi-specific class would require updating every
7521
+ // component.
7522
+ //
7523
+ // When a controller element (e.g. combobox input) gains or loses focus,
7524
+ // notify the elements it controls via aria-controls so they re-check their focus state.
7525
+ const notifyAriaControlled = (el, e) => {
7526
+ const controlledIds = el.getAttribute("aria-controls");
7527
+ if (!controlledIds) {
7528
+ return;
7529
+ }
7530
+ for (const id of controlledIds.split(" ")) {
7531
+ const controlled = document.getElementById(id);
7532
+ if (controlled) {
7533
+ requestPseudoStateCheck(controlled, { event: e });
7367
7534
  }
7368
- if (el.type === "radio") {
7369
- // Listen to changes on the radio group
7370
- const radioSet =
7371
- el.closest("[data-radio-list], fieldset, form") || document;
7372
- radioSet.addEventListener("input", callback);
7373
-
7374
- // Intercept programmatic changes to .checked property
7375
- const originalDescriptor = Object.getOwnPropertyDescriptor(
7376
- HTMLInputElement.prototype,
7377
- "checked",
7378
- );
7379
- Object.defineProperty(el, "checked", {
7380
- get: originalDescriptor.get,
7381
- set(value) {
7382
- originalDescriptor.set.call(this, value);
7383
- callback();
7384
- },
7385
- configurable: true,
7386
- });
7387
- return () => {
7388
- radioSet.removeEventListener("input", callback);
7389
- // Restore original property descriptor
7390
- Object.defineProperty(el, "checked", originalDescriptor);
7391
- };
7535
+ }
7536
+ };
7537
+ // Check if any element whose aria-controls includes el's id currently has focus.
7538
+ const isControlledByFocusedElement = (
7539
+ el,
7540
+ { requireFocusVisible = false } = {},
7541
+ ) => {
7542
+ const id = el.id;
7543
+ if (!id) {
7544
+ return false;
7545
+ }
7546
+ const controllers = document.querySelectorAll(`[aria-controls~="${id}"]`);
7547
+ for (const controller of controllers) {
7548
+ // If the controller is inside the element it controls, the element already
7549
+ // receives native :focus/:focus-within — no need to inherit focus from it.
7550
+ if (el.contains(controller)) {
7551
+ continue;
7392
7552
  }
7393
- if (el.tagName === "INPUT") {
7394
- el.addEventListener("input", callback);
7395
- return () => {
7396
- el.removeEventListener("input", callback);
7397
- };
7553
+ const pseudoClass = requireFocusVisible ? ":focus-visible" : ":focus";
7554
+ if (controller.matches(pseudoClass)) {
7555
+ return true;
7398
7556
  }
7399
- return () => {};
7400
- },
7401
- test: (el) => el.matches(":checked"),
7402
- },
7403
- ":focus": {
7557
+ }
7558
+ return false;
7559
+ };
7560
+
7561
+ definePseudoClass(":focus", {
7404
7562
  attribute: "data-focus",
7405
7563
  setup: (el, callback) => {
7406
- el.addEventListener("focusin", callback);
7407
- el.addEventListener("focusout", callback);
7564
+ const onFocusChange = (e) => {
7565
+ callback();
7566
+ notifyAriaControlled(el, e);
7567
+ };
7568
+ el.addEventListener("focusin", onFocusChange);
7569
+ el.addEventListener("focusout", onFocusChange);
7408
7570
  return () => {
7409
- el.removeEventListener("focusin", callback);
7410
- el.removeEventListener("focusout", callback);
7571
+ el.removeEventListener("focusin", onFocusChange);
7572
+ el.removeEventListener("focusout", onFocusChange);
7411
7573
  };
7412
7574
  },
7413
7575
  test: (el) => {
@@ -7418,17 +7580,28 @@ const PSEUDO_CLASSES = {
7418
7580
  if (focusProxy) {
7419
7581
  return document.querySelector(`#${focusProxy}`).matches(":focus");
7420
7582
  }
7583
+ if (isControlledByFocusedElement(el)) {
7584
+ return true;
7585
+ }
7421
7586
  return false;
7422
7587
  },
7423
- },
7424
- ":focus-visible": {
7588
+ });
7589
+ definePseudoClass(":focus-visible", {
7425
7590
  attribute: "data-focus-visible",
7426
7591
  setup: (el, callback) => {
7592
+ const onFocusChange = (e) => {
7593
+ callback();
7594
+ notifyAriaControlled(el, e);
7595
+ };
7427
7596
  document.addEventListener("keydown", callback);
7428
7597
  document.addEventListener("keyup", callback);
7598
+ el.addEventListener("focusin", onFocusChange);
7599
+ el.addEventListener("focusout", onFocusChange);
7429
7600
  return () => {
7430
7601
  document.removeEventListener("keydown", callback);
7431
7602
  document.removeEventListener("keyup", callback);
7603
+ el.removeEventListener("focusin", onFocusChange);
7604
+ el.removeEventListener("focusout", onFocusChange);
7432
7605
  };
7433
7606
  },
7434
7607
  test: (el) => {
@@ -7441,70 +7614,39 @@ const PSEUDO_CLASSES = {
7441
7614
  .querySelector(`#${focusProxy}`)
7442
7615
  .matches(":focus-visible");
7443
7616
  }
7444
- return false;
7445
- },
7446
- },
7447
- ":disabled": {
7448
- attribute: "data-disabled",
7449
- add: (el) => {
7450
- if (
7451
- el.tagName === "BUTTON" ||
7452
- el.tagName === "INPUT" ||
7453
- el.tagName === "SELECT" ||
7454
- el.tagName === "TEXTAREA"
7455
- ) {
7456
- el.disabled = true;
7617
+ if (isControlledByFocusedElement(el, { requireFocusVisible: true })) {
7618
+ return true;
7457
7619
  }
7620
+ return false;
7458
7621
  },
7459
- remove: (el) => {
7460
- if (
7461
- el.tagName === "BUTTON" ||
7462
- el.tagName === "INPUT" ||
7463
- el.tagName === "SELECT" ||
7464
- el.tagName === "TEXTAREA"
7465
- ) {
7466
- el.disabled = false;
7467
- }
7622
+ });
7623
+ definePseudoClass(":focus-within", {
7624
+ attribute: "data-focus-within",
7625
+ setup: (el, callback) => {
7626
+ const onFocusChange = (e) => {
7627
+ callback();
7628
+ notifyAriaControlled(el, e);
7629
+ };
7630
+ el.addEventListener("focusin", onFocusChange);
7631
+ el.addEventListener("focusout", onFocusChange);
7632
+ return () => {
7633
+ el.removeEventListener("focusin", onFocusChange);
7634
+ el.removeEventListener("focusout", onFocusChange);
7635
+ };
7468
7636
  },
7469
- },
7470
- ":read-only": {
7471
- attribute: "data-readonly",
7472
- add: (el) => {
7473
- if (
7474
- el.tagName === "INPUT" ||
7475
- el.tagName === "SELECT" ||
7476
- el.tagName === "TEXTAREA"
7477
- ) {
7478
- if (el.type === "checkbox" || el.type === "radio") {
7479
- // there is no readOnly for checkboxes/radios
7480
- return;
7481
- }
7482
- el.readOnly = true;
7637
+ test: (el) => {
7638
+ if (el.matches(":focus-within")) {
7639
+ return true;
7483
7640
  }
7484
- },
7485
- remove: (el) => {
7486
- if (
7487
- el.tagName === "INPUT" ||
7488
- el.tagName === "SELECT" ||
7489
- el.tagName === "TEXTAREA"
7490
- ) {
7491
- if (el.type === "checkbox" || el.type === "radio") {
7492
- // there is no readOnly for checkboxes/radios
7493
- return;
7494
- }
7495
- el.readOnly = false;
7641
+ if (isControlledByFocusedElement(el)) {
7642
+ return true;
7496
7643
  }
7644
+ return false;
7497
7645
  },
7498
- },
7499
- ":valid": {
7500
- attribute: "data-valid",
7501
- test: (el) => el.matches(":valid"),
7502
- },
7503
- ":invalid": {
7504
- attribute: "data-invalid",
7505
- test: (el) => el.matches(":invalid"),
7506
- },
7507
- "::highlight": {},
7646
+ });
7647
+ }
7648
+
7649
+ Object.assign(PSEUDO_CLASSES, {
7508
7650
  ":-navi-pointed": {
7509
7651
  attribute: "data-pointed",
7510
7652
  },
@@ -7541,35 +7683,84 @@ const PSEUDO_CLASSES = {
7541
7683
  ":-navi-void": {
7542
7684
  attribute: "data-void",
7543
7685
  },
7544
- ":-navi-has-value": {
7545
- attribute: "data-has-value",
7686
+ "::highlight": {},
7687
+ });
7688
+ definePseudoClass(":-navi-has-value", {
7689
+ attribute: "data-has-value",
7690
+ setup: (el, callback) => {
7691
+ return listenInputValue(el, callback);
7692
+ },
7693
+ test: (el) => {
7694
+ if (el.value === "") {
7695
+ return false;
7696
+ }
7697
+ return true;
7698
+ },
7699
+ });
7700
+ {
7701
+ const pressedElements = new WeakSet();
7702
+ definePseudoClass(":-navi-pressed", {
7703
+ attribute: "data-pressed",
7546
7704
  setup: (el, callback) => {
7547
- return listenInputValue(el, callback);
7548
- },
7549
- test: (el) => {
7550
- if (el.value === "") {
7551
- return false;
7552
- }
7553
- return true;
7705
+ // Prefer :-navi-pressed over :active for interactive elements because:
7706
+ // - :active only tracks the primary (left) button; right-click and touch
7707
+ // long-press do not trigger :active reliably across browsers.
7708
+ // - :-navi-pressed explicitly ignores non-primary buttons (e.g. right-click)
7709
+ // and correctly clears pressed state when a context menu opens on long-press,
7710
+ // which would otherwise leave the element stuck in a pressed appearance.
7711
+
7712
+ // Note: it might be tempting to use el.setPointerCapture() here so that pointerup
7713
+ // always fires on el regardless of where the pointer is released. However,
7714
+ // pointer capture routes all subsequent pointer events to the capturing element,
7715
+ // which means any other element in the tree that expects to receive pointerup,
7716
+ // mouseup, click, etc. after a pointerdown will silently not get them.
7717
+ // For example a <label> that reacts to mousedown + click, or a third-party
7718
+ // library that attaches its own listeners, would break because an ancestor
7719
+ // grabbed the pointer out from under them.
7720
+ // To avoid forcing every such element to declare an opt-out attribute
7721
+ // (e.g. navi-own-pointer-capture) we simply listen on document instead,
7722
+ // which is safe and does not interfere with anyone else's event flow.
7723
+ const onPointerDown = (e) => {
7724
+ if (e.button !== 0) {
7725
+ // only left pointer (mouse left click, touch, pen)
7726
+ return;
7727
+ }
7728
+ pressedElements.add(el);
7729
+ const onRelease = () => {
7730
+ pressedElements.delete(el);
7731
+ document.removeEventListener("pointercancel", onRelease, true);
7732
+ document.removeEventListener("pointerup", onRelease, true);
7733
+ document.removeEventListener("contextmenu", onContextMenu, true);
7734
+ callback();
7735
+ };
7736
+ const onContextMenu = (e) => {
7737
+ // On touch devices, a long-press triggers the context menu.
7738
+ // If the context menu is not prevented, it means it will open and the
7739
+ // pointer events (pointerup, lostpointercapture) won't fire normally,
7740
+ // leaving the element stuck in pressed state. We clear it manually.
7741
+ // e.button === -1 means the event was synthesized from a long-press (not a real mouse click).
7742
+ if (e.button === -1 && !e.defaultPrevented) {
7743
+ pressedElements.delete(el);
7744
+ document.removeEventListener("pointercancel", onRelease, true);
7745
+ document.removeEventListener("pointerup", onRelease, true);
7746
+ document.removeEventListener("contextmenu", onContextMenu, true);
7747
+ callback();
7748
+ }
7749
+ };
7750
+ document.addEventListener("pointercancel", onRelease, true);
7751
+ document.addEventListener("pointerup", onRelease, true);
7752
+ document.addEventListener("contextmenu", onContextMenu, true);
7753
+ callback();
7754
+ };
7755
+ el.addEventListener("pointerdown", onPointerDown);
7756
+ return () => {
7757
+ el.removeEventListener("pointerdown", onPointerDown);
7758
+ pressedElements.delete(el);
7759
+ };
7554
7760
  },
7555
- },
7556
- };
7557
-
7558
- const NAVI_PSEUDO_STATE_CUSTOM_EVENT = "navi_pseudo_state";
7559
- const NAVI_CHECK_PSEUDO_STATE_CUSTOM_EVENT = "navi_check_pseudo_state";
7560
- const dispatchNaviPseudoStateEvent = (element, value, oldValue) => {
7561
- if (!element) {
7562
- return;
7563
- }
7564
- element.dispatchEvent(
7565
- new CustomEvent(NAVI_PSEUDO_STATE_CUSTOM_EVENT, {
7566
- detail: {
7567
- pseudoState: value,
7568
- oldPseudoState: oldValue,
7569
- },
7570
- }),
7571
- );
7572
- };
7761
+ test: (el) => pressedElements.has(el),
7762
+ });
7763
+ }
7573
7764
 
7574
7765
  const EMPTY_STATE = {};
7575
7766
  const initPseudoStyles = (
@@ -7592,7 +7783,7 @@ const initPseudoStyles = (
7592
7783
  const onStateChange = (value, oldValue) => {
7593
7784
  effect?.(value, oldValue);
7594
7785
  if (elementListeningPseudoState) {
7595
- dispatchNaviPseudoStateEvent(
7786
+ dispatchPseudoStateCustomEvent(
7596
7787
  elementListeningPseudoState,
7597
7788
  value,
7598
7789
  oldValue,
@@ -7661,7 +7852,7 @@ const initPseudoStyles = (
7661
7852
  state = event.detail.pseudoState;
7662
7853
  onStateChange(state, oldState);
7663
7854
  });
7664
- element.addEventListener(NAVI_CHECK_PSEUDO_STATE_CUSTOM_EVENT, () => {
7855
+ element.addEventListener("navi_pseudo_state_request_check", () => {
7665
7856
  checkPseudoClasses();
7666
7857
  });
7667
7858
 
@@ -8037,6 +8228,10 @@ const PSEUDO_CLASSES_DEFAULT = [];
8037
8228
  const PSEUDO_ELEMENTS_DEFAULT = [];
8038
8229
  const STYLE_CSS_VARS_DEFAULT = {};
8039
8230
  const PROPS_CSS_VARS_DEFAULT = {};
8231
+ // When only pseudoStateSelector is set (no visualSelector), the box owns its
8232
+ // visual identity. Only event handlers and these explicit props are forwarded
8233
+ // to the inner semantic/interactive child element.
8234
+ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
8040
8235
  const Box = props => {
8041
8236
  const {
8042
8237
  as = "div",
@@ -8331,14 +8526,21 @@ const Box = props => {
8331
8526
  return;
8332
8527
  }
8333
8528
  // not a style prop what do we do with it?
8334
- if (shouldForwardAllToChild) {
8335
- if (isPseudoStyle) ; else {
8336
- childForwardedProps[name] = value;
8337
- }
8338
- } else {
8339
- if (isPseudoStyle) {
8529
+ // When pseudoStateSelector is set, the child element is the semantic/interactive one
8530
+ // When both selectors are set the child IS the component (e.g. Button with scale
8531
+ // transform) — forward everything so it behaves like a normal element.
8532
+ // When only pseudoStateSelector is set, the box keeps its own visual identity
8533
+ // (border, background, overflow…) and the child is just the interactive/semantic
8534
+ // element inside it. Only event handlers (onXxx) belong on that child; everything
8535
+ // else stays on the box.
8536
+ if (isPseudoStyle) {
8537
+ if (shouldForwardAllToChild) ; else {
8340
8538
  console.warn(`unsupported pseudo style key "${name}"`);
8539
+ selfForwardedProps[name] = value;
8341
8540
  }
8541
+ } else if (shouldForwardAllToChild) {
8542
+ childForwardedProps[name] = value;
8543
+ } else {
8342
8544
  selfForwardedProps[name] = value;
8343
8545
  }
8344
8546
  return;
@@ -8369,6 +8571,19 @@ const Box = props => {
8369
8571
  selfForwardedProps[propName] = propValue;
8370
8572
  continue;
8371
8573
  }
8574
+ const isEventHandler = propName.startsWith("on");
8575
+ if (isEventHandler) {
8576
+ if (pseudoStateSelector) {
8577
+ childForwardedProps[propName] = propValue;
8578
+ continue;
8579
+ }
8580
+ selfForwardedProps[propName] = propValue;
8581
+ continue;
8582
+ }
8583
+ if (pseudoStateSelector && PSEUDO_STATE_CHILD_PROP_SET.has(propName)) {
8584
+ childForwardedProps[propName] = propValue;
8585
+ continue;
8586
+ }
8372
8587
  visitProp(propValue, propName, styleContext, boxStyles, "prop");
8373
8588
  }
8374
8589
  if (typeof style === "string") {
@@ -15904,50 +16119,50 @@ const useActionEvents = (
15904
16119
  }
15905
16120
 
15906
16121
  return addManyEventListeners(element, {
15907
- cancel: (e) => {
16122
+ navi_cancel: (e) => {
15908
16123
  // cancel don't need to check for actionOrigin because
15909
16124
  // it's actually unrelated to a specific actions
15910
16125
  // in that sense it should likely be moved elsewhere as it's related to
15911
16126
  // interaction and constraint validation, not to a specific action
15912
16127
  onCancel?.(e, e.detail.reason);
15913
16128
  },
15914
- actionrequested: (e) => {
16129
+ navi_action_requested: (e) => {
15915
16130
  if (e.detail.actionOrigin !== actionOrigin) {
15916
16131
  return;
15917
16132
  }
15918
16133
  onRequested?.(e);
15919
16134
  },
15920
- actionprevented: (e) => {
16135
+ navi_action_prevented: (e) => {
15921
16136
  if (e.detail.actionOrigin !== actionOrigin) {
15922
16137
  return;
15923
16138
  }
15924
16139
  onPrevented?.(e);
15925
16140
  },
15926
- action: (e) => {
16141
+ navi_action: (e) => {
15927
16142
  if (e.detail.actionOrigin !== actionOrigin) {
15928
16143
  return;
15929
16144
  }
15930
16145
  onAction?.(e);
15931
16146
  },
15932
- actionstart: (e) => {
16147
+ navi_action_start: (e) => {
15933
16148
  if (e.detail.actionOrigin !== actionOrigin) {
15934
16149
  return;
15935
16150
  }
15936
16151
  onStart?.(e);
15937
16152
  },
15938
- actionabort: (e) => {
16153
+ navi_action_abort: (e) => {
15939
16154
  if (e.detail.actionOrigin !== actionOrigin) {
15940
16155
  return;
15941
16156
  }
15942
16157
  onAbort?.(e);
15943
16158
  },
15944
- actionerror: (e) => {
16159
+ navi_action_error: (e) => {
15945
16160
  if (e.detail.actionOrigin !== actionOrigin) {
15946
16161
  return;
15947
16162
  }
15948
16163
  onError?.(e.detail.error, e);
15949
16164
  },
15950
- actionend: onEnd,
16165
+ navi_action_end: onEnd,
15951
16166
  });
15952
16167
  }, [
15953
16168
  actionOrigin,
@@ -16024,7 +16239,7 @@ installImportMetaCssBuild(import.meta);
16024
16239
  * - Centers in viewport when no anchor element provided or anchor is too big
16025
16240
  */
16026
16241
 
16027
- const css$z = /* css */`
16242
+ const css$B = /* css */`
16028
16243
  @layer navi {
16029
16244
  .navi_callout {
16030
16245
  --callout-success-color: #4caf50;
@@ -16198,7 +16413,7 @@ const openCallout = (message, {
16198
16413
  showErrorStack,
16199
16414
  debug = false
16200
16415
  } = {}) => {
16201
- import.meta.css = [css$z, "@jsenv/navi/src/field/validation/callout/callout.js"];
16416
+ import.meta.css = [css$B, "@jsenv/navi/src/field/validation/callout/callout.js"];
16202
16417
  const callout = {
16203
16418
  opened: true,
16204
16419
  close: null,
@@ -18082,9 +18297,12 @@ const requestAction = (
18082
18297
 
18083
18298
  // If validation failed, dispatch actionprevented and return
18084
18299
  if (!isValid) {
18085
- const actionPreventedCustomEvent = new CustomEvent("actionprevented", {
18086
- detail: customEventDetail,
18087
- });
18300
+ const actionPreventedCustomEvent = new CustomEvent(
18301
+ "navi_action_prevented",
18302
+ {
18303
+ detail: customEventDetail,
18304
+ },
18305
+ );
18088
18306
  elementForDispatch.dispatchEvent(actionPreventedCustomEvent);
18089
18307
  return false;
18090
18308
  }
@@ -18096,16 +18314,19 @@ const requestAction = (
18096
18314
  if (confirmMessage) {
18097
18315
  // eslint-disable-next-line no-alert
18098
18316
  if (!window.confirm(confirmMessage)) {
18099
- const actionPreventedCustomEvent = new CustomEvent("actionprevented", {
18100
- detail: customEventDetail,
18101
- });
18317
+ const actionPreventedCustomEvent = new CustomEvent(
18318
+ "navi_action_prevented",
18319
+ {
18320
+ detail: customEventDetail,
18321
+ },
18322
+ );
18102
18323
  elementForDispatch.dispatchEvent(actionPreventedCustomEvent);
18103
18324
  return false;
18104
18325
  }
18105
18326
  }
18106
18327
 
18107
18328
  // All good, dispatch the action
18108
- const actionCustomEvent = new CustomEvent("action", {
18329
+ const actionCustomEvent = new CustomEvent("navi_action", {
18109
18330
  detail: customEventDetail,
18110
18331
  });
18111
18332
  elementForDispatch.dispatchEvent(actionCustomEvent);
@@ -18175,7 +18396,7 @@ const installCustomConstraintValidation = (
18175
18396
  }
18176
18397
 
18177
18398
  const dispatchCancelCustomEvent = (options) => {
18178
- const cancelEvent = new CustomEvent("cancel", options);
18399
+ const cancelEvent = new CustomEvent("navi_cancel", options);
18179
18400
  element.dispatchEvent(cancelEvent);
18180
18401
  };
18181
18402
  const closeElementValidationMessage = (reason) => {
@@ -18234,14 +18455,7 @@ const installCustomConstraintValidation = (
18234
18455
 
18235
18456
  resetValidity({ fromRequestAction });
18236
18457
  for (const constraint of constraintSet) {
18237
- // Some components (Select, List) proxy their value through a hidden <input>
18238
- // identified by data-input-proxy. When present, constraints run against
18239
- // that proxy element so they can read standard properties like .value,
18240
- // .required, .name without needing to know about each component's internals.
18241
- const inputProxySelector = element.getAttribute("data-input-proxy");
18242
- const fieldForConstraint = inputProxySelector
18243
- ? (element.ownerDocument.querySelector(inputProxySelector) ?? element)
18244
- : element;
18458
+ const fieldForConstraint = element;
18245
18459
  const constraintCleanupSet = new Set();
18246
18460
  const registerChange = (register) => {
18247
18461
  const registerResult = register(() => {
@@ -18440,11 +18654,16 @@ const installCustomConstraintValidation = (
18440
18654
  }
18441
18655
 
18442
18656
  checkValidity();
18657
+
18658
+ const resetOnInteraction = (e) => {
18659
+ customMessageMap.clear();
18660
+ closeElementValidationMessage(e.type);
18661
+ checkValidity();
18662
+ };
18663
+
18443
18664
  {
18444
- const oninput = () => {
18445
- customMessageMap.clear();
18446
- closeElementValidationMessage("input_event");
18447
- checkValidity();
18665
+ const oninput = (e) => {
18666
+ resetOnInteraction(e);
18448
18667
  };
18449
18668
  element.addEventListener("input", oninput);
18450
18669
  addTeardown(() => {
@@ -18452,15 +18671,67 @@ const installCustomConstraintValidation = (
18452
18671
  });
18453
18672
  }
18454
18673
 
18674
+ {
18675
+ // When the user clicks the field (or the interactive element rendered in place of it,
18676
+ // e.g. the .navi_select button for a hidden input), treat it as intent to fix the issue
18677
+ // and dismiss the callout — unless the status is "error", which requires explicit action.
18678
+ const interactionTarget = (() => {
18679
+ const renderedBy = element.getAttribute("data-rendered-by");
18680
+ if (renderedBy) {
18681
+ return element.closest(renderedBy) || element;
18682
+ }
18683
+ return element;
18684
+ })();
18685
+ const onmousedown = (e) => {
18686
+ if (!validationInterface.validationMessage) {
18687
+ return;
18688
+ }
18689
+ if (failedConstraintInfo && failedConstraintInfo.status === "error") {
18690
+ return;
18691
+ }
18692
+ resetOnInteraction(e);
18693
+ };
18694
+ interactionTarget.addEventListener("mousedown", onmousedown);
18695
+ addTeardown(() => {
18696
+ interactionTarget.removeEventListener("mousedown", onmousedown);
18697
+ });
18698
+ }
18699
+
18700
+ check_on_hidden_input_value: {
18701
+ // Hidden inputs (used by Select, List) don't fire "input" or "change" events
18702
+ // when their value is set programmatically. We intercept the value setter to
18703
+ // detect those changes and re-run validation.
18704
+ if (element.type !== "hidden") {
18705
+ break check_on_hidden_input_value;
18706
+ }
18707
+ const nativeDescriptor = Object.getOwnPropertyDescriptor(
18708
+ HTMLInputElement.prototype,
18709
+ "value",
18710
+ );
18711
+ Object.defineProperty(element, "value", {
18712
+ get() {
18713
+ return nativeDescriptor.get.call(this);
18714
+ },
18715
+ set(newValue) {
18716
+ nativeDescriptor.set.call(this, newValue);
18717
+ resetOnInteraction(new CustomEvent("programmatic_value_change"));
18718
+ },
18719
+ configurable: true,
18720
+ });
18721
+ addTeardown(() => {
18722
+ delete element.value;
18723
+ });
18724
+ }
18725
+
18455
18726
  {
18456
18727
  // this ensure we re-check validity (and remove message no longer relevant)
18457
18728
  // once the action ends (used to remove the NOT_BUSY_CONSTRAINT message)
18458
- const onactionend = () => {
18729
+ const onNaviActionEnd = () => {
18459
18730
  checkValidity();
18460
18731
  };
18461
- element.addEventListener("actionend", onactionend);
18732
+ element.addEventListener("navi_action_end", onNaviActionEnd);
18462
18733
  addTeardown(() => {
18463
- element.removeEventListener("actionend", onactionend);
18734
+ element.removeEventListener("navi_action_end", onNaviActionEnd);
18464
18735
  });
18465
18736
  }
18466
18737
 
@@ -18678,7 +18949,7 @@ const installCustomConstraintValidation = (
18678
18949
  form.setAttribute("novalidate", ""); // make sure browser don't prevent "submit" nor display messages
18679
18950
  const removeListener = addEventListener(form, "submit", (e) => {
18680
18951
  e.preventDefault();
18681
- const actionCustomEvent = new CustomEvent("action", {
18952
+ const actionCustomEvent = new CustomEvent("navi_action", {
18682
18953
  detail: {
18683
18954
  action: null,
18684
18955
  event: e,
@@ -18799,7 +19070,7 @@ const dispatchActionRequestedCustomEvent = (
18799
19070
  elementWithAction,
18800
19071
  { actionOrigin = "action_prop", event, requester },
18801
19072
  ) => {
18802
- const actionRequestedCustomEvent = new CustomEvent("actionrequested", {
19073
+ const actionRequestedCustomEvent = new CustomEvent("navi_action_requested", {
18803
19074
  cancelable: true,
18804
19075
  detail: {
18805
19076
  actionOrigin,
@@ -19712,7 +19983,16 @@ const useBoundAction = (action, actionParamsSignal) => {
19712
19983
  const actionCallbackRef = useRef();
19713
19984
 
19714
19985
  if (!action) {
19715
- return null;
19986
+ const existingAction = actionRef.current;
19987
+ if (existingAction) {
19988
+ return existingAction;
19989
+ }
19990
+ const noopAction = createAction(() => {}, { params: undefined });
19991
+ const noopActionBound = actionParamsSignal
19992
+ ? noopAction.bindParams(actionParamsSignal)
19993
+ : noopAction;
19994
+ actionRef.current = noopActionBound;
19995
+ return noopActionBound;
19716
19996
  }
19717
19997
  if (isFunctionButNotAnActionFunction(action)) {
19718
19998
  actionCallbackRef.current = action;
@@ -19875,7 +20155,7 @@ const useExecuteAction = (
19875
20155
  const validationMessageTarget = requester || elementRef.current;
19876
20156
  validationMessageTargetRef.current = validationMessageTarget;
19877
20157
 
19878
- dispatchCustomEvent("actionstart", {
20158
+ dispatchCustomEvent("navi_action_start", {
19879
20159
  detail: sharedActionEventDetail,
19880
20160
  });
19881
20161
 
@@ -19902,7 +20182,7 @@ const useExecuteAction = (
19902
20182
  // but other side effects might do this
19903
20183
  elementRef.current
19904
20184
  ) {
19905
- dispatchCustomEvent("actionabort", {
20185
+ dispatchCustomEvent("navi_action_abort", {
19906
20186
  detail: {
19907
20187
  ...sharedActionEventDetail,
19908
20188
  reason,
@@ -19917,7 +20197,7 @@ const useExecuteAction = (
19917
20197
  // but other side effects might do this
19918
20198
  elementRef.current
19919
20199
  ) {
19920
- dispatchCustomEvent("actionerror", {
20200
+ dispatchCustomEvent("navi_action_error", {
19921
20201
  detail: {
19922
20202
  ...sharedActionEventDetail,
19923
20203
  error,
@@ -19937,7 +20217,7 @@ const useExecuteAction = (
19937
20217
  // but other side effects might do this
19938
20218
  elementRef.current
19939
20219
  ) {
19940
- dispatchCustomEvent("actionend", {
20220
+ dispatchCustomEvent("navi_action_end", {
19941
20221
  detail: {
19942
20222
  ...sharedActionEventDetail,
19943
20223
  data,
@@ -20245,7 +20525,7 @@ const applyKeyboardShortcuts = (shortcuts, keyboardEvent) => {
20245
20525
  continue;
20246
20526
  }
20247
20527
  const returnValue = shortcutCandidate.handler(keyboardEvent);
20248
- if (returnValue) {
20528
+ if (returnValue === false) {
20249
20529
  keyboardEvent.preventDefault();
20250
20530
  }
20251
20531
  return shortcutCandidate;
@@ -20372,121 +20652,6 @@ const isSameKey = (browserEventKey, key) => {
20372
20652
  return false;
20373
20653
  };
20374
20654
 
20375
- /**
20376
- * Toggles a `data-dark-background` attribute on the referenced element based on its
20377
- * computed background color. Pair it with a CSS variable to get automatic
20378
- * light/dark text without hard-coding colors:
20379
- *
20380
- * ```css
20381
- * .my-element {
20382
- * --color-contrasting: black;
20383
- * &[data-dark-background] {
20384
- * --color-contrasting: white;
20385
- * }
20386
- * color: var(--color-contrasting);
20387
- * }
20388
- * ```
20389
- *
20390
- * - `data-dark-background` is **set** when the background is dark enough that white text
20391
- * provides better (or equal) contrast.
20392
- * - `data-dark-background` is **absent** when black text is the better choice.
20393
- *
20394
- * @param {import("preact").RefObject} ref - Ref to the element that receives
20395
- * the `data-dark-background` attribute and is also passed to `contrastColor` for
20396
- * resolving CSS variables.
20397
- * @param {object} [options]
20398
- * @param {string} [options.backgroundElementSelector] - CSS selector relative
20399
- * to `ref.current` pointing to a child element whose `background-color`
20400
- * should be tested instead of the element itself. Useful when the element
20401
- * has a transparent background but contains a coloured child (e.g. a fill
20402
- * bar inside a track).
20403
- * @param {string} [options.colorProperty] - CSS property to read instead of
20404
- * `background-color`. Useful for SVG elements where the color is expressed
20405
- * as `fill` or `stroke`.
20406
- */
20407
- const useDarkBackgroundAttribute = (
20408
- ref,
20409
- deps = [],
20410
- {
20411
- backgroundElementSelector,
20412
- colorProperty = "backgroundColor",
20413
- attributeName = "data-dark-background",
20414
- luminanceThreshold,
20415
- hardcoded = {},
20416
- } = {},
20417
- ) => {
20418
- const innerDeps = [
20419
- ...deps,
20420
- // ref can change if the component passes a different ref on different renders
20421
- // (e.g. to control which element's color is being checked by switching the ref)
20422
- ref,
20423
- // backgroundElementSelector can change if the component passes a different selector on different renders
20424
- // (e.g. to control which child element's color is being checked by switching the selector)
20425
- backgroundElementSelector,
20426
- colorProperty,
20427
- ];
20428
-
20429
- const hardcodedMap = new Map();
20430
- for (const key of Object.keys(hardcoded)) {
20431
- const value = hardcoded[key];
20432
- innerDeps.push(key, value);
20433
- const colorString = normalizeColorString(key);
20434
- hardcodedMap.set(colorString, value);
20435
- }
20436
-
20437
- useLayoutEffect(() => {
20438
- const el = ref.current;
20439
- if (!el) {
20440
- return undefined;
20441
- }
20442
- let elementToCheck = el;
20443
- if (backgroundElementSelector) {
20444
- elementToCheck = el.querySelector(backgroundElementSelector);
20445
- if (!elementToCheck) {
20446
- return undefined;
20447
- }
20448
- }
20449
- const updateAttribute = () => {
20450
- const computedStyle = getComputedStyle(elementToCheck);
20451
- const color = computedStyle[colorProperty];
20452
- if (!color) {
20453
- el.removeAttribute(attributeName);
20454
- return;
20455
- }
20456
- const colorString = normalizeColorString(color, el);
20457
- const hardcodedContrast = hardcodedMap.get(colorString);
20458
- let isDark;
20459
- if (hardcodedContrast) {
20460
- isDark = hardcodedContrast === "white";
20461
- } else if (luminanceThreshold !== undefined) {
20462
- const luminance = resolveColorLuminance(color, el);
20463
- isDark = luminance !== undefined && luminance <= luminanceThreshold;
20464
- } else {
20465
- isDark = contrastColor(color, el) === "white";
20466
- }
20467
- if (isDark) {
20468
- el.setAttribute(attributeName, "");
20469
- } else {
20470
- el.removeAttribute(attributeName);
20471
- }
20472
- };
20473
- updateAttribute();
20474
- el.addEventListener(NAVI_PSEUDO_STATE_CUSTOM_EVENT, updateAttribute);
20475
- return () => {
20476
- el.removeEventListener(NAVI_PSEUDO_STATE_CUSTOM_EVENT, updateAttribute);
20477
- el.removeAttribute(attributeName);
20478
- };
20479
- }, innerDeps);
20480
- };
20481
-
20482
- const normalizeColorString = (color, el) => {
20483
- const colorRgba = resolveCSSColor(color, el);
20484
- if (!colorRgba) {
20485
- return "";
20486
- }
20487
- return String(colorRgba);
20488
- };
20489
-
20490
20655
  const useInitialTextSelection = (ref, textSelection) => {
20491
20656
  const deps = [];
20492
20657
  if (Array.isArray(textSelection)) {
@@ -20586,7 +20751,8 @@ const selectByTextStrings = (element, range, startText, endText) => {
20586
20751
  }
20587
20752
  };
20588
20753
 
20589
- installImportMetaCssBuild(import.meta);const css$y = /* css */`
20754
+ installImportMetaCssBuild(import.meta);// https://jsfiddle.net/v5xzJ/4/
20755
+ const css$A = /* css */`
20590
20756
  @layer navi {
20591
20757
  .navi_text {
20592
20758
  &[data-skeleton] {
@@ -20601,10 +20767,6 @@ installImportMetaCssBuild(import.meta);const css$y = /* css */`
20601
20767
  .navi_text {
20602
20768
  position: relative;
20603
20769
 
20604
- &[data-dark-background] {
20605
- color: white;
20606
- }
20607
-
20608
20770
  /* There is a chrome specific bug that prevents text-transform: capitalize to be applied in nested DOM structure */
20609
20771
  /* The CSS below ensure capitalize is propagated to the bold clones */
20610
20772
  &[data-capitalize] {
@@ -20943,7 +21105,14 @@ const OverflowPinnedElementContext = createContext(null);
20943
21105
  * like the skeleton container).
20944
21106
  */
20945
21107
  const Text = props => {
20946
- import.meta.css = [css$y, "@jsenv/navi/src/text/text.jsx"];
21108
+ const defaultRef = useRef();
21109
+ const ref = props.ref || defaultRef;
21110
+ return jsx(TextDispatcher, {
21111
+ ...props,
21112
+ ref: ref
21113
+ });
21114
+ };
21115
+ const TextDispatcher = props => {
20947
21116
  if (props.loading || props.skeleton) {
20948
21117
  return jsx(TextSkeleton, {
20949
21118
  ...props
@@ -20964,10 +21133,81 @@ const Text = props => {
20964
21133
  ...props
20965
21134
  });
20966
21135
  }
20967
- return jsx(TextBasic, {
21136
+ return jsx(TextUI, {
20968
21137
  ...props
20969
21138
  });
20970
21139
  };
21140
+ const TextUI = props => {
21141
+ import.meta.css = [css$A, "@jsenv/navi/src/text/text.jsx"];
21142
+ let {
21143
+ ref,
21144
+ spacing,
21145
+ preventSpaceUnderlines = false,
21146
+ boldStable,
21147
+ holdSpaceForStyle,
21148
+ capitalize,
21149
+ children,
21150
+ childrenOutsideFlow,
21151
+ ...rest
21152
+ } = props;
21153
+ const defaultSpace = preventSpaceUnderlines ? FAKE_SPACE : REGULAR_SPACE;
21154
+ const resolvedSpacing = spacing ?? defaultSpace;
21155
+ const boxProps = {
21156
+ "as": "span",
21157
+ "data-capitalize": capitalize ? "" : undefined,
21158
+ ...rest,
21159
+ ref,
21160
+ "baseClassName": withPropsClassName("navi_text", rest.baseClassName)
21161
+ };
21162
+ const shouldPreserveSpacing = rest.as === "pre" || rest.flex || rest.grid;
21163
+ if (shouldPreserveSpacing) {
21164
+ boxProps.spacing = resolvedSpacing;
21165
+ } else {
21166
+ children = applySpacingOnTextChildren(children, resolvedSpacing, defaultSpace);
21167
+ }
21168
+ if (boldStable) {
21169
+ const {
21170
+ bold
21171
+ } = boxProps;
21172
+ return jsxs(Box, {
21173
+ ...boxProps,
21174
+ bold: undefined,
21175
+ "data-bold": bold ? "" : undefined,
21176
+ "data-contains-absolute-child": "",
21177
+ children: [jsx("span", {
21178
+ className: "navi_text_bold_background",
21179
+ "aria-hidden": "true",
21180
+ children: children
21181
+ }), children, childrenOutsideFlow]
21182
+ });
21183
+ }
21184
+ if (holdSpaceForStyle) {
21185
+ // The sizer technique prevents layout shifts when styles that affect text dimensions change.
21186
+ // - navi_text_sizer_placeholder: invisible, rendered with holdSpaceForStyle applied so it
21187
+ // always occupies the "maximum" dimensions (e.g. bold + larger font-size).
21188
+ // - navi_text_sizer_overlay: absolutely positioned on top, renders the actual visible text
21189
+ // with its current style. Transitions can be applied on this element from the outside.
21190
+ return jsxs(Box, {
21191
+ ...boxProps,
21192
+ children: [jsxs("span", {
21193
+ className: "navi_text_sizer",
21194
+ children: [jsx("span", {
21195
+ className: "navi_text_sizer_placeholder",
21196
+ "aria-hidden": "true",
21197
+ style: holdSpaceForStyle,
21198
+ children: children
21199
+ }), jsx("span", {
21200
+ className: "navi_text_sizer_overlay",
21201
+ children: children
21202
+ })]
21203
+ }), childrenOutsideFlow]
21204
+ });
21205
+ }
21206
+ return jsxs(Box, {
21207
+ ...boxProps,
21208
+ children: [children, childrenOutsideFlow]
21209
+ });
21210
+ };
20971
21211
  const TextSkeleton = ({
20972
21212
  loading,
20973
21213
  children,
@@ -20993,7 +21233,7 @@ const TextSkeleton = ({
20993
21233
  "aria-hidden": "true",
20994
21234
  children: "W"
20995
21235
  });
20996
- return jsx(Text, {
21236
+ return jsx(TextDispatcher, {
20997
21237
  "data-skeleton": "",
20998
21238
  "data-loading": loading ? "" : undefined,
20999
21239
  ...props,
@@ -21009,7 +21249,7 @@ const TextOverflow = ({
21009
21249
  ...rest
21010
21250
  }) => {
21011
21251
  const [OverflowPinnedElement, setOverflowPinnedElement] = useState(null);
21012
- return jsx(Text, {
21252
+ return jsx(TextDispatcher, {
21013
21253
  flex: true,
21014
21254
  block: true,
21015
21255
  as: "div",
@@ -21057,92 +21297,19 @@ const TextOverflowPinned = ({
21057
21297
  return text;
21058
21298
  };
21059
21299
  const TextWithSelectRange = ({
21300
+ ref,
21060
21301
  selectRange,
21061
21302
  ...props
21062
21303
  }) => {
21063
- const defaultRef = useRef();
21064
- const ref = props.ref || defaultRef;
21065
21304
  useInitialTextSelection(ref, selectRange);
21066
- return jsx(Text, {
21305
+ return jsx(TextDispatcher, {
21306
+ ...props,
21067
21307
  ref: ref,
21068
- ...props
21069
- });
21070
- };
21071
- const TextBasic = ({
21072
- spacing,
21073
- preventSpaceUnderlines = false,
21074
- boldStable,
21075
- holdSpaceForStyle,
21076
- capitalize,
21077
- children,
21078
- childrenOutsideFlow,
21079
- basePseudoState,
21080
- ...rest
21081
- }) => {
21082
- const defaultRef = useRef();
21083
- const ref = rest.ref || defaultRef;
21084
- const bgDeps = basePseudoState ? Object.values(basePseudoState) : [];
21085
- useDarkBackgroundAttribute(ref, bgDeps);
21086
- const defaultSpace = preventSpaceUnderlines ? FAKE_SPACE : REGULAR_SPACE;
21087
- const resolvedSpacing = spacing ?? defaultSpace;
21088
- const boxProps = {
21089
- "as": "span",
21090
- "data-capitalize": capitalize ? "" : undefined,
21091
- ...rest,
21092
- ref,
21093
- "baseClassName": withPropsClassName("navi_text", rest.baseClassName)
21094
- };
21095
- const shouldPreserveSpacing = rest.as === "pre" || rest.flex || rest.grid;
21096
- if (shouldPreserveSpacing) {
21097
- boxProps.spacing = resolvedSpacing;
21098
- } else {
21099
- children = applySpacingOnTextChildren(children, resolvedSpacing, defaultSpace);
21100
- }
21101
- if (boldStable) {
21102
- const {
21103
- bold
21104
- } = boxProps;
21105
- return jsxs(Box, {
21106
- ...boxProps,
21107
- bold: undefined,
21108
- "data-bold": bold ? "" : undefined,
21109
- "data-contains-absolute-child": "",
21110
- children: [jsx("span", {
21111
- className: "navi_text_bold_background",
21112
- "aria-hidden": "true",
21113
- children: children
21114
- }), children, childrenOutsideFlow]
21115
- });
21116
- }
21117
- if (holdSpaceForStyle) {
21118
- // The sizer technique prevents layout shifts when styles that affect text dimensions change.
21119
- // - navi_text_sizer_placeholder: invisible, rendered with holdSpaceForStyle applied so it
21120
- // always occupies the "maximum" dimensions (e.g. bold + larger font-size).
21121
- // - navi_text_sizer_overlay: absolutely positioned on top, renders the actual visible text
21122
- // with its current style. Transitions can be applied on this element from the outside.
21123
- return jsxs(Box, {
21124
- ...boxProps,
21125
- children: [jsxs("span", {
21126
- className: "navi_text_sizer",
21127
- children: [jsx("span", {
21128
- className: "navi_text_sizer_placeholder",
21129
- "aria-hidden": "true",
21130
- style: holdSpaceForStyle,
21131
- children: children
21132
- }), jsx("span", {
21133
- className: "navi_text_sizer_overlay",
21134
- children: children
21135
- })]
21136
- }), childrenOutsideFlow]
21137
- });
21138
- }
21139
- return jsxs(Box, {
21140
- ...boxProps,
21141
- children: [children, childrenOutsideFlow]
21308
+ selectRange: undefined
21142
21309
  });
21143
21310
  };
21144
21311
 
21145
- installImportMetaCssBuild(import.meta);const css$x = /* css */`
21312
+ installImportMetaCssBuild(import.meta);const css$z = /* css */`
21146
21313
  .navi_text_anchor {
21147
21314
  vertical-align: baseline;
21148
21315
  user-select: none;
@@ -21177,7 +21344,7 @@ const TextAnchor = ({
21177
21344
  textSize,
21178
21345
  lineLayout
21179
21346
  }) => {
21180
- import.meta.css = [css$x, "@jsenv/navi/src/text/text_anchor.jsx"];
21347
+ import.meta.css = [css$z, "@jsenv/navi/src/text/text_anchor.jsx"];
21181
21348
  const anchorRef = useRef();
21182
21349
  useLayoutEffect(() => {
21183
21350
  const anchorEl = anchorRef.current;
@@ -21275,7 +21442,7 @@ const computeTopOffset = ({
21275
21442
  };
21276
21443
  const charTopCanvas = document.createElement("canvas");
21277
21444
 
21278
- installImportMetaCssBuild(import.meta);const css$w = /* css */`
21445
+ installImportMetaCssBuild(import.meta);const css$y = /* css */`
21279
21446
  @layer navi {
21280
21447
  /* Ensure data attributes from box.jsx can win to update display */
21281
21448
  .navi_icon {
@@ -21348,7 +21515,7 @@ const Icon = ({
21348
21515
  lineLayout,
21349
21516
  ...props
21350
21517
  }) => {
21351
- import.meta.css = [css$w, "@jsenv/navi/src/text/icon.jsx"];
21518
+ import.meta.css = [css$y, "@jsenv/navi/src/text/icon.jsx"];
21352
21519
  const innerChildren = href ? jsx("svg", {
21353
21520
  width: "100%",
21354
21521
  height: "100%",
@@ -21676,6 +21843,100 @@ document.body.addEventListener(
21676
21843
  { capture: true },
21677
21844
  );
21678
21845
 
21846
+ const LIGHT_ACCENT_ATTRIBUTE = "data-accent-light";
21847
+ const VERY_LIGHT_ACCENT_ATTRIBUTE = "data-accent-very-light";
21848
+ const DARK_CONTRAST_ATTRIBUTE = "data-accent-needs-dark-fg";
21849
+ const LIGHT_LUMINANCE_THRESHOLD = 0.5;
21850
+ const VERY_LIGHT_LUMINANCE_THRESHOLD = 0.92;
21851
+ const DARK_CONTRAST_LIGHTNESS_THRESHOLD = 0.65;
21852
+
21853
+ /**
21854
+ * Sets data attributes on an element based on the OKLCH lightness and contrast
21855
+ * of a CSS color (typically an accent/brand color). All thresholds use OKLCH L
21856
+ * (0–1, perceptually uniform scale).
21857
+ *
21858
+ * Three boolean attributes are managed independently:
21859
+ *
21860
+ * ## `data-accent-light` (set when OKLCH L > 0.5)
21861
+ * The accent color is perceptually light (orange, green, pink, yellow…).
21862
+ * Use to adjust color-mix direction so hover/active effects darken toward
21863
+ * black instead of lightening toward white.
21864
+ *
21865
+ * ## `data-accent-very-light` (set when OKLCH L > 0.92)
21866
+ * The accent color is near-white or white. Use to show a grey background on
21867
+ * unchecked state so the component boundary remains visible against white
21868
+ * page backgrounds.
21869
+ *
21870
+ * ## `data-accent-needs-dark-fg` (set when OKLCH L > 0.65)
21871
+ * The best contrasting foreground color against the accent is dark (black).
21872
+ * Use to render checkmarks, icons, or text in a dark color instead of white.
21873
+ *
21874
+ * @param {import("preact").RefObject} ref - Ref to the root element that receives the attributes.
21875
+ * @param {string} accentColor - The accent color value. When it changes, attributes are recomputed.
21876
+ * @param {object} [options]
21877
+ * @param {string} [options.elementSelector] - CSS selector to find the element whose computed color is read.
21878
+ * Defaults to the root element itself. Useful when the color is applied to a probe/child element.
21879
+ * @param {string} [options.colorProperty="backgroundColor"] - Computed style property to read (e.g. "color", "borderColor").
21880
+ */
21881
+ const useAccentColorAttributes = (
21882
+ ref,
21883
+ accentColor,
21884
+ { elementSelector, colorProperty = "backgroundColor" } = {},
21885
+ ) => {
21886
+ useLayoutEffect(() => {
21887
+ const el = ref.current;
21888
+ if (!el) {
21889
+ return undefined;
21890
+ }
21891
+ let elementToCheck = el;
21892
+ if (elementSelector) {
21893
+ elementToCheck = el.querySelector(elementSelector);
21894
+ if (!elementToCheck) {
21895
+ return undefined;
21896
+ }
21897
+ }
21898
+ const updateAttributes = () => {
21899
+ const computedStyle = getComputedStyle(elementToCheck);
21900
+ const color = computedStyle[colorProperty];
21901
+ if (!color) {
21902
+ el.removeAttribute(LIGHT_ACCENT_ATTRIBUTE);
21903
+ el.removeAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE);
21904
+ el.removeAttribute(DARK_CONTRAST_ATTRIBUTE);
21905
+ return;
21906
+ }
21907
+ const luminance = resolveOklchLightness(color, el);
21908
+ if (luminance !== null && luminance > LIGHT_LUMINANCE_THRESHOLD) {
21909
+ el.setAttribute(LIGHT_ACCENT_ATTRIBUTE, "");
21910
+ } else {
21911
+ el.removeAttribute(LIGHT_ACCENT_ATTRIBUTE);
21912
+ }
21913
+ if (luminance !== null && luminance > VERY_LIGHT_LUMINANCE_THRESHOLD) {
21914
+ el.setAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE, "");
21915
+ } else {
21916
+ el.removeAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE);
21917
+ }
21918
+ const bestContrast = contrastColor(
21919
+ color,
21920
+ el,
21921
+ DARK_CONTRAST_LIGHTNESS_THRESHOLD,
21922
+ );
21923
+ if (bestContrast === "black") {
21924
+ el.setAttribute(DARK_CONTRAST_ATTRIBUTE, "");
21925
+ } else {
21926
+ el.removeAttribute(DARK_CONTRAST_ATTRIBUTE);
21927
+ }
21928
+ };
21929
+ updateAttributes();
21930
+ el.addEventListener(NAVI_PSEUDO_STATE_CUSTOM_EVENT, updateAttributes);
21931
+ return () => {
21932
+ el.removeEventListener(NAVI_PSEUDO_STATE_CUSTOM_EVENT, updateAttributes);
21933
+ el.removeAttribute(LIGHT_ACCENT_ATTRIBUTE);
21934
+ el.removeAttribute(VERY_LIGHT_ACCENT_ATTRIBUTE);
21935
+ el.removeAttribute(DARK_CONTRAST_ATTRIBUTE);
21936
+ };
21937
+ }, [ref, accentColor, elementSelector, colorProperty]);
21938
+ };
21939
+
21679
21940
  const useFormEvents = (
21680
21941
  elementRef,
21681
21942
  {
@@ -21713,12 +21974,12 @@ const useFormEvents = (
21713
21974
  }
21714
21975
  return addManyEventListeners(form, {
21715
21976
  reset: onFormReset,
21716
- actionrequested: onFormActionRequested,
21717
- actionprevented: onFormActionPrevented,
21718
- actionstart: onFormActionStart,
21719
- actionabort: onFormActionAbort,
21720
- actionerror: onFormActionError,
21721
- actionend: onFormActionEnd,
21977
+ navi_action_requested: onFormActionRequested,
21978
+ navi_action_prevented: onFormActionPrevented,
21979
+ navi_action_start: onFormActionStart,
21980
+ navi_action_abort: onFormActionAbort,
21981
+ navi_action_error: onFormActionError,
21982
+ navi_action_end: onFormActionEnd,
21722
21983
  });
21723
21984
  }, [
21724
21985
  onFormReset,
@@ -22053,7 +22314,7 @@ const useUIGroupStateController = (
22053
22314
  return notifyParentAboutChildUnmount;
22054
22315
  }, []);
22055
22316
 
22056
- const onChange = (_, e) => {
22317
+ const onChange = (_, e, { notifyExternal = true } = {}) => {
22057
22318
  if (groupIsRenderingRef.current) {
22058
22319
  pendingChangeRef.current = true;
22059
22320
  return;
@@ -22063,7 +22324,7 @@ const useUIGroupStateController = (
22063
22324
  emptyState,
22064
22325
  );
22065
22326
  const uiStateController = uiStateControllerRef.current;
22066
- uiStateController.setUIState(newUIState, e);
22327
+ uiStateController.setUIState(newUIState, e, { notifyExternal });
22067
22328
  };
22068
22329
 
22069
22330
  useLayoutEffect(() => {
@@ -22073,6 +22334,7 @@ const useUIGroupStateController = (
22073
22334
  onChange(
22074
22335
  null,
22075
22336
  new CustomEvent(`${componentType}_batched_ui_state_update`),
22337
+ { notifyExternal: false },
22076
22338
  );
22077
22339
  }
22078
22340
  });
@@ -22098,7 +22360,7 @@ const useUIGroupStateController = (
22098
22360
  value,
22099
22361
  uiAction,
22100
22362
  uiState: emptyState,
22101
- setUIState: (newUIState, e) => {
22363
+ setUIState: (newUIState, e, { notifyExternal = true } = {}) => {
22102
22364
  const currentUIState = uiStateController.uiState;
22103
22365
  if (newUIState === currentUIState) {
22104
22366
  return;
@@ -22108,7 +22370,9 @@ const useUIGroupStateController = (
22108
22370
  `${componentType}.setUIState(${JSON.stringify(newUIState)}, "${e.type}") -> updates from ${JSON.stringify(currentUIState)} to ${JSON.stringify(newUIState)}`,
22109
22371
  );
22110
22372
  publishUIState(newUIState);
22111
- uiStateController.onUIStateChange?.(newUIState, e);
22373
+ if (notifyExternal) {
22374
+ uiStateController.uiAction?.(newUIState, e);
22375
+ }
22112
22376
  notifyParentAboutChildUIStateChange(e);
22113
22377
  },
22114
22378
  registerChild: (childUIStateController) => {
@@ -22123,6 +22387,7 @@ const useUIGroupStateController = (
22123
22387
  onChange(
22124
22388
  childUIStateController,
22125
22389
  new CustomEvent(`${childComponentType}_mount`),
22390
+ { notifyExternal: false },
22126
22391
  );
22127
22392
  },
22128
22393
  onChildUIStateChange: (childUIStateController, e) => {
@@ -22152,6 +22417,7 @@ const useUIGroupStateController = (
22152
22417
  onChange(
22153
22418
  childUIStateController,
22154
22419
  new CustomEvent(`${childComponentType}_unmount`),
22420
+ { notifyExternal: false },
22155
22421
  );
22156
22422
  },
22157
22423
  resetUIState: (e) => {
@@ -22199,7 +22465,7 @@ const useUIState = (uiStateController) => {
22199
22465
  return trackedUIState;
22200
22466
  };
22201
22467
 
22202
- installImportMetaCssBuild(import.meta);const css$v = /* css */`
22468
+ installImportMetaCssBuild(import.meta);const css$x = /* css */`
22203
22469
  @layer navi {
22204
22470
  .navi_button {
22205
22471
  --button-outline-width: 1px;
@@ -22294,8 +22560,8 @@ installImportMetaCssBuild(import.meta);const css$v = /* css */`
22294
22560
  touch-action: manipulation;
22295
22561
  user-select: none;
22296
22562
 
22297
- &[data-dark-background] {
22298
- --button-color: white;
22563
+ &[data-accent-needs-dark-fg] {
22564
+ --button-color: black;
22299
22565
  }
22300
22566
 
22301
22567
  &[data-icon] {
@@ -22513,7 +22779,7 @@ const ButtonUI = props => {
22513
22779
  children,
22514
22780
  ...rest
22515
22781
  } = props;
22516
- import.meta.css = [css$v, "@jsenv/navi/src/field/button.jsx"];
22782
+ import.meta.css = [css$x, "@jsenv/navi/src/field/button.jsx"];
22517
22783
  const contextLoading = useContext(LoadingContext);
22518
22784
  const contextLoadingElement = useContext(LoadingElementContext);
22519
22785
  const contextReadOnly = useContext(ReadOnlyContext);
@@ -22537,7 +22803,7 @@ const ButtonUI = props => {
22537
22803
  innerTarget = target === undefined ? isSameSite ? undefined : "_blank" : target;
22538
22804
  innerRel = rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel;
22539
22805
  }
22540
- useDarkBackgroundAttribute(ref, [innerLoading, innerDisabled, innerReadOnly]);
22806
+ useAccentColorAttributes(ref, null);
22541
22807
  const renderButtonContent = buttonProps => {
22542
22808
  return jsxs(Text, {
22543
22809
  ...buttonProps,
@@ -22760,7 +23026,7 @@ const ButtonWithActionInsideForm = props => {
22760
23026
  action: undefined,
22761
23027
  type: type,
22762
23028
  loading: innerLoading,
22763
- onactionrequested: e => {
23029
+ onnavi_action_requested: e => {
22764
23030
  forwardActionRequested(e, actionBoundToFormParams, e.target.form);
22765
23031
  },
22766
23032
  children: children
@@ -22818,7 +23084,7 @@ const WarningSvg = () => {
22818
23084
  });
22819
23085
  };
22820
23086
 
22821
- installImportMetaCssBuild(import.meta);import.meta.css = [/* css */`
23087
+ installImportMetaCssBuild(import.meta);const css$w = /* css */`
22822
23088
  @layer navi {
22823
23089
  .navi_message_box {
22824
23090
  --background-color-info: var(--navi-info-color-light);
@@ -22861,10 +23127,7 @@ installImportMetaCssBuild(import.meta);import.meta.css = [/* css */`
22861
23127
  border-top-left-radius: 6px;
22862
23128
  border-bottom-left-radius: 6px;
22863
23129
  }
22864
- `, "@jsenv/navi/src/text/message_box.jsx"];
22865
- const MessageBoxPseudoClasses = [":-navi-status-info", ":-navi-status-success", ":-navi-status-warning", ":-navi-status-error"];
22866
- const MessageBoxStatusContext = createContext();
22867
- const MessageBoxReportTitleChildContext = createContext();
23130
+ `;
22868
23131
  const MessageBox = ({
22869
23132
  status = "info",
22870
23133
  padding = "sm",
@@ -22874,6 +23137,7 @@ const MessageBox = ({
22874
23137
  onClose,
22875
23138
  ...rest
22876
23139
  }) => {
23140
+ import.meta.css = [css$w, "@jsenv/navi/src/text/message_box.jsx"];
22877
23141
  const [hasTitleChild, setHasTitleChild] = useState(false);
22878
23142
  const innerLeftStripe = leftStripe === undefined ? hasTitleChild : leftStripe;
22879
23143
  if (icon === true) {
@@ -22928,8 +23192,11 @@ const MessageBox = ({
22928
23192
  })
22929
23193
  });
22930
23194
  };
23195
+ const MessageBoxPseudoClasses = [":-navi-status-info", ":-navi-status-success", ":-navi-status-warning", ":-navi-status-error"];
23196
+ const MessageBoxStatusContext = createContext();
23197
+ const MessageBoxReportTitleChildContext = createContext();
22931
23198
 
22932
- installImportMetaCssBuild(import.meta);import.meta.css = [/* css */`
23199
+ installImportMetaCssBuild(import.meta);const css$v = /* css */`
22933
23200
  .navi_message_box {
22934
23201
  .navi_title {
22935
23202
  margin-top: 0;
@@ -22937,13 +23204,9 @@ installImportMetaCssBuild(import.meta);import.meta.css = [/* css */`
22937
23204
  color: var(--x-message-color);
22938
23205
  }
22939
23206
  }
22940
- `, "@jsenv/navi/src/text/title.jsx"];
22941
- const TitleLevelContext = createContext();
22942
- const useTitleLevel = () => {
22943
- return useContext(TitleLevelContext);
22944
- };
22945
- const TitlePseudoClasses = [":hover"];
23207
+ `;
22946
23208
  const Title = props => {
23209
+ import.meta.css = [css$v, "@jsenv/navi/src/text/title.jsx"];
22947
23210
  const messageBoxStatus = useContext(MessageBoxStatusContext);
22948
23211
  const innerAs = props.as || (messageBoxStatus ? "h4" : "h1");
22949
23212
  const titleLevel = parseInt(innerAs.slice(1));
@@ -22961,6 +23224,11 @@ const Title = props => {
22961
23224
  })
22962
23225
  });
22963
23226
  };
23227
+ const TitleLevelContext = createContext();
23228
+ const useTitleLevel = () => {
23229
+ return useContext(TitleLevelContext);
23230
+ };
23231
+ const TitlePseudoClasses = [":hover"];
22964
23232
 
22965
23233
  /**
22966
23234
  * Hook that reactively checks if a URL is visited.
@@ -23161,11 +23429,6 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
23161
23429
  }
23162
23430
  }
23163
23431
 
23164
- /* Dark background */
23165
- &[data-dark-background].navi_text {
23166
- --x-link-contrasting-color: white;
23167
- --x-link-color: var(--link-color, white);
23168
- }
23169
23432
  /* Interactive */
23170
23433
  &[data-interactive] {
23171
23434
  cursor: pointer;
@@ -24331,22 +24594,6 @@ installImportMetaCssBuild(import.meta);const css$q = /* css */`
24331
24594
  }
24332
24595
  }
24333
24596
  `;
24334
- const ReportReadOnlyOnLabelContext = createContext();
24335
- const ReportDisabledOnLabelContext = createContext();
24336
- const ReportInteractiveOnLabelContext = createContext();
24337
- const reportReadOnlyToLabel = value => {
24338
- const reportReadOnly = useContext(ReportReadOnlyOnLabelContext);
24339
- reportReadOnly?.(value);
24340
- };
24341
- const reportInteractiveToLabel = value => {
24342
- const reportInteractive = useContext(ReportInteractiveOnLabelContext);
24343
- reportInteractive?.(value);
24344
- };
24345
- const reportDisabledToLabel = value => {
24346
- const reportDisabled = useContext(ReportDisabledOnLabelContext);
24347
- reportDisabled?.(value);
24348
- };
24349
- const LabelPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
24350
24597
  const Label = props => {
24351
24598
  import.meta.css = [css$q, "@jsenv/navi/src/field/label.jsx"];
24352
24599
  const {
@@ -24381,6 +24628,22 @@ const Label = props => {
24381
24628
  })
24382
24629
  });
24383
24630
  };
24631
+ const LabelPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
24632
+ const ReportReadOnlyOnLabelContext = createContext();
24633
+ const ReportDisabledOnLabelContext = createContext();
24634
+ const ReportInteractiveOnLabelContext = createContext();
24635
+ const reportReadOnlyToLabel = value => {
24636
+ const reportReadOnly = useContext(ReportReadOnlyOnLabelContext);
24637
+ reportReadOnly?.(value);
24638
+ };
24639
+ const reportInteractiveToLabel = value => {
24640
+ const reportInteractive = useContext(ReportInteractiveOnLabelContext);
24641
+ reportInteractive?.(value);
24642
+ };
24643
+ const reportDisabledToLabel = value => {
24644
+ const reportDisabled = useContext(ReportDisabledOnLabelContext);
24645
+ reportDisabled?.(value);
24646
+ };
24384
24647
 
24385
24648
  installImportMetaCssBuild(import.meta);const css$p = /* css */`
24386
24649
  @layer navi {
@@ -24396,15 +24659,15 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
24396
24659
  --outline-color: var(--navi-focus-outline-color);
24397
24660
  --loader-color: var(--navi-loader-color);
24398
24661
  --border-color: light-dark(#767676, #8e8e93);
24399
- --background-color: rgba(0, 0, 0, 0.15);
24662
+ --background-color: white;
24400
24663
  --accent-color: light-dark(#4476ff, #3b82f6);
24401
24664
  --background-color-checked: var(--accent-color);
24402
24665
  --border-color-checked: var(--accent-color);
24403
- --checkmark-color: rgb(55, 55, 55);
24666
+ --checkmark-color: white;
24404
24667
  --cursor: pointer;
24405
24668
  --color-mix-light: black;
24406
24669
  --color-mix-dark: white;
24407
- --color-mix: var(--color-mix-light);
24670
+ --color-mix: var(--color-mix-dark);
24408
24671
 
24409
24672
  /* Hover */
24410
24673
  --border-color-hover: color-mix(in srgb, var(--border-color) 60%, black);
@@ -24497,11 +24760,6 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
24497
24760
  var(--button-background-color) 95%,
24498
24761
  black
24499
24762
  );
24500
-
24501
- &[data-dark-background] {
24502
- --color-mix: var(--color-mix-dark);
24503
- --checkmark-color: white;
24504
- }
24505
24763
  }
24506
24764
  }
24507
24765
 
@@ -24602,12 +24860,9 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
24602
24860
  }
24603
24861
  }
24604
24862
 
24605
- &[data-dark-background]:not([data-appearance="toggle"]) {
24606
- --x-background-color: white;
24607
- --x-checkmark-color: var(--checkmark-color);
24608
- &[data-checked] {
24609
- --x-background-color: var(--background-color-checked);
24610
- }
24863
+ /* Accent color adaptations */
24864
+ &[data-accent-light] {
24865
+ --color-mix: var(--color-mix-light);
24611
24866
  }
24612
24867
 
24613
24868
  /* Checkbox appearance */
@@ -24629,6 +24884,16 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
24629
24884
  transition-timing-function: ease;
24630
24885
  }
24631
24886
  }
24887
+
24888
+ &[data-accent-very-light] {
24889
+ --x-background-color: rgba(0, 0, 0, 0.15);
24890
+ &[data-checked] {
24891
+ --x-background-color: var(--background-color-checked);
24892
+ }
24893
+ }
24894
+ &[data-accent-needs-dark-fg] {
24895
+ --x-checkmark-color: rgb(55, 55, 55);
24896
+ }
24632
24897
  }
24633
24898
 
24634
24899
  /* Toggle appearance */
@@ -24689,6 +24954,10 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
24689
24954
  );
24690
24955
  }
24691
24956
  }
24957
+
24958
+ &[data-accent-very-light] {
24959
+ --toggle-thumb-color: rgb(55, 55, 55);
24960
+ }
24692
24961
  }
24693
24962
 
24694
24963
  &[data-appearance="icon"] {
@@ -24839,9 +25108,8 @@ const InputCheckboxUI = props => {
24839
25108
  });
24840
25109
  const renderCheckboxMemoized = useCallback(renderCheckbox, [id, innerName, checked, innerRequired]);
24841
25110
  const boxRef = useRef();
24842
- useDarkBackgroundAttribute(boxRef, [accentColor], {
24843
- backgroundElementSelector: ".navi_checkbox_accent_probe",
24844
- luminanceThreshold: 0.82
25111
+ useAccentColorAttributes(boxRef, accentColor, {
25112
+ elementSelector: ".navi_checkbox_accent_probe"
24845
25113
  });
24846
25114
  return jsxs(Box, {
24847
25115
  as: "span",
@@ -25164,12 +25432,12 @@ installImportMetaCssBuild(import.meta);const css$o = /* css */`
25164
25432
 
25165
25433
  --color-mix-light: black;
25166
25434
  --color-mix-dark: white;
25167
- --color-mix: var(--color-mix-light);
25435
+ --color-mix: var(--color-mix-dark);
25168
25436
 
25169
25437
  --outline-color: var(--navi-focus-outline-color);
25170
25438
  --loader-color: var(--navi-loader-color);
25171
25439
  --border-color: light-dark(#767676, #8e8e93);
25172
- --x-background-color: rgba(0, 0, 0, 0.15);
25440
+ --background-color: white;
25173
25441
  --accent-color: light-dark(#4476ff, #3b82f6);
25174
25442
  --radiomark-color: var(--accent-color);
25175
25443
  --border-color-checked: var(--accent-color);
@@ -25287,7 +25555,6 @@ installImportMetaCssBuild(import.meta);const css$o = /* css */`
25287
25555
  }
25288
25556
  /* Checked */
25289
25557
  &[data-checked] {
25290
- --x-background-color: transparent;
25291
25558
  --x-border-color: var(--border-color-checked);
25292
25559
 
25293
25560
  &[data-hover] {
@@ -25324,11 +25591,14 @@ installImportMetaCssBuild(import.meta);const css$o = /* css */`
25324
25591
  }
25325
25592
  }
25326
25593
 
25327
- &[data-dark-background] {
25328
- --x-background-color: white;
25329
- --color-mix: var(--color-mix-dark);
25594
+ &[data-accent-light] {
25595
+ --color-mix: var(--color-mix-light);
25596
+ }
25597
+
25598
+ &[data-accent-very-light] {
25599
+ --x-background-color: rgba(0, 0, 0, 0.15);
25330
25600
  &[data-checked] {
25331
- --x-background-color: white;
25601
+ --x-background-color: rgba(0, 0, 0, 0.15);
25332
25602
  }
25333
25603
  }
25334
25604
 
@@ -25599,9 +25869,8 @@ const InputRadioUI = props => {
25599
25869
  });
25600
25870
  const renderRadioMemoized = useCallback(renderRadio, [innerName, checked, innerRequired]);
25601
25871
  const boxRef = useRef();
25602
- useDarkBackgroundAttribute(boxRef, [remainingProps.accentColor], {
25603
- backgroundElementSelector: ".navi_radio_accent_probe",
25604
- luminanceThreshold: 0.82
25872
+ useAccentColorAttributes(boxRef, remainingProps.accentColor, {
25873
+ elementSelector: ".navi_radio_accent_probe"
25605
25874
  });
25606
25875
  return jsxs(Box, {
25607
25876
  as: "span",
@@ -25730,6 +25999,9 @@ installImportMetaCssBuild(import.meta);const css$n = /* css */`
25730
25999
  --outline-color: var(--navi-focus-outline-color);
25731
26000
  --loader-color: var(--navi-loader-color);
25732
26001
  --accent-color: rgb(24, 117, 255);
26002
+ --color-mix-light: black;
26003
+ --color-mix-dark: white;
26004
+ --color-mix: var(--color-mix-dark);
25733
26005
 
25734
26006
  --border-color: rgb(150, 150, 150);
25735
26007
  --track-border-color: color-mix(
@@ -25747,9 +26019,21 @@ installImportMetaCssBuild(import.meta);const css$n = /* css */`
25747
26019
  var(--track-border-color) 75%,
25748
26020
  black
25749
26021
  );
25750
- --track-color-hover: color-mix(in srgb, var(--fill-color) 95%, black);
25751
- --fill-color-hover: color-mix(in srgb, var(--fill-color) 80%, black);
25752
- --thumb-color-hover: color-mix(in srgb, var(--thumb-color) 80%, black);
26022
+ --track-color-hover: color-mix(
26023
+ in srgb,
26024
+ var(--fill-color) 95%,
26025
+ var(--color-mix)
26026
+ );
26027
+ --fill-color-hover: color-mix(
26028
+ in srgb,
26029
+ var(--fill-color) 80%,
26030
+ var(--color-mix)
26031
+ );
26032
+ --thumb-color-hover: color-mix(
26033
+ in srgb,
26034
+ var(--thumb-color) 80%,
26035
+ var(--color-mix)
26036
+ );
25753
26037
  /* Pressed */
25754
26038
  --border-color-pressed: color-mix(
25755
26039
  in srgb,
@@ -25826,6 +26110,15 @@ installImportMetaCssBuild(import.meta);const css$n = /* css */`
25826
26110
  }
25827
26111
  }
25828
26112
 
26113
+ .navi_input_range_accent_probe {
26114
+ position: absolute;
26115
+ width: 0;
26116
+ height: 0;
26117
+ background-color: var(--accent-color);
26118
+ visibility: hidden;
26119
+ pointer-events: none;
26120
+ }
26121
+
25829
26122
  .navi_input_range_background {
25830
26123
  position: absolute;
25831
26124
  width: 100%;
@@ -25910,77 +26203,51 @@ installImportMetaCssBuild(import.meta);const css$n = /* css */`
25910
26203
  --x-fill-color: var(--fill-color-disabled);
25911
26204
  --x-thumb-color: var(--thumb-color-disabled);
25912
26205
  --x-thumb-cursor: default;
26206
+ --x-accent-color: var(--accent-color-disabled);
26207
+ }
26208
+ /* Callout (info, warning, error) */
26209
+ &[data-callout] {
25913
26210
  }
25914
- }
25915
26211
 
25916
- /* Disabled */
25917
- .navi_input_range[data-disabled] {
25918
- --x-background-color: var(--background-color-disabled);
25919
- --x-accent-color: var(--accent-color-disabled);
25920
- }
25921
- /* Callout (info, warning, error) */
25922
- .navi_input_range[data-callout] {
25923
- /* What can we do? */
26212
+ &[data-accent-light] {
26213
+ --color-mix: var(--color-mix-light);
26214
+ }
26215
+ &[data-accent-very-light] {
26216
+ --background-color: rgba(0, 0, 0, 0.15);
26217
+ --track-border-color: rgba(0, 0, 0, 0.25);
26218
+ }
25924
26219
  }
25925
26220
  `;
25926
26221
  const InputRange = props => {
25927
- import.meta.css = [css$n, "@jsenv/navi/src/field/input_range.jsx"];
26222
+ const defaultRef = useRef();
26223
+ const ref = props.ref || defaultRef;
25928
26224
  const uiStateController = useUIStateController(props, "input");
25929
26225
  const uiState = useUIState(uiStateController);
25930
- const input = renderActionableComponent(props, {
25931
- Basic: InputRangeBasic,
25932
- WithAction: InputRangeWithAction
25933
- });
25934
26226
  return jsx(UIStateControllerContext.Provider, {
25935
26227
  value: uiStateController,
25936
26228
  children: jsx(UIStateContext.Provider, {
25937
26229
  value: uiState,
25938
- children: input
26230
+ children: jsx(InputRangeDispatcher, {
26231
+ ...props,
26232
+ ref: ref
26233
+ })
25939
26234
  })
25940
26235
  });
25941
26236
  };
25942
- const RangeStyleCSSVars = {
25943
- "outlineWidth": "--outline-width",
25944
- "borderRadius": "--border-radius",
25945
- "borderColor": "--border-color",
25946
- "backgroundColor": "--background-color",
25947
- "accentColor": "--accent-color",
25948
- ":hover": {
25949
- borderColor: "--border-color-hover",
25950
- backgroundColor: "--background-color-hover",
25951
- fillColor: "--fill-color-hover",
25952
- thumbColor: "--thumb-color-hover"
25953
- },
25954
- ":-navi-pressed": {
25955
- borderColor: "--border-color-hover",
25956
- backgroundColor: "--background-color-hover",
25957
- fillColor: "--fill-color-pressed",
25958
- thumbColor: "--thumb-color-pressed"
25959
- },
25960
- ":read-only": {
25961
- borderColor: "--border-color-readonly",
25962
- backgroundColor: "--background-color-readonly",
25963
- fillColor: "--fill-color-readonly",
25964
- thumbColor: "--thumb-color-readonly"
25965
- },
25966
- ":disabled": {
25967
- borderColor: "--border-color-disabled",
25968
- backgroundColor: "--background-color-disabled",
25969
- fillColor: "--fill-color-disabled",
25970
- thumbColor: "--thumb-color-disabled"
26237
+ const InputRangeDispatcher = props => {
26238
+ if (props.action) {
26239
+ return jsx(InputRangeWithAction, {
26240
+ ...props
26241
+ });
25971
26242
  }
26243
+ return jsx(InputRangeUI, {
26244
+ ...props
26245
+ });
25972
26246
  };
25973
- const RangePseudoClasses = [":hover", ":active", ":-navi-pressed", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
25974
- const RangePseudoElements = ["::-navi-loader"];
25975
- const RangeChildPropSet = new Set([...fieldPropSet]);
25976
- const InputRangeBasic = props => {
25977
- const contextReadOnly = useContext(ReadOnlyContext);
25978
- const contextDisabled = useContext(DisabledContext);
25979
- const contextLoading = useContext(LoadingContext);
25980
- const contextLoadingElement = useContext(LoadingElementContext);
25981
- const uiStateController = useContext(UIStateControllerContext);
25982
- const uiState = useContext(UIStateContext);
26247
+ const InputRangeUI = props => {
26248
+ import.meta.css = [css$n, "@jsenv/navi/src/field/input_range.jsx"];
25983
26249
  const {
26250
+ ref,
25984
26251
  onInput,
25985
26252
  readOnly,
25986
26253
  disabled,
@@ -25990,8 +26257,12 @@ const InputRangeBasic = props => {
25990
26257
  autoSelect,
25991
26258
  ...rest
25992
26259
  } = props;
25993
- const defaultRef = useRef();
25994
- const ref = rest.ref || defaultRef;
26260
+ const contextReadOnly = useContext(ReadOnlyContext);
26261
+ const contextDisabled = useContext(DisabledContext);
26262
+ const contextLoading = useContext(LoadingContext);
26263
+ const contextLoadingElement = useContext(LoadingElementContext);
26264
+ const uiStateController = useContext(UIStateControllerContext);
26265
+ const uiState = useContext(UIStateContext);
25995
26266
  const innerValue = uiState;
25996
26267
  const innerLoading = loading || contextLoading && contextLoadingElement === ref.current;
25997
26268
  const innerReadOnly = readOnly || contextReadOnly || innerLoading || uiStateController.readOnly;
@@ -26005,6 +26276,13 @@ const InputRangeBasic = props => {
26005
26276
  autoSelect
26006
26277
  });
26007
26278
  const remainingProps = useConstraints(ref, rest);
26279
+ const {
26280
+ accentColor
26281
+ } = remainingProps;
26282
+ const boxRef = useRef();
26283
+ useAccentColorAttributes(boxRef, accentColor, {
26284
+ elementSelector: ".navi_input_range_accent_probe"
26285
+ });
26008
26286
  const innerOnInput = useStableCallback(onInput);
26009
26287
  const focusProxyId = `input_range_focus_proxy_${useId()}`;
26010
26288
  const inertButFocusable = innerReadOnly && !innerDisabled;
@@ -26097,11 +26375,14 @@ const InputRangeBasic = props => {
26097
26375
  hasChildFunction: true,
26098
26376
  baseChildPropSet: RangeChildPropSet,
26099
26377
  ...remainingProps,
26100
- ref: undefined,
26378
+ ref: boxRef,
26101
26379
  autoFocus: undefined // See use_auto_focus.js
26102
26380
  ,
26103
26381
 
26104
- children: [jsx(LoaderBackground, {
26382
+ children: [jsx("span", {
26383
+ className: "navi_input_range_accent_probe",
26384
+ "aria-hidden": "true"
26385
+ }), jsx(LoaderBackground, {
26105
26386
  loading: innerLoading,
26106
26387
  color: "var(--loader-color)",
26107
26388
  inset: -1
@@ -26120,9 +26401,43 @@ const InputRangeBasic = props => {
26120
26401
  }), renderInputMemoized]
26121
26402
  });
26122
26403
  };
26404
+ const RangeStyleCSSVars = {
26405
+ "outlineWidth": "--outline-width",
26406
+ "borderRadius": "--border-radius",
26407
+ "borderColor": "--border-color",
26408
+ "backgroundColor": "--background-color",
26409
+ "accentColor": "--accent-color",
26410
+ ":hover": {
26411
+ borderColor: "--border-color-hover",
26412
+ backgroundColor: "--background-color-hover",
26413
+ fillColor: "--fill-color-hover",
26414
+ thumbColor: "--thumb-color-hover"
26415
+ },
26416
+ ":-navi-pressed": {
26417
+ borderColor: "--border-color-hover",
26418
+ backgroundColor: "--background-color-hover",
26419
+ fillColor: "--fill-color-pressed",
26420
+ thumbColor: "--thumb-color-pressed"
26421
+ },
26422
+ ":read-only": {
26423
+ borderColor: "--border-color-readonly",
26424
+ backgroundColor: "--background-color-readonly",
26425
+ fillColor: "--fill-color-readonly",
26426
+ thumbColor: "--thumb-color-readonly"
26427
+ },
26428
+ ":disabled": {
26429
+ borderColor: "--border-color-disabled",
26430
+ backgroundColor: "--background-color-disabled",
26431
+ fillColor: "--fill-color-disabled",
26432
+ thumbColor: "--thumb-color-disabled"
26433
+ }
26434
+ };
26435
+ const RangePseudoClasses = [":hover", ":active", ":-navi-pressed", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
26436
+ const RangePseudoElements = ["::-navi-loader"];
26437
+ const RangeChildPropSet = new Set([...fieldPropSet]);
26123
26438
  const InputRangeWithAction = props => {
26124
- const uiState = useContext(UIStateContext);
26125
26439
  const {
26440
+ ref,
26126
26441
  action,
26127
26442
  actionDebounce,
26128
26443
  actionAfterChange,
@@ -26137,8 +26452,7 @@ const InputRangeWithAction = props => {
26137
26452
  actionErrorEffect,
26138
26453
  ...rest
26139
26454
  } = props;
26140
- const defaultRef = useRef();
26141
- const ref = props.ref || defaultRef;
26455
+ const uiState = useContext(UIStateContext);
26142
26456
  const [boundAction] = useActionBoundToOneParam(action, uiState);
26143
26457
  const {
26144
26458
  loading: actionLoading
@@ -26178,12 +26492,13 @@ const InputRangeWithAction = props => {
26178
26492
  onError: onActionError,
26179
26493
  onEnd: onActionEnd
26180
26494
  });
26181
- return jsx(InputRangeBasic, {
26495
+ return jsx(InputRangeDispatcher, {
26182
26496
  "data-action": boundAction.name,
26183
26497
  "data-action-debounce": actionDebounce,
26184
26498
  "data-action-after-change": actionAfterChange ? "" : undefined,
26185
26499
  ...rest,
26186
26500
  ref: ref,
26501
+ action: undefined,
26187
26502
  loading: loading || actionLoading
26188
26503
  });
26189
26504
  };
@@ -26207,64 +26522,54 @@ const SearchSvg = () => jsx("svg", {
26207
26522
  })
26208
26523
  });
26209
26524
 
26210
- /**
26211
- * Navi uses three categories of custom events:
26212
- *
26213
- * 1. **Internal events** (`dispatchInternalCustomEvent`) — a component communicates
26214
- * with other navi components internally. Not meant to be observed from outside.
26215
- * They do not bubble so they stay contained within the subtree that handles them.
26216
- * Names often reflect their internal nature (e.g. `navi_check_pseudo_state`).
26217
- *
26218
- * 2. **Public events** (`dispatchPublicCustomEvent`) — a component exposes information
26219
- * about something that happened (e.g. `navi_list_select`). They bubble so any
26220
- * ancestor can observe them. These are part of the public API and should be documented.
26221
- *
26222
- * 3. **Request events** (`dispatchCustomEvent`) — code *outside* a component asks it
26223
- * to perform an action (e.g. `navi_list_request_open`). They are cancelable so the
26224
- * component can signal whether it handled the request. Names are prefixed
26225
- * with `request_` by convention.
26226
- */
26227
-
26525
+ installImportMetaCssBuild(import.meta);const css$m = /* css */`
26526
+ @layer navi {
26527
+ .navi_separator {
26528
+ --size: 1px;
26529
+ --color: #e4e4e7;
26530
+ --spacing: 0.5em;
26531
+ --spacing-start: 0.5em;
26532
+ --spacing-end: 0.5em;
26533
+ }
26534
+ }
26228
26535
 
26229
- /**
26230
- * Dispatches a public event from `el`, announcing something that happened.
26231
- * Bubbles so any ancestor can observe it.
26232
- */
26233
- const dispatchPublicCustomEvent = (
26234
- el,
26235
- customEventName,
26236
- customEventDetail,
26237
- ) => {
26238
- const customEvent = new CustomEvent(customEventName, {
26239
- detail: resolveEventDetail(customEventDetail),
26240
- bubbles: true,
26241
- });
26242
- return el.dispatchEvent(customEvent);
26243
- };
26536
+ .navi_separator {
26537
+ width: 100%;
26538
+ height: var(--size);
26539
+ margin-top: var(--spacing-start, var(--spacing));
26540
+ margin-bottom: var(--spacing-end, var(--spacing));
26541
+ flex-shrink: 0;
26542
+ background: var(--color);
26543
+ border: none;
26244
26544
 
26245
- /**
26246
- * Dispatches a request event *at* `el`, asking it to perform an action.
26247
- * Cancelable — returns `false` if the component called `preventDefault()`,
26248
- * indicating it did not (or could not) handle the request.
26249
- * Names are conventionally prefixed with `request_` (e.g. `navi_list_request_open`).
26250
- */
26251
- const dispatchCustomEvent = (el, customEventName, customEventDetail) => {
26252
- const customEvent = new CustomEvent(customEventName, {
26253
- detail: resolveEventDetail(customEventDetail),
26254
- cancelable: true,
26255
- });
26256
- return el.dispatchEvent(customEvent);
26257
- };
26545
+ &[data-vertical] {
26546
+ display: inline-block;
26258
26547
 
26259
- const resolveEventDetail = (customEventDetail) => {
26260
- const { event, ...rest } = customEventDetail ?? {};
26261
- let resolvedEvent;
26262
- if (event?.detail?.event !== undefined) {
26263
- resolvedEvent = event.detail.event;
26264
- } else if (event !== undefined) {
26265
- resolvedEvent = event;
26548
+ width: var(--size);
26549
+ height: 1lh;
26550
+ margin-top: 0;
26551
+ margin-right: var(--spacing-end, var(--spacing));
26552
+ margin-bottom: 0;
26553
+ margin-left: var(--spacing-start, var(--spacing));
26554
+ vertical-align: bottom;
26555
+ }
26266
26556
  }
26267
- return { ...rest, event: resolvedEvent };
26557
+ `;
26558
+ const SeparatorStyleCSSVars = {
26559
+ color: "--color"
26560
+ };
26561
+ const Separator = ({
26562
+ vertical,
26563
+ ...props
26564
+ }) => {
26565
+ import.meta.css = [css$m, "@jsenv/navi/src/layout/separator.jsx"];
26566
+ return jsx(Box, {
26567
+ as: vertical ? "span" : "hr",
26568
+ ...props,
26569
+ "data-vertical": vertical ? "" : undefined,
26570
+ baseClassName: "navi_separator",
26571
+ styleCSSVars: SeparatorStyleCSSVars
26572
+ });
26268
26573
  };
26269
26574
 
26270
26575
  /*
@@ -26669,9 +26974,10 @@ const RenderWindowContext = createContext(null);
26669
26974
  // Carries the separator element/function down to each ListItem so separators
26670
26975
  // are only rendered between items that actually mount (post-filter, post-window).
26671
26976
  const SeparatorContext = createContext(null);
26672
- const css$m = /* css */`
26977
+ const css$l = /* css */`
26673
26978
  @layer navi {
26674
26979
  .navi_list_container {
26980
+ --list-outline-width: 1px;
26675
26981
  --list-border-radius: 4px;
26676
26982
  --list-border-width: 1px;
26677
26983
  --list-border-color: light-dark(#ccc, #555);
@@ -26688,23 +26994,21 @@ const css$m = /* css */`
26688
26994
  --list-item-color-hover: var(--list-item-color);
26689
26995
  --list-item-background-color-hover: light-dark(#f5f5f5, #2a2a2a);
26690
26996
 
26691
- /* Pointed (keyboard navigation position) */
26692
- --list-item-color-pointed: var(--list-item-color);
26693
- --list-item-background-color-pointed: light-dark(#c2d7fc, #1a4a9e);
26694
-
26695
- /* Selected */
26696
- --list-item-color-selected: light-dark(#1a73e8, #7baaf7);
26697
- --list-item-background-color-selected: light-dark(#e8f0fe, #1c3a6e);
26698
- --list-item-font-weight-selected: 500;
26997
+ /* Pointed by mouse subtle, just a shade above background */
26998
+ --list-item-color-mouse-pointed: var(--list-item-color);
26999
+ --list-item-background-color-mouse-pointed: light-dark(#ebebeb, #303030);
26699
27000
 
26700
- /* Pointed+selected: darken the selected background slightly */
26701
- --list-item-color-pointed-selected: var(--list-item-color-selected);
26702
- --list-item-background-color-pointed-selected: color-mix(
26703
- in srgb,
26704
- var(--list-item-background-color-selected) 80%,
26705
- black
27001
+ /* Pointed by keyboard subtle light blue highlight */
27002
+ --list-item-color-keyboard-pointed: var(--list-item-color);
27003
+ --list-item-background-color-keyboard-pointed: light-dark(
27004
+ #c2dcff,
27005
+ #1c3a6e
26706
27006
  );
26707
27007
 
27008
+ /* Selected — vivid blue accent */
27009
+ --list-item-color-selected: light-dark(#ffffff, #ffffff);
27010
+ --list-item-background-color-selected: light-dark(#1a73e8, #2b5fcc);
27011
+
26708
27012
  /* Disabled */
26709
27013
  --list-item-color-disabled: light-dark(#aaa, #555);
26710
27014
  --list-item-background-color-disabled: var(--list-item-background-color);
@@ -26728,11 +27032,11 @@ const css$m = /* css */`
26728
27032
  }
26729
27033
 
26730
27034
  .navi_list_container {
26731
- --x-border-radius: var(--list-border-radius);
26732
- --x-border-width: var(--list-border-width);
26733
- --x-border-color: var(--list-border-color);
26734
- --x-border-style: var(--list-border-style);
26735
- --x-background-color: var(--list-background-color);
27035
+ --x-list-border-radius: var(--list-border-radius);
27036
+ --x-list-border-width: var(--list-border-width);
27037
+ --x-list-border-color: var(--list-border-color);
27038
+ --x-list-border-style: var(--list-border-style);
27039
+ --x-list-background-color: var(--list-background-color);
26736
27040
  /* When typing inside an input browser tries to keep caret visible */
26737
27041
  /* For input within a sticky element inside a scrollable container */
26738
27042
  /* Browser will try to scroll that input into view */
@@ -26747,15 +27051,33 @@ const css$m = /* css */`
26747
27051
  var(--list-footer-height, 0px) + var(--list-scroll-padding-bottom, 0px)
26748
27052
  );
26749
27053
 
27054
+ display: flex;
26750
27055
  width: fit-content;
26751
27056
  max-width: 100%;
26752
- max-height: var(--list-max-height);
26753
- background-color: var(--x-background-color);
26754
- border: var(--x-border-width) var(--x-border-style) var(--x-border-color);
26755
- border-radius: var(--x-border-radius);
27057
+ flex-direction: column;
27058
+ background-color: var(--x-list-background-color);
27059
+ /* Use a transparent real border to reserve layout space, and draw the
27060
+ visible border via outline (inset via negative offset). This way the
27061
+ focus ring can simply widen the outline without shifting layout. */
27062
+ border: var(--x-list-border-width) solid transparent;
27063
+ border-radius: var(--x-list-border-radius);
27064
+ outline: var(--x-list-border-width) var(--x-list-border-style)
27065
+ var(--x-list-border-color);
27066
+ outline-offset: calc(-1 * var(--x-list-border-width));
26756
27067
  transition: opacity 0.2s ease;
26757
- overflow: auto;
26758
- overscroll-behavior: inherit; /* inherit select behavior */
27068
+ /* overflow:hidden is required on the container (not the inner scroll element)
27069
+ so that border-radius clips the content correctly. Without it, items near
27070
+ the corners would visually overflow the rounded corners during scroll. */
27071
+ overflow: hidden;
27072
+
27073
+ .navi_list_scroll_container {
27074
+ width: inherit;
27075
+ min-width: inherit;
27076
+ max-width: inherit;
27077
+ max-height: var(--list-max-height);
27078
+ overflow: auto;
27079
+ overscroll-behavior: inherit; /* inherit select behavior */
27080
+ }
26759
27081
 
26760
27082
  &[data-expand-x] {
26761
27083
  width: 100%;
@@ -26763,18 +27085,42 @@ const css$m = /* css */`
26763
27085
  &[popover] {
26764
27086
  position: absolute;
26765
27087
  inset: unset;
27088
+ display: none;
26766
27089
  min-width: var(--list-anchor-width, 0px);
26767
27090
  max-width: 95vw;
26768
27091
  margin: 0;
26769
27092
  padding: 0;
27093
+
27094
+ &:popover-open {
27095
+ display: flex;
27096
+ }
27097
+ .navi_list {
27098
+ width: 100%;
27099
+ }
27100
+
27101
+ &[data-anchor-hidden] {
27102
+ opacity: 0;
27103
+ pointer-events: none;
27104
+ }
26770
27105
  }
26771
- &[data-anchor-hidden] {
26772
- opacity: 0;
26773
- pointer-events: none;
27106
+
27107
+ &[data-focus] {
27108
+ /* outline: var(--list-outline-width) solid var(--navi-focus-outline-color);
27109
+ outline-offset: calc(-1 * var(--list-outline-width)); */
26774
27110
  }
27111
+ &[data-focus-visible] {
27112
+ outline-width: calc(var(--list-border-width) + var(--list-outline-width));
27113
+ outline-color: var(--navi-focus-outline-color);
27114
+ outline-offset: calc(
27115
+ -1 * (var(--list-border-width) + var(--list-outline-width))
27116
+ );
27117
+ .navi_list {
27118
+ outline: none;
27119
+ }
27120
+ }
27121
+
26775
27122
  &[data-callout] {
26776
- --x-border-color: var(--callout-color);
26777
- --x-outline-color: var(--callout-color);
27123
+ --x-list-border-color: var(--callout-color);
26778
27124
  }
26779
27125
  }
26780
27126
 
@@ -26797,15 +27143,15 @@ const css$m = /* css */`
26797
27143
  }
26798
27144
 
26799
27145
  .navi_list_item {
26800
- --x-color: var(--list-item-color);
26801
- --x-background-color: var(--list-item-background-color);
26802
- --x-font-weight: var(--list-item-font-weight);
27146
+ --x-list-item-color: var(--list-item-color);
27147
+ --x-list-item-background-color: var(--list-item-background-color);
27148
+ --x-list-item-font-weight: var(--list-item-font-weight);
26803
27149
  box-sizing: border-box;
26804
27150
  min-width: 100%;
26805
27151
  padding: var(--list-item-padding);
26806
- color: var(--x-color);
26807
- font-weight: var(--x-font-weight);
26808
- background-color: var(--x-background-color);
27152
+ color: var(--x-list-item-color);
27153
+ font-weight: var(--x-list-item-font-weight);
27154
+ background-color: var(--x-list-item-background-color);
26809
27155
  /*
26810
27156
  CSS impossible d'obtenir un layout qui ferait en gros:
26811
27157
  width = max(min(max-content, 100%), unbreakable-content)
@@ -26829,27 +27175,31 @@ const css$m = /* css */`
26829
27175
  &[data-interactive] {
26830
27176
  cursor: pointer;
26831
27177
  user-select: none;
26832
- /* &:hover {
26833
- --x-color: var(--list-item-color-hover);
26834
- --x-background-color: var(--list-item-background-color-hover);
26835
- } */
26836
27178
  }
26837
27179
  &[data-pointed] {
26838
- --x-color: var(--list-item-color-pointed);
26839
- --x-background-color: var(--list-item-background-color-pointed);
27180
+ --x-list-item-color: var(--list-item-color-mouse-pointed);
27181
+ --x-list-item-background-color: var(
27182
+ --list-item-background-color-mouse-pointed
27183
+ );
26840
27184
  }
26841
27185
  &[data-selected] {
26842
- --x-color: var(--list-item-color-selected);
26843
- --x-background-color: var(--list-item-background-color-selected);
26844
- --x-font-weight: var(--list-item-font-weight-selected);
26845
- }
26846
- &[data-pointed][data-selected] {
26847
- --x-color: var(--list-item-color-pointed-selected);
26848
- --x-background-color: var(--list-item-background-color-pointed-selected);
27186
+ --x-list-item-color: var(--list-item-color-selected);
27187
+ --x-list-item-background-color: var(
27188
+ --list-item-background-color-selected
27189
+ );
27190
+ &[data-pointed] {
27191
+ /* Here important should no beed need, but for some reason it is */
27192
+ --x-list-item-background-color: var(
27193
+ --list-item-background-color-selected,
27194
+ var(--list-item-background-color-mouse-pointed)
27195
+ ) !important;
27196
+ }
26849
27197
  }
26850
27198
  &[data-disabled] {
26851
- --x-color: var(--list-item-color-disabled);
26852
- --x-background-color: var(--list-item-background-color-disabled);
27199
+ --x-list-item-color: var(--list-item-color-disabled);
27200
+ --x-list-item-background-color: var(
27201
+ --list-item-background-color-disabled
27202
+ );
26853
27203
  cursor: not-allowed;
26854
27204
  pointer-events: none;
26855
27205
  }
@@ -26858,10 +27208,41 @@ const css$m = /* css */`
26858
27208
  }
26859
27209
  }
26860
27210
 
27211
+ .navi_list_container {
27212
+ &[data-focus-within] {
27213
+ .navi_list_item {
27214
+ &[data-pointed-by-keyboard] {
27215
+ --x-list-item-color: var(--list-item-color-keyboard-pointed);
27216
+ --x-list-item-background-color: var(
27217
+ --list-item-background-color-keyboard-pointed
27218
+ );
27219
+ }
27220
+
27221
+ /* Selected must win over pointed-by-keyboard */
27222
+ &[data-selected] {
27223
+ --x-list-item-color: var(--list-item-color-selected);
27224
+ --x-list-item-background-color: var(
27225
+ --list-item-background-color-selected
27226
+ );
27227
+ /* Selected + pointed by keyboard: use keyboard color as fallback
27228
+ so that if --list-item-background-color-selected is reset the
27229
+ keyboard-pointed highlight still shows. */
27230
+ &[data-pointed-by-keyboard] {
27231
+ --x-list-item-background-color: var(
27232
+ --list-item-background-color-selected,
27233
+ var(--list-item-background-color-keyboard-pointed)
27234
+ );
27235
+ }
27236
+ }
27237
+ }
27238
+ }
27239
+ }
27240
+
26861
27241
  /* Virtual scroll fillers — must remain invisible.
26862
27242
  The browser may briefly flash them during scroll before the render window
26863
27243
  updates, so giving them a visible background would cause visual glitches. */
26864
27244
  .navi_list_virtual_filler {
27245
+ display: inline-block;
26865
27246
  height: 0px;
26866
27247
  list-style: none;
26867
27248
  /* background: pink; */
@@ -27012,16 +27393,11 @@ const List = props => {
27012
27393
  const ListDispatcher = props => {
27013
27394
  const alreadyInteractive = useContext(ListInteractiveContext);
27014
27395
  const parentUIStateController = useContext(ParentUIStateControllerContext);
27015
- if (!alreadyInteractive && props.action) {
27396
+ if (!alreadyInteractive && (props.action || props.uiAction || parentUIStateController)) {
27016
27397
  return jsx(ListWithAction, {
27017
27398
  ...props
27018
27399
  });
27019
27400
  }
27020
- if (!alreadyInteractive && (props.uiAction || parentUIStateController)) {
27021
- return jsx(ListInteractive, {
27022
- ...props
27023
- });
27024
- }
27025
27401
  if (props.popover === true) {
27026
27402
  return jsx(ListWithPopover, {
27027
27403
  ...props
@@ -27037,18 +27413,19 @@ const ListDispatcher = props => {
27037
27413
  });
27038
27414
  };
27039
27415
  const ListUI = props => {
27040
- import.meta.css = [css$m, "@jsenv/navi/src/field/list/list.jsx"];
27416
+ import.meta.css = [css$l, "@jsenv/navi/src/field/list/list.jsx"];
27041
27417
  const {
27042
27418
  ref,
27043
27419
  renderBudget = RENDER_BUDGET_DEFAULT,
27044
- listId,
27045
- listRole,
27420
+ id,
27421
+ role,
27046
27422
  fallback,
27047
27423
  noMatchFallback,
27048
27424
  separator,
27049
27425
  children,
27050
27426
  popover,
27051
27427
  expandX,
27428
+ expand,
27052
27429
  maxHeight,
27053
27430
  onListVisibleItemsChange,
27054
27431
  virtualItemHeight,
@@ -27059,13 +27436,17 @@ const ListUI = props => {
27059
27436
  required,
27060
27437
  ...rest
27061
27438
  } = props;
27439
+ if (renderBudget < 30) {
27440
+ console.warn(`List: renderBudget=${renderBudget} is too low. A renderBudget below 30 is not supported: on large screens or when the list grows, items outside the window would appear as blank space instead of rendered content. Use a value of at least 30, or omit the prop to use the default (${RENDER_BUDGET_DEFAULT}).`);
27441
+ }
27062
27442
  const hiddenInputId = useId();
27063
27443
 
27064
27444
  // lockSize: capture the container's dimensions on first render so filtering
27065
27445
  // cannot collapse the layout. Measurement happens on the initial (unfiltered)
27066
27446
  // state because the parent controls hidden props before any search is applied.
27447
+ const containerRef = useRef(null);
27067
27448
  const sizeLocked = useRef(false);
27068
- useDisplayedLayoutEffect(ref, listContainerEl => {
27449
+ useDisplayedLayoutEffect(containerRef, listContainerEl => {
27069
27450
  if (!lockSize) {
27070
27451
  return undefined;
27071
27452
  }
@@ -27096,63 +27477,68 @@ const ListUI = props => {
27096
27477
  onListVisibleItemsChange?.(tracker.visibleItemsSignal.peek());
27097
27478
  }
27098
27479
  });
27099
- const ulRef = useRef(null);
27100
27480
  const {
27101
27481
  virtualItemHeightSignal,
27102
27482
  renderWindow,
27103
27483
  scrollToItem,
27104
27484
  pendingScrollRef
27105
27485
  } = useListScrollSync({
27486
+ containerRef,
27106
27487
  ref,
27107
- ulRef,
27108
27488
  tracker,
27109
27489
  renderBudget,
27110
27490
  virtualItemHeight,
27111
27491
  searchText
27112
27492
  });
27493
+ const idDefault = useId();
27494
+ const innerId = id || idDefault;
27113
27495
  const renderList = listProps => {
27114
- const listIdDefault = useId();
27115
- const innerListid = listId || listIdDefault;
27116
- return jsx(UnorderedList, {
27117
- ref: ulRef,
27118
- id: innerListid,
27119
- role: listRole,
27120
- fallback: fallback,
27121
- noMatchFallback: noMatchFallback,
27122
- searchText: searchText,
27123
- separator: separator,
27124
- expandX: expandX,
27125
- ...listProps,
27126
- tracker: tracker,
27127
- renderWindow: renderWindow,
27128
- virtualItemHeightSignal: virtualItemHeightSignal,
27129
- children: jsx(PendingScrollRefContext.Provider, {
27130
- value: pendingScrollRef,
27131
- children: jsx(ListIdContext.Provider, {
27132
- value: innerListid,
27133
- children: children
27496
+ return jsx("div", {
27497
+ className: "navi_list_scroll_container",
27498
+ children: jsx(UnorderedList, {
27499
+ ref: ref,
27500
+ id: innerId,
27501
+ role: role,
27502
+ fallback: fallback,
27503
+ noMatchFallback: noMatchFallback,
27504
+ searchText: searchText,
27505
+ separator: separator === true ? jsx(Separator, {
27506
+ margin: "0"
27507
+ }) : separator,
27508
+ expandX: expandX || expand,
27509
+ ...listProps,
27510
+ tracker: tracker,
27511
+ renderWindow: renderWindow,
27512
+ virtualItemHeightSignal: virtualItemHeightSignal,
27513
+ children: jsx(PendingScrollRefContext.Provider, {
27514
+ value: pendingScrollRef,
27515
+ children: jsx(ListIdContext.Provider, {
27516
+ value: innerId,
27517
+ children: children
27518
+ })
27134
27519
  })
27135
27520
  })
27136
27521
  });
27137
27522
  };
27138
- const renderListMemoized = useCallback(renderList, [listId, listRole, fallback, noMatchFallback, searchText, separator, expandX, renderWindow, children]);
27523
+ const renderListMemoized = useCallback(renderList, [innerId, role, fallback, noMatchFallback, searchText, separator, expandX, expand, renderWindow, children]);
27139
27524
  const inputRef = useRef(null);
27140
27525
  const remainingProps = useConstraints(inputRef, rest, {
27141
27526
  disabled: !name
27142
27527
  });
27143
27528
  return jsxs(Box, {
27144
27529
  ...remainingProps,
27145
- ref: ref,
27530
+ ref: containerRef,
27146
27531
  baseClassName: "navi_list_container",
27147
27532
  popover: popover,
27148
- "data-expand-x": expandX ? "" : undefined,
27533
+ "data-expand-x": expandX || expand ? "" : undefined,
27149
27534
  expandX: expandX,
27535
+ expand: expand,
27150
27536
  maxHeight: maxHeight,
27151
27537
  styleCSSVars: LIST_STYLE_CSS_VARS,
27152
27538
  pseudoClasses: LIST_PSEUDO_CLASSES,
27539
+ pseudoStateSelector: ".navi_list",
27153
27540
  hasChildFunction: true,
27154
27541
  "data-navi-value": value || undefined,
27155
- "data-input-proxy": name ? `#${CSS.escape(hiddenInputId)}` : undefined,
27156
27542
  onnavi_list_request_nav: e => {
27157
27543
  const {
27158
27544
  item
@@ -27160,8 +27546,7 @@ const ListUI = props => {
27160
27546
  if (!item) {
27161
27547
  return;
27162
27548
  }
27163
- // navi_list_nav is dispatched by scrollToItem after the scroll
27164
- // completes (including the async path via pendingScrollRef).
27549
+ // navi_list_nav is dispatched immediately by scrollToItem (before scroll).
27165
27550
  scrollToItem(item, {
27166
27551
  reason: "navi_list_request_nav",
27167
27552
  event: e.detail.event
@@ -27169,12 +27554,19 @@ const ListUI = props => {
27169
27554
  },
27170
27555
  onnavi_list_request_select: e => {
27171
27556
  const {
27172
- item
27557
+ item,
27558
+ event
27173
27559
  } = e.detail;
27174
27560
  if (!item) {
27175
27561
  return;
27176
27562
  }
27177
- dispatchPublicCustomEvent(e.currentTarget, "navi_list_select", {
27563
+ // Nav to the selected item first (updates uiState, scrolls, etc.)
27564
+ scrollToItem(item, {
27565
+ reason: "navi_list_request_select",
27566
+ event: event || e
27567
+ });
27568
+ const listEl = e.currentTarget;
27569
+ dispatchPublicCustomEvent(listEl, "navi_list_select", {
27178
27570
  item,
27179
27571
  event: e
27180
27572
  });
@@ -27190,16 +27582,23 @@ const ListUI = props => {
27190
27582
  }), renderListMemoized]
27191
27583
  });
27192
27584
  };
27585
+ const LIST_STYLE_CSS_VARS = {
27586
+ maxHeight: "--list-max-height",
27587
+ borderColor: "--list-border-color",
27588
+ borderRadius: "--list-border-radius",
27589
+ borderWidth: "--list-border-width"
27590
+ };
27591
+ const LIST_PSEUDO_CLASSES = [":hover", ":focus", ":focus-visible", ":focus-within", ":read-only", ":disabled", ":-navi-void", ":-navi-has-value", ":-navi-expanded"];
27193
27592
  const useListScrollSync = ({
27593
+ containerRef,
27194
27594
  ref,
27195
- ulRef,
27196
27595
  tracker,
27197
27596
  renderBudget,
27198
27597
  virtualItemHeight,
27199
27598
  searchText
27200
27599
  }) => {
27201
27600
  const debugScroll = useDebugScroll();
27202
- const virtualItemHeightSignal = useVirtualItemHeightSignal(ulRef, virtualItemHeight);
27601
+ const virtualItemHeightSignal = useVirtualItemHeightSignal(ref, virtualItemHeight);
27203
27602
  const [renderWindow, setRenderWindow] = useState({
27204
27603
  start: 0,
27205
27604
  end: renderBudget
@@ -27242,21 +27641,28 @@ const useListScrollSync = ({
27242
27641
  if (index >= itemCount) {
27243
27642
  index = itemCount - 1;
27244
27643
  }
27245
- const srollItemIntoView = itemEl => {
27644
+ const scrollItemIntoView = itemEl => {
27246
27645
  const trigger = `"${event.type}" on ${getElementSignature(event.target)} (${reason})`;
27247
27646
  const block = event.type === "keydown" ? "nearest" : "center";
27248
27647
  const scrollToItemCall = `${getElementSignature(itemEl)}.scrollIntoView({ block: "${block}", container: "nearest" })`;
27648
+ const listScrollContainerEl = containerRef.current.querySelector(`.navi_list_scroll_container`);
27249
27649
  debugScroll(`${trigger} -> ${scrollToItemCall}`);
27250
27650
  scrollIntoViewScoped(itemEl, {
27251
- container: ref.current,
27651
+ container: listScrollContainerEl,
27252
27652
  block
27253
27653
  });
27254
- const listContainerEl = ref.current;
27255
- dispatchPublicCustomEvent(listContainerEl, "navi_list_nav", {
27654
+ dispatchPublicCustomEvent(listEl, "navi_list_scroll", {
27256
27655
  event,
27257
27656
  item
27258
27657
  });
27259
27658
  };
27659
+
27660
+ // Dispatch navi_list_nav immediately — do not wait for scroll to complete.
27661
+ const listEl = ref.current;
27662
+ dispatchPublicCustomEvent(listEl, "navi_list_nav", {
27663
+ event,
27664
+ item
27665
+ });
27260
27666
  const {
27261
27667
  start,
27262
27668
  end
@@ -27265,30 +27671,30 @@ const useListScrollSync = ({
27265
27671
  if (isInWindow) {
27266
27672
  const itemEl = document.getElementById(item.id);
27267
27673
  if (itemEl) {
27268
- srollItemIntoView(itemEl);
27674
+ scrollItemIntoView(itemEl);
27269
27675
  return;
27270
27676
  }
27271
27677
  }
27272
27678
  // Not in DOM — shift the render window. The item will read
27273
- // pendingScrollRef on mount and call scrollIntoViewWithStickyAwareness,
27274
- // then call onScrolled so we can dispatch navi_list_scroll.
27679
+ // pendingScrollRef on mount and scroll into view.
27275
27680
  pendingScrollRef.current = {
27276
27681
  id: item.id,
27277
27682
  resolve: itemEl => {
27278
27683
  pendingScrollRef.current = null;
27279
- srollItemIntoView(itemEl);
27684
+ scrollItemIntoView(itemEl);
27280
27685
  }
27281
27686
  };
27282
27687
  const half = Math.floor(renderBudget / 2);
27283
27688
  const newStart = Math.max(0, index - half);
27284
27689
  const newEnd = newStart + renderBudget;
27285
- updateRenderWindow(newStart, newEnd, `item to scroll out of render window`);
27690
+ updateRenderWindow(newStart, newEnd, `item to scroll (at ${index}) is out of render window`);
27286
27691
  };
27287
27692
  const currentScrollRef = useRef(null);
27288
27693
  const updateCurrentScroll = () => {
27289
- const listContainerEl = ref.current;
27290
- const currentScrollLeft = listContainerEl.scrollLeft;
27291
- const currentScrollTop = listContainerEl.scrollTop;
27694
+ const listContainerEl = containerRef.current;
27695
+ const listScrollContainerEl = listContainerEl.querySelector(`.navi_list_scroll_container`);
27696
+ const currentScrollLeft = listScrollContainerEl.scrollLeft;
27697
+ const currentScrollTop = listScrollContainerEl.scrollTop;
27292
27698
  const renderWindow = renderWindowRef.current;
27293
27699
  currentScrollRef.current = {
27294
27700
  left: currentScrollLeft,
@@ -27313,7 +27719,7 @@ const useListScrollSync = ({
27313
27719
  // Scroll to the selected item when the list is first presented on screen.
27314
27720
  // Skipped when inside a closed <dialog>/<details> (scrollIntoView is a no-op
27315
27721
  // on hidden elements); re-runs automatically every time the ancestor opens.
27316
- useDisplayedLayoutEffect(ref, (el, openEvent) => {
27722
+ useDisplayedLayoutEffect(containerRef, (el, openEvent) => {
27317
27723
  updateCurrentScroll();
27318
27724
  const items = tracker.itemsSignal.peek();
27319
27725
  const firstSelected = items.find(i => i.selected);
@@ -27346,7 +27752,8 @@ const useListScrollSync = ({
27346
27752
  const savedScrollRef = useRef(null);
27347
27753
  const topMatchScoresKeyRef = useRef("");
27348
27754
  useLayoutEffect(() => {
27349
- const listContainerEl = ref.current;
27755
+ const listContainerEl = containerRef.current;
27756
+ const listScrollContainerEl = listContainerEl.querySelector(`.navi_list_scroll_container`);
27350
27757
  if (!searchText) {
27351
27758
  // no search -> try to restore scroll position
27352
27759
  topMatchScoresKeyRef.current = "";
@@ -27362,8 +27769,8 @@ const useListScrollSync = ({
27362
27769
  const left = savedScroll.left;
27363
27770
  const top = savedScroll.top;
27364
27771
  // use scrollTo to respect eventual css scroll-behavior: smooth;
27365
- debugScroll(`restore scroll: ${getElementSignature(listContainerEl)}.scrollTo({ left: ${left}, top: ${top} })`);
27366
- listContainerEl.scrollTo({
27772
+ debugScroll(`restore scroll: ${getElementSignature(listScrollContainerEl)}.scrollTo({ left: ${left}, top: ${top} })`);
27773
+ listScrollContainerEl.scrollTo({
27367
27774
  left: savedScroll.left,
27368
27775
  top: savedScroll.top
27369
27776
  });
@@ -27376,15 +27783,15 @@ const useListScrollSync = ({
27376
27783
  item
27377
27784
  } = getScrollInfo({
27378
27785
  scrollTop: savedScroll.top
27379
- }, listContainerEl, tracker, virtualItemHeightSignal, renderWindowRef);
27380
- dispatchPublicCustomEvent(listContainerEl, "navi_list_nav", {
27786
+ }, listScrollContainerEl, tracker, virtualItemHeightSignal, renderWindowRef);
27787
+ const listEl = ref.current;
27788
+ dispatchPublicCustomEvent(listEl, "navi_list_nav", {
27381
27789
  item,
27382
27790
  event: new CustomEvent("navi_scroll_restore")
27383
27791
  });
27384
27792
  });
27793
+ return;
27385
27794
  }
27386
-
27387
- // During search -> watch for changes in the top items or their scores.
27388
27795
  const visibleItems = tracker.visibleItemsSignal.peek();
27389
27796
  const topItems = visibleItems.slice(0, renderBudget);
27390
27797
  const topMatchScoresKey = topItems.map(i => `${i.id}:${i.matchScore ?? ""}`).join(",");
@@ -27410,12 +27817,12 @@ const useListScrollSync = ({
27410
27817
 
27411
27818
  // Scroll listener — slides the window as the user scrolls.
27412
27819
  useLayoutEffect(() => {
27413
- const listContainerEl = ref.current;
27820
+ const listContainerEl = containerRef.current;
27414
27821
  if (!listContainerEl) {
27415
27822
  return undefined;
27416
27823
  }
27824
+ const listScrollContainerEl = listContainerEl.querySelector(`.navi_list_scroll_container`);
27417
27825
  const listEl = listContainerEl.querySelector(".navi_list");
27418
- const scrollContainer = getScrollContainer(listEl);
27419
27826
  const onScroll = () => {
27420
27827
  updateCurrentScroll();
27421
27828
  const visibleItemCount = tracker.visibleCountSignal.peek();
@@ -27428,8 +27835,8 @@ const useListScrollSync = ({
27428
27835
  }
27429
27836
  let reason = "";
27430
27837
  const scrollInfo = getScrollInfo({
27431
- scrollTop: listContainerEl.scrollTop
27432
- }, listContainerEl, tracker, virtualItemHeightSignal, renderWindowRef);
27838
+ scrollTop: listScrollContainerEl.scrollTop
27839
+ }, listScrollContainerEl, tracker, virtualItemHeightSignal, renderWindowRef);
27433
27840
  if (!scrollInfo) {
27434
27841
  return;
27435
27842
  }
@@ -27446,11 +27853,11 @@ const useListScrollSync = ({
27446
27853
  }
27447
27854
  updateRenderWindow(newStart, newEnd, reason);
27448
27855
  };
27449
- scrollContainer.addEventListener("scroll", onScroll, {
27856
+ listScrollContainerEl.addEventListener("scroll", onScroll, {
27450
27857
  passive: true
27451
27858
  });
27452
27859
  return () => {
27453
- scrollContainer.removeEventListener("scroll", onScroll);
27860
+ listScrollContainerEl.removeEventListener("scroll", onScroll);
27454
27861
  };
27455
27862
  }, [renderBudget]);
27456
27863
  return {
@@ -27466,10 +27873,10 @@ const useListScrollSync = ({
27466
27873
  // Returns { index, item, reason } or null if nothing can be determined.
27467
27874
  const getScrollInfo = ({
27468
27875
  scrollTop
27469
- }, listContainerEl, tracker, virtualItemHeightSignal, renderWindowRef) => {
27470
- const listEl = listContainerEl.querySelector(".navi_list");
27876
+ }, listScrollContainerEl, tracker, virtualItemHeightSignal, renderWindowRef) => {
27877
+ const listEl = listScrollContainerEl.querySelector(".navi_list");
27471
27878
  const items = tracker.itemsSignal.peek();
27472
- const containerRect = listContainerEl.getBoundingClientRect();
27879
+ const containerRect = listScrollContainerEl.getBoundingClientRect();
27473
27880
  let hitEl = null;
27474
27881
  let hitFiller = null;
27475
27882
  for (let y = containerRect.top + 1; y < containerRect.bottom; y += 4) {
@@ -27520,7 +27927,7 @@ const getScrollInfo = ({
27520
27927
  reason: "no hit"
27521
27928
  };
27522
27929
  };
27523
- const useVirtualItemHeightSignal = (ulRef, virtualItemHeightProp = 0) => {
27930
+ const useVirtualItemHeightSignal = (ref, virtualItemHeightProp = 0) => {
27524
27931
  const virtualHeightSignalRef = useRef(null);
27525
27932
  if (!virtualHeightSignalRef.current) {
27526
27933
  virtualHeightSignalRef.current = signal(virtualItemHeightProp);
@@ -27534,11 +27941,11 @@ const useVirtualItemHeightSignal = (ulRef, virtualItemHeightProp = 0) => {
27534
27941
  if (virtualHeightSignal.peek() !== 0) {
27535
27942
  return;
27536
27943
  }
27537
- const ulEl = ulRef.current;
27538
- if (!ulEl) {
27944
+ const listEl = ref.current;
27945
+ if (!listEl) {
27539
27946
  return;
27540
27947
  }
27541
- const firstListItem = ulEl.querySelector(REAL_LIST_ITEM_SELECTOR);
27948
+ const firstListItem = listEl.querySelector(REAL_LIST_ITEM_SELECTOR);
27542
27949
  if (!firstListItem) {
27543
27950
  return;
27544
27951
  }
@@ -27547,13 +27954,7 @@ const useVirtualItemHeightSignal = (ulRef, virtualItemHeightProp = 0) => {
27547
27954
  });
27548
27955
  return virtualHeightSignal;
27549
27956
  };
27550
- const LIST_STYLE_CSS_VARS = {
27551
- maxHeight: "--list-max-height",
27552
- borderColor: "--list-border-color",
27553
- borderRadius: "--list-border-radius",
27554
- borderWidth: "--list-border-width"
27555
- };
27556
- const LIST_PSEUDO_CLASSES = [":-navi-void"];
27957
+
27557
27958
  // Inner <ul> — hosts the fillers + items.
27558
27959
  // Creates a virtualItemHeight signal so TopFiller and BottomFiller can
27559
27960
  // subscribe to it independently. When virtualItemHeight is passed as a prop it
@@ -27696,7 +28097,8 @@ const ListWithPopover = props => {
27696
28097
  ...props,
27697
28098
  popover: "manual",
27698
28099
  onnavi_list_request_open: e => {
27699
- const listContainerEl = e.currentTarget;
28100
+ const listEl = e.currentTarget;
28101
+ const listContainerEl = listEl.closest(".navi_list_container");
27700
28102
  const anchor = e.detail?.anchor;
27701
28103
  listContainerEl.showPopover();
27702
28104
  const positionPopover = () => {
@@ -27732,23 +28134,26 @@ const ListWithPopover = props => {
27732
28134
  positionPopover();
27733
28135
  });
27734
28136
  cleanupRef.current = () => cleanup.disconnect();
27735
- dispatchPublicCustomEvent(listContainerEl, "navi_list_open", {
28137
+ dispatchPublicCustomEvent(listEl, "navi_list_open", {
27736
28138
  event: e
27737
28139
  });
27738
28140
  },
27739
28141
  onnavi_list_request_close: e => {
27740
- const listContainerEl = e.currentTarget;
28142
+ const listEl = e.currentTarget;
28143
+ const listContainerEl = listEl.closest(".navi_list_container");
27741
28144
  cleanupRef.current?.();
27742
28145
  listContainerEl.removeAttribute("data-anchor-hidden");
27743
28146
  listContainerEl.hidePopover();
27744
- dispatchPublicCustomEvent(listContainerEl, "navi_list_close", {
28147
+ dispatchPublicCustomEvent(listEl, "navi_list_close", {
27745
28148
  event: e
27746
28149
  });
27747
28150
  }
27748
28151
  });
27749
28152
  };
27750
28153
 
27751
- // Interactive variant with action: calls the action whenever a value is selected.
28154
+ // Interactive variant: manages hover/keyboard/selection state and handles the
28155
+ // navi event protocol. When an action is provided it binds the action to ui state
28156
+ // and fires it on select. When only uiAction is provided it calls it directly.
27752
28157
  const ListWithAction = props => {
27753
28158
  const {
27754
28159
  ref,
@@ -27760,10 +28165,15 @@ const ListWithAction = props => {
27760
28165
  onActionAbort,
27761
28166
  onActionError,
27762
28167
  onActionEnd,
27763
- uiAction,
27764
28168
  ...rest
27765
28169
  } = props;
27766
- const boundAction = useAction(action);
28170
+ // Setup uiStateController and bind action to uiState
28171
+ const uiStateController = useUIStateController(props, "list", {
28172
+ allowNameless: true
28173
+ });
28174
+ const uiState = useUIState(uiStateController);
28175
+ // Bind action to uiState (like InputTextualWithAction, null-safe when no action)
28176
+ const [boundAction] = useActionBoundToOneParam(action, uiState);
27767
28177
  const {
27768
28178
  loading: actionLoading
27769
28179
  } = useActionStatus(boundAction);
@@ -27779,34 +28189,8 @@ const ListWithAction = props => {
27779
28189
  onError: onActionError,
27780
28190
  onEnd: onActionEnd
27781
28191
  });
27782
- const innerUiAction = (value, event) => {
27783
- uiAction?.(value, event);
27784
- // Dispatch action request so useActionEvents can pick it up
27785
- if (ref && ref.current) {
27786
- dispatchCustomEvent(ref.current, "navi_action_requested", {
27787
- bubbles: true,
27788
- detail: {
27789
- value
27790
- }
27791
- });
27792
- }
27793
- };
27794
- return jsx(List, {
27795
- "data-action": boundAction.name,
27796
- ...rest,
27797
- ref: ref,
27798
- action: undefined,
27799
- loading: loading || actionLoading,
27800
- uiAction: innerUiAction
27801
- });
27802
- };
27803
28192
 
27804
- // Interactive variant: manages hover/keyboard/selection state and handles the
27805
- // navi event protocol, then delegates rendering to ListUI.
27806
- const ListInteractive = props => {
27807
- const uiStateController = useUIStateController(props, "list", {
27808
- allowNameless: true
27809
- });
28193
+ // Mouse/keyboard pointed state
27810
28194
  const [mousePointedId, setMousePointedId] = useState(null);
27811
28195
  const [keyboardPointedId, setKeyboardPointedId] = useState(null);
27812
28196
  const anchorIdRef = useRef(null);
@@ -27828,146 +28212,163 @@ const ListInteractive = props => {
27828
28212
  }
27829
28213
  return visibleItemsRef.current.find(i => i.id === anchorId);
27830
28214
  };
27831
- return jsx(ListInteractiveContext.Provider, {
27832
- value: true,
27833
- children: jsx(ListMousePointedIdContext.Provider, {
27834
- value: mousePointedId,
27835
- children: jsx(ListKeyboardPointedIdContext.Provider, {
27836
- value: keyboardPointedId,
27837
- children: jsx(List, {
27838
- keyboardInteractions: true,
27839
- ...props,
27840
- uiAction: undefined,
27841
- onListVisibleItemsChange: visibleItems => {
27842
- props.onListVisibleItemsChange?.(visibleItems);
27843
- visibleItemsRef.current = visibleItems;
27844
- },
27845
- onnavi_list_request_hover: e => {
27846
- const {
27847
- item
27848
- } = e.detail;
27849
- setMousePointedId(item ? item.id : null);
27850
- },
27851
- onnavi_list_request_nav_from_current: e => {
27852
- const {
27853
- event = e,
27854
- goal
27855
- } = e.detail;
27856
- const visibleItems = visibleItemsRef.current;
27857
- const visibleItemCount = visibleItems.length;
27858
- if (visibleItemCount === 0) {
27859
- return;
27860
- }
27861
- const anchorIndex = getAnchorIndex();
27862
- const isDisabledIndex = i => Boolean(visibleItems[i]?.disabled);
27863
- const resolveIndex = () => {
27864
- if (goal === "down") {
27865
- if (anchorIndex === -1) {
27866
- let i = 0;
27867
- while (i < visibleItemCount && isDisabledIndex(i)) {
27868
- i++;
27869
- }
27870
- return i < visibleItemCount ? i : anchorIndex;
27871
- }
27872
- let belowIndex = anchorIndex + 1;
27873
- while (belowIndex < visibleItemCount && isDisabledIndex(belowIndex)) {
27874
- belowIndex++;
27875
- }
27876
- return belowIndex < visibleItemCount ? belowIndex : anchorIndex;
27877
- }
27878
- if (goal === "up") {
27879
- if (anchorIndex === -1) {
27880
- let i = visibleItemCount - 1;
27881
- while (i >= 0 && isDisabledIndex(i)) {
27882
- i--;
27883
- }
27884
- return i >= 0 ? i : anchorIndex;
27885
- }
27886
- let aboveIndex = anchorIndex - 1;
27887
- while (aboveIndex >= 0 && isDisabledIndex(aboveIndex)) {
27888
- aboveIndex--;
27889
- }
27890
- return aboveIndex >= 0 ? aboveIndex : anchorIndex;
27891
- }
27892
- if (goal === "first") {
27893
- let i = 0;
27894
- while (i < visibleItemCount && isDisabledIndex(i)) {
27895
- i++;
27896
- }
27897
- return i < visibleItemCount ? i : anchorIndex;
27898
- }
27899
- if (goal === "last") {
27900
- let i = visibleItemCount - 1;
27901
- while (i >= 0 && isDisabledIndex(i)) {
27902
- i--;
27903
- }
27904
- return i >= 0 ? i : anchorIndex;
27905
- }
27906
- return anchorIndex;
27907
- };
27908
- const index = resolveIndex();
27909
- if (index === anchorIndex) {
27910
- return;
27911
- }
27912
- if (event.type === "keydown") {
27913
- event.preventDefault();
27914
- }
27915
- const item = visibleItems[index];
27916
- dispatchCustomEvent(e.currentTarget, "navi_list_request_nav", {
27917
- event: e,
27918
- item
27919
- });
27920
- },
27921
- onnavi_list_request_interaction_state_reset: () => {
27922
- setAnchorId(null);
27923
- setKeyboardPointedId(null);
27924
- setMousePointedId(null);
27925
- },
27926
- onnavi_list_request_select_current: e => {
27927
- const item = getAnchorItem();
27928
- dispatchCustomEvent(e.currentTarget, "navi_list_request_select", {
27929
- event: e,
27930
- item
27931
- });
27932
- },
27933
- onnavi_list_nav: e => {
27934
- const {
27935
- item,
27936
- event
27937
- } = e.detail;
27938
- const id = item.id;
27939
- if (event.type === "navi_list_nav_top_on_displayed") {
27940
- // arrow down should focus first item for instance
27941
- setAnchorId(null);
27942
- } else {
27943
- setAnchorId(id);
27944
- }
27945
- if (event.type === "keydown") {
27946
- setKeyboardPointedId(id);
27947
- } else {
27948
- setKeyboardPointedId(null);
28215
+ const listVnode = jsx(List, {
28216
+ keyboardInteractions: true,
28217
+ "data-action": boundAction.name,
28218
+ ...rest,
28219
+ ref: ref,
28220
+ action: undefined,
28221
+ uiAction: undefined,
28222
+ loading: loading || actionLoading,
28223
+ value: uiState,
28224
+ onListVisibleItemsChange: visibleItems => {
28225
+ props.onListVisibleItemsChange?.(visibleItems);
28226
+ visibleItemsRef.current = visibleItems;
28227
+ },
28228
+ onnavi_list_request_hover: e => {
28229
+ const {
28230
+ item
28231
+ } = e.detail;
28232
+ setMousePointedId(item ? item.id : null);
28233
+ },
28234
+ onnavi_list_request_nav_from_current: e => {
28235
+ const {
28236
+ event = e,
28237
+ goal
28238
+ } = e.detail;
28239
+ const visibleItems = visibleItemsRef.current;
28240
+ const visibleItemCount = visibleItems.length;
28241
+ if (visibleItemCount === 0) {
28242
+ return;
28243
+ }
28244
+ const anchorIndex = getAnchorIndex();
28245
+ const isDisabledIndex = i => Boolean(visibleItems[i]?.disabled);
28246
+ const resolveIndex = () => {
28247
+ if (goal === "down") {
28248
+ if (anchorIndex === -1) {
28249
+ let i = 0;
28250
+ while (i < visibleItemCount && isDisabledIndex(i)) {
28251
+ i++;
27949
28252
  }
27950
- },
27951
- onnavi_list_select: e => {
27952
- const {
27953
- item,
27954
- event
27955
- } = e.detail;
27956
- const id = item.id;
27957
- setAnchorId(id);
27958
- if (event.type === "keydown") {
27959
- setKeyboardPointedId(id);
27960
- } else {
27961
- setKeyboardPointedId(null);
28253
+ return i < visibleItemCount ? i : anchorIndex;
28254
+ }
28255
+ let belowIndex = anchorIndex + 1;
28256
+ while (belowIndex < visibleItemCount && isDisabledIndex(belowIndex)) {
28257
+ belowIndex++;
28258
+ }
28259
+ return belowIndex < visibleItemCount ? belowIndex : anchorIndex;
28260
+ }
28261
+ if (goal === "up") {
28262
+ if (anchorIndex === -1) {
28263
+ let i = visibleItemCount - 1;
28264
+ while (i >= 0 && isDisabledIndex(i)) {
28265
+ i--;
27962
28266
  }
27963
- const value = item.value;
27964
- uiStateController.setUIState(value, event);
28267
+ return i >= 0 ? i : anchorIndex;
28268
+ }
28269
+ let aboveIndex = anchorIndex - 1;
28270
+ while (aboveIndex >= 0 && isDisabledIndex(aboveIndex)) {
28271
+ aboveIndex--;
28272
+ }
28273
+ return aboveIndex >= 0 ? aboveIndex : anchorIndex;
28274
+ }
28275
+ if (goal === "first") {
28276
+ let i = 0;
28277
+ while (i < visibleItemCount && isDisabledIndex(i)) {
28278
+ i++;
27965
28279
  }
28280
+ return i < visibleItemCount ? i : anchorIndex;
28281
+ }
28282
+ if (goal === "last") {
28283
+ let i = visibleItemCount - 1;
28284
+ while (i >= 0 && isDisabledIndex(i)) {
28285
+ i--;
28286
+ }
28287
+ return i >= 0 ? i : anchorIndex;
28288
+ }
28289
+ return anchorIndex;
28290
+ };
28291
+ const index = resolveIndex();
28292
+ if (index === anchorIndex) {
28293
+ if (event.type === "keydown") {
28294
+ event.preventDefault();
28295
+ }
28296
+ return;
28297
+ }
28298
+ if (event.type === "keydown") {
28299
+ event.preventDefault();
28300
+ }
28301
+ const item = visibleItems[index];
28302
+ dispatchCustomEvent(e.currentTarget, "navi_list_request_nav", {
28303
+ event: e,
28304
+ item
28305
+ });
28306
+ },
28307
+ onnavi_list_request_interaction_state_reset: () => {
28308
+ setAnchorId(null);
28309
+ setKeyboardPointedId(null);
28310
+ setMousePointedId(null);
28311
+ },
28312
+ onnavi_list_request_select_current: e => {
28313
+ const item = getAnchorItem();
28314
+ dispatchCustomEvent(e.currentTarget, "navi_list_request_select", {
28315
+ event: e,
28316
+ item
28317
+ });
28318
+ },
28319
+ onnavi_list_nav: e => {
28320
+ const {
28321
+ item,
28322
+ event
28323
+ } = e.detail;
28324
+ const id = item ? item.id : null;
28325
+ const isNonUserNav = event.type === "navi_list_nav_top_on_displayed" || event.type === "navi_list_top_match_change";
28326
+ if (isNonUserNav) {
28327
+ setAnchorId(null);
28328
+ } else {
28329
+ setAnchorId(id);
28330
+ }
28331
+ if (event.type === "keydown") {
28332
+ setKeyboardPointedId(id);
28333
+ } else {
28334
+ setKeyboardPointedId(null);
28335
+ }
28336
+ const isAutomaticNav = event.type === "navi_list_nav_top_on_displayed" || event.type === "navi_list_top_match_change" || event.type === "navi_scroll_restore";
28337
+ if (item && !isAutomaticNav) {
28338
+ uiStateController.setUIState(item.value, event);
28339
+ }
28340
+ }
28341
+ // Dispatch action request on select
28342
+ ,
28343
+
28344
+ onnavi_list_select: e => {
28345
+ const listEl = e.currentTarget;
28346
+ dispatchActionRequestedCustomEvent(listEl, {
28347
+ event: e,
28348
+ requester: e.target
28349
+ });
28350
+ }
28351
+ });
28352
+ return jsx(UIStateControllerContext.Provider, {
28353
+ value: uiStateController,
28354
+ children: jsx(UIStateContext.Provider, {
28355
+ value: uiState,
28356
+ children: jsx(ListInteractiveContext.Provider, {
28357
+ value: true,
28358
+ children: jsx(ListMousePointedIdContext.Provider, {
28359
+ value: mousePointedId,
28360
+ children: jsx(ListKeyboardPointedIdContext.Provider, {
28361
+ value: keyboardPointedId,
28362
+ children: listVnode
28363
+ })
27966
28364
  })
27967
28365
  })
27968
28366
  })
27969
28367
  });
27970
28368
  };
28369
+
28370
+ // Interactive variant: manages hover/keyboard/selection state and handles the
28371
+ // navi event protocol, then delegates rendering to ListUI.
27971
28372
  const ListWithKeyboardInteractions = props => {
27972
28373
  const {
27973
28374
  autoFocus,
@@ -28207,7 +28608,19 @@ const ListItemReal = ({
28207
28608
  // directly to the correct text node positions without re-searching.
28208
28609
  const textNodes = [];
28209
28610
  let totalLength = 0;
28210
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
28611
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
28612
+ acceptNode: node => {
28613
+ // Skip text nodes inside aria-hidden elements (icons, decorative emoji, etc.)
28614
+ let el = node.parentElement;
28615
+ while (el && el !== root) {
28616
+ if (el.getAttribute("aria-hidden") === "true") {
28617
+ return NodeFilter.FILTER_REJECT;
28618
+ }
28619
+ el = el.parentElement;
28620
+ }
28621
+ return NodeFilter.FILTER_ACCEPT;
28622
+ }
28623
+ });
28211
28624
  let node;
28212
28625
  while (node = walker.nextNode()) {
28213
28626
  textNodes.push({
@@ -28254,13 +28667,14 @@ const ListItemReal = ({
28254
28667
  "navi-list-item-real": "",
28255
28668
  "data-interactive": isInteractive ? "" : undefined,
28256
28669
  "data-anchor": isPointedByKeyboard ? "" : undefined,
28257
- "data-disabled": disabled ? "" : undefined,
28670
+ ...rest,
28671
+ ref: ref,
28258
28672
  onMouseEnter: e => {
28259
28673
  if (disabled) {
28260
28674
  return;
28261
28675
  }
28262
- const listContainerEl = e.currentTarget.closest(".navi_list_container");
28263
- dispatchCustomEvent(listContainerEl, "navi_list_request_hover", {
28676
+ const listEl = e.currentTarget.closest(".navi_list");
28677
+ dispatchCustomEvent(listEl, "navi_list_request_hover", {
28264
28678
  item,
28265
28679
  event: e
28266
28680
  });
@@ -28270,8 +28684,8 @@ const ListItemReal = ({
28270
28684
  if (disabled) {
28271
28685
  return;
28272
28686
  }
28273
- const listContainerEl = e.currentTarget.closest(".navi_list_container");
28274
- dispatchCustomEvent(listContainerEl, "navi_list_request_hover", {
28687
+ const listEl = e.currentTarget.closest(".navi_list");
28688
+ dispatchCustomEvent(listEl, "navi_list_request_hover", {
28275
28689
  item: null,
28276
28690
  event: e
28277
28691
  });
@@ -28284,23 +28698,21 @@ const ListItemReal = ({
28284
28698
  if (e.button !== 0) {
28285
28699
  return;
28286
28700
  }
28287
- const listContainerEl = e.currentTarget.closest(".navi_list_container");
28288
- dispatchCustomEvent(listContainerEl, "navi_list_request_select", {
28701
+ const listEl = e.currentTarget.closest(".navi_list");
28702
+ dispatchCustomEvent(listEl, "navi_list_request_select", {
28289
28703
  item,
28290
28704
  event: e
28291
28705
  });
28292
28706
  rest.onMouseDown?.(e);
28293
28707
  },
28294
- ...rest,
28295
- ref: ref,
28296
28708
  basePseudoState: {
28297
- ...rest.basePseudoState,
28298
28709
  ":disabled": Boolean(disabled),
28299
28710
  ":-navi-pointed": isPointed,
28300
28711
  ":-navi-pointed-by-mouse": isPointedByMouse,
28301
28712
  ":-navi-pointed-by-keyboard": isPointedByKeyboard,
28302
28713
  ":-navi-pointed-by-proxy": isPointedByProxy,
28303
- ":-navi-selected": selected
28714
+ ":-navi-selected": selected,
28715
+ ...rest.basePseudoState
28304
28716
  },
28305
28717
  children: children
28306
28718
  });
@@ -28310,9 +28722,13 @@ const LIST_ITEM_STYLE_CSS_VARS = {
28310
28722
  "color": "--list-item-color",
28311
28723
  "backgroundColor": "--list-item-background-color",
28312
28724
  "fontWeight": "--list-item-font-weight",
28313
- ":-navi-pointed": {
28314
- color: "--list-item-color-pointed",
28315
- backgroundColor: "--list-item-background-color-pointed"
28725
+ ":-navi-pointed-by-keyboard": {
28726
+ color: "--list-item-color-keyboard-pointed",
28727
+ backgroundColor: "--list-item-background-color-keyboard-pointed"
28728
+ },
28729
+ ":-navi-pointed-by-mouse": {
28730
+ color: "--list-item-color-mouse-pointed",
28731
+ backgroundColor: "--list-item-background-color-mouse-pointed"
28316
28732
  },
28317
28733
  ":hover": {
28318
28734
  color: "--list-item-color-hover",
@@ -28320,8 +28736,7 @@ const LIST_ITEM_STYLE_CSS_VARS = {
28320
28736
  },
28321
28737
  ":-navi-selected": {
28322
28738
  color: "--list-item-color-selected",
28323
- backgroundColor: "--list-item-background-color-selected",
28324
- fontWeight: "--list-item-font-weight-selected"
28739
+ backgroundColor: "--list-item-background-color-selected"
28325
28740
  },
28326
28741
  ":disabled": {
28327
28742
  color: "--list-item-color-disabled",
@@ -28395,9 +28810,9 @@ const ListItemHeader = props => {
28395
28810
  const defaultRef = useRef(null);
28396
28811
  const ref = props.ref || defaultRef;
28397
28812
  useDisplayedLayoutEffect(ref, headerEl => {
28398
- const scrollContainer = getScrollContainer(headerEl);
28813
+ const listContainerEl = headerEl.closest(".navi_list_container");
28399
28814
  const headerHeight = headerEl.getBoundingClientRect().height;
28400
- scrollContainer.style.setProperty("--list-header-height", `${headerHeight}px`);
28815
+ listContainerEl.style.setProperty("--list-header-height", `${headerHeight}px`);
28401
28816
  }, []);
28402
28817
  return jsx(ListItem, {
28403
28818
  ...props,
@@ -28410,9 +28825,9 @@ const ListItemFooter = props => {
28410
28825
  const defaultRef = useRef(null);
28411
28826
  const ref = props.ref || defaultRef;
28412
28827
  useDisplayedLayoutEffect(ref, headerEl => {
28413
- const scrollContainer = getScrollContainer(headerEl);
28828
+ const listContainerEl = headerEl.closest(".navi_list_container");
28414
28829
  const headerHeight = headerEl.getBoundingClientRect().height;
28415
- scrollContainer.style.setProperty("--list-footer-height", `${headerHeight}px`);
28830
+ listContainerEl.style.setProperty("--list-footer-height", `${headerHeight}px`);
28416
28831
  }, []);
28417
28832
  return jsx(ListItem, {
28418
28833
  ...props,
@@ -28421,42 +28836,42 @@ const ListItemFooter = props => {
28421
28836
  baseClassName: "navi_list_item_footer"
28422
28837
  });
28423
28838
  };
28424
- const requestListNavFromCurrent = (listContainerElement, {
28839
+ const requestListNavFromCurrent = (listEl, {
28425
28840
  event,
28426
28841
  goal
28427
28842
  }) => {
28428
- return dispatchCustomEvent(listContainerElement, "navi_list_request_nav_from_current", {
28843
+ return dispatchCustomEvent(listEl, "navi_list_request_nav_from_current", {
28429
28844
  event,
28430
28845
  goal
28431
28846
  });
28432
28847
  };
28433
- const requestListSelectCurrent = (listContainerElement, {
28848
+ const requestListSelectCurrent = (listEl, {
28434
28849
  event
28435
28850
  }) => {
28436
- return dispatchCustomEvent(listContainerElement, "navi_list_request_select_current", {
28851
+ return dispatchCustomEvent(listEl, "navi_list_request_select_current", {
28437
28852
  event
28438
28853
  });
28439
28854
  };
28440
- const requestListInteractionStateReset = (listContainerElement, {
28855
+ const requestListInteractionStateReset = (listEl, {
28441
28856
  event
28442
28857
  }) => {
28443
- return dispatchCustomEvent(listContainerElement, "navi_list_request_interaction_state_reset", {
28858
+ return dispatchCustomEvent(listEl, "navi_list_request_interaction_state_reset", {
28444
28859
  event
28445
28860
  });
28446
28861
  };
28447
- const requestListOpen = (listContainerElement, {
28862
+ const requestListOpen = (listEl, {
28448
28863
  event,
28449
28864
  anchor
28450
28865
  }) => {
28451
- return dispatchCustomEvent(listContainerElement, "navi_list_request_open", {
28866
+ return dispatchCustomEvent(listEl, "navi_list_request_open", {
28452
28867
  event,
28453
28868
  anchor
28454
28869
  });
28455
28870
  };
28456
- const requestListClose = (listContainerElement, {
28871
+ const requestListClose = (listEl, {
28457
28872
  event
28458
28873
  }) => {
28459
- return dispatchCustomEvent(listContainerElement, "navi_list_request_close", {
28874
+ return dispatchCustomEvent(listEl, "navi_list_request_close", {
28460
28875
  event
28461
28876
  });
28462
28877
  };
@@ -28478,7 +28893,7 @@ installImportMetaCssBuild(import.meta);/**
28478
28893
  * - <InputCheckbox /> for type="checkbox"
28479
28894
  * - <InputRadio /> for type="radio"
28480
28895
  */
28481
- const css$l = /* css */`
28896
+ const css$k = /* css */`
28482
28897
  @layer navi {
28483
28898
  .navi_input {
28484
28899
  --border-radius: 2px;
@@ -28739,7 +29154,7 @@ const InputTextualDispatcher = props => {
28739
29154
  };
28740
29155
  const InputNativeContext = createContext(null);
28741
29156
  const InputTextualUI = props => {
28742
- import.meta.css = [css$l, "@jsenv/navi/src/field/input_textual.jsx"];
29157
+ import.meta.css = [css$k, "@jsenv/navi/src/field/input_textual.jsx"];
28743
29158
  const {
28744
29159
  ref,
28745
29160
  type,
@@ -28993,18 +29408,16 @@ const InputControllingList = props => {
28993
29408
  onKeyDown,
28994
29409
  ...rest
28995
29410
  } = props;
28996
- const getListContainerEl = () => {
28997
- const listEl = document.getElementById(listId);
28998
- const listContainerEl = listEl.parentNode;
28999
- return listContainerEl;
29411
+ const getListEl = () => {
29412
+ return document.getElementById(listId);
29000
29413
  };
29001
29414
  useEffect(() => {
29002
29415
  const inputEl = ref.current;
29003
29416
  if (!inputEl) {
29004
29417
  return undefined;
29005
29418
  }
29006
- const listContainerEl = getListContainerEl();
29007
- if (!listContainerEl) {
29419
+ const listEl = getListEl();
29420
+ if (!listEl) {
29008
29421
  return undefined;
29009
29422
  }
29010
29423
  const onListSelect = e => {
@@ -29020,48 +29433,48 @@ const InputControllingList = props => {
29020
29433
  }
29021
29434
  }
29022
29435
  };
29023
- listContainerEl.addEventListener("navi_list_select", onListSelect);
29436
+ listEl.addEventListener("navi_list_select", onListSelect);
29024
29437
  return () => {
29025
- listContainerEl.removeEventListener("navi_list_select", onListSelect);
29438
+ listEl.removeEventListener("navi_list_select", onListSelect);
29026
29439
  };
29027
29440
  }, []);
29028
29441
  const onKeyDownWithShortcuts = shortcutsViaOnKeyDown({
29029
29442
  arrowdown: e => {
29030
- const listContainerEl = getListContainerEl();
29443
+ const listEl = getListEl();
29031
29444
  e.stopPropagation(); // when within a list, prevent list from handling it twice
29032
- return requestListNavFromCurrent(listContainerEl, {
29445
+ return requestListNavFromCurrent(listEl, {
29033
29446
  event: e,
29034
29447
  goal: "down"
29035
29448
  });
29036
29449
  },
29037
29450
  arrowup: e => {
29038
- const listContainerEl = getListContainerEl();
29451
+ const listEl = getListEl();
29039
29452
  e.stopPropagation(); // when within a list, prevent list from handling it twice
29040
- return requestListNavFromCurrent(listContainerEl, {
29453
+ return requestListNavFromCurrent(listEl, {
29041
29454
  event: e,
29042
29455
  goal: "up"
29043
29456
  });
29044
29457
  },
29045
29458
  home: e => {
29046
- const listContainerEl = getListContainerEl();
29459
+ const listEl = getListEl();
29047
29460
  e.stopPropagation(); // when within a list, prevent list from handling it twice
29048
- return requestListNavFromCurrent(listContainerEl, {
29461
+ return requestListNavFromCurrent(listEl, {
29049
29462
  event: e,
29050
29463
  goal: "first"
29051
29464
  });
29052
29465
  },
29053
29466
  end: e => {
29054
- const listContainerEl = getListContainerEl();
29467
+ const listEl = getListEl();
29055
29468
  e.stopPropagation(); // when within a list, prevent list from handling it twice
29056
- return requestListNavFromCurrent(listContainerEl, {
29469
+ return requestListNavFromCurrent(listEl, {
29057
29470
  event: e,
29058
29471
  goal: "last"
29059
29472
  });
29060
29473
  },
29061
29474
  enter: e => {
29062
- const listContainerEl = getListContainerEl();
29475
+ const listEl = getListEl();
29063
29476
  e.stopPropagation(); // when within a list, prevent list from handling it twice
29064
- return requestListSelectCurrent(listContainerEl, {
29477
+ return requestListSelectCurrent(listEl, {
29065
29478
  event: e
29066
29479
  });
29067
29480
  },
@@ -29070,14 +29483,14 @@ const InputControllingList = props => {
29070
29483
  // when the escape is meant to clear the search input (otherwise it would close the select too)
29071
29484
  if (e.currentTarget.type === "search" && e.currentTarget.value !== "") {
29072
29485
  e.stopPropagation();
29073
- return false;
29486
+ return true;
29074
29487
  }
29075
- const listContainerEl = getListContainerEl();
29488
+ const listEl = getListEl();
29076
29489
  // here we allow propagation of escape up to the <select> to allow closing if within a select
29077
29490
  // it also means list might catch escape and reset again but it's ok to reset twice here as it won't cause side effects
29078
29491
  // (if we need the same pattern for other events where it could be problematic we would have to mark
29079
29492
  // event as handled somehow to prevent list containing input to react to it)
29080
- return requestListInteractionStateReset(listContainerEl, {
29493
+ return requestListInteractionStateReset(listEl, {
29081
29494
  event: e
29082
29495
  });
29083
29496
  }
@@ -29119,18 +29532,16 @@ const InputTextualWithSuggestions = props => {
29119
29532
  expandedRef.current = false;
29120
29533
  setExpanded(false);
29121
29534
  };
29122
- const getListContainerEl = () => {
29123
- const listEl = document.getElementById(suggestions);
29124
- const listContainerEl = listEl.parentNode;
29125
- return listContainerEl;
29535
+ const getListEl = () => {
29536
+ return document.getElementById(suggestions);
29126
29537
  };
29127
29538
  const showSuggestions = e => {
29128
29539
  if (expandedRef.current) {
29129
29540
  return;
29130
29541
  }
29131
- const listContainerEl = getListContainerEl();
29132
- if (listContainerEl) {
29133
- requestListOpen(listContainerEl, {
29542
+ const listEl = getListEl();
29543
+ if (listEl) {
29544
+ requestListOpen(listEl, {
29134
29545
  event: e,
29135
29546
  anchor: ref.current
29136
29547
  });
@@ -29141,9 +29552,9 @@ const InputTextualWithSuggestions = props => {
29141
29552
  if (!expandedRef.current) {
29142
29553
  return;
29143
29554
  }
29144
- const listContainerEl = getListContainerEl();
29145
- if (!listContainerEl) {
29146
- requestListClose(listContainerEl, {
29555
+ const listEl = getListEl();
29556
+ if (listEl) {
29557
+ requestListClose(listEl, {
29147
29558
  event: e
29148
29559
  });
29149
29560
  collapse();
@@ -29151,8 +29562,8 @@ const InputTextualWithSuggestions = props => {
29151
29562
  };
29152
29563
  useEffect(() => {
29153
29564
  const inputEl = ref.current;
29154
- const listContainerEl = getListContainerEl();
29155
- if (!listContainerEl) {
29565
+ const listEl = getListEl();
29566
+ if (!listEl) {
29156
29567
  return undefined;
29157
29568
  }
29158
29569
  const onSelect = e => {
@@ -29168,9 +29579,9 @@ const InputTextualWithSuggestions = props => {
29168
29579
  }));
29169
29580
  hideSuggestions(e);
29170
29581
  };
29171
- listContainerEl.addEventListener("navi_list_select", onSelect);
29582
+ listEl.addEventListener("navi_list_select", onSelect);
29172
29583
  return () => {
29173
- listContainerEl.removeEventListener("navi_list_select", onSelect);
29584
+ listEl.removeEventListener("navi_list_select", onSelect);
29174
29585
  };
29175
29586
  }, [suggestions]);
29176
29587
  return jsx(ListIdContext.Provider, {
@@ -29382,7 +29793,7 @@ installImportMetaCssBuild(import.meta);/**
29382
29793
  * This means an editable thing MUST have a parent with position relative that wraps the content and the eventual editable input
29383
29794
  *
29384
29795
  */
29385
- const css$k = /* css */`
29796
+ const css$j = /* css */`
29386
29797
  .navi_editable_wrapper {
29387
29798
  --inset-top: 0px;
29388
29799
  --inset-right: 0px;
@@ -29431,7 +29842,7 @@ const useEditionController = () => {
29431
29842
  };
29432
29843
  };
29433
29844
  const Editable = props => {
29434
- import.meta.css = [css$k, "@jsenv/navi/src/field/edition/editable.jsx"];
29845
+ import.meta.css = [css$j, "@jsenv/navi/src/field/edition/editable.jsx"];
29435
29846
  let {
29436
29847
  children,
29437
29848
  action,
@@ -29700,7 +30111,7 @@ const Form = props => {
29700
30111
  });
29701
30112
  const uiState = useUIState(uiStateController);
29702
30113
  const form = renderActionableComponent(props, {
29703
- Basic: FormBasic,
30114
+ Basic: FormUI,
29704
30115
  WithAction: FormWithAction
29705
30116
  });
29706
30117
  return jsx(UIStateControllerContext.Provider, {
@@ -29711,7 +30122,7 @@ const Form = props => {
29711
30122
  })
29712
30123
  });
29713
30124
  };
29714
- const FormBasic = props => {
30125
+ const FormUI = props => {
29715
30126
  const uiStateController = useContext(UIStateControllerContext);
29716
30127
  const {
29717
30128
  readOnly,
@@ -29819,7 +30230,7 @@ const FormWithAction = props => {
29819
30230
  }
29820
30231
  });
29821
30232
  const innerLoading = loading || actionPending;
29822
- return jsx(FormBasic, {
30233
+ return jsx(FormUI, {
29823
30234
  "data-action": actionBoundToUIState.name,
29824
30235
  "data-method": action.meta?.httpVerb || method || "GET",
29825
30236
  ...rest,
@@ -29845,7 +30256,7 @@ const FormWithAction = props => {
29845
30256
  // form.dispatchEvent(customEvent);
29846
30257
  // };
29847
30258
 
29848
- installImportMetaCssBuild(import.meta);const css$j = /* css */`
30259
+ installImportMetaCssBuild(import.meta);const css$i = /* css */`
29849
30260
  .navi_group {
29850
30261
  --border-width: 1px;
29851
30262
 
@@ -29942,7 +30353,7 @@ const Group = ({
29942
30353
  vertical = row,
29943
30354
  ...props
29944
30355
  }) => {
29945
- import.meta.css = [css$j, "@jsenv/navi/src/field/group.jsx"];
30356
+ import.meta.css = [css$i, "@jsenv/navi/src/field/group.jsx"];
29946
30357
  if (typeof borderWidth === "string") {
29947
30358
  borderWidth = parseFloat(borderWidth);
29948
30359
  }
@@ -30133,7 +30544,7 @@ const useCleanup = () => {
30133
30544
  return cleanupMethods;
30134
30545
  };
30135
30546
 
30136
- installImportMetaCssBuild(import.meta);const css$i = /* css */`
30547
+ installImportMetaCssBuild(import.meta);const css$h = /* css */`
30137
30548
  .navi_dialog {
30138
30549
  &[open] {
30139
30550
  display: flex;
@@ -30146,7 +30557,7 @@ installImportMetaCssBuild(import.meta);const css$i = /* css */`
30146
30557
  }
30147
30558
  `;
30148
30559
  const Dialog = props => {
30149
- import.meta.css = [css$i, "@jsenv/navi/src/popup/dialog.jsx"];
30560
+ import.meta.css = [css$h, "@jsenv/navi/src/popup/dialog.jsx"];
30150
30561
  const {
30151
30562
  children,
30152
30563
  scrollTrap,
@@ -30214,12 +30625,18 @@ const Dialog = props => {
30214
30625
  ref: ref,
30215
30626
  baseClassName: "navi_dialog",
30216
30627
  onMouseDown: e => {
30628
+ rest.onMouseDown?.(e);
30217
30629
  // The <dialog> element covers the full viewport; clicking the backdrop
30218
30630
  // hits the dialog itself (not any child). Close when that happens.
30219
- if (!pointerTrap && e.target === ref.current) {
30631
+ if (!pointerTrap && e.button === 0 && e.target === ref.current) {
30220
30632
  onRequestClose(e);
30221
30633
  }
30222
- rest.onMouseDown?.(e);
30634
+ },
30635
+ onCancel: e => {
30636
+ // The browser fires "cancel" (then closes the dialog) when the user presses Escape.
30637
+ // Prevent the native close so we control the close flow and dispatch navi_dialog_close.
30638
+ e.preventDefault();
30639
+ onRequestClose(e);
30223
30640
  },
30224
30641
  onnavi_dialog_request_open: e => {
30225
30642
  const {
@@ -30251,7 +30668,7 @@ const requestDialogClose = (popoverElement, {
30251
30668
  });
30252
30669
  };
30253
30670
 
30254
- installImportMetaCssBuild(import.meta);const css$h = /* css */`
30671
+ installImportMetaCssBuild(import.meta);const css$g = /* css */`
30255
30672
  .navi_popover_backdrop {
30256
30673
  position: fixed;
30257
30674
  inset: 0;
@@ -30267,7 +30684,7 @@ installImportMetaCssBuild(import.meta);const css$h = /* css */`
30267
30684
  }
30268
30685
  `;
30269
30686
  const Popover = props => {
30270
- import.meta.css = [css$h, "@jsenv/navi/src/popup/popover.jsx"];
30687
+ import.meta.css = [css$g, "@jsenv/navi/src/popup/popover.jsx"];
30271
30688
  const {
30272
30689
  scrollTrap,
30273
30690
  pointerTrap,
@@ -30443,7 +30860,7 @@ const requestPopoverClose = (popoverElement, {
30443
30860
  });
30444
30861
  };
30445
30862
 
30446
- installImportMetaCssBuild(import.meta);const css$g = /* css */`
30863
+ installImportMetaCssBuild(import.meta);const css$f = /* css */`
30447
30864
  @layer navi {
30448
30865
  .navi_select {
30449
30866
  --select-border-radius: 2px;
@@ -30503,23 +30920,25 @@ installImportMetaCssBuild(import.meta);const css$g = /* css */`
30503
30920
  outline-offset: calc(-1 * var(--select-outline-width));
30504
30921
  user-select: none;
30505
30922
 
30506
- &:hover {
30923
+ --x-select-outline-width-focus-visible: calc(
30924
+ var(--select-border-width) + var(--select-outline-width)
30925
+ );
30926
+ --x-select-outline-offset-focus-visible: calc(
30927
+ -1 * (var(--select-border-width) + var(--select-outline-width))
30928
+ );
30929
+
30930
+ &[data-hover] {
30507
30931
  background-color: var(--select-background-color-hover);
30508
30932
  outline-color: var(--select-border-color-hover);
30509
30933
  }
30510
30934
 
30511
- &:focus,
30512
- &:focus-visible {
30513
- outline-width: calc(
30514
- var(--select-border-width) + var(--select-outline-width)
30515
- );
30516
- outline-color: var(--navi-focus-outline-color, #005fcc);
30517
- outline-offset: calc(
30518
- -1 * (var(--select-border-width) + var(--select-outline-width))
30519
- );
30935
+ &[data-focus-visible] {
30936
+ outline-width: var(--x-select-outline-width-focus-visible);
30937
+ outline-color: var(--navi-focus-outline-color);
30938
+ outline-offset: var(--x-select-outline-offset-focus-visible);
30520
30939
  }
30521
30940
 
30522
- &:disabled {
30941
+ &[data-disabled] {
30523
30942
  opacity: 0.5;
30524
30943
  cursor: default;
30525
30944
  }
@@ -30558,64 +30977,75 @@ installImportMetaCssBuild(import.meta);const css$g = /* css */`
30558
30977
  opacity: 0.6;
30559
30978
  }
30560
30979
 
30561
- /* When the list inside the popover has keyboard focus, keep the focus ring
30562
- on the select trigger for visual continuity */
30563
- &:not(:has(.navi_select_dialog .navi_list_container:focus)):has(
30564
- .navi_list_container:focus
30565
- ) {
30566
- outline-width: calc(
30567
- var(--select-border-width) + var(--select-outline-width)
30568
- );
30569
- outline-color: var(--navi-focus-outline-color, #005fcc);
30570
- outline-offset: calc(
30571
- -1 * (var(--select-border-width) + var(--select-outline-width))
30572
- );
30573
- }
30574
- .navi_list_container:focus {
30575
- outline: none;
30576
- }
30980
+ /* popover */
30981
+ &[aria-haspopup="listbox"] {
30982
+ &:has(.navi_list_container[data-focus-visible]) {
30983
+ outline-width: var(--x-select-outline-width-focus-visible);
30984
+ outline-color: var(--navi-focus-outline-color);
30985
+ outline-offset: var(--x-select-outline-offset-focus-visible);
30986
+ .navi_list_container {
30987
+ outline: none;
30988
+ }
30989
+ }
30577
30990
 
30578
- /* When the list inside the dialog has keyboard focus, show the focus ring
30579
- on the dialog instead */
30580
- .navi_select_dialog:has(.navi_list_container:focus) {
30581
- outline: var(--select-outline-width) solid
30582
- var(--navi-focus-outline-color, #005fcc);
30583
- }
30991
+ .navi_select_popover {
30992
+ position: absolute;
30993
+ inset: unset;
30994
+ min-width: var(--anchor-width, 0px);
30995
+ max-width: 95vw;
30996
+ max-height: 95dvh;
30997
+ margin: 0;
30998
+ padding: 0;
30999
+ background: white;
31000
+ border: none;
31001
+ border-radius: 0;
31002
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
31003
+ cursor: default; /* Reset pointer cursor within the select */
31004
+ overflow: auto;
31005
+ overscroll-behavior: none;
30584
31006
 
30585
- .navi_select_popover {
30586
- position: absolute;
30587
- inset: unset;
30588
- min-width: var(--select-anchor-width, 0px);
30589
- max-width: 95vw;
30590
- max-height: 95dvh;
30591
- margin: 0;
30592
- padding: 0;
30593
- background: white;
30594
- border: none;
30595
- border-radius: 0;
30596
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
30597
- cursor: default; /* Reset pointer cursor within the select */
30598
- overflow: auto;
30599
- overscroll-behavior: none;
31007
+ &:popover-open {
31008
+ display: flex;
31009
+ flex-direction: column;
31010
+ }
31011
+ }
30600
31012
  }
30601
31013
 
30602
- .navi_select_dialog {
30603
- max-height: 95dvh;
30604
- margin: auto;
30605
- padding: 0;
30606
- background: white;
30607
- border: none;
30608
- border-radius: 8px;
30609
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
30610
- cursor: default; /* Reset pointer cursor within the select */
31014
+ /* dialog */
31015
+ &[aria-haspopup="dialog"] {
31016
+ .navi_list_container {
31017
+ --list-max-height: none;
31018
+ }
30611
31019
 
30612
- &[open] {
30613
- display: flex;
30614
- flex-direction: column;
31020
+ /* When the list inside the dialog has keyboard focus, show the focus ring
31021
+ on the dialog instead */
31022
+ &:has(.navi_list_container[data-focus-visible]) {
31023
+ outline-width: var(--x-select-outline-width-focus-visible);
31024
+ outline-color: var(--navi-focus-outline-color);
31025
+ outline-offset: var(--x-select-outline-offset-focus-visible);
31026
+ .navi_list_container {
31027
+ outline: none;
31028
+ }
30615
31029
  }
30616
31030
 
30617
- &::backdrop {
30618
- background: rgba(0, 0, 0, 0.4);
31031
+ .navi_select_dialog {
31032
+ max-height: 95dvh;
31033
+ margin: auto;
31034
+ padding: 0;
31035
+ background: white;
31036
+ border: none;
31037
+ border-radius: 8px;
31038
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
31039
+ cursor: default; /* Reset pointer cursor within the select */
31040
+
31041
+ &[open] {
31042
+ display: flex;
31043
+ flex-direction: column;
31044
+ }
31045
+
31046
+ &::backdrop {
31047
+ background: rgba(0, 0, 0, 0.4);
31048
+ }
30619
31049
  }
30620
31050
  }
30621
31051
  }
@@ -30655,9 +31085,6 @@ const Select = props => {
30655
31085
  },
30656
31086
  emptyState: undefined
30657
31087
  });
30658
- uiStateController.onUIStateChange = (value, e) => {
30659
- uiStateController.uiAction?.(value, e);
30660
- };
30661
31088
  const uiState = useUIState(uiStateController);
30662
31089
  const value = Object.hasOwn(props, "value") ? props.value : uiState;
30663
31090
  return jsx(ParentUIStateControllerContext.Provider, {
@@ -30689,10 +31116,8 @@ const SelectDispatcher = props => {
30689
31116
  ...props
30690
31117
  });
30691
31118
  };
30692
- const SelectPlaceholderContext = createContext();
30693
- const SelectValueContext = createContext(null);
30694
31119
  const SelectUI = props => {
30695
- import.meta.css = [css$g, "@jsenv/navi/src/field/select.jsx"];
31120
+ import.meta.css = [css$f, "@jsenv/navi/src/field/select.jsx"];
30696
31121
  let {
30697
31122
  placeholder = "Select…",
30698
31123
  trigger,
@@ -30713,7 +31138,8 @@ const SelectUI = props => {
30713
31138
  const defaultRef = useRef();
30714
31139
  const ref = rest.ref || defaultRef;
30715
31140
  const hiddenInputId = useId();
30716
- const remainingProps = useConstraints(ref, rest);
31141
+ const hiddenInputRef = useRef(null);
31142
+ const remainingProps = useConstraints(hiddenInputRef, rest);
30717
31143
  const innerLoading = loading || contextLoading && contextLoadingElement === ref.current;
30718
31144
  const innerReadOnly = readOnly || contextReadOnly || innerLoading;
30719
31145
  const innerDisabled = disabled || contextDisabled;
@@ -30723,19 +31149,6 @@ const SelectUI = props => {
30723
31149
  useAutoFocus(ref, autoFocus, {
30724
31150
  preventScroll: autoFocusPreventScroll
30725
31151
  });
30726
-
30727
- // Re-run constraint validation when value changes (e.g. required constraint reads data-navi-value)
30728
- useLayoutEffect(() => {
30729
- const el = ref.current;
30730
- if (!el) {
30731
- return;
30732
- }
30733
- const validationInterface = el.__validationInterface__;
30734
- if (!validationInterface) {
30735
- return;
30736
- }
30737
- validationInterface.checkValidity();
30738
- }, [value]);
30739
31152
  if (trigger === undefined) {
30740
31153
  trigger = jsx(SelectTrigger, {});
30741
31154
  }
@@ -30748,7 +31161,6 @@ const SelectUI = props => {
30748
31161
  ,
30749
31162
 
30750
31163
  "data-navi-value": value || undefined,
30751
- "data-input-proxy": name ? `#${CSS.escape(hiddenInputId)}` : undefined,
30752
31164
  styleCSSVars: SelectStyleCSSVars,
30753
31165
  basePseudoState: {
30754
31166
  ...remainingProps.basePseudoState,
@@ -30763,6 +31175,7 @@ const SelectUI = props => {
30763
31175
  color: "var(--loader-color)",
30764
31176
  inset: -1
30765
31177
  }), jsx("input", {
31178
+ ref: hiddenInputRef,
30766
31179
  id: hiddenInputId,
30767
31180
  type: "hidden",
30768
31181
  name: name,
@@ -30778,6 +31191,8 @@ const SelectUI = props => {
30778
31191
  })]
30779
31192
  });
30780
31193
  };
31194
+ const SelectPlaceholderContext = createContext();
31195
+ const SelectValueContext = createContext(null);
30781
31196
  const SelectStyleCSSVars = {
30782
31197
  "borderWidth": "--select-border-width",
30783
31198
  "borderRadius": "--select-border-radius",
@@ -30875,7 +31290,7 @@ const SelectWithPopover = props => {
30875
31290
  anchor: ref.current
30876
31291
  });
30877
31292
  };
30878
- const requestClose = e => {
31293
+ const requestClose = (e = new CustomEvent("programmatic")) => {
30879
31294
  return requestPopoverClose(popoverRef.current, {
30880
31295
  event: e
30881
31296
  });
@@ -30884,10 +31299,10 @@ const SelectWithPopover = props => {
30884
31299
  const select = ref.current;
30885
31300
  debugFocus(`moveFocusToSelect("${e.type}")`);
30886
31301
  select.focus({
30887
- preventScroll: true,
30888
- focusVisible: true
31302
+ preventScroll: true
30889
31303
  });
30890
31304
  };
31305
+ const [shouldIgnoreThatClick, disableClickFor] = useIgnoreClickForMousedown();
30891
31306
  return jsx(SelectDispatcher, {
30892
31307
  disabled: disabled,
30893
31308
  "aria-haspopup": "listbox",
@@ -30913,6 +31328,9 @@ const SelectWithPopover = props => {
30913
31328
  // click triggered by enter won't open the popover
30914
31329
  return;
30915
31330
  }
31331
+ if (shouldIgnoreThatClick) {
31332
+ return;
31333
+ }
30916
31334
  // When a label is clicked it transfers focus to the select
30917
31335
  // in that case we want to open it (otherwise we have already opened on mousedown interaction)
30918
31336
  requestOpen(e);
@@ -30980,6 +31398,7 @@ const SelectWithPopover = props => {
30980
31398
  }
30981
31399
  // mousedown inside popover should not bubble to the select (would re-open it if that mousedown closes it)
30982
31400
  e.stopPropagation();
31401
+ disableClickFor(e);
30983
31402
  },
30984
31403
  onnavi_popover_open: e => {
30985
31404
  onOpen();
@@ -30997,7 +31416,10 @@ const SelectWithPopover = props => {
30997
31416
  scrollTrap: scrollTrap,
30998
31417
  pointerTrap: pointerTrap,
30999
31418
  focusTrap: focusTrap,
31000
- children: children
31419
+ children: jsx(SelectRequestCloseContext.Provider, {
31420
+ value: requestClose,
31421
+ children: children
31422
+ })
31001
31423
  })
31002
31424
  });
31003
31425
  };
@@ -31031,7 +31453,7 @@ const SelectWithDialog = props => {
31031
31453
  event: e
31032
31454
  });
31033
31455
  };
31034
- const requestClose = e => {
31456
+ const requestClose = (e = new CustomEvent("programmatic")) => {
31035
31457
  return requestDialogClose(dialogRef.current, {
31036
31458
  event: e
31037
31459
  });
@@ -31039,10 +31461,10 @@ const SelectWithDialog = props => {
31039
31461
  const moveFocusToSelect = e => {
31040
31462
  debugFocus(`moveFocusToSelect("${e.type}")`);
31041
31463
  ref.current.focus({
31042
- preventScroll: true,
31043
- focusVisible: true
31464
+ preventScroll: true
31044
31465
  });
31045
31466
  };
31467
+ const [shouldIgnoreThatClick, disableClickFor] = useIgnoreClickForMousedown();
31046
31468
  return jsx(SelectDispatcher, {
31047
31469
  disabled: disabled,
31048
31470
  "aria-haspopup": "dialog",
@@ -31068,6 +31490,11 @@ const SelectWithDialog = props => {
31068
31490
  // click triggered by enter won't open the dialog
31069
31491
  return;
31070
31492
  }
31493
+ if (shouldIgnoreThatClick) {
31494
+ // mousedown on the select already handled open/close; ignore this click
31495
+ // to avoid toggling the dialog again on mouseup
31496
+ return;
31497
+ }
31071
31498
  // When a label is clicked it transfers focus to the select, in that case we want to open it
31072
31499
  requestOpen(e);
31073
31500
  },
@@ -31123,13 +31550,74 @@ const SelectWithDialog = props => {
31123
31550
  }
31124
31551
  // mousedown inside dialog should not bubble to the select (would re-open it if that mousedown closes it)
31125
31552
  e.stopPropagation();
31553
+ disableClickFor(e);
31126
31554
  },
31127
31555
  scrollTrap: scrollTrap,
31128
31556
  pointerTrap: pointerTrap,
31129
- children: children
31557
+ children: jsx(SelectRequestCloseContext.Provider, {
31558
+ value: requestClose,
31559
+ children: children
31560
+ })
31130
31561
  })
31131
31562
  });
31132
31563
  };
31564
+ const SelectRequestCloseContext = createContext();
31565
+ const useSelectRequestClose = () => {
31566
+ return useContext(SelectRequestCloseContext);
31567
+ };
31568
+
31569
+ /**
31570
+ * Hook to prevent a `click` event from firing after a `mousedown` that already
31571
+ * handled an open/close action.
31572
+ *
31573
+ * Problem: when the user clicks a dialog's backdrop to close it, the browser
31574
+ * fires `mousedown` on the backdrop (which closes the dialog), then fires
31575
+ * `click` on whatever element is underneath once the dialog is gone. If that
31576
+ * element is the trigger button that originally opened the dialog, the `click`
31577
+ * would immediately re-open it.
31578
+ *
31579
+ * This problem only occurs when the dialog is closed on `mousedown`. If the
31580
+ * dialog were closed on `click` instead, the dialog would still be open when
31581
+ * the `click` fires on the backdrop, so the trigger button underneath would
31582
+ * never receive that `click`.
31583
+ *
31584
+ * Calling `stopPropagation()` or `preventDefault()` on the backdrop `mousedown`
31585
+ * does not help: the browser dispatches the subsequent `click` regardless,
31586
+ * targeting whichever element ends up under the pointer after the dialog closes.
31587
+ *
31588
+ * Usage:
31589
+ * const [shouldIgnoreThatClick, disableClickFor] = useIgnoreClickForMousedown();
31590
+ * // In onMouseDown (e.g. on the dialog backdrop): disableClickFor(e)
31591
+ * // In onClick (on the trigger): if (shouldIgnoreThatClick) return;
31592
+ *
31593
+ * `disableClickFor` arms the guard until the next `mouseup` on the document
31594
+ * (with a 1 s safety-net fallback), using `requestAnimationFrame` so the
31595
+ * `click` event — which fires synchronously after `mouseup` — is still blocked.
31596
+ */
31597
+ const useIgnoreClickForMousedown = () => {
31598
+ const pendingMousedownRef = useRef(false);
31599
+ const shouldIgnore = pendingMousedownRef.current;
31600
+ const disableClickFor = () => {
31601
+ pendingMousedownRef.current = true;
31602
+ const restoreClick = () => {
31603
+ clearTimeout(safetyTimeout);
31604
+ pendingMousedownRef.current = false;
31605
+ };
31606
+ const safetyTimeout = setTimeout(() => {
31607
+ pendingMousedownRef.current = false;
31608
+ restoreClick();
31609
+ }, 1000);
31610
+ document.addEventListener("mouseup", () => {
31611
+ requestAnimationFrame(() => {
31612
+ restoreClick();
31613
+ });
31614
+ }, {
31615
+ once: true,
31616
+ capture: true
31617
+ });
31618
+ };
31619
+ return [shouldIgnore, disableClickFor];
31620
+ };
31133
31621
 
31134
31622
  /**
31135
31623
  * applySearch — matches value against searchText.
@@ -31369,207 +31857,6 @@ const createSearch = (fields) => {
31369
31857
  };
31370
31858
  };
31371
31859
 
31372
- installImportMetaCssBuild(import.meta);const css$f = /* css */`
31373
- @layer navi {
31374
- .navi_dropdown_trigger {
31375
- --border-radius: 2px;
31376
- --border-width: 1px;
31377
- --outline-width: 1px;
31378
- --font-size: 14px;
31379
- --padding: 5px 8px;
31380
- --border-color: light-dark(#767676, #8e8e93);
31381
- --background-color: white;
31382
- --color: currentColor;
31383
- --placeholder-color: color-mix(in srgb, currentColor 60%, transparent);
31384
- --border-color-hover: color-mix(in srgb, var(--border-color) 70%, black);
31385
- --background-color-hover: color-mix(
31386
- in srgb,
31387
- var(--background-color) 95%,
31388
- black
31389
- );
31390
- }
31391
- }
31392
-
31393
- .navi_dropdown_trigger {
31394
- display: inline-flex;
31395
- box-sizing: border-box;
31396
- padding: var(--padding);
31397
- align-items: center;
31398
- gap: 6px;
31399
- color: var(--color);
31400
- font-size: var(--font-size);
31401
- text-align: left;
31402
- background-color: var(--background-color);
31403
- border: var(--border-width) solid transparent;
31404
- border-radius: var(--border-radius);
31405
- outline: var(--outline-width) solid var(--border-color);
31406
- outline-offset: calc(-1 * var(--outline-width));
31407
- cursor: pointer;
31408
- user-select: none;
31409
-
31410
- &:hover {
31411
- background-color: var(--background-color-hover);
31412
- outline-color: var(--border-color-hover);
31413
- }
31414
-
31415
- &:focus-visible {
31416
- outline-width: calc(var(--border-width) + var(--outline-width));
31417
- outline-color: var(--navi-focus-outline-color, #005fcc);
31418
- outline-offset: calc(-1 * (var(--border-width) + var(--outline-width)));
31419
- }
31420
- }
31421
-
31422
- .navi_dropdown_trigger_label {
31423
- min-width: 0;
31424
- flex: 1;
31425
- text-overflow: ellipsis;
31426
- white-space: nowrap;
31427
- overflow: hidden;
31428
- }
31429
-
31430
- .navi_dropdown_trigger_label[data-placeholder] {
31431
- color: var(--placeholder-color);
31432
- }
31433
-
31434
- .navi_dropdown_trigger_icon {
31435
- flex-shrink: 0;
31436
- opacity: 0.6;
31437
- }
31438
-
31439
- .navi_dropdown_dialog {
31440
- max-height: 95dvh;
31441
- margin: auto;
31442
- padding: 0;
31443
- background: white;
31444
- border: none;
31445
- border-radius: 8px;
31446
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
31447
-
31448
- &[open] {
31449
- display: flex;
31450
- flex-direction: column;
31451
- }
31452
-
31453
- &::backdrop {
31454
- background: rgba(0, 0, 0, 0.4);
31455
- }
31456
-
31457
- /* When the suggestion list inside the dialog has keyboard focus, show the
31458
- focus ring on the dialog itself and suppress it on the list container.
31459
- It's visually better */
31460
- &:has(.navi_list_container:focus-visible) {
31461
- outline: 1px solid var(--navi-focus-outline-color, #005fcc);
31462
- outline-offset: 1px;
31463
- }
31464
- & .navi_list_container:focus-visible {
31465
- outline: none;
31466
- }
31467
- }
31468
- `;
31469
- const DropdownCloseContext = createContext(null);
31470
-
31471
- /**
31472
- * Dropdown — a select-like trigger that opens a centered dialog.
31473
- *
31474
- * Props:
31475
- * value — the currently selected value (displayed in the trigger)
31476
- * placeholder — text shown when value is null/undefined/empty
31477
- * disabled — disable the trigger
31478
- * capturePointer — when true, clicking the backdrop does NOT close the dialog
31479
- * onOpen — called when the dialog opens
31480
- * onClose — called when the dialog closes
31481
- * children — content rendered inside the dialog
31482
- * ...rest — forwarded to the trigger <button>
31483
- */
31484
- const Dropdown = ({
31485
- value,
31486
- placeholder = "Select…",
31487
- disabled,
31488
- capturePointer,
31489
- captureScroll,
31490
- onOpen,
31491
- onClose,
31492
- children,
31493
- ...rest
31494
- }) => {
31495
- import.meta.css = [css$f, "@jsenv/navi/src/field/list/dropdown.jsx"];
31496
- const dialogRef = useRef(null);
31497
- const [open, setOpen] = useState(false);
31498
- const scrollTrapCleanupRef = useRef(null);
31499
- const openDialog = () => {
31500
- if (disabled) {
31501
- return;
31502
- }
31503
- const dialog = dialogRef.current;
31504
- if (!dialog || open) {
31505
- return;
31506
- }
31507
- dialog.showModal();
31508
- if (captureScroll) {
31509
- scrollTrapCleanupRef.current = trapScrollInside(dialog);
31510
- }
31511
- setOpen(true);
31512
- onOpen?.();
31513
- };
31514
- const closeDialog = () => {
31515
- const dialog = dialogRef.current;
31516
- if (!dialog || !open) {
31517
- return;
31518
- }
31519
- dialog.close();
31520
- setOpen(false);
31521
- if (captureScroll && scrollTrapCleanupRef.current) {
31522
- scrollTrapCleanupRef.current();
31523
- scrollTrapCleanupRef.current = null;
31524
- }
31525
- onClose?.();
31526
- };
31527
- const onDialogClick = e => {
31528
- // The <dialog> element itself is the backdrop area. Clicking directly on it
31529
- // (not on its content child) closes the dialog — unless capturePointer is set.
31530
- if (!capturePointer && e.target === dialogRef.current) {
31531
- closeDialog();
31532
- }
31533
- };
31534
- const hasValue = value !== null && value !== undefined && value !== "";
31535
- return jsxs(Fragment, {
31536
- children: [jsxs(Box, {
31537
- as: "button",
31538
- type: "button",
31539
- baseClassName: "navi_dropdown_trigger",
31540
- disabled: disabled,
31541
- onClick: openDialog,
31542
- ...rest,
31543
- children: [jsx("span", {
31544
- className: "navi_dropdown_trigger_label",
31545
- "data-placeholder": hasValue ? undefined : "",
31546
- children: hasValue ? String(value) : placeholder
31547
- }), jsx("span", {
31548
- className: "navi_dropdown_trigger_icon",
31549
- children: jsx(Icon, {
31550
- children: jsx(ChevronDownSvg, {})
31551
- })
31552
- })]
31553
- }), jsx("dialog", {
31554
- ref: dialogRef,
31555
- className: "navi_dropdown_dialog",
31556
- onClick: onDialogClick,
31557
- onClose: closeDialog,
31558
- children: jsx(DropdownCloseContext.Provider, {
31559
- value: closeDialog,
31560
- children: children
31561
- })
31562
- })]
31563
- });
31564
- };
31565
-
31566
- /**
31567
- * Hook to close the enclosing Dropdown dialog from inside its content.
31568
- */
31569
- const useDropdownClose = () => {
31570
- return useContext(DropdownCloseContext);
31571
- };
31572
-
31573
31860
  /**
31574
31861
  * useSearch — reorders items so matched ones come first (sorted by score desc),
31575
31862
  * followed by non-matched items in their natural order. No item is hidden.
@@ -35422,7 +35709,7 @@ installImportMetaCssBuild(import.meta);const css$7 = /* css */`
35422
35709
  --font-size: 0.7em;
35423
35710
  --x-background: var(--background);
35424
35711
  --x-background-color: var(--background-color, var(--x-background));
35425
- --x-color-contrasting: var(--navi-color-black);
35712
+ --x-color-contrasting: var(--navi-color-white);
35426
35713
  --x-color: var(--color, var(--x-color-contrasting));
35427
35714
  --padding-x: 0.8em;
35428
35715
  --padding-y: 0.4em;
@@ -35439,22 +35726,11 @@ installImportMetaCssBuild(import.meta);const css$7 = /* css */`
35439
35726
  background-color: var(--x-background-color);
35440
35727
  border-radius: 1em;
35441
35728
 
35442
- &[data-dark-background] {
35443
- --x-color-contrasting: var(--navi-color-white);
35729
+ &[data-accent-needs-dark-fg] {
35730
+ --x-color-contrasting: var(--navi-color-black);
35444
35731
  }
35445
35732
  }
35446
35733
  `;
35447
- const BadgeStyleCSSVars$1 = {
35448
- borderWidth: "--border-width",
35449
- borderRadius: "--border-radius",
35450
- paddingRight: "--padding-right",
35451
- paddingLeft: "--padding-left",
35452
- backgroundColor: "--background-color",
35453
- background: "--background",
35454
- borderColor: "--border-color",
35455
- color: "--color",
35456
- fontSize: "--font-size"
35457
- };
35458
35734
  const Badge = ({
35459
35735
  children,
35460
35736
  className,
@@ -35463,7 +35739,7 @@ const Badge = ({
35463
35739
  import.meta.css = [css$7, "@jsenv/navi/src/text/badge.jsx"];
35464
35740
  const defaultRef = useRef();
35465
35741
  const ref = props.ref || defaultRef;
35466
- useDarkBackgroundAttribute(ref);
35742
+ useAccentColorAttributes(ref, null);
35467
35743
  return jsx(Text, {
35468
35744
  ref: ref,
35469
35745
  className: withPropsClassName("navi_badge", className),
@@ -35473,6 +35749,17 @@ const Badge = ({
35473
35749
  children: children
35474
35750
  });
35475
35751
  };
35752
+ const BadgeStyleCSSVars$1 = {
35753
+ borderWidth: "--border-width",
35754
+ borderRadius: "--border-radius",
35755
+ paddingRight: "--padding-right",
35756
+ paddingLeft: "--padding-left",
35757
+ backgroundColor: "--background-color",
35758
+ background: "--background",
35759
+ borderColor: "--border-color",
35760
+ color: "--color",
35761
+ fontSize: "--font-size"
35762
+ };
35476
35763
 
35477
35764
  const LoadingDots = () => {
35478
35765
  return jsxs("svg", {
@@ -35548,7 +35835,7 @@ installImportMetaCssBuild(import.meta);const css$6 = /* css */`
35548
35835
  --font-size: 0.7em;
35549
35836
  --x-background: var(--background);
35550
35837
  --x-background-color: var(--background-color, var(--x-background));
35551
- --x-color-contrasting: var(--navi-color-black);
35838
+ --x-color-contrasting: var(--navi-color-white);
35552
35839
  --x-color: var(--color, var(--x-color-contrasting));
35553
35840
  --padding-x: 0.5em;
35554
35841
  --padding-y: 0.2em;
@@ -35557,8 +35844,8 @@ installImportMetaCssBuild(import.meta);const css$6 = /* css */`
35557
35844
  font-size: var(--font-size);
35558
35845
  vertical-align: inherit;
35559
35846
 
35560
- &[data-dark-background] {
35561
- --x-color-contrasting: var(--navi-color-white);
35847
+ &[data-accent-needs-dark-fg] {
35848
+ --x-color-contrasting: var(--navi-color-black);
35562
35849
  }
35563
35850
 
35564
35851
  &[data-loading] {
@@ -35632,17 +35919,6 @@ installImportMetaCssBuild(import.meta);const css$6 = /* css */`
35632
35919
  }
35633
35920
  }
35634
35921
  `;
35635
- const BadgeStyleCSSVars = {
35636
- borderWidth: "--border-width",
35637
- borderRadius: "--border-radius",
35638
- paddingRight: "--padding-right",
35639
- paddingLeft: "--padding-left",
35640
- backgroundColor: "--background-color",
35641
- background: "--background",
35642
- borderColor: "--border-color",
35643
- color: "--color",
35644
- fontSize: "--font-size"
35645
- };
35646
35922
  const BadgeCountOverflow = () => jsx("span", {
35647
35923
  className: "navi_count_badge_overflow",
35648
35924
  children: "+"
@@ -35666,7 +35942,7 @@ const BadgeCount = ({
35666
35942
  import.meta.css = [css$6, "@jsenv/navi/src/text/badge_count.jsx"];
35667
35943
  const defaultRef = useRef();
35668
35944
  const ref = props.ref || defaultRef;
35669
- useDarkBackgroundAttribute(ref, [loading]);
35945
+ useAccentColorAttributes(ref, null);
35670
35946
  let valueRequested = (() => {
35671
35947
  if (typeof children !== "string") return children;
35672
35948
  const parsed = Number(children);
@@ -35718,6 +35994,17 @@ const BadgeCount = ({
35718
35994
  })
35719
35995
  });
35720
35996
  };
35997
+ const BadgeStyleCSSVars = {
35998
+ borderWidth: "--border-width",
35999
+ borderRadius: "--border-radius",
36000
+ paddingRight: "--padding-right",
36001
+ paddingLeft: "--padding-left",
36002
+ backgroundColor: "--background-color",
36003
+ background: "--background",
36004
+ borderColor: "--border-color",
36005
+ color: "--color",
36006
+ fontSize: "--font-size"
36007
+ };
35721
36008
  const applyMaxToValue = (max, value) => {
35722
36009
  if (isNaN(value)) {
35723
36010
  return value;
@@ -35791,7 +36078,7 @@ const BadgeCountCircle = ({
35791
36078
  });
35792
36079
  };
35793
36080
 
35794
- installImportMetaCssBuild(import.meta);import.meta.css = [/* css */`
36081
+ installImportMetaCssBuild(import.meta);const css$5 = /* css */`
35795
36082
  @layer navi {
35796
36083
  .navi_caption {
35797
36084
  --color: #6b7280;
@@ -35807,14 +36094,12 @@ installImportMetaCssBuild(import.meta);import.meta.css = [/* css */`
35807
36094
  .navi_caption {
35808
36095
  color: var(--color);
35809
36096
  }
35810
- `, "@jsenv/navi/src/text/caption.jsx"];
35811
- const CaptionStyleCSSVars = {
35812
- color: "--color"
35813
- };
36097
+ `;
35814
36098
  const Caption = ({
35815
36099
  className,
35816
36100
  ...rest
35817
36101
  }) => {
36102
+ import.meta.css = [css$5, "@jsenv/navi/src/text/caption.jsx"];
35818
36103
  return jsx(Text, {
35819
36104
  as: "small",
35820
36105
  size: "0.8em" // We use em to be relative to the parent (we want to be smaller than the surrounding text)
@@ -35825,6 +36110,9 @@ const Caption = ({
35825
36110
  styleCSSVars: CaptionStyleCSSVars
35826
36111
  });
35827
36112
  };
36113
+ const CaptionStyleCSSVars = {
36114
+ color: "--color"
36115
+ };
35828
36116
 
35829
36117
  /**
35830
36118
  * Example of how you'd use this:
@@ -36194,7 +36482,7 @@ const interpolate = (template, values) => {
36194
36482
  });
36195
36483
  };
36196
36484
 
36197
- installImportMetaCssBuild(import.meta);const css$5 = /* css */`
36485
+ installImportMetaCssBuild(import.meta);const css$4 = /* css */`
36198
36486
  @layer navi {
36199
36487
  .navi_quantity {
36200
36488
  --unit-color: color-mix(in srgb, currentColor 50%, white);
@@ -36285,11 +36573,6 @@ QuantityIntl.addUnit = (unitName, langTranslations) => {
36285
36573
  });
36286
36574
  }
36287
36575
  };
36288
- const QuantityPropsCSSVars = {
36289
- unitColor: "--unit-color",
36290
- unitSizeRatio: "--unit-size-ratio"
36291
- };
36292
- const QuantityPseudoClasses = [":hover", ":active", ":read-only", ":disabled", ":-navi-loading"];
36293
36576
  const Quantity = ({
36294
36577
  children,
36295
36578
  unit,
@@ -36304,7 +36587,7 @@ const Quantity = ({
36304
36587
  bold = true,
36305
36588
  ...props
36306
36589
  }) => {
36307
- import.meta.css = [css$5, "@jsenv/navi/src/text/quantity.jsx"];
36590
+ import.meta.css = [css$4, "@jsenv/navi/src/text/quantity.jsx"];
36308
36591
  const value = parseQuantityValue(children);
36309
36592
  const valueRounded = integer && typeof value === "number" ? Math.round(value) : value;
36310
36593
  const valueFormatted = typeof valueRounded === "number" ? formatNumber(valueRounded, {
@@ -36346,6 +36629,11 @@ const Quantity = ({
36346
36629
  });
36347
36630
  };
36348
36631
  Quantity.Intl = QuantityIntl;
36632
+ const QuantityPropsCSSVars = {
36633
+ unitColor: "--unit-color",
36634
+ unitSizeRatio: "--unit-size-ratio"
36635
+ };
36636
+ const QuantityPseudoClasses = [":hover", ":active", ":read-only", ":disabled", ":-navi-loading"];
36349
36637
  const Unit = ({
36350
36638
  value,
36351
36639
  unit,
@@ -36388,7 +36676,7 @@ const parseQuantityValue = children => {
36388
36676
  return Number.isNaN(parsed) ? children : parsed;
36389
36677
  };
36390
36678
 
36391
- installImportMetaCssBuild(import.meta);const css$4 = /* css */`
36679
+ installImportMetaCssBuild(import.meta);const css$3 = /* css */`
36392
36680
  @layer navi {
36393
36681
  .navi_meter {
36394
36682
  --loader-color: var(--navi-loader-color);
@@ -36404,13 +36692,9 @@ installImportMetaCssBuild(import.meta);const css$4 = /* css */`
36404
36692
  --fill-color-suboptimum: light-dark(#fdb900, #ffc107);
36405
36693
  --fill-color-even-less-good: light-dark(#d83b01, #f44336);
36406
36694
 
36407
- --x-color: var(--navi-color-black);
36408
- --x-shadow-color: white;
36695
+ --x-color: white;
36696
+ --x-shadow-color: black;
36409
36697
  --shadow-size: 0.5em;
36410
- &[data-dark-background] {
36411
- --x-color: white;
36412
- --x-shadow-color: black;
36413
- }
36414
36698
  }
36415
36699
  }
36416
36700
 
@@ -36471,6 +36755,11 @@ installImportMetaCssBuild(import.meta);const css$4 = /* css */`
36471
36755
  opacity: 0.4;
36472
36756
  }
36473
36757
 
36758
+ &[data-accent-needs-dark-fg] {
36759
+ --x-color: white;
36760
+ --x-shadow-color: black;
36761
+ }
36762
+
36474
36763
  /* When caption is shown, the track takes the full height */
36475
36764
  &[data-has-caption] {
36476
36765
  .navi_meter_track_container {
@@ -36506,25 +36795,6 @@ installImportMetaCssBuild(import.meta);const css$4 = /* css */`
36506
36795
  }
36507
36796
  }
36508
36797
  `;
36509
- const MeterStyleCSSVars = {
36510
- trackColor: "--track-color",
36511
- borderColor: "--border-color",
36512
- borderRadius: "--border-radius",
36513
- height: "--height",
36514
- width: "--width"
36515
- };
36516
- const MeterPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading", ":-navi-meter-optimum", ":-navi-meter-suboptimum", ":-navi-meter-even-less-good"];
36517
- Object.assign(PSEUDO_CLASSES, {
36518
- ":-navi-meter-optimum": {
36519
- attribute: "data-optimum"
36520
- },
36521
- ":-navi-meter-suboptimum": {
36522
- attribute: "data-suboptimum"
36523
- },
36524
- ":-navi-meter-even-less-good": {
36525
- attribute: "data-even-less-good"
36526
- }
36527
- });
36528
36798
  const Meter = ({
36529
36799
  value = 0,
36530
36800
  min = 0,
@@ -36544,7 +36814,7 @@ const Meter = ({
36544
36814
  style,
36545
36815
  ...rest
36546
36816
  }) => {
36547
- import.meta.css = [css$4, "@jsenv/navi/src/text/meter.jsx"];
36817
+ import.meta.css = [css$3, "@jsenv/navi/src/text/meter.jsx"];
36548
36818
  const defaultRef = useRef();
36549
36819
  const ref = rest.ref || defaultRef;
36550
36820
  value = Number(value);
@@ -36573,8 +36843,8 @@ const Meter = ({
36573
36843
  // When fill covers less than half the track, the text center sits on the
36574
36844
  // empty track — use the track color for contrast. Otherwise use fill color.
36575
36845
  const backgroundElementSelector = fillRatio >= 0.5 ? ".navi_meter_fill" : ".navi_meter_track";
36576
- useDarkBackgroundAttribute(ref, [], {
36577
- backgroundElementSelector
36846
+ useAccentColorAttributes(ref, null, {
36847
+ elementSelector: backgroundElementSelector
36578
36848
  });
36579
36849
  return jsx(Box, {
36580
36850
  ref: ref,
@@ -36623,6 +36893,25 @@ const Meter = ({
36623
36893
  })
36624
36894
  });
36625
36895
  };
36896
+ const MeterStyleCSSVars = {
36897
+ trackColor: "--track-color",
36898
+ borderColor: "--border-color",
36899
+ borderRadius: "--border-radius",
36900
+ height: "--height",
36901
+ width: "--width"
36902
+ };
36903
+ const MeterPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading", ":-navi-meter-optimum", ":-navi-meter-suboptimum", ":-navi-meter-even-less-good"];
36904
+ Object.assign(PSEUDO_CLASSES, {
36905
+ ":-navi-meter-optimum": {
36906
+ attribute: "data-optimum"
36907
+ },
36908
+ ":-navi-meter-suboptimum": {
36909
+ attribute: "data-suboptimum"
36910
+ },
36911
+ ":-navi-meter-even-less-good": {
36912
+ attribute: "data-even-less-good"
36913
+ }
36914
+ });
36626
36915
  const getMeterLevel = (value, min, max, low, high, optimum) => {
36627
36916
  // Without low/high thresholds the whole range is one region → always optimum
36628
36917
  if (low === undefined && high === undefined) {
@@ -36767,7 +37056,7 @@ const SVGMaskOverlay = ({
36767
37056
  };
36768
37057
 
36769
37058
  installImportMetaCssBuild(import.meta);// We HAVE TO put paddings around the dialog to ensure window resizing respects this space
36770
- const css$3 = /* css */`
37059
+ const css$2 = /* css */`
36771
37060
  @layer navi {
36772
37061
  .navi_dialog_layout {
36773
37062
  --layout-margin: 30px;
@@ -36851,7 +37140,7 @@ const DialogLayout = ({
36851
37140
  alignY = "center",
36852
37141
  ...props
36853
37142
  }) => {
36854
- import.meta.css = [css$3, "@jsenv/navi/src/layout/dialog_layout.jsx"];
37143
+ import.meta.css = [css$2, "@jsenv/navi/src/layout/dialog_layout.jsx"];
36855
37144
  return jsx(Box, {
36856
37145
  baseClassName: "navi_dialog_layout",
36857
37146
  styleCSSVars: DialogLayoutStyleCSSVars,
@@ -36867,56 +37156,6 @@ const DialogLayout = ({
36867
37156
  });
36868
37157
  };
36869
37158
 
36870
- installImportMetaCssBuild(import.meta);const css$2 = /* css */`
36871
- @layer navi {
36872
- .navi_separator {
36873
- --size: 1px;
36874
- --color: #e4e4e7;
36875
- --spacing: 0.5em;
36876
- --spacing-start: 0.5em;
36877
- --spacing-end: 0.5em;
36878
- }
36879
- }
36880
-
36881
- .navi_separator {
36882
- width: 100%;
36883
- height: var(--size);
36884
- margin-top: var(--spacing-start, var(--spacing));
36885
- margin-bottom: var(--spacing-end, var(--spacing));
36886
- flex-shrink: 0;
36887
- background: var(--color);
36888
- border: none;
36889
-
36890
- &[data-vertical] {
36891
- display: inline-block;
36892
-
36893
- width: var(--size);
36894
- height: 1lh;
36895
- margin-top: 0;
36896
- margin-right: var(--spacing-end, var(--spacing));
36897
- margin-bottom: 0;
36898
- margin-left: var(--spacing-start, var(--spacing));
36899
- vertical-align: bottom;
36900
- }
36901
- }
36902
- `;
36903
- const SeparatorStyleCSSVars = {
36904
- color: "--color"
36905
- };
36906
- const Separator = ({
36907
- vertical,
36908
- ...props
36909
- }) => {
36910
- import.meta.css = [css$2, "@jsenv/navi/src/layout/separator.jsx"];
36911
- return jsx(Box, {
36912
- as: vertical ? "span" : "hr",
36913
- ...props,
36914
- "data-vertical": vertical ? "" : undefined,
36915
- baseClassName: "navi_separator",
36916
- styleCSSVars: SeparatorStyleCSSVars
36917
- });
36918
- };
36919
-
36920
37159
  installImportMetaCssBuild(import.meta);const css$1 = /* css */`
36921
37160
  @layer navi {
36922
37161
  .navi_viewport_layout {
@@ -37299,5 +37538,5 @@ const UserSvg = () => jsx("svg", {
37299
37538
  })
37300
37539
  });
37301
37540
 
37302
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, Checkbox, CheckboxList, CloseSvg, Code, Col, Colgroup, ConstructionSvg, Details, Dialog, DialogLayout, Dropdown, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemFooter, ListItemGroup, ListItemHeader, Loading, MessageBox, Meter, Nav, NaviDebug, Paragraph, Popover, Quantity, QuantityIntl, Radio, RadioList, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectionContext, Separator, SettingsSvg, SidePanel, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, actionRunEffect, addCustomMessage, applySearch, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createIntl, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, filterTableSelection, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, requestListClose, requestListOpen, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDarkBackgroundAttribute, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useDropdownClose, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSidePanelClose, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
37541
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, Checkbox, CheckboxList, CloseSvg, Code, Col, Colgroup, ConstructionSvg, Details, Dialog, DialogLayout, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemFooter, ListItemGroup, ListItemHeader, Loading, MessageBox, Meter, Nav, NaviDebug, Paragraph, Popover, Quantity, QuantityIntl, Radio, RadioList, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectionContext, Separator, SettingsSvg, SidePanel, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, actionRunEffect, addCustomMessage, applySearch, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createIntl, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, filterTableSelection, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, requestListClose, requestListOpen, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, useRouteStatus, useRunOnMount, useSearchText, useSelectRequestClose, useSelectableElement, useSelectionController, useSidePanelClose, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
37303
37542
  //# sourceMappingURL=jsenv_navi.js.map