diff --git a/.changeset/opinionated-mdx-rebuild.md b/.changeset/opinionated-mdx-rebuild.md new file mode 100644 index 0000000..fcc25dc --- /dev/null +++ b/.changeset/opinionated-mdx-rebuild.md @@ -0,0 +1,26 @@ +--- +"@fulldotdev/sitex": minor +--- + +Rebuild Sitex around MDX-only pages and a framework-owned document head. + +Breaking changes: + +- Pages are MDX files only. TSX files in `src/pages` fail the build with a migration error; move page markup into layouts and blocks. Dynamic `[param]` routes and the `paths` export are removed. +- The `site` config is required: `sitex({ site: { name, url } })`. Optional fields: `description`, `titleTemplate`, `locale`, `image`, `favicon`, `twitter`, `organization`. +- Frontmatter follows a validated schema: `layout`, `title`, and `description` are required. `type` (`webpage` | `article` | `faq`) drives JSON-LD structured data; `image`, `canonical`, and `index` are optional overrides. Articles require `publishedAt`; FAQ pages require `questions`. +- Apps do not provide `index.html`. Layouts render the full document (``/``/``) and Sitex parses the rendered page to add missing head defaults (charset, viewport, canonical, og:url, robots, favicon, generator) and hoist JSON-LD into the head. +- `src/styles/global.css` is required and automatically imported as the global stylesheet entry. Tailwind is not bundled by Sitex; apps that use Tailwind install `tailwindcss` and `@tailwindcss/vite` themselves. +- `MarkdownLayoutProps` is renamed to `LayoutProps` and now includes the page schema fields. `PageContext`, `StaticPath`, and `StaticPaths` are removed. +- `sitex:pages` has a stable, statically declared `Page` type; generated `.sitex/pages.d.ts` files are no longer written (legacy ones are cleaned up automatically). +- The `src/pages/examples/**` exemption is removed; everything under `src/pages` is a page. +- The main entry no longer exports UI components (`Layout`, `Section`, `Typography`, `Price`, and friends). They moved to the Sitex shadcn registry; install them with `shadcn add` from `https://sitex.full.dev/r/{name}.json`. `@fulldotdev/sitex` now exports types only, plus the `/plugin` and `/island` entries. +- Globals moved to `src/globals/index.yaml` as the root locale file. The root locale name comes from the new `site.locale` option (default `"en"`); additional files like `nl.yaml` define locale variants served under `/nl` route prefixes and are exposed through the `locales` export of `sitex:globals`. A single named file like `en.yaml` still works as the root locale. +- Static HTML is rendered in-process during `vp build` through a dedicated SSR build environment; Sitex no longer boots a second Vite server after the client build. + +New: + +- Link prefetching is on by default: speculation rules let supporting browsers prerender on hover, with a small hover-prefetch fallback module for others. Opt out with `prefetch: false` or `data-no-prefetch`. +- `vp build` writes `sitemap.xml` (respects `index: false` and `canonical` overrides, `lastmod` from `updatedAt`/`publishedAt`) and a default `robots.txt` when the app ships none. +- Path-alias imports (like `@/components/alert`) now resolve inside MDX pages on newer Vite resolvers. +- Frontmatter validation errors are targeted per field, and MDX pages are typechecked against their layout props via `.sitex/typecheck`. diff --git a/.gitignore b/.gitignore index 0e4ecde..910882e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ coverage/ # Local Netlify folder .netlify + +# generated shadcn registry +apps/docs/public/r/ diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 4557048..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,8 +0,0 @@ -# AGENTS.md - -- SiteX is a Vite-based React framework for static content sites. -- The repo follows the Vite+ monorepo shape: docs app in `apps/docs`, package in `packages/sitex`. -- Docs routes live in `apps/docs/src/pages` and render to static HTML by default. -- Client interactivity uses imported islands with `client:load`, `client:only`, `client:visible`, `client:idle`, or `client:media`. -- Keep routes thin: routes choose layouts, layouts compose blocks, blocks use UI. -- Do not add new rendering modes, generated routes, content collections, or new directives casually. diff --git a/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index 016365a..0000000 --- a/CONTEXT.md +++ /dev/null @@ -1,224 +0,0 @@ -# Sitex - -Sitex is an experimental framework for building static sites from React route modules with narrowly scoped client interactivity. - -## Language - -**Static route**: -A URL that Sitex can render to static HTML ahead of serving a request. A static route may be authored directly as a local route file today, and may later be generated from data without becoming an SSR route. -_Avoid_: SSR route, request route - -**Local route**: -A static route represented by one concrete route file in the app. This is the current v0 routing target because it keeps routing explicit and simple. -_Avoid_: dynamic route - -**Route module**: -A file discovered by Sitex routing. A local route module default-exports a page component; a future generated static route module may default-export `definePages(...)`. -_Avoid_: page - -**Page component**: -The React component exported by a local route module to render one static route. -_Avoid_: route, entry - -**Generated static route**: -A static route produced from one route module plus one or more data entries. This is a possible future routing shape for external or local data sources, but not the current v0 default. -_Avoid_: dynamic route, SSR route - -**Generated static route module**: -A future route module that explicitly exports `definePages(...)` to produce multiple static routes. Generated static route modules should be signaled by their API shape, not by catch-all filename syntax. -_Avoid_: catch-all route, `[...page]` - -**Route entry**: -One data item used by a future generated static route module to produce one static route. -_Avoid_: page, route - -**App-provided data**: -Data loaded or declared by the app and passed into Sitex route APIs. Sitex core should not require a built-in content collection model. -_Avoid_: Sitex content collection - -**Recommended app structure**: -An app-owned organization where route modules stay thin, layouts compose page chrome, blocks define reusable page sections, and UI primitives hold low-level design-system components. Sitex recommends this structure but does not require or interpret it. -_Avoid_: framework layout system - -**Reference app**: -An app that both documents Sitex and proves the recommended app structure against real static routes and islands. The docs app is the current reference app. -_Avoid_: component showcase - -**Repository shape**: -The repo follows Vite+'s monorepo shape: `packages/sitex` contains the framework package and `apps/docs` contains the docs/reference app. -_Avoid_: mixing package and app concerns in the repository root - -**Framework import**: -Published apps should import Sitex through package-style public imports such as `@fulldotdev/sitex/plugin`. The in-repo docs app may import package source directly when dogfooding unpublished changes. -_Avoid_: exposing framework internals as app-facing public APIs - -**Hidden island runtime**: -The React runtime code Sitex uses under the hood to mark, load, hydrate, and client-render islands. App authors should not need to import this directly. -_Avoid_: React helpers - -**Public surface**: -The current public surface should be the Sitex Vite plugin and JSX client directives. Other runtime pieces should stay hidden behind compiler/plugin behavior. -_Avoid_: direct island imports, document helper imports - -**Router export**: -Route rendering internals are framework-owned virtual module code. Apps should not import route rendering helpers directly. -_Avoid_: public router helpers - -**TypeScript config export**: -`@fulldotdev/sitex/tsconfig` may remain as a convenience for framework-required compiler settings and JSX directive types. It should not become a broad app preference bundle. -_Avoid_: app style config - -**Directive validation**: -TypeScript may broadly allow `client:*` attributes, but the compiler is responsible for rejecting invalid directive placement such as HTML elements or local components. -_Avoid_: type-only enforcement - -**Package build**: -Sitex is published as `@fulldotdev/sitex` from `packages/sitex`. Public app-facing imports should use exported package entrypoints, while the in-repo docs app may import package source directly when dogfooding unpublished changes. -_Avoid_: app imports from unexported internals - -**Production build**: -The public production build command should be `vp build`. Any custom Sitex build script is experiment scaffolding; cleanup should keep static route rendering and asset emission inside the Sitex Vite plugin. -_Avoid_: separate Sitex build command - -**Development server**: -The public development command should be `vp dev`. Sitex development behavior should come from the Vite plugin rather than a custom dev server command. -_Avoid_: separate Sitex dev command - -**Document shell**: -The app-owned HTML structure for a page, including ``, ``, and ``. Sitex supplies small document primitives, but the app decides the shell through its own layouts. -_Avoid_: framework document, global app root - -**Root document shell**: -The ordinary TSX document markup returned by each route render through the app's root layout. Sitex should not require Sitex-specific document components or validate more than asset injection needs. -_Avoid_: page fragment - -**Document metadata**: -Plain TSX rendered inside the app-owned document shell, such as `` and `<meta>` tags. Sitex should not require helper components for ordinary metadata. -_Avoid_: metadata API - -**Build asset injection**: -Framework-owned insertion of production asset tags such as stylesheet and island client scripts. Sitex should not require app-placed `HeadContent` or `Scripts` helper components for this. -_Avoid_: document helper components - -**Style entry**: -An app-owned CSS import that Vite includes in the module graph. Sitex should support normal Vite CSS imports wherever the app chooses to place them. -_Avoid_: required global stylesheet component - -**CSS API**: -Sitex should not expose a CSS-specific API. Styling should flow through normal Vite CSS imports and Sitex should ensure those assets appear in static output. -_Avoid_: stylesheet helper, styles config - -**Island**: -An imported React component that opts into browser interactivity inside otherwise static HTML. -_Avoid_: app root, hydrated page - -**Island root**: -The imported React component that carries a `client:*` directive. An island root cannot be an HTML element or a component declared locally inside the same route module. -_Avoid_: intrinsic island, inline island - -**Rendering mode**: -The way Sitex decides where a page or component is rendered. Current page modes are static by default and explicit request-time rendering with `export const render = "server"`. Current island modes are static plus client rendering with `client:load`, `client:visible`, `client:idle`, or `client:media`, and browser-only rendering with `client:only`. -_Avoid_: hydration mode - -**Static rendering**: -The default rendering mode. Static rendering produces HTML ahead of serving requests and does not attach browser interactivity. -_Avoid_: server rendering - -**Client rendering**: -Browser rendering used for interactivity. `client:load`, `client:visible`, `client:idle`, and `client:media` combine static rendering with client rendering, while `client:only` uses client rendering only. -_Avoid_: hydration - -**Server rendering**: -Request-time page rendering on a server. Sitex supports this only through explicit route opt-in with `export const render = "server"`. -_Avoid_: static rendering - -**Server directive**: -A possible future directive for request-time server rendering behavior. Server directives are reserved conceptually but are not part of the current Sitex API. -_Avoid_: current API - -**Client-loaded island**: -An island marked with `client:load`. A client-loaded island is statically rendered first and then client-rendered for interactivity. -_Avoid_: browser-only island - -**Lazy island**: -An island marked with `client:visible`, `client:idle`, or `client:media`. A lazy island is statically rendered first and then client-rendered after the chosen browser condition is met. -_Avoid_: separate route, server island - -**Static children**: -Children passed through an island boundary as static slot HTML. Static children stay visible but do not become live React children in the parent island's client tree. -_Avoid_: included children, live React children - -**Static slot model**: -The Astro-like model for island children. Children are rendered as static slot HTML, and any nested `client:*` components inside them are independent islands rather than part of the parent island's live React tree. -_Avoid_: live subtree hydration - -**Independent island**: -An island that client-renders separately from any surrounding island. Static children may contain independent islands, but those islands do not share a React tree, context, or state with the surrounding island. -_Avoid_: included island, subtree island - -**Island props**: -Data passed from static rendering into an island root. Island props must be JSON-serializable data; behavior should live inside the island root component. -_Avoid_: function props, React element props - -**Prop serialization**: -Sitex v0 should use a simple JSON-only serialization boundary for island props. Astro-like support for `Date`, `Map`, `Set`, `BigInt`, and similar values is a possible later enhancement, not cleanup scope. -_Avoid_: rich serializer - -**Invalid island prop**: -A value that cannot safely cross the static-to-client boundary, such as a function, symbol, React element, cyclic object, or ambiguous dropped value. Invalid island props should cause a hard framework error. -_Avoid_: stringified function, silently dropped prop - -**Prop serialization error**: -A targeted Sitex error raised when an island prop cannot be represented by the v0 JSON-only serializer. -_Avoid_: raw `JSON.stringify` failure - -**Browser-only island**: -An island marked with `client:only`. A browser-only island has no initial static render; it is invisible until the browser renders it. -_Avoid_: hydrated island, fallback island - -**Simplicity principle**: -Sitex should keep the current experiment extremely small and explicit, even when that postpones useful framework conveniences. -_Avoid_: complete framework - -## Flagged Ambiguities - -**Hydration naming**: -The code currently uses `hydration` names for the island runtime, but the preferred product language is rendering modes: static rendering, client rendering, and future server rendering. Do not change the code during the grilling session; revisit this in the later cleanup/refactor. - -**Document helpers**: -The current `HeadContent` and `Scripts` helpers leak build asset concerns into the app-owned document shell. Cleanup should remove them and replace them with framework-owned build asset injection. - -**Document validation**: -Sitex should not validate document structure beyond what it needs for asset injection. Missing insertion points should produce targeted errors only when injection cannot proceed. - -**Nested island transform**: -Static children should be allowed to contain independent islands. The current compiler may miss literal nested `client:*` directives inside the same transformed JSX subtree; revisit this in the later cleanup/refactor. - -**Subtree island API**: -The earlier `children:include` experiment made a parent island and its children one React tree. It may never be needed because shared client state can be modeled by creating a larger island root component. - -**Shared client tree**: -A set of interactive components that need shared React state or context. A shared client tree should be composed inside one island root component rather than through a subtree hydration directive. -_Avoid_: `children:include` - -## Example Dialogue - -Developer: "Should this service page be a server route?" - -Domain expert: "No. It should be a static route unless it actually needs request-time data." - -Developer: "Where do page metadata and scripts belong?" - -Domain expert: "In the app-owned document shell. Sitex can provide primitives, but the app's layouts own the actual document structure." - -Developer: "This sidebar needs search and theme toggling. Is the whole page an island?" - -Domain expert: "No. The sidebar can be an island, while the page body remains static children." - -Developer: "Should `client:only` include fallback children?" - -Domain expert: "No. A browser-only island has no initial static render; it appears when the browser renders it." - -Developer: "Filters and results need shared React state. Should I use `children:include`?" - -Domain expert: "No. Make a `Dashboard` island root that renders the provider, filters, and results internally." diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a8d54fb..f9e389d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -SiteX is experimental. Keep changes small, explicit, and aligned with the current scope. +Sitex is experimental. Keep changes small, explicit, and aligned with the current scope. ## Local setup @@ -20,7 +20,7 @@ pnpm ready ## Scope -- Keep the public API small: the Vite plugin, TypeScript config, and client directives. -- Prefer static routes and explicit TSX files. -- Do not add SSR, generated routes, content collections, or new client directives without first documenting the decision. -- Preserve the reference app as a simple content-site example. +- Keep the public API small: the Vite plugin, the type exports, the TypeScript config, and the `client:*` directives. +- Pages are static MDX files rendered through app layouts. Document the decision before adding rendering modes, generated routes, or content collections. +- UI components live in `packages/ui` and ship through the shadcn registry, not npm. +- The docs app in `apps/docs` is the reference app; keep it a simple content site. diff --git a/apps/docs/components.json b/apps/docs/components.json index fc691b6..d9441da 100644 --- a/apps/docs/components.json +++ b/apps/docs/components.json @@ -5,7 +5,7 @@ "tsx": true, "tailwind": { "config": "", - "css": "src/styles/globals.css", + "css": "src/index.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" diff --git a/apps/docs/package.json b/apps/docs/package.json index f6f429f..316a3ac 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -10,29 +10,30 @@ }, "dependencies": { "@base-ui/react": "catalog:", - "@fontsource-variable/geist": "^5.2.9", + "@fontsource-variable/geist": "catalog:", "@fulldotdev/sitex": "workspace:*", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^1.16.0", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "@shikijs/core": "^4.1.0", - "@shikijs/engine-javascript": "^4.1.0", - "@shikijs/langs": "^4.1.0", - "@shikijs/themes": "^4.1.0", - "tailwind-merge": "^3.6.0", - "tw-animate-css": "^1.4.0" + "@shikijs/core": "catalog:", + "@shikijs/engine-javascript": "catalog:", + "@shikijs/langs": "catalog:", + "@shikijs/themes": "catalog:", + "class-variance-authority": "catalog:", + "clsx": "catalog:", + "lucide-react": "catalog:", + "react": "catalog:", + "react-dom": "catalog:", + "schema-dts": "catalog:", + "tailwind-merge": "catalog:", + "tw-animate-css": "catalog:" }, "devDependencies": { - "@tailwindcss/vite": "^4.1.13", - "@types/react": "^19.1.12", - "@types/react-dom": "^19.1.9", - "@vitejs/plugin-react": "^5.0.2", - "shadcn": "^4.8.3", - "tailwindcss": "^4.1.13", - "typescript": "^5.9.2", - "vite": "^8.0.16", - "vite-plus": "^0.1.24" + "@tailwindcss/vite": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-react": "catalog:", + "shadcn": "catalog:", + "tailwindcss": "catalog:", + "typescript": "catalog:", + "vite": "catalog:", + "vite-plus": "catalog:" } } diff --git a/apps/docs/public/favicon.svg b/apps/docs/public/favicon.svg index 2cd3557..64b8581 100644 --- a/apps/docs/public/favicon.svg +++ b/apps/docs/public/favicon.svg @@ -1,7 +1,4 @@ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> - <rect width="32" height="32" rx="7" fill="#ff5e00" /> - <path - d="M6.8 6.6 16 12.7l9.2-6.1-6.1 9.4 6.1 9.4-9.2-6.1-9.2 6.1 6.1-9.4z" - fill="#ffffff" - /> + <rect width="32" height="32" rx="7" fill="#ff5e00"/> + <path d="M6.8 6.6 16 12.7l9.2-6.1-6.1 9.4 6.1 9.4-9.2-6.1-9.2 6.1 6.1-9.4z" fill="#ffffff" transform="translate(16 16) scale(1.12) translate(-16 -16)"/> </svg> diff --git a/apps/docs/src/assets/.gitkeep b/apps/docs/src/assets/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/docs/src/assets/.gitkeep @@ -0,0 +1 @@ + diff --git a/apps/docs/src/components/blocks/sidebar-1.tsx b/apps/docs/src/components/blocks/sidebar-1.tsx index ee2d5c3..56dad6f 100644 --- a/apps/docs/src/components/blocks/sidebar-1.tsx +++ b/apps/docs/src/components/blocks/sidebar-1.tsx @@ -2,25 +2,11 @@ import { type ReactNode } from "react" -import { ChevronDownIcon, ExternalLinkIcon } from "lucide-react" +import { ExternalLinkIcon } from "lucide-react" +import { ModeToggle } from "@/components/mode-toggle" import { ThemeProvider } from "@/components/theme-provider" -import { - Breadcrumb, - BreadcrumbEllipsis, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb" import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" import { Header, HeaderContainer, HeaderGroup } from "@/components/ui/header" import { Logo, LogoImage, LogoText } from "@/components/ui/logo" import { Separator } from "@/components/ui/separator" @@ -37,35 +23,30 @@ import { SidebarProvider, SidebarTrigger, } from "@/components/ui/sidebar" -import { ThemeToggle } from "@/components/ui/theme-toggle" import { cn } from "@/lib/utils" type NavigationGroup = { label: string href?: string - links?: { + links?: readonly { label: string href: string }[] } +type SidebarSection = { + label: string + href: string + navigation: readonly NavigationGroup[] +} + type Sidebar1Props = { className?: string logo: { label: string href: string } - breadcrumb: { - items: { - label: string - href: string - }[] - menu: { - label: string - href: string - }[] - } - navigation: NavigationGroup[] + sections: readonly SidebarSection[] githubRepo: string path: string children?: ReactNode @@ -77,24 +58,36 @@ function normalizePath(path: string) { return path.replace(/\/$/, "") } -function toDomId(value: string) { - return value.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") +function findActiveSection( + sections: readonly SidebarSection[], + currentPath: string +) { + const matches = sections.filter((section) => { + const sectionPath = normalizePath(section.href) + + return ( + currentPath === sectionPath || currentPath.startsWith(`${sectionPath}/`) + ) + }) + + return matches.sort((a, b) => b.href.length - a.href.length)[0] ?? sections[0] } function Sidebar1({ className, logo, - breadcrumb, - navigation, + sections, githubRepo, path, children, }: Sidebar1Props) { const currentPath = normalizePath(path) + const activeSection = findActiveSection(sections, currentPath) + const navigation = activeSection?.navigation ?? [] return ( <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> - <SidebarProvider> + <SidebarProvider className="font-sans antialiased"> <Sidebar className={cn(className)} collapsible="offcanvas" @@ -111,6 +104,24 @@ function Sidebar1({ <LogoText className="truncate">Sitex</LogoText> </Logo> </SidebarMenuButton> + {sections.length > 1 ? ( + <div className="flex gap-1 rounded-lg bg-sidebar-accent/50 p-1"> + {sections.map((section) => ( + <a + key={section.href} + href={section.href} + className={cn( + "flex-1 rounded-md px-2 py-1 text-center text-xs font-medium transition-colors", + section === activeSection + ? "bg-background text-foreground shadow-sm" + : "text-muted-foreground hover:text-foreground" + )} + > + {section.label} + </a> + ))} + </div> + ) : null} </SidebarHeader> <SidebarContent className="mask-[linear-gradient(to_bottom,transparent,black_1rem,black_calc(100%-1rem),transparent)] [-webkit-mask-image:linear-gradient(to_bottom,transparent,black_1rem,black_calc(100%-1rem),transparent)] group-data-[collapsible=icon]:mask-none group-data-[collapsible=icon]:[-webkit-mask-image:none]"> {navigation.map((group) => ( @@ -141,14 +152,6 @@ function Sidebar1({ orientation="vertical" className="my-auto mr-2 h-4" /> - <Breadcrumb className="hidden md:block"> - <BreadcrumbList> - <BreadcrumbTrail - breadcrumb={breadcrumb} - currentPath={currentPath} - /> - </BreadcrumbList> - </Breadcrumb> </HeaderGroup> <HeaderGroup className="gap-1"> <Button @@ -167,7 +170,7 @@ function Sidebar1({ <ExternalLinkIcon /> <span className="hidden sm:inline">GitHub</span> </Button> - <ThemeToggle /> + <ModeToggle /> </HeaderGroup> </HeaderContainer> </Header> @@ -185,132 +188,4 @@ function getNavigationItems(group: NavigationGroup) { return [] } -function BreadcrumbTrail({ - breadcrumb, - currentPath, -}: { - breadcrumb: Sidebar1Props["breadcrumb"] - currentPath: string -}) { - return ( - <> - {breadcrumb.items.map((item, index) => { - const isLast = index === breadcrumb.items.length - 1 - const isHome = item.href === "/" - const hasMenu = - !isHome && - breadcrumb.menu.some( - (menuItem) => - normalizePath(menuItem.href) === normalizePath(item.href) - ) - - return ( - <BreadcrumbFragment - currentPath={currentPath} - hasMenu={hasMenu} - isLast={isLast} - item={item} - key={`${item.href}-${index}`} - menu={breadcrumb.menu} - showSeparator={index > 0} - /> - ) - })} - {breadcrumb.menu.length > 0 && currentPath === "/" ? ( - <> - <BreadcrumbSeparator>/</BreadcrumbSeparator> - <BreadcrumbItem> - <DropdownMenu> - <DropdownMenuTrigger - aria-label="Open page menu" - className="inline-flex items-center leading-none transition-colors hover:text-foreground" - id="breadcrumb-page-menu" - > - <BreadcrumbEllipsis /> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuGroup> - {breadcrumb.menu.map((item) => ( - <a - className="flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none hover:bg-accent hover:text-accent-foreground" - href={item.href} - key={item.href} - > - {item.label} - </a> - ))} - </DropdownMenuGroup> - </DropdownMenuContent> - </DropdownMenu> - </BreadcrumbItem> - </> - ) : null} - </> - ) -} - -function BreadcrumbFragment({ - currentPath, - hasMenu, - isLast, - item, - menu, - showSeparator, -}: { - currentPath: string - hasMenu: boolean - isLast: boolean - item: { label: string; href: string } - menu: { label: string; href: string }[] - showSeparator: boolean -}) { - return ( - <> - {showSeparator ? <BreadcrumbSeparator>/</BreadcrumbSeparator> : null} - <BreadcrumbItem> - {hasMenu ? ( - <DropdownMenu> - <DropdownMenuTrigger - aria-current={isLast ? "page" : undefined} - aria-label={`${item.label} pages`} - className={cn( - "inline-flex items-center gap-1 transition-colors hover:text-foreground", - isLast && "text-foreground" - )} - id={`breadcrumb-menu-${toDomId(item.href)}`} - > - {item.label} - <ChevronDownIcon className="size-3.5 opacity-70" aria-hidden /> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuGroup> - {menu.map((menuItem) => ( - <a - aria-current={ - normalizePath(menuItem.href) === currentPath - ? "page" - : undefined - } - className="flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none hover:bg-accent hover:text-accent-foreground aria-[current=page]:text-foreground" - href={menuItem.href} - key={menuItem.href} - > - {menuItem.label} - </a> - ))} - </DropdownMenuGroup> - </DropdownMenuContent> - </DropdownMenu> - ) : isLast ? ( - <BreadcrumbPage>{item.label}</BreadcrumbPage> - ) : ( - <BreadcrumbLink render={<a href={item.href} />}> - {item.label} - </BreadcrumbLink> - )} - </BreadcrumbItem> - </> - ) -} - export { Sidebar1, type Sidebar1Props } diff --git a/apps/docs/src/components/layouts/base.tsx b/apps/docs/src/components/layouts/base.tsx deleted file mode 100644 index c76584c..0000000 --- a/apps/docs/src/components/layouts/base.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import type { ReactNode } from "react" - -import { Sidebar1 } from "@/components/blocks/sidebar-1" -import "@/styles/globals.css" - -export type BaseProps = { - title: string - description: string - path?: string - children?: ReactNode -} - -export type NavigationLink = { - href: string - label: string -} - -const docsNavigationGroups = [ - { - label: "Start", - links: [ - { href: "/docs", label: "Introduction" }, - { href: "/docs/installation", label: "Installation" }, - { href: "/docs/folder-structure", label: "Folder structure" }, - { href: "/docs/deployment", label: "Deployment" }, - ], - }, - { - label: "Pages", - links: [ - { href: "/docs/page-routing", label: "Routing" }, - { href: "/docs/tsx-pages", label: "TSX pages" }, - { href: "/docs/mdx", label: "MDX pages" }, - { href: "/docs/rendering-and-assets", label: "Page rendering" }, - { href: "/docs/content", label: "Pages API" }, - ], - }, - { - label: "Components", - links: [ - { href: "/docs/island-rendering", label: "Component rendering" }, - { href: "/docs/mdx-components", label: "MDX components" }, - ], - }, -] - -export const docsNavigation: NavigationLink[] = docsNavigationGroups.flatMap( - (group) => group.links -) - -const site = { - name: "Sitex Docs", - logo: { - label: "Sitex", - href: "/", - }, - header: { - githubRepo: "fulldotdev/sitex", - navigation: docsNavigation, - }, - sidebar: { - navigation: docsNavigationGroups, - }, -} - -function normalizePath(path: string) { - if (path === "/") return path - - return path.replace(/\/$/, "") -} - -function getPageBreadcrumbItems(path: string) { - const currentPath = normalizePath(path) - - if (currentPath === "/") { - return [{ label: "Home", href: "/" }] - } - - const currentPage = docsNavigation.find( - (item) => normalizePath(item.href) === currentPath - ) - - return [{ label: "Home", href: "/" }, ...(currentPage ? [currentPage] : [])] -} - -export default function Base({ - title, - description, - path = "/", - children, -}: BaseProps) { - const breadcrumbItems = getPageBreadcrumbItems(path) - - return ( - <html - className="dark overscroll-none scroll-smooth bg-background text-foreground has-data-[variant=inset]:bg-sidebar" - data-slot="layout" - lang="en" - > - <head> - <meta charSet="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> - <title>{title} - - - - - - - - -
- {children} -
-
- - - ) -} diff --git a/apps/docs/src/components/layouts/doc.tsx b/apps/docs/src/components/layouts/doc.tsx deleted file mode 100644 index a9e3fb2..0000000 --- a/apps/docs/src/components/layouts/doc.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { ComponentProps } from "react" - -import { Doc1 } from "@/components/blocks/doc-1" -import Base, { docsNavigation } from "@/components/layouts/base" - -type Props = ComponentProps & { - doc: Omit< - ComponentProps, - "children" | "previousPage" | "nextPage" - > -} - -function normalizePath(path: string) { - if (path === "/") return path - - return path.replace(/\/$/, "") -} - -export default function Doc({ doc, path = "/", children, ...props }: Props) { - const currentPath = normalizePath(path) - const currentIndex = docsNavigation.findIndex( - (item) => normalizePath(item.href) === currentPath - ) - const previousPage = - currentIndex > 0 ? docsNavigation[currentIndex - 1] : undefined - const nextPage = - currentIndex >= 0 ? docsNavigation[currentIndex + 1] : undefined - - return ( - - - {children} - - - ) -} diff --git a/apps/docs/src/components/layouts/home.tsx b/apps/docs/src/components/layouts/home.tsx deleted file mode 100644 index a1fcd38..0000000 --- a/apps/docs/src/components/layouts/home.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { ComponentProps } from "react" - -import { Features1 } from "@/components/blocks/features-1" -import { Hero1 } from "@/components/blocks/hero-1" -import Base from "@/components/layouts/base" - -type Props = ComponentProps & { - hero: ComponentProps - features: ComponentProps -} - -export default function Home({ hero, features, ...props }: Props) { - return ( - - - - - ) -} diff --git a/apps/docs/src/components/mdx-components/pre.tsx b/apps/docs/src/components/mdx-components/pre.tsx deleted file mode 100644 index 6b2d5be..0000000 --- a/apps/docs/src/components/mdx-components/pre.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import type { ComponentProps, ReactElement, ReactNode } from "react" - -import { CodeBlock } from "@/components/ui/code-block" - -type CodeBlockLanguage = ComponentProps["lang"] - -const codeBlockLanguages = new Set([ - "bash", - "html", - "json", - "markdown", - "mdx", - "text", - "ts", - "tsx", -]) - -function isReactElement(value: ReactNode): value is ReactElement<{ - className?: string - children?: ReactNode -}> { - return typeof value === "object" && value !== null && "props" in value -} - -function readCodeBlockLanguage(className: string | undefined) { - const language = className?.match(/\blanguage-([A-Za-z0-9_-]+)/)?.[1] - - return language && codeBlockLanguages.has(language as CodeBlockLanguage) - ? (language as CodeBlockLanguage) - : "text" -} - -function readCodeBlockValue(value: ReactNode) { - return typeof value === "string" || typeof value === "number" - ? String(value) - : "" -} - -export default function MdxPre({ children }: { children?: ReactNode }) { - if (!isReactElement(children)) { - return
{children}
- } - - return ( - - ) -} diff --git a/apps/docs/src/components/mode-toggle.tsx b/apps/docs/src/components/mode-toggle.tsx new file mode 100644 index 0000000..77d2fe6 --- /dev/null +++ b/apps/docs/src/components/mode-toggle.tsx @@ -0,0 +1,39 @@ +import { Moon, Sun } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { useTheme } from "@/components/theme-provider" + +export function ModeToggle() { + const { setTheme } = useTheme() + + return ( + + + + + Toggle theme + + } + /> + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ) +} diff --git a/apps/docs/src/components/theme-provider.tsx b/apps/docs/src/components/theme-provider.tsx index d533fb1..f30d3bc 100644 --- a/apps/docs/src/components/theme-provider.tsx +++ b/apps/docs/src/components/theme-provider.tsx @@ -1,10 +1,9 @@ -import type React from "react" import { createContext, useContext, useEffect, useState } from "react" type Theme = "dark" | "light" | "system" type ThemeProviderProps = { - children?: React.ReactNode + children: React.ReactNode defaultTheme?: Theme storageKey?: string } @@ -21,22 +20,6 @@ const initialState: ThemeProviderState = { const ThemeProviderContext = createContext(initialState) -function disableTransitionsTemporarily() { - const style = document.createElement("style") - style.appendChild( - document.createTextNode("*,*::before,*::after{transition:none!important}") - ) - document.head.appendChild(style) - - window.getComputedStyle(document.body) - - requestAnimationFrame(() => { - requestAnimationFrame(() => { - style.remove() - }) - }) -} - export function ThemeProvider({ children, defaultTheme = "system", @@ -44,6 +27,8 @@ export function ThemeProvider({ ...props }: ThemeProviderProps) { const [theme, setTheme] = useState(() => + // Pages prerender without a window; the head script applies the class + // before first paint. typeof window === "undefined" ? defaultTheme : (localStorage.getItem(storageKey) as Theme) || defaultTheme @@ -52,7 +37,6 @@ export function ThemeProvider({ useEffect(() => { const root = window.document.documentElement - disableTransitionsTemporarily() root.classList.remove("light", "dark") if (theme === "system") { @@ -71,10 +55,7 @@ export function ThemeProvider({ const value = { theme, setTheme: (theme: Theme) => { - if (typeof window !== "undefined") { - localStorage.setItem(storageKey, theme) - } - + localStorage.setItem(storageKey, theme) setTheme(theme) }, } diff --git a/apps/docs/src/components/ui/alert.tsx b/apps/docs/src/components/ui/alert.tsx deleted file mode 100644 index 2d8ecac..0000000 --- a/apps/docs/src/components/ui/alert.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const alertVariants = cva( - "group/alert relative grid w-full gap-0.5 rounded-lg border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4", - { - variants: { - variant: { - default: "bg-card text-card-foreground", - destructive: - "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -function Alert({ - className, - variant, - ...props -}: React.ComponentProps<"div"> & VariantProps) { - return ( -
- ) -} - -function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground", - className - )} - {...props} - /> - ) -} - -function AlertDescription({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} - -function AlertAction({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -export { Alert, AlertTitle, AlertDescription, AlertAction } diff --git a/apps/docs/src/components/ui/breadcrumb.tsx b/apps/docs/src/components/ui/breadcrumb.tsx deleted file mode 100644 index 4ef7e26..0000000 --- a/apps/docs/src/components/ui/breadcrumb.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import * as React from "react" -import { mergeProps } from "@base-ui/react/merge-props" -import { useRender } from "@base-ui/react/use-render" - -import { cn } from "@/lib/utils" -import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react" - -function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) { - return ( -