@jsenv/navi 0.28.9 → 0.28.11

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.
@@ -5356,7 +5356,7 @@ const stateSignal = (defaultValue, options = {}) => {
5356
5356
  const existingEntry = globalSignalRegistry.get(signalIdString);
5357
5357
  {
5358
5358
  throw new Error(
5359
- `Signal ID conflict: A signal with ID "${signalIdString}" already exists (existing default: ${existingEntry.options.getDefaultValue()})`,
5359
+ `Signal ID conflict: A signal with ID "${signalIdString}" already exists (existing default: ${existingEntry.options.getDefaultValue()}). If this is the same stateSignal() call site running twice, the module was evaluated twice — check the network tab for the same file requested both bare and with "?hot=".`,
5360
5360
  );
5361
5361
  }
5362
5362
  }
@@ -6521,6 +6521,13 @@ const DIMENSION_PROPS = {
6521
6521
  const inHorizontalFlexFlow =
6522
6522
  parentBoxFlow === "flex-x" || parentBoxFlow === "inline-flex-x";
6523
6523
  if (inHorizontalFlexFlow) {
6524
+ if (value === "content") {
6525
+ // flex-basis stays auto: the item still takes the free space, but its
6526
+ // content size is what line-breaking sees — so in a flexWrap parent a
6527
+ // neighbor that no longer fits wraps to the next line. With basis 0%
6528
+ // (below) nothing ever wraps: every item claims a size of zero.
6529
+ return { flexGrow: 1 };
6530
+ }
6524
6531
  // Parent is flex-x: grow as flex item
6525
6532
  return { flexGrow: 1, flexBasis: "0%" };
6526
6533
  }
@@ -6540,6 +6547,11 @@ const DIMENSION_PROPS = {
6540
6547
  const inVerticalFlexFlow =
6541
6548
  parentBoxFlow === "flex-y" || parentBoxFlow === "inline-flex-y";
6542
6549
  if (inVerticalFlexFlow) {
6550
+ if (value === "content") {
6551
+ // Same as expandX="content": grow from the content size, so a
6552
+ // flexWrap parent can wrap.
6553
+ return { flexGrow: 1 };
6554
+ }
6543
6555
  // Parent is flex-y: grow as flex item
6544
6556
  return { flexGrow: 1, flexBasis: "0%" };
6545
6557
  }
@@ -17435,8 +17447,12 @@ const canContainCallout = element => {
17435
17447
  if (VOID_ELEMENT_TAG_NAMES.has(element.tagName)) {
17436
17448
  return false;
17437
17449
  }
17438
- if (element.tagName === "BUTTON") {
17439
- // callout itself contains a button, browser would not let that happen
17450
+ if (element.tagName === "BUTTON" || element.getAttribute("role") === "button") {
17451
+ // Never mount the callout inside a button, native or role="button":
17452
+ // - a <button> cannot contain the callout's own close button
17453
+ // - presses inside the callout would bubble into the button's own press
17454
+ // handlers: a preventDefault there makes the callout text unselectable,
17455
+ // and a handler that opens the callout re-opens it (visible blink)
17440
17456
  return false;
17441
17457
  }
17442
17458
  return true;
@@ -33930,10 +33946,13 @@ installImportMetaCssBuild(import.meta);const css$E = /* css */`
33930
33946
  black
33931
33947
  );
33932
33948
  /* Readonly */
33949
+ /* Fading toward the surface, not toward white: "washed out" means
33950
+ closer to the paper behind the button, whatever color that paper is
33951
+ (white here IS the surface only in the light theme). */
33933
33952
  --button-border-color-readonly: color-mix(
33934
33953
  in srgb,
33935
33954
  var(--button-border-color) 30%,
33936
- white
33955
+ var(--navi-surface-color)
33937
33956
  );
33938
33957
  --button-background-color-readonly: var(--button-background-color);
33939
33958
  --button-color-readonly: color-mix(
@@ -35070,7 +35089,7 @@ installImportMetaCssBuild(import.meta);/**
35070
35089
  * - variant="discrete" + backgroundColor: the color applies at rest and hover,
35071
35090
  * and the field goes transparent while focused.
35072
35091
  */
35073
- const css$D = /* css */`
35092
+ const inputCss = /* css */`
35074
35093
  @layer navi {
35075
35094
  .navi_input {
35076
35095
  --border-radius: var(--navi-control-border-radius);
@@ -35401,7 +35420,7 @@ const useInputTextualProps = props => {
35401
35420
  });
35402
35421
  };
35403
35422
  const InputTextualUI = props => {
35404
- import.meta.css = [css$D, "@jsenv/navi/src/control/input/input_textual.jsx"];
35423
+ import.meta.css = [inputCss, "@jsenv/navi/src/control/input/input_textual.jsx"];
35405
35424
  // Spacing props travel to CSS as a raw custom property value, so the size
35406
35425
  // keywords have to become lengths here — "s" reaching CSS untouched makes the
35407
35426
  // declaration invalid, silently, and the gap just goes away.
@@ -35520,6 +35539,9 @@ const RealInput = ({
35520
35539
  "navi-max-length": maxLength
35521
35540
  });
35522
35541
  };
35542
+
35543
+ // Shared with textarea.jsx: a textarea is styled as a .navi_input box, so the
35544
+ // two read the same style props and pseudo states.
35523
35545
  const InputStyleCSSVars = {
35524
35546
  "slotSpacing": "--slot-spacing",
35525
35547
  "outlineWidth": "--outline-width",
@@ -36058,7 +36080,7 @@ installImportMetaCssBuild(import.meta);/**
36058
36080
  * This means an editable thing MUST have a parent with position relative that wraps the content and the eventual editable input
36059
36081
  *
36060
36082
  */
36061
- const css$C = /* css */`
36083
+ const css$D = /* css */`
36062
36084
  .navi_editable_wrapper {
36063
36085
  --inset-top: 0px;
36064
36086
  --inset-right: 0px;
@@ -36107,7 +36129,7 @@ const useEditionController = () => {
36107
36129
  };
36108
36130
  };
36109
36131
  const Editable = props => {
36110
- import.meta.css = [css$C, "@jsenv/navi/src/control/edition/editable.jsx"];
36132
+ import.meta.css = [css$D, "@jsenv/navi/src/control/edition/editable.jsx"];
36111
36133
  let {
36112
36134
  children,
36113
36135
  action,
@@ -36521,7 +36543,7 @@ HTMLFormElement.prototype.requestSubmit = function (submitter) {
36521
36543
  // form.dispatchEvent(customEvent);
36522
36544
  // };
36523
36545
 
36524
- installImportMetaCssBuild(import.meta);const css$B = /* css */`
36546
+ installImportMetaCssBuild(import.meta);const css$C = /* css */`
36525
36547
  .navi_group {
36526
36548
  --group-border-width: 1px;
36527
36549
 
@@ -36617,7 +36639,7 @@ const Group = ({
36617
36639
  vertical = row,
36618
36640
  ...props
36619
36641
  }) => {
36620
- import.meta.css = [css$B, "@jsenv/navi/src/control/group.jsx"];
36642
+ import.meta.css = [css$C, "@jsenv/navi/src/control/group.jsx"];
36621
36643
  return jsx(Box, {
36622
36644
  baseClassName: "navi_group",
36623
36645
  "data-vertical": vertical ? "" : undefined,
@@ -37027,7 +37049,7 @@ installImportMetaCssBuild(import.meta);/**
37027
37049
  * pair. One popup holding slides of its own contents has no such problem — and
37028
37050
  * it is the same component in the document, in a dialog or in a popover.
37029
37051
  */
37030
- const css$A = /* css */`
37052
+ const css$B = /* css */`
37031
37053
  /* Every slide in the same grid cell: the box then measures itself on the
37032
37054
  LARGEST of them, in both directions, without anything being measured by
37033
37055
  hand — which is also why nothing here resizes as the slides change. Each
@@ -37325,7 +37347,7 @@ const SlideContainer = ({
37325
37347
  children,
37326
37348
  ...rest
37327
37349
  }) => {
37328
- import.meta.css = [css$A, "@jsenv/navi/src/layout/slide_container.jsx"];
37350
+ import.meta.css = [css$B, "@jsenv/navi/src/layout/slide_container.jsx"];
37329
37351
  const debugFocus = useDebugFocus();
37330
37352
  const trackRef = useRef();
37331
37353
  // The box itself: it is what takes the keyboard when what is on screen holds
@@ -39925,7 +39947,7 @@ installImportMetaCssBuild(import.meta);/**
39925
39947
  * reaches the real container.
39926
39948
  */
39927
39949
  let openLocalDialogCount = 0;
39928
- const css$z = /* css */`
39950
+ const css$A = /* css */`
39929
39951
  @layer navi {
39930
39952
  .navi_dialog {
39931
39953
  /* Min gap between the dialog and the edges of its container. Written
@@ -40332,7 +40354,7 @@ const css$z = /* css */`
40332
40354
  * @param {import("ignore:preact").ComponentChildren} props.children
40333
40355
  */
40334
40356
  const Dialog = props => {
40335
- import.meta.css = [css$z, "@jsenv/navi/src/layout/dialog.jsx"];
40357
+ import.meta.css = [css$A, "@jsenv/navi/src/layout/dialog.jsx"];
40336
40358
  if (props.openController) {
40337
40359
  return jsx(ControlledDialog, {
40338
40360
  ...props
@@ -41150,7 +41172,7 @@ installImportMetaCssBuild(import.meta);/**
41150
41172
  * and applied.
41151
41173
  */
41152
41174
  let openLocalPopoverCount = 0;
41153
- const css$y = /* css */`
41175
+ const css$z = /* css */`
41154
41176
  @layer navi {
41155
41177
  .navi_popover {
41156
41178
  /* soft: user-configurable preferred max-height. Kept as a *default*
@@ -41517,7 +41539,7 @@ const css$y = /* css */`
41517
41539
  * @param {import("ignore:preact").ComponentChildren} props.children
41518
41540
  */
41519
41541
  const Popover = props => {
41520
- import.meta.css = [css$y, "@jsenv/navi/src/layout/popover.jsx"];
41542
+ import.meta.css = [css$z, "@jsenv/navi/src/layout/popover.jsx"];
41521
41543
  if (props.openController) {
41522
41544
  return jsx(ControlledPopover, {
41523
41545
  ...props
@@ -42481,7 +42503,7 @@ installImportMetaCssBuild(import.meta);/**
42481
42503
  * pass through untouched via `...rest` to whichever of Popover/Dialog
42482
42504
  * actually renders.
42483
42505
  */
42484
- const css$x = /* css */`
42506
+ const css$y = /* css */`
42485
42507
  @layer navi {
42486
42508
  .navi_popup {
42487
42509
  --popup-border-radius: var(--navi-popup-border-radius);
@@ -42575,7 +42597,7 @@ const css$x = /* css */`
42575
42597
  * @param {import("ignore:preact").ComponentChildren} props.children
42576
42598
  */
42577
42599
  const Popup = props => {
42578
- import.meta.css = [css$x, "@jsenv/navi/src/layout/popup.jsx"];
42600
+ import.meta.css = [css$y, "@jsenv/navi/src/layout/popup.jsx"];
42579
42601
  const {
42580
42602
  mode: modeProp,
42581
42603
  maxWidth,
@@ -42718,7 +42740,7 @@ const commitSubtree = (controller, e) => {
42718
42740
  }
42719
42741
  };
42720
42742
 
42721
- installImportMetaCssBuild(import.meta);const css$w = /* css */`
42743
+ installImportMetaCssBuild(import.meta);const css$x = /* css */`
42722
42744
  .navi_picker {
42723
42745
  /* Sizing ceilings (maxmax), background, box-shadow, outline, padding,
42724
42746
  overflow... are already handled correctly by Popup/Popover/Dialog
@@ -42830,7 +42852,7 @@ installImportMetaCssBuild(import.meta);const css$w = /* css */`
42830
42852
  }
42831
42853
  `;
42832
42854
  const PickerCustomResolver = props => {
42833
- import.meta.css = [css$w, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
42855
+ import.meta.css = [css$x, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
42834
42856
  if (props.children === undefined) {
42835
42857
  return jsx(PickerNative, {
42836
42858
  ...props
@@ -43475,7 +43497,7 @@ const LoadingIndicator = ({
43475
43497
  });
43476
43498
  };
43477
43499
 
43478
- installImportMetaCssBuild(import.meta);const css$v = /* css */`
43500
+ installImportMetaCssBuild(import.meta);const css$w = /* css */`
43479
43501
  @layer navi {
43480
43502
  .navi_separator {
43481
43503
  --size: 1px;
@@ -43553,7 +43575,7 @@ const Separator = ({
43553
43575
  style,
43554
43576
  ...props
43555
43577
  }) => {
43556
- import.meta.css = [css$v, "@jsenv/navi/src/layout/separator.jsx"];
43578
+ import.meta.css = [css$w, "@jsenv/navi/src/layout/separator.jsx"];
43557
43579
  return jsx(Box, {
43558
43580
  as: vertical ? "span" : "hr",
43559
43581
  ...props,
@@ -43649,6 +43671,10 @@ const lengthValue = value => typeof value === "number" ? `${value}px` : value;
43649
43671
  *
43650
43672
  * getTrackedItemByIndex(index): synchronous O(1) lookup of a visible item by
43651
43673
  * its visible rank. Returns undefined when index is out of range.
43674
+ *
43675
+ * peekItems(): the items as they stand right now, without waiting for the
43676
+ * deferred notification — what a sibling rendering after the items must read
43677
+ * to paint them in the same commit.
43652
43678
  */
43653
43679
 
43654
43680
  const useItemTracker = ({ onChange } = {}) => {
@@ -43751,7 +43777,8 @@ const createItemTracker = (onChange) => {
43751
43777
  visibleItemsSignal.value = visibleItems;
43752
43778
  someChange = true;
43753
43779
  }
43754
- const noMatchCountModified = noMatchCountSignal.peek() !== newNoMatchCount;
43780
+ const noMatchCountModified =
43781
+ noMatchCountSignal.peek() !== newNoMatchCount;
43755
43782
  if (noMatchCountModified) {
43756
43783
  noMatchCountSignal.value = newNoMatchCount;
43757
43784
  someChange = true;
@@ -43945,9 +43972,32 @@ const createItemTracker = (onChange) => {
43945
43972
  return registrations.get(key);
43946
43973
  };
43947
43974
 
43975
+ // The items as they stand right now, notification pending or not — same
43976
+ // content as itemsSignal, minus the wait.
43977
+ //
43978
+ // Items register during their own render, while the signal is only updated
43979
+ // on a deferred microtask (see notify): a sibling rendering after them would
43980
+ // otherwise paint from an empty list and correct itself a frame later. That
43981
+ // frame is visible whenever the painted size feeds a layout decision — a
43982
+ // dialog sizing itself on its content measures the empty version and shifts
43983
+ // once the real one lands. Reading this instead makes the first paint the
43984
+ // right one. Callers must still subscribe to itemsSignal to re-render on
43985
+ // LATER changes; this is the value to display, not the notification.
43986
+ const peekItems = () => {
43987
+ if (!notifyScheduled) {
43988
+ return itemsSignal.peek();
43989
+ }
43990
+ const items = [];
43991
+ for (const key of allOrderedKeys) {
43992
+ items.push(allRegistrations.get(key));
43993
+ }
43994
+ return items;
43995
+ };
43996
+
43948
43997
  return {
43949
43998
  useTrackItem,
43950
43999
  getTrackedItemByIndex,
44000
+ peekItems,
43951
44001
  itemsSignal,
43952
44002
  visibleItemsSignal,
43953
44003
  countSignal,
@@ -44010,7 +44060,7 @@ const ListItemFooter = props => {
44010
44060
  });
44011
44061
  };
44012
44062
 
44013
- installImportMetaCssBuild(import.meta);const css$u = /* css */`
44063
+ installImportMetaCssBuild(import.meta);const css$v = /* css */`
44014
44064
  @layer navi {
44015
44065
  .navi_list_container[navi-selectable] {
44016
44066
  /* Focus outline */
@@ -44214,7 +44264,7 @@ const ListSelectableResolver = props => {
44214
44264
  };
44215
44265
  const ListSelectable = props => {
44216
44266
  const Next = useNextResolver();
44217
- import.meta.css = [css$u, "@jsenv/navi/src/control/list/list_selectable.jsx"];
44267
+ import.meta.css = [css$v, "@jsenv/navi/src/control/list/list_selectable.jsx"];
44218
44268
  // we allow ourselves to auto-generate a name
44219
44269
  const defaultName = useId();
44220
44270
  props.name = props.name || `listbox_${defaultName}`;
@@ -44756,7 +44806,7 @@ const SeparatorContext = createContext(null);
44756
44806
  // Set by <List itemTransition>: each row then gets a view-transition-name of
44757
44807
  // its own, so a change wrapped in a view transition animates row by row.
44758
44808
  const ItemTransitionContext = createContext(false);
44759
- const css$t = /* css */`
44809
+ const css$u = /* css */`
44760
44810
  @layer navi {
44761
44811
  .navi_list_container {
44762
44812
  --list-outline-width: 1px;
@@ -45244,7 +45294,7 @@ const css$t = /* css */`
45244
45294
  }
45245
45295
  `;
45246
45296
  const ListUI = props => {
45247
- import.meta.css = [css$t, "@jsenv/navi/src/control/list/list.jsx"];
45297
+ import.meta.css = [css$u, "@jsenv/navi/src/control/list/list.jsx"];
45248
45298
  const {
45249
45299
  ref,
45250
45300
  renderBudget: renderBudgetProp = RENDER_BUDGET_DEFAULT,
@@ -46809,7 +46859,7 @@ const PickerPresetResolver = props => {
46809
46859
  });
46810
46860
  };
46811
46861
 
46812
- installImportMetaCssBuild(import.meta);const css$s = /* css */`
46862
+ installImportMetaCssBuild(import.meta);const css$t = /* css */`
46813
46863
  @layer navi {
46814
46864
  }
46815
46865
  .navi_badge {
@@ -46888,7 +46938,7 @@ const Badge = ({
46888
46938
  className,
46889
46939
  ...props
46890
46940
  }) => {
46891
- import.meta.css = [css$s, "@jsenv/navi/src/text/badge.jsx"];
46941
+ import.meta.css = [css$t, "@jsenv/navi/src/text/badge.jsx"];
46892
46942
  const defaultRef = useRef();
46893
46943
  props.ref = props.ref || defaultRef;
46894
46944
  const {
@@ -46935,7 +46985,7 @@ const BadgeButton = props => {
46935
46985
  };
46936
46986
  Badge.Button = BadgeButton;
46937
46987
 
46938
- installImportMetaCssBuild(import.meta);const css$r = /* css */`
46988
+ installImportMetaCssBuild(import.meta);const css$s = /* css */`
46939
46989
  @layer navi {
46940
46990
  }
46941
46991
  .navi_badge_list {
@@ -46960,7 +47010,7 @@ const BadgeList = ({
46960
47010
  max,
46961
47011
  ...props
46962
47012
  }) => {
46963
- import.meta.css = [css$r, "@jsenv/navi/src/text/badge_list.jsx"];
47013
+ import.meta.css = [css$s, "@jsenv/navi/src/text/badge_list.jsx"];
46964
47014
  const measureRef = useRef();
46965
47015
  const visibleRef = useRef();
46966
47016
  useLayoutEffect(() => {
@@ -47035,7 +47085,7 @@ const BadgeList = ({
47035
47085
  });
47036
47086
  };
47037
47087
 
47038
- installImportMetaCssBuild(import.meta);const css$q = /* css */`
47088
+ installImportMetaCssBuild(import.meta);const css$r = /* css */`
47039
47089
  .navi_color {
47040
47090
  display: block;
47041
47091
  aspect-ratio: 1/1;
@@ -47066,7 +47116,7 @@ const Color = ({
47066
47116
  children,
47067
47117
  ...rest
47068
47118
  }) => {
47069
- import.meta.css = [css$q, "@jsenv/navi/src/text/color.jsx"];
47119
+ import.meta.css = [css$r, "@jsenv/navi/src/text/color.jsx"];
47070
47120
  const color = children || undefined;
47071
47121
  return jsx(Box, {
47072
47122
  as: "span",
@@ -47521,7 +47571,7 @@ const PickerFileUI = () => {
47521
47571
  return String(value);
47522
47572
  };
47523
47573
 
47524
- installImportMetaCssBuild(import.meta);const css$p = /* css */`
47574
+ installImportMetaCssBuild(import.meta);const css$q = /* css */`
47525
47575
  @layer navi {
47526
47576
  .navi_picker {
47527
47577
  --picker-border-radius: var(--navi-control-border-radius);
@@ -47847,7 +47897,7 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
47847
47897
  }
47848
47898
  `;
47849
47899
  const PickerButton = props => {
47850
- import.meta.css = [css$p, "@jsenv/navi/src/control/picker/picker.jsx"];
47900
+ import.meta.css = [css$q, "@jsenv/navi/src/control/picker/picker.jsx"];
47851
47901
  if (typeof props.maxLines === "string") {
47852
47902
  props.maxLines = parseInt(props.maxLines);
47853
47903
  }
@@ -48305,7 +48355,7 @@ installImportMetaCssBuild(import.meta);/**
48305
48355
  * is what keeps the focus where the travel happens instead of moving it into a
48306
48356
  * slide that is about to leave.
48307
48357
  */
48308
- const css$o = /* css */`
48358
+ const css$p = /* css */`
48309
48359
  .navi_picker_spin {
48310
48360
  /* What the loading outline is drawn around. */
48311
48361
  position: relative;
@@ -48544,7 +48594,7 @@ const DaySpin = ({
48544
48594
  nextLabel,
48545
48595
  ...rest
48546
48596
  }) => {
48547
- import.meta.css = [css$o, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
48597
+ import.meta.css = [css$p, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
48548
48598
  const id = useId();
48549
48599
  const containerId = `${id}_days`;
48550
48600
  const pickerId = `${id}_picker`;
@@ -48939,7 +48989,7 @@ const addDays = (day, count) => {
48939
48989
  };
48940
48990
 
48941
48991
  installImportMetaCssBuild(import.meta);// TOFIX: select in data then reset, it reset to red/blue instead of red/blue/green
48942
- const css$n = /* css */`
48992
+ const css$o = /* css */`
48943
48993
  .navi_checkbox_group {
48944
48994
  border-style: solid;
48945
48995
 
@@ -48959,7 +49009,7 @@ const CheckboxGroup = props => {
48959
49009
  return checkboxGroup;
48960
49010
  };
48961
49011
  const CheckboxGroupInterface = props => {
48962
- import.meta.css = [css$n, "@jsenv/navi/src/control/input/checkbox_group.jsx"];
49012
+ import.meta.css = [css$o, "@jsenv/navi/src/control/input/checkbox_group.jsx"];
48963
49013
  const {
48964
49014
  ref
48965
49015
  } = props;
@@ -48990,6 +49040,197 @@ const CheckboxGroupInterface = props => {
48990
49040
  });
48991
49041
  };
48992
49042
 
49043
+ installImportMetaCssBuild(import.meta);/**
49044
+ * Multiline text control that grows with what is typed.
49045
+ *
49046
+ * Autosize is native: `field-sizing: content` lets the browser size the
49047
+ * textarea from its value — no hidden mirror textarea to measure against (the
49048
+ * technique libraries used before the property existed). `minRows`/`maxRows`
49049
+ * become min/max heights in `lh` units on top of it; past `maxRows` the
49050
+ * content scrolls.
49051
+ *
49052
+ * TextareaCharCount is the counter that goes with it, and the caller places
49053
+ * it: under the box, in a form footer, next to a label — fed with the same
49054
+ * value/signal as the textarea. The textarea draws no counter of its own.
49055
+ *
49056
+ * Styled as a `.navi_input` box (border, background, focus ring, readonly and
49057
+ * disabled fades, variants): one look for everything one types into. The
49058
+ * shared sheet is registered here too — a page may render a Textarea without
49059
+ * any Input.
49060
+ */
49061
+ const css$n = /* css */`
49062
+ .navi_input.navi_textarea {
49063
+ .navi_control_input {
49064
+ min-height: calc(var(--textarea-min-rows, 1.5) * 1lh);
49065
+ /* Above maxRows the box stops growing and the content scrolls. The
49066
+ 99999 fallback means "no cap" without needing a conditional rule. */
49067
+ max-height: calc(var(--textarea-max-rows, 99999) * 1lh);
49068
+ field-sizing: content;
49069
+ /* Explicit, never normal: minRows/maxRows are lengths in lh, and with
49070
+ line-height normal the lh unit resolves to a theoretical value that
49071
+ does not match the real rendered line — the box then jumps by a few
49072
+ pixels the moment the first character replaces the theory with a real
49073
+ line. One number for both keeps every row count exact. */
49074
+ line-height: 1.5;
49075
+ /* The control grows itself; resizable below hands the handle back. */
49076
+ resize: none;
49077
+ overflow: auto;
49078
+ }
49079
+ &[data-resizable] .navi_control_input {
49080
+ height: calc(var(--textarea-min-rows, 1.5) * 1lh);
49081
+ /* The two are exclusive: with field-sizing content the browser removes
49082
+ the resize handle (the size follows the content, there is nothing to
49083
+ drag). resizable means the hand takes over — fixed sizing, starting
49084
+ at minRows, and the drag writes its own inline height from there. */
49085
+ field-sizing: fixed;
49086
+ resize: vertical;
49087
+ }
49088
+ }
49089
+ .navi_textarea_char_count {
49090
+ color: color-mix(in srgb, currentColor 60%, transparent);
49091
+ font-size: 0.75em;
49092
+ user-select: none;
49093
+ }
49094
+ `;
49095
+
49096
+ /**
49097
+ * @type {import("ignore:preact").FunctionComponent<{
49098
+ * value?: string,
49099
+ * defaultValue?: string,
49100
+ * signal?: import("@preact/signals").Signal<string>,
49101
+ * name?: string,
49102
+ * minRows?: number,
49103
+ * maxRows?: number,
49104
+ * resizable?: boolean,
49105
+ * maxLength?: number,
49106
+ * width?: string,
49107
+ * [key: string]: any,
49108
+ * }>}
49109
+ * @param {number} [minRows=1.5] Height the empty control starts at, in lines.
49110
+ * The default shows half of a second line: enough to read "multiline" at a
49111
+ * glance without the height of a full extra row.
49112
+ * @param {number} [maxRows] Lines after which the control stops growing and
49113
+ * scrolls instead. Without it the control grows with its content.
49114
+ * @param {boolean} [resizable] Give the browser's vertical resize handle back.
49115
+ * A manual resize takes over from the automatic growth.
49116
+ * @param {number} [maxLength] The character limit, validated at submit. Pair
49117
+ * with `maxLengthGuard` to block typing past it, and render a
49118
+ * TextareaCharCount to show it.
49119
+ * @param {string} [width="35ch"] The control's width. Fixed on purpose: with
49120
+ * field-sizing the width would otherwise follow the longest line, and a box
49121
+ * that widens while one types is a box one chases.
49122
+ */
49123
+ const Textarea = ({
49124
+ // Destructured, never deleted off the props object: Preact reuses the same
49125
+ // props object when an internal state update re-renders the component, so a
49126
+ // delete would make these props vanish from the second render on (the box
49127
+ // then jumps back to the default minRows at the first keystroke).
49128
+ minRows = 1.5,
49129
+ maxRows,
49130
+ resizable,
49131
+ width = "35ch",
49132
+ ...props
49133
+ }) => {
49134
+ import.meta.css = [inputCss + css$n, "@jsenv/navi/src/control/input/textarea.jsx"];
49135
+ const defaultRef = useRef(null);
49136
+ props.ref = props.ref || defaultRef;
49137
+ const [rootProps, hostProps, childrenWrapperProps] = useControlProps(props, {
49138
+ controlType: "input"
49139
+ });
49140
+ const {
49141
+ basePseudoState,
49142
+ children
49143
+ } = hostProps;
49144
+ // Children go through ControlChildrenWrapper below; inside the <textarea>
49145
+ // element they would become its text content.
49146
+ delete hostProps.children;
49147
+ const loading = basePseudoState[":-navi-loading"];
49148
+ delete rootProps.width;
49149
+ hostProps.width = width;
49150
+ return jsxs(Box, {
49151
+ as: "span",
49152
+ inline: true,
49153
+ flex: true,
49154
+ baseClassName: "navi_input",
49155
+ className: "navi_textarea",
49156
+ ...rootProps,
49157
+ basePseudoState: basePseudoState,
49158
+ "data-resizable": resizable ? "" : undefined,
49159
+ styleCSSVars: InputStyleCSSVars,
49160
+ pseudoStateSelector: ".navi_control_input",
49161
+ pseudoClasses: InputPseudoClasses,
49162
+ pseudoElements: InputPseudoElements,
49163
+ "data-callout-anchor": ".navi_control_input",
49164
+ style: {
49165
+ "--textarea-min-rows": minRows,
49166
+ "--textarea-max-rows": maxRows,
49167
+ ...rootProps.style
49168
+ },
49169
+ children: [jsx(LoadingOutline, {
49170
+ loading: loading,
49171
+ color: "var(--loader-color)",
49172
+ inset: -1
49173
+ }), jsx(RealTextarea, {
49174
+ ...hostProps
49175
+ }), jsx(ControlChildrenWrapper, {
49176
+ ...childrenWrapperProps,
49177
+ children: children
49178
+ })]
49179
+ });
49180
+ };
49181
+
49182
+ /**
49183
+ * The counter that goes with a Textarea: "50/200" — how many characters are
49184
+ * typed over how many the limit allows, the way Material writes it (just the
49185
+ * count when there is no `maxLength`). Where it goes is the caller's call,
49186
+ * which is why it is a separate component rather than something the textarea
49187
+ * draws: put it under the box, in a form footer, next to a label, and feed it
49188
+ * the same value or signal as the textarea.
49189
+ *
49190
+ * @type {import("ignore:preact").FunctionComponent<{
49191
+ * value?: string,
49192
+ * signal?: import("@preact/signals").Signal<string>,
49193
+ * maxLength?: number,
49194
+ * [key: string]: any,
49195
+ * }>}
49196
+ * @param {string} [value] The text being counted. Say `signal` instead for a
49197
+ * two-way bound textarea: reading it here subscribes the count to it.
49198
+ * @param {number} [maxLength] The limit, shown after the count ("50/200").
49199
+ * Without it the count stands alone.
49200
+ */
49201
+ const TextareaCharCount = ({
49202
+ value,
49203
+ signal,
49204
+ maxLength,
49205
+ ...rest
49206
+ }) => {
49207
+ import.meta.css = [css$n, "@jsenv/navi/src/control/input/textarea.jsx"];
49208
+ const resolvedValue = signal ? signal.value : value;
49209
+ const length = typeof resolvedValue === "string" ? resolvedValue.length : 0;
49210
+ return jsx(Box, {
49211
+ as: "span",
49212
+ baseClassName: "navi_textarea_char_count",
49213
+ ...rest,
49214
+ children: maxLength === undefined ? length : `${length}/${maxLength}`
49215
+ });
49216
+ };
49217
+ const RealTextarea = ({
49218
+ maxLength,
49219
+ ...domProps
49220
+ }) => {
49221
+ const autoSelectReadOnlyProps = useAutoSelectReadOnly(domProps);
49222
+ return jsx(Box, {
49223
+ ...domProps,
49224
+ as: "textarea",
49225
+ baseClassName: "navi_control_input",
49226
+ ...autoSelectReadOnlyProps,
49227
+ // Native maxLength stays off, like RealInput in input_textual.jsx: the
49228
+ // maxLengthGuard handles live blocking, the constraint validates at
49229
+ // submit, and navi-max-length keeps the value readable from the DOM.
49230
+ "navi-max-length": maxLength
49231
+ });
49232
+ };
49233
+
48993
49234
  const Unit = ({
48994
49235
  unit,
48995
49236
  plural,
@@ -50523,12 +50764,16 @@ const css$k = /* css */`
50523
50764
  as rows scroll under the pointer. */
50524
50765
  user-select: none;
50525
50766
  -webkit-tap-highlight-color: transparent;
50526
- /* Rendering virtualization: only the rows within each wheel's own viewport
50527
- are painted; the rest (clipped by the viewport's overflow) are skipped.
50528
- The box is still laid out (fixed main-axis size below), so offsets, snap
50529
- and the wrap math are unaffected this cuts paint/compositing cost so a
50530
- wheel with many values (or a page full of wheels) scrolls smoothly. */
50531
- content-visibility: auto;
50767
+ /* NO content-visibility here, ever. content-visibility: auto was tried
50768
+ (to skip painting clipped rows) and it breaks the first opening of any
50769
+ popup holding a wheel: rows born inside a closed dialog are "skipped"
50770
+ with no last-remembered size, so they measure as empty (padding-only) on
50771
+ the open's own layout the dialog positions itself against that width
50772
+ and visibly slides once the real width lands. The un-skip only happens
50773
+ at the next update-the-rendering step: no forced reflow can read the
50774
+ true size any earlier, making the bug unfixable from the dialog side.
50775
+ There is also nothing left to win: the window renders only
50776
+ visibleCount + 2 recycled rows, all in or against the viewport. */
50532
50777
  }
50533
50778
 
50534
50779
  /* Orientation-specific sizing/layout. The mask worn by the base layer: the
@@ -51496,23 +51741,16 @@ function WheelUI(props) {
51496
51741
  const trackedItems = tracker.itemsSignal.value;
51497
51742
  const trackedItemsRef = useRef(trackedItems);
51498
51743
  trackedItemsRef.current = trackedItems;
51499
- const itemCount = trackedItems.length;
51500
51744
 
51501
51745
  // Virtualization: render only visibleCount + 2 rows (one buffer each side to
51502
51746
  // cover the partial rows a scroll reveals + a re-render's one-frame lag) and
51503
- // recycle them, filling each from trackedItems. `centerRowSignal` holds the row
51504
- // currently at the center (round(pos / itemSize)); it changes only when the
51505
- // wheel crosses a row (not every frame), so the window re-renders on demand, not
51506
- // per frame. WheelWindow derives its top slot (base) from that row and its own
51507
- // windowSize so a windowSize change (items still registering) recomputes base
51508
- // instead of leaving the top rows unrendered. The per-frame transform is
51509
- // imperative (--wheel-offset); renderedBaseRef mirrors the base the DOM shows so
51510
- // the transform stays consistent with it.
51511
- // visibleCount + 2, but never more rows than there are values.
51512
- let windowSize = visibleCount + 2;
51513
- if (windowSize > itemCount) {
51514
- windowSize = itemCount;
51515
- }
51747
+ // recycle them. `centerRowSignal` holds the row currently at the center
51748
+ // (round(pos / itemSize)); it changes only when the wheel crosses a row (not
51749
+ // every frame), so the window re-renders on demand, not per frame. WheelWindow
51750
+ // owns both its size and its top slot (base), derived from that row and from the
51751
+ // items it reads off the tracker itself see its own comment. The per-frame
51752
+ // transform is imperative (--wheel-offset); renderedBaseRef mirrors the base the
51753
+ // DOM shows so the transform stays consistent with it.
51516
51754
  const centerRowSignal = useSignal(0);
51517
51755
  const renderedBaseRef = useRef(0);
51518
51756
  // Row size in px, measured once from a rendered slot (rows are uniform).
@@ -52216,8 +52454,8 @@ function WheelUI(props) {
52216
52454
  className: "navi_wheel_list",
52217
52455
  children: jsx(WheelWindow, {
52218
52456
  centerRowSignal: centerRowSignal,
52219
- windowSize: windowSize,
52220
- trackedItems: trackedItems,
52457
+ visibleCount: visibleCount,
52458
+ tracker: tracker,
52221
52459
  isLoop: isLoop,
52222
52460
  onBaseCommit: commitRenderedBase
52223
52461
  })
@@ -52230,8 +52468,8 @@ function WheelUI(props) {
52230
52468
  className: "navi_wheel_list",
52231
52469
  children: jsx(WheelWindow, {
52232
52470
  centerRowSignal: centerRowSignal,
52233
- windowSize: windowSize,
52234
- trackedItems: trackedItems,
52471
+ visibleCount: visibleCount,
52472
+ tracker: tracker,
52235
52473
  isLoop: isLoop
52236
52474
  })
52237
52475
  })
@@ -52248,21 +52486,38 @@ const WHEEL_PSEUDO_CLASSES = [":focus-within", ":focus-visible", ":read-only", "
52248
52486
 
52249
52487
  // The recycled window: a fixed set of `windowSize` <li> slots (one per visible
52250
52488
  // row + 2 buffer), keyed by slot position so each DOM node is reused and only its
52251
- // content re-renders. `base` (the top slot's value index) is derived here from the
52252
- // center row + windowSize computing it here, not upstream, means a windowSize
52253
- // change (items still registering) recomputes base correctly. Each slot shows
52254
- // trackedItems[base + slot] (wrapped when looping). It reads centerRowSignal, so
52255
- // it re-renders ONLY when the wheel crosses a row, not every frame the per-frame
52256
- // motion is the imperative transform in applyOffset. On each new base it calls
52257
- // onBaseCommit so the transform re-syncs to the freshly rendered rows same-frame.
52489
+ // content re-renders. `base` (the top slot's value index) and windowSize are both
52490
+ // derived here rather than upstream, from the items read straight off the tracker:
52491
+ // this component renders right after the Wheel.Item children, which register
52492
+ // during their own render, so it can draw the real rows in the very first commit.
52493
+ // Taking them from the parent instead would mean drawing an empty wheel first (the
52494
+ // parent renders before its children, and the tracker's signal only lands on a
52495
+ // microtask) a wheel with no width, which is enough to misplace whatever sizes
52496
+ // itself on it, e.g. a dialog centered on its content.
52497
+ // Each slot shows items[base + slot] (wrapped when looping). It reads
52498
+ // centerRowSignal, so it re-renders ONLY when the wheel crosses a row, not every
52499
+ // frame — the per-frame motion is the imperative transform in applyOffset. On each
52500
+ // new base it calls onBaseCommit so the transform re-syncs to the freshly rendered
52501
+ // rows same-frame.
52258
52502
  const WheelWindow = ({
52259
52503
  centerRowSignal,
52260
- windowSize,
52261
- trackedItems,
52504
+ visibleCount,
52505
+ tracker,
52262
52506
  isLoop,
52263
52507
  onBaseCommit
52264
52508
  }) => {
52509
+ // Subscribing is what re-renders this window when items are added or removed
52510
+ // later; what to draw right now is read synchronously below.
52511
+ // eslint-disable-next-line no-unused-expressions
52512
+ tracker.itemsSignal.value;
52513
+ const trackedItems = tracker.peekItems();
52265
52514
  const count = trackedItems.length;
52515
+ // visibleCount + 2 (one buffer row each side), but never more rows than there
52516
+ // are values.
52517
+ let windowSize = visibleCount + 2;
52518
+ if (windowSize > count) {
52519
+ windowSize = count;
52520
+ }
52266
52521
  let base = centerRowSignal.value - Math.floor(windowSize / 2);
52267
52522
  // Non-loop: keep the window inside the value range so every slot maps to a real
52268
52523
  // value; the transform then leaves the blank runway above the first / below the
@@ -57762,9 +58017,9 @@ const css$2 = /* css */`
57762
58017
  .navi_card_layout {
57763
58018
  --layout-margin: 30px;
57764
58019
  --layout-padding: 20px;
57765
- --layout-background: white;
58020
+ --layout-background: var(--navi-surface-color);
57766
58021
  --layout-border-width: 2px;
57767
- --layout-border-color: lightgrey;
58022
+ --layout-border-color: var(--navi-popup-border-color);
57768
58023
  --layout-border-radius: 10px;
57769
58024
  --layout-min-width: 300px;
57770
58025
  --layout-min-height: auto;
@@ -57883,7 +58138,9 @@ installImportMetaCssBuild(import.meta);const css$1 = /* css */`
57883
58138
  @layer navi {
57884
58139
  .navi_viewport_layout {
57885
58140
  --layout-padding: 40px;
57886
- --layout-background: white;
58141
+ /* The page's own paper: the shared surface token, so a dark theme gets a
58142
+ dark page without every screen overriding it. */
58143
+ --layout-background: var(--navi-surface-color);
57887
58144
  }
57888
58145
  }
57889
58146
 
@@ -58090,14 +58347,17 @@ const css = /* css */`
58090
58347
  z-index: 1;
58091
58348
  background-color: var(--navi-popup-background-color);
58092
58349
  }
58350
+ /* The separator token, not the popup border: these lines split two
58351
+ regions of the panel's own surface (the head/foot from what scrolls
58352
+ under them), they are not the panel's edge. */
58093
58353
  .navi_side_panel_head {
58094
58354
  top: 0;
58095
- border-bottom: 1px solid var(--navi-popup-border-color);
58355
+ border-bottom: 1px solid var(--navi-separator-color-default);
58096
58356
  }
58097
58357
  .navi_side_panel_foot {
58098
58358
  bottom: 0;
58099
58359
  padding: 12px 16px;
58100
- border-top: 1px solid var(--navi-popup-border-color);
58360
+ border-top: 1px solid var(--navi-separator-color-default);
58101
58361
  }
58102
58362
  }
58103
58363
  `;
@@ -58411,5 +58671,5 @@ const UserSvg = () => jsx("svg", {
58411
58671
  })
58412
58672
  });
58413
58673
 
58414
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
58674
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
58415
58675
  //# sourceMappingURL=jsenv_navi.js.map