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,461 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import type { SeriesData } from '../data/series';
|
|
6
|
+
import StatusMark from './StatusMark';
|
|
7
|
+
import SeriesChart from './SeriesChart';
|
|
8
|
+
import RangeBar from './RangeBar';
|
|
9
|
+
import CodeTabs from './CodeTabs';
|
|
10
|
+
import { DownloadModal, CiteModal } from './Modal';
|
|
11
|
+
|
|
12
|
+
const ExternalIcon = () => (
|
|
13
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" style={{ verticalAlign: '-1px' }} aria-hidden="true">
|
|
14
|
+
<path d="M13 5h6v6" />
|
|
15
|
+
<path d="M19 5l-8 8" />
|
|
16
|
+
<path d="M19 13v5a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h5" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const DownloadIcon = () => (
|
|
21
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
22
|
+
<path d="M12 3v12" />
|
|
23
|
+
<path d="M7 11l5 5 5-5" />
|
|
24
|
+
<path d="M5 21h14" />
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const ArrowIcon = () => (
|
|
29
|
+
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
30
|
+
<path d="M5 12h14" />
|
|
31
|
+
<path d="M13 6l6 6-6 6" />
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
interface SeriesPageProps {
|
|
36
|
+
data: SeriesData;
|
|
37
|
+
brand?: string;
|
|
38
|
+
siteUrl?: string;
|
|
39
|
+
apiUrl?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Modal = 'download' | 'cite' | null;
|
|
43
|
+
|
|
44
|
+
export default function SeriesPage({ data: d, brand = 'Console', siteUrl = 'https://console.dev', apiUrl = 'https://api.console.dev' }: SeriesPageProps) {
|
|
45
|
+
const [modal, setModal] = useState<Modal>(null);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<div style={{ maxWidth: 1100, margin: '0 auto', padding: '0 32px' }}>
|
|
50
|
+
|
|
51
|
+
{/* HEADER */}
|
|
52
|
+
<section style={{ padding: '28px 0 26px' }}>
|
|
53
|
+
{/* Breadcrumb */}
|
|
54
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'var(--tertiary)' }}>
|
|
55
|
+
<Link href="#" style={{ color: 'var(--secondary)', textDecoration: 'none' }}>Data</Link>
|
|
56
|
+
<span>/</span>
|
|
57
|
+
<Link href={`/${d.group}`} style={{ color: 'var(--secondary)', textDecoration: 'none' }}>{d.groupLabel}</Link>
|
|
58
|
+
<span>/</span>
|
|
59
|
+
<span style={{ color: 'var(--ink)' }}>{d.sym}</span>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* Aligned grid: eyebrow↔status, ticker↔value, name↔delta */}
|
|
63
|
+
<div
|
|
64
|
+
style={{
|
|
65
|
+
display: 'grid',
|
|
66
|
+
gridTemplateColumns: '1fr auto',
|
|
67
|
+
columnGap: 30,
|
|
68
|
+
rowGap: 10,
|
|
69
|
+
alignItems: 'baseline',
|
|
70
|
+
marginTop: 18,
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
{/* Row 1 */}
|
|
74
|
+
<div
|
|
75
|
+
style={{
|
|
76
|
+
justifySelf: 'start',
|
|
77
|
+
display: 'inline-flex',
|
|
78
|
+
alignItems: 'center',
|
|
79
|
+
gap: 8,
|
|
80
|
+
fontSize: 12,
|
|
81
|
+
fontWeight: 600,
|
|
82
|
+
letterSpacing: '.04em',
|
|
83
|
+
textTransform: 'uppercase',
|
|
84
|
+
color: 'var(--accent)',
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--accent)', display: 'inline-block' }} />
|
|
88
|
+
{d.eyebrow}
|
|
89
|
+
</div>
|
|
90
|
+
<div style={{ justifySelf: 'end' }}>
|
|
91
|
+
<StatusMark color={d.statusColor} label={d.statusLabel} pulse={true} />
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Row 2 */}
|
|
95
|
+
<h1
|
|
96
|
+
style={{
|
|
97
|
+
justifySelf: 'start',
|
|
98
|
+
margin: 0,
|
|
99
|
+
fontFamily: 'var(--head)',
|
|
100
|
+
fontSize: 38,
|
|
101
|
+
fontWeight: 600,
|
|
102
|
+
letterSpacing: '-0.02em',
|
|
103
|
+
lineHeight: 1.02,
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{d.sym}
|
|
107
|
+
</h1>
|
|
108
|
+
<div style={{ justifySelf: 'end', fontSize: 42, fontWeight: 600, letterSpacing: '-0.02em', lineHeight: 1 }}>
|
|
109
|
+
{d.value}{' '}
|
|
110
|
+
{d.valueUnit && (
|
|
111
|
+
<span style={{ fontSize: 16, fontWeight: 500, color: 'var(--tertiary)' }}>{d.valueUnit}</span>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Row 3 */}
|
|
116
|
+
<div style={{ justifySelf: 'start', fontSize: 14, color: 'var(--secondary)' }}>
|
|
117
|
+
({d.name})
|
|
118
|
+
</div>
|
|
119
|
+
<div style={{ justifySelf: 'end' }}>
|
|
120
|
+
<span
|
|
121
|
+
style={{
|
|
122
|
+
display: 'inline-flex',
|
|
123
|
+
alignItems: 'center',
|
|
124
|
+
gap: 6,
|
|
125
|
+
fontSize: 14,
|
|
126
|
+
fontWeight: 500,
|
|
127
|
+
color: d.deltaColor,
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
<span style={{ width: 7, height: 7, borderRadius: '50%', background: d.deltaColor, display: 'inline-block' }} />
|
|
131
|
+
{d.delta}{' '}
|
|
132
|
+
<span style={{ color: 'var(--tertiary)', fontWeight: 400 }}>{d.deltaNote}</span>
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Action buttons */}
|
|
138
|
+
<div style={{ display: 'flex', gap: 12, marginTop: 28, flexWrap: 'wrap' }}>
|
|
139
|
+
<a
|
|
140
|
+
href="#"
|
|
141
|
+
style={{
|
|
142
|
+
display: 'inline-flex',
|
|
143
|
+
alignItems: 'center',
|
|
144
|
+
gap: 7,
|
|
145
|
+
fontSize: 14,
|
|
146
|
+
fontWeight: 500,
|
|
147
|
+
padding: '9px 15px',
|
|
148
|
+
borderRadius: 4,
|
|
149
|
+
border: '0.5px solid transparent',
|
|
150
|
+
background: 'var(--accent)',
|
|
151
|
+
color: '#fff',
|
|
152
|
+
textDecoration: 'none',
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
Get API key
|
|
156
|
+
</a>
|
|
157
|
+
<button
|
|
158
|
+
onClick={() => setModal('download')}
|
|
159
|
+
style={{
|
|
160
|
+
display: 'inline-flex',
|
|
161
|
+
alignItems: 'center',
|
|
162
|
+
gap: 7,
|
|
163
|
+
fontSize: 14,
|
|
164
|
+
fontWeight: 500,
|
|
165
|
+
fontFamily: 'inherit',
|
|
166
|
+
padding: '9px 15px',
|
|
167
|
+
borderRadius: 4,
|
|
168
|
+
border: '0.5px solid var(--hairline-2)',
|
|
169
|
+
background: 'var(--surface)',
|
|
170
|
+
color: 'var(--ink)',
|
|
171
|
+
cursor: 'pointer',
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
Download CSV <DownloadIcon />
|
|
175
|
+
</button>
|
|
176
|
+
<button
|
|
177
|
+
onClick={() => setModal('cite')}
|
|
178
|
+
style={{
|
|
179
|
+
display: 'inline-flex',
|
|
180
|
+
alignItems: 'center',
|
|
181
|
+
gap: 7,
|
|
182
|
+
fontSize: 14,
|
|
183
|
+
fontWeight: 500,
|
|
184
|
+
fontFamily: 'inherit',
|
|
185
|
+
padding: '9px 15px',
|
|
186
|
+
borderRadius: 4,
|
|
187
|
+
border: '0.5px solid var(--hairline-2)',
|
|
188
|
+
background: 'var(--surface)',
|
|
189
|
+
color: 'var(--ink)',
|
|
190
|
+
cursor: 'pointer',
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
Cite this series
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
</section>
|
|
197
|
+
|
|
198
|
+
{/* CHART + RANGE */}
|
|
199
|
+
<section style={{ padding: '0 0 30px', borderBottom: '0.5px solid var(--hairline)' }}>
|
|
200
|
+
<SeriesChart
|
|
201
|
+
points={d.points}
|
|
202
|
+
line={d.line}
|
|
203
|
+
axisStart={d.axisStart}
|
|
204
|
+
axisMid={d.axisMid}
|
|
205
|
+
axisEnd={d.axisEnd}
|
|
206
|
+
/>
|
|
207
|
+
<RangeBar ranges={d.ranges} defaultIndex={2} />
|
|
208
|
+
</section>
|
|
209
|
+
|
|
210
|
+
{/* GEO LEDE */}
|
|
211
|
+
<section style={{ padding: '30px 0', borderBottom: '0.5px solid var(--hairline)' }}>
|
|
212
|
+
<p style={{ fontSize: 18, color: 'var(--secondary)', lineHeight: 1.6 }}>
|
|
213
|
+
As of{' '}
|
|
214
|
+
<b style={{ color: 'var(--ink)', fontWeight: 500 }}>{d.asOf}</b>, {d.sym} ({d.name}) is{' '}
|
|
215
|
+
<b style={{ color: 'var(--ink)', fontWeight: 500 }}>{d.value}</b>, reconciled from{' '}
|
|
216
|
+
<b style={{ color: 'var(--ink)', fontWeight: 500 }}>{d.sourceFeeds}</b> and refreshed every{' '}
|
|
217
|
+
<b style={{ color: 'var(--ink)', fontWeight: 500 }}>{d.frequency}</b>. Every observation is
|
|
218
|
+
point-in-time and names the feed it came from — backtest-safe and reproducible. {d.sym} is
|
|
219
|
+
available over REST and WebSocket on the {brand} API, with a free tier to start.
|
|
220
|
+
</p>
|
|
221
|
+
</section>
|
|
222
|
+
|
|
223
|
+
{/* QUERY / CODE TABS */}
|
|
224
|
+
<section style={{ padding: '32px 0', borderBottom: '0.5px solid var(--hairline)' }}>
|
|
225
|
+
<h2
|
|
226
|
+
style={{
|
|
227
|
+
fontFamily: 'var(--head)',
|
|
228
|
+
fontSize: 20,
|
|
229
|
+
fontWeight: 600,
|
|
230
|
+
letterSpacing: '-0.01em',
|
|
231
|
+
marginBottom: 6,
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
How do I query {d.sym}?
|
|
235
|
+
</h2>
|
|
236
|
+
<p style={{ fontSize: 15, color: 'var(--secondary)', marginBottom: 16 }}>
|
|
237
|
+
One authenticated GET returns the latest value with its timestamp and source. Swap the language tab for your stack.
|
|
238
|
+
</p>
|
|
239
|
+
<CodeTabs group={d.group} id={d.id} apiUrl={apiUrl} />
|
|
240
|
+
<div
|
|
241
|
+
style={{
|
|
242
|
+
marginTop: 12,
|
|
243
|
+
fontSize: 13,
|
|
244
|
+
color: 'var(--secondary)',
|
|
245
|
+
display: 'flex',
|
|
246
|
+
alignItems: 'center',
|
|
247
|
+
gap: 8,
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>✓</span>
|
|
251
|
+
Every field in the response names the feed and timestamp it came from.
|
|
252
|
+
</div>
|
|
253
|
+
</section>
|
|
254
|
+
|
|
255
|
+
{/* REFERENCE CARD */}
|
|
256
|
+
<section style={{ padding: '32px 0', borderBottom: '0.5px solid var(--hairline)' }}>
|
|
257
|
+
<h2 style={{ fontFamily: 'var(--head)', fontSize: 20, fontWeight: 600, letterSpacing: '-0.01em', marginBottom: 14 }}>
|
|
258
|
+
Reference
|
|
259
|
+
</h2>
|
|
260
|
+
<div
|
|
261
|
+
style={{
|
|
262
|
+
background: 'var(--surface)',
|
|
263
|
+
border: '0.5px solid var(--hairline-2)',
|
|
264
|
+
borderRadius: 8,
|
|
265
|
+
padding: '20px 22px',
|
|
266
|
+
}}
|
|
267
|
+
>
|
|
268
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
269
|
+
<span
|
|
270
|
+
style={{
|
|
271
|
+
fontFamily: 'var(--mono)',
|
|
272
|
+
fontSize: 11,
|
|
273
|
+
fontWeight: 600,
|
|
274
|
+
letterSpacing: '.04em',
|
|
275
|
+
color: '#fff',
|
|
276
|
+
background: 'var(--ink)',
|
|
277
|
+
borderRadius: 4,
|
|
278
|
+
padding: '3px 8px',
|
|
279
|
+
}}
|
|
280
|
+
>
|
|
281
|
+
GET
|
|
282
|
+
</span>
|
|
283
|
+
<span style={{ fontFamily: 'var(--mono)', fontSize: 15, color: 'var(--ink)' }}>{d.endpoint}</span>
|
|
284
|
+
</div>
|
|
285
|
+
<p style={{ fontSize: 14, color: 'var(--secondary)', marginTop: 12, lineHeight: 1.55 }}>
|
|
286
|
+
{d.endpointDesc}
|
|
287
|
+
</p>
|
|
288
|
+
<div
|
|
289
|
+
style={{
|
|
290
|
+
borderTop: '0.5px solid var(--hairline)',
|
|
291
|
+
marginTop: 16,
|
|
292
|
+
paddingTop: 18,
|
|
293
|
+
display: 'grid',
|
|
294
|
+
gridTemplateColumns: 'repeat(4, 1fr)',
|
|
295
|
+
gap: 24,
|
|
296
|
+
}}
|
|
297
|
+
>
|
|
298
|
+
{[
|
|
299
|
+
{ label: 'Rate limit', val: d.rate },
|
|
300
|
+
{ label: 'Latency p95', val: d.latency },
|
|
301
|
+
{ label: 'Source feed', val: d.sourceFeeds },
|
|
302
|
+
{ label: 'Frequency', val: d.frequency },
|
|
303
|
+
].map(({ label, val }) => (
|
|
304
|
+
<div key={label}>
|
|
305
|
+
<div style={{ fontSize: 10, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '.07em', color: 'var(--tertiary)' }}>
|
|
306
|
+
{label}
|
|
307
|
+
</div>
|
|
308
|
+
<div style={{ fontSize: 15, marginTop: 5 }}>{val}</div>
|
|
309
|
+
</div>
|
|
310
|
+
))}
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
</section>
|
|
314
|
+
|
|
315
|
+
{/* PROVENANCE */}
|
|
316
|
+
<section style={{ padding: '32px 0', borderBottom: '0.5px solid var(--hairline)' }}>
|
|
317
|
+
<h2 style={{ fontFamily: 'var(--head)', fontSize: 20, fontWeight: 600, letterSpacing: '-0.01em', marginBottom: 14 }}>
|
|
318
|
+
Provenance
|
|
319
|
+
</h2>
|
|
320
|
+
<div
|
|
321
|
+
style={{
|
|
322
|
+
background: 'var(--surface)',
|
|
323
|
+
border: '0.5px solid var(--hairline-2)',
|
|
324
|
+
borderRadius: 8,
|
|
325
|
+
padding: '20px 22px',
|
|
326
|
+
display: 'flex',
|
|
327
|
+
alignItems: 'center',
|
|
328
|
+
justifyContent: 'space-between',
|
|
329
|
+
gap: 18,
|
|
330
|
+
flexWrap: 'wrap',
|
|
331
|
+
}}
|
|
332
|
+
>
|
|
333
|
+
<div>
|
|
334
|
+
<div style={{ fontSize: 15 }}>
|
|
335
|
+
Reconciled against <span style={{ fontWeight: 500 }}>{d.sourceName}</span> · parser v2.1
|
|
336
|
+
</div>
|
|
337
|
+
<div style={{ fontSize: 13, color: 'var(--tertiary)', marginTop: 4 }}>
|
|
338
|
+
Methodology and revision log are public and versioned.
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
<a
|
|
342
|
+
href="#"
|
|
343
|
+
style={{
|
|
344
|
+
display: 'inline-flex',
|
|
345
|
+
alignItems: 'center',
|
|
346
|
+
gap: 7,
|
|
347
|
+
fontSize: 14,
|
|
348
|
+
fontWeight: 500,
|
|
349
|
+
padding: '9px 15px',
|
|
350
|
+
borderRadius: 4,
|
|
351
|
+
border: '0.5px solid var(--hairline-2)',
|
|
352
|
+
background: 'var(--surface)',
|
|
353
|
+
color: 'var(--ink)',
|
|
354
|
+
textDecoration: 'none',
|
|
355
|
+
}}
|
|
356
|
+
>
|
|
357
|
+
Read methodology <ExternalIcon />
|
|
358
|
+
</a>
|
|
359
|
+
</div>
|
|
360
|
+
</section>
|
|
361
|
+
|
|
362
|
+
{/* MORE IN GROUP */}
|
|
363
|
+
<section style={{ padding: '32px 0', borderBottom: '0.5px solid var(--hairline)' }}>
|
|
364
|
+
<h2 style={{ fontFamily: 'var(--head)', fontSize: 20, fontWeight: 600, letterSpacing: '-0.01em', marginBottom: 16 }}>
|
|
365
|
+
More in {d.groupLabel}
|
|
366
|
+
</h2>
|
|
367
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 14 }}>
|
|
368
|
+
{d.related.map((r) => (
|
|
369
|
+
<Link
|
|
370
|
+
key={r.sym}
|
|
371
|
+
href={`/${r.group}/${r.sym.replace('/', '').toLowerCase()}`}
|
|
372
|
+
style={{
|
|
373
|
+
display: 'block',
|
|
374
|
+
background: 'var(--surface)',
|
|
375
|
+
border: '0.5px solid var(--hairline-2)',
|
|
376
|
+
borderRadius: 8,
|
|
377
|
+
padding: '18px 20px',
|
|
378
|
+
color: 'inherit',
|
|
379
|
+
textDecoration: 'none',
|
|
380
|
+
}}
|
|
381
|
+
>
|
|
382
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
383
|
+
<span style={{ fontFamily: 'var(--mono)', fontSize: 15, fontWeight: 600, letterSpacing: '-0.01em' }}>
|
|
384
|
+
{r.sym}
|
|
385
|
+
</span>
|
|
386
|
+
<StatusMark color={r.color} label={r.status} size={6} fontSize={12} />
|
|
387
|
+
</div>
|
|
388
|
+
<div style={{ fontSize: 13, color: 'var(--secondary)', marginTop: 6 }}>{r.name}</div>
|
|
389
|
+
<div style={{ fontSize: 18, fontWeight: 600, letterSpacing: '-0.01em', marginTop: 10 }}>{r.val}</div>
|
|
390
|
+
</Link>
|
|
391
|
+
))}
|
|
392
|
+
</div>
|
|
393
|
+
</section>
|
|
394
|
+
|
|
395
|
+
{/* RELATED READING */}
|
|
396
|
+
<section style={{ padding: '32px 0 8px' }}>
|
|
397
|
+
<h2 style={{ fontFamily: 'var(--head)', fontSize: 20, fontWeight: 600, letterSpacing: '-0.01em', marginBottom: 16 }}>
|
|
398
|
+
Related reading
|
|
399
|
+
</h2>
|
|
400
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 14 }}>
|
|
401
|
+
{d.reading.map((item) => (
|
|
402
|
+
<a
|
|
403
|
+
key={item.title}
|
|
404
|
+
href="#"
|
|
405
|
+
style={{
|
|
406
|
+
display: 'flex',
|
|
407
|
+
flexDirection: 'column',
|
|
408
|
+
gap: 8,
|
|
409
|
+
background: 'var(--surface)',
|
|
410
|
+
border: '0.5px solid var(--hairline-2)',
|
|
411
|
+
borderRadius: 8,
|
|
412
|
+
padding: '18px 20px',
|
|
413
|
+
color: 'inherit',
|
|
414
|
+
textDecoration: 'none',
|
|
415
|
+
minHeight: 108,
|
|
416
|
+
}}
|
|
417
|
+
>
|
|
418
|
+
<span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '.07em', color: 'var(--accent)' }}>
|
|
419
|
+
{item.type}
|
|
420
|
+
</span>
|
|
421
|
+
<span style={{ fontSize: 15, fontWeight: 500, color: 'var(--ink)', lineHeight: 1.4 }}>
|
|
422
|
+
{item.title}
|
|
423
|
+
</span>
|
|
424
|
+
<span
|
|
425
|
+
style={{
|
|
426
|
+
marginTop: 'auto',
|
|
427
|
+
display: 'inline-flex',
|
|
428
|
+
alignItems: 'center',
|
|
429
|
+
gap: 6,
|
|
430
|
+
fontSize: 13,
|
|
431
|
+
fontWeight: 500,
|
|
432
|
+
color: 'var(--accent)',
|
|
433
|
+
}}
|
|
434
|
+
>
|
|
435
|
+
Read <ArrowIcon />
|
|
436
|
+
</span>
|
|
437
|
+
</a>
|
|
438
|
+
))}
|
|
439
|
+
</div>
|
|
440
|
+
</section>
|
|
441
|
+
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
{/* MODALS */}
|
|
445
|
+
{modal === 'download' && (
|
|
446
|
+
<DownloadModal sym={d.sym} onClose={() => setModal(null)} />
|
|
447
|
+
)}
|
|
448
|
+
{modal === 'cite' && (
|
|
449
|
+
<CiteModal
|
|
450
|
+
sym={d.sym}
|
|
451
|
+
group={d.group}
|
|
452
|
+
id={d.id}
|
|
453
|
+
name={d.name}
|
|
454
|
+
brand={brand}
|
|
455
|
+
siteUrl={siteUrl}
|
|
456
|
+
onClose={() => setModal(null)}
|
|
457
|
+
/>
|
|
458
|
+
)}
|
|
459
|
+
</>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
interface StatusMarkProps {
|
|
2
|
+
color: string;
|
|
3
|
+
label: string;
|
|
4
|
+
pulse?: boolean;
|
|
5
|
+
size?: number;
|
|
6
|
+
fontSize?: number;
|
|
7
|
+
mono?: boolean;
|
|
8
|
+
uppercase?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function StatusMark({
|
|
12
|
+
color,
|
|
13
|
+
label,
|
|
14
|
+
pulse = false,
|
|
15
|
+
size = 7,
|
|
16
|
+
fontSize = 13,
|
|
17
|
+
mono = false,
|
|
18
|
+
uppercase = false,
|
|
19
|
+
}: StatusMarkProps) {
|
|
20
|
+
return (
|
|
21
|
+
<span
|
|
22
|
+
style={{
|
|
23
|
+
display: 'inline-flex',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
gap: 6,
|
|
26
|
+
fontSize,
|
|
27
|
+
fontWeight: 500,
|
|
28
|
+
color,
|
|
29
|
+
fontFamily: mono ? 'var(--mono)' : undefined,
|
|
30
|
+
textTransform: uppercase ? 'uppercase' : undefined,
|
|
31
|
+
letterSpacing: uppercase ? '.03em' : undefined,
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<span
|
|
35
|
+
style={{
|
|
36
|
+
width: size,
|
|
37
|
+
height: size,
|
|
38
|
+
borderRadius: '50%',
|
|
39
|
+
background: color,
|
|
40
|
+
display: 'inline-block',
|
|
41
|
+
animation: pulse ? 'cpulse 1.8s ease-in-out infinite' : undefined,
|
|
42
|
+
flexShrink: 0,
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
{label}
|
|
46
|
+
</span>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// Box-with-45°-arrow external link glyph — reused in source block + tape rows
|
|
4
|
+
interface ExternalLinkProps {
|
|
5
|
+
href?: string;
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
style?: React.CSSProperties;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function ExternalLinkIcon({ href = '#', width = 14, height = 14, style }: ExternalLinkProps) {
|
|
12
|
+
return (
|
|
13
|
+
<a
|
|
14
|
+
href={href}
|
|
15
|
+
style={{ display: 'inline-flex', color: 'var(--accent)', ...style }}
|
|
16
|
+
target="_blank"
|
|
17
|
+
rel="noopener noreferrer"
|
|
18
|
+
>
|
|
19
|
+
<svg
|
|
20
|
+
viewBox="0 0 24 24"
|
|
21
|
+
width={width}
|
|
22
|
+
height={height}
|
|
23
|
+
fill="none"
|
|
24
|
+
stroke="currentColor"
|
|
25
|
+
strokeWidth="1.8"
|
|
26
|
+
strokeLinecap="round"
|
|
27
|
+
strokeLinejoin="round"
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
style={{ verticalAlign: '-1px' }}
|
|
30
|
+
>
|
|
31
|
+
<path d="M13 5h6v6" />
|
|
32
|
+
<path d="M19 5l-8 8" />
|
|
33
|
+
<path d="M19 13v5a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h5" />
|
|
34
|
+
</svg>
|
|
35
|
+
</a>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
interface FactRow {
|
|
6
|
+
k: string;
|
|
7
|
+
v: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface FactsCardProps {
|
|
11
|
+
rows: FactRow[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function FactsCard({ rows }: FactsCardProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div style={{
|
|
17
|
+
background: 'var(--surface)',
|
|
18
|
+
border: '.5px solid var(--hairline-2)',
|
|
19
|
+
borderRadius: 8,
|
|
20
|
+
padding: '4px 22px',
|
|
21
|
+
}}>
|
|
22
|
+
{rows.map((row, i) => (
|
|
23
|
+
<div
|
|
24
|
+
key={i}
|
|
25
|
+
style={{
|
|
26
|
+
display: 'flex',
|
|
27
|
+
alignItems: 'center',
|
|
28
|
+
justifyContent: 'space-between',
|
|
29
|
+
gap: 18,
|
|
30
|
+
padding: '13px 0',
|
|
31
|
+
borderBottom: i < rows.length - 1 ? '.5px solid var(--hairline)' : 'none',
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<span style={{ fontSize: 13, color: 'var(--secondary)', flex: 'none' }}>{row.k}</span>
|
|
35
|
+
<span style={{ fontSize: 14, textAlign: 'right' }}>{row.v}</span>
|
|
36
|
+
</div>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export default function LedgerFooter() {
|
|
4
|
+
return (
|
|
5
|
+
<footer style={{ borderTop: '.5px solid var(--hairline)', padding: '22px 0 60px', marginTop: 62 }}>
|
|
6
|
+
<div style={{
|
|
7
|
+
maxWidth: 760,
|
|
8
|
+
margin: '0 auto',
|
|
9
|
+
padding: '0 28px',
|
|
10
|
+
fontSize: 12,
|
|
11
|
+
color: 'var(--tertiary)',
|
|
12
|
+
display: 'flex',
|
|
13
|
+
justifyContent: 'space-between',
|
|
14
|
+
gap: 16,
|
|
15
|
+
flexWrap: 'wrap',
|
|
16
|
+
}}>
|
|
17
|
+
<span>PublicTrades · the disclosure tape · by the makers of the open filing index</span>
|
|
18
|
+
<span style={{ fontFamily: 'var(--mono)', letterSpacing: '-.01em' }}>Ledger v0.2</span>
|
|
19
|
+
</div>
|
|
20
|
+
</footer>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface LedgerNavProps {
|
|
4
|
+
onGoTape?: () => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default function LedgerNav({ onGoTape }: LedgerNavProps) {
|
|
8
|
+
return (
|
|
9
|
+
<header style={{ borderBottom: '.5px solid var(--hairline)', background: 'var(--paper)' }}>
|
|
10
|
+
<nav style={{
|
|
11
|
+
maxWidth: 760,
|
|
12
|
+
margin: '0 auto',
|
|
13
|
+
padding: '18px 28px',
|
|
14
|
+
display: 'flex',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
justifyContent: 'space-between',
|
|
17
|
+
}}>
|
|
18
|
+
{/* Wordmark */}
|
|
19
|
+
<button
|
|
20
|
+
onClick={onGoTape}
|
|
21
|
+
style={{
|
|
22
|
+
display: 'flex',
|
|
23
|
+
alignItems: 'center',
|
|
24
|
+
gap: 9,
|
|
25
|
+
fontWeight: 600,
|
|
26
|
+
fontSize: 16,
|
|
27
|
+
letterSpacing: '-.01em',
|
|
28
|
+
cursor: 'pointer',
|
|
29
|
+
background: 'none',
|
|
30
|
+
border: 'none',
|
|
31
|
+
padding: 0,
|
|
32
|
+
color: 'inherit',
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<span style={{
|
|
36
|
+
width: 9,
|
|
37
|
+
height: 9,
|
|
38
|
+
borderRadius: '50%',
|
|
39
|
+
background: 'var(--accent)',
|
|
40
|
+
display: 'inline-block',
|
|
41
|
+
}} />
|
|
42
|
+
PublicTrades
|
|
43
|
+
</button>
|
|
44
|
+
|
|
45
|
+
{/* Nav links */}
|
|
46
|
+
<div style={{ display: 'flex', gap: 22, alignItems: 'center', fontSize: 14, color: 'var(--secondary)' }}>
|
|
47
|
+
<button
|
|
48
|
+
onClick={onGoTape}
|
|
49
|
+
style={{ color: 'var(--secondary)', cursor: 'pointer', background: 'none', border: 'none', padding: 0, fontSize: 'inherit' }}
|
|
50
|
+
>
|
|
51
|
+
Tape
|
|
52
|
+
</button>
|
|
53
|
+
<a href="#" style={{ color: 'var(--secondary)' }}>Docs</a>
|
|
54
|
+
<a href="#" style={{ color: 'var(--secondary)' }}>Pricing</a>
|
|
55
|
+
<a
|
|
56
|
+
href="#"
|
|
57
|
+
style={{
|
|
58
|
+
display: 'inline-flex',
|
|
59
|
+
alignItems: 'center',
|
|
60
|
+
gap: 7,
|
|
61
|
+
fontSize: 13,
|
|
62
|
+
fontWeight: 500,
|
|
63
|
+
padding: '6px 12px',
|
|
64
|
+
borderRadius: 4,
|
|
65
|
+
cursor: 'pointer',
|
|
66
|
+
border: '.5px solid var(--hairline-2)',
|
|
67
|
+
background: 'transparent',
|
|
68
|
+
color: 'var(--ink)',
|
|
69
|
+
textDecoration: 'none',
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
Get API key
|
|
73
|
+
</a>
|
|
74
|
+
</div>
|
|
75
|
+
</nav>
|
|
76
|
+
</header>
|
|
77
|
+
);
|
|
78
|
+
}
|