Skip to content

Commit 711bec2

Browse files
Polish dev tool trigger
1 parent 55934bd commit 711bec2

3 files changed

Lines changed: 50 additions & 8 deletions

File tree

.claude/CLAUDE-KNOWLEDGE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,3 +366,15 @@ A: The `/api/v1/internal/metrics` response now intentionally includes `analytics
366366

367367
## Q: Why can environment config override writes fail with a product/product-line customer type warning after creating a preview project?
368368
A: The environment override endpoint validates the new environment override against the rendered branch config. Preview dummy payments data must therefore be internally coherent: products assigned to a product line need the same `customerType` as that product line, otherwise unrelated environment patches can fail with warnings like `Product "growth" has customer type "user" but its product line "workspace" has customer type "team"`.
369+
370+
## Q: How do you keep the Stack Auth dev tool from reopening automatically after navigation or reload?
371+
A: Treat `isOpen` as mount-local state in `packages/template/src/dev-tool/dev-tool-core.ts`: load persisted preferences with `isOpen: false`, and save state back to localStorage with `isOpen: false` so tab/size preferences persist without reopening the panel on the next mount.
372+
373+
## Q: How should the Stack Auth dev tool indicator avoid being hidden by other dev indicators?
374+
A: Do not dynamically reflow around framework indicators; that makes pointer interaction brittle. Keep the trigger anchored to its saved corner and give `.sdt-trigger` a max practical z-index (`2147483647`) so the Stack indicator renders above Next/Turbo overlays.
375+
376+
## Q: How should Stack Auth dev tool trigger movement feel?
377+
A: Dragging should remain instant/direct, but programmatic moves like snap-to-corner after drag, resize reposition, and post-measurement correction should use a short snappy left/top transition. In `dev-tool-core.ts`, toggle a dedicated animation class only for those programmatic updates and remove it shortly after.
378+
379+
## Q: How do you prevent duplicate Stack Auth dev tool indicators from multiple package/module instances?
380+
A: `createDevTool` in `packages/template/src/dev-tool/dev-tool-core.ts` should register a browser-wide singleton instance on `window` with an idempotent cleanup function, call any previous global cleanup before mounting, and remove leftover `#__stack-dev-tool-root` nodes as a fallback for older instances that did not register cleanup.

packages/template/src/dev-tool/dev-tool-core.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -420,13 +420,35 @@ function createTrigger(onClick: () => void): { element: HTMLElement; cleanup: ()
420420
} catch {}
421421
}
422422

