tournament-brackets-ui 0.1.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 (268) hide show
  1. package/.storybook/main.js +23 -0
  2. package/.storybook/preview.js +13 -0
  3. package/README.md +70 -0
  4. package/README.old.md +1 -0
  5. package/commands.txt +41 -0
  6. package/dist/esm/CollapsedLeft-B5z4pKZL.js +1042 -0
  7. package/dist/esm/CollapsedLeft-B5z4pKZL.js.map +1 -0
  8. package/dist/esm/CollapsedLeft-B6FNo8Dp.js +727 -0
  9. package/dist/esm/CollapsedLeft-B6FNo8Dp.js.map +1 -0
  10. package/dist/esm/CollapsedLeft-BB1dXff3.js +1215 -0
  11. package/dist/esm/CollapsedLeft-BB1dXff3.js.map +1 -0
  12. package/dist/esm/CollapsedLeft-BMZwFt9m.js +879 -0
  13. package/dist/esm/CollapsedLeft-BMZwFt9m.js.map +1 -0
  14. package/dist/esm/CollapsedLeft-BYfnV5-9.js +1228 -0
  15. package/dist/esm/CollapsedLeft-BYfnV5-9.js.map +1 -0
  16. package/dist/esm/CollapsedLeft-C3BOW8Vh.js +977 -0
  17. package/dist/esm/CollapsedLeft-C3BOW8Vh.js.map +1 -0
  18. package/dist/esm/CollapsedLeft-CBPTeMn9.js +922 -0
  19. package/dist/esm/CollapsedLeft-CBPTeMn9.js.map +1 -0
  20. package/dist/esm/CollapsedLeft-CNXxlyEt.js +887 -0
  21. package/dist/esm/CollapsedLeft-CNXxlyEt.js.map +1 -0
  22. package/dist/esm/CollapsedLeft-DN7qy7MO.js +961 -0
  23. package/dist/esm/CollapsedLeft-DN7qy7MO.js.map +1 -0
  24. package/dist/esm/CollapsedLeft-DO5DyVjM.js +870 -0
  25. package/dist/esm/CollapsedLeft-DO5DyVjM.js.map +1 -0
  26. package/dist/esm/CollapsedLeft-DP4ftgM6.js +796 -0
  27. package/dist/esm/CollapsedLeft-DP4ftgM6.js.map +1 -0
  28. package/dist/esm/CollapsedLeft-DQO8oQL_.js +800 -0
  29. package/dist/esm/CollapsedLeft-DQO8oQL_.js.map +1 -0
  30. package/dist/esm/CollapsedLeft-DXyWTNAC.js +1047 -0
  31. package/dist/esm/CollapsedLeft-DXyWTNAC.js.map +1 -0
  32. package/dist/esm/CollapsedLeft-DZGhG_-q.js +728 -0
  33. package/dist/esm/CollapsedLeft-DZGhG_-q.js.map +1 -0
  34. package/dist/esm/CollapsedLeft-bAhAurv-.js +908 -0
  35. package/dist/esm/CollapsedLeft-bAhAurv-.js.map +1 -0
  36. package/dist/esm/CollapsedLeft-hF26CY1G.js +1003 -0
  37. package/dist/esm/CollapsedLeft-hF26CY1G.js.map +1 -0
  38. package/dist/esm/CollapsedRight-1CTx2ozt.js +860 -0
  39. package/dist/esm/CollapsedRight-1CTx2ozt.js.map +1 -0
  40. package/dist/esm/CollapsedRight-2mcguPWm.js +715 -0
  41. package/dist/esm/CollapsedRight-2mcguPWm.js.map +1 -0
  42. package/dist/esm/CollapsedRight-BCLyZXf2.js +863 -0
  43. package/dist/esm/CollapsedRight-BCLyZXf2.js.map +1 -0
  44. package/dist/esm/CollapsedRight-C13Vq_e3.js +1293 -0
  45. package/dist/esm/CollapsedRight-C13Vq_e3.js.map +1 -0
  46. package/dist/esm/CollapsedRight-C3-pe8RB.js +799 -0
  47. package/dist/esm/CollapsedRight-C3-pe8RB.js.map +1 -0
  48. package/dist/esm/CollapsedRight-CPLDkLx5.js +678 -0
  49. package/dist/esm/CollapsedRight-CPLDkLx5.js.map +1 -0
  50. package/dist/esm/CollapsedRight-CdwTany1.js +1388 -0
  51. package/dist/esm/CollapsedRight-CdwTany1.js.map +1 -0
  52. package/dist/esm/CollapsedRight-DGOaJHqo.js +1123 -0
  53. package/dist/esm/CollapsedRight-DGOaJHqo.js.map +1 -0
  54. package/dist/esm/CollapsedRight-DH1HTBg_.js +1034 -0
  55. package/dist/esm/CollapsedRight-DH1HTBg_.js.map +1 -0
  56. package/dist/esm/CollapsedRight-D_0End0L.js +1013 -0
  57. package/dist/esm/CollapsedRight-D_0End0L.js.map +1 -0
  58. package/dist/esm/CollapsedRight-Da0UMNrq.js +1547 -0
  59. package/dist/esm/CollapsedRight-Da0UMNrq.js.map +1 -0
  60. package/dist/esm/CollapsedRight-DjoLogzE.js +916 -0
  61. package/dist/esm/CollapsedRight-DjoLogzE.js.map +1 -0
  62. package/dist/esm/CollapsedRight-DoOykWbk.js +1219 -0
  63. package/dist/esm/CollapsedRight-DoOykWbk.js.map +1 -0
  64. package/dist/esm/CollapsedRight-RCxUtg7-.js +1140 -0
  65. package/dist/esm/CollapsedRight-RCxUtg7-.js.map +1 -0
  66. package/dist/esm/CollapsedRight-U7cf6t10.js +946 -0
  67. package/dist/esm/CollapsedRight-U7cf6t10.js.map +1 -0
  68. package/dist/esm/CollapsedRight-WXy5eWFq.js +1177 -0
  69. package/dist/esm/CollapsedRight-WXy5eWFq.js.map +1 -0
  70. package/dist/esm/Expanded-BAxI7aFR.js +1540 -0
  71. package/dist/esm/Expanded-BAxI7aFR.js.map +1 -0
  72. package/dist/esm/Expanded-BCEVV7zK.js +1572 -0
  73. package/dist/esm/Expanded-BCEVV7zK.js.map +1 -0
  74. package/dist/esm/Expanded-BJtgsFxf.js +2474 -0
  75. package/dist/esm/Expanded-BJtgsFxf.js.map +1 -0
  76. package/dist/esm/Expanded-C8Bxoixp.js +1374 -0
  77. package/dist/esm/Expanded-C8Bxoixp.js.map +1 -0
  78. package/dist/esm/Expanded-CBu5Dj05.js +1838 -0
  79. package/dist/esm/Expanded-CBu5Dj05.js.map +1 -0
  80. package/dist/esm/Expanded-CDYmOWYL.js +1589 -0
  81. package/dist/esm/Expanded-CDYmOWYL.js.map +1 -0
  82. package/dist/esm/Expanded-CUg2EMDY.js +2242 -0
  83. package/dist/esm/Expanded-CUg2EMDY.js.map +1 -0
  84. package/dist/esm/Expanded-CZX_MCio.js +1406 -0
  85. package/dist/esm/Expanded-CZX_MCio.js.map +1 -0
  86. package/dist/esm/Expanded-CmtBl1ZS.js +1626 -0
  87. package/dist/esm/Expanded-CmtBl1ZS.js.map +1 -0
  88. package/dist/esm/Expanded-Co8V5jgl.js +2024 -0
  89. package/dist/esm/Expanded-Co8V5jgl.js.map +1 -0
  90. package/dist/esm/Expanded-D2_h7YPI.js +1032 -0
  91. package/dist/esm/Expanded-D2_h7YPI.js.map +1 -0
  92. package/dist/esm/Expanded-DFP1nUBY.js +1381 -0
  93. package/dist/esm/Expanded-DFP1nUBY.js.map +1 -0
  94. package/dist/esm/Expanded-DPFndGqw.js +1118 -0
  95. package/dist/esm/Expanded-DPFndGqw.js.map +1 -0
  96. package/dist/esm/Expanded-Jrq0Bh65.js +1739 -0
  97. package/dist/esm/Expanded-Jrq0Bh65.js.map +1 -0
  98. package/dist/esm/Expanded-RJigoSw3.js +1552 -0
  99. package/dist/esm/Expanded-RJigoSw3.js.map +1 -0
  100. package/dist/esm/Expanded-vqC8qzDP.js +1388 -0
  101. package/dist/esm/Expanded-vqC8qzDP.js.map +1 -0
  102. package/dist/esm/ExpandedBase-BUpZvswi.js +88 -0
  103. package/dist/esm/ExpandedBase-BUpZvswi.js.map +1 -0
  104. package/dist/esm/NameRow-B-GWjlou.js +50 -0
  105. package/dist/esm/NameRow-B-GWjlou.js.map +1 -0
  106. package/dist/esm/Team-CLk1m4Vj.js +25 -0
  107. package/dist/esm/Team-CLk1m4Vj.js.map +1 -0
  108. package/dist/esm/TeamsBase-moB9MaNw.js +44 -0
  109. package/dist/esm/TeamsBase-moB9MaNw.js.map +1 -0
  110. package/dist/esm/index.css +1 -0
  111. package/dist/esm/index.js +112 -0
  112. package/dist/esm/index.js.map +1 -0
  113. package/dist/index.cjs +57871 -0
  114. package/dist/index.cjs.map +1 -0
  115. package/dist/index.css +1 -0
  116. package/package.json +87 -0
  117. package/public/favicon.ico +0 -0
  118. package/public/index.html +43 -0
  119. package/public/logo192.png +0 -0
  120. package/public/logo512.png +0 -0
  121. package/public/manifest.json +25 -0
  122. package/public/robots.txt +3 -0
  123. package/rollup.config.mjs +41 -0
  124. package/src/allocatePlayers.js +235 -0
  125. package/src/components/Facades/CollapsedLeft.jsx +26 -0
  126. package/src/components/Facades/CollapsedRight.jsx +27 -0
  127. package/src/components/Facades/Expanded.jsx +27 -0
  128. package/src/components/Individuals/08-Player/CollapsedLeft.jsx +340 -0
  129. package/src/components/Individuals/08-Player/CollapsedRight.jsx +276 -0
  130. package/src/components/Individuals/08-Player/Expanded.jsx +407 -0
  131. package/src/components/Individuals/09-Player/CollapsedLeft.jsx +386 -0
  132. package/src/components/Individuals/09-Player/CollapsedRight.jsx +343 -0
  133. package/src/components/Individuals/09-Player/Expanded.jsx +614 -0
  134. package/src/components/Individuals/10-Player/CollapsedLeft.jsx +315 -0
  135. package/src/components/Individuals/10-Player/CollapsedRight.jsx +367 -0
  136. package/src/components/Individuals/10-Player/Expanded.jsx +611 -0
  137. package/src/components/Individuals/11-Player/CollapsedLeft.jsx +352 -0
  138. package/src/components/Individuals/11-Player/CollapsedRight.jsx +412 -0
  139. package/src/components/Individuals/11-Player/Expanded.jsx +616 -0
  140. package/src/components/Individuals/12-Player/CollapsedLeft.jsx +396 -0
  141. package/src/components/Individuals/12-Player/CollapsedRight.jsx +451 -0
  142. package/src/components/Individuals/12-Player/Expanded.jsx +622 -0
  143. package/src/components/Individuals/13-Player/CollapsedLeft.jsx +426 -0
  144. package/src/components/Individuals/13-Player/CollapsedRight.jsx +490 -0
  145. package/src/components/Individuals/13-Player/Expanded.jsx +701 -0
  146. package/src/components/Individuals/14-Player/CollapsedLeft.jsx +458 -0
  147. package/src/components/Individuals/14-Player/CollapsedRight.jsx +509 -0
  148. package/src/components/Individuals/14-Player/Expanded.jsx +801 -0
  149. package/src/components/Individuals/16-Player/CollapsedLeft.jsx +514 -0
  150. package/src/components/Individuals/16-Player/CollapsedRight.jsx +608 -0
  151. package/src/components/Individuals/16-Player/Expanded.jsx +963 -0
  152. package/src/components/Individuals/stylesheet.module.css +360 -0
  153. package/src/components/NameRow.jsx +21 -0
  154. package/src/components/NameRowFillable.jsx +30 -0
  155. package/src/components/ScoreRow.jsx +29 -0
  156. package/src/components/ScoreRowFillable.jsx +64 -0
  157. package/src/components/Team.jsx +20 -0
  158. package/src/components/TeamFillable.jsx +25 -0
  159. package/src/components/Teams/08-Team/CollapsedLeft.jsx +304 -0
  160. package/src/components/Teams/08-Team/CollapsedRight.jsx +232 -0
  161. package/src/components/Teams/08-Team/Expanded.jsx +356 -0
  162. package/src/components/Teams/09-Team/CollapsedLeft.jsx +335 -0
  163. package/src/components/Teams/09-Team/CollapsedRight.jsx +282 -0
  164. package/src/components/Teams/09-Team/Expanded.jsx +486 -0
  165. package/src/components/Teams/10-Team/CollapsedLeft.jsx +269 -0
  166. package/src/components/Teams/10-Team/CollapsedRight.jsx +304 -0
  167. package/src/components/Teams/10-Team/Expanded.jsx +483 -0
  168. package/src/components/Teams/11-Team/CollapsedLeft.jsx +292 -0
  169. package/src/components/Teams/11-Team/CollapsedRight.jsx +335 -0
  170. package/src/components/Teams/11-Team/Expanded.jsx +491 -0
  171. package/src/components/Teams/12-Team/CollapsedLeft.jsx +328 -0
  172. package/src/components/Teams/12-Team/CollapsedRight.jsx +363 -0
  173. package/src/components/Teams/12-Team/Expanded.jsx +497 -0
  174. package/src/components/Teams/13-Team/CollapsedLeft.jsx +355 -0
  175. package/src/components/Teams/13-Team/CollapsedRight.jsx +399 -0
  176. package/src/components/Teams/13-Team/Expanded.jsx +569 -0
  177. package/src/components/Teams/14-Team/CollapsedLeft.jsx +382 -0
  178. package/src/components/Teams/14-Team/CollapsedRight.jsx +420 -0
  179. package/src/components/Teams/14-Team/Expanded.jsx +661 -0
  180. package/src/components/Teams/16-Team/CollapsedLeft.jsx +437 -0
  181. package/src/components/Teams/16-Team/CollapsedRight.jsx +496 -0
  182. package/src/components/Teams/16-Team/Expanded.jsx +795 -0
  183. package/src/components/Teams/stylesheet.module.css +300 -0
  184. package/src/components/_internal/ExpandedBase.jsx +60 -0
  185. package/src/components/_internal/TeamsBase.jsx +45 -0
  186. package/src/components/_internal/VariantFacade.jsx +26 -0
  187. package/src/index.js +3 -0
  188. package/src/scripts/convert-html-to-jsx.js +46 -0
  189. package/src/scripts/inject-players.js +45 -0
  190. package/src/scripts/scoreUtils.js +25 -0
  191. package/src/scripts/update-classnames.js +26 -0
  192. package/src/seeding.js +488 -0
  193. package/src/stories/Button.jsx +39 -0
  194. package/src/stories/Button.stories.js +49 -0
  195. package/src/stories/Configure.mdx +364 -0
  196. package/src/stories/Header.jsx +56 -0
  197. package/src/stories/Header.stories.js +29 -0
  198. package/src/stories/Individuals/08-Player/CollapsedLeft.stories.jsx +30 -0
  199. package/src/stories/Individuals/08-Player/CollapsedRight.stories.jsx +30 -0
  200. package/src/stories/Individuals/08-Player/Expanded.stories.jsx +30 -0
  201. package/src/stories/Individuals/09-Player/CollapsedLeft.stories.jsx +30 -0
  202. package/src/stories/Individuals/09-Player/CollapsedRight.stories.jsx +30 -0
  203. package/src/stories/Individuals/09-Player/Expanded.stories.jsx +30 -0
  204. package/src/stories/Individuals/10-Player/CollapsedLeft.stories.jsx +30 -0
  205. package/src/stories/Individuals/10-Player/CollapsedRight.stories.jsx +30 -0
  206. package/src/stories/Individuals/10-Player/Expanded.stories.jsx +30 -0
  207. package/src/stories/Individuals/11-Player/CollapsedLeft.stories.jsx +30 -0
  208. package/src/stories/Individuals/11-Player/CollapsedRight.stories.jsx +30 -0
  209. package/src/stories/Individuals/11-Player/Expanded.stories.jsx +30 -0
  210. package/src/stories/Individuals/12-Player/CollapsedLeft.stories.jsx +30 -0
  211. package/src/stories/Individuals/12-Player/CollapsedRight.stories.jsx +30 -0
  212. package/src/stories/Individuals/12-Player/Expanded.stories.jsx +30 -0
  213. package/src/stories/Individuals/13-Player/CollapsedLeft.stories.jsx +30 -0
  214. package/src/stories/Individuals/13-Player/CollapsedRight.stories.jsx +30 -0
  215. package/src/stories/Individuals/13-Player/Expanded.stories.jsx +30 -0
  216. package/src/stories/Individuals/14-Player/CollapsedLeft.stories.jsx +30 -0
  217. package/src/stories/Individuals/14-Player/CollapsedRight.stories.jsx +30 -0
  218. package/src/stories/Individuals/14-Player/Expanded.stories.jsx +30 -0
  219. package/src/stories/Individuals/16-Player/CollapsedLeft.stories.jsx +30 -0
  220. package/src/stories/Individuals/16-Player/CollapsedRight.stories.jsx +30 -0
  221. package/src/stories/Individuals/16-Player/Expanded.stories.jsx +30 -0
  222. package/src/stories/Page.jsx +69 -0
  223. package/src/stories/Page.stories.js +28 -0
  224. package/src/stories/Teams/08-Team/CollapsedLeft.stories.jsx +33 -0
  225. package/src/stories/Teams/08-Team/CollapsedRight.stories.jsx +33 -0
  226. package/src/stories/Teams/08-Team/Expanded.stories.jsx +33 -0
  227. package/src/stories/Teams/09-Team/CollapsedLeft.stories.jsx +33 -0
  228. package/src/stories/Teams/09-Team/CollapsedRight.stories.jsx +33 -0
  229. package/src/stories/Teams/09-Team/Expanded.stories.jsx +33 -0
  230. package/src/stories/Teams/10-Team/CollapsedLeft.stories.jsx +33 -0
  231. package/src/stories/Teams/10-Team/CollapsedRight.stories.jsx +33 -0
  232. package/src/stories/Teams/10-Team/Expanded.stories.jsx +33 -0
  233. package/src/stories/Teams/11-Team/CollapsedLeft.stories.jsx +33 -0
  234. package/src/stories/Teams/11-Team/CollapsedRight.stories.jsx +33 -0
  235. package/src/stories/Teams/11-Team/Expanded.stories.jsx +33 -0
  236. package/src/stories/Teams/12-Team/CollapsedLeft.stories.jsx +33 -0
  237. package/src/stories/Teams/12-Team/CollapsedRight.stories.jsx +33 -0
  238. package/src/stories/Teams/12-Team/Expanded.stories.jsx +33 -0
  239. package/src/stories/Teams/13-Team/CollapsedLeft.stories.jsx +33 -0
  240. package/src/stories/Teams/13-Team/CollapsedRight.stories.jsx +33 -0
  241. package/src/stories/Teams/13-Team/Expanded.stories.jsx +33 -0
  242. package/src/stories/Teams/14-Team/CollapsedLeft.stories.jsx +33 -0
  243. package/src/stories/Teams/14-Team/CollapsedRight.stories.jsx +33 -0
  244. package/src/stories/Teams/14-Team/Expanded.stories.jsx +33 -0
  245. package/src/stories/Teams/16-Team/CollapsedLeft.stories.jsx +33 -0
  246. package/src/stories/Teams/16-Team/CollapsedRight.stories.jsx +33 -0
  247. package/src/stories/Teams/16-Team/Expanded.stories.jsx +33 -0
  248. package/src/stories/assets/accessibility.png +0 -0
  249. package/src/stories/assets/accessibility.svg +1 -0
  250. package/src/stories/assets/addon-library.png +0 -0
  251. package/src/stories/assets/assets.png +0 -0
  252. package/src/stories/assets/avif-test-image.avif +0 -0
  253. package/src/stories/assets/context.png +0 -0
  254. package/src/stories/assets/discord.svg +1 -0
  255. package/src/stories/assets/docs.png +0 -0
  256. package/src/stories/assets/figma-plugin.png +0 -0
  257. package/src/stories/assets/github.svg +1 -0
  258. package/src/stories/assets/share.png +0 -0
  259. package/src/stories/assets/styling.png +0 -0
  260. package/src/stories/assets/testing.png +0 -0
  261. package/src/stories/assets/theming.png +0 -0
  262. package/src/stories/assets/tutorials.svg +1 -0
  263. package/src/stories/assets/youtube.svg +1 -0
  264. package/src/stories/button.css +30 -0
  265. package/src/stories/data/players.js +116 -0
  266. package/src/stories/data/teams.js +32 -0
  267. package/src/stories/header.css +32 -0
  268. package/src/stories/page.css +68 -0
