@jsenv/navi 0.23.9 → 0.24.1

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.
@@ -1,5 +1,5 @@
1
1
  import { installImportMetaCssBuild } from "./jsenv_navi_side_effects.js";
2
- import { isValidElement, h, createContext, toChildArray, render, createRef, cloneElement } from "preact";
2
+ import { isValidElement, h, createContext, options, toChildArray, render, createRef, cloneElement } from "preact";
3
3
  import { useErrorBoundary, useLayoutEffect, useEffect, useMemo, useRef, useState, useCallback, useContext, 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";
@@ -7226,6 +7226,12 @@ const PSEUDO_CLASSES = {
7226
7226
  attribute: "data-invalid",
7227
7227
  test: (el) => el.matches(":invalid"),
7228
7228
  },
7229
+ ":-navi-highlighted": {
7230
+ attribute: "data-highlighted",
7231
+ },
7232
+ ":-navi-selected": {
7233
+ attribute: "data-selected",
7234
+ },
7229
7235
  ":-navi-loading": {
7230
7236
  attribute: "data-loading",
7231
7237
  },
@@ -7515,6 +7521,111 @@ const updateStyle = (element, style, preventInitialTransition) => {
7515
7521
  styleKeySetWeakMap.set(element, styleKeySet);
7516
7522
  };
7517
7523
 
7524
+ // Implementation notes:
7525
+ //
7526
+ // options.__r fires before each component render — we capture the current
7527
+ // component instance (vnode.__c) so useEarlyDOMEffect can register itself.
7528
+ //
7529
+ // options.__c (commitRoot) fires after refs are assigned and before any
7530
+ // useLayoutEffect runs. We flush all pending effects there.
7531
+ // The DOM node is read from component.__v.__e (vnode → root DOM node),
7532
+ // which Preact sets during diffing, before options.__c fires.
7533
+ //
7534
+ // stateMap (WeakMap) stores { cleanup, deps } per component instance.
7535
+ // It's auto-GC'd when a component is destroyed; options.unmount also
7536
+ // deletes entries eagerly to release cleanup functions sooner.
7537
+ //
7538
+ // pendingMap (Map) holds effects registered during the current render pass.
7539
+ // It is always fully cleared in options.__c — bounded to one commit, no leak.
7540
+
7541
+ /**
7542
+ * Like useLayoutEffect, but runs before any layout effect in the commit —
7543
+ * including those of descendant components.
7544
+ *
7545
+ * Use this when a parent needs to mutate the DOM (e.g. apply styles) so that
7546
+ * children can read those mutations in their own useLayoutEffect.
7547
+ *
7548
+ * The DOM node of the component is passed as the first argument to fn.
7549
+ * The effect is skipped if no DOM node is found (e.g. on a fragment root).
7550
+ *
7551
+ * Supports deps and cleanup return, same as useLayoutEffect.
7552
+ */
7553
+ const useEarlyDOMEffect = (fn, deps) => {
7554
+ const component = _currentComponent;
7555
+ if (component) {
7556
+ pendingMap.set(component, { fn, deps });
7557
+ }
7558
+ };
7559
+
7560
+ // Populated during render, consumed + cleared in options.__c each commit.
7561
+ const pendingMap = new Map(); // component → { fn, deps, ref }
7562
+
7563
+ // Persists across commits. WeakMap → no leak when component is destroyed.
7564
+ const stateMap = new WeakMap(); // component → { cleanup, deps }
7565
+
7566
+ let _currentComponent = null;
7567
+ const _prevBeforeRender = options.__r;
7568
+ options.__r = (vnode) => {
7569
+ _currentComponent = vnode.__c;
7570
+ if (_prevBeforeRender) {
7571
+ _prevBeforeRender(vnode);
7572
+ }
7573
+ };
7574
+
7575
+ const _prevCommit = options.__c;
7576
+ options.__c = (root, commitQueue) => {
7577
+ for (const [component, { fn, deps }] of pendingMap) {
7578
+ // component.__v is the component's vnode; __e is its root DOM node.
7579
+ // Both are set during diff, before options.__c fires.
7580
+ const element = component.__v && component.__v.__e;
7581
+ if (!element) {
7582
+ continue;
7583
+ }
7584
+ const prev = stateMap.get(component);
7585
+ const prevDeps = prev ? prev.deps : undefined;
7586
+ let depsChanged;
7587
+ if (!prevDeps || !deps || prevDeps.length !== deps.length) {
7588
+ depsChanged = true;
7589
+ } else {
7590
+ for (let i = 0; i < deps.length; i++) {
7591
+ if (!Object.is(deps[i], prevDeps[i])) {
7592
+ depsChanged = true;
7593
+ break;
7594
+ }
7595
+ }
7596
+ }
7597
+ if (depsChanged) {
7598
+ if (prev && prev.cleanup) {
7599
+ prev.cleanup();
7600
+ }
7601
+ const result = fn(element);
7602
+ const cleanup = typeof result === "function" ? result : undefined;
7603
+ stateMap.set(component, { cleanup, deps });
7604
+ }
7605
+ }
7606
+ pendingMap.clear();
7607
+ if (_prevCommit) {
7608
+ _prevCommit(root, commitQueue);
7609
+ }
7610
+ };
7611
+
7612
+ const _prevUnmount = options.unmount;
7613
+ options.unmount = (vnode) => {
7614
+ const component = vnode.__c;
7615
+ if (component) {
7616
+ const state = stateMap.get(component);
7617
+ if (state && state.cleanup) {
7618
+ state.cleanup();
7619
+ }
7620
+ // stateMap is a WeakMap so the entry is GC'd automatically,
7621
+ // but deleting explicitly releases the cleanup fn sooner.
7622
+ stateMap.delete(component);
7623
+ }
7624
+ if (_prevUnmount) {
7625
+ _prevUnmount(vnode);
7626
+ }
7627
+ };
7628
+
7518
7629
  installImportMetaCssBuild(import.meta);/**
7519
7630
  * Box - A Swiss Army Knife for Layout
7520
7631
  *
@@ -7539,6 +7650,33 @@ installImportMetaCssBuild(import.meta);/**
7539
7650
  * ## Spacing & Sizing
7540
7651
  *
7541
7652
  * Props for margin, padding, gap, width, height, expand, shrink, and more.
7653
+ *
7654
+ * ## Pseudo-class Styles
7655
+ *
7656
+ * The `style` prop supports pseudo-class keys alongside regular CSS properties.
7657
+ * This lets you express hover, focus, and custom interaction states in one object,
7658
+ * without writing CSS or adding class names:
7659
+ *
7660
+ * ```jsx
7661
+ * <Box
7662
+ * style={{
7663
+ * backgroundColor: "blue",
7664
+ * ":-navi:pressed": {
7665
+ * backgroundColor: "darkblue",
7666
+ * },
7667
+ * ":hover": {
7668
+ * backgroundColor: "lightblue",
7669
+ * },
7670
+ * }}
7671
+ * />
7672
+ * ```
7673
+ *
7674
+ * Styles are applied directly to the DOM (not via Preact's style prop) for two reasons:
7675
+ * 1. **Pseudo-class support**: reacting to `:hover`, `:focus`, or custom states like
7676
+ * `:-navi:pressed` without re-rendering the component on every pseudo state change.
7677
+ * 2. **Correct initial render**: pseudo-class state must be read from the DOM node at
7678
+ * mount time. Preact's style prop runs before the DOM exists, so the right initial
7679
+ * style can only be determined once the node is available.
7542
7680
  */
7543
7681
  import.meta.css = [/* css */`
7544
7682
  [navi-box-flow="inline"] {
@@ -7930,42 +8068,36 @@ const Box = props => {
7930
8068
  visitProp(styleValue, styleName, styleContext, boxStyles, "style");
7931
8069
  }
7932
8070
  }
7933
- const updateStyle = useCallback(state => {
7934
- const boxEl = ref.current;
7935
- applyStyle(boxEl, boxStyles, state, boxPseudoNamedStyles, preventInitialTransition);
7936
- }, styleDeps);
7937
- const finalStyleDeps = [pseudoStateSelector, innerPseudoState, updateStyle];
8071
+ styleDeps.push(pseudoStateSelector, innerPseudoState);
7938
8072
  let innerPseudoClasses;
7939
8073
  if (pseudoClassesFromStyleSet.size) {
7940
8074
  innerPseudoClasses = [...pseudoClasses];
7941
8075
  if (pseudoClasses !== PSEUDO_CLASSES_DEFAULT) {
7942
- finalStyleDeps.push(...pseudoClasses);
8076
+ styleDeps.push(...pseudoClasses);
7943
8077
  }
7944
8078
  for (const key of pseudoClassesFromStyleSet) {
7945
8079
  innerPseudoClasses.push(key);
7946
- finalStyleDeps.push(key);
8080
+ styleDeps.push(key);
7947
8081
  }
7948
8082
  } else {
7949
8083
  innerPseudoClasses = pseudoClasses;
7950
8084
  if (pseudoClasses !== PSEUDO_CLASSES_DEFAULT) {
7951
- finalStyleDeps.push(...pseudoClasses);
8085
+ styleDeps.push(...pseudoClasses);
7952
8086
  }
7953
8087
  }
7954
- useLayoutEffect(() => {
7955
- const boxEl = ref.current;
7956
- if (!boxEl) {
7957
- return null;
7958
- }
8088
+ useEarlyDOMEffect(boxEl => {
7959
8089
  const pseudoStateEl = pseudoStateSelector ? boxEl.querySelector(pseudoStateSelector) : boxEl;
7960
8090
  const visualEl = visualSelector ? boxEl.querySelector(visualSelector) : null;
7961
8091
  return initPseudoStyles(pseudoStateEl, {
7962
8092
  pseudoClasses: innerPseudoClasses,
7963
8093
  pseudoState: innerPseudoState,
7964
- effect: updateStyle,
8094
+ effect: state => {
8095
+ applyStyle(boxEl, boxStyles, state, boxPseudoNamedStyles, preventInitialTransition);
8096
+ },
7965
8097
  elementToImpact: boxEl,
7966
8098
  elementListeningPseudoState: visualEl === pseudoStateEl ? null : visualEl
7967
8099
  });
7968
- }, finalStyleDeps);
8100
+ }, styleDeps);
7969
8101
  }
7970
8102
 
7971
8103
  // When hasChildFunction is used it means
@@ -13884,7 +14016,7 @@ const RouteActive = ({
13884
14016
  const routeAction = (
13885
14017
  route,
13886
14018
  action,
13887
- paramsEffect = () => route.paramsSignal.value,
14019
+ paramsEffect = () => true,
13888
14020
  options = {},
13889
14021
  ) => {
13890
14022
  const actionBoundToRoute = actionRunEffect(
@@ -15656,7 +15788,7 @@ installImportMetaCssBuild(import.meta);
15656
15788
  * - Centers in viewport when no anchor element provided or anchor is too big
15657
15789
  */
15658
15790
 
15659
- import.meta.css = [/* css */`
15791
+ const css$w = /* css */`
15660
15792
  @layer navi {
15661
15793
  .navi_callout {
15662
15794
  --callout-success-color: #4caf50;
@@ -15798,7 +15930,7 @@ import.meta.css = [/* css */`
15798
15930
  }
15799
15931
  }
