@inferencesh/ui 0.0.0-stage → 0.1.1

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 (165) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +68 -2
  3. package/components.json +22 -0
  4. package/package.json +231 -4
  5. package/src/components/audio-player.tsx +549 -0
  6. package/src/components/button.tsx +13 -0
  7. package/src/components/chart.tsx +403 -0
  8. package/src/components/circuit.tsx +276 -0
  9. package/src/components/code-block/code-block.tsx +260 -0
  10. package/src/components/code-block/index.ts +18 -0
  11. package/src/components/code-block/languages.ts +311 -0
  12. package/src/components/code-block/styles.ts +39 -0
  13. package/src/components/code-block/tokenizer.ts +290 -0
  14. package/src/components/code-block/types.ts +38 -0
  15. package/src/components/code-block/utils.ts +39 -0
  16. package/src/components/code.tsx +48 -0
  17. package/src/components/cookie-consent.tsx +137 -0
  18. package/src/components/copy-button.tsx +44 -0
  19. package/src/components/country-dropdown.tsx +188 -0
  20. package/src/components/glow-loader-fast.tsx +32 -0
  21. package/src/components/glow-loader-mono.tsx +33 -0
  22. package/src/components/glow-loader-sm-fast.tsx +32 -0
  23. package/src/components/glow-loader-sm-mono.tsx +34 -0
  24. package/src/components/glow-loader-sm.tsx +32 -0
  25. package/src/components/glow-loader.tsx +32 -0
  26. package/src/components/glow-logo.tsx +32 -0
  27. package/src/components/gradient.tsx +80 -0
  28. package/src/components/index.ts +45 -0
  29. package/src/components/inline-sidebar.tsx +351 -0
  30. package/src/components/input-color.tsx +349 -0
  31. package/src/components/logo-base.tsx +213 -0
  32. package/src/components/logo-color.tsx +23 -0
  33. package/src/components/logo-full.tsx +71 -0
  34. package/src/components/logo-svg.tsx +23 -0
  35. package/src/components/menu-navigation.tsx +359 -0
  36. package/src/components/multi-select.tsx +721 -0
  37. package/src/components/pastel-gradient.tsx +25 -0
  38. package/src/components/pixel-symmetry.tsx +151 -0
  39. package/src/components/progress.tsx +28 -0
  40. package/src/components/scroll-more.tsx +131 -0
  41. package/src/components/select.tsx +85 -0
  42. package/src/components/shimmering-text.tsx +108 -0
  43. package/src/components/sidebar-light.tsx +102 -0
  44. package/src/components/sonner.tsx +38 -0
  45. package/src/components/theme-switcher-sidebar.tsx +56 -0
  46. package/src/components/theme-switcher-simple.tsx +110 -0
  47. package/src/components/theme-switcher.tsx +47 -0
  48. package/src/components/toggle-group.tsx +55 -0
  49. package/src/components/tooltip-button.tsx +91 -0
  50. package/src/components/tooltip.tsx +71 -0
  51. package/src/components/tree-view.tsx +499 -0
  52. package/src/components/unglow-loader.tsx +33 -0
  53. package/src/components/waveform.tsx +1632 -0
  54. package/src/components/zoomable-image.tsx +76 -0
  55. package/src/docs/api-docs.tsx +763 -0
  56. package/src/docs/callout.tsx +160 -0
  57. package/src/docs/code-group.tsx +215 -0
  58. package/src/docs/copy-menu.tsx +143 -0
  59. package/src/docs/doc-tabs.tsx +84 -0
  60. package/src/docs/index.ts +16 -0
  61. package/src/docs/steps.tsx +105 -0
  62. package/src/docs/table-of-contents.tsx +174 -0
  63. package/src/examples/guide-markdown.ts +129 -0
  64. package/src/examples/index.ts +1153 -0
  65. package/src/global.d.ts +1 -0
  66. package/src/hooks/use-copy-to-clipboard.ts +36 -0
  67. package/src/hooks/use-mobile.ts +19 -0
  68. package/src/lib/display-name.ts +15 -0
  69. package/src/lib/example-input.ts +130 -0
  70. package/src/lib/plans.ts +44 -0
  71. package/src/lib/safe-content.ts +137 -0
  72. package/src/lib/schema.ts +320 -0
  73. package/src/lib/storage.ts +108 -0
  74. package/src/markdown/docs-markdown-renderer.tsx +518 -0
  75. package/src/markdown/helpers.ts +95 -0
  76. package/src/markdown/index.ts +24 -0
  77. package/src/markdown/markdown-renderer.tsx +232 -0
  78. package/src/markdown/youtube-embed.tsx +43 -0
  79. package/src/og/image.ts +312 -0
  80. package/src/og/index.tsx +430 -0
  81. package/src/og/query.ts +26 -0
  82. package/src/shadcn/accordion.tsx +71 -0
  83. package/src/shadcn/alert-dialog.tsx +187 -0
  84. package/src/shadcn/alert.tsx +75 -0
  85. package/src/shadcn/aspect-ratio.tsx +22 -0
  86. package/src/shadcn/attachment.tsx +207 -0
  87. package/src/shadcn/avatar.tsx +106 -0
  88. package/src/shadcn/badge.tsx +51 -0
  89. package/src/shadcn/breadcrumb.tsx +124 -0
  90. package/src/shadcn/bubble.tsx +127 -0
  91. package/src/shadcn/button-group.tsx +87 -0
  92. package/src/shadcn/button.tsx +57 -0
  93. package/src/shadcn/calendar.tsx +219 -0
  94. package/src/shadcn/card.tsx +102 -0
  95. package/src/shadcn/carousel.tsx +242 -0
  96. package/src/shadcn/chart.tsx +372 -0
  97. package/src/shadcn/checkbox.tsx +28 -0
  98. package/src/shadcn/collapsible.tsx +19 -0
  99. package/src/shadcn/combobox.tsx +297 -0
  100. package/src/shadcn/command.tsx +194 -0
  101. package/src/shadcn/context-menu.tsx +270 -0
  102. package/src/shadcn/dialog.tsx +158 -0
  103. package/src/shadcn/direction.tsx +4 -0
  104. package/src/shadcn/drawer.tsx +227 -0
  105. package/src/shadcn/dropdown-menu.tsx +265 -0
  106. package/src/shadcn/empty.tsx +103 -0
  107. package/src/shadcn/field.tsx +238 -0
  108. package/src/shadcn/form.tsx +173 -0
  109. package/src/shadcn/hover-card.tsx +50 -0
  110. package/src/shadcn/index.ts +53 -0
  111. package/src/shadcn/input-group.tsx +156 -0
  112. package/src/shadcn/input-otp.tsx +84 -0
  113. package/src/shadcn/input.tsx +19 -0
  114. package/src/shadcn/item.tsx +201 -0
  115. package/src/shadcn/kbd.tsx +26 -0
  116. package/src/shadcn/label.tsx +19 -0
  117. package/src/shadcn/marker.tsx +70 -0
  118. package/src/shadcn/menubar.tsx +278 -0
  119. package/src/shadcn/message-scroller.tsx +131 -0
  120. package/src/shadcn/message.tsx +91 -0
  121. package/src/shadcn/native-select.tsx +60 -0
  122. package/src/shadcn/navigation-menu.tsx +167 -0
  123. package/src/shadcn/pagination.tsx +130 -0
  124. package/src/shadcn/popover.tsx +87 -0
  125. package/src/shadcn/progress.tsx +82 -0
  126. package/src/shadcn/radio-group.tsx +35 -0
  127. package/src/shadcn/resizable.tsx +49 -0
  128. package/src/shadcn/scroll-area.tsx +52 -0
  129. package/src/shadcn/select.tsx +200 -0
  130. package/src/shadcn/separator.tsx +22 -0
  131. package/src/shadcn/sheet.tsx +136 -0
  132. package/src/shadcn/sidebar.tsx +721 -0
  133. package/src/shadcn/skeleton.tsx +13 -0
  134. package/src/shadcn/slider.tsx +51 -0
  135. package/src/shadcn/sonner.tsx +49 -0
  136. package/src/shadcn/spinner.tsx +10 -0
  137. package/src/shadcn/switch.tsx +29 -0
  138. package/src/shadcn/table.tsx +115 -0
  139. package/src/shadcn/tabs.tsx +79 -0
  140. package/src/shadcn/textarea.tsx +17 -0
  141. package/src/shadcn/toggle-group.tsx +89 -0
  142. package/src/shadcn/toggle.tsx +44 -0
  143. package/src/shadcn/tooltip.tsx +63 -0
  144. package/src/theme/config.ts +48 -0
  145. package/src/theme/css/base.css +92 -0
  146. package/src/theme/css/bitboy.css +75 -0
  147. package/src/theme/css/dark.css +47 -0
  148. package/src/theme/css/designer.css +122 -0
  149. package/src/theme/css/fonts.css +33 -0
  150. package/src/theme/css/globals.css +14 -0
  151. package/src/theme/css/hackintosh.css +57 -0
  152. package/src/theme/css/llama.css +103 -0
  153. package/src/theme/css/safelist.css +39 -0
  154. package/src/theme/css/shadcn.css +77 -0
  155. package/src/theme/css/theme.css +216 -0
  156. package/src/theme/embed-theme.ts +146 -0
  157. package/src/theme/fonts/FiraCode-VF.woff2 +0 -0
  158. package/src/theme/fonts/Hack-Regular.woff2 +0 -0
  159. package/src/theme/fonts/LICENSE-FiraCode.txt +93 -0
  160. package/src/theme/fonts/LICENSE-Hack.txt +45 -0
  161. package/src/theme/fonts/LICENSE-MonaSans.txt +93 -0
  162. package/src/theme/fonts/Mona-Sans.woff2 +0 -0
  163. package/src/theme/index.ts +15 -0
  164. package/src/utils/cn.ts +3 -0
  165. 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
+ }