@lexibase/rdx-ui 1.0.0-beta.1 → 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.
@@ -0,0 +1,231 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import { usePathname } from 'next/navigation'
5
+
6
+ import { cn } from '@lexibase/rdx-ui/lib/utils'
7
+ import {
8
+ Popover,
9
+ PopoverContent,
10
+ PopoverTitle,
11
+ PopoverTrigger,
12
+ type PopoverActions,
13
+ } from './popover'
14
+
15
+ type Heading = { id: string; title: string; depth: number }
16
+ type HeadingNode = Heading & { children: HeadingNode[] }
17
+
18
+ function headingTree(headings: Heading[]) {
19
+ const roots: HeadingNode[] = []
20
+ const parents: HeadingNode[] = []
21
+ for (const heading of headings) {
22
+ while (
23
+ parents.length &&
24
+ parents[parents.length - 1]!.depth >= heading.depth
25
+ )
26
+ parents.pop()
27
+ const node: HeadingNode = { ...heading, children: [] }
28
+ const siblings = parents[parents.length - 1]?.children ?? roots
29
+ siblings.push(node)
30
+ parents.push(node)
31
+ }
32
+ return roots
33
+ }
34
+
35
+ function HeadingLinks({
36
+ nodes,
37
+ baseDepth,
38
+ activeId,
39
+ }: {
40
+ nodes: HeadingNode[]
41
+ baseDepth: number
42
+ activeId: string
43
+ }) {
44
+ return (
45
+ <ol className="space-y-0.5">
46
+ {nodes.map((heading) => (
47
+ <li key={heading.id}>
48
+ <a
49
+ href={`#${encodeURIComponent(heading.id)}`}
50
+ aria-current={heading.id === activeId ? 'location' : undefined}
51
+ style={{ paddingLeft: 8 + (heading.depth - baseDepth) * 12 }}
52
+ className={cn(
53
+ 'block rounded-md py-1.5 pr-2 text-xs leading-5 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
54
+ heading.id === activeId &&
55
+ 'bg-accent/60 font-medium text-foreground'
56
+ )}
57
+ >
58
+ {heading.title}
59
+ </a>
60
+ {heading.children.length > 0 && (
61
+ <HeadingLinks
62
+ nodes={heading.children}
63
+ baseDepth={baseDepth}
64
+ activeId={activeId}
65
+ />
66
+ )}
67
+ </li>
68
+ ))}
69
+ </ol>
70
+ )
71
+ }
72
+
73
+ /** Read the rendered MDX, sharing exactly the IDs used by document links. */
74
+ export function DocsToc({ contentId = 'rdx-main' }: { contentId?: string }) {
75
+ const pathname = usePathname()
76
+ return <PageToc key={pathname} contentId={contentId} />
77
+ }
78
+
79
+ function PageToc({ contentId }: { contentId: string }) {
80
+ const [headings, setHeadings] = useState<Heading[]>([])
81
+ const [activeId, setActiveId] = useState('')
82
+ const actions = useRef<PopoverActions | null>(null)
83
+
84
+ useEffect(() => {
85
+ const content = document.getElementById(contentId)
86
+ if (!content) return
87
+ let elements: HTMLHeadingElement[] = []
88
+ let frame = 0
89
+ let scrollRoot: HTMLElement | null = content.parentElement
90
+ while (
91
+ scrollRoot &&
92
+ !/(auto|scroll)/.test(getComputedStyle(scrollRoot).overflowY)
93
+ ) {
94
+ scrollRoot = scrollRoot.parentElement
95
+ }
96
+ const scrollTarget = scrollRoot ?? window
97
+ // Keep native fragment links/history; smooth the actual document scroller.
98
+ // Tailwind's motion-safe variant leaves reduced-motion navigation instant.
99
+ const scrollElement = scrollRoot ?? document.documentElement
100
+ const alreadySmooth = scrollElement.classList.contains(
101
+ 'motion-safe:scroll-smooth'
102
+ )
103
+ scrollElement.classList.add('motion-safe:scroll-smooth')
104
+ function updateActive() {
105
+ frame = 0
106
+ const top = (scrollRoot?.getBoundingClientRect().top ?? 0) + 96
107
+ let current = elements[0]?.id ?? ''
108
+ for (const heading of elements) {
109
+ if (heading.getBoundingClientRect().top > top) break
110
+ current = heading.id
111
+ }
112
+ // A short final section cannot reach the top: there is no content left
113
+ // to scroll. Reaching the document's bottom must still activate it.
114
+ if (
115
+ elements.length &&
116
+ scrollElement.scrollHeight > scrollElement.clientHeight + 2 &&
117
+ scrollElement.scrollTop + scrollElement.clientHeight >=
118
+ scrollElement.scrollHeight - 2
119
+ )
120
+ current = elements[elements.length - 1]!.id
121
+ setActiveId(current)
122
+ }
123
+ function schedule() {
124
+ if (!frame) frame = requestAnimationFrame(updateActive)
125
+ }
126
+ function collect() {
127
+ if (!window.matchMedia('(min-width: 1280px)').matches)
128
+ actions.current?.close()
129
+ elements = Array.from(
130
+ content!.querySelectorAll<HTMLHeadingElement>(
131
+ 'h2[id], h3[id], h4[id], h5[id], h6[id]'
132
+ )
133
+ ).filter(
134
+ (element) =>
135
+ element.getClientRects().length > 0 &&
136
+ !!element.id &&
137
+ !!element.textContent?.trim() &&
138
+ getComputedStyle(element).visibility !== 'hidden'
139
+ )
140
+ setHeadings(
141
+ elements
142
+ .map((element) => ({
143
+ id: element.id,
144
+ title: element.textContent?.trim() ?? '',
145
+ depth: Number(element.tagName.slice(1)),
146
+ }))
147
+ .filter((heading) => heading.title)
148
+ )
149
+ schedule()
150
+ }
151
+ const initialFrame = requestAnimationFrame(collect)
152
+ const observer = new MutationObserver(collect)
153
+ observer.observe(content, {
154
+ childList: true,
155
+ subtree: true,
156
+ characterData: true,
157
+ attributes: true,
158
+ attributeFilter: ['hidden', 'style', 'class', 'id'],
159
+ })
160
+ scrollTarget.addEventListener('scroll', schedule, { passive: true })
161
+ window.addEventListener('resize', collect)
162
+ const resize = new ResizeObserver(schedule)
163
+ resize.observe(content)
164
+ return () => {
165
+ observer.disconnect()
166
+ resize.disconnect()
167
+ scrollTarget.removeEventListener('scroll', schedule)
168
+ window.removeEventListener('resize', collect)
169
+ cancelAnimationFrame(frame)
170
+ cancelAnimationFrame(initialFrame)
171
+ if (!alreadySmooth)
172
+ scrollElement.classList.remove('motion-safe:scroll-smooth')
173
+ }
174
+ }, [contentId])
175
+
176
+ if (headings.length < 2) return null
177
+ const baseDepth = Math.min(...headings.map((heading) => heading.depth))
178
+ return (
179
+ <aside
180
+ data-rdx-toc
181
+ className="fixed right-4 top-1/2 z-30 hidden -translate-y-1/2 xl:block"
182
+ >
183
+ <Popover actionsRef={actions}>
184
+ <PopoverTrigger
185
+ openOnHover
186
+ delay={120}
187
+ aria-label="On this page"
188
+ className="flex max-h-[40vh] w-12 flex-col items-end gap-2 overflow-hidden rounded-md p-3 text-muted-foreground/40 outline-none transition-colors hover:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring"
189
+ >
190
+ {headings.map((heading) => (
191
+ <span
192
+ key={heading.id}
193
+ aria-hidden="true"
194
+ className={cn(
195
+ 'h-0.5 shrink-0 rounded-full bg-current transition-colors',
196
+ heading.depth === baseDepth ? 'w-5' : 'w-3',
197
+ heading.id === activeId && 'text-foreground'
198
+ )}
199
+ />
200
+ ))}
201
+ </PopoverTrigger>
202
+ <PopoverContent
203
+ side="left"
204
+ sideOffset={0}
205
+ onMouseLeave={(event) => {
206
+ // Base UI keeps click-opened popovers pinned; the TOC instead
207
+ // stays open only while the pointer is over its popup or marks.
208
+ if (
209
+ event.relatedTarget instanceof Element &&
210
+ event.relatedTarget.closest('[data-rdx-toc]')
211
+ )
212
+ return
213
+ actions.current?.close()
214
+ }}
215
+ className="hidden max-h-[min(60vh,var(--available-height))] w-64 overflow-y-auto xl:flex"
216
+ >
217
+ <PopoverTitle className="px-2 pt-1 text-xs text-muted-foreground">
218
+ On this page
219
+ </PopoverTitle>
220
+ <nav aria-label="On this page">
221
+ <HeadingLinks
222
+ nodes={headingTree(headings)}
223
+ baseDepth={baseDepth}
224
+ activeId={activeId}
225
+ />
226
+ </nav>
227
+ </PopoverContent>
228
+ </Popover>
229
+ </aside>
230
+ )
231
+ }
@@ -1,15 +1,15 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
4
+ import { Menu as DropdownMenuPrimitive } from '@base-ui/react/menu'
5
5
  import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
