@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.
- package/App.tsx +52 -0
- package/Dockerfile +45 -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 +1944 -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/_headers +4 -0
- package/_redirects +1 -0
- package/adsconfig.ts +59 -0
- package/biome.jsonc +134 -0
- package/bun.lock +2331 -0
- package/capacitor.config.ts +51 -0
- package/checkoutlinks.ts +80 -0
- package/content.ts +425 -0
- package/db.ts +219 -0
- package/devthink.toml +5 -0
- package/docs/.gitkeep +0 -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 +200 -4
- package/pnpm-workspace.yaml +82 -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/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
package/db.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* db.ts — the self-hosted sqlite runtime of the cobaia: the better-sqlite3
|
|
3
|
+
* bootstrap and the parameterized query helpers of the family site. Every
|
|
4
|
+
* statement is prepared and bound — no sql is ever assembled by string
|
|
5
|
+
* concatenation. The schema mirrors schema.prisma (the documentation of
|
|
6
|
+
* record) and the boot DDL is the idempotent seed-on-first-run: the tables
|
|
7
|
+
* exist after the first open, always. The platform gateway serves the same
|
|
8
|
+
* tables over HTTPS (GET/POST /api/v1/onboarding); no server route lives
|
|
9
|
+
* here.
|
|
10
|
+
*/
|
|
11
|
+
import { openStorageLane } from "@wenathlan/vault/storagelane";
|
|
12
|
+
import { LABEL_MANIFEST, LABEL_REVISION } from "./labels";
|
|
13
|
+
|
|
14
|
+
/** the correlated tables the runtime keeps (the family sync contract names). */
|
|
15
|
+
export const tables = ["onboarding_states", "labels"] as const;
|
|
16
|
+
|
|
17
|
+
/** the one storage lane of the site store (WAL, foreign keys, patient locks — the app's own file, the shared mechanism of the vault). */
|
|
18
|
+
const lane = openStorageLane({ defaultFile: "./cobaia.db" });
|
|
19
|
+
|
|
20
|
+
/** the one sqlite connection of the runtime (re-exported seat of the lane). */
|
|
21
|
+
export const db = lane.db;
|
|
22
|
+
|
|
23
|
+
/** the parameterized helpers of the site store (re-exported seats of the lane). */
|
|
24
|
+
export const { execute, queryAll, queryOne } = lane;
|
|
25
|
+
|
|
26
|
+
/** the parameter types the helpers bind (re-exported seat of the lane). */
|
|
27
|
+
export type { SqlParam } from "@wenathlan/vault/storagelane";
|
|
28
|
+
|
|
29
|
+
const SCHEMA = `
|
|
30
|
+
CREATE TABLE IF NOT EXISTS onboarding_states (
|
|
31
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
32
|
+
visitor TEXT NOT NULL UNIQUE,
|
|
33
|
+
introseen INTEGER NOT NULL DEFAULT 0,
|
|
34
|
+
onboardingseen INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
updatedAt TEXT NOT NULL DEFAULT (datetime('now'))
|
|
36
|
+
);
|
|
37
|
+
CREATE TABLE IF NOT EXISTS labels (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
text TEXT NOT NULL,
|
|
40
|
+
revision INTEGER NOT NULL DEFAULT 0,
|
|
41
|
+
updatedAt TEXT NOT NULL DEFAULT (datetime('now')),
|
|
42
|
+
translated INTEGER NOT NULL DEFAULT 0
|
|
43
|
+
);
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
/** Creates the tables when they do not exist (idempotent, mirrors the prisma schema). */
|
|
47
|
+
export function ensureSchema(): void {
|
|
48
|
+
lane.exec(SCHEMA);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// the boot seed: the tables exist from the first open, always (idempotent).
|
|
52
|
+
ensureSchema();
|
|
53
|
+
|
|
54
|
+
/** one row of the onboarding_states table (the entry-flow gates of a visitor). */
|
|
55
|
+
export type OnboardingStateRow = {
|
|
56
|
+
id: number;
|
|
57
|
+
visitor: string;
|
|
58
|
+
introseen: number;
|
|
59
|
+
onboardingseen: number;
|
|
60
|
+
updatedAt: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** the stage flags the entry flow consults, as booleans. */
|
|
64
|
+
export type OnboardingFlags = { introseen: boolean; onboardingseen: boolean };
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reads the onboarding stage flags of one visitor (parameterized; undefined
|
|
68
|
+
* before the first write). The browser accessor serves the same flags over
|
|
69
|
+
* GET /api/v1/onboarding.
|
|
70
|
+
*
|
|
71
|
+
* @param visitor the visitor key of the row.
|
|
72
|
+
* @returns the flags, or undefined when the visitor never wrote.
|
|
73
|
+
*/
|
|
74
|
+
export function getOnboardingState(visitor: string): OnboardingFlags | undefined {
|
|
75
|
+
const row = queryOne<Pick<OnboardingStateRow, "introseen" | "onboardingseen">>(
|
|
76
|
+
"SELECT introseen, onboardingseen FROM onboarding_states WHERE visitor = ?",
|
|
77
|
+
[visitor],
|
|
78
|
+
);
|
|
79
|
+
if (!row) return undefined;
|
|
80
|
+
return { introseen: row.introseen === 1, onboardingseen: row.onboardingseen === 1 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Marks one onboarding stage seen for a visitor — idempotent seen-wins
|
|
85
|
+
* upsert (a stage seen anywhere stays seen), prepared and parameterized,
|
|
86
|
+
* never concatenated SQL.
|
|
87
|
+
*
|
|
88
|
+
* @param visitor the visitor key of the row.
|
|
89
|
+
* @param stage the stage the visitor just saw (or deliberately skipped).
|
|
90
|
+
* @returns the flags after the write.
|
|
91
|
+
*/
|
|
92
|
+
export function setOnboardingStage(visitor: string, stage: "intro" | "onboarding"): OnboardingFlags {
|
|
93
|
+
execute(
|
|
94
|
+
`INSERT INTO onboarding_states (visitor, introseen, onboardingseen, updatedAt)
|
|
95
|
+
VALUES (?, ?, ?, datetime('now'))
|
|
96
|
+
ON CONFLICT(visitor) DO UPDATE SET
|
|
97
|
+
introseen = MAX(introseen, excluded.introseen),
|
|
98
|
+
onboardingseen = MAX(onboardingseen, excluded.onboardingseen),
|
|
99
|
+
updatedAt = datetime('now')`,
|
|
100
|
+
[visitor, stage === "intro" ? 1 : 0, stage === "onboarding" ? 1 : 0],
|
|
101
|
+
);
|
|
102
|
+
return getOnboardingState(visitor) ?? { introseen: false, onboardingseen: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* --------------------------------------------------------------------- */
|
|
106
|
+
/* labels — the interface copy store */
|
|
107
|
+
/* --------------------------------------------------------------------- */
|
|
108
|
+
|
|
109
|
+
/** one labels row: the interface copy store. the id is the page.widget.key
|
|
110
|
+
* path, the text rides the english source of truth (labels.ts), and the
|
|
111
|
+
* translated flag protects a runtime translation from the seed overwrite.
|
|
112
|
+
* the shape mirrors the browser seed rows in labels.ts one to one. */
|
|
113
|
+
export type LabelRow = {
|
|
114
|
+
id: string;
|
|
115
|
+
text: string;
|
|
116
|
+
revision: number;
|
|
117
|
+
updatedAt: string;
|
|
118
|
+
translated?: boolean;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** the raw labels projection sqlite answers (translated rides the 0/1 cell). */
|
|
122
|
+
type LabelRowSql = {
|
|
123
|
+
id: string;
|
|
124
|
+
text: string;
|
|
125
|
+
revision: number;
|
|
126
|
+
updatedAt: string;
|
|
127
|
+
translated: number;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** Projects one raw labels row into the stored label projection. */
|
|
131
|
+
function labelRowOf(row: LabelRowSql): LabelRow {
|
|
132
|
+
return {
|
|
133
|
+
id: row.id,
|
|
134
|
+
text: row.text,
|
|
135
|
+
revision: row.revision,
|
|
136
|
+
updatedAt: row.updatedAt,
|
|
137
|
+
...(row.translated === 1 ? { translated: true } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Writes one label row (idempotent upsert; the caller owns the revision),
|
|
143
|
+
* prepared and parameterized, never concatenated SQL.
|
|
144
|
+
*
|
|
145
|
+
* @param id the page.widget.key path of the label.
|
|
146
|
+
* @param text the surface text.
|
|
147
|
+
* @param revision the manifest revision the text carries.
|
|
148
|
+
* @param translated true when the row carries a runtime translation.
|
|
149
|
+
* @returns the stored label projection.
|
|
150
|
+
*/
|
|
151
|
+
export function saveLabel(id: string, text: string, revision: number, translated = false): LabelRow {
|
|
152
|
+
execute(
|
|
153
|
+
`INSERT INTO labels (id, text, revision, updatedAt, translated) VALUES (?, ?, ?, datetime('now'), ?)
|
|
154
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
155
|
+
text = excluded.text,
|
|
156
|
+
revision = excluded.revision,
|
|
157
|
+
updatedAt = excluded.updatedAt,
|
|
158
|
+
translated = excluded.translated`,
|
|
159
|
+
[id, text, revision, translated ? 1 : 0],
|
|
160
|
+
);
|
|
161
|
+
const row = queryOne<LabelRowSql>("SELECT id, text, revision, updatedAt, translated FROM labels WHERE id = ?", [id]);
|
|
162
|
+
if (!row) throw new Error(`label "${id}" write did not land`);
|
|
163
|
+
return labelRowOf(row);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Reads one label row by its page.widget.key path.
|
|
168
|
+
*
|
|
169
|
+
* @param id the label id.
|
|
170
|
+
* @returns the row, or undefined when absent.
|
|
171
|
+
*/
|
|
172
|
+
export function readLabel(id: string): LabelRow | undefined {
|
|
173
|
+
const row = queryOne<LabelRowSql>("SELECT id, text, revision, updatedAt, translated FROM labels WHERE id = ?", [id]);
|
|
174
|
+
return row ? labelRowOf(row) : undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Reads every label row of the store ordered by id.
|
|
179
|
+
*
|
|
180
|
+
* @returns the label rows.
|
|
181
|
+
*/
|
|
182
|
+
export function readAllLabels(): LabelRow[] {
|
|
183
|
+
return queryAll<LabelRowSql>("SELECT id, text, revision, updatedAt, translated FROM labels ORDER BY id ASC").map(
|
|
184
|
+
labelRowOf,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Ingests the english manifest of labels.ts into the labels table — the
|
|
190
|
+
* db-side twin of the browser seedLabels: idempotent, the changed rows
|
|
191
|
+
* refresh, and the translated rows stay protected from the overwrite.
|
|
192
|
+
*
|
|
193
|
+
* @returns the seeded and protected row counts.
|
|
194
|
+
*/
|
|
195
|
+
export function seedLabelRows(): { seeded: number; protected: number } {
|
|
196
|
+
ensureSchema();
|
|
197
|
+
const rows = new Map(readAllLabels().map((row) => [row.id, row]));
|
|
198
|
+
let seeded = 0;
|
|
199
|
+
let saved = 0;
|
|
200
|
+
for (const [id, text] of Object.entries(LABEL_MANIFEST)) {
|
|
201
|
+
const row = rows.get(id);
|
|
202
|
+
if (row === undefined || (row.translated !== true && row.text !== text)) {
|
|
203
|
+
saveLabel(id, text, LABEL_REVISION, row?.translated ?? false);
|
|
204
|
+
seeded += 1;
|
|
205
|
+
}
|
|
206
|
+
saved += 1;
|
|
207
|
+
}
|
|
208
|
+
return { seeded, protected: saved - seeded };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* the copy law rides the db boot: the label seed ingests the english manifest
|
|
212
|
+
* of labels.ts beside the schema (the interface strings land in the store,
|
|
213
|
+
* the translated rows protected from the overwrite). best effort: a sealed
|
|
214
|
+
* store never blocks the boot, the manifest always answers the wingets. */
|
|
215
|
+
try {
|
|
216
|
+
seedLabelRows();
|
|
217
|
+
} catch {
|
|
218
|
+
/* the manifest answers; the store catches up on the next boot */
|
|
219
|
+
}
|
package/devthink.toml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# application identity on the platform
|
|
2
|
+
app = "cobaia"
|
|
3
|
+
role = "the monetization site of the family: the front door of the ten deployable apps — pricing, support, the app directory, the update blog and the legal pages, real content with honest ad slots and zero tracking"
|
|
4
|
+
theme = "Sol"
|
|
5
|
+
pages = ["home", "pricing", "support", "apps", "legal", "blog", "notfound"]
|
package/docs/.gitkeep
ADDED
|
File without changes
|
package/familyurl.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* familyurl.ts — the address book of the family deploy units that share the
|
|
3
|
+
* host with the cobaia. The family redirects both ways: the site directory
|
|
4
|
+
* (/apps) links every sibling application and the DevThink OS, and every
|
|
5
|
+
* sibling links back through its own copy of this helper. The deploy units
|
|
6
|
+
* sit beside this one as sibling folders on the shared origin, so the url
|
|
7
|
+
* resolves one level above this application (../slug/), with the slug
|
|
8
|
+
* trimmed of any slashes so both bare names and pasted segments resolve to
|
|
9
|
+
* the same address. Pure and side-effect free: the helper only shapes
|
|
10
|
+
* strings.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolves the url of a family deploy unit that shares this origin.
|
|
15
|
+
*
|
|
16
|
+
* @param slug the deploy unit segment (the app folder name).
|
|
17
|
+
* @returns the relative url of the family application, one level above this
|
|
18
|
+
* deploy unit, always carrying the trailing slash.
|
|
19
|
+
*/
|
|
20
|
+
export function familyurl(slug: string): string {
|
|
21
|
+
return `../${slug.replace(/^\/+|\/+$/g, "")}/`;
|
|
22
|
+
}
|
package/favicon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="cobaia favicon"><title>cobaia</title><rect width="32" height="32" rx="8" fill="#b45309"/></svg>
|
package/icon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" role="img" aria-label="cobaia icon"><title>cobaia</title><defs><linearGradient id="cbbg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f59e0b"/><stop offset="1" stop-color="#b45309"/></linearGradient></defs><rect width="96" height="96" rx="24" fill="url(#cbbg)"/><g fill="none" stroke="#fbf5ea" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"><path d="M38 26 H58 M42 26 V44 L30 66 A4 4 0 0 0 34 72 H62 A4 4 0 0 0 66 66 L54 44 V26"/><path d="M36 56 H60"/></g></svg>
|
package/index.html
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!-- web entry of the theme -->
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
7
|
+
<meta
|
|
8
|
+
name="description"
|
|
9
|
+
content="cobaia is the front door of the DevThink family: the pricing, the support page, the app directory and the update blog for ten deployable web applications."
|
|
10
|
+
/>
|
|
11
|
+
<meta name="theme-color" content="#0a0a0b" />
|
|
12
|
+
<link rel="icon" href="./favicon.svg" />
|
|
13
|
+
<link rel="manifest" href="./manifest.webmanifest" />
|
|
14
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
15
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
16
|
+
<link
|
|
17
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=Geist+Mono:wght@100..900&display=swap"
|
|
18
|
+
rel="stylesheet"
|
|
19
|
+
/>
|
|
20
|
+
<title>cobaia — the devthink family site</title>
|
|
21
|
+
<script type="module" src="./App.tsx"></script>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<div id="root"></div>
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
package/labels.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* labels.ts — the copy law of cobaia: every interface string lives in the
|
|
3
|
+
* label store, the wingets and page anchors read through this typed
|
|
4
|
+
* accessor, and a hardcoded string inside a widget is a defect. the
|
|
5
|
+
* manifest below is the english source of truth extracted from the real
|
|
6
|
+
* strings of the Sol wingets and the page anchors; the seed ingests it
|
|
7
|
+
* into the labels store (the site db through db.ts on the server, the
|
|
8
|
+
* browser rows cache in the visitor machine) and the seed never overwrites
|
|
9
|
+
* a translated row.
|
|
10
|
+
*
|
|
11
|
+
* the browser never imports db.ts (better-sqlite3 cannot ride the static
|
|
12
|
+
* bundle): the type comes across erased, the browser seed mirrors the rows
|
|
13
|
+
* into the local cache.
|
|
14
|
+
*
|
|
15
|
+
* data stays data: the app roster (apps.roster.ts — slugs, accents, the
|
|
16
|
+
* roles each app declares in its own devthink.toml), the plan billing
|
|
17
|
+
* values and the support hrefs (checkoutlinks.ts — ids, prices, checkout
|
|
18
|
+
* links; the plan prose itself rides here), the ad slot registry
|
|
19
|
+
* (adsconfig.ts — the fixed ad contract label and note ride there, pinned
|
|
20
|
+
* by the tests), the blog posts (posts.ts) and the route hrefs keep
|
|
21
|
+
* flowing from the data modules; the prose and the affordances around
|
|
22
|
+
* them ride here.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createLabelStore } from "@wenathlan/vault/labelstore";
|
|
26
|
+
|
|
27
|
+
/** the label revision of the manifest: bump on every copy wave so the seed
|
|
28
|
+
* refreshes the store rows of the changed strings. */
|
|
29
|
+
export const LABEL_REVISION = 4;
|
|
30
|
+
|
|
31
|
+
/** the english manifest: the page.widget.key path answers the surface text.
|
|
32
|
+
* every entry is the verbatim string the wingets and page anchors rendered
|
|
33
|
+
* before the copy wave — zero invented copy. */
|
|
34
|
+
export const LABEL_MANIFEST = {
|
|
35
|
+
// the window shell widget (the one chrome of the content site)
|
|
36
|
+
"shell.skip": "skip to content",
|
|
37
|
+
"shell.window.aria": "cobaia",
|
|
38
|
+
"shell.brand.aria": "cobaia — home",
|
|
39
|
+
"shell.name": "cobaia",
|
|
40
|
+
"shell.role": "the devthink family site",
|
|
41
|
+
"shell.caption.minimize": "Minimize cobaia",
|
|
42
|
+
"shell.caption.maximize": "Maximize cobaia",
|
|
43
|
+
"shell.caption.close": "Close cobaia",
|
|
44
|
+
"shell.nav.aria": "site sections",
|
|
45
|
+
"shell.nav.apps": "apps",
|
|
46
|
+
"shell.nav.pricing": "pricing",
|
|
47
|
+
"shell.nav.blog": "blog",
|
|
48
|
+
"shell.nav.support": "support",
|
|
49
|
+
"shell.foot.aboutHead": "cobaia",
|
|
50
|
+
"shell.foot.aboutText":
|
|
51
|
+
"the front door of the devthink family: ten applications that run anywhere, tracked nowhere. pricing, support, the app directory and the update blog live here.",
|
|
52
|
+
"shell.foot.siteHead": "site",
|
|
53
|
+
"shell.foot.apps": "apps",
|
|
54
|
+
"shell.foot.pricing": "pricing",
|
|
55
|
+
"shell.foot.blog": "blog",
|
|
56
|
+
"shell.foot.support": "support",
|
|
57
|
+
"shell.foot.legal": "legal",
|
|
58
|
+
"shell.foot.familyHead": "the family",
|
|
59
|
+
"shell.colophon": "cobaia — a provisional name. part of the devthink family · ten honest tools under one roof.",
|
|
60
|
+
// the home page anchor (the landing)
|
|
61
|
+
"home.aria.hero": "cobaia — the devthink family site",
|
|
62
|
+
"home.eyebrow": "the devthink family site",
|
|
63
|
+
"home.title": "One family. Ten tools. Zero tracking.",
|
|
64
|
+
"home.lede":
|
|
65
|
+
"devthink builds and ships a family of web applications — sandboxes, studios, tools and a game platform — hosted free, tracked nowhere. this site is the front door: the directory, the pricing, the support page and the update blog.",
|
|
66
|
+
"home.action.pricing": "see the pricing",
|
|
67
|
+
"home.action.apps": "browse the ten apps",
|
|
68
|
+
"home.section.stepsAria": "what the family is made of",
|
|
69
|
+
"home.section.stepsTitle": "What the family is made of",
|
|
70
|
+
"home.section.stepsLede": "the same three choices behind every member of the family.",
|
|
71
|
+
"home.steps.engine.n": "01",
|
|
72
|
+
"home.steps.engine.title": "one core",
|
|
73
|
+
"home.steps.engine.text":
|
|
74
|
+
"the sandbox and the storage are shared parts. every application assembles the parts it needs and ships beside the others.",
|
|
75
|
+
"home.steps.static.n": "02",
|
|
76
|
+
"home.steps.static.title": "static first",
|
|
77
|
+
"home.steps.static.text":
|
|
78
|
+
"every public surface builds down to one stylesheet and a handful of bundles on free static hosting. nothing is stored on the visitor machine — no cookies, no accounts, no tracking.",
|
|
79
|
+
"home.steps.honest.n": "03",
|
|
80
|
+
"home.steps.honest.title": "honest money",
|
|
81
|
+
"home.steps.honest.text":
|
|
82
|
+
"the family runs on about us$15 a month of domains. the pricing and the support page exist to cover that floor first — and the apps themselves stay free of ads.",
|
|
83
|
+
"home.section.previewAria": "a preview of the app directory",
|
|
84
|
+
"home.section.previewTitle": "A few of the ten",
|
|
85
|
+
"home.section.previewLede": "every member lives beside this site, one link away. the full directory lives on",
|
|
86
|
+
"home.section.previewLink": "/apps",
|
|
87
|
+
"home.section.previewTail": ".",
|
|
88
|
+
"home.appcard.open": "open {slug}",
|
|
89
|
+
"home.section.supportAria": "supporting the family",
|
|
90
|
+
"home.section.supportTitle": "Keep the family independent",
|
|
91
|
+
"home.section.supportLede":
|
|
92
|
+
"github sponsors and ko-fi keep the domains paid and the tools free — both charge zero platform fees, so the support goes straight to the hosting floor.",
|
|
93
|
+
"home.action.support": "ways to support",
|
|
94
|
+
// the apps page anchor (the directory)
|
|
95
|
+
"apps.aria.hero": "the devthink app directory",
|
|
96
|
+
"apps.eyebrow": "the directory",
|
|
97
|
+
"apps.title": "Ten apps. One family.",
|
|
98
|
+
"apps.lede":
|
|
99
|
+
"every member of the family lives beside this site and links back to it. the roles below are the honest ones each app declares for itself; the accents are the family's spec table.",
|
|
100
|
+
"apps.section.rosterAria": "the family roster",
|
|
101
|
+
"apps.card.open": "open {slug}",
|
|
102
|
+
// the pricing page anchor (the plans)
|
|
103
|
+
"pricing.action.choose": "choose {name}",
|
|
104
|
+
"pricing.action.notConfigured": "checkout not configured yet",
|
|
105
|
+
"pricing.aria.hero": "cobaia pricing",
|
|
106
|
+
"pricing.eyebrow": "pricing",
|
|
107
|
+
"pricing.title": "Pay for what you use. Or use it free.",
|
|
108
|
+
"pricing.lede":
|
|
109
|
+
"the core surface of every application is free and stays free. the paid plans add the pro features of one app or of the whole family. the prices below are placeholders until billing goes live — a plan activates the moment its checkout link is configured, and says so honestly until then.",
|
|
110
|
+
"pricing.section.plansAria": "the plans",
|
|
111
|
+
"pricing.card.planAria": "{name} plan",
|
|
112
|
+
"pricing.section.moneyAria": "what the money covers",
|
|
113
|
+
"pricing.section.moneyTitle": "What the money covers",
|
|
114
|
+
"pricing.section.moneyLede":
|
|
115
|
+
"the family runs on about us$15 a month of domains and free static hosting. the paid plans cover that floor first, then the higher tiers. there are no ads inside the applications — the ad slots live only on this site's content pages, and they are labeled and reserved.",
|
|
116
|
+
"pricing.section.moneyNote":
|
|
117
|
+
"billing decision pending: stripe payment links or paddle (merchant of record). the checkout hrefs are configuration at the app root — never code, never a committed key.",
|
|
118
|
+
// the plan prose of the pricing cards and the intro plans zone (the
|
|
119
|
+
// billing values — ids, prices, hrefs — stay in checkoutlinks.ts; the
|
|
120
|
+
// words ride here so the store answers every plan sentence)
|
|
121
|
+
"pricing.plan.free.name": "Free",
|
|
122
|
+
"pricing.plan.free.per": "month",
|
|
123
|
+
"pricing.plan.free.blurb": "every app's core surface, for as long as the family is up.",
|
|
124
|
+
"pricing.plan.free.feat.1": "the core surface of all ten applications",
|
|
125
|
+
"pricing.plan.free.feat.2": "no ads inside the apps, ever",
|
|
126
|
+
"pricing.plan.free.feat.3": "community support through the public issue tracker",
|
|
127
|
+
"pricing.plan.pro.name": "Pro",
|
|
128
|
+
"pricing.plan.pro.per": "month",
|
|
129
|
+
"pricing.plan.pro.blurb": "one app and its pro features, billed per application.",
|
|
130
|
+
"pricing.plan.pro.feat.1": "the pro features of one chosen application",
|
|
131
|
+
"pricing.plan.pro.feat.2": "priority routes on the shared gateway",
|
|
132
|
+
"pricing.plan.pro.feat.3": "early access to that app's new surfaces",
|
|
133
|
+
"pricing.plan.studio.name": "Studio",
|
|
134
|
+
"pricing.plan.studio.per": "month",
|
|
135
|
+
"pricing.plan.studio.blurb": "the whole family at once, for people who live in the tools.",
|
|
136
|
+
"pricing.plan.studio.feat.1": "the pro features of every application in the family",
|
|
137
|
+
"pricing.plan.studio.feat.2": "early access across the family",
|
|
138
|
+
"pricing.plan.studio.feat.3": "a direct line to the maintainer for feature requests",
|
|
139
|
+
// the support page anchor (the channels)
|
|
140
|
+
"support.action.notConfigured": "link not configured yet",
|
|
141
|
+
"support.aria.hero": "cobaia support",
|
|
142
|
+
"support.eyebrow": "support",
|
|
143
|
+
"support.title": "Keep the family independent.",
|
|
144
|
+
"support.lede":
|
|
145
|
+
"the family runs on about us$15 a month of domains and free hosting. support covers that floor, keeps every tool free to use and keeps the applications free of ads. both channels below charge zero platform fees, so the support goes where it says.",
|
|
146
|
+
"support.section.channelsAria": "the support channels",
|
|
147
|
+
"support.channel.sponsors.name": "GitHub Sponsors",
|
|
148
|
+
"support.channel.sponsors.promise":
|
|
149
|
+
"sponsor the maintainer through github. zero platform fee, payouts land automatically, and the sponsorship shows up next to the public work it funds.",
|
|
150
|
+
"support.channel.sponsors.cta": "sponsor on github",
|
|
151
|
+
"support.channel.kofi.name": "Ko-fi",
|
|
152
|
+
"support.channel.kofi.promise":
|
|
153
|
+
"one-off donations and monthly memberships through ko-fi. zero fee on donations, no account required on your side beyond the payment.",
|
|
154
|
+
"support.channel.kofi.cta": "buy a coffee",
|
|
155
|
+
"support.section.otherAria": "other ways to help",
|
|
156
|
+
"support.section.otherTitle": "Other ways to help",
|
|
157
|
+
"support.section.otherLede":
|
|
158
|
+
"use the tools and tell the truth about them — report what breaks in the public issue tracker, write about what the family does well or badly, and link the directory when a tool earns it. none of it costs anything and all of it compounds.",
|
|
159
|
+
"support.section.otherNote":
|
|
160
|
+
"the honest line: no ad clicking, no traffic exchange, no bots. automated traffic against an ad surface is invalid traffic — it would end the ad account and the domain reputation. the family does not do it.",
|
|
161
|
+
// the blog page anchor (the update blog)
|
|
162
|
+
"blog.aria.hero": "the family update blog",
|
|
163
|
+
"blog.eyebrow": "updates",
|
|
164
|
+
"blog.title": "What the family is building.",
|
|
165
|
+
"blog.lede":
|
|
166
|
+
"short, written updates on the family — how the sites are built, what they cost and why they ship the way they do. every post lives on this page; the index jumps to it.",
|
|
167
|
+
"blog.section.indexAria": "the post index",
|
|
168
|
+
"blog.section.fullAria": "the posts in full",
|
|
169
|
+
"blog.post.meta": "{date} · {words} words · {tags}",
|
|
170
|
+
// the legal page anchor (privacy and terms)
|
|
171
|
+
"legal.aria.hero": "cobaia legal",
|
|
172
|
+
"legal.eyebrow": "legal",
|
|
173
|
+
"legal.title": "Privacy and terms.",
|
|
174
|
+
"legal.lede":
|
|
175
|
+
"the short version: the family sites are static files, they collect nothing by default, they track nobody, and the details below are the long version of the same promise. last updated 2026-10-10.",
|
|
176
|
+
"legal.privacy.aria": "privacy policy",
|
|
177
|
+
"legal.privacy.title": "Privacy policy",
|
|
178
|
+
"legal.privacy.collect.head": "What we collect: nothing, by default.",
|
|
179
|
+
"legal.privacy.collect.text":
|
|
180
|
+
"The family sites are static files served from a CDN. They set no cookies, run no analytics, embed no fingerprinting and write nothing to your browser — no local storage, no indexed databases, no session records. The only thing a visitor machine does is load the interface.",
|
|
181
|
+
"legal.privacy.logs.head": "Server logs.",
|
|
182
|
+
"legal.privacy.logs.text":
|
|
183
|
+
"The hosting providers (netlify, vercel, cloudflare and equivalents) keep standard access logs on their own infrastructure for security and abuse prevention. The family does not add identifiers to them and does not export them for profiling.",
|
|
184
|
+
"legal.privacy.ads.head": "Advertising.",
|
|
185
|
+
"legal.privacy.ads.text":
|
|
186
|
+
"The family site runs reserved, visibly labeled ad containers on its content pages. No ad script ships in the code: the advertising partner's loader is only added after the partner approves the site, and it arrives through the deploy environment. Once live, third-party vendors including Google may serve ads and use cookies to personalize them, as governed by the partner's own policies. You can opt out of personalized advertising through your ad settings (for Google, adssettings.google.com). Ads never appear inside the applications themselves.",
|
|
187
|
+
"legal.privacy.analytics.head": "Analytics.",
|
|
188
|
+
"legal.privacy.analytics.text":
|
|
189
|
+
"There is no analytics on the family sites today. If that ever changes, it will be opt-in, minimal and disclosed on this page before it goes live.",
|
|
190
|
+
"legal.privacy.payments.head": "Payments.",
|
|
191
|
+
"legal.privacy.payments.text":
|
|
192
|
+
"When paid plans go live, checkout runs on the payment provider's own pages (stripe, paddle or equivalents). The family sites never see or store card data.",
|
|
193
|
+
"legal.privacy.contact.head": "Contact.",
|
|
194
|
+
"legal.privacy.contact.text":
|
|
195
|
+
"Privacy questions and data requests go through the public issue tracker of the family repository — the same channel the software itself uses.",
|
|
196
|
+
"legal.terms.aria": "terms of use",
|
|
197
|
+
"legal.terms.title": "Terms of use",
|
|
198
|
+
"legal.terms.service.head": "The service.",
|
|
199
|
+
"legal.terms.service.text":
|
|
200
|
+
"The family applications and this site are provided as is, without warranty of any kind. They are tools built in public: features change, break and get fixed in the open. Nothing here is offered as professional advice or with any availability guarantee.",
|
|
201
|
+
"legal.terms.use.head": "Acceptable use.",
|
|
202
|
+
"legal.terms.use.text":
|
|
203
|
+
"Use the tools like a person. Do not run automated traffic against the sites or, above all, against any advertising surface: clicking your own ads, exchanging traffic or driving bots at ad containers is invalid traffic — it defrauds the advertisers, it ends the ad account and it burns the domain reputation. The family does not do it and asks nobody else to.",
|
|
204
|
+
"legal.terms.data.head": "Your data.",
|
|
205
|
+
"legal.terms.data.text":
|
|
206
|
+
"Since the sites collect nothing, there is nothing to export or delete. What you do inside the hosted tools stays governed by the tool's own page.",
|
|
207
|
+
"legal.terms.ip.head": "Intellectual property.",
|
|
208
|
+
"legal.terms.ip.text":
|
|
209
|
+
'The family code is free software under its repository license; the name devthink and the family marks belong to their author. The site name "cobaia" is provisional and may change.',
|
|
210
|
+
"legal.terms.changes.head": "Changes.",
|
|
211
|
+
"legal.terms.changes.text":
|
|
212
|
+
"These terms change when the facts change, and the changes land on this page with a new date. Using the sites after a change means you accept it.",
|
|
213
|
+
// the notfound page anchor (the empty address)
|
|
214
|
+
"notfound.eyebrow": "404",
|
|
215
|
+
"notfound.title": "This address is not part of the family.",
|
|
216
|
+
"notfound.lede":
|
|
217
|
+
"the page you asked for does not exist here. the front door and the app directory are the honest way back in.",
|
|
218
|
+
"notfound.action.home": "back to the front door",
|
|
219
|
+
"notfound.action.apps": "browse the apps",
|
|
220
|
+
// the intro page anchor (the bare presentation — the family grammar's last
|
|
221
|
+
// seat: every app carries an intro; the cobaia one presents the real offer,
|
|
222
|
+
// never invented numbers, never fake testimonials)
|
|
223
|
+
"intro.aria.page": "cobaia — the intro",
|
|
224
|
+
"intro.aria.splash": "the cobaia intro splash",
|
|
225
|
+
"intro.splash.name": "cobaia",
|
|
226
|
+
"intro.splash.role": "the devthink family site",
|
|
227
|
+
"intro.splash.skip": "click, tap or any key to skip",
|
|
228
|
+
"intro.bar.aria": "the intro doors",
|
|
229
|
+
"intro.hero.eyebrow": "the presentation",
|
|
230
|
+
"intro.hero.title": "The family's front door, and its price list.",
|
|
231
|
+
"intro.hero.lede":
|
|
232
|
+
"cobaia is the monetization face of the devthink family, kept apart so the ten applications stay clean — no ads inside the apps. the directory, the plans, the support channels and the update blog live here, on one static site that collects nothing by default.",
|
|
233
|
+
"intro.hero.cta": "enter the site",
|
|
234
|
+
"intro.hero.cue": "see the plans",
|
|
235
|
+
"intro.hero.note":
|
|
236
|
+
"the name is provisional, the hosting is free and static, the storage is zero — the long version of that promise rides the legal page.",
|
|
237
|
+
"intro.plans.aria": "the plans as they really are",
|
|
238
|
+
"intro.plans.eyebrow": "the plans",
|
|
239
|
+
"intro.plans.title": "The plans, as they really are.",
|
|
240
|
+
"intro.plans.lede":
|
|
241
|
+
"the same three plans the pricing page carries — us$0, 5 and 19 a month, placeholder prices until billing lands. a plan activates the moment its checkout link is configured, and says so honestly until then.",
|
|
242
|
+
"intro.plans.choose": "choose {name}",
|
|
243
|
+
"intro.plans.notConfigured": "checkout not configured yet",
|
|
244
|
+
"intro.plans.door": "the full pricing page",
|
|
245
|
+
"intro.doors.aria": "the doors of the site",
|
|
246
|
+
"intro.doors.eyebrow": "the doors",
|
|
247
|
+
"intro.doors.title": "Where the family lives.",
|
|
248
|
+
"intro.doors.apps": "the app directory",
|
|
249
|
+
"intro.doors.appsText":
|
|
250
|
+
"ten apps live beside this site. the full directory, with the honest role each application declares for itself, is one door away.",
|
|
251
|
+
"intro.doors.appsCta": "browse the ten apps",
|
|
252
|
+
"intro.doors.legal": "privacy and terms",
|
|
253
|
+
"intro.doors.legalText":
|
|
254
|
+
"the family sites are static files: they collect nothing by default and they track nobody. the privacy policy and the terms of use are the long version of the same promise.",
|
|
255
|
+
"intro.doors.legalCta": "read the legal page",
|
|
256
|
+
"intro.footer.siteAria": "the site doors",
|
|
257
|
+
"intro.footer.familyAria": "the family row",
|
|
258
|
+
"intro.footer.family": "the family",
|
|
259
|
+
"intro.footer.memberTip": "open {slug} — the door beside this site",
|
|
260
|
+
// the ad slot widget (the reserved box of the content pages)
|
|
261
|
+
"ads.slot.reserved": "reserved · {format}",
|
|
262
|
+
} as const;
|
|
263
|
+
|
|
264
|
+
export type LabelKey = keyof typeof LABEL_MANIFEST & string;
|
|
265
|
+
|
|
266
|
+
/** the store mechanism rides the vault factory (the storage law of the
|
|
267
|
+
* family: the vault owns the store, the app owns the manifest — the
|
|
268
|
+
* mechanism is imported, never duplicated). the surface answers its own no-storage law: the visitor browser never writes. */
|
|
269
|
+
const store = createLabelStore({
|
|
270
|
+
manifest: LABEL_MANIFEST,
|
|
271
|
+
revision: LABEL_REVISION,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
/** the typed accessor of the copy: the only door a widget or page anchor uses. */
|
|
275
|
+
export function label(key: LabelKey): string {
|
|
276
|
+
return store.label(key);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** the placeholder door: fills the {token} slots of one format key with the
|
|
280
|
+
* runtime values (the store keeps the sentence, the widget passes the data). */
|
|
281
|
+
export function labelfill(key: LabelKey, values: Record<string, string>): string {
|
|
282
|
+
return store.labelfill(key, values);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** the seed ingests the manifest into the labels store, idempotent, and the
|
|
286
|
+
* translated rows stay protected from the overwrite. in the browser the rows
|
|
287
|
+
* ride the local cache of the site db; on the server db.ts seeds the same
|
|
288
|
+
* manifest into the labels table through seedLabelRows. */
|
|
289
|
+
export function seedLabels(): Promise<{ seeded: number; protected: number }> {
|
|
290
|
+
return store.seedLabels();
|
|
291
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cobaia",
|
|
3
|
+
"short_name": "cobaia",
|
|
4
|
+
"description": "The front door of the DevThink family: pricing, support, the app directory and the update blog for ten deployable web applications.",
|
|
5
|
+
"id": "/",
|
|
6
|
+
"start_url": "/",
|
|
7
|
+
"scope": "/",
|
|
8
|
+
"display": "standalone",
|
|
9
|
+
"display_override": ["window-controls-overlay", "standalone", "minimal-ui"],
|
|
10
|
+
"orientation": "any",
|
|
11
|
+
"background_color": "#0a0a0b",
|
|
12
|
+
"theme_color": "#f59e0b",
|
|
13
|
+
"lang": "en",
|
|
14
|
+
"dir": "ltr",
|
|
15
|
+
"categories": ["developer", "productivity"],
|
|
16
|
+
"icons": [
|
|
17
|
+
{
|
|
18
|
+
"src": "./favicon.svg",
|
|
19
|
+
"sizes": "any",
|
|
20
|
+
"type": "image/svg+xml",
|
|
21
|
+
"purpose": "any"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"src": "./favicon.svg",
|
|
25
|
+
"sizes": "any",
|
|
26
|
+
"type": "image/svg+xml",
|
|
27
|
+
"purpose": "any maskable"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"src": "./icon.svg",
|
|
31
|
+
"sizes": "any",
|
|
32
|
+
"type": "image/svg+xml",
|
|
33
|
+
"purpose": "any"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"src": "./icon.svg",
|
|
37
|
+
"sizes": "any",
|
|
38
|
+
"type": "image/svg+xml",
|
|
39
|
+
"purpose": "any maskable"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"shortcuts": [
|
|
43
|
+
{
|
|
44
|
+
"name": "Pricing",
|
|
45
|
+
"short_name": "Pricing",
|
|
46
|
+
"url": "/pricing"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "App directory",
|
|
50
|
+
"short_name": "Apps",
|
|
51
|
+
"url": "/apps"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"related_applications": [],
|
|
55
|
+
"prefer_related_applications": false
|
|
56
|
+
}
|
package/mime.types
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# MIME types served by the site
|
|
2
|
+
text/html html htm
|
|
3
|
+
text/css css
|
|
4
|
+
text/javascript js mjs
|
|
5
|
+
application/json json
|
|
6
|
+
application/manifest+json webmanifest
|
|
7
|
+
image/svg+xml svg
|
|
8
|
+
image/png png
|
|
9
|
+
image/jpeg jpg jpeg
|
|
10
|
+
image/x-icon ico
|
|
11
|
+
font/woff2 woff2
|
|
12
|
+
application/wasm wasm
|
package/netlify.toml
ADDED