@aria-framework/theme 0.4.1 → 0.6.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/README.md +42 -8
- package/build.js +10 -3
- package/package.json +2 -1
- package/src/components/sidebar.css +15 -14
- package/tokens.json +17 -6
- package/web/theme-init.js +38 -0
- package/web/theme.css +21 -17
package/README.md
CHANGED
|
@@ -76,10 +76,12 @@ belongs here; if it knows about your domain, it stays in the app.
|
|
|
76
76
|
|
|
77
77
|
### Component: sidebar rail
|
|
78
78
|
|
|
79
|
-
A
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
A nav rail themed **per scheme** — light rail in light mode, dark rail in dark
|
|
80
|
+
mode (driven by the `--side-*` tokens, including `--side-strong` for emphasis
|
|
81
|
+
text and `--side-accent` for the active bar / icon / focus ring). Mobile-first:
|
|
82
|
+
off-canvas overlay under 768px (hamburger + backdrop + Esc/backdrop-tap close,
|
|
83
|
+
focus trapped via `inert`); an in-flow rail on desktop with a persisted collapse
|
|
84
|
+
to a 64px icon rail. The package ships the **CSS** (in `theme.css`) and the
|
|
83
85
|
**behavior** (`sidebar.js`); the app ships the **markup** against this contract:
|
|
84
86
|
|
|
85
87
|
```html
|
|
@@ -114,12 +116,35 @@ trapped via `inert`); an in-flow rail on desktop with a persisted collapse to a
|
|
|
114
116
|
Notes:
|
|
115
117
|
- `data-storage-key` namespaces the persisted collapsed state per app (default
|
|
116
118
|
`aria-sidebar-collapsed`). Keep an app's historical key across migrations.
|
|
117
|
-
- To avoid a flash of the expanded rail on load,
|
|
118
|
-
|
|
119
|
+
- To avoid a flash of the expanded rail on load, load `theme-init.js` (below)
|
|
120
|
+
synchronously in `<head>` — it pre-applies the class before first paint.
|
|
119
121
|
- State classes live on `<html>`: `.sidebar-open` (mobile overlay),
|
|
120
122
|
`.sidebar-collapsed` (desktop icon rail).
|
|
121
|
-
- Sidebar tokens are themeable in `tokens.json` under `"sidebar"` (
|
|
122
|
-
`--side-*`
|
|
123
|
+
- Sidebar tokens are themeable in `tokens.json` under `"sidebar"` (per-scheme
|
|
124
|
+
`light`/`dark` sub-objects, emitted as `--side-*` in `:root` and the dark
|
|
125
|
+
block). Icons in the examples are Bootstrap Icons, but any icon font works.
|
|
126
|
+
- If your app paints something with `var(--side-*)` that should IGNORE the
|
|
127
|
+
scheme (e.g. a permanently-dark login hero panel), use fixed values there —
|
|
128
|
+
since 0.5.0 these tokens follow the active scheme.
|
|
129
|
+
|
|
130
|
+
## Pre-paint init (`theme-init.js`, since 0.6.0)
|
|
131
|
+
|
|
132
|
+
Resolves the color mode BEFORE first paint (no FOUC) and pre-applies the
|
|
133
|
+
collapsed-sidebar class. Load it synchronously in `<head>` — NOT `defer`red:
|
|
134
|
+
|
|
135
|
+
```html
|
|
136
|
+
<html data-theme-pref="system"> <!-- server stamps light|dark|system -->
|
|
137
|
+
<head>
|
|
138
|
+
<script src="/theme/theme-init.js" data-storage-key="myapp-sidebar-collapsed"></script>
|
|
139
|
+
...
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- `data-theme-pref="system"` resolves from the OS color scheme and live-updates
|
|
143
|
+
if the OS theme changes; `light`/`dark` are forced modes. The result lands on
|
|
144
|
+
`<html data-bs-theme="...">` (Bootstrap 5.3 convention, what `theme.css` keys on).
|
|
145
|
+
- `data-storage-key` MUST match the key given to `sidebar.js` (same default:
|
|
146
|
+
`aria-sidebar-collapsed`).
|
|
147
|
+
- CSP-safe: external + self-hosted, works under `script-src 'self'`.
|
|
123
148
|
|
|
124
149
|
## Mobile (Expo / React Native)
|
|
125
150
|
|
|
@@ -168,6 +193,15 @@ Token change → edit `tokens.json`, run `npm run build`, commit the regenerated
|
|
|
168
193
|
`src/components/*.css`. Sidebar carries its own focus-visible contrast rule.
|
|
169
194
|
- **v0.4.1** — `sidebar.js` double-include guard (a second script tag no longer
|
|
170
195
|
registers duplicate listeners); this changelog added.
|
|
196
|
+
- **v0.5.0** — sidebar rail is now **per-scheme** (light rail in light mode).
|
|
197
|
+
`tokens.json` `"sidebar"` gained `light`/`dark` sub-objects and two new tokens:
|
|
198
|
+
`strong` (emphasis text, replaces hardcoded `#fff`) and `accent` (active bar /
|
|
199
|
+
icon / focus ring). Apps that used `--side-*` for deliberately-always-dark
|
|
200
|
+
surfaces must pin fixed colors instead.
|
|
201
|
+
- **v0.6.0** — added **`web/theme-init.js`**: pre-paint color-mode resolver
|
|
202
|
+
(`data-theme-pref` light/dark/system + live OS-change sync) and
|
|
203
|
+
collapsed-sidebar pre-apply, parameterized by the same `data-storage-key` as
|
|
204
|
+
`sidebar.js`. Apps can delete their local copies.
|
|
171
205
|
|
|
172
206
|
> **0.x semver reminder:** `^0.4.0` matches `<0.5.0` only — consumers pick up
|
|
173
207
|
> patches via `npm update`, but every **minor** bump (0.4 → 0.5) requires
|
package/build.js
CHANGED
|
@@ -22,6 +22,9 @@ const hexToRgb = (hex) => {
|
|
|
22
22
|
for (const key of ["palettes", "sidebar", "status", "priority", "space"]) {
|
|
23
23
|
if (!T[key]) throw new Error(`tokens.json is missing the "${key}" section (required since theme 0.4.0)`);
|
|
24
24
|
}
|
|
25
|
+
if (!T.sidebar.light || !T.sidebar.dark) {
|
|
26
|
+
throw new Error('tokens.json "sidebar" must have "light" and "dark" sub-objects (per-scheme since theme 0.5.0)');
|
|
27
|
+
}
|
|
25
28
|
|
|
26
29
|
/* ── web/theme.css ────────────────────────────────────────────────────────── */
|
|
27
30
|
const cssTokens = (p) => ` --brand:${p.brand}; --brand-deep:${p.brandDeep}; --brand-bright:${p.brandBright}; --brand-rgb:${hexToRgb(p.brand)};
|
|
@@ -31,6 +34,9 @@ const cssTokens = (p) => ` --brand:${p.brand}; --brand-deep:${p.brandDeep}; --b
|
|
|
31
34
|
--shadow:${p.shadowWeb};
|
|
32
35
|
--shadow-lg:${p.shadowLgWeb};`;
|
|
33
36
|
|
|
37
|
+
const sideTokens = (s) => ` --side-bg1:${s.bg1}; --side-bg2:${s.bg2}; --side-tx:${s.tx}; --side-muted:${s.muted}; --side-strong:${s.strong};
|
|
38
|
+
--side-active:${s.active}; --side-line:${s.line}; --side-card:${s.card}; --side-accent:${s.accent};`;
|
|
39
|
+
|
|
34
40
|
const themeCss = `${GEN_CSS}
|
|
35
41
|
/* @aria-framework/theme — Field Ops Console design system (web).
|
|
36
42
|
Drop-in token layer for any Bootstrap 5.3 app. Load order in <head>:
|
|
@@ -42,12 +48,13 @@ const themeCss = `${GEN_CSS}
|
|
|
42
48
|
--font-sans:${FONT_SANS};
|
|
43
49
|
--font-mono:${FONT_MONO};
|
|
44
50
|
${cssTokens(T.palettes.light)}
|
|
45
|
-
/* Sidebar rail (
|
|
46
|
-
|
|
47
|
-
--side-active:${T.sidebar.active}; --side-line:${T.sidebar.line}; --side-card:${T.sidebar.card};
|
|
51
|
+
/* Sidebar rail (light) */
|
|
52
|
+
${sideTokens(T.sidebar.light)}
|
|
48
53
|
}
|
|
49
54
|
[data-bs-theme="dark"]{
|
|
50
55
|
${cssTokens(T.palettes.dark)}
|
|
56
|
+
/* Sidebar rail (dark) */
|
|
57
|
+
${sideTokens(T.sidebar.dark)}
|
|
51
58
|
}`;
|
|
52
59
|
|
|
53
60
|
/* ── mobile/tokens.ts ─────────────────────────────────────────────────────── */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aria-framework/theme",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Aria App Framework — theme module. Field Ops Console design system: tokens.json single source of record + a generator that emits a Bootstrap 5.3 web theme (theme.css + self-hosted fonts) and a React Native palette/provider/primitives. Light + dark.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"exports": {
|
|
27
27
|
"./theme.css": "./web/theme.css",
|
|
28
28
|
"./sidebar.js": "./web/sidebar.js",
|
|
29
|
+
"./theme-init.js": "./web/theme-init.js",
|
|
29
30
|
"./mobile": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
30
31
|
"./tokens": { "types": "./dist/tokens.d.ts", "default": "./dist/tokens.js" },
|
|
31
32
|
"./fonts": { "types": "./dist/fonts.d.ts", "default": "./dist/fonts.js" },
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* ── Component: sidebar rail (.s-side) ───────────────────────────────────────
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
Nav rail, themed per scheme via the --side-* tokens (light rail in light
|
|
3
|
+
mode, dark rail in dark mode). Mobile-first: off-canvas overlay by default;
|
|
4
|
+
the ≥768px block puts it back in flow as the desktop rail with a collapsible
|
|
5
|
+
icon-rail mode. Behavior lives in sidebar.js (served beside theme.css).
|
|
5
6
|
|
|
6
7
|
MARKUP CONTRACT (the app renders this; see package README):
|
|
7
8
|
#wrapper > .s-side + #page-content-wrapper
|
|
@@ -37,7 +38,7 @@ html.sidebar-open #sidebar-backdrop{ opacity:1; visibility:visible; }
|
|
|
37
38
|
background:linear-gradient(140deg, var(--brand-bright), var(--brand-deep)); color:#fff; font-weight:800; font-size:17px;
|
|
38
39
|
box-shadow:0 0 0 1px rgba(255,255,255,.06), 0 6px 16px -6px var(--brand);
|
|
39
40
|
}
|
|
40
|
-
.s-name{ display:block; color
|
|
41
|
+
.s-name{ display:block; color:var(--side-strong); font-weight:700; font-size:16px; letter-spacing:-.01em; line-height:1; }
|
|
41
42
|
.s-ver{ display:block; font-family:var(--font-mono); font-size:11px; color:var(--side-muted); margin-top:3px; }
|
|
42
43
|
|
|
43
44
|
.s-nav{ flex:1; overflow-y:auto; padding:4px 12px 12px; }
|
|
@@ -52,10 +53,10 @@ html.sidebar-open #sidebar-backdrop{ opacity:1; visibility:visible; }
|
|
|
52
53
|
white-space:nowrap;
|
|
53
54
|
}
|
|
54
55
|
.s-link i{ font-size:16px; width:18px; text-align:center; opacity:.85; }
|
|
55
|
-
.s-link:hover{ background:var(--side-card); color
|
|
56
|
-
.s-link.active{ background:var(--side-active); color
|
|
57
|
-
.s-link.active::before{ content:""; position:absolute; left:-12px; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:var(--
|
|
58
|
-
.s-link.active i{ opacity:1; color:var(--
|
|
56
|
+
.s-link:hover{ background:var(--side-card); color:var(--side-strong); }
|
|
57
|
+
.s-link.active{ background:var(--side-active); color:var(--side-strong); }
|
|
58
|
+
.s-link.active::before{ content:""; position:absolute; left:-12px; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:var(--side-accent); }
|
|
59
|
+
.s-link.active i{ opacity:1; color:var(--side-accent); }
|
|
59
60
|
|
|
60
61
|
.s-me{
|
|
61
62
|
margin:10px 12px 14px; padding:10px 12px; border-radius:11px; display:flex; align-items:center; gap:10px;
|
|
@@ -66,11 +67,11 @@ html.sidebar-open #sidebar-backdrop{ opacity:1; visibility:visible; }
|
|
|
66
67
|
background:linear-gradient(140deg, var(--brand), var(--brand-deep)); color:#fff; font-weight:700; font-size:13px;
|
|
67
68
|
}
|
|
68
69
|
.s-me-who{ min-width:0; line-height:1.25; flex:1; }
|
|
69
|
-
.s-me-who b{ display:block; color
|
|
70
|
+
.s-me-who b{ display:block; color:var(--side-strong); font-size:13.5px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
70
71
|
.s-me-who span{ font-family:var(--font-mono); font-size:10.5px; color:var(--side-muted); text-transform:uppercase; letter-spacing:.06em; }
|
|
71
72
|
.s-out-form{ margin:0; }
|
|
72
73
|
.s-out{ border:0; background:transparent; color:var(--side-muted); font-size:16px; line-height:1; padding:4px; cursor:pointer; }
|
|
73
|
-
.s-out:hover{ color
|
|
74
|
+
.s-out:hover{ color:var(--side-strong); }
|
|
74
75
|
|
|
75
76
|
/* Collapse toggle — the sidebar-header chevron (shown on desktop) and the
|
|
76
77
|
topbar hamburger (shown on mobile, opens the overlay). */
|
|
@@ -78,13 +79,13 @@ html.sidebar-open #sidebar-backdrop{ opacity:1; visibility:visible; }
|
|
|
78
79
|
flex:0 0 auto; margin-left:auto; width:28px; height:28px; padding:0; border:0; cursor:pointer;
|
|
79
80
|
background:transparent; color:var(--side-muted); border-radius:7px; display:none; place-items:center; font-size:15px;
|
|
80
81
|
}
|
|
81
|
-
.s-collapse:hover{ background:var(--side-card); color
|
|
82
|
+
.s-collapse:hover{ background:var(--side-card); color:var(--side-strong); }
|
|
82
83
|
.s-collapse i{ display:block; transition:transform .18s ease; } /* rotate the <i> (centre origin) — no SVG-pivot bug */
|
|
83
84
|
html.sidebar-collapsed .s-collapse i{ transform:rotate(180deg); } /* points right = expand */
|
|
84
85
|
|
|
85
|
-
/*
|
|
86
|
-
var(--brand), which
|
|
87
|
-
.s-link:focus-visible, .s-out:focus-visible, .s-brand-link:focus-visible, .s-collapse:focus-visible { outline-color: var(--
|
|
86
|
+
/* Keyboard-focus ring in the rail's own accent — the base a11y rule uses
|
|
87
|
+
var(--brand), which can vanish against the rail background. */
|
|
88
|
+
.s-link:focus-visible, .s-out:focus-visible, .s-brand-link:focus-visible, .s-collapse:focus-visible { outline-color: var(--side-accent); }
|
|
88
89
|
|
|
89
90
|
.topbar-toggle{
|
|
90
91
|
flex:0 0 auto; width:38px; height:38px; border-radius:10px; border:1px solid var(--line);
|
package/tokens.json
CHANGED
|
@@ -33,12 +33,23 @@
|
|
|
33
33
|
"status": { "new": "#2563eb", "open": "#0e8fa8", "pending": "#d9870b", "resolved": "#0e9e6e", "closed": "#7a848b" },
|
|
34
34
|
"priority": { "low": "#9aa6ae", "medium": "#2563eb", "high": "#ea580c", "critical": "#dc2626" },
|
|
35
35
|
"space": { "xs": 4, "sm": 8, "md": 12, "lg": 16, "xl": 24 },
|
|
36
|
-
"$sidebarComment": "Sidebar rail tokens (web sidebar component)
|
|
36
|
+
"$sidebarComment": "Sidebar rail tokens (web sidebar component), PER-SCHEME (--side-* in :root = light, overridden in [data-bs-theme=dark]). 'strong' = emphasis text (brand name, active/hover labels); 'accent' = active bar / active icon / focus ring — brand-bright pops on the dark rail, plain brand reads better on the light one.",
|
|
37
37
|
"sidebar": {
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
"light": {
|
|
39
|
+
"bg1": "#FBFCFA", "bg2": "#F3F4F0",
|
|
40
|
+
"tx": "#4A555E", "muted": "#8C969D", "strong": "#18222A",
|
|
41
|
+
"active": "rgba(14,122,110,.10)",
|
|
42
|
+
"line": "rgba(24,34,42,.08)",
|
|
43
|
+
"card": "rgba(24,34,42,.05)",
|
|
44
|
+
"accent": "#0E7A6E"
|
|
45
|
+
},
|
|
46
|
+
"dark": {
|
|
47
|
+
"bg1": "#0E1417", "bg2": "#141C21",
|
|
48
|
+
"tx": "#BAC3C8", "muted": "#69767D", "strong": "#FFFFFF",
|
|
49
|
+
"active": "rgba(21,184,166,.13)",
|
|
50
|
+
"line": "rgba(255,255,255,.07)",
|
|
51
|
+
"card": "rgba(255,255,255,.045)",
|
|
52
|
+
"accent": "#15B8A6"
|
|
53
|
+
}
|
|
43
54
|
}
|
|
44
55
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* theme-init.js — resolve the active color mode BEFORE first paint (no FOUC).
|
|
3
|
+
*
|
|
4
|
+
* Load synchronously in <head> (NOT deferred) so it runs before the body
|
|
5
|
+
* renders. The server stamps <html data-theme-pref="light|dark|system"> from
|
|
6
|
+
* the signed-in user's saved preference and a best-guess data-bs-theme. When
|
|
7
|
+
* the preference is "system" we resolve it here from the OS color scheme and
|
|
8
|
+
* keep it in sync if the OS theme changes while the app is open. CSP-safe:
|
|
9
|
+
* external, self-hosted, no inline script.
|
|
10
|
+
*
|
|
11
|
+
* Also pre-applies the collapsed-sidebar preference so a collapsed rail does
|
|
12
|
+
* not flash open on load. The localStorage key comes from this script tag's
|
|
13
|
+
* data-storage-key attribute and MUST match the one passed to sidebar.js:
|
|
14
|
+
*
|
|
15
|
+
* <script src="/theme/theme-init.js" data-storage-key="myapp-sidebar-collapsed"></script>
|
|
16
|
+
*/
|
|
17
|
+
(function () {
|
|
18
|
+
var el = document.documentElement;
|
|
19
|
+
var script = document.currentScript;
|
|
20
|
+
var sidebarKey = (script && script.dataset && script.dataset.storageKey) || 'aria-sidebar-collapsed';
|
|
21
|
+
|
|
22
|
+
var pref = el.getAttribute('data-theme-pref') || 'system';
|
|
23
|
+
function resolve() {
|
|
24
|
+
var mode = pref === 'dark' ? 'dark'
|
|
25
|
+
: pref === 'light' ? 'light'
|
|
26
|
+
: (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
|
27
|
+
el.setAttribute('data-bs-theme', mode);
|
|
28
|
+
}
|
|
29
|
+
resolve();
|
|
30
|
+
if (pref === 'system' && window.matchMedia) {
|
|
31
|
+
try { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', resolve); }
|
|
32
|
+
catch (e) { /* older browsers: no live OS-change updates */ }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
if (localStorage.getItem(sidebarKey) === '1') el.classList.add('sidebar-collapsed');
|
|
37
|
+
} catch (e) { /* private mode / storage disabled */ }
|
|
38
|
+
})();
|
package/web/theme.css
CHANGED
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
--text:#18222A; --text-soft:#5C6770; --text-faint:#929BA1;
|
|
15
15
|
--shadow:0 1px 2px rgba(16,24,28,.05), 0 4px 14px -6px rgba(16,24,28,.12);
|
|
16
16
|
--shadow-lg:0 18px 50px -18px rgba(16,24,28,.30);
|
|
17
|
-
/* Sidebar rail (
|
|
18
|
-
--side-bg1:#
|
|
19
|
-
--side-active:rgba(
|
|
17
|
+
/* Sidebar rail (light) */
|
|
18
|
+
--side-bg1:#FBFCFA; --side-bg2:#F3F4F0; --side-tx:#4A555E; --side-muted:#8C969D; --side-strong:#18222A;
|
|
19
|
+
--side-active:rgba(14,122,110,.10); --side-line:rgba(24,34,42,.08); --side-card:rgba(24,34,42,.05); --side-accent:#0E7A6E;
|
|
20
20
|
}
|
|
21
21
|
[data-bs-theme="dark"]{
|
|
22
22
|
--brand:#16B5A3; --brand-deep:#0E7A6E; --brand-bright:#2FE0CC; --brand-rgb:22,181,163;
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
--text:#E7EDEF; --text-soft:#9BA7AE; --text-faint:#69767D;
|
|
26
26
|
--shadow:0 1px 2px rgba(0,0,0,.3), 0 8px 24px -10px rgba(0,0,0,.6);
|
|
27
27
|
--shadow-lg:0 24px 60px -18px rgba(0,0,0,.8);
|
|
28
|
+
/* Sidebar rail (dark) */
|
|
29
|
+
--side-bg1:#0E1417; --side-bg2:#141C21; --side-tx:#BAC3C8; --side-muted:#69767D; --side-strong:#FFFFFF;
|
|
30
|
+
--side-active:rgba(21,184,166,.13); --side-line:rgba(255,255,255,.07); --side-card:rgba(255,255,255,.045); --side-accent:#15B8A6;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/* ── Base — static layer (fonts, Bootstrap mapping, edge-clarity, buttons, a11y).
|
|
@@ -92,9 +95,10 @@ textarea:focus-visible, [tabindex]:focus-visible {
|
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
/* ── Component: sidebar rail (.s-side) ───────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
Nav rail, themed per scheme via the --side-* tokens (light rail in light
|
|
99
|
+
mode, dark rail in dark mode). Mobile-first: off-canvas overlay by default;
|
|
100
|
+
the ≥768px block puts it back in flow as the desktop rail with a collapsible
|
|
101
|
+
icon-rail mode. Behavior lives in sidebar.js (served beside theme.css).
|
|
98
102
|
|
|
99
103
|
MARKUP CONTRACT (the app renders this; see package README):
|
|
100
104
|
#wrapper > .s-side + #page-content-wrapper
|
|
@@ -130,7 +134,7 @@ html.sidebar-open #sidebar-backdrop{ opacity:1; visibility:visible; }
|
|
|
130
134
|
background:linear-gradient(140deg, var(--brand-bright), var(--brand-deep)); color:#fff; font-weight:800; font-size:17px;
|
|
131
135
|
box-shadow:0 0 0 1px rgba(255,255,255,.06), 0 6px 16px -6px var(--brand);
|
|
132
136
|
}
|
|
133
|
-
.s-name{ display:block; color
|
|
137
|
+
.s-name{ display:block; color:var(--side-strong); font-weight:700; font-size:16px; letter-spacing:-.01em; line-height:1; }
|
|
134
138
|
.s-ver{ display:block; font-family:var(--font-mono); font-size:11px; color:var(--side-muted); margin-top:3px; }
|
|
135
139
|
|
|
136
140
|
.s-nav{ flex:1; overflow-y:auto; padding:4px 12px 12px; }
|
|
@@ -145,10 +149,10 @@ html.sidebar-open #sidebar-backdrop{ opacity:1; visibility:visible; }
|
|
|
145
149
|
white-space:nowrap;
|
|
146
150
|
}
|
|
147
151
|
.s-link i{ font-size:16px; width:18px; text-align:center; opacity:.85; }
|
|
148
|
-
.s-link:hover{ background:var(--side-card); color
|
|
149
|
-
.s-link.active{ background:var(--side-active); color
|
|
150
|
-
.s-link.active::before{ content:""; position:absolute; left:-12px; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:var(--
|
|
151
|
-
.s-link.active i{ opacity:1; color:var(--
|
|
152
|
+
.s-link:hover{ background:var(--side-card); color:var(--side-strong); }
|
|
153
|
+
.s-link.active{ background:var(--side-active); color:var(--side-strong); }
|
|
154
|
+
.s-link.active::before{ content:""; position:absolute; left:-12px; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:var(--side-accent); }
|
|
155
|
+
.s-link.active i{ opacity:1; color:var(--side-accent); }
|
|
152
156
|
|
|
153
157
|
.s-me{
|
|
154
158
|
margin:10px 12px 14px; padding:10px 12px; border-radius:11px; display:flex; align-items:center; gap:10px;
|
|
@@ -159,11 +163,11 @@ html.sidebar-open #sidebar-backdrop{ opacity:1; visibility:visible; }
|
|
|
159
163
|
background:linear-gradient(140deg, var(--brand), var(--brand-deep)); color:#fff; font-weight:700; font-size:13px;
|
|
160
164
|
}
|
|
161
165
|
.s-me-who{ min-width:0; line-height:1.25; flex:1; }
|
|
162
|
-
.s-me-who b{ display:block; color
|
|
166
|
+
.s-me-who b{ display:block; color:var(--side-strong); font-size:13.5px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
163
167
|
.s-me-who span{ font-family:var(--font-mono); font-size:10.5px; color:var(--side-muted); text-transform:uppercase; letter-spacing:.06em; }
|
|
164
168
|
.s-out-form{ margin:0; }
|
|
165
169
|
.s-out{ border:0; background:transparent; color:var(--side-muted); font-size:16px; line-height:1; padding:4px; cursor:pointer; }
|
|
166
|
-
.s-out:hover{ color
|
|
170
|
+
.s-out:hover{ color:var(--side-strong); }
|
|
167
171
|
|
|
168
172
|
/* Collapse toggle — the sidebar-header chevron (shown on desktop) and the
|
|
169
173
|
topbar hamburger (shown on mobile, opens the overlay). */
|
|
@@ -171,13 +175,13 @@ html.sidebar-open #sidebar-backdrop{ opacity:1; visibility:visible; }
|
|
|
171
175
|
flex:0 0 auto; margin-left:auto; width:28px; height:28px; padding:0; border:0; cursor:pointer;
|
|
172
176
|
background:transparent; color:var(--side-muted); border-radius:7px; display:none; place-items:center; font-size:15px;
|
|
173
177
|
}
|
|
174
|
-
.s-collapse:hover{ background:var(--side-card); color
|
|
178
|
+
.s-collapse:hover{ background:var(--side-card); color:var(--side-strong); }
|
|
175
179
|
.s-collapse i{ display:block; transition:transform .18s ease; } /* rotate the <i> (centre origin) — no SVG-pivot bug */
|
|
176
180
|
html.sidebar-collapsed .s-collapse i{ transform:rotate(180deg); } /* points right = expand */
|
|
177
181
|
|
|
178
|
-
/*
|
|
179
|
-
var(--brand), which
|
|
180
|
-
.s-link:focus-visible, .s-out:focus-visible, .s-brand-link:focus-visible, .s-collapse:focus-visible { outline-color: var(--
|
|
182
|
+
/* Keyboard-focus ring in the rail's own accent — the base a11y rule uses
|
|
183
|
+
var(--brand), which can vanish against the rail background. */
|
|
184
|
+
.s-link:focus-visible, .s-out:focus-visible, .s-brand-link:focus-visible, .s-collapse:focus-visible { outline-color: var(--side-accent); }
|
|
181
185
|
|
|
182
186
|
.topbar-toggle{
|
|
183
187
|
flex:0 0 auto; width:38px; height:38px; border-radius:10px; border:1px solid var(--line);
|