@jsenv/navi 0.29.101 → 0.29.103

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.
@@ -49,6 +49,16 @@ const css$15 = /* css */`
49
49
  --navi-z-index-control-hovered: 1;
50
50
  --navi-z-index-control-focused: 2;
51
51
 
52
+ /* A control the user is not on and yet still has to paint in front: one
53
+ holding something open (a picker showing its list, an expandable
54
+ header). Its border keeps the color the open state gives it, while
55
+ hover and focus have both moved on — into the popup, or onto whatever
56
+ the pointer travelled to next — so neither of the two values above is
57
+ there to raise it, and the neighbour painted after it cuts the very
58
+ border that says what is open. Above both: the member holding the
59
+ popup open outranks a member merely hovered or focused. */
60
+ --navi-z-index-control-expanded: 3;
61
+
52
62
  /* Kept stuck while something scrolls under it: a list header, the head
53
63
  and foot of a side panel, a table's sticky cells, the header and
54
64
  footer of any scrolling Box. Above raised controls — a control
@@ -1329,6 +1339,10 @@ naviI18n.addAll({
1329
1339
  en: "More actions",
1330
1340
  fr: "Autres actions",
1331
1341
  },
1342
+ "button.remove": {
1343
+ en: "Remove",
1344
+ fr: "Retirer",
1345
+ },
1332
1346
  });
1333
1347
 
1334
1348
  // Default built-in translations — apps can override any key via add()
@@ -1628,6 +1642,14 @@ naviI18n.addAll({
1628
1642
  fr: "Cette option n'est pas disponible.",
1629
1643
  en: "This option is not available.",
1630
1644
  },
1645
+ "constraint.readonly.selection": {
1646
+ fr: "La sélection ne peut plus être modifiée.",
1647
+ en: "This selection cannot be changed.",
1648
+ },
1649
+ "constraint.readonly.choice": {
1650
+ fr: "Ce choix ne peut plus être changé.",
1651
+ en: "This choice cannot be changed.",
1652
+ },
1631
1653
  "constraint.readonly.item": {
1632
1654
  fr: "Cet élément n'est pas disponible.",
1633
1655
  en: "This item is not available.",
@@ -2104,6 +2126,276 @@ naviI18n.addAll({
2104
2126
 
2105
2127
  const FormContext = createContext();
2106
2128
 
2129
+ const CONSTRAINT_ATTRIBUTE_SET = new Set();
2130
+
2131
+ const CONSTRAINT_NAME_TO_PROP = {
2132
+ disabled: "disabledMessage",
2133
+ required: "requiredMessage",
2134
+ pattern: "patternMessage",
2135
+ type_email: "typeMessage",
2136
+ type_number: "typeMessage",
2137
+ min_length: "minLengthMessage",
2138
+ max_length: "maxLengthMessage",
2139
+ min: "minMessage",
2140
+ max: "maxMessage",
2141
+ single_space: "singleSpaceMessage",
2142
+ same_as: "sameAsMessage",
2143
+ min_lower_letter: "minLowerLetterMessage",
2144
+ min_upper_letter: "minUpperLetterMessage",
2145
+ min_digit: "minDigitMessage",
2146
+ min_special_char: "minSpecialCharMessage",
2147
+ one_of: "oneOfMessage",
2148
+ readonly: "readOnlyMessage",
2149
+ available: "availableMessage",
2150
+ };
2151
+
2152
+ const CONSTRAINT_MESSAGE_PROP_NAME_SET = new Set(
2153
+ Object.values(CONSTRAINT_NAME_TO_PROP),
2154
+ );
2155
+
2156
+ const extractMessageAndRemainingProps = (props) => {
2157
+ const ownMessages = {};
2158
+ const remaining = {};
2159
+ const keyToVisit = new Set(Object.keys(props));
2160
+ for (const key of keyToVisit) {
2161
+ if (CONSTRAINT_MESSAGE_PROP_NAME_SET.has(key)) {
2162
+ ownMessages[key] = props[key];
2163
+ } else {
2164
+ remaining[key] = props[key];
2165
+ }
2166
+ }
2167
+ return [ownMessages, remaining];
2168
+ };
2169
+
2170
+ const getConstraintMessage = (
2171
+ controller,
2172
+ constraint,
2173
+ generatedMessage,
2174
+ { requester },
2175
+ ) => {
2176
+ const { name: constraintName } = constraint;
2177
+ const propName = CONSTRAINT_NAME_TO_PROP[constraintName];
2178
+
2179
+ // 1. Search first on the requester (e.g. the <li> that was clicked),
2180
+ // then fall back to element (e.g. the hidden <input>).
2181
+ if (requester) {
2182
+ const requesterController = requester.__uiStateController__;
2183
+ if (requesterController && requesterController !== controller) {
2184
+ const requesterControllerMessage = requesterController.props[propName];
2185
+ if (requesterControllerMessage) {
2186
+ return {
2187
+ message: requesterControllerMessage,
2188
+ origin: "requester controller",
2189
+ };
2190
+ }
2191
+ }
2192
+ }
2193
+
2194
+ const controllerMessage = controller.props[propName];
2195
+ if (controllerMessage) {
2196
+ return {
2197
+ message: controllerMessage,
2198
+ origin: "controller",
2199
+ };
2200
+ }
2201
+
2202
+ return {
2203
+ message: generatedMessage,
2204
+ origin: "generated message",
2205
+ };
2206
+ };
2207
+
2208
+ // prop that we'll set on the control
2209
+ const CONTROL_ATTRIBUTE_SET = new Set([
2210
+ ...CONSTRAINT_ATTRIBUTE_SET,
2211
+
2212
+ "ref",
2213
+ "children",
2214
+ "id",
2215
+ "name",
2216
+ "type",
2217
+ "value",
2218
+ "checked",
2219
+ "placeholder",
2220
+ "inputMode",
2221
+ "autoComplete",
2222
+ "spellcheck",
2223
+ "autoCorrect",
2224
+ "aria-controls",
2225
+ "tabIndex",
2226
+ "command",
2227
+ "commandFor",
2228
+ "command-value", // not standard but make sense, allow to give param to the command in question
2229
+ "list",
2230
+
2231
+ // "ui-action-target",
2232
+ "navi-input-type",
2233
+ "navi-value-pad",
2234
+ "navi-control-proxy-for",
2235
+ "navi-command-proxy-for",
2236
+ "navi-command-target",
2237
+ "onnavi_command",
2238
+ "onnavi_request_open",
2239
+ "onnavi_request_close",
2240
+
2241
+ "data-callout-arrow-x",
2242
+ "data-callout-point-to-border-box",
2243
+ "data-callout-point-to-content-box",
2244
+ "data-callout-viewport-spacing",
2245
+ "data-callout-position",
2246
+ "data-callout-position-fixed",
2247
+
2248
+ "data-testid", // playwright, cypress
2249
+ "data-separator", // used by InputGroup paste-to-fill
2250
+ ]);
2251
+ // prop concerning control but that won't end up in the DOM if not inside CONTROL_ATTRIBUTE_SET
2252
+ const CONTROL_PROP_SET = new Set([
2253
+ ...CONTROL_ATTRIBUTE_SET,
2254
+ ...CONSTRAINT_MESSAGE_PROP_NAME_SET,
2255
+
2256
+ "action",
2257
+ "confirm",
2258
+ "confirmPopupContent",
2259
+ "actionEvent",
2260
+ "actionAfterChange",
2261
+ "actionOnMouseDown",
2262
+ "actionDebounce",
2263
+ // A signal bound two-way to the control: its value seeds the control's state and
2264
+ // is written back on every uiAction. Precludes value/checked (see createControlInfo).
2265
+ "signal",
2266
+ "defaultValue",
2267
+ "defaultChecked",
2268
+ "readOnly", // will depend wether readOnly is supported
2269
+
2270
+ "loading",
2271
+ "basePseudoState",
2272
+ "constraints",
2273
+
2274
+ // A real target inside a zone that belongs to another control — see
2275
+ // own_target.js.
2276
+ "ownTarget",
2277
+
2278
+ "autoFocus",
2279
+ "autoFocusVisible",
2280
+ "autoFocusSelect",
2281
+
2282
+ "onMouseDown",
2283
+ "onClick",
2284
+ "onKeyDown",
2285
+ "onPaste",
2286
+ "onInput",
2287
+ "eventReactionDefinitions",
2288
+
2289
+ "onCancel",
2290
+ "cancelOnBlurInvalid",
2291
+ "cancelOnEscape",
2292
+ "onActionPrevented",
2293
+ "onActionStart",
2294
+ "onActionAborted",
2295
+ "onActionError",
2296
+ "actionErrorEffect",
2297
+ "errorMapping",
2298
+ "onActionEnd",
2299
+
2300
+ "resetOnCancel",
2301
+ "resetOnAbort",
2302
+ "resetOnError",
2303
+ "optimistic",
2304
+
2305
+ "charGuard",
2306
+ "maxLengthGuard",
2307
+ ]);
2308
+
2309
+ const MessagePropsRefContext = createContext();
2310
+
2311
+ const ControlIdContext = createContext();
2312
+ const ControlNameContext = createContext();
2313
+ const DisabledContext = createContext();
2314
+ const ReadOnlyContext = createContext();
2315
+ const RequiredContext = createContext();
2316
+ const LoadingContext$1 = createContext();
2317
+ createContext();
2318
+
2319
+ const ActionContext = createContext();
2320
+ const ActionRequesterContext = createContext();
2321
+
2322
+ /**
2323
+ * A target of its own inside a zone that belongs to another control.
2324
+ *
2325
+ * A pressable row, a picker's façade, a slide that travels under the finger:
2326
+ * each of them answers a press that lands anywhere in its box. An affordance an
2327
+ * application draws in there — a chip's cross, an eye that opens a profile, a
2328
+ * diskette that saves a guest — is aimed AT, not merely inside, and the press
2329
+ * belongs to it alone.
2330
+ *
2331
+ * Saying that by hand takes three guards, one per moment of the same press: the
2332
+ * pointerdown where gestures are arbitrated, the mousedown where a picker opens,
2333
+ * the click where it opens too when a gesture disputed the press. All three are
2334
+ * navi's own knowledge of navi's own event flow, and an application that gets
2335
+ * one wrong finds out by opening a popup it meant to keep shut. `ownTarget` is
2336
+ * that knowledge, said once.
2337
+ *
2338
+ * The other half is interactivity: the affordance is a control, so it already
2339
+ * refuses on its own terms once the zone around it holds it read-only — but a
2340
+ * caller's `onClick` is plain DOM and fires before any gate. So an own target
2341
+ * withholds the caller's handler until its own gate has allowed it, and by
2342
+ * default goes rather than greys: a remove cross that still removes is worse
2343
+ * than no cross, and one that refuses politely still says "there is something to
2344
+ * remove here" on a row that is only being read.
2345
+ */
2346
+
2347
+
2348
+ const OWN_TARGET_ATTRIBUTE = "data-navi-own-target";
2349
+
2350
+ /**
2351
+ * Whether `event` was aimed at an own target sitting below this control — in
2352
+ * which case the control is not what the press was for and must not answer it.
2353
+ *
2354
+ * Read from the event's target rather than from a mark left by a handler: the
2355
+ * question is "who is this press for", and the DOM between the pointer and the
2356
+ * control is the whole answer. Nothing is asked of the own target itself, which
2357
+ * is what lets it be anything — a button, a link, a field.
2358
+ */
2359
+ const isAimedAtOwnTargetBelow = (event, controlHost) => {
2360
+ const target = event?.target;
2361
+ if (!target || typeof target.closest !== "function") {
2362
+ return false;
2363
+ }
2364
+ const ownTarget = target.closest(`[${OWN_TARGET_ATTRIBUTE}]`);
2365
+ if (!ownTarget) {
2366
+ return false;
2367
+ }
2368
+ // The claim is against what is ABOVE it: the control that IS the own target,
2369
+ // and any control living inside it, are being aimed at like anything else.
2370
+ if (ownTarget === controlHost || ownTarget.contains(controlHost)) {
2371
+ return false;
2372
+ }
2373
+ // From the root rather than the host: a layered control (a picker holding an
2374
+ // input) has its gate on the input, and what the application drew sits beside
2375
+ // it, not in it.
2376
+ const controlRoot = controlHost.closest("[navi-control]") || controlHost;
2377
+ return controlRoot.contains(ownTarget);
2378
+ };
2379
+
2380
+ /**
2381
+ * Whether an own target has nothing to offer where it sits: the zone around it
2382
+ * is read-only, disabled or busy, so the affordance goes.
2383
+ *
2384
+ * `ownTarget="refuse"` keeps it on screen instead, refusing with a callout like
2385
+ * every other navi control — for an affordance whose presence is information in
2386
+ * itself (an eye that opens a profile is worth seeing on a row being read).
2387
+ */
2388
+ const useOwnTargetHidden = (props) => {
2389
+ const disabled = useContext(DisabledContext);
2390
+ const readOnly = useContext(ReadOnlyContext);
2391
+ const loading = useContext(LoadingContext$1);
2392
+ const { ownTarget } = props;
2393
+ if (!ownTarget || ownTarget === "refuse") {
2394
+ return false;
2395
+ }
2396
+ return Boolean(disabled || readOnly || loading);
2397
+ };
2398
+
2107
2399
  /*
2108
2400
  * Deep structural equality for arbitrary JS values — what `===` can't do but this
2109
2401
  * codebase constantly needs: memoization cache keys ({ id: 1 } equal to { id: 1 }),
@@ -6480,83 +6772,6 @@ const isInertOnClick = (element) => {
6480
6772
  return true;
6481
6773
  };
6482
6774
 
6483
- const CONSTRAINT_NAME_TO_PROP = {
6484
- disabled: "disabledMessage",
6485
- required: "requiredMessage",
6486
- pattern: "patternMessage",
6487
- type_email: "typeMessage",
6488
- type_number: "typeMessage",
6489
- min_length: "minLengthMessage",
6490
- max_length: "maxLengthMessage",
6491
- min: "minMessage",
6492
- max: "maxMessage",
6493
- single_space: "singleSpaceMessage",
6494
- same_as: "sameAsMessage",
6495
- min_lower_letter: "minLowerLetterMessage",
6496
- min_upper_letter: "minUpperLetterMessage",
6497
- min_digit: "minDigitMessage",
6498
- min_special_char: "minSpecialCharMessage",
6499
- one_of: "oneOfMessage",
6500
- readonly: "readOnlyMessage",
6501
- available: "availableMessage",
6502
- };
6503
-
6504
- const CONSTRAINT_MESSAGE_PROP_NAME_SET = new Set(
6505
- Object.values(CONSTRAINT_NAME_TO_PROP),
6506
- );
6507
-
6508
- const extractMessageAndRemainingProps = (props) => {
6509
- const ownMessages = {};
6510
- const remaining = {};
6511
- const keyToVisit = new Set(Object.keys(props));
6512
- for (const key of keyToVisit) {
6513
- if (CONSTRAINT_MESSAGE_PROP_NAME_SET.has(key)) {
6514
- ownMessages[key] = props[key];
6515
- } else {
6516
- remaining[key] = props[key];
6517
- }
6518
- }
6519
- return [ownMessages, remaining];
6520
- };
6521
-
6522
- const getConstraintMessage = (
6523
- controller,
6524
- constraint,
6525
- generatedMessage,
6526
- { requester },
6527
- ) => {
6528
- const { name: constraintName } = constraint;
6529
- const propName = CONSTRAINT_NAME_TO_PROP[constraintName];
6530
-
6531
- // 1. Search first on the requester (e.g. the <li> that was clicked),
6532
- // then fall back to element (e.g. the hidden <input>).
6533
- if (requester) {
6534
- const requesterController = requester.__uiStateController__;
6535
- if (requesterController && requesterController !== controller) {
6536
- const requesterControllerMessage = requesterController.props[propName];
6537
- if (requesterControllerMessage) {
6538
- return {
6539
- message: requesterControllerMessage,
6540
- origin: "requester controller",
6541
- };
6542
- }
6543
- }
6544
- }
6545
-
6546
- const controllerMessage = controller.props[propName];
6547
- if (controllerMessage) {
6548
- return {
6549
- message: controllerMessage,
6550
- origin: "controller",
6551
- };
6552
- }
6553
-
6554
- return {
6555
- message: generatedMessage,
6556
- origin: "generated message",
6557
- };
6558
- };
6559
-
6560
6775
  /**
6561
6776
  * DOM utilities for the proxy control pattern.
6562
6777
  *
@@ -8231,8 +8446,6 @@ const ABORTED = { id: "aborted" };
8231
8446
  const FAILED = { id: "failed" };
8232
8447
  const COMPLETED = { id: "completed" };
8233
8448
 
8234
- const CONSTRAINT_ATTRIBUTE_SET = new Set();
8235
-
8236
8449
  const BUSY_CONSTRAINT = {
8237
8450
  name: "busy",
8238
8451
  messageAttribute: "data-busy-message",
@@ -9977,7 +10190,7 @@ const formatDateIso = (iso, inputType) => {
9977
10190
  const READONLY_CONSTRAINT = {
9978
10191
  name: "readonly",
9979
10192
  messageAttribute: "data-readonly-message",
9980
- check: (field) => {
10193
+ check: (field, { intent } = {}) => {
9981
10194
  const readOnly = Boolean(
9982
10195
  field.controlHostProps.readOnly ||
9983
10196
  field.controlHostProps["aria-readonly"] === "true",
@@ -9986,6 +10199,18 @@ const READONLY_CONSTRAINT = {
9986
10199
  return null;
9987
10200
  }
9988
10201
 
10202
+ // Read-only, and what it opens still opens: a picker's answer often lives
10203
+ // in a shape only its popup draws — a plan with one tile ringed, a wheel
10204
+ // stopped on a time — and refusing to open leaves that shape unreadable.
10205
+ // Opening reads and nothing more, so it goes through; everything that would
10206
+ // write is refused below, and the popup content is handed the same
10207
+ // read-only state so each control in there refuses on its own terms. Which
10208
+ // controls say so, and when, is theirs to answer (see createControlInfo's
10209
+ // readOnlyOpens).
10210
+ if (intent === "read" && field.readOnlyOpens) {
10211
+ return null;
10212
+ }
10213
+
9989
10214
  // A selection guarding its length (see maxLengthGuard) is what holds this
9990
10215
  // one back, so max_length is what refuses it: same name, same message, same
9991
10216
  // `maxLengthMessage` to say it in the caller's own words. Read-only is only
@@ -10050,6 +10275,8 @@ const readOnlyMessage = (field) => {
10050
10275
  * → "navi_request_interaction" event
10051
10276
  * → onRequestInteraction
10052
10277
  * → check disabled / read-only / busy (via controller.controlInteraction)
10278
+ * against the interaction's `intent` ("write" by default, "read" for one
10279
+ * that only shows what is already there — see READONLY_CONSTRAINT)
10053
10280
  * → if blocked → prevented()
10054
10281
  * → if allowed → allowed()
10055
10282
  * → (in allowed callback) setUIState(value)
@@ -10083,10 +10310,10 @@ const createControlInteraction = (
10083
10310
  // The title this rule put on the element, if any (see checkInteractivity).
10084
10311
  let titleWritten = null;
10085
10312
 
10086
- const checkInteractivity = ({ event } = {}) => {
10313
+ const checkInteractivity = ({ event, intent = "write" } = {}) => {
10087
10314
  interactionFailedConstraintInfo = null;
10088
10315
  for (const constraint of INTERACTION_CONSTRAINT_SET) {
10089
- const checkResult = constraint.check(controller);
10316
+ const checkResult = constraint.check(controller, { intent });
10090
10317
  if (!checkResult) {
10091
10318
  continue;
10092
10319
  }
@@ -10113,7 +10340,7 @@ const createControlInteraction = (
10113
10340
  if (!mci) {
10114
10341
  continue;
10115
10342
  }
10116
- const canInteract = mci.checkInteractivity({ event });
10343
+ const canInteract = mci.checkInteractivity({ event, intent });
10117
10344
  if (canInteract) {
10118
10345
  continue;
10119
10346
  }
@@ -10127,8 +10354,14 @@ const createControlInteraction = (
10127
10354
  }
10128
10355
 
10129
10356
  // Keep title attribute in sync for accessibility.
10357
+ // Only off a check that asked the general question: a title is read
10358
+ // whenever a pointer rests on the element, so it says what is true of the
10359
+ // control as a whole. A check made for an interaction that only reads (a
10360
+ // read-only picker asked to open its popup) answers about that one
10361
+ // interaction — writing the title from it would take away the "read-only"
10362
+ // the first time someone opened the popup.
10130
10363
  const titleLess = !controller.controlHostProps?.title;
10131
- if (titleLess) {
10364
+ if (intent === "write" && titleLess) {
10132
10365
  const element = controller.ref.current;
10133
10366
  if (element) {
10134
10367
  if (interactionFailedConstraintInfo) {
@@ -10234,6 +10467,12 @@ const onRequestInteraction = (
10234
10467
  const {
10235
10468
  event,
10236
10469
  name,
10470
+ // What this interaction would do to the control: write it, or only read it.
10471
+ // Everything writes unless it says otherwise — an interaction that merely
10472
+ // shows what is already there (opening a picker's popup, closing it again)
10473
+ // says "read", and that is what a control held read-only can still let
10474
+ // through (see READONLY_CONSTRAINT).
10475
+ intent = "write",
10237
10476
  bypassInteractivity = false,
10238
10477
  prevented,
10239
10478
  allowed,
@@ -10259,12 +10498,26 @@ const onRequestInteraction = (
10259
10498
 
10260
10499
  const currentTarget = requestInteractionCustomEvent.currentTarget;
10261
10500
  const controlHost = findControlHost(currentTarget) || currentTarget;
10501
+
10502
+ // Aimed at something else that lives in this control's box: a chip's cross, an
10503
+ // eye, a diskette. The press is that affordance's alone (see own_target.js),
10504
+ // and stepping back here — rather than stopping the propagation over there —
10505
+ // is what leaves the event whole for everything that is not a navi
10506
+ // interaction. Stepping back, not refusing: the reaction never happened, so
10507
+ // its `prevented`/`always` (an `e.preventDefault()`, for most of them) have
10508
+ // nothing to undo and would take the press from the affordance itself.
10509
+ if (isAimedAtOwnTargetBelow(event, controlHost)) {
10510
+ debugInteraction(event, `"${name}" is for an own target below`);
10511
+ requestInteractionCustomEvent.preventDefault();
10512
+ return false;
10513
+ }
10514
+
10262
10515
  const controller = controlHost.__uiStateController__;
10263
10516
 
10264
10517
  if (controller && !bypassInteractivity) {
10265
10518
  const ci = controller?.rules.interaction;
10266
10519
  if (ci) {
10267
- const canInteract = ci.checkInteractivity({ event });
10520
+ const canInteract = ci.checkInteractivity({ event, intent });
10268
10521
  if (!canInteract) {
10269
10522
  const failedInfo =
10270
10523
  ci.interactionFailedConstraintInfo ??
@@ -16049,6 +16302,26 @@ const POSITION_PROPS = {
16049
16302
  return { transform: `skew(${value})` };
16050
16303
  },
16051
16304
  };
16305
+ const singleLineEllipsisStyles = () => {
16306
+ return {
16307
+ overflow: "hidden",
16308
+ textOverflow: "ellipsis",
16309
+ overflowWrap: "normal",
16310
+ };
16311
+ };
16312
+ // The lines beyond the clamp are still laid out, so the clip edge decides
16313
+ // whether the top of the first hidden one is visible: "overflow: hidden" clips
16314
+ // at the padding box and lets it show inside the block-end padding. Clipping at
16315
+ // the content box instead ends the element right after its last visible line.
16316
+ const lineClampStyles = (value) => {
16317
+ return {
16318
+ "overflow": "clip",
16319
+ "overflowClipMargin": "content-box",
16320
+ "display": "-webkit-box",
16321
+ "-webkit-box-orient": "vertical",
16322
+ "-webkit-line-clamp": value,
16323
+ };
16324
+ };
16052
16325
  const TYPO_PROPS = {
16053
16326
  font: applyOnCSSProp("fontFamily"),
16054
16327
  fontFamily: PASS_THROUGH,
@@ -16086,39 +16359,21 @@ const TYPO_PROPS = {
16086
16359
  return null;
16087
16360
  }
16088
16361
  if (value === 1 || value === "1") {
16089
- return {
16090
- overflow: "hidden",
16091
- textOverflow: "ellipsis",
16092
- overflowWrap: "normal",
16093
- };
16362
+ return singleLineEllipsisStyles();
16094
16363
  }
16095
- return {
16096
- "overflow": "hidden",
16097
- "display": "-webkit-box",
16098
- "-webkit-box-orient": "vertical",
16099
- "-webkit-line-clamp": value,
16100
- };
16364
+ return lineClampStyles(value);
16101
16365
  },
16102
16366
  overflowEllipsis: (value) => {
16103
16367
  if (!value) {
16104
16368
  return null;
16105
16369
  }
16106
- return {
16107
- overflow: "hidden",
16108
- textOverflow: "ellipsis",
16109
- overflowWrap: "normal",
16110
- };
16370
+ return singleLineEllipsisStyles();
16111
16371
  },
16112
16372
  lineClamp: (value) => {
16113
16373
  if (!value) {
16114
16374
  return null;
16115
16375
  }
16116
- return {
16117
- "overflow": "hidden",
16118
- "display": "-webkit-box",
16119
- "-webkit-box-orient": "vertical",
16120
- "-webkit-line-clamp": value,
16121
- };
16376
+ return lineClampStyles(value);
16122
16377
  },
16123
16378
  textAlign: PASS_THROUGH,
16124
16379
  textBox: PASS_THROUGH,
@@ -16164,6 +16419,7 @@ const VISUAL_PROPS = {
16164
16419
  overflow: PASS_THROUGH,
16165
16420
  overflowX: PASS_THROUGH,
16166
16421
  overflowY: PASS_THROUGH,
16422
+ overflowClipMargin: PASS_THROUGH,
16167
16423
  objectFit: PASS_THROUGH,
16168
16424
  accentColor: PASS_THROUGH,
16169
16425
  scrollbarWidth: PASS_THROUGH,
@@ -23431,116 +23687,6 @@ registerNaviCommand("--navi-unselect", (source, event) => {
23431
23687
  };
23432
23688
  });
23433
23689
 
23434
- // prop that we'll set on the control
23435
- const CONTROL_ATTRIBUTE_SET = new Set([
23436
- ...CONSTRAINT_ATTRIBUTE_SET,
23437
-
23438
- "ref",
23439
- "children",
23440
- "id",
23441
- "name",
23442
- "type",
23443
- "value",
23444
- "checked",
23445
- "placeholder",
23446
- "inputMode",
23447
- "autoComplete",
23448
- "spellcheck",
23449
- "autoCorrect",
23450
- "aria-controls",
23451
- "tabIndex",
23452
- "command",
23453
- "commandFor",
23454
- "command-value", // not standard but make sense, allow to give param to the command in question
23455
- "list",
23456
-
23457
- // "ui-action-target",
23458
- "navi-input-type",
23459
- "navi-value-pad",
23460
- "navi-control-proxy-for",
23461
- "navi-command-proxy-for",
23462
- "navi-command-target",
23463
- "onnavi_command",
23464
- "onnavi_request_open",
23465
- "onnavi_request_close",
23466
-
23467
- "data-callout-arrow-x",
23468
- "data-callout-point-to-border-box",
23469
- "data-callout-point-to-content-box",
23470
- "data-callout-viewport-spacing",
23471
- "data-callout-position",
23472
- "data-callout-position-fixed",
23473
-
23474
- "data-testid", // playwright, cypress
23475
- "data-separator", // used by InputGroup paste-to-fill
23476
- ]);
23477
- // prop concerning control but that won't end up in the DOM if not inside CONTROL_ATTRIBUTE_SET
23478
- const CONTROL_PROP_SET = new Set([
23479
- ...CONTROL_ATTRIBUTE_SET,
23480
- ...CONSTRAINT_MESSAGE_PROP_NAME_SET,
23481
-
23482
- "action",
23483
- "confirm",
23484
- "confirmPopupContent",
23485
- "actionEvent",
23486
- "actionAfterChange",
23487
- "actionOnMouseDown",
23488
- "actionDebounce",
23489
- // A signal bound two-way to the control: its value seeds the control's state and
23490
- // is written back on every uiAction. Precludes value/checked (see createControlInfo).
23491
- "signal",
23492
- "defaultValue",
23493
- "defaultChecked",
23494
- "readOnly", // will depend wether readOnly is supported
23495
-
23496
- "loading",
23497
- "basePseudoState",
23498
- "constraints",
23499
-
23500
- "autoFocus",
23501
- "autoFocusVisible",
23502
- "autoFocusSelect",
23503
-
23504
- "onMouseDown",
23505
- "onClick",
23506
- "onKeyDown",
23507
- "onPaste",
23508
- "onInput",
23509
- "eventReactionDefinitions",
23510
-
23511
- "onCancel",
23512
- "cancelOnBlurInvalid",
23513
- "cancelOnEscape",
23514
- "onActionPrevented",
23515
- "onActionStart",
23516
- "onActionAborted",
23517
- "onActionError",
23518
- "actionErrorEffect",
23519
- "errorMapping",
23520
- "onActionEnd",
23521
-
23522
- "resetOnCancel",
23523
- "resetOnAbort",
23524
- "resetOnError",
23525
- "optimistic",
23526
-
23527
- "charGuard",
23528
- "maxLengthGuard",
23529
- ]);
23530
-
23531
- const MessagePropsRefContext = createContext();
23532
-
23533
- const ControlIdContext = createContext();
23534
- const ControlNameContext = createContext();
23535
- const DisabledContext = createContext();
23536
- const ReadOnlyContext = createContext();
23537
- const RequiredContext = createContext();
23538
- const LoadingContext$1 = createContext();
23539
- createContext();
23540
-
23541
- const ActionContext = createContext();
23542
- const ActionRequesterContext = createContext();
23543
-
23544
23690
  /**
23545
23691
  * How a control tells the labels pointing at it what it is (disabled, readOnly,
23546
23692
  * required) and when it goes away.
@@ -25762,18 +25908,18 @@ const useUIFacadeStateController = (props, realUIStateController) => {
25762
25908
  if (child !== firstChildControllerRef.current) {
25763
25909
  return;
25764
25910
  }
25765
- if (
25766
- silent &&
25767
- uiStateHoldsNothing(child.uiState) &&
25768
- !uiStateHoldsNothing(s.realUIStateController.uiState)
25769
- ) {
25911
+ if (silent && uiStateHoldsNothing(child.uiState)) {
25770
25912
  // A silent sync means the child's own structure changed (children
25771
25913
  // mounted/unmounted), not that the user acted. A child that ends up
25772
25914
  // with no value there is one that currently *cannot* express one —
25773
25915
  // a <List loading> holds no items yet, a popup whose items are gone
25774
25916
  // aggregates to the empty array its stateType falls back to — which
25775
25917
  // must not read as the user clearing the picker, nor fire its
25776
- // uiAction.
25918
+ // uiAction. And when the picker holds nothing either, there is
25919
+ // still nothing to adopt: the sync would only respell one nothing
25920
+ // as another (an array picker's [] becoming undefined, say) and
25921
+ // hand that to uiAction — an empty array picker opened its popup
25922
+ // and told its owner the value changed.
25777
25923
  return;
25778
25924
  }
25779
25925
  updatingRef.current = true;
@@ -25786,7 +25932,18 @@ const useUIFacadeStateController = (props, realUIStateController) => {
25786
25932
  detail: {},
25787
25933
  });
25788
25934
  chainEvent(propagateUpEvent, e);
25789
- s.realUIStateController.setUIState(child.uiState, propagateUpEvent);
25935
+ // What the popup aggregates arrives in the popup's terms, where an
25936
+ // empty multiple list is `undefined`. The picker answers a question of
25937
+ // its own shape (see resolveEmptyUIState): a <Picker type="array">
25938
+ // whose last item was unselected holds [], the same thing clearing it
25939
+ // leaves — not a value that changes type on its owner the moment it
25940
+ // empties.
25941
+ const { emptyUIState } = s.realUIStateController;
25942
+ const uiStateToAdopt =
25943
+ child.uiState === undefined && emptyUIState !== undefined
25944
+ ? emptyUIState
25945
+ : child.uiState;
25946
+ s.realUIStateController.setUIState(uiStateToAdopt, propagateUpEvent);
25790
25947
  updatingRef.current = false;
25791
25948
  },
25792
25949
  };
@@ -26696,15 +26853,34 @@ const useControlProps = (props, {
26696
26853
  }
26697
26854
  return dispatched;
26698
26855
  };
26699
- const applyEventReaction = (eventName, e) => {
26856
+
26857
+ // What the caller wrote runs from inside the gate when this control is an
26858
+ // own target: a plain `onClick` is DOM, and it would fire from a cross drawn
26859
+ // greyed by the read-only control the affordance sits in (see own_target.js).
26860
+ const callerHandlerIsGated = Boolean(props.ownTarget);
26861
+ const gateCallerHandler = (handler, e) => {
26862
+ if (!handler) {
26863
+ return undefined;
26864
+ }
26865
+ if (callerHandlerIsGated) {
26866
+ return handler;
26867
+ }
26868
+ handler(e);
26869
+ return undefined;
26870
+ };
26871
+ const applyEventReaction = (eventName, e, callerHandler) => {
26700
26872
  const defaultEventReactionDefinition = defaultEventReactionDefinitions?.[eventName];
26701
26873
  const customEventReactionDefinition = eventReactionDefinitions?.[eventName];
26702
26874
  const reaction = customEventReactionDefinition?.(e) ?? defaultEventReactionDefinition?.(e);
26703
26875
  if (!reaction) {
26876
+ // No reaction means no gate: there is nothing here that could refuse the
26877
+ // caller's handler, so withholding it would only lose it.
26878
+ callerHandler?.(e);
26704
26879
  return false;
26705
26880
  }
26706
26881
  const {
26707
26882
  name,
26883
+ intent,
26708
26884
  bypassInteractivity = false,
26709
26885
  allowed,
26710
26886
  prevented,
@@ -26714,6 +26890,7 @@ const useControlProps = (props, {
26714
26890
  return dispatchRequestInteraction(control, {
26715
26891
  event: e,
26716
26892
  name,
26893
+ intent,
26717
26894
  bypassInteractivity,
26718
26895
  prevented: () => {
26719
26896
  debugInteraction(e, `interaction not allowed`);
@@ -26723,19 +26900,18 @@ const useControlProps = (props, {
26723
26900
  prevented?.();
26724
26901
  },
26725
26902
  allowed: () => {
26903
+ callerHandler?.(e);
26726
26904
  allowed?.();
26727
26905
  },
26728
26906
  always
26729
26907
  });
26730
26908
  };
26731
26909
  const onMouseDown = e => {
26732
- props.onMouseDown?.(e);
26733
- applyEventReaction("mouseDown", e);
26910
+ applyEventReaction("mouseDown", e, gateCallerHandler(props.onMouseDown, e));
26734
26911
  transferFocusToTarget(e);
26735
26912
  };
26736
26913
  const onClick = e => {
26737
- props.onClick?.(e);
26738
- applyEventReaction("click", e);
26914
+ applyEventReaction("click", e, gateCallerHandler(props.onClick, e));
26739
26915
  transferFocusToTarget(e);
26740
26916
  };
26741
26917
  const onKeyDown = e => {
@@ -26853,6 +27029,7 @@ const createControlInfo = (props, {
26853
27029
  let defaultStatePropName;
26854
27030
  let stateInitial;
26855
27031
  let readOnlySupported = false;
27032
+ let readOnlyOpens = false;
26856
27033
  let disabledSupported = false;
26857
27034
  let hasStateProp;
26858
27035
  let value;
@@ -26939,6 +27116,14 @@ const createControlInfo = (props, {
26939
27116
  // aria-readonly plus a refused interaction — see the select reactions in
26940
27117
  // getDefaultEventReactionDefinitions.
26941
27118
  readOnlySupported = controlType === "picker" && INPUT_TYPE_SUPPORTING_READONLY_SET.has(typeProp);
27119
+ // A picker's popup is content of its own — a plan with one tile ringed, a
27120
+ // wheel stopped on a time, a list showing what was chosen — so read-only
27121
+ // does not close it: it opens, and everything in it is held read-only in
27122
+ // turn (see the ReadOnlyContext in picker.jsx). Two pickers this is not
27123
+ // true of: one with no popup of its own, which opens the browser's and
27124
+ // cannot hold that read-only (see PickerNative), and one whose caller says
27125
+ // its popup is a form with nothing to read (openWhileReadOnly={false}).
27126
+ readOnlyOpens = controlType === "picker" && props.children !== undefined && props.openWhileReadOnly !== false;
26942
27127
  }
26943
27128
 
26944
27129
  // The suggestion the control starts on, as opposed to what it holds — what a
@@ -26965,6 +27150,7 @@ const createControlInfo = (props, {
26965
27150
  signalHoldsChecked,
26966
27151
  stateFromSignal,
26967
27152
  readOnlySupported,
27153
+ readOnlyOpens,
26968
27154
  disabledSupported
26969
27155
  };
26970
27156
  };
@@ -27248,6 +27434,15 @@ const useInteractiveProps = (props, {
27248
27434
  } = props;
27249
27435
  const [controlRootProps, controlHostProps] = splitControlProps(props);
27250
27436
  controlRootProps["navi-control"] = controlInfo.controlType;
27437
+ if (props.ownTarget) {
27438
+ // "This press is mine" said in the DOM, because that is where it is read
27439
+ // from the outside: by the controls above (see own_target.js), and by the
27440
+ // two gesture readers below — the one that travels a box and the one that
27441
+ // carries a piece, each with its own way of being told to keep out.
27442
+ controlRootProps[OWN_TARGET_ATTRIBUTE] = "";
27443
+ controlRootProps["data-no-drag-travel"] = "";
27444
+ controlRootProps["data-drag-ignore"] = "";
27445
+ }
27251
27446
  const {
27252
27447
  "navi-control-proxy-for": naviProxyFor
27253
27448
  } = props;
@@ -27315,6 +27510,10 @@ const useInteractiveProps = (props, {
27315
27510
  const actionLoading = optimistic ? false : actionStatus.loading;
27316
27511
  const loadingResolved = loadingBase || actionLoading;
27317
27512
  const readOnlyResolved = readOnlyBase || actionLoading;
27513
+ // Read-only, and what this control opens still opens: reading what is in
27514
+ // there changes nothing. Read by READONLY_CONSTRAINT, which lets an
27515
+ // interaction that only reads through on it.
27516
+ uiStateController.readOnlyOpens = Boolean(controlInfo.readOnlyOpens);
27318
27517
  // Both halves of "busy" that do not come from the bound action, kept apart
27319
27518
  // from each other and from it: BUSY_CONSTRAINT answers each from its own
27320
27519
  // live source rather than from the rendered aria-busy, which conflates all
@@ -28360,6 +28559,7 @@ const ButtonFirstResolver = props => {
28360
28559
  const Next = useNextResolver();
28361
28560
  const defaultRef = useRef(null);
28362
28561
  props.ref = props.ref || defaultRef;
28562
+ const ownTargetHidden = useOwnTargetHidden(props);
28363
28563
 
28364
28564
  // Attached to the element rather than kept as a prop: the action a button
28365
28565
  // requests is not always run by the button (a submit button hands the send to
@@ -28369,6 +28569,9 @@ const ButtonFirstResolver = props => {
28369
28569
  message: props.confirm,
28370
28570
  content: props.confirmPopupContent
28371
28571
  });
28572
+ if (ownTargetHidden) {
28573
+ return null;
28574
+ }
28372
28575
  return jsx(Next, {
28373
28576
  ...props
28374
28577
  });
@@ -28459,6 +28662,21 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
28459
28662
  children: naviI18n("button.open")
28460
28663
  })
28461
28664
  };
28665
+
28666
+ /**
28667
+ * @type {import("ignore:preact").FunctionComponent<{
28668
+ * ownTarget?: boolean | "refuse",
28669
+ * [key: string]: any,
28670
+ * }>}
28671
+ * @param {boolean|"refuse"} [ownTarget] A real target inside a zone that belongs
28672
+ * to another control — a chip's cross on a picker's façade, an eye on a
28673
+ * pressable row, a diskette inside a slide that travels. The press is this
28674
+ * button's alone (no travel starts, no popup opens, nothing above answers) and
28675
+ * its `onClick` waits for its own interaction gate instead of firing from the
28676
+ * DOM. Where the zone around it is read-only, disabled or busy the button
28677
+ * goes; `"refuse"` keeps it on screen refusing with a callout, for an
28678
+ * affordance whose presence is information in itself.
28679
+ */
28462
28680
  const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
28463
28681
 
28464
28682
  // How long a popup waits before handing the focus to a field, when giving it
@@ -30019,6 +30237,10 @@ const css$_ = /* css */`
30019
30237
  padding: 0;
