@wenathlan/cobaia 2.0.164 → 2.0.166

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/sol/Sol.tsx CHANGED
@@ -9,15 +9,15 @@
9
9
  * this file follows the new name and App.tsx keeps importing the anchor by
10
10
  * the folder path.
11
11
  *
12
- * The cobaia is a CONTENT site (the monetization surface of the family): no
13
- * onboarding, no entry gate — every content route is a page of the site, and
14
- * the home renders directly on the bare root. The intro (/intro) is the one
15
- * public presentation page of the family grammar (every app carries one) and
16
- * mounts BARE — outside the window chrome, like the sibling sites mount
17
- * theirs; the shell's close-caption door relaunches the site at it. Every
18
- * content route renders as the page body of the ONE shared shell (the header
19
- * and the footer); every page is freely reachable, and the catch-all lands
20
- * on the 404 anchor.
12
+ * The cobaia is a CONTENT site (the monetization surface of the family)
13
+ * riding the standardized family entry flow: the intro (/intro) is the
14
+ * presentation splash and the onboarding (/onboarding) the first-run
15
+ * frames — both mount BARE, outside the site chrome, like the sibling apps
16
+ * mount theirs — and the home renders behind the entry gate (the entrada:
17
+ * the flow decides, a finished visitor sees the content). Every other
18
+ * content route renders as the page body of the ONE shared shell (the
19
+ * header and the footer); every page stays freely reachable, and the
20
+ * catch-all lands on the 404 anchor.
21
21
  */
22
22
  import type { ReactNode } from "react";
23
23
  import { Route, Switch } from "wouter";
@@ -25,9 +25,11 @@ import AppsAnchor from "./apps/apps";
25
25
  import BlogAnchor from "./blog/blog";
26
26
  import HomeAnchor from "./home/home";
27
27
  import IntroAnchor from "./intro/intro";
28
+ import OnboardingAnchor from "./onboarding/onboarding";
28
29
  import LegalAnchor from "./legal/legal";
29
30
  import NotFoundAnchor from "./notfound/notfound";
30
31
  import PricingAnchor from "./pricing/pricing";
32
+ import { EntryGate } from "./wingets";
31
33
  import { Shell } from "./shell/Shell";
32
34
  import SupportAnchor from "./support/support";
33
35
 
@@ -46,18 +48,21 @@ function Unrouted() {
46
48
  );
47
49
  }
48
50
 
