@wenathlan/cobaia 0.0.0-stage → 2.0.158

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 (66) hide show
  1. package/App.tsx +52 -0
  2. package/Dockerfile +48 -0
  3. package/_headers +4 -0
  4. package/_redirects +1 -0
  5. package/adsconfig.ts +59 -0
  6. package/biome.jsonc +134 -0
  7. package/bun.lock +2451 -0
  8. package/capacitor.config.ts +51 -0
  9. package/checkoutlinks.ts +80 -0
  10. package/cli.ts +114 -0
  11. package/cobaia.csproj +22 -0
  12. package/cobaia.gemspec +14 -0
  13. package/cobaia.java +16 -0
  14. package/content.ts +425 -0
  15. package/db.ts +219 -0
  16. package/devthink.toml +5 -0
  17. package/docs/features.md +58 -0
  18. package/docs/monetization.md +61 -0
  19. package/familyurl.ts +22 -0
  20. package/favicon.svg +1 -0
  21. package/icon.svg +1 -0
  22. package/index.html +26 -0
  23. package/labels.ts +291 -0
  24. package/manifest.webmanifest +56 -0
  25. package/mime.types +12 -0
  26. package/netlify.toml +2 -0
  27. package/package.json +207 -4
  28. package/pnpm-workspace.yaml +101 -0
  29. package/pnpmboot.package-lock.json +157 -0
  30. package/pnpmboot.package.json +6 -0
  31. package/pom.xml +29 -0
  32. package/posts.ts +54 -0
  33. package/robots.txt +90 -0
  34. package/roster.ts +29 -0
  35. package/sandboxmodality.ts +138 -0
  36. package/schema.prisma +31 -0
  37. package/sitemap.xml +42 -0
  38. package/sol/Sol.tsx +91 -0
  39. package/sol/apps/apps.tsx +66 -0
  40. package/sol/blog/blog.tsx +86 -0
  41. package/sol/home/home.tsx +140 -0
  42. package/sol/intro/intro.tsx +187 -0
  43. package/sol/legal/legal.tsx +76 -0
  44. package/sol/notfound/notfound.tsx +32 -0
  45. package/sol/pricing/pricing.tsx +98 -0
  46. package/sol/shell/Shell.tsx +161 -0
  47. package/sol/sol.css +1951 -0
  48. package/sol/support/support.tsx +102 -0
  49. package/sol/wingets/IntroFooter.tsx +69 -0
  50. package/sol/wingets/IntroPlans.tsx +93 -0
  51. package/sol/wingets/IntroSplash.tsx +105 -0
  52. package/sol/wingets/ShellAdsslot.tsx +47 -0
  53. package/sol/wingets/ShellBrand.tsx +86 -0
  54. package/sol/wingets/index.ts +18 -0
  55. package/tests/adsconfig.test.ts +51 -0
  56. package/tests/checkoutlinks.test.ts +73 -0
  57. package/tests/familyurl.test.ts +40 -0
  58. package/tests/posts.test.ts +76 -0
  59. package/tests/sandboxmodality.test.ts +77 -0
  60. package/tsconfig.json +22 -0
  61. package/tsconfig.node.json +22 -0
  62. package/vercel.json +3 -0
  63. package/vite.config.ts +44 -0
  64. package/vitest.config.ts +42 -0
  65. package/wrangler.toml +7 -0
  66. package/README.md +0 -3
