@wenathlan/cobaia 2.0.165 → 2.0.167

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.
Files changed (51) hide show
  1. package/.dockerignore +1 -1
  2. package/App.tsx +2 -31
  3. package/Dockerfile +162 -4
  4. package/biome.jsonc +3 -3
  5. package/build.gradle.kts +4 -4
  6. package/bun.lock +1820 -487
  7. package/cobaia.csproj +2 -2
  8. package/cobaia.gemspec +2 -2
  9. package/cobaia.java +2 -2
  10. package/db.ts +87 -3
  11. package/docker-compose.yml +58 -0
  12. package/docker.config +74 -0
  13. package/docs/cobaia.md +31 -0
  14. package/docs/distributionlanes.md +1 -1
  15. package/keywords.txt +1 -0
  16. package/labels.ts +23 -1
  17. package/monetization.ts +248 -0
  18. package/nginx.conf +106 -0
  19. package/onboardingstate.ts +211 -0
  20. package/package.json +430 -83
  21. package/pnpm-workspace.yaml +60 -38
  22. package/pom.xml +4 -4
  23. package/routerbase.ts +1 -0
  24. package/schema.prisma +23 -0
  25. package/sol/Sol.tsx +20 -15
  26. package/sol/apps/apps.tsx +1 -1
  27. package/sol/blog/blog.tsx +1 -1
  28. package/sol/home/home.tsx +14 -4
  29. package/sol/intro/intro.tsx +13 -5
  30. package/sol/onboarding/onboarding.tsx +146 -0
  31. package/sol/pricing/pricing.tsx +1 -1
  32. package/sol/shell/Shell.tsx +1 -1
  33. package/sol/sol.css +153 -0
  34. package/sol/support/support.tsx +1 -1
  35. package/sol/wingets/HomeEntrygate.tsx +64 -0
  36. package/sol/wingets/IntroFooter.tsx +1 -1
  37. package/sol/wingets/IntroPlans.tsx +1 -1
  38. package/sol/wingets/ShellAdsslot.tsx +1 -1
  39. package/sol/wingets/index.ts +3 -1
  40. package/tauri.conf.json +1 -1
  41. package/{tests → test}/adsconfig.test.ts +2 -2
  42. package/{tests → test}/checkoutlinks.test.ts +2 -2
  43. package/test/entryflow.test.ts +124 -0
  44. package/{tests → test}/familyurl.test.ts +1 -1
  45. package/{tests → test}/posts.test.ts +2 -2
  46. package/{tests → test}/sandboxmodality.test.ts +1 -1
  47. package/vitest.config.ts +1 -1
  48. package/adsconfig.ts +0 -59
  49. package/checkoutlinks.ts +0 -80
  50. package/posts.ts +0 -54
  51. package/roster.ts +0 -29
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
+ }
@@ -13,7 +13,7 @@
13
13
  * nothing that asks for support ever sits beside an advertisement.
14
14
  */
15
15
  import type { CSSProperties } from "react";
16
- import { isconfigured, supportlinks } from "../../checkoutlinks";
16
+ import { isconfigured, supportlinks } from "../../monetization";
17
17
  import { useContent } from "../../content.ts";
18
18
  import { label } from "../../labels";
19
19
 
@@ -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;
@@ -13,7 +13,7 @@ import type { CSSProperties } from "react";
13
13
  import { useContent } from "../../content.ts";
14
14
  import { familyurl } from "../../familyurl";
15
15
  import { type LabelKey, label } from "../../labels";
16
- import { roster } from "../../roster.ts";
16
+ import { roster } from "../../monetization.ts";
17
17
 
18
18
  /** the site doors of the footer row, in reading order (the labels ride the
19
19
  * shell foot keys — the same words the chrome's footer carries). */
@@ -15,7 +15,7 @@
15
15
 
16
16
  import type { CSSProperties } from "react";
17
17
  import { Link } from "wouter";
18
- import { checkoutlinks, type checkoutplan, isconfigured } from "../../checkoutlinks";
18
+ import { checkoutlinks, type checkoutplan, isconfigured } from "../../monetization";
19
19
  import { useContent } from "../../content.ts";
