@bongos/core 1.19.390 → 1.19.392

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.390",
6
- "core_contract": "1.19.390",
7
- "source_commit": "fca78267668e7bfe1241ef5502900854cb91a59d",
5
+ "core_version": "1.19.392",
6
+ "core_contract": "1.19.392",
7
+ "source_commit": "4190fb38da41bebf7a88f615a8ab79fedd639d41",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-28T01:40:58.750Z",
9
+ "built_at": "2026-08-28T13:35:20.944Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 339,
12
+ "docs_redacted": 340,
13
13
  "agent_docs_stubbed": 22,
14
- "functional_verbatim": 1622,
14
+ "functional_verbatim": 1628,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1983,
20
- "tree_sha256": "cfb7d40bbbc5a242cfd992804860f21603eb1faa51af33d4dd6739464e971005",
19
+ "file_count": 1990,
20
+ "tree_sha256": "f7ac578ee44964ff1176606f3ecd818002c622d2c3916e3cc351ee9f789ec202",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -42,7 +42,7 @@
42
42
  {
43
43
  "path": ".claude/skills/builder-claim/SKILL.md",
44
44
  "mode": "0000644",
45
- "sha256": "4e6aeafd02b609df2a7651e11238a1b4c2f45b61f8a8f4122200c530aae81b25"
45
+ "sha256": "248ba892c506cb2ba5787dd9bc11978623d7116e9eb911e79d9c6b33b2d7374e"
46
46
  },
47
47
  {
48
48
  "path": ".claude/skills/builder-connect/SKILL.md",
@@ -1569,6 +1569,26 @@
1569
1569
  "mode": "0000644",
1570
1570
  "sha256": "c77dbf2152c6109b6ce925e2badf6dfaa74f74c2d0cb7eb76fa1101eec7b3310"
1571
1571
  },
1572
+ {
1573
+ "path": "docs/design/mocks/hub/hub-sheet.html",
1574
+ "mode": "0000644",
1575
+ "sha256": "195002c1d320a42f15bcae68d38fd60d49facd339a34a32860b32cbebcbf15a7"
1576
+ },
1577
+ {
1578
+ "path": "docs/design/mocks/hub/v1.html",
1579
+ "mode": "0000644",
1580
+ "sha256": "a71d44848f98465aa5bfea1af4786c1c15841b94acd479a88f73260666812215"
1581
+ },
1582
+ {
1583
+ "path": "docs/design/mocks/hub/v2.html",
1584
+ "mode": "0000644",
1585
+ "sha256": "5ba56b1c17921bd19032d74e7547339946e260841889fc1af35d8e07c6962cf7"
1586
+ },
1587
+ {
1588
+ "path": "docs/design/mocks/hub/v3.html",
1589
+ "mode": "0000644",
1590
+ "sha256": "fe34413d588f2b1afd327a9246a8ce5c995cbe7e5e50232db8115d8e615b6c5d"
1591
+ },
1572
1592
  {
1573
1593
  "path": "docs/design/mocks/landing-page-mock.html",
1574
1594
  "mode": "0000644",
@@ -1744,6 +1764,11 @@
1744
1764
  "mode": "0000644",
1745
1765
  "sha256": "8a8430af4bbf082d3bab28d5b82b353a6921782014c2852a87eb1056ba101722"
1746
1766
  },
