T7.2 - F6/D3: the form gets structure - a section rail, one section open
F6 as amended by D3 (Aug 18): one page, persistent side navigation, sections collapsible, only the current one open by default, plus Expand all. Tabs were rejected in D3 because they hide sections a first-time author does not know exist. What changed: - The jump-chip strip (#section-nav, span onclick) is gone. In its place a <nav> section rail of real <button> entries, aria-current on the current section, 44px tap targets, above the form at 390px and beside it at 1440px. - Every section heading is now a disclosure <button> with aria-expanded and aria-controls. One section open at rest; Expand all (aria-pressed) opens everything and is remembered per browser. - Sections are URL-addressable (?section=, T4.2 machinery) and a deep link to a collapsed section expands it. Positional-id fallback removed: a card without an id gets a console.error and no rail entry, never an invented sec-N id that would ride into shareable URLs and move between visits. - General Information (1,288px on its own) split into #general-card and #assign-card (Assignment & Schedule). The split is presentational: both cards are the ONE CR-006 section `general` (WP_SECTION_NODES lists both), so wp-sections.js and the SOP wizard are untouched. CR-001's adjacency (P6 activity beside due date) is preserved and asserted. - gotoSection() flushes autosave, which the deleted chips used to do. - secMakeToggle() preserves every element child of a heading - help tips go outside the button, everything else inside the label. The first version cleared textContent and destroyed #saved-count, which killed boot one line short of wpCreatorReady with the page still visibly rendered. - BL-013 folded in per the task: the T3.4 focus ring on the rebuilt form. frame_check reports outline solid 2px on creator inputs. Height, measured not asserted: 5,399px before; 1,995px at rest at 1440x900. DONE-WHEN NOT FULLY MET - stated per CLAUDE.md rather than marked complete: "no single view exceeds roughly two screen heights at rest" reads 2.22 screens (1995/900). The remaining gap is page chrome this wave reworks: .ctx-bar (67px, T7.4) and .release-banner (45px, T7.5). The criterion was already amended once (D3, "at rest") and is not being moved again to fit; tests/form_structure_check.py keeps the check red and it is re-measured at the end of wave 7. Every other done-when entry passes. Backlog: BL-001's cause corrected a third time - at rest the overflow is help.js's .help-tip::after tooltip (481 vs 390), the S8 component T9.5 rebuilds; the tables still overflow only when expanded. Deliberately not fixed here - a fix would be thrown away with the component at T9.5. Verification (each probe run alone): form_structure_check 50/51 (the height check above), sections_check 95/95, generalinfo_check 49/49, frame_check 39/39 regression pass. Items: F6, D3, BL-013, BL-001 (re-measured) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1398,70 +1398,288 @@ function syncToolTabs(){
|
||||
|
||||
// Sticky save bar + section-nav chrome (shown only on the editable form view).
|
||||
function setFormChrome(on){
|
||||
const nav=document.getElementById('section-nav'), save=document.getElementById('sticky-save');
|
||||
if(nav) nav.style.display = on ? '' : 'none';
|
||||
const rail=document.getElementById('section-rail'), save=document.getElementById('sticky-save');
|
||||
if(rail) rail.style.display = on ? '' : 'none';
|
||||
if(save) save.style.display = on ? 'flex' : 'none';
|
||||
document.body.classList.toggle('has-sticky-save', !!on);
|
||||
if(on){ buildSectionNav(); updateStickyStatus(); makeCollapsible(); initSectionNavAutoHide(); }
|
||||
document.body.classList.toggle('has-sec-rail', !!on);
|
||||
if(on){ initSectionNavAutoHide(); buildSectionRail(); updateStickyStatus(); }
|
||||
else positionSectionNav();
|
||||
}
|
||||
// Make each form card collapsible by clicking its heading (idempotent).
|
||||
function makeCollapsible(){
|
||||
document.querySelectorAll('.main > .card').forEach(card=>{
|
||||
if(card.id==='saved-card') return;
|
||||
const head=card.querySelector('.section-header, .sub-heading');
|
||||
if(!head || head.dataset.collapsible) return;
|
||||
head.dataset.collapsible='1';
|
||||
head.style.cursor='pointer';
|
||||
const chev=document.createElement('span'); chev.className='collapse-chev'; chev.textContent='▾';
|
||||
head.insertBefore(chev, head.firstChild);
|
||||
head.addEventListener('click', e=>{
|
||||
if(['INPUT','SELECT','TEXTAREA','BUTTON','A'].includes(e.target.tagName) || e.target.classList.contains('help-tip')) return;
|
||||
const collapsed=card.classList.toggle('collapsed');
|
||||
chev.textContent = collapsed ? '▸' : '▾';
|
||||
// ── SECTION STRUCTURE (F6 / D3) ───────────────────────────────────────────────
|
||||
// F6 measured this form at 11 cards in one 5,399px scroll, with a strip of jump
|
||||
// chips standing in for structure. D3 settled the shape, and it is not tabs:
|
||||
// one page, navigation down the side, sections collapsible, only the current one
|
||||
// open, plus an `Expand all` for people who would rather scroll straight through.
|
||||
//
|
||||
// Tabs were rejected because they hide sections a first-time author does not know
|
||||
// exist. The uncollapsed long form was rejected because it is the page F6 exists
|
||||
// to fix. So the height criterion became "at rest" - the state the page loads in -
|
||||
// which is what D3 amended it to, in writing, rather than quietly failing the old
|
||||
// one.
|
||||
//
|
||||
// Two accessibility defects come out with the old strip, both named in CLAUDE.md:
|
||||
// - the jump chips were `<span onclick>`; there were two left in the app
|
||||
// - collapsing was a click listener on a heading `<div>`, so it was mouse-only
|
||||
// Every control here is a real button. The rail is built FROM THE CARDS, so a
|
||||
// section added later, or suppressed by a CR-006 toggle, changes the rail without
|
||||
// anyone remembering to maintain a second list.
|
||||
|
||||
const SEC_EXPAND_KEY = 'wp_iwp_expand_all';
|
||||
let secExpandAll = false;
|
||||
let secCurrent = '';
|
||||
let _secSpyBound = false;
|
||||
|
||||
// The cards that are sections: rendered, and carrying a heading to name them.
|
||||
// `hidden` and `display:none` are how CR-006 and applyKindVisibility() suppress
|
||||
// one, and a table of contents has to lose exactly what the form lost.
|
||||
function secCards(){
|
||||
return [...document.querySelectorAll('.main > .card')].filter(card => {
|
||||
// `Saved work packages` is a list of OTHER packages, sitting at the bottom of
|
||||
// the form for editing THIS one - and the navigator panel already lists exactly
|
||||
// the same thing. It is not a section of the package, so it gets no rail entry.
|
||||
// It is still collapsed by default (secApplyOpenState), because 310px of a
|
||||
// duplicate list is not what the page should open on.
|
||||
if(card.id === 'saved-card') return false;
|
||||
if(card.hidden || card.style.display === 'none') return false;
|
||||
return !!card.querySelector('.section-title, .sub-heading');
|
||||
});
|
||||
}
|
||||
|
||||
function secLabel(card){
|
||||
const h = card.querySelector('.section-title, .sub-heading');
|
||||
if(!h) return '';
|
||||
const clone = h.cloneNode(true);
|
||||
clone.querySelectorAll('.help-tip, .auto-tag, .collapse-chev').forEach(x => x.remove());
|
||||
return clone.textContent.trim().replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
// Everything below the heading, wrapped once so `aria-controls` has a real target
|
||||
// and collapsing is `hidden` on one element rather than a `:not()` rule listing
|
||||
// the heading classes. The old CSS did the latter, which meant the disclosure
|
||||
// state lived nowhere a screen reader could read it.
|
||||
function secBody(card){
|
||||
let body = card.querySelector(':scope > .card-body');
|
||||
if(body) return body;
|
||||
const head = card.querySelector(':scope > .section-header, :scope > .sub-heading');
|
||||
if(!head) return null;
|
||||
body = document.createElement('div');
|
||||
body.className = 'card-body';
|
||||
body.id = (card.id || 'sec') + '-body';
|
||||
const rest = [];
|
||||
for(let n = head.nextSibling; n; n = n.nextSibling) rest.push(n);
|
||||
rest.forEach(n => body.appendChild(n));
|
||||
card.appendChild(body);
|
||||
return body;
|
||||
}
|
||||
|
||||
// Turn the heading into a disclosure button, in place and once. The label goes
|
||||
// inside the button; the help tip and the description stay OUTSIDE it, because a
|
||||
// tooltip trigger nested in a button is two controls in one hit area.
|
||||
function secMakeToggle(card){
|
||||
if(card.dataset.secReady) return;
|
||||
const body = secBody(card);
|
||||
if(!body) return;
|
||||
const title = card.querySelector(':scope > .section-header > .section-title')
|
||||
|| card.querySelector(':scope > .sub-heading');
|
||||
if(!title) return;
|
||||
|
||||
const label = secLabel(card);
|
||||
// Every ELEMENT in the heading has to survive, not only the text. `saved-card`'s
|
||||
// heading carries <span id="saved-count">, which renderSavedList() writes to on
|
||||
// every save - clearing the heading destroyed it, renderSavedList() then threw on
|
||||
// a null, and boot stopped one line short of setting wpCreatorReady. The page
|
||||
// still rendered, so it looked like a slow load rather than a crash.
|
||||
//
|
||||
// Help tips go OUTSIDE the button: a tooltip trigger nested inside a button is
|
||||
// two controls sharing one hit area. Everything else goes inside, because it is
|
||||
// part of the heading's own text - a count, a badge, a tag.
|
||||
const kids = [...title.children];
|
||||
const tips = kids.filter(el => el.classList.contains('help-tip'));
|
||||
const inside = kids.filter(el => !el.classList.contains('help-tip'));
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'card-toggle';
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
btn.setAttribute('aria-controls', body.id);
|
||||
btn.innerHTML = '<span class="collapse-chev" aria-hidden="true"></span>'
|
||||
+ '<span class="card-toggle-label"></span>';
|
||||
const labelEl = btn.querySelector('.card-toggle-label');
|
||||
labelEl.textContent = label;
|
||||
inside.forEach(el => labelEl.appendChild(el));
|
||||
title.textContent = '';
|
||||
title.appendChild(btn);
|
||||
tips.forEach(t => title.appendChild(t));
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
const open = card.classList.contains('collapsed');
|
||||
secSetOpen(card, open);
|
||||
// Opening a section by its own header makes it the one you are in, which the
|
||||
// rail has to agree with - otherwise the rail marks a section you left.
|
||||
if(open){ secCurrent = card.id; secMarkRail(); secSyncUrl(); }
|
||||
});
|
||||
card.dataset.secReady = '1';
|
||||
}
|
||||
|
||||
function secSetOpen(card, open){
|
||||
const btn = card.querySelector(':scope .card-toggle');
|
||||
const body = card.querySelector(':scope > .card-body');
|
||||
card.classList.toggle('collapsed', !open);
|
||||
if(btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
if(body) body.hidden = !open;
|
||||
}
|
||||
|
||||
function secIsOpen(card){ return !card.classList.contains('collapsed'); }
|
||||
|
||||
// ── the rail ─────────────────────────────────────────────────────────────────
|
||||
function buildSectionRail(){
|
||||
const rail = document.getElementById('section-rail');
|
||||
const list = document.getElementById('sec-rail-list');
|
||||
if(!rail || !list) return;
|
||||
let cards = secCards();
|
||||
// Every section carries a real id in the markup. A positional fallback was here
|
||||
// and it was a bug waiting to be shipped: the id it invents goes into ?section=
|
||||
// as a shareable address, and the INDEX moves whenever the set of visible
|
||||
// sections changes - a CR-006 toggle, the BIM flag, or a card being inserted
|
||||
// ahead of it. A link somebody sent then opens a different section, silently and
|
||||
// with no error. Name the card in the markup instead; complain loudly if not.
|
||||
cards.forEach(card => {
|
||||
if(!card.id){
|
||||
// Not a toast: this is a defect in the page, not something a user can act on.
|
||||
try { console.error('T7.2: a form section has no id; its rail entry cannot be'
|
||||
+ ' addressed. Give it one in wp-creation-index.html.', card); } catch(e){}
|
||||
return;
|
||||
}
|
||||
secMakeToggle(card);
|
||||
});
|
||||
cards = cards.filter(c => c.id);
|
||||
|
||||
list.innerHTML = cards.map(card =>
|
||||
'<li><button type="button" class="sec-rail-item" data-sec="' + esc(card.id) + '">'
|
||||
+ esc(secLabel(card)) + '</button></li>').join('');
|
||||
list.querySelectorAll('.sec-rail-item').forEach(b => {
|
||||
b.addEventListener('click', () => gotoSection(b.dataset.sec));
|
||||
});
|
||||
rail.hidden = cards.length < 2;
|
||||
|
||||
// Whatever the URL asks for, else the first section. Never "all of them", which
|
||||
// is the state F6 is about.
|
||||
const want = (typeof WPUrl !== 'undefined' && WPUrl.get && WPUrl.get('section')) || '';
|
||||
const target = cards.some(c => c.id === want) ? want : (cards[0] && cards[0].id) || '';
|
||||
secApplyOpenState(target);
|
||||
secBindSpy();
|
||||
}
|
||||
|
||||
// One section open, or all of them. Called on every render, so it is also what
|
||||
// keeps a suppressed section from being left expanded behind the scenes.
|
||||
function secApplyOpenState(currentId){
|
||||
const cards = secCards();
|
||||
secCurrent = currentId || secCurrent || (cards[0] && cards[0].id) || '';
|
||||
cards.forEach(card => secSetOpen(card, secExpandAll || card.id === secCurrent));
|
||||
// The saved-package list is not in `cards` - it is not a section - but it is on
|
||||
// the page and it is 310px tall, so it collapses like everything else. It gets
|
||||
// its disclosure button here because the rail loop no longer reaches it.
|
||||
const saved = document.getElementById('saved-card');
|
||||
if(saved && saved.style.display !== 'none'){
|
||||
secMakeToggle(saved);
|
||||
secSetOpen(saved, secExpandAll);
|
||||
}
|
||||
secMarkRail();
|
||||
}
|
||||
|
||||
function secMarkRail(){
|
||||
document.querySelectorAll('.sec-rail-item').forEach(b => {
|
||||
const on = b.dataset.sec === secCurrent;
|
||||
b.classList.toggle('is-current', on);
|
||||
// aria-current="true", not "page": these are places in a page, not pages.
|
||||
if(on) b.setAttribute('aria-current', 'true');
|
||||
else b.removeAttribute('aria-current');
|
||||
});
|
||||
}
|
||||
|
||||
function secSyncUrl(opts){
|
||||
if(typeof WPUrl === 'undefined') return;
|
||||
const patch = { section: secCurrent || '' };
|
||||
(opts && opts.replace ? WPUrl.replace : WPUrl.push).call(WPUrl, patch);
|
||||
}
|
||||
|
||||
// Open a section and go to it. Focus moves to the section's own heading button,
|
||||
// not just the scroll position: a keyboard user who activates a rail item has to
|
||||
// land somewhere, and landing nowhere is why jump links are not navigation.
|
||||
function gotoSection(id, opts){
|
||||
const card = document.getElementById(id);
|
||||
if(!card) return false;
|
||||
// Moving section is a "you have visibly moved on" moment, so flush the draft
|
||||
// rather than wait out the debounce. This was bound to the old jump chips at
|
||||
// initAutosave() time, which only worked because the chips were built before it
|
||||
// ran; it belongs here, where every section change goes through one door -
|
||||
// rail click, keyboard, deep link and Back alike.
|
||||
if(typeof WPAutosave !== 'undefined' && WPAutosave.flush) WPAutosave.flush('section');
|
||||
secApplyOpenState(id);
|
||||
const btn = card.querySelector('.card-toggle');
|
||||
if(btn){
|
||||
btn.focus({preventScroll:true});
|
||||
card.scrollIntoView({behavior:'smooth', block:'start'});
|
||||
}
|
||||
if(!(opts && opts.fromUrl)) secSyncUrl();
|
||||
return true;
|
||||
}
|
||||
|
||||
function toggleExpandAll(){
|
||||
setExpandAll(!secExpandAll);
|
||||
}
|
||||
|
||||
function setExpandAll(on){
|
||||
secExpandAll = !!on;
|
||||
try { localStorage.setItem(SEC_EXPAND_KEY, secExpandAll ? '1' : '0'); } catch(e){}
|
||||
const btn = document.getElementById('sec-expand-all');
|
||||
if(btn){
|
||||
btn.setAttribute('aria-pressed', secExpandAll ? 'true' : 'false');
|
||||
btn.textContent = secExpandAll ? 'Collapse all' : 'Expand all';
|
||||
}
|
||||
secApplyOpenState(secCurrent);
|
||||
track(secExpandAll ? 'sections_expand_all' : 'sections_collapse_all');
|
||||
}
|
||||
|
||||
// With one section open the current one is whatever you opened. With Expand all
|
||||
// on, that answer is wrong the moment you scroll, so the rail follows the form.
|
||||
function secBindSpy(){
|
||||
if(_secSpyBound) return;
|
||||
_secSpyBound = true;
|
||||
window.addEventListener('scroll', () => {
|
||||
if(!secExpandAll) return;
|
||||
const rail = document.getElementById('section-rail');
|
||||
if(!rail || rail.hidden) return;
|
||||
const line = (document.querySelector('.header') || {offsetHeight:0}).offsetHeight + 80;
|
||||
let seen = '';
|
||||
secCards().forEach(card => {
|
||||
if(card.getBoundingClientRect().top <= line) seen = card.id;
|
||||
});
|
||||
});
|
||||
}
|
||||
function buildSectionNav(){
|
||||
const nav=document.getElementById('section-nav'); if(!nav) return;
|
||||
const chips=[];
|
||||
document.querySelectorAll('.main > .card').forEach((card,i)=>{
|
||||
// `hidden` is how CR-006 suppresses a section; the chip strip is a table of
|
||||
// contents for the form, so it has to lose the same entries.
|
||||
if(card.id==='saved-card' || card.style.display==='none' || card.hidden) return;
|
||||
const h=card.querySelector('.section-title, .sub-heading'); if(!h) return;
|
||||
const clone=h.cloneNode(true); clone.querySelectorAll('.help-tip').forEach(x=>x.remove());
|
||||
const label=clone.textContent.trim().replace(/\s+/g,' '); if(!label) return;
|
||||
if(!card.id) card.id='sec-'+i;
|
||||
chips.push(`<span class="sec-chip" onclick="document.getElementById('${card.id}').scrollIntoView({behavior:'smooth',block:'start'})">${esc(label)}</span>`);
|
||||
});
|
||||
nav.innerHTML=chips.join('');
|
||||
}
|
||||
// Keep the section-nav pinned just below the sticky header (so it stays put while
|
||||
// scrolling instead of hiding behind the header), and let it slide out of the way
|
||||
// while reading (scroll down), snapping back the moment you scroll up.
|
||||
function positionSectionNav(){
|
||||
const nav=document.getElementById('section-nav'), hdr=document.querySelector('.header');
|
||||
if(nav && hdr) nav.style.top = hdr.offsetHeight + 'px';
|
||||
// The navigator drawer + its handle hang below the page header. Deliberately NOT
|
||||
// including the section-nav height: that bar is sticky, so at scroll 0 it sits
|
||||
// further down the page and the handle would float over the chrome.
|
||||
document.documentElement.style.setProperty('--rail-top', (hdr?hdr.offsetHeight:0)+'px');
|
||||
}
|
||||
let _snLastY=0, _snBound=false;
|
||||
function initSectionNavAutoHide(){
|
||||
positionSectionNav();
|
||||
if(_snBound) return; _snBound=true;
|
||||
window.addEventListener('resize', positionSectionNav, {passive:true});
|
||||
window.addEventListener('scroll', ()=>{
|
||||
const nav=document.getElementById('section-nav');
|
||||
if(!nav || nav.style.display==='none') return;
|
||||
const y=window.scrollY||document.documentElement.scrollTop||0;
|
||||
if(y>_snLastY+4 && y>140) nav.classList.add('nav-hidden'); // scrolling down
|
||||
else if(y<_snLastY-4) nav.classList.remove('nav-hidden'); // scrolling up
|
||||
_snLastY=y;
|
||||
if(seen && seen !== secCurrent){ secCurrent = seen; secMarkRail(); }
|
||||
}, {passive:true});
|
||||
}
|
||||
|
||||
// The drawer and the comments panel hang below the page chrome, and the chrome is
|
||||
// taller since T7.1 - app bar, tab strip, toolbar. Measure it rather than assume.
|
||||
function positionSectionNav(){
|
||||
const parts = ['.header', '.main-nav', '.wp-toolbar']
|
||||
.map(sel => document.querySelector(sel))
|
||||
.filter(Boolean);
|
||||
const h = parts.reduce((n, el) => n + el.offsetHeight, 0);
|
||||
document.documentElement.style.setProperty('--rail-top', h + 'px');
|
||||
const rail = document.getElementById('section-rail');
|
||||
if(rail) rail.style.setProperty('--sec-rail-top', h + 'px');
|
||||
}
|
||||
|
||||
let _secChromeBound = false;
|
||||
function initSectionNavAutoHide(){
|
||||
positionSectionNav();
|
||||
if(_secChromeBound) return;
|
||||
_secChromeBound = true;
|
||||
window.addEventListener('resize', positionSectionNav, {passive:true});
|
||||
try { setExpandAll(localStorage.getItem(SEC_EXPAND_KEY) === '1'); } catch(e){}
|
||||
}
|
||||
|
||||
function updateStickyStatus(){
|
||||
const el=document.getElementById('sticky-status'); if(!el) return;
|
||||
const r=readiness(); const st=getRadio('status');
|
||||
@@ -1621,7 +1839,10 @@ function wpLocationText(p){
|
||||
// of its own; CR-004 gives it structured fields in wave 6 and only this line
|
||||
// changes then.
|
||||
const WP_SECTION_NODES = {
|
||||
general: ['#general-card'],
|
||||
// Two cards, one section. T7.2 split General Information because it was 1,288px
|
||||
// on its own; the split is presentational and CR-006 still governs both through
|
||||
// the single `general` id, so wp-sections.js and the SOP wizard are untouched.
|
||||
general: ['#general-card', '#assign-card'],
|
||||
location: ['#location-card'],
|
||||
scope: ['#scope-card'],
|
||||
assets: ['#asset-card'],
|
||||
@@ -1682,7 +1903,7 @@ function applySopSections(sections, fields){
|
||||
});
|
||||
// The section chips are a table of contents for the form, so they have to lose
|
||||
// the same entries.
|
||||
if(typeof buildSectionNav === 'function') buildSectionNav();
|
||||
if(typeof buildSectionRail === 'function') buildSectionRail();
|
||||
// A package already on screen is re-rendered, or the view keeps showing a
|
||||
// section the form no longer has.
|
||||
if(currentView === 'Package View' && _lastRenderedPkg) renderPackage(_lastRenderedPkg);
|
||||
@@ -1750,9 +1971,11 @@ const WP_NAV_CRITICAL_CSS = `
|
||||
/* Modals are hidden by a stylesheet rule; without it their contents render inline
|
||||
in the middle of the form. Same reasoning as the panel: this is a floor. */
|
||||
.modal-overlay:not(.open),.cmt-overlay:not(.open){display:none!important;}
|
||||
/* The jump bar's sticky offset is set inline by JS; give it a sane default so a
|
||||
stale stylesheet can't park it behind the opaque header. */
|
||||
.section-nav-bar{position:sticky;top:48px;z-index:30;background:#fff;}
|
||||
/* F6/D3: the rail must not paint over the form before the real sheet lands,
|
||||
and a section must never be stuck collapsed if that sheet never arrives.
|
||||
Same reasoning as the panel above: this is a floor, not an override. */
|
||||
.sec-rail[hidden]{display:none;}
|
||||
.card.collapsed > .card-body[hidden]{display:none;}
|
||||
`;
|
||||
|
||||
function injectWpNavCriticalCss(){
|
||||
@@ -2757,9 +2980,6 @@ function initAutosave(){
|
||||
// the truth about. The bar is rendered by the app, so mount when it exists.
|
||||
const mountHost = document.querySelector('#sticky-save .sticky-status') || document.querySelector('#sticky-save');
|
||||
if(mountHost) WPAutosave.mountIndicator(mountHost);
|
||||
// Section changes are a "you have visibly moved on" moment, so flush rather than
|
||||
// wait out the debounce.
|
||||
document.querySelectorAll('.sec-chip').forEach(c=>c.addEventListener('click', ()=>WPAutosave.flush('section')));
|
||||
offerDraftRecovery();
|
||||
}
|
||||
|
||||
@@ -2935,6 +3155,12 @@ function bootData(){
|
||||
document.querySelectorAll('.main > .card, .main > .nav-row').forEach(e=>e.style.display='');
|
||||
syncToolTabs();
|
||||
}
|
||||
// F6/D3: which section you are in is state, so Back moves between sections
|
||||
// as well as between packages. fromUrl, because the URL already says this -
|
||||
// recording it again would make the first Back appear to do nothing.
|
||||
if(state.section && currentView !== 'Dashboard'){
|
||||
gotoSection(state.section, {fromUrl:true});
|
||||
}
|
||||
});
|
||||
}
|
||||
initAutosave();
|
||||
|
||||
@@ -99,8 +99,10 @@
|
||||
<!-- RELEASE READINESS BANNER -->
|
||||
<div class="release-banner" id="release-banner"></div>
|
||||
|
||||
<!-- SECTION NAV (jump links, built from the form cards) -->
|
||||
<div class="section-nav-bar" id="section-nav"></div>
|
||||
<!-- F6/D3: the jump-link strip stood here. It was a row of `<span onclick>` chips
|
||||
that scrolled you somewhere inside a 5,399px page and then told you nothing
|
||||
about where you had landed. The replacement is a real rail, declared below the
|
||||
form so it can be a sticky column beside it at desk width. -->
|
||||
|
||||
<div class="wp-layout">
|
||||
|
||||
@@ -225,7 +227,29 @@
|
||||
WP_FIELD_NODES addresses. -->
|
||||
<div class="field" id="field-costCode"><label>Cost code</label><select id="wp_cost"></select><div class="field-hint sop-hint">Acumatica cost codes</div></div>
|
||||
<div class="field" id="field-acumaticaTask"><label>Acumatica task</label><input type="text" id="wp_wbs" placeholder="Acumatica task no."></div>
|
||||
<!-- Moved up from the second grid by T7.2. The specification section is
|
||||
classification, not assignment: it is read-only and filled from the WP
|
||||
type on the SOP, so it belongs beside Subject and Type. -->
|
||||
<div class="field"><label>Specification section</label>
|
||||
<input type="text" id="wp_spec" readonly class="locked-field" placeholder="set on the WP type in the SOP">
|
||||
<div class="field-hint" id="spec-folder-link"></div></div>
|
||||
</div>
|
||||
<div class="field field-grid col1"><div class="field"><label>Description</label><textarea id="wp_desc" rows="2" placeholder="Short summary of the package"></textarea></div></div>
|
||||
<div class="field field-grid col1" id="bimlink-wrap"><div class="field"><label>Enabled by — BIM package(s)<span class="help-tip" data-tip="Advanced Work Packaging traceability: link the BIM / model package(s) that enabled this install package. Paste the MWP number(s) or a link to the model package.">i</span></label><input type="text" id="wp_bimlink" placeholder="e.g. MWP07-FAB-CONDUITS, or a link to the model package"></div></div>
|
||||
</div>
|
||||
|
||||
<!-- ASSIGNMENT & SCHEDULE (F6 / T7.2)
|
||||
Split out of General Information, which was 1,288px on its own and the
|
||||
whole of the gap between the page at rest and F6's two-screen bar. The
|
||||
split is presentational: BOTH cards are the CR-006 section `general`
|
||||
(WP_SECTION_NODES.general lists both), so the shared section registry in
|
||||
wp-sections.js is untouched, the SOP wizard still shows one toggle, and
|
||||
turning General Information off still hides every field it hid before.
|
||||
|
||||
CR-001 requires the P6 activity to sit beside the due date. Both are here,
|
||||
adjacent, and tests/generalinfo_check.py asserts the adjacency. -->
|
||||
<div class="card" id="assign-card">
|
||||
<div class="sub-heading">Assignment & Schedule</div>
|
||||
<div class="field-grid">
|
||||
<div class="field"><label>Owner <span class="help-tip" data-tip="The accountable owner (a user account on this project). Assigning notifies them by email if email notifications are enabled in the admin console.">i</span></label><select id="wp_assignee"><option value="">— Unassigned —</option></select></div>
|
||||
<div class="field"><label>Assignees<span class="help-tip" data-tip="The crew and staff working this package. Pick from the project team named on the SOP; anyone without a user account can still be added by name.">i</span></label>
|
||||
@@ -254,12 +278,7 @@
|
||||
<input type="text" id="wp_p6_id" placeholder="e.g. A1234"></div>
|
||||
<div class="field"><label>P6 activity description</label>
|
||||
<input type="text" id="wp_p6_desc" placeholder="what that activity covers"></div>
|
||||
<div class="field"><label>Specification section</label>
|
||||
<input type="text" id="wp_spec" readonly class="locked-field" placeholder="set on the WP type in the SOP">
|
||||
<div class="field-hint" id="spec-folder-link"></div></div>
|
||||
</div>
|
||||
<div class="field field-grid col1"><div class="field"><label>Description</label><textarea id="wp_desc" rows="2" placeholder="Short summary of the package"></textarea></div></div>
|
||||
<div class="field field-grid col1" id="bimlink-wrap"><div class="field"><label>Enabled by — BIM package(s)<span class="help-tip" data-tip="Advanced Work Packaging traceability: link the BIM / model package(s) that enabled this install package. Paste the MWP number(s) or a link to the model package.">i</span></label><input type="text" id="wp_bimlink" placeholder="e.g. MWP07-FAB-CONDUITS, or a link to the model package"></div></div>
|
||||
</div>
|
||||
|
||||
<!-- BIM / MODEL DETAILS (shown for BIM/VDC SOPs) -->
|
||||
@@ -380,7 +399,12 @@
|
||||
</div>
|
||||
|
||||
<!-- SIGN-OFFS -->
|
||||
<div class="card">
|
||||
<!-- The only .main > .card that had no id. buildSectionRail() filled the gap
|
||||
positionally - `card.id = "sec-" + i` - and that id then rode into ?section=
|
||||
as a shareable address. It moved every time the set of VISIBLE sections
|
||||
changed: a CR-006 toggle, the BIM flag, or T7.2 inserting #assign-card ahead
|
||||
of it. A link somebody sent then opened a different section, silently. -->
|
||||
<div class="card" id="signoff-card">
|
||||
<div class="sub-heading">Approvals & Sign-offs</div>
|
||||
<div class="notice">Per the AWP IWP checklist. A package should be signed by these roles before release.</div>
|
||||
<div class="table-wrap"><table><thead><tr><th style="width:220px">Role</th><th>Name</th><th style="width:150px">Date</th><th style="width:80px;text-align:center">Signed</th></tr></thead><tbody id="signoff-body"></tbody></table></div>
|
||||
@@ -438,6 +462,25 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SECTION RAIL (F6 / D3)
|
||||
Built by buildSectionRail() from the cards themselves, so a section added or
|
||||
suppressed by a CR-006 toggle changes the rail without anybody maintaining a
|
||||
second list. Every entry is a real <button>, the current one carries
|
||||
aria-current, and each card's heading became a disclosure button with
|
||||
aria-expanded - the two accessibility defects F6's old chip strip and
|
||||
makeCollapsible() were carrying.
|
||||
|
||||
A sticky column beside the form at 1200px and up; a horizontal strip above it
|
||||
below that, which is what fits at 390px. -->
|
||||
<nav class="sec-rail" id="section-rail" aria-label="Form sections" hidden>
|
||||
<div class="sec-rail-head">
|
||||
<span class="sec-rail-title">Sections</span>
|
||||
<button type="button" class="sec-rail-all" id="sec-expand-all" aria-pressed="false"
|
||||
onclick="toggleExpandAll()">Expand all</button>
|
||||
</div>
|
||||
<ul class="sec-rail-list" id="sec-rail-list"></ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- HOLD LOG MODAL (comment 7) -->
|
||||
|
||||
@@ -149,7 +149,10 @@
|
||||
auto-hiding overlay drawer (see below) rather than a column, so it never takes
|
||||
width away from the form — which matters most when this page is embedded in the
|
||||
suite's tab and every pixel is shared with the app chrome. */
|
||||
.wp-layout { display: block; width: 100%; margin: 0; }
|
||||
/* F6/D3: flex, so the section rail declared AFTER the form in the markup can sit
|
||||
ABOVE it at narrow widths (order:-1) and BESIDE it at desk width. Declaring it
|
||||
after .main is what lets it be a sticky column without wrapping the layout. */
|
||||
.wp-layout { display: flex; flex-direction: column; width: 100%; margin: 0; }
|
||||
.main { min-width: 0; max-width: none; margin: 0;
|
||||
padding: 22px 28px 72px calc(var(--nav-w,288px) + 28px);
|
||||
transition: padding-left .18s ease; }
|
||||
@@ -659,21 +662,132 @@
|
||||
.so-date { font-size:13px; font-variant-numeric:tabular-nums; }
|
||||
.so-ovr { margin-left:8px; font-size:11px; }
|
||||
|
||||
/* Collapsible form sections */
|
||||
.collapse-chev { display:inline-block; width:1em; margin-right:7px; color:var(--text-muted); font-size:11px; user-select:none; }
|
||||
.card.collapsed > :not(.section-header):not(.sub-heading) { display:none !important; }
|
||||
.card.collapsed .section-desc { display:none; }
|
||||
/* ── COLLAPSIBLE SECTIONS + SECTION RAIL (F6 / D3) ─────────────────────────
|
||||
What stood here: a `.section-nav-bar` of `<span onclick>` jump chips, and a
|
||||
`.card.collapsed > :not(.section-header):not(.sub-heading)` rule that hid a
|
||||
card's contents without any element carrying the disclosure state. Neither
|
||||
was reachable by keyboard, and neither said anything to a screen reader.
|
||||
|
||||
/* Section nav (jump chips) */
|
||||
.section-nav-bar{ position:sticky; top:0; z-index:30; display:flex; flex-wrap:wrap; gap:6px;
|
||||
padding:8px 12px 8px calc(var(--nav-w,288px) + 28px); background:var(--wp-scrim-frosted); backdrop-filter:blur(4px);
|
||||
border-bottom:1px solid var(--border); box-shadow:var(--wp-shadow-navbar);
|
||||
transition:transform .22s ease; }
|
||||
.section-nav-bar:empty{ display:none; }
|
||||
.section-nav-bar.nav-hidden{ transform:translateY(-160%); }
|
||||
.sec-chip{ font-size:12px; font-weight:600; color:var(--text-muted); background:var(--surface2);
|
||||
border:1px solid var(--border); border-radius:14px; padding:4px 11px; cursor:pointer; white-space:nowrap; }
|
||||
.sec-chip:hover{ border-color:var(--accent); color:var(--accent); }
|
||||
Now: every heading is a real disclosure button, its contents are one
|
||||
`.card-body` so `aria-controls` has a target, and the table of contents is a
|
||||
rail of real buttons that marks where you are.
|
||||
|
||||
D3 amended F6's height criterion to "at rest". At rest one section is open,
|
||||
which is what keeps this page under two screen heights; `Expand all` is a
|
||||
deliberate choice to exceed it. */
|
||||
|
||||
/* The disclosure button lives INSIDE the existing heading, so .sub-heading's
|
||||
trailing rule and the help tip beside it keep working untouched. */
|
||||
.card-toggle {
|
||||
display: inline-flex; align-items: center; gap: 9px;
|
||||
background: none; border: 0; padding: 0; margin: 0;
|
||||
font: inherit; color: inherit; text-align: left; cursor: pointer;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.card-toggle:hover { color: var(--accent); }
|
||||
.card-toggle-label { min-width: 0; }
|
||||
|
||||
/* Drawn, not typed. A glyph here would be a fourth icon idiom on a page S6 is
|
||||
already going to have to reconcile, and it would render differently per
|
||||
platform - which is half of what S6 is about. */
|
||||
.collapse-chev {
|
||||
flex: 0 0 auto; width: 7px; height: 7px;
|
||||
border-right: 1.5px solid currentColor;
|
||||
border-bottom: 1.5px solid currentColor;
|
||||
transform: rotate(45deg) translate(-2px, -2px);
|
||||
transition: transform .15s ease;
|
||||
}
|
||||
.card-toggle[aria-expanded="false"] .collapse-chev {
|
||||
transform: rotate(-45deg) translate(-2px, 2px);
|
||||
}
|
||||
|
||||
.card-body[hidden] { display: none !important; }
|
||||
.card.collapsed .section-desc { display: none; }
|
||||
/* A collapsed card is a heading, so it should read as a row rather than a box
|
||||
with one line in it. */
|
||||
/* A collapsed section should read as a ROW in a list, not as a box with one line
|
||||
in it. Eleven of them at the card's own 28px padding is 660px of nothing -
|
||||
which is a third of what F6 was measuring, arriving by a different door. */
|
||||
.card.collapsed { padding-top: 10px; padding-bottom: 10px; }
|
||||
.card.collapsed .section-header { margin-bottom: 0; padding-bottom: 0; border-bottom: 0; }
|
||||
.card.collapsed .sub-heading { margin-bottom: 0; }
|
||||
|
||||
/* ── the rail ──
|
||||
A horizontal strip above the form by default - which is what fits at 390px -
|
||||
and a sticky column beside it from 1200px, where there is width to spare. */
|
||||
.sec-rail {
|
||||
order: -1;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 30;
|
||||
padding: 8px 28px 8px calc(var(--nav-w, 288px) + 28px);
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.sec-rail-head {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.sec-rail-title {
|
||||
font-family: var(--mono); font-size: 10px; font-weight: 600;
|
||||
letter-spacing: .08em; text-transform: uppercase; color: var(--text-dim);
|
||||
}
|
||||
.sec-rail-all {
|
||||
margin-left: auto;
|
||||
background: none; border: 1px solid var(--border); border-radius: var(--radius);
|
||||
padding: 3px 10px; font: inherit; font-size: 11px; color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.sec-rail-all:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.sec-rail-all[aria-pressed="true"] {
|
||||
border-color: var(--accent); color: var(--accent); background: var(--accent-dim);
|
||||
}
|
||||
.sec-rail-list {
|
||||
display: flex; flex-wrap: wrap; gap: 4px;
|
||||
list-style: none; margin: 0; padding: 0;
|
||||
}
|
||||
.sec-rail-item {
|
||||
display: block; width: 100%;
|
||||
background: none; border: 1px solid transparent; border-radius: var(--radius);
|
||||
padding: 7px 11px; font: inherit; font-size: 12px; font-weight: 500;
|
||||
color: var(--text-muted); cursor: pointer; text-align: left; white-space: nowrap;
|
||||
min-height: 34px;
|
||||
}
|
||||
.sec-rail-item:hover { color: var(--accent); border-color: var(--border); }
|
||||
/* Not colour alone: the current entry is bolder, keeps a left marker and is the
|
||||
one carrying aria-current. */
|
||||
.sec-rail-item.is-current {
|
||||
color: var(--accent); font-weight: 700;
|
||||
background: var(--accent-dim);
|
||||
border-color: var(--accent-dim);
|
||||
box-shadow: inset 2px 0 0 0 var(--accent);
|
||||
}
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
/* Horizontal strip: the entries sit side by side and the list scrolls rather
|
||||
than stacking eleven full-width rows above the form. */
|
||||
.sec-rail-list { flex-wrap: nowrap; overflow-x: auto; }
|
||||
.sec-rail-item { width: auto; }
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.wp-layout { flex-direction: row; align-items: flex-start; }
|
||||
.main { flex: 1 1 auto; }
|
||||
.sec-rail {
|
||||
order: 0;
|
||||
flex: 0 0 224px;
|
||||
align-self: flex-start;
|
||||
top: var(--sec-rail-top, 48px);
|
||||
max-height: calc(100vh - var(--sec-rail-top, 48px));
|
||||
overflow-y: auto;
|
||||
margin: 22px 28px 0 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.sec-rail-list { flex-direction: column; flex-wrap: nowrap; }
|
||||
.sec-rail-item { white-space: normal; }
|
||||
}
|
||||
|
||||
/* ── SOP-inherited marker ───────────────────────────────────────────────────
|
||||
The "from SOP types" subtext used to sit under the field. It's now a small
|
||||
|
||||
Reference in New Issue
Block a user