rcf-lite 0.30.0 → 0.32.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 +67 -0
- package/blueprints/deploy-cloudflare-workers/contributions/user-stories/deploy-cloudflare-workers-us-12113.json +1 -1
- 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-017.json +4 -2
- package/rcf/code-nodes/cn-771.json +15 -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/code-nodes/cn-778.json +14 -0
- package/rcf/fbs/fbs-004.json +4 -1
- package/rcf/fbs/fbs-201.json +2 -1
- 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/tacs/tac-4127-readiness-view.json +3 -2
- package/rcf/test-suites/ts-004.json +82 -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-223.json +24 -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-18001.json +6 -1
- package/rcf/user-stories/us-202.json +25 -1
- 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 +20 -1
- package/src/cli/create.js +22 -2
- package/src/cli/freeze.js +8 -0
- package/src/cli/ledger.js +96 -4
- package/src/cli/readiness.js +17 -39
- package/src/core/errors/index.js +7 -1
- package/src/core/store/writer.js +181 -13
- package/src/define/interface-paths.js +47 -0
- package/src/define/ledgers.js +86 -7
- package/src/mcp/tools.js +1 -1
- package/src/query/gates.js +431 -163
- package/src/view/html-page.js +43 -13
- package/src/view/live-client.js +18 -10
- package/src/view/page-init.js +149 -8
- package/src/view/style.css +107 -20
- 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>
|
|
@@ -831,21 +843,33 @@ function buildSpecFilterText(fbs) {
|
|
|
831
843
|
}
|
|
832
844
|
|
|
833
845
|
/**
|
|
834
|
-
* Viewer UI refresh PR 2 (shell polish, Baz 2026-10-02; decision 17)
|
|
835
|
-
* the
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
*
|
|
846
|
+
* Viewer UI refresh PR 2 (shell polish, Baz 2026-10-02; decision 17)
|
|
847
|
+
* with the issue-295 flip (w-2026-10-06-dave-001): the one-bar header
|
|
848
|
+
* carries the project name only, sourced from `prd.productName` first
|
|
849
|
+
* (the human-readable name an owner edits via `rcf define update
|
|
850
|
+
* PRD-001 --set productName=...`), then `manifest.projectName` (the
|
|
851
|
+
* init-time value kept for ids / paths), then `prd.productTitle` (the
|
|
852
|
+
* PRD title the brief names). Decision 17 remains: project name
|
|
853
|
+
* prominent after the icon mark, tool name out of the bar, no
|
|
854
|
+
* technology chip. When the tree exposes no project name, the whole
|
|
855
|
+
* product block collapses and the brand icon stands on its own
|
|
856
|
+
* (silent empty state; CSS `.product:empty { display:none; }`).
|
|
857
|
+
*
|
|
858
|
+
* The precedence flip is a lite-native cut (US-202, Part 2 cut b):
|
|
859
|
+
* `rcf define update MANIFEST` is not addressable in the current CLI,
|
|
860
|
+
* so before this change manifest.projectName was a one-shot init value
|
|
861
|
+
* and the only way to rename the audit surface was a hand edit of
|
|
862
|
+
* manifest.json. prd.productName is already updatable via `rcf define
|
|
863
|
+
* update PRD-001 --set productName=...`; preferring it in the header
|
|
864
|
+
* means the display name is lite-native without touching the manifest
|
|
865
|
+
* (ids and paths are untouched).
|
|
842
866
|
*
|
|
843
867
|
* @param {import('./tree-model.js').BuiltTreeModel} model
|
|
844
868
|
* @returns {{ projectName: string | null }}
|
|
845
869
|
*/
|
|
846
870
|
export function resolveProductHeader(model) {
|
|
847
|
-
const projectName = model.
|
|
848
|
-
?? model.
|
|
871
|
+
const projectName = model.prd?.productName
|
|
872
|
+
?? model.manifest?.projectName
|
|
849
873
|
?? model.prd?.productTitle
|
|
850
874
|
?? null;
|
|
851
875
|
return { projectName };
|
|
@@ -858,7 +882,13 @@ function renderProductBlock(header) {
|
|
|
858
882
|
// empty state (brief) survives screen-reader output too.
|
|
859
883
|
return '<div class="product"></div>';
|
|
860
884
|
}
|
|
861
|
-
|
|
885
|
+
// w-2026-10-06-dave-001 (issue 295): .product .name clips its text
|
|
886
|
+
// with text-overflow:ellipsis so a long project name yields rather
|
|
887
|
+
// than pushing the tablist off the bar; the title attribute carries
|
|
888
|
+
// the full name so hover reveals it. Both the title and the body go
|
|
889
|
+
// through escapeHtml so the attribute is safe against injection.
|
|
890
|
+
const safe = escapeHtml(header.projectName);
|
|
891
|
+
return `<div class="product"><span class="name" title="${safe}">${safe}</span>
|
|
862
892
|
<span class="sub">review surface</span></div>`;
|
|
863
893
|
}
|
|
864
894
|
|
package/src/view/live-client.js
CHANGED
|
@@ -507,19 +507,27 @@
|
|
|
507
507
|
// the panels back in line with the aria-selected button and re-run
|
|
508
508
|
// Mermaid in the visible one.
|
|
509
509
|
function resyncTabsAfterSwap(doc, win) {
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
// Scope to
|
|
510
|
+
// Issue 297 (w-2026-10-06-dave-003, AC-18001-8): the authoritative
|
|
511
|
+
// tab list is the server-rendered nav.tabs itself, not a parallel
|
|
512
|
+
// array that can drift. Earlier the list was hand-copied here and
|
|
513
|
+
// omitted 'readiness' (added as tab 1 in US-18001); a Readiness
|
|
514
|
+
// SSE swap fell back to Overview and both panels went visible.
|
|
515
|
+
// Deriving from the DOM means a future tab cannot be forgotten
|
|
516
|
+
// the same way. Scope to nav.tabs to avoid matching pm-group-btn
|
|
517
517
|
// nodes (they also carry role="tab").
|
|
518
|
+
var buttons = doc.querySelectorAll('nav.tabs [role="tab"][data-tab]');
|
|
519
|
+
var names = [];
|
|
520
|
+
for (var b = 0; b < buttons.length; b += 1) {
|
|
521
|
+
var n = buttons[b].getAttribute ? buttons[b].getAttribute('data-tab') : null;
|
|
522
|
+
if (n) names.push(n);
|
|
523
|
+
}
|
|
518
524
|
var selectedBtn = doc.querySelector('nav.tabs [role="tab"][aria-selected="true"]');
|
|
519
525
|
var selected = selectedBtn && selectedBtn.getAttribute ? selectedBtn.getAttribute('data-tab') : null;
|
|
520
|
-
if (!selected ||
|
|
521
|
-
|
|
522
|
-
|
|
526
|
+
if (!selected || names.indexOf(selected) === -1) {
|
|
527
|
+
selected = names.indexOf('overview') !== -1 ? 'overview' : (names[0] || 'overview');
|
|
528
|
+
}
|
|
529
|
+
for (var i = 0; i < names.length; i += 1) {
|
|
530
|
+
var name = names[i];
|
|
523
531
|
var panel = doc.getElementById('tab-' + name);
|
|
524
532
|
if (!panel) continue;
|
|
525
533
|
if (name === selected) panel.removeAttribute('hidden');
|
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
|
@@ -236,6 +236,10 @@ header.app-header .bar {
|
|
|
236
236
|
color: var(--sv-ink);
|
|
237
237
|
overflow: hidden;
|
|
238
238
|
text-overflow: ellipsis;
|
|
239
|
+
/* w-2026-10-06-dave-001 (issue 295): .product is a flex container; the
|
|
240
|
+
ellipsis only triggers when its child is itself free to shrink below
|
|
241
|
+
intrinsic. min-width:0 unlocks that. */
|
|
242
|
+
min-width: 0;
|
|
239
243
|
}
|
|
240
244
|
|
|
241
245
|
.product .sub {
|
|
@@ -248,8 +252,12 @@ nav.tabs {
|
|
|
248
252
|
display: flex;
|
|
249
253
|
gap: 0.25rem;
|
|
250
254
|
margin-left: 1rem;
|
|
251
|
-
|
|
252
|
-
|
|
255
|
+
/* w-2026-10-06-dave-001 (issue 295): the tablist grows to fill the
|
|
256
|
+
bar but never shrinks below its intrinsic width (flex-shrink:0),
|
|
257
|
+
so every main tab stays visible and the Find button in .tools
|
|
258
|
+
never sits on top of a clipped tab. .product carries the shrink
|
|
259
|
+
budget via its own min-width:0 and the ellipsis on .product .name. */
|
|
260
|
+
flex: 1 0 auto;
|
|
253
261
|
overflow-x: auto;
|
|
254
262
|
-webkit-overflow-scrolling: touch;
|
|
255
263
|
}
|
|
@@ -810,7 +818,10 @@ footer.app-footer a {
|
|
|
810
818
|
|
|
811
819
|
footer.app-footer a:hover { text-decoration: underline; }
|
|
812
820
|
|
|
813
|
-
|
|
821
|
+
/* Baz 2026-10-04 w-2026-10-04-dave-001: separator was `--sv-control`
|
|
822
|
+
which fell under 4.5:1 in both themes (3.51:1 light, 4.21:1 dark);
|
|
823
|
+
`--sv-muted` is the "muted text" token designed for this. */
|
|
824
|
+
footer.app-footer .sep { color: var(--sv-muted); }
|
|
814
825
|
|
|
815
826
|
footer.app-footer .live {
|
|
816
827
|
margin-left: auto;
|
|
@@ -955,7 +966,7 @@ html[data-embed="1"] .pm-controls { top: 3rem; }
|
|
|
955
966
|
color: var(--sv-ink);
|
|
956
967
|
font-weight: 600;
|
|
957
968
|
box-shadow: var(--shadow-sm);
|
|
958
|
-
border-color: var(--sv-
|
|
969
|
+
border-color: var(--sv-control);
|
|
959
970
|
}
|
|
960
971
|
|
|
961
972
|
.pm-status-filter {
|
|
@@ -1151,7 +1162,10 @@ details.pm-bucket[open] > summary.pm-bucket-heading {
|
|
|
1151
1162
|
.pm-mini-review { color: var(--sv-link); background: var(--sv-active); border-color: var(--sv-link); }
|
|
1152
1163
|
.pm-mini-draft { color: var(--sv-muted); background: var(--sv-raised); border-color: var(--sv-border); }
|
|
1153
1164
|
.pm-mini-needsRevision { color: var(--sv-danger); background: var(--sv-dangerbg); border-color: var(--sv-danger); }
|
|
1154
|
-
|
|
1165
|
+
/* Baz 2026-10-04 w-2026-10-04-dave-001: `--sv-control` on `--sv-raised`
|
|
1166
|
+
was 3.51:1 light / 4.21:1 dark, below the 4.5:1 floor for small text.
|
|
1167
|
+
`--sv-muted` is the matching muted-text token and clears AA in both. */
|
|
1168
|
+
.pm-mini-superseded { color: var(--sv-muted); background: var(--sv-raised); border-color: var(--sv-border); }
|
|
1155
1169
|
|
|
1156
1170
|
.pm-bucket-body { padding: 0.75rem 1rem 1rem; }
|
|
1157
1171
|
.pm-bucket-body .pm-tad-root { margin-bottom: 0.65rem; }
|
|
@@ -1225,7 +1239,9 @@ details.pm-bucket[open] > summary.pm-bucket-heading {
|
|
|
1225
1239
|
}
|
|
1226
1240
|
.pm-blueprint-cap-count {
|
|
1227
1241
|
font-weight: 500;
|
|
1228
|
-
|
|
1242
|
+
/* Baz 2026-10-04 w-2026-10-04-dave-001: `--sv-control` fell under
|
|
1243
|
+
the 4.5:1 small-text floor; switched to `--sv-muted`. */
|
|
1244
|
+
color: var(--sv-muted);
|
|
1229
1245
|
margin-left: 0.25rem;
|
|
1230
1246
|
}
|
|
1231
1247
|
.pm-blueprint-cap-body { display: block; }
|
|
@@ -2260,19 +2276,38 @@ details.rcf-row.rcf-row--inner > summary { padding: 0.4rem 0.6rem; }
|
|
|
2260
2276
|
align-items: center;
|
|
2261
2277
|
gap: 0.35rem;
|
|
2262
2278
|
padding: 0.3rem 0.6rem;
|
|
2263
|
-
background: var(--sv-surface
|
|
2264
|
-
color: var(--sv-
|
|
2265
|
-
border: 1px solid var(--sv-border
|
|
2279
|
+
background: var(--sv-surface);
|
|
2280
|
+
color: var(--sv-ink);
|
|
2281
|
+
border: 1px solid var(--sv-border);
|
|
2266
2282
|
border-radius: 999px;
|
|
2267
2283
|
font: inherit;
|
|
2268
2284
|
font-size: 0.85rem;
|
|
2269
2285
|
line-height: 1;
|
|
2270
2286
|
cursor: pointer;
|
|
2271
|
-
transition: background-color 0.1s ease-out, border-color 0.1s ease-out;
|
|
2287
|
+
transition: background-color 0.1s ease-out, border-color 0.1s ease-out, color 0.1s ease-out;
|
|
2288
|
+
}
|
|
2289
|
+
/* Baz 2026-10-04 w-2026-10-04-dave-001: hover previously fell back to
|
|
2290
|
+
a hard-coded light grey (`#f0f3f8`) because `--sv-surface-hover` was
|
|
2291
|
+
never declared; in dark that painted dark ink on a light grey and
|
|
2292
|
+
made the FIND button unreadable. The hover now uses `--sv-active`
|
|
2293
|
+
(the Stravica brand "selected / hovered surface" token, defined in
|
|
2294
|
+
both the `:root` and `:root[data-theme="dark"]` blocks) with the
|
|
2295
|
+
control-strength border so the boundary stays >= 3:1 against the
|
|
2296
|
+
header surface in both themes. The focus-visible outline uses
|
|
2297
|
+
`--sv-focus` (brand focus token) at 3px with 2px offset. */
|
|
2298
|
+
.rcf-search-btn:hover,
|
|
2299
|
+
.rcf-search-btn:focus-visible {
|
|
2300
|
+
background: var(--sv-active);
|
|
2301
|
+
color: var(--sv-ink);
|
|
2302
|
+
border-color: var(--sv-control);
|
|
2303
|
+
}
|
|
2304
|
+
.rcf-search-btn:active {
|
|
2305
|
+
background: var(--sv-active);
|
|
2306
|
+
color: var(--sv-ink);
|
|
2307
|
+
border-color: var(--sv-focus);
|
|
2272
2308
|
}
|
|
2273
|
-
.rcf-search-btn:hover { background: var(--sv-surface-hover, #f0f3f8); }
|
|
2274
2309
|
.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.
|
|
2310
|
+
.rcf-search-btn .rcf-search-icon { flex-shrink: 0; opacity: 0.85; color: var(--sv-ink); }
|
|
2276
2311
|
.rcf-search-btn .rcf-search-kbd {
|
|
2277
2312
|
display: inline-flex;
|
|
2278
2313
|
gap: 0.1rem;
|
|
@@ -2282,9 +2317,10 @@ details.rcf-row.rcf-row--inner > summary { padding: 0.4rem 0.6rem; }
|
|
|
2282
2317
|
}
|
|
2283
2318
|
.rcf-search-btn .rcf-search-kbd kbd {
|
|
2284
2319
|
padding: 0 0.25rem;
|
|
2285
|
-
border: 1px solid var(--sv-border
|
|
2320
|
+
border: 1px solid var(--sv-border);
|
|
2286
2321
|
border-radius: 3px;
|
|
2287
|
-
background: var(--sv-surface
|
|
2322
|
+
background: var(--sv-surface);
|
|
2323
|
+
color: var(--sv-ink);
|
|
2288
2324
|
font-family: inherit;
|
|
2289
2325
|
}
|
|
2290
2326
|
|
|
@@ -2292,6 +2328,57 @@ details.rcf-row.rcf-row--inner > summary { padding: 0.4rem 0.6rem; }
|
|
|
2292
2328
|
.rcf-search-btn .rcf-search-kbd { display: none; }
|
|
2293
2329
|
}
|
|
2294
2330
|
|
|
2331
|
+
/* --------------------------------------------------------------------
|
|
2332
|
+
* Three-state theme control (ADR-4137, Baz 2026-10-04).
|
|
2333
|
+
*
|
|
2334
|
+
* Sits in `.tools` right of the SearchButton in non-embed mode; absent
|
|
2335
|
+
* from the DOM entirely under `data-embed="1"` (page-init.js removes
|
|
2336
|
+
* the element, renderPage({ embed: true }) also skips the markup).
|
|
2337
|
+
* The group buttons use the same ink/active/control tokens as the
|
|
2338
|
+
* SearchButton so hover and focus states stay above WCAG AA in both
|
|
2339
|
+
* themes.
|
|
2340
|
+
* -------------------------------------------------------------------- */
|
|
2341
|
+
|
|
2342
|
+
.rcf-theme {
|
|
2343
|
+
display: inline-flex;
|
|
2344
|
+
align-items: stretch;
|
|
2345
|
+
border: 1px solid var(--sv-border);
|
|
2346
|
+
border-radius: 999px;
|
|
2347
|
+
overflow: hidden;
|
|
2348
|
+
background: var(--sv-surface);
|
|
2349
|
+
}
|
|
2350
|
+
.rcf-theme-btn {
|
|
2351
|
+
appearance: none;
|
|
2352
|
+
background: transparent;
|
|
2353
|
+
color: var(--sv-ink);
|
|
2354
|
+
border: 0;
|
|
2355
|
+
padding: 0.3rem 0.55rem;
|
|
2356
|
+
font: inherit;
|
|
2357
|
+
font-size: 0.75rem;
|
|
2358
|
+
line-height: 1;
|
|
2359
|
+
cursor: pointer;
|
|
2360
|
+
transition: background-color 0.1s ease-out, color 0.1s ease-out;
|
|
2361
|
+
}
|
|
2362
|
+
.rcf-theme-btn + .rcf-theme-btn { border-left: 1px solid var(--sv-border); }
|
|
2363
|
+
.rcf-theme-btn:hover {
|
|
2364
|
+
background: var(--sv-active);
|
|
2365
|
+
color: var(--sv-ink);
|
|
2366
|
+
}
|
|
2367
|
+
.rcf-theme-btn:focus-visible {
|
|
2368
|
+
outline: 3px solid var(--sv-focus);
|
|
2369
|
+
outline-offset: -3px;
|
|
2370
|
+
background: var(--sv-active);
|
|
2371
|
+
}
|
|
2372
|
+
.rcf-theme-btn:active {
|
|
2373
|
+
background: var(--sv-active);
|
|
2374
|
+
color: var(--sv-ink);
|
|
2375
|
+
}
|
|
2376
|
+
.rcf-theme-btn[aria-pressed="true"] {
|
|
2377
|
+
background: var(--sv-button);
|
|
2378
|
+
color: var(--sv-onbutton);
|
|
2379
|
+
}
|
|
2380
|
+
html[data-embed="1"] .rcf-theme { display: none; }
|
|
2381
|
+
|
|
2295
2382
|
.rcf-lookup[hidden] { display: none !important; }
|
|
2296
2383
|
.rcf-lookup {
|
|
2297
2384
|
position: fixed;
|
|
@@ -2346,7 +2433,7 @@ details.rcf-row.rcf-row--inner > summary { padding: 0.4rem 0.6rem; }
|
|
|
2346
2433
|
border-radius: 4px;
|
|
2347
2434
|
cursor: pointer;
|
|
2348
2435
|
}
|
|
2349
|
-
.rcf-lookup-close:hover { color: var(--sv-text); border-color: var(--sv-
|
|
2436
|
+
.rcf-lookup-close:hover { color: var(--sv-text); border-color: var(--sv-control); }
|
|
2350
2437
|
.rcf-lookup-close:focus-visible { outline: 3px solid var(--sv-focus); outline-offset: 1px; }
|
|
2351
2438
|
|
|
2352
2439
|
.rcf-lookup-field { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
@@ -2360,9 +2447,9 @@ details.rcf-row.rcf-row--inner > summary { padding: 0.4rem 0.6rem; }
|
|
|
2360
2447
|
padding: 0.55rem 0.75rem;
|
|
2361
2448
|
font: inherit;
|
|
2362
2449
|
font-size: 0.95rem;
|
|
2363
|
-
border: 1px solid var(--sv-
|
|
2450
|
+
border: 1px solid var(--sv-control);
|
|
2364
2451
|
border-radius: 6px;
|
|
2365
|
-
background: var(--sv-surface
|
|
2452
|
+
background: var(--sv-surface);
|
|
2366
2453
|
color: var(--sv-text);
|
|
2367
2454
|
}
|
|
2368
2455
|
.rcf-lookup-input:focus-visible { outline: 3px solid var(--sv-focus); outline-offset: 1px; border-color: var(--sv-link, #3a5bb5); }
|
|
@@ -2389,9 +2476,9 @@ details.rcf-row.rcf-row--inner > summary { padding: 0.4rem 0.6rem; }
|
|
|
2389
2476
|
cursor: pointer;
|
|
2390
2477
|
border: 1px solid transparent;
|
|
2391
2478
|
}
|
|
2392
|
-
.rcf-lookup-row:hover { background: var(--sv-
|
|
2479
|
+
.rcf-lookup-row:hover { background: var(--sv-active); }
|
|
2393
2480
|
.rcf-lookup-row.is-active {
|
|
2394
|
-
background: var(--sv-
|
|
2481
|
+
background: var(--sv-active);
|
|
2395
2482
|
border-color: var(--sv-link, #3a5bb5);
|
|
2396
2483
|
}
|
|
2397
2484
|
.rcf-lookup-row mark {
|
|
@@ -2467,7 +2554,7 @@ details.rcf-row.rcf-row--inner > summary { padding: 0.4rem 0.6rem; }
|
|
|
2467
2554
|
font: inherit;
|
|
2468
2555
|
font-size: 0.85rem;
|
|
2469
2556
|
}
|
|
2470
|
-
.rcf-lookup-reload:hover { background: var(--sv-
|
|
2557
|
+
.rcf-lookup-reload:hover { background: var(--sv-active); }
|
|
2471
2558
|
.rcf-lookup-reload:focus-visible { outline: 3px solid var(--sv-focus); outline-offset: 1px; }
|
|
2472
2559
|
|
|
2473
2560
|
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
|
+
}
|