@apliteni/apliteni-ui 0.9.0 → 0.11.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.
- package/package.json +1 -1
- package/src/components/account-nav.js +47 -0
- package/src/components/confirm.js +131 -0
- package/src/components/drawer.js +36 -98
- package/src/components/nav.js +25 -6
- package/src/components/overlay.js +231 -0
- package/src/components/shell.js +235 -32
- package/src/components/success.js +2 -2
- package/src/components/topbar.js +30 -4
- package/src/index.css +1 -0
- package/src/index.js +1 -0
- package/src/inline.js +2 -0
- package/src/styles/base.css +11 -3
- package/src/styles/confirm.css +130 -0
- package/src/styles/drawer.css +30 -15
- package/src/styles/layout.css +197 -42
- package/src/styles/nav.css +33 -6
- package/src/tokens/accents.css +27 -4
- package/src/tokens/tokens.css +35 -8
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Confirm — the kit's confirmation dialog: a centred panel over a scrim, for
|
|
3
|
+
* the question a destructive action has to ask. Fully token-driven so it
|
|
4
|
+
* re-themes across accents + light/dark. Shares its scrim + focus-trap
|
|
5
|
+
* behaviour with the drawer (see overlay.js).
|
|
6
|
+
*
|
|
7
|
+
* Motion is the drawer's, deliberately: --dur-med/--ease, and `visibility`
|
|
8
|
+
* switched rather than transitioned. A transitioned visibility is still
|
|
9
|
+
* `hidden` in the frame the class lands, so openConfirm()'s focus() would have
|
|
10
|
+
* nothing to focus and the reader would be left on <body>. The root owns the
|
|
11
|
+
* switch; the panel and scrim inherit it.
|
|
12
|
+
*
|
|
13
|
+
* Local tokens (surface/shadow/scrim) so the panel + scrim re-theme cleanly:
|
|
14
|
+
* --confirm-surface panel background --confirm-shadow panel elevation
|
|
15
|
+
* --confirm-scrim backdrop colour (--scrim already re-themes per theme)
|
|
16
|
+
* ========================================================================== */
|
|
17
|
+
|
|
18
|
+
.ui-confirm {
|
|
19
|
+
--confirm-surface: var(--surface-2);
|
|
20
|
+
--confirm-shadow: var(--shadow-lg);
|
|
21
|
+
--confirm-scrim: var(--scrim);
|
|
22
|
+
--confirm-w: 420px;
|
|
23
|
+
|
|
24
|
+
position: fixed;
|
|
25
|
+
inset: 0;
|
|
26
|
+
/* One above the drawer's own overlay level: a confirm asks a question about the
|
|
27
|
+
drawer it was opened over, so it has to be readable above it whatever order
|
|
28
|
+
the two are mounted in. */
|
|
29
|
+
z-index: calc(var(--z-overlay) + 1);
|
|
30
|
+
pointer-events: none; /* container never blocks; scrim/panel opt back in */
|
|
31
|
+
|
|
32
|
+
/* Only the hide is transitioned, so the panel is still on screen while it
|
|
33
|
+
fades out; the show is instant, because focus moves in the same frame.
|
|
34
|
+
`linear` and not var(--ease): visibility is discrete, easing a step buys
|
|
35
|
+
nothing, and a curve that leaves [0, 1] would flip it mid-fade. */
|
|
36
|
+
visibility: hidden;
|
|
37
|
+
transition: visibility var(--dur-med) linear;
|
|
38
|
+
}
|
|
39
|
+
.ui-confirm.is-open { visibility: visible; transition: none; }
|
|
40
|
+
|
|
41
|
+
/* Hit-testable only while open. The root stays visible for --dur-med after
|
|
42
|
+
closeConfirm() has stopped trapping, and an accept button that is still
|
|
43
|
+
clickable in that window runs the caller's destructive work a second time. */
|
|
44
|
+
.ui-confirm.is-open .ui-confirm__scrim,
|
|
45
|
+
.ui-confirm.is-open .ui-confirm__panel { pointer-events: auto; }
|
|
46
|
+
|
|
47
|
+
.ui-confirm__scrim {
|
|
48
|
+
position: absolute;
|
|
49
|
+
inset: 0;
|
|
50
|
+
background: var(--confirm-scrim);
|
|
51
|
+
opacity: 0;
|
|
52
|
+
transition: opacity var(--dur-med) var(--ease);
|
|
53
|
+
}
|
|
54
|
+
.ui-confirm.is-open .ui-confirm__scrim { opacity: 1; }
|
|
55
|
+
|
|
56
|
+
/* Panel — centred with `translate` so the entrance can own `transform` without
|
|
57
|
+
dropping the panel into the corner when it resets to none. */
|
|
58
|
+
.ui-confirm__panel {
|
|
59
|
+
position: absolute;
|
|
60
|
+
top: 50%;
|
|
61
|
+
left: 50%;
|
|
62
|
+
translate: -50% -50%;
|
|
63
|
+
width: min(var(--confirm-w), calc(100vw - var(--space-8)));
|
|
64
|
+
/* A fixed, centred panel cannot be scrolled into view — scrollIntoView does
|
|
65
|
+
nothing to it — so a panel taller than the viewport puts its answers below
|
|
66
|
+
the fold for good. Bound it, and let the consequence scroll instead. The
|
|
67
|
+
dvh line wins where it is understood; the vh line is what holds it up
|
|
68
|
+
everywhere else. */
|
|
69
|
+
max-height: calc(100vh - var(--space-8));
|
|
70
|
+
max-height: calc(100dvh - var(--space-8));
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
gap: var(--space-2);
|
|
74
|
+
padding: var(--space-5);
|
|
75
|
+
background: var(--confirm-surface);
|
|
76
|
+
border: 1px solid var(--border);
|
|
77
|
+
border-radius: var(--radius-lg);
|
|
78
|
+
box-shadow: var(--confirm-shadow);
|
|
79
|
+
opacity: 0;
|
|
80
|
+
transform: translateY(8px);
|
|
81
|
+
transition:
|
|
82
|
+
opacity var(--dur-med) var(--ease),
|
|
83
|
+
transform var(--dur-med) var(--ease);
|
|
84
|
+
}
|
|
85
|
+
.ui-confirm.is-open .ui-confirm__panel { opacity: 1; transform: none; }
|
|
86
|
+
.ui-confirm__panel:focus-visible { outline: none; box-shadow: var(--confirm-shadow), var(--ring); }
|
|
87
|
+
|
|
88
|
+
/* -- Slots ----------------------------------------------------------------- */
|
|
89
|
+
/* The question and the answers keep their size; the consequence between them is
|
|
90
|
+
the one that gives, and scrolls — the same split the drawer's header / body /
|
|
91
|
+
footer makes. */
|
|
92
|
+
.ui-confirm__title {
|
|
93
|
+
margin: 0;
|
|
94
|
+
flex: none;
|
|
95
|
+
font-family: var(--font-sans);
|
|
96
|
+
font-size: var(--text-md);
|
|
97
|
+
font-weight: var(--weight-semibold);
|
|
98
|
+
line-height: 1.35;
|
|
99
|
+
color: var(--strong);
|
|
100
|
+
}
|
|
101
|
+
.ui-confirm__body {
|
|
102
|
+
margin: 0;
|
|
103
|
+
flex: 1 1 auto;
|
|
104
|
+
min-height: 0;
|
|
105
|
+
overflow-y: auto;
|
|
106
|
+
font-family: var(--font-sans);
|
|
107
|
+
font-size: var(--text-sm);
|
|
108
|
+
font-weight: var(--weight-normal);
|
|
109
|
+
line-height: 1.55;
|
|
110
|
+
color: var(--muted);
|
|
111
|
+
}
|
|
112
|
+
.ui-confirm__acts {
|
|
113
|
+
display: flex;
|
|
114
|
+
flex: none;
|
|
115
|
+
flex-wrap: wrap;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: flex-end;
|
|
118
|
+
gap: var(--space-2);
|
|
119
|
+
margin-top: var(--space-2);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* -- Reduced motion: no rise, just a fade --------------------------------- */
|
|
123
|
+
@media (prefers-reduced-motion: reduce) {
|
|
124
|
+
.ui-confirm { transition: visibility var(--dur-fast) linear; }
|
|
125
|
+
.ui-confirm__panel {
|
|
126
|
+
transform: none;
|
|
127
|
+
transition: opacity var(--dur-fast) linear;
|
|
128
|
+
}
|
|
129
|
+
.ui-confirm__scrim { transition: opacity var(--dur-fast) linear; }
|
|
130
|
+
}
|
package/src/styles/drawer.css
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/* ============================================================================
|
|
2
2
|
* Drawer — edge-anchored overlay panel (Sheet / Slide-over): a panel that
|
|
3
3
|
* slides in from any screen edge over a scrim. Fully token-driven so it
|
|
4
|
-
* re-themes across accents + light/dark. Shares its scrim + focus
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* re-themes across accents + light/dark. Shares its scrim + focus trap with
|
|
5
|
+
* confirm() (see overlay.js). `prefers-reduced-motion` drops the slide for a
|
|
6
|
+
* plain fade.
|
|
7
7
|
*
|
|
8
8
|
* Local tokens (surface/shadow/scrim) so the panel + scrim re-theme cleanly:
|
|
9
9
|
* --drawer-surface panel background --drawer-shadow panel elevation
|
|
@@ -21,18 +21,35 @@
|
|
|
21
21
|
inset: 0;
|
|
22
22
|
z-index: var(--z-overlay);
|
|
23
23
|
pointer-events: none; /* container never blocks; scrim/panel opt back in */
|
|
24
|
+
|
|
25
|
+
/* One place owns "is this drawer on screen at all", and the scrim and panel
|
|
26
|
+
inherit it. Only the HIDE is transitioned, so the panel is still on screen
|
|
27
|
+
while it slides out; the show is instant (`.is-open` cancels the
|
|
28
|
+
transition) because openDrawer() focuses a control in the same frame it
|
|
29
|
+
opens, and focus() on a hidden element does nothing. `linear` and not
|
|
30
|
+
var(--ease): visibility is discrete, easing a step buys nothing, and a curve
|
|
31
|
+
that leaves [0, 1] would flip it in the middle of the fade. */
|
|
32
|
+
visibility: hidden;
|
|
33
|
+
transition: visibility var(--dur-med) linear;
|
|
24
34
|
}
|
|
25
|
-
|
|
35
|
+
.ui-drawer.is-open { visibility: visible; transition: none; }
|
|
36
|
+
|
|
37
|
+
/* Hit-testable only while open. The root stays visible for --dur-med after
|
|
38
|
+
closeDrawer() has stopped trapping, and any control still clickable in that
|
|
39
|
+
window — the footer's actions, the close button — runs the caller's handler
|
|
40
|
+
again on the way out. */
|
|
41
|
+
.ui-drawer.is-open .ui-drawer__scrim,
|
|
42
|
+
.ui-drawer.is-open .ui-drawer__panel { pointer-events: auto; }
|
|
43
|
+
|
|
44
|
+
/* Scrim — fades in and out; the drawer root decides when it stops existing. */
|
|
26
45
|
.ui-drawer__scrim {
|
|
27
46
|
position: absolute;
|
|
28
47
|
inset: 0;
|
|
29
48
|
background: var(--drawer-scrim);
|
|
30
|
-
pointer-events: auto;
|
|
31
49
|
opacity: 0;
|
|
32
|
-
|
|
33
|
-
transition: opacity var(--dur-med) var(--ease), visibility var(--dur-med) var(--ease);
|
|
50
|
+
transition: opacity var(--dur-med) var(--ease);
|
|
34
51
|
}
|
|
35
|
-
.ui-drawer.is-open .ui-drawer__scrim { opacity: 1;
|
|
52
|
+
.ui-drawer.is-open .ui-drawer__scrim { opacity: 1; }
|
|
36
53
|
|
|
37
54
|
/* Panel — the sliding surface. Flex column: header / scrollable body / footer. */
|
|
38
55
|
.ui-drawer__panel {
|
|
@@ -41,16 +58,13 @@
|
|
|
41
58
|
flex-direction: column;
|
|
42
59
|
background: var(--drawer-surface);
|
|
43
60
|
box-shadow: var(--drawer-shadow);
|
|
44
|
-
pointer-events: auto;
|
|
45
61
|
opacity: 1;
|
|
46
|
-
visibility: hidden;
|
|
47
62
|
transition:
|
|
48
63
|
transform var(--dur-med) var(--ease),
|
|
49
|
-
opacity var(--dur-med) var(--ease)
|
|
50
|
-
visibility var(--dur-med) var(--ease);
|
|
64
|
+
opacity var(--dur-med) var(--ease);
|
|
51
65
|
}
|
|
52
66
|
.ui-drawer__panel:focus-visible { outline: none; box-shadow: var(--drawer-shadow), var(--ring); }
|
|
53
|
-
.ui-drawer.is-open .ui-drawer__panel {
|
|
67
|
+
.ui-drawer.is-open .ui-drawer__panel { transform: none; }
|
|
54
68
|
|
|
55
69
|
/* -- Edge anchoring + slide-in direction ---------------------------------- */
|
|
56
70
|
.ui-drawer--right .ui-drawer__panel,
|
|
@@ -154,11 +168,12 @@
|
|
|
154
168
|
|
|
155
169
|
/* -- Reduced motion: no slide, just a fade -------------------------------- */
|
|
156
170
|
@media (prefers-reduced-motion: reduce) {
|
|
171
|
+
.ui-drawer { transition: visibility var(--dur-fast) linear; }
|
|
157
172
|
.ui-drawer__panel {
|
|
158
173
|
transform: none !important;
|
|
159
174
|
opacity: 0;
|
|
160
|
-
transition: opacity var(--dur-fast) linear
|
|
175
|
+
transition: opacity var(--dur-fast) linear;
|
|
161
176
|
}
|
|
162
177
|
.ui-drawer.is-open .ui-drawer__panel { opacity: 1; }
|
|
163
|
-
.ui-drawer__scrim { transition: opacity var(--dur-fast) linear
|
|
178
|
+
.ui-drawer__scrim { transition: opacity var(--dur-fast) linear; }
|
|
164
179
|
}
|
package/src/styles/layout.css
CHANGED
|
@@ -1,54 +1,209 @@
|
|
|
1
1
|
/* ============================================================================
|
|
2
|
-
* Layout primitives —
|
|
2
|
+
* Layout primitives — the page shell, the auth card, the landing hero.
|
|
3
3
|
* These compose the example apps; each stays token-driven and themeable.
|
|
4
|
+
* The hand-written .ui-side rail and .ui-shell grid are gone: appShell() draws
|
|
5
|
+
* the rail with sidebarNav(), so nothing emitted that markup any more.
|
|
4
6
|
* ========================================================================== */
|
|
5
7
|
|
|
6
|
-
/* --
|
|
7
|
-
|
|
8
|
-
.ui-side
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
/* -- The one page shell (appShell) --------------------------------------- */
|
|
9
|
+
/* A full-height rail beside one <main>. The rail's own rows are sidebarNav()'s
|
|
10
|
+
(.ui-nav--side), so nothing here restyles a nav item — these rules place the
|
|
11
|
+
rail, its brand head and its signed-in footer, and set the reading column. */
|
|
12
|
+
.ui-app {
|
|
13
|
+
display: grid;
|
|
14
|
+
grid-template-columns: max-content 1fr;
|
|
15
|
+
min-height: calc(100vh - var(--ui-app-top, 0px));
|
|
16
|
+
background: var(--bg);
|
|
17
|
+
color: var(--text);
|
|
18
|
+
font-family: var(--font-sans);
|
|
19
|
+
}
|
|
20
|
+
/* With a topbar above it, the shell starts below one. 52px is .topbar's height
|
|
21
|
+
in topbar.css; the topbar sticks so the rail below it can too. */
|
|
22
|
+
.ui-app-page { --ui-app-top: 52px; }
|
|
23
|
+
.ui-app-page .topbar { position: sticky; top: 0; z-index: var(--z-sticky); }
|
|
24
|
+
|
|
25
|
+
/* --surface-2, not --bg-elevated: elevated resolves to --bg itself in the light
|
|
26
|
+
theme, an exact no-op that left the border as the whole of the rail.
|
|
27
|
+
overflow-y, because the box is pinned at the viewport's height for the whole
|
|
28
|
+
scroll of .ui-app — without it a row past that height paints below the fold
|
|
29
|
+
and no amount of document scrolling brings it back. The rail's own padding is
|
|
30
|
+
what keeps a focus ring out of the clip, so it stays above --ring's spread. */
|
|
31
|
+
.ui-app__rail {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: var(--space-5);
|
|
35
|
+
padding: var(--space-5) var(--space-4);
|
|
36
|
+
border-right: 1px solid var(--border);
|
|
37
|
+
background: var(--surface-2);
|
|
38
|
+
position: sticky;
|
|
39
|
+
top: var(--ui-app-top, 0px);
|
|
40
|
+
height: calc(100vh - var(--ui-app-top, 0px));
|
|
41
|
+
overflow-y: auto;
|
|
42
|
+
}
|
|
43
|
+
/* The rail's hover WAS --surface-2, so moving the rail onto it would have made
|
|
44
|
+
hovering a row do nothing. Scoped, so a sidebarNav() anywhere else is
|
|
45
|
+
untouched — and the destructive row is left out, because this rule ties with
|
|
46
|
+
nav.css's danger wash on specificity and wins on source order alone. Sign out
|
|
47
|
+
was quietly losing the --pink wash the guideline page cites nav.css for. The
|
|
48
|
+
current row is left out for the same reason: this rule outranks nav.css's
|
|
49
|
+
active hover, and painting --surface-3 over a row already resting on it is
|
|
50
|
+
what made the row you are standing on the one row that ignores the pointer. */
|
|
51
|
+
.ui-app__rail .ui-nav__item:not(.is-danger):not(.is-active):hover { background: var(--surface-3); }
|
|
52
|
+
.ui-app__brand {
|
|
16
53
|
display: flex;
|
|
17
54
|
align-items: center;
|
|
18
|
-
gap:
|
|
19
|
-
padding:
|
|
20
|
-
border-radius: 10px;
|
|
21
|
-
color: var(--text);
|
|
55
|
+
gap: var(--space-2);
|
|
56
|
+
padding: 2px 6px;
|
|
22
57
|
text-decoration: none;
|
|
23
|
-
font-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.ui-
|
|
29
|
-
.ui-
|
|
30
|
-
.ui-side a.out { color: var(--muted); }
|
|
31
|
-
.ui-side a.out:hover { color: var(--pink); }
|
|
58
|
+
font-weight: var(--weight-semibold);
|
|
59
|
+
letter-spacing: var(--tracking-tight);
|
|
60
|
+
}
|
|
61
|
+
/* Carries the shell's class as well as its own, for the same reason the rail
|
|
62
|
+
rows do (see nav.css): a host stylesheet's `a:link` is (0,1,1). */
|
|
63
|
+
.ui-app .ui-app__brand { color: var(--strong); }
|
|
64
|
+
.ui-app__brand svg { display: block; flex: none; }
|
|
32
65
|
|
|
33
|
-
/*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
.ui-
|
|
45
|
-
.ui-
|
|
46
|
-
.ui-
|
|
47
|
-
|
|
48
|
-
|
|
66
|
+
/* The nav grows, so the reader block below it pins to the bottom of a full-height
|
|
67
|
+
rail instead of hugging the last nav row, and the nav's own footer — sign out —
|
|
68
|
+
sits directly above it. */
|
|
69
|
+
.ui-app__rail .ui-nav--side { flex: 1 1 auto; }
|
|
70
|
+
.ui-app__rail .ui-nav__foot { margin-top: auto; }
|
|
71
|
+
|
|
72
|
+
/* Which row is current is structural here, not chromatic: the resting glyphs
|
|
73
|
+
step down so the current one is the brightest whatever --accent is doing, and
|
|
74
|
+
the bar beside it carries the hue. A 3x17px bar on a 45x44 tile was the
|
|
75
|
+
smallest of three weak signals doing the most work, so it grows with the row. */
|
|
76
|
+
.ui-app__rail .ui-nav__ic svg { opacity: 0.62; }
|
|
77
|
+
.ui-app__rail .ui-nav__item.is-active .ui-nav__ic svg,
|
|
78
|
+
.ui-app__rail .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: currentColor; }
|
|
79
|
+
.ui-app__rail .ui-nav__item.is-active::before { width: 3px; height: 62%; }
|
|
80
|
+
|
|
81
|
+
/* Sign out is a navigation row and rests like one. The danger intent is the
|
|
82
|
+
step nav.css writes for it, which a resting --muted was pre-empting — it made
|
|
83
|
+
the last row of the rail the quietest thing in it. Both of nav.css's danger
|
|
84
|
+
states are named here, not hover alone: this rule outranks them, so a state
|
|
85
|
+
it forgets is a state it silently cancels. */
|
|
86
|
+
.ui-app__rail .ui-nav__item.is-danger:not(:hover):not(:focus-visible) { color: var(--text); }
|
|
87
|
+
|
|
88
|
+
/* One rule closes the rail, not two twenty pixels apart: the nav's own footer
|
|
89
|
+
keeps its hairline and the reader block is separated by space alone — it has
|
|
90
|
+
an avatar and a weight change already. */
|
|
91
|
+
.ui-app__user {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: var(--space-2);
|
|
95
|
+
padding: var(--space-5) var(--space-1) 0;
|
|
96
|
+
}
|
|
97
|
+
.ui-app__av {
|
|
98
|
+
width: 30px; height: 30px; flex: none;
|
|
99
|
+
border-radius: var(--radius-pill);
|
|
100
|
+
background: var(--surface-3);
|
|
101
|
+
color: var(--strong);
|
|
102
|
+
display: grid; place-items: center;
|
|
103
|
+
font-size: 12px;
|
|
104
|
+
font-weight: var(--weight-semibold);
|
|
105
|
+
}
|
|
106
|
+
.ui-app__who { font-size: 12.5px; line-height: 1.3; min-width: 0; }
|
|
107
|
+
.ui-app__who b { display: block; color: var(--strong); font-weight: var(--weight-semibold); }
|
|
108
|
+
/* nowrap is what makes the ellipsis fire — without it the address wraps, and
|
|
109
|
+
the rail column is max-content, so a long one widens the rail instead. */
|
|
110
|
+
.ui-app__who span { display: block; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
111
|
+
|
|
112
|
+
/* The reading column centres in the track it is given — without width: 100% it
|
|
113
|
+
was hugging the rail, 519px of dead background on the right at 1728px wide.
|
|
114
|
+
The width is what keeps the auto margins honest: an auto inline margin takes
|
|
115
|
+
a grid item out of stretch, and Chrome then lays it out at its max-content
|
|
116
|
+
width, which is how the payout ledger pushed the page 529px sideways at 375.
|
|
117
|
+
The 860px is the floor for hand-written markup — shell.js validates what it
|
|
118
|
+
writes into --ui-app-main and falls back to this same number. */
|
|
119
|
+
.ui-app__main {
|
|
120
|
+
min-width: 0;
|
|
121
|
+
width: 100%;
|
|
122
|
+
max-width: var(--ui-app-main, 860px);
|
|
123
|
+
margin-inline: auto;
|
|
124
|
+
padding: 40px clamp(24px, 4vw, 56px) 72px;
|
|
125
|
+
}
|
|
126
|
+
/* Crumb, then the title block. At --space-2 the crumb sat 8px off the h1 and
|
|
127
|
+
the h1 8px off its subtitle, so all three ranks read as one. */
|
|
128
|
+
.ui-app__main .ui-nav--crumbs { margin-bottom: var(--space-5); }
|
|
129
|
+
.ui-app__main h1 {
|
|
130
|
+
color: var(--strong);
|
|
131
|
+
font-size: 30px;
|
|
132
|
+
font-weight: var(--weight-bold);
|
|
133
|
+
line-height: 1.1;
|
|
134
|
+
letter-spacing: var(--tracking-tight);
|
|
135
|
+
margin-bottom: 8px;
|
|
136
|
+
}
|
|
137
|
+
.ui-app__sub { color: var(--dim); font-size: 15px; line-height: 1.6; max-width: 60ch; margin-bottom: 32px; }
|
|
138
|
+
.ui-app__body { display: flex; flex-direction: column; gap: var(--space-5); }
|
|
139
|
+
/* A flex child's min-width is its content by default, so one wide block — a
|
|
140
|
+
ledger, a code sample — pushes the whole page sideways instead of scrolling
|
|
141
|
+
inside itself. This is what keeps the shell narrower than the window. */
|
|
142
|
+
.ui-app__body > * { min-width: 0; }
|
|
143
|
+
|
|
144
|
+
/* -- A row of controls above a list --------------------------------------- */
|
|
145
|
+
/* Filter fields, a segmented switch, the button that applies them. The row is
|
|
146
|
+
one line wherever the line holds and breaks where it does not, and the text
|
|
147
|
+
field is what makes that one decision instead of two: an .ui-input is
|
|
148
|
+
width:100%, so as a flex item its hypothetical size is the whole container
|
|
149
|
+
and wrapping alone put every control beside it on a second line at 1280 as
|
|
150
|
+
readily as at 375. Given a basis of its own the field shares the line, takes
|
|
151
|
+
the slack when there is any, and the line breaks only once the field would be
|
|
152
|
+
squeezed under that basis — 6rem, about as narrow as a placeholder plus a word
|
|
153
|
+
of what you typed. min-width:0 keeps an input's automatic minimum (its `size`
|
|
154
|
+
attribute, ~20 characters) from setting a floor well above the basis. */
|
|
155
|
+
.ui-toolbar { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
156
|
+
.ui-toolbar > .ui-input,
|
|
157
|
+
.ui-toolbar > .ui-input-group { flex: 1 1 6rem; min-width: 0; }
|
|
158
|
+
|
|
159
|
+
/* -- The narrow rail ------------------------------------------------------ */
|
|
160
|
+
/* Below this width the rail folds to an icon strip instead of disappearing.
|
|
161
|
+
sidebarNav() names every row at every width, so folding the label out of view
|
|
162
|
+
costs a screen reader nothing. Scoped to the shell's own rail — a sidebarNav()
|
|
163
|
+
somewhere else keeps its labels. The 44px floor is the touch target, and the
|
|
164
|
+
column is that floor plus the rail's own padding and border — measured, not
|
|
165
|
+
guessed. Nothing here resizes the glyph: an icon-sizing rule inside a media
|
|
166
|
+
query is one src/styles/icon-size.test.js cannot measure, and it says so. */
|
|
49
167
|
@media (max-width: 720px) {
|
|
50
|
-
.ui-
|
|
51
|
-
.ui-
|
|
168
|
+
.ui-app { grid-template-columns: 58px 1fr; }
|
|
169
|
+
.ui-app__rail { padding: var(--space-3) 6px; gap: var(--space-3); }
|
|
170
|
+
.ui-app__rail .ui-nav--side { width: auto; }
|
|
171
|
+
.ui-app__rail .ui-nav__cap,
|
|
172
|
+
.ui-app__rail .ui-nav__label,
|
|
173
|
+
.ui-app__rail .ui-nav__badge,
|
|
174
|
+
.ui-app__rail .ui-nav__caret,
|
|
175
|
+
.ui-app__rail .ui-app__who,
|
|
176
|
+
.ui-app__brand span { display: none; }
|
|
177
|
+
.ui-app__rail .ui-nav__item { justify-content: center; gap: 0; padding: 0; min-height: 44px; }
|
|
178
|
+
.ui-app__rail .ui-nav__item.is-active::before { left: 3px; }
|
|
179
|
+
.ui-app__rail .ui-nav__foot { padding-top: var(--space-2); }
|
|
180
|
+
.ui-app__brand { justify-content: center; padding: 2px 0; }
|
|
181
|
+
.ui-app__user { justify-content: center; padding-left: 0; padding-right: 0; }
|
|
182
|
+
/* A group's children are rail rows at this width too. The nested list is NOT
|
|
183
|
+
folded away: wireNav() toggles the `hidden` attribute and nothing else, so
|
|
184
|
+
a display:none here left the toggle announcing aria-expanded over a list it
|
|
185
|
+
could not reach — and took the row carrying aria-current with it. What goes
|
|
186
|
+
instead is the indent and its guide hairline; a 46px column has no room. */
|
|
187
|
+
.ui-app__rail .ui-nav__sub { margin: 0; padding-left: 0; border-left: 0; }
|
|
188
|
+
.ui-app__rail .ui-nav__item--sub.is-active::before { left: 3px; }
|
|
189
|
+
/* sidebarNav() takes `icon` as optional at every level, and the glyph is the
|
|
190
|
+
whole of a row on screen here — so a row without one, at any level, would be
|
|
191
|
+
a blank 44px target. It gets a dot in the row's own ink instead; the
|
|
192
|
+
aria-label names it either way. */
|
|
193
|
+
.ui-app__rail .ui-nav__item:not(:has(.ui-nav__ic))::after {
|
|
194
|
+
content: "";
|
|
195
|
+
width: 5px; height: 5px;
|
|
196
|
+
border-radius: 50%;
|
|
197
|
+
background: currentColor;
|
|
198
|
+
opacity: 0.62;
|
|
199
|
+
}
|
|
200
|
+
.ui-app__main { padding: 28px var(--space-4) 56px; }
|
|
201
|
+
.ui-app__main h1 { font-size: 25px; }
|
|
202
|
+
/* A settings row is label-beside-control down to the width where the control
|
|
203
|
+
no longer fits beside anything; below that it stacks. Scoped to the shell
|
|
204
|
+
rather than written into card.css, because a card row outside a shell is
|
|
205
|
+
somebody else's container to size — see the report. */
|
|
206
|
+
.ui-app__main .ui-card__row { flex-wrap: wrap; gap: var(--space-3); }
|
|
52
207
|
}
|
|
53
208
|
|
|
54
209
|
/* -- Auth card (sign-in / consent) — centered, glowing --------------------- */
|
package/src/styles/nav.css
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* ============================================================================
|
|
2
2
|
* Navigation — sidebar rail, horizontal tabs, breadcrumbs. One token-driven
|
|
3
3
|
* class family (ui-nav__…) so every variant re-themes across accents +
|
|
4
|
-
* light/dark.
|
|
5
|
-
*
|
|
4
|
+
* light/dark. appShell() draws its rail from sidebarNav(), so this file is the
|
|
5
|
+
* page shell's rail as well as a standalone component. States are pure tokens.
|
|
6
6
|
* ========================================================================== */
|
|
7
7
|
|
|
8
8
|
.ui-nav { font-family: var(--font-sans); }
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
width: 100%;
|
|
36
36
|
padding: 9px 12px;
|
|
37
37
|
border-radius: 10px;
|
|
38
|
-
color: var(--text);
|
|
39
38
|
text-decoration: none;
|
|
40
39
|
font-size: 14.5px;
|
|
41
40
|
font-weight: 400;
|
|
@@ -47,6 +46,7 @@
|
|
|
47
46
|
font-family: inherit;
|
|
48
47
|
transition: background 0.16s var(--ease), color 0.16s var(--ease);
|
|
49
48
|
}
|
|
49
|
+
.ui-nav .ui-nav__item { color: var(--text); } /* (0,2,0): a host sheet's a:link is (0,1,1) — why: docs/adr/0007-one-page-shell-built-from-the-kits-own-nav.md */
|
|
50
50
|
.ui-nav__item:hover { background: var(--surface-2); color: var(--strong); }
|
|
51
51
|
.ui-nav__item.is-active {
|
|
52
52
|
background: var(--surface-3);
|
|
@@ -75,12 +75,26 @@
|
|
|
75
75
|
.ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: var(--accent); }
|
|
76
76
|
.ui-nav__label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
77
77
|
|
|
78
|
-
/* Destructive row (sign out, revoke). Quiet at rest, --pink once
|
|
79
|
-
reaches it —
|
|
78
|
+
/* Destructive row (sign out, revoke). Quiet at rest, --pink once a pointer or
|
|
79
|
+
the keyboard reaches it — as .ui-btn--danger and .ui-dropdown__item do. */
|
|
80
80
|
.ui-nav__item.is-danger { color: var(--muted); }
|
|
81
81
|
.ui-nav__item.is-danger:hover { background: var(--glow-pink); color: var(--pink); }
|
|
82
|
+
/* Tabbing onto it is the same "about to click this" the pointer means. The ring
|
|
83
|
+
says where you are; only this says what the row does. */
|
|
84
|
+
.ui-nav__item.is-danger:focus-visible { background: var(--glow-pink); color: var(--pink); }
|
|
82
85
|
.ui-nav__item.is-disabled { color: var(--muted); opacity: 0.55; cursor: default; pointer-events: none; }
|
|
83
86
|
|
|
87
|
+
/* The current row under the pointer. It rests on --surface-3, which is also what
|
|
88
|
+
a resting row hovers to, so the one row with no hover response was the row the
|
|
89
|
+
reader is standing on. There is no surface above --surface-3, so the step is
|
|
90
|
+
mixed: --text is near-white in dark and near-black in light, and both themes
|
|
91
|
+
raise a surface by moving it away from the page. Written after the danger
|
|
92
|
+
rules so it cannot out-rank them — sign out is never the current row, but a
|
|
93
|
+
rule that could take its wash would be one to argue with later. */
|
|
94
|
+
.ui-nav__item.is-active:not(.is-danger):hover {
|
|
95
|
+
background: color-mix(in srgb, var(--text) 10%, var(--surface-3));
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
/* Per-item badge / counter */
|
|
85
99
|
.ui-nav__badge {
|
|
86
100
|
flex: none;
|
|
@@ -101,6 +115,16 @@
|
|
|
101
115
|
.ui-nav__badge.is-live { background: var(--chip-success-fill); color: var(--chip-success-ink); }
|
|
102
116
|
.ui-nav__badge.is-danger { background: var(--glow-pink); color: var(--pink); }
|
|
103
117
|
.ui-nav__item.is-active .ui-nav__badge.is-neutral { background: var(--surface-3); color: var(--strong); }
|
|
118
|
+
/* Same shape, one row up: on an active row the accent counter takes the row's
|
|
119
|
+
OWN surface instead of laying its wash over it. An active row is already
|
|
120
|
+
painted --surface-3, so --glow-purple there is two tints stacked. Every
|
|
121
|
+
figure here is measured against the tokens this kit now ships. The accent on
|
|
122
|
+
that pair falls under WCAG AA in four of the eight theme x accent cells —
|
|
123
|
+
4.08 in dark Nebula, where the flat surface reads 4.92 — and no alpha reaches
|
|
124
|
+
it, the largest that clears being 0.06 in dark Nebula and 0.02 in light
|
|
125
|
+
Emerald. So the accent wash is painted on a base surface, never a raised one.
|
|
126
|
+
See #157 and docs/adr/0006-the-accent-is-measured-against-its-own-wash.md. */
|
|
127
|
+
.ui-nav__item.is-active .ui-nav__badge.is-accent { background: var(--surface-3); color: var(--accent); }
|
|
104
128
|
|
|
105
129
|
/* Collapsible group */
|
|
106
130
|
.ui-nav__group { display: flex; flex-direction: column; }
|
|
@@ -213,11 +237,14 @@
|
|
|
213
237
|
gap: 6px;
|
|
214
238
|
padding: 3px 6px;
|
|
215
239
|
border-radius: 7px;
|
|
216
|
-
color: var(--dim);
|
|
217
240
|
text-decoration: none;
|
|
218
241
|
font-size: 13px;
|
|
219
242
|
transition: color 0.16s var(--ease), background 0.16s var(--ease);
|
|
220
243
|
}
|
|
244
|
+
/* Carries the block's class as well as its own, for the reason .ui-nav__item
|
|
245
|
+
does above: a linked crumb is an <a>, and a host stylesheet's `a:link` is
|
|
246
|
+
(0,1,1), which outranks a bare (0,1,0) class. */
|
|
247
|
+
.ui-nav .ui-nav__crumb { color: var(--dim); }
|
|
221
248
|
a.ui-nav__crumb:hover { color: var(--strong); background: var(--surface-2); }
|
|
222
249
|
a.ui-nav__crumb:focus-visible { outline: none; box-shadow: var(--ring); }
|
|
223
250
|
.ui-nav__crumb.is-current { color: var(--strong); font-weight: 500; }
|
package/src/tokens/accents.css
CHANGED
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
--accent: #ff6a3d;
|
|
22
22
|
--accent-strong: var(--accent);
|
|
23
23
|
--accent-contrast: #241006;
|
|
24
|
-
|
|
24
|
+
/* Thinned from 0.18 for WCAG AA: the ember accent cleared every flat surface
|
|
25
|
+
but not its own wash over a card, which lifts the ground toward the ink. The
|
|
26
|
+
hue is untouched. See issue #157. */
|
|
27
|
+
--glow-purple: rgba(255, 106, 61, 0.15);
|
|
25
28
|
--ring: 0 0 0 3px rgba(255, 106, 61, 0.38);
|
|
26
29
|
--grad-from: #ff7a45;
|
|
27
30
|
--grad-to: #ffcf6a;
|
|
@@ -49,7 +52,9 @@
|
|
|
49
52
|
--accent: #3b9dff;
|
|
50
53
|
--accent-strong: var(--accent);
|
|
51
54
|
--accent-contrast: #04121f;
|
|
52
|
-
|
|
55
|
+
/* Thinned from 0.18 for WCAG AA, the same wash-over-a-card pair as dark
|
|
56
|
+
Phoenix and from the same alpha. The hue is untouched. See issue #157. */
|
|
57
|
+
--glow-purple: rgba(59, 157, 255, 0.15);
|
|
53
58
|
--ring: 0 0 0 3px rgba(59, 157, 255, 0.38);
|
|
54
59
|
--grad-from: #3b9dff;
|
|
55
60
|
--grad-to: #20dcf5;
|
|
@@ -58,9 +63,25 @@
|
|
|
58
63
|
--purple: #1156b8;
|
|
59
64
|
--purple-light: #1568d6;
|
|
60
65
|
--purple-mid: #1560c8;
|
|
61
|
-
|
|
66
|
+
/* Deepened for WCAG AA on the raised grey panel and its wash: #1568d6 read
|
|
67
|
+
4.54:1 on --surface-3 and 3.98:1 on --glow-purple over it; #005bc8 clears
|
|
68
|
+
both at 5.43:1 and 4.76:1. Both washed figures are at the 0.10 alpha below,
|
|
69
|
+
which is what this cell ships — an earlier version of this comment quoted
|
|
70
|
+
them at the 0.05 the wash briefly carried, which flattered both inks.
|
|
71
|
+
Light Ocean is the one light accent #96 never deepened, because it happened
|
|
72
|
+
to clear plain white. Same kind of move, smaller: hue and chroma unchanged
|
|
73
|
+
in OKLCH (H 258, C 0.185) and four points of lightness down, where #96's
|
|
74
|
+
two took about ten points and gave up 15.9% of the chroma on Phoenix and
|
|
75
|
+
17.6% on Emerald. See issues #96 and #157. */
|
|
76
|
+
--accent: #005bc8;
|
|
62
77
|
--accent-strong: var(--accent);
|
|
63
78
|
--accent-contrast: #ffffff;
|
|
79
|
+
/* Back to 0.10, the alpha this cell carried before #157. #157 thinned it to
|
|
80
|
+
0.05 while --accent was still #1568d6, because no alpha could carry that
|
|
81
|
+
blue over the grey panel. Deepening the ink is what closed the pair, so the
|
|
82
|
+
wash does not have to be the thing that gives. The rgb stays --purple-light
|
|
83
|
+
rather than the accent — in light a glow tints --purple-light, which
|
|
84
|
+
stories/signal-contrast.test.js holds. See issue #157. */
|
|
64
85
|
--glow-purple: rgba(21, 104, 214, 0.10);
|
|
65
86
|
--ring: 0 0 0 3px rgba(21, 104, 214, 0.28);
|
|
66
87
|
--grad-from: #1568d6;
|
|
@@ -89,7 +110,9 @@
|
|
|
89
110
|
--accent: #087a52;
|
|
90
111
|
--accent-strong: var(--accent);
|
|
91
112
|
--accent-contrast: #ffffff;
|
|
92
|
-
|
|
113
|
+
/* Thinned from 0.10 for WCAG AA, the same grey-panel pair as light Ocean and
|
|
114
|
+
from the same alpha. See issue #157. */
|
|
115
|
+
--glow-purple: rgba(11, 156, 104, 0.08);
|
|
93
116
|
--ring: 0 0 0 3px rgba(11, 156, 104, 0.28);
|
|
94
117
|
--grad-from: #0b9c68;
|
|
95
118
|
--grad-to: #12b0a0;
|