@aria-framework/theme 0.4.0 → 0.5.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 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 dark nav rail, identical in light + dark modes. Mobile-first: off-canvas
80
- overlay under 768px (hamburger + backdrop + Esc/backdrop-tap close, focus
81
- trapped via `inert`); an in-flow rail on desktop with a persisted collapse to a
82
- 64px icon rail. The package ships the **CSS** (in `theme.css`) and the
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
@@ -118,8 +120,12 @@ Notes:
118
120
  script: `if (localStorage.getItem('<key>') === '1') document.documentElement.classList.add('sidebar-collapsed')`.
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"` (emitted as
122
- `--side-*`). Icons in the examples are Bootstrap Icons, but any icon font works.
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.
123
129
 
124
130
  ## Mobile (Expo / React Native)
125
131
 
@@ -159,3 +165,21 @@ Token change → edit `tokens.json`, run `npm run build`, commit the regenerated
159
165
  hand-maintained web/mobile.
160
166
  - **v0.2.0** — added `tokens.json` + `build.js` generator (single source of record;
161
167
  web + mobile generated from one file). Unified the drifted dark `--line-strong`.
168
+ - **v0.3.0** — renamed to `@aria-framework/theme` and published to npm. Mobile
169
+ sources now compile to `dist/` (.js + `.d.ts`, via a `prepare` script) so RN
170
+ apps can import the package out-of-tree; `exports` point at the built files.
171
+ - **v0.4.0** — first shell component: the **sidebar rail** (CSS + `--side-*`
172
+ tokens + `sidebar.js` behavior with a per-app `data-storage-key`; markup
173
+ contract above). Build now assembles generated tokens → `src/base.css` →
174
+ `src/components/*.css`. Sidebar carries its own focus-visible contrast rule.
175
+ - **v0.4.1** — `sidebar.js` double-include guard (a second script tag no longer
176
+ registers duplicate listeners); this changelog added.
177
+ - **v0.5.0** — sidebar rail is now **per-scheme** (light rail in light mode).
178
+ `tokens.json` `"sidebar"` gained `light`/`dark` sub-objects and two new tokens:
179
+ `strong` (emphasis text, replaces hardcoded `#fff`) and `accent` (active bar /
180
+ icon / focus ring). Apps that used `--side-*` for deliberately-always-dark
181
+ surfaces must pin fixed colors instead.
182
+
183
+ > **0.x semver reminder:** `^0.4.0` matches `<0.5.0` only — consumers pick up
184
+ > patches via `npm update`, but every **minor** bump (0.4 → 0.5) requires
185
+ > editing the range in each consuming app.
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 (scheme-independent: dark in both modes) */
46
- --side-bg1:${T.sidebar.bg1}; --side-bg2:${T.sidebar.bg2}; --side-tx:${T.sidebar.tx}; --side-muted:${T.sidebar.muted};
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.4.0",
3
+ "version": "0.5.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,
@@ -1,7 +1,8 @@
1
1
  /* ── Component: sidebar rail (.s-side) ───────────────────────────────────────
2
- Dark nav rail, mobile-first: off-canvas overlay by default; the ≥768px block
3
- puts it back in flow as the desktop rail with a collapsible icon-rail mode.
4
- Behavior lives in sidebar.js (same package, served beside theme.css).
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:#fff; font-weight:700; font-size:16px; letter-spacing:-.01em; line-height:1; }
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:#fff; }
56
- .s-link.active{ background:var(--side-active); color:#fff; }
57
- .s-link.active::before{ content:""; position:absolute; left:-12px; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:var(--brand-bright); }
58
- .s-link.active i{ opacity:1; color:var(--brand-bright); }
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:#fff; font-size:13.5px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
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:#fff; }
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:#fff; }
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
- /* Brighter keyboard-focus ring inside the dark rail — the base a11y rule uses
86
- var(--brand), which is near-invisible against the rail background. */
87
- .s-link:focus-visible, .s-out:focus-visible, .s-brand-link:focus-visible, .s-collapse:focus-visible { outline-color: var(--brand-bright); }
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) scheme-INDEPENDENT: the rail is dark in both light and dark modes, so these emit into :root only, as --side-*.",
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
- "bg1": "#0E1417", "bg2": "#141C21",
39
- "tx": "#BAC3C8", "muted": "#69767D",
40
- "active": "rgba(21,184,166,.13)",
41
- "line": "rgba(255,255,255,.07)",
42
- "card": "rgba(255,255,255,.045)"
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
  }
package/web/sidebar.js CHANGED
@@ -21,6 +21,12 @@
21
21
  * two layers can't drift. CSP-safe: external, self-hosted, no inline script.
22
22
  */
23
23
  (function () {
24
+ // Double-include guard: a second copy (e.g. two layout partials both adding
25
+ // the script tag) would register a second delegated listener, making every
26
+ // toggle fire twice — a silent, error-free dead sidebar. First copy wins.
27
+ if (window.__ariaSidebar) return;
28
+ window.__ariaSidebar = true;
29
+
24
30
  // currentScript is only set during initial execution — capture before deferring.
25
31
  var script = document.currentScript;
26
32
  var KEY = (script && script.dataset && script.dataset.storageKey) || 'aria-sidebar-collapsed';
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 (scheme-independent: dark in both modes) */
18
- --side-bg1:#0E1417; --side-bg2:#141C21; --side-tx:#BAC3C8; --side-muted:#69767D;
19
- --side-active:rgba(21,184,166,.13); --side-line:rgba(255,255,255,.07); --side-card:rgba(255,255,255,.045);
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
- Dark nav rail, mobile-first: off-canvas overlay by default; the ≥768px block
96
- puts it back in flow as the desktop rail with a collapsible icon-rail mode.
97
- Behavior lives in sidebar.js (same package, served beside theme.css).
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:#fff; font-weight:700; font-size:16px; letter-spacing:-.01em; line-height:1; }
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:#fff; }
149
- .s-link.active{ background:var(--side-active); color:#fff; }
150
- .s-link.active::before{ content:""; position:absolute; left:-12px; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:var(--brand-bright); }
151
- .s-link.active i{ opacity:1; color:var(--brand-bright); }
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:#fff; font-size:13.5px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
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:#fff; }
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:#fff; }
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
- /* Brighter keyboard-focus ring inside the dark rail — the base a11y rule uses
179
- var(--brand), which is near-invisible against the rail background. */
180
- .s-link:focus-visible, .s-out:focus-visible, .s-brand-link:focus-visible, .s-collapse:focus-visible { outline-color: var(--brand-bright); }
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);