30020
30238
  flex-direction: column;
30021
30239
 
30240
+ /* A new surface writes in its own ink, not in its container's — see
30241
+ --navi-popup-color. Declared here rather than left to the UA's own
30242
+ dialog { color: CanvasText } so the ink is themed along with the paper. */
30243
+ color: var(--navi-popup-color);
30022
30244
  background-color: var(--dialog-background-color);
30023
30245
  border-width: var(--dialog-border-width);
30024
30246
  border-style: solid;
@@ -31548,6 +31770,10 @@ const css$Z = /* css */`
31548
31770
  var(--x-popover-max-height)
31549
31771
  );
31550
31772
  max-height: var(--x-popover-max-height);
31773
+ /* A new surface writes in its own ink, not in its container's — see
31774
+ --navi-popup-color. The UA only resets color on a [popover] element,
31775
+ and the local renderer is a plain div. */
31776
+ color: var(--navi-popup-color);
31551
31777
  background-color: var(--popover-background-color);
31552
31778
  border-width: var(--popover-border-width);
31553
31779
  border-style: solid;
@@ -47678,7 +47904,27 @@ installImportMetaCssBuild(import.meta);const css$J = /* css */`
47678
47904
  border-width: var(--border-width);
47679
47905
  border-style: solid;
47680
47906
  border-color: var(--x-border-color);
