feat(feedback): three-step progress bar + "Sign up here" link

A real <ol> whose current step carries aria-current="step" and whose
done step is marked by a tick (never colour alone), following the page
state — step 1 when seats are available, step 1 done / step 2 current
when a seat is claimed, and no bar at all in the cohort-full and
count-unavailable states. The bar states no count and no deadline:
both come from the wire.

The claimed state gains the signup link: "Sign up here",
target="_blank" rel="noopener noreferrer", pointing at the directory
page's own #apply form — the one place the application lives, no
invented URL. Secondary to any button; the accessible text says it
opens a new window.

Untouched: the four states, the consent gate, the claim body, the
leftKeys contract, the leak rules, the 15 questions, the corrected copy.
This commit is contained in:
webmaster committed 2026-11-03 20:04:20 +00:00
1 parent 28d95b4dcd
commit bb963850b1
1 file changed
+100 -1
+100 -1
View File
@@ -145,6 +145,51 @@
.steps { margin: 0.4rem 0 0; padding-left: 1.2rem; font-size: 0.92rem; }
.steps li { margin: 0 0 0.3rem; }
/* ---- Progress bar (three steps) ---- */
.progress { margin: 0 0 1.15rem; }
.steps-bar {
list-style: none;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.3rem 0.6rem;
margin: 0;
padding: 0;
font-size: 0.85rem;
}
.steps-bar .step { display: flex; align-items: center; gap: 0.4rem; color: var(--ink-soft); }
.steps-bar .step + .step::before { content: "·"; margin-right: 0.6rem; color: var(--line-brass); }
.step-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.55rem;
height: 1.55rem;
border-radius: 999px;
border: 1px solid var(--line);
background: #fff;
font-size: 0.8rem;
line-height: 1;
}
/* The current step is the brass pill; a done step carries the ✓ mark (see the script) —
the tick is the non-colour cue, so a done step is never colour alone. */
.steps-bar .step-current { color: var(--ink); font-weight: 600; }
.steps-bar .step-current .step-mark { background: var(--brass); border-color: var(--brass); color: var(--midnight); }
.steps-bar .step-done .step-mark { background: var(--success); border-color: var(--success); color: #fff; }
.link-signup { color: var(--brass-deep); font-weight: 600; }
/* Visually hidden, still read aloud. */
.vh {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
/* ---- Feedback list (cohort full) ---- */
.fb { border-top: 1px solid var(--line); padding: 1rem 0 0; margin: 1rem 0 0; }
.fb:first-of-type { border-top: 0; padding-top: 0.2rem; margin-top: 0.4rem; }
@@ -258,6 +303,9 @@
var C = window.O4F;
var app = document.getElementById("app");
var claimedUpwork = ""; // the seat claim: an identifier, kept in memory, never in the URL
// The application form on the directory page — the anchor the page's own "Get listed"
// triggers use. There is no separate login or account page, so this is the one target.
var SIGNUP_URL = "https://odoo4projects.com/odoo-community-developers/#apply";
function el(tag, cls, text) {
var n = document.createElement(tag);
@@ -320,6 +368,42 @@
return b;
}
// ---- Progress bar (three steps) ---------------------------------------------------------
// Which step is current follows the page state — it is not decoration. It renders only
// where there is something to do: seats available (step 1 current) and seat claimed
// (step 1 done, step 2 current, step 3 visible as what comes next). The cohort-full and
// count-unavailable states render no bar at all. The bar states no count and no deadline:
// both come from the wire and are printed elsewhere, from the wire alone.
var STEP_LABELS = ["Claim seat", "Sign up", "Fill out feedback"];
function progressBar(current) {
var nav = el("nav", "progress");
nav.setAttribute("aria-label", "Signup progress");
var ol = el("ol", "steps-bar");
STEP_LABELS.forEach(function (label, i) {
var n = i + 1;
var li = el("li", "step");
var mark = el("span", "step-mark");
mark.setAttribute("aria-hidden", "true");
// A done step shows the ✓ mark; the current and upcoming steps show their number.
mark.textContent = n < current ? "\u2713" : String(n);
li.appendChild(mark);
li.appendChild(el("span", "step-label", label));
if (n === current) {
li.className = "step step-current";
li.setAttribute("aria-current", "step");
} else if (n < current) {
// Done is the ✓ mark AND a word for screen readers — never colour alone.
li.className = "step step-done";
li.appendChild(el("span", "vh", " (completed)"));
} else {
li.className = "step step-next";
}
ol.appendChild(li);
});
nav.appendChild(ol);
return nav;
}
// ---- The four states -----------------------------------------------------------------
// 1. Seats available. The number is read from the wire; a cap is not a remaining count.
function stateAvailable(seats, total) {
@@ -394,6 +478,7 @@
});
show([
progressBar(1),
panel([h, el("p", "detail", "One seat per freelancer, and there is a fixed number for this cohort.")]),
panel([el("h2", "", "Claim your seat"), form])
]);
@@ -427,7 +512,21 @@
}
claimed.appendChild(steps);
show([claimed, feedbackPanel()]);
// The one thing to do while a seat is held: go and finish the signup. A link, never a
// second button — the page keeps one primary action at a time, and the target is the
// application form the page's own "Get listed" triggers use.
var signup = el("p", "detail");
var up = el("a", "link-signup");
up.href = SIGNUP_URL;
up.target = "_blank";
up.rel = "noopener noreferrer";
up.appendChild(document.createTextNode("Sign up here"));
// Say it opens a new window, so a screen-reader user is not surprised by a new tab.
up.appendChild(el("span", "", " (opens in a new window)"));
signup.appendChild(up);
claimed.appendChild(signup);
show([progressBar(2), claimed, feedbackPanel()]);
}
function clockTime(iso) {