@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,275 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { useRef, useEffect, useState, useCallback } from "react"
5
+ import { cn } from "../../utils/cn"
6
+ import { useAdContext, useAdVisibility, type CustomAd } from "../waka-ad-provider"
7
+ import { WakaSponsoredBadge } from "../waka-sponsored-badge"
8
+ import { ExternalLink } from "lucide-react"
9
+
10
+ export type SponsoredCardVariant = "article" | "product" | "compact" | "horizontal"
11
+
12
+ export interface WakaSponsoredCardProps {
13
+ /** Unique slot ID */
14
+ slotId: string
15
+ /** Card variant */
16
+ variant?: SponsoredCardVariant
17
+ /** GPT ad unit path */
18
+ adUnitPath?: string
19
+ /** Show image */
20
+ showImage?: boolean
21
+ /** Show description */
22
+ showDescription?: boolean
23
+ /** Show CTA button */
24
+ showCta?: boolean
25
+ /** Image aspect ratio */
26
+ aspectRatio?: "video" | "square" | "portrait"
27
+ /** Custom class name */
28
+ className?: string
29
+ /** Callback when clicked */
30
+ onClick?: () => void
31
+ /** Callback when loaded */
32
+ onLoad?: () => void
33
+ }
34
+
35
+ export function WakaSponsoredCard({
36
+ slotId,
37
+ variant = "article",
38
+ adUnitPath,
39
+ showImage = true,
40
+ showDescription = true,
41
+ showCta = true,
42
+ aspectRatio = "video",
43
+ className,
44
+ onClick,
45
+ onLoad,
46
+ }: WakaSponsoredCardProps) {
47
+ const containerRef = useRef<HTMLDivElement>(null)
48
+ const { config, isReady, hasConsent, getCustomAd, trackEvent } = useAdContext()
49
+ const { isVisible } = useAdVisibility(containerRef)
50
+
51
+ const [status, setStatus] = useState<"loading" | "loaded" | "error">("loading")
52
+ const [ad, setAd] = useState<CustomAd | null>(null)
53
+
54
+ // Load ad
55
+ useEffect(() => {
56
+ if (!isReady || !isVisible || hasConsent === false) return
57
+
58
+ const loadAd = async () => {
59
+ try {
60
+ const customAd = await getCustomAd(slotId)
61
+ if (customAd) {
62
+ setAd(customAd)
63
+ setStatus("loaded")
64
+ trackEvent({ type: "loaded", slotId, timestamp: new Date() })
65
+ trackEvent({ type: "impression", slotId, timestamp: new Date() })
66
+ onLoad?.()
67
+
68
+ // Fire tracking
69
+ if (customAd.impressionUrl) {
70
+ fetch(customAd.impressionUrl, { mode: "no-cors" }).catch(() => {})
71
+ }
72
+ } else {
73
+ setStatus("error")
74
+ }
75
+ } catch {
76
+ setStatus("error")
77
+ }
78
+ }
79
+
80
+ loadAd()
81
+ }, [isReady, isVisible, hasConsent, slotId, getCustomAd, trackEvent, onLoad])
82
+
83
+ const handleClick = useCallback(() => {
84
+ if (!ad) return
85
+
86
+ trackEvent({ type: "click", slotId, timestamp: new Date() })
87
+ onClick?.()
88
+
89
+ if (ad.clickUrl) {
90
+ fetch(ad.clickUrl, { mode: "no-cors" }).catch(() => {})
91
+ }
92
+ if (ad.targetUrl) {
93
+ window.open(ad.targetUrl, "_blank", "noopener,noreferrer")
94
+ }
95
+ }, [ad, slotId, trackEvent, onClick])
96
+
97
+ const aspectRatioClasses = {
98
+ video: "aspect-video",
99
+ square: "aspect-square",
100
+ portrait: "aspect-[3/4]",
101
+ }
102
+
103
+ if (status === "error" || !ad) {
104
+ return null // Don't show anything if no ad
105
+ }
106
+
107
+ if (status === "loading") {
108
+ return (
109
+ <div
110
+ ref={containerRef}
111
+ className={cn(
112
+ "rounded-lg border bg-muted animate-pulse",
113
+ variant === "horizontal" ? "h-32" : "h-64",
114
+ className
115
+ )}
116
+ />
117
+ )
118
+ }
119
+
120
+ // Horizontal variant
121
+ if (variant === "horizontal") {
122
+ return (
123
+ <div ref={containerRef} className={cn("group", className)}>
124
+ <button
125
+ onClick={handleClick}
126
+ className="w-full flex gap-4 p-3 rounded-lg border bg-card hover:bg-accent/50 transition-colors text-left"
127
+ >
128
+ {showImage && ad.imageUrl && (
129
+ <div className="w-24 h-24 flex-shrink-0 rounded-md overflow-hidden bg-muted">
130
+ <img
131
+ src={ad.imageUrl}
132
+ alt=""
133
+ className="w-full h-full object-cover"
134
+ />
135
+ </div>
136
+ )}
137
+
138
+ <div className="flex-1 min-w-0">
139
+ <div className="flex items-start justify-between gap-2 mb-1">
140
+ <h3 className="font-semibold text-sm line-clamp-2 group-hover:text-primary transition-colors">
141
+ {ad.title}
142
+ </h3>
143
+ <WakaSponsoredBadge sponsor={ad.sponsor} size="sm" showIcon={false} />
144
+ </div>
145
+
146
+ {showDescription && ad.description && (
147
+ <p className="text-xs text-muted-foreground line-clamp-2 mb-2">
148
+ {ad.description}
149
+ </p>
150
+ )}
151
+
152
+ {showCta && ad.cta && (
153
+ <span className="text-xs text-primary font-medium inline-flex items-center gap-1">
154
+ {ad.cta}
155
+ <ExternalLink className="h-3 w-3" />
156
+ </span>
157
+ )}
158
+ </div>
159
+ </button>
160
+ </div>
161
+ )
162
+ }
163
+
164
+ // Compact variant
165
+ if (variant === "compact") {
166
+ return (
167
+ <div ref={containerRef} className={cn("group", className)}>
168
+ <button
169
+ onClick={handleClick}
170
+ className="w-full p-2 rounded-lg border bg-card hover:bg-accent/50 transition-colors text-left"
171
+ >
172
+ <div className="flex items-center gap-2 mb-1">
173
+ <WakaSponsoredBadge sponsor={ad.sponsor} size="sm" showIcon={false} />
174
+ </div>
175
+ <h3 className="font-medium text-sm line-clamp-2 group-hover:text-primary transition-colors">
176
+ {ad.title}
177
+ </h3>
178
+ </button>
179
+ </div>
180
+ )
181
+ }
182
+
183
+ // Product variant
184
+ if (variant === "product") {
185
+ return (
186
+ <div ref={containerRef} className={cn("group", className)}>
187
+ <button
188
+ onClick={handleClick}
189
+ className="w-full rounded-lg border bg-card overflow-hidden hover:shadow-md transition-shadow text-left"
190
+ >
191
+ {showImage && ad.imageUrl && (
192
+ <div className={cn("relative bg-muted", aspectRatioClasses[aspectRatio])}>
193
+ <img
194
+ src={ad.imageUrl}
195
+ alt=""
196
+ className="absolute inset-0 w-full h-full object-cover"
197
+ />
198
+ <div className="absolute top-2 left-2">
199
+ <WakaSponsoredBadge variant="subtle" size="sm" showIcon={false} />
200
+ </div>
201
+ </div>
202
+ )}
203
+
204
+ <div className="p-4">
205
+ <h3 className="font-semibold text-sm mb-1 group-hover:text-primary transition-colors line-clamp-2">
206
+ {ad.title}
207
+ </h3>
208
+
209
+ {showDescription && ad.description && (
210
+ <p className="text-xs text-muted-foreground line-clamp-2 mb-3">
211
+ {ad.description}
212
+ </p>
213
+ )}
214
+
215
+ {showCta && ad.cta && (
216
+ <span className="inline-flex items-center justify-center w-full px-4 py-2 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 transition-colors">
217
+ {ad.cta}
218
+ </span>
219
+ )}
220
+ </div>
221
+ </button>
222
+ </div>
223
+ )
224
+ }
225
+
226
+ // Article variant (default)
227
+ return (
228
+ <div ref={containerRef} className={cn("group", className)}>
229
+ <button
230
+ onClick={handleClick}
231
+ className="w-full rounded-lg border bg-card overflow-hidden hover:shadow-md transition-shadow text-left"
232
+ >
233
+ {showImage && ad.imageUrl && (
234
+ <div className={cn("relative bg-muted", aspectRatioClasses[aspectRatio])}>
235
+ <img
236
+ src={ad.imageUrl}
237
+ alt=""
238
+ className="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
239
+ />
240
+ <div className="absolute top-2 right-2">
241
+ <WakaSponsoredBadge variant="subtle" size="sm" />
242
+ </div>
243
+ </div>
244
+ )}
245
+
246
+ <div className="p-4">
247
+ {!showImage && (
248
+ <div className="mb-2">
249
+ <WakaSponsoredBadge sponsor={ad.sponsor} size="sm" />
250
+ </div>
251
+ )}
252
+
253
+ <h3 className="font-semibold mb-2 group-hover:text-primary transition-colors line-clamp-2">
254
+ {ad.title}
255
+ </h3>
256
+
257
+ {showDescription && ad.description && (
258
+ <p className="text-sm text-muted-foreground line-clamp-3 mb-3">
259
+ {ad.description}
260
+ </p>
261
+ )}
262
+
263
+ {showCta && ad.cta && (
264
+ <span className="text-sm text-primary font-medium inline-flex items-center gap-1">
265
+ {ad.cta}
266
+ <ExternalLink className="h-4 w-4" />
267
+ </span>
268
+ )}
269
+ </div>
270
+ </button>
271
+ </div>
272
+ )
273
+ }
274
+
275
+ export default WakaSponsoredCard
@@ -0,0 +1,127 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { WakaSponsoredCard, type SponsoredCardVariant } from "../waka-sponsored-card"
6
+
7
+ export interface WakaSponsoredFeedProps<T> {
8
+ /** Content items */
9
+ items: T[]
10
+ /** Render function for content items */
11
+ renderItem: (item: T, index: number) => React.ReactNode
12
+ /** Ad slot IDs to inject */
13
+ adSlots: Array<{
14
+ slotId: string
15
+ adUnitPath?: string
16
+ }>
17
+ /** Insert ad every N items */
18
+ adFrequency?: number
19
+ /** Starting position for first ad (0-indexed) */
20
+ adStartPosition?: number
21
+ /** Card variant for ads */
22
+ adVariant?: SponsoredCardVariant
23
+ /** Maximum number of ads to show */
24
+ maxAds?: number
25
+ /** Grid columns for items */
26
+ columns?: 1 | 2 | 3 | 4
27
+ /** Gap between items */
28
+ gap?: "sm" | "md" | "lg"
29
+ /** Custom class name */
30
+ className?: string
31
+ /** Key extractor for items */
32
+ keyExtractor?: (item: T, index: number) => string
33
+ }
34
+
35
+ export function WakaSponsoredFeed<T>({
36
+ items,
37
+ renderItem,
38
+ adSlots,
39
+ adFrequency = 5,
40
+ adStartPosition = 2,
41
+ adVariant = "article",
42
+ maxAds,
43
+ columns = 1,
44
+ gap = "md",
45
+ className,
46
+ keyExtractor,
47
+ }: WakaSponsoredFeedProps<T>) {
48
+ const gapClasses = {
49
+ sm: "gap-2",
50
+ md: "gap-4",
51
+ lg: "gap-6",
52
+ }
53
+
54
+ const columnClasses = {
55
+ 1: "grid-cols-1",
56
+ 2: "grid-cols-1 sm:grid-cols-2",
57
+ 3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
58
+ 4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
59
+ }
60
+
61
+ // Calculate positions for ads
62
+ const getAdsForPosition = (position: number): typeof adSlots[number] | null => {
63
+ if (position < adStartPosition) return null
64
+
65
+ const adIndex = Math.floor((position - adStartPosition) / (adFrequency + 1))
66
+ const isAdPosition = (position - adStartPosition) % (adFrequency + 1) === 0
67
+
68
+ if (!isAdPosition) return null
69
+ if (maxAds !== undefined && adIndex >= maxAds) return null
70
+ if (adIndex >= adSlots.length) return null
71
+
72
+ return adSlots[adIndex]
73
+ }
74
+
75
+ // Build the feed with interspersed ads
76
+ const feedItems: Array<{ type: "content"; item: T; index: number } | { type: "ad"; slot: typeof adSlots[number] }> = []
77
+ let contentIndex = 0
78
+ let position = 0
79
+
80
+ while (contentIndex < items.length) {
81
+ const adSlot = getAdsForPosition(position)
82
+
83
+ if (adSlot) {
84
+ feedItems.push({ type: "ad", slot: adSlot })
85
+ } else {
86
+ feedItems.push({ type: "content", item: items[contentIndex], index: contentIndex })
87
+ contentIndex++
88
+ }
89
+ position++
90
+ }
91
+
92
+ return (
93
+ <div
94
+ className={cn(
95
+ "grid",
96
+ columnClasses[columns],
97
+ gapClasses[gap],
98
+ className
99
+ )}
100
+ >
101
+ {feedItems.map((feedItem, idx) => {
102
+ if (feedItem.type === "ad") {
103
+ return (
104
+ <WakaSponsoredCard
105
+ key={`ad-${feedItem.slot.slotId}`}
106
+ slotId={feedItem.slot.slotId}
107
+ adUnitPath={feedItem.slot.adUnitPath}
108
+ variant={adVariant}
109
+ />
110
+ )
111
+ }
112
+
113
+ const key = keyExtractor
114
+ ? keyExtractor(feedItem.item, feedItem.index)
115
+ : `item-${feedItem.index}`
116
+
117
+ return (
118
+ <div key={key}>
119
+ {renderItem(feedItem.item, feedItem.index)}
120
+ </div>
121
+ )
122
+ })}
123
+ </div>
124
+ )
125
+ }
126
+
127
+ export default WakaSponsoredFeed
@@ -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 { Search, X, Clock, ArrowRight, FileText, User, Settings, Folder, ExternalLink } from "lucide-react"
6
7
 
7
8
  // ============================================================================
@@ -196,7 +197,7 @@ export function WakaSpotlight({
196
197
  const handleSelect = (item: SpotlightResult) => {
197
198
  item.action?.()
198
199
  if (item.href) {
199
- window.location.href = item.href
200
+ safeNavigate(item.href)
200
201
  }
201
202
  onOpenChange(false)
202
203
  }
@@ -64,13 +64,13 @@ const sizeConfig = {
64
64
  }
65
65
 
66
66
  // ============================================================================
67
- // Default colors
67
+ // Default colors (using CSS variables for theme support)
68
68
  // ============================================================================
69
69
 
70
70
  const defaultColors = {
71
- primary: "#22c55e", // green-500
72
- secondary: "#3b82f6", // blue-500
73
- accent: "#f59e0b", // amber-500
71
+ primary: "hsl(var(--success))",
72
+ secondary: "hsl(var(--info))",
73
+ accent: "hsl(var(--warning))",
74
74
  }
75
75
 
76
76
  // ============================================================================