@graph-render/tournament-tree 1.0.1

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 (50) hide show
  1. package/.eslintrc.json +6 -0
  2. package/CHANGELOG.md +23 -0
  3. package/README.md +0 -0
  4. package/dist/index.js +2258 -0
  5. package/dist/src/components/BracketToolbar.d.ts +11 -0
  6. package/dist/src/components/BracketToolbar.d.ts.map +1 -0
  7. package/dist/src/components/SquashNode.d.ts +10 -0
  8. package/dist/src/components/SquashNode.d.ts.map +1 -0
  9. package/dist/src/components/TournamentBracket.d.ts +4 -0
  10. package/dist/src/components/TournamentBracket.d.ts.map +1 -0
  11. package/dist/src/constants/index.d.ts +3 -0
  12. package/dist/src/constants/index.d.ts.map +1 -0
  13. package/dist/src/constants/node.d.ts +127 -0
  14. package/dist/src/constants/node.d.ts.map +1 -0
  15. package/dist/src/constants/tournament.d.ts +4 -0
  16. package/dist/src/constants/tournament.d.ts.map +1 -0
  17. package/dist/src/contexts/BracketThemeContext.d.ts +16 -0
  18. package/dist/src/contexts/BracketThemeContext.d.ts.map +1 -0
  19. package/dist/src/index.d.ts +9 -0
  20. package/dist/src/index.d.ts.map +1 -0
  21. package/dist/src/types/index.d.ts +3 -0
  22. package/dist/src/types/index.d.ts.map +1 -0
  23. package/dist/src/types/squash.d.ts +18 -0
  24. package/dist/src/types/squash.d.ts.map +1 -0
  25. package/dist/src/types/tournament.d.ts +13 -0
  26. package/dist/src/types/tournament.d.ts.map +1 -0
  27. package/dist/src/utils/pathKeys.d.ts +8 -0
  28. package/dist/src/utils/pathKeys.d.ts.map +1 -0
  29. package/dist/src/utils/roundLabels.d.ts +16 -0
  30. package/dist/src/utils/roundLabels.d.ts.map +1 -0
  31. package/dist/tsconfig.tsbuildinfo +1 -0
  32. package/index.html +18 -0
  33. package/package.json +51 -0
  34. package/project.json +60 -0
  35. package/src/components/BracketToolbar.tsx +135 -0
  36. package/src/components/SquashNode.tsx +813 -0
  37. package/src/components/TournamentBracket.tsx +992 -0
  38. package/src/constants/index.ts +2 -0
  39. package/src/constants/node.ts +96 -0
  40. package/src/constants/tournament.ts +54 -0
  41. package/src/contexts/BracketThemeContext.tsx +35 -0
  42. package/src/index.ts +12 -0
  43. package/src/types/index.ts +2 -0
  44. package/src/types/squash.ts +21 -0
  45. package/src/types/tournament.ts +14 -0
  46. package/src/utils/pathKeys.ts +50 -0
  47. package/src/utils/roundLabels.ts +110 -0
  48. package/tsconfig.json +19 -0
  49. package/tsconfig.node.json +11 -0
  50. package/vite.config.ts +21 -0
