@adecore/ui 0.15.0 → 0.16.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Switch.js +3 -1
- package/dist/format/index.d.ts +1 -1
- package/dist/format/index.js +1 -1
- package/dist/format/number.d.ts +2 -0
- package/dist/format/number.js +30 -1
- package/dist/index.d.ts +1 -1
- package/dist/locales/en.json +2 -1
- package/dist/locales/nl.json +2 -1
- package/dist/tabs/parts.d.ts +9 -3
- package/dist/tabs/parts.js +46 -8
- package/package.json +1 -1
- package/src/Switch.tsx +2 -1
- package/src/format/index.ts +12 -1
- package/src/format/number.ts +36 -1
- package/src/index.ts +1 -1
- package/src/locales/en.json +60 -59
- package/src/locales/nl.json +60 -59
- package/src/tabs/parts.tsx +101 -8
package/dist/Switch.js
CHANGED
|
@@ -3,5 +3,7 @@ import clsx from 'clsx';
|
|
|
3
3
|
import { Switch as BaseSwitch } from '@base-ui-components/react/switch';
|
|
4
4
|
/* On or off, as a switch rather than a checkbox: a setting that takes effect the moment it flips. */
|
|
5
5
|
export function Switch({ checked, onCheckedChange, label, disabled, className, ref }) {
|
|
6
|
-
return (_jsx(BaseSwitch.Root, { ref: ref, checked: checked, onCheckedChange: (next) => onCheckedChange(next), "aria-label": label, disabled: disabled,
|
|
6
|
+
return (_jsx(BaseSwitch.Root, { ref: ref, checked: checked, onCheckedChange: (next) => onCheckedChange(next), "aria-label": label, disabled: disabled,
|
|
7
|
+
// Base UI renders the root as a `span`, which ignores its size outside a flex row unless it lays out as a box.
|
|
8
|
+
className: clsx('relative inline-flex h-5 w-9 shrink-0 rounded-full bg-border-strong p-0.5 transition-colors data-checked:bg-accent data-disabled:opacity-50', className), children: _jsx(BaseSwitch.Thumb, { className: "block h-4 w-4 rounded-full bg-surface-raised shadow-raised transition-transform data-checked:translate-x-4" }) }));
|
|
7
9
|
}
|
package/dist/format/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { formatClock, formatDateTime, formatDay, formatDayClock, formatDayWithYear, formatHour, formatMoment, formatNumericDate, formatWeekdayClock, formatWeekdayDay, isSameDay } from './datetime.ts';
|
|
2
2
|
export { formatAgo, formatClockDuration, formatCountdown, formatDuration, formatElapsedShort } from './duration.ts';
|
|
3
3
|
export { FALLBACK_LOCALE, formatLocale, labelCollator, setFormatSource, systemLocale, useFormatLocale, type FormatSource } from './locale.ts';
|
|
4
|
-
export { formatBytes, formatDecimal, formatMoney, formatNumber, formatPercent, formatTokens, formatUsdSignificant } from './number.ts';
|
|
4
|
+
export { formatBytes, formatDecimal, formatFixed, formatMoney, formatNumber, formatNumeral, formatPercent, formatRounded, formatTokens, formatUsdSignificant } from './number.ts';
|
|
5
5
|
export { FORMAT_LANGUAGE, FORMAT_REGIONS, FORMAT_REGION_CHOICES, FORMAT_SYSTEM, formatRegionFrom, regionName } from './regions.ts';
|
|
6
6
|
export { localTimeZone } from './time-zone.ts';
|
package/dist/format/index.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
export { formatClock, formatDateTime, formatDay, formatDayClock, formatDayWithYear, formatHour, formatMoment, formatNumericDate, formatWeekdayClock, formatWeekdayDay, isSameDay } from './datetime.js';
|
|
3
3
|
export { formatAgo, formatClockDuration, formatCountdown, formatDuration, formatElapsedShort } from './duration.js';
|
|
4
4
|
export { FALLBACK_LOCALE, formatLocale, labelCollator, setFormatSource, systemLocale, useFormatLocale } from './locale.js';
|
|
5
|
-
export { formatBytes, formatDecimal, formatMoney, formatNumber, formatPercent, formatTokens, formatUsdSignificant } from './number.js';
|
|
5
|
+
export { formatBytes, formatDecimal, formatFixed, formatMoney, formatNumber, formatNumeral, formatPercent, formatRounded, formatTokens, formatUsdSignificant } from './number.js';
|
|
6
6
|
export { FORMAT_LANGUAGE, FORMAT_REGIONS, FORMAT_REGION_CHOICES, FORMAT_SYSTEM, formatRegionFrom, regionName } from './regions.js';
|
|
7
7
|
export { localTimeZone } from './time-zone.js';
|
package/dist/format/number.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export declare const formatNumber: (value: number) => string;
|
|
2
2
|
export declare const formatDecimal: (value: number) => string;
|
|
3
3
|
export declare const formatFixed: (value: number, decimals: number) => string;
|
|
4
|
+
export declare const formatRounded: (value: number, decimals: number) => string;
|
|
5
|
+
export declare const formatNumeral: (numeral: string | number) => string;
|
|
4
6
|
export declare const formatPercent: (value: number) => string;
|
|
5
7
|
export declare const formatMoney: (value: number, currency: string, maximumFractionDigits?: number) => string;
|
|
6
8
|
export declare const formatUsdSignificant: (value: number) => string;
|
package/dist/format/number.js
CHANGED
|
@@ -7,14 +7,43 @@ export const formatNumber = (value) => numberFormatter(WHOLE).format(Math.round(
|
|
|
7
7
|
export const formatDecimal = (value) => numberFormatter(ONE_DECIMAL).format(value);
|
|
8
8
|
const fixedSpecs = new Map();
|
|
9
9
|
/* Always `decimals` places, so values that follow each other keep one width: `1,0` and `1,5`, never `1` and `1,5`. */
|
|
10
|
-
export const formatFixed = (value, decimals) =>
|
|
10
|
+
export const formatFixed = (value, decimals) => numberFormatter(fixedSpec(decimals)).format(value);
|
|
11
|
+
const fixedSpec = (decimals) => {
|
|
11
12
|
let spec = fixedSpecs.get(decimals);
|
|
12
13
|
if (spec === undefined) {
|
|
13
14
|
spec = { minimumFractionDigits: decimals, maximumFractionDigits: decimals };
|
|
14
15
|
fixedSpecs.set(decimals, spec);
|
|
15
16
|
}
|
|
17
|
+
return spec;
|
|
18
|
+
};
|
|
19
|
+
const roundedSpecs = new Map();
|
|
20
|
+
/* At most `decimals` places, trailing zeros dropped, for a figure whose fraction may or may not be there: `39`, `1,95`. */
|
|
21
|
+
export const formatRounded = (value, decimals) => {
|
|
22
|
+
let spec = roundedSpecs.get(decimals);
|
|
23
|
+
if (spec === undefined) {
|
|
24
|
+
spec = { maximumFractionDigits: decimals };
|
|
25
|
+
roundedSpecs.set(decimals, spec);
|
|
26
|
+
}
|
|
16
27
|
return numberFormatter(spec).format(value);
|
|
17
28
|
};
|
|
29
|
+
/* Intl takes at most this many fraction digits. */
|
|
30
|
+
const MAX_FRACTION_DIGITS = 100;
|
|
31
|
+
const NUMERAL = /^[+-]?\d+(?:\.(\d+))?$/;
|
|
32
|
+
/*
|
|
33
|
+
* A numeral as a database writes it (`12900`, `-0.50`, `9007199254740993`), in the region's notation
|
|
34
|
+
* with every digit kept: a decimal or a big integer arrives as text, and a float would round it.
|
|
35
|
+
* Text that is no plain numeral, such as `NaN` or `1e999`, comes back as it is.
|
|
36
|
+
*/
|
|
37
|
+
export const formatNumeral = (numeral) => {
|
|
38
|
+
const text = typeof numeral === 'number' ? String(numeral) : numeral.trim();
|
|
39
|
+
const match = NUMERAL.exec(text);
|
|
40
|
+
if (match === null) {
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
const decimals = Math.min(match[1]?.length ?? 0, MAX_FRACTION_DIGITS);
|
|
44
|
+
// A string goes to Intl as it is, so it formats the exact decimal and not the nearest double.
|
|
45
|
+
return numberFormatter(fixedSpec(decimals)).format(text);
|
|
46
|
+
};
|
|
18
47
|
/* Percent as Activity Monitor writes it: a decimal under ten, where the difference still shows. */
|
|
19
48
|
export const formatPercent = (value) => `${value < 10 ? formatDecimal(value) : formatNumber(value)}%`;
|
|
20
49
|
/*
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type { DialogDescriptionProps, DialogFooterProps, DialogPopupProps, Dialo
|
|
|
9
9
|
export type { MenuCheckProps, MenuHintProps, MenuLabelProps, MenuPopupProps, MenuRowActionProps, MenuRowProps, MenuRowSubmenuTriggerProps } from './menu/parts.tsx';
|
|
10
10
|
export type { KeyValueListItemProps, KeyValueListNameProps, KeyValueListRootProps, KeyValueListValueProps } from './key-value-list/parts.tsx';
|
|
11
11
|
export type { PopoverPopupProps, PopupVariant, PreviewCardPopupProps } from './popover/parts.tsx';
|
|
12
|
-
export type { TabsCountProps, TabsListProps, TabsRootProps, TabsValue } from './tabs/parts.tsx';
|
|
12
|
+
export type { TabsCountProps, TabsListProps, TabsRootProps, TabsTabProps, TabsValue } from './tabs/parts.tsx';
|
|
13
13
|
export { AccentSwatches, type AccentSwatchesProps } from './AccentSwatches.tsx';
|
|
14
14
|
export { Banner, type BannerProps, type BannerTone } from './Banner.tsx';
|
|
15
15
|
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant } from './Button.tsx';
|
package/dist/locales/en.json
CHANGED
package/dist/locales/nl.json
CHANGED
package/dist/tabs/parts.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ComponentProps, type ReactElement } from 'react';
|
|
1
|
+
import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
|
|
2
2
|
import { Tabs as BaseTabs } from '@base-ui-components/react/tabs';
|
|
3
3
|
import { useRender } from '@base-ui-components/react/use-render';
|
|
4
4
|
export type TabsValue = BaseTabs.Tab.Value;
|
|
@@ -8,11 +8,17 @@ export type TabsRootProps = Omit<ComponentProps<typeof BaseTabs.Root>, 'onValueC
|
|
|
8
8
|
export declare function TabsRoot({ value, defaultValue, onValueChange, ...props }: TabsRootProps): import("react").JSX.Element;
|
|
9
9
|
export type TabsListProps = Omit<ComponentProps<typeof BaseTabs.List>, 'className'> & {
|
|
10
10
|
className?: string;
|
|
11
|
+
end?: ReactNode;
|
|
11
12
|
};
|
|
12
|
-
export declare function TabsList({ className, children, ...props }: TabsListProps): import("react").JSX.Element;
|
|
13
|
-
|
|
13
|
+
export declare function TabsList({ className, children, end, ...props }: TabsListProps): import("react").JSX.Element;
|
|
14
|
+
type TabProps = ComponentProps<typeof BaseTabs.Tab>;
|
|
15
|
+
export type TabsTabProps = TabProps & {
|
|
16
|
+
onClose?(): void;
|
|
17
|
+
};
|
|
18
|
+
export declare function TabsTab({ className, onClose, children, ...props }: TabsTabProps): import("react").JSX.Element;
|
|
14
19
|
export declare const TabsPanel: import("react").ForwardRefExoticComponent<import("@base-ui-components/react/tabs").TabsPanelProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
15
20
|
export type TabsCountProps = Omit<useRender.ComponentProps<'span'>, 'children'> & {
|
|
16
21
|
value: number;
|
|
17
22
|
};
|
|
18
23
|
export declare function TabsCount({ value, render, className, ref, ...props }: TabsCountProps): ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | null;
|
|
24
|
+
export {};
|
package/dist/tabs/parts.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Children, createContext, isValidElement, useContext, useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
|
-
import { ChevronDown } from 'lucide-react';
|
|
4
|
+
import { ChevronDown, X } from 'lucide-react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { Tabs as BaseTabs } from '@base-ui-components/react/tabs';
|
|
7
7
|
import { useRender } from '@base-ui-components/react/use-render';
|
|
8
8
|
import { withClass } from '../class-name.js';
|
|
9
9
|
import { formatNumber } from '../format/number.js';
|
|
10
10
|
import { Icon } from '../Icon.js';
|
|
11
|
+
import { IconButton } from '../IconButton.js';
|
|
11
12
|
import { MenuItem, MenuPopup, MenuRoot, MenuTrigger } from '../menu/parts.js';
|
|
13
|
+
import { Pill } from '../Pill.js';
|
|
12
14
|
import { shownTabs } from './overflow.js';
|
|
13
15
|
const SelectionContext = createContext({ value: undefined, select: () => { } });
|
|
14
16
|
/* Whether a part is drawn in the menu of tabs that do not fit, where a count goes to the end of its row. */
|
|
@@ -35,11 +37,12 @@ const sameIndices = (left, right) => left !== null && left.length === right.leng
|
|
|
35
37
|
* in a row nobody sees, so the strip knows what fits before it draws. The tabs are the direct children,
|
|
36
38
|
* since the menu repeats what a tab holds.
|
|
37
39
|
*/
|
|
38
|
-
export function TabsList({ className, children, ...props }) {
|
|
40
|
+
export function TabsList({ className, children, end, ...props }) {
|
|
39
41
|
const { t } = useTranslation('ui');
|
|
40
42
|
const { value, select } = useContext(SelectionContext);
|
|
41
43
|
const strip = useRef(null);
|
|
42
44
|
const measure = useRef(null);
|
|
45
|
+
const tail = useRef(null);
|
|
43
46
|
// Null until the first measure, which runs before the first paint.
|
|
44
47
|
const [shown, setShown] = useState(null);
|
|
45
48
|
const tabs = Children.toArray(children).filter((child) => isValidElement(child) && child.type === TabsTab);
|
|
@@ -52,7 +55,9 @@ export function TabsList({ className, children, ...props }) {
|
|
|
52
55
|
}
|
|
53
56
|
const fit = () => {
|
|
54
57
|
const style = getComputedStyle(host);
|
|
55
|
-
const
|
|
58
|
+
const gap = parseFloat(style.columnGap) || 0;
|
|
59
|
+
const taken = tail.current === null ? 0 : tail.current.getBoundingClientRect().width + gap;
|
|
60
|
+
const room = host.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight) - taken;
|
|
56
61
|
const cells = [...row.children].map((cell) => cell.getBoundingClientRect().width);
|
|
57
62
|
const more = cells.pop() ?? 0;
|
|
58
63
|
const next = shownTabs(cells, room, parseFloat(getComputedStyle(row).columnGap) || 0, more, picked);
|
|
@@ -62,14 +67,47 @@ export function TabsList({ className, children, ...props }) {
|
|
|
62
67
|
const observer = new ResizeObserver(fit);
|
|
63
68
|
observer.observe(host);
|
|
64
69
|
observer.observe(row);
|
|
70
|
+
if (tail.current !== null) {
|
|
71
|
+
observer.observe(tail.current);
|
|
72
|
+
}
|
|
65
73
|
return () => observer.disconnect();
|
|
66
74
|
});
|
|
67
75
|
const visible = shown === null ? tabs : shown.map((index) => tabs[index]);
|
|
68
76
|
const hidden = shown === null ? [] : tabs.filter((_, index) => !shown.includes(index));
|
|
69
|
-
return (_jsxs("div", { ref: strip, className: clsx('relative flex items-center gap-4 border-b border-border', className), children: [_jsx(BaseTabs.List, { className: "flex min-w-0 items-end gap-4", ...props, children: visible }), hidden.length > 0 && (_jsxs(MenuRoot, { children: [_jsxs(MenuTrigger, { className: MORE, children: [t('tabs.more'), _jsx(Icon, { icon: ChevronDown, size: 14 })] }), _jsx(MenuPopup, { align: "end", className: "min-w-48", children: _jsx(InMenuContext, { value: true, children: hidden.map((tab) => (_jsx(MenuItem, { disabled: tab.props.disabled, onClick: () => select(tab.props.value), children: tab.props.children }, tab.key))) }) })] })), _jsxs("div", { ref: measure, "aria-hidden": true, inert: true, className: "pointer-events-none invisible absolute top-0 left-0 flex gap-4", children: [tabs.map((tab) => (
|
|
77
|
+
return (_jsxs("div", { ref: strip, className: clsx('relative flex items-center gap-4 border-b border-border', className), children: [_jsx(BaseTabs.List, { className: "flex min-w-0 items-end gap-4", ...props, children: visible }), hidden.length > 0 && (_jsxs(MenuRoot, { children: [_jsxs(MenuTrigger, { className: MORE, children: [t('tabs.more'), _jsx(Icon, { icon: ChevronDown, size: 14 })] }), _jsx(MenuPopup, { align: "end", className: "min-w-48", children: _jsx(InMenuContext, { value: true, children: hidden.map((tab) => (_jsx(MenuItem, { disabled: tab.props.disabled, onClick: () => select(tab.props.value), children: tab.props.children }, tab.key))) }) })] })), end !== undefined && end !== null && (_jsx("div", { ref: tail, className: clsx('flex shrink-0 items-center gap-1', hidden.length === 0 && 'ml-auto'), children: end })), _jsxs("div", { ref: measure, "aria-hidden": true, inert: true, className: "pointer-events-none invisible absolute top-0 left-0 flex gap-4", children: [tabs.map((tab) => (_jsxs("span", { className: TAB, children: [tab.props.children, tab.props.onClose !== undefined && _jsx("span", { className: "size-5 shrink-0" })] }, tab.key))), _jsxs("span", { className: MORE, children: [t('tabs.more'), _jsx(Icon, { icon: ChevronDown, size: 14 })] })] })] }));
|
|
70
78
|
}
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
/* The close button stays out of the tab order, so a closable tab is still one stop; the keys close it from the tab itself. */
|
|
80
|
+
export function TabsTab({ className, onClose, children, ...props }) {
|
|
81
|
+
const { t } = useTranslation('ui');
|
|
82
|
+
if (onClose === undefined) {
|
|
83
|
+
return (_jsx(BaseTabs.Tab, { className: withClass(TAB, className), ...props, children: children }));
|
|
84
|
+
}
|
|
85
|
+
const { onKeyDown, onAuxClick, onMouseDown, ...rest } = props;
|
|
86
|
+
const handleKeyDown = (e) => {
|
|
87
|
+
onKeyDown?.(e);
|
|
88
|
+
if (!e.defaultPrevented && e.target === e.currentTarget && (e.key === 'Delete' || e.key === 'Backspace')) {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
onClose();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
// A middle press starts autoscroll on some platforms; the release is what closes.
|
|
94
|
+
const handleMouseDown = (e) => {
|
|
95
|
+
onMouseDown?.(e);
|
|
96
|
+
if (e.button === 1) {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const handleAuxClick = (e) => {
|
|
101
|
+
onAuxClick?.(e);
|
|
102
|
+
if (!e.defaultPrevented && e.button === 1) {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
onClose();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
return (_jsxs(BaseTabs.Tab, { nativeButton: false, render: _jsx("div", {}), className: withClass(TAB, className), onKeyDown: handleKeyDown, onMouseDown: handleMouseDown, onAuxClick: handleAuxClick, ...rest, children: [children, _jsx(IconButton, { icon: X, size: "2xs", label: t('tabs.close'), tabIndex: -1, onClick: (e) => {
|
|
108
|
+
e.stopPropagation();
|
|
109
|
+
onClose();
|
|
110
|
+
} })] }));
|
|
73
111
|
}
|
|
74
112
|
export const TabsPanel = BaseTabs.Panel;
|
|
75
113
|
/* How much a tab holds, after its label. Nothing at zero: an empty view needs no number to say so. */
|
|
@@ -81,8 +119,8 @@ export function TabsCount({ value, render, className, ref, ...props }) {
|
|
|
81
119
|
defaultTagName: 'span',
|
|
82
120
|
props: {
|
|
83
121
|
...props,
|
|
84
|
-
children: formatNumber(value),
|
|
85
|
-
className: clsx('
|
|
122
|
+
children: _jsx(Pill, { className: "tabular-nums", children: formatNumber(value) }),
|
|
123
|
+
className: clsx('inline-flex shrink-0', inMenu && 'ml-auto', className)
|
|
86
124
|
}
|
|
87
125
|
});
|
|
88
126
|
return value === 0 ? null : element;
|
package/package.json
CHANGED
package/src/Switch.tsx
CHANGED
|
@@ -21,8 +21,9 @@ export function Switch({ checked, onCheckedChange, label, disabled, className, r
|
|
|
21
21
|
onCheckedChange={(next) => onCheckedChange(next)}
|
|
22
22
|
aria-label={label}
|
|
23
23
|
disabled={disabled}
|
|
24
|
+
// Base UI renders the root as a `span`, which ignores its size outside a flex row unless it lays out as a box.
|
|
24
25
|
className={clsx(
|
|
25
|
-
'relative h-5 w-9 shrink-0 rounded-full bg-border-strong p-0.5 transition-colors data-checked:bg-accent data-disabled:opacity-50',
|
|
26
|
+
'relative inline-flex h-5 w-9 shrink-0 rounded-full bg-border-strong p-0.5 transition-colors data-checked:bg-accent data-disabled:opacity-50',
|
|
26
27
|
className
|
|
27
28
|
)}
|
|
28
29
|
>
|
package/src/format/index.ts
CHANGED
|
@@ -15,6 +15,17 @@ export {
|
|
|
15
15
|
} from './datetime.ts';
|
|
16
16
|
export { formatAgo, formatClockDuration, formatCountdown, formatDuration, formatElapsedShort } from './duration.ts';
|
|
17
17
|
export { FALLBACK_LOCALE, formatLocale, labelCollator, setFormatSource, systemLocale, useFormatLocale, type FormatSource } from './locale.ts';
|
|
18
|
-
export {
|
|
18
|
+
export {
|
|
19
|
+
formatBytes,
|
|
20
|
+
formatDecimal,
|
|
21
|
+
formatFixed,
|
|
22
|
+
formatMoney,
|
|
23
|
+
formatNumber,
|
|
24
|
+
formatNumeral,
|
|
25
|
+
formatPercent,
|
|
26
|
+
formatRounded,
|
|
27
|
+
formatTokens,
|
|
28
|
+
formatUsdSignificant
|
|
29
|
+
} from './number.ts';
|
|
19
30
|
export { FORMAT_LANGUAGE, FORMAT_REGIONS, FORMAT_REGION_CHOICES, FORMAT_SYSTEM, formatRegionFrom, regionName } from './regions.ts';
|
|
20
31
|
export { localTimeZone } from './time-zone.ts';
|
package/src/format/number.ts
CHANGED
|
@@ -12,15 +12,50 @@ export const formatDecimal = (value: number): string => numberFormatter(ONE_DECI
|
|
|
12
12
|
const fixedSpecs = new Map<number, Intl.NumberFormatOptions>();
|
|
13
13
|
|
|
14
14
|
/* Always `decimals` places, so values that follow each other keep one width: `1,0` and `1,5`, never `1` and `1,5`. */
|
|
15
|
-
export const formatFixed = (value: number, decimals: number): string =>
|
|
15
|
+
export const formatFixed = (value: number, decimals: number): string => numberFormatter(fixedSpec(decimals)).format(value);
|
|
16
|
+
|
|
17
|
+
const fixedSpec = (decimals: number): Intl.NumberFormatOptions => {
|
|
16
18
|
let spec = fixedSpecs.get(decimals);
|
|
17
19
|
if (spec === undefined) {
|
|
18
20
|
spec = { minimumFractionDigits: decimals, maximumFractionDigits: decimals };
|
|
19
21
|
fixedSpecs.set(decimals, spec);
|
|
20
22
|
}
|
|
23
|
+
return spec;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const roundedSpecs = new Map<number, Intl.NumberFormatOptions>();
|
|
27
|
+
|
|
28
|
+
/* At most `decimals` places, trailing zeros dropped, for a figure whose fraction may or may not be there: `39`, `1,95`. */
|
|
29
|
+
export const formatRounded = (value: number, decimals: number): string => {
|
|
30
|
+
let spec = roundedSpecs.get(decimals);
|
|
31
|
+
if (spec === undefined) {
|
|
32
|
+
spec = { maximumFractionDigits: decimals };
|
|
33
|
+
roundedSpecs.set(decimals, spec);
|
|
34
|
+
}
|
|
21
35
|
return numberFormatter(spec).format(value);
|
|
22
36
|
};
|
|
23
37
|
|
|
38
|
+
/* Intl takes at most this many fraction digits. */
|
|
39
|
+
const MAX_FRACTION_DIGITS = 100;
|
|
40
|
+
|
|
41
|
+
const NUMERAL = /^[+-]?\d+(?:\.(\d+))?$/;
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
* A numeral as a database writes it (`12900`, `-0.50`, `9007199254740993`), in the region's notation
|
|
45
|
+
* with every digit kept: a decimal or a big integer arrives as text, and a float would round it.
|
|
46
|
+
* Text that is no plain numeral, such as `NaN` or `1e999`, comes back as it is.
|
|
47
|
+
*/
|
|
48
|
+
export const formatNumeral = (numeral: string | number): string => {
|
|
49
|
+
const text = typeof numeral === 'number' ? String(numeral) : numeral.trim();
|
|
50
|
+
const match = NUMERAL.exec(text);
|
|
51
|
+
if (match === null) {
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
const decimals = Math.min(match[1]?.length ?? 0, MAX_FRACTION_DIGITS);
|
|
55
|
+
// A string goes to Intl as it is, so it formats the exact decimal and not the nearest double.
|
|
56
|
+
return numberFormatter(fixedSpec(decimals)).format(text as Intl.StringNumericLiteral);
|
|
57
|
+
};
|
|
58
|
+
|
|
24
59
|
/* Percent as Activity Monitor writes it: a decimal under ten, where the difference still shows. */
|
|
25
60
|
export const formatPercent = (value: number): string => `${value < 10 ? formatDecimal(value) : formatNumber(value)}%`;
|
|
26
61
|
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type {
|
|
|
23
23
|
} from './menu/parts.tsx';
|
|
24
24
|
export type { KeyValueListItemProps, KeyValueListNameProps, KeyValueListRootProps, KeyValueListValueProps } from './key-value-list/parts.tsx';
|
|
25
25
|
export type { PopoverPopupProps, PopupVariant, PreviewCardPopupProps } from './popover/parts.tsx';
|
|
26
|
-
export type { TabsCountProps, TabsListProps, TabsRootProps, TabsValue } from './tabs/parts.tsx';
|
|
26
|
+
export type { TabsCountProps, TabsListProps, TabsRootProps, TabsTabProps, TabsValue } from './tabs/parts.tsx';
|
|
27
27
|
|
|
28
28
|
export { AccentSwatches, type AccentSwatchesProps } from './AccentSwatches.tsx';
|
|
29
29
|
export { Banner, type BannerProps, type BannerTone } from './Banner.tsx';
|
package/src/locales/en.json
CHANGED
|
@@ -1,62 +1,63 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
2
|
+
"action": {
|
|
3
|
+
"cancel": "Cancel",
|
|
4
|
+
"copy": "Copy",
|
|
5
|
+
"copyError": "Copy error",
|
|
6
|
+
"dismiss": "Dismiss",
|
|
7
|
+
"reload": "Reload",
|
|
8
|
+
"retry": "Try again",
|
|
9
|
+
"select": "Select",
|
|
10
|
+
"selectAll": "Select all"
|
|
11
|
+
},
|
|
12
|
+
"accent": {
|
|
13
|
+
"more": "More colors"
|
|
14
|
+
},
|
|
15
|
+
"settings": {
|
|
16
|
+
"title": "Settings",
|
|
17
|
+
"section": "Settings section",
|
|
18
|
+
"sections": "Settings sections",
|
|
19
|
+
"close": "Close settings",
|
|
20
|
+
"failed": "This settings page failed to render",
|
|
21
|
+
"search": {
|
|
22
|
+
"placeholder": "Search settings",
|
|
23
|
+
"clear": "Clear search",
|
|
24
|
+
"none": "Nothing matches \"{{query}}\".",
|
|
25
|
+
"results": "Search results",
|
|
26
|
+
"pane": "Section"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"controls": {
|
|
30
|
+
"stepper": {
|
|
31
|
+
"smaller": "Smaller",
|
|
32
|
+
"larger": "Larger",
|
|
33
|
+
"smallerFor": "{{label}}: smaller",
|
|
34
|
+
"largerFor": "{{label}}: larger"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"iconPicker": {
|
|
38
|
+
"label": "Icon",
|
|
39
|
+
"none": "No icon",
|
|
40
|
+
"search": "Find an icon",
|
|
41
|
+
"clear": "Clear search",
|
|
42
|
+
"noMatch": "No icon matches “{{query}}”"
|
|
43
|
+
},
|
|
44
|
+
"zoom": {
|
|
45
|
+
"out": "Zoom out",
|
|
46
|
+
"in": "Zoom in",
|
|
47
|
+
"presets": "Zoom presets",
|
|
48
|
+
"fit": "Zoom to fit",
|
|
49
|
+
"fitEverything": "Fit everything"
|
|
50
|
+
},
|
|
51
|
+
"waveform": {
|
|
52
|
+
"position": "{{time}} of {{duration}}"
|
|
53
|
+
},
|
|
54
|
+
"projectSwitcher": {
|
|
55
|
+
"label": "Projects",
|
|
56
|
+
"recent": "Recent projects",
|
|
57
|
+
"actionsFor": "Actions for {{name}}"
|
|
58
|
+
},
|
|
59
|
+
"tabs": {
|
|
60
|
+
"more": "More",
|
|
61
|
+
"close": "Close tab"
|
|
27
62
|
}
|
|
28
|
-
},
|
|
29
|
-
"controls": {
|
|
30
|
-
"stepper": {
|
|
31
|
-
"smaller": "Smaller",
|
|
32
|
-
"larger": "Larger",
|
|
33
|
-
"smallerFor": "{{label}}: smaller",
|
|
34
|
-
"largerFor": "{{label}}: larger"
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
"iconPicker": {
|
|
38
|
-
"label": "Icon",
|
|
39
|
-
"none": "No icon",
|
|
40
|
-
"search": "Find an icon",
|
|
41
|
-
"clear": "Clear search",
|
|
42
|
-
"noMatch": "No icon matches “{{query}}”"
|
|
43
|
-
},
|
|
44
|
-
"zoom": {
|
|
45
|
-
"out": "Zoom out",
|
|
46
|
-
"in": "Zoom in",
|
|
47
|
-
"presets": "Zoom presets",
|
|
48
|
-
"fit": "Zoom to fit",
|
|
49
|
-
"fitEverything": "Fit everything"
|
|
50
|
-
},
|
|
51
|
-
"waveform": {
|
|
52
|
-
"position": "{{time}} of {{duration}}"
|
|
53
|
-
},
|
|
54
|
-
"projectSwitcher": {
|
|
55
|
-
"label": "Projects",
|
|
56
|
-
"recent": "Recent projects",
|
|
57
|
-
"actionsFor": "Actions for {{name}}"
|
|
58
|
-
},
|
|
59
|
-
"tabs": {
|
|
60
|
-
"more": "More"
|
|
61
|
-
}
|
|
62
63
|
}
|
package/src/locales/nl.json
CHANGED
|
@@ -1,62 +1,63 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
2
|
+
"action": {
|
|
3
|
+
"cancel": "Annuleren",
|
|
4
|
+
"copy": "Kopiëren",
|
|
5
|
+
"copyError": "Fout kopiëren",
|
|
6
|
+
"dismiss": "Sluiten",
|
|
7
|
+
"reload": "Herladen",
|
|
8
|
+
"retry": "Opnieuw proberen",
|
|
9
|
+
"select": "Selecteren",
|
|
10
|
+
"selectAll": "Alles selecteren"
|
|
11
|
+
},
|
|
12
|
+
"accent": {
|
|
13
|
+
"more": "Meer kleuren"
|
|
14
|
+
},
|
|
15
|
+
"settings": {
|
|
16
|
+
"title": "Instellingen",
|
|
17
|
+
"section": "Onderdeel van de instellingen",
|
|
18
|
+
"sections": "Onderdelen van de instellingen",
|
|
19
|
+
"close": "Instellingen sluiten",
|
|
20
|
+
"failed": "Deze instellingenpagina kon niet worden getekend",
|
|
21
|
+
"search": {
|
|
22
|
+
"placeholder": "Instellingen zoeken",
|
|
23
|
+
"clear": "Zoekopdracht wissen",
|
|
24
|
+
"none": "Niets komt overeen met \"{{query}}\".",
|
|
25
|
+
"results": "Zoekresultaten",
|
|
26
|
+
"pane": "Onderdeel"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"controls": {
|
|
30
|
+
"stepper": {
|
|
31
|
+
"smaller": "Kleiner",
|
|
32
|
+
"larger": "Groter",
|
|
33
|
+
"smallerFor": "{{label}}: kleiner",
|
|
34
|
+
"largerFor": "{{label}}: groter"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"iconPicker": {
|
|
38
|
+
"label": "Icoon",
|
|
39
|
+
"none": "Geen icoon",
|
|
40
|
+
"search": "Zoek een icoon",
|
|
41
|
+
"clear": "Zoekopdracht wissen",
|
|
42
|
+
"noMatch": "Geen icoon past bij “{{query}}”"
|
|
43
|
+
},
|
|
44
|
+
"zoom": {
|
|
45
|
+
"out": "Uitzoomen",
|
|
46
|
+
"in": "Inzoomen",
|
|
47
|
+
"presets": "Zoomstanden",
|
|
48
|
+
"fit": "Zoomen tot alles past",
|
|
49
|
+
"fitEverything": "Alles laten passen"
|
|
50
|
+
},
|
|
51
|
+
"waveform": {
|
|
52
|
+
"position": "{{time}} van {{duration}}"
|
|
53
|
+
},
|
|
54
|
+
"projectSwitcher": {
|
|
55
|
+
"label": "Projecten",
|
|
56
|
+
"recent": "Recente projecten",
|
|
57
|
+
"actionsFor": "Acties voor {{name}}"
|
|
58
|
+
},
|
|
59
|
+
"tabs": {
|
|
60
|
+
"more": "Meer",
|
|
61
|
+
"close": "Tabblad sluiten"
|
|
27
62
|
}
|
|
28
|
-
},
|
|
29
|
-
"controls": {
|
|
30
|
-
"stepper": {
|
|
31
|
-
"smaller": "Kleiner",
|
|
32
|
-
"larger": "Groter",
|
|
33
|
-
"smallerFor": "{{label}}: kleiner",
|
|
34
|
-
"largerFor": "{{label}}: groter"
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
"iconPicker": {
|
|
38
|
-
"label": "Icoon",
|
|
39
|
-
"none": "Geen icoon",
|
|
40
|
-
"search": "Zoek een icoon",
|
|
41
|
-
"clear": "Zoekopdracht wissen",
|
|
42
|
-
"noMatch": "Geen icoon past bij “{{query}}”"
|
|
43
|
-
},
|
|
44
|
-
"zoom": {
|
|
45
|
-
"out": "Uitzoomen",
|
|
46
|
-
"in": "Inzoomen",
|
|
47
|
-
"presets": "Zoomstanden",
|
|
48
|
-
"fit": "Zoomen tot alles past",
|
|
49
|
-
"fitEverything": "Alles laten passen"
|
|
50
|
-
},
|
|
51
|
-
"waveform": {
|
|
52
|
-
"position": "{{time}} van {{duration}}"
|
|
53
|
-
},
|
|
54
|
-
"projectSwitcher": {
|
|
55
|
-
"label": "Projecten",
|
|
56
|
-
"recent": "Recente projecten",
|
|
57
|
-
"actionsFor": "Acties voor {{name}}"
|
|
58
|
-
},
|
|
59
|
-
"tabs": {
|
|
60
|
-
"more": "Meer"
|
|
61
|
-
}
|
|
62
63
|
}
|
package/src/tabs/parts.tsx
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
createContext,
|
|
4
|
+
isValidElement,
|
|
5
|
+
useContext,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type ComponentProps,
|
|
10
|
+
type ReactElement,
|
|
11
|
+
type ReactNode
|
|
12
|
+
} from 'react';
|
|
2
13
|
import clsx from 'clsx';
|
|
3
|
-
import { ChevronDown } from 'lucide-react';
|
|
14
|
+
import { ChevronDown, X } from 'lucide-react';
|
|
4
15
|
import { useTranslation } from 'react-i18next';
|
|
5
16
|
import { Tabs as BaseTabs } from '@base-ui-components/react/tabs';
|
|
6
17
|
import { useRender } from '@base-ui-components/react/use-render';
|
|
7
18
|
import { withClass } from '../class-name.ts';
|
|
8
19
|
import { formatNumber } from '../format/number.ts';
|
|
9
20
|
import { Icon } from '../Icon.tsx';
|
|
21
|
+
import { IconButton } from '../IconButton.tsx';
|
|
10
22
|
import { MenuItem, MenuPopup, MenuRoot, MenuTrigger } from '../menu/parts.tsx';
|
|
23
|
+
import { Pill } from '../Pill.tsx';
|
|
11
24
|
import { shownTabs } from './overflow.ts';
|
|
12
25
|
|
|
13
26
|
export type TabsValue = BaseTabs.Tab.Value;
|
|
@@ -53,6 +66,8 @@ export function TabsRoot({ value, defaultValue = 0, onValueChange, ...props }: T
|
|
|
53
66
|
export type TabsListProps = Omit<ComponentProps<typeof BaseTabs.List>, 'className'> & {
|
|
54
67
|
/* On the strip, which runs the width of the pane; padding here insets the tabs from its edges. */
|
|
55
68
|
className?: string;
|
|
69
|
+
/* Drawn after the tabs, such as a button that adds one. It keeps its own width: the tabs fit in what is left. */
|
|
70
|
+
end?: ReactNode;
|
|
56
71
|
};
|
|
57
72
|
|
|
58
73
|
const sameIndices = (left: readonly number[] | null, right: readonly number[]): boolean =>
|
|
@@ -63,11 +78,12 @@ const sameIndices = (left: readonly number[] | null, right: readonly number[]):
|
|
|
63
78
|
* in a row nobody sees, so the strip knows what fits before it draws. The tabs are the direct children,
|
|
64
79
|
* since the menu repeats what a tab holds.
|
|
65
80
|
*/
|
|
66
|
-
export function TabsList({ className, children, ...props }: TabsListProps) {
|
|
81
|
+
export function TabsList({ className, children, end, ...props }: TabsListProps) {
|
|
67
82
|
const { t } = useTranslation('ui');
|
|
68
83
|
const { value, select } = useContext(SelectionContext);
|
|
69
84
|
const strip = useRef<HTMLDivElement>(null);
|
|
70
85
|
const measure = useRef<HTMLDivElement>(null);
|
|
86
|
+
const tail = useRef<HTMLDivElement>(null);
|
|
71
87
|
// Null until the first measure, which runs before the first paint.
|
|
72
88
|
const [shown, setShown] = useState<number[] | null>(null);
|
|
73
89
|
const tabs = Children.toArray(children).filter(
|
|
@@ -83,7 +99,9 @@ export function TabsList({ className, children, ...props }: TabsListProps) {
|
|
|
83
99
|
}
|
|
84
100
|
const fit = (): void => {
|
|
85
101
|
const style = getComputedStyle(host);
|
|
86
|
-
const
|
|
102
|
+
const gap = parseFloat(style.columnGap) || 0;
|
|
103
|
+
const taken = tail.current === null ? 0 : tail.current.getBoundingClientRect().width + gap;
|
|
104
|
+
const room = host.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight) - taken;
|
|
87
105
|
const cells = [...row.children].map((cell) => cell.getBoundingClientRect().width);
|
|
88
106
|
const more = cells.pop() ?? 0;
|
|
89
107
|
const next = shownTabs(cells, room, parseFloat(getComputedStyle(row).columnGap) || 0, more, picked);
|
|
@@ -93,6 +111,9 @@ export function TabsList({ className, children, ...props }: TabsListProps) {
|
|
|
93
111
|
const observer = new ResizeObserver(fit);
|
|
94
112
|
observer.observe(host);
|
|
95
113
|
observer.observe(row);
|
|
114
|
+
if (tail.current !== null) {
|
|
115
|
+
observer.observe(tail.current);
|
|
116
|
+
}
|
|
96
117
|
return () => observer.disconnect();
|
|
97
118
|
});
|
|
98
119
|
|
|
@@ -121,10 +142,16 @@ export function TabsList({ className, children, ...props }: TabsListProps) {
|
|
|
121
142
|
</MenuPopup>
|
|
122
143
|
</MenuRoot>
|
|
123
144
|
)}
|
|
145
|
+
{end !== undefined && end !== null && (
|
|
146
|
+
<div ref={tail} className={clsx('flex shrink-0 items-center gap-1', hidden.length === 0 && 'ml-auto')}>
|
|
147
|
+
{end}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
124
150
|
<div ref={measure} aria-hidden inert className="pointer-events-none invisible absolute top-0 left-0 flex gap-4">
|
|
125
151
|
{tabs.map((tab) => (
|
|
126
152
|
<span key={tab.key} className={TAB}>
|
|
127
153
|
{tab.props.children}
|
|
154
|
+
{tab.props.onClose !== undefined && <span className="size-5 shrink-0" />}
|
|
128
155
|
</span>
|
|
129
156
|
))}
|
|
130
157
|
<span className={MORE}>
|
|
@@ -136,8 +163,74 @@ export function TabsList({ className, children, ...props }: TabsListProps) {
|
|
|
136
163
|
);
|
|
137
164
|
}
|
|
138
165
|
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
type TabProps = ComponentProps<typeof BaseTabs.Tab>;
|
|
167
|
+
|
|
168
|
+
export type TabsTabProps = TabProps & {
|
|
169
|
+
/* Makes the tab closable: a close button in it, a middle click, and Delete or Backspace while the tab has focus. */
|
|
170
|
+
onClose?(): void;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/* The close button stays out of the tab order, so a closable tab is still one stop; the keys close it from the tab itself. */
|
|
174
|
+
export function TabsTab({ className, onClose, children, ...props }: TabsTabProps) {
|
|
175
|
+
const { t } = useTranslation('ui');
|
|
176
|
+
|
|
177
|
+
if (onClose === undefined) {
|
|
178
|
+
return (
|
|
179
|
+
<BaseTabs.Tab className={withClass(TAB, className)} {...props}>
|
|
180
|
+
{children}
|
|
181
|
+
</BaseTabs.Tab>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { onKeyDown, onAuxClick, onMouseDown, ...rest } = props;
|
|
186
|
+
|
|
187
|
+
const handleKeyDown = (e: Parameters<NonNullable<TabProps['onKeyDown']>>[0]): void => {
|
|
188
|
+
onKeyDown?.(e);
|
|
189
|
+
if (!e.defaultPrevented && e.target === e.currentTarget && (e.key === 'Delete' || e.key === 'Backspace')) {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
onClose();
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// A middle press starts autoscroll on some platforms; the release is what closes.
|
|
196
|
+
const handleMouseDown = (e: Parameters<NonNullable<TabProps['onAuxClick']>>[0]): void => {
|
|
197
|
+
onMouseDown?.(e);
|
|
198
|
+
if (e.button === 1) {
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const handleAuxClick = (e: Parameters<NonNullable<TabProps['onAuxClick']>>[0]): void => {
|
|
204
|
+
onAuxClick?.(e);
|
|
205
|
+
if (!e.defaultPrevented && e.button === 1) {
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
onClose();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<BaseTabs.Tab
|
|
213
|
+
nativeButton={false}
|
|
214
|
+
render={<div />}
|
|
215
|
+
className={withClass(TAB, className)}
|
|
216
|
+
onKeyDown={handleKeyDown}
|
|
217
|
+
onMouseDown={handleMouseDown}
|
|
218
|
+
onAuxClick={handleAuxClick}
|
|
219
|
+
{...rest}
|
|
220
|
+
>
|
|
221
|
+
{children}
|
|
222
|
+
<IconButton
|
|
223
|
+
icon={X}
|
|
224
|
+
size="2xs"
|
|
225
|
+
label={t('tabs.close')}
|
|
226
|
+
tabIndex={-1}
|
|
227
|
+
onClick={(e) => {
|
|
228
|
+
e.stopPropagation();
|
|
229
|
+
onClose();
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
</BaseTabs.Tab>
|
|
233
|
+
);
|
|
141
234
|
}
|
|
142
235
|
|
|
143
236
|
export const TabsPanel = BaseTabs.Panel;
|
|
@@ -155,8 +248,8 @@ export function TabsCount({ value, render, className, ref, ...props }: TabsCount
|
|
|
155
248
|
defaultTagName: 'span',
|
|
156
249
|
props: {
|
|
157
250
|
...props,
|
|
158
|
-
children: formatNumber(value)
|
|
159
|
-
className: clsx('
|
|
251
|
+
children: <Pill className="tabular-nums">{formatNumber(value)}</Pill>,
|
|
252
|
+
className: clsx('inline-flex shrink-0', inMenu && 'ml-auto', className)
|
|
160
253
|
}
|
|
161
254
|
});
|
|
162
255
|
return value === 0 ? null : element;
|