Skip to content

chore: release 0.3.0#1

Merged
barryking merged 2 commits into
mainfrom
feat/simplification-re-architecture
Mar 7, 2026
Merged

chore: release 0.3.0#1
barryking merged 2 commits into
mainfrom
feat/simplification-re-architecture

Conversation

@barryking

@barryking barryking commented Mar 7, 2026

Copy link
Copy Markdown
Owner

Summary

This PR re-architects Torchlit’s internals to make torchlit/service a pure, DOM-free core while preserving the documented public import paths:

  • torchlit
  • torchlit/service
  • torchlit/overlay

It also bumps the package version to 0.3.0 to reflect the service-layer architecture and type-surface changes.

What changed

  • Extracted the pure service layer into src/core
  • Moved DOM-specific target lookup, positioning, and scroll logic into src/dom
  • overlay-specific focus and step orchestration into src/overlay
  • Reduced src/tour-overlay.ts to a thinner composition/rendering layer
  • Kept stable public entrypoints and compatibility exports
  • Added regression and declaration-surface tests
  • Bumped version from 0.2.3 to 0.3.0

#Notable API / type changes

  • torchlit/service is now DOM-free in runtime code and generated declarations
  • Core TourSnapshot no longer includes targetElement or targetRect
  • DOM target resolution is no longer part of the supported service contract
  • Root torchlit continues to expose overlay-oriented types for compatibility

Update

This PR now also includes a small follow-up pass based on review feedback:

  • aligned vitest with @vitest/coverage-v8
  • removed the unsafe ShadowRoot cast in deepQuery
  • replaced remaining layout magic numbers with named constants
  • hardened positioning tests to derive expectations from shared constants
  • updated SECURITY.md to mark 0.3.x as the supported release line

Copilot AI review requested due to automatic review settings March 7, 2026 12:34
@coderabbitai

coderabbitai Bot commented Mar 7, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Restructures the tour library: extracts core service into src/core, adds DOM and overlay utilities (positioning, scroll manager, deepQuery, focus manager, step runner), refactors TourOverlay to use the new orchestrator, updates exports, bumps version to 0.3.0, and expands tests and CI coverage config.

Changes