@@ -0,0 +1,2 @@
1
+ export * from './node';
2
+ export * from './tournament';
@@ -0,0 +1,96 @@
1
+ import type { SquashPlayer } from '../types/squash';
2
+
3
+ export const NODE_DIMENSIONS = {
4
+ WIDTH: 280,
5
+ HEIGHT: 112,
6
+ } as const;
7
+
8
+ export const THEME_COLORS_LIGHT = {
9
+ BASE_BG: '#ffffff',
10
+ SURFACE_BG: '#f7f6f3',
11
+ HEADER_BG: '#ffffff',
12
+ HEADER_TITLE: '#2d2d2d',
13
+ HEADER_MUTED: '#6b6b6b',
14
+ HEADER_BORDER: '#ece9e2',
15
+ ICON_BG: '#7c9070',
16
+ ICON_FG: '#ffffff',
17
+ BADGE_BG: '#f1eee8',
18
+ BADGE_DOT: '#7c9070',
19
+ BADGE_TEXT: '#6b6b6b',
20
+ HOVER_BG: '#faf8f2',
21
+ CREST_BG: '#c9b8a8',
22
+ CREST_TEXT: '#5d4d3f',
23
+ WINNER_CREST_BG: '#7c9070',
24
+ WINNER_CREST_TEXT: '#ffffff',
25
+ ROW_BG: '#ffffff',
26
+ ROW_BG_WINNER: '#ffffff',
27
+ ROW_HOVER_BG: '#f7f3ec',
28
+ FOREGROUND: '#2d2d2d',
29
+ MUTED_TEXT: '#8e8e93',
30
+ DARK_TEXT: '#2d2d2d',
31
+ BORDER: '#f0efec',
32
+ DARK_BORDER: '#e5e4e1',
33
+ CARD_BORDER: '#d9d6cf',
34
+ WINNER_ACCENT: '#2d2d2d',
35
+ WINNING_SCORE: '#2d2d2d',
36
+ LIVE_WINNING_SCORE: '#2d2d2d',
37
+ LIVE_INDICATOR: '#d97706',
38
+ UPCOMING_TEXT: '#b0aba3',
39
+ EDGE_COLOR: '#d9d6cf',
40
+ LABEL_TEXT: '#8e8e93',
41
+ TOOLBAR_BG: '#f7f3ec',
42
+ TOOLBAR_BORDER: '#e4ded2',
43
+ TOOLBAR_ICON: '#4b5563',
44
+ TOOLBAR_ICON_ACTIVE: '#2d2d2d',
45
+ SHADOW: '0 20px 48px rgba(45, 45, 45, 0.08)',
46
+ CARD_SHADOW: '0 10px 24px rgba(45, 45, 45, 0.06)',
47
+ } as const;
48
+
49
+ export const THEME_COLORS_DARK = {
50
+ BASE_BG: '#1f242b',
51
+ SURFACE_BG: '#191e24',
52
+ HEADER_BG: '#232931',
53
+ HEADER_TITLE: '#f7f5ef',
54
+ HEADER_MUTED: '#c4beb1',
55
+ HEADER_BORDER: '#313844',
56
+ ICON_BG: '#7c9070',
57
+ ICON_FG: '#ffffff',
58
+ BADGE_BG: '#232b33',
59
+ BADGE_DOT: '#9ab08d',
60
+ BADGE_TEXT: '#d8d2c7',
61
+ HOVER_BG: '#242b33',
62
+ CREST_BG: '#53473e',
63
+ CREST_TEXT: '#efe7db',
64
+ WINNER_CREST_BG: '#8da180',
65
+ WINNER_CREST_TEXT: '#09120a',
66
+ ROW_BG: '#232931',
67
+ ROW_BG_WINNER: '#232931',
68
+ ROW_HOVER_BG: '#2a313a',
69
+ FOREGROUND: '#f7f5ef',
70
+ MUTED_TEXT: '#a7aaaf',
71
+ DARK_TEXT: '#f7f5ef',
72
+ BORDER: '#313844',
73
+ DARK_BORDER: '#47515f',
74
+ CARD_BORDER: '#5d6470',
75
+ WINNER_ACCENT: '#f7f5ef',
76
+ WINNING_SCORE: '#f7f5ef',
77
+ LIVE_WINNING_SCORE: '#f7f5ef',
78
+ LIVE_INDICATOR: '#f59e0b',
79
+ UPCOMING_TEXT: '#70757d',
80
+ EDGE_COLOR: '#5d6470',
81
+ LABEL_TEXT: '#a7aaaf',
82
+ TOOLBAR_BG: '#232b33',
83
+ TOOLBAR_BORDER: '#38424d',
84
+ TOOLBAR_ICON: '#d7d1c6',
85
+ TOOLBAR_ICON_ACTIVE: '#ffffff',
86
+ SHADOW: '0 22px 54px rgba(0, 0, 0, 0.28)',
87
+ CARD_SHADOW: '0 12px 32px rgba(0, 0, 0, 0.24)',
88
+ } as const;
89
+
90
+ // Default to light theme
91
+ export const THEME_COLORS = THEME_COLORS_LIGHT;
92
+
93
+ export const DEFAULT_PLAYERS: SquashPlayer[] = [
94
+ { name: 'TBD', seed: 0 },
95
+ { name: 'TBD', seed: 0 },
96
+ ];
@@ -0,0 +1,54 @@
1
+ import type { GraphConfig } from '@graph-render/types';
2
+ import { LayoutType, LayoutDirection, EdgeType } from '@graph-render/types';
3
+ import { NODE_DIMENSIONS } from './node';
4
+
5
+ export const DEFAULT_TOURNAMENT_CONFIG: Readonly<GraphConfig> = {
6
+ layout: LayoutType.Tree,
7
+ layoutDirection: LayoutDirection.LTR,
8
+ width: 1600,
9
+ height: 1200,
10
+ padding: 40,
11
+ defaultEdgeType: EdgeType.Undirected,
12
+ routingStyle: 'orthogonal',
13
+ curveEdges: false,
14
+ curveStrength: 0,
15
+ forceRightToLeft: true,
16
+ hoverHighlight: false,
17
+ hoverEdgeColor: '#7c9070',
18
+ hoverNodeInColor: '#7c9070',
19
+ hoverNodeOutColor: '#7c9070',
20
+ hoverNodeBothColor: '#7c9070',
21
+ theme: {
22
+ background: '#f7f6f3',
23
+ edgeColor: '#d9d6cf',
24
+ edgeWidth: 2,
25
+ nodeGap: 72,
26
+ fontFamily: '"Plus Jakarta Sans", "Segoe UI", system-ui, sans-serif',
27
+ },
28
+ fixedNodeSize: {
29
+ width: NODE_DIMENSIONS.WIDTH,
30
+ height: NODE_DIMENSIONS.HEIGHT,
31
+ },
32
+ labelOffset: 46,
33
+ labelPillBackground: 'transparent',
34
+ labelPillBorderColor: 'transparent',
35
+ labelPillTextColor: '#444b55',
36
+ } as const;
37
+
38
+ export const DARK_TOURNAMENT_CONFIG: Readonly<GraphConfig> = {
39
+ ...DEFAULT_TOURNAMENT_CONFIG,
40
+ hoverEdgeColor: '#9ab08d',
41
+ hoverNodeInColor: '#9ab08d',
42
+ hoverNodeOutColor: '#9ab08d',
43
+ hoverNodeBothColor: '#9ab08d',
44
+ labelPillBackground: 'transparent',
45
+ labelPillBorderColor: 'transparent',
46
+ labelPillTextColor: '#d8d2c7',
47
+ theme: {
48
+ background: '#191e24',
49
+ edgeColor: '#5d6470',
50
+ edgeWidth: 2,
51
+ nodeGap: 72,
52
+ fontFamily: '"Plus Jakarta Sans", "Segoe UI", system-ui, sans-serif',
53
+ },
54
+ } as const;
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import { THEME_COLORS_LIGHT, THEME_COLORS_DARK } from '../constants';
3
+
4
+ export type ThemeMode = 'light' | 'dark';
5
+
6
+ type ThemeColors = typeof THEME_COLORS_LIGHT | typeof THEME_COLORS_DARK;
7
+
8
+ interface BracketThemeContextValue {
9
+ mode: ThemeMode;
10
+ colors: ThemeColors;
11
+ }
12
+
13
+ const BracketThemeContext = React.createContext<BracketThemeContextValue>({
14
+ mode: 'light',
15
+ colors: THEME_COLORS_LIGHT,
16
+ });
17
+
18
+ export const useBracketTheme = () => React.useContext(BracketThemeContext);
19
+
20
+ interface BracketThemeProviderProps {
21
+ mode: ThemeMode;
22
+ children: React.ReactNode;
23
+ }
24
+
25
+ export const BracketThemeProvider: React.FC<BracketThemeProviderProps> = ({ mode, children }) => {
26
+ const value = React.useMemo(
27
+ () => ({
28
+ mode,
29
+ colors: mode === 'dark' ? THEME_COLORS_DARK : THEME_COLORS_LIGHT,
30
+ }),
31
+ [mode]
32
+ );
33
+
34
+ return <BracketThemeContext.Provider value={value}>{children}</BracketThemeContext.Provider>;
35
+ };
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { SquashNode } from './components/SquashNode';
2
+ export { TournamentBracket } from './components/TournamentBracket';
3
+ export { BracketToolbar } from './components/BracketToolbar';
4
+ export { injectTournamentPathKeys } from './utils/pathKeys';
5
+ export {
6
+ roundLabelsForGraph,
7
+ roundLabelsForMatchCount,
8
+ roundLabelsForRoundCount,
9
+ } from './utils/roundLabels';
10
+ export { BracketThemeProvider, useBracketTheme } from './contexts/BracketThemeContext';
11
+ export type { ThemeMode } from './contexts/BracketThemeContext';
12
+ export type { SquashNodeRenderMode, TournamentBracketProps } from './types';
@@ -0,0 +1,2 @@
1
+ export * from './squash';
2
+ export * from './tournament';
@@ -0,0 +1,21 @@
1
+ import type { NodeData, PositionedNode } from '@graph-render/types';
2
+
3
+ export interface SquashPlayer {
4
+ name: string;
5
+ seed?: number;
6
+ country?: string;
7
+ }
8
+
9
+ export type MatchStatus = 'completed' | 'live' | 'upcoming';
10
+
11
+ export interface SquashMatchMeta {
12
+ stage?: string;
13
+ players?: SquashPlayer[];
14
+ sets?: number[][];
15
+ tiebreaks?: (number[] | null)[]; // Tiebreak scores for each set, null if no tiebreak
16
+ status?: MatchStatus;
17
+ currentSet?: number; // For live matches, which set is being played
18
+ }
19
+
20
+ export type SquashNodeData = NodeData<unknown, SquashMatchMeta, string>;
21
+ export type SquashPositionedNode = PositionedNode<unknown, SquashMatchMeta, string>;
@@ -0,0 +1,14 @@
1
+ import type { GraphConfig, VertexComponent, NxGraphInput } from '@graph-render/types';
2
+
3
+ export type SquashNodeRenderMode = 'svg' | 'html' | 'export' | 'server';
4
+
5
+ export interface TournamentBracketProps {
6
+ graph: NxGraphInput;
7
+ config?: Partial<GraphConfig>;
8
+ vertexComponent?: VertexComponent;
9
+ nodeRenderMode?: SquashNodeRenderMode;
10
+ title?: string;
11
+ badgeText?: string;
12
+ showToolbar?: boolean;
13
+ onInvalidNode?: (nodeId: string, error: Error) => void;
14
+ }
@@ -0,0 +1,50 @@
1
+ import type { NxGraphInput } from '@graph-render/types';
2
+
3
+ /**
4
+ * Enrich tournament nodes with a generic `meta.pathKeys` array derived from
5
+ * `meta.players[*].name` so shared graph path traversal can follow players
6
+ * across rounds.
7
+ */
8
+ export function injectTournamentPathKeys(graph: NxGraphInput): NxGraphInput {
9
+ if (!graph.nodes) {
10
+ return graph;
11
+ }
12
+
13
+ let changed = false;
14
+ const nextNodes: typeof graph.nodes = {};
15
+
16
+ for (const [id, attrs] of Object.entries(graph.nodes)) {
17
+ const players = (attrs.meta as Record<string, unknown> | undefined)?.players;
18
+ if (!Array.isArray(players) || players.length === 0) {
19
+ nextNodes[id] = attrs;
20
+ continue;
21
+ }
22
+
23
+ const names = players
24
+ .map((player) => {
25
+ if (
26
+ player &&
27
+ typeof player === 'object' &&
28
+ typeof (player as Record<string, unknown>).name === 'string'
29
+ ) {
30
+ return ((player as Record<string, unknown>).name as string).trim();
31
+ }
32
+
33
+ return '';
34
+ })
35
+ .filter(Boolean);
36
+
37
+ if (names.length === 0) {
38
+ nextNodes[id] = attrs;
39
+ continue;
40
+ }
41
+
42
+ changed = true;
43
+ nextNodes[id] = {
44
+ ...attrs,
45
+ meta: { ...(attrs.meta as object | undefined), pathKeys: names } as Record<string, unknown>,
46
+ };
47
+ }
48
+
49
+ return changed ? { ...graph, nodes: nextNodes } : graph;
50
+ }
@@ -0,0 +1,110 @@
1
+ import type { NxGraphInput } from '@graph-render/types';
2
+
3
+ function buildOutgoingMap(graph: NxGraphInput): Map<string, string[]> {
4
+ const outgoing = new Map<string, string[]>();
5
+
6
+ Object.entries(graph.adj).forEach(([source, neighbors]) => {
7
+ outgoing.set(source, Object.keys(neighbors));
8
+ Object.keys(neighbors).forEach((target) => {
9
+ if (!outgoing.has(target)) {
10
+ outgoing.set(target, []);
11
+ }
12
+ });
13
+ });
14
+
15
+ Object.keys(graph.nodes ?? {}).forEach((nodeId) => {
16
+ if (!outgoing.has(nodeId)) {
17
+ outgoing.set(nodeId, []);
18
+ }
19
+ });
20
+
21
+ return outgoing;
22
+ }
23
+
24
+ function inferRoundCount(graph: NxGraphInput): number {
25
+ const outgoing = buildOutgoingMap(graph);
26
+ if (!outgoing.size) {
27
+ return 0;
28
+ }
29
+
30
+ const inDegree = new Map(Array.from(outgoing.keys(), (nodeId) => [nodeId, 0]));
31
+ outgoing.forEach((targets) => {
32
+ targets.forEach((target) => {
33
+ inDegree.set(target, (inDegree.get(target) ?? 0) + 1);
34
+ });
35
+ });
36
+
37
+ const roots = Array.from(inDegree.entries())
38
+ .filter(([, degree]) => degree === 0)
39
+ .map(([nodeId]) => nodeId);
40
+
41
+ if (!roots.length) {
42
+ return 0;
43
+ }
44
+
45
+ const queue = [...roots];
46
+ const levels = new Map<string, number>(roots.map((nodeId) => [nodeId, 0]));
47
+
48
+ for (let index = 0; index < queue.length; index += 1) {
49
+ const current = queue[index];
50
+ const level = levels.get(current) ?? 0;
51
+
52
+ (outgoing.get(current) ?? []).forEach((target) => {
53
+ const nextLevel = level + 1;
54
+ const existing = levels.get(target);
55
+ if (existing == null || nextLevel > existing) {
56
+ levels.set(target, nextLevel);
57
+ }
58
+
59
+ inDegree.set(target, (inDegree.get(target) ?? 0) - 1);
60
+ if ((inDegree.get(target) ?? 0) === 0) {
61
+ queue.push(target);
62
+ }
63
+ });
64
+ }
65
+
66
+ const maxLevel = Math.max(...levels.values());
67
+ return Number.isFinite(maxLevel) ? maxLevel + 1 : 0;
68
+ }
69
+
70
+ /**
71
+ * Derive round labels from the number of rounds.
72
+ * Example: 4 rounds -> ["1/8", "1/4", "1/2", "Final"].
73
+ */
74
+ export function roundLabelsForRoundCount(roundCount: number): string[] {
75
+ if (!Number.isFinite(roundCount) || roundCount <= 0) return [];
76
+
77
+ return Array.from({ length: roundCount }, (_, idx) => {
78
+ const remaining = roundCount - idx;
79
+ if (remaining === 1) {
80
+ return 'FINAL';
81
+ }
82
+
83
+ if (remaining === 2) {
84
+ return 'SEMIFINALS';
85
+ }
86
+
87
+ if (remaining === 3) {
88
+ return 'QUARTERFINALS';
89
+ }
90
+
91
+ return `ROUND OF ${2 ** (remaining - 1)}`;
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Derive round labels from the number of matches (nodes).
97
+ * Example: 15 matches -> 4 rounds => ["1/8", "1/4", "1/2", "Final"].
98
+ */
99
+ export function roundLabelsForMatchCount(matchCount: number): string[] {
100
+ if (!Number.isFinite(matchCount) || matchCount <= 0) return [];
101
+ const rounds = Math.max(1, Math.ceil(Math.log2(matchCount + 1)));
102
+ return roundLabelsForRoundCount(rounds);
103
+ }
104
+
105
+ /**
106
+ * Convenience helper to derive labels directly from a graph definition.
107
+ */
108
+ export function roundLabelsForGraph(graph: NxGraphInput): string[] {
109
+ return roundLabelsForRoundCount(inferRoundCount(graph));
110
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "extends": "../../tsconfig.react.json",
3
+ "compilerOptions": {
4
+ "composite": true,
5
+ "outDir": "./dist",
6
+ "noEmit": false,
7
+ "emitDeclarationOnly": false,
8
+ "allowImportingTsExtensions": false,
9
+ "useDefineForClassFields": true
10
+ },
11
+ "include": ["src"],
12
+ "exclude": ["node_modules", "dist"],
13
+ "references": [
14
+ { "path": "./tsconfig.node.json" },
15
+ { "path": "../types" },
16
+ { "path": "../core-graph-render" },
17
+ { "path": "../react-graph-render" }
18
+ ]
19
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "composite": true,
7
+ "allowSyntheticDefaultImports": true,
8
+ "types": []
9
+ },
10
+ "include": ["vite.config.ts"]
11
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ build: {
7
+ lib: {
8
+ entry: './src/index.ts',
9
+ name: 'ReactTournamentTree',
10
+ formats: ['es'],
11
+ fileName: () => 'index.js',
12
+ },
13
+ rollupOptions: {
14
+ external: ['react', 'react-dom', '@graph-render/react', '@graph-render/types'],
15
+ },
16
+ },
17
+ server: {
18
+ port: 5173,
19
+ open: true,
20
+ },
21
+ });