@wenathlan/cobaia 0.0.0-stage → 2.0.155

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 (58) hide show
  1. package/App.tsx +52 -0
  2. package/Dockerfile +45 -0
  3. package/Sol/Sol.tsx +91 -0
  4. package/Sol/apps/apps.tsx +66 -0
  5. package/Sol/blog/blog.tsx +86 -0
  6. package/Sol/home/home.tsx +140 -0
  7. package/Sol/intro/intro.tsx +187 -0
  8. package/Sol/legal/legal.tsx +76 -0
  9. package/Sol/notfound/notfound.tsx +32 -0
  10. package/Sol/pricing/pricing.tsx +98 -0
  11. package/Sol/shell/Shell.tsx +161 -0
  12. package/Sol/sol.css +1944 -0
  13. package/Sol/support/support.tsx +102 -0
  14. package/Sol/wingets/IntroFooter.tsx +69 -0
  15. package/Sol/wingets/IntroPlans.tsx +93 -0
  16. package/Sol/wingets/IntroSplash.tsx +105 -0
  17. package/Sol/wingets/ShellAdsslot.tsx +47 -0
  18. package/Sol/wingets/ShellBrand.tsx +86 -0
  19. package/Sol/wingets/index.ts +18 -0
  20. package/_headers +4 -0
  21. package/_redirects +1 -0
  22. package/adsconfig.ts +59 -0
  23. package/biome.jsonc +134 -0
  24. package/bun.lock +2331 -0
  25. package/capacitor.config.ts +51 -0
  26. package/checkoutlinks.ts +80 -0
  27. package/content.ts +425 -0
  28. package/db.ts +219 -0
  29. package/devthink.toml +5 -0
  30. package/docs/.gitkeep +0 -0
  31. package/familyurl.ts +22 -0
  32. package/favicon.svg +1 -0
  33. package/icon.svg +1 -0
  34. package/index.html +26 -0
  35. package/labels.ts +291 -0
  36. package/manifest.webmanifest +56 -0
  37. package/mime.types +12 -0
  38. package/netlify.toml +2 -0
  39. package/package.json +200 -4
  40. package/pnpm-workspace.yaml +82 -0
  41. package/posts.ts +54 -0
  42. package/robots.txt +90 -0
  43. package/roster.ts +29 -0
  44. package/sandboxmodality.ts +138 -0
  45. package/schema.prisma +31 -0
  46. package/sitemap.xml +42 -0
  47. package/tests/adsconfig.test.ts +51 -0
  48. package/tests/checkoutlinks.test.ts +73 -0
  49. package/tests/familyurl.test.ts +40 -0
  50. package/tests/posts.test.ts +76 -0
  51. package/tests/sandboxmodality.test.ts +77 -0
  52. package/tsconfig.json +22 -0
  53. package/tsconfig.node.json +22 -0
  54. package/vercel.json +3 -0
  55. package/vite.config.ts +44 -0
  56. package/vitest.config.ts +42 -0
  57. package/wrangler.toml +7 -0
  58. package/README.md +0 -3
