@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.
- package/.bongos-core.json +41 -26
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0201-cross-project-application-vouched-row-live-record.md +112 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +3 -0
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +172 -172
- package/docs/copy-registry.json +219 -219
- package/docs/design/mocks/hub/planets.html +232 -0
- package/docs/design/projects-hub-direction-v2.md +10 -0
- package/modules/platform-identity/migrations/platform_identity_012_project_template.sql +26 -0
- package/modules/platform-identity/platform-identity.js +18 -8
- package/modules/platform-identity/routes/projects.js +4 -0
- package/modules/platform-identity/tests/platform-identity.mjs +29 -0
- package/modules/ui-design/kit/fixtures/featured.json +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +107 -68
- package/public-landing/projects.html +40 -40
- package/public-landing/projects.states.json +1 -1
- package/src/module-api.js +14 -19
- package/tests/projects_hub.mjs +109 -0
|
@@ -69,7 +69,6 @@
|
|
|
69
69
|
/* flat, not a gradient to white: one ground from the top bar to the footer.
|
|
70
70
|
A fade-out at the footer would reintroduce the fog. */
|
|
71
71
|
--sky-bg:var(--bg);
|
|
72
|
-
--star-display:none;
|
|
73
72
|
--chrome-ink:var(--ink);
|
|
74
73
|
--chrome-ink-dim:var(--ink-soft);
|
|
75
74
|
--chrome-accent:var(--accent-deep);
|
|
@@ -109,7 +108,6 @@
|
|
|
109
108
|
--on-ok:#0b2e18;
|
|
110
109
|
--page-bg:var(--bg);
|
|
111
110
|
--sky-bg:var(--bg);
|
|
112
|
-
--star-display:block;
|
|
113
111
|
--chrome-ink:var(--ink);
|
|
114
112
|
--chrome-ink-dim:var(--ink-soft);
|
|
115
113
|
--chrome-accent:var(--accent-deep);
|
|
@@ -132,6 +130,7 @@
|
|
|
132
130
|
--label-space:.1em;
|
|
133
131
|
/* the view masthead's object (task 1003332): dark drops the contact shadow */
|
|
134
132
|
--mast-shadow:none;
|
|
133
|
+
--orb-shadow:opacity(1);
|
|
135
134
|
color-scheme:dark;
|
|
136
135
|
}
|
|
137
136
|
@media (prefers-color-scheme: dark){
|
|
@@ -146,7 +145,6 @@
|
|
|
146
145
|
--on-ok:#0b2e18;
|
|
147
146
|
--page-bg:var(--bg);
|
|
148
147
|
--sky-bg:var(--bg);
|
|
149
|
-
--star-display:block;
|
|
150
148
|
--chrome-ink:var(--ink);
|
|
151
149
|
--chrome-ink-dim:var(--ink-soft);
|
|
152
150
|
--chrome-accent:var(--accent-deep);
|
|
@@ -169,6 +167,7 @@
|
|
|
169
167
|
--label-space:.1em;
|
|
170
168
|
/* the view masthead's object (task 1003332): dark drops the contact shadow */
|
|
171
169
|
--mast-shadow:none;
|
|
170
|
+
--orb-shadow:opacity(1);
|
|
172
171
|
color-scheme:dark;
|
|
173
172
|
}
|
|
174
173
|
}
|
|
@@ -259,7 +258,16 @@
|
|
|
259
258
|
Root-absolute path: a url() in a custom property resolves against the
|
|
260
259
|
DOCUMENT. Light gives the object the world's one contact shadow. */
|
|
261
260
|
--plate-set-cut:url(/assets/world/r2-chrome-blobs-set-cut.webp);
|
|
261
|
+
/* the sky's planets take the world's other two whole objects (task 1003319):
|
|
262
|
+
the round blob and the bongo pair, alpha cutouts in both modes for the
|
|
263
|
+
masthead's reason. */
|
|
264
|
+
--plate-bead-cut:url(/assets/world/r2-chrome-blob-one-cut.webp);
|
|
265
|
+
--plate-bongos-cut:url(/assets/world/r2-bongos-chrome-a-cut.webp);
|
|
262
266
|
--mast-shadow:drop-shadow(0 26px 30px color-mix(in srgb,var(--ink) 20%,transparent));
|
|
267
|
+
/* the planet's contact shadow, scaled to a 60-130px object. Dark's twin is
|
|
268
|
+
a NO-OP filter (opacity(1), not none) so the search spotlight's
|
|
269
|
+
drop-shadow can compose after it in one filter list. */
|
|
270
|
+
--orb-shadow:drop-shadow(0 12px 14px color-mix(in srgb,var(--ink) 22%,transparent));
|
|
263
271
|
/* the footer's measured height per width — the sky is 100vh minus the bar and
|
|
264
272
|
this, so the map fits the viewport with no scrollbar (T2's rule); the
|
|
265
273
|
toolbar row that used to be in that sum is gone (task 1003300) */
|
|
@@ -287,12 +295,11 @@
|
|
|
287
295
|
html{scroll-behavior:smooth;}
|
|
288
296
|
body{font-family:var(--font-body);color:var(--ink);background:var(--page-bg);min-height:100vh;-webkit-font-smoothing:antialiased;overflow-x:hidden;}
|
|
289
297
|
|
|
290
|
-
/* ── the sky:
|
|
291
|
-
|
|
298
|
+
/* ── the sky: the page's fixed ground layer, the page ground in both modes.
|
|
299
|
+
Nothing is drawn on it: the twinkling stars left with task 1003319 (the world
|
|
300
|
+
draws no atmosphere, and a 3.2s loop read as a pulse). Fixed layer, never
|
|
292
301
|
fixed-attachment on body. ── */
|
|
293
302
|
.sky{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden;background:var(--sky-bg);}
|
|
294
|
-
.star{position:absolute;display:var(--star-display);border-radius:50%;background:#fff;box-shadow:0 0 3px rgba(255,255,255,.5);animation:twinkle 3.2s ease-in-out infinite;}
|
|
295
|
-
@keyframes twinkle{0%,100%{opacity:.22}50%{opacity:.95}}
|
|
296
303
|
|
|
297
304
|
/* ── header: mode-aware chrome (ink on sunrise, white + glow in space) ── */
|
|
298
305
|
.top{position:relative;z-index:2;display:flex;align-items:center;justify-content:space-between;padding:20px clamp(18px,4vw,40px);}
|
|
@@ -721,55 +728,88 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
721
728
|
.qWrap.is-open .qCount{display:none;}
|
|
722
729
|
}
|
|
723
730
|
|
|
724
|
-
/* ── the
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
731
|
+
/* ── THE FIELD: the void (task 1003319; the owner's ask at the hub pick,
|
|
732
|
+
docs/design/projects-hub-direction-v2.md §7, the round in §11). Nothing is
|
|
733
|
+
drawn on the ground — no grid, no stars: DESIGN.md's world draws no
|
|
734
|
+
atmosphere, and its depth comes from the rendered objects themselves. The
|
|
735
|
+
ground is the page ground in both modes (true black / paper), closed by the
|
|
736
|
+
bar's hairline above and the footer's below. Where things sit is still the
|
|
737
|
+
slot layout; how the sky moves is task 1002762's camera. Component-scoped
|
|
738
|
+
custom properties only — a page-local or component `:root` would outrank
|
|
739
|
+
the branding pack spliced after this file. ── */
|
|
729
740
|
.fieldWrap{padding:0;max-width:none;}
|
|
730
|
-
.field{position:relative;height:calc(100vh - var(--top-h) - var(--foot-h));min-height:340px;overflow:hidden;
|
|
731
|
-
--grid-ink:color-mix(in srgb,var(--rule) 92%,transparent);}
|
|
741
|
+
.field{position:relative;height:calc(100vh - var(--top-h) - var(--foot-h));min-height:340px;overflow:hidden;}
|
|
732
742
|
.field .ground{position:absolute;inset:0;background:var(--sky-bg);}
|
|
733
|
-
/* the gravity sheet, flattened: two gradients under a radial mask */
|
|
734
|
-
.field .ground::after{content:"";position:absolute;inset:-10%;
|
|
735
|
-
background-image:linear-gradient(var(--grid-ink) 1px,transparent 1px),
|
|
736
|
-
linear-gradient(90deg,var(--grid-ink) 1px,transparent 1px);
|
|
737
|
-
background-size:74px 74px;
|
|
738
|
-
-webkit-mask-image:radial-gradient(ellipse 62% 58% at 50% 50%,#000 12%,transparent 78%);
|
|
739
|
-
mask-image:radial-gradient(ellipse 62% 58% at 50% 50%,#000 12%,transparent 78%);}
|
|
740
|
-
.field .stars{position:absolute;inset:0;display:var(--star-display);}
|
|
741
|
-
.field .stars i{position:absolute;width:2px;height:2px;border-radius:50%;background:#fff;opacity:.55;}
|
|
742
743
|
|
|
743
|
-
/* a project rendered as
|
|
744
|
-
|
|
744
|
+
/* ── THE PLANET: a project rendered as one of the world's three whole chrome
|
|
745
|
+
objects in the project's accent (task 1003319; DESIGN.md "Project orbs" and
|
|
746
|
+
"Planet templates"). The orb's box is --d wide (the physics' diameter) by
|
|
747
|
+
the object's own aspect; the object is the disc's ::before — a colour layer
|
|
748
|
+
over the plate, both clipped by the plate's alpha as a MASK, so the accent
|
|
749
|
+
never paints the box. SHAPE (data-shape) is a plate + one rectangle on the
|
|
750
|
+
landing's crop maths; STYLE (data-style) is how the accent lands: tint = a
|
|
751
|
+
`color` blend (the accent's hue and chroma, the chrome's light), wash =
|
|
752
|
+
`overlay` (the chrome lit by the accent, its iridescence kept), chrome =
|
|
753
|
+
the plate as rendered. Colour comes from tokens, never a literal, so every
|
|
754
|
+
accent follows a re-theme. The relationship ring is a circle BEHIND the
|
|
755
|
+
object (the rail dot's grammar), never a box-shadow: a mask clips a shadow
|
|
756
|
+
away, and the world has no shadow vocabulary for UI. ── */
|
|
745
757
|
.worldOrb{position:absolute;transform:translate(-50%,-50%);display:flex;flex-direction:column;
|
|
746
|
-
align-items:center;gap:
|
|
747
|
-
.worldOrb .disc{
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
.worldOrb .disc::
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
.
|
|
765
|
-
|
|
766
|
-
.
|
|
767
|
-
|
|
768
|
-
.worldOrb[data-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
758
|
+
align-items:center;gap:10px;cursor:pointer;background:none;border:none;padding:0;text-decoration:none;}
|
|
759
|
+
.worldOrb .disc{--dd:calc(var(--d,96px) * var(--orb-scale,1));width:var(--dd);aspect-ratio:var(--tpl-ar,1);position:relative;isolation:isolate;
|
|
760
|
+
filter:var(--orb-shadow);transition:translate .4s cubic-bezier(.16,1,.3,1);}
|
|
761
|
+
/* the landing's hover: the object rises, 400ms on the world's exponential ease-out */
|
|
762
|
+
.worldOrb:hover .disc{translate:0 -6px;}
|
|
763
|
+
.worldOrb .disc::before{content:"";position:absolute;inset:0;
|
|
764
|
+
background-image:linear-gradient(var(--tpl-ink),var(--tpl-ink)),var(--tpl-src);
|
|
765
|
+
background-repeat:no-repeat;
|
|
766
|
+
background-size:100% 100%,calc(var(--dd) * var(--tpl-zoom)) auto;
|
|
767
|
+
background-position:0 0,calc(var(--dd) * var(--tpl-px)) calc(var(--dd) * var(--tpl-py));
|
|
768
|
+
background-blend-mode:var(--tpl-blend),normal;
|
|
769
|
+
-webkit-mask-image:var(--tpl-src);mask-image:var(--tpl-src);
|
|
770
|
+
-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
|
|
771
|
+
-webkit-mask-size:calc(var(--dd) * var(--tpl-zoom)) auto;mask-size:calc(var(--dd) * var(--tpl-zoom)) auto;
|
|
772
|
+
-webkit-mask-position:calc(var(--dd) * var(--tpl-px)) calc(var(--dd) * var(--tpl-py));
|
|
773
|
+
mask-position:calc(var(--dd) * var(--tpl-px)) calc(var(--dd) * var(--tpl-py));}
|
|
774
|
+
/* the shapes — the same rectangles the landing's .o1/.o2/.o3 declare
|
|
775
|
+
(tests/projects_hub.mjs pins the numbers equal):
|
|
776
|
+
blob r2-chrome-blob-one u[.14,.865] v[.172,.808] the round blob, whole
|
|
777
|
+
pair r2-bongos-chrome-a the whole plate the chrome pair
|
|
778
|
+
bead r2-chrome-blobs-set u[.810,.955] v[.556,.840] the set's small bead */
|
|
779
|
+
.worldOrb[data-shape="blob"]{--tpl-src:var(--plate-bead-cut);--tpl-ar:1.13994;--tpl-zoom:1.37931;--tpl-px:-.19310;--tpl-py:-.23724;}
|
|
780
|
+
.worldOrb[data-shape="pair"]{--tpl-src:var(--plate-bongos-cut);--tpl-ar:1;--tpl-zoom:1;--tpl-px:0;--tpl-py:0;}
|
|
781
|
+
.worldOrb[data-shape="bead"]{--tpl-src:var(--plate-set-cut);--tpl-ar:.91476;--tpl-zoom:6.89655;--tpl-px:-5.58621;--tpl-py:-2.14014;}
|
|
782
|
+
/* the styles */
|
|
783
|
+
.worldOrb[data-style="tint"]{--tpl-ink:var(--acc);--tpl-blend:color;}
|
|
784
|
+
.worldOrb[data-style="wash"]{--tpl-ink:var(--acc);--tpl-blend:overlay;}
|
|
785
|
+
.worldOrb[data-style="chrome"]{--tpl-ink:transparent;--tpl-blend:normal;}
|
|
786
|
+
/* the relationship ring: yours takes the accent (the one live thing about the
|
|
787
|
+
orb); building there is a hairline of it */
|
|
788
|
+
.worldOrb[data-rel="owner"] .disc::after,.worldOrb[data-rel="builder"] .disc::after{content:"";position:absolute;
|
|
789
|
+
left:50%;top:50%;width:calc(100% + 18px);aspect-ratio:1;translate:-50% -50%;border-radius:50%;z-index:-1;}
|
|
790
|
+
.worldOrb[data-rel="owner"] .disc::after{border:1.5px solid var(--accent);}
|
|
791
|
+
.worldOrb[data-rel="builder"] .disc::after{border:1px solid color-mix(in srgb,var(--accent) 55%,transparent);}
|
|
792
|
+
/* the labels, in the world's type: the name in the ledger's title voice, the
|
|
793
|
+
sub-line as a label; the halo is the world's black one, dark only */
|
|
794
|
+
.worldOrb .nm{font-family:var(--font-body);font-weight:500;font-size:15px;letter-spacing:-.015em;color:var(--ink);
|
|
795
|
+
text-shadow:var(--type-halo);white-space:nowrap;}
|
|
796
|
+
.worldOrb .sub{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);
|
|
797
|
+
text-transform:uppercase;color:var(--ink-faint);white-space:nowrap;}
|
|
798
|
+
/* the badge states a fact (yours / you build here), so it is a hairline pill
|
|
799
|
+
in the label face, not an accent fill (the One Warm Thing Rule); z-index 3
|
|
800
|
+
keeps it over the object it labels */
|
|
801
|
+
.worldOrb .badge{position:absolute;top:-14px;left:50%;transform:translateX(-50%);z-index:3;
|
|
802
|
+
font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;
|
|
803
|
+
font-weight:400;padding:3px 10px;border-radius:999px;white-space:nowrap;
|
|
804
|
+
background:var(--bg);color:var(--ink-faint);border:1px solid var(--rule);}
|
|
805
|
+
.worldOrb[data-rel="owner"] .badge{color:var(--ink);border-color:var(--ink-faint);}
|
|
772
806
|
.worldOrb .chip{transform:scale(.88);white-space:nowrap;}
|
|
807
|
+
/* narrow: the drawn object is four fifths of its box while the physics keeps
|
|
808
|
+
the whole box for spacing — at 390 the labels and the badges need the air
|
|
809
|
+
more than the object needs the size */
|
|
810
|
+
@media (max-width:900px){
|
|
811
|
+
.field{--orb-scale:.8;}
|
|
812
|
+
}
|
|
773
813
|
|
|
774
814
|
/* what the only-mine filter shows when you are connected to nothing. Not a
|
|
775
815
|
hypothetical — GET /my-projects returns { owned: [], building: [] } for the
|
|
@@ -777,16 +817,15 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
777
817
|
.fieldEmpty{position:absolute;inset:0;display:none;flex-direction:column;align-items:center;
|
|
778
818
|
justify-content:center;gap:14px;text-align:center;padding:26px;z-index:4;}
|
|
779
819
|
.field.isEmpty .fieldEmpty{display:flex;}
|
|
780
|
-
.fieldEmpty p{font-size:
|
|
781
|
-
|
|
782
|
-
.fieldEmpty .sub{font-size:13.5px;font-weight:600;color:var(--glass-ink);}
|
|
820
|
+
.fieldEmpty p{font-size:15.5px;font-weight:400;color:var(--ink);max-width:44ch;line-height:1.5;}
|
|
821
|
+
.fieldEmpty .sub{font-size:14px;font-weight:400;color:var(--ink-soft);}
|
|
783
822
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
823
|
+
/* the hint is one label line on the ground — no pill, no accent: it is
|
|
824
|
+
neither pressable nor live (task 1003319) */
|
|
825
|
+
.hintbar{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);display:flex;gap:18px;
|
|
826
|
+
flex-wrap:wrap;justify-content:center;font-family:var(--label-face);font-size:var(--label-size);
|
|
827
|
+
letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);max-width:92%;}
|
|
828
|
+
.hintbar kbd{font-family:inherit;font-size:inherit;color:var(--ink-soft);}
|
|
790
829
|
|
|
791
830
|
/* ── THE TABLE IN THE WORLD (task 1003332; owner's review of the shipped chrome).
|
|
792
831
|
The table is a VIEW of the same world as the sky, not a panel beside it: a
|
|
@@ -877,16 +916,16 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
877
916
|
/* a dimmed orb is backdrop — no pointer, no focus (the JS drops it from the
|
|
878
917
|
tab order); at .25 opacity its label could not carry interaction honestly */
|
|
879
918
|
.worldOrb.dim{opacity:.25;filter:saturate(.35);pointer-events:none;}
|
|
880
|
-
/* the spotlight
|
|
881
|
-
|
|
882
|
-
box-shadow
|
|
883
|
-
|
|
884
|
-
|
|
919
|
+
/* the spotlight is a drop-shadow in the accent on the disc's own filter — a
|
|
920
|
+
glow that follows the silhouette (the object is a mask, so an outline or a
|
|
921
|
+
box-shadow would be clipped away). Composed after the contact shadow, which
|
|
922
|
+
dark states as a no-op filter for exactly this. */
|
|
923
|
+
.worldOrb.match .disc{filter:var(--orb-shadow) drop-shadow(0 0 16px color-mix(in srgb,var(--acc,var(--accent)) 60%,transparent));}
|
|
885
924
|
.field .hoverCard{position:absolute;z-index:6;pointer-events:none;display:flex;flex-direction:column;gap:2px;
|
|
886
|
-
background:var(--bg-card);border:
|
|
925
|
+
background:var(--bg-card);border:1px solid var(--rule);border-radius:var(--radius-sm);
|
|
887
926
|
padding:9px 13px;max-width:240px;}
|
|
888
|
-
.field .hoverCard .hn{font-family:var(--font-
|
|
889
|
-
.field .hoverCard .ht{font-size:
|
|
927
|
+
.field .hoverCard .hn{font-family:var(--font-body);font-weight:500;font-size:14px;letter-spacing:-.015em;color:var(--ink);white-space:nowrap;}
|
|
928
|
+
.field .hoverCard .ht{font-size:12.5px;color:var(--ink-soft);font-weight:400;}
|
|
890
929
|
|
|
891
930
|
/* ── the project card ──
|
|
892
931
|
An overlay INSIDE the map, not a destination: opening a project must not cost
|
|
@@ -327,23 +327,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
327
327
|
</style>
|
|
328
328
|
</head>
|
|
329
329
|
<body>
|
|
330
|
-
<div class="sky" aria-hidden="true">
|
|
331
|
-
<span class="star" style="top:2%;left:12%;width:2px;height:2px;"></span>
|
|
332
|
-
<span class="star" style="top:5%;left:28%;width:1.5px;height:1.5px;animation-delay:.7s;"></span>
|
|
333
|
-
<span class="star" style="top:3%;left:47%;width:2px;height:2px;animation-delay:1.4s;"></span>
|
|
334
|
-
<span class="star" style="top:8%;left:63%;width:1.5px;height:1.5px;animation-delay:.3s;"></span>
|
|
335
|
-
<span class="star" style="top:4%;left:78%;width:2.5px;height:2.5px;animation-delay:2s;"></span>
|
|
336
|
-
<span class="star" style="top:10%;left:88%;width:1.5px;height:1.5px;animation-delay:1s;"></span>
|
|
337
|
-
<span class="star" style="top:14%;left:20%;width:1.5px;height:1.5px;animation-delay:1.8s;"></span>
|
|
338
|
-
<span class="star" style="top:22%;left:55%;width:2px;height:2px;animation-delay:.5s;"></span>
|
|
339
|
-
<span class="star" style="top:34%;left:36%;width:1.5px;height:1.5px;animation-delay:2.4s;"></span>
|
|
340
|
-
<span class="star" style="top:30%;left:78%;width:1.5px;height:1.5px;animation-delay:1.2s;"></span>
|
|
341
|
-
<span class="star" style="top:48%;left:8%;width:2px;height:2px;animation-delay:.9s;"></span>
|
|
342
|
-
<span class="star" style="top:55%;left:90%;width:1.5px;height:1.5px;animation-delay:1.6s;"></span>
|
|
343
|
-
<span class="star" style="top:68%;left:24%;width:1.5px;height:1.5px;animation-delay:2.1s;"></span>
|
|
344
|
-
<span class="star" style="top:76%;left:64%;width:2px;height:2px;animation-delay:.4s;"></span>
|
|
345
|
-
<span class="star" style="top:88%;left:42%;width:1.5px;height:1.5px;animation-delay:1.9s;"></span>
|
|
346
|
-
</div>
|
|
330
|
+
<div class="sky" aria-hidden="true"></div>
|
|
347
331
|
|
|
348
332
|
<!-- ══ THE BAR ═══════════════════════════════════════════════════════════════
|
|
349
333
|
A header of the PAGE, not of the orb field, so it never drifts, scales or
|
|
@@ -471,7 +455,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
471
455
|
<div class="fieldWrap" id="sub-sky-body">
|
|
472
456
|
<div class="field" id="orbField">
|
|
473
457
|
<div class="ground"></div>
|
|
474
|
-
<div class="stars" id="mapStars" aria-hidden="true"></div>
|
|
475
458
|
<!-- The camera (task 1002762). Pan and zoom are ONE transform on this
|
|
476
459
|
layer; orbs write only their own physics delta, so the two compose
|
|
477
460
|
for free and the ground, stars, empty state and hintbar stay put.
|
|
@@ -479,12 +462,13 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
479
462
|
sentinel spans, and a tag opened inside one would leave the child
|
|
480
463
|
instance an unbalanced tree. -->
|
|
481
464
|
<div class="skyCam" id="skyCam">
|
|
482
|
-
<!-- The platform's own orb
|
|
465
|
+
<!-- The platform's own orb: the bongo pair in chrome, the platform's own
|
|
466
|
+
mark (task 1003319). Static markup, not JS-built, precisely so the
|
|
483
467
|
apex-only sentinels can remove it: a child instance has no "this
|
|
484
468
|
platform" to point at, and stripApexOnly() is a string transform that
|
|
485
469
|
cannot reach anything a script renders later. -->
|
|
486
470
|
<!--apex-only:start-->
|
|
487
|
-
<a class="worldOrb" id="selfOrb" href="/" style="--acc:var(--accent);--d:
|
|
471
|
+
<a class="worldOrb" id="selfOrb" href="/" data-shape="pair" data-style="chrome" data-template="pair-chrome" style="--acc:var(--accent);--d:132px;left:50%;top:44%">
|
|
488
472
|
<span class="disc"></span>
|
|
489
473
|
<span class="nm" data-brand-product-inline>Cloud Bongos</span>
|
|
490
474
|
<span class="sub">this platform</span>
|
|
@@ -1282,9 +1266,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1282
1266
|
The orbs are placed on fixed slots: where things sit is this pass; how the
|
|
1283
1267
|
sky moves is goal 1000031. ══ */
|
|
1284
1268
|
|
|
1269
|
+
/* the third number is the orb's width (--d, the physics' diameter). A
|
|
1270
|
+
cutout's silhouette reads a quarter smaller than a filled disc of the
|
|
1271
|
+
same box, so the slots grew by that with the planets (task 1003319). */
|
|
1285
1272
|
var ORB_SLOTS = [
|
|
1286
|
-
[22, 30,
|
|
1287
|
-
[13, 58,
|
|
1273
|
+
[22, 30, 92], [78, 66, 75], [33, 74, 70], [70, 26, 80],
|
|
1274
|
+
[13, 58, 65], [88, 42, 62], [50, 80, 68], [62, 48, 72]
|
|
1288
1275
|
];
|
|
1289
1276
|
/* ONE accent palette for every orb on the page — the map's spheres and the
|
|
1290
1277
|
manage cards' — so a project reads the same colour in both views.
|
|
@@ -1294,6 +1281,16 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1294
1281
|
deriving it from contract tokens is what makes it survive a re-brand. */
|
|
1295
1282
|
var ORB_ACCENTS = ['var(--sea)', 'var(--terracotta)', 'var(--gold-soft)', 'var(--sun-lit)', 'var(--accent)'];
|
|
1296
1283
|
var ORB_GLOWS = ORB_ACCENTS.map(function (t) { return 'color-mix(in srgb,' + t + ' 45%,transparent)'; });
|
|
1284
|
+
/* THE PLANET TEMPLATES (task 1003319). A project's orb is one of the world's
|
|
1285
|
+
three whole chrome objects (blob / pair / bead, on the landing's own crop
|
|
1286
|
+
maths) in the project's accent, styled tint (the accent replaces the
|
|
1287
|
+
chrome's colour), wash (the accent lights it) or chrome (as rendered). A
|
|
1288
|
+
curated `template` on the catalog row wins; otherwise the same canonical
|
|
1289
|
+
walk that assigns the accent assigns the template, and the order here IS
|
|
1290
|
+
the walk: consecutive positions change both the shape and the style, and
|
|
1291
|
+
nine templates against five accents repeat only every forty-five projects.
|
|
1292
|
+
Mirrors platform-identity's PROJECT_TEMPLATES (the DB CHECK). */
|
|
1293
|
+
var ORB_TEMPLATES = ['blob-tint', 'pair-wash', 'bead-chrome', 'blob-wash', 'pair-chrome', 'bead-tint', 'blob-chrome', 'pair-tint', 'bead-wash'];
|
|
1297
1294
|
var catalog = [];
|
|
1298
1295
|
var mineOrigins = {}; /* normalized host → 'owner' | 'builder' */
|
|
1299
1296
|
var ownedInstances = []; /* the viewer's OWN provisioning rows (never torn_down) */
|
|
@@ -1329,16 +1326,26 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1329
1326
|
function assignAccents() {
|
|
1330
1327
|
catalog.slice()
|
|
1331
1328
|
.sort(function (a, b) { return String(a.origin).localeCompare(String(b.origin)); })
|
|
1332
|
-
.forEach(function (p, i) {
|
|
1329
|
+
.forEach(function (p, i) {
|
|
1330
|
+
p._acc = ORB_ACCENTS[i % ORB_ACCENTS.length];
|
|
1331
|
+
p._tpl = ORB_TEMPLATES[i % ORB_TEMPLATES.length];
|
|
1332
|
+
});
|
|
1333
1333
|
}
|
|
1334
1334
|
function accentOf(p) { return p._acc || ORB_ACCENTS[0]; }
|
|
1335
|
+
/* a curated template wins over the walk; an unknown name is not a choice */
|
|
1336
|
+
function templateOf(p) {
|
|
1337
|
+
return ORB_TEMPLATES.indexOf(p.template) !== -1 ? p.template : (p._tpl || ORB_TEMPLATES[0]);
|
|
1338
|
+
}
|
|
1335
1339
|
/* the manage cards' accent rule — one project, one colour, in the sky AND
|
|
1336
1340
|
the list. Shared with orbFor(), which used to carry this hash inline. */
|
|
1337
|
-
function
|
|
1341
|
+
function slugHash(slug) {
|
|
1338
1342
|
var h = 0, s = String(slug || '');
|
|
1339
1343
|
for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
|
1340
|
-
return h
|
|
1344
|
+
return h;
|
|
1341
1345
|
}
|
|
1346
|
+
function slugAccent(slug) { return slugHash(slug) % ORB_ACCENTS.length; }
|
|
1347
|
+
/* an owned-only entry has no catalog row to carry a template: its slug walks */
|
|
1348
|
+
function slugTemplate(slug) { return ORB_TEMPLATES[slugHash(slug) % ORB_TEMPLATES.length]; }
|
|
1342
1349
|
function relOf(p) {
|
|
1343
1350
|
/* an instance the viewer provisioned is a server-resolved authority too —
|
|
1344
1351
|
own-scoped by owner_builder_id, no client inference (direction §2
|
|
@@ -1431,6 +1438,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1431
1438
|
a.setAttribute('data-generated', '');
|
|
1432
1439
|
var rel = relOf(p);
|
|
1433
1440
|
a.setAttribute('data-rel', rel);
|
|
1441
|
+
/* the planet: shape and style as data attributes, cosmos.css does the rest */
|
|
1442
|
+
var tpl = templateOf(p), tplParts = tpl.split('-');
|
|
1443
|
+
a.setAttribute('data-template', tpl);
|
|
1444
|
+
a.setAttribute('data-shape', tplParts[0]);
|
|
1445
|
+
a.setAttribute('data-style', tplParts[1]);
|
|
1434
1446
|
var inst = p._inst;
|
|
1435
1447
|
var href = safeHttpUrl(p.origin);
|
|
1436
1448
|
if (inst) {
|
|
@@ -1533,7 +1545,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1533
1545
|
name: inst.slug, tagline: inst.domain || '',
|
|
1534
1546
|
origin: inst.domain ? 'https://' + inst.domain : '',
|
|
1535
1547
|
builders: null, tags: null, type: inst.type || null,
|
|
1536
|
-
_inst: inst, _acc: ORB_ACCENTS[slugAccent(inst.slug)]
|
|
1548
|
+
_inst: inst, _acc: ORB_ACCENTS[slugAccent(inst.slug)], _tpl: slugTemplate(inst.slug)
|
|
1537
1549
|
});
|
|
1538
1550
|
});
|
|
1539
1551
|
var list = browse.filter(function (p) {
|
|
@@ -2213,7 +2225,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2213
2225
|
that must not interpenetrate — sits above it; collisions measure from
|
|
2214
2226
|
the disc's own center */
|
|
2215
2227
|
var disc = el.querySelector('.disc');
|
|
2216
|
-
|
|
2228
|
+
/* a planet's box is not square (task 1003319): its own height, not r */
|
|
2229
|
+
var discOff = disc ? (disc.offsetTop + disc.offsetHeight / 2) - el.offsetHeight / 2 : 0;
|
|
2217
2230
|
/* reduced motion IS the mock: no carry-over, every orb sits exactly on
|
|
2218
2231
|
its slot (C9) — carrying frozen positions across a re-sort would
|
|
2219
2232
|
leave zero-motion orbs parked off their own anchors forever */
|
|
@@ -2491,19 +2504,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2491
2504
|
} else { stopSky(); hideHover(); }
|
|
2492
2505
|
}
|
|
2493
2506
|
|
|
2494
|
-
/* the starfield only paints where --star-display is block (dark mode) */
|
|
2495
|
-
(function stars() {
|
|
2496
|
-
var frag = document.createDocumentFragment();
|
|
2497
|
-
for (var i = 0; i < 90; i++) {
|
|
2498
|
-
var s = document.createElement('i');
|
|
2499
|
-
s.style.left = ((i * 37 + 11) % 100) + '%';
|
|
2500
|
-
s.style.top = ((i * 61 + 7) % 100) + '%';
|
|
2501
|
-
s.style.opacity = (0.25 + ((i * 13) % 50) / 100).toFixed(2);
|
|
2502
|
-
frag.appendChild(s);
|
|
2503
|
-
}
|
|
2504
|
-
$('mapStars').appendChild(frag);
|
|
2505
|
-
})();
|
|
2506
|
-
|
|
2507
2507
|
/* ══ THE CREATE WIZARD (task 1002763) — absorbed whole from /create, which
|
|
2508
2508
|
now 302s here. An inner scope so its ~40 helpers stay out of the hub's
|
|
2509
2509
|
namespace; it shares the hub's $, esc, API and `me`, so there is one
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"ignoreControls": [".skip", ".vh"],
|
|
9
9
|
"states": {
|
|
10
10
|
"out-map": { "auth": false, "expect": { "expanded": false } },
|
|
11
|
-
"in-map": { "auth": true, "expect": { "expanded": false } },
|
|
11
|
+
"in-map": { "auth": true, "expect": { "expanded": false, "count": [["#orbField .worldOrb[data-shape][data-style]", 4]], "attr": [["#selfOrb", "data-template", "pair-chrome"], ["#orbField a.worldOrb[data-origin=\"https://amazonprimea.com\"]", "data-template", "bead-wash"], ["#orbField a.worldOrb[data-origin=\"https://demo.cloudbongos.com\"]", "data-shape", "pair"], ["#orbField a.worldOrb[data-origin=\"https://emersonian-circles.cloudbongos.com\"]", "data-template", "bead-chrome"]] } },
|
|
12
12
|
"in-map-projects": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 350]], "expect": { "expanded": true, "open": "#menuProjects" } },
|
|
13
13
|
"in-map-profile": { "auth": true, "actions": [["click", "#who"], ["wait", 350]], "expect": { "expanded": true, "open": "#menuProfile" } },
|
|
14
14
|
"in-map-kbd": { "auth": true, "actions": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 350], ["press", "ArrowDown"], ["wait", 120]], "expect": { "expanded": true, "activeText": "Table" } },
|
package/src/module-api.js
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
// src/module-api.js — THE PUBLISHED, SEMVER'D DOORWAY (ADR 0083 / design 01§3).
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// Adding to this surface is a MINOR semver bump (safe — existing modules keep
|
|
13
|
-
// working). Changing or removing an export is a MAJOR bump (breaking). Each
|
|
14
|
-
// module declares the `coreVersion` it supports; `bongos upgrade` (BV1.R50)
|
|
15
|
-
// pre-checks every enabled module against CORE_VERSION below.
|
|
16
|
-
//
|
|
17
|
-
// Introduced by the doorway task (BV1.R37 / task 1406). The seam registry is in
|
|
18
|
-
// src/module-seams.js (hardened in BV1.R39 / task 1408); the loader that mounts
|
|
19
|
-
// modules behind kernel-composed auth is BV1.R41 / task 1410.
|
|
2
|
+
// The ONLY core file a module may `require`: it re-exports the kernel capabilities a
|
|
3
|
+
// module legitimately needs; everything else in the core is off-limits. The one-way
|
|
4
|
+
// rule (fitness-enforced, BV1.R40): module ──require──▶ src/module-api.js ──▶ kernel;
|
|
5
|
+
// core ──✗──▶ module and module ──✗──▶ module are forbidden (use a seam).
|
|
6
|
+
// Adding an export is a MINOR semver bump (safe); changing or removing one is MAJOR.
|
|
7
|
+
// Each module declares the `coreVersion` it supports; `bongos upgrade` (BV1.R50)
|
|
8
|
+
// pre-checks every enabled module against CORE_VERSION below. Introduced by BV1.R37
|
|
9
|
+
// (task 1406); seams in src/module-seams.js (BV1.R39 / task 1408); the loader that
|
|
10
|
+
// mounts modules behind kernel-composed auth is BV1.R41 / task 1410.
|
|
20
11
|
|
|
21
12
|
const auth = require('./bongos/auth');
|
|
22
13
|
const util = require('node:util');
|
|
@@ -1252,8 +1243,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1252
1243
|
// landed since 1.19.404 with no explicit bump. run 33212035244. (task 1002620)
|
|
1253
1244
|
// 1.19.406 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1254
1245
|
// landed since 1.19.405 with no explicit bump. run 33213981964. (task 1002620)
|
|
1246
|
+
// 1.19.407 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1247
|
+
// landed since 1.19.406 with no explicit bump. run 33214451873. (task 1002620)
|
|
1248
|
+
// 1.19.408 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1249
|
+
// landed since 1.19.407 with no explicit bump. run 33216756796. (task 1002620)
|
|
1255
1250
|
// ---------------------------------------------------------------------------
|
|
1256
|
-
const CORE_VERSION = '1.19.
|
|
1251
|
+
const CORE_VERSION = '1.19.408'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1257
1252
|
|
|
1258
1253
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1259
1254
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -1399,3 +1399,112 @@ test('the joinability in-flight line shares the visibility section’s push pred
|
|
|
1399
1399
|
assert.match(SCRIPT, /\$\('mJoin'\)\.addEventListener\('click'[\s\S]{0,200}data-join-save[\s\S]{0,40}doJoinSave\(\)/,
|
|
1400
1400
|
'the Save control is wired to ITS save — a copy that left the listener on #mVis ships a dead button');
|
|
1401
1401
|
});
|
|
1402
|
+
|
|
1403
|
+
/* ── the planets and the field (task 1003319) — the map's orbs as the landing's
|
|
1404
|
+
chrome objects in the project's accent, a per-project template with a
|
|
1405
|
+
deterministic default walk, and the field brought to the world. ── */
|
|
1406
|
+
test('the planet templates: nine names, the walk changes shape and style each step, the catalog and the migration agree', () => {
|
|
1407
|
+
const list = (SCRIPT.match(/var ORB_TEMPLATES = \[([^\]]*)\]/) || [, ''])[1];
|
|
1408
|
+
const names = [...list.matchAll(/'([a-z-]+)'/g)].map((m) => m[1]);
|
|
1409
|
+
assert.equal(names.length, 9);
|
|
1410
|
+
assert.equal(new Set(names).size, 9, 'nine distinct templates');
|
|
1411
|
+
for (const t of names) assert.match(t, /^(blob|pair|bead)-(tint|wash|chrome)$/);
|
|
1412
|
+
for (let i = 1; i < names.length; i++) {
|
|
1413
|
+
const [s0, y0] = names[i - 1].split('-');
|
|
1414
|
+
const [s1, y1] = names[i].split('-');
|
|
1415
|
+
assert.ok(s0 !== s1 && y0 !== y1, `walk step ${i} changes both shape and style`);
|
|
1416
|
+
}
|
|
1417
|
+
const pi = fs.readFileSync(path.join(ROOT, 'modules', 'platform-identity', 'platform-identity.js'), 'utf8');
|
|
1418
|
+
const server = [...((pi.match(/const PROJECT_TEMPLATES = \[([\s\S]*?)\];/) || [, ''])[1]).matchAll(/'([a-z-]+)'/g)].map((m) => m[1]);
|
|
1419
|
+
assert.deepEqual(server, names, 'the hub and the catalog walk the same list in the same order');
|
|
1420
|
+
const mig = fs.readFileSync(path.join(ROOT, 'modules', 'platform-identity', 'migrations', 'platform_identity_012_project_template.sql'), 'utf8');
|
|
1421
|
+
for (const t of names) assert.ok(mig.includes(`'${t}'`), `migration 012 names ${t}`);
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
test('the walk assigns the template by canonical position beside the accent; a curated template wins; the platform is the pair in chrome', () => {
|
|
1425
|
+
assert.match(SCRIPT, /function assignAccents[\s\S]*?p\._acc = ORB_ACCENTS\[i % ORB_ACCENTS\.length\];\s*p\._tpl = ORB_TEMPLATES\[i % ORB_TEMPLATES\.length\];/,
|
|
1426
|
+
'one walk, two assignments');
|
|
1427
|
+
assert.match(SCRIPT, /function templateOf\(p\) \{\s*return ORB_TEMPLATES\.indexOf\(p\.template\) !== -1 \? p\.template : \(p\._tpl \|\| ORB_TEMPLATES\[0\]\);/,
|
|
1428
|
+
'a curated name wins; an unknown name is not a choice');
|
|
1429
|
+
assert.match(SCRIPT, /_acc: ORB_ACCENTS\[slugAccent\(inst\.slug\)\], _tpl: slugTemplate\(inst\.slug\)/, 'an owned-only entry walks by its slug');
|
|
1430
|
+
assert.match(SCRIPT, /var tpl = templateOf\(p\), tplParts = tpl\.split\('-'\);\s*a\.setAttribute\('data-template', tpl\);\s*a\.setAttribute\('data-shape', tplParts\[0\]\);\s*a\.setAttribute\('data-style', tplParts\[1\]\);/,
|
|
1431
|
+
'the orb carries its template as data attributes; the sheet does the rest');
|
|
1432
|
+
assert.match(HUB, /<a class="worldOrb" id="selfOrb" href="\/" data-shape="pair" data-style="chrome" data-template="pair-chrome"/, 'the platform is its own mark');
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
test("the planet is the landing's object on the landing's crop maths: the three rectangles are byte-equal across the two sheets", () => {
|
|
1436
|
+
const LANDING = fs.readFileSync(path.join(ROOT, 'public-landing', 'index.html'), 'utf8');
|
|
1437
|
+
const landingCrop = (cls) => {
|
|
1438
|
+
const m = LANDING.match(new RegExp(`\\.${cls}\\{[^}]*--bead-ar:([^;]+);--bead-zoom:([^;]+);--bead-px:([^;]+);--bead-py:([^;}]+)`));
|
|
1439
|
+
assert.ok(m, `the landing declares .${cls}`);
|
|
1440
|
+
return m.slice(1, 5).map((s) => s.trim());
|
|
1441
|
+
};
|
|
1442
|
+
const hubCrop = (shape) => {
|
|
1443
|
+
const m = COSMOS.match(new RegExp(`\\.worldOrb\\[data-shape="${shape}"\\]\\{--tpl-src:var\\((--plate-[a-z-]+)\\);--tpl-ar:([^;]+);--tpl-zoom:([^;]+);--tpl-px:([^;]+);--tpl-py:([^;}]+)`));
|
|
1444
|
+
assert.ok(m, `the hub declares the ${shape}`);
|
|
1445
|
+
return { src: m[1], crop: m.slice(2, 6).map((s) => s.trim()) };
|
|
1446
|
+
};
|
|
1447
|
+
assert.deepEqual(hubCrop('blob').crop, landingCrop('o1'));
|
|
1448
|
+
assert.equal(hubCrop('blob').src, '--plate-bead-cut');
|
|
1449
|
+
assert.deepEqual(hubCrop('pair').crop, landingCrop('o2'));
|
|
1450
|
+
assert.equal(hubCrop('pair').src, '--plate-bongos-cut');
|
|
1451
|
+
assert.deepEqual(hubCrop('bead').crop, landingCrop('o3'));
|
|
1452
|
+
assert.equal(hubCrop('bead').src, '--plate-set-cut');
|
|
1453
|
+
// the plates by root-absolute token in the hub's own sheet (a url() in a custom property resolves against the document)
|
|
1454
|
+
assert.match(COSMOS, /--plate-bead-cut:url\(\/assets\/world\/r2-chrome-blob-one-cut\.webp\);/);
|
|
1455
|
+
assert.match(COSMOS, /--plate-bongos-cut:url\(\/assets\/world\/r2-bongos-chrome-a-cut\.webp\);/);
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
test('the style is a blend on a masked colour layer; the accent never paints the box; the ring and the spotlight follow the silhouette', () => {
|
|
1459
|
+
assert.match(COSMOS, /\.worldOrb \.disc::before\{content:"";position:absolute;inset:0;\s*background-image:linear-gradient\(var\(--tpl-ink\),var\(--tpl-ink\)\),var\(--tpl-src\);/);
|
|
1460
|
+
assert.match(COSMOS, /background-blend-mode:var\(--tpl-blend\),normal;\s*-webkit-mask-image:var\(--tpl-src\);mask-image:var\(--tpl-src\);/, 'the mask is the plate itself');
|
|
1461
|
+
assert.match(COSMOS, /\.worldOrb\[data-style="tint"\]\{--tpl-ink:var\(--acc\);--tpl-blend:color;\}/);
|
|
1462
|
+
assert.match(COSMOS, /\.worldOrb\[data-style="wash"\]\{--tpl-ink:var\(--acc\);--tpl-blend:overlay;\}/);
|
|
1463
|
+
assert.match(COSMOS, /\.worldOrb\[data-style="chrome"\]\{--tpl-ink:transparent;--tpl-blend:normal;\}/);
|
|
1464
|
+
assert.match(COSMOS, /\.worldOrb \.disc\{--dd:calc\(var\(--d,96px\) \* var\(--orb-scale,1\)\);width:var\(--dd\);aspect-ratio:var\(--tpl-ar,1\);position:relative;isolation:isolate;\s*filter:var\(--orb-shadow\);/,
|
|
1465
|
+
"the box is --d (scaled) by the object's own aspect");
|
|
1466
|
+
assert.doesNotMatch(COSMOS, /var\(--d,96px\) \* var\(--tpl-/, 'every crop calc reads the scaled width');
|
|
1467
|
+
assert.match(COSMOS, /@media \(max-width:900px\)\{\s*\.field\{--orb-scale:\.8;\}/, 'narrow: the object is four fifths of its box; the physics keeps the box');
|
|
1468
|
+
assert.doesNotMatch(COSMOS, /\.worldOrb \.disc\{[^}]*radial-gradient/, 'no painted ball');
|
|
1469
|
+
assert.doesNotMatch(COSMOS, /\.worldOrb\[data-rel="owner"\] \.disc\{box-shadow/, 'a mask clips a box-shadow away');
|
|
1470
|
+
assert.match(COSMOS, /\.worldOrb\[data-rel="owner"\] \.disc::after\{border:1\.5px solid var\(--accent\);\}/, 'the ring is a circle behind the object');
|
|
1471
|
+
assert.match(COSMOS, /\.worldOrb\.match \.disc\{filter:var\(--orb-shadow\) drop-shadow\(/, 'the spotlight composes after the contact shadow');
|
|
1472
|
+
assert.doesNotMatch(COSMOS, /\.worldOrb\.match \.disc\{outline/);
|
|
1473
|
+
assert.match(COSMOS, /--orb-shadow:drop-shadow\(0 12px 14px color-mix\(in srgb,var\(--ink\) 22%,transparent\)\);/);
|
|
1474
|
+
assert.equal((COSMOS.match(/--orb-shadow:opacity\(1\);/g) || []).length, 2, 'dark states the no-op filter in both twins');
|
|
1475
|
+
assert.match(COSMOS, /\.worldOrb:hover \.disc\{translate:0 -6px;\}/, "the landing's hover rise");
|
|
1476
|
+
assert.match(SCRIPT, /var discOff = disc \? \(disc\.offsetTop \+ disc\.offsetHeight \/ 2\) - el\.offsetHeight \/ 2 : 0;/,
|
|
1477
|
+
"the physics measures the disc's own height: a planet's box is not square");
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
test("the field is the void: no drawn grid, no stars, no twinkle; the labels, the badge and the hint in the world's type", () => {
|
|
1481
|
+
assert.doesNotMatch(COSMOS, /--grid-ink/);
|
|
1482
|
+
assert.doesNotMatch(COSMOS, /\.field \.ground::after/);
|
|
1483
|
+
assert.doesNotMatch(COSMOS, /--star-display/);
|
|
1484
|
+
assert.doesNotMatch(COSMOS, /@keyframes twinkle/);
|
|
1485
|
+
assert.doesNotMatch(HUB, /class="star"/);
|
|
1486
|
+
assert.doesNotMatch(HUB, /id="mapStars"/);
|
|
1487
|
+
assert.doesNotMatch(SCRIPT, /function stars\(\)/);
|
|
1488
|
+
assert.match(HUB, /<div class="sky" aria-hidden="true"><\/div>/, 'the ground layer stays, empty');
|
|
1489
|
+
assert.match(COSMOS, /\.worldOrb \.nm\{font-family:var\(--font-body\);font-weight:500;font-size:15px;letter-spacing:-\.015em;color:var\(--ink\);\s*text-shadow:var\(--type-halo\);/,
|
|
1490
|
+
"the name in the ledger's title voice, the world's black halo");
|
|
1491
|
+
assert.match(COSMOS, /\.worldOrb \.sub\{font-family:var\(--label-face\);/);
|
|
1492
|
+
assert.match(COSMOS, /\.worldOrb \.badge\{[^}]*background:var\(--bg\);color:var\(--ink-faint\);border:1px solid var\(--rule\);\}/, 'a hairline pill');
|
|
1493
|
+
assert.doesNotMatch(COSMOS, /\.worldOrb \.badge\{[^}]*--gold-soft/);
|
|
1494
|
+
assert.doesNotMatch(COSMOS, /\.worldOrb\[data-rel="owner"\] \.badge\{background:var\(--accent\)/, 'a fact is not an accent fill (the One Warm Thing Rule)');
|
|
1495
|
+
assert.match(COSMOS, /\.hintbar\{[^}]*font-family:var\(--label-face\);/);
|
|
1496
|
+
assert.doesNotMatch(COSMOS, /\.hintbar\{[^}]*border-radius:999px/, 'no pill');
|
|
1497
|
+
assert.doesNotMatch(COSMOS, /\.hintbar kbd\{[^}]*--accent/, 'no accent on a hint');
|
|
1498
|
+
assert.doesNotMatch(COSMOS, /\.field \.hoverCard\{[^}]*box-shadow/, 'no shadow vocabulary for UI');
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
test('the kit proves the field path: the fixture carries a curated template and the states file reads it back', () => {
|
|
1502
|
+
const fixture = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', 'featured.json'), 'utf8'));
|
|
1503
|
+
const otb = fixture.projects.find((p) => p.origin === 'https://amazonprimea.com');
|
|
1504
|
+
assert.equal(otb.template, 'bead-wash');
|
|
1505
|
+
assert.ok(fixture.projects.every((p) => 'template' in p), 'the wire never drops the key');
|
|
1506
|
+
const states = JSON.parse(fs.readFileSync(path.join(ROOT, 'public-landing', 'projects.states.json'), 'utf8'));
|
|
1507
|
+
const attrs = states.states['in-map'].expect.attr.map((a) => a.join(' '));
|
|
1508
|
+
assert.ok(attrs.includes('#selfOrb data-template pair-chrome'));
|
|
1509
|
+
assert.ok(attrs.some((a) => a.includes('amazonprimea.com') && a.endsWith('data-template bead-wash')), 'the curated template reaches the orb');
|
|
1510
|
+
});
|