1767
+ {
1768
+ "path": "docs/design/projects-hub-direction-v2.md",
1769
+ "mode": "0000644",
1770
+ "sha256": "a29bd3d9cfe29b4eb7dff290134d818996f691e7669d7c8f1abee3c3fd57422b"
1771
+ },
1747
1772
  {
1748
1773
  "path": "docs/design/projects-hub-direction.md",
1749
1774
  "mode": "0000644",
@@ -1792,7 +1817,7 @@
1792
1817
  {
1793
1818
  "path": "docs/file-map.md",
1794
1819
  "mode": "0000644",
1795
- "sha256": "c717435fcd8d12a1073419666cdeeec1ae40d988789f2980413d2c22e1c45784"
1820
+ "sha256": "4da98da4c1d5bc5e2a1c1949775f362d8867faa5a2a0321ccd954dc0a5b71202"
1796
1821
  },
1797
1822
  {
1798
1823
  "path": "docs/handoff-template.md",
@@ -5122,12 +5147,12 @@
5122
5147
  {
5123
5148
  "path": "package-lock.json",
5124
5149
  "mode": "0000644",
5125
- "sha256": "6df325de41dee29b93290e06d42cba92acbc357b5475c2bbefa70c8f9d96df1b"
5150
+ "sha256": "b36f0f8b8ea9a6748b1a3be5c3a8f54e33d20339b32c21e0ebb08cd71e1fe4c0"
5126
5151
  },
5127
5152
  {
5128
5153
  "path": "package.json",
5129
5154
  "mode": "0000644",
5130
- "sha256": "1f4bb5de0e76f44c4588ed68adc9731eb8aa0b5e22fd21d26b4f31880a655de0"
5155
+ "sha256": "95d40e824b8da871c7168496f16f6e36b91379d6c92489ce3f141a1f6fb2b854"
5131
5156
  },
5132
5157
  {
5133
5158
  "path": "public-docs/index.html",
@@ -5707,7 +5732,7 @@
5707
5732
  {
5708
5733
  "path": "scripts/gds/claim.js",
5709
5734
  "mode": "0000644",
5710
- "sha256": "528e0fd4a91933cab751524f4a0c930dd0f23945801538182343faacf02d9ef7"
5735
+ "sha256": "d2710556d771f5dcc7298806c91f3a33c7ad16b9f7738eef7edd14b7868d143b"
5711
5736
  },
5712
5737
  {
5713
5738
  "path": "scripts/gds/classify-discipline.js",
@@ -6729,6 +6754,11 @@
6729
6754
  "mode": "0000644",
6730
6755
  "sha256": "be4fec619d5a0c76ba05bcc75042c1497f70da21b54a3d646884e2228f90b2ba"
6731
6756
  },
6757
+ {
6758
+ "path": "scripts/gds/worktree-claim-guard.js",
6759
+ "mode": "0000644",
6760
+ "sha256": "1ad4dfc90a2c5cd8afe40cd43c6b3d119e9bb7710785e024b4dd8d45e7ab823f"
6761
+ },
6732
6762
  {
6733
6763
  "path": "scripts/gds/worktree.js",
6734
6764
  "mode": "0000644",
@@ -7112,7 +7142,7 @@
7112
7142
  {
7113
7143
  "path": "src/module-api.js",
7114
7144
  "mode": "0000644",
7115
- "sha256": "b0af1d77cee68628e7693a9beab490c3c66d9aa6ff091bc6da61fef4fe0f48a7"
7145
+ "sha256": "129f7524912a1dfda3b4d8307d0cf12b1869b3c937f173846098597d542fba09"
7116
7146
  },
7117
7147
  {
7118
7148
  "path": "src/module-loader/catalog.js",
@@ -9909,6 +9939,11 @@
9909
9939
  "mode": "0000644",
9910
9940
  "sha256": "43c7c42ffe92143443c45d5ba382d522efa0a16ef14f58675e6a86233e3f8e74"
9911
9941
  },
9942
+ {
9943
+ "path": "tests/worktree_claim_guard.mjs",
9944
+ "mode": "0000644",
9945
+ "sha256": "28662ce24931e2623c8d54e6931d80f400715a6661a6e78d638f7e8cdaa6db87"
9946
+ },
9912
9947
  {
9913
9948
  "path": "tests/worktree_disambiguation.mjs",
9914
9949
  "mode": "0000644",
@@ -9,7 +9,7 @@ You are claiming a task on behalf of the current builder. The claim is atomic
9
9
 
10
10
  ## What this skill does
11
11
 
12
- Runs `node scripts/gds/claim.js <task-id> [--worktree NAME]`. The CLI hits `POST /api/gds/claims`, which transactionally:
12
+ Runs `node scripts/gds/claim.js <task-id> [--worktree NAME]`. Before it hits the API at all, the CLI enforces **worktree-per-claim locally** (task 1002579 — see below). Then it hits `POST /api/gds/claims`, which transactionally:
13
13
 
14
14
  1. Verifies the task exists and is `ready`.
15
15
  2. Verifies all declared `dependencies[]` are in `shipped` status (V3.R02 / migration 020).
@@ -37,7 +37,13 @@ The server now **enforces** the one-claim half (task 1642): `POST /api/gds/claim
37
37
 
38
38
  1. **Confirm the task id with the user** if they didn't give a number directly.
39
39
 
40
- 2. **Determine the worktree name** (cross-platform). If we're inside a worktree, the worktree name is the leaf folder of the current path. Resolve it portably with node (works on Windows/macOS/Linux — no `basename`/`$PWD` shell-isms):
40
+ 2. **Be in a per-claim worktree, not the main checkout.** `claim.js` **refuses** (exit 2) when it is run in the primary worktree — worktree-per-claim is the project default (task 1642) and task 1002579 is the check behind it. Branching in the main checkout parks it off `main`, and `ship.js`'s local auto-merge then has nowhere to merge INTO: the ship strands at `confirmed` until the server reconciler or `/merge-mode` catches it. The refusal names the exact fix; run it and claim from the new tree:
41
+ ```
42
+ node scripts/gds/worktree.js add <task-id>
43
+ ```
44
+ A linked worktree claims normally, and a tree git can't classify is allowed through (the guard never blocks on a guess). The escape hatch — for a builder who genuinely means to work in the main checkout — is `--here`, which downgrades the refusal to a loud warning and accepts the `confirmed`/`/merge-mode` finish.
45
+
46
+ **Determine the worktree name** (cross-platform). The worktree name is the leaf folder of the current path. Resolve it portably with node (works on Windows/macOS/Linux — no `basename`/`$PWD` shell-isms):
41
47
  ```
42
48
  node -e "console.log(require('path').basename(process.cwd()))"
43
49
  ```
@@ -0,0 +1,192 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Hub composition round: bar, dropdowns, search and the poster card, three versions (task 1003299)</title>
7
+ <style>
8
+ :root{--ink:#14151a;--soft:#585c6b;--faint:#8a8fa0;--line:#d9dce5;--bg:#f3f4f8;--card:#ffffff;--kick:#0b5fff;}
9
+ :root[data-mode="dark"]{--ink:#f1f2f7;--soft:#a9aec0;--faint:#737890;--line:#2a2e3b;--bg:#0c0e14;--card:#151822;--kick:#7aa2ff;}
10
+ @media (prefers-color-scheme:dark){:root:not([data-mode="light"]){--ink:#f1f2f7;--soft:#a9aec0;--faint:#737890;--line:#2a2e3b;--bg:#0c0e14;--card:#151822;--kick:#7aa2ff;}}
11
+ *{box-sizing:border-box;margin:0;padding:0}
12
+ body{background:var(--bg);color:var(--ink);font-family:system-ui,-apple-system,"Segoe UI",sans-serif;padding:clamp(18px,3vw,40px);-webkit-font-smoothing:antialiased;line-height:1.5}
13
+ h1{font-size:clamp(22px,3vw,32px);letter-spacing:-.4px;margin-bottom:6px}
14
+ .sub{color:var(--soft);max-width:100ch;font-size:14.5px;margin-bottom:8px}
15
+ .sub b{color:var(--ink)}
16
+ .read{border-left:3px solid var(--kick);padding:10px 14px;margin:14px 0 18px;font-size:14px;max-width:110ch;background:color-mix(in srgb,var(--kick) 7%,transparent);border-radius:0 8px 8px 0}
17
+ .read code{font-family:ui-monospace,Consolas,monospace;font-size:12.5px}
18
+ .read ol,.read ul{margin:6px 0 0 18px}
19
+ .bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:6px 0 22px;position:sticky;top:0;background:var(--bg);padding:10px 0;z-index:5;border-bottom:1px solid var(--line)}
20
+ .bar span{font-size:11px;letter-spacing:.6px;text-transform:uppercase;color:var(--faint);margin-left:10px}
21
+ .bar span:first-child{margin-left:0}
22
+ .bar button,.bar select{cursor:pointer;font:inherit;font-weight:600;font-size:13px;padding:6px 14px;border-radius:999px;border:1.5px solid var(--line);background:var(--card);color:var(--ink)}
23
+ .bar button[aria-pressed="true"]{background:var(--ink);color:var(--card);border-color:var(--ink)}
24
+ .cand{background:var(--card);border:1.5px solid var(--line);border-radius:16px;padding:16px 18px 18px;margin-bottom:22px}
25
+ .kick{font-size:11px;letter-spacing:.9px;text-transform:uppercase;color:var(--kick);font-weight:700}
26
+ .cand h2{font-size:20px;margin:2px 0 4px;letter-spacing:-.2px}
27
+ .meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:8px 22px;font-size:13.5px;color:var(--soft);margin:6px 0 12px}
28
+ .meta b{color:var(--ink);font-weight:600}
29
+ .meta ul{margin:2px 0 0 16px}
30
+ .tiles{display:grid;grid-template-columns:3fr 3fr 1fr 1fr;gap:12px;align-items:start;margin-bottom:12px}
31
+ .boards{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;align-items:start}
32
+ @media (max-width:1100px){.tiles{grid-template-columns:1fr 1fr}.boards{grid-template-columns:1fr}}
33
+ @media (max-width:640px){.tiles{grid-template-columns:1fr}}
34
+ .tile{border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--bg)}
35
+ .tileHead{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:6px 10px;font-size:12px;color:var(--soft);border-bottom:1px solid var(--line)}
36
+ .tileHead a{color:var(--kick);font-weight:600;text-decoration:none;white-space:nowrap}
37
+ .frame{position:relative;width:100%;aspect-ratio:1440/900;overflow:hidden}
38
+ .frame iframe{position:absolute;top:0;left:0;width:1440px;height:900px;border:0;transform-origin:0 0}
39
+ .phone .frame{aspect-ratio:390/844}
40
+ .phone .frame iframe{width:390px;height:844px}
41
+ .tall .frame{aspect-ratio:1440/2400}
42
+ .tall .frame iframe{height:2400px}
43
+ .decide{border:1.5px solid var(--kick);border-radius:16px;padding:16px 18px;margin-bottom:22px;font-size:14px;max-width:120ch}
44
+ .decide h2{font-size:18px;margin-bottom:6px}
45
+ .decide ol,.decide ul{margin:6px 0 0 20px}
46
+ .decide li{margin:4px 0}
47
+ footer{color:var(--faint);font-size:12.5px;margin-top:28px}
48
+ </style>
49
+ <script>
50
+ (function(){try{var q=new URLSearchParams(location.search).get('mode');var m=(q==='light'||q==='dark')?q:'dark';document.documentElement.setAttribute('data-mode',m);}catch(e){}})();
51
+ </script>
52
+ </head>
53
+ <body>
54
+
55
+ <h1>The /projects hub in the chrome world: three versions of the bar, the dropdowns, the search and the poster card</h1>
56
+ <p class="sub">Round 4 of the apex pass (task 1003299, goal 1000080), after the landing was built and <code>DESIGN.md</code> recorded the world. Each version keeps the sky, the orbs, the hover card, the toolbar, the table (plus a hall glyph), the mine view and the new view exactly as they ship today, and recomposes what the owner settled: <b>one grounded top bar</b> (not sticky), the logo as home, a <b>Projects dropdown</b> holding Map / Your projects / New project, a <b>profile dropdown</b> on the identity chip, the <b>map search in the bar</b> (hidden off the map), no Home, no hall pill (hall entry becomes a per-project affordance), and the <b>poster card</b> with its degradation ladder. Mocks are judged <b>dark first</b>; flip to light. Open this sheet from the checkout that holds <code>docs/design/mocks/hub/</code>; the tiles are local iframes.</p>
57
+
58
+ <div class="read">
59
+ <b>How to read the three.</b> Same givens, same real data, same state contract, same sky. What differs is the bar's material, the menu's mechanism and the card's composition. <b>v1 "the shelf"</b>: the landing's pill grammar set down on a real shelf; floating card panels. <b>v2 "the hairline"</b>: no pills, hairlines only; the menu unfolds as a full-width band under the bar; the card's figures are leader-line callouts on the art. <b>v3 "the glass strip"</b>: one continuous glass surface the sky shows through; segments and attached glass panels; the card's name sits on the art. The state picker flips every version to the same state at once; <b>boards</b> show every bar state, the whole card ladder and the menu component on one tall page each. <code>open full</code> walks any state at full size.
60
+ </div>
61
+
62
+ <div class="decide" id="decision">
63
+ <h2>The decision this round asks of the owner</h2>
64
+ <ol>
65
+ <li><b>Pick one version</b> (or name a merge: for example v1's bar with v3's card).</li>
66
+ <li><b>Beyond the skin.</b> Each version's head comment lists what changes on the page beyond restyling, each item tagged <code>given</code> / <code>consequence</code> / <code>proposal</code>. The proposals are yours to take or leave; they are summarised per version below and in <code>docs/design/projects-hub-direction-v2.md</code>.</li>
67
+ <li><b>Two things the build needs that no feed carries yet</b> (not a design question, recorded so the pick is informed): the faces and the shipped-in-30-days figure need new enrichment fields; a per-project hall address is needed to know where the hall entry shows.</li>
68
+ </ol>
69
+ </div>
70
+
71
+ <div class="bar">
72
+ <span>mode</span>
73
+ <button data-md="dark" aria-pressed="true" type="button">dark</button>
74
+ <button data-md="light" aria-pressed="false" type="button">light</button>
75
+ <span>state</span>
76
+ <select id="statePick" aria-label="State shown in the first tile of every version">
77
+ <option value="in-map-projects">signed in, map, Projects menu open</option>
78
+ <option value="in-map">signed in, map</option>
79
+ <option value="out-map">signed out, map</option>
80
+ <option value="in-map-profile">signed in, profile menu open</option>
81
+ <option value="in-map-focus">keyboard focus in the menu</option>
82
+ <option value="in-map-search">search focused with a query</option>
83
+ <option value="in-mine">your projects (search hidden)</option>
84
+ <option value="in-new">new project (search hidden)</option>
85
+ <option value="out-new">signed out, new project</option>
86
+ <option value="in-table">table view with the hall glyph</option>
87
+ </select>
88
+ <span>card</span>
89
+ <select id="cardPick" aria-label="Card state shown in the second tile of every version">
90
+ <option value="card">the platform's own project, everything present</option>
91
+ <option value="card-member">Off the Boats: no art, a real zero, no hall entry</option>
92
+ <option value="card-noart">no art</option>
93
+ <option value="card-nofaces">no faces</option>
94
+ <option value="card-onefig">one figure</option>
95
+ <option value="card-nofig">no figures</option>
96
+ <option value="card-floor">everything null: today's card</option>
97
+ </select>
98
+ </div>
99
+
100
+ <section class="cand" id="v1">
101
+ <div class="kick">v1 · the shelf</div>
102
+ <h2>The landing's pill grammar, set down on a real shelf</h2>
103
+ <div class="meta"><div><b>The bar:</b> the landing's pills on a <code>bg-frame</code> shelf, 72px, hairline edge; the search a glass pill in the left group.</div><div><b>The menu:</b> a floating card panel, 240px, 8px under its trigger; the current view a 6px accent dot.</div><div><b>The card:</b> the poster as a stack: banner, disc, name, faces, a two-up figure band, hairline ledger rows, the landing's door bar.</div><div><b>Proposes:</b> About in the menu; the card at 560; Runs as the address; the empty state as a hairline; <code>not checked</code> for the status cell.</div></div>
104
+ <div class="tiles">
105
+ <div class="tile" data-v="v1" data-kind="state"><div class="tileHead"><span class="lab">in-map-projects</span><a class="open" href="v1.html?mode=dark&state=in-map-projects" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v1, the bar state picked above, 1440" loading="lazy"></iframe></div></div>
106
+ <div class="tile" data-v="v1" data-kind="card"><div class="tileHead"><span class="lab">card</span><a class="open" href="v1.html?mode=dark&state=card" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v1, the card state picked above, 1440" loading="lazy"></iframe></div></div>
107
+ <div class="tile phone" data-v="v1" data-kind="fixed" data-state="in-map-projects"><div class="tileHead"><span class="lab">390 · menu open</span><a class="open" href="v1.html?mode=dark&state=in-map-projects" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v1 at 390, the Projects menu open" loading="lazy"></iframe></div></div>
108
+ <div class="tile phone" data-v="v1" data-kind="fixed" data-state="card"><div class="tileHead"><span class="lab">390 · card</span><a class="open" href="v1.html?mode=dark&state=card" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v1 at 390, the card" loading="lazy"></iframe></div></div>
109
+ </div>
110
+ <div class="boards">
111
+ <div class="tile tall" data-v="v1" data-kind="fixed" data-state="board-bar"><div class="tileHead"><span class="lab">board · every bar state</span><a class="open" href="v1.html?mode=dark&state=board-bar" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v1, the bar states board" loading="lazy"></iframe></div></div>
112
+ <div class="tile tall" data-v="v1" data-kind="fixed" data-state="board-card"><div class="tileHead"><span class="lab">board · the card ladder</span><a class="open" href="v1.html?mode=dark&state=board-card" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v1, the card ladder board" loading="lazy"></iframe></div></div>
113
+ <div class="tile tall" data-v="v1" data-kind="fixed" data-state="board-menu"><div class="tileHead"><span class="lab">board · the menu and the search</span><a class="open" href="v1.html?mode=dark&state=board-menu" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v1, the menu component board" loading="lazy"></iframe></div></div>
114
+ </div>
115
+ </section>
116
+
117
+ <section class="cand" id="v2">
118
+ <div class="kick">v2 · the hairline</div>
119
+ <h2>Hairlines, not containers: the menu unfolds as a band under the bar</h2>
120
+ <div class="meta"><div><b>The bar:</b> no pills; the page ground closed by one hairline, 64px; a bare search field with a bottom hairline; lowercase text triggers, the open one underlined in the accent.</div><div><b>The menu:</b> a full-width band that unfolds under the bar and pushes the page down; three titled cells; the current one lit by a 2px accent hairline on top.</div><div><b>The card:</b> the specimen: the two figures as leader-line callouts on the art; a mono ledger; one accent door plus the hall as a text link.</div><div><b>Proposes:</b> the toolbar as a hairline row; the empty state as a band; About as a fourth cell; the card at 560; the under-24px controls floored.</div></div>
121
+ <div class="tiles">
122
+ <div class="tile" data-v="v2" data-kind="state"><div class="tileHead"><span class="lab">in-map-projects</span><a class="open" href="v2.html?mode=dark&state=in-map-projects" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v2, the bar state picked above, 1440" loading="lazy"></iframe></div></div>
123
+ <div class="tile" data-v="v2" data-kind="card"><div class="tileHead"><span class="lab">card</span><a class="open" href="v2.html?mode=dark&state=card" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v2, the card state picked above, 1440" loading="lazy"></iframe></div></div>
124
+ <div class="tile phone" data-v="v2" data-kind="fixed" data-state="in-map-projects"><div class="tileHead"><span class="lab">390 · menu open</span><a class="open" href="v2.html?mode=dark&state=in-map-projects" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v2 at 390, the Projects menu open" loading="lazy"></iframe></div></div>
125
+ <div class="tile phone" data-v="v2" data-kind="fixed" data-state="card"><div class="tileHead"><span class="lab">390 · card</span><a class="open" href="v2.html?mode=dark&state=card" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v2 at 390, the card" loading="lazy"></iframe></div></div>
126
+ </div>
127
+ <div class="boards">
128
+ <div class="tile tall" data-v="v2" data-kind="fixed" data-state="board-bar"><div class="tileHead"><span class="lab">board · every bar state</span><a class="open" href="v2.html?mode=dark&state=board-bar" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v2, the bar states board" loading="lazy"></iframe></div></div>
129
+ <div class="tile tall" data-v="v2" data-kind="fixed" data-state="board-card"><div class="tileHead"><span class="lab">board · the card ladder</span><a class="open" href="v2.html?mode=dark&state=board-card" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v2, the card ladder board" loading="lazy"></iframe></div></div>
130
+ <div class="tile tall" data-v="v2" data-kind="fixed" data-state="board-menu"><div class="tileHead"><span class="lab">board · the menu and the search</span><a class="open" href="v2.html?mode=dark&state=board-menu" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v2, the menu component board" loading="lazy"></iframe></div></div>
131
+ </div>
132
+ </section>
133
+
134
+ <section class="cand" id="v3">
135
+ <div class="kick">v3 · the glass strip</div>
136
+ <h2>One continuous glass surface the sky shows through</h2>
137
+ <div class="meta"><div><b>The bar:</b> one continuous glass plate over the sky, 64px; the search fused into it; the right end cut into three segments by hairlines.</div><div><b>The menu:</b> a card panel welded to the strip's bottom edge; the current view marked by an accent bar at its left.</div><div><b>The card:</b> the room: the name on the art's lower third over a card-ground scrim; a shelf line with the disc, the relationship and the faces; figures and facts as one hairline ledger; a full-width door bar.</div><div><b>Proposes:</b> the card at 640; the hover card on the panel's glass; search by tag; the table scrolling in its panel on a phone; the wizard's bongo pair on the chrome ramp; a rule for dark art on a paper card.</div></div>
138
+ <div class="tiles">
139
+ <div class="tile" data-v="v3" data-kind="state"><div class="tileHead"><span class="lab">in-map-projects</span><a class="open" href="v3.html?mode=dark&state=in-map-projects" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v3, the bar state picked above, 1440" loading="lazy"></iframe></div></div>
140
+ <div class="tile" data-v="v3" data-kind="card"><div class="tileHead"><span class="lab">card</span><a class="open" href="v3.html?mode=dark&state=card" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v3, the card state picked above, 1440" loading="lazy"></iframe></div></div>
141
+ <div class="tile phone" data-v="v3" data-kind="fixed" data-state="in-map-projects"><div class="tileHead"><span class="lab">390 · menu open</span><a class="open" href="v3.html?mode=dark&state=in-map-projects" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v3 at 390, the Projects menu open" loading="lazy"></iframe></div></div>
142
+ <div class="tile phone" data-v="v3" data-kind="fixed" data-state="card"><div class="tileHead"><span class="lab">390 · card</span><a class="open" href="v3.html?mode=dark&state=card" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v3 at 390, the card" loading="lazy"></iframe></div></div>
143
+ </div>
144
+ <div class="boards">
145
+ <div class="tile tall" data-v="v3" data-kind="fixed" data-state="board-bar"><div class="tileHead"><span class="lab">board · every bar state</span><a class="open" href="v3.html?mode=dark&state=board-bar" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v3, the bar states board" loading="lazy"></iframe></div></div>
146
+ <div class="tile tall" data-v="v3" data-kind="fixed" data-state="board-card"><div class="tileHead"><span class="lab">board · the card ladder</span><a class="open" href="v3.html?mode=dark&state=board-card" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v3, the card ladder board" loading="lazy"></iframe></div></div>
147
+ <div class="tile tall" data-v="v3" data-kind="fixed" data-state="board-menu"><div class="tileHead"><span class="lab">board · the menu and the search</span><a class="open" href="v3.html?mode=dark&state=board-menu" target="_blank" rel="noopener">open full</a></div><div class="frame"><iframe title="v3, the menu component board" loading="lazy"></iframe></div></div>
148
+ </div>
149
+ </section>
150
+
151
+ <footer>Task 1003299 · goal 1000080 · mocks under <code>docs/design/mocks/hub/</code> · the world: <code>DESIGN.md</code>, ADR 0196 · the record: <code>docs/design/projects-hub-direction-v2.md</code>.</footer>
152
+
153
+ <script>
154
+ (function () {
155
+ var mode = document.documentElement.getAttribute('data-mode') || 'dark';
156
+ var state = 'in-map-projects', card = 'card';
157
+ function src(v, st) { return v + '.html?mode=' + mode + '&state=' + st; }
158
+ function paint() {
159
+ document.querySelectorAll('.tile[data-v]').forEach(function (t) {
160
+ var v = t.getAttribute('data-v'), kind = t.getAttribute('data-kind');
161
+ var st = kind === 'state' ? state : kind === 'card' ? card : t.getAttribute('data-state');
162
+ var f = t.querySelector('iframe'), a = t.querySelector('a.open');
163
+ var u = src(v, st);
164
+ if (f.getAttribute('src') !== u) f.setAttribute('src', u);
165
+ if (a) a.setAttribute('href', u);
166
+ var lab = t.querySelector('.lab'); if (lab && (kind === 'state' || kind === 'card')) lab.textContent = st;
167
+ });
168
+ }
169
+ document.querySelectorAll('.bar button[data-md]').forEach(function (b) {
170
+ b.addEventListener('click', function () {
171
+ mode = b.getAttribute('data-md');
172
+ document.documentElement.setAttribute('data-mode', mode);
173
+ document.querySelectorAll('.bar button[data-md]').forEach(function (x) { x.setAttribute('aria-pressed', String(x === b)); });
174
+ paint();
175
+ });
176
+ });
177
+ document.getElementById('statePick').addEventListener('change', function (e) { state = e.target.value; paint(); });
178
+ document.getElementById('cardPick').addEventListener('change', function (e) { card = e.target.value; paint(); });
179
+ function scale() {
180
+ document.querySelectorAll('.frame').forEach(function (fr) {
181
+ var f = fr.querySelector('iframe'); if (!f) return;
182
+ var w = parseFloat(f.style.width || f.getAttribute('width')) || (fr.closest('.phone') ? 390 : 1440);
183
+ f.style.transform = 'scale(' + (fr.clientWidth / w) + ')';
184
+ });
185
+ }
186
+ window.addEventListener('resize', scale);
187
+ paint(); scale();
188
+ window.addEventListener('load', scale);
189
+ })();
190
+ </script>
191
+ </body>
192
+ </html>