Locked future-stage cards + journey rail
Future stages now render as preview-only "look ahead" cards instead of being hidden. A user at stage 2 can see headers and contents for stages 3, 4, 5, but those sections are visibly locked and uneditable. Locked-card treatment: - Dashed-rule border, paper-2 fill, no shadow — visually quieter than active cards - "Upcoming" pill in the header with a small lock glyph - Muted stage rank mark (dashed badge, low-opacity icon) - Panel content wrapped in fieldset[disabled] so every form control inside is natively non-interactive, with an opacity tweak for affordance - "A look ahead" banner explaining that fields will become editable when the co-op reaches this stage Section visibleWhen is still consulted on submit, so locked-stage values never get written back to CiviCRM even if data is prefilled. Journey rail: - Vertical rail (md+) in a new left gutter; each card carries an aligned marker. Past stages = filled leaf circle with check; current = filled leaf disc with rank number, leaf-100 halo ring, and a subtle rail-pulse box-shadow animation (motion-safe). A "Now" pill sits beneath the current marker. Future stages = dashed hollow ring with lock glyph. - Connector segments between markers are solid leaf when the next stage is past-or-current, dashed muted when future — so the transition from "traveled" to "ahead" reads at the right place in the journey. - Mobile fallback: a small vertical stem in the gap between adjacent cards, styled the same way (solid vs dashed) so the progression cue still reads on narrow viewports.
This commit is contained in:
@@ -22,6 +22,12 @@ interface StageSectionProps {
|
||||
formValues: Record<string, unknown>;
|
||||
/** Whether this is the section that matches the current org stage. */
|
||||
isCurrent: boolean;
|
||||
/**
|
||||
* Whether this stage is ahead of the org's current stage. Locked sections
|
||||
* still render so the user can see what's coming, but the fields inside
|
||||
* are wrapped in a disabled fieldset and never written on submit.
|
||||
*/
|
||||
locked: boolean;
|
||||
/** Whether the section starts open. */
|
||||
defaultOpen: boolean;
|
||||
/** Option groups fetched from CiviCRM, keyed by option_group_id. */
|
||||
@@ -46,6 +52,7 @@ export function StageSection({
|
||||
errors,
|
||||
formValues,
|
||||
isCurrent,
|
||||
locked,
|
||||
defaultOpen,
|
||||
options,
|
||||
}: StageSectionProps) {
|
||||
@@ -83,15 +90,22 @@ export function StageSection({
|
||||
0,
|
||||
);
|
||||
|
||||
const cardClass = isCurrent
|
||||
? "border-2 border-leaf-600 shadow-[0_1px_0_0_rgba(0,0,0,0.04),0_8px_24px_-12px_rgba(60,80,40,0.18)] bg-white/95"
|
||||
: locked
|
||||
? "border border-dashed border-rule bg-paper-2/30 shadow-none"
|
||||
: "border border-rule bg-white/95 shadow-sm";
|
||||
|
||||
const buttonClass = isCurrent
|
||||
? "bg-leaf-50/60"
|
||||
: locked
|
||||
? "hover:bg-paper-2/50"
|
||||
: "hover:bg-paper-2/60";
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby={headingId}
|
||||
className={
|
||||
"overflow-hidden rounded-lg bg-white/95 transition " +
|
||||
(isCurrent
|
||||
? "border-2 border-leaf-600 shadow-[0_1px_0_0_rgba(0,0,0,0.04),0_8px_24px_-12px_rgba(60,80,40,0.18)]"
|
||||
: "border border-rule shadow-sm")
|
||||
}
|
||||
className={"overflow-hidden rounded-lg transition " + cardClass}
|
||||
>
|
||||
<h2 id={headingId} className="m-0">
|
||||
<button
|
||||
@@ -101,12 +115,17 @@ export function StageSection({
|
||||
aria-controls={panelId}
|
||||
className={
|
||||
"group relative flex w-full items-center gap-4 px-5 py-4 text-left transition-colors sm:px-6 " +
|
||||
(isCurrent ? "bg-leaf-50/60" : "hover:bg-paper-2/60")
|
||||
buttonClass
|
||||
}
|
||||
>
|
||||
<StageRankMark rank={section.rank} isCurrent={isCurrent} />
|
||||
<StageRankMark rank={section.rank} isCurrent={isCurrent} locked={locked} />
|
||||
<span className="flex-1 min-w-0">
|
||||
<span className="block font-display text-lg font-medium text-ink leading-tight tracking-tight sm:text-xl">
|
||||
<span
|
||||
className={
|
||||
"block font-display text-lg font-medium leading-tight tracking-tight sm:text-xl " +
|
||||
(locked ? "text-ink-soft" : "text-ink")
|
||||
}
|
||||
>
|
||||
{section.label}
|
||||
</span>
|
||||
<span className="mt-1 flex items-center gap-3 text-xs text-ink-mute">
|
||||
@@ -116,6 +135,12 @@ export function StageSection({
|
||||
Current stage
|
||||
</span>
|
||||
)}
|
||||
{locked && (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full border border-rule bg-paper px-2 py-0.5 font-medium text-ink-mute uppercase tracking-[0.08em]">
|
||||
<LockGlyph />
|
||||
Upcoming
|
||||
</span>
|
||||
)}
|
||||
<span className="tabular-nums">
|
||||
{fieldCount} {fieldCount === 1 ? "field" : "fields"}
|
||||
</span>
|
||||
@@ -130,9 +155,17 @@ export function StageSection({
|
||||
role="region"
|
||||
aria-labelledby={headingId}
|
||||
hidden={!open}
|
||||
className="border-t border-rule-soft"
|
||||
className={"border-t " + (locked ? "border-rule" : "border-rule-soft")}
|
||||
>
|
||||
<div className="px-5 py-6 sm:px-7 sm:py-7">
|
||||
<fieldset
|
||||
disabled={locked}
|
||||
aria-disabled={locked || undefined}
|
||||
className={
|
||||
"px-5 py-6 sm:px-7 sm:py-7 " +
|
||||
(locked ? "opacity-80" : "")
|
||||
}
|
||||
>
|
||||
{locked && <LockedBanner />}
|
||||
{section.intro && (
|
||||
<p className="mb-6 max-w-prose text-[15px] leading-relaxed text-ink-soft">
|
||||
{section.intro}
|
||||
@@ -170,7 +203,7 @@ export function StageSection({
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -181,20 +214,35 @@ export function StageSection({
|
||||
* icon offset behind it. When current, the ring is leaf-green and the
|
||||
* number is white-on-green; otherwise it's quiet.
|
||||
*/
|
||||
function StageRankMark({ rank, isCurrent }: { rank: number; isCurrent: boolean }) {
|
||||
function StageRankMark({
|
||||
rank,
|
||||
isCurrent,
|
||||
locked,
|
||||
}: {
|
||||
rank: number;
|
||||
isCurrent: boolean;
|
||||
locked: boolean;
|
||||
}) {
|
||||
const iconClass = isCurrent
|
||||
? "text-leaf-700 opacity-100"
|
||||
: locked
|
||||
? "text-ink-mute opacity-30"
|
||||
: "text-leaf-600 opacity-50";
|
||||
const badgeClass = isCurrent
|
||||
? "bg-leaf-700 text-paper"
|
||||
: locked
|
||||
? "bg-paper text-ink-mute border border-dashed border-rule"
|
||||
: "bg-paper text-ink-soft border border-rule";
|
||||
return (
|
||||
<span aria-hidden className="relative inline-flex h-12 w-12 flex-shrink-0 items-center justify-center">
|
||||
<StageIcon
|
||||
rank={rank}
|
||||
className={
|
||||
"absolute inset-0 h-12 w-12 transition-opacity " +
|
||||
(isCurrent ? "text-leaf-700 opacity-100" : "text-leaf-600 opacity-50")
|
||||
}
|
||||
className={"absolute inset-0 h-12 w-12 transition-opacity " + iconClass}
|
||||
/>
|
||||
<span
|
||||
className={
|
||||
"relative z-10 inline-flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-semibold tabular-nums shadow-sm " +
|
||||
(isCurrent ? "bg-leaf-700 text-paper" : "bg-paper text-ink-soft border border-rule")
|
||||
badgeClass
|
||||
}
|
||||
style={{ marginLeft: 28, marginTop: 28 }}
|
||||
>
|
||||
@@ -204,6 +252,42 @@ function StageRankMark({ rank, isCurrent }: { rank: number; isCurrent: boolean }
|
||||
);
|
||||
}
|
||||
|
||||
function LockGlyph() {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-3 w-3"
|
||||
>
|
||||
<rect x="2.5" y="5.5" width="7" height="5" rx="0.75" />
|
||||
<path d="M4 5.5V4a2 2 0 0 1 4 0v1.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function LockedBanner() {
|
||||
return (
|
||||
<div
|
||||
role="note"
|
||||
className="mb-5 flex items-start gap-3 rounded-md border border-dashed border-rule bg-paper px-4 py-3"
|
||||
>
|
||||
<span aria-hidden className="mt-0.5 text-ink-mute">
|
||||
<LockGlyph />
|
||||
</span>
|
||||
<p className="text-xs leading-relaxed text-ink-soft">
|
||||
<span className="font-medium text-ink-soft">A look ahead.</span>{" "}
|
||||
These fields will become editable when your co-op reaches this stage. They're visible
|
||||
now so you can preview the framework you'll be working through.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Chevron({ open }: { open: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
Reference in New Issue
Block a user