@@ -0,0 +1,138 @@
1
+ // # sandboxmodality — the workload lane declaration of the cobaia
2
+ // application (root layer, wave WL1): the lanes this app owns in the
3
+ // family workload catalog, mirrored from the engine of record
4
+ // (saddle/workloadlaw.ts — the FAMILYLANES table) as the string contract
5
+ // the deploy and the platform speak. The mirror law (the same discipline
6
+ // the devthink modalities vendor rides): the rows are mirrored, not
7
+ // imported, because the published saddle envelope the container build
8
+ // installs does not carry the workload law yet and the family law bars a
9
+ // relative path across apps — the day the envelope publishes, the ids
10
+ // answer the same strings (the lockstep is the string contract, not the
11
+ // import). The app battery pins the mirror against the engine source
12
+ // through the library bridge so the vendor can never drift in silence.
13
+ //
14
+ // The seat law (the deploy's opt-in, the family's shared shape): the
15
+ // deploy pins COBAIA_SANDBOX_LANE — "local" (the default, the deploy's own process),
16
+ // "join" (the platform resolves every lane through the engine's join
17
+ // resolver — the nuclear modality law: the workload never names a
18
+ // modality, the join answers), or a modality id the deploy pins for the
19
+ // app's lanes (named here, validated by the platform's join at dispatch —
20
+ // this module never judges engine vocabulary, so the mirror cannot drift
21
+ // when the registry grows). The module never spawns anything and resolves
22
+ // no join: it names the lanes' truth; the platform resolves the sandbox.
23
+ // Zero consumer data, zero imports.
24
+
25
+ /** one mirrored workload lane row — the same five keys the engine law pins
26
+ * (app, lane, role, demand, today). */
27
+ export type AppWorkloadLane = {
28
+ /** the official app slug every row carries. */
29
+ readonly app: string;
30
+ /** the lane id inside the app (kebab-case, the engine catalog id). */
31
+ readonly lane: string;
32
+ /** what the lane does, in one sentence (the engine row's sentence). */
33
+ readonly role: string;
34
+ /** the demand the lane puts on the sandbox (gpu, network, isolation,
35
+ * boot budget — never a modality name: the nuclear law). */
36
+ readonly demand: {
37
+ readonly gpu: boolean;
38
+ readonly network: boolean;
39
+ readonly isolation: "process" | "namespace" | "vm" | "hardware";
40
+ readonly bootbudget?: number;
41
+ };
42
+ /** where the heavy pass runs today — the honest truth the census reads. */
43
+ readonly today: "engine-lane" | "app-lane";
44
+ };
45
+
46
+ /** the app slug every mirrored row carries. */
47
+ export const APPSLUG = "cobaia";
48
+
49
+ /** the workload lanes this app owns — the mirror of the engine catalog
50
+ * rows (saddle/workloadlaw.ts, the FAMILYLANES table; the battery pins
51
+ * the two surfaces equal). */
52
+ export const WORKLOADLANES: readonly AppWorkloadLane[] = Object.freeze([
53
+ {
54
+ app: "cobaia",
55
+ lane: "plan-checkout",
56
+ role: "the plan checkout surface: mrr plan rows, link validation and the billing webhook walk when it lands, secrets server-side",
57
+ demand: { gpu: false, network: true, isolation: "process", bootbudget: 25 },
58
+ today: "app-lane",
59
+ },
60
+ {
61
+ app: "cobaia",
62
+ lane: "ads-slot",
63
+ role: "the honest ad slot lane: the reserved slots and the rotation decision the deploy answers before the render",
64
+ demand: { gpu: false, network: true, isolation: "process", bootbudget: 25 },
65
+ today: "app-lane",
66
+ },
67
+ {
68
+ app: "cobaia",
69
+ lane: "label-seed",
70
+ role: "the label manifest seed of the monetization face: the store walk the sqlite vault carries",
71
+ demand: { gpu: false, network: false, isolation: "process", bootbudget: 25 },
72
+ today: "app-lane",
73
+ },
74
+ ]);
75
+
76
+ /** the environment name the deploy pins the sandbox lane seat through
77
+ * (the opt-in seat — documented in the env example, zero hardcode). */
78
+ export const SANDBOXLANEENV = "COBAIA_SANDBOX_LANE";
79
+
80
+ /** the typed answer of the seat: the declaration the deploy reads —
81
+ * local (the lanes run in the deploy's own process), join (the platform
82
+ * resolves every lane through the engine's join resolver), or the pinned
83
+ * modality id the deploy named (validated by the platform's join at
84
+ * dispatch, never judged here). every answer is honest: an absent or
85
+ * blank choice answers the declared default, never a silent switch. */
86
+ export type LaneSeatResult =
87
+ | { ok: true; seat: "local"; sandboxed: false }
88
+ | { ok: true; seat: "join"; sandboxed: true; pinned: false }
89
+ | { ok: true; seat: string; sandboxed: true; pinned: true };
90
+
91
+ /**
92
+ * Resolves the sandbox lane seat of this application — the deploy's
93
+ * opt-in: an absent or blank choice answers the declared default ("local"
94
+ * — the lanes run in the deploy's own process); "join" opts every lane
95
+ * onto the platform's join resolver (the nuclear modality law); any other
96
+ * non-empty choice is the modality id the deploy pins for the lanes
97
+ * (named by the deploy, validated by the platform's join at dispatch —
98
+ * this module never judges engine vocabulary, so the mirror cannot drift
99
+ * when the registry grows).
100
+ *
101
+ * @param choice the deploy's choice (the env value or the config row).
102
+ * @returns the seat declaration.
103
+ */
104
+ export function lanesandbox(choice: string | undefined): LaneSeatResult {
105
+ const name = choice?.trim() ?? "";
106
+ if (name.length === 0 || name === "local") {
107
+ return { ok: true, seat: "local", sandboxed: false };
108
+ }
109
+ if (name === "join") {
110
+ return { ok: true, seat: "join", sandboxed: true, pinned: false };
111
+ }
112
+ return { ok: true, seat: name, sandboxed: true, pinned: true };
113
+ }
114
+
115
+ /**
116
+ * Reads the lane seat off one environment lookup — the deploy pins
117
+ * COBAIA_SANDBOX_LANE (opt-in per deploy); the lookup is injected so the server lane
118
+ * hands process.env and any other lane hands its own seat. this module
119
+ * never spawns anything: it names the truth the platform resolves.
120
+ *
121
+ * @param lookup the environment lookup function (the env carrier).
122
+ * @returns the seat declaration.
123
+ */
124
+ export function lanesandboxfromenv(lookup: (key: string) => string | undefined): LaneSeatResult {
125
+ return lanesandbox(lookup(SANDBOXLANEENV));
126
+ }
127
+
128
+ /**
129
+ * Answers one mirrored lane row by lane id — the lookup the app surfaces
130
+ * and the batteries read.
131
+ *
132
+ * @param lane the lane id inside the app.
133
+ * @returns the mirrored row, or undefined when the app carries no such
134
+ * lane.
135
+ */
136
+ export function laneof(lane: string): AppWorkloadLane | undefined {
137
+ return WORKLOADLANES.find((row) => row.lane === lane);
138
+ }
package/schema.prisma ADDED
@@ -0,0 +1,31 @@
1
+ // cobaia prisma schema — the self-hosted database of the family site. cobaia
2
+ // is the monetization surface of the family: the content site that carries
3
+ // the plans, the support rails and the honest advertisement slots while the
4
+ // family apps stay clean.
5
+ //
6
+ // family sync contract (devthink master): a table of the same kind carries
7
+ // the same columns and the same pointers in every application that holds
8
+ // the kind — identity pointers are text, counts are 32-bit ints, time is
9
+ // datetime or iso text, and byte-scale pointers (sizes, lengths, offsets)
10
+ // are 64-bit BigInt, the pointer that holds terabytes. The sqlite
11
+ // autoincrement rowid is 64-bit by the engine contract. This file is the
12
+ // documentation of record: the runtime runs better-sqlite3 beside it and
13
+ // mirrors the same columns in its boot DDL (db.ts).
14
+ generator client {
15
+ provider = "prisma-client-js"
16
+ }
17
+
18
+ datasource db {
19
+ provider = "sqlite"
20
+ url = env("DATABASE_URL")
21
+ }
22
+
23
+ /// the first-run state of one visitor (the platform contract the gateway
24
+ /// serves over /api/v1/onboarding)
25
+ model OnboardingState {
26
+ id Int @id @default(autoincrement())
27
+ visitor String @unique
28
+ introseen Boolean @default(false)
29
+ onboardingseen Boolean @default(false)
30
+ updatedAt DateTime @updatedAt
31
+ }
package/sitemap.xml ADDED
@@ -0,0 +1,42 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!-- cobaia — the family site. No domain is hardcoded. The loc entries are
3
+ relative and resolve against whichever origin serves this sitemap; the
4
+ canonical domain binds on the deploy platform. -->
5
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
6
+ <url>
7
+ <loc>/</loc>
8
+ <lastmod>2026-10-10</lastmod>
9
+ <changefreq>weekly</changefreq>
10
+ <priority>1.0</priority>
11
+ </url>
12
+ <url>
13
+ <loc>/apps</loc>
14
+ <lastmod>2026-10-10</lastmod>
15
+ <changefreq>weekly</changefreq>
16
+ <priority>0.9</priority>
17
+ </url>
18
+ <url>
19
+ <loc>/pricing</loc>
20
+ <lastmod>2026-10-10</lastmod>
21
+ <changefreq>monthly</changefreq>
22
+ <priority>0.9</priority>
23
+ </url>
24
+ <url>
25
+ <loc>/blog</loc>
26
+ <lastmod>2026-10-06</lastmod>
27
+ <changefreq>weekly</changefreq>
28
+ <priority>0.8</priority>
29
+ </url>
30
+ <url>
31
+ <loc>/support</loc>
32
+ <lastmod>2026-10-10</lastmod>
33
+ <changefreq>monthly</changefreq>
34
+ <priority>0.6</priority>
35
+ </url>
36
+ <url>
37
+ <loc>/legal</loc>
38
+ <lastmod>2026-10-10</lastmod>
39
+ <changefreq>monthly</changefreq>
40
+ <priority>0.5</priority>
41
+ </url>
42
+ </urlset>
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;