423-
function applyPos(nextPos: Position) {
423+
let animationTimeout: number | null = null;
424+
425+
function setPositionAnimation(isAnimated: boolean) {
426+
if (animationTimeout !== null) {
427+
window.clearTimeout(animationTimeout);
428+
animationTimeout = null;
429+
}
430+
btn.classList.toggle('sdt-trigger-position-animated', isAnimated);
431+
if (isAnimated) {
432+
animationTimeout = window.setTimeout(() => {
433+
animationTimeout = null;
434+
btn.classList.remove('sdt-trigger-position-animated');
435+
}, 180);
436+
}
437+
}
438+
439+
function applyPos(nextPos: Position, options?: { animate?: boolean }) {
440+
setPositionAnimation(options?.animate === true);
424441
pos = nextPos;
425442
btn.style.left = pos.left + 'px';
426443
btn.style.top = pos.top + 'px';
427444
}
428445

429-
const btn = h('button', { className: 'sdt-trigger', 'aria-label': 'Toggle Stack Auth Dev Tools', title: 'Stack Auth Dev Tools' });
446+
const btn = h('button', {
447+
className: 'sdt-trigger',
448+
'aria-label': 'Toggle Stack Auth Dev Tools',
449+
'data-stack-devtool-trigger': 'true',
450+
title: 'Stack Auth Dev Tools',
451+
});
430452
const logoSpan = h('span', { className: 'sdt-trigger-logo' });
431453
setHtml(logoSpan, STACK_LOGO_SVG);
432454
btn.appendChild(logoSpan);
@@ -435,22 +457,23 @@ function createTrigger(onClick: () => void): { element: HTMLElement; cleanup: ()
435457
let pos = resolveTriggerPosition(placement, triggerSize, { width: window.innerWidth, height: window.innerHeight });
436458
applyPos(pos);
437459

460+
let dragState: { startX: number; startY: number; startLeft: number; startTop: number; didDrag: boolean } | null = null;
461+
438462
// After mount, measure the actual rendered size and re-snap if needed.
439463
requestAnimationFrame(() => {
440464
const rect = btn.getBoundingClientRect();
441465
if (rect.width > 0 && rect.height > 0) {
442466
triggerSize = { width: rect.width, height: rect.height };
443467
const measured = resolveTriggerPosition(placement, triggerSize, { width: window.innerWidth, height: window.innerHeight });
444468
if (measured.left !== pos.left || measured.top !== pos.top) {
445-
applyPos(measured);
469+
applyPos(measured, { animate: true });
446470
}
447471
}
448472
});
449473

450-
let dragState: { startX: number; startY: number; startLeft: number; startTop: number; didDrag: boolean } | null = null;
451-
452474
btn.addEventListener('pointerdown', (e) => {
453475
e.preventDefault();
476+
setPositionAnimation(false);
454477
btn.setPointerCapture(e.pointerId);
455478
dragState = { startX: e.clientX, startY: e.clientY, startLeft: pos.left, startTop: pos.top, didDrag: false };
456479
});
@@ -475,7 +498,7 @@ function createTrigger(onClick: () => void): { element: HTMLElement; cleanup: ()
475498
btn.releasePointerCapture(e.pointerId);
476499
if (ds.didDrag) {
477500
placement = getSnappedTriggerPlacement(pos, triggerSize, { width: window.innerWidth, height: window.innerHeight });
478-
applyPos(resolveTriggerPosition(placement, triggerSize, { width: window.innerWidth, height: window.innerHeight }));
501+
applyPos(resolveTriggerPosition(placement, triggerSize, { width: window.innerWidth, height: window.innerHeight }), { animate: true });
479502
savePlacement(placement);
480503
} else {
481504
onClick();
@@ -487,7 +510,7 @@ function createTrigger(onClick: () => void): { element: HTMLElement; cleanup: ()
487510
function onResize() {
488511
const resizedPos = resolveTriggerPosition(placement, triggerSize, { width: window.innerWidth, height: window.innerHeight });
489512
if (resizedPos.left !== pos.left || resizedPos.top !== pos.top) {
490-
applyPos(resizedPos);
513+
applyPos(resizedPos, { animate: true });
491514
}
492515
}
493516

@@ -496,6 +519,9 @@ function createTrigger(onClick: () => void): { element: HTMLElement; cleanup: ()
496519
return {
497520
element: btn,
498521
cleanup: () => {
522+
if (animationTimeout !== null) {
523+
window.clearTimeout(animationTimeout);
524+
}
499525
window.removeEventListener('resize', onResize);
500526
},
501527
};

packages/template/src/dev-tool/dev-tool-styles.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export const devToolCSS = `
5050
/* Trigger pill */
5151
.stack-devtool .sdt-trigger {
5252
position: fixed;
53-
z-index: 99999;
53+
z-index: 2147483647;
5454
display: flex;
5555
align-items: center;
5656
justify-content: center;
@@ -67,6 +67,10 @@ export const devToolCSS = `
6767
touch-action: none;
6868
}
6969
70+
.stack-devtool .sdt-trigger-position-animated {
71+
transition: left 0.14s cubic-bezier(0.2, 0.8, 0.2, 1), top 0.14s cubic-bezier(0.2, 0.8, 0.2, 1), background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
72+
}
73+
7074
.stack-devtool .sdt-trigger:hover {
7175
background: var(--sdt-bg-hover);
7276
border-color: var(--sdt-accent);

0 commit comments

Comments
 (0)