49
- /** Mounts the theme surface through the page anchors: the bare intro first
50
- * (the public presentation, outside the chrome, like the siblings mount
51
- * theirs), then the content pages of the site — the home on the bare root,
52
- * the five public sections, and the catch-all last. No entry flow: the site
53
- * is content end to end. */
51
+ /** Mounts the theme surface through the page anchors: the entry flow first
52
+ * (the bare intro splash, then the bare onboarding frames — the family
53
+ * law), then the content pages of the site — the home gated behind the
54
+ * entrada on the bare root, the five public sections, and the catch-all
55
+ * last. */
54
56
  export default function Sol() {
55
57
  return (
56
58
  <Switch>
57
59
  <Route path="/intro" component={IntroAnchor} />
60
+ <Route path="/onboarding" component={OnboardingAnchor} />
58
61
  <Route path="/">
59
62
  <Content>
60
- <HomeAnchor />
63
+ <EntryGate>
64
+ <HomeAnchor />
65
+ </EntryGate>
61
66
  </Content>
62
67
  </Route>
63
68
  <Route path="/apps">
package/sol/home/home.tsx CHANGED
@@ -11,8 +11,9 @@
11
11
  * three how-it-works steps on hairline seams (01 engine · 02 static · 03
12
12
  * honest), a four-card preview of the directory leading to /apps, the
13
13
  * support strip, and the one reserved ad slot at the end of the content.
14
- * No entry gate and no first-run flow: cobaia is a content site — the home
15
- * renders directly, every page is public.
14
+ * The home rides the entrada: the entry gate consults the first-run flow
15
+ * (intro → onboarding) before the landing shows — the family law every
16
+ * sibling app already answers.
16
17
  * Styled entirely by sol/sol.css.
17
18
  */
18
19
  import type { CSSProperties } from "react";
@@ -21,7 +22,7 @@ import { useContent } from "../../content.ts";
21
22
  import { familyurl } from "../../familyurl";
22
23
  import { type LabelKey, label, labelfill } from "../../labels";
23
24
  import { roster } from "../../roster.ts";
24
- import { AdSlot, BrandMark } from "../wingets";
25
+ import { AdSlot, BrandMark, EntryGate } from "../wingets";
25
26
 
26
27
  /** the featured deploy units of the landing preview (the full directory
27
28
  * lives on /apps): the OS, the engine, the store and one studio — read
@@ -41,6 +42,15 @@ const STEPS: readonly { n: LabelKey; title: LabelKey; text: LabelKey }[] = [
41
42
  ];
42
43
 
43
44
  export function Home() {
45
+ return (
46
+ <EntryGate>
47
+ <HomeSurface />
48
+ </EntryGate>
49
+ );
50
+ }
51
+
52
+ /** the home surface the gate reveals: the landing itself. */
53
+ function HomeSurface() {
44
54
  useContent();
45
55
  return (
46
56
  <div className="cb-page">
@@ -15,9 +15,13 @@
15
15
  * zone renders the real offer from the one checkoutlinks table (the not
16
16
  * configured state stays honest), the doors panes open the app directory
17
17
  * and the legal page, and the footer carries the doors (the site row, the
18
- * family row, the legal line). The entrance rides the cb-in stagger under
19
- * the second, the layers ride the surface-ladder bands, the door panes ride
20
- * the silk law. Public-site rules: zero network calls from the page itself,
18
+ * family row, the legal line). The visit marks the intro stage seen
19
+ * through the shared onboardingstate accessor (the family entry-flow
20
+ * contract: intro → onboarding → entrada — the stage flag rides the
21
+ * platform db with the localStorage cache as the offline mirror). The
22
+ * entrance rides the cb-in stagger under the second, the layers ride the
23
+ * surface-ladder bands, the door panes ride the silk law. Public-site
24
+ * rules: zero network calls from the page itself beyond the stage flag,
21
25
  * React state only; zero decorative dots; zero invented numbers and zero
22
26
  * fake testimonials — the real offer, read from the data modules.
23
27
  */
@@ -26,6 +30,7 @@ import type { CSSProperties } from "react";
26
30
  import { useCallback, useEffect, useState } from "react";
27
31
  import { Link } from "wouter";
28
32
  import { label } from "../../labels";
33
+ import { markOnboardingStage } from "../../onboardingstate.ts";
29
34
  import { BrandMark, IntroFooter, IntroPlans, IntroSplash } from "../wingets";
30
35
 
31
36
  /** the lifecycle of the opening act: resolving (the motion preference
@@ -59,9 +64,12 @@ function DoorGlyph() {
59
64
  export function Intro() {
60
65
  const [splash, setSplash] = useState<SplashPhase>("resolving");
61
66
 
62
- // the splash decision rides the motion preference alone — nothing is read
63
- // from the visitor machine and nothing is written back (the no storage law)
67
+ // the splash decision rides the motion preference, and the visit marks
68
+ // the intro stage seen through the shared accessor (the family flow
69
+ // contract: the db holds the flag, the cache mirrors it offline — the
70
+ // splash never replays for a visitor that finished the flow)
64
71
  useEffect(() => {
72
+ void markOnboardingStage("intro");
65
73
  if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
66
74
  setSplash("done");
67
75
  return;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * onboarding.tsx — the first-run steps of the ENTRY FLOW (the flow contract
3
+ * of the reform the family rides: intro → onboarding → entrada). Three
4
+ * short frames on the site voice over the deep canvas — what the site IS
5
+ * (the honest role: the family's monetization face, kept apart so the apps
6
+ * stay clean), what it DOES (the real offer read from the one content
7
+ * lane: the plans, the support rails, the honest ad slots) and the ENTER
8
+ * frame with the hand-over button. The finish AND the skip mark the
9
+ * onboarding stage seen through the shared onboardingstate accessor
10
+ * (DB-backed, cache-mirrored — seeing once is enough; the flow never
11
+ * replays) and navigate to "/", where the entry gate now lets the site
12
+ * take over. The copy rides the label store; no icon dependency (the site
13
+ * grammar draws its own glyphs); reduced motion rides the stylesheet hard
14
+ * stop. No opt-in switches here: the site holds no account and no visitor
15
+ * storage beyond the first-run stage itself.
16
+ */
17
+
18
+ import { useState } from "react";
19
+ import { useLocation } from "wouter";
20
+ import { type LabelKey, label } from "../../labels";
21
+ import { markOnboardingStage } from "../../onboardingstate.ts";
22
+ import { BrandMark } from "../wingets";
23
+
24
+ /** the three frames of the first run, in order — the copy rides the store
25
+ * (the id keeps the ladder segments keyed, the key answers the eyebrow). */
26
+ const STEPS: readonly { id: string; eyebrowKey: LabelKey }[] = [
27
+ { id: "is", eyebrowKey: "onboarding.steps.is.eyebrow" },
28
+ { id: "does", eyebrowKey: "onboarding.steps.does.eyebrow" },
29
+ { id: "enter", eyebrowKey: "onboarding.steps.enter.eyebrow" },
30
+ ];
31
+
32
+ /** the does-frame rows: the real offer the site carries, one label each
33
+ * (the plans, the support rails, the honest ad slots — nothing invented). */
34
+ const FACTS: readonly LabelKey[] = ["onboarding.facts.plans", "onboarding.facts.support", "onboarding.facts.ads"];
35
+
36
+ /** the chevron glyph of the pager actions: the inline 10×10 chevron the
37
+ * site grammar draws (no icon dependency rides the content site). */
38
+ function StepGlyph({ back }: { back?: boolean }) {
39
+ return (
40
+ <svg
41
+ className="cb-onboard__glyph"
42
+ width={10}
43
+ height={10}
44
+ viewBox="0 0 10 10"
45
+ aria-hidden="true"
46
+ focusable="false"
47
+ style={back ? { transform: "rotate(180deg)" } : undefined}
48
+ >
49
+ <path d="M3 0l5 5-5 5" fill="none" stroke="currentColor" strokeWidth="1.5" />
50
+ </svg>
51
+ );
52
+ }
53
+
54
+ /**
55
+ * The onboarding page: a three-step first-run dialog that hands the
56
+ * surface to the home of the site. Finish and skip both mark the
57
+ * onboarding stage seen and navigate to the gated home.
58
+ *
59
+ * @returns the onboarding element.
60
+ */
61
+ export default function Onboarding() {
62
+ const [, navigate] = useLocation();
63
+ const [step, setStep] = useState(0);
64
+ const last = step === STEPS.length - 1;
65
+
66
+ /** marks the stage seen and hands the surface to the home (the entry gate lets it through). */
67
+ const finish = (): void => {
68
+ void markOnboardingStage("onboarding").then(() => navigate("/"));
69
+ };
70
+
71
+ return (
72
+ <div className="cb-onboard" data-page="onboarding">
73
+ <section className="cb-onboard__card" aria-label={label("onboarding.aria")}>
74
+ <header className="cb-onboard__head">
75
+ <BrandMark size={18} />
76
+ <span className="cb-onboard__title">{label("onboarding.title")}</span>
77
+ </header>
78
+
79
+ <div className="cb-onboard__body">
80
+ {step === 0 && (
81
+ <article className="cb-onboard__step" key="is">
82
+ <p className="cb-eyebrow">{label(STEPS[0].eyebrowKey)}</p>
83
+ <h2 className="cb-onboard__lead">{label("onboarding.steps.is.title")}</h2>
84
+ <p className="cb-onboard__lede">{label("onboarding.steps.is.text")}</p>
85
+ </article>
86
+ )}
87
+ {step === 1 && (
88
+ <article className="cb-onboard__step" key="does">
89
+ <p className="cb-eyebrow">{label(STEPS[1].eyebrowKey)}</p>
90
+ <h2 className="cb-onboard__lead">{label("onboarding.steps.does.title")}</h2>
91
+ <p className="cb-onboard__lede">{label("onboarding.steps.does.text")}</p>
92
+ <ul className="cb-onboard__facts">
93
+ {FACTS.map((key) => (
94
+ <li key={key} className="cb-onboard__fact">
95
+ {label(key)}
96
+ </li>
97
+ ))}
98
+ </ul>
99
+ </article>
100
+ )}
101
+ {step === 2 && (
102
+ <article className="cb-onboard__step" key="enter">
103
+ <p className="cb-eyebrow">{label(STEPS[2].eyebrowKey)}</p>
104
+ <h2 className="cb-onboard__lead">{label("onboarding.steps.enter.title")}</h2>
105
+ <p className="cb-onboard__lede">{label("onboarding.steps.enter.text")}</p>
106
+ </article>
107
+ )}
108
+ </div>
109
+
110
+ <footer className="cb-onboard__foot">
111
+ <div className="cb-onboard__ladder" aria-hidden="true">
112
+ {STEPS.map((frame) => (
113
+ <span
114
+ key={frame.id}
115
+ className="cb-onboard__segrun"
116
+ data-active={STEPS[step].id === frame.id ? "true" : undefined}
117
+ />
118
+ ))}
119
+ </div>
120
+ <div className="cb-onboard__actions">
121
+ <button type="button" className="cb-onboard__skip" onClick={finish}>
122
+ {label("onboarding.skip")}
123
+ </button>
124
+ {step > 0 && (
125
+ <button type="button" className="cb-btn cb-btn--ghost" onClick={() => setStep((value) => value - 1)}>
126
+ <StepGlyph back />
127
+ {label("onboarding.back")}
128
+ </button>
129
+ )}
130
+ {last ? (
131
+ <button type="button" className="cb-btn" onClick={finish}>
132
+ {label("onboarding.enterCta")}
133
+ <StepGlyph />
134
+ </button>
135
+ ) : (
136
+ <button type="button" className="cb-btn" onClick={() => setStep((value) => value + 1)}>
137
+ {label("onboarding.next")}
138
+ <StepGlyph />
139
+ </button>
140
+ )}
141
+ </div>
142
+ </footer>
143
+ </section>
144
+ </div>
145
+ );
146
+ }
package/sol/sol.css CHANGED
@@ -125,6 +125,9 @@
125
125
  --color-win: #4cc2ff;
126
126
  --color-win-ink: #0a0a0a;
127
127
  --color-focus: var(--color-win);
128
+ /* the ice-vault accent of the family spec (PR9): the #a3d7e6 lane the
129
+ vault surfaces borrow — focus stays the windows #4cc2ff */
130
+ --accent-ice-vault: #a3d7e6;
128
131
 
129
132
  /* the canvas + the 8 surface rungs (elevation by color, never by border) */
130
133
  --sol-canvas: var(--color-canvas);
@@ -1949,3 +1952,153 @@ code {
1949
1952
  html {
1950
1953
  touch-action: pan-x pan-y;
1951
1954
  }
1955
+
1956
+ /* ---------- 21. THE ONBOARDING — the first-run frames of the entry flow
1957
+ (intro → onboarding → entrada; the family law). the three frames ride
1958
+ one centered card on the deep canvas — the site voice, no window chrome,
1959
+ responsive from the phone to the tv (the card caps its width and the
1960
+ feet stack under the narrow gate). ---------- */
1961
+ .cb-onboard {
1962
+ min-height: 100vh;
1963
+ display: grid;
1964
+ place-items: center;
1965
+ padding: 24px;
1966
+ background:
1967
+ radial-gradient(120% 90% at 50% -10%, rgb(255 255 255 / 4%), transparent 60%),
1968
+ var(--sol-canvas-deep);
1969
+ }
1970
+ .cb-onboard__card {
1971
+ width: min(560px, 100%);
1972
+ background: var(--sol-surface-2);
1973
+ border: 1px solid var(--sol-hairline);
1974
+ border-radius: 10px;
1975
+ box-shadow: 0 24px 48px -24px rgb(0 0 0 / 60%);
1976
+ overflow: hidden;
1977
+ }
1978
+ .cb-onboard__head {
1979
+ display: flex;
1980
+ align-items: center;
1981
+ gap: 10px;
1982
+ padding: 14px 18px;
1983
+ border-bottom: 1px solid var(--sol-hairline);
1984
+ background: var(--sol-surface-1);
1985
+ }
1986
+ .cb-onboard__title {
1987
+ font-size: 12px;
1988
+ font-weight: 550;
1989
+ letter-spacing: 0.08em;
1990
+ text-transform: lowercase;
1991
+ color: var(--sol-muted);
1992
+ }
1993
+ .cb-onboard__body {
1994
+ padding: 26px 24px 8px;
1995
+ min-height: 220px;
1996
+ }
1997
+ .cb-onboard__step {
1998
+ animation: cbOnboardIn 250ms var(--sol-ease, cubic-bezier(0.22, 1, 0.36, 1)) backwards;
1999
+ }
2000
+ @keyframes cbOnboardIn {
2001
+ from {
2002
+ opacity: 0;
2003
+ transform: translateY(8px);
2004
+ }
2005
+ }
2006
+ .cb-onboard__lead {
2007
+ margin: 6px 0 10px;
2008
+ font-size: 22px;
2009
+ line-height: 1.2;
2010
+ font-weight: 600;
2011
+ color: var(--sol-ink);
2012
+ }
2013
+ .cb-onboard__lede {
2014
+ margin: 0;
2015
+ max-width: 46ch;
2016
+ color: var(--sol-muted);
2017
+ }
2018
+ .cb-onboard__facts {
2019
+ list-style: none;
2020
+ margin: 14px 0 0;
2021
+ padding: 0;
2022
+ display: grid;
2023
+ gap: 8px;
2024
+ }
2025
+ .cb-onboard__fact {
2026
+ padding: 10px 12px;
2027
+ border: 1px solid var(--sol-hairline);
2028
+ border-radius: 8px;
2029
+ background: var(--sol-surface-1);
2030
+ color: var(--sol-muted);
2031
+ }
2032
+ .cb-onboard__foot {
2033
+ display: flex;
2034
+ align-items: center;
2035
+ justify-content: space-between;
2036
+ gap: 12px;
2037
+ flex-wrap: wrap;
2038
+ padding: 16px 18px;
2039
+ border-top: 1px solid var(--sol-hairline);
2040
+ background: var(--sol-surface-1);
2041
+ }
2042
+ .cb-onboard__ladder {
2043
+ display: flex;
2044
+ gap: 6px;
2045
+ }
2046
+ .cb-onboard__segrun {
2047
+ width: 18px;
2048
+ height: 3px;
2049
+ border-radius: 2px;
2050
+ background: var(--sol-hairline-strong);
2051
+ }
2052
+ .cb-onboard__segrun[data-active="true"] {
2053
+ background: var(--sol-ink);
2054
+ }
2055
+ .cb-onboard__actions {
2056
+ display: flex;
2057
+ align-items: center;
2058
+ gap: 8px;
2059
+ flex-wrap: wrap;
2060
+ }
2061
+ .cb-onboard__skip {
2062
+ margin-right: auto;
2063
+ font: inherit;
2064
+ font-size: 12px;
2065
+ color: var(--sol-faint);
2066
+ background: none;
2067
+ border: none;
2068
+ padding: 8px 6px;
2069
+ cursor: pointer;
2070
+ }
2071
+ .cb-onboard__skip:hover {
2072
+ color: var(--sol-ink);
2073
+ }
2074
+ .cb-onboard__skip:focus-visible {
2075
+ outline: 2px solid var(--color-focus);
2076
+ outline-offset: 2px;
2077
+ }
2078
+ .cb-onboard__actions .cb-btn {
2079
+ display: inline-flex;
2080
+ align-items: center;
2081
+ gap: 8px;
2082
+ font-size: 12px;
2083
+ padding: 9px 16px;
2084
+ }
2085
+ .cb-onboard__glyph {
2086
+ flex: none;
2087
+ }
2088
+ @media (max-width: 480px) {
2089
+ .cb-onboard {
2090
+ padding: 12px;
2091
+ align-items: end;
2092
+ }
2093
+ .cb-onboard__body {
2094
+ padding: 20px 16px 4px;
2095
+ }
2096
+ .cb-onboard__lead {
2097
+ font-size: 19px;
2098
+ }
2099
+ }
2100
+ @media (prefers-reduced-motion: reduce) {
2101
+ .cb-onboard__step {
2102
+ animation: none;
2103
+ }
2104
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * entrygate of cobaia — the entry gate of the SITE (the flow contract of
3
+ * the reform the family rides: intro → onboarding → entrada): the home
4
+ * anchor consults this component before rendering anything. On mount it
5
+ * reads the first-run state from onboardingstate (the DB-backed accessor —
6
+ * the offline cache answers the fast path, the platform gateway reconciles
7
+ * in the background); while the state is still unknown it renders null (no
8
+ * flash of the home), when the intro is unseen it replaces the route with
9
+ * /intro, when only the onboarding is unseen it replaces with /onboarding,
10
+ * and only a visitor that finished the flow ever sees the home. The gate
11
+ * never writes: marking the stages belongs to the intro and the onboarding
12
+ * pages.
13
+ */
14
+ import { type ReactNode, useEffect, useState } from "react";
15
+ import { useLocation } from "wouter";
16
+ import { type VisitorOnboarding, readOnboardingState } from "../../onboardingstate.ts";
17
+
18
+ /** the verdict of the gate: still resolving, or the route the visitor belongs to */
19
+ type GateVerdict = "resolving" | "intro" | "onboarding" | "home";
20
+
21
+ /**
22
+ * Resolves the gate verdict from a seen-state snapshot (the flow order:
23
+ * intro first, then onboarding, then the home).
24
+ *
25
+ * @param state the resolved snapshot.
26
+ * @returns the verdict the gate acts on.
27
+ */
28
+ export function verdictOf(state: VisitorOnboarding): GateVerdict {
29
+ if (!state.introseen) return "intro";
30
+ if (!state.onboardingseen) return "onboarding";
31
+ return "home";
32
+ }
33
+
34
+ /**
35
+ * The entry gate wrapping the home: null while resolving, a replace-navigation
36
+ * to the missing first-run stage, the home content once the flow is done.
37
+ *
38
+ * @param props the home children rendered behind the gate.
39
+ * @returns the gated home element.
40
+ */
41
+ export function EntryGate({ children }: { children: ReactNode }) {
42
+ const [, navigate] = useLocation();
43
+ const [verdict, setVerdict] = useState<GateVerdict>("resolving");
44
+
45
+ useEffect(() => {
46
+ let live = true;
47
+ void readOnboardingState().then((state) => {
48
+ if (live) setVerdict((current) => (current === "home" ? current : verdictOf(state)));
49
+ });
50
+ return () => {
51
+ live = false;
52
+ };
53
+ }, []);
54
+
55
+ useEffect(() => {
56
+ if (verdict === "intro") navigate("/intro", { replace: true });
57
+ else if (verdict === "onboarding") navigate("/onboarding", { replace: true });
58
+ }, [navigate, verdict]);
59
+
60
+ if (verdict !== "home") return null;
61
+ return <>{children}</>;
62
+ }
63
+
64
+ export default EntryGate;
@@ -6,6 +6,8 @@
6
6
  * the components stay complete independent files beside this surface.
7
7
  */
8
8
  export * from "../../roster.ts";
9
+ export * from "./HomeEntrygate.tsx";
10
+ export { default as EntryGate } from "./HomeEntrygate.tsx";
9
11
  export * from "./IntroFooter.tsx";
10
12
  export { default as IntroFooter } from "./IntroFooter.tsx";
11
13
  export * from "./IntroPlans.tsx";
package/tauri.conf.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Cobaia",
4
- "version": "2.0.163",
4
+ "version": "2.0.166",
5
5
  "identifier": "com.wenathlan.cobaia",
6
6
  "build": {
7
7
  "beforeDevCommand": "pnpm dev",
@@ -0,0 +1,124 @@
1
+ // # entryflow.test — the construction-flow contract of the cobaia: the
2
+ // standardized family law (intro → onboarding → entrada) the owner ordered
3
+ // and the db-backed content law behind it. Run: pnpm vitest run
4
+ // tests/entryflow.test.ts
5
+ //
6
+ // The suite watches three promises:
7
+ // 1. the flow — the intro mounts before the onboarding, the onboarding
8
+ // before the gated home (the entrada), and the onboarding page marks
9
+ // its stage through the shared onboardingstate accessor, never a local
10
+ // flag of its own;
11
+ // 2. the db — the content_rows table exists in the runtime schema, the
12
+ // boot seed ingests the manifest, and the rows read back carry the
13
+ // kinds the pages render (posts, roster, plans — no hardcoded row
14
+ // lives in a face);
15
+ // 3. the copy — every onboarding key the frames cite answers from the
16
+ // label manifest (a hardcoded string inside a widget is a defect).
17
+ import { readFileSync } from "node:fs";
18
+ import assert from "node:assert/strict";
19
+ import { describe, expect, it } from "vitest";
20
+ import { LABEL_MANIFEST, type LabelKey } from "../labels.ts";
21
+ import { CONTENTKINDS, seedPosts, seedRoster, seedPlans } from "../content.ts";
22
+
23
+ function read(path: string): string {
24
+ return readFileSync(new URL(path, import.meta.url), "utf8");
25
+ }
26
+
27
+ const SOL = read("../sol/Sol.tsx");
28
+ const HOME = read("../sol/home/home.tsx");
29
+ const INTRO = read("../sol/intro/intro.tsx");
30
+ const ONBOARDING = read("../sol/onboarding/onboarding.tsx");
31
+ const GATE = read("../sol/wingets/HomeEntrygate.tsx");
32
+ const STATE = read("../onboardingstate.ts");
33
+ const DB = read("../db.ts");
34
+ const CSS = read("../sol/sol.css");
35
+
36
+ describe("the entry flow contract (intro → onboarding → entrada)", () => {
37
+ it("mounts the flow before the fundamentals — intro first, onboarding second, the gated home third", () => {
38
+ expect(SOL).toMatch(/path="\/intro"/);
39
+ expect(SOL).toMatch(/path="\/onboarding"/);
40
+ expect(SOL.indexOf('"/intro"')).toBeLessThan(SOL.indexOf('"/onboarding"'));
41
+ expect(SOL.indexOf('"/onboarding"')).toBeLessThan(SOL.indexOf('"/"'));
42
+ });
43
+
44
+ it("rides the entrada behind the entry gate — the home renders only through the gate", () => {
45
+ expect(HOME).toMatch(/EntryGate/);
46
+ expect(HOME).toMatch(/<HomeSurface \/>/);
47
+ expect(GATE).toMatch(/navigate\("\/intro"/);
48
+ expect(GATE).toMatch(/navigate\("\/onboarding"/);
49
+ });
50
+
51
+ it("marks the stages through the shared db-backed accessor, never a local flag", () => {
52
+ expect(INTRO).toMatch(/markOnboardingStage\("intro"\)/);
53
+ expect(ONBOARDING).toMatch(/markOnboardingStage\("onboarding"\)/);
54
+ expect(STATE).toMatch(/cobaia\.onboarding\.seen/);
55
+ expect(STATE).toMatch(/\/api\/v1\/onboarding/);
56
+ expect(ONBOARDING).not.toMatch(/localStorage/);
57
+ });
58
+
59
+ it("answers every onboarding key the frames cite from the label manifest", () => {
60
+ const keys = [
61
+ "onboarding.aria",
62
+ "onboarding.title",
63
+ "onboarding.skip",
64
+ "onboarding.back",
65
+ "onboarding.next",
66
+ "onboarding.enterCta",
67
+ "onboarding.steps.is.eyebrow",
68
+ "onboarding.steps.is.title",
69
+ "onboarding.steps.is.text",
70
+ "onboarding.steps.does.eyebrow",
71
+ "onboarding.steps.does.title",
72
+ "onboarding.steps.does.text",
73
+ "onboarding.steps.enter.eyebrow",
74
+ "onboarding.steps.enter.title",
75
+ "onboarding.steps.enter.text",
76
+ "onboarding.facts.plans",
77
+ "onboarding.facts.support",
78
+ "onboarding.facts.ads",
79
+ ] as const satisfies readonly LabelKey[];
80
+ for (const key of keys) {
81
+ assert.ok(
82
+ typeof LABEL_MANIFEST[key] === "string" && LABEL_MANIFEST[key].length > 0,
83
+ `${key} must answer from the manifest`,
84
+ );
85
+ }
86
+ });
87
+
88
+ it("styles the frames responsively with the reduced-motion stop", () => {
89
+ expect(CSS).toMatch(/\.cb-onboard \{/);
90
+ expect(CSS).toMatch(/@media \(max-width: 480px\)/);
91
+ expect(CSS).toMatch(/prefers-reduced-motion: reduce/);
92
+ });
93
+ });
94
+
95
+ describe("the content law (the rows live in the db, the manifest is the seed)", () => {
96
+ it("carries the content_rows table in the runtime schema and the seed ingestion", () => {
97
+ expect(DB).toMatch(/CREATE TABLE IF NOT EXISTS content_rows/);
98
+ expect(DB).toMatch(/seedContentTable/);
99
+ expect(DB).toMatch(/readContentRows/);
100
+ });
101
+
102
+ it("seeds every kind the pages render — no face answers a fixed table of its own", () => {
103
+ assert.deepEqual([...CONTENTKINDS], [
104
+ "posts",
105
+ "roster",
106
+ "plans",
107
+ "supportlinks",
108
+ "ads.slots",
109
+ "ads.label",
110
+ "ads.empty",
111
+ ]);
112
+ assert.ok(seedPosts.length >= 3, "the blog seed carries the family posts");
113
+ assert.ok(seedRoster.length === 10, "the roster seed carries the ten family members");
114
+ assert.ok(seedPlans.length === 3, "the plans seed carries the three honest plans");
115
+ });
116
+
117
+ it("keeps the faces reading through the typed doors — no hardcoded row in a page", () => {
118
+ expect(HOME).toMatch(/from "\.\.\/\.\.\/roster\.ts"/);
119
+ expect(HOME).not.toMatch(/const ROSTER/);
120
+ expect(read("../sol/apps/apps.tsx")).not.toMatch(/const ROSTER/);
121
+ expect(read("../sol/pricing/pricing.tsx")).not.toMatch(/const PLANS/);
122
+ expect(read("../sol/blog/blog.tsx")).not.toMatch(/const POSTS/);
123
+ });
124
+ });