@attrus-ds/core 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +27 -27
- package/dist/assets/flags/Brazil-rounded.svg +15 -15
- package/dist/assets/flags/Brazil-square.svg +16 -16
- package/dist/assets/flags/Chile-rounded.svg +10 -10
- package/dist/assets/flags/Chile-square.svg +10 -10
- package/dist/assets/flags/Colombia-rounded.svg +9 -9
- package/dist/assets/flags/Colombia-square.svg +9 -9
- package/dist/assets/flags/EUA-rounded.svg +10 -10
- package/dist/assets/flags/EUA-square.svg +10 -10
- package/dist/assets/flags/EUR-rounded.svg +8 -8
- package/dist/assets/flags/EUR-square.svg +8 -8
- package/dist/assets/flags/Mexico-rounded.svg +18 -18
- package/dist/assets/flags/Mexico-square.svg +18 -18
- package/dist/assets/icon-attrus-dark.svg +2 -2
- package/dist/assets/icon-attrus-light.svg +2 -2
- package/dist/assets/logo-attrus-dark.svg +7 -7
- package/dist/assets/logo-attrus-light.svg +7 -7
- package/dist/css/components/buttons.css +301 -301
- package/dist/css/components/callout.css +195 -195
- package/dist/css/components/cards.css +272 -272
- package/dist/css/components/combobox.css +319 -319
- package/dist/css/components/empty-states.css +61 -61
- package/dist/css/components/inputs.css +975 -975
- package/dist/css/components/number-input.css +138 -138
- package/dist/css/components/overlays.css +374 -374
- package/dist/css/components/primitives.css +314 -314
- package/dist/css/components/sidebar.css +179 -179
- package/dist/css/components/smart-search.css +91 -91
- package/dist/css/components/status-pills.css +207 -207
- package/dist/css/components/stepper.css +309 -309
- package/dist/css/components/table.css +602 -602
- package/dist/css/components/tabs.css +319 -319
- package/dist/css/components/toast.css +243 -243
- package/dist/css/components/tooltip.css +231 -231
- package/dist/css/components/topbar.css +148 -148
- package/dist/css/tokens.css +666 -666
- package/dist/react/AppSidebar/AppSidebar.d.ts +50 -50
- package/dist/react/AppSidebar/AppSidebar.tsx +197 -197
- package/dist/react/AppTopbar/AppTopbar.d.ts +58 -58
- package/dist/react/AppTopbar/AppTopbar.tsx +203 -203
- package/dist/react/Atoms/Atoms.d.ts +149 -149
- package/dist/react/Atoms/Atoms.tsx +407 -407
- package/dist/react/Button/Button.d.ts +25 -25
- package/dist/react/Button/Button.tsx +83 -83
- package/dist/react/Callout/Callout.d.ts +29 -29
- package/dist/react/Callout/Callout.tsx +98 -98
- package/dist/react/Card/Card.d.ts +38 -38
- package/dist/react/Card/Card.tsx +93 -93
- package/dist/react/Combobox/Combobox.d.ts +52 -52
- package/dist/react/Combobox/Combobox.tsx +254 -254
- package/dist/react/DataTable/DataTable.d.ts +81 -81
- package/dist/react/DataTable/DataTable.tsx +308 -308
- package/dist/react/Drawer/Drawer.d.ts +35 -35
- package/dist/react/Drawer/Drawer.tsx +226 -226
- package/dist/react/Input/Input.d.ts +155 -155
- package/dist/react/Input/Input.tsx +478 -478
- package/dist/react/Modal/Modal.d.ts +27 -27
- package/dist/react/Modal/Modal.tsx +173 -173
- package/dist/react/NumberInput/NumberInput.d.ts +25 -25
- package/dist/react/NumberInput/NumberInput.tsx +117 -117
- package/dist/react/Pill/Pill.d.ts +31 -31
- package/dist/react/Pill/Pill.tsx +104 -104
- package/dist/react/SearchPill/SearchPill.d.ts +14 -14
- package/dist/react/SearchPill/SearchPill.tsx +40 -40
- package/dist/react/SmartSearch/SmartSearch.d.ts +65 -65
- package/dist/react/SmartSearch/SmartSearch.tsx +271 -271
- package/dist/react/Stepper/Stepper.d.ts +25 -25
- package/dist/react/Stepper/Stepper.tsx +94 -94
- package/dist/react/Tabs/Tabs.d.ts +32 -32
- package/dist/react/Tabs/Tabs.tsx +147 -147
- package/dist/react/Toast/Toast.d.ts +30 -30
- package/dist/react/Toast/Toast.tsx +94 -94
- package/dist/react/Tooltip/Tooltip.d.ts +23 -23
- package/dist/react/Tooltip/Tooltip.tsx +80 -80
- package/dist/react/ZeroState/ZeroState.d.ts +17 -17
- package/dist/react/ZeroState/ZeroState.tsx +45 -45
- package/dist/react/index.ts +35 -35
- package/dist/tokens/attrus-tokens.flat.json +1234 -1234
- package/dist/tokens/attrus-tokens.json +1089 -1089
- package/package.json +64 -64
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ATTRUS Stepper — typed wrapper over the canonical `.stepper` flow
|
|
5
|
-
* indicator (preview/components/stepper.css). States: todo / current /
|
|
6
|
-
* done / error / skipped. Orientations: horizontal (default), vertical.
|
|
7
|
-
* Density: compact. Minimal: rule (4px capsule, mono labels).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export type StepState = 'todo' | 'current' | 'done' | 'error' | 'skipped';
|
|
11
|
-
|
|
12
|
-
export interface StepItem {
|
|
13
|
-
key: string;
|
|
14
|
-
label: React.ReactNode;
|
|
15
|
-
help?: React.ReactNode;
|
|
16
|
-
state?: StepState;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface StepperProps extends React.OlHTMLAttributes<HTMLOListElement> {
|
|
20
|
-
steps: StepItem[];
|
|
21
|
-
vertical?: boolean;
|
|
22
|
-
compact?: boolean;
|
|
23
|
-
/** Minimal capsule+label variant (.is-rule) for tight headers. */
|
|
24
|
-
rule?: boolean;
|
|
25
|
-
/** ≤640px: hide labels, keep markers (.is-collapse-sm). */
|
|
26
|
-
collapseOnMobile?: boolean;
|
|
27
|
-
/** Step click (e.g. jump back to a done step). */
|
|
28
|
-
onStepClick?: (key: string) => void;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const CheckGlyph: React.FC = () => (
|
|
32
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
33
|
-
<path d="M20 6 9 17l-5-5" />
|
|
34
|
-
</svg>
|
|
35
|
-
);
|
|
36
|
-
const XGlyph: React.FC = () => (
|
|
37
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
38
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
39
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
40
|
-
</svg>
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
export const Stepper: React.FC<StepperProps> = ({
|
|
44
|
-
steps,
|
|
45
|
-
vertical = false,
|
|
46
|
-
compact = false,
|
|
47
|
-
rule = false,
|
|
48
|
-
collapseOnMobile = false,
|
|
49
|
-
onStepClick,
|
|
50
|
-
className,
|
|
51
|
-
...rest
|
|
52
|
-
}) => {
|
|
53
|
-
const cls = [
|
|
54
|
-
'stepper',
|
|
55
|
-
vertical ? 'is-vertical' : '',
|
|
56
|
-
compact ? 'is-compact' : '',
|
|
57
|
-
rule ? 'is-rule' : '',
|
|
58
|
-
collapseOnMobile ? 'is-collapse-sm' : '',
|
|
59
|
-
className || '',
|
|
60
|
-
]
|
|
61
|
-
.filter(Boolean)
|
|
62
|
-
.join(' ');
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<ol className={cls} {...rest}>
|
|
66
|
-
{steps.map((s, i) => {
|
|
67
|
-
const state = s.state || 'todo';
|
|
68
|
-
const stepCls = ['step', state !== 'todo' ? `is-${state}` : ''].filter(Boolean).join(' ');
|
|
69
|
-
const marker =
|
|
70
|
-
state === 'done' ? <CheckGlyph /> : state === 'error' ? <XGlyph /> : <span>{i + 1}</span>;
|
|
71
|
-
const clickable = onStepClick && (state === 'done' || state === 'error');
|
|
72
|
-
return (
|
|
73
|
-
<li key={s.key} className={stepCls} aria-current={state === 'current' ? 'step' : undefined}>
|
|
74
|
-
<span
|
|
75
|
-
className="step-marker"
|
|
76
|
-
role={clickable ? 'button' : undefined}
|
|
77
|
-
tabIndex={clickable ? 0 : undefined}
|
|
78
|
-
style={clickable ? { cursor: 'pointer' } : undefined}
|
|
79
|
-
onClick={clickable ? () => onStepClick!(s.key) : undefined}
|
|
80
|
-
>
|
|
81
|
-
{marker}
|
|
82
|
-
</span>
|
|
83
|
-
<span className="step-body">
|
|
84
|
-
<span className="step-label">{s.label}</span>
|
|
85
|
-
{s.help != null ? <span className="step-help">{s.help}</span> : null}
|
|
86
|
-
</span>
|
|
87
|
-
</li>
|
|
88
|
-
);
|
|
89
|
-
})}
|
|
90
|
-
</ol>
|
|
91
|
-
);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
export default Stepper;
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ATTRUS Stepper — typed wrapper over the canonical `.stepper` flow
|
|
5
|
+
* indicator (preview/components/stepper.css). States: todo / current /
|
|
6
|
+
* done / error / skipped. Orientations: horizontal (default), vertical.
|
|
7
|
+
* Density: compact. Minimal: rule (4px capsule, mono labels).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type StepState = 'todo' | 'current' | 'done' | 'error' | 'skipped';
|
|
11
|
+
|
|
12
|
+
export interface StepItem {
|
|
13
|
+
key: string;
|
|
14
|
+
label: React.ReactNode;
|
|
15
|
+
help?: React.ReactNode;
|
|
16
|
+
state?: StepState;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StepperProps extends React.OlHTMLAttributes<HTMLOListElement> {
|
|
20
|
+
steps: StepItem[];
|
|
21
|
+
vertical?: boolean;
|
|
22
|
+
compact?: boolean;
|
|
23
|
+
/** Minimal capsule+label variant (.is-rule) for tight headers. */
|
|
24
|
+
rule?: boolean;
|
|
25
|
+
/** ≤640px: hide labels, keep markers (.is-collapse-sm). */
|
|
26
|
+
collapseOnMobile?: boolean;
|
|
27
|
+
/** Step click (e.g. jump back to a done step). */
|
|
28
|
+
onStepClick?: (key: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const CheckGlyph: React.FC = () => (
|
|
32
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
33
|
+
<path d="M20 6 9 17l-5-5" />
|
|
34
|
+
</svg>
|
|
35
|
+
);
|
|
36
|
+
const XGlyph: React.FC = () => (
|
|
37
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
38
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
39
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
40
|
+
</svg>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const Stepper: React.FC<StepperProps> = ({
|
|
44
|
+
steps,
|
|
45
|
+
vertical = false,
|
|
46
|
+
compact = false,
|
|
47
|
+
rule = false,
|
|
48
|
+
collapseOnMobile = false,
|
|
49
|
+
onStepClick,
|
|
50
|
+
className,
|
|
51
|
+
...rest
|
|
52
|
+
}) => {
|
|
53
|
+
const cls = [
|
|
54
|
+
'stepper',
|
|
55
|
+
vertical ? 'is-vertical' : '',
|
|
56
|
+
compact ? 'is-compact' : '',
|
|
57
|
+
rule ? 'is-rule' : '',
|
|
58
|
+
collapseOnMobile ? 'is-collapse-sm' : '',
|
|
59
|
+
className || '',
|
|
60
|
+
]
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join(' ');
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<ol className={cls} {...rest}>
|
|
66
|
+
{steps.map((s, i) => {
|
|
67
|
+
const state = s.state || 'todo';
|
|
68
|
+
const stepCls = ['step', state !== 'todo' ? `is-${state}` : ''].filter(Boolean).join(' ');
|
|
69
|
+
const marker =
|
|
70
|
+
state === 'done' ? <CheckGlyph /> : state === 'error' ? <XGlyph /> : <span>{i + 1}</span>;
|
|
71
|
+
const clickable = onStepClick && (state === 'done' || state === 'error');
|
|
72
|
+
return (
|
|
73
|
+
<li key={s.key} className={stepCls} aria-current={state === 'current' ? 'step' : undefined}>
|
|
74
|
+
<span
|
|
75
|
+
className="step-marker"
|
|
76
|
+
role={clickable ? 'button' : undefined}
|
|
77
|
+
tabIndex={clickable ? 0 : undefined}
|
|
78
|
+
style={clickable ? { cursor: 'pointer' } : undefined}
|
|
79
|
+
onClick={clickable ? () => onStepClick!(s.key) : undefined}
|
|
80
|
+
>
|
|
81
|
+
{marker}
|
|
82
|
+
</span>
|
|
83
|
+
<span className="step-body">
|
|
84
|
+
<span className="step-label">{s.label}</span>
|
|
85
|
+
{s.help != null ? <span className="step-help">{s.help}</span> : null}
|
|
86
|
+
</span>
|
|
87
|
+
</li>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</ol>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export default Stepper;
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
export interface TabItem {
|
|
4
|
-
/** Stable identity — also what onChange receives. */
|
|
5
|
-
key: string;
|
|
6
|
-
/** Label. Strings get the bold-width reservation (no reflow on select). */
|
|
7
|
-
label: React.ReactNode;
|
|
8
|
-
/** Optional count badge (.tab-count). */
|
|
9
|
-
count?: number | string;
|
|
10
|
-
/** Icon-only segment (`.tab.is-icon`, pill variant) — label becomes aria-label; pass the glyph here. */
|
|
11
|
-
icon?: React.ReactNode;
|
|
12
|
-
disabled?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type TabsVariant = 'underline' | 'pill';
|
|
16
|
-
export type TabsSize = 'sm' | 'md' | 'lg';
|
|
17
|
-
export type TabsLayout = 'auto' | 'block' | 'equal' | 'scroll';
|
|
18
|
-
|
|
19
|
-
export interface TabsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
20
|
-
items: TabItem[];
|
|
21
|
-
/** Key of the selected tab (controlled). */
|
|
22
|
-
value: string;
|
|
23
|
-
onChange?: (key: string) => void;
|
|
24
|
-
variant?: TabsVariant;
|
|
25
|
-
size?: TabsSize;
|
|
26
|
-
layout?: TabsLayout;
|
|
27
|
-
/** Sliding indicator (`.is-animated` + `.tab-indicator`), ON by default; false = static indicator. */
|
|
28
|
-
animated?: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export declare const Tabs: React.FC<TabsProps>;
|
|
32
|
-
export default Tabs;
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface TabItem {
|
|
4
|
+
/** Stable identity — also what onChange receives. */
|
|
5
|
+
key: string;
|
|
6
|
+
/** Label. Strings get the bold-width reservation (no reflow on select). */
|
|
7
|
+
label: React.ReactNode;
|
|
8
|
+
/** Optional count badge (.tab-count). */
|
|
9
|
+
count?: number | string;
|
|
10
|
+
/** Icon-only segment (`.tab.is-icon`, pill variant) — label becomes aria-label; pass the glyph here. */
|
|
11
|
+
icon?: React.ReactNode;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type TabsVariant = 'underline' | 'pill';
|
|
16
|
+
export type TabsSize = 'sm' | 'md' | 'lg';
|
|
17
|
+
export type TabsLayout = 'auto' | 'block' | 'equal' | 'scroll';
|
|
18
|
+
|
|
19
|
+
export interface TabsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
20
|
+
items: TabItem[];
|
|
21
|
+
/** Key of the selected tab (controlled). */
|
|
22
|
+
value: string;
|
|
23
|
+
onChange?: (key: string) => void;
|
|
24
|
+
variant?: TabsVariant;
|
|
25
|
+
size?: TabsSize;
|
|
26
|
+
layout?: TabsLayout;
|
|
27
|
+
/** Sliding indicator (`.is-animated` + `.tab-indicator`), ON by default; false = static indicator. */
|
|
28
|
+
animated?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export declare const Tabs: React.FC<TabsProps>;
|
|
32
|
+
export default Tabs;
|
package/dist/react/Tabs/Tabs.tsx
CHANGED
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ATTRUS Tabs — typed wrapper over the canonical `.tabs` system
|
|
5
|
-
* (preview/components/tabs.css). CSS stays the visual source of truth.
|
|
6
|
-
*
|
|
7
|
-
* Variants: underline (default) | pill (segmented control).
|
|
8
|
-
* Sizes: sm | md (default) | lg (lg steps down to md on mobile via CSS).
|
|
9
|
-
* Layout: auto (content-sized) | block (full-width, equal columns) |
|
|
10
|
-
* equal (equal columns, content width) | scroll (force h-scroll).
|
|
11
|
-
* Animated BY DEFAULT: sliding indicator (underline → bar, pill → lifted
|
|
12
|
-
* surface) wired via `.is-animated` + `--ind-x/--ind-w`; pass
|
|
13
|
-
* animated={false} to opt out (static per-tab indicator).
|
|
14
|
-
* Mobile: underline rows scroll horizontally by default; the pill shell
|
|
15
|
-
* fills the row and scrolls internally — both handled by the CSS.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
export interface TabItem {
|
|
19
|
-
/** Stable identity — also what onChange receives. */
|
|
20
|
-
key: string;
|
|
21
|
-
/** Label. Strings get the bold-width reservation (no reflow on select). */
|
|
22
|
-
label: React.ReactNode;
|
|
23
|
-
/** Optional count badge (.tab-count). */
|
|
24
|
-
count?: number | string;
|
|
25
|
-
/** Icon-only segment (`.tab.is-icon`, pill variant) — label becomes aria-label; pass the glyph here. */
|
|
26
|
-
icon?: React.ReactNode;
|
|
27
|
-
disabled?: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export type TabsVariant = 'underline' | 'pill';
|
|
31
|
-
export type TabsSize = 'sm' | 'md' | 'lg';
|
|
32
|
-
export type TabsLayout = 'auto' | 'block' | 'equal' | 'scroll';
|
|
33
|
-
|
|
34
|
-
export interface TabsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
35
|
-
items: TabItem[];
|
|
36
|
-
/** Key of the selected tab (controlled). */
|
|
37
|
-
value: string;
|
|
38
|
-
onChange?: (key: string) => void;
|
|
39
|
-
variant?: TabsVariant;
|
|
40
|
-
size?: TabsSize;
|
|
41
|
-
layout?: TabsLayout;
|
|
42
|
-
/** Sliding indicator (`.is-animated` + `.tab-indicator`): underline → sliding bar,
|
|
43
|
-
* pill → sliding lifted surface. ON by default; pass false for the static indicator.
|
|
44
|
-
* Positioning JS (--ind-x/--ind-w + resize) is wired here. */
|
|
45
|
-
animated?: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const LAYOUT_CLASS: Record<TabsLayout, string> = {
|
|
49
|
-
auto: '',
|
|
50
|
-
block: 'is-block',
|
|
51
|
-
equal: 'is-equal',
|
|
52
|
-
scroll: 'is-scroll',
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export const Tabs: React.FC<TabsProps> = ({
|
|
56
|
-
items,
|
|
57
|
-
value,
|
|
58
|
-
onChange,
|
|
59
|
-
variant = 'underline',
|
|
60
|
-
size = 'md',
|
|
61
|
-
layout = 'auto',
|
|
62
|
-
animated = true,
|
|
63
|
-
className,
|
|
64
|
-
...rest
|
|
65
|
-
}) => {
|
|
66
|
-
const listRef = React.useRef<HTMLDivElement>(null);
|
|
67
|
-
|
|
68
|
-
// .is-animated contract: set --ind-x/--ind-w from the active tab; recompute on resize.
|
|
69
|
-
React.useLayoutEffect(() => {
|
|
70
|
-
if (!animated) return;
|
|
71
|
-
const list = listRef.current;
|
|
72
|
-
if (!list) return;
|
|
73
|
-
const position = () => {
|
|
74
|
-
const active = list.querySelector<HTMLElement>('.tab[aria-selected="true"]');
|
|
75
|
-
if (!active) return;
|
|
76
|
-
list.style.setProperty('--ind-x', active.offsetLeft + 'px');
|
|
77
|
-
list.style.setProperty('--ind-w', active.offsetWidth + 'px');
|
|
78
|
-
};
|
|
79
|
-
position();
|
|
80
|
-
const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(position) : null;
|
|
81
|
-
ro?.observe(list);
|
|
82
|
-
// Late font load changes tab widths without resizing a fixed-width list —
|
|
83
|
-
// reposition once webfonts settle so the indicator never sits misaligned.
|
|
84
|
-
if (typeof document !== 'undefined' && (document as Document & { fonts?: { ready: Promise<unknown> } }).fonts) {
|
|
85
|
-
(document as Document & { fonts: { ready: Promise<unknown> } }).fonts.ready.then(position).catch(() => undefined);
|
|
86
|
-
}
|
|
87
|
-
return () => ro?.disconnect();
|
|
88
|
-
}, [animated, value, items]);
|
|
89
|
-
|
|
90
|
-
const cls = [
|
|
91
|
-
'tabs',
|
|
92
|
-
variant === 'pill' ? 'tabs-pill' : '',
|
|
93
|
-
size !== 'md' ? `tabs-${size}` : '',
|
|
94
|
-
LAYOUT_CLASS[layout],
|
|
95
|
-
animated ? 'is-animated' : '',
|
|
96
|
-
className || '',
|
|
97
|
-
]
|
|
98
|
-
.filter(Boolean)
|
|
99
|
-
.join(' ');
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<div ref={listRef} className={cls} role="tablist" {...rest}>
|
|
103
|
-
{items.map((it) => (
|
|
104
|
-
<button
|
|
105
|
-
key={it.key}
|
|
106
|
-
type="button"
|
|
107
|
-
role="tab"
|
|
108
|
-
className={it.icon != null ? 'tab is-icon' : 'tab'}
|
|
109
|
-
aria-selected={value === it.key}
|
|
110
|
-
aria-label={it.icon != null && typeof it.label === 'string' ? it.label : undefined}
|
|
111
|
-
tabIndex={value === it.key ? 0 : -1}
|
|
112
|
-
disabled={it.disabled}
|
|
113
|
-
onClick={() => {
|
|
114
|
-
if (!it.disabled && onChange) onChange(it.key);
|
|
115
|
-
}}
|
|
116
|
-
onKeyDown={(e) => {
|
|
117
|
-
// Roving focus — ←/→ move between tabs, wrapping; Home/End jump.
|
|
118
|
-
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight' && e.key !== 'Home' && e.key !== 'End') return;
|
|
119
|
-
e.preventDefault();
|
|
120
|
-
const enabled = items.filter((x) => !x.disabled);
|
|
121
|
-
if (!enabled.length) return;
|
|
122
|
-
const idx = enabled.findIndex((x) => x.key === it.key);
|
|
123
|
-
let next = idx;
|
|
124
|
-
if (e.key === 'ArrowRight') next = (idx + 1) % enabled.length;
|
|
125
|
-
else if (e.key === 'ArrowLeft') next = (idx - 1 + enabled.length) % enabled.length;
|
|
126
|
-
else if (e.key === 'Home') next = 0;
|
|
127
|
-
else next = enabled.length - 1;
|
|
128
|
-
const btns = listRef.current?.querySelectorAll<HTMLButtonElement>('.tab:not([disabled])');
|
|
129
|
-
btns?.[next]?.focus();
|
|
130
|
-
}}
|
|
131
|
-
>
|
|
132
|
-
{it.icon != null ? (
|
|
133
|
-
it.icon
|
|
134
|
-
) : (
|
|
135
|
-
<span className="tab-label" data-label={typeof it.label === 'string' ? it.label : undefined}>
|
|
136
|
-
{it.label}
|
|
137
|
-
</span>
|
|
138
|
-
)}
|
|
139
|
-
{it.icon == null && it.count != null ? <span className="tab-count">{it.count}</span> : null}
|
|
140
|
-
</button>
|
|
141
|
-
))}
|
|
142
|
-
{animated ? <span className="tab-indicator" aria-hidden="true" /> : null}
|
|
143
|
-
</div>
|
|
144
|
-
);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
export default Tabs;
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ATTRUS Tabs — typed wrapper over the canonical `.tabs` system
|
|
5
|
+
* (preview/components/tabs.css). CSS stays the visual source of truth.
|
|
6
|
+
*
|
|
7
|
+
* Variants: underline (default) | pill (segmented control).
|
|
8
|
+
* Sizes: sm | md (default) | lg (lg steps down to md on mobile via CSS).
|
|
9
|
+
* Layout: auto (content-sized) | block (full-width, equal columns) |
|
|
10
|
+
* equal (equal columns, content width) | scroll (force h-scroll).
|
|
11
|
+
* Animated BY DEFAULT: sliding indicator (underline → bar, pill → lifted
|
|
12
|
+
* surface) wired via `.is-animated` + `--ind-x/--ind-w`; pass
|
|
13
|
+
* animated={false} to opt out (static per-tab indicator).
|
|
14
|
+
* Mobile: underline rows scroll horizontally by default; the pill shell
|
|
15
|
+
* fills the row and scrolls internally — both handled by the CSS.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface TabItem {
|
|
19
|
+
/** Stable identity — also what onChange receives. */
|
|
20
|
+
key: string;
|
|
21
|
+
/** Label. Strings get the bold-width reservation (no reflow on select). */
|
|
22
|
+
label: React.ReactNode;
|
|
23
|
+
/** Optional count badge (.tab-count). */
|
|
24
|
+
count?: number | string;
|
|
25
|
+
/** Icon-only segment (`.tab.is-icon`, pill variant) — label becomes aria-label; pass the glyph here. */
|
|
26
|
+
icon?: React.ReactNode;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type TabsVariant = 'underline' | 'pill';
|
|
31
|
+
export type TabsSize = 'sm' | 'md' | 'lg';
|
|
32
|
+
export type TabsLayout = 'auto' | 'block' | 'equal' | 'scroll';
|
|
33
|
+
|
|
34
|
+
export interface TabsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
35
|
+
items: TabItem[];
|
|
36
|
+
/** Key of the selected tab (controlled). */
|
|
37
|
+
value: string;
|
|
38
|
+
onChange?: (key: string) => void;
|
|
39
|
+
variant?: TabsVariant;
|
|
40
|
+
size?: TabsSize;
|
|
41
|
+
layout?: TabsLayout;
|
|
42
|
+
/** Sliding indicator (`.is-animated` + `.tab-indicator`): underline → sliding bar,
|
|
43
|
+
* pill → sliding lifted surface. ON by default; pass false for the static indicator.
|
|
44
|
+
* Positioning JS (--ind-x/--ind-w + resize) is wired here. */
|
|
45
|
+
animated?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const LAYOUT_CLASS: Record<TabsLayout, string> = {
|
|
49
|
+
auto: '',
|
|
50
|
+
block: 'is-block',
|
|
51
|
+
equal: 'is-equal',
|
|
52
|
+
scroll: 'is-scroll',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const Tabs: React.FC<TabsProps> = ({
|
|
56
|
+
items,
|
|
57
|
+
value,
|
|
58
|
+
onChange,
|
|
59
|
+
variant = 'underline',
|
|
60
|
+
size = 'md',
|
|
61
|
+
layout = 'auto',
|
|
62
|
+
animated = true,
|
|
63
|
+
className,
|
|
64
|
+
...rest
|
|
65
|
+
}) => {
|
|
66
|
+
const listRef = React.useRef<HTMLDivElement>(null);
|
|
67
|
+
|
|
68
|
+
// .is-animated contract: set --ind-x/--ind-w from the active tab; recompute on resize.
|
|
69
|
+
React.useLayoutEffect(() => {
|
|
70
|
+
if (!animated) return;
|
|
71
|
+
const list = listRef.current;
|
|
72
|
+
if (!list) return;
|
|
73
|
+
const position = () => {
|
|
74
|
+
const active = list.querySelector<HTMLElement>('.tab[aria-selected="true"]');
|
|
75
|
+
if (!active) return;
|
|
76
|
+
list.style.setProperty('--ind-x', active.offsetLeft + 'px');
|
|
77
|
+
list.style.setProperty('--ind-w', active.offsetWidth + 'px');
|
|
78
|
+
};
|
|
79
|
+
position();
|
|
80
|
+
const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(position) : null;
|
|
81
|
+
ro?.observe(list);
|
|
82
|
+
// Late font load changes tab widths without resizing a fixed-width list —
|
|
83
|
+
// reposition once webfonts settle so the indicator never sits misaligned.
|
|
84
|
+
if (typeof document !== 'undefined' && (document as Document & { fonts?: { ready: Promise<unknown> } }).fonts) {
|
|
85
|
+
(document as Document & { fonts: { ready: Promise<unknown> } }).fonts.ready.then(position).catch(() => undefined);
|
|
86
|
+
}
|
|
87
|
+
return () => ro?.disconnect();
|
|
88
|
+
}, [animated, value, items]);
|
|
89
|
+
|
|
90
|
+
const cls = [
|
|
91
|
+
'tabs',
|
|
92
|
+
variant === 'pill' ? 'tabs-pill' : '',
|
|
93
|
+
size !== 'md' ? `tabs-${size}` : '',
|
|
94
|
+
LAYOUT_CLASS[layout],
|
|
95
|
+
animated ? 'is-animated' : '',
|
|
96
|
+
className || '',
|
|
97
|
+
]
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.join(' ');
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div ref={listRef} className={cls} role="tablist" {...rest}>
|
|
103
|
+
{items.map((it) => (
|
|
104
|
+
<button
|
|
105
|
+
key={it.key}
|
|
106
|
+
type="button"
|
|
107
|
+
role="tab"
|
|
108
|
+
className={it.icon != null ? 'tab is-icon' : 'tab'}
|
|
109
|
+
aria-selected={value === it.key}
|
|
110
|
+
aria-label={it.icon != null && typeof it.label === 'string' ? it.label : undefined}
|
|
111
|
+
tabIndex={value === it.key ? 0 : -1}
|
|
112
|
+
disabled={it.disabled}
|
|
113
|
+
onClick={() => {
|
|
114
|
+
if (!it.disabled && onChange) onChange(it.key);
|
|
115
|
+
}}
|
|
116
|
+
onKeyDown={(e) => {
|
|
117
|
+
// Roving focus — ←/→ move between tabs, wrapping; Home/End jump.
|
|
118
|
+
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight' && e.key !== 'Home' && e.key !== 'End') return;
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
const enabled = items.filter((x) => !x.disabled);
|
|
121
|
+
if (!enabled.length) return;
|
|
122
|
+
const idx = enabled.findIndex((x) => x.key === it.key);
|
|
123
|
+
let next = idx;
|
|
124
|
+
if (e.key === 'ArrowRight') next = (idx + 1) % enabled.length;
|
|
125
|
+
else if (e.key === 'ArrowLeft') next = (idx - 1 + enabled.length) % enabled.length;
|
|
126
|
+
else if (e.key === 'Home') next = 0;
|
|
127
|
+
else next = enabled.length - 1;
|
|
128
|
+
const btns = listRef.current?.querySelectorAll<HTMLButtonElement>('.tab:not([disabled])');
|
|
129
|
+
btns?.[next]?.focus();
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{it.icon != null ? (
|
|
133
|
+
it.icon
|
|
134
|
+
) : (
|
|
135
|
+
<span className="tab-label" data-label={typeof it.label === 'string' ? it.label : undefined}>
|
|
136
|
+
{it.label}
|
|
137
|
+
</span>
|
|
138
|
+
)}
|
|
139
|
+
{it.icon == null && it.count != null ? <span className="tab-count">{it.count}</span> : null}
|
|
140
|
+
</button>
|
|
141
|
+
))}
|
|
142
|
+
{animated ? <span className="tab-indicator" aria-hidden="true" /> : null}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export default Tabs;
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
export type ToastTone = 'success' | 'warning' | 'danger' | 'info' | 'brand' | 'inverse';
|
|
4
|
-
export type ToastStackPosition = 'tr' | 'tl' | 'tc' | 'br' | 'bl' | 'bc';
|
|
5
|
-
|
|
6
|
-
export interface ToastProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
-
tone?: ToastTone;
|
|
8
|
-
/** Leading icon slot (.toast-icon). */
|
|
9
|
-
icon?: React.ReactNode;
|
|
10
|
-
/** Bold first line. */
|
|
11
|
-
title?: React.ReactNode;
|
|
12
|
-
/** Inline action link (.toast-action). */
|
|
13
|
-
action?: React.ReactNode;
|
|
14
|
-
onActionClick?: () => void;
|
|
15
|
-
/** Renders the × and calls back. */
|
|
16
|
-
onClose?: () => void;
|
|
17
|
-
/** Auto-dismiss countdown hairline (.toast-progress). */
|
|
18
|
-
progress?: boolean;
|
|
19
|
-
/** Exit animation hook (.is-leaving) — set just before removal. */
|
|
20
|
-
leaving?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ToastStackProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
24
|
-
/** Corner: tr (default) · tl · tc · br · bl · bc. */
|
|
25
|
-
position?: ToastStackPosition;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export declare const Toast: React.FC<ToastProps>;
|
|
29
|
-
export declare const ToastStack: React.FC<ToastStackProps>;
|
|
30
|
-
export default Toast;
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export type ToastTone = 'success' | 'warning' | 'danger' | 'info' | 'brand' | 'inverse';
|
|
4
|
+
export type ToastStackPosition = 'tr' | 'tl' | 'tc' | 'br' | 'bl' | 'bc';
|
|
5
|
+
|
|
6
|
+
export interface ToastProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
tone?: ToastTone;
|
|
8
|
+
/** Leading icon slot (.toast-icon). */
|
|
9
|
+
icon?: React.ReactNode;
|
|
10
|
+
/** Bold first line. */
|
|
11
|
+
title?: React.ReactNode;
|
|
12
|
+
/** Inline action link (.toast-action). */
|
|
13
|
+
action?: React.ReactNode;
|
|
14
|
+
onActionClick?: () => void;
|
|
15
|
+
/** Renders the × and calls back. */
|
|
16
|
+
onClose?: () => void;
|
|
17
|
+
/** Auto-dismiss countdown hairline (.toast-progress). */
|
|
18
|
+
progress?: boolean;
|
|
19
|
+
/** Exit animation hook (.is-leaving) — set just before removal. */
|
|
20
|
+
leaving?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ToastStackProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
24
|
+
/** Corner: tr (default) · tl · tc · br · bl · bc. */
|
|
25
|
+
position?: ToastStackPosition;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export declare const Toast: React.FC<ToastProps>;
|
|
29
|
+
export declare const ToastStack: React.FC<ToastStackProps>;
|
|
30
|
+
export default Toast;
|