@windrun-huaiin/base-ui 3.1.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 +190 -0
- package/dist/base-ui.css +3 -0
- package/dist/components/index.d.mts +144 -0
- package/dist/components/index.d.ts +144 -0
- package/dist/components/index.js +1699 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +1741 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/index.d.mts +47 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +6055 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5842 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/index.d.mts +24 -0
- package/dist/lib/index.d.ts +24 -0
- package/dist/lib/index.js +1324 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/index.mjs +1372 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/ui/index.d.mts +754 -0
- package/dist/ui/index.d.ts +754 -0
- package/dist/ui/index.js +5796 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +5593 -0
- package/dist/ui/index.mjs.map +1 -0
- package/package.json +120 -0
- package/src/assets/bitcoin.tsx +23 -0
- package/src/assets/clerk.tsx +23 -0
- package/src/assets/css.tsx +21 -0
- package/src/assets/csv.tsx +35 -0
- package/src/assets/d8.tsx +25 -0
- package/src/assets/diff.tsx +23 -0
- package/src/assets/dpa.tsx +22 -0
- package/src/assets/github.tsx +23 -0
- package/src/assets/html.tsx +22 -0
- package/src/assets/http.tsx +23 -0
- package/src/assets/index.ts +61 -0
- package/src/assets/iterm.tsx +23 -0
- package/src/assets/java.tsx +23 -0
- package/src/assets/json.tsx +23 -0
- package/src/assets/last-updated.tsx +23 -0
- package/src/assets/log.tsx +28 -0
- package/src/assets/mac.tsx +23 -0
- package/src/assets/markdown.tsx +24 -0
- package/src/assets/mdx.tsx +98 -0
- package/src/assets/mermaid.tsx +24 -0
- package/src/assets/scheme.tsx +22 -0
- package/src/assets/snippets.tsx +23 -0
- package/src/assets/sql.tsx +31 -0
- package/src/assets/subp.tsx +22 -0
- package/src/assets/t3p.tsx +23 -0
- package/src/assets/test.tsx +23 -0
- package/src/assets/txt.tsx +23 -0
- package/src/assets/xml.tsx +23 -0
- package/src/assets/yaml.tsx +23 -0
- package/src/components/404-page.tsx +106 -0
- package/src/components/global-icon.tsx +193 -0
- package/src/components/go-to-top.tsx +43 -0
- package/src/components/index.ts +10 -0
- package/src/components/language-detector.tsx +175 -0
- package/src/components/language-switcher.tsx +77 -0
- package/src/components/script/google-analytics-script.tsx +56 -0
- package/src/components/script/microsoft-clarity-script.tsx +24 -0
- package/src/index.ts +4 -0
- package/src/lib/icon-context.tsx +57 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/site-icon.tsx +46 -0
- package/src/lib/theme-util.ts +7 -0
- package/src/styles/base-ui.css +2 -0
- package/src/ui/accordion.tsx +58 -0
- package/src/ui/alert-dialog.tsx +141 -0
- package/src/ui/alert.tsx +59 -0
- package/src/ui/aspect-ratio.tsx +7 -0
- package/src/ui/avatar.tsx +50 -0
- package/src/ui/badge.tsx +36 -0
- package/src/ui/breadcrumb.tsx +115 -0
- package/src/ui/button.tsx +76 -0
- package/src/ui/calendar.tsx +66 -0
- package/src/ui/card.tsx +79 -0
- package/src/ui/carousel.tsx +262 -0
- package/src/ui/chart.tsx +365 -0
- package/src/ui/checkbox.tsx +30 -0
- package/src/ui/collapsible.tsx +11 -0
- package/src/ui/command.tsx +153 -0
- package/src/ui/context-menu.tsx +200 -0
- package/src/ui/dialog.tsx +122 -0
- package/src/ui/drawer.tsx +118 -0
- package/src/ui/dropdown-menu.tsx +200 -0
- package/src/ui/form.tsx +178 -0
- package/src/ui/hover-card.tsx +29 -0
- package/src/ui/index.ts +52 -0
- package/src/ui/input-otp.tsx +71 -0
- package/src/ui/input.tsx +22 -0
- package/src/ui/label.tsx +26 -0
- package/src/ui/language-button.tsx +43 -0
- package/src/ui/menubar.tsx +236 -0
- package/src/ui/navigation-menu.tsx +128 -0
- package/src/ui/pagination.tsx +117 -0
- package/src/ui/popover.tsx +31 -0
- package/src/ui/progress.tsx +28 -0
- package/src/ui/radio-group.tsx +44 -0
- package/src/ui/resizable.tsx +45 -0
- package/src/ui/scroll-area.tsx +48 -0
- package/src/ui/select.tsx +160 -0
- package/src/ui/separator.tsx +31 -0
- package/src/ui/sheet.tsx +140 -0
- package/src/ui/sidebar.tsx +763 -0
- package/src/ui/skeleton.tsx +15 -0
- package/src/ui/slider.tsx +28 -0
- package/src/ui/sonner.tsx +31 -0
- package/src/ui/switch.tsx +29 -0
- package/src/ui/table.tsx +117 -0
- package/src/ui/tabs.tsx +55 -0
- package/src/ui/textarea.tsx +22 -0
- package/src/ui/toast.tsx +129 -0
- package/src/ui/toaster.tsx +35 -0
- package/src/ui/toggle-group.tsx +61 -0
- package/src/ui/toggle.tsx +45 -0
- package/src/ui/tooltip.tsx +30 -0
- package/src/ui/use-mobile.tsx +19 -0
- package/src/ui/use-toast.ts +194 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { useState, useEffect } from 'react';
|
4
|
+
import { globalLucideIcons as icons } from '@base-ui/components/global-icon';
|
5
|
+
export default function GoToTop() {
|
6
|
+
const [isVisible, setIsVisible] = useState(false);
|
7
|
+
|
8
|
+
// 监听滚动事件
|
9
|
+
useEffect(() => {
|
10
|
+
const toggleVisibility = () => {
|
11
|
+
if (window.scrollY > 300) {
|
12
|
+
setIsVisible(true);
|
13
|
+
} else {
|
14
|
+
setIsVisible(false);
|
15
|
+
}
|
16
|
+
};
|
17
|
+
|
18
|
+
window.addEventListener('scroll', toggleVisibility);
|
19
|
+
return () => window.removeEventListener('scroll', toggleVisibility);
|
20
|
+
}, []);
|
21
|
+
|
22
|
+
// 回到顶部
|
23
|
+
const scrollToTop = () => {
|
24
|
+
window.scrollTo({
|
25
|
+
top: 0,
|
26
|
+
behavior: 'smooth'
|
27
|
+
});
|
28
|
+
};
|
29
|
+
|
30
|
+
return (
|
31
|
+
<>
|
32
|
+
{isVisible && (
|
33
|
+
<button
|
34
|
+
onClick={scrollToTop}
|
35
|
+
className="fixed bottom-6 right-6 p-3 bg-neutral-800 text-neutral-100 hover:bg-neutral-700 dark:bg-neutral-300 dark:text-neutral-900 dark:hover:bg-neutral-400 rounded-full shadow-lg transition-all z-50"
|
36
|
+
aria-label="Go to top"
|
37
|
+
>
|
38
|
+
<icons.ArrowUp size={20} />
|
39
|
+
</button>
|
40
|
+
)}
|
41
|
+
</>
|
42
|
+
);
|
43
|
+
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
// Base Components
|
2
|
+
export * from './404-page';
|
3
|
+
export * from './global-icon';
|
4
|
+
export * from './go-to-top';
|
5
|
+
export * from './language-detector';
|
6
|
+
export * from './language-switcher';
|
7
|
+
|
8
|
+
// Script Components
|
9
|
+
export * from './script/google-analytics-script';
|
10
|
+
export * from './script/microsoft-clarity-script';
|
@@ -0,0 +1,175 @@
|
|
1
|
+
/**
|
2
|
+
* @license
|
3
|
+
* MIT License
|
4
|
+
* Copyright (c) 2025 D8ger
|
5
|
+
*
|
6
|
+
* This source code is licensed under the MIT license found in the
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
8
|
+
*/
|
9
|
+
'use client'
|
10
|
+
|
11
|
+
import { globalLucideIcons as icons } from "@base-ui/components/global-icon"
|
12
|
+
import { useLocale, useTranslations } from 'next-intl'
|
13
|
+
import { useRouter } from 'next/navigation'
|
14
|
+
import { useEffect, useState } from 'react'
|
15
|
+
|
16
|
+
type I18nConfig = {
|
17
|
+
locales: readonly string[];
|
18
|
+
detector: {
|
19
|
+
storagePrefix: string;
|
20
|
+
storageKey: string;
|
21
|
+
autoCloseTimeout: number;
|
22
|
+
expirationDays: number;
|
23
|
+
};
|
24
|
+
};
|
25
|
+
|
26
|
+
interface LanguageDetectorProps {
|
27
|
+
i18nConfig: I18nConfig;
|
28
|
+
}
|
29
|
+
|
30
|
+
type Locale = string;
|
31
|
+
|
32
|
+
interface LanguagePreference {
|
33
|
+
locale: string;
|
34
|
+
status: 'accepted' | 'rejected';
|
35
|
+
timestamp: number;
|
36
|
+
}
|
37
|
+
|
38
|
+
export default function LanguageDetector({ i18nConfig }: LanguageDetectorProps) {
|
39
|
+
const [show, setShow] = useState(false)
|
40
|
+
const [detectedLocale, setDetectedLocale] = useState<Locale | null>(null)
|
41
|
+
const currentLocale = useLocale()
|
42
|
+
const router = useRouter()
|
43
|
+
const t = useTranslations('languageDetection')
|
44
|
+
|
45
|
+
// Get the storage key from the configuration
|
46
|
+
const LANGUAGE_PREFERENCE_KEY = `${i18nConfig.detector.storagePrefix}-${i18nConfig.detector.storageKey}`
|
47
|
+
|
48
|
+
useEffect(() => {
|
49
|
+
// Get the browser language
|
50
|
+
const browserLang = navigator.language.split('-')[0] as Locale
|
51
|
+
|
52
|
+
// Get the language preference from localStorage
|
53
|
+
const savedPreference = localStorage.getItem(LANGUAGE_PREFERENCE_KEY)
|
54
|
+
const preference: LanguagePreference | null = savedPreference
|
55
|
+
? JSON.parse(savedPreference)
|
56
|
+
: null
|
57
|
+
|
58
|
+
// Check if the language detection box should be displayed
|
59
|
+
const shouldShowDetector = () => {
|
60
|
+
if (!preference) return true;
|
61
|
+
|
62
|
+
// If the stored language is the same as the current language, do not display the detection box
|
63
|
+
if (preference.locale === currentLocale) return false;
|
64
|
+
|
65
|
+
// If the user has previously rejected switching to this language, do not display the detection box
|
66
|
+
if (preference.status === 'rejected' && preference.locale === browserLang) return false;
|
67
|
+
|
68
|
+
// If the user has previously accepted switching to this language, do not display the detection box
|
69
|
+
if (preference.status === 'accepted' && preference.locale === currentLocale) return false;
|
70
|
+
|
71
|
+
// Use the expiration time from the configuration
|
72
|
+
const expirationMs = i18nConfig.detector.expirationDays * 24 * 60 * 60 * 1000;
|
73
|
+
if (Date.now() - preference.timestamp < expirationMs) return false;
|
74
|
+
|
75
|
+
return true;
|
76
|
+
}
|
77
|
+
|
78
|
+
// Check if the browser language is in the supported language list and needs to display the detection box
|
79
|
+
if ((i18nConfig.locales as string[]).includes(browserLang) &&
|
80
|
+
browserLang !== currentLocale &&
|
81
|
+
shouldShowDetector()) {
|
82
|
+
setDetectedLocale(browserLang)
|
83
|
+
setShow(true)
|
84
|
+
|
85
|
+
// Use the automatic closing time from the configuration
|
86
|
+
const timer = setTimeout(() => {
|
87
|
+
console.log('[LanguageDetector] Auto closing after timeout')
|
88
|
+
setShow(false)
|
89
|
+
// Save the rejected state when the automatic closing occurs
|
90
|
+
savePreference(browserLang, 'rejected')
|
91
|
+
}, i18nConfig.detector.autoCloseTimeout)
|
92
|
+
|
93
|
+
return () => clearTimeout(timer)
|
94
|
+
}
|
95
|
+
}, [currentLocale])
|
96
|
+
|
97
|
+
// Save the language preference to localStorage
|
98
|
+
const savePreference = (locale: string, status: 'accepted' | 'rejected') => {
|
99
|
+
const preference: LanguagePreference = {
|
100
|
+
locale,
|
101
|
+
status,
|
102
|
+
timestamp: Date.now()
|
103
|
+
}
|
104
|
+
localStorage.setItem(LANGUAGE_PREFERENCE_KEY, JSON.stringify(preference))
|
105
|
+
}
|
106
|
+
|
107
|
+
const handleLanguageChange = () => {
|
108
|
+
if (detectedLocale) {
|
109
|
+
// Save the accepted state
|
110
|
+
savePreference(detectedLocale, 'accepted')
|
111
|
+
|
112
|
+
// Get the current path
|
113
|
+
const pathname = window.location.pathname
|
114
|
+
// Replace the language part
|
115
|
+
const newPathname = pathname.replace(`/${currentLocale}`, `/${detectedLocale}`)
|
116
|
+
// Redirect to the new path
|
117
|
+
router.push(newPathname)
|
118
|
+
setShow(false)
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
const handleClose = () => {
|
123
|
+
if (detectedLocale) {
|
124
|
+
// Save the rejected state
|
125
|
+
savePreference(detectedLocale, 'rejected')
|
126
|
+
}
|
127
|
+
setShow(false)
|
128
|
+
}
|
129
|
+
|
130
|
+
if (!detectedLocale || !show) return null
|
131
|
+
|
132
|
+
return (
|
133
|
+
<div className="fixed top-16 right-4 z-40 w-[420px]">
|
134
|
+
<div className={`shadow-lg rounded-lg transition-all duration-300 ${show ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'}
|
135
|
+
bg-linear-to-r from-purple-100/95 via-white/95 to-purple-100/95 backdrop-blur-xs
|
136
|
+
animate-gradient-x`}>
|
137
|
+
<div className="relative px-6 py-4 overflow-hidden">
|
138
|
+
<div className="relative z-10 flex flex-col gap-3">
|
139
|
+
<div className="flex items-start justify-between gap-4">
|
140
|
+
<div className="flex flex-col gap-1.5">
|
141
|
+
<h3 className="text-lg font-semibold text-gray-900">
|
142
|
+
{t('title')}
|
143
|
+
</h3>
|
144
|
+
<p className="text-base text-gray-600">
|
145
|
+
{t('description')} <span className="text-purple-500 font-semibold">{detectedLocale === 'zh' ? '中文' : 'English'}</span>?
|
146
|
+
</p>
|
147
|
+
</div>
|
148
|
+
<button
|
149
|
+
onClick={handleClose}
|
150
|
+
className="text-gray-500 hover:text-gray-700"
|
151
|
+
>
|
152
|
+
<icons.X className="h-5 w-5" />
|
153
|
+
</button>
|
154
|
+
</div>
|
155
|
+
<div className="flex items-center gap-3">
|
156
|
+
<button
|
157
|
+
onClick={handleClose}
|
158
|
+
className="flex-1 px-4 py-2 text-base bg-gray-100 text-gray-600 rounded-md hover:bg-gray-200"
|
159
|
+
>
|
160
|
+
{t('close')}
|
161
|
+
</button>
|
162
|
+
<button
|
163
|
+
onClick={handleLanguageChange}
|
164
|
+
className="flex-1 px-4 py-2 text-base bg-purple-500 text-white rounded-md hover:bg-purple-600"
|
165
|
+
>
|
166
|
+
{t('changeAction')}
|
167
|
+
</button>
|
168
|
+
</div>
|
169
|
+
</div>
|
170
|
+
<div className="absolute inset-0 bg-linear-to-r from-transparent via-purple-200/30 to-transparent animate-shimmer" />
|
171
|
+
</div>
|
172
|
+
</div>
|
173
|
+
</div>
|
174
|
+
)
|
175
|
+
}
|
@@ -0,0 +1,77 @@
|
|
1
|
+
/**
|
2
|
+
* @license
|
3
|
+
* MIT License
|
4
|
+
* Copyright (c) 2025 D8ger
|
5
|
+
*
|
6
|
+
* This source code is licensed under the MIT license found in the
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
8
|
+
*/
|
9
|
+
|
10
|
+
'use client'
|
11
|
+
|
12
|
+
import { usePathname, useRouter } from 'next/navigation'
|
13
|
+
import { useLocale } from 'next-intl'
|
14
|
+
import { globalLucideIcons as icons } from "@base-ui/components/global-icon"
|
15
|
+
import {
|
16
|
+
DropdownMenu,
|
17
|
+
DropdownMenuContent,
|
18
|
+
DropdownMenuItem,
|
19
|
+
DropdownMenuTrigger,
|
20
|
+
} from '@base-ui/ui/dropdown-menu'
|
21
|
+
import { LanguageButton } from '@base-ui/ui/language-button'
|
22
|
+
|
23
|
+
interface LanguageSwitcherProps {
|
24
|
+
locales: readonly string[];
|
25
|
+
localeLabels: Record<string, string>;
|
26
|
+
}
|
27
|
+
|
28
|
+
export default function LanguageSwitcher({ locales, localeLabels }: LanguageSwitcherProps) {
|
29
|
+
const locale = useLocale()
|
30
|
+
const router = useRouter()
|
31
|
+
const pathname = usePathname()
|
32
|
+
|
33
|
+
const handleLocaleChange = (newLocale: string) => {
|
34
|
+
const newPathname = pathname.replace(`/${locale}`, `/${newLocale}`)
|
35
|
+
router.push(newPathname)
|
36
|
+
}
|
37
|
+
|
38
|
+
return (
|
39
|
+
<DropdownMenu>
|
40
|
+
<DropdownMenuTrigger asChild>
|
41
|
+
<LanguageButton
|
42
|
+
variant="ghost"
|
43
|
+
size="icon"
|
44
|
+
className="bg-linear-to-r from-purple-400 to-pink-600 hover:from-purple-500 hover:to-pink-700 text-white transform hover:scale-110 transition-all duration-300"
|
45
|
+
>
|
46
|
+
<icons.Globe className="h-5 w-5" />
|
47
|
+
</LanguageButton>
|
48
|
+
</DropdownMenuTrigger>
|
49
|
+
<DropdownMenuContent
|
50
|
+
align="end"
|
51
|
+
sideOffset={5}
|
52
|
+
className="bg-white/90 dark:bg-gray-800/90 border-purple-100 dark:border-purple-800 w-[200px] p-2 backdrop-blur-xs translate-x-[50px]"
|
53
|
+
>
|
54
|
+
<div className="grid grid-cols-2 gap-1">
|
55
|
+
{locales.map((loc) => (
|
56
|
+
<DropdownMenuItem
|
57
|
+
key={loc}
|
58
|
+
className={`
|
59
|
+
px-2 py-2 text-sm cursor-pointer text-center justify-center
|
60
|
+
transition-all duration-300 ease-in-out
|
61
|
+
hover:scale-105 hover:shadow-md
|
62
|
+
rounded-md whitespace-nowrap
|
63
|
+
${locale === loc
|
64
|
+
? 'bg-linear-to-r from-purple-400 to-pink-600 text-white font-medium shadow-lg scale-105'
|
65
|
+
: 'hover:bg-linear-to-r hover:from-purple-400/10 hover:to-pink-600/10 hover:text-transparent hover:bg-clip-text'
|
66
|
+
}
|
67
|
+
`}
|
68
|
+
onClick={() => handleLocaleChange(loc)}
|
69
|
+
>
|
70
|
+
{localeLabels[loc]}
|
71
|
+
</DropdownMenuItem>
|
72
|
+
))}
|
73
|
+
</div>
|
74
|
+
</DropdownMenuContent>
|
75
|
+
</DropdownMenu>
|
76
|
+
)
|
77
|
+
}
|
@@ -0,0 +1,56 @@
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2
|
+
"use client";
|
3
|
+
|
4
|
+
import Script from "next/script";
|
5
|
+
|
6
|
+
const googleAnalyticsId = process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID!;
|
7
|
+
|
8
|
+
export function GoogleAnalyticsScript() {
|
9
|
+
// 只在生产环境中加载 Microsoft Clarity
|
10
|
+
if (process.env.NODE_ENV !== 'production') {
|
11
|
+
return null
|
12
|
+
}
|
13
|
+
|
14
|
+
return (
|
15
|
+
<>
|
16
|
+
<Script
|
17
|
+
strategy="afterInteractive"
|
18
|
+
src={`https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsId}`}
|
19
|
+
/>
|
20
|
+
<Script
|
21
|
+
id="google-analytics"
|
22
|
+
strategy="afterInteractive"
|
23
|
+
dangerouslySetInnerHTML={{
|
24
|
+
__html: `
|
25
|
+
window.dataLayer = window.dataLayer || [];
|
26
|
+
function gtag(){dataLayer.push(arguments);}
|
27
|
+
gtag('js', new Date());
|
28
|
+
gtag('config', '${googleAnalyticsId}');
|
29
|
+
`,
|
30
|
+
}}
|
31
|
+
/>
|
32
|
+
</>
|
33
|
+
);
|
34
|
+
}
|
35
|
+
|
36
|
+
export function useGoogleAnalytics() {
|
37
|
+
const trackEvent = (event: string, data?: Record<string, unknown>) => {
|
38
|
+
if (typeof window === "undefined" || !window.gtag) {
|
39
|
+
return;
|
40
|
+
}
|
41
|
+
|
42
|
+
window.gtag("event", event, data);
|
43
|
+
};
|
44
|
+
|
45
|
+
return {
|
46
|
+
trackEvent,
|
47
|
+
};
|
48
|
+
}
|
49
|
+
|
50
|
+
// 为 window 添加 gtag 类型定义
|
51
|
+
declare global {
|
52
|
+
interface Window {
|
53
|
+
dataLayer: any[];
|
54
|
+
gtag: (...args: any[]) => void;
|
55
|
+
}
|
56
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
'use client'
|
2
|
+
|
3
|
+
import Script from 'next/script'
|
4
|
+
|
5
|
+
const microsoftClarityId = process.env.NEXT_PUBLIC_MICROSOFT_CLARITY_ID!;
|
6
|
+
|
7
|
+
export default function MicrosoftClarityScript() {
|
8
|
+
// 只在生产环境中加载 Microsoft Clarity
|
9
|
+
if (process.env.NODE_ENV !== 'production') {
|
10
|
+
return null
|
11
|
+
}
|
12
|
+
|
13
|
+
return (
|
14
|
+
<Script id="microsoft-clarity" strategy="afterInteractive">
|
15
|
+
{`
|
16
|
+
(function(c,l,a,r,i,t,y){
|
17
|
+
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
18
|
+
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
19
|
+
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
20
|
+
})(window, document, "clarity", "script", "${microsoftClarityId}");
|
21
|
+
`}
|
22
|
+
</Script>
|
23
|
+
)
|
24
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { createContext, useContext, type ComponentType, type ReactNode } from 'react';
|
4
|
+
|
5
|
+
export interface IconConfig {
|
6
|
+
siteIcon?: ComponentType | string;
|
7
|
+
}
|
8
|
+
|
9
|
+
// icon config context, directly store the config value
|
10
|
+
const IconConfigContext = createContext<IconConfig | null>(null);
|
11
|
+
|
12
|
+
interface IconConfigProviderProps {
|
13
|
+
config: IconConfig;
|
14
|
+
children: ReactNode;
|
15
|
+
}
|
16
|
+
|
17
|
+
/**
|
18
|
+
* IconConfigProvider - icon config provider based on React Context
|
19
|
+
* directly store the config value, without depending on module state
|
20
|
+
*/
|
21
|
+
export function IconConfigProvider({ config, children }: IconConfigProviderProps) {
|
22
|
+
return (
|
23
|
+
<IconConfigContext.Provider value={config}>
|
24
|
+
{children}
|
25
|
+
</IconConfigContext.Provider>
|
26
|
+
);
|
27
|
+
}
|
28
|
+
|
29
|
+
/**
|
30
|
+
* internal hook: get icon config
|
31
|
+
* not exposed, only used by base-ui internal components
|
32
|
+
*/
|
33
|
+
function useIconConfig(): IconConfig {
|
34
|
+
const config = useContext(IconConfigContext);
|
35
|
+
|
36
|
+
if (config === null) {
|
37
|
+
throw new Error(
|
38
|
+
'[SiteIcon] IconConfigProvider not found. Please wrap your app with <IconConfigProvider config={{ siteIcon: "YourIcon" }}>.'
|
39
|
+
);
|
40
|
+
}
|
41
|
+
|
42
|
+
return config;
|
43
|
+
}
|
44
|
+
|
45
|
+
/**
|
46
|
+
* internal hook: safe get specific icon config
|
47
|
+
* not exposed, only used by base-ui internal components
|
48
|
+
*/
|
49
|
+
export function useIconConfigSafe(iconKey: keyof IconConfig): ComponentType | string | undefined {
|
50
|
+
try {
|
51
|
+
const config = useIconConfig();
|
52
|
+
return config[iconKey];
|
53
|
+
} catch {
|
54
|
+
// if there is no provider, return undefined, let the caller handle it
|
55
|
+
return undefined;
|
56
|
+
}
|
57
|
+
}
|
package/src/lib/index.ts
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { type LucideProps } from 'lucide-react';
|
4
|
+
import { cn } from '@lib/utils';
|
5
|
+
import { useIconConfigSafe } from '@base-ui/lib/icon-context';
|
6
|
+
import { globalLucideIcons } from '@base-ui/components/global-icon';
|
7
|
+
import { themeIconColor } from '@base-ui/lib/theme-util';
|
8
|
+
|
9
|
+
/**
|
10
|
+
* site icon component - client component
|
11
|
+
* based on React Context to get the config, solve the problem of cross-package module instance isolation
|
12
|
+
*/
|
13
|
+
export function SiteIcon({
|
14
|
+
size = 24,
|
15
|
+
className,
|
16
|
+
...props
|
17
|
+
}: Omit<LucideProps, 'children'>) {
|
18
|
+
const configuredIcon = useIconConfigSafe('siteIcon');
|
19
|
+
|
20
|
+
if (configuredIcon === undefined) {
|
21
|
+
throw new Error(
|
22
|
+
'[SiteIcon] Site icon is not configured. Please use <IconConfigProvider config={{ siteIcon: YourCustomIcon }}> or <IconConfigProvider config={{ siteIcon: "IconKeyName" }}> to set a custom site icon to avoid legal risks.'
|
23
|
+
);
|
24
|
+
}
|
25
|
+
|
26
|
+
// render the icon, pass in the config value and attributes
|
27
|
+
if (typeof configuredIcon === 'string') {
|
28
|
+
// string type: the key name of globalLucideIcons
|
29
|
+
if (configuredIcon === '') {
|
30
|
+
// empty string use default icon
|
31
|
+
const DefaultIcon = globalLucideIcons['Download' as keyof typeof globalLucideIcons];
|
32
|
+
return <DefaultIcon size={size} className={cn(themeIconColor, className)} {...props} />;
|
33
|
+
}
|
34
|
+
const IconComponent = globalLucideIcons[configuredIcon as keyof typeof globalLucideIcons];
|
35
|
+
if (!IconComponent) {
|
36
|
+
throw new Error(`[SiteIcon] Icon key "${configuredIcon}" not found in globalLucideIcons.`);
|
37
|
+
}
|
38
|
+
return <IconComponent size={size} className={cn(themeIconColor, className)} {...props} />;
|
39
|
+
} else {
|
40
|
+
// React component type: custom icon component
|
41
|
+
const CustomIcon = configuredIcon as React.ComponentType<LucideProps>;
|
42
|
+
const hasColorClass = className && /text-\w+/.test(className);
|
43
|
+
const finalClassName = hasColorClass ? className : cn(themeIconColor, className);
|
44
|
+
return <CustomIcon size={size} className={finalClassName} {...props} />;
|
45
|
+
}
|
46
|
+
}
|
@@ -0,0 +1,7 @@
|
|
1
|
+
|
2
|
+
// Attention: This icon color will be used in the entire project, and it depends on the ENV variable NEXT_PUBLIC_STYLE_ICON_COLOR
|
3
|
+
export const themeIconColor = process.env.NEXT_PUBLIC_STYLE_ICON_COLOR || "text-purple-500";
|
4
|
+
|
5
|
+
// Attention: This icon color will be used in the entire project, and it depends on the ENV variable NEXT_PUBLIC_STYLE_SVG_ICON_COLOR
|
6
|
+
export const themeSvgIconColor = process.env.NEXT_PUBLIC_STYLE_SVG_ICON_COLOR || "#AC62FD";
|
7
|
+
export const themeSvgIconSize = process.env.NEXT_PUBLIC_STYLE_SVG_ICON_SIZE || 18
|
@@ -0,0 +1,58 @@
|
|
1
|
+
"use client"
|
2
|
+
|
3
|
+
import * as React from "react"
|
4
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
5
|
+
import { globalLucideIcons as icons } from "@base-ui/components/global-icon"
|
6
|
+
|
7
|
+
import { cn } from "@lib/utils"
|
8
|
+
|
9
|
+
const Accordion = AccordionPrimitive.Root
|
10
|
+
|
11
|
+
const AccordionItem = React.forwardRef<
|
12
|
+
React.ElementRef<typeof AccordionPrimitive.Item>,
|
13
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
14
|
+
>(({ className, ...props }, ref) => (
|
15
|
+
<AccordionPrimitive.Item
|
16
|
+
ref={ref}
|
17
|
+
className={cn("border-b", className)}
|
18
|
+
{...props}
|
19
|
+
/>
|
20
|
+
))
|
21
|
+
AccordionItem.displayName = "AccordionItem"
|
22
|
+
|
23
|
+
const AccordionTrigger = React.forwardRef<
|
24
|
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
25
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
26
|
+
>(({ className, children, ...props }, ref) => (
|
27
|
+
<AccordionPrimitive.Header className="flex">
|
28
|
+
<AccordionPrimitive.Trigger
|
29
|
+
ref={ref}
|
30
|
+
className={cn(
|
31
|
+
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
32
|
+
className
|
33
|
+
)}
|
34
|
+
{...props}
|
35
|
+
>
|
36
|
+
{children}
|
37
|
+
<icons.ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
38
|
+
</AccordionPrimitive.Trigger>
|
39
|
+
</AccordionPrimitive.Header>
|
40
|
+
))
|
41
|
+
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
42
|
+
|
43
|
+
const AccordionContent = React.forwardRef<
|
44
|
+
React.ElementRef<typeof AccordionPrimitive.Content>,
|
45
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
46
|
+
>(({ className, children, ...props }, ref) => (
|
47
|
+
<AccordionPrimitive.Content
|
48
|
+
ref={ref}
|
49
|
+
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
50
|
+
{...props}
|
51
|
+
>
|
52
|
+
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
53
|
+
</AccordionPrimitive.Content>
|
54
|
+
))
|
55
|
+
|
56
|
+
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
57
|
+
|
58
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|