@gusnips/react 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/LICENSE +21 -0
- package/README.md +180 -0
- package/dist/api-client.d.ts +139 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +133 -0
- package/dist/api-client.js.map +1 -0
- package/dist/api-error.d.ts +45 -0
- package/dist/api-error.d.ts.map +1 -0
- package/dist/api-error.js +52 -0
- package/dist/api-error.js.map +1 -0
- package/dist/auth-store.d.ts +49 -0
- package/dist/auth-store.d.ts.map +1 -0
- package/dist/auth-store.js +27 -0
- package/dist/auth-store.js.map +1 -0
- package/dist/chunk-reload.d.ts +61 -0
- package/dist/chunk-reload.d.ts.map +1 -0
- package/dist/chunk-reload.js +92 -0
- package/dist/chunk-reload.js.map +1 -0
- package/dist/cn.d.ts +13 -0
- package/dist/cn.d.ts.map +1 -0
- package/dist/cn.js +16 -0
- package/dist/cn.js.map +1 -0
- package/dist/describe-error.d.ts +74 -0
- package/dist/describe-error.d.ts.map +1 -0
- package/dist/describe-error.js +70 -0
- package/dist/describe-error.js.map +1 -0
- package/dist/error-boundary.d.ts +61 -0
- package/dist/error-boundary.d.ts.map +1 -0
- package/dist/error-boundary.js +32 -0
- package/dist/error-boundary.js.map +1 -0
- package/dist/guards.d.ts +65 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +52 -0
- package/dist/guards.js.map +1 -0
- package/dist/hydrate.d.ts +30 -0
- package/dist/hydrate.d.ts.map +1 -0
- package/dist/hydrate.js +25 -0
- package/dist/hydrate.js.map +1 -0
- package/dist/i18n.d.ts +70 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +66 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/prerender-contract.d.ts +30 -0
- package/dist/prerender-contract.d.ts.map +1 -0
- package/dist/prerender-contract.js +30 -0
- package/dist/prerender-contract.js.map +1 -0
- package/dist/query.d.ts +28 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +51 -0
- package/dist/query.js.map +1 -0
- package/dist/sse.d.ts +64 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +105 -0
- package/dist/sse.js.map +1 -0
- package/dist/states.d.ts +69 -0
- package/dist/states.d.ts.map +1 -0
- package/dist/states.js +2 -0
- package/dist/states.js.map +1 -0
- package/dist/ui/class-name.d.ts +16 -0
- package/dist/ui/class-name.d.ts.map +1 -0
- package/dist/ui/class-name.js +13 -0
- package/dist/ui/class-name.js.map +1 -0
- package/dist/ui/combobox.d.ts +81 -0
- package/dist/ui/combobox.d.ts.map +1 -0
- package/dist/ui/combobox.js +70 -0
- package/dist/ui/combobox.js.map +1 -0
- package/dist/ui/dialog.d.ts +39 -0
- package/dist/ui/dialog.d.ts.map +1 -0
- package/dist/ui/dialog.js +32 -0
- package/dist/ui/dialog.js.map +1 -0
- package/dist/ui/drawer.d.ts +63 -0
- package/dist/ui/drawer.d.ts.map +1 -0
- package/dist/ui/drawer.js +55 -0
- package/dist/ui/drawer.js.map +1 -0
- package/dist/ui/glyphs.d.ts +4 -0
- package/dist/ui/glyphs.d.ts.map +1 -0
- package/dist/ui/glyphs.js +20 -0
- package/dist/ui/glyphs.js.map +1 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +48 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/input.d.ts +45 -0
- package/dist/ui/input.d.ts.map +1 -0
- package/dist/ui/input.js +108 -0
- package/dist/ui/input.js.map +1 -0
- package/dist/ui/menu.d.ts +70 -0
- package/dist/ui/menu.d.ts.map +1 -0
- package/dist/ui/menu.js +85 -0
- package/dist/ui/menu.js.map +1 -0
- package/dist/ui/select.d.ts +69 -0
- package/dist/ui/select.d.ts.map +1 -0
- package/dist/ui/select.js +48 -0
- package/dist/ui/select.js.map +1 -0
- package/dist/ui/tabs.d.ts +52 -0
- package/dist/ui/tabs.d.ts.map +1 -0
- package/dist/ui/tabs.js +62 -0
- package/dist/ui/tabs.js.map +1 -0
- package/package.json +124 -0
- package/src/api-client.test.ts +256 -0
- package/src/api-client.ts +305 -0
- package/src/api-error.ts +60 -0
- package/src/auth-store.ts +59 -0
- package/src/chunk-reload.test.ts +46 -0
- package/src/chunk-reload.ts +94 -0
- package/src/cn.ts +16 -0
- package/src/describe-error.test.ts +102 -0
- package/src/describe-error.ts +140 -0
- package/src/error-boundary.tsx +88 -0
- package/src/guards.tsx +99 -0
- package/src/hydrate.ts +36 -0
- package/src/i18n.test.ts +68 -0
- package/src/i18n.ts +101 -0
- package/src/index.ts +63 -0
- package/src/prerender-contract.ts +31 -0
- package/src/query.test.ts +54 -0
- package/src/query.ts +79 -0
- package/src/sse.test.ts +50 -0
- package/src/sse.ts +117 -0
- package/src/states.ts +71 -0
- package/src/ui/class-name.ts +23 -0
- package/src/ui/combobox.test.ts +32 -0
- package/src/ui/combobox.tsx +242 -0
- package/src/ui/dialog.tsx +87 -0
- package/src/ui/drawer.tsx +161 -0
- package/src/ui/focus.test.ts +82 -0
- package/src/ui/glyphs.tsx +35 -0
- package/src/ui/index.ts +145 -0
- package/src/ui/input.test.ts +22 -0
- package/src/ui/input.tsx +181 -0
- package/src/ui/menu.test.ts +73 -0
- package/src/ui/menu.tsx +186 -0
- package/src/ui/select.tsx +191 -0
- package/src/ui/tabs.tsx +95 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { Select as Primitive } from "@base-ui/react/select";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { mergeClassName, type StateClassName } from "./class-name.ts";
|
|
4
|
+
import { CheckGlyph, ChevronGlyph } from "./glyphs.tsx";
|
|
5
|
+
|
|
6
|
+
// The parts, for a select this wrapper's option list cannot express — groups, separators,
|
|
7
|
+
// a scrollable list with arrows.
|
|
8
|
+
export const SelectRoot = Primitive.Root;
|
|
9
|
+
export const SelectTrigger = Primitive.Trigger;
|
|
10
|
+
export const SelectValue = Primitive.Value;
|
|
11
|
+
export const SelectIcon = Primitive.Icon;
|
|
12
|
+
export const SelectPortal = Primitive.Portal;
|
|
13
|
+
export const SelectPositioner = Primitive.Positioner;
|
|
14
|
+
export const SelectPopup = Primitive.Popup;
|
|
15
|
+
export const SelectList = Primitive.List;
|
|
16
|
+
export const SelectItem = Primitive.Item;
|
|
17
|
+
export const SelectItemIndicator = Primitive.ItemIndicator;
|
|
18
|
+
export const SelectItemText = Primitive.ItemText;
|
|
19
|
+
export const SelectGroup = Primitive.Group;
|
|
20
|
+
export const SelectGroupLabel = Primitive.GroupLabel;
|
|
21
|
+
export const SelectSeparator = Primitive.Separator;
|
|
22
|
+
|
|
23
|
+
export interface SelectOption<T extends string = string> {
|
|
24
|
+
value: T;
|
|
25
|
+
label: string;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
icon?: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SelectProps<T extends string = string> = Omit<
|
|
31
|
+
Primitive.Root.Props<T, false>,
|
|
32
|
+
"children" | "items" | "defaultValue" | "multiple"
|
|
33
|
+
> & {
|
|
34
|
+
options?: readonly SelectOption<T>[];
|
|
35
|
+
/** Shown while nothing is selected. No default — a default would be English. */
|
|
36
|
+
placeholder?: ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* Shown instead of the list when `options` is empty. Required, because a popup that opens
|
|
39
|
+
* on nothing is a dead end: this is the line that says why it is empty.
|
|
40
|
+
*/
|
|
41
|
+
emptyLabel: ReactNode;
|
|
42
|
+
/**
|
|
43
|
+
* Label of an entry that puts the field back to nothing selected — for an optional
|
|
44
|
+
* question, where "I'd rather not say" has to stay reachable after the first answer.
|
|
45
|
+
* Rendered only once there is a value to take back; picking it calls `onValueChange(null)`.
|
|
46
|
+
*/
|
|
47
|
+
clearLabel?: string;
|
|
48
|
+
/** Accessible name for the trigger, when no visible label points at it. */
|
|
49
|
+
label?: string;
|
|
50
|
+
/** Mark that says "this opens". */
|
|
51
|
+
chevron?: ReactNode;
|
|
52
|
+
/** Mark next to the selected option. */
|
|
53
|
+
indicator?: ReactNode;
|
|
54
|
+
/** Classes for the trigger — the button you see when the popup is closed. */
|
|
55
|
+
className?: StateClassName<Primitive.Trigger.State>;
|
|
56
|
+
popupClassName?: StateClassName<Primitive.Popup.State>;
|
|
57
|
+
triggerProps?: Primitive.Trigger.Props;
|
|
58
|
+
/**
|
|
59
|
+
* Positioner props — `align`, `side`, `sideOffset`, `collisionPadding`, and the stacking
|
|
60
|
+
* order. The default `z-[60]` puts the list above the overlay layer (z-50: dialog, drawer);
|
|
61
|
+
* raise it with `positionerProps={{ className: "z-[80]" }}` if the app stacks something
|
|
62
|
+
* higher than a dialog above it.
|
|
63
|
+
*/
|
|
64
|
+
positionerProps?: Primitive.Positioner.Props;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A single-choice field. Trigger, portal, positioner, popup and list composed once; `name`,
|
|
69
|
+
* `form`, `required`, `id`, `disabled` and every other root prop pass straight through to
|
|
70
|
+
* Base UI's hidden input, so this submits inside a `<form>` like a native `<select>`.
|
|
71
|
+
*
|
|
72
|
+
* Controlled only, and that is the point of `value ?? null`: React and Base UI both read
|
|
73
|
+
* `value={undefined}` as "uncontrolled", so a caller holding `T | undefined` in state would
|
|
74
|
+
* silently hand the field back to the primitive the moment the value cleared, and every
|
|
75
|
+
* later render would be ignored. `null` says "nothing selected" out loud. `defaultValue` is
|
|
76
|
+
* not accepted here for the same reason — use the parts for an uncontrolled select.
|
|
77
|
+
*/
|
|
78
|
+
export function Select<T extends string = string>({
|
|
79
|
+
value,
|
|
80
|
+
options = [],
|
|
81
|
+
placeholder,
|
|
82
|
+
emptyLabel,
|
|
83
|
+
clearLabel,
|
|
84
|
+
label,
|
|
85
|
+
chevron,
|
|
86
|
+
indicator,
|
|
87
|
+
className,
|
|
88
|
+
popupClassName,
|
|
89
|
+
triggerProps,
|
|
90
|
+
positionerProps,
|
|
91
|
+
...rest
|
|
92
|
+
}: SelectProps<T>) {
|
|
93
|
+
const selected = options.find((option) => option.value === value);
|
|
94
|
+
// `undefined` when nothing is selected, and it has to be exactly that. Base UI's Value part
|
|
95
|
+
// falls back to `placeholder` only when it is handed NO children at all — an empty fragment
|
|
96
|
+
// still counts as children, which is how the donor ended up with a select that showed a
|
|
97
|
+
// blank trigger instead of "Choose one".
|
|
98
|
+
const selectedLabel = selected ? (
|
|
99
|
+
<>
|
|
100
|
+
{selected.icon ? (
|
|
101
|
+
<span className="mr-2 inline-flex align-middle">{selected.icon}</span>
|
|
102
|
+
) : null}
|
|
103
|
+
{selected.label}
|
|
104
|
+
</>
|
|
105
|
+
) : undefined;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Primitive.Root<T, false> value={value ?? null} items={options} {...rest}>
|
|
109
|
+
<Primitive.Trigger
|
|
110
|
+
aria-label={label}
|
|
111
|
+
{...triggerProps}
|
|
112
|
+
className={mergeClassName(
|
|
113
|
+
// No focus style: the trigger keeps the app's one `:focus-visible` outline. Swapping
|
|
114
|
+
// it for a `ring-*` loses forced-colors mode, where a box-shadow is forced to none.
|
|
115
|
+
"flex w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-left text-foreground disabled:pointer-events-none disabled:opacity-50",
|
|
116
|
+
className,
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
<Primitive.Value
|
|
120
|
+
placeholder={placeholder}
|
|
121
|
+
className="min-w-0 flex-1 truncate data-placeholder:text-muted-foreground"
|
|
122
|
+
>
|
|
123
|
+
{selectedLabel}
|
|
124
|
+
</Primitive.Value>
|
|
125
|
+
<Primitive.Icon className="shrink-0 text-muted-foreground">
|
|
126
|
+
{chevron ?? <ChevronGlyph />}
|
|
127
|
+
</Primitive.Icon>
|
|
128
|
+
</Primitive.Trigger>
|
|
129
|
+
<Primitive.Portal>
|
|
130
|
+
{/* Above the overlay layer (z-50: dialog, drawer). A select opened inside a dialog
|
|
131
|
+
portals to the body just like the dialog does, so nesting decides nothing and z
|
|
132
|
+
decides everything — one step lower and the list paints behind the panel that
|
|
133
|
+
anchors it. */}
|
|
134
|
+
<Primitive.Positioner
|
|
135
|
+
sideOffset={4}
|
|
136
|
+
{...positionerProps}
|
|
137
|
+
className={mergeClassName(
|
|
138
|
+
"z-[60] max-w-[var(--available-width)] outline-none",
|
|
139
|
+
positionerProps?.className,
|
|
140
|
+
)}
|
|
141
|
+
>
|
|
142
|
+
<Primitive.Popup
|
|
143
|
+
className={mergeClassName(
|
|
144
|
+
"min-w-[var(--anchor-width)] origin-[var(--transform-origin)] overflow-hidden border border-border bg-popover text-popover-foreground outline-none transition-[scale,opacity] data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0",
|
|
145
|
+
popupClassName,
|
|
146
|
+
)}
|
|
147
|
+
>
|
|
148
|
+
<Primitive.List className="max-h-[var(--available-height)] overflow-y-auto">
|
|
149
|
+
{clearLabel !== undefined && value != null ? (
|
|
150
|
+
// An Item with `value={null}`, not a button above the list: only an item is
|
|
151
|
+
// reachable by arrow keys and typeahead, and only an item closes the popup
|
|
152
|
+
// on its own. `null` is what arrives at `onValueChange`.
|
|
153
|
+
<Primitive.Item
|
|
154
|
+
value={null}
|
|
155
|
+
className="flex cursor-default items-center gap-2 px-3 py-2 text-muted-foreground outline-none data-highlighted:bg-accent data-highlighted:text-accent-foreground"
|
|
156
|
+
>
|
|
157
|
+
<span className="size-4 shrink-0" aria-hidden="true" />
|
|
158
|
+
<Primitive.ItemText>{clearLabel}</Primitive.ItemText>
|
|
159
|
+
</Primitive.Item>
|
|
160
|
+
) : null}
|
|
161
|
+
{options.length === 0 ? (
|
|
162
|
+
<div className="px-3 py-2 text-muted-foreground">{emptyLabel}</div>
|
|
163
|
+
) : (
|
|
164
|
+
options.map((option) => (
|
|
165
|
+
<Primitive.Item
|
|
166
|
+
key={option.value}
|
|
167
|
+
value={option.value}
|
|
168
|
+
disabled={option.disabled}
|
|
169
|
+
className="flex cursor-default items-center gap-2 px-3 py-2 outline-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-accent data-highlighted:text-accent-foreground"
|
|
170
|
+
>
|
|
171
|
+
{/* The mark keeps a column of its own whether or not it is showing, so
|
|
172
|
+
the labels do not shift by 16px when the selection moves. */}
|
|
173
|
+
<span className="size-4 shrink-0">
|
|
174
|
+
<Primitive.ItemIndicator>
|
|
175
|
+
{indicator ?? <CheckGlyph />}
|
|
176
|
+
</Primitive.ItemIndicator>
|
|
177
|
+
</span>
|
|
178
|
+
{option.icon ? (
|
|
179
|
+
<span className="inline-flex shrink-0">{option.icon}</span>
|
|
180
|
+
) : null}
|
|
181
|
+
<Primitive.ItemText>{option.label}</Primitive.ItemText>
|
|
182
|
+
</Primitive.Item>
|
|
183
|
+
))
|
|
184
|
+
)}
|
|
185
|
+
</Primitive.List>
|
|
186
|
+
</Primitive.Popup>
|
|
187
|
+
</Primitive.Positioner>
|
|
188
|
+
</Primitive.Portal>
|
|
189
|
+
</Primitive.Root>
|
|
190
|
+
);
|
|
191
|
+
}
|
package/src/ui/tabs.tsx
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Tabs as Primitive } from "@base-ui/react/tabs";
|
|
2
|
+
import { cn } from "../cn.ts";
|
|
3
|
+
import { mergeClassName } from "./class-name.ts";
|
|
4
|
+
|
|
5
|
+
// Root and Panel pass straight through: a wrapper around either would add a class string and
|
|
6
|
+
// nothing else, and Base UI already owns arrow-key navigation, activation mode and the
|
|
7
|
+
// tab/panel `aria-controls` wiring.
|
|
8
|
+
export const Tabs = Primitive.Root;
|
|
9
|
+
export const TabPanel = Primitive.Panel;
|
|
10
|
+
|
|
11
|
+
type TabListLabel =
|
|
12
|
+
| { label: string; "aria-labelledby"?: undefined }
|
|
13
|
+
| { label?: undefined; "aria-labelledby": string };
|
|
14
|
+
|
|
15
|
+
export type TabListProps = Omit<Primitive.List.Props, "aria-labelledby"> &
|
|
16
|
+
TabListLabel & {
|
|
17
|
+
/** Classes for the scroll wrapper — the element that owns the sideways scroll. */
|
|
18
|
+
wrapperClassName?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The rail, and the reason this file exists.
|
|
23
|
+
*
|
|
24
|
+
* It scrolls sideways rather than wrapping, because a second row of tabs pushes the panel
|
|
25
|
+
* below the fold on a phone. But the scroll cannot live on the rail itself: `overflow-x: auto`
|
|
26
|
+
* makes the element a scroll container, a scroll container clips everything its children paint
|
|
27
|
+
* outside the padding box, and a computed `overflow-x` of `auto` forces `overflow-y` from
|
|
28
|
+
* `visible` to `auto` as well. A self-scrolling rail therefore crops the focus ring on all
|
|
29
|
+
* FOUR sides of whichever tab the keyboard is on — top and bottom always, the left of the
|
|
30
|
+
* first tab and the right of the last.
|
|
31
|
+
*
|
|
32
|
+
* So the scroll goes on a wrapper, and the wrapper's padding is the room the ring needs. The
|
|
33
|
+
* number is derived, not inherited: `@gusnips/tokens` draws `outline: 2px solid` at
|
|
34
|
+
* `outline-offset: 2px`, so the ring paints from 2px to 4px beyond a tab's border box, and
|
|
35
|
+
* `p-1` is exactly those 4px. An outline reaches FURTHER than the `ring-2` box-shadow the
|
|
36
|
+
* donor had here, so the wrapper matters more now than it did there, not less.
|
|
37
|
+
*
|
|
38
|
+
* Naming the rail is not optional and not a comment: pass `label`, or point
|
|
39
|
+
* `aria-labelledby` at a heading. A tablist with neither is announced as nothing at all.
|
|
40
|
+
*/
|
|
41
|
+
export function TabList({ label, className, wrapperClassName, children, ...rest }: TabListProps) {
|
|
42
|
+
return (
|
|
43
|
+
<div className={cn("w-fit max-w-full overflow-x-auto p-1", wrapperClassName)}>
|
|
44
|
+
<Primitive.List
|
|
45
|
+
aria-label={label}
|
|
46
|
+
{...rest}
|
|
47
|
+
// `relative`, because TabIndicator positions itself against the rail.
|
|
48
|
+
className={mergeClassName("relative flex w-fit", className)}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</Primitive.List>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Styled off `aria-selected`, never a parallel `data-active`: the primitive writes both from
|
|
57
|
+
// one state, and styling the accessibility contract is what keeps what a screen reader
|
|
58
|
+
// announces and what an eye sees from ever drifting apart.
|
|
59
|
+
//
|
|
60
|
+
// `shrink-0` and `whitespace-nowrap` are what make the rail scroll instead of squashing — a
|
|
61
|
+
// tab that shrinks to fit is a tab nobody can read, and the scroll above exists precisely so
|
|
62
|
+
// it does not have to.
|
|
63
|
+
const tabClasses =
|
|
64
|
+
"shrink-0 cursor-pointer px-3 py-1.5 whitespace-nowrap text-muted-foreground select-none aria-selected:text-foreground [@media(pointer:coarse)]:min-h-11";
|
|
65
|
+
|
|
66
|
+
export function Tab({ className, ...rest }: Primitive.Tab.Props) {
|
|
67
|
+
return <Primitive.Tab {...rest} className={mergeClassName(tabClasses, className)} />;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The travelling marker under the active tab — a SEPARATE part, because the two donors
|
|
72
|
+
* disagreed about whether to have one at all and both were right about their own app. Render
|
|
73
|
+
* it or do not; the rail works either way.
|
|
74
|
+
*
|
|
75
|
+
* It must be a child of `TabList`: Base UI measures the active tab against the enclosing
|
|
76
|
+
* `[role="tablist"]` and writes `--active-tab-left` / `--active-tab-width` onto this element.
|
|
77
|
+
*
|
|
78
|
+
* `renderBeforeHydration` is on by default because this package's apps are prerendered — with
|
|
79
|
+
* it off, every prerendered page shows the rail with no marker until React arrives. It writes
|
|
80
|
+
* one inline script to do that; under a strict CSP without Base UI's `CspProvider` the script
|
|
81
|
+
* is blocked and the behaviour falls back to exactly what `false` would have done.
|
|
82
|
+
*/
|
|
83
|
+
export function TabIndicator({ className, ...rest }: Primitive.Indicator.Props) {
|
|
84
|
+
return (
|
|
85
|
+
<Primitive.Indicator
|
|
86
|
+
aria-hidden="true"
|
|
87
|
+
renderBeforeHydration
|
|
88
|
+
{...rest}
|
|
89
|
+
className={mergeClassName(
|
|
90
|
+
"absolute bottom-0 left-0 h-0.5 w-[var(--active-tab-width)] translate-x-[var(--active-tab-left)] bg-foreground transition-[translate,width]",
|
|
91
|
+
className,
|
|
92
|
+
)}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}
|