@exsto/ds 0.0.1-rc.21 → 0.0.1-rc.23
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 +12 -0
- package/dist/source-hash.txt +1 -1
- package/dist/styles.css +20 -5
- package/dist/theme.css +3 -0
- package/dist/ui/src/components/ui/button/button.js +3 -28
- package/dist/ui/src/components/ui/button/button.js.map +1 -1
- package/dist/ui/src/components/ui/combobox/combobox.js +1 -1
- package/dist/ui/src/components/ui/combobox/combobox.js.map +1 -1
- package/dist/ui/src/components/ui/empty/empty.d.ts +5 -1
- package/dist/ui/src/components/ui/empty/empty.js +13 -2
- package/dist/ui/src/components/ui/empty/empty.js.map +1 -1
- package/dist/ui/src/components/ui/scroll-area/scroll-area.js +4 -34
- package/dist/ui/src/components/ui/scroll-area/scroll-area.js.map +1 -1
- package/dist/ui/src/components/ui/tabs/tabs.d.ts +7 -2
- package/dist/ui/src/components/ui/tabs/tabs.js +97 -16
- package/dist/ui/src/components/ui/tabs/tabs.js.map +1 -1
- package/dist/ui/src/hooks/use-holds-focusable.d.ts +3 -0
- package/dist/ui/src/hooks/use-holds-focusable.js +33 -0
- package/dist/ui/src/hooks/use-holds-focusable.js.map +1 -0
- package/dist/ui/src/lib/focus-guards.js +2 -10
- package/dist/ui/src/lib/focus-guards.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -190,6 +190,18 @@ import { cn, POPUP_FRAME_CLASS, POPUP_LIST_HEIGHT_CLASS, POPUP_LIST_WIDTH_CLASS,
|
|
|
190
190
|
- `POPUP_GROUP_LABEL_CLASS` is a group's heading. `ComboboxGroup`, `ComboboxLabel`, `ComboboxCollection` and `ComboboxSeparator` give a Combobox its groups, drawn the same way Select draws its own.
|
|
191
191
|
- `POPUP_FRAME_CLASS` is the frame (a 10px radius and a 1px ring, no border). `POPUP_LIST_WIDTH_CLASS` is its width: at least the control and 8rem, as wide as the longest row, 28rem at most. `POPUP_LIST_HEIGHT_CLASS` caps the scrolling part: eight rows, or as many whole rows as the room allows, so a list never ends on half a row. Put it on a `ScrollArea` inside a popup that is a flex column with `max-h-(--available-height)`; the class carries the 4px of air above and below the rows as a margin, so the list inside takes `px-1` and no vertical padding (padding there scrolls with the rows and leaves a strip of the next row at the foot). `POPUP_LAYER_CLASS` (`z-60`) goes on the popup's positioner, so it is drawn above the Sheet, Dialog or Drawer (`z-50`) it is opened from.
|
|
192
192
|
|
|
193
|
+
## Changes in 0.0.1-rc.23
|
|
194
|
+
|
|
195
|
+
- **A TabsList keeps the chosen tab clear of its edge fade when the row changes size after load** (a font swapping in, data arriving). The scroll that reveals the chosen tab now repeats on a change of size until the reader scrolls, presses a key or touches; before, a chosen last tab could end 2px inside the edge with the fade covering most of it.
|
|
196
|
+
|
|
197
|
+
## Changes in 0.0.1-rc.22
|
|
198
|
+
|
|
199
|
+
- **Muted text on highlighted rows meets 4.5:1 (new token); apps import components.css and drop local workarounds.** The new `--muted-foreground-highlighted` (`oklch(0.52 0 0)` light; dark unchanged at `0.708`) is what `--muted-foreground` becomes inside a `[data-highlighted]` row or a selected Command item, by one rule in `components.css` (it ships in `@exsto/ds/styles.css`). A detail, a shortcut or an icon in a Select, Combobox, Command or menu row was 4.34:1 on the light row; any local `text-foreground/70` for it can go.
|
|
200
|
+
- **A TabsList wider than its box scrolls sideways inside it**, with a 24px fade at each edge that hides tabs. The selected tab and the tab holding focus are scrolled into view (the viewport only, never the page), and arrow keys, Home, End, the wheel and a click all work. A vertical list is unchanged, and an app that worked around 8 tabs running past the window can drop the workaround. Three things change: **a row that overflows is 8px taller** (the scrollbar keeps its room), **a tab's focus ring is drawn inside the tab** (the scroll area would clip one outside it), and **a TabsList given `self-center` in a flex parent no longer centres** (its parent is now the ScrollArea): use `mx-auto`.
|
|
201
|
+
- **The Combobox chevron ("Show options") is a tab stop**: it is a named button, and a named control should be reachable by keyboard, so Tab from the input lands on it and Enter or Space opens the list. It is one more Tab stop per Combobox; it was `tabindex="-1"`.
|
|
202
|
+
- **A tab panel is a tab stop only when nothing inside it can take Tab**, and then shows a 1px line inside its edge for the keyboard only; it used to draw a 2px outline round the whole panel. `TabsContent` carries `data-slot="tabs-content"`, and a `tabIndex` you pass still wins.
|
|
203
|
+
- **Empty is solid by default, with a `dashed` variant** (`variant="solid" | "dashed"`). It sets only the line style: Empty still draws no border until you add `border`, and `dashed` adds no width. Drop any `border-solid` override.
|
|
204
|
+
|
|
193
205
|
## Changes in 0.0.1-rc.21
|
|
194
206
|
|
|
195
207
|
- **Migration: check `state.kind` where you store the day a DatePicker reports.** A typed day outside `min`, `max` or `disabledDates` used to arrive as `onValueChange(undefined, { kind: 'refused', ... })`; it now arrives as `onValueChange(thatDay, { kind: 'out-of-range', day, text, problem })`. `day ?? ''` now stores the out-of-range day, which is what lets your form's rule see it and show its message; guard with `state.kind === 'day'` where an out-of-range day must not be kept. Text that is not a day is still `refused` with `undefined`.
|
package/dist/source-hash.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
237f9a6de1d109e6a11bc94cd65028316f93bb0be9dac50c29fd7b50604c39e5
|
package/dist/styles.css
CHANGED
|
@@ -1356,10 +1356,6 @@
|
|
|
1356
1356
|
flex: 1;
|
|
1357
1357
|
}
|
|
1358
1358
|
|
|
1359
|
-
.shrink {
|
|
1360
|
-
flex-shrink: 1;
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
1359
|
.shrink-0 {
|
|
1364
1360
|
flex-shrink: 0;
|
|
1365
1361
|
}
|
|
@@ -1745,6 +1741,10 @@
|
|
|
1745
1741
|
margin-inline-end: calc(calc(var(--spacing) * -2) * calc(1 - var(--tw-space-x-reverse)));
|
|
1746
1742
|
}
|
|
1747
1743
|
|
|
1744
|
+
.self-center {
|
|
1745
|
+
align-self: center;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
1748
|
.self-end {
|
|
1749
1749
|
align-self: flex-end;
|
|
1750
1750
|
}
|
|
@@ -1970,6 +1970,11 @@
|
|
|
1970
1970
|
border-style: none;
|
|
1971
1971
|
}
|
|
1972
1972
|
|
|
1973
|
+
.border-solid {
|
|
1974
|
+
--tw-border-style: solid;
|
|
1975
|
+
border-style: solid;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1973
1978
|
.border-\(--color-border\) {
|
|
1974
1979
|
border-color: var(--color-border);
|
|
1975
1980
|
}
|
|
@@ -3195,6 +3200,10 @@
|
|
|
3195
3200
|
height: calc(var(--spacing) * 8);
|
|
3196
3201
|
}
|
|
3197
3202
|
|
|
3203
|
+
.group-data-\[orientation\=horizontal\]\/tabs\:min-w-max:is(:where(.group\/tabs)[data-orientation="horizontal"] *) {
|
|
3204
|
+
min-width: max-content;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3198
3207
|
.group-data-\[orientation\=vertical\]\/attachment\:absolute:is(:where(.group\/attachment)[data-orientation="vertical"] *) {
|
|
3199
3208
|
position: absolute;
|
|
3200
3209
|
}
|
|
@@ -7468,6 +7477,7 @@
|
|
|
7468
7477
|
--secondary-foreground: oklch(20.5% 0 0);
|
|
7469
7478
|
--muted: oklch(97% 0 0);
|
|
7470
7479
|
--muted-foreground: oklch(54.5% 0 0);
|
|
7480
|
+
--muted-foreground-highlighted: oklch(52% 0 0);
|
|
7471
7481
|
--accent: oklch(97% 0 0);
|
|
7472
7482
|
--accent-foreground: oklch(20.5% 0 0);
|
|
7473
7483
|
--destructive: oklch(50.5% .213 27.518);
|
|
@@ -7538,6 +7548,7 @@
|
|
|
7538
7548
|
--secondary-foreground: oklch(98.5% 0 0);
|
|
7539
7549
|
--muted: oklch(26.9% 0 0);
|
|
7540
7550
|
--muted-foreground: oklch(70.8% 0 0);
|
|
7551
|
+
--muted-foreground-highlighted: oklch(70.8% 0 0);
|
|
7541
7552
|
--accent: oklch(26.9% 0 0);
|
|
7542
7553
|
--accent-foreground: oklch(98.5% 0 0);
|
|
7543
7554
|
--destructive: oklch(70.4% .191 22.216);
|
|
@@ -7724,7 +7735,7 @@
|
|
|
7724
7735
|
outline-style: none;
|
|
7725
7736
|
}
|
|
7726
7737
|
|
|
7727
|
-
[data-slot="scroll-area-viewport"]:focus-visible {
|
|
7738
|
+
:is([data-slot="scroll-area-viewport"], [data-slot="tabs-content"]):focus-visible {
|
|
7728
7739
|
outline-offset: -1px;
|
|
7729
7740
|
outline-width: 1px;
|
|
7730
7741
|
}
|
|
@@ -7799,6 +7810,10 @@
|
|
|
7799
7810
|
display: none;
|
|
7800
7811
|
}
|
|
7801
7812
|
|
|
7813
|
+
:is([data-highlighted], [data-slot="command-item"][data-selected="true"]) {
|
|
7814
|
+
--muted-foreground: var(--muted-foreground-highlighted);
|
|
7815
|
+
}
|
|
7816
|
+
|
|
7802
7817
|
@property --tw-translate-x {
|
|
7803
7818
|
syntax: "*";
|
|
7804
7819
|
inherits: false;
|
package/dist/theme.css
CHANGED
|
@@ -90,6 +90,8 @@
|
|
|
90
90
|
--muted: oklch(0.97 0 0);
|
|
91
91
|
/* Muted text: 4.5:1 on the page (white), a card and the muted surface (oklch 0.97). 0.556 was 4.35:1 on the latter. */
|
|
92
92
|
--muted-foreground: oklch(0.545 0 0);
|
|
93
|
+
/* Muted text on a highlighted row (the row is a tint of the foreground, 0.94 here): 4.5:1 there too, where 0.545 was 4.34:1. */
|
|
94
|
+
--muted-foreground-highlighted: oklch(0.52 0 0);
|
|
93
95
|
|
|
94
96
|
--accent: oklch(0.97 0 0);
|
|
95
97
|
--accent-foreground: oklch(0.205 0 0);
|
|
@@ -182,6 +184,7 @@
|
|
|
182
184
|
|
|
183
185
|
--muted: oklch(0.269 0 0);
|
|
184
186
|
--muted-foreground: oklch(0.708 0 0);
|
|
187
|
+
--muted-foreground-highlighted: oklch(0.708 0 0);
|
|
185
188
|
|
|
186
189
|
--accent: oklch(0.269 0 0);
|
|
187
190
|
--accent-foreground: oklch(0.985 0 0);
|
|
@@ -36,40 +36,15 @@ var buttonVariants = cva("group/button inline-flex shrink-0 cursor-pointer items
|
|
|
36
36
|
size: "default"
|
|
37
37
|
}
|
|
38
38
|
});
|
|
39
|
-
/**
|
|
40
|
-
* Whether this Button is rendering a link rather than a button, so it can keep
|
|
41
|
-
* link semantics. Covers `render={<a href="..." />}` and a routing component
|
|
42
|
-
* that takes an `href` (Next's Link). A component with a different prop for its
|
|
43
|
-
* destination (react-router's `to`) cannot be recognised from here: give those
|
|
44
|
-
* `nativeButton={false} role={undefined}` directly.
|
|
45
|
-
*/
|
|
39
|
+
/** Whether this Button renders a link (`render={<a />}` or a component taking `href`), so it keeps link semantics. */
|
|
46
40
|
var rendersALink = (render) => isValidElement(render) && (render.type === "a" || typeof render.props.href === "string");
|
|
47
|
-
/**
|
|
48
|
-
* A link that looks like a button is still a link, and has to be announced as
|
|
49
|
-
* one: it opens in a new tab, it has an address, Enter follows it and Space
|
|
50
|
-
* scrolls the page.
|
|
51
|
-
*
|
|
52
|
-
* Base UI needs telling, in two steps. Rendering an anchor while `nativeButton`
|
|
53
|
-
* is true (its default) warns in development and puts `type="button"` on the
|
|
54
|
-
* anchor; following the warning and passing `nativeButton={false}` then adds
|
|
55
|
-
* `role="button"`, which is what takes the link semantics away. So this does
|
|
56
|
-
* both: non-native, with the role and the tabindex Base UI would add cleared
|
|
57
|
-
* again. An explicit prop from the caller still wins over either.
|
|
58
|
-
*/
|
|
41
|
+
/** A link that looks like a button stays a link: not a native button, and without the role and tabindex Base UI would add. */
|
|
59
42
|
var LINK_PROPS = {
|
|
60
43
|
nativeButton: false,
|
|
61
44
|
role: void 0,
|
|
62
45
|
tabIndex: void 0
|
|
63
46
|
};
|
|
64
|
-
/**
|
|
65
|
-
* A disabled link is not :disabled (only a form control can be), so the markup has to say it: Base UI already marks it
|
|
66
|
-
* `aria-disabled` and `data-disabled` and stops the click, and this takes it out of the tab order, which LINK_PROPS
|
|
67
|
-
* would otherwise leave to the browser. The href is kept on purpose, so a routing component that requires one keeps
|
|
68
|
-
* working; the pointer is kept off the link by `data-link-disabled:pointer-events-none`, which also covers a middle
|
|
69
|
-
* click and the context menu, and the click and Enter are stopped by Base UI. It is a data attribute of its own, not
|
|
70
|
-
* `aria-disabled`, because an aria-disabled button (`focusableWhenDisabled`) is meant to stay hoverable and focusable so it
|
|
71
|
-
* can say why it cannot act; only a native :disabled button and this link are inert.
|
|
72
|
-
*/
|
|
47
|
+
/** A disabled link is out of the tab order and inert to the pointer (`data-link-disabled`); the href stays. */
|
|
73
48
|
var DISABLED_LINK_PROPS = {
|
|
74
49
|
...LINK_PROPS,
|
|
75
50
|
tabIndex: -1,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.js","names":[],"sources":["../../../../../../../ui/src/components/ui/button/button.tsx"],"sourcesContent":["import { Button as ButtonPrimitive } from '@base-ui/react/button';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { isValidElement } from 'react';\n\n/**\n * The fill of a disabled Button that has one: muted, whichever way it is disabled (a button's own disabled, Base UI's\n * data-disabled, or aria-disabled on a link), in the dark theme too, where the variants set their own fill. Ghost and link\n * have no fill to begin with and are given none: a grey box would turn a link into a button.\n */\nconst DISABLED_FILL =\n 'disabled:bg-muted data-disabled:bg-muted aria-disabled:bg-muted dark:disabled:bg-muted dark:data-disabled:bg-muted dark:aria-disabled:bg-muted';\n\nexport const buttonVariants = cva(\n \"group/button inline-flex shrink-0 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all motion-reduce:transition-none motion-reduce:transform-none outline-none select-none active:not-aria-[haspopup]:not-aria-disabled:translate-y-px disabled:pointer-events-none disabled:border-transparent disabled:text-foreground/60 disabled:shadow-none disabled:focus-visible:ring-0 dark:disabled:border-transparent data-link-disabled:pointer-events-none data-link-disabled:focus-visible:ring-0 data-disabled:border-transparent data-disabled:text-foreground/60 data-disabled:shadow-none dark:data-disabled:border-transparent aria-disabled:border-transparent aria-disabled:text-foreground/60 aria-disabled:shadow-none dark:aria-disabled:border-transparent aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n variant: {\n default: `bg-primary text-primary-foreground hover:bg-primary/80 ${DISABLED_FILL}`,\n outline: `border-control bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-control dark:bg-input/30 dark:hover:bg-input/50 ${DISABLED_FILL}`,\n secondary: `bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground ${DISABLED_FILL}`,\n ghost:\n 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 aria-disabled:hover:bg-transparent dark:aria-disabled:hover:bg-transparent',\n destructive: `bg-destructive/10 text-destructive hover:bg-destructive/15 dark:bg-destructive/15 dark:hover:bg-destructive/20 ${DISABLED_FILL}`,\n link: 'text-primary underline-offset-4 hover:underline aria-disabled:hover:no-underline',\n },\n size: {\n default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',\n xs: \"h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3\",\n sm: \"h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5\",\n lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',\n icon: 'size-8',\n 'icon-xs': \"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3\",\n 'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',\n 'icon-lg': 'size-9',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n },\n);\n\n
|
|
1
|
+
{"version":3,"file":"button.js","names":[],"sources":["../../../../../../../ui/src/components/ui/button/button.tsx"],"sourcesContent":["import { Button as ButtonPrimitive } from '@base-ui/react/button';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { isValidElement } from 'react';\n\n/**\n * The fill of a disabled Button that has one: muted, whichever way it is disabled (a button's own disabled, Base UI's\n * data-disabled, or aria-disabled on a link), in the dark theme too, where the variants set their own fill. Ghost and link\n * have no fill to begin with and are given none: a grey box would turn a link into a button.\n */\nconst DISABLED_FILL =\n 'disabled:bg-muted data-disabled:bg-muted aria-disabled:bg-muted dark:disabled:bg-muted dark:data-disabled:bg-muted dark:aria-disabled:bg-muted';\n\nexport const buttonVariants = cva(\n \"group/button inline-flex shrink-0 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all motion-reduce:transition-none motion-reduce:transform-none outline-none select-none active:not-aria-[haspopup]:not-aria-disabled:translate-y-px disabled:pointer-events-none disabled:border-transparent disabled:text-foreground/60 disabled:shadow-none disabled:focus-visible:ring-0 dark:disabled:border-transparent data-link-disabled:pointer-events-none data-link-disabled:focus-visible:ring-0 data-disabled:border-transparent data-disabled:text-foreground/60 data-disabled:shadow-none dark:data-disabled:border-transparent aria-disabled:border-transparent aria-disabled:text-foreground/60 aria-disabled:shadow-none dark:aria-disabled:border-transparent aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n variant: {\n default: `bg-primary text-primary-foreground hover:bg-primary/80 ${DISABLED_FILL}`,\n outline: `border-control bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-control dark:bg-input/30 dark:hover:bg-input/50 ${DISABLED_FILL}`,\n secondary: `bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground ${DISABLED_FILL}`,\n ghost:\n 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 aria-disabled:hover:bg-transparent dark:aria-disabled:hover:bg-transparent',\n destructive: `bg-destructive/10 text-destructive hover:bg-destructive/15 dark:bg-destructive/15 dark:hover:bg-destructive/20 ${DISABLED_FILL}`,\n link: 'text-primary underline-offset-4 hover:underline aria-disabled:hover:no-underline',\n },\n size: {\n default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',\n xs: \"h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3\",\n sm: \"h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5\",\n lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',\n icon: 'size-8',\n 'icon-xs': \"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3\",\n 'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',\n 'icon-lg': 'size-9',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n },\n);\n\n/** Whether this Button renders a link (`render={<a />}` or a component taking `href`), so it keeps link semantics. */\nconst rendersALink = (render: ButtonPrimitive.Props['render']): boolean =>\n isValidElement(render) && (render.type === 'a' || typeof (render.props as { href?: unknown }).href === 'string');\n\n/** A link that looks like a button stays a link: not a native button, and without the role and tabindex Base UI would add. */\nconst LINK_PROPS = { nativeButton: false, role: undefined, tabIndex: undefined } as const;\n\n/** A disabled link is out of the tab order and inert to the pointer (`data-link-disabled`); the href stays. */\nconst DISABLED_LINK_PROPS = { ...LINK_PROPS, tabIndex: -1, 'data-link-disabled': '' } as const;\n\nexport const Button = ({\n className,\n variant = 'default',\n size = 'default',\n ...props\n}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) => {\n return (\n <ButtonPrimitive\n data-slot=\"button\"\n {...(rendersALink(props.render) ? (props.disabled ? DISABLED_LINK_PROPS : LINK_PROPS) : {})}\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n />\n );\n};\n"],"mappings":";;;;;;;;;;;AAUA,IAAM,gBACJ;AAEF,IAAa,iBAAiB,IAC5B,2jCACA;CACE,UAAU;EACR,SAAS;GACP,SAAS,0DAA0D;GACnE,SAAS,sLAAsL;GAC/L,WAAW,kLAAkL;GAC7L,OACE;GACF,aAAa,kHAAkH;GAC/H,MAAM;EACR;EACA,MAAM;GACJ,SAAS;GACT,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,MAAM;GACN,WAAW;GACX,WAAW;GACX,WAAW;EACb;CACF;CACA,iBAAiB;EACf,SAAS;EACT,MAAM;CACR;AACF,CACF;;AAGA,IAAM,gBAAgB,WACpB,eAAe,MAAM,MAAM,OAAO,SAAS,OAAO,OAAQ,OAAO,MAA6B,SAAS;;AAGzG,IAAM,aAAa;CAAE,cAAc;CAAO,MAAM,KAAA;CAAW,UAAU,KAAA;AAAU;;AAG/E,IAAM,sBAAsB;CAAE,GAAG;CAAY,UAAU;CAAI,sBAAsB;AAAG;AAEpF,IAAa,YAAU,EACrB,WACA,UAAU,WACV,OAAO,WACP,GAAG,YAC8D;CACjE,OACE,oBAAC,QAAD;EACE,aAAU;EACV,GAAK,aAAa,MAAM,MAAM,IAAK,MAAM,WAAW,sBAAsB,aAAc,CAAC;EACzF,WAAW,GAAG,eAAe;GAAE;GAAS;GAAM;EAAU,CAAC,CAAC;EAC1D,GAAI;CACL,CAAA;AAEL"}
|
|
@@ -22,7 +22,7 @@ var ComboboxInput = ({ className, triggerLabel = "Show options", ...props }) =>
|
|
|
22
22
|
}), /* @__PURE__ */ jsx(Combobox.Trigger, {
|
|
23
23
|
"data-slot": "combobox-trigger",
|
|
24
24
|
className: "absolute right-1.75 flex size-6 items-center justify-center text-muted-foreground disabled:pointer-events-none disabled:opacity-50",
|
|
25
|
-
tabIndex:
|
|
25
|
+
tabIndex: 0,
|
|
26
26
|
"aria-label": triggerLabel,
|
|
27
27
|
children: /* @__PURE__ */ jsx(IconChevronDown, { className: "size-4" })
|
|
28
28
|
})]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"combobox.js","names":[],"sources":["../../../../../../../ui/src/components/ui/combobox/combobox.tsx"],"sourcesContent":["import { Combobox as ComboboxPrimitive } from '@base-ui/react/combobox';\nimport {\n POPUP_FRAME_CLASS,\n POPUP_GROUP_LABEL_CLASS,\n POPUP_ITEM_INDICATOR_CLASS,\n POPUP_LAYER_CLASS,\n POPUP_LIST_WIDTH_CLASS,\n popupItemClass,\n} from '@edyn/ui/lib/popup-list';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconCheck, IconChevronDown } from '@tabler/icons-react';\nimport type { ReactNode } from 'react';\nimport { PopupListScroll } from '../scroll-area';\n\nexport const Combobox = <TValue,>({ ...props }: ComboboxPrimitive.Root.Props<TValue>) => {\n return <ComboboxPrimitive.Root data-slot=\"combobox\" {...props} />;\n};\n\nexport interface IComboboxInputProps extends ComboboxPrimitive.Input.Props {\n /** The name of the chevron button that opens the list, for a screen reader and a voice command. Translate it with the rest of the page. */\n triggerLabel?: string;\n}\n\nexport const ComboboxInput = ({ className, triggerLabel = 'Show options', ...props }: IComboboxInputProps) => {\n return (\n <ComboboxPrimitive.InputGroup data-slot=\"combobox-input-group\" className=\"relative flex w-full items-center\">\n <ComboboxPrimitive.Input\n data-slot=\"combobox-input\"\n className={cn(\n 'h-8 w-full min-w-0 rounded-lg border border-control bg-transparent px-2.5 py-1 pr-8 text-base transition-colors outline-none placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',\n className,\n )}\n {...props}\n />\n <ComboboxPrimitive.Trigger\n data-slot=\"combobox-trigger\"\n // A 24px target (WCAG 2.5.8) around the 16px chevron, which stays 11px from the end of the field
|
|
1
|
+
{"version":3,"file":"combobox.js","names":[],"sources":["../../../../../../../ui/src/components/ui/combobox/combobox.tsx"],"sourcesContent":["import { Combobox as ComboboxPrimitive } from '@base-ui/react/combobox';\nimport {\n POPUP_FRAME_CLASS,\n POPUP_GROUP_LABEL_CLASS,\n POPUP_ITEM_INDICATOR_CLASS,\n POPUP_LAYER_CLASS,\n POPUP_LIST_WIDTH_CLASS,\n popupItemClass,\n} from '@edyn/ui/lib/popup-list';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconCheck, IconChevronDown } from '@tabler/icons-react';\nimport type { ReactNode } from 'react';\nimport { PopupListScroll } from '../scroll-area';\n\nexport const Combobox = <TValue,>({ ...props }: ComboboxPrimitive.Root.Props<TValue>) => {\n return <ComboboxPrimitive.Root data-slot=\"combobox\" {...props} />;\n};\n\nexport interface IComboboxInputProps extends ComboboxPrimitive.Input.Props {\n /** The name of the chevron button that opens the list, for a screen reader and a voice command. Translate it with the rest of the page. */\n triggerLabel?: string;\n}\n\nexport const ComboboxInput = ({ className, triggerLabel = 'Show options', ...props }: IComboboxInputProps) => {\n return (\n <ComboboxPrimitive.InputGroup data-slot=\"combobox-input-group\" className=\"relative flex w-full items-center\">\n <ComboboxPrimitive.Input\n data-slot=\"combobox-input\"\n className={cn(\n 'h-8 w-full min-w-0 rounded-lg border border-control bg-transparent px-2.5 py-1 pr-8 text-base transition-colors outline-none placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',\n className,\n )}\n {...props}\n />\n <ComboboxPrimitive.Trigger\n data-slot=\"combobox-trigger\"\n // A named 24px target (WCAG 2.5.8) around the 16px chevron, which stays 11px from the end of the field. A named control is a tab stop (Base UI's own default is -1).\n className=\"absolute right-1.75 flex size-6 items-center justify-center text-muted-foreground disabled:pointer-events-none disabled:opacity-50\"\n tabIndex={0}\n aria-label={triggerLabel}\n >\n <IconChevronDown className=\"size-4\" />\n </ComboboxPrimitive.Trigger>\n </ComboboxPrimitive.InputGroup>\n );\n};\n\nexport interface IComboboxContentProps extends Omit<ComboboxPrimitive.Popup.Props, 'children'> {\n /** Item nodes, or the Base UI function form `(item, index) => node` (requires `items` on the root). */\n children?: ComboboxPrimitive.List.Props['children'];\n /** Empty-state content; rendered (with the default text) only when the filtered list is empty. */\n empty?: ReactNode;\n /** Optional portal destination, used by shadow-DOM element wrappers. */\n portalContainer?: ComboboxPrimitive.Portal.Props['container'];\n}\n\nexport const ComboboxContent = ({ className, children, empty, portalContainer, ...props }: IComboboxContentProps) => {\n return (\n <ComboboxPrimitive.Portal data-slot=\"combobox-portal\" container={portalContainer}>\n <ComboboxPrimitive.Positioner data-slot=\"combobox-positioner\" sideOffset={4} align=\"start\" className={POPUP_LAYER_CLASS}>\n <ComboboxPrimitive.Popup\n data-slot=\"combobox-content\"\n className={cn(\n 'flex max-h-(--available-height) flex-col overflow-hidden',\n POPUP_FRAME_CLASS,\n POPUP_LIST_WIDTH_CLASS,\n 'motion-reduce:animate-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',\n className,\n )}\n {...props}\n >\n <ComboboxEmpty>{empty}</ComboboxEmpty>\n {/* The ScrollArea, not the List, is the scroll container: a filtered list\n that runs past the fold needs a bar and a fade to say so. Base UI\n keeps the highlighted item in view through the nearest scrollable\n ancestor, which is now the viewport. POPUP_LIST_HEIGHT_CLASS ends\n the list on a whole row. */}\n <PopupListScroll>\n <ComboboxPrimitive.List data-slot=\"combobox-list\" className=\"px-1\">\n {children}\n </ComboboxPrimitive.List>\n </PopupListScroll>\n </ComboboxPrimitive.Popup>\n </ComboboxPrimitive.Positioner>\n </ComboboxPrimitive.Portal>\n );\n};\n\nexport const ComboboxItem = ({ className, children, ...props }: ComboboxPrimitive.Item.Props) => {\n return (\n <ComboboxPrimitive.Item data-slot=\"combobox-item\" className={cn(popupItemClass('base-ui', true), className)} {...props}>\n <span className=\"flex-1\">{children}</span>\n <ComboboxPrimitive.ItemIndicator data-slot=\"combobox-item-indicator\" className={POPUP_ITEM_INDICATOR_CLASS}>\n <IconCheck className=\"size-3.5\" />\n </ComboboxPrimitive.ItemIndicator>\n </ComboboxPrimitive.Item>\n );\n};\n\nexport const ComboboxEmpty = ({ className, children, ...props }: ComboboxPrimitive.Empty.Props) => {\n return (\n <ComboboxPrimitive.Empty\n data-slot=\"combobox-empty\"\n // empty:py-0 collapses the padding while the list has results: Base UI\n // keeps this status element mounted and only fills it when the list is\n // empty (and only when the root has `items` to derive emptiness from).\n className={cn('py-4 text-center text-sm text-muted-foreground empty:py-0', className)}\n {...props}\n >\n {children ?? 'No results found.'}\n </ComboboxPrimitive.Empty>\n );\n};\n\n/**\n * A titled run of items. Put a `ComboboxLabel` first in it; with grouped `items` on the root, render the group's own items\n * through `ComboboxCollection` (Base UI's grouped-items pattern), so a filter hides a group whose items all drop out.\n */\nexport const ComboboxGroup = ({ ...props }: ComboboxPrimitive.Group.Props) => {\n return <ComboboxPrimitive.Group data-slot=\"combobox-group\" {...props} />;\n};\n\n/** A group's heading, the same one Select and the menus draw. */\nexport const ComboboxLabel = ({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) => {\n return <ComboboxPrimitive.GroupLabel data-slot=\"combobox-label\" className={cn(POPUP_GROUP_LABEL_CLASS, className)} {...props} />;\n};\n\n/** The items of one group, for grouped `items` on the root. */\nexport const ComboboxCollection = ({ ...props }: ComboboxPrimitive.Collection.Props) => {\n return <ComboboxPrimitive.Collection data-slot=\"combobox-collection\" {...props} />;\n};\n\nexport const ComboboxSeparator = ({ className, ...props }: ComboboxPrimitive.Separator.Props) => {\n return <ComboboxPrimitive.Separator data-slot=\"combobox-separator\" className={cn('-mx-1 my-1 h-px bg-border', className)} {...props} />;\n};\n"],"mappings":";;;;;;;AAcA,IAAa,cAAqB,EAAE,GAAG,YAAkD;CACvF,OAAO,oBAAC,SAAkB,MAAnB;EAAwB,aAAU;EAAW,GAAI;CAAQ,CAAA;AAClE;AAOA,IAAa,iBAAiB,EAAE,WAAW,eAAe,gBAAgB,GAAG,YAAiC;CAC5G,OACE,qBAAC,SAAkB,YAAnB;EAA8B,aAAU;EAAuB,WAAU;EAAzE,UAAA,CACE,oBAAC,SAAkB,OAAnB;GACE,aAAU;GACV,WAAW,GACT,6dACA,SACF;GACA,GAAI;EACL,CAAA,GACD,oBAAC,SAAkB,SAAnB;GACE,aAAU;GAEV,WAAU;GACV,UAAU;GACV,cAAY;GAEZ,UAAA,oBAAC,iBAAD,EAAiB,WAAU,SAAU,CAAA;EACZ,CAAA,CACC;;AAElC;AAWA,IAAa,mBAAmB,EAAE,WAAW,UAAU,OAAO,iBAAiB,GAAG,YAAmC;CACnH,OACE,oBAAC,SAAkB,QAAnB;EAA0B,aAAU;EAAkB,WAAW;EAC/D,UAAA,oBAAC,SAAkB,YAAnB;GAA8B,aAAU;GAAsB,YAAY;GAAG,OAAM;GAAQ,WAAW;GACpG,UAAA,qBAAC,SAAkB,OAAnB;IACE,aAAU;IACV,WAAW,GACT,4DACA,mBACA,wBACA,mKACA,SACF;IACA,GAAI;IATN,UAAA,CAWE,oBAAC,eAAD,EAAA,UAAgB,MAAqB,CAAA,GAMrC,oBAAC,iBAAD,EAAA,UACE,oBAAC,SAAkB,MAAnB;KAAwB,aAAU;KAAgB,WAAU;KACzD;IACqB,CAAA,EACT,CAAA,CACM;;EACG,CAAA;CACN,CAAA;AAE9B;AAEA,IAAa,gBAAgB,EAAE,WAAW,UAAU,GAAG,YAA0C;CAC/F,OACE,qBAAC,SAAkB,MAAnB;EAAwB,aAAU;EAAgB,WAAW,GAAG,eAAe,WAAW,IAAI,GAAG,SAAS;EAAG,GAAI;EAAjH,UAAA,CACE,oBAAC,QAAD;GAAM,WAAU;GAAU;EAAe,CAAA,GACzC,oBAAC,SAAkB,eAAnB;GAAiC,aAAU;GAA0B,WAAW;GAC9E,UAAA,oBAAC,WAAD,EAAW,WAAU,WAAY,CAAA;EACF,CAAA,CACX;;AAE5B;AAEA,IAAa,iBAAiB,EAAE,WAAW,UAAU,GAAG,YAA2C;CACjG,OACE,oBAAC,SAAkB,OAAnB;EACE,aAAU;EAIV,WAAW,GAAG,6DAA6D,SAAS;EACpF,GAAI;EAEH,UAAA,YAAY;CACU,CAAA;AAE7B;;;;;AAMA,IAAa,iBAAiB,EAAE,GAAG,YAA2C;CAC5E,OAAO,oBAAC,SAAkB,OAAnB;EAAyB,aAAU;EAAiB,GAAI;CAAQ,CAAA;AACzE;;AAGA,IAAa,iBAAiB,EAAE,WAAW,GAAG,YAAgD;CAC5F,OAAO,oBAAC,SAAkB,YAAnB;EAA8B,aAAU;EAAiB,WAAW,GAAG,yBAAyB,SAAS;EAAG,GAAI;CAAQ,CAAA;AACjI;;AAGA,IAAa,sBAAsB,EAAE,GAAG,YAAgD;CACtF,OAAO,oBAAC,SAAkB,YAAnB;EAA8B,aAAU;EAAsB,GAAI;CAAQ,CAAA;AACnF;AAEA,IAAa,qBAAqB,EAAE,WAAW,GAAG,YAA+C;CAC/F,OAAO,oBAAC,SAAkB,WAAnB;EAA6B,aAAU;EAAqB,WAAW,GAAG,6BAA6B,SAAS;EAAG,GAAI;CAAQ,CAAA;AACxI"}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { VariantProps } from 'class-variance-authority';
|
|
2
|
-
|
|
2
|
+
declare const emptyVariants: (props?: ({
|
|
3
|
+
variant?: "dashed" | "solid" | null | undefined;
|
|
4
|
+
} & import('class-variance-authority/types').ClassProp) | undefined) => string;
|
|
5
|
+
/** An empty state. It has no border until the caller adds `border`; `variant` is the line style that border takes. */
|
|
6
|
+
export declare const Empty: ({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps<typeof emptyVariants>) => import('react').JSX.Element;
|
|
3
7
|
export declare const EmptyHeader: ({ className, ...props }: React.ComponentProps<"div">) => import('react').JSX.Element;
|
|
4
8
|
declare const emptyMediaVariants: (props?: ({
|
|
5
9
|
variant?: "default" | "icon" | null | undefined;
|
|
@@ -2,10 +2,21 @@ import { cn } from "../../../lib/utils.js";
|
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import { cva } from "class-variance-authority";
|
|
4
4
|
//#region ../ui/src/components/ui/empty/empty.tsx
|
|
5
|
-
var
|
|
5
|
+
var emptyVariants = cva("flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl p-6 text-center text-balance", {
|
|
6
|
+
variants: {
|
|
7
|
+
/** The line style of the border you give it (`border`); Empty draws none of its own. */
|
|
8
|
+
variant: {
|
|
9
|
+
solid: "border-solid",
|
|
10
|
+
dashed: "border-dashed"
|
|
11
|
+
} },
|
|
12
|
+
defaultVariants: { variant: "solid" }
|
|
13
|
+
});
|
|
14
|
+
/** An empty state. It has no border until the caller adds `border`; `variant` is the line style that border takes. */
|
|
15
|
+
var Empty = ({ className, variant = "solid", ...props }) => {
|
|
6
16
|
return /* @__PURE__ */ jsx("div", {
|
|
7
17
|
"data-slot": "empty",
|
|
8
|
-
|
|
18
|
+
"data-variant": variant,
|
|
19
|
+
className: cn(emptyVariants({ variant }), className),
|
|
9
20
|
...props
|
|
10
21
|
});
|
|
11
22
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"empty.js","names":[],"sources":["../../../../../../../ui/src/components/ui/empty/empty.tsx"],"sourcesContent":["import { cn } from '@edyn/ui/lib/utils';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\
|
|
1
|
+
{"version":3,"file":"empty.js","names":[],"sources":["../../../../../../../ui/src/components/ui/empty/empty.tsx"],"sourcesContent":["import { cn } from '@edyn/ui/lib/utils';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nconst emptyVariants = cva('flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl p-6 text-center text-balance', {\n variants: {\n /** The line style of the border you give it (`border`); Empty draws none of its own. */\n variant: {\n solid: 'border-solid',\n dashed: 'border-dashed',\n },\n },\n defaultVariants: {\n variant: 'solid',\n },\n});\n\n/** An empty state. It has no border until the caller adds `border`; `variant` is the line style that border takes. */\nexport const Empty = ({ className, variant = 'solid', ...props }: React.ComponentProps<'div'> & VariantProps<typeof emptyVariants>) => {\n return <div data-slot=\"empty\" data-variant={variant} className={cn(emptyVariants({ variant }), className)} {...props} />;\n};\n\nexport const EmptyHeader = ({ className, ...props }: React.ComponentProps<'div'>) => {\n return <div data-slot=\"empty-header\" className={cn('flex max-w-sm flex-col items-center gap-2', className)} {...props} />;\n};\n\nconst emptyMediaVariants = cva('mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0', {\n variants: {\n variant: {\n default: 'bg-transparent',\n icon: \"flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4\",\n },\n },\n defaultVariants: {\n variant: 'default',\n },\n});\n\nexport const EmptyMedia = ({ className, variant = 'default', ...props }: React.ComponentProps<'div'> & VariantProps<typeof emptyMediaVariants>) => {\n return <div data-slot=\"empty-icon\" data-variant={variant} className={cn(emptyMediaVariants({ variant, className }))} {...props} />;\n};\n\nexport const EmptyTitle = ({ className, ...props }: React.ComponentProps<'div'>) => {\n return <div data-slot=\"empty-title\" className={cn('font-heading text-sm font-medium tracking-tight', className)} {...props} />;\n};\n\nexport const EmptyDescription = ({ className, ...props }: React.ComponentProps<'p'>) => {\n return (\n <div\n data-slot=\"empty-description\"\n className={cn('text-sm/6 text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary', className)}\n {...props}\n />\n );\n};\n\nexport const EmptyContent = ({ className, ...props }: React.ComponentProps<'div'>) => {\n return (\n <div\n data-slot=\"empty-content\"\n className={cn('flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance', className)}\n {...props}\n />\n );\n};\n"],"mappings":";;;;AAGA,IAAM,gBAAgB,IAAI,iHAAiH;CACzI,UAAU;;AAER,SAAS;EACP,OAAO;EACP,QAAQ;CACV,EACF;CACA,iBAAiB,EACf,SAAS,QACX;AACF,CAAC;;AAGD,IAAa,SAAS,EAAE,WAAW,UAAU,SAAS,GAAG,YAA8E;CACrI,OAAO,oBAAC,OAAD;EAAK,aAAU;EAAQ,gBAAc;EAAS,WAAW,GAAG,cAAc,EAAE,QAAQ,CAAC,GAAG,SAAS;EAAG,GAAI;CAAQ,CAAA;AACzH;AAEA,IAAa,eAAe,EAAE,WAAW,GAAG,YAAyC;CACnF,OAAO,oBAAC,OAAD;EAAK,aAAU;EAAe,WAAW,GAAG,6CAA6C,SAAS;EAAG,GAAI;CAAQ,CAAA;AAC1H;AAEA,IAAM,qBAAqB,IAAI,+FAA+F;CAC5H,UAAU,EACR,SAAS;EACP,SAAS;EACT,MAAM;CACR,EACF;CACA,iBAAiB,EACf,SAAS,UACX;AACF,CAAC;AAED,IAAa,cAAc,EAAE,WAAW,UAAU,WAAW,GAAG,YAAmF;CACjJ,OAAO,oBAAC,OAAD;EAAK,aAAU;EAAa,gBAAc;EAAS,WAAW,GAAG,mBAAmB;GAAE;GAAS;EAAU,CAAC,CAAC;EAAG,GAAI;CAAQ,CAAA;AACnI;AAEA,IAAa,cAAc,EAAE,WAAW,GAAG,YAAyC;CAClF,OAAO,oBAAC,OAAD;EAAK,aAAU;EAAc,WAAW,GAAG,mDAAmD,SAAS;EAAG,GAAI;CAAQ,CAAA;AAC/H;AAEA,IAAa,oBAAoB,EAAE,WAAW,GAAG,YAAuC;CACtF,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,qGAAqG,SAAS;EAC5H,GAAI;CACL,CAAA;AAEL;AAEA,IAAa,gBAAgB,EAAE,WAAW,GAAG,YAAyC;CACpF,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAW,GAAG,mFAAmF,SAAS;EAC1G,GAAI;CACL,CAAA;AAEL"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cn } from "../../../lib/utils.js";
|
|
2
|
+
import { useHoldsFocusable } from "../../../hooks/use-holds-focusable.js";
|
|
2
3
|
import { useMergedRefs } from "../../../hooks/use-merged-refs.js";
|
|
3
|
-
import { useEffect, useRef
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
import { ScrollArea } from "@base-ui/react/scroll-area";
|
|
6
7
|
//#region ../ui/src/components/ui/scroll-area/scroll-area.tsx
|
|
@@ -18,19 +19,8 @@ var ScrollBar = ({ className, orientation = "vertical", visibility = "hover", ..
|
|
|
18
19
|
* stylesheet reaches this element's own viewport through a child combinator, so a
|
|
19
20
|
* ScrollArea nested in another one cannot pick up its ancestor's edges.
|
|
20
21
|
*/
|
|
21
|
-
/**
|
|
22
|
-
* Base UI gives the root, the viewport and the content `role="presentation"`. On
|
|
23
|
-
* a div that changes nothing for a screen reader, but it is an EXPLICIT role, and
|
|
24
|
-
* axe treats explicit presentation children as foreign to a parent that owns
|
|
25
|
-
* required children. A ScrollArea inside `role="menu"` or `role="listbox"` then
|
|
26
|
-
* fails as a critical aria-required-children violation, while the same div with
|
|
27
|
-
* no role passes straight through. Clearing it is what makes a ScrollArea legal
|
|
28
|
-
* in a popup list. A spread because the literal `role={undefined}` is flagged by
|
|
29
|
-
* the a11y lint rule, which cannot tell it from an invalid role.
|
|
30
|
-
*/
|
|
22
|
+
/** Base UI's explicit `role="presentation"` on the parts fails axe's aria-required-children inside a menu or listbox; clearing it does not. */
|
|
31
23
|
var NO_ROLE = { role: void 0 };
|
|
32
|
-
/** What takes Tab inside a region: if any is there, the region is not a stop of its own. */
|
|
33
|
-
var FOCUSABLE_INSIDE = "a[href], button:not(:disabled), input:not([type=\"hidden\"]):not(:disabled), select:not(:disabled), textarea:not(:disabled), summary, [contenteditable], [tabindex]:not([tabindex=\"-1\"])";
|
|
34
24
|
/** Base UI makes the viewport a tab stop (0, or -1 with nothing to scroll); undefined takes that away. */
|
|
35
25
|
var NO_TAB_STOP = { tabIndex: void 0 };
|
|
36
26
|
var FADE_AXIS = {
|
|
@@ -67,27 +57,7 @@ var FADE_AXIS = {
|
|
|
67
57
|
var ScrollArea$1 = ({ className, children, orientation = "vertical", type = "hover", fade = false, fadeFrom = "card", scrollbarClassName, viewportId, viewportRef, viewportClassName, scrollRestorationId, focusable = true, viewPortId, viewPortRef, viewPortClassName, scrollVerticalClassName, scrollHorizontalClassName, ...props }) => {
|
|
68
58
|
const refInner = useRef(null);
|
|
69
59
|
const refsViewport = useMergedRefs(refInner, viewportRef ?? viewPortRef);
|
|
70
|
-
const
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
const el = refInner.current;
|
|
73
|
-
if (!el || !focusable) return;
|
|
74
|
-
const read = () => setHoldsFocusable(el.querySelector(FOCUSABLE_INSIDE) !== null);
|
|
75
|
-
read();
|
|
76
|
-
const observer = new MutationObserver(read);
|
|
77
|
-
observer.observe(el, {
|
|
78
|
-
childList: true,
|
|
79
|
-
subtree: true,
|
|
80
|
-
attributes: true,
|
|
81
|
-
attributeFilter: [
|
|
82
|
-
"tabindex",
|
|
83
|
-
"disabled",
|
|
84
|
-
"href"
|
|
85
|
-
]
|
|
86
|
-
});
|
|
87
|
-
return () => {
|
|
88
|
-
observer.disconnect();
|
|
89
|
-
};
|
|
90
|
-
}, [focusable]);
|
|
60
|
+
const holdsFocusable = useHoldsFocusable(refInner, focusable);
|
|
91
61
|
useEffect(() => {
|
|
92
62
|
const el = refInner.current;
|
|
93
63
|
if (!el) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-area.js","names":[],"sources":["../../../../../../../ui/src/components/ui/scroll-area/scroll-area.tsx"],"sourcesContent":["import { ScrollArea as BaseScrollArea } from '@base-ui/react/scroll-area';\nimport { useMergedRefs } from '@edyn/ui/hooks/use-merged-refs';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { type ComponentProps, type RefObject, useEffect, useRef, useState } from 'react';\n\n/**\n * When the bar shows: 'hover' on area hover or while scrolling (default),\n * 'scroll' only while scrolling, 'always' permanent. In every mode the bar is\n * mounted only while that axis actually overflows, so 'always' means \"always\n * when there is something to scroll\", never a bar on a list that fits.\n */\nexport type TScrollbarVisibility = 'hover' | 'scroll' | 'always';\n\n/**\n * Surface the edge fades blended into, back when they were painted.\n *\n * @deprecated The fades are cut into the content by a mask, so they match any\n * surface without being told which one. Still accepted, and ignored.\n */\nexport type TScrollAreaFadeFrom = 'card' | 'background' | 'sidebar' | 'popover';\n\nexport interface IScrollBarProps extends ComponentProps<typeof BaseScrollArea.Scrollbar> {\n visibility?: TScrollbarVisibility;\n}\n\nexport const ScrollBar = ({ className, orientation = 'vertical', visibility = 'hover', ...props }: IScrollBarProps) => {\n return (\n <BaseScrollArea.Scrollbar\n data-slot=\"scroll-area-scrollbar\"\n orientation={orientation}\n className={cn(\n 'peer/scrollbar z-100 flex touch-none select-none motion-safe:transition-opacity motion-safe:duration-150',\n // Size, gap and start read --scroll-bar-size, --scroll-bar-gap and --scroll-bar-start (see scroll-bar.css), whose\n // defaults are the 8px bar flush with the box that it has always been.\n orientation === 'vertical' &&\n 'h-(--scroll-bar-length) w-(--scroll-bar-size) me-(--scroll-bar-gap) mt-(--scroll-bar-offset-top) border-l border-l-transparent p-px',\n orientation === 'horizontal' &&\n 'h-(--scroll-bar-size) ms-(--scroll-bar-gap) me-(--scroll-bar-gap) mb-(--scroll-bar-gap) flex-col border-t border-t-transparent p-px',\n visibility !== 'always' && 'opacity-0 data-scrolling:opacity-100',\n visibility === 'hover' && 'data-hovering:opacity-100 group-hover/scroll-area:opacity-100',\n className,\n )}\n {...props}\n >\n <BaseScrollArea.Thumb className=\"relative flex-1 rounded-full bg-muted-foreground/50 transition-colors hover:bg-muted-foreground/70 data-scrolling:bg-muted-foreground/70\" />\n </BaseScrollArea.Scrollbar>\n );\n};\n\nexport interface IScrollAreaProps extends ComponentProps<typeof BaseScrollArea.Root> {\n /** Which axes get a styled scrollbar; content decides what actually overflows. */\n orientation?: 'horizontal' | 'vertical' | 'both';\n /** Scrollbar reveal behavior (mirrors the Radix ScrollArea `type` prop). */\n type?: TScrollbarVisibility;\n /** Edge fades hinting at clipped content; each one shows only while its own edge hides something. */\n fade?: boolean;\n /**\n * @deprecated No longer read. The fades mask the content, so they sit correctly\n * on any surface; passing this does nothing and is safe to delete.\n */\n fadeFrom?: TScrollAreaFadeFrom;\n /** Places both bars, for a host that gives them a gutter of their own (a sidebar menu). */\n scrollbarClassName?: string;\n viewportId?: string;\n /** Access to the scrollable viewport (e.g. to reset scroll position on route changes). */\n viewportRef?: RefObject<HTMLDivElement | null>;\n viewportClassName?: string;\n /** Published on the viewport as data-scroll-restoration-id, for a router that restores scroll position. */\n scrollRestorationId?: string;\n /**\n * Whether the viewport may be a tab stop (default). It is one only while it overflows and nothing inside can take Tab\n * (axe's scrollable-region-focusable), with a 1px keyboard-only line. Off inside a menu, whose items are the tab stops.\n */\n focusable?: boolean;\n /** @deprecated Use `viewportId`. */\n viewPortId?: string;\n /** @deprecated Use `viewportRef`. */\n viewPortRef?: RefObject<HTMLDivElement | null>;\n /** @deprecated Use `viewportClassName`. */\n viewPortClassName?: string;\n /** Places the vertical bar only; `scrollbarClassName` covers both. */\n scrollVerticalClassName?: string;\n /** Places the horizontal bar only; `scrollbarClassName` covers both. */\n scrollHorizontalClassName?: string;\n}\n\n/**\n * Which axes fade, published for scroll-fade.css to key its mask off. The\n * stylesheet reaches this element's own viewport through a child combinator, so a\n * ScrollArea nested in another one cannot pick up its ancestor's edges.\n */\n/**\n * Base UI gives the root, the viewport and the content `role=\"presentation\"`. On\n * a div that changes nothing for a screen reader, but it is an EXPLICIT role, and\n * axe treats explicit presentation children as foreign to a parent that owns\n * required children. A ScrollArea inside `role=\"menu\"` or `role=\"listbox\"` then\n * fails as a critical aria-required-children violation, while the same div with\n * no role passes straight through. Clearing it is what makes a ScrollArea legal\n * in a popup list. A spread because the literal `role={undefined}` is flagged by\n * the a11y lint rule, which cannot tell it from an invalid role.\n */\nconst NO_ROLE = { role: undefined } as const;\n\n/** What takes Tab inside a region: if any is there, the region is not a stop of its own. */\nconst FOCUSABLE_INSIDE =\n 'a[href], button:not(:disabled), input:not([type=\"hidden\"]):not(:disabled), select:not(:disabled), textarea:not(:disabled), summary, [contenteditable], [tabindex]:not([tabindex=\"-1\"])';\n\n/** Base UI makes the viewport a tab stop (0, or -1 with nothing to scroll); undefined takes that away. */\nconst NO_TAB_STOP = { tabIndex: undefined } as const;\n\nconst FADE_AXIS = {\n vertical: 'block',\n horizontal: 'inline',\n both: 'both',\n} as const;\n\n/**\n * Themed scroll container (Base UI): overflowing content scrolls INSIDE the component\n * with a slim overlay scrollbar instead of exposing a native browser scrollbar. House\n * rule: wide/tall content (tables, lists, code) always goes through this, never a bare\n * overflow-* utility.\n *\n * Cap the height with a plain class on the component itself, e.g.\n * `<ScrollArea className=\"max-h-88\">`. The viewport picks the cap up through\n * `max-h-inherit`, which matters because the viewport is the element that actually\n * scrolls: a cap that lands only on the root leaves tall content spilling out\n * underneath with no scrollbar, and that reads as a working height right up until the\n * content outgrows it.\n *\n * `fade` fades out each edge that is currently hiding content, rather than always\n * fading one: a permanent fade is a lie half the time, dimming the last row that fits\n * and promising more below when the reader is already at the end. It is cut into the\n * content by a mask (@edyn/theme/scroll-fade.css), so it needs no surface colour and\n * reads correctly over a tint, a gradient or a glow. A host that keeps something\n * unscrolled at the top, such as a sticky table header, exempts that band by setting\n * --scroll-fade-offset on the ScrollArea. The static faded-* utilities in\n * @edyn/theme/utilities.css are the other tool, for DECORATIVE edges that do not\n * track scroll position.\n *\n * The viewport publishes its size as --scroll-area-width/--scroll-area-height CSS vars\n * so children can size against the visible area.\n */\nexport const ScrollArea = ({\n className,\n children,\n orientation = 'vertical',\n type = 'hover',\n fade = false,\n fadeFrom = 'card',\n scrollbarClassName,\n viewportId,\n viewportRef,\n viewportClassName,\n scrollRestorationId,\n focusable = true,\n viewPortId,\n viewPortRef,\n viewPortClassName,\n scrollVerticalClassName,\n scrollHorizontalClassName,\n ...props\n}: IScrollAreaProps) => {\n const refInner = useRef<HTMLDivElement | null>(null);\n // The viewPort* spellings are the pre-rc.4 names, kept so existing callers\n // keep working; the viewport* ones match the rest of the ecosystem.\n const refsViewport = useMergedRefs(refInner, viewportRef ?? viewPortRef);\n\n const [holdsFocusable, setHoldsFocusable] = useState(false);\n\n useEffect(() => {\n const el = refInner.current;\n if (!el || !focusable) {\n return;\n }\n const read = () => setHoldsFocusable(el.querySelector(FOCUSABLE_INSIDE) !== null);\n read();\n const observer = new MutationObserver(read);\n observer.observe(el, { childList: true, subtree: true, attributes: true, attributeFilter: ['tabindex', 'disabled', 'href'] });\n return () => {\n observer.disconnect();\n };\n }, [focusable]);\n\n useEffect(() => {\n const el = refInner.current;\n if (!el) {\n return;\n }\n const root = el.parentElement;\n const content = el.firstElementChild;\n const observer = new ResizeObserver(() => {\n el.style.setProperty('--scroll-area-width', `${el.clientWidth}px`);\n el.style.setProperty('--scroll-area-height', `${el.clientHeight}px`);\n if (orientation !== 'vertical' && root && content instanceof HTMLElement) {\n // Content that fits the box but not with the bar's room under it would scroll for the bar: the bar overlays it instead.\n root.removeAttribute('data-bar-over');\n const style = getComputedStyle(root);\n const room = root.offsetHeight - content.offsetHeight;\n const bar = Number.parseFloat(style.getPropertyValue('--scroll-bar-size')) + Number.parseFloat(style.getPropertyValue('--scroll-bar-gap'));\n if (root.hasAttribute('data-has-overflow-x') && room >= 0 && room < bar) {\n root.setAttribute('data-bar-over', '');\n }\n }\n });\n observer.observe(el);\n if (orientation !== 'vertical' && root && content) {\n observer.observe(root);\n observer.observe(content);\n }\n return () => {\n observer.disconnect();\n };\n }, [orientation]);\n\n return (\n <BaseScrollArea.Root\n data-slot=\"scroll-area\"\n // Everything inside clips at this box, so a focus ring drawn outside an\n // element at the edge would be cut off; see @edyn/theme/focus.css.\n data-focus=\"inset\"\n // The single-row grid is load-bearing rather than tidying. `h-full` on the viewport\n // cannot be trusted to size it: a percentage height only resolves against a definite\n // containing block, and a host that sizes this by flex inside a max-height column\n // (`min-h-0 flex-1`) is not definite, so the viewport fell back to auto and grew to\n // its content instead of scrolling. A grid item stretches to its track, and the track\n // is sized from the root's used height, which flex has already settled. `grid-rows-1`\n // is `repeat(1, minmax(0, 1fr))`: one track that may shrink below its content.\n // While the content overflows sideways the root keeps the horizontal bar's height clear under the viewport, so the bar\n // stands below the last row. It is the root's padding, not the viewport's: padding inside the scroller made it scroll.\n // Where that room would only make content that fits scroll, the bar overlays the content instead (data-bar-over).\n className={cn(\n 'group/scroll-area relative grid min-w-0 grid-rows-1 overflow-hidden',\n orientation !== 'vertical' && 'data-has-overflow-x:not-data-bar-over:pb-(--scroll-bar-reserve)',\n className,\n )}\n data-fade={fade ? FADE_AXIS[orientation] : undefined}\n {...NO_ROLE}\n {...props}\n >\n {(orientation === 'vertical' || orientation === 'both') && (\n <ScrollBar orientation=\"vertical\" visibility={type} className={cn(scrollbarClassName, scrollVerticalClassName)} />\n )}\n {(orientation === 'horizontal' || orientation === 'both') && (\n <ScrollBar orientation=\"horizontal\" visibility={type} className={cn(scrollbarClassName, scrollHorizontalClassName)} />\n )}\n <BaseScrollArea.Corner />\n <BaseScrollArea.Viewport\n data-slot=\"scroll-area-viewport\"\n id={viewportId ?? viewPortId}\n ref={refsViewport}\n data-scroll-restoration-id={scrollRestorationId}\n {...NO_ROLE}\n {...(focusable && !holdsFocusable ? null : NO_TAB_STOP)}\n // One axis means one axis. Base UI leaves both axes scrollable, so a\n // vertical list could still be dragged sideways, and anything that put a\n // row in view (focus moving, scrollIntoView with inline: nearest) slid\n // the whole list across. The clamp is marked important because Base UI\n // sets the viewport's overflow itself.\n className={cn(\n 'max-h-inherit w-full rounded-[inherit]',\n orientation === 'vertical' && 'overflow-x-hidden!',\n orientation === 'horizontal' && 'overflow-y-hidden!',\n viewportClassName ?? viewPortClassName,\n )}\n >\n {/* Content is NOT optional: it carries the ResizeObserver that recomputes the\n thumb when the CONTENT resizes without the viewport resizing (e.g. a route\n swap to a shorter page). Without it the thumb keeps the previous page's\n geometry and a fully-visible page still shows a scrollbar.\n\n A vertical scroller also pins its content to the viewport's width, so a\n long label truncates instead of widening the list and inventing a\n horizontal scroll. Base UI sets min-width: fit-content inline, which is\n what makes that happen and why this is an inline style too. */}\n <BaseScrollArea.Content {...NO_ROLE} style={orientation === 'vertical' ? { minWidth: 0 } : undefined}>\n {children}\n </BaseScrollArea.Content>\n </BaseScrollArea.Viewport>\n </BaseScrollArea.Root>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scroll-area.js","names":[],"sources":["../../../../../../../ui/src/components/ui/scroll-area/scroll-area.tsx"],"sourcesContent":["import { ScrollArea as BaseScrollArea } from '@base-ui/react/scroll-area';\nimport { useHoldsFocusable } from '@edyn/ui/hooks/use-holds-focusable';\nimport { useMergedRefs } from '@edyn/ui/hooks/use-merged-refs';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { type ComponentProps, type RefObject, useEffect, useRef } from 'react';\n\n/**\n * When the bar shows: 'hover' on area hover or while scrolling (default),\n * 'scroll' only while scrolling, 'always' permanent. In every mode the bar is\n * mounted only while that axis actually overflows, so 'always' means \"always\n * when there is something to scroll\", never a bar on a list that fits.\n */\nexport type TScrollbarVisibility = 'hover' | 'scroll' | 'always';\n\n/**\n * Surface the edge fades blended into, back when they were painted.\n *\n * @deprecated The fades are cut into the content by a mask, so they match any\n * surface without being told which one. Still accepted, and ignored.\n */\nexport type TScrollAreaFadeFrom = 'card' | 'background' | 'sidebar' | 'popover';\n\nexport interface IScrollBarProps extends ComponentProps<typeof BaseScrollArea.Scrollbar> {\n visibility?: TScrollbarVisibility;\n}\n\nexport const ScrollBar = ({ className, orientation = 'vertical', visibility = 'hover', ...props }: IScrollBarProps) => {\n return (\n <BaseScrollArea.Scrollbar\n data-slot=\"scroll-area-scrollbar\"\n orientation={orientation}\n className={cn(\n 'peer/scrollbar z-100 flex touch-none select-none motion-safe:transition-opacity motion-safe:duration-150',\n // Size, gap and start read --scroll-bar-size, --scroll-bar-gap and --scroll-bar-start (see scroll-bar.css), whose\n // defaults are the 8px bar flush with the box that it has always been.\n orientation === 'vertical' &&\n 'h-(--scroll-bar-length) w-(--scroll-bar-size) me-(--scroll-bar-gap) mt-(--scroll-bar-offset-top) border-l border-l-transparent p-px',\n orientation === 'horizontal' &&\n 'h-(--scroll-bar-size) ms-(--scroll-bar-gap) me-(--scroll-bar-gap) mb-(--scroll-bar-gap) flex-col border-t border-t-transparent p-px',\n visibility !== 'always' && 'opacity-0 data-scrolling:opacity-100',\n visibility === 'hover' && 'data-hovering:opacity-100 group-hover/scroll-area:opacity-100',\n className,\n )}\n {...props}\n >\n <BaseScrollArea.Thumb className=\"relative flex-1 rounded-full bg-muted-foreground/50 transition-colors hover:bg-muted-foreground/70 data-scrolling:bg-muted-foreground/70\" />\n </BaseScrollArea.Scrollbar>\n );\n};\n\nexport interface IScrollAreaProps extends ComponentProps<typeof BaseScrollArea.Root> {\n /** Which axes get a styled scrollbar; content decides what actually overflows. */\n orientation?: 'horizontal' | 'vertical' | 'both';\n /** Scrollbar reveal behavior (mirrors the Radix ScrollArea `type` prop). */\n type?: TScrollbarVisibility;\n /** Edge fades hinting at clipped content; each one shows only while its own edge hides something. */\n fade?: boolean;\n /**\n * @deprecated No longer read. The fades mask the content, so they sit correctly\n * on any surface; passing this does nothing and is safe to delete.\n */\n fadeFrom?: TScrollAreaFadeFrom;\n /** Places both bars, for a host that gives them a gutter of their own (a sidebar menu). */\n scrollbarClassName?: string;\n viewportId?: string;\n /** Access to the scrollable viewport (e.g. to reset scroll position on route changes). */\n viewportRef?: RefObject<HTMLDivElement | null>;\n viewportClassName?: string;\n /** Published on the viewport as data-scroll-restoration-id, for a router that restores scroll position. */\n scrollRestorationId?: string;\n /**\n * Whether the viewport may be a tab stop (default). It is one only while it overflows and nothing inside can take Tab\n * (axe's scrollable-region-focusable), with a 1px keyboard-only line. Off inside a menu, whose items are the tab stops.\n */\n focusable?: boolean;\n /** @deprecated Use `viewportId`. */\n viewPortId?: string;\n /** @deprecated Use `viewportRef`. */\n viewPortRef?: RefObject<HTMLDivElement | null>;\n /** @deprecated Use `viewportClassName`. */\n viewPortClassName?: string;\n /** Places the vertical bar only; `scrollbarClassName` covers both. */\n scrollVerticalClassName?: string;\n /** Places the horizontal bar only; `scrollbarClassName` covers both. */\n scrollHorizontalClassName?: string;\n}\n\n/**\n * Which axes fade, published for scroll-fade.css to key its mask off. The\n * stylesheet reaches this element's own viewport through a child combinator, so a\n * ScrollArea nested in another one cannot pick up its ancestor's edges.\n */\n/** Base UI's explicit `role=\"presentation\"` on the parts fails axe's aria-required-children inside a menu or listbox; clearing it does not. */\nconst NO_ROLE = { role: undefined } as const;\n\n/** Base UI makes the viewport a tab stop (0, or -1 with nothing to scroll); undefined takes that away. */\nconst NO_TAB_STOP = { tabIndex: undefined } as const;\n\nconst FADE_AXIS = {\n vertical: 'block',\n horizontal: 'inline',\n both: 'both',\n} as const;\n\n/**\n * Themed scroll container (Base UI): overflowing content scrolls INSIDE the component\n * with a slim overlay scrollbar instead of exposing a native browser scrollbar. House\n * rule: wide/tall content (tables, lists, code) always goes through this, never a bare\n * overflow-* utility.\n *\n * Cap the height with a plain class on the component itself, e.g.\n * `<ScrollArea className=\"max-h-88\">`. The viewport picks the cap up through\n * `max-h-inherit`, which matters because the viewport is the element that actually\n * scrolls: a cap that lands only on the root leaves tall content spilling out\n * underneath with no scrollbar, and that reads as a working height right up until the\n * content outgrows it.\n *\n * `fade` fades out each edge that is currently hiding content, rather than always\n * fading one: a permanent fade is a lie half the time, dimming the last row that fits\n * and promising more below when the reader is already at the end. It is cut into the\n * content by a mask (@edyn/theme/scroll-fade.css), so it needs no surface colour and\n * reads correctly over a tint, a gradient or a glow. A host that keeps something\n * unscrolled at the top, such as a sticky table header, exempts that band by setting\n * --scroll-fade-offset on the ScrollArea. The static faded-* utilities in\n * @edyn/theme/utilities.css are the other tool, for DECORATIVE edges that do not\n * track scroll position.\n *\n * The viewport publishes its size as --scroll-area-width/--scroll-area-height CSS vars\n * so children can size against the visible area.\n */\nexport const ScrollArea = ({\n className,\n children,\n orientation = 'vertical',\n type = 'hover',\n fade = false,\n fadeFrom = 'card',\n scrollbarClassName,\n viewportId,\n viewportRef,\n viewportClassName,\n scrollRestorationId,\n focusable = true,\n viewPortId,\n viewPortRef,\n viewPortClassName,\n scrollVerticalClassName,\n scrollHorizontalClassName,\n ...props\n}: IScrollAreaProps) => {\n const refInner = useRef<HTMLDivElement | null>(null);\n // The viewPort* spellings are the pre-rc.4 names, kept so existing callers\n // keep working; the viewport* ones match the rest of the ecosystem.\n const refsViewport = useMergedRefs(refInner, viewportRef ?? viewPortRef);\n\n const holdsFocusable = useHoldsFocusable(refInner, focusable);\n\n useEffect(() => {\n const el = refInner.current;\n if (!el) {\n return;\n }\n const root = el.parentElement;\n const content = el.firstElementChild;\n const observer = new ResizeObserver(() => {\n el.style.setProperty('--scroll-area-width', `${el.clientWidth}px`);\n el.style.setProperty('--scroll-area-height', `${el.clientHeight}px`);\n if (orientation !== 'vertical' && root && content instanceof HTMLElement) {\n // Content that fits the box but not with the bar's room under it would scroll for the bar: the bar overlays it instead.\n root.removeAttribute('data-bar-over');\n const style = getComputedStyle(root);\n const room = root.offsetHeight - content.offsetHeight;\n const bar = Number.parseFloat(style.getPropertyValue('--scroll-bar-size')) + Number.parseFloat(style.getPropertyValue('--scroll-bar-gap'));\n if (root.hasAttribute('data-has-overflow-x') && room >= 0 && room < bar) {\n root.setAttribute('data-bar-over', '');\n }\n }\n });\n observer.observe(el);\n if (orientation !== 'vertical' && root && content) {\n observer.observe(root);\n observer.observe(content);\n }\n return () => {\n observer.disconnect();\n };\n }, [orientation]);\n\n return (\n <BaseScrollArea.Root\n data-slot=\"scroll-area\"\n // Everything inside clips at this box, so a focus ring drawn outside an\n // element at the edge would be cut off; see @edyn/theme/focus.css.\n data-focus=\"inset\"\n // A single-row grid sizes the viewport from the root's used height, which `h-full` cannot do in a flex column; the bar's\n // room is the root's padding, and data-bar-over overlays the bar where that room would only make content scroll.\n className={cn(\n 'group/scroll-area relative grid min-w-0 grid-rows-1 overflow-hidden',\n orientation !== 'vertical' && 'data-has-overflow-x:not-data-bar-over:pb-(--scroll-bar-reserve)',\n className,\n )}\n data-fade={fade ? FADE_AXIS[orientation] : undefined}\n {...NO_ROLE}\n {...props}\n >\n {(orientation === 'vertical' || orientation === 'both') && (\n <ScrollBar orientation=\"vertical\" visibility={type} className={cn(scrollbarClassName, scrollVerticalClassName)} />\n )}\n {(orientation === 'horizontal' || orientation === 'both') && (\n <ScrollBar orientation=\"horizontal\" visibility={type} className={cn(scrollbarClassName, scrollHorizontalClassName)} />\n )}\n <BaseScrollArea.Corner />\n <BaseScrollArea.Viewport\n data-slot=\"scroll-area-viewport\"\n id={viewportId ?? viewPortId}\n ref={refsViewport}\n data-scroll-restoration-id={scrollRestorationId}\n {...NO_ROLE}\n {...(focusable && !holdsFocusable ? null : NO_TAB_STOP)}\n // One axis means one axis. Base UI leaves both axes scrollable, so a\n // vertical list could still be dragged sideways, and anything that put a\n // row in view (focus moving, scrollIntoView with inline: nearest) slid\n // the whole list across. The clamp is marked important because Base UI\n // sets the viewport's overflow itself.\n className={cn(\n 'max-h-inherit w-full rounded-[inherit]',\n orientation === 'vertical' && 'overflow-x-hidden!',\n orientation === 'horizontal' && 'overflow-y-hidden!',\n viewportClassName ?? viewPortClassName,\n )}\n >\n {/* Content is NOT optional: it carries the ResizeObserver that recomputes the\n thumb when the CONTENT resizes without the viewport resizing (e.g. a route\n swap to a shorter page). Without it the thumb keeps the previous page's\n geometry and a fully-visible page still shows a scrollbar.\n\n A vertical scroller also pins its content to the viewport's width, so a\n long label truncates instead of widening the list and inventing a\n horizontal scroll. Base UI sets min-width: fit-content inline, which is\n what makes that happen and why this is an inline style too. */}\n <BaseScrollArea.Content {...NO_ROLE} style={orientation === 'vertical' ? { minWidth: 0 } : undefined}>\n {children}\n </BaseScrollArea.Content>\n </BaseScrollArea.Viewport>\n </BaseScrollArea.Root>\n );\n};\n"],"mappings":"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"}
|
|
@@ -19,7 +19,11 @@ export interface ITabsListProps extends VariantProps<typeof tabsListVariants>, O
|
|
|
19
19
|
className?: string;
|
|
20
20
|
children: React.ReactNode;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* A row of tabs wider than its box scrolls sideways inside it, with a fade at each edge that hides tabs, and the selected tab
|
|
24
|
+
* (and the one holding focus) is scrolled into view. A vertical list is not wrapped.
|
|
25
|
+
*/
|
|
26
|
+
export declare const TabsList: ({ className, variant, children, ref, ...props }: ITabsListProps) => React.JSX.Element;
|
|
23
27
|
export interface ITabsTriggerProps extends Omit<BaseTabs.Tab.Props, 'value' | 'disabled' | 'className' | 'children'> {
|
|
24
28
|
value: string;
|
|
25
29
|
disabled?: boolean;
|
|
@@ -33,4 +37,5 @@ export interface ITabsContentProps extends Omit<BaseTabs.Panel.Props, 'value' |
|
|
|
33
37
|
className?: string;
|
|
34
38
|
children: React.ReactNode;
|
|
35
39
|
}
|
|
36
|
-
|
|
40
|
+
/** A panel takes Tab only when nothing inside it can, and then shows a 1px line inside its edge (focus.css). */
|
|
41
|
+
export declare const TabsContent: ({ value, keepMounted, className, children, ref, ...props }: ITabsContentProps) => React.JSX.Element;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { cn } from "../../../lib/utils.js";
|
|
2
|
+
import { useHoldsFocusable } from "../../../hooks/use-holds-focusable.js";
|
|
3
|
+
import { useMergedRefs } from "../../../hooks/use-merged-refs.js";
|
|
4
|
+
import { ScrollArea } from "../scroll-area/scroll-area.js";
|
|
5
|
+
import { createContext, useContext, useEffect, useRef } from "react";
|
|
2
6
|
import { jsx } from "react/jsx-runtime";
|
|
3
7
|
import { cva } from "class-variance-authority";
|
|
4
8
|
import { Tabs } from "@base-ui/react/tabs";
|
|
5
9
|
//#region ../ui/src/components/ui/tabs/tabs.tsx
|
|
10
|
+
var OrientationContext = createContext("horizontal");
|
|
6
11
|
var Tabs$1 = ({ value, defaultValue, onValueChange, className, orientation = "horizontal", children, ...props }) => /* @__PURE__ */ jsx(Tabs.Root, {
|
|
7
12
|
"data-slot": "tabs",
|
|
8
13
|
...props,
|
|
@@ -14,22 +19,87 @@ var Tabs$1 = ({ value, defaultValue, onValueChange, className, orientation = "ho
|
|
|
14
19
|
onValueChange?.(String(next));
|
|
15
20
|
},
|
|
16
21
|
className: cn("group/tabs flex w-full gap-2 data-[orientation=horizontal]:flex-col", className),
|
|
17
|
-
children
|
|
22
|
+
children: /* @__PURE__ */ jsx(OrientationContext, {
|
|
23
|
+
value: orientation,
|
|
24
|
+
children
|
|
25
|
+
})
|
|
18
26
|
});
|
|
19
|
-
var tabsListVariants = cva("group/tabs-list
|
|
27
|
+
var tabsListVariants = cva("group/tabs-list flex w-fit items-center group-data-[orientation=horizontal]/tabs:min-w-max justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-8 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none", {
|
|
20
28
|
variants: { variant: {
|
|
21
29
|
default: "bg-muted",
|
|
22
30
|
line: "gap-1 bg-transparent"
|
|
23
31
|
} },
|
|
24
32
|
defaultVariants: { variant: "default" }
|
|
25
33
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
/** Room kept between a tab and the edge it is scrolled to: the 24px edge fade and a little more. */
|
|
35
|
+
var REVEAL_MARGIN = 32;
|
|
36
|
+
/** Scrolls a list sideways, inside its viewport only, until a tab stands clear of both edge fades. */
|
|
37
|
+
var reveal = (viewport, tab) => {
|
|
38
|
+
if (!tab) return;
|
|
39
|
+
const edge = viewport.getBoundingClientRect();
|
|
40
|
+
const box = tab.getBoundingClientRect();
|
|
41
|
+
if (box.left < edge.left + REVEAL_MARGIN) viewport.scrollBy({ left: box.left - edge.left - REVEAL_MARGIN });
|
|
42
|
+
else if (box.right > edge.right - REVEAL_MARGIN) viewport.scrollBy({ left: box.right - edge.right + REVEAL_MARGIN });
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* A row of tabs wider than its box scrolls sideways inside it, with a fade at each edge that hides tabs, and the selected tab
|
|
46
|
+
* (and the one holding focus) is scrolled into view. A vertical list is not wrapped.
|
|
47
|
+
*/
|
|
48
|
+
var TabsList = ({ className, variant = "default", children, ref, ...props }) => {
|
|
49
|
+
const horizontal = useContext(OrientationContext) === "horizontal";
|
|
50
|
+
const refViewport = useRef(null);
|
|
51
|
+
const refList = useRef(null);
|
|
52
|
+
const refs = useMergedRefs(refList, ref);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const viewport = refViewport.current;
|
|
55
|
+
const list = refList.current;
|
|
56
|
+
if (!horizontal || !viewport || !list) return;
|
|
57
|
+
const revealSelected = () => reveal(viewport, list.querySelector("[aria-selected=\"true\"]"));
|
|
58
|
+
const revealFocused = (event) => reveal(viewport, event.target.closest("[role=\"tab\"]"));
|
|
59
|
+
let reader = false;
|
|
60
|
+
const take = () => {
|
|
61
|
+
reader = true;
|
|
62
|
+
};
|
|
63
|
+
const sizes = new ResizeObserver(() => reader || revealSelected());
|
|
64
|
+
const events = [
|
|
65
|
+
"wheel",
|
|
66
|
+
"pointerdown",
|
|
67
|
+
"keydown",
|
|
68
|
+
"touchstart"
|
|
69
|
+
];
|
|
70
|
+
revealSelected();
|
|
71
|
+
sizes.observe(list);
|
|
72
|
+
sizes.observe(viewport);
|
|
73
|
+
for (const name of events) viewport.addEventListener(name, take, { passive: true });
|
|
74
|
+
const observer = new MutationObserver(revealSelected);
|
|
75
|
+
observer.observe(list, {
|
|
76
|
+
subtree: true,
|
|
77
|
+
attributes: true,
|
|
78
|
+
attributeFilter: ["aria-selected"]
|
|
79
|
+
});
|
|
80
|
+
list.addEventListener("focusin", revealFocused);
|
|
81
|
+
return () => {
|
|
82
|
+
observer.disconnect();
|
|
83
|
+
sizes.disconnect();
|
|
84
|
+
for (const name of events) viewport.removeEventListener(name, take);
|
|
85
|
+
list.removeEventListener("focusin", revealFocused);
|
|
86
|
+
};
|
|
87
|
+
}, [horizontal]);
|
|
88
|
+
const list = /* @__PURE__ */ jsx(Tabs.List, {
|
|
89
|
+
"data-slot": "tabs-list",
|
|
90
|
+
...props,
|
|
91
|
+
"data-variant": variant,
|
|
92
|
+
ref: refs,
|
|
93
|
+
className: cn(tabsListVariants({ variant }), className),
|
|
94
|
+
children
|
|
95
|
+
});
|
|
96
|
+
return horizontal ? /* @__PURE__ */ jsx(ScrollArea, {
|
|
97
|
+
orientation: "horizontal",
|
|
98
|
+
fade: true,
|
|
99
|
+
viewportRef: refViewport,
|
|
100
|
+
children: list
|
|
101
|
+
}) : list;
|
|
102
|
+
};
|
|
33
103
|
var TabsTrigger = ({ value, disabled, className, children, ...props }) => /* @__PURE__ */ jsx(Tabs.Tab, {
|
|
34
104
|
...props,
|
|
35
105
|
value,
|
|
@@ -37,13 +107,24 @@ var TabsTrigger = ({ value, disabled, className, children, ...props }) => /* @__
|
|
|
37
107
|
className: cn("relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\"size-\"])]:size-4", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", "data-active:bg-background data-active:text-foreground dark:data-active:border-control dark:data-active:bg-input/30 dark:data-active:text-foreground", "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100", className),
|
|
38
108
|
children
|
|
39
109
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
110
|
+
/** Base UI makes every open panel a tab stop; undefined takes that away. */
|
|
111
|
+
var NO_TAB_STOP = { tabIndex: void 0 };
|
|
112
|
+
/** A panel takes Tab only when nothing inside it can, and then shows a 1px line inside its edge (focus.css). */
|
|
113
|
+
var TabsContent = ({ value, keepMounted = false, className, children, ref, ...props }) => {
|
|
114
|
+
const refPanel = useRef(null);
|
|
115
|
+
const refs = useMergedRefs(refPanel, ref);
|
|
116
|
+
const holdsFocusable = useHoldsFocusable(refPanel);
|
|
117
|
+
return /* @__PURE__ */ jsx(Tabs.Panel, {
|
|
118
|
+
"data-slot": "tabs-content",
|
|
119
|
+
...holdsFocusable ? NO_TAB_STOP : null,
|
|
120
|
+
...props,
|
|
121
|
+
ref: refs,
|
|
122
|
+
value,
|
|
123
|
+
keepMounted,
|
|
124
|
+
className: cn("flex-1 text-sm outline-none", className),
|
|
125
|
+
children
|
|
126
|
+
});
|
|
127
|
+
};
|
|
47
128
|
//#endregion
|
|
48
129
|
export { Tabs$1 as Tabs, TabsContent, TabsList, TabsTrigger, tabsListVariants };
|
|
49
130
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs.js","names":[],"sources":["../../../../../../../ui/src/components/ui/tabs/tabs.tsx"],"sourcesContent":["import { Tabs as BaseTabs } from '@base-ui/react/tabs';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport type * as React from 'react';\n\nexport interface ITabsProps extends Omit<BaseTabs.Root.Props, 'value' | 'defaultValue' | 'onValueChange' | 'className' | 'orientation' | 'children'> {\n /** Controlled selected tab. Provide with `onValueChange`. */\n value?: string;\n /** Uncontrolled initial tab. Use this when the component should own its state. */\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n className?: string;\n orientation?: 'horizontal' | 'vertical';\n children: React.ReactNode;\n}\n\nexport const Tabs = ({ value, defaultValue, onValueChange, className, orientation = 'horizontal', children, ...props }: ITabsProps) => (\n <BaseTabs.Root\n data-slot=\"tabs\"\n {...props}\n data-orientation={orientation}\n value={value}\n defaultValue={defaultValue}\n orientation={orientation}\n onValueChange={(next) => {\n onValueChange?.(String(next));\n }}\n className={cn('group/tabs flex w-full gap-2 data-[orientation=horizontal]:flex-col', className)}\n >\n {children}\n </BaseTabs.Root>\n);\n\nexport const tabsListVariants = cva(\n 'group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-8 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none',\n {\n variants: {\n variant: {\n default: 'bg-muted',\n line: 'gap-1 bg-transparent',\n },\n },\n defaultVariants: {\n variant: 'default',\n },\n },\n);\n\nexport interface ITabsListProps extends VariantProps<typeof tabsListVariants>, Omit<BaseTabs.List.Props, 'className' | 'children'> {\n className?: string;\n children: React.ReactNode;\n}\n\nexport const TabsList = ({ className, variant = 'default', children, ...props }: ITabsListProps) => (\n <BaseTabs.List data-slot=\"tabs-list\" {...props} data-variant={variant} className={cn(tabsListVariants({ variant }), className)}>\n {children}\n </BaseTabs.List>\n);\n\nexport interface ITabsTriggerProps extends Omit<BaseTabs.Tab.Props, 'value' | 'disabled' | 'className' | 'children'> {\n value: string;\n disabled?: boolean;\n className?: string;\n children: React.ReactNode;\n}\n\nexport const TabsTrigger = ({ value, disabled, className, children, ...props }: ITabsTriggerProps) => (\n <BaseTabs.Tab\n {...props}\n value={value}\n disabled={disabled}\n className={cn(\n 'relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\"size-\"])]:size-4',\n 'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',\n 'data-active:bg-background data-active:text-foreground dark:data-active:border-control dark:data-active:bg-input/30 dark:data-active:text-foreground',\n 'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',\n className,\n )}\n >\n {children}\n </BaseTabs.Tab>\n);\n\nexport interface ITabsContentProps extends Omit<BaseTabs.Panel.Props, 'value' | 'keepMounted' | 'className' | 'children'> {\n value: string;\n keepMounted?: boolean;\n className?: string;\n children: React.ReactNode;\n}\n\nexport const TabsContent = ({ value, keepMounted = false, className, children, ...props }: ITabsContentProps) => (\n <BaseTabs.Panel {...props} value={value} keepMounted={keepMounted} className={cn('flex-1 text-sm outline-none', className)}>\n {children}\n </BaseTabs.Panel>\n);\n"],"mappings":";;;;;AAgBA,IAAa,UAAQ,EAAE,OAAO,cAAc,eAAe,WAAW,cAAc,cAAc,UAAU,GAAG,YAC7G,oBAAC,KAAS,MAAV;CACE,aAAU;CACV,GAAI;CACJ,oBAAkB;CACX;CACO;CACD;CACb,gBAAgB,SAAS;EACvB,gBAAgB,OAAO,IAAI,CAAC;CAC9B;CACA,WAAW,GAAG,uEAAuE,SAAS;CAE7F;AACY,CAAA;AAGjB,IAAa,mBAAmB,IAC9B,qRACA;CACE,UAAU,EACR,SAAS;EACP,SAAS;EACT,MAAM;CACR,EACF;CACA,iBAAiB,EACf,SAAS,UACX;AACF,CACF;AAOA,IAAa,YAAY,EAAE,WAAW,UAAU,WAAW,UAAU,GAAG,YACtE,oBAAC,KAAS,MAAV;CAAe,aAAU;CAAY,GAAI;CAAO,gBAAc;CAAS,WAAW,GAAG,iBAAiB,EAAE,QAAQ,CAAC,GAAG,SAAS;CAC1H;AACY,CAAA;AAUjB,IAAa,eAAe,EAAE,OAAO,UAAU,WAAW,UAAU,GAAG,YACrE,oBAAC,KAAS,KAAV;CACE,GAAI;CACG;CACG;CACV,WAAW,GACT,irBACA,iQACA,uJACA,4dACA,SACF;CAEC;AACW,CAAA;AAUhB,IAAa,eAAe,EAAE,OAAO,cAAc,OAAO,WAAW,UAAU,GAAG,YAChF,oBAAC,KAAS,OAAV;CAAgB,GAAI;CAAc;CAAoB;CAAa,WAAW,GAAG,+BAA+B,SAAS;CACtH;AACa,CAAA"}
|
|
1
|
+
{"version":3,"file":"tabs.js","names":[],"sources":["../../../../../../../ui/src/components/ui/tabs/tabs.tsx"],"sourcesContent":["import { Tabs as BaseTabs } from '@base-ui/react/tabs';\nimport { ScrollArea } from '@edyn/ui/components/ui/scroll-area';\nimport { useHoldsFocusable } from '@edyn/ui/hooks/use-holds-focusable';\nimport { useMergedRefs } from '@edyn/ui/hooks/use-merged-refs';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport type * as React from 'react';\nimport { createContext, useContext, useEffect, useRef } from 'react';\n\nexport interface ITabsProps extends Omit<BaseTabs.Root.Props, 'value' | 'defaultValue' | 'onValueChange' | 'className' | 'orientation' | 'children'> {\n /** Controlled selected tab. Provide with `onValueChange`. */\n value?: string;\n /** Uncontrolled initial tab. Use this when the component should own its state. */\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n className?: string;\n orientation?: 'horizontal' | 'vertical';\n children: React.ReactNode;\n}\n\nconst OrientationContext = createContext<'horizontal' | 'vertical'>('horizontal');\n\nexport const Tabs = ({ value, defaultValue, onValueChange, className, orientation = 'horizontal', children, ...props }: ITabsProps) => (\n <BaseTabs.Root\n data-slot=\"tabs\"\n {...props}\n data-orientation={orientation}\n value={value}\n defaultValue={defaultValue}\n orientation={orientation}\n onValueChange={(next) => {\n onValueChange?.(String(next));\n }}\n className={cn('group/tabs flex w-full gap-2 data-[orientation=horizontal]:flex-col', className)}\n >\n <OrientationContext value={orientation}>{children}</OrientationContext>\n </BaseTabs.Root>\n);\n\nexport const tabsListVariants = cva(\n 'group/tabs-list flex w-fit items-center group-data-[orientation=horizontal]/tabs:min-w-max justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-8 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none',\n {\n variants: {\n variant: {\n default: 'bg-muted',\n line: 'gap-1 bg-transparent',\n },\n },\n defaultVariants: {\n variant: 'default',\n },\n },\n);\n\nexport interface ITabsListProps extends VariantProps<typeof tabsListVariants>, Omit<BaseTabs.List.Props, 'className' | 'children'> {\n className?: string;\n children: React.ReactNode;\n}\n\n/** Room kept between a tab and the edge it is scrolled to: the 24px edge fade and a little more. */\nconst REVEAL_MARGIN = 32;\n\n/** Scrolls a list sideways, inside its viewport only, until a tab stands clear of both edge fades. */\nconst reveal = (viewport: HTMLElement, tab: Element | null) => {\n if (!tab) {\n return;\n }\n const edge = viewport.getBoundingClientRect();\n const box = tab.getBoundingClientRect();\n if (box.left < edge.left + REVEAL_MARGIN) {\n viewport.scrollBy({ left: box.left - edge.left - REVEAL_MARGIN });\n } else if (box.right > edge.right - REVEAL_MARGIN) {\n viewport.scrollBy({ left: box.right - edge.right + REVEAL_MARGIN });\n }\n};\n\n/**\n * A row of tabs wider than its box scrolls sideways inside it, with a fade at each edge that hides tabs, and the selected tab\n * (and the one holding focus) is scrolled into view. A vertical list is not wrapped.\n */\nexport const TabsList = ({ className, variant = 'default', children, ref, ...props }: ITabsListProps) => {\n const horizontal = useContext(OrientationContext) === 'horizontal';\n const refViewport = useRef<HTMLDivElement | null>(null);\n const refList = useRef<HTMLDivElement | null>(null);\n const refs = useMergedRefs(refList, ref);\n\n useEffect(() => {\n const viewport = refViewport.current;\n const list = refList.current;\n if (!horizontal || !viewport || !list) {\n return;\n }\n const revealSelected = () => reveal(viewport, list.querySelector('[aria-selected=\"true\"]'));\n const revealFocused = (event: FocusEvent) => reveal(viewport, (event.target as Element).closest('[role=\"tab\"]'));\n // Until the reader scrolls, a change of size (a font, a data load) keeps the chosen tab clear of the fades.\n let reader = false;\n const take = () => {\n reader = true;\n };\n const sizes = new ResizeObserver(() => reader || revealSelected());\n const events = ['wheel', 'pointerdown', 'keydown', 'touchstart'] as const;\n revealSelected();\n sizes.observe(list);\n sizes.observe(viewport);\n for (const name of events) {\n viewport.addEventListener(name, take, { passive: true });\n }\n const observer = new MutationObserver(revealSelected);\n observer.observe(list, { subtree: true, attributes: true, attributeFilter: ['aria-selected'] });\n list.addEventListener('focusin', revealFocused);\n return () => {\n observer.disconnect();\n sizes.disconnect();\n for (const name of events) {\n viewport.removeEventListener(name, take);\n }\n list.removeEventListener('focusin', revealFocused);\n };\n }, [horizontal]);\n\n const list = (\n <BaseTabs.List data-slot=\"tabs-list\" {...props} data-variant={variant} ref={refs} className={cn(tabsListVariants({ variant }), className)}>\n {children}\n </BaseTabs.List>\n );\n return horizontal ? (\n <ScrollArea orientation=\"horizontal\" fade viewportRef={refViewport}>\n {list}\n </ScrollArea>\n ) : (\n list\n );\n};\n\nexport interface ITabsTriggerProps extends Omit<BaseTabs.Tab.Props, 'value' | 'disabled' | 'className' | 'children'> {\n value: string;\n disabled?: boolean;\n className?: string;\n children: React.ReactNode;\n}\n\nexport const TabsTrigger = ({ value, disabled, className, children, ...props }: ITabsTriggerProps) => (\n <BaseTabs.Tab\n {...props}\n value={value}\n disabled={disabled}\n className={cn(\n 'relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\"size-\"])]:size-4',\n 'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',\n 'data-active:bg-background data-active:text-foreground dark:data-active:border-control dark:data-active:bg-input/30 dark:data-active:text-foreground',\n 'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',\n className,\n )}\n >\n {children}\n </BaseTabs.Tab>\n);\n\nexport interface ITabsContentProps extends Omit<BaseTabs.Panel.Props, 'value' | 'keepMounted' | 'className' | 'children'> {\n value: string;\n keepMounted?: boolean;\n className?: string;\n children: React.ReactNode;\n}\n\n/** Base UI makes every open panel a tab stop; undefined takes that away. */\nconst NO_TAB_STOP = { tabIndex: undefined } as const;\n\n/** A panel takes Tab only when nothing inside it can, and then shows a 1px line inside its edge (focus.css). */\nexport const TabsContent = ({ value, keepMounted = false, className, children, ref, ...props }: ITabsContentProps) => {\n const refPanel = useRef<HTMLDivElement | null>(null);\n const refs = useMergedRefs(refPanel, ref);\n const holdsFocusable = useHoldsFocusable(refPanel);\n return (\n <BaseTabs.Panel\n data-slot=\"tabs-content\"\n {...(holdsFocusable ? NO_TAB_STOP : null)}\n {...props}\n ref={refs}\n value={value}\n keepMounted={keepMounted}\n className={cn('flex-1 text-sm outline-none', className)}\n >\n {children}\n </BaseTabs.Panel>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
//#region ../ui/src/hooks/use-holds-focusable.ts
|
|
3
|
+
/** What takes Tab inside a region: if any is there, the region is not a stop of its own. */
|
|
4
|
+
var FOCUSABLE_INSIDE = "a[href], button:not(:disabled), input:not([type=\"hidden\"]):not(:disabled), select:not(:disabled), textarea:not(:disabled), summary, [contenteditable], [tabindex]:not([tabindex=\"-1\"])";
|
|
5
|
+
/** Whether something inside the element can take Tab, kept current as its content changes. */
|
|
6
|
+
var useHoldsFocusable = (ref, enabled = true) => {
|
|
7
|
+
const [holds, setHolds] = useState(false);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const el = ref.current;
|
|
10
|
+
if (!el || !enabled) return;
|
|
11
|
+
const read = () => setHolds(el.querySelector(FOCUSABLE_INSIDE) !== null);
|
|
12
|
+
read();
|
|
13
|
+
const observer = new MutationObserver(read);
|
|
14
|
+
observer.observe(el, {
|
|
15
|
+
childList: true,
|
|
16
|
+
subtree: true,
|
|
17
|
+
attributes: true,
|
|
18
|
+
attributeFilter: [
|
|
19
|
+
"tabindex",
|
|
20
|
+
"disabled",
|
|
21
|
+
"href"
|
|
22
|
+
]
|
|
23
|
+
});
|
|
24
|
+
return () => {
|
|
25
|
+
observer.disconnect();
|
|
26
|
+
};
|
|
27
|
+
}, [ref, enabled]);
|
|
28
|
+
return holds;
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { useHoldsFocusable };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=use-holds-focusable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-holds-focusable.js","names":[],"sources":["../../../../../ui/src/hooks/use-holds-focusable.ts"],"sourcesContent":["import { type RefObject, useEffect, useState } from 'react';\n\n/** What takes Tab inside a region: if any is there, the region is not a stop of its own. */\nconst FOCUSABLE_INSIDE =\n 'a[href], button:not(:disabled), input:not([type=\"hidden\"]):not(:disabled), select:not(:disabled), textarea:not(:disabled), summary, [contenteditable], [tabindex]:not([tabindex=\"-1\"])';\n\n/** Whether something inside the element can take Tab, kept current as its content changes. */\nexport const useHoldsFocusable = (ref: RefObject<HTMLElement | null>, enabled = true): boolean => {\n const [holds, setHolds] = useState(false);\n\n useEffect(() => {\n const el = ref.current;\n if (!el || !enabled) {\n return;\n }\n const read = () => setHolds(el.querySelector(FOCUSABLE_INSIDE) !== null);\n read();\n const observer = new MutationObserver(read);\n observer.observe(el, { childList: true, subtree: true, attributes: true, attributeFilter: ['tabindex', 'disabled', 'href'] });\n return () => {\n observer.disconnect();\n };\n }, [ref, enabled]);\n\n return holds;\n};\n"],"mappings":";;;AAGA,IAAM,mBACJ;;AAGF,IAAa,qBAAqB,KAAoC,UAAU,SAAkB;CAChG,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CAExC,gBAAgB;EACd,MAAM,KAAK,IAAI;EACf,IAAI,CAAC,MAAM,CAAC,SACV;EAEF,MAAM,aAAa,SAAS,GAAG,cAAc,gBAAgB,MAAM,IAAI;EACvE,KAAK;EACL,MAAM,WAAW,IAAI,iBAAiB,IAAI;EAC1C,SAAS,QAAQ,IAAI;GAAE,WAAW;GAAM,SAAS;GAAM,YAAY;GAAM,iBAAiB;IAAC;IAAY;IAAY;GAAM;EAAE,CAAC;EAC5H,aAAa;GACX,SAAS,WAAW;EACtB;CACF,GAAG,CAAC,KAAK,OAAO,CAAC;CAEjB,OAAO;AACT"}
|
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
//#region ../ui/src/lib/focus-guards.ts
|
|
3
3
|
/**
|
|
4
|
-
* Base UI
|
|
5
|
-
*
|
|
6
|
-
* assistive technology and can still be focused is a trap, and axe's aria-hidden-focus rule (WCAG 4.1.2) reports exactly that
|
|
7
|
-
* pair; whether as a violation or as "needs review" depends on the page, so an application's audit fails on every open menu
|
|
8
|
-
* while a test page does not. Base UI 1.8 and 1.9 render them this way and give a wrapper no way to change it.
|
|
9
|
-
*
|
|
10
|
-
* `useExposedFocusGuards` takes the `aria-hidden` off them while the popup is open. They stay focusable, so focus is handed
|
|
11
|
-
* back as before, and they stay empty, so there is nothing for a screen reader to read. It is called by the design system's
|
|
12
|
-
* popup content components, which exist only while their popup is open: one observer serves every popup open at once and
|
|
13
|
-
* goes away with the last of them. React does not write an attribute whose prop has not changed, so the removal holds.
|
|
4
|
+
* Base UI's focus-trap sentinels around an open popup are focusable and `aria-hidden`, which axe's aria-hidden-focus rule
|
|
5
|
+
* reports. `useExposedFocusGuards` takes the `aria-hidden` off them while a popup is open; one observer serves every open popup.
|
|
14
6
|
*/
|
|
15
7
|
var HIDDEN_GUARD = "[data-base-ui-focus-guard][aria-hidden]";
|
|
16
8
|
/** Takes `aria-hidden` off the guards in a subtree, or off the node itself when it is one. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"focus-guards.js","names":[],"sources":["../../../../../ui/src/lib/focus-guards.ts"],"sourcesContent":["import { useEffect } from 'react';\n\n/**\n * Base UI
|
|
1
|
+
{"version":3,"file":"focus-guards.js","names":[],"sources":["../../../../../ui/src/lib/focus-guards.ts"],"sourcesContent":["import { useEffect } from 'react';\n\n/**\n * Base UI's focus-trap sentinels around an open popup are focusable and `aria-hidden`, which axe's aria-hidden-focus rule\n * reports. `useExposedFocusGuards` takes the `aria-hidden` off them while a popup is open; one observer serves every open popup.\n */\n\nconst HIDDEN_GUARD = '[data-base-ui-focus-guard][aria-hidden]';\n\n/** Takes `aria-hidden` off the guards in a subtree, or off the node itself when it is one. */\nconst expose = (root: ParentNode | Element): void => {\n if (root instanceof Element && root.matches(HIDDEN_GUARD)) {\n root.removeAttribute('aria-hidden');\n }\n for (const guard of root.querySelectorAll(HIDDEN_GUARD)) {\n guard.removeAttribute('aria-hidden');\n }\n};\n\nlet users = 0;\nlet observer: MutationObserver | undefined;\n\nconst start = (): void => {\n users += 1;\n expose(document);\n if (observer !== undefined) {\n return;\n }\n observer = new MutationObserver((records) => {\n for (const record of records) {\n for (const added of record.addedNodes) {\n if (added instanceof Element) {\n expose(added);\n }\n }\n }\n });\n observer.observe(document.body, { childList: true, subtree: true });\n};\n\nconst stop = (): void => {\n users -= 1;\n if (users === 0) {\n observer?.disconnect();\n observer = undefined;\n }\n};\n\n/** Call from a popup's content component: while it is mounted, the focus guards around popups are not aria-hidden. */\nexport const useExposedFocusGuards = (): void => {\n useEffect(() => {\n start();\n return stop;\n }, []);\n};\n"],"mappings":";;;;;;AAOA,IAAM,eAAe;;AAGrB,IAAM,UAAU,SAAqC;CACnD,IAAI,gBAAgB,WAAW,KAAK,QAAQ,YAAY,GACtD,KAAK,gBAAgB,aAAa;CAEpC,KAAK,MAAM,SAAS,KAAK,iBAAiB,YAAY,GACpD,MAAM,gBAAgB,aAAa;AAEvC;AAEA,IAAI,QAAQ;AACZ,IAAI;AAEJ,IAAM,cAAoB;CACxB,SAAS;CACT,OAAO,QAAQ;CACf,IAAI,aAAa,KAAA,GACf;CAEF,WAAW,IAAI,kBAAkB,YAAY;EAC3C,KAAK,MAAM,UAAU,SACnB,KAAK,MAAM,SAAS,OAAO,YACzB,IAAI,iBAAiB,SACnB,OAAO,KAAK;CAIpB,CAAC;CACD,SAAS,QAAQ,SAAS,MAAM;EAAE,WAAW;EAAM,SAAS;CAAK,CAAC;AACpE;AAEA,IAAM,aAAmB;CACvB,SAAS;CACT,IAAI,UAAU,GAAG;EACf,UAAU,WAAW;EACrB,WAAW,KAAA;CACb;AACF;;AAGA,IAAa,8BAAoC;CAC/C,gBAAgB;EACd,MAAM;EACN,OAAO;CACT,GAAG,CAAC,CAAC;AACP"}
|