dev-api-ui 0.1.7

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.
Files changed (34) hide show
  1. package/README.md +36 -0
  2. package/package.json +40 -0
  3. package/ui/ThemeProvider.tsx +16 -0
  4. package/ui/components/ArticlePage.tsx +673 -0
  5. package/ui/components/AuthPage.tsx +109 -0
  6. package/ui/components/Callout.tsx +79 -0
  7. package/ui/components/Chart.tsx +100 -0
  8. package/ui/components/CodeTabs.tsx +145 -0
  9. package/ui/components/ComparePage.tsx +364 -0
  10. package/ui/components/DashboardPage.tsx +773 -0
  11. package/ui/components/DocsNav.tsx +80 -0
  12. package/ui/components/Footer.tsx +136 -0
  13. package/ui/components/HubPage.tsx +529 -0
  14. package/ui/components/Modal.tsx +412 -0
  15. package/ui/components/Nav.tsx +162 -0
  16. package/ui/components/OnThisPage.tsx +56 -0
  17. package/ui/components/ParamsTable.tsx +86 -0
  18. package/ui/components/RangeBar.tsx +68 -0
  19. package/ui/components/SeriesChart.tsx +218 -0
  20. package/ui/components/SeriesPage.tsx +461 -0
  21. package/ui/components/StatusMark.tsx +48 -0
  22. package/ui/components/publictrades/ExternalLink.tsx +37 -0
  23. package/ui/components/publictrades/FactsCard.tsx +40 -0
  24. package/ui/components/publictrades/LedgerFooter.tsx +22 -0
  25. package/ui/components/publictrades/LedgerNav.tsx +78 -0
  26. package/ui/components/publictrades/Mark.tsx +40 -0
  27. package/ui/components/publictrades/SourceBlock.tsx +91 -0
  28. package/ui/components/publictrades/Tape.tsx +164 -0
  29. package/ui/components/publictrades/ThreeStamps.tsx +47 -0
  30. package/ui/components/publictrades/types.ts +19 -0
  31. package/ui/data/series.ts +851 -0
  32. package/ui/index.ts +50 -0
  33. package/ui/publictrades.ts +16 -0
  34. package/ui/themes.css +81 -0
