@sntlr/registry-shell 1.0.0
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 +21 -0
- package/README.md +200 -0
- package/dist/adapter/custom.d.ts +47 -0
- package/dist/adapter/custom.js +53 -0
- package/dist/adapter/custom.js.map +1 -0
- package/dist/adapter/default.d.ts +40 -0
- package/dist/adapter/default.js +202 -0
- package/dist/adapter/default.js.map +1 -0
- package/dist/cli/build.d.ts +1 -0
- package/dist/cli/build.js +31 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/dev.d.ts +1 -0
- package/dist/cli/dev.js +26 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/index.d.ts +12 -0
- package/dist/cli/index.js +49 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +70 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/shared.d.ts +33 -0
- package/dist/cli/shared.js +278 -0
- package/dist/cli/shared.js.map +1 -0
- package/dist/cli/start.d.ts +1 -0
- package/dist/cli/start.js +24 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/config-loader.d.ts +49 -0
- package/dist/config-loader.js +140 -0
- package/dist/define-config.d.ts +188 -0
- package/dist/define-config.js +21 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +9 -0
- package/package.json +124 -0
- package/src/adapter/custom.ts +90 -0
- package/src/adapter/default.ts +241 -0
- package/src/cli/build.ts +38 -0
- package/src/cli/dev.ts +38 -0
- package/src/cli/index.ts +52 -0
- package/src/cli/init.ts +76 -0
- package/src/cli/shared.ts +306 -0
- package/src/cli/start.ts +28 -0
- package/src/config-loader.ts +190 -0
- package/src/define-config.ts +206 -0
- package/src/index.ts +17 -0
- package/src/next-app/app/[...asset]/route.ts +81 -0
- package/src/next-app/app/_user-global.css +6 -0
- package/src/next-app/app/_user-sources.css +9 -0
- package/src/next-app/app/a11y/[name]/route.ts +19 -0
- package/src/next-app/app/api/search-index/route.ts +19 -0
- package/src/next-app/app/components/[name]/page.tsx +61 -0
- package/src/next-app/app/components/layout.tsx +18 -0
- package/src/next-app/app/docs/[slug]/page.tsx +53 -0
- package/src/next-app/app/docs/layout.tsx +18 -0
- package/src/next-app/app/globals.css +329 -0
- package/src/next-app/app/layout.tsx +102 -0
- package/src/next-app/app/page.tsx +9 -0
- package/src/next-app/app/preview-snapshot/[name]/page.tsx +20 -0
- package/src/next-app/app/preview-snapshot/layout.tsx +17 -0
- package/src/next-app/app/props/[name]/route.ts +19 -0
- package/src/next-app/app/r/[name]/route.ts +14 -0
- package/src/next-app/app/tests/[name]/route.ts +19 -0
- package/src/next-app/components/a11y-info.tsx +287 -0
- package/src/next-app/components/a11y-provider.tsx +39 -0
- package/src/next-app/components/component-breadcrumb.tsx +55 -0
- package/src/next-app/components/component-icon.tsx +140 -0
- package/src/next-app/components/component-preview.tsx +13 -0
- package/src/next-app/components/component-tabs.tsx +209 -0
- package/src/next-app/components/docs-toc.tsx +86 -0
- package/src/next-app/components/global-mobile-sidebar.tsx +35 -0
- package/src/next-app/components/header.tsx +188 -0
- package/src/next-app/components/heading-anchor.tsx +52 -0
- package/src/next-app/components/homepage-demo.tsx +180 -0
- package/src/next-app/components/locale-toggle.tsx +35 -0
- package/src/next-app/components/localized-mdx-client.tsx +14 -0
- package/src/next-app/components/localized-mdx.tsx +27 -0
- package/src/next-app/components/mobile-sidebar.tsx +22 -0
- package/src/next-app/components/nav-data-provider.tsx +37 -0
- package/src/next-app/components/navigation-progress.tsx +62 -0
- package/src/next-app/components/preview-canvas.tsx +368 -0
- package/src/next-app/components/preview-controls.tsx +94 -0
- package/src/next-app/components/preview-layout.tsx +218 -0
- package/src/next-app/components/props-table.tsx +134 -0
- package/src/next-app/components/resizable-preview.tsx +101 -0
- package/src/next-app/components/search.tsx +177 -0
- package/src/next-app/components/settings-modal.tsx +98 -0
- package/src/next-app/components/shell-ui/accordion.tsx +70 -0
- package/src/next-app/components/shell-ui/backdrop.tsx +29 -0
- package/src/next-app/components/shell-ui/badge.tsx +55 -0
- package/src/next-app/components/shell-ui/breadcrumb.tsx +120 -0
- package/src/next-app/components/shell-ui/button.tsx +64 -0
- package/src/next-app/components/shell-ui/card.tsx +127 -0
- package/src/next-app/components/shell-ui/checkbox.tsx +33 -0
- package/src/next-app/components/shell-ui/dialog.tsx +171 -0
- package/src/next-app/components/shell-ui/empty-state.tsx +66 -0
- package/src/next-app/components/shell-ui/input.tsx +27 -0
- package/src/next-app/components/shell-ui/kbd.tsx +30 -0
- package/src/next-app/components/shell-ui/label.tsx +25 -0
- package/src/next-app/components/shell-ui/select.tsx +204 -0
- package/src/next-app/components/shell-ui/separator.tsx +32 -0
- package/src/next-app/components/shell-ui/skeleton.tsx +18 -0
- package/src/next-app/components/shell-ui/table.tsx +124 -0
- package/src/next-app/components/shell-ui/tabs.tsx +102 -0
- package/src/next-app/components/shell-ui/toggle.tsx +56 -0
- package/src/next-app/components/sidebar-layout.tsx +37 -0
- package/src/next-app/components/sidebar-provider.tsx +75 -0
- package/src/next-app/components/sidebar.tsx +222 -0
- package/src/next-app/components/snapshot-preview.tsx +28 -0
- package/src/next-app/components/test-info.tsx +155 -0
- package/src/next-app/components/theme-provider.tsx +16 -0
- package/src/next-app/components/theme-toggle.tsx +21 -0
- package/src/next-app/components/translated-text.tsx +8 -0
- package/src/next-app/fallback/homepage.tsx +112 -0
- package/src/next-app/fallback/previews.ts +17 -0
- package/src/next-app/hooks/use-active-section.ts +23 -0
- package/src/next-app/hooks/use-controls.ts +72 -0
- package/src/next-app/hooks/use-mobile.ts +19 -0
- package/src/next-app/lib/branding.ts +52 -0
- package/src/next-app/lib/components-nav.ts +8 -0
- package/src/next-app/lib/docs.ts +16 -0
- package/src/next-app/lib/github.ts +38 -0
- package/src/next-app/lib/i18n.tsx +630 -0
- package/src/next-app/lib/locales.ts +17 -0
- package/src/next-app/lib/preview-loader.ts +7 -0
- package/src/next-app/lib/registry-adapter.ts +199 -0
- package/src/next-app/lib/utils.ts +6 -0
- package/src/next-app/next-env.d.ts +6 -0
- package/src/next-app/next.config.ts +101 -0
- package/src/next-app/postcss.config.mjs +7 -0
- package/src/next-app/public/favicon.ico +0 -0
- package/src/next-app/public/favicon_dark.svg +3 -0
- package/src/next-app/public/favicon_light.svg +3 -0
- package/src/next-app/registry.config.ts +50 -0
- package/src/next-app/tsconfig.json +29 -0
- package/src/next-app/user-aliases.d.ts +17 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
4
|
+
import { FileCode } from "lucide-react"
|
|
5
|
+
import { Badge } from "@shell/components/shell-ui/badge"
|
|
6
|
+
import { EmptyState, EmptyStateIcon, EmptyStateDescription } from "@shell/components/shell-ui/empty-state"
|
|
7
|
+
import {
|
|
8
|
+
Table,
|
|
9
|
+
TableBody,
|
|
10
|
+
TableCell,
|
|
11
|
+
TableHead,
|
|
12
|
+
TableHeader,
|
|
13
|
+
TableRow,
|
|
14
|
+
} from "@shell/components/shell-ui/table"
|
|
15
|
+
|
|
16
|
+
interface PropDoc {
|
|
17
|
+
name: string
|
|
18
|
+
type: string
|
|
19
|
+
required: boolean
|
|
20
|
+
defaultValue: string | null
|
|
21
|
+
description: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ComponentDoc {
|
|
25
|
+
displayName: string
|
|
26
|
+
description: string
|
|
27
|
+
props: PropDoc[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function PropsTable({ name }: { name: string }) {
|
|
31
|
+
const [docs, setDocs] = useState<ComponentDoc[] | null>(null)
|
|
32
|
+
const [error, setError] = useState(false)
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
fetch(`/props/${name}.json`)
|
|
36
|
+
.then((r) => {
|
|
37
|
+
if (!r.ok) throw new Error()
|
|
38
|
+
return r.json()
|
|
39
|
+
})
|
|
40
|
+
.then(setDocs)
|
|
41
|
+
.catch(() => setError(true))
|
|
42
|
+
}, [name])
|
|
43
|
+
|
|
44
|
+
const isEmpty = error || (docs && docs.length === 0) || (docs && docs.every((c) => c.props.length === 0))
|
|
45
|
+
|
|
46
|
+
if (!docs && !error) return <p className="text-sm text-muted-foreground" role="status" aria-live="polite">Loading...</p>
|
|
47
|
+
|
|
48
|
+
if (isEmpty) return (
|
|
49
|
+
<EmptyState>
|
|
50
|
+
<EmptyStateIcon><FileCode /></EmptyStateIcon>
|
|
51
|
+
<EmptyStateDescription>No documented props for this component.</EmptyStateDescription>
|
|
52
|
+
</EmptyState>
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if (!docs) return null
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="space-y-8">
|
|
59
|
+
{docs.map((comp) => (
|
|
60
|
+
<div key={comp.displayName}>
|
|
61
|
+
{docs.length > 1 && (
|
|
62
|
+
<h4
|
|
63
|
+
id={`props-${comp.displayName.toLowerCase()}`}
|
|
64
|
+
className="text-base font-semibold mb-1"
|
|
65
|
+
>
|
|
66
|
+
{comp.displayName}
|
|
67
|
+
</h4>
|
|
68
|
+
)}
|
|
69
|
+
{comp.description && (
|
|
70
|
+
<p className="text-sm text-muted-foreground mb-3">
|
|
71
|
+
{comp.description}
|
|
72
|
+
</p>
|
|
73
|
+
)}
|
|
74
|
+
{comp.props.length > 0 ? (
|
|
75
|
+
<div className="rounded-lg border overflow-hidden">
|
|
76
|
+
<Table>
|
|
77
|
+
<TableHeader>
|
|
78
|
+
<TableRow>
|
|
79
|
+
<TableHead className="w-36">Prop</TableHead>
|
|
80
|
+
<TableHead className="w-48">Type</TableHead>
|
|
81
|
+
<TableHead className="w-24">Default</TableHead>
|
|
82
|
+
<TableHead>Description</TableHead>
|
|
83
|
+
</TableRow>
|
|
84
|
+
</TableHeader>
|
|
85
|
+
<TableBody>
|
|
86
|
+
{comp.props.map((prop) => (
|
|
87
|
+
<TableRow key={prop.name}>
|
|
88
|
+
<TableCell>
|
|
89
|
+
<code className="text-xs font-mono">{prop.name}</code>
|
|
90
|
+
{prop.required && (
|
|
91
|
+
<Badge
|
|
92
|
+
variant="destructive"
|
|
93
|
+
className="ml-1.5 text-[10px] px-1 py-0"
|
|
94
|
+
>
|
|
95
|
+
required
|
|
96
|
+
</Badge>
|
|
97
|
+
)}
|
|
98
|
+
</TableCell>
|
|
99
|
+
<TableCell>
|
|
100
|
+
<code className="text-xs font-mono text-muted-foreground">
|
|
101
|
+
{prop.type}
|
|
102
|
+
</code>
|
|
103
|
+
</TableCell>
|
|
104
|
+
<TableCell>
|
|
105
|
+
{prop.defaultValue ? (
|
|
106
|
+
<code className="text-xs font-mono">
|
|
107
|
+
{prop.defaultValue}
|
|
108
|
+
</code>
|
|
109
|
+
) : (
|
|
110
|
+
<span className="text-xs text-muted-foreground">
|
|
111
|
+
—
|
|
112
|
+
</span>
|
|
113
|
+
)}
|
|
114
|
+
</TableCell>
|
|
115
|
+
<TableCell className="text-sm">
|
|
116
|
+
{prop.description || (
|
|
117
|
+
<span className="text-muted-foreground">—</span>
|
|
118
|
+
)}
|
|
119
|
+
</TableCell>
|
|
120
|
+
</TableRow>
|
|
121
|
+
))}
|
|
122
|
+
</TableBody>
|
|
123
|
+
</Table>
|
|
124
|
+
</div>
|
|
125
|
+
) : (
|
|
126
|
+
<p className="text-sm text-muted-foreground">
|
|
127
|
+
No documented props.
|
|
128
|
+
</p>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef, useState, useEffect } from "react"
|
|
4
|
+
|
|
5
|
+
const STORAGE_KEY = "preview-height"
|
|
6
|
+
const DEFAULT_HEIGHT_DESKTOP = 384
|
|
7
|
+
const DEFAULT_HEIGHT_MOBILE = 600
|
|
8
|
+
const MIN_HEIGHT = 200
|
|
9
|
+
const MAX_HEIGHT = 1000
|
|
10
|
+
|
|
11
|
+
function getDefaultHeight(): number {
|
|
12
|
+
if (typeof window === "undefined") return DEFAULT_HEIGHT_DESKTOP
|
|
13
|
+
return window.matchMedia("(max-width: 767px)").matches
|
|
14
|
+
? DEFAULT_HEIGHT_MOBILE
|
|
15
|
+
: DEFAULT_HEIGHT_DESKTOP
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getStoredHeight(): number {
|
|
19
|
+
if (typeof window === "undefined") return DEFAULT_HEIGHT_DESKTOP
|
|
20
|
+
const stored = sessionStorage.getItem(STORAGE_KEY)
|
|
21
|
+
if (stored) {
|
|
22
|
+
const n = Number(stored)
|
|
23
|
+
if (!isNaN(n)) return Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, n))
|
|
24
|
+
}
|
|
25
|
+
return getDefaultHeight()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function ResizablePreview({ children }: { children: React.ReactNode }) {
|
|
29
|
+
const [height, setHeight] = useState(getStoredHeight)
|
|
30
|
+
const dragging = useRef(false)
|
|
31
|
+
const startY = useRef(0)
|
|
32
|
+
const startH = useRef(0)
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
sessionStorage.setItem(STORAGE_KEY, String(height))
|
|
36
|
+
}, [height])
|
|
37
|
+
|
|
38
|
+
const onMouseDown = useCallback((e: React.MouseEvent) => {
|
|
39
|
+
e.preventDefault()
|
|
40
|
+
dragging.current = true
|
|
41
|
+
startY.current = e.clientY
|
|
42
|
+
startH.current = height
|
|
43
|
+
document.body.style.cursor = "row-resize"
|
|
44
|
+
document.body.style.userSelect = "none"
|
|
45
|
+
|
|
46
|
+
function onMouseMove(ev: MouseEvent) {
|
|
47
|
+
if (!dragging.current) return
|
|
48
|
+
const newH = Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, startH.current + (ev.clientY - startY.current)))
|
|
49
|
+
setHeight(newH)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function onMouseUp() {
|
|
53
|
+
dragging.current = false
|
|
54
|
+
document.body.style.cursor = ""
|
|
55
|
+
document.body.style.userSelect = ""
|
|
56
|
+
document.removeEventListener("mousemove", onMouseMove)
|
|
57
|
+
document.removeEventListener("mouseup", onMouseUp)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
document.addEventListener("mousemove", onMouseMove)
|
|
61
|
+
document.addEventListener("mouseup", onMouseUp)
|
|
62
|
+
}, [height])
|
|
63
|
+
|
|
64
|
+
const onTouchStart = useCallback((e: React.TouchEvent) => {
|
|
65
|
+
if (e.touches.length !== 1) return
|
|
66
|
+
dragging.current = true
|
|
67
|
+
startY.current = e.touches[0].clientY
|
|
68
|
+
startH.current = height
|
|
69
|
+
|
|
70
|
+
function onTouchMove(ev: TouchEvent) {
|
|
71
|
+
if (!dragging.current || ev.touches.length !== 1) return
|
|
72
|
+
ev.preventDefault()
|
|
73
|
+
const newH = Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, startH.current + (ev.touches[0].clientY - startY.current)))
|
|
74
|
+
setHeight(newH)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onTouchEnd() {
|
|
78
|
+
dragging.current = false
|
|
79
|
+
document.removeEventListener("touchmove", onTouchMove)
|
|
80
|
+
document.removeEventListener("touchend", onTouchEnd)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
document.addEventListener("touchmove", onTouchMove, { passive: false })
|
|
84
|
+
document.addEventListener("touchend", onTouchEnd)
|
|
85
|
+
}, [height])
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div>
|
|
89
|
+
<div style={{ height }}>
|
|
90
|
+
{children}
|
|
91
|
+
</div>
|
|
92
|
+
<div
|
|
93
|
+
className="h-3 md:h-2 cursor-row-resize flex items-center justify-center hover:bg-muted/50 active:bg-muted/50 transition-colors group touch-none"
|
|
94
|
+
onMouseDown={onMouseDown}
|
|
95
|
+
onTouchStart={onTouchStart}
|
|
96
|
+
>
|
|
97
|
+
<div className="w-8 h-0.5 rounded-full bg-border group-hover:bg-muted-foreground transition-colors" />
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react"
|
|
4
|
+
import { useRouter } from "next/navigation"
|
|
5
|
+
import { BookOpen, Component, Search, X } from "lucide-react"
|
|
6
|
+
import { Command } from "cmdk"
|
|
7
|
+
import { Button } from "@shell/components/shell-ui/button"
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
} from "@shell/components/shell-ui/dialog"
|
|
13
|
+
import { useTranslations } from "@shell/lib/i18n"
|
|
14
|
+
|
|
15
|
+
export interface SearchItem {
|
|
16
|
+
label: string
|
|
17
|
+
href: string
|
|
18
|
+
group: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Module-level cache — survives re-renders, shared across mounts
|
|
22
|
+
let cachedItems: SearchItem[] | null = null
|
|
23
|
+
let fetchPromise: Promise<void> | null = null
|
|
24
|
+
|
|
25
|
+
function preloadSearchIndex() {
|
|
26
|
+
if (cachedItems || fetchPromise) return
|
|
27
|
+
fetchPromise = fetch("/api/search-index")
|
|
28
|
+
.then((r) => r.json())
|
|
29
|
+
.then((data) => { cachedItems = data })
|
|
30
|
+
.catch(() => {})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function SearchTrigger() {
|
|
34
|
+
return <SearchDialog />
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function SearchDialog() {
|
|
38
|
+
const router = useRouter()
|
|
39
|
+
const t = useTranslations()
|
|
40
|
+
const [open, setOpen] = useState(false)
|
|
41
|
+
const [query, setQuery] = useState("")
|
|
42
|
+
const [items, setItems] = useState<SearchItem[]>([])
|
|
43
|
+
const [loading, setLoading] = useState(false)
|
|
44
|
+
|
|
45
|
+
// Preload search index on mount (fires on page load)
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
preloadSearchIndex()
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const down = (e: KeyboardEvent) => {
|
|
52
|
+
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
53
|
+
e.preventDefault()
|
|
54
|
+
setOpen((o) => !o)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
document.addEventListener("keydown", down)
|
|
58
|
+
return () => document.removeEventListener("keydown", down)
|
|
59
|
+
}, [])
|
|
60
|
+
|
|
61
|
+
// When dialog opens, use cached data or wait for in-flight fetch
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!open || items.length > 0) return
|
|
64
|
+
if (cachedItems) {
|
|
65
|
+
setItems(cachedItems)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
setLoading(true)
|
|
69
|
+
const waitForCache = async () => {
|
|
70
|
+
if (fetchPromise) await fetchPromise
|
|
71
|
+
if (cachedItems) setItems(cachedItems)
|
|
72
|
+
setLoading(false)
|
|
73
|
+
}
|
|
74
|
+
waitForCache()
|
|
75
|
+
}, [open, items.length])
|
|
76
|
+
|
|
77
|
+
const onSelect = useCallback(
|
|
78
|
+
(href: string) => {
|
|
79
|
+
setOpen(false)
|
|
80
|
+
router.push(href)
|
|
81
|
+
},
|
|
82
|
+
[router]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
const groups = items.reduce<Record<string, SearchItem[]>>((acc, item) => {
|
|
86
|
+
;(acc[item.group] ??= []).push(item)
|
|
87
|
+
return acc
|
|
88
|
+
}, {})
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<>
|
|
92
|
+
{/* Desktop: full search bar */}
|
|
93
|
+
<Button
|
|
94
|
+
variant="outline"
|
|
95
|
+
size="sm"
|
|
96
|
+
className="hidden md:inline-flex gap-2 text-muted-foreground font-normal w-56 justify-start"
|
|
97
|
+
onClick={() => setOpen(true)}
|
|
98
|
+
>
|
|
99
|
+
<Search className="size-4" />
|
|
100
|
+
<span>{t("header.search")}</span>
|
|
101
|
+
<kbd className="ml-auto pointer-events-none hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground sm:flex">
|
|
102
|
+
<span className="text-xs">⌘</span>K
|
|
103
|
+
</kbd>
|
|
104
|
+
</Button>
|
|
105
|
+
{/* Mobile: icon only */}
|
|
106
|
+
<Button
|
|
107
|
+
variant="ghost"
|
|
108
|
+
size="icon"
|
|
109
|
+
className="md:hidden"
|
|
110
|
+
onClick={() => setOpen(true)}
|
|
111
|
+
aria-label="Search"
|
|
112
|
+
>
|
|
113
|
+
<Search className="size-4" />
|
|
114
|
+
</Button>
|
|
115
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
116
|
+
<DialogContent
|
|
117
|
+
className="overflow-hidden p-0 sm:max-w-lg max-md:max-w-full! max-md:h-dvh max-md:rounded-none max-md:border-0 max-md:top-0 max-md:translate-y-0"
|
|
118
|
+
showCloseButton={false}
|
|
119
|
+
>
|
|
120
|
+
<DialogTitle className="sr-only">Search</DialogTitle>
|
|
121
|
+
<Command
|
|
122
|
+
className="flex flex-col h-full [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium"
|
|
123
|
+
loop
|
|
124
|
+
>
|
|
125
|
+
<div className="flex items-center border-b px-3">
|
|
126
|
+
<Search className="mr-2 size-4 shrink-0 opacity-50" />
|
|
127
|
+
|
|
128
|
+
<Command.Input
|
|
129
|
+
value={query}
|
|
130
|
+
onValueChange={setQuery}
|
|
131
|
+
placeholder={t("search.placeholder")}
|
|
132
|
+
className="flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
133
|
+
/>
|
|
134
|
+
<button
|
|
135
|
+
onClick={() => setOpen(false)}
|
|
136
|
+
className="md:hidden p-1 rounded-md text-muted-foreground hover:text-foreground cursor-pointer"
|
|
137
|
+
aria-label="Close"
|
|
138
|
+
>
|
|
139
|
+
<X className="size-4" />
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
<Command.List className="max-h-80 max-md:max-h-none max-md:flex-1 overflow-y-auto p-1" aria-busy={loading} aria-live="polite">
|
|
143
|
+
{loading ? (
|
|
144
|
+
<div className="py-6 text-center text-sm text-muted-foreground" role="status">
|
|
145
|
+
{t("a11y.loading")}
|
|
146
|
+
</div>
|
|
147
|
+
) : (
|
|
148
|
+
<Command.Empty className="py-6 text-center text-sm text-muted-foreground">
|
|
149
|
+
{t("search.noResults")}
|
|
150
|
+
</Command.Empty>
|
|
151
|
+
)}
|
|
152
|
+
{Object.entries(groups).map(([group, groupItems]) => {
|
|
153
|
+
const Icon =
|
|
154
|
+
group === "Documentation" ? BookOpen : Component
|
|
155
|
+
return (
|
|
156
|
+
<Command.Group key={group} heading={group}>
|
|
157
|
+
{groupItems.map((item) => (
|
|
158
|
+
<Command.Item
|
|
159
|
+
key={item.href}
|
|
160
|
+
value={`${item.group} ${item.label}`}
|
|
161
|
+
onSelect={() => onSelect(item.href)}
|
|
162
|
+
className="relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground"
|
|
163
|
+
>
|
|
164
|
+
<Icon className="size-4 shrink-0 text-muted-foreground" />
|
|
165
|
+
{item.label}
|
|
166
|
+
</Command.Item>
|
|
167
|
+
))}
|
|
168
|
+
</Command.Group>
|
|
169
|
+
)
|
|
170
|
+
})}
|
|
171
|
+
</Command.List>
|
|
172
|
+
</Command>
|
|
173
|
+
</DialogContent>
|
|
174
|
+
</Dialog>
|
|
175
|
+
</>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { Settings, Sun, Moon, Monitor, Globe } from "lucide-react"
|
|
5
|
+
import { useTheme } from "next-themes"
|
|
6
|
+
import { Button } from "@shell/components/shell-ui/button"
|
|
7
|
+
import { Separator } from "@shell/components/shell-ui/separator"
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
DialogTrigger,
|
|
14
|
+
} from "@shell/components/shell-ui/dialog"
|
|
15
|
+
import { useLocale, useTranslations, type Locale } from "@shell/lib/i18n"
|
|
16
|
+
|
|
17
|
+
export function SettingsButton() {
|
|
18
|
+
const [open, setOpen] = useState(false)
|
|
19
|
+
const { theme, setTheme } = useTheme()
|
|
20
|
+
const { locale, setLocale } = useLocale()
|
|
21
|
+
const t = useTranslations()
|
|
22
|
+
|
|
23
|
+
const themes = [
|
|
24
|
+
{ id: "light" as const, label: t("settings.light"), icon: Sun },
|
|
25
|
+
{ id: "dark" as const, label: t("settings.dark"), icon: Moon },
|
|
26
|
+
{ id: "system" as const, label: t("settings.system"), icon: Monitor },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
31
|
+
<DialogTrigger asChild>
|
|
32
|
+
<Button
|
|
33
|
+
variant="ghost"
|
|
34
|
+
size="icon"
|
|
35
|
+
aria-label={t("settings.title")}
|
|
36
|
+
className="md:hidden"
|
|
37
|
+
>
|
|
38
|
+
<Settings className="size-4" />
|
|
39
|
+
</Button>
|
|
40
|
+
</DialogTrigger>
|
|
41
|
+
<DialogContent className="max-md:max-w-full! max-md:h-full max-md:rounded-none max-md:border-0">
|
|
42
|
+
<DialogHeader>
|
|
43
|
+
<DialogTitle>{t("settings.title")}</DialogTitle>
|
|
44
|
+
</DialogHeader>
|
|
45
|
+
|
|
46
|
+
<div className="space-y-6">
|
|
47
|
+
{/* Theme */}
|
|
48
|
+
<div className="space-y-3">
|
|
49
|
+
<h3 className="text-sm font-semibold flex items-center gap-2">
|
|
50
|
+
<Sun className="size-4" />
|
|
51
|
+
{t("settings.theme")}
|
|
52
|
+
</h3>
|
|
53
|
+
<div className="flex gap-2">
|
|
54
|
+
{themes.map((opt) => (
|
|
55
|
+
<Button
|
|
56
|
+
key={opt.id}
|
|
57
|
+
variant={theme === opt.id ? "default" : "outline"}
|
|
58
|
+
size="sm"
|
|
59
|
+
onClick={() => setTheme(opt.id)}
|
|
60
|
+
className="flex-1"
|
|
61
|
+
>
|
|
62
|
+
<opt.icon className="size-3.5 mr-1.5" />
|
|
63
|
+
{opt.label}
|
|
64
|
+
</Button>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<Separator />
|
|
70
|
+
|
|
71
|
+
{/* Language */}
|
|
72
|
+
<div className="space-y-3">
|
|
73
|
+
<h3 className="text-sm font-semibold flex items-center gap-2">
|
|
74
|
+
<Globe className="size-4" />
|
|
75
|
+
{t("settings.language")}
|
|
76
|
+
</h3>
|
|
77
|
+
<div className="flex gap-2">
|
|
78
|
+
{([
|
|
79
|
+
{ id: "en" as Locale, label: "English" },
|
|
80
|
+
{ id: "fr" as Locale, label: "Français" },
|
|
81
|
+
]).map((lang) => (
|
|
82
|
+
<Button
|
|
83
|
+
key={lang.id}
|
|
84
|
+
variant={locale === lang.id ? "default" : "outline"}
|
|
85
|
+
size="sm"
|
|
86
|
+
onClick={() => setLocale(lang.id)}
|
|
87
|
+
className="flex-1"
|
|
88
|
+
>
|
|
89
|
+
{lang.label}
|
|
90
|
+
</Button>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</DialogContent>
|
|
96
|
+
</Dialog>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ChevronDownIcon } from "lucide-react"
|
|
5
|
+
import { Accordion as AccordionPrimitive } from "radix-ui"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@shell/lib/utils"
|
|
8
|
+
|
|
9
|
+
/** Root container for a set of collapsible accordion sections. */
|
|
10
|
+
function Accordion({
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
|
13
|
+
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A single collapsible section within an accordion. */
|
|
17
|
+
function AccordionItem({
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
|
21
|
+
return (
|
|
22
|
+
<AccordionPrimitive.Item
|
|
23
|
+
data-slot="accordion-item"
|
|
24
|
+
className={cn("border-b last:border-b-0", className)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Clickable header that toggles the visibility of its associated accordion content. */
|
|
31
|
+
function AccordionTrigger({
|
|
32
|
+
className,
|
|
33
|
+
children,
|
|
34
|
+
...props
|
|
35
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
|
36
|
+
return (
|
|
37
|
+
<AccordionPrimitive.Header className="flex">
|
|
38
|
+
<AccordionPrimitive.Trigger
|
|
39
|
+
data-slot="accordion-trigger"
|
|
40
|
+
className={cn(
|
|
41
|
+
"flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
<ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
|
|
48
|
+
</AccordionPrimitive.Trigger>
|
|
49
|
+
</AccordionPrimitive.Header>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Animated collapsible content area revealed when its accordion item is expanded. */
|
|
54
|
+
function AccordionContent({
|
|
55
|
+
className,
|
|
56
|
+
children,
|
|
57
|
+
...props
|
|
58
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
|
59
|
+
return (
|
|
60
|
+
<AccordionPrimitive.Content
|
|
61
|
+
data-slot="accordion-content"
|
|
62
|
+
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
63
|
+
{...props}
|
|
64
|
+
>
|
|
65
|
+
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
|
66
|
+
</AccordionPrimitive.Content>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@shell/lib/utils"
|
|
4
|
+
|
|
5
|
+
/** A fixed overlay backdrop for modals, drawers, and floating panels. */
|
|
6
|
+
function Backdrop({
|
|
7
|
+
/** Additional CSS classes. */
|
|
8
|
+
className,
|
|
9
|
+
/** Whether the backdrop starts below the header (top: 3.5rem). */
|
|
10
|
+
belowHeader = false,
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<"div"> & {
|
|
13
|
+
/** Whether the backdrop starts below the header. */
|
|
14
|
+
belowHeader?: boolean
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
data-slot="backdrop"
|
|
19
|
+
className={cn(
|
|
20
|
+
"fixed inset-x-0 bottom-0 z-50 bg-black/40 dark:bg-black/60 transition-opacity duration-150 motion-reduce:transition-none",
|
|
21
|
+
belowHeader ? "top-14" : "top-0",
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { Backdrop }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { Slot } from "radix-ui"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@shell/lib/utils"
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
|
13
|
+
secondary:
|
|
14
|
+
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
|
15
|
+
destructive:
|
|
16
|
+
"bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
|
|
17
|
+
outline:
|
|
18
|
+
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
19
|
+
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
20
|
+
link: "text-primary underline-offset-4 [a&]:hover:underline",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: {
|
|
24
|
+
variant: "default",
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
function Badge({
|
|
30
|
+
className,
|
|
31
|
+
/** The visual style variant of the badge. */
|
|
32
|
+
variant = "default",
|
|
33
|
+
/** Whether to render as a child element using Radix Slot. */
|
|
34
|
+
asChild = false,
|
|
35
|
+
...props
|
|
36
|
+
}: React.ComponentProps<"span"> &
|
|
37
|
+
VariantProps<typeof badgeVariants> & {
|
|
38
|
+
/** The visual style variant of the badge. */
|
|
39
|
+
variant?: "default" | "secondary" | "destructive" | "outline"
|
|
40
|
+
/** Whether to render as a child element using Radix Slot. */
|
|
41
|
+
asChild?: boolean
|
|
42
|
+
}) {
|
|
43
|
+
const Comp = asChild ? Slot.Root : "span"
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Comp
|
|
47
|
+
data-slot="badge"
|
|
48
|
+
data-variant={variant}
|
|
49
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { Badge, badgeVariants }
|