@jsenv/navi 0.30.24 → 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 +134 -16
- package/dist/dev/jsenv_navi.js.map +4 -4
- package/dist/jsenv_navi.js +134 -16
- package/dist/jsenv_navi.js.map +4 -4
- package/docs/AI_INSTRUCTIONS.md +5 -1
- package/docs/resource.md +119 -10
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -27535,7 +27535,11 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
27535
27535
|
* (docs/resource.md).
|
|
27536
27536
|
*/
|
|
27537
27537
|
const upsertAnswer = (...args) => upsertWith(args, true);
|
|
27538
|
-
|
|
27538
|
+
// Batched like upsertWith: a removal observer may drop rows of another
|
|
27539
|
+
// store (a `.one()` declared with `onDelete: "cascade"`), and an effect
|
|
27540
|
+
// reading both runs once, never between the child leaving and its parents.
|
|
27541
|
+
const drop = (...args) => batch(() => dropNow(...args));
|
|
27542
|
+
const dropNow = (...args) => {
|
|
27539
27543
|
const removedItemArray = [];
|
|
27540
27544
|
const triggerRemovedMutations = () => {
|
|
27541
27545
|
if (removedItemArray.length === 0) {
|
|
@@ -27570,6 +27574,7 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
|
|
|
27570
27574
|
hasFound = true;
|
|
27571
27575
|
idToRemoveSet.delete(existingItemId);
|
|
27572
27576
|
idRemovedArray.push(existingItemId);
|
|
27577
|
+
removedItemArray.push(existingItem);
|
|
27573
27578
|
} else {
|
|
27574
27579
|
arrayWithoutDroppedItems.push(existingItem);
|
|
27575
27580
|
}
|
|
@@ -29629,6 +29634,15 @@ const createResource = (
|
|
|
29629
29634
|
* The property is reactive: updating the child item anywhere propagates immediately.
|
|
29630
29635
|
* The child resource exists independently — it is not owned by, nor deleted with, the parent.
|
|
29631
29636
|
*
|
|
29637
|
+
* The other way round is declared with `onDelete`. Left out, a parent outlives its
|
|
29638
|
+
* child: once the child leaves its store, the property reads an empty object (truthy,
|
|
29639
|
+
* `valueOf()` is `null`, no fields) until the parent is read again — a game whose
|
|
29640
|
+
* organizer is deleted is still a game. `onDelete: "cascade"` is for a parent that is
|
|
29641
|
+
* nothing without its child (an address-book row, the person in it): when the child
|
|
29642
|
+
* leaves its store, the parents pointing at it leave theirs in the same write, and
|
|
29643
|
+
* with them every list holding them. Which one a relation needs is decided in
|
|
29644
|
+
* docs/resource.md, "When the child is deleted".
|
|
29645
|
+
*
|
|
29632
29646
|
* Use it when the child is a first-class entity with its own store, shared across
|
|
29633
29647
|
* parents (a user referenced by many games). When the child only exists inside its
|
|
29634
29648
|
* owner, use `.scopedOne()` instead.
|
|
@@ -29648,7 +29662,12 @@ const createResource = (
|
|
|
29648
29662
|
*
|
|
29649
29663
|
* @param {string} propertyName - property holding the child on each parent item
|
|
29650
29664
|
* @param {Object} childResource - the independent resource created by `resource()`
|
|
29651
|
-
* @param {Object} [restCallbacks] - `{ rerunOn, dependencies, GET, PUT, DELETE }`
|
|
29665
|
+
* @param {Object} [restCallbacks] - `{ rerunOn, dependencies, onDelete, GET, PUT, DELETE }`
|
|
29666
|
+
* @param {"cascade"} [restCallbacks.onDelete] - `"cascade"`: a parent leaves its store
|
|
29667
|
+
* when its child leaves the child's (the child's `DELETE`, `DELETE_MANY`, a write
|
|
29668
|
+
* named with verb `DELETE`, a cascade of its own). Not the relation's own `DELETE`,
|
|
29669
|
+
* which unlinks: the property becomes `null` and the parent stays. Throws on a
|
|
29670
|
+
* relation declared on a scoped child.
|
|
29652
29671
|
* @see docs/resource.md
|
|
29653
29672
|
*/
|
|
29654
29673
|
stateFacade.one = (
|
|
@@ -29657,6 +29676,7 @@ const createResource = (
|
|
|
29657
29676
|
{
|
|
29658
29677
|
rerunOn: oneRerunOn,
|
|
29659
29678
|
dependencies: oneDependencies,
|
|
29679
|
+
onDelete,
|
|
29660
29680
|
|
|
29661
29681
|
GET,
|
|
29662
29682
|
PUT,
|
|
@@ -29664,16 +29684,54 @@ const createResource = (
|
|
|
29664
29684
|
} = {},
|
|
29665
29685
|
) => {
|
|
29666
29686
|
const declarationSite = getDeclarationSite();
|
|
29687
|
+
const relationLabel = `${name}.one("${propertyName}")`;
|
|
29667
29688
|
if (ownerName) {
|
|
29668
29689
|
refuseCallbacksNamingScopedItem(
|
|
29669
|
-
|
|
29690
|
+
relationLabel,
|
|
29670
29691
|
{ GET, PUT, DELETE },
|
|
29671
29692
|
{ name, ownerName, propertyName, declarationSite },
|
|
29672
29693
|
);
|
|
29673
29694
|
}
|
|
29695
|
+
if (onDelete !== undefined && onDelete !== "cascade") {
|
|
29696
|
+
throw new TypeError(
|
|
29697
|
+
`${relationLabel}: onDelete must be "cascade" or left out, got ${onDelete}.
|
|
29698
|
+
${relationLabel} source location: ${declarationSite}`,
|
|
29699
|
+
);
|
|
29700
|
+
}
|
|
29701
|
+
const cascade = onDelete === "cascade";
|
|
29702
|
+
if (cascade && ownerName) {
|
|
29703
|
+
// A scoped item lives in its owner's collection, not in a store this
|
|
29704
|
+
// relation could drop it from.
|
|
29705
|
+
throw new Error(
|
|
29706
|
+
`${relationLabel} cannot declare onDelete: "cascade": a "${name}" item exists only inside its "${ownerName}", and leaves through the "${ownerName}" callbacks.
|
|
29707
|
+
Read the "${ownerName}" again after the "${childResource.name}" is deleted (dependencies).
|
|
29708
|
+
${relationLabel} source location: ${declarationSite}`,
|
|
29709
|
+
);
|
|
29710
|
+
}
|
|
29674
29711
|
const childName = `${name}.${propertyName}`;
|
|
29675
29712
|
const childIdKey = childResource.idKey;
|
|
29676
29713
|
const childStore = childResource.store;
|
|
29714
|
+
// The id each parent points at, carried by the parent row (copies of a row
|
|
29715
|
+
// keep it, a spread does not): a removal is matched against the parents
|
|
29716
|
+
// the store holds at that moment, under the ids they have then.
|
|
29717
|
+
const childIdSymbol = cascade ? Symbol(`${childName} id`) : undefined;
|
|
29718
|
+
if (cascade) {
|
|
29719
|
+
childStore.observeRemovals((removedChildArray) => {
|
|
29720
|
+
const removedChildIdSet = new Set();
|
|
29721
|
+
for (const removedChild of removedChildArray) {
|
|
29722
|
+
removedChildIdSet.add(removedChild[childIdKey]);
|
|
29723
|
+
}
|
|
29724
|
+
const orphanIdArray = [];
|
|
29725
|
+
for (const parentItem of store.arraySignal.peek()) {
|
|
29726
|
+
if (removedChildIdSet.has(parentItem[childIdSymbol])) {
|
|
29727
|
+
orphanIdArray.push(parentItem[idKey]);
|
|
29728
|
+
}
|
|
29729
|
+
}
|
|
29730
|
+
if (orphanIdArray.length > 0) {
|
|
29731
|
+
store.drop(orphanIdArray);
|
|
29732
|
+
}
|
|
29733
|
+
});
|
|
29734
|
+
}
|
|
29677
29735
|
addItemSetup((item, answer) => {
|
|
29678
29736
|
const childItemIdSignal = signal();
|
|
29679
29737
|
const updateChildItemId = (value, answer) => {
|
|
@@ -29728,6 +29786,11 @@ const createResource = (
|
|
|
29728
29786
|
get: () => childItemFacadeSignal.value,
|
|
29729
29787
|
set: updateChildItemId,
|
|
29730
29788
|
});
|
|
29789
|
+
if (cascade) {
|
|
29790
|
+
Object.defineProperty(item, childIdSymbol, {
|
|
29791
|
+
get: () => childItemIdSignal.peek(),
|
|
29792
|
+
});
|
|
29793
|
+
}
|
|
29731
29794
|
debug$2(
|
|
29732
29795
|
`setup ${item}.${propertyName} is one "${childResource.name}" (current value: ${childItemSignal.peek()})`,
|
|
29733
29796
|
);
|
|
@@ -38153,10 +38216,12 @@ const barsOutside = (areaElement) => {
|
|
|
38153
38216
|
};
|
|
38154
38217
|
|
|
38155
38218
|
// Room around what a bar's picture holds for what paints past every box — a
|
|
38156
|
-
// shadow, the
|
|
38219
|
+
// shadow, the bar's own filter — which no API measures: a filter cuts whatever
|
|
38157
38220
|
// falls outside its region.
|
|
38158
38221
|
const WALL_FILTER_INK_ROOM = 100;
|
|
38159
38222
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
38223
|
+
// A blur() in a computed filter, its radius in px.
|
|
38224
|
+
const BLUR_FUNCTION_REGEXP = /blur\(([^)]*)\)/g;
|
|
38160
38225
|
|
|
38161
38226
|
// The bar's picture dimmed, not a rectangle over it: each wall's colour is
|
|
38162
38227
|
// flooded atop the bar's own pixels (Porter-Duff source-atop, which no blend
|
|
@@ -38184,11 +38249,17 @@ const dimBarPicture = (bar, sources) => {
|
|
|
38184
38249
|
const ownFilter = getComputedStyle(bar).filter;
|
|
38185
38250
|
const chain = ownFilter === "none" ? [] : [ownFilter];
|
|
38186
38251
|
for (const source of sources) {
|
|
38187
|
-
const backdropFilter =
|
|
38188
|
-
|
|
38189
|
-
|
|
38190
|
-
|
|
38191
|
-
chain.push(
|
|
38252
|
+
const backdropFilter = readBackdropFilter(source);
|
|
38253
|
+
if (backdropFilter !== "none") {
|
|
38254
|
+
// Blurred alone, the bar would fade to transparent over its edges and
|
|
38255
|
+
// show the live document through them: its blur stays inside its shape.
|
|
38256
|
+
chain.push(
|
|
38257
|
+
backdropFilter.replace(
|
|
38258
|
+
BLUR_FUNCTION_REGEXP,
|
|
38259
|
+
(_, radius) =>
|
|
38260
|
+
`url(#${createShapeBlurFilter(parseFloat(radius), region).id})`,
|
|
38261
|
+
),
|
|
38262
|
+
);
|
|
38192
38263
|
}
|
|
38193
38264
|
chain.push(`url(#${createWallFilter(source, region).id})`);
|
|
38194
38265
|
}
|
|
@@ -38197,7 +38268,59 @@ const dimBarPicture = (bar, sources) => {
|
|
|
38197
38268
|
dimmedBars.push(bar);
|
|
38198
38269
|
};
|
|
38199
38270
|
|
|
38271
|
+
// The wall's backdrop-filter as computed, so a blur radius written in em or
|
|
38272
|
+
// rem reads in px, as the wall resolves it where it paints.
|
|
38273
|
+
const readBackdropFilter = (source) => {
|
|
38274
|
+
const sourceStyle = getComputedStyle(source);
|
|
38275
|
+
const backdropFilter = sourceStyle
|
|
38276
|
+
.getPropertyValue("--backdrop-filter")
|
|
38277
|
+
.trim();
|
|
38278
|
+
if (!backdropFilter || backdropFilter === "none") {
|
|
38279
|
+
return "none";
|
|
38280
|
+
}
|
|
38281
|
+
const probe = document.createElement("div");
|
|
38282
|
+
probe.style.fontSize = sourceStyle.fontSize;
|
|
38283
|
+
probe.style.filter = backdropFilter;
|
|
38284
|
+
document.body.append(probe);
|
|
38285
|
+
const filter = getComputedStyle(probe).filter;
|
|
38286
|
+
probe.remove();
|
|
38287
|
+
return filter;
|
|
38288
|
+
};
|
|
38289
|
+
|
|
38200
38290
|
const createWallFilter = (source, region) => {
|
|
38291
|
+
const filter = createFilter(region);
|
|
38292
|
+
const flood = document.createElementNS(SVG_NS, "feFlood");
|
|
38293
|
+
flood.setAttribute("flood-color", readBackdropColor(source));
|
|
38294
|
+
const composite = document.createElementNS(SVG_NS, "feComposite");
|
|
38295
|
+
composite.setAttribute("in2", "SourceGraphic");
|
|
38296
|
+
composite.setAttribute("operator", "atop");
|
|
38297
|
+
filter.append(flood, composite);
|
|
38298
|
+
return filter;
|
|
38299
|
+
};
|
|
38300
|
+
|
|
38301
|
+
// Colour kept, alpha set to 1.
|
|
38302
|
+
const OPAQUE_MATRIX = "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 1";
|
|
38303
|
+
|
|
38304
|
+
// The wall's blur kept inside the bar. At rest the window is blurred as one
|
|
38305
|
+
// image; a CSS blur() on the bar sees only the bar, so its edges go half
|
|
38306
|
+
// transparent and its colour spreads past its box. Made opaque, each pixel is
|
|
38307
|
+
// the mean of the bar's own pixels around it (feColorMatrix works on
|
|
38308
|
+
// unpremultiplied colours), then cut back to the bar's own alpha.
|
|
38309
|
+
const createShapeBlurFilter = (stdDeviation, region) => {
|
|
38310
|
+
const filter = createFilter(region);
|
|
38311
|
+
const blur = document.createElementNS(SVG_NS, "feGaussianBlur");
|
|
38312
|
+
blur.setAttribute("stdDeviation", stdDeviation);
|
|
38313
|
+
const opaque = document.createElementNS(SVG_NS, "feColorMatrix");
|
|
38314
|
+
opaque.setAttribute("type", "matrix");
|
|
38315
|
+
opaque.setAttribute("values", OPAQUE_MATRIX);
|
|
38316
|
+
const clip = document.createElementNS(SVG_NS, "feComposite");
|
|
38317
|
+
clip.setAttribute("in2", "SourceAlpha");
|
|
38318
|
+
clip.setAttribute("operator", "in");
|
|
38319
|
+
filter.append(blur, opaque, clip);
|
|
38320
|
+
return filter;
|
|
38321
|
+
};
|
|
38322
|
+
|
|
38323
|
+
const createFilter = (region) => {
|
|
38201
38324
|
if (!wallFilterSvg) {
|
|
38202
38325
|
wallFilterSvg = document.createElementNS(SVG_NS, "svg");
|
|
38203
38326
|
wallFilterSvg.setAttribute(WALL_FILTERS_ATTRIBUTE, "");
|
|
@@ -38207,18 +38330,13 @@ const createWallFilter = (source, region) => {
|
|
|
38207
38330
|
wallFilterCount++;
|
|
38208
38331
|
const filter = document.createElementNS(SVG_NS, "filter");
|
|
38209
38332
|
filter.id = `navi-transition-wall-${wallFilterCount}`;
|
|
38210
|
-
// linearRGB, the default, floods another colour than the wall's
|
|
38333
|
+
// linearRGB, the default, floods another colour than the wall's and blurs
|
|
38334
|
+
// in another space than its backdrop-filter.
|
|
38211
38335
|
filter.setAttribute("color-interpolation-filters", "sRGB");
|
|
38212
38336
|
filter.setAttribute("x", region.x);
|
|
38213
38337
|
filter.setAttribute("y", region.y);
|
|
38214
38338
|
filter.setAttribute("width", region.width);
|
|
38215
38339
|
filter.setAttribute("height", region.height);
|
|
38216
|
-
const flood = document.createElementNS(SVG_NS, "feFlood");
|
|
38217
|
-
flood.setAttribute("flood-color", readBackdropColor(source));
|
|
38218
|
-
const composite = document.createElementNS(SVG_NS, "feComposite");
|
|
38219
|
-
composite.setAttribute("in2", "SourceGraphic");
|
|
38220
|
-
composite.setAttribute("operator", "atop");
|
|
38221
|
-
filter.append(flood, composite);
|
|
38222
38340
|
wallFilterSvg.appendChild(filter);
|
|
38223
38341
|
return filter;
|
|
38224
38342
|
};
|