@lexibase/rdx-ui 1.0.0-beta.1 → 1.0.0-beta.2

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.
@@ -1,8 +1,7 @@
1
1
  import type { ReactNode } from 'react'
2
2
  import { Github } from 'lucide-react'
3
- import Link from 'next/link'
4
3
 
5
- import { Button } from './button'
4
+ import { ButtonLink } from './button-link'
6
5
  import { Logo } from './logo'
7
6
  import { Separator } from './separator'
8
7
  import { ModeToggle } from './mode-toggle'
@@ -16,6 +15,7 @@ export function Navbar({
16
15
  docsHref = '/docs',
17
16
  docsLink,
18
17
  versionMenu,
18
+ search,
19
19
  github,
20
20
  showToggleMode = false,
21
21
  }: {
@@ -26,6 +26,7 @@ export function Navbar({
26
26
  docsHref?: string
27
27
  docsLink?: ReactNode
28
28
  versionMenu?: ReactNode
29
+ search?: ReactNode
29
30
  github?: { visible: boolean; url: string }
30
31
  showToggleMode?: boolean
31
32
  }) {
@@ -36,48 +37,41 @@ export function Navbar({
36
37
  className="w-full flex flex-wrap shrink-0 items-center justify-between px-6 py-2 border-b border-border gap-y-2"
37
38
  >
38
39
  <div className="flex items-center gap-1">
39
- <Button
40
+ <ButtonLink
40
41
  variant="ghost"
41
42
  size={title === '' ? 'icon' : 'default'}
42
- asChild
43
43
  aria-label={siteTitle}
44
+ href={homeHref}
45
+ className="cursor-default flex items-center gap-2"
44
46
  >
45
- <Link
46
- href={homeHref}
47
- className="cursor-default flex items-center gap-2"
48
- >
49
- {logo}
50
- {title !== '' && <span>{title}</span>}
51
- </Link>
52
- </Button>
47
+ {logo}
48
+ {title !== '' && <span>{title}</span>}
49
+ </ButtonLink>
53
50
  </div>
54
51
  <div className="flex items-center md:gap-x-2 h-4">
52
+ {search}
55
53
  {versionMenu}
56
54
  {docsLink ?? (
57
- <Button variant="link" size="sm" asChild>
58
- <Link href={docsHref}>Docs</Link>
59
- </Button>
55
+ <ButtonLink variant="link" size="sm" href={docsHref}>
56
+ Docs
57
+ </ButtonLink>
60
58
  )}
61
59
  {github?.visible && github.url && (
62
60
  <>
63
61
  <Separator orientation="vertical" />
64
- <Button
62
+ <ButtonLink
65
63
  variant="ghost"
66
64
  size="icon"
67
65
  title="GitHub"
68
- asChild
69
66
  aria-label="GitHub"
67
+ href={github.url}
68
+ target={github.url === '#' ? undefined : '_blank'}
69
+ rel={github.url === '#' ? undefined : 'noopener noreferrer'}
70
+ className="cursor-default"
70
71
  >
71
- <Link
72
- className="cursor-default"
73
- href={github.url}
74
- target={github.url === '#' ? undefined : '_blank'}
75
- rel={github.url === '#' ? undefined : 'noopener noreferrer'}
76
- >
77
- <Github className="h-[1.2rem] w-[1.2rem]" />
78
- <span className="sr-only">GitHub</span>
79
- </Link>
80
- </Button>
72
+ <Github className="h-[1.2rem] w-[1.2rem]" />
73
+ <span className="sr-only">GitHub</span>
74
+ </ButtonLink>
81
75
  </>
82
76
  )}
83
77
  {showToggleMode && (
@@ -0,0 +1,68 @@
1
+ 'use client'
2
+
3
+ // Adapted from shadcn/ui's Base Nova Popover.
4
+ import * as React from 'react'
5
+ import { Popover as PopoverPrimitive } from '@base-ui/react/popover'
6
+
7
+ import { cn, mergeStateClassName } from '@lexibase/rdx-ui/lib/utils'
8
+
9
+ export type PopoverActions = PopoverPrimitive.Root.Actions
10
+
11
+ function Popover(props: React.ComponentProps<typeof PopoverPrimitive.Root>) {
12
+ return <PopoverPrimitive.Root {...props} />
13
+ }
14
+
15
+ function PopoverTrigger(
16
+ props: React.ComponentProps<typeof PopoverPrimitive.Trigger>
17
+ ) {
18
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
19
+ }
20
+
21
+ function PopoverContent({
22
+ className,
23
+ align = 'center',
24
+ alignOffset = 0,
25
+ side = 'bottom',
26
+ sideOffset = 4,
27
+ ...props
28
+ }: React.ComponentProps<typeof PopoverPrimitive.Popup> &
29
+ Pick<
30
+ React.ComponentProps<typeof PopoverPrimitive.Positioner>,
31
+ 'align' | 'alignOffset' | 'side' | 'sideOffset'
32
+ >) {
33
+ return (
34
+ <PopoverPrimitive.Portal>
35
+ <PopoverPrimitive.Positioner
36
+ align={align}
37
+ alignOffset={alignOffset}
38
+ side={side}
39
+ sideOffset={sideOffset}
40
+ className="isolate z-50"
41
+ >
42
+ <PopoverPrimitive.Popup
43
+ data-slot="popover-content"
44
+ className={mergeStateClassName(
45
+ 'z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 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 motion-reduce:data-open:animate-none motion-reduce:data-closed:animate-none',
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ </PopoverPrimitive.Positioner>
51
+ </PopoverPrimitive.Portal>
52
+ )
53
+ }
54
+
55
+ function PopoverTitle({
56
+ className,
57
+ ...props
58
+ }: React.ComponentProps<typeof PopoverPrimitive.Title>) {
59
+ return (
60
+ <PopoverPrimitive.Title
61
+ data-slot="popover-title"
62
+ className={cn('font-medium', className)}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ export { Popover, PopoverTrigger, PopoverContent, PopoverTitle }
@@ -1,7 +1,6 @@
1
1
  'use client'
2
2
 
3
- import * as React from 'react'
4
- import * as SeparatorPrimitive from '@radix-ui/react-separator'
3
+ import { Separator as SeparatorPrimitive } from '@base-ui/react/separator'
5
4
 
6
5
  import { cn } from '@lexibase/rdx-ui/lib/utils'
7
6
 
@@ -10,16 +9,19 @@ function Separator({
10
9
  orientation = 'horizontal',
11
10
  decorative = true,
12
11
  ...props
13
- }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
12
+ }: SeparatorPrimitive.Props & { decorative?: boolean }) {
14
13
  return (
15
- <SeparatorPrimitive.Root
14
+ <SeparatorPrimitive
16
15
  data-slot="separator"
17
- decorative={decorative}
16
+ role={decorative ? 'none' : 'separator'}
17
+ aria-hidden={decorative || undefined}
18
18
  orientation={orientation}
19
- className={cn(
20
- 'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
21
- className
22
- )}
19
+ className={(state) =>
20
+ cn(
21
+ 'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
22
+ typeof className === 'function' ? className(state) : className
23
+ )
24
+ }
23
25
  {...props}
24
26
  />
25
27
  )
@@ -0,0 +1,183 @@
1
+ 'use client'
2
+
3
+ import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'
4
+ import { Check, Copy } from 'lucide-react'
5
+ import { useEffect, useRef, useState } from 'react'
6
+
7
+ import { mergeStateClassName } from '../lib/utils'
8
+ import { Button } from './button'
9
+
10
+ // Keep document structure and code whitespace, excluding interactive UI chrome.
11
+ function panelText(node: Node): string {
12
+ if (node.nodeType === 3) {
13
+ const text = node.textContent ?? ''
14
+ return text.includes('\n') && !text.trim() ? '' : text
15
+ }
16
+ if (!(node instanceof HTMLElement)) return ''
17
+ if (
18
+ node.hidden ||
19
+ node.inert ||
20
+ node.getAttribute('aria-hidden') === 'true' ||
21
+ node.matches(
22
+ 'button, [role="status"], [data-slot="tabs-header"], script, style'
23
+ )
24
+ )
25
+ return ''
26
+ if (node.matches('[data-rdx-code-block]')) {
27
+ return `${node.querySelector('pre')?.textContent ?? ''}\n`
28
+ }
29
+ if (node.tagName === 'PRE') return `${node.textContent ?? ''}\n`
30
+ if (node.tagName === 'BR') return '\n'
31
+ const text = Array.from(node.childNodes, panelText).join('')
32
+ if (!text) return ''
33
+ if (
34
+ node.matches('p, h1, h2, h3, h4, h5, h6, blockquote, ul, ol, div, aside')
35
+ ) {
36
+ return text.endsWith('\n\n')
37
+ ? text
38
+ : text + (text.endsWith('\n') ? '\n' : '\n\n')
39
+ }
40
+ if (node.matches('li, tr')) return `${text}\n`
41
+ if (node.matches('th, td')) return `${text}\t`
42
+ return text
43
+ }
44
+
45
+ function CopyTabButton() {
46
+ const [status, setStatus] = useState<'idle' | 'copied' | 'error'>('idle')
47
+ const pending = useRef(false)
48
+ const mounted = useRef(false)
49
+ const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
50
+ useEffect(() => {
51
+ mounted.current = true
52
+ return () => {
53
+ mounted.current = false
54
+ clearTimeout(timer.current)
55
+ }
56
+ }, [])
57
+
58
+ async function copy(button: HTMLButtonElement) {
59
+ if (pending.current) return
60
+ pending.current = true
61
+ clearTimeout(timer.current)
62
+ try {
63
+ const root = button.closest('[data-slot="tabs"]')
64
+ const panel = Array.from(
65
+ root?.querySelectorAll<HTMLElement>('[role="tabpanel"]') ?? []
66
+ ).find(
67
+ (element) =>
68
+ element.closest('[data-slot="tabs"]') === root &&
69
+ !element.hidden &&
70
+ !element.inert
71
+ )
72
+ if (!panel || !navigator.clipboard?.writeText)
73
+ throw new Error('Clipboard unavailable')
74
+ await navigator.clipboard.writeText(
75
+ panelText(panel).replace(/^\n+|\n+$/g, '')
76
+ )
77
+ if (mounted.current) {
78
+ setStatus('copied')
79
+ timer.current = setTimeout(() => setStatus('idle'), 2000)
80
+ }
81
+ } catch {
82
+ if (mounted.current) setStatus('error')
83
+ } finally {
84
+ pending.current = false
85
+ }
86
+ }
87
+
88
+ const Icon = status === 'copied' ? Check : Copy
89
+ return (
90
+ <>
91
+ <Button
92
+ type="button"
93
+ variant="ghost"
94
+ size="icon"
95
+ aria-label="Copy active tab"
96
+ title={
97
+ status === 'error'
98
+ ? 'Could not copy. Select the tab content and copy it manually.'
99
+ : status === 'copied'
100
+ ? 'Copied!'
101
+ : 'Copy active tab'
102
+ }
103
+ data-status={status}
104
+ className="mr-1.5 size-7 shrink-0 text-muted-foreground hover:text-foreground data-[status=copied]:text-emerald-700 dark:data-[status=copied]:text-emerald-400 data-[status=error]:text-amber-600 [&>svg]:size-3.5"
105
+ onClick={(event) => copy(event.currentTarget)}
106
+ >
107
+ <Icon aria-hidden="true" />
108
+ </Button>
109
+ <span
110
+ role="status"
111
+ aria-live="polite"
112
+ aria-atomic="true"
113
+ className="sr-only"
114
+ >
115
+ {status === 'copied'
116
+ ? 'Tab content copied to clipboard.'
117
+ : status === 'error'
118
+ ? 'Could not copy. Select the tab content and copy it manually.'
119
+ : ''}
120
+ </span>
121
+ </>
122
+ )
123
+ }
124
+
125
+ function Tabs({ className, ...props }: TabsPrimitive.Root.Props) {
126
+ return (
127
+ <TabsPrimitive.Root
128
+ data-slot="tabs"
129
+ className={mergeStateClassName(
130
+ 'my-6 min-w-0 max-w-full rounded-lg border border-border bg-muted/20 text-foreground',
131
+ className
132
+ )}
133
+ {...props}
134
+ />
135
+ )
136
+ }
137
+
138
+ function TabsList({ className, ...props }: TabsPrimitive.List.Props) {
139
+ return (
140
+ <div
141
+ data-slot="tabs-header"
142
+ className="flex min-w-0 items-center rounded-t-lg border-b border-border bg-muted/30"
143
+ >
144
+ <TabsPrimitive.List
145
+ data-slot="tabs-list"
146
+ className={mergeStateClassName(
147
+ 'flex min-w-0 flex-1 gap-0.5 overflow-x-auto p-1.5 data-[orientation=vertical]:flex-col',
148
+ className
149
+ )}
150
+ {...props}
151
+ />
152
+ <CopyTabButton />
153
+ </div>
154
+ )
155
+ }
156
+
157
+ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
158
+ return (
159
+ <TabsPrimitive.Tab
160
+ data-slot="tabs-trigger"
161
+ className={mergeStateClassName(
162
+ 'relative shrink-0 rounded-md border border-transparent px-2.5 py-1 text-sm font-medium whitespace-nowrap text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring data-active:border-border data-active:bg-background data-active:text-foreground data-active:shadow-xs data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:bg-transparent data-disabled:hover:text-muted-foreground motion-reduce:transition-none',
163
+ className
164
+ )}
165
+ {...props}
166
+ />
167
+ )
168
+ }
169
+
170
+ function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
171
+ return (
172
+ <TabsPrimitive.Panel
173
+ data-slot="tabs-content"
174
+ className={mergeStateClassName(
175
+ 'min-w-0 rounded-b-lg p-3 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring [&>:first-child]:mt-0 [&>:last-child]:mb-0 [&>[data-rdx-code-block]]:my-3 [&_[data-rdx-code-block]>pre]:p-3',
176
+ className
177
+ )}
178
+ {...props}
179
+ />
180
+ )
181
+ }
182
+
183
+ export { Tabs, TabsList, TabsTrigger, TabsContent }
@@ -10,6 +10,7 @@ import {
10
10
  DropdownMenuContent,
11
11
  DropdownMenuGroup,
12
12
  DropdownMenuItem,
13
+ DropdownMenuLinkItem,
13
14
  DropdownMenuLabel,
14
15
  DropdownMenuSeparator,
15
16
  DropdownMenuShortcut,
@@ -29,7 +30,7 @@ function VersionIcon({ kind }: { kind: VersionOption['kind'] }) {
29
30
  return <Icon className="size-4" />
30
31
  }
31
32
 
32
- /** Uses the same Radix primitives, sizes, icons and states as the showcase. */
33
+ /** Shared version menu for the runtime and development showcase. */
33
34
  export function VersionDropdown({
34
35
  options,
35
36
  currentId,
@@ -61,44 +62,47 @@ export function VersionDropdown({
61
62
  <DropdownMenuItem
62
63
  key={option.id}
63
64
  className={className}
64
- onSelect={() => onSelect(option.id)}
65
+ onClick={() => onSelect(option.id)}
65
66
  >
66
67
  {content}
67
68
  </DropdownMenuItem>
68
69
  ) : (
69
- <DropdownMenuItem key={option.id} className={className} asChild>
70
- <Link
71
- href={option.href}
72
- aria-current={option.id === current.id ? 'true' : undefined}
73
- >
74
- {content}
75
- </Link>
76
- </DropdownMenuItem>
70
+ <DropdownMenuLinkItem
71
+ key={option.id}
72
+ className={className}
73
+ closeOnClick
74
+ render={<Link href={option.href} />}
75
+ aria-current={option.id === current.id ? 'true' : undefined}
76
+ >
77
+ {content}
78
+ </DropdownMenuLinkItem>
77
79
  )
78
80
  }
79
81
  return (
80
82
  <DropdownMenu>
81
- <DropdownMenuTrigger asChild>
82
- <Button
83
- data-rdx-version-trigger
84
- variant="outline"
85
- size="sm"
86
- className="min-w-[134px] capitalize"
87
- aria-label={`Documentation version: ${current.label}`}
83
+ <DropdownMenuTrigger
84
+ render={
85
+ <Button
86
+ data-rdx-version-trigger
87
+ variant="outline"
88
+ size="sm"
89
+ className="min-w-[134px] capitalize"
90
+ aria-label={`Documentation version: ${current.label}`}
91
+ />
92
+ }
93
+ >
94
+ {current.label}
95
+ <DropdownMenuShortcut
96
+ className={
97
+ current.kind === 'live'
98
+ ? 'text-blue-500'
99
+ : current.kind === 'archived'
100
+ ? 'text-red-500'
101
+ : 'text-primary'
102
+ }
88
103
  >
89
- {current.label}
90
- <DropdownMenuShortcut
91
- className={
92
- current.kind === 'live'
93
- ? 'text-blue-500'
94
- : current.kind === 'archived'
95
- ? 'text-red-500'
96
- : 'text-primary'
97
- }
98
- >
99
- <VersionIcon kind={current.kind} />
100
- </DropdownMenuShortcut>
101
- </Button>
104
+ <VersionIcon kind={current.kind} />
105
+ </DropdownMenuShortcut>
102
106
  </DropdownMenuTrigger>
103
107
  <DropdownMenuContent className="w-56" align="start" collisionPadding={8}>
104
108
  {live.length > 0 && (
@@ -0,0 +1,39 @@
1
+ import Link from 'next/link'
2
+ import { ArrowRight } from 'lucide-react'
3
+
4
+ import { Callout } from './callout'
5
+
6
+ /** Presentation only: version selection remains the responsibility of the caller. */
7
+ export function VersionNotice({
8
+ kind,
9
+ currentHref,
10
+ }: {
11
+ kind: 'canary' | 'outdated'
12
+ currentHref: string
13
+ }) {
14
+ const isCanary = kind === 'canary'
15
+ return (
16
+ <Callout
17
+ data-rdx-version-notice={kind}
18
+ variant={isCanary ? 'tip' : 'warning'}
19
+ title={isCanary ? 'Pre-release documentation' : 'Outdated documentation'}
20
+ className="mt-0 mb-5"
21
+ >
22
+ <p>
23
+ {isCanary
24
+ ? 'This is a pre-release version of the documentation.'
25
+ : 'This is an outdated version of the documentation.'}{' '}
26
+ <Link
27
+ href={currentHref}
28
+ className="font-medium text-foreground underline decoration-current/40 underline-offset-4 transition-colors hover:decoration-current focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
29
+ >
30
+ Go back to the current version
31
+ <ArrowRight
32
+ aria-hidden="true"
33
+ className="ml-1 inline size-3.5 align-text-bottom"
34
+ />
35
+ </Link>
36
+ </p>
37
+ </Callout>
38
+ )
39
+ }
@@ -0,0 +1,68 @@
1
+ import type { RdxSearchEntry, RdxSearchIndex } from '@lexibase/rdx-types'
2
+
3
+ export function normalizeSearch(value: string) {
4
+ return value.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase()
5
+ }
6
+
7
+ export function prepareSearch(index: RdxSearchIndex) {
8
+ if (index.schemaVersion !== 1 || !Array.isArray(index.entries))
9
+ throw new Error('Invalid search index')
10
+ return index.entries.map((entry) => {
11
+ if (
12
+ !['href', 'title', 'category', 'section', 'text'].every(
13
+ (field) => typeof entry[field as keyof RdxSearchEntry] === 'string'
14
+ ) ||
15
+ !/^\/docs(?:\/|$)/.test(entry.href)
16
+ )
17
+ throw new Error('Invalid search entry')
18
+ return {
19
+ entry,
20
+ title: normalizeSearch(entry.title),
21
+ section: normalizeSearch(entry.section),
22
+ category: normalizeSearch(entry.category),
23
+ text: normalizeSearch(entry.text),
24
+ }
25
+ })
26
+ }
27
+
28
+ export type PreparedSearch = ReturnType<typeof prepareSearch>
29
+
30
+ /** Require every query term, favor page/section titles, and bound result count. */
31
+ export function searchDocumentation(index: PreparedSearch, query: string) {
32
+ const normalized = normalizeSearch(query.trim().slice(0, 120))
33
+ const terms = [...new Set(normalized.split(/\s+/).filter(Boolean))].slice(
34
+ 0,
35
+ 8
36
+ )
37
+ if (!terms.length) return []
38
+ return index
39
+ .map((item) => {
40
+ let score = 0
41
+ for (const term of terms) {
42
+ const weight =
43
+ (item.title.includes(term) ? 12 : 0) +
44
+ (item.section.includes(term) ? 10 : 0) +
45
+ (item.category.includes(term) ? 2 : 0) +
46
+ (item.text.includes(term) ? 1 : 0)
47
+ if (!weight) return { item, score: 0 }
48
+ score += weight
49
+ }
50
+ if (item.title === normalized) score += 30
51
+ if (item.section === normalized) score += 25
52
+ return { item, score }
53
+ })
54
+ .filter((item) => item.score > 0)
55
+ .sort(
56
+ (a, b) =>
57
+ b.score - a.score || a.item.entry.href.localeCompare(b.item.entry.href)
58
+ )
59
+ .slice(0, 12)
60
+ .map(({ item }) => {
61
+ const term = terms.find((term) => item.text.includes(term))
62
+ const offset = term ? Math.max(0, item.text.indexOf(term) - 50) : 0
63
+ return {
64
+ ...item.entry,
65
+ excerpt: `${offset ? '…' : ''}${item.entry.text.slice(offset, offset + 170)}${item.entry.text.length > offset + 170 ? '…' : ''}`,
66
+ }
67
+ })
68
+ }
package/src/lib/utils.ts CHANGED
@@ -4,3 +4,13 @@ import { twMerge } from 'tailwind-merge'
4
4
  export function cn(...inputs: ClassValue[]) {
5
5
  return twMerge(clsx(inputs))
6
6
  }
7
+
8
+ /** Preserve Base UI state callbacks while merging the shared theme classes. */
9
+ export function mergeStateClassName<State>(
10
+ base: string,
11
+ className?: string | ((state: State) => string | undefined)
12
+ ) {
13
+ return typeof className === 'function'
14
+ ? (state: State) => cn(base, className(state))
15
+ : cn(base, className)
16
+ }
@@ -77,6 +77,28 @@
77
77
  }
78
78
 
79
79
  @theme inline {
80
+ /* shadcn accordion animation, using the height measured by Base UI. */
81
+ --animate-accordion-down: accordion-down 0.2s ease-out;
82
+ --animate-accordion-up: accordion-up 0.2s ease-out;
83
+
84
+ @keyframes accordion-down {
85
+ from {
86
+ height: 0;
87
+ }
88
+ to {
89
+ height: var(--accordion-panel-height);
90
+ }
91
+ }
92
+
93
+ @keyframes accordion-up {
94
+ from {
95
+ height: var(--accordion-panel-height);
96
+ }
97
+ to {
98
+ height: 0;
99
+ }
100
+ }
101
+
80
102
  --color-background: var(--background);
81
103
  --color-foreground: var(--foreground);
82
104
  --color-card: var(--card);
@@ -123,3 +145,15 @@
123
145
  @apply bg-background text-foreground;
124
146
  }
125
147
  }
148
+
149
+ /* Shiki uses the same GitHub light/dark palette as shadcn's highlighter.
150
+ Keep the approved RDX surface and apply only token foreground colors. */
151
+ [data-rdx-code-block] pre.shiki,
152
+ [data-rdx-code-block] pre.shiki span {
153
+ color: var(--shiki-light, inherit);
154
+ }
155
+
156
+ .dark [data-rdx-code-block] pre.shiki,
157
+ .dark [data-rdx-code-block] pre.shiki span {
158
+ color: var(--shiki-dark, inherit);
159
+ }
@@ -0,0 +1,2 @@
1
+ // Global styles are loaded for their side effects; they export no JS values.
2
+ export {}