@jsenv/navi 0.17.2 → 0.17.3

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.
@@ -1,13 +1,19 @@
1
1
  import { installImportMetaCss } from "./jsenv_navi_side_effects.js";
2
+ import { isValidElement, createContext, toChildArray, render, createRef, cloneElement } from "preact";
2
3
  import { useErrorBoundary, useLayoutEffect, useEffect, useCallback, useRef, useState, useContext, useMemo, useImperativeHandle, useId } from "preact/hooks";
3
4
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
4
- import { createIterableWeakSet, mergeOneStyle, stringifyStyle, createPubSub, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, findBefore, findAfter, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, createStyleController, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, hasCSSSizeUnit, resolveCSSSize, activeElementSignal, canInterceptKeys, pickLightOrDark, resolveColorLuminance, initFocusGroup, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, elementIsFocusable } from "@jsenv/dom";
5
+ import { createIterableWeakSet, mergeOneStyle, stringifyStyle, createPubSub, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, findBefore, findAfter, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, createStyleController, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, hasCSSSizeUnit, resolveCSSSize, activeElementSignal, canInterceptKeys, initFocusGroup, elementIsFocusable, pickLightOrDark, resolveColorLuminance, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
5
6
  import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
6
7
  import { effect, signal, computed, batch, useSignal } from "@preact/signals";
7
8
  import { createValidity } from "@jsenv/validity";
8
- import { createContext, toChildArray, render, isValidElement, createRef, cloneElement } from "preact";
9
9
  import { createPortal, forwardRef } from "preact/compat";
10
10
 
11
+ const IDLE = { id: "idle" };
12
+ const RUNNING = { id: "running" };
13
+ const ABORTED = { id: "aborted" };
14
+ const FAILED = { id: "failed" };
15
+ const COMPLETED = { id: "completed" };
16
+
11
17
  const actionPrivatePropertiesWeakMap = new WeakMap();
12
18
  const getActionPrivateProperties = (action) => {
13
19
  const actionPrivateProperties = actionPrivatePropertiesWeakMap.get(action);
@@ -20,11 +26,19 @@ const setActionPrivateProperties = (action, properties) => {
20
26
  actionPrivatePropertiesWeakMap.set(action, properties);
21
27
  };
22
28
 
23
- const IDLE = { id: "idle" };
24
- const RUNNING = { id: "running" };
25
- const ABORTED = { id: "aborted" };
26
- const FAILED = { id: "failed" };
27
- const COMPLETED = { id: "completed" };
29
+ const getActionStatus = (action) => {
30
+ const { runningStateSignal, errorSignal, computedDataSignal } =
31
+ getActionPrivateProperties(action);
32
+ const runningState = runningStateSignal.value;
33
+ const idle = runningState === IDLE;
34
+ const aborted = runningState === ABORTED;
35
+ const error = errorSignal.value;
36
+ const loading = runningState === RUNNING;
37
+ const completed = runningState === COMPLETED;
38
+ const data = computedDataSignal.value;
39
+
40
+ return { idle, loading, aborted, error, completed, data };
41
+ };
28
42
 
29
43
  const useActionStatus = (action) => {
30
44
  if (!action) {
@@ -96,6 +110,23 @@ const ActionRenderer = ({
96
110
  children,
97
111
  disabled
98
112
  }) => {
113
+ if (action === undefined) {
114
+ throw new Error("ActionRenderer requires an action to render, but none was provided.");
115
+ }
116
+ let renderBranches;
117
+ if (typeof children === "function") {
118
+ renderBranches = {
119
+ completed: children
120
+ };
121
+ } else if (isValidElement(children)) {
122
+ renderBranches = {
123
+ always: () => children
124
+ };
125
+ } else if (isPlainObject$1(children)) {
126
+ renderBranches = children;
127
+ } else {
128
+ renderBranches = {};
129
+ }
99
130
  const {
100
131
  idle: renderIdle = renderIdleDefault,
101
132
  loading: renderLoading = renderLoadingDefault,
@@ -103,15 +134,7 @@ const ActionRenderer = ({
103
134
  error: renderError = renderErrorDefault,
104
135
  completed: renderCompleted,
105
136
  always: renderAlways
106
- } = typeof children === "function" ? {
107
- completed: children
108
- } : children || {};
109
- if (disabled) {
110
- return null;
111
- }
112
- if (action === undefined) {
113
- throw new Error("ActionRenderer requires an action to render, but none was provided.");
114
- }
137
+ } = renderBranches;
115
138
  const {
116
139
  idle,
117
140
  loading,
@@ -142,7 +165,9 @@ const ActionRenderer = ({
142
165
  actionUIRenderedPromiseWeakMap.delete(action);
143
166
  };
144
167
  }, [action]);
145
-
168
+ if (disabled) {
169
+ return null;
170
+ }
146
171
  // If renderAlways is provided, it wins and handles all rendering
147
172
  if (renderAlways) {
148
173
  return renderAlways({
@@ -205,6 +230,16 @@ const useUIRenderedPromise = action => {
205
230
  actionUIRenderedPromiseWeakMap.set(action, promise);
206
231
  return promise;
207
232
  };
233
+ const isPlainObject$1 = obj => {
234
+ if (typeof obj !== "object" || obj === null) {
235
+ return false;
236
+ }
237
+ let proto = obj;
238
+ while (Object.getPrototypeOf(proto) !== null) {
239
+ proto = Object.getPrototypeOf(proto);
240
+ }
241
+ return Object.getPrototypeOf(obj) === proto || Object.getPrototypeOf(obj) === null;
242
+ };
208
243
 
209
244
  const isSignal = (value) => {
210
245
  return getSignalType(value) !== null;
@@ -1408,7 +1443,7 @@ const createAction = (callback, rootOptions = {}) => {
1408
1443
  }
1409
1444
 
1410
1445
  // ✅ CAS 2: Objet -> vérifier s'il contient des signals
1411
- if (newParamsOrSignal && typeof newParamsOrSignal === "object") {
1446
+ if (isPlainObject(newParamsOrSignal)) {
1412
1447
  const staticParams = {};
1413
1448
  const signalMap = new Map();
1414
1449
 
@@ -1459,7 +1494,7 @@ const createAction = (callback, rootOptions = {}) => {
1459
1494
  return createActionProxyFromSignal(action, paramsSignal, options);
1460
1495
  }
1461
1496
 
1462
- // ✅ CAS 3: Primitive -> action enfant
1497
+ // ✅ CAS 3: Primitive or objects like DOMEvents etc -> action enfant
1463
1498
  return createChildAction({
1464
1499
  params: newParamsOrSignal,
1465
1500
  ...options,
@@ -1737,7 +1772,7 @@ const createAction = (callback, rootOptions = {}) => {
1737
1772
  if (isPrerun && abortSignal.aborted) {
1738
1773
  prerunProtectionRegistry.unprotect(action);
1739
1774
  }
1740
- onAbort(e, action);
1775
+ onAbort?.(e, action);
1741
1776
  return e;
1742
1777
  }
1743
1778
  if (e.name === "AbortError") {
@@ -2114,6 +2149,19 @@ const generateActionName = (name, params) => {
2114
2149
  return `${name}${argsString}`;
2115
2150
  };
2116
2151
 
2152
+ const isPlainObject = (obj) => {
2153
+ if (typeof obj !== "object" || obj === null) {
2154
+ return false;
2155
+ }
2156
+ let proto = obj;
2157
+ while (Object.getPrototypeOf(proto) !== null) {
2158
+ proto = Object.getPrototypeOf(proto);
2159
+ }
2160
+ return (
2161
+ Object.getPrototypeOf(obj) === proto || Object.getPrototypeOf(obj) === null
2162
+ );
2163
+ };
2164
+
2117
2165
  const useActionData = (action) => {
2118
2166
  if (!action) {
2119
2167
  return undefined;
@@ -10188,6 +10236,12 @@ const setupPatterns = (patternDefinitions) => {
10188
10236
 
10189
10237
  // Phase 1: Create all pattern objects
10190
10238
  for (const [key, urlPatternRaw] of Object.entries(patternDefinitions)) {
10239
+ if (typeof urlPatternRaw !== "string") {
10240
+ throw new TypeError(
10241
+ `expects a route pattern string, but received ${urlPatternRaw} for route "${key}".`,
10242
+ );
10243
+ }
10244
+
10191
10245
  // Create the unified pattern object
10192
10246
  const pattern = createRoutePattern(urlPatternRaw);
10193
10247
 
@@ -10402,17 +10456,29 @@ const setupRoutes = (routeDefinition) => {
10402
10456
  };
10403
10457
 
10404
10458
  const useRouteStatus = (route) => {
10405
- const { urlSignal, matchingSignal, paramsSignal, visitedSignal } = route;
10459
+ const {
10460
+ urlSignal,
10461
+ matchingSignal,
10462
+ paramsSignal,
10463
+ visitedSignal,
10464
+ actionStatusSignal,
10465
+ } = route;
10406
10466
  const url = urlSignal.value;
10407
10467
  const matching = matchingSignal.value;
10408
10468
  const params = paramsSignal.value;
10409
10469
  const visited = visitedSignal.value;
10470
+ const { loading, aborted, error, completed, data } = actionStatusSignal.value;
10410
10471
 
10411
10472
  return {
10412
10473
  url,
10413
10474
  matching,
10414
10475
  params,
10415
10476
  visited,
10477
+ loading,
10478
+ aborted,
10479
+ error,
10480
+ completed,
10481
+ data,
10416
10482
  };
10417
10483
  };
10418
10484
 
@@ -10464,6 +10530,8 @@ const updateRoutes = (
10464
10530
  // state
10465
10531
  } = {},
10466
10532
  ) => {
10533
+ const returnValue = {};
10534
+
10467
10535
  const routeMatchInfoSet = new Set();
10468
10536
  for (const route of routeSet) {
10469
10537
  const routePrivateProperties = getRoutePrivateProperties(route);
@@ -10511,279 +10579,283 @@ const updateRoutes = (
10511
10579
  });
10512
10580
  }
10513
10581
 
10514
- // Apply all signal updates in a batch
10515
- const matchingRouteSet = new Set();
10516
- batch(() => {
10517
- for (const {
10518
- route,
10519
- routePrivateProperties,
10520
- newMatching,
10521
- newParams,
10522
- } of routeMatchInfoSet) {
10523
- const { updateStatus } = routePrivateProperties;
10524
- const visited = isVisited(route.url);
10525
- updateStatus({
10526
- matching: newMatching,
10527
- params: newParams,
10528
- visited,
10529
- });
10530
- if (newMatching) {
10531
- matchingRouteSet.add(route);
10532
- }
10533
- }
10534
-
10582
+ {
10535
10583
  // URL -> Signal synchronization (moved from individual route effects to eliminate circular dependency)
10536
10584
  // Prevent signal-to-URL synchronization during URL-to-signal synchronization
10537
10585
  isUpdatingRoutesFromUrl = true;
10586
+ // Apply all signal updates in a batch
10587
+ const matchingRouteSet = new Set();
10588
+ batch(() => {
10589
+ for (const {
10590
+ route,
10591
+ routePrivateProperties,
10592
+ newMatching,
10593
+ newParams,
10594
+ } of routeMatchInfoSet) {
10595
+ const { updateStatus } = routePrivateProperties;
10596
+ const visited = isVisited(route.url);
10597
+ updateStatus({
10598
+ matching: newMatching,
10599
+ params: newParams,
10600
+ visited,
10601
+ });
10602
+ if (newMatching) {
10603
+ matchingRouteSet.add(route);
10604
+ }
10605
+ }
10538
10606
 
10539
- for (const {
10540
- route,
10541
- routePrivateProperties,
10542
- newMatching,
10543
- } of routeMatchInfoSet) {
10544
- const { routePattern } = routePrivateProperties;
10545
- const { connectionMap } = routePattern;
10546
-
10547
- for (const [paramName, connection] of connectionMap) {
10548
- const { signal: paramSignal, debug } = connection;
10549
- const rawParams = route.rawParamsSignal.value;
10550
- const urlParamValue = rawParams[paramName];
10607
+ for (const {
10608
+ route,
10609
+ routePrivateProperties,
10610
+ newMatching,
10611
+ } of routeMatchInfoSet) {
10612
+ const { routePattern } = routePrivateProperties;
10613
+ const { connectionMap } = routePattern;
10551
10614
 
10552
- if (!newMatching) {
10553
- // Route doesn't match - check if any matching route extracts this parameter
10554
- let parameterExtractedByMatchingRoute = false;
10555
- let matchingRouteInSameFamily = false;
10615
+ for (const [paramName, connection] of connectionMap) {
10616
+ const { signal: paramSignal, debug } = connection;
10617
+ const rawParams = route.rawParamsSignal.value;
10618
+ const urlParamValue = rawParams[paramName];
10619
+
10620
+ if (!newMatching) {
10621
+ // Route doesn't match - check if any matching route extracts this parameter
10622
+ let parameterExtractedByMatchingRoute = false;
10623
+ let matchingRouteInSameFamily = false;
10624
+
10625
+ for (const otherRoute of routeSet) {
10626
+ if (otherRoute === route || !otherRoute.matching) {
10627
+ continue;
10628
+ }
10629
+ const otherRawParams = otherRoute.rawParamsSignal.value;
10630
+ const otherRoutePrivateProperties =
10631
+ getRoutePrivateProperties(otherRoute);
10556
10632
 
10557
- for (const otherRoute of routeSet) {
10558
- if (otherRoute === route || !otherRoute.matching) {
10559
- continue;
10560
- }
10561
- const otherRawParams = otherRoute.rawParamsSignal.value;
10562
- const otherRoutePrivateProperties =
10563
- getRoutePrivateProperties(otherRoute);
10633
+ // Check if this matching route extracts the parameter
10634
+ if (paramName in otherRawParams) {
10635
+ parameterExtractedByMatchingRoute = true;
10636
+ }
10564
10637
 
10565
- // Check if this matching route extracts the parameter
10566
- if (paramName in otherRawParams) {
10567
- parameterExtractedByMatchingRoute = true;
10568
- }
10638
+ // Check if this matching route is in the same family using parent-child relationships
10639
+ const thisPatternObj = routePattern;
10640
+ const otherPatternObj = otherRoutePrivateProperties.routePattern;
10569
10641
 
10570
- // Check if this matching route is in the same family using parent-child relationships
10571
- const thisPatternObj = routePattern;
10572
- const otherPatternObj = otherRoutePrivateProperties.routePattern;
10573
-
10574
- // Routes are in same family if they share a hierarchical relationship:
10575
- // 1. One is parent/ancestor of the other
10576
- // 2. They share a common parent/ancestor
10577
- let inSameFamily = false;
10578
-
10579
- // Check if other route is ancestor of this route
10580
- let currentParent = thisPatternObj.parent;
10581
- while (currentParent) {
10582
- if (currentParent === otherPatternObj) {
10583
- inSameFamily = true;
10584
- break;
10585
- }
10586
- currentParent = currentParent.parent;
10587
- }
10642
+ // Routes are in same family if they share a hierarchical relationship:
10643
+ // 1. One is parent/ancestor of the other
10644
+ // 2. They share a common parent/ancestor
10645
+ let inSameFamily = false;
10588
10646
 
10589
- // Check if this route is ancestor of other route
10590
- if (!inSameFamily) {
10591
- currentParent = otherPatternObj.parent;
10647
+ // Check if other route is ancestor of this route
10648
+ let currentParent = thisPatternObj.parent;
10592
10649
  while (currentParent) {
10593
- if (currentParent === thisPatternObj) {
10650
+ if (currentParent === otherPatternObj) {
10594
10651
  inSameFamily = true;
10595
10652
  break;
10596
10653
  }
10597
10654
  currentParent = currentParent.parent;
10598
10655
  }
10599
- }
10600
10656
 
10601
- // Check if they share a common parent (siblings or cousins)
10602
- if (!inSameFamily) {
10603
- const thisAncestors = new Set();
10604
- currentParent = thisPatternObj.parent;
10605
- while (currentParent) {
10606
- thisAncestors.add(currentParent);
10607
- currentParent = currentParent.parent;
10657
+ // Check if this route is ancestor of other route
10658
+ if (!inSameFamily) {
10659
+ currentParent = otherPatternObj.parent;
10660
+ while (currentParent) {
10661
+ if (currentParent === thisPatternObj) {
10662
+ inSameFamily = true;
10663
+ break;
10664
+ }
10665
+ currentParent = currentParent.parent;
10666
+ }
10608
10667
  }
10609
10668
 
10610
- currentParent = otherPatternObj.parent;
10611
- while (currentParent) {
10612
- if (thisAncestors.has(currentParent)) {
10613
- inSameFamily = true;
10614
- break;
10669
+ // Check if they share a common parent (siblings or cousins)
10670
+ if (!inSameFamily) {
10671
+ const thisAncestors = new Set();
10672
+ currentParent = thisPatternObj.parent;
10673
+ while (currentParent) {
10674
+ thisAncestors.add(currentParent);
10675
+ currentParent = currentParent.parent;
10676
+ }
10677
+
10678
+ currentParent = otherPatternObj.parent;
10679
+ while (currentParent) {
10680
+ if (thisAncestors.has(currentParent)) {
10681
+ inSameFamily = true;
10682
+ break;
10683
+ }
10684
+ currentParent = currentParent.parent;
10615
10685
  }
10616
- currentParent = currentParent.parent;
10617
10686
  }
10618
- }
10619
10687
 
10620
- if (inSameFamily) {
10621
- matchingRouteInSameFamily = true;
10688
+ if (inSameFamily) {
10689
+ matchingRouteInSameFamily = true;
10690
+ }
10622
10691
  }
10623
- }
10624
10692
 
10625
- // Only reset signal if:
10626
- // 1. We're navigating within the same route family (not to completely unrelated routes)
10627
- // 2. AND no matching route extracts this parameter from URL
10628
- // 3. AND parameter has no default value (making it truly optional)
10629
- if (matchingRouteInSameFamily && !parameterExtractedByMatchingRoute) {
10630
- const defaultValue = connection.getDefaultValue();
10631
- if (defaultValue === undefined) {
10632
- // Parameter is not extracted within same family and has no default - reset it
10633
- if (debug) {
10693
+ // Only reset signal if:
10694
+ // 1. We're navigating within the same route family (not to completely unrelated routes)
10695
+ // 2. AND no matching route extracts this parameter from URL
10696
+ // 3. AND parameter has no default value (making it truly optional)
10697
+ if (
10698
+ matchingRouteInSameFamily &&
10699
+ !parameterExtractedByMatchingRoute
10700
+ ) {
10701
+ const defaultValue = connection.getDefaultValue();
10702
+ if (defaultValue === undefined) {
10703
+ // Parameter is not extracted within same family and has no default - reset it
10704
+ if (debug) {
10705
+ console.debug(
10706
+ `[route] Same family navigation, ${paramName} not extracted and has no default: resetting signal`,
10707
+ );
10708
+ }
10709
+ paramSignal.value = undefined;
10710
+ } else if (debug) {
10711
+ // Parameter has a default value - preserve current signal value
10634
10712
  console.debug(
10635
- `[route] Same family navigation, ${paramName} not extracted and has no default: resetting signal`,
10713
+ `[route] Parameter ${paramName} has default value ${defaultValue}: preserving signal value: ${paramSignal.value}`,
10636
10714
  );
10637
10715
  }
10638
- paramSignal.value = undefined;
10639
10716
  } else if (debug) {
10640
- // Parameter has a default value - preserve current signal value
10641
- console.debug(
10642
- `[route] Parameter ${paramName} has default value ${defaultValue}: preserving signal value: ${paramSignal.value}`,
10643
- );
10717
+ if (!matchingRouteInSameFamily) {
10718
+ console.debug(
10719
+ `[route] Different route family: preserving ${paramName} signal value: ${paramSignal.value}`,
10720
+ );
10721
+ } else {
10722
+ console.debug(
10723
+ `[route] Parameter ${paramName} extracted by matching route: preserving signal value: ${paramSignal.value}`,
10724
+ );
10725
+ }
10644
10726
  }
10645
- } else if (debug) {
10646
- if (!matchingRouteInSameFamily) {
10647
- console.debug(
10648
- `[route] Different route family: preserving ${paramName} signal value: ${paramSignal.value}`,
10649
- );
10650
- } else {
10727
+ continue;
10728
+ }
10729
+
10730
+ // URL -> Signal sync: When route matches, ensure signal matches URL state
10731
+ // URL is the source of truth for explicit parameters
10732
+ const value = paramSignal.peek();
10733
+ if (urlParamValue === undefined) {
10734
+ // No URL parameter - reset signal to its current default value
10735
+ // (handles both static fallback and dynamic default cases)
10736
+ const defaultValue = connection.getDefaultValue();
10737
+ if (connection.isDefaultValue(value)) {
10738
+ // Signal already has correct default value, no sync needed
10739
+ continue;
10740
+ }
10741
+ if (debug) {
10651
10742
  console.debug(
10652
- `[route] Parameter ${paramName} extracted by matching route: preserving signal value: ${paramSignal.value}`,
10743
+ `[route] URL->Signal: ${paramName} not in URL, reset signal to default (${defaultValue})`,
10653
10744
  );
10654
10745
  }
10746
+ paramSignal.value = defaultValue;
10747
+ continue;
10655
10748
  }
10656
- continue;
10657
- }
10658
-
10659
- // URL -> Signal sync: When route matches, ensure signal matches URL state
10660
- // URL is the source of truth for explicit parameters
10661
- const value = paramSignal.peek();
10662
- if (urlParamValue === undefined) {
10663
- // No URL parameter - reset signal to its current default value
10664
- // (handles both static fallback and dynamic default cases)
10665
- const defaultValue = connection.getDefaultValue();
10666
- if (connection.isDefaultValue(value)) {
10667
- // Signal already has correct default value, no sync needed
10749
+ if (urlParamValue === value) {
10750
+ // Values already match, no sync needed
10668
10751
  continue;
10669
10752
  }
10670
10753
  if (debug) {
10671
10754
  console.debug(
10672
- `[route] URL->Signal: ${paramName} not in URL, reset signal to default (${defaultValue})`,
10755
+ `[route] URL->Signal: ${paramName}=${urlParamValue} in url, sync signal with url`,
10673
10756
  );
10674
10757
  }
10675
- paramSignal.value = defaultValue;
10676
- continue;
10677
- }
10678
- if (urlParamValue === value) {
10679
- // Values already match, no sync needed
10758
+ paramSignal.value = urlParamValue;
10680
10759
  continue;
10681
10760
  }
10682
- if (debug) {
10683
- console.debug(
10684
- `[route] URL->Signal: ${paramName}=${urlParamValue} in url, sync signal with url`,
10685
- );
10686
- }
10687
- paramSignal.value = urlParamValue;
10688
- continue;
10689
10761
  }
10690
- }
10691
- });
10692
-
10693
- // Reset flag after URL -> Signal synchronization is complete
10694
- isUpdatingRoutesFromUrl = false;
10762
+ });
10763
+ // Reset flag after URL -> Signal synchronization is complete
10764
+ isUpdatingRoutesFromUrl = false;
10695
10765
 
10696
- // must be after paramsSignal.value update to ensure the proxy target is set
10697
- // (so after the batch call)
10698
- const toLoadSet = new Set();
10699
- const toReloadSet = new Set();
10700
- const abortSignalMap = new Map();
10701
- const routeLoadRequestedMap = new Map();
10766
+ Object.assign(returnValue, { matchingRouteSet });
10767
+ }
10702
10768
 
10703
- const shouldLoadOrReload = (route, shouldLoad) => {
10704
- const routeAction = route.action;
10705
- const currentAction = routeAction.getCurrentAction();
10706
- if (shouldLoad) {
10707
- if (
10708
- navigationType === "replace" ||
10709
- currentAction.aborted ||
10710
- currentAction.error
10711
- ) {
10712
- shouldLoad = false;
10769
+ {
10770
+ // must be after paramsSignal.value update to ensure the proxy target is set
10771
+ // (so after the batch call)
10772
+ const toLoadSet = new Set();
10773
+ const toReloadSet = new Set();
10774
+ const abortSignalMap = new Map();
10775
+ const routeLoadRequestedMap = new Map();
10776
+ const shouldLoadOrReload = (route, shouldLoad) => {
10777
+ const routeAction = route.action;
10778
+ const currentAction = routeAction.getCurrentAction();
10779
+ if (shouldLoad) {
10780
+ if (
10781
+ navigationType === "replace" ||
10782
+ currentAction.aborted ||
10783
+ currentAction.error
10784
+ ) {
10785
+ shouldLoad = false;
10786
+ }
10787
+ }
10788
+ if (shouldLoad) {
10789
+ toLoadSet.add(currentAction);
10790
+ } else {
10791
+ toReloadSet.add(currentAction);
10792
+ }
10793
+ routeLoadRequestedMap.set(route, currentAction);
10794
+ // Create a new abort controller for this action
10795
+ const actionAbortController = new AbortController();
10796
+ actionAbortControllerWeakMap.set(currentAction, actionAbortController);
10797
+ abortSignalMap.set(currentAction, actionAbortController.signal);
10798
+ };
10799
+ const shouldLoad = (route) => {
10800
+ shouldLoadOrReload(route, true);
10801
+ };
10802
+ const shouldReload = (route) => {
10803
+ shouldLoadOrReload(route, false);
10804
+ };
10805
+ const shouldAbort = (route) => {
10806
+ const routeAction = route.action;
10807
+ const currentAction = routeAction.getCurrentAction();
10808
+ const actionAbortController =
10809
+ actionAbortControllerWeakMap.get(currentAction);
10810
+ if (actionAbortController) {
10811
+ actionAbortController.abort(`route no longer matching`);
10812
+ actionAbortControllerWeakMap.delete(currentAction);
10813
+ }
10814
+ };
10815
+ for (const {
10816
+ route,
10817
+ routePrivateProperties,
10818
+ newMatching,
10819
+ oldMatching,
10820
+ newParams,
10821
+ oldParams,
10822
+ } of routeMatchInfoSet) {
10823
+ const routeAction = route.action;
10824
+ if (!routeAction) {
10825
+ continue;
10713
10826
  }
10714
- }
10715
- if (shouldLoad) {
10716
- toLoadSet.add(currentAction);
10717
- } else {
10718
- toReloadSet.add(currentAction);
10719
- }
10720
- routeLoadRequestedMap.set(route, currentAction);
10721
- // Create a new abort controller for this action
10722
- const actionAbortController = new AbortController();
10723
- actionAbortControllerWeakMap.set(currentAction, actionAbortController);
10724
- abortSignalMap.set(currentAction, actionAbortController.signal);
10725
- };
10726
-
10727
- const shouldLoad = (route) => {
10728
- shouldLoadOrReload(route, true);
10729
- };
10730
- const shouldReload = (route) => {
10731
- shouldLoadOrReload(route, false);
10732
- };
10733
- const shouldAbort = (route) => {
10734
- const routeAction = route.action;
10735
- const currentAction = routeAction.getCurrentAction();
10736
- const actionAbortController =
10737
- actionAbortControllerWeakMap.get(currentAction);
10738
- if (actionAbortController) {
10739
- actionAbortController.abort(`route no longer matching`);
10740
- actionAbortControllerWeakMap.delete(currentAction);
10741
- }
10742
- };
10743
-
10744
- for (const {
10745
- route,
10746
- routePrivateProperties,
10747
- newMatching,
10748
- oldMatching,
10749
- newParams,
10750
- oldParams,
10751
- } of routeMatchInfoSet) {
10752
- const routeAction = route.action;
10753
- if (!routeAction) {
10754
- continue;
10755
- }
10756
10827
 
10757
- const becomesMatching = newMatching && !oldMatching;
10758
- const becomesNotMatching = !newMatching && oldMatching;
10759
- const paramsChangedWhileMatching =
10760
- newMatching && oldMatching && newParams !== oldParams;
10828
+ const becomesMatching = newMatching && !oldMatching;
10829
+ const becomesNotMatching = !newMatching && oldMatching;
10830
+ const paramsChangedWhileMatching =
10831
+ newMatching && oldMatching && newParams !== oldParams;
10761
10832
 
10762
- // Handle actions for routes that become matching
10763
- if (becomesMatching) {
10764
- shouldLoad(route);
10765
- continue;
10766
- }
10833
+ // Handle actions for routes that become matching
10834
+ if (becomesMatching) {
10835
+ shouldLoad(route);
10836
+ continue;
10837
+ }
10767
10838
 
10768
- // Handle actions for routes that become not matching - abort them
10769
- if (becomesNotMatching && ROUTE_DEACTIVATION_STRATEGY === "abort") {
10770
- shouldAbort(route);
10771
- continue;
10772
- }
10839
+ // Handle actions for routes that become not matching - abort them
10840
+ if (becomesNotMatching && ROUTE_DEACTIVATION_STRATEGY === "abort") {
10841
+ shouldAbort(route);
10842
+ continue;
10843
+ }
10773
10844
 
10774
- // Handle parameter changes while route stays matching
10775
- if (paramsChangedWhileMatching) {
10776
- shouldReload(route);
10845
+ // Handle parameter changes while route stays matching
10846
+ if (paramsChangedWhileMatching) {
10847
+ shouldReload(route);
10848
+ }
10777
10849
  }
10850
+ Object.assign(returnValue, {
10851
+ loadSet: toLoadSet,
10852
+ reloadSet: toReloadSet,
10853
+ abortSignalMap,
10854
+ routeLoadRequestedMap,
10855
+ });
10778
10856
  }
10779
10857
 
10780
- return {
10781
- loadSet: toLoadSet,
10782
- reloadSet: toReloadSet,
10783
- abortSignalMap,
10784
- routeLoadRequestedMap,
10785
- matchingRouteSet,
10786
- };
10858
+ return returnValue;
10787
10859
  };
10788
10860
 
10789
10861
  const registerRoute = (routePattern) => {
@@ -10799,10 +10871,8 @@ const registerRoute = (routePattern) => {
10799
10871
  matching: false,
10800
10872
  params: ROUTE_NOT_MATCHING_PARAMS,
10801
10873
  buildUrl: null,
10802
- bindAction: null,
10803
10874
  relativeUrl: null,
10804
10875
  url: null,
10805
- action: null,
10806
10876
  matchingSignal: null,
10807
10877
  paramsSignal: null,
10808
10878
  urlSignal: null,
@@ -10811,6 +10881,10 @@ const registerRoute = (routePattern) => {
10811
10881
  toString: () => {
10812
10882
  return `route "${cleanPattern}"`;
10813
10883
  },
10884
+
10885
+ bindAction: null,
10886
+ action: null,
10887
+ actionStatusSignal: null,
10814
10888
  };
10815
10889
  routeSet.add(route);
10816
10890
  const routePrivateProperties = {
@@ -11047,7 +11121,14 @@ const registerRoute = (routePattern) => {
11047
11121
  cleanupCallbackSet.add(disposeRelativeUrlEffect);
11048
11122
  cleanupCallbackSet.add(disposeUrlEffect);
11049
11123
 
11050
- // action stuff (for later)
11124
+ // action
11125
+ route.actionStatusSignal = signal({
11126
+ loading: false,
11127
+ error: null,
11128
+ aborted: false,
11129
+ completed: true,
11130
+ data: undefined,
11131
+ });
11051
11132
  route.bindAction = (action) => {
11052
11133
  const { store } = action.meta;
11053
11134
  if (store) {
@@ -11077,6 +11158,10 @@ const registerRoute = (routePattern) => {
11077
11158
 
11078
11159
  const actionBoundToThisRoute = action.bindParams(route.paramsSignal);
11079
11160
  route.action = actionBoundToThisRoute;
11161
+ route.actionStatusSignal = computed(() => {
11162
+ const actionStatus = getActionStatus(actionBoundToThisRoute);
11163
+ return actionStatus;
11164
+ });
11080
11165
  return actionBoundToThisRoute;
11081
11166
  };
11082
11167
 
@@ -11761,7 +11846,8 @@ const useForceRender = () => {
11761
11846
  * . Tester le code splitting avec .lazy + import dynamique
11762
11847
  * pour les elements des routes
11763
11848
  *
11764
- * 3. Ajouter la possibilite d'avoir des action sur les routes
11849
+ * 3. Ajouter la possibilite d'avoir des
11850
+ * sur les routes
11765
11851
  * Tester juste les data pour commencer
11766
11852
  * On aura ptet besoin d'un useRouteData au lieu de passer par un element qui est une fonction
11767
11853
  * pour que react ne re-render pas tout
@@ -18531,6 +18617,9 @@ const useKeyboardShortcuts = (
18531
18617
 
18532
18618
  useEffect(() => {
18533
18619
  const element = elementRef.current;
18620
+ if (!element) {
18621
+ return null;
18622
+ }
18534
18623
  const shortcutsCopy = [];
18535
18624
  for (const shortcutCandidate of shortcuts) {
18536
18625
  shortcutsCopy.push({
@@ -18543,7 +18632,8 @@ const useKeyboardShortcuts = (
18543
18632
  return false;
18544
18633
  }
18545
18634
  const { action } = shortcutCandidate;
18546
- return requestAction(element, action, {
18635
+ const actionWithEvent = action.bindParams(keyboardEvent);
18636
+ return requestAction(element, actionWithEvent, {
18547
18637
  actionOrigin: "keyboard_shortcut",
18548
18638
  event: keyboardEvent,
18549
18639
  requester: document.activeElement,
@@ -18846,11 +18936,15 @@ const useUIStateController = (
18846
18936
  getStateFromProp = (prop) => prop,
18847
18937
  getPropFromState = (state) => state,
18848
18938
  getStateFromParent,
18939
+ persists,
18849
18940
  } = {},
18850
18941
  ) => {
18851
18942
  const parentUIStateController = useContext(ParentUIStateControllerContext);
18852
18943
  const formContext = useContext(FormContext);
18853
18944
  const { id, name, onUIStateChange, action } = props;
18945
+ if (persists === undefined && formContext) {
18946
+ persists = true;
18947
+ }
18854
18948
  const uncontrolled = !formContext && !action;
18855
18949
  const [navState, setNavState] = useNavState$1(id);
18856
18950
 
@@ -18867,7 +18961,7 @@ const useUIStateController = (
18867
18961
  // not controlled but want an initial state (a value or being checked)
18868
18962
  return getStateFromProp(defaultState);
18869
18963
  }
18870
- if (formContext && navState) {
18964
+ if (persists && navState) {
18871
18965
  // not controlled but want to use value from nav state
18872
18966
  // (I think this should likely move earlier to win over the hasUIStateProp when it's undefined)
18873
18967
  return getStateFromProp(navState);
@@ -18971,7 +19065,7 @@ const useUIStateController = (
18971
19065
  getStateFromProp,
18972
19066
  setUIState: (prop, e) => {
18973
19067
  const newUIState = uiStateController.getStateFromProp(prop);
18974
- if (formContext) {
19068
+ if (persists) {
18975
19069
  setNavState(prop);
18976
19070
  }
18977
19071
  const currentUIState = uiStateController.uiState;
@@ -18991,7 +19085,7 @@ const useUIStateController = (
18991
19085
  uiStateController.setUIState(currentState, e);
18992
19086
  },
18993
19087
  actionEnd: () => {
18994
- if (formContext) {
19088
+ if (persists) {
18995
19089
  setNavState(undefined);
18996
19090
  }
18997
19091
  },
@@ -20126,7 +20220,8 @@ const LinkPlain = props => {
20126
20220
  discrete,
20127
20221
  blankTargetIcon,
20128
20222
  anchorIcon,
20129
- icon,
20223
+ startIcon,
20224
+ endIcon,
20130
20225
  spacing,
20131
20226
  revealOnInteraction = Boolean(titleLevel),
20132
20227
  hrefFallback = !anchor,
@@ -20149,29 +20244,29 @@ const LinkPlain = props => {
20149
20244
  } = getHrefTargetInfo(href);
20150
20245
  const innerTarget = target === undefined ? isSameSite ? "_self" : "_blank" : target;
20151
20246
  const innerRel = rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel;
20152
- let innerIcon;
20153
- if (icon === undefined) {
20247
+ let innerEndIcon;
20248
+ if (endIcon === undefined) {
20154
20249
  // Check for special protocol or domain-specific icons first
20155
20250
  if (href?.startsWith("tel:")) {
20156
- innerIcon = jsx(PhoneSvg, {});
20251
+ innerEndIcon = jsx(PhoneSvg, {});
20157
20252
  } else if (href?.startsWith("sms:")) {
20158
- innerIcon = jsx(SmsSvg, {});
20253
+ innerEndIcon = jsx(SmsSvg, {});
20159
20254
  } else if (href?.startsWith("mailto:")) {
20160
- innerIcon = jsx(EmailSvg, {});
20255
+ innerEndIcon = jsx(EmailSvg, {});
20161
20256
  } else if (href?.includes("github.com")) {
20162
- innerIcon = jsx(GithubSvg, {});
20257
+ innerEndIcon = jsx(GithubSvg, {});
20163
20258
  } else {
20164
20259
  // Fall back to default icon logic
20165
20260
  const innerBlankTargetIcon = blankTargetIcon === undefined ? innerTarget === "_blank" : blankTargetIcon;
20166
20261
  const innerAnchorIcon = anchorIcon === undefined ? isAnchor : anchorIcon;
20167
20262
  if (innerBlankTargetIcon) {
20168
- innerIcon = innerBlankTargetIcon === true ? jsx(LinkBlankTargetSvg, {}) : innerBlankTargetIcon;
20263
+ innerEndIcon = innerBlankTargetIcon === true ? jsx(LinkBlankTargetSvg, {}) : innerBlankTargetIcon;
20169
20264
  } else if (innerAnchorIcon) {
20170
- innerIcon = innerAnchorIcon === true ? jsx(LinkAnchorSvg, {}) : anchorIcon;
20265
+ innerEndIcon = innerAnchorIcon === true ? jsx(LinkAnchorSvg, {}) : anchorIcon;
20171
20266
  }
20172
20267
  }
20173
20268
  } else {
20174
- innerIcon = icon;
20269
+ innerEndIcon = endIcon;
20175
20270
  }
20176
20271
  const innerChildren = children || (hrefFallback ? href : children);
20177
20272
  return jsxs(Box, {
@@ -20225,12 +20320,15 @@ const LinkPlain = props => {
20225
20320
  }
20226
20321
  onKeyDown?.(e);
20227
20322
  },
20228
- children: [jsx(LoaderBackground, {
20323
+ children: [startIcon && jsx(Icon, {
20324
+ marginRight: innerChildren ? "xxs" : undefined,
20325
+ children: startIcon
20326
+ }), jsx(LoaderBackground, {
20229
20327
  loading: loading,
20230
20328
  color: "var(--link-loader-color)"
20231
- }), applySpacingOnTextChildren(innerChildren, spacing), innerIcon && jsx(Icon, {
20329
+ }), applySpacingOnTextChildren(innerChildren, spacing), endIcon && jsx(Icon, {
20232
20330
  marginLeft: innerChildren ? "xxs" : undefined,
20233
- children: innerIcon
20331
+ children: innerEndIcon
20234
20332
  })]
20235
20333
  });
20236
20334
  };
@@ -20759,151 +20857,565 @@ const TabBasic = ({
20759
20857
  });
20760
20858
  };
20761
20859
 
20762
- const createAvailableConstraint = (
20763
- // the set might be incomplete (the front usually don't have the full copy of all the items from the backend)
20764
- // but this is already nice to help user with what we know
20765
- // it's also possible that front is unsync with backend, preventing user to choose a value
20766
- // that is actually free.
20767
- // But this is unlikely to happen and user could reload the page to be able to choose that name
20768
- // that suddenly became available
20769
- existingValueSet,
20770
- message = `"{value}" est utilisé. Veuillez entrer une autre valeur.`,
20860
+ const useFocusGroup = (
20861
+ elementRef,
20862
+ { enabled = true, direction, skipTab, loop, name } = {},
20771
20863
  ) => {
20772
- return {
20773
- name: "available",
20774
- messageAttribute: "data-available-message",
20775
- check: (field) => {
20776
- const fieldValue = field.value;
20777
- const hasConflict = existingValueSet.has(fieldValue);
20778
- // console.log({
20779
- // inputValue,
20780
- // names: Array.from(otherNameSet.values()),
20781
- // hasConflict,
20782
- // });
20783
- if (hasConflict) {
20784
- return replaceStringVars(message, {
20785
- "{value}": fieldValue,
20786
- });
20787
- }
20788
- return "";
20789
- },
20790
- };
20864
+ useLayoutEffect(() => {
20865
+ if (!enabled) {
20866
+ return null;
20867
+ }
20868
+ const focusGroup = initFocusGroup(elementRef.current, {
20869
+ direction,
20870
+ skipTab,
20871
+ loop,
20872
+ name,
20873
+ });
20874
+ return focusGroup.cleanup;
20875
+ }, [direction, skipTab, loop, name]);
20791
20876
  };
20792
20877
 
20793
- const DEFAULT_VALIDITY_STATE = { valid: true };
20794
- const useConstraintValidityState = (ref) => {
20795
- const checkValue = () => {
20796
- const element = ref.current;
20797
- if (!element) {
20798
- return DEFAULT_VALIDITY_STATE;
20878
+ installImportMetaCss(import.meta);const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
20879
+ const downArrowPath = "M480-280L160-600l80-80 240 240 240-240 80 80-320 320z";
20880
+ import.meta.css = /* css */`
20881
+ .navi_summary_marker {
20882
+ width: 1em;
20883
+ height: 1em;
20884
+ line-height: 1em;
20885
+
20886
+ .navi_summary_marker_loading_container {
20887
+ transform: scale(0.3);
20888
+ transition: transform 0.3s linear;
20889
+
20890
+ .navi_summary_marker_background_circle,
20891
+ .navi_summary_marker_foreground_circle {
20892
+ opacity: 0;
20893
+ transition: opacity 0.3s ease-in-out;
20894
+ }
20895
+
20896
+ .navi_summary_marker_foreground_circle {
20897
+ stroke-dasharray: 503 1507; /* ~25% of circle perimeter */
20898
+ stroke-dashoffset: 0;
20899
+ animation: progress-around-circle 1.5s linear infinite;
20900
+ }
20799
20901
  }
20800
- const { __validationInterface__ } = element;
20801
- if (!__validationInterface__) {
20802
- return DEFAULT_VALIDITY_STATE;
20902
+
20903
+ .navi_summary_marker_arrow {
20904
+ opacity: 1;
20905
+ transition: opacity 0.3s ease-in-out;
20906
+ animation-duration: 0.3s;
20907
+ animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
20908
+ animation-fill-mode: forwards;
20909
+
20910
+ &[data-animation-target="down"] {
20911
+ animation-name: morph-to-down;
20912
+ }
20913
+
20914
+ &[data-animation-target="right"] {
20915
+ animation-name: morph-to-right;
20916
+ }
20803
20917
  }
20804
- const value = __validationInterface__.getConstraintValidityState();
20805
- return value;
20806
- };
20807
20918
 
20808
- const [constraintValidityState, setConstraintValidityState] =
20809
- useState(checkValue);
20919
+ &[data-loading] {
20920
+ .navi_summary_marker_loading_container {
20921
+ transform: scale(1);
20810
20922
 
20811
- useLayoutEffect(() => {
20812
- const element = ref.current;
20813
- if (!element) {
20814
- return;
20923
+ .navi_summary_marker_background_circle {
20924
+ opacity: 0.2;
20925
+ }
20926
+ .navi_summary_marker_foreground_circle {
20927
+ opacity: 1;
20928
+ }
20929
+ }
20930
+ .navi_summary_marker_arrow {
20931
+ opacity: 0;
20932
+ }
20815
20933
  }
20816
- setConstraintValidityState(checkValue());
20817
- element.addEventListener(NAVI_VALIDITY_CHANGE_CUSTOM_EVENT, () => {
20818
- setConstraintValidityState(checkValue());
20819
- });
20934
+ }
20935
+ @keyframes progress-around-circle {
20936
+ 0% {
20937
+ stroke-dashoffset: 0;
20938
+ }
20939
+ 100% {
20940
+ stroke-dashoffset: -2010;
20941
+ }
20942
+ }
20943
+ @keyframes morph-to-down {
20944
+ from {
20945
+ d: path("${rightArrowPath}");
20946
+ }
20947
+ to {
20948
+ d: path("${downArrowPath}");
20949
+ }
20950
+ }
20951
+ @keyframes morph-to-right {
20952
+ from {
20953
+ d: path("${downArrowPath}");
20954
+ }
20955
+ to {
20956
+ d: path("${rightArrowPath}");
20957
+ }
20958
+ }
20959
+ `;
20960
+ const SummaryMarker = ({
20961
+ open,
20962
+ loading
20963
+ }) => {
20964
+ const showLoading = useDebounceTrue(loading, 300);
20965
+ const mountedRef = useRef(false);
20966
+ const prevOpenRef = useRef(open);
20967
+ useLayoutEffect(() => {
20968
+ mountedRef.current = true;
20969
+ return () => {
20970
+ mountedRef.current = false;
20971
+ };
20820
20972
  }, []);
20821
-
20822
- return constraintValidityState;
20973
+ const shouldAnimate = mountedRef.current && prevOpenRef.current !== open;
20974
+ prevOpenRef.current = open;
20975
+ return jsx("span", {
20976
+ className: "navi_summary_marker",
20977
+ "data-loading": showLoading ? "" : undefined,
20978
+ children: jsxs("svg", {
20979
+ viewBox: "0 -960 960 960",
20980
+ xmlns: "http://www.w3.org/2000/svg",
20981
+ children: [jsxs("g", {
20982
+ className: "navi_summary_marker_loading_container",
20983
+ "transform-origin": "480px -480px",
20984
+ children: [jsx("circle", {
20985
+ className: "navi_summary_marker_background_circle",
20986
+ cx: "480",
20987
+ cy: "-480",
20988
+ r: "320",
20989
+ stroke: "currentColor",
20990
+ fill: "none",
20991
+ strokeWidth: "60",
20992
+ opacity: "0.2"
20993
+ }), jsx("circle", {
20994
+ className: "navi_summary_marker_foreground_circle",
20995
+ cx: "480",
20996
+ cy: "-480",
20997
+ r: "320",
20998
+ stroke: "currentColor",
20999
+ fill: "none",
21000
+ strokeWidth: "60",
21001
+ strokeLinecap: "round",
21002
+ strokeDasharray: "503 1507"
21003
+ })]
21004
+ }), jsx("g", {
21005
+ "transform-origin": "480px -480px",
21006
+ children: jsx("path", {
21007
+ className: "navi_summary_marker_arrow",
21008
+ fill: "currentColor",
21009
+ "data-animation-target": shouldAnimate ? open ? "down" : "right" : undefined,
21010
+ d: open ? downArrowPath : rightArrowPath
21011
+ })
21012
+ })]
21013
+ })
21014
+ });
20823
21015
  };
20824
21016
 
20825
21017
  installImportMetaCss(import.meta);import.meta.css = /* css */`
20826
- @layer navi {
20827
- label {
21018
+ .navi_details {
21019
+ position: relative;
21020
+ z-index: 1;
21021
+ display: flex;
21022
+ flex-shrink: 0;
21023
+ flex-direction: column;
21024
+
21025
+ summary {
21026
+ display: flex;
21027
+ flex-shrink: 0;
21028
+ flex-direction: column;
20828
21029
  cursor: pointer;
20829
- }
21030
+ user-select: none;
20830
21031
 
20831
- label[data-readonly],
20832
- label[data-disabled] {
20833
- color: rgba(0, 0, 0, 0.5);
20834
- cursor: default;
21032
+ &:focus {
21033
+ z-index: 1;
21034
+ }
21035
+
21036
+ .navi_summary_body {
21037
+ display: flex;
21038
+ width: 100%;
21039
+ flex-direction: row;
21040
+ align-items: center;
21041
+ gap: 0.2em;
21042
+
21043
+ .navi_summary_label {
21044
+ display: flex;
21045
+ padding-right: 10px;
21046
+ flex: 1;
21047
+ align-items: center;
21048
+ gap: 0.2em;
21049
+ }
21050
+ }
20835
21051
  }
20836
21052
  }
20837
21053
  `;
20838
- const ReportReadOnlyOnLabelContext = createContext();
20839
- const ReportDisabledOnLabelContext = createContext();
20840
- const LabelPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
20841
- const Label = props => {
21054
+ const Details = props => {
20842
21055
  const {
20843
- readOnly,
20844
- disabled,
20845
- children,
20846
- ...rest
21056
+ value = "on",
21057
+ persists
20847
21058
  } = props;
20848
- const [inputReadOnly, setInputReadOnly] = useState(false);
20849
- const innerReadOnly = readOnly || inputReadOnly;
20850
- const [inputDisabled, setInputDisabled] = useState(false);
20851
- const innerDisabled = disabled || inputDisabled;
20852
- return jsx(Box, {
20853
- ...rest,
20854
- as: "label",
20855
- pseudoClasses: LabelPseudoClasses,
20856
- basePseudoState: {
20857
- ":read-only": innerReadOnly,
20858
- ":disabled": innerDisabled
20859
- },
20860
- children: jsx(ReportReadOnlyOnLabelContext.Provider, {
20861
- value: setInputReadOnly,
20862
- children: jsx(ReportDisabledOnLabelContext.Provider, {
20863
- value: setInputDisabled,
20864
- children: children
20865
- })
21059
+ const uiStateController = useUIStateController(props, "details", {
21060
+ statePropName: "open",
21061
+ defaultStatePropName: "defaultOpen",
21062
+ fallbackState: false,
21063
+ getStateFromProp: open => open ? value : undefined,
21064
+ getPropFromState: Boolean,
21065
+ persists
21066
+ });
21067
+ const uiState = useUIState(uiStateController);
21068
+ const details = renderActionableComponent(props, {
21069
+ Basic: DetailsBasic,
21070
+ WithAction: DetailsWithAction
21071
+ });
21072
+ return jsx(UIStateControllerContext.Provider, {
21073
+ value: uiStateController,
21074
+ children: jsx(UIStateContext.Provider, {
21075
+ value: uiState,
21076
+ children: details
20866
21077
  })
20867
21078
  });
20868
21079
  };
21080
+ const DetailsBasic = props => {
21081
+ const uiStateController = useContext(UIStateControllerContext);
21082
+ const uiState = useContext(UIStateContext);
21083
+ const {
21084
+ id,
21085
+ label = "Summary",
21086
+ loading,
21087
+ focusGroup,
21088
+ focusGroupDirection,
21089
+ arrowKeyShortcuts = true,
21090
+ openKeyShortcut = "ArrowRight",
21091
+ closeKeyShortcut = "ArrowLeft",
21092
+ onToggle,
21093
+ children,
21094
+ ...rest
21095
+ } = props;
21096
+ const defaultRef = useRef();
21097
+ const ref = rest.ref || defaultRef;
21098
+ const open = Boolean(uiState);
21099
+ useFocusGroup(ref, {
21100
+ enabled: focusGroup,
21101
+ name: typeof focusGroup === "string" ? focusGroup : undefined,
21102
+ direction: focusGroupDirection
21103
+ });
20869
21104
 
20870
- installImportMetaCss(import.meta);import.meta.css = /* css */`
20871
- @layer navi {
20872
- .navi_checkbox {
20873
- --margin: 3px 3px 3px 4px;
20874
- --outline-offset: 1px;
20875
- --outline-width: 2px;
20876
- --border-width: 1px;
20877
- --border-radius: 2px;
20878
- --width: 0.815em;
20879
- --height: 0.815em;
20880
-
20881
- --outline-color: var(--navi-focus-outline-color);
20882
- --loader-color: var(--navi-loader-color);
20883
- --border-color: light-dark(#767676, #8e8e93);
20884
- --background-color: white;
20885
- --accent-color: light-dark(#4476ff, #3b82f6);
20886
- --background-color-checked: var(--accent-color);
20887
- --border-color-checked: var(--accent-color);
20888
- --checkmark-color-light: white;
20889
- --checkmark-color-dark: rgb(55, 55, 55);
20890
- --checkmark-color: var(--checkmark-color-light);
20891
- --cursor: pointer;
20892
-
20893
- --color-mix-light: black;
20894
- --color-mix-dark: white;
20895
- --color-mix: var(--color-mix-light);
21105
+ /**
21106
+ * Browser will dispatch "toggle" event even if we set open={true}
21107
+ * When rendering the component for the first time
21108
+ * We have to ensure the initial "toggle" event is ignored.
21109
+ *
21110
+ * If we don't do that code will think the details has changed and run logic accordingly
21111
+ * For example it will try to navigate to the current url while we are already there
21112
+ *
21113
+ * See:
21114
+ * - https://techblog.thescore.com/2024/10/08/why-we-decided-to-change-how-the-details-element-works/
21115
+ * - https://github.com/whatwg/html/issues/4500
21116
+ * - https://stackoverflow.com/questions/58942600/react-html-details-toggles-uncontrollably-when-starts-open
21117
+ *
21118
+ */
20896
21119
 
20897
- /* Hover */
20898
- --border-color-hover: color-mix(in srgb, var(--border-color) 60%, black);
20899
- --border-color-hover-checked: color-mix(
20900
- in srgb,
20901
- var(--border-color-checked) 80%,
20902
- var(--color-mix)
20903
- );
20904
- --background-color-hover: var(--background-color);
20905
- --background-color-hover-checked: color-mix(
20906
- in srgb,
21120
+ const summaryRef = useRef(null);
21121
+ useKeyboardShortcuts(ref, [{
21122
+ key: openKeyShortcut,
21123
+ enabled: arrowKeyShortcuts,
21124
+ when: e => document.activeElement === summaryRef.current &&
21125
+ // avoid handling openKeyShortcut twice when keydown occurs inside nested details
21126
+ !e.defaultPrevented,
21127
+ action: e => {
21128
+ const details = ref.current;
21129
+ if (!details.open) {
21130
+ e.preventDefault();
21131
+ details.open = true;
21132
+ return;
21133
+ }
21134
+ const summary = summaryRef.current;
21135
+ const firstFocusableElementInDetails = findAfter(summary, elementIsFocusable, {
21136
+ root: details
21137
+ });
21138
+ if (!firstFocusableElementInDetails) {
21139
+ return;
21140
+ }
21141
+ e.preventDefault();
21142
+ firstFocusableElementInDetails.focus();
21143
+ }
21144
+ }, {
21145
+ key: closeKeyShortcut,
21146
+ enabled: arrowKeyShortcuts,
21147
+ when: () => {
21148
+ const details = ref.current;
21149
+ return details.open;
21150
+ },
21151
+ action: e => {
21152
+ const details = ref.current;
21153
+ const summary = summaryRef.current;
21154
+ if (document.activeElement === summary) {
21155
+ e.preventDefault();
21156
+ summary.focus();
21157
+ details.open = false;
21158
+ } else {
21159
+ e.preventDefault();
21160
+ summary.focus();
21161
+ }
21162
+ }
21163
+ }]);
21164
+ const mountedRef = useRef(false);
21165
+ useEffect(() => {
21166
+ mountedRef.current = true;
21167
+ }, []);
21168
+ return jsxs(Box, {
21169
+ as: "details",
21170
+ ...rest,
21171
+ ref: ref,
21172
+ id: id,
21173
+ baseClassName: "navi_details",
21174
+ onToggle: e => {
21175
+ const isOpen = e.newState === "open";
21176
+ if (mountedRef.current) {
21177
+ if (isOpen) {
21178
+ uiStateController.setUIState(true, e);
21179
+ } else {
21180
+ uiStateController.setUIState(false, e);
21181
+ }
21182
+ }
21183
+ onToggle?.(e);
21184
+ },
21185
+ open: open,
21186
+ children: [jsx("summary", {
21187
+ ref: summaryRef,
21188
+ children: jsxs("div", {
21189
+ className: "navi_summary_body",
21190
+ children: [jsx(SummaryMarker, {
21191
+ open: open,
21192
+ loading: loading
21193
+ }), jsx("div", {
21194
+ className: "navi_summary_label",
21195
+ children: label
21196
+ })]
21197
+ })
21198
+ }), children]
21199
+ });
21200
+ };
21201
+ const DetailsWithAction = props => {
21202
+ const uiStateController = useContext(UIStateControllerContext);
21203
+ const {
21204
+ action,
21205
+ loading,
21206
+ onToggle,
21207
+ onCancel,
21208
+ onActionPrevented,
21209
+ onActionStart,
21210
+ onActionAbort,
21211
+ onActionError,
21212
+ onActionEnd,
21213
+ children,
21214
+ ...rest
21215
+ } = props;
21216
+ const defaultRef = useRef();
21217
+ const ref = rest.ref || defaultRef;
21218
+ const effectiveAction = useAction(action);
21219
+ const actionStatus = useActionStatus(effectiveAction);
21220
+ const {
21221
+ loading: actionLoading
21222
+ } = actionStatus;
21223
+ const executeAction = useExecuteAction(ref, {
21224
+ // the error will be displayed by actionRenderer inside <details>
21225
+ errorEffect: "none"
21226
+ });
21227
+ useActionEvents(ref, {
21228
+ onCancel: (e, reason) => {
21229
+ if (reason === "blur_invalid") {
21230
+ return;
21231
+ }
21232
+ uiStateController.resetUIState(e);
21233
+ onCancel?.(e, reason);
21234
+ },
21235
+ onPrevented: onActionPrevented,
21236
+ onRequested: e => forwardActionRequested(e, effectiveAction),
21237
+ onAction: executeAction,
21238
+ onStart: onActionStart,
21239
+ onAbort: e => {
21240
+ uiStateController.resetUIState(e);
21241
+ onActionAbort?.(e);
21242
+ },
21243
+ onError: e => {
21244
+ uiStateController.resetUIState(e);
21245
+ onActionError?.(e);
21246
+ },
21247
+ onEnd: e => {
21248
+ onActionEnd?.(e);
21249
+ }
21250
+ });
21251
+ return jsx(DetailsBasic, {
21252
+ ...rest,
21253
+ ref: ref,
21254
+ loading: loading || actionLoading,
21255
+ onToggle: toggleEvent => {
21256
+ const isOpen = toggleEvent.newState === "open";
21257
+ if (isOpen) {
21258
+ dispatchActionRequestedCustomEvent(toggleEvent.target, {
21259
+ event: toggleEvent,
21260
+ requester: toggleEvent.target
21261
+ });
21262
+ } else {
21263
+ effectiveAction.abort();
21264
+ }
21265
+ onToggle?.(toggleEvent);
21266
+ },
21267
+ children: jsx(ActionRenderer, {
21268
+ action: effectiveAction,
21269
+ children: children
21270
+ })
21271
+ });
21272
+ };
21273
+
21274
+ const createAvailableConstraint = (
21275
+ // the set might be incomplete (the front usually don't have the full copy of all the items from the backend)
21276
+ // but this is already nice to help user with what we know
21277
+ // it's also possible that front is unsync with backend, preventing user to choose a value
21278
+ // that is actually free.
21279
+ // But this is unlikely to happen and user could reload the page to be able to choose that name
21280
+ // that suddenly became available
21281
+ existingValueSet,
21282
+ message = `"{value}" est utilisé. Veuillez entrer une autre valeur.`,
21283
+ ) => {
21284
+ return {
21285
+ name: "available",
21286
+ messageAttribute: "data-available-message",
21287
+ check: (field) => {
21288
+ const fieldValue = field.value;
21289
+ const hasConflict = existingValueSet.has(fieldValue);
21290
+ // console.log({
21291
+ // inputValue,
21292
+ // names: Array.from(otherNameSet.values()),
21293
+ // hasConflict,
21294
+ // });
21295
+ if (hasConflict) {
21296
+ return replaceStringVars(message, {
21297
+ "{value}": fieldValue,
21298
+ });
21299
+ }
21300
+ return "";
21301
+ },
21302
+ };
21303
+ };
21304
+
21305
+ const DEFAULT_VALIDITY_STATE = { valid: true };
21306
+ const useConstraintValidityState = (ref) => {
21307
+ const checkValue = () => {
21308
+ const element = ref.current;
21309
+ if (!element) {
21310
+ return DEFAULT_VALIDITY_STATE;
21311
+ }
21312
+ const { __validationInterface__ } = element;
21313
+ if (!__validationInterface__) {
21314
+ return DEFAULT_VALIDITY_STATE;
21315
+ }
21316
+ const value = __validationInterface__.getConstraintValidityState();
21317
+ return value;
21318
+ };
21319
+
21320
+ const [constraintValidityState, setConstraintValidityState] =
21321
+ useState(checkValue);
21322
+
21323
+ useLayoutEffect(() => {
21324
+ const element = ref.current;
21325
+ if (!element) {
21326
+ return;
21327
+ }
21328
+ setConstraintValidityState(checkValue());
21329
+ element.addEventListener(NAVI_VALIDITY_CHANGE_CUSTOM_EVENT, () => {
21330
+ setConstraintValidityState(checkValue());
21331
+ });
21332
+ }, []);
21333
+
21334
+ return constraintValidityState;
21335
+ };
21336
+
21337
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
21338
+ @layer navi {
21339
+ label {
21340
+ cursor: pointer;
21341
+ }
21342
+
21343
+ label[data-readonly],
21344
+ label[data-disabled] {
21345
+ color: rgba(0, 0, 0, 0.5);
21346
+ cursor: default;
21347
+ }
21348
+ }
21349
+ `;
21350
+ const ReportReadOnlyOnLabelContext = createContext();
21351
+ const ReportDisabledOnLabelContext = createContext();
21352
+ const LabelPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
21353
+ const Label = props => {
21354
+ const {
21355
+ readOnly,
21356
+ disabled,
21357
+ children,
21358
+ ...rest
21359
+ } = props;
21360
+ const [inputReadOnly, setInputReadOnly] = useState(false);
21361
+ const innerReadOnly = readOnly || inputReadOnly;
21362
+ const [inputDisabled, setInputDisabled] = useState(false);
21363
+ const innerDisabled = disabled || inputDisabled;
21364
+ return jsx(Box, {
21365
+ ...rest,
21366
+ as: "label",
21367
+ pseudoClasses: LabelPseudoClasses,
21368
+ basePseudoState: {
21369
+ ":read-only": innerReadOnly,
21370
+ ":disabled": innerDisabled
21371
+ },
21372
+ children: jsx(ReportReadOnlyOnLabelContext.Provider, {
21373
+ value: setInputReadOnly,
21374
+ children: jsx(ReportDisabledOnLabelContext.Provider, {
21375
+ value: setInputDisabled,
21376
+ children: children
21377
+ })
21378
+ })
21379
+ });
21380
+ };
21381
+
21382
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
21383
+ @layer navi {
21384
+ .navi_checkbox {
21385
+ --margin: 3px 3px 3px 4px;
21386
+ --outline-offset: 1px;
21387
+ --outline-width: 2px;
21388
+ --border-width: 1px;
21389
+ --border-radius: 2px;
21390
+ --width: 0.815em;
21391
+ --height: 0.815em;
21392
+
21393
+ --outline-color: var(--navi-focus-outline-color);
21394
+ --loader-color: var(--navi-loader-color);
21395
+ --border-color: light-dark(#767676, #8e8e93);
21396
+ --background-color: white;
21397
+ --accent-color: light-dark(#4476ff, #3b82f6);
21398
+ --background-color-checked: var(--accent-color);
21399
+ --border-color-checked: var(--accent-color);
21400
+ --checkmark-color-light: white;
21401
+ --checkmark-color-dark: rgb(55, 55, 55);
21402
+ --checkmark-color: var(--checkmark-color-light);
21403
+ --cursor: pointer;
21404
+
21405
+ --color-mix-light: black;
21406
+ --color-mix-dark: white;
21407
+ --color-mix: var(--color-mix-light);
21408
+
21409
+ /* Hover */
21410
+ --border-color-hover: color-mix(in srgb, var(--border-color) 60%, black);
21411
+ --border-color-hover-checked: color-mix(
21412
+ in srgb,
21413
+ var(--border-color-checked) 80%,
21414
+ var(--color-mix)
21415
+ );
21416
+ --background-color-hover: var(--background-color);
21417
+ --background-color-hover-checked: color-mix(
21418
+ in srgb,
20907
21419
  var(--background-color-checked) 80%,
20908
21420
  var(--color-mix)
20909
21421
  );
@@ -22638,6 +23150,7 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
22638
23150
  --border-width: 1px;
22639
23151
  --outline-width: 1px;
22640
23152
  --outer-width: calc(var(--border-width) + var(--outline-width));
23153
+ --font-size: 14px;
22641
23154
 
22642
23155
  /* Default */
22643
23156
  --outline-color: var(--navi-focus-outline-color);
@@ -22657,6 +23170,9 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
22657
23170
  --color-hover: var(--color);
22658
23171
  /* Active */
22659
23172
  --border-color-active: color-mix(in srgb, var(--border-color) 90%, black);
23173
+ /* Focus */
23174
+ --border-color-focus: var(--border-color);
23175
+ --background-color-focus: var(--background-color);
22660
23176
  /* Readonly */
22661
23177
  --border-color-readonly: color-mix(
22662
23178
  in srgb,
@@ -22685,6 +23201,8 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
22685
23201
  border-radius: inherit;
22686
23202
  cursor: inherit;
22687
23203
 
23204
+ --start-icon-size: 0;
23205
+ --end-icon-size: 0;
22688
23206
  --x-outline-width: var(--outline-width);
22689
23207
  --x-border-radius: var(--border-radius);
22690
23208
  --x-border-width: var(--border-width);
@@ -22695,19 +23213,31 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
22695
23213
  --x-color: var(--color);
22696
23214
  --x-placeholder-color: var(--placeholder-color);
22697
23215
 
23216
+ --x-padding-top-base: var(
23217
+ --padding-top,
23218
+ var(--padding-y, var(--padding, 1px))
23219
+ );
23220
+ --x-padding-right-base: var(
23221
+ --padding-right,
23222
+ var(--padding-x, var(--padding, 2px))
23223
+ );
23224
+ --x-padding-bottom-base: var(
23225
+ --padding-bottom,
23226
+ var(--padding-y, var(--padding, 1px))
23227
+ );
23228
+ --x-padding-left-base: var(
23229
+ --padding-left,
23230
+ var(--padding-x, var(--padding, 2px))
23231
+ );
23232
+
22698
23233
  .navi_native_input {
22699
23234
  box-sizing: border-box;
22700
- padding-top: var(--padding-top, var(--padding-y, var(--padding, 1px)));
22701
- padding-right: var(
22702
- --padding-right,
22703
- var(--padding-x, var(--padding, 2px))
22704
- );
22705
- padding-bottom: var(
22706
- --padding-bottom,
22707
- var(--padding-y, var(--padding, 1px))
22708
- );
22709
- padding-left: var(--padding-left, var(--padding-x, var(--padding, 2px)));
23235
+ padding-top: var(--x-padding-top-base);
23236
+ padding-right: calc(var(--x-padding-right-base) + var(--end-icon-size));
23237
+ padding-bottom: var(--x-padding-bottom-base);
23238
+ padding-left: calc(var(--x-padding-left-base) + var(--start-icon-size));
22710
23239
  color: var(--x-color);
23240
+ font-size: var(--font-size);
22711
23241
  background-color: var(--x-background-color);
22712
23242
  border-width: var(--x-outer-width);
22713
23243
  border-width: var(--x-outer-width);
@@ -22732,17 +23262,19 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
22732
23262
  position: absolute;
22733
23263
  top: 0;
22734
23264
  bottom: 0;
22735
- left: 0.25em;
23265
+ left: var(--x-padding-left-base);
23266
+ font-size: var(--font-size);
22736
23267
  }
22737
23268
  .navi_input_end_button {
22738
23269
  position: absolute;
22739
23270
  top: 0;
22740
- right: 0.25em;
23271
+ right: var(--x-padding-right-base);
22741
23272
  bottom: 0;
22742
23273
  display: inline-flex;
22743
23274
  margin: 0;
22744
23275
  padding: 0;
22745
23276
  justify-content: center;
23277
+ font-size: var(--font-size);
22746
23278
  background: none;
22747
23279
  border: none;
22748
23280
  opacity: 0;
@@ -22768,17 +23300,48 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
22768
23300
  }
22769
23301
  }
22770
23302
  }
22771
-
22772
23303
  &[data-start-icon] {
22773
- .navi_native_input {
22774
- padding-left: 20px;
22775
- }
23304
+ --start-icon-size: 1em;
22776
23305
  }
22777
23306
  &[data-end-icon] {
23307
+ --end-icon-size: 1em;
23308
+ }
23309
+
23310
+ /* Readonly */
23311
+ &[data-readonly] {
23312
+ --x-border-color: var(--border-color-readonly);
23313
+ --x-background-color: var(--background-color-readonly);
23314
+ --x-color: var(--color-readonly);
23315
+ }
23316
+ /* Focus */
23317
+ &[data-focus],
23318
+ &[data-focus-visible] {
23319
+ --x-background-color: var(--background-color-focus);
23320
+ --x-border-color: var(--border-color-focus);
23321
+
22778
23322
  .navi_native_input {
22779
- padding-right: 20px;
23323
+ outline-width: var(--x-outer-width);
23324
+ outline-offset: calc(-1 * var(--x-outer-width));
23325
+ --x-border-color: var(--x-outline-color);
22780
23326
  }
22781
23327
  }
23328
+ /* Hover */
23329
+ &[data-hover] {
23330
+ --x-background-color: var(--background-color-hover);
23331
+ --x-border-color: var(--border-color-hover);
23332
+ --x-color: var(--color-hover);
23333
+ }
23334
+
23335
+ /* Disabled */
23336
+ &[data-disabled] {
23337
+ --x-border-color: var(--border-color-disabled);
23338
+ --x-background-color: var(--background-color-disabled);
23339
+ --x-color: var(--color-disabled);
23340
+ }
23341
+ /* Callout (info, warning, error) */
23342
+ &[data-callout] {
23343
+ --x-border-color: var(--callout-color);
23344
+ }
22782
23345
  }
22783
23346
 
22784
23347
  .navi_input .navi_native_input::placeholder {
@@ -22790,29 +23353,6 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
22790
23353
  /* Fortunately we can override it as follow */
22791
23354
  -webkit-text-fill-color: var(--x-color) !important;
22792
23355
  }
22793
- /* Readonly */
22794
- .navi_input[data-readonly] {
22795
- --x-border-color: var(--border-color-readonly);
22796
- --x-background-color: var(--background-color-readonly);
22797
- --x-color: var(--color-readonly);
22798
- }
22799
- /* Focus */
22800
- .navi_input[data-focus] .navi_native_input,
22801
- .navi_input[data-focus-visible] .navi_native_input {
22802
- outline-width: var(--x-outer-width);
22803
- outline-offset: calc(-1 * var(--x-outer-width));
22804
- --x-border-color: var(--x-outline-color);
22805
- }
22806
- /* Disabled */
22807
- .navi_input[data-disabled] {
22808
- --x-border-color: var(--border-color-disabled);
22809
- --x-background-color: var(--background-color-disabled);
22810
- --x-color: var(--color-disabled);
22811
- }
22812
- /* Callout (info, warning, error) */
22813
- .navi_input[data-callout] {
22814
- --x-border-color: var(--callout-color);
22815
- }
22816
23356
  `;
22817
23357
  const InputTextual = props => {
22818
23358
  const uiStateController = useUIStateController(props, "input");
@@ -22833,10 +23373,14 @@ const InputStyleCSSVars = {
22833
23373
  "outlineWidth": "--outline-width",
22834
23374
  "borderWidth": "--border-width",
22835
23375
  "borderRadius": "--border-radius",
23376
+ "padding": "--padding",
23377
+ "paddingX": "--padding-x",
23378
+ "paddingY": "--padding-y",
22836
23379
  "paddingTop": "--padding-top",
22837
23380
  "paddingRight": "--padding-right",
22838
23381
  "paddingBottom": "--padding-bottom",
22839
23382
  "paddingLeft": "--padding-left",
23383
+ "background": "--background",
22840
23384
  "backgroundColor": "--background-color",
22841
23385
  "borderColor": "--border-color",
22842
23386
  "color": "--color",
@@ -22845,7 +23389,12 @@ const InputStyleCSSVars = {
22845
23389
  borderColor: "--border-color-hover",
22846
23390
  color: "--color-hover"
22847
23391
  },
23392
+ ":focus": {
23393
+ backgroundColor: "--background-color-focus",
23394
+ borderColor: "--border-color-focus"
23395
+ },
22848
23396
  ":active": {
23397
+ backgroundColor: "--background-color-active",
22849
23398
  borderColor: "--border-color-active"
22850
23399
  },
22851
23400
  ":read-only": {
@@ -24355,24 +24904,6 @@ const createItemTracker = () => {
24355
24904
  return [useItemTrackerProvider, useTrackItem, useTrackedItem, useTrackedItems];
24356
24905
  };
24357
24906
 
24358
- const useFocusGroup = (
24359
- elementRef,
24360
- { enabled = true, direction, skipTab, loop, name } = {},
24361
- ) => {
24362
- useLayoutEffect(() => {
24363
- if (!enabled) {
24364
- return null;
24365
- }
24366
- const focusGroup = initFocusGroup(elementRef.current, {
24367
- direction,
24368
- skipTab,
24369
- loop,
24370
- name,
24371
- });
24372
- return focusGroup.cleanup;
24373
- }, [direction, skipTab, loop, name]);
24374
- };
24375
-
24376
24907
  const Z_INDEX_EDITING = 1; /* To go above neighbours, but should not be too big to stay under the sticky cells */
24377
24908
 
24378
24909
  /* needed because cell uses position:relative, sticky must win even if before in DOM order */
@@ -28136,454 +28667,103 @@ const CodeBox = ({
28136
28667
  ...props
28137
28668
  }) => {
28138
28669
  return jsx(Text, {
28139
- as: "pre",
28140
- ...props,
28141
- children: jsx(Text, {
28142
- as: "code",
28143
- children: children
28144
- })
28145
- });
28146
- };
28147
-
28148
- const Paragraph = props => {
28149
- return jsx(Text, {
28150
- marginTop: "md",
28151
- ...props,
28152
- as: "p",
28153
- ...props
28154
- });
28155
- };
28156
-
28157
- const Image = props => {
28158
- return jsx(Box, {
28159
- ...props,
28160
- as: "img"
28161
- });
28162
- };
28163
-
28164
- const Svg = props => {
28165
- return jsx(Box, {
28166
- ...props,
28167
- as: "svg"
28168
- });
28169
- };
28170
-
28171
- installImportMetaCss(import.meta);import.meta.css = /* css */`
28172
- .svg_mask_content * {
28173
- color: black !important;
28174
- opacity: 1 !important;
28175
- fill: black !important;
28176
- fill-opacity: 1 !important;
28177
- stroke: black !important;
28178
- stroke-opacity: 1 !important;
28179
- }
28180
- `;
28181
- const SVGMaskOverlay = ({
28182
- viewBox,
28183
- children
28184
- }) => {
28185
- if (!Array.isArray(children)) {
28186
- return children;
28187
- }
28188
- if (children.length === 1) {
28189
- return children[0];
28190
- }
28191
- if (!viewBox) {
28192
- console.error("SVGComposition requires an explicit viewBox");
28193
- return null;
28194
- }
28195
-
28196
- // First SVG is the base, all others are overlays
28197
- const [baseSvg, ...overlaySvgs] = children;
28198
-
28199
- // Generate unique ID for this instance
28200
- const instanceId = `svgmo-${Math.random().toString(36).slice(2, 9)}`;
28201
-
28202
- // Create nested masked elements
28203
- let maskedElement = baseSvg;
28204
-
28205
- // Apply each mask in sequence
28206
- overlaySvgs.forEach((overlaySvg, index) => {
28207
- const maskId = `mask-${instanceId}-${index}`;
28208
- maskedElement = jsx("g", {
28209
- mask: `url(#${maskId})`,
28210
- children: maskedElement
28211
- });
28212
- });
28213
- return jsxs("svg", {
28214
- viewBox: viewBox,
28215
- width: "100%",
28216
- height: "100%",
28217
- children: [jsx("defs", {
28218
- children: overlaySvgs.map((overlaySvg, index) => {
28219
- const maskId = `mask-${instanceId}-${index}`;
28220
-
28221
- // IMPORTANT: clone the overlay SVG exactly as is, just add the mask class
28222
- return jsxs("mask", {
28223
- id: maskId,
28224
- children: [jsx("rect", {
28225
- width: "100%",
28226
- height: "100%",
28227
- fill: "white"
28228
- }), cloneElement(overlaySvg, {
28229
- className: "svg_mask_content" // Apply styling to make it black
28230
- })]
28231
- }, maskId);
28232
- })
28233
- }), maskedElement, overlaySvgs]
28234
- });
28235
- };
28236
-
28237
- installImportMetaCss(import.meta);const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
28238
- const downArrowPath = "M480-280L160-600l80-80 240 240 240-240 80 80-320 320z";
28239
- import.meta.css = /* css */`
28240
- .summary_marker {
28241
- width: 1em;
28242
- height: 1em;
28243
- line-height: 1em;
28244
- }
28245
- .summary_marker_svg .arrow {
28246
- animation-duration: 0.3s;
28247
- animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
28248
- animation-fill-mode: forwards;
28249
- }
28250
- .summary_marker_svg .arrow[data-animation-target="down"] {
28251
- animation-name: morph-to-down;
28252
- }
28253
- @keyframes morph-to-down {
28254
- from {
28255
- d: path("${rightArrowPath}");
28256
- }
28257
- to {
28258
- d: path("${downArrowPath}");
28259
- }
28260
- }
28261
- .summary_marker_svg .arrow[data-animation-target="right"] {
28262
- animation-name: morph-to-right;
28263
- }
28264
- @keyframes morph-to-right {
28265
- from {
28266
- d: path("${downArrowPath}");
28267
- }
28268
- to {
28269
- d: path("${rightArrowPath}");
28270
- }
28271
- }
28272
-
28273
- .summary_marker_svg .foreground_circle {
28274
- stroke-dasharray: 503 1507; /* ~25% of circle perimeter */
28275
- stroke-dashoffset: 0;
28276
- animation: progress-around-circle 1.5s linear infinite;
28277
- }
28278
- @keyframes progress-around-circle {
28279
- 0% {
28280
- stroke-dashoffset: 0;
28281
- }
28282
- 100% {
28283
- stroke-dashoffset: -2010;
28284
- }
28285
- }
28286
-
28287
- /* fading and scaling */
28288
- .summary_marker_svg .arrow {
28289
- opacity: 1;
28290
- transition: opacity 0.3s ease-in-out;
28291
- }
28292
- .summary_marker_svg .loading_container {
28293
- transform: scale(0.3);
28294
- transition: transform 0.3s linear;
28295
- }
28296
- .summary_marker_svg .background_circle,
28297
- .summary_marker_svg .foreground_circle {
28298
- opacity: 0;
28299
- transition: opacity 0.3s ease-in-out;
28300
- }
28301
- .summary_marker_svg[data-loading] .arrow {
28302
- opacity: 0;
28303
- }
28304
- .summary_marker_svg[data-loading] .loading_container {
28305
- transform: scale(1);
28306
- }
28307
- .summary_marker_svg[data-loading] .background_circle {
28308
- opacity: 0.2;
28309
- }
28310
- .summary_marker_svg[data-loading] .foreground_circle {
28311
- opacity: 1;
28312
- }
28313
- `;
28314
- const SummaryMarker = ({
28315
- open,
28316
- loading
28317
- }) => {
28318
- const showLoading = useDebounceTrue(loading, 300);
28319
- const mountedRef = useRef(false);
28320
- const prevOpenRef = useRef(open);
28321
- useLayoutEffect(() => {
28322
- mountedRef.current = true;
28323
- return () => {
28324
- mountedRef.current = false;
28325
- };
28326
- }, []);
28327
- const shouldAnimate = mountedRef.current && prevOpenRef.current !== open;
28328
- prevOpenRef.current = open;
28329
- return jsx("span", {
28330
- className: "summary_marker",
28331
- children: jsxs("svg", {
28332
- className: "summary_marker_svg",
28333
- viewBox: "0 -960 960 960",
28334
- xmlns: "http://www.w3.org/2000/svg",
28335
- "data-loading": open ? showLoading || undefined : undefined,
28336
- children: [jsxs("g", {
28337
- className: "loading_container",
28338
- "transform-origin": "480px -480px",
28339
- children: [jsx("circle", {
28340
- className: "background_circle",
28341
- cx: "480",
28342
- cy: "-480",
28343
- r: "320",
28344
- stroke: "currentColor",
28345
- fill: "none",
28346
- strokeWidth: "60",
28347
- opacity: "0.2"
28348
- }), jsx("circle", {
28349
- className: "foreground_circle",
28350
- cx: "480",
28351
- cy: "-480",
28352
- r: "320",
28353
- stroke: "currentColor",
28354
- fill: "none",
28355
- strokeWidth: "60",
28356
- strokeLinecap: "round",
28357
- strokeDasharray: "503 1507"
28358
- })]
28359
- }), jsx("g", {
28360
- className: "arrow_container",
28361
- "transform-origin": "480px -480px",
28362
- children: jsx("path", {
28363
- className: "arrow",
28364
- fill: "currentColor",
28365
- "data-animation-target": shouldAnimate ? open ? "down" : "right" : undefined,
28366
- d: open ? downArrowPath : rightArrowPath
28367
- })
28368
- })]
28670
+ as: "pre",
28671
+ ...props,
28672
+ children: jsx(Text, {
28673
+ as: "code",
28674
+ children: children
28369
28675
  })
28370
28676
  });
28371
28677
  };
28372
28678
 
28679
+ const Paragraph = props => {
28680
+ return jsx(Text, {
28681
+ marginTop: "md",
28682
+ ...props,
28683
+ as: "p",
28684
+ ...props
28685
+ });
28686
+ };
28687
+
28688
+ const Image = props => {
28689
+ return jsx(Box, {
28690
+ ...props,
28691
+ as: "img"
28692
+ });
28693
+ };
28694
+
28695
+ const Svg = props => {
28696
+ return jsx(Box, {
28697
+ ...props,
28698
+ as: "svg"
28699
+ });
28700
+ };
28701
+
28373
28702
  installImportMetaCss(import.meta);import.meta.css = /* css */`
28374
- .navi_details {
28375
- position: relative;
28376
- z-index: 1;
28377
- display: flex;
28378
- flex-shrink: 0;
28379
- flex-direction: column;
28703
+ .svg_mask_content * {
28704
+ color: black !important;
28705
+ opacity: 1 !important;
28706
+ fill: black !important;
28707
+ fill-opacity: 1 !important;
28708
+ stroke: black !important;
28709
+ stroke-opacity: 1 !important;
28380
28710
  }
28381
-
28382
- .navi_details > summary {
28383
- display: flex;
28384
- flex-shrink: 0;
28385
- flex-direction: column;
28386
- cursor: pointer;
28387
- user-select: none;
28711
+ `;
28712
+ const SVGMaskOverlay = ({
28713
+ viewBox,
28714
+ children
28715
+ }) => {
28716
+ if (!Array.isArray(children)) {
28717
+ return children;
28388
28718
  }
28389
- .summary_body {
28390
- display: flex;
28391
- width: 100%;
28392
- flex-direction: row;
28393
- align-items: center;
28394
- gap: 0.2em;
28719
+ if (children.length === 1) {
28720
+ return children[0];
28395
28721
  }
28396
- .summary_label {
28397
- display: flex;
28398
- padding-right: 10px;
28399
- flex: 1;
28400
- align-items: center;
28401
- gap: 0.2em;
28722
+ if (!viewBox) {
28723
+ console.error("SVGComposition requires an explicit viewBox");
28724
+ return null;
28402
28725
  }
28403
28726
 
28404
- .navi_details > summary:focus {
28405
- z-index: 1;
28406
- }
28407
- `;
28408
- const Details = forwardRef((props, ref) => {
28409
- return renderActionableComponent(props, ref);
28410
- });
28411
- const DetailsBasic = forwardRef((props, ref) => {
28412
- const {
28413
- id,
28414
- label = "Summary",
28415
- open,
28416
- loading,
28417
- className,
28418
- focusGroup,
28419
- focusGroupDirection,
28420
- arrowKeyShortcuts = true,
28421
- openKeyShortcut = "ArrowRight",
28422
- closeKeyShortcut = "ArrowLeft",
28423
- onToggle,
28424
- children,
28425
- ...rest
28426
- } = props;
28427
- const innerRef = useRef();
28428
- useImperativeHandle(ref, () => innerRef.current);
28429
- const [navState, setNavState] = useNavState$1(id);
28430
- const [innerOpen, innerOpenSetter] = useState(open || navState);
28431
- useFocusGroup(innerRef, {
28432
- enabled: focusGroup,
28433
- name: typeof focusGroup === "string" ? focusGroup : undefined,
28434
- direction: focusGroupDirection
28435
- });
28727
+ // First SVG is the base, all others are overlays
28728
+ const [baseSvg, ...overlaySvgs] = children;
28436
28729
 
28437
- /**
28438
- * Browser will dispatch "toggle" event even if we set open={true}
28439
- * When rendering the component for the first time
28440
- * We have to ensure the initial "toggle" event is ignored.
28441
- *
28442
- * If we don't do that code will think the details has changed and run logic accordingly
28443
- * For example it will try to navigate to the current url while we are already there
28444
- *
28445
- * See:
28446
- * - https://techblog.thescore.com/2024/10/08/why-we-decided-to-change-how-the-details-element-works/
28447
- * - https://github.com/whatwg/html/issues/4500
28448
- * - https://stackoverflow.com/questions/58942600/react-html-details-toggles-uncontrollably-when-starts-open
28449
- *
28450
- */
28730
+ // Generate unique ID for this instance
28731
+ const instanceId = `svgmo-${Math.random().toString(36).slice(2, 9)}`;
28451
28732
 
28452
- const summaryRef = useRef(null);
28453
- useKeyboardShortcuts(innerRef, [{
28454
- key: openKeyShortcut,
28455
- enabled: arrowKeyShortcuts,
28456
- when: e => document.activeElement === summaryRef.current &&
28457
- // avoid handling openKeyShortcut twice when keydown occurs inside nested details
28458
- !e.defaultPrevented,
28459
- action: e => {
28460
- const details = innerRef.current;
28461
- if (!details.open) {
28462
- e.preventDefault();
28463
- details.open = true;
28464
- return;
28465
- }
28466
- const summary = summaryRef.current;
28467
- const firstFocusableElementInDetails = findAfter(summary, elementIsFocusable, {
28468
- root: details
28469
- });
28470
- if (!firstFocusableElementInDetails) {
28471
- return;
28472
- }
28473
- e.preventDefault();
28474
- firstFocusableElementInDetails.focus();
28475
- }
28476
- }, {
28477
- key: closeKeyShortcut,
28478
- enabled: arrowKeyShortcuts,
28479
- when: () => {
28480
- const details = innerRef.current;
28481
- return details.open;
28482
- },
28483
- action: e => {
28484
- const details = innerRef.current;
28485
- const summary = summaryRef.current;
28486
- if (document.activeElement === summary) {
28487
- e.preventDefault();
28488
- summary.focus();
28489
- details.open = false;
28490
- } else {
28491
- e.preventDefault();
28492
- summary.focus();
28493
- }
28494
- }
28495
- }]);
28496
- const mountedRef = useRef(false);
28497
- useEffect(() => {
28498
- mountedRef.current = true;
28499
- }, []);
28500
- return jsxs("details", {
28501
- ...rest,
28502
- ref: innerRef,
28503
- id: id,
28504
- className: ["navi_details", ...(className ? className.split(" ") : [])].join(" "),
28505
- onToggle: e => {
28506
- const isOpen = e.newState === "open";
28507
- if (mountedRef.current) {
28508
- if (isOpen) {
28509
- innerOpenSetter(true);
28510
- setNavState(true);
28511
- } else {
28512
- innerOpenSetter(false);
28513
- setNavState(undefined);
28514
- }
28515
- }
28516
- onToggle?.(e);
28517
- },
28518
- open: innerOpen,
28519
- children: [jsx("summary", {
28520
- ref: summaryRef,
28521
- children: jsxs("div", {
28522
- className: "summary_body",
28523
- children: [jsx(SummaryMarker, {
28524
- open: innerOpen,
28525
- loading: loading
28526
- }), jsx("div", {
28527
- className: "summary_label",
28528
- children: label
28529
- })]
28530
- })
28531
- }), children]
28532
- });
28533
- });
28534
- forwardRef((props, ref) => {
28535
- const {
28536
- action,
28537
- loading,
28538
- onToggle,
28539
- onActionPrevented,
28540
- onActionStart,
28541
- onActionError,
28542
- onActionEnd,
28543
- children,
28544
- ...rest
28545
- } = props;
28546
- const innerRef = useRef();
28547
- useImperativeHandle(ref, () => innerRef.current);
28548
- const effectiveAction = useAction(action);
28549
- const {
28550
- loading: actionLoading
28551
- } = useActionStatus(effectiveAction);
28552
- const executeAction = useExecuteAction(innerRef, {
28553
- // the error will be displayed by actionRenderer inside <details>
28554
- errorEffect: "none"
28555
- });
28556
- useActionEvents(innerRef, {
28557
- onPrevented: onActionPrevented,
28558
- onAction: e => {
28559
- executeAction(e);
28560
- },
28561
- onStart: onActionStart,
28562
- onError: onActionError,
28563
- onEnd: onActionEnd
28733
+ // Create nested masked elements
28734
+ let maskedElement = baseSvg;
28735
+
28736
+ // Apply each mask in sequence
28737
+ overlaySvgs.forEach((overlaySvg, index) => {
28738
+ const maskId = `mask-${instanceId}-${index}`;
28739
+ maskedElement = jsx("g", {
28740
+ mask: `url(#${maskId})`,
28741
+ children: maskedElement
28742
+ });
28564
28743
  });
28565
- return jsx(DetailsBasic, {
28566
- ...rest,
28567
- ref: innerRef,
28568
- loading: loading || actionLoading,
28569
- onToggle: toggleEvent => {
28570
- const isOpen = toggleEvent.newState === "open";
28571
- if (isOpen) {
28572
- requestAction(toggleEvent.target, effectiveAction, {
28573
- event: toggleEvent,
28574
- method: "run"
28575
- });
28576
- } else {
28577
- effectiveAction.abort();
28578
- }
28579
- onToggle?.(toggleEvent);
28580
- },
28581
- children: jsx(ActionRenderer, {
28582
- action: effectiveAction,
28583
- children: children
28584
- })
28744
+ return jsxs("svg", {
28745
+ viewBox: viewBox,
28746
+ width: "100%",
28747
+ height: "100%",
28748
+ children: [jsx("defs", {
28749
+ children: overlaySvgs.map((overlaySvg, index) => {
28750
+ const maskId = `mask-${instanceId}-${index}`;
28751
+
28752
+ // IMPORTANT: clone the overlay SVG exactly as is, just add the mask class
28753
+ return jsxs("mask", {
28754
+ id: maskId,
28755
+ children: [jsx("rect", {
28756
+ width: "100%",
28757
+ height: "100%",
28758
+ fill: "white"
28759
+ }), cloneElement(overlaySvg, {
28760
+ className: "svg_mask_content" // Apply styling to make it black
28761
+ })]
28762
+ }, maskId);
28763
+ })
28764
+ }), maskedElement, overlaySvgs]
28585
28765
  });
28586
- });
28766
+ };
28587
28767
 
28588
28768
  installImportMetaCss(import.meta);import.meta.css = /* css */`
28589
28769
  @layer navi {