Revert "Unify workspace dropdowns; Update layouts (#458)"

This reverts commit 4bc8448a1d.
This commit is contained in:
gbuomprisco
2026-03-11 14:47:47 +08:00
parent 4bc8448a1d
commit 4912e402a3
530 changed files with 11182 additions and 14382 deletions

View File

@@ -55,10 +55,7 @@ create policy "projects_write" on public.projects for all
Use `server-action-builder` skill for detailed patterns. Use `server-action-builder` skill for detailed patterns.
**Rule: Services are decoupled from interfaces.** The service is pure logic that receives dependencies (database client, **Rule: Services are decoupled from interfaces.** The service is pure logic that receives dependencies (database client, etc.) as arguments — it never imports framework-specific modules. The server action is a thin adapter that resolves dependencies and calls the service. This means the same service can be called from a server action, an MCP tool, a CLI command, or a unit test with zero changes.
etc.) as arguments — it never imports framework-specific modules. The server action is a thin adapter that resolves
dependencies and calls the service. This means the same service can be called from a server action, an MCP tool, a CLI
command, or a unit test with zero changes.
Create in route's `_lib/server/` directory: Create in route's `_lib/server/` directory:

View File

@@ -19,8 +19,8 @@ export class AuthPageObject {
} }
async signOut() { async signOut() {
await this.page.click('[data-test="workspace-dropdown-trigger"]'); await this.page.click('[data-test="account-dropdown-trigger"]');
await this.page.click('[data-test="workspace-sign-out"]'); await this.page.click('[data-test="account-dropdown-sign-out"]');
} }
async bootstrapUser(params: { email: string; password: string; name: string }) { async bootstrapUser(params: { email: string; password: string; name: string }) {
@@ -47,19 +47,9 @@ export class AuthPageObject {
## Common Selectors ## Common Selectors
```typescript ```typescript
// Workspace dropdown (sidebar header - combined account switcher + user menu) // Account dropdown
'[data-test="workspace-dropdown-trigger"]' // Opens the dropdown '[data-test="account-dropdown-trigger"]'
'[data-test="workspace-switch-submenu"]' // Sub-trigger for workspace switching '[data-test="account-dropdown-sign-out"]'
'[data-test="workspace-switch-content"]' // Sub-menu content with workspace list
'[data-test="workspace-team-item"]' // Individual team items in switcher
'[data-test="create-team-trigger"]' // Create team button in switcher
'[data-test="workspace-sign-out"]' // Sign out button
'[data-test="workspace-settings-link"]' // Settings link
'[data-test="account-dropdown-display-name"]' // User display name (inside dropdown panel)
// Opening the workspace switcher (two-step: open dropdown, then submenu)
await page.click('[data-test="workspace-dropdown-trigger"]');
await page.click('[data-test="workspace-switch-submenu"]');
// Navigation // Navigation
'[data-test="sidebar-menu"]' '[data-test="sidebar-menu"]'

View File

@@ -126,7 +126,7 @@ export function CreateEntityForm() {
reValidateMode: 'onChange', reValidateMode: 'onChange',
}); });
const onSubmit = (data: z.output<typeof CreateEntitySchema>) => { const onSubmit = (data: z.infer<typeof CreateEntitySchema>) => {
setError(false); setError(false);
startTransition(async () => { startTransition(async () => {
@@ -147,7 +147,7 @@ export function CreateEntityForm() {
<If condition={error}> <If condition={error}>
<Alert variant="destructive"> <Alert variant="destructive">
<AlertDescription> <AlertDescription>
<Trans i18nKey="common.errors.generic" /> <Trans i18nKey="common:errors.generic" />
</AlertDescription> </AlertDescription>
</Alert> </Alert>
</If> </If>
@@ -177,9 +177,9 @@ export function CreateEntityForm() {
data-test="submit-entity-button" data-test="submit-entity-button"
> >
{pending ? ( {pending ? (
<Trans i18nKey="common.creating" /> <Trans i18nKey="common:creating" />
) : ( ) : (
<Trans i18nKey="common.create" /> <Trans i18nKey="common:create" />
)} )}
</Button> </Button>
</Form> </Form>

View File

@@ -145,7 +145,7 @@ import { toast } from '@kit/ui/sonner';
<If condition={error}> <If condition={error}>
<Alert variant="destructive"> <Alert variant="destructive">
<AlertDescription> <AlertDescription>
<Trans i18nKey="common.errors.generic" /> <Trans i18nKey="common:errors.generic" />
</AlertDescription> </AlertDescription>
</Alert> </Alert>
</If> </If>
@@ -160,9 +160,9 @@ import { toast } from '@kit/ui/sonner';
data-test="submit-button" data-test="submit-button"
> >
{pending ? ( {pending ? (
<Trans i18nKey="common.submitting" /> <Trans i18nKey="common:submitting" />
) : ( ) : (
<Trans i18nKey="common.submit" /> <Trans i18nKey="common:submit" />
)} )}
</Button> </Button>
``` ```
@@ -199,7 +199,7 @@ export function MyForm() {
mode: 'onChange', mode: 'onChange',
}); });
const onSubmit = (data: z.output<typeof MySchema>) => { const onSubmit = (data: z.infer<typeof MySchema>) => {
setError(false); setError(false);
startTransition(async () => { startTransition(async () => {
@@ -220,7 +220,7 @@ export function MyForm() {
<If condition={error}> <If condition={error}>
<Alert variant="destructive"> <Alert variant="destructive">
<AlertDescription> <AlertDescription>
<Trans i18nKey="common.errors.generic" /> <Trans i18nKey="common:errors.generic" />
</AlertDescription> </AlertDescription>
</Alert> </Alert>
</If> </If>

View File

@@ -17,21 +17,19 @@ Create validation schema in `_lib/schemas/`:
```typescript ```typescript
// _lib/schemas/feature.schema.ts // _lib/schemas/feature.schema.ts
import * as z from 'zod'; import { z } from 'zod';
export const CreateFeatureSchema = z.object({ export const CreateFeatureSchema = z.object({
name: z.string().min(1, 'Name is required'), name: z.string().min(1, 'Name is required'),
accountId: z.string().uuid('Invalid account ID'), accountId: z.string().uuid('Invalid account ID'),
}); });
export type CreateFeatureInput = z.output<typeof CreateFeatureSchema>; export type CreateFeatureInput = z.infer<typeof CreateFeatureSchema>;
``` ```
### Step 2: Create Service Layer ### Step 2: Create Service Layer
**North star: services are decoupled from their interface.** The service is pure logic — it receives a database client **North star: services are decoupled from their interface.** The service is pure logic — it receives a database client as a dependency, never imports one. This means the same service works whether called from a server action, an MCP tool, a CLI command, or a plain unit test.
as a dependency, never imports one. This means the same service works whether called from a server action, an MCP tool,
a CLI command, or a plain unit test.
Create service in `_lib/server/`: Create service in `_lib/server/`:
@@ -64,13 +62,11 @@ class FeatureService {
} }
``` ```
The service never calls `getSupabaseServerClient()` — the caller provides the client. This keeps the service testable ( The service never calls `getSupabaseServerClient()` — the caller provides the client. This keeps the service testable (pass a mock client) and reusable (any interface can supply its own client).
pass a mock client) and reusable (any interface can supply its own client).
### Step 3: Create Server Action (Thin Adapter) ### Step 3: Create Server Action (Thin Adapter)
The action is a **thin adapter** — it resolves dependencies (client, logger) and delegates to the service. No business The action is a **thin adapter** — it resolves dependencies (client, logger) and delegates to the service. No business logic lives here.
logic lives here.
Create action in `_lib/server/server-actions.ts`: Create action in `_lib/server/server-actions.ts`:
@@ -111,18 +107,13 @@ export const createFeatureAction = enhanceAction(
## Key Patterns ## Key Patterns
1. **Services are pure, interfaces are thin adapters.** The service contains all business logic. The server action (or 1. **Services are pure, interfaces are thin adapters.** The service contains all business logic. The server action (or MCP tool, or CLI command) is glue code that resolves dependencies and calls the service. If an MCP tool and a server action do the same thing, they call the same service function.
MCP tool, or CLI command) is glue code that resolves dependencies and calls the service. If an MCP tool and a server 2. **Inject dependencies, don't import them in services.** Services receive their database client, logger, or any I/O capability as constructor arguments — never by importing framework-specific modules. This keeps them testable with stubs and reusable across interfaces.
action do the same thing, they call the same service function.
2. **Inject dependencies, don't import them in services.** Services receive their database client, logger, or any I/O
capability as constructor arguments — never by importing framework-specific modules. This keeps them testable with
stubs and reusable across interfaces.
3. **Schema in separate file** - Reusable between client and server 3. **Schema in separate file** - Reusable between client and server
4. **Logging** - Always log before and after operations 4. **Logging** - Always log before and after operations
5. **Revalidation** - Use `revalidatePath` after mutations 5. **Revalidation** - Use `revalidatePath` after mutations
6. **Trust RLS** - Don't add manual auth checks (RLS handles it) 6. **Trust RLS** - Don't add manual auth checks (RLS handles it)
7. **Testable in isolation** - Because services accept their dependencies, you can test them with a mock client and no 7. **Testable in isolation** - Because services accept their dependencies, you can test them with a mock client and no running infrastructure
running infrastructure
## File Structure ## File Structure

View File

@@ -29,8 +29,8 @@ export const myAction = enhanceAction(
### Handler Parameters ### Handler Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
|-----------|--------------------|------------------------------------| |-----------|------|-------------|
| `data` | `z.output<Schema>` | Validated input data | | `data` | `z.infer<Schema>` | Validated input data |
| `user` | `User` | Authenticated user (if auth: true) | | `user` | `User` | Authenticated user (if auth: true) |
## enhanceRouteHandler API ## enhanceRouteHandler API
@@ -69,7 +69,7 @@ export const GET = enhanceRouteHandler(
## Common Zod Patterns ## Common Zod Patterns
```typescript ```typescript
import * as z from 'zod'; import { z } from 'zod';
// Basic schema // Basic schema
export const CreateItemSchema = z.object({ export const CreateItemSchema = z.object({

View File

@@ -9,9 +9,7 @@ You are an expert at building pure, testable services that are decoupled from th
## North Star ## North Star
**Every service is decoupled from its interface (I/O).** A service takes plain data in, does work, and returns plain **Every service is decoupled from its interface (I/O).** A service takes plain data in, does work, and returns plain data out. It has no knowledge of whether it was called from an MCP tool, a server action, a CLI command, a route handler, or a test. The caller is a thin adapter that resolves dependencies and delegates.
data out. It has no knowledge of whether it was called from an MCP tool, a server action, a CLI command, a route
handler, or a test. The caller is a thin adapter that resolves dependencies and delegates.
## Workflow ## Workflow
@@ -23,7 +21,7 @@ Start with the input/output types. These are plain TypeScript — no framework t
```typescript ```typescript
// _lib/schemas/project.schema.ts // _lib/schemas/project.schema.ts
import * as z from 'zod'; import { z } from 'zod';
export const CreateProjectSchema = z.object({ export const CreateProjectSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
@@ -42,8 +40,7 @@ export interface Project {
### Step 2: Build the Service ### Step 2: Build the Service
The service receives all dependencies through its constructor. It never imports framework-specific modules ( The service receives all dependencies through its constructor. It never imports framework-specific modules (`getSupabaseServerClient`, `getLogger`, `revalidatePath`, etc.).
`getSupabaseServerClient`, `getLogger`, `revalidatePath`, etc.).
```typescript ```typescript
// _lib/server/project.service.ts // _lib/server/project.service.ts
@@ -98,8 +95,7 @@ class ProjectService {
### Step 3: Write Thin Adapters ### Step 3: Write Thin Adapters
Each interface is a thin adapter — it resolves dependencies, calls the service, and handles interface-specific Each interface is a thin adapter — it resolves dependencies, calls the service, and handles interface-specific concerns (revalidation, redirects, MCP formatting, CLI output).
concerns (revalidation, redirects, MCP formatting, CLI output).
**Server Action adapter:** **Server Action adapter:**
@@ -238,25 +234,20 @@ describe('ProjectService', () => {
## Rules ## Rules
1. **Services are pure functions over data.** Plain objects/primitives in, plain objects/primitives out. No `Request`/ 1. **Services are pure functions over data.** Plain objects/primitives in, plain objects/primitives out. No `Request`/`Response`, no MCP context, no `FormData`.
`Response`, no MCP context, no `FormData`.
2. **Inject dependencies, never import them.** The service receives its database client, storage client, or any I/O 2. **Inject dependencies, never import them.** The service receives its database client, storage client, or any I/O capability as a constructor argument. Never call `getSupabaseServerClient()` inside a service.
capability as a constructor argument. Never call `getSupabaseServerClient()` inside a service.
3. **Adapters are trivial glue.** A server action resolves the client, calls the service, and handles `revalidatePath`. 3. **Adapters are trivial glue.** A server action resolves the client, calls the service, and handles `revalidatePath`. An MCP tool resolves the client, calls the service, and formats the response. No business logic in adapters.
An MCP tool resolves the client, calls the service, and formats the response. No business logic in adapters.
4. **One service, many callers.** If two interfaces do the same thing, they call the same service function. Duplicating 4. **One service, many callers.** If two interfaces do the same thing, they call the same service function. Duplicating logic is a violation.
logic is a violation.
5. **Testable in isolation.** Pass a mock client, assert the output. If you need a running database to test a service, 5. **Testable in isolation.** Pass a mock client, assert the output. If you need a running database to test a service, refactor until you don't.
refactor until you don't.
## What Goes Where ## What Goes Where
| Concern | Location | Example | | Concern | Location | Example |
|------------------------|-------------------------------------------|-------------------------------------------| |---------|----------|---------|
| Input validation (Zod) | `_lib/schemas/` | `CreateProjectSchema` | | Input validation (Zod) | `_lib/schemas/` | `CreateProjectSchema` |
| Business logic | `_lib/server/*.service.ts` | `ProjectService.create()` | | Business logic | `_lib/server/*.service.ts` | `ProjectService.create()` |
| Auth check | Adapter (`enhanceAction({ auth: true })`) | Server action wrapper | | Auth check | Adapter (`enhanceAction({ auth: true })`) | Server action wrapper |
@@ -314,5 +305,4 @@ const result = await client.from('projects').insert(...).select().single();
## Reference ## Reference
See `[Examples](examples.md)` for more patterns including services with multiple dependencies, services that compose See `[Examples](examples.md)` for more patterns including services with multiple dependencies, services that compose other services, and testing strategies.
other services, and testing strategies.

View File

@@ -3,7 +3,7 @@ on:
push: push:
branches: [ main ] branches: [ main ]
pull_request: pull_request:
branches: [ main, v3 ] branches: [ main ]
jobs: jobs:
typescript: typescript:
name: ʦ TypeScript name: ʦ TypeScript

View File

@@ -556,8 +556,8 @@ function MyFeaturePage() {
return ( return (
<> <>
<MyFeatureHeader <MyFeatureHeader
title={<Trans i18nKey={'common.routes.myFeature'} />} title={<Trans i18nKey={'common:routes.myFeature'} />}
description={<Trans i18nKey={'common.myFeatureDescription'} />} description={<Trans i18nKey={'common:myFeatureDescription'} />}
/> />
<PageBody> <PageBody>
@@ -830,7 +830,7 @@ import { ProfileAvatar } from '@kit/ui/profile-avatar';
## Core Shadcn UI Components ## Core Shadcn UI Components
| Component | Description | Import Path | | Component | Description | Import Path |
|------------------|-------------------------------------------|-------------------------------------------------------------------------------------------------| |-----------|-------------|-------------|
| `Accordion` | Expandable/collapsible content sections | `@kit/ui/accordion` [accordion.tsx](mdc:packages/ui/src/shadcn/accordion.tsx) | | `Accordion` | Expandable/collapsible content sections | `@kit/ui/accordion` [accordion.tsx](mdc:packages/ui/src/shadcn/accordion.tsx) |
| `AlertDialog` | Modal dialog for important actions | `@kit/ui/alert-dialog` [alert-dialog.tsx](mdc:packages/ui/src/shadcn/alert-dialog.tsx) | | `AlertDialog` | Modal dialog for important actions | `@kit/ui/alert-dialog` [alert-dialog.tsx](mdc:packages/ui/src/shadcn/alert-dialog.tsx) |
| `Alert` | Status/notification messages | `@kit/ui/alert` [alert.tsx](mdc:packages/ui/src/shadcn/alert.tsx) | | `Alert` | Status/notification messages | `@kit/ui/alert` [alert.tsx](mdc:packages/ui/src/shadcn/alert.tsx) |
@@ -856,7 +856,7 @@ import { ProfileAvatar } from '@kit/ui/profile-avatar';
| `Select` | Dropdown selection menu | `@kit/ui/select` [select.tsx](mdc:packages/ui/src/shadcn/select.tsx) | | `Select` | Dropdown selection menu | `@kit/ui/select` [select.tsx](mdc:packages/ui/src/shadcn/select.tsx) |
| `Separator` | Visual divider between content | `@kit/ui/separator` [separator.tsx](mdc:packages/ui/src/shadcn/separator.tsx) | | `Separator` | Visual divider between content | `@kit/ui/separator` [separator.tsx](mdc:packages/ui/src/shadcn/separator.tsx) |
| `Sheet` | Sliding panel from screen edge | `@kit/ui/sheet` [sheet.tsx](mdc:packages/ui/src/shadcn/sheet.tsx) | | `Sheet` | Sliding panel from screen edge | `@kit/ui/sheet` [sheet.tsx](mdc:packages/ui/src/shadcn/sheet.tsx) |
| `Sidebar` | Advanced sidebar navigation | `@kit/ui/sidebar` [sidebar.tsx](mdc:packages/ui/src/shadcn/sidebar.tsx) | | `Sidebar` | Advanced sidebar navigation | `@kit/ui/shadcn-sidebar` [sidebar.tsx](mdc:packages/ui/src/shadcn/sidebar.tsx) |
| `Skeleton` | Loading placeholder | `@kit/ui/skeleton` [skeleton.tsx](mdc:packages/ui/src/shadcn/skeleton.tsx) | | `Skeleton` | Loading placeholder | `@kit/ui/skeleton` [skeleton.tsx](mdc:packages/ui/src/shadcn/skeleton.tsx) |
| `Switch` | Toggle control | `@kit/ui/switch` [switch.tsx](mdc:packages/ui/src/shadcn/switch.tsx) | | `Switch` | Toggle control | `@kit/ui/switch` [switch.tsx](mdc:packages/ui/src/shadcn/switch.tsx) |
| `Toast` | Toaster | `@kit/ui/sonner` [sonner.tsx](mdc:packages/ui/src/shadcn/sonner.tsx) | | `Toast` | Toaster | `@kit/ui/sonner` [sonner.tsx](mdc:packages/ui/src/shadcn/sonner.tsx) |
@@ -920,7 +920,7 @@ Zod schemas should be defined in the `schema` folder and exported, so we can reu
```tsx ```tsx
// _lib/schema/create-note.schema.ts // _lib/schema/create-note.schema.ts
import * as z from 'zod'; import { z } from 'zod';
export const CreateNoteSchema = z.object({ export const CreateNoteSchema = z.object({
title: z.string().min(1), title: z.string().min(1),
@@ -935,7 +935,7 @@ Server Actions [server-actions.mdc](mdc:.cursor/rules/server-actions.mdc) can he
```tsx ```tsx
'use server'; 'use server';
import * as z from 'zod'; import { z } from 'zod';
import { enhanceAction } from '@kit/next/actions'; import { enhanceAction } from '@kit/next/actions';
import { CreateNoteSchema } from '../schema/create-note.schema'; import { CreateNoteSchema } from '../schema/create-note.schema';
@@ -965,7 +965,7 @@ Then create a client component to handle the form submission:
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import * as z from 'zod'; import { z } from 'zod';
import { Textarea } from '@kit/ui/textarea'; import { Textarea } from '@kit/ui/textarea';
import { Input } from '@kit/ui/input'; import { Input } from '@kit/ui/input';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
@@ -1436,7 +1436,7 @@ You always must use `(security_invoker = true)` for views.
```tsx ```tsx
'use server'; 'use server';
import * as z from 'zod'; import { z } from 'zod';
import { enhanceAction } from '@kit/next/actions'; import { enhanceAction } from '@kit/next/actions';
import { EntitySchema } from '../entity.schema.ts`; import { EntitySchema } from '../entity.schema.ts`;
@@ -1463,7 +1463,7 @@ export const myServerAction = enhanceAction(
- To create API routes (route.ts), always use the `enhanceRouteHandler` function from the "@kit/supabase/routes" package. [index.ts](mdc:packages/next/src/routes/index.ts) - To create API routes (route.ts), always use the `enhanceRouteHandler` function from the "@kit/supabase/routes" package. [index.ts](mdc:packages/next/src/routes/index.ts)
```tsx ```tsx
import * as z from 'zod'; import { z } from 'zod';
import { enhanceRouteHandler } from '@kit/next/routes'; import { enhanceRouteHandler } from '@kit/next/routes';
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';

1
.npmrc
View File

@@ -3,6 +3,7 @@ dedupe-peer-dependents=true
use-lockfile-v6=true use-lockfile-v6=true
resolution-mode=highest resolution-mode=highest
package-manager-strict=false package-manager-strict=false
public-hoist-pattern[]=*i18next*
public-hoist-pattern[]=*eslint* public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier* public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=*require-in-the-middle* public-hoist-pattern[]=*require-in-the-middle*

View File

@@ -40,8 +40,8 @@ pnpm format:fix # Format code
## Key Patterns (Quick Reference) ## Key Patterns (Quick Reference)
| Pattern | Import | Details | | Pattern | Import | Details |
|----------------|--------------------------------------------------------------|-------------------------------| |---------|--------|---------|
| Server Actions | `authActionClient` from `@kit/next/safe-action` | `packages/next/AGENTS.md` | | Server Actions | `enhanceAction` from `@kit/next/actions` | `packages/next/AGENTS.md` |
| Route Handlers | `enhanceRouteHandler` from `@kit/next/routes` | `packages/next/AGENTS.md` | | Route Handlers | `enhanceRouteHandler` from `@kit/next/routes` | `packages/next/AGENTS.md` |
| Server Client | `getSupabaseServerClient` from `@kit/supabase/server-client` | `packages/supabase/AGENTS.md` | | Server Client | `getSupabaseServerClient` from `@kit/supabase/server-client` | `packages/supabase/AGENTS.md` |
| UI Components | `@kit/ui/{component}` | `packages/ui/AGENTS.md` | | UI Components | `@kit/ui/{component}` | `packages/ui/AGENTS.md` |

View File

@@ -120,7 +120,9 @@ export function AlertDialogStory() {
const generateCode = () => { const generateCode = () => {
let code = `<AlertDialog>\n`; let code = `<AlertDialog>\n`;
code += ` <AlertDialogTrigger render={<Button variant="${controls.triggerVariant}">${controls.triggerText}</Button>} />\n`; code += ` <AlertDialogTrigger asChild>\n`;
code += ` <Button variant="${controls.triggerVariant}">${controls.triggerText}</Button>\n`;
code += ` </AlertDialogTrigger>\n`;
code += ` <AlertDialogContent>\n`; code += ` <AlertDialogContent>\n`;
code += ` <AlertDialogHeader>\n`; code += ` <AlertDialogHeader>\n`;
@@ -177,14 +179,11 @@ export function AlertDialogStory() {
const renderPreview = () => { const renderPreview = () => {
return ( return (
<AlertDialog> <AlertDialog>
<AlertDialogTrigger <AlertDialogTrigger asChild>
render={
<Button variant={controls.triggerVariant}> <Button variant={controls.triggerVariant}>
{controls.triggerText} {controls.triggerText}
</Button> </Button>
} </AlertDialogTrigger>
/>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
{controls.withIcon ? ( {controls.withIcon ? (
@@ -342,11 +341,11 @@ export function AlertDialogStory() {
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<AlertDialog> <AlertDialog>
<AlertDialogTrigger <AlertDialogTrigger asChild>
render={<Button variant="destructive" size="sm" />} <Button variant="destructive" size="sm">
>
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
Delete Item Delete Item
</Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@@ -371,9 +370,11 @@ export function AlertDialogStory() {
</AlertDialog> </AlertDialog>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger render={<Button variant="outline" />}> <AlertDialogTrigger asChild>
<Button variant="outline">
<LogOut className="mr-2 h-4 w-4" /> <LogOut className="mr-2 h-4 w-4" />
Sign Out Sign Out
</Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@@ -396,9 +397,11 @@ export function AlertDialogStory() {
</AlertDialog> </AlertDialog>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger render={<Button variant="outline" />}> <AlertDialogTrigger asChild>
<Button variant="outline">
<UserX className="mr-2 h-4 w-4" /> <UserX className="mr-2 h-4 w-4" />
Remove User Remove User
</Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@@ -435,9 +438,11 @@ export function AlertDialogStory() {
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<AlertDialog> <AlertDialog>
<AlertDialogTrigger render={<Button variant="outline" />}> <AlertDialogTrigger asChild>
<Button variant="outline">
<Archive className="mr-2 h-4 w-4" /> <Archive className="mr-2 h-4 w-4" />
Archive Project Archive Project
</Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@@ -460,9 +465,11 @@ export function AlertDialogStory() {
</AlertDialog> </AlertDialog>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger render={<Button />}> <AlertDialogTrigger asChild>
<Button>
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
Export Data Export Data
</Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@@ -486,9 +493,11 @@ export function AlertDialogStory() {
</AlertDialog> </AlertDialog>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger render={<Button variant="outline" />}> <AlertDialogTrigger asChild>
<Button variant="outline">
<RefreshCw className="mr-2 h-4 w-4" /> <RefreshCw className="mr-2 h-4 w-4" />
Reset Settings Reset Settings
</Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@@ -526,11 +535,11 @@ export function AlertDialogStory() {
<div className="space-y-3"> <div className="space-y-3">
<h4 className="text-sm font-semibold">Error/Destructive</h4> <h4 className="text-sm font-semibold">Error/Destructive</h4>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger <AlertDialogTrigger asChild>
render={<Button variant="destructive" size="sm" />} <Button variant="destructive" size="sm">
>
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
Delete Forever Delete Forever
</Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@@ -558,11 +567,11 @@ export function AlertDialogStory() {
<div className="space-y-3"> <div className="space-y-3">
<h4 className="text-sm font-semibold">Warning</h4> <h4 className="text-sm font-semibold">Warning</h4>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger <AlertDialogTrigger asChild>
render={<Button variant="outline" size="sm" />} <Button variant="outline" size="sm">
>
<AlertTriangle className="mr-2 h-4 w-4" /> <AlertTriangle className="mr-2 h-4 w-4" />
Unsaved Changes Unsaved Changes
</Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@@ -588,11 +597,11 @@ export function AlertDialogStory() {
<div className="space-y-3"> <div className="space-y-3">
<h4 className="text-sm font-semibold">Info</h4> <h4 className="text-sm font-semibold">Info</h4>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger <AlertDialogTrigger asChild>
render={<Button variant="outline" size="sm" />} <Button variant="outline" size="sm">
>
<Share className="mr-2 h-4 w-4" /> <Share className="mr-2 h-4 w-4" />
Share Publicly Share Publicly
</Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@@ -618,9 +627,11 @@ export function AlertDialogStory() {
<div className="space-y-3"> <div className="space-y-3">
<h4 className="text-sm font-semibold">Success</h4> <h4 className="text-sm font-semibold">Success</h4>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger render={<Button size="sm" />}> <AlertDialogTrigger asChild>
<Button size="sm">
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
Complete Setup Complete Setup
</Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>

View File

@@ -33,6 +33,7 @@ interface ButtonControls {
loading: boolean; loading: boolean;
withIcon: boolean; withIcon: boolean;
fullWidth: boolean; fullWidth: boolean;
asChild: boolean;
} }
const variantOptions = [ const variantOptions = [
@@ -67,6 +68,7 @@ export function ButtonStory() {
loading: false, loading: false,
withIcon: false, withIcon: false,
fullWidth: false, fullWidth: false,
asChild: false,
}); });
const generateCode = () => { const generateCode = () => {
@@ -75,12 +77,14 @@ export function ButtonStory() {
variant: controls.variant, variant: controls.variant,
size: controls.size, size: controls.size,
disabled: controls.disabled, disabled: controls.disabled,
asChild: controls.asChild,
className: controls.fullWidth ? 'w-full' : '', className: controls.fullWidth ? 'w-full' : '',
}, },
{ {
variant: 'default', variant: 'default',
size: 'default', size: 'default',
disabled: false, disabled: false,
asChild: false,
className: '', className: '',
}, },
); );
@@ -190,6 +194,15 @@ export function ButtonStory() {
onCheckedChange={(checked) => updateControl('fullWidth', checked)} onCheckedChange={(checked) => updateControl('fullWidth', checked)}
/> />
</div> </div>
<div className="flex items-center justify-between">
<Label htmlFor="asChild">As Child</Label>
<Switch
id="asChild"
checked={controls.asChild}
onCheckedChange={(checked) => updateControl('asChild', checked)}
/>
</div>
</> </>
); );

View File

@@ -276,11 +276,11 @@ export default function CalendarStory() {
<Card> <Card>
<CardContent className="flex justify-center pt-6"> <CardContent className="flex justify-center pt-6">
<Popover> <Popover>
<PopoverTrigger <PopoverTrigger asChild>
render={<Button variant="outline" className="justify-start" />} <Button variant="outline" className="justify-start">
>
<CalendarIcon className="mr-2 h-4 w-4" /> <CalendarIcon className="mr-2 h-4 w-4" />
Pick a date Pick a date
</Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start"> <PopoverContent className="w-auto p-0" align="start">
<Calendar <Calendar

View File

@@ -320,12 +320,10 @@ export function CardButtonStory() {
</thead> </thead>
<tbody> <tbody>
<tr className="border-b"> <tr className="border-b">
<td className="p-3 font-mono text-sm">render</td> <td className="p-3 font-mono text-sm">asChild</td>
<td className="p-3 font-mono text-sm"> <td className="p-3 font-mono text-sm">boolean</td>
React.ReactElement <td className="p-3 font-mono text-sm">false</td>
</td> <td className="p-3">Render as child element</td>
<td className="p-3 font-mono text-sm">-</td>
<td className="p-3">Compose with a custom element</td>
</tr> </tr>
<tr className="border-b"> <tr className="border-b">
<td className="p-3 font-mono text-sm">className</td> <td className="p-3 font-mono text-sm">className</td>

View File

@@ -139,8 +139,8 @@ export function DialogStory() {
}); });
let code = `<Dialog>\n`; let code = `<Dialog>\n`;
code += ` <DialogTrigger render={<Button variant="${controls.triggerVariant}" />}>\n`; code += ` <DialogTrigger asChild>\n`;
code += ` ${controls.triggerText}\n`; code += ` <Button variant="${controls.triggerVariant}">${controls.triggerText}</Button>\n`;
code += ` </DialogTrigger>\n`; code += ` </DialogTrigger>\n`;
code += ` <DialogContent${contentPropsString}>\n`; code += ` <DialogContent${contentPropsString}>\n`;
code += ` <DialogHeader>\n`; code += ` <DialogHeader>\n`;
@@ -182,8 +182,8 @@ export function DialogStory() {
if (controls.withFooter) { if (controls.withFooter) {
code += ` <DialogFooter>\n`; code += ` <DialogFooter>\n`;
code += ` <DialogClose render={<Button variant="outline" />}>\n`; code += ` <DialogClose asChild>\n`;
code += ` Cancel\n`; code += ` <Button variant="outline">Cancel</Button>\n`;
code += ` </DialogClose>\n`; code += ` </DialogClose>\n`;
code += ` <Button>Save Changes</Button>\n`; code += ` <Button>Save Changes</Button>\n`;
code += ` </DialogFooter>\n`; code += ` </DialogFooter>\n`;
@@ -198,8 +198,10 @@ export function DialogStory() {
const renderPreview = () => { const renderPreview = () => {
return ( return (
<Dialog> <Dialog>
<DialogTrigger render={<Button variant={controls.triggerVariant} />}> <DialogTrigger asChild>
<Button variant={controls.triggerVariant}>
{controls.triggerText} {controls.triggerText}
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
className={cn( className={cn(
@@ -269,8 +271,8 @@ export function DialogStory() {
{controls.withFooter && ( {controls.withFooter && (
<DialogFooter> <DialogFooter>
<DialogClose render={<Button variant="outline" />}> <DialogClose asChild>
Cancel <Button variant="outline">Cancel</Button>
</DialogClose> </DialogClose>
<Button>Save Changes</Button> <Button>Save Changes</Button>
</DialogFooter> </DialogFooter>
@@ -389,9 +391,11 @@ export function DialogStory() {
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<Dialog> <Dialog>
<DialogTrigger render={<Button variant="outline" />}> <DialogTrigger asChild>
<Button variant="outline">
<Info className="mr-2 h-4 w-4" /> <Info className="mr-2 h-4 w-4" />
Info Dialog Info Dialog
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
@@ -408,15 +412,19 @@ export function DialogStory() {
</p> </p>
</div> </div>
<DialogFooter> <DialogFooter>
<DialogClose render={<Button />}>Got it</DialogClose> <DialogClose asChild>
<Button>Got it</Button>
</DialogClose>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog> <Dialog>
<DialogTrigger render={<Button />}> <DialogTrigger asChild>
<Button>
<Edit className="mr-2 h-4 w-4" /> <Edit className="mr-2 h-4 w-4" />
Edit Profile Edit Profile
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
@@ -448,8 +456,8 @@ export function DialogStory() {
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<DialogClose render={<Button variant="outline" />}> <DialogClose asChild>
Cancel <Button variant="outline">Cancel</Button>
</DialogClose> </DialogClose>
<Button>Save Changes</Button> <Button>Save Changes</Button>
</DialogFooter> </DialogFooter>
@@ -457,9 +465,11 @@ export function DialogStory() {
</Dialog> </Dialog>
<Dialog> <Dialog>
<DialogTrigger render={<Button variant="secondary" />}> <DialogTrigger asChild>
<Button variant="secondary">
<Settings className="mr-2 h-4 w-4" /> <Settings className="mr-2 h-4 w-4" />
Settings Settings
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
@@ -489,8 +499,8 @@ export function DialogStory() {
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<DialogClose render={<Button variant="outline" />}> <DialogClose asChild>
Cancel <Button variant="outline">Cancel</Button>
</DialogClose> </DialogClose>
<Button>Save</Button> <Button>Save</Button>
</DialogFooter> </DialogFooter>
@@ -508,8 +518,10 @@ export function DialogStory() {
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<Dialog> <Dialog>
<DialogTrigger render={<Button variant="outline" size="sm" />}> <DialogTrigger asChild>
<Button variant="outline" size="sm">
Small Dialog Small Dialog
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-md"> <DialogContent className="max-w-md">
<DialogHeader> <DialogHeader>
@@ -524,14 +536,16 @@ export function DialogStory() {
</p> </p>
</div> </div>
<DialogFooter> <DialogFooter>
<DialogClose render={<Button />}>Close</DialogClose> <DialogClose asChild>
<Button>Close</Button>
</DialogClose>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog> <Dialog>
<DialogTrigger render={<Button variant="outline" />}> <DialogTrigger asChild>
Large Dialog <Button variant="outline">Large Dialog</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader> <DialogHeader>
@@ -557,8 +571,8 @@ export function DialogStory() {
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<DialogClose render={<Button variant="outline" />}> <DialogClose asChild>
Cancel <Button variant="outline">Cancel</Button>
</DialogClose> </DialogClose>
<Button>Save</Button> <Button>Save</Button>
</DialogFooter> </DialogFooter>
@@ -576,9 +590,11 @@ export function DialogStory() {
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<Dialog> <Dialog>
<DialogTrigger render={<Button variant="outline" />}> <DialogTrigger asChild>
<Button variant="outline">
<Image className="mr-2 h-4 w-4" /> <Image className="mr-2 h-4 w-4" />
Image Gallery Image Gallery
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader> <DialogHeader>
@@ -611,9 +627,11 @@ export function DialogStory() {
</Dialog> </Dialog>
<Dialog> <Dialog>
<DialogTrigger render={<Button variant="outline" />}> <DialogTrigger asChild>
<Button variant="outline">
<MessageSquare className="mr-2 h-4 w-4" /> <MessageSquare className="mr-2 h-4 w-4" />
Feedback Feedback
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
@@ -650,8 +668,8 @@ export function DialogStory() {
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<DialogClose render={<Button variant="outline" />}> <DialogClose asChild>
Cancel <Button variant="outline">Cancel</Button>
</DialogClose> </DialogClose>
<Button> <Button>
<MessageSquare className="mr-2 h-4 w-4" /> <MessageSquare className="mr-2 h-4 w-4" />
@@ -718,8 +736,8 @@ export function DialogStory() {
<div> <div>
<h4 className="mb-3 text-lg font-semibold">DialogTrigger</h4> <h4 className="mb-3 text-lg font-semibold">DialogTrigger</h4>
<p className="text-muted-foreground mb-3 text-sm"> <p className="text-muted-foreground mb-3 text-sm">
The element that opens the dialog. Use the render prop to compose The element that opens the dialog. Use asChild prop to render as
with a custom element. child element.
</p> </p>
</div> </div>

View File

@@ -2,7 +2,7 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Code2, FileText, Search } from 'lucide-react'; import { Code2, FileText, Search } from 'lucide-react';
@@ -35,7 +35,6 @@ export function DocsSidebar({
selectedCategory, selectedCategory,
}: DocsSidebarProps) { }: DocsSidebarProps) {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const searchParams = useSearchParams();
const router = useRouter(); const router = useRouter();
const filteredComponents = COMPONENTS_REGISTRY.filter((c) => const filteredComponents = COMPONENTS_REGISTRY.filter((c) =>
@@ -51,21 +50,21 @@ export function DocsSidebar({
.sort((a, b) => a.name.localeCompare(b.name)); .sort((a, b) => a.name.localeCompare(b.name));
const onCategorySelect = (category: string | null) => { const onCategorySelect = (category: string | null) => {
const sp = new URLSearchParams(searchParams); const searchParams = new URLSearchParams(window.location.search);
sp.set('category', category || ''); searchParams.set('category', category || '');
router.push(`/components?${sp.toString()}`); router.push(`/components?${searchParams.toString()}`);
}; };
const onComponentSelect = (component: ComponentInfo) => { const onComponentSelect = (component: ComponentInfo) => {
const sp = new URLSearchParams(searchParams); const searchParams = new URLSearchParams(window.location.search);
sp.set('component', component.name); searchParams.set('component', component.name);
router.push(`/components?${sp.toString()}`); router.push(`/components?${searchParams.toString()}`);
}; };
return ( return (
<div className="bg-muted/30 flex h-screen w-80 flex-col overflow-hidden border-r"> <div className="bg-muted/30 flex h-screen w-80 flex-col overflow-hidden border-r">
{/* Header */} {/* Header */}
<div className="shrink-0 border-b p-4"> <div className="flex-shrink-0 border-b p-4">
<div className="mb-2 flex items-center gap-2"> <div className="mb-2 flex items-center gap-2">
<Code2 className="text-primary h-6 w-6" /> <Code2 className="text-primary h-6 w-6" />
@@ -78,14 +77,13 @@ export function DocsSidebar({
</div> </div>
{/* Controls */} {/* Controls */}
<div className="shrink-0 space-y-2 border-b p-4"> <div className="flex-shrink-0 space-y-2 border-b p-4">
{/* Category Select */} {/* Category Select */}
<div className="space-y-2"> <div className="space-y-2">
<Select <Select
defaultValue={selectedCategory || 'all'} value={selectedCategory || 'all'}
onValueChange={(value) => { onValueChange={(value) => {
const category = value === 'all' ? null : value; const category = value === 'all' ? null : value;
onCategorySelect(category); onCategorySelect(category);
// Select first component in the filtered results // Select first component in the filtered results
@@ -98,12 +96,8 @@ export function DocsSidebar({
} }
}} }}
> >
<SelectTrigger className="w-full"> <SelectTrigger>
<SelectValue> <SelectValue placeholder={'Select a category'} />
{(category) => {
return category === 'all' ? 'All Categories' : category;
}}
</SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -160,7 +154,7 @@ export function DocsSidebar({
{/* Components List - Scrollable */} {/* Components List - Scrollable */}
<div className="flex flex-1 flex-col overflow-y-auto"> <div className="flex flex-1 flex-col overflow-y-auto">
<div className="shrink-0 p-4 pb-2"> <div className="flex-shrink-0 p-4 pb-2">
<h3 className="flex items-center gap-2 text-sm font-semibold"> <h3 className="flex items-center gap-2 text-sm font-semibold">
<FileText className="h-4 w-4" /> <FileText className="h-4 w-4" />
Components Components

View File

@@ -101,18 +101,13 @@ const examples = [
return ( return (
<div className="flex min-h-32 items-center justify-center"> <div className="flex min-h-32 items-center justify-center">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger asChild>
render={ <Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Button
variant="ghost"
className="relative h-8 w-8 rounded-full"
/>
}
>
<Avatar className="h-8 w-8"> <Avatar className="h-8 w-8">
<AvatarImage src="/avatars/01.png" alt="@username" /> <AvatarImage src="/avatars/01.png" alt="@username" />
<AvatarFallback>JD</AvatarFallback> <AvatarFallback>JD</AvatarFallback>
</Avatar> </Avatar>
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount> <DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal"> <DropdownMenuLabel className="font-normal">
@@ -190,11 +185,11 @@ const examples = [
</div> </div>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger asChild>
render={<Button variant="ghost" className="h-8 w-8 p-0" />} <Button variant="ghost" className="h-8 w-8 p-0">
>
<span className="sr-only">Open menu</span> <span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48"> <DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem onClick={() => setSelectedAction('open')}> <DropdownMenuItem onClick={() => setSelectedAction('open')}>
@@ -280,9 +275,11 @@ const examples = [
return ( return (
<div className="flex min-h-48 items-center justify-center"> <div className="flex min-h-48 items-center justify-center">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}> <DropdownMenuTrigger asChild>
<Button variant="outline">
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Create New Create New
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-56"> <DropdownMenuContent className="w-56">
<DropdownMenuLabel>Create Content</DropdownMenuLabel> <DropdownMenuLabel>Create Content</DropdownMenuLabel>
@@ -396,11 +393,11 @@ const examples = [
<span className="text-sm">Appearance & Layout</span> <span className="text-sm">Appearance & Layout</span>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger asChild>
render={<Button variant="outline" size="sm" />} <Button variant="outline" size="sm">
>
<Settings className="mr-2 h-4 w-4" /> <Settings className="mr-2 h-4 w-4" />
Configure Configure
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-64" align="end"> <DropdownMenuContent className="w-64" align="end">
<DropdownMenuLabel>View Options</DropdownMenuLabel> <DropdownMenuLabel>View Options</DropdownMenuLabel>
@@ -550,10 +547,10 @@ const examples = [
</div> </div>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger asChild>
render={<Button variant="ghost" className="h-8 w-8 p-0" />} <Button variant="ghost" className="h-8 w-8 p-0">
>
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48"> <DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem <DropdownMenuItem
@@ -866,7 +863,7 @@ export default function DropdownMenuStory() {
modal: controls.modal ? true : undefined, modal: controls.modal ? true : undefined,
}); });
const dropdownStructure = `<DropdownMenu${rootProps}>\n <DropdownMenuTrigger render={<Button variant="outline" />}>\n Open Menu\n </DropdownMenuTrigger>\n <DropdownMenuContent${contentProps}>\n <DropdownMenuItem>\n <User className="mr-2 h-4 w-4" />\n <span>Profile</span>\n </DropdownMenuItem>\n <DropdownMenuItem>\n <Settings className="mr-2 h-4 w-4" />\n <span>Settings</span>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem>\n <LogOut className="mr-2 h-4 w-4" />\n <span>Log out</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n</DropdownMenu>`; const dropdownStructure = `<DropdownMenu${rootProps}>\n <DropdownMenuTrigger asChild>\n <Button variant="outline">Open Menu</Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent${contentProps}>\n <DropdownMenuItem>\n <User className="mr-2 h-4 w-4" />\n <span>Profile</span>\n </DropdownMenuItem>\n <DropdownMenuItem>\n <Settings className="mr-2 h-4 w-4" />\n <span>Settings</span>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem>\n <LogOut className="mr-2 h-4 w-4" />\n <span>Log out</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n</DropdownMenu>`;
return `${importStatement}\n${buttonImport}\n${iconImport}\n\n${dropdownStructure}`; return `${importStatement}\n${buttonImport}\n${iconImport}\n\n${dropdownStructure}`;
}; };
@@ -974,8 +971,8 @@ export default function DropdownMenuStory() {
const previewContent = ( const previewContent = (
<div className="flex justify-center p-6"> <div className="flex justify-center p-6">
<DropdownMenu modal={controls.modal}> <DropdownMenu modal={controls.modal}>
<DropdownMenuTrigger render={<Button variant="outline" />}> <DropdownMenuTrigger asChild>
Open Menu <Button variant="outline">Open Menu</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
side={controls.side} side={controls.side}

View File

@@ -4,7 +4,7 @@ import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import * as z from 'zod'; import { z } from 'zod';
import { Badge } from '@kit/ui/badge'; import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
@@ -119,7 +119,7 @@ export default function FormStory() {
const formImport = generateImportStatement(formComponents, '@kit/ui/form'); const formImport = generateImportStatement(formComponents, '@kit/ui/form');
const inputImport = generateImportStatement(['Input'], '@kit/ui/input'); const inputImport = generateImportStatement(['Input'], '@kit/ui/input');
const buttonImport = generateImportStatement(['Button'], '@kit/ui/button'); const buttonImport = generateImportStatement(['Button'], '@kit/ui/button');
const hookFormImports = `import { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport * as z from 'zod';`; const hookFormImports = `import { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { z } from 'zod';`;
let schemaCode = ''; let schemaCode = '';
let formFieldsCode = ''; let formFieldsCode = '';
@@ -130,19 +130,19 @@ export default function FormStory() {
formFieldsCode = ` <FormField\n control={form.control}\n name="username"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Username</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="Enter username" {...field} />\n </FormControl>${controls.showDescriptions ? '\n <FormDescription>\n Your public display name.\n </FormDescription>' : ''}${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="email"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Email</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="email" placeholder="Enter email" {...field} />\n </FormControl>${controls.showDescriptions ? "\n <FormDescription>\n We'll never share your email.\n </FormDescription>" : ''}${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`; formFieldsCode = ` <FormField\n control={form.control}\n name="username"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Username</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="Enter username" {...field} />\n </FormControl>${controls.showDescriptions ? '\n <FormDescription>\n Your public display name.\n </FormDescription>' : ''}${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="email"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Email</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="email" placeholder="Enter email" {...field} />\n </FormControl>${controls.showDescriptions ? "\n <FormDescription>\n We'll never share your email.\n </FormDescription>" : ''}${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
onSubmitCode = ` function onSubmit(values: z.output<typeof formSchema>) {\n console.log('Form submitted:', values);\n }`; onSubmitCode = ` function onSubmit(values: z.infer<typeof formSchema>) {\n console.log('Form submitted:', values);\n }`;
} else if (controls.formType === 'advanced') { } else if (controls.formType === 'advanced') {
schemaCode = `const formSchema = z.object({\n firstName: z.string().min(1, 'First name is required.'),\n lastName: z.string().min(1, 'Last name is required.'),\n email: z.string().email('Please enter a valid email address.'),\n});`; schemaCode = `const formSchema = z.object({\n firstName: z.string().min(1, 'First name is required.'),\n lastName: z.string().min(1, 'Last name is required.'),\n email: z.string().email('Please enter a valid email address.'),\n});`;
formFieldsCode = ` <FormField\n control={form.control}\n name="firstName"\n render={({ field }) => (\n <FormItem>\n <FormLabel>First Name</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="John" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="lastName"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Last Name</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="Doe" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`; formFieldsCode = ` <FormField\n control={form.control}\n name="firstName"\n render={({ field }) => (\n <FormItem>\n <FormLabel>First Name</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="John" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="lastName"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Last Name</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="Doe" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
onSubmitCode = ` function onSubmit(values: z.output<typeof formSchema>) {\n console.log('Advanced form submitted:', values);\n }`; onSubmitCode = ` function onSubmit(values: z.infer<typeof formSchema>) {\n console.log('Advanced form submitted:', values);\n }`;
} else { } else {
schemaCode = `const formSchema = z.object({\n password: z.string().min(8, 'Password must be at least 8 characters.'),\n confirmPassword: z.string(),\n}).refine((data) => data.password === data.confirmPassword, {\n message: 'Passwords do not match.',\n path: ['confirmPassword'],\n});`; schemaCode = `const formSchema = z.object({\n password: z.string().min(8, 'Password must be at least 8 characters.'),\n confirmPassword: z.string(),\n}).refine((data) => data.password === data.confirmPassword, {\n message: 'Passwords do not match.',\n path: ['confirmPassword'],\n});`;
formFieldsCode = ` <FormField\n control={form.control}\n name="password"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Password</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="password" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="confirmPassword"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Confirm Password</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="password" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`; formFieldsCode = ` <FormField\n control={form.control}\n name="password"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Password</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="password" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="confirmPassword"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Confirm Password</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="password" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
onSubmitCode = ` function onSubmit(values: z.output<typeof formSchema>) {\n console.log('Validation form submitted:', values);\n }`; onSubmitCode = ` function onSubmit(values: z.infer<typeof formSchema>) {\n console.log('Validation form submitted:', values);\n }`;
} }
const defaultValuesCode = const defaultValuesCode =
@@ -152,13 +152,13 @@ export default function FormStory() {
? ` defaultValues: {\n firstName: '',\n lastName: '',\n email: '',\n },` ? ` defaultValues: {\n firstName: '',\n lastName: '',\n email: '',\n },`
: ` defaultValues: {\n password: '',\n confirmPassword: '',\n },`; : ` defaultValues: {\n password: '',\n confirmPassword: '',\n },`;
const fullFormCode = `${hookFormImports}\n${formImport}\n${inputImport}\n${buttonImport}\n\n${schemaCode}\n\nfunction MyForm() {\n const form = useForm<z.output<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n${defaultValuesCode}\n });\n\n${onSubmitCode}\n\n return (\n <Form {...form}>\n <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">\n${formFieldsCode}\n <Button type="submit"${controls.disabled ? ' disabled' : ''}>Submit</Button>\n </form>\n </Form>\n );\n}`; const fullFormCode = `${hookFormImports}\n${formImport}\n${inputImport}\n${buttonImport}\n\n${schemaCode}\n\nfunction MyForm() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n${defaultValuesCode}\n });\n\n${onSubmitCode}\n\n return (\n <Form {...form}>\n <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">\n${formFieldsCode}\n <Button type="submit"${controls.disabled ? ' disabled' : ''}>Submit</Button>\n </form>\n </Form>\n );\n}`;
return fullFormCode; return fullFormCode;
}; };
// Basic form // Basic form
const basicForm = useForm<z.output<typeof basicFormSchema>>({ const basicForm = useForm<z.infer<typeof basicFormSchema>>({
resolver: zodResolver(basicFormSchema), resolver: zodResolver(basicFormSchema),
defaultValues: { defaultValues: {
username: '', username: '',
@@ -169,7 +169,7 @@ export default function FormStory() {
}); });
// Advanced form // Advanced form
const advancedForm = useForm<z.output<typeof advancedFormSchema>>({ const advancedForm = useForm<z.infer<typeof advancedFormSchema>>({
resolver: zodResolver(advancedFormSchema), resolver: zodResolver(advancedFormSchema),
defaultValues: { defaultValues: {
firstName: '', firstName: '',
@@ -183,7 +183,7 @@ export default function FormStory() {
}); });
// Validation form // Validation form
const validationForm = useForm<z.output<typeof validationFormSchema>>({ const validationForm = useForm<z.infer<typeof validationFormSchema>>({
resolver: zodResolver(validationFormSchema), resolver: zodResolver(validationFormSchema),
defaultValues: { defaultValues: {
password: '', password: '',
@@ -1056,7 +1056,7 @@ export default function FormStory() {
<pre className="overflow-x-auto text-sm"> <pre className="overflow-x-auto text-sm">
{`import { useForm } from 'react-hook-form'; {`import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod'; import { z } from 'zod';
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@kit/ui/form'; import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@kit/ui/form';
const formSchema = z.object({ const formSchema = z.object({
@@ -1065,7 +1065,7 @@ const formSchema = z.object({
}); });
function MyForm() { function MyForm() {
const form = useForm<z.output<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
username: '', username: '',
@@ -1073,7 +1073,7 @@ function MyForm() {
}, },
}); });
function onSubmit(values: z.output<typeof formSchema>) { function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values); console.log(values);
} }

View File

@@ -99,7 +99,7 @@ export function KbdStory() {
let snippet = groupLines.join('\n'); let snippet = groupLines.join('\n');
if (controls.showTooltip) { if (controls.showTooltip) {
snippet = `<TooltipProvider>\n <Tooltip>\n <TooltipTrigger render={<Button variant="outline" />}>\n Command palette\n </TooltipTrigger>\n <TooltipContent className="flex items-center gap-2">\n <span>Press</span>\n ${groupLines.join('\n ')}\n </TooltipContent>\n </Tooltip>\n</TooltipProvider>`; snippet = `<TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant="outline">Command palette</Button>\n </TooltipTrigger>\n <TooltipContent className="flex items-center gap-2">\n <span>Press</span>\n ${groupLines.join('\n ')}\n </TooltipContent>\n </Tooltip>\n</TooltipProvider>`;
} }
return formatCodeBlock(snippet, [ return formatCodeBlock(snippet, [
@@ -115,11 +115,11 @@ export function KbdStory() {
{controls.showTooltip ? ( {controls.showTooltip ? (
<TooltipProvider delayDuration={200}> <TooltipProvider delayDuration={200}>
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger asChild>
render={<Button variant="outline" className="gap-2" />} <Button variant="outline" className="gap-2">
>
<Command className="h-4 w-4" /> <Command className="h-4 w-4" />
Command palette Command palette
</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="flex items-center gap-2"> <TooltipContent className="flex items-center gap-2">
<span>Press</span> <span>Press</span>

View File

@@ -136,13 +136,11 @@ export function SimpleDataTableStory() {
{controls.showActions && ( {controls.showActions && (
<TableCell> <TableCell>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger asChild>
render={ <Button variant="ghost" className="h-8 w-8 p-0">
<Button variant="ghost" className="h-8 w-8 p-0" />
}
>
<span className="sr-only">Open menu</span> <span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem <DropdownMenuItem

View File

@@ -100,7 +100,7 @@ export function SwitchStory() {
className: cn( className: cn(
controls.size === 'sm' && 'h-4 w-7', controls.size === 'sm' && 'h-4 w-7',
controls.size === 'lg' && 'h-6 w-11', controls.size === 'lg' && 'h-6 w-11',
controls.error && 'data-checked:bg-destructive', controls.error && 'data-[state=checked]:bg-destructive',
), ),
}; };
@@ -200,7 +200,7 @@ export function SwitchStory() {
className={cn( className={cn(
controls.size === 'sm' && 'h-4 w-7', controls.size === 'sm' && 'h-4 w-7',
controls.size === 'lg' && 'h-6 w-11', controls.size === 'lg' && 'h-6 w-11',
controls.error && 'data-checked:bg-destructive', controls.error && 'data-[state=checked]:bg-destructive',
)} )}
/> />
); );
@@ -616,7 +616,7 @@ export function SwitchStory() {
</Label> </Label>
<Switch <Switch
id="error-switch" id="error-switch"
className="data-checked:bg-destructive" className="data-[state=checked]:bg-destructive"
/> />
</div> </div>
<p className="text-destructive text-sm"> <p className="text-destructive text-sm">
@@ -642,7 +642,7 @@ export function SwitchStory() {
<div> <div>
<h4 className="mb-3 text-lg font-semibold">Switch</h4> <h4 className="mb-3 text-lg font-semibold">Switch</h4>
<p className="text-muted-foreground mb-3 text-sm"> <p className="text-muted-foreground mb-3 text-sm">
A toggle switch component for boolean states. Built on Base UI A toggle switch component for boolean states. Built on Radix UI
Switch primitive. Switch primitive.
</p> </p>
<div className="overflow-x-auto"> <div className="overflow-x-auto">

View File

@@ -62,9 +62,9 @@ interface TabsControlsProps {
const variantClasses = { const variantClasses = {
default: '', default: '',
pills: pills:
'[&>div]:bg-background [&>div]:border [&>div]:rounded-lg [&>div]:p-1 [&_button]:rounded-md [&_button[data-active]]:bg-primary [&_button[data-active]]:text-primary-foreground', '[&>div]:bg-background [&>div]:border [&>div]:rounded-lg [&>div]:p-1 [&_button]:rounded-md [&_button[data-state=active]]:bg-primary [&_button[data-state=active]]:text-primary-foreground',
underline: underline:
'[&>div]:bg-transparent [&>div]:border-b [&>div]:rounded-none [&_button]:rounded-none [&_button]:border-b-2 [&_button]:border-transparent [&_button[data-active]]:border-primary [&_button[data-active]]:bg-transparent', '[&>div]:bg-transparent [&>div]:border-b [&>div]:rounded-none [&_button]:rounded-none [&_button]:border-b-2 [&_button]:border-transparent [&_button[data-state=active]]:border-primary [&_button[data-state=active]]:bg-transparent',
}; };
const sizeClasses = { const sizeClasses = {
@@ -683,28 +683,28 @@ function App() {
<TabsList className="h-auto rounded-none border-b bg-transparent p-0"> <TabsList className="h-auto rounded-none border-b bg-transparent p-0">
<TabsTrigger <TabsTrigger
value="overview" value="overview"
className="data-active:border-primary rounded-none border-b-2 border-transparent data-active:bg-transparent" className="data-[state=active]:border-primary rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent"
> >
<BarChart3 className="mr-2 h-4 w-4" /> <BarChart3 className="mr-2 h-4 w-4" />
Overview Overview
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="users" value="users"
className="data-active:border-primary rounded-none border-b-2 border-transparent data-active:bg-transparent" className="data-[state=active]:border-primary rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent"
> >
<User className="mr-2 h-4 w-4" /> <User className="mr-2 h-4 w-4" />
Users Users
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="revenue" value="revenue"
className="data-active:border-primary rounded-none border-b-2 border-transparent data-active:bg-transparent" className="data-[state=active]:border-primary rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent"
> >
<CreditCard className="mr-2 h-4 w-4" /> <CreditCard className="mr-2 h-4 w-4" />
Revenue Revenue
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="reports" value="reports"
className="data-active:border-primary rounded-none border-b-2 border-transparent data-active:bg-transparent" className="data-[state=active]:border-primary rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent"
> >
<FileText className="mr-2 h-4 w-4" /> <FileText className="mr-2 h-4 w-4" />
Reports Reports
@@ -905,7 +905,8 @@ const apiReference = {
{ {
name: '...props', name: '...props',
type: 'React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>', type: 'React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>',
description: 'All additional props from Base UI Tabs.Root component.', description:
'All props from Radix UI Tabs.Root component including asChild, id, etc.',
}, },
], ],
examples: [ examples: [

View File

@@ -144,23 +144,22 @@ function TooltipStory() {
let code = `<TooltipProvider${providerPropsString}>\n`; let code = `<TooltipProvider${providerPropsString}>\n`;
code += ` <Tooltip>\n`; code += ` <Tooltip>\n`;
code += ` <TooltipTrigger asChild>\n`;
if (controls.triggerType === 'button') { if (controls.triggerType === 'button') {
code += ` <TooltipTrigger render={<Button variant="${controls.triggerVariant}" />}>\n`; code += ` <Button variant="${controls.triggerVariant}">Hover me</Button>\n`;
code += ` Hover me\n`;
} else if (controls.triggerType === 'icon') { } else if (controls.triggerType === 'icon') {
code += ` <Button variant="${controls.triggerVariant}" size="icon">\n`;
const iconName = selectedIconData?.icon.name || 'Info'; const iconName = selectedIconData?.icon.name || 'Info';
code += ` <TooltipTrigger render={<Button variant="${controls.triggerVariant}" size="icon" />}>\n`;
code += ` <${iconName} className="h-4 w-4" />\n`; code += ` <${iconName} className="h-4 w-4" />\n`;
code += ` </Button>\n`;
} else if (controls.triggerType === 'text') { } else if (controls.triggerType === 'text') {
code += ` <TooltipTrigger render={<span className="cursor-help underline decoration-dotted" />}>\n`; code += ` <span className="cursor-help underline decoration-dotted">Hover me</span>\n`;
code += ` Hover me\n`;
} else if (controls.triggerType === 'input') { } else if (controls.triggerType === 'input') {
code += ` <TooltipTrigger render={<Input placeholder="Hover over this input" />} />\n`; code += ` <Input placeholder="Hover over this input" />\n`;
} }
if (controls.triggerType !== 'input') {
code += ` </TooltipTrigger>\n`; code += ` </TooltipTrigger>\n`;
}
code += ` <TooltipContent${contentPropsString}>\n`; code += ` <TooltipContent${contentPropsString}>\n`;
code += ` <p>${controls.content}</p>\n`; code += ` <p>${controls.content}</p>\n`;
code += ` </TooltipContent>\n`; code += ` </TooltipContent>\n`;
@@ -171,50 +170,28 @@ function TooltipStory() {
}; };
const renderPreview = () => { const renderPreview = () => {
const renderTrigger = () => { const trigger = (() => {
switch (controls.triggerType) { switch (controls.triggerType) {
case 'button': case 'button':
return ( return <Button variant={controls.triggerVariant}>Hover me</Button>;
<TooltipTrigger
render={<Button variant={controls.triggerVariant} />}
>
Hover me
</TooltipTrigger>
);
case 'icon': case 'icon':
return ( return (
<TooltipTrigger <Button variant={controls.triggerVariant} size="icon">
render={<Button variant={controls.triggerVariant} size="icon" />}
>
<IconComponent className="h-4 w-4" /> <IconComponent className="h-4 w-4" />
</TooltipTrigger> </Button>
); );
case 'text': case 'text':
return ( return (
<TooltipTrigger <span className="cursor-help underline decoration-dotted">
render={
<span className="cursor-help underline decoration-dotted" />
}
>
Hover me Hover me
</TooltipTrigger> </span>
); );
case 'input': case 'input':
return ( return <Input placeholder="Hover over this input" />;
<TooltipTrigger
render={<Input placeholder="Hover over this input" />}
/>
);
default: default:
return ( return <Button variant={controls.triggerVariant}>Hover me</Button>;
<TooltipTrigger
render={<Button variant={controls.triggerVariant} />}
>
Hover me
</TooltipTrigger>
);
} }
}; })();
return ( return (
<div className="flex min-h-[200px] items-center justify-center"> <div className="flex min-h-[200px] items-center justify-center">
@@ -224,7 +201,7 @@ function TooltipStory() {
disableHoverableContent={controls.disableHoverableContent} disableHoverableContent={controls.disableHoverableContent}
> >
<Tooltip> <Tooltip>
{renderTrigger()} <TooltipTrigger asChild>{trigger}</TooltipTrigger>
<TooltipContent <TooltipContent
side={controls.side} side={controls.side}
align={controls.align} align={controls.align}
@@ -399,9 +376,11 @@ function TooltipStory() {
<TooltipProvider> <TooltipProvider>
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
<Tooltip> <Tooltip>
<TooltipTrigger render={<Button variant="outline" />}> <TooltipTrigger asChild>
<Button variant="outline">
<Info className="mr-2 h-4 w-4" /> <Info className="mr-2 h-4 w-4" />
Info Button Info Button
</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>This provides additional information</p> <p>This provides additional information</p>
@@ -409,8 +388,10 @@ function TooltipStory() {
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger render={<Button variant="ghost" size="icon" />}> <TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<HelpCircle className="h-4 w-4" /> <HelpCircle className="h-4 w-4" />
</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Click for help documentation</p> <p>Click for help documentation</p>
@@ -418,12 +399,10 @@ function TooltipStory() {
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger asChild>
render={ <span className="cursor-help underline decoration-dotted">
<span className="cursor-help underline decoration-dotted" />
}
>
Hover for explanation Hover for explanation
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>This term needs clarification for better understanding</p> <p>This term needs clarification for better understanding</p>
@@ -431,9 +410,9 @@ function TooltipStory() {
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger asChild>
render={<Input placeholder="Hover me" className="w-48" />} <Input placeholder="Hover me" className="w-48" />
/> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Enter your email address here</p> <p>Enter your email address here</p>
</TooltipContent> </TooltipContent>
@@ -455,10 +434,10 @@ function TooltipStory() {
{/* Top Row */} {/* Top Row */}
<div></div> <div></div>
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger asChild>
render={<Button variant="outline" size="sm" />} <Button variant="outline" size="sm">
>
Top Top
</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side="top">
<p>Tooltip on top</p> <p>Tooltip on top</p>
@@ -468,10 +447,10 @@ function TooltipStory() {
{/* Middle Row */} {/* Middle Row */}
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger asChild>
render={<Button variant="outline" size="sm" />} <Button variant="outline" size="sm">
>
Left Left
</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="left"> <TooltipContent side="left">
<p>Tooltip on left</p> <p>Tooltip on left</p>
@@ -481,10 +460,10 @@ function TooltipStory() {
<span className="text-muted-foreground text-sm">Center</span> <span className="text-muted-foreground text-sm">Center</span>
</div> </div>
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger asChild>
render={<Button variant="outline" size="sm" />} <Button variant="outline" size="sm">
>
Right Right
</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right"> <TooltipContent side="right">
<p>Tooltip on right</p> <p>Tooltip on right</p>
@@ -494,10 +473,10 @@ function TooltipStory() {
{/* Bottom Row */} {/* Bottom Row */}
<div></div> <div></div>
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger asChild>
render={<Button variant="outline" size="sm" />} <Button variant="outline" size="sm">
>
Bottom Bottom
</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom"> <TooltipContent side="bottom">
<p>Tooltip on bottom</p> <p>Tooltip on bottom</p>
@@ -519,9 +498,11 @@ function TooltipStory() {
<TooltipProvider> <TooltipProvider>
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
<Tooltip> <Tooltip>
<TooltipTrigger render={<Button variant="outline" />}> <TooltipTrigger asChild>
<Button variant="outline">
<Star className="mr-2 h-4 w-4" /> <Star className="mr-2 h-4 w-4" />
Premium Feature Premium Feature
</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="max-w-xs"> <TooltipContent className="max-w-xs">
<div className="space-y-1"> <div className="space-y-1">
@@ -535,9 +516,11 @@ function TooltipStory() {
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger render={<Button variant="outline" />}> <TooltipTrigger asChild>
<Button variant="outline">
<Settings className="mr-2 h-4 w-4" /> <Settings className="mr-2 h-4 w-4" />
Advanced Settings Advanced Settings
</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<div className="space-y-1"> <div className="space-y-1">
@@ -554,9 +537,11 @@ function TooltipStory() {
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger render={<Button variant="destructive" />}> <TooltipTrigger asChild>
<Button variant="destructive">
<AlertCircle className="mr-2 h-4 w-4" /> <AlertCircle className="mr-2 h-4 w-4" />
Delete Account Delete Account
</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="border-destructive bg-destructive text-destructive-foreground max-w-xs"> <TooltipContent className="border-destructive bg-destructive text-destructive-foreground max-w-xs">
<div className="space-y-1"> <div className="space-y-1">
@@ -583,10 +568,10 @@ function TooltipStory() {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger asChild>
render={<Button size="icon" variant="ghost" />} <Button size="icon" variant="ghost">
>
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />
</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Copy to clipboard</p> <p>Copy to clipboard</p>
@@ -594,10 +579,10 @@ function TooltipStory() {
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger asChild>
render={<Button size="icon" variant="ghost" />} <Button size="icon" variant="ghost">
>
<Download className="h-4 w-4" /> <Download className="h-4 w-4" />
</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Download file</p> <p>Download file</p>
@@ -605,10 +590,10 @@ function TooltipStory() {
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger asChild>
render={<Button size="icon" variant="ghost" />} <Button size="icon" variant="ghost">
>
<Share className="h-4 w-4" /> <Share className="h-4 w-4" />
</Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Share with others</p> <p>Share with others</p>
@@ -620,11 +605,9 @@ function TooltipStory() {
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="username">Username</Label> <Label htmlFor="username">Username</Label>
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger asChild>
render={
<Input id="username" placeholder="Enter username" /> <Input id="username" placeholder="Enter username" />
} </TooltipTrigger>
/>
<TooltipContent> <TooltipContent>
<p>Must be 3-20 characters, letters and numbers only</p> <p>Must be 3-20 characters, letters and numbers only</p>
</TooltipContent> </TooltipContent>
@@ -633,7 +616,9 @@ function TooltipStory() {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Tooltip> <Tooltip>
<TooltipTrigger render={<Checkbox id="terms" />} /> <TooltipTrigger asChild>
<Checkbox id="terms" />
</TooltipTrigger>
<TooltipContent className="max-w-xs"> <TooltipContent className="max-w-xs">
<p> <p>
By checking this, you agree to our Terms of Service and By checking this, you agree to our Terms of Service and
@@ -766,7 +751,7 @@ function TooltipStory() {
</li> </li>
<li> <li>
<strong>TooltipTrigger:</strong> Element that triggers the <strong>TooltipTrigger:</strong> Element that triggers the
tooltip (use render prop) tooltip (use asChild prop)
</li> </li>
</ul> </ul>
</div> </div>

View File

@@ -492,7 +492,7 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
status: 'stable', status: 'stable',
component: CardButtonStory, component: CardButtonStory,
sourceFile: '@kit/ui/card-button', sourceFile: '@kit/ui/card-button',
props: ['className', 'children', 'onClick', 'disabled'], props: ['asChild', 'className', 'children', 'onClick', 'disabled'],
icon: MousePointer, icon: MousePointer,
}, },
@@ -950,7 +950,7 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
status: 'stable', status: 'stable',
component: ItemStory, component: ItemStory,
sourceFile: '@kit/ui/item', sourceFile: '@kit/ui/item',
props: ['variant', 'size', 'className'], props: ['variant', 'size', 'asChild', 'className'],
icon: Layers, icon: Layers,
}, },
@@ -1004,7 +1004,7 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
status: 'stable', status: 'stable',
component: BreadcrumbStory, component: BreadcrumbStory,
sourceFile: '@kit/ui/breadcrumb', sourceFile: '@kit/ui/breadcrumb',
props: ['separator', 'href', 'className'], props: ['separator', 'asChild', 'href', 'className'],
icon: ChevronRight, icon: ChevronRight,
}, },

View File

@@ -1,3 +1,4 @@
import { withI18n } from '../../lib/i18n/with-i18n';
import { DocsContent } from './components/docs-content'; import { DocsContent } from './components/docs-content';
import { DocsHeader } from './components/docs-header'; import { DocsHeader } from './components/docs-header';
import { DocsSidebar } from './components/docs-sidebar'; import { DocsSidebar } from './components/docs-sidebar';
@@ -28,4 +29,4 @@ async function ComponentDocsPage(props: ComponentDocsPageProps) {
); );
} }
export default ComponentDocsPage; export default withI18n(ComponentDocsPage);

View File

@@ -67,10 +67,10 @@ export default async function EmailPage(props: EmailPageProps) {
Remember that the below is an approximation of the email. Always test Remember that the below is an approximation of the email. Always test
it in your inbox.{' '} it in your inbox.{' '}
<Dialog> <Dialog>
<DialogTrigger <DialogTrigger asChild>
render={<Button variant={'link'} className="p-0 underline" />} <Button variant={'link'} className="p-0 underline">
>
Test Email Test Email
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>

View File

@@ -1,4 +1,4 @@
import * as z from 'zod'; import { z } from 'zod';
export const EmailTesterFormSchema = z.object({ export const EmailTesterFormSchema = z.object({
username: z.string().min(1), username: z.string().min(1),

View File

@@ -49,16 +49,13 @@ export default async function EmailsPage() {
<div className={'grid grid-cols-1 gap-4 md:grid-cols-4'}> <div className={'grid grid-cols-1 gap-4 md:grid-cols-4'}>
{categoryTemplates.map((template) => ( {categoryTemplates.map((template) => (
<CardButton <CardButton key={template.id} asChild>
key={template.id}
render={
<Link href={`/emails/${template.id}`}> <Link href={`/emails/${template.id}`}>
<CardButtonHeader> <CardButtonHeader>
<CardButtonTitle>{template.name}</CardButtonTitle> <CardButtonTitle>{template.name}</CardButtonTitle>
</CardButtonHeader> </CardButtonHeader>
</Link> </Link>
} </CardButton>
/>
))} ))}
</div> </div>
</div> </div>

View File

@@ -2,7 +2,6 @@ import type { Metadata } from 'next';
import { DevToolLayout } from '@/components/app-layout'; import { DevToolLayout } from '@/components/app-layout';
import { RootProviders } from '@/components/root-providers'; import { RootProviders } from '@/components/root-providers';
import { getMessages } from 'next-intl/server';
import '../styles/globals.css'; import '../styles/globals.css';
@@ -11,17 +10,15 @@ export const metadata: Metadata = {
description: 'The dev tool for Makerkit', description: 'The dev tool for Makerkit',
}; };
export default async function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const messages = await getMessages();
return ( return (
<html lang="en"> <html lang="en">
<body> <body>
<RootProviders messages={messages}> <RootProviders>
<DevToolLayout>{children}</DevToolLayout> <DevToolLayout>{children}</DevToolLayout>
</RootProviders> </RootProviders>
</body> </body>

View File

@@ -37,6 +37,7 @@ export default async function DashboardPage() {
return ( return (
<Page style={'custom'}> <Page style={'custom'}>
<PageHeader <PageHeader
displaySidebarTrigger={false}
title={'Dev Tool'} title={'Dev Tool'}
description={'Kit MCP status for this workspace'} description={'Kit MCP status for this workspace'}
/> />

View File

@@ -1,4 +1,4 @@
import * as z from 'zod'; import { z } from 'zod';
export const CreatePRDSchema = z.object({ export const CreatePRDSchema = z.object({
title: z title: z
@@ -32,4 +32,4 @@ export const CreatePRDSchema = z.object({
.min(1, 'At least one success metric is required'), .min(1, 'At least one success metric is required'),
}); });
export type CreatePRDData = z.output<typeof CreatePRDSchema>; export type CreatePRDData = z.infer<typeof CreatePRDSchema>;

View File

@@ -131,14 +131,12 @@ export function TranslationsComparison({
<If condition={locales.length > 1}> <If condition={locales.length > 1}>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger asChild>
render={
<Button variant="outline" className="ml-auto"> <Button variant="outline" className="ml-auto">
Select Languages Select Languages
<ChevronDownIcon className="ml-2 h-4 w-4" /> <ChevronDownIcon className="ml-2 h-4 w-4" />
</Button> </Button>
} </DropdownMenuTrigger>
/>
<DropdownMenuContent align="end" className="w-[200px]"> <DropdownMenuContent align="end" className="w-[200px]">
{locales.map((locale) => ( {locales.map((locale) => (

View File

@@ -2,7 +2,7 @@
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import * as z from 'zod'; import { z } from 'zod';
import { findWorkspaceRoot } from '@kit/mcp-server/env'; import { findWorkspaceRoot } from '@kit/mcp-server/env';
import { import {

View File

@@ -731,15 +731,13 @@ function FilterSwitcher(props: {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger asChild>
render={
<Button variant="outline" className="font-normal"> <Button variant="outline" className="font-normal">
{buttonLabel()} {buttonLabel()}
<ChevronsUpDownIcon className="text-muted-foreground ml-1 h-3 w-3" /> <ChevronsUpDownIcon className="text-muted-foreground ml-1 h-3 w-3" />
</Button> </Button>
} </DropdownMenuTrigger>
/>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
@@ -888,8 +886,7 @@ function Summary({ appState }: { appState: AppEnvState }) {
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger asChild>
render={
<Button <Button
variant="outline" variant="outline"
size={'sm'} size={'sm'}
@@ -913,16 +910,14 @@ function Summary({ appState }: { appState: AppEnvState }) {
toast.promise(promise, { toast.promise(promise, {
loading: 'Copying environment variables...', loading: 'Copying environment variables...',
success: 'Environment variables copied to clipboard.', success: 'Environment variables copied to clipboard.',
error: error: 'Failed to copy environment variables to clipboard',
'Failed to copy environment variables to clipboard',
}); });
}} }}
> >
<CopyIcon className={'mr-2 h-4 w-4'} /> <CopyIcon className={'mr-2 h-4 w-4'} />
<span>Copy env file to clipboard</span> <span>Copy env file to clipboard</span>
</Button> </Button>
} </TooltipTrigger>
/>
<TooltipContent> <TooltipContent>
Copy environment variables to clipboard. You can place it in your Copy environment variables to clipboard. You can place it in your

View File

@@ -2,7 +2,7 @@
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import * as z from 'zod'; import { z } from 'zod';
import { import {
createKitEnvDeps, createKitEnvDeps,

View File

@@ -1,6 +1,6 @@
import { DevToolSidebar } from '@/components/app-sidebar'; import { DevToolSidebar } from '@/components/app-sidebar';
import { SidebarInset, SidebarProvider } from '@kit/ui/sidebar'; import { SidebarInset, SidebarProvider } from '@kit/ui/shadcn-sidebar';
export function DevToolLayout(props: React.PropsWithChildren) { export function DevToolLayout(props: React.PropsWithChildren) {
return ( return (

View File

@@ -24,7 +24,7 @@ import {
SidebarMenuSub, SidebarMenuSub,
SidebarMenuSubButton, SidebarMenuSubButton,
SidebarMenuSubItem, SidebarMenuSubItem,
} from '@kit/ui/sidebar'; } from '@kit/ui/shadcn-sidebar';
import { isRouteActive } from '@kit/ui/utils'; import { isRouteActive } from '@kit/ui/utils';
const routes = [ const routes = [
@@ -92,14 +92,14 @@ export function DevToolSidebar({
{route.children.map((child) => ( {route.children.map((child) => (
<SidebarMenuSubItem key={child.path}> <SidebarMenuSubItem key={child.path}>
<SidebarMenuSubButton <SidebarMenuSubButton
render={ asChild
isActive={isRouteActive(child.path, pathname, false)}
>
<Link href={child.path}> <Link href={child.path}>
<child.Icon className="h-4 w-4" /> <child.Icon className="h-4 w-4" />
<span>{child.label}</span> <span>{child.label}</span>
</Link> </Link>
} </SidebarMenuSubButton>
isActive={isRouteActive(child.path, pathname, false)}
/>
</SidebarMenuSubItem> </SidebarMenuSubItem>
))} ))}
</SidebarMenuSub> </SidebarMenuSub>
@@ -107,13 +107,13 @@ export function DevToolSidebar({
) : ( ) : (
<SidebarMenuButton <SidebarMenuButton
isActive={isRouteActive(route.path, pathname, false)} isActive={isRouteActive(route.path, pathname, false)}
render={ asChild
>
<Link href={route.path}> <Link href={route.path}>
<route.Icon className="h-4 w-4" /> <route.Icon className="h-4 w-4" />
<span>{route.label}</span> <span>{route.label}</span>
</Link> </Link>
} </SidebarMenuButton>
/>
)} )}
</SidebarMenuItem> </SidebarMenuItem>
))} ))}

View File

@@ -3,18 +3,18 @@
import { useState } from 'react'; import { useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { AbstractIntlMessages } from 'next-intl';
import { I18nClientProvider } from '@kit/i18n/provider'; import { I18nProvider } from '@kit/i18n/provider';
import { Toaster } from '@kit/ui/sonner'; import { Toaster } from '@kit/ui/sonner';
export function RootProviders( import { i18nResolver } from '../lib/i18n/i18n.resolver';
props: React.PropsWithChildren<{ messages: AbstractIntlMessages }>, import { getI18nSettings } from '../lib/i18n/i18n.settings';
) {
export function RootProviders(props: React.PropsWithChildren) {
return ( return (
<I18nClientProvider locale="en" messages={props.messages}> <I18nProvider settings={getI18nSettings('en')} resolver={i18nResolver}>
<ReactQueryProvider>{props.children}</ReactQueryProvider> <ReactQueryProvider>{props.children}</ReactQueryProvider>
</I18nClientProvider> </I18nProvider>
); );
} }

View File

@@ -33,7 +33,7 @@ interface ServiceCardProps {
export const ServiceCard = ({ name, status }: ServiceCardProps) => { export const ServiceCard = ({ name, status }: ServiceCardProps) => {
return ( return (
<Card className="w-full max-w-2xl"> <Card className="w-full max-w-2xl">
<CardContent> <CardContent className="p-4">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">

View File

@@ -1,26 +0,0 @@
import { getRequestConfig } from 'next-intl/server';
import account from '../../web/i18n/messages/en/account.json';
import auth from '../../web/i18n/messages/en/auth.json';
import billing from '../../web/i18n/messages/en/billing.json';
import common from '../../web/i18n/messages/en/common.json';
import marketing from '../../web/i18n/messages/en/marketing.json';
import teams from '../../web/i18n/messages/en/teams.json';
export default getRequestConfig(async () => {
return {
locale: 'en',
messages: {
common,
auth,
account,
teams,
billing,
marketing,
},
timeZone: 'UTC',
getMessageFallback(info) {
return info.key;
},
};
});

View File

@@ -0,0 +1,13 @@
import { createI18nServerInstance } from './i18n.server';
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;
export function withI18n<Params extends object>(
Component: LayoutOrPageComponent<Params>,
) {
return async function I18nServerComponentWrapper(params: Params) {
await createI18nServerInstance();
return <Component {...params} />;
};
}

View File

@@ -1,12 +1,8 @@
import type { NextConfig } from 'next'; import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
reactStrictMode: true, reactStrictMode: true,
transpilePackages: ['@kit/ui', '@kit/shared', '@kit/i18n'], transpilePackages: ['@kit/ui', '@kit/shared'],
reactCompiler: true, reactCompiler: true,
devIndicators: { devIndicators: {
position: 'bottom-right', position: 'bottom-right',
@@ -18,4 +14,4 @@ const nextConfig: NextConfig = {
}, },
}; };
export default withNextIntl(nextConfig); export default nextConfig;

View File

@@ -13,7 +13,6 @@
"@tanstack/react-query": "catalog:", "@tanstack/react-query": "catalog:",
"lucide-react": "catalog:", "lucide-react": "catalog:",
"next": "catalog:", "next": "catalog:",
"next-intl": "catalog:",
"nodemailer": "catalog:", "nodemailer": "catalog:",
"react": "catalog:", "react": "catalog:",
"react-dom": "catalog:", "react-dom": "catalog:",
@@ -36,7 +35,7 @@
"babel-plugin-react-compiler": "1.0.0", "babel-plugin-react-compiler": "1.0.0",
"pino-pretty": "13.0.0", "pino-pretty": "13.0.0",
"react-hook-form": "catalog:", "react-hook-form": "catalog:",
"recharts": "3.7.0", "recharts": "2.15.3",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
"tw-animate-css": "catalog:", "tw-animate-css": "catalog:",
"typescript": "^5.9.3", "typescript": "^5.9.3",

View File

@@ -66,6 +66,26 @@
--animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes fade-up { @keyframes fade-up {
0% { 0% {
opacity: 0; opacity: 0;

View File

@@ -4,7 +4,7 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"report": "playwright show-report", "report": "playwright show-report",
"test": "playwright test --max-failures=1 --workers=4", "test": "playwright test --max-failures=1",
"test:fast": "playwright test --max-failures=1 --workers=16", "test:fast": "playwright test --max-failures=1 --workers=16",
"test:setup": "playwright test tests/auth.setup.ts", "test:setup": "playwright test tests/auth.setup.ts",
"test:ui": "playwright test --ui" "test:ui": "playwright test --ui"

View File

@@ -38,8 +38,6 @@ test.describe('Account Settings', () => {
await Promise.all([request, response]); await Promise.all([request, response]);
await page.locator('[data-test="workspace-dropdown-trigger"]').click();
await expect(account.getProfileName()).toHaveText(name); await expect(account.getProfileName()).toHaveText(name);
}); });

View File

@@ -34,17 +34,17 @@ test.describe('Admin', () => {
await page.goto('/admin'); await page.goto('/admin');
// Check all stat cards are present // Check all stat cards are present
await expect(page.getByText('Users', { exact: true })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible();
await expect( await expect(
page.getByText('Team Accounts', { exact: true }), page.getByRole('heading', { name: 'Team Accounts' }),
).toBeVisible(); ).toBeVisible();
await expect( await expect(
page.getByText('Paying Customers', { exact: true }), page.getByRole('heading', { name: 'Paying Customers' }),
).toBeVisible(); ).toBeVisible();
await expect(page.getByText('Trials', { exact: true })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Trials' })).toBeVisible();
// Verify stat values are numbers // Verify stat values are numbers
const stats = await page.$$('.text-3xl.font-bold'); const stats = await page.$$('.text-3xl.font-bold');

View File

@@ -31,17 +31,8 @@ export class AuthPageObject {
} }
async signOut() { async signOut() {
const trigger = this.page.locator( await this.page.click('[data-test="account-dropdown-trigger"]');
'[data-test="workspace-dropdown-trigger"], [data-test="account-dropdown-trigger"]', await this.page.click('[data-test="account-dropdown-sign-out"]');
);
await trigger.click();
const signOutButton = this.page.locator(
'[data-test="workspace-sign-out"], [data-test="account-dropdown-sign-out"]',
);
await signOutButton.click();
} }
async signIn(params: { email: string; password: string }) { async signIn(params: { email: string; password: string }) {

View File

@@ -4,7 +4,7 @@ import { expect, test } from '@playwright/test';
test.describe('Healthcheck endpoint', () => { test.describe('Healthcheck endpoint', () => {
test('returns healthy status', async ({ request }) => { test('returns healthy status', async ({ request }) => {
const response = await request.get('/api/healthcheck'); const response = await request.get('/healthcheck');
expect(response.status()).toBe(200); expect(response.status()).toBe(200);

View File

@@ -46,7 +46,7 @@ export class InvitationsPageObject {
`[data-test="invite-member-form-item"]:nth-child(${nth}) [data-test="role-selector-trigger"]`, `[data-test="invite-member-form-item"]:nth-child(${nth}) [data-test="role-selector-trigger"]`,
); );
await this.page.getByRole('option', { name: invite.role }).click(); await this.page.click(`[data-test="role-option-${invite.role}"]`);
if (index < invites.length - 1) { if (index < invites.length - 1) {
await form.locator('[data-test="add-new-invite-button"]').click(); await form.locator('[data-test="add-new-invite-button"]').click();

View File

@@ -36,13 +36,13 @@ export class TeamAccountsPageObject {
} }
getTeamFromSelector(teamName: string) { getTeamFromSelector(teamName: string) {
return this.page.locator('[data-test="workspace-team-item"]', { return this.page.locator(`[data-test="account-selector-team"]`, {
hasText: teamName, hasText: teamName,
}); });
} }
getTeams() { getTeams() {
return this.page.locator('[data-test="workspace-team-item"]'); return this.page.locator('[data-test="account-selector-team"]');
} }
goToSettings() { goToSettings() {
@@ -83,11 +83,10 @@ export class TeamAccountsPageObject {
openAccountsSelector() { openAccountsSelector() {
return expect(async () => { return expect(async () => {
await this.page.click('[data-test="workspace-dropdown-trigger"]'); await this.page.click('[data-test="account-selector-trigger"]');
await this.page.click('[data-test="workspace-switch-submenu"]');
return expect( return expect(
this.page.locator('[data-test="workspace-switch-content"]'), this.page.locator('[data-test="account-selector-content"]'),
).toBeVisible(); ).toBeVisible();
}).toPass(); }).toPass();
} }
@@ -116,7 +115,7 @@ export class TeamAccountsPageObject {
async createTeam({ teamName, slug } = this.createTeamName()) { async createTeam({ teamName, slug } = this.createTeamName()) {
await this.openAccountsSelector(); await this.openAccountsSelector();
await this.page.click('[data-test="create-team-trigger"]'); await this.page.click('[data-test="create-team-account-trigger"]');
await this.page.fill( await this.page.fill(
'[data-test="create-team-form"] [data-test="team-name-input"]', '[data-test="create-team-form"] [data-test="team-name-input"]',
@@ -141,15 +140,14 @@ export class TeamAccountsPageObject {
await this.openAccountsSelector(); await this.openAccountsSelector();
await expect(this.getTeamFromSelector(teamName)).toBeVisible(); await expect(this.getTeamFromSelector(teamName)).toBeVisible();
// Close the selector (Escape closes submenu, then parent dropdown) // Close the selector
await this.page.keyboard.press('Escape');
await this.page.keyboard.press('Escape'); await this.page.keyboard.press('Escape');
} }
async createTeamWithNonLatinName(teamName: string, slug: string) { async createTeamWithNonLatinName(teamName: string, slug: string) {
await this.openAccountsSelector(); await this.openAccountsSelector();
await this.page.click('[data-test="create-team-trigger"]'); await this.page.click('[data-test="create-team-account-trigger"]');
await this.page.fill( await this.page.fill(
'[data-test="create-team-form"] [data-test="team-name-input"]', '[data-test="create-team-form"] [data-test="team-name-input"]',
@@ -179,8 +177,7 @@ export class TeamAccountsPageObject {
await this.openAccountsSelector(); await this.openAccountsSelector();
await expect(this.getTeamFromSelector(teamName)).toBeVisible(); await expect(this.getTeamFromSelector(teamName)).toBeVisible();
// Close the selector (Escape closes submenu, then parent dropdown) // Close the selector
await this.page.keyboard.press('Escape');
await this.page.keyboard.press('Escape'); await this.page.keyboard.press('Escape');
} }
@@ -210,10 +207,11 @@ export class TeamAccountsPageObject {
} }
async deleteAccount(email: string) { async deleteAccount(email: string) {
await expect(async () => {
await this.page.click('[data-test="delete-team-trigger"]'); await this.page.click('[data-test="delete-team-trigger"]');
await this.otp.completeOtpVerification(email); await this.otp.completeOtpVerification(email);
await expect(async () => {
const click = this.page.click( const click = this.page.click(
'[data-test="delete-team-form-confirm-button"]', '[data-test="delete-team-form-confirm-button"]',
); );

View File

@@ -88,7 +88,7 @@ test.describe('Team Accounts', () => {
await teamAccounts.createTeam(); await teamAccounts.createTeam();
await teamAccounts.openAccountsSelector(); await teamAccounts.openAccountsSelector();
await page.click('[data-test="create-team-trigger"]'); await page.click('[data-test="create-team-account-trigger"]');
await teamAccounts.tryCreateTeam('billing'); await teamAccounts.tryCreateTeam('billing');
@@ -202,7 +202,7 @@ test.describe('Team Accounts', () => {
// Use non-Latin name to trigger the slug field visibility // Use non-Latin name to trigger the slug field visibility
await teamAccounts.openAccountsSelector(); await teamAccounts.openAccountsSelector();
await page.click('[data-test="create-team-trigger"]'); await page.click('[data-test="create-team-account-trigger"]');
await page.fill( await page.fill(
'[data-test="create-team-form"] [data-test="team-name-input"]', '[data-test="create-team-form"] [data-test="team-name-input"]',

View File

@@ -38,7 +38,6 @@ NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION=true
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=true NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=true
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true
NEXT_PUBLIC_ENABLE_TEAMS_ACCOUNTS_ONLY=false
NEXT_PUBLIC_LANGUAGE_PRIORITY=application NEXT_PUBLIC_LANGUAGE_PRIORITY=application
# NEXTJS # NEXTJS

View File

@@ -0,0 +1,30 @@
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export async function generateMetadata() {
const { t } = await createI18nServerInstance();
return {
title: t('marketing:cookiePolicy'),
};
}
async function CookiePolicyPage() {
const { t } = await createI18nServerInstance();
return (
<div>
<SitePageHeader
title={t(`marketing:cookiePolicy`)}
subtitle={t(`marketing:cookiePolicyDescription`)}
/>
<div className={'container mx-auto py-8'}>
<div>Your terms of service content here</div>
</div>
</div>
);
}
export default withI18n(CookiePolicyPage);

View File

@@ -0,0 +1,30 @@
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export async function generateMetadata() {
const { t } = await createI18nServerInstance();
return {
title: t('marketing:privacyPolicy'),
};
}
async function PrivacyPolicyPage() {
const { t } = await createI18nServerInstance();
return (
<div>
<SitePageHeader
title={t('marketing:privacyPolicy')}
subtitle={t('marketing:privacyPolicyDescription')}
/>
<div className={'container mx-auto py-8'}>
<div>Your terms of service content here</div>
</div>
</div>
);
}
export default withI18n(PrivacyPolicyPage);

View File

@@ -0,0 +1,30 @@
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export async function generateMetadata() {
const { t } = await createI18nServerInstance();
return {
title: t('marketing:termsOfService'),
};
}
async function TermsOfServicePage() {
const { t } = await createI18nServerInstance();
return (
<div>
<SitePageHeader
title={t(`marketing:termsOfService`)}
subtitle={t(`marketing:termsOfServiceDescription`)}
/>
<div className={'container mx-auto py-8'}>
<div>Your terms of service content here</div>
</div>
</div>
);
}
export default withI18n(TermsOfServicePage);

View File

@@ -8,10 +8,10 @@ export function SiteFooter() {
return ( return (
<Footer <Footer
logo={<AppLogo className="w-[85px] md:w-[95px]" />} logo={<AppLogo className="w-[85px] md:w-[95px]" />}
description={<Trans i18nKey="marketing.footerDescription" />} description={<Trans i18nKey="marketing:footerDescription" />}
copyright={ copyright={
<Trans <Trans
i18nKey="marketing.copyright" i18nKey="marketing:copyright"
values={{ values={{
product: appConfig.name, product: appConfig.name,
year: new Date().getFullYear(), year: new Date().getFullYear(),
@@ -20,35 +20,35 @@ export function SiteFooter() {
} }
sections={[ sections={[
{ {
heading: <Trans i18nKey="marketing.about" />, heading: <Trans i18nKey="marketing:about" />,
links: [ links: [
{ href: '/blog', label: <Trans i18nKey="marketing.blog" /> }, { href: '/blog', label: <Trans i18nKey="marketing:blog" /> },
{ href: '/contact', label: <Trans i18nKey="marketing.contact" /> }, { href: '/contact', label: <Trans i18nKey="marketing:contact" /> },
], ],
}, },
{ {
heading: <Trans i18nKey="marketing.product" />, heading: <Trans i18nKey="marketing:product" />,
links: [ links: [
{ {
href: '/docs', href: '/docs',
label: <Trans i18nKey="marketing.documentation" />, label: <Trans i18nKey="marketing:documentation" />,
}, },
], ],
}, },
{ {
heading: <Trans i18nKey="marketing.legal" />, heading: <Trans i18nKey="marketing:legal" />,
links: [ links: [
{ {
href: '/terms-of-service', href: '/terms-of-service',
label: <Trans i18nKey="marketing.termsOfService" />, label: <Trans i18nKey="marketing:termsOfService" />,
}, },
{ {
href: '/privacy-policy', href: '/privacy-policy',
label: <Trans i18nKey="marketing.privacyPolicy" />, label: <Trans i18nKey="marketing:privacyPolicy" />,
}, },
{ {
href: '/cookie-policy', href: '/cookie-policy',
label: <Trans i18nKey="marketing.cookiePolicy" />, label: <Trans i18nKey="marketing:cookiePolicy" />,
}, },
], ],
}, },

View File

@@ -31,7 +31,6 @@ const MobileModeToggle = dynamic(
const paths = { const paths = {
home: pathsConfig.app.home, home: pathsConfig.app.home,
profileSettings: pathsConfig.app.personalAccountSettings,
}; };
const features = { const features = {
@@ -79,28 +78,26 @@ function AuthButtons() {
<div className={'flex items-center gap-x-2'}> <div className={'flex items-center gap-x-2'}>
<Button <Button
nativeButton={false}
className={'hidden md:flex md:text-sm'} className={'hidden md:flex md:text-sm'}
render={ asChild
<Link href={pathsConfig.auth.signIn}>
<Trans i18nKey={'auth.signIn'} />
</Link>
}
variant={'outline'} variant={'outline'}
size={'sm'} size={'sm'}
/> >
<Link href={pathsConfig.auth.signIn}>
<Trans i18nKey={'auth:signIn'} />
</Link>
</Button>
<Button <Button
nativeButton={false} asChild
render={
<Link href={pathsConfig.auth.signUp}>
<Trans i18nKey={'auth.signUp'} />
</Link>
}
className="text-xs md:text-sm" className="text-xs md:text-sm"
variant={'default'} variant={'default'}
size={'sm'} size={'sm'}
/> >
<Link href={pathsConfig.auth.signUp}>
<Trans i18nKey={'auth:signUp'} />
</Link>
</Button>
</div> </div>
</div> </div>
); );

View File

@@ -9,7 +9,7 @@ import { SiteNavigation } from './site-navigation';
export function SiteHeader(props: { user?: JWTUserData | null }) { export function SiteHeader(props: { user?: JWTUserData | null }) {
return ( return (
<Header <Header
logo={<AppLogo className="mx-auto sm:mx-0" href="/" />} logo={<AppLogo />}
navigation={<SiteNavigation />} navigation={<SiteNavigation />}
actions={<SiteHeaderAccountSection user={props.user ?? null} />} actions={<SiteHeaderAccountSection user={props.user ?? null} />}
/> />

View File

@@ -15,23 +15,23 @@ import { SiteNavigationItem } from './site-navigation-item';
const links = { const links = {
Blog: { Blog: {
label: 'marketing.blog', label: 'marketing:blog',
path: '/blog', path: '/blog',
}, },
Changelog: { Changelog: {
label: 'marketing.changelog', label: 'marketing:changelog',
path: '/changelog', path: '/changelog',
}, },
Docs: { Docs: {
label: 'marketing.documentation', label: 'marketing:documentation',
path: '/docs', path: '/docs',
}, },
Pricing: { Pricing: {
label: 'marketing.pricing', label: 'marketing:pricing',
path: '/pricing', path: '/pricing',
}, },
FAQ: { FAQ: {
label: 'marketing.faq', label: 'marketing:faq',
path: '/faq', path: '/faq',
}, },
}; };
@@ -74,14 +74,11 @@ function MobileDropdown() {
const className = 'flex w-full h-full items-center'; const className = 'flex w-full h-full items-center';
return ( return (
<DropdownMenuItem <DropdownMenuItem key={item.path} asChild>
key={item.path}
render={
<Link className={className} href={item.path}> <Link className={className} href={item.path}>
<Trans i18nKey={item.label} /> <Trans i18nKey={item.label} />
</Link> </Link>
} </DropdownMenuItem>
/>
); );
})} })}
</DropdownMenuContent> </DropdownMenuContent>

View File

@@ -6,6 +6,8 @@ import { notFound } from 'next/navigation';
import { createCmsClient } from '@kit/cms'; import { createCmsClient } from '@kit/cms';
import { withI18n } from '~/lib/i18n/with-i18n';
import { Post } from '../../blog/_components/post'; import { Post } from '../../blog/_components/post';
interface BlogPageProps { interface BlogPageProps {
@@ -73,4 +75,4 @@ async function BlogPost({ params }: BlogPageProps) {
); );
} }
export default BlogPost; export default withI18n(BlogPost);

View File

@@ -25,7 +25,7 @@ export function BlogPagination(props: {
}} }}
> >
<ArrowLeft className={'mr-2 h-4'} /> <ArrowLeft className={'mr-2 h-4'} />
<Trans i18nKey={'marketing.blogPaginationPrevious'} /> <Trans i18nKey={'marketing:blogPaginationPrevious'} />
</Button> </Button>
</If> </If>
@@ -36,7 +36,7 @@ export function BlogPagination(props: {
navigate(props.currentPage + 1); navigate(props.currentPage + 1);
}} }}
> >
<Trans i18nKey={'marketing.blogPaginationNext'} /> <Trans i18nKey={'marketing:blogPaginationNext'} />
<ArrowRight className={'ml-2 h-4'} /> <ArrowRight className={'ml-2 h-4'} />
</Button> </Button>
</If> </If>

View File

@@ -2,13 +2,14 @@ import { cache } from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { getLocale, getTranslations } from 'next-intl/server';
import { createCmsClient } from '@kit/cms'; import { createCmsClient } from '@kit/cms';
import { getLogger } from '@kit/shared/logger'; import { getLogger } from '@kit/shared/logger';
import { If } from '@kit/ui/if'; import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
// local imports // local imports
import { SitePageHeader } from '../_components/site-page-header'; import { SitePageHeader } from '../_components/site-page-header';
import { BlogPagination } from './_components/blog-pagination'; import { BlogPagination } from './_components/blog-pagination';
@@ -23,8 +24,7 @@ const BLOG_POSTS_PER_PAGE = 10;
export const generateMetadata = async ( export const generateMetadata = async (
props: BlogPageProps, props: BlogPageProps,
): Promise<Metadata> => { ): Promise<Metadata> => {
const t = await getTranslations('marketing'); const { t, resolvedLanguage } = await createI18nServerInstance();
const resolvedLanguage = await getLocale();
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const limit = BLOG_POSTS_PER_PAGE; const limit = BLOG_POSTS_PER_PAGE;
@@ -34,8 +34,8 @@ export const generateMetadata = async (
const { total } = await getContentItems(resolvedLanguage, limit, offset); const { total } = await getContentItems(resolvedLanguage, limit, offset);
return { return {
title: t('blog'), title: t('marketing:blog'),
description: t('blogSubtitle'), description: t('marketing:blogSubtitle'),
pagination: { pagination: {
previous: page > 0 ? `/blog?page=${page - 1}` : undefined, previous: page > 0 ? `/blog?page=${page - 1}` : undefined,
next: offset + limit < total ? `/blog?page=${page + 1}` : undefined, next: offset + limit < total ? `/blog?page=${page + 1}` : undefined,
@@ -67,8 +67,7 @@ const getContentItems = cache(
); );
async function BlogPage(props: BlogPageProps) { async function BlogPage(props: BlogPageProps) {
const t = await getTranslations('marketing'); const { t, resolvedLanguage: language } = await createI18nServerInstance();
const language = await getLocale();
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const limit = BLOG_POSTS_PER_PAGE; const limit = BLOG_POSTS_PER_PAGE;
@@ -83,12 +82,15 @@ async function BlogPage(props: BlogPageProps) {
return ( return (
<> <>
<SitePageHeader title={t('blog')} subtitle={t('blogSubtitle')} /> <SitePageHeader
title={t('marketing:blog')}
subtitle={t('marketing:blogSubtitle')}
/>
<div className={'container flex flex-col space-y-6 py-8'}> <div className={'container flex flex-col space-y-6 py-8'}>
<If <If
condition={posts.length > 0} condition={posts.length > 0}
fallback={<Trans i18nKey="marketing.noPosts" />} fallback={<Trans i18nKey="marketing:noPosts" />}
> >
<PostsGridList> <PostsGridList>
{posts.map((post, idx) => { {posts.map((post, idx) => {
@@ -109,7 +111,7 @@ async function BlogPage(props: BlogPageProps) {
); );
} }
export default BlogPage; export default withI18n(BlogPage);
function PostsGridList({ children }: React.PropsWithChildren) { function PostsGridList({ children }: React.PropsWithChildren) {
return ( return (

View File

@@ -6,6 +6,8 @@ import { notFound } from 'next/navigation';
import { createCmsClient } from '@kit/cms'; import { createCmsClient } from '@kit/cms';
import { withI18n } from '~/lib/i18n/with-i18n';
import { ChangelogDetail } from '../_components/changelog-detail'; import { ChangelogDetail } from '../_components/changelog-detail';
interface ChangelogEntryPageProps { interface ChangelogEntryPageProps {
@@ -105,4 +107,4 @@ async function ChangelogEntryPage({ params }: ChangelogEntryPageProps) {
); );
} }
export default ChangelogEntryPage; export default withI18n(ChangelogEntryPage);

View File

@@ -22,7 +22,7 @@ export function ChangelogHeader({ entry }: { entry: Cms.ContentItem }) {
className="text-muted-foreground hover:text-primary flex items-center gap-1.5 text-sm font-medium transition-colors" className="text-muted-foreground hover:text-primary flex items-center gap-1.5 text-sm font-medium transition-colors"
> >
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
<Trans i18nKey="marketing.changelog" /> <Trans i18nKey="marketing:changelog" />
</Link> </Link>
</div> </div>
</div> </div>

View File

@@ -24,8 +24,8 @@ function NavLink({ entry, direction }: NavLinkProps) {
const Icon = isPrevious ? ChevronLeft : ChevronRight; const Icon = isPrevious ? ChevronLeft : ChevronRight;
const i18nKey = isPrevious const i18nKey = isPrevious
? 'marketing.changelogNavigationPrevious' ? 'marketing:changelogNavigationPrevious'
: 'marketing.changelogNavigationNext'; : 'marketing:changelogNavigationNext';
return ( return (
<Link <Link

View File

@@ -22,29 +22,24 @@ export function ChangelogPagination({
return ( return (
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
{canGoToPreviousPage && ( {canGoToPreviousPage && (
<Button <Button asChild variant="outline" size="sm">
render={<Link href={`/changelog?page=${previousPage}`} />} <Link href={`/changelog?page=${previousPage}`}>
variant="outline"
size="sm"
>
<ArrowLeft className="mr-2 h-3 w-3" /> <ArrowLeft className="mr-2 h-3 w-3" />
<span> <span>
<Trans i18nKey="marketing.changelogPaginationPrevious" /> <Trans i18nKey="marketing:changelogPaginationPrevious" />
</span> </span>
</Link>
</Button> </Button>
)} )}
{canGoToNextPage && ( {canGoToNextPage && (
<Button <Button asChild variant="outline" size="sm">
render={<Link href={`/changelog?page=${nextPage}`} />} <Link href={`/changelog?page=${nextPage}`}>
variant="outline"
size="sm"
>
<span> <span>
<Trans i18nKey="marketing.changelogPaginationNext" /> <Trans i18nKey="marketing:changelogPaginationNext" />
</span> </span>
<ArrowRight className="ml-2 h-3 w-3" /> <ArrowRight className="ml-2 h-3 w-3" />
</Link>
</Button> </Button>
)} )}
</div> </div>

View File

@@ -2,13 +2,14 @@ import { cache } from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { getLocale, getTranslations } from 'next-intl/server';
import { createCmsClient } from '@kit/cms'; import { createCmsClient } from '@kit/cms';
import { getLogger } from '@kit/shared/logger'; import { getLogger } from '@kit/shared/logger';
import { If } from '@kit/ui/if'; import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { SitePageHeader } from '../_components/site-page-header'; import { SitePageHeader } from '../_components/site-page-header';
import { ChangelogEntry } from './_components/changelog-entry'; import { ChangelogEntry } from './_components/changelog-entry';
import { ChangelogPagination } from './_components/changelog-pagination'; import { ChangelogPagination } from './_components/changelog-pagination';
@@ -22,8 +23,7 @@ const CHANGELOG_ENTRIES_PER_PAGE = 50;
export const generateMetadata = async ( export const generateMetadata = async (
props: ChangelogPageProps, props: ChangelogPageProps,
): Promise<Metadata> => { ): Promise<Metadata> => {
const t = await getTranslations('marketing'); const { t, resolvedLanguage } = await createI18nServerInstance();
const resolvedLanguage = await getLocale();
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const limit = CHANGELOG_ENTRIES_PER_PAGE; const limit = CHANGELOG_ENTRIES_PER_PAGE;
@@ -33,8 +33,8 @@ export const generateMetadata = async (
const { total } = await getContentItems(resolvedLanguage, limit, offset); const { total } = await getContentItems(resolvedLanguage, limit, offset);
return { return {
title: t('changelog'), title: t('marketing:changelog'),
description: t('changelogSubtitle'), description: t('marketing:changelogSubtitle'),
pagination: { pagination: {
previous: page > 0 ? `/changelog?page=${page - 1}` : undefined, previous: page > 0 ? `/changelog?page=${page - 1}` : undefined,
next: offset + limit < total ? `/changelog?page=${page + 1}` : undefined, next: offset + limit < total ? `/changelog?page=${page + 1}` : undefined,
@@ -66,8 +66,7 @@ const getContentItems = cache(
); );
async function ChangelogPage(props: ChangelogPageProps) { async function ChangelogPage(props: ChangelogPageProps) {
const t = await getTranslations('marketing'); const { t, resolvedLanguage: language } = await createI18nServerInstance();
const language = await getLocale();
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const limit = CHANGELOG_ENTRIES_PER_PAGE; const limit = CHANGELOG_ENTRIES_PER_PAGE;
@@ -83,14 +82,14 @@ async function ChangelogPage(props: ChangelogPageProps) {
return ( return (
<> <>
<SitePageHeader <SitePageHeader
title={t('changelog')} title={t('marketing:changelog')}
subtitle={t('changelogSubtitle')} subtitle={t('marketing:changelogSubtitle')}
/> />
<div className="container flex max-w-4xl flex-col space-y-12 py-12"> <div className="container flex max-w-4xl flex-col space-y-12 py-12">
<If <If
condition={entries.length > 0} condition={entries.length > 0}
fallback={<Trans i18nKey="marketing.noChangelogEntries" />} fallback={<Trans i18nKey="marketing:noChangelogEntries" />}
> >
<div className="space-y-0"> <div className="space-y-0">
{entries.map((entry, index) => { {entries.map((entry, index) => {
@@ -115,4 +114,4 @@ async function ChangelogPage(props: ChangelogPageProps) {
); );
} }
export default ChangelogPage; export default withI18n(ChangelogPage);

View File

@@ -1,9 +1,8 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useTransition } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useAction } from 'next-safe-action/hooks';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
@@ -24,20 +23,13 @@ import { ContactEmailSchema } from '~/(marketing)/contact/_lib/contact-email.sch
import { sendContactEmail } from '~/(marketing)/contact/_lib/server/server-actions'; import { sendContactEmail } from '~/(marketing)/contact/_lib/server/server-actions';
export function ContactForm() { export function ContactForm() {
const [pending, startTransition] = useTransition();
const [state, setState] = useState({ const [state, setState] = useState({
success: false, success: false,
error: false, error: false,
}); });
const { execute, isPending } = useAction(sendContactEmail, {
onSuccess: () => {
setState({ success: true, error: false });
},
onError: () => {
setState({ error: true, success: false });
},
});
const form = useForm({ const form = useForm({
resolver: zodResolver(ContactEmailSchema), resolver: zodResolver(ContactEmailSchema),
defaultValues: { defaultValues: {
@@ -60,7 +52,15 @@ export function ContactForm() {
<form <form
className={'flex flex-col space-y-4'} className={'flex flex-col space-y-4'}
onSubmit={form.handleSubmit((data) => { onSubmit={form.handleSubmit((data) => {
execute(data); startTransition(async () => {
try {
await sendContactEmail(data);
setState({ success: true, error: false });
} catch {
setState({ error: true, success: false });
}
});
})} })}
> >
<FormField <FormField
@@ -69,7 +69,7 @@ export function ContactForm() {
return ( return (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
<Trans i18nKey={'marketing.contactName'} /> <Trans i18nKey={'marketing:contactName'} />
</FormLabel> </FormLabel>
<FormControl> <FormControl>
@@ -88,7 +88,7 @@ export function ContactForm() {
return ( return (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
<Trans i18nKey={'marketing.contactEmail'} /> <Trans i18nKey={'marketing:contactEmail'} />
</FormLabel> </FormLabel>
<FormControl> <FormControl>
@@ -107,7 +107,7 @@ export function ContactForm() {
return ( return (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
<Trans i18nKey={'marketing.contactMessage'} /> <Trans i18nKey={'marketing:contactMessage'} />
</FormLabel> </FormLabel>
<FormControl> <FormControl>
@@ -124,8 +124,8 @@ export function ContactForm() {
}} }}
/> />
<Button disabled={isPending} type={'submit'}> <Button disabled={pending} type={'submit'}>
<Trans i18nKey={'marketing.sendMessage'} /> <Trans i18nKey={'marketing:sendMessage'} />
</Button> </Button>
</form> </form>
</Form> </Form>
@@ -136,11 +136,11 @@ function SuccessAlert() {
return ( return (
<Alert variant={'success'}> <Alert variant={'success'}>
<AlertTitle> <AlertTitle>
<Trans i18nKey={'marketing.contactSuccess'} /> <Trans i18nKey={'marketing:contactSuccess'} />
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
<Trans i18nKey={'marketing.contactSuccessDescription'} /> <Trans i18nKey={'marketing:contactSuccessDescription'} />
</AlertDescription> </AlertDescription>
</Alert> </Alert>
); );
@@ -150,11 +150,11 @@ function ErrorAlert() {
return ( return (
<Alert variant={'destructive'}> <Alert variant={'destructive'}>
<AlertTitle> <AlertTitle>
<Trans i18nKey={'marketing.contactError'} /> <Trans i18nKey={'marketing:contactError'} />
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
<Trans i18nKey={'marketing.contactErrorDescription'} /> <Trans i18nKey={'marketing:contactErrorDescription'} />
</AlertDescription> </AlertDescription>
</Alert> </Alert>
); );

View File

@@ -1,4 +1,4 @@
import * as z from 'zod'; import { z } from 'zod';
export const ContactEmailSchema = z.object({ export const ContactEmailSchema = z.object({
name: z.string().min(1).max(200), name: z.string().min(1).max(200),

View File

@@ -1,29 +1,30 @@
'use server'; 'use server';
import * as z from 'zod'; import { z } from 'zod';
import { getMailer } from '@kit/mailers'; import { getMailer } from '@kit/mailers';
import { publicActionClient } from '@kit/next/safe-action'; import { enhanceAction } from '@kit/next/actions';
import { ContactEmailSchema } from '../contact-email.schema'; import { ContactEmailSchema } from '../contact-email.schema';
const contactEmail = z const contactEmail = z
.string({ .string({
error: description: `The email where you want to receive the contact form submissions.`,
required_error:
'Contact email is required. Please use the environment variable CONTACT_EMAIL.', 'Contact email is required. Please use the environment variable CONTACT_EMAIL.',
}) })
.parse(process.env.CONTACT_EMAIL); .parse(process.env.CONTACT_EMAIL);
const emailFrom = z const emailFrom = z
.string({ .string({
error: description: `The email sending address.`,
required_error:
'Sender email is required. Please use the environment variable EMAIL_SENDER.', 'Sender email is required. Please use the environment variable EMAIL_SENDER.',
}) })
.parse(process.env.EMAIL_SENDER); .parse(process.env.EMAIL_SENDER);
export const sendContactEmail = publicActionClient export const sendContactEmail = enhanceAction(
.schema(ContactEmailSchema) async (data) => {
.action(async ({ parsedInput: data }) => {
const mailer = await getMailer(); const mailer = await getMailer();
await mailer.sendEmail({ await mailer.sendEmail({
@@ -42,4 +43,9 @@ export const sendContactEmail = publicActionClient
}); });
return {}; return {};
}); },
{
schema: ContactEmailSchema,
auth: false,
},
);

View File

@@ -1,25 +1,28 @@
import { getTranslations } from 'next-intl/server';
import { Heading } from '@kit/ui/heading'; import { Heading } from '@kit/ui/heading';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { SitePageHeader } from '~/(marketing)/_components/site-page-header'; import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import { ContactForm } from '~/(marketing)/contact/_components/contact-form'; import { ContactForm } from '~/(marketing)/contact/_components/contact-form';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export async function generateMetadata() { export async function generateMetadata() {
const t = await getTranslations('marketing'); const { t } = await createI18nServerInstance();
return { return {
title: t('contact'), title: t('marketing:contact'),
}; };
} }
async function ContactPage() { async function ContactPage() {
const t = await getTranslations('marketing'); const { t } = await createI18nServerInstance();
return ( return (
<div> <div>
<SitePageHeader title={t(`contact`)} subtitle={t(`contactDescription`)} /> <SitePageHeader
title={t(`marketing:contact`)}
subtitle={t(`marketing:contactDescription`)}
/>
<div className={'container mx-auto'}> <div className={'container mx-auto'}>
<div <div
@@ -32,11 +35,11 @@ async function ContactPage() {
> >
<div> <div>
<Heading level={3}> <Heading level={3}>
<Trans i18nKey={'marketing.contactHeading'} /> <Trans i18nKey={'marketing:contactHeading'} />
</Heading> </Heading>
<p className={'text-muted-foreground'}> <p className={'text-muted-foreground'}>
<Trans i18nKey={'marketing.contactSubheading'} /> <Trans i18nKey={'marketing:contactSubheading'} />
</p> </p>
</div> </div>
@@ -48,4 +51,4 @@ async function ContactPage() {
); );
} }
export default ContactPage; export default withI18n(ContactPage);

View File

@@ -7,6 +7,8 @@ import { If } from '@kit/ui/if';
import { Separator } from '@kit/ui/separator'; import { Separator } from '@kit/ui/separator';
import { cn } from '@kit/ui/utils'; import { cn } from '@kit/ui/utils';
import { withI18n } from '~/lib/i18n/with-i18n';
// local imports // local imports
import { DocsCards } from '../_components/docs-cards'; import { DocsCards } from '../_components/docs-cards';
@@ -89,4 +91,4 @@ async function DocumentationPage({ params }: DocumentationPageProps) {
); );
} }
export default DocumentationPage; export default withI18n(DocumentationPage);

View File

@@ -3,7 +3,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { SidebarMenuButton, SidebarMenuItem } from '@kit/ui/sidebar'; import { SidebarMenuButton, SidebarMenuItem } from '@kit/ui/shadcn-sidebar';
import { cn, isRouteActive } from '@kit/ui/utils'; import { cn, isRouteActive } from '@kit/ui/utils';
export function DocsNavLink({ export function DocsNavLink({
@@ -12,18 +12,20 @@ export function DocsNavLink({
children, children,
}: React.PropsWithChildren<{ label: string; url: string }>) { }: React.PropsWithChildren<{ label: string; url: string }>) {
const currentPath = usePathname(); const currentPath = usePathname();
const isCurrent = isRouteActive(url, currentPath); const isCurrent = isRouteActive(url, currentPath, true);
return ( return (
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton <SidebarMenuButton
render={<Link href={url} />} asChild
isActive={isCurrent} isActive={isCurrent}
className={cn('text-secondary-foreground transition-all')} className={cn('text-secondary-foreground transition-all')}
> >
<Link href={url}>
<span className="block max-w-full truncate">{label}</span> <span className="block max-w-full truncate">{label}</span>
{children} {children}
</Link>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
); );

View File

@@ -16,7 +16,7 @@ export function DocsNavigationCollapsible(
const prefix = props.prefix; const prefix = props.prefix;
const isChildActive = props.node.children.some((child) => const isChildActive = props.node.children.some((child) =>
isRouteActive(prefix + '/' + child.url, currentPath), isRouteActive(prefix + '/' + child.url, currentPath, false),
); );
return ( return (

View File

@@ -10,12 +10,12 @@ import {
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
SidebarMenuSub, SidebarMenuSub,
} from '@kit/ui/sidebar'; } from '@kit/ui/shadcn-sidebar';
import { DocsNavLink } from '~/(marketing)/docs/_components/docs-nav-link'; import { DocsNavLink } from '~/(marketing)/docs/_components/docs-nav-link';
import { DocsNavigationCollapsible } from '~/(marketing)/docs/_components/docs-navigation-collapsible'; import { DocsNavigationCollapsible } from '~/(marketing)/docs/_components/docs-navigation-collapsible';
import { FloatingDocumentationNavigationButton } from './floating-docs-navigation-button'; import { FloatingDocumentationNavigation } from './floating-docs-navigation';
function Node({ function Node({
node, node,
@@ -85,11 +85,13 @@ function NodeTrigger({
}) { }) {
if (node.collapsible) { if (node.collapsible) {
return ( return (
<CollapsibleTrigger render={<SidebarMenuItem />}> <CollapsibleTrigger asChild>
<SidebarMenuItem>
<SidebarMenuButton> <SidebarMenuButton>
{label} {label}
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" /> <ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem>
</CollapsibleTrigger> </CollapsibleTrigger>
); );
} }
@@ -135,10 +137,12 @@ export function DocsNavigation({
return ( return (
<> <>
<Sidebar <Sidebar
variant={'sidebar'} variant={'ghost'}
className={'sticky z-1 mt-4 max-h-full overflow-y-auto pr-4'} className={
'border-border/50 sticky z-1 mt-4 max-h-full overflow-y-auto pr-4'
}
> >
<SidebarGroup> <SidebarGroup className="p-0">
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu className={'pb-48'}> <SidebarMenu className={'pb-48'}>
<Tree pages={pages} level={0} prefix={prefix} /> <Tree pages={pages} level={0} prefix={prefix} />
@@ -147,7 +151,17 @@ export function DocsNavigation({
</SidebarGroup> </SidebarGroup>
</Sidebar> </Sidebar>
<FloatingDocumentationNavigationButton /> <div className={'lg:hidden'}>
<FloatingDocumentationNavigation>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<Tree pages={pages} level={0} prefix={prefix} />
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</FloatingDocumentationNavigation>
</div>
</> </>
); );
} }

View File

@@ -0,0 +1,72 @@
'use client';
import { useEffect, useEffectEvent, useMemo, useState } from 'react';
import { usePathname } from 'next/navigation';
import { Menu } from 'lucide-react';
import { isBrowser } from '@kit/shared/utils';
import { Button } from '@kit/ui/button';
import { If } from '@kit/ui/if';
export function FloatingDocumentationNavigation(
props: React.PropsWithChildren,
) {
const activePath = usePathname();
const body = useMemo(() => {
return isBrowser() ? document.body : null;
}, []);
const [isVisible, setIsVisible] = useState(false);
const enableScrolling = useEffectEvent(
() => body && (body.style.overflowY = ''),
);
const disableScrolling = useEffectEvent(
() => body && (body.style.overflowY = 'hidden'),
);
// enable/disable body scrolling when the docs are toggled
useEffect(() => {
if (isVisible) {
disableScrolling();
} else {
enableScrolling();
}
}, [isVisible]);
// hide docs when navigating to another page
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsVisible(false);
}, [activePath]);
const onClick = () => {
setIsVisible(!isVisible);
};
return (
<>
<If condition={isVisible}>
<div
className={
'fixed top-0 left-0 z-10 h-screen w-full p-4' +
' dark:bg-background flex flex-col space-y-4 overflow-auto bg-white'
}
>
{props.children}
</div>
</If>
<Button
className={'fixed right-5 bottom-5 z-10 h-16 w-16 rounded-full'}
onClick={onClick}
>
<Menu className={'h-8'} />
</Button>
</>
);
}

View File

@@ -1,6 +1,6 @@
import { getLocale } from 'next-intl/server'; import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
import { SidebarProvider } from '@kit/ui/sidebar'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
// local imports // local imports
import { DocsNavigation } from './_components/docs-navigation'; import { DocsNavigation } from './_components/docs-navigation';
@@ -8,8 +8,8 @@ import { getDocs } from './_lib/server/docs.loader';
import { buildDocumentationTree } from './_lib/utils'; import { buildDocumentationTree } from './_lib/utils';
async function DocsLayout({ children }: React.PropsWithChildren) { async function DocsLayout({ children }: React.PropsWithChildren) {
const locale = await getLocale(); const { resolvedLanguage } = await createI18nServerInstance();
const docs = await getDocs(locale); const docs = await getDocs(resolvedLanguage);
const tree = buildDocumentationTree(docs); const tree = buildDocumentationTree(docs);
return ( return (

View File

@@ -1,21 +1,21 @@
import { getLocale, getTranslations } from 'next-intl/server'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { SitePageHeader } from '../_components/site-page-header'; import { SitePageHeader } from '../_components/site-page-header';
import { DocsCards } from './_components/docs-cards'; import { DocsCards } from './_components/docs-cards';
import { getDocs } from './_lib/server/docs.loader'; import { getDocs } from './_lib/server/docs.loader';
export const generateMetadata = async () => { export const generateMetadata = async () => {
const t = await getTranslations('marketing'); const { t } = await createI18nServerInstance();
return { return {
title: t('documentation'), title: t('marketing:documentation'),
}; };
}; };
async function DocsPage() { async function DocsPage() {
const t = await getTranslations('marketing'); const { t, resolvedLanguage } = await createI18nServerInstance();
const locale = await getLocale(); const items = await getDocs(resolvedLanguage);
const items = await getDocs(locale);
// Filter out any docs that have a parentId, as these are children of other docs // Filter out any docs that have a parentId, as these are children of other docs
const cards = items.filter((item) => !item.parentId); const cards = items.filter((item) => !item.parentId);
@@ -23,8 +23,8 @@ async function DocsPage() {
return ( return (
<div className={'flex w-full flex-1 flex-col gap-y-6 xl:gap-y-8'}> <div className={'flex w-full flex-1 flex-col gap-y-6 xl:gap-y-8'}>
<SitePageHeader <SitePageHeader
title={t('documentation')} title={t('marketing:documentation')}
subtitle={t('documentationSubtitle')} subtitle={t('marketing:documentationSubtitle')}
/> />
<div className={'relative flex size-full justify-center overflow-y-auto'}> <div className={'relative flex size-full justify-center overflow-y-auto'}>
@@ -34,4 +34,4 @@ async function DocsPage() {
); );
} }
export default DocsPage; export default withI18n(DocsPage);

View File

@@ -1,30 +1,31 @@
import Link from 'next/link'; import Link from 'next/link';
import { ArrowRight, ChevronDown } from 'lucide-react'; import { ArrowRight, ChevronDown } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { SitePageHeader } from '~/(marketing)/_components/site-page-header'; import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export const generateMetadata = async () => { export const generateMetadata = async () => {
const t = await getTranslations('marketing'); const { t } = await createI18nServerInstance();
return { return {
title: t('faq'), title: t('marketing:faq'),
}; };
}; };
async function FAQPage() { async function FAQPage() {
const t = await getTranslations('marketing'); const { t } = await createI18nServerInstance();
// replace this content with translations // replace this content with translations
const faqItems = [ const faqItems = [
{ {
// or: t('faq.question1') // or: t('marketing:faq.question1')
question: `Do you offer a free trial?`, question: `Do you offer a free trial?`,
// or: t('faq.answer1') // or: t('marketing:faq.answer1')
answer: `Yes, we offer a 14-day free trial. You can cancel at any time during the trial period and you won't be charged.`, answer: `Yes, we offer a 14-day free trial. You can cancel at any time during the trial period and you won't be charged.`,
}, },
{ {
@@ -73,7 +74,10 @@ async function FAQPage() {
/> />
<div className={'flex flex-col space-y-4 xl:space-y-8'}> <div className={'flex flex-col space-y-4 xl:space-y-8'}>
<SitePageHeader title={t('faq')} subtitle={t('faqSubtitle')} /> <SitePageHeader
title={t('marketing:faq')}
subtitle={t('marketing:faqSubtitle')}
/>
<div className={'container flex flex-col items-center space-y-8 pb-16'}> <div className={'container flex flex-col items-center space-y-8 pb-16'}>
<div className="divide-border flex w-full max-w-xl flex-col divide-y divide-dashed rounded-md border"> <div className="divide-border flex w-full max-w-xl flex-col divide-y divide-dashed rounded-md border">
@@ -83,16 +87,14 @@ async function FAQPage() {
</div> </div>
<div> <div>
<Button <Button asChild variant={'outline'}>
nativeButton={false} <Link href={'/contact'}>
render={<Link href={'/contact'} />}
variant={'link'}
>
<span> <span>
<Trans i18nKey={'marketing.contactFaq'} /> <Trans i18nKey={'marketing:contactFaq'} />
</span> </span>
<ArrowRight className={'ml-2 w-4'} /> <ArrowRight className={'ml-2 w-4'} />
</Link>
</Button> </Button>
</div> </div>
</div> </div>
@@ -101,7 +103,7 @@ async function FAQPage() {
); );
} }
export default FAQPage; export default withI18n(FAQPage);
function FaqItem({ function FaqItem({
item, item,

View File

@@ -3,6 +3,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { SiteFooter } from '~/(marketing)/_components/site-footer'; import { SiteFooter } from '~/(marketing)/_components/site-footer';
import { SiteHeader } from '~/(marketing)/_components/site-header'; import { SiteHeader } from '~/(marketing)/_components/site-header';
import { withI18n } from '~/lib/i18n/with-i18n';
async function SiteLayout(props: React.PropsWithChildren) { async function SiteLayout(props: React.PropsWithChildren) {
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
@@ -19,4 +20,4 @@ async function SiteLayout(props: React.PropsWithChildren) {
); );
} }
export default SiteLayout; export default withI18n(SiteLayout);

View File

@@ -20,6 +20,7 @@ import { Trans } from '@kit/ui/trans';
import billingConfig from '~/config/billing.config'; import billingConfig from '~/config/billing.config';
import pathsConfig from '~/config/paths.config'; import pathsConfig from '~/config/paths.config';
import { withI18n } from '~/lib/i18n/with-i18n';
function Home() { function Home() {
return ( return (
@@ -29,13 +30,11 @@ function Home() {
pill={ pill={
<Pill label={'New'}> <Pill label={'New'}>
<span>The SaaS Starter Kit for ambitious developers</span> <span>The SaaS Starter Kit for ambitious developers</span>
<PillActionButton <PillActionButton asChild>
render={
<Link href={'/auth/sign-up'}> <Link href={'/auth/sign-up'}>
<ArrowRightIcon className={'h-4 w-4'} /> <ArrowRightIcon className={'h-4 w-4'} />
</Link> </Link>
} </PillActionButton>
/>
</Pill> </Pill>
} }
title={ title={
@@ -171,7 +170,7 @@ function Home() {
); );
} }
export default Home; export default withI18n(Home);
function MainCallToActionButton() { function MainCallToActionButton() {
return ( return (
@@ -180,7 +179,7 @@ function MainCallToActionButton() {
<Link href={'/auth/sign-up'}> <Link href={'/auth/sign-up'}>
<span className={'flex items-center space-x-0.5'}> <span className={'flex items-center space-x-0.5'}>
<span> <span>
<Trans i18nKey={'common.getStarted'} /> <Trans i18nKey={'common:getStarted'} />
</span> </span>
<ArrowRightIcon <ArrowRightIcon
@@ -195,7 +194,7 @@ function MainCallToActionButton() {
<CtaButton variant={'link'} className="h-10 text-sm"> <CtaButton variant={'link'} className="h-10 text-sm">
<Link href={'/pricing'}> <Link href={'/pricing'}>
<Trans i18nKey={'common.pricing'} /> <Trans i18nKey={'common:pricing'} />
</Link> </Link>
</CtaButton> </CtaButton>
</div> </div>

View File

@@ -1,16 +1,16 @@
import { getTranslations } from 'next-intl/server';
import { PricingTable } from '@kit/billing-gateway/marketing'; import { PricingTable } from '@kit/billing-gateway/marketing';
import { SitePageHeader } from '~/(marketing)/_components/site-page-header'; import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
import billingConfig from '~/config/billing.config'; import billingConfig from '~/config/billing.config';
import pathsConfig from '~/config/paths.config'; import pathsConfig from '~/config/paths.config';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export const generateMetadata = async () => { export const generateMetadata = async () => {
const t = await getTranslations('marketing'); const { t } = await createI18nServerInstance();
return { return {
title: t('pricing'), title: t('marketing:pricing'),
}; };
}; };
@@ -20,11 +20,14 @@ const paths = {
}; };
async function PricingPage() { async function PricingPage() {
const t = await getTranslations('marketing'); const { t } = await createI18nServerInstance();
return ( return (
<div className={'flex flex-col space-y-8'}> <div className={'flex flex-col space-y-8'}>
<SitePageHeader title={t('pricing')} subtitle={t('pricingSubtitle')} /> <SitePageHeader
title={t('marketing:pricing')}
subtitle={t('marketing:pricingSubtitle')}
/>
<div className={'container mx-auto pb-8 xl:pb-16'}> <div className={'container mx-auto pb-8 xl:pb-16'}>
<PricingTable paths={paths} config={billingConfig} /> <PricingTable paths={paths} config={billingConfig} />
@@ -33,4 +36,4 @@ async function PricingPage() {
); );
} }
export default PricingPage; export default withI18n(PricingPage);

View File

@@ -1,30 +0,0 @@
import { getTranslations } from 'next-intl/server';
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
export async function generateMetadata() {
const t = await getTranslations('marketing');
return {
title: t('cookiePolicy'),
};
}
async function CookiePolicyPage() {
const t = await getTranslations('marketing');
return (
<div>
<SitePageHeader
title={t(`marketing.cookiePolicy`)}
subtitle={t(`marketing.cookiePolicyDescription`)}
/>
<div className={'container mx-auto py-8'}>
<div>Your terms of service content here</div>
</div>
</div>
);
}
export default CookiePolicyPage;

Some files were not shown because too many files have changed in this diff Show More