create-catalyst-app 1.1.0 → 1.2.0-canary.2
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/Readme.md +13 -0
- package/package.json +1 -1
- package/scripts/appIdentity.cjs +77 -0
- package/scripts/cli.cjs +13 -1
- package/scripts/generateDocs.cjs +6 -0
- package/templates/common/config/config.json +9 -0
- package/templates/common/public/android/splashscreen.png +0 -0
- package/templates/common/public/icon.png +0 -0
- package/templates/common/public/ios/splashscreen.png +0 -0
- package/templates/common/public/offline.html +46 -25
- package/templates/common/public/splashscreen.html +466 -0
- package/templates/common/server/index.js +36 -1
- package/templates/common/src/js/containers/Home/Home.js +119 -14
- package/templates/common/src/js/containers/Home/Home.scss +402 -18
- package/templates/common/src/js/containers/Home/Mark.js +82 -0
- package/templates/common/src/js/containers/Home/fonts/LICENSE-inter.txt +93 -0
- package/templates/common/src/js/containers/Home/fonts/LICENSE-jetbrains-mono.txt +93 -0
- package/templates/common/src/js/containers/Home/fonts/inter.woff2 +0 -0
- package/templates/common/src/js/containers/Home/fonts/jetbrains-mono.woff2 +0 -0
- package/templates/common/src/js/routes/index.js +4 -1
- package/templates/none-js/package.json +6 -2
- package/templates/none-ts/package.json +5 -1
- package/templates/redux-js/package.json +6 -2
- package/templates/redux-ts/package.json +5 -1
- package/templates/rtk-js/package.json +6 -2
- package/templates/rtk-ts/package.json +5 -1
- package/test/appIdentity.test.ts +142 -0
- package/test/errors.test.ts +40 -0
|
@@ -1,21 +1,126 @@
|
|
|
1
|
-
|
|
1
|
+
// Your starter page. Replace this file, or delete the whole Home folder
|
|
2
|
+
// (and its route in src/js/routes/index.js) when you are ready for your own.
|
|
3
|
+
import React, { useEffect, useState } from "react"
|
|
4
|
+
import Mark from "./Mark"
|
|
2
5
|
import css from "./Home.scss"
|
|
3
6
|
|
|
7
|
+
const DOCS = "https://catalyst.1mg.com/content"
|
|
8
|
+
|
|
9
|
+
const NEXT_STEPS = [
|
|
10
|
+
{
|
|
11
|
+
title: "Add a page",
|
|
12
|
+
body: "Scaffold a route and its container. Run this from the app folder.",
|
|
13
|
+
code: "npx create-catalyst-app new-route",
|
|
14
|
+
href: `${DOCS}/Routing/routing/index`,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
title: "Load data",
|
|
18
|
+
body: "Add a serverFetcher to this page, then read the result in your component.",
|
|
19
|
+
code: "useCurrentRouteData()",
|
|
20
|
+
href: `${DOCS}/data-fetching`,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
title: "Ship to a phone",
|
|
24
|
+
body: "Build this same code into a native Android or iOS app.",
|
|
25
|
+
code: "npm run buildApp:android",
|
|
26
|
+
href: `${DOCS}/Guides%20and%20Tutorials/First%20Universal%20App/first-universal-app`,
|
|
27
|
+
},
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const HOOKS = [
|
|
31
|
+
"useCamera",
|
|
32
|
+
"useFilePicker",
|
|
33
|
+
"useHapticFeedback",
|
|
34
|
+
"useNotification",
|
|
35
|
+
"useGoogleSignIn",
|
|
36
|
+
"useNetworkStatus",
|
|
37
|
+
"useSafeArea",
|
|
38
|
+
"useAI",
|
|
39
|
+
]
|
|
40
|
+
|
|
4
41
|
function Home() {
|
|
42
|
+
const [hydratedIn, setHydratedIn] = useState(null)
|
|
43
|
+
|
|
44
|
+
// Effects never run on the server, so this fires once the page is hydrated.
|
|
45
|
+
// performance.now() counts from the start of navigation.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
setHydratedIn(Math.round(performance.now()))
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
5
50
|
return (
|
|
6
|
-
<div className={css.
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
href="https://catalyst.1mg.com"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
51
|
+
<div className={css.page} data-hydrated={hydratedIn !== null}>
|
|
52
|
+
<title>Your app is running | Catalyst</title>
|
|
53
|
+
|
|
54
|
+
<nav className={css.nav}>
|
|
55
|
+
<span className={css.brand}>Catalyst</span>
|
|
56
|
+
<span className={css.navLinks}>
|
|
57
|
+
<a href="https://catalyst.1mg.com">Docs</a>
|
|
58
|
+
<a href="https://github.com/tata1mg/catalyst-core">GitHub</a>
|
|
59
|
+
</span>
|
|
60
|
+
</nav>
|
|
61
|
+
|
|
62
|
+
<main>
|
|
63
|
+
<section className={css.hero}>
|
|
64
|
+
<div className={css.stage}>
|
|
65
|
+
<span className={css.burst} aria-hidden="true" />
|
|
66
|
+
<Mark />
|
|
67
|
+
<span className={css.spark} aria-hidden="true" />
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div className={css.intro}>
|
|
71
|
+
<h1 className={css.title}>
|
|
72
|
+
Your app is <em>running.</em>
|
|
73
|
+
</h1>
|
|
74
|
+
<p className={css.lede}>
|
|
75
|
+
Open <code>src/js/containers/Home/Home.js</code>, change anything, and save. This
|
|
76
|
+
page updates on its own.
|
|
77
|
+
</p>
|
|
78
|
+
<p className={css.status} role="status">
|
|
79
|
+
<span>Rendered on the server</span>
|
|
80
|
+
<span>
|
|
81
|
+
{hydratedIn === null
|
|
82
|
+
? "Hydrating in your browser"
|
|
83
|
+
: `Interactive in ${hydratedIn} ms`}
|
|
84
|
+
</span>
|
|
85
|
+
</p>
|
|
86
|
+
</div>
|
|
87
|
+
</section>
|
|
88
|
+
|
|
89
|
+
<section className={css.next} aria-labelledby="next-heading">
|
|
90
|
+
<h2 id="next-heading" className={css.heading}>
|
|
91
|
+
Build on it.
|
|
92
|
+
</h2>
|
|
93
|
+
<ul className={css.steps}>
|
|
94
|
+
{NEXT_STEPS.map((step) => (
|
|
95
|
+
<li key={step.title}>
|
|
96
|
+
<a className={css.step} href={step.href}>
|
|
97
|
+
<strong>{step.title}</strong>
|
|
98
|
+
<span>{step.body}</span>
|
|
99
|
+
<code>{step.code}</code>
|
|
100
|
+
</a>
|
|
101
|
+
</li>
|
|
102
|
+
))}
|
|
103
|
+
</ul>
|
|
104
|
+
</section>
|
|
105
|
+
|
|
106
|
+
<section className={css.native} aria-labelledby="native-heading">
|
|
107
|
+
<h2 id="native-heading" className={css.heading}>
|
|
108
|
+
Native features are plain <em>React hooks.</em>
|
|
109
|
+
</h2>
|
|
110
|
+
<p className={css.hooksLede}>
|
|
111
|
+
Import them from <code>catalyst-core/hooks</code>. They work on Android and iOS, and
|
|
112
|
+
some work on the web too.
|
|
113
|
+
</p>
|
|
114
|
+
<p className={css.hooks}>
|
|
115
|
+
{HOOKS.map((hook) => (
|
|
116
|
+
<a key={hook} href={`${DOCS}/API%20Reference/hooks`}>
|
|
117
|
+
{hook}
|
|
118
|
+
<i>()</i>
|
|
119
|
+
</a>
|
|
120
|
+
))}
|
|
121
|
+
</p>
|
|
122
|
+
</section>
|
|
123
|
+
</main>
|
|
19
124
|
</div>
|
|
20
125
|
)
|
|
21
126
|
}
|
|
@@ -1,34 +1,418 @@
|
|
|
1
|
+
@font-face {
|
|
2
|
+
font-family: "Inter Catalyst";
|
|
3
|
+
src: url("./fonts/inter.woff2") format("woff2");
|
|
4
|
+
font-weight: 100 900;
|
|
5
|
+
font-display: swap;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@font-face {
|
|
9
|
+
font-family: "JetBrains Mono Catalyst";
|
|
10
|
+
src: url("./fonts/jetbrains-mono.woff2") format("woff2");
|
|
11
|
+
font-weight: 100 800;
|
|
12
|
+
font-display: swap;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
:root {
|
|
16
|
+
color-scheme: light dark;
|
|
17
|
+
--paper: #f5f4ff;
|
|
18
|
+
--ink: #14132a;
|
|
19
|
+
--muted: #4b4a6b;
|
|
20
|
+
--accent: #5353dc;
|
|
21
|
+
--hairline: rgba(20, 19, 42, 0.14);
|
|
22
|
+
--dot: rgba(83, 83, 220, 0.2);
|
|
23
|
+
--wedge: rgba(122, 122, 255, 0.11);
|
|
24
|
+
--ray-outer: #5b5be6;
|
|
25
|
+
--ray-inner: #e3e3ff;
|
|
26
|
+
--spark: #ffffff;
|
|
27
|
+
--spark-glow: rgba(122, 122, 255, 0.55);
|
|
28
|
+
--sans: "Inter Catalyst", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
29
|
+
--mono: "JetBrains Mono Catalyst", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
30
|
+
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@media (prefers-color-scheme: dark) {
|
|
34
|
+
:root {
|
|
35
|
+
--paper: #0b0a14;
|
|
36
|
+
--ink: #f3f2ff;
|
|
37
|
+
--muted: #a9a8c9;
|
|
38
|
+
--accent: #a0a0ff;
|
|
39
|
+
--hairline: rgba(243, 242, 255, 0.16);
|
|
40
|
+
--dot: rgba(160, 160, 255, 0.14);
|
|
41
|
+
--wedge: rgba(122, 122, 255, 0.09);
|
|
42
|
+
--ray-outer: #7a7aff;
|
|
43
|
+
--ray-inner: #ffffff;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
1
47
|
body {
|
|
2
48
|
margin: 0;
|
|
49
|
+
background: var(--paper);
|
|
3
50
|
}
|
|
4
51
|
|
|
5
|
-
.
|
|
6
|
-
|
|
7
|
-
|
|
52
|
+
.page {
|
|
53
|
+
position: relative;
|
|
54
|
+
isolation: isolate;
|
|
55
|
+
overflow: clip;
|
|
56
|
+
min-height: 100dvh;
|
|
57
|
+
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom)
|
|
58
|
+
env(safe-area-inset-left);
|
|
59
|
+
background-color: var(--paper);
|
|
60
|
+
background-image: radial-gradient(var(--dot) 1px, transparent 1.5px);
|
|
61
|
+
background-size: 28px 28px;
|
|
62
|
+
color: var(--ink);
|
|
63
|
+
font-family: var(--sans);
|
|
64
|
+
font-size: 1rem;
|
|
65
|
+
line-height: 1.55;
|
|
66
|
+
-webkit-font-smoothing: antialiased;
|
|
67
|
+
|
|
68
|
+
a {
|
|
69
|
+
color: inherit;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
a:focus-visible {
|
|
73
|
+
outline: 2px solid var(--accent);
|
|
74
|
+
outline-offset: 3px;
|
|
75
|
+
border-radius: 4px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
code {
|
|
79
|
+
font-family: var(--mono);
|
|
80
|
+
font-size: 0.92em;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
em {
|
|
84
|
+
color: var(--accent);
|
|
85
|
+
font-style: normal;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The sunburst from the release films, centred on the spark.
|
|
90
|
+
.burst {
|
|
91
|
+
position: absolute;
|
|
92
|
+
z-index: -1;
|
|
93
|
+
top: 50%;
|
|
94
|
+
left: 100%;
|
|
95
|
+
width: 170vmax;
|
|
96
|
+
aspect-ratio: 1;
|
|
97
|
+
border-radius: 50%;
|
|
98
|
+
translate: -50% -50%;
|
|
99
|
+
background: repeating-conic-gradient(from 3deg, var(--wedge) 0 9deg, transparent 9deg 25.7deg);
|
|
100
|
+
mask-image: radial-gradient(closest-side, transparent 1%, #000 10%, transparent 60%);
|
|
101
|
+
-webkit-mask-image: radial-gradient(closest-side, transparent 1%, #000 10%, transparent 60%);
|
|
102
|
+
opacity: 0;
|
|
103
|
+
scale: 0.94;
|
|
104
|
+
transition:
|
|
105
|
+
opacity 900ms var(--ease-out),
|
|
106
|
+
scale 1200ms var(--ease-out);
|
|
8
107
|
}
|
|
9
108
|
|
|
10
|
-
.
|
|
11
|
-
|
|
12
|
-
|
|
109
|
+
[data-hydrated="true"] .burst {
|
|
110
|
+
opacity: 1;
|
|
111
|
+
scale: 1;
|
|
13
112
|
}
|
|
14
113
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
114
|
+
.nav {
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: space-between;
|
|
118
|
+
max-width: 1240px;
|
|
119
|
+
margin: 0 auto;
|
|
120
|
+
padding: 24px clamp(24px, 5vw, 56px);
|
|
121
|
+
box-sizing: border-box;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.brand {
|
|
125
|
+
font-size: 1.0625rem;
|
|
126
|
+
font-weight: 750;
|
|
127
|
+
letter-spacing: -0.03em;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.navLinks {
|
|
131
|
+
display: flex;
|
|
132
|
+
gap: 8px;
|
|
133
|
+
font-size: 0.9375rem;
|
|
134
|
+
|
|
135
|
+
a {
|
|
136
|
+
padding: 10px 12px;
|
|
137
|
+
color: var(--muted);
|
|
138
|
+
text-decoration: none;
|
|
139
|
+
transition: color 150ms ease-out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
a:last-child {
|
|
143
|
+
margin-right: -12px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
a:hover {
|
|
147
|
+
color: var(--ink);
|
|
18
148
|
}
|
|
19
149
|
}
|
|
20
150
|
|
|
21
|
-
.
|
|
22
|
-
background-color: #282c34;
|
|
23
|
-
min-height: 100vh;
|
|
151
|
+
.hero {
|
|
24
152
|
display: flex;
|
|
25
|
-
flex-direction: column;
|
|
26
153
|
align-items: center;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
154
|
+
gap: clamp(20px, 3.4vw, 52px);
|
|
155
|
+
max-width: 1240px;
|
|
156
|
+
min-height: calc(min(84dvh, 780px) - 88px);
|
|
157
|
+
margin: 0 auto;
|
|
158
|
+
padding: 0 clamp(24px, 5vw, 56px) 48px;
|
|
159
|
+
box-sizing: border-box;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.stage {
|
|
163
|
+
position: relative;
|
|
164
|
+
flex: none;
|
|
165
|
+
width: clamp(120px, 21vw, 300px);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.mark {
|
|
169
|
+
display: block;
|
|
170
|
+
width: 100%;
|
|
171
|
+
overflow: visible;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.rayOuter {
|
|
175
|
+
stop-color: var(--ray-outer);
|
|
30
176
|
}
|
|
31
177
|
|
|
32
|
-
.
|
|
33
|
-
color:
|
|
178
|
+
.rayInner {
|
|
179
|
+
stop-color: var(--ray-inner);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.ray {
|
|
183
|
+
animation: ray-in 720ms var(--ease-out) var(--delay) both;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The spark sits where the rays meet, and lights up once React has hydrated.
|
|
187
|
+
.spark {
|
|
188
|
+
position: absolute;
|
|
189
|
+
top: 50%;
|
|
190
|
+
left: 100%;
|
|
191
|
+
width: 7%;
|
|
192
|
+
aspect-ratio: 1;
|
|
193
|
+
border-radius: 50%;
|
|
194
|
+
translate: -50% -50%;
|
|
195
|
+
background: var(--spark);
|
|
196
|
+
box-shadow:
|
|
197
|
+
0 0 6px 2px var(--spark),
|
|
198
|
+
0 0 24px 8px var(--spark-glow),
|
|
199
|
+
0 0 80px 30px var(--spark-glow);
|
|
200
|
+
opacity: 0;
|
|
201
|
+
scale: 0.4;
|
|
202
|
+
transition:
|
|
203
|
+
opacity 500ms var(--ease-out),
|
|
204
|
+
scale 700ms var(--ease-out);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
[data-hydrated="true"] .spark {
|
|
208
|
+
opacity: 1;
|
|
209
|
+
scale: 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.intro {
|
|
213
|
+
min-width: 0;
|
|
214
|
+
animation: rise-in 640ms var(--ease-out) 240ms both;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.title {
|
|
218
|
+
margin: 0;
|
|
219
|
+
font-size: clamp(2.75rem, 7.4vw, 6rem);
|
|
220
|
+
font-weight: 800;
|
|
221
|
+
line-height: 1;
|
|
222
|
+
letter-spacing: -0.04em;
|
|
223
|
+
text-wrap: balance;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.lede {
|
|
227
|
+
max-width: 34rem;
|
|
228
|
+
margin: 24px 0 0;
|
|
229
|
+
color: var(--muted);
|
|
230
|
+
font-size: clamp(1.0625rem, 1.5vw, 1.25rem);
|
|
231
|
+
line-height: 1.5;
|
|
232
|
+
text-wrap: pretty;
|
|
233
|
+
|
|
234
|
+
code {
|
|
235
|
+
color: var(--ink);
|
|
236
|
+
font-weight: 500;
|
|
237
|
+
overflow-wrap: anywhere;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.status {
|
|
242
|
+
display: flex;
|
|
243
|
+
flex-wrap: wrap;
|
|
244
|
+
gap: 6px 22px;
|
|
245
|
+
margin: 28px 0 0;
|
|
246
|
+
color: var(--muted);
|
|
247
|
+
font-family: var(--mono);
|
|
248
|
+
font-size: 0.8125rem;
|
|
249
|
+
font-variant-numeric: tabular-nums;
|
|
250
|
+
|
|
251
|
+
span::before {
|
|
252
|
+
content: "\2713\00a0";
|
|
253
|
+
color: var(--accent);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
span:last-child::before {
|
|
257
|
+
content: "\2026\00a0";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
[data-hydrated="true"] .status span:last-child::before {
|
|
262
|
+
content: "\2713\00a0";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.next,
|
|
266
|
+
.native {
|
|
267
|
+
max-width: 1240px;
|
|
268
|
+
margin: 0 auto;
|
|
269
|
+
padding: clamp(56px, 8vw, 104px) clamp(24px, 5vw, 56px) 0;
|
|
270
|
+
box-sizing: border-box;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.native {
|
|
274
|
+
padding-bottom: clamp(64px, 9vw, 120px);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.heading {
|
|
278
|
+
margin: 0;
|
|
279
|
+
font-size: clamp(2rem, 4.4vw, 3.5rem);
|
|
280
|
+
font-weight: 800;
|
|
281
|
+
line-height: 1.05;
|
|
282
|
+
letter-spacing: -0.035em;
|
|
283
|
+
text-wrap: balance;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.steps {
|
|
287
|
+
margin: 32px 0 0;
|
|
288
|
+
padding: 0;
|
|
289
|
+
border-top: 1px solid var(--hairline);
|
|
290
|
+
list-style: none;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.step {
|
|
294
|
+
display: grid;
|
|
295
|
+
grid-template-columns: 11rem minmax(0, 1fr) 19.5rem;
|
|
296
|
+
align-items: baseline;
|
|
297
|
+
gap: 6px 32px;
|
|
298
|
+
padding: 22px 0;
|
|
299
|
+
border-bottom: 1px solid var(--hairline);
|
|
300
|
+
text-decoration: none;
|
|
301
|
+
|
|
302
|
+
strong {
|
|
303
|
+
font-size: 1.125rem;
|
|
304
|
+
font-weight: 700;
|
|
305
|
+
letter-spacing: -0.015em;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
span {
|
|
309
|
+
color: var(--muted);
|
|
310
|
+
text-wrap: pretty;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
code {
|
|
314
|
+
color: var(--accent);
|
|
315
|
+
transition: translate 200ms var(--ease-out);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
@media (hover: hover) and (pointer: fine) {
|
|
320
|
+
.step:hover code {
|
|
321
|
+
translate: 4px 0;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.hooks a:hover {
|
|
325
|
+
color: var(--accent);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.hooksLede {
|
|
330
|
+
max-width: 38rem;
|
|
331
|
+
margin: 16px 0 0;
|
|
332
|
+
color: var(--muted);
|
|
333
|
+
font-size: 1.0625rem;
|
|
334
|
+
text-wrap: pretty;
|
|
335
|
+
|
|
336
|
+
code {
|
|
337
|
+
color: var(--ink);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.hooks {
|
|
342
|
+
display: flex;
|
|
343
|
+
flex-wrap: wrap;
|
|
344
|
+
gap: 2px clamp(18px, 2.6vw, 36px);
|
|
345
|
+
margin: 32px 0 0;
|
|
346
|
+
font-family: var(--mono);
|
|
347
|
+
font-size: clamp(1.5rem, 3.5vw, 2.75rem);
|
|
348
|
+
font-weight: 700;
|
|
349
|
+
line-height: 1.3;
|
|
350
|
+
letter-spacing: -0.025em;
|
|
351
|
+
|
|
352
|
+
a {
|
|
353
|
+
text-decoration: none;
|
|
354
|
+
transition: color 150ms ease-out;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
i {
|
|
358
|
+
color: var(--accent);
|
|
359
|
+
font-style: normal;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
@keyframes ray-in {
|
|
364
|
+
from {
|
|
365
|
+
opacity: 0;
|
|
366
|
+
transform: translate(var(--from-x), var(--from-y));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
@keyframes rise-in {
|
|
371
|
+
from {
|
|
372
|
+
opacity: 0;
|
|
373
|
+
transform: translateY(10px);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
@media (max-width: 960px) {
|
|
378
|
+
.step {
|
|
379
|
+
grid-template-columns: minmax(0, 1fr);
|
|
380
|
+
|
|
381
|
+
code {
|
|
382
|
+
margin-top: 6px;
|
|
383
|
+
overflow-wrap: anywhere;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
@media (max-width: 760px) {
|
|
389
|
+
.hero {
|
|
390
|
+
flex-direction: column;
|
|
391
|
+
align-items: flex-start;
|
|
392
|
+
gap: 36px;
|
|
393
|
+
min-height: 0;
|
|
394
|
+
padding-top: 20px;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.stage {
|
|
398
|
+
width: clamp(120px, 30vw, 170px);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.hooks {
|
|
402
|
+
flex-direction: column;
|
|
403
|
+
align-items: flex-start;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
@media (prefers-reduced-motion: reduce) {
|
|
408
|
+
.ray,
|
|
409
|
+
.intro {
|
|
410
|
+
animation: none;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.burst,
|
|
414
|
+
.spark,
|
|
415
|
+
.step code {
|
|
416
|
+
transition: none;
|
|
417
|
+
}
|
|
34
418
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import css from "./Home.scss"
|
|
3
|
+
|
|
4
|
+
// The 14 rays of the Catalyst mark, as [outer x, outer y, inner x, inner y].
|
|
5
|
+
const RAYS = [
|
|
6
|
+
[11.8, 89.7, 80, 90.4],
|
|
7
|
+
[33.9, 51.3, 92, 70.7],
|
|
8
|
+
[56, 13.2, 103, 53.6],
|
|
9
|
+
[100.6, 11.8, 124.5, 52.6],
|
|
10
|
+
[145.3, 12.5, 146.2, 52],
|
|
11
|
+
[145.3, 167, 146.2, 128],
|
|
12
|
+
[100.4, 167.6, 124.4, 128.2],
|
|
13
|
+
[56, 167.1, 103, 127.8],
|
|
14
|
+
[33.7, 128.9, 92, 109.5],
|
|
15
|
+
[122.2, 51.5, 135.5, 72.8],
|
|
16
|
+
[78.4, 51.5, 112.8, 71.3],
|
|
17
|
+
[56.4, 89.6, 102.6, 90.4],
|
|
18
|
+
[78.5, 128.3, 112.7, 109.6],
|
|
19
|
+
[122.3, 128.4, 135.7, 108.2],
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const OUTER_RADIUS = 11.8
|
|
23
|
+
const INNER_RADIUS = 5.8
|
|
24
|
+
|
|
25
|
+
// A capsule that narrows from the outer end to the inner end.
|
|
26
|
+
function rayPath([x1, y1, x2, y2]) {
|
|
27
|
+
const length = Math.hypot(x2 - x1, y2 - y1)
|
|
28
|
+
const nx = -(y2 - y1) / length
|
|
29
|
+
const ny = (x2 - x1) / length
|
|
30
|
+
const point = (x, y, radius, side) =>
|
|
31
|
+
`${(x + nx * radius * side).toFixed(2)} ${(y + ny * radius * side).toFixed(2)}`
|
|
32
|
+
|
|
33
|
+
return [
|
|
34
|
+
`M${point(x1, y1, OUTER_RADIUS, 1)}`,
|
|
35
|
+
`L${point(x2, y2, INNER_RADIUS, 1)}`,
|
|
36
|
+
`A${INNER_RADIUS} ${INNER_RADIUS} 0 0 0 ${point(x2, y2, INNER_RADIUS, -1)}`,
|
|
37
|
+
`L${point(x1, y1, OUTER_RADIUS, -1)}`,
|
|
38
|
+
`A${OUTER_RADIUS} ${OUTER_RADIUS} 0 0 0 ${point(x1, y1, OUTER_RADIUS, 1)}`,
|
|
39
|
+
"Z",
|
|
40
|
+
].join("")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function Mark() {
|
|
44
|
+
return (
|
|
45
|
+
<svg className={css.mark} viewBox="0 0 158 180" aria-hidden="true">
|
|
46
|
+
<defs>
|
|
47
|
+
{RAYS.map(([x1, y1, x2, y2], index) => (
|
|
48
|
+
<linearGradient
|
|
49
|
+
key={index}
|
|
50
|
+
id={`catalyst-ray-${index}`}
|
|
51
|
+
gradientUnits="userSpaceOnUse"
|
|
52
|
+
x1={x1}
|
|
53
|
+
y1={y1}
|
|
54
|
+
x2={x2}
|
|
55
|
+
y2={y2}
|
|
56
|
+
>
|
|
57
|
+
<stop offset="0" className={css.rayOuter} />
|
|
58
|
+
<stop offset="1" className={css.rayInner} />
|
|
59
|
+
</linearGradient>
|
|
60
|
+
))}
|
|
61
|
+
</defs>
|
|
62
|
+
{RAYS.map((ray, index) => {
|
|
63
|
+
const length = Math.hypot(ray[2] - ray[0], ray[3] - ray[1])
|
|
64
|
+
return (
|
|
65
|
+
<path
|
|
66
|
+
key={index}
|
|
67
|
+
className={css.ray}
|
|
68
|
+
d={rayPath(ray)}
|
|
69
|
+
fill={`url(#catalyst-ray-${index})`}
|
|
70
|
+
style={{
|
|
71
|
+
"--from-x": `${((ray[0] - ray[2]) / length) * 26}px`,
|
|
72
|
+
"--from-y": `${((ray[1] - ray[3]) / length) * 26}px`,
|
|
73
|
+
"--delay": `${index * 34}ms`,
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
})}
|
|
78
|
+
</svg>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default Mark
|