From 529637b8a9bd3a2dc7df714840f1efd3efee66a0 Mon Sep 17 00:00:00 2001 From: Tim Delisle Date: Fri, 14 Nov 2025 21:24:25 -0800 Subject: [PATCH 01/21] fixed templates page --- apps/framework-docs-v2/.npmrc | 20 +- .../content/templates/index.mdx | 60 +--- apps/framework-docs-v2/package.json | 2 + .../src/app/[...slug]/page.tsx | 12 +- .../strategy/platform-engineering/page.tsx | 66 ++++ .../src/app/templates/layout.tsx | 31 ++ .../src/app/templates/page.tsx | 62 ++++ .../src/app/templates/templates-side-nav.tsx | 296 ++++++++++++++++++ .../src/components/mdx-renderer.tsx | 2 + .../src/components/mdx/command-snippet.tsx | 42 +++ .../src/components/mdx/index.ts | 1 + .../src/components/mdx/template-card.tsx | 155 +++++---- .../src/components/mdx/template-grid.tsx | 159 ++-------- .../src/components/ui/checkbox.tsx | 30 ++ .../src/components/ui/command.tsx | 2 +- .../src/components/ui/input.tsx | 2 +- .../src/components/ui/item.tsx | 164 ++++++++++ .../src/components/ui/select.tsx | 2 +- apps/framework-docs-v2/src/lib/content.ts | 12 +- apps/framework-docs-v2/src/styles/globals.css | 4 +- pnpm-lock.yaml | 93 +++++- 21 files changed, 937 insertions(+), 280 deletions(-) create mode 100644 apps/framework-docs-v2/src/app/guides/strategy/platform-engineering/page.tsx create mode 100644 apps/framework-docs-v2/src/app/templates/layout.tsx create mode 100644 apps/framework-docs-v2/src/app/templates/page.tsx create mode 100644 apps/framework-docs-v2/src/app/templates/templates-side-nav.tsx create mode 100644 apps/framework-docs-v2/src/components/mdx/command-snippet.tsx create mode 100644 apps/framework-docs-v2/src/components/ui/checkbox.tsx create mode 100644 apps/framework-docs-v2/src/components/ui/item.tsx diff --git a/apps/framework-docs-v2/.npmrc b/apps/framework-docs-v2/.npmrc index afab184d37..be313dafb4 100644 --- a/apps/framework-docs-v2/.npmrc +++ b/apps/framework-docs-v2/.npmrc @@ -1,6 +1,20 @@ # Force all dependencies to be hoisted locally to this app's node_modules # This prevents TypeScript from finding React types in nested node_modules -# This overrides the root .npmrc which prevents hoisting to support multiple React versions -# Since this app only uses React 19, we can safely hoist everything here -shamefully-hoist=true +# This works with the root .npmrc which prevents React from being hoisted to root +# Since this app only uses React 19, we can safely hoist everything locally here +# +# IMPORTANT: We use hoist-pattern instead of shamefully-hoist=true to avoid +# conflicts with the root hoisting pattern when running pnpm add from this directory +# (e.g., via shadcn CLI). This hoists everything locally without modifying root structure. +hoist-pattern[]=* + +# Match root public-hoist-pattern to prevent ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF +# This ensures compatibility when running pnpm add from this directory +public-hoist-pattern[]=!react +public-hoist-pattern[]=!react-dom +public-hoist-pattern[]=!react/jsx-runtime +public-hoist-pattern[]=!react-dom/server +public-hoist-pattern[]=!react/jsx-dev-runtime +public-hoist-pattern[]=!@types/react +public-hoist-pattern[]=!@types/react-dom diff --git a/apps/framework-docs-v2/content/templates/index.mdx b/apps/framework-docs-v2/content/templates/index.mdx index 29cdba52cf..149114ccc2 100644 --- a/apps/framework-docs-v2/content/templates/index.mdx +++ b/apps/framework-docs-v2/content/templates/index.mdx @@ -5,69 +5,13 @@ order: 2 category: getting-started --- -import { CTACards, CTACard } from "@/components/mdx"; -import { Badge } from "@/components/ui/badge"; -import Link from "next/link"; -import { TemplatesGridServer } from "@/components/mdx"; +import { TemplatesGridServer, CommandSnippet } from "@/components/mdx"; # Templates & Apps Moose provides two ways to get started: **templates** and **demo apps**. Templates are simple skeleton applications that you can initialize with `moose init`, while demo apps are more advanced examples available on GitHub that showcase real-world use cases and integrations. -**Initialize a template:** -```bash filename="Terminal" copy -moose init PROJECT_NAME TEMPLATE_NAME -``` - -**List available templates:** -```bash filename="Terminal" copy -moose template list -``` - -## Popular Apps - - - - - - - - - - ---- + ## Browse Apps and Templates diff --git a/apps/framework-docs-v2/package.json b/apps/framework-docs-v2/package.json index ebb96fdf62..9780a46f10 100644 --- a/apps/framework-docs-v2/package.json +++ b/apps/framework-docs-v2/package.json @@ -21,11 +21,13 @@ "@next/mdx": "^16.0.1", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.1.7", diff --git a/apps/framework-docs-v2/src/app/[...slug]/page.tsx b/apps/framework-docs-v2/src/app/[...slug]/page.tsx index 35a58dc1d6..cfb3523efc 100644 --- a/apps/framework-docs-v2/src/app/[...slug]/page.tsx +++ b/apps/framework-docs-v2/src/app/[...slug]/page.tsx @@ -19,17 +19,18 @@ export async function generateStaticParams() { const slugs = getAllSlugs(); // Generate params for each slug + // Note: templates is excluded from getAllSlugs() as it is now an explicit page const allParams: { slug: string[] }[] = slugs.map((slug) => ({ slug: slug.split("/"), })); - // Also add section index routes (moosestack, ai, hosting, templates) - // These map to section/index.mdx files + // Also add section index routes (moosestack, ai, hosting, guides) + // Note: templates is now an explicit page, so it's excluded here allParams.push( { slug: ["moosestack"] }, { slug: ["ai"] }, { slug: ["hosting"] }, - { slug: ["templates"] }, + { slug: ["guides"] }, ); return allParams; @@ -81,6 +82,11 @@ export default async function DocPage({ params }: PageProps) { const slug = slugArray.join("/"); + // Templates is now an explicit page, so it should not be handled by this catch-all route + if (slug.startsWith("templates/")) { + notFound(); + } + let content; try { content = await parseMarkdownContent(slug); diff --git a/apps/framework-docs-v2/src/app/guides/strategy/platform-engineering/page.tsx b/apps/framework-docs-v2/src/app/guides/strategy/platform-engineering/page.tsx new file mode 100644 index 0000000000..9439ecb4e1 --- /dev/null +++ b/apps/framework-docs-v2/src/app/guides/strategy/platform-engineering/page.tsx @@ -0,0 +1,66 @@ +import { notFound } from "next/navigation"; +import type { Metadata } from "next"; +import { parseMarkdownContent } from "@/lib/content"; +import { TOCNav } from "@/components/navigation/toc-nav"; +import { MDXRenderer } from "@/components/mdx-renderer"; +import { DocBreadcrumbs } from "@/components/navigation/doc-breadcrumbs"; +import { buildDocBreadcrumbs } from "@/lib/breadcrumbs"; + +export const dynamic = "force-dynamic"; + +export async function generateMetadata(): Promise { + try { + const content = await parseMarkdownContent( + "guides/strategy/platform-engineering", + ); + return { + title: + content.frontMatter.title ? + `${content.frontMatter.title} | MooseStack Documentation` + : "Platform Engineering | MooseStack Documentation", + description: + content.frontMatter.description || + "Guide to platform engineering strategy with MooseStack", + }; + } catch (error) { + return { + title: "Platform Engineering | MooseStack Documentation", + description: "Guide to platform engineering strategy with MooseStack", + }; + } +} + +export default async function PlatformEngineeringPage() { + let content; + try { + content = await parseMarkdownContent( + "guides/strategy/platform-engineering", + ); + } catch (error) { + notFound(); + } + + const breadcrumbs = buildDocBreadcrumbs( + "guides/strategy/platform-engineering", + typeof content.frontMatter.title === "string" ? + content.frontMatter.title + : undefined, + ); + + return ( + <> +
+ +
+ {content.isMDX ? + + :
} +
+
+ + + ); +} diff --git a/apps/framework-docs-v2/src/app/templates/layout.tsx b/apps/framework-docs-v2/src/app/templates/layout.tsx new file mode 100644 index 0000000000..25d5d3afb1 --- /dev/null +++ b/apps/framework-docs-v2/src/app/templates/layout.tsx @@ -0,0 +1,31 @@ +import type { ReactNode } from "react"; +import { Suspense } from "react"; +import { TemplatesSideNav } from "./templates-side-nav"; +import { AnalyticsProvider } from "@/components/analytics-provider"; +import { SidebarInset } from "@/components/ui/sidebar"; + +interface TemplatesLayoutProps { + children: ReactNode; +} + +export default async function TemplatesLayout({ + children, +}: TemplatesLayoutProps) { + return ( + +
+ }> + + + +
+ {/* Reserve space for the right TOC on xl+ screens */} +
+ {children} +
+
+
+
+
+ ); +} diff --git a/apps/framework-docs-v2/src/app/templates/page.tsx b/apps/framework-docs-v2/src/app/templates/page.tsx new file mode 100644 index 0000000000..ca7f80b21c --- /dev/null +++ b/apps/framework-docs-v2/src/app/templates/page.tsx @@ -0,0 +1,62 @@ +import { notFound } from "next/navigation"; +import type { Metadata } from "next"; +import { parseMarkdownContent } from "@/lib/content"; +import { TOCNav } from "@/components/navigation/toc-nav"; +import { MDXRenderer } from "@/components/mdx-renderer"; +import { DocBreadcrumbs } from "@/components/navigation/doc-breadcrumbs"; +import { buildDocBreadcrumbs } from "@/lib/breadcrumbs"; + +export const dynamic = "force-dynamic"; + +export async function generateMetadata(): Promise { + try { + const content = await parseMarkdownContent("templates/index"); + return { + title: + content.frontMatter.title ? + `${content.frontMatter.title} | MooseStack Documentation` + : "Templates & Apps | MooseStack Documentation", + description: + content.frontMatter.description || + "Browse templates and demo apps for MooseStack", + }; + } catch (error) { + return { + title: "Templates & Apps | MooseStack Documentation", + description: "Browse templates and demo apps for MooseStack", + }; + } +} + +export default async function TemplatesPage() { + let content; + try { + content = await parseMarkdownContent("templates/index"); + } catch (error) { + notFound(); + } + + const breadcrumbs = buildDocBreadcrumbs( + "templates/index", + typeof content.frontMatter.title === "string" ? + content.frontMatter.title + : undefined, + ); + + return ( + <> +
+ +
+ {content.isMDX ? + + :
} +
+
+ + + ); +} diff --git a/apps/framework-docs-v2/src/app/templates/templates-side-nav.tsx b/apps/framework-docs-v2/src/app/templates/templates-side-nav.tsx new file mode 100644 index 0000000000..dc0d894822 --- /dev/null +++ b/apps/framework-docs-v2/src/app/templates/templates-side-nav.tsx @@ -0,0 +1,296 @@ +"use client"; + +import * as React from "react"; +import { useSearchParams, useRouter, usePathname } from "next/navigation"; +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { IconX } from "@tabler/icons-react"; + +type LanguageFilter = "typescript" | "python" | null; +type CategoryFilter = ("starter" | "framework" | "example")[]; +type TypeFilter = "template" | "app" | null; + +export function TemplatesSideNav() { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + // Get filter values from URL params + const typeFilter = (searchParams.get("type") as TypeFilter) || null; + const languageFilter = + (searchParams.get("language") as LanguageFilter) || null; + const categoryFilter = React.useMemo(() => { + const categoryParam = searchParams.get("category"); + if (!categoryParam) return []; + return categoryParam + .split(",") + .filter( + (c): c is "starter" | "framework" | "example" => + c === "starter" || c === "framework" || c === "example", + ); + }, [searchParams]); + + const hasActiveFilters = + typeFilter !== null || languageFilter !== null || categoryFilter.length > 0; + + // Update URL params when filters change + const updateFilters = React.useCallback( + (updates: { + type?: TypeFilter; + language?: LanguageFilter; + category?: CategoryFilter; + }) => { + const params = new URLSearchParams(searchParams.toString()); + + if (updates.type !== undefined) { + if (updates.type === null) { + params.delete("type"); + } else { + params.set("type", updates.type); + } + } + + if (updates.language !== undefined) { + if (updates.language === null) { + params.delete("language"); + } else { + params.set("language", updates.language); + } + } + + if (updates.category !== undefined) { + if (updates.category.length === 0) { + params.delete("category"); + } else { + params.set("category", updates.category.join(",")); + } + } + + router.push(`${pathname}?${params.toString()}`); + }, + [router, pathname, searchParams], + ); + + const clearFilters = () => { + updateFilters({ type: null, language: null, category: [] }); + }; + + return ( + + + + Filters + + {/* Type Filter */} + +
+ +
+
+ { + if (checked) { + updateFilters({ type: "template" }); + } else { + updateFilters({ type: null }); + } + }} + /> + +
+
+ { + if (checked) { + updateFilters({ type: "app" }); + } else { + updateFilters({ type: null }); + } + }} + /> + +
+
+
+
+ + {/* Language Filter */} + +
+ +
+
+ { + if (checked) { + updateFilters({ language: "typescript" }); + } else { + updateFilters({ language: null }); + } + }} + /> + +
+
+ { + if (checked) { + updateFilters({ language: "python" }); + } else { + updateFilters({ language: null }); + } + }} + /> + +
+
+
+
+ + {/* Category Filter */} + +
+ +
+
+ { + if (checked) { + updateFilters({ + category: [...categoryFilter, "starter"], + }); + } else { + updateFilters({ + category: categoryFilter.filter( + (c) => c !== "starter", + ), + }); + } + }} + /> + +
+
+ { + if (checked) { + updateFilters({ + category: [...categoryFilter, "framework"], + }); + } else { + updateFilters({ + category: categoryFilter.filter( + (c) => c !== "framework", + ), + }); + } + }} + /> + +
+
+ { + if (checked) { + updateFilters({ + category: [...categoryFilter, "example"], + }); + } else { + updateFilters({ + category: categoryFilter.filter( + (c) => c !== "example", + ), + }); + } + }} + /> + +
+
+
+
+ + {/* Clear Filters Button */} + {hasActiveFilters && ( + + + + Clear Filters + + + )} +
+
+
+
+ ); +} diff --git a/apps/framework-docs-v2/src/components/mdx-renderer.tsx b/apps/framework-docs-v2/src/components/mdx-renderer.tsx index cb30fc5550..3f4ff5ccf7 100644 --- a/apps/framework-docs-v2/src/components/mdx-renderer.tsx +++ b/apps/framework-docs-v2/src/components/mdx-renderer.tsx @@ -27,6 +27,7 @@ import { Security, BreakingChanges, TemplatesGridServer, + CommandSnippet, } from "@/components/mdx"; import { FileTreeFolder, FileTreeFile } from "@/components/mdx/file-tree"; import { CodeEditor } from "@/components/ui/shadcn-io/code-editor"; @@ -120,6 +121,7 @@ export async function MDXRenderer({ source }: MDXRendererProps) { Security, BreakingChanges, TemplatesGridServer, + CommandSnippet, CodeEditor, Separator, Tabs, diff --git a/apps/framework-docs-v2/src/components/mdx/command-snippet.tsx b/apps/framework-docs-v2/src/components/mdx/command-snippet.tsx new file mode 100644 index 0000000000..b43cbaf35b --- /dev/null +++ b/apps/framework-docs-v2/src/components/mdx/command-snippet.tsx @@ -0,0 +1,42 @@ +"use client"; + +import * as React from "react"; +import { + Snippet, + SnippetHeader, + SnippetTabsList, + SnippetTabsTrigger, + SnippetTabsContent, + SnippetCopyButton, +} from "@/components/ui/snippet"; + +interface CommandSnippetProps { + initCommand?: string; + listCommand?: string; + initLabel?: string; + listLabel?: string; +} + +export function CommandSnippet({ + initCommand = "moose init PROJECT_NAME TEMPLATE_NAME", + listCommand = "moose template list", + initLabel = "Init", + listLabel = "List", +}: CommandSnippetProps) { + const [value, setValue] = React.useState("init"); + const currentCommand = value === "init" ? initCommand : listCommand; + + return ( + + + + {initLabel} + {listLabel} + + + + {initCommand} + {listCommand} + + ); +} diff --git a/apps/framework-docs-v2/src/components/mdx/index.ts b/apps/framework-docs-v2/src/components/mdx/index.ts index ebdb8480cf..e34439c4a0 100644 --- a/apps/framework-docs-v2/src/components/mdx/index.ts +++ b/apps/framework-docs-v2/src/components/mdx/index.ts @@ -8,6 +8,7 @@ export { } from "./staggered-card"; export { Callout } from "./callout"; export { LanguageTabs, LanguageTabContent } from "./language-tabs"; +export { CommandSnippet } from "./command-snippet"; export { CodeSnippet } from "./code-snippet"; export { CodeEditorWrapper } from "./code-editor-wrapper"; export { ToggleBlock } from "./toggle-block"; diff --git a/apps/framework-docs-v2/src/components/mdx/template-card.tsx b/apps/framework-docs-v2/src/components/mdx/template-card.tsx index 0e3ccba497..9e78e47774 100644 --- a/apps/framework-docs-v2/src/components/mdx/template-card.tsx +++ b/apps/framework-docs-v2/src/components/mdx/template-card.tsx @@ -11,7 +11,9 @@ import { CardFooter, CardHeader, } from "@/components/ui/card"; -import { IconBrandGithub } from "@tabler/icons-react"; +import { IconBrandGithub, IconRocket } from "@tabler/icons-react"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; import { Snippet, SnippetCopyButton, @@ -52,12 +54,6 @@ export function TemplateCard({ item, className }: TemplateCardProps) { const template = isTemplate ? (item as TemplateMetadata) : null; const app = !isTemplate ? (item as AppMetadata) : null; - const categoryColors = { - starter: "border-blue-200 dark:border-blue-800", - framework: "border-purple-200 dark:border-purple-800", - example: "border-green-200 dark:border-green-800", - }; - const categoryLabels = { starter: "Starter", framework: "Framework", @@ -82,9 +78,6 @@ export function TemplateCard({ item, className }: TemplateCardProps) { @@ -93,11 +86,8 @@ export function TemplateCard({ item, className }: TemplateCardProps) {
{language && ( - - {language === "typescript" ? "TS" : "Python"} + + {language === "typescript" ? "TypeScript" : "Python"} )} {isTemplate && template && ( @@ -111,70 +101,101 @@ export function TemplateCard({ item, className }: TemplateCardProps) { )}
-

+

{isTemplate ? formatTemplateName(name) : name}

- - {description} - - {frameworks.length > 0 && ( -
-

- Frameworks: -

-
- {frameworks.map((framework) => ( - - {framework} - - ))} -
-
- )} - - {features.length > 0 && ( -
-

- Features: -

-
- {features.map((feature) => ( - - {feature} - - ))} -
+ + {description} + {isTemplate && template && ( +
+
)}
{isTemplate && template && ( -
- -
+ <> + {(frameworks.length > 0 || features.length > 0) && ( + <> + +
+ {frameworks.map((framework) => ( + + {framework} + + ))} + {features.map((feature) => ( + + {feature} + + ))} +
+ + )} + )} - {!isTemplate && app && app.blogPost && ( - - Read Blog Post → - + {!isTemplate && app && ( + <> + {app.blogPost && ( + + Read Blog Post → + + )} + {app.blogPost && (frameworks.length > 0 || features.length > 0) && ( + + )} + {(frameworks.length > 0 || features.length > 0) && ( +
+ {frameworks.map((framework) => ( + + {framework} + + ))} + {features.map((feature) => ( + + {feature} + + ))} +
+ )} + )} - - - View on GitHub - +
+ + +
); diff --git a/apps/framework-docs-v2/src/components/mdx/template-grid.tsx b/apps/framework-docs-v2/src/components/mdx/template-grid.tsx index 753ec43fa1..23eb147392 100644 --- a/apps/framework-docs-v2/src/components/mdx/template-grid.tsx +++ b/apps/framework-docs-v2/src/components/mdx/template-grid.tsx @@ -1,11 +1,11 @@ "use client"; import * as React from "react"; +import { useSearchParams } from "next/navigation"; import { cn } from "@/lib/utils"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { TemplateCard } from "./template-card"; import type { ItemMetadata, TemplateMetadata } from "@/lib/template-types"; import { IconSearch, IconX } from "@tabler/icons-react"; @@ -20,13 +20,33 @@ type CategoryFilter = ("starter" | "framework" | "example")[]; type TypeFilter = "template" | "app" | null; export function TemplateGrid({ items, className }: TemplateGridProps) { + const searchParams = useSearchParams(); const [searchQuery, setSearchQuery] = React.useState(""); - const [languageFilter, setLanguageFilter] = - React.useState(null); - const [categoryFilter, setCategoryFilter] = React.useState( - [], - ); - const [typeFilter, setTypeFilter] = React.useState(null); + + // Read filters from URL params (set by TemplatesSideNav) + const typeFilter = React.useMemo(() => { + const type = searchParams.get("type"); + return (type === "template" || type === "app" ? type : null) as TypeFilter; + }, [searchParams]); + + const languageFilter = React.useMemo(() => { + const language = searchParams.get("language"); + return ( + language === "typescript" || language === "python" ? + language + : null) as LanguageFilter; + }, [searchParams]); + + const categoryFilter = React.useMemo(() => { + const categoryParam = searchParams.get("category"); + if (!categoryParam) return []; + return categoryParam + .split(",") + .filter( + (c): c is "starter" | "framework" | "example" => + c === "starter" || c === "framework" || c === "example", + ) as CategoryFilter; + }, [searchParams]); const filteredItems = React.useMemo(() => { return items.filter((item) => { @@ -88,18 +108,10 @@ export function TemplateGrid({ items, className }: TemplateGridProps) { categoryFilter.length > 0 || typeFilter !== null; - const clearFilters = () => { - setSearchQuery(""); - setLanguageFilter(null); - setCategoryFilter([]); - setTypeFilter(null); - }; - return (
- {/* Filters */} -
- {/* Search */} + {/* Search - kept in main content area */} +
)}
- - {/* Type Filter */} -
- - { - if (value === "" || value === undefined) { - setTypeFilter(null); - } else if (value === "template" || value === "app") { - setTypeFilter(value as TypeFilter); - } - }} - variant="outline" - className="w-full" - > - - Templates - - - Apps - - -
- - {/* Language and Category Filters */} -
-
- - { - if (value === "" || value === undefined) { - setLanguageFilter(null); - } else if (value === "typescript" || value === "python") { - setLanguageFilter(value as LanguageFilter); - } - }} - variant="outline" - className="w-full" - > - - TypeScript - - - Python - - -
- -
- - { - setCategoryFilter(value as CategoryFilter); - }} - variant="outline" - className="w-full" - > - - Starter - - - Framework - - - Example - - -
-
- - {/* Clear filters button */} + {/* Results count */} {hasActiveFilters && ( -
- +
{filteredItems.length} item{filteredItems.length !== 1 ? "s" : ""} diff --git a/apps/framework-docs-v2/src/components/ui/checkbox.tsx b/apps/framework-docs-v2/src/components/ui/checkbox.tsx new file mode 100644 index 0000000000..c450e30dd5 --- /dev/null +++ b/apps/framework-docs-v2/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client"; + +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { IconCheck } from "@tabler/icons-react"; + +import { cn } from "@/lib/utils"; + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/apps/framework-docs-v2/src/components/ui/command.tsx b/apps/framework-docs-v2/src/components/ui/command.tsx index 525ebddd2f..6be2a79deb 100644 --- a/apps/framework-docs-v2/src/components/ui/command.tsx +++ b/apps/framework-docs-v2/src/components/ui/command.tsx @@ -66,7 +66,7 @@ const CommandInput = React.forwardRef< >( , + VariantProps { + asChild?: boolean; +} + +const Item = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "div"; + return ( + + ); + }, +); +Item.displayName = "Item"; + +const ItemGroup = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +ItemGroup.displayName = "ItemGroup"; + +const ItemSeparator = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +ItemSeparator.displayName = "ItemSeparator"; + +const ItemMedia = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { + variant?: "default" | "icon" | "image"; + } +>(({ className, variant = "default", ...props }, ref) => ( +
+)); +ItemMedia.displayName = "ItemMedia"; + +const ItemContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +ItemContent.displayName = "ItemContent"; + +const ItemTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +ItemTitle.displayName = "ItemTitle"; + +const ItemDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +ItemDescription.displayName = "ItemDescription"; + +const ItemActions = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +ItemActions.displayName = "ItemActions"; + +const ItemHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +ItemHeader.displayName = "ItemHeader"; + +const ItemFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +ItemFooter.displayName = "ItemFooter"; + +export { + Item, + ItemGroup, + ItemSeparator, + ItemMedia, + ItemContent, + ItemTitle, + ItemDescription, + ItemActions, + ItemHeader, + ItemFooter, +}; diff --git a/apps/framework-docs-v2/src/components/ui/select.tsx b/apps/framework-docs-v2/src/components/ui/select.tsx index 9b01fc1fb9..0a7d581cd5 100644 --- a/apps/framework-docs-v2/src/components/ui/select.tsx +++ b/apps/framework-docs-v2/src/components/ui/select.tsx @@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef< span]:line-clamp-1", + "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-card px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className, )} {...props} diff --git a/apps/framework-docs-v2/src/lib/content.ts b/apps/framework-docs-v2/src/lib/content.ts index 15af3315d9..861e1b7538 100644 --- a/apps/framework-docs-v2/src/lib/content.ts +++ b/apps/framework-docs-v2/src/lib/content.ts @@ -31,7 +31,8 @@ export function getContentFiles(): string[] { /** * Recursively get all markdown files in a directory - * Excludes the 'shared' folder + * Excludes the 'shared' folder and 'templates' folder + * (templates is now an explicit page in the app directory) */ function getAllMarkdownFiles(dir: string, baseDir: string): string[] { const files: string[] = []; @@ -39,11 +40,12 @@ function getAllMarkdownFiles(dir: string, baseDir: string): string[] { for (const entry of entries) { const fullPath = path.join(dir, entry.name); - // Skip the shared folder - if (entry.isDirectory() && entry.name === "shared") { - continue; - } + // Skip the shared folder and templates folder + // (templates is now an explicit page in app directory) if (entry.isDirectory()) { + if (entry.name === "shared" || entry.name === "templates") { + continue; + } files.push(...getAllMarkdownFiles(fullPath, baseDir)); } else if ( entry.isFile() && diff --git a/apps/framework-docs-v2/src/styles/globals.css b/apps/framework-docs-v2/src/styles/globals.css index bcb1670268..2b86243865 100644 --- a/apps/framework-docs-v2/src/styles/globals.css +++ b/apps/framework-docs-v2/src/styles/globals.css @@ -40,9 +40,9 @@ } .dark { - --background: 0 0% 3.9%; + --background: 0 0% 0%; --foreground: 0 0% 98%; - --card: 0 0% 3.9%; + --card: 240 8.9% 3.9%; --card-foreground: 0 0% 98%; --popover: 0 0% 3.9%; --popover-foreground: 0 0% 98%; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20d90858fb..96a5522d1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,7 +62,7 @@ importers: devDependencies: '@clickhouse/client': specifier: latest - version: 1.12.1 + version: 1.13.0 '@iarna/toml': specifier: ^3.0.0 version: 3.0.0 @@ -256,6 +256,9 @@ importers: '@radix-ui/react-avatar': specifier: ^1.0.4 version: 1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-collapsible': specifier: ^1.1.11 version: 1.1.12(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -271,6 +274,9 @@ importers: '@radix-ui/react-navigation-menu': specifier: ^1.2.13 version: 1.2.14(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-scroll-area': specifier: ^1.2.2 version: 1.2.10(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -1280,8 +1286,8 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} - '@clickhouse/client-common@1.12.1': - resolution: {integrity: sha512-ccw1N6hB4+MyaAHIaWBwGZ6O2GgMlO99FlMj0B0UEGfjxM9v5dYVYql6FpP19rMwrVAroYs/IgX2vyZEBvzQLg==} + '@clickhouse/client-common@1.13.0': + resolution: {integrity: sha512-QlGUMd3EaKkIRLCv0WW8Rw9cOlqhwQPT+ucNWY8eC4UALsMhJLpa0H7Cd7MYc9CEtTv/xlr3IcYw5Tdho4Hr2g==} '@clickhouse/client-common@1.5.0': resolution: {integrity: sha512-U3vDp+PDnNVEv6kia+Mq5ygnlMZzsYU+3TX+0da3XvL926jzYLMBlIvFUxe2+/5k47ySvnINRC/2QxVK7PC2/A==} @@ -1292,8 +1298,8 @@ packages: '@clickhouse/client-web@1.5.0': resolution: {integrity: sha512-21+c2UJ4cx9SPiIWQThCLULb8h/zng0pNrtTwbbnaoCqMbasyRCyRTHs3wRr7fqRUcZ3p9krIPuN0gnJw3GJ6Q==} - '@clickhouse/client@1.12.1': - resolution: {integrity: sha512-7ORY85rphRazqHzImNXMrh4vsaPrpetFoTWpZYueCO2bbO6PXYDXp/GQ4DgxnGIqbWB/Di1Ai+Xuwq2o7DJ36A==} + '@clickhouse/client@1.13.0': + resolution: {integrity: sha512-uK+zqPaJnAoq3QIOvUNbHtbWUhyg2A/aSbdJtrY2+kawp4SMBLcfIbB9ucRv5Yht1CAa3b24CiUlypkmgarukg==} engines: {node: '>=16'} '@clickhouse/client@1.8.1': @@ -2493,6 +2499,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collapsible@1.1.12': resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} peerDependencies: @@ -2682,6 +2701,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popper@1.2.8': resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} peerDependencies: @@ -10104,7 +10136,7 @@ snapshots: '@chevrotain/utils@11.0.3': {} - '@clickhouse/client-common@1.12.1': {} + '@clickhouse/client-common@1.13.0': {} '@clickhouse/client-common@1.5.0': {} @@ -10114,9 +10146,9 @@ snapshots: dependencies: '@clickhouse/client-common': 1.5.0 - '@clickhouse/client@1.12.1': + '@clickhouse/client@1.13.0': dependencies: - '@clickhouse/client-common': 1.12.1 + '@clickhouse/client-common': 1.13.0 '@clickhouse/client@1.8.1': dependencies: @@ -11309,6 +11341,22 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -11704,6 +11752,29 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + aria-hidden: 1.2.6 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-remove-scroll: 2.7.1(@types/react@19.2.2)(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -15267,7 +15338,7 @@ snapshots: eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.4(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) @@ -15327,7 +15398,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -15368,7 +15439,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 From 7a3aac7942fa2bddcfb3d9aa56e97bee3787d03a Mon Sep 17 00:00:00 2001 From: Tim Delisle Date: Fri, 14 Nov 2025 21:47:46 -0800 Subject: [PATCH 02/21] attempt build fix --- apps/framework-docs-v2/.npmrc | 20 -------------------- apps/framework-docs-v2/tsconfig.json | 7 +++++-- apps/framework-docs/tsconfig.json | 7 +++++-- packages/ts-config/nextjs.json | 13 ++++--------- 4 files changed, 14 insertions(+), 33 deletions(-) delete mode 100644 apps/framework-docs-v2/.npmrc diff --git a/apps/framework-docs-v2/.npmrc b/apps/framework-docs-v2/.npmrc deleted file mode 100644 index be313dafb4..0000000000 --- a/apps/framework-docs-v2/.npmrc +++ /dev/null @@ -1,20 +0,0 @@ -# Force all dependencies to be hoisted locally to this app's node_modules -# This prevents TypeScript from finding React types in nested node_modules -# This works with the root .npmrc which prevents React from being hoisted to root -# Since this app only uses React 19, we can safely hoist everything locally here -# -# IMPORTANT: We use hoist-pattern instead of shamefully-hoist=true to avoid -# conflicts with the root hoisting pattern when running pnpm add from this directory -# (e.g., via shadcn CLI). This hoists everything locally without modifying root structure. -hoist-pattern[]=* - -# Match root public-hoist-pattern to prevent ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF -# This ensures compatibility when running pnpm add from this directory -public-hoist-pattern[]=!react -public-hoist-pattern[]=!react-dom -public-hoist-pattern[]=!react/jsx-runtime -public-hoist-pattern[]=!react-dom/server -public-hoist-pattern[]=!react/jsx-dev-runtime -public-hoist-pattern[]=!@types/react -public-hoist-pattern[]=!@types/react-dom - diff --git a/apps/framework-docs-v2/tsconfig.json b/apps/framework-docs-v2/tsconfig.json index 7a7e4d75f0..29c0f8e1cc 100644 --- a/apps/framework-docs-v2/tsconfig.json +++ b/apps/framework-docs-v2/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@repo/ts-config/base.json", + "extends": "@repo/ts-config/nextjs.json", "compilerOptions": { "plugins": [ { @@ -21,7 +21,10 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", - "incremental": true + "incremental": true, + "declaration": false, + "declarationMap": false, + "emitDeclarationOnly": false }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] diff --git a/apps/framework-docs/tsconfig.json b/apps/framework-docs/tsconfig.json index 9b213fb0d0..aa438ff45e 100644 --- a/apps/framework-docs/tsconfig.json +++ b/apps/framework-docs/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@repo/ts-config/base.json", + "extends": "@repo/ts-config/nextjs.json", "compilerOptions": { "plugins": [ { @@ -21,7 +21,10 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", - "incremental": true + "incremental": true, + "declaration": false, + "declarationMap": false, + "emitDeclarationOnly": false }, "include": [ "next-env.d.ts", diff --git a/packages/ts-config/nextjs.json b/packages/ts-config/nextjs.json index 6a8050d396..f83274c7ec 100644 --- a/packages/ts-config/nextjs.json +++ b/packages/ts-config/nextjs.json @@ -1,15 +1,10 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "display": "Next.js", + "display": "Next.js App", "extends": "./base.json", "compilerOptions": { - "plugins": [{ "name": "next" }], - "moduleResolution": "NodeNext", - "allowJs": true, - "jsx": "preserve", - "noEmit": true, - "paths": { - "@ui/*": ["../../packages/design-system/*"] - } + "declaration": false, + "declarationMap": false, + "emitDeclarationOnly": false } } From 5a3dceca39517da5587f238f621e8a507addaf78 Mon Sep 17 00:00:00 2001 From: Tim Delisle Date: Sat, 15 Nov 2025 16:05:45 -0800 Subject: [PATCH 03/21] updated template cards --- .../src/components/mdx/template-card.tsx | 156 +++++++++--------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/apps/framework-docs-v2/src/components/mdx/template-card.tsx b/apps/framework-docs-v2/src/components/mdx/template-card.tsx index 9e78e47774..81225fb132 100644 --- a/apps/framework-docs-v2/src/components/mdx/template-card.tsx +++ b/apps/framework-docs-v2/src/components/mdx/template-card.tsx @@ -11,9 +11,8 @@ import { CardFooter, CardHeader, } from "@/components/ui/card"; -import { IconBrandGithub, IconRocket } from "@tabler/icons-react"; +import { IconBrandGithub, IconRocket, IconBook } from "@tabler/icons-react"; import { Button } from "@/components/ui/button"; -import { Separator } from "@/components/ui/separator"; import { Snippet, SnippetCopyButton, @@ -53,6 +52,7 @@ export function TemplateCard({ item, className }: TemplateCardProps) { const isTemplate = item.type === "template"; const template = isTemplate ? (item as TemplateMetadata) : null; const app = !isTemplate ? (item as AppMetadata) : null; + const [chipsExpanded, setChipsExpanded] = React.useState(false); const categoryLabels = { starter: "Starter", @@ -74,6 +74,17 @@ export function TemplateCard({ item, className }: TemplateCardProps) { const description = isTemplate ? template!.description : app!.description; const name = isTemplate ? template!.name : app!.name; + // Combine frameworks and features into a single array with type info + const allChips = [ + ...frameworks.map((f) => ({ value: f, type: "framework" as const })), + ...features.map((f) => ({ value: f, type: "feature" as const })), + ]; + + const MAX_VISIBLE_CHIPS = 3; + const visibleChips = + chipsExpanded ? allChips : allChips.slice(0, MAX_VISIBLE_CHIPS); + const hiddenCount = allChips.length - MAX_VISIBLE_CHIPS; + return (
-
- {language && ( - - {language === "typescript" ? "TypeScript" : "Python"} - - )} - {isTemplate && template && ( - - {categoryLabels[template.category]} - - )} - {!isTemplate && ( - - Demo App - - )} +
+ {(() => { + const labels: string[] = []; + if (language) { + labels.push( + language === "typescript" ? "TypeScript" : "Python", + ); + } + if (isTemplate && template) { + labels.push(categoryLabels[template.category]); + } + if (!isTemplate) { + labels.push("Demo App"); + } + return ( + + {labels.join(" • ")} + + ); + })()}

{isTemplate ? formatTemplateName(name) : name}

+ {allChips.length > 0 && ( +
+ {visibleChips.map((chip) => ( + + {chip.value} + + ))} + {!chipsExpanded && hiddenCount > 0 && ( + setChipsExpanded(true)} + > + {hiddenCount} more + + )} + {chipsExpanded && ( + setChipsExpanded(false)} + > + Show less + + )} +
+ )}
@@ -116,66 +164,6 @@ export function TemplateCard({ item, className }: TemplateCardProps) { )} - {isTemplate && template && ( - <> - {(frameworks.length > 0 || features.length > 0) && ( - <> - -
- {frameworks.map((framework) => ( - - {framework} - - ))} - {features.map((feature) => ( - - {feature} - - ))} -
- - )} - - )} - {!isTemplate && app && ( - <> - {app.blogPost && ( - - Read Blog Post → - - )} - {app.blogPost && (frameworks.length > 0 || features.length > 0) && ( - - )} - {(frameworks.length > 0 || features.length > 0) && ( -
- {frameworks.map((framework) => ( - - {framework} - - ))} - {features.map((feature) => ( - - {feature} - - ))} -
- )} - - )}
+ {!isTemplate && app && app.blogPost && ( + + )} + +
+ ); +} diff --git a/apps/framework-docs-v2/src/components/guides/guide-steps-nav.tsx b/apps/framework-docs-v2/src/components/guides/guide-steps-nav.tsx new file mode 100644 index 0000000000..c21d443733 --- /dev/null +++ b/apps/framework-docs-v2/src/components/guides/guide-steps-nav.tsx @@ -0,0 +1,190 @@ +"use client"; + +import * as React from "react"; +import { usePathname, useSearchParams } from "next/navigation"; +import Link from "next/link"; +import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { useLanguage } from "@/hooks/use-language"; + +interface Step { + slug: string; + stepNumber: number; + title: string; +} + +interface GuideStepsNavProps { + steps: Step[]; + currentSlug: string; + children?: React.ReactNode; +} + +export function GuideStepsNav({ + steps, + currentSlug, + children, +}: GuideStepsNavProps) { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const { language } = useLanguage(); + const [currentStepIndex, setCurrentStepIndex] = React.useState(0); + + // Determine current step from URL hash or default to first step + React.useEffect(() => { + const hash = window.location.hash; + if (hash) { + const stepMatch = hash.match(/step-(\d+)/); + if (stepMatch) { + const stepNum = parseInt(stepMatch[1]!, 10); + const index = steps.findIndex((s) => s.stepNumber === stepNum); + if (index >= 0) { + setCurrentStepIndex(index); + } + } + } + }, [steps]); + + // Update URL hash and show/hide steps when step changes + React.useEffect(() => { + if (steps.length > 0 && currentStepIndex < steps.length) { + const currentStep = steps[currentStepIndex]; + if (currentStep) { + const hasPrevious = currentStepIndex > 0; + const hasNext = currentStepIndex < steps.length - 1; + + // Update URL hash + window.history.replaceState( + null, + "", + `${pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ""}#step-${currentStep.stepNumber}`, + ); + + // Show/hide step content + const stepContents = document.querySelectorAll(".step-content"); + stepContents.forEach((content, index) => { + if (index === currentStepIndex) { + content.classList.remove("hidden"); + content.classList.add("block"); + } else { + content.classList.add("hidden"); + content.classList.remove("block"); + } + }); + + // Update card header with current step info + const cardTitle = document.querySelector(".step-card-title"); + const cardBadge = document.querySelector(".step-card-badge"); + const buttonsContainer = document.getElementById( + "step-nav-buttons-container", + ); + if (cardTitle) cardTitle.textContent = currentStep.title; + if (cardBadge) + cardBadge.textContent = currentStep.stepNumber.toString(); + + // Update navigation buttons + if (buttonsContainer) { + buttonsContainer.innerHTML = ` + + + `; + } + } + } + }, [currentStepIndex, steps, pathname, searchParams]); + + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const hasPrevious = currentStepIndex > 0; + const hasNext = currentStepIndex < steps.length - 1; + + const goToStep = (index: number) => { + if (index >= 0 && index < steps.length) { + setCurrentStepIndex(index); + // Scroll to top of steps section + const element = document.getElementById("guide-steps"); + if (element) { + element.scrollIntoView({ behavior: "smooth", block: "start" }); + } + } + }; + + // Expose goToStep to window for button onclick handlers + React.useEffect(() => { + (window as any).__goToStep = goToStep; + return () => { + delete (window as any).__goToStep; + }; + }, [goToStep]); + + const buildUrl = (stepSlug: string) => { + const params = new URLSearchParams(searchParams.toString()); + params.set("lang", language); + return `/${stepSlug}?${params.toString()}`; + }; + + return ( + <> +
+

Implementation Steps

+
+ {steps.map((step, index) => ( + + ))} +
+
+ + {children} + + {/* Step list for navigation */} +
+

All Steps

+
+ {steps.map((step, index) => ( + { + e.preventDefault(); + goToStep(index); + }} + className={`flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors ${ + index === currentStepIndex ? + "bg-accent text-accent-foreground" + : "hover:bg-accent/50" + }`} + > + + {step.stepNumber} + + {step.title} + + ))} +
+
+ + ); +} diff --git a/apps/framework-docs-v2/src/components/guides/guide-steps-wrapper.tsx b/apps/framework-docs-v2/src/components/guides/guide-steps-wrapper.tsx new file mode 100644 index 0000000000..f23846b21b --- /dev/null +++ b/apps/framework-docs-v2/src/components/guides/guide-steps-wrapper.tsx @@ -0,0 +1,66 @@ +import { GuideStepsNav } from "./guide-steps-nav"; +import { StepContent } from "./step-content"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; + +interface GuideStepsWrapperProps { + steps: Array<{ + slug: string; + stepNumber: number; + title: string; + }>; + stepsWithContent: Array<{ + slug: string; + stepNumber: number; + title: string; + content: string | null; + isMDX: boolean; + }>; + currentSlug: string; +} + +export async function GuideStepsWrapper({ + steps, + stepsWithContent, + currentSlug, +}: GuideStepsWrapperProps) { + // Render all step content on the server + const renderedSteps = await Promise.all( + stepsWithContent.map(async (step, index) => { + if (!step.content) return null; + return ( +
+ +
+ ); + }), + ); + + return ( +
+ + + +
+
+ + {steps[0]?.stepNumber || 1} + + + {steps[0]?.title || "Step 1"} + +
+
+
+
+ +
{renderedSteps}
+
+
+
+ ); +} diff --git a/apps/framework-docs-v2/src/components/guides/guide-steps.tsx b/apps/framework-docs-v2/src/components/guides/guide-steps.tsx new file mode 100644 index 0000000000..ab5f980354 --- /dev/null +++ b/apps/framework-docs-v2/src/components/guides/guide-steps.tsx @@ -0,0 +1,183 @@ +"use client"; + +import * as React from "react"; +import { usePathname, useSearchParams } from "next/navigation"; +import Link from "next/link"; +import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { useLanguage } from "@/hooks/use-language"; + +interface Step { + slug: string; + stepNumber: number; + title: string; +} + +interface GuideStepsProps { + steps: Step[]; + renderedSteps: React.ReactElement[]; + currentSlug: string; +} + +export function GuideSteps({ + steps, + renderedSteps, + currentSlug, +}: GuideStepsProps) { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const { language } = useLanguage(); + const [currentStepIndex, setCurrentStepIndex] = React.useState(0); + + // Determine current step from URL hash or default to first step + React.useEffect(() => { + const hash = window.location.hash; + if (hash) { + const stepMatch = hash.match(/step-(\d+)/); + if (stepMatch) { + const stepNum = parseInt(stepMatch[1]!, 10); + const index = steps.findIndex((s) => s.stepNumber === stepNum); + if (index >= 0) { + setCurrentStepIndex(index); + } + } + } + }, [steps]); + + // Update URL hash when step changes + React.useEffect(() => { + if (steps.length > 0 && currentStepIndex < steps.length) { + const currentStep = steps[currentStepIndex]; + if (currentStep) { + window.history.replaceState( + null, + "", + `${pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ""}#step-${currentStep.stepNumber}`, + ); + } + } + }, [currentStepIndex, steps, pathname, searchParams]); + + if (steps.length === 0) return null; + + const currentStep = steps[currentStepIndex]; + const currentRenderedStep = renderedSteps[currentStepIndex]; + const hasPrevious = currentStepIndex > 0; + const hasNext = currentStepIndex < steps.length - 1; + + const goToStep = (index: number) => { + if (index >= 0 && index < steps.length) { + setCurrentStepIndex(index); + // Scroll to top of steps section + const element = document.getElementById("guide-steps"); + if (element) { + element.scrollIntoView({ behavior: "smooth", block: "start" }); + } + } + }; + + const buildUrl = (stepSlug: string) => { + const params = new URLSearchParams(searchParams.toString()); + params.set("lang", language); + return `/${stepSlug}?${params.toString()}`; + }; + + return ( +
+
+

Implementation Steps

+
+ {steps.map((step, index) => ( + + ))} +
+
+ + + +
+
+ {currentStep.stepNumber} + {currentStep.title} +
+
+ + +
+
+
+ +
+ {renderedSteps.map((stepContent, index) => ( +
+ {stepContent || ( +
+ Step content not available +
+ )} +
+ ))} +
+
+
+ + {/* Step list for navigation */} +
+

All Steps

+
+ {steps.map((step, index) => ( + { + e.preventDefault(); + goToStep(index); + }} + className={`flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors ${ + index === currentStepIndex ? + "bg-accent text-accent-foreground" + : "hover:bg-accent/50" + }`} + > + + {step.stepNumber} + + {step.title} + + ))} +
+
+
+ ); +} diff --git a/apps/framework-docs-v2/src/components/guides/step-content.tsx b/apps/framework-docs-v2/src/components/guides/step-content.tsx new file mode 100644 index 0000000000..b64c151cfc --- /dev/null +++ b/apps/framework-docs-v2/src/components/guides/step-content.tsx @@ -0,0 +1,22 @@ +import { MDXRenderer } from "@/components/mdx-renderer"; + +interface StepContentProps { + content: string; + isMDX: boolean; +} + +export async function StepContent({ content, isMDX }: StepContentProps) { + if (!content) { + return ( +
Step content not available
+ ); + } + + return ( +
+ {isMDX ? + + :
} +
+ ); +} diff --git a/apps/framework-docs-v2/src/components/navigation/side-nav.tsx b/apps/framework-docs-v2/src/components/navigation/side-nav.tsx index fb1380ed5f..e3b5ef66bf 100644 --- a/apps/framework-docs-v2/src/components/navigation/side-nav.tsx +++ b/apps/framework-docs-v2/src/components/navigation/side-nav.tsx @@ -9,9 +9,11 @@ import { Sidebar, SidebarContent, SidebarGroup, + SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuAction, + SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, @@ -174,10 +176,21 @@ function NavItemComponent({ item }: { item: NavPage }) { }, [hasChildren, item.children, pathname]); const defaultOpen = isActive || hasActiveDescendant; + const [isOpen, setIsOpen] = React.useState(defaultOpen); + + // Update open state when active state changes + React.useEffect(() => { + setIsOpen(isActive || hasActiveDescendant); + }, [isActive, hasActiveDescendant]); if (hasChildren) { return ( - + @@ -224,6 +237,91 @@ function NavItemComponent({ item }: { item: NavPage }) { ); } +function NestedNavItemComponent({ + item, + pathname, + searchParams, + language, +}: { + item: NavPage; + pathname: string; + searchParams: URLSearchParams; + language: string; +}) { + const childHasChildren = item.children && item.children.length > 0; + const childHref = (() => { + const params = new URLSearchParams(searchParams.toString()); + params.set("lang", language); + return `/${item.slug}?${params.toString()}`; + })(); + const childIsActive = pathname === `/${item.slug}`; + + // Recursively check if any descendant is active + const checkDescendant = (children: NavItem[]): boolean => { + return children.some((c) => { + if (c.type === "page") { + if (pathname === `/${c.slug}`) return true; + if (c.children) return checkDescendant(c.children); + } + return false; + }); + }; + const hasActiveDescendant = + childHasChildren ? checkDescendant(item.children!) : false; + const defaultOpen = childIsActive || hasActiveDescendant; + const [isOpen, setIsOpen] = React.useState(defaultOpen); + + React.useEffect(() => { + setIsOpen(childIsActive || hasActiveDescendant); + }, [childIsActive, hasActiveDescendant]); + + if (childHasChildren) { + return ( + + + + + {item.icon && } + {item.title} + + + + + + Toggle + + + + + {renderNavChildren( + item.children!, + pathname, + searchParams, + language, + )} + + + + + ); + } + + return ( + + + + {item.icon && } + {item.title} + + + + ); +} + function renderNavChildren( children: NavItem[], pathname: string, @@ -231,169 +329,19 @@ function renderNavChildren( language: string, ): React.ReactNode[] { const elements: React.ReactNode[] = []; - let currentGroup: NavPage[] = []; - let currentLabel: string | null = null; - - const flushGroup = () => { - if (currentGroup.length > 0) { - currentGroup.forEach((child: NavPage) => { - const childHasChildren = child.children && child.children.length > 0; - const childHref = (() => { - const params = new URLSearchParams(searchParams.toString()); - params.set("lang", language); - return `/${child.slug}?${params.toString()}`; - })(); - const childIsActive = pathname === `/${child.slug}`; - - // Recursively check if any descendant is active - const checkDescendant = (children: NavItem[]): boolean => { - return children.some((c) => { - if (c.type === "page") { - if (pathname === `/${c.slug}`) return true; - if (c.children) return checkDescendant(c.children); - } - return false; - }); - }; - const hasActiveDescendant = - childHasChildren ? checkDescendant(child.children!) : false; - const defaultOpen = childIsActive || hasActiveDescendant; - - if (childHasChildren) { - // Render nested collapsible item - using same pattern as top-level - // SidebarMenuSubItem needs relative positioning for SidebarMenuAction - elements.push( - - - - - {child.icon && } - {child.title} - - - - - - Toggle - - - - - {renderNavChildren( - child.children!, - pathname, - searchParams, - language, - )} - - - - , - ); - } else { - // Render simple link for leaf nodes - elements.push( - - - - {child.icon && } - {child.title} - - - , - ); - } - }); - currentGroup = []; - } - }; children.forEach((child) => { - if (child.type === "separator") { - flushGroup(); - currentLabel = null; - } else if (child.type === "label") { - flushGroup(); - currentLabel = child.title; - } else if (child.type === "section") { - flushGroup(); - // Check if any item in the section is active to determine default open state - const hasActiveItem = child.items.some((item) => { - if (item.type === "page") { - return pathname === `/${item.slug}`; - } - return false; - }); - - // Render collapsible section within the submenu - // We need to render the trigger and items as siblings, not nested - const sectionItems: React.ReactNode[] = []; - child.items.forEach((item) => { - if (item.type === "page") { - const itemHref = (() => { - const params = new URLSearchParams(searchParams.toString()); - params.set("lang", language); - return `/${item.slug}?${params.toString()}`; - })(); - const itemIsActive = pathname === `/${item.slug}`; - sectionItems.push( - - - - {item.icon && } - {item.title} - - - , - ); - } - }); - - elements.push( - - - - - - {child.icon && ( - - )} - - {child.title} - - - - - - - - {sectionItems} - - , - ); - } else if (child.type === "page") { - if (currentLabel && currentGroup.length === 0) { - // Add label before first item in group - elements.push( - - {currentLabel} - , - ); - } - currentGroup.push(child); - } + if (child.type !== "page") return; + elements.push( + , + ); }); - flushGroup(); + return elements; } diff --git a/apps/framework-docs-v2/src/config/navigation.ts b/apps/framework-docs-v2/src/config/navigation.ts index b387df8438..e1348ca9e9 100644 --- a/apps/framework-docs-v2/src/config/navigation.ts +++ b/apps/framework-docs-v2/src/config/navigation.ts @@ -1,3 +1,4 @@ +import * as React from "react"; import type { Language } from "@/lib/content-types"; import { IconChartArea, @@ -877,259 +878,85 @@ const guidesNavigationConfig: NavigationConfig = [ items: [ { type: "page", - slug: "guides/applications/performant-dashboards/overview", + slug: "guides/applications/performant-dashboards/guide-overview", title: "Performant Dashboards", icon: IconChartLine, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/applications/performant-dashboards/guide-overview", - title: "Overview", + slug: "guides/applications/performant-dashboards/existing-oltp-db", + title: "From Existing OLTP DB", languages: ["typescript", "python"], }, { - type: "section", - title: "Existing OLTP DB", - items: [ - { - type: "page", - slug: "guides/applications/performant-dashboards/existing-oltp-db/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/performant-dashboards/existing-oltp-db/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/performant-dashboards/existing-oltp-db/1-setup-connection", - title: "Setup Connection", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/performant-dashboards/existing-oltp-db/2-create-materialized-view", - title: "Create Materialized View", - languages: ["typescript", "python"], - }, - ], - }, - { - type: "section", + type: "page", + slug: "guides/applications/performant-dashboards/new-application", title: "New Application", - items: [ - { - type: "page", - slug: "guides/applications/performant-dashboards/new-application/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/performant-dashboards/new-application/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/performant-dashboards/new-application/1-initialize-project", - title: "Initialize Project", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, ], }, { type: "page", - slug: "guides/applications/in-app-chat-analytics/overview", + slug: "guides/applications/in-app-chat-analytics/guide-overview", title: "In-App Chat Analytics", icon: IconMessageChatbot, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/applications/in-app-chat-analytics/guide-overview", - title: "Overview", - languages: ["typescript", "python"], - }, - { - type: "section", + slug: "guides/applications/in-app-chat-analytics/existing-chat-system", title: "Existing Chat System", - items: [ - { - type: "page", - slug: "guides/applications/in-app-chat-analytics/existing-chat-system/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/in-app-chat-analytics/existing-chat-system/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/in-app-chat-analytics/existing-chat-system/1-integrate-event-tracking", - title: "Integrate Event Tracking", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, { - type: "section", + type: "page", + slug: "guides/applications/in-app-chat-analytics/new-chat-feature", title: "New Chat Feature", - items: [ - { - type: "page", - slug: "guides/applications/in-app-chat-analytics/new-chat-feature/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/in-app-chat-analytics/new-chat-feature/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/in-app-chat-analytics/new-chat-feature/1-setup-chat-schema", - title: "Setup Chat Schema", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, ], }, { type: "page", - slug: "guides/applications/automated-reports/overview", + slug: "guides/applications/automated-reports/guide-overview", title: "Automated Reports", icon: IconFileReport, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/applications/automated-reports/guide-overview", - title: "Overview", - languages: ["typescript", "python"], - }, - { - type: "section", + slug: "guides/applications/automated-reports/scheduled-reports", title: "Scheduled Reports", - items: [ - { - type: "page", - slug: "guides/applications/automated-reports/scheduled-reports/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/automated-reports/scheduled-reports/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/automated-reports/scheduled-reports/1-create-report-template", - title: "Create Report Template", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, { - type: "section", + type: "page", + slug: "guides/applications/automated-reports/event-driven-reports", title: "Event-Driven Reports", - items: [ - { - type: "page", - slug: "guides/applications/automated-reports/event-driven-reports/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/automated-reports/event-driven-reports/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/automated-reports/event-driven-reports/1-setup-event-triggers", - title: "Setup Event Triggers", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, ], }, { type: "page", - slug: "guides/applications/going-to-production/overview", + slug: "guides/applications/going-to-production/guide-overview", title: "Going to Production", icon: IconCloudUpload, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/applications/going-to-production/guide-overview", - title: "Overview", - languages: ["typescript", "python"], - }, - { - type: "section", + slug: "guides/applications/going-to-production/local-development", title: "Local Development", - items: [ - { - type: "page", - slug: "guides/applications/going-to-production/local-development/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/going-to-production/local-development/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/going-to-production/local-development/1-prepare-environment", - title: "Prepare Environment", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, { - type: "section", + type: "page", + slug: "guides/applications/going-to-production/staging-environment", title: "Staging Environment", - items: [ - { - type: "page", - slug: "guides/applications/going-to-production/staging-environment/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/going-to-production/staging-environment/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/applications/going-to-production/staging-environment/1-deploy-infrastructure", - title: "Deploy Infrastructure", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, ], }, @@ -1141,214 +968,70 @@ const guidesNavigationConfig: NavigationConfig = [ items: [ { type: "page", - slug: "guides/data-management/migrations/overview", + slug: "guides/data-management/migrations/guide-overview", title: "Migrations", icon: IconDatabaseImport, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/data-management/migrations/guide-overview", - title: "Overview", - languages: ["typescript", "python"], - }, - { - type: "section", + slug: "guides/data-management/migrations/schema-changes", title: "Schema Changes", - items: [ - { - type: "page", - slug: "guides/data-management/migrations/schema-changes/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-management/migrations/schema-changes/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-management/migrations/schema-changes/1-create-migration-script", - title: "Create Migration Script", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, { - type: "section", + type: "page", + slug: "guides/data-management/migrations/data-migration", title: "Data Migration", - items: [ - { - type: "page", - slug: "guides/data-management/migrations/data-migration/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-management/migrations/data-migration/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-management/migrations/data-migration/1-backup-existing-data", - title: "Backup Existing Data", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, { - type: "section", + type: "page", + slug: "guides/data-management/migrations/version-upgrades", title: "Version Upgrades", - items: [ - { - type: "page", - slug: "guides/data-management/migrations/version-upgrades/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-management/migrations/version-upgrades/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-management/migrations/version-upgrades/1-review-changelog", - title: "Review Changelog", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, ], }, { type: "page", - slug: "guides/data-management/impact-analysis/overview", + slug: "guides/data-management/impact-analysis/guide-overview", title: "Impact Analysis", icon: IconChartDots, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/data-management/impact-analysis/guide-overview", - title: "Overview", - languages: ["typescript", "python"], - }, - { - type: "section", + slug: "guides/data-management/impact-analysis/schema-changes", title: "Schema Changes", - items: [ - { - type: "page", - slug: "guides/data-management/impact-analysis/schema-changes/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-management/impact-analysis/schema-changes/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-management/impact-analysis/schema-changes/1-identify-dependencies", - title: "Identify Dependencies", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, { - type: "section", + type: "page", + slug: "guides/data-management/impact-analysis/query-changes", title: "Query Changes", - items: [ - { - type: "page", - slug: "guides/data-management/impact-analysis/query-changes/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-management/impact-analysis/query-changes/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-management/impact-analysis/query-changes/1-analyze-query-performance", - title: "Analyze Query Performance", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, ], }, { type: "page", - slug: "guides/data-management/change-data-capture/overview", + slug: "guides/data-management/change-data-capture/guide-overview", title: "Change Data Capture", icon: IconBolt, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/data-management/change-data-capture/guide-overview", - title: "Overview", - languages: ["typescript", "python"], - }, - { - type: "section", + slug: "guides/data-management/change-data-capture/database-cdc", title: "Database CDC", - items: [ - { - type: "page", - slug: "guides/data-management/change-data-capture/database-cdc/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-management/change-data-capture/database-cdc/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-management/change-data-capture/database-cdc/1-enable-cdc-logging", - title: "Enable CDC Logging", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, { - type: "section", + type: "page", + slug: "guides/data-management/change-data-capture/application-events", title: "Application Events", - items: [ - { - type: "page", - slug: "guides/data-management/change-data-capture/application-events/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-management/change-data-capture/application-events/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-management/change-data-capture/application-events/1-implement-event-emitter", - title: "Implement Event Emitter", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, ], }, @@ -1360,280 +1043,112 @@ const guidesNavigationConfig: NavigationConfig = [ items: [ { type: "page", - slug: "guides/data-warehousing/customer-data-platform/overview", + slug: "guides/data-warehousing/customer-data-platform/guide-overview", title: "Customer Data Platform", icon: IconUsers, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/data-warehousing/customer-data-platform/guide-overview", - title: "Overview", - languages: ["typescript", "python"], - }, - { - type: "section", + slug: "guides/data-warehousing/customer-data-platform/existing-customer-data", title: "Existing Customer Data", - items: [ - { - type: "page", - slug: "guides/data-warehousing/customer-data-platform/existing-customer-data/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-warehousing/customer-data-platform/existing-customer-data/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-warehousing/customer-data-platform/existing-customer-data/1-consolidate-data-sources", - title: "Consolidate Data Sources", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, { - type: "section", + type: "page", + slug: "guides/data-warehousing/customer-data-platform/multi-source-integration", title: "Multi-Source Integration", - items: [ - { - type: "page", - slug: "guides/data-warehousing/customer-data-platform/multi-source-integration/overview", - title: "Implementation Overview", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-warehousing/customer-data-platform/multi-source-integration/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-warehousing/customer-data-platform/multi-source-integration/1-setup-connectors", - title: "Setup Connectors", - languages: ["typescript", "python"], - }, - ], + languages: ["typescript", "python"], }, ], }, { type: "page", - slug: "guides/data-warehousing/operational-analytics/overview", + slug: "guides/data-warehousing/operational-analytics/guide-overview", title: "Operational Analytics", icon: IconChartBarOff, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/data-warehousing/operational-analytics/application-metrics/overview", + slug: "guides/data-warehousing/operational-analytics/application-metrics", title: "Application Metrics", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/data-warehousing/operational-analytics/application-metrics/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-warehousing/operational-analytics/application-metrics/1-instrument-application", - title: "Instrument Application", - languages: ["typescript", "python"], - }, - ], }, { type: "page", - slug: "guides/data-warehousing/operational-analytics/infrastructure-monitoring/overview", + slug: "guides/data-warehousing/operational-analytics/infrastructure-monitoring", title: "Infrastructure Monitoring", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/data-warehousing/operational-analytics/infrastructure-monitoring/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-warehousing/operational-analytics/infrastructure-monitoring/1-collect-system-metrics", - title: "Collect System Metrics", - languages: ["typescript", "python"], - }, - ], }, ], }, { type: "page", - slug: "guides/data-warehousing/startup-metrics/overview", + slug: "guides/data-warehousing/startup-metrics/guide-overview", title: "Startup Metrics", icon: IconChartBar, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/data-warehousing/startup-metrics/product-metrics/overview", + slug: "guides/data-warehousing/startup-metrics/product-metrics", title: "Product Metrics", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/data-warehousing/startup-metrics/product-metrics/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-warehousing/startup-metrics/product-metrics/1-define-kpis", - title: "Define KPIs", - languages: ["typescript", "python"], - }, - ], }, { type: "page", - slug: "guides/data-warehousing/startup-metrics/business-metrics/overview", + slug: "guides/data-warehousing/startup-metrics/business-metrics", title: "Business Metrics", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/data-warehousing/startup-metrics/business-metrics/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-warehousing/startup-metrics/business-metrics/1-setup-revenue-tracking", - title: "Setup Revenue Tracking", - languages: ["typescript", "python"], - }, - ], }, ], }, { type: "page", - slug: "guides/data-warehousing/connectors/overview", + slug: "guides/data-warehousing/connectors/guide-overview", title: "Connectors", icon: IconStack, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/data-warehousing/connectors/database-connector/overview", + slug: "guides/data-warehousing/connectors/database-connector", title: "Database Connector", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/data-warehousing/connectors/database-connector/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-warehousing/connectors/database-connector/1-configure-connection", - title: "Configure Connection", - languages: ["typescript", "python"], - }, - ], }, { type: "page", - slug: "guides/data-warehousing/connectors/api-connector/overview", + slug: "guides/data-warehousing/connectors/api-connector", title: "API Connector", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/data-warehousing/connectors/api-connector/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-warehousing/connectors/api-connector/1-setup-authentication", - title: "Setup Authentication", - languages: ["typescript", "python"], - }, - ], }, { type: "page", - slug: "guides/data-warehousing/connectors/custom-connector/overview", + slug: "guides/data-warehousing/connectors/custom-connector", title: "Custom Connector", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/data-warehousing/connectors/custom-connector/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-warehousing/connectors/custom-connector/1-create-connector-class", - title: "Create Connector Class", - languages: ["typescript", "python"], - }, - ], }, ], }, { type: "page", - slug: "guides/data-warehousing/pipelines/overview", + slug: "guides/data-warehousing/pipelines/guide-overview", title: "Pipelines", icon: IconRoute, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/data-warehousing/pipelines/etl-pipeline/overview", + slug: "guides/data-warehousing/pipelines/etl-pipeline", title: "ETL Pipeline", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/data-warehousing/pipelines/etl-pipeline/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-warehousing/pipelines/etl-pipeline/1-extract-data", - title: "Extract Data", - languages: ["typescript", "python"], - }, - ], }, { type: "page", - slug: "guides/data-warehousing/pipelines/streaming-pipeline/overview", + slug: "guides/data-warehousing/pipelines/streaming-pipeline", title: "Streaming Pipeline", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/data-warehousing/pipelines/streaming-pipeline/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/data-warehousing/pipelines/streaming-pipeline/1-setup-stream-source", - title: "Setup Stream Source", - languages: ["typescript", "python"], - }, - ], }, ], }, @@ -1645,99 +1160,43 @@ const guidesNavigationConfig: NavigationConfig = [ items: [ { type: "page", - slug: "guides/methodology/data-as-code/overview", + slug: "guides/methodology/data-as-code/guide-overview", title: "Data as Code", icon: IconCode, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/methodology/data-as-code/version-control-setup/overview", + slug: "guides/methodology/data-as-code/version-control-setup", title: "Version Control Setup", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/methodology/data-as-code/version-control-setup/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/methodology/data-as-code/version-control-setup/1-initialize-repository", - title: "Initialize Repository", - languages: ["typescript", "python"], - }, - ], }, { type: "page", - slug: "guides/methodology/data-as-code/cicd-integration/overview", + slug: "guides/methodology/data-as-code/cicd-integration", title: "CI/CD Integration", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/methodology/data-as-code/cicd-integration/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/methodology/data-as-code/cicd-integration/1-create-pipeline-config", - title: "Create Pipeline Config", - languages: ["typescript", "python"], - }, - ], }, ], }, { type: "page", - slug: "guides/methodology/dora-for-data/overview", + slug: "guides/methodology/dora-for-data/guide-overview", title: "DORA for Data", icon: IconTrendingUp, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/methodology/dora-for-data/deployment-frequency/overview", + slug: "guides/methodology/dora-for-data/deployment-frequency", title: "Deployment Frequency", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/methodology/dora-for-data/deployment-frequency/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/methodology/dora-for-data/deployment-frequency/1-measure-current-frequency", - title: "Measure Current Frequency", - languages: ["typescript", "python"], - }, - ], }, { type: "page", - slug: "guides/methodology/dora-for-data/lead-time/overview", + slug: "guides/methodology/dora-for-data/lead-time", title: "Lead Time", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/methodology/dora-for-data/lead-time/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/methodology/dora-for-data/lead-time/1-track-change-lifecycle", - title: "Track Change Lifecycle", - languages: ["typescript", "python"], - }, - ], }, ], }, @@ -1749,197 +1208,85 @@ const guidesNavigationConfig: NavigationConfig = [ items: [ { type: "page", - slug: "guides/strategy/ai-enablement/overview", + slug: "guides/strategy/ai-enablement/guide-overview", title: "AI Enablement", icon: IconBrain, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/strategy/ai-enablement/llm-integration/overview", + slug: "guides/strategy/ai-enablement/llm-integration", title: "LLM Integration", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/strategy/ai-enablement/llm-integration/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/strategy/ai-enablement/llm-integration/1-choose-llm-provider", - title: "Choose LLM Provider", - languages: ["typescript", "python"], - }, - ], }, { type: "page", - slug: "guides/strategy/ai-enablement/vector-search/overview", + slug: "guides/strategy/ai-enablement/vector-search", title: "Vector Search", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/strategy/ai-enablement/vector-search/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/strategy/ai-enablement/vector-search/1-setup-vector-database", - title: "Setup Vector Database", - languages: ["typescript", "python"], - }, - ], }, ], }, { type: "page", - slug: "guides/strategy/data-foundation/overview", + slug: "guides/strategy/data-foundation/guide-overview", title: "Data Foundation", icon: IconDatabase, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/strategy/data-foundation/greenfield-project/overview", + slug: "guides/strategy/data-foundation/greenfield-project", title: "Greenfield Project", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/strategy/data-foundation/greenfield-project/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/strategy/data-foundation/greenfield-project/1-design-data-architecture", - title: "Design Data Architecture", - languages: ["typescript", "python"], - }, - ], }, { type: "page", - slug: "guides/strategy/data-foundation/legacy-system-migration/overview", + slug: "guides/strategy/data-foundation/legacy-system-migration", title: "Legacy System Migration", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/strategy/data-foundation/legacy-system-migration/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/strategy/data-foundation/legacy-system-migration/1-assess-current-state", - title: "Assess Current State", - languages: ["typescript", "python"], - }, - ], }, ], }, { type: "page", - slug: "guides/strategy/platform-engineering/overview", + slug: "guides/strategy/platform-engineering/guide-overview", title: "Platform Engineering", icon: IconServer, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/strategy/platform-engineering/internal-platform/overview", + slug: "guides/strategy/platform-engineering/internal-platform", title: "Internal Platform", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/strategy/platform-engineering/internal-platform/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/strategy/platform-engineering/internal-platform/1-define-platform-scope", - title: "Define Platform Scope", - languages: ["typescript", "python"], - }, - ], }, { type: "page", - slug: "guides/strategy/platform-engineering/self-service-tools/overview", + slug: "guides/strategy/platform-engineering/self-service-tools", title: "Self-Service Tools", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/strategy/platform-engineering/self-service-tools/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/strategy/platform-engineering/self-service-tools/1-create-developer-portal", - title: "Create Developer Portal", - languages: ["typescript", "python"], - }, - ], }, ], }, { type: "page", - slug: "guides/strategy/olap-evaluation/overview", + slug: "guides/strategy/olap-evaluation/guide-overview", title: "OLAP Evaluation", icon: IconDatabase, languages: ["typescript", "python"], children: [ { type: "page", - slug: "guides/strategy/olap-evaluation/performance-requirements/overview", + slug: "guides/strategy/olap-evaluation/performance-requirements", title: "Performance Requirements", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/strategy/olap-evaluation/performance-requirements/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/strategy/olap-evaluation/performance-requirements/1-benchmark-queries", - title: "Benchmark Queries", - languages: ["typescript", "python"], - }, - ], }, { type: "page", - slug: "guides/strategy/olap-evaluation/scale-requirements/overview", + slug: "guides/strategy/olap-evaluation/scale-requirements", title: "Scale Requirements", languages: ["typescript", "python"], - children: [ - { - type: "page", - slug: "guides/strategy/olap-evaluation/scale-requirements/requirements", - title: "Requirements", - languages: ["typescript", "python"], - }, - { - type: "page", - slug: "guides/strategy/olap-evaluation/scale-requirements/1-estimate-data-volume", - title: "Estimate Data Volume", - languages: ["typescript", "python"], - }, - ], }, ], }, diff --git a/apps/framework-docs-v2/src/lib/content.ts b/apps/framework-docs-v2/src/lib/content.ts index 861e1b7538..9097d01027 100644 --- a/apps/framework-docs-v2/src/lib/content.ts +++ b/apps/framework-docs-v2/src/lib/content.ts @@ -244,3 +244,52 @@ export function getAllSlugs(): string[] { const uniqueSlugs = Array.from(new Set(slugs)); return uniqueSlugs; } + +/** + * Discover step files in a directory + * Returns step files matching the pattern: {number}-{name}.mdx + * Sorted by step number + */ +export function discoverStepFiles(slug: string): Array<{ + slug: string; + stepNumber: number; + title: string; +}> { + const dirPath = path.join(CONTENT_ROOT, slug); + + if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) { + return []; + } + + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + const steps: Array<{ slug: string; stepNumber: number; title: string }> = []; + + for (const entry of entries) { + if (!entry.isFile()) continue; + + // Match pattern: {number}-{name}.mdx + const stepMatch = entry.name.match(/^(\d+)-(.+)\.mdx$/); + if (!stepMatch) continue; + + const stepNumber = parseInt(stepMatch[1]!, 10); + const stepName = stepMatch[2]; + if (!stepName) continue; + + const stepSlug = `${slug}/${entry.name.replace(/\.mdx$/, "")}`; + + // Read front matter to get title + const filePath = path.join(dirPath, entry.name); + const fileContents = fs.readFileSync(filePath, "utf8"); + const { data } = matter(fileContents); + const frontMatter = data as FrontMatter; + + steps.push({ + slug: stepSlug, + stepNumber, + title: (frontMatter.title as string) || stepName.replace(/-/g, " "), + }); + } + + // Sort by step number + return steps.sort((a, b) => a.stepNumber - b.stepNumber); +} From 227bd93cecf64284dfaac640dd1b378c4974ce98 Mon Sep 17 00:00:00 2001 From: Tim Delisle Date: Sun, 16 Nov 2025 17:35:51 -0800 Subject: [PATCH 07/21] fixed some styles --- apps/framework-docs-v2/package.json | 2 +- .../src/components/navigation/side-nav.tsx | 2 - .../src/components/navigation/toc-nav.tsx | 115 +++++++++++++++++- .../src/components/ui/popover.tsx | 33 +++++ pnpm-lock.yaml | 2 +- 5 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 apps/framework-docs-v2/src/components/ui/popover.tsx diff --git a/apps/framework-docs-v2/package.json b/apps/framework-docs-v2/package.json index 9780a46f10..14453c6d43 100644 --- a/apps/framework-docs-v2/package.json +++ b/apps/framework-docs-v2/package.json @@ -29,7 +29,7 @@ "@radix-ui/react-navigation-menu": "^1.2.13", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.2", - "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", diff --git a/apps/framework-docs-v2/src/components/navigation/side-nav.tsx b/apps/framework-docs-v2/src/components/navigation/side-nav.tsx index e3b5ef66bf..7df49d05a1 100644 --- a/apps/framework-docs-v2/src/components/navigation/side-nav.tsx +++ b/apps/framework-docs-v2/src/components/navigation/side-nav.tsx @@ -9,11 +9,9 @@ import { Sidebar, SidebarContent, SidebarGroup, - SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuAction, - SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, diff --git a/apps/framework-docs-v2/src/components/navigation/toc-nav.tsx b/apps/framework-docs-v2/src/components/navigation/toc-nav.tsx index 0be4c2b7f6..f0fa5352f4 100644 --- a/apps/framework-docs-v2/src/components/navigation/toc-nav.tsx +++ b/apps/framework-docs-v2/src/components/navigation/toc-nav.tsx @@ -1,9 +1,29 @@ "use client"; import { useEffect, useState } from "react"; +import { usePathname } from "next/navigation"; import { cn } from "@/lib/utils"; import type { Heading } from "@/lib/content-types"; -import { IconExternalLink } from "@tabler/icons-react"; +import { + IconExternalLink, + IconPlus, + IconInfoCircle, +} from "@tabler/icons-react"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Label } from "../ui/label"; interface TOCNavProps { headings: Heading[]; @@ -15,6 +35,10 @@ interface TOCNavProps { export function TOCNav({ headings, helpfulLinks }: TOCNavProps) { const [activeId, setActiveId] = useState(""); + const [scope, setScope] = useState<"initiative" | "project">("initiative"); + const pathname = usePathname(); + const isGuidePage = + pathname?.startsWith("/guides/") && pathname !== "/guides"; useEffect(() => { if (headings.length === 0) return; @@ -123,7 +147,7 @@ export function TOCNav({ headings, helpfulLinks }: TOCNavProps) { } return ( -
+ } + > + + + ); + }); return (
diff --git a/apps/framework-docs-v2/src/components/navigation/toc-nav.tsx b/apps/framework-docs-v2/src/components/navigation/toc-nav.tsx index f0fa5352f4..dabd27899c 100644 --- a/apps/framework-docs-v2/src/components/navigation/toc-nav.tsx +++ b/apps/framework-docs-v2/src/components/navigation/toc-nav.tsx @@ -153,9 +153,9 @@ export function TOCNav({ headings, helpfulLinks }: TOCNavProps) {

On this page

); @@ -453,7 +454,7 @@ export function MDXPre({ children, ...props }: MDXCodeBlockProps) { props["data-rehype-pretty-code-title"] || props["data-filename"] || props["title"]; // Also check for title prop directly - const hasCopy = props["data-copy"] !== undefined; + const hasCopy = props["data-copy"] !== "false"; const isShell = SHELL_LANGUAGES.has(language); const isConfigFile = CONFIG_LANGUAGES.has(language); @@ -473,7 +474,7 @@ export function MDXPre({ children, ...props }: MDXCodeBlockProps) { code={codeText} language={language} filename={filename || undefined} - copyButton={true} + copyButton={hasCopy} />
); @@ -506,7 +507,7 @@ export function MDXPre({ children, ...props }: MDXCodeBlockProps) { } // If filename is provided and no copy attribute, use animated CodeEditor - if (filename && !hasCopy) { + if (filename && props["data-copy"] === undefined) { // Determine if this is a terminal based on language const isTerminalLang = SHELL_LANGUAGES.has(language); return ( @@ -531,7 +532,7 @@ export function MDXPre({ children, ...props }: MDXCodeBlockProps) { code={codeText} language={language || "typescript"} filename={filename || undefined} - copyButton={true} + copyButton={hasCopy} />
); @@ -578,6 +579,7 @@ export function MDXCode({ children, className, ...props }: MDXCodeProps) { // Config files use CodeSnippet const filename = props["data-rehype-pretty-code-title"] || props["data-filename"]; + const hasCopy = props["data-copy"] !== "false"; return (
@@ -585,19 +587,20 @@ export function MDXCode({ children, className, ...props }: MDXCodeProps) { code={codeText} language={language} filename={filename} - copyButton={true} + copyButton={hasCopy} />
); } // Default to CodeSnippet for editable code blocks + const hasCopy = props["data-copy"] !== "false"; return (
); diff --git a/apps/framework-docs-v2/src/lib/github-stars.ts b/apps/framework-docs-v2/src/lib/github-stars.ts index 09c821bedf..c2764d5c45 100644 --- a/apps/framework-docs-v2/src/lib/github-stars.ts +++ b/apps/framework-docs-v2/src/lib/github-stars.ts @@ -7,15 +7,23 @@ import { unstable_cache } from "next/cache"; */ async function fetchGitHubStars(): Promise { try { + const headers: HeadersInit = { + // GitHub API requires a user-agent + "User-Agent": "MooseDocs", + }; + + // Add Authorization header with token if available to increase rate limit + // Without token: 60 requests/hour + // With token: 5,000 requests/hour + const githubToken = process.env.GITHUB_TOKEN; + if (githubToken) { + headers.Authorization = `token ${githubToken}`; + } + const response = await fetch( "https://api.github.com/repos/514-labs/moose", { - headers: { - // GitHub API requires a user-agent - "User-Agent": "MooseDocs", - // Optional: Add Authorization header with token to increase rate limit - // Authorization: `token ${process.env.GITHUB_TOKEN}`, - }, + headers, }, ); From 3115c6145c79e9ec9f5f09d7e960c54af8e7dabc Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 20 Nov 2025 17:42:58 -0800 Subject: [PATCH 14/21] adding library back in --- apps/framework-docs-v2/package.json | 1 + pnpm-lock.yaml | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/framework-docs-v2/package.json b/apps/framework-docs-v2/package.json index dd9895136c..1ec487751a 100644 --- a/apps/framework-docs-v2/package.json +++ b/apps/framework-docs-v2/package.json @@ -67,6 +67,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", + "unist-util-visit": "^5.0.0", "zod": "^3.25.76" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d7945029e..e7726b6930 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,7 +65,7 @@ importers: devDependencies: '@clickhouse/client': specifier: latest - version: 1.13.0 + version: 1.14.0 '@iarna/toml': specifier: ^3.0.0 version: 3.0.0 @@ -397,6 +397,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) + unist-util-visit: + specifier: ^5.0.0 + version: 5.0.0 zod: specifier: ^3.25.76 version: 3.25.76 @@ -1351,8 +1354,8 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} - '@clickhouse/client-common@1.13.0': - resolution: {integrity: sha512-QlGUMd3EaKkIRLCv0WW8Rw9cOlqhwQPT+ucNWY8eC4UALsMhJLpa0H7Cd7MYc9CEtTv/xlr3IcYw5Tdho4Hr2g==} + '@clickhouse/client-common@1.14.0': + resolution: {integrity: sha512-CyUcv2iCkZ1A++vmOSufYRpHR3aAWVfbrWed7ATzf0yyx/BW/2SEqlL07vBpSRa3BIkQe/DSOHVv8JkWZpUOwQ==} '@clickhouse/client-common@1.5.0': resolution: {integrity: sha512-U3vDp+PDnNVEv6kia+Mq5ygnlMZzsYU+3TX+0da3XvL926jzYLMBlIvFUxe2+/5k47ySvnINRC/2QxVK7PC2/A==} @@ -1363,8 +1366,8 @@ packages: '@clickhouse/client-web@1.5.0': resolution: {integrity: sha512-21+c2UJ4cx9SPiIWQThCLULb8h/zng0pNrtTwbbnaoCqMbasyRCyRTHs3wRr7fqRUcZ3p9krIPuN0gnJw3GJ6Q==} - '@clickhouse/client@1.13.0': - resolution: {integrity: sha512-uK+zqPaJnAoq3QIOvUNbHtbWUhyg2A/aSbdJtrY2+kawp4SMBLcfIbB9ucRv5Yht1CAa3b24CiUlypkmgarukg==} + '@clickhouse/client@1.14.0': + resolution: {integrity: sha512-co2spjR7wZoZ3Ck0H/jv76bpiuO3oJHtOmq9/gxFiod2DcT9NFg01u/hXcG8MJFnEJuMB6e3vGqS6IOnLwHqRw==} engines: {node: '>=16'} '@clickhouse/client@1.8.1': @@ -10319,7 +10322,7 @@ snapshots: '@chevrotain/utils@11.0.3': {} - '@clickhouse/client-common@1.13.0': {} + '@clickhouse/client-common@1.14.0': {} '@clickhouse/client-common@1.5.0': {} @@ -10329,9 +10332,9 @@ snapshots: dependencies: '@clickhouse/client-common': 1.5.0 - '@clickhouse/client@1.13.0': + '@clickhouse/client@1.14.0': dependencies: - '@clickhouse/client-common': 1.13.0 + '@clickhouse/client-common': 1.14.0 '@clickhouse/client@1.8.1': dependencies: From eff9086c0ca0240f4b29c880e14cfeafe60824f2 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 25 Nov 2025 17:12:29 -0800 Subject: [PATCH 15/21] fix side nav --- apps/framework-docs-v2/public/sitemap-0.xml | 302 +++++++++--------- .../src/components/navigation/side-nav.tsx | 35 +- .../src/components/ui/sidebar.tsx | 19 ++ 3 files changed, 194 insertions(+), 162 deletions(-) diff --git a/apps/framework-docs-v2/public/sitemap-0.xml b/apps/framework-docs-v2/public/sitemap-0.xml index 31972a45f9..88aaa0ea28 100644 --- a/apps/framework-docs-v2/public/sitemap-0.xml +++ b/apps/framework-docs-v2/public/sitemap-0.xml @@ -1,154 +1,154 @@ -https://docs.fiveonefour.com/ai2025-11-21T01:39:42.116Zdaily0.7 -https://docs.fiveonefour.com/ai/data-collection-policy2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/demos/context2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/demos/dlqs2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/demos/egress2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/demos/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/demos/ingest2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/demos/model-data2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/demos/mvs2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/getting-started/claude2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/getting-started/cursor2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/getting-started/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/getting-started/other-clients2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/getting-started/vs-code2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/getting-started/windsurf2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/guides/clickhouse-chat2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/guides/clickhouse-proj2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/guides/from-template2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/guides/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/reference/cli-reference2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/reference/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/reference/mcp-json-reference2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/ai/reference/tool-reference2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/hosting2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/hosting/deployment2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/hosting/getting-started2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/hosting/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/hosting/overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/apis/admin-api2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/apis/analytics-api2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/apis/auth2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/apis/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/apis/ingest-api2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/apis/openapi-sdk2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/apis/trigger-api2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/app-api-frameworks/express2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/app-api-frameworks/fastapi2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/app-api-frameworks/fastify2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/app-api-frameworks/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/app-api-frameworks/koa2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/app-api-frameworks/nextjs2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/app-api-frameworks/raw-nodejs2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/changelog2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/configuration2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/contribution/documentation2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/contribution/framework2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/data-modeling2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/data-sources2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/deploying/configuring-moose-for-cloud2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/deploying/deploying-on-an-offline-server2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/deploying/deploying-on-ecs2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/deploying/deploying-on-kubernetes2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/deploying/deploying-with-docker-compose2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/deploying/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/deploying/monitoring2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/deploying/packaging-moose-for-deployment2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/deploying/preparing-clickhouse-redpanda2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/getting-started/from-clickhouse2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/getting-started/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/getting-started/quickstart2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/help/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/help/minimum-requirements2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/help/troubleshooting2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/in-your-stack2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/local-dev-environment2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/metrics2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/migrate/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/migrate/lifecycle2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/migrate/migration-types2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/moose-cli2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/moosedev-mcp2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/apply-migrations2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/db-pull2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/external-tables2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/indexes2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/insert-data2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/model-materialized-view2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/model-table2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/model-view2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/planned-migrations2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/read-data2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/schema-change2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/schema-optimization2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/schema-versioning2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/supported-types2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/olap/ttl2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/quickstart2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/reference/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/streaming/connect-cdc2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/streaming/consumer-functions2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/streaming/create-stream2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/streaming/dead-letter-queues2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/streaming/from-your-code2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/streaming/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/streaming/schema-registry2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/streaming/sync-to-table2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/streaming/transform-functions2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/workflows/cancel-workflow2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/workflows/define-workflow2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/workflows/index2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/workflows/retries-and-timeouts2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/workflows/schedule-workflow2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/moosestack/workflows/trigger-workflow2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/applications/automated-reports2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/applications/automated-reports/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/applications/going-to-production2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/applications/going-to-production/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/applications/in-app-chat-analytics2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/applications/in-app-chat-analytics/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/applications/performant-dashboards2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/applications/performant-dashboards/existing-oltp-db2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/applications/performant-dashboards/existing-oltp-db/1-setup-connection2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/applications/performant-dashboards/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/applications/performant-dashboards/scratch/1-init2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-management/change-data-capture2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-management/change-data-capture/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-management/impact-analysis2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-management/impact-analysis/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-management/migrations2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-management/migrations/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-warehousing/connectors2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-warehousing/connectors/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-warehousing/customer-data-platform2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-warehousing/customer-data-platform/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-warehousing/operational-analytics2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-warehousing/operational-analytics/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-warehousing/pipelines2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-warehousing/pipelines/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-warehousing/startup-metrics2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/data-warehousing/startup-metrics/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/methodology/data-as-code2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/methodology/data-as-code/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/methodology/dora-for-data2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/methodology/dora-for-data/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/strategy/ai-enablement2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/strategy/ai-enablement/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/strategy/data-foundation2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/strategy/data-foundation/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/strategy/olap-evaluation2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/strategy/olap-evaluation/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/strategy/platform-engineering2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides/strategy/platform-engineering/guide-overview2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/guides2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com2025-11-21T01:39:42.117Zdaily0.7 -https://docs.fiveonefour.com/templates2025-11-21T01:39:42.117Zdaily0.7 +https://docs.fiveonefour.com/ai2025-11-21T02:43:16.692Zdaily0.7 +https://docs.fiveonefour.com/ai/data-collection-policy2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/demos/context2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/demos/dlqs2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/demos/egress2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/demos/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/demos/ingest2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/demos/model-data2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/demos/mvs2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/getting-started/claude2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/getting-started/cursor2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/getting-started/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/getting-started/other-clients2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/getting-started/vs-code2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/getting-started/windsurf2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/guides/clickhouse-chat2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/guides/clickhouse-proj2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/guides/from-template2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/guides/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/reference/cli-reference2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/reference/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/reference/mcp-json-reference2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/ai/reference/tool-reference2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/hosting2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/hosting/deployment2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/hosting/getting-started2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/hosting/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/hosting/overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/apis/admin-api2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/apis/analytics-api2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/apis/auth2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/apis/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/apis/ingest-api2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/apis/openapi-sdk2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/apis/trigger-api2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/app-api-frameworks/express2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/app-api-frameworks/fastapi2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/app-api-frameworks/fastify2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/app-api-frameworks/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/app-api-frameworks/koa2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/app-api-frameworks/nextjs2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/app-api-frameworks/raw-nodejs2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/changelog2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/configuration2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/contribution/documentation2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/contribution/framework2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/data-modeling2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/data-sources2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/deploying/configuring-moose-for-cloud2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/deploying/deploying-on-an-offline-server2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/deploying/deploying-on-ecs2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/deploying/deploying-on-kubernetes2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/deploying/deploying-with-docker-compose2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/deploying/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/deploying/monitoring2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/deploying/packaging-moose-for-deployment2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/deploying/preparing-clickhouse-redpanda2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/getting-started/from-clickhouse2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/getting-started/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/getting-started/quickstart2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/help/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/help/minimum-requirements2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/help/troubleshooting2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/in-your-stack2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/local-dev-environment2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/metrics2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/migrate/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/migrate/lifecycle2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/migrate/migration-types2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/moose-cli2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/moosedev-mcp2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/apply-migrations2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/db-pull2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/external-tables2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/indexes2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/insert-data2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/model-materialized-view2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/model-table2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/model-view2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/planned-migrations2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/read-data2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/schema-change2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/schema-optimization2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/schema-versioning2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/supported-types2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/olap/ttl2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/quickstart2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/reference/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/streaming/connect-cdc2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/streaming/consumer-functions2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/streaming/create-stream2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/streaming/dead-letter-queues2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/streaming/from-your-code2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/streaming/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/streaming/schema-registry2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/streaming/sync-to-table2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/streaming/transform-functions2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/workflows/cancel-workflow2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/workflows/define-workflow2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/workflows/index2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/workflows/retries-and-timeouts2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/workflows/schedule-workflow2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/moosestack/workflows/trigger-workflow2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/applications/automated-reports2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/applications/automated-reports/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/applications/going-to-production2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/applications/going-to-production/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/applications/in-app-chat-analytics2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/applications/in-app-chat-analytics/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/applications/performant-dashboards2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/applications/performant-dashboards/existing-oltp-db2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/applications/performant-dashboards/existing-oltp-db/1-setup-connection2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/applications/performant-dashboards/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/applications/performant-dashboards/scratch/1-init2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-management/change-data-capture2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-management/change-data-capture/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-management/impact-analysis2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-management/impact-analysis/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-management/migrations2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-management/migrations/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-warehousing/connectors2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-warehousing/connectors/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-warehousing/customer-data-platform2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-warehousing/customer-data-platform/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-warehousing/operational-analytics2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-warehousing/operational-analytics/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-warehousing/pipelines2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-warehousing/pipelines/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-warehousing/startup-metrics2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/data-warehousing/startup-metrics/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/methodology/data-as-code2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/methodology/data-as-code/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/methodology/dora-for-data2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/methodology/dora-for-data/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/strategy/ai-enablement2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/strategy/ai-enablement/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/strategy/data-foundation2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/strategy/data-foundation/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/strategy/olap-evaluation2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/strategy/olap-evaluation/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/strategy/platform-engineering2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides/strategy/platform-engineering/guide-overview2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/guides2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com2025-11-21T02:43:16.693Zdaily0.7 +https://docs.fiveonefour.com/templates2025-11-21T02:43:16.693Zdaily0.7 \ No newline at end of file diff --git a/apps/framework-docs-v2/src/components/navigation/side-nav.tsx b/apps/framework-docs-v2/src/components/navigation/side-nav.tsx index 7df49d05a1..11ce273d6c 100644 --- a/apps/framework-docs-v2/src/components/navigation/side-nav.tsx +++ b/apps/framework-docs-v2/src/components/navigation/side-nav.tsx @@ -17,6 +17,7 @@ import { SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, + SidebarMenuSubLabel, } from "@/components/ui/sidebar"; import { Collapsible, @@ -327,18 +328,30 @@ function renderNavChildren( language: string, ): React.ReactNode[] { const elements: React.ReactNode[] = []; + let isFirstLabel = true; - children.forEach((child) => { - if (child.type !== "page") return; - elements.push( - , - ); + children.forEach((child, index) => { + if (child.type === "label") { + elements.push( + + {child.title} + , + ); + isFirstLabel = false; + } else if (child.type === "separator") { + // Separators are handled via label spacing - skip rendering them + return; + } else if (child.type === "page") { + elements.push( + , + ); + } }); return elements; diff --git a/apps/framework-docs-v2/src/components/ui/sidebar.tsx b/apps/framework-docs-v2/src/components/ui/sidebar.tsx index 23f6ea7503..0e424297cd 100644 --- a/apps/framework-docs-v2/src/components/ui/sidebar.tsx +++ b/apps/framework-docs-v2/src/components/ui/sidebar.tsx @@ -753,6 +753,24 @@ const SidebarMenuSubButton = React.forwardRef< }); SidebarMenuSubButton.displayName = "SidebarMenuSubButton"; +const SidebarMenuSubLabel = React.forwardRef< + HTMLLIElement, + React.ComponentProps<"li"> & { isFirst?: boolean } +>(({ className, isFirst = false, ...props }, ref) => ( +
  • +)); +SidebarMenuSubLabel.displayName = "SidebarMenuSubLabel"; + export { Sidebar, SidebarContent, @@ -773,6 +791,7 @@ export { SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, + SidebarMenuSubLabel, SidebarProvider, SidebarRail, SidebarSeparator, From 1439d75b966a26fad4b09251e37b65f81c6c7b0e Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 25 Nov 2025 17:23:48 -0800 Subject: [PATCH 16/21] update to codeblocks and attributes --- .../src/components/mdx-renderer.tsx | 48 +-- .../src/components/mdx/index.ts | 3 + .../src/components/mdx/server-code-block.tsx | 297 ++++++++++++++++++ .../src/components/mdx/server-figure.tsx | 119 +++++++ .../src/components/mdx/shell-snippet.tsx | 36 +++ .../src/lib/rehype-code-meta.ts | 141 +++++++++ 6 files changed, 607 insertions(+), 37 deletions(-) create mode 100644 apps/framework-docs-v2/src/components/mdx/server-code-block.tsx create mode 100644 apps/framework-docs-v2/src/components/mdx/server-figure.tsx create mode 100644 apps/framework-docs-v2/src/components/mdx/shell-snippet.tsx create mode 100644 apps/framework-docs-v2/src/lib/rehype-code-meta.ts diff --git a/apps/framework-docs-v2/src/components/mdx-renderer.tsx b/apps/framework-docs-v2/src/components/mdx-renderer.tsx index b560603590..9e7e80d5b6 100644 --- a/apps/framework-docs-v2/src/components/mdx-renderer.tsx +++ b/apps/framework-docs-v2/src/components/mdx-renderer.tsx @@ -36,16 +36,16 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; import { IconTerminal, IconFileCode } from "@tabler/icons-react"; import { - MDXPre, - MDXCode, - MDXFigure, -} from "@/components/mdx/code-block-wrapper"; + ServerCodeBlock, + ServerInlineCode, +} from "@/components/mdx/server-code-block"; +import { ServerFigure } from "@/components/mdx/server-figure"; import Link from "next/link"; import remarkGfm from "remark-gfm"; import rehypeSlug from "rehype-slug"; import rehypeAutolinkHeadings from "rehype-autolink-headings"; import rehypePrettyCode from "rehype-pretty-code"; -import { visit } from "unist-util-visit"; +import { rehypeCodeMeta } from "@/lib/rehype-code-meta"; interface MDXRendererProps { source: string; @@ -125,10 +125,10 @@ export async function MDXRenderer({ source }: MDXRendererProps) { SourceCodeLink, Link, - figure: MDXFigure, - // wrap with not-prose class - pre: MDXPre, - code: MDXCode, + // Code block handling - server-side rendered + figure: ServerFigure, + pre: ServerCodeBlock, + code: ServerInlineCode, }; return ( @@ -148,34 +148,8 @@ export async function MDXRenderer({ source }: MDXRendererProps) { keepBackground: false, }, ], - // Custom plugin to extract copy attribute from metadata and set it on pre element - () => { - return (tree: any) => { - visit(tree, "element", (node: any) => { - // Check pre elements and their code children for metadata - if (node.tagName === "pre" && node.children) { - for (const child of node.children) { - if (child.tagName === "code" && child.data?.meta) { - const codeMetaString = child.data.meta as string; - // Parse copy attribute: copy="false", copy="true", or just "copy" (which means true) - const copyFalseMatch = - codeMetaString.match(/copy=["']?false["']?/); - - if (copyFalseMatch) { - // copy="false" or copy=false - hide copy button - if (!node.properties) { - node.properties = {}; - } - node.properties["data-copy"] = "false"; - } - // If copy="true" or just "copy", don't set anything (default behavior shows copy button) - break; // Only process first code child - } - } - } - }); - }; - }, + // Generic plugin to extract all meta attributes as data-* props + rehypeCodeMeta, ], }, }} diff --git a/apps/framework-docs-v2/src/components/mdx/index.ts b/apps/framework-docs-v2/src/components/mdx/index.ts index e34439c4a0..5320abeeac 100644 --- a/apps/framework-docs-v2/src/components/mdx/index.ts +++ b/apps/framework-docs-v2/src/components/mdx/index.ts @@ -11,6 +11,9 @@ export { LanguageTabs, LanguageTabContent } from "./language-tabs"; export { CommandSnippet } from "./command-snippet"; export { CodeSnippet } from "./code-snippet"; export { CodeEditorWrapper } from "./code-editor-wrapper"; +export { ShellSnippet } from "./shell-snippet"; +export { ServerCodeBlock, ServerInlineCode } from "./server-code-block"; +export { ServerFigure } from "./server-figure"; export { ToggleBlock } from "./toggle-block"; export { BulletPointsCard, diff --git a/apps/framework-docs-v2/src/components/mdx/server-code-block.tsx b/apps/framework-docs-v2/src/components/mdx/server-code-block.tsx new file mode 100644 index 0000000000..c5f5f9b7f6 --- /dev/null +++ b/apps/framework-docs-v2/src/components/mdx/server-code-block.tsx @@ -0,0 +1,297 @@ +import React from "react"; +import { cn } from "@/lib/utils"; +import { CodeSnippet } from "./code-snippet"; +import { CodeEditorWrapper } from "./code-editor-wrapper"; +import { ShellSnippet } from "./shell-snippet"; +import { extractTextContent } from "@/lib/extract-text-content"; + +// Shell languages that should use terminal styling +const SHELL_LANGUAGES = new Set([ + "bash", + "sh", + "shell", + "zsh", + "fish", + "powershell", + "cmd", +]); + +// Config/data file languages that should always use static CodeSnippet +const CONFIG_LANGUAGES = new Set([ + "toml", + "yaml", + "yml", + "json", + "jsonc", + "ini", + "properties", + "config", +]); + +/** + * Props interface for server-side code block + * All data-* attributes from markdown are available here + */ +export interface ServerCodeBlockProps + extends React.HTMLAttributes { + // Standard rehype-pretty-code attributes + "data-language"?: string; + "data-theme"?: string; + "data-rehype-pretty-code-fragment"?: string; + "data-rehype-pretty-code-title"?: string; + + // Custom attributes from markdown meta + "data-filename"?: string; + "data-copy"?: string; + "data-variant"?: string; + "data-duration"?: string; + "data-delay"?: string; + "data-writing"?: string; + "data-linenumbers"?: string; + + children?: React.ReactNode; +} + +/** + * Extracts the language from data attributes or className + */ +function getLanguage(props: ServerCodeBlockProps): string { + const dataLang = props["data-language"]; + if (dataLang) { + return dataLang.toLowerCase(); + } + + if (typeof props.className === "string") { + const match = props.className.match(/language-(\w+)/); + if (match?.[1]) { + return match[1].toLowerCase(); + } + } + + return ""; +} + +/** + * Find the code element in children + */ +function findCodeElement( + node: React.ReactNode, + depth = 0, +): React.ReactElement | undefined { + if (depth > 10) return undefined; + + if (Array.isArray(node)) { + for (const item of node) { + const found = findCodeElement(item, depth + 1); + if (found) return found; + } + return undefined; + } + + if (!React.isValidElement(node)) return undefined; + + const nodeType = node.type; + const nodeProps = (node.props as Record) || {}; + + if (nodeType === React.Fragment && nodeProps.children) { + return findCodeElement(nodeProps.children as React.ReactNode, depth + 1); + } + + if (typeof nodeType === "string" && nodeType === "code") { + return node; + } + + if (nodeProps.children) { + return findCodeElement(nodeProps.children as React.ReactNode, depth + 1); + } + + return undefined; +} + +/** + * Server-side code block component + * + * Extracts all code block attributes and routes to the appropriate + * client-side component based on language and attributes. + */ +export function ServerCodeBlock({ + children, + ...props +}: ServerCodeBlockProps): React.ReactElement { + // Check if this is a code block processed by rehype-pretty-code + const isCodeBlock = props["data-rehype-pretty-code-fragment"] !== undefined; + + if (!isCodeBlock) { + // Not a code block, render as regular pre element + const { className, ...restProps } = props; + return ( +
    +        {children}
    +      
    + ); + } + + // Extract code content + const codeElement = findCodeElement(children); + const codeText = + codeElement ? + extractTextContent( + (codeElement.props as Record) + .children as React.ReactNode, + ).trim() + : extractTextContent(children).trim(); + + // Extract all attributes (supports multiple sources for backwards compat) + const language = getLanguage(props); + + // Filename: check title (from rehype-pretty-code), filename, or direct title + const filename = + props["data-rehype-pretty-code-title"] || + props["data-filename"] || + ((props as Record)["title"] as string | undefined); + + // Copy button: defaults to true unless explicitly set to "false" + const showCopy = props["data-copy"] !== "false"; + + // Variant: "terminal" or "ide" + const variant = props["data-variant"] as "terminal" | "ide" | undefined; + + // Animation settings + const duration = + props["data-duration"] ? parseFloat(props["data-duration"]) : undefined; + const delay = + props["data-delay"] ? parseFloat(props["data-delay"]) : undefined; + const writing = props["data-writing"] !== "false"; + + // Line numbers: defaults to true unless explicitly set to "false" + const lineNumbers = props["data-linenumbers"] !== "false"; + + // Determine component type based on language and attributes + const isShell = SHELL_LANGUAGES.has(language); + const isConfigFile = CONFIG_LANGUAGES.has(language); + + // Routing logic: + // 1. Config files → Always static CodeSnippet (never animated) + // 2. Shell + filename + copy=false → Animated CodeEditorWrapper (terminal style) + // 3. Shell (all other cases) → ShellSnippet (copyable Terminal tab UI) + // 4. Non-shell + filename + no copy attr → Animated CodeEditorWrapper + // 5. Default → Static CodeSnippet + + // Config files always use static CodeSnippet (never animated) + if (isConfigFile) { + return ( +
    + +
    + ); + } + + // Shell commands: Use animated terminal only when explicitly copy=false with filename + // Otherwise, always use ShellSnippet (the Terminal tab UI with copy button) + if (isShell) { + // Only use animated terminal when explicitly no copy button wanted + if (filename && props["data-copy"] === "false") { + return ( +
    + +
    + ); + } + + // All other shell commands use ShellSnippet (Terminal tab with copy) + return ( +
    + +
    + ); + } + + // Non-shell: animate if filename present and copy not explicitly set + const shouldAnimate = filename && props["data-copy"] === undefined; + + if (shouldAnimate) { + return ( +
    + +
    + ); + } + + // Default: static CodeSnippet + return ( +
    + +
    + ); +} + +/** + * Server-side inline code component + */ +export function ServerInlineCode({ + children, + className, + ...props +}: React.HTMLAttributes): React.ReactElement { + const isCodeBlock = + className?.includes("language-") || + (props as Record)["data-language"]; + + if (isCodeBlock) { + // This is a code block that should be handled by ServerCodeBlock + // This is a fallback for when code is not wrapped in pre + const language = getLanguage(props as ServerCodeBlockProps); + const codeText = extractTextContent(children).trim(); + + return ( +
    + +
    + ); + } + + // Inline code - simple styled element + return ( + + {children} + + ); +} diff --git a/apps/framework-docs-v2/src/components/mdx/server-figure.tsx b/apps/framework-docs-v2/src/components/mdx/server-figure.tsx new file mode 100644 index 0000000000..b9d7a08744 --- /dev/null +++ b/apps/framework-docs-v2/src/components/mdx/server-figure.tsx @@ -0,0 +1,119 @@ +import React from "react"; + +interface MDXFigureProps extends React.HTMLAttributes { + "data-rehype-pretty-code-figure"?: string; + children?: React.ReactNode; +} + +/** + * Extracts text content from a React node (for figcaption titles) + */ +function extractTextFromNode(node: React.ReactNode): string { + if (typeof node === "string") { + return node; + } + if (typeof node === "number") { + return String(node); + } + if (Array.isArray(node)) { + return node.map(extractTextFromNode).join(""); + } + if (React.isValidElement(node)) { + const props = node.props as Record; + return extractTextFromNode(props.children as React.ReactNode); + } + return ""; +} + +/** + * Server-side component that handles figure wrapper from rehype-pretty-code + * Extracts the title from figcaption and passes it to the pre element + */ +export function ServerFigure({ + children, + ...props +}: MDXFigureProps): React.ReactElement { + // Only handle code block figures + // data-rehype-pretty-code-figure is present (even if empty string) for code blocks + if (props["data-rehype-pretty-code-figure"] === undefined) { + return
    {children}
    ; + } + + // For code blocks, extract figcaption title and pass to pre + const childrenArray = React.Children.toArray(children); + + // Find figcaption and pre elements + let figcaption: React.ReactElement | null = null; + let preElement: React.ReactElement | null = null; + + childrenArray.forEach((child) => { + if (React.isValidElement(child)) { + const childType = child.type; + const childProps = (child.props as Record) || {}; + + // Check if it's a native HTML element by checking if type is a string + if (typeof childType === "string") { + if (childType === "figcaption") { + figcaption = child; + } else if (childType === "pre") { + preElement = child; + } + } else { + // For React components (like ServerCodeBlock) + // Check if it has code block attributes + const hasCodeBlockAttrs = + childProps["data-rehype-pretty-code-fragment"] !== undefined || + childProps["data-language"] !== undefined || + childProps["data-theme"] !== undefined; + + // If it has code block attributes, it's the pre element + if (hasCodeBlockAttrs || !preElement) { + preElement = child; + } + } + } + }); + + // Extract filename from figcaption (title from markdown) + const figcaptionTitle = + figcaption ? + extractTextFromNode( + (figcaption.props as Record) + .children as React.ReactNode, + ).trim() + : undefined; + + const preProps = + preElement ? (preElement.props as Record) || {} : {}; + + // Prioritize figcaption title (from markdown title="...") over any existing attributes + const filename = + figcaptionTitle || + (preProps["data-rehype-pretty-code-title"] as string | undefined) || + (preProps["data-filename"] as string | undefined); + + // If we have a pre element, ensure the filename is set on both attributes + if (preElement) { + const hasCodeBlockAttrs = + preProps["data-language"] !== undefined || + preProps["data-theme"] !== undefined; + const fragmentValue = + preProps["data-rehype-pretty-code-fragment"] !== undefined ? + preProps["data-rehype-pretty-code-fragment"] + : hasCodeBlockAttrs ? "" + : undefined; + + const updatedPre = React.cloneElement(preElement, { + ...preProps, + "data-filename": filename || undefined, + "data-rehype-pretty-code-title": filename || undefined, + ...(fragmentValue !== undefined ? + { "data-rehype-pretty-code-fragment": fragmentValue } + : {}), + }); + return <>{updatedPre}; + } + + // Fallback: render children + return <>{children}; +} diff --git a/apps/framework-docs-v2/src/components/mdx/shell-snippet.tsx b/apps/framework-docs-v2/src/components/mdx/shell-snippet.tsx new file mode 100644 index 0000000000..1ad348c608 --- /dev/null +++ b/apps/framework-docs-v2/src/components/mdx/shell-snippet.tsx @@ -0,0 +1,36 @@ +"use client"; + +import React from "react"; +import { + Snippet, + SnippetCopyButton, + SnippetHeader, + SnippetTabsContent, + SnippetTabsList, + SnippetTabsTrigger, +} from "@/components/ui/snippet"; + +interface ShellSnippetProps { + code: string; + language: string; +} + +/** + * Client component for shell/terminal code snippets + * Displays with "Terminal" label and copy button + */ +export function ShellSnippet({ code, language }: ShellSnippetProps) { + const [value, setValue] = React.useState("terminal"); + + return ( + + + + Terminal + + + + {code} + + ); +} diff --git a/apps/framework-docs-v2/src/lib/rehype-code-meta.ts b/apps/framework-docs-v2/src/lib/rehype-code-meta.ts new file mode 100644 index 0000000000..ed64837837 --- /dev/null +++ b/apps/framework-docs-v2/src/lib/rehype-code-meta.ts @@ -0,0 +1,141 @@ +import { visit } from "unist-util-visit"; + +/** + * Generic rehype plugin that extracts all code block meta attributes + * and sets them as data-* attributes on the pre element. + * + * Supports: + * - key="value" or key='value' (quoted values) + * - key=value (unquoted values) + * - key (flag-style, sets data-key="true") + * + * Examples: + * ```ts filename="example.ts" copy + * ```bash variant="terminal" duration=3 delay=0.5 + * ```python copy=false lineNumbers + */ + +interface HastElement { + type: "element"; + tagName: string; + properties?: Record; + children?: HastNode[]; + data?: Record; +} + +interface HastText { + type: "text"; + value: string; +} + +type HastNode = HastElement | HastText | { type: string }; + +interface HastRoot { + type: "root"; + children: HastNode[]; +} + +export function rehypeCodeMeta() { + return (tree: HastRoot) => { + visit(tree, "element", (node: HastElement) => { + // Only process pre elements with code children + if (node.tagName !== "pre" || !node.children) { + return; + } + + for (const child of node.children) { + if ( + child.type === "element" && + (child as HastElement).tagName === "code" && + (child as HastElement).data?.meta + ) { + const meta = (child as HastElement).data?.meta as string; + const attributes = parseMetaString(meta); + + // Ensure properties object exists + if (!node.properties) { + node.properties = {}; + } + + // Set each parsed attribute as a data-* attribute + for (const [key, value] of Object.entries(attributes)) { + // Use lowercase keys with data- prefix + const dataKey = `data-${key.toLowerCase()}`; + node.properties[dataKey] = value; + } + + // Only process the first code child + break; + } + } + }); + }; +} + +/** + * Parses a code block meta string into key-value pairs + * + * Handles: + * - key="value" or key='value' + * - key=value (no quotes) + * - key (flag, becomes "true") + */ +function parseMetaString(meta: string): Record { + const attributes: Record = {}; + + if (!meta || typeof meta !== "string") { + return attributes; + } + + // Regex patterns for different attribute formats + // Pattern 1: key="value" or key='value' (quoted) + const quotedPattern = /(\w+)=["']([^"']*)["']/g; + // Pattern 2: key=value (unquoted, stops at whitespace) + const unquotedPattern = /(\w+)=([^\s"']+)/g; + // Pattern 3: standalone key (flag-style) + const flagPattern = /(?:^|\s)(\w+)(?=\s|$)/g; + + // Track which parts of the string we've processed + let processed = meta; + + // First, extract quoted values + let match: RegExpExecArray | null = quotedPattern.exec(meta); + while (match !== null) { + const key = match[1]; + const value = match[2]; + if (key) { + attributes[key] = value ?? ""; + // Mark as processed by replacing with spaces + processed = processed.replace(match[0], " ".repeat(match[0].length)); + } + match = quotedPattern.exec(meta); + } + + // Then, extract unquoted values from remaining string + match = unquotedPattern.exec(processed); + while (match !== null) { + const key = match[1]; + const value = match[2]; + if (key && !attributes[key]) { + attributes[key] = value ?? ""; + } + match = unquotedPattern.exec(processed); + } + + // Finally, extract flags from remaining string + // Reset processed to only include non-key=value parts + const remainingParts = processed.split(/\w+=\S+/).join(" "); + match = flagPattern.exec(remainingParts); + while (match !== null) { + const key = match[1]; + // Only add if not already set + if (key && !attributes[key]) { + attributes[key] = "true"; + } + match = flagPattern.exec(remainingParts); + } + + return attributes; +} + +export default rehypeCodeMeta; From a0d6c2eedc348a71339ebab5b5bddaf434dc2feb Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 25 Nov 2025 17:31:25 -0800 Subject: [PATCH 17/21] fix build --- .../src/components/mdx/server-figure.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/framework-docs-v2/src/components/mdx/server-figure.tsx b/apps/framework-docs-v2/src/components/mdx/server-figure.tsx index b9d7a08744..75b1a7621a 100644 --- a/apps/framework-docs-v2/src/components/mdx/server-figure.tsx +++ b/apps/framework-docs-v2/src/components/mdx/server-figure.tsx @@ -75,13 +75,13 @@ export function ServerFigure({ }); // Extract filename from figcaption (title from markdown) - const figcaptionTitle = - figcaption ? - extractTextFromNode( - (figcaption.props as Record) - .children as React.ReactNode, - ).trim() - : undefined; + let figcaptionTitle: string | undefined; + if (figcaption !== null) { + const figcaptionProps = figcaption.props as Record; + figcaptionTitle = extractTextFromNode( + figcaptionProps.children as React.ReactNode, + ).trim(); + } const preProps = preElement ? (preElement.props as Record) || {} : {}; From 781fe2b614e1f1ebca29d67b4d2dd0673df23e63 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 26 Nov 2025 20:25:30 -0800 Subject: [PATCH 18/21] added the filetree component --- .../content/moosestack/configuration.mdx | 19 +-- .../src/components/mdx/file-tree.tsx | 123 +++++++++++++++--- 2 files changed, 117 insertions(+), 25 deletions(-) diff --git a/apps/framework-docs-v2/content/moosestack/configuration.mdx b/apps/framework-docs-v2/content/moosestack/configuration.mdx index 28702959eb..9b8a03d18b 100644 --- a/apps/framework-docs-v2/content/moosestack/configuration.mdx +++ b/apps/framework-docs-v2/content/moosestack/configuration.mdx @@ -4,7 +4,7 @@ description: Configure your MooseStack project order: 1 --- -import { Callout } from "@/components/mdx"; +import { Callout, FileTree } from "@/components/mdx"; # Project Configuration @@ -117,14 +117,15 @@ MOOSE_
    __=value ### Complete Example **File structure:** -``` -my-moose-project/ -├── .env # Base config -├── .env.dev # Dev overrides -├── .env.prod # Prod overrides -├── .env.local # Local secrets (gitignored) -└── moose.config.toml # Structured config -``` + + + + + + + + + **.env** (committed): ```bash diff --git a/apps/framework-docs-v2/src/components/mdx/file-tree.tsx b/apps/framework-docs-v2/src/components/mdx/file-tree.tsx index 52343f95cd..36d9602df2 100644 --- a/apps/framework-docs-v2/src/components/mdx/file-tree.tsx +++ b/apps/framework-docs-v2/src/components/mdx/file-tree.tsx @@ -1,37 +1,128 @@ "use client"; -import React from "react"; +import * as React from "react"; +import { IconChevronRight, IconFile, IconFolder } from "@tabler/icons-react"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { cn } from "@/lib/utils"; + +// ============================================================================ +// FileTree Root +// ============================================================================ interface FileTreeProps { children: React.ReactNode; + className?: string; } +/** + * FileTree component for MDX documentation + * + * Usage in MDX: + * ```mdx + * + * + * + * + * + * + * + * + * ``` + */ +export function FileTree({ children, className }: FileTreeProps) { + return ( +
    +
      {children}
    +
    + ); +} + +// ============================================================================ +// FileTreeFolder +// ============================================================================ + interface FileTreeFolderProps { name: string; children?: React.ReactNode; + defaultOpen?: boolean; } -interface FileTreeFileProps { - name: string; +export function FileTreeFolder({ + name, + children, + defaultOpen = true, +}: FileTreeFolderProps) { + return ( +
  • + + + + + +
      + {children} +
    +
    +
    +
  • + ); } -export function FileTree({ children }: FileTreeProps) { - return
    {children}
    ; +// ============================================================================ +// FileTreeFile +// ============================================================================ + +interface FileTreeFileProps { + name: string; } -export function FileTreeFolder({ name, children }: FileTreeFolderProps) { +export function FileTreeFile({ name }: FileTreeFileProps) { return ( -
    -
    {name}/
    -
    {children}
    -
    +
  • +
    svg]:size-4 [&>svg]:shrink-0", + )} + > + + {name} +
    +
  • ); } -export function FileTreeFile({ name }: FileTreeFileProps) { - return
    {name}
    ; -} +// ============================================================================ +// Attach sub-components for dot notation +// ============================================================================ -// Attach sub-components to FileTree for nested usage -(FileTree as any).Folder = FileTreeFolder; -(FileTree as any).File = FileTreeFile; +FileTree.Folder = FileTreeFolder; +FileTree.File = FileTreeFile; From b95377700dc205930a9720f99a2b836b9ef3c815 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 28 Nov 2025 11:43:49 -0800 Subject: [PATCH 19/21] fix some biome errors and styles --- .../src/components/mdx/code-snippet.tsx | 324 +++++++++++++++++- .../src/components/mdx/inline-code.tsx | 87 +++++ .../src/components/mdx/server-code-block.tsx | 172 +++++++++- .../src/lib/rehype-code-meta.ts | 178 +++++++--- 4 files changed, 696 insertions(+), 65 deletions(-) create mode 100644 apps/framework-docs-v2/src/components/mdx/inline-code.tsx diff --git a/apps/framework-docs-v2/src/components/mdx/code-snippet.tsx b/apps/framework-docs-v2/src/components/mdx/code-snippet.tsx index 3316156f11..538a48d884 100644 --- a/apps/framework-docs-v2/src/components/mdx/code-snippet.tsx +++ b/apps/framework-docs-v2/src/components/mdx/code-snippet.tsx @@ -11,12 +11,23 @@ import { CodeBlockContent, } from "@/components/ui/shadcn-io/code-block"; +/** + * Parsed substring highlight with optional occurrence filter + */ +interface SubstringHighlight { + pattern: string; + occurrences?: number[]; +} + interface CodeSnippetProps { code: string; language?: string; filename?: string; copyButton?: boolean; lineNumbers?: boolean; + highlightLines?: number[]; + highlightStrings?: SubstringHighlight[]; + isAnsi?: boolean; className?: string; } @@ -59,14 +70,299 @@ function CopyButton({ ); } +/** + * Parse ANSI escape codes and convert to styled HTML + */ +function parseAnsi(text: string): string { + const colors: Record = { + 30: "color: #000", + 31: "color: #c00", + 32: "color: #0a0", + 33: "color: #a50", + 34: "color: #00a", + 35: "color: #a0a", + 36: "color: #0aa", + 37: "color: #aaa", + 90: "color: #555", + 91: "color: #f55", + 92: "color: #5f5", + 93: "color: #ff5", + 94: "color: #55f", + 95: "color: #f5f", + 96: "color: #5ff", + 97: "color: #fff", + }; + + const bgColors: Record = { + 40: "background-color: #000", + 41: "background-color: #c00", + 42: "background-color: #0a0", + 43: "background-color: #a50", + 44: "background-color: #00a", + 45: "background-color: #a0a", + 46: "background-color: #0aa", + 47: "background-color: #aaa", + 100: "background-color: #555", + 101: "background-color: #f55", + 102: "background-color: #5f5", + 103: "background-color: #ff5", + 104: "background-color: #55f", + 105: "background-color: #f5f", + 106: "background-color: #5ff", + 107: "background-color: #fff", + }; + + // biome-ignore lint/complexity/useRegexLiterals: Using constructor to avoid control character lint error + const ansiPattern = new RegExp("\\x1b\\[([0-9;]*)m", "g"); + let result = ""; + let lastIndex = 0; + let currentStyles: string[] = []; + + let match = ansiPattern.exec(text); + while (match !== null) { + const textBefore = text.slice(lastIndex, match.index); + if (textBefore) { + const escapedText = textBefore + .replace(/&/g, "&") + .replace(//g, ">"); + + if (currentStyles.length > 0) { + result += `${escapedText}`; + } else { + result += escapedText; + } + } + + const codes = match[1] ? match[1].split(";").map(Number) : [0]; + + for (const code of codes) { + if (code === 0) { + currentStyles = []; + } else if (code === 1) { + currentStyles.push("font-weight: bold"); + } else if (code === 2) { + currentStyles.push("opacity: 0.75"); + } else if (code === 3) { + currentStyles.push("font-style: italic"); + } else if (code === 4) { + currentStyles.push("text-decoration: underline"); + } else if (code === 9) { + currentStyles.push("text-decoration: line-through"); + } else if (colors[code]) { + currentStyles.push(colors[code]); + } else if (bgColors[code]) { + currentStyles.push(bgColors[code]); + } + } + + lastIndex = ansiPattern.lastIndex; + match = ansiPattern.exec(text); + } + + const remainingText = text.slice(lastIndex); + if (remainingText) { + const escapedText = remainingText + .replace(/&/g, "&") + .replace(//g, ">"); + + if (currentStyles.length > 0) { + result += `${escapedText}`; + } else { + result += escapedText; + } + } + + return result; +} + +/** + * Custom CodeBlockContent that supports line and substring highlighting + */ +function HighlightedCodeBlockContent({ + code, + language, + highlightLines, + highlightStrings, +}: { + code: string; + language: string; + highlightLines: number[]; + highlightStrings: SubstringHighlight[]; +}) { + const [highlightedCode, setHighlightedCode] = React.useState(""); + const [isLoading, setIsLoading] = React.useState(true); + + React.useEffect(() => { + const loadHighlightedCode = async () => { + try { + const { codeToHtml } = await import("shiki"); + + const languageMap: Record = { + gitignore: "text", + env: "text", + dotenv: "text", + }; + const mappedLanguage = languageMap[language.toLowerCase()] || language; + + const html = await codeToHtml(code, { + lang: mappedLanguage, + themes: { + light: "vitesse-light", + dark: "vitesse-dark", + }, + transformers: [ + { + line(node, line) { + // Add highlighted class to specified lines + if (highlightLines.includes(line)) { + this.addClassToHast(node, "highlighted"); + } + }, + }, + ], + }); + + // Apply substring highlighting if needed + let finalHtml = html; + if (highlightStrings.length > 0) { + finalHtml = applySubstringHighlighting(html, highlightStrings); + } + + setHighlightedCode(finalHtml); + setIsLoading(false); + } catch { + // Fallback + try { + const { codeToHtml } = await import("shiki"); + const html = await codeToHtml(code, { + lang: "text", + themes: { + light: "vitesse-light", + dark: "vitesse-dark", + }, + }); + setHighlightedCode(html); + } catch { + const lines = code.split("\n"); + const html = `
    ${lines.map((line) => `${line.replace(//g, ">")}`).join("\n")}
    `; + setHighlightedCode(html); + } + setIsLoading(false); + } + }; + + loadHighlightedCode(); + }, [code, language, highlightLines, highlightStrings]); + + if (isLoading) { + return ( +
    +        
    +          {code.split("\n").map((line, i) => (
    +            // biome-ignore lint/suspicious/noArrayIndexKey: Static code lines have no unique ID
    +            
    +              {line}
    +            
    +          ))}
    +        
    +      
    + ); + } + + return ( +
    + ); +} + +function escapeRegExp(string: string): string { + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function applySubstringHighlighting( + html: string, + highlightStrings: SubstringHighlight[], +): string { + let result = html; + + for (const { pattern, occurrences } of highlightStrings) { + const escapedPattern = escapeRegExp(pattern); + let occurrenceCount = 0; + + // Replace pattern occurrences, respecting occurrence filter + result = result.replace( + new RegExp(`(?<=>)([^<]*?)${escapedPattern}`, "g"), + (match, prefix) => { + occurrenceCount++; + const shouldHighlight = + !occurrences || occurrences.includes(occurrenceCount); + + if (shouldHighlight) { + return `>${prefix}${pattern}`; + } + return match; + }, + ); + } + + return result; +} + export function CodeSnippet({ code, language = "typescript", filename, copyButton = true, lineNumbers = true, + highlightLines = [], + highlightStrings = [], + isAnsi = false, className, }: CodeSnippetProps) { + // For ANSI blocks, render with ANSI parsing + if (isAnsi) { + const lines = code.split("\n"); + return ( +
    + {copyButton && } + {filename && ( +
    + {filename} +
    + )} +
    +
    +            
    +              {lines.map((line, i) => (
    +                // biome-ignore lint/suspicious/noArrayIndexKey: Static code lines have no unique ID
    +                
    +                  
    +                
    +              ))}
    +            
    +          
    +
    +
    + ); + } + + // Check if we need custom highlighting + const needsCustomHighlighting = + highlightLines.length > 0 || highlightStrings.length > 0; + return (
    - - {item.code} - + {needsCustomHighlighting ? + + : + {item.code} + + } )} diff --git a/apps/framework-docs-v2/src/components/mdx/inline-code.tsx b/apps/framework-docs-v2/src/components/mdx/inline-code.tsx new file mode 100644 index 0000000000..374c8a4f49 --- /dev/null +++ b/apps/framework-docs-v2/src/components/mdx/inline-code.tsx @@ -0,0 +1,87 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; + +interface InlineCodeProps { + code: string; + language: string; + className?: string; +} + +const darkModeStyles = cn( + "dark:[&_.shiki]:!text-[var(--shiki-dark)]", + "dark:[&_.shiki_span]:!text-[var(--shiki-dark)]", +); + +/** + * Inline code with syntax highlighting + * Used for the Nextra-style `code{:lang}` syntax + */ +export function InlineCode({ code, language, className }: InlineCodeProps) { + const [highlightedCode, setHighlightedCode] = React.useState(""); + const [isLoading, setIsLoading] = React.useState(true); + + React.useEffect(() => { + const loadHighlightedCode = async () => { + try { + const { codeToHtml } = await import("shiki"); + + const html = await codeToHtml(code, { + lang: language, + themes: { + light: "vitesse-light", + dark: "vitesse-dark", + }, + }); + + // Extract just the code content, removing the pre/code wrapper + // The output is usually:
    ...
    + const match = html.match(/]*>([\s\S]*)<\/code>/); + if (match?.[1]) { + // Remove the line span wrapper for inline display + const content = match[1].replace( + /([\s\S]*?)<\/span>/g, + "$1", + ); + setHighlightedCode(content); + } else { + setHighlightedCode(code); + } + setIsLoading(false); + } catch { + // Fallback to plain text + setHighlightedCode(code); + setIsLoading(false); + } + }; + + loadHighlightedCode(); + }, [code, language]); + + if (isLoading) { + return ( + + {code} + + ); + } + + return ( + + ); +} diff --git a/apps/framework-docs-v2/src/components/mdx/server-code-block.tsx b/apps/framework-docs-v2/src/components/mdx/server-code-block.tsx index c5f5f9b7f6..bf523147a7 100644 --- a/apps/framework-docs-v2/src/components/mdx/server-code-block.tsx +++ b/apps/framework-docs-v2/src/components/mdx/server-code-block.tsx @@ -3,6 +3,7 @@ import { cn } from "@/lib/utils"; import { CodeSnippet } from "./code-snippet"; import { CodeEditorWrapper } from "./code-editor-wrapper"; import { ShellSnippet } from "./shell-snippet"; +import { InlineCode } from "./inline-code"; import { extractTextContent } from "@/lib/extract-text-content"; // Shell languages that should use terminal styling @@ -28,6 +29,14 @@ const CONFIG_LANGUAGES = new Set([ "config", ]); +/** + * Parsed substring highlight with optional occurrence filter + */ +interface SubstringHighlight { + pattern: string; + occurrences?: number[]; +} + /** * Props interface for server-side code block * All data-* attributes from markdown are available here @@ -48,6 +57,14 @@ export interface ServerCodeBlockProps "data-delay"?: string; "data-writing"?: string; "data-linenumbers"?: string; + "data-showlinenumbers"?: string; + + // Line and substring highlighting (Nextra-style) + "data-highlight-lines"?: string; + "data-highlight-strings"?: string; + + // Animation flag (Nextra extension) + "data-animate"?: string; children?: React.ReactNode; } @@ -108,11 +125,69 @@ function findCodeElement( return undefined; } +/** + * Parse line highlight specification into array of line numbers + * Handles: "1", "1,4-5", "1-3,7,9-11" + */ +function parseLineHighlights(spec: string | undefined): number[] { + if (!spec) return []; + + const lines: number[] = []; + const parts = spec.split(","); + + for (const part of parts) { + const trimmed = part.trim(); + if (trimmed.includes("-")) { + const [start, end] = trimmed.split("-").map((n) => parseInt(n, 10)); + if ( + start !== undefined && + end !== undefined && + !isNaN(start) && + !isNaN(end) + ) { + for (let i = start; i <= end; i++) { + lines.push(i); + } + } + } else { + const num = parseInt(trimmed, 10); + if (!isNaN(num)) { + lines.push(num); + } + } + } + + return lines; +} + +/** + * Parse substring highlights from JSON string + */ +function parseSubstringHighlights( + jsonStr: string | undefined, +): SubstringHighlight[] { + if (!jsonStr) return []; + + try { + return JSON.parse(jsonStr) as SubstringHighlight[]; + } catch { + return []; + } +} + /** * Server-side code block component * * Extracts all code block attributes and routes to the appropriate * client-side component based on language and attributes. + * + * Supports Nextra-style syntax: + * - ```js {1,4-5} → Line highlighting + * - ```js /useState/ → Substring highlighting + * - ```js copy → Copy button + * - ```js showLineNumbers→ Line numbers + * - ```js filename="x" → File header + * - ```js animate → Animated typing effect */ export function ServerCodeBlock({ children, @@ -156,29 +231,60 @@ export function ServerCodeBlock({ // Variant: "terminal" or "ide" const variant = props["data-variant"] as "terminal" | "ide" | undefined; - // Animation settings + // Animation settings - explicit animate flag takes precedence + const animateFlag = props["data-animate"]; + const shouldAnimate = animateFlag === "true"; + const shouldNotAnimate = animateFlag === "false"; + const duration = props["data-duration"] ? parseFloat(props["data-duration"]) : undefined; const delay = props["data-delay"] ? parseFloat(props["data-delay"]) : undefined; const writing = props["data-writing"] !== "false"; - // Line numbers: defaults to true unless explicitly set to "false" - const lineNumbers = props["data-linenumbers"] !== "false"; + // Line numbers: support both linenumbers and showlinenumbers + const lineNumbersFlag = + props["data-showlinenumbers"] ?? props["data-linenumbers"]; + const lineNumbers = lineNumbersFlag !== "false"; + + // Highlighting + const highlightLines = parseLineHighlights(props["data-highlight-lines"]); + const highlightStrings = parseSubstringHighlights( + props["data-highlight-strings"], + ); // Determine component type based on language and attributes const isShell = SHELL_LANGUAGES.has(language); const isConfigFile = CONFIG_LANGUAGES.has(language); + const isAnsi = language === "ansi"; + + // ANSI blocks render as plain text with ANSI escape code handling + if (isAnsi) { + return ( +
    + +
    + ); + } // Routing logic: - // 1. Config files → Always static CodeSnippet (never animated) - // 2. Shell + filename + copy=false → Animated CodeEditorWrapper (terminal style) - // 3. Shell (all other cases) → ShellSnippet (copyable Terminal tab UI) - // 4. Non-shell + filename + no copy attr → Animated CodeEditorWrapper - // 5. Default → Static CodeSnippet - - // Config files always use static CodeSnippet (never animated) - if (isConfigFile) { + // 1. Config files → Always static CodeSnippet (never animated unless explicit) + // 2. Explicit animate flag → Use CodeEditorWrapper + // 3. Explicit animate=false → Use CodeSnippet + // 4. Shell + filename + copy=false → Animated CodeEditorWrapper (terminal style) + // 5. Shell (all other cases) → ShellSnippet (copyable Terminal tab UI) + // 6. Non-shell + filename + no copy attr + no animate=false → Animated CodeEditorWrapper + // 7. Default → Static CodeSnippet + + // Config files use static CodeSnippet unless explicitly animated + if (isConfigFile && !shouldAnimate) { return (
    +
    + ); + } + + // Explicit animate flag + if (shouldAnimate) { + return ( +
    +
    ); } // Shell commands: Use animated terminal only when explicitly copy=false with filename + // and animate flag is not explicitly false // Otherwise, always use ShellSnippet (the Terminal tab UI with copy button) if (isShell) { // Only use animated terminal when explicitly no copy button wanted - if (filename && props["data-copy"] === "false") { + if (filename && props["data-copy"] === "false" && !shouldNotAnimate) { return (
    ); @@ -255,6 +385,8 @@ export function ServerCodeBlock({ /** * Server-side inline code component + * + * Supports Nextra-style inline highlighting: `code{:lang}` */ export function ServerInlineCode({ children, @@ -282,6 +414,18 @@ export function ServerInlineCode({ ); } + // Check for inline code with language hint: `code{:lang}` + const textContent = + typeof children === "string" ? children : extractTextContent(children); + const inlineLangMatch = textContent.match(/^(.+)\{:(\w+)\}$/); + + if (inlineLangMatch) { + const [, code, lang] = inlineLangMatch; + if (code && lang) { + return ; + } + } + // Inline code - simple styled element return ( 0) { + node.properties["data-highlight-strings"] = JSON.stringify( + parsed.highlightStrings, + ); + } + // Only process the first code child break; } @@ -73,69 +90,148 @@ export function rehypeCodeMeta() { } /** - * Parses a code block meta string into key-value pairs + * Parsed substring highlight with optional occurrence filter + */ +interface SubstringHighlight { + pattern: string; + occurrences?: number[]; // undefined = all occurrences +} + +/** + * Result of parsing the meta string + */ +interface ParsedMeta { + attributes: Record; + highlightLines: string | null; // e.g., "1,4-5" + highlightStrings: SubstringHighlight[]; +} + +/** + * Parses a code block meta string into key-value pairs, line highlights, + * and substring highlights. * * Handles: * - key="value" or key='value' * - key=value (no quotes) * - key (flag, becomes "true") + * - {1,4-5} (line highlighting) + * - /substring/ (substring highlighting) + * - /substring/1 or /substring/1-3 or /substring/1,3 (occurrence filtering) */ -function parseMetaString(meta: string): Record { - const attributes: Record = {}; +function parseMetaString(meta: string): ParsedMeta { + const result: ParsedMeta = { + attributes: {}, + highlightLines: null, + highlightStrings: [], + }; if (!meta || typeof meta !== "string") { - return attributes; + return result; } - // Regex patterns for different attribute formats - // Pattern 1: key="value" or key='value' (quoted) - const quotedPattern = /(\w+)=["']([^"']*)["']/g; - // Pattern 2: key=value (unquoted, stops at whitespace) - const unquotedPattern = /(\w+)=([^\s"']+)/g; - // Pattern 3: standalone key (flag-style) - const flagPattern = /(?:^|\s)(\w+)(?=\s|$)/g; - - // Track which parts of the string we've processed let processed = meta; - // First, extract quoted values - let match: RegExpExecArray | null = quotedPattern.exec(meta); - while (match !== null) { + // 1. Extract line highlighting: {1,4-5} + const lineHighlightMatch = processed.match(/\{([^}]+)\}/); + if (lineHighlightMatch?.[1]) { + result.highlightLines = lineHighlightMatch[1]; + processed = processed.replace(lineHighlightMatch[0], " "); + } + + // 2. Extract substring highlighting: /pattern/ or /pattern/occurrences + // Pattern: /[^/]+/(?:\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*)? + const substringPattern = /\/([^/]+)\/(\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*)?/g; + let substringMatch = substringPattern.exec(processed); + while (substringMatch !== null) { + const pattern = substringMatch[1]; + const occurrenceSpec = substringMatch[2]; + + if (pattern) { + const highlight: SubstringHighlight = { pattern }; + + if (occurrenceSpec) { + highlight.occurrences = parseOccurrenceSpec(occurrenceSpec); + } + + result.highlightStrings.push(highlight); + } + substringMatch = substringPattern.exec(processed); + } + + // Remove substring patterns from processed string for attribute parsing + processed = processed.replace(substringPattern, " "); + + // 3. Extract quoted values: key="value" or key='value' + const quotedPattern = /(\w+)=["']([^"']*)["']/g; + for (const match of meta.matchAll(quotedPattern)) { const key = match[1]; const value = match[2]; if (key) { - attributes[key] = value ?? ""; - // Mark as processed by replacing with spaces + result.attributes[key] = value ?? ""; processed = processed.replace(match[0], " ".repeat(match[0].length)); } - match = quotedPattern.exec(meta); } - // Then, extract unquoted values from remaining string - match = unquotedPattern.exec(processed); - while (match !== null) { + // 4. Extract unquoted values: key=value + const unquotedPattern = /(\w+)=([^\s"'{}\/]+)/g; + for (const match of processed.matchAll(unquotedPattern)) { const key = match[1]; const value = match[2]; - if (key && !attributes[key]) { - attributes[key] = value ?? ""; + if (key && !result.attributes[key]) { + result.attributes[key] = value ?? ""; } - match = unquotedPattern.exec(processed); } - // Finally, extract flags from remaining string + // 5. Extract flags (standalone words) // Reset processed to only include non-key=value parts - const remainingParts = processed.split(/\w+=\S+/).join(" "); - match = flagPattern.exec(remainingParts); - while (match !== null) { + const remainingParts = processed + .replace(/\w+=\S+/g, " ") + .replace(/\{[^}]*\}/g, " ") + .replace(/\/[^/]+\/\S*/g, " "); + const flagPattern = /(?:^|\s)(\w+)(?=\s|$)/g; + for (const match of remainingParts.matchAll(flagPattern)) { const key = match[1]; - // Only add if not already set - if (key && !attributes[key]) { - attributes[key] = "true"; + if (key && !result.attributes[key]) { + result.attributes[key] = "true"; + } + } + + return result; +} + +/** + * Parse occurrence specification like "1", "1-3", "1,3", "1-3,5" + * Returns array of 1-indexed occurrence numbers + */ +function parseOccurrenceSpec(spec: string): number[] { + const occurrences: number[] = []; + const parts = spec.split(","); + + for (const part of parts) { + const trimmed = part.trim(); + if (trimmed.includes("-")) { + // Range: "1-3" + const [start, end] = trimmed.split("-").map((n) => parseInt(n, 10)); + if ( + start !== undefined && + end !== undefined && + !Number.isNaN(start) && + !Number.isNaN(end) + ) { + for (let i = start; i <= end; i++) { + occurrences.push(i); + } + } + } else { + // Single number: "1" + const num = parseInt(trimmed, 10); + if (!Number.isNaN(num)) { + occurrences.push(num); + } } - match = flagPattern.exec(remainingParts); } - return attributes; + return occurrences; } export default rehypeCodeMeta; From 5a0122533405591cf1689693fc3bba3af132e865 Mon Sep 17 00:00:00 2001 From: Olivia Kane Date: Tue, 2 Dec 2025 12:33:35 -0500 Subject: [PATCH 20/21] docs: add guides content model proposal --- .../guides-specs/GUIDE-CONTENT-MODEL.md | 614 ++++++++++++++++++ .../guides-specs/content-model.ts | 235 +++++++ .../guides-specs/debezium-guide-example.mdx | 384 +++++++++++ apps/framework-docs-v2/guides-specs/index.ts | 7 + 4 files changed, 1240 insertions(+) create mode 100644 apps/framework-docs-v2/guides-specs/GUIDE-CONTENT-MODEL.md create mode 100644 apps/framework-docs-v2/guides-specs/content-model.ts create mode 100644 apps/framework-docs-v2/guides-specs/debezium-guide-example.mdx create mode 100644 apps/framework-docs-v2/guides-specs/index.ts diff --git a/apps/framework-docs-v2/guides-specs/GUIDE-CONTENT-MODEL.md b/apps/framework-docs-v2/guides-specs/GUIDE-CONTENT-MODEL.md new file mode 100644 index 0000000000..47e5fa9932 --- /dev/null +++ b/apps/framework-docs-v2/guides-specs/GUIDE-CONTENT-MODEL.md @@ -0,0 +1,614 @@ +# Guide Content Model - API Specification + +**Status**: API defined, implementation pending + +This document defines the developer-facing API for technology-variant guides. + +**Capabilities:** +- Render guides with technology-variant content +- Export entire guide to Linear as a project with issues +- Export individual steps as agent prompts for coding assistants + +--- + +## Developer Experience + +Authors write MDX naturally, defining dimensions in frontmatter and using conditional components inline: + +## Quick Start + +```mdx +--- +title: Set Up Your Database +techSelector: + - dimension: oltp + label: Database + options: + - { value: postgresql, label: PostgreSQL, default: true } + - { value: mysql, label: MySQL } +--- + +import { TechContextProvider, TechSelector, When, Steps, Step } from "@514labs/design-system-components/guides"; + + + + + + + + + + + +Run this SQL command: + +```sql +SHOW wal_level; +``` + + + + + +Check your MySQL config: + +```sql +SHOW VARIABLES LIKE 'log_bin'; +``` + + + + + + + +Run the dev server: + +```bash +moose dev +``` + + + + + + +``` + +Step numbers are assigned automatically based on render order. Conditional steps are numbered correctly—if a step is hidden, subsequent steps renumber. + +--- + +## Frontmatter Config + +Define your dimensions in YAML frontmatter: + +```yaml +--- +title: My Guide +description: Guide description +techSelector: + - dimension: oltp + label: Source Database + options: + - { value: postgresql, label: PostgreSQL, default: true } + - { value: mysql, label: MySQL } + - dimension: language + label: Language + options: + - { value: typescript, label: TypeScript, default: true } + - { value: python, label: Python } +--- +``` + +Dimension names are **open-ended strings**. Use any name relevant to your guide: + +| Common Dimensions | Custom Examples | +|-------------------|-----------------| +| `language`, `oltp`, `olap`, `streaming`, `orm`, `deployment`, `cloud`, `packageManager` | `authProvider`, `paymentGateway`, `ciPlatform`, `containerRuntime` | + +--- + +## Conditional Components + +### `` - Show content for a specific value + +```mdx + + +PostgreSQL uses WAL (Write-Ahead Logging) for replication. + +```sql +ALTER SYSTEM SET wal_level = logical; +``` + + +``` + +### `` with multiple values + +```mdx + + +Install the npm package: + +```bash +npm install @514labs/moose-lib +``` + + +``` + +### `` - Show content when condition is NOT met + +```mdx + + +Since you're using an ORM, you can reuse your existing models. + + +``` + +### `` / `` - Mutually exclusive content + +When every option has distinct content: + +```mdx + + + +## PostgreSQL Setup + +Enable logical replication in `postgresql.conf`: + +```properties +wal_level = logical +``` + + + + +## MySQL Setup + +Enable binary logging in `my.cnf`: + +```properties +log_bin = mysql-bin +binlog_format = ROW +``` + + + +``` + +### `` - Inline dynamic text + +Insert the user's current selection: + +```mdx +Now that you've configured your database, +you can start streaming changes to ClickHouse. +``` + +Renders as: "Now that you've configured your **PostgreSQL** database..." + +Custom labels: + +```mdx + +``` + +### `` - Complex predicates + +For AND/OR/NOT logic: + +```mdx + + +Drizzle with TypeScript setup... + + + + + +ORM-specific instructions... + + + +This section only applies to cloud deployments.

    } +> + +Cloud deployment instructions... + +
    +``` + +--- + +## Complete Example + +```mdx +--- +title: Stream Data from Your Database with Debezium +description: Mirror your database to ClickHouse in real-time. +techSelector: + - dimension: oltp + label: Source Database + options: + - { value: postgresql, label: PostgreSQL, default: true } + - { value: mysql, label: MySQL } + - dimension: orm + label: Schema Source + options: + - { value: none, label: Generate from DB, default: true } + - { value: drizzle, label: Drizzle ORM } + - { value: prisma, label: Prisma } +--- + +import { + TechContextProvider, + TechSelector, + Steps, + Step, + When, + NotWhen, + TechSwitch, + TechCase, + TechRef +} from "@514labs/design-system-components/guides"; + + + + + +# Stream Data from Your Database with Debezium + +This guide shows you how to stream changes from your +database to ClickHouse in real-time. + + + + + +Copy the environment file and set your database credentials: + +```bash +cp .env.example .env.dev +``` + + + + +```properties +DB_HOST=your_postgres_host +DB_PORT=5432 +CDC_TOPIC_PREFIX=pg-cdc +``` + + + + +```properties +DB_HOST=your_mysql_host +DB_PORT=3306 +CDC_TOPIC_PREFIX=mysql-cdc +``` + + + + + + + + + + +Debezium needs PostgreSQL's logical replication. Check it's enabled: + +```sql +SHOW wal_level; +``` + +It must be `logical`. If not, update `postgresql.conf` and restart. + +Create a replication user: + +```sql +CREATE USER cdc_user WITH PASSWORD 'secure_password'; +ALTER USER cdc_user WITH REPLICATION; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO cdc_user; +``` + + + + + +Debezium needs MySQL's binary logging. Check it's enabled: + +```sql +SHOW VARIABLES LIKE 'log_bin'; +``` + +It must be `ON`. If not, update `my.cnf`: + +```properties +[mysqld] +server-id=1 +log_bin=mysql-bin +binlog_format=ROW +``` + +Create a CDC user: + +```sql +CREATE USER 'cdc_user'@'%' IDENTIFIED BY 'secure_password'; +GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'cdc_user'@'%'; +``` + + + + + + + + + +Since you're using , reuse your existing models: + + + + +```typescript +import { customerAddresses } from "./schema"; + +export type CustomerAddress = typeof customerAddresses.$inferSelect; +``` + + + + +```typescript +import type { CustomerAddress } from "@prisma/client"; + +export type { CustomerAddress }; +``` + + + + + + + + +Generate TypeScript types from your database: + + + + +```bash +npx kanel --connectionString $DATABASE_URL --output ./generated +``` + + + + +```bash +npx mysql-schema-ts mysql://user:pass@localhost/db --output ./generated +``` + + + + + + + + + + +## Verification + +Any change in your table will now appear in ClickHouse: + +```bash +moose query "SELECT * FROM customer_addresses" +``` + + +``` + +--- + +## Export Capabilities + +### Step Metadata for Export + +Each step can include metadata for Linear and agent exports: + +```tsx + + +...step content... + + +``` + +### Export to Linear + +Export the entire guide as a Linear project: + +```tsx +// In guide frontmatter +--- +project: + name: "CDC Pipeline Setup" + team: "Platform" + priority: 2 + labels: ["infrastructure", "q1-2024"] +--- +``` + +UI provides: +- "Export to Linear" button on guide page +- Creates project with issues for each step +- Acceptance criteria become issue checklists +- Dependencies map to issue links + +### Export as Agent Prompt + +Each step has a "Copy as Prompt" button that generates: + +```markdown +## Goal + +Set up PostgreSQL connection with Drizzle ORM + +## Files + +- `src/db/index.ts` +- `src/db/schema.ts` +- `.env` + +## Instructions + +[Step content rendered as markdown] + +## Commands + +```bash +pnpm db:generate +pnpm db:migrate +``` + +## Expected Outcome + +Database tables are created and queryable + +## Avoid + +- Don't commit .env file +- Don't use raw SQL +``` + +### Programmatic Export + +```tsx +import { + stepToLinearIssue, + stepToAgentPrompt, + stepsToLinearProject, + copyStepAsAgentPrompt +} from "@514labs/design-system-components/guides"; + +// Single step → Linear issue +const issue = stepToLinearIssue(stepData); + +// Single step → Agent prompt +const prompt = stepToAgentPrompt(stepData); + +// All steps → Linear project +const project = stepsToLinearProject(projectMeta, allSteps); + +// Copy to clipboard +await copyStepAsAgentPrompt(stepData); +``` + +--- + +## Best Practices + +**Keep variations minimal.** Most content should be unconditional. Only wrap the parts that genuinely differ. + +**Use `` for inline names.** Instead of `PostgreSQLMySQL`, just use ``. + +**Test all combinations.** Before publishing, cycle through each option and verify the content makes sense. + +**Nest markdown naturally.** The components work with standard markdown—code blocks, headers, lists all work inside conditionals. + +--- + +## Implementation Checklist + +### TechContextProvider +- [ ] Parse config from `frontmatter.techSelector` or `config` prop +- [ ] Initialize state with defaults from config +- [ ] Persist to localStorage when `storageKey` provided +- [ ] Hydrate from localStorage on mount (avoid SSR mismatch) +- [ ] Expose context via React Context + +### TechSelector +- [ ] Render dropdown for each dimension +- [ ] Update context on selection change +- [ ] Style: filter bar aesthetic, responsive + +### Conditional / When / NotWhen +- [ ] Evaluate predicates (`equals`, `oneOf`, `and`, `or`, `not`) +- [ ] Show/hide children based on evaluation +- [ ] Support `fallback` prop + +### TechSwitch / TechCase +- [ ] Match current dimension value to case +- [ ] Render matching case's children +- [ ] Support fallback when no match + +### TechRef +- [ ] Get current value for dimension +- [ ] Apply custom labels if provided +- [ ] Render inline (no wrapper element) + +### Steps / Step +- [ ] Track rendered steps in order +- [ ] Assign sequential numbers (skip hidden conditional steps) +- [ ] Style: number badge, title, content layout +- [ ] Handle dynamic re-numbering when conditionals change +- [ ] Render export buttons (Linear, Agent Prompt) +- [ ] Extract step content as markdown for export + +### Export - Linear +- [ ] "Export to Linear" button on guide page +- [ ] Convert steps to Linear project JSON +- [ ] Map estimates to story points +- [ ] Map acceptance criteria to checklist markdown +- [ ] Map dependencies to issue links +- [ ] Copy single issue markdown to clipboard + +### Export - Agent Prompt +- [ ] "Copy as Prompt" button on each step +- [ ] Generate structured prompt from step metadata +- [ ] Include files, commands, expected outcome +- [ ] Resolve conditional content based on current tech context +- [ ] Copy to clipboard with success feedback diff --git a/apps/framework-docs-v2/guides-specs/content-model.ts b/apps/framework-docs-v2/guides-specs/content-model.ts new file mode 100644 index 0000000000..297c98a91b --- /dev/null +++ b/apps/framework-docs-v2/guides-specs/content-model.ts @@ -0,0 +1,235 @@ +/** + * Guide Content Model - Type Definitions + * + * These types define the data structures for technology-variant guides. + * Supports: + * - MDX rendering with conditional content + * - Export to Linear as projects/issues + * - Export as coding agent prompts + */ + +// ============================================================================= +// TECHNOLOGY CONTEXT +// ============================================================================= + +export type TechDimension = string; + +export const CommonDimensions = { + language: "language", + framework: "framework", + scope: "scope", + oltp: "oltp", + olap: "olap", + streaming: "streaming", + deployment: "deployment", + cloud: "cloud", + orm: "orm", + packageManager: "packageManager", +} as const; + +export type TechContext = Record; + +export type TechPredicate = + | { dimension: TechDimension; equals: string } + | { dimension: TechDimension; oneOf: string[] } + | { and: TechPredicate[] } + | { or: TechPredicate[] } + | { not: TechPredicate }; + +// ============================================================================= +// FRONTMATTER CONFIG +// ============================================================================= + +export type TechSelectorConfig = { + dimensions: TechSelectorDimension[]; +}; + +export type TechSelectorDimension = { + dimension: TechDimension; + label: string; + options: TechSelectorOption[]; +}; + +export type TechSelectorOption = { + value: string; + label: string; + default?: boolean; +}; + +export type GuideFrontmatter = { + title: string; + description?: string; + techSelector?: TechSelectorDimension[]; + /** Linear project metadata for export */ + project?: ProjectMeta; + [key: string]: unknown; +}; + +// ============================================================================= +// PROJECT / TASK METADATA (for Linear export) +// ============================================================================= + +export type ProjectMeta = { + /** Linear project name */ + name: string; + /** Project description */ + description?: string; + /** Team or area label */ + team?: string; + /** Priority: 0 (urgent) - 4 (low) */ + priority?: 0 | 1 | 2 | 3 | 4; + /** Labels to apply to all issues */ + labels?: string[]; +}; + +export type TaskMeta = { + /** Issue title (defaults to step title) */ + title?: string; + /** Detailed description for the issue */ + description?: string; + /** Acceptance criteria as checklist items */ + acceptanceCriteria?: string[]; + /** Story points or t-shirt size */ + estimate?: "xs" | "s" | "m" | "l" | "xl" | number; + /** Labels for this specific task */ + labels?: string[]; + /** IDs of steps this depends on */ + dependsOn?: string[]; + /** Assignee hint (role or person) */ + assignee?: string; +}; + +// ============================================================================= +// AGENT PROMPT METADATA +// ============================================================================= + +/** + * Agent prompt metadata. + * + * Most fields are DERIVED from step content: + * - goal: defaults to step title + * - files: extracted from code blocks with filenames + * - commands: extracted from ```bash code blocks + * - context: extracted from prose paragraphs + * + * Only specify fields here to OVERRIDE or ADD to derived values. + */ +export type AgentPromptMeta = { + /** Override goal (defaults to step title) */ + goal?: string; + /** Additional files beyond those in code blocks */ + files?: string[]; + /** Additional commands beyond those in code blocks */ + commands?: string[]; + /** Expected outcome description */ + expectedOutcome?: string; + /** Additional context beyond prose content */ + context?: string; + /** Don't do these things */ + avoid?: string[]; +}; + +// ============================================================================= +// STEP PROPS (extended) +// ============================================================================= + +export type StepMeta = { + /** Unique identifier for dependencies */ + id?: string; + /** Step title */ + title: string; + /** Task metadata for Linear export */ + task?: TaskMeta; + /** Agent prompt metadata for coding assistant export */ + agent?: AgentPromptMeta; + /** Condition for showing this step */ + when?: TechPredicate; +}; + +// ============================================================================= +// BLOCK METADATA (for exportable content blocks) +// ============================================================================= + +export type CodeBlockMeta = { + /** Filename to create/modify */ + filename?: string; + /** Language hint */ + language?: string; + /** Description of what this code does */ + description?: string; + /** Is this the complete file or a snippet? */ + complete?: boolean; +}; + +export type CommandBlockMeta = { + /** Shell command(s) */ + command: string | string[]; + /** What this command does */ + description?: string; + /** Working directory hint */ + cwd?: string; + /** Expected output pattern */ + expectedOutput?: string; +}; + +// ============================================================================= +// DERIVED CONTENT (extracted from markdown) +// ============================================================================= + +/** + * Content derived from parsing step markdown. + * Used to auto-populate agent prompts and Linear issues. + */ +export type DerivedStepContent = { + /** Prose paragraphs (non-code content) */ + prose: string[]; + /** Code blocks with metadata */ + codeBlocks: { + language: string; + content: string; + filename?: string; + }[]; + /** Shell commands (from ```bash blocks) */ + commands: string[]; + /** File paths mentioned (from code block filenames or inline `path` refs) */ + files: string[]; + /** Headings within the step */ + headings: string[]; +}; + +// ============================================================================= +// EXPORT HELPERS (types for generated output) +// ============================================================================= + +export type LinearProject = { + name: string; + description?: string; + issues: LinearIssue[]; +}; + +export type LinearIssue = { + title: string; + description: string; + priority?: number; + estimate?: number; + labels?: string[]; + /** Markdown body */ + body: string; +}; + +/** + * Generated agent prompt. + * Combines explicit AgentPromptMeta with DerivedStepContent. + */ +export type AgentPrompt = { + /** One-line goal (from title or override) */ + goal: string; + /** Full prompt text */ + prompt: string; + /** Files mentioned (derived + explicit) */ + files: string[]; + /** Commands mentioned (derived + explicit) */ + commands: string[]; + /** Prose context (derived + explicit) */ + context: string; +}; diff --git a/apps/framework-docs-v2/guides-specs/debezium-guide-example.mdx b/apps/framework-docs-v2/guides-specs/debezium-guide-example.mdx new file mode 100644 index 0000000000..154ae871b2 --- /dev/null +++ b/apps/framework-docs-v2/guides-specs/debezium-guide-example.mdx @@ -0,0 +1,384 @@ +--- +title: Stream Data from Your Database with Debezium +description: Learn how to use the Debezium CDC template to stream data from your database to ClickHouse in real-time. + +techSelector: + - dimension: oltp + label: Source Database + options: + - { value: postgresql, label: PostgreSQL, default: true } + - { value: mysql, label: MySQL } + - dimension: orm + label: Schema Source + options: + - { value: none, label: Generate from DB, default: true } + - { value: drizzle, label: Drizzle ORM } + - { value: prisma, label: Prisma } + +project: + name: CDC Pipeline Setup + description: Set up real-time data streaming from OLTP database to ClickHouse + team: "{{ linear.issueAssignedTeam }}" ## THE USER MUST SET THIS WHEN THEY EXPORT TO LINEAR, THE SELECTION IS INJECTED HERE + priority: "{{ linear.issuePriority }}" ## THE USER MUST SET THIS WHEN THEY EXPORT TO LINEAR, THE SELECTION IS INJECTED HERE + labels: "{{ linear.issueLabels }}" ## THE USER MUST SET THIS WHEN THEY EXPORT TO LINEAR, THE SELECTION IS INJECTED HERE +--- + +import { FileTree } from "@/components/mdx"; +import { + TechContextProvider, + TechSelector, + Steps, + Step, + When, + NotWhen, + TechSwitch, + TechCase, + TechRef, +} from "@514labs/design-system-components/guides"; + + + +# Stream Data from Your Database with Debezium + + + +This guide shows you how to use the [**Debezium CDC Template**](https://github.com/514-labs/debezium-cdc). You will learn how to set up the Debezium connector with your database and mirror your data into ClickHouse in real-time. + +## Architecture Overview + +At a high level, the pipeline works like this: + +```txt +[Your Database] -> Kafka -> ClickHouse +``` + +- **Debezium** acts as the bridge between your database and Kafka. It watches for changes and publishes them to Kafka topics. +- **MooseStack** acts as the bridge between Kafka and ClickHouse. It serves as your "pipeline-as-code" layer where you define your ClickHouse tables, Kafka streams, and transformation logic. + + + + + +Clone the [Debezium CDC Template](https://github.com/514-labs/debezium-cdc) and install dependencies: + +```bash +git clone https://github.com/514-labs/debezium-cdc.git +cd debezium-cdc +pnpm install +``` + + + + + +The template uses environment variables for database passwords and connector settings. + +Copy the environment file: + +```bash +cp .env.example .env.dev +``` + +Open `.env.dev` and configure your database connection: + +```properties filename=".env.dev" +DB_HOST=your_database_host +DB_PORT=your_database_port +DB_NAME=your_database_name +DB_USER=your_database_user +DB_PASSWORD=your_database_password +``` + +Configure CDC settings: + + + + +```properties filename=".env.dev" +CDC_TABLE_INCLUDE_LIST=public.* +CDC_TOPIC_PREFIX=pg-cdc +``` + + + + +```properties filename=".env.dev" +CDC_TABLE_INCLUDE_LIST=mydb.* +CDC_TOPIC_PREFIX=mysql-cdc +``` + + + + + + + + + + +Debezium needs PostgreSQL's logical replication. + +Check `wal_level`: + +```sql +SHOW wal_level; +``` + +It must be `logical`. If not, update `postgresql.conf` and restart Postgres. + +Create a replication user: + +```sql +CREATE USER cdc_user WITH PASSWORD 'secure_password'; +ALTER USER cdc_user WITH REPLICATION; +GRANT USAGE ON SCHEMA public TO cdc_user; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO cdc_user; +``` + + + + + +Debezium needs MySQL's binary logging. + +Check binary logging: + +```sql +SHOW VARIABLES LIKE 'log_bin'; +``` + +It must be `ON`. If not, update `my.cnf`: + +```properties filename="my.cnf" +[mysqld] +server-id=1 +log_bin=mysql-bin +binlog_format=ROW +binlog_row_image=FULL +``` + +Create a CDC user: + +```sql +CREATE USER 'cdc_user'@'%' IDENTIFIED BY 'secure_password'; +GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'cdc_user'@'%'; +``` + + + + + + + +Start the development environment: + +```bash +moose dev +``` + +Check the logs for: +- Infrastructure starting (Redpanda, Kafka Connect, ClickHouse) +- `setup-cdc.ts` running +- `✅ Connector registered!` + + + + + +Import Kafka topic definitions: + +```bash +moose-cli kafka pull localhost:19092 --path cdc-pipeline/1-sources +``` + + + +Export types from your schema: + + + + +```typescript filename="cdc-pipeline/oltp/schema.ts" +import { customerAddresses } from "../../postgres/src/schema"; + +export type CustomerAddress = typeof customerAddresses.$inferSelect; +``` + + + + +```typescript filename="cdc-pipeline/oltp/schema.ts" +import type { CustomerAddress as PrismaCustomerAddress } from "@prisma/client"; + +export type CustomerAddress = PrismaCustomerAddress; +``` + + + + + + + + +Generate TypeScript types from your database: + + + + +```bash +npx kanel --connectionString $DATABASE_URL --output ./cdc-pipeline/generated-models +``` + + + + +```bash +npx mysql-schema-ts mysql://user:pass@localhost/db --output ./cdc-pipeline/generated-models +``` + + + + + + +Create typed topics in `cdc-pipeline/1-sources/typed-topics.ts`: + +```typescript filename="cdc-pipeline/1-sources/typed-topics.ts" +import { Stream } from "@514labs/moose-lib"; +import { PgCdcPublicCustomerAddressesStream } from "./externalTopics"; +import { GenericCDCEvent } from "../models"; +import { CustomerAddress } from "../../oltp/schema"; + +export const cdcCustomerAddresses = PgCdcPublicCustomerAddressesStream as Stream< + GenericCDCEvent +>; +``` + +Define the OLAP table: + +```typescript filename="cdc-pipeline/3-destinations/olap-tables.ts" +import { OlapTable, ClickHouseEngines, UInt64, UInt8 } from "@514labs/moose-lib"; +import { CustomerAddress } from "../../oltp/schema"; + +export type CdcFields = { + _is_deleted: UInt8; + ts_ms: UInt64; + lsn: UInt64; +}; + +export type OlapCustomerAddress = CustomerAddress & CdcFields; + +export const olapCustomerAddresses = new OlapTable( + "customer_addresses", + { + engine: ClickHouseEngines.ReplacingMergeTree, + ver: "lsn", + isDeleted: "_is_deleted", + orderByFields: ["id"], + } +); +``` + +Create the transform: + +```typescript filename="cdc-pipeline/2-transforms/customer-addresses.ts" +import { cdcCustomerAddresses } from "../1-sources/typed-topics"; +import { processedCustomerAddresses } from "../3-destinations/sink-topics"; +import { handleCDCPayload } from "./payload-handler"; +import { GenericCDCEvent, OlapCustomerAddress } from "../models"; +import { CustomerAddress } from "../../oltp/schema"; + +cdcCustomerAddresses.addTransform( + processedCustomerAddresses, + (message: GenericCDCEvent) => { + const result = handleCDCPayload(message); + return result as unknown as OlapCustomerAddress; + } +); +``` + + + + + +## Verification + +Any change in your table will now appear in ClickHouse: + +```bash +moose query "SELECT * FROM customer_addresses" +``` + + diff --git a/apps/framework-docs-v2/guides-specs/index.ts b/apps/framework-docs-v2/guides-specs/index.ts new file mode 100644 index 0000000000..edd26cbe4a --- /dev/null +++ b/apps/framework-docs-v2/guides-specs/index.ts @@ -0,0 +1,7 @@ +/** + * Guide Content Model - Type Definitions + * + * See GUIDE-CONTENT-MODEL.md for full specification. + */ + +export * from "./content-model"; From a839934ee97f83f9defb4d925a80ec342f3dd698 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 2 Dec 2025 09:43:48 -0800 Subject: [PATCH 21/21] Revert "docs: add guides content model proposal" This reverts commit 5a0122533405591cf1689693fc3bba3af132e865. --- .../guides-specs/GUIDE-CONTENT-MODEL.md | 614 ------------------ .../guides-specs/content-model.ts | 235 ------- .../guides-specs/debezium-guide-example.mdx | 384 ----------- apps/framework-docs-v2/guides-specs/index.ts | 7 - 4 files changed, 1240 deletions(-) delete mode 100644 apps/framework-docs-v2/guides-specs/GUIDE-CONTENT-MODEL.md delete mode 100644 apps/framework-docs-v2/guides-specs/content-model.ts delete mode 100644 apps/framework-docs-v2/guides-specs/debezium-guide-example.mdx delete mode 100644 apps/framework-docs-v2/guides-specs/index.ts diff --git a/apps/framework-docs-v2/guides-specs/GUIDE-CONTENT-MODEL.md b/apps/framework-docs-v2/guides-specs/GUIDE-CONTENT-MODEL.md deleted file mode 100644 index 47e5fa9932..0000000000 --- a/apps/framework-docs-v2/guides-specs/GUIDE-CONTENT-MODEL.md +++ /dev/null @@ -1,614 +0,0 @@ -# Guide Content Model - API Specification - -**Status**: API defined, implementation pending - -This document defines the developer-facing API for technology-variant guides. - -**Capabilities:** -- Render guides with technology-variant content -- Export entire guide to Linear as a project with issues -- Export individual steps as agent prompts for coding assistants - ---- - -## Developer Experience - -Authors write MDX naturally, defining dimensions in frontmatter and using conditional components inline: - -## Quick Start - -```mdx ---- -title: Set Up Your Database -techSelector: - - dimension: oltp - label: Database - options: - - { value: postgresql, label: PostgreSQL, default: true } - - { value: mysql, label: MySQL } ---- - -import { TechContextProvider, TechSelector, When, Steps, Step } from "@514labs/design-system-components/guides"; - - - - - - - - - - - -Run this SQL command: - -```sql -SHOW wal_level; -``` - - - - - -Check your MySQL config: - -```sql -SHOW VARIABLES LIKE 'log_bin'; -``` - - - - - - - -Run the dev server: - -```bash -moose dev -``` - - - - - - -``` - -Step numbers are assigned automatically based on render order. Conditional steps are numbered correctly—if a step is hidden, subsequent steps renumber. - ---- - -## Frontmatter Config - -Define your dimensions in YAML frontmatter: - -```yaml ---- -title: My Guide -description: Guide description -techSelector: - - dimension: oltp - label: Source Database - options: - - { value: postgresql, label: PostgreSQL, default: true } - - { value: mysql, label: MySQL } - - dimension: language - label: Language - options: - - { value: typescript, label: TypeScript, default: true } - - { value: python, label: Python } ---- -``` - -Dimension names are **open-ended strings**. Use any name relevant to your guide: - -| Common Dimensions | Custom Examples | -|-------------------|-----------------| -| `language`, `oltp`, `olap`, `streaming`, `orm`, `deployment`, `cloud`, `packageManager` | `authProvider`, `paymentGateway`, `ciPlatform`, `containerRuntime` | - ---- - -## Conditional Components - -### `` - Show content for a specific value - -```mdx - - -PostgreSQL uses WAL (Write-Ahead Logging) for replication. - -```sql -ALTER SYSTEM SET wal_level = logical; -``` - - -``` - -### `` with multiple values - -```mdx - - -Install the npm package: - -```bash -npm install @514labs/moose-lib -``` - - -``` - -### `` - Show content when condition is NOT met - -```mdx - - -Since you're using an ORM, you can reuse your existing models. - - -``` - -### `` / `` - Mutually exclusive content - -When every option has distinct content: - -```mdx - - - -## PostgreSQL Setup - -Enable logical replication in `postgresql.conf`: - -```properties -wal_level = logical -``` - - - - -## MySQL Setup - -Enable binary logging in `my.cnf`: - -```properties -log_bin = mysql-bin -binlog_format = ROW -``` - - - -``` - -### `` - Inline dynamic text - -Insert the user's current selection: - -```mdx -Now that you've configured your database, -you can start streaming changes to ClickHouse. -``` - -Renders as: "Now that you've configured your **PostgreSQL** database..." - -Custom labels: - -```mdx - -``` - -### `` - Complex predicates - -For AND/OR/NOT logic: - -```mdx - - -Drizzle with TypeScript setup... - - - - - -ORM-specific instructions... - - - -This section only applies to cloud deployments.

    } -> - -Cloud deployment instructions... - -
    -``` - ---- - -## Complete Example - -```mdx ---- -title: Stream Data from Your Database with Debezium -description: Mirror your database to ClickHouse in real-time. -techSelector: - - dimension: oltp - label: Source Database - options: - - { value: postgresql, label: PostgreSQL, default: true } - - { value: mysql, label: MySQL } - - dimension: orm - label: Schema Source - options: - - { value: none, label: Generate from DB, default: true } - - { value: drizzle, label: Drizzle ORM } - - { value: prisma, label: Prisma } ---- - -import { - TechContextProvider, - TechSelector, - Steps, - Step, - When, - NotWhen, - TechSwitch, - TechCase, - TechRef -} from "@514labs/design-system-components/guides"; - - - - - -# Stream Data from Your Database with Debezium - -This guide shows you how to stream changes from your -database to ClickHouse in real-time. - - - - - -Copy the environment file and set your database credentials: - -```bash -cp .env.example .env.dev -``` - - - - -```properties -DB_HOST=your_postgres_host -DB_PORT=5432 -CDC_TOPIC_PREFIX=pg-cdc -``` - - - - -```properties -DB_HOST=your_mysql_host -DB_PORT=3306 -CDC_TOPIC_PREFIX=mysql-cdc -``` - - - - - - - - - - -Debezium needs PostgreSQL's logical replication. Check it's enabled: - -```sql -SHOW wal_level; -``` - -It must be `logical`. If not, update `postgresql.conf` and restart. - -Create a replication user: - -```sql -CREATE USER cdc_user WITH PASSWORD 'secure_password'; -ALTER USER cdc_user WITH REPLICATION; -GRANT SELECT ON ALL TABLES IN SCHEMA public TO cdc_user; -``` - - - - - -Debezium needs MySQL's binary logging. Check it's enabled: - -```sql -SHOW VARIABLES LIKE 'log_bin'; -``` - -It must be `ON`. If not, update `my.cnf`: - -```properties -[mysqld] -server-id=1 -log_bin=mysql-bin -binlog_format=ROW -``` - -Create a CDC user: - -```sql -CREATE USER 'cdc_user'@'%' IDENTIFIED BY 'secure_password'; -GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'cdc_user'@'%'; -``` - - - - - - - - - -Since you're using , reuse your existing models: - - - - -```typescript -import { customerAddresses } from "./schema"; - -export type CustomerAddress = typeof customerAddresses.$inferSelect; -``` - - - - -```typescript -import type { CustomerAddress } from "@prisma/client"; - -export type { CustomerAddress }; -``` - - - - - - - - -Generate TypeScript types from your database: - - - - -```bash -npx kanel --connectionString $DATABASE_URL --output ./generated -``` - - - - -```bash -npx mysql-schema-ts mysql://user:pass@localhost/db --output ./generated -``` - - - - - - - - - - -## Verification - -Any change in your table will now appear in ClickHouse: - -```bash -moose query "SELECT * FROM customer_addresses" -``` - - -``` - ---- - -## Export Capabilities - -### Step Metadata for Export - -Each step can include metadata for Linear and agent exports: - -```tsx - - -...step content... - - -``` - -### Export to Linear - -Export the entire guide as a Linear project: - -```tsx -// In guide frontmatter ---- -project: - name: "CDC Pipeline Setup" - team: "Platform" - priority: 2 - labels: ["infrastructure", "q1-2024"] ---- -``` - -UI provides: -- "Export to Linear" button on guide page -- Creates project with issues for each step -- Acceptance criteria become issue checklists -- Dependencies map to issue links - -### Export as Agent Prompt - -Each step has a "Copy as Prompt" button that generates: - -```markdown -## Goal - -Set up PostgreSQL connection with Drizzle ORM - -## Files - -- `src/db/index.ts` -- `src/db/schema.ts` -- `.env` - -## Instructions - -[Step content rendered as markdown] - -## Commands - -```bash -pnpm db:generate -pnpm db:migrate -``` - -## Expected Outcome - -Database tables are created and queryable - -## Avoid - -- Don't commit .env file -- Don't use raw SQL -``` - -### Programmatic Export - -```tsx -import { - stepToLinearIssue, - stepToAgentPrompt, - stepsToLinearProject, - copyStepAsAgentPrompt -} from "@514labs/design-system-components/guides"; - -// Single step → Linear issue -const issue = stepToLinearIssue(stepData); - -// Single step → Agent prompt -const prompt = stepToAgentPrompt(stepData); - -// All steps → Linear project -const project = stepsToLinearProject(projectMeta, allSteps); - -// Copy to clipboard -await copyStepAsAgentPrompt(stepData); -``` - ---- - -## Best Practices - -**Keep variations minimal.** Most content should be unconditional. Only wrap the parts that genuinely differ. - -**Use `` for inline names.** Instead of `PostgreSQLMySQL`, just use ``. - -**Test all combinations.** Before publishing, cycle through each option and verify the content makes sense. - -**Nest markdown naturally.** The components work with standard markdown—code blocks, headers, lists all work inside conditionals. - ---- - -## Implementation Checklist - -### TechContextProvider -- [ ] Parse config from `frontmatter.techSelector` or `config` prop -- [ ] Initialize state with defaults from config -- [ ] Persist to localStorage when `storageKey` provided -- [ ] Hydrate from localStorage on mount (avoid SSR mismatch) -- [ ] Expose context via React Context - -### TechSelector -- [ ] Render dropdown for each dimension -- [ ] Update context on selection change -- [ ] Style: filter bar aesthetic, responsive - -### Conditional / When / NotWhen -- [ ] Evaluate predicates (`equals`, `oneOf`, `and`, `or`, `not`) -- [ ] Show/hide children based on evaluation -- [ ] Support `fallback` prop - -### TechSwitch / TechCase -- [ ] Match current dimension value to case -- [ ] Render matching case's children -- [ ] Support fallback when no match - -### TechRef -- [ ] Get current value for dimension -- [ ] Apply custom labels if provided -- [ ] Render inline (no wrapper element) - -### Steps / Step -- [ ] Track rendered steps in order -- [ ] Assign sequential numbers (skip hidden conditional steps) -- [ ] Style: number badge, title, content layout -- [ ] Handle dynamic re-numbering when conditionals change -- [ ] Render export buttons (Linear, Agent Prompt) -- [ ] Extract step content as markdown for export - -### Export - Linear -- [ ] "Export to Linear" button on guide page -- [ ] Convert steps to Linear project JSON -- [ ] Map estimates to story points -- [ ] Map acceptance criteria to checklist markdown -- [ ] Map dependencies to issue links -- [ ] Copy single issue markdown to clipboard - -### Export - Agent Prompt -- [ ] "Copy as Prompt" button on each step -- [ ] Generate structured prompt from step metadata -- [ ] Include files, commands, expected outcome -- [ ] Resolve conditional content based on current tech context -- [ ] Copy to clipboard with success feedback diff --git a/apps/framework-docs-v2/guides-specs/content-model.ts b/apps/framework-docs-v2/guides-specs/content-model.ts deleted file mode 100644 index 297c98a91b..0000000000 --- a/apps/framework-docs-v2/guides-specs/content-model.ts +++ /dev/null @@ -1,235 +0,0 @@ -/** - * Guide Content Model - Type Definitions - * - * These types define the data structures for technology-variant guides. - * Supports: - * - MDX rendering with conditional content - * - Export to Linear as projects/issues - * - Export as coding agent prompts - */ - -// ============================================================================= -// TECHNOLOGY CONTEXT -// ============================================================================= - -export type TechDimension = string; - -export const CommonDimensions = { - language: "language", - framework: "framework", - scope: "scope", - oltp: "oltp", - olap: "olap", - streaming: "streaming", - deployment: "deployment", - cloud: "cloud", - orm: "orm", - packageManager: "packageManager", -} as const; - -export type TechContext = Record; - -export type TechPredicate = - | { dimension: TechDimension; equals: string } - | { dimension: TechDimension; oneOf: string[] } - | { and: TechPredicate[] } - | { or: TechPredicate[] } - | { not: TechPredicate }; - -// ============================================================================= -// FRONTMATTER CONFIG -// ============================================================================= - -export type TechSelectorConfig = { - dimensions: TechSelectorDimension[]; -}; - -export type TechSelectorDimension = { - dimension: TechDimension; - label: string; - options: TechSelectorOption[]; -}; - -export type TechSelectorOption = { - value: string; - label: string; - default?: boolean; -}; - -export type GuideFrontmatter = { - title: string; - description?: string; - techSelector?: TechSelectorDimension[]; - /** Linear project metadata for export */ - project?: ProjectMeta; - [key: string]: unknown; -}; - -// ============================================================================= -// PROJECT / TASK METADATA (for Linear export) -// ============================================================================= - -export type ProjectMeta = { - /** Linear project name */ - name: string; - /** Project description */ - description?: string; - /** Team or area label */ - team?: string; - /** Priority: 0 (urgent) - 4 (low) */ - priority?: 0 | 1 | 2 | 3 | 4; - /** Labels to apply to all issues */ - labels?: string[]; -}; - -export type TaskMeta = { - /** Issue title (defaults to step title) */ - title?: string; - /** Detailed description for the issue */ - description?: string; - /** Acceptance criteria as checklist items */ - acceptanceCriteria?: string[]; - /** Story points or t-shirt size */ - estimate?: "xs" | "s" | "m" | "l" | "xl" | number; - /** Labels for this specific task */ - labels?: string[]; - /** IDs of steps this depends on */ - dependsOn?: string[]; - /** Assignee hint (role or person) */ - assignee?: string; -}; - -// ============================================================================= -// AGENT PROMPT METADATA -// ============================================================================= - -/** - * Agent prompt metadata. - * - * Most fields are DERIVED from step content: - * - goal: defaults to step title - * - files: extracted from code blocks with filenames - * - commands: extracted from ```bash code blocks - * - context: extracted from prose paragraphs - * - * Only specify fields here to OVERRIDE or ADD to derived values. - */ -export type AgentPromptMeta = { - /** Override goal (defaults to step title) */ - goal?: string; - /** Additional files beyond those in code blocks */ - files?: string[]; - /** Additional commands beyond those in code blocks */ - commands?: string[]; - /** Expected outcome description */ - expectedOutcome?: string; - /** Additional context beyond prose content */ - context?: string; - /** Don't do these things */ - avoid?: string[]; -}; - -// ============================================================================= -// STEP PROPS (extended) -// ============================================================================= - -export type StepMeta = { - /** Unique identifier for dependencies */ - id?: string; - /** Step title */ - title: string; - /** Task metadata for Linear export */ - task?: TaskMeta; - /** Agent prompt metadata for coding assistant export */ - agent?: AgentPromptMeta; - /** Condition for showing this step */ - when?: TechPredicate; -}; - -// ============================================================================= -// BLOCK METADATA (for exportable content blocks) -// ============================================================================= - -export type CodeBlockMeta = { - /** Filename to create/modify */ - filename?: string; - /** Language hint */ - language?: string; - /** Description of what this code does */ - description?: string; - /** Is this the complete file or a snippet? */ - complete?: boolean; -}; - -export type CommandBlockMeta = { - /** Shell command(s) */ - command: string | string[]; - /** What this command does */ - description?: string; - /** Working directory hint */ - cwd?: string; - /** Expected output pattern */ - expectedOutput?: string; -}; - -// ============================================================================= -// DERIVED CONTENT (extracted from markdown) -// ============================================================================= - -/** - * Content derived from parsing step markdown. - * Used to auto-populate agent prompts and Linear issues. - */ -export type DerivedStepContent = { - /** Prose paragraphs (non-code content) */ - prose: string[]; - /** Code blocks with metadata */ - codeBlocks: { - language: string; - content: string; - filename?: string; - }[]; - /** Shell commands (from ```bash blocks) */ - commands: string[]; - /** File paths mentioned (from code block filenames or inline `path` refs) */ - files: string[]; - /** Headings within the step */ - headings: string[]; -}; - -// ============================================================================= -// EXPORT HELPERS (types for generated output) -// ============================================================================= - -export type LinearProject = { - name: string; - description?: string; - issues: LinearIssue[]; -}; - -export type LinearIssue = { - title: string; - description: string; - priority?: number; - estimate?: number; - labels?: string[]; - /** Markdown body */ - body: string; -}; - -/** - * Generated agent prompt. - * Combines explicit AgentPromptMeta with DerivedStepContent. - */ -export type AgentPrompt = { - /** One-line goal (from title or override) */ - goal: string; - /** Full prompt text */ - prompt: string; - /** Files mentioned (derived + explicit) */ - files: string[]; - /** Commands mentioned (derived + explicit) */ - commands: string[]; - /** Prose context (derived + explicit) */ - context: string; -}; diff --git a/apps/framework-docs-v2/guides-specs/debezium-guide-example.mdx b/apps/framework-docs-v2/guides-specs/debezium-guide-example.mdx deleted file mode 100644 index 154ae871b2..0000000000 --- a/apps/framework-docs-v2/guides-specs/debezium-guide-example.mdx +++ /dev/null @@ -1,384 +0,0 @@ ---- -title: Stream Data from Your Database with Debezium -description: Learn how to use the Debezium CDC template to stream data from your database to ClickHouse in real-time. - -techSelector: - - dimension: oltp - label: Source Database - options: - - { value: postgresql, label: PostgreSQL, default: true } - - { value: mysql, label: MySQL } - - dimension: orm - label: Schema Source - options: - - { value: none, label: Generate from DB, default: true } - - { value: drizzle, label: Drizzle ORM } - - { value: prisma, label: Prisma } - -project: - name: CDC Pipeline Setup - description: Set up real-time data streaming from OLTP database to ClickHouse - team: "{{ linear.issueAssignedTeam }}" ## THE USER MUST SET THIS WHEN THEY EXPORT TO LINEAR, THE SELECTION IS INJECTED HERE - priority: "{{ linear.issuePriority }}" ## THE USER MUST SET THIS WHEN THEY EXPORT TO LINEAR, THE SELECTION IS INJECTED HERE - labels: "{{ linear.issueLabels }}" ## THE USER MUST SET THIS WHEN THEY EXPORT TO LINEAR, THE SELECTION IS INJECTED HERE ---- - -import { FileTree } from "@/components/mdx"; -import { - TechContextProvider, - TechSelector, - Steps, - Step, - When, - NotWhen, - TechSwitch, - TechCase, - TechRef, -} from "@514labs/design-system-components/guides"; - - - -# Stream Data from Your Database with Debezium - - - -This guide shows you how to use the [**Debezium CDC Template**](https://github.com/514-labs/debezium-cdc). You will learn how to set up the Debezium connector with your database and mirror your data into ClickHouse in real-time. - -## Architecture Overview - -At a high level, the pipeline works like this: - -```txt -[Your Database] -> Kafka -> ClickHouse -``` - -- **Debezium** acts as the bridge between your database and Kafka. It watches for changes and publishes them to Kafka topics. -- **MooseStack** acts as the bridge between Kafka and ClickHouse. It serves as your "pipeline-as-code" layer where you define your ClickHouse tables, Kafka streams, and transformation logic. - - - - - -Clone the [Debezium CDC Template](https://github.com/514-labs/debezium-cdc) and install dependencies: - -```bash -git clone https://github.com/514-labs/debezium-cdc.git -cd debezium-cdc -pnpm install -``` - - - - - -The template uses environment variables for database passwords and connector settings. - -Copy the environment file: - -```bash -cp .env.example .env.dev -``` - -Open `.env.dev` and configure your database connection: - -```properties filename=".env.dev" -DB_HOST=your_database_host -DB_PORT=your_database_port -DB_NAME=your_database_name -DB_USER=your_database_user -DB_PASSWORD=your_database_password -``` - -Configure CDC settings: - - - - -```properties filename=".env.dev" -CDC_TABLE_INCLUDE_LIST=public.* -CDC_TOPIC_PREFIX=pg-cdc -``` - - - - -```properties filename=".env.dev" -CDC_TABLE_INCLUDE_LIST=mydb.* -CDC_TOPIC_PREFIX=mysql-cdc -``` - - - - - - - - - - -Debezium needs PostgreSQL's logical replication. - -Check `wal_level`: - -```sql -SHOW wal_level; -``` - -It must be `logical`. If not, update `postgresql.conf` and restart Postgres. - -Create a replication user: - -```sql -CREATE USER cdc_user WITH PASSWORD 'secure_password'; -ALTER USER cdc_user WITH REPLICATION; -GRANT USAGE ON SCHEMA public TO cdc_user; -GRANT SELECT ON ALL TABLES IN SCHEMA public TO cdc_user; -``` - - - - - -Debezium needs MySQL's binary logging. - -Check binary logging: - -```sql -SHOW VARIABLES LIKE 'log_bin'; -``` - -It must be `ON`. If not, update `my.cnf`: - -```properties filename="my.cnf" -[mysqld] -server-id=1 -log_bin=mysql-bin -binlog_format=ROW -binlog_row_image=FULL -``` - -Create a CDC user: - -```sql -CREATE USER 'cdc_user'@'%' IDENTIFIED BY 'secure_password'; -GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'cdc_user'@'%'; -``` - - - - - - - -Start the development environment: - -```bash -moose dev -``` - -Check the logs for: -- Infrastructure starting (Redpanda, Kafka Connect, ClickHouse) -- `setup-cdc.ts` running -- `✅ Connector registered!` - - - - - -Import Kafka topic definitions: - -```bash -moose-cli kafka pull localhost:19092 --path cdc-pipeline/1-sources -``` - - - -Export types from your schema: - - - - -```typescript filename="cdc-pipeline/oltp/schema.ts" -import { customerAddresses } from "../../postgres/src/schema"; - -export type CustomerAddress = typeof customerAddresses.$inferSelect; -``` - - - - -```typescript filename="cdc-pipeline/oltp/schema.ts" -import type { CustomerAddress as PrismaCustomerAddress } from "@prisma/client"; - -export type CustomerAddress = PrismaCustomerAddress; -``` - - - - - - - - -Generate TypeScript types from your database: - - - - -```bash -npx kanel --connectionString $DATABASE_URL --output ./cdc-pipeline/generated-models -``` - - - - -```bash -npx mysql-schema-ts mysql://user:pass@localhost/db --output ./cdc-pipeline/generated-models -``` - - - - - - -Create typed topics in `cdc-pipeline/1-sources/typed-topics.ts`: - -```typescript filename="cdc-pipeline/1-sources/typed-topics.ts" -import { Stream } from "@514labs/moose-lib"; -import { PgCdcPublicCustomerAddressesStream } from "./externalTopics"; -import { GenericCDCEvent } from "../models"; -import { CustomerAddress } from "../../oltp/schema"; - -export const cdcCustomerAddresses = PgCdcPublicCustomerAddressesStream as Stream< - GenericCDCEvent ->; -``` - -Define the OLAP table: - -```typescript filename="cdc-pipeline/3-destinations/olap-tables.ts" -import { OlapTable, ClickHouseEngines, UInt64, UInt8 } from "@514labs/moose-lib"; -import { CustomerAddress } from "../../oltp/schema"; - -export type CdcFields = { - _is_deleted: UInt8; - ts_ms: UInt64; - lsn: UInt64; -}; - -export type OlapCustomerAddress = CustomerAddress & CdcFields; - -export const olapCustomerAddresses = new OlapTable( - "customer_addresses", - { - engine: ClickHouseEngines.ReplacingMergeTree, - ver: "lsn", - isDeleted: "_is_deleted", - orderByFields: ["id"], - } -); -``` - -Create the transform: - -```typescript filename="cdc-pipeline/2-transforms/customer-addresses.ts" -import { cdcCustomerAddresses } from "../1-sources/typed-topics"; -import { processedCustomerAddresses } from "../3-destinations/sink-topics"; -import { handleCDCPayload } from "./payload-handler"; -import { GenericCDCEvent, OlapCustomerAddress } from "../models"; -import { CustomerAddress } from "../../oltp/schema"; - -cdcCustomerAddresses.addTransform( - processedCustomerAddresses, - (message: GenericCDCEvent) => { - const result = handleCDCPayload(message); - return result as unknown as OlapCustomerAddress; - } -); -``` - - - - - -## Verification - -Any change in your table will now appear in ClickHouse: - -```bash -moose query "SELECT * FROM customer_addresses" -``` - - diff --git a/apps/framework-docs-v2/guides-specs/index.ts b/apps/framework-docs-v2/guides-specs/index.ts deleted file mode 100644 index edd26cbe4a..0000000000 --- a/apps/framework-docs-v2/guides-specs/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Guide Content Model - Type Definitions - * - * See GUIDE-CONTENT-MODEL.md for full specification. - */ - -export * from "./content-model";