Moved all 17 components from ui/ into atomic design tiers: atoms (Button, IconButton, Input, Textarea, Select, Checkbox, Radio, Switch, Badge, Tag, Chip, Tooltip) and molecules (Alert, Accordion, Card, Dialog, Popover). Updated all Storybook titles and cross-component imports. Changed Input icons to primary-dark and replaced palette token references with semantic tokens. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
251 lines
8.3 KiB
TypeScript
251 lines
8.3 KiB
TypeScript
import { useState } from 'react'
|
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
import { AlertTriangle } from 'lucide-react'
|
|
import { Dialog, DialogHeader, DialogTitle, DialogDescription, DialogContent, DialogFooter } from './Dialog'
|
|
import { Button } from '@/components/atoms/Button'
|
|
import { Input } from '@/components/atoms/Input'
|
|
|
|
const meta: Meta<typeof Dialog> = {
|
|
title: 'Molecules/Dialog',
|
|
component: Dialog,
|
|
tags: ['autodocs'],
|
|
argTypes: {
|
|
size: {
|
|
control: 'select',
|
|
options: ['sm', 'default', 'lg', 'full'],
|
|
},
|
|
closeOnBackdrop: {
|
|
control: 'boolean',
|
|
},
|
|
},
|
|
}
|
|
|
|
export default meta
|
|
type Story = StoryObj<typeof meta>
|
|
|
|
// --- Default ---
|
|
|
|
export const Default: Story = {
|
|
render: () => {
|
|
const [open, setOpen] = useState(false)
|
|
return (
|
|
<>
|
|
<Button onClick={() => setOpen(true)}>Open dialog</Button>
|
|
<Dialog open={open} onClose={() => setOpen(false)}>
|
|
<DialogHeader onClose={() => setOpen(false)}>
|
|
<DialogTitle>Dialog title</DialogTitle>
|
|
<DialogDescription>A short description of the dialog purpose.</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogContent>
|
|
<p className="text-body text-text">
|
|
This is the dialog body. It can contain any content — text, forms, lists, or other
|
|
components.
|
|
</p>
|
|
</DialogContent>
|
|
<DialogFooter>
|
|
<Button variant="secondary" intent="neutral" onClick={() => setOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={() => setOpen(false)}>Confirm</Button>
|
|
</DialogFooter>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
},
|
|
}
|
|
|
|
// --- Small ---
|
|
|
|
export const Small: Story = {
|
|
render: () => {
|
|
const [open, setOpen] = useState(false)
|
|
return (
|
|
<>
|
|
<Button onClick={() => setOpen(true)}>Open small dialog</Button>
|
|
<Dialog open={open} onClose={() => setOpen(false)} size="sm">
|
|
<DialogHeader onClose={() => setOpen(false)}>
|
|
<DialogTitle>Quick confirmation</DialogTitle>
|
|
</DialogHeader>
|
|
<DialogContent>
|
|
<p className="text-body text-text">Are you sure you want to proceed?</p>
|
|
</DialogContent>
|
|
<DialogFooter>
|
|
<Button variant="tertiary" intent="neutral" onClick={() => setOpen(false)}>
|
|
No
|
|
</Button>
|
|
<Button onClick={() => setOpen(false)}>Yes</Button>
|
|
</DialogFooter>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
},
|
|
}
|
|
|
|
// --- Large ---
|
|
|
|
export const Large: Story = {
|
|
render: () => {
|
|
const [open, setOpen] = useState(false)
|
|
return (
|
|
<>
|
|
<Button onClick={() => setOpen(true)}>Open large dialog</Button>
|
|
<Dialog open={open} onClose={() => setOpen(false)} size="lg">
|
|
<DialogHeader onClose={() => setOpen(false)}>
|
|
<DialogTitle>Review submission details</DialogTitle>
|
|
<DialogDescription>
|
|
Please review the information below before submitting.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogContent>
|
|
<div className="space-y-3">
|
|
<div className="rounded-lg bg-bg px-4 py-3">
|
|
<p className="text-small font-bold text-text">Participant count</p>
|
|
<p className="text-body text-text-secondary">24 participants across 3 schools</p>
|
|
</div>
|
|
<div className="rounded-lg bg-bg px-4 py-3">
|
|
<p className="text-small font-bold text-text">Data collection period</p>
|
|
<p className="text-body text-text-secondary">March 2026 — June 2026</p>
|
|
</div>
|
|
<div className="rounded-lg bg-bg px-4 py-3">
|
|
<p className="text-small font-bold text-text">Ethics approval</p>
|
|
<p className="text-body text-text-secondary">SERAP 2026-0142 (approved)</p>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
<DialogFooter>
|
|
<Button variant="secondary" intent="neutral" onClick={() => setOpen(false)}>
|
|
Go back
|
|
</Button>
|
|
<Button onClick={() => setOpen(false)}>Submit</Button>
|
|
</DialogFooter>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
},
|
|
}
|
|
|
|
// --- Danger confirmation ---
|
|
|
|
export const DangerConfirmation: Story = {
|
|
render: () => {
|
|
const [open, setOpen] = useState(false)
|
|
return (
|
|
<>
|
|
<Button intent="danger" onClick={() => setOpen(true)}>
|
|
Delete project
|
|
</Button>
|
|
<Dialog open={open} onClose={() => setOpen(false)} size="sm">
|
|
<DialogHeader onClose={() => setOpen(false)}>
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex size-10 items-center justify-center rounded-full bg-error/10">
|
|
<AlertTriangle className="size-5 text-error" />
|
|
</div>
|
|
<DialogTitle>Delete project?</DialogTitle>
|
|
</div>
|
|
</DialogHeader>
|
|
<DialogContent>
|
|
<p className="text-body text-text">
|
|
This action cannot be undone. All data, themes, and participant responses associated
|
|
with this project will be permanently deleted.
|
|
</p>
|
|
</DialogContent>
|
|
<DialogFooter>
|
|
<Button variant="secondary" intent="neutral" onClick={() => setOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button intent="danger" onClick={() => setOpen(false)}>
|
|
Delete
|
|
</Button>
|
|
</DialogFooter>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
},
|
|
}
|
|
|
|
// --- With form ---
|
|
|
|
export const WithForm: Story = {
|
|
render: () => {
|
|
const [open, setOpen] = useState(false)
|
|
return (
|
|
<>
|
|
<Button onClick={() => setOpen(true)}>Create new theme</Button>
|
|
<Dialog open={open} onClose={() => setOpen(false)}>
|
|
<DialogHeader onClose={() => setOpen(false)}>
|
|
<DialogTitle>New theme</DialogTitle>
|
|
<DialogDescription>
|
|
Give your theme a name and description to help organise your findings.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogContent>
|
|
<div className="space-y-4">
|
|
<Input label="Theme name" placeholder="e.g. Student engagement" />
|
|
<Input label="Description" placeholder="Brief summary of this theme" hint="Optional — you can add this later" />
|
|
</div>
|
|
</DialogContent>
|
|
<DialogFooter>
|
|
<Button variant="secondary" intent="neutral" onClick={() => setOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={() => setOpen(false)}>Create theme</Button>
|
|
</DialogFooter>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
},
|
|
}
|
|
|
|
// --- No close button ---
|
|
|
|
export const NoCloseButton: Story = {
|
|
render: () => {
|
|
const [open, setOpen] = useState(false)
|
|
return (
|
|
<>
|
|
<Button onClick={() => setOpen(true)}>Open without close button</Button>
|
|
<Dialog open={open} onClose={() => setOpen(false)} closeOnBackdrop={false}>
|
|
<DialogHeader>
|
|
<DialogTitle>Terms of use</DialogTitle>
|
|
</DialogHeader>
|
|
<DialogContent>
|
|
<p className="text-body text-text">
|
|
You must accept the terms of use before continuing. This dialog cannot be dismissed
|
|
by clicking the backdrop or pressing Escape — only through the action buttons.
|
|
</p>
|
|
</DialogContent>
|
|
<DialogFooter>
|
|
<Button variant="secondary" intent="neutral" onClick={() => setOpen(false)}>
|
|
Decline
|
|
</Button>
|
|
<Button onClick={() => setOpen(false)}>Accept</Button>
|
|
</DialogFooter>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
},
|
|
}
|
|
|
|
// --- Content only ---
|
|
|
|
export const ContentOnly: Story = {
|
|
render: () => {
|
|
const [open, setOpen] = useState(false)
|
|
return (
|
|
<>
|
|
<Button onClick={() => setOpen(true)}>Open minimal dialog</Button>
|
|
<Dialog open={open} onClose={() => setOpen(false)} size="sm">
|
|
<DialogContent className="py-6">
|
|
<p className="text-center text-body text-text">
|
|
Your changes have been saved.
|
|
</p>
|
|
<div className="mt-4 flex justify-center">
|
|
<Button onClick={() => setOpen(false)}>Done</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
},
|
|
}
|