@rxova/astro-ui 0.0.0 → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/package.json +65 -6
- package/src/components/BackLink.astro +24 -0
- package/src/components/CodeRecipes.astro +67 -0
- package/src/components/CtaBand.astro +121 -0
- package/src/components/DataTable.astro +114 -0
- package/src/components/DocAccordion.astro +15 -0
- package/src/components/DocAccordionItem.astro +83 -0
- package/src/components/Header.astro +148 -0
- package/src/components/Icon.astro +29 -0
- package/src/components/ModeTabs.astro +124 -0
- package/src/components/PageHeader.astro +63 -0
- package/src/components/ProjectSwitcher.astro +95 -0
- package/src/components/ProofStats.astro +207 -0
- package/src/components/QuickStart.astro +156 -0
- package/src/components/ScreenshotGrid.astro +164 -0
- package/src/components/Section.astro +103 -0
- package/src/components/ShowMore.astro +82 -0
- package/src/components/SiteFooter.astro +149 -0
- package/src/components/SiteShell.astro +155 -0
- package/src/components/SizeTable.astro +118 -0
- package/src/components/ThemeScript.astro +31 -0
- package/src/components/ThemeToggle.astro +163 -0
- package/src/components/ValueGrid.astro +116 -0
- package/src/components/VisuallyHidden.astro +23 -0
- package/src/components/icons/Collapse.astro +11 -0
- package/src/components/icons/Expand.astro +11 -0
- package/src/components/icons/Next.astro +11 -0
- package/src/components/icons/Pause.astro +11 -0
- package/src/components/icons/Play.astro +11 -0
- package/src/components/icons/Prev.astro +11 -0
- package/src/components/icons/Replay.astro +11 -0
- package/src/env.d.ts +15 -0
- package/src/lib/entries.ts +72 -0
- package/src/lib/icons.ts +15 -0
- package/src/scripts/show-more.ts +46 -0
- package/src/starlight/Footer.astro +16 -0
- package/src/starlight/SiteTitle.astro +34 -0
- package/src/starlight/SocialIcons.astro +18 -0
- package/src/starlight/ThemeSelect.astro +42 -0
- package/src/starlight/index.ts +103 -0
- package/src/styles/chrome.css +8 -0
- package/src/styles/document.css +72 -0
- package/src/styles/footer.css +124 -0
- package/src/styles/landing.css +55 -0
- package/src/styles/mermaid.css +75 -0
- package/src/styles/starlight.css +162 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Artifact document for rxova.org's non-Starlight pages: metadata and assets only, no header,
|
|
4
|
+
* footer, analytics or theme bootstrap. rxova-website supplies the public shell at deploy time.
|
|
5
|
+
*/
|
|
6
|
+
import '@rxova/brand/fonts.css'
|
|
7
|
+
import '../styles/document.css'
|
|
8
|
+
|
|
9
|
+
import { canonicalUrl, siteUrl } from '@rxova/brand'
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
title: string
|
|
13
|
+
description: string
|
|
14
|
+
/** Canonical path, e.g. `/blog/why-rxova-has-a-blog`. Normalised below. */
|
|
15
|
+
path: string
|
|
16
|
+
ogType?: 'website' | 'article'
|
|
17
|
+
/** Absolute URL of the social card, chosen by the caller; defaults to the umbrella card. */
|
|
18
|
+
image?: string
|
|
19
|
+
/** Advertises an RSS feed for this surface, as `rel="alternate"`. */
|
|
20
|
+
feed?: { href: string; title: string }
|
|
21
|
+
/** JSON-LD for this page, emitted verbatim as `application/ld+json`. */
|
|
22
|
+
jsonLd?: unknown
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { title, description, path, ogType = 'website', image, feed, jsonLd } = Astro.props
|
|
26
|
+
|
|
27
|
+
// Normalised here so no caller can declare a canonical URL without the trailing slash (a 301).
|
|
28
|
+
const canonical = canonicalUrl(path)
|
|
29
|
+
|
|
30
|
+
const ogImage = image ?? siteUrl('/og/rxova.png')
|
|
31
|
+
|
|
32
|
+
/** Serialise JSON-LD for an inline `<script>`; `<` is escaped so a `</script>` cannot close it. */
|
|
33
|
+
const serialiseJsonLd = (value: unknown): string => JSON.stringify(value).replace(/</g, '\\u003c')
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<!doctype html>
|
|
37
|
+
<html lang="en">
|
|
38
|
+
<head>
|
|
39
|
+
<meta charset="utf-8" />
|
|
40
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
41
|
+
<link rel="canonical" href={canonical} />
|
|
42
|
+
<title>{title}</title>
|
|
43
|
+
<meta name="description" content={description} />
|
|
44
|
+
<meta property="og:title" content={title} />
|
|
45
|
+
<meta property="og:description" content={description} />
|
|
46
|
+
<meta property="og:type" content={ogType} />
|
|
47
|
+
<meta property="og:url" content={canonical} />
|
|
48
|
+
<meta property="og:image" content={ogImage} />
|
|
49
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
50
|
+
{feed && (
|
|
51
|
+
<link rel="alternate" type="application/rss+xml" title={feed.title} href={feed.href} />
|
|
52
|
+
)}
|
|
53
|
+
{jsonLd && (
|
|
54
|
+
<script type="application/ld+json" is:inline set:html={serialiseJsonLd(jsonLd)}></script>
|
|
55
|
+
)}
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
<main>
|
|
59
|
+
<slot />
|
|
60
|
+
</main>
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
main {
|
|
64
|
+
max-width: var(--rx-max-wide);
|
|
65
|
+
margin: 0 auto;
|
|
66
|
+
padding: clamp(2rem, 6vh, 3.5rem) 1.5rem 4rem;
|
|
67
|
+
}
|
|
68
|
+
</style>
|
|
69
|
+
|
|
70
|
+
<!-- Prose styling for rendered markdown. Global because <Content /> markup carries no Astro
|
|
71
|
+
scoping attributes. -->
|
|
72
|
+
<style is:global>
|
|
73
|
+
.prose {
|
|
74
|
+
color: var(--rx-muted);
|
|
75
|
+
line-height: 1.7;
|
|
76
|
+
}
|
|
77
|
+
.prose > * + * {
|
|
78
|
+
margin-top: 1.1rem;
|
|
79
|
+
}
|
|
80
|
+
.prose h2 {
|
|
81
|
+
color: var(--rx-fg);
|
|
82
|
+
font-size: 1.25rem;
|
|
83
|
+
font-weight: 640;
|
|
84
|
+
letter-spacing: -0.01em;
|
|
85
|
+
margin-top: 2.5rem;
|
|
86
|
+
}
|
|
87
|
+
.prose h3 {
|
|
88
|
+
color: var(--rx-fg);
|
|
89
|
+
font-size: 1.05rem;
|
|
90
|
+
font-weight: 620;
|
|
91
|
+
margin-top: 2rem;
|
|
92
|
+
}
|
|
93
|
+
.prose a {
|
|
94
|
+
color: var(--rx-fg);
|
|
95
|
+
text-decoration: underline;
|
|
96
|
+
text-underline-offset: 2px;
|
|
97
|
+
}
|
|
98
|
+
.prose strong {
|
|
99
|
+
color: var(--rx-fg);
|
|
100
|
+
font-weight: 620;
|
|
101
|
+
}
|
|
102
|
+
.prose ul,
|
|
103
|
+
.prose ol {
|
|
104
|
+
margin-left: 1.2rem;
|
|
105
|
+
}
|
|
106
|
+
.prose li + li {
|
|
107
|
+
margin-top: 0.35rem;
|
|
108
|
+
}
|
|
109
|
+
.prose code {
|
|
110
|
+
font-family: var(--rx-font-mono);
|
|
111
|
+
font-size: 0.88em;
|
|
112
|
+
background: var(--rx-tag-bg);
|
|
113
|
+
padding: 0.05rem 0.35rem;
|
|
114
|
+
border-radius: 4px;
|
|
115
|
+
}
|
|
116
|
+
.prose pre {
|
|
117
|
+
background: var(--rx-card);
|
|
118
|
+
border: 1px solid var(--rx-rule);
|
|
119
|
+
border-radius: 10px;
|
|
120
|
+
padding: 1rem 1.1rem;
|
|
121
|
+
/* Long lines scroll inside the block; the page itself never does. */
|
|
122
|
+
overflow-x: auto;
|
|
123
|
+
}
|
|
124
|
+
.prose pre code {
|
|
125
|
+
background: none;
|
|
126
|
+
padding: 0;
|
|
127
|
+
}
|
|
128
|
+
.prose blockquote {
|
|
129
|
+
border-left: 2px solid var(--rx-rule);
|
|
130
|
+
padding-left: 1rem;
|
|
131
|
+
color: var(--rx-faint);
|
|
132
|
+
}
|
|
133
|
+
.prose img {
|
|
134
|
+
max-width: 100%;
|
|
135
|
+
height: auto;
|
|
136
|
+
border-radius: 10px;
|
|
137
|
+
}
|
|
138
|
+
.prose table {
|
|
139
|
+
width: 100%;
|
|
140
|
+
border-collapse: collapse;
|
|
141
|
+
font-size: 0.95rem;
|
|
142
|
+
}
|
|
143
|
+
.prose th,
|
|
144
|
+
.prose td {
|
|
145
|
+
border-bottom: 1px solid var(--rx-rule);
|
|
146
|
+
padding: 0.5rem 0.6rem;
|
|
147
|
+
text-align: left;
|
|
148
|
+
}
|
|
149
|
+
.prose th {
|
|
150
|
+
color: var(--rx-fg);
|
|
151
|
+
font-weight: 620;
|
|
152
|
+
}
|
|
153
|
+
</style>
|
|
154
|
+
</body>
|
|
155
|
+
</html>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Per-package bundle budgets as a real table: right-aligned tabular figures are the shape that
|
|
4
|
+
* supports comparing against whatever the reader ships today.
|
|
5
|
+
*/
|
|
6
|
+
export interface SizeRow {
|
|
7
|
+
pkg: string
|
|
8
|
+
entry: string
|
|
9
|
+
limitKb: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
rows: readonly SizeRow[]
|
|
14
|
+
/** The column heading: "brotli", "gzip". */
|
|
15
|
+
compression: string
|
|
16
|
+
caption: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { rows, compression, caption } = Astro.props
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
{/* Focusable and labelled: the box scrolls sideways on a narrow viewport, and a scrollable region needs a tab stop. */}
|
|
23
|
+
<div class="rx-sizes" tabindex="0" role="region" aria-label="Bundle size budgets">
|
|
24
|
+
<table class="rx-sizes__table">
|
|
25
|
+
<caption class="rx-sizes__caption">{caption}</caption>
|
|
26
|
+
<thead>
|
|
27
|
+
<tr>
|
|
28
|
+
<th scope="col">Package</th>
|
|
29
|
+
<th scope="col">You import</th>
|
|
30
|
+
<th scope="col" class="rx-sizes__num">
|
|
31
|
+
{compression}
|
|
32
|
+
</th>
|
|
33
|
+
</tr>
|
|
34
|
+
</thead>
|
|
35
|
+
<tbody>
|
|
36
|
+
{rows.map((row) => (
|
|
37
|
+
<tr>
|
|
38
|
+
<th scope="row" class="rx-sizes__pkg">
|
|
39
|
+
{row.pkg}
|
|
40
|
+
</th>
|
|
41
|
+
<td>
|
|
42
|
+
<code>{row.entry}</code>
|
|
43
|
+
</td>
|
|
44
|
+
<td class="rx-sizes__num">≤ {row.limitKb} kB</td>
|
|
45
|
+
</tr>
|
|
46
|
+
))}
|
|
47
|
+
</tbody>
|
|
48
|
+
</table>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<style>
|
|
52
|
+
.rx-sizes {
|
|
53
|
+
overflow-x: auto;
|
|
54
|
+
border: 1px solid var(--rx-rule);
|
|
55
|
+
border-radius: var(--rx-radius);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.rx-sizes__table {
|
|
59
|
+
width: 100%;
|
|
60
|
+
border-collapse: collapse;
|
|
61
|
+
font-size: 0.9rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.rx-sizes__caption {
|
|
65
|
+
padding: 1rem 1.25rem;
|
|
66
|
+
/* --rx-muted, not --rx-faint: faint on the card is ~3.4:1 and fails AA here. */
|
|
67
|
+
color: var(--rx-muted);
|
|
68
|
+
font-size: 0.82rem;
|
|
69
|
+
line-height: 1.5;
|
|
70
|
+
text-align: start;
|
|
71
|
+
caption-side: bottom;
|
|
72
|
+
border-top: 1px solid var(--rx-rule);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.rx-sizes__table th,
|
|
76
|
+
.rx-sizes__table td {
|
|
77
|
+
padding: 0.85rem 1.25rem;
|
|
78
|
+
text-align: start;
|
|
79
|
+
border-bottom: 1px solid var(--rx-rule);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.rx-sizes__table thead th {
|
|
83
|
+
background: var(--rx-card);
|
|
84
|
+
color: var(--rx-muted);
|
|
85
|
+
font-size: 0.75rem;
|
|
86
|
+
font-weight: 700;
|
|
87
|
+
letter-spacing: 0.06em;
|
|
88
|
+
text-transform: uppercase;
|
|
89
|
+
white-space: nowrap;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.rx-sizes__table tbody tr:last-child th,
|
|
93
|
+
.rx-sizes__table tbody tr:last-child td {
|
|
94
|
+
border-bottom: 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.rx-sizes__pkg {
|
|
98
|
+
color: var(--rx-fg);
|
|
99
|
+
font-family: var(--rx-font-mono);
|
|
100
|
+
font-size: 0.84rem;
|
|
101
|
+
font-weight: 500;
|
|
102
|
+
white-space: nowrap;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.rx-sizes__table code {
|
|
106
|
+
color: var(--rx-muted);
|
|
107
|
+
font-family: var(--rx-font-mono);
|
|
108
|
+
font-size: 0.84rem;
|
|
109
|
+
white-space: nowrap;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.rx-sizes__num {
|
|
113
|
+
text-align: end;
|
|
114
|
+
color: var(--rx-fg);
|
|
115
|
+
font-variant-numeric: tabular-nums;
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The pre-paint theme script for non-Starlight surfaces; goes in `<head>`. It reads Starlight's
|
|
4
|
+
* `starlight-theme` key so the choice carries across the origin; no saved choice => CSS default.
|
|
5
|
+
*/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<script is:inline>
|
|
9
|
+
;(function () {
|
|
10
|
+
function stored() {
|
|
11
|
+
try {
|
|
12
|
+
const t = localStorage.getItem('starlight-theme')
|
|
13
|
+
return t === 'light' || t === 'dark' ? t : null
|
|
14
|
+
} catch {
|
|
15
|
+
/* localStorage may be unavailable */
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function sync() {
|
|
20
|
+
const t = stored()
|
|
21
|
+
if (t) document.documentElement.setAttribute('data-theme', t)
|
|
22
|
+
else document.documentElement.removeAttribute('data-theme')
|
|
23
|
+
}
|
|
24
|
+
sync()
|
|
25
|
+
// Back from a docs site restores this page from the bfcache without rerunning the script;
|
|
26
|
+
// `pageshow` is the only notification, so re-sync the theme there.
|
|
27
|
+
window.addEventListener('pageshow', function (e) {
|
|
28
|
+
if (e.persisted) sync()
|
|
29
|
+
})
|
|
30
|
+
})()
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The light/dark toggle for non-Starlight surfaces. Pairs with ThemeScript.astro in <head>, which
|
|
4
|
+
* avoids the flash; this only handles clicks and keeps the icon honest.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Pin the button to the viewport's top-right corner; `false` when it sits in a header. */
|
|
9
|
+
floating?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { floating = true } = Astro.props
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<button
|
|
16
|
+
id="theme-toggle"
|
|
17
|
+
class:list={['theme-toggle', { 'theme-toggle--floating': floating }]}
|
|
18
|
+
type="button"
|
|
19
|
+
aria-label="Toggle theme"
|
|
20
|
+
>
|
|
21
|
+
<span class="icon" aria-hidden="true">
|
|
22
|
+
<svg
|
|
23
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
24
|
+
viewBox="0 0 24 24"
|
|
25
|
+
width="24"
|
|
26
|
+
height="24"
|
|
27
|
+
fill="none"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
stroke-width="2"
|
|
30
|
+
stroke-linecap="round"
|
|
31
|
+
stroke-linejoin="round"
|
|
32
|
+
>
|
|
33
|
+
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
|
|
34
|
+
</svg>
|
|
35
|
+
</span>
|
|
36
|
+
</button>
|
|
37
|
+
|
|
38
|
+
<style>
|
|
39
|
+
/* Quiet by default: no border, no fill, the same muted ink as the nav links beside it. */
|
|
40
|
+
.theme-toggle {
|
|
41
|
+
flex: none;
|
|
42
|
+
width: 2rem;
|
|
43
|
+
height: 2rem;
|
|
44
|
+
display: grid;
|
|
45
|
+
place-items: center;
|
|
46
|
+
border: 1px solid transparent;
|
|
47
|
+
border-radius: 8px;
|
|
48
|
+
background: none;
|
|
49
|
+
color: var(--rx-muted);
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
transition:
|
|
52
|
+
color 0.15s ease,
|
|
53
|
+
border-color 0.15s ease,
|
|
54
|
+
background-color 0.2s ease;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Floating over arbitrary content, it keeps the border and fill so it still reads as a button. */
|
|
58
|
+
.theme-toggle--floating {
|
|
59
|
+
position: fixed;
|
|
60
|
+
top: 1rem;
|
|
61
|
+
right: 1rem;
|
|
62
|
+
/* Above SiteShell's sticky header (z-index 10), so the fixed button stays reachable. */
|
|
63
|
+
z-index: 20;
|
|
64
|
+
width: 2.25rem;
|
|
65
|
+
height: 2.25rem;
|
|
66
|
+
border-color: var(--rx-rule);
|
|
67
|
+
background: var(--rx-card);
|
|
68
|
+
color: var(--rx-fg);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.theme-toggle .icon {
|
|
72
|
+
display: grid;
|
|
73
|
+
place-items: center;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* `:global` because the script swaps the icon via innerHTML, which carries no Astro scoping
|
|
77
|
+
attributes. */
|
|
78
|
+
.theme-toggle :global(svg) {
|
|
79
|
+
width: 1.05rem;
|
|
80
|
+
height: 1.05rem;
|
|
81
|
+
display: block;
|
|
82
|
+
stroke-width: 1.6;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.theme-toggle:hover {
|
|
86
|
+
color: var(--rx-fg);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.theme-toggle--floating:hover {
|
|
90
|
+
border-color: var(--rx-faint);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.theme-toggle:focus-visible {
|
|
94
|
+
outline: 2px solid var(--rx-fg);
|
|
95
|
+
outline-offset: 2px;
|
|
96
|
+
}
|
|
97
|
+
</style>
|
|
98
|
+
|
|
99
|
+
<script is:inline>
|
|
100
|
+
;(function () {
|
|
101
|
+
const root = document.documentElement
|
|
102
|
+
const btn = document.getElementById('theme-toggle')
|
|
103
|
+
const glyph = btn.querySelector('.icon')
|
|
104
|
+
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
|
105
|
+
|
|
106
|
+
// Lucide icons (ISC licensed).
|
|
107
|
+
const ICON_MOON =
|
|
108
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>'
|
|
109
|
+
const ICON_SUN =
|
|
110
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>'
|
|
111
|
+
|
|
112
|
+
function current() {
|
|
113
|
+
return root.getAttribute('data-theme') || (media.matches ? 'dark' : 'light')
|
|
114
|
+
}
|
|
115
|
+
function stored() {
|
|
116
|
+
try {
|
|
117
|
+
const t = localStorage.getItem('starlight-theme')
|
|
118
|
+
return t === 'light' || t === 'dark' ? t : null
|
|
119
|
+
} catch {
|
|
120
|
+
/* localStorage may be unavailable */
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Re-read the saved choice and reflect it in the DOM. Same job as the
|
|
125
|
+
// pre-paint script in <head>, minus the write.
|
|
126
|
+
function sync() {
|
|
127
|
+
const t = stored()
|
|
128
|
+
if (t) root.setAttribute('data-theme', t)
|
|
129
|
+
else root.removeAttribute('data-theme')
|
|
130
|
+
render()
|
|
131
|
+
}
|
|
132
|
+
function render() {
|
|
133
|
+
const dark = current() === 'dark'
|
|
134
|
+
// Show the icon of the theme you'd switch TO.
|
|
135
|
+
glyph.innerHTML = dark ? ICON_SUN : ICON_MOON
|
|
136
|
+
btn.setAttribute('aria-label', dark ? 'Switch to light theme' : 'Switch to dark theme')
|
|
137
|
+
}
|
|
138
|
+
function apply(t) {
|
|
139
|
+
root.setAttribute('data-theme', t)
|
|
140
|
+
try {
|
|
141
|
+
// Starlight's key — see the pre-paint script in <head>.
|
|
142
|
+
localStorage.setItem('starlight-theme', t)
|
|
143
|
+
} catch {
|
|
144
|
+
/* localStorage may be unavailable */
|
|
145
|
+
}
|
|
146
|
+
render()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
btn.addEventListener('click', function () {
|
|
150
|
+
apply(current() === 'dark' ? 'light' : 'dark')
|
|
151
|
+
})
|
|
152
|
+
// Track the OS preference while the user hasn't overridden it.
|
|
153
|
+
media.addEventListener('change', function () {
|
|
154
|
+
if (!root.getAttribute('data-theme')) render()
|
|
155
|
+
})
|
|
156
|
+
// Back from a docs site restores this page from the bfcache without rerunning any script;
|
|
157
|
+
// `pageshow` is the only notification, so re-sync the theme there.
|
|
158
|
+
window.addEventListener('pageshow', function (e) {
|
|
159
|
+
if (e.persisted) sync()
|
|
160
|
+
})
|
|
161
|
+
render()
|
|
162
|
+
})()
|
|
163
|
+
</script>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A grid of claims in the landing's language: a gradient hairline over each, a faint-ink Lucide
|
|
4
|
+
* icon beside the title, the body hanging under it. `icon` is a `lucide-static` name, inlined at build time.
|
|
5
|
+
*/
|
|
6
|
+
import { lucideGlyph } from '../lib/icons.ts'
|
|
7
|
+
|
|
8
|
+
export interface ValueItem {
|
|
9
|
+
title: string
|
|
10
|
+
/** Set as HTML, so it may carry inline `<code>`. */
|
|
11
|
+
body: string
|
|
12
|
+
icon: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
items: readonly ValueItem[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { items } = Astro.props
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<ul class="rx-vg">
|
|
23
|
+
{items.map((item) => (
|
|
24
|
+
<li class="rx-vg__item">
|
|
25
|
+
<svg
|
|
26
|
+
class="rx-vg__icon"
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
fill="none"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
stroke-width="1.5"
|
|
31
|
+
stroke-linecap="round"
|
|
32
|
+
stroke-linejoin="round"
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
set:html={lucideGlyph(item.icon)}
|
|
35
|
+
/>
|
|
36
|
+
<h3 class="rx-vg__title">{item.title}</h3>
|
|
37
|
+
<p class="rx-vg__body" set:html={item.body} />
|
|
38
|
+
</li>
|
|
39
|
+
))}
|
|
40
|
+
</ul>
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
.rx-vg {
|
|
44
|
+
display: grid;
|
|
45
|
+
gap: 2rem 2.5rem;
|
|
46
|
+
margin: 0;
|
|
47
|
+
padding: 0;
|
|
48
|
+
list-style: none;
|
|
49
|
+
grid-template-columns: 1fr;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Explicit counts: auto-fit lands on four at the wide measure and leaves a half-empty last row. */
|
|
53
|
+
@media (min-width: 38rem) {
|
|
54
|
+
.rx-vg {
|
|
55
|
+
grid-template-columns: repeat(2, 1fr);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (min-width: 60rem) {
|
|
60
|
+
.rx-vg {
|
|
61
|
+
grid-template-columns: repeat(3, 1fr);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Icon beside the title, body under the title; 2px of the gradient over each binds them into one set. */
|
|
66
|
+
.rx-vg__item {
|
|
67
|
+
display: grid;
|
|
68
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
69
|
+
column-gap: 0.7rem;
|
|
70
|
+
row-gap: 0.4rem;
|
|
71
|
+
align-content: start;
|
|
72
|
+
padding-top: 1rem;
|
|
73
|
+
border-top: 2px solid transparent;
|
|
74
|
+
border-image: var(--rx-gradient) 1;
|
|
75
|
+
border-image-width: 2px 0 0 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.rx-vg__icon {
|
|
79
|
+
grid-column: 1;
|
|
80
|
+
grid-row: 1;
|
|
81
|
+
width: 1.375rem;
|
|
82
|
+
height: 1.375rem;
|
|
83
|
+
align-self: center;
|
|
84
|
+
color: var(--rx-faint);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.rx-vg__title {
|
|
88
|
+
grid-column: 2;
|
|
89
|
+
grid-row: 1;
|
|
90
|
+
margin: 0;
|
|
91
|
+
color: var(--rx-fg);
|
|
92
|
+
font-size: 1.05rem;
|
|
93
|
+
font-weight: 620;
|
|
94
|
+
line-height: 1.3;
|
|
95
|
+
letter-spacing: -0.015em;
|
|
96
|
+
text-wrap: balance;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.rx-vg__body {
|
|
100
|
+
grid-column: 2;
|
|
101
|
+
grid-row: 2;
|
|
102
|
+
margin: 0;
|
|
103
|
+
color: var(--rx-muted);
|
|
104
|
+
font-size: 0.95rem;
|
|
105
|
+
line-height: 1.55;
|
|
106
|
+
text-wrap: pretty;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.rx-vg__body :global(code) {
|
|
110
|
+
padding: 0.1em 0.35em;
|
|
111
|
+
background: var(--rx-tag-bg);
|
|
112
|
+
border-radius: var(--rx-radius-sm);
|
|
113
|
+
font-family: var(--rx-font-mono);
|
|
114
|
+
font-size: 0.86em;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Text only screen readers get; unlike `display: none`, it stays in the accessibility tree. */
|
|
3
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
4
|
+
|
|
5
|
+
type Props = HTMLAttributes<'span'>
|
|
6
|
+
|
|
7
|
+
const { class: className, ...rest } = Astro.props
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<span class:list={['visually-hidden', className]} {...rest}>
|
|
11
|
+
<slot />
|
|
12
|
+
</span>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
.visually-hidden {
|
|
16
|
+
position: absolute;
|
|
17
|
+
width: 1px;
|
|
18
|
+
height: 1px;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
clip-path: inset(50%);
|
|
21
|
+
white-space: nowrap;
|
|
22
|
+
}
|
|
23
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ComponentProps } from 'astro/types'
|
|
3
|
+
import Icon from '../Icon.astro'
|
|
4
|
+
|
|
5
|
+
/** Four corners pointing in. */
|
|
6
|
+
type Props = Omit<ComponentProps<typeof Icon>, 'solid'>
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<Icon {...Astro.props}>
|
|
10
|
+
<path d="M6 2.75V6H2.75M13.25 6H10V2.75M10 13.25V10h3.25M2.75 10H6v3.25"></path>
|
|
11
|
+
</Icon>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ComponentProps } from 'astro/types'
|
|
3
|
+
import Icon from '../Icon.astro'
|
|
4
|
+
|
|
5
|
+
/** Four corners pointing out. */
|
|
6
|
+
type Props = Omit<ComponentProps<typeof Icon>, 'solid'>
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<Icon {...Astro.props}>
|
|
10
|
+
<path d="M2.75 6V2.75H6M10 2.75h3.25V6M13.25 10v3.25H10M6 13.25H2.75V10"></path>
|
|
11
|
+
</Icon>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ComponentProps } from 'astro/types'
|
|
3
|
+
import Icon from '../Icon.astro'
|
|
4
|
+
|
|
5
|
+
/** A right chevron. */
|
|
6
|
+
type Props = Omit<ComponentProps<typeof Icon>, 'solid'>
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<Icon {...Astro.props}>
|
|
10
|
+
<path d="M6 3.5 10.5 8 6 12.5"></path>
|
|
11
|
+
</Icon>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ComponentProps } from 'astro/types'
|
|
3
|
+
import Icon from '../Icon.astro'
|
|
4
|
+
|
|
5
|
+
/** Two bars. */
|
|
6
|
+
type Props = Omit<ComponentProps<typeof Icon>, 'solid'>
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<Icon {...Astro.props}>
|
|
10
|
+
<path d="M5.25 3.5v9M10.75 3.5v9"></path>
|
|
11
|
+
</Icon>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ComponentProps } from 'astro/types'
|
|
3
|
+
import Icon from '../Icon.astro'
|
|
4
|
+
|
|
5
|
+
/** A solid triangle. */
|
|
6
|
+
type Props = Omit<ComponentProps<typeof Icon>, 'solid'>
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<Icon {...Astro.props} solid>
|
|
10
|
+
<path d="M5 3.2v9.6L12.6 8z"></path>
|
|
11
|
+
</Icon>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ComponentProps } from 'astro/types'
|
|
3
|
+
import Icon from '../Icon.astro'
|
|
4
|
+
|
|
5
|
+
/** A left chevron. */
|
|
6
|
+
type Props = Omit<ComponentProps<typeof Icon>, 'solid'>
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<Icon {...Astro.props}>
|
|
10
|
+
<path d="M10 3.5 5.5 8l4.5 4.5"></path>
|
|
11
|
+
</Icon>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ComponentProps } from 'astro/types'
|
|
3
|
+
import Icon from '../Icon.astro'
|
|
4
|
+
|
|
5
|
+
/** An arrow turning back to its start. */
|
|
6
|
+
type Props = Omit<ComponentProps<typeof Icon>, 'solid'>
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<Icon {...Astro.props}>
|
|
10
|
+
<path d="M3.2 8a4.8 4.8 0 1 0 1.4-3.4M4.2 2.4v2.8H7"></path>
|
|
11
|
+
</Icon>
|