@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.
- 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,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'
|