15800
15932
  }
15801
- `, "@jsenv/navi/src/field/validation/callout/callout.js"];
15933
+ `;
15802
15934
 
15803
15935
  /**
15804
15936
  * Shows a callout attached to the specified element
@@ -15830,6 +15962,7 @@ const openCallout = (message, {
15830
15962
  showErrorStack,
15831
15963
  debug = false
15832
15964
  } = {}) => {
15965
+ import.meta.css = [css$w, "@jsenv/navi/src/field/validation/callout/callout.js"];
15833
15966
  const callout = {
15834
15967
  opened: true,
15835
15968
  close: null,
@@ -15989,8 +16122,14 @@ const openCallout = (message, {
15989
16122
  }
15990
16123
  allowWheelThrough(calloutElement, anchorElement);
15991
16124
  anchorElement.setAttribute("data-callout", calloutId);
16125
+ dispatchCalloutCustomElement(anchorElement, new CustomEvent("navi_callout_open", {
16126
+ bubbles: true
16127
+ }));
15992
16128
  addTeardown(() => {
15993
16129
  anchorElement.removeAttribute("data-callout");
16130
+ dispatchCalloutCustomElement(anchorElement, new CustomEvent("navi_callout_close", {
16131
+ bubbles: true
16132
+ }));
15994
16133
  });
15995
16134
  addStatusEffect(status => {
15996
16135
  if (!status) {
@@ -16621,6 +16760,20 @@ const generateSvgWithoutArrow = (width, height) => {
16621
16760
  />
16622
16761
  </svg>`;
16623
16762
  };
16763
+ const dispatchCalloutCustomElement = (anchorElement, customEvent) => {
16764
+ let targetElement;
16765
+ const visualSelector = anchorElement.getAttribute("data-visual-selector");
16766
+ if (visualSelector) {
16767
+ const visualElement = anchorElement.querySelector(visualSelector);
16768
+ if (visualElement) {
16769
+ targetElement = visualElement;
16770
+ }
16771
+ } else {
16772
+ targetElement = anchorElement;
16773
+ }
16774
+ console.log("dispatch on", targetElement, "event", customEvent);
16775
+ targetElement.dispatchEvent(customEvent);
16776
+ };
16624
16777
 
16625
16778
  /**
16626
16779
  * Creates a live mirror of a source DOM element that automatically stays in sync.
@@ -16996,6 +17149,56 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-special-charset");
16996
17149
  CONSTRAINT_ATTRIBUTE_SET.add("data-min-special-char");
16997
17150
  CONSTRAINT_ATTRIBUTE_SET.add("data-min-special-char-message");
16998
17151
 
17152
+ const ONE_OF_CONSTRAINT = {
17153
+ name: "one_of",
17154
+ messageAttribute: "data-one-of-message",
17155
+ check: (field) => {
17156
+ const oneOf = field.getAttribute("data-one-of");
17157
+ if (!oneOf) {
17158
+ return null;
17159
+ }
17160
+ const fieldValue = field.value;
17161
+ if (!fieldValue) {
17162
+ return null;
17163
+ }
17164
+ const listEl = document.querySelector(oneOf);
17165
+ if (!listEl) {
17166
+ console.warn(
17167
+ `One of constraint: could not find element for selector "${oneOf}"`,
17168
+ );
17169
+ return null;
17170
+ }
17171
+ const allowedValues = collectAllowedValues(listEl);
17172
+ if (allowedValues.size === 0) {
17173
+ return null;
17174
+ }
17175
+ if (allowedValues.has(fieldValue)) {
17176
+ return null;
17177
+ }
17178
+ const message = field.getAttribute("data-one-of-message");
17179
+ if (message) {
17180
+ return message;
17181
+ }
17182
+ return `Veuillez choisir une valeur parmi les suggestions.`;
17183
+ },
17184
+ };
17185
+ CONSTRAINT_ATTRIBUTE_SET.add("data-one-of");
17186
+ CONSTRAINT_ATTRIBUTE_SET.add("data-one-of-message");
17187
+
17188
+ const collectAllowedValues = (listEl) => {
17189
+ const values = new Set();
17190
+ for (const optionEl of listEl.querySelectorAll("[role='option']")) {
17191
+ const value =
17192
+ optionEl.dataset.value ??
17193
+ optionEl.getAttribute("value") ??
17194
+ optionEl.textContent.trim();
17195
+ if (value) {
17196
+ values.add(value);
17197
+ }
17198
+ }
17199
+ return values;
17200
+ };
17201
+
16999
17202
  const READONLY_CONSTRAINT = {
17000
17203
  name: "readonly",
17001
17204
  messageAttribute: "data-readonly-message",
@@ -17748,6 +17951,7 @@ const NAVI_CONSTRAINT_SET = new Set([
17748
17951
  MIN_UPPER_LETTER_CONSTRAINT,
17749
17952
  MIN_LOWER_LETTER_CONSTRAINT,
17750
17953
  SAME_AS_CONSTRAINT,
17954
+ ONE_OF_CONSTRAINT,
17751
17955
  READONLY_CONSTRAINT,
17752
17956
  ]);
17753
17957
  const DEFAULT_CONSTRAINT_SET = new Set([
@@ -20166,7 +20370,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
20166
20370
  };
20167
20371
 
20168
20372
  installImportMetaCssBuild(import.meta);/* eslint-disable jsenv/no-unknown-params */