47681
- border-radius: var(--border-radius);
47907
+ /* Squared from the outside, corner by corner: a checkbox in a group can
47908
+ arrive wrapped (in a label, in a row carrying a state), and the ask then
47909
+ travels down as inherited custom properties rather than as a radius
47910
+ landing on this element. Each corner falls back to the checkbox's own
47911
+ radius when nothing asks for anything. */
47912
+ border-top-left-radius: var(
47913
+ --x-corner-top-left-radius,
47914
+ var(--border-radius)
47915
+ );
47916
+ border-top-right-radius: var(
47917
+ --x-corner-top-right-radius,
47918
+ var(--border-radius)
47919
+ );
47920
+ border-bottom-right-radius: var(
47921
+ --x-corner-bottom-right-radius,
47922
+ var(--border-radius)
47923
+ );
47924
+ border-bottom-left-radius: var(
47925
+ --x-corner-bottom-left-radius,
47926
+ var(--border-radius)
47927
+ );
47682
47928
  outline-width: var(--outline-width);
47683
47929
  outline-style: none;
47684
47930
  outline-color: var(--outline-color);
@@ -48644,7 +48890,26 @@ installImportMetaCssBuild(import.meta);const css$H = /* css */`
48644
48890
  border-width: var(--button-border-width);
48645
48891
  border-style: solid;
48646
48892
  border-color: var(--x-border-color);
48647
- border-radius: var(--button-border-radius);
48893
+ /* Squared from the outside, corner by corner: a row of these is the
48894
+ segmented control a Group is for, and one of them can arrive wrapped
48895
+ (in a tooltip, in a label) — so the ask travels down as inherited
48896
+ custom properties rather than as a radius landing on this element. */
48897
+ border-top-left-radius: var(
48898
+ --x-corner-top-left-radius,
48899
+ var(--button-border-radius)
48900
+ );
48901
+ border-top-right-radius: var(
48902
+ --x-corner-top-right-radius,
48903
+ var(--button-border-radius)
48904
+ );
48905
+ border-bottom-right-radius: var(
48906
+ --x-corner-bottom-right-radius,
48907
+ var(--button-border-radius)
48908
+ );
48909
+ border-bottom-left-radius: var(
48910
+ --x-corner-bottom-left-radius,
48911
+ var(--button-border-radius)
48912
+ );
48648
48913
 
48649
48914
  .navi_icon,
48650
48915
  img {
@@ -48953,6 +49218,28 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
48953
49218
  --x-thumb-color: var(--thumb-color);
48954
49219
  --x-thumb-border: none;
48955
49220
  --x-thumb-cursor: var(--thumb-cursor);
49221
+ /* Squared from the outside, corner by corner — resolved here once because
49222
+ what draws the frame is not this element but the three layers stacked
49223
+ below it (background, track, fill), which cannot take it by inherit the
49224
+ way a control with a single frame does. A range can arrive wrapped, so
49225
+ the ask travels down as inherited custom properties; each corner falls
49226
+ back to the track's own radius when nothing asks for anything. */
49227
+ --x-range-corner-top-left: var(
49228
+ --x-corner-top-left-radius,
49229
+ var(--border-radius)
49230
+ );
49231
+ --x-range-corner-top-right: var(
49232
+ --x-corner-top-right-radius,
49233
+ var(--border-radius)
49234
+ );
49235
+ --x-range-corner-bottom-right: var(
49236
+ --x-corner-bottom-right-radius,
49237
+ var(--border-radius)
49238
+ );
49239
+ --x-range-corner-bottom-left: var(
49240
+ --x-corner-bottom-left-radius,
49241
+ var(--border-radius)
49242
+ );
48956
49243
 
48957
49244
  position: relative;
48958
49245
  box-sizing: border-box;
@@ -48961,10 +49248,26 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
48961
49248
  margin: 2px;
48962
49249
  flex-direction: row;
48963
49250
  align-items: center;
48964
- /* Just for the outline, the real border radius of the range is fixed */
48965
49251
  font-size: var(--font-size);
48966
49252
  font-family: var(--font-family);
48967
- border-radius: var(--outline-border-radius);
49253
+ /* The ring around the whole control, which follows the claim too: a range
49254
+ squared along a seam must not keep a rounded ring over it. */
49255
+ border-top-left-radius: var(
49256
+ --x-corner-top-left-radius,
49257
+ var(--outline-border-radius)
49258
+ );
49259
+ border-top-right-radius: var(
49260
+ --x-corner-top-right-radius,
49261
+ var(--outline-border-radius)
49262
+ );
49263
+ border-bottom-right-radius: var(
49264
+ --x-corner-bottom-right-radius,
49265
+ var(--outline-border-radius)
49266
+ );
49267
+ border-bottom-left-radius: var(
49268
+ --x-corner-bottom-left-radius,
49269
+ var(--outline-border-radius)
49270
+ );
48968
49271
  outline-width: var(--outline-width);
48969
49272
  outline-style: none;
48970
49273
  outline-color: var(--outline-color);
@@ -49005,7 +49308,10 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
49005
49308
  border-width: var(--border-width);
49006
49309
  border-style: solid;
49007
49310
  border-color: var(--x-border-color);
49008
- border-radius: var(--border-radius);
49311
+ border-top-left-radius: var(--x-range-corner-top-left);
49312
+ border-top-right-radius: var(--x-range-corner-top-right);
49313
+ border-bottom-right-radius: var(--x-range-corner-bottom-right);
49314
+ border-bottom-left-radius: var(--x-range-corner-bottom-left);
49009
49315
  }
49010
49316
  .navi_input_range_track {
49011
49317
  position: absolute;
@@ -49015,7 +49321,10 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
49015
49321
  border-width: var(--border-width);
49016
49322
  border-style: solid;
49017
49323
  border-color: var(--x-track-border-color);
49018
- border-radius: var(--border-radius);
49324
+ border-top-left-radius: var(--x-range-corner-top-left);
49325
+ border-top-right-radius: var(--x-range-corner-top-right);
49326
+ border-bottom-right-radius: var(--x-range-corner-bottom-right);
49327
+ border-bottom-left-radius: var(--x-range-corner-bottom-left);
49019
49328
  }
49020
49329
  .navi_input_range_fill {
49021
49330
  position: absolute;
@@ -49023,7 +49332,10 @@ installImportMetaCssBuild(import.meta);const css$G = /* css */`
49023
49332
  height: var(--height);
