@jsenv/navi 0.16.50 → 0.16.52

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.
@@ -300,7 +300,9 @@ const getSignalType = (value) => {
300
300
  *
301
301
  * @param {any} a - First value to compare
302
302
  * @param {any} b - Second value to compare
303
- * @param {Set} seenSet - Internal cycle detection set (automatically managed)
303
+ * @param {Object} [options={}] - Comparison options
304
+ * @param {Function} [options.keyComparator] - Custom comparator function for object properties and array elements
305
+ * @param {boolean} [options.ignoreArrayOrder=false] - If true, arrays are considered equal regardless of element order
304
306
  * @returns {boolean} true if values are deeply equal, false otherwise
305
307
  */
306
308
 
@@ -319,7 +321,11 @@ const getSignalType = (value) => {
319
321
  */
320
322
  const SYMBOL_IDENTITY = Symbol.for("navi_object_identity");
321
323
 
322
- const compareTwoJsValues = (rootA, rootB, { keyComparator } = {}) => {
324
+ const compareTwoJsValues = (
325
+ rootA,
326
+ rootB,
327
+ { keyComparator, ignoreArrayOrder = false } = {},
328
+ ) => {
323
329
  const seenSet = new Set();
324
330
  const compare = (a, b) => {
325
331
  if (a === b) {
@@ -370,6 +376,32 @@ const compareTwoJsValues = (rootA, rootB, { keyComparator } = {}) => {
370
376
  if (a.length !== b.length) {
371
377
  return false;
372
378
  }
379
+ if (ignoreArrayOrder) {
380
+ // Unordered array comparison: each element in 'a' must have a match in 'b'
381
+ const usedIndices = new Set();
382
+ for (let i = 0; i < a.length; i++) {
383
+ const aValue = a[i];
384
+ let foundMatch = false;
385
+
386
+ for (let j = 0; j < b.length; j++) {
387
+ if (usedIndices.has(j)) continue; // Already matched with another element
388
+
389
+ const bValue = b[j];
390
+ const comparator = keyComparator || compare;
391
+ if (comparator(aValue, bValue, i, compare)) {
392
+ foundMatch = true;
393
+ usedIndices.add(j);
394
+ break;
395
+ }
396
+ }
397
+
398
+ if (!foundMatch) {
399
+ return false;
400
+ }
401
+ }
402
+ return true;
403
+ }
404
+ // Ordered array comparison (original behavior)
373
405
  let i = 0;
374
406
  while (i < a.length) {
375
407
  const aValue = a[i];
@@ -2393,11 +2425,17 @@ const stateSignal = (defaultValue, options = {}) => {
2393
2425
  if (dynamicDefaultSignal) {
2394
2426
  const dynamicValue = dynamicDefaultSignal.peek();
2395
2427
  if (dynamicValue === undefined) {
2396
- return value !== staticDefaultValue;
2428
+ return !compareTwoJsValues(value, staticDefaultValue, {
2429
+ ignoreArrayOrder: true,
2430
+ });
2397
2431
  }
2398
- return value !== dynamicValue;
2432
+ return !compareTwoJsValues(value, dynamicValue, {
2433
+ ignoreArrayOrder: true,
2434
+ });
2399
2435
  }
2400
- return value !== staticDefaultValue;
2436
+ return !compareTwoJsValues(value, staticDefaultValue, {
2437
+ ignoreArrayOrder: true,
2438
+ });
2401
2439
  };
2402
2440
 
2403
2441
  // Create signal with initial value: use stored value, or undefined to indicate no explicit value
@@ -2553,6 +2591,21 @@ const stateSignal = (defaultValue, options = {}) => {
2553
2591
  }
2554
2592
  effect(() => {
2555
2593
  const value = preactSignal.value;
2594
+
2595
+ if (dynamicDefaultSignal) {
2596
+ // With dynamic defaults: always persist to preserve user intent
2597
+ // even when value matches dynamic defaults that may change
2598
+ if (value !== undefined) {
2599
+ if (debug) {
2600
+ console.debug(
2601
+ `[stateSignal:${signalIdString}] dynamic default: writing to localStorage "${localStorageKey}"=${value}`,
2602
+ );
2603
+ }
2604
+ writeIntoLocalStorage(value);
2605
+ }
2606
+ return;
2607
+ }
2608
+ // Static defaults: only persist custom values
2556
2609
  if (isCustomValue(value)) {
2557
2610
  if (debug) {
2558
2611
  console.debug(
@@ -4517,54 +4570,6 @@ const isProps = (value) => {
4517
4570
  return value !== null && typeof value === "object";
4518
4571
  };
4519
4572
 
4520
- /**
4521
- * Creates a signal that stays synchronized with an external value,
4522
- * only updating the signal when the value actually changes.
4523
- *
4524
- * This hook solves a common reactive UI pattern where:
4525
- * 1. A signal controls a UI element (like an input field)
4526
- * 2. The UI element can be modified by user interaction
4527
- * 3. When the external "source of truth" changes, it should take precedence
4528
- *
4529
- * @param {any} value - The external value to sync with (the "source of truth")
4530
- * @param {any} [initialValue] - Optional initial value for the signal (defaults to value)
4531
- * @returns {Signal} A signal that tracks the external value but allows temporary local changes
4532
- *
4533
- * @example
4534
- * const FileNameEditor = ({ file }) => {
4535
- * // Signal stays in sync with file.name, but allows user editing
4536
- * const nameSignal = useSignalSync(file.name);
4537
- *
4538
- * return (
4539
- * <Editable
4540
- * valueSignal={nameSignal} // User can edit this
4541
- * action={renameFileAction} // Saves changes
4542
- * />
4543
- * );
4544
- * };
4545
- *
4546
- * // Scenario:
4547
- * // 1. file.name = "doc.txt", nameSignal.value = "doc.txt"
4548
- * // 2. User types "report" -> nameSignal.value = "report.txt"
4549
- * // 3. External update: file.name = "shared-doc.txt"
4550
- * // 4. Next render: nameSignal.value = "shared-doc.txt" (model wins!)
4551
- *
4552
- */
4553
-
4554
- const useSignalSync = (value, initialValue = value) => {
4555
- const signal = useSignal(initialValue);
4556
- const previousValueRef = useRef(value);
4557
-
4558
- // Only update signal when external value actually changes
4559
- // This preserves user input between external changes
4560
- if (previousValueRef.current !== value) {
4561
- previousValueRef.current = value;
4562
- signal.value = value; // Model takes precedence
4563
- }
4564
-
4565
- return signal;
4566
- };
4567
-
4568
4573
  const addIntoArray = (array, ...valuesToAdd) => {
4569
4574
  if (valuesToAdd.length === 1) {
4570
4575
  const [valueToAdd] = valuesToAdd;
@@ -4627,6 +4632,69 @@ const removeFromArray = (array, ...valuesToRemove) => {
4627
4632
  return hasRemovedValues ? arrayWithoutTheseValues : array;
4628
4633
  };
4629
4634
 
4635
+ const useArraySignalMembership = (arraySignal, id) => {
4636
+ const array = arraySignal.value;
4637
+ const found = array.includes(id);
4638
+ return [
4639
+ found,
4640
+ (enabled) => {
4641
+ if (enabled) {
4642
+ arraySignal.value = addIntoArray(array, id);
4643
+ } else {
4644
+ arraySignal.value = removeFromArray(array, id);
4645
+ }
4646
+ },
4647
+ ];
4648
+ };
4649
+
4650
+ /**
4651
+ * Creates a signal that stays synchronized with an external value,
4652
+ * only updating the signal when the value actually changes.
4653
+ *
4654
+ * This hook solves a common reactive UI pattern where:
4655
+ * 1. A signal controls a UI element (like an input field)
4656
+ * 2. The UI element can be modified by user interaction
4657
+ * 3. When the external "source of truth" changes, it should take precedence
4658
+ *
4659
+ * @param {any} value - The external value to sync with (the "source of truth")
4660
+ * @param {any} [initialValue] - Optional initial value for the signal (defaults to value)
4661
+ * @returns {Signal} A signal that tracks the external value but allows temporary local changes
4662
+ *
4663
+ * @example
4664
+ * const FileNameEditor = ({ file }) => {
4665
+ * // Signal stays in sync with file.name, but allows user editing
4666
+ * const nameSignal = useSignalSync(file.name);
4667
+ *
4668
+ * return (
4669
+ * <Editable
4670
+ * valueSignal={nameSignal} // User can edit this
4671
+ * action={renameFileAction} // Saves changes
4672
+ * />
4673
+ * );
4674
+ * };
4675
+ *
4676
+ * // Scenario:
4677
+ * // 1. file.name = "doc.txt", nameSignal.value = "doc.txt"
4678
+ * // 2. User types "report" -> nameSignal.value = "report.txt"
4679
+ * // 3. External update: file.name = "shared-doc.txt"
4680
+ * // 4. Next render: nameSignal.value = "shared-doc.txt" (model wins!)
4681
+ *
4682
+ */
4683
+
4684
+ const useSignalSync = (value, initialValue = value) => {
4685
+ const signal = useSignal(initialValue);
4686
+ const previousValueRef = useRef(value);
4687
+
4688
+ // Only update signal when external value actually changes
4689
+ // This preserves user input between external changes
4690
+ if (previousValueRef.current !== value) {
4691
+ previousValueRef.current = value;
4692
+ signal.value = value; // Model takes precedence
4693
+ }
4694
+
4695
+ return signal;
4696
+ };
4697
+
4630
4698
  /**
4631
4699
  * Picks the best initial value from three options using a simple priority system.
4632
4700
  *
@@ -28456,5 +28524,5 @@ const UserSvg = () => jsx("svg", {
28456
28524
  })
28457
28525
  });
28458
28526
 
28459
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, BadgeCount, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, Checkbox, CheckboxList, Code, Col, Colgroup, ConstructionSvg, Details, DialogLayout, Editable, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, Group, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, MessageBox, Paragraph, Radio, RadioList, Route, RouteLink, Routes, RowNumberCol, RowNumberTableCell, SINGLE_SPACE_CONSTRAINT, SVGMaskOverlay, SearchSvg, Select, SelectionContext, Separator, SettingsSvg, StarSvg, SummaryMarker, Svg, Tab, TabList, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, addCustomMessage, clearAllRoutes, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, rerunActions, resource, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, updateActions, useActionData, useActionStatus, useCalloutClose, useCellsAndColumns, useConstraintValidityState, useDependenciesDiff, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useMatchingRouteInfo, useNavState$1 as useNavState, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage };
28527
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, BadgeCount, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, Checkbox, CheckboxList, Code, Col, Colgroup, ConstructionSvg, Details, DialogLayout, Editable, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Form, Group, HeartSvg, HomeSvg, Icon, Image, Input, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, MessageBox, Paragraph, Radio, RadioList, Route, RouteLink, Routes, RowNumberCol, RowNumberTableCell, SINGLE_SPACE_CONSTRAINT, SVGMaskOverlay, SearchSvg, Select, SelectionContext, Separator, SettingsSvg, StarSvg, SummaryMarker, Svg, Tab, TabList, Table, TableCell, Tbody, Text, Thead, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, addCustomMessage, clearAllRoutes, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, forwardActionRequested, installCustomConstraintValidation, isCellSelected, isColumnSelected, isRowSelected, localStorageSignal, navBack, navForward, navTo, openCallout, rawUrlPart, reload, removeCustomMessage, requestAction, rerunActions, resource, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, updateActions, useActionData, useActionStatus, useArraySignalMembership, useCalloutClose, useCellsAndColumns, useConstraintValidityState, useDependenciesDiff, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useMatchingRouteInfo, useNavState$1 as useNavState, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage };
28460
28528
  //# sourceMappingURL=jsenv_navi.js.map