20
20
  import { label, labelfill } from "../../labels";
21
21
 
@@ -15,7 +15,7 @@
15
15
  * automated traffic ever touches an ad surface.
16
16
  */
17
17
  import type { CSSProperties } from "react";
18
- import { ADS_EMPTY, ADS_LABEL, adsslotfor, adsslotlive } from "../../adsconfig.ts";
18
+ import { ADS_EMPTY, ADS_LABEL, adsslotfor, adsslotlive } from "../../monetization.ts";
19
19
  import { labelfill } from "../../labels";
20
20
 
21
21
  type AdSlotProps = {
@@ -5,7 +5,9 @@
5
5
  * lives exactly once (here) instead of repeating across every page —
6
6
  * the components stay complete independent files beside this surface.
7
7
  */
8
- export * from "../../roster.ts";
8
+ export { type familymember, roster } from "../../monetization.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.165",
4
+ "version": "2.0.167",
5
5
  "identifier": "com.wenathlan.cobaia",
6
6
  "build": {
7
7
  "beforeDevCommand": "pnpm dev",
@@ -1,5 +1,5 @@
1
1
  // # adsconfig.test — the honest ad slot registry of the family site.
2
- // Run: pnpm vitest run tests/adsconfig.test.ts
2
+ // Run: pnpm vitest run test/adsconfig.test.ts
3
3
  // The suite watches the promise the ad slot makes: one reserved slot per
4
4
  // content page (home, apps, blog), every slot id shipping EMPTY (no ad
5
5
  // network script, no id, no key is ever committed — the loader enters after
@@ -7,7 +7,7 @@
7
7
  // aspect-ratio shapes the containers reserve so the page never shifts.
8
8
  import assert from "node:assert/strict";
9
9
  import { describe, expect, it } from "vitest";
10
- import { ADS_EMPTY, ADS_LABEL, ADSSLOTS, adsslotfor, adsslotlive } from "../adsconfig.ts";
10
+ import { ADS_EMPTY, ADS_LABEL, ADSSLOTS, adsslotfor, adsslotlive } from "../monetization.ts";
11
11
 
12
12
  describe("the ad slot registry", () => {
13
13
  it("reserves one slot per content page and nothing else", () => {
@@ -1,5 +1,5 @@
1
1
  // # checkoutlinks.test — the billing configuration of the family site.
2
- // Run: pnpm vitest run tests/checkoutlinks.test.ts
2
+ // Run: pnpm vitest run test/checkoutlinks.test.ts
3
3
  // The suite watches the promise the pricing page makes: the three plans
4
4
  // exist with the placeholder prices (US$0 / 5 / 19 a month), every checkout
5
5
  // href ships EMPTY so the interface renders the honest empty state instead
@@ -10,7 +10,7 @@
10
10
  // the billing table cites, so a card never renders a bare key.
11
11
  import assert from "node:assert/strict";
12
12
  import { describe, expect, it } from "vitest";
13
- import { checkoutlinks, isconfigured, planbyid, supportlinks } from "../checkoutlinks.ts";
13
+ import { checkoutlinks, isconfigured, planbyid, supportlinks } from "../monetization.ts";
14
14
  import { LABEL_MANIFEST } from "../labels.ts";
15
15
 
16
16
  describe("checkout links", () => {
@@ -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
+ // test/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 "\.\.\/\.\.\/monetization\.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
+ });
@@ -1,5 +1,5 @@
1
1
  // # familyurl.test — the address book contract of the family deploy units.
2
- // Run: pnpm vitest run tests/familyurl.test.ts
2
+ // Run: pnpm vitest run test/familyurl.test.ts
3
3
  // The suite watches the promise the directory and the footer make: every
4
4
  // family member resolves one level above this application, always with the
5
5
  // trailing slash, and pasted segments or stray slashes land on the same
@@ -1,12 +1,12 @@
1
1
  // # posts.test — the seed blog of the family site.
2
- // Run: pnpm vitest run tests/posts.test.ts
2
+ // Run: pnpm vitest run test/posts.test.ts
3
3
  // The suite watches the promise the blog page makes: the index is sorted
4
4
  // newest first no matter the array order, the slugs are unique and url-safe,
5
5
  // every seed post carries a real ~300 word body (the ad networks approve
6
6
  // content, not containers) and the honest word counter agrees with itself.
7
7
  import assert from "node:assert/strict";
8
8
  import { describe, expect, it } from "vitest";
9
- import { postindex, posts, postwords } from "../posts.ts";
9
+ import { postindex, posts, postwords } from "../monetization.ts";
10
10
 
11
11
  // the content lane answers the same rows the seed carries (wave of the
12
12
  // content law): the const retired, the accessor serves the identical data
@@ -9,7 +9,7 @@
9
9
  // modality id pins the seat for the platform's join to validate at
10
10
  // dispatch — this module never judges engine vocabulary). runnable with
11
11
  // vitest:
12
- // pnpm vitest run tests/sandboxmodality.test.ts
12
+ // pnpm vitest run test/sandboxmodality.test.ts
13
13
  import assert from "node:assert/strict";
14
14
  import { lanesof } from "@wenathlan/saddle/workloadlaw";
15
15
  import { describe, it } from "vitest";
package/vitest.config.ts CHANGED
@@ -32,7 +32,7 @@ export default defineConfig({
32
32
  },
33
33
  },
34
34
  test: {
35
- include: ["tests/*.test.ts"],
35
+ include: ["test/*.test.ts"],
36
36
  environment: "node",
37
37
  /* the timeout stays five seconds on every native runner and scales through the env the emulated arm64 container leg sets (a qemu-interpreted second answers the same green suite many times slower), so an emulated runner never fails a test a native runner passed */
38
38
  testTimeout: Number(process.env.COBAIA_TEST_TIMEOUT_MS ?? 5000),
package/adsconfig.ts DELETED
@@ -1,59 +0,0 @@
1
- /**
2
- * adsconfig.ts — the honest ad slot registry of the family site, pure data
3
- * at the app root. The family site carries ONE reserved ad container per
4
- * content page (home, the app directory, the blog index), always at the end
5
- * of the content, always labeled "advertisement", always a fixed aspect
6
- * ratio so the page never shifts when a real unit arrives.
7
- *
8
- * THE SCRIPT RULE: no ad network script lives in this repository. The
9
- * AdSense (or equivalent) loader is injected AFTER the network approves the
10
- * site, through the deploy environment only. Until then the slot ids stay
11
- * empty and the container renders the honest empty note. No network call,
12
- * no click encouragement, no automated traffic ever touches an ad surface —
13
- * invalid traffic is the one thing that kills both the ad account and the
14
- * domain reputation, so the family does not do it.
15
- */
16
-
17
- /** one reserved ad slot: the config id and the reserved shape. */
18
- export type adsslot = {
19
- /** the ad network slot id — empty until the owner configures it after approval */
20
- slotid: string;
21
- /** the display format the reserved box mimics */
22
- format: string;
23
- /** the css aspect-ratio value the container reserves (never shifts layout) */
24
- ratio: string;
25
- };
26
-
27
- /** the fixed label every ad container renders, visible and honest. */
28
- export const ADS_LABEL = "advertisement";
29
-
30
- /** the honest note the container renders while no slot id is configured. */
31
- export const ADS_EMPTY = "ad slot not configured yet";
32
-
33
- /** the reserved slots per content page, keyed by the page folder name. */
34
- export const ADSSLOTS: Readonly<Record<string, adsslot>> = {
35
- home: { slotid: "", format: "display", ratio: "728 / 90" },
36
- apps: { slotid: "", format: "in-feed", ratio: "3 / 1" },
37
- blog: { slotid: "", format: "in-article", ratio: "3 / 1" },
38
- };
39
-
40
- /**
41
- * Reads the reserved slot of one content page.
42
- *
43
- * @param page the page folder name (home, apps, blog).
44
- * @returns the slot, or undefined when the page carries no slot.
45
- */
46
- export function adsslotfor(page: string): adsslot | undefined {
47
- return ADSSLOTS[page];
48
- }
49
-
50
- /**
51
- * Answers whether a slot is configured with a real network id. An empty
52
- * (or whitespace) slot id means the container stays an honest placeholder.
53
- *
54
- * @param slot the reserved slot entry.
55
- * @returns true only when the slot id carries a value.
56
- */
57
- export function adsslotlive(slot: adsslot | undefined): boolean {
58
- return slot !== undefined && slot.slotid.trim().length > 0;
59
- }
package/checkoutlinks.ts DELETED
@@ -1,80 +0,0 @@
1
- /**
2
- * checkoutlinks.ts — the typed accessor of the billing configuration of
3
- * the family site: the thin door the pricing page, the support page and
4
- * the intro plans zone import, answering the plan table and the support
5
- * channels of the content lane (content.ts — the manifest; db.ts — the
6
- * content_rows table; the platform gateway — GET /api/v1/ui/content). The
7
- * data never lives here anymore: checkoutlinks() and supportlinks() answer
8
- * the live rows the platform served, the manifest seed otherwise (the
9
- * uistore chain). The payment links themselves are configuration, never
10
- * code: the owner pins the checkout hrefs (Stripe Payment Links or Paddle
11
- * overlays) into the store once billing goes live. Until then every href
12
- * ships empty and the interface renders the honest empty state — "checkout
13
- * not configured yet" — instead of pretending.
14
- *
15
- * The words never ride here: every plan sentence (name, billing period,
16
- * blurb, features) answers from the label store (labels.ts — the manifest
17
- * the seed carries into the site database), so the pricing cards and the
18
- * intro plans zone read their prose through the typed accessor and this
19
- * lane keeps only the billing values — ids, prices and links.
20
- *
21
- * No network call ever happens here: the links are plain hrefs the visitor
22
- * chooses to follow, no key is committed (not even a placeholder that looks
23
- * real) and no script is loaded. The prices are placeholders (US$0 / 5 / 19
24
- * per month) until the billing platform decision lands.
25
- */
26
-
27
- import {
28
- type checkoutplan,
29
- contenttable,
30
- contentvalue,
31
- seedPlans,
32
- seedSupportLinks,
33
- type supportlinks as supportlinkstable,
34
- } from "./content.ts";
35
-
36
- export type { checkoutplan, supportlinkstable as supportlinks };
37
-
38
- /**
39
- * The typed accessor of the pricing table: the live db rows when the
40
- * platform answered, the manifest seed otherwise — in display order (the
41
- * prose answers from the label store; only the billing values ride this
42
- * lane).
43
- *
44
- * @returns the plans of the /pricing page.
45
- */
46
- export function checkoutlinks(): readonly checkoutplan[] {
47
- return contenttable("plans", seedPlans);
48
- }
49
-
50
- /**
51
- * The typed accessor of the support channels: the live db row when the
52
- * platform answered, the manifest seed otherwise — the same rule as the
53
- * plans: hrefs stay empty until the owner configures them.
54
- *
55
- * @returns the support channels of the /support page.
56
- */
57
- export function supportlinks(): Readonly<supportlinkstable> {
58
- return contentvalue("supportlinks", seedSupportLinks);
59
- }
60
-
61
- /**
62
- * Answers whether a configured link is live. An empty (or whitespace) href
63
- * means the feature is not configured yet and the interface must say so.
64
- *
65
- * @param href the configured link href.
66
- * @returns true only when the href carries a real target.
67
- */
68
- export function isconfigured(href: string): boolean {
69
- return href.trim().length > 0;
70
- }
71
-
72
- /**
73
- * Finds one plan of the table by its stable id.
74
- *
75
- * @param id the plan id (free, pro, studio).
76
- * @returns the plan, or undefined when no plan carries the id.
77
- */
78
- export function planbyid(id: string): checkoutplan | undefined {
79
- return checkoutlinks().find((plan) => plan.id === id);
80
- }