20169
- const css$u = /* css */`
20373
+ const css$v = /* css */`
20170
20374
  @layer navi {
20171
20375
  .navi_text {
20172
20376
  &[data-skeleton] {
@@ -20475,7 +20679,7 @@ const shouldInjectSpacingBetween = (left, right) => {
20475
20679
  };
20476
20680
  const OverflowPinnedElementContext = createContext(null);
20477
20681
  const Text = props => {
20478
- import.meta.css = [css$u, "@jsenv/navi/src/text/text.jsx"];
20682
+ import.meta.css = [css$v, "@jsenv/navi/src/text/text.jsx"];
20479
20683
  if (props.loading || props.skeleton) {
20480
20684
  return jsx(TextSkeleton, {
20481
20685
  ...props
@@ -20671,7 +20875,7 @@ const TextBasic = ({
20671
20875
  });
20672
20876
  };
20673
20877
 
20674
- installImportMetaCssBuild(import.meta);const css$t = /* css */`
20878
+ installImportMetaCssBuild(import.meta);const css$u = /* css */`
20675
20879
  .navi_text_anchor {
20676
20880
  vertical-align: baseline;
20677
20881
  user-select: none;
@@ -20706,7 +20910,7 @@ const TextAnchor = ({
20706
20910
  textSize,
20707
20911
  lineLayout
20708
20912
  }) => {
20709
- import.meta.css = [css$t, "@jsenv/navi/src/text/text_anchor.jsx"];
20913
+ import.meta.css = [css$u, "@jsenv/navi/src/text/text_anchor.jsx"];
20710
20914
  const anchorRef = useRef();
20711
20915
  useLayoutEffect(() => {
20712
20916
  const anchorEl = anchorRef.current;
@@ -20801,7 +21005,7 @@ const computeTopOffset = ({
20801
21005
  };
20802
21006
  const charTopCanvas = document.createElement("canvas");
20803
21007
 
20804
- installImportMetaCssBuild(import.meta);const css$s = /* css */`
21008
+ installImportMetaCssBuild(import.meta);const css$t = /* css */`
20805
21009
  @layer navi {
20806
21010
  /* Ensure data attributes from box.jsx can win to update display */
20807
21011
  .navi_icon {
@@ -20874,7 +21078,7 @@ const Icon = ({
20874
21078
  lineLayout,
20875
21079
  ...props
20876
21080
  }) => {
20877
- import.meta.css = [css$s, "@jsenv/navi/src/text/icon.jsx"];
21081
+ import.meta.css = [css$t, "@jsenv/navi/src/text/icon.jsx"];
20878
21082
  const innerChildren = href ? jsx("svg", {
20879
21083
  width: "100%",
20880
21084
  height: "100%",
@@ -20955,6 +21159,109 @@ const Icon = ({
20955
21159
  });
20956
21160
  };
20957
21161
 
21162
+ /**
21163
+ * Toggles a `data-dark-background` attribute on the referenced element based on its
21164
+ * computed background color. Pair it with a CSS variable to get automatic
21165
+ * light/dark text without hard-coding colors:
21166
+ *
21167
+ * ```css
21168
+ * .my-element {
21169
+ * --color-contrasting: black;
21170
+ * &[data-dark-background] {
21171
+ * --color-contrasting: white;
21172
+ * }
21173
+ * color: var(--color-contrasting);
21174
+ * }
21175
+ * ```
21176
+ *
21177
+ * - `data-dark-background` is **set** when the background is dark enough that white text
21178
+ * provides better (or equal) contrast.
21179
+ * - `data-dark-background` is **absent** when black text is the better choice.
21180
+ *
21181
+ * @param {import("preact").RefObject} ref - Ref to the element that receives
21182
+ * the `data-dark-background` attribute and is also passed to `contrastColor` for
21183
+ * resolving CSS variables.
21184
+ * @param {object} [options]
21185
+ * @param {string} [options.backgroundElementSelector] - CSS selector relative
21186
+ * to `ref.current` pointing to a child element whose `background-color`
21187
+ * should be tested instead of the element itself. Useful when the element
21188
+ * has a transparent background but contains a coloured child (e.g. a fill
21189
+ * bar inside a track).
21190
+ */
21191
+
21192
+ const useDarkBackgroundAttribute = (
21193
+ ref,
21194
+ deps = [],
21195
+ {
21196
+ backgroundElementSelector,
21197
+ attributeName = "data-dark-background",
21198
+ hardcoded = {},
21199
+ } = {},
21200
+ ) => {
21201
+ const innerDeps = [
21202
+ ...deps,
21203
+ // ref can change is the component pass a different ref on different render based on some logic
21204
+ // (can be used to control which element backgroundColor is being checked by switching the ref to another element)
21205
+ ref,
21206
+ // backgroundElementSelector can change if the component pass a different selector on different render based on some logic
21207
+ // (can be used to control which element backgroundColor is being checked by switching the selector to point to another element)
21208
+ backgroundElementSelector,
21209
+ ];
21210
+
21211
+ const hardcodedMap = new Map();
21212
+ for (const key of Object.keys(hardcoded)) {
21213
+ const value = hardcoded[key];
21214
+ innerDeps.push(key, value);
21215
+ const colorString = normalizeColorString(key);
21216
+ hardcodedMap.set(colorString, value);
21217
+ }
21218
+
21219
+ useLayoutEffect(() => {
21220
+ const el = ref.current;
21221
+ if (!el) {
21222
+ return undefined;
21223
+ }
21224
+ let elementToCheck = el;
21225
+ if (backgroundElementSelector) {
21226
+ elementToCheck = el.querySelector(backgroundElementSelector);
21227
+ if (!elementToCheck) {
21228
+ return undefined;
21229
+ }
21230
+ }
21231
+ const updateAttribute = () => {
21232
+ const computedStyle = getComputedStyle(elementToCheck);
21233
+ const backgroundColor = computedStyle.backgroundColor;
21234
+ if (!backgroundColor) {
21235
+ el.removeAttribute(attributeName);
21236
+ return;
21237
+ }
21238
+ const backgroundColorString = normalizeColorString(backgroundColor, el);
21239
+ const hardcodedContrast = hardcodedMap.get(backgroundColorString);
21240
+ const contrastingColor =
21241
+ hardcodedContrast || contrastColor(backgroundColor, el);
21242
+ if (contrastingColor === "white") {
21243
+ el.setAttribute(attributeName, "");
21244
+ } else {
21245
+ el.removeAttribute(attributeName);
21246
+ }
21247
+ };
21248
+ updateAttribute();
21249
+ el.addEventListener(NAVI_PSEUDO_STATE_CUSTOM_EVENT, updateAttribute);
21250
+ return () => {
21251
+ el.removeEventListener(NAVI_PSEUDO_STATE_CUSTOM_EVENT, updateAttribute);
21252
+ el.removeAttribute(attributeName);
21253
+ };
21254
+ }, innerDeps);
21255
+ };
21256
+
21257
+ const normalizeColorString = (color, el) => {
21258
+ const colorRgba = resolveCSSColor(color, el);
21259
+ if (!colorRgba) {
21260
+ return "";
21261
+ }
21262
+ return String(colorRgba);
21263
+ };
21264
+
20958
21265
  const useFormEvents = (
20959
21266
  elementRef,
20960
21267
  {
@@ -21477,7 +21784,7 @@ const useUIState = (uiStateController) => {
21477
21784
  };
21478
21785
 
21479
21786
  installImportMetaCssBuild(import.meta);/* eslint-disable jsenv/no-unknown-params */
21480
- const css$r = /* css */`
21787
+ const css$s = /* css */`
21481
21788
  @layer navi {
21482
21789
  .navi_button {
21483
21790
  --button-outline-width: 1px;
@@ -21559,8 +21866,6 @@ const css$r = /* css */`
21559
21866
  --x-button-background-color: var(--button-background-color);
21560
21867
  --x-button-color: var(--button-color);
21561
21868
  --x-button-cursor: var(--button-cursor);
21562
-
21563
- position: relative;
21564
21869
  box-sizing: border-box;
21565
21870
  aspect-ratio: inherit;
21566
21871
  padding: 0;
@@ -21573,6 +21878,10 @@ const css$r = /* css */`
21573
21878
  touch-action: manipulation;
21574
21879
  user-select: none;
21575
21880
 
21881
+ &[data-dark-background] {
21882
+ --button-color: white;
21883
+ }
21884
+
21576
21885
  &[data-icon] {
21577
21886
  --button-padding: 0;
21578
21887
  }
@@ -21741,7 +22050,7 @@ const css$r = /* css */`
21741
22050
  }
21742
22051
  `;
21743
22052
  const Button = props => {
21744
- import.meta.css = [css$r, "@jsenv/navi/src/field/button.jsx"];
22053
+ import.meta.css = [css$s, "@jsenv/navi/src/field/button.jsx"];
21745
22054
  return renderActionableComponent(props, {
21746
22055
  Basic: ButtonBasicDispatch,
21747
22056
  WithAction: ButtonWithAction,
@@ -21855,6 +22164,7 @@ const ButtonBasic = props => {
21855
22164
  innerTarget = target === undefined ? isSameSite ? undefined : "_blank" : target;
21856
22165
  innerRel = rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel;
21857
22166
  }
22167
+ useDarkBackgroundAttribute(ref, [innerLoading, innerDisabled, innerReadOnly]);
21858
22168
  const renderButtonContent = buttonProps => {
21859
22169
  return jsxs(Text, {
21860
22170
  ...buttonProps,
@@ -22217,104 +22527,6 @@ const Title = props => {
22217
22527
  });
22218
22528
  };
22219
22529
 
22220
- /**
22221
- * Toggles a `data-dark-background` attribute on the referenced element based on its
22222
- * computed background color. Pair it with a CSS variable to get automatic
22223
- * light/dark text without hard-coding colors:
22224
- *
22225
- * ```css
22226
- * .my-element {
22227
- * --color-contrasting: black;
22228
- * &[data-dark-background] {
22229
- * --color-contrasting: white;
22230
- * }
22231
- * color: var(--color-contrasting);
22232
- * }
22233
- * ```
22234
- *
22235
- * - `data-dark-background` is **set** when the background is dark enough that white text
22236
- * provides better (or equal) contrast.
22237
- * - `data-dark-background` is **absent** when black text is the better choice.
22238
- *
22239
- * @param {import("preact").RefObject} ref - Ref to the element that receives
22240
- * the `data-dark-background` attribute and is also passed to `contrastColor` for
22241
- * resolving CSS variables.
22242
- * @param {object} [options]
22243
- * @param {string} [options.backgroundElementSelector] - CSS selector relative
22244
- * to `ref.current` pointing to a child element whose `background-color`
22245
- * should be tested instead of the element itself. Useful when the element
22246
- * has a transparent background but contains a coloured child (e.g. a fill
22247
- * bar inside a track).
22248
- */
22249
-
22250
- const useDarkBackgroundAttribute = (
22251
- ref,
22252
- deps = [],
22253
- {
22254
- backgroundElementSelector,
22255
- attributeName = "data-dark-background",
22256
- hardcoded = {},
22257
- } = {},
22258
- ) => {
22259
- const innerDeps = [
22260
- ...deps,
22261
- // ref can change is the component pass a different ref on different render based on some logic
22262
- // (can be used to control which element backgroundColor is being checked by switching the ref to another element)
22263
- ref,
22264
- // backgroundElementSelector can change if the component pass a different selector on different render based on some logic
22265
- // (can be used to control which element backgroundColor is being checked by switching the selector to point to another element)
22266
- backgroundElementSelector,
22267
- ];
22268
-
22269
- const hardcodedMap = new Map();
22270
- for (const key of Object.keys(hardcoded)) {
22271
- const value = hardcoded[key];
22272
- innerDeps.push(key, value);
22273
- const colorString = normalizeColorString(key);
22274
- hardcodedMap.set(colorString, value);
22275
- }
22276
-
22277
- useLayoutEffect(() => {
22278
- const el = ref.current;
22279
- if (!el) {
22280
- return null;
22281
- }
22282
- let elementToCheck = el;
22283
- if (backgroundElementSelector) {
22284
- elementToCheck = el.querySelector(backgroundElementSelector);
22285
- if (!elementToCheck) {
22286
- return null;
22287
- }
22288
- }
22289
- const computedStyle = getComputedStyle(elementToCheck);
22290
- const backgroundColor = computedStyle.backgroundColor;
22291
- if (!backgroundColor) {
22292
- el.removeAttribute(attributeName);
22293
- return null;
22294
- }
22295
- const backgroundColorString = normalizeColorString(backgroundColor, el);
22296
- const hardcodedContrast = hardcodedMap.get(backgroundColorString);
22297
- const contrastingColor =
22298
- hardcodedContrast || contrastColor(backgroundColor, el);
22299
- if (contrastingColor === "white") {
22300
- el.setAttribute(attributeName, "");
22301
- return () => {
22302
- el.removeAttribute(attributeName);
22303
- };
22304
- }
22305
- el.removeAttribute(attributeName);
22306
- return null;
22307
- }, innerDeps);
22308
- };
22309
-
22310
- const normalizeColorString = (color, el) => {
22311
- const colorRgba = resolveCSSColor(color, el);
22312
- if (!colorRgba) {
22313
- return "";
22314
- }
22315
- return String(colorRgba);
22316
- };
22317
-
22318
22530
  /**
22319
22531
  * Hook that reactively checks if a URL is visited.
22320
22532
  * Re-renders when the visited URL set changes.
@@ -22370,7 +22582,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
22370
22582
  };
22371
22583
 
22372
22584
  installImportMetaCssBuild(import.meta);/* eslint-disable jsenv/no-unknown-params */
22373
- const css$q = /* css */`
22585
+ const css$r = /* css */`
22374
22586
  @layer navi {
22375
22587
  .navi_link {
22376
22588
  --link-border-radius: unset;
@@ -22442,10 +22654,22 @@ const css$q = /* css */`
22442
22654
 
22443
22655
  position: relative;
22444
22656
  aspect-ratio: inherit;
22445
- padding-top: max(var(--x-link-padding-top), var(--link-loading-outline-size));
22446
- padding-right: max(var(--x-link-padding-right), var(--link-loading-outline-size));
22447
- padding-bottom: max(var(--x-link-padding-bottom), var(--link-loading-outline-size));
22448
- padding-left: max(var(--x-link-padding-left), var(--link-loading-outline-size));
22657
+ padding-top: max(
22658
+ var(--x-link-padding-top),
22659
+ var(--link-loading-outline-size)
22660
+ );
22661
+ padding-right: max(
22662
+ var(--x-link-padding-right),
22663
+ var(--link-loading-outline-size)
22664
+ );
22665
+ padding-bottom: max(
22666
+ var(--x-link-padding-bottom),
22667
+ var(--link-loading-outline-size)
22668
+ );
22669
+ padding-left: max(
22670
+ var(--x-link-padding-left),
22671
+ var(--link-loading-outline-size)
22672
+ );
22449
22673
  color: var(--x-link-color);
22450
22674
  text-decoration: var(--x-link-text-decoration);
22451
22675
  background: var(--x-link-background);
@@ -22712,13 +22936,10 @@ Object.assign(PSEUDO_CLASSES, {
22712
22936
  },
22713
22937
  ":-navi-href-current": {
22714
22938
  attribute: "data-href-current"
22715
- },
22716
- ":-navi-selected": {
22717
- attribute: "data-selected"
22718
22939
  }
22719
22940
  });
22720
22941
  const Link = props => {
22721
- import.meta.css = [css$q, "@jsenv/navi/src/nav/link/link.jsx"];
22942
+ import.meta.css = [css$r, "@jsenv/navi/src/nav/link/link.jsx"];
22722
22943
  return renderActionableComponent(props, {
22723
22944
  Basic: LinkBasic,
22724
22945
  WithAction: LinkWithAction
@@ -22980,7 +23201,7 @@ installImportMetaCssBuild(import.meta);/**
22980
23201
  * TabList component with support for horizontal and vertical layouts
22981
23202
  * https://dribbble.com/search/tabs
22982
23203
  */
22983
- const css$p = /* css */`
23204
+ const css$q = /* css */`
22984
23205
  @layer navi {
22985
23206
  .navi_nav {
22986
23207
  --nav-border: none;
@@ -23116,7 +23337,7 @@ const Nav = ({
23116
23337
  panelBorderConnection,
23117
23338
  ...props
23118
23339
  }) => {
23119
- import.meta.css = [css$p, "@jsenv/navi/src/nav/link/nav.jsx"];
23340
+ import.meta.css = [css$q, "@jsenv/navi/src/nav/link/nav.jsx"];
23120
23341
  children = toChildArray(children);
23121
23342
  return jsx(Box, {
23122
23343
  as: "nav",
@@ -23164,7 +23385,7 @@ const useFocusGroup = (
23164
23385
 
23165
23386
  installImportMetaCssBuild(import.meta);const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
23166
23387
  const downArrowPath = "M480-280L160-600l80-80 240 240 240-240 80 80-320 320z";
23167
- const css$o = /* css */`
23388
+ const css$p = /* css */`
23168
23389
  .navi_summary_marker {
23169
23390
  width: 1em;
23170
23391
  height: 1em;
@@ -23249,7 +23470,7 @@ const SummaryMarker = ({
23249
23470
  open,
23250
23471
  loading
23251
23472
  }) => {
23252
- import.meta.css = [css$o, "@jsenv/navi/src/field/details/summary_marker.jsx"];
23473
+ import.meta.css = [css$p, "@jsenv/navi/src/field/details/summary_marker.jsx"];
23253
23474
  const showLoading = useDebounceTrue(loading, 300);
23254
23475
  const mountedRef = useRef(false);
23255
23476
  const prevOpenRef = useRef(open);
@@ -23303,7 +23524,7 @@ const SummaryMarker = ({
23303
23524
  });
23304
23525
  };
23305
23526
 
23306
- installImportMetaCssBuild(import.meta);const css$n = /* css */`
23527
+ installImportMetaCssBuild(import.meta);const css$o = /* css */`
23307
23528
  .navi_details {
23308
23529
  position: relative;
23309
23530
  z-index: 1;
@@ -23340,7 +23561,7 @@ installImportMetaCssBuild(import.meta);const css$n = /* css */`
23340
23561
  }
23341
23562
  `;
23342
23563
  const Details = props => {
23343
- import.meta.css = [css$n, "@jsenv/navi/src/field/details/details.jsx"];
23564
+ import.meta.css = [css$o, "@jsenv/navi/src/field/details/details.jsx"];
23344
23565
  const {
23345
23566
  value = "on",
23346
23567
  persists
@@ -23655,7 +23876,7 @@ const fieldPropSet = new Set([
23655
23876
  "data-testid",
23656
23877
  ]);
23657
23878
 
23658
- installImportMetaCssBuild(import.meta);const css$m = /* css */`
23879
+ installImportMetaCssBuild(import.meta);const css$n = /* css */`
23659
23880
  @layer navi {
23660
23881
  label {
23661
23882
  &[data-interactive] {
@@ -23687,7 +23908,7 @@ const reportDisabledToLabel = value => {
23687
23908
  };
23688
23909
  const LabelPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
23689
23910
  const Label = props => {
23690
- import.meta.css = [css$m, "@jsenv/navi/src/field/label.jsx"];
23911
+ import.meta.css = [css$n, "@jsenv/navi/src/field/label.jsx"];
23691
23912
  const {
23692
23913
  readOnly,
23693
23914
  disabled,
@@ -23721,7 +23942,7 @@ const Label = props => {
23721
23942
  });
23722
23943
  };
23723
23944
 
23724
- installImportMetaCssBuild(import.meta);const css$l = /* css */`
23945
+ installImportMetaCssBuild(import.meta);const css$m = /* css */`
23725
23946
  @layer navi {
23726
23947
  .navi_checkbox {
23727
23948
  --margin: 3px 3px 3px 4px;
@@ -24048,7 +24269,7 @@ installImportMetaCssBuild(import.meta);const css$l = /* css */`
24048
24269
  }
24049
24270
  `;
24050
24271
  const InputCheckbox = props => {
24051
- import.meta.css = [css$l, "@jsenv/navi/src/field/input_checkbox.jsx"];
24272
+ import.meta.css = [css$m, "@jsenv/navi/src/field/input_checkbox.jsx"];
24052
24273
  const {
24053
24274
  value = "on"
24054
24275
  } = props;
@@ -24462,7 +24683,7 @@ forwardRef((props, ref) => {
24462
24683
  });
24463
24684
  });
24464
24685
 
24465
- installImportMetaCssBuild(import.meta);const css$k = /* css */`
24686
+ installImportMetaCssBuild(import.meta);const css$l = /* css */`
24466
24687
  @layer navi {
24467
24688
  .navi_radio {
24468
24689
  --margin: 3px 3px 0 5px;
@@ -24755,7 +24976,7 @@ installImportMetaCssBuild(import.meta);const css$k = /* css */`
24755
24976
  }
24756
24977
  `;
24757
24978
  const InputRadio = props => {
24758
- import.meta.css = [css$k, "@jsenv/navi/src/field/input_radio.jsx"];
24979
+ import.meta.css = [css$l, "@jsenv/navi/src/field/input_radio.jsx"];
24759
24980
  const {
24760
24981
  value = "on"
24761
24982
  } = props;
@@ -25001,7 +25222,7 @@ const InputRadioWithAction = () => {
25001
25222
  throw new Error(`<Input type="radio" /> with an action make no sense. Use <RadioList action={something} /> instead`);
25002
25223
  };
25003
25224
 
25004
- installImportMetaCssBuild(import.meta);const css$j = /* css */`
25225
+ installImportMetaCssBuild(import.meta);const css$k = /* css */`
25005
25226
  @layer navi {
25006
25227
  .navi_input_range {
25007
25228
  --border-radius: 6px;
@@ -25210,7 +25431,7 @@ installImportMetaCssBuild(import.meta);const css$j = /* css */`
25210
25431
  }
25211
25432
  `;
25212
25433
  const InputRange = props => {
25213
- import.meta.css = [css$j, "@jsenv/navi/src/field/input_range.jsx"];
25434
+ import.meta.css = [css$k, "@jsenv/navi/src/field/input_range.jsx"];
25214
25435
  const uiStateController = useUIStateController(props, "input");
25215
25436
  const uiState = useUIState(uiStateController);
25216
25437
  const input = renderActionableComponent(props, {
@@ -25480,24 +25701,8 @@ const SearchSvg = () => jsx("svg", {
25480
25701
  })
25481
25702
  });
25482
25703
 
25483
- installImportMetaCssBuild(import.meta);/**
25484
- * Input component for all textual input types.
25485
- *
25486
- * Supports:
25487
- * - text (default)
25488
- * - password
25489
- * - hidden
25490
- * - email
25491
- * - url
25492
- * - search
25493
- * - tel
25494
- * - etc.
25495
- *
25496
- * For non-textual inputs, specialized components will be used:
25497
- * - <InputCheckbox /> for type="checkbox"
25498
- * - <InputRadio /> for type="radio"
25499
- */
25500
- const css$i = /* css */`
25704
+ installImportMetaCssBuild(import.meta);/* eslint-disable jsenv/no-unknown-params */
25705
+ const css$j = /* css */`
25501
25706
  @layer navi {
25502
25707
  .navi_input {
25503
25708
  --border-radius: 2px;
@@ -25709,7 +25914,7 @@ const css$i = /* css */`
25709
25914
  }
25710
25915
  `;
25711
25916
  const InputTextual = props => {
25712
- import.meta.css = [css$i, "@jsenv/navi/src/field/input_textual.jsx"];
25917
+ import.meta.css = [css$j, "@jsenv/navi/src/field/input_textual.jsx"];
25713
25918
  const uiStateController = useUIStateController(props, "input");
25714
25919
  const uiState = useUIState(uiStateController);
25715
25920
  const input = renderActionableComponent(props, {
@@ -25782,6 +25987,197 @@ Object.assign(PSEUDO_CLASSES, {
25782
25987
  const InputPseudoElements = ["::-navi-loader"];
25783
25988
  const InputChildPropSet = new Set([...fieldPropSet]);
25784
25989
  const InputTextualBasic = props => {
25990
+ if (props.combobox) {
25991
+ return jsx(InputTextualCombobox, {
25992
+ ...props
25993
+ });
25994
+ }
25995
+ return jsx(InputTextualPlain, {
25996
+ ...props
25997
+ });
25998
+ };
25999
+ const InputTextualCombobox = ({
26000
+ combobox,
26001
+ onInput,
26002
+ onFocus,
26003
+ onBlur,
26004
+ ...rest
26005
+ }) => {
26006
+ const defaultRef = useRef();
26007
+ const ref = rest.ref || defaultRef;
26008
+ const [comboboxOpen, setComboboxOpen] = useState(false);
26009
+ const comboboxOpenRef = useRef(false);
26010
+ comboboxOpenRef.current = comboboxOpen;
26011
+ const showPopover = e => {
26012
+ if (comboboxOpenRef.current) {
26013
+ return;
26014
+ }
26015
+ console.debug(`showPopover (e.type:${e.type})`);
26016
+ const popoverEl = document.getElementById(combobox);
26017
+ positionPopover();
26018
+ popoverEl.showPopover();
26019
+ comboboxOpenRef.current = true;
26020
+ setComboboxOpen(true);
26021
+ window.addEventListener("scroll", positionPopover, {
26022
+ capture: true,
26023
+ passive: true
26024
+ });
26025
+ };
26026
+ const hidePopover = e => {
26027
+ if (!comboboxOpenRef.current) {
26028
+ return;
26029
+ }
26030
+ console.debug(`hidePopover (e.type:${e.type})`);
26031
+ comboboxOpenRef.current = false;
26032
+ setComboboxOpen(false);
26033
+ window.removeEventListener("scroll", positionPopover, {
26034
+ capture: true
26035
+ });
26036
+ const popoverEl = document.getElementById(combobox);
26037
+ if (popoverEl) {
26038
+ popoverEl.dispatchEvent(new CustomEvent("combobox-clear"));
26039
+ popoverEl.hidePopover();
26040
+ }
26041
+ setComboboxOpen(false);
26042
+ };
26043
+ const positionPopover = () => {
26044
+ const input = ref.current;
26045
+ const rect = input.getBoundingClientRect();
26046
+ const popoverEl = document.getElementById(combobox);
26047
+ if (popoverEl) {
26048
+ popoverEl.style.top = `${rect.bottom + 2}px`;
26049
+ popoverEl.style.left = `${rect.left}px`;
26050
+ popoverEl.style.width = `${rect.width}px`;
26051
+ }
26052
+ };
26053
+ const dispatchToOptionList = customEvent => {
26054
+ const popoverEl = document.getElementById(combobox);
26055
+ if (!popoverEl) {
26056
+ return false;
26057
+ }
26058
+ popoverEl.dispatchEvent(customEvent);
26059
+ return customEvent.defaultPrevented;
26060
+ };
26061
+ useKeyboardShortcuts(ref, [{
26062
+ key: "arrowdown",
26063
+ description: "Open popover and highlight next option",
26064
+ handler: e => {
26065
+ showPopover(e);
26066
+ const popoverEl = document.getElementById(combobox);
26067
+ if (!popoverEl) {
26068
+ return false;
26069
+ }
26070
+ popoverEl.dispatchEvent(new CustomEvent("combobox-navigate", {
26071
+ detail: {
26072
+ direction: "down"
26073
+ }
26074
+ }));
26075
+ return true;
26076
+ }
26077
+ }, {
26078
+ key: "arrowup",
26079
+ description: "Open popover and highlight previous option",
26080
+ handler: e => {
26081
+ showPopover(e);
26082
+ return dispatchToOptionList(new CustomEvent("combobox-navigate", {
26083
+ detail: {
26084
+ direction: "up"
26085
+ }
26086
+ }));
26087
+ }
26088
+ }, {
26089
+ key: "home",
26090
+ description: "Highlight first option",
26091
+ handler: () => {
26092
+ if (!comboboxOpenRef.current) {
26093
+ return false;
26094
+ }
26095
+ return dispatchToOptionList(new CustomEvent("combobox-navigate", {
26096
+ detail: {
26097
+ direction: "first"
26098
+ }
26099
+ }));
26100
+ }
26101
+ }, {
26102
+ key: "end",
26103
+ description: "Highlight last option",
26104
+ handler: () => {
26105
+ if (!comboboxOpenRef.current) {
26106
+ return false;
26107
+ }
26108
+ return dispatchToOptionList(new CustomEvent("combobox-navigate", {
26109
+ detail: {
26110
+ direction: "last"
26111
+ }
26112
+ }));
26113
+ }
26114
+ }, {
26115
+ key: "enter",
26116
+ description: "Confirm highlighted option",
26117
+ handler: () => {
26118
+ if (!comboboxOpenRef.current) {
26119
+ return false;
26120
+ }
26121
+ return dispatchToOptionList(new CustomEvent("combobox-confirm", {
26122
+ cancelable: true
26123
+ }));
26124
+ }
26125
+ }, {
26126
+ key: "escape",
26127
+ description: "Close popover",
26128
+ handler: e => {
26129
+ if (!comboboxOpenRef.current) {
26130
+ return false;
26131
+ }
26132
+ hidePopover(e);
26133
+ return true;
26134
+ }
26135
+ }]);
26136
+ useEffect(() => {
26137
+ const inputEl = ref.current;
26138
+ const popoverEl = document.getElementById(combobox);
26139
+ if (!popoverEl) {
26140
+ return undefined;
26141
+ }
26142
+ const onSelected = e => {
26143
+ inputEl.value = e.detail.value;
26144
+ inputEl.dispatchEvent(new Event("input", {
26145
+ bubbles: true
26146
+ }));
26147
+ hidePopover(e);
26148
+ };
26149
+ popoverEl.addEventListener("combobox-selected", onSelected);
26150
+ return () => {
26151
+ popoverEl.removeEventListener("combobox-selected", onSelected);
26152
+ };
26153
+ }, [combobox]);
26154
+ return jsx(InputTextualPlain, {
26155
+ ref: ref,
26156
+ role: "combobox",
26157
+ autoComplete: "off",
26158
+ "aria-controls": combobox,
26159
+ "aria-haspopup": "listbox",
26160
+ "aria-expanded": comboboxOpen,
26161
+ "aria-autocomplete": "list",
26162
+ onnavi_callout_open: e => {
26163
+ hidePopover(e);
26164
+ },
26165
+ onFocus: e => {
26166
+ onFocus?.(e);
26167
+ showPopover(e);
26168
+ },
26169
+ onBlur: e => {
26170
+ onBlur?.(e);
26171
+ hidePopover(e);
26172
+ },
26173
+ onInput: e => {
26174
+ onInput?.(e);
26175
+ showPopover(e);
26176
+ },
26177
+ ...rest
26178
+ });
26179
+ };
26180
+ const InputTextualPlain = props => {
25785
26181
  const contextReadOnly = useContext(ReadOnlyContext);
25786
26182
  const contextDisabled = useContext(DisabledContext);
25787
26183
  const contextLoading = useContext(LoadingContext);
@@ -25819,6 +26215,9 @@ const InputTextualBasic = props => {
25819
26215
  const innerOnInput = useStableCallback(onInput);
25820
26216
  const autoId = useId();
25821
26217
  const innerId = rest.id || autoId;
26218
+ const {
26219
+ ...remainingRest
26220
+ } = remainingProps;
25822
26221
  const renderInput = inputProps => {
25823
26222
  return jsx(Box, {
25824
26223
  ...inputProps,
@@ -25887,7 +26286,7 @@ const InputTextualBasic = props => {
25887
26286
  baseChildPropSet: InputChildPropSet,
25888
26287
  "data-start-icon": innerIcon ? "" : undefined,
25889
26288
  "data-end-icon": cancelButton ? "" : undefined,
25890
- ...remainingProps,
26289
+ ...remainingRest,
25891
26290
  ref: undefined,
25892
26291
  children: [jsx(LoaderBackground, {
25893
26292
  loading: innerLoading,
@@ -26079,7 +26478,7 @@ installImportMetaCssBuild(import.meta);/**
26079
26478
  * This means an editable thing MUST have a parent with position relative that wraps the content and the eventual editable input
26080
26479
  *
26081
26480
  */
26082
- const css$h = /* css */`
26481
+ const css$i = /* css */`
26083
26482
  .navi_editable_wrapper {
26084
26483
  --inset-top: 0px;
26085
26484
  --inset-right: 0px;
@@ -26128,7 +26527,7 @@ const useEditionController = () => {
26128
26527
  };
26129
26528
  };
26130
26529
  const Editable = props => {
26131
- import.meta.css = [css$h, "@jsenv/navi/src/field/edition/editable.jsx"];
26530
+ import.meta.css = [css$i, "@jsenv/navi/src/field/edition/editable.jsx"];
26132
26531
  let {
26133
26532
  children,
26134
26533
  action,
@@ -26539,7 +26938,7 @@ const FormWithAction = props => {
26539
26938
  // form.dispatchEvent(customEvent);
26540
26939
  // };
26541
26940
 
26542
- installImportMetaCssBuild(import.meta);const css$g = /* css */`
26941
+ installImportMetaCssBuild(import.meta);const css$h = /* css */`
26543
26942
  .navi_group {
26544
26943
  --border-width: 1px;
26545
26944
 
@@ -26636,7 +27035,7 @@ const Group = ({
26636
27035
  vertical = row,
26637
27036
  ...props
26638
27037
  }) => {
26639
- import.meta.css = [css$g, "@jsenv/navi/src/field/group.jsx"];
27038
+ import.meta.css = [css$h, "@jsenv/navi/src/field/group.jsx"];
26640
27039
  if (typeof borderWidth === "string") {
26641
27040
  borderWidth = parseFloat(borderWidth);
26642
27041
  }
@@ -26654,6 +27053,386 @@ const Group = ({
26654
27053
  });
26655
27054
  };
26656
27055
 
27056
+ const createItemTracker = () => {
27057
+ const ItemTrackerContext = createContext();
27058
+ const useItemTrackerProvider = () => {
27059
+ const itemsRef = useRef([]);
27060
+ const items = itemsRef.current;
27061
+ const itemCountRef = useRef(0);
27062
+ const tracker = useMemo(() => {
27063
+ const ItemTrackerProvider = ({
27064
+ children
27065
+ }) => {
27066
+ // Reset on each render to start fresh
27067
+ tracker.reset();
27068
+ return jsx(ItemTrackerContext.Provider, {
27069
+ value: tracker,
27070
+ children: children
27071
+ });
27072
+ };
27073
+ ItemTrackerProvider.items = items;
27074
+ return {
27075
+ ItemTrackerProvider,
27076
+ items,
27077
+ registerItem: data => {
27078
+ const index = itemCountRef.current++;
27079
+ items[index] = data;
27080
+ return index;
27081
+ },
27082
+ getItem: index => {
27083
+ return items[index];
27084
+ },
27085
+ getAllItems: () => {
27086
+ return items;
27087
+ },
27088
+ reset: () => {
27089
+ items.length = 0;
27090
+ itemCountRef.current = 0;
27091
+ }
27092
+ };
27093
+ }, []);
27094
+ return tracker.ItemTrackerProvider;
27095
+ };
27096
+ const useTrackItem = data => {
27097
+ const tracker = useContext(ItemTrackerContext);
27098
+ if (!tracker) {
27099
+ throw new Error("useTrackItem must be used within SimpleItemTrackerProvider");
27100
+ }
27101
+ return tracker.registerItem(data);
27102
+ };
27103
+ const useTrackedItem = index => {
27104
+ const trackedItems = useTrackedItems();
27105
+ const item = trackedItems[index];
27106
+ return item;
27107
+ };
27108
+ const useTrackedItems = () => {
27109
+ const tracker = useContext(ItemTrackerContext);
27110
+ if (!tracker) {
27111
+ throw new Error("useTrackedItems must be used within SimpleItemTrackerProvider");
27112
+ }
27113
+ return tracker.items;
27114
+ };
27115
+ return [useItemTrackerProvider, useTrackItem, useTrackedItem, useTrackedItems];
27116
+ };
27117
+
27118
+ installImportMetaCssBuild(import.meta);const [useOptionItemTrackerProvider, useTrackOption] = createItemTracker();
27119
+
27120
+ /**
27121
+ * OptionList + Option: a composable accessible listbox.
27122
+ *
27123
+ * Usage:
27124
+ * <OptionList id="my-list" value={selected} onChange={setSelected}>
27125
+ * <Option value="a">Option A</Option>
27126
+ * <Option value="b">Option B</Option>
27127
+ * </OptionList>
27128
+ *
27129
+ * CSS vars on .navi_option_list:
27130
+ * --border-radius, --border-width, --border-color, --background-color, --max-height
27131
+ *
27132
+ * CSS vars on .navi_option:
27133
+ * --padding, --color, --background-color, --font-weight
27134
+ * --color-hover, --background-color-hover
27135
+ * --color-highlighted, --background-color-highlighted
27136
+ * --color-selected, --background-color-selected, --font-weight-selected
27137
+ * --color-highlighted-selected, --background-color-highlighted-selected
27138
+ */
27139
+
27140
+ const css$g = /* css */`
27141
+ @layer navi {
27142
+ .navi_option_list {
27143
+ --border-radius: 4px;
27144
+ --border-width: 1px;
27145
+ --border-color: light-dark(#ccc, #555);
27146
+ --background-color: light-dark(#fff, #1e1e1e);
27147
+ --max-height: 220px;
27148
+ }
27149
+ .navi_option {
27150
+ --padding: 8px 12px;
27151
+ --color: inherit;
27152
+ --background-color: transparent;
27153
+ --font-weight: inherit;
27154
+
27155
+ /* Hover (mouse) */
27156
+ --color-hover: var(--color);
27157
+ --background-color-hover: light-dark(#f5f5f5, #2a2a2a);
27158
+
27159
+ /* Highlighted (keyboard navigation cursor) */
27160
+ --color-highlighted: var(--color);
27161
+ --background-color-highlighted: light-dark(#e8f0fe, #1c3a6e);
27162
+
27163
+ /* Selected */
27164
+ --color-selected: light-dark(#1a73e8, #7baaf7);
27165
+ --background-color-selected: light-dark(#e8f0fe, #1c3a6e);
27166
+ --font-weight-selected: 500;
27167
+
27168
+ /* Highlighted + selected */
27169
+ --color-highlighted-selected: var(--color-selected);
27170
+ --background-color-highlighted-selected: light-dark(#d2e3fc, #174ea6);
27171
+ }
27172
+ }
27173
+
27174
+ .navi_option_list {
27175
+ --x-border-radius: var(--border-radius);
27176
+ --x-border-width: var(--border-width);
27177
+ --x-border-color: var(--border-color);
27178
+ --x-background-color: var(--background-color);
27179
+ box-sizing: border-box;
27180
+ max-height: var(--max-height);
27181
+
27182
+ margin: 0;
27183
+ padding: 0;
27184
+ list-style: none;
27185
+ background-color: var(--x-background-color);
27186
+ border: var(--x-border-width) solid var(--x-border-color);
27187
+ border-radius: var(--x-border-radius);
27188
+ outline: none;
27189
+ overflow-y: auto;
27190
+
27191
+ /* Popover reset — browser adds border, background, padding, margin by default */
27192
+ &[popover] {
27193
+ position: fixed;
27194
+ inset: unset;
27195
+ margin: 0;
27196
+ padding: 0;
27197
+ border: none;
27198
+ }
27199
+ }
27200
+ .navi_option {
27201
+ --x-color: var(--color);
27202
+ --x-background-color: var(--background-color);
27203
+ --x-font-weight: var(--font-weight);
27204
+
27205
+ padding: var(--padding);
27206
+ color: var(--x-color);
27207
+ font-weight: var(--x-font-weight);
27208
+ background-color: var(--x-background-color);
27209
+ cursor: pointer;
27210
+ user-select: none;
27211
+
27212
+ &:hover {
27213
+ --x-color: var(--color-hover);
27214
+ --x-background-color: var(--background-color-hover);
27215
+ }
27216
+
27217
+ &[data-highlighted] {
27218
+ --x-color: var(--color-highlighted);
27219
+ --x-background-color: var(--background-color-highlighted);
27220
+ }
27221
+
27222
+ &[data-selected] {
27223
+ --x-color: var(--color-selected);
27224
+ --x-background-color: var(--background-color-selected);
27225
+ --x-font-weight: var(--font-weight-selected);
27226
+ }
27227
+
27228
+ &[data-highlighted][data-selected] {
27229
+ --x-color: var(--color-highlighted-selected);
27230
+ --x-background-color: var(--background-color-highlighted-selected);
27231
+ }
27232
+ }
27233
+ `;
27234
+
27235
+ /**
27236
+ * Context OptionList provides downward to its Option children.
27237
+ */
27238
+ const OptionListContext = createContext(null);
27239
+ const OptionList = ({
27240
+ popover,
27241
+ onChange: onChangeProp,
27242
+ children,
27243
+ ...rest
27244
+ }) => {
27245
+ import.meta.css = [css$g, "@jsenv/navi/src/field/option_list.jsx"];
27246
+ const ItemTrackerProvider = useOptionItemTrackerProvider();
27247
+ const [highlightedValue, setHighlightedValue] = useState(null);
27248
+ const highlightedValueRef = useRef(null);
27249
+ highlightedValueRef.current = highlightedValue;
27250
+ const ownId = useId();
27251
+ const id = rest.id ?? ownId;
27252
+ const listRef = useRef(null);
27253
+ const effectiveOnChange = popover ? value => {
27254
+ onChangeProp?.(value);
27255
+ listRef.current?.dispatchEvent(new CustomEvent("combobox-selected", {
27256
+ detail: {
27257
+ value
27258
+ },
27259
+ bubbles: true
27260
+ }));
27261
+ } : onChangeProp;
27262
+ const onChangeRef = useRef(effectiveOnChange);
27263
+ onChangeRef.current = effectiveOnChange;
27264
+ const navigate = direction => {
27265
+ const values = ItemTrackerProvider.items.filter(item => !item.hidden).map(item => item.value);
27266
+ if (values.length === 0) {
27267
+ return false;
27268
+ }
27269
+ const current = highlightedValueRef.current;
27270
+ if (direction === "down") {
27271
+ const idx = current === null ? -1 : values.indexOf(current);
27272
+ setHighlightedValue(values[idx < values.length - 1 ? idx + 1 : idx]);
27273
+ } else if (direction === "up") {
27274
+ const idx = current === null ? -1 : values.indexOf(current);
27275
+ setHighlightedValue(values[idx > 0 ? idx - 1 : 0]);
27276
+ } else if (direction === "first") {
27277
+ setHighlightedValue(values[0]);
27278
+ } else if (direction === "last") {
27279
+ setHighlightedValue(values[values.length - 1]);
27280
+ }
27281
+ return true;
27282
+ };
27283
+
27284
+ // Listen for commands dispatched by a linked Input (combobox mode)
27285
+ const noopRef = useRef(null);
27286
+ useEffect(() => {
27287
+ if (!popover || !listRef.current) {
27288
+ return undefined;
27289
+ }
27290
+ const el = listRef.current;
27291
+ const onNavigate = e => {
27292
+ navigate(e.detail.direction);
27293
+ };
27294
+ const onConfirm = e => {
27295
+ const current = highlightedValueRef.current;
27296
+ if (current !== null) {
27297
+ onChangeRef.current?.(current);
27298
+ e.preventDefault();
27299
+ }
27300
+ };
27301
+ const onClear = () => {
27302
+ setHighlightedValue(null);
27303
+ };
27304
+ el.addEventListener("combobox-navigate", onNavigate);
27305
+ el.addEventListener("combobox-confirm", onConfirm);
27306
+ el.addEventListener("combobox-clear", onClear);
27307
+ return () => {
27308
+ el.removeEventListener("combobox-navigate", onNavigate);
27309
+ el.removeEventListener("combobox-confirm", onConfirm);
27310
+ el.removeEventListener("combobox-clear", onClear);
27311
+ };
27312
+ }, [popover]);
27313
+ useKeyboardShortcuts(popover ? noopRef : listRef, [{
27314
+ key: "arrowdown",
27315
+ description: "Highlight next option",
27316
+ handler: () => navigate("down")
27317
+ }, {
27318
+ key: "arrowup",
27319
+ description: "Highlight previous option",
27320
+ handler: () => navigate("up")
27321
+ }, {
27322
+ key: "home",
27323
+ description: "Highlight first option",
27324
+ handler: () => navigate("first")
27325
+ }, {
27326
+ key: "end",
27327
+ description: "Highlight last option",
27328
+ handler: () => navigate("last")
27329
+ }, {
27330
+ key: "enter",
27331
+ description: "Select highlighted option",
27332
+ handler: () => {
27333
+ const current = highlightedValueRef.current;
27334
+ if (current === null) {
27335
+ return false;
27336
+ }
27337
+ onChangeRef.current?.(current);
27338
+ return true;
27339
+ }
27340
+ }, {
27341
+ key: "escape",
27342
+ description: "Clear highlighted option",
27343
+ handler: () => {
27344
+ setHighlightedValue(null);
27345
+ return true;
27346
+ }
27347
+ }]);
27348
+ const optionListContext = {
27349
+ highlightedValue,
27350
+ setHighlightedValue,
27351
+ onSelect: effectiveOnChange
27352
+ };
27353
+ return jsx(Box, {
27354
+ as: "ul",
27355
+ ref: listRef,
27356
+ id: id,
27357
+ role: "listbox",
27358
+ tabIndex: popover ? -1 : 0,
27359
+ popover: popover ? "manual" : undefined,
27360
+ ...rest,
27361
+ baseClassName: "navi_option_list",
27362
+ children: jsx(OptionListContext.Provider, {
27363
+ value: optionListContext,
27364
+ children: jsx(ItemTrackerProvider, {
27365
+ children: children
27366
+ })
27367
+ })
27368
+ });
27369
+ };
27370
+ const OPTION_PSEUDO_CLASSES = [":-navi-highlighted", ":-navi-selected"];
27371
+ const Option = ({
27372
+ value,
27373
+ selected,
27374
+ hidden,
27375
+ children,
27376
+ ...rest
27377
+ }) => {
27378
+ import.meta.css = [css$g, "@jsenv/navi/src/field/option_list.jsx"];
27379
+ const optionId = useId();
27380
+ const id = rest.id || optionId;
27381
+ useTrackOption({
27382
+ value,
27383
+ optionId: id,
27384
+ hidden
27385
+ });
27386
+ const {
27387
+ highlightedValue,
27388
+ setHighlightedValue,
27389
+ onSelect
27390
+ } = useContext(OptionListContext);
27391
+ const isHighlighted = highlightedValue === value;
27392
+ const optionRef = useRef(null);
27393
+ useEffect(() => {
27394
+ const optionEl = optionRef.current;
27395
+ if (isHighlighted && optionEl) {
27396
+ optionEl.scrollIntoView({
27397
+ block: "nearest"
27398
+ });
27399
+ }
27400
+ }, [isHighlighted]);
27401
+ return jsx(Box, {
27402
+ as: "li",
27403
+ ref: optionRef,
27404
+ baseClassName: "navi_option",
27405
+ id: optionId,
27406
+ role: "option",
27407
+ "aria-selected": selected,
27408
+ "aria-hidden": hidden ? true : undefined,
27409
+ hidden: hidden,
27410
+ basePseudoState: {
27411
+ ":-navi-highlighted": isHighlighted,
27412
+ ":-navi-selected": selected
27413
+ },
27414
+ pseudoClasses: OPTION_PSEUDO_CLASSES,
27415
+ onMouseEnter: () => {
27416
+ if (!hidden) {
27417
+ setHighlightedValue(value);
27418
+ }
27419
+ },
27420
+ onMouseLeave: () => {
27421
+ if (!hidden) {
27422
+ setHighlightedValue(null);
27423
+ }
27424
+ },
27425
+ onMouseDown: e => {
27426
+ if (hidden || e.button !== 0) {
27427
+ return;
27428
+ }
27429
+ onSelect?.(value);
27430
+ },
27431
+ ...rest,
27432
+ children: children
27433
+ });
27434
+ };
27435
+
26657
27436
  const RadioList = props => {
26658
27437
  const uiStateController = useUIGroupStateController(props, "radio_list", {
26659
27438
  childComponentType: "radio",
@@ -27124,6 +27903,131 @@ const filterTableSelection = (selection, predicate) => {
27124
27903
  return matching;
27125
27904
  };
27126
27905
 
27906
+ // https://github.com/reach/reach-ui/tree/b3d94d22811db6b5c0f272b9a7e2e3c1bb4699ae/packages/descendants
27907
+ // https://github.com/pacocoursey/use-descendants/tree/master
27908
+
27909
+ const createIsolatedItemTracker = () => {
27910
+ // Producer contexts (ref-based, no re-renders)
27911
+ const ProducerTrackerContext = createContext();
27912
+ const ProducerItemCountRefContext = createContext();
27913
+ const ProducerListRenderIdContext = createContext();
27914
+
27915
+ // Consumer contexts (state-based, re-renders)
27916
+ const ConsumerItemsContext = createContext();
27917
+ const useIsolatedItemTrackerProvider = () => {
27918
+ const itemsRef = useRef([]);
27919
+ const items = itemsRef.current;
27920
+ const itemCountRef = useRef();
27921
+ const itemTracker = useMemo(() => {
27922
+ // Snapshot taken by FlushSentinel after all producer children rendered.
27923
+ // Consumers read from this — always up-to-date within the same render pass.
27924
+ const itemsSnapshotRef = {
27925
+ current: items
27926
+ };
27927
+ const registerItem = (index, value) => {
27928
+ const hasValue = index in items;
27929
+ if (hasValue) {
27930
+ const currentValue = items[index];
27931
+ if (compareTwoJsValues(currentValue, value)) {
27932
+ return;
27933
+ }
27934
+ }
27935
+ items[index] = value;
27936
+ };
27937
+ const getProducerItem = itemIndex => {
27938
+ return items[itemIndex];
27939
+ };
27940
+ const ItemProducerProvider = ({
27941
+ children
27942
+ }) => {
27943
+ items.length = 0;
27944
+ itemCountRef.current = 0;
27945
+ const listRenderId = {};
27946
+ return jsx(ProducerItemCountRefContext.Provider, {
27947
+ value: itemCountRef,
27948
+ children: jsx(ProducerListRenderIdContext.Provider, {
27949
+ value: listRenderId,
27950
+ children: jsxs(ProducerTrackerContext.Provider, {
27951
+ value: itemTracker,
27952
+ children: [children, jsx(FlushSentinel, {})]
27953
+ })
27954
+ })
27955
+ });
27956
+ };
27957
+
27958
+ // Renders after all producer children (e.g. <Col>) have registered their
27959
+ // items. Taking a snapshot here guarantees the consumer sees the correct
27960
+ // item list within the same render pass, without any heuristic.
27961
+ const FlushSentinel = () => {
27962
+ itemsSnapshotRef.current = items;
27963
+ return null;
27964
+ };
27965
+ const ItemConsumerProvider = ({
27966
+ children
27967
+ }) => {
27968
+ // FlushSentinel (last child of ItemProducerProvider) already set
27969
+ // itemsSnapshotRef.current to the up-to-date items array before any
27970
+ // consumer rendered. Reading from the snapshot is always correct.
27971
+ return jsx(ConsumerItemsContext.Provider, {
27972
+ value: itemsSnapshotRef.current,
27973
+ children: children
27974
+ });
27975
+ };
27976
+ return {
27977
+ registerItem,
27978
+ getProducerItem,
27979
+ ItemProducerProvider,
27980
+ ItemConsumerProvider
27981
+ };
27982
+ }, []);
27983
+ const {
27984
+ ItemProducerProvider,
27985
+ ItemConsumerProvider
27986
+ } = itemTracker;
27987
+ return [ItemProducerProvider, ItemConsumerProvider, items];
27988
+ };
27989
+
27990
+ // Hook for producers to register items (ref-based, no re-renders)
27991
+ const useTrackIsolatedItem = data => {
27992
+ const listRenderId = useContext(ProducerListRenderIdContext);
27993
+ const itemCountRef = useContext(ProducerItemCountRefContext);
27994
+ const itemTracker = useContext(ProducerTrackerContext);
27995
+ const listRenderIdRef = useRef();
27996
+ const itemIndexRef = useRef();
27997
+ const dataRef = useRef();
27998
+ const prevListRenderId = listRenderIdRef.current;
27999
+ if (prevListRenderId === listRenderId) {
28000
+ const itemIndex = itemIndexRef.current;
28001
+ itemTracker.registerItem(itemIndex, data);
28002
+ dataRef.current = data;
28003
+ return itemIndex;
28004
+ }
28005
+ listRenderIdRef.current = listRenderId;
28006
+ const itemCount = itemCountRef.current;
28007
+ const itemIndex = itemCount;
28008
+ itemCountRef.current = itemIndex + 1;
28009
+ itemIndexRef.current = itemIndex;
28010
+ dataRef.current = data;
28011
+ itemTracker.registerItem(itemIndex, data);
28012
+ return itemIndex;
28013
+ };
28014
+ const useTrackedIsolatedItem = itemIndex => {
28015
+ const items = useTrackedIsolatedItems();
28016
+ const item = items[itemIndex];
28017
+ return item;
28018
+ };
28019
+
28020
+ // Hooks for consumers to read items (state-based, re-renders)
28021
+ const useTrackedIsolatedItems = () => {
28022
+ const consumerItems = useContext(ConsumerItemsContext);
28023
+ if (!consumerItems) {
28024
+ throw new Error("useTrackedIsolatedItems must be used within <ItemConsumerProvider />");
28025
+ }
28026
+ return consumerItems;
28027
+ };
28028
+ return [useIsolatedItemTrackerProvider, useTrackIsolatedItem, useTrackedIsolatedItem, useTrackedIsolatedItems];
28029
+ };
28030
+
27127
28031
  const Z_INDEX_EDITING = 1; /* To go above neighbours, but should not be too big to stay under the sticky cells */
27128
28032
 
27129
28033
  /* needed because cell uses position:relative, sticky must win even if before in DOM order */
@@ -27580,193 +28484,6 @@ const createTableAttributeSync = (table, tableClone) => {
27580
28484
  return observer;
27581
28485
  };
27582
28486
 
27583
- // https://github.com/reach/reach-ui/tree/b3d94d22811db6b5c0f272b9a7e2e3c1bb4699ae/packages/descendants
27584
- // https://github.com/pacocoursey/use-descendants/tree/master
27585
-
27586
- const createIsolatedItemTracker = () => {
27587
- // Producer contexts (ref-based, no re-renders)
27588
- const ProducerTrackerContext = createContext();
27589
- const ProducerItemCountRefContext = createContext();
27590
- const ProducerListRenderIdContext = createContext();
27591
-
27592
- // Consumer contexts (state-based, re-renders)
27593
- const ConsumerItemsContext = createContext();
27594
- const useIsolatedItemTrackerProvider = () => {
27595
- const itemsRef = useRef([]);
27596
- const items = itemsRef.current;
27597
- const itemCountRef = useRef();
27598
- const itemTracker = useMemo(() => {
27599
- // Snapshot taken by FlushSentinel after all producer children rendered.
27600
- // Consumers read from this — always up-to-date within the same render pass.
27601
- const itemsSnapshotRef = {
27602
- current: items
27603
- };
27604
- const registerItem = (index, value) => {
27605
- const hasValue = index in items;
27606
- if (hasValue) {
27607
- const currentValue = items[index];
27608
- if (compareTwoJsValues(currentValue, value)) {
27609
- return;
27610
- }
27611
- }
27612
- items[index] = value;
27613
- };
27614
- const getProducerItem = itemIndex => {
27615
- return items[itemIndex];
27616
- };
27617
- const ItemProducerProvider = ({
27618
- children
27619
- }) => {
27620
- items.length = 0;
27621
- itemCountRef.current = 0;
27622
- const listRenderId = {};
27623
- return jsx(ProducerItemCountRefContext.Provider, {
27624
- value: itemCountRef,
27625
- children: jsx(ProducerListRenderIdContext.Provider, {
27626
- value: listRenderId,
27627
- children: jsxs(ProducerTrackerContext.Provider, {
27628
- value: itemTracker,
27629
- children: [children, jsx(FlushSentinel, {})]
27630
- })
27631
- })
27632
- });
27633
- };
27634
-
27635
- // Renders after all producer children (e.g. <Col>) have registered their
27636
- // items. Taking a snapshot here guarantees the consumer sees the correct
27637
- // item list within the same render pass, without any heuristic.
27638
- const FlushSentinel = () => {
27639
- itemsSnapshotRef.current = items;
27640
- return null;
27641
- };
27642
- const ItemConsumerProvider = ({
27643
- children
27644
- }) => {
27645
- // FlushSentinel (last child of ItemProducerProvider) already set
27646
- // itemsSnapshotRef.current to the up-to-date items array before any
27647
- // consumer rendered. Reading from the snapshot is always correct.
27648
- return jsx(ConsumerItemsContext.Provider, {
27649
- value: itemsSnapshotRef.current,
27650
- children: children
27651
- });
27652
- };
27653
- return {
27654
- registerItem,
27655
- getProducerItem,
27656
- ItemProducerProvider,
27657
- ItemConsumerProvider
27658
- };
27659
- }, []);
27660
- const {
27661
- ItemProducerProvider,
27662
- ItemConsumerProvider
27663
- } = itemTracker;
27664
- return [ItemProducerProvider, ItemConsumerProvider, items];
27665
- };
27666
-
27667
- // Hook for producers to register items (ref-based, no re-renders)
27668
- const useTrackIsolatedItem = data => {
27669
- const listRenderId = useContext(ProducerListRenderIdContext);
27670
- const itemCountRef = useContext(ProducerItemCountRefContext);
27671
- const itemTracker = useContext(ProducerTrackerContext);
27672
- const listRenderIdRef = useRef();
27673
- const itemIndexRef = useRef();
27674
- const dataRef = useRef();
27675
- const prevListRenderId = listRenderIdRef.current;
27676
- if (prevListRenderId === listRenderId) {
27677
- const itemIndex = itemIndexRef.current;
27678
- itemTracker.registerItem(itemIndex, data);
27679
- dataRef.current = data;
27680
- return itemIndex;
27681
- }
27682
- listRenderIdRef.current = listRenderId;
27683
- const itemCount = itemCountRef.current;
27684
- const itemIndex = itemCount;
27685
- itemCountRef.current = itemIndex + 1;
27686
- itemIndexRef.current = itemIndex;
27687
- dataRef.current = data;
27688
- itemTracker.registerItem(itemIndex, data);
27689
- return itemIndex;
27690
- };
27691
- const useTrackedIsolatedItem = itemIndex => {
27692
- const items = useTrackedIsolatedItems();
27693
- const item = items[itemIndex];
27694
- return item;
27695
- };
27696
-
27697
- // Hooks for consumers to read items (state-based, re-renders)
27698
- const useTrackedIsolatedItems = () => {
27699
- const consumerItems = useContext(ConsumerItemsContext);
27700
- if (!consumerItems) {
27701
- throw new Error("useTrackedIsolatedItems must be used within <ItemConsumerProvider />");
27702
- }
27703
- return consumerItems;
27704
- };
27705
- return [useIsolatedItemTrackerProvider, useTrackIsolatedItem, useTrackedIsolatedItem, useTrackedIsolatedItems];
27706
- };
27707
-
27708
- const createItemTracker = () => {
27709
- const ItemTrackerContext = createContext();
27710
- const useItemTrackerProvider = () => {
27711
- const itemsRef = useRef([]);
27712
- const items = itemsRef.current;
27713
- const itemCountRef = useRef(0);
27714
- const tracker = useMemo(() => {
27715
- const ItemTrackerProvider = ({
27716
- children
27717
- }) => {
27718
- // Reset on each render to start fresh
27719
- tracker.reset();
27720
- return jsx(ItemTrackerContext.Provider, {
27721
- value: tracker,
27722
- children: children
27723
- });
27724
- };
27725
- ItemTrackerProvider.items = items;
27726
- return {
27727
- ItemTrackerProvider,
27728
- items,
27729
- registerItem: data => {
27730
- const index = itemCountRef.current++;
27731
- items[index] = data;
27732
- return index;
27733
- },
27734
- getItem: index => {
27735
- return items[index];
27736
- },
27737
- getAllItems: () => {
27738
- return items;
27739
- },
27740
- reset: () => {
27741
- items.length = 0;
27742
- itemCountRef.current = 0;
27743
- }
27744
- };
27745
- }, []);
27746
- return tracker.ItemTrackerProvider;
27747
- };
27748
- const useTrackItem = data => {
27749
- const tracker = useContext(ItemTrackerContext);
27750
- if (!tracker) {
27751
- throw new Error("useTrackItem must be used within SimpleItemTrackerProvider");
27752
- }
27753
- return tracker.registerItem(data);
27754
- };
27755
- const useTrackedItem = index => {
27756
- const trackedItems = useTrackedItems();
27757
- const item = trackedItems[index];
27758
- return item;
27759
- };
27760
- const useTrackedItems = () => {
27761
- const tracker = useContext(ItemTrackerContext);
27762
- if (!tracker) {
27763
- throw new Error("useTrackedItems must be used within SimpleItemTrackerProvider");
27764
- }
27765
- return tracker.items;
27766
- };
27767
- return [useItemTrackerProvider, useTrackItem, useTrackedItem, useTrackedItems];
27768
- };
27769
-
27770
28487
  const TableSizeContext = createContext();
27771
28488
 
27772
28489
  const useTableSizeContextValue = ({
@@ -32685,5 +33402,5 @@ const UserSvg = () => jsx("svg", {
32685
33402
  })
32686
33403
  });
32687
33404
 
32688
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, Checkbox, CheckboxList, CloseSvg, Code, Col, Colgroup, ConstructionSvg, Details, DialogLayout, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, Loading, MessageBox, Meter, Nav, Paragraph, 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, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createIntl, createRequestCanceller, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, filterTableSelection, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDarkBackgroundAttribute, useDependenciesDiff, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState$1 as useNavState, useOrderedColumns, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSidePanelClose, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage };
33405
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, Checkbox, CheckboxList, CloseSvg, Code, Col, Colgroup, ConstructionSvg, Details, DialogLayout, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, Loading, MessageBox, Meter, Nav, Option, OptionList, Paragraph, 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, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createIntl, createRequestCanceller, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, filterTableSelection, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDarkBackgroundAttribute, useDependenciesDiff, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState$1 as useNavState, useOrderedColumns, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSidePanelClose, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage };
32689
33406
  //# sourceMappingURL=jsenv_navi.js.map