@dilipod/ui 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dilipod/ui",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Dilipod Design System - Shared UI components and styles",
5
5
  "author": "Dilipod <hello@dilipod.com>",
6
6
  "license": "MIT",
@@ -67,6 +67,7 @@
67
67
  "@radix-ui/react-dropdown-menu": "^2.1.3",
68
68
  "@radix-ui/react-navigation-menu": "^1.2.14",
69
69
  "@radix-ui/react-slot": "^1.2.3",
70
+ "@radix-ui/react-toast": "^1.2.15",
70
71
  "class-variance-authority": "^0.7.1",
71
72
  "clsx": "^2.1.1",
72
73
  "tailwind-merge": "^3.3.0"
@@ -3,16 +3,16 @@
3
3
  import * as React from 'react'
4
4
  import { cn } from '../lib/utils'
5
5
  import { Label } from './label'
6
- import { Input } from './input'
7
- import { Textarea } from './textarea'
8
6
 
9
7
  export interface FormFieldProps {
10
8
  /** Field label */
11
9
  label?: string
12
10
  /** Error message to display */
13
11
  error?: string
14
- /** Helper text to display */
12
+ /** Helper text to display below the field */
15
13
  helperText?: string
14
+ /** Hint element to display on the right side of the label (e.g., link) */
15
+ hint?: React.ReactNode
16
16
  /** Whether the field is required */
17
17
  required?: boolean
18
18
  /** Field ID for accessibility */
@@ -24,7 +24,7 @@ export interface FormFieldProps {
24
24
  }
25
25
 
26
26
  const FormField = React.forwardRef<HTMLDivElement, FormFieldProps>(
27
- ({ label, error, helperText, required, id, className, children, ...props }, ref) => {
27
+ ({ label, error, helperText, hint, required, id, className, children, ...props }, ref) => {
28
28
  const fieldId = id || React.useId()
29
29
  const errorId = `${fieldId}-error`
30
30
  const helperId = `${fieldId}-helper`
@@ -48,10 +48,15 @@ const FormField = React.forwardRef<HTMLDivElement, FormFieldProps>(
48
48
 
49
49
  return (
50
50
  <div ref={ref} className={cn('space-y-2', className)} {...props}>
51
- {label && (
52
- <Label htmlFor={fieldId} className={required ? 'after:content-["*"] after:ml-0.5 after:text-red-500' : ''}>
53
- {label}
54
- </Label>
51
+ {(label || hint) && (
52
+ <div className="flex items-center justify-between">
53
+ {label && (
54
+ <Label htmlFor={fieldId} className={required ? 'after:content-["*"] after:ml-0.5 after:text-red-500' : ''}>
55
+ {label}
56
+ </Label>
57
+ )}
58
+ {hint && <div>{hint}</div>}
59
+ </div>
55
60
  )}
56
61
  {enhancedChildren}
57
62
  {error && (
@@ -29,6 +29,12 @@ export interface SidebarProps extends React.HTMLAttributes<HTMLElement> {
29
29
  label?: string
30
30
  icon?: React.ComponentType<React.SVGProps<SVGSVGElement> & { weight?: 'fill' | 'regular' }>
31
31
  }
32
+ /** Optional assistant button configuration */
33
+ assistantButton?: {
34
+ label?: string
35
+ icon?: React.ComponentType<React.SVGProps<SVGSVGElement> & { weight?: 'fill' | 'regular' }>
36
+ onClick?: () => void
37
+ }
32
38
  /** Header content (e.g., Logo) */
33
39
  header?: React.ReactNode
34
40
  /** Custom Link component (e.g., Next.js Link) */
@@ -92,6 +98,7 @@ const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(
92
98
  pathname,
93
99
  searchButton,
94
100
  helpLink,
101
+ assistantButton,
95
102
  header,
96
103
  LinkComponent,
97
104
  className,
@@ -159,7 +166,7 @@ const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(
159
166
  {children}
160
167
 
161
168
  {/* Bottom Navigation */}
162
- {(bottomNav.length > 0 || helpLink) && (
169
+ {(bottomNav.length > 0 || helpLink || assistantButton) && (
163
170
  <div className="px-3 pb-3 space-y-1 border-t pt-3">
164
171
  {bottomNav.map((item) => (
165
172
  <SidebarNavItem
@@ -169,6 +176,17 @@ const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(
169
176
  LinkComponent={LinkComponent}
170
177
  />
171
178
  ))}
179
+ {assistantButton && (
180
+ <button
181
+ onClick={assistantButton.onClick}
182
+ className="flex w-full items-center gap-3 rounded-sm px-3 py-2 text-sm bg-[var(--cyan)]/10 text-[var(--black)] hover:bg-[var(--cyan)]/20 transition-colors font-medium"
183
+ >
184
+ {assistantButton.icon && (
185
+ <assistantButton.icon className="h-4 w-4 text-[var(--cyan)]" />
186
+ )}
187
+ {assistantButton.label}
188
+ </button>
189
+ )}
172
190
  {helpLink && (
173
191
  <a
174
192
  href={helpLink.href}
@@ -0,0 +1,144 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as ToastPrimitives from '@radix-ui/react-toast'
5
+ import { cva, type VariantProps } from 'class-variance-authority'
6
+ import { X, CheckCircle, WarningCircle, Info } from '@phosphor-icons/react'
7
+ import { cn } from '../lib/utils'
8
+
9
+ const ToastProvider = ToastPrimitives.Provider
10
+
11
+ const ToastViewport = React.forwardRef<
12
+ React.ComponentRef<typeof ToastPrimitives.Viewport>,
13
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
14
+ >(({ className, ...props }, ref) => (
15
+ <ToastPrimitives.Viewport
16
+ ref={ref}
17
+ className={cn(
18
+ 'fixed bottom-0 right-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:max-w-[420px]',
19
+ className
20
+ )}
21
+ {...props}
22
+ />
23
+ ))
24
+ ToastViewport.displayName = ToastPrimitives.Viewport.displayName
25
+
26
+ const toastVariants = cva(
27
+ 'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-sm border p-4 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-bottom-full',
28
+ {
29
+ variants: {
30
+ variant: {
31
+ default: 'border-gray-200 bg-white text-[var(--black)]',
32
+ success: 'border-emerald-200 bg-emerald-50 text-emerald-900',
33
+ error: 'border-red-200 bg-red-50 text-red-900',
34
+ warning: 'border-amber-200 bg-amber-50 text-amber-900',
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: 'default',
39
+ },
40
+ }
41
+ )
42
+
43
+ const Toast = React.forwardRef<
44
+ React.ComponentRef<typeof ToastPrimitives.Root>,
45
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
46
+ VariantProps<typeof toastVariants>
47
+ >(({ className, variant, ...props }, ref) => {
48
+ return (
49
+ <ToastPrimitives.Root
50
+ ref={ref}
51
+ className={cn(toastVariants({ variant }), className)}
52
+ {...props}
53
+ />
54
+ )
55
+ })
56
+ Toast.displayName = ToastPrimitives.Root.displayName
57
+
58
+ const ToastAction = React.forwardRef<
59
+ React.ComponentRef<typeof ToastPrimitives.Action>,
60
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
61
+ >(({ className, ...props }, ref) => (
62
+ <ToastPrimitives.Action
63
+ ref={ref}
64
+ className={cn(
65
+ 'inline-flex h-8 shrink-0 items-center justify-center rounded-sm border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
66
+ className
67
+ )}
68
+ {...props}
69
+ />
70
+ ))
71
+ ToastAction.displayName = ToastPrimitives.Action.displayName
72
+
73
+ const ToastClose = React.forwardRef<
74
+ React.ComponentRef<typeof ToastPrimitives.Close>,
75
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
76
+ >(({ className, ...props }, ref) => (
77
+ <ToastPrimitives.Close
78
+ ref={ref}
79
+ className={cn(
80
+ 'absolute right-2 top-2 rounded-sm p-1 text-gray-500 opacity-0 transition-opacity hover:text-gray-900 focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100',
81
+ className
82
+ )}
83
+ toast-close=""
84
+ {...props}
85
+ >
86
+ <X size={16} />
87
+ </ToastPrimitives.Close>
88
+ ))
89
+ ToastClose.displayName = ToastPrimitives.Close.displayName
90
+
91
+ const ToastTitle = React.forwardRef<
92
+ React.ComponentRef<typeof ToastPrimitives.Title>,
93
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
94
+ >(({ className, ...props }, ref) => (
95
+ <ToastPrimitives.Title
96
+ ref={ref}
97
+ className={cn('text-sm font-semibold', className)}
98
+ {...props}
99
+ />
100
+ ))
101
+ ToastTitle.displayName = ToastPrimitives.Title.displayName
102
+
103
+ const ToastDescription = React.forwardRef<
104
+ React.ComponentRef<typeof ToastPrimitives.Description>,
105
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
106
+ >(({ className, ...props }, ref) => (
107
+ <ToastPrimitives.Description
108
+ ref={ref}
109
+ className={cn('text-sm opacity-90', className)}
110
+ {...props}
111
+ />
112
+ ))
113
+ ToastDescription.displayName = ToastPrimitives.Description.displayName
114
+
115
+ type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
116
+
117
+ type ToastActionElement = React.ReactElement<typeof ToastAction>
118
+
119
+ // Toast icon helper
120
+ const ToastIcon = ({ variant }: { variant?: 'default' | 'success' | 'error' | 'warning' }) => {
121
+ switch (variant) {
122
+ case 'success':
123
+ return <CheckCircle size={20} weight="fill" className="text-emerald-600" />
124
+ case 'error':
125
+ return <WarningCircle size={20} weight="fill" className="text-red-600" />
126
+ case 'warning':
127
+ return <WarningCircle size={20} weight="fill" className="text-amber-600" />
128
+ default:
129
+ return <Info size={20} weight="fill" className="text-gray-600" />
130
+ }
131
+ }
132
+
133
+ export {
134
+ type ToastProps,
135
+ type ToastActionElement,
136
+ ToastProvider,
137
+ ToastViewport,
138
+ Toast,
139
+ ToastTitle,
140
+ ToastDescription,
141
+ ToastClose,
142
+ ToastAction,
143
+ ToastIcon,
144
+ }
@@ -0,0 +1,40 @@
1
+ 'use client'
2
+
3
+ import {
4
+ Toast,
5
+ ToastClose,
6
+ ToastDescription,
7
+ ToastIcon,
8
+ ToastProvider,
9
+ ToastTitle,
10
+ ToastViewport,
11
+ } from './toast'
12
+ import { useToast } from './use-toast'
13
+
14
+ export function Toaster() {
15
+ const { toasts } = useToast()
16
+
17
+ return (
18
+ <ToastProvider>
19
+ {toasts.map(function ({ id, title, description, action, variant, ...props }) {
20
+ const safeVariant = variant ?? undefined
21
+ return (
22
+ <Toast key={id} variant={safeVariant} {...props}>
23
+ <div className="flex gap-3">
24
+ <ToastIcon variant={safeVariant} />
25
+ <div className="grid gap-1">
26
+ {title && <ToastTitle>{title}</ToastTitle>}
27
+ {description && (
28
+ <ToastDescription>{description}</ToastDescription>
29
+ )}
30
+ </div>
31
+ </div>
32
+ {action}
33
+ <ToastClose />
34
+ </Toast>
35
+ )
36
+ })}
37
+ <ToastViewport />
38
+ </ToastProvider>
39
+ )
40
+ }
@@ -0,0 +1,187 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import type { ToastActionElement, ToastProps } from './toast'
5
+
6
+ const TOAST_LIMIT = 5
7
+ const TOAST_REMOVE_DELAY = 5000
8
+
9
+ type ToasterToast = ToastProps & {
10
+ id: string
11
+ title?: React.ReactNode
12
+ description?: React.ReactNode
13
+ action?: ToastActionElement
14
+ }
15
+
16
+ const actionTypes = {
17
+ ADD_TOAST: 'ADD_TOAST',
18
+ UPDATE_TOAST: 'UPDATE_TOAST',
19
+ DISMISS_TOAST: 'DISMISS_TOAST',
20
+ REMOVE_TOAST: 'REMOVE_TOAST',
21
+ } as const
22
+
23
+ let count = 0
24
+
25
+ function genId() {
26
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
27
+ return count.toString()
28
+ }
29
+
30
+ type ActionType = typeof actionTypes
31
+
32
+ type Action =
33
+ | {
34
+ type: ActionType['ADD_TOAST']
35
+ toast: ToasterToast
36
+ }
37
+ | {
38
+ type: ActionType['UPDATE_TOAST']
39
+ toast: Partial<ToasterToast>
40
+ }
41
+ | {
42
+ type: ActionType['DISMISS_TOAST']
43
+ toastId?: ToasterToast['id']
44
+ }
45
+ | {
46
+ type: ActionType['REMOVE_TOAST']
47
+ toastId?: ToasterToast['id']
48
+ }
49
+
50
+ interface State {
51
+ toasts: ToasterToast[]
52
+ }
53
+
54
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
55
+
56
+ const addToRemoveQueue = (toastId: string) => {
57
+ if (toastTimeouts.has(toastId)) {
58
+ return
59
+ }
60
+
61
+ const timeout = setTimeout(() => {
62
+ toastTimeouts.delete(toastId)
63
+ dispatch({
64
+ type: 'REMOVE_TOAST',
65
+ toastId: toastId,
66
+ })
67
+ }, TOAST_REMOVE_DELAY)
68
+
69
+ toastTimeouts.set(toastId, timeout)
70
+ }
71
+
72
+ export const reducer = (state: State, action: Action): State => {
73
+ switch (action.type) {
74
+ case 'ADD_TOAST':
75
+ return {
76
+ ...state,
77
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
78
+ }
79
+
80
+ case 'UPDATE_TOAST':
81
+ return {
82
+ ...state,
83
+ toasts: state.toasts.map((t) =>
84
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
85
+ ),
86
+ }
87
+
88
+ case 'DISMISS_TOAST': {
89
+ const { toastId } = action
90
+
91
+ if (toastId) {
92
+ addToRemoveQueue(toastId)
93
+ } else {
94
+ state.toasts.forEach((toast) => {
95
+ addToRemoveQueue(toast.id)
96
+ })
97
+ }
98
+
99
+ return {
100
+ ...state,
101
+ toasts: state.toasts.map((t) =>
102
+ t.id === toastId || toastId === undefined
103
+ ? {
104
+ ...t,
105
+ open: false,
106
+ }
107
+ : t
108
+ ),
109
+ }
110
+ }
111
+ case 'REMOVE_TOAST':
112
+ if (action.toastId === undefined) {
113
+ return {
114
+ ...state,
115
+ toasts: [],
116
+ }
117
+ }
118
+ return {
119
+ ...state,
120
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
121
+ }
122
+ }
123
+ }
124
+
125
+ const listeners: Array<(state: State) => void> = []
126
+
127
+ let memoryState: State = { toasts: [] }
128
+
129
+ function dispatch(action: Action) {
130
+ memoryState = reducer(memoryState, action)
131
+ listeners.forEach((listener) => {
132
+ listener(memoryState)
133
+ })
134
+ }
135
+
136
+ type Toast = Omit<ToasterToast, 'id'>
137
+
138
+ function toast({ ...props }: Toast) {
139
+ const id = genId()
140
+
141
+ const update = (props: ToasterToast) =>
142
+ dispatch({
143
+ type: 'UPDATE_TOAST',
144
+ toast: { ...props, id },
145
+ })
146
+ const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
147
+
148
+ dispatch({
149
+ type: 'ADD_TOAST',
150
+ toast: {
151
+ ...props,
152
+ id,
153
+ open: true,
154
+ onOpenChange: (open) => {
155
+ if (!open) dismiss()
156
+ },
157
+ },
158
+ })
159
+
160
+ return {
161
+ id: id,
162
+ dismiss,
163
+ update,
164
+ }
165
+ }
166
+
167
+ function useToast() {
168
+ const [state, setState] = React.useState<State>(memoryState)
169
+
170
+ React.useEffect(() => {
171
+ listeners.push(setState)
172
+ return () => {
173
+ const index = listeners.indexOf(setState)
174
+ if (index > -1) {
175
+ listeners.splice(index, 1)
176
+ }
177
+ }
178
+ }, [state])
179
+
180
+ return {
181
+ ...state,
182
+ toast,
183
+ dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
184
+ }
185
+ }
186
+
187
+ export { useToast, toast }
package/src/index.ts CHANGED
@@ -139,6 +139,22 @@ export {
139
139
  export { Divider } from './components/divider'
140
140
  export type { DividerProps } from './components/divider'
141
141
 
142
+ // Toast Components
143
+ export {
144
+ ToastProvider,
145
+ ToastViewport,
146
+ Toast,
147
+ ToastTitle,
148
+ ToastDescription,
149
+ ToastClose,
150
+ ToastAction,
151
+ ToastIcon,
152
+ } from './components/toast'
153
+ export type { ToastProps, ToastActionElement } from './components/toast'
154
+
155
+ export { Toaster } from './components/toaster'
156
+ export { useToast, toast } from './components/use-toast'
157
+
142
158
  // Utilities
143
159
  export { cn } from './lib/utils'
144
160