@xjur-ui/react 1.0.2 → 1.0.4

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.
Files changed (42) hide show
  1. package/.turbo/turbo-build.log +26 -16
  2. package/CHANGELOG.md +20 -0
  3. package/dist/components/avatar-group.d.ts +34 -0
  4. package/dist/components/avatar-group.js +179 -0
  5. package/dist/components/avatar.js +1 -0
  6. package/dist/components/chip.js +1 -0
  7. package/dist/components/date-picker.js +1 -1
  8. package/dist/components/divider.js +2 -1
  9. package/dist/components/dropdown-menu.js +3 -2
  10. package/dist/components/icon.d.ts +1 -1
  11. package/dist/components/icon.js +5 -1
  12. package/dist/components/input.js +1 -1
  13. package/dist/components/otp-input.js +1 -1
  14. package/dist/components/rich-editor.d.ts +25 -0
  15. package/dist/components/rich-editor.js +723 -0
  16. package/dist/components/search-input.js +1 -1
  17. package/dist/components/select.js +2 -2
  18. package/dist/components/skeleton.d.ts +6 -0
  19. package/dist/components/skeleton.js +16 -0
  20. package/dist/components/table.d.ts +17 -0
  21. package/dist/components/table.js +196 -0
  22. package/dist/components/tooltip.d.ts +11 -0
  23. package/dist/components/tooltip.js +58 -0
  24. package/dist/components/typography.d.ts +1 -1
  25. package/dist/index.css +401 -23
  26. package/package.json +14 -2
  27. package/src/components/avatar-group.tsx +76 -0
  28. package/src/components/avatar.tsx +1 -0
  29. package/src/components/date-picker.tsx +1 -1
  30. package/src/components/divider.tsx +2 -1
  31. package/src/components/dropdown-menu.tsx +1 -1
  32. package/src/components/input.tsx +1 -1
  33. package/src/components/otp-input.tsx +1 -1
  34. package/src/components/rich-editor.tsx +371 -0
  35. package/src/components/search-input.tsx +1 -2
  36. package/src/components/select.tsx +1 -1
  37. package/src/components/skeleton.tsx +12 -0
  38. package/src/components/table.tsx +160 -0
  39. package/src/components/tooltip.tsx +63 -0
  40. package/src/hooks/index.ts +1 -0
  41. package/src/hooks/use-os.ts +32 -0
  42. package/src/resources/icons.ts +5 -1
