@jsenv/navi 0.29.443 → 0.29.445

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.
@@ -6502,6 +6502,10 @@ const CONTROL_PROP_SET = new Set([
6502
6502
  "charGuard",
6503
6503
  "maxLengthGuard",
6504
6504
 
6505
+ // A checkbox that is a yes/no: `true` checked, `false` unchecked, instead of
6506
+ // HTML's value-or-nothing (see resolveEmptyUIState).
6507
+ "boolean",
6508
+
6505
6509
  // This control answers for itself: it registers into no group, no form and no
6506
6510
  // picker around it (see useUIStateController). It belongs here rather than
6507
6511
  // being deleted off `props` where it is read: props is the vnode's own object
@@ -43198,6 +43202,14 @@ const useUIStateController = (
43198
43202
  return ownUIState || parentUIState;
43199
43203
  })
43200
43204
  : ownUIStateSignal;
43205
+ // What the control is worth, which its action is bound to: what it
43206
+ // holds, or its own empty when it holds nothing. Apart from
43207
+ // uiStateSignal because unchecked is `undefined` there (the DOM reads any
43208
+ // other value as checked), while a yes/no unchecked is worth `false`.
43209
+ const emptyUIStateSignal = signal(emptyUIState);
43210
+ const valueSignal = computed(() =>
43211
+ uiStateOrEmpty(uiStateSignal.value, emptyUIStateSignal.value),
43212
+ );
43201
43213
 
43202
43214
  // The two-way half of a bound `signal` prop: setting it re-renders and
43203
43215
  // re-syncs via state_prop_change, but with the same value → guarded as a
@@ -43246,6 +43258,7 @@ const useUIStateController = (
43246
43258
  // travel command ended up carrying a picker's selection: see
43247
43259
  // resolveCommandValue in commands.js.
43248
43260
  ownUIStateSignal,
43261
+ valueSignal,
43249
43262
  value: controlInfo.value,
43250
43263
  // The suggestion this control started on — what tells a field showing
43251
43264
  // its default from one carrying an answer (see isUIStateHeld).
@@ -43309,8 +43322,12 @@ const useUIStateController = (
43309
43322
  }
43310
43323
  s.uiActionInternal?.(currentUIState, e);
43311
43324
  if (s.uiAction) {
43312
- debugUIState(`calling uiAction for ${controlType}`, currentUIState);
43313
- s.uiAction(currentUIState, e);
43325
+ const value = uiStateOrEmpty(
43326
+ currentUIState,
43327
+ controller.emptyUIState,
43328
+ );
43329
+ debugUIState(`calling uiAction for ${controlType}`, value);
43330
+ s.uiAction(value, e);
43314
43331
  }
43315
43332
  if (skipCommand) ; else {
43316
43333
  const command = controller.controlHostProps.command;
@@ -43814,6 +43831,7 @@ const useUIStateController = (
43814
43831
  return {
43815
43832
  controller,
43816
43833
  parentUiStateSignalHolder,
43834
+ emptyUIStateSignal,
43817
43835
  ...liveValues(),
43818
43836
  };
43819
43837
  },
@@ -43935,6 +43953,7 @@ const useUIStateController = (
43935
43953
  );
43936
43954
  scope.parentUiStateSignalHolder.value =
43937
43955
  parentUIStateController?.uiStateSignal ?? null;
43956
+ scope.emptyUIStateSignal.value = emptyUIState;
43938
43957
 
43939
43958
  const { controller } = scope;
43940
43959
  const controllerRef = controller.ref;
@@ -44145,14 +44164,11 @@ const keptAndNamedChildUIStates = (children, fallbackState, kept) => {
44145
44164
  const readNamedChildUIStates = (children, { warnNameless }) => {
44146
44165
  const values = {};
44147
44166
  for (const child of children) {
44148
- const { name, emptyUIState } = child;
44167
+ const { name } = child;
44149
44168
  // A control holding nothing writes its own empty, not a hole: the key is in
44150
44169
  // the object either way, and what is read from it keeps the shape the
44151
44170
  // reader was promised (see resolveEmptyUIState).
44152
- const uiState =
44153
- child.uiState === undefined && emptyUIState !== undefined
44154
- ? emptyUIState
44155
- : child.uiState;
44171
+ const uiState = uiStateOrEmpty(child.uiState, child.emptyUIState);
44156
44172
  if (!name) {
44157
44173
  if (isNamelessGrouping(child, uiState)) {
44158
44174
  Object.assign(
@@ -45499,11 +45515,10 @@ const useUIFacadeStateController = (props, realUIStateController) => {
45499
45515
  // whose last item was unselected holds [], the same thing clearing it
45500
45516
  // leaves — not a value that changes type on its owner the moment it
45501
45517
  // empties.
45502
- const { emptyUIState } = s.realUIStateController;
45503
- const uiStateToAdopt =
45504
- child.uiState === undefined && emptyUIState !== undefined
45505
- ? emptyUIState
45506
- : child.uiState;
45518
+ const uiStateToAdopt = uiStateOrEmpty(
45519
+ child.uiState,
45520
+ s.realUIStateController.emptyUIState,
45521
+ );
45507
45522
  s.realUIStateController.setUIState(uiStateToAdopt, propagateUpEvent);
45508
45523
  updatingRef.current = false;
45509
45524
  },
@@ -45821,8 +45836,9 @@ const resolveEmptyUIState = (props, controlType) => {
45821
45836
  if (controlType === "input" && props.type === "checkbox") {
45822
45837
  // A checkbox is a member of a set, the way HTML has it: checked it sends
45823
45838
  // its value ("on" by default), unchecked it sends nothing at all. Only one
45824
- // holding `true` is a yes/no, and a yes/no nobody said yes to is `false`.
45825
- return props.value === true ? false : undefined;
45839
+ // declared `boolean` is a yes/no, and a yes/no nobody said yes to is
45840
+ // `false`.
45841
+ return props.boolean ? false : undefined;
45826
45842
  }
45827
45843
  const stateShape = props["navi-state-shape"];
45828
45844
  if (stateShape === "array") {
@@ -45834,6 +45850,15 @@ const resolveEmptyUIState = (props, controlType) => {
45834
45850
  return undefined;
45835
45851
  };
45836
45852
 
45853
+ // What a control holding `uiState` is worth: its own empty when it holds
45854
+ // nothing, so the reader gets the shape it was promised.
45855
+ const uiStateOrEmpty = (uiState, emptyUIState) => {
45856
+ if (uiState === undefined && emptyUIState !== undefined) {
45857
+ return emptyUIState;
45858
+ }
45859
+ return uiState;
45860
+ };
45861
+
45837
45862
  // What a cleared control shows: its own empty, kept in the shape it was holding.
45838
45863
  const resolveClearedUIState = (controller) => {
45839
45864
  const { controlType, props } = controller;
@@ -46069,7 +46094,7 @@ const useControlProps = (props, {
46069
46094
  // of a form would re-render at each change of any of its fields.
46070
46095
  // eslint-disable-next-line no-unused-expressions
46071
46096
  uiStateController.ownUIStateSignal.value;
46072
- const boundAction = useAction(props.action, uiStateController.uiStateSignal);
46097
+ const boundAction = useAction(props.action, uiStateController.valueSignal);
46073
46098
  const [controlRootProps, controlHostProps] = useInteractiveProps(props, {
46074
46099
  uiStateController,
46075
46100
  boundAction,
@@ -56643,7 +56668,8 @@ a.navi_button {
56643
56668
 
56644
56669
  &[data-icon] {
56645
56670
  --button-padding: 0;
56646
- display: inline-flex;
56671
+ justify-content: center;
56672
+ align-items: center;
56647
56673
  }
56648
56674
 
56649
56675
  &[data-cta] {
@@ -56741,7 +56767,12 @@ const ButtonUI = props => {
56741
56767
  });
56742
56768
  return jsxs(Box, {
56743
56769
  inline: true,
56744
- block: true,
56770
+ block: true
56771
+ // An icon is laid out as a box rather than on a line of text that is not
56772
+ // there, which is also what makes alignX/alignY place it. Before the
56773
+ // caller's props, so their own flex direction wins.
56774
+ ,
56775
+ flex: icon ? "x" : undefined,
56745
56776
  ...buttonControlRootProps,
56746
56777
  ...buttonControlHostProps,
56747
56778
  // eslint-disable-next-line react/no-children-prop
@@ -58190,7 +58221,23 @@ const css$M = /* css */`@layer navi {
58190
58221
  const InputCheckbox = props => {
58191
58222
  const defaultRef = useRef();
58192
58223
  props.ref = props.ref || defaultRef;
58193
- props.value = props.value === undefined ? "on" : props.value;
58224
+ const valueWarnedRef = useRef(false);
58225
+ if (!valueWarnedRef.current) {
58226
+ // `true` is what `boolean` writes into `value` below, and Preact hands the
58227
+ // same props object back on a re-render the component triggers itself.
58228
+ if (props.boolean && props.value !== undefined && props.value !== true) {
58229
+ valueWarnedRef.current = true;
58230
+ console.warn(`[checkbox] "boolean" is true or false, its "value" is ignored: remove value={${JSON.stringify(props.value)}}.`);
58231
+ } else if (!props.boolean && props.value === true) {
58232
+ valueWarnedRef.current = true;
58233
+ console.warn(`[checkbox] value={true} is a value like any other: unchecked is undefined. For a yes/no (true or false), use the "boolean" prop.`);
58234
+ }
58235
+ }
58236
+ if (props.boolean) {
58237
+ props.value = true;
58238
+ } else if (props.value === undefined) {
58239
+ props.value = "on";
58240
+ }
58194
58241
  if (props.headless) {
58195
58242
  return jsx(InputCheckboxHeadless, {
58196
58243
  ...props,
@@ -81316,7 +81363,6 @@ const css$w = /* css */`@layer navi;
81316
81363
  background: var(--x-background);
81317
81364
  background-color: var(--x-background-color);
81318
81365
  border-radius: 1em;
81319
- align-items: stretch;
81320
81366
  display: inline;
81321
81367
  position: relative;
81322
81368
 
@@ -81326,13 +81372,25 @@ const css$w = /* css */`@layer navi;
81326
81372
 
81327
81373
  &[data-text-overflow] {
81328
81374
  display: inline;
81375
+ }
81329
81376
 
81330
- & .navi_text_overflow_wrapper {
81331
- gap: 0;
81332
- }
81377
+ &[data-has-button] {
81378
+ align-items: center;
81379
+ display: inline-flex;
81380
+ }
81381
+
81382
+ & .navi_badge_label {
81383
+ text-overflow: ellipsis;
81384
+ text-box: trim-both cap alphabetic;
81385
+ overflow-clip-margin: padding-box calc((1lh - 1em) / 2);
81386
+ align-self: baseline;
81387
+ min-width: 0;
81388
+ overflow-x: clip;
81333
81389
  }
81334
81390
 
81335
81391
  & [role="button"] {
81392
+ box-sizing: content-box;
81393
+ height: 1cap;
81336
81394
  margin-top: calc(-1 * var(--x-badge-padding-top));
81337
81395
  margin-bottom: calc(-1 * var(--x-badge-padding-bottom));
81338
81396
  padding-top: var(--x-badge-padding-top);
@@ -81342,6 +81400,7 @@ const css$w = /* css */`@layer navi;
81342
81400
  cursor: pointer;
81343
81401
  pointer-events: auto;
81344
81402
  user-select: none;
81403
+ flex-shrink: 0;
81345
81404
  align-items: center;
81346
81405
  display: inline-flex;
81347
81406
 
@@ -81390,6 +81449,16 @@ const BadgeUI = ({
81390
81449
  ref
81391
81450
  } = props;
81392
81451
  useAccentColorAttributes(ref, null);
81452
+ const childArray = toChildArray(children);
81453
+ if (childArray.some(isBadgeButton)) {
81454
+ props = {
81455
+ "inline": true,
81456
+ "flex": true,
81457
+ ...props,
81458
+ "data-has-button": ""
81459
+ };
81460
+ children = wrapLabels(childArray);
81461
+ }
81393
81462
  return jsx(Text, {
81394
81463
  className: withPropsClassName("navi_badge", className),
81395
81464
  bold: true,
@@ -81450,6 +81519,35 @@ const BadgeButtonUI = props => {
81450
81519
  });
81451
81520
  };
81452
81521
  Badge.Button = BadgeButton;
81522
+ const isBadgeButton = child => {
81523
+ return isValidElement(child) && child.type === BadgeButton;
81524
+ };
81525
+ // Each run of children between buttons becomes one label: a single flex item
81526
+ // holding its text, so the trim reaches a bare string and the text keeps its
81527
+ // own baseline alignment inside.
81528
+ const wrapLabels = childArray => {
81529
+ const items = [];
81530
+ let run = [];
81531
+ const closeRun = () => {
81532
+ if (run.length > 0) {
81533
+ items.push(jsx("span", {
81534
+ className: "navi_badge_label",
81535
+ children: run
81536
+ }));
81537
+ run = [];
81538
+ }
81539
+ };
81540
+ for (const child of childArray) {
81541
+ if (isBadgeButton(child)) {
81542
+ closeRun();
81543
+ items.push(child);
81544
+ } else {
81545
+ run.push(child);
81546
+ }
81547
+ }
81548
+ closeRun();
81549
+ return items;
81550
+ };
81453
81551
 
81454
81552
  installImportMetaCssBuild(import.meta);
81455
81553
  const css$v = /* css */`@layer navi;
@@ -82189,8 +82287,6 @@ const PickerChip = ({
82189
82287
  ...rest
82190
82288
  }) => {
82191
82289
  return jsxs(Badge, {
82192
- inline: true,
82193
- flex: true,
82194
82290
  ...rest,
82195
82291
  children: [children, jsx(Badge.Button, {
82196
82292
  selfInteractions: "click",
@@ -87002,14 +87098,7 @@ const SplitButton = props => {
87002
87098
  children: [jsx(Button, {
87003
87099
  icon: true,
87004
87100
  paddingX: "s",
87005
- expandY: true
87006
- // flex + align: the icon is the whole content, so it is laid out as
87007
- // its own box in the middle of the button. Left to flow inline it
87008
- // would sit on the baseline of a line of text that is not there,
87009
- // with the descender space still kept below it.
87010
- ,
87011
- flex: true,
87012
- align: "center",
87101
+ expandY: true,
87013
87102
  "aria-label": menuLabel,
87014
87103
  command: "--navi-open",
87015
87104
  commandFor: menuId,
@@ -94128,8 +94217,6 @@ const ButtonCopyToClipboard = ({
94128
94217
  children: "Copié !"
94129
94218
  }), jsx(Button, {
94130
94219
  className: "navi_copy_button",
94131
- flex: "y",
94132
- alignY: "center",
94133
94220
  icon: true,
94134
94221
  revealOnInteraction: true,
94135
94222
  square: true,
@@ -95018,8 +95105,6 @@ const MessageBox = ({
95018
95105
  action: onClose,
95019
95106
  icon: true,
95020
95107
  border: "none",
95021
- alignX: "center",
95022
- alignY: "center",
95023
95108
  style: {
95024
95109
  ":hover": {
95025
95110
  backgroundColor: "rgba(0, 0, 0, 0.1)"