@inferencesh/ui 0.0.0-stage → 0.1.2
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/LICENSE +22 -0
- package/README.md +68 -2
- package/components.json +22 -0
- package/package.json +231 -4
- package/src/components/audio-player.tsx +549 -0
- package/src/components/button.tsx +13 -0
- package/src/components/chart.tsx +403 -0
- package/src/components/circuit.tsx +276 -0
- package/src/components/code-block/code-block.tsx +260 -0
- package/src/components/code-block/index.ts +18 -0
- package/src/components/code-block/languages.ts +311 -0
- package/src/components/code-block/styles.ts +39 -0
- package/src/components/code-block/tokenizer.ts +290 -0
- package/src/components/code-block/types.ts +38 -0
- package/src/components/code-block/utils.ts +39 -0
- package/src/components/code.tsx +48 -0
- package/src/components/cookie-consent.tsx +137 -0
- package/src/components/copy-button.tsx +44 -0
- package/src/components/country-dropdown.tsx +188 -0
- package/src/components/glow-loader-fast.tsx +32 -0
- package/src/components/glow-loader-mono.tsx +33 -0
- package/src/components/glow-loader-sm-fast.tsx +32 -0
- package/src/components/glow-loader-sm-mono.tsx +34 -0
- package/src/components/glow-loader-sm.tsx +32 -0
- package/src/components/glow-loader.tsx +32 -0
- package/src/components/glow-logo.tsx +32 -0
- package/src/components/gradient.tsx +80 -0
- package/src/components/index.ts +45 -0
- package/src/components/inline-sidebar.tsx +351 -0
- package/src/components/input-color.tsx +349 -0
- package/src/components/logo-base.tsx +213 -0
- package/src/components/logo-color.tsx +23 -0
- package/src/components/logo-full.tsx +71 -0
- package/src/components/logo-svg.tsx +23 -0
- package/src/components/menu-navigation.tsx +359 -0
- package/src/components/multi-select.tsx +721 -0
- package/src/components/pastel-gradient.tsx +25 -0
- package/src/components/pixel-symmetry.tsx +151 -0
- package/src/components/progress.tsx +28 -0
- package/src/components/scroll-more.tsx +131 -0
- package/src/components/select.tsx +85 -0
- package/src/components/shimmering-text.tsx +108 -0
- package/src/components/sidebar-light.tsx +102 -0
- package/src/components/sonner.tsx +38 -0
- package/src/components/theme-switcher-sidebar.tsx +56 -0
- package/src/components/theme-switcher-simple.tsx +110 -0
- package/src/components/theme-switcher.tsx +47 -0
- package/src/components/toggle-group.tsx +55 -0
- package/src/components/tooltip-button.tsx +91 -0
- package/src/components/tooltip.tsx +71 -0
- package/src/components/tree-view.tsx +499 -0
- package/src/components/unglow-loader.tsx +33 -0
- package/src/components/waveform.tsx +1632 -0
- package/src/components/zoomable-image.tsx +76 -0
- package/src/docs/api-docs.tsx +763 -0
- package/src/docs/callout.tsx +160 -0
- package/src/docs/code-group.tsx +215 -0
- package/src/docs/copy-menu.tsx +143 -0
- package/src/docs/doc-tabs.tsx +84 -0
- package/src/docs/index.ts +16 -0
- package/src/docs/steps.tsx +105 -0
- package/src/docs/table-of-contents.tsx +174 -0
- package/src/examples/guide-markdown.ts +129 -0
- package/src/examples/index.ts +1153 -0
- package/src/global.d.ts +1 -0
- package/src/hooks/use-copy-to-clipboard.ts +36 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/lib/display-name.ts +15 -0
- package/src/lib/example-input.ts +130 -0
- package/src/lib/plans.ts +44 -0
- package/src/lib/safe-content.ts +137 -0
- package/src/lib/schema.ts +320 -0
- package/src/lib/storage.ts +108 -0
- package/src/markdown/docs-markdown-renderer.tsx +518 -0
- package/src/markdown/helpers.ts +95 -0
- package/src/markdown/index.ts +24 -0
- package/src/markdown/markdown-renderer.tsx +232 -0
- package/src/markdown/youtube-embed.tsx +43 -0
- package/src/og/image.ts +312 -0
- package/src/og/index.tsx +430 -0
- package/src/og/query.ts +26 -0
- package/src/shadcn/accordion.tsx +71 -0
- package/src/shadcn/alert-dialog.tsx +187 -0
- package/src/shadcn/alert.tsx +75 -0
- package/src/shadcn/aspect-ratio.tsx +22 -0
- package/src/shadcn/attachment.tsx +207 -0
- package/src/shadcn/avatar.tsx +106 -0
- package/src/shadcn/badge.tsx +51 -0
- package/src/shadcn/breadcrumb.tsx +124 -0
- package/src/shadcn/bubble.tsx +127 -0
- package/src/shadcn/button-group.tsx +87 -0
- package/src/shadcn/button.tsx +57 -0
- package/src/shadcn/calendar.tsx +219 -0
- package/src/shadcn/card.tsx +102 -0
- package/src/shadcn/carousel.tsx +242 -0
- package/src/shadcn/chart.tsx +372 -0
- package/src/shadcn/checkbox.tsx +28 -0
- package/src/shadcn/collapsible.tsx +19 -0
- package/src/shadcn/combobox.tsx +297 -0
- package/src/shadcn/command.tsx +194 -0
- package/src/shadcn/context-menu.tsx +270 -0
- package/src/shadcn/dialog.tsx +158 -0
- package/src/shadcn/direction.tsx +4 -0
- package/src/shadcn/drawer.tsx +227 -0
- package/src/shadcn/dropdown-menu.tsx +265 -0
- package/src/shadcn/empty.tsx +103 -0
- package/src/shadcn/field.tsx +238 -0
- package/src/shadcn/form.tsx +173 -0
- package/src/shadcn/hover-card.tsx +50 -0
- package/src/shadcn/index.ts +53 -0
- package/src/shadcn/input-group.tsx +156 -0
- package/src/shadcn/input-otp.tsx +84 -0
- package/src/shadcn/input.tsx +19 -0
- package/src/shadcn/item.tsx +201 -0
- package/src/shadcn/kbd.tsx +26 -0
- package/src/shadcn/label.tsx +19 -0
- package/src/shadcn/marker.tsx +70 -0
- package/src/shadcn/menubar.tsx +278 -0
- package/src/shadcn/message-scroller.tsx +131 -0
- package/src/shadcn/message.tsx +91 -0
- package/src/shadcn/native-select.tsx +60 -0
- package/src/shadcn/navigation-menu.tsx +167 -0
- package/src/shadcn/pagination.tsx +130 -0
- package/src/shadcn/popover.tsx +87 -0
- package/src/shadcn/progress.tsx +82 -0
- package/src/shadcn/radio-group.tsx +35 -0
- package/src/shadcn/resizable.tsx +49 -0
- package/src/shadcn/scroll-area.tsx +52 -0
- package/src/shadcn/select.tsx +200 -0
- package/src/shadcn/separator.tsx +22 -0
- package/src/shadcn/sheet.tsx +136 -0
- package/src/shadcn/sidebar.tsx +721 -0
- package/src/shadcn/skeleton.tsx +13 -0
- package/src/shadcn/slider.tsx +51 -0
- package/src/shadcn/sonner.tsx +49 -0
- package/src/shadcn/spinner.tsx +10 -0
- package/src/shadcn/switch.tsx +29 -0
- package/src/shadcn/table.tsx +115 -0
- package/src/shadcn/tabs.tsx +79 -0
- package/src/shadcn/textarea.tsx +17 -0
- package/src/shadcn/toggle-group.tsx +89 -0
- package/src/shadcn/toggle.tsx +44 -0
- package/src/shadcn/tooltip.tsx +63 -0
- package/src/theme/config.ts +48 -0
- package/src/theme/css/base.css +92 -0
- package/src/theme/css/bitboy.css +75 -0
- package/src/theme/css/dark.css +47 -0
- package/src/theme/css/designer.css +122 -0
- package/src/theme/css/fonts.css +33 -0
- package/src/theme/css/globals.css +14 -0
- package/src/theme/css/hackintosh.css +57 -0
- package/src/theme/css/llama.css +103 -0
- package/src/theme/css/safelist.css +39 -0
- package/src/theme/css/shadcn.css +77 -0
- package/src/theme/css/theme.css +216 -0
- package/src/theme/embed-theme.ts +146 -0
- package/src/theme/fonts/FiraCode-VF.woff2 +0 -0
- package/src/theme/fonts/Hack-Regular.woff2 +0 -0
- package/src/theme/fonts/LICENSE-FiraCode.txt +93 -0
- package/src/theme/fonts/LICENSE-Hack.txt +45 -0
- package/src/theme/fonts/LICENSE-MonaSans.txt +93 -0
- package/src/theme/fonts/Mona-Sans.woff2 +0 -0
- package/src/theme/index.ts +15 -0
- package/src/utils/cn.ts +3 -0
- package/src/utils/index.ts +2 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as RechartsPrimitive from "recharts"
|
|
5
|
+
|
|
6
|
+
import { cn } from "cn"
|
|
7
|
+
import { safeCssColor, safeCssIdent } from "../lib/safe-content"
|
|
8
|
+
|
|
9
|
+
// Format: { THEME_NAME: CSS_SELECTOR }
|
|
10
|
+
const THEMES = { light: "", dark: ".dark" } as const
|
|
11
|
+
|
|
12
|
+
export type ChartConfig = {
|
|
13
|
+
[k in string]: {
|
|
14
|
+
label?: React.ReactNode
|
|
15
|
+
icon?: React.ComponentType
|
|
16
|
+
} & (
|
|
17
|
+
| { color?: string; theme?: never }
|
|
18
|
+
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Tooltip payload type for recharts v3
|
|
23
|
+
type TooltipPayloadItem = {
|
|
24
|
+
name?: string
|
|
25
|
+
dataKey?: string | number
|
|
26
|
+
value?: number | string
|
|
27
|
+
type?: string
|
|
28
|
+
color?: string
|
|
29
|
+
payload?: Record<string, unknown>
|
|
30
|
+
fill?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Legend payload type for recharts v3
|
|
34
|
+
type LegendPayloadItem = {
|
|
35
|
+
value?: string
|
|
36
|
+
dataKey?: string | number
|
|
37
|
+
type?: string
|
|
38
|
+
color?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type ChartContextProps = {
|
|
42
|
+
config: ChartConfig
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
|
46
|
+
|
|
47
|
+
function useChart() {
|
|
48
|
+
const context = React.useContext(ChartContext)
|
|
49
|
+
|
|
50
|
+
if (!context) {
|
|
51
|
+
throw new Error("useChart must be used within a <ChartContainer />")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return context
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ChartContainer({
|
|
58
|
+
id,
|
|
59
|
+
className,
|
|
60
|
+
children,
|
|
61
|
+
config,
|
|
62
|
+
...props
|
|
63
|
+
}: React.ComponentProps<"div"> & {
|
|
64
|
+
config: ChartConfig
|
|
65
|
+
children: React.ComponentProps<
|
|
66
|
+
typeof RechartsPrimitive.ResponsiveContainer
|
|
67
|
+
>["children"]
|
|
68
|
+
}) {
|
|
69
|
+
const uniqueId = React.useId()
|
|
70
|
+
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<ChartContext.Provider value={{ config }}>
|
|
74
|
+
<div
|
|
75
|
+
data-slot="chart"
|
|
76
|
+
data-chart={chartId}
|
|
77
|
+
className={cn(
|
|
78
|
+
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex w-full aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden [&_.recharts-responsive-container]:!flex-1 [&_.recharts-responsive-container]:!min-w-0",
|
|
79
|
+
className
|
|
80
|
+
)}
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
<ChartStyle id={chartId} config={config} />
|
|
84
|
+
<RechartsPrimitive.ResponsiveContainer
|
|
85
|
+
width="100%"
|
|
86
|
+
height="100%"
|
|
87
|
+
initialDimension={{ width: 320, height: 200 }}
|
|
88
|
+
>
|
|
89
|
+
{children}
|
|
90
|
+
</RechartsPrimitive.ResponsiveContainer>
|
|
91
|
+
</div>
|
|
92
|
+
</ChartContext.Provider>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
97
|
+
// The id, series keys and colours can come from data (an agent's chart
|
|
98
|
+
// widget): written into a <style> element unchecked, a value could close
|
|
99
|
+
// the rule and style the whole page (safe-content).
|
|
100
|
+
const colorConfig = Object.entries(config).filter(
|
|
101
|
+
([key, config]) => safeCssIdent(key) && (config.theme || config.color)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if (!colorConfig.length || !safeCssIdent(id)) {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<style
|
|
110
|
+
dangerouslySetInnerHTML={{
|
|
111
|
+
__html: Object.entries(THEMES)
|
|
112
|
+
.map(
|
|
113
|
+
([theme, prefix]) => `
|
|
114
|
+
${prefix} [data-chart=${id}] {
|
|
115
|
+
${colorConfig
|
|
116
|
+
.map(([key, itemConfig]) => {
|
|
117
|
+
const color =
|
|
118
|
+
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
|
119
|
+
itemConfig.color
|
|
120
|
+
const safe = safeCssColor(color)
|
|
121
|
+
return safe ? ` --color-${key}: ${safe};` : null
|
|
122
|
+
})
|
|
123
|
+
.join("\n")}
|
|
124
|
+
}
|
|
125
|
+
`
|
|
126
|
+
)
|
|
127
|
+
.join("\n"),
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const ChartTooltip = RechartsPrimitive.Tooltip
|
|
134
|
+
|
|
135
|
+
interface ChartTooltipContentProps extends React.ComponentProps<"div"> {
|
|
136
|
+
active?: boolean
|
|
137
|
+
payload?: TooltipPayloadItem[]
|
|
138
|
+
label?: string | number
|
|
139
|
+
labelFormatter?: (label: unknown, payload: TooltipPayloadItem[]) => React.ReactNode
|
|
140
|
+
formatter?: (
|
|
141
|
+
value: number | string,
|
|
142
|
+
name: string,
|
|
143
|
+
item: TooltipPayloadItem,
|
|
144
|
+
index: number,
|
|
145
|
+
payload: Record<string, unknown>
|
|
146
|
+
) => React.ReactNode
|
|
147
|
+
hideLabel?: boolean
|
|
148
|
+
hideIndicator?: boolean
|
|
149
|
+
indicator?: "line" | "dot" | "dashed"
|
|
150
|
+
nameKey?: string
|
|
151
|
+
labelKey?: string
|
|
152
|
+
labelClassName?: string
|
|
153
|
+
color?: string
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ChartTooltipContent({
|
|
157
|
+
active,
|
|
158
|
+
payload,
|
|
159
|
+
className,
|
|
160
|
+
indicator = "dot",
|
|
161
|
+
hideLabel = false,
|
|
162
|
+
hideIndicator = false,
|
|
163
|
+
label,
|
|
164
|
+
labelFormatter,
|
|
165
|
+
labelClassName,
|
|
166
|
+
formatter,
|
|
167
|
+
color,
|
|
168
|
+
nameKey,
|
|
169
|
+
labelKey,
|
|
170
|
+
}: ChartTooltipContentProps) {
|
|
171
|
+
const { config } = useChart()
|
|
172
|
+
|
|
173
|
+
const tooltipLabel = React.useMemo(() => {
|
|
174
|
+
if (hideLabel || !payload?.length) {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const [item] = payload
|
|
179
|
+
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
|
180
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
181
|
+
const value =
|
|
182
|
+
!labelKey && typeof label === "string"
|
|
183
|
+
? config[label as keyof typeof config]?.label || label
|
|
184
|
+
: itemConfig?.label
|
|
185
|
+
|
|
186
|
+
if (labelFormatter) {
|
|
187
|
+
return (
|
|
188
|
+
<div className={cn("font-medium", labelClassName)}>
|
|
189
|
+
{labelFormatter(value, payload)}
|
|
190
|
+
</div>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!value) {
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return <div className={cn("font-medium", labelClassName)}><span>{value}</span></div>
|
|
199
|
+
}, [
|
|
200
|
+
label,
|
|
201
|
+
labelFormatter,
|
|
202
|
+
payload,
|
|
203
|
+
hideLabel,
|
|
204
|
+
labelClassName,
|
|
205
|
+
config,
|
|
206
|
+
labelKey,
|
|
207
|
+
])
|
|
208
|
+
|
|
209
|
+
if (!active || !payload?.length) {
|
|
210
|
+
return null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const nestLabel = payload.length === 1 && indicator !== "dot"
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div
|
|
217
|
+
className={cn(
|
|
218
|
+
"border-border/50 bg-background grid w-fit items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
|
219
|
+
className
|
|
220
|
+
)}
|
|
221
|
+
>
|
|
222
|
+
{!nestLabel ? tooltipLabel : null}
|
|
223
|
+
<div className="grid gap-1.5">
|
|
224
|
+
{payload
|
|
225
|
+
.filter((item: TooltipPayloadItem) => item.type !== "none")
|
|
226
|
+
.map((item: TooltipPayloadItem, index: number) => {
|
|
227
|
+
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
|
228
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
229
|
+
const indicatorColor = color || (item.payload?.fill as string) || item.color
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div
|
|
233
|
+
key={item.dataKey}
|
|
234
|
+
className={cn(
|
|
235
|
+
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
|
236
|
+
indicator === "dot" && "items-center"
|
|
237
|
+
)}
|
|
238
|
+
>
|
|
239
|
+
{formatter && item?.value !== undefined && item.name ? (
|
|
240
|
+
formatter(item.value, item.name, item, index, item.payload || {})
|
|
241
|
+
) : (
|
|
242
|
+
<>
|
|
243
|
+
{itemConfig?.icon ? (
|
|
244
|
+
<itemConfig.icon />
|
|
245
|
+
) : (
|
|
246
|
+
!hideIndicator && (
|
|
247
|
+
<div
|
|
248
|
+
className={cn(
|
|
249
|
+
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
|
250
|
+
{
|
|
251
|
+
"h-2.5 w-2.5": indicator === "dot",
|
|
252
|
+
"w-1": indicator === "line",
|
|
253
|
+
"w-0 border-[1.5px] border-dashed bg-transparent":
|
|
254
|
+
indicator === "dashed",
|
|
255
|
+
"my-0.5": nestLabel && indicator === "dashed",
|
|
256
|
+
}
|
|
257
|
+
)}
|
|
258
|
+
style={
|
|
259
|
+
{
|
|
260
|
+
"--color-bg": indicatorColor,
|
|
261
|
+
"--color-border": indicatorColor,
|
|
262
|
+
} as React.CSSProperties
|
|
263
|
+
}
|
|
264
|
+
/>
|
|
265
|
+
)
|
|
266
|
+
)}
|
|
267
|
+
<div
|
|
268
|
+
className={cn(
|
|
269
|
+
"flex flex-1 justify-between leading-none",
|
|
270
|
+
nestLabel ? "items-end" : "items-center"
|
|
271
|
+
)}
|
|
272
|
+
>
|
|
273
|
+
<div className="grid gap-1.5">
|
|
274
|
+
{nestLabel ? tooltipLabel : null}
|
|
275
|
+
<span className="text-muted-foreground">
|
|
276
|
+
{itemConfig?.label || item.name}
|
|
277
|
+
</span>
|
|
278
|
+
</div>
|
|
279
|
+
{item.value != null && (
|
|
280
|
+
<span className="text-foreground font-mono font-medium tabular-nums">
|
|
281
|
+
{item.value.toLocaleString()}
|
|
282
|
+
</span>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
</>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
)
|
|
289
|
+
})}
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const ChartLegend = RechartsPrimitive.Legend
|
|
296
|
+
|
|
297
|
+
interface ChartLegendContentProps extends React.ComponentProps<"div"> {
|
|
298
|
+
payload?: LegendPayloadItem[]
|
|
299
|
+
verticalAlign?: "top" | "bottom" | "middle"
|
|
300
|
+
hideIcon?: boolean
|
|
301
|
+
nameKey?: string
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function ChartLegendContent({
|
|
305
|
+
className,
|
|
306
|
+
hideIcon = false,
|
|
307
|
+
payload,
|
|
308
|
+
verticalAlign = "bottom",
|
|
309
|
+
nameKey,
|
|
310
|
+
}: ChartLegendContentProps) {
|
|
311
|
+
const { config } = useChart()
|
|
312
|
+
|
|
313
|
+
if (!payload?.length) {
|
|
314
|
+
return null
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<div
|
|
319
|
+
className={cn(
|
|
320
|
+
"flex items-center justify-center gap-4",
|
|
321
|
+
verticalAlign === "top" ? "pb-3" : "pt-3",
|
|
322
|
+
className
|
|
323
|
+
)}
|
|
324
|
+
>
|
|
325
|
+
{payload
|
|
326
|
+
.filter((item: LegendPayloadItem) => item.type !== "none")
|
|
327
|
+
.map((item: LegendPayloadItem) => {
|
|
328
|
+
const key = `${nameKey || item.dataKey || "value"}`
|
|
329
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<div
|
|
333
|
+
key={item.value}
|
|
334
|
+
className={cn(
|
|
335
|
+
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
|
336
|
+
)}
|
|
337
|
+
>
|
|
338
|
+
{itemConfig?.icon && !hideIcon ? (
|
|
339
|
+
<itemConfig.icon />
|
|
340
|
+
) : (
|
|
341
|
+
<div
|
|
342
|
+
className="h-2 w-2 shrink-0 rounded-[2px]"
|
|
343
|
+
style={{
|
|
344
|
+
backgroundColor: item.color,
|
|
345
|
+
}}
|
|
346
|
+
/>
|
|
347
|
+
)}
|
|
348
|
+
{itemConfig?.label}
|
|
349
|
+
</div>
|
|
350
|
+
)
|
|
351
|
+
})}
|
|
352
|
+
</div>
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Helper to extract item config from a payload.
|
|
357
|
+
function getPayloadConfigFromPayload(
|
|
358
|
+
config: ChartConfig,
|
|
359
|
+
payload: unknown,
|
|
360
|
+
key: string
|
|
361
|
+
) {
|
|
362
|
+
if (typeof payload !== "object" || payload === null) {
|
|
363
|
+
return undefined
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const payloadPayload =
|
|
367
|
+
"payload" in payload &&
|
|
368
|
+
typeof payload.payload === "object" &&
|
|
369
|
+
payload.payload !== null
|
|
370
|
+
? payload.payload
|
|
371
|
+
: undefined
|
|
372
|
+
|
|
373
|
+
let configLabelKey: string = key
|
|
374
|
+
|
|
375
|
+
if (
|
|
376
|
+
key in payload &&
|
|
377
|
+
typeof payload[key as keyof typeof payload] === "string"
|
|
378
|
+
) {
|
|
379
|
+
configLabelKey = payload[key as keyof typeof payload] as string
|
|
380
|
+
} else if (
|
|
381
|
+
payloadPayload &&
|
|
382
|
+
key in payloadPayload &&
|
|
383
|
+
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
|
384
|
+
) {
|
|
385
|
+
configLabelKey = payloadPayload[
|
|
386
|
+
key as keyof typeof payloadPayload
|
|
387
|
+
] as string
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return configLabelKey in config
|
|
391
|
+
? config[configLabelKey]
|
|
392
|
+
: config[key as keyof typeof config]
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export {
|
|
396
|
+
ChartContext,
|
|
397
|
+
ChartContainer,
|
|
398
|
+
ChartTooltip,
|
|
399
|
+
ChartTooltipContent,
|
|
400
|
+
ChartLegend,
|
|
401
|
+
ChartLegendContent,
|
|
402
|
+
ChartStyle,
|
|
403
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
type Segment = [[number, number], [number, number]];
|
|
4
|
+
|
|
5
|
+
function linesIntersect(
|
|
6
|
+
a1: [number, number],
|
|
7
|
+
a2: [number, number],
|
|
8
|
+
b1: [number, number],
|
|
9
|
+
b2: [number, number]
|
|
10
|
+
): boolean {
|
|
11
|
+
const det = (x1: number, y1: number, x2: number, y2: number) => x1 * y2 - x2 * y1;
|
|
12
|
+
|
|
13
|
+
const [ax, ay] = a1;
|
|
14
|
+
const [bx, by] = a2;
|
|
15
|
+
const [cx, cy] = b1;
|
|
16
|
+
const [dx, dy] = b2;
|
|
17
|
+
|
|
18
|
+
const d1 = det(bx - ax, by - ay, cx - ax, cy - ay);
|
|
19
|
+
const d2 = det(bx - ax, by - ay, dx - ax, dy - ay);
|
|
20
|
+
const d3 = det(dx - cx, dy - cy, ax - cx, ay - cy);
|
|
21
|
+
const d4 = det(dx - cx, dy - cy, bx - cx, by - cy);
|
|
22
|
+
|
|
23
|
+
return d1 * d2 < 0 && d3 * d4 < 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function CircuitBackground({
|
|
27
|
+
cellSize = 24,
|
|
28
|
+
strokeWidth = 4.5,
|
|
29
|
+
maxPathLength = 12,
|
|
30
|
+
minPathLength = 2,
|
|
31
|
+
attempts = 10,
|
|
32
|
+
className = "",
|
|
33
|
+
}: {
|
|
34
|
+
cellSize?: number;
|
|
35
|
+
strokeWidth?: number;
|
|
36
|
+
maxPathLength?: number;
|
|
37
|
+
minPathLength?: number;
|
|
38
|
+
attempts?: number;
|
|
39
|
+
className?: string;
|
|
40
|
+
}) {
|
|
41
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
42
|
+
const svgRef = useRef<string | null>(null);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!containerRef.current || svgRef.current) return;
|
|
46
|
+
|
|
47
|
+
const container = containerRef.current;
|
|
48
|
+
const { offsetWidth, offsetHeight } = container;
|
|
49
|
+
const width = offsetWidth + cellSize * 4
|
|
50
|
+
const height = offsetHeight + cellSize * 4
|
|
51
|
+
const cols = Math.floor(width / cellSize) + 2;
|
|
52
|
+
const rows = Math.floor(height / cellSize) + 2;
|
|
53
|
+
|
|
54
|
+
const colors = ["#FF569D", "#5415ff", "#FFF14C", "#00D08A"];
|
|
55
|
+
const xmlns = "http://www.w3.org/2000/svg";
|
|
56
|
+
const svg: string[] = [
|
|
57
|
+
`<svg xmlns="${xmlns}" width="${width}" height="${height}" viewBox="${cellSize} ${cellSize} ${width} ${height}" fill="none">`,
|
|
58
|
+
`<rect width="200%" height="200%" fill="hsl(0 0% 5.9%)"/>`,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const visited = new Set<string>();
|
|
62
|
+
const segments: Segment[] = [];
|
|
63
|
+
|
|
64
|
+
const isInside = (x: number, y: number) => x >= 0 && x <= cols && y >= 0 && y <= rows;
|
|
65
|
+
const vecKey = ([dx, dy]: number[]) => `${dx},${dy}`;
|
|
66
|
+
const pointKey = (x: number, y: number) => `${x},${y}`;
|
|
67
|
+
|
|
68
|
+
const getTurnOptions = ([dx, dy]: number[]): number[][] => {
|
|
69
|
+
const norm = (n: number) => (n < 0 ? -1 : n > 0 ? 1 : 0);
|
|
70
|
+
dx = norm(dx);
|
|
71
|
+
dy = norm(dy);
|
|
72
|
+
|
|
73
|
+
if (dx === 0 && dy === 0) return [];
|
|
74
|
+
|
|
75
|
+
const options: [number, number][] = [];
|
|
76
|
+
|
|
77
|
+
if (dx === 0 || dy === 0) {
|
|
78
|
+
// orthogonal: yield (straight + 2 diagonals)
|
|
79
|
+
options.push([dx, dy]);
|
|
80
|
+
options.push([dx + dy, dy + dx]);
|
|
81
|
+
options.push([dx - dy, dy - dx]);
|
|
82
|
+
} else {
|
|
83
|
+
// diagonal: yield (straight + x-only + y-only)
|
|
84
|
+
options.push([dx, dy]);
|
|
85
|
+
options.push([dx, 0]);
|
|
86
|
+
options.push([0, dy]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return options.filter(([x, y]) =>
|
|
90
|
+
Math.abs(x) <= 1 && Math.abs(y) <= 1 &&
|
|
91
|
+
!(x === 0 && y === 0)
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const directions: number[][] = [
|
|
96
|
+
[1, 0], [-1, 0], [0, 1], [0, -1],
|
|
97
|
+
[1, 1], [-1, 1], [1, -1], [-1, -1],
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const randomInt = (n: number) => Math.floor(Math.random() * n);
|
|
101
|
+
const midpoint = (a: number, b: number) => (a + b) / 2;
|
|
102
|
+
|
|
103
|
+
const realAttempts = cols * rows * attempts;
|
|
104
|
+
for (let i = 0; i < realAttempts; i++) {
|
|
105
|
+
const x = randomInt(cols);
|
|
106
|
+
const y = randomInt(rows);
|
|
107
|
+
const startKey = pointKey(x, y);
|
|
108
|
+
if (visited.has(startKey)) continue;
|
|
109
|
+
|
|
110
|
+
let color = colors[randomInt(colors.length)];
|
|
111
|
+
const hex = color.slice(1);
|
|
112
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
113
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
114
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
115
|
+
const brightness = 0.7 + randomInt(30) / 100; // brightness between 0.5 and 1.0
|
|
116
|
+
const brightR = Math.round(r * brightness);
|
|
117
|
+
const brightG = Math.round(g * brightness);
|
|
118
|
+
const brightB = Math.round(b * brightness);
|
|
119
|
+
color = `rgb(${brightR}, ${brightG}, ${brightB})`;
|
|
120
|
+
|
|
121
|
+
const path: [number, number][] = [[x, y]];
|
|
122
|
+
visited.add(pointKey(x, y));
|
|
123
|
+
let head = [x, y];
|
|
124
|
+
let tail = [x, y];
|
|
125
|
+
let headDir: [number, number] | null = null;
|
|
126
|
+
let tailDir: [number, number] | null = null;
|
|
127
|
+
|
|
128
|
+
const getDirection = (path: [number, number][], fromHead: boolean): [number, number] | null => {
|
|
129
|
+
if (fromHead && path.length >= 2) {
|
|
130
|
+
const len = path.length;
|
|
131
|
+
const [x1, y1] = path[len - 2];
|
|
132
|
+
const [x2, y2] = path[len - 1];
|
|
133
|
+
return [x2 - x1, y2 - y1];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!fromHead && path.length >= 2) {
|
|
137
|
+
const [x1, y1] = path[0];
|
|
138
|
+
const [x2, y2] = path[1];
|
|
139
|
+
return [x1 - x2, y1 - y2];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const grow = (fromHead: boolean) => {
|
|
146
|
+
const origin = fromHead ? head : tail;
|
|
147
|
+
let dir = fromHead ? headDir : tailDir;
|
|
148
|
+
|
|
149
|
+
if (!dir) {
|
|
150
|
+
if (fromHead && path.length === 1) {
|
|
151
|
+
for (const d of directions.sort(() => 0.5 - Math.random())) {
|
|
152
|
+
const [dx, dy] = d;
|
|
153
|
+
const nx = origin[0] + dx;
|
|
154
|
+
const ny = origin[1] + dy;
|
|
155
|
+
const point = pointKey(nx, ny);
|
|
156
|
+
if (!isInside(nx, ny) || visited.has(point)) continue;
|
|
157
|
+
|
|
158
|
+
const x1 = origin[0] * cellSize + cellSize / 2;
|
|
159
|
+
const y1 = origin[1] * cellSize + cellSize / 2;
|
|
160
|
+
const x2 = nx * cellSize + cellSize / 2;
|
|
161
|
+
const y2 = ny * cellSize + cellSize / 2;
|
|
162
|
+
const intersects = segments.some(
|
|
163
|
+
([a, b]) => linesIntersect([x1, y1], [x2, y2], a, b)
|
|
164
|
+
);
|
|
165
|
+
if (intersects) continue;
|
|
166
|
+
|
|
167
|
+
segments.push([[x1, y1], [x2, y2]]);
|
|
168
|
+
visited.add(point);
|
|
169
|
+
path.push([nx, ny]);
|
|
170
|
+
head = [nx, ny];
|
|
171
|
+
headDir = [dx, dy];
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
} else if (!fromHead && path.length >= 2) {
|
|
176
|
+
dir = getDirection(path, fromHead);
|
|
177
|
+
if (!dir) return false;
|
|
178
|
+
tailDir = dir;
|
|
179
|
+
} else {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const options = getTurnOptions(dir);
|
|
185
|
+
for (const [dx, dy] of options.sort(() => 0.5 - Math.random())) {
|
|
186
|
+
const nx = origin[0] + dx;
|
|
187
|
+
const ny = origin[1] + dy;
|
|
188
|
+
const point = pointKey(nx, ny);
|
|
189
|
+
if (!isInside(nx, ny) || visited.has(point)) continue;
|
|
190
|
+
|
|
191
|
+
const x1 = origin[0] * cellSize + cellSize / 2;
|
|
192
|
+
const y1 = origin[1] * cellSize + cellSize / 2;
|
|
193
|
+
const x2 = nx * cellSize + cellSize / 2;
|
|
194
|
+
const y2 = ny * cellSize + cellSize / 2;
|
|
195
|
+
const intersects = segments.some(
|
|
196
|
+
([a, b]) => linesIntersect([x1, y1], [x2, y2], a, b)
|
|
197
|
+
);
|
|
198
|
+
if (intersects) continue;
|
|
199
|
+
|
|
200
|
+
segments.push([[x1, y1], [x2, y2]]);
|
|
201
|
+
visited.add(point);
|
|
202
|
+
|
|
203
|
+
if (fromHead) {
|
|
204
|
+
path.push([nx, ny]);
|
|
205
|
+
head = [nx, ny];
|
|
206
|
+
headDir = [dx, dy];
|
|
207
|
+
} else {
|
|
208
|
+
path.unshift([nx, ny]);
|
|
209
|
+
tail = [nx, ny];
|
|
210
|
+
tailDir = [dx, dy];
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return false;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
let headStuck = false;
|
|
219
|
+
let tailStuck = false;
|
|
220
|
+
|
|
221
|
+
while (path.length < maxPathLength && !(headStuck && tailStuck)) {
|
|
222
|
+
const fromHead = Math.random() < 0.5;
|
|
223
|
+
const grown = grow(fromHead);
|
|
224
|
+
|
|
225
|
+
if (fromHead) headStuck = !grown;
|
|
226
|
+
else tailStuck = !grown;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (path.length < minPathLength && path.length > 1) continue;
|
|
230
|
+
|
|
231
|
+
let d = "";
|
|
232
|
+
for (let j = 0; j < path.length; j++) {
|
|
233
|
+
const [gx, gy] = path[j];
|
|
234
|
+
const px = gx * cellSize + cellSize / 2;
|
|
235
|
+
const py = gy * cellSize + cellSize / 2;
|
|
236
|
+
if (j === 0) {
|
|
237
|
+
d += `M ${px} ${py}`;
|
|
238
|
+
} else {
|
|
239
|
+
d += ` L ${px} ${py}`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (path.length < minPathLength) {
|
|
244
|
+
color = "#333333";
|
|
245
|
+
}
|
|
246
|
+
svg.push(
|
|
247
|
+
`<path d="${d}" stroke="${color}" stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round" fill="none"/>`
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const circleR = strokeWidth * 1.8;
|
|
251
|
+
const dotR = strokeWidth * 0.9;
|
|
252
|
+
const [sx, sy] = path[0];
|
|
253
|
+
const [ex, ey] = path[path.length - 1];
|
|
254
|
+
const sxpx = sx * cellSize + cellSize / 2;
|
|
255
|
+
const sypx = sy * cellSize + cellSize / 2;
|
|
256
|
+
const expx = ex * cellSize + cellSize / 2;
|
|
257
|
+
const eypx = ey * cellSize + cellSize / 2;
|
|
258
|
+
|
|
259
|
+
svg.push(
|
|
260
|
+
`<circle cx="${sxpx}" cy="${sypx}" r="${circleR}" fill="${color}"/>`,
|
|
261
|
+
`<circle cx="${sxpx}" cy="${sypx}" r="${dotR}" fill="hsl(0 0% 5.9%)"/>`,
|
|
262
|
+
`<circle cx="${expx}" cy="${eypx}" r="${circleR}" fill="${color}"/>`,
|
|
263
|
+
`<circle cx="${expx}" cy="${eypx}" r="${dotR}" fill="hsl(0 0% 5.9%)"/>`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
svg.push(`</svg>`);
|
|
268
|
+
svgRef.current = svg.join("");
|
|
269
|
+
container.style.backgroundImage = `url("data:image/svg+xml;utf8,${encodeURIComponent(
|
|
270
|
+
svgRef.current
|
|
271
|
+
)}")`;
|
|
272
|
+
container.style.backgroundSize = "cover";
|
|
273
|
+
}, [cellSize, strokeWidth, maxPathLength, minPathLength, attempts]);
|
|
274
|
+
|
|
275
|
+
return <div ref={containerRef} className={className} />;
|
|
276
|
+
}
|