@oddsmith/ui 0.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.
Files changed (185) hide show
  1. package/.eleventy.cjs +14 -0
  2. package/LICENSE +28 -0
  3. package/README.md +118 -0
  4. package/custom-elements.json +1539 -0
  5. package/docs/_README/index.html +4 -0
  6. package/docs/api/index.html +2100 -0
  7. package/docs/components.bundle.js +1669 -0
  8. package/docs/components.bundle.js.map +1 -0
  9. package/docs/docs.css +162 -0
  10. package/docs/examples/index.html +56 -0
  11. package/docs/index.html +53 -0
  12. package/docs/install/index.html +45 -0
  13. package/docs/prism-okaidia.css +123 -0
  14. package/docs-src/.nojekyll +0 -0
  15. package/docs-src/_README.md +7 -0
  16. package/docs-src/_data/api.11tydata.js +8 -0
  17. package/docs-src/_includes/example.11ty.js +35 -0
  18. package/docs-src/_includes/footer.11ty.js +6 -0
  19. package/docs-src/_includes/header.11ty.js +7 -0
  20. package/docs-src/_includes/nav.11ty.js +11 -0
  21. package/docs-src/_includes/page.11ty.js +32 -0
  22. package/docs-src/_includes/relative-path.cjs +9 -0
  23. package/docs-src/api.11ty.js +85 -0
  24. package/docs-src/bundle.ts +9 -0
  25. package/docs-src/docs.css +162 -0
  26. package/docs-src/examples/index.md +15 -0
  27. package/docs-src/index.md +39 -0
  28. package/docs-src/install.md +28 -0
  29. package/docs-src/package.json +3 -0
  30. package/index.html +19 -0
  31. package/karma.conf.cjs +24 -0
  32. package/main.css +210 -0
  33. package/main.ts +124 -0
  34. package/package.json +86 -0
  35. package/previews/casino.ts +12 -0
  36. package/previews/catalog.ts +94 -0
  37. package/previews/leaderboard-v1.ts +12 -0
  38. package/previews/leaderboard-v2.ts +17 -0
  39. package/previews/sample-data.ts +101 -0
  40. package/previews/sf-leaderboard.ts +100 -0
  41. package/previews/sf-live-feed.ts +15 -0
  42. package/previews/streaks.ts +40 -0
  43. package/previews/types.ts +18 -0
  44. package/src/components/README.md +16 -0
  45. package/src/components/casino-leaderboard/casino-leaderboard.html +80 -0
  46. package/src/components/casino-leaderboard/casino-leaderboard.scss +585 -0
  47. package/src/components/casino-leaderboard/casino-leaderboard.ts +136 -0
  48. package/src/components/casino-leaderboard/data.ts +111 -0
  49. package/src/components/casino-leaderboard/index.ts +5 -0
  50. package/src/components/casino-leaderboard/todo.txt +2 -0
  51. package/src/components/casino-leaderboard/types.ts +19 -0
  52. package/src/components/leaderboard/components/leaderboard.ts +373 -0
  53. package/src/components/leaderboard/components/player-card.ts +342 -0
  54. package/src/components/leaderboard/components/ui.ts +452 -0
  55. package/src/components/leaderboard/data.ts +152 -0
  56. package/src/components/leaderboard/index.ts +2 -0
  57. package/src/components/leaderboard/main.ts +42 -0
  58. package/src/components/leaderboard/styles.ts +67 -0
  59. package/src/components/leaderboard/types.ts +28 -0
  60. package/src/components/leaderboard-v2/components/sf-leaderboard-player.ts +451 -0
  61. package/src/components/leaderboard-v2/components/sf-leaderboard-ui.ts +512 -0
  62. package/src/components/leaderboard-v2/components/sf-leaderboard.ts +205 -0
  63. package/src/components/leaderboard-v2/constants.ts +16 -0
  64. package/src/components/leaderboard-v2/demo/sample-data.ts +152 -0
  65. package/src/components/leaderboard-v2/events.ts +13 -0
  66. package/src/components/leaderboard-v2/icons.ts +22 -0
  67. package/src/components/leaderboard-v2/index.ts +23 -0
  68. package/src/components/leaderboard-v2/sf-leaderboard.html +1 -0
  69. package/src/components/leaderboard-v2/sf-leaderboard.scss +382 -0
  70. package/src/components/leaderboard-v2/tokens.ts +35 -0
  71. package/src/components/leaderboard-v2/types.ts +30 -0
  72. package/src/components/sf-leaderboard/index.ts +77 -0
  73. package/src/components/sf-leaderboard/sections/footer-section/footer-section.host.ts +3 -0
  74. package/src/components/sf-leaderboard/sections/footer-section/footer-section.html +3 -0
  75. package/src/components/sf-leaderboard/sections/footer-section/footer-section.scss +18 -0
  76. package/src/components/sf-leaderboard/sections/footer-section/footer-section.ts +22 -0
  77. package/src/components/sf-leaderboard/sections/header-section/header-section.host.ts +14 -0
  78. package/src/components/sf-leaderboard/sections/header-section/header-section.html +27 -0
  79. package/src/components/sf-leaderboard/sections/header-section/header-section.scss +189 -0
  80. package/src/components/sf-leaderboard/sections/header-section/header-section.ts +70 -0
  81. package/src/components/sf-leaderboard/sections/ranking-section/ranking-section.host.ts +22 -0
  82. package/src/components/sf-leaderboard/sections/ranking-section/ranking-section.html +38 -0
  83. package/src/components/sf-leaderboard/sections/ranking-section/ranking-section.scss +99 -0
  84. package/src/components/sf-leaderboard/sections/ranking-section/ranking-section.ts +121 -0
  85. package/src/components/sf-leaderboard/sections/stats-section/stats-section.host.ts +8 -0
  86. package/src/components/sf-leaderboard/sections/stats-section/stats-section.html +6 -0
  87. package/src/components/sf-leaderboard/sections/stats-section/stats-section.scss +44 -0
  88. package/src/components/sf-leaderboard/sections/stats-section/stats-section.ts +41 -0
  89. package/src/components/sf-leaderboard/sections/table-section/table-section.host.ts +17 -0
  90. package/src/components/sf-leaderboard/sections/table-section/table-section.html +19 -0
  91. package/src/components/sf-leaderboard/sections/table-section/table-section.scss +37 -0
  92. package/src/components/sf-leaderboard/sections/table-section/table-section.ts +108 -0
  93. package/src/components/sf-leaderboard/services/index.ts +22 -0
  94. package/src/components/sf-leaderboard/services/sf-leaderboard-data.service.ts +54 -0
  95. package/src/components/sf-leaderboard/services/sf-leaderboard.state.ts +160 -0
  96. package/src/components/sf-leaderboard/shared/components/activity-feed/activity-feed.host.ts +7 -0
  97. package/src/components/sf-leaderboard/shared/components/activity-feed/activity-feed.html +10 -0
  98. package/src/components/sf-leaderboard/shared/components/activity-feed/activity-feed.scss +180 -0
  99. package/src/components/sf-leaderboard/shared/components/activity-feed/activity-feed.ts +88 -0
  100. package/src/components/sf-leaderboard/shared/components/filters/filters.host.ts +12 -0
  101. package/src/components/sf-leaderboard/shared/components/filters/filters.html +22 -0
  102. package/src/components/sf-leaderboard/shared/components/filters/filters.scss +122 -0
  103. package/src/components/sf-leaderboard/shared/components/filters/filters.ts +75 -0
  104. package/src/components/sf-leaderboard/shared/components/player-avatar/player-avatar.host.ts +9 -0
  105. package/src/components/sf-leaderboard/shared/components/player-avatar/player-avatar.html +5 -0
  106. package/src/components/sf-leaderboard/shared/components/player-avatar/player-avatar.scss +81 -0
  107. package/src/components/sf-leaderboard/shared/components/player-avatar/player-avatar.ts +34 -0
  108. package/src/components/sf-leaderboard/shared/components/podium/map-players.ts +24 -0
  109. package/src/components/sf-leaderboard/shared/components/podium/podium.host.ts +10 -0
  110. package/src/components/sf-leaderboard/shared/components/podium/podium.html +53 -0
  111. package/src/components/sf-leaderboard/shared/components/podium/podium.scss +580 -0
  112. package/src/components/sf-leaderboard/shared/components/podium/podium.ts +49 -0
  113. package/src/components/sf-leaderboard/shared/components/podium/podium.types.ts +9 -0
  114. package/src/components/sf-leaderboard/shared/components/rank-badge/rank-badge.host.ts +11 -0
  115. package/src/components/sf-leaderboard/shared/components/rank-badge/rank-badge.html +9 -0
  116. package/src/components/sf-leaderboard/shared/components/rank-badge/rank-badge.scss +98 -0
  117. package/src/components/sf-leaderboard/shared/components/rank-badge/rank-badge.ts +63 -0
  118. package/src/components/sf-leaderboard/shared/components/stat-card/stat-card.host.ts +9 -0
  119. package/src/components/sf-leaderboard/shared/components/stat-card/stat-card.html +15 -0
  120. package/src/components/sf-leaderboard/shared/components/stat-card/stat-card.scss +210 -0
  121. package/src/components/sf-leaderboard/shared/components/stat-card/stat-card.ts +36 -0
  122. package/src/components/sf-leaderboard/shared/components/table/table.host.ts +5 -0
  123. package/src/components/sf-leaderboard/shared/components/table/table.html +11 -0
  124. package/src/components/sf-leaderboard/shared/components/table/table.scss +212 -0
  125. package/src/components/sf-leaderboard/shared/components/table/table.ts +111 -0
  126. package/src/components/sf-leaderboard/shared/constants/defaults.ts +7 -0
  127. package/src/components/sf-leaderboard/shared/constants/filters.ts +16 -0
  128. package/src/components/sf-leaderboard/shared/constants/index.ts +5 -0
  129. package/src/components/sf-leaderboard/shared/constants/player-stats.ts +3 -0
  130. package/src/components/sf-leaderboard/shared/constants/stats-overview.ts +38 -0
  131. package/src/components/sf-leaderboard/shared/constants/tags.ts +16 -0
  132. package/src/components/sf-leaderboard/shared/styles/_section.scss +35 -0
  133. package/src/components/sf-leaderboard/shared/types/data.ts +29 -0
  134. package/src/components/sf-leaderboard/shared/types/events.ts +30 -0
  135. package/src/components/sf-leaderboard/shared/types/player-stats.ts +3 -0
  136. package/src/components/sf-leaderboard/shared/types/sections.ts +100 -0
  137. package/src/components/sf-leaderboard/shared/utils/utils.ts +17 -0
  138. package/src/components/sf-leaderboard/theme/THEMING.md +54 -0
  139. package/src/components/sf-leaderboard/theme/context.ts +16 -0
  140. package/src/components/sf-leaderboard/theme/default-theme.ts +4 -0
  141. package/src/components/sf-leaderboard/theme/hex-to-rgb.ts +25 -0
  142. package/src/components/sf-leaderboard/theme/index.ts +18 -0
  143. package/src/components/sf-leaderboard/theme/inject-theme.ts +39 -0
  144. package/src/components/sf-leaderboard/theme/load-theme.ts +26 -0
  145. package/src/components/sf-leaderboard/theme/merge-theme.ts +59 -0
  146. package/src/components/sf-leaderboard/theme/scss/_colors.scss +101 -0
  147. package/src/components/sf-leaderboard/theme/scss/shared.scss +123 -0
  148. package/src/components/sf-leaderboard/theme/styles.ts +6 -0
  149. package/src/components/sf-leaderboard/theme/theme-to-css-vars.ts +99 -0
  150. package/src/components/sf-leaderboard/theme/themes/fallback.json +62 -0
  151. package/src/components/sf-leaderboard/theme/themes/red.json +62 -0
  152. package/src/components/sf-leaderboard/theme/types.ts +71 -0
  153. package/src/components/sf-live-feed/components/avatar/avatar.host.ts +5 -0
  154. package/src/components/sf-live-feed/components/avatar/avatar.html +3 -0
  155. package/src/components/sf-live-feed/components/avatar/avatar.scss +24 -0
  156. package/src/components/sf-live-feed/components/avatar/avatar.ts +27 -0
  157. package/src/components/sf-live-feed/components/sf-live-feed/sf-live-feed.host.ts +8 -0
  158. package/src/components/sf-live-feed/components/sf-live-feed/sf-live-feed.html +10 -0
  159. package/src/components/sf-live-feed/components/sf-live-feed/sf-live-feed.scss +177 -0
  160. package/src/components/sf-live-feed/components/sf-live-feed/sf-live-feed.ts +65 -0
  161. package/src/components/sf-live-feed/constants.ts +4 -0
  162. package/src/components/sf-live-feed/demo/sample-data.ts +34 -0
  163. package/src/components/sf-live-feed/index.ts +19 -0
  164. package/src/components/sf-live-feed/styles/theme.scss +19 -0
  165. package/src/components/sf-live-feed/styles/theme.ts +5 -0
  166. package/src/components/sf-live-feed/types.ts +19 -0
  167. package/src/components/sf-live-feed/utils.ts +17 -0
  168. package/src/components/streaks/constants.ts +17 -0
  169. package/src/components/streaks/demo/sample-steps.ts +10 -0
  170. package/src/components/streaks/events.ts +8 -0
  171. package/src/components/streaks/index.ts +16 -0
  172. package/src/components/streaks/sf-streaks.html +26 -0
  173. package/src/components/streaks/sf-streaks.scss +351 -0
  174. package/src/components/streaks/sf-streaks.ts +235 -0
  175. package/src/components/streaks/types.ts +7 -0
  176. package/src/lib/lit/component.ts +10 -0
  177. package/src/lib/lit/safe-custom-element.ts +12 -0
  178. package/src/lib/lit/scss.ts +6 -0
  179. package/src/vite-env.d.ts +18 -0
  180. package/styles/global.css +125 -0
  181. package/todo.txt +54 -0
  182. package/tsconfig.json +31 -0
  183. package/vite.config.ts +56 -0
  184. package/vite.docs.config.ts +33 -0
  185. package/vite.lit-html-plugin.ts +43 -0
