@lexibase/rdx-ui 1.1.0 → 2.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/README.md CHANGED
@@ -45,11 +45,10 @@ manual-copy guidance instead of a false success message.
45
45
 
46
46
  The shared components use `@base-ui/react` and the RDX theme.
47
47
  `components.json` selects shadcn/ui's `base-nova` style.
48
- Use `render` for interactive composition and `ButtonLink` for navigation:
48
+ Use `render` for interactive composition and `Button` with `href` for navigation:
49
49
 
50
50
  ```tsx
51
51
  import { Button } from '@lexibase/rdx-ui/components/button'
52
- import { ButtonLink } from '@lexibase/rdx-ui/components/button-link'
53
52
  import {
54
53
  DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
55
54
  DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem,
@@ -57,7 +56,7 @@ import {
57
56
  } from '@lexibase/rdx-ui/components/dropdown-menu'
58
57
  import Link from 'next/link'
59
58
 
60
- <ButtonLink href="/docs" variant="outline">Documentation</ButtonLink>
59
+ <Button href="/docs" variant="link">Documentation</Button>
61
60
 
62
61
  <DropdownMenu>
63
62
  <DropdownMenuTrigger render={<Button variant="outline" />}>
@@ -75,8 +74,8 @@ import Link from 'next/link'
75
74
  </DropdownMenu>
76
75
  ```
77
76
 
78
- `ButtonLink` renders a Next.js link with button styling, not `role="button"`.
79
- `Button` renders a native button; use `type="submit"` explicitly for form submission.
77
+ `Button` with `href` renders a native Next.js link; use `variant="link"` for text links.
78
+ Without `href`, `Button` renders a native button; use `type="submit"` explicitly for form submission.
80
79
  Menu actions use `onClick`. Labels must be inside `DropdownMenuGroup`.
81
80
  Checkbox values are boolean; checkbox/radio items remain open unless
82
81
  `closeOnClick` is enabled.
@@ -0,0 +1,29 @@
1
+ # Third-party notices
2
+
3
+ ## shadcn/ui
4
+
5
+ Table and Kbd are adapted from the official base-nova registry components:
6
+ https://ui.shadcn.com/docs/components/base/table and
7
+ https://ui.shadcn.com/docs/components/base/kbd.
8
+
9
+ MIT License
10
+
11
+ Copyright (c) 2023 shadcn
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexibase/rdx-ui",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": "^22.18.0 || ^24.11.0"
@@ -8,6 +8,7 @@
8
8
  "description": "The shared RDX documentation theme and UI components.",
9
9
  "license": "MIT",
10
10
  "files": [
11
+ "THIRD_PARTY_NOTICES.md",
11
12
  "src",
12
13
  "postcss.config.mjs",
13
14
  "postcss-plugin.cjs",
@@ -33,7 +34,7 @@
33
34
  "tailwind-merge": "^3.7.0",
34
35
  "tailwindcss": "^4.3.3",
35
36
  "tw-animate-css": "^1.4.0",
36
- "@lexibase/rdx-types": "1.1.0"
37
+ "@lexibase/rdx-types": "1.1.1"
37
38
  },
38
39
  "peerDependencies": {
39
40
  "next": "^16.4.2",
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { Button as ButtonPrimitive } from '@base-ui/react/button'
4
4
  import { cva, type VariantProps } from 'class-variance-authority'
5
+ import Link from 'next/link'
6
+ import type { ComponentProps } from 'react'
5
7
 
6
8
  import { cn } from '@lexibase/rdx-ui/lib/utils'
7
9
 
@@ -43,13 +45,23 @@ const buttonVariants = cva(
43
45
  }
44
46
  )
45
47
 