Cohort / File(s) Summary
Version & Config
package.json, vitest.config.ts, .gitignore
Bump to 0.3.0; add Vitest v8 coverage config; add coverage/ to .gitignore.
Documentation
README.md, SECURITY.md
Rewrite README project structure and wording (DOM-free); update supported versions in SECURITY.md.
Core Module
src/core/tour-service.ts, src/core/types.ts
Add generic TourService<TStep> implementation and core types (storage, config, definitions, snapshots, listeners).
DOM Utilities
src/dom/deep-query.ts, src/dom/positioning.ts, src/dom/scroll-manager.ts, src/dom/target-resolver.ts
New DOM helpers: deepQuery (supports ShadowRoot), tooltip positioning, scroll-and-settle + restore, and target resolution/waiting.
Overlay Utilities
src/overlay/focus-manager.ts, src/overlay/step-runner.ts, src/overlay/types.ts
Add FocusManager, StepRunner orchestration (auto-advance, beforeShow, target resolution), and ResolvedTourSnapshot type.
API / Export Changes
src/index.ts, src/tour-service.ts, src/types.ts, src/utils/deep-query.ts
Re-export core service/types from core/; move deepQuery implementation to dom/ and re-export; simplify TourStep/TourDefinition surface and re-export legacy types from core/types.
Overlay Refactor
src/tour-overlay.ts
Refactor TorchlitOverlay to use StepRunner and FocusManager, centralize positioning logic, route-change dispatch, teardown handling, and resolved snapshot flow.
Tests
test/*.test.ts (new/updated)
Add declaration-surface, positioning, step-runner tests; update deep-query, tour-overlay, and tour-service tests to reflect new APIs and behaviors.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant UI as Client/UI
participant Overlay as TorchlitOverlay
participant Runner as StepRunner
participant Service as TourService
participant Resolver as TargetResolver
participant Scroll as ScrollManager

UI->>Overlay: user action / service event
Overlay->>Service: subscribe / request snapshot
Service-->>Overlay: TourSnapshot
Overlay->>Runner: prepareStep(snapshot)
Runner->>Service: getTour(snapshot.tourId)
Runner->>Resolver: waitForTarget(step.target)
Resolver-->>Runner: targetElement / null
Runner->>Scroll: scrollAndSettle(targetElement, placement)
Scroll-->>Runner: settled
Runner-->>Overlay: ResolvedTourSnapshot
Overlay->>Overlay: render tooltip / manage focus

Note: colored rectangles not required for this simple flow.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hopping through modules bright and neat,

Core and DOM now tidy, fleet.
Steps resolved, focus kept in sight,
Tooltips land and tests take flight—
Version three, I clap my paws with delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'chore: release 0.3.0' accurately reflects the main purpose of the PR—a version bump and release, which aligns with the extensive refactoring and API changes documented in the PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/simplification-re-architecture

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
package.json (1)

73-78: ⚠️ Potential issue | 🟠 Major

Align @vitest/coverage-v8 and vitest versions.

@vitest/coverage-v8@3.2.4 and vitest@3.0.0 have a version mismatch. The Vitest coverage plugin maintains strict peer dependencies with the main vitest package and requires closely aligned versions. Update vitest to at least ^3.2.4 to match @vitest/coverage-v8, or vice versa, to avoid runtime compatibility issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 73 - 78, The dependency versions for
`@vitest/coverage-v8` and vitest are misaligned; update one so they match (e.g.,
set "vitest" to "^3.2.4" to align with "@vitest/coverage-v8": "^3.2.4" or
downgrade the coverage package to the vitest version), then run your package
manager to update the lockfile and reinstall, and run the test suite to verify
compatibility; target the package.json entries for "@vitest/coverage-v8" and
"vitest" when making the change.
🧹 Nitpick comments (8)
src/dom/deep-query.ts (1)

11-14: Consider using ShadowRoot type instead of casting to Document.

The cast as unknown as Document works but obscures the actual type. ShadowRoot has querySelector and can be used directly with a union type.

♻️ Suggested type refinement
 export function deepQuery(
   selector: string,
-  root: Element | Document = document.body,
+  root: Element | Document | ShadowRoot = document.body,
 ): Element | null {
   const found = root.querySelector(selector);
   if (found) return found;

   const children = root.querySelectorAll('*');
   for (const element of children) {
     if (element.shadowRoot) {
-      const shadowResult = deepQuery(
-        selector,
-        element.shadowRoot as unknown as Document,
-      );
+      const shadowResult = deepQuery(selector, element.shadowRoot);
       if (shadowResult) return shadowResult;
     }
   }

   return null;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dom/deep-query.ts` around lines 11 - 14, The code is casting
element.shadowRoot to Document for deepQuery; instead use the actual ShadowRoot
type and update deepQuery's parameter types to accept both Document and
ShadowRoot. Change the call to pass element.shadowRoot as ShadowRoot (or guard
with if (element.shadowRoot) ...) and modify deepQuery's signature (and any
related overloads) to accept Document | ShadowRoot so querySelector is correctly
typed for shadowResult and no unsafe cast is needed.
src/dom/positioning.ts (1)

69-77: Consider extracting magic number 80 as a named constant.

The value 80 appears twice (lines 71 and 76) for vertical centering offset in left/right placements. A named constant like TOOLTIP_VERTICAL_OFFSET would improve readability and maintainability.

♻️ Proposed refactor
 export const TOOLTIP_W = 320;
 export const TOOLTIP_H_MAX = 270;
 export const GAP = 16;
 export const VIEWPORT_MARGIN = 24;
+const TOOLTIP_VERTICAL_OFFSET = 80;

 // ... in getTooltipPosition:
     case 'right':
       return {
-        top: visibleCenterY - 80,
+        top: visibleCenterY - TOOLTIP_VERTICAL_OFFSET,
         left: rect.right + spotlightPadding + GAP,
       };
     case 'left':
       return {
-        top: visibleCenterY - 80,
+        top: visibleCenterY - TOOLTIP_VERTICAL_OFFSET,
         left: rect.left - spotlightPadding - GAP - TOOLTIP_W,
       };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dom/positioning.ts` around lines 69 - 77, The vertical offset value 80
used in the 'right' and 'left' placement cases should be extracted to a named
constant (e.g., TOOLTIP_VERTICAL_OFFSET) to avoid magic numbers; replace both
occurrences in the positioning logic where top is computed from visibleCenterY
(currently "visibleCenterY - 80") with "visibleCenterY -
TOOLTIP_VERTICAL_OFFSET", declare the constant near the other layout constants
(alongside TOOLTIP_W, GAP, spotlightPadding) and ensure any exports or local
scope match the file's pattern.
src/dom/scroll-manager.ts (1)

20-36: Consider that fitsInViewport check uses pre-scroll rect position.

The fitsInViewport check on line 23 uses rect obtained before any scrolling occurs (line 20). This should be fine since we're checking if the element's height fits within the viewport (which doesn't change with scrolling), but the variable name rect might be slightly misleading as it represents the initial position.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dom/scroll-manager.ts` around lines 20 - 36, The pre-scroll bounding rect
variable is misleadingly named `rect` in the scroll logic; rename it to
`initialRect` or `elementRect` (the result of element.getBoundingClientRect())
and add a short comment near fitsInViewport usage to clarify this is the
pre-scroll measurement used only to check element height vs viewport, leaving
the rest of logic (scrollIntoView, desiredTop calculation using `placement`,
`TOOLTIP_H_MAX`, `GAP`, `spotlightPadding`, and window.scrollTo) unchanged so
it’s clear you aren’t relying on a post-scroll rect.
src/core/tour-service.ts (1)

144-159: Consider whether onSkip should be called before clearing activeTourId.

Currently tour?.onSkip?.() is called on line 158 after activeTourId is set to null (line 155) and notify() is called (line 157). If the onSkip callback queries isActive() or getSnapshot(), it will see the tour as already inactive. This may or may not be the intended behavior—verify if callbacks should have access to the tour state before it's cleared.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/tour-service.ts` around lines 144 - 159, The skipTour method
currently invokes tour?.onSkip?.() after clearing activeTourId, resetting
currentStepIndex and calling notify(), so any onSkip callback that calls
isActive() or getSnapshot() will observe the tour as inactive; move the
tour?.onSkip?.() invocation to before clearing activeTourId/currentStepIndex and
before notify(), ensuring the callback can see the active state, while keeping
the logic that pushes id into persistedState.dismissed, calls saveState(), then
calls onSkip, then clears activeTourId and currentStepIndex and finally calls
notify(); update the skipTour function (referenced by name) accordingly.
test/positioning.test.ts (1)

61-67: Test assertions rely on specific constant values.

The expected values { top: 166, left: 100 } and { top: 24, left: 680 } are derived from the positioning constants. If GAP, TOOLTIP_W, VIEWPORT_MARGIN, or TOOLTIP_H_MAX change, these tests will fail. Consider documenting the calculation or importing constants to make the relationship explicit.

For reference:

  • top: 166 = 100 (rect.top) + 40 (rect.height) + 10 (padding) + 16 (GAP) = 166 ✓
  • left: 100 = 200 + 60 - 160 = 100 ✓
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/positioning.test.ts` around lines 61 - 67, Replace hard-coded expected
numbers with values computed from the same positioning constants so tests don’t
break when GAP/TOOLTIP_W/VIEWPORT_MARGIN/TOOLTIP_H_MAX change: in the test file
import the relevant constants (e.g., GAP, TOOLTIP_W, VIEWPORT_MARGIN,
TOOLTIP_H_MAX) and compute expectedPosition using the same math used by
getTooltipPosition (e.g., top = rect.top + rect.height + padding + GAP; left =
rect.left + rect.width/2 - TOOLTIP_W/2) and compute expectedClamped by applying
clampToViewport to the computed coordinates (or using VIEWPORT_MARGIN and
TOOLTIP_W/TOOLTIP_H_MAX to derive the clamped values), then assert against those
computed expectations instead of literal numbers.
test/step-runner.test.ts (1)

58-62: Address Biome lint warning for forEach callback.

The static analysis tool flags that the forEach callback should not return a value. While element.remove() returns void, adding explicit braces clarifies intent and silences the linter.

🔧 Proposed fix
 afterEach(() => {
-  cleanup.forEach(element => element.remove());
+  cleanup.forEach(element => { element.remove(); });
   cleanup.length = 0;
   vi.restoreAllMocks();
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/step-runner.test.ts` around lines 58 - 62, The linter warns about the
arrow callback in the afterEach block returning a value; update the afterEach
cleanup to use an explicit block-bodied callback so the forEach callback does
not implicitly return anything — e.g., replace the current
cleanup.forEach(element => element.remove()) call in the afterEach function with
a block-bodied callback that calls element.remove(); (keep the rest of the
teardown, including cleanup.length = 0 and vi.restoreAllMocks()).
test/tour-overlay.test.ts (1)

68-72: Address Biome lint warning for forEach callback.

Same pattern as in test/step-runner.test.ts. Adding explicit braces clarifies intent.

🔧 Proposed fix
 afterEach(() => {
-  cleanup.forEach(element => element.remove());
+  cleanup.forEach(element => { element.remove(); });
   cleanup.length = 0;
   vi.restoreAllMocks();
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/tour-overlay.test.ts` around lines 68 - 72, The Biome lint warning is
caused by the concise arrow callback used in the afterEach cleanup; update the
afterEach block so the forEach callback uses an explicit block body instead of
an expression. Locate the afterEach function (symbol: afterEach) and change the
cleanup.forEach callback to use braces (i.e., element => { element.remove(); })
while keeping the subsequent cleanup.length = 0 and vi.restoreAllMocks() lines
unchanged.
src/overlay/step-runner.ts (1)

93-111: Consider extracting magic numbers to named constants.

The values 270, 32, and 16 appear to represent tooltip dimensions and spacing but are not self-documenting. Consider extracting them to named constants for clarity and consistency with the GAP/VIEWPORT_MARGIN constants used elsewhere in the codebase.

+const TOOLTIP_HEIGHT_ESTIMATE = 270;
+const TOOLTIP_PADDING = 32;
+const TOOLTIP_MARGIN = 16;
+
 private shouldScrollIntoView(snapshot: ResolvedTourSnapshot): boolean {
   const rect = snapshot.targetRect;
   if (!rect) return false;

   const viewportHeight = window.innerHeight;
   const fits =
-    rect.height + 270 + 32 < viewportHeight;
+    rect.height + TOOLTIP_HEIGHT_ESTIMATE + TOOLTIP_PADDING < viewportHeight;
   const inView = fits
     ? rect.top >= 0 &&
       rect.bottom <= viewportHeight &&
       rect.left >= 0 &&
       rect.right <= window.innerWidth
     : snapshot.step.placement === 'top'
-      ? rect.top >= 270 + 16 + this.options.spotlightPadding &&
+      ? rect.top >= TOOLTIP_HEIGHT_ESTIMATE + TOOLTIP_MARGIN + this.options.spotlightPadding &&
         rect.top < viewportHeight
       : rect.top >= 0 && rect.top < viewportHeight;

   return !inView;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/overlay/step-runner.ts` around lines 93 - 111, In shouldScrollIntoView,
replace the magic numbers 270, 32, and 16 with descriptive named constants
(e.g., TOOLTIP_HEIGHT, TOOLTIP_MARGIN/BOTTOM_PADDING, SPOTLIGHT_TOP_OFFSET or
reuse existing GAP/VIEWPORT_MARGIN constants) declared near the top of the
module so their meaning is clear; update the calculations (the fits check and
the top comparisons) to use those constants instead of raw literals and ensure
any combined expressions (like 270 + 16 + this.options.spotlightPadding) are
expressed with named constants to improve readability and maintain consistency
with other constants used in the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/dom/positioning.ts`:
- Around line 84-88: The 'top' placement currently sets top to rect.top -
spotlightPadding - GAP which positions the tooltip's top edge too low; update
the calculation in the switch case handling 'top' so top = rect.top -
spotlightPadding - GAP - TOOLTIP_H_MAX (i.e., subtract TOOLTIP_H_MAX), keeping
the left logic unchanged; this aligns with how clampToViewport reserves space
for TOOLTIP_H_MAX and avoids overlap with the target (refer to TOOLTIP_W,
TOOLTIP_H_MAX, spotlightPadding, GAP, and the clampToViewport usage).

---

Outside diff comments:
In `@package.json`:
- Around line 73-78: The dependency versions for `@vitest/coverage-v8` and vitest
are misaligned; update one so they match (e.g., set "vitest" to "^3.2.4" to
align with "@vitest/coverage-v8": "^3.2.4" or downgrade the coverage package to
the vitest version), then run your package manager to update the lockfile and
reinstall, and run the test suite to verify compatibility; target the
package.json entries for "@vitest/coverage-v8" and "vitest" when making the
change.

---

Nitpick comments:
In `@src/core/tour-service.ts`:
- Around line 144-159: The skipTour method currently invokes tour?.onSkip?.()
after clearing activeTourId, resetting currentStepIndex and calling notify(), so
any onSkip callback that calls isActive() or getSnapshot() will observe the tour
as inactive; move the tour?.onSkip?.() invocation to before clearing
activeTourId/currentStepIndex and before notify(), ensuring the callback can see
the active state, while keeping the logic that pushes id into
persistedState.dismissed, calls saveState(), then calls onSkip, then clears
activeTourId and currentStepIndex and finally calls notify(); update the
skipTour function (referenced by name) accordingly.

In `@src/dom/deep-query.ts`:
- Around line 11-14: The code is casting element.shadowRoot to Document for
deepQuery; instead use the actual ShadowRoot type and update deepQuery's
parameter types to accept both Document and ShadowRoot. Change the call to pass
element.shadowRoot as ShadowRoot (or guard with if (element.shadowRoot) ...) and
modify deepQuery's signature (and any related overloads) to accept Document |
ShadowRoot so querySelector is correctly typed for shadowResult and no unsafe
cast is needed.

In `@src/dom/positioning.ts`:
- Around line 69-77: The vertical offset value 80 used in the 'right' and 'left'
placement cases should be extracted to a named constant (e.g.,
TOOLTIP_VERTICAL_OFFSET) to avoid magic numbers; replace both occurrences in the
positioning logic where top is computed from visibleCenterY (currently
"visibleCenterY - 80") with "visibleCenterY - TOOLTIP_VERTICAL_OFFSET", declare
the constant near the other layout constants (alongside TOOLTIP_W, GAP,
spotlightPadding) and ensure any exports or local scope match the file's
pattern.

In `@src/dom/scroll-manager.ts`:
- Around line 20-36: The pre-scroll bounding rect variable is misleadingly named
`rect` in the scroll logic; rename it to `initialRect` or `elementRect` (the
result of element.getBoundingClientRect()) and add a short comment near
fitsInViewport usage to clarify this is the pre-scroll measurement used only to
check element height vs viewport, leaving the rest of logic (scrollIntoView,
desiredTop calculation using `placement`, `TOOLTIP_H_MAX`, `GAP`,
`spotlightPadding`, and window.scrollTo) unchanged so it’s clear you aren’t
relying on a post-scroll rect.

In `@src/overlay/step-runner.ts`:
- Around line 93-111: In shouldScrollIntoView, replace the magic numbers 270,
32, and 16 with descriptive named constants (e.g., TOOLTIP_HEIGHT,
TOOLTIP_MARGIN/BOTTOM_PADDING, SPOTLIGHT_TOP_OFFSET or reuse existing
GAP/VIEWPORT_MARGIN constants) declared near the top of the module so their
meaning is clear; update the calculations (the fits check and the top
comparisons) to use those constants instead of raw literals and ensure any
combined expressions (like 270 + 16 + this.options.spotlightPadding) are
expressed with named constants to improve readability and maintain consistency
with other constants used in the file.

In `@test/positioning.test.ts`:
- Around line 61-67: Replace hard-coded expected numbers with values computed
from the same positioning constants so tests don’t break when
GAP/TOOLTIP_W/VIEWPORT_MARGIN/TOOLTIP_H_MAX change: in the test file import the
relevant constants (e.g., GAP, TOOLTIP_W, VIEWPORT_MARGIN, TOOLTIP_H_MAX) and
compute expectedPosition using the same math used by getTooltipPosition (e.g.,
top = rect.top + rect.height + padding + GAP; left = rect.left + rect.width/2 -
TOOLTIP_W/2) and compute expectedClamped by applying clampToViewport to the
computed coordinates (or using VIEWPORT_MARGIN and TOOLTIP_W/TOOLTIP_H_MAX to
derive the clamped values), then assert against those computed expectations
instead of literal numbers.

In `@test/step-runner.test.ts`:
- Around line 58-62: The linter warns about the arrow callback in the afterEach
block returning a value; update the afterEach cleanup to use an explicit
block-bodied callback so the forEach callback does not implicitly return
anything — e.g., replace the current cleanup.forEach(element =>
element.remove()) call in the afterEach function with a block-bodied callback
that calls element.remove(); (keep the rest of the teardown, including
cleanup.length = 0 and vi.restoreAllMocks()).

In `@test/tour-overlay.test.ts`:
- Around line 68-72: The Biome lint warning is caused by the concise arrow
callback used in the afterEach cleanup; update the afterEach block so the
forEach callback uses an explicit block body instead of an expression. Locate
the afterEach function (symbol: afterEach) and change the cleanup.forEach
callback to use braces (i.e., element => { element.remove(); }) while keeping
the subsequent cleanup.length = 0 and vi.restoreAllMocks() lines unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: da87776b-f2d9-483e-bf14-b68dd4707864

📥 Commits

Reviewing files that changed from the base of the PR and between e655101 and a478b6c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (24)
  • .gitignore
  • README.md
  • package.json
  • src/core/tour-service.ts
  • src/core/types.ts
  • src/dom/deep-query.ts
  • src/dom/positioning.ts
  • src/dom/scroll-manager.ts
  • src/dom/target-resolver.ts
  • src/index.ts
  • src/overlay/focus-manager.ts
  • src/overlay/step-runner.ts
  • src/overlay/types.ts
  • src/tour-overlay.ts
  • src/tour-service.ts
  • src/types.ts
  • src/utils/deep-query.ts
  • test/declaration-surface.test.ts
  • test/deep-query.test.ts
  • test/positioning.test.ts
  • test/step-runner.test.ts
  • test/tour-overlay.test.ts
  • test/tour-service.test.ts
  • vitest.config.ts

Comment thread src/dom/positioning.ts
Comment on lines +84 to +88
case 'top':
return {
top: rect.top - spotlightPadding - GAP,
left: rect.left + rect.width / 2 - TOOLTIP_W / 2,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's CSS that positions the tooltip from bottom edge for 'top' placement
rg -n "arrow-top|placement.*top" --type css --type ts -A 3 -B 3

Repository: barryking/torchlit

Length of output: 2656


🏁 Script executed:

sed -n '80,105p' src/dom/positioning.ts

Repository: barryking/torchlit

Length of output: 787


Subtract TOOLTIP_H_MAX from top placement calculation.

The top coordinate for top placement should account for the tooltip's height. The current code rect.top - spotlightPadding - GAP positions the tooltip's top edge at that coordinate, causing it to overlap the target. The clampToViewport function treats pos.top as the tooltip's top edge and reserves space for TOOLTIP_H_MAX below it, confirming the tooltip height must be subtracted in the initial positioning.

Suggested fix
    case 'top':
      return {
-       top: rect.top - spotlightPadding - GAP,
+       top: rect.top - spotlightPadding - GAP - TOOLTIP_H_MAX,
        left: rect.left + rect.width / 2 - TOOLTIP_W / 2,
      };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'top':
return {
top: rect.top - spotlightPadding - GAP,
left: rect.left + rect.width / 2 - TOOLTIP_W / 2,
};
case 'top':
return {
top: rect.top - spotlightPadding - GAP - TOOLTIP_H_MAX,
left: rect.left + rect.width / 2 - TOOLTIP_W / 2,
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dom/positioning.ts` around lines 84 - 88, The 'top' placement currently
sets top to rect.top - spotlightPadding - GAP which positions the tooltip's top
edge too low; update the calculation in the switch case handling 'top' so top =
rect.top - spotlightPadding - GAP - TOOLTIP_H_MAX (i.e., subtract
TOOLTIP_H_MAX), keeping the left logic unchanged; this aligns with how
clampToViewport reserves space for TOOLTIP_H_MAX and avoids overlap with the
target (refer to TOOLTIP_W, TOOLTIP_H_MAX, spotlightPadding, GAP, and the
clampToViewport usage).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR releases Torchlit 0.3.0 by restructuring the library into a DOM-free core service (src/core) with DOM/overlay behavior moved into dedicated modules, while keeping the documented public entrypoints (torchlit, torchlit/service, torchlit/overlay).

Changes:

  • Extracted the headless tour state engine into src/core and moved DOM-specific behaviors into src/dom and src/overlay.
  • Refactored TorchlitOverlay to compose new StepRunner, FocusManager, and DOM helpers instead of owning all orchestration logic.
  • Added/updated Vitest coverage + new regression/type-surface tests; bumped version to 0.3.0.

Reviewed changes

Copilot reviewed 23 out of 25 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
vitest.config.ts Enables v8 coverage reporting for src/** and outputs text/lcov.
test/tour-service.test.ts Updates service tests to run in Node and validate DOM-free behavior + persistence via injected storage.
test/tour-overlay.test.ts Updates overlay regressions for new orchestration (service attach, scroll restore, cached targets).
test/step-runner.test.ts Adds unit tests for step preparation (beforeShow, route events, lazy targets, auto-advance).
test/positioning.test.ts Adds unit tests for extracted positioning helpers.
test/deep-query.test.ts Updates deepQuery tests to new module location and expands shadow DOM coverage.
test/declaration-surface.test.ts Asserts torchlit/service declarations don’t reference lit.
src/utils/deep-query.ts Keeps a compatibility re-export to the new DOM deepQuery implementation.
src/types.ts Narrows “public” types by re-exporting core service types and keeping overlay-facing step/definition types.
src/tour-service.ts Converts torchlit/service entrypoint to a thin re-export of the new core service.
src/tour-overlay.ts Refactors overlay into a thinner composition layer using new DOM + overlay helper modules.
src/overlay/types.ts Introduces ResolvedTourSnapshot (core snapshot + tour + resolved DOM target info).
src/overlay/step-runner.ts Adds step orchestration (hooks, route event, lazy targets, scroll-into-view decisions).
src/overlay/focus-manager.ts Extracts focus capture/restore and focus trap logic from the overlay.
src/index.ts Keeps root barrel exports stable while pointing deepQuery at the new module.
src/dom/target-resolver.ts Adds DOM target lookup + lazy target waiting via MutationObserver.
src/dom/scroll-manager.ts Extracts scroll-into-view and end-of-tour scroll restoration logic.
src/dom/positioning.ts Extracts placement, tooltip positioning, viewport clamping, and arrow positioning helpers.
src/dom/deep-query.ts Moves deepQuery into DOM module.
package.json Bumps version to 0.3.0 and adds @vitest/coverage-v8.
pnpm-lock.yaml Locks @vitest/coverage-v8 and related dependency graph changes.
README.md Updates docs to describe the new core/dom/overlay structure and DOM-free service entrypoint.
.gitignore Adds coverage/ output directory.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/core/types.ts
name: string;
trigger: 'first-visit' | 'manual';
steps: TStep[];
loop?: boolean;

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

core/types TourDefinition is missing onEndScroll, but the overlay reads tour.onEndScroll (e.g. in TorchlitOverlay.handleTourChange) and the public root types (src/types.ts) still define onEndScroll on TourDefinition. As-is, TypeScript users calling createTourService().register({ ... , onEndScroll: 'top' }) will hit excess-property errors even though the overlay supports the feature. Consider adding onEndScroll?: 'restore' | 'top' | 'none' to the core TourDefinition (it’s DOM-free) or otherwise widening the service’s accepted tour definition type so the root TourDefinition is assignable without casts.

Suggested change
loop?: boolean;
loop?: boolean;
onEndScroll?: 'restore' | 'top' | 'none';

Copilot uses AI. Check for mistakes.
Comment thread src/tour-overlay.ts
Comment on lines 417 to 421
override updated(changed: Map<string, unknown>) {
if (changed.has('service') && this.service) {
this.unsubscribe?.();
this.attachService();
}

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the service property changes, updated() unsubscribes and creates a new StepRunner, but it doesn't clear any auto-advance timer that might be running on the previous StepRunner instance. That can leave a stray timeout firing after the service swap. Clear/teardown the existing stepRunner (or call clearAutoAdvance()) before replacing it in attachService()/updated().

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +108
const viewportHeight = window.innerHeight;
const fits =
rect.height + 270 + 32 < viewportHeight;
const inView = fits
? rect.top >= 0 &&
rect.bottom <= viewportHeight &&
rect.left >= 0 &&
rect.right <= window.innerWidth
: snapshot.step.placement === 'top'
? rect.top >= 270 + 16 + this.options.spotlightPadding &&
rect.top < viewportHeight
: rect.top >= 0 && rect.top < viewportHeight;

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StepRunner.shouldScrollIntoView() hardcodes tooltip sizing constants (270, 32, 16) that duplicate values already defined in dom/positioning (TOOLTIP_H_MAX, GAP). This makes future UI tweaks easy to miss and can desync scroll behavior from positioning behavior. Prefer importing and using the shared constants/helpers instead of repeating the numbers here.

Copilot uses AI. Check for mistakes.
Comment thread src/dom/deep-query.ts
Comment on lines +1 to +15
export function deepQuery(
selector: string,
root: Element | Document = document.body,
): Element | null {
const found = root.querySelector(selector);
if (found) return found;

const children = root.querySelectorAll('*');
for (const element of children) {
if (element.shadowRoot) {
const shadowResult = deepQuery(
selector,
element.shadowRoot as unknown as Document,
);
if (shadowResult) return shadowResult;

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deepQuery accepts Element | Document, but the implementation also recurses into ShadowRoot and currently casts shadowRoot to Document. This is type-unsafe and makes it harder to reuse deepQuery starting from a shadow root. Consider widening the root parameter to include ShadowRoot (or a ParentNode/Document | Element | ShadowRoot-style type) so the recursion doesn’t require an incorrect cast.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +21
export function resolveTarget(
targetId: string,
targetAttribute = DEFAULT_TARGET_ATTR,
root: Element | Document = document.body,
): Element | null {
if (!targetId || targetId === '_none_') return null;
return deepQuery(resolveTargetSelector(targetId, targetAttribute), root);
}

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveTarget() takes root: Element | Document, but callers may reasonably want to search within a ShadowRoot (especially since deepQuery supports it). Widening this parameter type (and default) to include ShadowRoot would avoid forcing consumers into as unknown as Document casts and better matches the actual behavior.

Copilot uses AI. Check for mistakes.
Comment thread src/tour-overlay.ts
Comment on lines 451 to +463
private attachService() {
this.unsubscribe = this.service.subscribe(snap => this.handleTourChange(snap));
}
this.stepRunner = new StepRunner({
getCurrentSnapshot: () => this.service.getSnapshot() as CoreTourSnapshot<TourStep> | null,
getTour: tourId => this.getTourDefinition(tourId),
nextStep: () => this.service.nextStep(),
spotlightPadding: this.service.spotlightPadding,
targetAttribute: this.service.targetAttribute,
dispatchRouteChange: route => this.dispatchRouteChange(route),
});

/* ── Auto-advance ───────────────────────────────── */
this.unsubscribe = this.service.subscribe(snapshot => {
void this.handleTourChange(snapshot as CoreTourSnapshot<TourStep> | null);
});