49024
49333
  background: var(--x-fill-color);
49025
49334
  background-clip: content-box;
49026
- border-radius: var(--border-radius);
49335
+ border-top-left-radius: var(--x-range-corner-top-left);
49336
+ border-top-right-radius: var(--x-range-corner-top-right);
49337
+ border-bottom-right-radius: var(--x-range-corner-bottom-right);
49338
+ border-bottom-left-radius: var(--x-range-corner-bottom-left);
49027
49339
  clip-path: inset(0 calc((1 - var(--x-fill-ratio)) * 100%) 0 0);
49028
49340
  }
49029
49341
  .navi_input_range_thumb {
@@ -50095,7 +50407,27 @@ const inputCss = /* css */`
50095
50407
  border-width: var(--border-width);
50096
50408
  border-style: solid;
50097
50409
  border-color: var(--x-border-color);
50098
- border-radius: var(--border-radius);
50410
+ /* Squared from the outside, corner by corner: an input is not always the
50411
+ member a Group joins — it can arrive wrapped (in a Box carrying a state,
50412
+ in a tooltip) — so the ask travels down as inherited custom properties
50413
+ rather than as a radius landing on this element. Each corner falls back
50414
+ to the input's own radius when nothing asks for anything. */
50415
+ border-top-left-radius: var(
50416
+ --x-corner-top-left-radius,
50417
+ var(--border-radius)
50418
+ );
50419
+ border-top-right-radius: var(
50420
+ --x-corner-top-right-radius,
50421
+ var(--border-radius)
50422
+ );
50423
+ border-bottom-right-radius: var(
50424
+ --x-corner-bottom-right-radius,
50425
+ var(--border-radius)
50426
+ );
50427
+ border-bottom-left-radius: var(
50428
+ --x-corner-bottom-left-radius,
50429
+ var(--border-radius)
50430
+ );
50099
50431
  outline-width: var(--outline-width);
50100
50432
  outline-color: var(--outline-color);
50101
50433
  outline-offset: var(--outline-offset);
@@ -51836,7 +52168,7 @@ const css$E = /* css */`
51836
52168
  positioned element to mean anything, hence position: relative.
51837
52169
  Deliberately not paired with isolation: isolate — a stacking context
51838
52170
  here would also trap the popup of a picker held in the group, which
51839
- counts on its own band reaching the whole page. What keeps these two
52171
+ counts on its own band reaching the whole page. What keeps these
51840
52172
  values from escaping is instead that everything they could reach is a
51841
52173
  band above them (see navi_z_indexes.js). */
51842
52174
  > *:hover,
@@ -51856,6 +52188,25 @@ const css$E = /* css */`
51856
52188
  position: relative;
51857
52189
  z-index: var(--navi-z-index-control-focused);
51858
52190
  }
52191
+ /* The member holding something open. Neither of the two above covers it:
52192
+ the click that opened the popup gives no focus ring, the focus itself
52193
+ left for the popup's content, and the pointer is free to travel to a
52194
+ neighbour — yet the member keeps the border color its open state gives
52195
+ it, and that border is exactly what the neighbour painted after it
52196
+ slices. Read as a state, not as a pseudo-class: :active only lasts as
52197
+ long as the button is held down, and while it is held :hover is true
52198
+ anyway, so it would add nothing here.
52199
+
52200
+ :has, for the same reason as focus-visible above — the group member can
52201
+ be an enrobage around the control that expands — and reaching a popup
52202
+ held inline (a Popover with layer="local" renders inside its member)
52203
+ costs nothing: that popup only reads expanded while its own member is,
52204
+ which is the member this raises. */
52205
+ > *[aria-expanded="true"],
52206
+ > *:has([aria-expanded="true"]) {
52207
+ position: relative;
52208
+ z-index: var(--navi-z-index-control-expanded);
52209
+ }
51859
52210
 
51860
52211
  /* Horizontal (default): Cumulative margin for border overlap */
51861
52212
  &:not([data-vertical]) {
@@ -55286,6 +55637,16 @@ const isTextInputElement = (el) => {
55286
55637
  );
55287
55638
  };
55288
55639
 
55640
+ // How many lines the thing around a component gives it, when that thing caps
55641
+ // its own height and cannot cap what it holds.
55642
+ //
55643
+ // A <Picker> is the case it exists for: its value is clamped with maxLines,
55644
+ // which is CSS line-clamp — it counts line boxes of inline text and has no idea
55645
+ // what a wrapped flex row is. So a <BadgeList> rendered as a picker's ui reads
55646
+ // the number from here and caps its own rows to it, and the picker turns its
55647
+ // own clamp off (see .navi_picker_value:has(.navi_badge_list) in picker.jsx).
55648
+ const MaxLinesContext = createContext(undefined);
55649
+
55289
55650
  // When a component render a prop that can be anything (js value of preact element)
55290
55651
  // make sure it cannot throw during render by converting it to a string if it's not a valid preact element or a primitive value
55291
55652
  const renderSafe = (value) => {
@@ -55783,6 +56144,14 @@ const PickerNative = props => {
55783
56144
  dispatchRequestInteraction(pickerInput, {
55784
56145
  event: e,
55785
56146
  name: "navi_request_open to show native picker",
56147
+ // No "read" intent here, unlike a picker holding a popup of its own
56148
+ // (see PickerCustom): the browser's picker cannot be held read-only,
56149
+ // whichever way its type falls. Where `readonly` applies (date, time,
56150
+ // month, number…) the input is not mutable and showPicker() refuses
56151
+ // it — there is nothing to open. Where it does not (color, file) the
56152
+ // browser opens all the same and writes whatever is chosen straight
56153
+ // into the input, which is read-only in name only. So a read-only
56154
+ // native picker says why instead, on the trigger.
55786
56155
  prevented: () => {
55787
56156
  e.preventDefault();
55788
56157
  },
@@ -55973,14 +56342,20 @@ const PickerCustom = props => {
55973
56342
  // already what it is, and this only re-runs the same reaction.
55974
56343
  const inputEl = getPickerInput(ref.current);
55975
56344
  const valueAtClose = getUIStateFromElement(inputEl);
55976
- if (valueAtOpen === undefined && compareTwoJsValues(valueAtClose, valueAtOpen)) {
56345
+ const controller = inputEl?.__uiStateController__;
56346
+ if (controller?.controlHostProps.readOnly) {
56347
+ // Opened only to be read: what it shows stays the suggestion it
56348
+ // was. A look is not an answer, and the signal behind it is not
56349
+ // written by one.
56350
+ debugPopup(closeEvent, `picker is read-only -> nothing to commit`);
56351
+ } else if (valueAtOpen === undefined && compareTwoJsValues(valueAtClose, valueAtOpen)) {
55977
56352
  // Same third case onRequestClose steps around: nothing held,
55978
56353
  // nothing shown, nothing picked. There is no suggestion here to
55979
56354
  // turn into an answer.
55980
56355
  debugPopup(closeEvent, `picker showed nothing -> nothing to commit`);
55981
56356
  } else {
55982
56357
  debugPopup(closeEvent, `picker defined a suggestion -> commit it`);
55983
- commitUIStateAsAnswer(inputEl?.__uiStateController__, closeEvent);
56358
+ commitUIStateAsAnswer(controller, closeEvent);
55984
56359
  }
55985
56360
  }
55986
56361
  leaveExpanded({
@@ -56069,6 +56444,13 @@ const PickerCustom = props => {
56069
56444
  requestInteraction({
56070
56445
  event: e,
56071
56446
  name: "navi_request_open_event",
56447
+ // Showing what the picker already holds, in the shape only the popup
56448
+ // draws it in — nothing of the value is written on the way in, nor on
56449
+ // the way out. Every interaction below says the same, which is what
56450
+ // lets a read-only picker be opened and read while everything that
56451
+ // would write it (paste, cut, the clear cross) stays refused. See
56452
+ // READONLY_CONSTRAINT.
56453
+ intent: "read",
56072
56454
  allowed: () => {
56073
56455
  requestOpen(e);
56074
56456
  }
@@ -56077,6 +56459,7 @@ const PickerCustom = props => {
56077
56459
  onnavi_request_close: e => {
56078
56460
  requestInteraction({
56079
56461
  event: e,
56462
+ intent: "read",
56080
56463
  allowed: () => {
56081
56464
  requestClose(e, {
56082
56465
  isCancel: e.detail.isCancel
@@ -56095,6 +56478,7 @@ const PickerCustom = props => {
56095
56478
  "a-z": e => {
56096
56479
  return {
56097
56480
  name: "letter key to open",
56481
+ intent: "read",
56098
56482
  allowed: () => {
56099
56483
  requestOpen(e);
56100
56484
  }
@@ -56103,6 +56487,7 @@ const PickerCustom = props => {
56103
56487
  "0-9": e => {
56104
56488
  return {
56105
56489
  name: "numeric key to open",
56490
+ intent: "read",
56106
56491
  allowed: () => {
56107
56492
  requestOpen(e);
56108
56493
  }
@@ -56111,6 +56496,7 @@ const PickerCustom = props => {
56111
56496
  "arrowdown": e => {
56112
56497
  return {
56113
56498
  name: "arrow_down_to_open",
56499
+ intent: "read",
56114
56500
  allowed: () => {
56115
56501
  requestOpen(e);
56116
56502
  e.preventDefault(); // prevent container scroll
@@ -56120,6 +56506,7 @@ const PickerCustom = props => {
56120
56506
  "arrowup": e => {
56121
56507
  return {
56122
56508
  name: "arrow_up_to_open",
56509
+ intent: "read",
56123
56510
  allowed: () => {
56124
56511
  requestOpen(e);
56125
56512
  e.preventDefault(); // prevent container scroll
@@ -56129,6 +56516,7 @@ const PickerCustom = props => {
56129
56516
  "space": e => {
56130
56517
  return {
56131
56518
  name: "space_to_open",
56519
+ intent: "read",
56132
56520
  allowed: () => {
56133
56521
  requestOpen(e);
56134
56522
  e.preventDefault(); // prevent scroll
@@ -56143,6 +56531,7 @@ const PickerCustom = props => {
56143
56531
  }
56144
56532
  return {
56145
56533
  name: "enter_to_open",
56534
+ intent: "read",
56146
56535
  allowed: () => {
56147
56536
  requestOpen(e);
56148
56537
  e.preventDefault(); // prevent form submission
@@ -56156,6 +56545,7 @@ const PickerCustom = props => {
56156
56545
  const isCancel = escapeEffect === "cancel";
56157
56546
  return {
56158
56547
  name: isCancel ? "escape_to_cancel" : "escape_to_close",
56548
+ intent: "read",
56159
56549
  allowed: () => {
56160
56550
  requestClose(e, {
56161
56551
  isCancel
@@ -56187,6 +56577,7 @@ const PickerCustom = props => {
56187
56577
  // choice being taken from anyone.
56188
56578
  return {
56189
56579
  name: "mousedown to close picker",
56580
+ intent: "read",
56190
56581
  allowed: () => requestClose(e, {
56191
56582
  isCancel: true
56192
56583
  })
@@ -56197,6 +56588,7 @@ const PickerCustom = props => {
56197
56588
  }
56198
56589
  return {
56199
56590
  name: "mousedown to open picker",
56591
+ intent: "read",
56200
56592
  allowed: () => {
56201
56593
  debugFocus(e, `prevent browser giving focus to button (mousedown.preventDefault())`);
56202
56594
  requestOpen(e);
@@ -56214,6 +56606,7 @@ const PickerCustom = props => {
56214
56606
  // above), this is where the picker opens for real.
56215
56607
  return {
56216
56608
  name: e.detail === 0 ? "click (keyboard or progammatic) to open picker" : "click to open picker",
56609
+ intent: "read",
56217
56610
  prevented: () => {
56218
56611
  e.preventDefault();
56219
56612
  },
@@ -57652,12 +58045,24 @@ const ListItemSelectable = props => {
57652
58045
  ...rest
57653
58046
  } = props;
57654
58047
  const multiple = useContext(SelectableListMultipleContext);
58048
+ // Whose reason it is that this row cannot be taken. Read-only reaching it
58049
+ // from above is the LIST's, and what is settled is then the whole answer —
58050
+ // said as the selection where several things are taken, as the choice where
58051
+ // one thing is. Said of each row in turn, "this option is not available"
58052
+ // describes something else entirely: a list where each row happens to be
58053
+ // unavailable for its own reasons, which goes on being said that way. Busy
58054
+ // is not settled either: a list waiting on something says nothing about what
58055
+ // will be possible once it is done, so the row keeps its own words.
58056
+ const readOnlyFromAbove = useContext(ReadOnlyContext);
58057
+ const loadingFromAbove = useContext(LoadingContext$1);
58058
+ const answerIsSettled = Boolean(readOnlyFromAbove) && !loadingFromAbove;
58059
+ const readOnlyMessageKey = answerIsSettled ? multiple ? `constraint.readonly.selection` : `constraint.readonly.choice` : `constraint.readonly.option`;
57655
58060
  const inputRef = useRef();
57656
58061
  const inputType = multiple ? "checkbox" : "radio";
57657
58062
  const inputId = `${id}_input`;
57658
58063
  inputRef.nullCanHappen = true; // virtualization
57659
58064
  const [checkableRootProps, checkableProps, controlChildrenWrapperProps] = useCheckableProps({
57660
- readOnlyMessage: naviI18n(`constraint.readonly.option`, props),
58065
+ readOnlyMessage: naviI18n(readOnlyMessageKey, props),
57661
58066
  ...rest,
57662
58067
  ref: inputRef,
57663
58068
  id: inputId,
@@ -62302,6 +62707,51 @@ const PickerPresetResolver = props => {
62302
62707
  });
62303
62708
  };
62304
62709
 
62710
+ // Put around its children by BadgeList so a Badge below knows it is inside one.
62711
+ // A badge then renders nothing at all: it hands its props to the list and the
62712
+ // list renders it. Badges register in tree order, so by the time the list gets
62713
+ // to its own content it holds them all, in source order, and knows how many
62714
+ // there are before deciding what to show — without ever walking children
62715
+ // vnodes, and without rendering a badge it then has to take back.
62716
+ const BadgeListContext = createContext(null);
62717
+
62718
+ const createBadgeRegistry = () => {
62719
+ let pass = 0;
62720
+ let passCount = 0;
62721
+ let entries = [];
62722
+ let childrenAreNew = true;
62723
+
62724
+ return {
62725
+ // Called by BadgeList at the top of every render. childrenAreNew says
62726
+ // whether it was handed fresh children vnodes: when it re-renders on its
62727
+ // own (its own state changed) Preact hands the untouched children straight
62728
+ // back and skips them, so none of the badges registers again. That empty
62729
+ // pass means "unchanged", not "no badge left" — see getEntries.
62730
+ startPass: (areNew) => {
62731
+ pass++;
62732
+ passCount = 0;
62733
+ childrenAreNew = areNew;
62734
+ },
62735
+ // entryState is the badge's own memory. A badge that re-renders on its own
62736
+ // must update its entry, not append a second one.
62737
+ register: (entryState, props) => {
62738
+ if (entryState.pass !== pass) {
62739
+ entryState.pass = pass;
62740
+ entryState.index = passCount;
62741
+ passCount++;
62742
+ }
62743
+ entries[entryState.index] = props;
62744
+ },
62745
+ getEntries: () => {
62746
+ if (passCount === 0 && !childrenAreNew) {
62747
+ return entries;
62748
+ }
62749
+ entries.length = passCount;
62750
+ return entries;
62751
+ },
62752
+ };
62753
+ };
62754
+
62305
62755
  installImportMetaCssBuild(import.meta);const css$x = /* css */`
62306
62756
  @layer navi {
62307
62757
  }
@@ -62360,6 +62810,11 @@ installImportMetaCssBuild(import.meta);const css$x = /* css */`
62360
62810
  align-items: stretch;
62361
62811
  color: var(--x-color);
62362
62812
  font-size: var(--font-size);
62813
+ /* Cuts the font's half-leading above the first line and below the last one,
62814
+ down to cap-height/baseline: the padding becomes the only vertical space
62815
+ and the text is exactly centered. Space between wrapped lines is left
62816
+ untouched, unlike a line-height tweak. */
62817
+ text-box: trim-both cap alphabetic;
62363
62818
  background: var(--x-background);
62364
62819
  background-color: var(--x-background-color);
62365
62820
  border-radius: 1em;
@@ -62409,7 +62864,22 @@ installImportMetaCssBuild(import.meta);const css$x = /* css */`
62409
62864
  }
