@api7/ui-components 0.6.0 → 0.8.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 +170 -9
- package/dist/components/anchor/anchor.d.ts +28 -0
- package/dist/components/anchor/anchor.js +249 -0
- package/dist/components/breadcrumb/breadcrumb.d.ts +15 -0
- package/dist/components/breadcrumb/breadcrumb.js +60 -0
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/button/button.js +1 -1
- package/dist/components/calendar/calendar.js +24 -3
- package/dist/components/cascader/cascader-data.d.ts +26 -0
- package/dist/components/cascader/cascader-data.js +110 -0
- package/dist/components/cascader/cascader.d.ts +41 -0
- package/dist/components/cascader/cascader.js +513 -0
- package/dist/components/code/code.d.ts +9 -0
- package/dist/components/code/code.js +20 -0
- package/dist/components/code/copy-button.js +42 -0
- package/dist/components/code-block/code-block.d.ts +16 -0
- package/dist/components/code-block/code-block.js +45 -0
- package/dist/components/code-block/highlight.js +67 -0
- package/dist/components/collapsible/collapsible.d.ts +14 -0
- package/dist/components/collapsible/collapsible.js +36 -0
- package/dist/components/combobox/combobox-loading.d.ts +18 -0
- package/dist/components/combobox/combobox-loading.js +148 -0
- package/dist/components/combobox/combobox.d.ts +13 -6
- package/dist/components/combobox/combobox.js +58 -10
- package/dist/components/data-table/data-table.d.ts +27 -3
- package/dist/components/data-table/data-table.js +155 -40
- package/dist/components/data-table/expandable-row.js +40 -0
- package/dist/components/data-table/filter-header.d.ts +28 -5
- package/dist/components/data-table/filter-header.js +103 -40
- package/dist/components/data-table/index.d.ts +3 -3
- package/dist/components/data-table/use-table-layout.js +84 -0
- package/dist/components/date-picker/date-picker.js +11 -2
- package/dist/components/description-list/description-list.d.ts +1 -1
- package/dist/components/dialog/dialog.js +1 -1
- package/dist/components/drawer/drawer.js +1 -1
- package/dist/components/empty/empty.d.ts +1 -1
- package/dist/components/field/labeled-field.js +1 -1
- package/dist/components/file-upload/file-upload.d.ts +44 -0
- package/dist/components/file-upload/file-upload.js +195 -0
- package/dist/components/form/choice-fields.js +2 -2
- package/dist/components/form/date-fields.d.ts +1 -1
- package/dist/components/form/date-fields.js +1 -1
- package/dist/components/form/form-item.js +1 -1
- package/dist/components/form/form.js +1 -1
- package/dist/components/form/index.d.ts +2 -2
- package/dist/components/form/index.js +4 -4
- package/dist/components/form/text-field.js +2 -2
- package/dist/components/form/toggle-fields.d.ts +1 -1
- package/dist/components/form/toggle-fields.js +3 -3
- package/dist/components/pagination/pagination.d.ts +20 -4
- package/dist/components/pagination/pagination.js +83 -32
- package/dist/components/relative-date-range-picker/relative-date-range-picker.d.ts +14 -0
- package/dist/components/relative-date-range-picker/relative-date-range-picker.js +245 -0
- package/dist/components/relative-date-range-picker/relative-date-range.d.ts +26 -0
- package/dist/components/relative-date-range-picker/relative-date-range.js +109 -0
- package/dist/components/select/select.js +1 -1
- package/dist/components/sidebar/sidebar.js +2 -2
- package/dist/components/steps/steps.d.ts +23 -0
- package/dist/components/steps/steps.js +86 -0
- package/dist/components/table/table.d.ts +2 -1
- package/dist/components/table/table.js +2 -1
- package/dist/components/toast/toast.d.ts +1 -1
- package/dist/components/toast/toast.js +1 -1
- package/dist/config/config-provider.d.ts +2 -2
- package/dist/config/messages.d.ts +115 -1
- package/dist/config/messages.js +140 -2
- package/dist/hooks/use-clipboard.d.ts +11 -0
- package/dist/hooks/use-clipboard.js +75 -0
- package/dist/hooks/use-field-label-with-text.js +32 -0
- package/dist/index.d.ts +26 -13
- package/dist/index.js +15 -4
- package/dist/theme.css +1 -0
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.js +1 -1
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -16,14 +16,14 @@ yarn add @api7/ui-components tailwindcss react-day-picker lucide-react
|
|
|
16
16
|
|
|
17
17
|
`tailwindcss` is already there if the application uses Tailwind. Install the integration of Tailwind for your build tool as well (`@tailwindcss/vite`, `@tailwindcss/postcss` or `@tailwindcss/cli`). The import of the theme works with the Vite plugin, the PostCSS plugin and the CLI, which are the ones tested. Other build tools, such as webpack, are not tested. The package expects these peer dependencies in the application:
|
|
18
18
|
|
|
19
|
-
| Package | Version | Needed for
|
|
20
|
-
| ----------------------- | --------- |
|
|
21
|
-
| `react`, `react-dom` | `^19` | Everything
|
|
22
|
-
| `tailwindcss` | `^4.1.0` | Compiles the classes of the components and registers the theme. See [Quick start](#quick-start)
|
|
23
|
-
| `lucide-react` | `^1` | Icons used inside components
|
|
24
|
-
| `react-day-picker` | `^10.0.1` | Required. `Calendar`, `DatePicker` and `
|
|
25
|
-
| `@tanstack/react-form` | `^1.33.0` | `@api7/ui-components/form` (optional)
|
|
26
|
-
| `@tanstack/react-table` | `^9.0.0` | `@api7/ui-components/data-table` (optional)
|
|
19
|
+
| Package | Version | Needed for |
|
|
20
|
+
| ----------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| `react`, `react-dom` | `^19` | Everything |
|
|
22
|
+
| `tailwindcss` | `^4.1.0` | Compiles the classes of the components and registers the theme. See [Quick start](#quick-start) |
|
|
23
|
+
| `lucide-react` | `^1` | Icons used inside components |
|
|
24
|
+
| `react-day-picker` | `^10.0.1` | Required. `Calendar`, `DatePicker`, `DateRangePicker` and `RelativeDateRangePicker` import it, and the package declares it as a required peer, so install it even if you do not use them |
|
|
25
|
+
| `@tanstack/react-form` | `^1.33.0` | `@api7/ui-components/form` (optional) |
|
|
26
|
+
| `@tanstack/react-table` | `^9.0.0` | `@api7/ui-components/data-table` (optional) |
|
|
27
27
|
|
|
28
28
|
Add the optional ones only if you import those entry points.
|
|
29
29
|
|
|
@@ -100,6 +100,10 @@ Add the optional ones only if you import those entry points.
|
|
|
100
100
|
|
|
101
101
|
The package is ESM only. Component modules keep their `"use client"` directive, so the components work in a React Server Components setup such as the Next.js app router.
|
|
102
102
|
|
|
103
|
+
## Code
|
|
104
|
+
|
|
105
|
+
For inline code, highlighted code blocks, line numbers, wrapping and localized copy feedback, see [Code and CodeBlock](src/components/code/README.md). Both components and the reusable `useClipboard` hook are exported from the package root.
|
|
106
|
+
|
|
103
107
|
## Buttons
|
|
104
108
|
|
|
105
109
|
`Button` takes a `variant`: `primary` (the default), `secondary`, `outline`, `ghost`, `destructive` or `link`. It takes a `size`: `sm`, `md` (the default), `lg` or `icon`. `loading` disables the button and shows a spinner, and `loadingLabel` sets the text that screen readers announce.
|
|
@@ -136,6 +140,45 @@ export function DocsLink() {
|
|
|
136
140
|
}
|
|
137
141
|
```
|
|
138
142
|
|
|
143
|
+
## Breadcrumbs
|
|
144
|
+
|
|
145
|
+
Compose a `Breadcrumb` navigation region from `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbPage` and `BreadcrumbSeparator`. Links use muted text, the current page uses foreground text, and the list uses 14px text with 8px gaps.
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
import {
|
|
149
|
+
Breadcrumb,
|
|
150
|
+
BreadcrumbItem,
|
|
151
|
+
BreadcrumbLink,
|
|
152
|
+
BreadcrumbList,
|
|
153
|
+
BreadcrumbPage,
|
|
154
|
+
BreadcrumbSeparator,
|
|
155
|
+
} from '@api7/ui-components';
|
|
156
|
+
|
|
157
|
+
<Breadcrumb>
|
|
158
|
+
<BreadcrumbList>
|
|
159
|
+
<BreadcrumbItem>
|
|
160
|
+
<BreadcrumbLink href="/docs">Docs</BreadcrumbLink>
|
|
161
|
+
</BreadcrumbItem>
|
|
162
|
+
<BreadcrumbSeparator />
|
|
163
|
+
<BreadcrumbItem>
|
|
164
|
+
<BreadcrumbLink href="/docs/components">Components</BreadcrumbLink>
|
|
165
|
+
</BreadcrumbItem>
|
|
166
|
+
<BreadcrumbSeparator />
|
|
167
|
+
<BreadcrumbItem>
|
|
168
|
+
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
|
|
169
|
+
</BreadcrumbItem>
|
|
170
|
+
</BreadcrumbList>
|
|
171
|
+
</Breadcrumb>;
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`BreadcrumbLink` renders an anchor. For a router link, pass `render={<Link to="/docs" />}` with your router's `Link`; it must forward props and its ref to an anchor. The render prop composes the link's handlers, styles and refs. `BreadcrumbPage` is noninteractive text with `aria-current="page"`. To keep the current page clickable, use `BreadcrumbLink` with `aria-current="page"` and `className="text-foreground"` instead.
|
|
175
|
+
|
|
176
|
+
`BreadcrumbSeparator` defaults to a chevron and is hidden from assistive technology. Pass children to replace it, such as `<BreadcrumbSeparator>/</BreadcrumbSeparator>`, and place separators only between items. The navigation label comes from `ConfigProvider`'s `breadcrumb.label` message; override it with `aria-label` or `aria-labelledby` to distinguish multiple trails.
|
|
177
|
+
|
|
178
|
+
Existing dictionaries typed as `Messages` can omit `breadcrumb`. `ConfigProvider` fills in the localized default, and `useMessages()` returns the complete messages.
|
|
179
|
+
|
|
180
|
+
The list and long labels wrap within the available width. Each part accepts native props, a ref and `className`. Put icons, menus or loading placeholders inside an item as needed. Padding, background, sticky positioning and deciding when to show the trail belong to the application.
|
|
181
|
+
|
|
139
182
|
## Spinners
|
|
140
183
|
|
|
141
184
|
`Spinner` is an SVG that spins, and it is a status, so a screen reader announces it. It takes a `size`: `xs`, `sm`, `md` (the default) or `lg`. Its colour is the text colour, so set it with a `text-*` class, and any other `className` is merged onto the SVG.
|
|
@@ -180,6 +223,76 @@ Things worth knowing before you build on it:
|
|
|
180
223
|
- An indeterminate bar slides a segment along the track. When the user prefers reduced motion it shows the whole track at half opacity instead of a static segment, so it is not read as a partial value. The width of a determinate bar is animated too, and that stops as well.
|
|
181
224
|
- The fill uses the text colour of each tone (`primary-text`, `success-foreground`, `warning-foreground` and `destructive-text`), because those stay 3:1 against the track in both themes, and the plain tone colours do not.
|
|
182
225
|
|
|
226
|
+
## Combobox
|
|
227
|
+
|
|
228
|
+
`Combobox` filters local options or accepts a `loadItems` callback for debounced remote search and paginated loading, including loading states and retry actions. Both modes use the same input, content, and selection parts. See the [Combobox guide](src/components/combobox/README.md) for the loader contract, selection, and changing data sources.
|
|
229
|
+
|
|
230
|
+
## Cascader
|
|
231
|
+
|
|
232
|
+
`Cascader` selects a hierarchical path in one popup, with optional intermediate selection, lazy children, loaded-path search, and keyboard navigation. See the [Cascader guide](src/components/cascader/README.md) for options, controlled values, loading, and form integration.
|
|
233
|
+
|
|
234
|
+
## Collapsible
|
|
235
|
+
|
|
236
|
+
`Collapsible` groups a disclosure trigger and its content. Use `defaultOpen` for local state, or `open` and `onOpenChange` to control it. The root also works without a trigger when another control owns its state.
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@api7/ui-components';
|
|
240
|
+
|
|
241
|
+
<Collapsible defaultOpen>
|
|
242
|
+
<CollapsibleTrigger>Additional details</CollapsibleTrigger>
|
|
243
|
+
<CollapsibleContent>
|
|
244
|
+
<div className="pt-3">More information about this item.</div>
|
|
245
|
+
</CollapsibleContent>
|
|
246
|
+
</Collapsible>;
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`CollapsibleTrigger` renders a button with an automatically rotating chevron. It supports Enter and Space, exposes `aria-expanded` and `aria-controls`, and accepts `render={<Button variant="outline" />}` to compose with a button. `disabled` on the root or trigger prevents interaction. The root has no border or padding so it can fit within a navigation group, card or form.
|
|
250
|
+
|
|
251
|
+
`CollapsibleContent` unmounts after closing by default. Set `keepMounted` on it to retain local state and uncontrolled form inputs while closed; hidden content is excluded from keyboard navigation. Put spacing on a child of the content, as in the example, so it collapses fully. The height and opacity transitions respect reduced motion. All parts accept native props, refs, `render` and a string `className`.
|
|
252
|
+
|
|
253
|
+
For several sections, render one controlled `Collapsible` per item with a stable key. Store a single active ID to allow only one open section, or a list of IDs for independent expansion. The [Grouped Storybook example](../storybook/stories/components/collapsible/collapsible.stories.tsx) demonstrates independent panels, expand-all and collapse-all controls. When sections need headings, put each trigger inside a heading of the appropriate level and give its content `role="region"` and `aria-labelledby` pointing at the trigger's ID. Keep adjacent action buttons outside the heading and trigger.
|
|
254
|
+
|
|
255
|
+
## Anchor navigation
|
|
256
|
+
|
|
257
|
+
`Anchor` renders a table of contents as native links and follows the current section while scrolling. Each item has a unique same-document fragment `href`, a `title` (a React node) and optional nested `children`. Give the navigation an `aria-label` or `aria-labelledby`.
|
|
258
|
+
|
|
259
|
+
The active link uses semibold primary text. The navigation has no vertical track, indicator bar or independent scrollbar; it stays alongside the scrolling page by default.
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
import { Anchor } from '@api7/ui-components';
|
|
263
|
+
|
|
264
|
+
const items = [
|
|
265
|
+
{ href: '#overview', title: 'Overview' },
|
|
266
|
+
{ href: '#settings', title: 'Settings', children: [{ href: '#notifications', title: 'Notifications' }] },
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
export function SettingsPage() {
|
|
270
|
+
return (
|
|
271
|
+
<div className="grid grid-cols-1 items-start gap-8 md:grid-cols-[200px_minmax(0,1fr)]">
|
|
272
|
+
<Anchor aria-label="On this page" className="static md:sticky" offsetTop={64} targetOffset={80} items={items} />
|
|
273
|
+
<main className="min-w-0 break-words">
|
|
274
|
+
<section id="overview">Overview content</section>
|
|
275
|
+
<section id="settings">Settings content</section>
|
|
276
|
+
<section id="notifications">Notification preferences</section>
|
|
277
|
+
</main>
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
- Scrolling defaults to the window. For a scrollable element, pass `getContainer={() => scrollRef.current}`. Targets must be inside that container. Its border is excluded when calculating offsets.
|
|
284
|
+
- Keep `items` and `getContainer` stable across unrelated renders, using a module-level array or `useMemo` for items and `useCallback` for the container getter. Changes to these props rebuild the scroll observers. `onActiveChange` can be an inline callback; its identity does not rebuild observers.
|
|
285
|
+
- `sticky` defaults to `true`; `offsetTop` sets its CSS sticky offset in pixels, and `targetOffset` sets the scroll destination and active section threshold. `targetOffset` defaults to `offsetTop`, which defaults to zero. Sticky positioning is limited by the navigation's containing block, so keep it alongside the full content column, as above.
|
|
286
|
+
- Clicks scroll smoothly without changing the URL or adding history entries. Set `scrollBehavior="instant"` for immediate scrolling or `updateHash` to add a fragment history entry on navigation. Reduced motion always uses instant scrolling. Modified clicks retain native browser behavior.
|
|
287
|
+
- The clicked link stays active during smooth navigation. Scroll tracking resumes when the animation ends or the user interrupts it with a pointer, touch, wheel or scrolling key.
|
|
288
|
+
- The first visible target is active before scrolling reaches it. The last target becomes active at the bottom, even if its section is short. Missing and hidden targets are skipped; changes to the items or document layout refresh the active section. `onActiveChange(href)` reports changes, including an empty string when no target remains.
|
|
289
|
+
- Links remain keyboard accessible, use `aria-current="location"` for the active section, and move focus to the destination after navigation. `onNavigate(event, item)` can cancel a jump with `event.preventDefault()`.
|
|
290
|
+
- The root accepts native `nav` props, including `ref`, `className` and `style`. Use the `anchor`, `anchor-list` and `anchor-link` data slots for more specific overrides. Width and surrounding layout belong to the application.
|
|
291
|
+
|
|
292
|
+
## File selection and uploads
|
|
293
|
+
|
|
294
|
+
`FileUpload` selects local files with type, size and custom validation, displays removable file rows, and shows loading or upload progress. It supports controlled and uncontrolled selections, multiple files, custom previews and a button-only layout. The application owns file reading and network requests. See the [FileUpload guide](src/components/file-upload/README.md) for the API and examples.
|
|
295
|
+
|
|
183
296
|
## Forms
|
|
184
297
|
|
|
185
298
|
`@api7/ui-components/form` wraps TanStack Form. `useAppForm` returns a form whose `AppField` hands you ready made fields: `TextField`, `TextareaField`, `NumberField`, `SelectField`, `ComboboxField`, `DatePickerField`, `DateRangePickerField`, `CheckboxField`, `SwitchField` and `RadioGroupField`:
|
|
@@ -327,6 +440,52 @@ Things worth knowing before you build on it:
|
|
|
327
440
|
- A value can hold any content. It grows with its content and long text wraps, so do not give a value a fixed height.
|
|
328
441
|
- A tooltip in a label is a `Tooltip` you place there, and its trigger needs an accessible name, as it does anywhere else.
|
|
329
442
|
|
|
443
|
+
## Steps
|
|
444
|
+
|
|
445
|
+
`Steps` shows progress through an ordered sequence. It uses 24px indicators with dots for waiting and active steps, a check for completed steps and a cross for errors. Horizontal lists place labels below the indicators; `orientation="vertical"` places them beside the indicators.
|
|
446
|
+
|
|
447
|
+
```tsx
|
|
448
|
+
import { Button, Steps } from '@api7/ui-components';
|
|
449
|
+
import { useState } from 'react';
|
|
450
|
+
|
|
451
|
+
const items = [{ title: 'Details' }, { title: 'Review' }, { title: 'Publish' }];
|
|
452
|
+
|
|
453
|
+
export function DocumentWizard() {
|
|
454
|
+
const [current, setCurrent] = useState(0);
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<div className="flex flex-col gap-6">
|
|
458
|
+
<Steps items={items} current={current} aria-label="Document publishing" />
|
|
459
|
+
<div hidden={current !== 0}>Document details</div>
|
|
460
|
+
<div hidden={current !== 1}>Review the document</div>
|
|
461
|
+
<div hidden={current !== 2}>Ready to publish</div>
|
|
462
|
+
<div className="flex justify-end gap-2">
|
|
463
|
+
<Button variant="outline" disabled={current === 0} onClick={() => setCurrent(current - 1)}>
|
|
464
|
+
Previous
|
|
465
|
+
</Button>
|
|
466
|
+
<Button disabled={current === items.length - 1} onClick={() => setCurrent(current + 1)}>
|
|
467
|
+
Next
|
|
468
|
+
</Button>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
| Prop | Meaning |
|
|
476
|
+
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
477
|
+
| `items` | A readonly array of `StepsItem`: `title`, optional `description`, decorative `icon`, `status`, `disabled` and stable React `key`. Titles and descriptions accept non-interactive React content. |
|
|
478
|
+
| `current` | Zero-based active index, default 0. Earlier steps are complete and later steps are waiting. Set it to `items.length` after the operation succeeds to show completion without a current step. Finite indexes are rounded down and clamped to 0 through `items.length`; non-finite values use 0. |
|
|
479
|
+
| `status` | Status of the current step: `process` (default), `wait`, `finish` or `error`. An item's explicit status overrides the inferred status. |
|
|
480
|
+
| `orientation` | `horizontal` (default) or `vertical`. Choose vertical for long sequences in narrow containers. |
|
|
481
|
+
| `onStepChange` | Optional `(index: number) => void`. Enables step buttons and requests a different index without changing `current` internally. The current step does not request a change. |
|
|
482
|
+
| `disabled` | Disables all step buttons. An individual item's `disabled` disables only that button. These props do not change progress status. |
|
|
483
|
+
| Native list props | `className`, `style`, `ref`, `aria-label`, `aria-labelledby` and other ordered-list attributes are forwarded to the root. |
|
|
484
|
+
|
|
485
|
+
Without `onStepChange`, Steps is a read-only ordered list. With it, every step is a native button, reachable with Tab and activated with Enter or Space; disabled steps are skipped. The current step carries `aria-current="step"`. The list name and status descriptions come from `ConfigProvider` messages under `steps` (`label`, `wait`, `process`, `finish`, `error`), with English and Chinese defaults. Give each flow a meaningful `aria-label` or `aria-labelledby`.
|
|
486
|
+
|
|
487
|
+
Steps does not own panels, validation or submission. Compose these with `Button`, using `disabled` and `loading` while validating or saving, and update `current` only when a transition succeeds. Keep panels mounted with `hidden` to retain form state, or store their values in the parent. To allow revisiting completed steps, supply `onStepChange` and disable steps that are not yet available. The Storybook `Wizard` example includes required input, backward navigation, retained values, loading and successful completion.
|
|
488
|
+
|
|
330
489
|
## Theming
|
|
331
490
|
|
|
332
491
|
The library theme is the default theme of the application. The colors, the radii and the font are Tailwind theme variables, such as `--color-primary` and `--font-sans`, and the application overrides them in its own CSS after the import, as [Using it with your Tailwind CSS](#using-it-with-your-tailwind-css) explains. The light values are the values of the page, `[data-theme=light]` repeats them for a region, and `[data-theme=dark]` holds the dark values. The values themselves are in `src/tokens/semantic.ts`.
|
|
@@ -358,7 +517,7 @@ The components are ordinary Tailwind utilities, so there is one stylesheet and o
|
|
|
358
517
|
What the theme changes in the application:
|
|
359
518
|
|
|
360
519
|
- It adds the semantic colors to the Tailwind theme: `background`, `foreground`, `card`, `popover`, `primary`, `primary-text`, `muted`, `accent`, `destructive`, `success`, `warning`, `info`, `border`, `input`, `ring` and the others listed in `src/tokens/semantic.ts`. The names are common, so an application that already defines `--color-primary` or `--color-muted` gets the value of whichever declaration comes last.
|
|
361
|
-
- It sets `rounded-sm` to 2px and `rounded-
|
|
520
|
+
- It sets `rounded-sm` to 2px, `rounded-md` to 4px and `rounded-lg` to 8px, for the application's own markup as well. The 8px size is for outer containers, while the existing control sizes stay unchanged. Other radii and the whole color palette stay as Tailwind defines them.
|
|
362
521
|
- It sets `--font-sans`. Tailwind's theme points `--default-font-family` at it, so Inter becomes the default font of the whole application, not only of the `font-sans` utility.
|
|
363
522
|
- It defines the `dark` variant, as described in [Dark mode](#dark-mode).
|
|
364
523
|
|
|
@@ -395,3 +554,5 @@ import { Popover, PopoverArrow, PopoverContent, PopoverTrigger } from '@api7/ui-
|
|
|
395
554
|
</PopoverContent>
|
|
396
555
|
</Popover>;
|
|
397
556
|
```
|
|
557
|
+
|
|
558
|
+
See the [DataTable guide](src/components/data-table/README.md) for column sizing, pinned columns, successive table groups, custom column filters, and expanded detail rows.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ComponentProps, MouseEvent, ReactNode } from "react";
|
|
2
|
+
export interface AnchorItem {
|
|
3
|
+
/** A unique fragment link to an element in the same document. */
|
|
4
|
+
href: string;
|
|
5
|
+
title: ReactNode;
|
|
6
|
+
children?: readonly AnchorItem[];
|
|
7
|
+
}
|
|
8
|
+
export type AnchorProps = Omit<ComponentProps<'nav'>, 'children'> & {
|
|
9
|
+
/** Keep the array identity stable when the items have not changed. */
|
|
10
|
+
items: readonly AnchorItem[];
|
|
11
|
+
/** The scrolling element. Defaults to the window. Memoize the callback to avoid rebuilding observers. */
|
|
12
|
+
getContainer?: () => HTMLElement | Window | null;
|
|
13
|
+
/** Keeps the navigation sticky within its containing block. Defaults to true. */
|
|
14
|
+
sticky?: boolean;
|
|
15
|
+
/** Sticky navigation offset in pixels. Defaults to 0. */
|
|
16
|
+
offsetTop?: number;
|
|
17
|
+
/** Scroll destination and active section offset in pixels. Defaults to offsetTop. */
|
|
18
|
+
targetOffset?: number;
|
|
19
|
+
/** Defaults to smooth. Reduced motion always uses instant scrolling. */
|
|
20
|
+
scrollBehavior?: ScrollBehavior;
|
|
21
|
+
/** Adds a history entry on navigation. Defaults to false. */
|
|
22
|
+
updateHash?: boolean;
|
|
23
|
+
onActiveChange?: (href: string) => void;
|
|
24
|
+
/** Prevent the default event to cancel navigation. */
|
|
25
|
+
onNavigate?: (event: MouseEvent<HTMLAnchorElement>, item: AnchorItem) => void;
|
|
26
|
+
};
|
|
27
|
+
declare function Anchor({ items, getContainer, sticky, offsetTop, targetOffset, scrollBehavior, updateHash, onActiveChange, onNavigate, className, style, ...props }: AnchorProps): import("react").JSX.Element;
|
|
28
|
+
export { Anchor };
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/cn.js";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
// src/components/anchor/anchor.tsx
|
|
6
|
+
function flattenItems(items) {
|
|
7
|
+
return items.flatMap((item) => [item, ...flattenItems(item.children ?? [])]);
|
|
8
|
+
}
|
|
9
|
+
function getTarget(href, container) {
|
|
10
|
+
if (!href.startsWith("#") || href.length === 1) return null;
|
|
11
|
+
let id = href.slice(1);
|
|
12
|
+
try {
|
|
13
|
+
id = decodeURIComponent(id);
|
|
14
|
+
} catch {}
|
|
15
|
+
const target = document.getElementById(id);
|
|
16
|
+
if (!target || container instanceof HTMLElement && !container.contains(target)) return null;
|
|
17
|
+
if (!target.getClientRects().length || getComputedStyle(target).visibility === "hidden") return null;
|
|
18
|
+
return target;
|
|
19
|
+
}
|
|
20
|
+
function getScrollPosition(container) {
|
|
21
|
+
if (container instanceof HTMLElement) return {
|
|
22
|
+
top: container.getBoundingClientRect().top + container.clientTop,
|
|
23
|
+
scrollTop: container.scrollTop,
|
|
24
|
+
height: container.clientHeight,
|
|
25
|
+
scrollHeight: container.scrollHeight
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
top: 0,
|
|
29
|
+
scrollTop: window.scrollY,
|
|
30
|
+
height: window.innerHeight,
|
|
31
|
+
scrollHeight: document.documentElement.scrollHeight
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function trackScroll(container, onFinish) {
|
|
35
|
+
let timeout;
|
|
36
|
+
const scrollEndTarget = container instanceof HTMLElement ? container : document;
|
|
37
|
+
const cleanup = () => {
|
|
38
|
+
clearTimeout(timeout);
|
|
39
|
+
container.removeEventListener("scroll", onScroll);
|
|
40
|
+
scrollEndTarget.removeEventListener("scrollend", finish);
|
|
41
|
+
for (const type of [
|
|
42
|
+
"wheel",
|
|
43
|
+
"touchstart",
|
|
44
|
+
"pointerdown",
|
|
45
|
+
"keydown"
|
|
46
|
+
]) container.removeEventListener(type, interrupt);
|
|
47
|
+
};
|
|
48
|
+
const finish = (event) => {
|
|
49
|
+
if (event && event.target !== scrollEndTarget) return;
|
|
50
|
+
cleanup();
|
|
51
|
+
onFinish();
|
|
52
|
+
};
|
|
53
|
+
const onScroll = () => {
|
|
54
|
+
clearTimeout(timeout);
|
|
55
|
+
timeout = setTimeout(finish, 200);
|
|
56
|
+
};
|
|
57
|
+
const interrupt = (event) => {
|
|
58
|
+
if (event instanceof KeyboardEvent && ![
|
|
59
|
+
"ArrowUp",
|
|
60
|
+
"ArrowDown",
|
|
61
|
+
"PageUp",
|
|
62
|
+
"PageDown",
|
|
63
|
+
"Home",
|
|
64
|
+
"End",
|
|
65
|
+
" "
|
|
66
|
+
].includes(event.key)) return;
|
|
67
|
+
container.scrollTo({
|
|
68
|
+
top: getScrollPosition(container).scrollTop,
|
|
69
|
+
behavior: "instant"
|
|
70
|
+
});
|
|
71
|
+
finish();
|
|
72
|
+
};
|
|
73
|
+
container.addEventListener("scroll", onScroll, { passive: true });
|
|
74
|
+
scrollEndTarget.addEventListener("scrollend", finish);
|
|
75
|
+
for (const type of [
|
|
76
|
+
"wheel",
|
|
77
|
+
"touchstart",
|
|
78
|
+
"pointerdown",
|
|
79
|
+
"keydown"
|
|
80
|
+
]) container.addEventListener(type, interrupt, { passive: true });
|
|
81
|
+
onScroll();
|
|
82
|
+
return cleanup;
|
|
83
|
+
}
|
|
84
|
+
function Anchor({ items, getContainer, sticky = true, offsetTop = 0, targetOffset = offsetTop, scrollBehavior = "smooth", updateHash = false, onActiveChange, onNavigate, className, style, ...props }) {
|
|
85
|
+
const links = useMemo(() => flattenItems(items), [items]);
|
|
86
|
+
const [activeHref, setActiveHref] = useState("");
|
|
87
|
+
const activeRef = useRef("");
|
|
88
|
+
const onActiveChangeRef = useRef(onActiveChange);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
onActiveChangeRef.current = onActiveChange;
|
|
91
|
+
}, [onActiveChange]);
|
|
92
|
+
const scheduleUpdateRef = useRef(void 0);
|
|
93
|
+
const scrollingToRef = useRef(null);
|
|
94
|
+
const clearScrollTracking = useCallback(() => {
|
|
95
|
+
scrollingToRef.current?.cleanup();
|
|
96
|
+
scrollingToRef.current = null;
|
|
97
|
+
}, []);
|
|
98
|
+
const restoreFocusTarget = useRef(void 0);
|
|
99
|
+
useEffect(() => () => {
|
|
100
|
+
restoreFocusTarget.current?.();
|
|
101
|
+
clearScrollTracking();
|
|
102
|
+
}, [clearScrollTracking]);
|
|
103
|
+
const changeActive = useCallback((href) => {
|
|
104
|
+
if (href === activeRef.current) return;
|
|
105
|
+
activeRef.current = href;
|
|
106
|
+
setActiveHref(href);
|
|
107
|
+
onActiveChangeRef.current?.(href);
|
|
108
|
+
}, []);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const container = getContainer ? getContainer() : window;
|
|
111
|
+
if (!container) return;
|
|
112
|
+
let frame = 0;
|
|
113
|
+
const update = () => {
|
|
114
|
+
frame = 0;
|
|
115
|
+
const scrollingTo = scrollingToRef.current;
|
|
116
|
+
if (scrollingTo) {
|
|
117
|
+
if (scrollingTo.container === container && links.some((item) => item.href === scrollingTo.href) && getTarget(scrollingTo.href, container)) return;
|
|
118
|
+
clearScrollTracking();
|
|
119
|
+
}
|
|
120
|
+
const position = getScrollPosition(container);
|
|
121
|
+
const targets = links.flatMap((item) => {
|
|
122
|
+
const target = getTarget(item.href, container);
|
|
123
|
+
return target ? [{
|
|
124
|
+
href: item.href,
|
|
125
|
+
top: target.getBoundingClientRect().top
|
|
126
|
+
}] : [];
|
|
127
|
+
});
|
|
128
|
+
targets.sort((a, b) => a.top - b.top);
|
|
129
|
+
let current = targets[0]?.href ?? "";
|
|
130
|
+
for (const target of targets) if (target.top <= position.top + targetOffset + 1) current = target.href;
|
|
131
|
+
if (position.scrollTop > 0 && position.scrollTop + position.height >= position.scrollHeight - 1) current = targets.at(-1)?.href ?? "";
|
|
132
|
+
changeActive(current);
|
|
133
|
+
};
|
|
134
|
+
const scheduleUpdate = () => {
|
|
135
|
+
if (!frame) frame = requestAnimationFrame(update);
|
|
136
|
+
};
|
|
137
|
+
scheduleUpdateRef.current = scheduleUpdate;
|
|
138
|
+
const root = container instanceof HTMLElement ? container : document.body;
|
|
139
|
+
const resizeObserver = new ResizeObserver(scheduleUpdate);
|
|
140
|
+
const observeTargets = () => {
|
|
141
|
+
resizeObserver.disconnect();
|
|
142
|
+
resizeObserver.observe(root);
|
|
143
|
+
for (const item of links) {
|
|
144
|
+
const target = getTarget(item.href, container);
|
|
145
|
+
if (target) resizeObserver.observe(target);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const mutationObserver = new MutationObserver(() => {
|
|
149
|
+
observeTargets();
|
|
150
|
+
scheduleUpdate();
|
|
151
|
+
});
|
|
152
|
+
observeTargets();
|
|
153
|
+
mutationObserver.observe(root, {
|
|
154
|
+
childList: true,
|
|
155
|
+
subtree: true,
|
|
156
|
+
attributes: true,
|
|
157
|
+
attributeFilter: [
|
|
158
|
+
"id",
|
|
159
|
+
"hidden",
|
|
160
|
+
"class",
|
|
161
|
+
"style"
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
container.addEventListener("scroll", scheduleUpdate, { passive: true });
|
|
165
|
+
window.addEventListener("resize", scheduleUpdate);
|
|
166
|
+
scheduleUpdate();
|
|
167
|
+
return () => {
|
|
168
|
+
scheduleUpdateRef.current = void 0;
|
|
169
|
+
cancelAnimationFrame(frame);
|
|
170
|
+
resizeObserver.disconnect();
|
|
171
|
+
mutationObserver.disconnect();
|
|
172
|
+
container.removeEventListener("scroll", scheduleUpdate);
|
|
173
|
+
window.removeEventListener("resize", scheduleUpdate);
|
|
174
|
+
};
|
|
175
|
+
}, [
|
|
176
|
+
links,
|
|
177
|
+
getContainer,
|
|
178
|
+
targetOffset,
|
|
179
|
+
changeActive,
|
|
180
|
+
clearScrollTracking
|
|
181
|
+
]);
|
|
182
|
+
const navigate = (event, item) => {
|
|
183
|
+
onNavigate?.(event, item);
|
|
184
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
185
|
+
if (!item.href.startsWith("#")) return;
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
const container = getContainer ? getContainer() : window;
|
|
188
|
+
if (!container) return;
|
|
189
|
+
const target = getTarget(item.href, container);
|
|
190
|
+
if (!target) return;
|
|
191
|
+
const position = getScrollPosition(container);
|
|
192
|
+
const top = target.getBoundingClientRect().top - position.top + position.scrollTop - targetOffset;
|
|
193
|
+
const behavior = window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "instant" : scrollBehavior;
|
|
194
|
+
clearScrollTracking();
|
|
195
|
+
const destination = Math.max(0, Math.min(top, position.scrollHeight - position.height));
|
|
196
|
+
if (behavior !== "instant" && Math.abs(destination - position.scrollTop) > 1) scrollingToRef.current = {
|
|
197
|
+
container,
|
|
198
|
+
href: item.href,
|
|
199
|
+
cleanup: trackScroll(container, () => {
|
|
200
|
+
scrollingToRef.current = null;
|
|
201
|
+
scheduleUpdateRef.current?.();
|
|
202
|
+
})
|
|
203
|
+
};
|
|
204
|
+
restoreFocusTarget.current?.();
|
|
205
|
+
if (!target.hasAttribute("tabindex")) {
|
|
206
|
+
target.setAttribute("tabindex", "-1");
|
|
207
|
+
const restore = () => {
|
|
208
|
+
if (target.getAttribute("tabindex") === "-1") target.removeAttribute("tabindex");
|
|
209
|
+
target.removeEventListener("blur", restore);
|
|
210
|
+
restoreFocusTarget.current = void 0;
|
|
211
|
+
};
|
|
212
|
+
restoreFocusTarget.current = restore;
|
|
213
|
+
target.addEventListener("blur", restore, { once: true });
|
|
214
|
+
}
|
|
215
|
+
target.focus({ preventScroll: true });
|
|
216
|
+
container.scrollTo({
|
|
217
|
+
top,
|
|
218
|
+
behavior
|
|
219
|
+
});
|
|
220
|
+
changeActive(item.href);
|
|
221
|
+
scheduleUpdateRef.current?.();
|
|
222
|
+
if (updateHash && window.location.hash !== item.href) window.history.pushState(window.history.state, "", item.href);
|
|
223
|
+
};
|
|
224
|
+
const renderItems = (entries, depth = 0) => /* @__PURE__ */ jsx("ul", {
|
|
225
|
+
className: "m-0 list-none p-0",
|
|
226
|
+
"data-slot": "anchor-list",
|
|
227
|
+
children: entries.map((item) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("a", {
|
|
228
|
+
href: item.href,
|
|
229
|
+
"aria-current": activeHref === item.href ? "location" : void 0,
|
|
230
|
+
"data-active": activeHref === item.href ? "" : void 0,
|
|
231
|
+
"data-slot": "anchor-link",
|
|
232
|
+
className: cn("block rounded-sm pr-3 text-sm leading-[1.8] break-words text-muted-foreground no-underline outline-none hover:text-primary-text focus-visible:ring-2 focus-visible:ring-ring/40 data-active:font-semibold data-active:text-primary-text", depth === 0 ? "py-1" : "py-0.5"),
|
|
233
|
+
style: { paddingLeft: 16 + depth * 16 },
|
|
234
|
+
onClick: (event) => navigate(event, item),
|
|
235
|
+
children: item.title
|
|
236
|
+
}), item.children?.length ? renderItems(item.children, depth + 1) : null] }, item.href))
|
|
237
|
+
});
|
|
238
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
239
|
+
className: cn("relative min-w-0", sticky && "sticky self-start", className),
|
|
240
|
+
style: {
|
|
241
|
+
...sticky ? { top: offsetTop } : {},
|
|
242
|
+
...style
|
|
243
|
+
},
|
|
244
|
+
...props,
|
|
245
|
+
"data-slot": "anchor",
|
|
246
|
+
children: renderItems(items)
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
export { Anchor };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ComponentProps } from "react";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
export type BreadcrumbProps = ComponentProps<'nav'>;
|
|
4
|
+
declare function Breadcrumb({ className, ...props }: BreadcrumbProps): import("react").JSX.Element;
|
|
5
|
+
export type BreadcrumbListProps = ComponentProps<'ol'>;
|
|
6
|
+
declare function BreadcrumbList({ className, ...props }: BreadcrumbListProps): import("react").JSX.Element;
|
|
7
|
+
export type BreadcrumbItemProps = ComponentProps<'li'>;
|
|
8
|
+
declare function BreadcrumbItem({ className, ...props }: BreadcrumbItemProps): import("react").JSX.Element;
|
|
9
|
+
export type BreadcrumbLinkProps = useRender.ComponentProps<'a'>;
|
|
10
|
+
declare function BreadcrumbLink({ render, className, ...props }: BreadcrumbLinkProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
11
|
+
export type BreadcrumbPageProps = ComponentProps<'span'>;
|
|
12
|
+
declare function BreadcrumbPage({ className, ...props }: BreadcrumbPageProps): import("react").JSX.Element;
|
|
13
|
+
export type BreadcrumbSeparatorProps = ComponentProps<'li'>;
|
|
14
|
+
declare function BreadcrumbSeparator({ className, children, ...props }: BreadcrumbSeparatorProps): import("react").JSX.Element;
|
|
15
|
+
export { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useMessages } from "../../config/config-provider.js";
|
|
3
|
+
import { cn } from "../../lib/cn.js";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { ChevronRight } from "lucide-react";
|
|
6
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
7
|
+
// src/components/breadcrumb/breadcrumb.tsx
|
|
8
|
+
function Breadcrumb({ className, ...props }) {
|
|
9
|
+
const messages = useMessages();
|
|
10
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
11
|
+
"aria-label": messages.breadcrumb.label,
|
|
12
|
+
className: cn("text-sm", className),
|
|
13
|
+
...props,
|
|
14
|
+
"data-slot": "breadcrumb"
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function BreadcrumbList({ className, ...props }) {
|
|
18
|
+
return /* @__PURE__ */ jsx("ol", {
|
|
19
|
+
className: cn("flex flex-wrap items-center gap-2 text-muted-foreground", className),
|
|
20
|
+
...props,
|
|
21
|
+
"data-slot": "breadcrumb-list"
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function BreadcrumbItem({ className, ...props }) {
|
|
25
|
+
return /* @__PURE__ */ jsx("li", {
|
|
26
|
+
className: cn("inline-flex min-w-0 max-w-full items-center gap-2", className),
|
|
27
|
+
...props,
|
|
28
|
+
"data-slot": "breadcrumb-item"
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function BreadcrumbLink({ render, className, ...props }) {
|
|
32
|
+
return useRender({
|
|
33
|
+
defaultTagName: "a",
|
|
34
|
+
render,
|
|
35
|
+
props: {
|
|
36
|
+
...props,
|
|
37
|
+
className: cn("min-w-0 rounded-sm wrap-anywhere outline-none hover:text-foreground hover:underline focus-visible:ring-2 focus-visible:ring-ring/40", className),
|
|
38
|
+
"data-slot": "breadcrumb-link"
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function BreadcrumbPage({ className, ...props }) {
|
|
43
|
+
return /* @__PURE__ */ jsx("span", {
|
|
44
|
+
"aria-current": "page",
|
|
45
|
+
className: cn("min-w-0 text-foreground wrap-anywhere", className),
|
|
46
|
+
...props,
|
|
47
|
+
"data-slot": "breadcrumb-page"
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function BreadcrumbSeparator({ className, children, ...props }) {
|
|
51
|
+
return /* @__PURE__ */ jsx("li", {
|
|
52
|
+
className: cn("inline-flex shrink-0 items-center [&>svg]:size-4", className),
|
|
53
|
+
...props,
|
|
54
|
+
role: "presentation",
|
|
55
|
+
"aria-hidden": "true",
|
|
56
|
+
"data-slot": "breadcrumb-separator",
|
|
57
|
+
children: children ?? /* @__PURE__ */ jsx(ChevronRight, {})
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { VariantProps } from "class-variance-authority";
|
|
2
1
|
import { Button } from "@base-ui/react/button";
|
|
2
|
+
import { VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const buttonVariants: (props?: ({
|
|
4
4
|
variant?: "primary" | "secondary" | "destructive" | "link" | "outline" | "ghost" | null | undefined;
|
|
5
5
|
size?: "sm" | "md" | "lg" | "icon" | null | undefined;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { useMessages } from "../../config/config-provider.js";
|
|
3
3
|
import { cn } from "../../lib/cn.js";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { Button } from "@base-ui/react/button";
|
|
5
6
|
import { cva } from "class-variance-authority";
|
|
6
7
|
import { LoaderCircle } from "lucide-react";
|
|
7
|
-
import { Button } from "@base-ui/react/button";
|
|
8
8
|
// src/components/button/button.tsx
|
|
9
9
|
const buttonVariants = cva("inline-flex shrink-0 items-center justify-center gap-2 rounded-md border border-transparent text-sm font-medium whitespace-nowrap transition-colors outline-none select-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", {
|
|
10
10
|
variants: {
|