diff --git a/frontend/src/App.css b/frontend/src/App.css index f6d77fb..be10c43 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1534,13 +1534,10 @@ a.app-footer__repo:hover { margin: 0 auto 2rem; padding: 1.25rem 1.5rem; background: var(--color-bg-surface); - border: 1px solid var(--color-border); - border-radius: 0.5rem; + border: 2px solid transparent; + border-image: linear-gradient(135deg, #22d3ee, #a855f7) 1; text-align: left; position: relative; - box-shadow: - 0 0 20px rgba(34, 211, 238, 0.15), - 0 0 40px rgba(34, 211, 238, 0.05); } .home-featured__label { @@ -1867,7 +1864,9 @@ a.app-footer__repo:hover { .technique-columns__sidebar { position: sticky; - top: 1.5rem; + top: 3.5rem; /* below sticky title bar */ + max-height: calc(100vh - 4rem); + overflow-y: auto; } @media (max-width: 768px) { @@ -1875,7 +1874,7 @@ a.app-footer__repo:hover { grid-template-columns: 1fr; } .technique-columns__sidebar { - position: static; + display: none; /* ToC hidden on mobile — single-column reading */ } } @@ -2037,67 +2036,68 @@ a.app-footer__repo:hover { /* ── Sticky Reading Header ────────────────────────────────────────────────── */ -.reading-header { - position: fixed; +/* ── Sticky title bar (within article content area) ───────────────────── */ + +.technique-title-bar { + position: sticky; top: 0; - left: 0; - right: 0; - z-index: 150; - transform: translateY(-100%); - transition: transform 0.25s ease; - background: var(--bg-card); + z-index: 100; + background: var(--color-bg); border-bottom: 1px solid var(--color-border); - pointer-events: none; + padding: 0.75rem 0; + margin-bottom: 0.75rem; } -.reading-header--visible { - transform: translateY(0); - pointer-events: auto; +.technique-title-bar__inner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; } -.reading-header__inner { - max-width: 1200px; - margin: 0 auto; - padding: 0.5rem 1.5rem; +.technique-title-bar__title { + font-size: 1.25rem; + font-weight: 700; + color: var(--color-text-primary); + margin: 0; + line-height: 1.3; display: flex; align-items: center; gap: 0.5rem; - min-height: 36px; - overflow: hidden; -} - -.reading-header__title { - font-weight: 600; - font-size: 0.85rem; - color: var(--text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex-shrink: 1; + flex: 1; min-width: 0; } -.reading-header__separator { - color: var(--text-muted); +.technique-title-bar__meta { + display: flex; + align-items: center; + gap: 0.75rem; flex-shrink: 0; } -.reading-header__section { - font-size: 0.8rem; - color: var(--text-secondary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex-shrink: 1; - min-width: 0; +.technique-title-bar__creator { + display: flex; + align-items: center; + gap: 0.375rem; + font-size: 0.8125rem; + color: var(--color-text-secondary); + text-decoration: none; + transition: color 150ms ease; +} + +.technique-title-bar__creator:hover { + color: var(--color-accent); } @media (max-width: 600px) { - .reading-header__inner { - padding: 0.4rem 1rem; + .technique-title-bar__title { + font-size: 1.0625rem; } - .reading-header__section { - display: none; + .technique-title-bar__inner { + flex-direction: column; + align-items: flex-start; + gap: 0.375rem; } } @@ -2231,67 +2231,98 @@ a.app-footer__repo:hover { scroll-margin-top: 5rem; } -/* ── Key moments list ─────────────────────────────────────────────────────── */ +/* ── Key Moments — bibliography/sources style ─────────────────────────── */ -.technique-moments { +.technique-sources { + margin-top: 2.5rem; margin-bottom: 2rem; + padding-top: 1.5rem; + border-top: 1px solid var(--color-border); } -.technique-moments h2 { - font-size: 1.25rem; +.technique-sources h2 { + font-size: 1.125rem; font-weight: 700; - margin-bottom: 0.75rem; + margin-bottom: 1rem; + color: var(--color-text-primary); } -.technique-moments__list { +.technique-sources__list { list-style: none; + padding: 0; + margin: 0; display: flex; flex-direction: column; - gap: 0.75rem; + gap: 0.125rem; } -.technique-moment { - padding: 0.875rem 1rem; - background: var(--color-bg-surface); - border: 1px solid var(--color-border); - border-radius: 0.5rem; +.technique-source { + display: flex; + gap: 0.625rem; + padding: 0.5rem 0; + align-items: flex-start; + font-size: 0.8125rem; + line-height: 1.5; + border-bottom: 1px solid var(--color-border-subtle, rgba(255,255,255,0.04)); } -.technique-moment__title { - display: block; - margin: 0 0 0.25rem 0; - font-size: 0.9375rem; +.technique-source:last-child { + border-bottom: none; +} + +.technique-source__index { + color: var(--color-accent); font-weight: 600; - line-height: 1.3; + font-size: 0.75rem; + flex-shrink: 0; + min-width: 1.5rem; + font-variant-numeric: tabular-nums; + padding-top: 0.0625rem; } -.technique-moment__meta { +.technique-source__body { + display: flex; + flex-direction: column; + gap: 0.125rem; + min-width: 0; +} + +.technique-source__title { + font-weight: 600; + color: var(--color-text-primary); +} + +.technique-source__detail { display: flex; align-items: center; gap: 0.5rem; - margin-bottom: 0.25rem; flex-wrap: wrap; -} - -.technique-moment__time { - font-size: 0.75rem; - color: var(--color-text-secondary); - font-variant-numeric: tabular-nums; -} - -.technique-moment__source { - font-size: 0.75rem; color: var(--color-text-muted); + font-size: 0.75rem; +} + +.technique-source__file { font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", monospace; - max-width: 20rem; + max-width: 24rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.technique-moment__summary { - font-size: 0.8125rem; +.technique-source__time { + font-variant-numeric: tabular-nums; +} + +.technique-source__type { + color: var(--color-accent); + font-size: 0.6875rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.technique-source__summary { color: var(--color-text-secondary); + font-size: 0.8125rem; line-height: 1.5; } @@ -2368,13 +2399,13 @@ a.app-footer__repo:hover { .technique-related__grid { display: grid; - grid-template-columns: 1fr; + grid-template-columns: 1fr 1fr; gap: 0.75rem; } -@media (min-width: 600px) { +@media (max-width: 600px) { .technique-related__grid { - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr; } } diff --git a/frontend/src/components/ReadingHeader.tsx b/frontend/src/components/ReadingHeader.tsx deleted file mode 100644 index d5649d3..0000000 --- a/frontend/src/components/ReadingHeader.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Sticky reading header that appears when the article H1 scrolls out of view. - * - * Shows the article title and current section name in a thin fixed bar - * at the top of the viewport. Uses CSS transform for slide-in/out animation. - */ - -interface ReadingHeaderProps { - /** Article title */ - title: string; - /** Currently active section heading (from scroll-spy) */ - currentSection: string; - /** Whether the header should be visible (H1 is out of viewport) */ - visible: boolean; -} - -export default function ReadingHeader({ title, currentSection, visible }: ReadingHeaderProps) { - return ( -
-
- {title} - {currentSection && ( - <> - · - {currentSection} - - )} -
-
- ); -} diff --git a/frontend/src/components/TableOfContents.tsx b/frontend/src/components/TableOfContents.tsx index ab68e68..89c27c0 100644 --- a/frontend/src/components/TableOfContents.tsx +++ b/frontend/src/components/TableOfContents.tsx @@ -4,8 +4,10 @@ * Renders a nested list of anchor links matching the H2/H3 section structure. * Uses slugified headings as IDs for scroll targeting. * Receives activeId from parent (TechniquePage) which owns the IntersectionObserver. + * Smooth-scrolls to target on click with offset for the sticky title bar. */ +import { useCallback } from "react"; import type { BodySectionV2 } from "../api/public-client"; export function slugify(text: string): string { @@ -21,6 +23,16 @@ interface TableOfContentsProps { } export default function TableOfContents({ sections, activeId }: TableOfContentsProps) { + const handleClick = useCallback((e: React.MouseEvent, targetId: string) => { + e.preventDefault(); + const el = document.getElementById(targetId); + if (el) { + el.scrollIntoView({ behavior: "smooth", block: "start" }); + // Update URL hash without jumping + window.history.replaceState(null, "", `#${targetId}`); + } + }, []); + if (sections.length === 0) return null; return ( @@ -34,6 +46,7 @@ export default function TableOfContents({ sections, activeId }: TableOfContentsP
  • handleClick(e, sectionSlug)} className={`technique-toc__link${isActive ? " technique-toc__link--active" : ""}`} > {section.heading} @@ -47,6 +60,7 @@ export default function TableOfContents({ sections, activeId }: TableOfContentsP
  • handleClick(e, subSlug)} className={`technique-toc__sublink${isSubActive ? " technique-toc__sublink--active" : ""}`} > {sub.heading} diff --git a/frontend/src/pages/TechniquePage.tsx b/frontend/src/pages/TechniquePage.tsx index fa6f918..8e473a3 100644 --- a/frontend/src/pages/TechniquePage.tsx +++ b/frontend/src/pages/TechniquePage.tsx @@ -1,9 +1,9 @@ /** * Technique page detail view with version switching. * - * Fetches a single technique by slug. When historical versions exist, - * shows a version switcher that lets admins view previous snapshots - * with pipeline metadata (prompt hashes, model config). + * Layout: sticky title bar at top of article, 2-column grid below. + * Left column: prose, key moments (bibliography), signal chains, related techniques. + * Right column: ToC (sticky, scrolls with viewer). */ import { useEffect, useMemo, useRef, useState } from "react"; @@ -20,7 +20,6 @@ import { import ReportIssueModal from "../components/ReportIssueModal"; import CopyLinkButton from "../components/CopyLinkButton"; import CreatorAvatar from "../components/CreatorAvatar"; -import ReadingHeader from "../components/ReadingHeader"; import TableOfContents, { slugify } from "../components/TableOfContents"; import { parseCitations } from "../utils/citations"; import { useDocumentTitle } from "../hooks/useDocumentTitle"; @@ -43,7 +42,6 @@ function formatDate(iso: string): string { /** Extract the displayable fields from a version snapshot, matching TechniqueDetail shape. */ function snapshotToOverlay(snapshot: Record) { - // body_sections can be either list (v2) or dict (v1) let bodySections: BodySectionV2[] | Record | undefined; if (Array.isArray(snapshot.body_sections)) { bodySections = snapshot.body_sections as BodySectionV2[]; @@ -113,13 +111,12 @@ export default function TechniquePage() { const data = await fetchTechnique(slug); if (!cancelled) { setTechnique(data); - // Load versions if any exist if (data.version_count > 0) { try { const vRes = await fetchTechniqueVersions(slug); if (!cancelled) setVersions(vRes.items); } catch { - // Non-critical — version list fails silently + // Non-critical } } } @@ -172,12 +169,9 @@ export default function TechniquePage() { }; }, [slug, selectedVersion]); - // --- Scroll-spy: activeId for ToC and ReadingHeader --- - // IMPORTANT: These hooks must be above early returns to maintain - // consistent hook ordering across renders (React rules of hooks). + // --- Scroll-spy: activeId for ToC --- const [activeId, setActiveId] = useState(""); - const [h1Visible, setH1Visible] = useState(true); - const h1Ref = useRef(null); + const titleBarRef = useRef(null); // Overlay snapshot fields when viewing a historical version const isHistorical = selectedVersion !== "current" && versionDetail != null; @@ -202,35 +196,7 @@ export default function TechniquePage() { return ids; }, [displayFormat, displaySections]); - // Build a map from slug → heading text for ReadingHeader display - const sectionHeadingMap = useMemo(() => { - if (displayFormat !== "v2" || !Array.isArray(displaySections)) return new Map(); - const map = new Map(); - for (const section of displaySections as BodySectionV2[]) { - const sectionSlug = slugify(section.heading); - map.set(sectionSlug, section.heading); - for (const sub of section.subsections) { - map.set(`${sectionSlug}--${slugify(sub.heading)}`, sub.heading); - } - } - return map; - }, [displayFormat, displaySections]); - - // Observe H1 visibility — drives reading header show/hide - useEffect(() => { - const el = h1Ref.current; - if (!el) return; - const observer = new IntersectionObserver( - ([entry]) => { - setH1Visible(entry?.isIntersecting ?? true); - }, - { threshold: 0 } - ); - observer.observe(el); - return () => observer.disconnect(); - }, [technique]); // re-attach when technique changes - - // Observe section headings — drives activeId for ToC + ReadingHeader + // Observe section headings — drives activeId for ToC useEffect(() => { if (allSectionIds.length === 0) return; const observer = new IntersectionObserver( @@ -263,8 +229,6 @@ export default function TechniquePage() { } }, [technique]); - const currentSectionHeading = sectionHeadingMap.get(activeId) ?? ""; - if (loading) { return
    Loading technique…
    ; } @@ -298,16 +262,10 @@ export default function TechniquePage() { const displayPlugins = overlay?.plugins ?? technique.plugins; const displayQuality = overlay?.source_quality ?? technique.source_quality; + const isV2 = displayFormat === "v2" && Array.isArray(displaySections) && (displaySections as BodySectionV2[]).length > 0; + return (
    - {/* Reading header — v2 pages only */} - {displayFormat === "v2" && Array.isArray(displaySections) && (displaySections as BodySectionV2[]).length > 0 && ( - - )} {/* Back link */} ← Back @@ -336,85 +294,76 @@ export default function TechniquePage() { )} -
    -
    -
    -

    {displayTitle}

    + {/* Sticky title bar — sits at top of article, becomes sticky on scroll */} +
    +
    +

    {displayTitle}

    +
    {displayCategory && ( {displayCategory} )} + {technique.creator_info && ( + + + {technique.creator_info.name} + + )}
    - {technique.creator_info && ( -
    - - - {technique.creator_info.name} - - {technique.creator_info.genres && technique.creator_info.genres.length > 0 && ( - - {technique.creator_info.genres.map((g) => ( - {g} - ))} - +
    +
    + + {/* Stats + version switcher */} +
    + {(() => { + const sourceCount = new Set( + technique.key_moments + .map((km) => km.video_filename) + .filter(Boolean), + ).size; + const momentCount = technique.key_moments.length; + const updated = new Date(technique.updated_at).toLocaleDateString( + "en-US", + { year: "numeric", month: "short", day: "numeric" }, + ); + const parts = [ + `Compiled from ${sourceCount} source${sourceCount !== 1 ? "s" : ""}`, + `${momentCount} key moment${momentCount !== 1 ? "s" : ""}`, + ]; + if (technique.version_count > 0) { + parts.push( + `${technique.version_count} version${technique.version_count !== 1 ? "s" : ""}`, + ); + } + parts.push(`Last updated ${updated}`); + return parts.join(" · "); + })()} +
    + +
    + {versions.length > 0 && ( +
    + + + {versionLoading && ( + Loading… )}
    )} - - - {/* Meta stats line */} -
    - {(() => { - const sourceCount = new Set( - technique.key_moments - .map((km) => km.video_filename) - .filter(Boolean), - ).size; - const momentCount = technique.key_moments.length; - const updated = new Date(technique.updated_at).toLocaleDateString( - "en-US", - { year: "numeric", month: "short", day: "numeric" }, - ); - const parts = [ - `Compiled from ${sourceCount} source${sourceCount !== 1 ? "s" : ""}`, - `${momentCount} key moment${momentCount !== 1 ? "s" : ""}`, - ]; - if (technique.version_count > 0) { - parts.push( - `${technique.version_count} version${technique.version_count !== 1 ? "s" : ""}`, - ); - } - parts.push(`Last updated ${updated}`); - return parts.join(" · "); - })()} -
    - - {/* Version switcher */} -
    - {versions.length > 0 && ( -
    - - - {versionLoading && ( - Loading… - )} -
    - )} - -
    +
    {/* Pipeline metadata for historical versions */} {isHistorical && versionDetail.pipeline_metadata && ( @@ -474,179 +423,181 @@ export default function TechniquePage() { /> )} - - {/* Tags + plugin pills — scoped to left column */} - {((displayTags && displayTags.length > 0) || (displayPlugins && displayPlugins.length > 0)) && ( -
    - {displayTags && displayTags.map((tag) => ( - - {tag} - - ))} - {displayPlugins && displayPlugins.length > 0 && displayPlugins.map((plugin) => ( - - {plugin} - - ))} -
    - )} - - {/* Summary */} - {displaySummary && ( -
    -

    {displaySummary}

    -
    + {/* Tags + plugin pills */} + {((displayTags && displayTags.length > 0) || (displayPlugins && displayPlugins.length > 0)) && ( +
    + {displayTags && displayTags.map((tag) => ( + + {tag} + + ))} + {displayPlugins && displayPlugins.length > 0 && displayPlugins.map((plugin) => ( + + {plugin} + + ))} +
    )} - {/* Study guide prose — body_sections */} - {displaySections && - (Array.isArray(displaySections) ? displaySections.length > 0 : Object.keys(displaySections).length > 0) && ( -
    - {displayFormat === "v2" && Array.isArray(displaySections) ? ( - <> - {(displaySections as BodySectionV2[]).map((section) => { - const sectionSlug = slugify(section.heading); + {/* Two-column layout: main content + ToC sidebar */} +
    +
    + {/* Summary */} + {displaySummary && ( +
    +

    {displaySummary}

    +
    + )} + + {/* Study guide prose — body_sections */} + {displaySections && + (Array.isArray(displaySections) ? displaySections.length > 0 : Object.keys(displaySections).length > 0) && ( +
    + {displayFormat === "v2" && Array.isArray(displaySections) ? ( + <> + {(displaySections as BodySectionV2[]).map((section) => { + const sectionSlug = slugify(section.heading); + return ( +
    +

    {section.heading}

    + {section.content && ( +

    {parseCitations(section.content, technique.key_moments)}

    + )} + {section.subsections.map((sub) => { + const subSlug = `${sectionSlug}--${slugify(sub.heading)}`; + return ( +
    +

    {sub.heading}

    + {sub.content && ( +

    {parseCitations(sub.content, technique.key_moments)}

    + )} +
    + ); + })} +
    + ); + })} + + ) : ( + /* V1 dict format */ + Object.entries(displaySections as Record).map( + ([sectionTitle, content]: [string, unknown]) => ( +
    +

    {sectionTitle}

    + {typeof content === "string" ? ( +

    {content as string}

    + ) : typeof content === "object" && content !== null ? ( +
    +                            {JSON.stringify(content, null, 2)}
    +                          
    + ) : ( +

    {String(content as string)}

    + )} +
    + ), + ) + )} +
    + )} + + {/* Key Moments — bibliography style */} + {technique.key_moments.length > 0 && ( +
    +

    Key Moments

    +
      + {technique.key_moments.map((km, idx) => ( +
    1. + [{idx}] +
      + {km.title} + + {km.video_filename && ( + {km.video_filename} + )} + + {formatTime(km.start_time)}–{formatTime(km.end_time)} + + {km.content_type} + + {km.summary} +
      +
    2. + ))} +
    +
    + )} + + {/* Signal chains */} + {displayChains && + displayChains.length > 0 && ( +
    +

    Signal Chains

    + {displayChains.map((chain, i) => { + const chainObj = chain as Record; + const chainName = + typeof chainObj["name"] === "string" + ? chainObj["name"] + : `Chain ${i + 1}`; + const steps = Array.isArray(chainObj["steps"]) + ? (chainObj["steps"] as string[]) + : []; return ( -
    -

    {section.heading}

    - {section.content && ( -

    {parseCitations(section.content, technique.key_moments)}

    +
    +

    {chainName}

    + {steps.length > 0 && ( +
    + {steps.map((step, j) => ( + + {j > 0 && ( + + {" → "} + + )} + + {String(step)} + + + ))} +
    )} - {section.subsections.map((sub) => { - const subSlug = `${sectionSlug}--${slugify(sub.heading)}`; - return ( -
    -

    {sub.heading}

    - {sub.content && ( -

    {parseCitations(sub.content, technique.key_moments)}

    - )} -
    - ); - })}
    ); })} - - ) : ( - /* V1 dict format — original rendering */ - Object.entries(displaySections as Record).map( - ([sectionTitle, content]: [string, unknown]) => ( -
    -

    {sectionTitle}

    - {typeof content === "string" ? ( -

    {content as string}

    - ) : typeof content === "object" && content !== null ? ( -
    -                        {JSON.stringify(content, null, 2)}
    -                      
    - ) : ( -

    {String(content as string)}

    +
    + )} + + {/* Related techniques */} + {technique.related_links.length > 0 && ( +
    +

    Related Techniques

    +
    + {technique.related_links.map((link) => ( +
    + + {link.target_title} + + {link.creator_name && ( + {link.creator_name} + )} + {link.topic_category && ( + {link.topic_category} + )} + {link.reason && ( +

    {link.reason}

    )}
    - ), - ) - )} -
    - )} - -
    -
    - {/* Table of Contents — v2 pages only */} - {displayFormat === "v2" && Array.isArray(displaySections) && (displaySections as BodySectionV2[]).length > 0 && ( - - )} - {/* Key moments (always from live data — not versioned) */} - {technique.key_moments.length > 0 && ( -
    -

    Key Moments

    -
      - {technique.key_moments.map((km) => ( -
    1. -

      {km.title}

      -
      - {km.video_filename && ( - - {km.video_filename} - - )} - - {formatTime(km.start_time)} – {formatTime(km.end_time)} - - - {km.content_type} - -
      -

      {km.summary}

      -
    2. - ))} -
    -
    - )} - - {/* Signal chains */} - {displayChains && - displayChains.length > 0 && ( -
    -

    Signal Chains

    - {displayChains.map((chain, i) => { - const chainObj = chain as Record; - const chainName = - typeof chainObj["name"] === "string" - ? chainObj["name"] - : `Chain ${i + 1}`; - const steps = Array.isArray(chainObj["steps"]) - ? (chainObj["steps"] as string[]) - : []; - return ( -
    -

    {chainName}

    - {steps.length > 0 && ( -
    - {steps.map((step, j) => ( - - {j > 0 && ( - - {" → "} - - )} - - {String(step)} - - - ))} -
    - )} -
    - ); - })} -
    - )} - - {/* Related techniques (always from live data) */} - {technique.related_links.length > 0 && ( -
    -

    Related Techniques

    -
    - {technique.related_links.map((link) => ( -
    - - {link.target_title} - - {link.creator_name && ( - {link.creator_name} - )} - {link.topic_category && ( - {link.topic_category} - )} - {link.reason && ( -

    {link.reason}

    - )} + ))}
    - ))} -
    -
    - )} +
    + )}
    + + {/* Sidebar: ToC only */} + {isV2 && ( + + )}
    {/* Footer actions */} diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index 32fedc2..a04a926 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ReadingHeader.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"} \ No newline at end of file