diff --git a/apps/framework-docs-v2/.npmrc b/apps/framework-docs-v2/.npmrc deleted file mode 100644 index afab184d37..0000000000 --- a/apps/framework-docs-v2/.npmrc +++ /dev/null @@ -1,6 +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 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 - diff --git a/apps/framework-docs-v2/README.md b/apps/framework-docs-v2/README.md index 51fe19b55a..c9b6722671 100644 --- a/apps/framework-docs-v2/README.md +++ b/apps/framework-docs-v2/README.md @@ -28,6 +28,42 @@ pnpm build pnpm test:snippets ``` +## Environment Variables + +Create a `.env.local` file in the root directory with the following variables: + +```bash +# GitHub API token (optional but recommended) +# Without token: 60 requests/hour rate limit +# With token: 5,000 requests/hour rate limit +GITHUB_TOKEN=your_github_token_here +``` + +### Creating a GitHub Token + +**Option 1: Using GitHub CLI (recommended)** + +If you have the GitHub CLI (`gh`) installed and authenticated: + +```bash +# Get your current GitHub token +gh auth token + +# Or create a new token with specific scopes +gh auth refresh -s public_repo +``` + +Then add the token to your `.env.local` file. + +**Option 2: Using the Web Interface** + +1. Go to https://github.com/settings/tokens +2. Click "Generate new token" → "Generate new token (classic)" +3. Give it a name (e.g., "Moose Docs") +4. Select the `public_repo` scope (or no scopes needed for public repos) +5. Generate and copy the token +6. Add it to your `.env.local` file + ## Structure - `/src/app/typescript` - TypeScript documentation diff --git a/apps/framework-docs-v2/content/guides/applications/automated-reports/guide-overview.mdx b/apps/framework-docs-v2/content/guides/applications/automated-reports/guide-overview.mdx new file mode 100644 index 0000000000..83bede250b --- /dev/null +++ b/apps/framework-docs-v2/content/guides/applications/automated-reports/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: Automated Reports Overview +description: Overview of building automated reporting systems with MooseStack +--- + +# Automated Reports Overview + +This guide covers how to build automated reporting systems using MooseStack workflows and APIs. + +## Overview + +Automated reporting systems enable scheduled and event-driven report generation, distribution, and management. + +## Getting Started + +Select a starting point from the sidebar to begin implementing automated reports. + diff --git a/apps/framework-docs-v2/content/guides/applications/going-to-production/guide-overview.mdx b/apps/framework-docs-v2/content/guides/applications/going-to-production/guide-overview.mdx new file mode 100644 index 0000000000..4a678d9e11 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/applications/going-to-production/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: Going to Production Overview +description: Overview of preparing and deploying MooseStack applications to production +--- + +# Going to Production Overview + +This guide covers best practices and considerations for deploying MooseStack applications to production environments. + +## Overview + +Deploying to production requires careful planning around infrastructure, monitoring, security, and scalability. + +## Getting Started + +Select a starting point from the sidebar to begin preparing for production deployment. + diff --git a/apps/framework-docs-v2/content/guides/applications/in-app-chat-analytics/guide-overview.mdx b/apps/framework-docs-v2/content/guides/applications/in-app-chat-analytics/guide-overview.mdx new file mode 100644 index 0000000000..2a66499cc5 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/applications/in-app-chat-analytics/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: In-App Chat Analytics Overview +description: Overview of implementing analytics for in-app chat features +--- + +# In-App Chat Analytics Overview + +This guide shows you how to implement analytics for in-app chat features using MooseStack. + +## Overview + +Implementing analytics for in-app chat requires tracking messages, user interactions, and engagement metrics in real-time. + +## Getting Started + +Select a starting point from the sidebar to begin implementing in-app chat analytics. + diff --git a/apps/framework-docs-v2/content/guides/applications/performant-dashboards/existing-oltp-db.mdx b/apps/framework-docs-v2/content/guides/applications/performant-dashboards/existing-oltp-db.mdx new file mode 100644 index 0000000000..9e2771e003 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/applications/performant-dashboards/existing-oltp-db.mdx @@ -0,0 +1,36 @@ +--- +title: From Existing OLTP DB +description: Build performant dashboards by connecting to your existing OLTP database +--- + +# From Existing OLTP DB + +This guide walks you through building performant dashboards by connecting to your existing OLTP (Online Transaction Processing) database and creating optimized materialized views for analytics. + +## Overview + +When you have an existing OLTP database (like PostgreSQL, MySQL, or SQL Server), you can leverage MooseStack to create high-performance dashboards without disrupting your production database. This approach involves: + +1. **Connecting** to your existing database +2. **Creating materialized views** that aggregate and pre-compute data +3. **Querying** the materialized views for fast dashboard responses + +## Benefits + +- **No disruption** to your production OLTP database +- **Fast queries** through pre-aggregated materialized views +- **Real-time updates** as data changes in your source database +- **Scalable** architecture that separates transactional and analytical workloads + +## Prerequisites + +Before starting, ensure you have: + +- Access to your existing OLTP database +- Database connection credentials +- A MooseStack project initialized + +## Implementation Steps + +Follow the steps below to implement performant dashboards from your existing OLTP database. Each step builds on the previous one, guiding you through the complete setup process. + diff --git a/apps/framework-docs-v2/content/guides/applications/performant-dashboards/existing-oltp-db/1-setup-connection.mdx b/apps/framework-docs-v2/content/guides/applications/performant-dashboards/existing-oltp-db/1-setup-connection.mdx new file mode 100644 index 0000000000..7437a35590 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/applications/performant-dashboards/existing-oltp-db/1-setup-connection.mdx @@ -0,0 +1,80 @@ +--- +title: Setup Connection +description: Configure connection to your existing OLTP database +--- + +import { LanguageTabs, LanguageTabContent } from "@/components/mdx"; + +# Step 1: Setup Connection + +In this step, you'll configure MooseStack to connect to your existing OLTP database. + +## Overview + +MooseStack needs to connect to your existing database to read data and create materialized views. This connection is configured securely and doesn't require any changes to your production database. + +## Configuration + + + +```typescript filename="moose.config.ts" +import { defineConfig } from "@514labs/moose-cli"; + +export default defineConfig({ + dataSources: { + postgres: { + type: "postgres", + host: process.env.DB_HOST || "localhost", + port: parseInt(process.env.DB_PORT || "5432"), + database: process.env.DB_NAME || "mydb", + user: process.env.DB_USER || "postgres", + password: process.env.DB_PASSWORD || "", + }, + }, +}); +``` + + +```python filename="moose.config.py" +from moose_cli import define_config + +config = define_config( + data_sources={ + "postgres": { + "type": "postgres", + "host": "localhost", + "port": 5432, + "database": "mydb", + "user": "postgres", + "password": "", + } + } +) +``` + + + +## Environment Variables + +For security, store sensitive credentials in environment variables: + +```bash filename=".env" +DB_HOST=your-db-host +DB_PORT=5432 +DB_NAME=your-database +DB_USER=your-username +DB_PASSWORD=your-password +``` + +## Verify Connection + +After configuring the connection, verify it works: + +```bash +moose db ping +``` + +## Next Steps + +Once your connection is configured, proceed to the next step to create materialized views. + diff --git a/apps/framework-docs-v2/content/guides/applications/performant-dashboards/existing-oltp-db/2-create-materialized-view[@scope=initiative].mdx b/apps/framework-docs-v2/content/guides/applications/performant-dashboards/existing-oltp-db/2-create-materialized-view[@scope=initiative].mdx new file mode 100644 index 0000000000..d699bb3515 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/applications/performant-dashboards/existing-oltp-db/2-create-materialized-view[@scope=initiative].mdx @@ -0,0 +1,108 @@ +--- +title: Create Materialized View (Initiative) +description: Create optimized materialized views for initiative-level dashboard queries +--- + +import { LanguageTabs, LanguageTabContent } from "@/components/mdx"; + +# Step 2: Create Materialized View (Initiative Scope) + +In this step, you'll create materialized views that pre-aggregate data from your OLTP database for fast dashboard queries, specifically tailored for initiative-level reporting. + +## Overview + +Materialized views store pre-computed query results, allowing dashboards to load instantly without querying your production OLTP database directly. This step shows you how to define and create these views. + +## Define Materialized View + + + +```typescript filename="dashboard-views.ts" +import { OlapMaterializedView, OlapTable } from "@514labs/moose-lib"; + +// Define the source table (from your OLTP DB) +interface OrdersTable { + id: string; + customer_id: string; + amount: number; + created_at: Date; +} + +// Define the materialized view +interface InitiativeSalesView { + date: Date; + initiative_id: string; + total_sales: number; + order_count: number; +} + +export const initiativeSalesView = new OlapMaterializedView( + "initiative_sales", + { + source: "orders", // References your OLTP table + query: ` + SELECT + toDate(created_at) as date, + initiative_id, + sum(amount) as total_sales, + count(*) as order_count + FROM orders + GROUP BY date, initiative_id + `, + refresh: "incremental", // Update as new data arrives + } +); +``` + + +```python filename="dashboard_views.py" +from moose_lib import OlapMaterializedView +from pydantic import BaseModel +from datetime import date + +# Define the materialized view +class InitiativeSalesView(BaseModel): + date: date + initiative_id: str + total_sales: float + order_count: int + +initiative_sales_view = OlapMaterializedView[InitiativeSalesView]( + "initiative_sales", + source="orders", + query=""" + SELECT + toDate(created_at) as date, + initiative_id, + sum(amount) as total_sales, + count(*) as order_count + FROM orders + GROUP BY date, initiative_id + """, + refresh="incremental", +) +``` + + + +## Apply the View + +Create the materialized view in your database: + +```bash +moose db migrate +``` + +## Query the View + +Once created, you can query the materialized view directly: + +```typescript +const results = await initiativeSalesView.select({ + date: { $gte: new Date("2024-01-01") }, +}); +``` + +## Next Steps + +Your materialized view is now ready! You can use it in your dashboard queries for fast, pre-aggregated data. diff --git a/apps/framework-docs-v2/content/guides/applications/performant-dashboards/existing-oltp-db/2-create-materialized-view[@scope=project].mdx b/apps/framework-docs-v2/content/guides/applications/performant-dashboards/existing-oltp-db/2-create-materialized-view[@scope=project].mdx new file mode 100644 index 0000000000..957905cba8 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/applications/performant-dashboards/existing-oltp-db/2-create-materialized-view[@scope=project].mdx @@ -0,0 +1,105 @@ +--- +title: Create Materialized View +description: Create optimized materialized views for dashboard queries +--- + +import { LanguageTabs, LanguageTabContent } from "@/components/mdx"; + +# Step 2: Create Materialized View + +In this step, you'll create materialized views that pre-aggregate data from your OLTP database for fast dashboard queries. + +## Overview + +Materialized views store pre-computed query results, allowing dashboards to load instantly without querying your production OLTP database directly. This step shows you how to define and create these views. + +## Define Materialized View + + + +```typescript filename="dashboard-views.ts" +import { OlapMaterializedView, OlapTable } from "@514labs/moose-lib"; + +// Define the source table (from your OLTP DB) +interface OrdersTable { + id: string; + customer_id: string; + amount: number; + created_at: Date; +} + +// Define the materialized view +interface DailySalesView { + date: Date; + total_sales: number; + order_count: number; +} + +export const dailySalesView = new OlapMaterializedView( + "daily_sales", + { + source: "orders", // References your OLTP table + query: ` + SELECT + toDate(created_at) as date, + sum(amount) as total_sales, + count(*) as order_count + FROM orders + GROUP BY date + `, + refresh: "incremental", // Update as new data arrives + } +); +``` + + +```python filename="dashboard_views.py" +from moose_lib import OlapMaterializedView +from pydantic import BaseModel +from datetime import date + +# Define the materialized view +class DailySalesView(BaseModel): + date: date + total_sales: float + order_count: int + +daily_sales_view = OlapMaterializedView[DailySalesView]( + "daily_sales", + source="orders", + query=""" + SELECT + toDate(created_at) as date, + sum(amount) as total_sales, + count(*) as order_count + FROM orders + GROUP BY date + """, + refresh="incremental", +) +``` + + + +## Apply the View + +Create the materialized view in your database: + +```bash +moose db migrate +``` + +## Query the View + +Once created, you can query the materialized view directly: + +```typescript +const results = await dailySalesView.select({ + date: { $gte: new Date("2024-01-01") }, +}); +``` + +## Next Steps + +Your materialized view is now ready! You can use it in your dashboard queries for fast, pre-aggregated data. + diff --git a/apps/framework-docs-v2/content/guides/applications/performant-dashboards/guide-overview.mdx b/apps/framework-docs-v2/content/guides/applications/performant-dashboards/guide-overview.mdx new file mode 100644 index 0000000000..f560deff1a --- /dev/null +++ b/apps/framework-docs-v2/content/guides/applications/performant-dashboards/guide-overview.mdx @@ -0,0 +1,24 @@ +--- +title: Performant Dashboards Overview +description: Overview of building high-performance dashboards with MooseStack +--- + +# Performant Dashboards Overview + +This guide covers best practices for building performant dashboards using MooseStack. + +## Overview + +Building dashboards that load quickly and provide real-time insights requires careful consideration of data modeling, query optimization, and caching strategies. + +## Key Concepts + +- Materialized views for pre-aggregated data +- Efficient query patterns +- Caching strategies +- Real-time data updates + +## Getting Started + +Select a starting point from the sidebar to begin implementing performant dashboards. + diff --git a/apps/framework-docs-v2/content/guides/applications/performant-dashboards/guide.toml b/apps/framework-docs-v2/content/guides/applications/performant-dashboards/guide.toml new file mode 100644 index 0000000000..b230fac9eb --- /dev/null +++ b/apps/framework-docs-v2/content/guides/applications/performant-dashboards/guide.toml @@ -0,0 +1,49 @@ +id = "performant-dashboards" +title = "Performant Dashboards Guide" + +[[options]] +id = "scope" +label = "Scope" +type = "select" + [[options.values]] + id = "initiative" + label = "Initiative" + + [[options.values]] + id = "project" + label = "Project" + +[[options]] +id = "starting-point" +label = "Starting Point" +type = "select" + [[options.values]] + id = "existing-oltp" + label = "Existing Postgres/MySQL" + + [[options.values]] + id = "scratch" + label = "From Scratch" + +[[options]] +id = "lang" +label = "Language" +type = "select" + [[options.values]] + id = "ts" + label = "TypeScript" + + [[options.values]] + id = "python" + label = "Python" + +# Flow Definitions +# Keys match the 'starting-point' values (primary branching logic) +[flows.existing-oltp] +stepsDir = "existing-oltp-db" +title = "Connect Existing DB" + +[flows.scratch] +stepsDir = "scratch" +title = "Build from Scratch" + diff --git a/apps/framework-docs-v2/content/guides/data-management/change-data-capture/guide-overview.mdx b/apps/framework-docs-v2/content/guides/data-management/change-data-capture/guide-overview.mdx new file mode 100644 index 0000000000..05a3182d76 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/data-management/change-data-capture/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: Change Data Capture Overview +description: Overview of implementing change data capture (CDC) with MooseStack +--- + +# Change Data Capture Overview + +This guide covers how to implement change data capture (CDC) to track and replicate database changes in real-time using MooseStack. + +## Overview + +Change Data Capture enables real-time synchronization and event-driven architectures by capturing database changes as they occur. + +## Getting Started + +Select a starting point from the sidebar to begin implementing CDC. + diff --git a/apps/framework-docs-v2/content/guides/data-management/impact-analysis/guide-overview.mdx b/apps/framework-docs-v2/content/guides/data-management/impact-analysis/guide-overview.mdx new file mode 100644 index 0000000000..e20651c381 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/data-management/impact-analysis/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: Impact Analysis Overview +description: Overview of analyzing the impact of schema changes +--- + +# Impact Analysis Overview + +This guide covers how to analyze the impact of schema changes before applying them. + +## Overview + +Impact analysis helps you understand how schema changes will affect your queries, applications, and downstream systems. + +## Getting Started + +Select a starting point from the sidebar to begin analyzing schema changes. + diff --git a/apps/framework-docs-v2/content/guides/data-management/migrations/guide-overview.mdx b/apps/framework-docs-v2/content/guides/data-management/migrations/guide-overview.mdx new file mode 100644 index 0000000000..26c1088877 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/data-management/migrations/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: Migrations Overview +description: Overview of managing database migrations with MooseStack +--- + +# Migrations Overview + +This guide covers best practices for managing database migrations in MooseStack projects. + +## Overview + +Database migrations allow you to evolve your schema safely and track changes over time. + +## Getting Started + +Select a starting point from the sidebar to begin managing migrations. + diff --git a/apps/framework-docs-v2/content/guides/data-warehousing/connectors/guide-overview.mdx b/apps/framework-docs-v2/content/guides/data-warehousing/connectors/guide-overview.mdx new file mode 100644 index 0000000000..7a7c174e2b --- /dev/null +++ b/apps/framework-docs-v2/content/guides/data-warehousing/connectors/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: Connectors Overview +description: Overview of using connectors to integrate data sources with MooseStack +--- + +# Connectors Overview + +This guide covers how to use connectors to integrate various data sources with your MooseStack data warehouse. + +## Overview + +Connectors enable seamless integration with databases, APIs, and other data sources. + +## Getting Started + +Select a starting point from the sidebar to begin using connectors. + diff --git a/apps/framework-docs-v2/content/guides/data-warehousing/customer-data-platform/guide-overview.mdx b/apps/framework-docs-v2/content/guides/data-warehousing/customer-data-platform/guide-overview.mdx new file mode 100644 index 0000000000..ca95c8f960 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/data-warehousing/customer-data-platform/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: Customer Data Platform Overview +description: Overview of building a customer data platform with MooseStack +--- + +# Customer Data Platform Overview + +This guide shows you how to build a customer data platform (CDP) using MooseStack. + +## Overview + +A customer data platform unifies customer data from multiple sources to create a comprehensive view of each customer. + +## Getting Started + +Select a starting point from the sidebar to begin building your customer data platform. + diff --git a/apps/framework-docs-v2/content/guides/data-warehousing/operational-analytics/guide-overview.mdx b/apps/framework-docs-v2/content/guides/data-warehousing/operational-analytics/guide-overview.mdx new file mode 100644 index 0000000000..48c542f72b --- /dev/null +++ b/apps/framework-docs-v2/content/guides/data-warehousing/operational-analytics/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: Operational Analytics Overview +description: Overview of implementing operational analytics with MooseStack +--- + +# Operational Analytics Overview + +This guide covers how to implement operational analytics to monitor and optimize your business operations. + +## Overview + +Operational analytics provides real-time insights into business processes, infrastructure, and application performance. + +## Getting Started + +Select a starting point from the sidebar to begin implementing operational analytics. + diff --git a/apps/framework-docs-v2/content/guides/data-warehousing/pipelines/guide-overview.mdx b/apps/framework-docs-v2/content/guides/data-warehousing/pipelines/guide-overview.mdx new file mode 100644 index 0000000000..cf1c8d1c36 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/data-warehousing/pipelines/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: Pipelines Overview +description: Overview of building data pipelines with MooseStack +--- + +# Pipelines Overview + +This guide covers how to build and manage data pipelines using MooseStack to transform, aggregate, and move data through your data warehouse. + +## Overview + +Data pipelines automate the flow of data from source systems to your data warehouse, enabling reliable and scalable data processing. + +## Getting Started + +Select a starting point from the sidebar to begin building data pipelines. + diff --git a/apps/framework-docs-v2/content/guides/data-warehousing/startup-metrics/guide-overview.mdx b/apps/framework-docs-v2/content/guides/data-warehousing/startup-metrics/guide-overview.mdx new file mode 100644 index 0000000000..dbf399285d --- /dev/null +++ b/apps/framework-docs-v2/content/guides/data-warehousing/startup-metrics/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: Startup Metrics Overview +description: Overview of tracking key startup metrics with MooseStack +--- + +# Startup Metrics Overview + +This guide covers how to track and analyze key startup metrics using MooseStack. + +## Overview + +Startup metrics help you measure product growth, user engagement, and business performance. + +## Getting Started + +Select a starting point from the sidebar to begin tracking startup metrics. + diff --git a/apps/framework-docs-v2/content/guides/methodology/data-as-code/guide-overview.mdx b/apps/framework-docs-v2/content/guides/methodology/data-as-code/guide-overview.mdx new file mode 100644 index 0000000000..ce655f24ae --- /dev/null +++ b/apps/framework-docs-v2/content/guides/methodology/data-as-code/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: Data as Code Overview +description: Overview of implementing data as code practices with MooseStack +--- + +# Data as Code Overview + +This guide covers how to implement data as code practices using MooseStack's code-first approach. + +## Overview + +Data as code enables version control, collaboration, and automated deployment of data infrastructure. + +## Getting Started + +Select a starting point from the sidebar to begin implementing data as code. + diff --git a/apps/framework-docs-v2/content/guides/methodology/dora-for-data/guide-overview.mdx b/apps/framework-docs-v2/content/guides/methodology/dora-for-data/guide-overview.mdx new file mode 100644 index 0000000000..5371fb57d2 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/methodology/dora-for-data/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: DORA for Data Overview +description: Overview of implementing DORA metrics for data engineering teams +--- + +# DORA for Data Overview + +This guide shows you how to implement DORA (DevOps Research and Assessment) metrics for data engineering teams. + +## Overview + +DORA metrics help data engineering teams measure and improve their delivery performance. + +## Getting Started + +Select a starting point from the sidebar to begin implementing DORA metrics. + diff --git a/apps/framework-docs-v2/content/guides/strategy/ai-enablement/guide-overview.mdx b/apps/framework-docs-v2/content/guides/strategy/ai-enablement/guide-overview.mdx new file mode 100644 index 0000000000..9fe85df641 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/strategy/ai-enablement/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: AI Enablement Overview +description: Overview of enabling AI capabilities with MooseStack +--- + +# AI Enablement Overview + +This guide covers how to enable AI capabilities in your data stack using MooseStack. + +## Overview + +AI enablement requires robust data infrastructure, vector search capabilities, and seamless LLM integration. + +## Getting Started + +Select a starting point from the sidebar to begin enabling AI capabilities. + diff --git a/apps/framework-docs-v2/content/guides/strategy/data-foundation/guide-overview.mdx b/apps/framework-docs-v2/content/guides/strategy/data-foundation/guide-overview.mdx new file mode 100644 index 0000000000..2f278fbdc9 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/strategy/data-foundation/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: Data Foundation Overview +description: Overview of building a solid data foundation for your organization +--- + +# Data Foundation Overview + +This guide covers how to build a solid data foundation that enables reliable, scalable, and maintainable data operations. + +## Overview + +A strong data foundation is essential for long-term success with data-driven decision making. + +## Getting Started + +Select a starting point from the sidebar to begin building your data foundation. + diff --git a/apps/framework-docs-v2/content/guides/strategy/olap-evaluation/guide-overview.mdx b/apps/framework-docs-v2/content/guides/strategy/olap-evaluation/guide-overview.mdx new file mode 100644 index 0000000000..881e730a32 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/strategy/olap-evaluation/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: OLAP Evaluation Overview +description: Overview of evaluating OLAP databases and systems for your use case +--- + +# OLAP Evaluation Overview + +This guide covers how to evaluate OLAP databases and systems to choose the right solution for your analytical workloads. + +## Overview + +Choosing the right OLAP database requires careful evaluation of performance, scale, and feature requirements. + +## Getting Started + +Select a starting point from the sidebar to begin evaluating OLAP systems. + diff --git a/apps/framework-docs-v2/content/guides/strategy/platform-engineering/guide-overview.mdx b/apps/framework-docs-v2/content/guides/strategy/platform-engineering/guide-overview.mdx new file mode 100644 index 0000000000..1f5cb01229 --- /dev/null +++ b/apps/framework-docs-v2/content/guides/strategy/platform-engineering/guide-overview.mdx @@ -0,0 +1,17 @@ +--- +title: Platform Engineering Overview +description: Overview of implementing platform engineering practices for data infrastructure +--- + +# Platform Engineering Overview + +This guide covers how to implement platform engineering practices to build and maintain self-service data infrastructure. + +## Overview + +Platform engineering enables teams to build and operate data infrastructure more efficiently through self-service tools and automation. + +## Getting Started + +Select a starting point from the sidebar to begin implementing platform engineering practices. + 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/content/moosestack/migrate/index.mdx b/apps/framework-docs-v2/content/moosestack/migrate/index.mdx index 28bdfee785..bb2fbc6034 100644 --- a/apps/framework-docs-v2/content/moosestack/migrate/index.mdx +++ b/apps/framework-docs-v2/content/moosestack/migrate/index.mdx @@ -371,6 +371,6 @@ You'll still get migrations for all other schema changes (adding tables, modifyi - Verify object definitions in your main file - Ensure all required fields are properly typed - **Stuck migration lock**: If you see "Migration already in progress" but no migration is running, wait 5 minutes for automatic expiry or manually clear it: - ```sql + ```sql copy=false DELETE FROM _MOOSE_STATE WHERE key = 'migration_lock'; ``` 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/next.config.js b/apps/framework-docs-v2/next.config.js index 0214205314..ddd0c45aa7 100644 --- a/apps/framework-docs-v2/next.config.js +++ b/apps/framework-docs-v2/next.config.js @@ -10,6 +10,13 @@ const createWithVercelToolbar = require("@vercel/toolbar/plugins/next"); /** @type {import('next').NextConfig} */ const nextConfig = { + // Based on the provided documentation, cacheComponents is a root-level option + cacheComponents: true, + + experimental: { + // Removing dynamicIO as it caused an error and might be implied or renamed + }, + reactStrictMode: true, pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"], images: { diff --git a/apps/framework-docs-v2/package.json b/apps/framework-docs-v2/package.json index ebb96fdf62..1ec487751a 100644 --- a/apps/framework-docs-v2/package.json +++ b/apps/framework-docs-v2/package.json @@ -21,13 +21,15 @@ "@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-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", @@ -37,6 +39,8 @@ "@radix-ui/react-use-controllable-state": "^1.2.2", "@shikijs/transformers": "^3.14.0", "@tabler/icons-react": "^3.35.0", + "@tanstack/react-form": "^1.25.0", + "@tanstack/zod-adapter": "^1.136.18", "@types/mdx": "^2.0.13", "@vercel/toolbar": "^0.1.41", "class-variance-authority": "^0.7.1", @@ -62,7 +66,9 @@ "shiki": "^3.14.0", "sonner": "^2.0.7", "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "unist-util-visit": "^5.0.0", + "zod": "^3.25.76" }, "devDependencies": { "@repo/eslint-config-custom": "workspace:*", diff --git a/apps/framework-docs-v2/public/robots.txt b/apps/framework-docs-v2/public/robots.txt index 2fe0e16a59..a3a8c086e2 100644 --- a/apps/framework-docs-v2/public/robots.txt +++ b/apps/framework-docs-v2/public/robots.txt @@ -3,7 +3,7 @@ User-agent: * Allow: / # Host -Host: https://docs.moosestack.com +Host: https://docs.fiveonefour.com # Sitemaps -Sitemap: https://docs.moosestack.com/sitemap.xml +Sitemap: https://docs.fiveonefour.com/sitemap.xml diff --git a/apps/framework-docs-v2/public/sitemap-0.xml b/apps/framework-docs-v2/public/sitemap-0.xml index 102b6d1480..88aaa0ea28 100644 --- a/apps/framework-docs-v2/public/sitemap-0.xml +++ b/apps/framework-docs-v2/public/sitemap-0.xml @@ -1,114 +1,154 @@ -https://docs.moosestack.com/ai2025-11-10T02:40:55.309Zdaily0.7 -https://docs.moosestack.com/ai/data-collection-policy2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/demos/context2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/demos/dlqs2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/demos/egress2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/demos/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/demos/ingest2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/demos/model-data2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/demos/mvs2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/getting-started/claude2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/getting-started/cursor2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/getting-started/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/getting-started/other-clients2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/getting-started/vs-code2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/getting-started/windsurf2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/guides/clickhouse-chat2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/guides/clickhouse-proj2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/guides/from-template2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/guides/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/overview2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/reference/cli-reference2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/reference/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/reference/mcp-json-reference2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/ai/reference/tool-reference2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/hosting2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/hosting/deployment2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/hosting/getting-started2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/hosting/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/hosting/overview2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/apis/admin-api2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/apis/analytics-api2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/apis/auth2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/apis/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/apis/ingest-api2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/apis/openapi-sdk2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/apis/trigger-api2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/app-api-frameworks/express2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/app-api-frameworks/fastapi2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/app-api-frameworks/fastify2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/app-api-frameworks/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/app-api-frameworks/koa2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/app-api-frameworks/nextjs2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/app-api-frameworks/raw-nodejs2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/changelog2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/configuration2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/contribution/documentation2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/contribution/framework2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/data-modeling2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/data-sources2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/deploying/configuring-moose-for-cloud2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/deploying/deploying-on-an-offline-server2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/deploying/deploying-on-ecs2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/deploying/deploying-on-kubernetes2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/deploying/deploying-with-docker-compose2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/deploying/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/deploying/monitoring2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/deploying/packaging-moose-for-deployment2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/deploying/preparing-clickhouse-redpanda2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/getting-started/from-clickhouse2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/getting-started/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/getting-started/quickstart2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/help/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/help/minimum-requirements2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/help/troubleshooting2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/in-your-stack2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/local-dev-environment2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/metrics2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/migrate/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/migrate/lifecycle2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/migrate/migration-types2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/moose-cli2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/moosedev-mcp2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/apply-migrations2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/db-pull2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/external-tables2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/indexes2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/insert-data2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/model-materialized-view2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/model-table2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/model-view2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/planned-migrations2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/read-data2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/schema-change2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/schema-optimization2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/schema-versioning2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/supported-types2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/olap/ttl2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/overview2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/quickstart2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/reference/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/streaming/connect-cdc2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/streaming/consumer-functions2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/streaming/create-stream2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/streaming/dead-letter-queues2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/streaming/from-your-code2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/streaming/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/streaming/schema-registry2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/streaming/sync-to-table2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/streaming/transform-functions2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/templates-examples2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/workflows/cancel-workflow2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/workflows/define-workflow2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/workflows/index2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/workflows/retries-and-timeouts2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/workflows/schedule-workflow2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com/moosestack/workflows/trigger-workflow2025-11-10T02:40:55.310Zdaily0.7 -https://docs.moosestack.com2025-11-10T02:40:55.310Zdaily0.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/public/sitemap.xml b/apps/framework-docs-v2/public/sitemap.xml index b7449c4902..c30ae08b51 100644 --- a/apps/framework-docs-v2/public/sitemap.xml +++ b/apps/framework-docs-v2/public/sitemap.xml @@ -1,4 +1,4 @@ -https://docs.moosestack.com/sitemap-0.xml +https://docs.fiveonefour.com/sitemap-0.xml \ No newline at end of file diff --git a/apps/framework-docs-v2/src/app/[...slug]/page.tsx b/apps/framework-docs-v2/src/app/(docs)/[...slug]/page.tsx similarity index 81% rename from apps/framework-docs-v2/src/app/[...slug]/page.tsx rename to apps/framework-docs-v2/src/app/(docs)/[...slug]/page.tsx index 35a58dc1d6..1edb56e23b 100644 --- a/apps/framework-docs-v2/src/app/[...slug]/page.tsx +++ b/apps/framework-docs-v2/src/app/(docs)/[...slug]/page.tsx @@ -6,7 +6,7 @@ import { MDXRenderer } from "@/components/mdx-renderer"; import { DocBreadcrumbs } from "@/components/navigation/doc-breadcrumbs"; import { buildDocBreadcrumbs } from "@/lib/breadcrumbs"; -export const dynamic = "force-dynamic"; +// export const dynamic = "force-dynamic"; interface PageProps { params: Promise<{ @@ -18,18 +18,22 @@ interface PageProps { export async function generateStaticParams() { const slugs = getAllSlugs(); + // Filter out templates and guides slugs (they have their own explicit pages) + const filteredSlugs = slugs.filter( + (slug) => !slug.startsWith("templates/") && !slug.startsWith("guides/"), + ); + // Generate params for each slug - const allParams: { slug: string[] }[] = slugs.map((slug) => ({ + const allParams: { slug: string[] }[] = filteredSlugs.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) + // Note: templates and guides are now explicit pages, so they're excluded here allParams.push( { slug: ["moosestack"] }, { slug: ["ai"] }, { slug: ["hosting"] }, - { slug: ["templates"] }, ); return allParams; @@ -81,6 +85,11 @@ export default async function DocPage({ params }: PageProps) { const slug = slugArray.join("/"); + // Templates and guides are now explicit pages, so they should not be handled by this catch-all route + if (slug.startsWith("templates/") || slug.startsWith("guides/")) { + notFound(); + } + let content; try { content = await parseMarkdownContent(slug); diff --git a/apps/framework-docs-v2/src/app/(docs)/guides/[...slug]/page.tsx b/apps/framework-docs-v2/src/app/(docs)/guides/[...slug]/page.tsx new file mode 100644 index 0000000000..3e112e50cc --- /dev/null +++ b/apps/framework-docs-v2/src/app/(docs)/guides/[...slug]/page.tsx @@ -0,0 +1,246 @@ +import { notFound } from "next/navigation"; +import type { Metadata } from "next"; +import { + getAllSlugs, + parseMarkdownContent, + discoverStepFiles, +} 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"; +import { GuideStepsWrapper } from "@/components/guides/guide-steps-wrapper"; +import { DynamicGuideBuilder } from "@/components/guides/dynamic-guide-builder"; +import { parseGuideManifest, getCachedGuideSteps } from "@/lib/guide-content"; + +// export const dynamic = "force-dynamic"; + +interface PageProps { + params: Promise<{ + slug: string[]; + }>; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +} + +export async function generateStaticParams() { + // Get all slugs and filter for guides + const slugs = getAllSlugs(); + + // Filter for guides slugs and generate params + const guideSlugs = slugs.filter((slug) => slug.startsWith("guides/")); + + // Remove the "guides/" prefix and split into array + const allParams: { slug: string[] }[] = guideSlugs + .map((slug) => slug.replace(/^guides\//, "")) + .filter((slug) => slug !== "index") // Exclude the index page + .map((slug) => ({ + slug: slug.split("/"), + })); + + return allParams; +} + +export async function generateMetadata({ + params, +}: PageProps): Promise { + const resolvedParams = await params; + const slugArray = resolvedParams.slug; + + // Handle empty slug array (shouldn't happen with [...slug] but be safe) + if (!slugArray || slugArray.length === 0) { + return { + title: "Guides | MooseStack Documentation", + description: + "Comprehensive guides for building applications, managing data, and implementing data warehousing strategies", + }; + } + + const slug = `guides/${slugArray.join("/")}`; + + try { + const content = await parseMarkdownContent(slug); + return { + title: + content.frontMatter.title ? + `${content.frontMatter.title} | MooseStack Documentation` + : "Guides | MooseStack Documentation", + description: + content.frontMatter.description || + "Comprehensive guides for building applications, managing data, and implementing data warehousing strategies", + }; + } catch (error) { + return { + title: "Guides | MooseStack Documentation", + description: + "Comprehensive guides for building applications, managing data, and implementing data warehousing strategies", + }; + } +} + +export default async function GuidePage({ params, searchParams }: PageProps) { + const resolvedParams = await params; + const resolvedSearchParams = await searchParams; + const slugArray = resolvedParams.slug; + + // Handle empty slug array (shouldn't happen with [...slug] but be safe) + if (!slugArray || slugArray.length === 0) { + notFound(); + } + + const slug = `guides/${slugArray.join("/")}`; + + let content; + try { + content = await parseMarkdownContent(slug); + } catch (error) { + notFound(); + } + + const breadcrumbs = buildDocBreadcrumbs( + slug, + typeof content.frontMatter.title === "string" ? + content.frontMatter.title + : undefined, + ); + + // Check if this is a dynamic guide by checking for guide.toml + const guideManifest = await parseGuideManifest(slug); + + if (guideManifest) { + // DYNAMIC GUIDE LOGIC + + // Flatten search params to Record for our cache function + const queryParams: Record = {}; + Object.entries(resolvedSearchParams).forEach(([key, value]) => { + if (typeof value === "string") { + queryParams[key] = value; + } else if (Array.isArray(value) && value.length > 0 && value[0]) { + // Take first value if array + queryParams[key] = value[0]; + } + }); + + // Fetch steps here (cached function) + const steps = await getCachedGuideSteps(slug, queryParams); + + const allHeadings = [...content.headings]; + if (steps.length > 0) { + // Add steps as headings in TOC, avoiding duplicates + const existingIds = new Set(allHeadings.map((h) => h.id)); + steps.forEach((step) => { + const stepId = `step-${step.stepNumber}`; + // Only add if ID doesn't already exist + if (!existingIds.has(stepId)) { + allHeadings.push({ + level: 2, + text: `${step.stepNumber}. ${step.title}`, + id: stepId, + }); + existingIds.add(stepId); + } + }); + } + + return ( + <> +
+ +
+ {content.isMDX ? + + :
} +
+ + + + {steps.length > 0 ? + step)} + stepsWithContent={steps} + currentSlug={slug} + /> + :
+ No steps found for this configuration. Please try different + options. +
+ } +
+ + + ); + } + + // STATIC GUIDE LOGIC (Fallback) + + // Discover step files for this starting point page + const steps = discoverStepFiles(slug); + + // Load step content server-side and pre-render MDX + const stepsWithContent = await Promise.all( + steps.map(async (step) => { + try { + const stepContent = await parseMarkdownContent(step.slug); + return { + ...step, + content: stepContent.content, + isMDX: stepContent.isMDX ?? false, + }; + } catch (error) { + console.error(`Failed to load step ${step.slug}:`, error); + return { + ...step, + content: null, + isMDX: false, + }; + } + }), + ); + + // Combine page headings with step headings for TOC + const allHeadings = [...content.headings]; + if (steps.length > 0) { + // Add steps as headings in TOC, avoiding duplicates + const existingIds = new Set(allHeadings.map((h) => h.id)); + steps.forEach((step) => { + const stepId = `step-${step.stepNumber}`; + // Only add if ID doesn't already exist + if (!existingIds.has(stepId)) { + allHeadings.push({ + level: 2, + text: `${step.stepNumber}. ${step.title}`, + id: stepId, + }); + existingIds.add(stepId); + } + }); + } + + return ( + <> +
+ +
+ {content.isMDX ? + + :
} +
+ {steps.length > 0 && ( + step, + )} + stepsWithContent={stepsWithContent} + currentSlug={slug} + /> + )} +
+ + + ); +} diff --git a/apps/framework-docs-v2/src/app/(docs)/guides/page.tsx b/apps/framework-docs-v2/src/app/(docs)/guides/page.tsx new file mode 100644 index 0000000000..d7261b2c69 --- /dev/null +++ b/apps/framework-docs-v2/src/app/(docs)/guides/page.tsx @@ -0,0 +1,63 @@ +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/index"); + return { + title: + content.frontMatter.title ? + `${content.frontMatter.title} | MooseStack Documentation` + : "Guides | MooseStack Documentation", + description: + content.frontMatter.description || + "Comprehensive guides for building applications, managing data, and implementing data warehousing strategies", + }; + } catch (error) { + return { + title: "Guides | MooseStack Documentation", + description: + "Comprehensive guides for building applications, managing data, and implementing data warehousing strategies", + }; + } +} + +export default async function GuidesPage() { + let content; + try { + content = await parseMarkdownContent("guides/index"); + } catch (error) { + notFound(); + } + + const breadcrumbs = buildDocBreadcrumbs( + "guides/index", + typeof content.frontMatter.title === "string" ? + content.frontMatter.title + : undefined, + ); + + return ( + <> +
+ +
+ {content.isMDX ? + + :
} +
+
+ + + ); +} diff --git a/apps/framework-docs-v2/src/app/[...slug]/layout.tsx b/apps/framework-docs-v2/src/app/(docs)/layout.tsx similarity index 74% rename from apps/framework-docs-v2/src/app/[...slug]/layout.tsx rename to apps/framework-docs-v2/src/app/(docs)/layout.tsx index 043aa12216..fb0caf00fa 100644 --- a/apps/framework-docs-v2/src/app/[...slug]/layout.tsx +++ b/apps/framework-docs-v2/src/app/(docs)/layout.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from "react"; import { Suspense } from "react"; +import { headers } from "next/headers"; import { SideNav } from "@/components/navigation/side-nav"; import { AnalyticsProvider } from "@/components/analytics-provider"; import { SidebarInset } from "@/components/ui/sidebar"; @@ -7,20 +8,22 @@ import { showDataSourcesPage } from "@/flags"; interface DocLayoutProps { children: ReactNode; - params: Promise<{ - slug?: string[]; - }>; } async function FilteredSideNav() { // Evaluate feature flag + // Note: Accessing headers() in the parent component marks this as dynamic, + // which allows Date.now() usage in the flags SDK const showDataSources = await showDataSourcesPage().catch(() => false); // Pass flag to SideNav, which will filter navigation items after language filtering return ; } -export default async function DocLayout({ children, params }: DocLayoutProps) { +export default async function DocLayout({ children }: DocLayoutProps) { + // Access headers() to mark this layout as dynamic, which allows Date.now() usage + // in the flags SDK without triggering Next.js static generation errors + await headers(); return (
diff --git a/apps/framework-docs-v2/src/app/api/templates/route.ts b/apps/framework-docs-v2/src/app/api/templates/route.ts index 4b4b63504e..9a2d2fe393 100644 --- a/apps/framework-docs-v2/src/app/api/templates/route.ts +++ b/apps/framework-docs-v2/src/app/api/templates/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { getAllItems } from "@/lib/templates"; -export const dynamic = "force-static"; +// export const dynamic = "force-static"; export async function GET() { try { diff --git a/apps/framework-docs-v2/src/app/layout.tsx b/apps/framework-docs-v2/src/app/layout.tsx index 967ccb9aa6..036d2e3fdd 100644 --- a/apps/framework-docs-v2/src/app/layout.tsx +++ b/apps/framework-docs-v2/src/app/layout.tsx @@ -1,16 +1,13 @@ import type { Metadata } from "next"; import type { ReactNode } from "react"; import { Suspense } from "react"; -import { cookies } from "next/headers"; import "@/styles/globals.css"; import { ThemeProvider } from "@/components/theme-provider"; import { LanguageProviderWrapper } from "@/components/language-provider-wrapper"; -import { TopNav } from "@/components/navigation/top-nav"; +import { TopNavWithFlags } from "@/components/navigation/top-nav-with-flags"; import { SidebarProvider } from "@/components/ui/sidebar"; import { Toaster } from "@/components/ui/sonner"; import { ScrollRestoration } from "@/components/scroll-restoration"; -import { getGitHubStars } from "@/lib/github-stars"; -import { showHostingSection, showGuidesSection, showAiSection } from "@/flags"; import { VercelToolbar } from "@vercel/toolbar/next"; export const metadata: Metadata = { @@ -19,22 +16,13 @@ export const metadata: Metadata = { }; // Force dynamic to enable cookie-based flag overrides -export const dynamic = "force-dynamic"; +// export const dynamic = "force-dynamic"; export default async function RootLayout({ children, }: Readonly<{ children: ReactNode; }>) { - const stars = await getGitHubStars(); - - // Evaluate feature flags (reads cookies automatically for overrides) - const [showHosting, showGuides, showAi] = await Promise.all([ - showHostingSection().catch(() => false), - showGuidesSection().catch(() => false), - showAiSection().catch(() => true), - ]); - const shouldInjectToolbar = process.env.NODE_ENV === "development"; return ( @@ -51,14 +39,7 @@ export default async function RootLayout({
- }> - - + {children}
diff --git a/apps/framework-docs-v2/src/app/page.tsx b/apps/framework-docs-v2/src/app/page.tsx index 6d280ef3c0..d65e2187e3 100644 --- a/apps/framework-docs-v2/src/app/page.tsx +++ b/apps/framework-docs-v2/src/app/page.tsx @@ -11,7 +11,7 @@ import { IconDatabase, IconCloud, IconSparkles } from "@tabler/icons-react"; import { showHostingSection, showAiSection } from "@/flags"; import { cn } from "@/lib/utils"; -export const dynamic = "force-dynamic"; +// export const dynamic = "force-dynamic"; export default async function HomePage() { // Evaluate feature flags 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..0bae069442 --- /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/guides/dynamic-guide-builder.tsx b/apps/framework-docs-v2/src/components/guides/dynamic-guide-builder.tsx new file mode 100644 index 0000000000..0b4b4b19e1 --- /dev/null +++ b/apps/framework-docs-v2/src/components/guides/dynamic-guide-builder.tsx @@ -0,0 +1,17 @@ +"use client"; + +import React from "react"; +import { GuideForm } from "./guide-form"; +import { GuideManifest } from "@/lib/guide-types"; + +interface DynamicGuideBuilderProps { + manifest: GuideManifest; +} + +export function DynamicGuideBuilder({ manifest }: DynamicGuideBuilderProps) { + return ( +
+ +
+ ); +} diff --git a/apps/framework-docs-v2/src/components/guides/guide-form.tsx b/apps/framework-docs-v2/src/components/guides/guide-form.tsx new file mode 100644 index 0000000000..1849a6a6f5 --- /dev/null +++ b/apps/framework-docs-v2/src/components/guides/guide-form.tsx @@ -0,0 +1,213 @@ +"use client"; + +import * as React from "react"; +import { useForm } from "@tanstack/react-form"; +import { useRouter, useSearchParams, usePathname } from "next/navigation"; +import { z } from "zod"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { GuideManifest } from "@/lib/guide-types"; +import { useLanguage } from "@/hooks/use-language"; + +interface GuideFormProps { + manifest: GuideManifest; +} + +export function GuideForm({ manifest }: GuideFormProps) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const { language } = useLanguage(); + + // Create a dynamic schema based on the manifest options + const schema = React.useMemo(() => { + const shape: Record = {}; + manifest.options.forEach((option) => { + shape[option.id] = z.string().min(1, "Selection is required"); + }); + return z.object(shape); + }, [manifest]); + + // Initialize default values from URL search params, manifest defaults, or language preference + const defaultValues = React.useMemo(() => { + const values: Record = {}; + manifest.options.forEach((option) => { + const paramValue = searchParams.get(option.id); + if (paramValue) { + values[option.id] = paramValue; + } else if (option.id === "lang") { + // For language, use the global language preference (from localStorage or default to typescript) + values[option.id] = language === "typescript" ? "ts" : "python"; + } else if (option.defaultValue) { + values[option.id] = option.defaultValue; + } else { + // Default to first value if available, to ensure we have a valid state if possible + if (option.values.length > 0 && option.values[0]) { + values[option.id] = option.values[0].id; + } + } + }); + return values; + }, [manifest, searchParams, language]); + + const form = useForm({ + defaultValues, + onSubmit: async ({ value }) => { + // Validate before submitting + const result = schema.safeParse(value); + if (!result.success) { + console.error("Form validation failed:", result.error); + return; + } + // Update URL with new values, preserving lang param + const params = new URLSearchParams(searchParams.toString()); + Object.entries(value).forEach(([key, val]) => { + if (val) { + params.set(key, val as string); + } else { + params.delete(key); + } + }); + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, + }); + + // Update form when guide-related searchParams change + React.useEffect(() => { + const currentValues: Record = {}; + let hasGuideParams = false; + + manifest.options.forEach((option) => { + const paramValue = searchParams.get(option.id); + if (paramValue) { + currentValues[option.id] = paramValue; + hasGuideParams = true; + } else if (option.id === "lang") { + // Sync language from global preference if not in URL + currentValues[option.id] = language === "typescript" ? "ts" : "python"; + hasGuideParams = true; + } + }); + + // Only update if we have guide params and they differ from form state + if (hasGuideParams) { + const needsUpdate = manifest.options.some((option) => { + const currentValue = currentValues[option.id]; + const formValue = form.state.values[option.id] as string | undefined; + return currentValue && currentValue !== formValue; + }); + + if (needsUpdate) { + Object.entries(currentValues).forEach(([key, value]) => { + form.setFieldValue(key, value); + }); + } + } + }, [searchParams, manifest, language]); + + return ( +
+
+

