Răsfoiți Sursa

update spec

Yuki Takei 1 zi în urmă
părinte
comite
7e4683feda

+ 121 - 95
.kiro/specs/auto-scroll/design.md

@@ -17,6 +17,7 @@
 
 ### Non-Goals
 - Adding `data-growi-is-content-rendering` to attachment-refs (Ref/Refs/RefImg/RefsImg/Gallery), or RichAttachment — these also cause layout shifts but require more complex integration; deferred to follow-up
+- Replacing SearchResultContent's keyword-highlight scroll with hash-based scroll (search pages have no URL hash)
 - Supporting non-browser environments (SSR) — this is a client-only hook
 
 ## Architecture
@@ -34,19 +35,25 @@ The rendering attribute `data-growi-rendering` is defined in `@growi/core` and c
 
 ### Architecture Pattern & Boundary Map
 
+> **Note**: This diagram reflects the final architecture after Task 8 module reorganization. See "Task 8 Design" section below for the migration details.
+
 ```mermaid
 graph TB
     subgraph growi_core[growi core]
         CONST[Rendering Status Constants]
     end
 
-    subgraph apps_app[apps app - client hooks]
-        HOOK[useContentAutoScroll]
+    subgraph shared_util[src/client/util]
         WATCH[watchRenderingAndReScroll]
     end
 
-    subgraph page_views[Content Views]
+    subgraph page_view[src/components/PageView]
+        UHAS[useHashAutoScroll]
         PV[PageView]
+    end
+
+    subgraph search[features/search/.../SearchPage]
+        UKR[useKeywordRescroll]
         SRC[SearchResultContent]
     end
 
@@ -57,9 +64,10 @@ graph TB
         LSX[Lsx]
     end
 
-    PV -->|calls, default scrollTo| HOOK
-    SRC -->|calls, custom scrollTo| HOOK
-    HOOK -->|delegates| WATCH
+    PV -->|calls| UHAS
+    UHAS -->|imports| WATCH
+    SRC -->|calls| UKR
+    UKR -->|imports| WATCH
     WATCH -->|queries| CONST
     DV -->|sets/toggles| CONST
     MV -->|sets/toggles| CONST
@@ -68,10 +76,9 @@ graph TB
 ```
 
 **Architecture Integration**:
-- Selected pattern: Custom hook with options object — idiomatic React, testable, extensible
-- Domain boundaries: Hook logic in `src/hooks/`, constants in `@growi/core`, attribute lifecycle in each renderer package
+- Selected pattern: Co-located hooks per consumer + shared utility function — idiomatic React, testable, minimal coupling
+- Domain boundaries: `watchRenderingAndReScroll` (shared pure function) in `src/client/util/`, consumer-specific hooks co-located with their components, constants in `@growi/core`, attribute lifecycle in each renderer package
 - Existing patterns preserved: MutationObserver + polling hybrid, timeout-based safety bounds
-- New components rationale: `src/hooks/` directory needed for cross-feature hooks not tied to a specific feature module
 - Steering compliance: Named exports, immutable patterns, co-located tests
 
 ### Technology Stack