62410
62865
  }
62411
62866
  `;
62412
- const Badge = ({
62867
+ const Badge = props => {
62868
+ const badgeList = useContext(BadgeListContext);
62869
+ const entryStateRef = useRef();
62870
+ if (badgeList) {
62871
+ // Inside a BadgeList the badge renders nothing: it hands itself over and
62872
+ // the list renders it, which is how the list gets to see them all before
62873
+ // deciding how many it shows. See badge_list_context.js.
62874
+ const entryState = entryStateRef.current || (entryStateRef.current = {});
62875
+ badgeList.register(entryState, props);
62876
+ return null;
62877
+ }
62878
+ return jsx(BadgeUI, {
62879
+ ...props
62880
+ });
62881
+ };
62882
+ const BadgeUI = ({
62413
62883
  children,
62414
62884
  className,
62415
62885
  ...props
@@ -62424,7 +62894,14 @@ const Badge = ({
62424
62894
  return jsx(Text, {
62425
62895
  className: withPropsClassName("navi_badge", className),
62426
62896
  bold: true,
62427
- maxLines: 1,
62897
+ maxLines: 1
62898
+ // The text-box trim ends the content box at the baseline, and a clamped
62899
+ // badge is clipped there: descenders of the last visible line would be
62900
+ // cut. Halfway to the next line's cap top keeps them, still above any
62901
+ // ink from the line the clamp hides.
62902
+ ,
62903
+
62904
+ overflowClipMargin: "content-box calc((1lh - 1cap) / 2)",
62428
62905
  ...props,
62429
62906
  styleCSSVars: BadgeStyleCSSVars,
62430
62907
  spacing: jsx("span", {}),
@@ -62448,6 +62925,15 @@ const BadgeStyleCSSVars = {
62448
62925
  fontSize: "--font-size"
62449
62926
  };
62450
62927
  const BadgeButton = props => {
62928
+ const ownTargetHidden = useOwnTargetHidden(props);
62929
+ if (ownTargetHidden) {
62930
+ return null;
62931
+ }
62932
+ return jsx(BadgeButtonUI, {
62933
+ ...props
62934
+ });
62935
+ };
62936
+ const BadgeButtonUI = props => {
62451
62937
  const defaultRef = useRef();
62452
62938
  props.ref = props.ref || defaultRef;
62453
62939
  const [buttonRootProps, buttonHostProps] = useControlProps(props, {
@@ -62478,20 +62964,208 @@ installImportMetaCssBuild(import.meta);const css$w = /* css */`
62478
62964
  visibility: hidden;
62479
62965
  pointer-events: none;
62480
62966
  }
62967
+
62968
+ /* maxLines renders every badge for one layout, reads where the rows fell,
62969
+ then renders again with only what fits. The in-between is hidden rather
62970
+ than clipped: the badges that don't make it must leave the DOM, not sit
62971
+ there cut in half. Both renders land in the same frame (the second one
62972
+ is queued from a layout effect), so nothing shows up half measured. */
62973
+ &[navi-badge-list-measuring] {
62974
+ visibility: hidden;
62975
+ }
62481
62976
  }
62482
62977
 
62483
62978
  .navi_badge.navi_badge_more {
62484
62979
  white-space: nowrap;
62485
62980
  }