package/App.tsx ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * App — the global anchor (layer 1 of the anchor architecture) and the only
3
+ * TSX outside the cobaia theme. cobaia is the family site: the front door
4
+ * of the DevThink family — the pricing, the support page, the app directory
5
+ * and the update blog for the ten deployable applications. The site is a
6
+ * plain CONTENT surface: no onboarding, no entry gate — the home renders
7
+ * directly and every page is public; the intro (/intro) is the one bare
8
+ * presentation page the family grammar gives every app, mounted full-bleed
9
+ * under the family fullscreen law. The root manages the theme as a single
10
+ * file: it imports only the theme anchor (Sol/Sol.tsx — the file named
11
+ * after the theme folder, beside Sol/sol.css), the theme stylesheet and the
12
+ * root logics, wraps the anchor in the wouter router (the subpath the pages
13
+ * deploy owns answers through the declared base, the same mount grammar
14
+ * every sibling carries) and renders the theme into the root element —
15
+ * the site never booted without this mount (the blank cobaia defect).
16
+ * Every page and component lives behind the anchor chain (App → Sol → the
17
+ * page anchors → the loose components); nothing is imported directly.
18
+ */
19
+ import { createRoot } from "react-dom/client";
20
+ import { Router as WouterRouter } from "wouter";
21
+ import Sol from "./Sol/Sol";
22
+ import "@fontsource-variable/geist-mono";
23
+ import "@fontsource-variable/inter";
24
+ import "./Sol/sol.css";
25
+ import { seedLabels } from "./labels";
26
+
27
+ // the copy law rides the boot: the seed refreshes the label store rows from
28
+ // the manifest (the browser cache mirrors the site db rows) before the theme
29
+ // renders — best effort, the manifest always answers the wingets.
30
+ if (typeof window !== "undefined") void seedLabels().catch(() => {});
31
+
32
+ function routerBase(): string {
33
+ // the base rides the BUILD declaration only (the sibling grammar): the
34
+ // pages subpath owns /devthink/cobaia/, and that declaration arrives
35
+ // through the vite base — deriving it from the runtime path made every
36
+ // deep link look like a deployment subfolder
37
+ const declaredBase = import.meta.env.BASE_URL;
38
+ if (declaredBase !== "/" && declaredBase !== "./") return declaredBase.replace(/\/$/, "");
39
+ return "";
40
+ }
41
+
42
+ function App() {
43
+ return (
44
+ <WouterRouter base={routerBase()}>
45
+ <Sol />
46
+ </WouterRouter>
47
+ );
48
+ }
49
+
50
+ const root = document.getElementById("root");
51
+ if (!root) throw new Error("cobaia root element is missing.");
52
+ createRoot(root).render(<App />);
package/Dockerfile ADDED
@@ -0,0 +1,45 @@
1
+ # cobaia 2.0.134 — THE SITE CONTAINER FILE
2
+ #
3
+ # The family container doctrine in the sol shape: one versioned static
4
+ # container per site. The sol theme builds inside the container — stage one
5
+ # installs the locked pnpm tree and runs the vite build, stage two serves
6
+ # the built theme through the unprivileged nginx image. The version rides
7
+ # the ARG the metadata lockstep stamps and the OCI labels declare — the
8
+ # registry lanes pass it at build time, nothing is hardcoded beside the
9
+ # stamp.
10
+
11
+ FROM node:26-alpine@sha256:40cd011b55415bc894e45b02fdd5a5dfde056921b7d23a64c74f80497af48797 AS solbuild
12
+
13
+ ARG COBAIA_VERSION=2.0.134
14
+
15
+ WORKDIR /src
16
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
17
+ RUN apk add --no-cache python3 make g++ \
18
+ && npm install -g --no-fund --no-audit pnpm@12.3.4 \
19
+ && pnpm install --frozen-lockfile
20
+ COPY . .
21
+ RUN pnpm build
22
+
23
+ FROM nginxinc/nginx-unprivileged:1.29-alpine@sha256:0c79d56aee561a1d81c63f00eee5fb5fe29279560cdc55e91425133104c7fbe6
24
+
25
+ ARG COBAIA_VERSION=2.0.134
26
+
27
+ LABEL org.opencontainers.image.title="cobaia" \
28
+ org.opencontainers.image.description="cobaia — the monetization surface: the ads, the plans and the conversion, kept apart from the main sites." \
29
+ org.opencontainers.image.version="${COBAIA_VERSION}" \
30
+ org.opencontainers.image.licenses="GPL-3.0-only" \
31
+ org.opencontainers.image.source="https://github.com/wenathlan/devthink" \
32
+ org.opencontainers.image.documentation="https://github.com/wenathlan/devthink/tree/main/cobaia"
33
+
34
+ COPY --from=solbuild /src/ /usr/share/nginx/html/
35
+
36
+ # the explicit non-root runtime user (uid 101 is the nginx-unprivileged
37
+ # default): the scan gate refuses an image without a pinned unprivileged
38
+ # user, so the declaration is spelled out instead of inherited.
39
+ USER 101
40
+
41
+ # the unprivileged image answers 8080; the healthcheck probes the served
42
+ # root the same way the pages lane does.
43
+ EXPOSE 8080
44
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
45
+ CMD ["wget", "-qO-", "http://127.0.0.1:8080/"]
package/Sol/Sol.tsx ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Sol theme anchor — layer 2 of the anchor architecture.
3
+ * The file carrying the theme folder's own name (Sol/Sol.tsx, beside
4
+ * Sol/sol.css) is the path manager of the pages: it imports one anchor per
5
+ * page folder (`folder/folder.tsx`, each beside the loose components its
6
+ * folder keeps) and mounts every route of the theme. App.tsx consumes only
7
+ * this file and the theme stylesheet; no page component is ever imported
8
+ * outside this layer. When a theme folder changes its name (Moon, Aqua),
9
+ * this file follows the new name and App.tsx keeps importing the anchor by
10
+ * the folder path.
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.
21
+ */
22
+ import type { ReactNode } from "react";
23
+ import { Route, Switch } from "wouter";
24
+ import AppsAnchor from "./apps/apps";
25
+ import BlogAnchor from "./blog/blog";
26
+ import HomeAnchor from "./home/home";
27
+ import IntroAnchor from "./intro/intro";
28
+ import LegalAnchor from "./legal/legal";
29
+ import NotFoundAnchor from "./notfound/notfound";
30
+ import PricingAnchor from "./pricing/pricing";
31
+ import { Shell } from "./shell/Shell";
32
+ import SupportAnchor from "./support/support";
33
+
34
+ /** the content pages: the site chrome around one page anchor */
35
+ function Content({ children }: { children: ReactNode }) {
36
+ return <Shell>{children}</Shell>;
37
+ }
38
+
39
+ /** the unrouted addresses: the 404 answer of the cobaia, inside the site
40
+ * chrome (the theme catch-all) */
41
+ function Unrouted() {
42
+ return (
43
+ <Shell>
44
+ <NotFoundAnchor />
45
+ </Shell>
46
+ );
47
+ }
48
+
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. */
54
+ export default function Sol() {
55
+ return (
56
+ <Switch>
57
+ <Route path="/intro" component={IntroAnchor} />
58
+ <Route path="/">
59
+ <Content>
60
+ <HomeAnchor />
61
+ </Content>
62
+ </Route>
63
+ <Route path="/apps">
64
+ <Content>
65
+ <AppsAnchor />
66
+ </Content>
67
+ </Route>
68
+ <Route path="/pricing">
69
+ <Content>
70
+ <PricingAnchor />
71
+ </Content>
72
+ </Route>
73
+ <Route path="/support">
74
+ <Content>
75
+ <SupportAnchor />
76
+ </Content>
77
+ </Route>
78
+ <Route path="/blog">
79
+ <Content>
80
+ <BlogAnchor />
81
+ </Content>
82
+ </Route>
83
+ <Route path="/legal">
84
+ <Content>
85
+ <LegalAnchor />
86
+ </Content>
87
+ </Route>
88
+ <Route component={Unrouted} />
89
+ </Switch>
90
+ );
91
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * apps page anchor — layer 3 of the anchor architecture.
3
+ * The file carrying the folder's own name is the path manager of the page:
4
+ * it imports the loose components beside it (the roster data), mounts the
5
+ * page and re-exports the public component surface. Only the theme anchor
6
+ * (Sol/Sol.tsx) consumes this file.
7
+ *
8
+ * The app directory of the cobaia content site: the ten deploy units of the
9
+ * family, each card carrying the honest one-line role and the deep link
10
+ * through the familyurl contract (../slug/ — the family redirects both
11
+ * ways). The accents come from the spec §12 table mirrored in the roster;
12
+ * the roles are the ones each app declares in its own devthink.toml. The
13
+ * in-feed ad slot closes the feed at the end of the content.
14
+ */
15
+ import type { CSSProperties } from "react";
16
+ import { useContent } from "../../content.ts";
17
+ import { familyurl } from "../../familyurl";
18
+ import { label, labelfill } from "../../labels";
19
+ import { roster } from "../../roster.ts";
20
+ import { AdSlot } from "../wingets";
21
+
22
+ export function Apps() {
23
+ useContent();
24
+ return (
25
+ <div className="cb-page">
26
+ <section className="cb-hero" aria-label={label("apps.aria.hero")}>
27
+ <p className="cb-eyebrow cb-in" style={{ "--cb-d": 0 } as CSSProperties}>
28
+ {label("apps.eyebrow")}
29
+ </p>
30
+ <h1 className="cb-title cb-in" style={{ "--cb-d": 1 } as CSSProperties}>
31
+ {label("apps.title")}
32
+ </h1>
33
+ <p className="cb-lede cb-in" style={{ "--cb-d": 2 } as CSSProperties}>
34
+ {label("apps.lede")}
35
+ </p>
36
+ </section>
37
+
38
+ <section className="cb-section" aria-label={label("apps.section.rosterAria")}>
39
+ <div className="cb-appgrid">
40
+ {roster().map((member, index) => (
41
+ <a
42
+ key={member.slug}
43
+ className="cb-appcard cb-in"
44
+ href={familyurl(member.slug)}
45
+ target="_blank"
46
+ rel="noreferrer"
47
+ style={{ "--cb-dot": member.accent, "--cb-d": Math.min(index + 1, 6) } as CSSProperties}
48
+ >
49
+ <span className="cb-apphead">
50
+ <span className="cb-appdot" aria-hidden="true" />
51
+ <span className="cb-appname">{member.slug}</span>
52
+ </span>
53
+ <span className="cb-approle">{member.role}</span>
54
+ <span className="cb-appopen">{labelfill("apps.card.open", { slug: member.slug })}</span>
55
+ </a>
56
+ ))}
57
+ </div>
58
+ </section>
59
+
60
+ {/* the one reserved ad slot of the page: after the feed it belongs to */}
61
+ <AdSlot page="apps" />
62
+ </div>
63
+ );
64
+ }
65
+
66
+ export default Apps;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * blog page anchor — layer 3 of the anchor architecture.
3
+ * The file carrying the folder's own name is the path manager of the page:
4
+ * it imports the loose components beside it, mounts the page and re-exports
5
+ * the public component surface. Only the theme anchor (Sol/Sol.tsx) consumes
6
+ * this file.
7
+ *
8
+ * The update blog of the cobaia content site: the sorted index (the pure
9
+ * posts module at the app root owns the posts and the ordering) and the full
10
+ * bodies rendered below as real articles — the in-article surface of the
11
+ * site. Every post title wears the trylle display type (the clamp, the
12
+ * -0.02em track) so the articles open like the site hero does. The tree
13
+ * rule keeps this one page (no post subfolders), so every post anchors in
14
+ * place and the index links jump to the article.
15
+ */
16
+ import type { CSSProperties } from "react";
17
+ import { useContent } from "../../content.ts";
18
+ import { label, labelfill } from "../../labels";
19
+ import { type blogpost, postindex, postwords } from "../../posts";
20
+ import { AdSlot } from "../wingets";
21
+
22
+ /** the meta line of one post: the date and the honest word count. */
23
+ function PostMeta({ post }: { post: blogpost }) {
24
+ return (
25
+ <p className="cb-postdate">
26
+ {labelfill("blog.post.meta", {
27
+ date: post.date,
28
+ words: String(postwords(post)),
29
+ tags: post.tags.join(" / "),
30
+ })}
31
+ </p>
32
+ );
33
+ }
34
+
35
+ export function Blog() {
36
+ useContent();
37
+ const posts = postindex();
38
+ return (
39
+ <div className="cb-page">
40
+ <section className="cb-hero" aria-label={label("blog.aria.hero")}>
41
+ <p className="cb-eyebrow cb-in" style={{ "--cb-d": 0 } as CSSProperties}>
42
+ {label("blog.eyebrow")}
43
+ </p>
44
+ <h1 className="cb-title cb-in" style={{ "--cb-d": 1 } as CSSProperties}>
45
+ {label("blog.title")}
46
+ </h1>
47
+ <p className="cb-lede cb-in" style={{ "--cb-d": 2 } as CSSProperties}>
48
+ {label("blog.lede")}
49
+ </p>
50
+ </section>
51
+
52
+ <section className="cb-section" aria-label={label("blog.section.indexAria")}>
53
+ <ul className="cb-postlist">
54
+ {posts.map((post, index) => (
55
+ <li key={post.slug} className="cb-postrow cb-in" style={{ "--cb-d": index + 1 } as CSSProperties}>
56
+ <PostMeta post={post} />
57
+ <a className="cb-postlink" href={`#${post.slug}`}>
58
+ {post.title}
59
+ </a>
60
+ <p className="cb-postexcerpt">{post.excerpt}</p>
61
+ </li>
62
+ ))}
63
+ </ul>
64
+ </section>
65
+
66
+ <section className="cb-section" aria-label={label("blog.section.fullAria")}>
67
+ {posts.map((post) => (
68
+ <article key={post.slug} id={post.slug} className="cb-article" aria-label={post.title}>
69
+ <h2 className="cb-posttitle">{post.title}</h2>
70
+ <PostMeta post={post} />
71
+ <div className="cb-articlebody">
72
+ {post.body.map((paragraph) => (
73
+ <p key={paragraph}>{paragraph}</p>
74
+ ))}
75
+ </div>
76
+ </article>
77
+ ))}
78
+ </section>
79
+
80
+ {/* the one reserved ad slot of the page, after the articles */}
81
+ <AdSlot page="blog" />
82
+ </div>
83
+ );
84
+ }
85
+
86
+ export default Blog;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * home page anchor — layer 3 of the anchor architecture.
3
+ * The file carrying the folder's own name is the path manager of the page:
4
+ * it imports the loose components beside it, mounts the page and re-exports
5
+ * the public component surface. Only the theme anchor (Sol/Sol.tsx) consumes
6
+ * this file.
7
+ *
8
+ * The landing of the cobaia content site: what the devthink family is, said
9
+ * directly — ten deployable applications on one engine, shipped as static
10
+ * sites, tracked nowhere. Left-aligned hero (no centered hero + pill), the
11
+ * three how-it-works steps on hairline seams (01 engine · 02 static · 03
12
+ * honest), a four-card preview of the directory leading to /apps, the
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.
16
+ * Styled entirely by Sol/sol.css.
17
+ */
18
+ import type { CSSProperties } from "react";
19
+ import { Link } from "wouter";
20
+ import { useContent } from "../../content.ts";
21
+ import { familyurl } from "../../familyurl";
22
+ import { type LabelKey, label, labelfill } from "../../labels";
23
+ import { roster } from "../../roster.ts";
24
+ import { AdSlot, BrandMark } from "../wingets";
25
+
26
+ /** the featured deploy units of the landing preview (the full directory
27
+ * lives on /apps): the OS, the engine, the store and one studio — read
28
+ * from the one roster table so the preview can never drift from /apps. */
29
+ const FEATUREDSLUGS: readonly string[] = ["devthink", "saddle", "vault", "cadria"];
30
+ /** the featured rows answer the accessor at render time (the content
31
+ * lane law): the seed answers the first paint and the useContent mount
32
+ * refreshes the derivation when the live rows land. */
33
+ const featured = () => roster().filter((member) => FEATUREDSLUGS.includes(member.slug));
34
+
35
+ /** the three steps of the landing: what the family is made of — the text
36
+ * rides the label accessor (the store keeps the sentence). */
37
+ const STEPS: readonly { n: LabelKey; title: LabelKey; text: LabelKey }[] = [
38
+ { n: "home.steps.engine.n", title: "home.steps.engine.title", text: "home.steps.engine.text" },
39
+ { n: "home.steps.static.n", title: "home.steps.static.title", text: "home.steps.static.text" },
40
+ { n: "home.steps.honest.n", title: "home.steps.honest.title", text: "home.steps.honest.text" },
41
+ ];
42
+
43
+ export function Home() {
44
+ useContent();
45
+ return (
46
+ <div className="cb-page">
47
+ {/* the hero: the family said plainly */}
48
+ <section className="cb-hero" aria-label={label("home.aria.hero")}>
49
+ <div className="cb-in" style={{ "--cb-d": 0 } as CSSProperties}>
50
+ <BrandMark size={52} />
51
+ </div>
52
+ <p className="cb-eyebrow cb-in" style={{ "--cb-d": 1 } as CSSProperties}>
53
+ {label("home.eyebrow")}
54
+ </p>
55
+ <h1 className="cb-title cb-in" style={{ "--cb-d": 2 } as CSSProperties}>
56
+ {label("home.title")}
57
+ </h1>
58
+ <p className="cb-lede cb-in" style={{ "--cb-d": 3 } as CSSProperties}>
59
+ {label("home.lede")}
60
+ </p>
61
+ <div className="cb-btnrow cb-in" style={{ "--cb-d": 4 } as CSSProperties}>
62
+ <Link className="cb-btn" href="/pricing">
63
+ {label("home.action.pricing")}
64
+ </Link>
65
+ <Link className="cb-btn cb-btn--ghost" href="/apps">
66
+ {label("home.action.apps")}
67
+ </Link>
68
+ </div>
69
+ </section>
70
+
71
+ {/* the three steps, on hairline seams */}
72
+ <section className="cb-section" aria-label={label("home.section.stepsAria")}>
73
+ <div className="cb-secthead cb-in" style={{ "--cb-d": 0 } as CSSProperties}>
74
+ <h2 className="cb-secttitle">{label("home.section.stepsTitle")}</h2>
75
+ <p className="cb-sectlede">{label("home.section.stepsLede")}</p>
76
+ </div>
77
+ <div className="cb-grid cb-grid--3">
78
+ {STEPS.map((step, index) => (
79
+ <div key={step.n} className="cb-card cb-in" style={{ "--cb-d": index + 1 } as CSSProperties}>
80
+ <p className="cb-cardmeta">{label(step.n)}</p>
81
+ <h3 className="cb-cardtitle">{label(step.title)}</h3>
82
+ <p className="cb-cardtext">{label(step.text)}</p>
83
+ </div>
84
+ ))}
85
+ </div>
86
+ </section>
87
+
88
+ {/* the directory preview: four of the ten, the rest on /apps */}
89
+ <section className="cb-section" aria-label={label("home.section.previewAria")}>
90
+ <div className="cb-secthead cb-in" style={{ "--cb-d": 0 } as CSSProperties}>
91
+ <h2 className="cb-secttitle">{label("home.section.previewTitle")}</h2>
92
+ <p className="cb-sectlede">
93
+ {label("home.section.previewLede")}{" "}
94
+ <Link className="cb-link" href="/apps">
95
+ {label("home.section.previewLink")}
96
+ </Link>
97
+ {label("home.section.previewTail")}
98
+ </p>
99
+ </div>
100
+ <div className="cb-appgrid">
101
+ {featured().map((app, index) => (
102
+ <a
103
+ key={app.slug}
104
+ className="cb-appcard cb-in"
105
+ href={familyurl(app.slug)}
106
+ target="_blank"
107
+ rel="noreferrer"
108
+ style={{ "--cb-dot": app.accent, "--cb-d": index + 1 } as CSSProperties}
109
+ >
110
+ <span className="cb-apphead">
111
+ <span className="cb-appdot" aria-hidden="true" />
112
+ <span className="cb-appname">{app.slug}</span>
113
+ </span>
114
+ <span className="cb-approle">{app.role}</span>
115
+ <span className="cb-appopen">{labelfill("home.appcard.open", { slug: app.slug })}</span>
116
+ </a>
117
+ ))}
118
+ </div>
119
+ </section>
120
+
121
+ {/* the support strip: the honest ask */}
122
+ <section className="cb-section" aria-label={label("home.section.supportAria")}>
123
+ <div className="cb-secthead cb-in" style={{ "--cb-d": 0 } as CSSProperties}>
124
+ <h2 className="cb-secttitle">{label("home.section.supportTitle")}</h2>
125
+ <p className="cb-sectlede">{label("home.section.supportLede")}</p>
126
+ </div>
127
+ <div className="cb-btnrow cb-in" style={{ "--cb-d": 1 } as CSSProperties}>
128
+ <Link className="cb-btn cb-btn--ghost" href="/support">
129
+ {label("home.action.support")}
130
+ </Link>
131
+ </div>
132
+ </section>
133
+
134
+ {/* the one reserved ad slot of the page, at the end of the content */}
135
+ <AdSlot page="home" />
136
+ </div>
137
+ );
138
+ }
139
+
140
+ export default Home;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * intro page anchor — layer 3 of the anchor architecture. The file carrying
3
+ * the folder's own name is the path manager of the page: it imports the
4
+ * loose components beside it, mounts the page and re-exports the public
5
+ * component surface. Only the theme anchor (Sol/Sol.tsx) consumes this file.
6
+ *
7
+ * The PRESENTATION page of cobaia — the family grammar's last seat (every
8
+ * app of the family carries one): the page mounts BARE at /intro, no window
9
+ * chrome, riding the site voice, not the frame. The akash splash opens over
10
+ * the landing (the mark spring on the overshoot curve, the name rise, the
11
+ * role fade; click/tap/key skip; reduced motion never plays; the no storage
12
+ * law holds — no seen flag, the splash plays per visit and writes nothing),
13
+ * then the landing answers: the hero says what the surface is (the
14
+ * monetization face kept apart so the main sites stay clean), the plans
15
+ * zone renders the real offer from the one checkoutlinks table (the not
16
+ * configured state stays honest), the doors panes open the app directory
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,
21
+ * React state only; zero decorative dots; zero invented numbers and zero
22
+ * fake testimonials — the real offer, read from the data modules.
23
+ */
24
+
25
+ import type { CSSProperties } from "react";
26
+ import { useCallback, useEffect, useState } from "react";
27
+ import { Link } from "wouter";
28
+ import { label } from "../../labels";
29
+ import { BrandMark, IntroFooter, IntroPlans, IntroSplash } from "../wingets";
30
+
31
+ /** the lifecycle of the opening act: resolving (the motion preference
32
+ * decides) → playing (the akash floor over the landing) → done (the
33
+ * landing alone). No seen state is consulted — the no storage law. */
34
+ type SplashPhase = "resolving" | "playing" | "done";
35
+
36
+ /** the door glyph of the panes: the inline 10×10 chevron the chrome's
37
+ * caption grammar draws (the app carries no icon dependency). */
38
+ function DoorGlyph() {
39
+ return (
40
+ <svg
41
+ className="cb-intropane__glyph"
42
+ width="10"
43
+ height="10"
44
+ viewBox="0 0 10 10"
45
+ aria-hidden="true"
46
+ focusable="false"
47
+ >
48
+ <path d="M3 0l5 5-5 5" fill="none" stroke="currentColor" strokeWidth="1.5" />
49
+ </svg>
50
+ );
51
+ }
52
+
53
+ /**
54
+ * The intro page: the splash plays over the landing, then the bare
55
+ * presentation answers — hero, plans, doors, footer.
56
+ *
57
+ * @returns the intro element (null while the motion preference decides).
58
+ */
59
+ export function Intro() {
60
+ const [splash, setSplash] = useState<SplashPhase>("resolving");
61
+
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)
64
+ useEffect(() => {
65
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
66
+ setSplash("done");
67
+ return;
68
+ }
69
+ setSplash("playing");
70
+ }, []);
71
+
72
+ /** the slide completed: the splash unmounts, the landing owns the page. */
73
+ const onSplashDone = useCallback(() => {
74
+ setSplash("done");
75
+ }, []);
76
+
77
+ /** the quiet cue: walks the page to the plans band. */
78
+ const cuePlans = useCallback(() => {
79
+ const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
80
+ document.getElementById("intro-plans")?.scrollIntoView({ behavior: reduced ? "auto" : "smooth", block: "start" });
81
+ }, []);
82
+
83
+ // the phase decision holds the paint: nothing renders while it resolves —
84
+ // no flash of the landing under a splash that will never play
85
+ if (splash === "resolving") return null;
86
+
87
+ return (
88
+ <div className="cb-intro" data-page="intro">
89
+ {/* the skip link rides outside no frame — the bare page answers the
90
+ * viewport directly; the splash sits above it while it plays */}
91
+ <a className="cb-skip" href="#intro-content">
92
+ {label("shell.skip")}
93
+ </a>
94
+
95
+ {/* the akash floor: the opening act rides over the landing until its
96
+ * slide completes; reduced motion never mounts it */}
97
+ {splash === "playing" && <IntroSplash onDone={onSplashDone} />}
98
+
99
+ {/* the intro bar: the wordmark and the two quiet doors — the ONE
100
+ * primary cta lives in the hero */}
101
+ <header className="cb-introbar">
102
+ <div className="cb-introbar__in">
103
+ <span className="cb-introbar__brand">
104
+ <BrandMark size={20} />
105
+ <span className="cb-introbar__word">{label("shell.name")}</span>
106
+ </span>
107
+ <span className="cb-introbar__spacer" />
108
+ <nav className="cb-introbar__nav" aria-label={label("intro.bar.aria")}>
109
+ <Link className="cb-introdoor" href="/apps">
110
+ {label("shell.foot.apps")}
111
+ </Link>
112
+ <Link className="cb-introdoor" href="/pricing">
113
+ {label("shell.foot.pricing")}
114
+ </Link>
115
+ </nav>
116
+ </div>
117
+ </header>
118
+
119
+ <main id="intro-content">
120
+ {/* the hero: what the surface is, said directly */}
121
+ <section className="cb-introhero" aria-label={label("intro.aria.page")}>
122
+ <div className="cb-introwrap">
123
+ <span className="cb-introhero__mark cb-in" style={{ "--cb-d": 0 } as CSSProperties} aria-hidden="true">
124
+ <BrandMark size={64} />
125
+ </span>
126
+ <p className="cb-eyebrow cb-in" style={{ "--cb-d": 1 } as CSSProperties}>
127
+ {label("intro.hero.eyebrow")}
128
+ </p>
129
+ <h1 className="cb-title cb-in" style={{ "--cb-d": 2 } as CSSProperties}>
130
+ {label("intro.hero.title")}
131
+ </h1>
132
+ <p className="cb-lede cb-in" style={{ "--cb-d": 3 } as CSSProperties}>
133
+ {label("intro.hero.lede")}
134
+ </p>
135
+ <div className="cb-btnrow cb-in" style={{ "--cb-d": 4 } as CSSProperties}>
136
+ <Link className="cb-btn" href="/">
137
+ {label("intro.hero.cta")}
138
+ </Link>
139
+ <button type="button" className="cb-btn cb-btn--ghost" onClick={cuePlans}>
140
+ {label("intro.hero.cue")}
141
+ </button>
142
+ </div>
143
+ <p className="cb-note cb-in" style={{ "--cb-d": 5 } as CSSProperties}>
144
+ {label("intro.hero.note")}
145
+ </p>
146
+ </div>
147
+ </section>
148
+
149
+ {/* the plans as they really are (the one checkoutlinks table) */}
150
+ <IntroPlans />
151
+
152
+ {/* the doors: the app directory and the legal page, on silk panes */}
153
+ <section className="cb-introband cb-introband--deep" aria-label={label("intro.doors.aria")}>
154
+ <div className="cb-introwrap">
155
+ <div className="cb-secthead cb-in" style={{ "--cb-d": 0 } as CSSProperties}>
156
+ <p className="cb-eyebrow">{label("intro.doors.eyebrow")}</p>
157
+ <h2 className="cb-secttitle">{label("intro.doors.title")}</h2>
158
+ </div>
159
+ <div className="cb-introdoorgrid">
160
+ <article className="cb-intropane cb-in" style={{ "--cb-d": 1 } as CSSProperties}>
161
+ <p className="cb-cardmeta">{label("intro.doors.apps")}</p>
162
+ <p className="cb-intropane__text">{label("intro.doors.appsText")}</p>
163
+ <Link className="cb-intropane__door" href="/apps">
164
+ {label("intro.doors.appsCta")}
165
+ <DoorGlyph />
166
+ </Link>
167
+ </article>
168
+ <article className="cb-intropane cb-in" style={{ "--cb-d": 2 } as CSSProperties}>
169
+ <p className="cb-cardmeta">{label("intro.doors.legal")}</p>
170
+ <p className="cb-intropane__text">{label("intro.doors.legalText")}</p>
171
+ <Link className="cb-intropane__door" href="/legal">
172
+ {label("intro.doors.legalCta")}
173
+ <DoorGlyph />
174
+ </Link>
175
+ </article>
176
+ </div>
177
+ </div>
178
+ </section>
179
+ </main>
180
+
181
+ {/* the footer carries the doors: the site row, the family row, the line */}
182
+ <IntroFooter />
183
+ </div>
184
+ );
185
+ }
186
+
187
+ export default Intro;