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.
- package/README.md +36 -0
- package/package.json +40 -0
- package/ui/ThemeProvider.tsx +16 -0
- package/ui/components/ArticlePage.tsx +673 -0
- package/ui/components/AuthPage.tsx +109 -0
- package/ui/components/Callout.tsx +79 -0
- package/ui/components/Chart.tsx +100 -0
- package/ui/components/CodeTabs.tsx +145 -0
- package/ui/components/ComparePage.tsx +364 -0
- package/ui/components/DashboardPage.tsx +773 -0
- package/ui/components/DocsNav.tsx +80 -0
- package/ui/components/Footer.tsx +136 -0
- package/ui/components/HubPage.tsx +529 -0
- package/ui/components/Modal.tsx +412 -0
- package/ui/components/Nav.tsx +162 -0
- package/ui/components/OnThisPage.tsx +56 -0
- package/ui/components/ParamsTable.tsx +86 -0
- package/ui/components/RangeBar.tsx +68 -0
- package/ui/components/SeriesChart.tsx +218 -0
- package/ui/components/SeriesPage.tsx +461 -0
- package/ui/components/StatusMark.tsx +48 -0
- package/ui/components/publictrades/ExternalLink.tsx +37 -0
- package/ui/components/publictrades/FactsCard.tsx +40 -0
- package/ui/components/publictrades/LedgerFooter.tsx +22 -0
- package/ui/components/publictrades/LedgerNav.tsx +78 -0
- package/ui/components/publictrades/Mark.tsx +40 -0
- package/ui/components/publictrades/SourceBlock.tsx +91 -0
- package/ui/components/publictrades/Tape.tsx +164 -0
- package/ui/components/publictrades/ThreeStamps.tsx +47 -0
- package/ui/components/publictrades/types.ts +19 -0
- package/ui/data/series.ts +851 -0
- package/ui/index.ts +50 -0
- package/ui/publictrades.ts +16 -0
- 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
|
+
}
|