@attrus-ds/core 1.0.13 → 1.0.14
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/dist/css/components/buttons.css +46 -15
- package/dist/css/components/callout.css +45 -8
- package/dist/css/components/combobox.css +36 -23
- package/dist/css/components/inputs.css +55 -22
- package/dist/css/components/overlays.css +1 -1
- package/dist/css/components/primitives.css +68 -0
- package/dist/css/components/sidebar.css +33 -0
- package/dist/css/components/smart-search.css +56 -23
- package/dist/css/components/status-pills.css +6 -6
- package/dist/css/components/table.css +602 -36
- package/dist/css/components/tabs.css +1 -0
- package/dist/css/components/toggle-chip.css +2 -2
- package/dist/css/components/topbar.css +180 -20
- package/dist/css/tokens.css +50 -10
- package/dist/react/Button/Button.d.ts +10 -2
- package/dist/react/Button/Button.tsx +12 -4
- package/dist/react/Combobox/Combobox.d.ts +5 -0
- package/dist/react/Combobox/Combobox.tsx +26 -4
- package/dist/react/DataTable/DataTable.d.ts +91 -0
- package/dist/react/DataTable/DataTable.tsx +413 -35
- package/dist/react/SearchPill/SearchPill.d.ts +3 -0
- package/dist/react/SearchPill/SearchPill.tsx +6 -1
- package/dist/react/SmartSearch/SmartSearch.d.ts +4 -0
- package/dist/react/SmartSearch/SmartSearch.tsx +14 -1
- package/package.json +1 -1
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
gap: 0;
|
|
99
99
|
}
|
|
100
100
|
.tabs-pill .tab {
|
|
101
|
+
transition: background var(--transition-fast), color var(--transition-fast), box-shadow var(--transition-fast);
|
|
101
102
|
padding: var(--space-2) var(--space-4);
|
|
102
103
|
border-radius: var(--radius-pill);
|
|
103
104
|
margin-bottom: 0;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
display: inline-flex;
|
|
16
16
|
align-items: center;
|
|
17
17
|
gap: var(--space-2);
|
|
18
|
-
height:
|
|
18
|
+
height: var(--control-h-sm);
|
|
19
19
|
padding: 0 var(--space-4);
|
|
20
20
|
background: var(--color-background-surface);
|
|
21
21
|
border: 1px solid var(--color-border-default);
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
|
|
95
95
|
/* ---------- Dense ---------- */
|
|
96
96
|
.toggle-chip-sm {
|
|
97
|
-
height:
|
|
97
|
+
height: var(--control-h-xs);
|
|
98
98
|
padding: 0 var(--space-3);
|
|
99
99
|
font-size: var(--fs-xs);
|
|
100
100
|
}
|
|
@@ -5,26 +5,42 @@
|
|
|
5
5
|
(preview/topbar.html). Token-driven → themes automatically.
|
|
6
6
|
|
|
7
7
|
Class map:
|
|
8
|
-
.topbar component root (
|
|
9
|
-
.
|
|
10
|
-
.
|
|
11
|
-
.
|
|
8
|
+
.topbar component root (60px bar, surface)
|
|
9
|
+
.topbar.is-sm/.is-lg bar density — retunes --tb-control-h (52 / 72px)
|
|
10
|
+
.topbar.is-compact narrow layout at any viewport (see Compact below)
|
|
11
|
+
.topbar.is-glass frosted chrome via --glass-chrome
|
|
12
|
+
.topbar.is-glass.is-sticky pinned, which is what makes the blur mean anything
|
|
13
|
+
.tb-control unified control, height = --tb-control-h
|
|
14
|
+
.tb-control.icon icon-only square
|
|
15
|
+
.tb-control.sm/.lg one-off control size (leaves bar height alone)
|
|
12
16
|
.tb-live live-session indicator (non-interactive)
|
|
13
|
-
.tb-live .dot
|
|
17
|
+
.tb-live .dot status dot
|
|
14
18
|
.tb-clock mono elapsed-time readout
|
|
15
19
|
.tb-flag-label ENG / lang label inside a control
|
|
20
|
+
.tb-lang language control — tagged so compact can hide it
|
|
16
21
|
.tb-bell notification button wrapper (badge anchor)
|
|
17
22
|
.tb-bell .badge unread red dot
|
|
18
|
-
.tb-avatar user avatar chip (
|
|
23
|
+
.tb-avatar user avatar chip (32px)
|
|
24
|
+
.tb-mark-full brand lockup (hidden when compact)
|
|
25
|
+
.tb-mark-symbol brand glyph (hidden when compact)
|
|
19
26
|
.tb-spacer margin-left:auto group wrapper
|
|
27
|
+
.tb-account avatar + identity trigger
|
|
28
|
+
.tb-account-id name / company stack inside the trigger
|
|
29
|
+
.tb-account-menu dropdown (head, -sep, -item, -item.is-danger)
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
The primary action is NOT a topbar class: use the canonical .btn .btn-primary
|
|
32
|
+
.btn-md (buttons.css) so the bar's CTA stays the system's button.
|
|
33
|
+
|
|
34
|
+
Canonical control height: var(--tb-control-h), which carries 40px as its own
|
|
35
|
+
fallback — a control used outside a .topbar still lands on the right box. All bar items
|
|
36
|
+
(controls, search, account) read it, so they scale together. 40px matches the
|
|
37
|
+
standard 40px input/button box used elsewhere, so a .tb-control and a .btn-md
|
|
38
|
+
in the same row align without per-instance tuning.
|
|
23
39
|
============================================================ */
|
|
24
40
|
|
|
25
41
|
.topbar {
|
|
26
42
|
height: 60px; flex-shrink: 0;
|
|
27
|
-
--tb-control-h:
|
|
43
|
+
--tb-control-h: var(--control-h-md);
|
|
28
44
|
background: var(--color-background-surface);
|
|
29
45
|
border-bottom: 1px solid var(--color-border-default);
|
|
30
46
|
display: flex; align-items: center; gap: var(--space-2-5);
|
|
@@ -33,7 +49,7 @@
|
|
|
33
49
|
|
|
34
50
|
/* Unified control — every interactive pill/button in the bar */
|
|
35
51
|
.tb-control {
|
|
36
|
-
height: var(--tb-control-h); min-height: var(--tb-control-h); box-sizing: border-box;
|
|
52
|
+
height: var(--tb-control-h, var(--control-h-md)); min-height: var(--tb-control-h, var(--control-h-md)); box-sizing: border-box;
|
|
37
53
|
display: inline-flex; align-items: center; gap: var(--space-2-5);
|
|
38
54
|
padding: 0 var(--space-3-5);
|
|
39
55
|
border-radius: var(--radius-sm-md);
|
|
@@ -48,17 +64,28 @@
|
|
|
48
64
|
.tb-control svg, .tb-control i { width: 14px; height: 14px; stroke-width: var(--icon-stroke); flex-shrink: 0; }
|
|
49
65
|
|
|
50
66
|
/* Icon-only square */
|
|
51
|
-
.tb-control.icon { width: var(--tb-control-h); padding: 0; justify-content: center; }
|
|
67
|
+
.tb-control.icon { width: var(--tb-control-h, var(--control-h-md)); padding: 0; justify-content: center; }
|
|
52
68
|
|
|
53
|
-
/* The bar's primary action is the canonical
|
|
54
|
-
|
|
69
|
+
/* The bar's primary action is the canonical .btn (buttons.css) — no topbar-local
|
|
70
|
+
button style. The bar does impose its own box on it, so the CTA tracks the
|
|
71
|
+
density instead of depending on the author picking the matching .btn-* class:
|
|
72
|
+
both now land on the same step of the height scale. */
|
|
73
|
+
.topbar .btn { height: var(--tb-control-h, var(--control-h-md)); }
|
|
55
74
|
|
|
56
75
|
/* Lang label inside a control */
|
|
57
76
|
.tb-control .tb-flag-label { font-size: var(--fs-sm); font-weight: var(--w-regular); color: var(--color-foreground-secondary); }
|
|
58
77
|
.tb-control .chev { opacity: .6; }
|
|
59
78
|
.tb-control .chev svg, .tb-control .chev i { width: 11px; height: 11px; }
|
|
60
79
|
|
|
61
|
-
/* Live indicator — looks like a control but is not interactive
|
|
80
|
+
/* Live indicator — looks like a control but is not interactive.
|
|
81
|
+
Baseline, not centre: the clock is mono --fs-xxs next to a sans --fs-sm label,
|
|
82
|
+
Alignment note: do NOT reach for align-items:baseline here. In a fixed-height
|
|
83
|
+
flex box it aligns the items to each other but glues the run to the top (10px
|
|
84
|
+
off centre in a 40px pill). The clock reading "higher" is a LEADING problem:
|
|
85
|
+
label is --fs-sm (18px box) beside a --fs-xxs clock (15px box), so centring
|
|
86
|
+
boxes of different heights centres the leading, not the glyphs. line-height:1
|
|
87
|
+
on both makes each box hug its letterforms, and the default centring lines
|
|
88
|
+
them up. */
|
|
62
89
|
.tb-live { cursor: default; color: var(--color-foreground-secondary); }
|
|
63
90
|
.tb-live:hover { border-color: var(--color-border-default); background: var(--color-background-surface); color: var(--color-foreground-secondary); }
|
|
64
91
|
.tb-live .dot {
|
|
@@ -66,9 +93,10 @@
|
|
|
66
93
|
background: var(--color-success-500);
|
|
67
94
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-success-500) 15%, transparent);
|
|
68
95
|
}
|
|
69
|
-
.tb-live .lbl { font-size: var(--fs-sm); font-weight: var(--w-regular); }
|
|
96
|
+
.tb-live .lbl { font-size: var(--fs-sm); font-weight: var(--w-regular); line-height: 1; }
|
|
70
97
|
.tb-clock { font-family: var(--font-mono);
|
|
71
|
-
font-size-adjust: var(--font-mono-adjust); font-size: var(--fs-xxs);
|
|
98
|
+
font-size-adjust: var(--font-mono-adjust); font-size: var(--fs-xxs); line-height: 1;
|
|
99
|
+
color: var(--color-foreground-muted); margin-left: var(--space-1); }
|
|
72
100
|
|
|
73
101
|
/* Notification button + unread badge */
|
|
74
102
|
.tb-bell .badge-anchor { position: relative; display: inline-flex; }
|
|
@@ -80,7 +108,8 @@
|
|
|
80
108
|
|
|
81
109
|
/* Avatar chip */
|
|
82
110
|
.tb-avatar {
|
|
83
|
-
|
|
111
|
+
/* One step below the control — the relation it already had at md (40→32) */
|
|
112
|
+
width: var(--control-h-sm); height: var(--control-h-sm); border-radius: var(--radius-pill);
|
|
84
113
|
background: var(--color-brand-solid); color: var(--color-brand-on-solid);
|
|
85
114
|
font-size: var(--fs-xs); font-weight: var(--w-bold);
|
|
86
115
|
display: inline-flex; align-items: center; justify-content: center;
|
|
@@ -91,17 +120,72 @@
|
|
|
91
120
|
.tb-spacer { margin-left: auto; display: flex; align-items: center; gap: var(--space-3); }
|
|
92
121
|
|
|
93
122
|
/* Search pill grows to fill the bar (component itself lives in inputs.css) */
|
|
94
|
-
.topbar .pill-search { height: var(--tb-control-h); box-sizing: border-box; flex: 1; max-width: 520px; min-width: 0; }
|
|
123
|
+
.topbar .pill-search { height: var(--tb-control-h, var(--control-h-md)); box-sizing: border-box; flex: 1; max-width: 520px; min-width: 0; }
|
|
124
|
+
|
|
125
|
+
/* ===== Compact / mobile =====
|
|
126
|
+
The collapse is driven by a MODIFIER, not only by the breakpoint. A media
|
|
127
|
+
query reads the viewport, so a bar rendered inside a narrow container — a doc
|
|
128
|
+
demo, a split pane, a drawer — would keep its desktop layout while visibly
|
|
129
|
+
lacking the room for it. `.is-compact` lets the consumer say "this bar is
|
|
130
|
+
narrow" directly, and the breakpoint simply opts every bar in below 720px.
|
|
95
131
|
|
|
96
|
-
|
|
132
|
+
Two collapses, both preserving the control box so no tap target shrinks:
|
|
133
|
+
1. brand off. Both marks hide: the menu button and the account already say
|
|
134
|
+
where you are, and a mark would eat room search and the CTA need.
|
|
135
|
+
2. search pill -> icon button. The input and the ⌘K hint hide and the glyph
|
|
136
|
+
centres in the same box. A bare icon would collapse to ~20px.
|
|
137
|
+
The language control and live clock hide outright: ambient status, not worth
|
|
138
|
+
a target at this width. */
|
|
139
|
+
.topbar.is-compact { gap: var(--space-2); padding: 0 var(--space-3); }
|
|
140
|
+
/* No brand at this width: the menu button and the account already say where
|
|
141
|
+
you are, and the mark would eat room the search and primary action need. */
|
|
142
|
+
.topbar.is-compact .tb-mark-full,
|
|
143
|
+
.topbar.is-compact .tb-mark-symbol { display: none; }
|
|
144
|
+
.topbar.is-compact .pill-search {
|
|
145
|
+
flex: 0 0 var(--tb-control-h);
|
|
146
|
+
width: var(--tb-control-h);
|
|
147
|
+
max-width: none;
|
|
148
|
+
padding: 0;
|
|
149
|
+
justify-content: center;
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
}
|
|
152
|
+
.topbar.is-compact .pill-search input,
|
|
153
|
+
.topbar.is-compact .pill-search .kbd { display: none; }
|
|
154
|
+
.topbar.is-compact .pill-search > i,
|
|
155
|
+
.topbar.is-compact .pill-search > svg { margin: 0; }
|
|
156
|
+
.topbar.is-compact .tb-live,
|
|
157
|
+
.topbar.is-compact .tb-lang { display: none; }
|
|
158
|
+
|
|
159
|
+
/* The breakpoint opts every bar in. Selectors mirror the block above on purpose:
|
|
160
|
+
plain CSS cannot hand a class to a media query, and a custom-property switch
|
|
161
|
+
for `display` would hide the intent. */
|
|
97
162
|
@media (max-width: 720px) {
|
|
98
163
|
.topbar { gap: var(--space-2); padding: 0 var(--space-3); }
|
|
164
|
+
.topbar .tb-mark-full,
|
|
165
|
+
.topbar .tb-mark-symbol { display: none; }
|
|
166
|
+
.topbar .pill-search {
|
|
167
|
+
flex: 0 0 var(--tb-control-h);
|
|
168
|
+
width: var(--tb-control-h);
|
|
169
|
+
max-width: none;
|
|
170
|
+
padding: 0;
|
|
171
|
+
justify-content: center;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
}
|
|
174
|
+
.topbar .pill-search input,
|
|
175
|
+
.topbar .pill-search .kbd { display: none; }
|
|
176
|
+
.topbar .pill-search > i,
|
|
177
|
+
.topbar .pill-search > svg { margin: 0; }
|
|
178
|
+
.topbar .tb-live,
|
|
179
|
+
.topbar .tb-lang { display: none; }
|
|
99
180
|
}
|
|
100
181
|
|
|
182
|
+
/* Above the breakpoint the symbol stays out of the way */
|
|
183
|
+
.topbar .tb-mark-symbol { display: none; }
|
|
184
|
+
|
|
101
185
|
/* ===== Account menu (avatar + identity → dropdown) ===== */
|
|
102
186
|
.tb-account-wrap { position: relative; margin-left: var(--space-1); }
|
|
103
187
|
.tb-account {
|
|
104
|
-
height: var(--tb-control-h); min-height: var(--tb-control-h); box-sizing: border-box;
|
|
188
|
+
height: var(--tb-control-h, var(--control-h-md)); min-height: var(--tb-control-h, var(--control-h-md)); box-sizing: border-box;
|
|
105
189
|
display: inline-flex; align-items: center; gap: var(--space-2);
|
|
106
190
|
padding: 0 var(--space-3-5) 0 var(--space-2-5);
|
|
107
191
|
border: 1px solid var(--color-border-default);
|
|
@@ -147,3 +231,79 @@
|
|
|
147
231
|
.tb-account-menu-item.is-danger { color: var(--color-feedback-danger-foreground); }
|
|
148
232
|
.tb-account-menu-item.is-danger svg, .tb-account-menu-item.is-danger i { color: var(--color-feedback-danger-foreground); }
|
|
149
233
|
.tb-account-menu-item.is-danger:hover { background: var(--color-feedback-danger-background); color: var(--color-feedback-danger-foreground); }
|
|
234
|
+
|
|
235
|
+
/* ============================================================
|
|
236
|
+
GLASS VARIANT — .topbar.is-glass
|
|
237
|
+
============================================================
|
|
238
|
+
Frosted chrome instead of a solid surface: the bar floats over content and
|
|
239
|
+
lets it show through. Consumes the semantic --glass-chrome pair, so it flips
|
|
240
|
+
to ink glass on dark with no per-theme rule here.
|
|
241
|
+
|
|
242
|
+
Two conditions it depends on, both easy to get wrong:
|
|
243
|
+
1. Something must scroll UNDER it. Over a flat canvas the blur has nothing to
|
|
244
|
+
sample and the bar just reads as a slightly muddier surface — use the solid
|
|
245
|
+
default there.
|
|
246
|
+
2. It must be sticky or fixed. A static frosted bar blurs whatever happens to
|
|
247
|
+
sit behind it in the layout, which is usually nothing.
|
|
248
|
+
|
|
249
|
+
The bottom hairline gets a touch more contrast than the solid variant: with a
|
|
250
|
+
translucent background the boundary is the only thing separating bar from
|
|
251
|
+
content. */
|
|
252
|
+
.topbar.is-glass {
|
|
253
|
+
background: var(--glass-chrome);
|
|
254
|
+
color: var(--glass-chrome-fg);
|
|
255
|
+
-webkit-backdrop-filter: var(--glass-blur);
|
|
256
|
+
backdrop-filter: var(--glass-blur);
|
|
257
|
+
border-bottom-color: var(--color-border-strong);
|
|
258
|
+
}
|
|
259
|
+
/* Sticky is what makes the effect mean anything */
|
|
260
|
+
.topbar.is-glass.is-sticky {
|
|
261
|
+
position: sticky;
|
|
262
|
+
top: 0;
|
|
263
|
+
z-index: 800;
|
|
264
|
+
}
|
|
265
|
+
/* No backdrop-filter (older Safari, Firefox with the flag off): fall back to an
|
|
266
|
+
opaque surface rather than a washed-out translucent bar with unreadable text. */
|
|
267
|
+
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
268
|
+
.topbar.is-glass { background: var(--color-background-surface); }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* ============================================================
|
|
272
|
+
SIZES — bar-level and per-control
|
|
273
|
+
============================================================
|
|
274
|
+
Everything in the bar reads --tb-control-h, so the honest lever is the BAR:
|
|
275
|
+
.is-sm / .is-lg retune the token and controls, search and account scale
|
|
276
|
+
together. Per-control .sm / .lg exist for the one-off case — a dense filter
|
|
277
|
+
pill beside standard controls — and deliberately do NOT change the bar height,
|
|
278
|
+
so a smaller control still sits centred in the same row.
|
|
279
|
+
|
|
280
|
+
Bar height tracks the control with room to breathe (control + 24px), which is
|
|
281
|
+
why it is set alongside the token rather than derived. */
|
|
282
|
+
.topbar.is-sm { height: 56px; --tb-control-h: var(--control-h-sm); }
|
|
283
|
+
.topbar.is-lg { height: 72px; --tb-control-h: var(--control-h-lg); }
|
|
284
|
+
|
|
285
|
+
.topbar.is-sm .tb-control,
|
|
286
|
+
.tb-control.sm { font-size: var(--fs-xs); padding: 0 var(--space-3); gap: var(--space-2); }
|
|
287
|
+
.topbar.is-lg .tb-control,
|
|
288
|
+
.tb-control.lg { font-size: var(--fs-md); padding: 0 var(--space-4-5); }
|
|
289
|
+
|
|
290
|
+
/* Per-control override: height only, so it centres inside the standard row */
|
|
291
|
+
.tb-control.sm { height: var(--control-h-sm); min-height: var(--control-h-sm); }
|
|
292
|
+
.tb-control.lg { height: var(--control-h-lg); min-height: var(--control-h-lg); }
|
|
293
|
+
.tb-control.sm.icon { width: var(--control-h-sm); }
|
|
294
|
+
.tb-control.lg.icon { width: var(--control-h-lg); }
|
|
295
|
+
.tb-control.sm > i, .tb-control.sm > svg,
|
|
296
|
+
.tb-control.sm .badge-anchor > i, .tb-control.sm .badge-anchor > svg { width: 12px; height: 12px; }
|
|
297
|
+
.tb-control.lg > i, .tb-control.lg > svg,
|
|
298
|
+
.tb-control.lg .badge-anchor > i, .tb-control.lg .badge-anchor > svg { width: 18px; height: 18px; }
|
|
299
|
+
|
|
300
|
+
/* Live indicator follows the same scale */
|
|
301
|
+
.tb-live.sm .lbl { font-size: var(--fs-xs); }
|
|
302
|
+
.tb-live.sm .dot { width: 5px; height: 5px; }
|
|
303
|
+
.tb-live.lg .lbl { font-size: var(--fs-md); }
|
|
304
|
+
.tb-live.lg .dot { width: 7px; height: 7px; }
|
|
305
|
+
.tb-live.lg .tb-clock { font-size: var(--fs-xs); }
|
|
306
|
+
|
|
307
|
+
/* Avatar and account keep pace with the bar */
|
|
308
|
+
.topbar.is-sm .tb-avatar { width: var(--control-h-xs); height: var(--control-h-xs); font-size: 10px; }
|
|
309
|
+
.topbar.is-lg .tb-avatar { width: var(--control-h-md); height: var(--control-h-md); font-size: var(--fs-sm); }
|
package/dist/css/tokens.css
CHANGED
|
@@ -41,11 +41,16 @@
|
|
|
41
41
|
--color-neutral-900: #0A0F1F; /* inverse surface (never pure black) */
|
|
42
42
|
--color-neutral-950: #05081A; /* deepest inverse */
|
|
43
43
|
|
|
44
|
-
/* --- Inverse "ink" tones (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
/* --- Inverse "ink" tones (dark canvas + the base the inverse layer tints) ---
|
|
45
|
+
Hue 226 held, lightness of every step held, saturation cut 56–63% → ~16%.
|
|
46
|
+
Above ~30% the eye names a background "navy" instead of reading it as a
|
|
47
|
+
near-black; at 16% the blue stops being a colour and becomes a temperature.
|
|
48
|
+
Contrast survives the change: text on canvas 17.27 → 16.5, and card-vs-canvas
|
|
49
|
+
separation improves slightly (1.10 → 1.15). */
|
|
50
|
+
--color-ink-base: #191B22; /* near-black, faintly blue */
|
|
51
|
+
--color-ink-raised: #21232D; /* card on inverse */
|
|
52
|
+
--color-ink-deep: #15171D; /* deeper than base — wells, alt rows */
|
|
53
|
+
--color-ink-hover: #282B37; /* hover state on inverse */
|
|
49
54
|
|
|
50
55
|
/* --- Brand primary · Navy (institutional — logo, headings, primary surfaces)
|
|
51
56
|
10-stop scale anchored at -500: #002142 (ATTRUS Navy from brand book).
|
|
@@ -232,7 +237,7 @@
|
|
|
232
237
|
--color-scrim: rgba(10, 15, 31, 0.32);
|
|
233
238
|
--scrim-blur: 6px;
|
|
234
239
|
--color-background-inverse-press: color-mix(in srgb, var(--color-ink-hover) 70%, var(--color-pure-white));
|
|
235
|
-
--color-background-inverse-selected: color-mix(in srgb, var(--color-brand-solid)
|
|
240
|
+
--color-background-inverse-selected: color-mix(in srgb, var(--color-brand-solid) 10%, var(--color-ink-base));
|
|
236
241
|
--color-background-inverse-brand: var(--color-brand-solid); /* CTA / hero with brand weight — follows product */
|
|
237
242
|
|
|
238
243
|
/* Foreground (text + icons) on LIGHT surfaces */
|
|
@@ -264,6 +269,10 @@
|
|
|
264
269
|
--color-border-default: color-mix(in srgb, var(--color-ink-base) 7%, transparent);
|
|
265
270
|
--color-border-strong: color-mix(in srgb, var(--color-ink-base) 12%, transparent);
|
|
266
271
|
--color-border-focus: var(--color-accent-solid); /* per-theme accent (Sky default) — follows the product's accent */
|
|
272
|
+
/* Hairline width. Every 1px rule in the system was a literal; naming it makes
|
|
273
|
+
the weight adjustable in one place — and a product theme that wants crisper
|
|
274
|
+
separation can raise it without touching 14 component files. */
|
|
275
|
+
--border-hairline: 1px; /* @kind spacing */
|
|
267
276
|
--color-border-accent: var(--color-accent-solid);
|
|
268
277
|
--color-border-success: var(--color-success-500);
|
|
269
278
|
--color-border-warning: var(--color-warning-500);
|
|
@@ -567,7 +576,18 @@
|
|
|
567
576
|
--space-7: 24px;
|
|
568
577
|
--space-8: 32px;
|
|
569
578
|
/* Indent step for a table subrow, multiplied by its depth (see table.css §12) */
|
|
570
|
-
--subrow-indent:
|
|
579
|
+
--subrow-indent: 16px; /* @kind spacing */
|
|
580
|
+
|
|
581
|
+
/* Control height scale — the box every in-line control shares.
|
|
582
|
+
Multiples of 8, so a control, a button and an input on the same row align
|
|
583
|
+
without per-instance tuning. Named to match the button sizes, because those
|
|
584
|
+
were the de-facto scale before this existed: .btn-xs/-sm/(md)/-lg.
|
|
585
|
+
Padding-based items (.sb-item, .tab) stay out of this on purpose — they are
|
|
586
|
+
list and navigation rows, not in-line controls. */
|
|
587
|
+
--control-h-xs: 24px; /* @kind spacing */
|
|
588
|
+
--control-h-sm: 32px; /* @kind spacing */
|
|
589
|
+
--control-h-md: 40px; /* @kind spacing */ /* default */
|
|
590
|
+
--control-h-lg: 48px; /* @kind spacing */
|
|
571
591
|
--space-9: 40px;
|
|
572
592
|
--space-10: 48px;
|
|
573
593
|
--space-12: 64px;
|
|
@@ -660,6 +680,13 @@
|
|
|
660
680
|
--w-regular: 400; /* @kind font */
|
|
661
681
|
--w-bold: 700; /* @kind font */
|
|
662
682
|
--w-black: 900; /* @kind font */
|
|
683
|
+
/* Mono emphasis — the weight amounts use. Kept separate from --w-bold (which
|
|
684
|
+
means Lato 700) because the two families are tuned apart: a future mono swap
|
|
685
|
+
re-tunes this one line without disturbing the sans scale.
|
|
686
|
+
Measured: 700 and 800 resolve to the SAME face here (ink 48.00 for both,
|
|
687
|
+
against 44.00 regular), so 800 bought nothing visually. 700 is the honest
|
|
688
|
+
value — it is what the browser draws and what the import requests. */
|
|
689
|
+
--w-mono-emphasis: 700; /* @kind font */
|
|
663
690
|
/* RETIRED — Lato has no Medium, SemiBold or ExtraBold. Requesting them from
|
|
664
691
|
Google Fonts returns nothing and the browser rounds silently (500→400,
|
|
665
692
|
600→700, 800→900), so the scale advertised 7 steps while rendering 4.
|
|
@@ -728,9 +755,9 @@
|
|
|
728
755
|
--color-brand-solid-active: var(--color-brand-primary-500); /* settles to navy */
|
|
729
756
|
|
|
730
757
|
/* Inverse layer — stays distinct: a brand-tinted dark, lifted off the ambient ink */
|
|
731
|
-
--color-background-inverse: color-mix(in srgb, var(--color-brand-solid)
|
|
732
|
-
--color-background-inverse-surface: color-mix(in srgb, var(--color-brand-solid)
|
|
733
|
-
--color-background-inverse-subtle: color-mix(in srgb, var(--color-brand-solid)
|
|
758
|
+
--color-background-inverse: color-mix(in srgb, var(--color-brand-solid) 11%, var(--color-ink-base));
|
|
759
|
+
--color-background-inverse-surface: color-mix(in srgb, var(--color-brand-solid) 14%, var(--color-ink-raised));
|
|
760
|
+
--color-background-inverse-subtle: color-mix(in srgb, var(--color-brand-solid) 8%, var(--color-ink-deep));
|
|
734
761
|
|
|
735
762
|
/* Foreground — light on dark */
|
|
736
763
|
--color-foreground-default: var(--color-neutral-50);
|
|
@@ -828,6 +855,19 @@ h6, .h6 { font-weight: var(--w-regular); font-size: var(--fs-lg); line-height:
|
|
|
828
855
|
font-size-adjust: var(--font-mono-adjust); font-size: .92em; }
|
|
829
856
|
|
|
830
857
|
/* Numbers that must line up in tables */
|
|
858
|
+
/* Visually hidden but announced — for text that a screen reader needs and the
|
|
859
|
+
eye does not. The clip-rect technique, not display:none (which removes it from
|
|
860
|
+
the accessibility tree) and not width:0 (which some engines skip). */
|
|
861
|
+
.sr-only {
|
|
862
|
+
position: absolute;
|
|
863
|
+
width: 1px; height: 1px;
|
|
864
|
+
padding: 0; margin: -1px;
|
|
865
|
+
overflow: hidden;
|
|
866
|
+
clip: rect(0, 0, 0, 0);
|
|
867
|
+
white-space: nowrap;
|
|
868
|
+
border: 0;
|
|
869
|
+
}
|
|
870
|
+
|
|
831
871
|
.tabular { font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' on, 'lnum' on; }
|
|
832
872
|
|
|
833
873
|
/* ============================================================
|
|
@@ -8,10 +8,10 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
|
|
|
8
8
|
variant?: ButtonVariant;
|
|
9
9
|
/** Control height: xs=24, sm=32, md=40 (default), lg=48. */
|
|
10
10
|
size?: ButtonSize;
|
|
11
|
+
/** Square, label-less button. `aria-label` is required. */
|
|
12
|
+
iconOnly?: boolean;
|
|
11
13
|
/** Shows the canonical spinner and disables interaction. */
|
|
12
14
|
loading?: boolean;
|
|
13
|
-
/** Square icon-only button (.btn-icon) — width locks to the height. */
|
|
14
|
-
iconOnly?: boolean;
|
|
15
15
|
/** Leading slot — typically an <Icon/>. */
|
|
16
16
|
icon?: React.ReactNode;
|
|
17
17
|
/** Trailing slot — e.g. a chevron. */
|
|
@@ -23,3 +23,11 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
|
|
|
23
23
|
export declare const Button: React.FC<ButtonProps>;
|
|
24
24
|
export declare const ButtonGroup: React.FC<React.HTMLAttributes<HTMLDivElement>>;
|
|
25
25
|
export default Button;
|
|
26
|
+
|
|
27
|
+
export interface ButtonGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
28
|
+
children?: React.ReactNode;
|
|
29
|
+
/** Names the cluster for assistive tech. */
|
|
30
|
+
'aria-label': string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export declare const ButtonGroup: React.FC<ButtonGroupProps>;
|
|
@@ -24,10 +24,12 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
|
|
|
24
24
|
variant?: ButtonVariant;
|
|
25
25
|
/** Control height: xs=24, sm=32, md=40 (default), lg=48. */
|
|
26
26
|
size?: ButtonSize;
|
|
27
|
+
/** Square, label-less button. `aria-label` is REQUIRED — an icon-only
|
|
28
|
+
control has no text node, so without it the button is unnamed to a screen
|
|
29
|
+
reader. That requirement is why this is a prop and not a bare class. */
|
|
30
|
+
iconOnly?: boolean;
|
|
27
31
|
/** Shows the canonical spinner and disables interaction. */
|
|
28
32
|
loading?: boolean;
|
|
29
|
-
/** Square icon-only button (.btn-icon) — width locks to the height. */
|
|
30
|
-
iconOnly?: boolean;
|
|
31
33
|
/** Leading slot — typically an <Icon/>. */
|
|
32
34
|
icon?: React.ReactNode;
|
|
33
35
|
/** Trailing slot — e.g. a chevron. */
|
|
@@ -40,8 +42,8 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
|
|
|
40
42
|
export const Button: React.FC<ButtonProps> = ({
|
|
41
43
|
variant = 'primary',
|
|
42
44
|
size = 'md',
|
|
43
|
-
loading = false,
|
|
44
45
|
iconOnly = false,
|
|
46
|
+
loading = false,
|
|
45
47
|
icon,
|
|
46
48
|
iconRight,
|
|
47
49
|
onInverse = false,
|
|
@@ -76,7 +78,13 @@ export default Button;
|
|
|
76
78
|
|
|
77
79
|
/** Segmented / toolbar cluster — children must be <Button>s (`.btn-group`):
|
|
78
80
|
* outer corners pill-rounded, inner seams collapsed. */
|
|
79
|
-
export
|
|
81
|
+
export interface ButtonGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
82
|
+
children?: React.ReactNode;
|
|
83
|
+
/** Names the cluster for assistive tech — "View mode", "Alignment". */
|
|
84
|
+
'aria-label': string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const ButtonGroup: React.FC<ButtonGroupProps> = ({ className, children, ...rest }) => (
|
|
80
88
|
<div role="group" className={['btn-group', className || ''].filter(Boolean).join(' ')} {...rest}>
|
|
81
89
|
{children}
|
|
82
90
|
</div>
|
|
@@ -45,6 +45,11 @@ export interface ComboboxProps {
|
|
|
45
45
|
disabled?: boolean;
|
|
46
46
|
/** Gallery/doc rendering: popover stays inline (not portaled). */
|
|
47
47
|
static?: boolean;
|
|
48
|
+
/** Which trigger edge the portaled popover is pinned to. The popover treats
|
|
49
|
+
the trigger's width as a MINIMUM and grows with its content, so the growth
|
|
50
|
+
needs a direction: 'start' grows right (default), 'end' grows left. Use
|
|
51
|
+
'end' for a compact control near the right edge. */
|
|
52
|
+
align?: 'start' | 'end';
|
|
48
53
|
className?: string;
|
|
49
54
|
style?: React.CSSProperties;
|
|
50
55
|
'aria-label'?: string;
|
|
@@ -53,6 +53,12 @@ export interface ComboboxProps {
|
|
|
53
53
|
/** Gallery/doc rendering: popover stays inline (absolute under the trigger),
|
|
54
54
|
not portaled. Use inside relative frames for side-by-side examples. */
|
|
55
55
|
static?: boolean;
|
|
56
|
+
/** Which trigger edge the portaled popover is pinned to. It matches the
|
|
57
|
+
trigger's width as a MINIMUM and grows with its content, so the growth has
|
|
58
|
+
to have a direction: 'start' grows right (default), 'end' grows left. Use
|
|
59
|
+
'end' for a compact control near the right edge, where growing right would
|
|
60
|
+
run off the viewport. */
|
|
61
|
+
align?: 'start' | 'end';
|
|
56
62
|
className?: string;
|
|
57
63
|
style?: React.CSSProperties;
|
|
58
64
|
'aria-label'?: string;
|
|
@@ -93,6 +99,7 @@ export const Combobox: React.FC<ComboboxProps> = ({
|
|
|
93
99
|
emptySub,
|
|
94
100
|
disabled = false,
|
|
95
101
|
static: isStatic = false,
|
|
102
|
+
align = 'start',
|
|
96
103
|
className,
|
|
97
104
|
style,
|
|
98
105
|
...rest
|
|
@@ -102,14 +109,15 @@ export const Combobox: React.FC<ComboboxProps> = ({
|
|
|
102
109
|
const [active, setActive] = React.useState<string | null>(null);
|
|
103
110
|
const wrapRef = React.useRef<HTMLDivElement>(null);
|
|
104
111
|
const popRef = React.useRef<HTMLDivElement>(null);
|
|
105
|
-
const [pos, setPos] = React.useState<{ left: number; top: number; width: number } | null>(null);
|
|
112
|
+
const [pos, setPos] = React.useState<{ left: number; right: number; top: number; width: number } | null>(null);
|
|
106
113
|
|
|
107
114
|
// Position the portaled popover under the trigger (fixed coords from rect).
|
|
115
|
+
// Both edges are captured because `align` decides which one anchors.
|
|
108
116
|
React.useLayoutEffect(() => {
|
|
109
117
|
if (!open || isStatic) return undefined;
|
|
110
118
|
const place = () => {
|
|
111
119
|
const r = wrapRef.current?.getBoundingClientRect();
|
|
112
|
-
if (r) setPos({ left: r.left, top: r.bottom + 8, width: r.width });
|
|
120
|
+
if (r) setPos({ left: r.left, right: window.innerWidth - r.right, top: r.bottom + 8, width: r.width });
|
|
113
121
|
};
|
|
114
122
|
place();
|
|
115
123
|
window.addEventListener('scroll', place, true);
|
|
@@ -284,8 +292,22 @@ export const Combobox: React.FC<ComboboxProps> = ({
|
|
|
284
292
|
{open && pos
|
|
285
293
|
? createPortal(
|
|
286
294
|
<div
|
|
287
|
-
className={['combobox', 'is-open', status === 'error' ? 'is-invalid' : ''].filter(Boolean).join(' ')}
|
|
288
|
-
style={{
|
|
295
|
+
className={['combobox', 'is-open', status === 'error' ? 'is-invalid' : '', className || ''].filter(Boolean).join(' ')}
|
|
296
|
+
style={{
|
|
297
|
+
/* MIN-width, not width: matching the trigger is right for a form
|
|
298
|
+
field, but as a fixed width it traps the list — a 50px chip
|
|
299
|
+
crushes its own options with no floor to rescue them. The
|
|
300
|
+
trigger sets the floor; content sets the rest. */
|
|
301
|
+
position: 'fixed',
|
|
302
|
+
top: pos.top,
|
|
303
|
+
...(align === 'end' ? { right: pos.right } : { left: pos.left }),
|
|
304
|
+
minWidth: pos.width,
|
|
305
|
+
margin: 0,
|
|
306
|
+
zIndex: 7000,
|
|
307
|
+
/* The consumer's own style comes last so it can override the
|
|
308
|
+
floor when the content still needs more room. */
|
|
309
|
+
...style,
|
|
310
|
+
}}
|
|
289
311
|
onKeyDown={onWrapKeyDown}
|
|
290
312
|
>
|
|
291
313
|
<div className="combobox-popover" ref={popRef} style={{ position: 'static' }}>{popoverInner}</div>
|