@bongos/core 1.19.406 → 1.19.408

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.
@@ -0,0 +1,232 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <script>
6
+ (function(){try{var q=new URLSearchParams(location.search || location.hash.slice(1) || (window.__Q || '')).get('mode');var m=(q==='light'||q==='dark')?q:localStorage.getItem('cb-color-mode');if(m==='light'||m==='dark')document.documentElement.setAttribute('data-mode',m);}catch(e){}})();
7
+ </script>
8
+ <meta name="viewport" content="width=device-width,initial-scale=1">
9
+ <title>Planet templates, the hub's sky</title>
10
+ <link rel="icon" href="data:,">
11
+ <link rel="preconnect" href="https://fonts.googleapis.com">
12
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
14
+ <link rel="stylesheet" href="../../../../public-landing/assets/cosmos.css">
15
+ <style>
16
+ body{
17
+ /* The composition round for task 1003319 (docs/design/projects-hub-direction-v2.md §11). cosmos.css
18
+ carries the hub's contract + derived tiers and the set plate; the sheet adds the other two cutouts
19
+ the sky needs (root-absolute, the world's rule: a url() in a custom property resolves against the
20
+ DOCUMENT) and the one token that flips with the mode, the object's contact shadow. */
21
+ --plate-bead-cut:url(/assets/world/r2-chrome-blob-one-cut.webp);
22
+ --plate-bongos-cut:url(/assets/world/r2-bongos-chrome-a-cut.webp);
23
+ --orb-shadow:drop-shadow(0 12px 14px color-mix(in srgb,var(--ink) 22%,transparent));
24
+ --grid-line:var(--rule-soft);
25
+ }
26
+ :root[data-mode="dark"] body{
27
+ --orb-shadow:opacity(1);
28
+ --grid-line:var(--rule-soft);
29
+ }
30
+ @media (prefers-color-scheme: dark){
31
+ :root:not([data-mode="light"]) body{
32
+ --orb-shadow:opacity(1);
33
+ --grid-line:var(--rule-soft);
34
+ }
35
+ }
36
+
37
+ /* ── the planet: one of the world's three whole objects on the landing's crop maths, in the
38
+ project's accent. The colour layer blends with the plate; the mask clips both to the cutout's
39
+ alpha, so the colour never paints the box. Shape = plate + rectangle; style = how the accent
40
+ lands (a colour blend, a hue blend, or not at all). ── */
41
+ .planet{position:relative;display:block;width:var(--d,64px);aspect-ratio:var(--tpl-ar);filter:var(--orb-shadow);isolation:isolate;}
42
+ .planet::before{content:"";position:absolute;inset:0;
43
+ background-image:linear-gradient(var(--tpl-ink),var(--tpl-ink)),var(--tpl-src);
44
+ background-repeat:no-repeat;
45
+ background-size:100% 100%,calc(var(--d,64px) * var(--tpl-zoom)) auto;
46
+ background-position:0 0,calc(var(--d,64px) * var(--tpl-px)) calc(var(--d,64px) * var(--tpl-py));
47
+ background-blend-mode:var(--tpl-blend),normal;
48
+ -webkit-mask-image:var(--tpl-src);mask-image:var(--tpl-src);
49
+ -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
50
+ -webkit-mask-size:calc(var(--d,64px) * var(--tpl-zoom)) auto;mask-size:calc(var(--d,64px) * var(--tpl-zoom)) auto;
51
+ -webkit-mask-position:calc(var(--d,64px) * var(--tpl-px)) calc(var(--d,64px) * var(--tpl-py));
52
+ mask-position:calc(var(--d,64px) * var(--tpl-px)) calc(var(--d,64px) * var(--tpl-py));}
53
+ /* shapes: r2-chrome-blob-one u[.14,.865] v[.172,.808]; the bongo pair whole; the set's small bead u[.810,.955] v[.556,.840] */
54
+ [data-shape="blob"]{--tpl-src:var(--plate-bead-cut);--tpl-ar:1.13994;--tpl-zoom:1.37931;--tpl-px:-.19310;--tpl-py:-.23724;}
55
+ [data-shape="pair"]{--tpl-src:var(--plate-bongos-cut);--tpl-ar:1;--tpl-zoom:1;--tpl-px:0;--tpl-py:0;}
56
+ [data-shape="bead"]{--tpl-src:var(--plate-set-cut);--tpl-ar:.91476;--tpl-zoom:6.89655;--tpl-px:-5.58621;--tpl-py:-2.14014;}
57
+ /* styles */
58
+ [data-style="tint"]{--tpl-ink:var(--acc);--tpl-blend:color;}
59
+ [data-style="wash"]{--tpl-ink:var(--acc);--tpl-blend:overlay;}
60
+ [data-style="chrome"]{--tpl-ink:transparent;--tpl-blend:normal;}
61
+ /* the candidates row (blend modes under consideration) */
62
+ [data-blend="color"]{--tpl-ink:var(--acc);--tpl-blend:color;}
63
+ [data-blend="hue"]{--tpl-ink:var(--acc);--tpl-blend:hue;}
64
+ [data-blend="soft-light"]{--tpl-ink:var(--acc);--tpl-blend:soft-light;}
65
+ [data-blend="overlay"]{--tpl-ink:var(--acc);--tpl-blend:overlay;}
66
+ [data-blend="multiply"]{--tpl-ink:var(--acc);--tpl-blend:multiply;}
67
+ [data-blend="screen"]{--tpl-ink:var(--acc);--tpl-blend:screen;}
68
+ [data-blend="none"]{--tpl-ink:transparent;--tpl-blend:normal;}
69
+
70
+ /* ── the sheet ── */
71
+ .sheet{padding:34px var(--gutter) 60px;max-width:1180px;margin:0 auto;}
72
+ .sheet h2{font-family:var(--font-display);font-weight:300;font-size:clamp(26px,3.1vw,42px);letter-spacing:-.035em;line-height:1.06;color:var(--ink);margin:0 0 6px;}
73
+ .sheet p{font-size:15.5px;line-height:1.5;color:var(--ink-soft);max-width:62ch;margin:0 0 26px;}
74
+ .rowLabel{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);padding:18px 0 12px;border-top:1px solid var(--rule-soft);margin-top:10px;}
75
+ .matrix{display:grid;grid-template-columns:repeat(9,1fr);gap:18px 14px;align-items:end;}
76
+ .cell{display:flex;flex-direction:column;align-items:center;gap:12px;min-width:0;}
77
+ .cell .planet{--d:78px;}
78
+ .cell .nm{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);white-space:nowrap;}
79
+ .cands{display:grid;grid-template-columns:repeat(7,1fr);gap:18px 14px;align-items:end;}
80
+ .cands .planet{--d:96px;}
81
+ @media (max-width:900px){
82
+ .matrix{grid-template-columns:repeat(3,1fr);}
83
+ .cands{grid-template-columns:repeat(4,1fr);}
84
+ .cell .planet{--d:64px;}
85
+ }
86
+
87
+ /* ── the field, both treatments: the world's grammar for the labels ── */
88
+ .mock .field{height:calc(100vh - var(--top-h) - 60px);}
89
+ .mock .worldOrb .disc{width:var(--d);height:auto;aspect-ratio:var(--tpl-ar);border-radius:0;background:none;box-shadow:none;filter:var(--orb-shadow);isolation:isolate;}
90
+ .mock .worldOrb .disc::before{content:"";position:absolute;inset:0;
91
+ background-image:linear-gradient(var(--tpl-ink),var(--tpl-ink)),var(--tpl-src);
92
+ background-repeat:no-repeat;
93
+ background-size:100% 100%,calc(var(--d) * var(--tpl-zoom)) auto;
94
+ background-position:0 0,calc(var(--d) * var(--tpl-px)) calc(var(--d) * var(--tpl-py));
95
+ background-blend-mode:var(--tpl-blend),normal;
96
+ -webkit-mask-image:var(--tpl-src);mask-image:var(--tpl-src);
97
+ -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
98
+ -webkit-mask-size:calc(var(--d) * var(--tpl-zoom)) auto;mask-size:calc(var(--d) * var(--tpl-zoom)) auto;
99
+ -webkit-mask-position:calc(var(--d) * var(--tpl-px)) calc(var(--d) * var(--tpl-py));
100
+ mask-position:calc(var(--d) * var(--tpl-px)) calc(var(--d) * var(--tpl-py));}
101
+ /* the relationship ring: a circle behind the object, the rail dot's grammar */
102
+ .mock .worldOrb .disc::after{content:none;border:0;}
103
+ .mock .worldOrb[data-rel="owner"] .disc::after,.mock .worldOrb[data-rel="builder"] .disc::after{content:"";position:absolute;left:50%;top:50%;
104
+ width:calc(100% + 18px);aspect-ratio:1;translate:-50% -50%;border-radius:50%;z-index:-1;}
105
+ .mock .worldOrb[data-rel="owner"] .disc::after{border:1.5px solid var(--accent);}
106
+ .mock .worldOrb[data-rel="builder"] .disc::after{border:1px solid color-mix(in srgb,var(--accent) 55%,transparent);}
107
+ .mock .worldOrb[data-rel="owner"] .disc,.mock .worldOrb[data-rel="builder"] .disc{box-shadow:none;}
108
+ .mock .worldOrb .nm{font-family:var(--font-body);font-weight:500;font-size:15px;letter-spacing:-.015em;color:var(--ink);text-shadow:var(--type-halo);}
109
+ .mock .worldOrb .sub{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);text-shadow:none;}
110
+ .mock .worldOrb .badge{top:-14px;background:var(--bg);color:var(--ink-faint);border:1px solid var(--rule);box-shadow:none;
111
+ font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;font-weight:400;padding:3px 10px;}
112
+ .mock .worldOrb[data-rel="owner"] .badge{background:var(--bg);color:var(--ink);border-color:var(--ink-faint);}
113
+ /* A: the void. nothing drawn — the ground is the ground */
114
+ .mock .field .ground::after{content:none;}
115
+ .mock .field .stars{display:none;}
116
+ /* B: the sheet — one hairline grid inside the camera, so it pans and zooms with the objects */
117
+ .mock .field.sheetGrid .skyCam::before{content:"";position:absolute;inset:-100%;
118
+ background-image:linear-gradient(var(--grid-line) 1px,transparent 1px),linear-gradient(90deg,var(--grid-line) 1px,transparent 1px);
119
+ background-size:92px 92px;pointer-events:none;}
120
+ .mock .hintbar{background:none;border:0;padding:0;backdrop-filter:none;font-family:var(--label-face);font-size:var(--label-size);
121
+ letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);gap:18px;}
122
+ .mock .hintbar kbd{background:none;padding:0;color:var(--ink-soft);font-family:inherit;font-size:inherit;}
123
+ .mock .foot{padding:18px var(--gutter);font-size:14px;color:var(--ink-soft);text-align:center;}
124
+
125
+ /* the state contract: one section shows */
126
+ section[data-state]{display:none;}
127
+ body[data-state="sheet"] section[data-state="sheet"],
128
+ body[data-state="void"] section[data-state="void"],
129
+ body[data-state="grid"] section[data-state="grid"]{display:block;}
130
+ .vh{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;}
131
+ .skip{position:absolute;left:12px;top:-60px;background:var(--bg-card);color:var(--ink);padding:11px 18px;border-radius:999px;}
132
+ .skip:focus{top:12px;}
133
+ .bar{height:var(--top-h);display:flex;align-items:center;padding:0 var(--gutter);border-bottom:1px solid var(--rule);font-family:var(--font-body);font-weight:500;font-size:17px;letter-spacing:-.015em;color:var(--ink);}
134
+ </style>
135
+ </head>
136
+ <body class="mock" data-state="sheet">
137
+ <!--
138
+ Planet templates, the composition round (task 1003319, goal 1000080).
139
+ ?state=sheet the nine templates at the five accents, and the blend candidates
140
+ ?state=void field treatment A: the void — nothing drawn, the objects carry the depth
141
+ ?state=grid field treatment B: the sheet — one hairline grid inside the camera
142
+ ?mode=dark|light
143
+ -->
144
+ <a class="skip" href="#main">Skip to content</a>
145
+ <header class="bar">Cloud Bongos</header>
146
+ <main id="main">
147
+ <h1 class="vh">Planet templates</h1>
148
+
149
+ <section data-state="sheet" class="sheet">
150
+ <h2>Nine templates, five accents</h2>
151
+ <p>Each project's orb is one of the world's three whole chrome objects, cut on the landing's own crop maths, in the project's accent. Three shapes by three styles: tint (the accent replaces the chrome's colour), wash (the accent takes only its hue), chrome (the plate as rendered).</p>
152
+ <div class="rowLabel">the blend candidates, one blob, the sea accent</div>
153
+ <div class="cands">
154
+ <div class="cell"><span class="planet" data-shape="blob" data-blend="color" style="--acc:var(--sea)"></span><span class="nm">color</span></div>
155
+ <div class="cell"><span class="planet" data-shape="blob" data-blend="hue" style="--acc:var(--sea)"></span><span class="nm">hue</span></div>
156
+ <div class="cell"><span class="planet" data-shape="blob" data-blend="soft-light" style="--acc:var(--sea)"></span><span class="nm">soft-light</span></div>
157
+ <div class="cell"><span class="planet" data-shape="blob" data-blend="overlay" style="--acc:var(--sea)"></span><span class="nm">overlay</span></div>
158
+ <div class="cell"><span class="planet" data-shape="blob" data-blend="multiply" style="--acc:var(--sea)"></span><span class="nm">multiply</span></div>
159
+ <div class="cell"><span class="planet" data-shape="blob" data-blend="screen" style="--acc:var(--sea)"></span><span class="nm">screen</span></div>
160
+ <div class="cell"><span class="planet" data-shape="blob" data-blend="none" style="--acc:var(--sea)"></span><span class="nm">none</span></div>
161
+ </div>
162
+ <div id="matrix"></div>
163
+ </section>
164
+
165
+ <section data-state="void">
166
+ <div class="fieldWrap"><div class="field" id="fieldA">
167
+ <div class="ground"></div>
168
+ <div class="skyCam">
169
+ <a class="worldOrb" href="#" data-rel="owner" data-shape="pair" data-style="chrome" style="--acc:var(--accent);--d:140px;left:50%;top:44%"><span class="badge">yours</span><span class="disc"></span><span class="nm">Cloud Bongos</span><span class="sub">this platform</span></a>
170
+ <a class="worldOrb" href="#" data-rel="none" data-shape="blob" data-style="tint" style="--acc:var(--terracotta);--d:92px;left:22%;top:30%"><span class="disc"></span><span class="nm">Demo</span></a>
171
+ <a class="worldOrb" href="#" data-rel="builder" data-shape="bead" data-style="wash" style="--acc:var(--gold-soft);--d:75px;left:78%;top:66%"><span class="badge">you build here</span><span class="disc"></span><span class="nm">Emersonian Circles</span></a>
172
+ <a class="worldOrb" href="#" data-rel="builder" data-shape="pair" data-style="tint" style="--acc:var(--sea);--d:70px;left:33%;top:74%"><span class="badge">you build here</span><span class="disc"></span><span class="nm">Off the Boats</span></a>
173
+ <a class="worldOrb" href="#" data-rel="none" data-shape="bead" data-style="chrome" style="--acc:var(--sun-lit);--d:80px;left:70%;top:26%"><span class="disc"></span><span class="nm">Mercury</span></a>
174
+ </div>
175
+ <div class="hintbar"><span><kbd>drag</kbd> pan</span><span><kbd>scroll</kbd> zoom</span><span><kbd>click a project</kbd> open it</span></div>
176
+ </div></div>
177
+ </section>
178
+
179
+ <section data-state="grid">
180
+ <div class="fieldWrap"><div class="field sheetGrid" id="fieldB">
181
+ <div class="ground"></div>
182
+ <div class="skyCam">
183
+ <a class="worldOrb" href="#" data-rel="owner" data-shape="pair" data-style="chrome" style="--acc:var(--accent);--d:140px;left:50%;top:44%"><span class="badge">yours</span><span class="disc"></span><span class="nm">Cloud Bongos</span><span class="sub">this platform</span></a>
184
+ <a class="worldOrb" href="#" data-rel="none" data-shape="blob" data-style="tint" style="--acc:var(--terracotta);--d:92px;left:22%;top:30%"><span class="disc"></span><span class="nm">Demo</span></a>
185
+ <a class="worldOrb" href="#" data-rel="builder" data-shape="bead" data-style="wash" style="--acc:var(--gold-soft);--d:75px;left:78%;top:66%"><span class="badge">you build here</span><span class="disc"></span><span class="nm">Emersonian Circles</span></a>
186
+ <a class="worldOrb" href="#" data-rel="builder" data-shape="pair" data-style="tint" style="--acc:var(--sea);--d:70px;left:33%;top:74%"><span class="badge">you build here</span><span class="disc"></span><span class="nm">Off the Boats</span></a>
187
+ <a class="worldOrb" href="#" data-rel="none" data-shape="bead" data-style="chrome" style="--acc:var(--sun-lit);--d:80px;left:70%;top:26%"><span class="disc"></span><span class="nm">Mercury</span></a>
188
+ </div>
189
+ <div class="hintbar"><span><kbd>drag</kbd> pan</span><span><kbd>scroll</kbd> zoom</span><span><kbd>click a project</kbd> open it</span></div>
190
+ </div></div>
191
+ </section>
192
+ </main>
193
+ <footer class="foot">A composition sheet for task 1003319. Nothing here is served.</footer>
194
+ <script>
195
+ /* The mock's state contract: read ?state= once, apply one state (sheet | void | grid). The
196
+ matrix is built with createElement + textContent. No motion in script. */
197
+ (function () {
198
+ var state = new URLSearchParams(location.search || location.hash.slice(1) || (window.__Q || '')).get('state') || 'sheet';
199
+ if (state !== 'sheet' && state !== 'void' && state !== 'grid') state = 'sheet';
200
+ document.body.setAttribute('data-state', state);
201
+ var ACCENTS = [['sea', 'var(--sea)'], ['terracotta', 'var(--terracotta)'], ['gold-soft', 'var(--gold-soft)'], ['sun-lit', 'var(--sun-lit)'], ['accent', 'var(--accent)']];
202
+ var TEMPLATES = ['blob-tint', 'pair-wash', 'bead-chrome', 'blob-wash', 'pair-chrome', 'bead-tint', 'blob-chrome', 'pair-tint', 'bead-wash'];
203
+ var host = document.getElementById('matrix');
204
+ ACCENTS.forEach(function (a) {
205
+ var label = document.createElement('div');
206
+ label.className = 'rowLabel';
207
+ label.textContent = 'the ' + a[0] + ' accent';
208
+ host.appendChild(label);
209
+ var row = document.createElement('div');
210
+ row.className = 'matrix';
211
+ TEMPLATES.forEach(function (t) {
212
+ var parts = t.split('-');
213
+ var cell = document.createElement('div');
214
+ cell.className = 'cell';
215
+ var p = document.createElement('span');
216
+ p.className = 'planet';
217
+ p.setAttribute('data-shape', parts[0]);
218
+ p.setAttribute('data-style', parts[1]);
219
+ p.style.setProperty('--acc', a[1]);
220
+ var n = document.createElement('span');
221
+ n.className = 'nm';
222
+ n.textContent = t;
223
+ cell.appendChild(p);
224
+ cell.appendChild(n);
225
+ row.appendChild(cell);
226
+ });
227
+ host.appendChild(row);
228
+ });
229
+ })();
230
+ </script>
231
+ </body>
232
+ </html>
@@ -160,3 +160,13 @@ Three mastheads, rendered dark and light at 1440 / 390 / 320 (ALL CLEAN) and mea
160
160
  **Not done, on purpose:** the platform card has no banner — the mock used the landing's social card as "the platform's art in the only sense it has one", but that image carries the landing's own headline and reads as a screenshot inside the card; the kit fixture uses a text-free plate instead. Whether the platform should carry a real `art_url` is the owner's call.
161
161
 
162
162
  **Owner notes received during the build (filed as task 1003333, the landing hero):** the two doors sit too high and central; the three hero figures with their leader lines leave the landing (they fit the projects page — this card keeps the grammar); the large chrome plates should drift slowly like a lava lamp in place of the small bubble.
163
+
164
+ ## 11. The planets and the field (task 1003319)
165
+
166
+ **Date:** 2026-08-28 · the owner's ask at the pick (§7): "templates for the planets themselves … they look like the blocks from our landing page mock with different colors, shapes, and styles", and "improve the look of the map a bit more as well".
167
+
168
+ **The round.** One composition sheet, `docs/design/mocks/hub/planets.html`, rendered through the kit's stub in both modes at 1440 and 390 (`?state=sheet`: the nine templates at the five accents plus a row of blend candidates; `?state=void` and `?state=grid`: the two field treatments with the real orbs; `check-mock.js` ALL RULES PASS). The templates are the landing's three whole chrome objects on the landing's own crop maths (`.o1/.o2/.o3`; the rectangles are now pinned equal across the two sheets), in the project's accent: **shape** blob / pair / bead by **style** tint / wash / chrome. The blend row decided the styles: `color` for tint (the accent's hue and chroma over the chrome's light — unmistakably the colour, still unmistakably chrome), `overlay` for wash (the chrome lit by the accent with its iridescence kept; `hue` and `soft-light` were too faint to read as a second style, `multiply` and `screen` lost the material), the plate itself for chrome. The field: **the void** (A) over **the sheet** (B, one hairline grid inside the camera so it pans and zooms with the objects). A is DESIGN.md read literally — no drawn atmosphere, the depth is the objects' — and is what was built; B stays on the sheet as the alternative should the owner want a ground reference under the camera.
169
+
170
+ **What was built.** In `cosmos.css` the orb's disc is a masked colour layer over a cutout (`.disc::before`: the plate as both background and mask, `background-blend-mode` by style), sized `--d` by the object's own aspect; `[data-shape]` carries the three crops, `[data-style]` the three blends; the relationship ring is a circle behind the object (a mask clips a box-shadow away), the search spotlight an accent drop-shadow on the disc's filter after the contact shadow (dark's `--orb-shadow` is `opacity(1)`, a no-op, so the two compose). The starfield, its 3.2s twinkle, the 74px gravity grid and `--star-display` are gone; the labels, the badge, the hint and the hover card moved onto the world's type (hairline pill, label face, no accent on a fact, no shadow). In `projects.html` `ORB_TEMPLATES` walks beside `ORB_ACCENTS` in `assignAccents` (consecutive positions change shape and style; nine against five repeat every forty-five projects), `templateOf()` prefers a curated name, an owned-only entry walks by its slug, the orb carries `data-template` / `data-shape` / `data-style`, the physics measures the disc's own height, and the slot widths grew a quarter (a cutout's silhouette reads smaller than a filled disc of the same box). The platform's own orb is the pair in chrome. On the catalog side migration `<redacted>` adds the nullable `template` column with its nine-value CHECK, `upsertProject` carries it COALESCE-preserved, `POST /projects` validates it against `PROJECT_TEMPLATES`, and the public `/projects/featured` projection returns it. The kit's fixture gives Example a curated `bead-wash` and the `in-map` state reads it back off the orb.
171
+
172
+ **Not done, on purpose:** no owner-facing picker — a template is set through the curation route today, the way `featured` is; whether a project's owner chooses their own planet from the manage view is the owner's call. The manage view's small `.orb` spheres keep their old look (the scope named the sky's `a.worldOrb` only).
@@ -0,0 +1,26 @@
1
+ -- platform_identity_012_project_template.sql — a project's planet template (task 1003319).
2
+ --
3
+ -- The hub's sky renders every project as one of the landing's chrome objects in the
4
+ -- project's accent (DESIGN.md "Project orbs" / "Planet templates"): a SHAPE (blob /
5
+ -- pair / bead — the world's three whole objects) and a STYLE (tint / wash / chrome —
6
+ -- how the accent lands on the chrome). Nullable on purpose: null means "not chosen",
7
+ -- and the hub assigns one deterministically from the project's canonical position —
8
+ -- the same walk that assigns its accent — so no row is back-filled with a guess and
9
+ -- a curated choice survives every re-render. An Archon sets it through POST /projects.
10
+ --
11
+ -- Additive + platform_identity_-namespaced (ADR 0083 §5): a new nullable column plus
12
+ -- a CHECK that only constrains non-null values. Idempotent: safe to re-run.
13
+
14
+ BEGIN;
15
+
16
+ ALTER TABLE platform_identity_projects
17
+ ADD COLUMN IF NOT EXISTS template text;
18
+
19
+ ALTER TABLE platform_identity_projects DROP CONSTRAINT IF EXISTS platform_identity_projects_template_check;
20
+ ALTER TABLE platform_identity_projects ADD CONSTRAINT platform_identity_projects_template_check
21
+ CHECK (template IS NULL OR template IN (
22
+ 'blob-tint', 'pair-wash', 'bead-chrome',
23
+ 'blob-wash', 'pair-chrome', 'bead-tint',
24
+ 'blob-chrome', 'pair-tint', 'bead-wash'));
25
+
26
+ COMMIT;
@@ -233,6 +233,12 @@ async function getClientByOrigin(origin, { pool = defaultPool } = {}) {
233
233
  // provisioning's copy on its own instances table).
234
234
  const PROJECT_TYPES = ['game', 'research', 'business', 'non-profit', 'not-sure'];
235
235
 
236
+ // The sky's planet templates (task 1003319): shape (blob / pair / bead, the world's three
237
+ // whole chrome objects) × style (how the accent lands: tint / wash / chrome). A CATALOG field
238
+ // curated through POST /projects; null = the hub's deterministic walk (the accent's), whose
239
+ // order this list IS. Pinned by the DB CHECK (migration 012); mirrored by ORB_TEMPLATES.
240
+ const PROJECT_TEMPLATES = ['blob-tint', 'pair-wash', 'bead-chrome', 'blob-wash', 'pair-chrome', 'bead-tint', 'blob-chrome', 'pair-tint', 'bead-wash'];
241
+
236
242
  // The stored JOIN DOOR (ADR 0182 D1, task 1002323). Three values, and ALL THREE ARE
237
243
  // VISIBLE: `private` is visible-but-closed (the `goals.visibility` meaning, ADR 0112),
238
244
  // `stealth` publishes EXISTENCE without IDENTITY. Mirrored by the CHECK in migration 011.
@@ -284,14 +290,16 @@ function publishableSql(alias) {
284
290
  // report from the authenticated instance (R08, task 1002327). Leaving the column out of
285
291
  // the upsert is what stops a client re-registration from silently resetting an owner's
286
292
  // door back to the 'public' default.
287
- async function upsertProject({ origin, name, tagline, description, artUrl, ownerGithubId, type }, { pool = defaultPool } = {}) {
293
+ async function upsertProject({ origin, name, tagline, description, artUrl, ownerGithubId, type, template }, { pool = defaultPool } = {}) {
288
294
  const norm = String(origin || '').replace(/\/+$/, '');
289
295
  if (!norm) throw new Error('project origin required');
290
296
  // off-vocabulary → null (the DB CHECK is the backstop; a declaration is never invented)
291
297
  const declaredType = PROJECT_TYPES.includes(type) ? type : null;
298
+ // the same rule for the planet template: an unknown name is not a choice
299
+ const declaredTemplate = PROJECT_TEMPLATES.includes(template) ? template : null;
292
300
  const { rows } = await pool.query(
293
- `INSERT INTO platform_identity_projects (origin, name, tagline, description, art_url, owner_github_id, type)
294
- VALUES ($1, $2, $3, $4, $5, $6, $7)
301
+ `INSERT INTO platform_identity_projects (origin, name, tagline, description, art_url, owner_github_id, type, template)
302
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
295
303
  ON CONFLICT (origin) DO UPDATE SET
296
304
  name = EXCLUDED.name,
297
305
  tagline = COALESCE(EXCLUDED.tagline, platform_identity_projects.tagline),
@@ -299,9 +307,10 @@ async function upsertProject({ origin, name, tagline, description, artUrl, owner
299
307
  art_url = COALESCE(EXCLUDED.art_url, platform_identity_projects.art_url),
300
308
  owner_github_id = COALESCE(EXCLUDED.owner_github_id, platform_identity_projects.owner_github_id),
301
309
  type = COALESCE(EXCLUDED.type, platform_identity_projects.type),
310
+ template = COALESCE(EXCLUDED.template, platform_identity_projects.template),
302
311
  updated_at = now()
303
- RETURNING id, origin, name, tagline, description, art_url, owner_github_id, type, status, featured, visibility`,
304
- [norm, name || norm, tagline ?? null, description ?? null, artUrl ?? null, ownerGithubId ?? null, declaredType],
312
+ RETURNING id, origin, name, tagline, description, art_url, owner_github_id, type, template, status, featured, visibility`,
313
+ [norm, name || norm, tagline ?? null, description ?? null, artUrl ?? null, ownerGithubId ?? null, declaredType, declaredTemplate],
305
314
  );
306
315
  return rows[0];
307
316
  }
@@ -311,7 +320,7 @@ async function upsertProject({ origin, name, tagline, description, artUrl, owner
311
320
  // it here rather than leaving each caller to re-derive the gate.
312
321
  async function getProject(id, { pool = defaultPool } = {}) {
313
322
  const { rows } = await pool.query(
314
- `SELECT id, origin, name, tagline, description, art_url, owner_github_id, type, status, featured, visibility
323
+ `SELECT id, origin, name, tagline, description, art_url, owner_github_id, type, template, status, featured, visibility
315
324
  FROM platform_identity_projects WHERE id = $1`,
316
325
  [id],
317
326
  );
@@ -327,7 +336,7 @@ async function getProject(id, { pool = defaultPool } = {}) {
327
336
  // it, so as a public "last updated" it would lie.
328
337
  async function listFeaturedProjects({ pool = defaultPool } = {}) {
329
338
  const { rows } = await pool.query(
330
- `SELECT origin, name, tagline, art_url, type, created_at
339
+ `SELECT origin, name, tagline, art_url, type, template, created_at
331
340
  FROM platform_identity_projects
332
341
  WHERE featured = true AND status = 'active'
333
342
  ORDER BY updated_at DESC`,
@@ -341,7 +350,7 @@ async function listFeaturedProjects({ pool = defaultPool } = {}) {
341
350
  // field so a curator can see WHY a row is dark matter instead of guessing (ADR 0182 D4).
342
351
  async function listAllProjects({ pool = defaultPool } = {}) {
343
352
  const { rows } = await pool.query(
344
- `SELECT id, origin, name, tagline, description, art_url, owner_github_id, type, status,
353
+ `SELECT id, origin, name, tagline, description, art_url, owner_github_id, type, template, status,
345
354
  featured, visibility, updated_at,
346
355
  ${publishableSql()} AS publishable
347
356
  FROM platform_identity_projects
@@ -1180,6 +1189,7 @@ module.exports = {
1180
1189
  publishableSql,
1181
1190
  upsertProject,
1182
1191
  PROJECT_TYPES,
1192
+ PROJECT_TEMPLATES,
1183
1193
  getProject,
1184
1194
  listFeaturedProjects,
1185
1195
  listAllProjects,
@@ -48,6 +48,9 @@ module.exports = function projectsRoutes() {
48
48
  description: { type: 'string', maxLength: 2000 },
49
49
  art_url: { type: 'string', maxLength: 2048 },
50
50
  type: { type: 'string', enum: [...pi.PROJECT_TYPES] },
51
+ // The planet template (task 1003319): the shape and style the sky cuts this
52
+ // project's orb in. Curated here, never self-reported; absent means the hub's walk.
53
+ template: { type: 'string', enum: [...pi.PROJECT_TEMPLATES] },
51
54
  })) return;
52
55
  try {
53
56
  const project = await pi.upsertProject({
@@ -57,6 +60,7 @@ module.exports = function projectsRoutes() {
57
60
  description: req.body.description,
58
61
  artUrl: req.body.art_url,
59
62
  type: req.body.type,
63
+ template: req.body.template,
60
64
  }, { pool });
61
65
  res.status(201).json({ project });
62
66
  } catch (err) {
@@ -226,6 +226,35 @@ await ta('upsertProject carries the declared type, COALESCE-preserved like tagli
226
226
  assert.ok(!pool2.calls[0].params.includes('spacefaring'), 'unknown types never reach the SQL');
227
227
  });
228
228
 
229
+ await ta('upsertProject carries the planet template, COALESCE-preserved; an unknown name is not a choice (task 1003319)', async () => {
230
+ const pool = mockPool([{ rows: [{ id: '1', origin: 'https://p', template: 'bead-wash' }] }]);
231
+ await P.upsertProject({ origin: 'https://p', name: 'Proj', template: 'bead-wash' }, { pool });
232
+ const c = pool.calls[0];
233
+ assert.match(c.text, /template\s*=\s*COALESCE/, 'a later templateless upsert must not erase a curated template');
234
+ assert.ok(c.params.includes('bead-wash'), 'the template value is parameterized');
235
+ const pool2 = mockPool([{ rows: [{ id: '2' }] }]);
236
+ await P.upsertProject({ origin: 'https://q', name: 'Q', template: 'saucer-neon' }, { pool: pool2 });
237
+ assert.ok(!pool2.calls[0].params.includes('saucer-neon'), 'unknown templates never reach the SQL');
238
+ // the vocabulary: the world's three whole objects × the three ways the accent lands,
239
+ // in the walk's order (consecutive positions change shape AND style)
240
+ assert.equal(P.PROJECT_TEMPLATES.length, 9);
241
+ assert.equal(new Set(P.PROJECT_TEMPLATES).size, 9, 'nine distinct templates');
242
+ for (const t of P.PROJECT_TEMPLATES) assert.match(t, /^(blob|pair|bead)-(tint|wash|chrome)$/);
243
+ for (let i = 1; i < P.PROJECT_TEMPLATES.length; i++) {
244
+ const [s0, y0] = P.PROJECT_TEMPLATES[i - 1].split('-');
245
+ const [s1, y1] = P.PROJECT_TEMPLATES[i].split('-');
246
+ assert.ok(s0 !== s1 && y0 !== y1, `walk step ${i} changes both shape and style`);
247
+ }
248
+ // the migration pins the same nine
249
+ const mig = readFileSync(new URL('../migrations/platform_identity_012_project_template.sql', import.meta.url), 'utf8');
250
+ for (const t of P.PROJECT_TEMPLATES) assert.ok(mig.includes(`'${t}'`), `migration 012 names ${t}`);
251
+ // and the public projection carries it, so the sky can read a curated choice
252
+ const pool3 = mockPool([{ rows: [] }]);
253
+ await P.listFeaturedProjects({ pool: pool3 });
254
+ const sel = (pool3.calls[0].text.match(/SELECT([\s\S]*?)FROM/i) || [, ''])[1];
255
+ assert.match(sel, /\btemplate\b/, 'the featured projection carries the template');
256
+ });
257
+
229
258
  await ta('listFeaturedProjects returns a PUBLIC projection (featured+active only, no owner/status)', async () => {
230
259
  const pool = mockPool([{ rows: [{ origin: 'https://p', name: 'Proj', tagline: 't', art_url: null, created_at: '2026-08-01T00:00:00Z' }] }]);
231
260
  const out = await P.listFeaturedProjects({ pool });
@@ -1 +1 @@
1
- {"projects":[{"origin":"https://amazonprimea.com","name":"Off the Boats","tagline":"A singular shop, and a broader videogame in a persistent ancient Greek world that collides with modern day commerce & products.","art_url":"/assets/world/r2-chrome-blob-one.jpg","type":"game","created_at":"2026-07-15T22:12:57.732Z","builders":11,"tags":["art-pipeline","character-anim","demo","game"],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":["https://avatars.githubusercontent.com/u/283020391?v=4","https://avatars.githubusercontent.com/u/64553469?v=4","https://avatars.githubusercontent.com/u/290184380?v=4","https://avatars.githubusercontent.com/u/295990394?v=4","https://avatars.githubusercontent.com/u/35337514?v=4"],"shipped_30d":0,"shipped_30d_capped":false,"hall_origin":"https://builders.amazonprimea.com"},{"origin":"https://demo.cloudbongos.com","name":"Demo","tagline":"A live demo instance on Cloud Bongos.","art_url":null,"type":null,"created_at":"2026-07-14T19:16:38.160Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null},{"origin":"https://emersonian-circles.cloudbongos.com","name":"Emersonian Circles","tagline":"An independent project built on Cloud Bongos.","art_url":null,"type":null,"created_at":"2026-07-25T02:21:55.641Z","builders":1,"tags":[],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":[],"shipped_30d":30,"shipped_30d_capped":true,"hall_origin":null}]}
1
+ {"projects":[{"origin":"https://amazonprimea.com","name":"Off the Boats","tagline":"A singular shop, and a broader videogame in a persistent ancient Greek world that collides with modern day commerce & products.","art_url":"/assets/world/r2-chrome-blob-one.jpg","type":"game","template":"bead-wash","created_at":"2026-07-15T22:12:57.732Z","builders":11,"tags":["art-pipeline","character-anim","demo","game"],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":["https://avatars.githubusercontent.com/u/283020391?v=4","https://avatars.githubusercontent.com/u/64553469?v=4","https://avatars.githubusercontent.com/u/290184380?v=4","https://avatars.githubusercontent.com/u/295990394?v=4","https://avatars.githubusercontent.com/u/35337514?v=4"],"shipped_30d":0,"shipped_30d_capped":false,"hall_origin":"https://builders.amazonprimea.com"},{"origin":"https://demo.cloudbongos.com","name":"Demo","tagline":"A live demo instance on Cloud Bongos.","art_url":null,"type":null,"template":null,"created_at":"2026-07-14T19:16:38.160Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null},{"origin":"https://emersonian-circles.cloudbongos.com","name":"Emersonian Circles","tagline":"An independent project built on Cloud Bongos.","art_url":null,"type":null,"template":null,"created_at":"2026-07-25T02:21:55.641Z","builders":1,"tags":[],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":[],"shipped_30d":30,"shipped_30d_capped":true,"hall_origin":null}]}
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.406",
3
+ "version": "1.19.408",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.406",
9
+ "version": "1.19.408",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.406",
3
+ "version": "1.19.408",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",