@wakastellar/ui 2.3.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/components/index.d.ts +15 -0
  2. package/dist/components/waka-ad-banner/index.d.ts +36 -0
  3. package/dist/components/waka-ad-fallback/index.d.ts +33 -0
  4. package/dist/components/waka-ad-inline/index.d.ts +15 -0
  5. package/dist/components/waka-ad-interstitial/index.d.ts +26 -0
  6. package/dist/components/waka-ad-placeholder/index.d.ts +17 -0
  7. package/dist/components/waka-ad-provider/index.d.ts +103 -0
  8. package/dist/components/waka-ad-sidebar/index.d.ts +18 -0
  9. package/dist/components/waka-ad-sticky-footer/index.d.ts +17 -0
  10. package/dist/components/waka-content-recommendation/index.d.ts +23 -0
  11. package/dist/components/waka-outstream-video/index.d.ts +24 -0
  12. package/dist/components/waka-sponsored-badge/index.d.ts +20 -0
  13. package/dist/components/waka-sponsored-card/index.d.ts +25 -0
  14. package/dist/components/waka-sponsored-feed/index.d.ts +31 -0
  15. package/dist/components/waka-video-ad/index.d.ts +32 -0
  16. package/dist/components/waka-video-overlay/index.d.ts +26 -0
  17. package/dist/index.cjs.js +177 -171
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.es.js +14535 -12812
  20. package/dist/utils/security.d.ts +96 -0
  21. package/package.json +4 -4
  22. package/src/blocks/sidebar/index.tsx +6 -6
  23. package/src/components/DataTable/templates/index.tsx +3 -2
  24. package/src/components/index.ts +94 -0
  25. package/src/components/waka-3d-pie-chart/index.tsx +11 -11
  26. package/src/components/waka-achievement-unlock/index.tsx +16 -16
  27. package/src/components/waka-ad-banner/index.tsx +275 -0
  28. package/src/components/waka-ad-fallback/index.tsx +181 -0
  29. package/src/components/waka-ad-inline/index.tsx +103 -0
  30. package/src/components/waka-ad-interstitial/index.tsx +278 -0
  31. package/src/components/waka-ad-placeholder/index.tsx +84 -0
  32. package/src/components/waka-ad-provider/index.tsx +329 -0
  33. package/src/components/waka-ad-sidebar/index.tsx +113 -0
  34. package/src/components/waka-ad-sticky-footer/index.tsx +125 -0
  35. package/src/components/waka-badge-showcase/index.tsx +12 -11
  36. package/src/components/waka-command-bar/index.tsx +2 -1
  37. package/src/components/waka-content-recommendation/index.tsx +294 -0
  38. package/src/components/waka-cost-breakdown/index.tsx +10 -10
  39. package/src/components/waka-funnel-chart/index.tsx +8 -8
  40. package/src/components/waka-health-pulse/index.tsx +6 -6
  41. package/src/components/waka-leaderboard/index.tsx +9 -9
  42. package/src/components/waka-loot-box/index.tsx +20 -20
  43. package/src/components/waka-outstream-video/index.tsx +240 -0
  44. package/src/components/waka-player-card/index.tsx +5 -5
  45. package/src/components/waka-quota-bar/index.tsx +4 -4
  46. package/src/components/waka-radar-score/index.tsx +10 -10
  47. package/src/components/waka-scratch-card/index.tsx +5 -4
  48. package/src/components/waka-server-rack/index.tsx +28 -27
  49. package/src/components/waka-sponsored-badge/index.tsx +97 -0
  50. package/src/components/waka-sponsored-card/index.tsx +275 -0
  51. package/src/components/waka-sponsored-feed/index.tsx +127 -0
  52. package/src/components/waka-spotlight/index.tsx +2 -1
  53. package/src/components/waka-success-explosion/index.tsx +4 -4
  54. package/src/components/waka-video-ad/index.tsx +406 -0
  55. package/src/components/waka-video-overlay/index.tsx +257 -0
  56. package/src/components/waka-xp-bar/index.tsx +13 -13
  57. package/src/styles/base.css +16 -0
  58. package/src/styles/tailwind.preset.js +12 -0
  59. package/src/styles/themes/forest.css +16 -0
  60. package/src/styles/themes/monochrome.css +16 -0
  61. package/src/styles/themes/perpetuity.css +16 -0
  62. package/src/styles/themes/sunset.css +16 -0
  63. package/src/styles/themes/twilight.css +16 -0
