Skip to content

Commit c1dcf67

Browse files
Copilothotlong
andcommitted
Add missing shadcn components: toast, date-picker, combobox
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 71ea7a3 commit c1dcf67

7 files changed

Lines changed: 546 additions & 0 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
"use client"
10+
11+
import {
12+
Toast,
13+
ToastClose,
14+
ToastDescription,
15+
ToastProvider,
16+
ToastTitle,
17+
ToastViewport,
18+
} from "../ui/toast"
19+
import { useToast } from "./use-toast"
20+
21+
export function Toaster() {
22+
const { toasts } = useToast()
23+
24+
return (
25+
<ToastProvider>
26+
{toasts.map(function ({ id, title, description, action, ...props }) {
27+
return (
28+
<Toast key={id} {...props}>
29+
<div className="grid gap-1">
30+
{title && <ToastTitle>{title}</ToastTitle>}
31+
{description && (
32+
<ToastDescription>{description}</ToastDescription>
33+
)}
34+
</div>
35+
{action}
36+
<ToastClose />
37+
</Toast>
38+
)
39+
})}
40+
<ToastViewport />
41+
</ToastProvider>
42+
)
43+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import * as React from "react"
10+
11+
import type {
12+
ToastActionElement,
13+
ToastProps,
14+
} from "../ui/toast"
15+
16+
const TOAST_LIMIT = 1
17+
const TOAST_REMOVE_DELAY = 1000000
18+
19+
type ToasterToast = ToastProps & {
20+
id: string
21+
title?: React.ReactNode
22+
description?: React.ReactNode
23+
action?: ToastActionElement
24+
}
25+
26+
const actionTypes = {
27+
ADD_TOAST: "ADD_TOAST",
28+
UPDATE_TOAST: "UPDATE_TOAST",
29+
DISMISS_TOAST: "DISMISS_TOAST",
30+
REMOVE_TOAST: "REMOVE_TOAST",
31+
} as const
32+
33+
let count = 0
34+
35+
function genId() {
36+
count = (count + 1) % Number.MAX_SAFE_INTEGER
37+
return count.toString()
38+
}
39+
40+
type ActionType = typeof actionTypes
41+
42+
type Action =
43+
| {
44+
type: ActionType["ADD_TOAST"]
45+
toast: ToasterToast
46+
}
47+
| {
48+
type: ActionType["UPDATE_TOAST"]
49+
toast: Partial<ToasterToast>
50+
}
51+
| {
52+
type: ActionType["DISMISS_TOAST"]
53+
toastId?: ToasterToast["id"]
54+
}
55+
| {
56+
type: ActionType["REMOVE_TOAST"]
57+
toastId?: ToasterToast["id"]
58+
}
59+
60+
interface State {
61+
toasts: ToasterToast[]
62+
}
63+
64+
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
65+
66+
const addToRemoveQueue = (toastId: string) => {
67+
if (toastTimeouts.has(toastId)) {
68+
return
69+
}
70+
71+
const timeout = setTimeout(() => {
72+
toastTimeouts.delete(toastId)
73+
dispatch({
74+
type: "REMOVE_TOAST",
75+
toastId: toastId,
76+
})
77+
}, TOAST_REMOVE_DELAY)
78+
79+
toastTimeouts.set(toastId, timeout)
80+
}
81+
82+
export const reducer = (state: State, action: Action): State => {
83+
switch (action.type) {
84+
case "ADD_TOAST":
85+
return {
86+
...state,
87+
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
88+
}
89+
90+
case "UPDATE_TOAST":
91+
return {
92+
...state,
93+
toasts: state.toasts.map((t) =>
94+
t.id === action.toast.id ? { ...t, ...action.toast } : t
95+
),
96+
}
97+
98+
case "DISMISS_TOAST": {
99+
const { toastId } = action
100+
101+
if (toastId) {
102+
addToRemoveQueue(toastId)
103+
} else {
104+
state.toasts.forEach((toast) => {
105+
addToRemoveQueue(toast.id)
106+
})
107+
}
108+
109+
return {
110+
...state,
111+
toasts: state.toasts.map((t) =>
112+
t.id === toastId || toastId === undefined
113+
? {
114+
...t,
115+
open: false,
116+
}
117+
: t
118+
),
119+
}
120+
}
121+
case "REMOVE_TOAST":
122+
if (action.toastId === undefined) {
123+
return {
124+
...state,
125+
toasts: [],
126+
}
127+
}
128+
return {
129+
...state,
130+
toasts: state.toasts.filter((t) => t.id !== action.toastId),
131+
}
132+
}
133+
}
134+
135+
const listeners: Array<(state: State) => void> = []
136+
137+
let memoryState: State = { toasts: [] }
138+
139+
function dispatch(action: Action) {
140+
memoryState = reducer(memoryState, action)
141+
listeners.forEach((listener) => {
142+
listener(memoryState)
143+
})
144+
}
145+
146+
type Toast = Omit<ToasterToast, "id">
147+
148+
function toast({ ...props }: Toast) {
149+
const id = genId()
150+
151+
const update = (props: ToasterToast) =>
152+
dispatch({
153+
type: "UPDATE_TOAST",
154+
toast: { ...props, id },
155+
})
156+
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
157+
158+
dispatch({
159+
type: "ADD_TOAST",
160+
toast: {
161+
...props,
162+
id,
163+
open: true,
164+
onOpenChange: (open) => {
165+
if (!open) dismiss()
166+
},
167+
},
168+
})
169+
170+
return {
171+
id: id,
172+
dismiss,
173+
update,
174+
}
175+
}
176+
177+
function useToast() {
178+
const [state, setState] = React.useState<State>(memoryState)
179+
180+
React.useEffect(() => {
181+
listeners.push(setState)
182+
return () => {
183+
const index = listeners.indexOf(setState)
184+
if (index > -1) {
185+
listeners.splice(index, 1)
186+
}
187+
}
188+
}, [state])
189+
190+
return {
191+
...state,
192+
toast,
193+
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
194+
}
195+
}
196+
197+
export { useToast, toast }

packages/components/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import './renderers';
1414
// Export utils
1515
export * from './lib/utils';
1616

17+
// Export hooks
18+
export * from './hooks/use-toast';
19+
1720
// Export raw Shadcn UI components
1821
export * from './ui';
1922

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
"use client"
10+
11+
import * as React from "react"
12+
import { Check, ChevronsUpDown } from "lucide-react"
13+
14+
import { cn } from "../lib/utils"
15+
import { Button } from "./button"
16+
import {
17+
Command,
18+
CommandEmpty,
19+
CommandGroup,
20+
CommandInput,
21+
CommandItem,
22+
CommandList,
23+
} from "./command"
24+
import {
25+
Popover,
26+
PopoverContent,
27+
PopoverTrigger,
28+
} from "./popover"
29+
30+
export interface ComboboxOption {
31+
value: string
32+
label: string
33+
}
34+
35+
export interface ComboboxProps {
36+
options: ComboboxOption[]
37+
value?: string
38+
onValueChange?: (value: string) => void
39+
placeholder?: string
40+
searchPlaceholder?: string
41+
emptyText?: string
42+
className?: string
43+
disabled?: boolean
44+
}
45+
46+
export function Combobox({
47+
options,
48+
value,
49+
onValueChange,
50+
placeholder = "Select option...",
51+
searchPlaceholder = "Search...",
52+
emptyText = "No option found.",
53+
className,
54+
disabled,
55+
}: ComboboxProps) {
56+
const [open, setOpen] = React.useState(false)
57+
58+
return (
59+
<Popover open={open} onOpenChange={setOpen}>
60+
<PopoverTrigger asChild>
61+
<Button
62+
variant="outline"
63+
role="combobox"
64+
aria-expanded={open}
65+
className={cn("w-[200px] justify-between", className)}
66+
disabled={disabled}
67+
>
68+
{value
69+
? options.find((option) => option.value === value)?.label
70+
: placeholder}
71+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
72+
</Button>
73+
</PopoverTrigger>
74+
<PopoverContent className="w-[200px] p-0">
75+
<Command>
76+
<CommandInput placeholder={searchPlaceholder} />
77+
<CommandList>
78+
<CommandEmpty>{emptyText}</CommandEmpty>
79+
<CommandGroup>
80+
{options.map((option) => (
81+
<CommandItem
82+
key={option.value}
83+
value={option.value}
84+
onSelect={(currentValue) => {
85+
onValueChange?.(currentValue === value ? "" : currentValue)
86+
setOpen(false)
87+
}}
88+
>
89+
<Check
90+
className={cn(
91+
"mr-2 h-4 w-4",
92+
value === option.value ? "opacity-100" : "opacity-0"
93+
)}
94+
/>
95+
{option.label}
96+
</CommandItem>
97+
))}
98+
</CommandGroup>
99+
</CommandList>
100+
</Command>
101+
</PopoverContent>
102+
</Popover>
103+
)
104+
}

0 commit comments

Comments
 (0)