@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,160 @@
1
+ "use client"
2
+
3
+ import { cn } from '../utils'
4
+ import {
5
+ AlertCircle,
6
+ AlertTriangle,
7
+ Info,
8
+ Lightbulb,
9
+ Rocket,
10
+ Sparkles,
11
+ Terminal,
12
+ } from 'lucide-react'
13
+ import React from 'react'
14
+
15
+ type CalloutType =
16
+ | 'note'
17
+ | 'tip'
18
+ | 'info'
19
+ | 'warning'
20
+ | 'danger'
21
+ | 'example'
22
+ | 'terminal'
23
+
24
+ interface CalloutProps {
25
+ type?: CalloutType
26
+ title?: string
27
+ children: React.ReactNode
28
+ className?: string
29
+ }
30
+
31
+ const calloutConfig: Record<
32
+ CalloutType,
33
+ {
34
+ icon: React.ComponentType<{ className?: string }>
35
+ bgClass: string
36
+ borderClass: string
37
+ iconClass: string
38
+ titleClass: string
39
+ defaultTitle: string
40
+ }
41
+ > = {
42
+ note: {
43
+ icon: Info,
44
+ bgClass: 'bg-blue-500/5',
45
+ borderClass: 'border-l-blue-500',
46
+ iconClass: 'text-blue-500',
47
+ titleClass: 'text-blue-600 dark:text-blue-400',
48
+ defaultTitle: 'Note',
49
+ },
50
+ tip: {
51
+ icon: Lightbulb,
52
+ bgClass: 'bg-emerald-500/5',
53
+ borderClass: 'border-l-emerald-500',
54
+ iconClass: 'text-emerald-500',
55
+ titleClass: 'text-emerald-600 dark:text-emerald-400',
56
+ defaultTitle: 'Tip',
57
+ },
58
+ info: {
59
+ icon: Sparkles,
60
+ bgClass: 'bg-brand-purple/5',
61
+ borderClass: 'border-l-brand-purple',
62
+ iconClass: 'text-brand-purple',
63
+ titleClass: 'text-brand-purple',
64
+ defaultTitle: 'Info',
65
+ },
66
+ warning: {
67
+ icon: AlertTriangle,
68
+ bgClass: 'bg-amber-500/5',
69
+ borderClass: 'border-l-amber-500',
70
+ iconClass: 'text-amber-500',
71
+ titleClass: 'text-amber-600 dark:text-amber-400',
72
+ defaultTitle: 'Warning',
73
+ },
74
+ danger: {
75
+ icon: AlertCircle,
76
+ bgClass: 'bg-red-500/5',
77
+ borderClass: 'border-l-red-500',
78
+ iconClass: 'text-red-500',
79
+ titleClass: 'text-red-600 dark:text-red-400',
80
+ defaultTitle: 'Danger',
81
+ },
82
+ example: {
83
+ icon: Rocket,
84
+ bgClass: 'bg-brand-pink/5',
85
+ borderClass: 'border-l-brand-pink',
86
+ iconClass: 'text-brand-pink',
87
+ titleClass: 'text-brand-pink',
88
+ defaultTitle: 'Example',
89
+ },
90
+ terminal: {
91
+ icon: Terminal,
92
+ bgClass: 'bg-zinc-500/5',
93
+ borderClass: 'border-l-zinc-500',
94
+ iconClass: 'text-zinc-500',
95
+ titleClass: 'text-zinc-600 dark:text-zinc-400',
96
+ defaultTitle: 'Terminal',
97
+ },
98
+ }
99
+
100
+ export function Callout({
101
+ type = 'note',
102
+ title,
103
+ children,
104
+ className,
105
+ }: CalloutProps) {
106
+ const config = calloutConfig[type]
107
+ const Icon = config.icon
108
+ const displayTitle = title || config.defaultTitle
109
+
110
+ return (
111
+ <div
112
+ className={cn(
113
+ 'my-6 rounded-lg border-l-4 p-4',
114
+ config.bgClass,
115
+ config.borderClass,
116
+ className
117
+ )}
118
+ >
119
+ <div className="flex items-start gap-3">
120
+ <Icon className={cn('h-5 w-5 mt-0.5 shrink-0', config.iconClass)} />
121
+ <div className="flex-1 min-w-0">
122
+ <p className={cn('font-semibold text-sm mb-1', config.titleClass)}>
123
+ {displayTitle}
124
+ </p>
125
+ <div className="text-sm text-muted-foreground [&>p]:m-0 [&>p:not(:last-child)]:mb-2">
126
+ {children}
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ )
132
+ }
133
+
134
+ // Inline badge variant for smaller callouts
135
+ export function CalloutBadge({
136
+ type = 'note',
137
+ children,
138
+ }: {
139
+ type?: CalloutType
140
+ children: React.ReactNode
141
+ }) {
142
+ const config = calloutConfig[type]
143
+ const Icon = config.icon
144
+
145
+ return (
146
+ <span
147
+ className={cn(
148
+ 'inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium',
149
+ config.bgClass,
150
+ config.titleClass
151
+ )}
152
+ >
153
+ <Icon className="h-3.5 w-3.5" />
154
+ {children}
155
+ </span>
156
+ )
157
+ }
158
+
159
+ export type { CalloutType, CalloutProps }
160
+
@@ -0,0 +1,215 @@
1
+ 'use client'
2
+
3
+ import { cn } from "cn"
4
+ import { localStore } from "../lib/storage"
5
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../shadcn/tabs"
6
+ import React, { createContext, useContext, useEffect, useState, useMemo } from 'react'
7
+ import { CodeBlock as MarkdownCodeBlock } from '../components/code-block/code-block'
8
+
9
+ // =============================================================================
10
+ // Language Sync Context
11
+ // =============================================================================
12
+
13
+ const STORAGE_KEY = 'docs-preferred-language'
14
+
15
+ type LanguageSyncContextValue = {
16
+ language: string
17
+ setLanguage: (lang: string) => void
18
+ }
19
+
20
+ const LanguageSyncContext = createContext<LanguageSyncContextValue | null>(null)
21
+
22
+ export function LanguageSyncProvider({ children }: { children: React.ReactNode }) {
23
+ const [language, setLanguageState] = useState('python')
24
+
25
+ useEffect(() => {
26
+ const stored = localStore.getItem(STORAGE_KEY)
27
+ if (stored) setLanguageState(stored)
28
+ }, [])
29
+
30
+ const setLanguage = (lang: string) => {
31
+ setLanguageState(lang)
32
+ localStore.setItem(STORAGE_KEY, lang)
33
+ }
34
+
35
+ return (
36
+ <LanguageSyncContext.Provider value={{ language, setLanguage }}>
37
+ {children}
38
+ </LanguageSyncContext.Provider>
39
+ )
40
+ }
41
+
42
+ export function useLanguageSync() {
43
+ return useContext(LanguageSyncContext)
44
+ }
45
+
46
+ // =============================================================================
47
+ // CodeGroup Component
48
+ // =============================================================================
49
+
50
+ interface CodeBlockInfo {
51
+ id: string
52
+ language: string
53
+ title: string
54
+ code: string
55
+ }
56
+
57
+ function extractCodeBlocks(children: React.ReactNode): CodeBlockInfo[] {
58
+ const blocks: CodeBlockInfo[] = []
59
+
60
+ // Helper to get text content from any node
61
+ const getTextContent = (node: React.ReactNode): string => {
62
+ if (typeof node === 'string') return node
63
+ if (typeof node === 'number') return String(node)
64
+ if (!node) return ''
65
+ if (Array.isArray(node)) return node.map(getTextContent).join('')
66
+ if (React.isValidElement(node)) {
67
+ return getTextContent((node.props as { children?: React.ReactNode }).children)
68
+ }
69
+ return ''
70
+ }
71
+
72
+ const processElement = (child: React.ReactNode) => {
73
+ if (!React.isValidElement(child)) return
74
+
75
+ // Get element type name (works for both string tags and components)
76
+ const typeName = typeof child.type === 'string'
77
+ ? child.type
78
+ : (child.type as React.ComponentType)?.displayName ||
79
+ (child.type as React.FC)?.name ||
80
+ ''
81
+
82
+ const props = child.props as Record<string, unknown>
83
+
84
+ // Handle MarkdownCodeBlock components (from react-markdown's pre handler)
85
+ if (typeName === 'MarkdownCodeBlock' || typeName.includes('CodeBlock')) {
86
+ const language = (props.language as string) || 'text'
87
+ const meta = props['data-meta'] as string | undefined
88
+ const code = getTextContent(props.children as React.ReactNode)
89
+ const title = meta || (language.charAt(0).toUpperCase() + language.slice(1))
90
+ const id = `block-${blocks.length}`
91
+ blocks.push({ id, language, title, code: code.trim() })
92
+ return
93
+ }
94
+
95
+ // Handle <pre> elements with <code> children
96
+ if (typeName === 'pre' || child.type === 'pre') {
97
+ const preChildren = React.Children.toArray(props.children as React.ReactNode)
98
+
99
+ for (const codeChild of preChildren) {
100
+ if (React.isValidElement(codeChild)) {
101
+ const codeTypeName = typeof codeChild.type === 'string'
102
+ ? codeChild.type
103
+ : (codeChild.type as React.ComponentType)?.displayName ||
104
+ (codeChild.type as React.FC)?.name ||
105
+ ''
106
+
107
+ if (codeTypeName === 'code' || codeChild.type === 'code') {
108
+ const codeProps = codeChild.props as { className?: string; children?: React.ReactNode; 'data-meta'?: string }
109
+ const className = codeProps.className || ''
110
+ const langMatch = /language-(\w+)/.exec(className)
111
+ const language = langMatch?.[1] || 'text'
112
+ const meta = codeProps['data-meta']
113
+ const code = getTextContent(codeProps.children)
114
+ const title = meta || (language.charAt(0).toUpperCase() + language.slice(1))
115
+ const id = `block-${blocks.length}`
116
+ blocks.push({ id, language, title, code: code.trim() })
117
+ }
118
+ }
119
+ }
120
+ return // Don't recurse into pre
121
+ }
122
+
123
+ // Handle divs that contain language info in className (common wrapper pattern)
124
+ if ((typeName === 'div' || child.type === 'div') && props.className) {
125
+ const className = String(props.className)
126
+ // Look for code blocks within this div
127
+ }
128
+
129
+ // Recurse into divs and other containers
130
+ if (props.children) {
131
+ React.Children.forEach(props.children as React.ReactNode, processElement)
132
+ }
133
+ }
134
+
135
+ React.Children.forEach(children, processElement)
136
+ return blocks
137
+ }
138
+
139
+ // Map common language names to canonical form
140
+ function normalizeLanguageName(lang: string): string {
141
+ const normalized = lang.toLowerCase()
142
+ if (normalized === 'typescript' || normalized === 'ts' || normalized === 'javascript' || normalized === 'js') {
143
+ return 'javascript'
144
+ }
145
+ if (normalized === 'py') return 'python'
146
+ if (normalized === 'sh' || normalized === 'shell') return 'bash'
147
+ return normalized
148
+ }
149
+
150
+ export interface CodeGroupProps {
151
+ children: React.ReactNode
152
+ className?: string
153
+ }
154
+
155
+ export function CodeGroup({ children, className }: CodeGroupProps) {
156
+ const languageSync = useLanguageSync()
157
+ const blocks = useMemo(() => extractCodeBlocks(children), [children])
158
+
159
+ // Determine which tab should be active
160
+ const [localValue, setLocalValue] = useState<string | undefined>()
161
+
162
+ // Find matching tab from synced language preference
163
+ const syncedValue = useMemo(() => {
164
+ if (!languageSync?.language) return undefined
165
+ const normalizedPref = normalizeLanguageName(languageSync.language)
166
+ const match = blocks.find(b => normalizeLanguageName(b.language) === normalizedPref)
167
+ return match?.id
168
+ }, [languageSync?.language, blocks])
169
+
170
+ const activeValue = localValue || syncedValue || blocks[0]?.id
171
+
172
+ const handleValueChange = (value: string) => {
173
+ setLocalValue(value)
174
+ // Sync the language (not the id) so other code groups on the page follow
175
+ const block = blocks.find(b => b.id === value)
176
+ if (block && languageSync) {
177
+ languageSync.setLanguage(block.language)
178
+ }
179
+ }
180
+
181
+ if (blocks.length === 0) {
182
+ return <div className={className}><span>{children}</span></div>
183
+ }
184
+
185
+ if (blocks.length === 1) {
186
+ return (
187
+ <MarkdownCodeBlock language={blocks[0].language} className={className} textSize="text-xs">
188
+ {blocks[0].code}
189
+ </MarkdownCodeBlock>
190
+ )
191
+ }
192
+
193
+ return (
194
+ <Tabs value={activeValue} onValueChange={handleValueChange} className={cn('my-6 gap-0', className)}>
195
+ <TabsList className="mb-0 rounded-b-none border-b-0">
196
+ {blocks.map((block) => (
197
+ <TabsTrigger key={block.id} value={block.id} className="text-xs">
198
+ {block.title}
199
+ </TabsTrigger>
200
+ ))}
201
+ </TabsList>
202
+ {blocks.map((block) => (
203
+ <TabsContent key={block.id} value={block.id} className="mt-0">
204
+ <MarkdownCodeBlock
205
+ language={block.language}
206
+ showHeader={false}
207
+ className="rounded-t-none border-t-0 my-0"
208
+ >
209
+ <code>{block.code}</code>
210
+ </MarkdownCodeBlock>
211
+ </TabsContent>
212
+ ))}
213
+ </Tabs>
214
+ )
215
+ }
@@ -0,0 +1,143 @@
1
+ "use client"
2
+
3
+ import { Button } from "../components/button"
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuSeparator,
9
+ DropdownMenuTrigger,
10
+ } from "../shadcn/dropdown-menu"
11
+ import { cn } from "cn"
12
+ import { Check, ChevronDown, Copy, FileText, MessageSquare, Type } from 'lucide-react'
13
+ import { useState } from 'react'
14
+ import { toast } from 'sonner'
15
+
16
+ interface CopyMenuProps {
17
+ title: string
18
+ content: string
19
+ url?: string
20
+ className?: string
21
+ }
22
+
23
+ // Convert markdown to plain text
24
+ function markdownToPlainText(markdown: string): string {
25
+ return markdown
26
+ // Remove code blocks
27
+ .replace(/```[\s\S]*?```/g, (match) => {
28
+ const code = match.replace(/```\w*\n?/g, '').replace(/```/g, '')
29
+ return code.trim()
30
+ })
31
+ // Remove inline code
32
+ .replace(/`([^`]+)`/g, '$1')
33
+ // Remove images
34
+ .replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1')
35
+ // Convert links to text
36
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
37
+ // Remove bold/italic
38
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
39
+ .replace(/\*([^*]+)\*/g, '$1')
40
+ .replace(/__([^_]+)__/g, '$1')
41
+ .replace(/_([^_]+)_/g, '$1')
42
+ // Remove headers but keep text
43
+ .replace(/^#{1,6}\s+/gm, '')
44
+ // Remove blockquotes
45
+ .replace(/^>\s+/gm, '')
46
+ // Remove horizontal rules
47
+ .replace(/^---+$/gm, '')
48
+ // Remove list markers
49
+ .replace(/^[\s]*[-*+]\s+/gm, '• ')
50
+ .replace(/^[\s]*\d+\.\s+/gm, '')
51
+ // Clean up extra whitespace
52
+ .replace(/\n{3,}/g, '\n\n')
53
+ .trim()
54
+ }
55
+
56
+ // Format content for LLMs (structured markdown with metadata)
57
+ function formatForLLM(title: string, content: string, url?: string): string {
58
+ const header = `# ${title}\n`
59
+ const urlLine = url ? `Source: ${url}\n\n` : '\n'
60
+ return header + urlLine + content
61
+ }
62
+
63
+ export function CopyMenu({ title, content, url, className }: CopyMenuProps) {
64
+ const [copied, setCopied] = useState<string | null>(null)
65
+
66
+ const handleCopy = async (type: 'page' | 'markdown' | 'plaintext') => {
67
+ let textToCopy: string
68
+
69
+ switch (type) {
70
+ case 'page':
71
+ textToCopy = formatForLLM(title, content, url)
72
+ break
73
+ case 'markdown':
74
+ textToCopy = content
75
+ break
76
+ case 'plaintext':
77
+ textToCopy = markdownToPlainText(content)
78
+ break
79
+ }
80
+
81
+ try {
82
+ await navigator.clipboard.writeText(textToCopy)
83
+ setCopied(type)
84
+ toast.success('copied to clipboard', {
85
+ description: type === 'page'
86
+ ? 'page copied with title and url'
87
+ : type === 'markdown'
88
+ ? 'markdown content copied'
89
+ : 'plain text copied',
90
+ })
91
+ setTimeout(() => setCopied(null), 2000)
92
+ } catch {
93
+ toast.error('failed to copy')
94
+ }
95
+ }
96
+
97
+ return (
98
+ <DropdownMenu>
99
+ <DropdownMenuTrigger render={<Button variant="outline" size="sm" className={cn('gap-2 text-xs text-muted-foreground hover:text-foreground cursor-pointer', className)} />}>
100
+ <Copy className="h-4 w-4" />
101
+ copy page
102
+ <ChevronDown className="h-3 w-3 opacity-50" />
103
+ </DropdownMenuTrigger>
104
+ <DropdownMenuContent align="end" className="w-56">
105
+ <DropdownMenuItem onClick={() => handleCopy('page')} className="gap-3 cursor-pointer">
106
+ <MessageSquare className="h-4 w-4 text-muted-foreground" />
107
+ <div className="flex flex-col gap-0.5">
108
+ <span className="font-medium">copy page</span>
109
+ <span className="text-xs text-muted-foreground">
110
+ copy as markdown for llms
111
+ </span>
112
+ </div>
113
+ {copied === 'page' && <Check className="h-4 w-4 ml-auto text-brand-green" />}
114
+ </DropdownMenuItem>
115
+
116
+ <DropdownMenuSeparator />
117
+
118
+ <DropdownMenuItem onClick={() => handleCopy('markdown')} className="gap-3 cursor-pointer">
119
+ <FileText className="h-4 w-4 text-muted-foreground" />
120
+ <div className="flex flex-col gap-0.5">
121
+ <span>copy as markdown</span>
122
+ <span className="text-xs text-muted-foreground">
123
+ raw markdown content
124
+ </span>
125
+ </div>
126
+ {copied === 'markdown' && <Check className="h-4 w-4 ml-auto text-brand-green" />}
127
+ </DropdownMenuItem>
128
+
129
+ <DropdownMenuItem onClick={() => handleCopy('plaintext')} className="gap-3 cursor-pointer">
130
+ <Type className="h-4 w-4 text-muted-foreground" />
131
+ <div className="flex flex-col gap-0.5">
132
+ <span>copy as plain text</span>
133
+ <span className="text-xs text-muted-foreground">
134
+ no formatting
135
+ </span>
136
+ </div>
137
+ {copied === 'plaintext' && <Check className="h-4 w-4 ml-auto text-brand-green" />}
138
+ </DropdownMenuItem>
139
+ </DropdownMenuContent>
140
+ </DropdownMenu>
141
+ )
142
+ }
143
+
@@ -0,0 +1,84 @@
1
+ 'use client'
2
+
3
+ import { cn } from "cn"
4
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../shadcn/tabs"
5
+ import React, { useMemo, useState } from 'react'
6
+
7
+ // =============================================================================
8
+ // DocsTabs Component
9
+ // =============================================================================
10
+
11
+ interface TabInfo {
12
+ title: string
13
+ children: React.ReactNode
14
+ }
15
+
16
+ function extractTabs(children: React.ReactNode): TabInfo[] {
17
+ const tabs: TabInfo[] = []
18
+
19
+ React.Children.forEach(children, (child) => {
20
+ if (!React.isValidElement(child)) return
21
+
22
+ // Check for <tab> or <Tab> elements
23
+ const type = child.type
24
+ const typeName = typeof type === 'string' ? type.toLowerCase() : ''
25
+
26
+ if (typeName === 'tab' || (typeof type === 'function' && (type as React.FC).displayName === 'DocsTab')) {
27
+ const props = child.props as { title?: string; children?: React.ReactNode }
28
+ const title = props.title || 'Tab'
29
+ tabs.push({ title, children: props.children })
30
+ }
31
+ })
32
+
33
+ return tabs
34
+ }
35
+
36
+ export interface DocsTabsProps {
37
+ children: React.ReactNode
38
+ className?: string
39
+ defaultValue?: string
40
+ }
41
+
42
+ export function DocsTabs({ children, className, defaultValue }: DocsTabsProps) {
43
+ const tabs = useMemo(() => extractTabs(children), [children])
44
+ const [value, setValue] = useState(defaultValue || tabs[0]?.title || '')
45
+
46
+ if (tabs.length === 0) {
47
+ return <div className={className}><span>{children}</span></div>
48
+ }
49
+
50
+ return (
51
+ <Tabs value={value} onValueChange={setValue} className={cn('my-6', className)}>
52
+ <TabsList>
53
+ {tabs.map((tab, i) => (
54
+ <TabsTrigger key={i} value={tab.title} className="text-sm">
55
+ {tab.title}
56
+ </TabsTrigger>
57
+ ))}
58
+ </TabsList>
59
+ {tabs.map((tab, i) => (
60
+ <TabsContent key={i} value={tab.title} className="mt-4">
61
+ <div className="prose prose-sm dark:prose-invert max-w-none">
62
+ {tab.children}
63
+ </div>
64
+ </TabsContent>
65
+ ))}
66
+ </Tabs>
67
+ )
68
+ }
69
+
70
+ // =============================================================================
71
+ // DocsTab Component (individual tab)
72
+ // =============================================================================
73
+
74
+ export interface DocsTabProps {
75
+ title: string
76
+ children: React.ReactNode
77
+ }
78
+
79
+ export function DocsTab({ children }: DocsTabProps) {
80
+ // This component is just a marker - actual rendering happens in DocsTabs
81
+ return <>{children}</>
82
+ }
83
+
84
+ DocsTab.displayName = 'DocsTab'
@@ -0,0 +1,16 @@
1
+ // Documentation rendering components
2
+ // Shared between app and web
3
+
4
+ export { TableOfContents } from './table-of-contents'
5
+ export type { TocItem } from './table-of-contents'
6
+
7
+ export { ApiDocs, DEFAULT_LANGUAGE, useApiDocsLanguage } from './api-docs'
8
+ export type { ApiDocsProps, ApiDocsApp, ApiDocsVersion, ApiDocsFormData, Language } from './api-docs'
9
+ export { Callout, CalloutBadge } from './callout'
10
+ export { CopyMenu } from './copy-menu'
11
+ export type { CalloutType, CalloutProps } from './callout'
12
+
13
+ // New docs components
14
+ export { CodeGroup, LanguageSyncProvider, useLanguageSync } from './code-group'
15
+ export { DocsTabs, DocsTab } from './doc-tabs'
16
+ export { Steps, Step } from './steps'