@windrun-huaiin/base-ui 6.0.3 → 7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/base-ui",
3
- "version": "6.0.3",
3
+ "version": "7.1.0",
4
4
  "description": "Base UI components for windrun-huaiin projects",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,9 +1,5 @@
1
1
  "use client"
2
2
  export * from './404-page';
3
- export * from './language-detector';
4
3
  export * from './language-switcher';
5
4
 
6
- // Script Components (All Client-side)
7
- export * from './script/google-analytics-script';
8
- export * from './script/microsoft-clarity-script';
9
5
 
@@ -0,0 +1,53 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ "use client";
3
+
4
+ import Script from "next/script";
5
+
6
+ interface GoogleAnalyticsClientProps {
7
+ analyticsId: string;
8
+ }
9
+
10
+ export function GoogleAnalyticsClient({ analyticsId }: GoogleAnalyticsClientProps) {
11
+ return (
12
+ <>
13
+ <Script
14
+ strategy="afterInteractive"
15
+ src={`https://www.googletagmanager.com/gtag/js?id=${analyticsId}`}
16
+ />
17
+ <Script
18
+ id="google-analytics"
19
+ strategy="afterInteractive"
20
+ dangerouslySetInnerHTML={{
21
+ __html: `
22
+ window.dataLayer = window.dataLayer || [];
23
+ function gtag(){dataLayer.push(arguments);}
24
+ gtag('js', new Date());
25
+ gtag('config', '${analyticsId}');
26
+ `,
27
+ }}
28
+ />
29
+ </>
30
+ );
31
+ }
32
+
33
+ export function useGoogleAnalytics() {
34
+ const trackEvent = (event: string, data?: Record<string, unknown>) => {
35
+ if (typeof window === "undefined" || !window.gtag) {
36
+ return;
37
+ }
38
+
39
+ window.gtag("event", event, data);
40
+ };
41
+
42
+ return {
43
+ trackEvent,
44
+ };
45
+ }
46
+
47
+ // Add gtag type definition to window
48
+ declare global {
49
+ interface Window {
50
+ dataLayer: any[];
51
+ gtag: (...args: any[]) => void;
52
+ }
53
+ }
@@ -1,56 +1,21 @@
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!;
1
+ import { GoogleAnalyticsClient } from './google-analytics-client';
7
2
 
8
3
  export function GoogleAnalyticsScript() {
9
4
  // Only load in production environment
10
5
  if (process.env.NODE_ENV !== 'production') {
11
- return null
6
+ return null;
12
7
  }
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
8
 
42
- window.gtag("event", event, data);
43
- };
9
+ // Get and validate Google Analytics ID
10
+ const googleAnalyticsId = process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID;
11
+
12
+ if (!googleAnalyticsId) {
13
+ console.warn('NEXT_PUBLIC_GOOGLE_ANALYTICS_ID is not configured');
14
+ return null;
15
+ }
44
16
 
45
- return {
46
- trackEvent,
47
- };
17
+ return <GoogleAnalyticsClient analyticsId={googleAnalyticsId} />;
48
18
  }
49
19
 
50
- // Add gtag type definition to window
51
- declare global {
52
- interface Window {
53
- dataLayer: any[];
54
- gtag: (...args: any[]) => void;
55
- }
56
- }
20
+ // Re-export the hook for convenience
21
+ export { useGoogleAnalytics } from './google-analytics-client';
@@ -0,0 +1,21 @@
1
+ 'use client'
2
+
3
+ import Script from 'next/script'
4
+
5
+ interface MicrosoftClarityClientProps {
6
+ clarityId: string;
7
+ }
8
+
9
+ export function MicrosoftClarityClient({ clarityId }: MicrosoftClarityClientProps) {
10
+ return (
11
+ <Script id="microsoft-clarity" strategy="afterInteractive">
12
+ {`
13
+ (function(c,l,a,r,i,t,y){
14
+ c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
15
+ t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
16
+ y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
17
+ })(window, document, "clarity", "script", "${clarityId}");
18
+ `}
19
+ </Script>
20
+ )
21
+ }
@@ -1,24 +1,18 @@
1
- 'use client'
2
-
3
- import Script from 'next/script'
4
-
5
- const microsoftClarityId = process.env.NEXT_PUBLIC_MICROSOFT_CLARITY_ID!;
1
+ import { MicrosoftClarityClient } from './microsoft-clarity-client';
6
2
 
7
3
  export function MicrosoftClarityScript() {
8
4
  // Only load in production environment
9
5
  if (process.env.NODE_ENV !== 'production') {
10
- return null
6
+ return null;
7
+ }
8
+
9
+ // Get and validate Microsoft Clarity ID
10
+ const microsoftClarityId = process.env.NEXT_PUBLIC_MICROSOFT_CLARITY_ID;
11
+
12
+ if (!microsoftClarityId) {
13
+ console.warn('NEXT_PUBLIC_MICROSOFT_CLARITY_ID is not configured');
14
+ return null;
11
15
  }
12
16
 
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
- )
17
+ return <MicrosoftClarityClient clarityId={microsoftClarityId} />;
24
18
  }
@@ -2,3 +2,7 @@
2
2
  export * from './global-icon';
3
3
  // Icon Configuration
4
4
  export { createSiteIcon } from './site-icon';
5
+
6
+ // Script Components (All Server-side)
7
+ export * from './script/google-analytics-script';
8
+ export * from './script/microsoft-clarity-script';
package/src/ui/button.tsx CHANGED
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import * as React from "react"
2
4
  import { Slot } from "@radix-ui/react-slot"
3
5
  import { cva, type VariantProps } from "class-variance-authority"
@@ -6,7 +6,7 @@
6
6
  * This source code is licensed under the MIT license found in the
7
7
  * LICENSE file in the root directory of this source tree.
8
8
  */
9
-
9
+ "use client";
10
10
  import * as React from "react"
11
11
  import { cn } from "@lib/utils"
12
12
 
@@ -1,175 +0,0 @@
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 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
- }