skillet-shared 0.2.0 → 0.4.0

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.
@@ -0,0 +1,19 @@
1
+ ---
2
+ import type { SiteInfo } from '../dist/site-info.js'
3
+
4
+ interface Props {
5
+ site: SiteInfo
6
+ }
7
+ const { site } = Astro.props
8
+ ---
9
+
10
+ <footer class="siteFoot">
11
+ <div class="footIn">
12
+ <nav aria-label="Site">
13
+ {site.footer.map(([href, label]) => (
14
+ <a href={href}>{label}</a>
15
+ ))}
16
+ <a href="#top">Back to top ↑</a>
17
+ </nav>
18
+ </div>
19
+ </footer>
@@ -0,0 +1,28 @@
1
+ ---
2
+ interface Props {
3
+ hint: string
4
+ }
5
+ const { hint: SIGNED_IN_HINT } = Astro.props
6
+ ---
7
+ <script is:inline define:vars={{ SIGNED_IN_HINT }}>
8
+ try {
9
+ if (localStorage.getItem(SIGNED_IN_HINT) === '1') {
10
+ const root = document.documentElement
11
+ root.dataset.home = 'pending'
12
+ for (const host of [
13
+ 'identitytoolkit.googleapis.com',
14
+ 'securetoken.googleapis.com',
15
+ 'firestore.googleapis.com',
16
+ ]) {
17
+ const link = document.createElement('link')
18
+ link.rel = 'preconnect'
19
+ link.href = `https://${host}`
20
+ link.crossOrigin = 'anonymous'
21
+ document.head.append(link)
22
+ }
23
+ setTimeout(() => {
24
+ delete root.dataset.home
25
+ }, 15000)
26
+ }
27
+ } catch {}
28
+ </script>
@@ -0,0 +1,77 @@
1
+ ---
2
+ import type { SiteInfo } from '../dist/site-info.js'
3
+
4
+ interface Props {
5
+ site: SiteInfo
6
+ account?: boolean
7
+ current?: string
8
+ }
9
+ const { site, account = true, current } = Astro.props
10
+ ---
11
+
12
+ <header class="siteBar">
13
+ <div class="barIn">
14
+ <a class="barBrand" href={site.home}>
15
+ <img src={site.icon} width="24" height="24" alt="" />
16
+ <span>{site.name}</span>
17
+ </a>
18
+ <nav class="barNav" aria-label="Site">
19
+ {site.nav.map(([href, label]) => (
20
+ <a href={href} aria-current={href === current ? 'page' : undefined}>
21
+ {label}
22
+ </a>
23
+ ))}
24
+ </nav>
25
+ <div class="barEnd">
26
+ {account && (
27
+ <Fragment>
28
+ {/* The question beside the ask, where the reader meets it. */}
29
+ <button class="why" id="why" type="button">
30
+ Why sign in?
31
+ </button>
32
+ <button
33
+ class="btn slim"
34
+ id="signIn"
35
+ type="button"
36
+ aria-label="Sign in"
37
+ >
38
+ <svg
39
+ class="ico"
40
+ viewBox="0 0 24 24"
41
+ fill="none"
42
+ stroke="currentColor"
43
+ stroke-width="1.7"
44
+ stroke-linecap="round"
45
+ stroke-linejoin="round"
46
+ aria-hidden="true"
47
+ >
48
+ <circle cx="12" cy="8" r="3.6" />
49
+ <path d="M4.8 20a7.2 7.2 0 0 1 14.4 0" />
50
+ </svg>
51
+ <span class="barLabel">Sign in</span>
52
+ </button>
53
+ <div class="acct" id="acct" hidden>
54
+ <button
55
+ class="acctBtn"
56
+ id="acctBtn"
57
+ type="button"
58
+ aria-controls="acctMenu"
59
+ aria-expanded="false"
60
+ >
61
+ <span class="avatar" id="avatar" />
62
+ </button>
63
+ <div class="acctMenu" id="acctMenu" hidden>
64
+ <p class="acctName" id="acctName" />
65
+ <button class="acctOut" id="signOut" type="button">
66
+ Sign out
67
+ </button>
68
+ </div>
69
+ </div>
70
+ </Fragment>
71
+ )}
72
+ <a class="btn primary slim" href={site.app}>
73
+ Open the app →
74
+ </a>
75
+ </div>
76
+ </div>
77
+ </header>
@@ -0,0 +1,32 @@
1
+ ---
2
+ // How a link to this page unfurls when somebody shares it. The values differ
3
+ // per product and per page; the tags do not, so the whole file is shared.
4
+ //
5
+ // The description here is the one a reader meets in a chat window rather than
6
+ // in a search result, so a page may hand in a shorter one than it gives
7
+ // `<meta name="description">`.
8
+ interface Props {
9
+ siteName: string
10
+ url: string
11
+ title: string
12
+ description: string
13
+ image: string
14
+ imageWidth: number
15
+ imageHeight: number
16
+ }
17
+ const { siteName, url, title, description, image, imageWidth, imageHeight } =
18
+ Astro.props
19
+ ---
20
+
21
+ <meta property="og:type" content="website" />
22
+ <meta property="og:site_name" content={siteName} />
23
+ <meta property="og:url" content={url} />
24
+ <meta property="og:title" content={title} />
25
+ <meta property="og:description" content={description} />
26
+ <meta property="og:image" content={image} />
27
+ <meta property="og:image:width" content={imageWidth} />
28
+ <meta property="og:image:height" content={imageHeight} />
29
+ <meta name="twitter:card" content="summary_large_image" />
30
+ <meta name="twitter:title" content={title} />
31
+ <meta name="twitter:description" content={description} />
32
+ <meta name="twitter:image" content={image} />
package/css/footer.css ADDED
@@ -0,0 +1,28 @@
1
+ /* The band at the bottom of every site page, landing, guide and privacy alike. */
2
+ .siteFoot {
3
+ border-top: 1px solid var(--border-faint);
4
+ background: var(--surface);
5
+ }
6
+ .footIn {
7
+ max-width: 63rem;
8
+ margin: 0 auto;
9
+ padding: 1.75rem max(1.15rem, env(safe-area-inset-left))
10
+ calc(2rem + env(safe-area-inset-bottom));
11
+ font-size: 0.86rem;
12
+ }
13
+ .footIn nav {
14
+ display: flex;
15
+ flex-wrap: wrap;
16
+ gap: 0.5rem 1.35rem;
17
+ margin-bottom: 0.9rem;
18
+ }
19
+ .footIn a {
20
+ color: var(--fg3);
21
+ text-decoration: none;
22
+ }
23
+ .footIn a:hover {
24
+ color: var(--fg);
25
+ }
26
+ .footIn small {
27
+ color: var(--fg4);
28
+ }
package/css/site.css ADDED
@@ -0,0 +1,717 @@
1
+ /* The static site's shared components, identical in videoskillet and bender:
2
+ the page base, buttons, the site bar, the account menu, the why-sign-in card,
3
+ the privacy, about and blog pages, the signed-in home and its skeleton.
4
+ Colours come from tokens.css and each product's --accent and --accent-ink. */
5
+ :root {
6
+ --measure: 46rem;
7
+ }
8
+ body {
9
+ margin: 0;
10
+ background: var(--bg);
11
+ color: var(--fg);
12
+ line-height: 1.65;
13
+ font-family: var(--font);
14
+ font-size: 16px;
15
+ -webkit-text-size-adjust: 100%;
16
+ }
17
+ a {
18
+ color: var(--accent);
19
+ }
20
+ /* `display` on a class beats the browser's own `[hidden]` rule, and the bar
21
+ hides an inline-flex button and a flex box by setting the attribute. */
22
+ [hidden] {
23
+ display: none !important;
24
+ }
25
+ .wrap {
26
+ max-width: var(--measure);
27
+ margin: 0 auto;
28
+ /* Inline only: `padding` here is a shorthand, and it used to zero the
29
+ `padding-block` a page's own sheet had set. The inset keeps the text clear
30
+ of a cutout, the way .barIn and .footIn already do — it reads as zero on
31
+ every screen that has no cutout. */
32
+ padding-inline: max(1.25rem, env(safe-area-inset-left));
33
+ }
34
+ .wide {
35
+ max-width: 72rem;
36
+ }
37
+
38
+ section {
39
+ padding: 3.5rem 0;
40
+ }
41
+ .head {
42
+ font-size: 1.35rem;
43
+ font-weight: 600;
44
+ color: var(--fg);
45
+ margin: 0 0 0.5rem;
46
+ }
47
+ .sub {
48
+ color: var(--fg4);
49
+ font-size: 0.95rem;
50
+ margin: 0 0 1.1rem;
51
+ max-width: var(--measure);
52
+ }
53
+
54
+ .btn {
55
+ display: inline-block;
56
+ padding: 0.72rem 1.35rem;
57
+ border-radius: 0.5rem;
58
+ border: 1px solid var(--border-faint);
59
+ background: var(--surface);
60
+ color: var(--fg);
61
+ text-decoration: none;
62
+ font-weight: 600;
63
+ font-size: 1rem;
64
+ }
65
+ .btn:hover {
66
+ border-color: var(--fg4);
67
+ }
68
+ .btn.primary {
69
+ background: var(--accent);
70
+ border-color: var(--accent);
71
+ color: var(--accent-ink);
72
+ }
73
+ .btn.primary:hover {
74
+ filter: brightness(1.08);
75
+ }
76
+
77
+ /* ── the site bar ──────────────────────────────────────────────────────────
78
+ A slim row over both states of `/`: the site's three destinations on the
79
+ left, the account and the app on the right. The guide's own bar is in
80
+ guide.css and keeps its shape; the two share the palette and nothing else,
81
+ because the middle of the guide's bar is the pages of one group and the
82
+ middle of this one is the site. */
83
+ .siteBar {
84
+ border-bottom: 1px solid var(--border-faint);
85
+ background: var(--bg);
86
+ }
87
+ .barIn {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 0 1.25rem;
91
+ height: 56px;
92
+ max-width: 72rem;
93
+ margin: 0 auto;
94
+ padding: 0 max(1.15rem, env(safe-area-inset-left));
95
+ }
96
+ .barBrand {
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 0.5rem;
100
+ font-weight: 700;
101
+ font-size: 15px;
102
+ color: var(--fg);
103
+ text-decoration: none;
104
+ white-space: nowrap;
105
+ }
106
+ .barNav {
107
+ display: flex;
108
+ gap: 1.15rem;
109
+ margin-right: auto;
110
+ }
111
+ .barNav a {
112
+ color: var(--fg3);
113
+ text-decoration: none;
114
+ font-size: 14px;
115
+ white-space: nowrap;
116
+ }
117
+ .barNav a:hover {
118
+ color: var(--fg);
119
+ }
120
+ .barEnd {
121
+ display: flex;
122
+ align-items: center;
123
+ gap: 0.6rem;
124
+ margin-left: auto;
125
+ }
126
+ /* The bar's own size for `.btn`, which the hero sets at full size. */
127
+ .btn.slim {
128
+ display: inline-flex;
129
+ align-items: center;
130
+ gap: 0.4rem;
131
+ padding: 0.4rem 0.85rem;
132
+ font-size: 0.875rem;
133
+ line-height: 1.25;
134
+ }
135
+ .ico {
136
+ width: 1.05rem;
137
+ height: 1.05rem;
138
+ flex: none;
139
+ }
140
+ /* On a phone the bar keeps the brand and the two buttons it exists for. The
141
+ destinations are in the footer and the rail, "Why sign in?" is asked again
142
+ under the hero, and the sign-in button keeps its icon and its label for
143
+ screen readers. The narrowest phones keep the logo without the name. */
144
+ .barEnd .btn {
145
+ white-space: nowrap;
146
+ justify-content: center;
147
+ min-width: 8.5rem;
148
+ }
149
+ @media (max-width: 44rem) {
150
+ .barIn {
151
+ gap: 0 0.75rem;
152
+ }
153
+ .barNav,
154
+ #why,
155
+ .barLabel {
156
+ display: none;
157
+ }
158
+ .barBrand {
159
+ margin-right: auto;
160
+ }
161
+ .barEnd {
162
+ gap: 0.5rem;
163
+ }
164
+ .barEnd .btn {
165
+ min-width: 0;
166
+ }
167
+ #signIn {
168
+ padding-inline: 0.55rem;
169
+ }
170
+ }
171
+ @media (max-width: 23rem) {
172
+ .barBrand span {
173
+ display: none;
174
+ }
175
+ }
176
+
177
+ .acct {
178
+ position: relative;
179
+ }
180
+ .acctBtn {
181
+ display: block;
182
+ padding: 0;
183
+ border: 0;
184
+ background: none;
185
+ cursor: pointer;
186
+ }
187
+ .avatar {
188
+ display: grid;
189
+ place-items: center;
190
+ width: 28px;
191
+ height: 28px;
192
+ border-radius: 50%;
193
+ overflow: hidden;
194
+ border: 1px solid var(--border-faint);
195
+ background: var(--surface);
196
+ color: var(--fg2);
197
+ font-size: 0.8rem;
198
+ font-weight: 600;
199
+ }
200
+ .avatar img {
201
+ width: 100%;
202
+ height: 100%;
203
+ object-fit: cover;
204
+ display: block;
205
+ }
206
+ .acctBtn:hover .avatar,
207
+ .acctBtn:focus-visible .avatar {
208
+ border-color: var(--accent);
209
+ }
210
+ .acctMenu {
211
+ position: absolute;
212
+ right: 0;
213
+ top: calc(100% + 0.45rem);
214
+ z-index: 20;
215
+ min-width: 11rem;
216
+ padding: 0.5rem;
217
+ border: 1px solid var(--border-faint);
218
+ border-radius: 0.5rem;
219
+ background: var(--surface);
220
+ }
221
+ .acctName {
222
+ margin: 0 0 0.3rem;
223
+ padding: 0.15rem 0.4rem;
224
+ font-size: 0.85rem;
225
+ color: var(--fg2);
226
+ overflow-wrap: anywhere;
227
+ }
228
+ .acctOut {
229
+ width: 100%;
230
+ padding: 0.35rem 0.4rem;
231
+ border: 0;
232
+ border-radius: 0.35rem;
233
+ background: none;
234
+ color: var(--fg3);
235
+ font: inherit;
236
+ font-size: 0.85rem;
237
+ text-align: left;
238
+ cursor: pointer;
239
+ }
240
+ .acctOut:hover {
241
+ background: var(--bg);
242
+ color: var(--fg);
243
+ }
244
+
245
+ /* ── why sign in ───────────────────────────────────────────────────────────
246
+ The question beside the bar's sign-in button and under the hero, and the card
247
+ both of them open. The card's prose is src/ui/whySignIn.ts, the same strings
248
+ the app's own dialog renders. */
249
+ .why {
250
+ font: inherit;
251
+ font-size: 0.9rem;
252
+ padding: 0.35rem 0.15rem;
253
+ background: none;
254
+ color: var(--fg4);
255
+ border: none;
256
+ text-decoration: underline;
257
+ text-underline-offset: 2px;
258
+ cursor: pointer;
259
+ }
260
+ .why:hover,
261
+ .why:focus-visible {
262
+ color: var(--fg);
263
+ }
264
+ .whyCard {
265
+ width: min(34rem, calc(100vw - 2rem));
266
+ padding: 1.4rem 1.5rem;
267
+ background: var(--surface);
268
+ color: var(--fg);
269
+ border: 1px solid var(--border-faint);
270
+ border-radius: 0.6rem;
271
+ }
272
+ .whyCard::backdrop {
273
+ background: #000a;
274
+ }
275
+ .whyTop {
276
+ display: flex;
277
+ align-items: baseline;
278
+ justify-content: space-between;
279
+ gap: 1rem;
280
+ }
281
+ .whyHead {
282
+ margin: 0 0 0.9rem;
283
+ font-size: 1.4rem;
284
+ }
285
+ /* Why the last sign-in did not finish, above the pitch and the retry. */
286
+ .whyTrouble {
287
+ margin: 0 0 0.9rem;
288
+ padding: 0.6rem 0.8rem;
289
+ border: 1px solid var(--danger);
290
+ border-radius: 0.4rem;
291
+ color: var(--danger);
292
+ font-size: 0.95rem;
293
+ }
294
+ /* The answer, at the size of the thing being answered. */
295
+ .whyPitch {
296
+ margin: 0 0 0.7rem;
297
+ font-size: 1.05rem;
298
+ color: var(--fg);
299
+ }
300
+ .whyLine {
301
+ margin: 0 0 0.7rem;
302
+ color: var(--fg3);
303
+ font-size: 0.95rem;
304
+ }
305
+ .whyLine a {
306
+ color: var(--accent);
307
+ white-space: nowrap;
308
+ }
309
+ .whyCta {
310
+ display: flex;
311
+ flex-wrap: wrap;
312
+ gap: 0.6rem;
313
+ margin: 1.1rem 0 0.8rem;
314
+ }
315
+ /* ── the privacy page ──────────────────────────────────────────────────────
316
+ One column of prose under the site bar, at the width the rest of the site
317
+ reads at. */
318
+ .privacy {
319
+ padding-top: 2.5rem;
320
+ padding-bottom: 4rem;
321
+ }
322
+ .privacy h1 {
323
+ margin: 0 0 0.6rem;
324
+ font-size: 2rem;
325
+ }
326
+ .privacy h2 {
327
+ margin: 2rem 0 0.5rem;
328
+ font-size: 1.25rem;
329
+ }
330
+ .privacy p {
331
+ margin: 0;
332
+ color: var(--fg3);
333
+ line-height: 1.65;
334
+ }
335
+ .privacy p + p {
336
+ margin-top: 0.8rem;
337
+ }
338
+ .privacy a {
339
+ color: var(--accent);
340
+ }
341
+ .privacyLead {
342
+ font-size: 1.1rem;
343
+ }
344
+
345
+ /* ── the about page and the blog ───────────────────────────────────────── */
346
+ .about,
347
+ .blog {
348
+ padding-top: 2.5rem;
349
+ padding-bottom: 4rem;
350
+ }
351
+ .about h1,
352
+ .blog h1 {
353
+ margin: 0 0 0.6rem;
354
+ font-size: 2rem;
355
+ }
356
+ .about p,
357
+ .blog p,
358
+ .blog li {
359
+ color: var(--fg3);
360
+ }
361
+ .about p {
362
+ margin: 0 0 0.8rem;
363
+ }
364
+ .aboutLead,
365
+ .blogLead {
366
+ font-size: 1.1rem;
367
+ }
368
+ .aboutLinks {
369
+ margin: 1.5rem 0 0;
370
+ padding-left: 1.2rem;
371
+ }
372
+ .aboutLinks li {
373
+ margin-bottom: 0.4rem;
374
+ }
375
+ .postMeta {
376
+ margin: 0.2rem 0 0;
377
+ font-variant-numeric: tabular-nums;
378
+ font-size: 0.85rem;
379
+ color: var(--fg4);
380
+ }
381
+ .postCard {
382
+ display: grid;
383
+ grid-template-columns: 14rem minmax(0, 1fr);
384
+ gap: 1.2rem;
385
+ margin-top: 2rem;
386
+ padding-top: 1.5rem;
387
+ border-top: 1px solid var(--border-faint);
388
+ }
389
+ .postCard img,
390
+ .postHero,
391
+ .postBody img {
392
+ display: block;
393
+ width: 100%;
394
+ height: auto;
395
+ border-radius: 4px;
396
+ }
397
+ .postCard h2 {
398
+ margin: 0;
399
+ font-size: 1.25rem;
400
+ }
401
+ .postCard h2 a {
402
+ color: var(--fg);
403
+ text-decoration: none;
404
+ }
405
+ .postCard h2 a:hover {
406
+ text-decoration: underline;
407
+ }
408
+ .postCard p {
409
+ margin: 0.4rem 0 0;
410
+ }
411
+ @media (max-width: 560px) {
412
+ .postCard {
413
+ grid-template-columns: 1fr;
414
+ }
415
+ }
416
+ .postFull h1 {
417
+ margin: 0.4rem 0 1rem;
418
+ }
419
+ .postLook {
420
+ font-weight: 600;
421
+ }
422
+ .postBody {
423
+ line-height: 1.6;
424
+ }
425
+ .postBody h2 {
426
+ margin: 2rem 0 0.6rem;
427
+ font-size: 1.25rem;
428
+ }
429
+ .postBody p {
430
+ margin: 0 0 0.9rem;
431
+ }
432
+ .postBody .anchor {
433
+ margin-left: 0.45em;
434
+ color: var(--fg4);
435
+ text-decoration: none;
436
+ font-weight: 400;
437
+ opacity: 0;
438
+ }
439
+ .postBody h2:hover .anchor,
440
+ .postBody .anchor:focus-visible {
441
+ opacity: 1;
442
+ }
443
+ .postBody img {
444
+ margin: 0.4rem 0 1rem;
445
+ }
446
+
447
+ /* ── the signed-in home ────────────────────────────────────────────────────
448
+ A rail of links and one column of sections, built by site/scripts/home.ts
449
+ once Firebase has answered. The cards are the gallery's own anatomy, so a
450
+ saved look and a demo read as the same kind of thing. */
451
+ .homeIn {
452
+ display: grid;
453
+ grid-template-columns: 200px minmax(0, 1fr);
454
+ gap: 2rem;
455
+ max-width: 88rem;
456
+ margin: 0 auto;
457
+ padding: 2rem max(1.15rem, env(safe-area-inset-left)) 3.5rem;
458
+ }
459
+ .rail {
460
+ display: flex;
461
+ flex-direction: column;
462
+ gap: 0.15rem;
463
+ position: sticky;
464
+ top: 1rem;
465
+ align-self: start;
466
+ }
467
+ .rail a {
468
+ padding: 0.4rem 0.6rem;
469
+ border-radius: 0.4rem;
470
+ color: var(--fg3);
471
+ text-decoration: none;
472
+ font-size: 0.92rem;
473
+ white-space: nowrap;
474
+ }
475
+ .rail a:hover {
476
+ background: var(--surface);
477
+ color: var(--fg);
478
+ }
479
+ .rail a.on {
480
+ background: var(--surface);
481
+ color: var(--accent);
482
+ }
483
+ .homeMain {
484
+ display: flex;
485
+ flex-direction: column;
486
+ gap: 2.75rem;
487
+ }
488
+ /* `section`'s 3.5rem belongs to the landing page's full-bleed bands; the
489
+ column spaces itself. */
490
+ .homeSec {
491
+ padding: 0;
492
+ scroll-margin-top: 1rem;
493
+ }
494
+ .homeSec .head {
495
+ font-size: 1.15rem;
496
+ }
497
+ /* The rail becomes the row under the bar: 200px of links beside a single
498
+ column of cards is most of a phone's width. */
499
+ @media (max-width: 52rem) {
500
+ .homeIn {
501
+ grid-template-columns: minmax(0, 1fr);
502
+ gap: 1.35rem;
503
+ }
504
+ .rail {
505
+ position: static;
506
+ flex-direction: row;
507
+ gap: 0.35rem;
508
+ overflow-x: auto;
509
+ scrollbar-width: none;
510
+ }
511
+ }
512
+
513
+ .resumeCard {
514
+ display: grid;
515
+ grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
516
+ border: 1px solid var(--border-faint);
517
+ border-radius: 0.6rem;
518
+ background: var(--surface);
519
+ overflow: hidden;
520
+ }
521
+ .resumeCard .shot {
522
+ position: relative;
523
+ display: block;
524
+ aspect-ratio: 5 / 4;
525
+ background: #000;
526
+ }
527
+ .resumeCard .shot .still {
528
+ position: absolute;
529
+ inset: 0;
530
+ width: 100%;
531
+ height: 100%;
532
+ display: block;
533
+ object-fit: cover;
534
+ }
535
+ .resumeBody {
536
+ display: flex;
537
+ flex-direction: column;
538
+ justify-content: center;
539
+ gap: 0.45rem;
540
+ padding: 1.35rem;
541
+ }
542
+ .when {
543
+ margin: 0;
544
+ color: var(--fg4);
545
+ font-size: 0.85rem;
546
+ }
547
+ .resumeSays {
548
+ margin: 0;
549
+ color: var(--fg2);
550
+ font-size: 0.95rem;
551
+ text-wrap: pretty;
552
+ }
553
+ .resumeCta {
554
+ display: flex;
555
+ flex-wrap: wrap;
556
+ gap: 0.6rem;
557
+ margin: 0.7rem 0 0;
558
+ }
559
+ @media (max-width: 44rem) {
560
+ .resumeCard {
561
+ grid-template-columns: minmax(0, 1fr);
562
+ }
563
+ }
564
+
565
+ /* A look with no still: the app mark on the card's own black, so the row of
566
+ cards keeps its shape. */
567
+ .shot.blank {
568
+ display: grid;
569
+ place-items: center;
570
+ background: #0a0b0d;
571
+ }
572
+ .resumeCard .shot.blank .blankMark {
573
+ position: static;
574
+ width: 2.5rem;
575
+ height: 2.5rem;
576
+ object-fit: contain;
577
+ opacity: 0.55;
578
+ }
579
+
580
+ /* A saved card with its verbs under it. The <li> carries the card's border so
581
+ the link and the row of buttons read as one card, and the row sits at the
582
+ bottom however long the name above it runs. */
583
+ .grid li:has(> .cardActions) {
584
+ display: flex;
585
+ flex-direction: column;
586
+ overflow: hidden;
587
+ border: 1px solid var(--border-faint);
588
+ border-radius: 0.6rem;
589
+ background: var(--surface);
590
+ }
591
+ .grid li:has(> .cardActions) > a:first-child {
592
+ flex: 1;
593
+ border: 0;
594
+ border-radius: 0;
595
+ }
596
+ .grid li:has(> a:first-child:hover, > a:first-child:focus-visible) {
597
+ border-color: var(--accent);
598
+ }
599
+ .cardActions {
600
+ display: flex;
601
+ flex-wrap: wrap;
602
+ align-items: center;
603
+ gap: 0.3rem 0.8rem;
604
+ padding: 0.45rem 0.85rem 0.6rem;
605
+ border-top: 1px solid var(--border-faint);
606
+ font-size: 0.8rem;
607
+ }
608
+ .cardAct {
609
+ padding: 0.15rem 0;
610
+ border: 0;
611
+ background: none;
612
+ color: var(--fg4);
613
+ font: inherit;
614
+ text-decoration: underline;
615
+ text-underline-offset: 2px;
616
+ cursor: pointer;
617
+ }
618
+ .cardAct:hover,
619
+ .cardAct:focus-visible {
620
+ color: var(--fg);
621
+ }
622
+ .cardAct:disabled {
623
+ opacity: 0.5;
624
+ cursor: default;
625
+ }
626
+ .cardAsk {
627
+ flex-basis: 100%;
628
+ color: var(--fg2);
629
+ overflow-wrap: anywhere;
630
+ }
631
+ .cardRename {
632
+ flex: 1 1 100%;
633
+ min-width: 0;
634
+ padding: 0.3rem 0.45rem;
635
+ border: 1px solid var(--border-faint);
636
+ border-radius: 0.35rem;
637
+ background: var(--bg);
638
+ color: var(--fg);
639
+ font: inherit;
640
+ }
641
+ .cardStatus {
642
+ flex-basis: 100%;
643
+ color: var(--fg3);
644
+ }
645
+ .cardStatus:empty {
646
+ display: none;
647
+ }
648
+
649
+ .empty {
650
+ padding: 2.25rem 1.5rem;
651
+ border: 1px dashed var(--border-faint);
652
+ border-radius: 0.6rem;
653
+ text-align: center;
654
+ }
655
+ .emptyHead {
656
+ margin: 0 0 0.5rem;
657
+ font-size: 1.05rem;
658
+ font-weight: 600;
659
+ }
660
+ .emptySays {
661
+ max-width: 34rem;
662
+ margin: 0 auto 1.15rem;
663
+ color: var(--fg4);
664
+ font-size: 0.92rem;
665
+ text-wrap: pretty;
666
+ }
667
+
668
+ /* ── waiting for Firebase ──────────────────────────────────────────────────
669
+ SignedInHint.astro's head script sets `data-home="pending"` on a browser that
670
+ has signed in before. The landing page and the sign-in controls stay out of
671
+ sight and the home's outline stands in until home.ts paints one state. */
672
+ .homeWait {
673
+ display: none;
674
+ }
675
+ html[data-home='pending'] .homeWait {
676
+ display: block;
677
+ min-height: 100vh;
678
+ }
679
+ html[data-home='pending'] #landing,
680
+ html[data-home='pending'] #signIn,
681
+ html[data-home='pending'] #why {
682
+ display: none;
683
+ }
684
+ .vh {
685
+ position: absolute;
686
+ width: 1px;
687
+ height: 1px;
688
+ overflow: hidden;
689
+ clip-path: inset(50%);
690
+ white-space: nowrap;
691
+ }
692
+ .bone {
693
+ display: block;
694
+ animation: bone 1.4s ease-in-out infinite;
695
+ }
696
+ @keyframes bone {
697
+ 50% {
698
+ opacity: 0.5;
699
+ }
700
+ }
701
+ @media (prefers-reduced-motion: reduce) {
702
+ .bone {
703
+ animation: none;
704
+ }
705
+ }
706
+ .bone {
707
+ border-radius: 0.4rem;
708
+ background: var(--surface);
709
+ }
710
+ .railBone {
711
+ width: 7rem;
712
+ height: 1.9rem;
713
+ }
714
+ .headBone {
715
+ width: 14rem;
716
+ height: 1.4rem;
717
+ }
@@ -0,0 +1 @@
1
+ export declare function startAnalytics(): void;
@@ -0,0 +1,16 @@
1
+ // A hit counter on every page the site serves. GoatCounter counts page views
2
+ // and sets no cookies, so no page asks first; /privacy/ says what it collects.
3
+ const COUNTER = 'https://cmdcolin.goatcounter.com/count';
4
+ // A browser a script is driving counts nothing: the harnesses that screenshot
5
+ // and record these pages are nobody's visit.
6
+ export function startAnalytics() {
7
+ if (navigator.webdriver)
8
+ return;
9
+ if (document.querySelector('script[data-goatcounter]'))
10
+ return;
11
+ const tag = document.createElement('script');
12
+ tag.async = true;
13
+ tag.src = 'https://gc.zgo.at/count.js';
14
+ tag.dataset.goatcounter = COUNTER;
15
+ document.head.append(tag);
16
+ }
package/dist/dom.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const need: (id: string) => HTMLElement;
package/dist/dom.js ADDED
@@ -0,0 +1,6 @@
1
+ export const need = (id) => {
2
+ const node = document.getElementById(id);
3
+ if (node === null)
4
+ throw new Error(`no #${id}`);
5
+ return node;
6
+ };
@@ -0,0 +1,9 @@
1
+ export type NavLink = readonly [href: string, label: string];
2
+ export interface SiteInfo {
3
+ name: string;
4
+ home: string;
5
+ icon: string;
6
+ app: string;
7
+ nav: readonly NavLink[];
8
+ footer: readonly NavLink[];
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ export type Point = readonly [number, number];
2
+ /**
3
+ * A run through the given corners, each rounded off by `radius`.
4
+ *
5
+ * The radius is capped at half of the shorter leg either side of a corner, so a
6
+ * short leg between two turns rounds to what fits instead of overshooting into
7
+ * the next one. Two points are a straight line and no corner at all.
8
+ */
9
+ export declare function route(pts: readonly Point[], radius: number): string;
10
+ /**
11
+ * The head on the end of a run: a triangle whose tip is the last point and
12
+ * whose base sits `len` back along the last leg, `half` to either side of it.
13
+ *
14
+ * Off the same list the wire is routed from, so the head cannot point somewhere
15
+ * the wire does not go. The two numbers are the drawing's, not this file's —
16
+ * the miniature and the card set their type at different sizes and their heads
17
+ * with it.
18
+ */
19
+ export declare function arrowhead(pts: readonly Point[], len: number, half: number): string;
@@ -0,0 +1,59 @@
1
+ const round = (n) => Math.round(n * 1000) / 1000;
2
+ const at = (p) => `${round(p[0])} ${round(p[1])}`;
3
+ const dist = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
4
+ // `by` units from `from` along the line to `to`. Clamped by the caller, not
5
+ // here: a zero-length leg has no direction to travel, and the corner it belongs
6
+ // to is the one place that knows what to do about it.
7
+ const toward = (from, to, by) => {
8
+ const d = dist(from, to);
9
+ if (d === 0)
10
+ return from;
11
+ return [
12
+ from[0] + ((to[0] - from[0]) * by) / d,
13
+ from[1] + ((to[1] - from[1]) * by) / d,
14
+ ];
15
+ };
16
+ /**
17
+ * A run through the given corners, each rounded off by `radius`.
18
+ *
19
+ * The radius is capped at half of the shorter leg either side of a corner, so a
20
+ * short leg between two turns rounds to what fits instead of overshooting into
21
+ * the next one. Two points are a straight line and no corner at all.
22
+ */
23
+ export function route(pts, radius) {
24
+ if (pts.length === 0)
25
+ return '';
26
+ const d = [`M${at(pts[0])}`];
27
+ for (let i = 1; i < pts.length - 1; i++) {
28
+ const prev = pts[i - 1];
29
+ const corner = pts[i];
30
+ const next = pts[i + 1];
31
+ const r = Math.min(radius, dist(prev, corner) / 2, dist(corner, next) / 2);
32
+ d.push(`L${at(toward(corner, prev, r))}`);
33
+ d.push(`Q${at(corner)} ${at(toward(corner, next, r))}`);
34
+ }
35
+ d.push(`L${at(pts[pts.length - 1])}`);
36
+ return d.join('');
37
+ }
38
+ /**
39
+ * The head on the end of a run: a triangle whose tip is the last point and
40
+ * whose base sits `len` back along the last leg, `half` to either side of it.
41
+ *
42
+ * Off the same list the wire is routed from, so the head cannot point somewhere
43
+ * the wire does not go. The two numbers are the drawing's, not this file's —
44
+ * the miniature and the card set their type at different sizes and their heads
45
+ * with it.
46
+ */
47
+ export function arrowhead(pts, len, half) {
48
+ if (pts.length < 2)
49
+ return '';
50
+ const tip = pts[pts.length - 1];
51
+ const back = toward(tip, pts[pts.length - 2], len);
52
+ // The perpendicular to the last leg, at unit length times `half`.
53
+ const [dx, dy] = [tip[0] - back[0], tip[1] - back[1]];
54
+ const n = Math.hypot(dx, dy);
55
+ if (n === 0)
56
+ return '';
57
+ const [px, py] = [(-dy / n) * half, (dx / n) * half];
58
+ return `M${at([back[0] + px, back[1] + py])}L${at(tip)}L${at([back[0] - px, back[1] - py])}Z`;
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillet-shared",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Code shared by bender and videoskillet",
5
5
  "license": "MIT",
6
6
  "author": "Colin Diesh",
@@ -19,10 +19,34 @@
19
19
  "types": "./dist/posts.d.ts",
20
20
  "default": "./dist/posts.js"
21
21
  },
22
- "./tokens.css": "./dist/tokens.css"
22
+ "./analytics": {
23
+ "types": "./dist/analytics.d.ts",
24
+ "default": "./dist/analytics.js"
25
+ },
26
+ "./dom": {
27
+ "types": "./dist/dom.d.ts",
28
+ "default": "./dist/dom.js"
29
+ },
30
+ "./wire-route": {
31
+ "types": "./dist/wire-route.d.ts",
32
+ "default": "./dist/wire-route.js"
33
+ },
34
+ "./tokens.css": "./dist/tokens.css",
35
+ "./site-info": {
36
+ "types": "./dist/site-info.d.ts",
37
+ "default": "./dist/site-info.js"
38
+ },
39
+ "./components/SiteBar.astro": "./components/SiteBar.astro",
40
+ "./components/Footer.astro": "./components/Footer.astro",
41
+ "./components/SocialMeta.astro": "./components/SocialMeta.astro",
42
+ "./components/SignedInHint.astro": "./components/SignedInHint.astro",
43
+ "./site.css": "./css/site.css",
44
+ "./footer.css": "./css/footer.css"
23
45
  },
24
46
  "files": [
25
- "dist"
47
+ "dist",
48
+ "components",
49
+ "css"
26
50
  ],
27
51
  "publishConfig": {
28
52
  "access": "public"