@jsenv/navi 0.29.88 → 0.29.90
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.
- package/dist/jsenv_navi.js +309 -57
- package/dist/jsenv_navi.js.map +21 -8
- package/docs/AI_INSTRUCTIONS.md +15 -0
- package/docs/control_object.md +38 -1
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -24381,6 +24381,13 @@ const GROUP_DEFAULTS = {
|
|
|
24381
24381
|
? emptyUIState
|
|
24382
24382
|
: child.uiState;
|
|
24383
24383
|
if (!name) {
|
|
24384
|
+
if (allowNameless) {
|
|
24385
|
+
// A control that says it is not a field is not one, whatever it
|
|
24386
|
+
// holds: a picker used as a door holds the shape its popup draws,
|
|
24387
|
+
// and merging that in would put the popup's keys in the object as
|
|
24388
|
+
// if the door had been a group.
|
|
24389
|
+
continue;
|
|
24390
|
+
}
|
|
24384
24391
|
// A nameless GROUP is a grouping, not a value: it exists to hold its
|
|
24385
24392
|
// children together (a WheelGroup sharing navigation, a fieldset-ish
|
|
24386
24393
|
// cluster) without claiming a key of its own, so what it holds is
|
|
@@ -24390,12 +24397,10 @@ const GROUP_DEFAULTS = {
|
|
|
24390
24397
|
Object.assign(groupValues, uiState);
|
|
24391
24398
|
continue;
|
|
24392
24399
|
}
|
|
24393
|
-
|
|
24394
|
-
|
|
24395
|
-
|
|
24396
|
-
|
|
24397
|
-
);
|
|
24398
|
-
}
|
|
24400
|
+
console.warn(
|
|
24401
|
+
"A group child is missing a name property, its state won't be included in the group state",
|
|
24402
|
+
child,
|
|
24403
|
+
);
|
|
24399
24404
|
continue;
|
|
24400
24405
|
}
|
|
24401
24406
|
groupValues[name] = uiState;
|
|
@@ -24520,6 +24525,14 @@ const useUIGroupStateController = (
|
|
|
24520
24525
|
: stateType === "object"
|
|
24521
24526
|
? EMPTY_OBJECT
|
|
24522
24527
|
: undefined;
|
|
24528
|
+
// A group told what it holds holds it from the start, before any child has
|
|
24529
|
+
// registered to show it: what it was given is the answer, and the children
|
|
24530
|
+
// are where that answer is shown (see stateGivenFromAbove).
|
|
24531
|
+
const stateInitial = hasValueProp
|
|
24532
|
+
? value
|
|
24533
|
+
: hasDefaultValueProp && defaultValue !== undefined
|
|
24534
|
+
? defaultValue
|
|
24535
|
+
: fallbackState;
|
|
24523
24536
|
const childUIStateControllerArrayRef = useRef([]);
|
|
24524
24537
|
const childUIStateControllerArray = childUIStateControllerArrayRef.current;
|
|
24525
24538
|
// Tracks children rejected by the filter and delegated upward (bubble-up).
|
|
@@ -24583,14 +24596,35 @@ const useUIGroupStateController = (
|
|
|
24583
24596
|
`Creating "${controlType}" ui state controller (monitoring some descendants ui state(s))"`,
|
|
24584
24597
|
);
|
|
24585
24598
|
const [publishUIState, subscribeUIState] = createPubSub();
|
|
24586
|
-
const uiStateSignal = signal(
|
|
24587
|
-
|
|
24588
|
-
|
|
24599
|
+
const uiStateSignal = signal(stateInitial);
|
|
24600
|
+
|
|
24601
|
+
// What the group is worth right now, and what it keeps when there is
|
|
24602
|
+
// nobody to ask: a list whose items have not arrived yet, a popup built
|
|
24603
|
+
// at open, a group whose children are still mounting. Such a group has
|
|
24604
|
+
// no opinion — its aggregate falls back to the empty of its type, and
|
|
24605
|
+
// taking that for an answer is how a value handed to it evaporates on
|
|
24606
|
+
// the way in, and how that emptiness then travels back up to whoever
|
|
24607
|
+
// handed it (a picker showing its row as unanswered).
|
|
24608
|
+
const aggregateGroupUIState = (whenNobodyCanAnswer) => {
|
|
24609
|
+
const someChildCanAnswer = childUIStateControllerArray.some(
|
|
24610
|
+
shouldPropagateStateToChild,
|
|
24611
|
+
);
|
|
24612
|
+
if (!someChildCanAnswer) {
|
|
24613
|
+
return whenNobodyCanAnswer;
|
|
24614
|
+
}
|
|
24589
24615
|
const aggChildState = resolvedAggregateChildStates(
|
|
24590
24616
|
childUIStateControllerArray,
|
|
24591
24617
|
fallbackState,
|
|
24592
24618
|
);
|
|
24593
|
-
|
|
24619
|
+
if (aggChildState !== undefined) {
|
|
24620
|
+
return aggChildState;
|
|
24621
|
+
}
|
|
24622
|
+
// A group with an aggregate of its own is the one who knows what its
|
|
24623
|
+
// children add up to, `undefined` included — half a time is not a time,
|
|
24624
|
+
// wheels nobody turned have settled nothing. Only the default shapes
|
|
24625
|
+
// fall back to the empty of their type, where "no child says anything"
|
|
24626
|
+
// and "the value is empty" are the same sentence.
|
|
24627
|
+
return stateShapeIsTheDefaultOne ? fallbackState : undefined;
|
|
24594
24628
|
};
|
|
24595
24629
|
|
|
24596
24630
|
// onChange and applyState live inside init so they close over the stable
|
|
@@ -24611,13 +24645,26 @@ const useUIGroupStateController = (
|
|
|
24611
24645
|
};
|
|
24612
24646
|
return;
|
|
24613
24647
|
}
|
|
24614
|
-
const
|
|
24648
|
+
const { controller } = s;
|
|
24649
|
+
// A child mounting or unmounting is not somebody answering: while the
|
|
24650
|
+
// children of a group are still arriving, their aggregate is a partial
|
|
24651
|
+
// reading, and taking it for the truth is how the value the group was
|
|
24652
|
+
// given gets destroyed one row at a time — the first row to register
|
|
24653
|
+
// aggregates alone, the group drops to that, and every row after it is
|
|
24654
|
+
// placed from what is left. A group that derived its own value has
|
|
24655
|
+
// nothing to protect and aggregates as usual.
|
|
24656
|
+
const groupUIState =
|
|
24657
|
+
notifyExternal === "silent" && controller.stateGivenFromAbove
|
|
24658
|
+
? controller.uiState
|
|
24659
|
+
: aggregateGroupUIState(controller.uiState);
|
|
24615
24660
|
debugUIGroup(
|
|
24616
24661
|
e,
|
|
24617
24662
|
`${controlType}.getUIState -> ${JSON.stringify(groupUIState)}`,
|
|
24618
24663
|
);
|
|
24619
|
-
const { controller } = s;
|
|
24620
24664
|
if (notifyExternal === true) {
|
|
24665
|
+
// Somebody answered: what the group is worth is what its children say
|
|
24666
|
+
// between them, from here on.
|
|
24667
|
+
controller.stateGivenFromAbove = false;
|
|
24621
24668
|
applyState(groupUIState, e);
|
|
24622
24669
|
} else if (notifyExternal === "silent") {
|
|
24623
24670
|
controller.syncInternalState(groupUIState);
|
|
@@ -24683,7 +24730,11 @@ const useUIGroupStateController = (
|
|
|
24683
24730
|
hasValueProp,
|
|
24684
24731
|
hasDefaultValueProp,
|
|
24685
24732
|
props,
|
|
24686
|
-
uiState:
|
|
24733
|
+
uiState: stateInitial,
|
|
24734
|
+
// Whether what the group holds was HANDED to it (a parent distributing,
|
|
24735
|
+
// a picker filling its popup, a value prop) rather than worked out from
|
|
24736
|
+
// its children. What it protects is read in onChange.
|
|
24737
|
+
stateGivenFromAbove: hasValueProp || hasDefaultValueProp,
|
|
24687
24738
|
uiStateSignal,
|
|
24688
24739
|
wantRequesterButtonState,
|
|
24689
24740
|
ref,
|
|
@@ -24696,9 +24747,6 @@ const useUIGroupStateController = (
|
|
|
24696
24747
|
if (!shouldPropagateStateToChild(childUIStateController)) {
|
|
24697
24748
|
return;
|
|
24698
24749
|
}
|
|
24699
|
-
if (childUIStateController.hasStateProp) {
|
|
24700
|
-
return;
|
|
24701
|
-
}
|
|
24702
24750
|
const childNewState = resolvedDistributeChildUIState(
|
|
24703
24751
|
groupUIState,
|
|
24704
24752
|
childUIStateController,
|
|
@@ -24706,6 +24754,22 @@ const useUIGroupStateController = (
|
|
|
24706
24754
|
if (childNewState === CANNOT_DERIVE) {
|
|
24707
24755
|
return;
|
|
24708
24756
|
}
|
|
24757
|
+
if (
|
|
24758
|
+
childUIStateController.hasStateProp &&
|
|
24759
|
+
!childUIStateController.props.signal
|
|
24760
|
+
) {
|
|
24761
|
+
// A child bound to a signal is placed like any other: bound is not
|
|
24762
|
+
// frozen, and the placement writes the signal, so both ends keep
|
|
24763
|
+
// saying the same thing. Only a child controlled by a `value` /
|
|
24764
|
+
// `checked` prop cannot be moved — its owner decides. Worth saying
|
|
24765
|
+
// out loud only when the two disagree: a child already showing what
|
|
24766
|
+
// the group would put there has lost nothing, and both being fed
|
|
24767
|
+
// from the same value is a legitimate way to write a group.
|
|
24768
|
+
if (
|
|
24769
|
+
!compareTwoJsValues(childNewState, childUIStateController.uiState)
|
|
24770
|
+
) ;
|
|
24771
|
+
return;
|
|
24772
|
+
}
|
|
24709
24773
|
childUIStateController.setUIState(childNewState, e);
|
|
24710
24774
|
},
|
|
24711
24775
|
setUIState: (newUIState, e) => {
|
|
@@ -24731,6 +24795,7 @@ const useUIGroupStateController = (
|
|
|
24731
24795
|
);
|
|
24732
24796
|
return;
|
|
24733
24797
|
}
|
|
24798
|
+
controller.stateGivenFromAbove = true;
|
|
24734
24799
|
const propagateEventType =
|
|
24735
24800
|
e.type === "initial_state_push"
|
|
24736
24801
|
? "initial_state_push"
|
|
@@ -24746,9 +24811,10 @@ const useUIGroupStateController = (
|
|
|
24746
24811
|
propagateDownEvent,
|
|
24747
24812
|
);
|
|
24748
24813
|
}
|
|
24749
|
-
const groupUIState = aggregateGroupUIState();
|
|
24814
|
+
const groupUIState = aggregateGroupUIState(newUIState);
|
|
24750
24815
|
if (e.type === "initial_state_push") {
|
|
24751
24816
|
controller.syncInternalState(groupUIState);
|
|
24817
|
+
writeBoundSignal(groupUIState);
|
|
24752
24818
|
return;
|
|
24753
24819
|
}
|
|
24754
24820
|
applyState(groupUIState, e, { internalBehavior: true });
|
|
@@ -24800,15 +24866,28 @@ const useUIGroupStateController = (
|
|
|
24800
24866
|
debugUIGroup(
|
|
24801
24867
|
`${controlType}.registerChild("${childControlType}") -> registered (total: ${childUIStateControllerArray.length})`,
|
|
24802
24868
|
);
|
|
24803
|
-
|
|
24869
|
+
const stateToPlaceChildFrom = controller.hasValueProp
|
|
24870
|
+
? controller.value
|
|
24871
|
+
: controller.hasDefaultValueProp
|
|
24872
|
+
? controller.defaultValue
|
|
24873
|
+
: // What the group HOLDS, for a child arriving after the value
|
|
24874
|
+
// did: a list item loaded later, a row scrolled back into a
|
|
24875
|
+
// virtualized list, a popup built at open. Two conditions, and
|
|
24876
|
+
// both are about not overwriting an answer with a silence — the
|
|
24877
|
+
// group must actually hold something, and the child must have
|
|
24878
|
+
// nothing of its own to show (one arriving with its own default
|
|
24879
|
+
// is answering, and the group is what its answers add up to).
|
|
24880
|
+
uiStateHoldsNothing(controller.uiState) ||
|
|
24881
|
+
!uiStateHoldsNothing(childUIStateController.uiState)
|
|
24882
|
+
? undefined
|
|
24883
|
+
: controller.uiState;
|
|
24884
|
+
if (stateToPlaceChildFrom !== undefined) {
|
|
24804
24885
|
const initialEvent = new CustomEvent("initial_state_push", {
|
|
24805
24886
|
detail: {},
|
|
24806
24887
|
});
|
|
24807
24888
|
controller.placeChildUIState(
|
|
24808
24889
|
childUIStateController,
|
|
24809
|
-
|
|
24810
|
-
? controller.value
|
|
24811
|
-
: controller.defaultValue,
|
|
24890
|
+
stateToPlaceChildFrom,
|
|
24812
24891
|
initialEvent,
|
|
24813
24892
|
);
|
|
24814
24893
|
}
|
|
@@ -25097,6 +25176,7 @@ const EMPTY_OBJECT = {};
|
|
|
25097
25176
|
*/
|
|
25098
25177
|
const useUIFacadeStateController = (props, realUIStateController) => {
|
|
25099
25178
|
const firstChildControllerRef = useRef(null);
|
|
25179
|
+
const namelessChildSetRef = useRef(new Set());
|
|
25100
25180
|
const updatingRef = useRef(false);
|
|
25101
25181
|
const debugPopup = useDebugPopup();
|
|
25102
25182
|
const debugInteraction = useDebugInteraction();
|
|
@@ -25128,6 +25208,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
25128
25208
|
// A control saying it is not a field is not the one the picker talks
|
|
25129
25209
|
// to: the search box above the list, the "select all" switch beside
|
|
25130
25210
|
// it. It is there to help find the answer, not to be it.
|
|
25211
|
+
namelessChildSetRef.current.add(childController);
|
|
25131
25212
|
return false;
|
|
25132
25213
|
}
|
|
25133
25214
|
if (childController.props["navi-list"]) {
|
|
@@ -25150,6 +25231,10 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
25150
25231
|
}
|
|
25151
25232
|
const child = firstChildControllerRef.current;
|
|
25152
25233
|
if (!child) {
|
|
25234
|
+
warnPopupHasNothingButNamelessControls(
|
|
25235
|
+
props,
|
|
25236
|
+
namelessChildSetRef.current,
|
|
25237
|
+
);
|
|
25153
25238
|
return;
|
|
25154
25239
|
}
|
|
25155
25240
|
updatingRef.current = true;
|
|
@@ -25298,6 +25383,11 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
25298
25383
|
|
|
25299
25384
|
const describePicker = (props) =>
|
|
25300
25385
|
`<Picker${props.name ? ` name="${props.name}"` : ""}${props.type ? ` type="${props.type}"` : ""}>`;
|
|
25386
|
+
const warnPopupHasNothingButNamelessControls = (props, namelessChildSet) => {
|
|
25387
|
+
{
|
|
25388
|
+
return;
|
|
25389
|
+
}
|
|
25390
|
+
};
|
|
25301
25391
|
|
|
25302
25392
|
/**
|
|
25303
25393
|
* Returns true when `e` should trigger parent notification (child → parent bubbling).
|
|
@@ -25370,6 +25460,11 @@ const PROPAGATE_DOWN_EVENT_SET = new Set([
|
|
|
25370
25460
|
"propagate_down_set_ui_state",
|
|
25371
25461
|
"propagate_down_reset_ui_state",
|
|
25372
25462
|
"propagate_down_clear_ui_state",
|
|
25463
|
+
// The FIRST value handed down is one too: a control placed as it registers
|
|
25464
|
+
// (a group filling a child that just arrived, a picker filling its popup)
|
|
25465
|
+
// holds it from that moment, and a signal that kept saying nothing would have
|
|
25466
|
+
// the app and the screen disagree from the very first paint.
|
|
25467
|
+
"initial_state_push",
|
|
25373
25468
|
]);
|
|
25374
25469
|
const isPropagateDownEvent = (e) => {
|
|
25375
25470
|
return PROPAGATE_DOWN_EVENT_SET.has(e.type);
|
|
@@ -28015,6 +28110,7 @@ const createOpenController = (
|
|
|
28015
28110
|
// Last: the close effects above are what starts the exit transition the
|
|
28016
28111
|
// content must outlive (see popup_content_mount.js).
|
|
28017
28112
|
controller.unmountContent?.();
|
|
28113
|
+
controller.onOpenedChange?.(false);
|
|
28018
28114
|
};
|
|
28019
28115
|
const controller = {
|
|
28020
28116
|
opened: false,
|
|
@@ -28033,6 +28129,11 @@ const createOpenController = (
|
|
|
28033
28129
|
// The counterpart, set only when the popup was told to throw its content
|
|
28034
28130
|
// away on close (`unmountWhenClosed`). Called from performClose above.
|
|
28035
28131
|
unmountContent: null,
|
|
28132
|
+
// Told whenever `opened` actually changes, whatever asked for it — an
|
|
28133
|
+
// interaction, a command, a prop. What lets a `signal` prop reflect the
|
|
28134
|
+
// popup's real state (see useOpenPropsEffectOnOpenController), called once
|
|
28135
|
+
// the open/close has fully happened rather than mid-sequence.
|
|
28136
|
+
onOpenedChange: null,
|
|
28036
28137
|
open: (e, detail) => {
|
|
28037
28138
|
if (controller.opened || !controller.openEffect) {
|
|
28038
28139
|
return;
|
|
@@ -28135,6 +28236,7 @@ const createOpenController = (
|
|
|
28135
28236
|
openEffectReturnValue?.(closeEvent);
|
|
28136
28237
|
};
|
|
28137
28238
|
closeHandlers = openHandler(requestOpenEvent) || null;
|
|
28239
|
+
controller.onOpenedChange?.(true);
|
|
28138
28240
|
},
|
|
28139
28241
|
requestClose: (
|
|
28140
28242
|
e = new CustomEvent("programmatic", { detail: {} }),
|
|
@@ -28263,10 +28365,23 @@ const scheduleMountOpen = (run) => {
|
|
|
28263
28365
|
* `requestOpen`/`requestClose` wrappers).
|
|
28264
28366
|
*
|
|
28265
28367
|
* @param {{ open: (e: Event, detail?: object) => void, requestClose: (e: Event, detail?: object) => void, opened: boolean }} openController
|
|
28266
|
-
* @param {{ open?: boolean|"interaction", defaultOpen?: boolean|"interaction" }} props
|
|
28368
|
+
* @param {{ open?: boolean|"interaction", defaultOpen?: boolean|"interaction", signal?: import("@preact/signals").Signal<boolean> }} props
|
|
28267
28369
|
*/
|
|
28268
28370
|
const useOpenPropsEffectOnOpenController = (openController, props) => {
|
|
28269
|
-
const {
|
|
28371
|
+
const { signal, defaultOpen } = props;
|
|
28372
|
+
// What the caller holds, however they hold it: an `open` they re-render
|
|
28373
|
+
// themselves, or a `signal` this hook also writes (see onOpenedChange below).
|
|
28374
|
+
// Reading .value during render is what subscribes the popup to it.
|
|
28375
|
+
const open = signal ? signal.value : props.open;
|
|
28376
|
+
// Assigned on every render, like openEffect, so it always closes over the
|
|
28377
|
+
// latest prop: a popup that opens or closes on its own (Escape, backdrop, a
|
|
28378
|
+
// --navi-close command) writes what happened into the signal, so whoever
|
|
28379
|
+
// holds it always reads where the popup is.
|
|
28380
|
+
openController.onOpenedChange = signal
|
|
28381
|
+
? (opened) => {
|
|
28382
|
+
signal.value = opened;
|
|
28383
|
+
}
|
|
28384
|
+
: null;
|
|
28270
28385
|
// Tracks whether the effect below has ever run before — only the very
|
|
28271
28386
|
// first run gets the "mount already open" treatment (`open` truthy from
|
|
28272
28387
|
// the start, or the uncontrolled, mount-only `defaultOpen`); every
|
|
@@ -28319,6 +28434,12 @@ const useOpenPropsEffectOnOpenController = (openController, props) => {
|
|
|
28319
28434
|
{ isCancel: true },
|
|
28320
28435
|
);
|
|
28321
28436
|
}
|
|
28437
|
+
if (signal) {
|
|
28438
|
+
// The request can be refused (a busy form denying the close): the popup
|
|
28439
|
+
// then stays where it was, and the signal is told so — otherwise it
|
|
28440
|
+
// would keep saying "closed" about a popup still open.
|
|
28441
|
+
signal.value = openController.opened;
|
|
28442
|
+
}
|
|
28322
28443
|
}, [open]);
|
|
28323
28444
|
};
|
|
28324
28445
|
|
|
@@ -29743,6 +29864,14 @@ const css$X = /* css */`
|
|
|
29743
29864
|
* the focus only leaves it for something that asked by name (`autoFocus` on
|
|
29744
29865
|
* that element, which outranks whatever the dialog says).
|
|
29745
29866
|
* @param {boolean} [props.open] - Controlled open state.
|
|
29867
|
+
* @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
|
|
29868
|
+
* state said the way every navi control says it: the dialog opens and closes
|
|
29869
|
+
* to match the signal, and writes into it whenever it opens or closes on its
|
|
29870
|
+
* own (Escape, backdrop, a --navi-close command) — one binding to both drive
|
|
29871
|
+
* the dialog and know where it is, and the state stays where the app put it.
|
|
29872
|
+
* Excludes `open`; `onOpen`/`onClose` still fire. A signal holding `true` at
|
|
29873
|
+
* mount behaves like `defaultOpen`: the dialog was already open, no entrance
|
|
29874
|
+
* plays.
|
|
29746
29875
|
* @param {boolean|"interaction"} [props.defaultOpen] - Uncontrolled, mount-only
|
|
29747
29876
|
* initial open state. `true` plays no entrance animation: the dialog was
|
|
29748
29877
|
* already open when the page appeared, and nothing was ever shown as "closed"
|
|
@@ -29825,6 +29954,7 @@ const UncontrolledDialog = props => {
|
|
|
29825
29954
|
return jsx(ControlledDialog, {
|
|
29826
29955
|
...props,
|
|
29827
29956
|
open: undefined,
|
|
29957
|
+
signal: undefined,
|
|
29828
29958
|
defaultOpen: undefined,
|
|
29829
29959
|
onClose: undefined,
|
|
29830
29960
|
openController: openController,
|
|
@@ -31182,6 +31312,14 @@ const css$W = /* css */`
|
|
|
31182
31312
|
* the focus only leaves it for something that asked by name (`autoFocus` on
|
|
31183
31313
|
* that element, which outranks whatever the popover says).
|
|
31184
31314
|
* @param {boolean} [props.open] - Controlled open state.
|
|
31315
|
+
* @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
|
|
31316
|
+
* state said the way every navi control says it: the popover opens and closes
|
|
31317
|
+
* to match the signal, and writes into it whenever it opens or closes on its
|
|
31318
|
+
* own (Escape, light dismiss, a --navi-close command) — one binding to both
|
|
31319
|
+
* drive the popover and know where it is, and the state stays where the app
|
|
31320
|
+
* put it. Excludes `open`; `onOpen`/`onClose` still fire. A signal holding
|
|
31321
|
+
* `true` at mount behaves like `defaultOpen`: the popover was already open,
|
|
31322
|
+
* no entrance plays.
|
|
31185
31323
|
* @param {boolean|"interaction"} [props.defaultOpen] - Uncontrolled, mount-only
|
|
31186
31324
|
* initial open state. `true` plays no entrance animation: the popover was
|
|
31187
31325
|
* already open when the page appeared, and nothing was ever shown as "closed"
|
|
@@ -31262,6 +31400,7 @@ const UncontrolledPopover = props => {
|
|
|
31262
31400
|
return jsx(ControlledPopover, {
|
|
31263
31401
|
...props,
|
|
31264
31402
|
open: undefined,
|
|
31403
|
+
signal: undefined,
|
|
31265
31404
|
defaultOpen: undefined,
|
|
31266
31405
|
onClose: undefined,
|
|
31267
31406
|
openController: openController,
|
|
@@ -54086,6 +54225,9 @@ const css$A = /* css */`
|
|
|
54086
54225
|
* to `--dialog-maxmax-height` (`data-expand-y`).
|
|
54087
54226
|
* @param {boolean} [props.scrollCapture] - Forwarded as-is.
|
|
54088
54227
|
* @param {boolean} [props.open] - Forwarded as-is (controlled).
|
|
54228
|
+
* @param {import("@preact/signals").Signal<boolean>} [props.signal] -
|
|
54229
|
+
* Forwarded as-is: one binding to both drive the popup's open state and
|
|
54230
|
+
* know where it is (see `Dialog`/`Popover`'s own `signal`).
|
|
54089
54231
|
* @param {boolean} [props.defaultOpen] - Forwarded as-is (uncontrolled,
|
|
54090
54232
|
* mount-only).
|
|
54091
54233
|
* @param {(event: Event) => void} [props.onClose] - Forwarded as-is.
|
|
@@ -68158,12 +68300,19 @@ const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
|
|
|
68158
68300
|
* past.
|
|
68159
68301
|
* @param {import("ignore:preact").ComponentChildren} [separator] What is written
|
|
68160
68302
|
* between the hours and the minutes. "h" in French, ":" elsewhere.
|
|
68303
|
+
* @param {string} [placeholder] What the wheels show while the time holds
|
|
68304
|
+
* nothing, as "HH:MM". Wheels have no blank row to land on, so their
|
|
68305
|
+
* placeholder is a position rather than a grey word — shown, but not an
|
|
68306
|
+
* answer: the value stays `undefined` until a wheel is turned or a real value
|
|
68307
|
+
* arrives. `defaultValue` is the other half of the pair — a time that IS the
|
|
68308
|
+
* answer, and where a reset goes back to.
|
|
68161
68309
|
* @param {object} [wheelProps] Anything a `Wheel` takes, said once for both of
|
|
68162
68310
|
* them — `visibleCount`, `itemWidth`, `glideSpeed`.
|
|
68163
68311
|
*/
|
|
68164
68312
|
const TimeWheel = ({
|
|
68165
68313
|
minuteStep = 1,
|
|
68166
68314
|
loop = true,
|
|
68315
|
+
placeholder,
|
|
68167
68316
|
separator = naviI18n("time.hour_separator"),
|
|
68168
68317
|
hourLabel = naviI18n("time.hour_label"),
|
|
68169
68318
|
minuteLabel = naviI18n("time.minute_label"),
|
|
@@ -68180,8 +68329,12 @@ const TimeWheel = ({
|
|
|
68180
68329
|
}
|
|
68181
68330
|
return minuteList;
|
|
68182
68331
|
}, [minuteStep]);
|
|
68332
|
+
const {
|
|
68333
|
+
aggregateChildStates
|
|
68334
|
+
} = useAnswered(placeholder, rest, aggregateTime);
|
|
68335
|
+
const placeholderParts = parseTimeParts(placeholder);
|
|
68183
68336
|
return jsxs(WheelGroup, {
|
|
68184
|
-
aggregateChildStates:
|
|
68337
|
+
aggregateChildStates: aggregateChildStates,
|
|
68185
68338
|
distributeChildUIState: distributeTime,
|
|
68186
68339
|
...rest,
|
|
68187
68340
|
children: [jsx(Wheel, {
|
|
@@ -68190,6 +68343,7 @@ const TimeWheel = ({
|
|
|
68190
68343
|
bounded: !loop,
|
|
68191
68344
|
size: size,
|
|
68192
68345
|
"aria-label": hourLabel,
|
|
68346
|
+
defaultValue: placeholderParts ? placeholderParts.hour : undefined,
|
|
68193
68347
|
...wheelProps,
|
|
68194
68348
|
children: HOURS.map(hour => jsx(Wheel.Item, {
|
|
68195
68349
|
value: hour,
|
|
@@ -68205,6 +68359,7 @@ const TimeWheel = ({
|
|
|
68205
68359
|
bounded: !loop,
|
|
68206
68360
|
size: size,
|
|
68207
68361
|
"aria-label": minuteLabel,
|
|
68362
|
+
defaultValue: placeholderParts ? placeholderParts.minute : undefined,
|
|
68208
68363
|
...wheelProps,
|
|
68209
68364
|
children: minutes.map(minute => jsx(Wheel.Item, {
|
|
68210
68365
|
value: minute,
|
|
@@ -68242,6 +68397,12 @@ const TimeWheel = ({
|
|
|
68242
68397
|
* one that goes backwards is not. It is what the bounds keep between them as
|
|
68243
68398
|
* they turn — turn the start into the end and the end moves along, keeping
|
|
68244
68399
|
* that much room.
|
|
68400
|
+
* @param {{ start?: string, end?: string }} [placeholder] What the two wheels
|
|
68401
|
+
* show while the span holds nothing — a position, since wheels have no blank
|
|
68402
|
+
* row to land on, and not an answer: the value stays `undefined` until one of
|
|
68403
|
+
* them is turned. One turn settles both, the untouched bound included, left
|
|
68404
|
+
* where the placeholder put it. For a span that is optional ("any time of
|
|
68405
|
+
* day") and still has to show hours.
|
|
68245
68406
|
* @param {object} [timeProps] Anything a `TimeWheel` takes, said once for both
|
|
68246
68407
|
* of them. `startTimeProps`/`endTimeProps` say it to one of the two, and win
|
|
68247
68408
|
* over this one.
|
|
@@ -68250,6 +68411,7 @@ const TimeRangeWheel = ({
|
|
|
68250
68411
|
minuteStep = 1,
|
|
68251
68412
|
minDuration = 0,
|
|
68252
68413
|
loop = true,
|
|
68414
|
+
placeholder,
|
|
68253
68415
|
size,
|
|
68254
68416
|
startLabel = naviI18n("time_range.from"),
|
|
68255
68417
|
endLabel = naviI18n("time_range.to"),
|
|
@@ -68261,6 +68423,12 @@ const TimeRangeWheel = ({
|
|
|
68261
68423
|
const startId = useId();
|
|
68262
68424
|
const startRef = useRef(null);
|
|
68263
68425
|
const endRef = useRef(null);
|
|
68426
|
+
// One turn settles the whole span: a start somebody chose makes the end an
|
|
68427
|
+
// answer too, left where the placeholder put it.
|
|
68428
|
+
const {
|
|
68429
|
+
answeredRef,
|
|
68430
|
+
aggregateChildStates
|
|
68431
|
+
} = useAnswered(placeholder, rest, aggregateSpan);
|
|
68264
68432
|
|
|
68265
68433
|
// What the pair does while it is being turned: the bound that just moved is
|
|
68266
68434
|
// the one the user is holding, so it stays where it was put and the OTHER one
|
|
@@ -68297,51 +68465,127 @@ const TimeRangeWheel = ({
|
|
|
68297
68465
|
event: e
|
|
68298
68466
|
});
|
|
68299
68467
|
};
|
|
68300
|
-
return
|
|
68468
|
+
return jsx(ControlGroup, {
|
|
68301
68469
|
flex: true,
|
|
68302
68470
|
alignY: "center",
|
|
68303
68471
|
spacing: "s",
|
|
68304
68472
|
size: size,
|
|
68473
|
+
aggregateChildStates: aggregateChildStates,
|
|
68305
68474
|
...rest,
|
|
68306
|
-
children:
|
|
68307
|
-
|
|
68308
|
-
children: startLabel
|
|
68309
|
-
|
|
68310
|
-
|
|
68311
|
-
|
|
68312
|
-
|
|
68313
|
-
|
|
68314
|
-
|
|
68315
|
-
|
|
68316
|
-
|
|
68317
|
-
|
|
68318
|
-
|
|
68319
|
-
|
|
68320
|
-
|
|
68321
|
-
|
|
68322
|
-
|
|
68323
|
-
|
|
68324
|
-
|
|
68325
|
-
|
|
68326
|
-
|
|
68327
|
-
|
|
68328
|
-
|
|
68329
|
-
|
|
68330
|
-
|
|
68331
|
-
|
|
68332
|
-
|
|
68333
|
-
|
|
68334
|
-
|
|
68335
|
-
|
|
68336
|
-
|
|
68337
|
-
|
|
68475
|
+
children: jsxs(AnsweredContext.Provider, {
|
|
68476
|
+
value: answeredRef,
|
|
68477
|
+
children: [startLabel === null ? null : jsx(Text, {
|
|
68478
|
+
size: size,
|
|
68479
|
+
children: startLabel
|
|
68480
|
+
}), jsx(TimeWheel, {
|
|
68481
|
+
id: startId,
|
|
68482
|
+
ref: startRef,
|
|
68483
|
+
name: "start",
|
|
68484
|
+
minuteStep: minuteStep,
|
|
68485
|
+
loop: loop,
|
|
68486
|
+
size: size,
|
|
68487
|
+
placeholder: placeholder ? placeholder.start : undefined,
|
|
68488
|
+
uiAction: (value, e) => keepBoundsApart("start", value, e),
|
|
68489
|
+
...timeProps,
|
|
68490
|
+
...startTimeProps
|
|
68491
|
+
}), endLabel === null ? null : jsx(Text, {
|
|
68492
|
+
size: size,
|
|
68493
|
+
children: endLabel
|
|
68494
|
+
}), jsx(TimeWheel, {
|
|
68495
|
+
ref: endRef,
|
|
68496
|
+
name: "end",
|
|
68497
|
+
minuteStep: minuteStep,
|
|
68498
|
+
loop: loop,
|
|
68499
|
+
size: size,
|
|
68500
|
+
placeholder: placeholder ? placeholder.end : undefined,
|
|
68501
|
+
uiAction: (value, e) => keepBoundsApart("end", value, e)
|
|
68502
|
+
// Which time it comes after, and how much room there must be between
|
|
68503
|
+
// the two: said on the LATER of the two, so the answer is given where
|
|
68504
|
+
// the time one would have to move is (see time_range_constraint.js).
|
|
68505
|
+
,
|
|
68506
|
+
"data-time-after": startId,
|
|
68507
|
+
"data-time-min-duration": minDuration,
|
|
68508
|
+
...timeProps,
|
|
68509
|
+
...endTimeProps
|
|
68510
|
+
})]
|
|
68511
|
+
})
|
|
68338
68512
|
});
|
|
68339
68513
|
};
|
|
68514
|
+
|
|
68515
|
+
/**
|
|
68516
|
+
* Wheels always show something — there is no blank row to land on — so a pair of
|
|
68517
|
+
* them cannot say "nothing set" by looking empty. Their `placeholder` is
|
|
68518
|
+
* therefore a position rather than a grey word: shown like a value, and not one.
|
|
68519
|
+
* The value stays `undefined` until a wheel is turned, which is what tells "any
|
|
68520
|
+
* time of day" from a span somebody chose. `defaultValue` remains what it is
|
|
68521
|
+
* everywhere else — a time that IS the answer.
|
|
68522
|
+
*
|
|
68523
|
+
* What counts as turned is read from the value itself rather than from a
|
|
68524
|
+
* gesture: while nothing has moved off the placeholder, nothing is set; the
|
|
68525
|
+
* moment it differs, it is an answer and stays one, even turned back onto the
|
|
68526
|
+
* placeholder. A wheel's own `uiAction` runs after its group has aggregated, so
|
|
68527
|
+
* a flag set from there would always be one turn late.
|
|
68528
|
+
*
|
|
68529
|
+
* The flag is a ref rather than state because the aggregate a group is created
|
|
68530
|
+
* with is the one it keeps: swapping the function on a later render changes
|
|
68531
|
+
* nothing (see useUIGroupStateController). One stable function reading one ref.
|
|
68532
|
+
*
|
|
68533
|
+
* A pair shares ONE flag through `AnsweredContext`: turning the start settles
|
|
68534
|
+
* the end too, left where the placeholder put it. Each of the two times gating
|
|
68535
|
+
* on its own would answer half a span — and would leave the pair nothing to
|
|
68536
|
+
* compare its own placeholder against.
|
|
68537
|
+
*/
|
|
68538
|
+
const AnsweredContext = createContext(null);
|
|
68539
|
+
const useAnswered = (placeholder, props, aggregateWhenAnswered) => {
|
|
68540
|
+
const answeredFromPair = useContext(AnsweredContext);
|
|
68541
|
+
const ownAnsweredRef = useRef(false);
|
|
68542
|
+
const answeredRef = answeredFromPair || ownAnsweredRef;
|
|
68543
|
+
if (!placeholder || isAnswerGivenByProps(props)) {
|
|
68544
|
+
answeredRef.current = true;
|
|
68545
|
+
}
|
|
68546
|
+
// Inside a pair, only the pair decides: a time that gated on its own would
|
|
68547
|
+
// hand the pair nothing to compare, and half a span cannot be read.
|
|
68548
|
+
const gates = !answeredFromPair;
|
|
68549
|
+
const placeholderRef = useRef(placeholder);
|
|
68550
|
+
placeholderRef.current = placeholder;
|
|
68551
|
+
const aggregateRef = useRef(null);
|
|
68552
|
+
if (!aggregateRef.current) {
|
|
68553
|
+
aggregateRef.current = children => {
|
|
68554
|
+
const aggregated = aggregateWhenAnswered(children);
|
|
68555
|
+
if (answeredRef.current) {
|
|
68556
|
+
return aggregated;
|
|
68557
|
+
}
|
|
68558
|
+
if (compareTwoJsValues(aggregated, placeholderRef.current)) {
|
|
68559
|
+
return undefined;
|
|
68560
|
+
}
|
|
68561
|
+
// It moved: from here on this is an answer, and stays one even when it is
|
|
68562
|
+
// turned back onto the placeholder — somebody chose that time.
|
|
68563
|
+
answeredRef.current = true;
|
|
68564
|
+
return aggregated;
|
|
68565
|
+
};
|
|
68566
|
+
}
|
|
68567
|
+
return {
|
|
68568
|
+
answeredRef,
|
|
68569
|
+
aggregateChildStates: gates ? aggregateRef.current : aggregateWhenAnswered
|
|
68570
|
+
};
|
|
68571
|
+
};
|
|
68572
|
+
const isAnswerGivenByProps = props => props.value !== undefined || props.defaultValue !== undefined || props.signal && props.signal.value !== undefined;
|
|
68340
68573
|
const HOURS = Array.from({
|
|
68341
68574
|
length: HOUR_COUNT
|
|
68342
68575
|
}, (_, hour) => hour);
|
|
68343
68576
|
const padTwo = value => String(value).padStart(2, "0");
|
|
68344
68577
|
|
|
68578
|
+
// The two times as one span, { start, end } — the shape a pair carries.
|
|
68579
|
+
const aggregateSpan = childUIStateControllers => {
|
|
68580
|
+
const span = {};
|
|
68581
|
+
for (const child of childUIStateControllers) {
|
|
68582
|
+
if (child.name === "start" || child.name === "end") {
|
|
68583
|
+
span[child.name] = child.uiState;
|
|
68584
|
+
}
|
|
68585
|
+
}
|
|
68586
|
+
return span;
|
|
68587
|
+
};
|
|
68588
|
+
|
|
68345
68589
|
// The two wheels as one value, "HH:MM".
|
|
68346
68590
|
const aggregateTime = childUIStateControllers => {
|
|
68347
68591
|
let hour = "";
|
|
@@ -74068,6 +74312,12 @@ const css = /* css */`
|
|
|
74068
74312
|
* @param {object} props
|
|
74069
74313
|
* @param {boolean} [props.open] - Controlled open state, forwarded as-is to
|
|
74070
74314
|
* `Popup`'s own `open`.
|
|
74315
|
+
* @param {import("@preact/signals").Signal<boolean>} [props.signal] - The open
|
|
74316
|
+
* state said the way every navi control says it: the panel opens and closes
|
|
74317
|
+
* to match the signal, and writes into it whenever it opens or closes on its
|
|
74318
|
+
* own (Escape, swipe, a --navi-close command) — one binding to both drive
|
|
74319
|
+
* the panel and know where it is. Forwarded as-is to `Popup`; excludes
|
|
74320
|
+
* `open` (see `Dialog`/`Popover`'s own `signal`).
|
|
74071
74321
|
* @param {boolean} [props.defaultOpen] - Uncontrolled, mount-only initial
|
|
74072
74322
|
* open state, forwarded as-is to `Popup`. Neither this nor `open` is
|
|
74073
74323
|
* required at all for a purely command-driven panel (an `id` plus a
|
|
@@ -74134,6 +74384,7 @@ const css = /* css */`
|
|
|
74134
74384
|
*/
|
|
74135
74385
|
const SidePanel = ({
|
|
74136
74386
|
open,
|
|
74387
|
+
signal,
|
|
74137
74388
|
defaultOpen,
|
|
74138
74389
|
onClose,
|
|
74139
74390
|
children,
|
|
@@ -74155,6 +74406,7 @@ const SidePanel = ({
|
|
|
74155
74406
|
return jsx(Popup, {
|
|
74156
74407
|
mode: mode,
|
|
74157
74408
|
open: open,
|
|
74409
|
+
signal: signal,
|
|
74158
74410
|
defaultOpen: defaultOpen,
|
|
74159
74411
|
onClose: onClose,
|
|
74160
74412
|
layer: layer,
|