@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,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* intro page anchor — layer 3 of the anchor architecture. The file carrying
|
|
3
|
+
* the folder's own name is the path manager of the page: it imports the
|
|
4
|
+
* loose components beside it, mounts the page and re-exports the public
|
|
5
|
+
* component surface. Only the theme anchor (sol/Sol.tsx) consumes this file.
|
|
6
|
+
*
|
|
7
|
+
* The PRESENTATION page of cobaia — the family grammar's last seat (every
|
|
8
|
+
* app of the family carries one): the page mounts BARE at /intro, no window
|
|
9
|
+
* chrome, riding the site voice, not the frame. The akash splash opens over
|
|
10
|
+
* the landing (the mark spring on the overshoot curve, the name rise, the
|
|
11
|
+
* role fade; click/tap/key skip; reduced motion never plays; the no storage
|
|
12
|
+
* law holds — no seen flag, the splash plays per visit and writes nothing),
|
|
13
|
+
* then the landing answers: the hero says what the surface is (the
|
|
14
|
+
* monetization face kept apart so the main sites stay clean), the plans
|
|
15
|
+
* zone renders the real offer from the one checkoutlinks table (the not
|
|
16
|
+
* configured state stays honest), the doors panes open the app directory
|
|
17
|
+
* and the legal page, and the footer carries the doors (the site row, the
|
|
18
|
+
* family row, the legal line). The entrance rides the cb-in stagger under
|
|
19
|
+
* the second, the layers ride the surface-ladder bands, the door panes ride
|
|
20
|
+
* the silk law. Public-site rules: zero network calls from the page itself,
|
|
21
|
+
* React state only; zero decorative dots; zero invented numbers and zero
|
|
22
|
+
* fake testimonials — the real offer, read from the data modules.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { CSSProperties } from "react";
|
|
26
|
+
import { useCallback, useEffect, useState } from "react";
|
|
27
|
+
import { Link } from "wouter";
|
|
28
|
+
import { label } from "../../labels";
|
|
29
|
+
import { BrandMark, IntroFooter, IntroPlans, IntroSplash } from "../wingets";
|
|
30
|
+
|
|
31
|
+
/** the lifecycle of the opening act: resolving (the motion preference
|
|
32
|
+
* decides) → playing (the akash floor over the landing) → done (the
|
|
33
|
+
* landing alone). No seen state is consulted — the no storage law. */
|
|
34
|
+
type SplashPhase = "resolving" | "playing" | "done";
|
|
35
|
+
|
|
36
|
+
/** the door glyph of the panes: the inline 10×10 chevron the chrome's
|
|
37
|
+
* caption grammar draws (the app carries no icon dependency). */
|
|
38
|
+
function DoorGlyph() {
|
|
39
|
+
return (
|
|
40
|
+
<svg
|
|
41
|
+
className="cb-intropane__glyph"
|
|
42
|
+
width="10"
|
|
43
|
+
height="10"
|
|
44
|
+
viewBox="0 0 10 10"
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
focusable="false"
|
|
47
|
+
>
|
|
48
|
+
<path d="M3 0l5 5-5 5" fill="none" stroke="currentColor" strokeWidth="1.5" />
|
|
49
|
+
</svg>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The intro page: the splash plays over the landing, then the bare
|
|
55
|
+
* presentation answers — hero, plans, doors, footer.
|
|
56
|
+
*
|
|
57
|
+
* @returns the intro element (null while the motion preference decides).
|
|
58
|
+
*/
|
|
59
|
+
export function Intro() {
|
|
60
|
+
const [splash, setSplash] = useState<SplashPhase>("resolving");
|
|
61
|
+
|
|
62
|
+
// the splash decision rides the motion preference alone — nothing is read
|
|
63
|
+
// from the visitor machine and nothing is written back (the no storage law)
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
66
|
+
setSplash("done");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
setSplash("playing");
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
/** the slide completed: the splash unmounts, the landing owns the page. */
|
|
73
|
+
const onSplashDone = useCallback(() => {
|
|
74
|
+
setSplash("done");
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
/** the quiet cue: walks the page to the plans band. */
|
|
78
|
+
const cuePlans = useCallback(() => {
|
|
79
|
+
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
80
|
+
document.getElementById("intro-plans")?.scrollIntoView({ behavior: reduced ? "auto" : "smooth", block: "start" });
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
// the phase decision holds the paint: nothing renders while it resolves —
|
|
84
|
+
// no flash of the landing under a splash that will never play
|
|
85
|
+
if (splash === "resolving") return null;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="cb-intro" data-page="intro">
|
|
89
|
+
{/* the skip link rides outside no frame — the bare page answers the
|
|
90
|
+
* viewport directly; the splash sits above it while it plays */}
|
|
91
|
+
<a className="cb-skip" href="#intro-content">
|
|
92
|
+
{label("shell.skip")}
|
|
93
|
+
</a>
|
|
94
|
+
|
|
95
|
+
{/* the akash floor: the opening act rides over the landing until its
|
|
96
|
+
* slide completes; reduced motion never mounts it */}
|
|
97
|
+
{splash === "playing" && <IntroSplash onDone={onSplashDone} />}
|
|
98
|
+
|
|
99
|
+
{/* the intro bar: the wordmark and the two quiet doors — the ONE
|
|
100
|
+
* primary cta lives in the hero */}
|
|
101
|
+
<header className="cb-introbar">
|
|
102
|
+
<div className="cb-introbar__in">
|
|
103
|
+
<span className="cb-introbar__brand">
|
|
104
|
+
<BrandMark size={20} />
|
|
105
|
+
<span className="cb-introbar__word">{label("shell.name")}</span>
|
|
106
|
+
</span>
|
|
107
|
+
<span className="cb-introbar__spacer" />
|
|
108
|
+
<nav className="cb-introbar__nav" aria-label={label("intro.bar.aria")}>
|
|
109
|
+
<Link className="cb-introdoor" href="/apps">
|
|
110
|
+
{label("shell.foot.apps")}
|
|
111
|
+
</Link>
|
|
112
|
+
<Link className="cb-introdoor" href="/pricing">
|
|
113
|
+
{label("shell.foot.pricing")}
|
|
114
|
+
</Link>
|
|
115
|
+
</nav>
|
|
116
|
+
</div>
|
|
117
|
+
</header>
|
|
118
|
+
|
|
119
|
+
<main id="intro-content">
|
|
120
|
+
{/* the hero: what the surface is, said directly */}
|
|
121
|
+
<section className="cb-introhero" aria-label={label("intro.aria.page")}>
|
|
122
|
+
<div className="cb-introwrap">
|
|
123
|
+
<span className="cb-introhero__mark cb-in" style={{ "--cb-d": 0 } as CSSProperties} aria-hidden="true">
|
|
124
|
+
<BrandMark size={64} />
|
|
125
|
+
</span>
|
|
126
|
+
<p className="cb-eyebrow cb-in" style={{ "--cb-d": 1 } as CSSProperties}>
|
|
127
|
+
{label("intro.hero.eyebrow")}
|
|
128
|
+
</p>
|
|
129
|
+
<h1 className="cb-title cb-in" style={{ "--cb-d": 2 } as CSSProperties}>
|
|
130
|
+
{label("intro.hero.title")}
|
|
131
|
+
</h1>
|
|
132
|
+
<p className="cb-lede cb-in" style={{ "--cb-d": 3 } as CSSProperties}>
|
|
133
|
+
{label("intro.hero.lede")}
|
|
134
|
+
</p>
|
|
135
|
+
<div className="cb-btnrow cb-in" style={{ "--cb-d": 4 } as CSSProperties}>
|
|
136
|
+
<Link className="cb-btn" href="/">
|
|
137
|
+
{label("intro.hero.cta")}
|
|
138
|
+
</Link>
|
|
139
|
+
<button type="button" className="cb-btn cb-btn--ghost" onClick={cuePlans}>
|
|
140
|
+
{label("intro.hero.cue")}
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
<p className="cb-note cb-in" style={{ "--cb-d": 5 } as CSSProperties}>
|
|
144
|
+
{label("intro.hero.note")}
|
|
145
|
+
</p>
|
|
146
|
+
</div>
|
|
147
|
+
</section>
|
|
148
|
+
|
|
149
|
+
{/* the plans as they really are (the one checkoutlinks table) */}
|
|
150
|
+
<IntroPlans />
|
|
151
|
+
|
|
152
|
+
{/* the doors: the app directory and the legal page, on silk panes */}
|
|
153
|
+
<section className="cb-introband cb-introband--deep" aria-label={label("intro.doors.aria")}>
|
|
154
|
+
<div className="cb-introwrap">
|
|
155
|
+
<div className="cb-secthead cb-in" style={{ "--cb-d": 0 } as CSSProperties}>
|
|
156
|
+
<p className="cb-eyebrow">{label("intro.doors.eyebrow")}</p>
|
|
157
|
+
<h2 className="cb-secttitle">{label("intro.doors.title")}</h2>
|
|
158
|
+
</div>
|
|
159
|
+
<div className="cb-introdoorgrid">
|
|
160
|
+
<article className="cb-intropane cb-in" style={{ "--cb-d": 1 } as CSSProperties}>
|
|
161
|
+
<p className="cb-cardmeta">{label("intro.doors.apps")}</p>
|
|
162
|
+
<p className="cb-intropane__text">{label("intro.doors.appsText")}</p>
|
|
163
|
+
<Link className="cb-intropane__door" href="/apps">
|
|
164
|
+
{label("intro.doors.appsCta")}
|
|
165
|
+
<DoorGlyph />
|
|
166
|
+
</Link>
|
|
167
|
+
</article>
|
|
168
|
+
<article className="cb-intropane cb-in" style={{ "--cb-d": 2 } as CSSProperties}>
|
|
169
|
+
<p className="cb-cardmeta">{label("intro.doors.legal")}</p>
|
|
170
|
+
<p className="cb-intropane__text">{label("intro.doors.legalText")}</p>
|
|
171
|
+
<Link className="cb-intropane__door" href="/legal">
|
|
172
|
+
{label("intro.doors.legalCta")}
|
|
173
|
+
<DoorGlyph />
|
|
174
|
+
</Link>
|
|
175
|
+
</article>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</section>
|
|
179
|
+
</main>
|
|
180
|
+
|
|
181
|
+
{/* the footer carries the doors: the site row, the family row, the line */}
|
|
182
|
+
<IntroFooter />
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default Intro;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* legal 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. The legal page of the cobaia content site: the privacy policy
|
|
7
|
+
* and the terms, written seriously and directly. The page discloses the
|
|
8
|
+
* advertising plan (labeled, reserved slots, third-party cookies only once
|
|
9
|
+
* the network is approved and live) and the analytics rule (none by default,
|
|
10
|
+
* opt-in and disclosed here first if that ever changes). No ad slot lives on
|
|
11
|
+
* this page: it is the compliance and health-check surface of the site.
|
|
12
|
+
* every paragraph rides the label accessor — the store keeps the sentence,
|
|
13
|
+
* the anchor keeps the rendering.
|
|
14
|
+
*/
|
|
15
|
+
import type { CSSProperties } from "react";
|
|
16
|
+
import { label } from "../../labels";
|
|
17
|
+
|
|
18
|
+
/** one legal paragraph: the strong lead and its body, both from the store. */
|
|
19
|
+
function Clause({ head, text }: { head: string; text: string }) {
|
|
20
|
+
return (
|
|
21
|
+
<p>
|
|
22
|
+
<strong>{head}</strong> {text}
|
|
23
|
+
</p>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Legal() {
|
|
28
|
+
return (
|
|
29
|
+
<div className="cb-page">
|
|
30
|
+
<section className="cb-hero" aria-label={label("legal.aria.hero")}>
|
|
31
|
+
<p className="cb-eyebrow cb-in" style={{ "--cb-d": 0 } as CSSProperties}>
|
|
32
|
+
{label("legal.eyebrow")}
|
|
33
|
+
</p>
|
|
34
|
+
<h1 className="cb-title cb-in" style={{ "--cb-d": 1 } as CSSProperties}>
|
|
35
|
+
{label("legal.title")}
|
|
36
|
+
</h1>
|
|
37
|
+
<p className="cb-lede cb-in" style={{ "--cb-d": 2 } as CSSProperties}>
|
|
38
|
+
{label("legal.lede")}
|
|
39
|
+
</p>
|
|
40
|
+
</section>
|
|
41
|
+
|
|
42
|
+
<article
|
|
43
|
+
className="cb-article cb-in"
|
|
44
|
+
style={{ "--cb-d": 1 } as CSSProperties}
|
|
45
|
+
aria-label={label("legal.privacy.aria")}
|
|
46
|
+
>
|
|
47
|
+
<h2 className="cb-secttitle">{label("legal.privacy.title")}</h2>
|
|
48
|
+
<div className="cb-articlebody">
|
|
49
|
+
<Clause head={label("legal.privacy.collect.head")} text={label("legal.privacy.collect.text")} />
|
|
50
|
+
<Clause head={label("legal.privacy.logs.head")} text={label("legal.privacy.logs.text")} />
|
|
51
|
+
<Clause head={label("legal.privacy.ads.head")} text={label("legal.privacy.ads.text")} />
|
|
52
|
+
<Clause head={label("legal.privacy.analytics.head")} text={label("legal.privacy.analytics.text")} />
|
|
53
|
+
<Clause head={label("legal.privacy.payments.head")} text={label("legal.privacy.payments.text")} />
|
|
54
|
+
<Clause head={label("legal.privacy.contact.head")} text={label("legal.privacy.contact.text")} />
|
|
55
|
+
</div>
|
|
56
|
+
</article>
|
|
57
|
+
|
|
58
|
+
<article
|
|
59
|
+
className="cb-article cb-in"
|
|
60
|
+
style={{ "--cb-d": 2 } as CSSProperties}
|
|
61
|
+
aria-label={label("legal.terms.aria")}
|
|
62
|
+
>
|
|
63
|
+
<h2 className="cb-secttitle">{label("legal.terms.title")}</h2>
|
|
64
|
+
<div className="cb-articlebody">
|
|
65
|
+
<Clause head={label("legal.terms.service.head")} text={label("legal.terms.service.text")} />
|
|
66
|
+
<Clause head={label("legal.terms.use.head")} text={label("legal.terms.use.text")} />
|
|
67
|
+
<Clause head={label("legal.terms.data.head")} text={label("legal.terms.data.text")} />
|
|
68
|
+
<Clause head={label("legal.terms.ip.head")} text={label("legal.terms.ip.text")} />
|
|
69
|
+
<Clause head={label("legal.terms.changes.head")} text={label("legal.terms.changes.text")} />
|
|
70
|
+
</div>
|
|
71
|
+
</article>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default Legal;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notfound 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. The cobaia copy answers for the family site: the address is not
|
|
7
|
+
* part of the family — the way out is the home and the directory.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Link } from "wouter";
|
|
11
|
+
|
|
12
|
+
import { label } from "../../labels";
|
|
13
|
+
|
|
14
|
+
export function NotFound() {
|
|
15
|
+
return (
|
|
16
|
+
<div className="cb-page cb-missing">
|
|
17
|
+
<p className="cb-eyebrow">{label("notfound.eyebrow")}</p>
|
|
18
|
+
<h1 className="cb-title">{label("notfound.title")}</h1>
|
|
19
|
+
<p className="cb-lede">{label("notfound.lede")}</p>
|
|
20
|
+
<div className="cb-btnrow">
|
|
21
|
+
<Link className="cb-btn" href="/">
|
|
22
|
+
{label("notfound.action.home")}
|
|
23
|
+
</Link>
|
|
24
|
+
<Link className="cb-btn cb-btn--ghost" href="/apps">
|
|
25
|
+
{label("notfound.action.apps")}
|
|
26
|
+
</Link>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default NotFound;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pricing 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 pricing table of the cobaia content site: three honest plans (free,
|
|
9
|
+
* pro, studio — us$0 / 5 / 19 a month, placeholders until billing lands).
|
|
10
|
+
* The billing values answer from the pure config at the app root
|
|
11
|
+
* (checkoutlinks.ts — ids, prices, checkout hrefs) and every plan sentence
|
|
12
|
+
* (name, blurb, features) rides the label store: a configured link renders
|
|
13
|
+
* a real anchor, an empty one renders the disabled button with the honest
|
|
14
|
+
* note "checkout not configured yet". No checkout page carries an ad — the
|
|
15
|
+
* ad slots live only on the content pages. Zero network calls: the links
|
|
16
|
+
* are plain hrefs the visitor chooses to follow.
|
|
17
|
+
*/
|
|
18
|
+
import type { CSSProperties } from "react";
|
|
19
|
+
import { checkoutlinks, type checkoutplan, isconfigured } from "../../checkoutlinks";
|
|
20
|
+
import { useContent } from "../../content.ts";
|
|
21
|
+
import { label, labelfill } from "../../labels";
|
|
22
|
+
|
|
23
|
+
/** renders one plan's checkout control: a live anchor when the owner
|
|
24
|
+
* configured the link, the honest disabled state until then. */
|
|
25
|
+
function PlanAction({ plan }: { plan: checkoutplan }) {
|
|
26
|
+
if (isconfigured(plan.href)) {
|
|
27
|
+
return (
|
|
28
|
+
<a className="cb-btn" href={plan.href} rel="noreferrer">
|
|
29
|
+
{labelfill("pricing.action.choose", { name: label(plan.nameKey) })}
|
|
30
|
+
</a>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<button className="cb-btn" type="button" disabled>
|
|
36
|
+
{labelfill("pricing.action.choose", { name: label(plan.nameKey) })}
|
|
37
|
+
</button>
|
|
38
|
+
<p className="cb-note">{label("pricing.action.notConfigured")}</p>
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function Pricing() {
|
|
44
|
+
useContent();
|
|
45
|
+
return (
|
|
46
|
+
<div className="cb-page">
|
|
47
|
+
<section className="cb-hero" aria-label={label("pricing.aria.hero")}>
|
|
48
|
+
<p className="cb-eyebrow cb-in" style={{ "--cb-d": 0 } as CSSProperties}>
|
|
49
|
+
{label("pricing.eyebrow")}
|
|
50
|
+
</p>
|
|
51
|
+
<h1 className="cb-title cb-in" style={{ "--cb-d": 1 } as CSSProperties}>
|
|
52
|
+
{label("pricing.title")}
|
|
53
|
+
</h1>
|
|
54
|
+
<p className="cb-lede cb-in" style={{ "--cb-d": 2 } as CSSProperties}>
|
|
55
|
+
{label("pricing.lede")}
|
|
56
|
+
</p>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
59
|
+
<section className="cb-section" aria-label={label("pricing.section.plansAria")}>
|
|
60
|
+
<div className="cb-plans">
|
|
61
|
+
{checkoutlinks().map((plan, index) => (
|
|
62
|
+
<article
|
|
63
|
+
key={plan.id}
|
|
64
|
+
className={plan.id === "studio" ? "cb-plan cb-plan--lead cb-in" : "cb-plan cb-in"}
|
|
65
|
+
style={{ "--cb-d": index + 1 } as CSSProperties}
|
|
66
|
+
aria-label={labelfill("pricing.card.planAria", { name: label(plan.nameKey) })}
|
|
67
|
+
>
|
|
68
|
+
<p className="cb-planname">{label(plan.nameKey)}</p>
|
|
69
|
+
<p className="cb-planprice">
|
|
70
|
+
${plan.priceusd}
|
|
71
|
+
<span className="cb-planper"> / {label(plan.perKey)}</span>
|
|
72
|
+
</p>
|
|
73
|
+
<p className="cb-planblurb">{label(plan.blurbKey)}</p>
|
|
74
|
+
<ul className="cb-planfeats">
|
|
75
|
+
{plan.featureKeys.map((featureKey) => (
|
|
76
|
+
<li key={featureKey} className="cb-planfeat">
|
|
77
|
+
{label(featureKey)}
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
</ul>
|
|
81
|
+
<PlanAction plan={plan} />
|
|
82
|
+
</article>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
|
|
87
|
+
<section className="cb-section" aria-label={label("pricing.section.moneyAria")}>
|
|
88
|
+
<div className="cb-secthead">
|
|
89
|
+
<h2 className="cb-secttitle">{label("pricing.section.moneyTitle")}</h2>
|
|
90
|
+
<p className="cb-sectlede">{label("pricing.section.moneyLede")}</p>
|
|
91
|
+
</div>
|
|
92
|
+
<p className="cb-note is-live">{label("pricing.section.moneyNote")}</p>
|
|
93
|
+
</section>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default Pricing;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shell.tsx — the ONE chrome of the cobaia content site under the family
|
|
3
|
+
* fullscreen law: the direct url owns the whole viewport (no floating
|
|
4
|
+
* window, no title bar, no captions — the window chrome exists only inside
|
|
5
|
+
* the DevThink OS power clip, and the OS draws that window itself), so the
|
|
6
|
+
* site renders full-bleed with its own voice — the sticky section bar (the
|
|
7
|
+
* acrylic nav pins over the drawn home mark), the page surface and the
|
|
8
|
+
* worked footer carrying the family row: every deploy unit of the family
|
|
9
|
+
* linked through familyurl(), because the family redirects both ways. The
|
|
10
|
+
* bar rides the trylle canvas with the Windows hands: the section links
|
|
11
|
+
* are the pins — the acrylic bar, the hover wash, the press bounce (scale
|
|
12
|
+
* 0.7, 100ms), the dtPopIntro mount pop and the 3px ladder indicator that
|
|
13
|
+
* grows under the active section; the focus underline and the active pin
|
|
14
|
+
* answer the Windows accent — the identity amber lives only in the brand
|
|
15
|
+
* mark. The active section link reads the location so the visitor always
|
|
16
|
+
* knows where they stand. Inside the OS power clip the embedded contract
|
|
17
|
+
* renders the SAME site surface under the shell--embedded seat: the
|
|
18
|
+
* sticky section bar, the page surface and the worked footer answer in
|
|
19
|
+
* both seats so the design is IDENTICAL at the direct url and inside the
|
|
20
|
+
* os window (the window-era law) — the host window owns the chrome
|
|
21
|
+
* there. Styled entirely by sol/sol.css (the fullscreen law
|
|
22
|
+
* block carries the two contracts).
|
|
23
|
+
*/
|
|
24
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
25
|
+
import { Link, useLocation } from "wouter";
|
|
26
|
+
import { useContent } from "../../content.ts";
|
|
27
|
+
import { familyurl } from "../../familyurl";
|
|
28
|
+
import { type LabelKey, label } from "../../labels";
|
|
29
|
+
import { roster } from "../../roster.ts";
|
|
30
|
+
import { BrandMark } from "../wingets";
|
|
31
|
+
|
|
32
|
+
/** the section links of the top bar, in reading order (home is the mark).
|
|
33
|
+
* the href rides the route table, the text rides the label accessor. */
|
|
34
|
+
const NAVLINKS: readonly { href: string; key: LabelKey }[] = [
|
|
35
|
+
{ href: "/apps", key: "shell.nav.apps" },
|
|
36
|
+
{ href: "/pricing", key: "shell.nav.pricing" },
|
|
37
|
+
{ href: "/blog", key: "shell.nav.blog" },
|
|
38
|
+
{ href: "/support", key: "shell.nav.support" },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/** the shared chrome: the full-bleed site surface — the sticky section bar
|
|
42
|
+
* (with the ONE logo slot as the home mark), the page surface and the
|
|
43
|
+
* footer row; the embedded pass renders the SAME site surface inside the
|
|
44
|
+
* os power clip (the window-era law: one design in both seats). */
|
|
45
|
+
export function Shell({ children }: { children: ReactNode }) {
|
|
46
|
+
useContent();
|
|
47
|
+
const [location] = useLocation();
|
|
48
|
+
|
|
49
|
+
// the embedded contract of the family fullscreen law: inside the os
|
|
50
|
+
// power clip the app rides the host window chrome — the seat class
|
|
51
|
+
// answers the mount, the tree never changes between the two doors
|
|
52
|
+
// (the window-era law: one design in both seats)
|
|
53
|
+
const framed = (() => {
|
|
54
|
+
try {
|
|
55
|
+
return window.self !== window.top;
|
|
56
|
+
} catch {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
60
|
+
|
|
61
|
+
const isActive = (href: string): boolean =>
|
|
62
|
+
href === "/" ? location === "/" : location === href || location.startsWith(`${href}/`);
|
|
63
|
+
|
|
64
|
+
// the embedded seat: inside the os power clip the app rides the host
|
|
65
|
+
// window chrome, but the SURFACE stays the same — the seat class answers
|
|
66
|
+
// the mount so differential styles (if any) can find it, the tree never
|
|
67
|
+
// changes between the two doors (the window-era law: one design)
|
|
68
|
+
const seat = framed ? "shell--embedded" : "shell--standalone";
|
|
69
|
+
|
|
70
|
+
// the surface pass: the direct url answers full-bleed — the window
|
|
71
|
+
// chrome exists only inside the os power clip, so the site owns the whole
|
|
72
|
+
// viewport; the sticky section bar stays as the site's own navigation,
|
|
73
|
+
// the page surface fills the middle and the worked footer closes it
|
|
74
|
+
return (
|
|
75
|
+
<div className={`shell ${seat}`}>
|
|
76
|
+
{/* the skip link answers the viewport: the surface owns the whole
|
|
77
|
+
* document, the skip jumps straight to the content anchor */}
|
|
78
|
+
<a className="cb-skip" href="#content">
|
|
79
|
+
{label("shell.skip")}
|
|
80
|
+
</a>
|
|
81
|
+
<div className="cb-root">
|
|
82
|
+
<header className="cb-top">
|
|
83
|
+
<nav className="cb-nav" aria-label={label("shell.nav.aria")}>
|
|
84
|
+
<Link
|
|
85
|
+
className="cb-brand"
|
|
86
|
+
href="/"
|
|
87
|
+
aria-label={label("shell.brand.aria")}
|
|
88
|
+
aria-current={isActive("/") ? "page" : undefined}
|
|
89
|
+
>
|
|
90
|
+
<BrandMark size={18} />
|
|
91
|
+
<strong>{label("shell.name")}</strong>
|
|
92
|
+
<span className="cb-brandtag">{label("shell.role")}</span>
|
|
93
|
+
</Link>
|
|
94
|
+
{NAVLINKS.map((link) => (
|
|
95
|
+
<Link
|
|
96
|
+
key={link.href}
|
|
97
|
+
className="cb-navlink"
|
|
98
|
+
href={link.href}
|
|
99
|
+
aria-current={isActive(link.href) ? "page" : undefined}
|
|
100
|
+
>
|
|
101
|
+
{label(link.key)}
|
|
102
|
+
</Link>
|
|
103
|
+
))}
|
|
104
|
+
</nav>
|
|
105
|
+
</header>
|
|
106
|
+
|
|
107
|
+
<main id="content" className="cb-main">
|
|
108
|
+
{children}
|
|
109
|
+
</main>
|
|
110
|
+
|
|
111
|
+
<footer className="cb-foot">
|
|
112
|
+
<div className="cb-footgrid">
|
|
113
|
+
<div className="cb-footcol cb-footabout">
|
|
114
|
+
<p className="cb-foothead">{label("shell.foot.aboutHead")}</p>
|
|
115
|
+
<p className="cb-foottext">{label("shell.foot.aboutText")}</p>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="cb-footcol">
|
|
118
|
+
<p className="cb-foothead">{label("shell.foot.siteHead")}</p>
|
|
119
|
+
<Link className="cb-footlink" href="/apps">
|
|
120
|
+
{label("shell.foot.apps")}
|
|
121
|
+
</Link>
|
|
122
|
+
<Link className="cb-footlink" href="/pricing">
|
|
123
|
+
{label("shell.foot.pricing")}
|
|
124
|
+
</Link>
|
|
125
|
+
<Link className="cb-footlink" href="/blog">
|
|
126
|
+
{label("shell.foot.blog")}
|
|
127
|
+
</Link>
|
|
128
|
+
<Link className="cb-footlink" href="/support">
|
|
129
|
+
{label("shell.foot.support")}
|
|
130
|
+
</Link>
|
|
131
|
+
<Link className="cb-footlink" href="/legal">
|
|
132
|
+
{label("shell.foot.legal")}
|
|
133
|
+
</Link>
|
|
134
|
+
</div>
|
|
135
|
+
<div className="cb-footcol cb-footfamily">
|
|
136
|
+
<p className="cb-foothead">{label("shell.foot.familyHead")}</p>
|
|
137
|
+
<div className="cb-famgrid">
|
|
138
|
+
{roster().map((member) => (
|
|
139
|
+
<a
|
|
140
|
+
key={member.slug}
|
|
141
|
+
className="cb-famlink"
|
|
142
|
+
href={familyurl(member.slug)}
|
|
143
|
+
target="_blank"
|
|
144
|
+
rel="noreferrer"
|
|
145
|
+
style={{ "--cb-dot": member.accent } as CSSProperties}
|
|
146
|
+
>
|
|
147
|
+
<span className="cb-famdot" aria-hidden="true" />
|
|
148
|
+
{member.slug}
|
|
149
|
+
</a>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
<p className="cb-colophon">{label("shell.colophon")}</p>
|
|
155
|
+
</footer>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export default Shell;
|