@nous-research/ui 0.14.2 → 0.16.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/CHANGELOG.md +227 -0
- package/README.md +24 -4
- package/dist/fonts.js +1 -0
- package/dist/hooks/use-capped-frame.js +1 -0
- package/dist/hooks/use-css-var-dims.js +1 -0
- package/dist/hooks/use-gpu-tier.js +1 -0
- package/dist/hooks/use-render-loop.js +1 -0
- package/dist/hooks/use-smooth-controls.js +1 -0
- package/dist/index.js +1 -0
- package/dist/ui/basic-page.js +1 -0
- package/dist/ui/components/animated-count.js +1 -0
- package/dist/ui/components/ascii.js +1 -0
- package/dist/ui/components/badge.js +2 -1
- package/dist/ui/components/badges/nous-girl.js +1 -0
- package/dist/ui/components/blend-mode.js +1 -0
- package/dist/ui/components/blink.js +1 -0
- package/dist/ui/components/button.js +2 -1
- package/dist/ui/components/checkbox.js +1 -0
- package/dist/ui/components/command-block.js +4 -3
- package/dist/ui/components/cursor.js +1 -0
- package/dist/ui/components/dropdown-menu.js +1 -0
- package/dist/ui/components/fit-text/index.js +1 -0
- package/dist/ui/components/graphs/bar-chart.js +1 -0
- package/dist/ui/components/graphs/index.js +1 -0
- package/dist/ui/components/graphs/line-chart.js +1 -0
- package/dist/ui/components/graphs/utils.js +1 -0
- package/dist/ui/components/grid/index.js +1 -0
- package/dist/ui/components/hover-bg.js +1 -0
- package/dist/ui/components/icons/arrow.js +1 -0
- package/dist/ui/components/icons/check.js +1 -0
- package/dist/ui/components/icons/chevron.js +1 -0
- package/dist/ui/components/icons/discord.js +1 -0
- package/dist/ui/components/icons/eye.js +1 -0
- package/dist/ui/components/icons/gear.js +1 -0
- package/dist/ui/components/icons/github.js +1 -0
- package/dist/ui/components/icons/hamburger.js +1 -0
- package/dist/ui/components/icons/heart.js +1 -0
- package/dist/ui/components/icons/index.js +1 -0
- package/dist/ui/components/icons/link.js +1 -0
- package/dist/ui/components/icons/minus.js +1 -0
- package/dist/ui/components/icons/search.js +1 -0
- package/dist/ui/components/image-distortion.js +1 -0
- package/dist/ui/components/leva-client.js +1 -0
- package/dist/ui/components/list-item.js +3 -2
- package/dist/ui/components/modal/index.js +1 -0
- package/dist/ui/components/modal/modal.css +1 -1
- package/dist/ui/components/overlays/blend-modes.js +1 -0
- package/dist/ui/components/overlays/glitch.js +1 -0
- package/dist/ui/components/overlays/greys.js +1 -0
- package/dist/ui/components/overlays/index.js +1 -0
- package/dist/ui/components/overlays/lens-layers.js +1 -0
- package/dist/ui/components/overlays/lens.js +1 -0
- package/dist/ui/components/overlays/noise.js +1 -0
- package/dist/ui/components/overlays/vignette.js +1 -0
- package/dist/ui/components/poster.js +1 -0
- package/dist/ui/components/progress.js +1 -0
- package/dist/ui/components/scene-canvas.js +1 -0
- package/dist/ui/components/scramble.js +1 -0
- package/dist/ui/components/segmented.js +5 -4
- package/dist/ui/components/select.js +1 -0
- package/dist/ui/components/selection-switcher.js +1 -0
- package/dist/ui/components/shader.js +1 -0
- package/dist/ui/components/socials.js +1 -0
- package/dist/ui/components/spinner.js +1 -0
- package/dist/ui/components/stats.js +2 -1
- package/dist/ui/components/switch.js +1 -0
- package/dist/ui/components/tabs.js +4 -3
- package/dist/ui/components/terminal-demo.js +2 -1
- package/dist/ui/components/theme-toggle.js +1 -0
- package/dist/ui/components/tier-card.js +2 -1
- package/dist/ui/components/tv.js +1 -0
- package/dist/ui/components/typography/h1.js +1 -0
- package/dist/ui/components/typography/h2.js +1 -0
- package/dist/ui/components/typography/index.js +1 -0
- package/dist/ui/components/typography/legend.js +1 -0
- package/dist/ui/components/typography/small.js +1 -0
- package/dist/ui/components/watchlist.js +2 -1
- package/dist/ui/footer.js +1 -0
- package/dist/ui/globals.css +33 -1
- package/dist/ui/header.js +1 -0
- package/dist/ui/layout-wrapper.js +2 -1
- package/dist/utils/color.js +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/poly.js +1 -0
- package/package.json +4 -2
- package/src/assets/filler-bg0.webp +0 -0
- package/src/assets.d.ts +38 -0
- package/src/fonts/Collapse-Bold.woff2 +0 -0
- package/src/fonts/Collapse-BoldItalic.woff2 +0 -0
- package/src/fonts/Collapse-Italic.woff2 +0 -0
- package/src/fonts/Collapse-Light.woff2 +0 -0
- package/src/fonts/Collapse-LightItalic.woff2 +0 -0
- package/src/fonts/Collapse-Regular.woff2 +0 -0
- package/src/fonts/Collapse-Thin.woff2 +0 -0
- package/src/fonts/Collapse-ThinItalic.woff2 +0 -0
- package/src/fonts/Mondwest-Regular.woff2 +0 -0
- package/src/fonts/Neuebit-Bold.woff2 +0 -0
- package/src/fonts/RulesCompressed-Medium.woff2 +0 -0
- package/src/fonts/RulesCompressed-Regular.woff2 +0 -0
- package/src/fonts/RulesExpanded-Bold.woff2 +0 -0
- package/src/fonts/RulesExpanded-Regular.woff2 +0 -0
- package/src/fonts.ts +6 -0
- package/src/hooks/use-capped-frame.ts +18 -0
- package/src/hooks/use-css-var-dims.ts +39 -0
- package/src/hooks/use-gpu-tier.ts +165 -0
- package/src/hooks/use-render-loop.ts +121 -0
- package/src/hooks/use-smooth-controls.ts +318 -0
- package/src/index.ts +109 -0
- package/src/ui/basic-page.tsx +34 -0
- package/src/ui/build.css +4 -0
- package/src/ui/components/animated-count.stories.tsx +67 -0
- package/src/ui/components/animated-count.tsx +168 -0
- package/src/ui/components/ascii.stories.tsx +30 -0
- package/src/ui/components/ascii.tsx +110 -0
- package/src/ui/components/badge.stories.tsx +31 -0
- package/src/ui/components/badge.tsx +60 -0
- package/src/ui/components/badges/nous-girl.tsx +52 -0
- package/src/ui/components/blend-mode.stories.tsx +33 -0
- package/src/ui/components/blend-mode.tsx +129 -0
- package/src/ui/components/blink.stories.tsx +32 -0
- package/src/ui/components/blink.tsx +21 -0
- package/src/ui/components/button.stories.tsx +68 -0
- package/src/ui/components/button.tsx +170 -0
- package/src/ui/components/checkbox.stories.tsx +113 -0
- package/src/ui/components/checkbox.tsx +36 -0
- package/src/ui/components/command-block.stories.tsx +52 -0
- package/src/ui/components/command-block.tsx +86 -0
- package/src/ui/components/cursor.tsx +115 -0
- package/src/ui/components/dropdown-menu.stories.tsx +52 -0
- package/src/ui/components/dropdown-menu.tsx +117 -0
- package/src/ui/components/fit-text/fit-text.css +42 -0
- package/src/ui/components/fit-text/index.stories.tsx +33 -0
- package/src/ui/components/fit-text/index.tsx +45 -0
- package/src/ui/components/graphs/bar-chart.tsx +153 -0
- package/src/ui/components/graphs/index.stories.tsx +64 -0
- package/src/ui/components/graphs/index.tsx +4 -0
- package/src/ui/components/graphs/line-chart.tsx +213 -0
- package/src/ui/components/graphs/utils.tsx +265 -0
- package/src/ui/components/grid/grid.css +79 -0
- package/src/ui/components/grid/index.tsx +19 -0
- package/src/ui/components/hover-bg.stories.tsx +29 -0
- package/src/ui/components/hover-bg.tsx +15 -0
- package/src/ui/components/icons/arrow.tsx +42 -0
- package/src/ui/components/icons/check.tsx +14 -0
- package/src/ui/components/icons/chevron.tsx +45 -0
- package/src/ui/components/icons/discord.tsx +16 -0
- package/src/ui/components/icons/eye.tsx +12 -0
- package/src/ui/components/icons/gear.tsx +51 -0
- package/src/ui/components/icons/github.tsx +16 -0
- package/src/ui/components/icons/hamburger.tsx +52 -0
- package/src/ui/components/icons/heart.tsx +12 -0
- package/src/ui/components/icons/index.ts +12 -0
- package/src/ui/components/icons/link.tsx +14 -0
- package/src/ui/components/icons/minus.tsx +14 -0
- package/src/ui/components/icons/search.tsx +28 -0
- package/src/ui/components/image-distortion.stories.tsx +120 -0
- package/src/ui/components/image-distortion.tsx +498 -0
- package/src/ui/components/leva-client.tsx +14 -0
- package/src/ui/components/list-item.stories.tsx +83 -0
- package/src/ui/components/list-item.tsx +37 -0
- package/src/ui/components/modal/index.stories.tsx +46 -0
- package/src/ui/components/modal/index.tsx +48 -0
- package/src/ui/components/modal/modal.css +36 -0
- package/src/ui/components/overlays/blend-modes.ts +13 -0
- package/src/ui/components/overlays/glitch.tsx +243 -0
- package/src/ui/components/overlays/greys.tsx +386 -0
- package/src/ui/components/overlays/index.tsx +47 -0
- package/src/ui/components/overlays/lens-layers.tsx +119 -0
- package/src/ui/components/overlays/lens.ts +91 -0
- package/src/ui/components/overlays/noise.tsx +174 -0
- package/src/ui/components/overlays/vignette.tsx +60 -0
- package/src/ui/components/poster.stories.tsx +513 -0
- package/src/ui/components/poster.tsx +411 -0
- package/src/ui/components/progress.stories.tsx +48 -0
- package/src/ui/components/progress.tsx +56 -0
- package/src/ui/components/scene-canvas.tsx +254 -0
- package/src/ui/components/scramble.stories.tsx +49 -0
- package/src/ui/components/scramble.tsx +95 -0
- package/src/ui/components/segmented.stories.tsx +101 -0
- package/src/ui/components/segmented.tsx +81 -0
- package/src/ui/components/select.stories.tsx +88 -0
- package/src/ui/components/select.tsx +267 -0
- package/src/ui/components/selection-switcher.tsx +44 -0
- package/src/ui/components/shader.tsx +83 -0
- package/src/ui/components/socials.tsx +42 -0
- package/src/ui/components/spinner.stories.tsx +101 -0
- package/src/ui/components/spinner.tsx +60 -0
- package/src/ui/components/stats.stories.tsx +24 -0
- package/src/ui/components/stats.tsx +53 -0
- package/src/ui/components/switch.stories.tsx +77 -0
- package/src/ui/components/switch.tsx +48 -0
- package/src/ui/components/tabs.stories.tsx +101 -0
- package/src/ui/components/tabs.tsx +66 -0
- package/src/ui/components/terminal-demo.stories.tsx +67 -0
- package/src/ui/components/terminal-demo.tsx +189 -0
- package/src/ui/components/theme-toggle.stories.tsx +47 -0
- package/src/ui/components/theme-toggle.tsx +66 -0
- package/src/ui/components/tier-card.stories.tsx +217 -0
- package/src/ui/components/tier-card.tsx +190 -0
- package/src/ui/components/tv.stories.tsx +37 -0
- package/src/ui/components/tv.tsx +257 -0
- package/src/ui/components/typography/h1.tsx +18 -0
- package/src/ui/components/typography/h2.tsx +18 -0
- package/src/ui/components/typography/index.tsx +54 -0
- package/src/ui/components/typography/legend.tsx +24 -0
- package/src/ui/components/typography/small.tsx +11 -0
- package/src/ui/components/watchlist.stories.tsx +33 -0
- package/src/ui/components/watchlist.tsx +105 -0
- package/src/ui/fonts.css +63 -0
- package/src/ui/footer.tsx +111 -0
- package/src/ui/globals.css +383 -0
- package/src/ui/header.tsx +398 -0
- package/src/ui/layout-wrapper.tsx +11 -0
- package/src/utils/color.ts +21 -0
- package/src/utils/index.ts +62 -0
- package/src/utils/poly.ts +26 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as Plot from '@observablehq/plot'
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
import { cn } from '../../../utils'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
accessor,
|
|
10
|
+
CHART_MARGINS,
|
|
11
|
+
CHART_STYLE,
|
|
12
|
+
type ChartProps,
|
|
13
|
+
Crosshair,
|
|
14
|
+
type CrosshairState,
|
|
15
|
+
type DataPoint,
|
|
16
|
+
setupCrosshair,
|
|
17
|
+
stylePlot,
|
|
18
|
+
useDims,
|
|
19
|
+
withChartBlend
|
|
20
|
+
} from './utils'
|
|
21
|
+
|
|
22
|
+
export const LineChart = withChartBlend(
|
|
23
|
+
<T extends DataPoint>({
|
|
24
|
+
backgroundColor: _,
|
|
25
|
+
className,
|
|
26
|
+
color: strokeColor,
|
|
27
|
+
curve = 'natural',
|
|
28
|
+
data = [],
|
|
29
|
+
formatTooltip,
|
|
30
|
+
formatX: formatXProp,
|
|
31
|
+
formatY: formatYProp,
|
|
32
|
+
series = 'series' as keyof T,
|
|
33
|
+
showArea = false,
|
|
34
|
+
x = 'label' as keyof T,
|
|
35
|
+
xTicks,
|
|
36
|
+
y = 'value' as keyof T,
|
|
37
|
+
yDomain = [0, 0.5],
|
|
38
|
+
yTicks = 4,
|
|
39
|
+
...props
|
|
40
|
+
}: LineChartProps<T> & { backgroundColor?: string; color?: string }) => {
|
|
41
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
42
|
+
const plotRef = useRef<HTMLDivElement>(null)
|
|
43
|
+
const [hovered, setHovered] = useState<null | T>(null)
|
|
44
|
+
const [crosshair, setCrosshair] = useState<CrosshairState>({ x: null })
|
|
45
|
+
const dims = useDims(ref)
|
|
46
|
+
|
|
47
|
+
const formatX = useCallback(
|
|
48
|
+
(v: unknown) =>
|
|
49
|
+
formatXProp?.(v) ??
|
|
50
|
+
((v as number) >= 1e3 ? `${(v as number) / 1e3}k` : `${v}`),
|
|
51
|
+
[formatXProp]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const formatY = useCallback(
|
|
55
|
+
(v: number) => formatYProp?.(v) ?? `${Math.round(v * 100)}%`,
|
|
56
|
+
[formatYProp]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const getX = useMemo(() => accessor<T, unknown>(x), [x])
|
|
60
|
+
const getY = useMemo(() => accessor<T, number>(y), [y])
|
|
61
|
+
const getZ = useCallback((d: T) => d[series], [series])
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (
|
|
65
|
+
!ref.current ||
|
|
66
|
+
!plotRef.current ||
|
|
67
|
+
!data.length ||
|
|
68
|
+
!dims.h ||
|
|
69
|
+
!dims.w
|
|
70
|
+
) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
plotRef.current.innerHTML = ''
|
|
75
|
+
|
|
76
|
+
const hasSeries = data.some(d => d[series] !== undefined)
|
|
77
|
+
|
|
78
|
+
const seriesIdx = hasSeries
|
|
79
|
+
? data.reduce(
|
|
80
|
+
(acc, d, i) => ((acc[d[series] as string] ??= i), acc),
|
|
81
|
+
{} as Record<string, number>
|
|
82
|
+
)
|
|
83
|
+
: {}
|
|
84
|
+
|
|
85
|
+
const n = Object.keys(seriesIdx).length
|
|
86
|
+
|
|
87
|
+
const opacity = (d: T) => {
|
|
88
|
+
if (!hasSeries) {
|
|
89
|
+
return 1
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (hovered) {
|
|
93
|
+
return d[series] === hovered[series] ? 1 : 0.2
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return 1 - (seriesIdx[d[series] as string] / Math.max(n - 1, 1)) * 0.2
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const lineOpts = {
|
|
100
|
+
curve,
|
|
101
|
+
x: getX as (d: T) => unknown,
|
|
102
|
+
y: getY,
|
|
103
|
+
...(hasSeries && { z: getZ as (d: T) => unknown })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const plot = Plot.plot({
|
|
107
|
+
...CHART_MARGINS,
|
|
108
|
+
height: dims.h,
|
|
109
|
+
marks: [
|
|
110
|
+
...(showArea
|
|
111
|
+
? [
|
|
112
|
+
Plot.areaY(data, {
|
|
113
|
+
...lineOpts,
|
|
114
|
+
fill: strokeColor,
|
|
115
|
+
fillOpacity: 0.15,
|
|
116
|
+
y1: yDomain[0]
|
|
117
|
+
})
|
|
118
|
+
]
|
|
119
|
+
: []),
|
|
120
|
+
Plot.lineY(data, {
|
|
121
|
+
...lineOpts,
|
|
122
|
+
stroke: 'transparent',
|
|
123
|
+
strokeWidth: 16
|
|
124
|
+
}),
|
|
125
|
+
Plot.lineY(data, {
|
|
126
|
+
...lineOpts,
|
|
127
|
+
stroke: strokeColor,
|
|
128
|
+
strokeOpacity: opacity,
|
|
129
|
+
strokeWidth: 1.5
|
|
130
|
+
})
|
|
131
|
+
],
|
|
132
|
+
style: { ...CHART_STYLE, fontStretch: 'expanded' },
|
|
133
|
+
width: dims.w,
|
|
134
|
+
x: { label: null, tickFormat: formatX, ticks: xTicks },
|
|
135
|
+
y: {
|
|
136
|
+
domain: yDomain,
|
|
137
|
+
grid: true,
|
|
138
|
+
label: null,
|
|
139
|
+
tickFormat: formatY,
|
|
140
|
+
ticks: yTicks
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
plot.addEventListener('input', () => setHovered(plot.value as null | T))
|
|
145
|
+
stylePlot(plot as HTMLElement)
|
|
146
|
+
|
|
147
|
+
plot.querySelectorAll('g[aria-label="line"] path').forEach(el =>
|
|
148
|
+
Object.assign((el as SVGPathElement).style, {
|
|
149
|
+
transition: 'stroke-opacity 0.2s'
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
plotRef.current.appendChild(plot)
|
|
154
|
+
|
|
155
|
+
const cleanup = setupCrosshair(
|
|
156
|
+
ref.current,
|
|
157
|
+
data,
|
|
158
|
+
d => getX(d) as number,
|
|
159
|
+
getY,
|
|
160
|
+
yDomain,
|
|
161
|
+
d => formatTooltip?.(d) ?? `${formatX(getX(d))}: ${formatY(getY(d))}`,
|
|
162
|
+
setCrosshair,
|
|
163
|
+
hasSeries ? d => getZ(d) : undefined
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return () => {
|
|
167
|
+
cleanup()
|
|
168
|
+
plot.parentNode && plot.remove()
|
|
169
|
+
}
|
|
170
|
+
}, [
|
|
171
|
+
curve,
|
|
172
|
+
data,
|
|
173
|
+
dims.h,
|
|
174
|
+
dims.w,
|
|
175
|
+
formatTooltip,
|
|
176
|
+
formatX,
|
|
177
|
+
formatY,
|
|
178
|
+
getX,
|
|
179
|
+
getY,
|
|
180
|
+
getZ,
|
|
181
|
+
hovered,
|
|
182
|
+
series,
|
|
183
|
+
showArea,
|
|
184
|
+
strokeColor,
|
|
185
|
+
xTicks,
|
|
186
|
+
yDomain,
|
|
187
|
+
yTicks
|
|
188
|
+
])
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div
|
|
192
|
+
className={cn('relative aspect-4/1 w-full overflow-clip', className)}
|
|
193
|
+
ref={ref}
|
|
194
|
+
{...props}
|
|
195
|
+
>
|
|
196
|
+
<div className="absolute inset-0" ref={plotRef} />
|
|
197
|
+
|
|
198
|
+
<Crosshair
|
|
199
|
+
color={strokeColor}
|
|
200
|
+
containerWidth={dims.w}
|
|
201
|
+
height={dims.h}
|
|
202
|
+
{...crosshair}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
interface LineChartProps<T extends DataPoint> extends ChartProps<T> {
|
|
210
|
+
curve?: 'basis' | 'catmull-rom' | 'linear' | 'natural' | 'step'
|
|
211
|
+
series?: keyof T
|
|
212
|
+
showArea?: boolean
|
|
213
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ComponentType,
|
|
5
|
+
type HTMLAttributes,
|
|
6
|
+
type RefObject,
|
|
7
|
+
useEffect,
|
|
8
|
+
useState
|
|
9
|
+
} from 'react'
|
|
10
|
+
|
|
11
|
+
import { useSmoothControls } from '../../../hooks/use-smooth-controls'
|
|
12
|
+
import {
|
|
13
|
+
type BlendColors,
|
|
14
|
+
type BlendModeProps
|
|
15
|
+
} from '../blend-mode'
|
|
16
|
+
|
|
17
|
+
export const accessor = <T, R>(key: ((d: T) => R) | keyof T) =>
|
|
18
|
+
typeof key === 'function' ? key : (d: T) => d[key] as R
|
|
19
|
+
|
|
20
|
+
export const CHART_MARGINS = {
|
|
21
|
+
marginBottom: 24,
|
|
22
|
+
marginLeft: 36,
|
|
23
|
+
marginRight: 12,
|
|
24
|
+
marginTop: 8
|
|
25
|
+
} as const
|
|
26
|
+
|
|
27
|
+
export const CHART_STYLE = {
|
|
28
|
+
background: 'transparent',
|
|
29
|
+
color: 'var(--midground)',
|
|
30
|
+
fontFamily: 'var(--font-mono), monospace',
|
|
31
|
+
fontSize: '11px',
|
|
32
|
+
overflow: 'hidden'
|
|
33
|
+
} as const
|
|
34
|
+
|
|
35
|
+
export const stylePlot = (plot: HTMLElement) => {
|
|
36
|
+
plot.querySelectorAll('[aria-label*="grid"] line').forEach(el =>
|
|
37
|
+
Object.assign((el as SVGLineElement).style, {
|
|
38
|
+
stroke: 'currentColor',
|
|
39
|
+
strokeDasharray: '2,4',
|
|
40
|
+
strokeOpacity: '0.3'
|
|
41
|
+
})
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
plot.querySelectorAll('text').forEach(el =>
|
|
45
|
+
Object.assign((el as SVGTextElement).style, {
|
|
46
|
+
fontSize: '11px',
|
|
47
|
+
fontWeight: '600'
|
|
48
|
+
})
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
plot
|
|
52
|
+
.querySelectorAll('[aria-label*="label"] text')
|
|
53
|
+
.forEach(el => ((el as SVGTextElement).style.opacity = '0.4'))
|
|
54
|
+
|
|
55
|
+
const svg = plot.querySelector('svg')
|
|
56
|
+
svg && (svg.style.display = 'block')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const useDims = (ref: RefObject<HTMLElement | null>) => {
|
|
60
|
+
const [dims, setDims] = useState({ h: 0, w: 0 })
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!ref.current) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const update = () => {
|
|
68
|
+
const { height: h, width: w } = ref.current!.getBoundingClientRect()
|
|
69
|
+
const [rh, rw] = [Math.round(h), Math.round(w)]
|
|
70
|
+
|
|
71
|
+
rh &&
|
|
72
|
+
rw &&
|
|
73
|
+
setDims(st => (st.h === rh && st.w === rw ? st : { h: rh, w: rw }))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
update()
|
|
77
|
+
|
|
78
|
+
const ro = new ResizeObserver(update)
|
|
79
|
+
ro.observe(ref.current)
|
|
80
|
+
|
|
81
|
+
return () => ro.disconnect()
|
|
82
|
+
}, [ref])
|
|
83
|
+
|
|
84
|
+
return dims
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const Crosshair = ({
|
|
88
|
+
color = 'var(--foreground)',
|
|
89
|
+
containerWidth,
|
|
90
|
+
height,
|
|
91
|
+
points,
|
|
92
|
+
x
|
|
93
|
+
}: CrosshairState & {
|
|
94
|
+
color?: string
|
|
95
|
+
containerWidth: number
|
|
96
|
+
height: number
|
|
97
|
+
}) => {
|
|
98
|
+
if (x === null) {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const nearRight = x > containerWidth * 0.7
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<>
|
|
106
|
+
<div
|
|
107
|
+
className="pointer-events-none absolute top-0 w-px"
|
|
108
|
+
style={{ background: color, height, left: x, opacity: 0.4 }}
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
{points?.map((pt, i) => (
|
|
112
|
+
<div
|
|
113
|
+
className="pointer-events-none absolute size-2 -translate-x-1/2 -translate-y-1/2 rounded-full"
|
|
114
|
+
key={i}
|
|
115
|
+
style={{ background: color, left: x, top: pt.dotY }}
|
|
116
|
+
/>
|
|
117
|
+
))}
|
|
118
|
+
|
|
119
|
+
{points?.map((pt, i) => (
|
|
120
|
+
<div
|
|
121
|
+
className="tooltip absolute -translate-y-1/2"
|
|
122
|
+
key={i}
|
|
123
|
+
style={{
|
|
124
|
+
left: nearRight ? undefined : x + 12,
|
|
125
|
+
right: nearRight ? containerWidth - x + 12 : undefined,
|
|
126
|
+
top: pt.dotY
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
{pt.tooltip}
|
|
130
|
+
</div>
|
|
131
|
+
))}
|
|
132
|
+
</>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const setupCrosshair = <T extends DataPoint>(
|
|
137
|
+
container: HTMLElement,
|
|
138
|
+
data: T[],
|
|
139
|
+
getX: (d: T) => number,
|
|
140
|
+
getY: (d: T) => number,
|
|
141
|
+
yDomain: [number, number],
|
|
142
|
+
formatTooltip: (d: T) => string,
|
|
143
|
+
onUpdate: (state: CrosshairState) => void,
|
|
144
|
+
getZ?: (d: T) => unknown
|
|
145
|
+
) => {
|
|
146
|
+
if (!data.length) {
|
|
147
|
+
return () => {}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const { marginBottom, marginLeft, marginRight, marginTop } = CHART_MARGINS
|
|
151
|
+
|
|
152
|
+
const seriesMap = data.reduce((m, d) => {
|
|
153
|
+
const key = getZ?.(d) ?? '__single__'
|
|
154
|
+
m.set(key, [...(m.get(key) ?? []), d])
|
|
155
|
+
|
|
156
|
+
return m
|
|
157
|
+
}, new Map<unknown, T[]>())
|
|
158
|
+
|
|
159
|
+
const sortedSeries = [...seriesMap.values()].map(s =>
|
|
160
|
+
[...s].sort((a, b) => getX(a) - getX(b))
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
const allX = data.map(getX)
|
|
164
|
+
const [xMin, xMax] = [Math.min(...allX), Math.max(...allX)]
|
|
165
|
+
|
|
166
|
+
const onMove = (e: MouseEvent) => {
|
|
167
|
+
const rect = container.getBoundingClientRect()
|
|
168
|
+
const [localX, localY] = [e.clientX - rect.left, e.clientY - rect.top]
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
localX < 0 ||
|
|
172
|
+
localX > rect.width ||
|
|
173
|
+
localY < 0 ||
|
|
174
|
+
localY > rect.height
|
|
175
|
+
) {
|
|
176
|
+
return onUpdate({ x: null })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const [chartLeft, chartRight] = [marginLeft, rect.width - marginRight]
|
|
180
|
+
const [chartTop, chartBottom] = [marginTop, rect.height - marginBottom]
|
|
181
|
+
|
|
182
|
+
if (localX < chartLeft || localX > chartRight) {
|
|
183
|
+
return onUpdate({ x: null })
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const pct = (localX - chartLeft) / (chartRight - chartLeft)
|
|
187
|
+
const xVal = xMin + pct * (xMax - xMin)
|
|
188
|
+
|
|
189
|
+
const points = sortedSeries.map(sorted => {
|
|
190
|
+
const idx = sorted.findIndex(d => getX(d) >= xVal)
|
|
191
|
+
|
|
192
|
+
const [closest, yVal] =
|
|
193
|
+
idx <= 0
|
|
194
|
+
? [sorted[0], getY(sorted[0])]
|
|
195
|
+
: idx >= sorted.length
|
|
196
|
+
? [sorted.at(-1)!, getY(sorted.at(-1)!)]
|
|
197
|
+
: (() => {
|
|
198
|
+
const [left, right] = [sorted[idx - 1], sorted[idx]]
|
|
199
|
+
const t = (xVal - getX(left)) / (getX(right) - getX(left))
|
|
200
|
+
|
|
201
|
+
return [
|
|
202
|
+
t < 0.5 ? left : right,
|
|
203
|
+
getY(left) + t * (getY(right) - getY(left))
|
|
204
|
+
] as const
|
|
205
|
+
})()
|
|
206
|
+
|
|
207
|
+
const yPct = (yVal - yDomain[0]) / (yDomain[1] - yDomain[0])
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
dotY: chartBottom - yPct * (chartBottom - chartTop),
|
|
211
|
+
tooltip: formatTooltip(closest)
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
onUpdate({ points, x: localX })
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
document.addEventListener('mousemove', onMove)
|
|
219
|
+
|
|
220
|
+
return () => document.removeEventListener('mousemove', onMove)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export const withChartBlend = <P extends BlendModeProps>(
|
|
224
|
+
Component: ComponentType<P>
|
|
225
|
+
) => {
|
|
226
|
+
const Wrapped = (props: Omit<P, keyof BlendColors>) => {
|
|
227
|
+
const { color } = useSmoothControls(
|
|
228
|
+
'Charts',
|
|
229
|
+
{ color: { value: '#709fea' } },
|
|
230
|
+
{ collapsed: true }
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return <Component {...(props as P)} color={color} />
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
Wrapped.displayName = `withChartBlend(${Component.displayName ?? Component.name})`
|
|
237
|
+
|
|
238
|
+
return Wrapped
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export type DataPoint = Record<string, unknown>
|
|
242
|
+
|
|
243
|
+
export interface CrosshairPoint {
|
|
244
|
+
dotY: number
|
|
245
|
+
tooltip: string
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface CrosshairState {
|
|
249
|
+
points?: CrosshairPoint[]
|
|
250
|
+
x: null | number
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface ChartProps<T = DataPoint>
|
|
254
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
255
|
+
data?: T[]
|
|
256
|
+
formatTooltip?: (d: T) => string
|
|
257
|
+
formatX?: (v: unknown) => string
|
|
258
|
+
formatY?: (v: number) => string
|
|
259
|
+
height?: number
|
|
260
|
+
x?: ((d: T) => unknown) | keyof T
|
|
261
|
+
xTicks?: number | number[]
|
|
262
|
+
y?: ((d: T) => number) | keyof T
|
|
263
|
+
yDomain?: [number, number]
|
|
264
|
+
yTicks?: number | number[]
|
|
265
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
.g {
|
|
2
|
+
@apply grid grid-cols-1 border-l border-current/20;
|
|
3
|
+
|
|
4
|
+
&:has(> .gc:nth-child(3)):not(:has(> .gc:nth-child(4))) > .gc:nth-child(2) {
|
|
5
|
+
@apply -order-1;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@media (width >= 40rem) {
|
|
9
|
+
&:has(> .gc:nth-child(2)) {
|
|
10
|
+
@apply grid-cols-2;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&:has(> .gc:nth-child(3)):not(:has(> .gc:nth-child(4)))
|
|
14
|
+
> .gc:nth-child(2) {
|
|
15
|
+
@apply -order-1 col-span-full;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@media (width >= 64rem) {
|
|
20
|
+
&:has(> .gc:nth-child(3)) {
|
|
21
|
+
@apply grid-cols-3;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&:has(> .gc:nth-child(4)) {
|
|
25
|
+
@apply grid-cols-4;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&:has(> .gc:nth-child(5)) {
|
|
29
|
+
@apply grid-cols-5;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&:has(> .gc:nth-child(6)) {
|
|
33
|
+
@apply grid-cols-6;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&:has(> .gc:nth-child(3)):not(:has(> .gc:nth-child(4))) {
|
|
37
|
+
grid-template-columns: 1fr 2fr 1fr;
|
|
38
|
+
|
|
39
|
+
> .gc:nth-child(2) {
|
|
40
|
+
@apply order-none col-auto;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
&:last-child {
|
|
46
|
+
@apply border-b;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&:first-child,
|
|
50
|
+
+ .g > .gc,
|
|
51
|
+
> .g {
|
|
52
|
+
@apply border-t;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.g {
|
|
56
|
+
@apply border-l-0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
&:not(:last-child) .gc:last-child {
|
|
60
|
+
@apply border-b-0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.gc {
|
|
64
|
+
min-width: 0;
|
|
65
|
+
@apply border-r border-inherit p-4;
|
|
66
|
+
|
|
67
|
+
&:has(> .gc) {
|
|
68
|
+
@apply p-0;
|
|
69
|
+
|
|
70
|
+
> .gc {
|
|
71
|
+
@apply border-r-0 not-first:border-t last:border-b-0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.post.gc {
|
|
77
|
+
@apply lg:p-12;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createElement } from 'react'
|
|
2
|
+
|
|
3
|
+
import { cn, polyRef } from '../../../utils'
|
|
4
|
+
|
|
5
|
+
export const Grid = polyRef<'div'>(({ as, className, ...rest }, ref) =>
|
|
6
|
+
createElement((as ?? 'div') as React.ElementType, {
|
|
7
|
+
...rest,
|
|
8
|
+
className: cn('g', className),
|
|
9
|
+
ref
|
|
10
|
+
})
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
export const Cell = polyRef<'div'>(({ as, className, ...rest }, ref) =>
|
|
14
|
+
createElement((as ?? 'div') as React.ElementType, {
|
|
15
|
+
...rest,
|
|
16
|
+
className: cn('gc', className),
|
|
17
|
+
ref
|
|
18
|
+
})
|
|
19
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
|
|
3
|
+
import { HoverBg } from './hover-bg'
|
|
4
|
+
import { Typography } from './typography'
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
component: HoverBg,
|
|
8
|
+
title: 'Components/HoverBg'
|
|
9
|
+
} satisfies Meta<typeof HoverBg>
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
|
|
13
|
+
type Story = StoryObj<typeof meta>
|
|
14
|
+
|
|
15
|
+
export const Row: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<div className="flex gap-2">
|
|
18
|
+
{['Alpha', 'Beta', 'Gamma'].map(label => (
|
|
19
|
+
<div
|
|
20
|
+
className="group relative flex items-center justify-center px-6 py-3"
|
|
21
|
+
key={label}
|
|
22
|
+
>
|
|
23
|
+
<Typography mono>{label}</Typography>
|
|
24
|
+
<HoverBg />
|
|
25
|
+
</div>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createElement } from 'react'
|
|
2
|
+
|
|
3
|
+
import { cn, polyRef } from '../../utils'
|
|
4
|
+
|
|
5
|
+
export const HoverBg = polyRef<'span'>(({ as, className, ...rest }, ref) =>
|
|
6
|
+
createElement((as ?? 'span') as React.ElementType, {
|
|
7
|
+
...rest,
|
|
8
|
+
className: cn(
|
|
9
|
+
'absolute inset-1 bg-midground pointer-events-none',
|
|
10
|
+
'opacity-5 transition-opacity duration-250 group-hover:opacity-5 opacity-0 group-hover:duration-0',
|
|
11
|
+
className
|
|
12
|
+
),
|
|
13
|
+
ref
|
|
14
|
+
})
|
|
15
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { SVGProps } from 'react'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../../utils'
|
|
4
|
+
|
|
5
|
+
export function ArrowIcon({
|
|
6
|
+
className,
|
|
7
|
+
direction = 'down',
|
|
8
|
+
...props
|
|
9
|
+
}: ArrowIconProps) {
|
|
10
|
+
return (
|
|
11
|
+
<svg
|
|
12
|
+
className={cn(
|
|
13
|
+
direction === 'up' && 'rotate-180',
|
|
14
|
+
direction === 'left' && 'rotate-90',
|
|
15
|
+
direction === 'right' && '-rotate-90',
|
|
16
|
+
'origin-center',
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
fill="none"
|
|
20
|
+
viewBox="0 0 13 15"
|
|
21
|
+
{...props}
|
|
22
|
+
>
|
|
23
|
+
<path
|
|
24
|
+
clipRule="evenodd"
|
|
25
|
+
d="M5 15V0h2.50075v15z"
|
|
26
|
+
fill="currentColor"
|
|
27
|
+
fillRule="evenodd"
|
|
28
|
+
/>
|
|
29
|
+
|
|
30
|
+
<path
|
|
31
|
+
clipRule="evenodd"
|
|
32
|
+
d="M10 12.5007H2.5V9.99998H10zM12.4976 9.99951H9.99805v-2.4996h2.49955zM2.4996 9.99951H0v-2.4996h2.4996z"
|
|
33
|
+
fill="currentColor"
|
|
34
|
+
fillRule="evenodd"
|
|
35
|
+
/>
|
|
36
|
+
</svg>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ArrowIconProps extends SVGProps<SVGSVGElement> {
|
|
41
|
+
direction?: 'down' | 'left' | 'right' | 'up'
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SVGProps } from 'react'
|
|
2
|
+
|
|
3
|
+
export function CheckIcon(props: SVGProps<SVGSVGElement>) {
|
|
4
|
+
return (
|
|
5
|
+
<svg fill="none" viewBox="0 0 12 12" {...props}>
|
|
6
|
+
<path
|
|
7
|
+
clipRule="evenodd"
|
|
8
|
+
d="M10.28 2.22a.75.75 0 0 1 0 1.06l-5.25 5.25a.75.75 0 0 1-1.06 0L1.72 6.28a.75.75 0 1 1 1.06-1.06L4.5 6.94l4.72-4.72a.75.75 0 0 1 1.06 0Z"
|
|
9
|
+
fill="currentColor"
|
|
10
|
+
fillRule="evenodd"
|
|
11
|
+
/>
|
|
12
|
+
</svg>
|
|
13
|
+
)
|
|
14
|
+
}
|