@xjur-ui/react 1.0.3 → 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.
@@ -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,63 @@
1
+ import * as TooltipPrimitive from '@radix-ui/react-tooltip'
2
+ import { cx } from '@xjur-ui/util'
3
+
4
+ function TooltipProvider({
5
+ delayDuration = 0,
6
+ ...props
7
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
8
+ return (
9
+ <TooltipPrimitive.Provider
10
+ data-slot="tooltip-provider"
11
+ delayDuration={delayDuration}
12
+ {...props}
13
+ />
14
+ )
15
+ }
16
+
17
+ function Tooltip({
18
+ ...props
19
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
20
+ return (
21
+ <TooltipProvider>
22
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
23
+ </TooltipProvider>
24
+ )
25
+ }
26
+
27
+ function TooltipTrigger({
28
+ ...props
29
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
30
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
31
+ }
32
+
33
+ function TooltipContent({
34
+ className,
35
+ sideOffset = 1,
36
+ variant = 'simple',
37
+ children,
38
+ ...props
39
+ }: React.ComponentProps<typeof TooltipPrimitive.Content> & {
40
+ variant?: 'simple' | 'rich'
41
+ }) {
42
+ return (
43
+ <TooltipPrimitive.Portal>
44
+ <TooltipPrimitive.Content
45
+ className={cx(
46
+ 'text-size-body-sm text-balance',
47
+ 'data-[variant=simple]:bg-alt-2 data-[variant=simple]:text-neutral-label data-[variant=simple]:rounded-small-1x data-[variant=simple]:px-small data-[variant=simple]:py-small-1x',
48
+ 'data-[variant=rich]:bg-layer-2-idle data-[variant=rich]:text-neutral-dark data-[variant=rich]:rounded-small data-[variant=rich]:border-border-layer-2 data-[variant=rich]:p-large data-[variant=rich]:border',
49
+ 'animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin)',
50
+ className
51
+ )}
52
+ data-slot="tooltip-content"
53
+ data-variant={variant}
54
+ sideOffset={sideOffset}
55
+ {...props}
56
+ >
57
+ {children}
58
+ </TooltipPrimitive.Content>
59
+ </TooltipPrimitive.Portal>
60
+ )
61
+ }
62
+
63
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1 @@
1
+ export { useOS } from './use-os'
@@ -0,0 +1,32 @@
1
+ import { useState, useEffect } from 'react'
2
+
3
+ export type OS = 'ios' | 'macos' | 'windows' | 'android' | 'linux' | 'unknown'
4
+
5
+ export function useOS() {
6
+ const [os, setOs] = useState<OS>('unknown')
7
+
8
+ useEffect(() => {
9
+ if (typeof window === 'undefined') return
10
+
11
+ const userAgent = window.navigator.userAgent
12
+
13
+ if (/iPad|iPhone|iPod/.test(userAgent)) {
14
+ setOs('ios')
15
+ } else if (/Mac/.test(userAgent)) {
16
+ setOs('macos')
17
+ } else if (/Win/.test(userAgent)) {
18
+ setOs('windows')
19
+ } else if (/Android/.test(userAgent)) {
20
+ setOs('android')
21
+ } else if (/Linux/.test(userAgent)) {
22
+ setOs('linux')
23
+ } else {
24
+ setOs('unknown')
25
+ }
26
+ }, [])
27
+
28
+ return {
29
+ os,
30
+ shortcut: ['macos', 'ios'].includes(os) ? '⌘' : 'Ctrl'
31
+ }
32
+ }
@@ -140,7 +140,11 @@ export const icons = [
140
140
  'history',
141
141
  'security',
142
142
  'bar_chart',
143
- 'category'
143
+ 'category',
144
+ 'format_underlined',
145
+ 'format_list_bulleted',
146
+ 'format_list_numbered',
147
+ 'redo'
144
148
  ] as const
145
149
 
146
150
  export type IconType = (typeof icons)[number]