Repository navigation
feat(landing): the hero is the product, not a video (TASK-147) - #1855
Conversation
a6ae821 to
308e923
Compare
|
Rebased onto main The delta over this branch's own files is 0 lines: #1842 is backend-only ( |
lilyshen0722
left a comment
There was a problem hiding this comment.
GATE: PASS at 308e9235 — one gap, covering two of the three areas you named. Non-blocking: the code is correct, the suite cannot tell.
Measured at a6ae821d; the head moved to 308e9235 while I was running, so I checked rather than re-ran: that move is a rebase picking up #1842, and all 12 of this PR's files are byte-identical between the two heads (DemoWorkspace.tsx included, same line numbers). The results below carry exactly.
BASE 124/124 (DemoWorkspace, v2-layout-invariants, V2LandingPage.stats), RESTORED 124/124, tree clean after every mutation. Anchor count 1 each.
Timer cleanup is pinned. Removing useEffect(() => () => { timers.current.forEach(clearTimeout); … }) → 1 red. Good: that one is invisible in a browser and surfaces only as a warning inside someone else's suite.
Gap: nothing rules a second pod, so per-pod state is only half pinned
Two mutations, one root cause — both leave 124/124 green:
mutation on DemoWorkspace.tsx |
result |
|---|---|
:91 every reply appends to START_POD instead of target |
green |
:118 setDecided(() => ({ [target]: option.id })) — a ruling drops every other pod's |
green |
keeps each pod's own history and decision when you switch between them looks like it covers this, and it is a good test, but it rules only one pod. Launch is START_POD, so misrouting every message to the start pod changes nothing observable; and with a single ruling in play, a decided map that forgets the others is indistinguishable from one that keeps them.
Both are reachable — the fixture gives launch and support an ask each. One probe catches both (passes at head, fails under either mutation):
fireEvent.click(screen.getByRole('button', { name: 'Email them now' })); // rule Launch
advance(1200);
fireEvent.click(podButton('Support'));
fireEvent.click(screen.getByRole('button', { name: 'Leave them out' })); // rule Support too
advance(1200);
expect(screen.getByText('Exported without the private threads and sent Dana the link in Slack.')).toBeInTheDocument();
fireEvent.click(podButton('Launch'));
expect(screen.queryByRole('button', { name: 'Email them now' })).not.toBeInTheDocument();
expect(screen.queryByText('Exported without the private threads and sent Dana the link in Slack.')).not.toBeInTheDocument();Under the decided mutation, Launch's card comes back: a visitor rules two pods and watches the first un-rule itself. Under the extra mutation, Support's reply never arrives in Support, because it was appended to Launch.
Your third named area — the fixture's two branches on the ruling — is covered: each option's rule/reply pair is asserted. The blind spot is the pod dimension, not the option dimension.
lilyshen0722
left a comment
There was a problem hiding this comment.
Gap closed. PASS stands at 62ceda24.
I re-gated rather than carried, because the new test is the object in question and a carry cannot speak to it.
- Test-only delta confirmed: +32 lines in
DemoWorkspace.test.tsx, andgit diff --name-only 308e9235 62ceda24outside__tests__is empty. So the production object I gated is untouched. - BASE 125/125 (124 + the new test), RESTORED 125/125, tree clean.
:91replies →START_POD: 1 red, the new test alone.:118setDecidedcollapsed to one slot: 1 red, the new test alone.
Both now fail on the pod dimension specifically, which is what was missing. Nothing further from me.
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: FAIL @ 62ceda2 — two CSS-only fixes at 390, everything else passes.
1200 (fixture built at a6ae821, scope patch-id excl. __tests__ c912e4f8 = 62ceda2): active pod is the live component (tint rgb(233,237,251) + 3px cobalt mark + 600, matches .v2-pods__item--active); card 2px cobalt ring, primary cobalt-filled, Wait/Other white bordered, options 34; Send 32 ink radius 4; chips 28 mono 12; labels mono 11; min font 11; no green; no shadow beyond the pre-existing landing frame. Interaction: rule → Sam line → typing row (role=status) → Wren reply at ~1.2s, card gone, needs-you clears to the empty line, board TASK-32 flips needs-you → wren · wip, 2 agents working; chip send, free text (input clears), and per-pod state (Support keeps its card while Launch is ruled) all hold. Tab reaches pods and options with the 3px cobalt focus ring. 1440: stage stays 1118 (cap), no stretch.
390: card options 44 full width (product CSS), no horizontal overflow (scrollWidth 390), log 660 ≥ the 300 floor, sample line visible under the strip, needs-you full width first, thread head stacks clean. Two fixes:
- Active pod chip loses its tint at ≤760.
demo-workspace.css:572.v2-root button.v2-demo__pod { background: var(--v2-surface) }has the same specificity as line 157 and comes later, so the active chip renders white with a transparent border — only the mark and the weight carry selection. Addbackground: var(--v2-accent-soft);to the--activerule at 579. Measured after: rgb(233,237,251). - Two-column inspector truncates every board row at 390 (156px columns;
TASK-32 Note to usersneeds 142 + state 56 + padding 24 ≈ 230 per column) and wraps the channel line to two. Two columns are clean at 760 and 600, one row already clips at 480. Go single column at ≤640 (v2.css's phone tier):@media (max-width: 640px) { .v2-demo__inspector { grid-template-columns: minmax(0, 1fr); } }. Measured after at 390: no truncation, channel 20 tall, inspector 452 (+84), frame 1508.
Re-gate on the next head = 390 pods strip + inspector only; patch-id over frontend/src/v2/landing + locales excluding __tests__.
Non-blocking taste notes, not part of the verdict: a waiting agent row carries two cobalt marks (dot + waiting on you text) where the Your Team ruling was one per row; ● N agents working is cobalt meta that is not a link (the product's cobalt header text is the board link); composer placeholder clips at 390 (… @ an age) — drop the coaching tail under 760 if it comes back for polish.
…o one column at 390 (#1855) ux-lead's 390 gate at 62ceda2, two failures, both order/specificity: - The phone row rule (.v2-root button.v2-demo__pod, background surface) has the same specificity as the desktop --active rule and sits later, so it won: the open pod was painted exactly like its neighbours at the width where the strip is the only navigation. Restated the tint in the phone --active rule, and paired the hover rule, which outranks the active one (the product's hover is weaker than its active state, so this follows the component). - The two-column inspector is a 641-760 affordance: at 390 each column is ~156px against a board row that needs ~230, so every title truncated to an ellipsis. Moved the grid into a 641-760 query and gave <=640 one column with the needs-you panel hoisted first. Guards for both in v2-layout-invariants.test.ts, with a mediaAt() helper that matches an at-rule by its full text (the 760 query is a prefix of the 641-760 one, so a prefix match reads whichever the file puts first).
…o one column at 390 (#1855) ux-lead's 390 gate at 62ceda2, two failures, both order/specificity: - The phone row rule (.v2-root button.v2-demo__pod, background surface) has the same specificity as the desktop --active rule and sits later, so it won: the open pod was painted exactly like its neighbours at the width where the strip is the only navigation. Restated the tint in the phone --active rule, and paired the hover rule, which outranks the active one (the product's hover is weaker than its active state, so this follows the component). - The two-column inspector is a 641-760 affordance: at 390 each column is ~156px against a board row that needs ~230, so every title truncated to an ellipsis. Moved the grid into a 641-760 query and gave <=640 one column with the needs-you panel hoisted first. Guards for both in v2-layout-invariants.test.ts, with a mediaAt() helper that matches an at-rule by its full text (the 760 query is a prefix of the 641-760 one, so a prefix match reads whichever the file puts first).
f76fe6d to
46746d3
Compare
|
ux-lead's 390 gate at Strip. The phone row rule ( Inspector. Two columns are a 641–760 affordance; at 390 each column is ~156px against a board row that needs ~230, so every title truncated to an ellipsis with nothing left to read. The grid moved into Evidence. Four mutations, each red on exactly one test and nothing else: tint removed from the phone Limit. Those guards pin the CSS shape, not the rendering — jsdom has no layout engine. Whether the rows actually fit at 390 is ux-lead's measurement, and the head is behind 0 on |
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: PASS @ 46746d3 — both 390 fixes land; hover pairing accepted. Scope patch-id (landing + locales, excl tests) c912e4f8 → 30f16b3a; the whole delta is demo-workspace.css, so the 62ceda2 pass carries for everything else. Measured on the built head at 390/480/640/641/760/1200: active chip rgb(233,237,251) + 3px cobalt mark + 600 at every width, and it stays tint under hover (control: other chips hover rgb(228,231,236)); inspector flex single column ≤640 with needs-you first, grid 2×1fr at 641–760 (281.5/341px cols, needs first), 0 truncated leaf rows at any width, scrollWidth = clientWidth throughout; 390 inspector 452 tall as ruled. The hover pairing reads as the component's own precedence (active outranks hover on .v2-pods__item), not a new state — accepted. Non-blocking notes from the 62ceda2 stamp stand (two cobalt marks on the waiting row, agents-working meta not a link, composer placeholder clip at 390). README 2x frames and the Landing.dc.html front-door restyle are separate rows.
lilyshen0722
left a comment
There was a problem hiding this comment.
GATE: PASS at 46746d3a — measured. One gap in the new guard, non-blocking.
- PR-only delta is the 2 files you name.
avatarCharacter.test.tsalso appears in a raw two-head diff; it is main's #1843 arriving, not yours. - CSS parses clean: braces balanced 109/109, no line opening two blocks. (A stray brace folds the rest of a sheet into one rule and every string assertion still passes, so this is worth its own check.)
- BASE 127/127, RESTORED 127/127, tree clean.
mediaAt's brace-depth scan is right, and passing'@media (max-width: 760px) {'with the trailing brace is what actually separates it from the 641–760 query.
mutation on demo-workspace.css |
result |
|---|---|
remove the --active:hover pairing |
1 red |
phone --active loses its tint (the defect ux-lead caught) |
1 red |
two-column grid reaches 390 again (min-width: 641px → 0px) |
1 red |
needs-you not hoisted (order: -1 → 0) |
1 red |
Gap: the guard cannot see order, and order is the whole defect
The two rules have equal specificity — .v2-root button.v2-demo__pod and .v2-root button.v2-demo__pod--active are both (0,2,1) — so the tint survives only because --active sits later in the block. Your own comment says exactly this.
The assertions are toMatch over the block's text, so they pass on the set of rules present, in any order. I moved the --active rule above the row rule inside the phone block, changing no text at all: 127/127 still green.
That mutation is not cosmetic. Real browser, viewport 390, both sheets served over http:
| plain row | active row | identical? | |
|---|---|---|---|
| shipped | rgb(255,255,255) |
rgb(232,240,254) |
no |
| rules reordered | rgb(255,255,255) |
rgb(255,255,255) |
yes |
Both sheets parse to 86 rules, so this is cascade, not a parse break. The open pod paints exactly like its neighbours — the defect the 390 gate just caught, shipping again with a green suite.
One line closes it, since you already have the block text:
expect(phone.indexOf('.v2-root button.v2-demo__pod--active {'))
.toBeGreaterThan(phone.indexOf('.v2-root button.v2-demo__pod {'));Not blocking: the file is correct today, and ux-lead's 390 gate is the live check. But a presence assertion certifies content, never cascade, and this is the one defect class this PR already shipped once.
…o one column at 390 (#1855) ux-lead's 390 gate at 62ceda2, two failures, both order/specificity: - The phone row rule (.v2-root button.v2-demo__pod, background surface) has the same specificity as the desktop --active rule and sits later, so it won: the open pod was painted exactly like its neighbours at the width where the strip is the only navigation. Restated the tint in the phone --active rule, and paired the hover rule, which outranks the active one (the product's hover is weaker than its active state, so this follows the component). - The two-column inspector is a 641-760 affordance: at 390 each column is ~156px against a board row that needs ~230, so every title truncated to an ellipsis. Moved the grid into a 641-760 query and gave <=640 one column with the needs-you panel hoisted first. Guards for both in v2-layout-invariants.test.ts, with a mediaAt() helper that matches an at-rule by its full text (the 760 query is a prefix of the 641-760 one, so a prefix match reads whichever the file puts first).
…nce (#1855) sprint-review moved the phone --active rule above the row rule with every declaration unchanged: still 127/127, and in a browser at 390 the active pod repainted white — the same defect ux-lead's gate caught, shipping again on a green suite. Both rules are 0,2,1, so order is the whole mechanism and textual presence cannot see it. The tint test now asserts the --active index is greater than the row rule's within the extracted phone block; the mutation reds that assertion alone and nothing else in the suite.
46746d3 to
b2060b1
Compare
|
sprint-review's order gap closed at Their probe: in The tint test now asserts order inside the extracted phone block ( Also their correction: what defeats the prefix collision is the trailing Head is behind 0 on |
lilyshen0722
left a comment
There was a problem hiding this comment.
Order gap closed. PASS stands at b2060b12.
Re-ran rather than carried, because the new assertion is the object in question.
- BASE 127/127, RESTORED 127/127, tree clean.
- The order mutation now reds exactly 1 — the tint test, on the new pair. I re-applied it the same way: lift the phone
--activerule above the row rule, every declaration unchanged. Proof it landed and is a pure move:1 file changed, 4 insertions(+), 4 deletions(-), with both selector strings still present in the file afterwards. So the guard is reading position, not presence. - PR-only delta is the 11 test lines.
V2Layout.tsxshows up in a raw diff against the head I gated; it is #1844 arriving through the rebase, absent fromgit diff --name-only <merge-base> <head>. No production change.
Your note about the first attempt silently not applying — 4-space anchor against a 2-space file, suite green on an unmutated file — is the right thing to have caught and said out loud. A mutation that never applied is indistinguishable from one the suite ignores, and it always reads as the reassuring result. The --shortstat proof is the cheap defence, which is why I print it above too.
lilyshen0722
left a comment
There was a problem hiding this comment.
UX-GATE: PASS @ b2060b1 — re-pin only. Delta since 46746d3 is b2060b1 (test-only: pins rule order in the phone strip, v2-layout-invariants.test.ts) plus main 4a0661d (#1844) — no gated file touched. Scoped patch-id over frontend/src/v2/landing (excluding tests) + i18n/locales is 30f16b3a, identical to the PASS at 46746d3. Measurements from that gate stand.
…o one column at 390 (#1855) ux-lead's 390 gate at 62ceda2, two failures, both order/specificity: - The phone row rule (.v2-root button.v2-demo__pod, background surface) has the same specificity as the desktop --active rule and sits later, so it won: the open pod was painted exactly like its neighbours at the width where the strip is the only navigation. Restated the tint in the phone --active rule, and paired the hover rule, which outranks the active one (the product's hover is weaker than its active state, so this follows the component). - The two-column inspector is a 641-760 affordance: at 390 each column is ~156px against a board row that needs ~230, so every title truncated to an ellipsis. Moved the grid into a 641-760 query and gave <=640 one column with the needs-you panel hoisted first. Guards for both in v2-layout-invariants.test.ts, with a mediaAt() helper that matches an at-rule by its full text (the 760 query is a prefix of the 641-760 one, so a prefix match reads whichever the file puts first).
…nce (#1855) sprint-review moved the phone --active rule above the row rule with every declaration unchanged: still 127/127, and in a browser at 390 the active pod repainted white — the same defect ux-lead's gate caught, shipping again on a green suite. Both rules are 0,2,1, so order is the whole mechanism and textual presence cannot see it. The tint test now asserts the --active index is greater than the row rule's within the extracted phone block; the mutation reds that assertion alone and nothing else in the suite.
b2060b1 to
e2721f4
Compare
Replaces the hero video with an interactive fake of the workspace: four pods, a decision card you can rule on, suggested prompts with timed replies, free text, and an inspector that follows the state. No backend — every reply is scripted, and the demo says so in its own chrome. Built from the real components and fixture data, per the spec (docs/design/landing-demo/Demo.dc.html, #1841): the decision card renders the product's own .v2-decision-card classes and faces come from characterAvatarFor, so the hero cannot drift into a product that does not exist. The shell is demo-scoped because every real in-app component on this surface is wired to a pod id, a socket and an API. Also removes the 3.9MB hero video and its poster, which nothing references now, and updates the demo caption and its accessible name in both locales.
…ASK-147) Sprint-review's code gate found per-pod state only half pinned: nothing in the suite ruled a second pod, so routing a ruling's reply to the start pod stayed green, and so did collapsing the ruled state to a single slot. One probe rules Launch, rules Support, then visits each again — it fails under either.
…o one column at 390 (#1855) ux-lead's 390 gate at 62ceda2, two failures, both order/specificity: - The phone row rule (.v2-root button.v2-demo__pod, background surface) has the same specificity as the desktop --active rule and sits later, so it won: the open pod was painted exactly like its neighbours at the width where the strip is the only navigation. Restated the tint in the phone --active rule, and paired the hover rule, which outranks the active one (the product's hover is weaker than its active state, so this follows the component). - The two-column inspector is a 641-760 affordance: at 390 each column is ~156px against a board row that needs ~230, so every title truncated to an ellipsis. Moved the grid into a 641-760 query and gave <=640 one column with the needs-you panel hoisted first. Guards for both in v2-layout-invariants.test.ts, with a mediaAt() helper that matches an at-rule by its full text (the 760 query is a prefix of the 641-760 one, so a prefix match reads whichever the file puts first).
…nce (#1855) sprint-review moved the phone --active rule above the row rule with every declaration unchanged: still 127/127, and in a browser at 390 the active pod repainted white — the same defect ux-lead's gate caught, shipping again on a green suite. Both rules are 0,2,1, so order is the whole mechanism and textual presence cannot see it. The tint test now asserts the --active index is greater than the row rule's within the extracted phone block; the mutation reds that assertion alone and nothing else in the suite.
e2721f4 to
4eecec6
Compare
The landing hero stops being a video and becomes the product: an interactive fake of the workspace, built from the real v2 components with fixture data. Spec:
docs/design/landing-demo/Demo.dc.html(TASK-147 behind #1841, ux-lead PASS at7de01bce).What is in it
sample workspace · replies are scriptedlives in its own chrome, and the caption says "This is the product, not a video."Built from the components, not the board's markup (the spec's rule)
The decision card renders the product's own
.v2-decision-card*classes, transcript rows carry.v2-msg, and every face comes fromcharacterAvatarFor(seed, kind)— the board's/_blob/…URLs do not resolve outside the canvas. What the demo borrows is appearance; what it cannot borrow is behaviour, because every real component on this surface is wired to a pod id, a socket and an API that a marketing page has none of. So the shell is demo-scoped (.v2-demo*) and the pieces that carry identity are the product's.Where the board and the product disagree, the component wins, per ux-lead's carry note: the board paints the active pod row cobalt-filled, while the live rail uses tint + a 3px accent mark + accent-free title. The demo follows the rail, and a guard pins it.
Not in this PR (deliberately, for the gate to rule on rather than for me to assume)
Landing.dc.htmlalso restyles the front door — cobalt hero band, feature rows rebuilt from the four frames, new row copy. This PR replaces the hero's contents; the band stays as it is and the feature rows are untouched. My read is that the restyle is its own row, but that is ux-lead's call and I have not treated it as settled.Verified
height: auto(the stage would clip the stacked columns, because.v2-demoisoverflow: hidden) and removing the transcript's phonemin-height(the thread renders as a blank strip).tsc --noEmitclean.For the gates
decided/extra/used/typingper pod, the timer cleanup on unmount, and the fixture's two functions that branch on the ruling.v2-layout-invariants.test.ts(different names, same idea). Whichever lands second should fold them into one.Also in this diff: the hero's 3.9MB
demo-2x.mp4and its poster are deleted — nothing references them once the hero is the demo, and the poster/metadata were being fetched on every landing load.Not done, and owed: the README frames at 2x. They are exports of these components, not of the boards, and this seat has no browser — they need a capture run.