46
- function Button({
47
- className,
48
- variant,
49
- size,
50
- active,
51
- ...props
52
- }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
48
+ type ButtonProps =
49
+ | (ButtonPrimitive.Props &
50
+ VariantProps<typeof buttonVariants> & { href?: never })
51
+ | (ComponentProps<typeof Link> & VariantProps<typeof buttonVariants>)
52
+
53
+ function Button(props: ButtonProps) {
54
+ if (props.href !== undefined) {
55
+ const { className, variant = 'link', size, active, ...linkProps } = props
56
+ return (
57
+ <Link
58
+ data-slot="button"
59
+ className={cn(buttonVariants({ variant, size, active }), className)}
60
+ {...linkProps}
61
+ />
62
+ )
63
+ }
64
+ const { className, variant, size, active, ...buttonProps } = props
53
65
  return (
54
66
  <ButtonPrimitive
55
67
  data-slot="button"
@@ -59,7 +71,7 @@ function Button({
59
71
  typeof className === 'function' ? className(state) : className
60
72
  )
61
73
  }
62
- {...props}
74
+ {...buttonProps}
63
75
  />
64
76
  )
65
77
  }
@@ -1,3 +1,4 @@
1
+ import { getMessages } from '../lib/i18n'
1
2
  import { Children, isValidElement, type ComponentProps } from 'react'
2
3
  import { CodeXml } from 'lucide-react'
3
4
 
@@ -6,10 +7,12 @@ import { CopyCodeButton } from './copy-code-button'
6
7
 
7
8
  /** Keep the code server-rendered; only clipboard interaction needs JavaScript. */
8
9
  export function CodeBlock({
10
+ lang,
9
11
  children,
10
12
  className,
11
13
  ...props
12
14
  }: ComponentProps<'pre'>) {
15
+ const messages = getMessages(lang)
13
16
  const code = Children.toArray(children).find(
14
17
  (child) =>
15
18
  isValidElement<{ className?: string }>(child) &&
@@ -35,7 +38,7 @@ export function CodeBlock({
35
38
  className="min-w-0 truncate font-mono text-xs font-medium text-muted-foreground"
36
39
  title={language}
37
40
  >
38
- {language ?? 'Code'}
41
+ {language ?? messages.code}
39
42
  </span>
40
43
  </div>
41
44
  <CopyCodeButton />
@@ -1,11 +1,14 @@
1
1
  'use client'
2
2
 
3
+ import { useMessages } from '../providers/locale-provider'
4
+
3
5
  import { useEffect, useRef, useState } from 'react'
4
6
  import { Check, Copy } from 'lucide-react'
5
7
 
6
8
  import { Button } from './button'
7
9
 
8
10
  export function CopyCodeButton() {
11
+ const messages = useMessages()
9
12
  const [status, setStatus] = useState<'idle' | 'copied' | 'error'>('idle')
10
13
  const pending = useRef(false)
11
14
  const mounted = useRef(false)
@@ -50,9 +53,9 @@ export function CopyCodeButton() {
50
53
  }
51
54
  >
52
55
  {status === 'copied'
53
- ? 'Code copied to clipboard.'
56
+ ? messages.codeCopied
54
57
  : status === 'error'
55
- ? 'Could not copy. Select the code and copy it manually.'
58
+ ? messages.copyCodeError
56
59
  : ''}
57
60
  </span>
58
61
  <Button
@@ -61,13 +64,13 @@ export function CopyCodeButton() {
61
64
  size="icon"
62
65
  data-status={status}
63
66
  className="size-7 shrink-0 border-border/70 bg-background/60 p-0 text-muted-foreground shadow-none hover:text-foreground data-[status=copied]:border-emerald-500/30 data-[status=copied]:bg-emerald-500/10 data-[status=copied]:text-emerald-700 dark:data-[status=copied]:text-emerald-400 data-[status=error]:border-amber-500/30 [&>svg]:size-3.5"
64
- aria-label="Copy code"
67
+ aria-label={messages.copyCode}
65
68
  title={
66
69
  status === 'copied'
67
- ? 'Copied!'
70
+ ? messages.copied
68
71
  : status === 'error'
69
- ? 'Could not copy. Try again or copy manually.'
70
- : 'Copy code'
72
+ ? messages.copyRetry
73
+ : messages.copyCode
71
74
  }
72
75
  onClick={(event) => copy(event.currentTarget)}
73
76
  >
@@ -1,5 +1,7 @@
1
1
  'use client'
2
2
 
3
+ import { useMessages } from '../providers/locale-provider'
4
+
3
5
  // shadcn/ui Base Nova Dialog, using the shared RDX theme and buttons.
4
6
  import * as React from 'react'
5
7
  import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
@@ -20,6 +22,7 @@ function DialogContent({
20
22
  children,
21
23
  ...props
22
24
  }: React.ComponentProps<typeof DialogPrimitive.Popup>) {
25
+ const messages = useMessages()
23
26
  return (
24
27
  <DialogPrimitive.Portal>
25
28
  <DialogPrimitive.Backdrop
@@ -43,7 +46,7 @@ function DialogContent({
43
46
  className="absolute top-2 right-2 size-7"
44
47
  />
45
48
  }
46
- aria-label="Close search"
49
+ aria-label={messages.closeSearch}
47
50
  >
48
51
  <X className="size-3.5" />
49
52
  </DialogPrimitive.Close>
@@ -1,5 +1,7 @@
1
1
  'use client'
2
2
 
3
+ import { useMessages } from '../providers/locale-provider'
4
+
3
5
  import {
4
6
  useDeferredValue,
5
7
  useEffect,
@@ -23,6 +25,8 @@ import {
23
25
  CommandItem,
24
26
  CommandList,
25
27
  } from './command'
28
+ import { formatMessage } from '../lib/i18n'
29
+
26
30
  import { Button } from './button'
27
31
 
28
32
  const cache = new Map<string, PreparedSearch>()
@@ -40,6 +44,7 @@ export function DocsSearchDialog({
40
44
  onOpenChange: (value: boolean) => void
41
45
  triggerRef: RefObject<HTMLButtonElement | null>
42
46
  }) {
47
+ const messages = useMessages()
43
48
  const router = useRouter()
44
49
  const pathname = usePathname()
45
50
  const [query, setQuery] = useState('')
@@ -95,15 +100,16 @@ export function DocsSearchDialog({
95
100
  return (
96
101
  <Dialog open={open} onOpenChange={onOpenChange}>
97
102
  <DialogContent finalFocus={triggerRef}>
98
- <DialogTitle className="sr-only">Search documentation</DialogTitle>
103
+ <DialogTitle className="sr-only">
104
+ {messages.searchDocumentation}
105
+ </DialogTitle>
99
106
  <DialogDescription className="sr-only">
100
- Search pages and sections in {version}. Use arrow keys to choose a
101
- result and Enter to open it.
107
+ {formatMessage(messages.searchDescription, { version })}
102
108
  </DialogDescription>
103
- <Command shouldFilter={false} label="Search documentation">
109
+ <Command shouldFilter={false} label={messages.searchDocumentation}>
104
110
  <CommandInput
105
- aria-label="Search documentation"
106
- placeholder="Search documentation…"
111
+ aria-label={messages.searchDocumentation}
112
+ placeholder={messages.searchPlaceholder}
107
113
  value={query}
108
114
  onValueChange={setQuery}
109
115
  maxLength={120}
@@ -111,21 +117,24 @@ export function DocsSearchDialog({
111
117
  <div className="px-2 pt-2 pb-1 text-[11px] text-muted-foreground">
112
118
  {version}
113
119
  </div>
114
- <CommandList aria-busy={loading}>
120
+ <CommandList
121
+ aria-busy={loading}
122
+ aria-label={messages.searchDocumentation}
123
+ >
115
124
  {loading ? (
116
125
  <div
117
126
  role="status"
118
127
  className="flex justify-center gap-2 p-6 text-sm text-muted-foreground"
119
128
  >
120
129
  <LoaderCircle className="size-4 animate-spin motion-reduce:animate-none" />
121
- Loading search…
130
+ {messages.loadingSearch}
122
131
  </div>
123
132
  ) : state.error ? (
124
133
  <div
125
134
  role="status"
126
135
  className="p-4 text-center text-sm text-muted-foreground"
127
136
  >
128
- Search is unavailable.{' '}
137
+ {messages.searchUnavailable}{' '}
129
138
  <Button
130
139
  variant="link"
131
140
  size="sm"
@@ -134,16 +143,16 @@ export function DocsSearchDialog({
134
143
  setRetry((value) => value + 1)
135
144
  }}
136
145
  >
137
- Try again
146
+ {messages.tryAgain}
138
147
  </Button>
139
148
  </div>
140
149
  ) : !query.trim() ? (
141
150
  <div className="px-4 py-6 text-center text-sm text-muted-foreground">
142
- Type to search this documentation.
151
+ {messages.typeToSearch}
143
152
  </div>
144
153
  ) : (
145
154
  <>
146
- <CommandEmpty>No results found.</CommandEmpty>
155
+ <CommandEmpty>{messages.noResults}</CommandEmpty>
147
156
  {results.map((entry) => (
148
157
  <CommandItem
149
158
  key={entry.href}
@@ -175,7 +184,8 @@ export function DocsSearchDialog({
175
184
  )}
176
185
  </CommandList>
177
186
  <div className="border-t px-2 py-2 text-[10px] text-muted-foreground">
178
- ↑ ↓ Navigate <span className="mx-2">↵ Open</span> Esc Close
187
+ ↑ ↓ {messages.navigate}{' '}
188
+ <span className="mx-2">↵ {messages.open}</span> Esc {messages.close}
179
189
  </div>
180
190
  </Command>
181
191
  </DialogContent>
@@ -1,9 +1,12 @@
1
1
  'use client'
2
2
 
3
+ import { useMessages } from '../providers/locale-provider'
4
+
3
5
  import { useEffect, useRef, useState } from 'react'
4
6
  import dynamic from 'next/dynamic'
5
7
  import { Search } from 'lucide-react'
6
8
  import { Button } from './button'
9
+ import { Kbd } from './kbd'
7
10
 
8
11
  const SearchDialog = dynamic(
9
12
  () =>
@@ -19,6 +22,7 @@ export function DocsSearch({
19
22
  indexUrl: string
20
23
  version: string
21
24
  }) {
25
+ const messages = useMessages()
22
26
  const [open, setOpen] = useState(false)
23
27
  const [activated, setActivated] = useState(false)
24
28
  const trigger = useRef<HTMLButtonElement | null>(null)
@@ -50,20 +54,20 @@ export function DocsSearch({
50
54
  data-rdx-search-trigger
51
55
  variant="outline"
52
56
  size="sm"
53
- aria-label="Search documentation"
57
+ aria-label={messages.searchDocumentation}
54
58
  aria-haspopup="dialog"
55
59
  aria-expanded={open}
56
60
  onClick={show}
57
61
  className="h-8 gap-2 px-2 text-muted-foreground sm:min-w-40 sm:justify-start"
58
62
  >
59
63
  <Search className="size-3.5" />
60
- <span className="hidden sm:inline">Search…</span>
61
- <kbd
62
- className="ml-auto hidden rounded border px-1 text-[10px] text-muted-foreground sm:inline"
64
+ <span className="hidden sm:inline">{messages.search}</span>
65
+ <Kbd
66
+ className="ml-auto hidden h-auto rounded border bg-transparent px-1 text-[10px] text-muted-foreground sm:inline"
63
67
  aria-hidden="true"
64
68
  >
65
69
  ⌘ / Ctrl K
66
- </kbd>
70
+ </Kbd>
67
71
  </Button>
68
72
  {activated && (
69
73
  <SearchDialog
@@ -1,10 +1,11 @@
1
1
  'use client'
2
2
 
3
+ import { useMessages } from '../providers/locale-provider'
4
+
3
5
  import { useEffect, useRef, useState } from 'react'
4
6
  import { SidebarClose, SidebarOpen } from 'lucide-react'
5
7
 
6
8
  import { Button } from './button'
7
- import { ButtonLink } from './button-link'
8
9
  import { Separator } from './separator'
9
10
 
10
11
  export type SidebarCategory = {
@@ -19,6 +20,7 @@ export function DocsSidebar({
19
20
  categories: SidebarCategory[]
20
21
  pathname: string
21
22
  }) {
23
+ const messages = useMessages()
22
24
  const [openPath, setOpenPath] = useState<string | null>(null)
23
25
  const isOpen = openPath === pathname
24
26
  const dialog = useRef<HTMLDialogElement>(null)
@@ -27,7 +29,7 @@ export function DocsSidebar({
27
29
  if (!isOpen && dialog.current?.open) dialog.current?.close()
28
30
  }, [isOpen])
29
31
  const content = (
30
- <nav aria-label="Documentation pages">
32
+ <nav aria-label={messages.documentationPages}>
31
33
  {categories.map((category, index) => (
32
34
  <div
33
35
  key={`${index}:${category.title}`}
@@ -37,7 +39,7 @@ export function DocsSidebar({
37
39
  {category.title}
38
40
  </p>
39
41
  {category.links.map((link) => (
40
- <ButtonLink
42
+ <Button
41
43
  key={link.href}
42
44
  variant="ghost"
43
45
  size="sm"
@@ -49,7 +51,7 @@ export function DocsSidebar({
49
51
  className="justify-start text-sm text-primary"
50
52
  >
51
53
  {link.label}
52
- </ButtonLink>
54
+ </Button>
53
55
  ))}
54
56
  {index < categories.length - 1 && <Separator className="my-4" />}
55
57
  </div>
@@ -62,7 +64,7 @@ export function DocsSidebar({
62
64
  <Button
63
65
  variant="ghost"
64
66
  onClick={() => setOpenPath(pathname)}
65
- aria-label="Open sidebar"
67
+ aria-label={messages.openSidebar}
66
68
  >
67
69
  <SidebarOpen className="size-4" />
68
70
  </Button>
@@ -75,7 +77,7 @@ export function DocsSidebar({
75
77
  </aside>
76
78
  <dialog
77
79
  ref={dialog}
78
- aria-label="Documentation navigation"
80
+ aria-label={messages.documentationNavigation}
79
81
  onClose={() => setOpenPath(null)}
80
82
  className="fixed inset-0 m-0 h-dvh max-h-none w-full max-w-none bg-background text-foreground border-0 p-4 backdrop:bg-black/40"
81
83
  >
@@ -83,7 +85,7 @@ export function DocsSidebar({
83
85
  <Button
84
86
  variant="ghost"
85
87
  onClick={() => setOpenPath(null)}
86
- aria-label="Close sidebar"
88
+ aria-label={messages.closeSidebar}
87
89
  >
88
90
  <SidebarClose className="size-4" />
89
91
  </Button>
@@ -1,5 +1,7 @@
1
1
  'use client'
2
2
 
3
+ import { useMessages } from '../providers/locale-provider'
4
+
3
5
  import { useEffect, useRef, useState } from 'react'
4
6
  import { usePathname } from 'next/navigation'
5
7
 
@@ -77,6 +79,7 @@ export function DocsToc({ contentId = 'rdx-main' }: { contentId?: string }) {
77
79
  }
78
80
 
79
81
  function PageToc({ contentId }: { contentId: string }) {
82
+ const messages = useMessages()
80
83
  const [headings, setHeadings] = useState<Heading[]>([])
81
84
  const [activeId, setActiveId] = useState('')
82
85
  const actions = useRef<PopoverActions | null>(null)
@@ -184,7 +187,7 @@ function PageToc({ contentId }: { contentId: string }) {
184
187
  <PopoverTrigger
185
188
  openOnHover
186
189
  delay={120}
187
- aria-label="On this page"
190
+ aria-label={messages.onThisPage}
188
191
  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
192
  >
190
193
  {headings.map((heading) => (
@@ -215,9 +218,9 @@ function PageToc({ contentId }: { contentId: string }) {
215
218
  className="hidden max-h-[min(60vh,var(--available-height))] w-64 overflow-y-auto xl:flex"
216
219
  >
217
220
  <PopoverTitle className="px-2 pt-1 text-xs text-muted-foreground">
218
- On this page
221
+ {messages.onThisPage}
219
222
  </PopoverTitle>
220
- <nav aria-label="On this page">
223
+ <nav aria-label={messages.onThisPage}>
221
224
  <HeadingLinks
222
225
  nodes={headingTree(headings)}
223
226
  baseDepth={baseDepth}
@@ -1,17 +1,21 @@
1
+ import { getMessages } from '../lib/i18n'
1
2
  import type { ReactNode } from 'react'
2
3
 
3
- import { ButtonLink } from './button-link'
4
+ import { Button } from './button'
4
5
 
5
6
  /** Shared site identity and framework credit for documentation footers. */
6
7
  export function FooterCredit({
8
+ lang,
7
9
  name,
8
10
  since,
9
11
  children,
10
12
  }: {
13
+ lang?: string
11
14
  name: string
12
15
  since?: number
13
16
  children?: ReactNode
14
17
  }) {
18
+ const messages = getMessages(lang)
15
19
  const currentYear = new Date().getFullYear()
16
20
  const years =
17
21
  since !== undefined && since !== currentYear
@@ -24,8 +28,8 @@ export function FooterCredit({
24
28
  &copy; {years} <strong>{name}</strong>
25
29
  {children}
26
30
  <span className="inline-block">
27
- {' · Powered by '}
28
- <ButtonLink
31
+ {` · ${messages.poweredBy} `}
32
+ <Button
29
33
  variant="link"
30
34
  size="link"
31
35
  href="https://github.com/lexibase/rdx"
@@ -33,7 +37,7 @@ export function FooterCredit({
33
37
  rel="noopener noreferrer"
34
38
  >
35
39
  RDX
36
- </ButtonLink>
40
+ </Button>
37
41
  </span>
38
42
  </p>
39
43
  </section>
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react'
2
2
 
3
- import { ButtonLink } from './button-link'
3
+ import { Button } from './button'
4
4
  import { Logo } from './logo'
5
5
 
6
6
  /** The default landing page from apps/web, not a separate marketing theme. */
@@ -32,13 +32,13 @@ export function Home({
32
32
  {buttons.length > 0 && (
33
33
  <div className="flex flex-wrap justify-center gap-3 mt-6 px-6">
34
34
  {buttons.map((button, index) => (
35
- <ButtonLink
35
+ <Button
36
36
  key={`${index}:${button.href}`}
37
37
  variant={index === 0 ? 'default' : 'outline'}
38
38
  href={button.href}
39
39
  >
40
40
  {button.label}
41
- </ButtonLink>
41
+ </Button>
42
42
  ))}
43
43
  </div>
44
44
  )}
@@ -0,0 +1,29 @@
1
+ import type { ComponentProps } from 'react'
2
+
3
+ import { cn } from '../lib/utils'
4
+
5
+ // shadcn/ui base-nova Kbd; see THIRD_PARTY_NOTICES.md for the upstream MIT license.
6
+ function Kbd({ className, ...props }: ComponentProps<'kbd'>) {
7
+ return (
8
+ <kbd
9
+ data-slot="kbd"
10
+ className={cn(
11
+ "pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm bg-muted px-1 font-sans text-xs font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 [&_svg:not([class*='size-'])]:size-3",
12
+ className
13
+ )}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ function KbdGroup({ className, ...props }: ComponentProps<'div'>) {
20
+ return (
21
+ <kbd
22
+ data-slot="kbd-group"
23
+ className={cn('inline-flex items-center gap-1', className)}
24
+ {...props}
25
+ />
26
+ )
27
+ }
28
+
29
+ export { Kbd, KbdGroup }
@@ -1,7 +1,11 @@
1
+ 'use client'
2
+
3
+ import { useMessages } from '../providers/locale-provider'
1
4
  import { Spline } from 'lucide-react'
2
5
  import { Button } from '@lexibase/rdx-ui/components/button'
3
6
 
4
7
  export function LoadingButton() {
8
+ const messages = useMessages()
5
9
  return (
6
10
  <Button
7
11
  variant="outline"
@@ -9,7 +13,7 @@ export function LoadingButton() {
9
13
  className="w-[134px] px-9 overflow-hidden"
10
14
  disabled
11
15
  aria-busy="true"
12
- aria-label="Loading versions"
16
+ aria-label={messages.loadingVersions}
13
17
  >
14
18
  <div className="animate-spin flex items-center justify-center">
15
19
  <Spline className="size-4" />