6
6
 
7
- import { cn } from '@lexibase/rdx-ui/lib/utils'
7
+ import { cn, mergeStateClassName } from '@lexibase/rdx-ui/lib/utils'
8
8
 
9
9
  function DropdownMenu({
10
10
  ...props
11
11
  }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
12
- return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
12
+ return <DropdownMenuPrimitive.Root {...props} />
13
13
  }
14
14
 
15
15
  function DropdownMenuPortal({
@@ -31,22 +31,42 @@ function DropdownMenuTrigger({
31
31
  )
32
32
  }
33
33
 
34
+ type DropdownMenuContentProps = React.ComponentProps<
35
+ typeof DropdownMenuPrimitive.Popup
36
+ > &
37
+ Pick<
38
+ React.ComponentProps<typeof DropdownMenuPrimitive.Positioner>,
39
+ 'align' | 'alignOffset' | 'side' | 'sideOffset' | 'collisionPadding'
40
+ >
41
+
34
42
  function DropdownMenuContent({
35
43
  className,
44
+ align = 'start',
45
+ alignOffset = 0,
46
+ side = 'bottom',
36
47
  sideOffset = 4,
48
+ collisionPadding = 8,
37
49
  ...props
38
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
50
+ }: DropdownMenuContentProps) {
39
51
  return (
40
52
  <DropdownMenuPrimitive.Portal>
41
- <DropdownMenuPrimitive.Content
42
- data-slot="dropdown-menu-content"
53
+ <DropdownMenuPrimitive.Positioner
54
+ className="isolate z-50 outline-none"
55
+ align={align}
56
+ alignOffset={alignOffset}
57
+ side={side}
43
58
  sideOffset={sideOffset}
44
- className={cn(
45
- 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
46
- className
47
- )}
48
- {...props}
49
- />
59
+ collisionPadding={collisionPadding}
60
+ >
61
+ <DropdownMenuPrimitive.Popup
62
+ data-slot="dropdown-menu-content"
63
+ className={mergeStateClassName(
64
+ 'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 max-h-(--available-height) min-w-[8rem] origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md outline-none',
65
+ className
66
+ )}
67
+ {...props}
68
+ />
69
+ </DropdownMenuPrimitive.Positioner>
50
70
  </DropdownMenuPrimitive.Portal>
51
71
  )
52
72
  }
@@ -73,8 +93,31 @@ function DropdownMenuItem({
73
93
  data-slot="dropdown-menu-item"
74
94
  data-inset={inset}
75
95
  data-variant={variant}
76
- className={cn(
77
- "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
96
+ className={mergeStateClassName(
97
+ "data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
98
+ className
99
+ )}
100
+ {...props}
101
+ />
102
+ )
103
+ }
104
+
105
+ function DropdownMenuLinkItem({
106
+ className,
107
+ inset,
108
+ variant = 'default',
109
+ ...props
110
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.LinkItem> & {
111
+ inset?: boolean
112
+ variant?: 'default' | 'destructive'
113
+ }) {
114
+ return (
115
+ <DropdownMenuPrimitive.LinkItem
116
+ data-slot="dropdown-menu-item"
117
+ data-inset={inset}
118
+ data-variant={variant}
119
+ className={mergeStateClassName(
120
+ "data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
78
121
  className
79
122
  )}
80
123
  {...props}
@@ -91,17 +134,17 @@ function DropdownMenuCheckboxItem({
91
134
  return (
92
135
  <DropdownMenuPrimitive.CheckboxItem
93
136
  data-slot="dropdown-menu-checkbox-item"
94
- className={cn(
95
- "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
137
+ className={mergeStateClassName(
138
+ "data-highlighted:bg-accent data-highlighted:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
96
139
  className
97
140
  )}
98
141
  checked={checked}
99
142
  {...props}
100
143
  >
101
144
  <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
102
- <DropdownMenuPrimitive.ItemIndicator>
145
+ <DropdownMenuPrimitive.CheckboxItemIndicator>
103
146
  <CheckIcon className="size-4" />
104
- </DropdownMenuPrimitive.ItemIndicator>
147
+ </DropdownMenuPrimitive.CheckboxItemIndicator>
105
148
  </span>
106
149
  {children}
107
150
  </DropdownMenuPrimitive.CheckboxItem>
@@ -127,16 +170,16 @@ function DropdownMenuRadioItem({
127
170
  return (
128
171
  <DropdownMenuPrimitive.RadioItem
129
172
  data-slot="dropdown-menu-radio-item"
130
- className={cn(
131
- "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
173
+ className={mergeStateClassName(
174
+ "data-highlighted:bg-accent data-highlighted:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
132
175
  className
133
176
  )}
134
177
  {...props}
135
178
  >
136
179
  <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
137
- <DropdownMenuPrimitive.ItemIndicator>
180
+ <DropdownMenuPrimitive.RadioItemIndicator>
138
181
  <CircleIcon className="size-2 fill-current" />
139
- </DropdownMenuPrimitive.ItemIndicator>
182
+ </DropdownMenuPrimitive.RadioItemIndicator>
140
183
  </span>
141
184
  {children}
142
185
  </DropdownMenuPrimitive.RadioItem>
@@ -147,14 +190,14 @@ function DropdownMenuLabel({
147
190
  className,
148
191
  inset,
149
192
  ...props
150
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
193
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.GroupLabel> & {
151
194
  inset?: boolean
152
195
  }) {
153
196
  return (
154
- <DropdownMenuPrimitive.Label
197
+ <DropdownMenuPrimitive.GroupLabel
155
198
  data-slot="dropdown-menu-label"
156
199
  data-inset={inset}
157
- className={cn(
200
+ className={mergeStateClassName(
158
201
  'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
159
202
  className
160
203
  )}
@@ -170,7 +213,7 @@ function DropdownMenuSeparator({
170
213
  return (
171
214
  <DropdownMenuPrimitive.Separator
172
215
  data-slot="dropdown-menu-separator"
173
- className={cn('bg-border -mx-1 my-1 h-px', className)}
216
+ className={mergeStateClassName('bg-border -mx-1 my-1 h-px', className)}
174
217
  {...props}
175
218
  />
176
219
  )
@@ -194,8 +237,8 @@ function DropdownMenuShortcut({
194
237
 
195
238
  function DropdownMenuSub({
196
239
  ...props
197
- }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
198
- return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
240
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubmenuRoot>) {
241
+ return <DropdownMenuPrimitive.SubmenuRoot {...props} />
199
242
  }
200
243
 
201
244
  function DropdownMenuSubTrigger({
@@ -203,36 +246,41 @@ function DropdownMenuSubTrigger({
203
246
  inset,
204
247
  children,
205
248
  ...props
206
- }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
249
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubmenuTrigger> & {
207
250
  inset?: boolean
208
251
  }) {
209
252
  return (
210
- <DropdownMenuPrimitive.SubTrigger
253
+ <DropdownMenuPrimitive.SubmenuTrigger
211
254
  data-slot="dropdown-menu-sub-trigger"
212
255
  data-inset={inset}
213
- className={cn(
214
- 'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
256
+ className={mergeStateClassName(
257
+ 'data-highlighted:bg-accent data-highlighted:text-accent-foreground data-popup-open:bg-accent data-popup-open:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
215
258
  className
216
259
  )}
217
260
  {...props}
218
261
  >
219
262
  {children}
220
263
  <ChevronRightIcon className="ml-auto size-4" />
221
- </DropdownMenuPrimitive.SubTrigger>
264
+ </DropdownMenuPrimitive.SubmenuTrigger>
222
265
  )
223
266
  }
224
267
 
225
268
  function DropdownMenuSubContent({
226
269
  className,
270
+ align = 'start',
271
+ alignOffset = -3,
272
+ side = 'right',
273
+ sideOffset = 0,
227
274
  ...props
228
- }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
275
+ }: DropdownMenuContentProps) {
229
276
  return (
230
- <DropdownMenuPrimitive.SubContent
277
+ <DropdownMenuContent
231
278
  data-slot="dropdown-menu-sub-content"
232
- className={cn(
233
- 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
234
- className
235
- )}
279
+ align={align}
280
+ alignOffset={alignOffset}
281
+ side={side}
282
+ sideOffset={sideOffset}
283
+ className={mergeStateClassName('shadow-lg', className)}
236
284
  {...props}
237
285
  />
238
286
  )
@@ -246,6 +294,7 @@ export {
246
294
  DropdownMenuGroup,
247
295
  DropdownMenuLabel,
248
296
  DropdownMenuItem,
297
+ DropdownMenuLinkItem,
249
298
  DropdownMenuCheckboxItem,
250
299
  DropdownMenuRadioGroup,
251
300
  DropdownMenuRadioItem,
@@ -1,7 +1,6 @@
1
1
  import type { ReactNode } from 'react'
2
- import Link from 'next/link'
3
2
 
4
- import { Button } from './button'
3
+ import { ButtonLink } from './button-link'
5
4
  import { Logo } from './logo'
6
5
 
7
6
  /** The default landing page from apps/web, not a separate marketing theme. */
@@ -33,13 +32,13 @@ export function Home({
33
32
  {buttons.length > 0 && (
34
33
  <div className="flex flex-wrap justify-center gap-3 mt-6 px-6">
35
34
  {buttons.map((button, index) => (
36
- <Button
35
+ <ButtonLink
37
36
  key={`${index}:${button.href}`}
38
37
  variant={index === 0 ? 'default' : 'outline'}
39
- asChild
38
+ href={button.href}
40
39
  >
41
- <Link href={button.href}>{button.label}</Link>
42
- </Button>
40
+ {button.label}
41
+ </ButtonLink>
43
42
  ))}
44
43
  </div>
45
44
  )}
@@ -1,51 +1,142 @@
1
1
  import type { ComponentProps } from 'react'
2
2
 
3
+ import { cn } from '../lib/utils'
4
+ import {
5
+ Accordion,
6
+ AccordionContent,
7
+ AccordionItem,
8
+ AccordionTrigger,
9
+ } from './accordion'
10
+ import { Blockquote } from './blockquote'
11
+ import { Callout } from './callout'
12
+ import { CodeBlock } from './code-block'
13
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from './tabs'
3
14
  import { Separator } from '@lexibase/rdx-ui/components/separator'
4
15
 
5
16
  export const mdxComponents = {
6
- h1: (props: ComponentProps<'h1'>) => (
7
- <h1 className="text-4xl font-bold my-6" {...props} />
17
+ h1: ({ className, ...props }: ComponentProps<'h1'>) => (
18
+ <h1 className={cn('text-4xl font-bold my-6', className)} {...props} />
8
19
  ),
9
- h2: (props: ComponentProps<'h2'>) => (
10
- <h2 className="text-2xl font-bold my-6" {...props} />
20
+ h2: ({ className, ...props }: ComponentProps<'h2'>) => (
21
+ <h2 className={cn('text-2xl font-bold my-6', className)} {...props} />
11
22
  ),
12
- h3: (props: ComponentProps<'h3'>) => (
13
- <h3 className="text-xl font-bold my-6" {...props} />
23
+ h3: ({ className, ...props }: ComponentProps<'h3'>) => (
24
+ <h3 className={cn('text-xl font-bold my-6', className)} {...props} />
14
25
  ),
15
- p: (props: ComponentProps<'p'>) => <p className="my-4" {...props} />,
16
- ul: (props: ComponentProps<'ul'>) => (
17
- <ul className="list-disc my-4 ml-6" {...props} />
26
+ p: ({ className, ...props }: ComponentProps<'p'>) => (
27
+ <p className={cn('my-4', className)} {...props} />
18
28
  ),
19
- ol: (props: ComponentProps<'ol'>) => (
20
- <ol className="list-decimal my-4 ml-6" {...props} />
21
- ),
22
- li: (props: ComponentProps<'li'>) => (
23
- <li className="my-2 before:mr-2" {...props} />
29
+ ul: ({ className, ...props }: ComponentProps<'ul'>) => (
30
+ <ul
31
+ className={cn(
32
+ 'list-disc my-4 ml-6 [&.contains-task-list]:list-none [&.contains-task-list]:ml-0',
33
+ className
34
+ )}
35
+ {...props}
36
+ />
24
37
  ),
25
- a: (props: ComponentProps<'a'>) => (
26
- <a className="underline text-chart-1" {...props} />
38
+ ol: ({ className, ...props }: ComponentProps<'ol'>) => (
39
+ <ol className={cn('list-decimal my-4 ml-6', className)} {...props} />
27
40
  ),
28
- blockquote: (props: ComponentProps<'blockquote'>) => (
29
- <blockquote
30
- className="bg-secondary text-muted-foreground border-l-2 pl-4 py-px my-2"
41
+ li: ({ className, ...props }: ComponentProps<'li'>) => (
42
+ <li
43
+ className={cn(
44
+ 'my-2 before:mr-2 [&>input[type=checkbox]]:mr-2',
45
+ className
46
+ )}
31
47
  {...props}
32
48
  />
33
49
  ),
34
- pre: (props: ComponentProps<'pre'>) => (
35
- <pre
36
- className="bg-secondary text-secondary-foreground rounded-md p-4 my-4 overflow-x-auto"
50
+ a: ({ className, ...props }: ComponentProps<'a'>) => (
51
+ <a
52
+ className={cn('underline text-chart-1 text-blue-500', className)}
37
53
  {...props}
38
54
  />
39
55
  ),
40
- code: (props: ComponentProps<'code'>) => (
56
+ blockquote: Blockquote,
57
+ pre: CodeBlock,
58
+ code: ({ className, ...props }: ComponentProps<'code'>) => (
41
59
  <code
42
- className="bg-secondary text-secondary-foreground rounded-sm px-1 py-0.5"
60
+ className={cn(
61
+ 'bg-secondary text-secondary-foreground rounded-sm px-1 py-0.5',
62
+ className
63
+ )}
43
64
  {...props}
44
65
  />
45
66
  ),
46
67
 
47
- // Custom
48
- Separator: (props: ComponentProps<typeof Separator>) => (
49
- <Separator className="my-8" {...props} />
68
+ h4: ({ className, ...props }: ComponentProps<'h4'>) => (
69
+ <h4 className={cn('text-lg font-semibold my-4', className)} {...props} />
70
+ ),
71
+ h5: ({ className, ...props }: ComponentProps<'h5'>) => (
72
+ <h5 className={cn('text-base font-semibold my-4', className)} {...props} />
73
+ ),
74
+ h6: ({ className, ...props }: ComponentProps<'h6'>) => (
75
+ <h6 className={cn('text-sm font-semibold my-4', className)} {...props} />
76
+ ),
77
+ hr: ({ className, ...props }: ComponentProps<'hr'>) => (
78
+ <hr className={cn('my-8 border-border', className)} {...props} />
79
+ ),
80
+ img: ({ className, alt = '', ...props }: ComponentProps<'img'>) => (
81
+ <img
82
+ alt={alt}
83
+ className={cn('max-w-full h-auto rounded-md my-4', className)}
84
+ {...props}
85
+ />
86
+ ),
87
+ table: ({ className, ...props }: ComponentProps<'table'>) => (
88
+ <div
89
+ data-slot="mdx-table-scroll"
90
+ role="region"
91
+ aria-label="Table"
92
+ tabIndex={0}
93
+ className="my-6 min-w-0 max-w-full overflow-x-auto rounded-lg border border-border bg-background focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
94
+ >
95
+ <table
96
+ className={cn(
97
+ 'w-full border-separate border-spacing-0 text-sm [&_tbody>tr]:transition-colors motion-reduce:[&_tbody>tr]:transition-none [&_tbody>tr:nth-child(even)]:bg-muted/20 [&_tbody>tr:hover]:bg-muted/40 [&_tbody>tr:last-child>td]:border-b-0',
98
+ className
99
+ )}
100
+ {...props}
101
+ />
102
+ </div>
103
+ ),
104
+ th: ({ className, scope = 'col', ...props }: ComponentProps<'th'>) => (
105
+ <th
106
+ scope={scope}
107
+ className={cn(
108
+ 'border-b border-r border-border bg-muted/40 px-4 py-3 text-left text-xs font-semibold tracking-wide text-foreground/90 last:border-r-0',
109
+ className
110
+ )}
111
+ {...props}
112
+ />
113
+ ),
114
+ td: ({ className, ...props }: ComponentProps<'td'>) => (
115
+ <td
116
+ className={cn(
117
+ 'min-w-40 border-b border-r border-border/70 px-4 py-3 align-top leading-6 last:border-r-0',
118
+ className
119
+ )}
120
+ {...props}
121
+ />
122
+ ),
123
+ Callout,
124
+ Accordion,
125
+ AccordionItem,
126
+ AccordionTrigger,
127
+ AccordionContent,
128
+ Tabs,
129
+ TabsList,
130
+ TabsTrigger,
131
+ TabsContent,
132
+ Separator: ({ className, ...props }: ComponentProps<typeof Separator>) => (
133
+ <Separator
134
+ className={
135
+ typeof className === 'function'
136
+ ? (state) => cn('my-8', className(state))
137
+ : cn('my-8', className)
138
+ }
139
+ {...props}
140
+ />
50
141
  ),
51
142
  }