@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,319 +1,319 @@
1
- /* ============================================================
2
- TABS — non-sequential navigation between sibling views
3
- Underline (default) for primary panel nav · pill (segmented)
4
- for compact filters/toolbars. Count badges, icons, keyboard
5
- nav. All values from tokens.
6
-
7
- Extracted from primitives.css (Nov 2025): Tabs is a stateful
8
- navigation component, not a headless atom. See tabs.html.
9
-
10
- Markup contract:
11
- <div class="tabs" role="tablist" aria-label="…">
12
- <button class="tab" role="tab" aria-selected="true">
13
- <span class="tab-label" data-label="All">All</span>
14
- <span class="tab-count">128</span>
15
- </button>
16
- <button class="tab" role="tab" aria-selected="false">…</button>
17
- </div>
18
- Pill variant: add .tabs-pill to the container.
19
- The .tab-label wrapper (with a data-label matching its text)
20
- reserves the bold width so the active/hover state never reflows.
21
- ============================================================ */
22
-
23
- .tabs {
24
- display: flex;
25
- gap: var(--space-1);
26
- border-bottom: 1px solid var(--color-border-default);
27
- position: relative;
28
- }
29
- .tab {
30
- display: inline-flex;
31
- align-items: center;
32
- gap: var(--space-2);
33
- padding: var(--space-3) var(--space-4);
34
- background: transparent;
35
- border: 0;
36
- cursor: pointer;
37
- font-family: var(--font-sans);
38
- font-size: var(--fs-sm);
39
- font-weight: var(--w-medium);
40
- color: var(--color-foreground-secondary);
41
- position: relative;
42
- white-space: nowrap;
43
- transition: var(--transition-fast);
44
- margin-bottom: -1px;
45
- }
46
- .tab:hover { color: var(--color-foreground-default); }
47
- .tab[aria-selected="true"],
48
- .tab.is-active {
49
- color: var(--color-brand-accent-700);
50
- font-weight: var(--w-semibold);
51
- }
52
- .tab[aria-selected="true"]::after,
53
- .tab.is-active::after {
54
- content: '';
55
- position: absolute;
56
- left: var(--space-3);
57
- right: var(--space-3);
58
- bottom: 0;
59
- height: 2px;
60
- background: var(--color-brand-accent-500);
61
- border-radius: var(--radius-xs) var(--radius-xs) 0 0;
62
- }
63
- .tab:focus-visible {
64
- outline: none;
65
- box-shadow: var(--shadow-focus);
66
- border-radius: var(--radius-sm);
67
- }
68
- .tab[disabled] { opacity: 0.45; color: var(--color-foreground-disabled); cursor: not-allowed; }
69
-
70
- .tab-count {
71
- display: inline-flex;
72
- align-items: center;
73
- justify-content: center;
74
- min-width: 20px;
75
- height: 20px;
76
- padding: 0 var(--space-2);
77
- font-family: var(--font-mono);
78
- font-size: var(--fs-xxs);
79
- font-weight: var(--w-bold);
80
- background: var(--color-background-subtle);
81
- color: var(--color-foreground-muted);
82
- border-radius: var(--radius-pill);
83
- }
84
- .tab[aria-selected="true"] .tab-count,
85
- .tab.is-active .tab-count {
86
- background: color-mix(in srgb, var(--color-brand-accent-500) 16%, transparent);
87
- color: var(--color-foreground-accent);
88
- }
89
-
90
- /* Pill tabs — alternative for compact, segmented contexts */
91
- .tabs-pill {
92
- border-bottom: 0;
93
- background: var(--color-background-subtle);
94
- border: 1px solid var(--color-border-default);
95
- border-radius: var(--radius-pill);
96
- padding: var(--space-1);
97
- gap: 0;
98
- }
99
- .tabs-pill .tab {
100
- padding: var(--space-2) var(--space-4);
101
- border-radius: var(--radius-pill);
102
- margin-bottom: 0;
103
- }
104
- .tabs-pill .tab::after { display: none; }
105
- .tabs-pill .tab[aria-selected="true"],
106
- .tabs-pill .tab.is-active {
107
- background: var(--color-background-surface);
108
- color: var(--color-foreground-default);
109
- box-shadow: var(--shadow-xs);
110
- }
111
-
112
- /* ---- Pill layout options ----------------------------------- */
113
- /* Mobile: the segmented shell fills the row and scrolls internally when
114
- its segments overflow (B1). The shell itself never wraps; tabs keep
115
- their size and the user swipes horizontally inside the pill. */
116
- @media (max-width: 720px) {
117
- .tabs-pill {
118
- display: flex;
119
- width: 100%;
120
- overflow-x: auto;
121
- flex-wrap: nowrap;
122
- scrollbar-width: none;
123
- -webkit-overflow-scrolling: touch;
124
- scroll-snap-type: x proximity;
125
- }
126
- .tabs-pill::-webkit-scrollbar { display: none; }
127
- .tabs-pill .tab { flex: 0 0 auto; scroll-snap-align: start; }
128
- }
129
-
130
- /* Equal-width segments: every tab the same width (good for 2–4
131
- short, symmetric options like Day/Week/Month). */
132
- .tabs-pill.is-equal { display: inline-flex; }
133
- .tabs-pill.is-equal .tab { flex: 1 1 0; justify-content: center; }
134
-
135
- /* Full-width: the segmented control fills its container. */
136
- .tabs-pill.is-block { display: flex; width: 100%; }
137
- .tabs-pill.is-block .tab { flex: 1 1 0; justify-content: center; }
138
-
139
- /* Equal-width underline tabs: each item takes the same width with its
140
- content horizontally centered. Use when tabs should read as a balanced row. */
141
- .tabs.is-block { display: flex; width: 100%; }
142
- .tabs.is-block .tab { flex: 1 1 0; justify-content: center; min-width: 0; }
143
- .tabs.is-block .tab-label { justify-items: center; min-width: 0; }
144
- .tabs.is-equal { display: flex; }
145
- .tabs.is-equal .tab { flex: 1 1 0; justify-content: center; min-width: 0; }
146
- .tabs.is-equal .tab-label { justify-items: center; min-width: 0; }
147
- /* Equal-width pill segments shouldn't overflow their labels either */
148
- .tabs-pill.is-equal .tab, .tabs-pill.is-block .tab { min-width: 0; }
149
-
150
- /* Icon-only pill: square segments, label provided via aria-label. */
151
- .tabs-pill .tab.is-icon { padding-left: var(--space-2); padding-right: var(--space-2); aspect-ratio: 1; justify-content: center; }
152
- .tabs-pill .tab.is-icon i, .tabs-pill .tab.is-icon svg { width: 16px; height: 16px; }
153
-
154
- /* ============================================================
155
- SIZES — apply on the container; both variants honour them.
156
- .tabs-sm dense (toolbars, card headers, drawers)
157
- (default) standard
158
- .tabs-lg spacious (page-level primary nav)
159
- ============================================================ */
160
- .tabs-sm .tab {
161
- padding: var(--space-2) var(--space-3);
162
- font-size: var(--fs-xs);
163
- gap: var(--space-1);
164
- }
165
- .tabs-sm .tab-count { min-width: 16px; height: 16px; font-size: 10px; padding: 0 var(--space-1); }
166
- .tabs-sm .tab i, .tabs-sm .tab svg { width: 13px; height: 13px; }
167
- .tabs-sm.tabs-pill .tab { padding: var(--space-1) var(--space-3); }
168
-
169
- .tabs-lg .tab {
170
- padding: var(--space-4) var(--space-5);
171
- font-size: var(--fs-md);
172
- gap: var(--space-3);
173
- }
174
- .tabs-lg .tab-count { min-width: 22px; height: 22px; font-size: var(--fs-xs); }
175
- .tabs-lg .tab i, .tabs-lg .tab svg { width: 17px; height: 17px; }
176
- .tabs-lg.tabs-pill .tab { padding: var(--space-3) var(--space-5); }
177
-
178
- /* Mobile: tabs-lg steps down to the default size so page-level nav
179
- isn't oversized on phones (C). */
180
- @media (max-width: 720px) {
181
- .tabs-lg .tab { padding: var(--space-3) var(--space-4); font-size: var(--fs-sm); gap: var(--space-2); }
182
- .tabs-lg .tab-count { min-width: 18px; height: 18px; font-size: var(--fs-xxs); }
183
- .tabs-lg .tab i, .tabs-lg .tab svg { width: 15px; height: 15px; }
184
- .tabs-lg.tabs-pill .tab { padding: var(--space-2) var(--space-4); }
185
- .tabs-lg .tab[aria-selected="true"]::after,
186
- .tabs-lg .tab.is-active::after { left: var(--space-3); right: var(--space-3); }
187
- }
188
- /* Keep the underline indicator inset consistent at each size */
189
- .tabs-sm .tab[aria-selected="true"]::after,
190
- .tabs-sm .tab.is-active::after { left: var(--space-2); right: var(--space-2); }
191
- .tabs-lg .tab[aria-selected="true"]::after,
192
- .tabs-lg .tab.is-active::after { left: var(--space-4); right: var(--space-4); }
193
-
194
- /* Tab icons inherit the size of the type they sit beside */
195
- .tab i, .tab svg { width: 15px; height: 15px; stroke-width: 2; }
196
-
197
- /* Label wrapper: reserves the SEMIBOLD width up front so toggling
198
- weight between states never reflows the row. The hidden ::after
199
- holds a bold copy of the text in a second (0-height) grid row, so
200
- the column width = max(regular, bold) = bold. No layout shift. */
201
- .tab-label {
202
- display: inline-grid;
203
- grid-template-columns: max-content;
204
- }
205
- .tab-label::after {
206
- content: attr(data-label);
207
- font-weight: var(--w-semibold);
208
- height: 0;
209
- visibility: hidden;
210
- overflow: hidden;
211
- pointer-events: none;
212
- user-select: none;
213
- }
214
-
215
- /* ============================================================
216
- ANIMATION — opt-in sliding indicator + panel fade
217
- Add .is-animated to the .tabs container. A single
218
- <span class="tab-indicator" aria-hidden="true"></span> is
219
- placed as the LAST child of the tablist; the JS controller
220
- sets --ind-x (translateX) and --ind-w (width) from the active
221
- tab, and recomputes on resize. Without the controller the
222
- indicator stays at 0 width — so .is-animated REQUIRES the JS.
223
- ============================================================ */
224
- .tabs.is-animated { position: relative; }
225
-
226
- /* Underline: suppress the per-tab static bar, show one sliding bar */
227
- .tabs.is-animated .tab[aria-selected="true"]::after,
228
- .tabs.is-animated .tab.is-active::after { display: none; }
229
-
230
- .tab-indicator {
231
- position: absolute;
232
- left: 0;
233
- bottom: 0;
234
- height: 2px;
235
- width: var(--ind-w, 0);
236
- transform: translateX(var(--ind-x, 0));
237
- background: var(--color-brand-accent-500);
238
- border-radius: var(--radius-xs) var(--radius-xs) 0 0;
239
- pointer-events: none;
240
- transition: transform var(--duration-base) var(--ease-emphasis),
241
- width var(--duration-base) var(--ease-emphasis);
242
- }
243
-
244
- /* Pill: the lifted white surface becomes a sliding element behind tabs */
245
- .tabs-pill.is-animated .tab { z-index: 1; }
246
- .tabs-pill.is-animated .tab[aria-selected="true"],
247
- .tabs-pill.is-animated .tab.is-active { background: transparent; box-shadow: none; }
248
- .tabs-pill.is-animated .tab-indicator {
249
- top: var(--space-1);
250
- bottom: var(--space-1);
251
- height: auto;
252
- background: var(--color-background-surface);
253
- box-shadow: var(--shadow-xs);
254
- border-radius: var(--radius-pill);
255
- z-index: 0;
256
- }
257
-
258
- /* Panel fade — add .tab-panel-animated to each [role="tabpanel"] */
259
- @keyframes tab-panel-in {
260
- from { opacity: 0; transform: translateY(4px); }
261
- to { opacity: 1; transform: translateY(0); }
262
- }
263
- .tab-panel-animated:not([hidden]) {
264
- animation: tab-panel-in var(--duration-base) var(--ease-standard);
265
- }
266
-
267
- @media (prefers-reduced-motion: reduce) {
268
- .tab { transition: none; }
269
- .tab-indicator { transition: none; }
270
- .tab-panel-animated:not([hidden]) { animation: none; }
271
- }
272
-
273
- /* ============================================================
274
- MOBILE — horizontal scroll for overflowing underline tabs
275
- Underline tabs scroll sideways by DEFAULT below 720px (A) so a
276
- long row never overflows the page or wraps. `.is-scroll` forces
277
- the same behaviour at any width. Pill tabs get their own mobile
278
- scroll rule above (the shell scrolls internally, B1).
279
- Note: overflow clips the box-shadow focus ring slightly at the
280
- top edge — acceptable tradeoff; the underline still reads.
281
- ============================================================ */
282
- @media (max-width: 720px) {
283
- .tabs:not(.tabs-pill):not(.is-block):not(.is-equal) {
284
- overflow-x: auto;
285
- flex-wrap: nowrap;
286
- scrollbar-width: none;
287
- -webkit-overflow-scrolling: touch;
288
- scroll-snap-type: x proximity;
289
- }
290
- .tabs:not(.tabs-pill):not(.is-block):not(.is-equal)::-webkit-scrollbar { display: none; }
291
- .tabs:not(.tabs-pill):not(.is-block):not(.is-equal) .tab { scroll-snap-align: start; flex: 0 0 auto; }
292
- }
293
- .tabs.is-scroll {
294
- overflow-x: auto;
295
- flex-wrap: nowrap;
296
- scrollbar-width: none;
297
- -webkit-overflow-scrolling: touch;
298
- scroll-snap-type: x proximity;
299
- }
300
- .tabs.is-scroll::-webkit-scrollbar { display: none; }
301
- .tabs.is-scroll .tab { scroll-snap-align: start; flex: 0 0 auto; }
302
- /* In a scroll container the box-shadow ring is clipped at the top edge,
303
- so swap to an inset outline that lives inside the tab. */
304
- .tabs.is-scroll .tab:focus-visible {
305
- box-shadow: none;
306
- outline: 2px solid var(--color-border-focus);
307
- outline-offset: -3px;
308
- }
309
-
310
- /* ============================================================
311
- TOUCH TARGETS — guarantee ≥44px hit height on touch devices.
312
- Tabs (esp. .tabs-sm) are visually short; on a coarse pointer
313
- we pad the tap area up to the platform minimum without
314
- changing the visual type size.
315
- ============================================================ */
316
- @media (pointer: coarse) {
317
- .tab { min-height: 44px; }
318
- .tabs-pill .tab { min-height: 40px; }
319
- }
1
+ /* ============================================================
2
+ TABS — non-sequential navigation between sibling views
3
+ Underline (default) for primary panel nav · pill (segmented)
4
+ for compact filters/toolbars. Count badges, icons, keyboard
5
+ nav. All values from tokens.
6
+
7
+ Extracted from primitives.css (Nov 2025): Tabs is a stateful
8
+ navigation component, not a headless atom. See tabs.html.
9
+
10
+ Markup contract:
11
+ <div class="tabs" role="tablist" aria-label="…">
12
+ <button class="tab" role="tab" aria-selected="true">
13
+ <span class="tab-label" data-label="All">All</span>
14
+ <span class="tab-count">128</span>
15
+ </button>
16
+ <button class="tab" role="tab" aria-selected="false">…</button>
17
+ </div>
18
+ Pill variant: add .tabs-pill to the container.
19
+ The .tab-label wrapper (with a data-label matching its text)
20
+ reserves the bold width so the active/hover state never reflows.
21
+ ============================================================ */
22
+
23
+ .tabs {
24
+ display: flex;
25
+ gap: var(--space-1);
26
+ border-bottom: 1px solid var(--color-border-default);
27
+ position: relative;
28
+ }
29
+ .tab {
30
+ display: inline-flex;
31
+ align-items: center;
32
+ gap: var(--space-2);
33
+ padding: var(--space-3) var(--space-4);
34
+ background: transparent;
35
+ border: 0;
36
+ cursor: pointer;
37
+ font-family: var(--font-sans);
38
+ font-size: var(--fs-sm);
39
+ font-weight: var(--w-medium);
40
+ color: var(--color-foreground-secondary);
41
+ position: relative;
42
+ white-space: nowrap;
43
+ transition: var(--transition-fast);
44
+ margin-bottom: -1px;
45
+ }
46
+ .tab:hover { color: var(--color-foreground-default); }
47
+ .tab[aria-selected="true"],
48
+ .tab.is-active {
49
+ color: var(--color-brand-accent-700);
50
+ font-weight: var(--w-semibold);
51
+ }
52
+ .tab[aria-selected="true"]::after,
53
+ .tab.is-active::after {
54
+ content: '';
55
+ position: absolute;
56
+ left: var(--space-3);
57
+ right: var(--space-3);
58
+ bottom: 0;
59
+ height: 2px;
60
+ background: var(--color-accent-solid);
61
+ border-radius: var(--radius-xs) var(--radius-xs) 0 0;
62
+ }
63
+ .tab:focus-visible {
64
+ outline: none;
65
+ box-shadow: var(--shadow-focus);
66
+ border-radius: var(--radius-sm);
67
+ }
68
+ .tab[disabled] { opacity: 0.45; color: var(--color-foreground-disabled); cursor: not-allowed; }
69
+
70
+ .tab-count {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ min-width: 20px;
75
+ height: 20px;
76
+ padding: 0 var(--space-2);
77
+ font-family: var(--font-mono);
78
+ font-size: var(--fs-xxs);
79
+ font-weight: var(--w-bold);
80
+ background: var(--color-background-subtle);
81
+ color: var(--color-foreground-muted);
82
+ border-radius: var(--radius-pill);
83
+ }
84
+ .tab[aria-selected="true"] .tab-count,
85
+ .tab.is-active .tab-count {
86
+ background: var(--color-accent-subtle);
87
+ color: var(--color-foreground-accent);
88
+ }
89
+
90
+ /* Pill tabs — alternative for compact, segmented contexts */
91
+ .tabs-pill {
92
+ border-bottom: 0;
93
+ background: var(--color-background-subtle);
94
+ border: 1px solid var(--color-border-default);
95
+ border-radius: var(--radius-pill);
96
+ padding: var(--space-1);
97
+ gap: 0;
98
+ }
99
+ .tabs-pill .tab {
100
+ padding: var(--space-2) var(--space-4);
101
+ border-radius: var(--radius-pill);
102
+ margin-bottom: 0;
103
+ }
104
+ .tabs-pill .tab::after { display: none; }
105
+ .tabs-pill .tab[aria-selected="true"],
106
+ .tabs-pill .tab.is-active {
107
+ background: var(--color-background-surface);
108
+ color: var(--color-foreground-default);
109
+ box-shadow: var(--shadow-xs);
110
+ }
111
+
112
+ /* ---- Pill layout options ----------------------------------- */
113
+ /* Mobile: the segmented shell fills the row and scrolls internally when
114
+ its segments overflow (B1). The shell itself never wraps; tabs keep
115
+ their size and the user swipes horizontally inside the pill. */
116
+ @media (max-width: 720px) {
117
+ .tabs-pill {
118
+ display: flex;
119
+ width: 100%;
120
+ overflow-x: auto;
121
+ flex-wrap: nowrap;
122
+ scrollbar-width: none;
123
+ -webkit-overflow-scrolling: touch;
124
+ scroll-snap-type: x proximity;
125
+ }
126
+ .tabs-pill::-webkit-scrollbar { display: none; }
127
+ .tabs-pill .tab { flex: 0 0 auto; scroll-snap-align: start; }
128
+ }
129
+
130
+ /* Equal-width segments: every tab the same width (good for 2–4
131
+ short, symmetric options like Day/Week/Month). */
132
+ .tabs-pill.is-equal { display: inline-flex; }
133
+ .tabs-pill.is-equal .tab { flex: 1 1 0; justify-content: center; }
134
+
135
+ /* Full-width: the segmented control fills its container. */
136
+ .tabs-pill.is-block { display: flex; width: 100%; }
137
+ .tabs-pill.is-block .tab { flex: 1 1 0; justify-content: center; }
138
+
139
+ /* Equal-width underline tabs: each item takes the same width with its
140
+ content horizontally centered. Use when tabs should read as a balanced row. */
141
+ .tabs.is-block { display: flex; width: 100%; }
142
+ .tabs.is-block .tab { flex: 1 1 0; justify-content: center; min-width: 0; }
143
+ .tabs.is-block .tab-label { justify-items: center; min-width: 0; }
144
+ .tabs.is-equal { display: flex; }
145
+ .tabs.is-equal .tab { flex: 1 1 0; justify-content: center; min-width: 0; }
146
+ .tabs.is-equal .tab-label { justify-items: center; min-width: 0; }
147
+ /* Equal-width pill segments shouldn't overflow their labels either */
148
+ .tabs-pill.is-equal .tab, .tabs-pill.is-block .tab { min-width: 0; }
149
+
150
+ /* Icon-only pill: square segments, label provided via aria-label. */
151
+ .tabs-pill .tab.is-icon { padding-left: var(--space-2); padding-right: var(--space-2); aspect-ratio: 1; justify-content: center; }
152
+ .tabs-pill .tab.is-icon i, .tabs-pill .tab.is-icon svg { width: 16px; height: 16px; }
153
+
154
+ /* ============================================================
155
+ SIZES — apply on the container; both variants honour them.
156
+ .tabs-sm dense (toolbars, card headers, drawers)
157
+ (default) standard
158
+ .tabs-lg spacious (page-level primary nav)
159
+ ============================================================ */
160
+ .tabs-sm .tab {
161
+ padding: var(--space-2) var(--space-3);
162
+ font-size: var(--fs-xs);
163
+ gap: var(--space-1);
164
+ }
165
+ .tabs-sm .tab-count { min-width: 16px; height: 16px; font-size: 10px; padding: 0 var(--space-1); }
166
+ .tabs-sm .tab i, .tabs-sm .tab svg { width: 13px; height: 13px; }
167
+ .tabs-sm.tabs-pill .tab { padding: var(--space-1) var(--space-3); }
168
+
169
+ .tabs-lg .tab {
170
+ padding: var(--space-4) var(--space-5);
171
+ font-size: var(--fs-md);
172
+ gap: var(--space-3);
173
+ }
174
+ .tabs-lg .tab-count { min-width: 22px; height: 22px; font-size: var(--fs-xs); }
175
+ .tabs-lg .tab i, .tabs-lg .tab svg { width: 17px; height: 17px; }
176
+ .tabs-lg.tabs-pill .tab { padding: var(--space-3) var(--space-5); }
177
+
178
+ /* Mobile: tabs-lg steps down to the default size so page-level nav
179
+ isn't oversized on phones (C). */
180
+ @media (max-width: 720px) {
181
+ .tabs-lg .tab { padding: var(--space-3) var(--space-4); font-size: var(--fs-sm); gap: var(--space-2); }
182
+ .tabs-lg .tab-count { min-width: 18px; height: 18px; font-size: var(--fs-xxs); }
183
+ .tabs-lg .tab i, .tabs-lg .tab svg { width: 15px; height: 15px; }
184
+ .tabs-lg.tabs-pill .tab { padding: var(--space-2) var(--space-4); }
185
+ .tabs-lg .tab[aria-selected="true"]::after,
186
+ .tabs-lg .tab.is-active::after { left: var(--space-3); right: var(--space-3); }
187
+ }
188
+ /* Keep the underline indicator inset consistent at each size */
189
+ .tabs-sm .tab[aria-selected="true"]::after,
190
+ .tabs-sm .tab.is-active::after { left: var(--space-2); right: var(--space-2); }
191
+ .tabs-lg .tab[aria-selected="true"]::after,
192
+ .tabs-lg .tab.is-active::after { left: var(--space-4); right: var(--space-4); }
193
+
194
+ /* Tab icons inherit the size of the type they sit beside */
195
+ .tab i, .tab svg { width: 15px; height: 15px; stroke-width: 2; }
196
+
197
+ /* Label wrapper: reserves the SEMIBOLD width up front so toggling
198
+ weight between states never reflows the row. The hidden ::after
199
+ holds a bold copy of the text in a second (0-height) grid row, so
200
+ the column width = max(regular, bold) = bold. No layout shift. */
201
+ .tab-label {
202
+ display: inline-grid;
203
+ grid-template-columns: max-content;
204
+ }
205
+ .tab-label::after {
206
+ content: attr(data-label);
207
+ font-weight: var(--w-semibold);
208
+ height: 0;
209
+ visibility: hidden;
210
+ overflow: hidden;
211
+ pointer-events: none;
212
+ user-select: none;
213
+ }
214
+
215
+ /* ============================================================
216
+ ANIMATION — opt-in sliding indicator + panel fade
217
+ Add .is-animated to the .tabs container. A single
218
+ <span class="tab-indicator" aria-hidden="true"></span> is
219
+ placed as the LAST child of the tablist; the JS controller
220
+ sets --ind-x (translateX) and --ind-w (width) from the active
221
+ tab, and recomputes on resize. Without the controller the
222
+ indicator stays at 0 width — so .is-animated REQUIRES the JS.
223
+ ============================================================ */
224
+ .tabs.is-animated { position: relative; }
225
+
226
+ /* Underline: suppress the per-tab static bar, show one sliding bar */
227
+ .tabs.is-animated .tab[aria-selected="true"]::after,
228
+ .tabs.is-animated .tab.is-active::after { display: none; }
229
+
230
+ .tab-indicator {
231
+ position: absolute;
232
+ left: 0;
233
+ bottom: 0;
234
+ height: 2px;
235
+ width: var(--ind-w, 0);
236
+ transform: translateX(var(--ind-x, 0));
237
+ background: var(--color-accent-solid);
238
+ border-radius: var(--radius-xs) var(--radius-xs) 0 0;
239
+ pointer-events: none;
240
+ transition: transform var(--duration-base) var(--ease-emphasis),
241
+ width var(--duration-base) var(--ease-emphasis);
242
+ }
243
+
244
+ /* Pill: the lifted white surface becomes a sliding element behind tabs */
245
+ .tabs-pill.is-animated .tab { z-index: 1; }
246
+ .tabs-pill.is-animated .tab[aria-selected="true"],
247
+ .tabs-pill.is-animated .tab.is-active { background: transparent; box-shadow: none; }
248
+ .tabs-pill.is-animated .tab-indicator {
249
+ top: var(--space-1);
250
+ bottom: var(--space-1);
251
+ height: auto;
252
+ background: var(--color-background-surface);
253
+ box-shadow: var(--shadow-xs);
254
+ border-radius: var(--radius-pill);
255
+ z-index: 0;
256
+ }
257
+
258
+ /* Panel fade — add .tab-panel-animated to each [role="tabpanel"] */
259
+ @keyframes tab-panel-in {
260
+ from { opacity: 0; transform: translateY(4px); }
261
+ to { opacity: 1; transform: translateY(0); }
262
+ }
263
+ .tab-panel-animated:not([hidden]) {
264
+ animation: tab-panel-in var(--duration-base) var(--ease-standard);
265
+ }
266
+
267
+ @media (prefers-reduced-motion: reduce) {
268
+ .tab { transition: none; }
269
+ .tab-indicator { transition: none; }
270
+ .tab-panel-animated:not([hidden]) { animation: none; }
271
+ }
272
+
273
+ /* ============================================================
274
+ MOBILE — horizontal scroll for overflowing underline tabs
275
+ Underline tabs scroll sideways by DEFAULT below 720px (A) so a
276
+ long row never overflows the page or wraps. `.is-scroll` forces
277
+ the same behaviour at any width. Pill tabs get their own mobile
278
+ scroll rule above (the shell scrolls internally, B1).
279
+ Note: overflow clips the box-shadow focus ring slightly at the
280
+ top edge — acceptable tradeoff; the underline still reads.
281
+ ============================================================ */
282
+ @media (max-width: 720px) {
283
+ .tabs:not(.tabs-pill):not(.is-block):not(.is-equal) {
284
+ overflow-x: auto;
285
+ flex-wrap: nowrap;
286
+ scrollbar-width: none;
287
+ -webkit-overflow-scrolling: touch;
288
+ scroll-snap-type: x proximity;
289
+ }
290
+ .tabs:not(.tabs-pill):not(.is-block):not(.is-equal)::-webkit-scrollbar { display: none; }
291
+ .tabs:not(.tabs-pill):not(.is-block):not(.is-equal) .tab { scroll-snap-align: start; flex: 0 0 auto; }
292
+ }
293
+ .tabs.is-scroll {
294
+ overflow-x: auto;
295
+ flex-wrap: nowrap;
296
+ scrollbar-width: none;
297
+ -webkit-overflow-scrolling: touch;
298
+ scroll-snap-type: x proximity;
299
+ }
300
+ .tabs.is-scroll::-webkit-scrollbar { display: none; }
301
+ .tabs.is-scroll .tab { scroll-snap-align: start; flex: 0 0 auto; }
302
+ /* In a scroll container the box-shadow ring is clipped at the top edge,
303
+ so swap to an inset outline that lives inside the tab. */
304
+ .tabs.is-scroll .tab:focus-visible {
305
+ box-shadow: none;
306
+ outline: 2px solid var(--color-border-focus);
307
+ outline-offset: -3px;
308
+ }
309
+
310
+ /* ============================================================
311
+ TOUCH TARGETS — guarantee ≥44px hit height on touch devices.
312
+ Tabs (esp. .tabs-sm) are visually short; on a coarse pointer
313
+ we pad the tap area up to the platform minimum without
314
+ changing the visual type size.
315
+ ============================================================ */
316
+ @media (pointer: coarse) {
317
+ .tab { min-height: 44px; }
318
+ .tabs-pill .tab { min-height: 40px; }
319
+ }