Unify workspace dropdowns; Update layouts (#458)

Unified Account and Workspace drop-downs; Layout updates, now header lives within the PageBody component; Sidebars now use floating variant
This commit is contained in:
Giancarlo Buomprisco
2026-03-11 14:45:42 +08:00
committed by GitHub
parent ca585e09be
commit 4bc8448a1d
530 changed files with 14398 additions and 11198 deletions

View File

@@ -55,7 +55,10 @@ create policy "projects_write" on public.projects for all
Use `server-action-builder` skill for detailed patterns.
**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.
**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.
Create in route's `_lib/server/` directory:

View File

@@ -19,8 +19,8 @@ export class AuthPageObject {
}
async signOut() {
await this.page.click('[data-test="account-dropdown-trigger"]');
await this.page.click('[data-test="account-dropdown-sign-out"]');
await this.page.click('[data-test="workspace-dropdown-trigger"]');
await this.page.click('[data-test="workspace-sign-out"]');
}
async bootstrapUser(params: { email: string; password: string; name: string }) {
@@ -47,9 +47,19 @@ export class AuthPageObject {
## Common Selectors
```typescript
// Account dropdown
'[data-test="account-dropdown-trigger"]'
'[data-test="account-dropdown-sign-out"]'
// Workspace dropdown (sidebar header - combined account switcher + user menu)
'[data-test="workspace-dropdown-trigger"]' // Opens the dropdown
'[data-test="workspace-switch-submenu"]' // Sub-trigger for workspace switching
'[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
'[data-test="sidebar-menu"]'

View File

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

View File

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

View File

@@ -17,19 +17,21 @@ Create validation schema in `_lib/schemas/`:
```typescript
// _lib/schemas/feature.schema.ts
import { z } from 'zod';
import * as z from 'zod';
export const CreateFeatureSchema = z.object({
name: z.string().min(1, 'Name is required'),
accountId: z.string().uuid('Invalid account ID'),
});
export type CreateFeatureInput = z.infer<typeof CreateFeatureSchema>;
export type CreateFeatureInput = z.output<typeof CreateFeatureSchema>;
```
### Step 2: Create Service Layer
**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.
**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.
Create service in `_lib/server/`:
@@ -62,11 +64,13 @@ class FeatureService {
}
```
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).
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).
### 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 logic lives here.
The action is a **thin adapter** — it resolves dependencies (client, logger) and delegates to the service. No business
logic lives here.
Create action in `_lib/server/server-actions.ts`:
@@ -107,13 +111,18 @@ export const createFeatureAction = enhanceAction(
## Key Patterns
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.
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.
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.
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
4. **Logging** - Always log before and after operations
5. **Revalidation** - Use `revalidatePath` after mutations
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 running infrastructure
7. **Testable in isolation** - Because services accept their dependencies, you can test them with a mock client and no
running infrastructure
## File Structure

View File

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

View File

@@ -9,7 +9,9 @@ You are an expert at building pure, testable services that are decoupled from th
## North Star
**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.
**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.
## Workflow
@@ -21,7 +23,7 @@ Start with the input/output types. These are plain TypeScript — no framework t
```typescript
// _lib/schemas/project.schema.ts
import { z } from 'zod';
import * as z from 'zod';
export const CreateProjectSchema = z.object({
name: z.string().min(1),
@@ -40,7 +42,8 @@ export interface Project {
### Step 2: Build the Service
The service receives all dependencies through its constructor. It never imports framework-specific modules (`getSupabaseServerClient`, `getLogger`, `revalidatePath`, etc.).
The service receives all dependencies through its constructor. It never imports framework-specific modules (
`getSupabaseServerClient`, `getLogger`, `revalidatePath`, etc.).
```typescript
// _lib/server/project.service.ts
@@ -95,7 +98,8 @@ class ProjectService {
### Step 3: Write Thin Adapters
Each interface is a thin adapter — it resolves dependencies, calls the service, and handles interface-specific concerns (revalidation, redirects, MCP formatting, CLI output).
Each interface is a thin adapter — it resolves dependencies, calls the service, and handles interface-specific
concerns (revalidation, redirects, MCP formatting, CLI output).
**Server Action adapter:**
@@ -234,27 +238,32 @@ describe('ProjectService', () => {
## Rules
1. **Services are pure functions over data.** Plain objects/primitives in, plain objects/primitives out. No `Request`/`Response`, no MCP context, no `FormData`.
1. **Services are pure functions over data.** Plain objects/primitives in, plain objects/primitives out. No `Request`/
`Response`, no MCP context, no `FormData`.
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.
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.
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.
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.
4. **One service, many callers.** If two interfaces do the same thing, they call the same service function. Duplicating logic is a violation.
4. **One service, many callers.** If two interfaces do the same thing, they call the same service function. Duplicating
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, refactor until you don't.
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.
## What Goes Where
| Concern | Location | Example |
|---------|----------|---------|
| Input validation (Zod) | `_lib/schemas/` | `CreateProjectSchema` |
| Business logic | `_lib/server/*.service.ts` | `ProjectService.create()` |
| Auth check | Adapter (`enhanceAction({ auth: true })`) | Server action wrapper |
| Logging | Adapter | `logger.info()` before/after service call |
| Cache revalidation | Adapter | `revalidatePath()` after mutation |
| Redirect | Adapter | `redirect()` after creation |
| MCP response format | Adapter | Return service result as MCP content |
| Concern | Location | Example |
|------------------------|-------------------------------------------|-------------------------------------------|
| Input validation (Zod) | `_lib/schemas/` | `CreateProjectSchema` |
| Business logic | `_lib/server/*.service.ts` | `ProjectService.create()` |
| Auth check | Adapter (`enhanceAction({ auth: true })`) | Server action wrapper |
| Logging | Adapter | `logger.info()` before/after service call |
| Cache revalidation | Adapter | `revalidatePath()` after mutation |
| Redirect | Adapter | `redirect()` after creation |
| MCP response format | Adapter | Return service result as MCP content |
## File Structure
@@ -305,4 +314,5 @@ const result = await client.from('projects').insert(...).select().single();
## Reference
See `[Examples](examples.md)` for more patterns including services with multiple dependencies, services that compose other services, and testing strategies.
See `[Examples](examples.md)` for more patterns including services with multiple dependencies, services that compose
other services, and testing strategies.

View File

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

View File

@@ -556,8 +556,8 @@ function MyFeaturePage() {
return (
<>
<MyFeatureHeader
title={<Trans i18nKey={'common:routes.myFeature'} />}
description={<Trans i18nKey={'common:myFeatureDescription'} />}
title={<Trans i18nKey={'common.routes.myFeature'} />}
description={<Trans i18nKey={'common.myFeatureDescription'} />}
/>
<PageBody>
@@ -829,40 +829,40 @@ import { ProfileAvatar } from '@kit/ui/profile-avatar';
## Core Shadcn UI Components
| Component | Description | Import Path |
|-----------|-------------|-------------|
| `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) |
| `Alert` | Status/notification messages | `@kit/ui/alert` [alert.tsx](mdc:packages/ui/src/shadcn/alert.tsx) |
| `Avatar` | User profile images with fallback | `@kit/ui/avatar` [avatar.tsx](mdc:packages/ui/src/shadcn/avatar.tsx) |
| `Badge` | Small status indicators | `@kit/ui/badge` [badge.tsx](mdc:packages/ui/src/shadcn/badge.tsx) |
| `Breadcrumb` | Navigation path indicators | `@kit/ui/breadcrumb` [breadcrumb.tsx](mdc:packages/ui/src/shadcn/breadcrumb.tsx) |
| `Button` | Clickable action elements | `@kit/ui/button` [button.tsx](mdc:packages/ui/src/shadcn/button.tsx) |
| `Calendar` | Date picker and date display | `@kit/ui/calendar` [calendar.tsx](mdc:packages/ui/src/shadcn/calendar.tsx) |
| `Card` | Container for grouped content | `@kit/ui/card` [card.tsx](mdc:packages/ui/src/shadcn/card.tsx) |
| `Checkbox` | Selection input | `@kit/ui/checkbox` [checkbox.tsx](mdc:packages/ui/src/shadcn/checkbox.tsx) |
| `Command` | Command palette interface | `@kit/ui/command` [command.tsx](mdc:packages/ui/src/shadcn/command.tsx) |
| `DataTable` | Table | `@kit/ui/data-table` [data-table.tsx](mdc:packages/ui/src/shadcn/data-table.tsx) |
| `Dialog` | Modal window for focused interactions | `@kit/ui/dialog` [dialog.tsx](mdc:packages/ui/src/shadcn/dialog.tsx) |
| `DropdownMenu` | Menu triggered by a button | `@kit/ui/dropdown-menu` [dropdown-menu.tsx](mdc:packages/ui/src/shadcn/dropdown-menu.tsx) |
| `Form` | Form components with validation | `@kit/ui/form` [form.tsx](mdc:packages/ui/src/shadcn/form.tsx) |
| `Input` | Text input field | `@kit/ui/input` [input.tsx](mdc:packages/ui/src/shadcn/input.tsx) |
| `Input OTP` | OTP Text input field | `@kit/ui/input-otp` [input-otp.tsx](mdc:packages/ui/src/shadcn/input-otp.tsx) |
| `Label` | Text label for form elements | `@kit/ui/label` [label.tsx](mdc:packages/ui/src/shadcn/label.tsx) |
| `NavigationMenu` | Hierarchical navigation component | `@kit/ui/navigation-menu` [navigation-menu.tsx](mdc:packages/ui/src/shadcn/navigation-menu.tsx) |
| `Popover` | Floating content triggered by interaction | `@kit/ui/popover` [popover.tsx](mdc:packages/ui/src/shadcn/popover.tsx) |
| `RadioGroup` | Radio button selection group | `@kit/ui/radio-group` [radio-group.tsx](mdc:packages/ui/src/shadcn/radio-group.tsx) |
| `ScrollArea` | Customizable scrollable area | `@kit/ui/scroll-area` [scroll-area.tsx](mdc:packages/ui/src/shadcn/scroll-area.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) |
| `Sheet` | Sliding panel from screen edge | `@kit/ui/sheet` [sheet.tsx](mdc:packages/ui/src/shadcn/sheet.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) |
| `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) |
| `Tabs` | Tab-based navigation | `@kit/ui/tabs` [tabs.tsx](mdc:packages/ui/src/shadcn/tabs.tsx) |
| `Textarea` | Multi-line text input | `@kit/ui/textarea` [textarea.tsx](mdc:packages/ui/src/shadcn/textarea.tsx) |
| `Tooltip` | Contextual information on hover | `@kit/ui/tooltip` [tooltip.tsx](mdc:packages/ui/src/shadcn/tooltip.tsx) |
| Component | Description | Import Path |
|------------------|-------------------------------------------|-------------------------------------------------------------------------------------------------|
| `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) |
| `Alert` | Status/notification messages | `@kit/ui/alert` [alert.tsx](mdc:packages/ui/src/shadcn/alert.tsx) |
| `Avatar` | User profile images with fallback | `@kit/ui/avatar` [avatar.tsx](mdc:packages/ui/src/shadcn/avatar.tsx) |
| `Badge` | Small status indicators | `@kit/ui/badge` [badge.tsx](mdc:packages/ui/src/shadcn/badge.tsx) |
| `Breadcrumb` | Navigation path indicators | `@kit/ui/breadcrumb` [breadcrumb.tsx](mdc:packages/ui/src/shadcn/breadcrumb.tsx) |
| `Button` | Clickable action elements | `@kit/ui/button` [button.tsx](mdc:packages/ui/src/shadcn/button.tsx) |
| `Calendar` | Date picker and date display | `@kit/ui/calendar` [calendar.tsx](mdc:packages/ui/src/shadcn/calendar.tsx) |
| `Card` | Container for grouped content | `@kit/ui/card` [card.tsx](mdc:packages/ui/src/shadcn/card.tsx) |
| `Checkbox` | Selection input | `@kit/ui/checkbox` [checkbox.tsx](mdc:packages/ui/src/shadcn/checkbox.tsx) |
| `Command` | Command palette interface | `@kit/ui/command` [command.tsx](mdc:packages/ui/src/shadcn/command.tsx) |
| `DataTable` | Table | `@kit/ui/data-table` [data-table.tsx](mdc:packages/ui/src/shadcn/data-table.tsx) |
| `Dialog` | Modal window for focused interactions | `@kit/ui/dialog` [dialog.tsx](mdc:packages/ui/src/shadcn/dialog.tsx) |
| `DropdownMenu` | Menu triggered by a button | `@kit/ui/dropdown-menu` [dropdown-menu.tsx](mdc:packages/ui/src/shadcn/dropdown-menu.tsx) |
| `Form` | Form components with validation | `@kit/ui/form` [form.tsx](mdc:packages/ui/src/shadcn/form.tsx) |
| `Input` | Text input field | `@kit/ui/input` [input.tsx](mdc:packages/ui/src/shadcn/input.tsx) |
| `Input OTP` | OTP Text input field | `@kit/ui/input-otp` [input-otp.tsx](mdc:packages/ui/src/shadcn/input-otp.tsx) |
| `Label` | Text label for form elements | `@kit/ui/label` [label.tsx](mdc:packages/ui/src/shadcn/label.tsx) |
| `NavigationMenu` | Hierarchical navigation component | `@kit/ui/navigation-menu` [navigation-menu.tsx](mdc:packages/ui/src/shadcn/navigation-menu.tsx) |
| `Popover` | Floating content triggered by interaction | `@kit/ui/popover` [popover.tsx](mdc:packages/ui/src/shadcn/popover.tsx) |
| `RadioGroup` | Radio button selection group | `@kit/ui/radio-group` [radio-group.tsx](mdc:packages/ui/src/shadcn/radio-group.tsx) |
| `ScrollArea` | Customizable scrollable area | `@kit/ui/scroll-area` [scroll-area.tsx](mdc:packages/ui/src/shadcn/scroll-area.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) |
| `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) |
| `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) |
| `Toast` | Toaster | `@kit/ui/sonner` [sonner.tsx](mdc:packages/ui/src/shadcn/sonner.tsx) |
| `Tabs` | Tab-based navigation | `@kit/ui/tabs` [tabs.tsx](mdc:packages/ui/src/shadcn/tabs.tsx) |
| `Textarea` | Multi-line text input | `@kit/ui/textarea` [textarea.tsx](mdc:packages/ui/src/shadcn/textarea.tsx) |
| `Tooltip` | Contextual information on hover | `@kit/ui/tooltip` [tooltip.tsx](mdc:packages/ui/src/shadcn/tooltip.tsx) |
## Makerkit-specific Components
@@ -920,7 +920,7 @@ Zod schemas should be defined in the `schema` folder and exported, so we can reu
```tsx
// _lib/schema/create-note.schema.ts
import { z } from 'zod';
import * as z from 'zod';
export const CreateNoteSchema = z.object({
title: z.string().min(1),
@@ -935,7 +935,7 @@ Server Actions [server-actions.mdc](mdc:.cursor/rules/server-actions.mdc) can he
```tsx
'use server';
import { z } from 'zod';
import * as z from 'zod';
import { enhanceAction } from '@kit/next/actions';
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 { useForm } from 'react-hook-form';
import { z } from 'zod';
import * as z from 'zod';
import { Textarea } from '@kit/ui/textarea';
import { Input } from '@kit/ui/input';
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
'use server';
import { z } from 'zod';
import * as z from 'zod';
import { enhanceAction } from '@kit/next/actions';
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)
```tsx
import { z } from 'zod';
import * as z from 'zod';
import { enhanceRouteHandler } from '@kit/next/routes';
import { NextResponse } from 'next/server';

1
.npmrc
View File

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

View File

@@ -39,13 +39,13 @@ pnpm format:fix # Format code
## Key Patterns (Quick Reference)
| Pattern | Import | Details |
|---------|--------|---------|
| Server Actions | `enhanceAction` from `@kit/next/actions` | `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` |
| UI Components | `@kit/ui/{component}` | `packages/ui/AGENTS.md` |
| Translations | `Trans` from `@kit/ui/trans` | `packages/ui/AGENTS.md` |
| Pattern | Import | Details |
|----------------|--------------------------------------------------------------|-------------------------------|
| Server Actions | `authActionClient` from `@kit/next/safe-action` | `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` |
| UI Components | `@kit/ui/{component}` | `packages/ui/AGENTS.md` |
| Translations | `Trans` from `@kit/ui/trans` | `packages/ui/AGENTS.md` |
## Authorization

View File

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

View File

@@ -33,7 +33,6 @@ interface ButtonControls {
loading: boolean;
withIcon: boolean;
fullWidth: boolean;
asChild: boolean;
}
const variantOptions = [
@@ -68,7 +67,6 @@ export function ButtonStory() {
loading: false,
withIcon: false,
fullWidth: false,
asChild: false,
});
const generateCode = () => {
@@ -77,14 +75,12 @@ export function ButtonStory() {
variant: controls.variant,
size: controls.size,
disabled: controls.disabled,
asChild: controls.asChild,
className: controls.fullWidth ? 'w-full' : '',
},
{
variant: 'default',
size: 'default',
disabled: false,
asChild: false,
className: '',
},
);
@@ -194,15 +190,6 @@ export function ButtonStory() {
onCheckedChange={(checked) => updateControl('fullWidth', checked)}
/>
</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>
<CardContent className="flex justify-center pt-6">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="justify-start">
<CalendarIcon className="mr-2 h-4 w-4" />
Pick a date
</Button>
<PopoverTrigger
render={<Button variant="outline" className="justify-start" />}
>
<CalendarIcon className="mr-2 h-4 w-4" />
Pick a date
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import * as z from 'zod';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
@@ -119,7 +119,7 @@ export default function FormStory() {
const formImport = generateImportStatement(formComponents, '@kit/ui/form');
const inputImport = generateImportStatement(['Input'], '@kit/ui/input');
const buttonImport = generateImportStatement(['Button'], '@kit/ui/button');
const hookFormImports = `import { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { z } from 'zod';`;
const hookFormImports = `import { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport * as z from 'zod';`;
let schemaCode = '';
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 />`;
onSubmitCode = ` function onSubmit(values: z.infer<typeof formSchema>) {\n console.log('Form submitted:', values);\n }`;
onSubmitCode = ` function onSubmit(values: z.output<typeof formSchema>) {\n console.log('Form submitted:', values);\n }`;
} 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});`;
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.infer<typeof formSchema>) {\n console.log('Advanced form submitted:', values);\n }`;
onSubmitCode = ` function onSubmit(values: z.output<typeof formSchema>) {\n console.log('Advanced form submitted:', values);\n }`;
} 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});`;
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.infer<typeof formSchema>) {\n console.log('Validation form submitted:', values);\n }`;
onSubmitCode = ` function onSubmit(values: z.output<typeof formSchema>) {\n console.log('Validation form submitted:', values);\n }`;
}
const defaultValuesCode =
@@ -152,13 +152,13 @@ export default function FormStory() {
? ` defaultValues: {\n firstName: '',\n lastName: '',\n email: '',\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.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}`;
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}`;
return fullFormCode;
};
// Basic form
const basicForm = useForm<z.infer<typeof basicFormSchema>>({
const basicForm = useForm<z.output<typeof basicFormSchema>>({
resolver: zodResolver(basicFormSchema),
defaultValues: {
username: '',
@@ -169,7 +169,7 @@ export default function FormStory() {
});
// Advanced form
const advancedForm = useForm<z.infer<typeof advancedFormSchema>>({
const advancedForm = useForm<z.output<typeof advancedFormSchema>>({
resolver: zodResolver(advancedFormSchema),
defaultValues: {
firstName: '',
@@ -183,7 +183,7 @@ export default function FormStory() {
});
// Validation form
const validationForm = useForm<z.infer<typeof validationFormSchema>>({
const validationForm = useForm<z.output<typeof validationFormSchema>>({
resolver: zodResolver(validationFormSchema),
defaultValues: {
password: '',
@@ -1056,7 +1056,7 @@ export default function FormStory() {
<pre className="overflow-x-auto text-sm">
{`import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import * as z from 'zod';
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@kit/ui/form';
const formSchema = z.object({
@@ -1065,7 +1065,7 @@ const formSchema = z.object({
});
function MyForm() {
const form = useForm<z.infer<typeof formSchema>>({
const form = useForm<z.output<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: '',
@@ -1073,7 +1073,7 @@ function MyForm() {
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
function onSubmit(values: z.output<typeof formSchema>) {
console.log(values);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import { withI18n } from '../../lib/i18n/with-i18n';
import { DocsContent } from './components/docs-content';
import { DocsHeader } from './components/docs-header';
import { DocsSidebar } from './components/docs-sidebar';
@@ -29,4 +28,4 @@ async function ComponentDocsPage(props: ComponentDocsPageProps) {
);
}
export default withI18n(ComponentDocsPage);
export default 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
it in your inbox.{' '}
<Dialog>
<DialogTrigger asChild>
<Button variant={'link'} className="p-0 underline">
Test Email
</Button>
<DialogTrigger
render={<Button variant={'link'} className="p-0 underline" />}
>
Test Email
</DialogTrigger>
<DialogContent>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -731,13 +731,15 @@ function FilterSwitcher(props: {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="font-normal">
{buttonLabel()}
<DropdownMenuTrigger
render={
<Button variant="outline" className="font-normal">
{buttonLabel()}
<ChevronsUpDownIcon className="text-muted-foreground ml-1 h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<ChevronsUpDownIcon className="text-muted-foreground ml-1 h-3 w-3" />
</Button>
}
/>
<DropdownMenuContent>
<DropdownMenuCheckboxItem
@@ -886,38 +888,41 @@ function Summary({ appState }: { appState: AppEnvState }) {
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size={'sm'}
onClick={() => {
let data = '';
<TooltipTrigger
render={
<Button
variant="outline"
size={'sm'}
onClick={() => {
let data = '';
const groups = getGroups(appState, () => true);
const groups = getGroups(appState, () => true);
groups.forEach((group) => {
data += `# ${group.category}\n`;
groups.forEach((group) => {
data += `# ${group.category}\n`;
group.variables.forEach((variable) => {
data += `${variable.key}=${variable.effectiveValue}\n`;
group.variables.forEach((variable) => {
data += `${variable.key}=${variable.effectiveValue}\n`;
});
data += '\n';
});
data += '\n';
});
const promise = copyToClipboard(data);
const promise = copyToClipboard(data);
toast.promise(promise, {
loading: 'Copying environment variables...',
success: 'Environment variables copied to clipboard.',
error: 'Failed to copy environment variables to clipboard',
});
}}
>
<CopyIcon className={'mr-2 h-4 w-4'} />
<span>Copy env file to clipboard</span>
</Button>
</TooltipTrigger>
toast.promise(promise, {
loading: 'Copying environment variables...',
success: 'Environment variables copied to clipboard.',
error:
'Failed to copy environment variables to clipboard',
});
}}
>
<CopyIcon className={'mr-2 h-4 w-4'} />
<span>Copy env file to clipboard</span>
</Button>
}
/>
<TooltipContent>
Copy environment variables to clipboard. You can place it in your

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
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

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

View File

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

View File

@@ -66,26 +66,6 @@
--animate-accordion-down: accordion-down 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 {
0% {
opacity: 0;

View File

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

View File

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

View File

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

View File

@@ -31,8 +31,17 @@ export class AuthPageObject {
}
async signOut() {
await this.page.click('[data-test="account-dropdown-trigger"]');
await this.page.click('[data-test="account-dropdown-sign-out"]');
const trigger = this.page.locator(
'[data-test="workspace-dropdown-trigger"], [data-test="account-dropdown-trigger"]',
);
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 }) {

View File

@@ -4,7 +4,7 @@ import { expect, test } from '@playwright/test';
test.describe('Healthcheck endpoint', () => {
test('returns healthy status', async ({ request }) => {
const response = await request.get('/healthcheck');
const response = await request.get('/api/healthcheck');
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"]`,
);
await this.page.click(`[data-test="role-option-${invite.role}"]`);
await this.page.getByRole('option', { name: invite.role }).click();
if (index < invites.length - 1) {
await form.locator('[data-test="add-new-invite-button"]').click();

View File

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

View File

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

View File

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

View File

@@ -1,30 +0,0 @@
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

@@ -1,30 +0,0 @@
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

@@ -1,30 +0,0 @@
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

@@ -1,72 +0,0 @@
'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

@@ -0,0 +1,30 @@
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;

View File

@@ -0,0 +1,30 @@
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('privacyPolicy'),
};
}
async function PrivacyPolicyPage() {
const t = await getTranslations('marketing');
return (
<div>
<SitePageHeader
title={t('privacyPolicy')}
subtitle={t('privacyPolicyDescription')}
/>
<div className={'container mx-auto py-8'}>
<div>Your terms of service content here</div>
</div>
</div>
);
}
export default PrivacyPolicyPage;

View File

@@ -0,0 +1,30 @@
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('termsOfService'),
};
}
async function TermsOfServicePage() {
const t = await getTranslations('marketing');
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 TermsOfServicePage;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,8 +6,6 @@ import { notFound } from 'next/navigation';
import { createCmsClient } from '@kit/cms';
import { withI18n } from '~/lib/i18n/with-i18n';
import { ChangelogDetail } from '../_components/changelog-detail';
interface ChangelogEntryPageProps {
@@ -107,4 +105,4 @@ async function ChangelogEntryPage({ params }: ChangelogEntryPageProps) {
);
}
export default withI18n(ChangelogEntryPage);
export default 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"
>
<ChevronLeft className="h-4 w-4" />
<Trans i18nKey="marketing:changelog" />
<Trans i18nKey="marketing.changelog" />
</Link>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { SidebarMenuButton, SidebarMenuItem } from '@kit/ui/shadcn-sidebar';
import { SidebarMenuButton, SidebarMenuItem } from '@kit/ui/sidebar';
import { cn, isRouteActive } from '@kit/ui/utils';
export function DocsNavLink({
@@ -12,20 +12,18 @@ export function DocsNavLink({
children,
}: React.PropsWithChildren<{ label: string; url: string }>) {
const currentPath = usePathname();
const isCurrent = isRouteActive(url, currentPath, true);
const isCurrent = isRouteActive(url, currentPath);
return (
<SidebarMenuItem>
<SidebarMenuButton
asChild
render={<Link href={url} />}
isActive={isCurrent}
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}
</Link>
{children}
</SidebarMenuButton>
</SidebarMenuItem>
);

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
'use client';
import { Menu } from 'lucide-react';
import { Button } from '@kit/ui/button';
import { useSidebar } from '@kit/ui/sidebar';
export function FloatingDocumentationNavigationButton() {
const { toggleSidebar } = useSidebar();
return (
<Button
size="custom"
variant="custom"
className={
'bg-primary fixed right-5 bottom-5 z-10 h-16! w-16! rounded-full! lg:hidden'
}
onClick={toggleSidebar}
>
<Menu className={'text-primary-foreground size-6'} />
</Button>
);
}

View File

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

View File

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

View File

@@ -1,31 +1,30 @@
import Link from 'next/link';
import { ArrowRight, ChevronDown } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { Button } from '@kit/ui/button';
import { Trans } from '@kit/ui/trans';
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 () => {
const { t } = await createI18nServerInstance();
const t = await getTranslations('marketing');
return {
title: t('marketing:faq'),
title: t('faq'),
};
};
async function FAQPage() {
const { t } = await createI18nServerInstance();
const t = await getTranslations('marketing');
// replace this content with translations
const faqItems = [
{
// or: t('marketing:faq.question1')
// or: t('faq.question1')
question: `Do you offer a free trial?`,
// or: t('marketing:faq.answer1')
// or: t('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.`,
},
{
@@ -74,10 +73,7 @@ async function FAQPage() {
/>
<div className={'flex flex-col space-y-4 xl:space-y-8'}>
<SitePageHeader
title={t('marketing:faq')}
subtitle={t('marketing:faqSubtitle')}
/>
<SitePageHeader title={t('faq')} subtitle={t('faqSubtitle')} />
<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">
@@ -87,14 +83,16 @@ async function FAQPage() {
</div>
<div>
<Button asChild variant={'outline'}>
<Link href={'/contact'}>
<span>
<Trans i18nKey={'marketing:contactFaq'} />
</span>
<Button
nativeButton={false}
render={<Link href={'/contact'} />}
variant={'link'}
>
<span>
<Trans i18nKey={'marketing.contactFaq'} />
</span>
<ArrowRight className={'ml-2 w-4'} />
</Link>
<ArrowRight className={'ml-2 w-4'} />
</Button>
</div>
</div>
@@ -103,7 +101,7 @@ async function FAQPage() {
);
}
export default withI18n(FAQPage);
export default FAQPage;
function FaqItem({
item,

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