1"use client"
2
3import * as React from "react"
4import { Camera, Check, SpinnerGap } from "@phosphor-icons/react"
5import { toast } from "sonner"
6
7import { Badge } from "@/components/ui/badge"
8import { Button } from "@/components/ui/button"
9import {
10 Card,
11 CardContent,
12 CardDescription,
13 CardFooter,
14 CardHeader,
15 CardTitle,
16} from "@/components/ui/card"
17import {
18 Carousel,
19 CarouselContent,
20 CarouselItem,
21 CarouselNext,
22 CarouselPrevious,
23} from "@/components/ui/carousel"
24import {
25 Dialog,
26 DialogContent,
27 DialogDescription,
28 DialogFooter,
29 DialogHeader,
30 DialogTitle,
31 DialogTrigger,
32} from "@/components/ui/dialog"
33import {
34 Drawer,
35 DrawerContent,
36 DrawerDescription,
37 DrawerHeader,
38 DrawerTitle,
39 DrawerTrigger,
40} from "@/components/ui/drawer"
41import { Skeleton } from "@/components/ui/skeleton"
42import { Slider } from "@/components/ui/slider"
43import { Toaster } from "@/components/ui/sonner"
44import { Switch } from "@/components/ui/switch"
45import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
46import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
47import {
48 Tooltip,
49 TooltipContent,
50 TooltipProvider,
51 TooltipTrigger,
52} from "@/components/ui/tooltip"
53
54function Section({
55 title,
56 children,
57}: {
58 title: string
59 children: React.ReactNode
60}) {
61 return (
62 <section className="flex flex-col gap-3">
63 <h2 className="font-mono text-xs font-bold tracking-[0.06em] uppercase">
64 {title}
65 </h2>
66 <div className="flex flex-col gap-5 rounded-panel border-3 border-ink bg-paper p-5 shadow-hard">
67 {children}
68 </div>
69 </section>
70 )
71}
72
73function Row({
74 label,
75 children,
76}: {
77 label: string
78 children: React.ReactNode
79}) {
80 return (
81 <div className="flex flex-col gap-2">
82 <p className="font-mono text-xs font-bold tracking-[0.06em] text-muted-foreground uppercase">
83 {label}
84 </p>
85 <div className="flex flex-wrap items-center gap-3">{children}</div>
86 </div>
87 )
88}
89
90function SwitchRow() {
91 const [on, setOn] = React.useState(true)
92
93 return (
94 <Row label="Bawaan, hidup, dan mati">
95 <div className="flex items-center gap-3">
96 {/* DESIGN-SYSTEM §9: sakelar selalu punya nama aksesibel, jadi contoh
97 di galeri pun tidak boleh telanjang (temuan axe M9). */}
98 <Switch
99 checked={on}
100 onCheckedChange={setOn}
101 id="gesture"
102 aria-label="Gesture Snap"
103 />
104 <span className="font-mono text-xs font-bold uppercase">
105 {on ? "Nyala" : "Mati"}
106 </span>
107 </div>
108 <div className="flex items-center gap-3">
109 <Switch checked disabled aria-label="Strip Hidup" />
110 <span className="font-mono text-xs font-bold uppercase">Nyala</span>
111 </div>
112 </Row>
113 )
114}
115
116export default function DevUiPage() {
117 if (process.env.NODE_ENV === "production") {
118 return (
119 <main className="flex flex-1 items-center justify-center p-6 text-sm">
120 Hanya saat pengembangan.
121 </main>
122 )
123 }
124
125 return (
126 <main className="stage-grid min-h-dvh">
127 <Toaster />
128 <TooltipProvider>
129 <div className="mx-auto flex max-w-[1280px] flex-col gap-8 px-4 py-10 sm:px-6 lg:px-10">
130 <header className="flex flex-col gap-2">
131 <p className="font-mono text-xs font-bold tracking-[0.06em] uppercase">
132 M0 Fondasi
133 </p>
134 <h1 className="font-display text-2xl font-black">
135 Galeri komponen
136 </h1>
137 <p className="max-w-2xl text-sm text-muted-foreground">
138 Bandingkan dengan berkas acuan di docs/ui-reference. Arahkan
139 kursor dan tekan Tab untuk melihat keadaan hover, fokus, dan
140 aktif.
141 </p>
142 </header>
143
144 <Section title="Tombol">
145 <Row label="Varian">
146 <Button>Mulai Foto</Button>
147 <Button variant="secondary">Pakai Foto Galeri</Button>
148 <Button variant="neutral">Cetak 4R</Button>
149 <Button variant="ghost">Ulang foto terakhir</Button>
150 </Row>
151 <Row label="Ukuran, termasuk tombol ikon 44 px dan 56 px">
152 <Button size="sm">Ambil Foto</Button>
153 <Button size="lg">Lanjut Hias</Button>
154 <Button size="icon" aria-label="Ambil foto">
155 <Camera weight="bold" className="size-6" />
156 </Button>
157 <Button size="icon-lg" aria-label="Ambil foto">
158 <Camera weight="bold" className="size-7" />
159 </Button>
160 </Row>
161 <Row label="Nonaktif">
162 <Button disabled>Lanjut Hias</Button>
163 <span className="text-sm text-muted-foreground">
164 Butuh 2 foto lagi.
165 </span>
166 </Row>
167 <Row label="Memuat">
168 <Button disabled aria-busy="true">
169 <SpinnerGap weight="bold" className="size-5 animate-spin" />
170 Menyiapkan gesture
171 </Button>
172 </Row>
173 </Section>
174
175 <Section title="Chip">
176 <Row label="Netral, positif, penghitung, dan lencana centang">
177 <Badge>Photobooth di browser</Badge>
178 <Badge variant="lime">4 foto siap</Badge>
179 <Badge variant="pink">2/4</Badge>
180 <Badge variant="lime">
181 <Check weight="bold" />
182 Terpilih
183 </Badge>
184 </Row>
185 </Section>
186
187 <Section title="Kartu">
188 <Row label="Bawaan">
189 <Card className="w-full max-w-sm">
190 <CardHeader>
191 <CardTitle>Strip Estafet</CardTitle>
192 <CardDescription>
193 Cukup ambil 2 foto. Sisanya diisi temanmu lewat link.
194 </CardDescription>
195 </CardHeader>
196 <CardContent className="text-sm">
197 Strip 4 foto, 2 slot milikmu dan 2 slot milik teman.
198 </CardContent>
199 <CardFooter className="gap-3">
200 <Button variant="secondary" size="sm">
201 Estafet ke Teman
202 </Button>
203 </CardFooter>
204 </Card>
205 </Row>
206 </Section>
207
208 <Section title="Swatch terpilih">
209 <Row label="Cincin violet dengan lencana centang">
210 <button
211 type="button"
212 aria-pressed="true"
213 aria-label="Frame Lime, terpilih"
214 className="relative size-16 rounded-thumb border-2 border-ink bg-lime ring-3 ring-violet ring-offset-3 ring-offset-paper focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-violet"
215 >
216 <Badge
217 variant="lime"
218 className="absolute -top-3 -right-4 px-1.5 py-0.5"
219 >
220 <Check weight="bold" />
221 </Badge>
222 </button>
223 <button
224 type="button"
225 aria-pressed="false"
226 aria-label="Frame Krim"
227 className="size-16 rounded-thumb border-2 border-ink bg-cream focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-violet"
228 />
229 </Row>
230 </Section>
231
232 <Section title="Sakelar">
233 <SwitchRow />
234 </Section>
235
236 <Section title="Tab pil">
237 <Row label="Terpilih dan berpindah">
238 <Tabs defaultValue="frame" className="w-full max-w-md">
239 <TabsList>
240 <TabsTrigger value="frame">Frame</TabsTrigger>
241 <TabsTrigger value="efek">Efek</TabsTrigger>
242 <TabsTrigger value="stiker">Stiker</TabsTrigger>
243 </TabsList>
244 <TabsContent value="frame" className="text-sm">
245 Enam frame, masing-masing dengan nama di bawah swatch.
246 </TabsContent>
247 <TabsContent value="efek" className="text-sm">
248 Enam efek foto dari filter bawaan Konva.
249 </TabsContent>
250 <TabsContent value="stiker" className="text-sm">
251 Dua belas stiker, bisa digeser, diputar, dan diubah ukuran.
252 </TabsContent>
253 </Tabs>
254 </Row>
255 <Row label="Toggle group, satu pilihan">
256 <ToggleGroup type="single" defaultValue="frame">
257 <ToggleGroupItem value="frame">Frame</ToggleGroupItem>
258 <ToggleGroupItem value="efek">Efek</ToggleGroupItem>
259 <ToggleGroupItem value="stiker">Stiker</ToggleGroupItem>
260 </ToggleGroup>
261 </Row>
262 </Section>
263
264 <Section title="Slider">
265 <Row label="Bawaan dan nonaktif">
266 <Slider
267 defaultValue={[40]}
268 aria-label="Ukuran stiker"
269 className="max-w-xs"
270 />
271 <Slider
272 defaultValue={[20]}
273 disabled
274 aria-label="Ukuran stiker, nonaktif"
275 className="max-w-xs"
276 />
277 </Row>
278 </Section>
279
280 <Section title="Dialog">
281 <Row label="Terbuka saat diklik, Esc menutup">
282 <Dialog>
283 <DialogTrigger asChild>
284 <Button>Izinkan Kamera</Button>
285 </DialogTrigger>
286 <DialogContent>
287 <DialogHeader>
288 <DialogTitle>Unggah strip?</DialogTitle>
289 <DialogDescription>
290 Strip diunggah ke server supaya bisa dibuka lewat link.
291 Link kedaluwarsa 7 hari.
292 </DialogDescription>
293 </DialogHeader>
294 <DialogFooter>
295 <Button variant="neutral">Batal</Button>
296 <Button>Unggah</Button>
297 </DialogFooter>
298 </DialogContent>
299 </Dialog>
300 </Row>
301 </Section>
302
303 <Section title="Lembar bawah">
304 <Row label="Untuk pilihan hias di layar sempit">
305 <Drawer>
306 <DrawerTrigger asChild>
307 <Button variant="secondary">Pilih frame</Button>
308 </DrawerTrigger>
309 <DrawerContent>
310 <DrawerHeader>
311 <DrawerTitle>Frame</DrawerTitle>
312 <DrawerDescription>
313 Pilih satu frame untuk stripmu.
314 </DrawerDescription>
315 </DrawerHeader>
316 <div className="grid grid-cols-3 gap-3 p-4">
317 {["Hitam", "Krem", "Pink"].map((name) => (
318 <div
319 key={name}
320 className="rounded-thumb border-2 border-ink bg-lime p-3 text-center text-sm font-bold"
321 >
322 {name}
323 </div>
324 ))}
325 </div>
326 </DrawerContent>
327 </Drawer>
328 </Row>
329 </Section>
330
331 <Section title="Tooltip">
332 <Row label="Muncul saat kursor berhenti di atas tombol">
333 <Tooltip>
334 <TooltipTrigger asChild>
335 <Button variant="neutral">Ulang foto terakhir</Button>
336 </TooltipTrigger>
337 <TooltipContent>Mengulang foto terakhir saja</TooltipContent>
338 </Tooltip>
339 </Row>
340 </Section>
341
342 <Section title="Toast">
343 <Row label="Pesan sementara">
344 <Button
345 variant="neutral"
346 onClick={() => toast.success("Link strip disalin")}
347 >
348 Tampilkan toast
349 </Button>
350 <Button
351 variant="ghost"
352 onClick={() => toast.error("Kamera belum diizinkan")}
353 >
354 Tampilkan galat
355 </Button>
356 </Row>
357 </Section>
358
359 <Section title="Skeleton">
360 <Row label="Memuat, mengikuti bentuk akhir">
361 <div className="flex w-full max-w-sm flex-col gap-3">
362 <Skeleton className="h-6 w-40" />
363 <Skeleton className="h-24 w-full" />
364 <Skeleton className="h-11 w-32" />
365 </div>
366 </Row>
367 </Section>
368
369 <Section title="Carousel">
370 <Row label="Daftar template di layar sempit">
371 <div className="w-full max-w-md overflow-hidden px-12">
372 <Carousel>
373 <CarouselContent>
374 {["Hitam", "Krem", "Pink", "Lime"].map((name) => (
375 <CarouselItem key={name} className="basis-1/2">
376 <div className="rounded-card border-2 border-ink bg-cream p-6 text-center font-display text-base font-extrabold">
377 {name}
378 </div>
379 </CarouselItem>
380 ))}
381 </CarouselContent>
382 <CarouselPrevious />
383 <CarouselNext />
384 </Carousel>
385 </div>
386 </Row>
387 </Section>
388 </div>
389 </TooltipProvider>
390 </main>
391 )
392}
393