feat: adjust onboarding ui

This commit is contained in:
Innei
2025-10-26 18:47:38 +08:00
parent bb3088208d
commit 310e1c502e
17 changed files with 546 additions and 215 deletions

View File

@@ -175,15 +175,17 @@ This dashboard follows a **linear design language** with clean lines and subtle
Core Design Principles:
- **No rounded corners**: All elements use sharp, clean edges
- **Linear gradient borders**: Use subtle gradient borders for visual separation
- **Hierarchical rounding**:
- Main page containers: Sharp edges with linear gradient borders
- Interactive elements (inputs, buttons, cards): `rounded-lg` for approachable feel
- **Linear gradient borders**: Use subtle gradient borders for main container separation
- **Minimal backgrounds**: Use solid colors (`bg-background`, `bg-background-tertiary`)
- **Clean typography**: Clear hierarchy with appropriate font sizes
- **Subtle interactions**: Focus rings and hover states with minimal animation
Form Elements (Inputs, Textareas, Selects):
- **Shape**: **NO** `rounded-xl` - use straight edges
- **Shape**: Use `rounded-lg` for subtle rounded corners (NOT sharp edges, NOT heavy rounding like `rounded-xl`)
- **Background**: Use `bg-background` for standard inputs
- **Border**: Use `border border-fill-tertiary` for default state
- **Padding**: Standard padding is `px-3 py-2` for inputs
@@ -215,7 +217,7 @@ Example (text input):
id="field-id"
type="text"
className={cx(
'w-full border border-fill-tertiary bg-background',
'w-full rounded-lg border border-fill-tertiary bg-background',
'px-3 py-2 text-sm text-text placeholder:text-text-tertiary/70',
'focus:outline-none focus:ring-2 focus:ring-accent/40',
'transition-all duration-200',
@@ -232,7 +234,7 @@ Example (textarea):
```tsx
<textarea
className={cx(
'w-full border border-fill-tertiary bg-background',
'w-full rounded-lg border border-fill-tertiary bg-background',
'px-3 py-2 text-sm text-text placeholder:text-text-tertiary/70',
'focus:outline-none focus:ring-2 focus:ring-accent/40',
'transition-all duration-200',
@@ -244,7 +246,7 @@ Example (textarea):
Buttons:
- **Shape**: **NO** `rounded-xl` - use straight edges
- **Shape**: Use `rounded-lg` for subtle rounded corners (consistent with form elements)
- **Padding**: Standard is `px-6 py-2.5` for medium buttons
- **Text Size**: Use `text-sm` with `font-medium`
- **Primary Button**:
@@ -266,7 +268,7 @@ Example (primary button):
<button
type="submit"
className={cx(
'px-6 py-2.5',
'rounded-lg px-6 py-2.5',
'bg-accent text-white font-medium text-sm',
'transition-all duration-200',
'hover:bg-accent/90',
@@ -284,7 +286,7 @@ Example (ghost button):
<button
type="button"
className={cx(
'px-6 py-2.5',
'rounded-lg px-6 py-2.5',
'text-sm font-medium text-text-secondary',
'hover:text-text hover:bg-fill/50',
'transition-all duration-200',
@@ -296,8 +298,12 @@ Example (ghost button):
Cards and Containers:
- **Shape**: **NO rounded corners** - use sharp edges
- **Borders**: Use linear gradient borders for main containers
- **Shape**:
- **Main page containers** (e.g., OnboardingWizard): **NO rounded corners** - use sharp edges with linear gradient borders
- **Inner content cards** (e.g., form sections, review cards): Use `rounded-lg` for visual hierarchy
- **Borders**:
- Main containers: Use linear gradient borders
- Inner cards: Use `border border-fill-tertiary`
- **Dividers**: Use horizontal gradient dividers for section separation
- Example: `<div className="h-[0.5px] bg-linear-to-r from-transparent via-text/20 to-transparent" />`
- **Backgrounds**: Use solid colors (`bg-background`, `bg-background-tertiary`)
@@ -346,7 +352,9 @@ Typography:
Do NOT:
- ❌ Use rounded corners (`rounded-xl`, `rounded-2xl`, `rounded-full`, etc.) - this design language uses **sharp edges**
- ❌ Use heavy rounding (`rounded-xl`, `rounded-2xl`, `rounded-full`) - use `rounded-lg` for form elements and cards
- ❌ Use sharp edges (no rounding) on form elements - always use `rounded-lg` for inputs, buttons, and cards
- ❌ Mix rounding styles - main page containers are sharp, all interactive elements use `rounded-lg`
- ❌ Use heavy borders or box shadows - use subtle linear gradients instead
- ❌ Use animated bottom borders or underlines on inputs (outdated pattern)
- ❌ Use large padding (`py-3`, `py-4`) on standard inputs - stick to `py-2` or `py-2.5`
@@ -452,9 +460,9 @@ Change checklist (agents):
- Pastel color tokens used
- Atoms created via createAtomHooks; selectors stable
- No edits to auto-generated files
- **UI Design**: Form inputs use **NO rounded corners**, `bg-background`, `border-fill-tertiary`, and `focus:ring-2 focus:ring-accent/40`
- **UI Design**: Buttons use **NO rounded corners**, `px-6 py-2.5`, `text-sm font-medium`
- **UI Design**: Main containers use linear gradient borders (see login.tsx example)
- **UI Design**: Form inputs use `rounded-lg`, `bg-background`, `border-fill-tertiary`, and `focus:ring-2 focus:ring-accent/40`
- **UI Design**: Buttons use `rounded-lg`, `px-6 py-2.5`, `text-sm font-medium`
- **UI Design**: Inner content cards use `rounded-lg` for visual hierarchy
- **UI Design**: Main page containers use linear gradient borders with sharp edges
- **UI Design**: All interactive elements have proper focus states and transitions
- **UI Design**: NO `rounded-xl`, `rounded-2xl`, or any border-radius classes
- Code passes pnpm lint, pnpm format, and pnpm build

View File

@@ -1,32 +0,0 @@
import type { FC, ReactNode } from 'react'
type LinearBorderBoxProps = {
children: ReactNode
className?: string
}
/**
* A container with linear gradient borders on all sides.
* Follows the design language from login page.
*/
export const LinearBorderBox: FC<LinearBorderBoxProps> = ({
children,
className = '',
}) => (
<div className={`relative ${className}`}>
{/* Top border */}
<div className="absolute left-0 top-0 right-0 h-[0.5px] bg-linear-to-r from-transparent via-text to-transparent" />
{/* Right border */}
<div className="absolute top-0 right-0 bottom-0 w-[0.5px] bg-linear-to-b from-transparent via-text to-transparent" />
{/* Bottom border */}
<div className="absolute left-0 bottom-0 right-0 h-[0.5px] bg-linear-to-r from-transparent via-text to-transparent" />
{/* Left border */}
<div className="absolute top-0 left-0 bottom-0 w-[0.5px] bg-linear-to-b from-transparent via-text to-transparent" />
{children}
</div>
)

View File

@@ -0,0 +1,98 @@
import { clsxm } from '@afilmory/utils'
import type { FC, ReactNode } from 'react'
type LinearBorderContainerProps = {
children: ReactNode
className?: string
/**
* If true, uses the advanced flex layout that allows borders to span the full height/width.
* This is needed when you have complex nested layouts (like grid) inside.
*
* If false, uses simple relative positioning (works for basic cases).
*/
useAdvancedLayout?: boolean
}
/**
* A container with linear gradient borders on all sides.
*
* **Two Layout Modes:**
*
* 1. **Simple mode** (`useAdvancedLayout={false}`, default):
* - Uses `position: relative` with absolutely positioned borders
* - Works for simple content without complex nested layouts
* - Borders may not span full height if content has complex flex/grid
*
* 2. **Advanced mode** (`useAdvancedLayout={true}`):
* - Uses flex layout with separate containers for right/bottom borders
* - Borders always span the full container dimensions
* - Required for complex nested layouts (like OnboardingWizard grid)
*
* @example
* ```tsx
* // Simple case (login form)
* <LinearBorderContainer className="p-12">
* <form>...</form>
* </LinearBorderContainer>
*
* // Complex case (onboarding wizard with grid)
* <LinearBorderContainer useAdvancedLayout className="bg-background-tertiary">
* <div className="grid lg:grid-cols-[280px_1fr]">
* <Sidebar />
* <Content />
* </div>
* </LinearBorderContainer>
* ```
*/
export const LinearBorderContainer: FC<LinearBorderContainerProps> = ({
children,
className,
useAdvancedLayout = false,
}) => {
if (!useAdvancedLayout) {
// Simple mode: works for basic content
return (
<div className={clsxm('relative', className)}>
{/* Top border */}
<div className="absolute left-0 top-0 right-0 h-[0.5px] bg-linear-to-r from-transparent via-text to-transparent" />
{/* Right border */}
<div className="absolute top-0 right-0 bottom-0 w-[0.5px] bg-linear-to-b from-transparent via-text to-transparent" />
{/* Bottom border */}
<div className="absolute left-0 bottom-0 right-0 h-[0.5px] bg-linear-to-r from-transparent via-text to-transparent" />
{/* Left border */}
<div className="absolute top-0 left-0 bottom-0 w-[0.5px] bg-linear-to-b from-transparent via-text to-transparent" />
{children}
</div>
)
}
// Advanced mode: uses flex layout for borders that span full dimensions
return (
<div className="flex flex-col">
<div className={clsxm('flex flex-row', className)}>
{/* Top border */}
<div className="absolute left-0 right-0 h-[0.5px] bg-linear-to-r from-transparent via-text to-transparent" />
{/* Left border */}
<div className="absolute top-0 bottom-0 w-[0.5px] bg-linear-to-b from-transparent via-text to-transparent" />
{/* Main content area */}
{children}
{/* Right border container */}
<div className="flex flex-col shrink-0">
<div className="absolute bottom-0 top-0 w-[0.5px] bg-linear-to-b from-transparent via-text to-transparent" />
</div>
</div>
{/* Bottom border container */}
<div className="shrink-0 w-[2px]">
<div className="absolute left-0 right-0 h-[0.5px] bg-linear-to-r from-transparent via-text to-transparent" />
</div>
</div>
)
}

View File

@@ -25,7 +25,7 @@ export const OnboardingFooter: FC<OnboardingFooterProps> = ({
<Button
type="button"
variant="ghost"
className="px-6 py-2.5 min-w-[120px] text-sm font-medium text-text-secondary hover:text-text hover:bg-fill/50 transition-all duration-200"
className="rounded-lg px-6 py-2.5 min-w-[120px] text-sm font-medium text-text-secondary hover:text-text hover:bg-fill/50 transition-all duration-200"
onClick={onBack}
disabled={disableBack || isSubmitting}
>
@@ -33,7 +33,7 @@ export const OnboardingFooter: FC<OnboardingFooterProps> = ({
</Button>
<Button
type="button"
className="px-6 py-2.5 min-w-[140px] bg-accent text-white text-sm font-medium hover:bg-accent/90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-accent/40 transition-all duration-200"
className="rounded-lg px-6 py-2.5 min-w-[140px] bg-accent text-white text-sm font-medium hover:bg-accent/90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-accent/40 transition-all duration-200"
onClick={onNext}
isLoading={isSubmitting}
>

View File

@@ -1,7 +1,9 @@
import { ScrollArea } from '@afilmory/ui'
import type { FC, ReactNode } from 'react'
import { ONBOARDING_STEPS } from '../constants'
import { useOnboardingWizard } from '../hooks/useOnboardingWizard'
import { LinearBorderContainer } from './LinearBorderContainer'
import { OnboardingFooter } from './OnboardingFooter'
import { OnboardingHeader } from './OnboardingHeader'
import { OnboardingSidebar } from './OnboardingSidebar'
@@ -89,18 +91,16 @@ export const OnboardingWizard: FC = () => {
}
return (
<div className="min-h-screen bg-background flex flex-col items-center justify-center px-4 py-10">
<div className="w-full max-w-7xl flex flex-row bg-background-tertiary">
{/* Top border */}
<div className="absolute left-0 right-0 h-[0.5px] bg-linear-to-r from-transparent via-text to-transparent" />
{/* Left border */}
<div className="absolute top-0 bottom-0 w-[0.5px] bg-linear-to-b from-transparent via-text to-transparent" />
<div className="grid lg:grid-cols-[280px_1fr]">
<div className="min-h-screen bg-background flex items-center justify-center px-4 py-10">
<LinearBorderContainer
useAdvancedLayout
className="w-full max-w-7xl h-[85vh] bg-background-tertiary"
>
<div className="grid h-full lg:grid-cols-[280px_1fr]">
{/* Sidebar */}
<div className="relative">
<div className="absolute top-0 right-0 bottom-0 w-[0.5px] bg-text/20" />
<div className="relative h-full">
{/* Vertical divider with gradient that fades at top/bottom */}
<div className="absolute top-0 right-0 bottom-0 w-[0.5px] bg-linear-to-b from-transparent via-text/20 to-transparent" />
<OnboardingSidebar
currentStepIndex={currentStepIndex}
canNavigateTo={canNavigateTo}
@@ -108,40 +108,43 @@ export const OnboardingWizard: FC = () => {
/>
</div>
{/* Main content */}
<main className="flex flex-col">
<OnboardingHeader
currentStepIndex={currentStepIndex}
totalSteps={ONBOARDING_STEPS.length}
step={currentStep}
/>
{/* Main content with fixed height and scrollable area */}
<main className="flex h-full flex-col w-[800px]">
{/* Fixed header */}
<div className="shrink-0">
<OnboardingHeader
currentStepIndex={currentStepIndex}
totalSteps={ONBOARDING_STEPS.length}
step={currentStep}
/>
{/* Horizontal divider */}
<div className="h-[0.5px] bg-linear-to-r from-transparent via-text/20 to-transparent" />
</div>
{/* Horizontal divider */}
<div className="h-[0.5px] bg-linear-to-r from-transparent via-text/30 to-transparent" />
{/* Scrollable content area */}
<div className="flex-1 overflow-hidden">
<ScrollArea rootClassName="h-full" viewportClassName="h-full">
<section className="p-12">
{stepContent[currentStep.id]}
</section>
</ScrollArea>
</div>
<section className="p-12">{stepContent[currentStep.id]}</section>
{/* Horizontal divider */}
<div className="h-[0.5px] bg-linear-to-r from-transparent via-text/30 to-transparent" />
<OnboardingFooter
onBack={goToPrevious}
onNext={goToNext}
disableBack={currentStepIndex === 0}
isSubmitting={mutation.isPending}
isLastStep={currentStepIndex === ONBOARDING_STEPS.length - 1}
/>
{/* Fixed footer */}
<div className="shrink-0">
{/* Horizontal divider */}
<div className="h-[0.5px] bg-linear-to-r from-transparent via-text/20 to-transparent" />
<OnboardingFooter
onBack={goToPrevious}
onNext={goToNext}
disableBack={currentStepIndex === 0}
isSubmitting={mutation.isPending}
isLastStep={currentStepIndex === ONBOARDING_STEPS.length - 1}
/>
</div>
</main>
</div>
<div className="flex flex-col shrink-0">
{/* Right border */}
<div className="absolute bottom-0 top-0 w-[0.5px] bg-linear-to-b from-transparent via-text to-transparent" />
</div>
</div>
<div className="shrink-0 w-[2px]">
{/* Bottom border */}
<div className="absolute left-0 right-0 h-[0.5px] bg-linear-to-r from-transparent via-text to-transparent" />
</div>
</LinearBorderContainer>
</div>
)
}

View File

@@ -1,4 +1,4 @@
import { cx } from '@afilmory/utils'
import { FormError, Input, Label } from '@afilmory/ui'
import type { FC } from 'react'
import type { AdminFormState, OnboardingErrors } from '../../types'
@@ -15,80 +15,51 @@ type AdminStepProps = {
export const AdminStep: FC<AdminStepProps> = ({ admin, errors, onChange }) => (
<form className="space-y-6" onSubmit={(event) => event.preventDefault()}>
<div className="grid gap-5 md:grid-cols-2">
<div>
<label className="text-sm font-medium text-text" htmlFor="admin-name">
Administrator name
</label>
<input
<div className="space-y-2">
<Label htmlFor="admin-name">Administrator name</Label>
<Input
id="admin-name"
value={admin.name}
onInput={(event) => onChange('name', event.currentTarget.value)}
placeholder="Studio Admin"
autoComplete="name"
className={cx(
'mt-2 w-full border border-fill-tertiary bg-background px-3 py-2 text-sm text-text placeholder:text-text-tertiary/70 focus:outline-none focus:ring-2 focus:ring-accent/40 transition-all duration-200',
errors['admin.name'] && 'border-red/60 focus:ring-red/30',
)}
error={!!errors['admin.name']}
/>
{errors['admin.name'] && (
<p className="mt-1 text-xs text-red">{errors['admin.name']}</p>
)}
<FormError>{errors['admin.name']}</FormError>
</div>
<div>
<label className="text-sm font-medium text-text" htmlFor="admin-email">
Administrator email
</label>
<input
<div className="space-y-2">
<Label htmlFor="admin-email">Administrator email</Label>
<Input
id="admin-email"
value={admin.email}
onInput={(event) => onChange('email', event.currentTarget.value)}
placeholder="admin@afilmory.app"
autoComplete="email"
className={cx(
'mt-2 w-full border border-fill-tertiary bg-background px-3 py-2 text-sm text-text placeholder:text-text-tertiary/70 focus:outline-none focus:ring-2 focus:ring-accent/40 transition-all duration-200',
errors['admin.email'] && 'border-red/60 focus:ring-red/30',
)}
error={!!errors['admin.email']}
/>
{errors['admin.email'] && (
<p className="mt-1 text-xs text-red">{errors['admin.email']}</p>
)}
<FormError>{errors['admin.email']}</FormError>
</div>
</div>
<div className="grid gap-5 md:grid-cols-2">
<div>
<label
className="text-sm font-medium text-text"
htmlFor="admin-password"
>
Password
</label>
<input
<div className="space-y-2">
<Label htmlFor="admin-password">Password</Label>
<Input
id="admin-password"
type="password"
value={admin.password}
onInput={(event) => onChange('password', event.currentTarget.value)}
placeholder="Minimum 8 characters"
autoComplete="new-password"
className={cx(
'mt-2 w-full border border-fill-tertiary bg-background px-3 py-2 text-sm text-text placeholder:text-text-tertiary/70 focus:outline-none focus:ring-2 focus:ring-accent/40 transition-all duration-200',
errors['admin.password'] && 'border-red/60 focus:ring-red/30',
)}
error={!!errors['admin.password']}
/>
{errors['admin.password'] && (
<p className="mt-1 text-xs text-red">{errors['admin.password']}</p>
)}
<FormError>{errors['admin.password']}</FormError>
</div>
<div>
<label
className="text-sm font-medium text-text"
htmlFor="admin-confirm"
>
Confirm password
</label>
<input
<div className="space-y-2">
<Label htmlFor="admin-confirm">Confirm password</Label>
<Input
id="admin-confirm"
type="password"
value={admin.confirmPassword}
@@ -97,17 +68,9 @@ export const AdminStep: FC<AdminStepProps> = ({ admin, errors, onChange }) => (
}
placeholder="Repeat password"
autoComplete="new-password"
className={cx(
'mt-2 w-full border border-fill-tertiary bg-background px-3 py-2 text-sm text-text placeholder:text-text-tertiary/70 focus:outline-none focus:ring-2 focus:ring-accent/40 transition-all duration-200',
errors['admin.confirmPassword'] &&
'border-red/60 focus:ring-red/30',
)}
error={!!errors['admin.confirmPassword']}
/>
{errors['admin.confirmPassword'] && (
<p className="mt-1 text-xs text-red">
{errors['admin.confirmPassword']}
</p>
)}
<FormError>{errors['admin.confirmPassword']}</FormError>
</div>
</div>

View File

@@ -2,7 +2,6 @@ import { Checkbox } from '@afilmory/ui'
import type { FC } from 'react'
import type {
OnboardingSettingKey,
SettingFieldDefinition,
} from '../../constants'
import type {
@@ -35,7 +34,7 @@ export const ReviewStep: FC<ReviewStepProps> = ({
onAcknowledgeChange,
}) => (
<div className="space-y-6">
<div className="border border-fill-tertiary bg-background p-6">
<div className="rounded-lg border border-fill-tertiary bg-background p-6">
<h3 className="text-sm font-semibold text-text mb-4">Tenant summary</h3>
<dl className="grid gap-4 text-sm text-text-secondary sm:grid-cols-2">
<div>
@@ -53,7 +52,7 @@ export const ReviewStep: FC<ReviewStepProps> = ({
</dl>
</div>
<div className="border border-fill-tertiary bg-background p-6">
<div className="rounded-lg border border-fill-tertiary bg-background p-6">
<h3 className="text-sm font-semibold text-text mb-4">Administrator</h3>
<dl className="grid gap-4 text-sm text-text-secondary sm:grid-cols-2">
<div>
@@ -71,7 +70,7 @@ export const ReviewStep: FC<ReviewStepProps> = ({
</dl>
</div>
<div className="border border-fill-tertiary bg-background p-6">
<div className="rounded-lg border border-fill-tertiary bg-background p-6">
<h3 className="text-sm font-semibold text-text mb-4">
Enabled integrations
</h3>
@@ -85,7 +84,7 @@ export const ReviewStep: FC<ReviewStepProps> = ({
{reviewSettings.map(({ definition, value }) => (
<li
key={definition.key}
className="border border-fill-tertiary bg-background px-4 py-3"
className="rounded-lg border border-fill-tertiary bg-background px-4 py-3"
>
<p className="text-sm font-medium text-text">
{definition.label}
@@ -99,7 +98,7 @@ export const ReviewStep: FC<ReviewStepProps> = ({
)}
</div>
<div className="border border-orange/40 bg-orange/5 p-6">
<div className="rounded-lg border border-orange/40 bg-orange/5 p-6">
<h3 className="flex items-center gap-2 text-sm font-semibold text-orange mb-2">
<i className="i-mingcute-alert-fill" />
Important

View File

@@ -1,4 +1,4 @@
import { Button } from '@afilmory/ui'
import { Button, FormError, Input, Textarea } from '@afilmory/ui'
import { cx } from '@afilmory/utils'
import type { FC } from 'react'
@@ -23,7 +23,7 @@ export const SettingsStep: FC<SettingsStepProps> = ({
{ONBOARDING_SETTING_SECTIONS.map((section) => (
<div
key={section.id}
className="border border-fill-tertiary bg-background p-6"
className="rounded-lg border border-fill-tertiary bg-background p-6"
>
<header className="flex flex-col gap-1 mb-5">
<h3 className="text-sm font-semibold text-text">{section.title}</h3>
@@ -39,7 +39,7 @@ export const SettingsStep: FC<SettingsStepProps> = ({
return (
<div
key={field.key}
className="border border-fill-tertiary bg-fill p-5"
className="rounded-lg border border-fill-tertiary bg-fill p-5"
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="flex-1">
@@ -66,42 +66,32 @@ export const SettingsStep: FC<SettingsStepProps> = ({
</div>
{state.enabled && (
<div className="mt-4">
<div className="mt-4 space-y-2">
{field.multiline ? (
<textarea
<Textarea
value={state.value}
onInput={(event) =>
onChange(field.key, event.currentTarget.value)
}
className={cx(
'w-full border border-fill-tertiary bg-background px-3 py-2 text-sm text-text placeholder:text-text-tertiary/70 focus:outline-none focus:ring-2 focus:ring-accent/40 transition-all duration-200',
hasError && 'border-red/60 focus:ring-red/30',
)}
rows={3}
placeholder={field.placeholder}
error={hasError}
/>
) : (
<input
<Input
type={field.sensitive ? 'password' : 'text'}
value={state.value}
onInput={(event) =>
onChange(field.key, event.currentTarget.value)
}
placeholder={field.placeholder}
className={cx(
'w-full border border-fill-tertiary bg-background px-3 py-2 text-sm text-text placeholder:text-text-tertiary/70 focus:outline-none focus:ring-2 focus:ring-accent/40 transition-all duration-200',
hasError && 'border-red/60 focus:ring-red/30',
)}
error={hasError}
autoComplete="off"
/>
)}
{errors[errorKey] && (
<p className="mt-1 text-xs text-red">
{errors[errorKey]}
</p>
)}
<FormError>{errors[errorKey]}</FormError>
{field.helper && (
<p className="mt-2 text-[11px] text-text-tertiary">
<p className="text-[11px] text-text-tertiary">
{field.helper}
</p>
)}

View File

@@ -1,5 +1,4 @@
import { Button } from '@afilmory/ui'
import { cx } from '@afilmory/utils'
import { Button, FormError, Input, Label } from '@afilmory/ui'
import type { FC } from 'react'
import type { OnboardingErrors, TenantFormState } from '../../types'
@@ -23,76 +22,55 @@ export const TenantStep: FC<TenantStepProps> = ({
}) => (
<form className="space-y-6" onSubmit={(event) => event.preventDefault()}>
<div className="grid gap-5 md:grid-cols-2">
<div>
<label className="text-sm font-medium text-text" htmlFor="tenant-name">
Workspace name
</label>
<input
<div className="space-y-2">
<Label htmlFor="tenant-name">Workspace name</Label>
<Input
id="tenant-name"
value={tenant.name}
onInput={(event) => onNameChange(event.currentTarget.value)}
placeholder="Afilmory Studio"
className={cx(
'mt-2 w-full border border-fill-tertiary bg-background px-3 py-2 text-sm text-text placeholder:text-text-tertiary/70 focus:outline-none focus:ring-2 focus:ring-accent/40 transition-all duration-200',
errors['tenant.name'] && 'border-red/60 focus:ring-red/30',
)}
error={!!errors['tenant.name']}
autoComplete="organization"
/>
{errors['tenant.name'] && (
<p className="mt-1 text-xs text-red">{errors['tenant.name']}</p>
)}
<FormError>{errors['tenant.name']}</FormError>
</div>
<div>
<label className="text-sm font-medium text-text" htmlFor="tenant-slug">
Tenant slug
</label>
<div className="mt-2 flex gap-2">
<input
<div className="space-y-2">
<Label htmlFor="tenant-slug">Tenant slug</Label>
<div className="flex gap-2">
<Input
id="tenant-slug"
value={tenant.slug}
onInput={(event) => onSlugChange(event.currentTarget.value)}
placeholder="afilmory"
className={cx(
'w-full border border-fill-tertiary bg-background px-3 py-2 text-sm text-text placeholder:text-text-tertiary/70 focus:outline-none focus:ring-2 focus:ring-accent/40 transition-all duration-200',
errors['tenant.slug'] && 'border-red/60 focus:ring-red/30',
)}
error={!!errors['tenant.slug']}
autoComplete="off"
/>
<Button
type="button"
variant="ghost"
className="px-6 py-2.5 min-w-[120px] text-sm font-medium text-text-secondary hover:text-text hover:bg-fill/50 transition-all duration-200"
className="rounded-lg px-6 py-2.5 min-w-[120px] text-sm font-medium text-text-secondary hover:text-text hover:bg-fill/50 transition-all duration-200"
onClick={onSuggestSlug}
>
Suggest
</Button>
</div>
{errors['tenant.slug'] && (
<p className="mt-1 text-xs text-red">{errors['tenant.slug']}</p>
)}
<FormError>{errors['tenant.slug']}</FormError>
</div>
</div>
<div>
<label className="text-sm font-medium text-text" htmlFor="tenant-domain">
Custom domain (optional)
</label>
<input
<div className="space-y-2">
<Label htmlFor="tenant-domain">Custom domain (optional)</Label>
<Input
id="tenant-domain"
value={tenant.domain}
onInput={(event) => onDomainChange(event.currentTarget.value)}
placeholder="gallery.afilmory.app"
className={cx(
'mt-2 w-full border border-fill-tertiary bg-background px-3 py-2 text-sm text-text placeholder:text-text-tertiary/70 focus:outline-none focus:ring-2 focus:ring-accent/40 transition-all duration-200',
errors['tenant.domain'] && 'border-red/60 focus:ring-red/30',
)}
error={!!errors['tenant.domain']}
autoComplete="off"
/>
{errors['tenant.domain'] && (
<p className="mt-1 text-xs text-red">{errors['tenant.domain']}</p>
)}
<p className="mt-2 text-xs text-text-tertiary">
<FormError>{errors['tenant.domain']}</FormError>
<p className="text-xs text-text-tertiary">
Domains enable automatic routing for tenant-specific dashboards.
Configure DNS separately after initialization.
</p>

View File

@@ -0,0 +1,39 @@
import { clsxm } from '@afilmory/utils'
import type { FC, HTMLAttributes } from 'react'
export interface FormErrorProps extends HTMLAttributes<HTMLParagraphElement> {
/**
* Error message to display
*/
children?: string
}
/**
* A styled error message component for form fields.
*
* Features:
* - Red text color
* - Consistent typography (text-xs)
* - Proper spacing (mt-1)
* - Only renders if children is provided
*
* @example
* ```tsx
* <FormError>{errors.email}</FormError>
* ```
*/
export const FormError: FC<FormErrorProps> = ({
children,
className,
...props
}) => {
if (!children) return null
return (
<p className={clsxm('mt-1 text-xs text-red', className)} {...props}>
{children}
</p>
)
}
FormError.displayName = 'FormError'

View File

@@ -0,0 +1,84 @@
import { clsxm } from '@afilmory/utils'
import type { FC, ReactNode } from 'react'
export interface FormFieldProps {
/**
* Label text for the field
*/
label: string
/**
* HTML id for the input element
*/
htmlFor: string
/**
* Error message to display
*/
error?: string
/**
* Helper text to display below the input
*/
helperText?: string
/**
* Whether the field is required
*/
required?: boolean
/**
* The input/textarea element
*/
children: ReactNode
/**
* Additional class name for the container
*/
className?: string
}
/**
* A form field container component with label, error message, and helper text.
*
* Features:
* - Consistent label styling
* - Error message display
* - Helper text support
* - Required field indicator
* - Proper spacing and typography
*
* @example
* ```tsx
* <FormField
* label="Email Address"
* htmlFor="email"
* error={errors.email}
* helperText="We'll never share your email"
* required
* >
* <Input
* id="email"
* type="email"
* error={!!errors.email}
* />
* </FormField>
* ```
*/
export const FormField: FC<FormFieldProps> = ({
label,
htmlFor,
error,
helperText,
required,
children,
className,
}) => (
<div className={clsxm('space-y-2', className)}>
<label htmlFor={htmlFor} className="text-text block text-sm font-medium">
{label}
{required && <span className="text-red ml-1">*</span>}
</label>
{children}
{error && <p className="text-red text-xs">{error}</p>}
{!error && helperText && (
<p className="text-text-tertiary text-xs">{helperText}</p>
)}
</div>
)
FormField.displayName = 'FormField'

View File

@@ -0,0 +1,45 @@
import { clsxm } from '@afilmory/utils'
import type { FC, HTMLAttributes } from 'react'
export interface FormHelperTextProps
extends HTMLAttributes<HTMLParagraphElement> {
/**
* Helper text to display
*/
children?: string
}
/**
* A styled helper text component for form fields.
*
* Features:
* - Muted text color (text-text-tertiary)
* - Consistent typography (text-xs)
* - Proper spacing (mt-2)
* - Only renders if children is provided
*
* @example
* ```tsx
* <FormHelperText>
* We'll never share your email with anyone else.
* </FormHelperText>
* ```
*/
export const FormHelperText: FC<FormHelperTextProps> = ({
children,
className,
...props
}) => {
if (!children) return null
return (
<p
className={clsxm('mt-2 text-xs text-text-tertiary', className)}
{...props}
>
{children}
</p>
)
}
FormHelperText.displayName = 'FormHelperText'

View File

@@ -0,0 +1,55 @@
import { clsxm } from '@afilmory/utils'
import type { InputHTMLAttributes } from 'react'
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
/**
* Whether the input has an error state
*/
error?: boolean
/**
* Additional class name
*/
className?: string
}
/**
* A styled input component following the dashboard design language.
*
* Features:
* - `rounded-lg` for approachable feel
* - Consistent padding and text styles
* - Focus ring with accent color
* - Error state support
* - Full TypeScript support with forwarded ref
*
* @example
* ```tsx
* <Input
* placeholder="Enter your email"
* type="email"
* error={hasError}
* />
* ```
*/
export const Input = ({
ref,
error,
className,
...props
}: InputProps & { ref?: React.RefObject<HTMLInputElement | null> }) => (
<input
ref={ref}
className={clsxm(
'w-full rounded-lg border border-fill-tertiary bg-background',
'px-3 py-2 text-sm text-text placeholder:text-text-tertiary/70',
'focus:outline-none focus:ring-2 focus:ring-accent/40',
'transition-all duration-200',
'disabled:cursor-not-allowed disabled:opacity-60',
error && 'border-red/60 focus:ring-red/30',
className,
)}
{...props}
/>
)
Input.displayName = 'Input'

View File

@@ -0,0 +1,41 @@
import { clsxm } from '@afilmory/utils'
import type { FC, LabelHTMLAttributes } from 'react'
export interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
/**
* Whether the field is required
*/
required?: boolean
}
/**
* A styled label component for form fields.
*
* Features:
* - Consistent text styling
* - Required field indicator
* - Proper typography
*
* @example
* ```tsx
* <Label htmlFor="email" required>
* Email Address
* </Label>
* ```
*/
export const Label: FC<LabelProps> = ({
required,
className,
children,
...props
}) => (
<label
className={clsxm('block text-sm font-medium text-text', className)}
{...props}
>
{children}
{required && <span className="text-red ml-1">*</span>}
</label>
)
Label.displayName = 'Label'

View File

@@ -0,0 +1,53 @@
import { clsxm } from '@afilmory/utils'
import type { TextareaHTMLAttributes } from 'react'
export interface TextareaProps
extends TextareaHTMLAttributes<HTMLTextAreaElement> {
/**
* Whether the textarea has an error state
*/
error?: boolean
/**
* Additional class name
*/
className?: string
}
/**
* A styled textarea component following the dashboard design language.
*
* Features:
* - `rounded-lg` for approachable feel
* - Consistent padding and text styles
* - Focus ring with accent color
* - Error state support
* - Full TypeScript support with forwarded ref
* - Auto-resizable height support via native rows prop
*
* @example
* ```tsx
* <Textarea
* placeholder="Enter description"
* rows={3}
* error={hasError}
* />
* ```
*/
export const Textarea = ({ ref, error, className, ...props }: TextareaProps & { ref?: React.RefObject<HTMLTextAreaElement | null> }) => (
<textarea
ref={ref}
className={clsxm(
'w-full rounded-lg border border-fill-tertiary bg-background',
'px-3 py-2 text-sm text-text placeholder:text-text-tertiary/70',
'focus:outline-none focus:ring-2 focus:ring-accent/40',
'transition-all duration-200',
'disabled:cursor-not-allowed disabled:opacity-60',
'resize-y',
error && 'border-red/60 focus:ring-red/30',
className,
)}
{...props}
/>
)
Textarea.displayName = 'Textarea'

View File

@@ -0,0 +1,6 @@
export * from './FormError'
export * from './FormField'
export * from './FormHelperText'
export * from './Input'
export * from './Label'
export * from './Textarea'

View File

@@ -4,6 +4,7 @@ export * from './checkbox'
export * from './context-menu'
export * from './dialog'
export * from './dropdown-menu'
export * from './form'
export * from './hover-card'
export * from './icons'
export * from './lazy-image'