package/src/seeding.js ADDED
@@ -0,0 +1,488 @@
1
+ // ---- 1) Static config ----
2
+ export const BRACKET_BYES = {
3
+ 8: 0,
4
+ 9: 1,
5
+ 10: 6,
6
+ 11: 5,
7
+ 12: 4,
8
+ 13: 3,
9
+ 14: 2,
10
+ 16: 0,
11
+ };
12
+
13
+ const ALLOWED_SIZES = [8, 9, 10, 11, 12, 13, 14, 16];
14
+ const MIN = ALLOWED_SIZES[0];
15
+ const MAX = ALLOWED_SIZES[ALLOWED_SIZES.length - 1];
16
+
17
+ // ---- 2) Pick bracket sizes (same logic) ----
18
+ export function pickBracketSizes(N) {
19
+ let count = 1;
20
+ while (count * MAX < N) count <<= 1;
21
+
22
+ if (N <= MIN * count) {
23
+ return Array(count).fill(MIN);
24
+ }
25
+
26
+ const base = Math.floor(N / count);
27
+ const sizes = Array(count).fill(Math.max(MIN, Math.min(MAX, base)));
28
+ let extra = N - sizes.reduce((sum, s) => sum + s, 0);
29
+
30
+ // round-robin distribute the extra slots
31
+ for (let i = 0; extra > 0; i = (i + 1) % count) {
32
+ if (sizes[i] < MAX) {
33
+ sizes[i]++;
34
+ extra--;
35
+ }
36
+ }
37
+ return sizes;
38
+ }
39
+
40
+ // ---- 3) Utilities ----
41
+ function byeCountForSize(size) {
42
+ return BRACKET_BYES[size] ?? 0;
43
+ }
44
+
45
+ function neighborsOf(i, total) {
46
+ const left = (i - 1 + total) % total;
47
+ const right = (i + 1) % total;
48
+ return [left, right];
49
+ }
50
+
51
+ function countByClub(players) {
52
+ const m = new Map();
53
+ for (const p of players) {
54
+ m.set(p.club, (m.get(p.club) || 0) + 1);
55
+ }
56
+ return m;
57
+ }
58
+
59
+ function sumByesForSizes(sizes) {
60
+ return sizes.reduce((acc, sz) => acc + byeCountForSize(sz), 0);
61
+ }
62
+
63
+ // Largest remainder apportionment for bye quotas: proportional fairness
64
+ function computeByeQuotaByClub(players, sizes) {
65
+ const totalByes = sumByesForSizes(sizes);
66
+ const totals = countByClub(players);
67
+ const N = players.length;
68
+
69
+ const entries = [...totals.entries()].map(([club, cnt]) => {
70
+ const exact = (totalByes * cnt) / N;
71
+ const floor = Math.floor(exact);
72
+ const remainder = exact - floor;
73
+ return { club, floor, remainder };
74
+ });
75
+
76
+ let allocated = entries.reduce((s, e) => s + e.floor, 0);
77
+ const remaining = totalByes - allocated;
78
+
79
+ entries.sort((a, b) => b.remainder - a.remainder); // biggest remainder first
80
+ for (let i = 0; i < remaining; i++) entries[i].floor += 1;
81
+
82
+ const quota = new Map();
83
+ for (const e of entries) quota.set(e.club, e.floor);
84
+ return quota; // Map<club, targetByesForTournament>
85
+ }
86
+
87
+ // ---- 5) Strong-player aware + club-balanced group distribution ----
88
+ // - Place strong first (avoid neighbor same-club strong).
89
+ // - Then place others, preferring the group that has the FEWEST of that club so far.
90
+ function distributeIntoGroups(players, sizes) {
91
+ const bracketCount = sizes.length;
92
+
93
+ const brackets = sizes.map((sz, i) => ({
94
+ group: i + 1,
95
+ size: sz,
96
+ players: [],
97
+ }));
98
+ const capacityLeft = sizes.map((s) => s);
99
+
100
+ const strong = players.filter((p) => p.bias);
101
+ const others = players.filter((p) => !p.bias);
102
+
103
+ // Track strong clubs per group for neighbor rule
104
+ const strongInGroupClub = Array.from({ length: bracketCount }, () => new Set());
105
+
106
+ // Track club counts per group for balancing
107
+ const clubCountInGroup = Array.from({ length: bracketCount }, () => new Map());
108
+
109
+ const incClub = (g, club) => {
110
+ const m = clubCountInGroup[g];
111
+ m.set(club, (m.get(club) || 0) + 1);
112
+ };
113
+
114
+ // Place strong first with scoring: capacity, no neighbor same-club strong, fewer club counts
115
+ for (const p of strong) {
116
+ let best = -1;
117
+ let bestScore = -Infinity;
118
+
119
+ for (let g = 0; g < bracketCount; g++) {
120
+ if (capacityLeft[g] <= 0) continue;
121
+
122
+ const neigh = neighborsOf(g, bracketCount);
123
+ const neighborConflict = neigh.some((ng) => strongInGroupClub[ng].has(p.club)) ? 1 : 0;
124
+ const sameGroupConflict = strongInGroupClub[g].has(p.club) ? 1 : 0;
125
+ const clubLoad = (clubCountInGroup[g].get(p.club) || 0);
126
+
127
+ // Heuristic score:
128
+ // +100 * capacity
129
+ // -20 if neighbor has same-club strong
130
+ // -10 if same group already has same-club strong
131
+ // -3 per existing members of this club in the group
132
+ const score =
133
+ capacityLeft[g] * 100 - neighborConflict * 20 - sameGroupConflict * 10 - clubLoad * 3;
134
+
135
+ if (score > bestScore) {
136
+ bestScore = score;
137
+ best = g;
138
+ }
139
+ }
140
+
141
+ if (best !== -1) {
142
+ brackets[best].players.push(p);
143
+ capacityLeft[best]--;
144
+ strongInGroupClub[best].add(p.club);
145
+ incClub(best, p.club);
146
+ }
147
+ }
148
+
149
+ // Place others: choose group with lowest club load, tie-break by capacity
150
+ for (const p of others) {
151
+ let best = -1;
152
+ let bestScore = -Infinity;
153
+
154
+ for (let g = 0; g < bracketCount; g++) {
155
+ if (capacityLeft[g] <= 0) continue;
156
+ const clubLoad = (clubCountInGroup[g].get(p.club) || 0);
157
+ const score = -clubLoad * 100 + capacityLeft[g]; // prefer groups with fewer of this club
158
+ if (score > bestScore) {
159
+ bestScore = score;
160
+ best = g;
161
+ }
162
+ }
163
+
164
+ if (best !== -1) {
165
+ brackets[best].players.push(p);
166
+ capacityLeft[best]--;
167
+ incClub(best, p.club);
168
+ }
169
+ }
170
+
171
+ return brackets;
172
+ }
173
+
174
+ // ---- 6) In-bracket ordering with fair byes + constraints (no nulls) ----
175
+ function orderBracket(players, size, byeAssignedPerClub, byeQuotaByClub) {
176
+ const byeCount = byeCountForSize(size);
177
+ const byeStart = size - byeCount; // indexes [byeStart .. size-1] are bye positions
178
+
179
+ const strong = players.filter((p) => p.bias);
180
+ const nonStrong = players.filter((p) => !p.bias);
181
+
182
+ // Helper: can this club still receive a bye (global fairness)?
183
+ const canClubTakeMoreByes = (club) => {
184
+ const used = byeAssignedPerClub.get(club) || 0;
185
+ const quota = byeQuotaByClub.get(club) || 0;
186
+ return used < quota;
187
+ };
188
+
189
+ // Sort candidates by "need" = (assigned/quota) ascending, then by frequency (optional)
190
+ const needScore = (club) => {
191
+ const used = byeAssignedPerClub.get(club) || 0;
192
+ const quota = byeQuotaByClub.get(club) || 0;
193
+ // If quota is zero, treat as high needScore so it's picked only if forced later
194
+ return quota > 0 ? used / quota : 1e9;
195
+ };
196
+
197
+ // We'll avoid giving multiple byes in the SAME bracket to the SAME club if alternatives exist
198
+ const bracketByeClubs = new Set();
199
+
200
+ // Pick K players for bye slots using fairness:
201
+ const byePicks = [];
202
+
203
+ // 6a) Prefer strong players that can still take byes (respect quotas)
204
+ const strongEligible = strong
205
+ .filter((p) => canClubTakeMoreByes(p.club))
206
+ .sort((a, b) => needScore(a.club) - needScore(b.club));
207
+
208
+ for (const s of strongEligible) {
209
+ if (byePicks.length >= byeCount) break;
210
+ // avoid duplicate club in this bracket bye region if possible
211
+ if (!bracketByeClubs.has(s.club)) {
212
+ byePicks.push(s);
213
+ bracketByeClubs.add(s.club);
214
+ byeAssignedPerClub.set(s.club, (byeAssignedPerClub.get(s.club) || 0) + 1);
215
+ }
216
+ }
217
+
218
+ // 6b) If not enough, allow remaining strong regardless of quota (but still try to avoid duplicates)
219
+ if (byePicks.length < byeCount) {
220
+ const strongFallback = strong
221
+ .filter((p) => !byePicks.includes(p))
222
+ .sort((a, b) => needScore(a.club) - needScore(b.club));
223
+ for (const s of strongFallback) {
224
+ if (byePicks.length >= byeCount) break;
225
+ if (!bracketByeClubs.has(s.club)) {
226
+ byePicks.push(s);
227
+ bracketByeClubs.add(s.club);
228
+ // Only increment assigned if they were actually under quota
229
+ if (canClubTakeMoreByes(s.club)) {
230
+ byeAssignedPerClub.set(s.club, (byeAssignedPerClub.get(s.club) || 0) + 1);
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ // 6c) Still not enough? Fill with non-strong fairly
237
+ if (byePicks.length < byeCount) {
238
+ const nonStrongEligible = nonStrong
239
+ .filter((p) => canClubTakeMoreByes(p.club))
240
+ .sort((a, b) => needScore(a.club) - needScore(b.club));
241
+ for (const p of nonStrongEligible) {
242
+ if (byePicks.length >= byeCount) break;
243
+ if (!bracketByeClubs.has(p.club)) {
244
+ byePicks.push(p);
245
+ bracketByeClubs.add(p.club);
246
+ byeAssignedPerClub.set(p.club, (byeAssignedPerClub.get(p.club) || 0) + 1);
247
+ }
248
+ }
249
+ }
250
+
251
+ // 6d) If STILL short (tight quotas), take anyone left (minimize duplicates if possible)
252
+ if (byePicks.length < byeCount) {
253
+ const rest = players.filter((p) => !byePicks.includes(p));
254
+ // prefer clubs not yet present in this bye region
255
+ rest.sort((a, b) => {
256
+ const aDup = bracketByeClubs.has(a.club) ? 1 : 0;
257
+ const bDup = bracketByeClubs.has(b.club) ? 1 : 0;
258
+ if (aDup !== bDup) return aDup - bDup;
259
+ return needScore(a.club) - needScore(b.club);
260
+ });
261
+ for (const p of rest) {
262
+ if (byePicks.length >= byeCount) break;
263
+ byePicks.push(p);
264
+ bracketByeClubs.add(p.club);
265
+ if (canClubTakeMoreByes(p.club)) {
266
+ byeAssignedPerClub.set(p.club, (byeAssignedPerClub.get(p.club) || 0) + 1);
267
+ }
268
+ }
269
+ }
270
+
271
+ // Build slots: players only, no nulls; put bye picks into tail indexes
272
+ const slots = Array(size);
273
+ const byeSet = new Set(byePicks);
274
+ let tail = byeStart;
275
+ for (const p of byePicks) {
276
+ slots[tail++] = p;
277
+ }
278
+
279
+ // Remaining players
280
+ const remaining = players.filter((p) => !byeSet.has(p));
281
+
282
+ // Place remaining strong spaced in non-bye region
283
+ const nonByeLen = size - byeCount;
284
+ const spreadOrder = (() => {
285
+ const mid = Math.floor(nonByeLen / 2);
286
+ const order = [];
287
+ let l = 0, r = mid;
288
+ while (order.length < nonByeLen) {
289
+ if (l < mid) order.push(l++);
290
+ if (r < nonByeLen) order.push(r++);
291
+ }
292
+ return order;
293
+ })();
294
+
295
+ // First, place remaining strong on spread positions
296
+ const remainingStrong = remaining.filter((p) => p.bias);
297
+ let so = 0;
298
+ for (const s of remainingStrong) {
299
+ while (so < spreadOrder.length && slots[spreadOrder[so]]) so++;
300
+ if (so >= spreadOrder.length) break;
301
+ slots[spreadOrder[so]] = s;
302
+ so++;
303
+ }
304
+
305
+ // Then fill rest with non-strong avoiding first-round same-club if possible
306
+ const pool = remaining.filter((p) => !p.bias);
307
+
308
+ function pickNotClub(club) {
309
+ if (!club) return pool.shift() || null;
310
+ const idx = pool.findIndex((p) => p.club !== club);
311
+ if (idx === -1) return pool.shift() || null;
312
+ return pool.splice(idx, 1)[0] || null;
313
+ }
314
+
315
+ for (let i = 0; i < nonByeLen; i += 2) {
316
+ const a = i, b = i + 1;
317
+ const clubA = slots[a]?.club ?? null;
318
+ const clubB = slots[b]?.club ?? null;
319
+
320
+ const fill = (idx, avoidClub) => {
321
+ if (slots[idx]) return;
322
+ const pick = pickNotClub(avoidClub);
323
+ if (pick) slots[idx] = pick;
324
+ };
325
+
326
+ if (clubA && !clubB) {
327
+ fill(b, clubA);
328
+ } else if (!clubA && clubB) {
329
+ fill(a, clubB);
330
+ } else if (!clubA && !clubB) {
331
+ const first = pickNotClub(null);
332
+ if (first) slots[a] = first;
333
+ const second = pickNotClub(first ? first.club : null);
334
+ if (second) slots[b] = second;
335
+ }
336
+ }
337
+
338
+ // Any leftover holes in non-bye region (rare): fill sequentially
339
+ for (let i = 0; i < nonByeLen && pool.length > 0; i++) {
340
+ if (!slots[i]) slots[i] = pool.shift();
341
+ }
342
+
343
+ // Sanity: fill any remaining empties (shouldn’t happen)
344
+ const leftovers = remaining.filter((p) => !slots.includes(p) && !byeSet.has(p));
345
+ for (let i = 0; i < size && leftovers.length > 0; i++) {
346
+ if (!slots[i]) slots[i] = leftovers.shift();
347
+ }
348
+
349
+ return slots;
350
+ }
351
+
352
+ // ---- 7) Main entry ----
353
+ export function assignBrackets(players) {
354
+ const sizes = pickBracketSizes(players.length);
355
+
356
+ // (A) Fair bye quota per club across the whole tournament
357
+ const byeQuotaByClub = computeByeQuotaByClub(players, sizes); // Map<club, quota>
358
+ const byeAssignedPerClub = new Map(); // Map<club, used>
359
+
360
+ // (B) Place players into groups with strong + club balancing
361
+ const grouped = distributeIntoGroups(players, sizes);
362
+
363
+ // (C) Order inside each bracket; assign byes fairly; no nulls
364
+ for (const br of grouped) {
365
+ br.players = orderBracket(br.players, br.size, byeAssignedPerClub, byeQuotaByClub);
366
+ }
367
+
368
+ return grouped;
369
+ }
370
+
371
+ /*
372
+ Bye quotas (global fairness): We compute how many byes each club should get overall, proportional to that club’s population, and then we respect that quota when assigning byes in each bracket. This prevents one big club from hogging all byes.
373
+
374
+ No duplicate club byes in the same bracket (if alternatives exist): inside a bracket’s bye region we avoid giving multiple byes to the same club to keep it fair round-by-round.
375
+
376
+ Club-balanced grouping: when placing players into groups, we always pick the group that currently has the fewest of that player’s club (subject to capacity), which evens out club distribution across all groups.
377
+ */
378
+
379
+
380
+ const divisionAPlayers = [
381
+ { id: "P001", name: "Amina Yusuf", club: "TKC", bias: false },
382
+ { id: "P002", name: "Hiroki Tanaka", club: "TKC", bias: false },
383
+ { id: "P003", name: "Lucas Oliveira", club: "TKC", bias: true },
384
+ { id: "P004", name: "Priya Singh", club: "TKC", bias: false },
385
+ { id: "P005", name: "Li Wei", club: "TKC", bias: false },
386
+ { id: "P006", name: "Jamal Thompson", club: "TKC", bias: false },
387
+ { id: "P007", name: "Sofia García", club: "TKC", bias: true },
388
+ { id: "P008", name: "Viktor Johansson", club: "TKC", bias: false },
389
+ { id: "P009", name: "Zuri Ndlovu", club: "TKC", bias: false },
390
+ { id: "P010", name: "Amara Diop", club: "TKC", bias: false },
391
+ { id: "P011", name: "Kenji Watanabe", club: "TKC", bias: false },
392
+ { id: "P012", name: "María Estévez", club: "TKC", bias: false },
393
+ { id: "P013", name: "Oskar Lindgren", club: "TKC", bias: false },
394
+ { id: "P014", name: "Aisha El-Sayed", club: "TKC", bias: false },
395
+ { id: "P015", name: "Linh Tran", club: "TKC", bias: false },
396
+ { id: "P016", name: "Diego Castillo", club: "ETO", bias: false },
397
+ { id: "P017", name: "Fatima Al-Hassan", club: "ETO", bias: false },
398
+ { id: "P018", name: "Olivia Montgomery", club: "ETO", bias: false },
399
+ { id: "P019", name: "Thiago Oliveira", club: "ETO", bias: false },
400
+ { id: "P020", name: "Zara Novak", club: "ETO", bias: false },
401
+ { id: "P021", name: "Amina Yusuf", club: "ETO", bias: false },
402
+ { id: "P022", name: "Hiroki Tanaka", club: "ETO", bias: true },
403
+ { id: "P023", name: "Lucas Oliveira", club: "ETO", bias: false },
404
+ { id: "P024", name: "Priya Singh", club: "ETO", bias: false },
405
+ { id: "P025", name: "Li Wei", club: "ETO", bias: false },
406
+ { id: "P026", name: "Jamal Thompson", club: "ETO", bias: false },
407
+ { id: "P027", name: "Sofia García", club: "ETO", bias: false },
408
+ { id: "P028", name: "Viktor Johansson", club: "UOT", bias: false },
409
+ { id: "P029", name: "Zuri Ndlovu", club: "UOT", bias: true },
410
+ { id: "P030", name: "Amara Diop", club: "UOT", bias: false },
411
+ { id: "P031", name: "Kenji Watanabe", club: "UOT", bias: false },
412
+ { id: "P032", name: "María Estévez", club: "UOT", bias: false },
413
+ { id: "P033", name: "Oskar Lindgren", club: "UOT", bias: true },
414
+ { id: "P034", name: "Aisha El-Sayed", club: "UOT", bias: false },
415
+ { id: "P035", name: "Linh Tran", club: "UOT", bias: false },
416
+ { id: "P036", name: "Diego Castillo", club: "UOT", bias: false },
417
+ { id: "P037", name: "Fatima Al-Hassan", club: "UOT", bias: false },
418
+ { id: "P038", name: "Olivia Montgomery", club: "UOT", bias: false },
419
+ { id: "P039", name: "Thiago Oliveira", club: "JCC", bias: false },
420
+ { id: "P040", name: "Zara Novak", club: "JCC", bias: false },
421
+ { id: "P041", name: "Amina Yusuf", club: "JCC", bias: false },
422
+ { id: "P042", name: "Hiroki Tanaka", club: "JCC", bias: false },
423
+ { id: "P043", name: "Lucas Oliveira", club: "JCC", bias: false },
424
+ { id: "P044", name: "Priya Singh", club: "JCC", bias: true },
425
+ { id: "P045", name: "Li Wei", club: "JCC", bias: false },
426
+ { id: "P046", name: "Jamal Thompson", club: "JCC", bias: false },
427
+ { id: "P047", name: "Sofia García", club: "JCC", bias: false },
428
+ { id: "P048", name: "Viktor Johansson", club: "JCC", bias: false },
429
+ { id: "P049", name: "Zuri Ndlovu", club: "UWA", bias: false },
430
+ { id: "P050", name: "Amara Diop", club: "UWA", bias: false },
431
+ { id: "P051", name: "Kenji Watanabe", club: "UWA", bias: false },
432
+ { id: "P052", name: "María Estévez", club: "UWA", bias: false },
433
+ { id: "P053", name: "Oskar Lindgren", club: "UWA", bias: false },
434
+ { id: "P054", name: "Aisha El-Sayed", club: "UWA", bias: false },
435
+ { id: "P055", name: "Linh Tran", club: "UWA", bias: false },
436
+ { id: "P056", name: "Diego Castillo", club: "UWA", bias: false },
437
+ { id: "P057", name: "Fatima Al-Hassan", club: "UWA", bias: false },
438
+ { id: "P058", name: "Olivia Montgomery", club: "UWA", bias: false },
439
+ { id: "P059", name: "Thiago Oliveira", club: "TMU", bias: false },
440
+ { id: "P060", name: "Zara Novak", club: "TMU", bias: false },
441
+ { id: "P061", name: "Amina Yusuf", club: "TMU", bias: false },
442
+ { id: "P062", name: "Hiroki Tanaka", club: "TMU", bias: false },
443
+ { id: "P063", name: "Lucas Oliveira", club: "TMU", bias: false },
444
+ { id: "P064", name: "Priya Singh", club: "TMU", bias: false },
445
+ { id: "P065", name: "Li Wei", club: "TMU", bias: false },
446
+ { id: "P066", name: "Jamal Thompson", club: "TMU", bias: false },
447
+ { id: "P067", name: "Sofia García", club: "TMU", bias: false },
448
+ { id: "P068", name: "Viktor Johansson", club: "MAR", bias: false },
449
+ { id: "P069", name: "Zuri Ndlovu", club: "MAR", bias: false },
450
+ { id: "P070", name: "Amara Diop", club: "MAR", bias: false },
451
+ { id: "P071", name: "Kenji Watanabe", club: "MAR", bias: false },
452
+ { id: "P072", name: "María Estévez", club: "MAR", bias: true },
453
+ { id: "P073", name: "Oskar Lindgren", club: "MAR", bias: false },
454
+ { id: "P074", name: "Aisha El-Sayed", club: "MAR", bias: false },
455
+ { id: "P075", name: "Linh Tran", club: "MAR", bias: false },
456
+ { id: "P076", name: "Diego Castillo", club: "MAR", bias: false },
457
+ { id: "P077", name: "Fatima Al-Hassan", club: "ARC", bias: false },
458
+ { id: "P078", name: "Olivia Montgomery", club: "ARC", bias: false },
459
+ { id: "P079", name: "Thiago Oliveira", club: "ARC", bias: false },
460
+ { id: "P080", name: "Zara Novak", club: "ARC", bias: false },
461
+ { id: "P081", name: "Amina Yusuf", club: "ARC", bias: false },
462
+ { id: "P082", name: "Hiroki Tanaka", club: "ARC", bias: false },
463
+ { id: "P083", name: "Lucas Oliveira", club: "ARC", bias: false },
464
+ { id: "P084", name: "Priya Singh", club: "ARC", bias: false },
465
+ { id: "P085", name: "Li Wei", club: "ARC", bias: false },
466
+ { id: "P086", name: "Jamal Thompson", club: "BLU", bias: false },
467
+ { id: "P087", name: "Sofia García", club: "BLU", bias: false },
468
+ { id: "P088", name: "Viktor Johansson", club: "BLU", bias: false },
469
+ { id: "P089", name: "Zuri Ndlovu", club: "BLU", bias: false },
470
+ { id: "P090", name: "Amara Diop", club: "BLU", bias: false },
471
+ { id: "P091", name: "Kenji Watanabe", club: "BLU", bias: false },
472
+ { id: "P092", name: "María Estévez", club: "BLU", bias: false },
473
+ { id: "P093", name: "Oskar Lindgren", club: "BLU", bias: false },
474
+ { id: "P094", name: "Aisha El-Sayed", club: "BLU", bias: false },
475
+ { id: "P095", name: "Linh Tran", club: "GRN", bias: false },
476
+ { id: "P096", name: "Diego Castillo", club: "GRN", bias: false },
477
+ { id: "P097", name: "Fatima Al-Hassan", club: "GRN", bias: false },
478
+ { id: "P098", name: "Olivia Montgomery", club: "GRN", bias: false },
479
+ { id: "P099", name: "Thiago Oliveira", club: "GRN", bias: false },
480
+ { id: "P100", name: "Zara Novak", club: "GRN", bias: false },
481
+ { id: "P101", name: "Amina Yusuf", club: "GRN", bias: false },
482
+ { id: "P102", name: "Hiroki Tanaka", club: "GRN", bias: false },
483
+ { id: "P103", name: "Lucas Oliveira", club: "GRN", bias: false },
484
+ ];
485
+
486
+ const divisionABrackets = assignBrackets(divisionAPlayers);
487
+ console.log("total players:", divisionAPlayers.length);
488
+ console.log(JSON.stringify(divisionABrackets, null, 2));
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+
3
+ import PropTypes from 'prop-types';
4
+
5
+ import './button.css';
6
+
7
+ /** Primary UI component for user interaction */
8
+ export const Button = ({
9
+ primary = false,
10
+ backgroundColor = null,
11
+ size = 'medium',
12
+ label,
13
+ ...props
14
+ }) => {
15
+ const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
16
+ return (
17
+ <button
18
+ type="button"
19
+ className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
20
+ style={backgroundColor && { backgroundColor }}
21
+ {...props}
22
+ >
23
+ {label}
24
+ </button>
25
+ );
26
+ };
27
+
28
+ Button.propTypes = {
29
+ /** Is this the principal call to action on the page? */
30
+ primary: PropTypes.bool,
31
+ /** What background color to use */
32
+ backgroundColor: PropTypes.string,
33
+ /** How large should the button be? */
34
+ size: PropTypes.oneOf(['small', 'medium', 'large']),
35
+ /** Button contents */
36
+ label: PropTypes.string.isRequired,
37
+ /** Optional click handler */
38
+ onClick: PropTypes.func,
39
+ };
@@ -0,0 +1,49 @@
1
+ import { fn } from '@storybook/test';
2
+
3
+ import { Button } from './Button';
4
+
5
+ // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
6
+ export default {
7
+ title: 'Example/Button',
8
+ component: Button,
9
+ parameters: {
10
+ // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
11
+ layout: 'centered',
12
+ },
13
+ // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
14
+ tags: ['autodocs'],
15
+ // More on argTypes: https://storybook.js.org/docs/api/argtypes
16
+ argTypes: {
17
+ backgroundColor: { control: 'color' },
18
+ },
19
+ // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
20
+ args: { onClick: fn() },
21
+ };
22
+
23
+ // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
24
+ export const Primary = {
25
+ args: {
26
+ primary: true,
27
+ label: 'Button',
28
+ },
29
+ };
30
+
31
+ export const Secondary = {
32
+ args: {
33
+ label: 'Button',
34
+ },
35
+ };
36
+
37
+ export const Large = {
38
+ args: {
39
+ size: 'large',
40
+ label: 'Button',
41
+ },
42
+ };
43
+
44
+ export const Small = {
45
+ args: {
46
+ size: 'small',
47
+ label: 'Button',
48
+ },
49
+ };