@@ -0,0 +1,512 @@
1
+ import { LitElement, html } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
4
+ import { css } from 'lit';
5
+ import { lbTokens } from '../tokens.js';
6
+ import { getIcon } from '../icons.js';
7
+ import type { LbBadge } from '../types.js';
8
+
9
+ @customElement('sf-lb-rank-badge')
10
+ class SfLbRankBadge extends LitElement {
11
+ static styles = [
12
+ lbTokens,
13
+ css`
14
+ .badge {
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ gap: 0.25rem;
19
+ border-radius: 8px;
20
+ font-family: var(--lb-font);
21
+ font-weight: 800;
22
+ font-size: 0.8rem;
23
+ min-width: 48px;
24
+ height: 32px;
25
+ padding: 0 0.5rem;
26
+ border: 1px solid transparent;
27
+ backdrop-filter: blur(8px);
28
+ }
29
+ .badge.rank-1 {
30
+ background: linear-gradient(
31
+ 135deg,
32
+ rgba(255, 213, 74, 0.35),
33
+ rgba(255, 170, 0, 0.2)
34
+ );
35
+ color: var(--lb-gold);
36
+ border-color: var(--lb-gold);
37
+ box-shadow: var(--lb-glow-gold);
38
+ text-shadow: 0 0 12px rgba(255, 213, 74, 0.6);
39
+ }
40
+ .badge.rank-2 {
41
+ background: linear-gradient(
42
+ 135deg,
43
+ rgba(192, 212, 232, 0.25),
44
+ rgba(160, 180, 210, 0.15)
45
+ );
46
+ color: var(--lb-silver);
47
+ border-color: var(--lb-silver);
48
+ box-shadow: 0 0 16px rgba(192, 212, 232, 0.35);
49
+ }
50
+ .badge.rank-3 {
51
+ background: linear-gradient(
52
+ 135deg,
53
+ rgba(205, 127, 50, 0.3),
54
+ rgba(180, 100, 40, 0.15)
55
+ );
56
+ color: var(--lb-bronze);
57
+ border-color: var(--lb-bronze);
58
+ box-shadow: 0 0 16px rgba(205, 127, 50, 0.35);
59
+ }
60
+ .badge.rank-default {
61
+ background: rgba(12, 14, 28, 0.7);
62
+ color: var(--lb-muted);
63
+ border-color: var(--lb-border);
64
+ }
65
+ .icon {
66
+ display: flex;
67
+ align-items: center;
68
+ }
69
+ `,
70
+ ];
71
+
72
+ @property({ type: Number }) rank = 1;
73
+
74
+ render() {
75
+ const rankClass = this.rank <= 3 ? `rank-${this.rank}` : 'rank-default';
76
+ const showIcon = this.rank <= 3;
77
+
78
+ return html`
79
+ <div class="badge ${rankClass}">
80
+ ${showIcon
81
+ ? html`<span class="icon">${unsafeHTML(getIcon('trophy', 12))}</span>`
82
+ : ''}
83
+ <span>${this.rank}</span>
84
+ </div>
85
+ `;
86
+ }
87
+ }
88
+
89
+ @customElement('sf-lb-xp-progress')
90
+ class SfLbXpProgress extends LitElement {
91
+ static styles = [
92
+ lbTokens,
93
+ css`
94
+ .container {
95
+ width: 100%;
96
+ }
97
+ .label-row {
98
+ display: flex;
99
+ align-items: center;
100
+ justify-content: space-between;
101
+ margin-bottom: 0.25rem;
102
+ }
103
+ .label {
104
+ font-size: 0.7rem;
105
+ color: var(--lb-muted);
106
+ letter-spacing: 0.04em;
107
+ }
108
+ .track {
109
+ width: 100%;
110
+ background: rgba(0, 0, 0, 0.45);
111
+ border-radius: 9999px;
112
+ overflow: hidden;
113
+ border: 1px solid var(--lb-border);
114
+ box-shadow: inset 0 0 8px rgba(0, 245, 255, 0.08);
115
+ }
116
+ .track.sm {
117
+ height: 6px;
118
+ }
119
+ .track.md {
120
+ height: 8px;
121
+ }
122
+ .track.lg {
123
+ height: 12px;
124
+ }
125
+ .fill {
126
+ height: 100%;
127
+ border-radius: 9999px;
128
+ background: linear-gradient(
129
+ 90deg,
130
+ var(--lb-cyan),
131
+ var(--lb-violet),
132
+ var(--lb-magenta)
133
+ );
134
+ box-shadow: 0 0 12px rgba(0, 245, 255, 0.5);
135
+ transition: width 0.5s ease-out;
136
+ }
137
+ `,
138
+ ];
139
+
140
+ @property({ type: Number }) xp = 0;
141
+ @property({ type: Number }) xpToNextLevel = 100;
142
+ @property({ type: Number }) level = 1;
143
+ @property({ type: Boolean }) showLabel = true;
144
+ @property({ type: String }) size: 'sm' | 'md' | 'lg' = 'md';
145
+
146
+ render() {
147
+ const progress = (this.xp / this.xpToNextLevel) * 100;
148
+
149
+ return html`
150
+ <div class="container">
151
+ ${this.showLabel
152
+ ? html`
153
+ <div class="label-row">
154
+ <span class="label">Level ${this.level}</span>
155
+ <span class="label"
156
+ >${this.xp.toLocaleString()} /
157
+ ${this.xpToNextLevel.toLocaleString()} XP</span
158
+ >
159
+ </div>
160
+ `
161
+ : ''}
162
+ <div class="track ${this.size}">
163
+ <div class="fill" style="width: ${progress}%"></div>
164
+ </div>
165
+ </div>
166
+ `;
167
+ }
168
+ }
169
+
170
+ @customElement('sf-lb-trend')
171
+ class SfLbTrend extends LitElement {
172
+ static styles = [
173
+ lbTokens,
174
+ css`
175
+ .container {
176
+ display: flex;
177
+ align-items: center;
178
+ gap: 2px;
179
+ }
180
+ .up {
181
+ color: var(--lb-success);
182
+ filter: drop-shadow(0 0 6px rgba(34, 255, 154, 0.5));
183
+ }
184
+ .down {
185
+ color: var(--lb-danger);
186
+ filter: drop-shadow(0 0 6px rgba(255, 68, 102, 0.5));
187
+ }
188
+ .same {
189
+ color: var(--lb-muted);
190
+ }
191
+ .change {
192
+ font-size: 0.75rem;
193
+ font-weight: 600;
194
+ font-family: var(--lb-font);
195
+ }
196
+ .icon {
197
+ display: flex;
198
+ align-items: center;
199
+ }
200
+ `,
201
+ ];
202
+
203
+ @property({ type: String }) trend: 'up' | 'down' | 'same' = 'same';
204
+ @property({ type: Number }) rankChange?: number;
205
+
206
+ render() {
207
+ const iconName =
208
+ this.trend === 'up'
209
+ ? 'trendingUp'
210
+ : this.trend === 'down'
211
+ ? 'trendingDown'
212
+ : 'minus';
213
+ const prefix = this.trend === 'up' ? '+' : this.trend === 'down' ? '-' : '';
214
+
215
+ return html`
216
+ <div class="container ${this.trend}">
217
+ <span class="icon">${unsafeHTML(getIcon(iconName, 16))}</span>
218
+ ${this.rankChange
219
+ ? html`<span class="change">${prefix}${this.rankChange}</span>`
220
+ : ''}
221
+ </div>
222
+ `;
223
+ }
224
+ }
225
+
226
+ @customElement('sf-lb-streak-badge')
227
+ class SfLbStreakBadge extends LitElement {
228
+ static styles = [
229
+ lbTokens,
230
+ css`
231
+ .badge {
232
+ display: flex;
233
+ align-items: center;
234
+ gap: 0.25rem;
235
+ padding: 0.25rem 0.5rem;
236
+ border-radius: 9999px;
237
+ background: rgba(255, 43, 214, 0.12);
238
+ border: 1px solid rgba(255, 43, 214, 0.35);
239
+ color: var(--lb-magenta);
240
+ font-size: 0.7rem;
241
+ font-weight: 600;
242
+ box-shadow: 0 0 14px rgba(255, 43, 214, 0.25);
243
+ }
244
+ .icon {
245
+ display: flex;
246
+ align-items: center;
247
+ }
248
+ `,
249
+ ];
250
+
251
+ @property({ type: Number }) streak = 0;
252
+
253
+ render() {
254
+ if (this.streak < 3) return html``;
255
+
256
+ return html`
257
+ <div class="badge">
258
+ <span class="icon">${unsafeHTML(getIcon('flame', 12))}</span>
259
+ <span>${this.streak}</span>
260
+ </div>
261
+ `;
262
+ }
263
+ }
264
+
265
+ @customElement('sf-lb-level-badge')
266
+ class SfLbLevelBadge extends LitElement {
267
+ static styles = [
268
+ lbTokens,
269
+ css`
270
+ .badge {
271
+ display: flex;
272
+ align-items: center;
273
+ justify-content: center;
274
+ border-radius: 9999px;
275
+ border-width: 2px;
276
+ border-style: solid;
277
+ background: rgba(5, 5, 16, 0.85);
278
+ font-family: var(--lb-font);
279
+ font-weight: 800;
280
+ backdrop-filter: blur(6px);
281
+ }
282
+ .badge.sm {
283
+ width: 24px;
284
+ height: 24px;
285
+ font-size: 10px;
286
+ }
287
+ .badge.md {
288
+ width: 32px;
289
+ height: 32px;
290
+ font-size: 12px;
291
+ }
292
+ .badge.lg {
293
+ width: 40px;
294
+ height: 40px;
295
+ font-size: 14px;
296
+ }
297
+ .badge.level-90 {
298
+ border-color: var(--lb-gold);
299
+ color: var(--lb-gold);
300
+ box-shadow: var(--lb-glow-gold);
301
+ }
302
+ .badge.level-70 {
303
+ border-color: var(--lb-magenta);
304
+ color: var(--lb-magenta);
305
+ box-shadow: 0 0 20px rgba(255, 43, 214, 0.4);
306
+ }
307
+ .badge.level-50 {
308
+ border-color: var(--lb-cyan);
309
+ color: var(--lb-cyan);
310
+ box-shadow: var(--lb-glow-cyan);
311
+ }
312
+ .badge.level-default {
313
+ border-color: var(--lb-muted);
314
+ color: var(--lb-muted);
315
+ }
316
+ `,
317
+ ];
318
+
319
+ @property({ type: Number }) level = 1;
320
+ @property({ type: String }) size: 'sm' | 'md' | 'lg' = 'md';
321
+
322
+ render() {
323
+ const levelClass =
324
+ this.level >= 90
325
+ ? 'level-90'
326
+ : this.level >= 70
327
+ ? 'level-70'
328
+ : this.level >= 50
329
+ ? 'level-50'
330
+ : 'level-default';
331
+
332
+ return html`
333
+ <div class="badge ${this.size} ${levelClass}">${this.level}</div>
334
+ `;
335
+ }
336
+ }
337
+
338
+ @customElement('sf-lb-badge-display')
339
+ class SfLbBadgeDisplay extends LitElement {
340
+ static styles = [
341
+ lbTokens,
342
+ css`
343
+ .container {
344
+ display: flex;
345
+ align-items: center;
346
+ gap: 0.25rem;
347
+ }
348
+ .badge {
349
+ display: flex;
350
+ align-items: center;
351
+ justify-content: center;
352
+ width: 26px;
353
+ height: 26px;
354
+ border-radius: 6px;
355
+ border: 1px solid;
356
+ font-size: 0.875rem;
357
+ backdrop-filter: blur(6px);
358
+ }
359
+ .badge.common {
360
+ background: rgba(123, 132, 153, 0.15);
361
+ border-color: rgba(123, 132, 153, 0.4);
362
+ color: var(--lb-muted);
363
+ }
364
+ .badge.rare {
365
+ background: rgba(0, 245, 255, 0.12);
366
+ border-color: rgba(0, 245, 255, 0.4);
367
+ color: var(--lb-cyan);
368
+ box-shadow: 0 0 10px rgba(0, 245, 255, 0.2);
369
+ }
370
+ .badge.epic {
371
+ background: rgba(255, 43, 214, 0.12);
372
+ border-color: rgba(255, 43, 214, 0.4);
373
+ color: var(--lb-magenta);
374
+ box-shadow: 0 0 10px rgba(255, 43, 214, 0.2);
375
+ }
376
+ .badge.legendary {
377
+ background: rgba(255, 213, 74, 0.15);
378
+ border-color: rgba(255, 213, 74, 0.5);
379
+ color: var(--lb-gold);
380
+ box-shadow: 0 0 12px rgba(255, 213, 74, 0.3);
381
+ }
382
+ .remaining {
383
+ font-size: 0.7rem;
384
+ color: var(--lb-muted);
385
+ margin-left: 0.25rem;
386
+ }
387
+ .icon {
388
+ display: flex;
389
+ align-items: center;
390
+ }
391
+ `,
392
+ ];
393
+
394
+ @property({ type: Array }) badges: LbBadge[] = [];
395
+ @property({ type: Number }) max = 3;
396
+
397
+ private getBadgeIcon(icon: string): string {
398
+ const iconMap: Record<string, string> = {
399
+ trophy: getIcon('trophy', 14),
400
+ flame: getIcon('flame', 14),
401
+ star: getIcon('star', 14),
402
+ crown: getIcon('crown', 14),
403
+ zap: getIcon('zap', 14),
404
+ gem: getIcon('gem', 14),
405
+ };
406
+ return iconMap[icon] || '';
407
+ }
408
+
409
+ render() {
410
+ const displayBadges = this.badges.slice(0, this.max);
411
+ const remaining = this.badges.length - this.max;
412
+
413
+ return html`
414
+ <div class="container">
415
+ ${displayBadges.map(
416
+ (badge) => html`
417
+ <div class="badge ${badge.rarity}" title="${badge.name}">
418
+ <span class="icon"
419
+ >${unsafeHTML(this.getBadgeIcon(badge.icon))}</span
420
+ >
421
+ </div>
422
+ `,
423
+ )}
424
+ ${remaining > 0
425
+ ? html`<span class="remaining">+${remaining}</span>`
426
+ : ''}
427
+ </div>
428
+ `;
429
+ }
430
+ }
431
+
432
+ @customElement('sf-lb-stat-card')
433
+ class SfLbStatCard extends LitElement {
434
+ static styles = [
435
+ lbTokens,
436
+ css`
437
+ .card {
438
+ display: flex;
439
+ flex-direction: column;
440
+ align-items: center;
441
+ justify-content: center;
442
+ padding: 1rem;
443
+ border-radius: var(--lb-radius);
444
+ background: var(--lb-surface);
445
+ border: 1px solid var(--lb-border);
446
+ backdrop-filter: blur(12px);
447
+ transition: all 0.25s ease;
448
+ }
449
+ .card.highlight {
450
+ border-color: rgba(0, 245, 255, 0.45);
451
+ box-shadow: var(--lb-glow-cyan);
452
+ background: linear-gradient(
453
+ 135deg,
454
+ rgba(0, 245, 255, 0.08),
455
+ rgba(139, 92, 246, 0.06)
456
+ );
457
+ }
458
+ .icon {
459
+ display: flex;
460
+ align-items: center;
461
+ margin-bottom: 0.5rem;
462
+ color: var(--lb-cyan);
463
+ filter: drop-shadow(0 0 8px rgba(0, 245, 255, 0.4));
464
+ }
465
+ .value {
466
+ font-family: var(--lb-font);
467
+ font-size: 1.5rem;
468
+ font-weight: 900;
469
+ color: var(--lb-foreground);
470
+ text-shadow: 0 0 16px rgba(240, 244, 255, 0.2);
471
+ }
472
+ .label {
473
+ font-size: 0.7rem;
474
+ color: var(--lb-muted);
475
+ letter-spacing: 0.08em;
476
+ text-transform: uppercase;
477
+ margin-top: 0.15rem;
478
+ }
479
+ `,
480
+ ];
481
+
482
+ @property({ type: String }) label = '';
483
+ @property({ type: String }) value = '';
484
+ @property({ type: String }) icon = '';
485
+ @property({ type: Boolean }) highlight = false;
486
+
487
+ render() {
488
+ return html`
489
+ <div class="card ${this.highlight ? 'highlight' : ''}">
490
+ ${this.icon
491
+ ? html`<span class="icon"
492
+ >${unsafeHTML(getIcon(this.icon as Parameters<typeof getIcon>[0], 20))}</span
493
+ >`
494
+ : ''}
495
+ <span class="value">${this.value}</span>
496
+ <span class="label">${this.label}</span>
497
+ </div>
498
+ `;
499
+ }
500
+ }
501
+
502
+ declare global {
503
+ interface HTMLElementTagNameMap {
504
+ 'sf-lb-rank-badge': SfLbRankBadge;
505
+ 'sf-lb-xp-progress': SfLbXpProgress;
506
+ 'sf-lb-trend': SfLbTrend;
507
+ 'sf-lb-streak-badge': SfLbStreakBadge;
508
+ 'sf-lb-level-badge': SfLbLevelBadge;
509
+ 'sf-lb-badge-display': SfLbBadgeDisplay;
510
+ 'sf-lb-stat-card': SfLbStatCard;
511
+ }
512
+ }
@@ -0,0 +1,205 @@
1
+ import { LitElement, html } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
4
+ import {
5
+ SF_LEADERBOARD_TAG,
6
+ DEFAULT_LB_TITLE,
7
+ DEFAULT_LB_SUBTITLE,
8
+ } from '../constants.js';
9
+ import {
10
+ LB_TAB_CHANGE,
11
+ LB_LOAD_MORE,
12
+ LB_PLAYER_SELECT,
13
+ } from '../events.js';
14
+ import type { LbTabChangeDetail, LbPlayerSelectDetail } from '../events.js';
15
+ import { getIcon, type LbIconName } from '../icons.js';
16
+ import type { LbPlayer, LbStats, LbTabId } from '../types.js';
17
+ import { scss } from '../../../lib/lit/scss.js';
18
+ import { lbTokens } from '../tokens.js';
19
+ import componentStyles from '../sf-leaderboard.scss?inline';
20
+ import renderTemplate from '../sf-leaderboard.html?lit-html';
21
+ import './sf-leaderboard-ui.js';
22
+ import './sf-leaderboard-player.js';
23
+
24
+ @customElement(SF_LEADERBOARD_TAG)
25
+ export class SfLeaderboard extends LitElement {
26
+ static styles = [lbTokens, scss(componentStyles)];
27
+
28
+ @property({ type: Array }) players: LbPlayer[] = [];
29
+ @property({ type: Object }) currentUser?: LbPlayer;
30
+ @property({ type: Object }) stats!: LbStats;
31
+ @property({ type: String, attribute: 'lb-title' }) lbTitle = DEFAULT_LB_TITLE;
32
+ @property({ type: String, attribute: 'lb-subtitle' })
33
+ lbSubtitle = DEFAULT_LB_SUBTITLE;
34
+
35
+ @state() private activeTab: LbTabId = 'global';
36
+ @state() private showPodium = true;
37
+
38
+ private readonly tabs: { id: LbTabId; label: string; icon: LbIconName }[] = [
39
+ { id: 'global', label: 'Global', icon: 'trophy' },
40
+ { id: 'weekly', label: 'Weekly', icon: 'zap' },
41
+ { id: 'friends', label: 'Friends', icon: 'users' },
42
+ ];
43
+
44
+ private _onTab(tab: LbTabId) {
45
+ this.activeTab = tab;
46
+ this.dispatchEvent(
47
+ new CustomEvent<LbTabChangeDetail>(LB_TAB_CHANGE, {
48
+ detail: { tab },
49
+ bubbles: true,
50
+ composed: true,
51
+ }),
52
+ );
53
+ }
54
+
55
+ private _onLoadMore() {
56
+ this.dispatchEvent(
57
+ new CustomEvent(LB_LOAD_MORE, { bubbles: true, composed: true }),
58
+ );
59
+ }
60
+
61
+ private _onPlayerClick(player: LbPlayer) {
62
+ this.dispatchEvent(
63
+ new CustomEvent<LbPlayerSelectDetail>(LB_PLAYER_SELECT, {
64
+ detail: { player },
65
+ bubbles: true,
66
+ composed: true,
67
+ }),
68
+ );
69
+ }
70
+
71
+ renderContent() {
72
+ return html`
73
+ <div class="shell">
74
+ <div class="live-badge">
75
+ <span class="live-dot"></span>
76
+ Live Arena
77
+ </div>
78
+
79
+ <header class="header">
80
+ <div class="title-row">
81
+ <span class="crown-icon">${unsafeHTML(getIcon('crown', 32))}</span>
82
+ <h1 class="title">${this.lbTitle}</h1>
83
+ <span class="crown-icon">${unsafeHTML(getIcon('crown', 32))}</span>
84
+ </div>
85
+ <p class="subtitle">${this.lbSubtitle}</p>
86
+ </header>
87
+
88
+ <div class="stats-grid">
89
+ <sf-lb-stat-card
90
+ label="Total Players"
91
+ value=${this.stats?.totalPlayers?.toLocaleString() ?? '—'}
92
+ icon="users"
93
+ ></sf-lb-stat-card>
94
+ <sf-lb-stat-card
95
+ label="Your Rank"
96
+ value=${this.stats ? `#${this.stats.yourRank}` : '—'}
97
+ icon="target"
98
+ ?highlight=${true}
99
+ ></sf-lb-stat-card>
100
+ <sf-lb-stat-card
101
+ label="Top Score"
102
+ value=${this.stats?.topScore?.toLocaleString() ?? '—'}
103
+ icon="trophy"
104
+ ></sf-lb-stat-card>
105
+ </div>
106
+
107
+ ${this.currentUser
108
+ ? html`
109
+ <section class="hero-card">
110
+ <div class="hero-header">
111
+ <div class="hero-info">
112
+ <div class="hero-avatar-wrap">
113
+ <img
114
+ class="hero-avatar"
115
+ src=${this.currentUser.avatar}
116
+ alt=${this.currentUser.username}
117
+ crossorigin="anonymous"
118
+ />
119
+ <span class="hero-level">${this.currentUser.level}</span>
120
+ </div>
121
+ <div>
122
+ <div class="hero-name">
123
+ ${this.currentUser.username}
124
+ <span class="you-chip">YOU</span>
125
+ </div>
126
+ <div class="hero-meta">
127
+ Rank #${this.currentUser.rank} ·
128
+ ${this.currentUser.wins} wins
129
+ </div>
130
+ </div>
131
+ </div>
132
+ <div class="hero-score-block">
133
+ <div class="hero-score">
134
+ ${this.currentUser.score.toLocaleString()}
135
+ </div>
136
+ <div class="hero-streak">
137
+ <span>${unsafeHTML(getIcon('flame', 14))}</span>
138
+ ${this.currentUser.streak} day streak
139
+ </div>
140
+ </div>
141
+ </div>
142
+ <sf-lb-xp-progress
143
+ .xp=${this.currentUser.xp}
144
+ .xpToNextLevel=${this.currentUser.xpToNextLevel}
145
+ .level=${this.currentUser.level}
146
+ size="lg"
147
+ ></sf-lb-xp-progress>
148
+ </section>
149
+ `
150
+ : ''}
151
+
152
+ <nav class="tabs">
153
+ ${this.tabs.map(
154
+ (tab) => html`
155
+ <button
156
+ class="tab ${this.activeTab === tab.id ? 'active' : ''}"
157
+ @click=${() => this._onTab(tab.id)}
158
+ >
159
+ <span>${unsafeHTML(getIcon(tab.icon, 16))}</span>
160
+ <span class="tab-label">${tab.label}</span>
161
+ </button>
162
+ `,
163
+ )}
164
+ </nav>
165
+
166
+ ${this.showPodium && this.players.length >= 3
167
+ ? html`<sf-lb-top-three .players=${this.players}></sf-lb-top-three>`
168
+ : ''}
169
+
170
+ <button
171
+ class="toggle-btn"
172
+ @click=${() => (this.showPodium = !this.showPodium)}
173
+ >
174
+ ${this.showPodium ? '▾ Hide champions podium' : '▸ Show champions podium'}
175
+ </button>
176
+
177
+ <div class="players-list">
178
+ ${this.players.map(
179
+ (player) => html`
180
+ <sf-lb-player-row
181
+ .player=${player}
182
+ .isCurrentUser=${this.currentUser?.id === player.id}
183
+ @click=${() => this._onPlayerClick(player)}
184
+ ></sf-lb-player-row>
185
+ `,
186
+ )}
187
+ </div>
188
+
189
+ <button class="load-more" @click=${this._onLoadMore}>
190
+ Load more contenders
191
+ </button>
192
+ </div>
193
+ `;
194
+ }
195
+
196
+ render() {
197
+ return renderTemplate(this);
198
+ }
199
+ }
200
+
201
+ declare global {
202
+ interface HTMLElementTagNameMap {
203
+ 'sf-leaderboard': SfLeaderboard;
204
+ }
205
+ }