@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.
@@ -27535,7 +27535,11 @@ ${[idKey, ...uniqueKeys].join(", ")}`,
27535
27535
  * (docs/resource.md).
27536
27536
  */
27537
27537
  const upsertAnswer = (...args) => upsertWith(args, true);
27538
- const drop = (...args) => {
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
- `${name}.one("${propertyName}")`,
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 blur of a wall — which no API measures: a filter cuts whatever
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 = getComputedStyle(source)
38188
- .getPropertyValue("--backdrop-filter")
38189
- .trim();
38190
- if (backdropFilter && backdropFilter !== "none") {
38191
- chain.push(backdropFilter);
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
  };
@@ -41141,10 +41259,14 @@ const RouteTravel = ({
41141
41259
  const pages = pagesRef.current;
41142
41260
  const currentIndex = currentPageIndex(pages);
41143
41261
  // Where the page of a window stands: the address, or where it last was for an
41144
- // address the row has no place for.
41262
+ // address the row has no place for, while the row still has that column.
41145
41263
  const liveIndexRef = useRef(0);
41146
41264
  if (currentIndex !== -1) {
41147
41265
  liveIndexRef.current = currentIndex;
41266
+ } else if (liveIndexRef.current >= pages.length) {
41267
+ // Past the row's end the page stands in a column of its own, off screen: a
41268
+ // still row does not scroll, and nothing scrolls a row to an address it lacks.
41269
+ liveIndexRef.current = pages.length === 0 ? 0 : pages.length - 1;
41148
41270
  }
41149
41271
  const liveIndex = liveIndexRef.current;
41150
41272
  const latestRef = useRef(null);