@arbol-ai/ui 0.0.0-stage → 0.1.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +98 -2
- package/dist/index.d.ts +217 -0
- package/dist/index.js +239 -0
- package/dist/index.js.map +1 -0
- package/dist/theme.css +533 -0
- package/package.json +118 -4
- package/skills/arbol-ui/references/i18n.md +41 -0
- package/skills/arbol-ui/references/setup.md +79 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Every version published to npm has a section here, which its GitHub release repeats. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com) and versions follow [Semantic Versioning](https://semver.org).
|
|
5
|
+
|
|
6
|
+
## 0.1.0 (2026-11-06)
|
|
7
|
+
|
|
8
|
+
- First release: the components Raíz, Agenda and Account share, their theme, Spanish and English strings,
|
|
9
|
+
the `arbol-ui` agent skill and the documentation site.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arbol
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,99 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @arbol-ai/ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Accessible React components for building dashboards fast: [React Aria Components](https://react-aria.adobe.com)
|
|
4
|
+
styled with [Tailwind CSS v4](https://tailwindcss.com), with a token-based theme (light, dark, brand and
|
|
5
|
+
mono), every built-in string in Spanish and English, and an agent skill that teaches coding agents how to
|
|
6
|
+
assemble pages with it. It is the shared UI of [Arbol](https://www.getarbol.com)'s Raíz, Agenda and Account
|
|
7
|
+
dashboards, and it is open source under the MIT license.
|
|
8
|
+
|
|
9
|
+
- **Accessible by construction**: keyboard, screen readers, focus management and touch come from React Aria.
|
|
10
|
+
- **Localized**: the library's own strings ship in `es` and `en`, follow your app's locale and can be
|
|
11
|
+
replaced with your own translation functions (Paraglide, i18next).
|
|
12
|
+
- **Router-agnostic**: every `href` routes through your router via one provider.
|
|
13
|
+
- **Dashboard-ready**: data tables on TanStack Table, filters, pagination, stats, charts on ECharts, forms on
|
|
14
|
+
TanStack Form, overlays with an unsaved-changes guard, an app shell and settings pages.
|
|
15
|
+
- **Documented**: JSDoc on every export, a live documentation site, and a skill for agents in `skills/`.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun add @arbol-ai/ui react-aria-components react-aria
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Peers: React 19, `react-aria-components` 1.21+, `react-aria` 3.52+ and Tailwind CSS 4.1+. Optional:
|
|
24
|
+
`echarts` 6 for `@arbol-ai/ui/charts`, `@tanstack/react-form` 1 for `@arbol-ai/ui/form`.
|
|
25
|
+
|
|
26
|
+
## Set up
|
|
27
|
+
|
|
28
|
+
1. Import the theme after Tailwind in your entry stylesheet:
|
|
29
|
+
|
|
30
|
+
```css
|
|
31
|
+
@import 'tailwindcss';
|
|
32
|
+
@import '@arbol-ai/ui/theme.css';
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
2. Render `UIProvider` once at the root, with your locale and router:
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { UIProvider } from '@arbol-ai/ui';
|
|
39
|
+
|
|
40
|
+
<UIProvider locale="es-CO" navigate={(to, options) => router.navigate({ ...options, to })}>
|
|
41
|
+
<App />
|
|
42
|
+
</UIProvider>;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
3. Use the components:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { Button, PageContainer, PageHeader, PageTitle } from '@arbol-ai/ui';
|
|
49
|
+
|
|
50
|
+
<PageContainer>
|
|
51
|
+
<PageHeader>
|
|
52
|
+
<PageTitle>Contacts</PageTitle>
|
|
53
|
+
<Button onPress={openNewContact}>New contact</Button>
|
|
54
|
+
</PageHeader>
|
|
55
|
+
</PageContainer>;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Dark mode is the `dark` class on `<html>`; the grayscale appearance is `data-theme="mono"`. Fonts are not
|
|
59
|
+
bundled: the theme names Arbol's faces, then the open Schibsted Grotesk and Martian Mono, then system fonts;
|
|
60
|
+
override `--ui-font-sans`, `--ui-font-heading` and `--ui-font-mono` to use your own.
|
|
61
|
+
|
|
62
|
+
## Text and languages
|
|
63
|
+
|
|
64
|
+
App copy (titles, labels, descriptions) is always passed by your app through props and children, already
|
|
65
|
+
translated. What a component says by itself ("Close", "Previous page", "1–15 of 120", "No results") ships in
|
|
66
|
+
English and Latin American Spanish and follows `UIProvider locale`. Replace any of those strings with your own
|
|
67
|
+
translation functions:
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
<UIProvider locale={locale} messages={{ 'overlay.close': m.ui_close, 'pagination.range': m.ui_pagination_range }}>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Documentation
|
|
74
|
+
|
|
75
|
+
- The documentation site (`bun run dev`) shows every component live, in both languages and themes.
|
|
76
|
+
- Every export has JSDoc with an example, so your editor documents it too.
|
|
77
|
+
- Coding agents: link the published skill into your project (`node_modules/@arbol-ai/ui/skills/arbol-ui`)
|
|
78
|
+
under `.agents/skills/` or `.claude/skills/`.
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
The project uses the [Vite+](https://viteplus.dev) toolchain (`vp`): one `vite.config.ts` drives tests, lint,
|
|
83
|
+
format, packaging and the commit hook.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
bun install
|
|
87
|
+
bunx vp dev site # documentation site on http://localhost:5173
|
|
88
|
+
bunx vp check # format, type-aware lint and type check
|
|
89
|
+
bunx vp test run # browser tests in Chromium (add --coverage for the 80% floor)
|
|
90
|
+
bunx vp pack # build the package into dist/
|
|
91
|
+
bun run verify # everything above plus the site build and package checks
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md). Releases: bump the version and add its CHANGELOG section in a pull
|
|
95
|
+
request; after the merge, CI publishes the package to npm with provenance and deploys the site.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
[MIT](./LICENSE) © Arbol
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
|
|
2
|
+
import { ButtonProps as ButtonProps$1, LinkProps, RouterConfig } from "react-aria-components";
|
|
3
|
+
import { ReactNode, Ref } from "react";
|
|
4
|
+
import { ClassValue } from "clsx";
|
|
5
|
+
import { VariantProps } from "class-variance-authority";
|
|
6
|
+
/**
|
|
7
|
+
* The library's own strings: the words a component renders by itself (a close button's label, a page
|
|
8
|
+
* range, a "no results" line). Everything else a component shows comes from the application, already
|
|
9
|
+
* translated, through props and children, so the library never needs to know the app's message system.
|
|
10
|
+
*
|
|
11
|
+
* Each component keeps its strings beside it (`pagination.messages.ts`, declared with
|
|
12
|
+
* {@link defineMessages}) and reads them with {@link useMessages}, in the locale React Aria's
|
|
13
|
+
* `I18nProvider` (or {@link UIProvider}) sets.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The locales the library ships, as dictionary keys. A locale matches exactly, then by language, then
|
|
17
|
+
* falls back to English: `es-CO`, `es-MX` and `es` all read `es-419`; `fr-FR` reads `en-US`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SHIPPED_LOCALES: readonly ['en-US', 'es-419'];
|
|
20
|
+
/** A locale the library ships translations for. */
|
|
21
|
+
type ShippedLocale = (typeof SHIPPED_LOCALES)[number];
|
|
22
|
+
/** The values a component passes to a message: counts as numbers, names as strings. */
|
|
23
|
+
type MessageVariables = Record<string, string | number>;
|
|
24
|
+
/** Locale-aware helpers every message function receives with its variables. */
|
|
25
|
+
interface MessageHelpers {
|
|
26
|
+
/** The active locale (`es-CO`, `en-US`). */
|
|
27
|
+
locale: string;
|
|
28
|
+
/** Formats a number for the locale: `1234` is `1.234` in `es-CO`, `1,234` in `en-US`. */
|
|
29
|
+
number: (value: number) => string;
|
|
30
|
+
/**
|
|
31
|
+
* Picks the plural form for a count with the locale's rules, falling back to `other`.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* plural(count, { one: '1 contact', other: `${number(count)} contacts` })
|
|
35
|
+
*/
|
|
36
|
+
plural: (count: number, forms: Partial<Record<Intl.LDMLPluralRule, string>> & {
|
|
37
|
+
other: string;
|
|
38
|
+
}) => string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One message: a string, or a function of the variables its component passes that returns one.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* 'pagination.range': ({ first, last, total }, { number }) =>
|
|
45
|
+
* `${number(Number(first))}–${number(Number(last))} of ${number(Number(total))}`
|
|
46
|
+
*/
|
|
47
|
+
type Message = string | ((variables: MessageVariables, helpers: MessageHelpers) => string);
|
|
48
|
+
/** A component's dictionary: every shipped locale, each with the same keys. */
|
|
49
|
+
type Messages<K extends string> = Record<ShippedLocale, Record<K, Message>>;
|
|
50
|
+
/**
|
|
51
|
+
* Replacements for the library's strings, by message key, applied in whichever locale is active. A
|
|
52
|
+
* value is a string, or a function of the message's variables, so an app's own translation functions
|
|
53
|
+
* plug in directly: Paraglide's `m.ui_close`, or `(variables) => t('ui.close', variables)` with
|
|
54
|
+
* i18next. The variables keep the names Arbol's catalogs already use (`first`, `last`, `total`,
|
|
55
|
+
* `count`, `page`, `label`).
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* { 'overlay.close': m.ui_close, 'pagination.range': m.ui_pagination_range }
|
|
59
|
+
*/
|
|
60
|
+
type MessageOverrides = Record<string, string | ((variables: MessageVariables) => string)>;
|
|
61
|
+
/**
|
|
62
|
+
* Declares a component's dictionary. TypeScript checks that every shipped locale translates every key
|
|
63
|
+
* the English one has. Keys are namespaced by component (`pagination.next`) so that an override
|
|
64
|
+
* addresses one message unambiguously.
|
|
65
|
+
*/
|
|
66
|
+
export declare function defineMessages<K extends string>(messages: Messages<K>): Messages<K>;
|
|
67
|
+
/** What {@link useMessages} returns. */
|
|
68
|
+
interface MessageFormatter<K extends string> {
|
|
69
|
+
/** The message for a key in the active locale, with its variables filled in. */
|
|
70
|
+
format: (key: K, variables?: MessageVariables) => string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Formats a component's strings in the current locale, with the application's overrides applied.
|
|
74
|
+
*
|
|
75
|
+
* @param messages - The component's dictionary, from {@link defineMessages}.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* const t = useMessages(paginationMessages);
|
|
79
|
+
* t.format('pagination.next'); // 'Next' or 'Siguiente'
|
|
80
|
+
*/
|
|
81
|
+
export declare function useMessages<K extends string>(messages: Messages<K>): MessageFormatter<K>;
|
|
82
|
+
/** The path type React Aria's links take; an app narrows it by augmenting `RouterConfig`. */
|
|
83
|
+
type Href = RouterConfig extends {
|
|
84
|
+
href: infer H;
|
|
85
|
+
} ? H : string;
|
|
86
|
+
/** The navigation options React Aria passes through; an app narrows them by augmenting `RouterConfig`. */
|
|
87
|
+
type RouterOptions = RouterConfig extends {
|
|
88
|
+
routerOptions: infer O;
|
|
89
|
+
} ? O : undefined;
|
|
90
|
+
/** Props of {@link UIProvider}. */
|
|
91
|
+
interface UIProviderProps {
|
|
92
|
+
/**
|
|
93
|
+
* The BCP 47 locale every component formats and speaks in (`es-CO`, `en-US`): dates and numbers,
|
|
94
|
+
* React Aria's built-in announcements, the library's own strings and the reading direction. Pass the
|
|
95
|
+
* locale of the app's message system (Paraglide's `getLocale()`, i18next's `i18n.language`).
|
|
96
|
+
* Defaults to the browser's language.
|
|
97
|
+
*/
|
|
98
|
+
locale?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Client-side navigation for every `href` the components render (links, menu items, tabs, rows).
|
|
101
|
+
* Pass the router's navigate; without it, an `href` is a full-page navigation.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* // TanStack Router
|
|
105
|
+
* navigate={(to, options) => router.navigate({ ...options, to })}
|
|
106
|
+
*/
|
|
107
|
+
navigate?: (path: Href, routerOptions?: RouterOptions) => void;
|
|
108
|
+
/**
|
|
109
|
+
* Turns a router path into the URL the browser shows (needed when the router adds a base path).
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* // TanStack Router
|
|
113
|
+
* useHref={(to) => router.buildLocation({ to }).href}
|
|
114
|
+
*/
|
|
115
|
+
useHref?: (href: Href) => string;
|
|
116
|
+
/**
|
|
117
|
+
* Replacements for the library's own strings, by message key, in whichever locale is active; see
|
|
118
|
+
* {@link MessageOverrides}. Paraglide's `m.*` functions plug in directly.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* messages={{ 'overlay.close': m.ui_close, 'pagination.range': m.ui_pagination_range }}
|
|
122
|
+
*/
|
|
123
|
+
messages?: MessageOverrides;
|
|
124
|
+
/** The application. */
|
|
125
|
+
children: ReactNode;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The root every Arbol UI application renders once, above everything else: it tells the components the
|
|
129
|
+
* locale (and with it, the language of their own strings), and how to navigate.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* import { getLocale } from './paraglide/runtime';
|
|
133
|
+
*
|
|
134
|
+
* <UIProvider locale={getLocale()} navigate={(to, options) => router.navigate({ ...options, to })}>
|
|
135
|
+
* <App />
|
|
136
|
+
* </UIProvider>
|
|
137
|
+
*/
|
|
138
|
+
export declare const UIProvider: ({ locale, navigate, useHref, messages, children }: UIProviderProps) => import("react").JSX.Element;
|
|
139
|
+
/**
|
|
140
|
+
* Joins class names and resolves Tailwind conflicts, the last class winning: `cn('px-2', 'px-4')` is
|
|
141
|
+
* `'px-4'`. Every component merges its own classes with the caller's `className` through it, so a
|
|
142
|
+
* caller can always override a component's styles.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* cn('rounded-field px-3', isCompact && 'px-2', className)
|
|
146
|
+
*/
|
|
147
|
+
export declare function cn(...inputs: ClassValue[]): string;
|
|
148
|
+
/**
|
|
149
|
+
* Merges a component's classes with a React Aria `className`, which is a string or a function of the
|
|
150
|
+
* component's render props (`isPressed`, `isSelected`…). The caller's classes come last, so they win.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* <AriaButton className={composeTailwindRenderProps(props.className, 'rounded-field px-3')} />
|
|
154
|
+
*/
|
|
155
|
+
export declare function composeTailwindRenderProps<T>(className: string | ((values: T) => string) | undefined, classes: ClassValue): string | ((values: T) => string);
|
|
156
|
+
/**
|
|
157
|
+
* The keyboard focus ring every focusable part shares: invisible for pointer users, a 2px ring in the
|
|
158
|
+
* `ring` token, offset from the background, for keyboard users (React Aria's `focus-visible`).
|
|
159
|
+
*/
|
|
160
|
+
export declare const focusRing = "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
|
|
161
|
+
/**
|
|
162
|
+
* The button styles, as a `cva` recipe, for anything that must look like a button: `Button`,
|
|
163
|
+
* `ButtonLink`, or a third-party trigger.
|
|
164
|
+
*
|
|
165
|
+
* - `variant`: `default` (the primary action, filled), `destructive` (filled red, for the irreversible
|
|
166
|
+
* action itself), `outline` and `secondary` (the second action), `ghost` (toolbar and icon actions),
|
|
167
|
+
* `link` (an action that reads as text).
|
|
168
|
+
* - `size`: `sm` (28px, toolbars and tables), `default` (32px), `lg` (40px), and the square `icon-sm`,
|
|
169
|
+
* `icon` and `icon-lg`. On touch screens every size takes taps across at least 44px.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* <a className={buttonVariants({ variant: 'outline', size: 'sm' })} href="/export.csv">Export</a>
|
|
173
|
+
*/
|
|
174
|
+
export declare const buttonVariants: (props?: ({
|
|
175
|
+
variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | undefined;
|
|
176
|
+
size?: "default" | "icon" | "icon-lg" | "icon-sm" | "lg" | "sm" | null | undefined;
|
|
177
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
178
|
+
/** The look of a button: its `variant` and `size` (see {@link buttonVariants}). */
|
|
179
|
+
type ButtonVariantProps = VariantProps<typeof buttonVariants>;
|
|
180
|
+
/** Props of {@link Button}: React Aria's button props, the look, and the attributes React Aria drops. */
|
|
181
|
+
interface ButtonProps extends Omit<ButtonProps$1, 'children'>, ButtonVariantProps {
|
|
182
|
+
/** The label, optionally with an icon before or after it. An icon-only button needs `aria-label`. */
|
|
183
|
+
children?: ReactNode;
|
|
184
|
+
/** The same as `isDisabled`, for code that uses the native attribute name. */
|
|
185
|
+
disabled?: boolean;
|
|
186
|
+
/** A native tooltip, for icon buttons that pair `aria-label` with a visible hint. */
|
|
187
|
+
title?: string;
|
|
188
|
+
/** The keyboard shortcut the button advertises to assistive technology, such as `Control+Enter`. */
|
|
189
|
+
'aria-keyshortcuts'?: string;
|
|
190
|
+
/** The underlying `<button>`. */
|
|
191
|
+
ref?: Ref<HTMLButtonElement>;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* A button: an action the user takes on this page. Built on React Aria's `Button`, so it is pressed
|
|
195
|
+
* the same way with a mouse, touch, the keyboard or a screen reader, and shows its focus ring only to
|
|
196
|
+
* keyboard users.
|
|
197
|
+
*
|
|
198
|
+
* `isPending` keeps the button focusable (so focus never jumps while a save runs), ignores presses,
|
|
199
|
+
* shows a spinner before the label and announces the busy state. Use `ButtonLink` for navigation.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* <Button onPress={save} isPending={isSaving}>Save changes</Button>
|
|
203
|
+
* <Button variant="ghost" size="icon" aria-label="Delete row"><TrashIcon /></Button>
|
|
204
|
+
*/
|
|
205
|
+
export declare const Button: ({ children, className, variant, size, disabled, isDisabled, isPending, title, 'aria-keyshortcuts': keyShortcuts, ref, ...props }: ButtonProps) => import("react").JSX.Element;
|
|
206
|
+
/** Props of {@link ButtonLink}: React Aria's link props and the button look. */
|
|
207
|
+
interface ButtonLinkProps extends LinkProps, ButtonVariantProps {}
|
|
208
|
+
/**
|
|
209
|
+
* A link that looks like a button: navigation presented as an action ("New contact" opening a form
|
|
210
|
+
* page). With {@link UIProvider}'s `navigate`, an internal `href` navigates client-side.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* <ButtonLink href="/contacts/new">New contact</ButtonLink>
|
|
214
|
+
*/
|
|
215
|
+
export declare const ButtonLink: ({ className, variant, size, ...props }: ButtonLinkProps) => import("react").JSX.Element;
|
|
216
|
+
export type { ButtonLinkProps, ButtonProps, ButtonVariantProps, Message, MessageFormatter, MessageHelpers, MessageOverrides, MessageVariables, Messages, ShippedLocale, UIProviderProps };
|
|
217
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Button as Button$1, I18nProvider, Link, RouterProvider, composeRenderProps, useLocale } from "react-aria-components";
|
|
3
|
+
import { createContext, useCallback, useContext, useMemo } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { clsx } from "clsx";
|
|
6
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
7
|
+
import { CircleNotchIcon } from "@phosphor-icons/react";
|
|
8
|
+
import { cva } from "class-variance-authority";
|
|
9
|
+
import { mergeRefs } from "react-aria";
|
|
10
|
+
|
|
11
|
+
// src/i18n/messages.ts
|
|
12
|
+
/**
|
|
13
|
+
* The library's own strings: the words a component renders by itself (a close button's label, a page
|
|
14
|
+
* range, a "no results" line). Everything else a component shows comes from the application, already
|
|
15
|
+
* translated, through props and children, so the library never needs to know the app's message system.
|
|
16
|
+
*
|
|
17
|
+
* Each component keeps its strings beside it (`pagination.messages.ts`, declared with
|
|
18
|
+
* {@link defineMessages}) and reads them with {@link useMessages}, in the locale React Aria's
|
|
19
|
+
* `I18nProvider` (or {@link UIProvider}) sets.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The locales the library ships, as dictionary keys. A locale matches exactly, then by language, then
|
|
23
|
+
* falls back to English: `es-CO`, `es-MX` and `es` all read `es-419`; `fr-FR` reads `en-US`.
|
|
24
|
+
*/
|
|
25
|
+
const SHIPPED_LOCALES = ["en-US", "es-419"];
|
|
26
|
+
/**
|
|
27
|
+
* Declares a component's dictionary. TypeScript checks that every shipped locale translates every key
|
|
28
|
+
* the English one has. Keys are namespaced by component (`pagination.next`) so that an override
|
|
29
|
+
* addresses one message unambiguously.
|
|
30
|
+
*/
|
|
31
|
+
function defineMessages(messages) {
|
|
32
|
+
return messages;
|
|
33
|
+
}
|
|
34
|
+
/** The overrides {@link UIProvider} passes down; `undefined` when the application sets none. */
|
|
35
|
+
const MessageOverridesContext = createContext(void 0);
|
|
36
|
+
/**
|
|
37
|
+
* Formats a component's strings in the current locale, with the application's overrides applied.
|
|
38
|
+
*
|
|
39
|
+
* @param messages - The component's dictionary, from {@link defineMessages}.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* const t = useMessages(paginationMessages);
|
|
43
|
+
* t.format('pagination.next'); // 'Next' or 'Siguiente'
|
|
44
|
+
*/
|
|
45
|
+
function useMessages(messages) {
|
|
46
|
+
const { locale } = useLocale();
|
|
47
|
+
const overrides = useContext(MessageOverridesContext);
|
|
48
|
+
return useMemo(() => {
|
|
49
|
+
const strings = stringsForLocale(messages, locale);
|
|
50
|
+
const numberFormat = new Intl.NumberFormat(locale);
|
|
51
|
+
const pluralRules = new Intl.PluralRules(locale);
|
|
52
|
+
const helpers = {
|
|
53
|
+
locale,
|
|
54
|
+
number: (value) => numberFormat.format(value),
|
|
55
|
+
plural: (count, forms) => forms[pluralRules.select(count)] ?? forms.other
|
|
56
|
+
};
|
|
57
|
+
return { format(key, variables = {}) {
|
|
58
|
+
const override = overrides?.[key];
|
|
59
|
+
if (override !== void 0) return typeof override === "function" ? override(variables) : override;
|
|
60
|
+
const message = strings[key];
|
|
61
|
+
return typeof message === "function" ? message(variables, helpers) : message;
|
|
62
|
+
} };
|
|
63
|
+
}, [
|
|
64
|
+
messages,
|
|
65
|
+
locale,
|
|
66
|
+
overrides
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
/** The shipped strings for a locale: the exact locale, then the same language, then English. */
|
|
70
|
+
function stringsForLocale(messages, locale) {
|
|
71
|
+
const [language] = locale.split("-");
|
|
72
|
+
return messages[SHIPPED_LOCALES.find((candidate) => candidate === locale) ?? SHIPPED_LOCALES.find((candidate) => candidate.split("-")[0] === language) ?? "en-US"];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/provider/ui-provider.tsx
|
|
76
|
+
/**
|
|
77
|
+
* The root every Arbol UI application renders once, above everything else: it tells the components the
|
|
78
|
+
* locale (and with it, the language of their own strings), and how to navigate.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* import { getLocale } from './paraglide/runtime';
|
|
82
|
+
*
|
|
83
|
+
* <UIProvider locale={getLocale()} navigate={(to, options) => router.navigate({ ...options, to })}>
|
|
84
|
+
* <App />
|
|
85
|
+
* </UIProvider>
|
|
86
|
+
*/
|
|
87
|
+
const UIProvider = ({ locale, navigate, useHref, messages, children }) => {
|
|
88
|
+
return /* @__PURE__ */ jsx(I18nProvider, {
|
|
89
|
+
locale,
|
|
90
|
+
children: /* @__PURE__ */ jsx(MessageOverridesContext, {
|
|
91
|
+
value: messages,
|
|
92
|
+
children: navigate ? /* @__PURE__ */ jsx(RouterProvider, {
|
|
93
|
+
navigate,
|
|
94
|
+
useHref,
|
|
95
|
+
children
|
|
96
|
+
}) : children
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// src/lib/class-names.ts
|
|
102
|
+
/** tailwind-merge, taught the theme's own radius names so that `rounded-card` and `rounded-lg` conflict. */
|
|
103
|
+
const twMerge = extendTailwindMerge({ extend: { theme: { radius: [
|
|
104
|
+
"field",
|
|
105
|
+
"menu",
|
|
106
|
+
"card",
|
|
107
|
+
"dialog"
|
|
108
|
+
] } } });
|
|
109
|
+
/**
|
|
110
|
+
* Joins class names and resolves Tailwind conflicts, the last class winning: `cn('px-2', 'px-4')` is
|
|
111
|
+
* `'px-4'`. Every component merges its own classes with the caller's `className` through it, so a
|
|
112
|
+
* caller can always override a component's styles.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* cn('rounded-field px-3', isCompact && 'px-2', className)
|
|
116
|
+
*/
|
|
117
|
+
function cn(...inputs) {
|
|
118
|
+
return twMerge(clsx(inputs));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Merges a component's classes with a React Aria `className`, which is a string or a function of the
|
|
122
|
+
* component's render props (`isPressed`, `isSelected`…). The caller's classes come last, so they win.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* <AriaButton className={composeTailwindRenderProps(props.className, 'rounded-field px-3')} />
|
|
126
|
+
*/
|
|
127
|
+
function composeTailwindRenderProps(className, classes) {
|
|
128
|
+
return composeRenderProps(className, (callerClassName) => cn(classes, callerClassName));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The keyboard focus ring every focusable part shares: invisible for pointer users, a 2px ring in the
|
|
132
|
+
* `ring` token, offset from the background, for keyboard users (React Aria's `focus-visible`).
|
|
133
|
+
*/
|
|
134
|
+
const focusRing = "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
|
|
135
|
+
|
|
136
|
+
// src/components/button/button.tsx
|
|
137
|
+
/**
|
|
138
|
+
* The button styles, as a `cva` recipe, for anything that must look like a button: `Button`,
|
|
139
|
+
* `ButtonLink`, or a third-party trigger.
|
|
140
|
+
*
|
|
141
|
+
* - `variant`: `default` (the primary action, filled), `destructive` (filled red, for the irreversible
|
|
142
|
+
* action itself), `outline` and `secondary` (the second action), `ghost` (toolbar and icon actions),
|
|
143
|
+
* `link` (an action that reads as text).
|
|
144
|
+
* - `size`: `sm` (28px, toolbars and tables), `default` (32px), `lg` (40px), and the square `icon-sm`,
|
|
145
|
+
* `icon` and `icon-lg`. On touch screens every size takes taps across at least 44px.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* <a className={buttonVariants({ variant: 'outline', size: 'sm' })} href="/export.csv">Export</a>
|
|
149
|
+
*/
|
|
150
|
+
const buttonVariants = cva([
|
|
151
|
+
"rounded-field relative inline-flex cursor-pointer items-center justify-center gap-1.5 border border-transparent",
|
|
152
|
+
"touch-target text-sm font-medium whitespace-nowrap select-none",
|
|
153
|
+
"transition-[color,background-color,border-color,box-shadow,transform,opacity] duration-150 ease-out",
|
|
154
|
+
"data-focus-visible:ring-offset-background outline-none data-focus-visible:ring-2 data-focus-visible:ring-offset-2",
|
|
155
|
+
"data-disabled:cursor-not-allowed data-disabled:opacity-50 data-pending:cursor-wait",
|
|
156
|
+
"[&_svg]:size-4 [&_svg]:shrink-0"
|
|
157
|
+
], {
|
|
158
|
+
variants: {
|
|
159
|
+
variant: {
|
|
160
|
+
default: "bg-primary text-primary-foreground data-focus-visible:ring-ring data-hovered:bg-primary/90 shadow-sm data-pressed:scale-[0.98]",
|
|
161
|
+
destructive: "bg-destructive text-destructive-foreground data-focus-visible:ring-destructive/40 data-hovered:bg-destructive/90 shadow-sm data-pressed:scale-[0.98]",
|
|
162
|
+
outline: "border-border text-foreground data-focus-visible:ring-gray-a4 data-hovered:bg-accent data-pressed:bg-gray-a3 bg-transparent",
|
|
163
|
+
secondary: "border-border bg-accent text-foreground data-focus-visible:ring-gray-a4 data-hovered:bg-gray-a3 data-pressed:bg-gray-a4",
|
|
164
|
+
ghost: "text-muted-foreground data-focus-visible:ring-gray-a4 data-hovered:bg-accent data-hovered:text-foreground",
|
|
165
|
+
link: "text-muted-foreground data-focus-visible:text-foreground data-hovered:text-foreground underline-offset-4 data-focus-visible:underline data-hovered:underline"
|
|
166
|
+
},
|
|
167
|
+
size: {
|
|
168
|
+
sm: "h-7 px-2.5 text-xs",
|
|
169
|
+
default: "h-8 px-3",
|
|
170
|
+
lg: "h-10 px-5",
|
|
171
|
+
"icon-sm": "size-7",
|
|
172
|
+
icon: "size-8",
|
|
173
|
+
"icon-lg": "size-10"
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
defaultVariants: {
|
|
177
|
+
variant: "default",
|
|
178
|
+
size: "default"
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
/**
|
|
182
|
+
* A button: an action the user takes on this page. Built on React Aria's `Button`, so it is pressed
|
|
183
|
+
* the same way with a mouse, touch, the keyboard or a screen reader, and shows its focus ring only to
|
|
184
|
+
* keyboard users.
|
|
185
|
+
*
|
|
186
|
+
* `isPending` keeps the button focusable (so focus never jumps while a save runs), ignores presses,
|
|
187
|
+
* shows a spinner before the label and announces the busy state. Use `ButtonLink` for navigation.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* <Button onPress={save} isPending={isSaving}>Save changes</Button>
|
|
191
|
+
* <Button variant="ghost" size="icon" aria-label="Delete row"><TrashIcon /></Button>
|
|
192
|
+
*/
|
|
193
|
+
const Button = ({ children, className, variant, size, disabled, isDisabled, isPending, title, "aria-keyshortcuts": keyShortcuts, ref, ...props }) => {
|
|
194
|
+
const setDropped = useCallback((node) => {
|
|
195
|
+
if (node) {
|
|
196
|
+
setOrRemoveAttribute(node, "title", title);
|
|
197
|
+
setOrRemoveAttribute(node, "aria-keyshortcuts", keyShortcuts);
|
|
198
|
+
}
|
|
199
|
+
}, [title, keyShortcuts]);
|
|
200
|
+
const mergedRef = useMemo(() => mergeRefs(ref, setDropped), [ref, setDropped]);
|
|
201
|
+
return /* @__PURE__ */ jsxs(Button$1, {
|
|
202
|
+
...props,
|
|
203
|
+
className: composeTailwindRenderProps(className, buttonVariants({
|
|
204
|
+
variant,
|
|
205
|
+
size
|
|
206
|
+
})),
|
|
207
|
+
"data-slot": "button",
|
|
208
|
+
isDisabled: disabled ?? isDisabled,
|
|
209
|
+
isPending,
|
|
210
|
+
ref: mergedRef,
|
|
211
|
+
children: [isPending === true ? /* @__PURE__ */ jsx(CircleNotchIcon, {
|
|
212
|
+
"aria-hidden": true,
|
|
213
|
+
className: "motion-safe:animate-spin"
|
|
214
|
+
}) : null, children]
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* A link that looks like a button: navigation presented as an action ("New contact" opening a form
|
|
219
|
+
* page). With {@link UIProvider}'s `navigate`, an internal `href` navigates client-side.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* <ButtonLink href="/contacts/new">New contact</ButtonLink>
|
|
223
|
+
*/
|
|
224
|
+
const ButtonLink = ({ className, variant, size, ...props }) => /* @__PURE__ */ jsx(Link, {
|
|
225
|
+
...props,
|
|
226
|
+
className: composeTailwindRenderProps(className, buttonVariants({
|
|
227
|
+
variant,
|
|
228
|
+
size
|
|
229
|
+
})),
|
|
230
|
+
"data-slot": "button-link"
|
|
231
|
+
});
|
|
232
|
+
/** Sets an attribute when it has a value and removes it when it does not. */
|
|
233
|
+
function setOrRemoveAttribute(node, name, value) {
|
|
234
|
+
if (value === void 0 || value === "") node.removeAttribute(name);
|
|
235
|
+
else node.setAttribute(name, value);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export { Button, ButtonLink, SHIPPED_LOCALES, UIProvider, buttonVariants, cn, composeTailwindRenderProps, defineMessages, focusRing, useMessages };
|
|
239
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["AriaButton","AriaLink"],"sources":["../src/i18n/messages.ts","../src/provider/ui-provider.tsx","../src/lib/class-names.ts","../src/components/button/button.tsx"],"sourcesContent":["import { createContext, useContext, useMemo } from 'react';\nimport { useLocale } from 'react-aria-components';\n\n/**\n * The library's own strings: the words a component renders by itself (a close button's label, a page\n * range, a \"no results\" line). Everything else a component shows comes from the application, already\n * translated, through props and children, so the library never needs to know the app's message system.\n *\n * Each component keeps its strings beside it (`pagination.messages.ts`, declared with\n * {@link defineMessages}) and reads them with {@link useMessages}, in the locale React Aria's\n * `I18nProvider` (or {@link UIProvider}) sets.\n */\n\n/**\n * The locales the library ships, as dictionary keys. A locale matches exactly, then by language, then\n * falls back to English: `es-CO`, `es-MX` and `es` all read `es-419`; `fr-FR` reads `en-US`.\n */\nexport const SHIPPED_LOCALES = ['en-US', 'es-419'] as const;\n\n/** A locale the library ships translations for. */\nexport type ShippedLocale = (typeof SHIPPED_LOCALES)[number];\n\n/** The values a component passes to a message: counts as numbers, names as strings. */\nexport type MessageVariables = Record<string, string | number>;\n\n/** Locale-aware helpers every message function receives with its variables. */\nexport interface MessageHelpers {\n /** The active locale (`es-CO`, `en-US`). */\n locale: string;\n /** Formats a number for the locale: `1234` is `1.234` in `es-CO`, `1,234` in `en-US`. */\n number: (value: number) => string;\n /**\n * Picks the plural form for a count with the locale's rules, falling back to `other`.\n *\n * @example\n * plural(count, { one: '1 contact', other: `${number(count)} contacts` })\n */\n plural: (count: number, forms: Partial<Record<Intl.LDMLPluralRule, string>> & { other: string }) => string;\n}\n\n/**\n * One message: a string, or a function of the variables its component passes that returns one.\n *\n * @example\n * 'pagination.range': ({ first, last, total }, { number }) =>\n * `${number(Number(first))}–${number(Number(last))} of ${number(Number(total))}`\n */\nexport type Message = string | ((variables: MessageVariables, helpers: MessageHelpers) => string);\n\n/** A component's dictionary: every shipped locale, each with the same keys. */\nexport type Messages<K extends string> = Record<ShippedLocale, Record<K, Message>>;\n\n/**\n * Replacements for the library's strings, by message key, applied in whichever locale is active. A\n * value is a string, or a function of the message's variables, so an app's own translation functions\n * plug in directly: Paraglide's `m.ui_close`, or `(variables) => t('ui.close', variables)` with\n * i18next. The variables keep the names Arbol's catalogs already use (`first`, `last`, `total`,\n * `count`, `page`, `label`).\n *\n * @example\n * { 'overlay.close': m.ui_close, 'pagination.range': m.ui_pagination_range }\n */\nexport type MessageOverrides = Record<string, string | ((variables: MessageVariables) => string)>;\n\n/**\n * Declares a component's dictionary. TypeScript checks that every shipped locale translates every key\n * the English one has. Keys are namespaced by component (`pagination.next`) so that an override\n * addresses one message unambiguously.\n */\nexport function defineMessages<K extends string>(messages: Messages<K>): Messages<K> {\n return messages;\n}\n\n/** The overrides {@link UIProvider} passes down; `undefined` when the application sets none. */\nexport const MessageOverridesContext = createContext<MessageOverrides | undefined>(undefined);\n\n/** What {@link useMessages} returns. */\nexport interface MessageFormatter<K extends string> {\n /** The message for a key in the active locale, with its variables filled in. */\n format: (key: K, variables?: MessageVariables) => string;\n}\n\n/**\n * Formats a component's strings in the current locale, with the application's overrides applied.\n *\n * @param messages - The component's dictionary, from {@link defineMessages}.\n *\n * @example\n * const t = useMessages(paginationMessages);\n * t.format('pagination.next'); // 'Next' or 'Siguiente'\n */\nexport function useMessages<K extends string>(messages: Messages<K>): MessageFormatter<K> {\n const { locale } = useLocale();\n const overrides = useContext(MessageOverridesContext);\n return useMemo(() => {\n const strings = stringsForLocale(messages, locale);\n const numberFormat = new Intl.NumberFormat(locale);\n const pluralRules = new Intl.PluralRules(locale);\n const helpers: MessageHelpers = {\n locale,\n number: (value) => numberFormat.format(value),\n plural: (count, forms) => forms[pluralRules.select(count)] ?? forms.other,\n };\n return {\n format(key, variables = {}) {\n const override = overrides?.[key];\n if (override !== undefined) {\n // An app's function gets the variables only: Paraglide reads a second argument as options.\n return typeof override === 'function' ? override(variables) : override;\n }\n const message: Message = strings[key];\n return typeof message === 'function' ? message(variables, helpers) : message;\n },\n };\n }, [messages, locale, overrides]);\n}\n\n/** The shipped strings for a locale: the exact locale, then the same language, then English. */\nfunction stringsForLocale<K extends string>(messages: Messages<K>, locale: string): Record<K, Message> {\n const [language] = locale.split('-');\n const shipped =\n SHIPPED_LOCALES.find((candidate) => candidate === locale) ??\n SHIPPED_LOCALES.find((candidate) => candidate.split('-')[0] === language) ??\n 'en-US';\n return messages[shipped];\n}\n","import type { ReactNode } from 'react';\nimport { I18nProvider, RouterProvider } from 'react-aria-components';\nimport type { RouterConfig } from 'react-aria-components';\n\nimport { MessageOverridesContext } from '../i18n/messages';\nimport type { MessageOverrides } from '../i18n/messages';\n\n/** The path type React Aria's links take; an app narrows it by augmenting `RouterConfig`. */\ntype Href = RouterConfig extends { href: infer H } ? H : string;\n\n/** The navigation options React Aria passes through; an app narrows them by augmenting `RouterConfig`. */\ntype RouterOptions = RouterConfig extends { routerOptions: infer O } ? O : undefined;\n\n/** Props of {@link UIProvider}. */\nexport interface UIProviderProps {\n /**\n * The BCP 47 locale every component formats and speaks in (`es-CO`, `en-US`): dates and numbers,\n * React Aria's built-in announcements, the library's own strings and the reading direction. Pass the\n * locale of the app's message system (Paraglide's `getLocale()`, i18next's `i18n.language`).\n * Defaults to the browser's language.\n */\n locale?: string;\n /**\n * Client-side navigation for every `href` the components render (links, menu items, tabs, rows).\n * Pass the router's navigate; without it, an `href` is a full-page navigation.\n *\n * @example\n * // TanStack Router\n * navigate={(to, options) => router.navigate({ ...options, to })}\n */\n navigate?: (path: Href, routerOptions?: RouterOptions) => void;\n /**\n * Turns a router path into the URL the browser shows (needed when the router adds a base path).\n *\n * @example\n * // TanStack Router\n * useHref={(to) => router.buildLocation({ to }).href}\n */\n useHref?: (href: Href) => string;\n /**\n * Replacements for the library's own strings, by message key, in whichever locale is active; see\n * {@link MessageOverrides}. Paraglide's `m.*` functions plug in directly.\n *\n * @example\n * messages={{ 'overlay.close': m.ui_close, 'pagination.range': m.ui_pagination_range }}\n */\n messages?: MessageOverrides;\n /** The application. */\n children: ReactNode;\n}\n\n/**\n * The root every Arbol UI application renders once, above everything else: it tells the components the\n * locale (and with it, the language of their own strings), and how to navigate.\n *\n * @example\n * import { getLocale } from './paraglide/runtime';\n *\n * <UIProvider locale={getLocale()} navigate={(to, options) => router.navigate({ ...options, to })}>\n * <App />\n * </UIProvider>\n */\nexport const UIProvider = ({ locale, navigate, useHref, messages, children }: UIProviderProps) => {\n const routed = navigate ? (\n <RouterProvider navigate={navigate} useHref={useHref}>\n {children}\n </RouterProvider>\n ) : (\n children\n );\n return (\n <I18nProvider locale={locale}>\n <MessageOverridesContext value={messages}>{routed}</MessageOverridesContext>\n </I18nProvider>\n );\n};\n","import { clsx } from 'clsx';\nimport type { ClassValue } from 'clsx';\nimport { composeRenderProps } from 'react-aria-components';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\n/** tailwind-merge, taught the theme's own radius names so that `rounded-card` and `rounded-lg` conflict. */\nconst twMerge = extendTailwindMerge({\n extend: { theme: { radius: ['field', 'menu', 'card', 'dialog'] } },\n});\n\n/**\n * Joins class names and resolves Tailwind conflicts, the last class winning: `cn('px-2', 'px-4')` is\n * `'px-4'`. Every component merges its own classes with the caller's `className` through it, so a\n * caller can always override a component's styles.\n *\n * @example\n * cn('rounded-field px-3', isCompact && 'px-2', className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n\n/**\n * Merges a component's classes with a React Aria `className`, which is a string or a function of the\n * component's render props (`isPressed`, `isSelected`…). The caller's classes come last, so they win.\n *\n * @example\n * <AriaButton className={composeTailwindRenderProps(props.className, 'rounded-field px-3')} />\n */\nexport function composeTailwindRenderProps<T>(\n className: string | ((values: T) => string) | undefined,\n classes: ClassValue\n): string | ((values: T) => string) {\n return composeRenderProps(className, (callerClassName) => cn(classes, callerClassName));\n}\n\n/**\n * The keyboard focus ring every focusable part shares: invisible for pointer users, a 2px ring in the\n * `ring` token, offset from the background, for keyboard users (React Aria's `focus-visible`).\n */\nexport const focusRing =\n 'outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background';\n","import { CircleNotchIcon } from '@phosphor-icons/react';\nimport { cva } from 'class-variance-authority';\nimport type { VariantProps } from 'class-variance-authority';\nimport { useCallback, useMemo } from 'react';\nimport type { ReactNode, Ref } from 'react';\nimport { mergeRefs } from 'react-aria';\nimport { Button as AriaButton, Link as AriaLink } from 'react-aria-components';\nimport type { ButtonProps as AriaButtonProps, LinkProps as AriaLinkProps } from 'react-aria-components';\n\nimport { composeTailwindRenderProps } from '../../lib/class-names';\n\n/**\n * The button styles, as a `cva` recipe, for anything that must look like a button: `Button`,\n * `ButtonLink`, or a third-party trigger.\n *\n * - `variant`: `default` (the primary action, filled), `destructive` (filled red, for the irreversible\n * action itself), `outline` and `secondary` (the second action), `ghost` (toolbar and icon actions),\n * `link` (an action that reads as text).\n * - `size`: `sm` (28px, toolbars and tables), `default` (32px), `lg` (40px), and the square `icon-sm`,\n * `icon` and `icon-lg`. On touch screens every size takes taps across at least 44px.\n *\n * @example\n * <a className={buttonVariants({ variant: 'outline', size: 'sm' })} href=\"/export.csv\">Export</a>\n */\nexport const buttonVariants = cva(\n [\n 'rounded-field relative inline-flex cursor-pointer items-center justify-center gap-1.5 border border-transparent',\n 'touch-target text-sm font-medium whitespace-nowrap select-none',\n 'transition-[color,background-color,border-color,box-shadow,transform,opacity] duration-150 ease-out',\n 'data-focus-visible:ring-offset-background outline-none data-focus-visible:ring-2 data-focus-visible:ring-offset-2',\n 'data-disabled:cursor-not-allowed data-disabled:opacity-50 data-pending:cursor-wait',\n '[&_svg]:size-4 [&_svg]:shrink-0',\n ],\n {\n variants: {\n variant: {\n default:\n 'bg-primary text-primary-foreground data-focus-visible:ring-ring data-hovered:bg-primary/90 shadow-sm data-pressed:scale-[0.98]',\n destructive:\n 'bg-destructive text-destructive-foreground data-focus-visible:ring-destructive/40 data-hovered:bg-destructive/90 shadow-sm data-pressed:scale-[0.98]',\n outline:\n 'border-border text-foreground data-focus-visible:ring-gray-a4 data-hovered:bg-accent data-pressed:bg-gray-a3 bg-transparent',\n secondary:\n 'border-border bg-accent text-foreground data-focus-visible:ring-gray-a4 data-hovered:bg-gray-a3 data-pressed:bg-gray-a4',\n ghost:\n 'text-muted-foreground data-focus-visible:ring-gray-a4 data-hovered:bg-accent data-hovered:text-foreground',\n link: 'text-muted-foreground data-focus-visible:text-foreground data-hovered:text-foreground underline-offset-4 data-focus-visible:underline data-hovered:underline',\n },\n size: {\n sm: 'h-7 px-2.5 text-xs',\n default: 'h-8 px-3',\n lg: 'h-10 px-5',\n 'icon-sm': 'size-7',\n icon: 'size-8',\n 'icon-lg': 'size-10',\n },\n },\n defaultVariants: { variant: 'default', size: 'default' },\n }\n);\n\n/** The look of a button: its `variant` and `size` (see {@link buttonVariants}). */\nexport type ButtonVariantProps = VariantProps<typeof buttonVariants>;\n\n/** Props of {@link Button}: React Aria's button props, the look, and the attributes React Aria drops. */\nexport interface ButtonProps extends Omit<AriaButtonProps, 'children'>, ButtonVariantProps {\n /** The label, optionally with an icon before or after it. An icon-only button needs `aria-label`. */\n children?: ReactNode;\n /** The same as `isDisabled`, for code that uses the native attribute name. */\n disabled?: boolean;\n /** A native tooltip, for icon buttons that pair `aria-label` with a visible hint. */\n title?: string;\n /** The keyboard shortcut the button advertises to assistive technology, such as `Control+Enter`. */\n 'aria-keyshortcuts'?: string;\n /** The underlying `<button>`. */\n ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * A button: an action the user takes on this page. Built on React Aria's `Button`, so it is pressed\n * the same way with a mouse, touch, the keyboard or a screen reader, and shows its focus ring only to\n * keyboard users.\n *\n * `isPending` keeps the button focusable (so focus never jumps while a save runs), ignores presses,\n * shows a spinner before the label and announces the busy state. Use `ButtonLink` for navigation.\n *\n * @example\n * <Button onPress={save} isPending={isSaving}>Save changes</Button>\n * <Button variant=\"ghost\" size=\"icon\" aria-label=\"Delete row\"><TrashIcon /></Button>\n */\nexport const Button = ({\n children,\n className,\n variant,\n size,\n disabled,\n isDisabled,\n isPending,\n title,\n 'aria-keyshortcuts': keyShortcuts,\n ref,\n ...props\n}: ButtonProps) => {\n // React Aria's button keeps only the attributes it knows: `title` and `aria-keyshortcuts` go on the\n // element directly.\n const setDropped = useCallback(\n (node: HTMLButtonElement | null) => {\n if (node) {\n setOrRemoveAttribute(node, 'title', title);\n setOrRemoveAttribute(node, 'aria-keyshortcuts', keyShortcuts);\n }\n },\n [title, keyShortcuts]\n );\n const mergedRef = useMemo(() => mergeRefs(ref, setDropped), [ref, setDropped]);\n return (\n <AriaButton\n {...props}\n className={composeTailwindRenderProps(className, buttonVariants({ variant, size }))}\n data-slot=\"button\"\n isDisabled={disabled ?? isDisabled}\n isPending={isPending}\n ref={mergedRef}\n >\n {isPending === true ? <CircleNotchIcon aria-hidden className=\"motion-safe:animate-spin\" /> : null}\n {children}\n </AriaButton>\n );\n};\n\n/** Props of {@link ButtonLink}: React Aria's link props and the button look. */\nexport interface ButtonLinkProps extends AriaLinkProps, ButtonVariantProps {}\n\n/**\n * A link that looks like a button: navigation presented as an action (\"New contact\" opening a form\n * page). With {@link UIProvider}'s `navigate`, an internal `href` navigates client-side.\n *\n * @example\n * <ButtonLink href=\"/contacts/new\">New contact</ButtonLink>\n */\nexport const ButtonLink = ({ className, variant, size, ...props }: ButtonLinkProps) => (\n <AriaLink\n {...props}\n className={composeTailwindRenderProps(className, buttonVariants({ variant, size }))}\n data-slot=\"button-link\"\n />\n);\n\n/** Sets an attribute when it has a value and removes it when it does not. */\nfunction setOrRemoveAttribute(node: HTMLElement, name: string, value: string | undefined) {\n if (value === undefined || value === '') {\n node.removeAttribute(name);\n } else {\n node.setAttribute(name, value);\n }\n}\n"],"mappings":"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"}
|