parthenon-ui 1.0.0
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 +74 -0
- package/src/components/.gitkeep +0 -0
- package/src/components/avatar.tsx +109 -0
- package/src/components/badge.tsx +52 -0
- package/src/components/button.tsx +122 -0
- package/src/components/card.tsx +108 -0
- package/src/components/checkbox.tsx +37 -0
- package/src/components/collapsible.tsx +21 -0
- package/src/components/color-picker.tsx +270 -0
- package/src/components/command.tsx +195 -0
- package/src/components/context-menu.tsx +270 -0
- package/src/components/dialog.tsx +169 -0
- package/src/components/dropdown-menu.tsx +279 -0
- package/src/components/empty.tsx +104 -0
- package/src/components/index.ts +27 -0
- package/src/components/input-group.tsx +155 -0
- package/src/components/input.tsx +27 -0
- package/src/components/label.tsx +18 -0
- package/src/components/popover.tsx +88 -0
- package/src/components/scroll-area.tsx +55 -0
- package/src/components/select.tsx +201 -0
- package/src/components/separator.tsx +23 -0
- package/src/components/sheet.tsx +138 -0
- package/src/components/sidebar.tsx +729 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/sonner.tsx +59 -0
- package/src/components/switch.tsx +51 -0
- package/src/components/table.tsx +375 -0
- package/src/components/tabs.tsx +80 -0
- package/src/components/textarea.tsx +18 -0
- package/src/components/tooltip.tsx +64 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/lib/.gitkeep +0 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/globals.css +654 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from "@workspace/ui/lib/utils"
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot="skeleton"
|
|
7
|
+
className={cn("animate-pulse rounded-xl bg-muted", className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton }
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { HugeiconsIcon } from "@hugeicons/react"
|
|
4
|
+
import { CheckmarkCircle02Icon, InformationCircleIcon, Alert02Icon, MultiplicationSignCircleIcon, Loading03Icon } from "@hugeicons/core-free-icons"
|
|
5
|
+
|
|
6
|
+
function getTheme(): "light" | "dark" {
|
|
7
|
+
if (typeof document === "undefined") return "light"
|
|
8
|
+
return document.documentElement.classList.contains("dark") ? "dark" : "light"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Toaster = ({ ...props }: ToasterProps) => {
|
|
12
|
+
const [theme, setTheme] = React.useState<"light" | "dark">(() => getTheme())
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
const observer = new MutationObserver(() => setTheme(getTheme()))
|
|
16
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] })
|
|
17
|
+
return () => observer.disconnect()
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Sonner
|
|
22
|
+
theme={theme}
|
|
23
|
+
className="toaster group"
|
|
24
|
+
icons={{
|
|
25
|
+
success: (
|
|
26
|
+
<HugeiconsIcon icon={CheckmarkCircle02Icon} strokeWidth={2} className="size-4" />
|
|
27
|
+
),
|
|
28
|
+
info: (
|
|
29
|
+
<HugeiconsIcon icon={InformationCircleIcon} strokeWidth={2} className="size-4" />
|
|
30
|
+
),
|
|
31
|
+
warning: (
|
|
32
|
+
<HugeiconsIcon icon={Alert02Icon} strokeWidth={2} className="size-4" />
|
|
33
|
+
),
|
|
34
|
+
error: (
|
|
35
|
+
<HugeiconsIcon icon={MultiplicationSignCircleIcon} strokeWidth={2} className="size-4" />
|
|
36
|
+
),
|
|
37
|
+
loading: (
|
|
38
|
+
<HugeiconsIcon icon={Loading03Icon} strokeWidth={2} className="size-4 animate-spin" />
|
|
39
|
+
),
|
|
40
|
+
}}
|
|
41
|
+
style={
|
|
42
|
+
{
|
|
43
|
+
"--normal-bg": "var(--popover)",
|
|
44
|
+
"--normal-text": "var(--popover-foreground)",
|
|
45
|
+
"--normal-border": "var(--border)",
|
|
46
|
+
"--border-radius": "var(--radius)",
|
|
47
|
+
} as React.CSSProperties
|
|
48
|
+
}
|
|
49
|
+
toastOptions={{
|
|
50
|
+
classNames: {
|
|
51
|
+
toast: "cn-toast",
|
|
52
|
+
},
|
|
53
|
+
}}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { Toaster }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@workspace/ui/lib/utils"
|
|
6
|
+
|
|
7
|
+
function Switch({
|
|
8
|
+
className,
|
|
9
|
+
size = "default",
|
|
10
|
+
...props
|
|
11
|
+
}: SwitchPrimitive.Root.Props & {
|
|
12
|
+
size?: "sm" | "default"
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<SwitchPrimitive.Root
|
|
16
|
+
data-slot="switch"
|
|
17
|
+
data-size={size}
|
|
18
|
+
className={cn(
|
|
19
|
+
"peer group/switch relative inline-flex shrink-0 cursor-pointer items-center rounded-full border border-transparent",
|
|
20
|
+
"transition-all duration-150 ease-in-out outline-none",
|
|
21
|
+
"focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
22
|
+
"data-[size=default]:h-6 data-[size=default]:w-11",
|
|
23
|
+
"data-[size=sm]:h-4.5 data-[size=sm]:w-8",
|
|
24
|
+
"data-checked:bg-primary",
|
|
25
|
+
"data-unchecked:bg-input dark:data-unchecked:bg-input/80",
|
|
26
|
+
"data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
27
|
+
"active:scale-[0.97]",
|
|
28
|
+
className
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
<SwitchPrimitive.Thumb
|
|
33
|
+
data-slot="switch-thumb"
|
|
34
|
+
className={cn(
|
|
35
|
+
"pointer-events-none block rounded-full bg-background shadow-sm",
|
|
36
|
+
"transition-all duration-150 ease-[cubic-bezier(0.34,1.56,0.64,1)]",
|
|
37
|
+
"group-data-[size=default]/switch:size-4.5 group-data-[size=sm]/switch:size-3",
|
|
38
|
+
"group-data-[size=default]/switch:ml-1",
|
|
39
|
+
"group-data-[size=sm]/switch:ml-0.5",
|
|
40
|
+
"group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)]",
|
|
41
|
+
"group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)]",
|
|
42
|
+
"group-data-[size=default]/switch:data-unchecked:translate-x-0",
|
|
43
|
+
"group-data-[size=sm]/switch:data-unchecked:translate-x-0",
|
|
44
|
+
"dark:data-checked:bg-primary-foreground dark:data-unchecked:bg-foreground"
|
|
45
|
+
)}
|
|
46
|
+
/>
|
|
47
|
+
</SwitchPrimitive.Root>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { Switch }
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { HugeiconsIcon } from "@hugeicons/react"
|
|
3
|
+
import { ArrowUp01Icon, ArrowDown01Icon } from "@hugeicons/core-free-icons"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@workspace/ui/lib/utils"
|
|
6
|
+
|
|
7
|
+
interface TableContextType<T> {
|
|
8
|
+
sortBy: keyof T | null
|
|
9
|
+
sortDesc: boolean
|
|
10
|
+
page: number
|
|
11
|
+
pageSize: number
|
|
12
|
+
data: T[]
|
|
13
|
+
columns: ColumnDef<T>[]
|
|
14
|
+
setSortBy: (col: keyof T) => void
|
|
15
|
+
setPageSize: (size: number) => void
|
|
16
|
+
setPage: (page: number) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* ------- Types ------- */
|
|
20
|
+
interface ColumnDef<T> {
|
|
21
|
+
header: string
|
|
22
|
+
accessor: keyof T | ((row: T) => React.ReactNode)
|
|
23
|
+
sortable?: boolean
|
|
24
|
+
className?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface TableProps<T> {
|
|
28
|
+
data: T[]
|
|
29
|
+
columns: ColumnDef<T>[]
|
|
30
|
+
initialPageSize?: number
|
|
31
|
+
className?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ------- Context ------- */
|
|
35
|
+
const TableContext = React.createContext<TableContextType<any> | null>(null)
|
|
36
|
+
|
|
37
|
+
/* ------- Hook ------- */
|
|
38
|
+
function useTableContext<T>() {
|
|
39
|
+
const context = React.useContext(TableContext)
|
|
40
|
+
if (!context) {
|
|
41
|
+
throw new Error("Table components must be used within Table")
|
|
42
|
+
}
|
|
43
|
+
return context as TableContextType<T>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ------- Table (wrapper) ------- */
|
|
47
|
+
function Table<T>({
|
|
48
|
+
className,
|
|
49
|
+
children,
|
|
50
|
+
...props
|
|
51
|
+
}: TableProps<T> & { className?: string; children: React.ReactNode }) {
|
|
52
|
+
const [sortBy, setSortBy] = React.useState<keyof T | null>(null)
|
|
53
|
+
const [sortDesc, setSortDesc] = React.useState(false)
|
|
54
|
+
const [page, setPage] = React.useState(0)
|
|
55
|
+
const [pageSize, setPageSize] = React.useState(props.initialPageSize ?? 10)
|
|
56
|
+
|
|
57
|
+
const sortedData = React.useMemo(() => {
|
|
58
|
+
if (!sortBy || !props.columns.some((c) => c.accessor === sortBy && c.sortable)) {
|
|
59
|
+
return props.data
|
|
60
|
+
}
|
|
61
|
+
return [...props.data].sort((a, b) => {
|
|
62
|
+
const aVal =
|
|
63
|
+
typeof sortBy === "function"
|
|
64
|
+
? sortBy(a)
|
|
65
|
+
: a[sortBy]
|
|
66
|
+
const bVal =
|
|
67
|
+
typeof sortBy === "function"
|
|
68
|
+
? sortBy(b)
|
|
69
|
+
: b[sortBy]
|
|
70
|
+
|
|
71
|
+
if (aVal === undefined) return 1
|
|
72
|
+
if (bVal === undefined) return -1
|
|
73
|
+
|
|
74
|
+
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
75
|
+
return sortDesc
|
|
76
|
+
? bVal.localeCompare(aVal)
|
|
77
|
+
: aVal.localeCompare(bVal)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (typeof aVal === "number" && typeof bVal === "number") {
|
|
81
|
+
return sortDesc ? bVal - aVal : aVal - bVal
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return sortDesc ? -1 : 1
|
|
85
|
+
})
|
|
86
|
+
}, [props.data, sortBy, sortDesc, props.columns])
|
|
87
|
+
|
|
88
|
+
const pageCount = Math.max(1, Math.ceil(sortedData.length / pageSize))
|
|
89
|
+
const paginatedData = React.useMemo(() => {
|
|
90
|
+
const start = page * pageSize
|
|
91
|
+
return sortedData.slice(start, start + pageSize)
|
|
92
|
+
}, [sortedData, page, pageSize])
|
|
93
|
+
|
|
94
|
+
const value: TableContextType<T> = {
|
|
95
|
+
sortBy,
|
|
96
|
+
sortDesc,
|
|
97
|
+
page,
|
|
98
|
+
pageSize,
|
|
99
|
+
data: props.data,
|
|
100
|
+
columns: props.columns,
|
|
101
|
+
setSortBy,
|
|
102
|
+
setPageSize: (size) => {
|
|
103
|
+
setPageSize(size)
|
|
104
|
+
setPage(0)
|
|
105
|
+
},
|
|
106
|
+
setPage: (num) => {
|
|
107
|
+
setPage(Math.max(0, Math.min(num, pageCount - 1)))
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<TableContext.Provider value={value}>
|
|
113
|
+
<div className={cn("w-full overflow-hidden rounded-2xl border ring-1 ring-inset ring-foreground/5 shadow-sm", className)}>
|
|
114
|
+
{children}
|
|
115
|
+
</div>
|
|
116
|
+
</TableContext.Provider>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ------- Table Header ------- */
|
|
121
|
+
function TableHeader({ className, children, ...props }: React.ComponentProps<"thead">) {
|
|
122
|
+
return (
|
|
123
|
+
<thead
|
|
124
|
+
data-slot="table-header"
|
|
125
|
+
className={cn("bg-muted", className)}
|
|
126
|
+
{...props}
|
|
127
|
+
>
|
|
128
|
+
{children}
|
|
129
|
+
</thead>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ------- Table Body ------- */
|
|
134
|
+
function TableBody({ className, children, ...props }: React.ComponentProps<"tbody">) {
|
|
135
|
+
return (
|
|
136
|
+
<tbody
|
|
137
|
+
data-slot="table-body"
|
|
138
|
+
className={cn("divide-y divide-muted/50", className)}
|
|
139
|
+
{...props}
|
|
140
|
+
>
|
|
141
|
+
{children}
|
|
142
|
+
</tbody>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* ------- Table Footer ------- */
|
|
147
|
+
function TableFooter({ className, children, ...props }: React.ComponentProps<"tfoot">) {
|
|
148
|
+
return (
|
|
149
|
+
<tfoot
|
|
150
|
+
data-slot="table-footer"
|
|
151
|
+
className={cn("", className)}
|
|
152
|
+
{...props}
|
|
153
|
+
>
|
|
154
|
+
{children}
|
|
155
|
+
</tfoot>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ------- Table Head (tr inside thead) ------- */
|
|
160
|
+
function TableHead({ className, children, ...props }: React.ComponentProps<"tr">) {
|
|
161
|
+
return (
|
|
162
|
+
<tr
|
|
163
|
+
data-slot="table-head"
|
|
164
|
+
className={cn("text-xs font-medium tracking-wider uppercase text-muted-foreground hover:bg-muted/50 transition-colors", className)}
|
|
165
|
+
{...props}
|
|
166
|
+
>
|
|
167
|
+
{children}
|
|
168
|
+
</tr>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* ------- Table Row (tr inside tbody) ------- */
|
|
173
|
+
function TableRow({ className, children, ...props }: React.ComponentProps<"tr">) {
|
|
174
|
+
return (
|
|
175
|
+
<tr
|
|
176
|
+
data-slot="table-row"
|
|
177
|
+
className={cn(
|
|
178
|
+
"hover:bg-muted/50 transition-colors focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
|
|
179
|
+
className
|
|
180
|
+
)}
|
|
181
|
+
{...props}
|
|
182
|
+
>
|
|
183
|
+
{children}
|
|
184
|
+
</tr>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ------- Table Cell (td/th) ------- */
|
|
189
|
+
interface TableCellProps<T> extends React.ComponentProps<"td"> {
|
|
190
|
+
column?: ColumnDef<T>
|
|
191
|
+
rowIndex?: number
|
|
192
|
+
}
|
|
193
|
+
function TableCell<T>({ className, children, column, ...props }: TableCellProps<T>) {
|
|
194
|
+
const { sortBy, sortDesc, setSortBy } = useTableContext<T>()
|
|
195
|
+
|
|
196
|
+
// If column is not provided, render as a plain cell
|
|
197
|
+
if (!column) {
|
|
198
|
+
return (
|
|
199
|
+
<td
|
|
200
|
+
data-slot="table-cell"
|
|
201
|
+
className={cn("px-4 py-3 text-sm", className)}
|
|
202
|
+
{...props}
|
|
203
|
+
>
|
|
204
|
+
{children}
|
|
205
|
+
</td>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const isSortable = column.sortable ?? false
|
|
210
|
+
const isSorted = sortBy === (column.accessor as keyof T)
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<td
|
|
214
|
+
data-slot="table-cell"
|
|
215
|
+
className={cn(
|
|
216
|
+
"px-4 py-3 text-sm",
|
|
217
|
+
isSortable && "cursor-pointer",
|
|
218
|
+
className
|
|
219
|
+
)}
|
|
220
|
+
onClick={() => {
|
|
221
|
+
if (isSortable) {
|
|
222
|
+
setSortBy(column.accessor as keyof T)
|
|
223
|
+
}
|
|
224
|
+
}}
|
|
225
|
+
{...props}
|
|
226
|
+
>
|
|
227
|
+
<div className="flex items-center space-x-2">
|
|
228
|
+
{children}
|
|
229
|
+
{isSortable && (
|
|
230
|
+
<span className="h-4 w-4 opacity-50 transition-opacity">
|
|
231
|
+
{isSorted ? (
|
|
232
|
+
sortDesc ? <HugeiconsIcon icon={ArrowDown01Icon} className="h-4 w-4" /> : <HugeiconsIcon icon={ArrowUp01Icon} className="h-4 w-4" />
|
|
233
|
+
) : (
|
|
234
|
+
<HugeiconsIcon icon={ArrowUp01Icon} className="h-4 w-4 opacity-25" />
|
|
235
|
+
)}
|
|
236
|
+
</span>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
</td>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* ------- Pagination Controls (to be placed in TableFooter) ------- */
|
|
244
|
+
function TablePagination() {
|
|
245
|
+
const { page, pageSize, setPageSize, setPage, data } = useTableContext<any>()
|
|
246
|
+
const sortedData = React.useMemo(() => {
|
|
247
|
+
// same sorting logic as Table component (duplicated for brevity)
|
|
248
|
+
// In real implementation we would move sorting logic to a separate hook; for now reuse.
|
|
249
|
+
const ctxSortBy: any = React.useContext(TableContext)?.sortBy
|
|
250
|
+
const ctxSortDesc: any = React.useContext(TableContext)?.sortDesc
|
|
251
|
+
const ctxColumns: any = React.useContext(TableContext)?.columns
|
|
252
|
+
if (!ctxSortBy || !ctxColumns.some((c: any) => c.accessor === ctxSortBy && c.sortable)) {
|
|
253
|
+
return data
|
|
254
|
+
}
|
|
255
|
+
return [...data].sort((a: any, b: any) => {
|
|
256
|
+
const aVal =
|
|
257
|
+
typeof ctxSortBy === "function"
|
|
258
|
+
? ctxSortBy(a)
|
|
259
|
+
: a[ctxSortBy]
|
|
260
|
+
const bVal =
|
|
261
|
+
typeof ctxSortBy === "function"
|
|
262
|
+
? ctxSortBy(b)
|
|
263
|
+
: b[ctxSortBy]
|
|
264
|
+
|
|
265
|
+
if (aVal === undefined) return 1
|
|
266
|
+
if (bVal === undefined) return -1
|
|
267
|
+
|
|
268
|
+
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
269
|
+
return ctxSortDesc
|
|
270
|
+
? bVal.localeCompare(aVal)
|
|
271
|
+
: aVal.localeCompare(bVal)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (typeof aVal === "number" && typeof bVal === "number") {
|
|
275
|
+
return ctxSortDesc ? bVal - aVal : aVal - bVal
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return ctxSortDesc ? -1 : 1
|
|
279
|
+
})
|
|
280
|
+
}, [data]) // Note: missing sort dependencies; but we keep simple
|
|
281
|
+
|
|
282
|
+
const pageCount = Math.max(1, Math.ceil(sortedData.length / pageSize))
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between pt-4 pb-6 px-4 text-sm text-muted-foreground">
|
|
286
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:space-x-2">
|
|
287
|
+
<div className="flex items-center space-x-2">
|
|
288
|
+
<button
|
|
289
|
+
onClick={() => setPage(0)}
|
|
290
|
+
disabled={page === 0}
|
|
291
|
+
className={cn(
|
|
292
|
+
"inline-flex h-9 w-9 items-center justify-center rounded-md border border-transparent bg-muted/50 px-2.5 text-sm font-medium ring-offset-background hover:bg-muted/60 disabled:pointer-events-none disabled:opacity-50",
|
|
293
|
+
page === 0 && "opacity-50 pointer-events-none"
|
|
294
|
+
)}
|
|
295
|
+
>
|
|
296
|
+
<span className="sr-only">Go to first page</span>
|
|
297
|
+
</button>
|
|
298
|
+
<button
|
|
299
|
+
onClick={() => setPage(page - 1)}
|
|
300
|
+
disabled={page === 0}
|
|
301
|
+
className={cn(
|
|
302
|
+
"inline-flex h-9 w-9 items-center justify-center rounded-md border border-transparent bg-muted/50 px-2.5 text-sm font-medium ring-offset-background hover:bg-muted/60 disabled:pointer-events-none disabled:opacity-50",
|
|
303
|
+
page === 0 && "opacity-50 pointer-events-none"
|
|
304
|
+
)}
|
|
305
|
+
>
|
|
306
|
+
<span className="sr-only">Go to previous page</span>
|
|
307
|
+
</button>
|
|
308
|
+
<span>
|
|
309
|
+
Page{" "}
|
|
310
|
+
<span className="font-medium">{page + 1}</span>{" "}
|
|
311
|
+
of{" "}
|
|
312
|
+
<span className="font-medium">{pageCount}</span>
|
|
313
|
+
</span>
|
|
314
|
+
<button
|
|
315
|
+
onClick={() => setPage(page + 1)}
|
|
316
|
+
disabled={page >= pageCount - 1}
|
|
317
|
+
className={cn(
|
|
318
|
+
"inline-flex h-9 w-9 items-center justify-center rounded-md border border-transparent bg-muted/50 px-2.5 text-sm font-medium ring-offset-background hover:bg-muted/60 disabled:pointer-events-none disabled:opacity-50",
|
|
319
|
+
page >= pageCount - 1 && "opacity-50 pointer-events-none"
|
|
320
|
+
)}
|
|
321
|
+
>
|
|
322
|
+
<span className="sr-only">Go to next page</span>
|
|
323
|
+
</button>
|
|
324
|
+
<button
|
|
325
|
+
onClick={() => setPage(pageCount - 1)}
|
|
326
|
+
disabled={page >= pageCount - 1}
|
|
327
|
+
className={cn(
|
|
328
|
+
"inline-flex h-9 w-9 items-center justify-center rounded-md border border-transparent bg-muted/50 px-2.5 text-sm font-medium ring-offset-background hover:bg-muted/60 disabled:pointer-events-none disabled:opacity-50",
|
|
329
|
+
page >= pageCount - 1 && "opacity-50 pointer-events-none"
|
|
330
|
+
)}
|
|
331
|
+
>
|
|
332
|
+
<span className="sr-only">Go to last page</span>
|
|
333
|
+
</button>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
<div className="mt-2 sm:mt-0 space-x-2">
|
|
337
|
+
<label
|
|
338
|
+
className="flex items-center space-x-2 text-sm font-medium"
|
|
339
|
+
>
|
|
340
|
+
Rows per page:
|
|
341
|
+
<select
|
|
342
|
+
value={pageSize}
|
|
343
|
+
onChange={(e) => {
|
|
344
|
+
setPageSize(Number(e.target.value))
|
|
345
|
+
setPage(0)
|
|
346
|
+
}}
|
|
347
|
+
className={cn(
|
|
348
|
+
"bg-muted border border-muted/50 rounded-md px-2.5 py-1.5 text-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
349
|
+
"focus-visible:outline-none"
|
|
350
|
+
)}
|
|
351
|
+
>
|
|
352
|
+
{[5, 10, 25, 50, 100].map((size) => (
|
|
353
|
+
<option key={size} value={size}>
|
|
354
|
+
{size}
|
|
355
|
+
</option>
|
|
356
|
+
))}
|
|
357
|
+
</select>
|
|
358
|
+
</label>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* ------- Export ------- */
|
|
365
|
+
export {
|
|
366
|
+
Table,
|
|
367
|
+
TableHeader,
|
|
368
|
+
TableBody,
|
|
369
|
+
TableFooter,
|
|
370
|
+
TableHead,
|
|
371
|
+
TableRow,
|
|
372
|
+
TableCell,
|
|
373
|
+
TablePagination,
|
|
374
|
+
}
|
|
375
|
+
export type { TableProps, ColumnDef }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@workspace/ui/lib/utils"
|
|
5
|
+
|
|
6
|
+
function Tabs({
|
|
7
|
+
className,
|
|
8
|
+
orientation = "horizontal",
|
|
9
|
+
...props
|
|
10
|
+
}: TabsPrimitive.Root.Props) {
|
|
11
|
+
return (
|
|
12
|
+
<TabsPrimitive.Root
|
|
13
|
+
data-slot="tabs"
|
|
14
|
+
data-orientation={orientation}
|
|
15
|
+
className={cn(
|
|
16
|
+
"group/tabs flex gap-2 data-horizontal:flex-col",
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const tabsListVariants = cva(
|
|
25
|
+
"group/tabs-list inline-flex w-fit items-center justify-center rounded-4xl p-[3px] text-muted-foreground group-data-horizontal/tabs:h-9 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col group-data-vertical/tabs:rounded-2xl data-[variant=line]:rounded-none",
|
|
26
|
+
{
|
|
27
|
+
variants: {
|
|
28
|
+
variant: {
|
|
29
|
+
default: "bg-muted",
|
|
30
|
+
line: "gap-1 bg-transparent",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultVariants: {
|
|
34
|
+
variant: "default",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
function TabsList({
|
|
40
|
+
className,
|
|
41
|
+
variant = "default",
|
|
42
|
+
...props
|
|
43
|
+
}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
|
|
44
|
+
return (
|
|
45
|
+
<TabsPrimitive.List
|
|
46
|
+
data-slot="tabs-list"
|
|
47
|
+
data-variant={variant}
|
|
48
|
+
className={cn(tabsListVariants({ variant }), className)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
|
55
|
+
return (
|
|
56
|
+
<TabsPrimitive.Tab
|
|
57
|
+
data-slot="tabs-trigger"
|
|
58
|
+
className={cn(
|
|
59
|
+
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-xl border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start group-data-vertical/tabs:px-2.5 group-data-vertical/tabs:py-1.5 hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
60
|
+
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
|
61
|
+
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
|
62
|
+
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-end-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
|
63
|
+
className
|
|
64
|
+
)}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
|
|
71
|
+
return (
|
|
72
|
+
<TabsPrimitive.Panel
|
|
73
|
+
data-slot="tabs-content"
|
|
74
|
+
className={cn("flex-1 text-sm outline-none", className)}
|
|
75
|
+
{...props}
|
|
76
|
+
/>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@workspace/ui/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
6
|
+
return (
|
|
7
|
+
<textarea
|
|
8
|
+
data-slot="textarea"
|
|
9
|
+
className={cn(
|
|
10
|
+
"flex field-sizing-content min-h-16 w-full resize-none rounded-xl border border-input bg-input/30 px-3 py-3 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Textarea }
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@workspace/ui/lib/utils"
|
|
4
|
+
|
|
5
|
+
function TooltipProvider({
|
|
6
|
+
delay = 0,
|
|
7
|
+
...props
|
|
8
|
+
}: TooltipPrimitive.Provider.Props) {
|
|
9
|
+
return (
|
|
10
|
+
<TooltipPrimitive.Provider
|
|
11
|
+
data-slot="tooltip-provider"
|
|
12
|
+
delay={delay}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
|
19
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
|
23
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function TooltipContent({
|
|
27
|
+
className,
|
|
28
|
+
side = "top",
|
|
29
|
+
sideOffset = 4,
|
|
30
|
+
align = "center",
|
|
31
|
+
alignOffset = 0,
|
|
32
|
+
children,
|
|
33
|
+
...props
|
|
34
|
+
}: TooltipPrimitive.Popup.Props &
|
|
35
|
+
Pick<
|
|
36
|
+
TooltipPrimitive.Positioner.Props,
|
|
37
|
+
"align" | "alignOffset" | "side" | "sideOffset"
|
|
38
|
+
>) {
|
|
39
|
+
return (
|
|
40
|
+
<TooltipPrimitive.Portal>
|
|
41
|
+
<TooltipPrimitive.Positioner
|
|
42
|
+
align={align}
|
|
43
|
+
alignOffset={alignOffset}
|
|
44
|
+
side={side}
|
|
45
|
+
sideOffset={sideOffset}
|
|
46
|
+
className="isolate z-50"
|
|
47
|
+
>
|
|
48
|
+
<TooltipPrimitive.Popup
|
|
49
|
+
data-slot="tooltip-content"
|
|
50
|
+
className={cn(
|
|
51
|
+
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-2xl bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pe-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-start-2 data-[side=inline-start]:slide-in-from-end-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-4xl data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-start-1 data-[side=inline-end]:translate-x-[1.5px] rtl:data-[side=inline-end]:-translate-x-[1.5px] data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-end-1 data-[side=inline-start]:translate-x-[-1.5px] rtl:data-[side=inline-start]:-translate-x-[-1.5px] data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:translate-x-[-1.5px] rtl:data-[side=left]:-translate-x-[-1.5px] data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:translate-x-[1.5px] rtl:data-[side=right]:-translate-x-[1.5px] data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
|
58
|
+
</TooltipPrimitive.Popup>
|
|
59
|
+
</TooltipPrimitive.Positioner>
|
|
60
|
+
</TooltipPrimitive.Portal>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
10
|
+
const onChange = () => {
|
|
11
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
12
|
+
}
|
|
13
|
+
mql.addEventListener("change", onChange)
|
|
14
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
15
|
+
return () => mql.removeEventListener("change", onChange)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
return !!isMobile
|
|
19
|
+
}
|
package/src/lib/.gitkeep
ADDED
|
File without changes
|