@jsenv/navi 0.18.15 → 0.18.17

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,8 @@ import { isValidElement, createContext, toChildArray, render, createRef, cloneEl
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";
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, hasCSSSizeUnit, resolveCSSSize, activeElementSignal, canInterceptKeys, initFocusGroup, elementIsFocusable, pickLightOrDark, resolveColorLuminance, dragAfterThreshold, getScrollContainer, 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, hasCSSSizeUnit, resolveCSSSize, activeElementSignal, canInterceptKeys, initFocusGroup, elementIsFocusable, contrastColor, resolveColorLuminance, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
+ export { contrastColor } from "@jsenv/dom";
7
8
  import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
8
9
  import { createValidity } from "@jsenv/validity";
9
10
  import { createPortal, forwardRef } from "preact/compat";
@@ -6472,6 +6473,7 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
6472
6473
  const PSEUDO_CLASSES_DEFAULT = [];
6473
6474
  const PSEUDO_ELEMENTS_DEFAULT = [];
6474
6475
  const STYLE_CSS_VARS_DEFAULT = {};
6476
+ const PROPS_CSS_VARS_DEFAULT = {};
6475
6477
  const Box = props => {
6476
6478
  const {
6477
6479
  as = "div",
@@ -6481,6 +6483,7 @@ const Box = props => {
6481
6483
  // style management
6482
6484
  style,
6483
6485
  styleCSSVars = STYLE_CSS_VARS_DEFAULT,
6486
+ propsCSSVars = PROPS_CSS_VARS_DEFAULT,
6484
6487
  basePseudoState,
6485
6488
  pseudoState,
6486
6489
  // for demo purposes it's possible to control pseudo state from props
@@ -6614,16 +6617,20 @@ const Box = props => {
6614
6617
  let boxPseudoNamedStyles = PSEUDO_NAMED_STYLES_DEFAULT;
6615
6618
  const shouldForwardAllToChild = visualSelector && pseudoStateSelector;
6616
6619
  const addStyle = (value, name, styleContext, stylesTarget, context) => {
6617
- styleDeps.push(name, value); // impact box style -> add to deps
6618
- const cssVar = styleContext.styleCSSVars[name];
6619
6620
  const mergedValue = prepareStyleValue(stylesTarget[name], value, name, styleContext, context);
6621
+ const cssVar = styleContext.styleCSSVars[name];
6620
6622
  if (cssVar) {
6621
- stylesTarget[cssVar] = mergedValue;
6623
+ addCSSVar(mergedValue, cssVar, stylesTarget);
6622
6624
  return true;
6623
6625
  }
6626
+ styleDeps.push(name, value); // impact box style -> add to deps
6624
6627
  stylesTarget[name] = mergedValue;
6625
6628
  return false;
6626
6629
  };
6630
+ const addCSSVar = (value, name, stylesTarget) => {
6631
+ styleDeps.push(name, value); // impact box style -> add to deps
6632
+ stylesTarget[name] = value;
6633
+ };
6627
6634
  const addStyleMaybeForwarding = (value, name, styleContext, stylesTarget, context, visualChildPropStrategy) => {
6628
6635
  if (!visualChildPropStrategy) {
6629
6636
  addStyle(value, name, styleContext, stylesTarget, context);
@@ -6688,6 +6695,11 @@ const Box = props => {
6688
6695
  addStyle(value, name, styleContext, boxStylesTarget, context);
6689
6696
  return;
6690
6697
  }
6698
+ const propCssVar = propsCSSVars[name];
6699
+ if (propCssVar) {
6700
+ addCSSVar(value, propCssVar, boxStylesTarget);
6701
+ return;
6702
+ }
6691
6703
  const isPseudoStyle = styleOrigin === "pseudo_style";
6692
6704
  if (isStyleProp(name)) {
6693
6705
  // it's a style prop, we need first to check if we have css var to handle them
@@ -16350,9 +16362,9 @@ const listenInputValue = (
16350
16362
  let timeout;
16351
16363
  let debounceTimeout;
16352
16364
 
16353
- const onAsyncEvent = (e) => {
16365
+ const onAsyncEvent = (e, options) => {
16354
16366
  timeout = setTimeout(() => {
16355
- onEvent(e);
16367
+ onEvent(e, options);
16356
16368
  }, 0);
16357
16369
  };
16358
16370
 
@@ -16431,13 +16443,16 @@ const listenInputValue = (
16431
16443
  input.removeEventListener("focus", onEvent);
16432
16444
  });
16433
16445
 
16434
- // "navi_delete_content" behaves like an async event
16435
- // a bit like form reset because
16436
- // our action will be updated async after the component re-renders
16437
- // and we need to wait that to happen to properly call action with the right value
16438
- input.addEventListener("navi_delete_content", onAsyncEvent);
16446
+ const onNaviDeleteContent = (e) => {
16447
+ // "navi_delete_content" behaves like an async event
16448
+ // a bit like form reset because
16449
+ // our action will be updated async after the component re-renders
16450
+ // and we need to wait that to happen to properly call action with the right value
16451
+ onAsyncEvent(e, { skipDebounce: true });
16452
+ };
16453
+ input.addEventListener("navi_delete_content", onNaviDeleteContent);
16439
16454
  addTeardown(() => {
16440
- input.removeEventListener("navi_delete_content", onAsyncEvent);
16455
+ input.removeEventListener("navi_delete_content", onNaviDeleteContent);
16441
16456
  });
16442
16457
  return () => {
16443
16458
  teardown();
@@ -22104,6 +22119,103 @@ const useConstraintValidityState = (ref) => {
22104
22119
  return constraintValidityState;
22105
22120
  };
22106
22121
 
22122
+ /**
22123
+ * Toggles a `data-dark-background` attribute on the referenced element based on its
22124
+ * computed background color. Pair it with a CSS variable to get automatic
22125
+ * light/dark text without hard-coding colors:
22126
+ *
22127
+ * ```css
22128
+ * .my-element {
22129
+ * --color-contrasting: black;
22130
+ * &[data-dark-background] {
22131
+ * --color-contrasting: white;
22132
+ * }
22133
+ * color: var(--color-contrasting);
22134
+ * }
22135
+ * ```
22136
+ *
22137
+ * - `data-dark-background` is **set** when the background is dark enough that white text
22138
+ * provides better (or equal) contrast.
22139
+ * - `data-dark-background` is **absent** when black text is the better choice.
22140
+ *
22141
+ * @param {import("preact").RefObject} ref - Ref to the element that receives
22142
+ * the `data-dark-background` attribute and is also passed to `contrastColor` for
22143
+ * resolving CSS variables.
22144
+ * @param {object} [options]
22145
+ * @param {string} [options.backgroundElementSelector] - CSS selector relative
22146
+ * to `ref.current` pointing to a child element whose `background-color`
22147
+ * should be tested instead of the element itself. Useful when the element
22148
+ * has a transparent background but contains a coloured child (e.g. a fill
22149
+ * bar inside a track).
22150
+ */
22151
+
22152
+ const useDarkBackgroundAttribute = (
22153
+ ref,
22154
+ deps = [],
22155
+ {
22156
+ backgroundElementSelector,
22157
+ attributeName = "data-dark-background",
22158
+ hardcoded = {},
22159
+ } = {},
22160
+ ) => {
22161
+ const innerDeps = [
22162
+ ...deps,
22163
+ // ref can change is the component pass a different ref on different render based on some logic
22164
+ // (can be used to control which element backgroundColor is being checked by switching the ref to another element)
22165
+ ref,
22166
+ // backgroundElementSelector can change if the component pass a different selector on different render based on some logic
22167
+ // (can be used to control which element backgroundColor is being checked by switching the selector to point to another element)
22168
+ backgroundElementSelector,
22169
+ ];
22170
+
22171
+ const hardcodedMap = new Map();
22172
+ for (const key of Object.keys(hardcoded)) {
22173
+ const value = hardcoded[key];
22174
+ innerDeps.push(key, value);
22175
+ const colorString = normalizeColorString(key);
22176
+ hardcodedMap.set(colorString, value);
22177
+ }
22178
+
22179
+ useLayoutEffect(() => {
22180
+ const el = ref.current;
22181
+ if (!el) {
22182
+ return null;
22183
+ }
22184
+ let elementToCheck = el;
22185
+ if (backgroundElementSelector) {
22186
+ elementToCheck = el.querySelector(backgroundElementSelector);
22187
+ if (!elementToCheck) {
22188
+ return null;
22189
+ }
22190
+ }
22191
+ const backgroundColor = getComputedStyle(elementToCheck).backgroundColor;
22192
+ if (!backgroundColor) {
22193
+ el.removeAttribute(attributeName);
22194
+ return null;
22195
+ }
22196
+ const backgroundColorString = normalizeColorString(backgroundColor, el);
22197
+ const hardcodedContrast = hardcodedMap.get(backgroundColorString);
22198
+ const contrastingColor =
22199
+ hardcodedContrast || contrastColor(backgroundColor, el);
22200
+ if (contrastingColor === "white") {
22201
+ el.setAttribute(attributeName, "");
22202
+ return () => {
22203
+ el.removeAttribute(attributeName);
22204
+ };
22205
+ }
22206
+ el.removeAttribute(attributeName);
22207
+ return null;
22208
+ }, innerDeps);
22209
+ };
22210
+
22211
+ const normalizeColorString = (color, el) => {
22212
+ const colorRgba = resolveCSSColor(color, el);
22213
+ if (!colorRgba) {
22214
+ return "";
22215
+ }
22216
+ return String(colorRgba);
22217
+ };
22218
+
22107
22219
  installImportMetaCss(import.meta);import.meta.css = /* css */`
22108
22220
  @layer navi {
22109
22221
  label {
@@ -22111,7 +22223,7 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
22111
22223
  cursor: pointer;
22112
22224
  }
22113
22225
 
22114
- &[data-read-only],
22226
+ &[data-readonly],
22115
22227
  &[data-disabled] {
22116
22228
  color: rgba(0, 0, 0, 0.5);
22117
22229
  cursor: default;
@@ -22187,14 +22299,9 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
22187
22299
  --accent-color: light-dark(#4476ff, #3b82f6);
22188
22300
  --background-color-checked: var(--accent-color);
22189
22301
  --border-color-checked: var(--accent-color);
22190
- --checkmark-color-light: white;
22191
- --checkmark-color-dark: rgb(55, 55, 55);
22192
- --checkmark-color: var(--checkmark-color-light);
22302
+ --checkmark-color: rgb(55, 55, 55);
22193
22303
  --cursor: pointer;
22194
-
22195
- --color-mix-light: black;
22196
- --color-mix-dark: white;
22197
- --color-mix: var(--color-mix-light);
22304
+ --color-mix: white;
22198
22305
 
22199
22306
  /* Hover */
22200
22307
  --border-color-hover: color-mix(in srgb, var(--border-color) 60%, black);
@@ -22288,9 +22395,9 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
22288
22395
  black
22289
22396
  );
22290
22397
 
22291
- &[data-dark] {
22292
- --color-mix: var(--color-mix-dark);
22293
- --checkmark-color: var(--checkmark-color-dark);
22398
+ &[data-dark-background] {
22399
+ --color-mix: black;
22400
+ --checkmark-color: white;
22294
22401
  }
22295
22402
  }
22296
22403
  }
@@ -22643,17 +22750,14 @@ const InputCheckboxBasic = props => {
22643
22750
  });
22644
22751
  const renderCheckboxMemoized = useCallback(renderCheckbox, [id, innerName, checked, innerRequired]);
22645
22752
  const boxRef = useRef();
22646
- useLayoutEffect(() => {
22647
- const naviCheckbox = boxRef.current;
22648
- const lightColor = "var(--checkmark-color-light)";
22649
- const darkColor = "var(--checkmark-color-dark)";
22650
- const colorPicked = pickLightOrDark("var(--accent-color)", lightColor, darkColor, naviCheckbox);
22651
- if (colorPicked === lightColor) {
22652
- naviCheckbox.removeAttribute("data-dark");
22653
- } else {
22654
- naviCheckbox.setAttribute("data-dark", "");
22753
+ useDarkBackgroundAttribute(boxRef, [accentColor, checked], {
22754
+ backgroundElementSelector: ".navi_checkbox_field",
22755
+ // the browser (chrome at least) prefer white in this scenario even if
22756
+ // the contrast is better with black, so we force it to white to match chrome behavior on this specific color
22757
+ hardcoded: {
22758
+ "#4476ff": "white"
22655
22759
  }
22656
- }, [accentColor]);
22760
+ });
22657
22761
  return jsxs(Box, {
22658
22762
  as: "span",
22659
22763
  ...remainingProps,
@@ -24417,7 +24521,7 @@ const InputTextualWithAction = props => {
24417
24521
  onEnd: onActionEnd
24418
24522
  });
24419
24523
  return jsx(InputTextualBasic, {
24420
- "data-action": boundAction.name,
24524
+ "data-action": boundAction.name || "anonymous",
24421
24525
  "data-action-debounce": actionDebounce,
24422
24526
  "data-action-after-change": actionAfterChange ? "" : undefined,
24423
24527
  ...rest,
@@ -28957,52 +29061,31 @@ const formatNumber = (value, { lang } = {}) => {
28957
29061
  return new Intl.NumberFormat(lang).format(value);
28958
29062
  };
28959
29063
 
28960
- const CSS_VAR_NAME = "--x-color-contrasting";
28961
-
28962
- const useContrastingColor = (ref, backgroundElementSelector) => {
28963
- useLayoutEffect(() => {
28964
- const el = ref.current;
28965
- if (!el) {
28966
- return;
28967
- }
28968
- let elementToCheck = el;
28969
- {
28970
- elementToCheck = el.querySelector(backgroundElementSelector);
28971
- if (!elementToCheck) {
28972
- return;
28973
- }
28974
- }
28975
- const lightColor = "var(--navi-color-light)";
28976
- const darkColor = "var(--navi-color-dark)";
28977
- const backgroundColor = getComputedStyle(elementToCheck).backgroundColor;
28978
- if (!backgroundColor) {
28979
- el.style.removeProperty(CSS_VAR_NAME);
28980
- return;
28981
- }
28982
- const colorPicked = pickLightOrDark(
28983
- backgroundColor,
28984
- lightColor,
28985
- darkColor,
28986
- el,
28987
- );
28988
- el.style.setProperty(CSS_VAR_NAME, colorPicked);
28989
- }, []);
28990
- };
28991
-
28992
29064
  installImportMetaCss(import.meta);import.meta.css = /* css */`
28993
29065
  @layer navi {
28994
29066
  }
28995
29067
  .navi_badge_count {
28996
29068
  --font-size: 0.7em;
28997
29069
  --x-background: var(--background);
28998
- --x-background-color: var(--background-color);
29070
+ --x-background-color: var(--background-color, var(--x-background));
29071
+ --x-color-contrasting: var(--navi-color-black);
29072
+ --x-color: var(--color, var(--x-color-contrasting));
28999
29073
  --padding-x: 0.5em;
29000
29074
  --padding-y: 0.2em;
29001
29075
  position: relative;
29002
29076
  display: inline-block;
29003
- color: var(--color, var(--x-color-contrasting));
29077
+ color: var(--x-color);
29004
29078
  font-size: var(--font-size);
29005
29079
 
29080
+ &[data-dark-background] {
29081
+ --x-color-contrasting: var(--navi-color-white);
29082
+ }
29083
+
29084
+ &[data-loading] {
29085
+ --x-background: transparent;
29086
+ --x-background-color: transparent;
29087
+ }
29088
+
29006
29089
  .navi_count_badge_overflow {
29007
29090
  position: relative;
29008
29091
  }
@@ -29015,7 +29098,7 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
29015
29098
  padding-left: var(--padding-x);
29016
29099
  line-height: normal;
29017
29100
  background: var(--x-background);
29018
- background-color: var(--x-background-color, var(--x-background));
29101
+ background-color: var(--x-background-color);
29019
29102
  border-radius: 1em;
29020
29103
  }
29021
29104
 
@@ -29031,7 +29114,7 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
29031
29114
  align-items: center;
29032
29115
  justify-content: center;
29033
29116
  background: var(--x-background);
29034
- background-color: var(--x-background-color, var(--x-background));
29117
+ background-color: var(--x-background-color);
29035
29118
  border-radius: 50%;
29036
29119
 
29037
29120
  &[data-single-char] {
@@ -29055,11 +29138,6 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
29055
29138
  font-size: var(--x-number-font-size);
29056
29139
  }
29057
29140
  }
29058
-
29059
- &[data-loading] {
29060
- --x-background: transparent;
29061
- --x-background-color: transparent;
29062
- }
29063
29141
  }
29064
29142
  `;
29065
29143
  const BadgeStyleCSSVars = {
@@ -29092,7 +29170,7 @@ const BadgeCount = ({
29092
29170
  }) => {
29093
29171
  const defaultRef = useRef();
29094
29172
  const ref = props.ref || defaultRef;
29095
- useContrastingColor(ref, ".navi_badge_count_visual");
29173
+ useDarkBackgroundAttribute(ref);
29096
29174
  let valueRequested = (() => {
29097
29175
  if (typeof children !== "string") return children;
29098
29176
  const parsed = Number(children);
@@ -29519,6 +29597,99 @@ const CodeBox = ({
29519
29597
  });
29520
29598
  };
29521
29599
 
29600
+ const navigator = typeof window === "undefined" ? undefined : window.navigator;
29601
+ const browserLang =
29602
+ typeof navigator !== "undefined"
29603
+ ? (navigator.language ?? navigator.languages?.[0])
29604
+ : undefined;
29605
+
29606
+ const createIntl = ({ systemLang = browserLang } = {}) => {
29607
+ const languageMap = new Map();
29608
+
29609
+ let defaultLang = systemLang;
29610
+
29611
+ const add = (lang, translations) => {
29612
+ // Accumulate: merge with any existing translations for this lang
29613
+ const existing = languageMap.get(lang);
29614
+ if (existing) {
29615
+ translations = { ...existing, ...translations };
29616
+ }
29617
+ // Derived language inherits all keys not explicitly overridden
29618
+ // e.g. "fr-provencal" inherits from "fr"
29619
+ const dashIndex = lang.indexOf("-");
29620
+ if (dashIndex !== -1) {
29621
+ const parentLang = lang.slice(0, dashIndex);
29622
+ const parentTranslations = languageMap.get(parentLang);
29623
+ if (parentTranslations) {
29624
+ translations = { ...parentTranslations, ...translations };
29625
+ }
29626
+ }
29627
+ languageMap.set(lang, translations);
29628
+
29629
+ defaultLang = matchBestLang(systemLang, languageMap);
29630
+ };
29631
+
29632
+ const _getTranslationTemplate = (key, lang) => {
29633
+ if (!lang) {
29634
+ // no lang specified
29635
+ return key;
29636
+ }
29637
+ const translations = languageMap.get(lang);
29638
+ if (!translations) {
29639
+ // code don't know this language
29640
+ return key;
29641
+ }
29642
+ const template = translations[key];
29643
+ if (!template) {
29644
+ // code know this language but have no translation for this key
29645
+ return key;
29646
+ }
29647
+ return template;
29648
+ };
29649
+
29650
+ const format = (key, values, { lang = defaultLang } = {}) => {
29651
+ const template = _getTranslationTemplate(key, lang);
29652
+ return interpolate(template, values);
29653
+ };
29654
+
29655
+ return { languageMap, add, format };
29656
+ };
29657
+
29658
+ // Walk "fr-CA-variant" → "fr-CA" → "fr" until a registered lang is found
29659
+ const matchLang = (lang, languageMap) => {
29660
+ if (languageMap.has(lang)) return lang;
29661
+ const parts = lang.split("-");
29662
+ while (parts.length > 1) {
29663
+ parts.pop();
29664
+ const candidate = parts.join("-");
29665
+ if (languageMap.has(candidate)) return candidate;
29666
+ }
29667
+ return null;
29668
+ };
29669
+
29670
+ // lang can be a string or an ordered array of preference strings
29671
+ const matchBestLang = (lang, languageMap) => {
29672
+ if (!lang) {
29673
+ return null;
29674
+ }
29675
+ const candidates = Array.isArray(lang) ? lang : [lang];
29676
+ for (const candidate of candidates) {
29677
+ const match = matchLang(candidate, languageMap);
29678
+ if (match) {
29679
+ return match;
29680
+ }
29681
+ }
29682
+ return null;
29683
+ };
29684
+
29685
+ const interpolate = (template, values) => {
29686
+ if (!values || typeof template !== "string") return template;
29687
+ return template.replace(/\{(\w+)\}/g, (_, key) => {
29688
+ const value = values[key];
29689
+ return value !== undefined ? String(value) : `{${key}}`;
29690
+ });
29691
+ };
29692
+
29522
29693
  installImportMetaCss(import.meta);import.meta.css = /* css */`
29523
29694
  @layer navi {
29524
29695
  .navi_quantity {
@@ -29579,12 +29750,49 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
29579
29750
  }
29580
29751
  }
29581
29752
  `;
29753
+ const QuantityIntl = createIntl();
29754
+ const wellKnownUnitMap = new Map();
29755
+ /**
29756
+ * Registers a unit with its translations per language, making it a "well-known"
29757
+ * unit that Quantity will automatically translate and pluralize.
29758
+ *
29759
+ * @param {string} unitName - The unit identifier used in the `unit` prop, e.g. `"minute"`.
29760
+ * @param {Record<string, string | [string, string]>} langTranslations
29761
+ * A map of language codes to translations. Each value is either:
29762
+ * - A tuple `[singular, plural]` for languages with distinct plural forms.
29763
+ * - A plain string for languages that use the same form for singular and plural (e.g. Japanese).
29764
+ *
29765
+ * @example
29766
+ * Quantity.Intl.addUnit("minute", {
29767
+ * en: ["minute", "minutes"],
29768
+ * fr: ["minute", "minutes"],
29769
+ * ja: "分",
29770
+ * });
29771
+ */
29772
+ QuantityIntl.addUnit = (unitName, langTranslations) => {
29773
+ const singularKey = unitName;
29774
+ const pluralKey = `${unitName}__plural`;
29775
+ wellKnownUnitMap.set(unitName, {
29776
+ singularKey,
29777
+ pluralKey
29778
+ });
29779
+ for (const [lang, translation] of Object.entries(langTranslations)) {
29780
+ const [singular, plural] = Array.isArray(translation) ? translation : [translation, translation];
29781
+ QuantityIntl.add(lang, {
29782
+ [singularKey]: singular,
29783
+ [pluralKey]: plural
29784
+ });
29785
+ }
29786
+ };
29787
+ const QuantityPropsCSSVars = {
29788
+ unitColor: "--unit-color",
29789
+ unitSizeRatio: "--unit-size-ratio"
29790
+ };
29582
29791
  const QuantityPseudoClasses = [":hover", ":active", ":read-only", ":disabled", ":-navi-loading"];
29583
29792
  const Quantity = ({
29584
29793
  children,
29585
29794
  unit,
29586
29795
  unitPosition = "right",
29587
- unitSizeRatio,
29588
29796
  label,
29589
29797
  size,
29590
29798
  lang,
@@ -29603,6 +29811,7 @@ const Quantity = ({
29603
29811
  return jsxs(Text, {
29604
29812
  baseClassName: "navi_quantity",
29605
29813
  "data-unit-bottom": unitBottom ? "" : undefined,
29814
+ propsCSSVars: QuantityPropsCSSVars,
29606
29815
  basePseudoState: {
29607
29816
  ":read-only": readOnly,
29608
29817
  ":disabled": disabled,
@@ -29624,18 +29833,49 @@ const Quantity = ({
29624
29833
  flowInline: true,
29625
29834
  children: jsx(LoadingDots, {})
29626
29835
  }) : valueFormatted
29627
- }), unit && jsx("span", {
29628
- className: "navi_quantity_unit",
29629
- style: {
29630
- ...(unitSizeRatio === undefined ? {} : {
29631
- "--unit-size-ratio": unitSizeRatio
29632
- })
29633
- },
29634
- children: unit
29836
+ }), unit && jsx(Unit, {
29837
+ value: value,
29838
+ unit: unit,
29839
+ lang: lang
29635
29840
  })]
29636
29841
  })]
29637
29842
  });
29638
29843
  };
29844
+ Quantity.Intl = QuantityIntl;
29845
+ const Unit = ({
29846
+ value,
29847
+ unit,
29848
+ lang
29849
+ }) => {
29850
+ let unitText = unit;
29851
+ if (Array.isArray(unit)) {
29852
+ const [singular, plural] = unit;
29853
+ unitText = value > 1 ? plural : singular;
29854
+ } else {
29855
+ const wellKnownUnit = wellKnownUnitMap.get(unit);
29856
+ if (wellKnownUnit) {
29857
+ const {
29858
+ singularKey,
29859
+ pluralKey
29860
+ } = wellKnownUnit;
29861
+ if (value > 1) {
29862
+ unitText = QuantityIntl.format(pluralKey, {
29863
+ x: value
29864
+ }, {
29865
+ lang
29866
+ });
29867
+ } else {
29868
+ unitText = QuantityIntl.format(singularKey, undefined, {
29869
+ lang
29870
+ });
29871
+ }
29872
+ }
29873
+ }
29874
+ return jsx("span", {
29875
+ className: "navi_quantity_unit",
29876
+ children: unitText
29877
+ });
29878
+ };
29639
29879
  const parseQuantityValue = children => {
29640
29880
  if (typeof children !== "string") {
29641
29881
  return children;
@@ -29659,6 +29899,14 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
29659
29899
  --fill-color-optimum: light-dark(#0f7c0f, #4caf50);
29660
29900
  --fill-color-suboptimum: light-dark(#fdb900, #ffc107);
29661
29901
  --fill-color-even-less-good: light-dark(#d83b01, #f44336);
29902
+
29903
+ --x-color: var(--navi-color-white);
29904
+ --x-shadow-color: black;
29905
+ --shadow-size: 0.5em;
29906
+ &[data-dark-background] {
29907
+ --x-color: black;
29908
+ --x-shadow-color: var(--navi-color-white);
29909
+ }
29662
29910
  }
29663
29911
  }
29664
29912
 
@@ -29704,11 +29952,11 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
29704
29952
  display: flex;
29705
29953
  align-items: center;
29706
29954
  justify-content: center;
29707
- color: var(--x-caption-color, white);
29955
+ color: var(--x-color);
29708
29956
  font-size: calc(var(--height) * 0.55);
29709
29957
  text-shadow:
29710
- 0 0 4px var(--x-caption-shadow-color, black),
29711
- 0 0 2px var(--x-caption-shadow-color, black);
29958
+ 0 0 var(--shadow-size) var(--x-shadow-color),
29959
+ 0 0 calc(var(--shadow-size) * 0.5) var(--x-shadow-color);
29712
29960
  white-space: nowrap;
29713
29961
  pointer-events: none;
29714
29962
  user-select: none;
@@ -29790,8 +30038,10 @@ const Meter = ({
29790
30038
  borderless,
29791
30039
  transition,
29792
30040
  style,
29793
- ...props
30041
+ ...rest
29794
30042
  }) => {
30043
+ const defaultRef = useRef();
30044
+ const ref = rest.ref || defaultRef;
29795
30045
  const clampedValue = value < min ? min : value > max ? max : value;
29796
30046
  const fillRatio = max === min ? 0 : (clampedValue - min) / (max - min);
29797
30047
  let children = caption;
@@ -29799,6 +30049,7 @@ const Meter = ({
29799
30049
  children = jsx(Quantity, {
29800
30050
  unit: "%",
29801
30051
  unitSizeRatio: "1",
30052
+ unitColor: "inherit",
29802
30053
  children: Math.round(fillRatio * 100)
29803
30054
  });
29804
30055
  }
@@ -29806,28 +30057,15 @@ const Meter = ({
29806
30057
  const fillColorVar = level === "optimum" ? "var(--fill-color-optimum)" : level === "suboptimum" ? "var(--fill-color-suboptimum)" : "var(--fill-color-even-less-good)";
29807
30058
  reportDisabledToLabel(disabled);
29808
30059
  reportReadOnlyToLabel(readOnly);
29809
- const trackContainerRef = useRef();
29810
- useLayoutEffect(() => {
29811
- if (!children) {
29812
- return;
29813
- }
29814
- const trackContainer = trackContainerRef.current;
29815
- if (!trackContainer) {
29816
- return;
29817
- }
29818
- // When fill covers less than half the track, the text center sits on the
29819
- // empty track — use the track color for contrast. Otherwise use fill color.
29820
- const bgEl = fillRatio >= 0.5 ? trackContainer.querySelector(".navi_meter_fill") : trackContainer.querySelector(".navi_meter_track");
29821
- if (!bgEl) {
29822
- return;
29823
- }
29824
- const bgColor = getComputedStyle(bgEl).backgroundColor;
29825
- const textColor = pickLightOrDark(bgColor, "white", "black", bgEl);
29826
- const shadowColor = textColor === "white" ? "black" : "white";
29827
- trackContainer.style.setProperty("--x-caption-color", textColor);
29828
- trackContainer.style.setProperty("--x-caption-shadow-color", shadowColor);
29829
- }, [children, level, fillRatio]);
30060
+
30061
+ // When fill covers less than half the track, the text center sits on the
30062
+ // empty track — use the track color for contrast. Otherwise use fill color.
30063
+ const backgroundElementSelector = fillRatio >= 0.5 ? ".navi_meter_fill" : ".navi_meter_track";
30064
+ useDarkBackgroundAttribute(ref, [], {
30065
+ backgroundElementSelector
30066
+ });
29830
30067
  return jsx(Box, {
30068
+ ref: ref,
29831
30069
  role: "meter",
29832
30070
  "aria-valuenow": clampedValue,
29833
30071
  "aria-valuemin": min,
@@ -29854,10 +30092,9 @@ const Meter = ({
29854
30092
  "--x-fill-color": fillColorVar,
29855
30093
  ...style
29856
30094
  },
29857
- ...props,
30095
+ ...rest,
29858
30096
  children: jsxs("span", {
29859
30097
  className: "navi_meter_track_container",
29860
- ref: trackContainerRef,
29861
30098
  children: [jsx(LoaderBackground, {
29862
30099
  loading: loading,
29863
30100
  color: "var(--loader-color)",
@@ -30318,5 +30555,5 @@ const UserSvg = () => jsx("svg", {
30318
30555
  })
30319
30556
  });
30320
30557
 
30321
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, BadgeCount, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, Checkbox, CheckboxList, Code, Col, Colgroup, ConstructionSvg, Details, DialogLayout, Editable, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, Group, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, MessageBox, Meter, Paragraph, Quantity, Radio, RadioList, Route, RouteLink, Routes, RowNumberCol, RowNumberTableCell, SINGLE_SPACE_CONSTRAINT, SVGMaskOverlay, SearchSvg, Select, SelectionContext, Separator, SettingsSvg, StarSvg, SummaryMarker, Svg, Tab, TabList, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, actionRunEffect, addCustomMessage, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, updateActions, useActionData, useActionStatus, useArraySignalMembership, useCalloutClose, useCancelPrevious, useCellsAndColumns, useConstraintValidityState, useDependenciesDiff, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useMatchingRouteInfo, useNavState$1 as useNavState, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage };
30558
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, BadgeCount, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, Checkbox, CheckboxList, Code, Col, Colgroup, ConstructionSvg, Details, DialogLayout, Editable, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, Group, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, MessageBox, Meter, Paragraph, Quantity, QuantityIntl, Radio, RadioList, Route, RouteLink, Routes, RowNumberCol, RowNumberTableCell, SINGLE_SPACE_CONSTRAINT, SVGMaskOverlay, SearchSvg, Select, SelectionContext, Separator, SettingsSvg, StarSvg, SummaryMarker, Svg, Tab, TabList, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, actionRunEffect, addCustomMessage, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createIntl, createRequestCanceller, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, updateActions, useActionData, useActionStatus, useArraySignalMembership, useCalloutClose, useCancelPrevious, useCellsAndColumns, useConstraintValidityState, useDarkBackgroundAttribute, useDependenciesDiff, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useMatchingRouteInfo, useNavState$1 as useNavState, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage };
30322
30559
  //# sourceMappingURL=jsenv_navi.js.map