62486
62981
  `;
62487
- const BadgeList = ({
62982
+
62983
+ // Groups badges by the row they wrapped onto.
62984
+ // The signal we look for is horizontal: inside a row each badge starts further
62985
+ // right than the previous one, at a wrap the next one starts back at the row
62986
+ // start. Vertical positions can't be used because "align-items: center" gives
62987
+ // badges of different heights different tops within a single row.
62988
+ const groupRectsByRow = elements => {
62989
+ const rows = [];
62990
+ let previousLeft = -Infinity;
62991
+ for (const element of elements) {
62992
+ const rect = element.getBoundingClientRect();
62993
+ if (rect.width === 0 && rect.height === 0) {
62994
+ continue;
62995
+ }
62996
+ if (rows.length === 0 || rect.left <= previousLeft) {
62997
+ rows.push([]);
62998
+ }
62999
+ rows[rows.length - 1].push(rect);
63000
+ previousLeft = rect.left;
63001
+ }
63002
+ return rows;
63003
+ };
63004
+
63005
+ // Reads a list that currently holds every badge plus the "+N" badge and tells
63006
+ // how many badges fit in maxLines rows.
63007
+ const measureRowFit = (listEl, maxLines) => {
63008
+ const elements = Array.from(listEl.children);
63009
+ if (elements.length < 2) {
63010
+ return elements.length;
63011
+ }
63012
+ // The "+N" badge is rendered last. It sits after every badge so it moves none
63013
+ // of them, which is what lets it be measured in the same layout it is left
63014
+ // out of.
63015
+ const moreRect = elements[elements.length - 1].getBoundingClientRect();
63016
+ const badgeElements = elements.slice(0, -1);
63017
+ const rows = groupRectsByRow(badgeElements);
63018
+ if (rows.length <= maxLines) {
63019
+ return badgeElements.length;
63020
+ }
63021
+ const styles = getComputedStyle(listEl);
63022
+ const gap = parseFloat(styles.columnGap) || 0;
63023
+ const contentRight = listEl.getBoundingClientRect().right - (parseFloat(styles.paddingRight) || 0) - (parseFloat(styles.borderRightWidth) || 0);
63024
+
63025
+ // The "+N" badge lands right after the last kept badge, so it eats into the
63026
+ // last visible row: drop badges from that row until it fits. Emptying that
63027
+ // row entirely is a valid outcome — the badge then wraps onto it and takes it
63028
+ // for itself, which is still within maxLines.
63029
+ const rowsKept = rows.slice(0, maxLines);
63030
+ const lastRow = rowsKept[rowsKept.length - 1];
63031
+ let lastRowCount = lastRow.length;
63032
+ while (lastRowCount > 0 && lastRow[lastRowCount - 1].right + gap + moreRect.width > contentRight + 0.5) {
63033
+ lastRowCount--;
63034
+ }
63035
+ return rowsKept.slice(0, -1).reduce((count, row) => count + row.length, 0) + lastRowCount;
63036
+ };
63037
+ const BADGE_LIST_PROPS = {
63038
+ inline: true,
63039
+ flex: "x",
63040
+ alignY: "center",
63041
+ spacing: "xs"
63042
+ };
63043
+
63044
+ // The badges hand themselves over as they render instead of being read upfront
63045
+ // from the children vnodes: see badge_list_context.js. Only worth it when the
63046
+ // list has something to decide — how many to show, or whether there is none at
63047
+ // all. Otherwise the badges render themselves and nothing has to be collected.
63048
+ const useBadgeRegistry = (children, enabled) => {
63049
+ const registryRef = useRef();
63050
+ const previousChildrenRef = useRef();
63051
+ if (!enabled) {
63052
+ return null;
63053
+ }
63054
+ const registry = registryRef.current || (registryRef.current = createBadgeRegistry());
63055
+ // Whether Preact is about to render the badges again or hand back the ones it
63056
+ // already has, which it does when the list re-renders on its own.
63057
+ registry.startPass(previousChildrenRef.current !== children);
63058
+ previousChildrenRef.current = children;
63059
+ return registry;
63060
+ };
63061
+
63062
+ /**
63063
+ * A row of badges that wraps.
63064
+ *
63065
+ * Four shapes behind one name, because what the list has to do at runtime is
63066
+ * not the same in each. Nothing is set up for a case that cannot happen: a
63067
+ * plain list is one element holding its children as-is — no registry, no
63068
+ * effect —, a capped one collects its badges but measures nothing, and only
63069
+ * shrinkWrap ever builds the measurement ghost.
63070
+ *
63071
+ * @param {import("ignore:preact").ComponentChildren} [fallback]
63072
+ * Rendered in place of the badges when there is none. Without it an empty
63073
+ * list renders nothing at all. In a <Picker> this is the only placeholder
63074
+ * the user gets — a picker given a `ui` does not draw its own — so pass the
63075
+ * placeholder text: `fallback="Select skills…"`. Plain text is the right
63076
+ * choice there: it reads at the picker's size, in the placeholder color the
63077
+ * picker gives its empty value slot, and the box stays the same height. A
63078
+ * transparent <Badge> matches the slot to the pixel instead, at the price of
63079
+ * badge-sized text. See docs/badge_list.md.
63080
+ * @param {boolean} [shrinkWrap]
63081
+ * Narrows the list down to its widest row so the last row isn't ragged.
63082
+ * Defaults to true inside a <Picker> — the trigger draws a border around the
63083
+ * list, so the ragged edge shows — and false elsewhere, where the work would
63084
+ * often go unseen: opt in where an edge is visible. Ignored when maxLines is
63085
+ * in play, which needs the full width to know where the rows fall.
63086
+ * @param {number} [max]
63087
+ * Caps how many badges are rendered; the surplus becomes a "+N" badge, which
63088
+ * takes one of the max slots.
63089
+ * @param {number} [maxLines]
63090
+ * Caps how many rows are shown, measured. Falls back to what the surrounding
63091
+ * component grants (a <Picker>, see max_lines_context.js).
63092
+ */
63093
+ const BadgeList = props => {
63094
+ import.meta.css = [css$w, "@jsenv/navi/src/text/badge_list.jsx"];
63095
+ const {
63096
+ maxLines,
63097
+ max,
63098
+ fallback
63099
+ } = props;
63100
+ const maxLinesFromAbove = useContext(MaxLinesContext);
63101
+ const maxLinesResolved = maxLines === undefined ? maxLinesFromAbove : maxLines;
63102
+ // Granted lines mean a clamping container — a <Picker> — whose border makes
63103
+ // the ragged last row show; nothing of the sort around us, and shrink
63104
+ // wrapping is work nobody sees unless asked for.
63105
+ const {
63106
+ shrinkWrap = maxLinesFromAbove !== undefined
63107
+ } = props;
63108
+ if (maxLinesResolved !== undefined) {
63109
+ // shrinkWrap is dropped on purpose: it narrows the list down to its widest
63110
+ // row, which would re-wrap the badges under the cap just measured.
63111
+ return jsx(BadgeListMaxLines, {
63112
+ ...props,
63113
+ maxLines: maxLinesResolved
63114
+ });
63115
+ }
63116
+ if (shrinkWrap) {
63117
+ return jsx(BadgeListShrinkWrap, {
63118
+ ...props
63119
+ });
63120
+ }
63121
+ if (max !== undefined || fallback !== undefined) {
63122
+ return jsx(BadgeListCounted, {
63123
+ ...props
63124
+ });
63125
+ }
63126
+ return jsx(BadgeListPlain, {
63127
+ ...props
63128
+ });
63129
+ };
63130
+
63131
+ // Nothing to measure, cap, or count: the badges render themselves — no
63132
+ // registry, no context, no effect, one element.
63133
+ const BadgeListPlain = props => {
63134
+ return jsx(Box, {
63135
+ baseClassName: "navi_badge_list",
63136
+ ...BADGE_LIST_PROPS,
63137
+ ...props
63138
+ });
63139
+ };
63140
+
63141
+ // max/fallback need the badge count, so the badges are collected — but nothing
63142
+ // is measured and no DOM is watched.
63143
+ const BadgeListCounted = ({
62488
63144
  fallback,
62489
63145
  children,
62490
- shrinkWrap = true,
62491
63146
  max,
62492
- ...props
63147
+ ...boxProps
62493
63148
  }) => {
62494
- import.meta.css = [css$w, "@jsenv/navi/src/text/badge_list.jsx"];
63149
+ const registry = useBadgeRegistry(children, true);
63150
+ return jsx(Box, {
63151
+ baseClassName: "navi_badge_list",
63152
+ ...BADGE_LIST_PROPS,
63153
+ ...boxProps,
63154
+ children: jsx(BadgeListChildren, {
63155
+ registry: registry,
63156
+ max: max,
63157
+ fallback: fallback,
63158
+ children: children
63159
+ })
63160
+ });
63161
+ };
63162
+ const BadgeListShrinkWrap = ({
63163
+ fallback,
63164
+ children,
63165
+ max,
63166
+ ...restProps
63167
+ }) => {
63168
+ const registry = useBadgeRegistry(children, max !== undefined || fallback !== undefined);
62495
63169
  const measureRef = useRef();
62496
63170
  const visibleRef = useRef();
62497
63171
  useLayoutEffect(() => {
@@ -62504,18 +63178,25 @@ const BadgeList = ({
62504
63178
  let rafId;
62505
63179
  const measure = () => {
62506
63180
  visibleEl.style.width = "";
62507
- if (shrinkWrap) {
62508
- // Clone the already-rendered DOM nodes instead of letting React/Preact
62509
- // render the children a second time into the ghost: re-rendering would
62510
- // instantiate Badge/Badge.Button a second time, double-registering
62511
- // their controllers (and any other mount side effect) under the same id.
62512
- measureEl.replaceChildren(...Array.from(visibleEl.children, child => child.cloneNode(true)));
62513
- const optimalWidth = measureWidestChildRow(measureEl);
62514
- if (optimalWidth !== null) {
62515
- visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
62516
- }
63181
+ // Clone the already-rendered DOM nodes instead of letting React/Preact
63182
+ // render the children a second time into the ghost: re-rendering would
63183
+ // instantiate Badge/Badge.Button a second time, double-registering their
63184
+ // controllers (and any other mount side effect) under the same id.
63185
+ measureEl.replaceChildren(...Array.from(visibleEl.children, child => child.cloneNode(true)));
63186
+ const optimalWidth = measureWidestChildRow(measureEl);
63187
+ if (optimalWidth !== null) {
63188
+ visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
62517
63189
  }
62518
63190
  };
63191
+
63192
+ // A single badge is a single row, and a single row is already the widest
63193
+ // one: there is nothing to narrow, and no width the list could be given
63194
+ // that would change that.
63195
+ if (visibleEl.children.length < 2) {
63196
+ visibleEl.style.width = "";
63197
+ measureEl.replaceChildren();
63198
+ return undefined;
63199
+ }
62519
63200
  measure();
62520
63201
  const onResize = () => {
62521
63202
  cancelAnimationFrame(rafId);
@@ -62532,36 +63213,201 @@ const BadgeList = ({
62532
63213
  observer?.disconnect();
62533
63214
  window.removeEventListener("resize", onResize);
62534
63215
  };
62535
- }, [shrinkWrap, children]);
62536
- const childArray = toChildArray(children);
62537
- const hasMax = max !== undefined && childArray.length > max;
62538
- const visibleChildren = hasMax ? childArray.slice(0, max - 1) : childArray;
62539
- const hiddenCount = hasMax ? childArray.length - (max - 1) : 0;
62540
- const sharedProps = {
62541
- inline: true,
62542
- flex: "x",
62543
- alignY: "center",
62544
- spacing: "xs",
62545
- ...props
63216
+ }, [children]);
63217
+ const boxProps = {
63218
+ ...BADGE_LIST_PROPS,
63219
+ ...restProps
62546
63220
  };
62547
- return jsxs(Box, {
62548
- relative: true,
62549
- children: [jsx(Box, {
62550
- baseClassName: "navi_badge_list",
62551
- ...sharedProps,
62552
- ref: measureRef,
62553
- "aria-hidden": "true",
62554
- "navi-badge-list-clone": ""
62555
- }), jsxs(Box, {
62556
- baseClassName: "navi_badge_list",
62557
- ...sharedProps,
62558
- ref: visibleRef,
62559
- children: [visibleChildren.length ? visibleChildren : fallback, hiddenCount > 0 && jsx(Badge, {
62560
- className: "navi_badge_more",
62561
- children: naviI18n("badge_list.more", {
62562
- count: hiddenCount
63221
+ return (
63222
+ // inline flex, not a plain block: the wrapper must sit on the line the way
63223
+ // the list itself would, otherwise the list lands a pixel low in some
63224
+ // containers.
63225
+ jsxs(Box, {
63226
+ relative: true,
63227
+ inline: true,
63228
+ flex: "x",
63229
+ children: [jsx(Box, {
63230
+ baseClassName: "navi_badge_list",
63231
+ ...boxProps,
63232
+ ref: measureRef,
63233
+ "aria-hidden": "true",
63234
+ "navi-badge-list-clone": ""
63235
+ }), jsx(Box, {
63236
+ baseClassName: "navi_badge_list",
63237
+ ...boxProps,
63238
+ ref: visibleRef,
63239
+ children: jsx(BadgeListChildren, {
63240
+ registry: registry,
63241
+ max: max,
63242
+ fallback: fallback,
63243
+ children: children
62563
63244
  })
62564
63245
  })]
63246
+ })
63247
+ );
63248
+ };
63249
+ const BadgeListMaxLines = ({
63250
+ fallback,
63251
+ children,
63252
+ max,
63253
+ maxLines,
63254
+ ...boxProps
63255
+ }) => {
63256
+ const registry = useBadgeRegistry(children, true);
63257
+ const visibleRef = useRef();
63258
+
63259
+ // What the measure found: how many badges there were and how many of them fit
63260
+ // in maxLines rows. null means "not measured yet": the list then renders them
63261
+ // all, hidden, for the layout effect to read.
63262
+ const [fit, setFit] = useState(null);
63263
+ // The two widths this list gives whatever is around it: the one it takes with
63264
+ // every badge rendered, and the one it settles on once the surplus is gone.
63265
+ // Neither is a reason to measure again — see the resize watch below.
63266
+ const selfWidthsRef = useRef({
63267
+ full: -1,
63268
+ settled: -1
63269
+ });
63270
+ const measuring = fit === null;
63271
+ // A single badge is a single row whatever the width, so nothing can move it
63272
+ // out of the cap and nothing has to be watched.
63273
+ const watchesResize = fit !== null && fit.count > 1;
63274
+
63275
+ // Runs after every render, which is when the badges have registered and the
63276
+ // DOM holds whatever this render asked for.
63277
+ useLayoutEffect(() => {
63278
+ const visibleEl = visibleRef.current;
63279
+ if (!visibleEl) {
63280
+ return;
63281
+ }
63282
+ const count = registry.getEntries().length;
63283
+ const width = visibleEl.parentElement?.getBoundingClientRect().width ?? -1;
63284
+ if (fit === null) {
63285
+ selfWidthsRef.current.full = width;
63286
+ setFit({
63287
+ count,
63288
+ shown: count < 2 ? count : measureRowFit(visibleEl, maxLines)
63289
+ });
63290
+ return;
63291
+ }
63292
+ selfWidthsRef.current.settled = width;
63293
+ if (fit.count !== count) {
63294
+ // Badges came or went: what was measured no longer describes them.
63295
+ setFit(null);
63296
+ }
63297
+ });
63298
+ useLayoutEffect(() => {
63299
+ const outerParent = visibleRef.current?.parentElement;
63300
+ if (!watchesResize || !outerParent) {
63301
+ return undefined;
63302
+ }
63303
+ let rafId;
63304
+ const remeasure = () => {
63305
+ // Only a width change can move the rows — a height change is this list
63306
+ // growing or shrinking, never the room it was given.
63307
+ //
63308
+ // And not every width change either. Nothing guarantees an ancestor whose
63309
+ // width does not follow its content (a column with align-items: start
63310
+ // sizes every row to what is inside it), so rendering every badge widens
63311
+ // what is being watched and dropping the surplus narrows it right back.
63312
+ // Those two widths are this list talking to itself; measuring again on
63313
+ // them never ends. Any other width is the room around it changing.
63314
+ const width = outerParent.getBoundingClientRect().width;
63315
+ const {
63316
+ full,
63317
+ settled
63318
+ } = selfWidthsRef.current;
63319
+ if (Math.abs(width - full) < 0.5 || Math.abs(width - settled) < 0.5) {
63320
+ return;
63321
+ }
63322
+ cancelAnimationFrame(rafId);
63323
+ rafId = requestAnimationFrame(() => setFit(null));
63324
+ };
63325
+ const observer = new ResizeObserver(remeasure);
63326
+ observer.observe(outerParent);
63327
+ window.addEventListener("resize", remeasure);
63328
+ return () => {
63329
+ cancelAnimationFrame(rafId);
63330
+ observer.disconnect();
63331
+ window.removeEventListener("resize", remeasure);
63332
+ };
63333
+ }, [watchesResize, maxLines]);
63334
+ return jsx(Box, {
63335
+ baseClassName: "navi_badge_list",
63336
+ ...BADGE_LIST_PROPS,
63337
+ ...boxProps,
63338
+ ref: visibleRef,
63339
+ "navi-badge-list-measuring": measuring ? "" : undefined,
63340
+ children: jsx(BadgeListChildren, {
63341
+ registry: registry,
63342
+ max: max,
63343
+ fallback: fallback,
63344
+ measuring: measuring,
63345
+ rowFit: fit?.shown ?? null,
63346
+ children: children
63347
+ })
63348
+ });
63349
+ };
63350
+ const BadgeListChildren = ({
63351
+ registry,
63352
+ children,
63353
+ fallback,
63354
+ max,
63355
+ measuring,
63356
+ rowFit
63357
+ }) => {
63358
+ if (!registry) {
63359
+ // The badges are on their own: nothing here decides which of them render.
63360
+ return children;
63361
+ }
63362
+ return jsxs(Fragment$1, {
63363
+ children: [jsx(BadgeListContext.Provider, {
63364
+ value: registry,
63365
+ children: children
63366
+ }), jsx(BadgeListContent, {
63367
+ registry: registry,
63368
+ fallback: fallback,
63369
+ max: max,
63370
+ measuring: measuring,
63371
+ rowFit: rowFit
63372
+ })]
63373
+ });
63374
+ };
63375
+ const BadgeListContent = ({
63376
+ registry,
63377
+ fallback,
63378
+ max,
63379
+ measuring,
63380
+ rowFit
63381
+ }) => {
63382
+ const entries = registry.getEntries();
63383
+ const count = entries.length;
63384
+ if (count === 0) {
63385
+ return fallback;
63386
+ }
63387
+
63388
+ // The "+N" badge stands among the badges, so it takes one of the max slots
63389
+ // when there is a surplus to name. A list of exactly `max` badges has nothing
63390
+ // to name and keeps all of them.
63391
+ let shownCount = max !== undefined && count > max ? max - 1 : count;
63392
+ if (!measuring && rowFit !== null && rowFit !== undefined && rowFit < shownCount) {
63393
+ shownCount = rowFit;
63394
+ }
63395
+ // While measuring, everything above is on screen (hidden) along with the "+N"
63396
+ // badge, so the layout effect can see where the rows fall and how much room
63397
+ // that badge asks for. Its label then reads the worst case — every badge
63398
+ // hidden — so the room reserved is never short.
63399
+ const hasMore = measuring || shownCount < count;
63400
+ return jsxs(Fragment$1, {
63401
+ children: [entries.slice(0, shownCount).map((badgeProps, index) =>
63402
+ // Keyed by position: a badge's own key went to the registering vnode
63403
+ // above and doesn't reach here, and badges keep no state worth moving.
63404
+ jsx(BadgeUI, {
63405
+ ...badgeProps
63406
+ }, index)), hasMore && jsx(BadgeUI, {
63407
+ className: "navi_badge_more",
63408
+ children: naviI18n("badge_list.more", {
63409
+ count: measuring ? count : count - shownCount
63410
+ })
62565
63411
  })]
62566
63412
  });
62567
63413
  };
@@ -62792,7 +63638,6 @@ const PickerObjectUI = () => {
62792
63638
  const PickerArray = props => {
62793
63639
  const Next = useNextResolver();
62794
63640
  return jsx(Next, {
62795
- maxLines: "3",
62796
63641
  ui: jsx(PickerArrayUI, {}),
62797
63642
  ...props,
62798
63643
  type: "navi_js",
@@ -62822,6 +63667,57 @@ const PickerArrayUI = () => {
62822
63667
  })
62823
63668
  });
62824
63669
  };
63670
+
63671
+ /**
63672
+ * One value the picker holds, drawn as a chip with a cross that takes it back
63673
+ * out. Sits wherever the application draws what was picked — on the picker's
63674
+ * façade (`ui`) or inside its popup — and both behave the same.
63675
+ *
63676
+ * The cross asks with `--navi-unselect` rather than writing a new list, and it
63677
+ * asks the picker — which holds what was picked, and hands it down to whatever
63678
+ * draws it in the popup. Nothing to name: the picker is the nearest control
63679
+ * around the chip. `commandFor` is for a chip that stands outside the picker it
63680
+ * speaks for.
63681
+ *
63682
+ * The cross is an own target (see own_target.js), so the press belongs to it
63683
+ * and not to the picker underneath, and it goes when the picker turns read-only
63684
+ * — a row being read still says what was picked, it just no longer offers to
63685
+ * unpick it.
63686
+ *
63687
+ * @type {import("ignore:preact").FunctionComponent<{
63688
+ * value: any,
63689
+ * commandFor?: string,
63690
+ * children?: import("ignore:preact").ComponentChildren,
63691
+ * [key: string]: any,
63692
+ * }>}
63693
+ * @param {any} value The value this chip stands for — one entry of what the
63694
+ * picker holds, and what `--navi-unselect` carries.
63695
+ * @param {string} [commandFor] The id of the picker to take the value out of,
63696
+ * when the chip does not sit inside it.
63697
+ */
63698
+ const PickerChip = ({
63699
+ value,
63700
+ commandFor,
63701
+ children,
63702
+ ...rest
63703
+ }) => {
63704
+ return jsxs(Badge, {
63705
+ inline: true,
63706
+ flex: true,
63707
+ ...rest,
63708
+ children: [children, jsx(Badge.Button, {
63709
+ ownTarget: true,
63710
+ command: "--navi-unselect",
63711
+ commandFor: commandFor,
63712
+ value: value,
63713
+ "aria-label": naviI18n("button.remove"),
63714
+ children: jsx(Icon, {
63715
+ lineOverflow: "allow",
63716
+ children: jsx(CloseSvg, {})
63717
+ })
63718
+ })]
63719
+ });
63720
+ };
62825
63721
  const PickerColor = props => {
62826
63722
  const Next = useNextResolver();
62827
63723
  return jsx(Next, {
@@ -63178,10 +64074,39 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
63178
64074
  /* The frame is drawn by the box, but its radius is declared here, on the
63179
64075
  control root, like every other navi control does — so anything styling
63180
64076
  the picker from the outside (a Group squaring the corners it joins) has
63181
- one element to talk to, and the box follows. */
63182
- border-radius: var(--picker-border-radius);
64077
+ one element to talk to, and the box follows.
64078
+ Corner by corner rather than as the shorthand: a picker is not always
64079
+ the member a Group joins — it can arrive wrapped (in a Box carrying a
64080
+ state, in a link, in a tooltip), and the ask then travels down as
64081
+ inherited custom properties instead of landing on this element as a
64082
+ radius. Each corner falls back to the picker's own radius when nothing
64083
+ asks for anything. */
64084
+ border-top-left-radius: var(
64085
+ --x-corner-top-left-radius,
64086
+ var(--picker-border-radius)
64087
+ );
64088
+ border-top-right-radius: var(
64089
+ --x-corner-top-right-radius,
64090
+ var(--picker-border-radius)
64091
+ );
64092
+ border-bottom-right-radius: var(
64093
+ --x-corner-bottom-right-radius,
64094
+ var(--picker-border-radius)
64095
+ );
64096
+ border-bottom-left-radius: var(
64097
+ --x-corner-bottom-left-radius,
64098
+ var(--picker-border-radius)
64099
+ );
63183
64100
 
63184
64101
  .navi_picker_box {
64102
+ /* The ask stops here: this element is the picker's frame, so nothing it
64103
+ holds is at the seam — the chevron and the clear cross in the slot, a
64104
+ button in a custom UI. */
64105
+ --x-corner-top-left-radius: initial;
64106
+ --x-corner-top-right-radius: initial;
64107
+ --x-corner-bottom-right-radius: initial;
64108
+ --x-corner-bottom-left-radius: initial;
64109
+
63185
64110
  position: relative;
63186
64111
  display: inline-flex;
63187
64112
  box-sizing: border-box;
@@ -63231,16 +64156,32 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
63231
64156
  &[navi-placeholder] {
63232
64157
  color: var(--picker-placeholder-color);
63233
64158
  }
64159
+
64160
+ /* A <BadgeList> caps its own rows: it renders the badges that fit and a
64161
+ "+N" badge for the rest, reading the number from MaxLinesContext right
64162
+ below. The picker must then not clamp on top of it — line-clamp turns
64163
+ the value into a -webkit-box and single-line truncation into a
64164
+ nowrap block, either of which takes the badge list out of the
64165
+ inline-flex layout it needs. maxLines writes those as inline styles on
64166
+ the element, hence !important. */
64167
+ &:has(.navi_badge_list) {
64168
+ display: inline-flex !important;
64169
+ -webkit-line-clamp: none !important;
64170
+ overflow: visible !important;
64171
+ -webkit-box-orient: horizontal !important;
64172
+ text-overflow: clip !important;
64173
+ white-space: normal !important;
64174
+ }
64175
+
64176
+ /* The façade is transparent to the pointer — a press on what it draws
64177
+ means "open the picker". An own target is the exception, the same way
64178
+ the clear cross is one in the slot below: it says the press is aimed at
64179
+ IT, so it has to be reachable at all. */
64180
+ [data-navi-own-target] {
64181
+ pointer-events: auto;
64182
+ }
63234
64183
  }
63235
64184
  .navi_picker_right_slot {
63236
- /* A corner claimed from the outside (see group.jsx) is the frame's, and
63237
- what lives in here is not the frame — a button in a slot, the content
63238
- of a popup — so the ask stops at this boundary. */
63239
- --x-corner-top-left-radius: initial;
63240
- --x-corner-top-right-radius: initial;
63241
- --x-corner-bottom-right-radius: initial;
63242
- --x-corner-bottom-left-radius: initial;
63243
-
63244
64185
  display: inline-flex;
63245
64186
  height: 1em;
63246
64187
  height: 1lh;
@@ -63335,6 +64276,15 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
63335
64276
  }
63336
64277
 
63337
64278
  .navi_picker_content {
64279
+ /* The other side of the frame: what a picker holds here is what its
64280
+ popup shows, and a popup is never at a seam. Popover and Dialog stop
64281
+ the ask at their own root too — this covers content that reaches the
64282
+ popup through neither. */
64283
+ --x-corner-top-left-radius: initial;
64284
+ --x-corner-top-right-radius: initial;
64285
+ --x-corner-bottom-right-radius: initial;
64286
+ --x-corner-bottom-left-radius: initial;
64287
+
63338
64288
  display: contents;
63339
64289
  text-align: initial; /* Don't inherit picker text align */
63340
64290
  }
@@ -63352,6 +64302,12 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
63352
64302
  --x-picker-icon-color: var(--picker-icon-color-readonly);
63353
64303
  --x-picker-cursor: default;
63354
64304
  }
64305
+ /* Read-only and still opening, so it still says so under the pointer.
64306
+ Before the disabled block below on purpose: a disabled picker opens
64307
+ nothing, read-only or not. */
64308
+ &[data-readonly-opens] {
64309
+ --x-picker-cursor: pointer;
64310
+ }
63355
64311
  /* Focus */
63356
64312
  &[data-focus-within]:has(.navi_picker_input[data-focus-visible]) {
63357
64313
  --x-picker-border-color: transparent;
@@ -63480,6 +64436,7 @@ const PickerButton = props => {
63480
64436
  // untouched (see the --navi-clear command).
63481
64437
  clearConfirm,
63482
64438
  clearConfirmPopupContent,
64439
+ readOnly,
63483
64440
  error
63484
64441
  } = props;
63485
64442
  const isSingleLine = maxLines === 1;
@@ -63497,210 +64454,298 @@ const PickerButton = props => {
63497
64454
  children
63498
64455
  } = inputProps;
63499
64456
  const loading = basePseudoState[":-navi-loading"];
64457
+ // The same chain useControlProps resolves (own prop first, then what the
64458
+ // group above says), for the two things it does not carry: the popup content,
64459
+ // which is read-only along with the picker, and the cursor, which stays a
64460
+ // pointer on a picker that still opens.
64461
+ const readOnlyFromAbove = useContext(ReadOnlyContext);
64462
+ const readOnlyResolved = readOnly || readOnlyFromAbove;
64463
+ // Read off the controller rather than worked out again: what makes a
64464
+ // read-only picker open is settled once, where the gate reads it (see
64465
+ // createControlInfo's readOnlyOpens). Needed here for the cursor.
64466
+ const readOnlyOpens = Boolean(readOnlyResolved) && uiStateController.readOnlyOpens;
64467
+ // Whether anything can still be changed — read by the clear cross below,
64468
+ // clearing being a modification like any other.
64469
+ const interactive = !basePseudoState[":disabled"] && !basePseudoState[":read-only"] && !loading;
63500
64470
  usePickerErrorCallout(uiStateController, error);
63501
- return jsxs(Box, {
63502
- as: "div",
63503
- ref: ref
63504
- // The flow this element really has (.navi_picker is display:inline-flex).
63505
- // Left unsaid, Box reads a <div> as block and resolves alignX into a
63506
- // text-align which is a different intention entirely (that one is the
63507
- // textAlign prop, placing the text INSIDE the value slot).
63508
- ,
64471
+ return (
64472
+ /* Read-only crosses into everything the picker is made of: what it really
64473
+ holds is drawn by controls of their own — in the popup, and on the façade
64474
+ where an application puts its own affordances and each of them refuses
64475
+ in its own words once told. Said from the read-only state alone, never
64476
+ from the busy one an action running for a moment is not the same thing
64477
+ as a value nobody may change. */
64478
+ jsx(ReadOnlyContext.Provider, {
64479
+ value: readOnlyResolved,
64480
+ children: jsxs(Box, {
64481
+ as: "div",
64482
+ ref: ref
64483
+ // The flow this element really has (.navi_picker is display:inline-flex).
64484
+ // Left unsaid, Box reads a <div> as block and resolves alignX into a
64485
+ // text-align — which is a different intention entirely (that one is the
64486
+ // textAlign prop, placing the text INSIDE the value slot).
64487
+ ,
63509
64488
 
63510
- inline: true,
63511
- flex: "x",
63512
- baseClassName: "navi_picker",
63513
- pseudoClasses: PICKER_BUTTON_PSEUDO_CLASSES,
63514
- "data-variant": variant,
63515
- "navi-picker": "",
63516
- "navi-single-line": isSingleLine ? "" : undefined,
63517
- "navi-ui-custom": ui === "default" ? undefined : "",
63518
- "data-popup-width-fit-content": popupWidthFitContent ? "" : undefined,
63519
- ...pickerRemainingProps,
63520
- basePseudoState: basePseudoState,
63521
- styleCSSVars: PickerStyleCSSVars,
63522
- variant: undefined,
63523
- rightSlotIcon: undefined,
63524
- rightSlotIconSize: undefined,
63525
- rightSlot: undefined,
63526
- clearConfirm: undefined,
63527
- clearConfirmPopupContent: undefined,
63528
- ui: undefined,
63529
- maxLines: undefined,
63530
- popupWidthFitContent: undefined,
63531
- error: undefined,
63532
- dayLabel: undefined
63533
- // This wrapper will receive keyboard event bubbling from the picker popup content
63534
- // we re-dispatch on the input (to get escape to close for instance)
63535
- ,
64489
+ inline: true,
64490
+ flex: "x",
64491
+ baseClassName: "navi_picker",
64492
+ pseudoClasses: PICKER_BUTTON_PSEUDO_CLASSES,
64493
+ "data-variant": variant,
64494
+ "navi-picker": "",
64495
+ "navi-single-line": isSingleLine ? "" : undefined,
64496
+ "navi-ui-custom": ui === "default" ? undefined : "",
64497
+ "data-readonly-opens": readOnlyOpens ? "" : undefined,
64498
+ "data-popup-width-fit-content": popupWidthFitContent ? "" : undefined,
64499
+ ...pickerRemainingProps,
64500
+ basePseudoState: basePseudoState,
64501
+ styleCSSVars: PickerStyleCSSVars,
64502
+ variant: undefined,
64503
+ rightSlotIcon: undefined,
64504
+ rightSlotIconSize: undefined,
64505
+ rightSlot: undefined,
64506
+ clearConfirm: undefined,
64507
+ clearConfirmPopupContent: undefined,
64508
+ openWhileReadOnly: undefined,
64509
+ ui: undefined,
64510
+ maxLines: undefined,
64511
+ popupWidthFitContent: undefined,
64512
+ error: undefined,
64513
+ dayLabel: undefined
64514
+ // This wrapper will receive keyboard event bubbling from the picker popup content
64515
+ // we re-dispatch on the input (to get escape to close for instance)
64516
+ ,
63536
64517
 
63537
- onKeyDown: inputProps.onKeyDown
63538
- // in case request open/close are dispatched on the control root ->
63539
- // redispatch them to the host
63540
- ,
64518
+ onKeyDown: inputProps.onKeyDown
64519
+ // in case request open/close are dispatched on the control root ->
64520
+ // redispatch them to the host
64521
+ ,
63541
64522
 
63542
- onnavi_request_open: inputProps.onnavi_request_open,
63543
- onnavi_request_close: inputProps.onnavi_request_close,
63544
- children: [jsxs("span", {
63545
- className: "navi_picker_box",
63546
- children: [variant === "headless" ? null : jsx(LoadingOutline, {
63547
- loading: loading,
63548
- color: "var(--picker-loader-color)",
63549
- inset: -2
63550
- }), jsx(PickerInput, {
63551
- tabIndex: variant === "headless" ? -1 : undefined,
63552
- "aria-hidden": variant === "headless" ? "true" : undefined,
63553
- ...inputProps,
63554
- // eslint-disable-next-line react/no-children-prop
63555
- children: undefined // we will render children into the div
64523
+ onnavi_request_open: inputProps.onnavi_request_open,
64524
+ onnavi_request_close: inputProps.onnavi_request_close
64525
+ // `--navi-select`/`--navi-unselect` about one entry of the list the
64526
+ // picker holds — a chip on the façade, a suggestion beside the field.
64527
+ // Answered here rather than by the control drawing that list in the
64528
+ // popup, even though rows are what such a control owns: a picker given
64529
+ // its own value builds its popup only on first open (see
64530
+ // popup_content_mount.js), so before that there is no such control at
64531
+ // all — and building the whole popup to have someone to talk to, for a
64532
+ // cross, is the wrong price. The picker holds the value in the first
64533
+ // place and hands it down whenever the popup is built.
63556
64534
  ,
63557
64535
 
63558
- ui: ui,
63559
- onFocus: e => {
63560
- inputProps.onFocus?.(e);
63561
- e.target.select();
64536
+ onnavi_request_select: e => {
64537
+ requestPickerListEntry(ref.current, inputRef.current, e, "select");
63562
64538
  },
63563
- onCopy: e => {
63564
- const pickerEl = ref.current;
63565
- if (isWithinPickerContent(e.target, pickerEl)) {
63566
- return;
63567
- }
63568
- const uiState = uiStateController.uiState;
63569
- if (uiState === undefined) {
63570
- return;
63571
- }
63572
- e.preventDefault();
63573
- const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
63574
- e.clipboardData.setData("text/plain", displayText);
63575
- e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
63576
- },
63577
- onCut: e => {
63578
- const pickerEl = ref.current;
63579
- if (isWithinPickerContent(e.target, pickerEl)) {
63580
- return;
63581
- }
63582
- const uiState = uiStateController.uiState;
63583
- if (uiState === undefined) {
63584
- return;
63585
- }
63586
- // the copy part don't need control to be interactable
63587
- const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
63588
- e.clipboardData.setData("text/plain", displayText);
63589
- e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
63590
- // the clear ui state part need control to be interactable
63591
- dispatchRequestInteraction(pickerEl, {
63592
- event: e,
63593
- name: "cut",
63594
- allowed: () => {
63595
- dispatchRequestClearUIState(inputRef.current, e);
63596
- }
63597
- });
63598
- e.preventDefault();
64539
+ onnavi_request_unselect: e => {
64540
+ requestPickerListEntry(ref.current, inputRef.current, e, "unselect");
63599
64541
  },
63600
- onPaste: e => {
63601
- const pickerEl = ref.current;
63602
- if (isWithinPickerContent(e.target, pickerEl)) {
63603
- // Don't intercept inside the picker popup content.
63604
- return;
63605
- }
63606
- const naviData = e.clipboardData.getData("application/x-navi");
63607
- let pasteValue;
63608
- if (naviData) {
63609
- try {
63610
- pasteValue = JSON.parse(naviData);
63611
- } catch {
63612
- pasteValue = naviData;
63613
- }
63614
- } else {
63615
- pasteValue = e.clipboardData.getData("text/plain");
63616
- }
63617
- dispatchRequestInteraction(pickerEl, {
63618
- event: e,
63619
- name: "paste",
63620
- allowed: () => {
63621
- dispatchRequestSetUIState(inputRef.current, pasteValue, {
63622
- event: e
64542
+ children: [jsxs("span", {
64543
+ className: "navi_picker_box",
64544
+ children: [variant === "headless" ? null : jsx(LoadingOutline, {
64545
+ loading: loading,
64546
+ color: "var(--picker-loader-color)",
64547
+ inset: -2
64548
+ }), jsx(PickerInput, {
64549
+ tabIndex: variant === "headless" ? -1 : undefined,
64550
+ "aria-hidden": variant === "headless" ? "true" : undefined,
64551
+ ...inputProps,
64552
+ // eslint-disable-next-line react/no-children-prop
64553
+ children: undefined // we will render children into the div
64554
+ ,
64555
+
64556
+ ui: ui,
64557
+ onFocus: e => {
64558
+ inputProps.onFocus?.(e);
64559
+ e.target.select();
64560
+ },
64561
+ onCopy: e => {
64562
+ const pickerEl = ref.current;
64563
+ if (isWithinPickerContent(e.target, pickerEl)) {
64564
+ return;
64565
+ }
64566
+ const uiState = uiStateController.uiState;
64567
+ if (uiState === undefined) {
64568
+ return;
64569
+ }
64570
+ e.preventDefault();
64571
+ const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
64572
+ e.clipboardData.setData("text/plain", displayText);
64573
+ e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
64574
+ },
64575
+ onCut: e => {
64576
+ const pickerEl = ref.current;
64577
+ if (isWithinPickerContent(e.target, pickerEl)) {
64578
+ return;
64579
+ }
64580
+ const uiState = uiStateController.uiState;
64581
+ if (uiState === undefined) {
64582
+ return;
64583
+ }
64584
+ // the copy part don't need control to be interactable
64585
+ const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
64586
+ e.clipboardData.setData("text/plain", displayText);
64587
+ e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
64588
+ // the clear ui state part need control to be interactable
64589
+ dispatchRequestInteraction(pickerEl, {
64590
+ event: e,
64591
+ name: "cut",
64592
+ allowed: () => {
64593
+ dispatchRequestClearUIState(inputRef.current, e);
64594
+ }
63623
64595
  });
64596
+ e.preventDefault();
64597
+ },
64598
+ onPaste: e => {
64599
+ const pickerEl = ref.current;
64600
+ if (isWithinPickerContent(e.target, pickerEl)) {
64601
+ // Don't intercept inside the picker popup content.
64602
+ return;
64603
+ }
64604
+ const naviData = e.clipboardData.getData("application/x-navi");
64605
+ let pasteValue;
64606
+ if (naviData) {
64607
+ try {
64608
+ pasteValue = JSON.parse(naviData);
64609
+ } catch {
64610
+ pasteValue = naviData;
64611
+ }
64612
+ } else {
64613
+ pasteValue = e.clipboardData.getData("text/plain");
64614
+ }
64615
+ dispatchRequestInteraction(pickerEl, {
64616
+ event: e,
64617
+ name: "paste",
64618
+ allowed: () => {
64619
+ dispatchRequestSetUIState(inputRef.current, pasteValue, {
64620
+ event: e
64621
+ });
64622
+ }
64623
+ });
64624
+ e.preventDefault();
63624
64625
  }
63625
- });
63626
- e.preventDefault();
63627
- }
63628
- }), variant === "icon" || variant === "headless" || ui === "default" ? null : jsx(Text, {
63629
- className: "navi_picker_value",
63630
- "navi-placeholder": value === undefined || value === "" ? "" : undefined,
63631
- maxLines: maxLines,
63632
- children: jsx(PickerContext.Provider, {
63633
- value: {
63634
- value,
63635
- placeholder,
63636
- maxLines
63637
- },
63638
- children: ui === undefined ? jsx(PickerDefaultUI, {}) : ui
63639
- })
63640
- }), variant === "headless" || ui === "default" ? null : jsx("span", {
63641
- className: "navi_picker_right_slot",
63642
- children: jsx(ControlIdContext.Provider, {
63643
- value: undefined,
63644
- children: jsx(ControlNameContext.Provider, {
63645
- value: undefined,
63646
- children: clearable && value !== undefined && value !== "" ? jsx(Button, {
63647
- command: "--navi-clear",
63648
- commandFor: inputProps.id
63649
- // The question, asked before the clear rather than by the
63650
- // action the clear sends — see the --navi-clear command.
63651
- ,
63652
-
63653
- confirm: clearConfirm,
63654
- confirmPopupContent: clearConfirmPopupContent,
63655
- tabIndex: "-1"
63656
- // No navi-focus-delegate, unlike the identical button inside an
63657
- // input: handing focus back to the picker's own input is what
63658
- // opens the popup, and clearing is the opposite intention.
63659
- ,
63660
-
63661
- icon: true,
63662
- variant: "discrete"
63663
- // What is busy once the clear is sent is the picker — the value
63664
- // being removed is the whole field's, and the picker already
63665
- // draws the wait around all of it. Two outlines for one wait is
63666
- // one too many.
63667
- ,
63668
-
63669
- loadingOutline: false
63670
- // preventDefault, not just tabIndex="-1": a mousedown focuses
63671
- // its target before any click happens, and this button should
63672
- // never hold focus at all — the field keeps it.
63673
- ,
63674
-
63675
- onMouseDown: e => {
63676
- e.preventDefault();
63677
- },
63678
- flex: true,
63679
- align: "center",
63680
- children: jsx(Icon, {
64626
+ }), variant === "icon" || variant === "headless" || ui === "default" ? null : jsx(Text, {
64627
+ className: "navi_picker_value",
64628
+ "navi-placeholder": uiStateHoldsNothing(value) ? "" : undefined,
64629
+ maxLines: maxLines,
64630
+ children: jsx(PickerOwnContent, {
64631
+ children: jsx(PickerContext.Provider, {
64632
+ value: {
64633
+ value,
64634
+ placeholder,
64635
+ maxLines
64636
+ },
64637
+ children: jsx(MaxLinesContext.Provider, {
64638
+ value: maxLines,
64639
+ children: ui === undefined ? jsx(PickerDefaultUI, {}) : ui
64640
+ })
64641
+ })
64642
+ })
64643
+ }), variant === "headless" || ui === "default" ? null : jsx("span", {
64644
+ className: "navi_picker_right_slot",
64645
+ children: jsx(PickerOwnContent, {
64646
+ children: clearable && interactive && value !== undefined && value !== "" ? jsx(Button, {
64647
+ command: "--navi-clear",
64648
+ commandFor: inputProps.id
64649
+ // The question, asked before the clear rather than by the
64650
+ // action the clear sends see the --navi-clear command.
64651
+ ,
64652
+
64653
+ confirm: clearConfirm,
64654
+ confirmPopupContent: clearConfirmPopupContent,
64655
+ tabIndex: "-1"
64656
+ // No navi-focus-delegate, unlike the identical button inside an
64657
+ // input: handing focus back to the picker's own input is what
64658
+ // opens the popup, and clearing is the opposite intention.
64659
+ ,
64660
+
64661
+ icon: true,
64662
+ variant: "discrete"
64663
+ // What is busy once the clear is sent is the picker — the value
64664
+ // being removed is the whole field's, and the picker already
64665
+ // draws the wait around all of it. Two outlines for one wait is
64666
+ // one too many.
64667
+ ,
64668
+
64669
+ loadingOutline: false
64670
+ // preventDefault, not just tabIndex="-1": a mousedown focuses
64671
+ // its target before any click happens, and this button should
64672
+ // never hold focus at all the field keeps it.
64673
+ ,
64674
+
64675
+ onMouseDown: e => {
64676
+ e.preventDefault();
64677
+ },
64678
+ flex: true,
64679
+ align: "center",
64680
+ children: jsx(Icon, {
64681
+ size: rightSlotIconSize,
64682
+ lineOverflow: "allow",
64683
+ children: jsx(CloseSvg, {})
64684
+ })
64685
+ }) : rightSlot === undefined ?
64686
+ // lineOverflow: what sits in the slot is an affordance, not a
64687
+ // character — a caller asking for a bigger one wants it bigger,
64688
+ // not capped at the height of the line it sits on
64689
+ jsx(Icon, {
63681
64690
  size: rightSlotIconSize,
63682
64691
  lineOverflow: "allow",
63683
- children: jsx(CloseSvg, {})
63684
- })
63685
- }) : rightSlot === undefined ?
63686
- // lineOverflow: what sits in the slot is an affordance, not a
63687
- // character — a caller asking for a bigger one wants it bigger,
63688
- // not capped at the height of the line it sits on
63689
- jsx(Icon, {
63690
- size: rightSlotIconSize,
63691
- lineOverflow: "allow",
63692
- children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
63693
- }) : rightSlot
64692
+ children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
64693
+ }) : rightSlot
64694
+ })
64695
+ })]
64696
+ }), jsx(ControlFacadeChildrenWrapper, {
64697
+ ...facadeChildrenProps,
64698
+ children: jsx("div", {
64699
+ className: "navi_picker_content",
64700
+ children: children
63694
64701
  })
63695
- })
63696
- })]
63697
- }), jsx(ControlFacadeChildrenWrapper, {
63698
- ...facadeChildrenProps,
63699
- children: jsx("div", {
63700
- className: "navi_picker_content",
63701
- children: children
64702
+ })]
63702
64703
  })
63703
- })]
64704
+ })
64705
+ );
64706
+ };
64707
+ // What the picker draws itself — the value it shows, the furniture in its slot,
64708
+ // and whatever a caller puts in either — is not another control of the field
64709
+ // around it: none of it may take the id (nor the name) a <Field> hands down,
64710
+ // which is the picker's. Two controls under one id is one registry entry, and
64711
+ // the one that unmounts first — the clear cross the moment the field it emptied
64712
+ // is empty, a chip the moment its value is taken out — takes the picker's own
64713
+ // entry with it, leaving the picker looking for a controller that is gone.
64714
+ const PickerOwnContent = ({
64715
+ children
64716
+ }) => jsx(ControlIdContext.Provider, {
64717
+ value: undefined,
64718
+ children: jsx(ControlNameContext.Provider, {
64719
+ value: undefined,
64720
+ children: children
64721
+ })
64722
+ });
64723
+
64724
+ // `id` is what --navi-select/--navi-unselect carry (a list addresses its rows by
64725
+ // id); asked of a picker, what they carry is one entry of the list the picker
64726
+ // holds — the same thing a `<Picker.Chip value>` stands for.
64727
+ const requestPickerListEntry = (pickerEl, pickerInputEl, e, goal) => {
64728
+ const uiState = getUIStateFromElement(pickerInputEl);
64729
+ if (!Array.isArray(uiState)) {
64730
+ return;
64731
+ }
64732
+ const {
64733
+ id: entry
64734
+ } = e.detail;
64735
+ const isThere = uiState.some(item => compareTwoJsValues(item, entry));
64736
+ if (goal === "select" ? isThere : !isThere) {
64737
+ return;
64738
+ }
64739
+ const uiStateNext = goal === "select" ? [...uiState, entry] : uiState.filter(item => !compareTwoJsValues(item, entry));
64740
+ dispatchRequestInteraction(pickerEl, {
64741
+ event: e,
64742
+ name: goal,
64743
+ prevented: () => e.preventDefault(),
64744
+ allowed: () => {
64745
+ dispatchRequestSetUIState(pickerInputEl, uiStateNext, {
64746
+ event: e
64747
+ });
64748
+ }
63704
64749
  });
63705
64750
  };
63706
64751
  const isWithinPickerContent = (el, pickerEl) => {
@@ -63845,6 +64890,7 @@ const PickerFirstResolver = props => {
63845
64890
  });
63846
64891
  };
63847
64892
  const Picker = createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
64893
+ Picker.Chip = PickerChip;
63848
64894
  Picker.UI = PickerDefaultUI;
63849
64895
  Picker.UI.Date = PickerDateUI;
63850
64896
  Picker.UI.Time = PickerTimeUI;
@@ -63957,7 +65003,27 @@ const css$t = /* css */`
63957
65003
  of one's own still wins — an inline style beats a stylesheet. */
