mirror of
https://github.com/Afilmory/afilmory
synced 2026-04-24 23:05:05 +00:00
feat: adjust onboarding ui
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
39
packages/ui/src/form/FormError.tsx
Normal file
39
packages/ui/src/form/FormError.tsx
Normal 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'
|
||||
84
packages/ui/src/form/FormField.tsx
Normal file
84
packages/ui/src/form/FormField.tsx
Normal 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'
|
||||
45
packages/ui/src/form/FormHelperText.tsx
Normal file
45
packages/ui/src/form/FormHelperText.tsx
Normal 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'
|
||||
55
packages/ui/src/form/Input.tsx
Normal file
55
packages/ui/src/form/Input.tsx
Normal 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'
|
||||
41
packages/ui/src/form/Label.tsx
Normal file
41
packages/ui/src/form/Label.tsx
Normal 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'
|
||||
53
packages/ui/src/form/Textarea.tsx
Normal file
53
packages/ui/src/form/Textarea.tsx
Normal 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'
|
||||
6
packages/ui/src/form/index.ts
Normal file
6
packages/ui/src/form/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './FormError'
|
||||
export * from './FormField'
|
||||
export * from './FormHelperText'
|
||||
export * from './Input'
|
||||
export * from './Label'
|
||||
export * from './Textarea'
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user