@@ -90,12 +97,12 @@ No new external dependencies are introduced.
 
 ```mermaid
 sequenceDiagram
-    participant Caller as Content View
-    participant Hook as useContentAutoScroll
+    participant Caller as Content View (PageView)
+    participant Hook as useHashAutoScroll
     participant DOM as DOM
     participant Watch as watchRenderingAndReScroll
 
-    Caller->>Hook: useContentAutoScroll options
+    Caller->>Hook: useHashAutoScroll options
     Hook->>Hook: Guard checks key, hash, container
 
     alt Target exists in DOM
@@ -131,55 +138,54 @@ Key decisions:
 
 | Requirement | Summary | Components | Interfaces | Flows |
 |-------------|---------|------------|------------|-------|
-| 1.1, 1.2 | Immediate scroll to hash target | useContentAutoScroll | UseContentAutoScrollOptions.resolveTarget | Auto-Scroll Lifecycle |
-| 1.3, 1.4, 1.5 | Guard conditions | useContentAutoScroll | UseContentAutoScrollOptions.key, contentContainerId | — |
-| 2.1, 2.2, 2.3 | Deferred scroll for lazy targets | useContentAutoScroll (target observer) | — | Auto-Scroll Lifecycle |
-| 3.1–3.6 | Re-scroll after rendering | watchRenderingAndReScroll | scrollTo callback | Auto-Scroll Lifecycle |
+| 1.1, 1.2 | Immediate scroll to hash target | useHashAutoScroll | UseHashAutoScrollOptions.resolveTarget | Auto-Scroll Lifecycle |
+| 1.3, 1.4, 1.5 | Guard conditions | useHashAutoScroll | UseHashAutoScrollOptions.key, contentContainerId | — |
+| 2.1, 2.2, 2.3 | Deferred scroll for lazy targets | useHashAutoScroll (target observer) | — | Auto-Scroll Lifecycle |
+| 3.1–3.6 | Re-scroll after rendering | watchRenderingAndReScroll | scrollToTarget callback | Auto-Scroll Lifecycle |
 | 4.1–4.7 | Rendering attribute protocol | Rendering Status Constants, DrawioViewer, MermaidViewer, PlantUmlViewer, Lsx | GROWI_IS_CONTENT_RENDERING_ATTR | — |
 | 4.8 | ResizeObserver re-render cycle | DrawioViewer | GROWI_IS_CONTENT_RENDERING_ATTR | — |
-| 5.1–5.5 | Page-type agnostic design | useContentAutoScroll, SearchResultContent | UseContentAutoScrollOptions | — |
-| 5.6, 5.7, 6.1–6.3 | Cleanup and safety | useContentAutoScroll, watchRenderingAndReScroll | cleanup functions | — |
+| 5.1–5.5 | Page-type agnostic design | watchRenderingAndReScroll (shared), useHashAutoScroll (PageView), useKeywordRescroll (Search) | — | — |
+| 5.6, 5.7, 6.1–6.3 | Cleanup and safety | useHashAutoScroll, useKeywordRescroll, watchRenderingAndReScroll | cleanup functions | — |
 
 ## Components and Interfaces
 
 | Component | Domain/Layer | Intent | Req Coverage | Key Dependencies | Contracts |
 |-----------|--------------|--------|--------------|------------------|-----------|
-| useContentAutoScroll | Client Hooks | Reusable auto-scroll hook with configurable target resolution and scroll behavior | 1, 2, 5, 6 | watchRenderingAndReScroll (P0), Rendering Status Constants (P1) | Service |
-| watchRenderingAndReScroll | Client Hooks (internal) | Polls for rendering-status attributes and re-scrolls until complete or timeout | 3, 6 | Rendering Status Constants (P0) | Service |
+| useHashAutoScroll | src/components/PageView | Hash-based auto-scroll hook for PageView with configurable target resolution and scroll behavior | 1, 2, 5, 6 | watchRenderingAndReScroll (P0), Rendering Status Constants (P1) | Service |
+| useKeywordRescroll | features/search/.../SearchPage | Keyword-highlight scroll hook with rendering watch integration for SearchResultContent | 5, 6 | watchRenderingAndReScroll (P0), scrollWithinContainer (P0) | Service |
+| watchRenderingAndReScroll | src/client/util | Shared utility: polls for rendering-status attributes and re-scrolls until complete or timeout | 3, 6 | Rendering Status Constants (P0) | Service |
 | Rendering Status Constants | @growi/core | Shared attribute name, value, and selector constants | 4 | None | State |
 | DrawioViewer (modification) | remark-drawio | Declarative rendering-status attribute toggle | 4.3, 4.4, 4.8 | Rendering Status Constants (P0) | State |
 | MermaidViewer (modification) | features/mermaid | Add rendering-status attribute lifecycle to async SVG render | 4.3, 4.4, 4.7 | Rendering Status Constants (P0) | State |
 | PlantUmlViewer (new) | features/plantuml | Wrap PlantUML `<img>` to provide rendering-status attribute lifecycle | 4.3, 4.4, 4.7 | Rendering Status Constants (P0) | State |
 | Lsx (modification) | remark-lsx | Add rendering-status attribute lifecycle to async page list fetch | 4.3, 4.4, 4.7 | Rendering Status Constants (P0) | State |
-| SearchResultContent (modification) | features/search | Integrate useContentAutoScroll with container-relative scrollTo; suppress keyword scroll when hash is present | 5.1, 5.2, 5.3, 5.5 | useContentAutoScroll (P0), scrollWithinContainer (P0) | State |
 
 ### Client Hooks
 
-#### useContentAutoScroll
+#### useHashAutoScroll
 
 | Field | Detail |
 |-------|--------|
-| Intent | Reusable hook that scrolls to a target element identified by URL hash, with support for lazy-rendered content and customizable scroll behavior |
+| Intent | Hash-based auto-scroll hook for PageView that scrolls to a target element identified by URL hash, with support for lazy-rendered content and customizable scroll behavior |
 | Requirements | 1.1–1.5, 2.1–2.3, 5.1–5.7, 6.1–6.3 |
 
 **Responsibilities & Constraints**
-- Orchestrates the full auto-scroll lifecycle: guard → resolve target → scroll → watch rendering
+- Orchestrates the full hash-based auto-scroll lifecycle: guard → resolve target → scroll → watch rendering
 - Always delegates to `watchRenderingAndReScroll` after the initial scroll — does **not** skip the watch even when no rendering elements are present at scroll time, because async renderers may mount later
-- Must not import page-specific or feature-specific modules
+- Co-located with `PageView.tsx` — this hook is hash-navigation–specific (`window.location.hash`)
 
 **Dependencies**
-- Outbound: `watchRenderingAndReScroll` — rendering watch delegation (P0)
+- Outbound: `watchRenderingAndReScroll` from `~/client/util/watch-rendering-and-rescroll` (P0)
 
 **Contracts**: Service [x]
 
 ##### Service Interface
 
 ```typescript
