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,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
+ }