@jsenv/navi 0.29.109 → 0.29.111

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.
@@ -4,14 +4,14 @@
4
4
  */
5
5
  import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
6
  export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
7
- import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, initFocusGroup, scrollIntoViewScoped, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
+ import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, initFocusGroup, scrollIntoViewScoped, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, isTouchDrivenEvent, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
8
  export { clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
9
- import { signal, computed, effect, batch, untracked, useSignal } from "@preact/signals";
9
+ import { signal, computed, effect, batch, useComputed, untracked, useSignal } from "@preact/signals";
10
10
  import { isValidElement, createContext, render, h, Fragment, toChildArray, options, cloneElement } from "preact";
11
11
  import { useErrorBoundary, useLayoutEffect, useContext, useCallback, useRef, useState, useEffect, useMemo, useId } from "preact/hooks";
12
12
  import { jsxs, jsx, Fragment as Fragment$1 } from "preact/jsx-runtime";
13
13
  import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
14
- import { parseDuration, durationContainsNaN, compareTwoDurations, durationToSeconds, createValidity, durationToISOString } from "@jsenv/validity";
14
+ import { parseDuration, durationContainsNaN, compareTwoDurations, durationToSeconds, DISPLAYABLE_RULE, MAX_LINE_BREAKS_RULE, NO_EMOJI_RULE, SINGLE_SPACE_RULE, createValidity, resolveCharClass, getCharClassMessageKey, compileCharClassAnchored, compileCharClass, CHAR_CLASS_PRESETS, durationToISOString } from "@jsenv/validity";
15
15
  export { compareTwoDurations, durationContainsNaN, durationToHours, durationToISOString, durationToMinutes, durationToNumber, durationToSeconds, durationToString, parseDuration } from "@jsenv/validity";
16
16
  import { Suspense, createPortal, forwardRef } from "preact/compat";
17
17
 
@@ -3676,8 +3676,55 @@ const findProxyControllers = (realInputId) => {
3676
3676
  return proxyControllersByRealInputId.get(realInputId) ?? null;
3677
3677
  };
3678
3678
 
3679
+ /**
3680
+ * The attributes constraints read, filled by each constraint module as it
3681
+ * evaluates. A constraint declares the attribute it wants (`"data-no-emoji"`)
3682
+ * and gets the prop for free: a control accepts the camelCase form
3683
+ * (`noEmoji`) and writes it on the control host under the attribute name — the
3684
+ * same conversion `element.dataset` does, so what a component is passed and
3685
+ * what ends up in the DOM read as one thing.
3686
+ */
3687
+
3679
3688
  const CONSTRAINT_ATTRIBUTE_SET = new Set();
3680
3689
 
3690
+ const dataAttributeCache = new Map();
3691
+ // A constraint imported lazily registers its attribute after controls have
3692
+ // already rendered, so an answer computed before it arrived must not survive it.
3693
+ let attributeCountWhenCached = 0;
3694
+ /**
3695
+ * The constraint attribute a prop stands for, `null` when it stands for none:
3696
+ * `"noEmoji"` → `"data-no-emoji"`.
3697
+ */
3698
+ const constraintAttributeFromProp = (key) => {
3699
+ if (attributeCountWhenCached !== CONSTRAINT_ATTRIBUTE_SET.size) {
3700
+ dataAttributeCache.clear();
3701
+ attributeCountWhenCached = CONSTRAINT_ATTRIBUTE_SET.size;
3702
+ }
3703
+ const fromCache = dataAttributeCache.get(key);
3704
+ if (fromCache !== undefined) {
3705
+ return fromCache;
3706
+ }
3707
+ let attribute = null;
3708
+ // An attribute is already written as one (`data-no-emoji`, `aria-label`) —
3709
+ // there is nothing to convert, and the literal lookup has already happened.
3710
+ if (!key.includes("-")) {
3711
+ const candidate = `data-${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
3712
+ if (CONSTRAINT_ATTRIBUTE_SET.has(candidate)) {
3713
+ attribute = candidate;
3714
+ }
3715
+ }
3716
+ dataAttributeCache.set(key, attribute);
3717
+ return attribute;
3718
+ };
3719
+
3720
+ /**
3721
+ * Whether a constraint attribute is on. Present means on — `""` is how HTML
3722
+ * writes a bare attribute — and only the values that say "passed, and off"
3723
+ * turn it off.
3724
+ */
3725
+ const isConstraintAttributeOn = (value) =>
3726
+ value !== undefined && value !== null && value !== false;
3727
+
3681
3728
  const CONSTRAINT_NAME_TO_PROP = {
3682
3729
  disabled: "disabledMessage",
3683
3730
  required: "requiredMessage",
@@ -3689,6 +3736,9 @@ const CONSTRAINT_NAME_TO_PROP = {
3689
3736
  min: "minMessage",
3690
3737
  max: "maxMessage",
3691
3738
  single_space: "singleSpaceMessage",
3739
+ displayable: "displayableMessage",
3740
+ max_line_breaks: "maxLineBreaksMessage",
3741
+ no_emoji: "noEmojiMessage",
3692
3742
  same_as: "sameAsMessage",
3693
3743
  min_lower_letter: "minLowerLetterMessage",
3694
3744
  min_upper_letter: "minUpperLetterMessage",
@@ -3755,10 +3805,13 @@ const getConstraintMessage = (
3755
3805
  };
3756
3806
  };
3757
3807
 
3758
- // prop that we'll set on the control
3808
+ // prop that we'll set on the control.
3809
+ // CONSTRAINT_ATTRIBUTE_SET is consulted through controlAttributeFromProp()
3810
+ // rather than spread in here: a constraint registers into it when its own
3811
+ // module evaluates, so anything read at module-eval time reads a set that is
3812
+ // still filling up — and in a bundle, whichever constraint happens to evaluate
3813
+ // last would silently lose its attribute.
3759
3814
  const CONTROL_ATTRIBUTE_SET = new Set([
3760
- ...CONSTRAINT_ATTRIBUTE_SET,
3761
-
3762
3815
  "ref",
3763
3816
  "children",
3764
3817
  "id",
@@ -3800,7 +3853,6 @@ const CONTROL_ATTRIBUTE_SET = new Set([
3800
3853
  ]);
3801
3854
  // prop concerning control but that won't end up in the DOM if not inside CONTROL_ATTRIBUTE_SET
3802
3855
  const CONTROL_PROP_SET = new Set([
3803
- ...CONTROL_ATTRIBUTE_SET,
3804
3856
  ...CONSTRAINT_MESSAGE_PROP_NAME_SET,
3805
3857
 
3806
3858
  "action",
@@ -3820,6 +3872,11 @@ const CONTROL_PROP_SET = new Set([
3820
3872
 
3821
3873
  "loading",
3822
3874
  "basePseudoState",
3875
+ // App constraints this control must satisfy, on top of the ones navi ships:
3876
+ // `constraints={[MY_CONSTRAINT]}`. A constraint is an object, so it carries
3877
+ // its own parameters — nothing has to travel through an attribute. Read on
3878
+ // every check in control_validation.js; `registerGlobalConstraint` is the
3879
+ // same thing for every control at once.
3823
3880
  "constraints",
3824
3881
 
3825
3882
  // A real target inside a zone that belongs to another control — see
@@ -3857,6 +3914,21 @@ const CONTROL_PROP_SET = new Set([
3857
3914
  "maxLengthGuard",
3858
3915
  ]);
3859
3916
 
3917
+ /**
3918
+ * The attribute a prop must be written as on the control host, `null` when the
3919
+ * prop is not one. A constraint attribute may be passed either way — as the
3920
+ * attribute itself (`data-no-emoji`) or as the prop it stands for (`noEmoji`).
3921
+ */
3922
+ const controlAttributeFromProp = (key) => {
3923
+ if (CONTROL_ATTRIBUTE_SET.has(key) || CONSTRAINT_ATTRIBUTE_SET.has(key)) {
3924
+ return key;
3925
+ }
3926
+ return constraintAttributeFromProp(key);
3927
+ };
3928
+
3929
+ const isControlProp = (key) =>
3930
+ CONTROL_PROP_SET.has(key) || controlAttributeFromProp(key) !== null;
3931
+
3860
3932
  const MessagePropsRefContext = createContext();
3861
3933
 
3862
3934
  const ControlIdContext = createContext();
@@ -7233,18 +7305,48 @@ naviI18n.addAll({
7233
7305
  fr: "L'heure doit être <strong>[max]</strong> ou moins.",
7234
7306
  en: "The time must be <strong>[max]</strong> or earlier.",
7235
7307
  },
7236
- "constraint.single_space.start.default": {
7308
+ "constraint.single_space.start": {
7237
7309
  fr: "Ce champ ne doit pas commencer par un espace.",
7238
7310
  en: "This field must not start with a space.",
7239
7311
  },
7240
- "constraint.single_space.end.default": {
7312
+ "constraint.single_space.end": {
7241
7313
  fr: "Ce champ ne doit pas finir par un espace.",
7242
7314
  en: "This field must not end with a space.",
7243
7315
  },
7244
- "constraint.single_space.consecutive.default": {
7316
+ "constraint.single_space.consecutive": {
7245
7317
  fr: "Ce champ ne doit pas contenir plusieurs espaces consécutifs.",
7246
7318
  en: "This field must not contain consecutive spaces.",
7247
7319
  },
7320
+ // [sample] is the offending character with its marks — a stack is invisible
7321
+ // as a description and obvious as a sample.
7322
+ "constraint.displayable.stacked_marks.singular": {
7323
+ fr: "Ce champ contient un caractère qui empile plus de <strong>[max]</strong> signes : « [sample] ».",
7324
+ en: "This field contains a character stacking more than <strong>[max]</strong> marks: “[sample]”.",
7325
+ },
7326
+ "constraint.displayable.stacked_marks.plural": {
7327
+ fr: "Ce champ contient [count] caractères qui empilent plus de <strong>[max]</strong> signes (tel que « [sample] »).",
7328
+ en: "This field contains [count] characters stacking more than <strong>[max]</strong> marks (such as “[sample]”).",
7329
+ },
7330
+ "constraint.displayable.invisible": {
7331
+ fr: "Ce champ doit contenir au moins un caractère visible.",
7332
+ en: "This field must contain at least one visible character.",
7333
+ },
7334
+ "constraint.displayable.blank_lines": {
7335
+ fr: "Ce champ ne doit pas contenir plusieurs lignes vides consécutives.",
7336
+ en: "This field must not contain consecutive blank lines.",
7337
+ },
7338
+ "constraint.displayable.dangling_joiner": {
7339
+ fr: "Ce champ contient un caractère de liaison invisible qui ne relie rien.",
7340
+ en: "This field contains an invisible joiner that joins nothing.",
7341
+ },
7342
+ "constraint.no_emoji.default": {
7343
+ fr: "Ce champ ne doit pas contenir d'emoji.",
7344
+ en: "This field must not contain emoji.",
7345
+ },
7346
+ "constraint.max_line_breaks.default": {
7347
+ fr: "Ce champ ne doit pas contenir plus de [max] retour[s] à la ligne.",
7348
+ en: "This field must not contain more than [max] line break[s].",
7349
+ },
7248
7350
  "constraint.min_lower_letter.password.singular": {
7249
7351
  fr: "Ce mot de passe doit contenir au moins une lettre minuscule.",
7250
7352
  en: "This password must contain at least one lowercase letter.",
@@ -7311,35 +7413,42 @@ naviI18n.addAll({
7311
7413
  },
7312
7414
  });
7313
7415
 
7314
- // charGuard / maxLengthGuard callout messages
7416
+ // Character class and maxLengthGuard messages. The char class keys are
7417
+ // @jsenv/validity's own ("char_class.slug"), prefixed with "constraint." —
7418
+ // the same sentence refuses a keystroke in a callout and a whole value in a
7419
+ // constraint, so there is one key for both.
7315
7420
  naviI18n.addAll({
7316
7421
  // Preset-specific char messages — more informative than the generic fallback
7317
- "constraint.guard.number": {
7422
+ "constraint.char_class.numeric": {
7318
7423
  fr: "Ce champ ne peut contenir que des chiffres.",
7319
7424
  en: "This field can only contain digits.",
7320
7425
  },
7321
- "constraint.guard.alpha": {
7426
+ "constraint.char_class.alpha": {
7322
7427
  fr: "Ce champ ne peut contenir que des lettres.",
7323
7428
  en: "This field can only contain letters.",
7324
7429
  },
7325
- "constraint.guard.alphanumeric": {
7430
+ "constraint.char_class.alphanumeric": {
7326
7431
  fr: "Ce champ ne peut contenir que des lettres et des chiffres.",
7327
7432
  en: "This field can only contain letters and digits.",
7328
7433
  },
7329
- "constraint.guard.uppercase": {
7434
+ "constraint.char_class.uppercase": {
7330
7435
  fr: "Ce champ ne peut contenir que des lettres majuscules.",
7331
7436
  en: "This field can only contain uppercase letters.",
7332
7437
  },
7333
- "constraint.guard.hex": {
7438
+ "constraint.char_class.hex": {
7334
7439
  fr: "Ce champ ne peut contenir que des chiffres hexadécimaux (0-9, A-F).",
7335
7440
  en: "This field can only contain hexadecimal digits (0-9, A-F).",
7336
7441
  },
7337
- "constraint.guard.slug": {
7442
+ "constraint.char_class.slug": {
7338
7443
  fr: "Ce champ ne peut contenir que des lettres minuscules, des chiffres et des tirets.",
7339
7444
  en: "This field can only contain lowercase letters, digits, and hyphens.",
7340
7445
  },
7341
7446
  // Generic fallback for custom char classes and other presets (tel, card, postal, iban…)
7342
- "constraint.guard.chars": {
7447
+ "constraint.char_class.no_emoji": {
7448
+ fr: "Ce champ ne peut pas contenir d'emoji.",
7449
+ en: "This field cannot contain emoji.",
7450
+ },
7451
+ "constraint.char_class.default": {
7343
7452
  fr: "Ce champ ne peut contenir que les caractères autorisés.",
7344
7453
  en: "This field can only contain allowed characters.",
7345
7454
  },
@@ -8487,7 +8596,11 @@ const PATTERN_CONSTRAINT = {
8487
8596
  if (!valueAsString) {
8488
8597
  return null;
8489
8598
  }
8490
- const regex = new RegExp(`^(?:${pattern})$`);
8599
+ // The `u` flag is how the platform compiles this same attribute, and what
8600
+ // lets a pattern speak about characters: `\p{...}` is only recognized under
8601
+ // it, and a range covers whole code points rather than the two halves an
8602
+ // astral character is made of.
8603
+ const regex = new RegExp(`^(?:${pattern})$`, "u");
8491
8604
  if (regex.test(valueAsString)) {
8492
8605
  return null;
8493
8606
  }
@@ -10969,6 +11082,177 @@ const isFunctionButNotAnActionFunction = (action) => {
10969
11082
  return typeof action === "function" && !action.isAction;
10970
11083
  };
10971
11084
 
11085
+ /**
11086
+ * Where navi meets @jsenv/validity.
11087
+ *
11088
+ * validity names a refusal with a key and its parameters rather than a
11089
+ * sentence, so that a field and a server can refuse in the same words in the
11090
+ * language of the person reading. navi is one of those two callers: it says the
11091
+ * sentence, in the browser, through `naviI18n`. The keys line up on purpose —
11092
+ * validity's `"single_space.start"` is navi's `"constraint.single_space.start"`
11093
+ * — so overriding a message is looking up one key, not going through a
11094
+ * translation table.
11095
+ *
11096
+ * The other direction is `constraintFromValidityRule`: a rule an app wrote for
11097
+ * its server, worn by a control as a constraint.
11098
+ */
11099
+
11100
+
11101
+ /**
11102
+ * Turns a @jsenv/validity rule into a constraint a control can wear, so an app
11103
+ * rule written once — in the package its server reads too — is checked on both
11104
+ * sides instead of being written twice.
11105
+ *
11106
+ * @param {object} rule
11107
+ * `{ name, applyOn(ruleValue, value, ruleConfig) }`, the same object passed
11108
+ * to `createValidity({ rules })`.
11109
+ * @param {object} [ruleConfig]
11110
+ * What parameterizes the rule, under its own name — `{ maxWords: 40 }` for a
11111
+ * rule named `maxWords`. Pass `formatMessage` here to say the refusal through
11112
+ * the app's own i18n; without it the key is looked up in `naviI18n` under
11113
+ * `constraint.<key>`, and a rule answering with a finished sentence is shown
11114
+ * as-is.
11115
+ *
11116
+ * Call it once, at module level: a constraint rebuilt on every render is a new
11117
+ * object on every check.
11118
+ */
11119
+ const constraintFromValidityRule = (rule, ruleConfig = {}) => {
11120
+ const { formatMessage, ...ruleParams } = ruleConfig;
11121
+ return {
11122
+ name: rule.name,
11123
+ check: (field) => {
11124
+ const result = rule.applyOn(
11125
+ ruleParams[rule.name],
11126
+ field.uiState,
11127
+ ruleParams,
11128
+ );
11129
+ if (!result) {
11130
+ return null;
11131
+ }
11132
+ if (typeof result === "string") {
11133
+ return result;
11134
+ }
11135
+ if (formatMessage) {
11136
+ return formatMessage(result.key, result.params);
11137
+ }
11138
+ return naviI18nFromValidityMessage(result);
11139
+ },
11140
+ };
11141
+ };
11142
+
11143
+ const naviI18nFromValidityMessage = ({ key, params }) => {
11144
+ if (params && typeof params.max === "number") {
11145
+ // Lets a template pluralize on the bound it names: "[max] retour[s]".
11146
+ return naviI18n(`constraint.${key}`, {
11147
+ ...params,
11148
+ s: params.max > 1 ? "s" : "",
11149
+ });
11150
+ }
11151
+ return naviI18n(`constraint.${key}`, params);
11152
+ };
11153
+
11154
+ /**
11155
+ * `data-displayable` — the value must be something the layout can actually
11156
+ * draw. What it refuses and why lives in @jsenv/validity's DISPLAYABLE_RULE:
11157
+ * these are display rules, not app rules, so a server re-checking the value
11158
+ * asks the exact same question and gets the exact same refusal.
11159
+ *
11160
+ * `data-max-stacked-marks` raises how many marks may stack on one base
11161
+ * character, for a language that needs more than the default.
11162
+ */
11163
+
11164
+
11165
+ const DISPLAYABLE_CONSTRAINT = {
11166
+ name: "displayable",
11167
+ messageAttribute: "data-displayable-message",
11168
+ check: (field) => {
11169
+ const displayable = field.controlHostProps["data-displayable"];
11170
+ if (!isConstraintAttributeOn(displayable)) {
11171
+ return null;
11172
+ }
11173
+ const valueAsString =
11174
+ field.uiState === undefined ? "" : String(field.uiState);
11175
+ const maxStackedMarksAttribute =
11176
+ field.controlHostProps["data-max-stacked-marks"];
11177
+ const result = DISPLAYABLE_RULE.applyOn(true, valueAsString, {
11178
+ maxStackedMarks:
11179
+ maxStackedMarksAttribute === undefined
11180
+ ? undefined
11181
+ : parseInt(maxStackedMarksAttribute, 10),
11182
+ });
11183
+ if (!result) {
11184
+ return null;
11185
+ }
11186
+ return naviI18nFromValidityMessage(result);
11187
+ },
11188
+ };
11189
+ CONSTRAINT_ATTRIBUTE_SET.add("data-displayable");
11190
+ CONSTRAINT_ATTRIBUTE_SET.add("data-max-stacked-marks");
11191
+
11192
+ /**
11193
+ * `data-max-line-breaks` — how many line breaks the value may hold. Counted in
11194
+ * breaks rather than in lines because how many lines a value renders as depends
11195
+ * on wrapping, which is the layout's answer, not the value's.
11196
+ *
11197
+ * The rule is @jsenv/validity's MAX_LINE_BREAKS_RULE — a textarea in the
11198
+ * browser and a server receiving the value both ask it.
11199
+ */
11200
+
11201
+
11202
+ const MAX_LINE_BREAKS_CONSTRAINT = {
11203
+ name: "max_line_breaks",
11204
+ messageAttribute: "data-max-line-breaks-message",
11205
+ check: (field) => {
11206
+ const maxLineBreaksAttribute =
11207
+ field.controlHostProps["data-max-line-breaks"];
11208
+ if (!isConstraintAttributeOn(maxLineBreaksAttribute)) {
11209
+ return null;
11210
+ }
11211
+ const maxLineBreaks = parseInt(maxLineBreaksAttribute, 10);
11212
+ if (isNaN(maxLineBreaks)) {
11213
+ return null;
11214
+ }
11215
+ const valueAsString =
11216
+ field.uiState === undefined ? "" : String(field.uiState);
11217
+ const result = MAX_LINE_BREAKS_RULE.applyOn(maxLineBreaks, valueAsString);
11218
+ if (!result) {
11219
+ return null;
11220
+ }
11221
+ return naviI18nFromValidityMessage(result);
11222
+ },
11223
+ };
11224
+ CONSTRAINT_ATTRIBUTE_SET.add("data-max-line-breaks");
11225
+
11226
+ /**
11227
+ * `data-no-emoji` — an app is free with emoji or it is not, and that is not the
11228
+ * layout's call: a row survives an emoji, a legal name, an identifier or a
11229
+ * title may still not want one. So it is its own switch rather than a part of
11230
+ * `data-displayable`.
11231
+ *
11232
+ * The rule is @jsenv/validity's NO_EMOJI_RULE. To refuse the keystroke instead
11233
+ * of the value, `charGuard="noEmoji"` is the same knowledge on the other side.
11234
+ */
11235
+
11236
+
11237
+ const NO_EMOJI_CONSTRAINT = {
11238
+ name: "no_emoji",
11239
+ messageAttribute: "data-no-emoji-message",
11240
+ check: (field) => {
11241
+ const noEmoji = field.controlHostProps["data-no-emoji"];
11242
+ if (!isConstraintAttributeOn(noEmoji)) {
11243
+ return null;
11244
+ }
11245
+ const valueAsString =
11246
+ field.uiState === undefined ? "" : String(field.uiState);
11247
+ const result = NO_EMOJI_RULE.applyOn(true, valueAsString);
11248
+ if (!result) {
11249
+ return null;
11250
+ }
11251
+ return naviI18nFromValidityMessage(result);
11252
+ },
11253
+ };
11254
+ CONSTRAINT_ATTRIBUTE_SET.add("data-no-emoji");
11255
+
10972
11256
  const MIN_LOWER_LETTER_CONSTRAINT = {
10973
11257
  name: "min_lower_letter",
10974
11258
  messageAttribute: "data-min-lower-letter-message",
@@ -11238,30 +11522,28 @@ const SAME_AS_CONSTRAINT = {
11238
11522
  };
11239
11523
  CONSTRAINT_ATTRIBUTE_SET.add("data-same-as");
11240
11524
 
11525
+ /**
11526
+ * `data-single-space` — no leading or trailing space, never two in a row.
11527
+ * The rule itself is @jsenv/validity's SINGLE_SPACE_RULE, so a server checking
11528
+ * the value again refuses it for the same reason and in the same words.
11529
+ */
11530
+
11531
+
11241
11532
  const SINGLE_SPACE_CONSTRAINT = {
11242
11533
  name: "single_space",
11243
11534
  messageAttribute: "data-single-space-message",
11244
11535
  check: (field) => {
11245
11536
  const singleSpace = field.controlHostProps["data-single-space"];
11246
- if (singleSpace === undefined) {
11537
+ if (!isConstraintAttributeOn(singleSpace)) {
11247
11538
  return null;
11248
11539
  }
11249
-
11250
11540
  const valueAsString =
11251
11541
  field.uiState === undefined ? "" : String(field.uiState);
11252
- const hasLeadingSpace = valueAsString.startsWith(" ");
11253
- const hasTrailingSpace = valueAsString.endsWith(" ");
11254
- const hasDoubleSpace = valueAsString.includes(" ");
11255
- if (!hasLeadingSpace && !hasTrailingSpace && !hasDoubleSpace) {
11542
+ const result = SINGLE_SPACE_RULE.applyOn(true, valueAsString);
11543
+ if (!result) {
11256
11544
  return null;
11257
11545
  }
11258
- if (hasLeadingSpace) {
11259
- return naviI18n("constraint.single_space.start.default");
11260
- }
11261
- if (hasTrailingSpace) {
11262
- return naviI18n("constraint.single_space.end.default");
11263
- }
11264
- return naviI18n("constraint.single_space.consecutive.default");
11546
+ return naviI18nFromValidityMessage(result);
11265
11547
  },
11266
11548
  };
11267
11549
  CONSTRAINT_ATTRIBUTE_SET.add("data-single-space");
@@ -11386,6 +11668,9 @@ const STANDARD_CONSTRAINT_SET = new Set([
11386
11668
  const NAVI_CONSTRAINT_SET = new Set([
11387
11669
  MIN_SPECIAL_CHAR_CONSTRAINT,
11388
11670
  SINGLE_SPACE_CONSTRAINT,
11671
+ DISPLAYABLE_CONSTRAINT,
11672
+ MAX_LINE_BREAKS_CONSTRAINT,
11673
+ NO_EMOJI_CONSTRAINT,
11389
11674
  MIN_DIGIT_CONSTRAINT,
11390
11675
  MIN_UPPER_LETTER_CONSTRAINT,
11391
11676
  MIN_LOWER_LETTER_CONSTRAINT,
@@ -11398,8 +11683,29 @@ const DEFAULT_CONSTRAINT_SET = new Set([
11398
11683
  ...NAVI_CONSTRAINT_SET,
11399
11684
  ]);
11400
11685
  const registerGlobalConstraint = (customConstraint) => {
11401
- NAVI_CONSTRAINT_SET.add(customConstraint);
11402
- DEFAULT_CONSTRAINT_SET.add(customConstraint);
11686
+ const constraint = normalizeConstraint(customConstraint);
11687
+ NAVI_CONSTRAINT_SET.add(constraint);
11688
+ DEFAULT_CONSTRAINT_SET.add(constraint);
11689
+ };
11690
+
11691
+ // A constraint may be written as a bare check function; the rest of the code
11692
+ // wants the object shape. The wrapper is cached so a function passed on every
11693
+ // render keeps one identity across checks.
11694
+ const constraintFromFunctionMap = new WeakMap();
11695
+ const normalizeConstraint = (constraint) => {
11696
+ if (typeof constraint !== "function") {
11697
+ return constraint;
11698
+ }
11699
+ const existing = constraintFromFunctionMap.get(constraint);
11700
+ if (existing) {
11701
+ return existing;
11702
+ }
11703
+ const constraintObject = {
11704
+ name: constraint.name || "custom_function",
11705
+ check: constraint,
11706
+ };
11707
+ constraintFromFunctionMap.set(constraint, constraintObject);
11708
+ return constraintObject;
11403
11709
  };
11404
11710
 
11405
11711
  const createControlValidation = (
@@ -11415,15 +11721,10 @@ const createControlValidation = (
11415
11721
  const dynamicConstraintSet = new Set();
11416
11722
  {
11417
11723
  controlValidity.registerConstraint = (constraint) => {
11418
- if (typeof constraint === "function") {
11419
- constraint = {
11420
- name: constraint.name || "custom_function",
11421
- check: constraint,
11422
- };
11423
- }
11424
- dynamicConstraintSet.add(constraint);
11724
+ const constraintObject = normalizeConstraint(constraint);
11725
+ dynamicConstraintSet.add(constraintObject);
11425
11726
  return () => {
11426
- dynamicConstraintSet.delete(constraint);
11727
+ dynamicConstraintSet.delete(constraintObject);
11427
11728
  };
11428
11729
  };
11429
11730
  }
@@ -11480,6 +11781,16 @@ const createControlValidation = (
11480
11781
  ...DEFAULT_CONSTRAINT_SET,
11481
11782
  ...dynamicConstraintSet,
11482
11783
  ]);
11784
+ // An app constraint declared at the call site: `constraints={[MY_CONSTRAINT]}`.
11785
+ // Read from the raw props on every check so a constraint whose parameters
11786
+ // are closed over is re-created freely, and last so the constraints navi
11787
+ // ships are the ones reported first (see pickConstraintFailureInfo).
11788
+ const constraintsFromProps = controller.props.constraints;
11789
+ if (constraintsFromProps) {
11790
+ for (const constraintFromProps of constraintsFromProps) {
11791
+ constraintSet.add(normalizeConstraint(constraintFromProps));
11792
+ }
11793
+ }
11483
11794
  const elementSig = getElementSignature(controller.ref.current);
11484
11795
  // Not logged: every control checks its constraints on every interaction and
11485
11796
  // almost always passes, so this line alone was most of the debug output —
@@ -11531,6 +11842,13 @@ const createControlValidation = (
11531
11842
  }
11532
11843
  }
11533
11844
 
11845
+ // Several constraints can fail at once and only one sentence is shown —
11846
+ // naming it here lets whoever draws its own summary of the failures say the
11847
+ // same thing as the callout instead of picking a second one.
11848
+ newConstraintValidityState.reported = failedConstraintInfo
11849
+ ? failedConstraintInfo.name
11850
+ : null;
11851
+
11534
11852
  const activeFailedConstraintInfo = failedConstraintInfo;
11535
11853
  if (activeFailedConstraintInfo) {
11536
11854
  const titleLess = controller.controlHostProps.title === undefined;
@@ -12807,12 +13125,18 @@ const useArraySignalMembership = (...args) => {
12807
13125
  "useArraySignalMembership requires at least 2 arguments: [arraySignal, id]",
12808
13126
  );
12809
13127
  }
13128
+ const [arraySignal, id] = args;
12810
13129
 
12811
- return useMemo(() => {
12812
- const [useIsMember, add, remove] = arraySignalMembership(...args);
12813
- const isMember = useIsMember();
12814
- return [isMember, add, remove];
12815
- }, args);
13130
+ // Through a computed so the component re-renders when its own membership
13131
+ // changes, not every time anything else is added to or removed from the
13132
+ // array: a list of 200 rows each watching the same array would otherwise all
13133
+ // re-render (and each re-scan the array) when one row is toggled.
13134
+ const isMember = useComputed(() => arraySignal.value.includes(id)).value;
13135
+ const [, add, remove] = useMemo(
13136
+ () => arraySignalMembership(arraySignal, id),
13137
+ [arraySignal, id],
13138
+ );
13139
+ return [isMember, add, remove];
12816
13140
  };
12817
13141
 
12818
13142
  const arraySignalMembership = (...args) => {
@@ -14169,7 +14493,7 @@ const createRangeReader = (
14169
14493
  // dropped here is one that gets asked for again after a remount.
14170
14494
  readRange.trimComposition = (keepFrom, keepTo, budget) => {
14171
14495
  const composition = findComposition(currentParams());
14172
- if (!composition || !budget || composition.idByIndex.size <= budget) {
14496
+ if (!composition || composition.idByIndex.size <= budget) {
14173
14497
  return;
14174
14498
  }
14175
14499
  for (const index of composition.idByIndex.keys()) {
@@ -19726,6 +20050,21 @@ import.meta.css = [/* css */`
19726
20050
  }
19727
20051
  }
19728
20052
 
20053
+ /* A corner claim travels down (see group.jsx) because the member joined to
20054
+ its neighbours is not always the thing that draws the frame: it can be a
20055
+ bare wrapper, a tooltip, a link, and the control inside is what has to
20056
+ square. That only holds while the wrapper adds nothing of its own. A box
20057
+ that paints a background or a border, or that insets what it holds, IS the
20058
+ frame at that spot — what is inside it sits on padding or on that
20059
+ background, never on the corner the group squared — so the claim stops
20060
+ here, exactly as a control stops it once it has answered. */
20061
+ [navi-box-frame] > * {
20062
+ --x-corner-top-left-radius: initial;
20063
+ --x-corner-top-right-radius: initial;
20064
+ --x-corner-bottom-right-radius: initial;
20065
+ --x-corner-bottom-left-radius: initial;
20066
+ }
20067
+
19729
20068
  @layer navi {
19730
20069
  /*
19731
20070
  When using square/circle/aspectRatio prop we expect box to respect the aspect ratio.
@@ -20012,10 +20351,27 @@ const computeBox = (props, parentBoxFlow) => {
20012
20351
  rest["data-body"] = "";
20013
20352
  // Padding is what decides whether the content reaches the body's own
20014
20353
  // corners — see the corner claims in this file's CSS.
20015
- if (!PADDING_PROP_NAMES.some(name => isNonZeroSpacing(rest[name]))) {
20354
+ let flush = true;
20355
+ for (const name of PADDING_PROP_SET) {
20356
+ if (declaresSomething(rest[name])) {
20357
+ flush = false;
20358
+ break;
20359
+ }
20360
+ }
20361
+ if (flush) {
20016
20362
  rest["data-body-flush"] = "";
20017
20363
  }
20018
20364
  }
20365
+ // A box that paints something of its own, or that insets what it holds, is
20366
+ // the frame at that spot: the corner claims coming from a Group are about
20367
+ // ITS corners and nothing inside reaches them, so they stop here — see this
20368
+ // file's CSS.
20369
+ for (const name of FRAME_PROP_SET) {
20370
+ if (declaresSomething(rest[name])) {
20371
+ rest["navi-box-frame"] = "";
20372
+ break;
20373
+ }
20374
+ }
20019
20375
  const defaultDisplay = getDefaultDisplay(TagName);
20020
20376
  let {
20021
20377
  inline,
@@ -20446,8 +20802,14 @@ const shouldInjectSeparatorBetween = (left, right) => {
20446
20802
  }
20447
20803
  return true;
20448
20804
  };
20449
- const PADDING_PROP_NAMES = ["padding", "paddingX", "paddingY", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft"];
20450
- const isNonZeroSpacing = value => {
20805
+ const PADDING_PROP_SET = new Set(["padding", "paddingX", "paddingY", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft"]);
20806
+ /* What makes a box the frame at its spot: what it paints of its own, and the
20807
+ padding holding its content away from its corners. Deliberately without the
20808
+ radius props: a radius alone paints nothing — it only says how a background
20809
+ or a border already there is cut — so a box carrying just a radius is still
20810
+ a wrapper around whatever draws. */
20811
+ const FRAME_PROP_SET = new Set(["background", "backgroundColor", "backgroundImage", "border", "borderTop", "borderRight", "borderBottom", "borderLeft", "borderWidth", "borderColor", "borderStyle", ...PADDING_PROP_SET]);
20812
+ const declaresSomething = value => {
20451
20813
  if (value === undefined || value === null || value === false) {
20452
20814
  return false;
20453
20815
  }
@@ -30042,6 +30404,13 @@ registerNaviCommand("--navi-unselect", (source, event) => {
30042
30404
  * observeAncestorOpenState for exactly how that's detected, and why it
30043
30405
  * matters that it happens before the browser paints.
30044
30406
  * - Inside an open ancestor → runs on mount AND every subsequent open.
30407
+ * - Inside the always-on-screen part of a *closed* one — a picker's façade,
30408
+ * an expandable's header, a <summary>: those elements are displayed the
30409
+ * whole time their ancestor reads as closed (aria-expanded on a trigger
30410
+ * describes the popup it controls, not its own contents). They run on
30411
+ * mount like anything else on screen, and the ancestor opening later
30412
+ * reveals nothing about them, so it does not re-run them either. See
30413
+ * isDisplayedDespiteClosedAncestor in @jsenv/dom.
30045
30414
  *
30046
30415
  * The callback's second argument is always a `navi_displayed` CustomEvent,
30047
30416
  * with `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
@@ -30074,6 +30443,10 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
30074
30443
  const callbackRef = useRef(callback);
30075
30444
  callbackRef.current = callback;
30076
30445
 
30446
+ // Set by the mount effect below for an element that lives in its openable
30447
+ // ancestor's façade rather than in what that ancestor opens.
30448
+ const displayedWhileAncestorClosedRef = useRef(false);
30449
+
30077
30450
  // Run on mount (or when deps change) — but only if the element is visible.
30078
30451
  useLayoutEffect(() => {
30079
30452
  const el = ref.current;
@@ -30086,9 +30459,16 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
30086
30459
  return;
30087
30460
  }
30088
30461
  if (!isAncestorOpen(ancestor)) {
30089
- // Ancestor is closed — skip now; the observeAncestorOpenState call
30090
- // below will fire once it opens.
30091
- return;
30462
+ if (!isDisplayedDespiteClosedAncestor(el)) {
30463
+ // Ancestor is closed and took this element off screen with it — skip
30464
+ // now; the observeAncestorOpenState call below will fire once it
30465
+ // opens.
30466
+ return;
30467
+ }
30468
+ // Closed, yet on screen: the ancestor is the trigger of what is
30469
+ // closed, not the thing itself, and this element belongs to the façade
30470
+ // it keeps showing.
30471
+ displayedWhileAncestorClosedRef.current = true;
30092
30472
  }
30093
30473
  callbackRef.current(el, createDisplayedEvent(ancestor, false));
30094
30474
  }, deps);
@@ -30107,6 +30487,12 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
30107
30487
  if (!isOpen) {
30108
30488
  return;
30109
30489
  }
30490
+ if (displayedWhileAncestorClosedRef.current) {
30491
+ // Façade content: on screen the whole time, so this opening reveals
30492
+ // nothing here — and `becauseAncestorOpened: true` about it would be
30493
+ // false in a way consumers act on (see use_auto_focus.js).
30494
+ return;
30495
+ }
30110
30496
  const lastEl = ref.current;
30111
30497
  callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
30112
30498
  });
@@ -30896,59 +31282,20 @@ const subscribeToControlState = (controlId, callback) => {
30896
31282
 
30897
31283
  const FormContext = createContext();
30898
31284
 
30899
- /**
30900
- * Named presets for the `charGuard` prop.
30901
- * Each value is a regex character class (including the [ ] delimiters).
30902
- */
30903
- const CHAR_CLASS_PRESETS = {
30904
- numeric: "[0-9]", // digits only
30905
- alpha: "[A-Za-z]", // letters only
30906
- alphanumeric: "[0-9A-Za-z]", // letters and digits
30907
- decimal: "[-0-9.,]", // digits, minus, dot, comma
30908
- uppercase: "[A-Z]", // uppercase letters only
30909
- tel: "[-0-9+() ]", // phone: digits, +, -, parens, space
30910
- email: "[a-zA-Z0-9._%+@-]", // email characters
30911
- card: "[0-9 ]", // credit card: digits and spaces
30912
- hex: "[0-9A-Fa-f]", // hexadecimal digits
30913
- pin: "[0-9]", // numeric PIN
30914
- postal: "[0-9A-Za-z -]", // postal code (FR, UK, US)
30915
- iban: "[0-9A-Z]", // IBAN: uppercase and digits
30916
- slug: "[a-z0-9-]", // URL slug
30917
- };
30918
-
30919
- // Specific i18n keys per preset — more informative than the generic fallback
30920
- const MESSAGE_KEY_FROM_PRESET = {
30921
- numeric: "constraint.guard.number",
30922
- pin: "constraint.guard.number",
30923
- alpha: "constraint.guard.alpha",
30924
- alphanumeric: "constraint.guard.alphanumeric",
30925
- uppercase: "constraint.guard.uppercase",
30926
- hex: "constraint.guard.hex",
30927
- slug: "constraint.guard.slug",
30928
- // tel, card, postal, iban, custom → generic fallback
30929
- };
30930
-
30931
- /**
30932
- * Returns the regex character class for a preset name, or the raw value as-is
30933
- * if it's not a known preset (allows custom classes like "[A-Z0-9_]").
30934
- */
30935
- const resolveCharClass = (value) => {
30936
- if (!value) return null;
30937
- return CHAR_CLASS_PRESETS[value] ?? value;
30938
- };
30939
-
30940
- /**
30941
- * Returns the i18n key for the char guard rejection message.
30942
- * Falls back to the generic "constraint.guard.chars" for custom classes and
30943
- * presets without a specific message.
30944
- */
30945
- const getCharGuardMessageKey = (value) => {
30946
- return MESSAGE_KEY_FROM_PRESET[value] ?? "constraint.guard.chars";
30947
- };
30948
-
30949
31285
  /**
30950
31286
  * Input guard — enforces character and length constraints during typing, paste,
30951
- * and external value sets.
31287
+ * and external value sets. What a character class holds and which sentence
31288
+ * refuses it comes from @jsenv/validity; the guard is what only a field can do,
31289
+ * blocking the keystroke before the value exists.
31290
+ *
31291
+ * The guard answers for the GESTURE, never for what the control already holds.
31292
+ * A value can arrive already outside the class or already too long — a
31293
+ * `defaultValue`, a signal, a value written from elsewhere — and refusing the
31294
+ * next keystroke over it would blame the person for a character they did not
31295
+ * type. So a change is refused only when it makes the value worse: one more
31296
+ * character outside the class, or one more character over the limit. What is
31297
+ * already there is the `charClass`/`maxLength` constraint's business, and it
31298
+ * says so at submit.
30952
31299
  *
30953
31300
  * The guard owns a single callout token (shared across all rejection reasons) so
30954
31301
  * successive rejections update the same callout rather than stacking.
@@ -30969,9 +31316,13 @@ const s = (n) => (n > 1 ? "s" : "");
30969
31316
  // Keydown: block only single printable characters that don't match the class.
30970
31317
  // Multi-character key names (Delete, ArrowLeft…) are always allowed.
30971
31318
  const getInvalidCharMessage = (char, { charClass, messageKey }) => {
30972
- if (char.length !== 1) return null;
30973
- if (new RegExp(charClass).test(char)) return null;
30974
- return naviI18n(messageKey);
31319
+ // Counted in code points: an astral character is one character typed, not two.
31320
+ const codePointCount = [...char].length;
31321
+ if (codePointCount !== 1) {
31322
+ return null;
31323
+ }
31324
+ if (compileCharClass(charClass).test(char)) return null;
31325
+ return naviI18nFromValidityMessage({ key: messageKey });
30975
31326
  };
30976
31327
 
30977
31328
  // Keydown: block when inserting one char would exceed maxLength.
@@ -30987,18 +31338,41 @@ const getMaxLengthInsertionMessage = (el, { maxLength }) => {
30987
31338
  });
30988
31339
  };
30989
31340
 
30990
- // Paste / set: block when value contains disallowed chars.
30991
- const getInvalidCharsMessage = (uiState, { charClass, messageKey }) => {
31341
+ const countCharsOutsideClass = (str, charClass) => {
31342
+ const regex = compileCharClass(charClass);
31343
+ let count = 0;
31344
+ for (const char of str) {
31345
+ if (!regex.test(char)) {
31346
+ count++;
31347
+ }
31348
+ }
31349
+ return count;
31350
+ };
31351
+
31352
+ // Paste / set: block when the gesture brings in a character the class refuses.
31353
+ const getInvalidCharsMessage = (
31354
+ uiState,
31355
+ { charClass, messageKey, uiStateNow },
31356
+ ) => {
30992
31357
  const str = uiState === undefined ? "" : String(uiState);
30993
- if (new RegExp(`^(?:${charClass})*$`).test(str)) return null;
30994
- return naviI18n(messageKey);
31358
+ if (compileCharClassAnchored(charClass).test(str)) return null;
31359
+ const strNow = uiStateNow === undefined ? "" : String(uiStateNow);
31360
+ if (
31361
+ countCharsOutsideClass(str, charClass) <=
31362
+ countCharsOutsideClass(strNow, charClass)
31363
+ ) {
31364
+ return null;
31365
+ }
31366
+ return naviI18nFromValidityMessage({ key: messageKey });
30995
31367
  };
30996
31368
 
30997
- // Paste / set: truncate when value exceeds maxLength.
30998
- const getLengthOverflowResult = (uiState, { maxLength }) => {
31369
+ // Paste / set: truncate what the gesture adds beyond the limit.
31370
+ const getLengthOverflowResult = (uiState, { maxLength, uiStateNow }) => {
30999
31371
  if (maxLength === undefined) return null;
31000
31372
  const str = uiState === undefined ? "" : String(uiState);
31001
31373
  if (str.length <= maxLength) return null;
31374
+ const strNow = uiStateNow === undefined ? "" : String(uiStateNow);
31375
+ if (str.length <= strNow.length) return null;
31002
31376
  return {
31003
31377
  fixedValue: str.slice(0, maxLength),
31004
31378
  message: naviI18n("constraint.guard.max_length.value", {
@@ -31037,7 +31411,7 @@ const createControlGuard = (controller) => {
31037
31411
 
31038
31412
  if (charGuard) {
31039
31413
  const charClass = resolveCharClass(charGuard);
31040
- const messageKey = getCharGuardMessageKey(charGuard);
31414
+ const messageKey = getCharClassMessageKey(charGuard);
31041
31415
  const charMsg = getInvalidCharMessage(e.key, { charClass, messageKey });
31042
31416
  if (charMsg) {
31043
31417
  show(charMsg, e);
@@ -31067,13 +31441,17 @@ const createControlGuard = (controller) => {
31067
31441
  */
31068
31442
  const checkUIState = (uiState, e) => {
31069
31443
  const { charGuard, maxLengthGuard } = controller.props;
31444
+ // What the control holds right now — setUIState has not written the new
31445
+ // value yet, and the paste path computes it without applying it.
31446
+ const uiStateNow = controller.uiState;
31070
31447
 
31071
31448
  if (charGuard) {
31072
31449
  const charClass = resolveCharClass(charGuard);
31073
- const messageKey = getCharGuardMessageKey(charGuard);
31450
+ const messageKey = getCharClassMessageKey(charGuard);
31074
31451
  const charsMsg = getInvalidCharsMessage(uiState, {
31075
31452
  charClass,
31076
31453
  messageKey,
31454
+ uiStateNow,
31077
31455
  });
31078
31456
  if (charsMsg) {
31079
31457
  show(charsMsg, e);
@@ -31084,6 +31462,7 @@ const createControlGuard = (controller) => {
31084
31462
  if (maxLengthGuard !== undefined) {
31085
31463
  const lengthResult = getLengthOverflowResult(uiState, {
31086
31464
  maxLength: maxLengthGuard,
31465
+ uiStateNow,
31087
31466
  });
31088
31467
  if (lengthResult) {
31089
31468
  show(lengthResult.message, e);
@@ -35111,13 +35490,15 @@ const splitControlProps = props => {
35111
35490
  };
35112
35491
  const controlRootProps = {};
35113
35492
  for (const key of Object.keys(props)) {
35114
- if (CONTROL_PROP_SET.has(key)) {
35115
- if (CONTROL_ATTRIBUTE_SET.has(key)) {
35116
- controlHostProps[key] = props[key];
35117
- }
35118
- } else {
35119
- controlRootProps[key] = props[key];
35493
+ const attributeName = controlAttributeFromProp(key);
35494
+ if (attributeName) {
35495
+ controlHostProps[attributeName] = props[key];
35496
+ continue;
35497
+ }
35498
+ if (isControlProp(key)) {
35499
+ continue;
35120
35500
  }
35501
+ controlRootProps[key] = props[key];
35121
35502
  }
35122
35503
  return [controlRootProps, controlHostProps];
35123
35504
  };
@@ -37217,6 +37598,7 @@ const TextAnchor = ({
37217
37598
  ref: anchorRef,
37218
37599
  className: "navi_text_anchor",
37219
37600
  "aria-hidden": "true",
37601
+ hidden: true,
37220
37602
  children: "\u200B"
37221
37603
  })]
37222
37604
  });
@@ -37884,8 +38266,10 @@ const shouldInjectSpacingBetween = (left, right) => {
37884
38266
  * @param {boolean} [emojiAsIcon]
37885
38267
  * Renders every emoji found in the string children as an `Icon`, so it sits
37886
38268
  * in the line like a character and never makes the line taller than the
37887
- * text. For free text a user typed (a message, a description) — the only
37888
- * place an emoji is expected; see `docs/typography.md`.
38269
+ * text. For free text a user typed (a message, a description); see
38270
+ * `docs/typography.md`. Only the strings this `Text` receives are rewritten —
38271
+ * a string a child component renders is out of reach, and that component
38272
+ * calls `renderEmojiAsIcon()` itself instead.
37889
38273
  *
37890
38274
  * @param {boolean} [preventSpaceUnderlines]
37891
38275
  * Replaces real space characters between children with padding-based spaces.
@@ -42702,7 +43086,16 @@ const createToggleEvent = open => {
42702
43086
  return toggleEvent;
42703
43087
  };
42704
43088
 
42705
- const DEFAULT_VALIDITY_STATE = { valid: true };
43089
+ const DEFAULT_VALIDITY_STATE = { valid: true, reported: null };
43090
+
43091
+ /**
43092
+ * The control's constraint validity, re-read whenever it changes:
43093
+ * `{ valid, reported, [constraintName]: null | failureInfo }`.
43094
+ *
43095
+ * `reported` names the constraint whose message the callout shows — the one
43096
+ * navi picks when several fail at once. A failure info carries `messageString`,
43097
+ * the sentence itself.
43098
+ */
42706
43099
  const useConstraintValidityState = (ref) => {
42707
43100
  const checkValue = () => {
42708
43101
  const element = ref.current;
@@ -46380,8 +46773,13 @@ installImportMetaCssBuild(import.meta);/**
46380
46773
  * "postal" → postal code (digits, letters, space, hyphen)
46381
46774
  * "iban" → IBAN (uppercase and digits)
46382
46775
  * "slug" → URL slug (lowercase, digits, hyphens)
46383
- * "[A-Z0-9]" any custom regex character class
46776
+ * "noEmoji" anything but an emoji
46777
+ * "[A-Z0-9]" → any custom regex character class, compiled with the `u`
46778
+ * flag: `\p{...}` is available, and an emoji counts as one
46779
+ * character rather than two halves.
46384
46780
  * inputMode and pattern are auto-derived from the preset when not explicitly set.
46781
+ * The presets come from @jsenv/validity, so the same name names the class a
46782
+ * server checks the value against (see docs/field_validation.md).
46385
46783
  *
46386
46784
  * - maxLengthGuard — combines maxLength + overflow guard in one prop.
46387
46785
  * Blocks keydown when the limit is reached; truncates on paste/set with an info callout.
@@ -59043,10 +59441,11 @@ const PendingScrollRefContext = createContext(null);
59043
59441
  // "muted" — keep in DOM, visible but opacified and still interactive
59044
59442
  const SearchNoMatchModeContext = createContext("remove");
59045
59443
 
59046
- // When total rendered items exceeds renderBudget, a render window [start, end)
59047
- // is activated to cap the number of DOM nodes. Items outside the window return
59048
- // null. The window slides as the user scrolls, using actual DOM positions
59049
- // (getBoundingClientRect) to find the first visible item no height estimation.
59444
+ // How many rows the list draws at once. Past that, a render window [start, end)
59445
+ // caps the number of DOM nodes: the window slides as the user scrolls, using
59446
+ // actual DOM positions (getBoundingClientRect) to find the first visible item
59447
+ // no height estimation. It frames the rows a run draws (see ListItems); items
59448
+ // declared one by one (<List.Item>) are all drawn, whatever the budget.
59050
59449
  const RENDER_BUDGET_DEFAULT = 100;
59051
59450
 
59052
59451
  // Attribute used on <li> elements rendered by ListItemReal so the scroll listener
@@ -59055,8 +59454,9 @@ const REAL_LIST_ITEM_SELECTOR = `[navi-list-item-real]`;
59055
59454
  // Rows standing in for content that has not arrived (see List's renderSkeleton).
59056
59455
  const SKELETON_LIST_ITEM_CLASS = "navi_list_item_skeleton";
59057
59456
 
59058
- // Carries the render window {start, end} (or null = render all) from
59059
- // List down to each ListItem.
59457
+ // Carries the render window {start, end} from List down to the runs of rows
59458
+ // inside it (see ListItems): a run draws the rows it frames and holds the room
59459
+ // of the others.
59060
59460
  const RenderWindowContext = createContext(null);
59061
59461
  // Carries List's own `columns` prop (a grid-template-columns value, e.g.
59062
59462
  // "1fr auto auto") down to each ListItem/filler/fallback so they can render
@@ -59893,12 +60293,46 @@ const ListUI = props => {
59893
60293
  searchText,
59894
60294
  horizontal
59895
60295
  });
60296
+
60297
+ // renderBudget frames the rows of a run; a list whose items are all declared
60298
+ // one by one draws every one of them, and the prop looks exactly like it is
60299
+ // doing something. Said once per list, when there is something drawn to
60300
+ // judge it on — a run mounting later (rows behind a loading state) is not a
60301
+ // list without one.
60302
+ const renderBudgetWarnedRef = useRef(false);
60303
+ useLayoutEffect(() => {
60304
+ if (props.renderBudget === undefined || renderBudgetWarnedRef.current) {
60305
+ return;
60306
+ }
60307
+ if (virtual.hasRuns() || tracker.itemsSignal.peek().length === 0) {
60308
+ return;
60309
+ }
60310
+ renderBudgetWarnedRef.current = true;
60311
+ console.warn(`List: renderBudget=${renderBudget} has no effect here. The render window frames the rows a run draws (<List.Items itemsAction>); items declared one by one (<List.Item>) are all rendered. Move the items to <List.Items> to cap the number of DOM nodes, or drop the prop.`);
60312
+ });
59896
60313
  virtual.captureAnchor = captureAnchor;
59897
60314
  virtual.virtualItemSizeSignal = virtualItemSizeSignal;
59898
60315
  virtual.horizontal = Boolean(horizontal);
59899
60316
  virtual.renderSkeleton = renderSkeleton;
60317
+
60318
+ // A row is addressed by id from outside (--navi-scroll, --navi-select): the
60319
+ // ones drawn have registered themselves with the tracker, and the ones a run
60320
+ // holds without drawing are known only to that run (see List.Items' row
60321
+ // locator). Both answer here, so a row is reachable whether or not the
60322
+ // window happens to frame it.
59900
60323
  const getItemById = itemId => {
59901
- return tracker.itemsSignal.peek().find(item => item.id === itemId);
60324
+ const itemDrawn = tracker.itemsSignal.peek().find(item => item.id === itemId);
60325
+ if (itemDrawn) {
60326
+ return itemDrawn;
60327
+ }
60328
+ const rowIndex = virtual.locateRow(itemId);
60329
+ if (rowIndex === null) {
60330
+ return undefined;
60331
+ }
60332
+ return {
60333
+ id: itemId,
60334
+ index: rowIndex
60335
+ };
59902
60336
  };
59903
60337
  const noMatchCount = tracker.noMatchCountSignal.value;
59904
60338
  // What the list stands for, which is not always what it holds: a run saying
@@ -62438,6 +62872,9 @@ const createListVirtual = () => {
62438
62872
  passId++;
62439
62873
  nextIndex = 0;
62440
62874
  },
62875
+ // Whether any run of rows lives in this list: what makes a render window
62876
+ // mean anything (see List's renderBudget).
62877
+ hasRuns: () => locatorByOwner.size > 0,
62441
62878
  setRowLocator: (ownerId, locate) => {
62442
62879
  locatorByOwner.set(ownerId, locate);
62443
62880
  },
@@ -62484,12 +62921,17 @@ const VISIBILITY_HIDDEN_STYLE = {
62484
62921
  * for them — which is what makes an infinitely scrolled list nothing more than
62485
62922
  * a list that says how many rows it has.
62486
62923
  *
62487
- * The rows come from `itemsAction(range)`: the run asks for what it is about to
62488
- * draw and keeps what it gets. The range says the same thing three ways, so a
62489
- * source can read it however it paginates — `{ start, end }` (places in the
62490
- * collection, a negative `start` counting back from the end like
62491
- * `Range: items=-25`, which is what a list opening on its last rows asks for
62492
- * before it knows how many there are), `limit` (how many rows), and
62924
+ * A collection held in memory is given whole: `items={rows}`. The run holds all
62925
+ * of them from the first render and never asks for anything it is still the
62926
+ * render window that decides how many are drawn.
62927
+ *
62928
+ * A collection read a slice at a time comes from `itemsAction(range)`: the run
62929
+ * asks for what it is about to draw and keeps what it gets. The range says the
62930
+ * same thing three ways, so a source can read it however it paginates —
62931
+ * `{ start, end }` (places in the collection, a negative `start` counting back
62932
+ * from the end like `Range: items=-25`, which is what a list opening on its
62933
+ * last rows asks for before it knows how many there are), `limit` (how many
62934
+ * rows), and
62493
62935
  * `before`/`after`/`around` (the id of a row to count from, for a source
62494
62936
  * paginating by cursor). Answer with the rows (an array — that is all of
62495
62937
  * them), or with a range the way a Content-Range does: `{ items, start, count }`
@@ -62503,11 +62945,11 @@ const VISIBILITY_HIDDEN_STYLE = {
62503
62945
  * copies of the JSON. The list holds the slices, the store holds the objects
62504
62946
  * (see docs/resource.md).
62505
62947
  *
62506
- * A collection held in memory answers synchronously: `itemsAction={() => rows}`.
62507
- * What it gives back is kept, so a collection that changes as a whole (a search
62508
- * reordering it) is a different collection: give the run a `key` that changes
62509
- * with it, the way one does for anything else that is not the same thing
62510
- * anymore.
62948
+ * A collection that changes as a whole (a search reordering it) is a different
62949
+ * collection: with `items`, another array is another collection and the run
62950
+ * draws it from its first row; with `itemsAction`, give the run a `key` that
62951
+ * changes with it, the way one does for anything else that is not the same
62952
+ * thing anymore.
62511
62953
  *
62512
62954
  * A row says what it is where it is drawn: `renderItem` returns a
62513
62955
  * `<List.Item>` carrying its own props (`selectable`, `value`, `selected`…),
@@ -62519,7 +62961,8 @@ const VISIBILITY_HIDDEN_STYLE = {
62519
62961
  *
62520
62962
  * @type {import("ignore:preact").FunctionComponent<{
62521
62963
  * renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("ignore:preact").ComponentChildren,
62522
- * itemsAction: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
62964
+ * items?: any[],
62965
+ * itemsAction?: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
62523
62966
  * count?: number,
62524
62967
  * groupBy?: (item: any, index: number) => any,
62525
62968
  * renderGroupLabel?: (item: any, index: number) => import("ignore:preact").ComponentChildren,
@@ -62534,6 +62977,13 @@ const VISIBILITY_HIDDEN_STYLE = {
62534
62977
  * What one row is, given the item and where it sits. `state.refreshing` says
62535
62978
  * the rows drawn are the ones from before while the run reads the collection
62536
62979
  * again — the list carries `navi-refreshing` for the same reason.
62980
+ * @param {any[]} [props.items]
62981
+ * The collection, when it is held in memory: all of it, in order. Nothing is
62982
+ * ever asked for — `itemsAction`, `count`, `pageSize` and `memoryBudget` have
62983
+ * no part to play, and no row is ever a skeleton.
62984
+ * @param {(range: object) => any} [props.itemsAction]
62985
+ * Where the rows come from when the collection is read a slice at a time:
62986
+ * a resource's range reader (`RESOURCE.GET_RANGE.bindParams(...)`).
62537
62987
  * @param {(item: any, index: number) => any} [props.groupBy]
62538
62988
  * What tells rows that belong together apart from the others — the day of a
62539
62989
  * message, the month of a game. Consecutive rows sharing it are wrapped in a
@@ -62556,7 +63006,8 @@ const VISIBILITY_HIDDEN_STYLE = {
62556
63006
  * How many rows the run keeps in memory. Past that, the ones far from what is
62557
63007
  * on screen are dropped (and asked for again if the user goes back) — the
62558
63008
  * same trade the render window makes with the DOM, one order of magnitude
62559
- * further out. `0` keeps everything.
63009
+ * further out. `Infinity` keeps every row the run ever received; `0` keeps
63010
+ * only the ones around the window.
62560
63011
  * @param {false|(index: number) => any} [props.renderSkeleton]
62561
63012
  * What to draw for a row the run does not hold. Defaults to List's own
62562
63013
  * `renderSkeleton`, then to a bare `<List.Item skeleton>`; `false` leaves the
@@ -62577,6 +63028,7 @@ const VISIBILITY_HIDDEN_STYLE = {
62577
63028
  */
62578
63029
  const ListItems = ({
62579
63030
  renderItem,
63031
+ items,
62580
63032
  itemsAction,
62581
63033
  count,
62582
63034
  pageSize,
@@ -62593,6 +63045,7 @@ const ListItems = ({
62593
63045
  const renderWindow = useContext(RenderWindowContext);
62594
63046
  const separator = useContext(SeparatorContext);
62595
63047
  const store = useItemStore({
63048
+ items,
62596
63049
  count,
62597
63050
  itemsAction,
62598
63051
  memoryBudget,
@@ -62883,11 +63336,17 @@ const rangeIsSame = (a, b) => {
62883
63336
  // are in all is enough to place it, so the pages need not be contiguous nor
62884
63337
  // arrive in order.
62885
63338
  const useItemStore = ({
63339
+ items,
62886
63340
  count,
62887
63341
  itemsAction,
62888
63342
  memoryBudget,
62889
63343
  onRequestStateChange
62890
63344
  }) => {
63345
+ // A collection given whole (`items`) is held from the first render: nothing
63346
+ // to ask for, nothing to keep across mounts, nothing to invalidate. The rest
63347
+ // of the store then never has a hole to fill, so it stays inert on its own —
63348
+ // the asking below finds nothing missing.
63349
+ const inMemory = items !== undefined;
62891
63350
  // The run's asking, on the same channel as the window it asks for — they are
62892
63351
  // one subject: what the list is about to draw is what it goes to fetch (see
62893
63352
  // `useRequestMissing`, and `updateRenderWindow` which logs the other half).
@@ -62909,8 +63368,28 @@ const useItemStore = ({
62909
63368
  // the reader holds for it (see resource_range_reader.js).
62910
63369
  useRef(false);
62911
63370
  const pagesRef = useRef(null);
63371
+ const itemsRef = useRef(items);
63372
+ const itemsHeldRef = useRef(false);
62912
63373
  let restored = false;
62913
- if (!pagesRef.current) {
63374
+ if (inMemory) {
63375
+ // The array as a whole is the collection: another array is another
63376
+ // collection, drawn from its first row (which is also why a run reading a
63377
+ // collection that changes as a whole takes a key).
63378
+ if (!pagesRef.current || itemsRef.current !== items) {
63379
+ itemsRef.current = items;
63380
+ const byIndex = new Map();
63381
+ let index = 0;
63382
+ while (index < items.length) {
63383
+ byIndex.set(index, items[index]);
63384
+ index++;
63385
+ }
63386
+ pagesRef.current = {
63387
+ byIndex,
63388
+ count: items.length
63389
+ };
63390
+ itemsHeldRef.current = false;
63391
+ }
63392
+ } else if (!pagesRef.current) {
62914
63393
  const composition = typeof itemsAction === "function" && itemsAction.readComposition ? itemsAction.readComposition() : null;
62915
63394
  if (composition && composition.count !== undefined) {
62916
63395
  pagesRef.current = composition;
@@ -62957,6 +63436,16 @@ const useItemStore = ({
62957
63436
  // range askable again (see the request memory just above).
62958
63437
  const [failure, setFailure] = useState(null);
62959
63438
  const virtual = useContext(ListVirtualContext);
63439
+ // The rows are there, which is what the list waits for to place itself on the
63440
+ // row it is held at (see placeWhereHeld). Said from an effect: a signal read
63441
+ // during this very render must not be written during it.
63442
+ useLayoutEffect(() => {
63443
+ if (!inMemory || itemsHeldRef.current) {
63444
+ return;
63445
+ }
63446
+ itemsHeldRef.current = true;
63447
+ virtual.pagesSignal.value = virtual.pagesSignal.peek() + 1;
63448
+ });
62960
63449
  // Before the first answer a run does not know how many rows it stands for.
62961
63450
  // It stands for a windowful of them: a list that is about to be filled looks
62962
63451
  // like rows on their way, not like an empty list.
@@ -63029,10 +63518,12 @@ const useItemStore = ({
63029
63518
  // dropped and simply asked for again if the user goes back — the same
63030
63519
  // trade the render window makes, one order of magnitude further out.
63031
63520
  forget: (windowFrom, windowTo) => {
63032
- const budget = memoryBudget === undefined ? ITEM_STORE_MAX_DEFAULT : memoryBudget;
63033
- if (!budget) {
63521
+ if (inMemory) {
63522
+ // Dropping a row here would drop it for good: there is no source to
63523
+ // ask it back from.
63034
63524
  return;
63035
63525
  }
63526
+ const budget = memoryBudget === undefined ? ITEM_STORE_MAX_DEFAULT : memoryBudget;
63036
63527
  const keepFrom = windowFrom - ITEM_STORE_KEEP_AROUND;
63037
63528
  const keepTo = windowTo + ITEM_STORE_KEEP_AROUND;
63038
63529
  if (pages.byIndex.size > budget) {
@@ -67469,11 +67960,18 @@ const css$r = /* css */`
67469
67960
  99999 fallback means "no cap" without needing a conditional rule. */
67470
67961
  max-height: calc(var(--textarea-max-rows, 99999) * 1lh);
67471
67962
  field-sizing: content;
67472
- /* Explicit, never normal: minRows/maxRows are lengths in lh, and with
67473
- line-height normal the lh unit resolves to a theoretical value that
67474
- does not match the real rendered line the box then jumps by a few
67475
- pixels the moment the first character replaces the theory with a real
67476
- line. One number for both keeps every row count exact. */
67963
+ /* Explicit, never normal, for two independent reasons.
67964
+ minRows/maxRows are lengths in lh, and with line-height normal the lh
67965
+ unit resolves to a theoretical value that does not match the real
67966
+ rendered line the box then jumps by a few pixels the moment the first
67967
+ character replaces the theory with a real line.
67968
+ And a line box under "normal" takes the height of the tallest font it
67969
+ holds, so the one line carrying an emoji stands taller than the ones
67970
+ around it — here, where the text is typed and no glyph can be wrapped
67971
+ the way emojiAsIcon wraps one, the line height is the only lever.
67972
+ 1.5 is also tall enough to contain an emoji's own box, so it is not
67973
+ clipped either; a tighter value would keep the rows even and cut the
67974
+ glyph. See docs/typography.md. */
67477
67975
  line-height: 1.5;
67478
67976
  /* The control grows itself; resizable below hands the handle back. */
67479
67977
  resize: none;
@@ -78739,5 +79237,5 @@ const UserSvg = () => jsx("svg", {
78739
79237
  })
78740
79238
  });
78741
79239
 
78742
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, renderEmojiAsIcon, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
79240
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, renderEmojiAsIcon, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
78743
79241
  //# sourceMappingURL=jsenv_navi.js.map