Copilot AI Mar 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attachService() uses casts for getSnapshot()/subscribe() (as CoreTourSnapshot<TourStep> | null). Since service is already typed as TourService<TourStep>, these casts should be unnecessary; keeping them can mask real type mismatches introduced later. Prefer removing the casts and letting the compiler enforce consistency between core and overlay snapshot types.

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/dom/positioning.ts (1)

109-122: Consider removing unreachable default case.

The TourPlacement type is exhaustive ('top' | 'bottom' | 'left' | 'right'), making the default case unreachable. However, keeping it as defensive coding is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dom/positioning.ts` around lines 109 - 122, The switch in getArrowClass
has an unreachable default for the exhaustive TourPlacement type; remove the
default branch and make the switch exhaustive by adding a compile-time check
(e.g., after the cases assign placement to a never-typed variable or call an
assertNever helper) so the TypeScript compiler will catch any future additions
to TourPlacement — update getArrowClass to handle 'top'|'bottom'|'left'|'right'
cases and include the never-based exhaustiveness check instead of returning
'arrow-bottom' in a default.
src/overlay/step-runner.ts (1)

96-113: Consider simplifying the visibility check logic.

The nested ternary in lines 102-110 is functionally correct but dense. Consider extracting into a helper function or using early returns for improved readability.

Suggested refactor
  private shouldScrollIntoView(snapshot: ResolvedTourSnapshot): boolean {
    const rect = snapshot.targetRect;
    if (!rect) return false;

    const viewportHeight = window.innerHeight;
    const fits = rect.height + TOOLTIP_H_MAX + TARGET_CONTEXT_MARGIN < viewportHeight;
-   const inView = fits
-     ? rect.top >= 0 &&
-       rect.bottom <= viewportHeight &&
-       rect.left >= 0 &&
-       rect.right <= window.innerWidth
-     : snapshot.step.placement === 'top'
-       ? rect.top >= TOOLTIP_H_MAX + GAP + this.options.spotlightPadding &&
-         rect.top < viewportHeight
-       : rect.top >= 0 && rect.top < viewportHeight;
+   
+   let inView: boolean;
+   if (fits) {
+     inView = rect.top >= 0 &&
+       rect.bottom <= viewportHeight &&
+       rect.left >= 0 &&
+       rect.right <= window.innerWidth;
+   } else if (snapshot.step.placement === 'top') {
+     inView = rect.top >= TOOLTIP_H_MAX + GAP + this.options.spotlightPadding &&
+       rect.top < viewportHeight;
+   } else {
+     inView = rect.top >= 0 && rect.top < viewportHeight;
+   }

    return !inView;
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/overlay/step-runner.ts` around lines 96 - 113, The nested ternary inside
shouldScrollIntoView makes the inView calculation hard to read; extract the
visibility logic into a small helper (e.g., isRectInView or
computeInViewForPlacement) or rewrite with early returns: compute fits as now,
then if fits call a clear helper that checks full-viewport containment using
rect.top/bottom/left/right and window dimensions; else branch by
snapshot.step.placement === 'top' and use explicit conditions that reference
TOOLTIP_H_MAX, GAP and this.options.spotlightPadding; keep the final return as
!inView. Ensure the helper takes the rect, placement and relevant constants so
callers are clearer and unit-testable (target: function shouldScrollIntoView,
snapshot.targetRect, snapshot.step.placement, TOOLTIP_H_MAX, GAP,
this.options.spotlightPadding).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/dom/positioning.ts`:
- Around line 109-122: The switch in getArrowClass has an unreachable default
for the exhaustive TourPlacement type; remove the default branch and make the
switch exhaustive by adding a compile-time check (e.g., after the cases assign
placement to a never-typed variable or call an assertNever helper) so the
TypeScript compiler will catch any future additions to TourPlacement — update
getArrowClass to handle 'top'|'bottom'|'left'|'right' cases and include the
never-based exhaustiveness check instead of returning 'arrow-bottom' in a
default.

In `@src/overlay/step-runner.ts`:
- Around line 96-113: The nested ternary inside shouldScrollIntoView makes the
inView calculation hard to read; extract the visibility logic into a small
helper (e.g., isRectInView or computeInViewForPlacement) or rewrite with early
returns: compute fits as now, then if fits call a clear helper that checks
full-viewport containment using rect.top/bottom/left/right and window
dimensions; else branch by snapshot.step.placement === 'top' and use explicit
conditions that reference TOOLTIP_H_MAX, GAP and this.options.spotlightPadding;
keep the final return as !inView. Ensure the helper takes the rect, placement
and relevant constants so callers are clearer and unit-testable (target:
function shouldScrollIntoView, snapshot.targetRect, snapshot.step.placement,
TOOLTIP_H_MAX, GAP, this.options.spotlightPadding).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6e81efe6-87dc-4511-8b1e-1e1d45c80848

📥 Commits

Reviewing files that changed from the base of the PR and between a478b6c and a043ffd.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • SECURITY.md
  • package.json
  • src/dom/deep-query.ts
  • src/dom/positioning.ts
  • src/dom/scroll-manager.ts
  • src/overlay/step-runner.ts
  • test/positioning.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/positioning.test.ts

@barryking barryking merged commit 18a9528 into main Mar 7, 2026
3 checks passed
@barryking barryking deleted the feat/simplification-re-architecture branch March 7, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants