feat(home): Getting Started moves first, collapsible, in-page anchor (#296)

Three tweaks to the post-PR-#291 Getting Started card:

1. Chronologically first. Moved from below the install-hero (where
   it sat as a static white card) to ABOVE it, inside the same
   `{% if not onboarded %}` guard. The blue hero is now the actual
   install flow that the card points at, not a peer that competes
   for attention.

2. Collapsed by default. Switched from <section> to <details> with
   no `open` attribute, so the page lands with just a quiet pill
   (`Getting Started — Two quick next steps — click to expand ›`).
   Expand to reveal the two rows. Chevron rotates 90deg when open
   via the `[open]` selector. Per-device dismiss X stays — generic
   `.home-card-close[data-dismiss-key]` handler now uses
   `closest('section, details')` so it works on both container types.

3. First row → #install-hero in-page anchor. Was `/setup` (which
   would round-trip to the same hero via a redirect through /setup).
   Anchored directly to the blue hero on the same page; copy reads
   "One-time install — walkthrough in the section below" so the
   user knows it's a scroll-to, not a navigation. Install-hero <div>
   gained `id="install-hero"`. `.install-hero { scroll-margin-top:
   88px }` keeps the hero's eyebrow clear of the 72px sticky header
   on the jump.

Second row link to /setup-advanced and the dismiss key unchanged.
GS disappears alongside the install-hero when the user is onboarded,
so the in-page anchor never dangles. Tests updated to assert the new
markup + onboarded-state hiding.
This commit is contained in:
Vojtech 2026-05-14 11:02:23 +04:00 committed by GitHub
parent 4501c9c3dd
commit 3d244038b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 129 additions and 52 deletions

View file