@@ -0,0 +1,76 @@
1
+ import { cx } from '@xjur-ui/util'
2
+ import { AvatarRoot, AvatarImage, AvatarFallback } from './avatar'
3
+ import { Typography } from './typography'
4
+
5
+ export interface AvatarGroupProps {
6
+ /**
7
+ * Lista de avatares para exibir
8
+ */
9
+ avatars: Array<{
10
+ src?: string
11
+ alt?: string
12
+ fallback?: string
13
+ }>
14
+ /**
15
+ * Número máximo de avatares a exibir antes de mostrar o indicador de overflow
16
+ * @default 3
17
+ */
18
+ max?: number
19
+ /**
20
+ * Tamanho dos avatares
21
+ * @default 'md'
22
+ */
23
+ size?: 'xs' | 'sm' | 'md' | 'lg'
24
+ /**
25
+ * Classe CSS adicional
26
+ */
27
+ className?: string
28
+ /**
29
+ * Espaçamento entre os avatares (em pixels)
30
+ * @default -8
31
+ */
32
+ spacing?: number
33
+ }
34
+
35
+ export function AvatarGroup({
36
+ avatars,
37
+ max = 3,
38
+ size = 'md',
39
+ className
40
+ }: AvatarGroupProps) {
41
+ const visibleAvatars = avatars.slice(0, max)
42
+ const remainingCount = avatars.length - max
43
+
44
+ return (
45
+ <div
46
+ className={cx('-space-x-small-1x flex items-center', className)}
47
+ data-slot="avatar-group"
48
+ >
49
+ {visibleAvatars.map((avatar, index) => (
50
+ <AvatarRoot
51
+ className="border-icon-neutral-5 border-2"
52
+ key={index}
53
+ size={size}
54
+ >
55
+ {avatar.src && (
56
+ <AvatarImage
57
+ alt={avatar.alt || `Avatar ${index + 1}`}
58
+ src={avatar.src}
59
+ />
60
+ )}
61
+ <AvatarFallback>{avatar.fallback || `A${index + 1}`}</AvatarFallback>
62
+ </AvatarRoot>
63
+ ))}
64
+
65
+ {remainingCount > 0 && (
66
+ <AvatarRoot className="bg-layer-1-hover" size={size}>
67
+ <AvatarFallback className="bg-layer-1-hover font-medium">
68
+ <Typography className="text-neutral-dark" weight="medium">
69
+ +{remainingCount}
70
+ </Typography>
71
+ </AvatarFallback>
72
+ </AvatarRoot>
73
+ )}
74
+ </div>
75
+ )
76
+ }
@@ -14,6 +14,7 @@ export function AvatarRoot({
14
14
  className={cx(
15
15
  'rounded-circle relative flex shrink-0 overflow-hidden',
16
16
  'data-[size=lg]:size-large-8x data-[size=md]:size-large-4x data-[size=sm]:size-large-2x data-[size=xs]:size-large-1x',
17
+ 'data-[size=lg]:text-size-headline-md data-[size=md]:text-size-label-lg data-[size=sm]:text-size-label-sm data-[size=xs]:text-size-label-sm',
17
18
  className
18
19
  )}
19
20
  data-size={size}
@@ -19,7 +19,7 @@ export function DatePickerTrigger({
19
19
  className,
20
20
  'group h-large-4x rounded-small-1x gap-small py-small_1x-alt px-small relative flex w-full cursor-pointer items-center',
21
21
  'bg-layer-2-idle border-border-layer-2 hover:border-border-layer-3 border',
22
- 'data-[state=open]:border-primary-idle data-[state=open]:shadow-focus-primary',
22
+ 'data-[state=open]:border-primary-idle',
23
23
  'has-[input:not(:placeholder-shown)]:text-primary-idle',
24
24
  'has-[input:not(:placeholder-shown)]:border-primary-idle'
25
25
  )}
@@ -11,7 +11,8 @@ export function Divider({
11
11
  return (
12
12
  <SeparatorPrimitive.Root
13
13
  className={cx(
14
- 'bg-border-layer-2 shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
14
+ 'bg-border-layer-2 shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full',
15
+ 'data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
15
16
  className
16
17
  )}
17
18
  data-slot="separator"
@@ -49,7 +49,7 @@ export function DropdownMenuContent({
49
49
  <DropdownMenuPrimitive.Content
50
50
  align={align}
51
51
  className={cx(
52
- 'group z-10 max-h-[var(--radix-popover-content-available-height)] max-w-[var(--radix-popover-trigger-width)] min-w-[240px]',
52
+ 'group z-10 max-h-[var(--radix-popover-content-available-height)] max-w-[var(--radix-popover-trigger-width)] min-w-[var(--radix-popover-trigger-width)]',
53
53
  className
54
54
  )}
55
55
  data-slot="dropdown-menu-content"
@@ -43,7 +43,7 @@ export function InputContent({ className, ...props }: ComponentProps<'div'>) {
43
43
  const variants = cva(
44
44
  [
45
45
  'relative h-large-4x rounded-small-1x gap-small py-small_1x-alt px-small flex items-center w-full',
46
- 'focus-within:border-primary-idle focus-within:shadow-focus-primary focus-within:border',
46
+ 'focus-within:border-primary-idle focus-within:border',
47
47
  'group-has-[div[data-slot=input-right-action-slot]]:rounded-r-none',
48
48
  'group-has-[div[data-slot=input-left-action-slot]]:rounded-l-none'
49
49
  ],
@@ -25,7 +25,7 @@ function Input({
25
25
  className={cx(
26
26
  className,
27
27
  'text-neutral-dark h-large-6x bg-layer-2-hover rounded-small-1x w-full text-center outline-0',
28
- 'focus-within:border-primary-idle focus-within:shadow-focus-primary focus-within:border'
28
+ 'focus-within:border-primary-idle focus-within:border'
29
29
  )}
30
30
  {...props}
31
31
  />
@@ -0,0 +1,371 @@
1
+ import { cx } from '@xjur-ui/util'
2
+ import { useCallback, useMemo, useState, type ComponentProps } from 'react'
3
+ import {
4
+ EditorContent,
5
+ EditorContext,
6
+ useCurrentEditor as useRichTextEditor,
7
+ useEditorState as useRichTextEditorState,
8
+ useEditor,
9
+ type EditorOptions,
10
+ Extension
11
+ } from '@tiptap/react'
12
+ import { StarterKit } from '@tiptap/starter-kit'
13
+ import { Heading, type Level } from '@tiptap/extension-heading'
14
+ import { Placeholder } from '@tiptap/extensions'
15
+ import { useOS } from '../hooks'
16
+ import { Icon, type IconType } from './icon'
17
+ import { Button, type ButtonProps } from './button'
18
+ import {
19
+ DropdownMenuContent,
20
+ DropdownMenuItem,
21
+ DropdownMenuItemText,
22
+ DropdownMenuList,
23
+ DropdownMenuRoot,
24
+ DropdownMenuTrigger
25
+ } from './dropdown-menu'
26
+ import { Tooltip, TooltipTrigger, TooltipContent } from './tooltip'
27
+ import { Typography } from './typography'
28
+
29
+ const headingClasses: Partial<Record<Level, string>> = {
30
+ 1: 'text-size-body-md',
31
+ 2: 'text-size-title-lg',
32
+ 3: 'text-size-headline-lg'
33
+ }
34
+
35
+ export function RichEditorRoot({
36
+ className,
37
+ editorOptions,
38
+ ...props
39
+ }: ComponentProps<'div'> & {
40
+ editorOptions?: Partial<
41
+ Omit<EditorOptions, 'extensions'> & {
42
+ extensions?: Extension[]
43
+ }
44
+ >
45
+ }) {
46
+ const editor = useEditor({
47
+ ...editorOptions,
48
+ extensions: [
49
+ StarterKit.configure({
50
+ bulletList: {
51
+ HTMLAttributes: {
52
+ class: 'list-disc list-outside pl-large-1x'
53
+ }
54
+ },
55
+ orderedList: {
56
+ HTMLAttributes: {
57
+ class: 'list-decimal list-outside pl-large-1x'
58
+ }
59
+ },
60
+ heading: false
61
+ }),
62
+ Placeholder.configure({
63
+ placeholder: ({ node, editor }) =>
64
+ node.type.name === 'paragraph' && editor.isEmpty
65
+ ? 'Digite aqui…'
66
+ : '',
67
+ showOnlyWhenEditable: true,
68
+ includeChildren: false,
69
+ emptyNodeClass:
70
+ 'before:content-[attr(data-placeholder)] before:absolute before:top-0 before:left-0 before:text-neutral-placeholder first:before:float-left first:before:content-[attr(data-placeholder)] first:before:pointer-events-none'
71
+ }),
72
+ Heading.configure({
73
+ levels: [1, 2, 3]
74
+ }).extend({
75
+ renderHTML({ node, HTMLAttributes }) {
76
+ const level: Level = node.attrs.level ?? 1
77
+
78
+ return [
79
+ 'h',
80
+ {
81
+ ...HTMLAttributes,
82
+ class: headingClasses[level]
83
+ },
84
+ 0
85
+ ]
86
+ }
87
+ }),
88
+ ...(editorOptions?.extensions ?? [])
89
+ ]
90
+ })
91
+
92
+ const value = useMemo(() => ({ editor }), [editor])
93
+
94
+ return (
95
+ <EditorContext.Provider value={value}>
96
+ <div
97
+ className={cx('gap-small flex w-full flex-col', className)}
98
+ data-slot="text-area-root"
99
+ {...props}
100
+ />
101
+ </EditorContext.Provider>
102
+ )
103
+ }
104
+
105
+ export function RichEditorContent({
106
+ className,
107
+ ...props
108
+ }: ComponentProps<'div'>) {
109
+ return (
110
+ <div
111
+ className={cx(
112
+ 'bg-layer-2-hover py-small_1x-alt px-small rounded-small-1x min-h-[116px] w-full border border-transparent',
113
+ 'hover:border-border-layer-2 transition-colors duration-200',
114
+ '[&_ul_ul]:list-[circle]',
115
+ '[&_ul_ul_ul]:list-[square]',
116
+ '[&_ol_ol]:list-[lower-alpha]',
117
+ '[&_ol_ol_ol]:list-[lower-roman]',
118
+ 'gap-small flex flex-col overflow-x-hidden overflow-y-auto',
119
+ className
120
+ )}
121
+ data-slot="text-area-content"
122
+ {...props}
123
+ />
124
+ )
125
+ }
126
+
127
+ export function RichEditorCaption({
128
+ className,
129
+ ...props
130
+ }: ComponentProps<'p'>) {
131
+ return (
132
+ <p
133
+ {...props}
134
+ className={cx(
135
+ 'text-size-body-sm text-neutral-dark font-normal',
136
+ className
137
+ )}
138
+ data-slot="text-area-caption"
139
+ />
140
+ )
141
+ }
142
+
143
+ export function RichEditorAreaText({
144
+ className,
145
+ ...props
146
+ }: Omit<ComponentProps<typeof EditorContent>, 'editor'>) {
147
+ const { editor } = useRichTextEditor()
148
+
149
+ return (
150
+ <EditorContent
151
+ className={cx(
152
+ 'text-size-body-md text-neutral-dark max-h-[116px] *:h-full *:w-full *:outline-0',
153
+ className
154
+ )}
155
+ editor={editor}
156
+ {...props}
157
+ />
158
+ )
159
+ }
160
+
161
+ export function RichEditorToolbar({
162
+ className,
163
+ ...props
164
+ }: ComponentProps<'div'>) {
165
+ return (
166
+ <div
167
+ className={cx(
168
+ 'h-large-4x gap-small-1x h-large-6px scrollbar-hide flex flex-nowrap items-center overflow-x-auto overflow-y-hidden',
169
+ className
170
+ )}
171
+ data-slot="text-area-toolbar"
172
+ {...props}
173
+ />
174
+ )
175
+ }
176
+
177
+ export function ToolbarButton({
178
+ appearance = 'ghost',
179
+ variant = 'secondary',
180
+ onMouseDown,
181
+ onPointerDown,
182
+ ...props
183
+ }: ButtonProps) {
184
+ return (
185
+ <Button
186
+ appearance={appearance}
187
+ size="icon_md"
188
+ tabIndex={-1}
189
+ type="button"
190
+ variant={variant}
191
+ onMouseDown={e => {
192
+ e.preventDefault()
193
+ onMouseDown?.(e)
194
+ }}
195
+ onPointerDown={e => {
196
+ e.preventDefault()
197
+ onPointerDown?.(e)
198
+ }}
199
+ {...props}
200
+ />
201
+ )
202
+ }
203
+
204
+ type ToggleButtonName =
205
+ | 'bold'
206
+ | 'italic'
207
+ | 'underline'
208
+ | 'strike'
209
+ | 'bulletList'
210
+ | 'orderedList'
211
+ | 'undo'
212
+ | 'redo'
213
+
214
+ export function ToggleButton({
215
+ name,
216
+ ...props
217
+ }: ButtonProps & { name: ToggleButtonName }) {
218
+ const { editor } = useRichTextEditor()
219
+ const { shortcut } = useOS()
220
+
221
+ const isActive = useRichTextEditorState({
222
+ editor,
223
+ selector: ctx => ctx.editor?.isActive(name) ?? false
224
+ })
225
+
226
+ const icon = useMemo(() => {
227
+ const iconsMap = new Map<ToggleButtonName, IconType>([
228
+ ['bold', 'format_bold'],
229
+ ['italic', 'format_italic'],
230
+ ['underline', 'format_underlined'],
231
+ ['strike', 'format_strikethrough'],
232
+ ['bulletList', 'format_list_bulleted'],
233
+ ['orderedList', 'format_list_numbered'],
234
+ ['undo', 'undo'],
235
+ ['redo', 'redo']
236
+ ])
237
+
238
+ return iconsMap.get(name)
239
+ }, [name])
240
+
241
+ const description = useMemo(() => {
242
+ const descriptionsMap = new Map<ToggleButtonName, string>([
243
+ ['bold', `Negrito (${shortcut} + B)`],
244
+ ['italic', `Itálico (${shortcut} + I)`],
245
+ ['underline', `Sublinhado (${shortcut} + U)`],
246
+ ['strike', `Tachado (${shortcut} + S)`],
247
+ ['bulletList', `Lista (${shortcut} + Shift + 7)`],
248
+ ['orderedList', `Lista numerada (${shortcut} + Shift + 8)`],
249
+ ['undo', `Desfazer (${shortcut} + Z)`],
250
+ ['redo', `Refazer (${shortcut} + Y)`]
251
+ ])
252
+
253
+ return descriptionsMap.get(name)
254
+ }, [name, shortcut])
255
+
256
+ const handleToggle = useCallback(() => {
257
+ if (!editor) throw new Error('Editor not found')
258
+
259
+ const commandsMap = new Map<ToggleButtonName, () => void>([
260
+ ['bold', () => editor.chain().focus().toggleBold().run()],
261
+ ['italic', () => editor.chain().focus().toggleItalic().run()],
262
+ ['underline', () => editor.chain().focus().toggleUnderline().run()],
263
+ ['strike', () => editor.chain().focus().toggleStrike().run()],
264
+ ['bulletList', () => editor.chain().focus().toggleBulletList().run()],
265
+ ['orderedList', () => editor.chain().focus().toggleOrderedList().run()],
266
+ ['undo', () => editor.chain().focus().undo().run()],
267
+ ['redo', () => editor.chain().focus().redo().run()]
268
+ ])
269
+
270
+ commandsMap.get(name)?.()
271
+ }, [editor, name])
272
+
273
+ return (
274
+ <Tooltip>
275
+ <TooltipTrigger asChild={true}>
276
+ <ToolbarButton
277
+ onClick={handleToggle}
278
+ {...(isActive && { appearance: 'solid' })}
279
+ {...props}
280
+ >
281
+ {icon && <Icon className="text-icon-neutral-3" name={icon} />}
282
+ </ToolbarButton>
283
+ </TooltipTrigger>
284
+
285
+ <TooltipContent>
286
+ {description && <Typography size="sm">{description}</Typography>}
287
+ </TooltipContent>
288
+ </Tooltip>
289
+ )
290
+ }
291
+
292
+ export function HeadingButton({
293
+ appearance = 'ghost',
294
+ variant = 'secondary',
295
+ ...props
296
+ }: ButtonProps) {
297
+ const [isOpen, setIsOpen] = useState(false)
298
+ const { editor } = useRichTextEditor()
299
+
300
+ const isActive = useRichTextEditorState({
301
+ editor,
302
+ selector: ctx => ctx.editor?.isActive('heading') ?? false
303
+ })
304
+
305
+ const isLevelActive = useCallback(
306
+ (level: Level) => {
307
+ if (!editor) throw new Error('Editor not found')
308
+ return editor.isActive('heading', { level })
309
+ },
310
+ [editor]
311
+ )
312
+
313
+ const toggleHeading = useCallback(
314
+ (level: Level) => {
315
+ if (!editor) throw new Error('Editor not found')
316
+ setIsOpen(false)
317
+ setTimeout(() => {
318
+ editor.chain().focus().toggleHeading({ level }).run()
319
+ }, 0)
320
+ },
321
+ [editor]
322
+ )
323
+
324
+ const levels: { level: Level; label: string; className?: string }[] = [
325
+ { level: 1, label: 'Normal', className: headingClasses[1] },
326
+ { level: 2, label: 'Grande', className: headingClasses[2] },
327
+ { level: 3, label: 'Enorme', className: headingClasses[3] }
328
+ ]
329
+
330
+ return (
331
+ <DropdownMenuRoot open={isOpen} onOpenChange={open => setIsOpen(open)}>
332
+ <DropdownMenuTrigger asChild={true}>
333
+ <Button
334
+ appearance={isActive ? 'solid' : appearance}
335
+ size="icon_md"
336
+ tabIndex={-1}
337
+ type="button"
338
+ variant={variant}
339
+ {...props}
340
+ >
341
+ <Tooltip>
342
+ <TooltipTrigger asChild={true}>
343
+ <Icon className="text-icon-neutral-3" name="format_size" />
344
+ </TooltipTrigger>
345
+
346
+ <TooltipContent>
347
+ <Typography size="sm">Tamanho</Typography>
348
+ </TooltipContent>
349
+ </Tooltip>
350
+ </Button>
351
+ </DropdownMenuTrigger>
352
+ <DropdownMenuContent>
353
+ <DropdownMenuList>
354
+ {levels.map(level => (
355
+ <DropdownMenuItem
356
+ isActive={isLevelActive(level.level)}
357
+ key={level.level}
358
+ onPointerDown={() => toggleHeading(level.level)}
359
+ >
360
+ <DropdownMenuItemText className={level.className}>
361
+ {level.label}
362
+ </DropdownMenuItemText>
363
+ </DropdownMenuItem>
364
+ ))}
365
+ </DropdownMenuList>
366
+ </DropdownMenuContent>
367
+ </DropdownMenuRoot>
368
+ )
369
+ }
370
+
371
+ export { useRichTextEditorState, useRichTextEditor }
@@ -39,8 +39,7 @@ export function SearchInputContent({
39
39
  {
40
40
  variants: {
41
41
  variant: {
42
- outlined:
43
- 'rounded-circle focus-within:border-primary-idle focus-within:shadow-focus-primary',
42
+ outlined: 'rounded-circle focus-within:border-primary-idle ',
44
43
  line: 'rounded-none border-t-0 border-b-1 border-x-0',
45
44
  action: 'rounded-tr-none rounded-br-none border-r-0'
46
45
  }
@@ -31,7 +31,7 @@ export function SelectTrigger({
31
31
  className,
32
32
  'group h-large-4x rounded-small-1x gap-small py-small_1x-alt px-small relative flex w-full cursor-pointer items-center',
33
33
  'bg-layer-2-idle border-border-layer-2 hover:border-border-layer-3 border',
34
- 'data-[state=open]:border-primary-idle data-[state=open]:shadow-focus-primary',
34
+ 'data-[state=open]:border-primary-idle',
35
35
  'has-[input:not(:placeholder-shown)]:text-primary-idle',
36
36
  'has-[input:not(:placeholder-shown)]:border-primary-idle'
37
37
  )}
@@ -0,0 +1,12 @@
1
+ import type { ComponentProps } from 'react'
2
+ import { cx } from '@xjur-ui/util'
3
+
4
+ export function Skeleton({ className, ...props }: ComponentProps<'div'>) {
5
+ return (
6
+ <div
7
+ className={cx('bg-layer-skeleton animate-pulse', className)}
8
+ data-slot="skeleton"
9
+ {...props}
10
+ />
11
+ )
12
+ }
@@ -0,0 +1,160 @@
1
+ import { cx } from '@xjur-ui/util'
2
+ import { Icon } from './icon'
3
+
4
+ export function Table({ className, ...props }: React.ComponentProps<'table'>) {
5
+ return (
6
+ <div
7
+ className="text-size-body-md scrollbar-thin relative w-full overflow-hidden transition-all hover:overflow-x-auto"
8
+ data-slot="table-container"
9
+ >
10
+ <table
11
+ className={cx('w-full caption-bottom', className)}
12
+ data-slot="table"
13
+ {...props}
14
+ />
15
+ </div>
16
+ )
17
+ }
18
+
19
+ export function TableHeader({
20
+ className,
21
+ ...props
22
+ }: React.ComponentProps<'thead'>) {
23
+ return (
24
+ <thead
25
+ className={cx(
26
+ 'group/table-header [&_tr]:border-border-layer-2 hover:bg-layer-2-hover [&_tr]:border-b',
27
+ className
28
+ )}
29
+ data-slot="table-header"
30
+ {...props}
31
+ />
32
+ )
33
+ }
34
+
35
+ export function TableBody({
36
+ className,
37
+ ...props
38
+ }: React.ComponentProps<'tbody'>) {
39
+ return (
40
+ <tbody
41
+ className={cx(
42
+ '[&_tr]:border-border-layer-2 [&_tr]:border-b [&_tr:last-child]:border-0',
43
+ className
44
+ )}
45
+ data-slot="table-body"
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ export function TableFooter({
52
+ className,
53
+ ...props
54
+ }: React.ComponentProps<'tfoot'>) {
55
+ return (
56
+ <tfoot
57
+ className={cx(
58
+ 'border-border-layer-2 border-t font-medium [&>tr]:last:border-b-0',
59
+ className
60
+ )}
61
+ data-slot="table-footer"
62
+ {...props}
63
+ />
64
+ )
65
+ }
66
+
67
+ export function TableRow({
68
+ className,
69
+ isSelected = false,
70
+ ...props
71
+ }: React.ComponentProps<'tr'> & { isSelected?: boolean }) {
72
+ return (
73
+ <tr
74
+ className={cx(
75
+ 'bg-layer-2-idle hover:bg-layer-2-hover ease border-b transition-colors duration-100',
76
+ 'active:bg-layer-2-pressed',
77
+ 'data-[selected=true]:bg-primary-alt-active',
78
+ className
79
+ )}
80
+ data-selected={isSelected}
81
+ data-slot="table-row"
82
+ {...props}
83
+ />
84
+ )
85
+ }
86
+
87
+ export function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
88
+ return (
89
+ <th
90
+ className={cx(
91
+ 'group/th text-neutral-placeholder text-size-label-sm h-large-4x py-small-1x px-small text-left align-middle font-medium whitespace-nowrap',
92
+ 'pr-large-2x relative',
93
+ 'animate-in fade-in duration-200 ease-in',
94
+ className
95
+ )}
96
+ data-slot="table-head"
97
+ {...props}
98
+ />
99
+ )
100
+ }
101
+
102
+ export function TableHeadContent({
103
+ className,
104
+ ...props
105
+ }: React.ComponentProps<'div'>) {
106
+ return (
107
+ <div
108
+ className={cx(
109
+ 'gap-small pr-large-3x relative flex w-fit items-center',
110
+ className
111
+ )}
112
+ {...props}
113
+ />
114
+ )
115
+ }
116
+
117
+ export function TableSort({
118
+ className,
119
+ sortedDirection,
120
+ ...props
121
+ }: React.ComponentProps<'button'> & { sortedDirection?: 'asc' | 'desc' }) {
122
+ return (
123
+ <button
124
+ className={cx(
125
+ 'size-large-1x rounded-small-1x',
126
+ 'flex cursor-pointer items-center justify-center',
127
+ 'hidden group-hover/th:flex',
128
+ 'data-[sorted=true]:flex',
129
+ 'transition-all duration-200 ease-out',
130
+ 'data-[sorted=true]:bg-primary-alt-active data-[sorted=true]:text-primary-label-active',
131
+ 'absolute top-1/2 right-0 -translate-y-1/2',
132
+ className
133
+ )}
134
+ data-sorted={sortedDirection !== undefined}
135
+ type="button"
136
+ {...props}
137
+ >
138
+ {sortedDirection !== undefined ? (
139
+ <Icon
140
+ name={sortedDirection === 'asc' ? 'arrow_downward' : 'arrow_upward'}
141
+ />
142
+ ) : (
143
+ <Icon name="swap_vert" />
144
+ )}
145
+ </button>
146
+ )
147
+ }
148
+
149
+ export function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
150
+ return (
151
+ <td
152
+ className={cx(
153
+ 'px-small py-none h-large-8x text-left align-middle whitespace-nowrap',
154
+ className
155
+ )}
156
+ data-slot="table-cell"
157
+ {...props}
158
+ />
159
+ )
160
+ }