Next.js Supabase V3 (#463)
Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
This commit is contained in:
committed by
GitHub
parent
4912e402a3
commit
7ebff31475
@@ -55,7 +55,10 @@ create policy "projects_write" on public.projects for all
|
|||||||
|
|
||||||
Use `server-action-builder` skill for detailed patterns.
|
Use `server-action-builder` skill for detailed patterns.
|
||||||
|
|
||||||
**Rule: Services are decoupled from interfaces.** The service is pure logic that receives dependencies (database client, 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:
|
Create in route's `_lib/server/` directory:
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export class AuthPageObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async signOut() {
|
async signOut() {
|
||||||
await this.page.click('[data-test="account-dropdown-trigger"]');
|
await this.page.click('[data-test="workspace-dropdown-trigger"]');
|
||||||
await this.page.click('[data-test="account-dropdown-sign-out"]');
|
await this.page.click('[data-test="workspace-sign-out"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
async bootstrapUser(params: { email: string; password: string; name: string }) {
|
async bootstrapUser(params: { email: string; password: string; name: string }) {
|
||||||
@@ -47,9 +47,19 @@ export class AuthPageObject {
|
|||||||
## Common Selectors
|
## Common Selectors
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Account dropdown
|
// Workspace dropdown (sidebar header - combined account switcher + user menu)
|
||||||
'[data-test="account-dropdown-trigger"]'
|
'[data-test="workspace-dropdown-trigger"]' // Opens the dropdown
|
||||||
'[data-test="account-dropdown-sign-out"]'
|
'[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
|
// Navigation
|
||||||
'[data-test="sidebar-menu"]'
|
'[data-test="sidebar-menu"]'
|
||||||
|
|||||||
@@ -5,192 +5,92 @@ description: Create or modify client-side forms in React applications following
|
|||||||
|
|
||||||
# React Form Builder Expert
|
# React Form Builder Expert
|
||||||
|
|
||||||
You are an expert React form architect specializing in building robust, accessible, and type-safe forms using react-hook-form, @kit/ui/form components, and Next.js server actions. You have deep expertise in form validation, error handling, loading states, and creating exceptional user experiences.
|
You are an expert React form architect specializing in building robust, accessible, and type-safe forms using react-hook-form, @kit/ui/form components, and Next.js server actions via next-safe-action. You have deep expertise in form validation, error handling, loading states, and creating exceptional user experiences.
|
||||||
|
|
||||||
## Core Responsibilities
|
## Core Responsibilities
|
||||||
|
|
||||||
You will create and modify client-side forms that strictly adhere to these architectural patterns:
|
You will create and modify client-side forms that strictly adhere to these architectural patterns:
|
||||||
|
|
||||||
### 1. Form Structure Requirements
|
### 1. Form Structure Requirements
|
||||||
|
|
||||||
- Always use `useForm` from react-hook-form WITHOUT redundant generic types when using zodResolver
|
- Always use `useForm` from react-hook-form WITHOUT redundant generic types when using zodResolver
|
||||||
- Implement Zod schemas for validation, stored in `_lib/schemas/` directory
|
- Implement Zod schemas for validation, stored in `_lib/schemas/` directory
|
||||||
- Use `@kit/ui/form` components (Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage)
|
- Use `@kit/ui/form` components (Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage)
|
||||||
- Handle loading states with `useTransition` hook
|
- ALWAYS use `useAction` from `next-safe-action/hooks` for server action integration — NEVER use raw `startTransition` + direct action calls
|
||||||
- Implement proper error handling with try/catch blocks
|
- Use `isPending` from `useAction` for loading states
|
||||||
|
|
||||||
### 2. Server Action Integration
|
### 2. Server Action Integration (next-safe-action)
|
||||||
- Call server actions within `startTransition` for proper loading states
|
|
||||||
- Handle redirect errors using `isRedirectError` from 'next/dist/client/components/redirect-error'
|
- ALWAYS use `useAction` hook from `next-safe-action/hooks` — this is the canonical pattern
|
||||||
- Display error states using Alert components from '@kit/ui/alert'
|
- Handle success/error via `onSuccess` and `onError` callbacks in `useAction` options
|
||||||
|
- Use `isPending` from `useAction` for button disabled state
|
||||||
|
- NEVER call server actions directly with `await myAction(data)` — always go through `execute(data)`
|
||||||
|
- NEVER use `useTransition` + `startTransition` for server action calls
|
||||||
|
- NEVER use `isRedirectError` — the `useAction` hook handles this internally
|
||||||
- Ensure server actions are imported from dedicated server files
|
- Ensure server actions are imported from dedicated server files
|
||||||
|
|
||||||
### 3. Code Organization Pattern
|
### 3. Code Organization Pattern
|
||||||
|
|
||||||
```
|
```
|
||||||
_lib/
|
_lib/
|
||||||
├── schemas/
|
├── schemas/
|
||||||
│ └── feature.schema.ts # Shared Zod schemas
|
│ └── feature.schema.ts # Shared Zod schemas
|
||||||
├── server/
|
├── server/
|
||||||
│ └── server-actions.ts # Server actions
|
│ └── server-actions.ts # Server actions (next-safe-action)
|
||||||
└── client/
|
└── client/
|
||||||
└── forms.tsx # Form components
|
└── forms.tsx # Form components
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Import Guidelines
|
### 4. Import Guidelines
|
||||||
|
|
||||||
- Toast notifications: `import { toast } from '@kit/ui/sonner'`
|
- Toast notifications: `import { toast } from '@kit/ui/sonner'`
|
||||||
- Form components: `import { Form, FormField, ... } from '@kit/ui/form'`
|
- Form components: `import { Form, FormField, ... } from '@kit/ui/form'`
|
||||||
|
- Action hook: `import { useAction } from 'next-safe-action/hooks'`
|
||||||
- Always check @kit/ui for components before using external packages
|
- Always check @kit/ui for components before using external packages
|
||||||
- Use `Trans` component from '@kit/ui/trans' for internationalization
|
- Use `Trans` component from '@kit/ui/trans' for internationalization
|
||||||
|
|
||||||
### 5. Best Practices You Must Follow
|
### 5. Best Practices You Must Follow
|
||||||
|
|
||||||
- Add `data-test` attributes for E2E testing on form elements and submit buttons
|
- Add `data-test` attributes for E2E testing on form elements and submit buttons
|
||||||
- Use `reValidateMode: 'onChange'` and `mode: 'onChange'` for responsive validation
|
|
||||||
- Implement proper TypeScript typing without using `any`
|
- Implement proper TypeScript typing without using `any`
|
||||||
- Handle both success and error states gracefully
|
- Handle both success and error states gracefully
|
||||||
- Use `If` component from '@kit/ui/if' for conditional rendering
|
- Use `If` component from '@kit/ui/if' for conditional rendering
|
||||||
- Disable submit buttons during pending states
|
- Disable submit buttons during pending states
|
||||||
- Include FormDescription for user guidance
|
- Include FormDescription for user guidance
|
||||||
- Use Dialog components from '@kit/ui/dialog' when forms are in modals
|
- When forms are inside dialogs, ALWAYS use `useAsyncDialog` from `@kit/ui/hooks/use-async-dialog` — it prevents the dialog from closing while an async operation is in progress (blocks Escape and backdrop clicks). Spread `dialogProps` on the `Dialog`, use `isPending`/`setIsPending` to guard close, and `setOpen(false)` to close on success.
|
||||||
|
|
||||||
### 6. State Management
|
### 6. State Management
|
||||||
- Use `useState` for error states
|
|
||||||
- Use `useTransition` for pending states
|
- Use `useState` for UI state (success/error display)
|
||||||
- Avoid multiple separate useState calls - prefer single state objects when appropriate
|
- Use `isPending` from `useAction` for loading states — NEVER `useTransition`
|
||||||
|
- Avoid multiple separate useState calls — prefer single state objects when appropriate
|
||||||
- Never use useEffect unless absolutely necessary and justified
|
- Never use useEffect unless absolutely necessary and justified
|
||||||
|
|
||||||
### 7. Validation Patterns
|
### 7. Validation Patterns
|
||||||
|
|
||||||
- Create reusable Zod schemas that can be shared between client and server
|
- Create reusable Zod schemas that can be shared between client and server
|
||||||
- Use schema.refine() for custom validation logic
|
- Use schema.refine() for custom validation logic
|
||||||
- Provide clear, user-friendly error messages
|
- Provide clear, user-friendly error messages
|
||||||
- Implement field-level validation with proper error display
|
- Implement field-level validation with proper error display
|
||||||
|
|
||||||
### 8. Error Handling Template
|
### 8. Type Safety
|
||||||
|
|
||||||
```typescript
|
- Let zodResolver infer types — don't add redundant generics to useForm
|
||||||
const onSubmit = (data: FormData) => {
|
|
||||||
startTransition(async () => {
|
|
||||||
try {
|
|
||||||
await serverAction(data);
|
|
||||||
} catch (error) {
|
|
||||||
if (!isRedirectError(error)) {
|
|
||||||
setError(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9. Type Safety
|
|
||||||
- Let zodResolver infer types - don't add redundant generics to useForm
|
|
||||||
- Export schema types when needed for reuse
|
- Export schema types when needed for reuse
|
||||||
- Ensure all form fields have proper typing
|
- Ensure all form fields have proper typing
|
||||||
|
|
||||||
### 10. Accessibility and UX
|
### 9. Accessibility and UX
|
||||||
|
|
||||||
- Always include FormLabel for screen readers
|
- Always include FormLabel for screen readers
|
||||||
- Provide helpful FormDescription text
|
- Provide helpful FormDescription text
|
||||||
- Show clear error messages with FormMessage
|
- Show clear error messages with FormMessage
|
||||||
- Implement loading indicators during form submission
|
- Implement loading indicators during form submission
|
||||||
- Use semantic HTML and ARIA attributes where appropriate
|
- Use semantic HTML and ARIA attributes where appropriate
|
||||||
|
|
||||||
## Complete Form Example
|
## Exemplars
|
||||||
|
|
||||||
```tsx
|
- Standalone form: `apps/web/app/[locale]/(marketing)/contact/_components/contact-form.tsx`
|
||||||
'use client';
|
- Dialog with form: `packages/features/team-accounts/src/components/create-team-account-dialog.tsx` — `useAsyncDialog` + form pattern
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { useTransition, useState } from 'react';
|
|
||||||
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
|
||||||
|
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
|
|
||||||
import { Input } from '@kit/ui/input';
|
|
||||||
import { Button } from '@kit/ui/button';
|
|
||||||
import { Alert, AlertDescription } from '@kit/ui/alert';
|
|
||||||
import { If } from '@kit/ui/if';
|
|
||||||
import { toast } from '@kit/ui/sonner';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
|
|
||||||
import { CreateEntitySchema } from '../_lib/schemas/entity.schema';
|
|
||||||
import { createEntityAction } from '../_lib/server/server-actions';
|
|
||||||
|
|
||||||
export function CreateEntityForm() {
|
|
||||||
const [pending, startTransition] = useTransition();
|
|
||||||
const [error, setError] = useState(false);
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
resolver: zodResolver(CreateEntitySchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
},
|
|
||||||
mode: 'onChange',
|
|
||||||
reValidateMode: 'onChange',
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = (data: z.infer<typeof CreateEntitySchema>) => {
|
|
||||||
setError(false);
|
|
||||||
|
|
||||||
startTransition(async () => {
|
|
||||||
try {
|
|
||||||
await createEntityAction(data);
|
|
||||||
toast.success('Entity created successfully');
|
|
||||||
} catch (e) {
|
|
||||||
if (!isRedirectError(e)) {
|
|
||||||
setError(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
||||||
<Form {...form}>
|
|
||||||
<If condition={error}>
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertDescription>
|
|
||||||
<Trans i18nKey="common:errors.generic" />
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</If>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
<Trans i18nKey="entity:name" />
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
data-test="entity-name-input"
|
|
||||||
placeholder="Enter name"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={pending}
|
|
||||||
data-test="submit-entity-button"
|
|
||||||
>
|
|
||||||
{pending ? (
|
|
||||||
<Trans i18nKey="common:creating" />
|
|
||||||
) : (
|
|
||||||
<Trans i18nKey="common:create" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
When creating forms, you will analyze requirements and produce complete, production-ready implementations that handle all edge cases, provide excellent user feedback, and maintain consistency with the codebase's established patterns. You prioritize type safety, reusability, and maintainability in every form you create.
|
|
||||||
|
|
||||||
Always verify that UI components exist in @kit/ui before importing from external packages, and ensure your forms integrate seamlessly with the project's internationalization system using Trans components.
|
|
||||||
|
|
||||||
## Components
|
## Components
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
## Import Pattern
|
## Import Pattern
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
import { useAction } from 'next-safe-action/hooks';
|
||||||
import { Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from '@kit/ui/form';
|
import { Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from '@kit/ui/form';
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
@@ -10,7 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { Textarea } from '@kit/ui/textarea';
|
import { Textarea } from '@kit/ui/textarea';
|
||||||
import { Checkbox } from '@kit/ui/checkbox';
|
import { Checkbox } from '@kit/ui/checkbox';
|
||||||
import { Switch } from '@kit/ui/switch';
|
import { Switch } from '@kit/ui/switch';
|
||||||
import { Alert, AlertDescription } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
import { toast } from '@kit/ui/sonner';
|
import { toast } from '@kit/ui/sonner';
|
||||||
@@ -21,7 +22,6 @@ import { toast } from '@kit/ui/sonner';
|
|||||||
```tsx
|
```tsx
|
||||||
<FormField
|
<FormField
|
||||||
name="fieldName"
|
name="fieldName"
|
||||||
control={form.control}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
@@ -48,7 +48,6 @@ import { toast } from '@kit/ui/sonner';
|
|||||||
```tsx
|
```tsx
|
||||||
<FormField
|
<FormField
|
||||||
name="category"
|
name="category"
|
||||||
control={form.control}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Category</FormLabel>
|
<FormLabel>Category</FormLabel>
|
||||||
@@ -74,7 +73,6 @@ import { toast } from '@kit/ui/sonner';
|
|||||||
```tsx
|
```tsx
|
||||||
<FormField
|
<FormField
|
||||||
name="acceptTerms"
|
name="acceptTerms"
|
||||||
control={form.control}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex items-center space-x-2">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -97,7 +95,6 @@ import { toast } from '@kit/ui/sonner';
|
|||||||
```tsx
|
```tsx
|
||||||
<FormField
|
<FormField
|
||||||
name="notifications"
|
name="notifications"
|
||||||
control={form.control}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center justify-between">
|
<FormItem className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -121,7 +118,6 @@ import { toast } from '@kit/ui/sonner';
|
|||||||
```tsx
|
```tsx
|
||||||
<FormField
|
<FormField
|
||||||
name="description"
|
name="description"
|
||||||
control={form.control}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
@@ -142,13 +138,14 @@ import { toast } from '@kit/ui/sonner';
|
|||||||
## Error Alert
|
## Error Alert
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<If condition={error}>
|
<Alert variant="destructive">
|
||||||
<Alert variant="destructive">
|
<AlertTitle>
|
||||||
|
<Trans i18nKey="common.errors.title" />
|
||||||
|
</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey="common:errors.generic" />
|
<Trans i18nKey="common.errors.generic" />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</If>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Submit Button
|
## Submit Button
|
||||||
@@ -156,14 +153,10 @@ import { toast } from '@kit/ui/sonner';
|
|||||||
```tsx
|
```tsx
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={pending}
|
disabled={isPending}
|
||||||
data-test="submit-button"
|
data-test="submit-button"
|
||||||
>
|
>
|
||||||
{pending ? (
|
<Trans i18nKey={isPending ? 'common.submitting' : 'common.submit'} />
|
||||||
<Trans i18nKey="common:submitting" />
|
|
||||||
) : (
|
|
||||||
<Trans i18nKey="common:submit" />
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -172,65 +165,79 @@ import { toast } from '@kit/ui/sonner';
|
|||||||
```tsx
|
```tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { useTransition, useState } from 'react';
|
|
||||||
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
|
||||||
import type { z } from 'zod';
|
|
||||||
|
|
||||||
|
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';
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Button } from '@kit/ui/button';
|
|
||||||
import { Alert, AlertDescription } from '@kit/ui/alert';
|
|
||||||
import { If } from '@kit/ui/if';
|
|
||||||
import { toast } from '@kit/ui/sonner';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { MySchema } from '../_lib/schemas/my.schema';
|
import { MySchema } from '../_lib/schemas/my.schema';
|
||||||
import { myAction } from '../_lib/server/server-actions';
|
import { myAction } from '../_lib/server/server-actions';
|
||||||
|
|
||||||
export function MyForm() {
|
export function MyForm() {
|
||||||
const [pending, startTransition] = useTransition();
|
const [state, setState] = useState({
|
||||||
const [error, setError] = useState(false);
|
success: false,
|
||||||
|
error: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { execute, isPending } = useAction(myAction, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setState({ success: true, error: false });
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setState({ error: true, success: false });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(MySchema),
|
resolver: zodResolver(MySchema),
|
||||||
defaultValues: { name: '' },
|
defaultValues: { name: '' },
|
||||||
mode: 'onChange',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: z.infer<typeof MySchema>) => {
|
if (state.success) {
|
||||||
setError(false);
|
return (
|
||||||
|
<Alert variant="success">
|
||||||
startTransition(async () => {
|
<AlertTitle>
|
||||||
try {
|
<Trans i18nKey="common.success" />
|
||||||
await myAction(data);
|
</AlertTitle>
|
||||||
toast.success('Success!');
|
</Alert>
|
||||||
} catch (e) {
|
);
|
||||||
if (!isRedirectError(e)) {
|
}
|
||||||
setError(true);
|
|
||||||
}
|
if (state.error) {
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
||||||
<Form {...form}>
|
|
||||||
<If condition={error}>
|
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
|
<AlertTitle>
|
||||||
|
<Trans i18nKey="common.errors.title" />
|
||||||
|
</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey="common:errors.generic" />
|
<Trans i18nKey="common.errors.generic" />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</If>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className="space-y-4"
|
||||||
|
onSubmit={form.handleSubmit((data) => {
|
||||||
|
execute(data);
|
||||||
|
})}
|
||||||
|
>
|
||||||
<FormField
|
<FormField
|
||||||
name="name"
|
name="name"
|
||||||
control={form.control}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>
|
||||||
|
<Trans i18nKey="namespace:name" />
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input data-test="name-input" {...field} />
|
<Input data-test="name-input" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -239,11 +246,11 @@ export function MyForm() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="submit" disabled={pending} data-test="submit-button">
|
<Button type="submit" disabled={isPending} data-test="submit-button">
|
||||||
{pending ? 'Saving...' : 'Save'}
|
<Trans i18nKey={isPending ? 'common.submitting' : 'common.submit'} />
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
|
||||||
</form>
|
</form>
|
||||||
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -17,19 +17,21 @@ Create validation schema in `_lib/schemas/`:
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// _lib/schemas/feature.schema.ts
|
// _lib/schemas/feature.schema.ts
|
||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
export const CreateFeatureSchema = z.object({
|
export const CreateFeatureSchema = z.object({
|
||||||
name: z.string().min(1, 'Name is required'),
|
name: z.string().min(1, 'Name is required'),
|
||||||
accountId: z.string().uuid('Invalid account ID'),
|
accountId: z.string().uuid('Invalid account ID'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateFeatureInput = z.infer<typeof CreateFeatureSchema>;
|
export type CreateFeatureInput = z.output<typeof CreateFeatureSchema>;
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Create Service Layer
|
### Step 2: Create Service Layer
|
||||||
|
|
||||||
**North star: services are decoupled from their interface.** The service is pure logic — it receives a database client 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/`:
|
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)
|
### 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`:
|
Create action in `_lib/server/server-actions.ts`:
|
||||||
|
|
||||||
@@ -107,13 +111,18 @@ export const createFeatureAction = enhanceAction(
|
|||||||
|
|
||||||
## Key Patterns
|
## Key Patterns
|
||||||
|
|
||||||
1. **Services are pure, interfaces are thin adapters.** The service contains all business logic. The server action (or 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.
|
1. **Services are pure, interfaces are thin adapters.** The service contains all business logic. The server action (or
|
||||||
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.
|
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
|
3. **Schema in separate file** - Reusable between client and server
|
||||||
4. **Logging** - Always log before and after operations
|
4. **Logging** - Always log before and after operations
|
||||||
5. **Revalidation** - Use `revalidatePath` after mutations
|
5. **Revalidation** - Use `revalidatePath` after mutations
|
||||||
6. **Trust RLS** - Don't add manual auth checks (RLS handles it)
|
6. **Trust RLS** - Don't add manual auth checks (RLS handles it)
|
||||||
7. **Testable in isolation** - Because services accept their dependencies, you can test them with a mock client and no 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
|
## File Structure
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export const myAction = enhanceAction(
|
|||||||
### Handler Parameters
|
### Handler Parameters
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
|-----------|------|-------------|
|
|-----------|--------------------|------------------------------------|
|
||||||
| `data` | `z.infer<Schema>` | Validated input data |
|
| `data` | `z.output<Schema>` | Validated input data |
|
||||||
| `user` | `User` | Authenticated user (if auth: true) |
|
| `user` | `User` | Authenticated user (if auth: true) |
|
||||||
|
|
||||||
## enhanceRouteHandler API
|
## enhanceRouteHandler API
|
||||||
@@ -69,7 +69,7 @@ export const GET = enhanceRouteHandler(
|
|||||||
## Common Zod Patterns
|
## Common Zod Patterns
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
// Basic schema
|
// Basic schema
|
||||||
export const CreateItemSchema = z.object({
|
export const CreateItemSchema = z.object({
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ You are an expert at building pure, testable services that are decoupled from th
|
|||||||
|
|
||||||
## North Star
|
## North Star
|
||||||
|
|
||||||
**Every service is decoupled from its interface (I/O).** A service takes plain data in, does work, and returns plain 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
|
## Workflow
|
||||||
|
|
||||||
@@ -21,7 +23,7 @@ Start with the input/output types. These are plain TypeScript — no framework t
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// _lib/schemas/project.schema.ts
|
// _lib/schemas/project.schema.ts
|
||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
export const CreateProjectSchema = z.object({
|
export const CreateProjectSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
@@ -40,7 +42,8 @@ export interface Project {
|
|||||||
|
|
||||||
### Step 2: Build the Service
|
### Step 2: Build the Service
|
||||||
|
|
||||||
The service receives all dependencies through its constructor. It never imports framework-specific modules (`getSupabaseServerClient`, `getLogger`, `revalidatePath`, etc.).
|
The service receives all dependencies through its constructor. It never imports framework-specific modules (
|
||||||
|
`getSupabaseServerClient`, `getLogger`, `revalidatePath`, etc.).
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// _lib/server/project.service.ts
|
// _lib/server/project.service.ts
|
||||||
@@ -95,7 +98,8 @@ class ProjectService {
|
|||||||
|
|
||||||
### Step 3: Write Thin Adapters
|
### Step 3: Write Thin Adapters
|
||||||
|
|
||||||
Each interface is a thin adapter — it resolves dependencies, calls the service, and handles interface-specific 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:**
|
**Server Action adapter:**
|
||||||
|
|
||||||
@@ -234,20 +238,25 @@ describe('ProjectService', () => {
|
|||||||
|
|
||||||
## Rules
|
## 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
|
## What Goes Where
|
||||||
|
|
||||||
| Concern | Location | Example |
|
| Concern | Location | Example |
|
||||||
|---------|----------|---------|
|
|------------------------|-------------------------------------------|-------------------------------------------|
|
||||||
| Input validation (Zod) | `_lib/schemas/` | `CreateProjectSchema` |
|
| Input validation (Zod) | `_lib/schemas/` | `CreateProjectSchema` |
|
||||||
| Business logic | `_lib/server/*.service.ts` | `ProjectService.create()` |
|
| Business logic | `_lib/server/*.service.ts` | `ProjectService.create()` |
|
||||||
| Auth check | Adapter (`enhanceAction({ auth: true })`) | Server action wrapper |
|
| Auth check | Adapter (`enhanceAction({ auth: true })`) | Server action wrapper |
|
||||||
@@ -305,4 +314,5 @@ const result = await client.from('projects').insert(...).select().single();
|
|||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
See `[Examples](examples.md)` for more patterns including services with multiple dependencies, services that compose 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.
|
||||||
|
|||||||
20
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
20
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
@@ -1,14 +1,12 @@
|
|||||||
name: "🐛 Bug Report"
|
name: '🐛 Bug Report'
|
||||||
description: Create a new ticket for a bug.
|
description: Create a new ticket for a bug.
|
||||||
title: "🐛 [BUG] - <title>"
|
title: '🐛 [BUG] - <title>'
|
||||||
labels: [
|
labels: ['bug']
|
||||||
"bug"
|
|
||||||
]
|
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
label: "Description"
|
label: 'Description'
|
||||||
description: Please enter an explicit description of your issue
|
description: Please enter an explicit description of your issue
|
||||||
placeholder: Short and explicit description of your incident...
|
placeholder: Short and explicit description of your incident...
|
||||||
validations:
|
validations:
|
||||||
@@ -16,7 +14,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: reprod
|
id: reprod
|
||||||
attributes:
|
attributes:
|
||||||
label: "Reproduction steps"
|
label: 'Reproduction steps'
|
||||||
description: Please enter an explicit description of your issue
|
description: Please enter an explicit description of your issue
|
||||||
value: |
|
value: |
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
@@ -29,13 +27,13 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: logs
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
label: "Logs"
|
label: 'Logs'
|
||||||
description: Please copy and paste any relevant log output. Please verify both the server logs and the browser logs (if applicable). This will be automatically formatted into code, so no need for backticks. If you cannot provide logs, simply write N/A.
|
description: Please copy and paste any relevant log output. Please verify both the server logs and the browser logs (if applicable). This will be automatically formatted into code, so no need for backticks. If you cannot provide logs, simply write N/A.
|
||||||
render: bash
|
render: bash
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: screenshot
|
id: screenshot
|
||||||
attributes:
|
attributes:
|
||||||
label: "Screenshots"
|
label: 'Screenshots'
|
||||||
description: If applicable, add screenshots to help explain your problem.
|
description: If applicable, add screenshots to help explain your problem.
|
||||||
value: |
|
value: |
|
||||||

|

|
||||||
@@ -45,7 +43,7 @@ body:
|
|||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: browsers
|
id: browsers
|
||||||
attributes:
|
attributes:
|
||||||
label: "Browsers"
|
label: 'Browsers'
|
||||||
description: What browsers are you seeing the problem on ?
|
description: What browsers are you seeing the problem on ?
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
@@ -59,7 +57,7 @@ body:
|
|||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: os
|
id: os
|
||||||
attributes:
|
attributes:
|
||||||
label: "OS"
|
label: 'OS'
|
||||||
description: What is the impacted environment ?
|
description: What is the impacted environment ?
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
|
|||||||
8
.github/workflows/workflow.yml
vendored
8
.github/workflows/workflow.yml
vendored
@@ -1,14 +1,14 @@
|
|||||||
name: Workflow
|
name: Workflow
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [main]
|
||||||
jobs:
|
jobs:
|
||||||
typescript:
|
typescript:
|
||||||
name: ʦ TypeScript
|
name: ʦ TypeScript
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ vars.RUNNER || 'ubuntu-latest' }}
|
||||||
env:
|
env:
|
||||||
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
|
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
|
||||||
SUPABASE_DB_WEBHOOK_SECRET: ${{ secrets.SUPABASE_DB_WEBHOOK_SECRET }}
|
SUPABASE_DB_WEBHOOK_SECRET: ${{ secrets.SUPABASE_DB_WEBHOOK_SECRET }}
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
name: ⚫️ Test
|
name: ⚫️ Test
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ vars.RUNNER || 'ubuntu-latest' }}
|
||||||
if: ${{ vars.ENABLE_E2E_JOB == 'true' }}
|
if: ${{ vars.ENABLE_E2E_JOB == 'true' }}
|
||||||
env:
|
env:
|
||||||
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
|
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
|
||||||
|
|||||||
@@ -556,8 +556,8 @@ function MyFeaturePage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MyFeatureHeader
|
<MyFeatureHeader
|
||||||
title={<Trans i18nKey={'common:routes.myFeature'} />}
|
title={<Trans i18nKey={'common.routes.myFeature'} />}
|
||||||
description={<Trans i18nKey={'common:myFeatureDescription'} />}
|
description={<Trans i18nKey={'common.myFeatureDescription'} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageBody>
|
<PageBody>
|
||||||
@@ -830,7 +830,7 @@ import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
|||||||
## Core Shadcn UI Components
|
## Core Shadcn UI Components
|
||||||
|
|
||||||
| Component | Description | Import Path |
|
| Component | Description | Import Path |
|
||||||
|-----------|-------------|-------------|
|
|------------------|-------------------------------------------|-------------------------------------------------------------------------------------------------|
|
||||||
| `Accordion` | Expandable/collapsible content sections | `@kit/ui/accordion` [accordion.tsx](mdc:packages/ui/src/shadcn/accordion.tsx) |
|
| `Accordion` | Expandable/collapsible content sections | `@kit/ui/accordion` [accordion.tsx](mdc:packages/ui/src/shadcn/accordion.tsx) |
|
||||||
| `AlertDialog` | Modal dialog for important actions | `@kit/ui/alert-dialog` [alert-dialog.tsx](mdc:packages/ui/src/shadcn/alert-dialog.tsx) |
|
| `AlertDialog` | Modal dialog for important actions | `@kit/ui/alert-dialog` [alert-dialog.tsx](mdc:packages/ui/src/shadcn/alert-dialog.tsx) |
|
||||||
| `Alert` | Status/notification messages | `@kit/ui/alert` [alert.tsx](mdc:packages/ui/src/shadcn/alert.tsx) |
|
| `Alert` | Status/notification messages | `@kit/ui/alert` [alert.tsx](mdc:packages/ui/src/shadcn/alert.tsx) |
|
||||||
@@ -856,7 +856,7 @@ import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
|||||||
| `Select` | Dropdown selection menu | `@kit/ui/select` [select.tsx](mdc:packages/ui/src/shadcn/select.tsx) |
|
| `Select` | Dropdown selection menu | `@kit/ui/select` [select.tsx](mdc:packages/ui/src/shadcn/select.tsx) |
|
||||||
| `Separator` | Visual divider between content | `@kit/ui/separator` [separator.tsx](mdc:packages/ui/src/shadcn/separator.tsx) |
|
| `Separator` | Visual divider between content | `@kit/ui/separator` [separator.tsx](mdc:packages/ui/src/shadcn/separator.tsx) |
|
||||||
| `Sheet` | Sliding panel from screen edge | `@kit/ui/sheet` [sheet.tsx](mdc:packages/ui/src/shadcn/sheet.tsx) |
|
| `Sheet` | Sliding panel from screen edge | `@kit/ui/sheet` [sheet.tsx](mdc:packages/ui/src/shadcn/sheet.tsx) |
|
||||||
| `Sidebar` | Advanced sidebar navigation | `@kit/ui/shadcn-sidebar` [sidebar.tsx](mdc:packages/ui/src/shadcn/sidebar.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) |
|
| `Skeleton` | Loading placeholder | `@kit/ui/skeleton` [skeleton.tsx](mdc:packages/ui/src/shadcn/skeleton.tsx) |
|
||||||
| `Switch` | Toggle control | `@kit/ui/switch` [switch.tsx](mdc:packages/ui/src/shadcn/switch.tsx) |
|
| `Switch` | Toggle control | `@kit/ui/switch` [switch.tsx](mdc:packages/ui/src/shadcn/switch.tsx) |
|
||||||
| `Toast` | Toaster | `@kit/ui/sonner` [sonner.tsx](mdc:packages/ui/src/shadcn/sonner.tsx) |
|
| `Toast` | Toaster | `@kit/ui/sonner` [sonner.tsx](mdc:packages/ui/src/shadcn/sonner.tsx) |
|
||||||
@@ -920,7 +920,7 @@ Zod schemas should be defined in the `schema` folder and exported, so we can reu
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// _lib/schema/create-note.schema.ts
|
// _lib/schema/create-note.schema.ts
|
||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
export const CreateNoteSchema = z.object({
|
export const CreateNoteSchema = z.object({
|
||||||
title: z.string().min(1),
|
title: z.string().min(1),
|
||||||
@@ -935,7 +935,7 @@ Server Actions [server-actions.mdc](mdc:.cursor/rules/server-actions.mdc) can he
|
|||||||
```tsx
|
```tsx
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
import { enhanceAction } from '@kit/next/actions';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
import { CreateNoteSchema } from '../schema/create-note.schema';
|
import { CreateNoteSchema } from '../schema/create-note.schema';
|
||||||
|
|
||||||
@@ -965,7 +965,7 @@ Then create a client component to handle the form submission:
|
|||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
import { Textarea } from '@kit/ui/textarea';
|
import { Textarea } from '@kit/ui/textarea';
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
|
||||||
@@ -1436,7 +1436,7 @@ You always must use `(security_invoker = true)` for views.
|
|||||||
```tsx
|
```tsx
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
import { enhanceAction } from '@kit/next/actions';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
import { EntitySchema } from '../entity.schema.ts`;
|
import { EntitySchema } from '../entity.schema.ts`;
|
||||||
|
|
||||||
@@ -1463,7 +1463,7 @@ export const myServerAction = enhanceAction(
|
|||||||
- To create API routes (route.ts), always use the `enhanceRouteHandler` function from the "@kit/supabase/routes" package. [index.ts](mdc:packages/next/src/routes/index.ts)
|
- To create API routes (route.ts), always use the `enhanceRouteHandler` function from the "@kit/supabase/routes" package. [index.ts](mdc:packages/next/src/routes/index.ts)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
import { enhanceRouteHandler } from '@kit/next/routes';
|
import { enhanceRouteHandler } from '@kit/next/routes';
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
|||||||
3
.npmrc
3
.npmrc
@@ -3,9 +3,6 @@ dedupe-peer-dependents=true
|
|||||||
use-lockfile-v6=true
|
use-lockfile-v6=true
|
||||||
resolution-mode=highest
|
resolution-mode=highest
|
||||||
package-manager-strict=false
|
package-manager-strict=false
|
||||||
public-hoist-pattern[]=*i18next*
|
|
||||||
public-hoist-pattern[]=*eslint*
|
|
||||||
public-hoist-pattern[]=*prettier*
|
|
||||||
public-hoist-pattern[]=*require-in-the-middle*
|
public-hoist-pattern[]=*require-in-the-middle*
|
||||||
public-hoist-pattern[]=*import-in-the-middle*
|
public-hoist-pattern[]=*import-in-the-middle*
|
||||||
public-hoist-pattern[]=*pino*
|
public-hoist-pattern[]=*pino*
|
||||||
64
.oxfmtrc.jsonc
Normal file
64
.oxfmtrc.jsonc
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"sortImports": {
|
||||||
|
"customGroups": [
|
||||||
|
{
|
||||||
|
"groupName": "server-only",
|
||||||
|
"elementNamePattern": ["server-only"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"groupName": "react",
|
||||||
|
"elementNamePattern": ["react", "react-dom"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"groupName": "next",
|
||||||
|
"elementNamePattern": ["next", "next/**"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"groupName": "supabase",
|
||||||
|
"elementNamePattern": ["@supabase/**"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"groupName": "kit",
|
||||||
|
"elementNamePattern": ["@kit/**"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"groupName": "app",
|
||||||
|
"elementNamePattern": ["~/**"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
"server-only",
|
||||||
|
"react",
|
||||||
|
"next",
|
||||||
|
"supabase",
|
||||||
|
"external",
|
||||||
|
"kit",
|
||||||
|
"app",
|
||||||
|
["parent", "sibling", "index"],
|
||||||
|
"style",
|
||||||
|
],
|
||||||
|
"newlinesBetween": true,
|
||||||
|
"order": "asc",
|
||||||
|
},
|
||||||
|
"sortTailwindcss": {
|
||||||
|
"functions": ["tw", "clsx", "cn", "cva"],
|
||||||
|
},
|
||||||
|
"ignorePatterns": [
|
||||||
|
"node_modules",
|
||||||
|
"database.types.ts",
|
||||||
|
"playwright-report",
|
||||||
|
"*.hbs",
|
||||||
|
"*.md",
|
||||||
|
"dist",
|
||||||
|
"build",
|
||||||
|
".next",
|
||||||
|
"next-env.d.ts",
|
||||||
|
],
|
||||||
|
}
|
||||||
59
.oxlintrc.json
Normal file
59
.oxlintrc.json
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["react", "nextjs", "import"],
|
||||||
|
"rules": {
|
||||||
|
"no-undef": "off",
|
||||||
|
"typescript/triple-slash-reference": "off",
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"import/no-anonymous-default-export": "off",
|
||||||
|
"import/named": "off",
|
||||||
|
"import/namespace": "off",
|
||||||
|
"import/default": "off",
|
||||||
|
"import/no-unresolved": "off",
|
||||||
|
"import/no-named-as-default-member": "off",
|
||||||
|
"import/no-named-as-default": "off",
|
||||||
|
"import/no-cycle": "off",
|
||||||
|
"import/no-unused-modules": "off",
|
||||||
|
"import/no-deprecated": "off",
|
||||||
|
"typescript/array-type": "off",
|
||||||
|
"typescript/no-unsafe-assignment": "off",
|
||||||
|
"typescript/no-unsafe-argument": "off",
|
||||||
|
"typescript/consistent-type-definitions": "off",
|
||||||
|
"typescript/no-unsafe-member-access": "off",
|
||||||
|
"typescript/non-nullable-type-assertion-style": "off",
|
||||||
|
"typescript/only-throw-error": "off",
|
||||||
|
"typescript/prefer-nullish-coalescing": "off",
|
||||||
|
"typescript/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"caughtErrorsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nextjs/no-html-link-for-pages": "off",
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"name": "react-i18next",
|
||||||
|
"importNames": ["Trans"],
|
||||||
|
"message": "Please use `@kit/ui/trans` instead"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ignorePatterns": [
|
||||||
|
"**/node_modules",
|
||||||
|
"**/database.types.ts",
|
||||||
|
"**/.next",
|
||||||
|
"**/public",
|
||||||
|
"**/__tests__/",
|
||||||
|
"dist",
|
||||||
|
"pnpm-lock.yaml",
|
||||||
|
"apps/dev-tool",
|
||||||
|
"tooling/scripts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
database.types.ts
|
|
||||||
playwright-report
|
|
||||||
*.hbs
|
|
||||||
*.md
|
|
||||||
dist
|
|
||||||
build
|
|
||||||
.next
|
|
||||||
next-env.d.ts
|
|
||||||
15
AGENTS.md
15
AGENTS.md
@@ -10,7 +10,7 @@
|
|||||||
## Monorepo Structure
|
## Monorepo Structure
|
||||||
|
|
||||||
| Directory | Purpose | Details |
|
| Directory | Purpose | Details |
|
||||||
|-----------|---------|---------|
|
| ------------------- | ----------------------------- | --------------------------------- |
|
||||||
| `apps/web` | Main Next.js app | See `apps/web/AGENTS.md` |
|
| `apps/web` | Main Next.js app | See `apps/web/AGENTS.md` |
|
||||||
| `apps/web/supabase` | Database schemas & migrations | See `apps/web/supabase/AGENTS.md` |
|
| `apps/web/supabase` | Database schemas & migrations | See `apps/web/supabase/AGENTS.md` |
|
||||||
| `apps/e2e` | Playwright E2E tests | See `apps/e2e/AGENTS.md` |
|
| `apps/e2e` | Playwright E2E tests | See `apps/e2e/AGENTS.md` |
|
||||||
@@ -19,6 +19,14 @@
|
|||||||
| `packages/next` | Next.js utilities | See `packages/next/AGENTS.md` |
|
| `packages/next` | Next.js utilities | See `packages/next/AGENTS.md` |
|
||||||
| `packages/features` | Feature packages | See `packages/features/AGENTS.md` |
|
| `packages/features` | Feature packages | See `packages/features/AGENTS.md` |
|
||||||
|
|
||||||
|
<!-- BEGIN:nextjs-agent-rules -->
|
||||||
|
|
||||||
|
# Next.js: ALWAYS read docs before coding
|
||||||
|
|
||||||
|
Before any Next.js work, find and read the relevant doc in `apps/web/node_modules/next/dist/docs/`. Your training data is outdated — the docs are the source of truth.
|
||||||
|
|
||||||
|
<!-- END:nextjs-agent-rules -->
|
||||||
|
|
||||||
## Multi-Tenant Architecture
|
## Multi-Tenant Architecture
|
||||||
|
|
||||||
- **Personal Accounts**: `auth.users.id = accounts.id`
|
- **Personal Accounts**: `auth.users.id = accounts.id`
|
||||||
@@ -40,8 +48,8 @@ pnpm format:fix # Format code
|
|||||||
## Key Patterns (Quick Reference)
|
## Key Patterns (Quick Reference)
|
||||||
|
|
||||||
| Pattern | Import | Details |
|
| Pattern | Import | Details |
|
||||||
|---------|--------|---------|
|
| -------------- | ------------------------------------------------------------ | ----------------------------- |
|
||||||
| Server Actions | `enhanceAction` from `@kit/next/actions` | `packages/next/AGENTS.md` |
|
| Server Actions | `authActionClient` from `@kit/next/safe-action` | `packages/next/AGENTS.md` |
|
||||||
| Route Handlers | `enhanceRouteHandler` from `@kit/next/routes` | `packages/next/AGENTS.md` |
|
| Route Handlers | `enhanceRouteHandler` from `@kit/next/routes` | `packages/next/AGENTS.md` |
|
||||||
| Server Client | `getSupabaseServerClient` from `@kit/supabase/server-client` | `packages/supabase/AGENTS.md` |
|
| Server Client | `getSupabaseServerClient` from `@kit/supabase/server-client` | `packages/supabase/AGENTS.md` |
|
||||||
| UI Components | `@kit/ui/{component}` | `packages/ui/AGENTS.md` |
|
| UI Components | `@kit/ui/{component}` | `packages/ui/AGENTS.md` |
|
||||||
@@ -55,6 +63,7 @@ pnpm format:fix # Format code
|
|||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
After implementation, always run:
|
After implementation, always run:
|
||||||
|
|
||||||
1. `pnpm typecheck`
|
1. `pnpm typecheck`
|
||||||
2. `pnpm lint:fix`
|
2. `pnpm lint:fix`
|
||||||
3. `pnpm format:fix`
|
3. `pnpm format:fix`
|
||||||
|
|||||||
@@ -120,9 +120,7 @@ export function AlertDialogStory() {
|
|||||||
|
|
||||||
const generateCode = () => {
|
const generateCode = () => {
|
||||||
let code = `<AlertDialog>\n`;
|
let code = `<AlertDialog>\n`;
|
||||||
code += ` <AlertDialogTrigger asChild>\n`;
|
code += ` <AlertDialogTrigger render={<Button variant="${controls.triggerVariant}">${controls.triggerText}</Button>} />\n`;
|
||||||
code += ` <Button variant="${controls.triggerVariant}">${controls.triggerText}</Button>\n`;
|
|
||||||
code += ` </AlertDialogTrigger>\n`;
|
|
||||||
code += ` <AlertDialogContent>\n`;
|
code += ` <AlertDialogContent>\n`;
|
||||||
code += ` <AlertDialogHeader>\n`;
|
code += ` <AlertDialogHeader>\n`;
|
||||||
|
|
||||||
@@ -179,11 +177,14 @@ export function AlertDialogStory() {
|
|||||||
const renderPreview = () => {
|
const renderPreview = () => {
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger
|
||||||
|
render={
|
||||||
<Button variant={controls.triggerVariant}>
|
<Button variant={controls.triggerVariant}>
|
||||||
{controls.triggerText}
|
{controls.triggerText}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
{controls.withIcon ? (
|
{controls.withIcon ? (
|
||||||
@@ -341,11 +342,11 @@ export function AlertDialogStory() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger
|
||||||
<Button variant="destructive" size="sm">
|
render={<Button variant="destructive" size="sm" />}
|
||||||
|
>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
Delete Item
|
Delete Item
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -370,11 +371,9 @@ export function AlertDialogStory() {
|
|||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">
|
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
Sign Out
|
Sign Out
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -397,11 +396,9 @@ export function AlertDialogStory() {
|
|||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">
|
|
||||||
<UserX className="mr-2 h-4 w-4" />
|
<UserX className="mr-2 h-4 w-4" />
|
||||||
Remove User
|
Remove User
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -438,11 +435,9 @@ export function AlertDialogStory() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">
|
|
||||||
<Archive className="mr-2 h-4 w-4" />
|
<Archive className="mr-2 h-4 w-4" />
|
||||||
Archive Project
|
Archive Project
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -465,11 +460,9 @@ export function AlertDialogStory() {
|
|||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger render={<Button />}>
|
||||||
<Button>
|
|
||||||
<Download className="mr-2 h-4 w-4" />
|
<Download className="mr-2 h-4 w-4" />
|
||||||
Export Data
|
Export Data
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -493,11 +486,9 @@ export function AlertDialogStory() {
|
|||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">
|
|
||||||
<RefreshCw className="mr-2 h-4 w-4" />
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
Reset Settings
|
Reset Settings
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -535,11 +526,11 @@ export function AlertDialogStory() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-sm font-semibold">Error/Destructive</h4>
|
<h4 className="text-sm font-semibold">Error/Destructive</h4>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger
|
||||||
<Button variant="destructive" size="sm">
|
render={<Button variant="destructive" size="sm" />}
|
||||||
|
>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
Delete Forever
|
Delete Forever
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -567,11 +558,11 @@ export function AlertDialogStory() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-sm font-semibold">Warning</h4>
|
<h4 className="text-sm font-semibold">Warning</h4>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger
|
||||||
<Button variant="outline" size="sm">
|
render={<Button variant="outline" size="sm" />}
|
||||||
|
>
|
||||||
<AlertTriangle className="mr-2 h-4 w-4" />
|
<AlertTriangle className="mr-2 h-4 w-4" />
|
||||||
Unsaved Changes
|
Unsaved Changes
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -597,11 +588,11 @@ export function AlertDialogStory() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-sm font-semibold">Info</h4>
|
<h4 className="text-sm font-semibold">Info</h4>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger
|
||||||
<Button variant="outline" size="sm">
|
render={<Button variant="outline" size="sm" />}
|
||||||
|
>
|
||||||
<Share className="mr-2 h-4 w-4" />
|
<Share className="mr-2 h-4 w-4" />
|
||||||
Share Publicly
|
Share Publicly
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -627,11 +618,9 @@ export function AlertDialogStory() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-sm font-semibold">Success</h4>
|
<h4 className="text-sm font-semibold">Success</h4>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger render={<Button size="sm" />}>
|
||||||
<Button size="sm">
|
|
||||||
<Download className="mr-2 h-4 w-4" />
|
<Download className="mr-2 h-4 w-4" />
|
||||||
Complete Setup
|
Complete Setup
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -850,10 +839,8 @@ export function AlertDialogStory() {
|
|||||||
<h4 className="text-sm font-semibold">Focus Management</h4>
|
<h4 className="text-sm font-semibold">Focus Management</h4>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
• Focus moves to Cancel button by default
|
• Focus moves to Cancel button by default
|
||||||
<br />
|
<br />• Tab navigation between Cancel and Action
|
||||||
• Tab navigation between Cancel and Action
|
<br />• Escape key activates Cancel action
|
||||||
<br />
|
|
||||||
• Escape key activates Cancel action
|
|
||||||
<br />• Enter key activates Action button when focused
|
<br />• Enter key activates Action button when focused
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -861,10 +848,8 @@ export function AlertDialogStory() {
|
|||||||
<h4 className="text-sm font-semibold">Content Guidelines</h4>
|
<h4 className="text-sm font-semibold">Content Guidelines</h4>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
• Use clear, specific titles and descriptions
|
• Use clear, specific titles and descriptions
|
||||||
<br />
|
<br />• Explain consequences of the action
|
||||||
• Explain consequences of the action
|
<br />• Use action-specific button labels
|
||||||
<br />
|
|
||||||
• Use action-specific button labels
|
|
||||||
<br />• Always provide a way to cancel
|
<br />• Always provide a way to cancel
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -872,8 +857,7 @@ export function AlertDialogStory() {
|
|||||||
<h4 className="text-sm font-semibold">Visual Design</h4>
|
<h4 className="text-sm font-semibold">Visual Design</h4>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
• Use appropriate icons and colors for severity
|
• Use appropriate icons and colors for severity
|
||||||
<br />
|
<br />• Make destructive actions visually distinct
|
||||||
• Make destructive actions visually distinct
|
|
||||||
<br />• Ensure sufficient contrast for all text
|
<br />• Ensure sufficient contrast for all text
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -892,8 +876,7 @@ export function AlertDialogStory() {
|
|||||||
<h4 className="text-sm font-semibold">Title Guidelines</h4>
|
<h4 className="text-sm font-semibold">Title Guidelines</h4>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
• Be specific about the action (not just "Are you sure?")
|
• Be specific about the action (not just "Are you sure?")
|
||||||
<br />
|
<br />• Use active voice ("Delete account" not "Account deletion")
|
||||||
• Use active voice ("Delete account" not "Account deletion")
|
|
||||||
<br />• Keep it concise but descriptive
|
<br />• Keep it concise but descriptive
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -901,10 +884,8 @@ export function AlertDialogStory() {
|
|||||||
<h4 className="text-sm font-semibold">Description Guidelines</h4>
|
<h4 className="text-sm font-semibold">Description Guidelines</h4>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
• Explain what will happen
|
• Explain what will happen
|
||||||
<br />
|
<br />• Mention if the action is irreversible
|
||||||
• Mention if the action is irreversible
|
<br />• Provide context about consequences
|
||||||
<br />
|
|
||||||
• Provide context about consequences
|
|
||||||
<br />• Use plain, non-technical language
|
<br />• Use plain, non-technical language
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -912,10 +893,8 @@ export function AlertDialogStory() {
|
|||||||
<h4 className="text-sm font-semibold">Button Labels</h4>
|
<h4 className="text-sm font-semibold">Button Labels</h4>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
• Use specific verbs ("Delete", "Save", "Continue")
|
• Use specific verbs ("Delete", "Save", "Continue")
|
||||||
<br />
|
<br />• Match the action being performed
|
||||||
• Match the action being performed
|
<br />• Avoid generic labels when possible
|
||||||
<br />
|
|
||||||
• Avoid generic labels when possible
|
|
||||||
<br />• Make the primary action clear
|
<br />• Make the primary action clear
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ interface ButtonControls {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
withIcon: boolean;
|
withIcon: boolean;
|
||||||
fullWidth: boolean;
|
fullWidth: boolean;
|
||||||
asChild: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const variantOptions = [
|
const variantOptions = [
|
||||||
@@ -68,7 +67,6 @@ export function ButtonStory() {
|
|||||||
loading: false,
|
loading: false,
|
||||||
withIcon: false,
|
withIcon: false,
|
||||||
fullWidth: false,
|
fullWidth: false,
|
||||||
asChild: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateCode = () => {
|
const generateCode = () => {
|
||||||
@@ -77,14 +75,12 @@ export function ButtonStory() {
|
|||||||
variant: controls.variant,
|
variant: controls.variant,
|
||||||
size: controls.size,
|
size: controls.size,
|
||||||
disabled: controls.disabled,
|
disabled: controls.disabled,
|
||||||
asChild: controls.asChild,
|
|
||||||
className: controls.fullWidth ? 'w-full' : '',
|
className: controls.fullWidth ? 'w-full' : '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
size: 'default',
|
size: 'default',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
asChild: false,
|
|
||||||
className: '',
|
className: '',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -194,15 +190,6 @@ export function ButtonStory() {
|
|||||||
onCheckedChange={(checked) => updateControl('fullWidth', checked)}
|
onCheckedChange={(checked) => updateControl('fullWidth', checked)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label htmlFor="asChild">As Child</Label>
|
|
||||||
<Switch
|
|
||||||
id="asChild"
|
|
||||||
checked={controls.asChild}
|
|
||||||
onCheckedChange={(checked) => updateControl('asChild', checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -276,11 +276,11 @@ export default function CalendarStory() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex justify-center pt-6">
|
<CardContent className="flex justify-center pt-6">
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger
|
||||||
<Button variant="outline" className="justify-start">
|
render={<Button variant="outline" className="justify-start" />}
|
||||||
|
>
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
Pick a date
|
Pick a date
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
<Calendar
|
<Calendar
|
||||||
|
|||||||
@@ -320,10 +320,12 @@ export function CardButtonStory() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr className="border-b">
|
<tr className="border-b">
|
||||||
<td className="p-3 font-mono text-sm">asChild</td>
|
<td className="p-3 font-mono text-sm">render</td>
|
||||||
<td className="p-3 font-mono text-sm">boolean</td>
|
<td className="p-3 font-mono text-sm">
|
||||||
<td className="p-3 font-mono text-sm">false</td>
|
React.ReactElement
|
||||||
<td className="p-3">Render as child element</td>
|
</td>
|
||||||
|
<td className="p-3 font-mono text-sm">-</td>
|
||||||
|
<td className="p-3">Compose with a custom element</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="border-b">
|
<tr className="border-b">
|
||||||
<td className="p-3 font-mono text-sm">className</td>
|
<td className="p-3 font-mono text-sm">className</td>
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ export function DialogStory() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let code = `<Dialog>\n`;
|
let code = `<Dialog>\n`;
|
||||||
code += ` <DialogTrigger asChild>\n`;
|
code += ` <DialogTrigger render={<Button variant="${controls.triggerVariant}" />}>\n`;
|
||||||
code += ` <Button variant="${controls.triggerVariant}">${controls.triggerText}</Button>\n`;
|
code += ` ${controls.triggerText}\n`;
|
||||||
code += ` </DialogTrigger>\n`;
|
code += ` </DialogTrigger>\n`;
|
||||||
code += ` <DialogContent${contentPropsString}>\n`;
|
code += ` <DialogContent${contentPropsString}>\n`;
|
||||||
code += ` <DialogHeader>\n`;
|
code += ` <DialogHeader>\n`;
|
||||||
@@ -182,8 +182,8 @@ export function DialogStory() {
|
|||||||
|
|
||||||
if (controls.withFooter) {
|
if (controls.withFooter) {
|
||||||
code += ` <DialogFooter>\n`;
|
code += ` <DialogFooter>\n`;
|
||||||
code += ` <DialogClose asChild>\n`;
|
code += ` <DialogClose render={<Button variant="outline" />}>\n`;
|
||||||
code += ` <Button variant="outline">Cancel</Button>\n`;
|
code += ` Cancel\n`;
|
||||||
code += ` </DialogClose>\n`;
|
code += ` </DialogClose>\n`;
|
||||||
code += ` <Button>Save Changes</Button>\n`;
|
code += ` <Button>Save Changes</Button>\n`;
|
||||||
code += ` </DialogFooter>\n`;
|
code += ` </DialogFooter>\n`;
|
||||||
@@ -198,10 +198,8 @@ export function DialogStory() {
|
|||||||
const renderPreview = () => {
|
const renderPreview = () => {
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger render={<Button variant={controls.triggerVariant} />}>
|
||||||
<Button variant={controls.triggerVariant}>
|
|
||||||
{controls.triggerText}
|
{controls.triggerText}
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -271,8 +269,8 @@ export function DialogStory() {
|
|||||||
|
|
||||||
{controls.withFooter && (
|
{controls.withFooter && (
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">Cancel</Button>
|
Cancel
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button>Save Changes</Button>
|
<Button>Save Changes</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -391,11 +389,9 @@ export function DialogStory() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">
|
|
||||||
<Info className="mr-2 h-4 w-4" />
|
<Info className="mr-2 h-4 w-4" />
|
||||||
Info Dialog
|
Info Dialog
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -412,19 +408,15 @@ export function DialogStory() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose render={<Button />}>Got it</DialogClose>
|
||||||
<Button>Got it</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger render={<Button />}>
|
||||||
<Button>
|
|
||||||
<Edit className="mr-2 h-4 w-4" />
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
Edit Profile
|
Edit Profile
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -456,8 +448,8 @@ export function DialogStory() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">Cancel</Button>
|
Cancel
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button>Save Changes</Button>
|
<Button>Save Changes</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -465,11 +457,9 @@ export function DialogStory() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger render={<Button variant="secondary" />}>
|
||||||
<Button variant="secondary">
|
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
Settings
|
Settings
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -499,8 +489,8 @@ export function DialogStory() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">Cancel</Button>
|
Cancel
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button>Save</Button>
|
<Button>Save</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -518,10 +508,8 @@ export function DialogStory() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger render={<Button variant="outline" size="sm" />}>
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
Small Dialog
|
Small Dialog
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-md">
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -536,16 +524,14 @@ export function DialogStory() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose render={<Button />}>Close</DialogClose>
|
||||||
<Button>Close</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">Large Dialog</Button>
|
Large Dialog
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -571,8 +557,8 @@ export function DialogStory() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">Cancel</Button>
|
Cancel
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button>Save</Button>
|
<Button>Save</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -590,11 +576,9 @@ export function DialogStory() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">
|
|
||||||
<Image className="mr-2 h-4 w-4" />
|
<Image className="mr-2 h-4 w-4" />
|
||||||
Image Gallery
|
Image Gallery
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -627,11 +611,9 @@ export function DialogStory() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">
|
|
||||||
<MessageSquare className="mr-2 h-4 w-4" />
|
<MessageSquare className="mr-2 h-4 w-4" />
|
||||||
Feedback
|
Feedback
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -668,8 +650,8 @@ export function DialogStory() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">Cancel</Button>
|
Cancel
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button>
|
<Button>
|
||||||
<MessageSquare className="mr-2 h-4 w-4" />
|
<MessageSquare className="mr-2 h-4 w-4" />
|
||||||
@@ -736,8 +718,8 @@ export function DialogStory() {
|
|||||||
<div>
|
<div>
|
||||||
<h4 className="mb-3 text-lg font-semibold">DialogTrigger</h4>
|
<h4 className="mb-3 text-lg font-semibold">DialogTrigger</h4>
|
||||||
<p className="text-muted-foreground mb-3 text-sm">
|
<p className="text-muted-foreground mb-3 text-sm">
|
||||||
The element that opens the dialog. Use asChild prop to render as
|
The element that opens the dialog. Use the render prop to compose
|
||||||
child element.
|
with a custom element.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -840,10 +822,8 @@ export function DialogStory() {
|
|||||||
<h4 className="text-sm font-semibold">Focus Management</h4>
|
<h4 className="text-sm font-semibold">Focus Management</h4>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
• Focus moves to dialog when opened
|
• Focus moves to dialog when opened
|
||||||
<br />
|
<br />• Focus returns to trigger when closed
|
||||||
• Focus returns to trigger when closed
|
<br />• Tab navigation stays within dialog
|
||||||
<br />
|
|
||||||
• Tab navigation stays within dialog
|
|
||||||
<br />• Escape key closes the dialog
|
<br />• Escape key closes the dialog
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { Code2, FileText, Search } from 'lucide-react';
|
import { Code2, FileText, Search } from 'lucide-react';
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ export function DocsSidebar({
|
|||||||
selectedCategory,
|
selectedCategory,
|
||||||
}: DocsSidebarProps) {
|
}: DocsSidebarProps) {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const filteredComponents = COMPONENTS_REGISTRY.filter((c) =>
|
const filteredComponents = COMPONENTS_REGISTRY.filter((c) =>
|
||||||
@@ -50,21 +51,21 @@ export function DocsSidebar({
|
|||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
const onCategorySelect = (category: string | null) => {
|
const onCategorySelect = (category: string | null) => {
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const sp = new URLSearchParams(searchParams);
|
||||||
searchParams.set('category', category || '');
|
sp.set('category', category || '');
|
||||||
router.push(`/components?${searchParams.toString()}`);
|
router.push(`/components?${sp.toString()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onComponentSelect = (component: ComponentInfo) => {
|
const onComponentSelect = (component: ComponentInfo) => {
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const sp = new URLSearchParams(searchParams);
|
||||||
searchParams.set('component', component.name);
|
sp.set('component', component.name);
|
||||||
router.push(`/components?${searchParams.toString()}`);
|
router.push(`/components?${sp.toString()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-muted/30 flex h-screen w-80 flex-col overflow-hidden border-r">
|
<div className="bg-muted/30 flex h-screen w-80 flex-col overflow-hidden border-r">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex-shrink-0 border-b p-4">
|
<div className="shrink-0 border-b p-4">
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
<Code2 className="text-primary h-6 w-6" />
|
<Code2 className="text-primary h-6 w-6" />
|
||||||
|
|
||||||
@@ -77,13 +78,14 @@ export function DocsSidebar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Controls */}
|
{/* 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 */}
|
{/* Category Select */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Select
|
<Select
|
||||||
value={selectedCategory || 'all'}
|
defaultValue={selectedCategory || 'all'}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const category = value === 'all' ? null : value;
|
const category = value === 'all' ? null : value;
|
||||||
|
|
||||||
onCategorySelect(category);
|
onCategorySelect(category);
|
||||||
|
|
||||||
// Select first component in the filtered results
|
// Select first component in the filtered results
|
||||||
@@ -96,8 +98,12 @@ export function DocsSidebar({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder={'Select a category'} />
|
<SelectValue>
|
||||||
|
{(category) => {
|
||||||
|
return category === 'all' ? 'All Categories' : category;
|
||||||
|
}}
|
||||||
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -154,7 +160,7 @@ export function DocsSidebar({
|
|||||||
|
|
||||||
{/* Components List - Scrollable */}
|
{/* Components List - Scrollable */}
|
||||||
<div className="flex flex-1 flex-col overflow-y-auto">
|
<div className="flex flex-1 flex-col overflow-y-auto">
|
||||||
<div className="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">
|
<h3 className="flex items-center gap-2 text-sm font-semibold">
|
||||||
<FileText className="h-4 w-4" />
|
<FileText className="h-4 w-4" />
|
||||||
Components
|
Components
|
||||||
|
|||||||
@@ -101,13 +101,18 @@ const examples = [
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-32 items-center justify-center">
|
<div className="flex min-h-32 items-center justify-center">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger
|
||||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
render={
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="relative h-8 w-8 rounded-full"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="h-8 w-8">
|
||||||
<AvatarImage src="/avatars/01.png" alt="@username" />
|
<AvatarImage src="/avatars/01.png" alt="@username" />
|
||||||
<AvatarFallback>JD</AvatarFallback>
|
<AvatarFallback>JD</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
@@ -185,11 +190,11 @@ const examples = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
render={<Button variant="ghost" className="h-8 w-8 p-0" />}
|
||||||
|
>
|
||||||
<span className="sr-only">Open menu</span>
|
<span className="sr-only">Open menu</span>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-48">
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
<DropdownMenuItem onClick={() => setSelectedAction('open')}>
|
<DropdownMenuItem onClick={() => setSelectedAction('open')}>
|
||||||
@@ -275,11 +280,9 @@ const examples = [
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-48 items-center justify-center">
|
<div className="flex min-h-48 items-center justify-center">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Create New
|
Create New
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56">
|
<DropdownMenuContent className="w-56">
|
||||||
<DropdownMenuLabel>Create Content</DropdownMenuLabel>
|
<DropdownMenuLabel>Create Content</DropdownMenuLabel>
|
||||||
@@ -393,11 +396,11 @@ const examples = [
|
|||||||
<span className="text-sm">Appearance & Layout</span>
|
<span className="text-sm">Appearance & Layout</span>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger
|
||||||
<Button variant="outline" size="sm">
|
render={<Button variant="outline" size="sm" />}
|
||||||
|
>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
Configure
|
Configure
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-64" align="end">
|
<DropdownMenuContent className="w-64" align="end">
|
||||||
<DropdownMenuLabel>View Options</DropdownMenuLabel>
|
<DropdownMenuLabel>View Options</DropdownMenuLabel>
|
||||||
@@ -547,10 +550,10 @@ const examples = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
render={<Button variant="ghost" className="h-8 w-8 p-0" />}
|
||||||
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-48">
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -863,7 +866,7 @@ export default function DropdownMenuStory() {
|
|||||||
modal: controls.modal ? true : undefined,
|
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}`;
|
return `${importStatement}\n${buttonImport}\n${iconImport}\n\n${dropdownStructure}`;
|
||||||
};
|
};
|
||||||
@@ -971,8 +974,8 @@ export default function DropdownMenuStory() {
|
|||||||
const previewContent = (
|
const previewContent = (
|
||||||
<div className="flex justify-center p-6">
|
<div className="flex justify-center p-6">
|
||||||
<DropdownMenu modal={controls.modal}>
|
<DropdownMenu modal={controls.modal}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">Open Menu</Button>
|
Open Menu
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
side={controls.side}
|
side={controls.side}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
@@ -119,7 +119,7 @@ export default function FormStory() {
|
|||||||
const formImport = generateImportStatement(formComponents, '@kit/ui/form');
|
const formImport = generateImportStatement(formComponents, '@kit/ui/form');
|
||||||
const inputImport = generateImportStatement(['Input'], '@kit/ui/input');
|
const inputImport = generateImportStatement(['Input'], '@kit/ui/input');
|
||||||
const buttonImport = generateImportStatement(['Button'], '@kit/ui/button');
|
const buttonImport = generateImportStatement(['Button'], '@kit/ui/button');
|
||||||
const hookFormImports = `import { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { 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 schemaCode = '';
|
||||||
let formFieldsCode = '';
|
let formFieldsCode = '';
|
||||||
@@ -130,19 +130,19 @@ export default function FormStory() {
|
|||||||
|
|
||||||
formFieldsCode = ` <FormField\n control={form.control}\n name="username"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Username</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="Enter username" {...field} />\n </FormControl>${controls.showDescriptions ? '\n <FormDescription>\n Your public display name.\n </FormDescription>' : ''}${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="email"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Email</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="email" placeholder="Enter email" {...field} />\n </FormControl>${controls.showDescriptions ? "\n <FormDescription>\n We'll never share your email.\n </FormDescription>" : ''}${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
|
formFieldsCode = ` <FormField\n control={form.control}\n name="username"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Username</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="Enter username" {...field} />\n </FormControl>${controls.showDescriptions ? '\n <FormDescription>\n Your public display name.\n </FormDescription>' : ''}${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="email"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Email</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="email" placeholder="Enter email" {...field} />\n </FormControl>${controls.showDescriptions ? "\n <FormDescription>\n We'll never share your email.\n </FormDescription>" : ''}${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
|
||||||
|
|
||||||
onSubmitCode = ` function onSubmit(values: z.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') {
|
} else if (controls.formType === 'advanced') {
|
||||||
schemaCode = `const formSchema = z.object({\n firstName: z.string().min(1, 'First name is required.'),\n lastName: z.string().min(1, 'Last name is required.'),\n email: z.string().email('Please enter a valid email address.'),\n});`;
|
schemaCode = `const formSchema = z.object({\n firstName: z.string().min(1, 'First name is required.'),\n lastName: z.string().min(1, 'Last name is required.'),\n email: z.string().email('Please enter a valid email address.'),\n});`;
|
||||||
|
|
||||||
formFieldsCode = ` <FormField\n control={form.control}\n name="firstName"\n render={({ field }) => (\n <FormItem>\n <FormLabel>First Name</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="John" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="lastName"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Last Name</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="Doe" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
|
formFieldsCode = ` <FormField\n control={form.control}\n name="firstName"\n render={({ field }) => (\n <FormItem>\n <FormLabel>First Name</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="John" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="lastName"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Last Name</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="Doe" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
|
||||||
|
|
||||||
onSubmitCode = ` function onSubmit(values: z.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 {
|
} else {
|
||||||
schemaCode = `const formSchema = z.object({\n password: z.string().min(8, 'Password must be at least 8 characters.'),\n confirmPassword: z.string(),\n}).refine((data) => data.password === data.confirmPassword, {\n message: 'Passwords do not match.',\n path: ['confirmPassword'],\n});`;
|
schemaCode = `const formSchema = z.object({\n password: z.string().min(8, 'Password must be at least 8 characters.'),\n confirmPassword: z.string(),\n}).refine((data) => data.password === data.confirmPassword, {\n message: 'Passwords do not match.',\n path: ['confirmPassword'],\n});`;
|
||||||
|
|
||||||
formFieldsCode = ` <FormField\n control={form.control}\n name="password"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Password</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="password" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="confirmPassword"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Confirm Password</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="password" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
|
formFieldsCode = ` <FormField\n control={form.control}\n name="password"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Password</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="password" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="confirmPassword"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Confirm Password</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="password" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
|
||||||
|
|
||||||
onSubmitCode = ` function onSubmit(values: z.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 =
|
const defaultValuesCode =
|
||||||
@@ -152,13 +152,13 @@ export default function FormStory() {
|
|||||||
? ` defaultValues: {\n firstName: '',\n lastName: '',\n email: '',\n },`
|
? ` defaultValues: {\n firstName: '',\n lastName: '',\n email: '',\n },`
|
||||||
: ` defaultValues: {\n password: '',\n confirmPassword: '',\n },`;
|
: ` defaultValues: {\n password: '',\n confirmPassword: '',\n },`;
|
||||||
|
|
||||||
const fullFormCode = `${hookFormImports}\n${formImport}\n${inputImport}\n${buttonImport}\n\n${schemaCode}\n\nfunction MyForm() {\n const form = useForm<z.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;
|
return fullFormCode;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Basic form
|
// Basic form
|
||||||
const basicForm = useForm<z.infer<typeof basicFormSchema>>({
|
const basicForm = useForm<z.output<typeof basicFormSchema>>({
|
||||||
resolver: zodResolver(basicFormSchema),
|
resolver: zodResolver(basicFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: '',
|
username: '',
|
||||||
@@ -169,7 +169,7 @@ export default function FormStory() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Advanced form
|
// Advanced form
|
||||||
const advancedForm = useForm<z.infer<typeof advancedFormSchema>>({
|
const advancedForm = useForm<z.output<typeof advancedFormSchema>>({
|
||||||
resolver: zodResolver(advancedFormSchema),
|
resolver: zodResolver(advancedFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
firstName: '',
|
firstName: '',
|
||||||
@@ -183,7 +183,7 @@ export default function FormStory() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Validation form
|
// Validation form
|
||||||
const validationForm = useForm<z.infer<typeof validationFormSchema>>({
|
const validationForm = useForm<z.output<typeof validationFormSchema>>({
|
||||||
resolver: zodResolver(validationFormSchema),
|
resolver: zodResolver(validationFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
password: '',
|
password: '',
|
||||||
@@ -1056,7 +1056,7 @@ export default function FormStory() {
|
|||||||
<pre className="overflow-x-auto text-sm">
|
<pre className="overflow-x-auto text-sm">
|
||||||
{`import { useForm } from 'react-hook-form';
|
{`import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@kit/ui/form';
|
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@kit/ui/form';
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
@@ -1065,7 +1065,7 @@ const formSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function MyForm() {
|
function MyForm() {
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.output<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: '',
|
username: '',
|
||||||
@@ -1073,7 +1073,7 @@ function MyForm() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
function onSubmit(values: z.output<typeof formSchema>) {
|
||||||
console.log(values);
|
console.log(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export function KbdStory() {
|
|||||||
let snippet = groupLines.join('\n');
|
let snippet = groupLines.join('\n');
|
||||||
|
|
||||||
if (controls.showTooltip) {
|
if (controls.showTooltip) {
|
||||||
snippet = `<TooltipProvider>\n <Tooltip>\n <TooltipTrigger 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, [
|
return formatCodeBlock(snippet, [
|
||||||
@@ -115,11 +115,11 @@ export function KbdStory() {
|
|||||||
{controls.showTooltip ? (
|
{controls.showTooltip ? (
|
||||||
<TooltipProvider delayDuration={200}>
|
<TooltipProvider delayDuration={200}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger
|
||||||
<Button variant="outline" className="gap-2">
|
render={<Button variant="outline" className="gap-2" />}
|
||||||
|
>
|
||||||
<Command className="h-4 w-4" />
|
<Command className="h-4 w-4" />
|
||||||
Command palette
|
Command palette
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="flex items-center gap-2">
|
<TooltipContent className="flex items-center gap-2">
|
||||||
<span>Press</span>
|
<span>Press</span>
|
||||||
|
|||||||
@@ -763,10 +763,8 @@ export function SelectStory() {
|
|||||||
<h4 className="text-sm font-semibold">Keyboard Navigation</h4>
|
<h4 className="text-sm font-semibold">Keyboard Navigation</h4>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
• Space/Enter opens the select
|
• Space/Enter opens the select
|
||||||
<br />
|
<br />• Arrow keys navigate options
|
||||||
• Arrow keys navigate options
|
<br />• Escape closes the dropdown
|
||||||
<br />
|
|
||||||
• Escape closes the dropdown
|
|
||||||
<br />• Type to search/filter options
|
<br />• Type to search/filter options
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -136,11 +136,13 @@ export function SimpleDataTableStory() {
|
|||||||
{controls.showActions && (
|
{controls.showActions && (
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
render={
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0" />
|
||||||
|
}
|
||||||
|
>
|
||||||
<span className="sr-only">Open menu</span>
|
<span className="sr-only">Open menu</span>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export function SwitchStory() {
|
|||||||
className: cn(
|
className: cn(
|
||||||
controls.size === 'sm' && 'h-4 w-7',
|
controls.size === 'sm' && 'h-4 w-7',
|
||||||
controls.size === 'lg' && 'h-6 w-11',
|
controls.size === 'lg' && 'h-6 w-11',
|
||||||
controls.error && 'data-[state=checked]:bg-destructive',
|
controls.error && 'data-checked:bg-destructive',
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ export function SwitchStory() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
controls.size === 'sm' && 'h-4 w-7',
|
controls.size === 'sm' && 'h-4 w-7',
|
||||||
controls.size === 'lg' && 'h-6 w-11',
|
controls.size === 'lg' && 'h-6 w-11',
|
||||||
controls.error && 'data-[state=checked]:bg-destructive',
|
controls.error && 'data-checked:bg-destructive',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -616,7 +616,7 @@ export function SwitchStory() {
|
|||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch
|
||||||
id="error-switch"
|
id="error-switch"
|
||||||
className="data-[state=checked]:bg-destructive"
|
className="data-checked:bg-destructive"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-destructive text-sm">
|
<p className="text-destructive text-sm">
|
||||||
@@ -642,7 +642,7 @@ export function SwitchStory() {
|
|||||||
<div>
|
<div>
|
||||||
<h4 className="mb-3 text-lg font-semibold">Switch</h4>
|
<h4 className="mb-3 text-lg font-semibold">Switch</h4>
|
||||||
<p className="text-muted-foreground mb-3 text-sm">
|
<p className="text-muted-foreground mb-3 text-sm">
|
||||||
A toggle switch component for boolean states. Built on Radix UI
|
A toggle switch component for boolean states. Built on Base UI
|
||||||
Switch primitive.
|
Switch primitive.
|
||||||
</p>
|
</p>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -792,8 +792,7 @@ export function SwitchStory() {
|
|||||||
<h4 className="text-sm font-semibold">Keyboard Navigation</h4>
|
<h4 className="text-sm font-semibold">Keyboard Navigation</h4>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
• Tab to focus the switch
|
• Tab to focus the switch
|
||||||
<br />
|
<br />• Space or Enter to toggle state
|
||||||
• Space or Enter to toggle state
|
|
||||||
<br />• Arrow keys when part of a radio group
|
<br />• Arrow keys when part of a radio group
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,9 +62,9 @@ interface TabsControlsProps {
|
|||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
default: '',
|
default: '',
|
||||||
pills:
|
pills:
|
||||||
'[&>div]:bg-background [&>div]:border [&>div]:rounded-lg [&>div]:p-1 [&_button]:rounded-md [&_button[data-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:
|
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 = {
|
const sizeClasses = {
|
||||||
@@ -683,28 +683,28 @@ function App() {
|
|||||||
<TabsList className="h-auto rounded-none border-b bg-transparent p-0">
|
<TabsList className="h-auto rounded-none border-b bg-transparent p-0">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="overview"
|
value="overview"
|
||||||
className="data-[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" />
|
<BarChart3 className="mr-2 h-4 w-4" />
|
||||||
Overview
|
Overview
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="users"
|
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" />
|
<User className="mr-2 h-4 w-4" />
|
||||||
Users
|
Users
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="revenue"
|
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" />
|
<CreditCard className="mr-2 h-4 w-4" />
|
||||||
Revenue
|
Revenue
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="reports"
|
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" />
|
<FileText className="mr-2 h-4 w-4" />
|
||||||
Reports
|
Reports
|
||||||
@@ -905,8 +905,7 @@ const apiReference = {
|
|||||||
{
|
{
|
||||||
name: '...props',
|
name: '...props',
|
||||||
type: 'React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>',
|
type: 'React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>',
|
||||||
description:
|
description: 'All additional props from Base UI Tabs.Root component.',
|
||||||
'All props from Radix UI Tabs.Root component including asChild, id, etc.',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
examples: [
|
examples: [
|
||||||
|
|||||||
@@ -144,22 +144,23 @@ function TooltipStory() {
|
|||||||
|
|
||||||
let code = `<TooltipProvider${providerPropsString}>\n`;
|
let code = `<TooltipProvider${providerPropsString}>\n`;
|
||||||
code += ` <Tooltip>\n`;
|
code += ` <Tooltip>\n`;
|
||||||
code += ` <TooltipTrigger asChild>\n`;
|
|
||||||
|
|
||||||
if (controls.triggerType === 'button') {
|
if (controls.triggerType === 'button') {
|
||||||
code += ` <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') {
|
} else if (controls.triggerType === 'icon') {
|
||||||
code += ` <Button variant="${controls.triggerVariant}" size="icon">\n`;
|
|
||||||
const iconName = selectedIconData?.icon.name || 'Info';
|
const iconName = selectedIconData?.icon.name || 'Info';
|
||||||
|
code += ` <TooltipTrigger render={<Button variant="${controls.triggerVariant}" size="icon" />}>\n`;
|
||||||
code += ` <${iconName} className="h-4 w-4" />\n`;
|
code += ` <${iconName} className="h-4 w-4" />\n`;
|
||||||
code += ` </Button>\n`;
|
|
||||||
} else if (controls.triggerType === 'text') {
|
} else if (controls.triggerType === 'text') {
|
||||||
code += ` <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') {
|
} else if (controls.triggerType === 'input') {
|
||||||
code += ` <Input placeholder="Hover over this input" />\n`;
|
code += ` <TooltipTrigger render={<Input placeholder="Hover over this input" />} />\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (controls.triggerType !== 'input') {
|
||||||
code += ` </TooltipTrigger>\n`;
|
code += ` </TooltipTrigger>\n`;
|
||||||
|
}
|
||||||
code += ` <TooltipContent${contentPropsString}>\n`;
|
code += ` <TooltipContent${contentPropsString}>\n`;
|
||||||
code += ` <p>${controls.content}</p>\n`;
|
code += ` <p>${controls.content}</p>\n`;
|
||||||
code += ` </TooltipContent>\n`;
|
code += ` </TooltipContent>\n`;
|
||||||
@@ -170,28 +171,50 @@ function TooltipStory() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderPreview = () => {
|
const renderPreview = () => {
|
||||||
const trigger = (() => {
|
const renderTrigger = () => {
|
||||||
switch (controls.triggerType) {
|
switch (controls.triggerType) {
|
||||||
case 'button':
|
case 'button':
|
||||||
return <Button variant={controls.triggerVariant}>Hover me</Button>;
|
return (
|
||||||
|
<TooltipTrigger
|
||||||
|
render={<Button variant={controls.triggerVariant} />}
|
||||||
|
>
|
||||||
|
Hover me
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
case 'icon':
|
case 'icon':
|
||||||
return (
|
return (
|
||||||
<Button variant={controls.triggerVariant} size="icon">
|
<TooltipTrigger
|
||||||
|
render={<Button variant={controls.triggerVariant} size="icon" />}
|
||||||
|
>
|
||||||
<IconComponent className="h-4 w-4" />
|
<IconComponent className="h-4 w-4" />
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
case 'text':
|
case 'text':
|
||||||
return (
|
return (
|
||||||
<span className="cursor-help underline decoration-dotted">
|
<TooltipTrigger
|
||||||
|
render={
|
||||||
|
<span className="cursor-help underline decoration-dotted" />
|
||||||
|
}
|
||||||
|
>
|
||||||
Hover me
|
Hover me
|
||||||
</span>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
case 'input':
|
case 'input':
|
||||||
return <Input placeholder="Hover over this input" />;
|
return (
|
||||||
|
<TooltipTrigger
|
||||||
|
render={<Input placeholder="Hover over this input" />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <Button variant={controls.triggerVariant}>Hover me</Button>;
|
return (
|
||||||
|
<TooltipTrigger
|
||||||
|
render={<Button variant={controls.triggerVariant} />}
|
||||||
|
>
|
||||||
|
Hover me
|
||||||
|
</TooltipTrigger>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})();
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[200px] items-center justify-center">
|
<div className="flex min-h-[200px] items-center justify-center">
|
||||||
@@ -201,7 +224,7 @@ function TooltipStory() {
|
|||||||
disableHoverableContent={controls.disableHoverableContent}
|
disableHoverableContent={controls.disableHoverableContent}
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
|
{renderTrigger()}
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
side={controls.side}
|
side={controls.side}
|
||||||
align={controls.align}
|
align={controls.align}
|
||||||
@@ -376,11 +399,9 @@ function TooltipStory() {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">
|
|
||||||
<Info className="mr-2 h-4 w-4" />
|
<Info className="mr-2 h-4 w-4" />
|
||||||
Info Button
|
Info Button
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>This provides additional information</p>
|
<p>This provides additional information</p>
|
||||||
@@ -388,10 +409,8 @@ function TooltipStory() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger render={<Button variant="ghost" size="icon" />}>
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<HelpCircle className="h-4 w-4" />
|
<HelpCircle className="h-4 w-4" />
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Click for help documentation</p>
|
<p>Click for help documentation</p>
|
||||||
@@ -399,10 +418,12 @@ function TooltipStory() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger
|
||||||
<span className="cursor-help underline decoration-dotted">
|
render={
|
||||||
|
<span className="cursor-help underline decoration-dotted" />
|
||||||
|
}
|
||||||
|
>
|
||||||
Hover for explanation
|
Hover for explanation
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>This term needs clarification for better understanding</p>
|
<p>This term needs clarification for better understanding</p>
|
||||||
@@ -410,9 +431,9 @@ function TooltipStory() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger
|
||||||
<Input placeholder="Hover me" className="w-48" />
|
render={<Input placeholder="Hover me" className="w-48" />}
|
||||||
</TooltipTrigger>
|
/>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Enter your email address here</p>
|
<p>Enter your email address here</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@@ -434,10 +455,10 @@ function TooltipStory() {
|
|||||||
{/* Top Row */}
|
{/* Top Row */}
|
||||||
<div></div>
|
<div></div>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger
|
||||||
<Button variant="outline" size="sm">
|
render={<Button variant="outline" size="sm" />}
|
||||||
|
>
|
||||||
Top
|
Top
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top">
|
<TooltipContent side="top">
|
||||||
<p>Tooltip on top</p>
|
<p>Tooltip on top</p>
|
||||||
@@ -447,10 +468,10 @@ function TooltipStory() {
|
|||||||
|
|
||||||
{/* Middle Row */}
|
{/* Middle Row */}
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger
|
||||||
<Button variant="outline" size="sm">
|
render={<Button variant="outline" size="sm" />}
|
||||||
|
>
|
||||||
Left
|
Left
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="left">
|
<TooltipContent side="left">
|
||||||
<p>Tooltip on left</p>
|
<p>Tooltip on left</p>
|
||||||
@@ -460,10 +481,10 @@ function TooltipStory() {
|
|||||||
<span className="text-muted-foreground text-sm">Center</span>
|
<span className="text-muted-foreground text-sm">Center</span>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger
|
||||||
<Button variant="outline" size="sm">
|
render={<Button variant="outline" size="sm" />}
|
||||||
|
>
|
||||||
Right
|
Right
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">
|
<TooltipContent side="right">
|
||||||
<p>Tooltip on right</p>
|
<p>Tooltip on right</p>
|
||||||
@@ -473,10 +494,10 @@ function TooltipStory() {
|
|||||||
{/* Bottom Row */}
|
{/* Bottom Row */}
|
||||||
<div></div>
|
<div></div>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger
|
||||||
<Button variant="outline" size="sm">
|
render={<Button variant="outline" size="sm" />}
|
||||||
|
>
|
||||||
Bottom
|
Bottom
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom">
|
<TooltipContent side="bottom">
|
||||||
<p>Tooltip on bottom</p>
|
<p>Tooltip on bottom</p>
|
||||||
@@ -498,11 +519,9 @@ function TooltipStory() {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">
|
|
||||||
<Star className="mr-2 h-4 w-4" />
|
<Star className="mr-2 h-4 w-4" />
|
||||||
Premium Feature
|
Premium Feature
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-xs">
|
<TooltipContent className="max-w-xs">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -516,11 +535,9 @@ function TooltipStory() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger render={<Button variant="outline" />}>
|
||||||
<Button variant="outline">
|
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
Advanced Settings
|
Advanced Settings
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -537,11 +554,9 @@ function TooltipStory() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger render={<Button variant="destructive" />}>
|
||||||
<Button variant="destructive">
|
|
||||||
<AlertCircle className="mr-2 h-4 w-4" />
|
<AlertCircle className="mr-2 h-4 w-4" />
|
||||||
Delete Account
|
Delete Account
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="border-destructive bg-destructive text-destructive-foreground max-w-xs">
|
<TooltipContent className="border-destructive bg-destructive text-destructive-foreground max-w-xs">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -568,10 +583,10 @@ function TooltipStory() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger
|
||||||
<Button size="icon" variant="ghost">
|
render={<Button size="icon" variant="ghost" />}
|
||||||
|
>
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Copy to clipboard</p>
|
<p>Copy to clipboard</p>
|
||||||
@@ -579,10 +594,10 @@ function TooltipStory() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger
|
||||||
<Button size="icon" variant="ghost">
|
render={<Button size="icon" variant="ghost" />}
|
||||||
|
>
|
||||||
<Download className="h-4 w-4" />
|
<Download className="h-4 w-4" />
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Download file</p>
|
<p>Download file</p>
|
||||||
@@ -590,10 +605,10 @@ function TooltipStory() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger
|
||||||
<Button size="icon" variant="ghost">
|
render={<Button size="icon" variant="ghost" />}
|
||||||
|
>
|
||||||
<Share className="h-4 w-4" />
|
<Share className="h-4 w-4" />
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Share with others</p>
|
<p>Share with others</p>
|
||||||
@@ -605,9 +620,11 @@ function TooltipStory() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="username">Username</Label>
|
<Label htmlFor="username">Username</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger
|
||||||
|
render={
|
||||||
<Input id="username" placeholder="Enter username" />
|
<Input id="username" placeholder="Enter username" />
|
||||||
</TooltipTrigger>
|
}
|
||||||
|
/>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Must be 3-20 characters, letters and numbers only</p>
|
<p>Must be 3-20 characters, letters and numbers only</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@@ -616,9 +633,7 @@ function TooltipStory() {
|
|||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger render={<Checkbox id="terms" />} />
|
||||||
<Checkbox id="terms" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="max-w-xs">
|
<TooltipContent className="max-w-xs">
|
||||||
<p>
|
<p>
|
||||||
By checking this, you agree to our Terms of Service and
|
By checking this, you agree to our Terms of Service and
|
||||||
@@ -751,7 +766,7 @@ function TooltipStory() {
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>TooltipTrigger:</strong> Element that triggers the
|
<strong>TooltipTrigger:</strong> Element that triggers the
|
||||||
tooltip (use asChild prop)
|
tooltip (use render prop)
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -856,8 +871,7 @@ function TooltipStory() {
|
|||||||
<h4 className="text-sm font-semibold">Keyboard Support</h4>
|
<h4 className="text-sm font-semibold">Keyboard Support</h4>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
• Tooltips appear on focus and disappear on blur
|
• Tooltips appear on focus and disappear on blur
|
||||||
<br />
|
<br />• Escape key dismisses tooltips
|
||||||
• Escape key dismisses tooltips
|
|
||||||
<br />• Tooltips don't trap focus or interfere with navigation
|
<br />• Tooltips don't trap focus or interfere with navigation
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -492,7 +492,7 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
|
|||||||
status: 'stable',
|
status: 'stable',
|
||||||
component: CardButtonStory,
|
component: CardButtonStory,
|
||||||
sourceFile: '@kit/ui/card-button',
|
sourceFile: '@kit/ui/card-button',
|
||||||
props: ['asChild', 'className', 'children', 'onClick', 'disabled'],
|
props: ['className', 'children', 'onClick', 'disabled'],
|
||||||
icon: MousePointer,
|
icon: MousePointer,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -950,7 +950,7 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
|
|||||||
status: 'stable',
|
status: 'stable',
|
||||||
component: ItemStory,
|
component: ItemStory,
|
||||||
sourceFile: '@kit/ui/item',
|
sourceFile: '@kit/ui/item',
|
||||||
props: ['variant', 'size', 'asChild', 'className'],
|
props: ['variant', 'size', 'className'],
|
||||||
icon: Layers,
|
icon: Layers,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1004,7 +1004,7 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
|
|||||||
status: 'stable',
|
status: 'stable',
|
||||||
component: BreadcrumbStory,
|
component: BreadcrumbStory,
|
||||||
sourceFile: '@kit/ui/breadcrumb',
|
sourceFile: '@kit/ui/breadcrumb',
|
||||||
props: ['separator', 'asChild', 'href', 'className'],
|
props: ['separator', 'href', 'className'],
|
||||||
icon: ChevronRight,
|
icon: ChevronRight,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { withI18n } from '../../lib/i18n/with-i18n';
|
|
||||||
import { DocsContent } from './components/docs-content';
|
import { DocsContent } from './components/docs-content';
|
||||||
import { DocsHeader } from './components/docs-header';
|
import { DocsHeader } from './components/docs-header';
|
||||||
import { DocsSidebar } from './components/docs-sidebar';
|
import { DocsSidebar } from './components/docs-sidebar';
|
||||||
@@ -29,4 +28,4 @@ async function ComponentDocsPage(props: ComponentDocsPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n(ComponentDocsPage);
|
export default ComponentDocsPage;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import 'server-only';
|
import 'server-only';
|
||||||
|
import { DatabaseTool } from '@kit/mcp-server/database';
|
||||||
|
|
||||||
import { relative } from 'path';
|
import { relative } from 'path';
|
||||||
|
|
||||||
import { DatabaseTool } from '@kit/mcp-server/database';
|
|
||||||
|
|
||||||
export interface DatabaseTable {
|
export interface DatabaseTable {
|
||||||
name: string;
|
name: string;
|
||||||
schema: string;
|
schema: string;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { relative } from 'path';
|
|
||||||
|
|
||||||
import { DatabaseTool } from '@kit/mcp-server/database';
|
import { DatabaseTool } from '@kit/mcp-server/database';
|
||||||
|
|
||||||
|
import { relative } from 'path';
|
||||||
|
|
||||||
export async function getTableDetailsAction(
|
export async function getTableDetailsAction(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
schema = 'public',
|
schema = 'public',
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { EmailTesterFormSchema } from '@/app/emails/lib/email-tester-form-schema';
|
|
||||||
import { sendEmailAction } from '@/app/emails/lib/server-actions';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
@@ -19,6 +17,9 @@ import { Input } from '@kit/ui/input';
|
|||||||
import { toast } from '@kit/ui/sonner';
|
import { toast } from '@kit/ui/sonner';
|
||||||
import { Switch } from '@kit/ui/switch';
|
import { Switch } from '@kit/ui/switch';
|
||||||
|
|
||||||
|
import { EmailTesterFormSchema } from '@/app/emails/lib/email-tester-form-schema';
|
||||||
|
import { sendEmailAction } from '@/app/emails/lib/server-actions';
|
||||||
|
|
||||||
export function EmailTesterForm(props: {
|
export function EmailTesterForm(props: {
|
||||||
template: string;
|
template: string;
|
||||||
settings: {
|
settings: {
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
import { EmailTesterForm } from '@/app/emails/[id]/components/email-tester-form';
|
|
||||||
import { EnvModeSelector } from '@/components/env-mode-selector';
|
|
||||||
import { IFrame } from '@/components/iframe';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createKitEmailsDeps,
|
createKitEmailsDeps,
|
||||||
createKitEmailsService,
|
createKitEmailsService,
|
||||||
@@ -17,6 +13,10 @@ import {
|
|||||||
} from '@kit/ui/dialog';
|
} from '@kit/ui/dialog';
|
||||||
import { Page, PageBody, PageHeader } from '@kit/ui/page';
|
import { Page, PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
|
import { EmailTesterForm } from '@/app/emails/[id]/components/email-tester-form';
|
||||||
|
import { EnvModeSelector } from '@/components/env-mode-selector';
|
||||||
|
import { IFrame } from '@/components/iframe';
|
||||||
|
|
||||||
type EnvMode = 'development' | 'production';
|
type EnvMode = 'development' | 'production';
|
||||||
|
|
||||||
type EmailPageProps = React.PropsWithChildren<{
|
type EmailPageProps = React.PropsWithChildren<{
|
||||||
@@ -67,10 +67,10 @@ export default async function EmailPage(props: EmailPageProps) {
|
|||||||
Remember that the below is an approximation of the email. Always test
|
Remember that the below is an approximation of the email. Always test
|
||||||
it in your inbox.{' '}
|
it in your inbox.{' '}
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger
|
||||||
<Button variant={'link'} className="p-0 underline">
|
render={<Button variant={'link'} className="p-0 underline" />}
|
||||||
|
>
|
||||||
Test Email
|
Test Email
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
export const EmailTesterFormSchema = z.object({
|
export const EmailTesterFormSchema = z.object({
|
||||||
username: z.string().min(1),
|
username: z.string().min(1),
|
||||||
|
|||||||
@@ -49,13 +49,16 @@ export default async function EmailsPage() {
|
|||||||
|
|
||||||
<div className={'grid grid-cols-1 gap-4 md:grid-cols-4'}>
|
<div className={'grid grid-cols-1 gap-4 md:grid-cols-4'}>
|
||||||
{categoryTemplates.map((template) => (
|
{categoryTemplates.map((template) => (
|
||||||
<CardButton key={template.id} asChild>
|
<CardButton
|
||||||
|
key={template.id}
|
||||||
|
render={
|
||||||
<Link href={`/emails/${template.id}`}>
|
<Link href={`/emails/${template.id}`}>
|
||||||
<CardButtonHeader>
|
<CardButtonHeader>
|
||||||
<CardButtonTitle>{template.name}</CardButtonTitle>
|
<CardButtonTitle>{template.name}</CardButtonTitle>
|
||||||
</CardButtonHeader>
|
</CardButtonHeader>
|
||||||
</Link>
|
</Link>
|
||||||
</CardButton>
|
}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { getMessages } from 'next-intl/server';
|
||||||
|
|
||||||
import { DevToolLayout } from '@/components/app-layout';
|
import { DevToolLayout } from '@/components/app-layout';
|
||||||
import { RootProviders } from '@/components/root-providers';
|
import { RootProviders } from '@/components/root-providers';
|
||||||
|
|
||||||
import '../styles/globals.css';
|
import '../styles/globals.css';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -10,15 +11,17 @@ export const metadata: Metadata = {
|
|||||||
description: 'The dev tool for Makerkit',
|
description: 'The dev tool for Makerkit',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
const messages = await getMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>
|
<body>
|
||||||
<RootProviders>
|
<RootProviders messages={messages}>
|
||||||
<DevToolLayout>{children}</DevToolLayout>
|
<DevToolLayout>{children}</DevToolLayout>
|
||||||
</RootProviders>
|
</RootProviders>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { execFile } from 'node:child_process';
|
|
||||||
import { access, readFile } from 'node:fs/promises';
|
|
||||||
import { join } from 'node:path';
|
|
||||||
import { promisify } from 'node:util';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type KitPrerequisitesDeps,
|
type KitPrerequisitesDeps,
|
||||||
createKitPrerequisitesService,
|
createKitPrerequisitesService,
|
||||||
} from '@kit/mcp-server/prerequisites';
|
} from '@kit/mcp-server/prerequisites';
|
||||||
|
|
||||||
|
import { execFile } from 'node:child_process';
|
||||||
|
import { access, readFile } from 'node:fs/promises';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
export async function loadDashboardKitPrerequisites() {
|
export async function loadDashboardKitPrerequisites() {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
|
import {
|
||||||
|
type KitStatusDeps,
|
||||||
|
createKitStatusService,
|
||||||
|
} from '@kit/mcp-server/status';
|
||||||
|
|
||||||
import { execFile } from 'node:child_process';
|
import { execFile } from 'node:child_process';
|
||||||
import { access, readFile, stat } from 'node:fs/promises';
|
import { access, readFile, stat } from 'node:fs/promises';
|
||||||
import { Socket } from 'node:net';
|
import { Socket } from 'node:net';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
|
|
||||||
import {
|
|
||||||
type KitStatusDeps,
|
|
||||||
createKitStatusService,
|
|
||||||
} from '@kit/mcp-server/status';
|
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
export async function loadDashboardKitStatus() {
|
export async function loadDashboardKitStatus() {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ServiceCard } from '@/components/status-tile';
|
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
||||||
import { Page, PageBody, PageHeader } from '@kit/ui/page';
|
import { Page, PageBody, PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
import { loadDashboardKitPrerequisites } from './lib/prerequisites-dashboard.loader';
|
import { loadDashboardKitPrerequisites } from './lib/prerequisites-dashboard.loader';
|
||||||
import { loadDashboardKitStatus } from './lib/status-dashboard.loader';
|
import { loadDashboardKitStatus } from './lib/status-dashboard.loader';
|
||||||
|
|
||||||
|
import { ServiceCard } from '@/components/status-tile';
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
export default async function DashboardPage() {
|
||||||
const [status, prerequisites] = await Promise.all([
|
const [status, prerequisites] = await Promise.all([
|
||||||
loadDashboardKitStatus(),
|
loadDashboardKitStatus(),
|
||||||
@@ -37,7 +37,6 @@ export default async function DashboardPage() {
|
|||||||
return (
|
return (
|
||||||
<Page style={'custom'}>
|
<Page style={'custom'}>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
displaySidebarTrigger={false}
|
|
||||||
title={'Dev Tool'}
|
title={'Dev Tool'}
|
||||||
description={'Kit MCP status for this workspace'}
|
description={'Kit MCP status for this workspace'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import { loadPRDPageData } from '../_lib/server/prd-page.loader';
|
import { loadPRDPageData } from '../_lib/server/prd-page.loader';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
export const CreatePRDSchema = z.object({
|
export const CreatePRDSchema = z.object({
|
||||||
title: z
|
title: z
|
||||||
@@ -32,4 +32,4 @@ export const CreatePRDSchema = z.object({
|
|||||||
.min(1, 'At least one success metric is required'),
|
.min(1, 'At least one success metric is required'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreatePRDData = z.infer<typeof CreatePRDSchema>;
|
export type CreatePRDData = z.output<typeof CreatePRDSchema>;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { relative } from 'node:path';
|
|
||||||
|
|
||||||
import { PRDManager } from '@kit/mcp-server/prd-manager';
|
import { PRDManager } from '@kit/mcp-server/prd-manager';
|
||||||
|
|
||||||
|
import { relative } from 'node:path';
|
||||||
|
|
||||||
interface PRDSummary {
|
interface PRDSummary {
|
||||||
filename: string;
|
filename: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import 'server-only';
|
import 'server-only';
|
||||||
|
import { PRDManager } from '@kit/mcp-server/prd-manager';
|
||||||
|
|
||||||
import { relative } from 'node:path';
|
import { relative } from 'node:path';
|
||||||
|
|
||||||
import { PRDManager } from '@kit/mcp-server/prd-manager';
|
|
||||||
|
|
||||||
export interface CustomPhase {
|
export interface CustomPhase {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -131,12 +131,14 @@ export function TranslationsComparison({
|
|||||||
|
|
||||||
<If condition={locales.length > 1}>
|
<If condition={locales.length > 1}>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger
|
||||||
|
render={
|
||||||
<Button variant="outline" className="ml-auto">
|
<Button variant="outline" className="ml-auto">
|
||||||
Select Languages
|
Select Languages
|
||||||
<ChevronDownIcon className="ml-2 h-4 w-4" />
|
<ChevronDownIcon className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<DropdownMenuContent align="end" className="w-[200px]">
|
<DropdownMenuContent align="end" className="w-[200px]">
|
||||||
{locales.map((locale) => (
|
{locales.map((locale) => (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
import { findWorkspaceRoot } from '@kit/mcp-server/env';
|
import { findWorkspaceRoot } from '@kit/mcp-server/env';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { envVariables } from '@/app/variables/lib/env-variables-model';
|
|
||||||
import { updateEnvironmentVariableAction } from '@/app/variables/lib/server-actions';
|
|
||||||
import { EnvModeSelector } from '@/components/env-mode-selector';
|
|
||||||
import {
|
import {
|
||||||
ChevronsUpDownIcon,
|
ChevronsUpDownIcon,
|
||||||
Copy,
|
Copy,
|
||||||
@@ -44,6 +41,10 @@ import { cn } from '@kit/ui/utils';
|
|||||||
import { AppEnvState, EnvVariableState } from '../lib/types';
|
import { AppEnvState, EnvVariableState } from '../lib/types';
|
||||||
import { DynamicFormInput } from './dynamic-form-input';
|
import { DynamicFormInput } from './dynamic-form-input';
|
||||||
|
|
||||||
|
import { envVariables } from '@/app/variables/lib/env-variables-model';
|
||||||
|
import { updateEnvironmentVariableAction } from '@/app/variables/lib/server-actions';
|
||||||
|
import { EnvModeSelector } from '@/components/env-mode-selector';
|
||||||
|
|
||||||
export function AppEnvironmentVariablesManager({
|
export function AppEnvironmentVariablesManager({
|
||||||
state,
|
state,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
@@ -731,13 +732,15 @@ function FilterSwitcher(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger
|
||||||
|
render={
|
||||||
<Button variant="outline" className="font-normal">
|
<Button variant="outline" className="font-normal">
|
||||||
{buttonLabel()}
|
{buttonLabel()}
|
||||||
|
|
||||||
<ChevronsUpDownIcon className="text-muted-foreground ml-1 h-3 w-3" />
|
<ChevronsUpDownIcon className="text-muted-foreground ml-1 h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuCheckboxItem
|
<DropdownMenuCheckboxItem
|
||||||
@@ -886,7 +889,8 @@ function Summary({ appState }: { appState: AppEnvState }) {
|
|||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger
|
||||||
|
render={
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
@@ -910,14 +914,16 @@ function Summary({ appState }: { appState: AppEnvState }) {
|
|||||||
toast.promise(promise, {
|
toast.promise(promise, {
|
||||||
loading: 'Copying environment variables...',
|
loading: 'Copying environment variables...',
|
||||||
success: 'Environment variables copied to clipboard.',
|
success: 'Environment variables copied to clipboard.',
|
||||||
error: 'Failed to copy environment variables to clipboard',
|
error:
|
||||||
|
'Failed to copy environment variables to clipboard',
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className={'mr-2 h-4 w-4'} />
|
<CopyIcon className={'mr-2 h-4 w-4'} />
|
||||||
<span>Copy env file to clipboard</span>
|
<span>Copy env file to clipboard</span>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
Copy environment variables to clipboard. You can place it in your
|
Copy environment variables to clipboard. You can place it in your
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
import { z } from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createKitEnvDeps,
|
createKitEnvDeps,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { use } from 'react';
|
import { use } from 'react';
|
||||||
|
|
||||||
import { EnvMode } from '@/app/variables/lib/types';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createKitEnvDeps,
|
createKitEnvDeps,
|
||||||
createKitEnvService,
|
createKitEnvService,
|
||||||
@@ -11,6 +9,8 @@ import { Page, PageBody, PageHeader } from '@kit/ui/page';
|
|||||||
|
|
||||||
import { AppEnvironmentVariablesManager } from './components/app-environment-variables-manager';
|
import { AppEnvironmentVariablesManager } from './components/app-environment-variables-manager';
|
||||||
|
|
||||||
|
import { EnvMode } from '@/app/variables/lib/types';
|
||||||
|
|
||||||
type VariablesPageProps = {
|
type VariablesPageProps = {
|
||||||
searchParams: Promise<{ mode?: EnvMode }>;
|
searchParams: Promise<{ mode?: EnvMode }>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { DevToolSidebar } from '@/components/app-sidebar';
|
import { SidebarInset, SidebarProvider } from '@kit/ui/sidebar';
|
||||||
|
|
||||||
import { SidebarInset, SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
import { DevToolSidebar } from '@/components/app-sidebar';
|
||||||
|
|
||||||
export function DevToolLayout(props: React.PropsWithChildren) {
|
export function DevToolLayout(props: React.PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<DevToolSidebar />
|
<DevToolSidebar />
|
||||||
|
|
||||||
<SidebarInset>{props.children}</SidebarInset>
|
<SidebarInset className="px-4">{props.children}</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
SidebarMenuSub,
|
SidebarMenuSub,
|
||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
} from '@kit/ui/shadcn-sidebar';
|
} from '@kit/ui/sidebar';
|
||||||
import { isRouteActive } from '@kit/ui/utils';
|
import { isRouteActive } from '@kit/ui/utils';
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -92,14 +92,14 @@ export function DevToolSidebar({
|
|||||||
{route.children.map((child) => (
|
{route.children.map((child) => (
|
||||||
<SidebarMenuSubItem key={child.path}>
|
<SidebarMenuSubItem key={child.path}>
|
||||||
<SidebarMenuSubButton
|
<SidebarMenuSubButton
|
||||||
asChild
|
render={
|
||||||
isActive={isRouteActive(child.path, pathname, false)}
|
|
||||||
>
|
|
||||||
<Link href={child.path}>
|
<Link href={child.path}>
|
||||||
<child.Icon className="h-4 w-4" />
|
<child.Icon className="h-4 w-4" />
|
||||||
<span>{child.label}</span>
|
<span>{child.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuSubButton>
|
}
|
||||||
|
isActive={isRouteActive(child.path, pathname, false)}
|
||||||
|
/>
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSubItem>
|
||||||
))}
|
))}
|
||||||
</SidebarMenuSub>
|
</SidebarMenuSub>
|
||||||
@@ -107,13 +107,13 @@ export function DevToolSidebar({
|
|||||||
) : (
|
) : (
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
isActive={isRouteActive(route.path, pathname, false)}
|
isActive={isRouteActive(route.path, pathname, false)}
|
||||||
asChild
|
render={
|
||||||
>
|
|
||||||
<Link href={route.path}>
|
<Link href={route.path}>
|
||||||
<route.Icon className="h-4 w-4" />
|
<route.Icon className="h-4 w-4" />
|
||||||
<span>{route.label}</span>
|
<span>{route.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { EnvMode } from '@/app/variables/lib/types';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -12,6 +10,8 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@kit/ui/select';
|
} from '@kit/ui/select';
|
||||||
|
|
||||||
|
import { EnvMode } from '@/app/variables/lib/types';
|
||||||
|
|
||||||
export function EnvModeSelector({ mode }: { mode: EnvMode }) {
|
export function EnvModeSelector({ mode }: { mode: EnvMode }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
export const IFrame: React.FC<
|
export const IFrame: React.FC<
|
||||||
|
|||||||
@@ -3,18 +3,18 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import type { AbstractIntlMessages } from 'next-intl';
|
||||||
|
|
||||||
import { I18nProvider } from '@kit/i18n/provider';
|
import { I18nClientProvider } from '@kit/i18n/provider';
|
||||||
import { Toaster } from '@kit/ui/sonner';
|
import { Toaster } from '@kit/ui/sonner';
|
||||||
|
|
||||||
import { i18nResolver } from '../lib/i18n/i18n.resolver';
|
export function RootProviders(
|
||||||
import { getI18nSettings } from '../lib/i18n/i18n.settings';
|
props: React.PropsWithChildren<{ messages: AbstractIntlMessages }>,
|
||||||
|
) {
|
||||||
export function RootProviders(props: React.PropsWithChildren) {
|
|
||||||
return (
|
return (
|
||||||
<I18nProvider settings={getI18nSettings('en')} resolver={i18nResolver}>
|
<I18nClientProvider locale="en" messages={props.messages}>
|
||||||
<ReactQueryProvider>{props.children}</ReactQueryProvider>
|
<ReactQueryProvider>{props.children}</ReactQueryProvider>
|
||||||
</I18nProvider>
|
</I18nClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ interface ServiceCardProps {
|
|||||||
export const ServiceCard = ({ name, status }: ServiceCardProps) => {
|
export const ServiceCard = ({ name, status }: ServiceCardProps) => {
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-2xl">
|
<Card className="w-full max-w-2xl">
|
||||||
<CardContent className="p-4">
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
|
|||||||
26
apps/dev-tool/i18n/request.ts
Normal file
26
apps/dev-tool/i18n/request.ts
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -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} />;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import type { NextConfig } from 'next';
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
transpilePackages: ['@kit/ui', '@kit/shared'],
|
transpilePackages: ['@kit/ui', '@kit/shared', '@kit/i18n'],
|
||||||
reactCompiler: true,
|
reactCompiler: true,
|
||||||
devIndicators: {
|
devIndicators: {
|
||||||
position: 'bottom-right',
|
position: 'bottom-right',
|
||||||
@@ -14,4 +18,4 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default withNextIntl(nextConfig);
|
||||||
|
|||||||
@@ -4,44 +4,41 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "git clean -xdf .next .turbo node_modules",
|
"clean": "git clean -xdf .next .turbo node_modules",
|
||||||
"dev": "next dev --port=3010 | pino-pretty -c",
|
"dev": "next dev --port=3010 | pino-pretty -c"
|
||||||
"format": "prettier --check --write \"**/*.{ts,tsx}\" --ignore-path=\"../../.prettierignore\""
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@faker-js/faker": "^10.2.0",
|
"@faker-js/faker": "catalog:",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "catalog:",
|
||||||
"@tanstack/react-query": "catalog:",
|
"@tanstack/react-query": "catalog:",
|
||||||
"lucide-react": "catalog:",
|
"lucide-react": "catalog:",
|
||||||
"next": "catalog:",
|
"next": "catalog:",
|
||||||
|
"next-intl": "catalog:",
|
||||||
"nodemailer": "catalog:",
|
"nodemailer": "catalog:",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "catalog:",
|
||||||
"rxjs": "^7.8.2"
|
"rxjs": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/email-templates": "workspace:*",
|
"@kit/email-templates": "workspace:*",
|
||||||
"@kit/i18n": "workspace:*",
|
"@kit/i18n": "workspace:*",
|
||||||
"@kit/mcp-server": "workspace:*",
|
"@kit/mcp-server": "workspace:*",
|
||||||
"@kit/next": "workspace:*",
|
"@kit/next": "workspace:*",
|
||||||
"@kit/prettier-config": "workspace:*",
|
|
||||||
"@kit/shared": "workspace:*",
|
"@kit/shared": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@tailwindcss/postcss": "^4.2.1",
|
"@tailwindcss/postcss": "catalog:",
|
||||||
"@types/node": "catalog:",
|
|
||||||
"@types/nodemailer": "catalog:",
|
"@types/nodemailer": "catalog:",
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
"@types/react-dom": "catalog:",
|
"@types/react-dom": "catalog:",
|
||||||
"babel-plugin-react-compiler": "1.0.0",
|
"babel-plugin-react-compiler": "catalog:",
|
||||||
"pino-pretty": "13.0.0",
|
"pino-pretty": "catalog:",
|
||||||
"react-hook-form": "catalog:",
|
"react-hook-form": "catalog:",
|
||||||
"recharts": "2.15.3",
|
"recharts": "catalog:",
|
||||||
"tailwindcss": "catalog:",
|
"tailwindcss": "catalog:",
|
||||||
"tw-animate-css": "catalog:",
|
"tw-animate-css": "catalog:",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "catalog:",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"prettier": "@kit/prettier-config",
|
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 1 versions",
|
"last 1 versions",
|
||||||
"> 0.7%",
|
"> 0.7%",
|
||||||
|
|||||||
@@ -66,26 +66,6 @@
|
|||||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||||
|
|
||||||
@keyframes accordion-down {
|
|
||||||
from {
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
height: var(--radix-accordion-content-height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes accordion-up {
|
|
||||||
from {
|
|
||||||
height: var(--radix-accordion-content-height);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-up {
|
@keyframes fade-up {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "web-e2e",
|
"name": "web-e2e",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"author": "Makerkit",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"report": "playwright show-report",
|
"report": "playwright show-report",
|
||||||
"test": "playwright test --max-failures=1",
|
"test": "playwright test --max-failures=1",
|
||||||
"test:fast": "playwright test --max-failures=1 --workers=16",
|
"test:fast": "playwright test --max-failures=1 --workers=16",
|
||||||
"test:setup": "playwright test tests/auth.setup.ts",
|
"test:setup": "playwright test tests/auth.setup.ts",
|
||||||
"test:ui": "playwright test --ui"
|
"test:ui": "ENABLE_BILLING_TESTS=true playwright test --ui"
|
||||||
},
|
},
|
||||||
"author": "Makerkit",
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "catalog:",
|
||||||
"@supabase/supabase-js": "catalog:",
|
"@supabase/supabase-js": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"dotenv": "catalog:",
|
||||||
"dotenv": "17.3.1",
|
"node-html-parser": "catalog:",
|
||||||
"node-html-parser": "^7.0.2",
|
"totp-generator": "catalog:"
|
||||||
"totp-generator": "^2.0.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,10 +38,12 @@ test.describe('Account Settings', () => {
|
|||||||
|
|
||||||
await Promise.all([request, response]);
|
await Promise.all([request, response]);
|
||||||
|
|
||||||
|
await page.locator('[data-test="workspace-dropdown-trigger"]').click();
|
||||||
|
|
||||||
await expect(account.getProfileName()).toHaveText(name);
|
await expect(account.getProfileName()).toHaveText(name);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('user can update their email', async ({ page }) => {
|
test('user can update their email', async () => {
|
||||||
const email = account.auth.createRandomEmail();
|
const email = account.auth.createRandomEmail();
|
||||||
|
|
||||||
await account.updateEmail(email);
|
await account.updateEmail(email);
|
||||||
|
|||||||
@@ -34,17 +34,17 @@ test.describe('Admin', () => {
|
|||||||
await page.goto('/admin');
|
await page.goto('/admin');
|
||||||
|
|
||||||
// Check all stat cards are present
|
// Check all stat cards are present
|
||||||
await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible();
|
await expect(page.getByText('Users', { exact: true })).toBeVisible();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('heading', { name: 'Team Accounts' }),
|
page.getByText('Team Accounts', { exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('heading', { name: 'Paying Customers' }),
|
page.getByText('Paying Customers', { exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Trials' })).toBeVisible();
|
await expect(page.getByText('Trials', { exact: true })).toBeVisible();
|
||||||
|
|
||||||
// Verify stat values are numbers
|
// Verify stat values are numbers
|
||||||
const stats = await page.$$('.text-3xl.font-bold');
|
const stats = await page.$$('.text-3xl.font-bold');
|
||||||
@@ -351,5 +351,5 @@ async function selectAccount(page: Page, email: string) {
|
|||||||
|
|
||||||
await link.click();
|
await link.click();
|
||||||
|
|
||||||
await page.waitForURL(/\/admin\/accounts\/[^\/]+/);
|
await page.waitForURL(/\/admin\/accounts\/[^/]+/);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { test } from '@playwright/test';
|
import { test } from '@playwright/test';
|
||||||
import { join } from 'node:path';
|
|
||||||
import { cwd } from 'node:process';
|
|
||||||
|
|
||||||
import { AuthPageObject } from './authentication/auth.po';
|
import { AuthPageObject } from './authentication/auth.po';
|
||||||
|
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { cwd } from 'node:process';
|
||||||
|
|
||||||
const testAuthFile = join(cwd(), '.auth/test@makerkit.dev.json');
|
const testAuthFile = join(cwd(), '.auth/test@makerkit.dev.json');
|
||||||
const ownerAuthFile = join(cwd(), '.auth/owner@makerkit.dev.json');
|
const ownerAuthFile = join(cwd(), '.auth/owner@makerkit.dev.json');
|
||||||
const superAdminAuthFile = join(cwd(), '.auth/super-admin@makerkit.dev.json');
|
const superAdminAuthFile = join(cwd(), '.auth/super-admin@makerkit.dev.json');
|
||||||
|
|||||||
@@ -31,8 +31,17 @@ export class AuthPageObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async signOut() {
|
async signOut() {
|
||||||
await this.page.click('[data-test="account-dropdown-trigger"]');
|
const trigger = this.page.locator(
|
||||||
await this.page.click('[data-test="account-dropdown-sign-out"]');
|
'[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 }) {
|
async signIn(params: { email: string; password: string }) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { expect, test } from '@playwright/test';
|
|||||||
|
|
||||||
test.describe('Healthcheck endpoint', () => {
|
test.describe('Healthcheck endpoint', () => {
|
||||||
test('returns healthy status', async ({ request }) => {
|
test('returns healthy status', async ({ request }) => {
|
||||||
const response = await request.get('/healthcheck');
|
const response = await request.get('/api/healthcheck');
|
||||||
|
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export class InvitationsPageObject {
|
|||||||
`[data-test="invite-member-form-item"]:nth-child(${nth}) [data-test="role-selector-trigger"]`,
|
`[data-test="invite-member-form-item"]:nth-child(${nth}) [data-test="role-selector-trigger"]`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.page.click(`[data-test="role-option-${invite.role}"]`);
|
await this.page.getByRole('option', { name: invite.role }).click();
|
||||||
|
|
||||||
if (index < invites.length - 1) {
|
if (index < invites.length - 1) {
|
||||||
await form.locator('[data-test="add-new-invite-button"]').click();
|
await form.locator('[data-test="add-new-invite-button"]').click();
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ export class TeamAccountsPageObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTeamFromSelector(teamName: string) {
|
getTeamFromSelector(teamName: string) {
|
||||||
return this.page.locator(`[data-test="account-selector-team"]`, {
|
return this.page.locator('[data-test="workspace-team-item"]', {
|
||||||
hasText: teamName,
|
hasText: teamName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTeams() {
|
getTeams() {
|
||||||
return this.page.locator('[data-test="account-selector-team"]');
|
return this.page.locator('[data-test="workspace-team-item"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
goToSettings() {
|
goToSettings() {
|
||||||
@@ -83,10 +83,11 @@ export class TeamAccountsPageObject {
|
|||||||
|
|
||||||
openAccountsSelector() {
|
openAccountsSelector() {
|
||||||
return expect(async () => {
|
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(
|
return expect(
|
||||||
this.page.locator('[data-test="account-selector-content"]'),
|
this.page.locator('[data-test="workspace-switch-content"]'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
}).toPass();
|
}).toPass();
|
||||||
}
|
}
|
||||||
@@ -115,7 +116,7 @@ export class TeamAccountsPageObject {
|
|||||||
async createTeam({ teamName, slug } = this.createTeamName()) {
|
async createTeam({ teamName, slug } = this.createTeamName()) {
|
||||||
await this.openAccountsSelector();
|
await this.openAccountsSelector();
|
||||||
|
|
||||||
await this.page.click('[data-test="create-team-account-trigger"]');
|
await this.page.click('[data-test="create-team-trigger"]');
|
||||||
|
|
||||||
await this.page.fill(
|
await this.page.fill(
|
||||||
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
||||||
@@ -140,14 +141,13 @@ export class TeamAccountsPageObject {
|
|||||||
await this.openAccountsSelector();
|
await this.openAccountsSelector();
|
||||||
await expect(this.getTeamFromSelector(teamName)).toBeVisible();
|
await expect(this.getTeamFromSelector(teamName)).toBeVisible();
|
||||||
|
|
||||||
// Close the selector
|
await this.closeAccountsSelector();
|
||||||
await this.page.keyboard.press('Escape');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createTeamWithNonLatinName(teamName: string, slug: string) {
|
async createTeamWithNonLatinName(teamName: string, slug: string) {
|
||||||
await this.openAccountsSelector();
|
await this.openAccountsSelector();
|
||||||
|
|
||||||
await this.page.click('[data-test="create-team-account-trigger"]');
|
await this.page.click('[data-test="create-team-trigger"]');
|
||||||
|
|
||||||
await this.page.fill(
|
await this.page.fill(
|
||||||
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
||||||
@@ -177,8 +177,15 @@ export class TeamAccountsPageObject {
|
|||||||
await this.openAccountsSelector();
|
await this.openAccountsSelector();
|
||||||
await expect(this.getTeamFromSelector(teamName)).toBeVisible();
|
await expect(this.getTeamFromSelector(teamName)).toBeVisible();
|
||||||
|
|
||||||
// Close the selector
|
await this.closeAccountsSelector();
|
||||||
await this.page.keyboard.press('Escape');
|
}
|
||||||
|
|
||||||
|
async closeAccountsSelector() {
|
||||||
|
await this.page.locator('body').click({ position: { x: 0, y: 0 } });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
this.page.locator('[data-test="workspace-switch-content"]'),
|
||||||
|
).toBeHidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSlugField() {
|
getSlugField() {
|
||||||
@@ -207,11 +214,10 @@ export class TeamAccountsPageObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteAccount(email: string) {
|
async deleteAccount(email: string) {
|
||||||
await expect(async () => {
|
|
||||||
await this.page.click('[data-test="delete-team-trigger"]');
|
await this.page.click('[data-test="delete-team-trigger"]');
|
||||||
|
|
||||||
await this.otp.completeOtpVerification(email);
|
await this.otp.completeOtpVerification(email);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
const click = this.page.click(
|
const click = this.page.click(
|
||||||
'[data-test="delete-team-form-confirm-button"]',
|
'[data-test="delete-team-form-confirm-button"]',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ test.describe('Team Accounts', () => {
|
|||||||
await teamAccounts.createTeam();
|
await teamAccounts.createTeam();
|
||||||
|
|
||||||
await teamAccounts.openAccountsSelector();
|
await teamAccounts.openAccountsSelector();
|
||||||
await page.click('[data-test="create-team-account-trigger"]');
|
await page.click('[data-test="create-team-trigger"]');
|
||||||
|
|
||||||
await teamAccounts.tryCreateTeam('billing');
|
await teamAccounts.tryCreateTeam('billing');
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ test.describe('Team Accounts', () => {
|
|||||||
|
|
||||||
// Use non-Latin name to trigger the slug field visibility
|
// Use non-Latin name to trigger the slug field visibility
|
||||||
await teamAccounts.openAccountsSelector();
|
await teamAccounts.openAccountsSelector();
|
||||||
await page.click('[data-test="create-team-account-trigger"]');
|
await page.click('[data-test="create-team-trigger"]');
|
||||||
|
|
||||||
await page.fill(
|
await page.fill(
|
||||||
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { AuthPageObject } from '../authentication/auth.po';
|
|
||||||
import { TeamBillingPageObject } from './team-billing.po';
|
import { TeamBillingPageObject } from './team-billing.po';
|
||||||
|
|
||||||
test.describe('Team Billing', () => {
|
test.describe('Team Billing', () => {
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ export class BillingPageObject {
|
|||||||
// wait a bit for the webhook to be processed
|
// wait a bit for the webhook to be processed
|
||||||
await this.page.waitForTimeout(1000);
|
await this.page.waitForTimeout(1000);
|
||||||
|
|
||||||
return this.page
|
await this.page.locator('[data-test="checkout-success-back-link"]').click();
|
||||||
.locator('[data-test="checkout-success-back-link"]')
|
|
||||||
.click();
|
await this.page.waitForURL('**/billing');
|
||||||
}
|
}
|
||||||
|
|
||||||
proceedToCheckout() {
|
proceedToCheckout() {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION=true
|
|||||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=true
|
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=true
|
||||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true
|
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true
|
||||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true
|
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true
|
||||||
|
NEXT_PUBLIC_ENABLE_TEAMS_ACCOUNTS_ONLY=false
|
||||||
NEXT_PUBLIC_LANGUAGE_PRIORITY=application
|
NEXT_PUBLIC_LANGUAGE_PRIORITY=application
|
||||||
|
|
||||||
# NEXTJS
|
# NEXTJS
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
|
||||||
|
|
||||||
// local imports
|
|
||||||
import { DocsNavigation } from './_components/docs-navigation';
|
|
||||||
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 tree = buildDocumentationTree(docs);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={'container h-[calc(100vh-56px)] overflow-y-hidden'}>
|
|
||||||
<SidebarProvider
|
|
||||||
className="lg:gap-x-6"
|
|
||||||
style={{ '--sidebar-width': '17em' } as React.CSSProperties}
|
|
||||||
>
|
|
||||||
<HideFooterStyles />
|
|
||||||
|
|
||||||
<DocsNavigation pages={tree} />
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</SidebarProvider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function HideFooterStyles() {
|
|
||||||
return (
|
|
||||||
<style
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: `
|
|
||||||
.site-footer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DocsLayout;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: t('marketing:documentation'),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
async function DocsPage() {
|
|
||||||
const { t, resolvedLanguage } = await createI18nServerInstance();
|
|
||||||
const items = await getDocs(resolvedLanguage);
|
|
||||||
|
|
||||||
// Filter out any docs that have a parentId, as these are children of other docs
|
|
||||||
const cards = items.filter((item) => !item.parentId);
|
|
||||||
|
|
||||||
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')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={'relative flex size-full justify-center overflow-y-auto'}>
|
|
||||||
<DocsCards cards={cards} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withI18n(DocsPage);
|
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -8,10 +8,10 @@ export function SiteFooter() {
|
|||||||
return (
|
return (
|
||||||
<Footer
|
<Footer
|
||||||
logo={<AppLogo className="w-[85px] md:w-[95px]" />}
|
logo={<AppLogo className="w-[85px] md:w-[95px]" />}
|
||||||
description={<Trans i18nKey="marketing:footerDescription" />}
|
description={<Trans i18nKey="marketing.footerDescription" />}
|
||||||
copyright={
|
copyright={
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="marketing:copyright"
|
i18nKey="marketing.copyright"
|
||||||
values={{
|
values={{
|
||||||
product: appConfig.name,
|
product: appConfig.name,
|
||||||
year: new Date().getFullYear(),
|
year: new Date().getFullYear(),
|
||||||
@@ -20,35 +20,35 @@ export function SiteFooter() {
|
|||||||
}
|
}
|
||||||
sections={[
|
sections={[
|
||||||
{
|
{
|
||||||
heading: <Trans i18nKey="marketing:about" />,
|
heading: <Trans i18nKey="marketing.about" />,
|
||||||
links: [
|
links: [
|
||||||
{ href: '/blog', label: <Trans i18nKey="marketing:blog" /> },
|
{ href: '/blog', label: <Trans i18nKey="marketing.blog" /> },
|
||||||
{ href: '/contact', label: <Trans i18nKey="marketing:contact" /> },
|
{ href: '/contact', label: <Trans i18nKey="marketing.contact" /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: <Trans i18nKey="marketing:product" />,
|
heading: <Trans i18nKey="marketing.product" />,
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
href: '/docs',
|
href: '/docs',
|
||||||
label: <Trans i18nKey="marketing:documentation" />,
|
label: <Trans i18nKey="marketing.documentation" />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: <Trans i18nKey="marketing:legal" />,
|
heading: <Trans i18nKey="marketing.legal" />,
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
href: '/terms-of-service',
|
href: '/terms-of-service',
|
||||||
label: <Trans i18nKey="marketing:termsOfService" />,
|
label: <Trans i18nKey="marketing.termsOfService" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/privacy-policy',
|
href: '/privacy-policy',
|
||||||
label: <Trans i18nKey="marketing:privacyPolicy" />,
|
label: <Trans i18nKey="marketing.privacyPolicy" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/cookie-policy',
|
href: '/cookie-policy',
|
||||||
label: <Trans i18nKey="marketing:cookiePolicy" />,
|
label: <Trans i18nKey="marketing.cookiePolicy" />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -31,6 +31,7 @@ const MobileModeToggle = dynamic(
|
|||||||
|
|
||||||
const paths = {
|
const paths = {
|
||||||
home: pathsConfig.app.home,
|
home: pathsConfig.app.home,
|
||||||
|
profileSettings: pathsConfig.app.personalAccountSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
const features = {
|
const features = {
|
||||||
@@ -78,26 +79,28 @@ function AuthButtons() {
|
|||||||
|
|
||||||
<div className={'flex items-center gap-x-2'}>
|
<div className={'flex items-center gap-x-2'}>
|
||||||
<Button
|
<Button
|
||||||
|
nativeButton={false}
|
||||||
className={'hidden md:flex md:text-sm'}
|
className={'hidden md:flex md:text-sm'}
|
||||||
asChild
|
render={
|
||||||
|
<Link href={pathsConfig.auth.signIn}>
|
||||||
|
<Trans i18nKey={'auth.signIn'} />
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
variant={'outline'}
|
variant={'outline'}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
>
|
/>
|
||||||
<Link href={pathsConfig.auth.signIn}>
|
|
||||||
<Trans i18nKey={'auth:signIn'} />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
asChild
|
nativeButton={false}
|
||||||
|
render={
|
||||||
|
<Link href={pathsConfig.auth.signUp}>
|
||||||
|
<Trans i18nKey={'auth.signUp'} />
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
className="text-xs md:text-sm"
|
className="text-xs md:text-sm"
|
||||||
variant={'default'}
|
variant={'default'}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
>
|
/>
|
||||||
<Link href={pathsConfig.auth.signUp}>
|
|
||||||
<Trans i18nKey={'auth:signUp'} />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -9,7 +9,7 @@ import { SiteNavigation } from './site-navigation';
|
|||||||
export function SiteHeader(props: { user?: JWTUserData | null }) {
|
export function SiteHeader(props: { user?: JWTUserData | null }) {
|
||||||
return (
|
return (
|
||||||
<Header
|
<Header
|
||||||
logo={<AppLogo />}
|
logo={<AppLogo className="mx-auto sm:mx-0" href="/" />}
|
||||||
navigation={<SiteNavigation />}
|
navigation={<SiteNavigation />}
|
||||||
actions={<SiteHeaderAccountSection user={props.user ?? null} />}
|
actions={<SiteHeaderAccountSection user={props.user ?? null} />}
|
||||||
/>
|
/>
|
||||||
@@ -15,23 +15,23 @@ import { SiteNavigationItem } from './site-navigation-item';
|
|||||||
|
|
||||||
const links = {
|
const links = {
|
||||||
Blog: {
|
Blog: {
|
||||||
label: 'marketing:blog',
|
label: 'marketing.blog',
|
||||||
path: '/blog',
|
path: '/blog',
|
||||||
},
|
},
|
||||||
Changelog: {
|
Changelog: {
|
||||||
label: 'marketing:changelog',
|
label: 'marketing.changelog',
|
||||||
path: '/changelog',
|
path: '/changelog',
|
||||||
},
|
},
|
||||||
Docs: {
|
Docs: {
|
||||||
label: 'marketing:documentation',
|
label: 'marketing.documentation',
|
||||||
path: '/docs',
|
path: '/docs',
|
||||||
},
|
},
|
||||||
Pricing: {
|
Pricing: {
|
||||||
label: 'marketing:pricing',
|
label: 'marketing.pricing',
|
||||||
path: '/pricing',
|
path: '/pricing',
|
||||||
},
|
},
|
||||||
FAQ: {
|
FAQ: {
|
||||||
label: 'marketing:faq',
|
label: 'marketing.faq',
|
||||||
path: '/faq',
|
path: '/faq',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -74,11 +74,14 @@ function MobileDropdown() {
|
|||||||
const className = 'flex w-full h-full items-center';
|
const className = 'flex w-full h-full items-center';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem key={item.path} asChild>
|
<DropdownMenuItem
|
||||||
|
key={item.path}
|
||||||
|
render={
|
||||||
<Link className={className} href={item.path}>
|
<Link className={className} href={item.path}>
|
||||||
<Trans i18nKey={item.label} />
|
<Trans i18nKey={item.label} />
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import { createCmsClient } from '@kit/cms';
|
import { createCmsClient } from '@kit/cms';
|
||||||
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
|
||||||
|
|
||||||
import { Post } from '../../blog/_components/post';
|
import { Post } from '../../blog/_components/post';
|
||||||
|
|
||||||
interface BlogPageProps {
|
interface BlogPageProps {
|
||||||
@@ -75,4 +72,4 @@ async function BlogPost({ params }: BlogPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n(BlogPost);
|
export default BlogPost;
|
||||||
@@ -25,7 +25,7 @@ export function BlogPagination(props: {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowLeft className={'mr-2 h-4'} />
|
<ArrowLeft className={'mr-2 h-4'} />
|
||||||
<Trans i18nKey={'marketing:blogPaginationPrevious'} />
|
<Trans i18nKey={'marketing.blogPaginationPrevious'} />
|
||||||
</Button>
|
</Button>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export function BlogPagination(props: {
|
|||||||
navigate(props.currentPage + 1);
|
navigate(props.currentPage + 1);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trans i18nKey={'marketing:blogPaginationNext'} />
|
<Trans i18nKey={'marketing.blogPaginationNext'} />
|
||||||
<ArrowRight className={'ml-2 h-4'} />
|
<ArrowRight className={'ml-2 h-4'} />
|
||||||
</Button>
|
</Button>
|
||||||
</If>
|
</If>
|
||||||
@@ -2,14 +2,13 @@ import { cache } from 'react';
|
|||||||
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { getLocale, getTranslations } from 'next-intl/server';
|
||||||
|
|
||||||
import { createCmsClient } from '@kit/cms';
|
import { createCmsClient } from '@kit/cms';
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
|
||||||
|
|
||||||
// local imports
|
// local imports
|
||||||
import { SitePageHeader } from '../_components/site-page-header';
|
import { SitePageHeader } from '../_components/site-page-header';
|
||||||
import { BlogPagination } from './_components/blog-pagination';
|
import { BlogPagination } from './_components/blog-pagination';
|
||||||
@@ -24,7 +23,8 @@ const BLOG_POSTS_PER_PAGE = 10;
|
|||||||
export const generateMetadata = async (
|
export const generateMetadata = async (
|
||||||
props: BlogPageProps,
|
props: BlogPageProps,
|
||||||
): Promise<Metadata> => {
|
): Promise<Metadata> => {
|
||||||
const { t, resolvedLanguage } = await createI18nServerInstance();
|
const t = await getTranslations('marketing');
|
||||||
|
const resolvedLanguage = await getLocale();
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
const limit = BLOG_POSTS_PER_PAGE;
|
const limit = BLOG_POSTS_PER_PAGE;
|
||||||
|
|
||||||
@@ -34,8 +34,8 @@ export const generateMetadata = async (
|
|||||||
const { total } = await getContentItems(resolvedLanguage, limit, offset);
|
const { total } = await getContentItems(resolvedLanguage, limit, offset);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: t('marketing:blog'),
|
title: t('blog'),
|
||||||
description: t('marketing:blogSubtitle'),
|
description: t('blogSubtitle'),
|
||||||
pagination: {
|
pagination: {
|
||||||
previous: page > 0 ? `/blog?page=${page - 1}` : undefined,
|
previous: page > 0 ? `/blog?page=${page - 1}` : undefined,
|
||||||
next: offset + limit < total ? `/blog?page=${page + 1}` : undefined,
|
next: offset + limit < total ? `/blog?page=${page + 1}` : undefined,
|
||||||
@@ -67,7 +67,8 @@ const getContentItems = cache(
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function BlogPage(props: BlogPageProps) {
|
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 searchParams = await props.searchParams;
|
||||||
|
|
||||||
const limit = BLOG_POSTS_PER_PAGE;
|
const limit = BLOG_POSTS_PER_PAGE;
|
||||||
@@ -82,15 +83,12 @@ async function BlogPage(props: BlogPageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SitePageHeader
|
<SitePageHeader title={t('blog')} subtitle={t('blogSubtitle')} />
|
||||||
title={t('marketing:blog')}
|
|
||||||
subtitle={t('marketing:blogSubtitle')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={'container flex flex-col space-y-6 py-8'}>
|
<div className={'container flex flex-col space-y-6 py-8'}>
|
||||||
<If
|
<If
|
||||||
condition={posts.length > 0}
|
condition={posts.length > 0}
|
||||||
fallback={<Trans i18nKey="marketing:noPosts" />}
|
fallback={<Trans i18nKey="marketing.noPosts" />}
|
||||||
>
|
>
|
||||||
<PostsGridList>
|
<PostsGridList>
|
||||||
{posts.map((post, idx) => {
|
{posts.map((post, idx) => {
|
||||||
@@ -111,7 +109,7 @@ async function BlogPage(props: BlogPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n(BlogPage);
|
export default BlogPage;
|
||||||
|
|
||||||
function PostsGridList({ children }: React.PropsWithChildren) {
|
function PostsGridList({ children }: React.PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import { createCmsClient } from '@kit/cms';
|
import { createCmsClient } from '@kit/cms';
|
||||||
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
|
||||||
|
|
||||||
import { ChangelogDetail } from '../_components/changelog-detail';
|
import { ChangelogDetail } from '../_components/changelog-detail';
|
||||||
|
|
||||||
interface ChangelogEntryPageProps {
|
interface ChangelogEntryPageProps {
|
||||||
@@ -107,4 +104,4 @@ async function ChangelogEntryPage({ params }: ChangelogEntryPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n(ChangelogEntryPage);
|
export default ChangelogEntryPage;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user