@ -120,7 +120,53 @@
margin-bottom: 18px;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
}
.home-mock .home-getting-started > header h2,
/* Getting Started uses <details> for native collapsed-by-default
behaviour. The summary owns the padding when collapsed; row layout
reveals on expand. Chevron rotates 90deg when open. */
.home-mock details.home-getting-started {
padding: 0;
}
.home-mock .home-gs-summary {
list-style: none;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
padding: 16px 24px;
user-select: none;
}
.home-mock .home-gs-summary::-webkit-details-marker { display: none; }
.home-mock .home-gs-summary-title {
font-size: 16px;
font-weight: 600;
color: var(--hp-text-primary);
}
.home-mock .home-gs-summary-hint {
flex: 1;
font-size: 13px;
color: var(--hp-text-secondary);
}
.home-mock .home-gs-summary-chev {
font-size: 18px;
color: var(--hp-text-muted);
transition: transform 0.15s;
}
.home-mock details.home-getting-started[open] .home-gs-summary-chev {
transform: rotate(90deg);
}
.home-mock details.home-getting-started[open] {
padding-bottom: 18px;
}
.home-mock details.home-getting-started[open] .home-gs-item {
margin-left: 24px;
margin-right: 24px;
}
/* Install-hero is the scroll target for Getting Started's first row.
Offset the anchor jump by the 72px sticky .app-header height + a
bit of breathing room so the hero's eyebrow lands cleanly under
the header bar. */
.home-mock .install-hero { scroll-margin-top: 88px; }
.home-mock .home-overview > h2,
.home-mock .home-usage > header h2 {
font-size: 18px;
@ -128,7 +174,6 @@
margin: 0 0 6px;
color: var(--hp-text-primary);
}
.home-mock .home-getting-started > header p,
.home-mock .home-usage > header p {
font-size: 13px;
color: var(--hp-text-secondary);
@ -1355,7 +1400,42 @@
etc.) stays. Offboarding escape hatch moved to a discrete strip
below; see `.offboard-strip`. #}
{% if not onboarded %}
<div class="install-hero">
{# Getting Started renders FIRST in the not-onboarded flow as a
collapsed-by-default <details>. Click the summary to expand the
two-row map. First row anchors back to the install hero just
below (#install-hero); second row leaves the page for
/setup-advanced. Per-device dismiss X (data-dismiss-key) survives
on the generic .home-card-close handler — selector widened in JS
to accept <details> containers too. Disappears post-onboarding
alongside the hero so the in-page anchor never dangles. #}
<details class="home-getting-started" id="homeGettingStarted">
<summary class="home-gs-summary">
<span class="home-gs-summary-title">Getting Started</span>
<span class="home-gs-summary-hint">Two quick next steps — click to expand</span>
<span class="home-gs-summary-chev" aria-hidden="true">&rsaquo;</span>
</summary>
<button type="button" class="home-card-close"
data-dismiss-key="agnes_home_gs_dismissed"
aria-label="Dismiss Getting Started">&times;</button>
<a class="home-gs-item" href="#install-hero">
<span class="ico" aria-hidden="true">&#x1F680;</span>
<div class="text">
<strong>Setup {{ instance_brand }} in your Claude Code</strong>
<span class="desc">One-time install — walkthrough in the section below.</span>
</div>
<span class="arrow" aria-hidden="true">&darr;</span>
</a>
<a class="home-gs-item" href="/setup-advanced">
<span class="ico" aria-hidden="true">&#x1F6E0;&#xFE0F;</span>
<div class="text">
<strong>Go deeper into your AI workspace</strong>
<span class="desc">VS Code layout, recommended plugins, multi-model second opinions, custom skills + rules + hooks, project workflows.</span>
</div>
<span class="arrow" aria-hidden="true">&rarr;</span>
</a>
</details>
<div class="install-hero" id="install-hero">
<button type="button" class="install-hero-close" id="installHeroClose"
data-target-source="self_acknowledged"
aria-label="I'm already set up — close this setup hero">&times;</button>
@ -1514,35 +1594,10 @@ Set-Location "$HOME\{{ workspace_dir }}"</span>
BEFORE Step 3's install runs ~20 commands. Gated by the same
`home_automode.show` flag at the call site. #}
{# Getting Started card — dismissible per-device via localStorage.
Two clickable rows pointing at the install flow (/setup) and the
deeper reference (/setup-advanced). Subsumes the legacy
`.advanced-pointer` row that used to sit above the news section. #}
<section class="home-getting-started" id="homeGettingStarted">
<button type="button" class="home-card-close"
data-dismiss-key="agnes_home_gs_dismissed"
aria-label="Dismiss Getting Started">&times;</button>
<header>
<h2>Getting Started</h2>
<p>Two quick next steps to get the most out of {{ instance_brand }}.</p>
</header>
<a class="home-gs-item" href="/setup">
<span class="ico" aria-hidden="true">&#x1F680;</span>
<div class="text">
<strong>Setup {{ instance_brand }} in your Claude Code</strong>
<span class="desc">One-time install: copies a setup script to your clipboard, paste into Claude Code, done in ~10 minutes.</span>
</div>
<span class="arrow" aria-hidden="true">&rarr;</span>
</a>
<a class="home-gs-item" href="/setup-advanced">
<span class="ico" aria-hidden="true">&#x1F6E0;&#xFE0F;</span>
<div class="text">
<strong>Go deeper into your AI workspace</strong>
<span class="desc">VS Code layout, recommended plugins, multi-model second opinions, custom skills + rules + hooks, project workflows.</span>
</div>
<span class="arrow" aria-hidden="true">&rarr;</span>
</a>
</section>
{# Getting Started was previously rendered HERE (between the offboard
strip and Overview) as a full-card <section>. Moved up to render
BEFORE the install-hero as a collapsed-by-default <details> — see
the block right after `{% if not onboarded %}` near line ~1357. #}
{# Overview section — operator-owned, opt-in. Body comes from the
`instance.overview` yaml field via get_instance_overview()
@ -2015,7 +2070,7 @@ Set-Location "$HOME\{{ workspace_dir }}"</span>
// zero per-card JS.
document.querySelectorAll('.home-card-close[data-dismiss-key]').forEach(function (btn) {
var key = btn.getAttribute('data-dismiss-key');
var section = btn.closest('section');
var section = btn.closest('section, details');
if (!section || !key) return;
try {
if (localStorage.getItem(key) === '1') {

View file

@ -335,27 +335,49 @@ def test_home_renders_connector_prompts_from_shared_module(fresh_db):
def test_getting_started_card_renders_on_home(fresh_db):
"""The dismissible Getting Started card sits between the install
hero and the connector tiles. Both rows must be present and point
at /setup and /setup-advanced respectively. State-independent:
renders for both onboarded and not-onboarded users (per-device
localStorage dismiss is the only off switch)."""
"""The dismissible Getting Started card now renders BEFORE the
install-hero (chronologically first in the not-onboarded flow) as
a <details> element collapsed by default so the install hero
stays visible on first paint. Disappears when the user is
onboarded (no `<details class="home-getting-started">`) so the
in-page #install-hero anchor on the first row never points at
nothing. First row links to #install-hero (same-page jump to the
blue setup hero); second row still leaves the page for
/setup-advanced."""
from src.db import get_system_db, close_system_db
for onboarded in (False, True):
conn = get_system_db()
try:
_, sess = _make_user_and_session(
conn, email=f"gs-{onboarded}@example.com", onboarded=onboarded
)
finally:
conn.close()
close_system_db()
body = _client().get("/home", cookies={"access_token": sess}).text
assert '<section class="home-getting-started"' in body
assert 'data-dismiss-key="agnes_home_gs_dismissed"' in body
assert 'class="home-gs-item" href="/setup"' in body
assert 'class="home-gs-item" href="/setup-advanced"' in body
# Not-onboarded: GS is rendered + install-hero anchor target exists.
conn = get_system_db()
try:
_, sess = _make_user_and_session(
conn, email="gs-not-onboarded@example.com", onboarded=False
)
finally:
conn.close()
close_system_db()
body = _client().get("/home", cookies={"access_token": sess}).text
assert '<details class="home-getting-started"' in body
assert 'data-dismiss-key="agnes_home_gs_dismissed"' in body
assert 'class="home-gs-item" href="#install-hero"' in body
assert 'class="home-gs-item" href="/setup-advanced"' in body
# Install-hero must carry the matching id so the first-row anchor
# resolves. Co-asserted with the GS markup so a refactor that drops
# one but not the other breaks here, not in the browser.
assert '<div class="install-hero" id="install-hero">' in body
# Onboarded: install-hero is gone, GS rides alongside it — neither
# renders. Prevents a dangling #install-hero anchor.
conn = get_system_db()
try:
_, sess2 = _make_user_and_session(
conn, email="gs-onboarded@example.com", onboarded=True
)
finally:
conn.close()
close_system_db()
body2 = _client().get("/home", cookies={"access_token": sess2}).text
assert '<details class="home-getting-started"' not in body2
assert '<div class="install-hero"' not in body2
def test_overview_section_renders_when_yaml_set(fresh_db, monkeypatch):