Customize Your Guide

+

+ Select your stack preferences to get a tailored guide. +

+
+ +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="grid gap-6 md:grid-cols-2 lg:grid-cols-3" + > + {manifest.options.map((option) => ( + { + // Handle language option specially - sync with global language preference + const handleLanguageChange = (value: string) => { + field.handleChange(value); + // Map guide form language values to URL lang param + const langParam = value === "ts" ? "typescript" : "python"; + const params = new URLSearchParams(searchParams.toString()); + params.set("lang", langParam); + // Preserve all other guide params + manifest.options.forEach((opt) => { + if (opt.id !== option.id && form.state.values[opt.id]) { + params.set(opt.id, form.state.values[opt.id] as string); + } + }); + router.push(`${pathname}?${params.toString()}`, { + scroll: false, + }); + }; + + const handleOtherChange = (value: string) => { + field.handleChange(value); + // Update URL directly without going through form submit to avoid loops + const params = new URLSearchParams(searchParams.toString()); + params.set(option.id, value); + // Preserve all other guide params (including lang) + manifest.options.forEach((opt) => { + if (opt.id !== option.id && form.state.values[opt.id]) { + params.set(opt.id, form.state.values[opt.id] as string); + } + }); + // Preserve lang param + const langParam = searchParams.get("lang"); + if (langParam) { + params.set("lang", langParam); + } + router.push(`${pathname}?${params.toString()}`, { + scroll: false, + }); + }; + + return ( +
+ + + {( + field.state.meta.isTouched && field.state.meta.errors.length + ) ? +

+ {field.state.meta.errors.join(", ")} +

+ : null} +
+ ); + }} + /> + ))} + +
+ ); +} diff --git a/apps/framework-docs-v2/src/components/guides/guide-steps-nav-buttons.tsx b/apps/framework-docs-v2/src/components/guides/guide-steps-nav-buttons.tsx new file mode 100644 index 0000000000..dc810aa1af --- /dev/null +++ b/apps/framework-docs-v2/src/components/guides/guide-steps-nav-buttons.tsx @@ -0,0 +1,47 @@ +"use client"; + +import * as React from "react"; +import { Button } from "@/components/ui/button"; +import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react"; + +interface GuideStepsNavButtonsProps { + currentStepIndex: number; + totalSteps: number; + onPrevious: () => void; + onNext: () => void; +} + +export function GuideStepsNavButtons({ + currentStepIndex, + totalSteps, + onPrevious, + onNext, +}: GuideStepsNavButtonsProps) { + const hasPrevious = currentStepIndex > 0; + const hasNext = currentStepIndex < totalSteps - 1; + + return ( +
+ + +
+ ); +} 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..6d24116b76 --- /dev/null +++ b/apps/framework-docs-v2/src/components/guides/guide-steps-wrapper.tsx @@ -0,0 +1,98 @@ +import { Suspense } from "react"; +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; +} + +async function StepContentWrapper({ + content, + isMDX, + slug, + index, +}: { + content: string; + isMDX: boolean; + slug: string; + index: number; +}) { + return ( +
+ +
+ ); +} + +export function GuideStepsWrapper({ + steps, + stepsWithContent, + currentSlug, +}: GuideStepsWrapperProps) { + // Render all step content with Suspense for async MDX rendering + const renderedSteps = stepsWithContent.map((step, index) => { + if (!step.content) return null; + return ( + +
Loading step content...
+
+ } + > + + + ); + }); + + 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..c32fbfe46b --- /dev/null +++ b/apps/framework-docs-v2/src/components/guides/guide-steps.tsx @@ -0,0 +1,185 @@ +"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]; + if (!currentStep) return null; + + 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/mdx-renderer.tsx b/apps/framework-docs-v2/src/components/mdx-renderer.tsx index cb30fc5550..9e7e80d5b6 100644 --- a/apps/framework-docs-v2/src/components/mdx-renderer.tsx +++ b/apps/framework-docs-v2/src/components/mdx-renderer.tsx @@ -27,40 +27,32 @@ 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"; import { Separator } from "@/components/ui/separator"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; +import { IconTerminal, IconFileCode } from "@tabler/icons-react"; import { - IconTerminal, - IconFileCode, - IconRocket, - IconDatabase, - IconDeviceLaptop, - IconBrandGithub, - IconInfoCircle, - IconCheck, - IconClock, -} from "@tabler/icons-react"; -import { - MDXPre, - MDXCode, - MDXFigure, -} from "@/components/mdx/code-block-wrapper"; -import { PathConfig } from "@/lib/path-config"; + 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 { rehypeCodeMeta } from "@/lib/rehype-code-meta"; interface MDXRendererProps { source: string; } export async function MDXRenderer({ source }: MDXRendererProps) { + "use cache"; // Create FileTree with nested components const FileTreeWithSubcomponents = Object.assign(FileTree, { Folder: FileTreeFolder, @@ -120,6 +112,7 @@ export async function MDXRenderer({ source }: MDXRendererProps) { Security, BreakingChanges, TemplatesGridServer, + CommandSnippet, CodeEditor, Separator, Tabs, @@ -132,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 ( @@ -153,10 +146,10 @@ export async function MDXRenderer({ source }: MDXRendererProps) { { theme: "github-dark", keepBackground: false, - // Keep rehype-pretty-code for now to mark code blocks, - // but our components will handle the actual rendering }, ], + // Generic plugin to extract all meta attributes as data-* props + rehypeCodeMeta, ], }, }} diff --git a/apps/framework-docs-v2/src/components/mdx/code-block-wrapper.tsx b/apps/framework-docs-v2/src/components/mdx/code-block-wrapper.tsx index 7efea85c06..76302de0f7 100644 --- a/apps/framework-docs-v2/src/components/mdx/code-block-wrapper.tsx +++ b/apps/framework-docs-v2/src/components/mdx/code-block-wrapper.tsx @@ -76,6 +76,7 @@ interface MDXCodeProps extends React.HTMLAttributes { "data-rehype-pretty-code-fragment"?: string; "data-rehype-pretty-code-title"?: string; "data-filename"?: string; + "data-copy"?: string; children?: React.ReactNode; } @@ -365,7 +366,7 @@ export function MDXPre({ children, ...props }: MDXCodeBlockProps) { props["data-rehype-pretty-code-title"] || props["data-filename"] || props["title"]; - const hasCopy = props["data-copy"] !== undefined; + const hasCopy = props["data-copy"] !== "false"; const isShell = SHELL_LANGUAGES.has(language); const isConfigFile = CONFIG_LANGUAGES.has(language); @@ -377,7 +378,7 @@ export function MDXPre({ children, ...props }: MDXCodeBlockProps) { code={codeText} language={language} filename={filename || undefined} - copyButton={true} + copyButton={hasCopy} />
); @@ -430,7 +431,7 @@ export function MDXPre({ children, ...props }: MDXCodeBlockProps) { code={codeText} language={language || "typescript"} filename={filename || undefined} - copyButton={true} + copyButton={hasCopy} /> ); @@ -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} /> ); @@ -543,9 +544,15 @@ export function MDXCode({ children, className, ...props }: MDXCodeProps) { const isInline = !className?.includes("language-") && !props["data-language"]; if (isInline) { - // Inline code - render as normal code element + // Inline code - render as normal code element with proper styling return ( - + {children} ); @@ -572,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 (
@@ -579,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/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/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/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; diff --git a/apps/framework-docs-v2/src/components/mdx/index.ts b/apps/framework-docs-v2/src/components/mdx/index.ts index ebdb8480cf..5320abeeac 100644 --- a/apps/framework-docs-v2/src/components/mdx/index.ts +++ b/apps/framework-docs-v2/src/components/mdx/index.ts @@ -8,8 +8,12 @@ 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 { 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/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 new file mode 100644 index 0000000000..bf523147a7 --- /dev/null +++ b/apps/framework-docs-v2/src/components/mdx/server-code-block.tsx @@ -0,0 +1,441 @@ +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 { InlineCode } from "./inline-code"; +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", +]); + +/** + * 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 + */ +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; + "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; +} + +/** + * 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; +} + +/** + * 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, + ...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 - 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: 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 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" && !shouldNotAnimate) { + return ( +
    + +
    + ); + } + + // All other shell commands use ShellSnippet (Terminal tab with copy) + return ( +
    + +
    + ); + } + + // Non-shell: animate if filename present and copy not explicitly set + // unless animate is explicitly false + const legacyAnimate = + filename && props["data-copy"] === undefined && !shouldNotAnimate; + + if (legacyAnimate) { + return ( +
    + +
    + ); + } + + // Default: static CodeSnippet + return ( +
    + +
    + ); +} + +/** + * Server-side inline code component + * + * Supports Nextra-style inline highlighting: `code{:lang}` + */ +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 ( +
    + +
    + ); + } + + // 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 ( + + {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..75b1a7621a --- /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) + 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) || {} : {}; + + // 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/components/mdx/template-card.tsx b/apps/framework-docs-v2/src/components/mdx/template-card.tsx index 0e3ccba497..c229f41f38 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,8 @@ import { CardFooter, CardHeader, } from "@/components/ui/card"; -import { IconBrandGithub } from "@tabler/icons-react"; +import { IconBrandGithub, IconRocket, IconBook } from "@tabler/icons-react"; +import { Button } from "@/components/ui/button"; import { Snippet, SnippetCopyButton, @@ -51,12 +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 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 [chipsExpanded, setChipsExpanded] = React.useState(false); const categoryLabels = { starter: "Starter", @@ -78,103 +74,120 @@ 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" ? "TS" : "Python"} - - )} - {isTemplate && template && ( - - {categoryLabels[template.category]} - - )} - {!isTemplate && ( - - Demo App - - )} -
    -

    - {isTemplate ? formatTemplateName(name) : name} -

    -
    + +
    + {(() => { + 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(" • ")} + + ); + })()}
    -
    - - {description} - - {frameworks.length > 0 && ( -
    -

    - Frameworks: -

    -
    - {frameworks.map((framework) => ( - - {framework} - - ))} -
    +

    + {isTemplate ? formatTemplateName(name) : name} +

    + {allChips.length > 0 && ( +
    + {visibleChips.map((chip) => ( + + {chip.value} + + ))} + {!chipsExpanded && hiddenCount > 0 && ( + setChipsExpanded(true)} + > + {hiddenCount} more + + )} + {chipsExpanded && ( + setChipsExpanded(false)} + > + Show less + + )}
    )} - - {features.length > 0 && ( -
    -

    - Features: -

    -
    - {features.map((feature) => ( - - {feature} - - ))} -
    -
    - )} - - + + + {description} {isTemplate && template && (
    )} - {!isTemplate && app && app.blogPost && ( - - Read Blog Post → - - )} - - - View on GitHub - +
    + +
    + + {!isTemplate && app && app.blogPost && ( + + )} + +
    ); 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/navigation/side-nav.tsx b/apps/framework-docs-v2/src/components/navigation/side-nav.tsx index e9ac6690fa..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, @@ -155,18 +156,42 @@ function NavItemComponent({ item }: { item: NavPage }) { const isActive = pathname === `/${item.slug}`; const hasChildren = item.children && item.children.length > 0; - const hasActiveChild = - hasChildren && - item.children?.some( - (child) => child.type === "page" && pathname === `/${child.slug}`, - ); - const defaultOpen = isActive || hasActiveChild; + + // Recursively check if any descendant is active + const hasActiveDescendant = React.useMemo(() => { + if (!hasChildren) return false; + + const checkDescendant = (children: NavItem[]): boolean => { + return children.some((child) => { + if (child.type === "page") { + if (pathname === `/${child.slug}`) return true; + if (child.children) return checkDescendant(child.children); + } + return false; + }); + }; + + return checkDescendant(item.children!); + }, [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 ( - + - + {item.icon && } {item.title} @@ -183,67 +208,12 @@ function NavItemComponent({ item }: { item: NavPage }) { - {(() => { - const elements: React.ReactNode[] = []; - let currentGroup: NavPage[] = []; - let currentLabel: string | null = null; - - const flushGroup = () => { - if (currentGroup.length > 0) { - currentGroup.forEach((child: NavPage) => { - const childHref = (() => { - const params = new URLSearchParams( - searchParams.toString(), - ); - params.set("lang", language); - return `/${child.slug}?${params.toString()}`; - })(); - const childIsActive = pathname === `/${child.slug}`; - elements.push( - - - - {child.icon && ( - - )} - {child.title} - - - , - ); - }); - currentGroup = []; - } - }; - - item.children?.forEach((child) => { - if (child.type === "separator") { - flushGroup(); - currentLabel = null; - } else if (child.type === "label") { - flushGroup(); - currentLabel = child.title; - } else if (child.type === "page") { - if (currentLabel && currentGroup.length === 0) { - // Add label before first item in group - elements.push( - - {currentLabel} - , - ); - } - currentGroup.push(child); - } - }); - flushGroup(); - return elements; - })()} + {renderNavChildren( + item.children, + pathname, + searchParams, + language, + )} @@ -265,3 +235,124 @@ 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, + searchParams: URLSearchParams, + language: string, +): React.ReactNode[] { + const elements: React.ReactNode[] = []; + let isFirstLabel = true; + + 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/navigation/toc-nav.tsx b/apps/framework-docs-v2/src/components/navigation/toc-nav.tsx index 0be4c2b7f6..dabd27899c 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,15 +147,15 @@ export function TOCNav({ headings, helpfulLinks }: TOCNavProps) { } return ( -