@rxova/astro-ui 0.1.0 → 0.3.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/README.md +22 -10
- package/package.json +3 -2
- 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 +1 -1
- package/src/components/Icon.astro +29 -0
- package/src/components/ModeTabs.astro +124 -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/SiteFooter.astro +15 -13
- package/src/components/SizeTable.astro +118 -0
- package/src/components/ThemeToggle.astro +1 -1
- package/src/components/ValueGrid.astro +116 -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/lib/icons.ts +15 -0
- package/src/starlight/index.ts +1 -2
- package/src/styles/landing.css +55 -0
- package/src/styles/mermaid.css +75 -0
- package/src/components/SiteShell.astro +0 -155
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The proof band: a gradient-framed panel of figures that count up the first time it scrolls into view.
|
|
4
|
+
* Figures are server-rendered at their final value, so with JS off or motion reduced it is a static panel.
|
|
5
|
+
*/
|
|
6
|
+
export interface Stat {
|
|
7
|
+
value: string
|
|
8
|
+
label: string
|
|
9
|
+
note: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
stats: readonly Stat[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { stats } = Astro.props
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<div class="rx-stats">
|
|
20
|
+
<div class="rx-stats__glow" aria-hidden="true"></div>
|
|
21
|
+
<div class="rx-stats__frame">
|
|
22
|
+
<ul class="rx-stats__grid">
|
|
23
|
+
{stats.map((stat) => (
|
|
24
|
+
<li class="rx-stats__item">
|
|
25
|
+
<p class="rx-stats__value">{stat.value}</p>
|
|
26
|
+
<p class="rx-stats__label">{stat.label}</p>
|
|
27
|
+
<p class="rx-stats__note">{stat.note}</p>
|
|
28
|
+
</li>
|
|
29
|
+
))}
|
|
30
|
+
</ul>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<script>
|
|
35
|
+
const DURATION = 1100
|
|
36
|
+
|
|
37
|
+
const animate = (panel: HTMLElement) => {
|
|
38
|
+
// Values are display strings ("7.58 kB", "95%"); the leading number animates and the rest stays.
|
|
39
|
+
const figures = [...panel.querySelectorAll<HTMLElement>('.rx-stats__value')].flatMap((el) => {
|
|
40
|
+
const match = /^(\d+(?:\.\d+)?)(.*)$/s.exec(el.textContent ?? '')
|
|
41
|
+
if (!match) return []
|
|
42
|
+
const [, number = '', suffix = ''] = match
|
|
43
|
+
const final = el.textContent ?? ''
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
el,
|
|
47
|
+
target: Number(number),
|
|
48
|
+
decimals: (number.split('.')[1] ?? '').length,
|
|
49
|
+
suffix,
|
|
50
|
+
final,
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const frame = (start: number) => {
|
|
56
|
+
const t = Math.min(1, (performance.now() - start) / DURATION)
|
|
57
|
+
// easeOutExpo: quick to nearly-there, then settles.
|
|
58
|
+
const eased = t === 1 ? 1 : 1 - Math.pow(2, -10 * t)
|
|
59
|
+
for (const f of figures) {
|
|
60
|
+
f.el.textContent = t === 1 ? f.final : (f.target * eased).toFixed(f.decimals) + f.suffix
|
|
61
|
+
}
|
|
62
|
+
if (t < 1) requestAnimationFrame(() => frame(start))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const observer = new IntersectionObserver(
|
|
66
|
+
(entries) => {
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (!entry.isIntersecting) continue
|
|
69
|
+
observer.disconnect()
|
|
70
|
+
for (const f of figures) f.el.textContent = '0' + f.suffix
|
|
71
|
+
requestAnimationFrame(() => frame(performance.now()))
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{ threshold: 0.3 },
|
|
75
|
+
)
|
|
76
|
+
observer.observe(panel)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
80
|
+
for (const panel of document.querySelectorAll<HTMLElement>('.rx-stats')) animate(panel)
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<style>
|
|
85
|
+
.rx-stats {
|
|
86
|
+
position: relative;
|
|
87
|
+
isolation: isolate;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* A blurred copy of the frame's gradient behind it; its own element, since the frame paints a background. */
|
|
91
|
+
.rx-stats__glow {
|
|
92
|
+
position: absolute;
|
|
93
|
+
z-index: -1;
|
|
94
|
+
inset: -1.25rem -0.5rem;
|
|
95
|
+
border-radius: calc(var(--rx-radius) + 1rem);
|
|
96
|
+
background: var(--rx-gradient);
|
|
97
|
+
background-size: 220% 100%;
|
|
98
|
+
filter: blur(38px);
|
|
99
|
+
opacity: 0.2;
|
|
100
|
+
animation: rx-stats-sweep 14s linear infinite;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* 1px of padding over a gradient is the border: border-image does not follow border-radius. */
|
|
104
|
+
.rx-stats__frame {
|
|
105
|
+
padding: 1px;
|
|
106
|
+
border-radius: var(--rx-radius);
|
|
107
|
+
background: var(--rx-gradient);
|
|
108
|
+
background-size: 220% 100%;
|
|
109
|
+
animation: rx-stats-sweep 14s linear infinite;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@keyframes rx-stats-sweep {
|
|
113
|
+
to {
|
|
114
|
+
background-position: 220% 0;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.rx-stats__grid {
|
|
119
|
+
display: grid;
|
|
120
|
+
margin: 0;
|
|
121
|
+
padding: 0;
|
|
122
|
+
list-style: none;
|
|
123
|
+
background: var(--rx-card);
|
|
124
|
+
border-radius: calc(var(--rx-radius) - 1px);
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
grid-template-columns: 1fr;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@media (min-width: 38rem) {
|
|
130
|
+
.rx-stats__grid {
|
|
131
|
+
grid-template-columns: repeat(2, 1fr);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@media (min-width: 58rem) {
|
|
136
|
+
.rx-stats__grid {
|
|
137
|
+
grid-template-columns: repeat(3, 1fr);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Dividers by outline, not grid gap: with an odd tile count a gap leaves a hole that reads as a broken tile. */
|
|
142
|
+
.rx-stats__item {
|
|
143
|
+
position: relative;
|
|
144
|
+
padding: 1.7rem 1.5rem;
|
|
145
|
+
box-shadow: 0 0 0 1px var(--rx-rule);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* A gradient wash that fades up from the bottom of the hovered tile. */
|
|
149
|
+
.rx-stats__item::after {
|
|
150
|
+
content: '';
|
|
151
|
+
position: absolute;
|
|
152
|
+
inset: 0;
|
|
153
|
+
background: var(--rx-gradient);
|
|
154
|
+
opacity: 0;
|
|
155
|
+
transition: opacity 0.3s ease;
|
|
156
|
+
pointer-events: none;
|
|
157
|
+
mask-image: linear-gradient(to top, #000, transparent 72%);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.rx-stats__item:hover::after {
|
|
161
|
+
opacity: 0.1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.rx-stats__value,
|
|
165
|
+
.rx-stats__label,
|
|
166
|
+
.rx-stats__note {
|
|
167
|
+
position: relative;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.rx-stats__value {
|
|
171
|
+
margin: 0;
|
|
172
|
+
color: var(--rx-fg);
|
|
173
|
+
font-size: clamp(2rem, 1.5rem + 1.9vw, 2.9rem);
|
|
174
|
+
font-weight: 640;
|
|
175
|
+
line-height: 1;
|
|
176
|
+
letter-spacing: -0.03em;
|
|
177
|
+
/* Tabular figures so a counting number cannot jitter its own width. */
|
|
178
|
+
font-variant-numeric: tabular-nums;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.rx-stats__label {
|
|
182
|
+
margin: 0.7rem 0 0;
|
|
183
|
+
color: var(--rx-fg);
|
|
184
|
+
font-size: 0.95rem;
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.rx-stats__note {
|
|
189
|
+
margin: 0.35rem 0 0;
|
|
190
|
+
/* --rx-muted, not --rx-faint: faint on the card is ~3.4:1 and fails AA here. */
|
|
191
|
+
color: var(--rx-muted);
|
|
192
|
+
font-size: 0.82rem;
|
|
193
|
+
line-height: 1.5;
|
|
194
|
+
text-wrap: pretty;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@media (prefers-reduced-motion: reduce) {
|
|
198
|
+
.rx-stats__glow,
|
|
199
|
+
.rx-stats__frame {
|
|
200
|
+
animation: none;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.rx-stats__item::after {
|
|
204
|
+
transition: none;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
</style>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* An install command with a copy button beside a "Use" pane. The snippet arrives through the slot,
|
|
4
|
+
* so a page can write a fenced block and get the same Shiki highlighting as the docs.
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
command: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { command } = Astro.props
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<div class="rx-qs">
|
|
14
|
+
<div class="rx-qs__pane">
|
|
15
|
+
<p class="rx-qs__label">Install</p>
|
|
16
|
+
<div class="rx-qs__term">
|
|
17
|
+
{/* Focusable and labelled: a nowrap command over overflow-x scrolls, and a scrollable region needs a tab stop. */}
|
|
18
|
+
<code class="rx-qs__cmd" tabindex="0" role="region" aria-label="Install command">
|
|
19
|
+
<span class="rx-qs__prompt" aria-hidden="true">
|
|
20
|
+
$
|
|
21
|
+
</span>
|
|
22
|
+
{command}
|
|
23
|
+
</code>
|
|
24
|
+
<button class="rx-qs__copy" type="button" data-copy={command}>
|
|
25
|
+
<span class="rx-qs__copy-idle">Copy</span>
|
|
26
|
+
<span class="rx-qs__copy-done" aria-hidden="true">
|
|
27
|
+
Copied
|
|
28
|
+
</span>
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="rx-qs__pane">
|
|
34
|
+
<p class="rx-qs__label">Use</p>
|
|
35
|
+
<div class="rx-qs__code">
|
|
36
|
+
<slot />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<script>
|
|
42
|
+
for (const button of document.querySelectorAll<HTMLButtonElement>('.rx-qs__copy')) {
|
|
43
|
+
button.addEventListener('click', async () => {
|
|
44
|
+
try {
|
|
45
|
+
await navigator.clipboard.writeText(button.dataset.copy ?? '')
|
|
46
|
+
} catch {
|
|
47
|
+
// Clipboard access can be denied; saying nothing beats claiming a copy that did not happen.
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
button.dataset.state = 'done'
|
|
51
|
+
window.setTimeout(() => delete button.dataset.state, 1600)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<style>
|
|
57
|
+
.rx-qs {
|
|
58
|
+
display: grid;
|
|
59
|
+
gap: 1.25rem;
|
|
60
|
+
grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.rx-qs__pane {
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
min-width: 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.rx-qs__label {
|
|
70
|
+
margin: 0 0 0.6rem;
|
|
71
|
+
/* --rx-muted, not --rx-faint: faint on the card is ~3.4:1 and fails AA here. */
|
|
72
|
+
color: var(--rx-muted);
|
|
73
|
+
font-size: 0.72rem;
|
|
74
|
+
font-weight: 700;
|
|
75
|
+
letter-spacing: 0.08em;
|
|
76
|
+
text-transform: uppercase;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Not `flex: 1`: a one-line command stretched to the snippet's height floats in an empty box. */
|
|
80
|
+
.rx-qs__term {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
gap: 1rem;
|
|
84
|
+
padding: 1rem 1rem 1rem 1.15rem;
|
|
85
|
+
background: var(--rx-card);
|
|
86
|
+
border: 1px solid var(--rx-rule);
|
|
87
|
+
border-radius: var(--rx-radius);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.rx-qs__cmd {
|
|
91
|
+
flex: 1;
|
|
92
|
+
min-width: 0;
|
|
93
|
+
overflow-x: auto;
|
|
94
|
+
color: var(--rx-fg);
|
|
95
|
+
font-family: var(--rx-font-mono);
|
|
96
|
+
font-size: 0.88rem;
|
|
97
|
+
white-space: nowrap;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.rx-qs__prompt {
|
|
101
|
+
margin-right: 0.6rem;
|
|
102
|
+
color: var(--rx-primary);
|
|
103
|
+
user-select: none;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* One label over the other, so the button keeps its size when the state flips. */
|
|
107
|
+
.rx-qs__copy {
|
|
108
|
+
display: grid;
|
|
109
|
+
flex: none;
|
|
110
|
+
padding: 0.4rem 0.85rem;
|
|
111
|
+
color: var(--rx-muted);
|
|
112
|
+
background: var(--rx-bg);
|
|
113
|
+
border: 1px solid var(--rx-rule-strong);
|
|
114
|
+
border-radius: var(--rx-radius-pill);
|
|
115
|
+
font: inherit;
|
|
116
|
+
font-size: 0.78rem;
|
|
117
|
+
font-weight: 500;
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.rx-qs__copy:hover {
|
|
122
|
+
color: var(--rx-fg);
|
|
123
|
+
border-color: var(--rx-primary);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.rx-qs__copy-idle,
|
|
127
|
+
.rx-qs__copy-done {
|
|
128
|
+
grid-area: 1 / 1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.rx-qs__copy-done,
|
|
132
|
+
.rx-qs__copy[data-state='done'] .rx-qs__copy-idle {
|
|
133
|
+
visibility: hidden;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.rx-qs__copy[data-state='done'] .rx-qs__copy-done {
|
|
137
|
+
visibility: visible;
|
|
138
|
+
color: var(--rx-primary);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.rx-qs__code {
|
|
142
|
+
flex: 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* The slotted block renders its own <pre>; frame it like the terminal so the panes read as a pair. */
|
|
146
|
+
.rx-qs__code :global(pre) {
|
|
147
|
+
height: 100%;
|
|
148
|
+
margin: 0;
|
|
149
|
+
padding: 1rem 1.15rem;
|
|
150
|
+
border: 1px solid var(--rx-rule);
|
|
151
|
+
border-radius: var(--rx-radius);
|
|
152
|
+
overflow-x: auto;
|
|
153
|
+
font-size: 0.84rem;
|
|
154
|
+
line-height: 1.6;
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Screenshots shown rather than described: each in a hairline frame through Astro's `<Image>`,
|
|
4
|
+
* a mono label and a line of detail under it, and an optional link beneath the row.
|
|
5
|
+
*/
|
|
6
|
+
import { Image } from 'astro:assets'
|
|
7
|
+
import type { ImageMetadata } from 'astro'
|
|
8
|
+
|
|
9
|
+
export interface Shot {
|
|
10
|
+
src: ImageMetadata
|
|
11
|
+
alt: string
|
|
12
|
+
title: string
|
|
13
|
+
body: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
shots: readonly Shot[]
|
|
18
|
+
link?: { href: string; text: string; external?: boolean }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { shots, link } = Astro.props
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<div class="rx-shots">
|
|
25
|
+
<ul class="rx-shots__grid">
|
|
26
|
+
{shots.map((shot, index) => (
|
|
27
|
+
<li class="rx-shots__item">
|
|
28
|
+
<figure class="rx-shots__figure">
|
|
29
|
+
<div class="rx-shots__frame">
|
|
30
|
+
<Image src={shot.src} alt={shot.alt} loading="lazy" />
|
|
31
|
+
</div>
|
|
32
|
+
<figcaption class="rx-shots__caption">
|
|
33
|
+
<span class="rx-shots__index" aria-hidden="true">
|
|
34
|
+
{String(index + 1).padStart(2, '0')}
|
|
35
|
+
</span>
|
|
36
|
+
<span class="rx-shots__title">{shot.title}</span>
|
|
37
|
+
<span class="rx-shots__body">{shot.body}</span>
|
|
38
|
+
</figcaption>
|
|
39
|
+
</figure>
|
|
40
|
+
</li>
|
|
41
|
+
))}
|
|
42
|
+
</ul>
|
|
43
|
+
|
|
44
|
+
{link && (
|
|
45
|
+
<a class="rx-shots__link" href={link.href} rel={link.external ? 'noopener' : undefined}>
|
|
46
|
+
{link.text}
|
|
47
|
+
<svg
|
|
48
|
+
viewBox="0 0 16 16"
|
|
49
|
+
width="14"
|
|
50
|
+
height="14"
|
|
51
|
+
fill="none"
|
|
52
|
+
stroke="currentColor"
|
|
53
|
+
stroke-width="1.6"
|
|
54
|
+
stroke-linecap="round"
|
|
55
|
+
stroke-linejoin="round"
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
>
|
|
58
|
+
{link.external ? <path d="M5 3h8v8M13 3 3 13" /> : <path d="M3 8h10M9 4l4 4-4 4" />}
|
|
59
|
+
</svg>
|
|
60
|
+
</a>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<style>
|
|
65
|
+
.rx-shots__grid {
|
|
66
|
+
display: grid;
|
|
67
|
+
gap: 2rem 2.5rem;
|
|
68
|
+
margin: 0;
|
|
69
|
+
padding: 0;
|
|
70
|
+
list-style: none;
|
|
71
|
+
grid-template-columns: 1fr;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Viewport breakpoints, like ValueGrid: three across on a wide screen, two on a tablet, one on a phone. */
|
|
75
|
+
@media (min-width: 38rem) {
|
|
76
|
+
.rx-shots__grid {
|
|
77
|
+
grid-template-columns: repeat(2, 1fr);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@media (min-width: 60rem) {
|
|
82
|
+
.rx-shots__grid {
|
|
83
|
+
grid-template-columns: repeat(3, 1fr);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.rx-shots__item {
|
|
88
|
+
min-width: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.rx-shots__figure {
|
|
92
|
+
margin: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* A hairline, not a card: the screenshot is the surface. The frame owns the radius. */
|
|
96
|
+
.rx-shots__frame {
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
border: 1px solid var(--rx-rule);
|
|
99
|
+
border-radius: var(--rx-radius-sm);
|
|
100
|
+
background: var(--rx-card);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.rx-shots__frame :global(img) {
|
|
104
|
+
display: block;
|
|
105
|
+
width: 100%;
|
|
106
|
+
height: auto;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Index beside the label, detail hanging under the label; the indent binds the pair. */
|
|
110
|
+
.rx-shots__caption {
|
|
111
|
+
display: grid;
|
|
112
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
113
|
+
column-gap: 0.7rem;
|
|
114
|
+
row-gap: 0.3rem;
|
|
115
|
+
margin-top: 0.9rem;
|
|
116
|
+
align-content: start;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.rx-shots__index,
|
|
120
|
+
.rx-shots__title {
|
|
121
|
+
grid-row: 1;
|
|
122
|
+
font-family: var(--rx-font-mono);
|
|
123
|
+
font-size: 0.74rem;
|
|
124
|
+
letter-spacing: 0.06em;
|
|
125
|
+
text-transform: uppercase;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.rx-shots__index {
|
|
129
|
+
grid-column: 1;
|
|
130
|
+
color: var(--rx-faint);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.rx-shots__title {
|
|
134
|
+
grid-column: 2;
|
|
135
|
+
color: var(--rx-fg);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.rx-shots__body {
|
|
139
|
+
grid-column: 2;
|
|
140
|
+
grid-row: 2;
|
|
141
|
+
color: var(--rx-muted);
|
|
142
|
+
font-size: 0.9rem;
|
|
143
|
+
line-height: 1.5;
|
|
144
|
+
text-wrap: pretty;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.rx-shots__link {
|
|
148
|
+
display: inline-flex;
|
|
149
|
+
align-items: center;
|
|
150
|
+
gap: 0.45rem;
|
|
151
|
+
margin-top: 1.75rem;
|
|
152
|
+
color: var(--rx-fg);
|
|
153
|
+
font-size: 0.95rem;
|
|
154
|
+
font-weight: 500;
|
|
155
|
+
text-decoration: none;
|
|
156
|
+
border-bottom: 1px solid var(--rx-rule-strong);
|
|
157
|
+
padding-bottom: 0.15rem;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.rx-shots__link:hover {
|
|
161
|
+
color: var(--rx-primary);
|
|
162
|
+
border-color: currentColor;
|
|
163
|
+
}
|
|
164
|
+
</style>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A landing-page section: eyebrow, heading, lede, then the slot. `not-content` opts the subtree out
|
|
4
|
+
* of Starlight's prose rules, which would otherwise give every stat and card title a paragraph's measure.
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
id?: string
|
|
8
|
+
eyebrow?: string
|
|
9
|
+
title: string
|
|
10
|
+
lede?: string
|
|
11
|
+
/** Draws the gradient hairline above the heading; the brand spends the gradient sparingly. */
|
|
12
|
+
accent?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { id, eyebrow, title, lede, accent = false } = Astro.props
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<section class:list={['rx-sec', 'not-content', { 'rx-sec--accent': accent }]} id={id}>
|
|
19
|
+
<header class="rx-sec__head">
|
|
20
|
+
{eyebrow && <p class="rx-sec__eyebrow">{eyebrow}</p>}
|
|
21
|
+
<h2 class="rx-sec__title">{title}</h2>
|
|
22
|
+
{lede && <p class="rx-sec__lede">{lede}</p>}
|
|
23
|
+
</header>
|
|
24
|
+
<slot />
|
|
25
|
+
</section>
|
|
26
|
+
|
|
27
|
+
<style>
|
|
28
|
+
/* Unlayered, so it beats Starlight's `.sl-container > * + *` gap without a specificity fight. */
|
|
29
|
+
.rx-sec {
|
|
30
|
+
margin-block: 5.5rem 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* rem, not ch: `ch` resolves against this 1rem block, not the ~2.35rem heading it wraps. */
|
|
34
|
+
.rx-sec__head {
|
|
35
|
+
max-width: 48rem;
|
|
36
|
+
margin-bottom: 2rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.rx-sec__eyebrow {
|
|
40
|
+
margin: 0 0 0.75rem;
|
|
41
|
+
/* --rx-muted, not --rx-faint: faint on the card is ~3.4:1 and fails AA here. */
|
|
42
|
+
color: var(--rx-muted);
|
|
43
|
+
font-size: 0.75rem;
|
|
44
|
+
font-weight: 700;
|
|
45
|
+
letter-spacing: 0.08em;
|
|
46
|
+
text-transform: uppercase;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.rx-sec__title {
|
|
50
|
+
margin: 0;
|
|
51
|
+
color: var(--rx-fg);
|
|
52
|
+
font-size: clamp(1.6rem, 1.15rem + 1.7vw, 2.35rem);
|
|
53
|
+
font-weight: 640;
|
|
54
|
+
line-height: 1.15;
|
|
55
|
+
letter-spacing: -0.02em;
|
|
56
|
+
text-wrap: balance;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.rx-sec__lede {
|
|
60
|
+
max-width: 62ch;
|
|
61
|
+
margin: 0.9rem 0 0;
|
|
62
|
+
color: var(--rx-muted);
|
|
63
|
+
font-size: 1.05rem;
|
|
64
|
+
line-height: 1.6;
|
|
65
|
+
text-wrap: pretty;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.rx-sec--accent .rx-sec__head {
|
|
69
|
+
padding-top: 1.75rem;
|
|
70
|
+
border-top: 2px solid transparent;
|
|
71
|
+
border-image: var(--rx-gradient) 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Direct children only: a descendant selector would outrank the slotted components' own scoped rules. */
|
|
75
|
+
.rx-sec > :global(pre) {
|
|
76
|
+
margin: 0;
|
|
77
|
+
padding: 1.1rem 1.25rem;
|
|
78
|
+
background: var(--rx-card);
|
|
79
|
+
border: 1px solid var(--rx-rule);
|
|
80
|
+
border-radius: var(--rx-radius);
|
|
81
|
+
overflow-x: auto;
|
|
82
|
+
font-size: 0.86rem;
|
|
83
|
+
line-height: 1.65;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.rx-sec > :global(p) {
|
|
87
|
+
margin: 1.5rem 0 0;
|
|
88
|
+
color: var(--rx-muted);
|
|
89
|
+
font-size: 0.98rem;
|
|
90
|
+
line-height: 1.62;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.rx-sec > :global(p a) {
|
|
94
|
+
color: var(--rx-primary);
|
|
95
|
+
font-weight: 500;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@media (min-width: 50rem) {
|
|
99
|
+
.rx-sec {
|
|
100
|
+
margin-block: 7rem 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* The rxova footer on every surface: brand block, link columns, then copyright and legal links.
|
|
4
|
-
* Styles live in `../styles/footer.css
|
|
4
|
+
* Styles live in `../styles/footer.css`. Hrefs are absolute unless `origin` is '' (on rxova.org).
|
|
5
5
|
*/
|
|
6
|
-
import { PROJECTS, RXOVA_ORIGIN, getProject,
|
|
6
|
+
import { PROJECTS, RXOVA_ORIGIN, getProject, type ProjectId } from '@rxova/brand'
|
|
7
7
|
|
|
8
8
|
/** A standalone surface of rxova.org, for the "Site" column. */
|
|
9
9
|
export interface SiteLink {
|
|
10
10
|
label: string
|
|
11
|
-
/**
|
|
11
|
+
/** A page URL ending in `/`: absolute, or root-relative when `origin` is ''. */
|
|
12
12
|
href: string
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -27,27 +27,29 @@ interface Props {
|
|
|
27
27
|
* Defaults to every project, which is right for a docs site.
|
|
28
28
|
*/
|
|
29
29
|
projects?: readonly SiteLink[]
|
|
30
|
+
/** Prefix for every rxova.org link. '' on rxova.org itself, so links stay on the serving origin. */
|
|
31
|
+
origin?: string
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
const { project, docs, site, projects } = Astro.props
|
|
34
|
+
const { project, docs, site, projects, origin = RXOVA_ORIGIN } = Astro.props
|
|
33
35
|
const self = project ? getProject(project) : undefined
|
|
34
36
|
const year = new Date().getFullYear()
|
|
35
37
|
|
|
36
38
|
const projectLinks: readonly SiteLink[] =
|
|
37
|
-
projects ?? PROJECTS.map((p) => ({ label: p.label, href: `${
|
|
39
|
+
projects ?? PROJECTS.map((p) => ({ label: p.label, href: `${origin}${p.mount}` }))
|
|
38
40
|
|
|
39
41
|
const surfaces: readonly SiteLink[] = site ?? [
|
|
40
|
-
{ label: 'Blog', href:
|
|
41
|
-
{ label: 'Updates', href:
|
|
42
|
-
{ label: 'About', href:
|
|
42
|
+
{ label: 'Blog', href: `${origin}/blog/` },
|
|
43
|
+
{ label: 'Updates', href: `${origin}/updates/` },
|
|
44
|
+
{ label: 'About', href: `${origin}/about/` },
|
|
43
45
|
]
|
|
44
46
|
---
|
|
45
47
|
|
|
46
48
|
<footer class="rx-footer">
|
|
47
49
|
<div class="rx-footer__top">
|
|
48
50
|
<div class="rx-footer__brand">
|
|
49
|
-
<a class="rx-footer__mark" href={
|
|
50
|
-
<img src={
|
|
51
|
+
<a class="rx-footer__mark" href={`${origin}/`}>
|
|
52
|
+
<img src={`${origin}/rxova-logo-256.png`} alt="" width="28" height="28" loading="lazy" />
|
|
51
53
|
<span>Rxova</span>
|
|
52
54
|
</a>
|
|
53
55
|
{/* Not "React libraries": most ship a framework-agnostic core; some are extensions/CLIs. */}
|
|
@@ -112,7 +114,7 @@ const surfaces: readonly SiteLink[] = site ?? [
|
|
|
112
114
|
<h2 class="rx-footer__title">Site</h2>
|
|
113
115
|
<ul class="rx-footer__list">
|
|
114
116
|
<li>
|
|
115
|
-
<a href={
|
|
117
|
+
<a href={`${origin}/`}>Home</a>
|
|
116
118
|
</li>
|
|
117
119
|
{surfaces.map((s) => (
|
|
118
120
|
<li>
|
|
@@ -142,8 +144,8 @@ const surfaces: readonly SiteLink[] = site ?? [
|
|
|
142
144
|
<div class="rx-footer__legal">
|
|
143
145
|
<p>© {year} Rxova · MIT licensed</p>
|
|
144
146
|
<div class="rx-footer__legal-links">
|
|
145
|
-
<a href={
|
|
146
|
-
<a href={
|
|
147
|
+
<a href={`${origin}/privacy/`}>Privacy</a>
|
|
148
|
+
<a href={`${origin}/terms/`}>Terms</a>
|
|
147
149
|
</div>
|
|
148
150
|
</div>
|
|
149
151
|
</footer>
|