-/** Configuration for the auto-scroll hook */
-interface UseContentAutoScrollOptions {
+/** Configuration for the hash-based auto-scroll hook */
+interface UseHashAutoScrollOptions {
   /**
    * Unique key that triggers re-execution when changed.
-   * Typically a page ID, search query ID, or other view identifier.
    * When null/undefined, all scroll processing is skipped.
    */
   key: string | undefined | null;
@@ -202,7 +208,7 @@ interface UseContentAutoScrollOptions {
 }
 
 /** Hook signature */
-function useContentAutoScroll(options: UseContentAutoScrollOptions): void;
+function useHashAutoScroll(options: UseHashAutoScrollOptions): void;
 ```
 
 - Preconditions: Called within a React component; browser environment with `window.location.hash` available
@@ -210,10 +216,51 @@ function useContentAutoScroll(options: UseContentAutoScrollOptions): void;
 - Invariants: At most one target observer and one rendering watch active per hook instance
 
 **Implementation Notes**
-- File location: `apps/app/src/client/hooks/use-content-auto-scroll/use-content-auto-scroll.ts`
-- Test file: `apps/app/src/client/hooks/use-content-auto-scroll/use-content-auto-scroll.spec.tsx`
+- File location: `apps/app/src/components/PageView/use-hash-auto-scroll.ts`
+- Test file: `apps/app/src/components/PageView/use-hash-auto-scroll.spec.tsx`
 - The `resolveTarget` and `scrollTo` callbacks should be wrapped in `useRef` to avoid re-triggering the effect when callback identity changes
-- Export both `useContentAutoScroll` and `watchRenderingAndReScroll` as named exports for independent testability
+
+---
+
+#### useKeywordRescroll
+
+| Field | Detail |
+|-------|--------|
+| Intent | Keyword-highlight scroll hook for SearchResultContent that scrolls to the first `.highlighted-keyword` element and re-scrolls after async renderers settle |
+| Requirements | 5.1–5.7, 6.1–6.3 |
+
+**Responsibilities & Constraints**
+- MutationObserver on container for keyword highlight detection (debounced 500ms)
+- `watchRenderingAndReScroll` integration for async renderer layout shift compensation
+- Cleanup of both MO and rendering watch on key change or unmount
+- Co-located with `SearchResultContent.tsx`
+
+**Dependencies**
+- Outbound: `watchRenderingAndReScroll` from `~/client/util/watch-rendering-and-rescroll` (P0)
+- Outbound: `scrollWithinContainer` from `~/client/util/smooth-scroll` (P0)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+interface UseKeywordRescrollOptions {
+  /** Ref to the scrollable container element */
+  scrollElementRef: RefObject<HTMLElement | null>;
+  /** Unique key that triggers re-execution (typically page._id) */
+  key: string;
+}
+
+function useKeywordRescroll(options: UseKeywordRescrollOptions): void;
+```
+
+- Preconditions: `scrollElementRef.current` is a mounted scroll container
+- Postconditions: On unmount or key change, MO disconnected, rendering watch cleaned up, debounce cancelled
+
+**Implementation Notes**
+- File location: `apps/app/src/features/search/client/components/SearchPage/use-keyword-rescroll.ts`
+- Test file: `apps/app/src/features/search/client/components/SearchPage/use-keyword-rescroll.spec.tsx`
+- Helper functions (`scrollToKeyword`, `scrollToTargetWithinContainer`) are defined in the hook file since only this hook uses them
 
 ---
 
@@ -221,7 +268,7 @@ function useContentAutoScroll(options: UseContentAutoScrollOptions): void;
 
 | Field | Detail |
 |-------|--------|
-| Intent | Pure function (not a hook) that monitors rendering-status attributes and periodically re-scrolls until rendering completes or timeout |
+| Intent | Pure function (not a hook) that monitors rendering-status attributes and periodically re-scrolls until rendering completes or timeout. Shared utility consumed by both `useHashAutoScroll` and `useKeywordRescroll`. |
 | Requirements | 3.1–3.6, 6.1–6.3 |
 
 **Responsibilities & Constraints**
@@ -254,6 +301,8 @@ function watchRenderingAndReScroll(
 - Invariants: At most one poll timer active at any time; stopped flag prevents post-cleanup execution
 
 **Implementation Notes**
+- File location: `apps/app/src/client/util/watch-rendering-and-rescroll.ts` (co-located with `smooth-scroll.ts`)
+- Test file: `apps/app/src/client/util/watch-rendering-and-rescroll.spec.tsx`
 - Add a `stopped` boolean flag checked inside timer callbacks to prevent race conditions between cleanup and queued timer execution
 - When `checkAndSchedule` detects that no rendering elements remain and a timer is currently active, cancel the active timer immediately — avoids a redundant re-scroll after rendering has already completed
 - The MutationObserver watches `childList`, `subtree`, and `attributes` (filtered to the rendering-status attribute) — the `childList` + `subtree` combination is what detects late-mounting async renderers
@@ -376,66 +425,24 @@ const GROWI_IS_CONTENT_RENDERING_SELECTOR =
 
 | Field | Detail |
 |-------|--------|
-| Intent | Integrate `useContentAutoScroll` for hash-based navigation within the search result content pane; coordinate with the existing keyword-highlight scroll to prevent position conflicts |
-| Requirements | 5.1, 5.2, 5.3, 5.5 |
-
-**Background**: `SearchResultContent` renders page content inside a div with `overflow-y-scroll` (`#search-result-content-body-container`). It already has a separate keyword-highlight scroll mechanism — a `useEffect` with no dependency array that uses `MutationObserver` to scroll to the first `.highlighted-keyword` element using `scrollWithinContainer`. These two scroll mechanisms must coexist without overriding each other.
+| Intent | Integrate rendering-watch into SearchResultContent's keyword scroll so that layout shifts from async renderers are compensated |
+| Requirements | 5.1, 5.4, 5.5, 6.1 |
 
-**Container-Relative Scroll Problem**
+**Background**: `SearchResultContent` renders page content inside a div with `overflow-y-scroll` (`#search-result-content-body-container`). The keyword-highlight scroll mechanism was originally inlined as a `useEffect` with no dependency array and no cleanup.
 
-`element.scrollIntoView()` (the hook's default `scrollTo`) scrolls the viewport, not the scrolling container. Since `#search-result-content-body-container` is the scrolling unit, a custom `scrollTo` is required:
-
-```
-scrollTo(target):
-  distance = target.getBoundingClientRect().top
-            - container.getBoundingClientRect().top
-            - SCROLL_OFFSET_TOP
-  scrollWithinContainer(container, distance)
-```
+**Post-Implementation Correction**: The initial design (tasks 6.1–6.3) attempted to integrate `useContentAutoScroll` (hash-based) into SearchResultContent. This was architecturally incorrect — search pages use `/search?q=foo` with no URL hash, so the hash-driven hook would never activate. See `research.md` "Post-Implementation Finding" for details.
 
-The `container` reference is obtained via `scrollElementRef.current` (the existing React ref already present in the component). The `SCROLL_OFFSET_TOP = 30` constant is reused from the keyword scroll for visual consistency.
-
-**Scroll Conflict Resolution**
-
-When a URL hash is present, both mechanisms would fire:
-1. `useContentAutoScroll` → scroll to the hash target element
-2. Keyword MutationObserver → scroll to the first `.highlighted-keyword` element (500ms debounced)
-
-This creates a race condition where the keyword scroll overrides the hash scroll. Resolution strategy:
-
-> **When `window.location.hash` is non-empty, the keyword-highlight `useEffect` returns early.** Hash-based scroll takes priority.
-
-Concretely, the existing keyword-scroll `useEffect` gains a guard at the top:
-
-```
-if (window.location.hash.length > 0) return;
-```
-
-When no hash is present, keyword scroll proceeds exactly as before — no behavior change for the common case.
+**Final Architecture**: The keyword scroll effect was extracted into a dedicated `useKeywordRescroll` hook (co-located with SearchResultContent), which directly integrates `watchRenderingAndReScroll` for rendering compensation. No hash-based scroll is used in SearchResultContent.
 
 **Hook Call Site**
 
 ```typescript
-const scrollTo = useCallback((target: HTMLElement) => {
-  const container = scrollElementRef.current;
-  if (container == null) return;
-  const distance =
-    target.getBoundingClientRect().top -
-    container.getBoundingClientRect().top -
-    SCROLL_OFFSET_TOP;
-  scrollWithinContainer(container, distance);
-}, []);
-
-useContentAutoScroll({
-  key: page._id,
-  contentContainerId: 'search-result-content-body-container',
-  scrollTo,
-});
+useKeywordRescroll({ scrollElementRef, key: page._id });
 ```
 
-- `resolveTarget` defaults to `document.getElementById` — heading elements have `id` attributes set by the remark processing pipeline, so the default resolver works without customization.
-- `scrollTo` uses the existing `scrollElementRef` directly to avoid redundant `getElementById` lookup.
-- `useCallback` with empty deps array ensures callback identity is stable across renders (the hook wraps it in a ref internally, but stable identity avoids any risk of spurious re-renders).
+- `scrollElementRef` is the existing React ref pointing to the scroll container
+- `key: page._id` triggers re-execution when the selected page changes
+- The hook internally handles MutationObserver setup, debounced keyword scroll, rendering watch, and full cleanup
 
 **File**: `apps/app/src/features/search/client/components/SearchPage/SearchResultContent.tsx`
 
@@ -570,17 +577,21 @@ This feature operates entirely in the browser DOM layer with no server interacti
 
 ## Testing Strategy
 
-### Unit Tests (co-located in `src/client/hooks/use-content-auto-scroll/`)
+### useHashAutoScroll Tests (co-located in `src/components/PageView/`)
 
 1. **Guard conditions**: Verify no-op when key is null, hash is empty, or container not found (1.3–1.5)
 2. **Immediate scroll**: Target exists in DOM → `scrollTo` called once (1.1)
 3. **Encoded hash**: URI-encoded hash decoded and resolved correctly (1.2)
 4. **Custom resolveTarget**: Provided closure is called instead of default `getElementById` (5.2)
 5. **Custom scrollTo**: Provided scroll function is called instead of default `scrollIntoView` (5.3)
-6. **Late-mounting renderers**: Rendering elements that appear after the initial scroll are detected and trigger a re-scroll (key scenario for Mermaid/PlantUML)
-7. **No spurious re-scroll when no renderers**: When no rendering elements ever appear, the watch times out without calling `scrollTo` again (validates always-start trade-off)
+6. **Late-mounting renderers**: Rendering elements that appear after the initial scroll are detected and trigger a re-scroll
+7. **No spurious re-scroll when no renderers**: When no rendering elements ever appear, the watch times out without calling `scrollTo` again
+8. **Deferred scroll**: Target appears after initial render via MutationObserver (2.1, 2.2)
+9. **Target observation timeout**: 10s timeout when target never appears (2.3)
+10. **Key change cleanup**: Observers and timers from previous run are released (5.6)
+11. **Unmount cleanup**: All resources released (5.7, 6.1)
 
-### Integration Tests (watchRenderingAndReScroll)
+### watchRenderingAndReScroll Tests (co-located in `src/client/util/`)
 
 1. **Rendering elements present**: Poll timer fires at 5s, re-scroll executes (3.1)
 2. **No rendering elements**: No timer scheduled (3.3)
@@ -590,14 +601,29 @@ This feature operates entirely in the browser DOM layer with no server interacti
 6. **Watch timeout**: All resources cleaned up after 10s (3.6, 6.2)
 7. **Cleanup prevents post-cleanup execution**: Stopped flag prevents race (6.1)
 8. **Rendering completes before first timer**: Immediate re-scroll fires via wasRendering path, no extra scroll after that
+9. **Active timer cancelled when rendering elements removed**: Avoids redundant re-scroll
+
+### useKeywordRescroll Tests (co-located in `features/search/.../SearchPage/`)
+
+1. **watchRenderingAndReScroll called with scroll container**: Correct container element passed
+2. **scrollToKeyword scrolls to first .highlighted-keyword**: Container-relative scroll calculation verified
+3. **scrollToKeyword returns false when no keyword found**: No scroll attempted
+4. **MutationObserver set up on container**: Correct observe config verified
+5. **Cleanup on unmount**: MO disconnected, rendering watch cleanup called, debounce cancelled
+6. **Key change re-runs effect**: New watch started for new key
+7. **Null container guard**: No-op when scrollElementRef.current is null
+
+### SearchResultContent Tests (co-located with component)
+
+1. **Hook integration**: `useKeywordRescroll` called with correct key and scroll container ref
+2. **Key change**: Hook re-called with new key on page change
 
 ### MermaidViewer Tests
 
-1. **rAF cleanup on unmount**: When component unmounts during the async render, the pending `requestAnimationFrame` is cancelled — no `setAttribute` call after unmount
-2. **isDarkMode change re-renders correctly**: Attribute resets to `"true"` on re-render and transitions to `"false"` via rAF after the new render completes
+1. **rAF cleanup on unmount**: Pending `requestAnimationFrame` cancelled on unmount
+2. **Rendering attribute lifecycle**: "true" initially → "false" via rAF after render → "false" immediately on error
 
-### Hook Lifecycle Tests
+### PlantUmlViewer Tests
 
-1. **Key change**: Cleanup runs, new scroll cycle starts (5.6)
-2. **Unmount**: All observers and timers cleaned up (5.7, 6.1)
-3. **Re-render with same key**: Effect does not re-trigger (stability)
+1. **Rendering attribute lifecycle**: "true" initially → "false" on img load → "false" on img error
+2. **img src**: Correct src attribute rendered

+ 2 - 2
.kiro/specs/auto-scroll/requirements.md

@@ -63,7 +63,7 @@ The following reviewer feedback is incorporated into these requirements:
 4. When the component finishes rendering or encounters an error, the component shall set the attribute value to indicate "completed" (e.g., `"false"`) rather than removing the attribute entirely — the attribute lifecycle shall be declarative (value toggle), not imperative (add/remove).
 5. The attribute shall be included in the component's HTML sanitization allowlist so that it survives remark/rehype processing.
 6. The CSS selector used by the auto-scroll system shall match only the "in progress" state (e.g., `[attr="true"]`), not the completed state.
-7. The following async-rendering components shall adopt the attribute protocol in this scope: DrawioViewer, MermaidViewer, and lsx (Lsx). Other async renderers (PlantUML, attachment-refs, RichAttachment) are deferred to follow-up work.
+7. The following async-rendering components shall adopt the attribute protocol in this scope: DrawioViewer, MermaidViewer, PlantUmlViewer (new wrapper component), and lsx (Lsx). Other async renderers (attachment-refs, RichAttachment) are deferred to follow-up work.
 8. When a component triggers a secondary re-render that will cause a layout shift (e.g., via ResizeObserver detecting container size changes after initial render), the component shall reset the attribute value to `"true"` before the re-render begins and allow the existing completion path to set it back to `"false"` when done. This ensures the auto-scroll system tracks all layout-shifting render cycles, not only the initial one.
 
 ### Requirement 5: Page-Type Agnostic Design
@@ -76,7 +76,7 @@ The following reviewer feedback is incorporated into these requirements:
 2. The hook shall accept an optional target-resolving function (closure) that returns the target `HTMLElement | null`. When not provided, the hook shall default to resolving the target via `document.getElementById` using the decoded hash.
 3. The hook shall accept an optional scroll function that defines how to scroll to the target element. When not provided, the hook shall default to `element.scrollIntoView()`. This allows callers (e.g., SearchResultContent) to supply a custom scroll strategy.
 4. The hook shall not import or depend on any page-specific state (Jotai atoms, SWR hooks, or page models).
-5. The hook shall be located in a shared directory (e.g., `src/client/hooks/`) and named to reflect its general-purpose nature — not tied to a specific page component.
+5. The shared rendering-watch utility (`watchRenderingAndReScroll`) shall be located in a shared directory (e.g., `src/client/util/`). Each consumer-specific hook shall be co-located with its consumer component and named to reflect its purpose (e.g., hash-based scroll for PageView, keyword-based re-scroll for SearchResultContent).
 6. When the key parameter changes, the hook shall clean up any active observers and timers from the previous run and re-execute the scroll logic.
 7. When the component using the hook unmounts, the hook shall clean up all MutationObservers, timers, and rendering watch resources.
 

+ 5 - 5
.kiro/specs/auto-scroll/spec.json

@@ -1,9 +1,9 @@
 {
   "feature_name": "auto-scroll",
   "created_at": "2026-04-02T00:00:00.000Z",
-  "updated_at": "2026-04-06T00:00:00.000Z",
+  "updated_at": "2026-04-07T00:00:00.000Z",
   "language": "en",
-  "phase": "tasks-generated",
+  "phase": "implementation-complete",
   "approvals": {
     "requirements": {
       "generated": true,
@@ -15,9 +15,9 @@
     },
     "tasks": {
       "generated": true,
-      "approved": false,
-      "notes": "Task 8 redesigned as module reorganization (5 subtasks). Tasks 1–7 complete."
+      "approved": true,
+      "notes": "All tasks (1–8) complete. Design updated to reflect final architecture after module reorganization."
     }
   },
-  "ready_for_implementation": false
+  "ready_for_implementation": true
 }