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