@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.
- 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 +713 -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 +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-
|
|
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:
|
|
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-
|
|
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
|
+
}
|