diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..df59db5
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,89 @@
+node_modules
+public/build
+build
+dist
+out
+coverage
+.history
+.react-router
+
+# Other Coverage tools
+*.lcov
+
+# macOS
+.DS_*
+
+# Cache Directories and files
+.cache
+.yarn*
+.env*
+!.env.example
+.swp*
+.turbo
+.npm
+.stylelintcache
+*.tsbuildinfo
+.node_repl_history
+
+# Lock files from other package managers
+package-lock.json
+yarn.lock
+
+# General tempory files and directories
+t?mp
+.t?mp
+*.t?mp
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Output of 'npm pack'
+*.tgz
+*.tar
+*.tar.gz
+*.tar.bz2
+*.tbz
+*.zip
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+vite.config.ts.*
+
+# Playwright various test reports
+test-results
+playwright-report
+blob-report
+
+
+# Editors
+.idea/workspace.xml
+.idea/usage.statistics.xml
+.idea/shelf
+
+# Dont commit sqlite database files
+*.db
+*.sqlite
+*.sqlite3
+*.db-journal
+
+
+# Content collections output files
+.content-collections
+
+# Output base directory of the documentation
+generated-docs/
diff --git a/.env.example b/.env.example
index d6b7df4..02cb40a 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1,4 @@
GITHUB_OWNER="github-owner" # Your username or organization name (Optional. For edit/report an issue for the documentation page)
GITHUB_REPO="github-repo" # Repository name (Optional. For edit/report an issue for the documentation page)
APP_ROOT_PATH="/path/to/your/app" # Optional. Default is `process.cwd()`
+GITHUB_REPO_URL="github-repo-url" # Optional. If you want to have GitHub icon link in the header or footer
diff --git a/.github/workflows/validate.yml b/.github/workflows/ci.yml
similarity index 70%
rename from .github/workflows/validate.yml
rename to .github/workflows/ci.yml
index c1598c4..b979290 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/ci.yml
@@ -1,4 +1,4 @@
-name: ๐ Validation Pipeline
+name: ๐ Validation & Deploy Pipeline
concurrency:
group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -67,3 +67,24 @@ jobs:
# Only works if you set `reportOnFailure: true` in your vite config as specified above
if: always()
uses: davelosert/vitest-coverage-report-action@v2
+
+
+ deploy:
+ needs: [lint, typecheck, check-unused, vitest]
+ name: ๐ Deploy PR Preview
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: forge-42/fly-deploy@v1.0.0-rc.2
+ id: deploy
+ env:
+ FLY_ORG: ${{ vars.FLY_ORG }}
+ FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
+ FLY_REGION: ${{ vars.FLY_REGION }}
+ with:
+ app_name: ${{github.event.repository.name}}-${{ github.event.number }}
+ env_vars: |
+ APP_ENV=staging
+ GITHUB_OWNER=${{github.repository_owner}}
+ GITHUB_REPO=${{github.event.repository.name}}
+ GITHUB_REPO_URL=https://github.com/${{ github.repository }}
diff --git a/.github/workflows/pr-close.yml b/.github/workflows/pr-close.yml
new file mode 100644
index 0000000..1f8f72a
--- /dev/null
+++ b/.github/workflows/pr-close.yml
@@ -0,0 +1,27 @@
+name: ๐งน PR Close
+
+concurrency:
+ group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+on:
+ pull_request:
+ branches: [main]
+ types: closed
+
+jobs:
+
+ destroy-pr-preview:
+ name: ๐งน Destroy PR Preview
+ runs-on: ubuntu-latest
+ environment:
+ name: pr-preview
+ steps:
+ - uses: actions/checkout@v4
+ - uses: forge-42/fly-destroy@v1.0.0-rc.2
+ id: destroy
+ env:
+ FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
+ FLY_ORG: ${{ vars.FLY_ORG }}
+ with:
+ app_name: ${{ env.FLY_ORG }}-${{ github.event.number }}
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..0534ba1
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,51 @@
+
+# syntax = docker/dockerfile:1.4
+
+# Base dependencies stage
+ARG NODE_VERSION=22.17.0
+FROM node:${NODE_VERSION}-slim AS base
+
+LABEL fly_launch_runtime="Node.js"
+
+# Node.js app lives here
+WORKDIR /app
+
+# Set production environment
+ENV NODE_ENV="production"
+
+# Install pnpm
+ARG PNPM_VERSION=10.13.0
+RUN npm install -g pnpm@$PNPM_VERSION
+
+
+# Throw-away build stage to reduce size of final image
+FROM base AS build
+
+# Install packages needed to build node modules
+RUN apt-get update -qq && \
+ apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3 git
+
+# Install node modules
+COPY .npmrc package.json pnpm-lock.yaml ./
+RUN pnpm install --frozen-lockfile --prod=false
+
+# Copy application code
+COPY . .
+
+# Build application
+RUN pnpm run generate:docs
+RUN pnpm run build
+
+# Remove development dependencies
+RUN pnpm prune --prod
+
+
+# Final stage for app image
+FROM base
+
+# Copy built application
+COPY --from=build /app /app
+
+# Start the server by default, this can be overwritten at runtime
+EXPOSE 3000
+CMD [ "pnpm", "run", "start" ]
diff --git a/README.md b/README.md
index 28283d7..602a5a6 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ This folder contains all the resources used by the documentation site, such as S
`content/`
-This folder contains sections and subsections with .mdx files that hold your documentation content. Below is the recommended structure to follow.
+This folder contains .md and .mdx files that hold your documentation content. Below is the recommended structure to follow.
An example of a valid content/ folder structure for organizing your package documentation:
@@ -46,12 +46,15 @@ content/
โโโ 01-buttons.mdx
โโโ 02-modals.mdx
```
-- Top-level .mdx files (like 01-changelog.mdx) are allowed.
-- Sections (like 04-getting-started, 05-core-features) are subfolders inside the content/ folder.
-- Subsections (like 03-data-management, 04-ui-components) are nested folders within sections.
-- Each section or subsection should have an index.md file for its sidebar title.
+- Top-level .mdx files (like 01-changelog.mdx) are allowed, but we recommend placing them in order before the sections, as shown in the example.
-### Example of the valid `02-introduction.mdx` file:
+- Sections (like 04-getting-started, 05-core-features) are subfolders inside the `content` folder.
+
+- Subsections (like 03-data-management, 04-ui-components) are nested folders within sections. Filenames inside them should start with `01-*.mdx`.
+
+- Each section or subsection should include an `index.md` file, which defines its sidebar title.
+
+### Example of the valid `**/*.mdx` file:
```
---
title: "Introduction to Forge42 Base Stack"
@@ -75,7 +78,7 @@ cd my-app
npm install
```
-### Example of the valid `04-getting-started/index.md` file:
+### Example of the valid `**/*.md` file:
```
---
title: Getting Started
diff --git a/app/components/command-k/components/search-input.tsx b/app/components/command-k/components/search-input.tsx
index 7ccf51f..da71c9f 100644
--- a/app/components/command-k/components/search-input.tsx
+++ b/app/components/command-k/components/search-input.tsx
@@ -13,7 +13,7 @@ export function SearchInput({ value, onChange, placeholder, ref }: SearchInputPr
return (
diff --git a/app/components/command-k/components/trigger-button.tsx b/app/components/command-k/components/trigger-button.tsx
index 3c243e2..e95d791 100644
--- a/app/components/command-k/components/trigger-button.tsx
+++ b/app/components/command-k/components/trigger-button.tsx
@@ -12,7 +12,7 @@ export const TriggerButton = ({
type="button"
onClick={onOpen}
className={cn(
- "group flex items-center gap-2 rounded-lg border px-3 py-2 text-sm shadow-sm transition-all duration-200",
+ "group flex items-center gap-2 rounded-lg border px-2 py-1.5 text-sm shadow-sm transition-all duration-200 xl:px-3 xl:py-2",
"border-[var(--color-trigger-border)] bg-[var(--color-trigger-bg)] text-[var(--color-trigger-text)]",
"hover:border-[var(--color-trigger-hover-border)] hover:bg-[var(--color-trigger-hover-bg)] hover:shadow-md",
"focus:border-[var(--color-trigger-focus-border)] focus:outline-none focus:ring-2 focus:ring-[var(--color-trigger-focus-ring)]"
@@ -22,8 +22,8 @@ export const TriggerButton = ({
name="Search"
className={cn("size-4 transition-colors", "group-hover:text-[var(--color-trigger-hover-text)]")}
/>
-
{placeholder}
-
+
{placeholder}
+
page._meta.fileName !== "_index.mdx" && page.slug !== "_index")
+ .filter((page) => page.slug !== "_index")
.flatMap((page) => {
const pageSlug = getPageSlug(page)
const pageUrl = pageSlug.startsWith("/") ? pageSlug : `/${pageSlug}`
const sections = extractHeadingSections(page.rawMdx)
-
return sections.map((section) => {
const heading = section.heading === "_intro" ? page.title : section.heading
diff --git a/app/components/command-k/hooks/use-debounce.ts b/app/components/command-k/hooks/use-debounce.ts
new file mode 100644
index 0000000..1a01372
--- /dev/null
+++ b/app/components/command-k/hooks/use-debounce.ts
@@ -0,0 +1,12 @@
+import { useEffect, useState } from "react"
+
+export function useDebounce(value: T, delay = 250) {
+ const [debouncedValue, setDebouncedValue] = useState(value)
+
+ useEffect(() => {
+ const id = setTimeout(() => setDebouncedValue(value), delay)
+ return () => clearTimeout(id)
+ }, [value, delay])
+
+ return debouncedValue
+}
diff --git a/app/components/command-k/hooks/use-search.ts b/app/components/command-k/hooks/use-search.ts
index cd06c74..607b86e 100644
--- a/app/components/command-k/hooks/use-search.ts
+++ b/app/components/command-k/hooks/use-search.ts
@@ -1,9 +1,10 @@
-import { useState } from "react"
+import { useEffect, useRef, useState } from "react"
import { useFetcher } from "react-router"
import z from "zod"
import type { Version } from "~/utils/version-resolvers"
import { versions } from "~/utils/versions"
import type { SearchResult } from "../search-types"
+import { useDebounce } from "./use-debounce"
export const commandKSearchParamsSchema = z.object({
query: z.string(),
@@ -23,31 +24,36 @@ function createCommandKSearchParams(params: Record) {
return { params: new URLSearchParams(result.data) }
}
+const debounceMs = 250
+const minChars = 1
+
export function useSearch({ version }: { version: Version }) {
const fetcher = useFetcher<{ results: SearchResult[] }>()
const [query, setQuery] = useState("")
- //we will show results as soon as we have a non-empty query
- //this does not debounce or wait for fetcher.state === "idle".
+ const debouncedQuery = useDebounce(query, debounceMs)
+ const lastLoadedRef = useRef(null)
+
const results = query.trim() ? (fetcher.data?.results ?? []) : []
function search(q: string) {
- const trimmed = q.trim()
+ setQuery(q)
+ }
- if (!trimmed) {
- setQuery("")
+ useEffect(() => {
+ const trimmed = debouncedQuery.trim()
+ if (!trimmed || trimmed.length < minChars) {
+ lastLoadedRef.current = null
return
}
- setQuery(trimmed)
+ if (lastLoadedRef.current === trimmed) return
+ lastLoadedRef.current = trimmed
+
const { params } = createCommandKSearchParams({ query: trimmed, version })
- if (!params) {
- // biome-ignore lint/suspicious/noConsole: keep for debugging
- console.error("Failed to create search parameters.")
- return
- }
+ if (!params) return
fetcher.load(`/search?${params.toString()}`)
- }
+ }, [debouncedQuery, version, fetcher])
return {
results,
diff --git a/app/components/icon-link.tsx b/app/components/icon-link.tsx
new file mode 100644
index 0000000..280ee9e
--- /dev/null
+++ b/app/components/icon-link.tsx
@@ -0,0 +1,28 @@
+import type { ComponentProps } from "react"
+import { Icon } from "~/ui/icon/icon"
+import type { IconName } from "~/ui/icon/icons/types"
+import { cn } from "~/utils/css"
+
+interface IconLinkProps extends ComponentProps<"a"> {
+ name: IconName
+}
+
+export const IconLink = ({ name, className, ...props }: IconLinkProps) => {
+ const { href } = props
+ const isExternal = typeof href === "string" && /^https?:\/\//i.test(href)
+ return (
+
+
+
+ )
+}
diff --git a/app/components/logo.tsx b/app/components/logo.tsx
index 7a6644a..78cf6c2 100644
--- a/app/components/logo.tsx
+++ b/app/components/logo.tsx
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
export const Logo = ({ children }: { children: ReactNode }) => {
return (
-
+
{children}
)
diff --git a/app/components/page-mdx-article.tsx b/app/components/page-mdx-article.tsx
index 89f8de2..7419032 100644
--- a/app/components/page-mdx-article.tsx
+++ b/app/components/page-mdx-article.tsx
@@ -1,18 +1,18 @@
-import type { Page } from "~/routes/documentation-page"
+import type { Page } from "content-collections"
import { Title } from "~/ui/title"
import { MDXWrapper } from "./mdx-wrapper"
export default function PageMdxArticle({ page }: { page: Page }) {
return (
-
+
{page.title}
{page.description && (
-
+
{page.description}
-
+
)}
diff --git a/app/components/sidebar/build-breadcrumbs.ts b/app/components/sidebar/build-breadcrumbs.ts
new file mode 100644
index 0000000..8e23ede
--- /dev/null
+++ b/app/components/sidebar/build-breadcrumbs.ts
@@ -0,0 +1,41 @@
+import type { Page } from "content-collections"
+import type { SidebarSection } from "~/utils/create-sidebar-tree"
+import { buildDocPathFromSlug } from "~/utils/path-builders"
+
+export const buildBreadcrumbs = (
+ items: SidebarSection[],
+ pathname: string,
+ documentationPages: Pick[] = []
+) => {
+ // for standalone pages: /:filename
+ for (const page of documentationPages) {
+ const docPath = buildDocPathFromSlug(page.slug)
+ if (docPath === pathname) {
+ return [page.title]
+ }
+ }
+
+ // for sectioned pages: /:section/:subsection?/:filename
+ let trail: string[] = []
+
+ const walk = (section: SidebarSection, acc: string[]): boolean => {
+ for (const doc of section.documentationPages) {
+ const docPath = buildDocPathFromSlug(doc.slug)
+ if (docPath === pathname) {
+ trail = [...acc, section.title, doc.title]
+ return true
+ }
+ }
+
+ for (const sub of section.subsections) {
+ if (walk(sub, [...acc, section.title])) return true
+ }
+ return false
+ }
+
+ for (const root of items) {
+ if (walk(root, [])) break
+ }
+
+ return trail
+}
diff --git a/app/components/sidebar/build-breadcrumbs.tsx b/app/components/sidebar/build-breadcrumbs.tsx
deleted file mode 100644
index 5efc4c0..0000000
--- a/app/components/sidebar/build-breadcrumbs.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { href } from "react-router"
-import { splitSlugAndAppendVersion } from "~/utils/split-slug-and-append-version"
-import type { SidebarSection } from "./sidebar"
-
-// builds a breadcrumb trail from sidebar sections based on the current pathname
-export const buildBreadcrumb = (items: SidebarSection[], pathname: string) => {
- let trail: string[] = []
-
- const walk = (section: SidebarSection, acc: string[]) => {
- for (const doc of section.documentationPages) {
- const docPath = href("/:version/:section/:subsection?/:filename", splitSlugAndAppendVersion(doc.slug))
- if (docPath === pathname) {
- trail = [...acc, section.title, doc.title]
- return true
- }
- }
-
- for (const sub of section.subsections) {
- if (walk(sub, [...acc, section.title])) return true
- }
- return false
- }
-
- for (const root of items) {
- if (walk(root, [])) break
- }
-
- return trail
-}
diff --git a/app/components/sidebar/desktop-sidebar.tsx b/app/components/sidebar/desktop-sidebar.tsx
index 49bd847..eade5e9 100644
--- a/app/components/sidebar/desktop-sidebar.tsx
+++ b/app/components/sidebar/desktop-sidebar.tsx
@@ -1,14 +1,14 @@
+import type { SidebarTree } from "~/utils/create-sidebar-tree"
import { cn } from "~/utils/css"
-import type { SidebarSection } from "./sidebar"
import { SidebarContent } from "./sidebar-content"
-export const DesktopSidebarPanel = ({ items, className }: { items: SidebarSection[]; className: string }) => (
+export const DesktopSidebarPanel = ({ sidebarTree, className }: { sidebarTree: SidebarTree; className: string }) => (
-
+
)
diff --git a/app/components/sidebar/mobile-sidebar.tsx b/app/components/sidebar/mobile-sidebar.tsx
index f0c06f7..f08ea8b 100644
--- a/app/components/sidebar/mobile-sidebar.tsx
+++ b/app/components/sidebar/mobile-sidebar.tsx
@@ -1,26 +1,35 @@
+import { useParams } from "react-router"
+import { useDocumentationLayoutLoaderData } from "~/hooks/use-documentation-layout-loader-data"
import { BreadcrumbItem, Breadcrumbs } from "~/ui/breadcrumbs"
+import { IconButton } from "~/ui/icon-button"
import { Icon } from "~/ui/icon/icon"
+import type { SidebarTree } from "~/utils/create-sidebar-tree"
import { cn } from "~/utils/css"
+import { buildBreadcrumbs } from "./build-breadcrumbs"
import { useMobileSidebar } from "./mobile-sidebar-context"
-import type { SidebarSection } from "./sidebar"
import { SidebarContent } from "./sidebar-content"
const MobileSidebarMenuButton = () => {
const { open } = useMobileSidebar()
return (
-
+ className="text-[var(--color-text-normal)] transition-colors duration-200 hover:text-[var(--color-text-hover)]"
+ aria-label="Navigation menu"
+ />
)
}
-export const MobileSidebarHeader = ({ breadcrumbs }: { breadcrumbs: string[] }) => {
+export const MobileSidebarHeader = () => {
+ const params = useParams()
+ const {
+ sidebarTree: { sections, documentationPages },
+ } = useDocumentationLayoutLoaderData()
+ const { section, subsection, filename } = params
+ const currentPath = `/${[section, subsection, filename].filter(Boolean).join("/")}`
+ const breadcrumbs = buildBreadcrumbs(sections, currentPath, documentationPages)
return (
@@ -64,10 +73,10 @@ const MobileSidebarCloseButton = () => {
}
export const MobileSidebarPanel = ({
- items,
+ sidebarTree,
className,
}: {
- items: SidebarSection[]
+ sidebarTree: SidebarTree
className: string
}) => {
const { close, isOpen } = useMobileSidebar()
@@ -78,10 +87,9 @@ export const MobileSidebarPanel = ({
isOpen ? "translate-x-0" : "-translate-x-full",
className
)}
- aria-modal="true"
aria-label="Navigation menu"
>
-
+
)
diff --git a/app/components/sidebar/sidebar-content.tsx b/app/components/sidebar/sidebar-content.tsx
index 3b45c4f..6372021 100644
--- a/app/components/sidebar/sidebar-content.tsx
+++ b/app/components/sidebar/sidebar-content.tsx
@@ -1,19 +1,42 @@
import { useMobileView } from "~/hooks/use-mobile-view"
import { Accordion } from "~/ui/accordion"
-import type { SidebarSection } from "./sidebar"
-import { SectionItem } from "./sidebar-section"
+import type { SidebarTree } from "~/utils/create-sidebar-tree"
+import { buildStandaloneTo } from "~/utils/path-builders"
+import { useCurrentVersion } from "~/utils/version-resolvers"
+import { DocumentationNavLink, SectionItem } from "./sidebar-items"
-export const SidebarContent = ({ items, onClose }: { items: SidebarSection[]; onClose?: () => void }) => {
+export const SidebarContent = ({
+ sidebarTree,
+ onClose,
+}: {
+ sidebarTree: SidebarTree
+ onClose?: () => void
+}) => {
const { isMobile } = useMobileView()
-
+ const handle = isMobile ? onClose : undefined
+ const { sections, documentationPages } = sidebarTree
+ const version = useCurrentVersion()
return (
diff --git a/app/components/sidebar/sidebar-section.tsx b/app/components/sidebar/sidebar-items.tsx
similarity index 56%
rename from app/components/sidebar/sidebar-section.tsx
rename to app/components/sidebar/sidebar-items.tsx
index c5fb493..392c328 100644
--- a/app/components/sidebar/sidebar-section.tsx
+++ b/app/components/sidebar/sidebar-items.tsx
@@ -1,6 +1,7 @@
-import { NavLink, href } from "react-router"
+import { NavLink } from "react-router"
import { AccordionItem } from "~/ui/accordion"
-import { splitSlugAndAppendVersion } from "~/utils/split-slug-and-append-version"
+import { buildSectionedTo } from "~/utils/path-builders"
+import { useCurrentVersion } from "~/utils/version-resolvers"
import type { SidebarSection } from "./sidebar"
const getIndentClass = (depth: number) => {
@@ -8,58 +9,64 @@ const getIndentClass = (depth: number) => {
return indentMap[depth as keyof typeof indentMap] || "ml-8"
}
-interface SectionItemProps {
- item: SidebarSection
+type DocumentationNavLinkProps = {
+ title: string
+ to: string
depth?: number
- onItemClick?: () => void
-}
-
-interface SectionItemLinkProps {
- documentPage: { slug: string; title: string }
- depth: number
- onItemClick?: () => void
+ onClick?: () => void
}
-const SectionTitle = ({ title }: { title: string }) => {
- return (
-
- {title}
-
- )
-}
-
-const SectionItemLink = ({ documentPage, depth, onItemClick }: SectionItemLinkProps) => {
+export function DocumentationNavLink({ title, to, depth = 0, onClick }: DocumentationNavLinkProps) {
const indentClass = getIndentClass(depth)
return (
- `block rounded-md px-3 py-2 text-xs sm:text-sm md:text-base ${indentClass}
- ${isPending ? "text-[var(--color-text-hover)]" : ""}
- ${
- isActive
- ? "bg-[var(--color-background-active)] font-medium text-[var(--color-text-active)]"
- : "text-[var(--color-text-normal)] hover:text-[var(--color-text-hover)]"
- }
- `
+ `block rounded-md px-3 py-2 text-sm md:text-base ${indentClass}
+ ${isPending ? "text-[var(--color-text-hover)]" : ""}
+ ${
+ isActive
+ ? "bg-[var(--color-background-active)] font-medium text-[var(--color-text-active)]"
+ : "text-[var(--color-text-normal)] hover:text-[var(--color-text-hover)]"
+ }`
}
>
- {documentPage.title}
+ {title}
)
}
+interface SectionItemProps {
+ item: SidebarSection
+ depth?: number
+ onItemClick?: () => void
+}
+
+const SectionTitle = ({ title }: { title: string }) => {
+ return (
+
+ {title}
+
+ )
+}
+
export const SectionItem = ({ item, depth = 0, onItemClick }: SectionItemProps) => {
const isTopLevel = depth === 0
-
+ const version = useCurrentVersion()
const content = (
{item.documentationPages.length > 0 && (
{item.documentationPages.map((doc) => (
-
+
))}
)}
@@ -78,8 +85,8 @@ export const SectionItem = ({ item, depth = 0, onItemClick }: SectionItemProps)
return (
diff --git a/app/components/sidebar/sidebar.tsx b/app/components/sidebar/sidebar.tsx
index 42a7a43..b5a2bf8 100644
--- a/app/components/sidebar/sidebar.tsx
+++ b/app/components/sidebar/sidebar.tsx
@@ -1,6 +1,5 @@
-import { useLocation } from "react-router"
+import type { SidebarTree } from "~/utils/create-sidebar-tree"
import { cn } from "~/utils/css"
-import { buildBreadcrumb } from "./build-breadcrumbs"
import { DesktopSidebarPanel } from "./desktop-sidebar"
import { MobileSidebarHeader, MobileSidebarOverlay, MobileSidebarPanel } from "./mobile-sidebar"
import { MobileSidebarProvider } from "./mobile-sidebar-context"
@@ -9,29 +8,23 @@ export type SidebarSection = {
title: string
slug: string
subsections: SidebarSection[]
- documentationPages: {
- title: string
- slug: string
- }[]
+ documentationPages: { title: string; slug: string }[]
}
+
interface SidebarProps {
- items: SidebarSection[]
+ sidebarTree: SidebarTree
className?: string
}
-export const Sidebar = ({ items, className = "" }: SidebarProps) => {
- const location = useLocation()
- const breadcrumbs = buildBreadcrumb(items, location.pathname)
-
+export const Sidebar = ({ sidebarTree, className = "" }: SidebarProps) => {
return (
<>
-
-
+
-
+
-
+
>
diff --git a/app/components/sidebar/tests/build-breadcrumbs.test.ts b/app/components/sidebar/tests/build-breadcrumbs.test.ts
new file mode 100644
index 0000000..6920ede
--- /dev/null
+++ b/app/components/sidebar/tests/build-breadcrumbs.test.ts
@@ -0,0 +1,105 @@
+import type { Page } from "content-collections"
+import type { SidebarSection } from "~/utils/create-sidebar-tree"
+import { buildBreadcrumbs } from "../build-breadcrumbs"
+
+const makePage = (slug: string, title: string, section = slug.split("/")[0] ?? ""): Page => ({
+ content: "",
+ slug,
+ section,
+ rawMdx: "",
+ title,
+ summary: "",
+ description: "",
+ _meta: {
+ filePath: "",
+ fileName: "",
+ directory: "",
+ path: "",
+ extension: ".mdx",
+ },
+})
+
+const makeSection = (overrides: Partial
= {}): SidebarSection => ({
+ title: "",
+ slug: "",
+ documentationPages: [],
+ subsections: [],
+ ...overrides,
+})
+
+const makeStandalone = (slug: string, title: string): Pick => ({
+ slug,
+ title,
+})
+
+describe("buildBreadcrumbs", () => {
+ it("returns [] when pathname doesn't match any doc", () => {
+ const items: SidebarSection[] = [
+ makeSection({
+ title: "Getting Started",
+ slug: "getting-started",
+ documentationPages: [makePage("getting-started/intro", "Intro")],
+ }),
+ ]
+
+ expect(buildBreadcrumbs(items, "/getting-started/unknown")).toEqual([])
+ })
+
+ it("returns [section, doc] for a top-level doc within a section", () => {
+ const items: SidebarSection[] = [
+ makeSection({
+ title: "Getting Started",
+ slug: "getting-started",
+ documentationPages: [makePage("getting-started/intro", "Intro")],
+ }),
+ ]
+
+ expect(buildBreadcrumbs(items, "/getting-started/intro")).toEqual(["Getting Started", "Intro"])
+ })
+
+ it("returns full trail for a nested doc (root โ sub โ doc)", () => {
+ const items: SidebarSection[] = [
+ makeSection({
+ title: "Configuration",
+ slug: "configuration",
+ subsections: [
+ makeSection({
+ title: "Advanced",
+ slug: "configuration/advanced",
+ documentationPages: [makePage("configuration/advanced/tuning", "Tuning")],
+ }),
+ ],
+ documentationPages: [makePage("configuration/setup", "Setup")],
+ }),
+ ]
+
+ expect(buildBreadcrumbs(items, "/configuration/advanced/tuning")).toEqual(["Configuration", "Advanced", "Tuning"])
+ })
+
+ it("returns [] for an empty sidebar", () => {
+ const items: SidebarSection[] = []
+ expect(buildBreadcrumbs(items, "/any-path")).toEqual([])
+ })
+
+ it("returns [doc] for a standalone top-level doc", () => {
+ const items: SidebarSection[] = []
+ const standalone = [makeStandalone("changelog", "Changelog")]
+
+ expect(buildBreadcrumbs(items, "/changelog", standalone)).toEqual(["Changelog"])
+ })
+
+ it("matches sectioned path when pathname is sectioned, and standalone when pathname is standalone", () => {
+ const items: SidebarSection[] = [
+ makeSection({
+ title: "Guides",
+ slug: "guides",
+ documentationPages: [makePage("guides/quickstart", "Quickstart")],
+ }),
+ ]
+ const standalone = [makeStandalone("quickstart", "Quickstart")]
+
+ expect(buildBreadcrumbs(items, "/guides/quickstart", standalone)).toEqual(["Guides", "Quickstart"])
+
+ expect(buildBreadcrumbs(items, "/quickstart", standalone)).toEqual(["Quickstart"])
+ })
+})
diff --git a/app/components/sidebar/tests/build-breadcrumbs.test.tsx b/app/components/sidebar/tests/build-breadcrumbs.test.tsx
deleted file mode 100644
index 723b4e6..0000000
--- a/app/components/sidebar/tests/build-breadcrumbs.test.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { describe, expect, it, vi } from "vitest"
-import type { SidebarSection } from "../sidebar"
-
-vi.mock("~/utils/split-slug-and-append-version", () => ({
- splitSlugAndAppendVersion: (slug: string) => {
- const parts = slug.split("/").filter(Boolean)
- const version = "v1.0.0"
-
- if (parts.length === 2) {
- const [section, filename] = parts
- return { version, section, filename }
- }
- if (parts.length === 3) {
- const [section, subsection, filename] = parts
- return { version, section, subsection, filename }
- }
-
- throw new Error(`Bad slug in test: ${slug}`)
- },
-}))
-
-import { buildBreadcrumb } from "../build-breadcrumbs"
-
-type Doc = { slug: string; title: string }
-const makeDoc = (slug: string, title: string): Doc => ({ slug, title })
-
-type MinimalSection = Pick
-const makeSection = (overrides: Partial = {}) => ({
- title: "",
- slug: "",
- documentationPages: [],
- subsections: [],
- ...overrides,
-})
-
-describe("buildBreadcrumb (versioned paths via splitSlugAndAppendVersion)", () => {
- it("returns [] when pathname doesn't match any doc", () => {
- const items = [
- makeSection({
- title: "Getting Started",
- slug: "getting-started",
- documentationPages: [makeDoc("getting-started/intro", "Intro")],
- }),
- ]
- expect(buildBreadcrumb(items, "/v1.0.0/getting-started/unknown")).toEqual([])
- })
-
- it("returns [section, doc] for a top-level doc", () => {
- const items = [
- makeSection({
- title: "Getting Started",
- slug: "getting-started",
- documentationPages: [makeDoc("getting-started/intro", "Intro")],
- }),
- ]
- expect(buildBreadcrumb(items, "/v1.0.0/getting-started/intro")).toEqual(["Getting Started", "Intro"])
- })
-
- it("returns full trail for a nested doc (root โ sub โ doc)", () => {
- const items = [
- makeSection({
- title: "Configuration",
- slug: "configuration",
- subsections: [
- makeSection({
- title: "Advanced",
- slug: "configuration/advanced",
- documentationPages: [makeDoc("configuration/advanced/tuning", "Tuning")],
- }),
- ],
- documentationPages: [makeDoc("configuration/setup", "Setup")],
- }),
- ]
- expect(buildBreadcrumb(items, "/v1.0.0/configuration/advanced/tuning")).toEqual([
- "Configuration",
- "Advanced",
- "Tuning",
- ])
- })
-})
diff --git a/app/components/theme-toggle.tsx b/app/components/theme-toggle.tsx
index 32e4a45..0b0ea47 100644
--- a/app/components/theme-toggle.tsx
+++ b/app/components/theme-toggle.tsx
@@ -22,12 +22,10 @@ export function ThemeToggle() {
const isDarkTheme = theme === "dark"
return (
-
-
-
+
)
}
diff --git a/app/components/versions-dropdown.tsx b/app/components/versions-dropdown.tsx
index bf64dae..a6b0d1b 100644
--- a/app/components/versions-dropdown.tsx
+++ b/app/components/versions-dropdown.tsx
@@ -1,12 +1,12 @@
import { useState } from "react"
import { useNavigate } from "react-router"
import { Icon } from "~/ui/icon/icon"
-import { getCurrentVersion, homepageUrlWithVersion, isKnownVersion } from "~/utils/version-resolvers"
+import { homepageUrlWithVersion, isKnownVersion, useCurrentVersion } from "~/utils/version-resolvers"
import { versions } from "~/utils/versions"
export function VersionDropdown() {
const navigate = useNavigate()
- const { version: currentVersion } = getCurrentVersion()
+ const currentVersion = useCurrentVersion()
const [selectedVersion, setSelectedVersion] = useState(currentVersion)
function onChange(e: React.ChangeEvent) {
@@ -29,7 +29,7 @@ export function VersionDropdown() {
)
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
index f0cf59e..4e466ef 100644
--- a/app/entry.server.tsx
+++ b/app/entry.server.tsx
@@ -9,10 +9,12 @@ import i18n from "./localization/i18n" // your i18n configuration file
import i18nextOpts from "./localization/i18n.server"
import { resources } from "./localization/resource"
import { preloadSearchIndexes } from "./server/search-index"
+import { preloadContentCollections } from "./utils/load-content"
// Reject all pending promises from handler functions after 10 seconds
export const streamTimeout = 10000
+await preloadContentCollections()
await preloadSearchIndexes()
export default async function handleRequest(
diff --git a/app/env.server.ts b/app/env.server.ts
index ea802be..61d3ae2 100644
--- a/app/env.server.ts
+++ b/app/env.server.ts
@@ -4,7 +4,8 @@ const envSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
APP_ENV: z.enum(["development", "staging", "production"]).default("development"),
GITHUB_OWNER: z.string()?.optional(), // optional - for edit/report an issue for the documentation page
- GITHUB_REPO: z.string()?.optional(), // optional - for edit/report an issue for the documentation page
+ GITHUB_REPO: z.string()?.optional(), // optional - for edit/report an issue for the documentation page,
+ GITHUB_REPO_URL: z.string().optional(), // optional - for navigation to the github repository page
})
type ServerEnv = z.infer
@@ -53,6 +54,7 @@ export function getClientEnv() {
NODE_ENV: serverEnv.NODE_ENV,
GITHUB_OWNER: serverEnv.GITHUB_OWNER,
GITHUB_REPO: serverEnv.GITHUB_REPO,
+ GITHUB_REPO_URL: serverEnv.GITHUB_REPO_URL,
}
}
diff --git a/app/hooks/use-previous-next-pages.ts b/app/hooks/use-previous-next-pages.ts
index a2882d5..a90cdbc 100644
--- a/app/hooks/use-previous-next-pages.ts
+++ b/app/hooks/use-previous-next-pages.ts
@@ -1,21 +1,41 @@
import { href, useLocation } from "react-router"
-import type { SidebarSection } from "~/components/sidebar/sidebar"
+import type { SidebarTree } from "~/utils/create-sidebar-tree"
import { flattenSidebarItems } from "~/utils/flatten-sidebar"
-import { splitSlugAndAppendVersion } from "~/utils/split-slug-and-append-version"
+import { splitSlug } from "~/utils/split-slug"
+import { useCurrentVersion } from "~/utils/version-resolvers"
-export function usePreviousNextPages(sections: SidebarSection[]) {
+function buildDocHref(slug: string, version: string) {
+ const parts = slug.split("/").filter(Boolean)
+ if (parts.length === 1) {
+ const filename = parts[0]
+ return href("/:version/:section?/:subsection?/:filename", { version, filename })
+ }
+ const { section, subsection, filename } = splitSlug(slug)
+ return href("/:version/:section?/:subsection?/:filename", {
+ version,
+ section,
+ subsection,
+ filename,
+ })
+}
+
+export function usePreviousNextPages(sidebarTree: SidebarTree) {
const { pathname } = useLocation()
+ const version = useCurrentVersion()
+ const { sections, documentationPages } = sidebarTree
+
+ const flatFromSections = flattenSidebarItems(sections)
+ const flatStandalone = documentationPages.map((p) => ({ title: p.title, slug: p.slug }))
+ const flatPages = [...flatStandalone, ...flatFromSections]
- const flatPages = flattenSidebarItems(sections)
const currentIndex = flatPages.findIndex((p) => pathname.endsWith(p.slug))
const getNavItem = (index: number) => {
const item = flatPages[index]
if (!item) return undefined
-
return {
title: item.title,
- to: href("/:version/:section/:subsection?/:filename", splitSlugAndAppendVersion(item.slug)),
+ to: buildDocHref(item.slug, version),
}
}
diff --git a/app/root.tsx b/app/root.tsx
index afea2ae..5ca7444 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -17,13 +17,18 @@ import type { Route } from "./+types/root"
import { ClientHintCheck, getHints } from "./services/client-hints"
import tailwindcss from "./tailwind.css?url"
import { fonts } from "./utils/fonts"
+import { getDomain } from "./utils/get-domain"
import { THEME, getStorageItem, setStorageItem } from "./utils/local-storage"
import { getSystemTheme } from "./utils/theme"
+import { normalizeVersion } from "./utils/version-resolvers"
-export async function loader({ context, request }: Route.LoaderArgs) {
+export async function loader({ context, request, params }: Route.LoaderArgs) {
const { lang, clientEnv } = context
const hints = getHints(request)
- return { lang, clientEnv, hints }
+ const { version } = params
+ const { version: normalizedVersion } = normalizeVersion(version)
+ const { domain } = getDomain(request)
+ return { lang, clientEnv, hints, version: normalizedVersion, domain }
}
export const links: LinksFunction = () => [{ rel: "stylesheet", href: tailwindcss }]
diff --git a/app/routes.ts b/app/routes.ts
index 0f336b3..fa4da1f 100644
--- a/app/routes.ts
+++ b/app/routes.ts
@@ -5,7 +5,7 @@ export default [
route("search", "routes/search.ts"),
layout("routes/documentation-layout.tsx", [
route(":version?/home", "routes/documentation-homepage.tsx"),
- route(":version/:section/:subsection?/:filename", "routes/documentation-page.tsx"),
+ route(":version/:section?/:subsection?/:filename", "routes/documentation-page.tsx"),
]),
route("sitemap-index.xml", "routes/sitemap-index[.]xml.ts"),
route("robots.txt", "routes/robots[.]txt.ts"),
diff --git a/app/routes/documentation-homepage.tsx b/app/routes/documentation-homepage.tsx
index 28f2bdb..9ab2888 100644
--- a/app/routes/documentation-homepage.tsx
+++ b/app/routes/documentation-homepage.tsx
@@ -1,16 +1,30 @@
import GithubContributeLinks from "~/components/github-contribute-links"
import PageMdxArticle from "~/components/page-mdx-article"
-import { loadContentCollections } from "~/utils/load-content-collections"
+import { getDomain } from "~/utils/get-domain"
+import { getContent } from "~/utils/load-content"
+import { generateMetaFields } from "~/utils/seo"
import { resolveVersionForHomepage } from "~/utils/version-resolvers"
import type { Route } from "./+types/documentation-homepage"
-export async function loader({ params }: Route.LoaderArgs) {
+export const meta = ({ data }: Route.MetaArgs) => {
+ const { page, domain, version } = data
+ const title = page.title
+ const description = page.description
+ return generateMetaFields({
+ domain,
+ path: `/${version}/home`,
+ title: `${title} ยท Package Name`,
+ description,
+ })
+}
+
+export async function loader({ params, request }: Route.LoaderArgs) {
const { version } = resolveVersionForHomepage(params.version)
- const { allPages } = await loadContentCollections(version)
+ const { allPages } = await getContent(version)
const page = allPages.find((p) => p._meta.path === "_index")
if (!page) throw new Response("Not Found", { status: 404 })
-
- return { page, version }
+ const { domain } = getDomain(request)
+ return { page, version, domain }
}
export default function DocumentationHomepage({ loaderData }: Route.ComponentProps) {
diff --git a/app/routes/documentation-layout.tsx b/app/routes/documentation-layout.tsx
index 5a90979..2a391fd 100644
--- a/app/routes/documentation-layout.tsx
+++ b/app/routes/documentation-layout.tsx
@@ -1,6 +1,7 @@
-import { Outlet } from "react-router"
+import { Outlet, useRouteLoaderData } from "react-router"
import { CommandK } from "~/components/command-k/components/command-k"
import { Header } from "~/components/header"
+import { IconLink } from "~/components/icon-link"
import { Logo } from "~/components/logo"
import { Sidebar } from "~/components/sidebar/sidebar"
import { ThemeToggle } from "~/components/theme-toggle"
@@ -16,6 +17,8 @@ export async function loader({ params, request }: Route.LoaderArgs) {
}
export default function DocumentationLayout({ loaderData }: Route.ComponentProps) {
const { sidebarTree, version } = loaderData
+ const { clientEnv } = useRouteLoaderData("root")
+ const { GITHUB_REPO_URL } = clientEnv
return (
@@ -24,20 +27,22 @@ export default function DocumentationLayout({ loaderData }: Route.ComponentProps
{/* Replace with your Logo */}
REACT ROUTER DEVTOOLS
-
-
-
+
+
+
+ {GITHUB_REPO_URL && }
-
+
+ {/* You can add custom footer component here */}
)
}
diff --git a/app/routes/documentation-page.tsx b/app/routes/documentation-page.tsx
index d80348e..4339773 100644
--- a/app/routes/documentation-page.tsx
+++ b/app/routes/documentation-page.tsx
@@ -5,27 +5,42 @@ import { TableOfContents } from "~/components/table-of-content"
import { useDocumentationLayoutLoaderData } from "~/hooks/use-documentation-layout-loader-data"
import { usePreviousNextPages } from "~/hooks/use-previous-next-pages"
import { extractHeadingTreeFromMarkdown } from "~/utils/extract-heading-tree-from-mdx"
-import { loadContentCollections } from "~/utils/load-content-collections"
+import { getDomain } from "~/utils/get-domain"
+import { getContent } from "~/utils/load-content"
+import { buildDocPathFromSlug, buildDocSlug } from "~/utils/path-builders"
+import { generateMetaFields } from "~/utils/seo"
import { normalizeVersion } from "~/utils/version-resolvers"
import type { Route } from "./+types/documentation-page"
-export async function loader({ params }: Route.LoaderArgs) {
+export const meta = ({ data }: Route.MetaArgs) => {
+ const { page, domain, version } = data
+ const docPath = buildDocPathFromSlug(page.slug)
+ const fullPath = `/${[version, docPath.replace(/^\//, "")].filter(Boolean).join("/")}`
+
+ return generateMetaFields({
+ domain,
+ path: fullPath,
+ // change "Package Name" to your package name
+ title: `${page.title} ยท Package Name`,
+ description: page.description,
+ })
+}
+
+export async function loader({ params, request }: Route.LoaderArgs) {
const { version: v, section, subsection, filename } = params
- if (!section || !filename) throw new Response("Not Found", { status: 404 })
+ if (!filename) throw new Response("Not Found", { status: 404 })
const { version } = normalizeVersion(v)
+ const slug = buildDocSlug({ section, subsection, filename })
- const slug = [section, subsection, filename].filter(Boolean).join("/")
-
- const { allPages } = await loadContentCollections(version)
+ const { allPages } = await getContent(version)
const page = allPages.find((p) => p.slug === slug)
if (!page) throw new Response("Not Found", { status: 404 })
- return { page, version }
+ const { domain } = getDomain(request)
+ return { page, version, domain }
}
-export type Page = Awaited>["page"]
-
export default function DocumentationPage({ loaderData }: Route.ComponentProps) {
const { page } = loaderData
const { sidebarTree } = useDocumentationLayoutLoaderData()
@@ -38,12 +53,12 @@ export default function DocumentationPage({ loaderData }: Route.ComponentProps)
-
+
)
}
diff --git a/app/routes/index.tsx b/app/routes/index.tsx
index baced90..0b89f42 100644
--- a/app/routes/index.tsx
+++ b/app/routes/index.tsx
@@ -2,8 +2,28 @@ import { href, useNavigate } from "react-router"
import { Header } from "~/components/header"
import { Logo } from "~/components/logo"
import { Icon } from "~/ui/icon/icon"
+import { getDomain } from "~/utils/get-domain"
+import { generateMetaFields } from "~/utils/seo"
import { getLatestVersion } from "~/utils/version-resolvers"
+import type { Route } from "./+types"
+export const meta = ({ data }: Route.MetaArgs) => {
+ const { domain } = data
+
+ return generateMetaFields({
+ domain,
+ path: "/",
+ // change "Package Name" to your package name
+ title: "Package Name",
+ // update description
+ description: "Professional Development Made Simple",
+ })
+}
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const { domain } = getDomain(request)
+ return { domain }
+}
export default function Index() {
const navigate = useNavigate()
// Customize index page
@@ -21,7 +41,7 @@ export default function Index() {
Version {getLatestVersion()} Now Available
-
+
Professional Development
diff --git a/app/server/search-index.ts b/app/server/search-index.ts
index 8390151..e1296b8 100644
--- a/app/server/search-index.ts
+++ b/app/server/search-index.ts
@@ -14,6 +14,7 @@ export async function preloadSearchIndexes() {
if (!searchIndexes.has(version)) {
const { allPages } = await loadContentCollections(version)
const searchIndex = createSearchIndex(allPages)
+
searchIndexes.set(version, searchIndex)
}
})
diff --git a/app/ui/accordion.tsx b/app/ui/accordion.tsx
index 62bba16..04b16cb 100644
--- a/app/ui/accordion.tsx
+++ b/app/ui/accordion.tsx
@@ -37,7 +37,7 @@ const AccordionContent = ({ isOpen, children }: { isOpen: boolean; children: Rea
* Content visibility toggles when the heading is clicked. Supports smooth transitions.
*
* @param title - The heading text for the accordion item.
- * @param titleElement - The HTML heading element tag to render (`h1` through `h6`).
+ * @param titleElement - The HTML heading element tag to render (`h1` through `h5`).
* @param titleClassName - Optional classes to customize the title's appearance.
* @param content - The content to show/hide when toggling the accordion.
* @param defaultOpen - Whether the item should be open by default.
@@ -66,18 +66,19 @@ export const AccordionItem = ({
const [isOpen, setIsOpen] = useState(defaultOpen)
const buttonClasses =
- "flex gap-2 items-center w-full p-2 transition-all duration-200 text-[var(--color-text-normal)] hover:text-[var(--color-text-hover)] hover:cursor-pointer rounded-md"
+ "flex gap-2 items-center w-full p-2 transition-transform duration-200 text-[var(--color-text-normal)] hover:text-[var(--color-text-hover)] hover:cursor-pointer rounded-md"
const iconClasses = "w-4 h-4 transition-transform duration-300"
return (
-
)
diff --git a/app/ui/alert.tsx b/app/ui/alert.tsx
index 7d9c99a..e196583 100644
--- a/app/ui/alert.tsx
+++ b/app/ui/alert.tsx
@@ -2,7 +2,6 @@ import type { ReactNode } from "react"
import { useTranslation } from "react-i18next"
import { cn } from "~/utils/css"
import { Icon } from "./icon/icon"
-import { Title } from "./title"
interface AlertProps {
children: ReactNode
@@ -54,15 +53,20 @@ export const Alert = ({ children, title, variant, className = "" }: AlertProps)
const defaultTitle = variant === "info" ? t("titles.good_to_know") : t("titles.warning")
return (
-
-
+
+
{getIcon()}
-
- {title || defaultTitle}
-
+
{title || defaultTitle}
-
{children}
+
+ {children}
+
)
}
diff --git a/app/ui/breadcrumbs.tsx b/app/ui/breadcrumbs.tsx
index 1be92e2..5c3705c 100644
--- a/app/ui/breadcrumbs.tsx
+++ b/app/ui/breadcrumbs.tsx
@@ -17,7 +17,7 @@ interface BreadcrumbItemProps {
export const BreadcrumbItem = ({ children, href, isActive = false, className }: BreadcrumbItemProps) => {
const classes = cn(
- "block text-ellipsis text-start font-medium text-[var(--color-text-normal)] text-sm sm:text-base md:text-lg",
+ "text-ellipsis text-start font-medium text-[var(--color-text-normal)] text-sm sm:text-base md:text-lg",
isActive && "pointer-events-none font-semibold text-[var(--color-text-active)]",
className
)
@@ -36,18 +36,16 @@ export const BreadcrumbItem = ({ children, href, isActive = false, className }:
}
export const Breadcrumbs = ({ children, className }: BreadcrumbsProps) => {
- const breadcrumbItems = Children.toArray(children).filter(
+ const items = Children.toArray(children).filter(
(child) => isValidElement(child) && child.type === BreadcrumbItem
) as React.ReactElement
[]
return (
-