@jsenv/navi 0.30.23 → 0.30.25
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/dev/jsenv_navi.js +139 -17
- package/dist/dev/jsenv_navi.js.map +5 -5
- package/dist/jsenv_navi.js +139 -17
- package/dist/jsenv_navi.js.map +5 -5
- package/docs/AI_INSTRUCTIONS.md +5 -1
- package/docs/actions.md +2 -2
- package/docs/list_refresh.md +8 -0
- package/docs/resource.md +123 -13
- package/package.json +1 -1
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -28608,7 +28608,11 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
28608
28608
|
* (docs/resource.md).
|
|
28609
28609
|
*/
|
|
28610
28610
|
const upsertAnswer = (...args) => upsertWith(args, true);
|
|
28611
|
-
|
|
28611
|
+
// Batched like upsertWith: a removal observer may drop rows of another
|
|
28612
|
+
// store (a `.one()` declared with `onDelete: "cascade"`), and an effect
|
|
28613
|
+
// reading both runs once, never between the child leaving and its parents.
|
|
28614
|
+
const drop = (...args) => batch(() => dropNow(...args));
|
|
28615
|
+
const dropNow = (...args) => {
|
|
28612
28616
|
const removedItemArray = [];
|
|
28613
28617
|
const triggerRemovedMutations = () => {
|
|
28614
28618
|
if (removedItemArray.length === 0) {
|
|
@@ -28643,6 +28647,7 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
28643
28647
|
hasFound = true;
|
|
28644
28648
|
idToRemoveSet.delete(existingItemId);
|
|
28645
28649
|
idRemovedArray.push(existingItemId);
|
|
28650
|
+
removedItemArray.push(existingItem);
|
|
28646
28651
|
} else {
|
|
28647
28652
|
arrayWithoutDroppedItems.push(existingItem);
|
|
28648
28653
|
}
|
|
@@ -30753,6 +30758,15 @@ const createResource = (
|
|
|
30753
30758
|
* The property is reactive: updating the child item anywhere propagates immediately.
|
|
30754
30759
|
* The child resource exists independently — it is not owned by, nor deleted with, the parent.
|
|
30755
30760
|
*
|
|
30761
|
+
* The other way round is declared with `onDelete`. Left out, a parent outlives its
|
|
30762
|
+
* child: once the child leaves its store, the property reads an empty object (truthy,
|
|
30763
|
+
* `valueOf()` is `null`, no fields) until the parent is read again — a game whose
|
|
30764
|
+
* organizer is deleted is still a game. `onDelete: "cascade"` is for a parent that is
|
|
30765
|
+
* nothing without its child (an address-book row, the person in it): when the child
|
|
30766
|
+
* leaves its store, the parents pointing at it leave theirs in the same write, and
|
|
30767
|
+
* with them every list holding them. Which one a relation needs is decided in
|
|
30768
|
+
* docs/resource.md, "When the child is deleted".
|
|
30769
|
+
*
|
|
30756
30770
|
* Use it when the child is a first-class entity with its own store, shared across
|
|
30757
30771
|
* parents (a user referenced by many games). When the child only exists inside its
|
|
30758
30772
|
* owner, use `.scopedOne()` instead.
|
|
@@ -30772,7 +30786,12 @@ const createResource = (
|
|
|
30772
30786
|
*
|
|
30773
30787
|
* @param {string} propertyName - property holding the child on each parent item
|
|
30774
30788
|
* @param {Object} childResource - the independent resource created by `resource()`
|
|
30775
|
-
* @param {Object} [restCallbacks] - `{ rerunOn, dependencies, GET, PUT, DELETE }`
|
|
30789
|
+
* @param {Object} [restCallbacks] - `{ rerunOn, dependencies, onDelete, GET, PUT, DELETE }`
|
|
30790
|
+
* @param {"cascade"} [restCallbacks.onDelete] - `"cascade"`: a parent leaves its store
|
|
30791
|
+
* when its child leaves the child's (the child's `DELETE`, `DELETE_MANY`, a write
|
|
30792
|
+
* named with verb `DELETE`, a cascade of its own). Not the relation's own `DELETE`,
|
|
30793
|
+
* which unlinks: the property becomes `null` and the parent stays. Throws on a
|
|
30794
|
+
* relation declared on a scoped child.
|
|
30776
30795
|
* @see docs/resource.md
|
|
30777
30796
|
*/
|
|
30778
30797
|
stateFacade.one = (
|
|
@@ -30781,6 +30800,7 @@ const createResource = (
|
|
|
30781
30800
|
{
|
|
30782
30801
|
rerunOn: oneRerunOn,
|
|
30783
30802
|
dependencies: oneDependencies,
|
|
30803
|
+
onDelete,
|
|
30784
30804
|
|
|
30785
30805
|
GET,
|
|
30786
30806
|
PUT,
|
|
@@ -30788,16 +30808,54 @@ const createResource = (
|
|
|
30788
30808
|
} = {},
|
|
30789
30809
|
) => {
|
|
30790
30810
|
const declarationSite = getDeclarationSite();
|
|
30811
|
+
const relationLabel = `${name}.one("${propertyName}")`;
|
|
30791
30812
|
if (ownerName) {
|
|
30792
30813
|
refuseCallbacksNamingScopedItem(
|
|
30793
|
-
|
|
30814
|
+
relationLabel,
|
|
30794
30815
|
{ GET, PUT, DELETE },
|
|
30795
30816
|
{ name, ownerName, propertyName, declarationSite },
|
|
30796
30817
|
);
|
|
30797
30818
|
}
|
|
30819
|
+
if (onDelete !== undefined && onDelete !== "cascade") {
|
|
30820
|
+
throw new TypeError(
|
|
30821
|
+
`${relationLabel}: onDelete must be "cascade" or left out, got ${onDelete}.
|
|
30822
|
+
${relationLabel} source location: ${declarationSite}`,
|
|
30823
|
+
);
|
|
30824
|
+
}
|
|
30825
|
+
const cascade = onDelete === "cascade";
|
|
30826
|
+
if (cascade && ownerName) {
|
|
30827
|
+
// A scoped item lives in its owner's collection, not in a store this
|
|
30828
|
+
// relation could drop it from.
|
|
30829
|
+
throw new Error(
|
|
30830
|
+
`${relationLabel} cannot declare onDelete: "cascade": a "${name}" item exists only inside its "${ownerName}", and leaves through the "${ownerName}" callbacks.
|
|
30831
|
+
Read the "${ownerName}" again after the "${childResource.name}" is deleted (dependencies).
|
|
30832
|
+
${relationLabel} source location: ${declarationSite}`,
|
|
30833
|
+
);
|
|
30834
|
+
}
|
|
30798
30835
|
const childName = `${name}.${propertyName}`;
|
|
30799
30836
|
const childIdKey = childResource.idKey;
|
|
30800
30837
|
const childStore = childResource.store;
|
|
30838
|
+
// The id each parent points at, carried by the parent row (copies of a row
|
|
30839
|
+
// keep it, a spread does not): a removal is matched against the parents
|
|
30840
|
+
// the store holds at that moment, under the ids they have then.
|
|
30841
|
+
const childIdSymbol = cascade ? Symbol(`${childName} id`) : undefined;
|
|
30842
|
+
if (cascade) {
|
|
30843
|
+
childStore.observeRemovals((removedChildArray) => {
|
|
30844
|
+
const removedChildIdSet = new Set();
|
|
30845
|
+
for (const removedChild of removedChildArray) {
|
|
30846
|
+
removedChildIdSet.add(removedChild[childIdKey]);
|
|
30847
|
+
}
|
|
30848
|
+
const orphanIdArray = [];
|
|
30849
|
+
for (const parentItem of store.arraySignal.peek()) {
|
|
30850
|
+
if (removedChildIdSet.has(parentItem[childIdSymbol])) {
|
|
30851
|
+
orphanIdArray.push(parentItem[idKey]);
|
|
30852
|
+
}
|
|
30853
|
+
}
|
|
30854
|
+
if (orphanIdArray.length > 0) {
|
|
30855
|
+
store.drop(orphanIdArray);
|
|
30856
|
+
}
|
|
30857
|
+
});
|
|
30858
|
+
}
|
|
30801
30859
|
addItemSetup((item, answer) => {
|
|
30802
30860
|
const childItemIdSignal = signal();
|
|
30803
30861
|
const updateChildItemId = (value, answer) => {
|
|
@@ -30852,6 +30910,11 @@ const createResource = (
|
|
|
30852
30910
|
get: () => childItemFacadeSignal.value,
|
|
30853
30911
|
set: updateChildItemId,
|
|
30854
30912
|
});
|
|
30913
|
+
if (cascade) {
|
|
30914
|
+
Object.defineProperty(item, childIdSymbol, {
|
|
30915
|
+
get: () => childItemIdSignal.peek(),
|
|
30916
|
+
});
|
|
30917
|
+
}
|
|
30855
30918
|
debug$2(
|
|
30856
30919
|
`setup ${item}.${propertyName} is one "${childResource.name}" (current value: ${childItemSignal.peek()})`,
|
|
30857
30920
|
);
|
|
@@ -39453,10 +39516,12 @@ const barsOutside = (areaElement) => {
|
|
|
39453
39516
|
};
|
|
39454
39517
|
|
|
39455
39518
|
// Room around what a bar's picture holds for what paints past every box — a
|
|
39456
|
-
// shadow, the
|
|
39519
|
+
// shadow, the bar's own filter — which no API measures: a filter cuts whatever
|
|
39457
39520
|
// falls outside its region.
|
|
39458
39521
|
const WALL_FILTER_INK_ROOM = 100;
|
|
39459
39522
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
39523
|
+
// A blur() in a computed filter, its radius in px.
|
|
39524
|
+
const BLUR_FUNCTION_REGEXP = /blur\(([^)]*)\)/g;
|
|
39460
39525
|
|
|
39461
39526
|
// The bar's picture dimmed, not a rectangle over it: each wall's colour is
|
|
39462
39527
|
// flooded atop the bar's own pixels (Porter-Duff source-atop, which no blend
|
|
@@ -39484,11 +39549,17 @@ const dimBarPicture = (bar, sources) => {
|
|
|
39484
39549
|
const ownFilter = getComputedStyle(bar).filter;
|
|
39485
39550
|
const chain = ownFilter === "none" ? [] : [ownFilter];
|
|
39486
39551
|
for (const source of sources) {
|
|
39487
|
-
const backdropFilter =
|
|
39488
|
-
|
|
39489
|
-
|
|
39490
|
-
|
|
39491
|
-
chain.push(
|
|
39552
|
+
const backdropFilter = readBackdropFilter(source);
|
|
39553
|
+
if (backdropFilter !== "none") {
|
|
39554
|
+
// Blurred alone, the bar would fade to transparent over its edges and
|
|
39555
|
+
// show the live document through them: its blur stays inside its shape.
|
|
39556
|
+
chain.push(
|
|
39557
|
+
backdropFilter.replace(
|
|
39558
|
+
BLUR_FUNCTION_REGEXP,
|
|
39559
|
+
(_, radius) =>
|
|
39560
|
+
`url(#${createShapeBlurFilter(parseFloat(radius), region).id})`,
|
|
39561
|
+
),
|
|
39562
|
+
);
|
|
39492
39563
|
}
|
|
39493
39564
|
chain.push(`url(#${createWallFilter(source, region).id})`);
|
|
39494
39565
|
}
|
|
@@ -39497,7 +39568,59 @@ const dimBarPicture = (bar, sources) => {
|
|
|
39497
39568
|
dimmedBars.push(bar);
|
|
39498
39569
|
};
|
|
39499
39570
|
|
|
39571
|
+
// The wall's backdrop-filter as computed, so a blur radius written in em or
|
|
39572
|
+
// rem reads in px, as the wall resolves it where it paints.
|
|
39573
|
+
const readBackdropFilter = (source) => {
|
|
39574
|
+
const sourceStyle = getComputedStyle(source);
|
|
39575
|
+
const backdropFilter = sourceStyle
|
|
39576
|
+
.getPropertyValue("--backdrop-filter")
|
|
39577
|
+
.trim();
|
|
39578
|
+
if (!backdropFilter || backdropFilter === "none") {
|
|
39579
|
+
return "none";
|
|
39580
|
+
}
|
|
39581
|
+
const probe = document.createElement("div");
|
|
39582
|
+
probe.style.fontSize = sourceStyle.fontSize;
|
|
39583
|
+
probe.style.filter = backdropFilter;
|
|
39584
|
+
document.body.append(probe);
|
|
39585
|
+
const filter = getComputedStyle(probe).filter;
|
|
39586
|
+
probe.remove();
|
|
39587
|
+
return filter;
|
|
39588
|
+
};
|
|
39589
|
+
|
|
39500
39590
|
const createWallFilter = (source, region) => {
|
|
39591
|
+
const filter = createFilter(region);
|
|
39592
|
+
const flood = document.createElementNS(SVG_NS, "feFlood");
|
|
39593
|
+
flood.setAttribute("flood-color", readBackdropColor(source));
|
|
39594
|
+
const composite = document.createElementNS(SVG_NS, "feComposite");
|
|
39595
|
+
composite.setAttribute("in2", "SourceGraphic");
|
|
39596
|
+
composite.setAttribute("operator", "atop");
|
|
39597
|
+
filter.append(flood, composite);
|
|
39598
|
+
return filter;
|
|
39599
|
+
};
|
|
39600
|
+
|
|
39601
|
+
// Colour kept, alpha set to 1.
|
|
39602
|
+
const OPAQUE_MATRIX = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 1";
|
|
39603
|
+
|
|
39604
|
+
// The wall's blur kept inside the bar. At rest the window is blurred as one
|
|
39605
|
+
// image; a CSS blur() on the bar sees only the bar, so its edges go half
|
|
39606
|
+
// transparent and its colour spreads past its box. Made opaque, each pixel is
|
|
39607
|
+
// the mean of the bar's own pixels around it (feColorMatrix works on
|
|
39608
|
+
// unpremultiplied colours), then cut back to the bar's own alpha.
|
|
39609
|
+
const createShapeBlurFilter = (stdDeviation, region) => {
|
|
39610
|
+
const filter = createFilter(region);
|
|
39611
|
+
const blur = document.createElementNS(SVG_NS, "feGaussianBlur");
|
|
39612
|
+
blur.setAttribute("stdDeviation", stdDeviation);
|
|
39613
|
+
const opaque = document.createElementNS(SVG_NS, "feColorMatrix");
|
|
39614
|
+
opaque.setAttribute("type", "matrix");
|
|
39615
|
+
opaque.setAttribute("values", OPAQUE_MATRIX);
|
|
39616
|
+
const clip = document.createElementNS(SVG_NS, "feComposite");
|
|
39617
|
+
clip.setAttribute("in2", "SourceAlpha");
|
|
39618
|
+
clip.setAttribute("operator", "in");
|
|
39619
|
+
filter.append(blur, opaque, clip);
|
|
39620
|
+
return filter;
|
|
39621
|
+
};
|
|
39622
|
+
|
|
39623
|
+
const createFilter = (region) => {
|
|
39501
39624
|
if (!wallFilterSvg) {
|
|
39502
39625
|
wallFilterSvg = document.createElementNS(SVG_NS, "svg");
|
|
39503
39626
|
wallFilterSvg.setAttribute(WALL_FILTERS_ATTRIBUTE, "");
|
|
@@ -39507,18 +39630,13 @@ const createWallFilter = (source, region) => {
|
|
|
39507
39630
|
wallFilterCount++;
|
|
39508
39631
|
const filter = document.createElementNS(SVG_NS, "filter");
|
|
39509
39632
|
filter.id = `navi-transition-wall-${wallFilterCount}`;
|
|
39510
|
-
// linearRGB, the default, floods another colour than the wall's
|
|
39633
|
+
// linearRGB, the default, floods another colour than the wall's and blurs
|
|
39634
|
+
// in another space than its backdrop-filter.
|
|
39511
39635
|
filter.setAttribute("color-interpolation-filters", "sRGB");
|
|
39512
39636
|
filter.setAttribute("x", region.x);
|
|
39513
39637
|
filter.setAttribute("y", region.y);
|
|
39514
39638
|
filter.setAttribute("width", region.width);
|
|
39515
39639
|
filter.setAttribute("height", region.height);
|
|
39516
|
-
const flood = document.createElementNS(SVG_NS, "feFlood");
|
|
39517
|
-
flood.setAttribute("flood-color", readBackdropColor(source));
|
|
39518
|
-
const composite = document.createElementNS(SVG_NS, "feComposite");
|
|
39519
|
-
composite.setAttribute("in2", "SourceGraphic");
|
|
39520
|
-
composite.setAttribute("operator", "atop");
|
|
39521
|
-
filter.append(flood, composite);
|
|
39522
39640
|
wallFilterSvg.appendChild(filter);
|
|
39523
39641
|
return filter;
|
|
39524
39642
|
};
|
|
@@ -42441,10 +42559,14 @@ const RouteTravel = ({
|
|
|
42441
42559
|
const pages = pagesRef.current;
|
|
42442
42560
|
const currentIndex = currentPageIndex(pages);
|
|
42443
42561
|
// Where the page of a window stands: the address, or where it last was for an
|
|
42444
|
-
// address the row has no place for.
|
|
42562
|
+
// address the row has no place for, while the row still has that column.
|
|
42445
42563
|
const liveIndexRef = useRef(0);
|
|
42446
42564
|
if (currentIndex !== -1) {
|
|
42447
42565
|
liveIndexRef.current = currentIndex;
|
|
42566
|
+
} else if (liveIndexRef.current >= pages.length) {
|
|
42567
|
+
// Past the row's end the page stands in a column of its own, off screen: a
|
|
42568
|
+
// still row does not scroll, and nothing scrolls a row to an address it lacks.
|
|
42569
|
+
liveIndexRef.current = pages.length === 0 ? 0 : pages.length - 1;
|
|
42448
42570
|
}
|
|
42449
42571
|
const liveIndex = liveIndexRef.current;
|
|
42450
42572
|
const latestRef = useRef(null);
|