@linxin666/dsh-pet 0.2.7 → 0.2.9

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/lib/client.js CHANGED
@@ -245,26 +245,29 @@ window.__ModuleLoader__.load({
245
245
  const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
246
246
  el.style.backgroundPosition = position(segment.from);
247
247
  if (reduceMotion || segment.from === segment.to) return;
248
- let raf = 0;
248
+ let timer = 0;
249
249
  let index = segment.from;
250
250
  let elapsed = 0;
251
251
  let last = performance.now();
252
- const tick = (ts) => {
253
- const delta = ts - last;
254
- last = ts;
252
+ const tick = () => {
253
+ const now = performance.now();
254
+ const delta = now - last;
255
+ last = now;
255
256
  elapsed += delta;
256
- const duration = decoration.durations[index] ?? 160;
257
+ const duration = decoration.durations[index] ?? 120;
257
258
  if (elapsed >= duration) {
258
- elapsed = 0;
259
- if (index < segment.to) index += 1;
260
- else if (decoration.loop) index = segment.from;
259
+ do {
260
+ elapsed -= duration;
261
+ if (index < segment.to) index += 1;
262
+ else if (decoration.loop) index = segment.from;
263
+ } while (elapsed >= duration);
261
264
  el.style.backgroundPosition = position(index);
262
265
  }
263
266
  if (!decoration.loop && index === segment.to) return;
264
- raf = requestAnimationFrame(tick);
267
+ timer = window.setTimeout(tick, Math.max(1, duration - elapsed));
265
268
  };
266
- raf = requestAnimationFrame(tick);
267
- return () => cancelAnimationFrame(raf);
269
+ timer = window.setTimeout(tick, 0);
270
+ return () => window.clearTimeout(timer);
268
271
  }, [
269
272
  shown,
270
273
  segmentKey,
@@ -1233,6 +1236,48 @@ window.__ModuleLoader__.load({
1233
1236
  }
1234
1237
  };
1235
1238
  //#endregion
1239
+ //#region src/client/ui-teardown.ts
1240
+ /**
1241
+ * Cross-bundle-instance teardown slot for the page-global pet root
1242
+ * (issue #785).
1243
+ *
1244
+ * A client bundle swap (HMR rebuilt frame, plugin update, duplicate
1245
+ * injection) runs a new apply body while the previous instance's fiber
1246
+ * may still be draining. Module state does not survive the swap, so a
1247
+ * closure guard only sees one apply body and the previous instance's
1248
+ * container keeps sitting on document.body: the page shows two pets
1249
+ * until a full refresh. The slot rides globalThis (which does survive)
1250
+ * so a re-apply can find the previous instance and unmount its React
1251
+ * root cleanly before mounting its own; the previous fiber's later
1252
+ * disposal stays a no-op through the idempotent teardowns.
1253
+ */
1254
+ const SLOT = Symbol.for("dsh-pet.client-ui-teardown");
1255
+ /**
1256
+ * Claim the page-global pet UI slot with the current instance's teardown
1257
+ * (React root unmount + container removal + poll stop).
1258
+ * @param teardown - what a later instance runs to take the slot over.
1259
+ * @returns a disposer that clears the slot when the current instance's
1260
+ * UI is torn down (settings toggle, takeover, or fiber disposal).
1261
+ */
1262
+ function registerPetUiTeardown(teardown) {
1263
+ const slot = globalThis;
1264
+ slot[SLOT] = teardown;
1265
+ return () => {
1266
+ if (slot[SLOT] === teardown) slot[SLOT] = void 0;
1267
+ };
1268
+ }
1269
+ /**
1270
+ * Run the previous instance's teardown if one is registered, so the
1271
+ * re-applying instance becomes the sole owner of the page-global pet
1272
+ * root. No-op when the previous fiber already tore down cleanly.
1273
+ */
1274
+ function takeoverPetUiTeardown() {
1275
+ const slot = globalThis;
1276
+ const teardown = slot[SLOT];
1277
+ slot[SLOT] = void 0;
1278
+ teardown?.();
1279
+ }
1280
+ //#endregion
1236
1281
  //#region \0dsh-css:packages/dsh-pet/src/client/settings-card.module.css.mjs
1237
1282
  const css$1 = ".kKk9aW_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.kKk9aW_card:hover{border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.kKk9aW_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.kKk9aW_headerStatic{border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.kKk9aW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.kKk9aW_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.kKk9aW_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.kKk9aW_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_chevronOpen{transform:rotate(180deg)}.kKk9aW_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.kKk9aW_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.kKk9aW_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.kKk9aW_discard,.kKk9aW_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.kKk9aW_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.kKk9aW_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.kKk9aW_discard:disabled,.kKk9aW_save:disabled{opacity:.4;cursor:default}.kKk9aW_discard:focus-visible,.kKk9aW_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.kKk9aW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.kKk9aW_field+.kKk9aW_field{border-top:1px solid var(--dsw-alias-border-l2)}.kKk9aW_head{align-items:center;gap:8px;display:flex}.kKk9aW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.kKk9aW_badges{align-items:center;gap:8px;display:inline-flex}.kKk9aW_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.kKk9aW_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.kKk9aW_reset:disabled{cursor:default}.kKk9aW_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.kKk9aW_input,.kKk9aW_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_input:focus-visible,.kKk9aW_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.kKk9aW_input:disabled,.kKk9aW_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.kKk9aW_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.kKk9aW_selectWrap{position:relative}.kKk9aW_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.kKk9aW_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.kKk9aW_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_selectChevronOpen{transform:rotate(180deg)}.kKk9aW_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.kKk9aW_selectPopupOpen{opacity:1;transform:none}.kKk9aW_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.kKk9aW_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;flex-shrink:0;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.kKk9aW_selectOption:hover,.kKk9aW_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.kKk9aW_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.kKk9aW_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.kKk9aW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.kKk9aW_card,.kKk9aW_header,.kKk9aW_chevron,.kKk9aW_chevronOpen,.kKk9aW_discard,.kKk9aW_save,.kKk9aW_selectChevron,.kKk9aW_selectChevronOpen,.kKk9aW_selectPopup{transition:none}}";
1238
1283
  const tagId$1 = "@linxin666/dsh-pet/settings-card.module.css";
@@ -2488,8 +2533,18 @@ window.__ModuleLoader__.load({
2488
2533
  };
2489
2534
  });
2490
2535
  let disposeUi;
2536
+ let clearUiTeardown;
2537
+ let uiDead = false;
2538
+ const killUi = () => {
2539
+ if (uiDead) return;
2540
+ uiDead = true;
2541
+ clearUiTeardown?.();
2542
+ clearUiTeardown = void 0;
2543
+ disposeUi?.();
2544
+ disposeUi = void 0;
2545
+ };
2491
2546
  const syncUi = () => {
2492
- if (enabled() && disposeUi === void 0) {
2547
+ if (!uiDead && enabled() && disposeUi === void 0) {
2493
2548
  const petStore = createPetStore().create();
2494
2549
  const setSnapshot = petStore.actions.setSnapshot;
2495
2550
  const setPets = petStore.actions.setPets;
@@ -2590,6 +2645,8 @@ window.__ModuleLoader__.load({
2590
2645
  setFeedback(null);
2591
2646
  }
2592
2647
  });
2648
+ takeoverPetUiTeardown();
2649
+ for (const stale of Array.from(document.querySelectorAll("div[data-dsh-pet-root]"))) stale.remove();
2593
2650
  const container = document.createElement("div");
2594
2651
  container.dataset.dshPetRoot = "";
2595
2652
  container.dataset.dshPlugin = "pet";
@@ -2599,18 +2656,31 @@ window.__ModuleLoader__.load({
2599
2656
  ...injected(),
2600
2657
  t
2601
2658
  }));
2659
+ let uiGone = false;
2602
2660
  disposeUi = () => {
2661
+ if (uiGone) return;
2662
+ uiGone = true;
2663
+ clearUiTeardown?.();
2664
+ clearUiTeardown = void 0;
2603
2665
  petRoot.unmount();
2604
2666
  container.remove();
2605
2667
  disposePoll();
2606
2668
  disposeUi = void 0;
2607
2669
  };
2608
- } else if (!enabled() && disposeUi !== void 0) {
2670
+ clearUiTeardown = registerPetUiTeardown(() => {
2671
+ uiDead = true;
2672
+ disposeUi?.();
2673
+ });
2674
+ } else if (!uiDead && !enabled() && disposeUi !== void 0) {
2609
2675
  disposeUi();
2610
2676
  disposeUi = void 0;
2611
2677
  }
2612
2678
  };
2613
- settingsScope.subscribe(syncUi);
2679
+ const unsubscribeSettings = settingsScope.subscribe(syncUi);
2680
+ ctx.effect(() => () => {
2681
+ unsubscribeSettings();
2682
+ killUi();
2683
+ }, "pet: client lifecycle");
2614
2684
  syncUi();
2615
2685
  }
2616
2686
  //#endregion