@attrus-ds/core 0.2.0 → 1.0.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.
Files changed (80) hide show
  1. package/LICENSE +27 -27
  2. package/dist/assets/flags/Brazil-rounded.svg +15 -15
  3. package/dist/assets/flags/Brazil-square.svg +16 -16
  4. package/dist/assets/flags/Chile-rounded.svg +10 -10
  5. package/dist/assets/flags/Chile-square.svg +10 -10
  6. package/dist/assets/flags/Colombia-rounded.svg +9 -9
  7. package/dist/assets/flags/Colombia-square.svg +9 -9
  8. package/dist/assets/flags/EUA-rounded.svg +10 -10
  9. package/dist/assets/flags/EUA-square.svg +10 -10
  10. package/dist/assets/flags/EUR-rounded.svg +8 -8
  11. package/dist/assets/flags/EUR-square.svg +8 -8
  12. package/dist/assets/flags/Mexico-rounded.svg +18 -18
  13. package/dist/assets/flags/Mexico-square.svg +18 -18
  14. package/dist/assets/icon-attrus-dark.svg +2 -2
  15. package/dist/assets/icon-attrus-light.svg +2 -2
  16. package/dist/assets/logo-attrus-dark.svg +7 -7
  17. package/dist/assets/logo-attrus-light.svg +7 -7
  18. package/dist/css/components/buttons.css +301 -301
  19. package/dist/css/components/callout.css +195 -195
  20. package/dist/css/components/cards.css +272 -272
  21. package/dist/css/components/combobox.css +319 -319
  22. package/dist/css/components/empty-states.css +61 -61
  23. package/dist/css/components/inputs.css +975 -975
  24. package/dist/css/components/number-input.css +138 -138
  25. package/dist/css/components/overlays.css +374 -374
  26. package/dist/css/components/primitives.css +314 -314
  27. package/dist/css/components/sidebar.css +179 -179
  28. package/dist/css/components/smart-search.css +91 -91
  29. package/dist/css/components/status-pills.css +207 -207
  30. package/dist/css/components/stepper.css +309 -309
  31. package/dist/css/components/table.css +602 -602
  32. package/dist/css/components/tabs.css +319 -319
  33. package/dist/css/components/toast.css +243 -243
  34. package/dist/css/components/tooltip.css +231 -231
  35. package/dist/css/components/topbar.css +148 -148
  36. package/dist/css/tokens.css +713 -666
  37. package/dist/react/AppSidebar/AppSidebar.d.ts +50 -50
  38. package/dist/react/AppSidebar/AppSidebar.tsx +197 -197
  39. package/dist/react/AppTopbar/AppTopbar.d.ts +58 -58
  40. package/dist/react/AppTopbar/AppTopbar.tsx +203 -203
  41. package/dist/react/Atoms/Atoms.d.ts +149 -149
  42. package/dist/react/Atoms/Atoms.tsx +407 -407
  43. package/dist/react/Button/Button.d.ts +25 -25
  44. package/dist/react/Button/Button.tsx +83 -83
  45. package/dist/react/Callout/Callout.d.ts +29 -29
  46. package/dist/react/Callout/Callout.tsx +98 -98
  47. package/dist/react/Card/Card.d.ts +38 -38
  48. package/dist/react/Card/Card.tsx +93 -93
  49. package/dist/react/Combobox/Combobox.d.ts +52 -52
  50. package/dist/react/Combobox/Combobox.tsx +254 -254
  51. package/dist/react/DataTable/DataTable.d.ts +81 -81
  52. package/dist/react/DataTable/DataTable.tsx +308 -308
  53. package/dist/react/Drawer/Drawer.d.ts +35 -35
  54. package/dist/react/Drawer/Drawer.tsx +226 -226
  55. package/dist/react/Input/Input.d.ts +155 -155
  56. package/dist/react/Input/Input.tsx +478 -478
  57. package/dist/react/Modal/Modal.d.ts +27 -27
  58. package/dist/react/Modal/Modal.tsx +173 -173
  59. package/dist/react/NumberInput/NumberInput.d.ts +25 -25
  60. package/dist/react/NumberInput/NumberInput.tsx +117 -117
  61. package/dist/react/Pill/Pill.d.ts +31 -31
  62. package/dist/react/Pill/Pill.tsx +104 -104
  63. package/dist/react/SearchPill/SearchPill.d.ts +14 -14
  64. package/dist/react/SearchPill/SearchPill.tsx +40 -40
  65. package/dist/react/SmartSearch/SmartSearch.d.ts +65 -65
  66. package/dist/react/SmartSearch/SmartSearch.tsx +271 -271
  67. package/dist/react/Stepper/Stepper.d.ts +25 -25
  68. package/dist/react/Stepper/Stepper.tsx +94 -94
  69. package/dist/react/Tabs/Tabs.d.ts +32 -32
  70. package/dist/react/Tabs/Tabs.tsx +147 -147
  71. package/dist/react/Toast/Toast.d.ts +30 -30
  72. package/dist/react/Toast/Toast.tsx +94 -94
  73. package/dist/react/Tooltip/Tooltip.d.ts +23 -23
  74. package/dist/react/Tooltip/Tooltip.tsx +80 -80
  75. package/dist/react/ZeroState/ZeroState.d.ts +17 -17
  76. package/dist/react/ZeroState/ZeroState.tsx +45 -45
  77. package/dist/react/index.ts +35 -35
  78. package/dist/tokens/attrus-tokens.flat.json +1234 -1234
  79. package/dist/tokens/attrus-tokens.json +1089 -1089
  80. package/package.json +66 -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;
@@ -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;