@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.
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,213 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ // =============================================================================
6
+ // CANONICAL SVG PATH DATA - Single source of truth for all logo variants
7
+ // =============================================================================
8
+
9
+ /** Inner triangle paths (the three internal colored strokes) */
10
+ export const INNER_PATHS = {
11
+ green: "M201.89,131.89l98.4,170.21c.77,1.33-.19,3-1.73,3H2",
12
+ purple: "M126.96,261.83v-.02l98.28-170.19c.77-1.33,2.69-1.33,3.46,0l148.31,256.81",
13
+ yellow: "M277.02,261.81H80.44c-1.54,0-2.5-1.67-1.73-3L227,2",
14
+ } as const;
15
+
16
+ /** Outer border paths (the pink/coral perimeter strokes) */
17
+ export const OUTER_PATHS = {
18
+ a: "M2,305.1l16.37,28.32c5.36,9.27,15.26,14.99,25.97,14.99l332.67.03",
19
+ b: "M227,2l-32.72.02c-10.71,0-20.61,5.72-25.96,15L2,305.1",
20
+ c: "M377.01,348.44l16.35-28.33c5.35-9.28,5.35-20.71,0-29.99L227,2",
21
+ } as const;
22
+
23
+ /** Canonical viewBox for the logo SVG */
24
+ export const LOGO_VIEWBOX = "0 0 399.37 350.44";
25
+
26
+ /** Brand colors */
27
+ export const COLORS = {
28
+ green: "#00D08A",
29
+ purple: "#5415FF",
30
+ yellow: "#FFF14C",
31
+ pink: "#FF569D",
32
+ } as const;
33
+
34
+ // =============================================================================
35
+ // LOGO BASE COMPONENT
36
+ // =============================================================================
37
+
38
+ export interface LogoBaseProps {
39
+ /** Width of the SVG (default: 240) */
40
+ width?: number;
41
+ /** Height of the SVG (default: 240) */
42
+ height?: number;
43
+ /** Additional className for the container */
44
+ className?: string;
45
+ /** Stroke width for all paths (default: 4) */
46
+ strokeWidth?: number;
47
+ /** Enable glow animation (default: true) */
48
+ animated?: boolean;
49
+ /** Animation duration in seconds (default: 2) */
50
+ animationDuration?: number;
51
+ /** Stagger delay between path animations in seconds (default: 0.05) */
52
+ animationStagger?: number;
53
+ /** Use colored strokes vs monochrome (default: true) */
54
+ colored?: boolean;
55
+ /** Custom monochrome color (CSS value, e.g. "hsl(var(--muted-foreground) / 0.2)") */
56
+ monoColor?: string;
57
+ /** Enable blur/glow effect layer (default: true) */
58
+ withGlow?: boolean;
59
+ /** Unique ID prefix for filters (auto-generated if not provided) */
60
+ filterId?: string;
61
+ /** Announces the logo as a status (e.g. "loading") to assistive tech */
62
+ label?: string;
63
+ }
64
+
65
+ const LogoBase: React.FC<LogoBaseProps> = ({
66
+ width = 240,
67
+ height = 240,
68
+ className,
69
+ strokeWidth = 4,
70
+ animated = true,
71
+ animationDuration = 2,
72
+ animationStagger = 0.02,
73
+ colored = true,
74
+ monoColor,
75
+ withGlow = true,
76
+ filterId,
77
+ label,
78
+ }) => {
79
+ // Generate unique filter ID to avoid conflicts when multiple logos are rendered
80
+ const rawId = React.useId();
81
+ const uniqueId = rawId.replace(/:/g, '');
82
+ const blurFilterId = filterId ?? `logo-blur-${uniqueId}`;
83
+
84
+ // Determine colors: mono with custom color, mono with currentColor, or full brand colors
85
+ const effectiveMonoColor = monoColor ?? "currentColor";
86
+ const innerColors = colored
87
+ ? COLORS
88
+ : { green: effectiveMonoColor, purple: effectiveMonoColor, yellow: effectiveMonoColor };
89
+ const outerColor = colored ? COLORS.pink : effectiveMonoColor;
90
+
91
+ // CSS for animations (scoped to this instance)
92
+ const styles = `
93
+ /* Base stroke styling */
94
+ .logo-outer-${uniqueId},
95
+ .logo-inner-${uniqueId} {
96
+ fill: none;
97
+ stroke-width: ${strokeWidth};
98
+ stroke-miterlimit: 100;
99
+ stroke-linecap: round;
100
+ will-change: stroke-dashoffset, stroke-dasharray;
101
+ transform: translateZ(0);
102
+ }
103
+
104
+ .logo-outer-${uniqueId} {
105
+ stroke: ${outerColor};
106
+ ${animated ? `
107
+ stroke-dasharray: 500, 500;
108
+ stroke-dashoffset: -500;
109
+ animation: logo-outside-${uniqueId} ${animationDuration}s ease-in-out infinite;
110
+ ` : ""}
111
+ }
112
+
113
+ .logo-inner-${uniqueId} {
114
+ ${animated ? `
115
+ stroke-dasharray: 500, 500;
116
+ stroke-dashoffset: -500;
117
+ animation: logo-inside-${uniqueId} ${animationDuration}s ease-in-out infinite;
118
+ ` : ""}
119
+ }
120
+
121
+ .logo-outer-b-${uniqueId} { animation-delay: ${animationStagger}s; }
122
+ .logo-outer-c-${uniqueId} { animation-delay: ${animationStagger * 2}s; }
123
+ .logo-inner-purple-${uniqueId} { animation-delay: ${animationStagger}s; }
124
+ .logo-inner-yellow-${uniqueId} { animation-delay: ${animationStagger * 2}s; }
125
+
126
+ ${animated ? `
127
+ @keyframes logo-outside-${uniqueId} {
128
+ 0% { stroke-dashoffset: -500; }
129
+ 45% { stroke-dashoffset: 0; }
130
+ 55% { stroke-dashoffset: 0; }
131
+ 100% { stroke-dashoffset: 500; }
132
+ }
133
+
134
+ @keyframes logo-inside-${uniqueId} {
135
+ 0% { stroke-dashoffset: -500; }
136
+ 45% { stroke-dashoffset: 0; }
137
+ 55% { stroke-dashoffset: 0; }
138
+ 100% { stroke-dashoffset: 500; }
139
+ }
140
+ ` : ""}
141
+
142
+ .logo-blur-group-${uniqueId} {
143
+ will-change: filter;
144
+ }
145
+
146
+ .dark .logo-blur-group-${uniqueId},
147
+ [data-theme="dark"] .logo-blur-group-${uniqueId} {
148
+ mix-blend-mode: screen;
149
+ }
150
+
151
+ .dark .logo-blend-overlay-${uniqueId},
152
+ [data-theme="dark"] .logo-blend-overlay-${uniqueId} {
153
+ mix-blend-mode: lighten;
154
+ }
155
+ `;
156
+
157
+ return (
158
+ <div className={`flex items-center justify-center ${className ?? ""}`} role={label ? "status" : undefined} aria-label={label}>
159
+ <svg
160
+ xmlns="http://www.w3.org/2000/svg"
161
+ viewBox={LOGO_VIEWBOX}
162
+ width={width}
163
+ height={height}
164
+ style={{ overflow: "visible" }}
165
+ >
166
+ <defs>
167
+ <filter id={blurFilterId}>
168
+ <feGaussianBlur stdDeviation="5" />
169
+ <feColorMatrix
170
+ type="matrix"
171
+ values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.8 0"
172
+ />
173
+ </filter>
174
+ </defs>
175
+
176
+ {/* Static base layer — always visible so the logo never goes blank */}
177
+ {animated && (
178
+ <g opacity={withGlow ? "0.15" : "1"}>
179
+ {[INNER_PATHS.green, INNER_PATHS.purple, INNER_PATHS.yellow, OUTER_PATHS.a, OUTER_PATHS.b, OUTER_PATHS.c].map((d, i) => (
180
+ <path key={i} d={d} fill="none" stroke={withGlow ? effectiveMonoColor : "hsl(var(--border))"} strokeWidth={strokeWidth} strokeMiterlimit={100} strokeLinecap="round" />
181
+ ))}
182
+ </g>
183
+ )}
184
+
185
+ {/* Glow/blur layer */}
186
+ {withGlow && (
187
+ <g filter={`url(#${blurFilterId})`} className={`logo-blur-group-${uniqueId}`}>
188
+ <path className={`logo-inner-${uniqueId}`} style={{ stroke: innerColors.green }} d={INNER_PATHS.green} />
189
+ <path className={`logo-inner-${uniqueId} logo-inner-purple-${uniqueId}`} style={{ stroke: innerColors.purple }} d={INNER_PATHS.purple} />
190
+ <path className={`logo-inner-${uniqueId} logo-inner-yellow-${uniqueId}`} style={{ stroke: innerColors.yellow }} d={INNER_PATHS.yellow} />
191
+ <path className={`logo-outer-${uniqueId}`} d={OUTER_PATHS.a} />
192
+ <path className={`logo-outer-${uniqueId} logo-outer-b-${uniqueId}`} d={OUTER_PATHS.b} />
193
+ <path className={`logo-outer-${uniqueId} logo-outer-c-${uniqueId}`} d={OUTER_PATHS.c} />
194
+ </g>
195
+ )}
196
+
197
+ {/* Main visible layer */}
198
+ <g>
199
+ <path className={`logo-inner-${uniqueId}${withGlow ? ` logo-blend-overlay-${uniqueId}` : ""}`} style={{ stroke: innerColors.green }} d={INNER_PATHS.green} />
200
+ <path className={`logo-inner-${uniqueId} logo-inner-purple-${uniqueId}${withGlow ? ` logo-blend-overlay-${uniqueId}` : ""}`} style={{ stroke: innerColors.purple }} d={INNER_PATHS.purple} />
201
+ <path className={`logo-inner-${uniqueId} logo-inner-yellow-${uniqueId}${withGlow ? ` logo-blend-overlay-${uniqueId}` : ""}`} style={{ stroke: innerColors.yellow }} d={INNER_PATHS.yellow} />
202
+ <path className={`logo-outer-${uniqueId}`} d={OUTER_PATHS.a} />
203
+ <path className={`logo-outer-${uniqueId} logo-outer-b-${uniqueId}`} d={OUTER_PATHS.b} />
204
+ <path className={`logo-outer-${uniqueId} logo-outer-c-${uniqueId}`} d={OUTER_PATHS.c} />
205
+ </g>
206
+ </svg>
207
+
208
+ <style>{styles}</style>
209
+ </div>
210
+ );
211
+ };
212
+
213
+ export default LogoBase;
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+ import LogoBase from "./logo-base";
3
+
4
+ /**
5
+ * Static colored logo (no animation).
6
+ * Used for nav headers, small brand marks, etc.
7
+ */
8
+ function LogoColor(props: React.SVGProps<SVGSVGElement> & { size?: number }) {
9
+ const { size = 18, className, ...rest } = props;
10
+ return (
11
+ <LogoBase
12
+ width={size}
13
+ height={size}
14
+ className={className}
15
+ strokeWidth={0.5}
16
+ animated={false}
17
+ colored={true}
18
+ withGlow={false}
19
+ />
20
+ );
21
+ }
22
+
23
+ export default LogoColor;
@@ -0,0 +1,71 @@
1
+ import * as React from "react"
2
+
3
+ function LogoSvg(props: React.SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg
6
+ width={18}
7
+ height={16}
8
+ viewBox="0 0 385.87 339"
9
+ fill="none"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ {...props}
12
+ >
13
+ <defs>
14
+ <style>
15
+ {`.logo-full-1{fill:url(#linear-gradient-3)}
16
+ .logo-full-2{fill:url(#linear-gradient-2)}
17
+ .logo-full-3{fill:url(#linear-gradient)}`}
18
+ </style>
19
+ <linearGradient
20
+ id="linear-gradient"
21
+ x1=".1"
22
+ y1="145.5"
23
+ x2="278.64"
24
+ y2="145.5"
25
+ gradientUnits="userSpaceOnUse"
26
+ >
27
+ <stop offset="0" stopColor="#fff14c" />
28
+ <stop offset="1" stopColor="#ff569d" />
29
+ </linearGradient>
30
+ <linearGradient
31
+ id="linear-gradient-2"
32
+ x1="235.28"
33
+ y1="411.28"
34
+ x2="95.92"
35
+ y2="169.91"
36
+ gradientUnits="userSpaceOnUse"
37
+ >
38
+ <stop offset="0" stopColor="#f08aff" />
39
+ <stop offset="1" stopColor="#5415ff" />
40
+ </linearGradient>
41
+ <linearGradient
42
+ id="linear-gradient-3"
43
+ x1="349.77"
44
+ y1="72.37"
45
+ x2="210.48"
46
+ y2="313.61"
47
+ gradientUnits="userSpaceOnUse"
48
+ >
49
+ <stop offset="0" stopColor="#fff14c" />
50
+ <stop offset="1" stopColor="#00d08a" />
51
+ </linearGradient>
52
+ </defs>
53
+ <g id="Layer_1-2">
54
+ <path
55
+ className="logo-full-3"
56
+ d="M.1,291C27.32,243.84,129.02,67.7,159.45,14.99c5.36-9.28,15.26-14.99,25.98-14.99h33.41c-32.88,56.95-126.96,219.91-140.88,244.01-.77,1.33.2,2.99,1.74,2.99h173.54l25.4,44H.1Z"
57
+ />
58
+ <path
59
+ className="logo-full-2"
60
+ d="M16.69,323.97L0,295h282.12c1.54,0,2.5-1.67,1.73-3l-86.84-150.43,25.37-43.94,42.16,73.03c33.95,58.81,73.13,126.68,97.19,168.34H42.69c-10.73,0-20.64-5.73-26-15.03Z"
61
+ />
62
+ <path
63
+ className="logo-full-1"
64
+ d="M220.64,92.63c-10.85,18.78-69.6,120.54-86.82,150.37h-50.73C119.17,180.52,187.08,62.88,222.37,1.76c24.91,43.15,72.98,126.41,113.71,196.97l45.76,79.27c5.35,9.27,5.36,20.69.01,29.97l-16.7,29c-21.16-36.64-126.27-218.71-141.06-244.33-.77-1.33-2.69-1.34-3.46,0Z"
65
+ />
66
+ </g>
67
+ </svg>
68
+ )
69
+ }
70
+
71
+ export default LogoSvg
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+ import LogoBase from "./logo-base";
3
+
4
+ /**
5
+ * Monochrome outline logo (uses currentColor, no animation).
6
+ * Adapts to text color context.
7
+ */
8
+ function LogoSvg(props: React.SVGProps<SVGSVGElement> & { size?: number, strokeWidth?: number }) {
9
+ const { size = 18, strokeWidth = 2, className, ...rest } = props;
10
+ return (
11
+ <LogoBase
12
+ width={size}
13
+ height={size}
14
+ className={className}
15
+ strokeWidth={strokeWidth}
16
+ animated={false}
17
+ colored={false}
18
+ withGlow={false}
19
+ />
20
+ );
21
+ }
22
+
23
+ export default LogoSvg;
@@ -0,0 +1,359 @@
1
+ import type { MenuDTO, MenuItem } from "@inferencesh/sdk";
2
+
3
+ /** The part of a menu the navigation reads. */
4
+ export type Menu = Pick<MenuDTO, "items">;
5
+ import { cn } from "cn";
6
+ import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
7
+ import React, { ComponentType, ReactNode, useState } from "react";
8
+
9
+ export interface LinkProps {
10
+ href: string;
11
+ className?: string;
12
+ children: ReactNode;
13
+ }
14
+
15
+ export interface MenuNavigationProps {
16
+ menu?: Menu;
17
+ basePath: string;
18
+ className?: string;
19
+ currentSlug: string;
20
+ LinkComponent: ComponentType<LinkProps>;
21
+ }
22
+
23
+ // Check if any descendant has the current slug (for auto-expanding)
24
+ function hasActiveDescendant(item: MenuItem, currentSlug: string): boolean {
25
+ if (item.slug === currentSlug) return true;
26
+ if (item.children) {
27
+ return item.children.some(child => hasActiveDescendant(child, currentSlug));
28
+ }
29
+ return false;
30
+ }
31
+
32
+ // Recursive menu item renderer for nested items
33
+ interface MenuItemRendererProps {
34
+ item: MenuItem;
35
+ basePath: string;
36
+ currentSlug: string;
37
+ LinkComponent: ComponentType<LinkProps>;
38
+ depth: number; // 0 = top level, 1 = first children, 2 = second children, etc.
39
+ }
40
+
41
+ function MenuItemRenderer({
42
+ item,
43
+ basePath,
44
+ currentSlug,
45
+ LinkComponent: Link,
46
+ depth,
47
+ }: MenuItemRendererProps) {
48
+ const hasChildren = item.children && item.children.length > 0;
49
+ const isActive = currentSlug === item.slug;
50
+ const hasActiveChild = hasChildren && hasActiveDescendant(item, currentSlug);
51
+
52
+ // Auto-expand if this item or any descendant is active
53
+ const [isExpanded, setIsExpanded] = useState(
54
+ item.expanded !== undefined ? item.expanded : hasActiveChild
55
+ );
56
+ const panelId = `menu-${React.useId()}`;
57
+
58
+ // Calculate indentation based on depth
59
+ const paddingLeft = depth === 0 ? "px-3" : depth === 1 ? "pl-3 pr-3" : "pl-6 pr-3";
60
+
61
+ // Items with children act as collapsible groups
62
+ if (hasChildren) {
63
+ return (
64
+ <li>
65
+ <button
66
+ onClick={() => setIsExpanded(!isExpanded)}
67
+ aria-expanded={isExpanded}
68
+ aria-controls={panelId}
69
+ className={cn(
70
+ "cursor-pointer w-full group flex items-center justify-between gap-2 text-sm py-2 rounded-lg transition-all duration-150 hover:bg-muted/70",
71
+ paddingLeft,
72
+ isActive
73
+ ? "bg-brand-pink/10 text-brand-pink font-medium"
74
+ : hasActiveChild
75
+ ? "text-foreground font-medium"
76
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/70"
77
+ )}
78
+ >
79
+ <span className="flex items-center gap-2">
80
+ <span
81
+ className={cn(
82
+ "w-1 h-1 rounded-full transition-all",
83
+ isActive
84
+ ? "bg-brand-pink w-1.5 h-1.5"
85
+ : hasActiveChild
86
+ ? "bg-foreground/50"
87
+ : "bg-muted-foreground/30 group-hover:bg-muted-foreground/50"
88
+ )}
89
+ />
90
+ {item.label}
91
+ </span>
92
+ <ChevronDown
93
+ className={cn(
94
+ "w-4 h-4 text-muted-foreground transition-transform duration-200",
95
+ isExpanded ? "rotate-0" : "-rotate-90"
96
+ )}
97
+ />
98
+ </button>
99
+ {/* Rendered whether or not the group is open, and hidden with
100
+ CSS when it is closed. Dropping the children from the markup
101
+ also drops them from the page a crawler sees: a page in a
102
+ closed group is then linked from nowhere but the sitemap. */}
103
+ <ul id={panelId} hidden={!isExpanded} className="mt-0.5 space-y-0.5">
104
+ {item.children!.map((child) => (
105
+ <MenuItemRenderer
106
+ key={child.id || child.slug}
107
+ item={child}
108
+ basePath={basePath}
109
+ currentSlug={currentSlug}
110
+ LinkComponent={Link}
111
+ depth={depth + 1}
112
+ />
113
+ ))}
114
+ </ul>
115
+ </li>
116
+ );
117
+ }
118
+
119
+ // Regular link item (no children)
120
+ return (
121
+ <li>
122
+ <Link
123
+ href={`${basePath}/${item.slug}`}
124
+ className={cn(
125
+ "group flex items-center gap-2 text-sm py-2 rounded-lg transition-all duration-150",
126
+ paddingLeft,
127
+ isActive
128
+ ? "bg-brand-pink/10 text-brand-pink font-medium"
129
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/70"
130
+ )}
131
+ >
132
+ <span
133
+ className={cn(
134
+ "w-1 h-1 rounded-full transition-all",
135
+ isActive
136
+ ? "bg-brand-pink w-1.5 h-1.5"
137
+ : "bg-muted-foreground/30 group-hover:bg-muted-foreground/50"
138
+ )}
139
+ />
140
+ {item.label}
141
+ </Link>
142
+ </li>
143
+ );
144
+ }
145
+
146
+ // Sidebar navigation component
147
+ export function MenuNavigation({
148
+ menu,
149
+ basePath,
150
+ className,
151
+ currentSlug,
152
+ LinkComponent: Link,
153
+ }: MenuNavigationProps) {
154
+ if (!menu?.items) return null;
155
+
156
+ return (
157
+ <nav className={cn("space-y-6", className)}>
158
+ {menu.items.map((group: MenuItem, index: number) => (
159
+ <div key={group.id || group.label}>
160
+ {group.is_group ? (
161
+ <div className="space-y-1">
162
+ <div className="font-semibold text-xs tracking-wider text-muted-foreground/70">
163
+ {group.slug ? (() => {
164
+ const isActive = currentSlug === group.slug;
165
+ return (
166
+ <Link
167
+ href={`${basePath}/${group.slug}`}
168
+ className={cn(
169
+ "group flex items-center gap-2 text-sm rounded-lg transition-all duration-150 p-2",
170
+ isActive
171
+ ? "text-brand-pink hover:bg-brand-pink/10"
172
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/70"
173
+ )}
174
+ >
175
+ {group.label}
176
+ </Link>
177
+ );
178
+ })() : (
179
+ <span>{group.label}</span>
180
+ )}
181
+
182
+ </div>
183
+ <ul className="space-y-0.5">
184
+ {group.children?.map((item: MenuItem) => (
185
+ <MenuItemRenderer
186
+ key={item.id || item.slug}
187
+ item={item}
188
+ basePath={basePath}
189
+ currentSlug={currentSlug}
190
+ LinkComponent={Link}
191
+ depth={1}
192
+ />
193
+ ))}
194
+ </ul>
195
+ </div>
196
+ ) : (
197
+ <Link
198
+ href={`${basePath}/${group.slug}`}
199
+ className={cn(
200
+ "group flex items-center gap-2 text-sm font-medium py-2 px-3 rounded-lg transition-all duration-150",
201
+ currentSlug === group.slug
202
+ ? "bg-brand-pink/10 text-brand-pink"
203
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/70"
204
+ )}
205
+ >
206
+ <span
207
+ className={cn(
208
+ "w-1.5 h-1.5 rounded-full transition-all",
209
+ currentSlug === group.slug
210
+ ? "bg-brand-pink"
211
+ : "bg-muted-foreground/30 group-hover:bg-muted-foreground/50"
212
+ )}
213
+ />
214
+ {group.label}
215
+ </Link>
216
+ )}
217
+ {/* Subtle divider between groups */}
218
+ {index < menu.items.length - 1 && (
219
+ <div className="mt-6 border-t border-border/50" />
220
+ )}
221
+ </div>
222
+ ))
223
+ }
224
+ </nav >
225
+ );
226
+ }
227
+
228
+ // Get all pages in order from menu (recursive to support any nesting depth)
229
+ function collectPagesFromItems(items: MenuItem[], pages: { slug: string; label: string }[]) {
230
+ for (const item of items) {
231
+ if (item.slug) {
232
+ pages.push({ slug: item.slug, label: item.label });
233
+ }
234
+ if (item.children && item.children.length > 0) {
235
+ collectPagesFromItems(item.children, pages);
236
+ }
237
+ }
238
+ }
239
+
240
+ function getAllPagesFromMenu(menu: Menu): { slug: string; label: string }[] {
241
+ if (!menu?.items) return [];
242
+
243
+ const pages: { slug: string; label: string }[] = [];
244
+
245
+ for (const group of menu.items) {
246
+ if (group.slug) {
247
+ pages.push({ slug: group.slug, label: group.label });
248
+ }
249
+ if (group.children && group.children.length > 0) {
250
+ collectPagesFromItems(group.children, pages);
251
+ }
252
+ }
253
+
254
+ return pages;
255
+ }
256
+
257
+ export interface PageNavigationProps {
258
+ menu?: Menu;
259
+ basePath: string;
260
+ currentSlug: string;
261
+ LinkComponent: ComponentType<LinkProps>;
262
+ }
263
+
264
+ // Prev/Next navigation component
265
+ export function PageNavigation({
266
+ menu,
267
+ basePath,
268
+ currentSlug,
269
+ LinkComponent: Link,
270
+ }: PageNavigationProps) {
271
+ if (!menu) return null;
272
+
273
+ const pages = getAllPagesFromMenu(menu);
274
+ const currentIndex = pages.findIndex((p) => p.slug === currentSlug);
275
+
276
+ const prevPage = currentIndex > 0 ? pages[currentIndex - 1] : null;
277
+ const nextPage =
278
+ currentIndex < pages.length - 1 ? pages[currentIndex + 1] : null;
279
+
280
+ if (!prevPage && !nextPage) return null;
281
+
282
+ return (
283
+ <div className="grid grid-cols-2 gap-4 pt-10 mt-10 border-t border-border">
284
+ {prevPage ? (
285
+ <Link
286
+ href={`${basePath}/${prevPage.slug}`}
287
+ className="group flex flex-col gap-2 p-4 rounded-xl border border-border hover:border-brand-pink/40 hover:bg-brand-pink/[0.02] transition-all"
288
+ >
289
+ <span className="flex items-center gap-1 text-xs text-muted-foreground">
290
+ <ChevronLeft className="w-3 h-3" />
291
+ previous
292
+ </span>
293
+ <span className="text-sm font-medium text-foreground group-hover:text-brand-pink transition-colors">
294
+ {prevPage.label}
295
+ </span>
296
+ </Link>
297
+ ) : (
298
+ <div />
299
+ )}
300
+
301
+ {nextPage ? (
302
+ <Link
303
+ href={`${basePath}/${nextPage.slug}`}
304
+ className="group flex flex-col gap-2 p-4 rounded-xl border border-border hover:border-brand-pink/40 hover:bg-brand-pink/[0.02] transition-all text-right"
305
+ >
306
+ <span className="flex items-center justify-end gap-1 text-xs text-muted-foreground">
307
+ next
308
+ <ChevronRight className="w-3 h-3" />
309
+ </span>
310
+ <span className="text-sm font-medium text-foreground group-hover:text-brand-pink transition-colors">
311
+ {nextPage.label}
312
+ </span>
313
+ </Link>
314
+ ) : (
315
+ <div />
316
+ )}
317
+ </div>
318
+ );
319
+ }
320
+
321
+ export interface MenuSidebarProps {
322
+ menu?: Menu;
323
+ basePath: string;
324
+ title?: string;
325
+ currentSlug: string;
326
+ LinkComponent: ComponentType<LinkProps>;
327
+ }
328
+
329
+ // Wrapper for sticky sidebar
330
+ export function MenuSidebar({
331
+ menu,
332
+ basePath,
333
+ title,
334
+ currentSlug,
335
+ LinkComponent: Link,
336
+ }: MenuSidebarProps) {
337
+ if (!menu) return null;
338
+
339
+ return (
340
+ <div className="sticky top-0 overflow-y-auto pr-2 -mr-2 py-2">
341
+ {title && (
342
+ <div className="px-2 mb-6">
343
+ <Link
344
+ href={basePath}
345
+ className="text-lg font-bold text-foreground hover:text-brand-pink transition-colors"
346
+ >
347
+ {title}
348
+ </Link>
349
+ </div>
350
+ )}
351
+ <MenuNavigation
352
+ menu={menu}
353
+ basePath={basePath}
354
+ currentSlug={currentSlug}
355
+ LinkComponent={Link}
356
+ />
357
+ </div>
358
+ );
359
+ }