@@ -0,0 +1,329 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { createContext, useContext, useState, useEffect, useCallback, useRef } from "react"
5
+
6
+ // ==================== Types ====================
7
+
8
+ export type AdNetwork = "gpt" | "custom" | "adsense"
9
+ export type AdSize = "leaderboard" | "rectangle" | "skyscraper" | "billboard" | "mobile-banner" | "custom"
10
+ export type AdPosition = "top" | "bottom" | "sidebar" | "inline" | "interstitial" | "sticky"
11
+
12
+ export interface AdSlot {
13
+ id: string
14
+ network: AdNetwork
15
+ size: AdSize
16
+ position: AdPosition
17
+ targeting?: Record<string, string | string[]>
18
+ refreshInterval?: number // in seconds, 0 = no refresh
19
+ lazyLoad?: boolean
20
+ customWidth?: number
21
+ customHeight?: number
22
+ }
23
+
24
+ export interface AdConfig {
25
+ network: AdNetwork
26
+ publisherId?: string
27
+ // GPT specific
28
+ gptNetworkCode?: string
29
+ gptAdUnits?: Record<string, string>
30
+ // Custom ad server
31
+ customAdServer?: string
32
+ customApiKey?: string
33
+ // General settings
34
+ enableLazyLoad?: boolean
35
+ lazyLoadOffset?: number // pixels before viewport
36
+ enableRefresh?: boolean
37
+ defaultRefreshInterval?: number // seconds
38
+ enableConsent?: boolean
39
+ testMode?: boolean
40
+ debugMode?: boolean
41
+ }
42
+
43
+ export interface AdEvent {
44
+ type: "impression" | "click" | "viewable" | "error" | "loaded" | "empty"
45
+ slotId: string
46
+ timestamp: Date
47
+ data?: Record<string, unknown>
48
+ }
49
+
50
+ export interface CustomAd {
51
+ id: string
52
+ imageUrl?: string
53
+ videoUrl?: string
54
+ targetUrl: string
55
+ title?: string
56
+ description?: string
57
+ sponsor?: string
58
+ cta?: string
59
+ trackingPixels?: string[]
60
+ impressionUrl?: string
61
+ clickUrl?: string
62
+ }
63
+
64
+ interface AdContextValue {
65
+ config: AdConfig
66
+ isReady: boolean
67
+ hasConsent: boolean | null
68
+ setConsent: (consent: boolean) => void
69
+ registerSlot: (slot: AdSlot) => void
70
+ unregisterSlot: (slotId: string) => void
71
+ refreshSlot: (slotId: string) => void
72
+ refreshAll: () => void
73
+ getCustomAd: (slotId: string) => Promise<CustomAd | null>
74
+ trackEvent: (event: AdEvent) => void
75
+ slots: Map<string, AdSlot>
76
+ }
77
+
78
+ // ==================== Size Definitions ====================
79
+
80
+ export const AD_SIZES: Record<AdSize, { width: number; height: number; label: string }> = {
81
+ leaderboard: { width: 728, height: 90, label: "Leaderboard (728x90)" },
82
+ rectangle: { width: 300, height: 250, label: "Medium Rectangle (300x250)" },
83
+ skyscraper: { width: 160, height: 600, label: "Wide Skyscraper (160x600)" },
84
+ billboard: { width: 970, height: 250, label: "Billboard (970x250)" },
85
+ "mobile-banner": { width: 320, height: 50, label: "Mobile Banner (320x50)" },
86
+ custom: { width: 0, height: 0, label: "Custom Size" },
87
+ }
88
+
89
+ // ==================== Context ====================
90
+
91
+ const AdContext = createContext<AdContextValue | null>(null)
92
+
93
+ export function useAdContext() {
94
+ const context = useContext(AdContext)
95
+ if (!context) {
96
+ throw new Error("useAdContext must be used within WakaAdProvider")
97
+ }
98
+ return context
99
+ }
100
+
101
+ // ==================== Hooks ====================
102
+
103
+ export function useAdVisibility(ref: React.RefObject<HTMLElement>, threshold = 0.5) {
104
+ const [isVisible, setIsVisible] = useState(false)
105
+ const [viewableTime, setViewableTime] = useState(0)
106
+ const viewableStartRef = useRef<number | null>(null)
107
+
108
+ useEffect(() => {
109
+ if (!ref.current) return
110
+
111
+ const observer = new IntersectionObserver(
112
+ ([entry]) => {
113
+ const nowVisible = entry.intersectionRatio >= threshold
114
+ setIsVisible(nowVisible)
115
+
116
+ if (nowVisible && !viewableStartRef.current) {
117
+ viewableStartRef.current = Date.now()
118
+ } else if (!nowVisible && viewableStartRef.current) {
119
+ setViewableTime((prev) => prev + (Date.now() - viewableStartRef.current!))
120
+ viewableStartRef.current = null
121
+ }
122
+ },
123
+ { threshold: [0, threshold, 1] }
124
+ )
125
+
126
+ observer.observe(ref.current)
127
+ return () => observer.disconnect()
128
+ }, [ref, threshold])
129
+
130
+ return { isVisible, viewableTime }
131
+ }
132
+
133
+ export function useAdConsent() {
134
+ const { hasConsent, setConsent } = useAdContext()
135
+ return { hasConsent, setConsent }
136
+ }
137
+
138
+ export function useAdSlot(slot: AdSlot) {
139
+ const { registerSlot, unregisterSlot, refreshSlot, isReady } = useAdContext()
140
+
141
+ useEffect(() => {
142
+ if (isReady) {
143
+ registerSlot(slot)
144
+ return () => unregisterSlot(slot.id)
145
+ }
146
+ }, [slot.id, isReady, registerSlot, unregisterSlot, slot])
147
+
148
+ const refresh = useCallback(() => refreshSlot(slot.id), [slot.id, refreshSlot])
149
+
150
+ return { refresh, isReady }
151
+ }
152
+
153
+ // ==================== Provider ====================
154
+
155
+ interface WakaAdProviderProps {
156
+ children: React.ReactNode
157
+ config: AdConfig
158
+ onEvent?: (event: AdEvent) => void
159
+ }
160
+
161
+ export function WakaAdProvider({ children, config, onEvent }: WakaAdProviderProps) {
162
+ const [isReady, setIsReady] = useState(false)
163
+ const [hasConsent, setHasConsent] = useState<boolean | null>(null)
164
+ const [slots, setSlots] = useState<Map<string, AdSlot>>(new Map())
165
+ const eventQueueRef = useRef<AdEvent[]>([])
166
+
167
+ // Initialize ad network
168
+ useEffect(() => {
169
+ const initNetwork = async () => {
170
+ if (config.network === "gpt" && typeof window !== "undefined") {
171
+ // Load GPT script
172
+ if (!window.googletag) {
173
+ const script = document.createElement("script")
174
+ script.src = "https://securepubads.g.doubleclick.net/tag/js/gpt.js"
175
+ script.async = true
176
+ document.head.appendChild(script)
177
+
178
+ await new Promise<void>((resolve) => {
179
+ script.onload = () => resolve()
180
+ })
181
+ }
182
+
183
+ window.googletag = window.googletag || { cmd: [] }
184
+ window.googletag.cmd.push(() => {
185
+ window.googletag.pubads().enableSingleRequest()
186
+ if (config.enableLazyLoad) {
187
+ window.googletag.pubads().enableLazyLoad({
188
+ fetchMarginPercent: 100,
189
+ renderMarginPercent: config.lazyLoadOffset || 50,
190
+ mobileScaling: 2.0,
191
+ })
192
+ }
193
+ window.googletag.enableServices()
194
+ })
195
+ }
196
+
197
+ setIsReady(true)
198
+ }
199
+
200
+ initNetwork()
201
+ }, [config])
202
+
203
+ // Check for existing consent
204
+ useEffect(() => {
205
+ if (config.enableConsent && typeof window !== "undefined") {
206
+ // Check for TCF v2 consent
207
+ const storedConsent = localStorage.getItem("ad_consent")
208
+ if (storedConsent !== null) {
209
+ setHasConsent(storedConsent === "true")
210
+ }
211
+ } else {
212
+ setHasConsent(true) // No consent required
213
+ }
214
+ }, [config.enableConsent])
215
+
216
+ const setConsent = useCallback((consent: boolean) => {
217
+ setHasConsent(consent)
218
+ if (typeof window !== "undefined") {
219
+ localStorage.setItem("ad_consent", String(consent))
220
+ }
221
+ }, [])
222
+
223
+ const registerSlot = useCallback((slot: AdSlot) => {
224
+ setSlots((prev) => {
225
+ const newSlots = new Map(prev)
226
+ newSlots.set(slot.id, slot)
227
+ return newSlots
228
+ })
229
+ }, [])
230
+
231
+ const unregisterSlot = useCallback((slotId: string) => {
232
+ setSlots((prev) => {
233
+ const newSlots = new Map(prev)
234
+ newSlots.delete(slotId)
235
+ return newSlots
236
+ })
237
+ }, [])
238
+
239
+ const refreshSlot = useCallback(
240
+ (slotId: string) => {
241
+ if (config.network === "gpt" && window.googletag) {
242
+ window.googletag.cmd.push(() => {
243
+ window.googletag.pubads().refresh()
244
+ })
245
+ }
246
+ // For custom network, trigger re-fetch
247
+ },
248
+ [config.network]
249
+ )
250
+
251
+ const refreshAll = useCallback(() => {
252
+ slots.forEach((_, slotId) => refreshSlot(slotId))
253
+ }, [slots, refreshSlot])
254
+
255
+ const getCustomAd = useCallback(
256
+ async (slotId: string): Promise<CustomAd | null> => {
257
+ if (!config.customAdServer) return null
258
+
259
+ try {
260
+ const response = await fetch(`${config.customAdServer}/ad`, {
261
+ method: "POST",
262
+ headers: {
263
+ "Content-Type": "application/json",
264
+ ...(config.customApiKey && { Authorization: `Bearer ${config.customApiKey}` }),
265
+ },
266
+ body: JSON.stringify({
267
+ slotId,
268
+ slot: slots.get(slotId),
269
+ testMode: config.testMode,
270
+ }),
271
+ })
272
+
273
+ if (!response.ok) return null
274
+ return response.json()
275
+ } catch {
276
+ return null
277
+ }
278
+ },
279
+ [config.customAdServer, config.customApiKey, config.testMode, slots]
280
+ )
281
+
282
+ const trackEvent = useCallback(
283
+ (event: AdEvent) => {
284
+ if (config.debugMode) {
285
+ console.log("[WakaAd]", event.type, event.slotId, event.data)
286
+ }
287
+ onEvent?.(event)
288
+ eventQueueRef.current.push(event)
289
+ },
290
+ [config.debugMode, onEvent]
291
+ )
292
+
293
+ const value: AdContextValue = {
294
+ config,
295
+ isReady,
296
+ hasConsent,
297
+ setConsent,
298
+ registerSlot,
299
+ unregisterSlot,
300
+ refreshSlot,
301
+ refreshAll,
302
+ getCustomAd,
303
+ trackEvent,
304
+ slots,
305
+ }
306
+
307
+ return <AdContext.Provider value={value}>{children}</AdContext.Provider>
308
+ }
309
+
310
+ // ==================== Type augmentation for GPT ====================
311
+
312
+ declare global {
313
+ interface Window {
314
+ googletag: {
315
+ cmd: Array<() => void>
316
+ defineSlot: (adUnitPath: string, size: [number, number], divId: string) => unknown
317
+ pubads: () => {
318
+ enableSingleRequest: () => void
319
+ enableLazyLoad: (config: Record<string, unknown>) => void
320
+ refresh: () => void
321
+ setTargeting: (key: string, value: string | string[]) => void
322
+ }
323
+ enableServices: () => void
324
+ display: (divId: string) => void
325
+ }
326
+ }
327
+ }
328
+
329
+ export default WakaAdProvider
@@ -0,0 +1,113 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { useRef, useState, useEffect } from "react"
5
+ import { cn } from "../../utils/cn"
6
+ import { WakaAdBanner, type WakaAdBannerProps } from "../waka-ad-banner"
7
+
8
+ export interface WakaAdSidebarProps extends Omit<WakaAdBannerProps, "size"> {
9
+ /** Enable sticky behavior */
10
+ sticky?: boolean
11
+ /** Top offset when sticky (in pixels) */
12
+ stickyOffset?: number
13
+ /** Bottom boundary element selector */
14
+ boundarySelector?: string
15
+ /** Gap between multiple ads */
16
+ gap?: number
17
+ /** Additional ads to show below */
18
+ additionalSlots?: Array<{
19
+ slotId: string
20
+ adUnitPath?: string
21
+ }>
22
+ }
23
+
24
+ export function WakaAdSidebar({
25
+ slotId,
26
+ sticky = true,
27
+ stickyOffset = 80,
28
+ boundarySelector,
29
+ gap = 16,
30
+ additionalSlots = [],
31
+ className,
32
+ ...bannerProps
33
+ }: WakaAdSidebarProps) {
34
+ const containerRef = useRef<HTMLDivElement>(null)
35
+ const [isSticky, setIsSticky] = useState(false)
36
+ const [stickyStyle, setStickyStyle] = useState<React.CSSProperties>({})
37
+
38
+ useEffect(() => {
39
+ if (!sticky || !containerRef.current) return
40
+
41
+ const container = containerRef.current
42
+ const boundary = boundarySelector ? document.querySelector(boundarySelector) : null
43
+
44
+ const handleScroll = () => {
45
+ const containerRect = container.getBoundingClientRect()
46
+ const boundaryRect = boundary?.getBoundingClientRect()
47
+
48
+ // Check if we should be sticky
49
+ if (containerRect.top <= stickyOffset) {
50
+ // Check if we've hit the boundary
51
+ if (boundaryRect) {
52
+ const maxTop = boundaryRect.top - container.offsetHeight - gap
53
+ if (maxTop < stickyOffset) {
54
+ // Stop at boundary
55
+ setStickyStyle({
56
+ position: "absolute",
57
+ top: boundary!.offsetTop - container.offsetHeight - gap,
58
+ width: containerRect.width,
59
+ })
60
+ setIsSticky(false)
61
+ return
62
+ }
63
+ }
64
+
65
+ setIsSticky(true)
66
+ setStickyStyle({
67
+ position: "fixed",
68
+ top: stickyOffset,
69
+ width: containerRect.width,
70
+ })
71
+ } else {
72
+ setIsSticky(false)
73
+ setStickyStyle({})
74
+ }
75
+ }
76
+
77
+ window.addEventListener("scroll", handleScroll, { passive: true })
78
+ handleScroll() // Initial check
79
+
80
+ return () => window.removeEventListener("scroll", handleScroll)
81
+ }, [sticky, stickyOffset, boundarySelector, gap])
82
+
83
+ return (
84
+ <div
85
+ ref={containerRef}
86
+ className={cn("relative", className)}
87
+ data-sticky={isSticky}
88
+ >
89
+ <div style={stickyStyle}>
90
+ <div className="flex flex-col" style={{ gap }}>
91
+ <WakaAdBanner
92
+ slotId={slotId}
93
+ size="skyscraper"
94
+ {...bannerProps}
95
+ />
96
+
97
+ {additionalSlots.map((slot, index) => (
98
+ <WakaAdBanner
99
+ key={slot.slotId}
100
+ slotId={slot.slotId}
101
+ size="rectangle"
102
+ adUnitPath={slot.adUnitPath}
103
+ lazyLoad
104
+ {...bannerProps}
105
+ />
106
+ ))}
107
+ </div>
108
+ </div>
109
+ </div>
110
+ )
111
+ }
112
+
113
+ export default WakaAdSidebar
@@ -0,0 +1,125 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { useState, useEffect } from "react"
5
+ import { cn } from "../../utils/cn"
6
+ import { WakaAdBanner, type WakaAdBannerProps } from "../waka-ad-banner"
7
+ import { X } from "lucide-react"
8
+
9
+ export interface WakaAdStickyFooterProps extends Omit<WakaAdBannerProps, "size"> {
10
+ /** Allow user to dismiss */
11
+ dismissable?: boolean
12
+ /** Show close button after delay (seconds) */
13
+ showCloseAfter?: number
14
+ /** Animation style */
15
+ animation?: "slide" | "fade" | "none"
16
+ /** Safe area padding (for mobile notches) */
17
+ safeAreaPadding?: boolean
18
+ /** Background blur */
19
+ blur?: boolean
20
+ /** Container class name */
21
+ containerClassName?: string
22
+ }
23
+
24
+ export function WakaAdStickyFooter({
25
+ slotId,
26
+ dismissable = true,
27
+ showCloseAfter = 0,
28
+ animation = "slide",
29
+ safeAreaPadding = true,
30
+ blur = true,
31
+ className,
32
+ containerClassName,
33
+ ...bannerProps
34
+ }: WakaAdStickyFooterProps) {
35
+ const [isVisible, setIsVisible] = useState(true)
36
+ const [canClose, setCanClose] = useState(showCloseAfter === 0)
37
+ const [isAnimatingOut, setIsAnimatingOut] = useState(false)
38
+
39
+ // Delayed close button
40
+ useEffect(() => {
41
+ if (showCloseAfter > 0 && !canClose) {
42
+ const timer = setTimeout(() => {
43
+ setCanClose(true)
44
+ }, showCloseAfter * 1000)
45
+ return () => clearTimeout(timer)
46
+ }
47
+ }, [showCloseAfter, canClose])
48
+
49
+ const handleClose = () => {
50
+ if (!dismissable || !canClose) return
51
+
52
+ if (animation !== "none") {
53
+ setIsAnimatingOut(true)
54
+ setTimeout(() => {
55
+ setIsVisible(false)
56
+ }, 300)
57
+ } else {
58
+ setIsVisible(false)
59
+ }
60
+ }
61
+
62
+ if (!isVisible) return null
63
+
64
+ const animationClasses = {
65
+ slide: cn(
66
+ "transition-transform duration-300",
67
+ isAnimatingOut ? "translate-y-full" : "translate-y-0"
68
+ ),
69
+ fade: cn(
70
+ "transition-opacity duration-300",
71
+ isAnimatingOut ? "opacity-0" : "opacity-100"
72
+ ),
73
+ none: "",
74
+ }
75
+
76
+ return (
77
+ <div
78
+ className={cn(
79
+ "fixed bottom-0 left-0 right-0 z-40",
80
+ blur && "backdrop-blur-sm",
81
+ safeAreaPadding && "pb-safe",
82
+ animationClasses[animation],
83
+ containerClassName
84
+ )}
85
+ role="complementary"
86
+ aria-label="Advertisement"
87
+ >
88
+ <div
89
+ className={cn(
90
+ "relative flex items-center justify-center",
91
+ "bg-background/95 border-t border-border shadow-lg",
92
+ "py-2 px-4",
93
+ className
94
+ )}
95
+ >
96
+ {/* Close button */}
97
+ {dismissable && (
98
+ <button
99
+ onClick={handleClose}
100
+ disabled={!canClose}
101
+ className={cn(
102
+ "absolute top-1 right-1 p-1 rounded-full transition-all",
103
+ canClose
104
+ ? "bg-muted hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground cursor-pointer"
105
+ : "bg-muted/50 text-muted-foreground/50 cursor-not-allowed"
106
+ )}
107
+ aria-label="Close advertisement"
108
+ >
109
+ <X className="h-4 w-4" />
110
+ </button>
111
+ )}
112
+
113
+ {/* Ad content */}
114
+ <WakaAdBanner
115
+ slotId={slotId}
116
+ size="mobile-banner"
117
+ showBadge={false}
118
+ {...bannerProps}
119
+ />
120
+ </div>
121
+ </div>
122
+ )
123
+ }
124
+
125
+ export default WakaAdStickyFooter
@@ -55,41 +55,42 @@ export interface WakaBadgeShowcaseProps {
55
55
  // Rarity Configuration
56
56
  // ============================================================================
57
57
 
58
+ // Rarity configuration using CSS variables for theme support
58
59
  const rarityConfig = {
59
60
  common: {
60
61
  gradient: "from-slate-400 to-slate-500",
61
- glow: "#94a3b8",
62
+ glow: "hsl(var(--muted-foreground))",
62
63
  border: "border-slate-400",
63
64
  bg: "bg-slate-100 dark:bg-slate-800",
64
65
  text: "text-slate-600 dark:text-slate-300",
65
- shine: "rgba(148, 163, 184, 0.6)",
66
+ shine: "hsl(var(--muted-foreground) / 0.6)",
66
67
  label: "Common",
67
68
  },
68
69
  rare: {
69
70
  gradient: "from-blue-400 to-blue-600",
70
- glow: "#3b82f6",
71
+ glow: "hsl(var(--info))",
71
72
  border: "border-blue-400",
72
73
  bg: "bg-blue-100 dark:bg-blue-900",
73
74
  text: "text-blue-600 dark:text-blue-300",
74
- shine: "rgba(59, 130, 246, 0.6)",
75
+ shine: "hsl(var(--info) / 0.6)",
75
76
  label: "Rare",
76
77
  },
77
78
  epic: {
78
79
  gradient: "from-purple-400 to-purple-600",
79
- glow: "#a855f7",
80
+ glow: "hsl(var(--chart-3))",
80
81
  border: "border-purple-400",
81
82
  bg: "bg-purple-100 dark:bg-purple-900",
82
83
  text: "text-purple-600 dark:text-purple-300",
83
- shine: "rgba(168, 85, 247, 0.6)",
84
+ shine: "hsl(var(--chart-3) / 0.6)",
84
85
  label: "Epic",
85
86
  },
86
87
  legendary: {
87
88
  gradient: "from-amber-400 via-orange-500 to-red-500",
88
- glow: "#f59e0b",
89
+ glow: "hsl(var(--warning))",
89
90
  border: "border-amber-400",
90
91
  bg: "bg-amber-100 dark:bg-amber-900",
91
92
  text: "text-amber-600 dark:text-amber-300",
92
- shine: "rgba(245, 158, 11, 0.8)",
93
+ shine: "hsl(var(--warning) / 0.8)",
93
94
  label: "Legendary",
94
95
  },
95
96
  }
@@ -195,9 +196,9 @@ function NewBadgeIndicator() {
195
196
  <>
196
197
  <div className="absolute -top-1 -right-1 z-20">
197
198
  <span className="relative flex h-4 w-4">
198
- <span className="animate-badge-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
199
- <span className="relative inline-flex rounded-full h-4 w-4 bg-green-500 items-center justify-center">
200
- <span className="text-[8px] font-bold text-white">!</span>
199
+ <span className="animate-badge-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75" />
200
+ <span className="relative inline-flex rounded-full h-4 w-4 bg-success items-center justify-center">
201
+ <span className="text-[8px] font-bold text-success-foreground">!</span>
201
202
  </span>
202
203
  </span>
203
204
  </div>
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as React from "react"
4
4
  import { cn } from "../../utils/cn"
5
+ import { safeNavigate } from "../../utils/security"
5
6
  import { Command, Search, ArrowRight, CornerDownLeft, Hash, Settings, User, FileText, Zap } from "lucide-react"
6
7
 
7
8
  // ============================================================================
@@ -214,7 +215,7 @@ export function WakaCommandBar({
214
215
  onSelect?.(item)
215
216
  item.action?.()
216
217
  if (item.href) {
217
- window.location.href = item.href
218
+ safeNavigate(item.href)
218
219
  }
219
220
  onOpenChange(false)
220
221
  }