@@ -0,0 +1,412 @@
1
+ 'use client';
2
+
3
+ import { ReactNode, useEffect, useRef, useState } from 'react';
4
+
5
+ interface ModalProps {
6
+ onClose: () => void;
7
+ children: ReactNode;
8
+ maxWidth?: number;
9
+ }
10
+
11
+ const CloseIcon = () => (
12
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" aria-hidden="true">
13
+ <path d="M6 6l12 12" />
14
+ <path d="M18 6L6 18" />
15
+ </svg>
16
+ );
17
+
18
+ export function Modal({ onClose, children, maxWidth = 540 }: ModalProps) {
19
+ const panelRef = useRef<HTMLDivElement>(null);
20
+ const triggerRef = useRef<Element | null>(null);
21
+
22
+ useEffect(() => {
23
+ triggerRef.current = document.activeElement;
24
+ panelRef.current?.focus();
25
+ document.body.style.overflow = 'hidden';
26
+
27
+ const onKeyDown = (e: KeyboardEvent) => {
28
+ if (e.key === 'Escape') {
29
+ onClose();
30
+ return;
31
+ }
32
+ if (e.key !== 'Tab') return;
33
+ const panel = panelRef.current;
34
+ if (!panel) return;
35
+ const focusable = Array.from(
36
+ panel.querySelectorAll<HTMLElement>(
37
+ 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])'
38
+ )
39
+ );
40
+ if (focusable.length === 0) return;
41
+ const first = focusable[0];
42
+ const last = focusable[focusable.length - 1];
43
+ if (e.shiftKey) {
44
+ if (document.activeElement === first) {
45
+ e.preventDefault();
46
+ last.focus();
47
+ }
48
+ } else {
49
+ if (document.activeElement === last) {
50
+ e.preventDefault();
51
+ first.focus();
52
+ }
53
+ }
54
+ };
55
+
56
+ document.addEventListener('keydown', onKeyDown);
57
+ return () => {
58
+ document.removeEventListener('keydown', onKeyDown);
59
+ document.body.style.overflow = '';
60
+ (triggerRef.current as HTMLElement | null)?.focus();
61
+ };
62
+ }, [onClose]);
63
+
64
+ return (
65
+ <div
66
+ onClick={onClose}
67
+ style={{
68
+ position: 'fixed',
69
+ inset: 0,
70
+ zIndex: 50,
71
+ background: 'rgba(22,24,29,.5)',
72
+ display: 'flex',
73
+ alignItems: 'center',
74
+ justifyContent: 'center',
75
+ padding: 24,
76
+ animation: 'mfade .12s ease-out',
77
+ }}
78
+ >
79
+ <div
80
+ ref={panelRef}
81
+ role="dialog"
82
+ aria-modal="true"
83
+ aria-labelledby="modal-title"
84
+ tabIndex={-1}
85
+ onClick={(e) => e.stopPropagation()}
86
+ style={{
87
+ width: '100%',
88
+ maxWidth,
89
+ maxHeight: '86vh',
90
+ overflow: 'auto',
91
+ background: 'var(--surface)',
92
+ border: '0.5px solid var(--hairline-2)',
93
+ borderRadius: 10,
94
+ animation: 'mrise .16s ease-out',
95
+ outline: 'none',
96
+ }}
97
+ >
98
+ {children}
99
+ </div>
100
+ </div>
101
+ );
102
+ }
103
+
104
+ interface ModalHeaderProps {
105
+ title: string;
106
+ subtitle: string;
107
+ onClose: () => void;
108
+ }
109
+
110
+ export function ModalHeader({ title, subtitle, onClose }: ModalHeaderProps) {
111
+ return (
112
+ <div
113
+ style={{
114
+ display: 'flex',
115
+ alignItems: 'flex-start',
116
+ justifyContent: 'space-between',
117
+ gap: 16,
118
+ padding: '20px 22px',
119
+ borderBottom: '0.5px solid var(--hairline)',
120
+ }}
121
+ >
122
+ <div>
123
+ <h3
124
+ id="modal-title"
125
+ style={{
126
+ fontFamily: 'var(--mono)',
127
+ fontSize: 18,
128
+ fontWeight: 600,
129
+ letterSpacing: '-0.01em',
130
+ }}
131
+ >
132
+ {title}
133
+ </h3>
134
+ <p style={{ fontSize: 13, color: 'var(--secondary)', marginTop: 4 }}>{subtitle}</p>
135
+ </div>
136
+ <button
137
+ onClick={onClose}
138
+ style={{
139
+ flex: 'none',
140
+ width: 30,
141
+ height: 30,
142
+ display: 'flex',
143
+ alignItems: 'center',
144
+ justifyContent: 'center',
145
+ border: '0.5px solid var(--hairline-2)',
146
+ borderRadius: 4,
147
+ background: 'var(--surface)',
148
+ color: 'var(--secondary)',
149
+ cursor: 'pointer',
150
+ }}
151
+ aria-label="Close"
152
+ >
153
+ <CloseIcon />
154
+ </button>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ const DownloadIcon = () => (
160
+ <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
161
+ <path d="M12 3v12" />
162
+ <path d="M7 11l5 5 5-5" />
163
+ <path d="M5 21h14" />
164
+ </svg>
165
+ );
166
+
167
+ const LockIcon = () => (
168
+ <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
169
+ <rect x="5" y="11" width="14" height="9" rx="1.5" />
170
+ <path d="M8 11V8a4 4 0 0 1 8 0v3" />
171
+ </svg>
172
+ );
173
+
174
+ const DL_ROWS = [
175
+ { gran: 'End of day (daily)', note: 'one settle per day', free: true },
176
+ { gran: 'Hourly', note: 'hourly bars', free: true },
177
+ { gran: '15-minute', note: 'intraday bars', free: false },
178
+ { gran: '1-minute', note: 'high-resolution', free: false },
179
+ { gran: 'Tick', note: 'every print', free: false },
180
+ ];
181
+
182
+ interface DownloadModalProps {
183
+ sym: string;
184
+ onClose: () => void;
185
+ }
186
+
187
+ export function DownloadModal({ sym, onClose }: DownloadModalProps) {
188
+ const [fmt, setFmt] = useState<'CSV' | 'JSON' | 'Parquet'>('CSV');
189
+ return (
190
+ <Modal onClose={onClose}>
191
+ <ModalHeader
192
+ title={`Download ${sym}`}
193
+ subtitle="Anyone can export end-of-day and hourly. Intraday — 15-minute and finer — needs a Pro key."
194
+ onClose={onClose}
195
+ />
196
+ <div style={{ padding: '18px 22px' }}>
197
+ <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '.07em', color: 'var(--tertiary)', marginBottom: 8 }}>
198
+ Format
199
+ </div>
200
+ <div aria-label="Format" style={{ display: 'flex', gap: 8, marginBottom: 20 }}>
201
+ {(['CSV', 'JSON', 'Parquet'] as const).map((f) => (
202
+ <button
203
+ key={f}
204
+ aria-pressed={fmt === f}
205
+ onClick={() => setFmt(f)}
206
+ style={{
207
+ fontSize: 13,
208
+ fontWeight: 500,
209
+ padding: '6px 12px',
210
+ borderRadius: 4,
211
+ border: `0.5px solid ${fmt === f ? 'var(--accent)' : 'var(--hairline-2)'}`,
212
+ background: fmt === f ? '#F3FAF6' : 'var(--surface)',
213
+ color: fmt === f ? 'var(--accent)' : 'var(--secondary)',
214
+ cursor: 'pointer',
215
+ }}
216
+ >
217
+ {f}
218
+ </button>
219
+ ))}
220
+ </div>
221
+
222
+ <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '.07em', color: 'var(--tertiary)', marginBottom: 8 }}>
223
+ Granularity
224
+ </div>
225
+ <div style={{ border: '0.5px solid var(--hairline-2)', borderRadius: 8, overflow: 'hidden' }}>
226
+ {DL_ROWS.map((row, i) => (
227
+ <div
228
+ key={row.gran}
229
+ style={{
230
+ display: 'flex',
231
+ alignItems: 'center',
232
+ justifyContent: 'space-between',
233
+ gap: 14,
234
+ padding: '13px 16px',
235
+ borderBottom: i < DL_ROWS.length - 1 ? '0.5px solid var(--hairline)' : 'none',
236
+ }}
237
+ >
238
+ <div>
239
+ <div style={{ fontSize: 14, fontWeight: 500, color: row.free ? 'var(--ink)' : 'var(--tertiary)' }}>
240
+ {row.gran}
241
+ </div>
242
+ <div style={{ fontSize: 12, color: 'var(--tertiary)', marginTop: 2 }}>{row.note}</div>
243
+ </div>
244
+ {row.free ? (
245
+ <a
246
+ href="#"
247
+ style={{
248
+ flex: 'none',
249
+ display: 'inline-flex',
250
+ alignItems: 'center',
251
+ gap: 6,
252
+ fontSize: 13,
253
+ fontWeight: 500,
254
+ padding: '7px 12px',
255
+ borderRadius: 4,
256
+ border: '0.5px solid var(--hairline-2)',
257
+ background: 'var(--surface)',
258
+ color: 'var(--ink)',
259
+ textDecoration: 'none',
260
+ }}
261
+ >
262
+ Download <DownloadIcon />
263
+ </a>
264
+ ) : (
265
+ <span
266
+ style={{
267
+ flex: 'none',
268
+ display: 'inline-flex',
269
+ alignItems: 'center',
270
+ gap: 6,
271
+ fontSize: 12,
272
+ fontWeight: 500,
273
+ color: 'var(--tertiary)',
274
+ }}
275
+ >
276
+ <LockIcon /> Pro
277
+ </span>
278
+ )}
279
+ </div>
280
+ ))}
281
+ </div>
282
+ </div>
283
+ <div
284
+ style={{
285
+ display: 'flex',
286
+ alignItems: 'center',
287
+ justifyContent: 'space-between',
288
+ gap: 16,
289
+ padding: '16px 22px',
290
+ borderTop: '0.5px solid var(--hairline)',
291
+ flexWrap: 'wrap',
292
+ }}
293
+ >
294
+ <span style={{ fontSize: 13, color: 'var(--secondary)' }}>Need intraday or tick history?</span>
295
+ <a
296
+ href="#"
297
+ style={{
298
+ display: 'inline-flex',
299
+ alignItems: 'center',
300
+ gap: 7,
301
+ fontSize: 14,
302
+ fontWeight: 500,
303
+ padding: '9px 15px',
304
+ borderRadius: 4,
305
+ border: '0.5px solid transparent',
306
+ background: 'var(--accent)',
307
+ color: '#fff',
308
+ textDecoration: 'none',
309
+ }}
310
+ >
311
+ Upgrade to Pro
312
+ </a>
313
+ </div>
314
+ </Modal>
315
+ );
316
+ }
317
+
318
+ interface CiteModalProps {
319
+ sym: string;
320
+ group: string;
321
+ id: string;
322
+ name: string;
323
+ onClose: () => void;
324
+ brand?: string;
325
+ siteUrl?: string;
326
+ }
327
+
328
+ export function CiteModal({ sym, group, id, name, onClose, brand = 'Console', siteUrl = 'https://console.dev' }: CiteModalProps) {
329
+ const citeUrl = `${siteUrl}/${group}/${id}?as_of=2026-06-16`;
330
+ const citeText = `${brand} (2026). ${name} (${sym}) [Data set]. Retrieved 2026-06-16, from ${citeUrl}`;
331
+ const bibtex = `@misc{${brand.replace(/[^a-z0-9]/gi, '')}_${id.replace(/[^a-z0-9]/gi, '')}_2026,\n title = {${name} (${sym})},\n author = {${brand}},\n year = {2026},\n note = {Point-in-time, as of 2026-06-16},\n url = {${citeUrl}}\n}`;
332
+ const [copied, setCopied] = useState<'url' | 'text' | 'bibtex' | null>(null);
333
+ const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
334
+ useEffect(() => () => clearTimeout(timerRef.current), []);
335
+
336
+ function copy(key: 'url' | 'text' | 'bibtex', value: string) {
337
+ navigator.clipboard.writeText(value).then(() => {
338
+ setCopied(key);
339
+ timerRef.current = setTimeout(() => setCopied(null), 1500);
340
+ });
341
+ }
342
+
343
+ return (
344
+ <Modal onClose={onClose} maxWidth={560}>
345
+ <ModalHeader
346
+ title={`Cite ${sym}`}
347
+ subtitle="Point-in-time citations resolve to the value as it was known on the date — reproducible and stable."
348
+ onClose={onClose}
349
+ />
350
+ <div style={{ padding: '18px 22px', display: 'grid', gap: 18 }}>
351
+ <div>
352
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 7 }}>
353
+ <span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '.07em', color: 'var(--tertiary)' }}>
354
+ Permalink · point-in-time
355
+ </span>
356
+ <button onClick={() => copy('url', citeUrl)} style={{ fontSize: 11, color: 'var(--accent)', fontWeight: 500, cursor: 'pointer', background: 'none', border: 'none', padding: 0 }}>{copied === 'url' ? 'Copied!' : 'Copy'}</button>
357
+ </div>
358
+ <div
359
+ style={{
360
+ fontFamily: 'var(--mono)',
361
+ fontSize: 13,
362
+ color: 'var(--ink)',
363
+ border: '0.5px solid var(--hairline-2)',
364
+ borderRadius: 4,
365
+ padding: '10px 12px',
366
+ background: 'var(--paper)',
367
+ wordBreak: 'break-all',
368
+ }}
369
+ >
370
+ {citeUrl}
371
+ </div>
372
+ </div>
373
+ <div>
374
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 7 }}>
375
+ <span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '.07em', color: 'var(--tertiary)' }}>
376
+ Plain citation
377
+ </span>
378
+ <button onClick={() => copy('text', citeText)} style={{ fontSize: 11, color: 'var(--accent)', fontWeight: 500, cursor: 'pointer', background: 'none', border: 'none', padding: 0 }}>{copied === 'text' ? 'Copied!' : 'Copy'}</button>
379
+ </div>
380
+ <p style={{ fontSize: 14, color: 'var(--ink)', lineHeight: 1.55 }}>{citeText}</p>
381
+ </div>
382
+ <div>
383
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 7 }}>
384
+ <span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '.07em', color: 'var(--tertiary)' }}>
385
+ BibTeX
386
+ </span>
387
+ <button onClick={() => copy('bibtex', bibtex)} style={{ fontSize: 11, color: 'var(--code-dim)', fontWeight: 500, cursor: 'pointer', background: 'none', border: 'none', padding: 0 }}>{copied === 'bibtex' ? 'Copied!' : 'copy'}</button>
388
+ </div>
389
+ <pre
390
+ style={{
391
+ margin: 0,
392
+ padding: '14px 14px',
393
+ fontFamily: 'var(--mono)',
394
+ fontSize: 12.5,
395
+ lineHeight: 1.6,
396
+ color: 'var(--code-fg)',
397
+ background: 'var(--code-bg)',
398
+ border: '0.5px solid #23262B',
399
+ borderRadius: 8,
400
+ overflowX: 'auto',
401
+ }}
402
+ >
403
+ {bibtex}
404
+ </pre>
405
+ </div>
406
+ </div>
407
+ <div style={{ padding: '14px 22px', borderTop: '0.5px solid var(--hairline)', fontSize: 12, color: 'var(--tertiary)' }}>
408
+ Citations resolve point-in-time and never change once published.
409
+ </div>
410
+ </Modal>
411
+ );
412
+ }
@@ -0,0 +1,162 @@
1
+ import Link from 'next/link';
2
+
3
+ export type NavLink = { label: string; href: string };
4
+
5
+ export interface NavProps {
6
+ wordmark?: string;
7
+ links?: NavLink[];
8
+ signInHref?: string;
9
+ getKeyHref?: string;
10
+ getKeyLabel?: string;
11
+ searchPlaceholder?: string;
12
+ }
13
+
14
+ const DEFAULT_LINKS: NavLink[] = [
15
+ { label: 'Data', href: '/fx' },
16
+ { label: 'Docs', href: '/docs' },
17
+ { label: 'Pricing', href: '/pricing' },
18
+ { label: 'Status', href: '/status' },
19
+ ];
20
+
21
+ export default function Nav({
22
+ wordmark = 'Console',
23
+ links = DEFAULT_LINKS,
24
+ signInHref = '/login',
25
+ getKeyHref = '/signup',
26
+ getKeyLabel = 'Get API key',
27
+ searchPlaceholder = 'Search series, docs…',
28
+ }: NavProps = {}) {
29
+ return (
30
+ <header
31
+ style={{
32
+ borderBottom: '0.5px solid var(--hairline)',
33
+ background: 'color-mix(in srgb, var(--paper) 85%, transparent)',
34
+ backdropFilter: 'saturate(180%) blur(8px)',
35
+ position: 'sticky',
36
+ top: 0,
37
+ zIndex: 20,
38
+ }}
39
+ >
40
+ <nav
41
+ style={{
42
+ maxWidth: 1100,
43
+ margin: '0 auto',
44
+ padding: '14px 32px',
45
+ display: 'flex',
46
+ alignItems: 'center',
47
+ justifyContent: 'space-between',
48
+ gap: 24,
49
+ }}
50
+ >
51
+ {/* Left: wordmark + search */}
52
+ <div
53
+ style={{
54
+ display: 'flex',
55
+ alignItems: 'center',
56
+ gap: 18,
57
+ flex: 1,
58
+ minWidth: 0,
59
+ maxWidth: 540,
60
+ }}
61
+ >
62
+ <Link
63
+ href="/"
64
+ style={{
65
+ display: 'flex',
66
+ alignItems: 'center',
67
+ gap: 9,
68
+ fontFamily: 'var(--mono)',
69
+ fontWeight: 600,
70
+ fontSize: 16,
71
+ letterSpacing: '-0.01em',
72
+ flex: 'none',
73
+ color: 'inherit',
74
+ textDecoration: 'none',
75
+ }}
76
+ >
77
+ <span
78
+ style={{
79
+ width: 9,
80
+ height: 9,
81
+ background: 'var(--accent)',
82
+ transform: 'rotate(45deg)',
83
+ display: 'inline-block',
84
+ }}
85
+ />
86
+ {wordmark}
87
+ </Link>
88
+ <span
89
+ style={{
90
+ display: 'flex',
91
+ alignItems: 'center',
92
+ gap: 9,
93
+ flex: 1,
94
+ minWidth: 0,
95
+ fontSize: 13,
96
+ color: 'var(--tertiary)',
97
+ border: '0.5px solid var(--hairline-2)',
98
+ borderRadius: 4,
99
+ padding: '8px 12px',
100
+ background: 'var(--surface)',
101
+ }}
102
+ >
103
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" aria-hidden="true">
104
+ <circle cx="11" cy="11" r="7" />
105
+ <path d="M21 21l-4-4" />
106
+ </svg>
107
+ <span style={{ flex: 1 }}>{searchPlaceholder}</span>
108
+ <span
109
+ style={{
110
+ border: '0.5px solid var(--hairline-2)',
111
+ borderRadius: 3,
112
+ padding: '1px 6px',
113
+ fontSize: 11,
114
+ }}
115
+ >
116
+ ⌘K
117
+ </span>
118
+ </span>
119
+ </div>
120
+
121
+ {/* Right: links + actions */}
122
+ <div style={{ display: 'flex', alignItems: 'center', gap: 18, flex: 'none' }}>
123
+ <div style={{ display: 'flex', gap: 20, fontSize: 14, color: 'var(--secondary)' }}>
124
+ {links.map((l) => (
125
+ <Link key={l.label} href={l.href} style={{ color: 'var(--secondary)', textDecoration: 'none' }}>{l.label}</Link>
126
+ ))}
127
+ </div>
128
+ <a
129
+ href={signInHref}
130
+ style={{
131
+ fontSize: 13,
132
+ fontWeight: 500,
133
+ padding: '7px 13px',
134
+ borderRadius: 4,
135
+ border: '0.5px solid var(--hairline-2)',
136
+ background: 'var(--surface)',
137
+ color: 'var(--ink)',
138
+ textDecoration: 'none',
139
+ }}
140
+ >
141
+ Sign in
142
+ </a>
143
+ <a
144
+ href={getKeyHref}
145
+ style={{
146
+ fontSize: 13,
147
+ fontWeight: 500,
148
+ padding: '7px 13px',
149
+ borderRadius: 4,
150
+ border: '0.5px solid transparent',
151
+ background: 'var(--accent)',
152
+ color: '#fff',
153
+ textDecoration: 'none',
154
+ }}
155
+ >
156
+ {getKeyLabel}
157
+ </a>
158
+ </div>
159
+ </nav>
160
+ </header>
161
+ );
162
+ }
@@ -0,0 +1,56 @@
1
+ interface Anchor {
2
+ id: string;
3
+ label: string;
4
+ }
5
+
6
+ interface OnThisPageProps {
7
+ anchors: Anchor[];
8
+ active: string;
9
+ }
10
+
11
+ export default function OnThisPage({ anchors, active }: OnThisPageProps) {
12
+ return (
13
+ <aside
14
+ style={{
15
+ position: 'sticky',
16
+ top: 57,
17
+ alignSelf: 'start',
18
+ padding: '34px 22px 40px',
19
+ }}
20
+ >
21
+ <div
22
+ style={{
23
+ fontSize: 11,
24
+ fontWeight: 600,
25
+ textTransform: 'uppercase',
26
+ letterSpacing: '.07em',
27
+ color: 'var(--tertiary)',
28
+ marginBottom: 10,
29
+ }}
30
+ >
31
+ On this page
32
+ </div>
33
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
34
+ {anchors.map((a) => {
35
+ const isActive = active === a.id;
36
+ return (
37
+ <a
38
+ key={a.id}
39
+ href={`#${a.id}`}
40
+ aria-current={isActive ? 'page' : undefined}
41
+ style={{
42
+ fontSize: 13,
43
+ textDecoration: 'none',
44
+ color: isActive ? 'var(--accent)' : 'var(--secondary)',
45
+ borderLeft: `2px solid ${isActive ? 'var(--accent)' : 'transparent'}`,
46
+ paddingLeft: 10,
47
+ }}
48
+ >
49
+ {a.label}
50
+ </a>
51
+ );
52
+ })}
53
+ </div>
54
+ </aside>
55
+ );
56
+ }