@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.
- package/.eleventy.cjs +14 -0
- package/LICENSE +28 -0
- package/README.md +118 -0
- package/custom-elements.json +1539 -0
- package/docs/_README/index.html +4 -0
- package/docs/api/index.html +2100 -0
- package/docs/components.bundle.js +1669 -0
- package/docs/components.bundle.js.map +1 -0
- package/docs/docs.css +162 -0
- package/docs/examples/index.html +56 -0
- package/docs/index.html +53 -0
- package/docs/install/index.html +45 -0
- package/docs/prism-okaidia.css +123 -0
- package/docs-src/.nojekyll +0 -0
- package/docs-src/_README.md +7 -0
- package/docs-src/_data/api.11tydata.js +8 -0
- package/docs-src/_includes/example.11ty.js +35 -0
- package/docs-src/_includes/footer.11ty.js +6 -0
- package/docs-src/_includes/header.11ty.js +7 -0
- package/docs-src/_includes/nav.11ty.js +11 -0
- package/docs-src/_includes/page.11ty.js +32 -0
- package/docs-src/_includes/relative-path.cjs +9 -0
- package/docs-src/api.11ty.js +85 -0
- package/docs-src/bundle.ts +9 -0
- package/docs-src/docs.css +162 -0
- package/docs-src/examples/index.md +15 -0
- package/docs-src/index.md +39 -0
- package/docs-src/install.md +28 -0
- package/docs-src/package.json +3 -0
- package/index.html +19 -0
- package/karma.conf.cjs +24 -0
- package/main.css +210 -0
- package/main.ts +124 -0
- package/package.json +86 -0
- package/previews/casino.ts +12 -0
- package/previews/catalog.ts +94 -0
- package/previews/leaderboard-v1.ts +12 -0
- package/previews/leaderboard-v2.ts +17 -0
- package/previews/sample-data.ts +101 -0
- package/previews/sf-leaderboard.ts +100 -0
- package/previews/sf-live-feed.ts +15 -0
- package/previews/streaks.ts +40 -0
- package/previews/types.ts +18 -0
- package/src/components/README.md +16 -0
- package/src/components/casino-leaderboard/casino-leaderboard.html +80 -0
- package/src/components/casino-leaderboard/casino-leaderboard.scss +585 -0
- package/src/components/casino-leaderboard/casino-leaderboard.ts +136 -0
- package/src/components/casino-leaderboard/data.ts +111 -0
- package/src/components/casino-leaderboard/index.ts +5 -0
- package/src/components/casino-leaderboard/todo.txt +2 -0
- package/src/components/casino-leaderboard/types.ts +19 -0
- package/src/components/leaderboard/components/leaderboard.ts +373 -0
- package/src/components/leaderboard/components/player-card.ts +342 -0
- package/src/components/leaderboard/components/ui.ts +452 -0
- package/src/components/leaderboard/data.ts +152 -0
- package/src/components/leaderboard/index.ts +2 -0
- package/src/components/leaderboard/main.ts +42 -0
- package/src/components/leaderboard/styles.ts +67 -0
- package/src/components/leaderboard/types.ts +28 -0
- package/src/components/leaderboard-v2/components/sf-leaderboard-player.ts +451 -0
- package/src/components/leaderboard-v2/components/sf-leaderboard-ui.ts +512 -0
- package/src/components/leaderboard-v2/components/sf-leaderboard.ts +205 -0
- package/src/components/leaderboard-v2/constants.ts +16 -0
- package/src/components/leaderboard-v2/demo/sample-data.ts +152 -0
- package/src/components/leaderboard-v2/events.ts +13 -0
- package/src/components/leaderboard-v2/icons.ts +22 -0
- package/src/components/leaderboard-v2/index.ts +23 -0
- package/src/components/leaderboard-v2/sf-leaderboard.html +1 -0
- package/src/components/leaderboard-v2/sf-leaderboard.scss +382 -0
- package/src/components/leaderboard-v2/tokens.ts +35 -0
- package/src/components/leaderboard-v2/types.ts +30 -0
- package/src/components/sf-leaderboard/index.ts +77 -0
- package/src/components/sf-leaderboard/sections/footer-section/footer-section.host.ts +3 -0
- package/src/components/sf-leaderboard/sections/footer-section/footer-section.html +3 -0
- package/src/components/sf-leaderboard/sections/footer-section/footer-section.scss +18 -0
- package/src/components/sf-leaderboard/sections/footer-section/footer-section.ts +22 -0
- package/src/components/sf-leaderboard/sections/header-section/header-section.host.ts +14 -0
- package/src/components/sf-leaderboard/sections/header-section/header-section.html +27 -0
- package/src/components/sf-leaderboard/sections/header-section/header-section.scss +189 -0
- package/src/components/sf-leaderboard/sections/header-section/header-section.ts +70 -0
- package/src/components/sf-leaderboard/sections/ranking-section/ranking-section.host.ts +22 -0
- package/src/components/sf-leaderboard/sections/ranking-section/ranking-section.html +38 -0
- package/src/components/sf-leaderboard/sections/ranking-section/ranking-section.scss +99 -0
- package/src/components/sf-leaderboard/sections/ranking-section/ranking-section.ts +121 -0
- package/src/components/sf-leaderboard/sections/stats-section/stats-section.host.ts +8 -0
- package/src/components/sf-leaderboard/sections/stats-section/stats-section.html +6 -0
- package/src/components/sf-leaderboard/sections/stats-section/stats-section.scss +44 -0
- package/src/components/sf-leaderboard/sections/stats-section/stats-section.ts +41 -0
- package/src/components/sf-leaderboard/sections/table-section/table-section.host.ts +17 -0
- package/src/components/sf-leaderboard/sections/table-section/table-section.html +19 -0
- package/src/components/sf-leaderboard/sections/table-section/table-section.scss +37 -0
- package/src/components/sf-leaderboard/sections/table-section/table-section.ts +108 -0
- package/src/components/sf-leaderboard/services/index.ts +22 -0
- package/src/components/sf-leaderboard/services/sf-leaderboard-data.service.ts +54 -0
- package/src/components/sf-leaderboard/services/sf-leaderboard.state.ts +160 -0
- package/src/components/sf-leaderboard/shared/components/activity-feed/activity-feed.host.ts +7 -0
- package/src/components/sf-leaderboard/shared/components/activity-feed/activity-feed.html +10 -0
- package/src/components/sf-leaderboard/shared/components/activity-feed/activity-feed.scss +180 -0
- package/src/components/sf-leaderboard/shared/components/activity-feed/activity-feed.ts +88 -0
- package/src/components/sf-leaderboard/shared/components/filters/filters.host.ts +12 -0
- package/src/components/sf-leaderboard/shared/components/filters/filters.html +22 -0
- package/src/components/sf-leaderboard/shared/components/filters/filters.scss +122 -0
- package/src/components/sf-leaderboard/shared/components/filters/filters.ts +75 -0
- package/src/components/sf-leaderboard/shared/components/player-avatar/player-avatar.host.ts +9 -0
- package/src/components/sf-leaderboard/shared/components/player-avatar/player-avatar.html +5 -0
- package/src/components/sf-leaderboard/shared/components/player-avatar/player-avatar.scss +81 -0
- package/src/components/sf-leaderboard/shared/components/player-avatar/player-avatar.ts +34 -0
- package/src/components/sf-leaderboard/shared/components/podium/map-players.ts +24 -0
- package/src/components/sf-leaderboard/shared/components/podium/podium.host.ts +10 -0
- package/src/components/sf-leaderboard/shared/components/podium/podium.html +53 -0
- package/src/components/sf-leaderboard/shared/components/podium/podium.scss +580 -0
- package/src/components/sf-leaderboard/shared/components/podium/podium.ts +49 -0
- package/src/components/sf-leaderboard/shared/components/podium/podium.types.ts +9 -0
- package/src/components/sf-leaderboard/shared/components/rank-badge/rank-badge.host.ts +11 -0
- package/src/components/sf-leaderboard/shared/components/rank-badge/rank-badge.html +9 -0
- package/src/components/sf-leaderboard/shared/components/rank-badge/rank-badge.scss +98 -0
- package/src/components/sf-leaderboard/shared/components/rank-badge/rank-badge.ts +63 -0
- package/src/components/sf-leaderboard/shared/components/stat-card/stat-card.host.ts +9 -0
- package/src/components/sf-leaderboard/shared/components/stat-card/stat-card.html +15 -0
- package/src/components/sf-leaderboard/shared/components/stat-card/stat-card.scss +210 -0
- package/src/components/sf-leaderboard/shared/components/stat-card/stat-card.ts +36 -0
- package/src/components/sf-leaderboard/shared/components/table/table.host.ts +5 -0
- package/src/components/sf-leaderboard/shared/components/table/table.html +11 -0
- package/src/components/sf-leaderboard/shared/components/table/table.scss +212 -0
- package/src/components/sf-leaderboard/shared/components/table/table.ts +111 -0
- package/src/components/sf-leaderboard/shared/constants/defaults.ts +7 -0
- package/src/components/sf-leaderboard/shared/constants/filters.ts +16 -0
- package/src/components/sf-leaderboard/shared/constants/index.ts +5 -0
- package/src/components/sf-leaderboard/shared/constants/player-stats.ts +3 -0
- package/src/components/sf-leaderboard/shared/constants/stats-overview.ts +38 -0
- package/src/components/sf-leaderboard/shared/constants/tags.ts +16 -0
- package/src/components/sf-leaderboard/shared/styles/_section.scss +35 -0
- package/src/components/sf-leaderboard/shared/types/data.ts +29 -0
- package/src/components/sf-leaderboard/shared/types/events.ts +30 -0
- package/src/components/sf-leaderboard/shared/types/player-stats.ts +3 -0
- package/src/components/sf-leaderboard/shared/types/sections.ts +100 -0
- package/src/components/sf-leaderboard/shared/utils/utils.ts +17 -0
- package/src/components/sf-leaderboard/theme/THEMING.md +54 -0
- package/src/components/sf-leaderboard/theme/context.ts +16 -0
- package/src/components/sf-leaderboard/theme/default-theme.ts +4 -0
- package/src/components/sf-leaderboard/theme/hex-to-rgb.ts +25 -0
- package/src/components/sf-leaderboard/theme/index.ts +18 -0
- package/src/components/sf-leaderboard/theme/inject-theme.ts +39 -0
- package/src/components/sf-leaderboard/theme/load-theme.ts +26 -0
- package/src/components/sf-leaderboard/theme/merge-theme.ts +59 -0
- package/src/components/sf-leaderboard/theme/scss/_colors.scss +101 -0
- package/src/components/sf-leaderboard/theme/scss/shared.scss +123 -0
- package/src/components/sf-leaderboard/theme/styles.ts +6 -0
- package/src/components/sf-leaderboard/theme/theme-to-css-vars.ts +99 -0
- package/src/components/sf-leaderboard/theme/themes/fallback.json +62 -0
- package/src/components/sf-leaderboard/theme/themes/red.json +62 -0
- package/src/components/sf-leaderboard/theme/types.ts +71 -0
- package/src/components/sf-live-feed/components/avatar/avatar.host.ts +5 -0
- package/src/components/sf-live-feed/components/avatar/avatar.html +3 -0
- package/src/components/sf-live-feed/components/avatar/avatar.scss +24 -0
- package/src/components/sf-live-feed/components/avatar/avatar.ts +27 -0
- package/src/components/sf-live-feed/components/sf-live-feed/sf-live-feed.host.ts +8 -0
- package/src/components/sf-live-feed/components/sf-live-feed/sf-live-feed.html +10 -0
- package/src/components/sf-live-feed/components/sf-live-feed/sf-live-feed.scss +177 -0
- package/src/components/sf-live-feed/components/sf-live-feed/sf-live-feed.ts +65 -0
- package/src/components/sf-live-feed/constants.ts +4 -0
- package/src/components/sf-live-feed/demo/sample-data.ts +34 -0
- package/src/components/sf-live-feed/index.ts +19 -0
- package/src/components/sf-live-feed/styles/theme.scss +19 -0
- package/src/components/sf-live-feed/styles/theme.ts +5 -0
- package/src/components/sf-live-feed/types.ts +19 -0
- package/src/components/sf-live-feed/utils.ts +17 -0
- package/src/components/streaks/constants.ts +17 -0
- package/src/components/streaks/demo/sample-steps.ts +10 -0
- package/src/components/streaks/events.ts +8 -0
- package/src/components/streaks/index.ts +16 -0
- package/src/components/streaks/sf-streaks.html +26 -0
- package/src/components/streaks/sf-streaks.scss +351 -0
- package/src/components/streaks/sf-streaks.ts +235 -0
- package/src/components/streaks/types.ts +7 -0
- package/src/lib/lit/component.ts +10 -0
- package/src/lib/lit/safe-custom-element.ts +12 -0
- package/src/lib/lit/scss.ts +6 -0
- package/src/vite-env.d.ts +18 -0
- package/styles/global.css +125 -0
- package/todo.txt +54 -0
- package/tsconfig.json +31 -0
- package/vite.config.ts +56 -0
- package/vite.docs.config.ts +33 -0
- package/vite.lit-html-plugin.ts +43 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import { property } from 'lit/decorators.js';
|
|
3
|
+
import { Component } from '../../../../../../lib/lit/component.js';
|
|
4
|
+
import { scss } from '../../../../../../lib/lit/scss.js';
|
|
5
|
+
import {
|
|
6
|
+
SF_LEADERBOARD_PLAYER_AVATAR,
|
|
7
|
+
SF_LEADERBOARD_RANK_BADGE,
|
|
8
|
+
SF_LEADERBOARD_TABLE,
|
|
9
|
+
} from '../../constants/tags.js';
|
|
10
|
+
import { SF_LEADERBOARD_PLAYER_SELECT } from '../../types/events.js';
|
|
11
|
+
import { sfLeaderboardTheme } from '../../../theme/styles.js';
|
|
12
|
+
import type { Player } from '../../types/data.js';
|
|
13
|
+
import { formatCurrency } from '../../utils/utils.js';
|
|
14
|
+
import '../rank-badge/rank-badge.js';
|
|
15
|
+
import '../player-avatar/player-avatar.js';
|
|
16
|
+
import type { TableHost } from './table.host.js';
|
|
17
|
+
import renderTemplate from './table.html?lit-html';
|
|
18
|
+
import styles from './table.scss?inline';
|
|
19
|
+
|
|
20
|
+
@Component({ selector: SF_LEADERBOARD_TABLE })
|
|
21
|
+
export class SfLeaderboardTable extends LitElement implements TableHost {
|
|
22
|
+
static styles = [sfLeaderboardTheme, scss(styles)];
|
|
23
|
+
|
|
24
|
+
@property({ type: Array }) players: Player[] = [];
|
|
25
|
+
|
|
26
|
+
private handlePlayerClick(player: Player) {
|
|
27
|
+
this.dispatchEvent(
|
|
28
|
+
new CustomEvent(SF_LEADERBOARD_PLAYER_SELECT, {
|
|
29
|
+
detail: player,
|
|
30
|
+
bubbles: true,
|
|
31
|
+
composed: true,
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private createStreakCell(player: Player): HTMLElement {
|
|
37
|
+
const cell = document.createElement('div');
|
|
38
|
+
cell.className = 'col-streak stat-cell streak hide-mobile';
|
|
39
|
+
if (player.streak > 0) {
|
|
40
|
+
const icon = document.createElement('span');
|
|
41
|
+
icon.className = 'icon';
|
|
42
|
+
icon.textContent = '🔥';
|
|
43
|
+
cell.append(icon, document.createTextNode(String(player.streak)));
|
|
44
|
+
} else {
|
|
45
|
+
const dash = document.createElement('span');
|
|
46
|
+
dash.textContent = '-';
|
|
47
|
+
cell.append(dash);
|
|
48
|
+
}
|
|
49
|
+
return cell;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private createRow(player: Player, index: number): HTMLElement {
|
|
53
|
+
const isTop3 = player.rank <= 3;
|
|
54
|
+
const highWinRate = player.winRate >= 60;
|
|
55
|
+
|
|
56
|
+
const row = document.createElement('div');
|
|
57
|
+
row.className = `table-row ${isTop3 ? 'top-3' : ''}`;
|
|
58
|
+
row.style.animationDelay = `${index * 50}ms`;
|
|
59
|
+
row.addEventListener('click', () => this.handlePlayerClick(player));
|
|
60
|
+
|
|
61
|
+
const rankCol = document.createElement('div');
|
|
62
|
+
rankCol.className = 'col-rank';
|
|
63
|
+
const badge = document.createElement(SF_LEADERBOARD_RANK_BADGE);
|
|
64
|
+
badge.rank = player.rank;
|
|
65
|
+
badge.previousRank = player.previousRank || player.rank;
|
|
66
|
+
badge.size = 'sm';
|
|
67
|
+
rankCol.append(badge);
|
|
68
|
+
|
|
69
|
+
const playerCol = document.createElement('div');
|
|
70
|
+
playerCol.className = 'col-player player-cell';
|
|
71
|
+
const avatar = document.createElement(SF_LEADERBOARD_PLAYER_AVATAR);
|
|
72
|
+
avatar.username = player.username;
|
|
73
|
+
avatar.avatar = player.avatar || '';
|
|
74
|
+
avatar.size = 'sm';
|
|
75
|
+
avatar.isOnline = player.isOnline;
|
|
76
|
+
avatar.highlight = isTop3;
|
|
77
|
+
const details = document.createElement('div');
|
|
78
|
+
details.className = 'player-details';
|
|
79
|
+
const name = document.createElement('div');
|
|
80
|
+
name.className = `player-name ${isTop3 ? 'highlight' : ''}`;
|
|
81
|
+
name.textContent = player.username;
|
|
82
|
+
const level = document.createElement('span');
|
|
83
|
+
level.className = 'player-level';
|
|
84
|
+
level.textContent = `LVL ${player.level}`;
|
|
85
|
+
details.append(name, level);
|
|
86
|
+
playerCol.append(avatar, details);
|
|
87
|
+
|
|
88
|
+
const gamesCol = document.createElement('div');
|
|
89
|
+
gamesCol.className = 'col-games stat-cell hide-mobile';
|
|
90
|
+
gamesCol.innerHTML = `<span class="icon">🏆</span>${player.gamesPlayed.toLocaleString()}`;
|
|
91
|
+
|
|
92
|
+
const winRateCol = document.createElement('div');
|
|
93
|
+
winRateCol.className = `col-win-rate stat-cell win-rate ${highWinRate ? 'high' : ''} hide-mobile`;
|
|
94
|
+
winRateCol.innerHTML = `<span class="icon">🎯</span>${player.winRate}%`;
|
|
95
|
+
|
|
96
|
+
const winningsCol = document.createElement('div');
|
|
97
|
+
winningsCol.className = 'col-winnings winnings-cell';
|
|
98
|
+
winningsCol.innerHTML = `<span class="winnings ${isTop3 ? 'highlight' : ''}">${formatCurrency(player.totalWinnings)}</span><span class="chevron">→</span>`;
|
|
99
|
+
|
|
100
|
+
row.append(rankCol, playerCol, gamesCol, winRateCol, this.createStreakCell(player), winningsCol);
|
|
101
|
+
return row;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
renderRows() {
|
|
105
|
+
return this.players.map((player, index) => this.createRow(player, index));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
render() {
|
|
109
|
+
return renderTemplate(this);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Category, TimeFrame } from '../types/data.js';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_LB_TITLE = 'Leaderboard';
|
|
4
|
+
export const DEFAULT_LB_SUBTITLE = 'Real-time rankings';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_TIMEFRAME: TimeFrame = 'week';
|
|
7
|
+
export const DEFAULT_CATEGORY: Category = 'all';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Category, TimeFrame } from '../types/data.js';
|
|
2
|
+
|
|
3
|
+
export const TIMEFRAME_OPTIONS: { label: string; value: TimeFrame }[] = [
|
|
4
|
+
{ label: 'Today', value: 'today' },
|
|
5
|
+
{ label: 'This Week', value: 'week' },
|
|
6
|
+
{ label: 'This Month', value: 'month' },
|
|
7
|
+
{ label: 'All Time', value: 'all' },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export const CATEGORY_OPTIONS: { label: string; value: Category }[] = [
|
|
11
|
+
{ label: 'All Games', value: 'all' },
|
|
12
|
+
{ label: 'Poker', value: 'poker' },
|
|
13
|
+
{ label: 'Blackjack', value: 'blackjack' },
|
|
14
|
+
{ label: 'Roulette', value: 'roulette' },
|
|
15
|
+
{ label: 'Slots', value: 'slots' },
|
|
16
|
+
];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface SfLeaderboardOverviewStat {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
icon: string;
|
|
5
|
+
trendValue: number;
|
|
6
|
+
trendPositive: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_OVERVIEW_STATS: SfLeaderboardOverviewStat[] = [
|
|
10
|
+
{
|
|
11
|
+
label: 'Total Players',
|
|
12
|
+
value: '12,847',
|
|
13
|
+
icon: '👥',
|
|
14
|
+
trendValue: 12.5,
|
|
15
|
+
trendPositive: true,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
label: 'Total Wagered',
|
|
19
|
+
value: '$84.2M',
|
|
20
|
+
icon: '💰',
|
|
21
|
+
trendValue: 8.3,
|
|
22
|
+
trendPositive: true,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
label: 'Active Games',
|
|
26
|
+
value: '1,247',
|
|
27
|
+
icon: '🎮',
|
|
28
|
+
trendValue: 3.2,
|
|
29
|
+
trendPositive: true,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: 'Biggest Win',
|
|
33
|
+
value: '$125K',
|
|
34
|
+
icon: '⚡',
|
|
35
|
+
trendValue: 45.8,
|
|
36
|
+
trendPositive: true,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Custom element tag names for the sf-leaderboard package. */
|
|
2
|
+
export const SF_LEADERBOARD_TAG = 'sf-leaderboard';
|
|
3
|
+
export const SF_LEADERBOARD_STAT_CARD = 'sf-leaderboard-stat-card';
|
|
4
|
+
export const SF_LEADERBOARD_FILTERS = 'sf-leaderboard-filters';
|
|
5
|
+
export const SF_LEADERBOARD_TABLE = 'sf-leaderboard-table';
|
|
6
|
+
export const SF_LEADERBOARD_PODIUM = 'sf-leaderboard-podium';
|
|
7
|
+
export const SF_LEADERBOARD_ACTIVITY_FEED = 'sf-leaderboard-activity-feed';
|
|
8
|
+
export const SF_LEADERBOARD_RANK_BADGE = 'sf-leaderboard-rank-badge';
|
|
9
|
+
export const SF_LEADERBOARD_PLAYER_AVATAR = 'sf-leaderboard-player-avatar';
|
|
10
|
+
export const SF_LEADERBOARD_PLAYER_STATS_CARD =
|
|
11
|
+
'sf-leaderboard-player-stats-card';
|
|
12
|
+
export const SF_LEADERBOARD_HEADER_SECTION = 'sf-leaderboard-header-section';
|
|
13
|
+
export const SF_LEADERBOARD_STATS_SECTION = 'sf-leaderboard-stats-section';
|
|
14
|
+
export const SF_LEADERBOARD_RANKING_SECTION = 'sf-leaderboard-ranking-section';
|
|
15
|
+
export const SF_LEADERBOARD_TABLE_SECTION = 'sf-leaderboard-table-section';
|
|
16
|
+
export const SF_LEADERBOARD_FOOTER_SECTION = 'sf-leaderboard-footer-section';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.section {
|
|
2
|
+
margin-bottom: clamp(1.5rem, 5vw, 3rem);
|
|
3
|
+
min-width: 0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.section-header {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: 0.5rem;
|
|
10
|
+
margin-bottom: 1rem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.section-title {
|
|
14
|
+
font-size: 1.125rem;
|
|
15
|
+
font-weight: 700;
|
|
16
|
+
margin: 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.elite-badge {
|
|
20
|
+
font-size: 0.65rem;
|
|
21
|
+
padding: 0.2rem 0.5rem;
|
|
22
|
+
border-radius: var(--radius-sm);
|
|
23
|
+
background: var(--medal-gold-bg);
|
|
24
|
+
border: 1px solid var(--medal-gold-border);
|
|
25
|
+
color: var(--gold);
|
|
26
|
+
font-weight: 600;
|
|
27
|
+
letter-spacing: 0.04em;
|
|
28
|
+
text-transform: uppercase;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@media (max-width: 639px) {
|
|
32
|
+
.section-title {
|
|
33
|
+
font-size: 1rem;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface Player {
|
|
2
|
+
id: string;
|
|
3
|
+
username: string;
|
|
4
|
+
avatar?: string;
|
|
5
|
+
rank: number;
|
|
6
|
+
previousRank?: number;
|
|
7
|
+
totalWinnings: number;
|
|
8
|
+
winRate: number;
|
|
9
|
+
gamesPlayed: number;
|
|
10
|
+
streak: number;
|
|
11
|
+
level: number;
|
|
12
|
+
badges: string[];
|
|
13
|
+
isOnline?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Activity {
|
|
17
|
+
id: string;
|
|
18
|
+
type: 'win' | 'jackpot' | 'streak' | 'achievement';
|
|
19
|
+
player: {
|
|
20
|
+
username: string;
|
|
21
|
+
avatar?: string;
|
|
22
|
+
};
|
|
23
|
+
message: string;
|
|
24
|
+
amount?: number;
|
|
25
|
+
timestamp: Date;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type TimeFrame = 'today' | 'week' | 'month' | 'all';
|
|
29
|
+
export type Category = 'all' | 'poker' | 'blackjack' | 'roulette' | 'slots';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Category, Player, TimeFrame } from './data.js';
|
|
2
|
+
|
|
3
|
+
export const SF_LEADERBOARD_TIMEFRAME_CHANGE = 'timeframe-change';
|
|
4
|
+
export const SF_LEADERBOARD_CATEGORY_CHANGE = 'category-change';
|
|
5
|
+
export const SF_LEADERBOARD_SEARCH_CHANGE = 'search-change';
|
|
6
|
+
export const SF_LEADERBOARD_PLAYER_SELECT = 'player-select';
|
|
7
|
+
export const SF_LEADERBOARD_REFRESH = 'refresh';
|
|
8
|
+
export const SF_LEADERBOARD_VIEW_STATS = 'view-stats';
|
|
9
|
+
|
|
10
|
+
export interface SfLeaderboardRefreshDetail {
|
|
11
|
+
/** Optional hook for clients that batch refresh with timeframe/category. */
|
|
12
|
+
timeframe: TimeFrame;
|
|
13
|
+
category: Category;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SfLeaderboardTimeframeChangeDetail {
|
|
17
|
+
timeframe: TimeFrame;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SfLeaderboardCategoryChangeDetail {
|
|
21
|
+
category: Category;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SfLeaderboardSearchChangeDetail {
|
|
25
|
+
query: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SfLeaderboardPlayerSelectDetail {
|
|
29
|
+
player: Player;
|
|
30
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Category, Player, TimeFrame } from './data.js';
|
|
2
|
+
|
|
3
|
+
/** Top bar: title, optional subtitle, live badge, back control. Omit `header` to hide. */
|
|
4
|
+
export interface SfLeaderboardHeaderSection {
|
|
5
|
+
title: string;
|
|
6
|
+
subtitle?: string;
|
|
7
|
+
icon?: string;
|
|
8
|
+
showLive?: boolean;
|
|
9
|
+
/** When set, renders the back button (`label` defaults to "Back"). */
|
|
10
|
+
backButton?: {
|
|
11
|
+
label?: string;
|
|
12
|
+
};
|
|
13
|
+
isRefreshing?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** One overview tile (`sf-leaderboard-stat-card`). */
|
|
17
|
+
export interface SfLeaderboardStatItem {
|
|
18
|
+
label: string;
|
|
19
|
+
value: string;
|
|
20
|
+
icon: string;
|
|
21
|
+
trendValue?: number;
|
|
22
|
+
trendPositive?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Overview stat cards (2×2 grid, 4-across on wide screens).
|
|
27
|
+
* Omit `stats` to hide the whole section.
|
|
28
|
+
*/
|
|
29
|
+
export interface SfLeaderboardStatsSection {
|
|
30
|
+
title?: string;
|
|
31
|
+
items: SfLeaderboardStatItem[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Podium block inside `ranking` (left column). Omit to hide podium. */
|
|
35
|
+
export interface SfLeaderboardPodiumSection {
|
|
36
|
+
title?: string;
|
|
37
|
+
badge?: string;
|
|
38
|
+
players: Player[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Player stats block inside `ranking` (right column). Omit to hide. */
|
|
42
|
+
export interface SfLeaderboardPlayerStatsSection {
|
|
43
|
+
title?: string;
|
|
44
|
+
rank: number;
|
|
45
|
+
percentile?: number;
|
|
46
|
+
message?: string;
|
|
47
|
+
username?: string;
|
|
48
|
+
earnings?: string;
|
|
49
|
+
winRate?: number;
|
|
50
|
+
gamesPlayed?: number;
|
|
51
|
+
winStreak?: number;
|
|
52
|
+
level?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Ranking row: podium (left) + player stats (right).
|
|
57
|
+
* Omit `ranking` entirely to hide the whole section.
|
|
58
|
+
*/
|
|
59
|
+
export interface SfLeaderboardRankingSection {
|
|
60
|
+
podium?: SfLeaderboardPodiumSection;
|
|
61
|
+
playerStats?: SfLeaderboardPlayerStatsSection;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Filters bar for the table. Omit on `table` to hide filters UI. */
|
|
65
|
+
export interface SfLeaderboardTableFiltersSection {
|
|
66
|
+
searchQuery?: string;
|
|
67
|
+
timeframe?: TimeFrame;
|
|
68
|
+
category?: Category;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Rankings table (bottom). Omit `table` to hide.
|
|
73
|
+
*/
|
|
74
|
+
export interface SfLeaderboardTableSection {
|
|
75
|
+
title?: string;
|
|
76
|
+
subtitle?: string;
|
|
77
|
+
players: Player[];
|
|
78
|
+
filters?: SfLeaderboardTableFiltersSection;
|
|
79
|
+
footer?: {
|
|
80
|
+
text?: string;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Page footer (below table). Omit `footer` to hide. */
|
|
85
|
+
export interface SfLeaderboardFooterSection {
|
|
86
|
+
text: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Layout config for `<sf-leaderboard>`.
|
|
91
|
+
* Only keys you pass are rendered, in order:
|
|
92
|
+
* header → stats → ranking → table → footer.
|
|
93
|
+
*/
|
|
94
|
+
export interface SfLeaderboardLayout {
|
|
95
|
+
header?: SfLeaderboardHeaderSection;
|
|
96
|
+
stats?: SfLeaderboardStatsSection;
|
|
97
|
+
ranking?: SfLeaderboardRankingSection;
|
|
98
|
+
table?: SfLeaderboardTableSection;
|
|
99
|
+
footer?: SfLeaderboardFooterSection;
|
|
100
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function formatCurrency(amount: number): string {
|
|
2
|
+
if (amount >= 1000000) return `$${(amount / 1000000).toFixed(2)}M`;
|
|
3
|
+
if (amount >= 1000) return `$${(amount / 1000).toFixed(1)}K`;
|
|
4
|
+
return `$${amount.toFixed(2)}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatTime(date: Date): string {
|
|
8
|
+
const now = new Date();
|
|
9
|
+
const diff = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
10
|
+
if (diff < 60) return `${diff}s ago`;
|
|
11
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
12
|
+
return `${Math.floor(diff / 3600)}h ago`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getInitials(name: string): string {
|
|
16
|
+
return name.slice(0, 2).toUpperCase();
|
|
17
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Theming
|
|
2
|
+
|
|
3
|
+
## Folder layout
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
src/
|
|
7
|
+
components/sf-leaderboard/ # Lit UI (podium, table, demo, package index)
|
|
8
|
+
old-components/ # Legacy packages (streaks, v1/v2 leaderboard, …)
|
|
9
|
+
theme/
|
|
10
|
+
themes/*.theme.json # Brand JSON files
|
|
11
|
+
load-theme.ts # fetch + parse JSON
|
|
12
|
+
inject-theme.ts # write CSS vars to DOM (:root)
|
|
13
|
+
theme-to-css-vars.ts # theme object → --color-* map
|
|
14
|
+
scss/_colors.scss # map-get / color.base(1) helpers
|
|
15
|
+
scss/shared.scss # semantic aliases + .card rules
|
|
16
|
+
sf-leaderboard/ # package API: data, constants, demo, types
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Flow (required order)
|
|
20
|
+
|
|
21
|
+
1. **Load** — `const theme = await loadThemeFromJson('/src/theme/themes/fallback.json')`
|
|
22
|
+
2. **Inject** — `injectTheme(theme)` → sets variables on `document.documentElement`
|
|
23
|
+
3. **Style** — SCSS uses `@use 'colors' as c` and `color.base(1)` → `var(--color-base-1, fallback)`
|
|
24
|
+
|
|
25
|
+
Variables inherit into every component shadow root automatically.
|
|
26
|
+
|
|
27
|
+
## SCSS
|
|
28
|
+
|
|
29
|
+
```scss
|
|
30
|
+
@use 'colors' as color;
|
|
31
|
+
|
|
32
|
+
.panel {
|
|
33
|
+
background: color.base(2);
|
|
34
|
+
color: color.primary(1);
|
|
35
|
+
border-color: color.place-first-icon();
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Vite `loadPaths` includes `src/theme/scss` so `@use 'colors'` resolves to `_colors.scss`.
|
|
40
|
+
|
|
41
|
+
## Client
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import {
|
|
45
|
+
loadThemeFromJson,
|
|
46
|
+
injectTheme,
|
|
47
|
+
parseThemeJson,
|
|
48
|
+
} from '@skinforge/sf-leaderboard';
|
|
49
|
+
|
|
50
|
+
const theme = await loadThemeFromJson('/assets/themes/brand.theme.json');
|
|
51
|
+
injectTheme(theme);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Demo loads `themes/fallback.json` on mount (see `components/sf-leaderboard/demo/main.ts`).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DEFAULT_SF_LEADERBOARD_THEME } from './default-theme.js';
|
|
2
|
+
import type { SfLeaderboardTheme } from './types.js';
|
|
3
|
+
|
|
4
|
+
let activeTheme: SfLeaderboardTheme = DEFAULT_SF_LEADERBOARD_THEME;
|
|
5
|
+
|
|
6
|
+
export function setActiveTheme(theme: SfLeaderboardTheme): void {
|
|
7
|
+
activeTheme = theme;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getActiveTheme(): SfLeaderboardTheme {
|
|
11
|
+
return activeTheme;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getThemeLabel(key: string, fallback: string): string {
|
|
15
|
+
return activeTheme.labels?.[key] ?? fallback;
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** `#RRGGBB` or `#RGB` → `r, g, b` for use in `rgba(var(--x-rgb), α)`. */
|
|
2
|
+
export function hexToRgbChannels(hex: string): string | null {
|
|
3
|
+
const normalized = hex.trim().replace(/^#/, '');
|
|
4
|
+
if (!/^[0-9a-fA-F]{3,8}$/.test(normalized)) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let r: number;
|
|
9
|
+
let g: number;
|
|
10
|
+
let b: number;
|
|
11
|
+
|
|
12
|
+
if (normalized.length === 3) {
|
|
13
|
+
r = parseInt(normalized[0] + normalized[0], 16);
|
|
14
|
+
g = parseInt(normalized[1] + normalized[1], 16);
|
|
15
|
+
b = parseInt(normalized[2] + normalized[2], 16);
|
|
16
|
+
} else if (normalized.length === 6) {
|
|
17
|
+
r = parseInt(normalized.slice(0, 2), 16);
|
|
18
|
+
g = parseInt(normalized.slice(2, 4), 16);
|
|
19
|
+
b = parseInt(normalized.slice(4, 6), 16);
|
|
20
|
+
} else {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return `${r}, ${g}, ${b}`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
SfLeaderboardTheme,
|
|
3
|
+
SfLeaderboardPaletteTheme,
|
|
4
|
+
SfLeaderboardPlacementTheme,
|
|
5
|
+
SfLeaderboardTableColumnTheme,
|
|
6
|
+
SfLeaderboardTypographyTheme,
|
|
7
|
+
TableColumnId,
|
|
8
|
+
ColorBaseStep,
|
|
9
|
+
ColorPrimaryStep,
|
|
10
|
+
} from './types.js';
|
|
11
|
+
|
|
12
|
+
export { sfLeaderboardTheme } from './styles.js';
|
|
13
|
+
export { loadThemeFromJson, parseThemeJson } from './load-theme.js';
|
|
14
|
+
export { injectTheme, clearTheme } from './inject-theme.js';
|
|
15
|
+
export { getActiveTheme, getThemeLabel, setActiveTheme } from './context.js';
|
|
16
|
+
export { mergeSfLeaderboardTheme } from './merge-theme.js';
|
|
17
|
+
export { themeToCssVars } from './theme-to-css-vars.js';
|
|
18
|
+
export { DEFAULT_SF_LEADERBOARD_THEME } from './default-theme.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { setActiveTheme } from './context.js';
|
|
2
|
+
import { themeToCssVars } from './theme-to-css-vars.js';
|
|
3
|
+
import type { SfLeaderboardTheme } from './types.js';
|
|
4
|
+
|
|
5
|
+
const THEME_ATTR = 'data-sf-theme';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Injects theme CSS variables into the DOM so every component shadow root inherits them.
|
|
9
|
+
* Default target is `document.documentElement` (:root).
|
|
10
|
+
*/
|
|
11
|
+
export function injectTheme(
|
|
12
|
+
theme: SfLeaderboardTheme,
|
|
13
|
+
root: HTMLElement = document.documentElement,
|
|
14
|
+
): void {
|
|
15
|
+
const vars = themeToCssVars(theme);
|
|
16
|
+
root.setAttribute(THEME_ATTR, theme.id);
|
|
17
|
+
setActiveTheme(theme);
|
|
18
|
+
|
|
19
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
20
|
+
root.style.setProperty(key, value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Removes theme variables injected on a DOM node. */
|
|
25
|
+
export function clearTheme(
|
|
26
|
+
root: HTMLElement = document.documentElement,
|
|
27
|
+
): void {
|
|
28
|
+
root.removeAttribute(THEME_ATTR);
|
|
29
|
+
const toRemove: string[] = [];
|
|
30
|
+
for (let i = 0; i < root.style.length; i++) {
|
|
31
|
+
const name = root.style.item(i);
|
|
32
|
+
if (name.startsWith('--color-') || name.startsWith('--place-') || name.startsWith('--table-col-') || name.startsWith('--font-')) {
|
|
33
|
+
toRemove.push(name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const name of toRemove) {
|
|
37
|
+
root.style.removeProperty(name);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { mergeSfLeaderboardTheme } from './merge-theme.js';
|
|
2
|
+
import { DEFAULT_SF_LEADERBOARD_THEME } from './default-theme.js';
|
|
3
|
+
import type { SfLeaderboardTheme } from './types.js';
|
|
4
|
+
|
|
5
|
+
/** Load theme JSON from a URL (dev server, CDN, CMS). */
|
|
6
|
+
export async function loadThemeFromJson(url: string): Promise<SfLeaderboardTheme> {
|
|
7
|
+
const response = await fetch(url);
|
|
8
|
+
if (!response.ok) {
|
|
9
|
+
throw new Error(`Failed to load theme: ${response.status} ${url}`);
|
|
10
|
+
}
|
|
11
|
+
const json: unknown = await response.json();
|
|
12
|
+
return parseThemeJson(json);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Parse theme JSON already fetched or bundled. Supports `{ theme: {…} }` or flat root. */
|
|
16
|
+
export function parseThemeJson(json: unknown): SfLeaderboardTheme {
|
|
17
|
+
if (!json || typeof json !== 'object') {
|
|
18
|
+
throw new Error('Invalid theme JSON: expected an object');
|
|
19
|
+
}
|
|
20
|
+
const root = json as Record<string, unknown>;
|
|
21
|
+
const payload = (root.theme ?? root) as Partial<SfLeaderboardTheme>;
|
|
22
|
+
if (!payload.palette || !payload.placement || !payload.typography) {
|
|
23
|
+
throw new Error('Invalid theme JSON: missing palette, placement, or typography');
|
|
24
|
+
}
|
|
25
|
+
return mergeSfLeaderboardTheme(DEFAULT_SF_LEADERBOARD_THEME, payload);
|
|
26
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { SfLeaderboardTheme } from './types.js';
|
|
2
|
+
|
|
3
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Deep-merge client theme onto defaults (client wins). */
|
|
8
|
+
export function mergeSfLeaderboardTheme(
|
|
9
|
+
base: SfLeaderboardTheme,
|
|
10
|
+
overrides?: Partial<SfLeaderboardTheme> | null,
|
|
11
|
+
): SfLeaderboardTheme {
|
|
12
|
+
if (!overrides) {
|
|
13
|
+
return base;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const result = structuredClone(base);
|
|
17
|
+
|
|
18
|
+
if (overrides.id) result.id = overrides.id;
|
|
19
|
+
if (overrides.name) result.name = overrides.name;
|
|
20
|
+
if (overrides.labels) {
|
|
21
|
+
result.labels = { ...result.labels, ...overrides.labels };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (overrides.palette) {
|
|
25
|
+
result.palette = {
|
|
26
|
+
...result.palette,
|
|
27
|
+
...overrides.palette,
|
|
28
|
+
base: { ...result.palette.base, ...overrides.palette.base },
|
|
29
|
+
primary: { ...result.palette.primary, ...overrides.palette.primary },
|
|
30
|
+
semantic: { ...result.palette.semantic, ...overrides.palette.semantic },
|
|
31
|
+
surface: { ...result.palette.surface, ...overrides.palette.surface },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (overrides.typography) {
|
|
36
|
+
result.typography = { ...result.typography, ...overrides.typography };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (overrides.placement) {
|
|
40
|
+
result.placement = {
|
|
41
|
+
first: { ...result.placement.first, ...overrides.placement.first },
|
|
42
|
+
second: { ...result.placement.second, ...overrides.placement.second },
|
|
43
|
+
third: { ...result.placement.third, ...overrides.placement.third },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (overrides.table?.columns) {
|
|
48
|
+
for (const [key, value] of Object.entries(overrides.table.columns)) {
|
|
49
|
+
if (!isPlainObject(value)) continue;
|
|
50
|
+
const existing = result.table.columns[key as keyof typeof result.table.columns];
|
|
51
|
+
result.table.columns[key as keyof typeof result.table.columns] = {
|
|
52
|
+
...existing,
|
|
53
|
+
...value,
|
|
54
|
+
} as (typeof result.table.columns)[keyof typeof result.table.columns];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|