63958
65004
  border: var(--navi-control-border-width) solid
63959
65005
  var(--navi-control-border-color);
63960
- border-radius: var(--navi-control-border-radius);
65006
+ /* Squared from the outside, corner by corner: a spin is not always the
65007
+ member a Group joins — it can arrive wrapped — so the ask travels down
65008
+ as inherited custom properties rather than as a radius landing on this
65009
+ element. The chevrons take these corners back by inherit (see below), so
65010
+ they follow. */
65011
+ border-top-left-radius: var(
65012
+ --x-corner-top-left-radius,
65013
+ var(--navi-control-border-radius)
65014
+ );
65015
+ border-top-right-radius: var(
65016
+ --x-corner-top-right-radius,
65017
+ var(--navi-control-border-radius)
65018
+ );
65019
+ border-bottom-right-radius: var(
65020
+ --x-corner-bottom-right-radius,
65021
+ var(--navi-control-border-radius)
65022
+ );
65023
+ border-bottom-left-radius: var(
65024
+ --x-corner-bottom-left-radius,
65025
+ var(--navi-control-border-radius)
65026
+ );
63961
65027
  outline-width: var(--navi-focus-outline-width);
63962
65028
  /* Just outside the border, never on it: the ring belongs to the whole
63963
65029
  control — the two chevrons included, since pressing one lands the
@@ -64014,6 +65080,13 @@ const css$t = /* css */`
64014
65080
  opens its calendar. Around the whole control it would open under a chevron;
64015
65081
  around the value it opens under the value one pressed. */
64016
65082
  .navi_picker_spin_middle {
65083
+ /* The ask stops here: the frame is the spin's box, and the picker or the
65084
+ field standing in the middle of it is behind that frame, not at a seam. */
65085
+ --x-corner-top-left-radius: initial;
65086
+ --x-corner-top-right-radius: initial;
65087
+ --x-corner-bottom-right-radius: initial;
65088
+ --x-corner-bottom-left-radius: initial;
65089
+
64017
65090
  position: relative;
64018
65091
  display: flex;
64019
65092
  min-width: 0;
@@ -64191,7 +65264,26 @@ const css$t = /* css */`
64191
65264
  font-family: var(--navi-control-font-family);
64192
65265
  border: var(--navi-control-border-width) solid
64193
65266
  var(--navi-control-border-color);
64194
- border-radius: var(--navi-control-border-radius);
65267
+ /* Squared from the outside, corner by corner (see .navi_picker_spin
65268
+ above): the group is the member a Group joins, and it can arrive
65269
+ wrapped. The spins inside give their radius up and take the corners of
65270
+ this one back through inherit, so they follow. */
65271
+ border-top-left-radius: var(
65272
+ --x-corner-top-left-radius,
65273
+ var(--navi-control-border-radius)
65274
+ );
65275
+ border-top-right-radius: var(
65276
+ --x-corner-top-right-radius,
65277
+ var(--navi-control-border-radius)
65278
+ );
65279
+ border-bottom-right-radius: var(
65280
+ --x-corner-bottom-right-radius,
65281
+ var(--navi-control-border-radius)
65282
+ );
65283
+ border-bottom-left-radius: var(
65284
+ --x-corner-bottom-left-radius,
65285
+ var(--navi-control-border-radius)
65286
+ );
64195
65287
  outline-width: var(--navi-focus-outline-width);
64196
65288
  outline-color: var(--navi-focus-outline-color);
64197
65289
  outline-offset: 0px;