@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
|
@@ -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;
|