diff --git a/.changeset/busy-lizards-dance.md b/.changeset/busy-lizards-dance.md
new file mode 100644
index 00000000..84e42432
--- /dev/null
+++ b/.changeset/busy-lizards-dance.md
@@ -0,0 +1,13 @@
+---
+'@example/ui-playground': patch
+'@genseki/react': patch
+---
+
+New components
+- New `dropzone` component & examples
+- New `file-preview` component & examples
+- New `color-picker` component & examples
+- New `slider` component & examples
+- New `checkbox` component & examples
+- New peer dependency color for managing internal color system
+- New peer dependencu react-dropzone for managing file dropzone state
\ No newline at end of file
diff --git a/examples/ui-playground/package.json b/examples/ui-playground/package.json
index 78f5e38c..7865216f 100644
--- a/examples/ui-playground/package.json
+++ b/examples/ui-playground/package.json
@@ -26,6 +26,7 @@
"@tiptap/extension-text-style": "^2.26.3",
"@tiptap/extension-underline": "^2.26.3",
"@tiptap/starter-kit": "^2.26.3",
+ "color": "^5.0.2",
"date-fns": "^4.1.0",
"next": "15.2.2",
"next-themes": "^0.4.6",
diff --git a/examples/ui-playground/src/app/playground/shadcn/checkbox-section.tsx b/examples/ui-playground/src/app/playground/shadcn/checkbox-section.tsx
new file mode 100644
index 00000000..f66e41e9
--- /dev/null
+++ b/examples/ui-playground/src/app/playground/shadcn/checkbox-section.tsx
@@ -0,0 +1,344 @@
+import React from 'react'
+
+import { CheckIcon, MinusIcon, XIcon } from '@phosphor-icons/react'
+
+import { Checkbox, Label, Typography } from '@genseki/react/v2'
+
+import { PlaygroundCard } from '~/src/components/card'
+
+// Basic Checkbox
+function BasicCheckbox() {
+ return (
+
+
+ Square
+
+
+
+ Accept terms and conditions (Default variant)
+
+
+
+
+ Accept terms and conditions (Default gvariant with undetermine icon)
+
+
+
+
+
+ Accept terms and conditions (Incorrect variant)
+
+
+
+
+
+ Accept terms and conditions (Correct variant)
+
+
+
+ Rounded
+
+
+
+
+ Accept terms and conditions (Default variant)
+
+
+
+
+
+ Accept terms and conditions (Default gvariant with undetermine icon)
+
+
+
+
+
+ Accept terms and conditions (Incorrect variant)
+
+
+
+
+
+ Accept terms and conditions (Correct variant)
+
+
+
+ )
+}
+
+// Checked State
+function CheckedCheckbox() {
+ const [checked, setChecked] = React.useState(true)
+
+ return (
+
+ {
+ setChecked(value === true || false)
+ }}
+ />
+ This is checked by default
+
+ )
+}
+
+// Disabled Checkbox
+function DisabledCheckbox() {
+ return (
+
+
+
+ Disabled unchecked
+
+
+
+ Disabled checked
+
+
+ )
+}
+
+// Checkbox with Error State
+function CheckboxWithError() {
+ return (
+
+
+
+ This field has an error
+
+
+ You must accept the terms to continue
+
+
+ )
+}
+
+// Checkbox Group
+function CheckboxGroup() {
+ const [items, setItems] = React.useState({
+ apples: false,
+ oranges: false,
+ bananas: false,
+ })
+
+ return (
+
+
Select your favorite fruits:
+
+
+ setItems({ ...items, apples: checked as boolean })}
+ />
+ Apples
+
+
+ setItems({ ...items, oranges: checked as boolean })}
+ />
+ Oranges
+
+
+ setItems({ ...items, bananas: checked as boolean })}
+ />
+ Bananas
+
+
+
+ )
+}
+
+// Controlled Checkbox
+function ControlledCheckbox() {
+ const [agreed, setAgreed] = React.useState(false)
+
+ return (
+
+
+ setAgreed(value === true || false)}
+ />
+
+ I agree to the terms and conditions. I understand that this is a binding agreement.
+
+
+
+ Status: {agreed ? 'Agreed' : 'Not agreed'}
+
+
+ )
+}
+
+// Checkbox with Description
+function CheckboxWithDescription() {
+ return (
+
+
+
+
+
+ Subscribe to newsletter
+
+
+ Get notified about new features and updates.
+
+
+
+
+ )
+}
+
+// Custom Styled Checkbox
+function CustomStyledCheckbox() {
+ return (
+
+
+
+ Custom styled checkbox
+
+
+ )
+}
+
+// All Checkbox States
+function AllCheckboxStates() {
+ return (
+
+
+
+ Unchecked
+
+
+
+ Checked
+
+
+
+ Indeterminate
+
+
+
+ Focused (click to see)
+
+
+ )
+}
+
+export function CheckboxSection() {
+ return (
+ <>
+
+
+
+ A basic checkbox input with a label.
+
+
+
+
+
+
+
+
+ A checkbox that is checked by default with controlled state.
+
+
+
+
+
+
+
+
+ Checkbox in disabled state, both checked and unchecked.
+
+
+
+
+
+
+
+
+ Checkbox in error state with validation feedback.
+
+
+
+
+
+
+
+
+ Multiple checkboxes used together in a group.
+
+
+
+
+
+
+
+
+ Checkbox with controlled state and real-time status feedback.
+
+
+
+
+
+
+
+
+ Checkbox with additional descriptive text.
+
+
+
+
+
+
+
+
+ Checkbox with custom styling for checked and unchecked states.
+
+
+
+
+
+
+
+
+ Demonstration of all possible checkbox states.
+
+
+
+
+ >
+ )
+}
diff --git a/examples/ui-playground/src/app/playground/shadcn/color-picker-section.tsx b/examples/ui-playground/src/app/playground/shadcn/color-picker-section.tsx
new file mode 100644
index 00000000..6fc8e423
--- /dev/null
+++ b/examples/ui-playground/src/app/playground/shadcn/color-picker-section.tsx
@@ -0,0 +1,362 @@
+import React, { startTransition, useState } from 'react'
+
+import { type ColorLike } from 'color'
+
+import {
+ Button,
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+ Typography,
+ useColorPicker,
+} from '@genseki/react/v2'
+import {
+ ColorPicker,
+ ColorPickerAlpha,
+ ColorPickerEyeDropper,
+ ColorPickerFormat,
+ ColorPickerHue,
+ ColorPickerOutput,
+ ColorPickerSelection,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@genseki/react/v2'
+
+import { InformationCard, PlaygroundCard } from '~/src/components/card'
+
+// Basic Color Picker
+function BasicColorPicker() {
+ const [color, setColor] = useState([0, 0, 0, 1]) // RGBA - [r, g, b, alpha]
+
+ return (
+
+
{
+ startTransition(() => setColor(v)) // This is a trick to optimize
+ }}
+ className="max-w-sm rounded-md border bg-background p-4 shadow-sm"
+ >
+
+
+
+
+
+
+
+
+ Selected color: rgba({color.toString()})
+
+
+ )
+}
+
+function CustomSelector() {
+ const { mode, setMode } = useColorPicker() // You can use `useColorPicker` under `ColorPicker`
+ const formats = ['hex', 'rgb', 'css', 'hsl']
+
+ return (
+
+
+
+ {mode.toUpperCase()}
+
+
+
+ {formats.map((format) => (
+
+ {format.toUpperCase()}
+
+ ))}
+
+
+ )
+}
+
+// Compact Color Picker
+function CompactColorPicker() {
+ const [color, setColor] = React.useState([255, 0, 0, 1])
+
+ return (
+
+
{
+ startTransition(() => setColor(v))
+ }}
+ >
+
+
+
+
+
+
+
+
+ )
+}
+
+// Large Color Picker
+function LargeColorPicker() {
+ const [color, setColor] = React.useState([255, 255, 0, 1])
+
+ return (
+
+
{
+ startTransition(() => setColor(v))
+ }}
+ >
+
+
+
+
+
+
+
+
+ )
+}
+
+// Color Picker with Preset Colors
+function ColorPickerWithPresets() {
+ const [color, setColor] = React.useState([255, 0, 0, 1])
+
+ const presets = [
+ [255, 0, 0, 1], // Red
+ [255, 255, 0, 1], // Yellow
+ [0, 255, 0, 1], // Green
+ [0, 255, 255, 1], // Cyan
+ [0, 0, 255, 1], // Blue
+ [255, 0, 255, 1], // Magenta
+ [128, 128, 128, 1], // Gray
+ [0, 0, 0, 1], // Black
+ ]
+
+ return (
+
+
{
+ startTransition(() => setColor(v))
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ Preset Colors:
+
+
+ {presets.map((preset, index) => (
+ setColor(preset as ColorLike)}
+ />
+ ))}
+
+
+
+ )
+}
+
+// Color Picker Without EyeDropper
+function ColorPickerWithoutEyeDropper() {
+ const [color, setColor] = React.useState([255, 140, 0, 1])
+
+ return (
+
+
{
+ startTransition(() => setColor(v))
+ }}
+ >
+
+
+
+
+
+
+
+
+ )
+}
+
+// Color Picker with Popover
+function ColorPickerWithPopover() {
+ const [color, setColor] = React.useState([255, 0, 150, 1])
+ const [isOpen, setIsOpen] = React.useState(false)
+
+ return (
+
+
+
+
+
+ Pick a Color
+
+
+
+ {
+ startTransition(() => setColor(v))
+ }}
+ className="gap-6"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ Current: rgba({color.toString()})
+
+
+
+ )
+}
+
+export function ColorPickerSection() {
+ return (
+ <>
+
+
+ You can use the
+
+ useColorPicker
+
+ under the{' '}
+
+ {' '}
+
+ component
+
+
+
+
+ A complete color picker with selection area, hue/alpha sliders, and output formats.
+
+
+
+
+
+
+
+
+ A more compact version of the color picker for space-constrained layouts.
+
+
+
+
+
+
+
+
+ A larger color picker with increased selection area for better precision.
+
+
+
+
+
+
+
+
+ Color picker with preset color swatches for quick selection.
+
+
+
+
+
+
+
+
+ Color picker without the eye dropper tool for simpler interfaces.
+
+
+
+
+
+
+
+
+ Color picker inside a popover triggered by a button with live preview.
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/examples/ui-playground/src/app/playground/shadcn/date-picker-section.tsx b/examples/ui-playground/src/app/playground/shadcn/date-picker-section.tsx
index 1bb8bfb4..2f251000 100644
--- a/examples/ui-playground/src/app/playground/shadcn/date-picker-section.tsx
+++ b/examples/ui-playground/src/app/playground/shadcn/date-picker-section.tsx
@@ -1,7 +1,7 @@
import React, { useState } from 'react'
import type { DateRange } from 'react-day-picker'
-import { CalendarDotsIcon, ClockIcon, WarningIcon } from '@phosphor-icons/react'
+import { CalendarDotsIcon, ClockIcon } from '@phosphor-icons/react'
import {
addDays,
endOfMonth,
@@ -27,7 +27,7 @@ import {
Typography,
} from '@genseki/react/v2'
-import { PlaygroundCard } from '../../../components/card'
+import { InformationCard, PlaygroundCard } from '../../../components/card'
function BasicDatePicker() {
const [date, setDate] = useState()
@@ -534,12 +534,9 @@ function DisabledDates() {
export function DatePickerSection() {
return (
-
-
+
Our example use{' '}
-
- Popover
- ,{' '}
+ Popover ,{' '}
PopoverContent
{' '}
@@ -559,7 +556,7 @@ export function DatePickerSection() {
DatePickerTrigger
-
+
A simple default datepicker
diff --git a/examples/ui-playground/src/app/playground/shadcn/drop-zone-section.tsx b/examples/ui-playground/src/app/playground/shadcn/drop-zone-section.tsx
new file mode 100644
index 00000000..beedcbf9
--- /dev/null
+++ b/examples/ui-playground/src/app/playground/shadcn/drop-zone-section.tsx
@@ -0,0 +1,450 @@
+import React, { Fragment, useState } from 'react'
+
+import { XIcon } from '@phosphor-icons/react'
+import { useRouter } from 'next/navigation'
+
+import {
+ Button,
+ DropZoneArea,
+ DropZoneEmptyContent,
+ DropZoneEmptyUploadButton,
+ DropZoneEmptyUploadCaption,
+ DropZoneEmptyUploadDescription,
+ DropZoneNonemptyContent,
+ FilePreview,
+ FilePreviewAddons,
+ FilePreviewContent,
+ FilePreviewStatus,
+ FilePreviewThumbnail,
+ FilePreviewTitle,
+ Typography,
+ useDropZone,
+} from '@genseki/react/v2'
+import { DropzoneProvider } from '@genseki/react/v2'
+
+import { InformationCard, PlaygroundCard } from '~/src/components/card'
+
+function CustomDropzoneContent(props: { removePreview: () => void }) {
+ const fullPreview = false // Try change this flag
+ const dropzoneCtx = useDropZone()
+ const router = useRouter()
+
+ const isValidPreview = dropzoneCtx.previews.every((preview) => {
+ return (
+ preview.type.startsWith('image/') ||
+ preview.type.startsWith('video/') ||
+ preview.type.startsWith('audio/')
+ )
+ })
+
+ if (!isValidPreview) {
+ return (
+
+
+
+
+
+ Unsupported file type
+
+
+ )
+ }
+
+ if (isValidPreview && !fullPreview) {
+ return (
+ // Handle more customization by yourselves here
+
+ {dropzoneCtx.previews.map((preview) => (
+
+
+ {/*
+ * You can pass a preview as a children of FilePreviewThumbnail
+ *
+ *
+ *
+ */}
+
+ PDF 2024-my-portfolioss.pdf
+
+
+ props.removePreview()}
+ className="px-4 py-2 rounded-sm bg-surface-primary-hover text-text-secondary cursor-pointer"
+ >
+
+ Change
+
+
+ props.removePreview()}
+ className="px-4 py-2 rounded-sm bg-surface-incorrect text-text-incorrect-bold cursor-pointer"
+ >
+
+ Remove
+
+
+
+
+
+ ))}
+
+ )
+ }
+
+ // This is an another preview example
+ return (
+
+
{
+ props.removePreview()
+ router.replace('#drop-zone', { scroll: true })
+ }}
+ >
+
+ Remove
+
+ {dropzoneCtx.previews?.map(({ type, url }) => (
+
+ {type.startsWith('image/') ? (
+ // eslint-disable-next-line @next/next/no-img-element
+
URL.revokeObjectURL(url)}
+ className="rounded-md"
+ />
+ ) : type.startsWith('video/') ? (
+
+ URL.revokeObjectURL(url)} />
+
+ ) : type.startsWith('audio/') ? (
+
+ URL.revokeObjectURL(url)} />
+
+ ) : null}
+
+ ))}
+
+ )
+}
+
+// Custom Dropzone
+function CustomDropzone() {
+ const [files, setFiles] = useState([])
+ const [previews, setPreviews] = useState<{ type: string; url: string }[]>([])
+
+ const handleDrop = (acceptedFiles: File[]) => {
+ setFiles(acceptedFiles)
+ const newPreviews = acceptedFiles.map((file) => ({
+ type: file.type,
+ url: URL.createObjectURL(file),
+ }))
+ setPreviews(newPreviews)
+ }
+
+ const handleError = (error: Error) => {
+ console.log(error)
+ }
+
+ const removePreview = () => {
+ setFiles([])
+ setPreviews([])
+ }
+
+ return (
+
+
+
+ {/* These content will be displayed if no previewable item */}
+
+
+
+
+
+
+
+
+ {/* These content will be displayed if there're available previewable item */}
+
+
+
+ )
+}
+
+// Dropzone with Image Preview
+function DropzoneWithImagePreview() {
+ const [files, setFiles] = useState([])
+ const [previews, setPreviews] = useState<{ type: string; url: string }[]>([])
+
+ const handleDrop = (acceptedFiles: File[]) => {
+ setFiles(acceptedFiles)
+ const newPreviews = acceptedFiles.map((file) => ({
+ type: file.type,
+ url: URL.createObjectURL(file),
+ }))
+ setPreviews(newPreviews)
+ }
+
+ const handleError = (error: Error) => {
+ console.error(error)
+ }
+
+ const removePreview = () => {
+ setFiles([])
+ setPreviews([])
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {previews.map((preview) => (
+
+ {preview.type.startsWith('image/') && (
+ // eslint-disable-next-line @next/next/no-img-element
+
URL.revokeObjectURL(preview.url)}
+ />
+ )}
+
+
+ Remove
+
+
+ ))}
+
+
+
+ )
+}
+
+// Multiple File Dropzone
+function MultipleFileDropzone() {
+ const [files, setFiles] = useState([])
+ const [previews, setPreviews] = useState<{ type: string; url: string }[]>([])
+
+ const handleDrop = (acceptedFiles: File[]) => {
+ setFiles(acceptedFiles)
+ const newPreviews = acceptedFiles.map((file) => ({
+ type: file.type,
+ url: URL.createObjectURL(file),
+ }))
+ setPreviews(newPreviews)
+ }
+
+ const handleError = (error: Error) => {
+ console.error(error)
+ }
+
+ const removePreview = (index: number) => {
+ setFiles(files.filter((_, i) => i !== index))
+ setPreviews(previews.filter((_, i) => i !== index))
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {previews.map((preview, index) => (
+
+
+
+
+ {files[index]?.name || `File ${index + 1}`}
+
+
+ removePreview(index)}
+ className="px-4 py-2 rounded-sm bg-surface-incorrect text-text-incorrect-bold cursor-pointer"
+ >
+
+ Remove
+
+
+
+
+
+
+ ))}
+
+
+
+ )
+}
+
+// Dropzone with File Size Validation
+function DropzoneWithValidation() {
+ const [files, setFiles] = useState([])
+ const [previews, setPreviews] = useState<{ type: string; url: string }[]>([])
+ const [error, setError] = useState(null)
+
+ const handleDrop = (acceptedFiles: File[]) => {
+ setFiles(acceptedFiles)
+ setError(null)
+ const newPreviews = acceptedFiles.map((file) => ({
+ type: file.type,
+ url: URL.createObjectURL(file),
+ }))
+ setPreviews(newPreviews)
+ }
+
+ const handleError = (error: Error) => {
+ setError(error.message)
+ }
+
+ const removePreview = () => {
+ setFiles([])
+ setPreviews([])
+ setError(null)
+ }
+
+ return (
+
+
+
+
+
+
+
+ {error && {error} }
+
+
+
+
+
+ {previews.map((preview, index) => (
+
+
+
+ {files[index]?.name || 'Uploaded file'}
+
+
+
+
+ Remove
+
+
+
+
+
+ ))}
+
+
+
+ )
+}
+
+export function DropZoneSection() {
+ return (
+ <>
+
+
+ You can use the
+
+ useDropZone
+ {' '}
+ under the{' '}
+
+ {' '}
+ {' '}
+ component to manage the file
+
+
+
+ A basic dropzone for file uploads with drag and drop functionality.
+
+
+
+
+
+
+
+
+ Dropzone that accepts only image files and displays a preview with hover effects.
+
+
+
+
+
+
+
+
+ Dropzone that allows uploading multiple files at once with individual file management.
+
+
+
+
+
+
+
+
+ Dropzone with file size validation showing error messages for oversized files.
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/examples/ui-playground/src/app/playground/shadcn/page-sidebar.tsx b/examples/ui-playground/src/app/playground/shadcn/page-sidebar.tsx
index fd22c53c..29f2c4ad 100644
--- a/examples/ui-playground/src/app/playground/shadcn/page-sidebar.tsx
+++ b/examples/ui-playground/src/app/playground/shadcn/page-sidebar.tsx
@@ -17,7 +17,10 @@ import {
const navigationItems = [
{ href: '#button', label: 'Button' },
{ href: '#combobox', label: 'Combobox' },
+ { href: '#checkbox', label: 'Checkbox' },
+ { href: '#color-picker', label: 'Color picker' },
{ href: '#date-picker', label: 'Date picker' },
+ { href: '#drop-zone', label: 'Drop zone' },
{ href: '#dialog', label: 'Dialog' },
{ href: '#input', label: 'Input' },
{ href: '#input-otp', label: 'Input OTP' },
@@ -25,6 +28,7 @@ const navigationItems = [
{ href: '#pagination', label: 'Pagination' },
{ href: '#progress', label: 'Progress' },
{ href: '#select', label: 'Select' },
+ { href: '#slider', label: 'Slider' },
{ href: '#switch', label: 'Switch' },
{ href: '#tabs', label: 'Tabs' },
{ href: '#tooltip', label: 'Tooltip' },
diff --git a/examples/ui-playground/src/app/playground/shadcn/page.tsx b/examples/ui-playground/src/app/playground/shadcn/page.tsx
index 21362957..912b9bb4 100644
--- a/examples/ui-playground/src/app/playground/shadcn/page.tsx
+++ b/examples/ui-playground/src/app/playground/shadcn/page.tsx
@@ -4,10 +4,13 @@ import * as React from 'react'
import { Typography } from '@genseki/react/v2'
import { ButtonSection } from './button-section'
+import { CheckboxSection } from './checkbox-section'
import { CollapsibleSection } from './collapsible-section'
+import { ColorPickerSection } from './color-picker-section'
import { ComboboxSection } from './combobox-section'
import { DatePickerSection } from './date-picker-section'
import { DialogSection } from './dialog-section'
+import { DropZoneSection } from './drop-zone-section'
import { DropdownMenuSection } from './dropdown-menu-section'
import { InputOtpSection } from './input-otp-section'
import { InputSection } from './input-section'
@@ -16,6 +19,7 @@ import PageSidebar from './page-sidebar'
import { PaginationSection } from './pagination-section'
import { ProgressSection } from './progress-section'
import { SelectSection } from './select-section'
+import { SliderSection } from './slider-section'
import { SwitchSection } from './switch-section'
import { TabsSection } from './tabs-section'
import { TextareaSection } from './textarea-section'
@@ -36,6 +40,14 @@ export default function ComboboxPage() {
Button
+
+ Checkbox
+
+
+
+ Color Picker
+
+
Combobox
@@ -48,6 +60,10 @@ export default function ComboboxPage() {
Dialog
+
+ Drop Zone
+
+
Input
@@ -72,6 +88,10 @@ export default function ComboboxPage() {
Select
+
+ Slider
+
+
Switch
diff --git a/examples/ui-playground/src/app/playground/shadcn/slider-section.tsx b/examples/ui-playground/src/app/playground/shadcn/slider-section.tsx
new file mode 100644
index 00000000..08e27362
--- /dev/null
+++ b/examples/ui-playground/src/app/playground/shadcn/slider-section.tsx
@@ -0,0 +1,285 @@
+import React from 'react'
+
+import { Label, Slider, Typography } from '@genseki/react/v2'
+
+import { PlaygroundCard } from '~/src/components/card'
+
+// Basic Slider
+function BasicSlider() {
+ const [value, setValue] = React.useState([50])
+
+ return (
+
+
+ Value
+
+ {value[0]}%
+
+
+
+
+ )
+}
+
+// Slider with Range
+function SliderWithRange() {
+ const [value, setValue] = React.useState([20, 80])
+
+ return (
+
+
+ Range
+
+ {value[0]}% - {value[1]}%
+
+
+
+
+ )
+}
+
+// Slider with Steps
+function SliderWithSteps() {
+ const [value, setValue] = React.useState([50])
+
+ return (
+
+
+ Volume
+
+ {value[0]}%
+
+
+
+
+ )
+}
+
+// Slider with Min/Max
+function SliderWithMinMax() {
+ const [value, setValue] = React.useState([25])
+
+ return (
+
+
+ Temperature (°C)
+
+ {value[0]}°C
+
+
+
+
+ )
+}
+
+// Disabled Slider
+function DisabledSlider() {
+ const [value] = React.useState([50])
+
+ return (
+
+
+ Disabled
+
+ {value[0]}%
+
+
+
+
+ )
+}
+
+// Vertical Slider
+function VerticalSlider() {
+ const [value, setValue] = React.useState([50])
+
+ return (
+
+
+ Vertical Slider
+
+ {value[0]}%
+
+
+
+
+
+
+ )
+}
+
+// Slider with Marks
+function SliderWithMarks() {
+ const [value, setValue] = React.useState([50])
+
+ return (
+
+
+ Rating
+
+ {value[0]}
+
+
+
+
+ 0
+ 1
+ 2
+ 3
+ 4
+ 5
+
+
+ )
+}
+
+// Slider with Custom Styling
+function SliderWithCustomStyling() {
+ const [value, setValue] = React.useState([75])
+
+ return (
+
+
+ Custom Styled Slider
+
+ {value[0]}%
+
+
+
+
+ )
+}
+
+// Multiple Sliders
+function MultipleSliders() {
+ const [volume, setVolume] = React.useState([50])
+ const [brightness, setBrightness] = React.useState([75])
+ const [contrast, setContrast] = React.useState([60])
+
+ return (
+
+
+
+ Volume
+
+ {volume[0]}%
+
+
+
+
+
+
+
+ Brightness
+
+ {brightness[0]}%
+
+
+
+
+
+
+
+ Contrast
+
+ {contrast[0]}%
+
+
+
+
+
+ )
+}
+
+export function SliderSection() {
+ return (
+ <>
+
+
+
+ A basic slider with a single value.
+
+
+
+
+
+
+
+
+ A slider with two thumbs for selecting a range of values.
+
+
+
+
+
+
+
+
+ A slider with discrete step increments.
+
+
+
+
+
+
+
+
+ A slider with custom minimum and maximum values.
+
+
+
+
+
+
+
+
+ A disabled slider that cannot be interacted with.
+
+
+
+
+
+
+
+
+ A vertical orientation slider.
+
+
+
+
+
+
+
+
+ A slider with step marks and labels below.
+
+
+
+
+
+
+
+
+ A slider with custom colors and styling.
+
+
+
+
+
+
+
+
+ Multiple independent sliders for different settings.
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/examples/ui-playground/src/components/card.tsx b/examples/ui-playground/src/components/card.tsx
index d0fca1bc..4d63c516 100644
--- a/examples/ui-playground/src/components/card.tsx
+++ b/examples/ui-playground/src/components/card.tsx
@@ -1,5 +1,6 @@
import type React from 'react'
+import { type Icon, WarningIcon } from '@phosphor-icons/react'
import { StarIcon } from '@phosphor-icons/react/dist/ssr'
import { Typography } from '@genseki/react/v2'
@@ -45,3 +46,18 @@ export const PlaygroundCard = ({
)
}
+
+export function InformationCard({
+ icon: Icon = WarningIcon,
+ ...props
+}: {
+ icon?: Icon
+ children?: React.ReactNode
+}) {
+ return (
+
+
+ {props.children}
+
+ )
+}
diff --git a/internals/project-config/tsconfig/base.json b/internals/project-config/tsconfig/base.json
index 6eecfeb5..f4b4a825 100644
--- a/internals/project-config/tsconfig/base.json
+++ b/internals/project-config/tsconfig/base.json
@@ -18,7 +18,7 @@
"skipLibCheck": true,
"strict": true,
"resolveJsonModule": true,
- "verbatimModuleSyntax": true,
+ "verbatimModuleSyntax": true
},
"exclude": ["node_modules"]
}
diff --git a/packages/react/package.json b/packages/react/package.json
index 0ee153ae..4ef95a94 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -52,6 +52,7 @@
"@intentui/icons": "^1.10.31",
"@internationalized/date": "^3.8.2",
"@phosphor-icons/react": "^2.1.8",
+ "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -60,6 +61,7 @@
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
@@ -115,16 +117,20 @@
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.3.4",
+ "color": "^5.0.2",
"react-day-picker": "^9.11.1",
+ "react-dropzone": "^14.3.8",
"tsup": "^8.5.0",
"type-fest": "^4.38.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.3"
},
"peerDependencies": {
+ "color": "^5.0.2",
"react": "^19.1.0",
"react-day-picker": "^9.11.1",
"react-dom": "^19.1.0",
+ "react-dropzone": "^14.3.8",
"zod": "^4.0.11"
}
}
diff --git a/packages/react/src/react/components/primitives/checkbox.tsx b/packages/react/src/react/components/primitives/checkbox.tsx
index 45ce8bb0..3bc38b9e 100644
--- a/packages/react/src/react/components/primitives/checkbox.tsx
+++ b/packages/react/src/react/components/primitives/checkbox.tsx
@@ -21,12 +21,18 @@ import { composeTailwindRenderProps } from './primitive'
import { cn } from '../../utils/cn'
+/**
+ * @deprecated
+ */
interface CheckboxGroupProps extends CheckboxGroupPrimitiveProps {
label?: string
description?: string
errorMessage?: string | ((validation: ValidationResult) => string)
}
+/**
+ * @deprecated
+ */
const CheckboxGroup = forwardRef(function CheckboxGroup(
{ className, ...props }: CheckboxGroupProps,
ref: React.ForwardedRef
@@ -45,6 +51,9 @@ const CheckboxGroup = forwardRef(function CheckboxGroup(
)
})
+/**
+ * @deprecated
+ */
const checkboxStyles = tv({
base: 'group flex items-center gap-2 text-sm transition',
variants: {
@@ -54,6 +63,9 @@ const checkboxStyles = tv({
},
})
+/**
+ * @deprecated
+ */
const boxStyles = tv({
base: 'inset-ring inset-ring-fg/10 flex size-8 shrink-0 items-center justify-center rounded-sm text-bg transition *:data-[slot=icon]:size-6',
variants: {
@@ -76,6 +88,9 @@ const boxStyles = tv({
},
})
+/**
+ * @deprecated
+ */
interface CheckboxProps extends CheckboxPrimitiveProps {
description?: string
errorMessage?: string
@@ -84,6 +99,9 @@ interface CheckboxProps extends CheckboxPrimitiveProps {
isTextCenter?: boolean
}
+/**
+ * @deprecated
+ */
const Checkbox = forwardRef(function Checkbox(
{ className, size = 'md', isTextCenter = true, ...props }: CheckboxProps,
ref: React.ForwardedRef
diff --git a/packages/react/src/react/components/primitives/color-picker.tsx b/packages/react/src/react/components/primitives/color-picker.tsx
index a96cf78f..49c664d7 100644
--- a/packages/react/src/react/components/primitives/color-picker.tsx
+++ b/packages/react/src/react/components/primitives/color-picker.tsx
@@ -22,6 +22,9 @@ import { Popover, PopoverContent, type PopoverContentProps } from './popover'
import { BaseIcon } from '../../components/primitives/base-icon'
import { cn } from '../../utils/cn'
+/**
+ * @deprecated
+ */
interface ColorPickerProps
extends ColorPickerPrimitiveProps,
Pick {
@@ -36,6 +39,9 @@ interface ColorPickerProps
onPopupOpenChange?: (open: boolean) => void
}
+/**
+ * @deprecated
+ */
const ColorPicker = ({
showArrow = false,
placement = 'bottom start',
@@ -103,10 +109,16 @@ const ColorPicker = ({
declare global {
interface Window {
+ /**
+ * @deprecated
+ */
EyeDropper?: new () => { open: () => Promise<{ sRGBHex: string }> }
}
}
+/**
+ * @deprecated
+ */
const EyeDropper = () => {
const state = use(ColorPickerStateContext)!
diff --git a/packages/react/src/react/components/primitives/color-slider.tsx b/packages/react/src/react/components/primitives/color-slider.tsx
index a661159f..c9d3d685 100644
--- a/packages/react/src/react/components/primitives/color-slider.tsx
+++ b/packages/react/src/react/components/primitives/color-slider.tsx
@@ -13,6 +13,9 @@ import { tv } from 'tailwind-variants'
import { ColorThumb } from './color-thumb'
import { Label } from './field'
+/**
+ * @deprecated
+ */
const trackStyles = tv({
base: 'group col-span-2 rounded-lg',
variants: {
@@ -26,11 +29,17 @@ const trackStyles = tv({
},
})
+/**
+ * @deprecated
+ */
interface ColorSliderProps extends ColorSliderPrimitiveProps {
label?: string
showOutput?: boolean
}
+/**
+ * @deprecated
+ */
const colorSliderStyles = tv({
base: 'group relative py-2',
variants: {
@@ -43,6 +52,9 @@ const colorSliderStyles = tv({
},
},
})
+/**
+ * @deprecated
+ */
const ColorSlider = ({ showOutput = true, label, className, ...props }: ColorSliderProps) => {
return (
(
diff --git a/packages/react/src/react/styles/tailwind.css b/packages/react/src/react/styles/tailwind.css
index 2ea7c804..f959a101 100644
--- a/packages/react/src/react/styles/tailwind.css
+++ b/packages/react/src/react/styles/tailwind.css
@@ -161,7 +161,7 @@
--color-icon-secondary: var(--color-bluegray-600);
--color-icon-tertiary: var(--color-bluegray-400);
- --color-icon-disabled: var(--color-bluegray-100);
+ --color-icon-disabled: var(--color-bluegray-300);
--color-icon-brand: var(--color-pumpkin-500);
--color-icon-brand-bold: var(--color-pumpkin-600);
diff --git a/packages/react/v2/components/primitives/checkbox.tsx b/packages/react/v2/components/primitives/checkbox.tsx
new file mode 100644
index 00000000..656fa1c5
--- /dev/null
+++ b/packages/react/v2/components/primitives/checkbox.tsx
@@ -0,0 +1,78 @@
+'use client'
+
+import * as React from 'react'
+
+import { CheckIcon, type Icon } from '@phosphor-icons/react'
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+import { cn } from '../../../src/react/utils/cn'
+
+type Variants = {
+ variant: Record<'default' | 'incorrect' | 'correct', any>
+ shape: Record<'square' | 'rounded', any>
+}
+
+const checkbox = cva(
+ [
+ 'peer ring-offset-2 focus-visible:border-ring focus-visible:ring-ring size-8 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[2px]',
+ 'disabled:cursor-not-allowed data-[state=checked]:disabled:border-border disabled:bg-surface-tertiary data-[state=checked]:disabled:bg-surface-tertiary data-[state=checked]:disabled:text-icon-disabled',
+ 'aria-invalid:ring-destructive dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[state=checked]:aria-invalid:border-destructive',
+ ],
+ {
+ variants: {
+ variant: {
+ default:
+ 'border-input data-[state=checked]:border-none data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:data-[state=checked]:bg-primary',
+ incorrect:
+ 'border-input data-[state=checked]:border-none data-[state=checked]:bg-icon-incorrect data-[state=checked]:text-text-inverse',
+ correct:
+ 'border-input data-[state=checked]:border-none data-[state=checked]:bg-icon-correct data-[state=checked]:text-text-inverse',
+ },
+ shape: {
+ square: '',
+ rounded: 'rounded-full',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ shape: 'square',
+ },
+ }
+)
+
+const checkboxIndicator = cva('', {
+ variants: {
+ variant: { default: null, incorrect: null, correct: null },
+ shape: { square: 'size-7', rounded: 'size-6' },
+ },
+ defaultVariants: {
+ shape: 'square',
+ },
+})
+
+function Checkbox({
+ className,
+ variant,
+ shape,
+ icon: Icon = CheckIcon,
+ ...props
+}: React.ComponentProps &
+ VariantProps & { icon?: Icon }) {
+ return (
+
+
+
+
+
+ )
+}
+
+export { Checkbox }
diff --git a/packages/react/v2/components/primitives/color-picker.tsx b/packages/react/v2/components/primitives/color-picker.tsx
new file mode 100644
index 00000000..ef8af9a2
--- /dev/null
+++ b/packages/react/v2/components/primitives/color-picker.tsx
@@ -0,0 +1,413 @@
+'use client'
+import {
+ type ComponentProps,
+ type HTMLAttributes,
+ memo,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react'
+
+import { EyedropperIcon } from '@phosphor-icons/react'
+import * as Slider from '@radix-ui/react-slider'
+import Color from 'color'
+
+import { Button } from './button'
+import { Input } from './input'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select'
+import { Typography } from './typography'
+
+import { createRequiredContext } from '../../../src/react/hooks/create-required-context'
+import { cn } from '../../../src/react/utils/cn'
+
+interface ColorPickerContextValue {
+ hue: number
+ saturation: number
+ lightness: number
+ alpha: number
+ mode: string
+ setHue: (hue: number) => void
+ setSaturation: (saturation: number) => void
+ setLightness: (lightness: number) => void
+ setAlpha: (alpha: number) => void
+ setMode: (mode: string) => void
+}
+
+export const [ColorPickerContextProvider, useColorPicker] =
+ createRequiredContext('Color picker context')
+
+export type ColorPickerProps = HTMLAttributes & {
+ value?: Parameters[0]
+ defaultValue?: Parameters[0]
+ onChange?: (value: Parameters[0]) => void
+}
+export const ColorPicker = ({
+ value,
+ defaultValue = '#000000',
+ onChange,
+ className,
+ ...props
+}: ColorPickerProps) => {
+ // Handle controlled vs uncontrolled
+ const currentValue = value !== undefined ? value : defaultValue
+ const initialColor = Color(currentValue)
+
+ const [hue, setHue] = useState(() => {
+ try {
+ return initialColor.hue() || 0
+ } catch {
+ return 0
+ }
+ })
+ const [saturation, setSaturation] = useState(() => {
+ try {
+ return initialColor.saturationl() || 100
+ } catch {
+ return 100
+ }
+ })
+ const [lightness, setLightness] = useState(() => {
+ try {
+ return initialColor.lightness() || 50
+ } catch {
+ return 50
+ }
+ })
+ const [alpha, setAlpha] = useState(() => {
+ try {
+ return initialColor.alpha() * 100 || 100
+ } catch {
+ return 100
+ }
+ })
+ const [mode, setMode] = useState('hex')
+ const isInitialMount = useRef(true)
+
+ // Update color when controlled value changes
+ useEffect(() => {
+ if (value !== undefined && !isInitialMount.current) {
+ try {
+ const color = Color(value)
+ setHue(color.hue() || 0)
+ setSaturation(color.saturationl() || 100)
+ setLightness(color.lightness() || 50)
+ setAlpha(color.alpha() * 100 || 100)
+ } catch (error) {
+ console.error('Failed to parse color value:', error)
+ }
+ }
+ }, [value])
+
+ // Notify parent of changes (skip initial mount)
+ useEffect(() => {
+ if (onChange && !isInitialMount.current) {
+ const color = Color.hsl(hue, saturation, lightness).alpha(alpha / 100)
+ const rgba = color.rgb().array()
+ onChange([rgba[0], rgba[1], rgba[2], alpha / 100])
+ }
+ isInitialMount.current = false
+ }, [hue, saturation, lightness, alpha, onChange])
+ return (
+
+
+
+ )
+}
+export type ColorPickerSelectionProps = HTMLAttributes
+export const ColorPickerSelection = memo(({ className, ...props }: ColorPickerSelectionProps) => {
+ const containerRef = useRef(null)
+ const [isDragging, setIsDragging] = useState(false)
+ const [positionX, setPositionX] = useState(0)
+ const [positionY, setPositionY] = useState(0)
+ const { hue, setSaturation, setLightness } = useColorPicker()
+ const backgroundGradient = useMemo(() => {
+ return `linear-gradient(0deg, rgba(0,0,0,1), rgba(0,0,0,0)),
+ linear-gradient(90deg, rgba(255,255,255,1), rgba(255,255,255,0)),
+ hsl(${hue}, 100%, 50%)`
+ }, [hue])
+ const handlePointerMove = useCallback(
+ (event: PointerEvent) => {
+ if (!(isDragging && containerRef.current)) {
+ return
+ }
+ const rect = containerRef.current.getBoundingClientRect()
+ const x = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width))
+ const y = Math.max(0, Math.min(1, (event.clientY - rect.top) / rect.height))
+ setPositionX(x)
+ setPositionY(y)
+ setSaturation(x * 100)
+ const topLightness = x < 0.01 ? 100 : 50 + 50 * (1 - x)
+ const lightness = topLightness * (1 - y)
+ setLightness(lightness)
+ },
+ [isDragging, setSaturation, setLightness]
+ )
+ useEffect(() => {
+ const handlePointerUp = () => setIsDragging(false)
+ if (isDragging) {
+ window.addEventListener('pointermove', handlePointerMove)
+ window.addEventListener('pointerup', handlePointerUp)
+ }
+ return () => {
+ window.removeEventListener('pointermove', handlePointerMove)
+ window.removeEventListener('pointerup', handlePointerUp)
+ }
+ }, [isDragging, handlePointerMove])
+ return (
+ {
+ e.preventDefault()
+ setIsDragging(true)
+ handlePointerMove(e.nativeEvent)
+ }}
+ ref={containerRef}
+ style={{
+ background: backgroundGradient,
+ }}
+ {...props}
+ >
+
+
+ )
+})
+ColorPickerSelection.displayName = 'ColorPickerSelection'
+
+export type ColorPickerHueProps = ComponentProps
+
+export const ColorPickerHue = ({ className, ...props }: ColorPickerHueProps) => {
+ const { hue, setHue } = useColorPicker()
+ return (
+ setHue(hue)}
+ step={1}
+ value={[hue]}
+ {...props}
+ >
+
+
+
+
+
+ )
+}
+export type ColorPickerAlphaProps = ComponentProps
+
+export const ColorPickerAlpha = ({ className, ...props }: ColorPickerAlphaProps) => {
+ const { alpha, setAlpha } = useColorPicker()
+ return (
+ setAlpha(alpha)}
+ step={1}
+ value={[alpha]}
+ {...props}
+ >
+
+
+
+
+
+
+ )
+}
+export type ColorPickerEyeDropperProps = ComponentProps
+export const ColorPickerEyeDropper = ({ className, ...props }: ColorPickerEyeDropperProps) => {
+ const { setHue, setSaturation, setLightness, setAlpha } = useColorPicker()
+ const handleEyeDropper = async () => {
+ try {
+ // @ts-expect-error - EyeDropper API is experimental
+ const eyeDropper = new EyeDropper()
+ const result = await eyeDropper.open()
+ const color = Color(result.sRGBHex)
+ const [h, s, l] = color.hsl().array()
+ setHue(h)
+ setSaturation(s)
+ setLightness(l)
+ setAlpha(100)
+ } catch (error) {
+ console.error('EyeDropper failed:', error)
+ }
+ }
+ return (
+
+
+
+ )
+}
+
+const formats = ['hex', 'rgb', 'css', 'hsl']
+
+export type ColorPickerOutputProps = ComponentProps
+
+export const ColorPickerOutput = ({ className, ...props }: ColorPickerOutputProps) => {
+ const { mode, setMode } = useColorPicker()
+
+ return (
+
+
+
+ {mode.toUpperCase()}
+
+
+
+ {formats.map((format) => (
+
+ {format.toUpperCase()}
+
+ ))}
+
+
+ )
+}
+type PercentageInputProps = ComponentProps
+const PercentageInput = ({ className, ...props }: PercentageInputProps) => {
+ return (
+
+
+
+ %
+
+
+ )
+}
+export type ColorPickerFormatProps = HTMLAttributes
+export const ColorPickerFormat = ({ className, ...props }: ColorPickerFormatProps) => {
+ const { hue, saturation, lightness, alpha, mode } = useColorPicker()
+ const color = Color.hsl(hue, saturation, lightness, alpha / 100)
+ if (mode === 'hex') {
+ const hex = color.hex()
+ return (
+
+ )
+ }
+ if (mode === 'rgb') {
+ const rgb = color
+ .rgb()
+ .array()
+ .map((value) => Math.round(value))
+ return (
+
+ {rgb.map((value, index) => (
+
+ ))}
+
+
+ )
+ }
+ if (mode === 'css') {
+ const rgb = color
+ .rgb()
+ .array()
+ .map((value) => Math.round(value))
+ return (
+
+
+
+ )
+ }
+ if (mode === 'hsl') {
+ const hsl = color
+ .hsl()
+ .array()
+ .map((value) => Math.round(value))
+ return (
+
+ {hsl.map((value, index) => (
+
+ ))}
+
+
+ )
+ }
+ return null
+}
diff --git a/packages/react/v2/components/primitives/drop-zone.tsx b/packages/react/v2/components/primitives/drop-zone.tsx
new file mode 100644
index 00000000..a9d01205
--- /dev/null
+++ b/packages/react/v2/components/primitives/drop-zone.tsx
@@ -0,0 +1,308 @@
+'use client'
+
+import type { ReactNode } from 'react'
+import React from 'react'
+import type { DropEvent, DropzoneOptions, DropzoneState, FileRejection } from 'react-dropzone'
+import { useDropzone as useReactDropzone } from 'react-dropzone'
+
+import { PaperclipIcon } from '@phosphor-icons/react'
+
+import { Button } from './button'
+import { Typography } from './typography'
+
+import { createRequiredContext } from '../../../src/react/hooks/create-required-context'
+import { cn } from '../../../src/react/utils/cn'
+declare namespace Intl {
+ type ListType = 'conjunction' | 'disjunction'
+
+ interface ListFormatOptions {
+ localeMatcher?: 'lookup' | 'best fit'
+ type?: ListType
+ style?: 'long' | 'short' | 'narrow'
+ }
+
+ interface ListFormatPart {
+ type: 'element' | 'literal'
+ value: string
+ }
+
+ class ListFormat {
+ constructor(locales?: string | string[], options?: ListFormatOptions)
+ format(values: any[]): string
+ formatToParts(values: any[]): ListFormatPart[]
+ supportedLocalesOf(locales: string | string[], options?: ListFormatOptions): string[]
+ }
+}
+
+interface DropzoneContextType extends DropzoneState {
+ src?: File[]
+ previews: { type: string; url: string }[]
+ accept?: DropzoneOptions['accept']
+ maxSize?: DropzoneOptions['maxSize']
+ minSize?: DropzoneOptions['minSize']
+ maxFiles?: DropzoneOptions['maxFiles']
+}
+
+interface FinalDropzoneContextType extends DropzoneContextType {
+ caption: string
+}
+
+const renderBytes = (bytes: number) => {
+ const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
+ let size = bytes
+ let unitIndex = 0
+
+ while (size >= 1024 && unitIndex < units.length - 1) {
+ size /= 1024
+ unitIndex++
+ }
+
+ return `${size.toFixed(2)}${units[unitIndex]}`
+}
+
+export const [_DropzoneProvider, useDropZone] = createRequiredContext<
+ DropzoneContextType,
+ FinalDropzoneContextType
+>('Dropzone context', {
+ valueMapper(value) {
+ let caption = ''
+
+ if (value.accept) {
+ caption += 'Accepts '
+ caption += new Intl.ListFormat('en').format(Object.keys(value.accept))
+ }
+
+ if (value.minSize && value.maxSize) {
+ caption += ` between ${renderBytes(value.minSize)} and ${renderBytes(value.maxSize)}`
+ } else if (value.minSize) {
+ caption += ` at least ${renderBytes(value.minSize)}`
+ } else if (value.maxSize) {
+ caption += ` less than ${renderBytes(value.maxSize)}`
+ }
+
+ return { ...value, caption }
+ },
+})
+
+export interface DropzoneProps extends Omit {
+ id?: string
+ src: File[]
+ previews: { type: string; url: string }[]
+ className?: string
+ onDrop?: (acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent) => void
+ children?: ReactNode
+}
+
+/**
+ * @description Dropzone provider, children under this component can use `useDropZone`
+ * @default maxSize is 20MB
+ */
+export const DropzoneProvider = ({
+ src,
+ accept,
+ maxSize,
+ minSize,
+ maxFiles = 1,
+ previews = [],
+ onDrop,
+ onError,
+ disabled,
+ children,
+ ...props
+}: DropzoneProps) => {
+ const dropzoneCtx = useReactDropzone({
+ accept,
+ maxFiles,
+ maxSize: maxSize ?? 1024 * 1024 * 20, // 20MB
+ minSize,
+ onError,
+ disabled,
+ onDrop: (acceptedFiles, fileRejections, event) => {
+ if (fileRejections.length > 0) {
+ const message = fileRejections.at(0)?.errors.at(0)?.message
+ onError?.(new Error(message))
+ return
+ }
+
+ onDrop?.(acceptedFiles, fileRejections, event)
+ },
+ ...props,
+ })
+
+ return (
+ <_DropzoneProvider
+ src={src}
+ previews={previews}
+ accept={accept}
+ maxSize={maxSize}
+ minSize={minSize}
+ maxFiles={maxFiles}
+ {...dropzoneCtx}
+ >
+ {children}
+
+ )
+}
+
+/**
+ * @description render if no previewable item
+ */
+export const DropZoneNonemptyContent = (props: { children?: React.ReactNode }) => {
+ const dropzoneCtx = useDropZone()
+
+ if (dropzoneCtx.previews.length === 0) return null
+
+ return props.children
+}
+
+/**
+ * @description render if has a previewable item
+ */
+export const DropZoneEmptyContent = (props: { children?: React.ReactNode }) => {
+ const dropzoneCtx = useDropZone()
+
+ if (dropzoneCtx.previews.length > 0) return null
+
+ return props.children
+}
+
+export const DropZoneArea = (props: React.ComponentPropsWithRef<'button'>) => {
+ const dropzoneCtx = useDropZone()
+
+ return (
+
+
+ {props.children}
+
+ )
+}
+
+export type DropzoneContentProps = {
+ children?: ReactNode
+ className?: string
+}
+
+// /**
+// * @description `DropzoneContent` displays content after user upload media,
+// * you can pass a children as a custom renderer and empower with `useDropZone`
+// * hook to access underlying uploaded items data
+// */
+// export const DropzoneContent = ({ children, className }: DropzoneContentProps) => {
+// const { previews } = useDropZone()
+
+// if (previews.length === 0) return null
+
+// if (children) return children
+
+// return (
+//
+// {previews?.map(({ type, url }) => (
+//
+// {type.startsWith('image/') ? (
+// URL.revokeObjectURL(url)} />
+// ) : type.startsWith('video/') ? (
+//
+// URL.revokeObjectURL(url)} />
+//
+// ) : type.startsWith('audio/') ? (
+//
+// URL.revokeObjectURL(url)} />
+//
+// ) : (
+// Unsupported file type
+// )}
+//
+// ))}
+//
+// )
+// }
+
+export type DropzoneEmptyStateProps = {
+ children?: ReactNode
+ className?: string
+}
+
+export const DropZoneEmptyUploadButton = ({
+ className,
+ ...props
+}: React.ComponentPropsWithRef) => {
+ const { maxFiles } = useDropZone()
+
+ return (
+
+
+ Upload {maxFiles === 1 ? 'a file' : 'files'}
+
+ }
+ {...props}
+ />
+ )
+}
+
+export const DropZoneEmptyUploadDescription = ({
+ className,
+ ...props
+}: React.ComponentPropsWithRef) => {
+ return (
+
+ Click to upload or drag and drop PNG, JPG, GIF
+ (max. 2MB)
+ >
+ }
+ {...props}
+ />
+ )
+}
+
+export const DropZoneEmptyUploadCaption = ({
+ className,
+ ...props
+}: React.ComponentPropsWithRef) => {
+ const { caption } = useDropZone()
+
+ if (!caption) return null
+
+ return (
+
+ {caption}.
+
+ )
+}
diff --git a/packages/react/v2/components/primitives/file-preview.tsx b/packages/react/v2/components/primitives/file-preview.tsx
new file mode 100644
index 00000000..9e1b1a68
--- /dev/null
+++ b/packages/react/v2/components/primitives/file-preview.tsx
@@ -0,0 +1,173 @@
+'use client'
+
+import type React from 'react'
+import { useMemo } from 'react'
+
+import {
+ CheckCircleIcon,
+ CircleDashedIcon,
+ CloudArrowUpIcon,
+ FileIcon,
+ XCircleIcon,
+} from '@phosphor-icons/react'
+import { Slot } from '@radix-ui/react-slot'
+
+import { Typography } from './typography'
+
+import { cn } from '../../../src/react/utils/cn'
+
+function FilePreview(props: { children?: React.ReactNode }) {
+ return (
+
+ {props.children}
+
+ )
+}
+
+function FilePreviewThumbnail(props: { children?: React.ReactNode; className?: string }) {
+ if (!props.children)
+ return (
+
+
+
+ )
+
+ return (
+
+ {props.children}
+
+ )
+}
+
+function FilePreviewContent({
+ asChild,
+ className,
+ ...props
+}: React.ComponentPropsWithRef<'div'> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : 'div'
+
+ return (
+
+ )
+}
+
+function FilePreviewTitle(props: { children?: React.ReactNode }) {
+ return (
+
+ {props.children}
+
+ )
+}
+
+function FilePreviewStatus({
+ status,
+ className,
+ classNames,
+ ...props
+}: {
+ status: 'PENDING' | 'COMPLETED' | 'FAILED'
+ className?: string
+ classNames?: {
+ icon?: string
+ text?: string
+ }
+} & React.ComponentPropsWithRef<'div'>) {
+ const content = useMemo(() => {
+ if (status === 'PENDING') {
+ return (
+ <>
+
+
+ Pending
+
+ >
+ )
+ }
+
+ if (status === 'COMPLETED') {
+ return (
+ <>
+
+
+ Completed
+
+ >
+ )
+ }
+
+ if (status === 'FAILED') {
+ return (
+ <>
+
+
+ Failed
+
+ >
+ )
+ }
+
+ return (
+ <>
+
+
+ Failed
+
+ >
+ )
+ }, [classNames?.icon, classNames?.text])
+
+ return (
+
+ {content}
+
+ )
+}
+
+function FilePreviewAddons(props: { className?: string; children?: React.ReactNode }) {
+ return {props.children}
+}
+
+export {
+ FilePreview,
+ FilePreviewAddons,
+ FilePreviewContent,
+ FilePreviewStatus,
+ FilePreviewThumbnail,
+ FilePreviewTitle,
+}
diff --git a/packages/react/v2/components/primitives/index.ts b/packages/react/v2/components/primitives/index.ts
index 7f4eba28..d1191cba 100644
--- a/packages/react/v2/components/primitives/index.ts
+++ b/packages/react/v2/components/primitives/index.ts
@@ -2,12 +2,16 @@ export * from './breadcrumb'
export * from './button'
export * from './button-group'
export * from './calendar'
+export * from './checkbox'
export * from './collapsible'
+export * from './color-picker'
export * from './combobox'
export * from './command'
export * from './date-picker'
export * from './dialog'
+export * from './drop-zone'
export * from './dropdown-menu'
+export * from './file-preview'
export * from './input'
export * from './input-group'
export * from './input-otp'
@@ -21,6 +25,7 @@ export * from './separator'
export * from './sheet'
export * from './sidebar'
export * from './skeleton'
+export * from './slider'
export * from './sonner'
export * from './switch'
export * from './tabs'
diff --git a/packages/react/v2/components/primitives/slider.tsx b/packages/react/v2/components/primitives/slider.tsx
new file mode 100644
index 00000000..866cb4ab
--- /dev/null
+++ b/packages/react/v2/components/primitives/slider.tsx
@@ -0,0 +1,59 @@
+'use client'
+
+import * as React from 'react'
+
+import * as SliderPrimitive from '@radix-ui/react-slider'
+
+import { cn } from '../../../src/react/utils/cn'
+
+function Slider({
+ className,
+ defaultValue,
+ value,
+ min = 0,
+ max = 100,
+ ...props
+}: React.ComponentProps) {
+ const _values = React.useMemo(
+ () => (Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]),
+ [value, defaultValue, min, max]
+ )
+
+ return (
+
+
+
+
+ {Array.from({ length: _values.length }, (_, index) => (
+
+ ))}
+
+ )
+}
+
+export { Slider }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 82bb03aa..4a1434d2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -189,6 +189,9 @@ importers:
'@tiptap/starter-kit':
specifier: ^2.26.3
version: 2.26.3
+ color:
+ specifier: ^5.0.2
+ version: 5.0.2
date-fns:
specifier: ^4.1.0
version: 4.1.0
@@ -448,6 +451,9 @@ importers:
'@phosphor-icons/react':
specifier: ^2.1.8
version: 2.1.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-checkbox':
+ specifier: ^1.3.3
+ version: 1.3.3(@types/react-dom@19.1.6(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-collapsible':
specifier: ^1.1.12
version: 1.1.12(@types/react-dom@19.1.6(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -472,6 +478,9 @@ importers:
'@radix-ui/react-separator':
specifier: ^1.1.7
version: 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-slider':
+ specifier: ^1.3.6
+ version: 1.3.6(@types/react-dom@19.1.6(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot':
specifier: ^1.2.0
version: 1.2.2(@types/react@19.1.6)(react@19.1.0)
@@ -632,9 +641,15 @@ importers:
'@vitejs/plugin-react':
specifier: ^4.3.4
version: 4.4.1(vite@6.3.5(@types/node@24.0.15)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3))
+ color:
+ specifier: ^5.0.2
+ version: 5.0.2
react-day-picker:
specifier: ^9.11.1
version: 9.11.1(react@19.1.0)
+ react-dropzone:
+ specifier: ^14.3.8
+ version: 14.3.8(react@19.1.0)
tsup:
specifier: ^8.5.0
version: 8.5.0(jiti@2.4.2)(postcss@8.5.3)(tsx@4.20.3)(typescript@5.8.2)
@@ -1587,6 +1602,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-checkbox@1.3.3':
+ resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-collapsible@1.1.12':
resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==}
peerDependencies:
@@ -1853,6 +1881,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-slider@1.3.6':
+ resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-slot@1.2.2':
resolution: {integrity: sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==}
peerDependencies:
@@ -3476,6 +3517,10 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
+ attr-accept@2.2.5:
+ resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==}
+ engines: {node: '>=4'}
+
available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
@@ -3593,16 +3638,32 @@ packages:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
+ color-convert@3.1.2:
+ resolution: {integrity: sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==}
+ engines: {node: '>=14.6'}
+
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ color-name@2.0.2:
+ resolution: {integrity: sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==}
+ engines: {node: '>=12.20'}
+
color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
+ color-string@2.1.2:
+ resolution: {integrity: sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==}
+ engines: {node: '>=18'}
+
color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
+ color@5.0.2:
+ resolution: {integrity: sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==}
+ engines: {node: '>=18'}
+
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
@@ -4019,6 +4080,10 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
+ file-selector@2.1.2:
+ resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==}
+ engines: {node: '>= 12'}
+
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -5061,6 +5126,12 @@ packages:
peerDependencies:
react: ^19.1.0
+ react-dropzone@14.3.8:
+ resolution: {integrity: sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==}
+ engines: {node: '>= 10.13'}
+ peerDependencies:
+ react: '>= 16.8 || 18.0.0'
+
react-hook-form@7.56.3:
resolution: {integrity: sha512-IK18V6GVbab4TAo1/cz3kqajxbDPGofdF0w7VHdCo0Nt8PrPlOZcuuDq9YYIV1BtjcX78x0XsldbQRQnQXWXmw==}
engines: {node: '>=18.0.0'}
@@ -7057,6 +7128,22 @@ snapshots:
'@types/react': 19.1.6
'@types/react-dom': 19.1.6(@types/react@19.1.6)
+ '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.1.6(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.6)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.6)(react@19.1.0)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.6(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.6)(react@19.1.0)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.6)(react@19.1.0)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.6)(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.6
+ '@types/react-dom': 19.1.6(@types/react@19.1.6)
+
'@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.1.6(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
@@ -7347,6 +7434,25 @@ snapshots:
'@types/react': 19.1.6
'@types/react-dom': 19.1.6(@types/react@19.1.6)
+ '@radix-ui/react-slider@1.3.6(@types/react-dom@19.1.6(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/number': 1.1.1
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.6)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.6)(react@19.1.0)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.1.6)(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.6)(react@19.1.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.6)(react@19.1.0)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.6)(react@19.1.0)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.6)(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.6
+ '@types/react-dom': 19.1.6(@types/react@19.1.6)
+
'@radix-ui/react-slot@1.2.2(@types/react@19.1.6)(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.6)(react@19.1.0)
@@ -9619,6 +9725,8 @@ snapshots:
async-function@1.0.0: {}
+ attr-accept@2.2.5: {}
+
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.1.0
@@ -9748,20 +9856,35 @@ snapshots:
dependencies:
color-name: 1.1.4
+ color-convert@3.1.2:
+ dependencies:
+ color-name: 2.0.2
+
color-name@1.1.4: {}
+ color-name@2.0.2: {}
+
color-string@1.9.1:
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
optional: true
+ color-string@2.1.2:
+ dependencies:
+ color-name: 2.0.2
+
color@4.2.3:
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
optional: true
+ color@5.0.2:
+ dependencies:
+ color-convert: 3.1.2
+ color-string: 2.1.2
+
commander@4.1.1: {}
concat-map@0.0.1: {}
@@ -10274,6 +10397,10 @@ snapshots:
dependencies:
flat-cache: 4.0.1
+ file-selector@2.1.2:
+ dependencies:
+ tslib: 2.8.1
+
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@@ -11347,6 +11474,13 @@ snapshots:
react: 19.1.0
scheduler: 0.26.0
+ react-dropzone@14.3.8(react@19.1.0):
+ dependencies:
+ attr-accept: 2.2.5
+ file-selector: 2.1.2
+ prop-types: 15.8.1
+ react: 19.1.0
+
react-hook-form@7.56.3(react@19.1.0):
dependencies:
react: 19.1.0