tournaments-bracket 2.0.4

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/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17
+
18
+ ```js
19
+ export default defineConfig([
20
+ globalIgnores(['dist']),
21
+ {
22
+ files: ['**/*.{ts,tsx}'],
23
+ extends: [
24
+ // Other configs...
25
+
26
+ // Remove tseslint.configs.recommended and replace with this
27
+ tseslint.configs.recommendedTypeChecked,
28
+ // Alternatively, use this for stricter rules
29
+ tseslint.configs.strictTypeChecked,
30
+ // Optionally, add this for stylistic rules
31
+ tseslint.configs.stylisticTypeChecked,
32
+
33
+ // Other configs...
34
+ ],
35
+ languageOptions: {
36
+ parserOptions: {
37
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
+ tsconfigRootDir: import.meta.dirname,
39
+ },
40
+ // other options...
41
+ },
42
+ },
43
+ ])
44
+ ```
45
+
46
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47
+
48
+ ```js
49
+ // eslint.config.js
50
+ import reactX from 'eslint-plugin-react-x'
51
+ import reactDom from 'eslint-plugin-react-dom'
52
+
53
+ export default defineConfig([
54
+ globalIgnores(['dist']),
55
+ {
56
+ files: ['**/*.{ts,tsx}'],
57
+ extends: [
58
+ // Other configs...
59
+ // Enable lint rules for React
60
+ reactX.configs['recommended-typescript'],
61
+ // Enable lint rules for React DOM
62
+ reactDom.configs.recommended,
63
+ ],
64
+ languageOptions: {
65
+ parserOptions: {
66
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
67
+ tsconfigRootDir: import.meta.dirname,
68
+ },
69
+ // other options...
70
+ },
71
+ },
72
+ ])
73
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import path from "path";
3
+ import { generateBracket } from "../core/generateBracket.js";
4
+ const teamsPath = path.resolve(process.cwd(), "src/teams.ts");
5
+ const teamsUrl = new URL(`file://${teamsPath}`);
6
+ (async () => {
7
+ const teamsModule = await import(teamsUrl.href);
8
+ const teams = teamsModule.teams;
9
+ generateBracket(teams, path.resolve(process.cwd(), "src/bracket/rounds.ts"));
10
+ })();
@@ -0,0 +1,7 @@
1
+ import { Match as MatchType } from '../core/types';
2
+ type Props = {
3
+ match: MatchType;
4
+ isFinal?: boolean;
5
+ };
6
+ export declare function Match({ match, isFinal }: Props): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,7 @@
1
+ import { Round } from '../core/types';
2
+ type Props = {
3
+ round: Round;
4
+ matchRefs: React.MutableRefObject<Record<number, HTMLDivElement | null>>;
5
+ };
6
+ export declare function RoundColumn({ round, matchRefs }: Props): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,7 @@
1
+ import { Team } from '../core/types';
2
+ type Props = {
3
+ team?: Team | null;
4
+ status: "winner" | "loser" | "default";
5
+ };
6
+ export declare function TeamBox({ team, status }: Props): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,10 @@
1
+ import { BracketTheme, Round } from '../core/types';
2
+ type Props = {
3
+ rounds: Round[];
4
+ theme?: BracketTheme & {
5
+ lineStyle?: "straight" | "angled" | "curved";
6
+ };
7
+ className?: string;
8
+ };
9
+ export declare function TournamentBracket({ rounds, theme, className }: Props): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { Team } from "../core/types.js";
2
+ export declare function generateBracket(teams: Team[], outputFile: string): void;
@@ -0,0 +1,31 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ export function generateBracket(teams, outputFile) {
4
+ const rounds = [];
5
+ let matchId = 1;
6
+ let currentRoundTeams = [...teams];
7
+ while (currentRoundTeams.length > 1) {
8
+ const roundMatches = [];
9
+ for (let i = 0; i < currentRoundTeams.length; i += 2) {
10
+ const match = {
11
+ id: matchId++,
12
+ teams: [
13
+ currentRoundTeams[i] ?? null,
14
+ currentRoundTeams[i + 1] ?? null
15
+ ],
16
+ winnerId: null
17
+ };
18
+ roundMatches.push(match);
19
+ }
20
+ rounds.push(roundMatches);
21
+ currentRoundTeams = Array(Math.ceil(currentRoundTeams.length / 2)).fill(null);
22
+ }
23
+ const outputPath = path.resolve(outputFile);
24
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
25
+ const content = `import type { Round } from "tournaments-bracket";
26
+
27
+ export const rounds: Round[] = ${JSON.stringify(rounds, null, 2)};
28
+ `;
29
+ fs.writeFileSync(outputPath, content);
30
+ console.log("✔ Bracket generated:", outputPath);
31
+ }
@@ -0,0 +1,24 @@
1
+ export type Team = {
2
+ id: number | string;
3
+ name: string;
4
+ logo?: string;
5
+ };
6
+ export type Match = {
7
+ id: number;
8
+ teams: [Team | null, Team | null];
9
+ winnerId?: Team["id"] | null;
10
+ };
11
+ export type BracketTheme = {
12
+ bgTeam?: string;
13
+ borderColor?: string;
14
+ lineColor?: string;
15
+ teamWidth?: string;
16
+ lineWidth?: string;
17
+ borderWin?: string;
18
+ opacityWin?: string;
19
+ bgWin?: string;
20
+ borderLos?: string;
21
+ opacityLos?: string;
22
+ bgLos?: string;
23
+ };
24
+ export type Round = Match[];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { TournamentBracket } from './components/TournamentBracket';
2
+ export type { Team, Match, BracketTheme } from './core/types';
@@ -0,0 +1,103 @@
1
+ import { jsx as o, jsxs as p, Fragment as F } from "react/jsx-runtime";
2
+ import { useRef as B, useState as S, useLayoutEffect as T } from "react";
3
+ function M({ team: t, status: n }) {
4
+ return /* @__PURE__ */ o("div", { className: `team ${t ? `${n}` : "empty"}`, children: t ? /* @__PURE__ */ p(F, { children: [
5
+ t.logo && /* @__PURE__ */ o("img", { src: t.logo, alt: t.name }),
6
+ /* @__PURE__ */ o("span", { children: t.name })
7
+ ] }) : /* @__PURE__ */ o("span", { children: "Нет участника" }) });
8
+ }
9
+ function P({ match: t, isFinal: n }) {
10
+ const [r, e] = t.teams, i = t.winnerId === r?.id ? r : t.winnerId === e?.id ? e : null, u = i && r && e ? i.id === r.id ? e : r : null;
11
+ return /* @__PURE__ */ p("div", { className: "group", children: [
12
+ /* @__PURE__ */ o(
13
+ M,
14
+ {
15
+ team: r,
16
+ status: i?.id === r?.id ? "winner" : u?.id === r?.id ? "loser" : "default"
17
+ }
18
+ ),
19
+ !(n && !e) && /* @__PURE__ */ o(
20
+ M,
21
+ {
22
+ team: e,
23
+ status: i?.id === e?.id ? "winner" : u?.id === e?.id ? "loser" : "default"
24
+ }
25
+ )
26
+ ] });
27
+ }
28
+ function j(t) {
29
+ return t ? {
30
+ "--tb-bg-team": t.bgTeam,
31
+ "--tb-border": t.borderColor,
32
+ "--tb-stroke": t.lineColor,
33
+ "--tb-team-width": t.teamWidth,
34
+ "--tb-line-width": t.lineWidth,
35
+ "--tb-border-winner": t.borderWin,
36
+ "--tb-opacity-winner": t.opacityWin,
37
+ "--tb-bg-winner": t.bgWin,
38
+ "--tb-border-loser": t.borderLos,
39
+ "--tb-opacity-loser": t.opacityLos,
40
+ "--tb-bg-loser": t.bgLos
41
+ } : {};
42
+ }
43
+ function N(t, n, r = "angled", e = 24) {
44
+ switch (r) {
45
+ case "curved": {
46
+ const i = t.x + (n.x - t.x) / 2;
47
+ return `M ${t.x} ${t.y} C ${i} ${t.y}, ${i} ${n.y}, ${n.x} ${n.y}`;
48
+ }
49
+ case "straight":
50
+ return `M ${t.x} ${t.y} L ${n.x} ${n.y}`;
51
+ default:
52
+ return `M ${t.x} ${t.y} H ${t.x + e} V ${n.y} H ${n.x}`;
53
+ }
54
+ }
55
+ function X({ rounds: t, theme: n, className: r }) {
56
+ const e = B(null), i = B({}), [u, v] = S([]);
57
+ function $(c, l) {
58
+ const s = c.getBoundingClientRect(), a = l.getBoundingClientRect();
59
+ return {
60
+ x: s.right - a.left,
61
+ y: s.top - a.top + s.height / 2
62
+ };
63
+ }
64
+ return T(() => {
65
+ if (!e.current) return;
66
+ const c = e.current, l = [], s = n?.lineStyle ?? "angled";
67
+ t.forEach((a, d) => {
68
+ a.forEach((f, E) => {
69
+ const g = i.current[f.id];
70
+ if (!g) return;
71
+ const b = g.querySelector(".team:nth-child(1)"), m = g.querySelector(".team:nth-child(2)");
72
+ if (d === t.length - 1 && a.length === 1 || !b || !m) return;
73
+ const h = $(b, c), y = $(m, c);
74
+ l.push(N(h, y, s));
75
+ const w = { x: (h.x + y.x) / 2, y: (h.y + y.y) / 2 };
76
+ if (d < t.length - 1) {
77
+ const W = t[d + 1][Math.floor(E / 2)], C = i.current[W?.id];
78
+ if (!C) return;
79
+ const x = C.getBoundingClientRect(), R = c.getBoundingClientRect(), k = { x: x.left - R.left, y: x.top - R.top + x.height / 2 }, L = { x: w.x + 24, y: w.y };
80
+ l.push(N(L, k, s));
81
+ }
82
+ });
83
+ }), v(l);
84
+ }, [t, n]), /* @__PURE__ */ p("div", { className: `tournament-bracket bracket ${r ?? ""}`, style: j(n), ref: e, children: [
85
+ /* @__PURE__ */ o("svg", { className: "lines-layer", children: u.map((c, l) => /* @__PURE__ */ o("path", { d: c, stroke: n?.lineColor, fill: "none", strokeWidth: n?.lineWidth ?? 2 }, l)) }),
86
+ t.map((c, l) => /* @__PURE__ */ o("div", { className: "round-column", children: c.map((s, a) => {
87
+ const d = a === t.length - 1 && c.length === 1;
88
+ return /* @__PURE__ */ o(
89
+ "div",
90
+ {
91
+ ref: (f) => {
92
+ i.current[s.id] = f;
93
+ },
94
+ children: /* @__PURE__ */ o(P, { match: s, isFinal: d })
95
+ },
96
+ s.id
97
+ );
98
+ }) }, l))
99
+ ] });
100
+ }
101
+ export {
102
+ X as TournamentBracket
103
+ };
@@ -0,0 +1 @@
1
+ (function(a,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("react/jsx-runtime"),require("react")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react"],e):(a=typeof globalThis<"u"?globalThis:a||self,e(a.TournamentsBracket={},a.jsxRuntime,a.React))})(this,(function(a,e,u){"use strict";function $({team:t,status:n}){return e.jsx("div",{className:`team ${t?`${n}`:"empty"}`,children:t?e.jsxs(e.Fragment,{children:[t.logo&&e.jsx("img",{src:t.logo,alt:t.name}),e.jsx("span",{children:t.name})]}):e.jsx("span",{children:"Нет участника"})})}function N({match:t,isFinal:n}){const[i,r]=t.teams,o=t.winnerId===i?.id?i:t.winnerId===r?.id?r:null,g=o&&i&&r?o.id===i.id?r:i:null;return e.jsxs("div",{className:"group",children:[e.jsx($,{team:i,status:o?.id===i?.id?"winner":g?.id===i?.id?"loser":"default"}),!(n&&!r)&&e.jsx($,{team:r,status:o?.id===r?.id?"winner":g?.id===r?.id?"loser":"default"})]})}function E(t){return t?{"--tb-bg-team":t.bgTeam,"--tb-border":t.borderColor,"--tb-stroke":t.lineColor,"--tb-team-width":t.teamWidth,"--tb-line-width":t.lineWidth,"--tb-border-winner":t.borderWin,"--tb-opacity-winner":t.opacityWin,"--tb-bg-winner":t.bgWin,"--tb-border-loser":t.borderLos,"--tb-opacity-loser":t.opacityLos,"--tb-bg-loser":t.bgLos}:{}}function w(t,n,i="angled",r=24){switch(i){case"curved":{const o=t.x+(n.x-t.x)/2;return`M ${t.x} ${t.y} C ${o} ${t.y}, ${o} ${n.y}, ${n.x} ${n.y}`}case"straight":return`M ${t.x} ${t.y} L ${n.x} ${n.y}`;default:return`M ${t.x} ${t.y} H ${t.x+r} V ${n.y} H ${n.x}`}}function S({rounds:t,theme:n,className:i}){const r=u.useRef(null),o=u.useRef({}),[g,W]=u.useState([]);function B(c,l){const s=c.getBoundingClientRect(),d=l.getBoundingClientRect();return{x:s.right-d.left,y:s.top-d.top+s.height/2}}return u.useLayoutEffect(()=>{if(!r.current)return;const c=r.current,l=[],s=n?.lineStyle??"angled";t.forEach((d,f)=>{d.forEach((h,m)=>{const y=o.current[h.id];if(!y)return;const C=y.querySelector(".team:nth-child(1)"),M=y.querySelector(".team:nth-child(2)");if(f===t.length-1&&d.length===1||!C||!M)return;const p=B(C,c),x=B(M,c);l.push(w(p,x,s));const T={x:(p.x+x.x)/2,y:(p.y+x.y)/2};if(f<t.length-1){const L=t[f+1][Math.floor(m/2)],k=o.current[L?.id];if(!k)return;const b=k.getBoundingClientRect(),v=c.getBoundingClientRect(),j={x:b.left-v.left,y:b.top-v.top+b.height/2},F={x:T.x+24,y:T.y};l.push(w(F,j,s))}})}),W(l)},[t,n]),e.jsxs("div",{className:`tournament-bracket bracket ${i??""}`,style:E(n),ref:r,children:[e.jsx("svg",{className:"lines-layer",children:g.map((c,l)=>e.jsx("path",{d:c,stroke:n?.lineColor,fill:"none",strokeWidth:n?.lineWidth??2},l))}),t.map((c,l)=>e.jsx("div",{className:"round-column",children:c.map((s,d)=>{const f=d===t.length-1&&c.length===1;return e.jsx("div",{ref:h=>{o.current[s.id]=h},children:e.jsx(N,{match:s,isFinal:f})},s.id)})},l))]})}a.TournamentBracket=S,Object.defineProperty(a,Symbol.toStringTag,{value:"Module"})}));
@@ -0,0 +1,103 @@
1
+ .tournament-bracket {
2
+ --tb-bg-team: #0f172a;
3
+ --tb-border: #334155;
4
+ --tb-stroke: #38bdf8;
5
+ --tb-smooth: round;
6
+ --tb-team-width: 260px;
7
+ --tb-gap-rounds: 80px;
8
+ --tb-gap-matches: 20px;
9
+ --tb-line-width: 2px;
10
+ --tb-border-winner: #2ecc71;
11
+ --tb-opacity-winner: 1;
12
+ --tb-bg-winner: #0f172a;
13
+ --tb-border-loser: #e74c3c;
14
+ --tb-opacity-loser: 0.7;
15
+ --tb-bg-loser: #0f172a;
16
+ }
17
+
18
+ .bracket {
19
+ display: flex;
20
+ flex-direction: row;
21
+ position: relative;
22
+ gap: 120px;
23
+ padding: 40px;
24
+ }
25
+
26
+ .round-column {
27
+ display: flex;
28
+ flex-direction: column;
29
+ align-items: center;
30
+ justify-content: space-around;
31
+ gap: 20px;
32
+ }
33
+
34
+ .round {
35
+ display: grid;
36
+ grid-auto-rows: minmax(120px, auto);
37
+ gap: 60px;
38
+ }
39
+
40
+ .group,
41
+ .teams
42
+ {
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: 10px;
46
+ }
47
+
48
+ .match {
49
+ position: relative;
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: 20px;
53
+ }
54
+
55
+ .team {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 10px;
59
+ width: var(--tb-team-width);
60
+ padding: 8px 12px;
61
+ background: var(--tb-bg-team);
62
+ border-radius: 10px;
63
+ border: 1px solid var(--tb-border);
64
+ }
65
+
66
+ .team img {
67
+ width: 36px;
68
+ height: 36px;
69
+ }
70
+
71
+ .team.empty {
72
+ justify-content: center;
73
+ opacity: 0.6;
74
+ border-style: dashed;
75
+ }
76
+
77
+ .lines-layer {
78
+ position: absolute;
79
+ inset: 0;
80
+ width: 100%;
81
+ height: 100%;
82
+ pointer-events: none;
83
+ }
84
+
85
+ .lines-layer path {
86
+ stroke: var(--tb-stroke);
87
+ stroke-width: var(--tb-line-width,2);
88
+ fill: none;
89
+ stroke-linejoin: var(--tb-smooth);
90
+ stroke-linecap: var(--tb-smooth);
91
+ }
92
+
93
+ .team.winner {
94
+ border-color: var(--tb-border-winner);
95
+ background: var(--tb-bg-winner);
96
+ opacity: var(--tb-opacity-winner);
97
+ }
98
+
99
+ .team.loser {
100
+ border-color: var(--tb-border-loser);
101
+ background: var(--tb-bg-loser);
102
+ opacity: var(--tb-opacity-loser);
103
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "tournaments-bracket",
3
+ "version": "2.0.4",
4
+ "type": "module",
5
+
6
+ "main": "./dist/index.umd.js",
7
+ "module": "./dist/index.es.js",
8
+ "types": "./dist/index.d.ts",
9
+
10
+ "files": ["dist"],
11
+ "scripts": {
12
+ "build": "vite build && tsc -b tsconfig.cli.json"
13
+ },
14
+ "bin": {
15
+ "tournaments-bracket-generate": "./dist/cli/generate.js"
16
+ },
17
+ "peerDependencies": {
18
+ "react": "^19.2.3",
19
+ "react-dom": "^19.2.3"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^25.0.3",
23
+ "@types/react": "^19.2.7",
24
+ "ts-node": "^10.9.2",
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "dependencies": {
28
+ "@vitejs/plugin-react": "^5.1.2",
29
+ "vite": "^7.3.0",
30
+ "vite-plugin-dts": "^4.5.4"
31
+ }
32
+ }