rcf-lite 0.30.0 → 0.31.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/CHANGELOG.md +46 -0
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/define-draft-shapes.md +15 -10
- package/package.json +1 -1
- package/rcf/adrs/adr-4131-gates-bite-ack-recorded-override.json +3 -3
- package/rcf/adrs/adr-4137-standalone-theme-persistence.json +30 -0
- package/rcf/adrs/adr-4138-tree-wide-checks-before-scope-shortcircuit.json +35 -0
- package/rcf/code-nodes/cn-771.json +14 -0
- package/rcf/code-nodes/cn-772.json +14 -0
- package/rcf/code-nodes/cn-773.json +16 -0
- package/rcf/code-nodes/cn-774.json +20 -0
- package/rcf/code-nodes/cn-775.json +15 -0
- package/rcf/code-nodes/cn-776.json +17 -0
- package/rcf/code-nodes/cn-777.json +21 -0
- package/rcf/fbs/fbs-202.json +18 -2
- package/rcf/fbs/fbs-203.json +12 -4
- package/rcf/requirements/req-002.json +2 -2
- package/rcf/requirements/req-173.json +2 -2
- package/rcf/requirements/req-174.json +2 -2
- package/rcf/tacs/tac-4122-define-gates.json +7 -2
- package/rcf/test-suites/ts-007.json +1 -1
- package/rcf/test-suites/ts-215.json +64 -1
- package/rcf/test-suites/ts-216.json +18 -2
- package/rcf/test-suites/ts-219.json +10 -2
- package/rcf/test-suites/ts-220.json +18 -2
- package/rcf/test-suites/ts-225.json +10 -2
- package/rcf/test-suites/ts-229.json +49 -3
- package/rcf/test-suites/ts-230.json +26 -2
- package/rcf/test-suites/ts-231.json +16 -1
- package/rcf/test-suites/ts-236.json +44 -0
- package/rcf/user-stories/us-17302.json +36 -1
- package/rcf/user-stories/us-17401.json +11 -1
- package/rcf/user-stories/us-17402.json +6 -1
- package/rcf/user-stories/us-17403.json +11 -1
- package/rcf/user-stories/us-17404.json +16 -1
- package/rcf/user-stories/us-17405.json +7 -2
- package/rcf/user-stories/us-205.json +6 -1
- package/rcf/user-stories/us-206.json +25 -4
- package/rcf/user-stories/us-207.json +36 -0
- package/releases/releases.yaml +11 -1
- package/src/cli/create.js +22 -2
- package/src/cli/ledger.js +96 -4
- package/src/cli/readiness.js +15 -1
- package/src/core/errors/index.js +7 -1
- package/src/core/store/writer.js +181 -13
- package/src/define/ledgers.js +86 -7
- package/src/mcp/tools.js +1 -1
- package/src/query/gates.js +383 -150
- package/src/view/html-page.js +14 -2
- package/src/view/page-init.js +149 -8
- package/src/view/style.css +97 -18
- package/src/view/view-index.js +22 -0
package/src/view/html-page.js
CHANGED
|
@@ -59,7 +59,7 @@ import { renderEntitySelector } from './components/entity-selector.js';
|
|
|
59
59
|
import { renderSubTabStrip } from './components/sub-tab-strip.js';
|
|
60
60
|
import { buildDagLayout, buildDagInspectorPayload, renderBuildDag } from './build-dag.js';
|
|
61
61
|
import { computeReqNeedsWorkIds, needsWorkReasonFor } from './needs-work.js';
|
|
62
|
-
import { renderLookupModal, renderSearchButton } from './view-index.js';
|
|
62
|
+
import { renderLookupModal, renderSearchButton, renderThemeControl } from './view-index.js';
|
|
63
63
|
|
|
64
64
|
// Umbrella version stamped at module load (same pattern as src/ruleset/index.js).
|
|
65
65
|
// Used by the shell footer so the muted "RCF Lite X.Y.Z" line tracks the
|
|
@@ -81,10 +81,21 @@ const LIVE_WRAPPER_CLOSE = '</div>';
|
|
|
81
81
|
* Render the complete index.html string. Phase 3.8: always includes the
|
|
82
82
|
* live-content wrapper and the live-client script tag.
|
|
83
83
|
*
|
|
84
|
+
* `opts.embed = true` strips the three-state theme control from the
|
|
85
|
+
* emitted HTML entirely (ADR-4137 server-side layer). The production
|
|
86
|
+
* server path at `renderModelToPage` serves one page to every client,
|
|
87
|
+
* so it never passes `embed`; the client-side layer in `page-init.js`
|
|
88
|
+
* removes `[data-rcf-theme-control]` from the DOM when `?embed=1` is
|
|
89
|
+
* on the URL. The option is here so `renderPage({ embed: true })` is
|
|
90
|
+
* a one-call proof that the control markup can be absent (used by
|
|
91
|
+
* `test/view/theme-control.test.js` for AC-206-9).
|
|
92
|
+
*
|
|
84
93
|
* @param {import('./tree-model.js').BuiltTreeModel} model
|
|
94
|
+
* @param {{ embed?: boolean }} [opts]
|
|
85
95
|
* @returns {string}
|
|
86
96
|
*/
|
|
87
|
-
export function renderPage(model) {
|
|
97
|
+
export function renderPage(model, opts) {
|
|
98
|
+
const embed = Boolean(opts && opts.embed);
|
|
88
99
|
const header = resolveProductHeader(model);
|
|
89
100
|
const projectName = header.projectName ?? 'RCF project';
|
|
90
101
|
const contentHtml = renderContent(model);
|
|
@@ -124,6 +135,7 @@ export function renderPage(model) {
|
|
|
124
135
|
</nav>
|
|
125
136
|
<div class="tools" aria-label="Viewer tools">
|
|
126
137
|
${renderSearchButton()}
|
|
138
|
+
${embed ? '' : renderThemeControl()}
|
|
127
139
|
</div>
|
|
128
140
|
</div>
|
|
129
141
|
</header>
|
package/src/view/page-init.js
CHANGED
|
@@ -62,15 +62,151 @@
|
|
|
62
62
|
else root.removeAttribute('data-embed');
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Theme provenance (PR 291 landing review F1). live-client re-runs
|
|
66
|
+
// rcfPage.init() after every SSE swap, so the boot theme must be
|
|
67
|
+
// resolved and applied ONCE. The state lives on <html> (which
|
|
68
|
+
// survives every swap and any re-evaluation of this script), like
|
|
69
|
+
// the __rcf*Wired sentinels elsewhere in this file. `source` is the
|
|
70
|
+
// last thing that set the theme: 'query' (?theme=), 'stored'
|
|
71
|
+
// (rcf-view:v1:theme), 'default' (auto), 'host' (rcf-view-theme
|
|
72
|
+
// postMessage) or 'control' (a click on the theme control). A
|
|
73
|
+
// re-init never overrides 'host' or 'control'.
|
|
74
|
+
function themeState() {
|
|
75
|
+
var root = document.documentElement;
|
|
76
|
+
if (!root.__rcfThemeState) root.__rcfThemeState = { booted: false, source: null, choice: 'auto' };
|
|
77
|
+
return root.__rcfThemeState;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function setTheme(mode, source) {
|
|
81
|
+
applyTheme(mode === 'auto' ? null : mode);
|
|
82
|
+
var st = themeState();
|
|
83
|
+
st.source = source;
|
|
84
|
+
st.choice = mode;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Three-state theme control (ADR-4137 standalone-theme-persistence,
|
|
88
|
+
// Baz 2026-10-04 w-2026-10-04-dave-001). The control writes
|
|
89
|
+
// `rcf-view:v1:theme` in non-embed mode only; under embed the control
|
|
90
|
+
// is removed from the DOM entirely and the key is never read or
|
|
91
|
+
// written. The key sits under the shared `rcf-view:v1:` namespace so
|
|
92
|
+
// the PR 9 localStorage namespace lint stays green.
|
|
93
|
+
var THEME_STORAGE_KEY = 'rcf-view:v1:theme';
|
|
94
|
+
|
|
95
|
+
function isEmbedActive() {
|
|
96
|
+
try { return document.documentElement.getAttribute('data-embed') === '1'; }
|
|
97
|
+
catch (e) { return false; }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function safeStorage() {
|
|
101
|
+
try {
|
|
102
|
+
if (typeof window === 'undefined' || !window.localStorage) return null;
|
|
103
|
+
return window.localStorage;
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// localStorage can throw on strict privacy modes; treat as absent.
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readStoredTheme() {
|
|
111
|
+
// The storage boundary (ADR-4137 consequences): under embed we
|
|
112
|
+
// never read the shared key, so a future re-render of the control
|
|
113
|
+
// under embed cannot accidentally pick up a value either.
|
|
114
|
+
if (isEmbedActive()) return null;
|
|
115
|
+
var s = safeStorage();
|
|
116
|
+
if (!s) return null;
|
|
117
|
+
try {
|
|
118
|
+
var v = s.getItem(THEME_STORAGE_KEY);
|
|
119
|
+
if (v === 'light' || v === 'dark' || v === 'auto') return v;
|
|
120
|
+
return null;
|
|
121
|
+
} catch (e) { return null; }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeStoredTheme(v) {
|
|
125
|
+
if (isEmbedActive()) return; // never write under embed (ADR-4137)
|
|
126
|
+
if (v !== 'light' && v !== 'dark' && v !== 'auto') return;
|
|
127
|
+
var s = safeStorage();
|
|
128
|
+
if (!s) return;
|
|
129
|
+
try { s.setItem(THEME_STORAGE_KEY, v); } catch (e) { /* quota or disabled */ }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Reflect the current effective theme choice (light|dark|auto) on
|
|
133
|
+
// the control's three buttons. Idempotent across live swaps so the
|
|
134
|
+
// Router re-run after an SSE innerHTML swap of `#rcf-live-content`
|
|
135
|
+
// re-syncs (the control itself sits outside the swap wrapper so it
|
|
136
|
+
// survives, but a fresh page-init call still re-wires guards).
|
|
137
|
+
function reflectThemeChoice(mode) {
|
|
138
|
+
var buttons = document.querySelectorAll('[data-rcf-theme-control] [data-rcf-theme]');
|
|
139
|
+
for (var i = 0; i < buttons.length; i += 1) {
|
|
140
|
+
var b = buttons[i];
|
|
141
|
+
var isTarget = b.getAttribute('data-rcf-theme') === mode;
|
|
142
|
+
b.setAttribute('aria-pressed', isTarget ? 'true' : 'false');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function wireThemeControl() {
|
|
147
|
+
if (isEmbedActive()) {
|
|
148
|
+
// ADR-4137: control is absent from the DOM entirely under embed,
|
|
149
|
+
// not merely hidden by CSS. renderPage({ embed: true }) already
|
|
150
|
+
// omits the markup; this strip-on-boot is the client-side layer
|
|
151
|
+
// for the production path where the server renders one page for
|
|
152
|
+
// every client regardless of the client's query.
|
|
153
|
+
var ctl = document.querySelector('[data-rcf-theme-control]');
|
|
154
|
+
if (ctl && ctl.parentNode) ctl.parentNode.removeChild(ctl);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
var buttons = document.querySelectorAll('[data-rcf-theme-control] [data-rcf-theme]');
|
|
158
|
+
for (var i = 0; i < buttons.length; i += 1) {
|
|
159
|
+
var btn = buttons[i];
|
|
160
|
+
if (btn.__rcfThemeWired) continue;
|
|
161
|
+
btn.__rcfThemeWired = true;
|
|
162
|
+
btn.addEventListener('click', function (ev) {
|
|
163
|
+
var mode = ev.currentTarget.getAttribute('data-rcf-theme');
|
|
164
|
+
if (mode !== 'light' && mode !== 'dark' && mode !== 'auto') return;
|
|
165
|
+
setTheme(mode, 'control');
|
|
166
|
+
writeStoredTheme(mode);
|
|
167
|
+
reflectThemeChoice(mode);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Resolve the effective theme choice for boot, in the precedence
|
|
173
|
+
// ADR-4137 pins: explicit ?theme= wins first; then (non-embed only)
|
|
174
|
+
// the stored value; then auto. The stored value is NEVER consulted
|
|
175
|
+
// under embed. Returns { mode: 'light'|'dark'|'auto', source:
|
|
176
|
+
// 'query'|'stored'|'default' }.
|
|
177
|
+
function resolveBootTheme(query) {
|
|
178
|
+
var q = query && (query.theme === 'light' || query.theme === 'dark' || query.theme === 'auto')
|
|
179
|
+
? query.theme : null;
|
|
180
|
+
if (q) return { mode: q, source: 'query' };
|
|
181
|
+
var stored = readStoredTheme();
|
|
182
|
+
if (stored === 'light' || stored === 'dark' || stored === 'auto') return { mode: stored, source: 'stored' };
|
|
183
|
+
return { mode: 'auto', source: 'default' };
|
|
184
|
+
}
|
|
185
|
+
|
|
65
186
|
function initShellFromQuery() {
|
|
66
187
|
var q = parseQuery(window.location.search);
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
// style.css. Absent stays light (the base :root tokens).
|
|
70
|
-
var theme = q.theme;
|
|
71
|
-
if (theme === 'light' || theme === 'dark' || theme === 'auto') applyTheme(theme === 'auto' ? null : theme);
|
|
72
|
-
// embed: any value other than exactly "1" is ignored.
|
|
188
|
+
// embed first so isEmbedActive() reads the right boundary when
|
|
189
|
+
// the theme precedence check consults storage.
|
|
73
190
|
applyEmbed(q.embed === '1');
|
|
191
|
+
var st = themeState();
|
|
192
|
+
var qTheme = (q.theme === 'light' || q.theme === 'dark' || q.theme === 'auto') ? q.theme : null;
|
|
193
|
+
if (!st.booted) {
|
|
194
|
+
// First init: resolve the boot theme in ADR-4137 precedence and
|
|
195
|
+
// apply it once.
|
|
196
|
+
st.booted = true;
|
|
197
|
+
var boot = resolveBootTheme(q);
|
|
198
|
+
setTheme(boot.mode, boot.source);
|
|
199
|
+
} else if (qTheme && st.source === 'query') {
|
|
200
|
+
// Re-init (SSE swap): re-assert ?theme= only while it is still
|
|
201
|
+
// the last thing that set the theme. A host postMessage or a
|
|
202
|
+
// control click since boot wins and is never overridden; with no
|
|
203
|
+
// ?theme= a re-init applies nothing (embed host themes survive).
|
|
204
|
+
setTheme(qTheme, 'query');
|
|
205
|
+
}
|
|
206
|
+
// Strip or wire the control after embed has been stamped; the
|
|
207
|
+
// helper reads data-embed to pick the branch.
|
|
208
|
+
wireThemeControl();
|
|
209
|
+
reflectThemeChoice(st.choice);
|
|
74
210
|
}
|
|
75
211
|
|
|
76
212
|
var messageListenerWired = false;
|
|
@@ -84,7 +220,12 @@
|
|
|
84
220
|
if (!ev || ev.origin !== window.location.origin) return;
|
|
85
221
|
var data = ev.data || {};
|
|
86
222
|
if (data.type !== 'rcf-view-theme') return;
|
|
87
|
-
if (data.theme === 'light' || data.theme === 'dark')
|
|
223
|
+
if (data.theme === 'light' || data.theme === 'dark') {
|
|
224
|
+
setTheme(data.theme, 'host');
|
|
225
|
+
// Live postMessage never writes the stored key (ADR-4137):
|
|
226
|
+
// the host-driven swap is a session-only override.
|
|
227
|
+
reflectThemeChoice(data.theme);
|
|
228
|
+
}
|
|
88
229
|
});
|
|
89
230
|
}
|
|
90
231
|
|
|
@@ -632,7 +773,7 @@
|
|
|
632
773
|
btn.__rcfFixtureThemeWired = true;
|
|
633
774
|
btn.addEventListener('click', function (ev) {
|
|
634
775
|
var mode = ev.currentTarget.getAttribute('data-rcf-fixture-theme');
|
|
635
|
-
|
|
776
|
+
setTheme(mode === 'light' || mode === 'dark' ? mode : 'auto', 'control');
|
|
636
777
|
var buttons = document.querySelectorAll('[data-rcf-fixture-theme]');
|
|
637
778
|
for (var j = 0; j < buttons.length; j += 1) {
|
|
638
779
|
var b = buttons[j];
|
package/src/view/style.css
CHANGED
|
@@ -810,7 +810,10 @@ footer.app-footer a {
|
|
|
810
810
|
|
|
811
811
|
footer.app-footer a:hover { text-decoration: underline; }
|
|
812
812
|
|
|
813
|
-
|
|
813
|
+
/* Baz 2026-10-04 w-2026-10-04-dave-001: separator was `--sv-control`
|
|
814
|
+
which fell under 4.5:1 in both themes (3.51:1 light, 4.21:1 dark);
|
|
815
|
+
`--sv-muted` is the "muted text" token designed for this. */
|
|
816
|
+
footer.app-footer .sep { color: var(--sv-muted); }
|
|
814
817
|
|
|
815
818
|
footer.app-footer .live {
|
|
816
819
|
margin-left: auto;
|
|
@@ -955,7 +958,7 @@ html[data-embed="1"] .pm-controls { top: 3rem; }
|
|
|
955
958
|
color: var(--sv-ink);
|
|
956
959
|
font-weight: 600;
|
|
957
960
|
box-shadow: var(--shadow-sm);
|
|
958
|
-
border-color: var(--sv-
|
|
961
|
+
border-color: var(--sv-control);
|
|
959
962
|
}
|
|
960
963
|
|
|
961
964
|
.pm-status-filter {
|
|
@@ -1151,7 +1154,10 @@ details.pm-bucket[open] > summary.pm-bucket-heading {
|
|
|
1151
1154
|
.pm-mini-review { color: var(--sv-link); background: var(--sv-active); border-color: var(--sv-link); }
|
|
1152
1155
|
.pm-mini-draft { color: var(--sv-muted); background: var(--sv-raised); border-color: var(--sv-border); }
|
|
1153
1156
|
.pm-mini-needsRevision { color: var(--sv-danger); background: var(--sv-dangerbg); border-color: var(--sv-danger); }
|
|
1154
|
-
|
|
1157
|
+
/* Baz 2026-10-04 w-2026-10-04-dave-001: `--sv-control` on `--sv-raised`
|
|
1158
|
+
was 3.51:1 light / 4.21:1 dark, below the 4.5:1 floor for small text.
|
|
1159
|
+
`--sv-muted` is the matching muted-text token and clears AA in both. */
|
|
1160
|
+
.pm-mini-superseded { color: var(--sv-muted); background: var(--sv-raised); border-color: var(--sv-border); }
|
|
1155
1161
|
|
|
1156
1162
|
.pm-bucket-body { padding: 0.75rem 1rem 1rem; }
|
|
1157
1163
|
.pm-bucket-body .pm-tad-root { margin-bottom: 0.65rem; }
|
|
@@ -1225,7 +1231,9 @@ details.pm-bucket[open] > summary.pm-bucket-heading {
|
|
|
1225
1231
|
}
|
|
1226
1232
|
.pm-blueprint-cap-count {
|
|
1227
1233
|
font-weight: 500;
|
|
1228
|
-
|
|
1234
|
+
/* Baz 2026-10-04 w-2026-10-04-dave-001: `--sv-control` fell under
|
|
1235
|
+
the 4.5:1 small-text floor; switched to `--sv-muted`. */
|
|
1236
|
+
color: var(--sv-muted);
|
|
1229
1237
|
margin-left: 0.25rem;
|
|
1230
1238
|
}
|
|
1231
1239
|
.pm-blueprint-cap-body { display: block; }
|
|
@@ -2260,19 +2268,38 @@ details.rcf-row.rcf-row--inner > summary { padding: 0.4rem 0.6rem; }
|
|
|
2260
2268
|
align-items: center;
|
|
2261
2269
|
gap: 0.35rem;
|
|
2262
2270
|
padding: 0.3rem 0.6rem;
|
|
2263
|
-
background: var(--sv-surface
|
|
2264
|
-
color: var(--sv-
|
|
2265
|
-
border: 1px solid var(--sv-border
|
|
2271
|
+
background: var(--sv-surface);
|
|
2272
|
+
color: var(--sv-ink);
|
|
2273
|
+
border: 1px solid var(--sv-border);
|
|
2266
2274
|
border-radius: 999px;
|
|
2267
2275
|
font: inherit;
|
|
2268
2276
|
font-size: 0.85rem;
|
|
2269
2277
|
line-height: 1;
|
|
2270
2278
|
cursor: pointer;
|
|
2271
|
-
transition: background-color 0.1s ease-out, border-color 0.1s ease-out;
|
|
2279
|
+
transition: background-color 0.1s ease-out, border-color 0.1s ease-out, color 0.1s ease-out;
|
|
2280
|
+
}
|
|
2281
|
+
/* Baz 2026-10-04 w-2026-10-04-dave-001: hover previously fell back to
|
|
2282
|
+
a hard-coded light grey (`#f0f3f8`) because `--sv-surface-hover` was
|
|
2283
|
+
never declared; in dark that painted dark ink on a light grey and
|
|
2284
|
+
made the FIND button unreadable. The hover now uses `--sv-active`
|
|
2285
|
+
(the Stravica brand "selected / hovered surface" token, defined in
|
|
2286
|
+
both the `:root` and `:root[data-theme="dark"]` blocks) with the
|
|
2287
|
+
control-strength border so the boundary stays >= 3:1 against the
|
|
2288
|
+
header surface in both themes. The focus-visible outline uses
|
|
2289
|
+
`--sv-focus` (brand focus token) at 3px with 2px offset. */
|
|
2290
|
+
.rcf-search-btn:hover,
|
|
2291
|
+
.rcf-search-btn:focus-visible {
|
|
2292
|
+
background: var(--sv-active);
|
|
2293
|
+
color: var(--sv-ink);
|
|
2294
|
+
border-color: var(--sv-control);
|
|
2295
|
+
}
|
|
2296
|
+
.rcf-search-btn:active {
|
|
2297
|
+
background: var(--sv-active);
|
|
2298
|
+
color: var(--sv-ink);
|
|
2299
|
+
border-color: var(--sv-focus);
|
|
2272
2300
|
}
|
|
2273
|
-
.rcf-search-btn:hover { background: var(--sv-surface-hover, #f0f3f8); }
|
|
2274
2301
|
.rcf-search-btn:focus-visible { outline: 3px solid var(--sv-focus); outline-offset: 2px; }
|
|
2275
|
-
.rcf-search-btn .rcf-search-icon { flex-shrink: 0; opacity: 0.
|
|
2302
|
+
.rcf-search-btn .rcf-search-icon { flex-shrink: 0; opacity: 0.85; color: var(--sv-ink); }
|
|
2276
2303
|
.rcf-search-btn .rcf-search-kbd {
|
|
2277
2304
|
display: inline-flex;
|
|
2278
2305
|
gap: 0.1rem;
|
|
@@ -2282,9 +2309,10 @@ details.rcf-row.rcf-row--inner > summary { padding: 0.4rem 0.6rem; }
|
|
|
2282
2309
|
}
|
|
2283
2310
|
.rcf-search-btn .rcf-search-kbd kbd {
|
|
2284
2311
|
padding: 0 0.25rem;
|
|
2285
|
-
border: 1px solid var(--sv-border
|
|
2312
|
+
border: 1px solid var(--sv-border);
|
|
2286
2313
|
border-radius: 3px;
|
|
2287
|
-
background: var(--sv-surface
|
|
2314
|
+
background: var(--sv-surface);
|
|
2315
|
+
color: var(--sv-ink);
|
|
2288
2316
|
font-family: inherit;
|
|
2289
2317
|
}
|
|
2290
2318
|
|
|
@@ -2292,6 +2320,57 @@ details.rcf-row.rcf-row--inner > summary { padding: 0.4rem 0.6rem; }
|
|
|
2292
2320
|
.rcf-search-btn .rcf-search-kbd { display: none; }
|
|
2293
2321
|
}
|
|
2294
2322
|
|
|
2323
|
+
/* --------------------------------------------------------------------
|
|
2324
|
+
* Three-state theme control (ADR-4137, Baz 2026-10-04).
|
|
2325
|
+
*
|
|
2326
|
+
* Sits in `.tools` right of the SearchButton in non-embed mode; absent
|
|
2327
|
+
* from the DOM entirely under `data-embed="1"` (page-init.js removes
|
|
2328
|
+
* the element, renderPage({ embed: true }) also skips the markup).
|
|
2329
|
+
* The group buttons use the same ink/active/control tokens as the
|
|
2330
|
+
* SearchButton so hover and focus states stay above WCAG AA in both
|
|
2331
|
+
* themes.
|
|
2332
|
+
* -------------------------------------------------------------------- */
|
|
2333
|
+
|
|
2334
|
+
.rcf-theme {
|
|
2335
|
+
display: inline-flex;
|
|
2336
|
+
align-items: stretch;
|
|
2337
|
+
border: 1px solid var(--sv-border);
|
|
2338
|
+
border-radius: 999px;
|
|
2339
|
+
overflow: hidden;
|
|
2340
|
+
background: var(--sv-surface);
|
|
2341
|
+
}
|
|
2342
|
+
.rcf-theme-btn {
|
|
2343
|
+
appearance: none;
|
|
2344
|
+
background: transparent;
|
|
2345
|
+
color: var(--sv-ink);
|
|
2346
|
+
border: 0;
|
|
2347
|
+
padding: 0.3rem 0.55rem;
|
|
2348
|
+
font: inherit;
|
|
2349
|
+
font-size: 0.75rem;
|
|
2350
|
+
line-height: 1;
|
|
2351
|
+
cursor: pointer;
|
|
2352
|
+
transition: background-color 0.1s ease-out, color 0.1s ease-out;
|
|
2353
|
+
}
|
|
2354
|
+
.rcf-theme-btn + .rcf-theme-btn { border-left: 1px solid var(--sv-border); }
|
|
2355
|
+
.rcf-theme-btn:hover {
|
|
2356
|
+
background: var(--sv-active);
|
|
2357
|
+
color: var(--sv-ink);
|
|
2358
|
+
}
|
|
2359
|
+
.rcf-theme-btn:focus-visible {
|
|
2360
|
+
outline: 3px solid var(--sv-focus);
|
|
2361
|
+
outline-offset: -3px;
|
|
2362
|
+
background: var(--sv-active);
|
|
2363
|
+
}
|
|
2364
|
+
.rcf-theme-btn:active {
|
|
2365
|
+
background: var(--sv-active);
|
|
2366
|
+
color: var(--sv-ink);
|
|
2367
|
+
}
|
|
2368
|
+
.rcf-theme-btn[aria-pressed="true"] {
|
|
2369
|
+
background: var(--sv-button);
|
|
2370
|
+
color: var(--sv-onbutton);
|
|
2371
|
+
}
|
|
2372
|
+
html[data-embed="1"] .rcf-theme { display: none; }
|
|
2373
|
+
|
|
2295
2374
|
.rcf-lookup[hidden] { display: none !important; }
|
|
2296
2375
|
.rcf-lookup {
|
|
2297
2376
|
position: fixed;
|
|
@@ -2346,7 +2425,7 @@ details.rcf-row.rcf-row--inner > summary { padding: 0.4rem 0.6rem; }
|
|
|
2346
2425
|
border-radius: 4px;
|
|
2347
2426
|
cursor: pointer;
|
|
2348
2427
|
}
|
|
2349
|
-
.rcf-lookup-close:hover { color: var(--sv-text); border-color: var(--sv-
|
|
2428
|
+
.rcf-lookup-close:hover { color: var(--sv-text); border-color: var(--sv-control); }
|
|
2350
2429
|
.rcf-lookup-close:focus-visible { outline: 3px solid var(--sv-focus); outline-offset: 1px; }
|
|
2351
2430
|
|
|
2352
2431
|
.rcf-lookup-field { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
@@ -2360,9 +2439,9 @@ details.rcf-row.rcf-row--inner > summary { padding: 0.4rem 0.6rem; }
|
|
|
2360
2439
|
padding: 0.55rem 0.75rem;
|
|
2361
2440
|
font: inherit;
|
|
2362
2441
|
font-size: 0.95rem;
|
|
2363
|
-
border: 1px solid var(--sv-
|
|
2442
|
+
border: 1px solid var(--sv-control);
|
|
2364
2443
|
border-radius: 6px;
|
|
2365
|
-
background: var(--sv-surface
|
|
2444
|
+
background: var(--sv-surface);
|
|
2366
2445
|
color: var(--sv-text);
|
|
2367
2446
|
}
|
|
2368
2447
|
.rcf-lookup-input:focus-visible { outline: 3px solid var(--sv-focus); outline-offset: 1px; border-color: var(--sv-link, #3a5bb5); }
|
|
@@ -2389,9 +2468,9 @@ details.rcf-row.rcf-row--inner > summary { padding: 0.4rem 0.6rem; }
|
|
|
2389
2468
|
cursor: pointer;
|
|
2390
2469
|
border: 1px solid transparent;
|
|
2391
2470
|
}
|
|
2392
|
-
.rcf-lookup-row:hover { background: var(--sv-
|
|
2471
|
+
.rcf-lookup-row:hover { background: var(--sv-active); }
|
|
2393
2472
|
.rcf-lookup-row.is-active {
|
|
2394
|
-
background: var(--sv-
|
|
2473
|
+
background: var(--sv-active);
|
|
2395
2474
|
border-color: var(--sv-link, #3a5bb5);
|
|
2396
2475
|
}
|
|
2397
2476
|
.rcf-lookup-row mark {
|
|
@@ -2467,7 +2546,7 @@ details.rcf-row.rcf-row--inner > summary { padding: 0.4rem 0.6rem; }
|
|
|
2467
2546
|
font: inherit;
|
|
2468
2547
|
font-size: 0.85rem;
|
|
2469
2548
|
}
|
|
2470
|
-
.rcf-lookup-reload:hover { background: var(--sv-
|
|
2549
|
+
.rcf-lookup-reload:hover { background: var(--sv-active); }
|
|
2471
2550
|
.rcf-lookup-reload:focus-visible { outline: 3px solid var(--sv-focus); outline-offset: 1px; }
|
|
2472
2551
|
|
|
2473
2552
|
html[data-theme="dark"] .rcf-lookup-backdrop { background: rgba(0, 0, 0, 0.55); }
|
package/src/view/view-index.js
CHANGED
|
@@ -321,3 +321,25 @@ export function renderSearchButton() {
|
|
|
321
321
|
<span class="rcf-search-kbd" aria-hidden="true"><kbd>Ctrl</kbd>+<kbd>F</kbd></span>
|
|
322
322
|
</button>`;
|
|
323
323
|
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Three-state Light|Dark|Auto theme control for the standalone viewer
|
|
327
|
+
* shell (viewer UI refresh design section 11 recommendation 6, Baz's
|
|
328
|
+
* 2026-10-04 review `w-2026-10-04-dave-001`). The control is rendered
|
|
329
|
+
* server-side without an initial `aria-pressed=true` on any button; the
|
|
330
|
+
* boot path in `page-init.js` reads `?theme=`, then (non-embed only)
|
|
331
|
+
* `localStorage['rcf-view:v1:theme']` per ADR-4137, then defaults to
|
|
332
|
+
* Auto, and stamps the right button with `aria-pressed="true"` from
|
|
333
|
+
* that resolved value. Under `embed=1` `html-page.js` omits this call
|
|
334
|
+
* entirely so no element with `[data-rcf-theme-control]` reaches the
|
|
335
|
+
* DOM and the storage key is never read or written (ADR-4137).
|
|
336
|
+
*
|
|
337
|
+
* @returns {string}
|
|
338
|
+
*/
|
|
339
|
+
export function renderThemeControl() {
|
|
340
|
+
return `<div class="rcf-theme" data-rcf-theme-control role="group" aria-label="Theme">
|
|
341
|
+
<button type="button" class="rcf-theme-btn" data-rcf-theme="light" aria-pressed="false" title="Light theme" aria-label="Light theme">Light</button>
|
|
342
|
+
<button type="button" class="rcf-theme-btn" data-rcf-theme="dark" aria-pressed="false" title="Dark theme" aria-label="Dark theme">Dark</button>
|
|
343
|
+
<button type="button" class="rcf-theme-btn" data-rcf-theme="auto" aria-pressed="false" title="Follow system theme" aria-label="Follow system theme">Auto</button>
|
|
344
|
+
</div>`;
|
|
345
|
+
}
|