@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.
- package/App.tsx +52 -0
- package/Dockerfile +48 -0
- package/_headers +4 -0
- package/_redirects +1 -0
- package/adsconfig.ts +59 -0
- package/biome.jsonc +134 -0
- package/bun.lock +2451 -0
- package/capacitor.config.ts +51 -0
- package/checkoutlinks.ts +80 -0
- package/cli.ts +114 -0
- package/cobaia.csproj +22 -0
- package/cobaia.gemspec +14 -0
- package/cobaia.java +16 -0
- package/content.ts +425 -0
- package/db.ts +219 -0
- package/devthink.toml +5 -0
- package/docs/features.md +58 -0
- package/docs/monetization.md +61 -0
- package/familyurl.ts +22 -0
- package/favicon.svg +1 -0
- package/icon.svg +1 -0
- package/index.html +26 -0
- package/labels.ts +291 -0
- package/manifest.webmanifest +56 -0
- package/mime.types +12 -0
- package/netlify.toml +2 -0
- package/package.json +207 -4
- package/pnpm-workspace.yaml +101 -0
- package/pnpmboot.package-lock.json +157 -0
- package/pnpmboot.package.json +6 -0
- package/pom.xml +29 -0
- package/posts.ts +54 -0
- package/robots.txt +90 -0
- package/roster.ts +29 -0
- package/sandboxmodality.ts +138 -0
- package/schema.prisma +31 -0
- package/sitemap.xml +42 -0
- package/sol/Sol.tsx +91 -0
- package/sol/apps/apps.tsx +66 -0
- package/sol/blog/blog.tsx +86 -0
- package/sol/home/home.tsx +140 -0
- package/sol/intro/intro.tsx +187 -0
- package/sol/legal/legal.tsx +76 -0
- package/sol/notfound/notfound.tsx +32 -0
- package/sol/pricing/pricing.tsx +98 -0
- package/sol/shell/Shell.tsx +161 -0
- package/sol/sol.css +1951 -0
- package/sol/support/support.tsx +102 -0
- package/sol/wingets/IntroFooter.tsx +69 -0
- package/sol/wingets/IntroPlans.tsx +93 -0
- package/sol/wingets/IntroSplash.tsx +105 -0
- package/sol/wingets/ShellAdsslot.tsx +47 -0
- package/sol/wingets/ShellBrand.tsx +86 -0
- package/sol/wingets/index.ts +18 -0
- package/tests/adsconfig.test.ts +51 -0
- package/tests/checkoutlinks.test.ts +73 -0
- package/tests/familyurl.test.ts +40 -0
- package/tests/posts.test.ts +76 -0
- package/tests/sandboxmodality.test.ts +77 -0
- package/tsconfig.json +22 -0
- package/tsconfig.node.json +22 -0
- package/vercel.json +3 -0
- package/vite.config.ts +44 -0
- package/vitest.config.ts +42 -0
- package/wrangler.toml +7 -0
- 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;
|