diff --git a/AGENTS.md b/AGENTS.md index 05430e50e..93a155ec0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,11 @@ ## General -- only rarely use comments, prefer descriptive variable and function names (leave existing comments as is) +- only rarely use comments, prefer descriptive variable and function names (leave existing comments as is). - if you encounter an existing TODO comment assume it is there for a reason and do not remove it - task is not considered completely until `pnpm run checks` passes - normal file structure has constants at the top immediately followed by the main function body of the file. Helpers are used to structure the code and they are at the bottom of the file (main implementation first, at the top of the file) - note: any formatting issue (such as tabs vs. spaces) can be resolved by running the `pnpm run biome:fix` command +- typical way to structure pure logic is into Modules divided by logical domains which are imported with the "* as Module" import and then used like so "Module.foo()". These functions always need JSDoc. ## Commands @@ -28,7 +29,7 @@ - prefer functional components over class components - prefer using hooks over class lifecycle methods -- do not use `useMemo`, `useCallback` or `useReducer` at all +- do not use `useMemo`, `useCallback` unless it is to stabilize a `useEffect` dependency array value - state management is done via plain `useState` and React Context API - avoid using `useEffect` - split bigger components into smaller ones @@ -55,6 +56,7 @@ - database code should only be written in Repository files - down migrations are not needed, only up migrations - every database id is of type number +- if we are working on a branch by default we should add to the migration this branch added instead of creating a brand new one - `/app/db/tables.ts` contains all tables and columns available - `db.sqlite3` is development database - `db-test.sqlite3` is the unit test database (should be blank sans migrations ran) @@ -65,11 +67,6 @@ - library used for unit testing is Vitest - Vitest browser mode can be used to write tests for components -## Testing in Chrome - -- some pages need authentication, you should impersonate "Sendou" user which can be done on the /admin page -- port can be checked from the `.env` file, you can assume dev server is already running - ## i18n - by default everything should be translated via i18next diff --git a/README.md b/README.md index f6319729b..d3e757da5 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,8 @@ Another key objective is to bridge the gap between casual and competitive player
Screenshots - - - + +
diff --git a/app/components/BuildCard.tsx b/app/components/BuildCard.tsx index b148249c4..ae37a5669 100644 --- a/app/components/BuildCard.tsx +++ b/app/components/BuildCard.tsx @@ -72,7 +72,7 @@ export function BuildCard({ build, owner, canEdit = false }: BuildProps) { } = build; const isNoGear = [headGearSplId, clothesGearSplId, shoesGearSplId].some( - (id) => id === -1, + (id) => typeof id !== "number", ); return ( @@ -274,7 +274,7 @@ function AbilitiesRowWithGear({ }: { gearType: GearType; abilities: AbilityType[]; - gearId: number; + gearId: number | null; }) { const { t } = useTranslation(["gear"]); const translatedGearName = t( @@ -283,7 +283,7 @@ function AbilitiesRowWithGear({ return ( <> - {gearId !== -1 ? ( + {typeof gearId === "number" ? ( void; }) { const { t, i18n } = useTranslation(["front"]); + const isHydrated = useHydrated(); if (events.length === 0) { return ( @@ -21,6 +24,10 @@ export function EventsList({ ); } + if (!isHydrated) { + return ; + } + const getDayKey = (timestamp: number) => { const date = new Date(timestamp * 1000); return date.toDateString(); diff --git a/app/components/MapPoolSelector.module.css b/app/components/MapPoolSelector.module.css index 0e7c22890..958061795 100644 --- a/app/components/MapPoolSelector.module.css +++ b/app/components/MapPoolSelector.module.css @@ -1,7 +1,8 @@ .stageRow { display: flex; width: 100%; - align-items: flex-end; + align-items: stretch; + gap: var(--s-2); } .stageNameRow { @@ -27,6 +28,8 @@ .stageImage { border-radius: var(--radius-box); + height: 100%; + object-fit: cover; } .modeButtonsContainer { diff --git a/app/components/elements/Dialog.module.css b/app/components/elements/Dialog.module.css index e7d6f2b17..7a609fbad 100644 --- a/app/components/elements/Dialog.module.css +++ b/app/components/elements/Dialog.module.css @@ -46,6 +46,7 @@ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + margin: auto; &[data-entering] { animation: zoom-in-95 300ms ease-out; diff --git a/app/components/layout/AuthErrorDialog.tsx b/app/components/layout/AuthErrorDialog.tsx new file mode 100644 index 000000000..8ecd648ab --- /dev/null +++ b/app/components/layout/AuthErrorDialog.tsx @@ -0,0 +1,65 @@ +import type { TFunction } from "i18next"; +import { createPortal } from "react-dom"; +import { useTranslation } from "react-i18next"; +import { useSearchParams } from "react-router"; +import { SendouDialog } from "~/components/elements/Dialog"; +import { useUser } from "~/features/auth/core/user"; +import { useHydrated } from "~/hooks/useHydrated"; +import { SENDOU_INK_DISCORD_URL } from "~/utils/urls"; +import styles from "./UserItem.module.css"; + +export function AuthErrorDialog() { + const { t } = useTranslation(); + const isHydrated = useHydrated(); + const user = useUser(); + const [searchParams] = useSearchParams(); + const authError = searchParams.get("authError"); + + if (authError == null || !isHydrated || user) return null; + + return createPortal( + +
+ {authErrorContent(authError, t)} +
+
, + document.body, + ); +} + +function authErrorContent(authError: string, t: TFunction) { + switch (authError) { + case "aborted": + return t("auth.errors.discordPermissions"); + case "discordOverloaded": + return ( + <> + {t("auth.errors.discordOverloaded")}{" "} + + {SENDOU_INK_DISCORD_URL} + + + ); + case "unverifiedEmail": + return t("auth.errors.unverifiedEmail"); + case "browserPrivacy": + return t("auth.errors.browserPrivacy"); + default: + return ( + <> + {t("auth.errors.unknown")}{" "} + + {SENDOU_INK_DISCORD_URL} + + + ); + } +} diff --git a/app/components/layout/ChatSidebar.tsx b/app/components/layout/ChatSidebar.tsx index 68feb5b8c..f3826ecff 100644 --- a/app/components/layout/ChatSidebar.tsx +++ b/app/components/layout/ChatSidebar.tsx @@ -66,7 +66,9 @@ function RoomList({ onClose }: { onClose?: () => void }) { .filter((room) => room.expiresAt > Date.now()) .sort((a, b) => { if (a.isObsolete !== b.isObsolete) return a.isObsolete ? 1 : -1; - return 0; + const aRecency = a.lastMessageTimestamp || a.createdAt; + const bRecency = b.lastMessageTimestamp || b.createdAt; + return bRecency - aRecency; }); return ( diff --git a/app/components/layout/GlobalSearch.module.css b/app/components/layout/GlobalSearch.module.css index fc9b8675d..47ea40621 100644 --- a/app/components/layout/GlobalSearch.module.css +++ b/app/components/layout/GlobalSearch.module.css @@ -98,15 +98,38 @@ outline: none; } +.inputContainer { + display: flex; + align-items: center; + border-bottom: 1px solid var(--color-border); + + &:focus-within { + border-color: var(--color-text-accent); + } +} + +.inputPrefix { + font-size: var(--font-xs); + color: var(--color-text-high); + margin-left: var(--s-4); + background-color: var(--color-bg-high); + border-radius: var(--radius-selector); + height: var(--selector-size); + padding-inline: var(--s-1); + text-align: center; + width: var(--s-8); + line-height: 1.75; +} + .input.input { padding: var(--s-4); border: none; - border-bottom: 1px solid var(--color-border); border-radius: 0; background: transparent; font-size: var(--font-md); color: var(--color-text); height: auto; + padding-left: 0; & input::placeholder { color: var(--color-text-high); @@ -114,7 +137,6 @@ &:focus-within { outline: none; - border-color: var(--color-text-accent); } } diff --git a/app/components/layout/GlobalSearch.tsx b/app/components/layout/GlobalSearch.tsx index 810d5ec27..81805f49e 100644 --- a/app/components/layout/GlobalSearch.tsx +++ b/app/components/layout/GlobalSearch.tsx @@ -3,6 +3,7 @@ import type { TFunction } from "i18next"; import { Search } from "lucide-react"; import * as React from "react"; import { + Button, Dialog, DialogTrigger, ListBox, @@ -48,6 +49,14 @@ const SEARCH_TYPES = [ ] as const; type SearchType = (typeof SEARCH_TYPES)[number]; +const SEARCH_TYPE_TO_PREFIX: Record = { + weapons: "w", + users: "u", + teams: "t", + organizations: "o", + tournaments: "to", +}; + const STORAGE_KEY = "global-search-search-type"; function searchTypeIconPath(type: SearchType): string { @@ -128,15 +137,11 @@ export function GlobalSearch() { return ( - + @@ -188,6 +193,7 @@ function GlobalSearchContent({ React.useState( resolveInitialWeapon(initialWeaponId, t), ); + const inputRef = React.useRef(null); const listBoxRef = React.useRef(null); const modifierKeyRef = React.useRef(false); @@ -215,7 +221,7 @@ function GlobalSearchContent({ useDebounce( () => { if (searchType === "weapons") return; - if (!query) return; + if (query.length < 3) return; fetcher.load( `/search?q=${encodeURIComponent(query)}&type=${searchType}&limit=10`, ); @@ -224,11 +230,13 @@ function GlobalSearchContent({ [query, searchType], ); - const results = fetcher.data?.results ?? []; - const hasQuery = query.length > 0; + const hasQuery = query.length >= 3; + const fetchedType = fetcher.data?.type ?? null; + const results = + hasQuery && fetchedType === searchType ? (fetcher.data?.results ?? []) : []; const weaponResults = - searchType === "weapons" ? filterWeaponResults(query, t) : []; + searchType === "weapons" && hasQuery ? filterWeaponResults(query, t) : []; const recentWeapons: SelectedWeapon[] = searchType === "weapons" @@ -266,6 +274,26 @@ function GlobalSearchContent({ setSelectedWeapon(null); }; + const handleQueryChange = (e: React.ChangeEvent) => { + const value = e.target.value; + const separatorMatch = value.match(/^([a-zA-Z]+)\.$/); + + if (separatorMatch) { + const typedPrefix = separatorMatch[1]; + const matchedType = SEARCH_TYPES.find( + (type) => SEARCH_TYPE_TO_PREFIX[type] === typedPrefix, + ); + if (matchedType) { + setSearchType(matchedType); + setSelectedWeapon(null); + setQuery(""); + return; + } + } + + setQuery(value); + }; + const handleInputKeyDown = (e: React.KeyboardEvent) => { const currentResults = searchType === "weapons" ? weaponResults : results; if (e.key === "ArrowDown" && currentResults.length > 0) { @@ -302,15 +330,20 @@ function GlobalSearchContent({ return (
- setQuery(e.target.value)} - onKeyDown={handleInputKeyDown} - icon={} - /> +
+

+ {`${SEARCH_TYPE_TO_PREFIX[searchType]}.`} +

+ } + /> +
-
- {children} -
- {authError != null && - isHydrated && - createPortal( - -
- {authError === "aborted" ? ( - t("auth.errors.discordPermissions") - ) : ( - <> - {t("auth.errors.unknown")}{" "} - - {SENDOU_INK_DISCORD_URL} - - - )} -
-
, - document.body, - )} - +
+ {children} +
); } diff --git a/app/components/layout/index.module.css b/app/components/layout/index.module.css index bc907cdcc..bc0132551 100644 --- a/app/components/layout/index.module.css +++ b/app/components/layout/index.module.css @@ -86,11 +86,18 @@ } } +.pageIconWrapper { + display: flex; + align-items: center; + flex-shrink: 0; + width: 28px; + height: 28px; + animation: fadeInFull 200ms ease-out 150ms both; +} + .pageIcon { width: 28px; height: 28px; - flex-shrink: 0; - animation: fadeInFull 200ms ease-out 150ms both; object-fit: cover; } diff --git a/app/components/layout/index.tsx b/app/components/layout/index.tsx index 28e5be824..9474f3f75 100644 --- a/app/components/layout/index.tsx +++ b/app/components/layout/index.tsx @@ -43,6 +43,7 @@ import { NotificationDot } from "../NotificationDot"; import { ListLink, SideNav, SideNavFooter, SideNavHeader } from "../SideNav"; import sideNavStyles from "../SideNav.module.css"; import { StreamListItems } from "../StreamListItems"; +import { AuthErrorDialog } from "./AuthErrorDialog"; import { ChatSidebar } from "./ChatSidebar"; import { Footer } from "./Footer"; import styles from "./index.module.css"; @@ -386,21 +387,21 @@ export function Layout({ - - - - - - - - - + + + + + + setSideNavCollapsed(!sideNavCollapsed)} className={styles.sideNavCollapseButton} @@ -447,6 +448,7 @@ export function Layout({ setChatSidebarOpen(false)} />
) : null} + ); } @@ -542,16 +544,26 @@ function PageIcon({ crumb }: { crumb: Breadcrumb }) { const isExternal = crumb.imgPath.includes("."); const iconClass = clsx(styles.pageIcon, "rounded"); - return isExternal ? ( - - ) : ( - + return ( +
+ {isExternal ? ( + + ) : ( + + )} +
); } diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts index 8f39dd692..f093aa742 100644 --- a/app/db/seed/index.ts +++ b/app/db/seed/index.ts @@ -241,6 +241,7 @@ const basicSeeds = (variation?: SeedVariation | null) => [ liveStreams, splatoonRotations, variation === "FINALIZED_BRACKET" ? finalizedBracket : undefined, + variation === "AB_RR" ? abDivisionsTournament : undefined, ]; export async function seed(variation?: SeedVariation | null) { @@ -510,6 +511,106 @@ function finalizedBracket() { } } +const AB_RR_TOURNAMENT_ID = 8; +const AB_RR_EVENT_ID = 208; +const AB_RR_TEAM_ID_OFFSET = 700; +const AB_RR_TEAM_COUNT = 12; + +function abDivisionsTournament() { + sql + .prepare( + `insert into "Tournament" ("id", "mapPickingStyle", "settings") + values ($id, $mapPickingStyle, $settings)`, + ) + .run({ + id: AB_RR_TOURNAMENT_ID, + mapPickingStyle: "AUTO_ALL", + settings: JSON.stringify({ + bracketProgression: [ + { + type: "round_robin", + name: "Groups stage", + requiresCheckIn: false, + settings: { + hasAbDivisions: true, + teamsPerGroup: AB_RR_TEAM_COUNT, + }, + }, + ], + }), + }); + + sql + .prepare( + `insert into "CalendarEvent" ("id", "name", "description", "discordInviteCode", "bracketUrl", "authorId", "tournamentId") + values ($id, $name, $description, $discordInviteCode, $bracketUrl, $authorId, $tournamentId)`, + ) + .run({ + id: AB_RR_EVENT_ID, + name: "A/B Divisions Cup", + description: "Bipartite round robin tournament for testing", + discordInviteCode: "abrr", + bracketUrl: "https://example.com", + authorId: ADMIN_ID, + tournamentId: AB_RR_TOURNAMENT_ID, + }); + + sql + .prepare( + `insert into "CalendarEventDate" ("eventId", "startTime") + values ($eventId, $startTime)`, + ) + .run({ + eventId: AB_RR_EVENT_ID, + startTime: dateToDatabaseTimestamp(new Date(Date.now() - 1000 * 60 * 30)), + }); + + const userIds = userIdsInAscendingOrderById(); + const now = dateToDatabaseTimestamp(new Date()); + + for (let i = 0; i < AB_RR_TEAM_COUNT; i++) { + const teamId = AB_RR_TEAM_ID_OFFSET + i + 1; + + sql + .prepare( + `insert into "TournamentTeam" ("id", "name", "createdAt", "tournamentId", "inviteCode", "seed") + values ($id, $name, $createdAt, $tournamentId, $inviteCode, $seed)`, + ) + .run({ + id: teamId, + name: `AB Team ${i + 1}`, + createdAt: now, + tournamentId: AB_RR_TOURNAMENT_ID, + inviteCode: shortNanoid(), + seed: i + 1, + }); + + sql + .prepare( + `insert into "TournamentTeamCheckIn" ("tournamentTeamId", "checkedInAt") + values ($tournamentTeamId, $checkedInAt)`, + ) + .run({ + tournamentTeamId: teamId, + checkedInAt: now, + }); + + for (let j = 0; j < 4; j++) { + sql + .prepare( + `insert into "TournamentTeamMember" ("tournamentTeamId", "userId", "createdAt", "role") + values ($tournamentTeamId, $userId, $createdAt, $role)`, + ) + .run({ + tournamentTeamId: teamId, + userId: userIds.shift()!, + createdAt: now, + role: j === 0 ? "OWNER" : "REGULAR", + }); + } + } +} + function wipeDB() { const tablesToDelete = [ "ScrimPost", diff --git a/app/db/tables.ts b/app/db/tables.ts index 4e37b9d38..fa125b60d 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -150,14 +150,14 @@ export type BadgeOwner = { }; export interface Build { - clothesGearSplId: number; + clothesGearSplId: number | null; description: string | null; - headGearSplId: number; + headGearSplId: number | null; id: GeneratedAlways; modes: JSONColumnTypeNullable; ownerId: number; private: DBBoolean | null; - shoesGearSplId: number; + shoesGearSplId: number | null; title: string; updatedAt: Generated; } @@ -520,6 +520,7 @@ export interface TournamentSettings { maxMembersPerTeam?: number; isTest?: boolean; isDraft?: boolean; + requireSendouQParticipation?: boolean; } export interface CastedMatchesInfo { @@ -743,6 +744,8 @@ export interface TournamentStageSettings { thirdPlaceMatch?: boolean; // RR teamsPerGroup?: number; + /** (RR only) When true, teams are split into A and B divisions and matches only pair A-vs-B. Only valid on starting brackets. */ + hasAbDivisions?: boolean; // SWISS groupCount?: number; // SWISS @@ -813,6 +816,8 @@ export interface TournamentTeam { isPlaceholder: Generated; lfgNote: string | null; chatCode: Generated; + /** A/B division assignment for bipartite round robin brackets. `0` = A, `1` = B, `null` = unassigned. */ + abDivision: number | null; } export interface TournamentTeamCheckIn { diff --git a/app/features/api-private/constants.ts b/app/features/api-private/constants.ts index d4611d366..80c54f859 100644 --- a/app/features/api-private/constants.ts +++ b/app/features/api-private/constants.ts @@ -8,4 +8,5 @@ export const SEED_VARIATIONS = [ "NO_SQ_GROUPS", "TEAM_MAP_PREFS", "FINALIZED_BRACKET", + "AB_RR", ] as const; diff --git a/app/features/art/actions/art.new.server.ts b/app/features/art/actions/art.new.server.ts index ae9149a71..be31e60af 100644 --- a/app/features/art/actions/art.new.server.ts +++ b/app/features/art/actions/art.new.server.ts @@ -5,6 +5,7 @@ import { redirect } from "react-router"; import * as ArtRepository from "~/features/art/ArtRepository.server"; import { requireUser } from "~/features/auth/core/user.server"; import { uploadStreamToS3 } from "~/features/img-upload/s3.server"; +import { ALLOWED_IMAGE_EXTENSIONS } from "~/features/img-upload/upload-constants"; import { notify } from "~/features/notifications/core/notify.server"; import { requireRole } from "~/modules/permissions/guards.server"; import { dateToDatabaseTimestamp } from "~/utils/dates"; @@ -73,11 +74,15 @@ export const action: ActionFunction = async ({ request }) => { fileUpload.fieldName === "img" || fileUpload.fieldName === "smallImg" ) { - const ending = fileUpload.name.split(".").pop(); + const ending = fileUpload.name.split(".").pop()?.toLowerCase(); invariant( ending && ending !== fileUpload.name, `File missing extension: "${fileUpload.name}"`, ); + invariant( + ALLOWED_IMAGE_EXTENSIONS.includes(ending), + `Invalid file extension: "${ending}"`, + ); const newFilename = `${preDecidedFilename}${fileUpload.fieldName === "smallImg" ? "-small" : ""}.${ending}`; const uploadedFileLocation = await uploadStreamToS3( diff --git a/app/features/articles/core/bySlug.server.ts b/app/features/articles/core/bySlug.server.ts index 92029da0d..aa6cf0df4 100644 --- a/app/features/articles/core/bySlug.server.ts +++ b/app/features/articles/core/bySlug.server.ts @@ -5,10 +5,15 @@ import { ZodError, type z } from "zod"; import { ARTICLES_FOLDER_PATH } from "../articles-constants"; import { articleDataSchema } from "../articles-schemas.server"; +const RESOLVED_ARTICLES_DIR = path.resolve(ARTICLES_FOLDER_PATH); + export function articleBySlug(slug: string) { + const validFiles = fs.globSync("*.md", { cwd: RESOLVED_ARTICLES_DIR }); + if (!validFiles.includes(`${slug}.md`)) return null; + try { const rawMarkdown = fs.readFileSync( - path.join(ARTICLES_FOLDER_PATH, `${slug}.md`), + path.join(RESOLVED_ARTICLES_DIR, `${slug}.md`), "utf8", ); const { content, data } = matter(rawMarkdown); diff --git a/app/features/associations/core/Association.test.ts b/app/features/associations/core/Association.test.ts index 3919056a3..adecb7cb5 100644 --- a/app/features/associations/core/Association.test.ts +++ b/app/features/associations/core/Association.test.ts @@ -7,7 +7,6 @@ describe("isVisible", () => { it("should return true if visibility is null", () => { const args: Association.IsVisibleArgs = { visibility: null, - time: new Date(), associations: null, }; expect(Association.isVisible(args)).toBe(true); @@ -16,7 +15,6 @@ describe("isVisible", () => { it("should return false if not member of the association", () => { const args: Association.IsVisibleArgs = { visibility: { forAssociation: 1 }, - time: new Date(), associations: null, }; expect(Association.isVisible(args)).toBe(false); @@ -25,7 +23,6 @@ describe("isVisible", () => { it("should return true if member of the association", () => { const args: Association.IsVisibleArgs = { visibility: { forAssociation: 1 }, - time: new Date(), associations: { actual: [{ id: 1 }], virtual: [], @@ -37,7 +34,6 @@ describe("isVisible", () => { it("should return true if member of the virtual association", () => { const args: Association.IsVisibleArgs = { visibility: { forAssociation: "+1" }, - time: new Date(), associations: { actual: [], virtual: ["+1"], @@ -56,7 +52,6 @@ describe("isVisible", () => { { at: dateToDatabaseTimestamp(visibleAt), forAssociation: 1 }, ], }, - time: new Date(), associations: { actual: [{ id: 1 }], virtual: [], @@ -106,7 +101,6 @@ describe("isVisible", () => { it("should return true if viewer is a friend of the content owner", () => { const args: Association.IsVisibleArgs = { visibility: { forAssociation: "FRIENDS" }, - time: new Date(), associations: { actual: [], virtual: [], @@ -120,7 +114,6 @@ describe("isVisible", () => { it("should return false if viewer is not a friend of the content owner", () => { const args: Association.IsVisibleArgs = { visibility: { forAssociation: "FRIENDS" }, - time: new Date(), associations: { actual: [], virtual: [], @@ -134,7 +127,6 @@ describe("isVisible", () => { it("should return false for FRIENDS visibility when not logged in", () => { const args: Association.IsVisibleArgs = { visibility: { forAssociation: "FRIENDS" }, - time: new Date(), associations: null, }; expect(Association.isVisible(args)).toBe(false); diff --git a/app/features/associations/core/Association.ts b/app/features/associations/core/Association.ts index 2d2ef3bac..8a4b9351c 100644 --- a/app/features/associations/core/Association.ts +++ b/app/features/associations/core/Association.ts @@ -4,7 +4,7 @@ import type { AssociationVisibility } from "../associations-types"; export interface IsVisibleArgs { visibility: AssociationVisibility | null; - time: Date; + time?: Date; associations: { virtual: Array; actual: Array<{ id: number }>; @@ -20,7 +20,7 @@ export function isVisible(args: IsVisibleArgs) { args.visibility.forAssociation, ]; - const dbTime = dateToDatabaseTimestamp(args.time); + const dbTime = dateToDatabaseTimestamp(args.time ?? new Date()); for (const visibility of args.visibility.notFoundInstructions ?? []) { if (dbTime > visibility.at) { currentVisibility.push(visibility.forAssociation); diff --git a/app/features/auth/core/DiscordStrategy.server.ts b/app/features/auth/core/DiscordStrategy.server.ts index 4e1121106..6517cb755 100644 --- a/app/features/auth/core/DiscordStrategy.server.ts +++ b/app/features/auth/core/DiscordStrategy.server.ts @@ -117,7 +117,7 @@ export const DiscordStrategy = () => { return userFromDb.id; } catch (e) { logger.error("Failed to finish authentication:\n", e); - throw new Error("Failed to finish authentication"); + throw e; } }, ); diff --git a/app/features/auth/core/errors.ts b/app/features/auth/core/errors.ts index 76abc84ad..e887f68dc 100644 --- a/app/features/auth/core/errors.ts +++ b/app/features/auth/core/errors.ts @@ -1 +1,6 @@ -export type AuthErrorCode = "aborted" | "unknown"; +export type AuthErrorCode = + | "aborted" + | "discordOverloaded" + | "unverifiedEmail" + | "browserPrivacy" + | "unknown"; diff --git a/app/features/auth/core/routes.server.ts b/app/features/auth/core/routes.server.ts index 69f57bcfa..98620e552 100644 --- a/app/features/auth/core/routes.server.ts +++ b/app/features/auth/core/routes.server.ts @@ -19,6 +19,7 @@ import { IMPERSONATED_SESSION_KEY, SESSION_KEY, } from "./authenticator.server"; +import type { AuthErrorCode } from "./errors"; import { authSessionStorage } from "./session.server"; import { getUser } from "./user.server"; @@ -47,8 +48,11 @@ export const callbackLoader: LoaderFunction = async ({ request }) => { }); } catch (error) { if (error instanceof Error) { - logger.error("Error during authentication:", error); - throw redirect(authErrorUrl("unknown")); + logger.error( + `Error during authentication (${classifyAuthError(error)}):`, + error, + ); + throw redirect(authErrorUrl(classifyAuthError(error))); } throw error; @@ -209,3 +213,24 @@ export const logInViaLinkLoader: LoaderFunction = async ({ request }) => { headers: { "Set-Cookie": await authSessionStorage.commitSession(session) }, }); }; + +function classifyAuthError(error: Error): AuthErrorCode { + const message = error.message; + + if ( + message.includes("rate limited") || + ("status" in error && error.status === 429) + ) { + return "discordOverloaded"; + } + + if (message === "Unverified user") { + return "unverifiedEmail"; + } + + if (message.includes("Missing state")) { + return "browserPrivacy"; + } + + return "unknown"; +} diff --git a/app/features/badges/homemade.json b/app/features/badges/homemade.json index 69a2a43af..ab565d0ef 100644 --- a/app/features/badges/homemade.json +++ b/app/features/badges/homemade.json @@ -147,6 +147,10 @@ "displayName": "Biggest School in the Sea", "authorDiscordId": "629773997822967819" }, + "bubblyb": { + "displayName": "Bubbly's Birthdays", + "authorDiscordId": "752582395076673577" + }, "bucketmeow12": { "displayName": "Token Of Appreciation", "authorDiscordId": "1170249805373657093" @@ -335,6 +339,10 @@ "displayName": "Mesozoic Mayhem: Dreadnoughtus Undefeated", "authorDiscordId": "352207524390240257" }, + "dreamteam": { + "displayName": "Dream Team", + "authorDiscordId": "900408802938081350" + }, "dualin": { "displayName": "Dualin' Horizons", "authorDiscordId": "752582395076673577" @@ -859,6 +867,10 @@ "displayName": "Oktofest LAN (Second)", "authorDiscordId": "751912670403362836" }, + "oocee": { + "displayName": "OCE Open Series", + "authorDiscordId": "1170249805373657093" + }, "order1": { "displayName": "ORDR S1", "authorDiscordId": "338806780446638082" @@ -1483,6 +1495,10 @@ "displayName": "WRC #0", "authorDiscordId": "683603297839743009" }, + "wwarning": { + "displayName": "Without Warning", + "authorDiscordId": "752582395076673577" + }, "zekkocup": { "displayName": "Zekko Cup", "authorDiscordId": "1320944066002681876" diff --git a/app/features/build-stats/build-stats-utils.ts b/app/features/build-stats/build-stats-utils.ts index 89c8e9c37..ec19d81de 100644 --- a/app/features/build-stats/build-stats-utils.ts +++ b/app/features/build-stats/build-stats-utils.ts @@ -1,3 +1,4 @@ +import * as R from "remeda"; import { abilities } from "~/modules/in-game-lists/abilities"; import type { Ability } from "~/modules/in-game-lists/types"; import invariant from "~/utils/invariant"; @@ -101,18 +102,14 @@ type AbilityCountsMap = Map; const POPULAR_BUILDS_TO_SHOW = 25; export function popularBuilds(builds: Array) { - const counts = new Map(); - for (const build of builds) { - const summedUpAbilities = sumUpAbilities(build); - const serializedAbilities = serializeAbilityCountsMap(summedUpAbilities); - - counts.set(serializedAbilities, (counts.get(serializedAbilities) ?? 0) + 1); - } - - const serializedToShow = Array.from(counts.entries()) - .sort((a, b) => b[1] - a[1]) - .filter(([, count]) => count > 1) - .slice(0, POPULAR_BUILDS_TO_SHOW); + const serializedToShow = R.pipe( + builds, + R.countBy((build) => serializeAbilityCountsMap(sumUpAbilities(build))), + R.entries(), + R.sortBy([([, count]) => count, "desc"]), + R.filter(([, count]) => count > 1), + R.take(POPULAR_BUILDS_TO_SHOW), + ); return serializedToShowToResultType(serializedToShow); } diff --git a/app/features/builds/BuildRepository.server.ts b/app/features/builds/BuildRepository.server.ts index ff3839911..0441ef29a 100644 --- a/app/features/builds/BuildRepository.server.ts +++ b/app/features/builds/BuildRepository.server.ts @@ -1,5 +1,6 @@ import type { ExpressionBuilder, Transaction } from "kysely"; import { jsonArrayFrom } from "kysely/helpers/sqlite"; +import * as R from "remeda"; import { db } from "~/db/sql"; import type { BuildWeapon, DB, Tables, TablesInsertable } from "~/db/tables"; import { modesShort } from "~/modules/in-game-lists/modes"; @@ -10,6 +11,7 @@ import type { ModeShort, } from "~/modules/in-game-lists/types"; import { weaponIdToArrayWithAlts } from "~/modules/in-game-lists/weapon-ids"; +import { dateToDatabaseTimestamp } from "~/utils/dates"; import { LimitReachedError } from "~/utils/errors"; import invariant from "~/utils/invariant"; import { commonUserJsonObject } from "~/utils/kysely.server"; @@ -76,22 +78,15 @@ function dbAbilitiesToArrayOfArrays( Pick >, ): BuildAbilitiesTuple { - const sorted = abilities - .slice() - .sort((a, b) => { - if (a.gearType === b.gearType) return a.slotIndex - b.slotIndex; - - return gearOrder.indexOf(a.gearType) - gearOrder.indexOf(b.gearType); - }) - .map((a) => a.ability); + const sorted = R.sortBy( + abilities, + (a) => gearOrder.indexOf(a.gearType), + (a) => a.slotIndex, + ).map((a) => a.ability); invariant(sorted.length === 12, "expected 12 abilities"); - return [ - [sorted[0], sorted[1], sorted[2], sorted[3]], - [sorted[4], sorted[5], sorted[6], sorted[7]], - [sorted[8], sorted[9], sorted[10], sorted[11]], - ]; + return R.chunk(sorted, 4) as BuildAbilitiesTuple; } interface CreateArgs { @@ -107,6 +102,14 @@ interface CreateArgs { private: TablesInsertable["Build"]["private"]; } +function serializeModes(modes: Array | null) { + if (!modes || modes.length === 0) return null; + + return JSON.stringify( + modes.slice().sort((a, b) => modesShort.indexOf(a) - modesShort.indexOf(b)), + ); +} + async function createInTrx({ args, trx, @@ -120,22 +123,29 @@ async function createInTrx({ ownerId: args.ownerId, title: args.title, description: args.description, - modes: - args.modes && args.modes.length > 0 - ? JSON.stringify( - args.modes - .slice() - .sort((a, b) => modesShort.indexOf(a) - modesShort.indexOf(b)), - ) - : null, - headGearSplId: args.headGearSplId ?? -1, - clothesGearSplId: args.clothesGearSplId ?? -1, - shoesGearSplId: args.shoesGearSplId ?? -1, + modes: serializeModes(args.modes), + headGearSplId: args.headGearSplId, + clothesGearSplId: args.clothesGearSplId, + shoesGearSplId: args.shoesGearSplId, private: args.private, }) .returningAll() .executeTakeFirstOrThrow(); + await populateBuildChildrenInTrx({ trx, buildId, updatedAt, args }); +} + +async function populateBuildChildrenInTrx({ + trx, + buildId, + updatedAt, + args, +}: { + trx: Transaction; + buildId: number; + updatedAt: number; + args: CreateArgs; +}) { await trx .insertInto("BuildWeapon") .values( @@ -204,8 +214,37 @@ export async function create(args: CreateArgs) { export async function update(args: CreateArgs & { id: number }) { return db.transaction().execute(async (trx) => { - await trx.deleteFrom("Build").where("id", "=", args.id).execute(); - await createInTrx({ args, trx }); + const { updatedAt } = await trx + .updateTable("Build") + .set({ + title: args.title, + description: args.description, + modes: serializeModes(args.modes), + headGearSplId: args.headGearSplId, + clothesGearSplId: args.clothesGearSplId, + shoesGearSplId: args.shoesGearSplId, + private: args.private, + updatedAt: dateToDatabaseTimestamp(new Date()), + }) + .where("id", "=", args.id) + .returning("updatedAt") + .executeTakeFirstOrThrow(); + + await trx + .deleteFrom("BuildWeapon") + .where("buildId", "=", args.id) + .execute(); + await trx + .deleteFrom("BuildAbility") + .where("buildId", "=", args.id) + .execute(); + + await populateBuildChildrenInTrx({ + trx, + buildId: args.id, + updatedAt, + args, + }); }); } @@ -281,65 +320,23 @@ export async function allByWeaponId( const { limit, sortAbilities: shouldSortAbilities = false } = options; const weaponIds = weaponIdToArrayWithAlts(weaponId); - let rows: Awaited>; + // For weapons with alts, run separate queries and merge. + // This allows each query to use the covering index for ordering, + // which is ~6x faster than using IN with multiple values. + const allResults = await Promise.all( + weaponIds.map((id) => buildsByWeaponIdQuery(id, limit)), + ); - if (weaponIds.length === 1) { - rows = await buildsByWeaponIdQuery(weaponIds[0], limit); - } else { - // For weapons with alts, run separate queries and merge. - // This allows each query to use the covering index for ordering, - // which is ~6x faster than using IN with multiple values. - const allResults = await Promise.all( - weaponIds.map((id) => buildsByWeaponIdQuery(id, limit)), - ); - - const seenBuildIds = new Set(); - type BuildRow = Awaited>[number]; - const merged: BuildRow[] = []; - - // Merge results maintaining sort order (tier asc, isTop500 desc, updatedAt desc) - // Since each query returns sorted results, we can interleave them - const pointers = allResults.map(() => 0); - - while (merged.length < limit) { - let bestIdx = -1; - let bestRow: BuildRow | null = null; - - for (let i = 0; i < allResults.length; i++) { - while ( - pointers[i] < allResults[i].length && - seenBuildIds.has(allResults[i][pointers[i]].id) - ) { - pointers[i]++; - } - - if (pointers[i] >= allResults[i].length) continue; - - const row = allResults[i][pointers[i]]; - - if ( - !bestRow || - row.bwTier < bestRow.bwTier || - (row.bwTier === bestRow.bwTier && - row.bwIsTop500 > bestRow.bwIsTop500) || - (row.bwTier === bestRow.bwTier && - row.bwIsTop500 === bestRow.bwIsTop500 && - row.bwUpdatedAt > bestRow.bwUpdatedAt) - ) { - bestIdx = i; - bestRow = row; - } - } - - if (bestIdx === -1 || !bestRow) break; - - seenBuildIds.add(bestRow.id); - merged.push(bestRow); - pointers[bestIdx]++; - } - - rows = merged; - } + const rows = R.pipe( + allResults.flat(), + R.sortBy( + (row) => row.bwTier, + [(row) => row.bwIsTop500, "desc"], + [(row) => row.bwUpdatedAt, "desc"], + ), + R.uniqueBy((row) => row.id), + R.take(limit), + ); return rows.map((row) => { const abilities = dbAbilitiesToArrayOfArrays(row.abilities); @@ -408,8 +405,8 @@ function hasXRankPlacement(eb: ExpressionBuilder) { eb .selectFrom("Build") .select("BuildWeapon.buildId") - .leftJoin("SplatoonPlayer", "SplatoonPlayer.userId", "Build.ownerId") - .leftJoin( + .innerJoin("SplatoonPlayer", "SplatoonPlayer.userId", "Build.ownerId") + .innerJoin( "XRankPlacement", "XRankPlacement.playerId", "SplatoonPlayer.id", diff --git a/app/features/builds/builds-constants.ts b/app/features/builds/builds-constants.ts index 92dcb9cbf..9e5413eba 100644 --- a/app/features/builds/builds-constants.ts +++ b/app/features/builds/builds-constants.ts @@ -4,9 +4,11 @@ export const MAX_BUILD_FILTERS = 6; export const FILTER_SEARCH_PARAM_KEY = "f"; -export const PATCHES = [ +type Patch = { patch: string; date: string }; + +export const PATCHES: Array = [ { - path: "11.1.0", + patch: "11.1.0", date: "2026-03-18", }, { diff --git a/app/features/builds/builds-schemas.server.ts b/app/features/builds/builds-schemas.server.ts index 784a32ca7..97445309a 100644 --- a/app/features/builds/builds-schemas.server.ts +++ b/app/features/builds/builds-schemas.server.ts @@ -1,11 +1,16 @@ import { z } from "zod"; +import { MAX_AP } from "~/features/build-analyzer/analyzer-constants"; import { ability, modeShort, safeJSONParse } from "~/utils/zod"; -import { MAX_BUILD_FILTERS } from "./builds-constants"; +import { + BUILDS_PAGE_BATCH_SIZE, + BUILDS_PAGE_MAX_BUILDS, + MAX_BUILD_FILTERS, +} from "./builds-constants"; const abilityFilterSchema = z.object({ type: z.literal("ability"), ability: z.string().toUpperCase().pipe(ability), - value: z.union([z.number(), z.boolean()]), + value: z.union([z.int().min(0).max(MAX_AP), z.boolean()]), comparison: z .string() .toUpperCase() @@ -20,7 +25,7 @@ const modeFilterSchema = z.object({ const dateFilterSchema = z.object({ type: z.literal("date"), - date: z.string(), + date: z.iso.date(), }); export const buildFiltersSearchParams = z.preprocess( @@ -36,3 +41,10 @@ export const buildFiltersSearchParams = z.preprocess( export type BuildFiltersFromSearchParams = NonNullable< z.infer >; + +export const buildsLimitSearchParam = z.coerce + .number() + .int() + .positive() + .catch(BUILDS_PAGE_BATCH_SIZE) + .transform((value) => Math.min(value, BUILDS_PAGE_MAX_BUILDS)); diff --git a/app/features/builds/builds-types.ts b/app/features/builds/builds-types.ts index 3e32599da..f1e3c1f0f 100644 --- a/app/features/builds/builds-types.ts +++ b/app/features/builds/builds-types.ts @@ -9,26 +9,24 @@ export interface BuildWeaponWithTop500Info { isTop500: number; } -type WithId = T & { id: string }; - -export type AbilityBuildFilter = WithId<{ +export type AbilityBuildFilter = { type: "ability"; ability: Ability; /** Ability points value or "has"/"doesn't have" */ - value?: number | boolean; + value: number | boolean; comparison?: "AT_LEAST" | "AT_MOST"; -}>; +}; -export type ModeBuildFilter = WithId<{ +export type ModeBuildFilter = { type: "mode"; mode: ModeShort; -}>; +}; -export type DateBuildFilter = WithId<{ +export type DateBuildFilter = { type: "date"; /** YYYY-MM-DD */ date: string; -}>; +}; export type BuildFilter = | AbilityBuildFilter diff --git a/app/features/builds/components/FilterSection.tsx b/app/features/builds/components/FilterSection.tsx index 40d08aaae..fa4d7bfda 100644 --- a/app/features/builds/components/FilterSection.tsx +++ b/app/features/builds/components/FilterSection.tsx @@ -12,7 +12,7 @@ import type { Ability as AbilityType, ModeShort, } from "~/modules/in-game-lists/types"; -import { dateToYYYYMMDD } from "~/utils/dates"; +import { dateToYYYYMMDD, isValidDate } from "~/utils/dates"; import { PATCHES } from "../builds-constants"; import type { AbilityBuildFilter, @@ -198,27 +198,20 @@ function DateFilter({ const { t } = useTranslation(["builds"]); const { formatDate } = useTimeFormat(); - const selectValue = () => { - const dateString = dateToYYYYMMDD(new Date(filter.date)); - - if ( - PATCHES.find(({ date }) => { - return new Date(date).toISOString().split("T")[0] === dateString; - }) - ) { - return dateString; - } - - return "CUSTOM"; - }; + const selectValue = () => + PATCHES.some(({ date }) => date === filter.date) ? filter.date : "CUSTOM"; // on Saturday so it doesn't overlap with actual path dates (no patches on Saturdays) const oneMonthAgoOnSaturday = new Date(); - oneMonthAgoOnSaturday.setDate(oneMonthAgoOnSaturday.getDate() - 30); - oneMonthAgoOnSaturday.setDate( - oneMonthAgoOnSaturday.getDate() - oneMonthAgoOnSaturday.getDay() + 6, + oneMonthAgoOnSaturday.setUTCDate(oneMonthAgoOnSaturday.getUTCDate() - 30); + oneMonthAgoOnSaturday.setUTCDate( + oneMonthAgoOnSaturday.getUTCDate() - oneMonthAgoOnSaturday.getUTCDay() + 6, ); + const customDate = isValidDate(new Date(filter.date)) + ? new Date(filter.date) + : oneMonthAgoOnSaturday; + return (
@@ -255,7 +248,7 @@ function DateFilter({ {selectValue() === "CUSTOM" ? ( onChange({ date: e.target.value })} max={dateToYYYYMMDD(new Date())} data-testid="date-input" diff --git a/app/features/builds/core/ability-sorting.server.ts b/app/features/builds/core/ability-sorting.server.ts index 915323185..0b0093c35 100644 --- a/app/features/builds/core/ability-sorting.server.ts +++ b/app/features/builds/core/ability-sorting.server.ts @@ -54,24 +54,13 @@ const sortAbilityCount = (a: [Ability, number], b: [Ability, number]) => { return b[1] - a[1]; }; function subAbilitiesSorted(abilities: BuildAbilitiesTuple): Ability[] { - const subAbilitiesUnsorted = [ - abilities[0].slice(1), - abilities[1].slice(1), - abilities[2].slice(1), - ].flat(); + const subAbilitiesUnsorted = abilities.flatMap((row) => row.slice(1)); - const counts = Array.from( - subAbilitiesUnsorted - .reduce((acc, cur) => { - if (!acc.has(cur)) { - acc.set(cur, 1); - } else { - acc.set(cur, acc.get(cur)! + 1); - } - return acc; - }, new Map()) - .entries(), - ).sort(sortAbilityCount); + const countsMap = new Map(); + for (const ability of subAbilitiesUnsorted) { + countsMap.set(ability, (countsMap.get(ability) ?? 0) + 1); + } + const counts = Array.from(countsMap).sort(sortAbilityCount); const subAbilities: Ability[][] = [[], [], []]; while (counts.length > 0) { diff --git a/app/features/builds/core/filter.server.ts b/app/features/builds/core/filter.server.ts index d1c0e5aa2..f28fd223f 100644 --- a/app/features/builds/core/filter.server.ts +++ b/app/features/builds/core/filter.server.ts @@ -73,7 +73,7 @@ function matchesAbilityFilter({ filter, }: { build: PartialBuild; - filter: Omit; + filter: AbilityBuildFilter; }) { if (typeof filter.value === "boolean") { const hasAbility = build.abilities.flat().includes(filter.ability); @@ -94,7 +94,7 @@ function matchesModeFilter({ filter, }: { build: PartialBuild; - filter: Omit; + filter: ModeBuildFilter; }) { if (!build.modes) return false; @@ -106,7 +106,7 @@ function matchesDateFilter({ filter, }: { build: PartialBuild; - filter: Omit; + filter: DateBuildFilter; }) { const date = new Date(filter.date); diff --git a/app/features/builds/loaders/builds.$slug.server.ts b/app/features/builds/loaders/builds.$slug.server.ts index 6e88bd4e6..6a62f1687 100644 --- a/app/features/builds/loaders/builds.$slug.server.ts +++ b/app/features/builds/loaders/builds.$slug.server.ts @@ -6,11 +6,13 @@ import { weaponNameSlugToId } from "~/utils/unslugify.server"; import { mySlugify } from "~/utils/urls"; import * as BuildRepository from "../BuildRepository.server"; import { - BUILDS_PAGE_BATCH_SIZE, BUILDS_PAGE_MAX_BUILDS, FILTER_SEARCH_PARAM_KEY, } from "../builds-constants"; -import { buildFiltersSearchParams } from "../builds-schemas.server"; +import { + buildFiltersSearchParams, + buildsLimitSearchParam, +} from "../builds-schemas.server"; import { filterBuilds } from "../core/filter.server"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { @@ -25,10 +27,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { } const url = new URL(request.url); - const limit = Math.min( - Number(url.searchParams.get("limit") ?? BUILDS_PAGE_BATCH_SIZE), - BUILDS_PAGE_MAX_BUILDS, - ); + const limit = buildsLimitSearchParam.parse(url.searchParams.get("limit")); const weaponName = t(`weapons:MAIN_${weaponId}`); diff --git a/app/features/builds/routes/builds.$slug.test.ts b/app/features/builds/routes/builds.$slug.test.ts new file mode 100644 index 000000000..d7445fe11 --- /dev/null +++ b/app/features/builds/routes/builds.$slug.test.ts @@ -0,0 +1,143 @@ +import { describe, expect, test } from "vitest"; +import { buildFiltersMeaningfullyChanged } from "./builds.$slug"; + +const sp = (filters?: unknown, extra: Record = {}) => { + const params = new URLSearchParams(extra); + if (filters !== undefined) params.set("f", JSON.stringify(filters)); + return params; +}; + +const ability = ( + value: number, + comparison: "AT_LEAST" | "AT_MOST" = "AT_LEAST", + abilityName = "ISM", +) => ({ type: "ability", ability: abilityName, comparison, value }); + +const mode = (modeShort: "SZ" | "TC" | "RM" | "CB") => ({ + type: "mode", + mode: modeShort, +}); + +const date = (dateString: string) => ({ type: "date", date: dateString }); + +describe("buildFiltersMeaningfullyChanged", () => { + test("no filters either side -> not changed", () => { + expect(buildFiltersMeaningfullyChanged(sp(), sp())).toBe(false); + }); + + test("identical filters in same order -> not changed", () => { + expect( + buildFiltersMeaningfullyChanged( + sp([ability(10), mode("SZ")]), + sp([ability(10), mode("SZ")]), + ), + ).toBe(false); + }); + + test("identical filters in different order -> not changed", () => { + expect( + buildFiltersMeaningfullyChanged( + sp([ability(10), mode("SZ")]), + sp([mode("SZ"), ability(10)]), + ), + ).toBe(false); + }); + + test("ability filter value differs -> changed", () => { + expect( + buildFiltersMeaningfullyChanged(sp([ability(10)]), sp([ability(20)])), + ).toBe(true); + }); + + test("different filter count -> changed", () => { + expect( + buildFiltersMeaningfullyChanged( + sp([ability(10)]), + sp([ability(10), mode("SZ")]), + ), + ).toBe(true); + }); + + test("duplicate ability filters with different values are not equal", () => { + // Regression test for the subset-check bug. + // old = [ability(5), ability(10)], new = [ability(10), ability(10)] + // the previous implementation would incorrectly return "not changed". + expect( + buildFiltersMeaningfullyChanged( + sp([ability(5), ability(10)]), + sp([ability(10), ability(10)]), + ), + ).toBe(true); + }); + + test("AT_LEAST 0 ability filter added on the new side -> not changed", () => { + expect( + buildFiltersMeaningfullyChanged( + sp([ability(10)]), + sp([ability(10), ability(0)]), + ), + ).toBe(false); + }); + + test("both sides only have meaningless filters -> not changed", () => { + expect( + buildFiltersMeaningfullyChanged(sp([ability(0)]), sp([ability(0)])), + ).toBe(false); + }); + + test("mode filter changed -> changed", () => { + expect( + buildFiltersMeaningfullyChanged(sp([mode("SZ")]), sp([mode("TC")])), + ).toBe(true); + }); + + test("mode filter same -> not changed", () => { + expect( + buildFiltersMeaningfullyChanged(sp([mode("SZ")]), sp([mode("SZ")])), + ).toBe(false); + }); + + test("date filter changed -> changed", () => { + expect( + buildFiltersMeaningfullyChanged( + sp([date("2026-01-01")]), + sp([date("2026-02-01")]), + ), + ).toBe(true); + }); + + test("ability comparison flipped (AT_LEAST -> AT_MOST) -> changed", () => { + expect( + buildFiltersMeaningfullyChanged( + sp([ability(10, "AT_LEAST")]), + sp([ability(10, "AT_MOST")]), + ), + ).toBe(true); + }); + + test("old has filters, new has none -> changed", () => { + expect(buildFiltersMeaningfullyChanged(sp([ability(10)]), sp())).toBe(true); + }); + + test("old has only meaningless filters, new has none -> not changed", () => { + expect(buildFiltersMeaningfullyChanged(sp([ability(0)]), sp())).toBe(false); + }); + + test("malformed JSON in `f` param does not throw and is treated as empty", () => { + const malformed = new URLSearchParams(); + malformed.set("f", "not-json"); + expect(buildFiltersMeaningfullyChanged(malformed, sp())).toBe(false); + expect(buildFiltersMeaningfullyChanged(sp([ability(10)]), malformed)).toBe( + true, + ); + }); + + test("mixed filter types in different orders -> not changed", () => { + expect( + buildFiltersMeaningfullyChanged( + sp([ability(10), mode("SZ"), date("2026-01-01")]), + sp([date("2026-01-01"), ability(10), mode("SZ")]), + ), + ).toBe(false); + }); +}); diff --git a/app/features/builds/routes/builds.$slug.tsx b/app/features/builds/routes/builds.$slug.tsx index eaf503866..c43b42231 100644 --- a/app/features/builds/routes/builds.$slug.tsx +++ b/app/features/builds/routes/builds.$slug.tsx @@ -6,8 +6,6 @@ import { Funnel, Map as MapIcon, } from "lucide-react"; -import { nanoid } from "nanoid"; -import * as React from "react"; import { useTranslation } from "react-i18next"; import type { MetaFunction } from "react-router"; import { @@ -49,9 +47,63 @@ export { loader }; import styles from "./builds.$slug.module.css"; -const filterOutMeaninglessFilters = ( - filter: Unpacked, -) => { +type ParsedFilter = Unpacked; + +/** + * Returns true if the meaningful build filters in `next` differ from those in `current`. + * Order-insensitive and duplicate-safe; AT_LEAST 0 ability filters are treated as no-ops. + */ +export function buildFiltersMeaningfullyChanged( + current: URLSearchParams, + next: URLSearchParams, +): boolean { + const oldFilters = extractMeaningfulFilters(current); + const newFilters = extractMeaningfulFilters(next); + + return !R.isDeepEqual( + R.sortBy(oldFilters, filterKey), + R.sortBy(newFilters, filterKey), + ); +} + +export const shouldRevalidate: ShouldRevalidateFunction = (args) => { + if (isRevalidation(args)) return true; + + if ( + args.currentUrl.searchParams.get("limit") !== + args.nextUrl.searchParams.get("limit") + ) { + return true; + } + + if ( + buildFiltersMeaningfullyChanged( + args.currentUrl.searchParams, + args.nextUrl.searchParams, + ) + ) { + return args.defaultShouldRevalidate; + } + + return false; +}; + +function parseFiltersFromSearchParams( + searchParams: URLSearchParams, +): BuildFilter[] { + const raw = searchParams.get(FILTER_SEARCH_PARAM_KEY); + if (!raw) return []; + + return safeJSONParse(raw, []); +} + +function extractMeaningfulFilters( + searchParams: URLSearchParams, +): BuildFiltersFromSearchParams { + return parseFiltersFromSearchParams(searchParams).filter(isMeaningfulFilter); +} + +function isMeaningfulFilter(filter: ParsedFilter): boolean { if (filter.type !== "ability") return true; return ( @@ -59,74 +111,13 @@ const filterOutMeaninglessFilters = ( typeof filter.value !== "number" || filter.value > 0 ); -}; -export const shouldRevalidate: ShouldRevalidateFunction = (args) => { - if (isRevalidation(args)) return true; +} - const oldLimit = args.currentUrl.searchParams.get("limit"); - const newLimit = args.nextUrl.searchParams.get("limit"); - - // limit was changed -> revalidate - if (oldLimit !== newLimit) { - return true; - } - - const rawOldFilters = args.currentUrl.searchParams.get( - FILTER_SEARCH_PARAM_KEY, - ); - const oldFilters = rawOldFilters - ? safeJSONParse(rawOldFilters, []).filter( - filterOutMeaninglessFilters, - ) - : null; - const rawNewFilters = args.nextUrl.searchParams.get(FILTER_SEARCH_PARAM_KEY); - const newFilters = rawNewFilters - ? // no safeJSONParse as the value should be coming from app code and should be trustworthy - (JSON.parse(rawNewFilters) as BuildFiltersFromSearchParams).filter( - filterOutMeaninglessFilters, - ) - : null; - - // meaningful filter was added/removed -> revalidate - if (oldFilters && newFilters && oldFilters.length !== newFilters.length) { - return true; - } - // no meaningful filters were or going to be in use -> skip revalidation - if ( - oldFilters && - newFilters && - oldFilters.length === 0 && - newFilters.length === 0 - ) { - return false; - } - // all meaningful filters identical -> skip revalidation - if ( - newFilters?.every((f1) => - oldFilters?.some((f2) => { - if (f1.type !== f2.type) return false; - - if (f1.type === "mode" && f2.type === "mode") { - return f1.mode === f2.mode; - } - if (f1.type === "date" && f2.type === "date") { - return f1.date === f2.date; - } - if (f1.type !== "ability" || f2.type !== "ability") return false; - - return ( - f1.ability === f2.ability && - f1.comparison === f2.comparison && - f1.value === f2.value - ); - }), - ) - ) { - return false; - } - - return args.defaultShouldRevalidate; -}; +function filterKey(filter: ParsedFilter): string { + if (filter.type === "mode") return `mode:${filter.mode}`; + if (filter.type === "date") return `date:${filter.date}`; + return `ability:${filter.ability}:${filter.comparison}:${filter.value}`; +} export const meta: MetaFunction = (args) => { if (!args.data) return []; @@ -185,22 +176,14 @@ export function BuildCards({ data }: { data: SerializeFrom }) { export default function WeaponsBuildsPage() { const data = useLoaderData(); const { t } = useTranslation(["common", "builds"]); - const [, setSearchParams] = useSearchParams(); - const [filters, setFilters] = React.useState( - data.filters ? data.filters.map((f) => ({ ...f, id: nanoid() })) : [], - ); + const [searchParams, setSearchParams] = useSearchParams(); + const filters = parseFiltersFromSearchParams(searchParams); - const filtersForSearchParams = (filters: BuildFilter[]) => - JSON.stringify( - filters.map((f) => { - return R.omit(f, ["id"]); - }), - ); const syncSearchParams = (newFilters: BuildFilter[]) => { setSearchParams( - filtersForSearchParams.length > 0 + newFilters.length > 0 ? { - [FILTER_SEARCH_PARAM_KEY]: filtersForSearchParams(newFilters), + [FILTER_SEARCH_PARAM_KEY]: JSON.stringify(newFilters), } : {}, ); @@ -210,7 +193,6 @@ export default function WeaponsBuildsPage() { const newFilter: BuildFilter = type === "ability" ? { - id: nanoid(), type: "ability", ability: "ISM", comparison: "AT_LEAST", @@ -218,43 +200,32 @@ export default function WeaponsBuildsPage() { } : type === "date" ? { - id: nanoid(), type: "date", date: PATCHES[0].date, } : { - id: nanoid(), type: "mode", mode: "SZ", }; - const newFilters = [...filters, newFilter]; - setFilters(newFilters); - - // no need to sync new ability filter as this doesn't have effect till they make other choices - if (type !== "ability") { - syncSearchParams(newFilters); - } + syncSearchParams([...filters, newFilter]); }; const handleFilterChange = (i: number, newFilter: Partial) => { - const newFilters = structuredClone(filters); - - newFilters[i] = { - ...(filters[i] as AbilityBuildFilter), - ...(newFilter as AbilityBuildFilter), - }; - - setFilters(newFilters); + const newFilters = filters.map((f, index) => + index === i + ? ({ + ...(f as AbilityBuildFilter), + ...(newFilter as AbilityBuildFilter), + } as BuildFilter) + : f, + ); syncSearchParams(newFilters); }; const handleFilterDelete = (i: number) => { - const newFilters = filters.filter((_, index) => index !== i); - setFilters(newFilters); - - syncSearchParams(newFilters); + syncSearchParams(filters.filter((_, index) => index !== i)); }; const loadMoreLink = () => { @@ -263,7 +234,7 @@ export default function WeaponsBuildsPage() { params.set("limit", String(data.limit + BUILDS_PAGE_BATCH_SIZE)); if (filters.length > 0) { - params.set(FILTER_SEARCH_PARAM_KEY, filtersForSearchParams(filters)); + params.set(FILTER_SEARCH_PARAM_KEY, JSON.stringify(filters)); } return `?${params.toString()}`; @@ -338,7 +309,7 @@ export default function WeaponsBuildsPage() {
{filters.map((filter, i) => ( handleFilterChange(i, newFilter)} diff --git a/app/features/calendar/CalendarRepository.server.ts b/app/features/calendar/CalendarRepository.server.ts index 375ff07fa..003fdfabd 100644 --- a/app/features/calendar/CalendarRepository.server.ts +++ b/app/features/calendar/CalendarRepository.server.ts @@ -437,6 +437,7 @@ type CreateArgs = Pick< teamsPerGroup?: number; thirdPlaceMatch?: boolean; requireInGameNames?: boolean; + requireSendouQParticipation?: boolean; isRanked?: boolean; isTest?: boolean; isDraft?: boolean; @@ -481,6 +482,7 @@ export async function create(args: CreateArgs) { autonomousSubs: args.autonomousSubs, regClosesAt: args.regClosesAt, requireInGameNames: args.requireInGameNames, + requireSendouQParticipation: args.requireSendouQParticipation, minMembersPerTeam: args.minMembersPerTeam, maxMembersPerTeam: args.maxMembersPerTeam, swiss: @@ -694,6 +696,7 @@ async function updateTournamentTables( autonomousSubs: args.autonomousSubs, regClosesAt: args.regClosesAt, requireInGameNames: args.requireInGameNames, + requireSendouQParticipation: args.requireSendouQParticipation, minMembersPerTeam: args.minMembersPerTeam, maxMembersPerTeam: args.maxMembersPerTeam, swiss: diff --git a/app/features/calendar/actions/calendar.new.server.ts b/app/features/calendar/actions/calendar.new.server.ts index d17a3b695..7a033fe94 100644 --- a/app/features/calendar/actions/calendar.new.server.ts +++ b/app/features/calendar/actions/calendar.new.server.ts @@ -100,6 +100,7 @@ export const action: ActionFunction = async ({ request }) => { enableNoScreenToggle: data.enableNoScreenToggle ?? undefined, enableSubs: data.enableSubs ?? undefined, requireInGameNames: data.requireInGameNames ?? undefined, + requireSendouQParticipation: data.requireSendouQParticipation ?? undefined, autonomousSubs: data.autonomousSubs ?? undefined, tournamentToCopyId: data.tournamentToCopyId, regClosesAt: data.regClosesAt diff --git a/app/features/calendar/calendar-schemas.server.ts b/app/features/calendar/calendar-schemas.server.ts index d92bfe325..4e2b1b114 100644 --- a/app/features/calendar/calendar-schemas.server.ts +++ b/app/features/calendar/calendar-schemas.server.ts @@ -85,6 +85,10 @@ export const newCalendarEventActionSchema = z checkboxValueToBoolean, z.boolean().nullish(), ), + requireSendouQParticipation: z.preprocess( + checkboxValueToBoolean, + z.boolean().nullish(), + ), minMembersPerTeam: z.preprocess( actualNumber, z.number().int().min(1).max(4).nullish(), diff --git a/app/features/calendar/calendar-schemas.ts b/app/features/calendar/calendar-schemas.ts index 8252df7d0..3db80b10f 100644 --- a/app/features/calendar/calendar-schemas.ts +++ b/app/features/calendar/calendar-schemas.ts @@ -252,6 +252,7 @@ export const bracketProgressionSchema = z.preprocess( .object({ thirdPlaceMatch: z.boolean().optional(), teamsPerGroup: z.number().int().optional(), + hasAbDivisions: z.boolean().optional(), groupCount: z.number().int().optional(), roundCount: z.number().int().optional(), advanceThreshold: z.number().int().optional(), diff --git a/app/features/calendar/calendar-types.ts b/app/features/calendar/calendar-types.ts index 1e4d35d0b..263e67b87 100644 --- a/app/features/calendar/calendar-types.ts +++ b/app/features/calendar/calendar-types.ts @@ -44,6 +44,7 @@ export interface ShowcaseCalendarEvent extends CommonEvent { startTime: number; /** Tournament is hidden from the public (test tournament) */ hidden: boolean; + isFinalized: boolean; minMembersPerTeam: number; firstPlacer: { teamName: string; diff --git a/app/features/calendar/components/BracketProgressionSelector.tsx b/app/features/calendar/components/BracketProgressionSelector.tsx index 6d5e5224c..196dd2490 100644 --- a/app/features/calendar/components/BracketProgressionSelector.tsx +++ b/app/features/calendar/components/BracketProgressionSelector.tsx @@ -318,10 +318,14 @@ function TournamentFormatBracketSelector({ id="teamsPerGroup" disabled={bracket.disabled} > - - - - + {(bracket.settings.hasAbDivisions + ? TOURNAMENT.RR_AB_DIVISIONS_TEAMS_PER_GROUP_OPTIONS + : TOURNAMENT.RR_TEAMS_PER_GROUP_OPTIONS + ).map((n) => ( + + ))} Participants are distributed equally, so groups may have fewer @@ -330,6 +334,44 @@ function TournamentFormatBracketSelector({
) : null} + {bracket.type === "round_robin" && !bracket.sources ? ( +
+ + { + const currentTeamsPerGroup = + bracket.settings.teamsPerGroup ?? + TOURNAMENT.RR_DEFAULT_TEAM_COUNT_PER_GROUP; + + const maxWithoutAb = Math.max( + ...TOURNAMENT.RR_TEAMS_PER_GROUP_OPTIONS, + ); + + let nextTeamsPerGroup = currentTeamsPerGroup; + if (isSelected && currentTeamsPerGroup % 2 !== 0) { + nextTeamsPerGroup = currentTeamsPerGroup + 1; + } else if (!isSelected && currentTeamsPerGroup > maxWithoutAb) { + nextTeamsPerGroup = maxWithoutAb; + } + + updateBracket({ + settings: { + ...bracket.settings, + hasAbDivisions: isSelected, + teamsPerGroup: nextTeamsPerGroup, + }, + }); + }} + isDisabled={bracket.disabled} + /> + + Teams split into A and B pools; every A plays every B once + +
+ ) : null} + {bracket.type === "swiss" ? (
diff --git a/app/features/calendar/routes/calendar.new.tsx b/app/features/calendar/routes/calendar.new.tsx index 39fdd9e9d..4bcd7ebd2 100644 --- a/app/features/calendar/routes/calendar.new.tsx +++ b/app/features/calendar/routes/calendar.new.tsx @@ -259,6 +259,7 @@ function EventForm() { /> {!eventToEdit ? : null} + ) : null} {data.isAddingTournament ? ( @@ -996,6 +997,38 @@ function DraftToggle() { ); } +function AdminOnlySettings() { + const isAdmin = useHasRole("ADMIN"); + + if (!isAdmin) return null; + + return ; +} + +function RequireSendouQParticipationToggle() { + const baseEvent = useBaseEvent(); + const [requireSendouQParticipation, setRequireSendouQParticipation] = + React.useState( + baseEvent?.tournament?.ctx.settings.requireSendouQParticipation ?? false, + ); + const id = React.useId(); + + return ( +
+ + + + Players must have played enough SendouQ matches this season to register + +
+ ); +} + function RegClosesAtSelect() { const baseEvent = useBaseEvent(); const [regClosesAt, setRegClosesAt] = React.useState( diff --git a/app/features/chat/ChatProvider.tsx b/app/features/chat/ChatProvider.tsx index 9876bbfce..a4a52abdc 100644 --- a/app/features/chat/ChatProvider.tsx +++ b/app/features/chat/ChatProvider.tsx @@ -327,8 +327,10 @@ function ChatProviderInner({ ), ); - if (roomCode === activeRoom && chatOpen) { + const isOwnMessage = msg.userId === userId; + if (isOwnMessage || (roomCode === activeRoom && chatOpen)) { writeLastReadCount(roomCode, msg.totalMessageCount); + setUnreadCounts((prev) => ({ ...prev, [roomCode]: 0 })); } else { setUnreadCounts((prev) => ({ ...prev, @@ -402,7 +404,7 @@ function ChatProviderInner({ const messagesForRoom = React.useCallback( (chatCode: string) => { - return (messagesByRoom[chatCode] ?? []).sort( + return (messagesByRoom[chatCode] ?? []).toSorted( (a, b) => a.timestamp - b.timestamp, ); }, diff --git a/app/features/chat/chat-utils.ts b/app/features/chat/chat-utils.ts index 9cc422422..316989bb8 100644 --- a/app/features/chat/chat-utils.ts +++ b/app/features/chat/chat-utils.ts @@ -3,8 +3,10 @@ import type { ChatMessage } from "./chat-types"; const STAFF_EXTRA_DAYS = 7; +/** Should a chat room be still accessible via chat code. */ export function chatAccessible(args: { - isStaff: boolean; + /** Is the user site staff? Allows them to see the chat code for extra days. */ + isStaff?: boolean; expiresAfterDays: number; comparedTo: Date; }): boolean { diff --git a/app/features/comp-analyzer/components/SelectedWeapons.browser.test.tsx b/app/features/comp-analyzer/components/SelectedWeapons.browser.test.tsx index 1a94d9335..4acbadaec 100644 --- a/app/features/comp-analyzer/components/SelectedWeapons.browser.test.tsx +++ b/app/features/comp-analyzer/components/SelectedWeapons.browser.test.tsx @@ -8,6 +8,7 @@ import { SelectedWeapons } from "./SelectedWeapons"; const defaultProps: ComponentProps = { selectedWeaponIds: [], onRemove: vi.fn(), + onReorder: vi.fn(), }; function renderSelectedWeapons( @@ -100,9 +101,7 @@ describe("SelectedWeapons", () => { selectedWeaponIds: [0], }); - const removeButton = screen.getByRole("button", { - name: "Remove weapon", - }); + const removeButton = screen.getByTestId("remove-weapon-0"); await expect.element(removeButton).toBeVisible(); }); diff --git a/app/features/comp-analyzer/components/SelectedWeapons.module.css b/app/features/comp-analyzer/components/SelectedWeapons.module.css index 035001244..144e3bbfd 100644 --- a/app/features/comp-analyzer/components/SelectedWeapons.module.css +++ b/app/features/comp-analyzer/components/SelectedWeapons.module.css @@ -8,6 +8,11 @@ display: flex; align-items: center; gap: var(--s-2); + transition: opacity 0.2s; + + &.isDragging { + opacity: 0.5; + } } .weaponImageContainer { @@ -100,6 +105,28 @@ } } +.dragHandle { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + padding: 0; + background: none; + border: none; + font-size: var(--font-md); + line-height: 1; + color: var(--color-text-high); + cursor: grab; + user-select: none; + touch-action: none; + flex-shrink: 0; + + &:active { + cursor: grabbing; + } +} + .subSpecialContainer { display: flex; gap: var(--s-2); diff --git a/app/features/comp-analyzer/components/SelectedWeapons.tsx b/app/features/comp-analyzer/components/SelectedWeapons.tsx index e511d53a8..35b7328d3 100644 --- a/app/features/comp-analyzer/components/SelectedWeapons.tsx +++ b/app/features/comp-analyzer/components/SelectedWeapons.tsx @@ -1,3 +1,20 @@ +import type { DragEndEvent } from "@dnd-kit/core"; +import { + closestCenter, + DndContext, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import clsx from "clsx"; import { useTranslation } from "react-i18next"; import { Image, WeaponImage } from "~/components/Image"; import { mainWeaponParams } from "~/features/build-analyzer/core/utils"; @@ -13,81 +30,163 @@ import styles from "./SelectedWeapons.module.css"; interface SelectedWeaponsProps { selectedWeaponIds: MainWeaponId[]; onRemove: (index: number) => void; + onReorder: (newIds: MainWeaponId[]) => void; } export function SelectedWeapons({ selectedWeaponIds, onRemove, + onReorder, }: SelectedWeaponsProps) { const { t } = useTranslation(["weapons", "analyzer"]); - const slots = Array.from({ length: MAX_WEAPONS }, (_, i) => { - return selectedWeaponIds[i] ?? null; - }); + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + const oldIndex = selectedWeaponIds.indexOf(active.id as MainWeaponId); + const newIndex = selectedWeaponIds.indexOf(over.id as MainWeaponId); + + const newIds = [...selectedWeaponIds]; + const [removed] = newIds.splice(oldIndex, 1); + newIds.splice(newIndex, 0, removed); + + onReorder(newIds); + } + }; + + const emptySlotCount = MAX_WEAPONS - selectedWeaponIds.length; + const showDragHandle = selectedWeaponIds.length > 1; return (
- {slots.map((weaponId, index) => { - if (weaponId === null) { - return ( -
-
- -
-
- - {t("analyzer:comp.pickWeapon")} - -
-
-
- ); - } - - const params = mainWeaponParams(weaponId); - - return ( -
-
- -
-
- - {t(`weapons:MAIN_${weaponId}`)} - - -
-
-
- {t(`weapons:SUB_${params.subWeaponId}`)} -
-
- {t(`weapons:SPECIAL_${params.specialWeaponId}`)} -
-
+ + + {selectedWeaponIds.map((weaponId, index) => ( + + ))} + + + {Array.from({ length: emptySlotCount }, (_, i) => ( +
+
+
- ); - })} +
+ + {t("analyzer:comp.pickWeapon")} + +
+
+
+ ))} +
+ ); +} + +interface SortableWeaponRowProps { + weaponId: MainWeaponId; + index: number; + onRemove: (index: number) => void; + showDragHandle: boolean; +} + +function SortableWeaponRow({ + weaponId, + index, + onRemove, + showDragHandle, +}: SortableWeaponRowProps) { + const { t } = useTranslation(["weapons", "analyzer"]); + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: weaponId }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + const params = mainWeaponParams(weaponId); + + return ( +
+
+ +
+
+ + {t(`weapons:MAIN_${weaponId}`)} + + {showDragHandle ? ( + + ) : null} + +
+
+
+ {t(`weapons:SUB_${params.subWeaponId}`)} +
+
+ {t(`weapons:SPECIAL_${params.specialWeaponId}`)} +
+
); } diff --git a/app/features/comp-analyzer/core/weapon-range.ts b/app/features/comp-analyzer/core/weapon-range.ts index d94fa91f0..073addb13 100644 --- a/app/features/comp-analyzer/core/weapon-range.ts +++ b/app/features/comp-analyzer/core/weapon-range.ts @@ -159,7 +159,7 @@ export interface WeaponRangeResult { trajectory?: TrajectoryPoint[]; } -function getWeaponRange(weaponId: MainWeaponId): WeaponRangeResult { +export function getWeaponRange(weaponId: MainWeaponId): WeaponRangeResult { const category = getWeaponCategoryName(weaponId); if (!category) { diff --git a/app/features/comp-analyzer/routes/comp-analyzer.tsx b/app/features/comp-analyzer/routes/comp-analyzer.tsx index 19eee3712..e7b7ed91b 100644 --- a/app/features/comp-analyzer/routes/comp-analyzer.tsx +++ b/app/features/comp-analyzer/routes/comp-analyzer.tsx @@ -78,6 +78,7 @@ function CompAnalyzerPage() { ("ALL"); - const nowUnixLive = useNowUnix(); + const nowUnixLive = useNowUnix(data.now); const allInThePast = data.rotations.every( (rotation) => rotation.endTime <= nowUnixLive, @@ -48,8 +47,6 @@ export function SplatoonRotations() { if (allInThePast || data.rotations.length === 0) return null; - const nowUnix = databaseTimestampNow(); - const rotationsByType = new Map< string, { @@ -63,8 +60,8 @@ export function SplatoonRotations() { if (activeFilter !== "ALL" && rotation.mode !== activeFilter) continue; const isCurrent = - rotation.startTime <= nowUnix && rotation.endTime > nowUnix; - const isNext = rotation.startTime > nowUnix; + rotation.startTime <= nowUnixLive && rotation.endTime > nowUnixLive; + const isNext = rotation.startTime > nowUnixLive; if (!isCurrent && !isNext) continue; @@ -130,12 +127,11 @@ export function SplatoonRotations() { ); } -function useNowUnix() { - const [now, setNow] = React.useState(() => - dateToDatabaseTimestamp(new Date()), - ); +function useNowUnix(initialNow: number) { + const [now, setNow] = React.useState(initialNow); React.useEffect(() => { + setNow(dateToDatabaseTimestamp(new Date())); const interval = setInterval(() => { setNow(dateToDatabaseTimestamp(new Date())); }, 60_000); diff --git a/app/features/front-page/core/ShowcaseTournaments.server.ts b/app/features/front-page/core/ShowcaseTournaments.server.ts index c7c791027..2064eef28 100644 --- a/app/features/front-page/core/ShowcaseTournaments.server.ts +++ b/app/features/front-page/core/ShowcaseTournaments.server.ts @@ -183,8 +183,12 @@ async function cachedTournaments() { } function deleteExtraResults(tournaments: ShowcaseCalendarEvent[]) { + const threeDaysAgo = databaseTimestampThreeDaysAgo(); const nonResults = tournaments.filter( - (tournament) => !tournament.firstPlacer, + (tournament) => + !tournament.firstPlacer && + !tournament.isFinalized && + tournament.startTime > threeDaysAgo, ); const rankedResults = tournaments @@ -312,6 +316,7 @@ function mapTournamentFromDB( tier: tournament.tier ?? null, tentativeTier, hidden: Boolean(tournament.hidden), + isFinalized: Boolean(tournament.isFinalized), minMembersPerTeam: tournament.settings.minMembersPerTeam ?? 4, modes: null, hasVods: (tournament.vodCount ?? 0) > 0, @@ -377,6 +382,14 @@ function databaseTimestampWeekFromNow() { return dateToDatabaseTimestamp(now); } +function databaseTimestampThreeDaysAgo() { + const now = new Date(); + + now.setDate(now.getDate() - 3); + + return dateToDatabaseTimestamp(now); +} + function databaseTimestampSixHoursAgo() { const now = new Date(); diff --git a/app/features/front-page/loaders/index.server.ts b/app/features/front-page/loaders/index.server.ts index 0d3fc74c3..6e4de50b4 100644 --- a/app/features/front-page/loaders/index.server.ts +++ b/app/features/front-page/loaders/index.server.ts @@ -8,6 +8,7 @@ import * as Seasons from "~/features/mmr/core/Seasons"; import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server"; import * as SplatoonRotationRepository from "~/features/splatoon-rotations/SplatoonRotationRepository.server"; import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server"; +import { databaseTimestampNow } from "~/utils/dates"; import type { SerializeFrom } from "~/utils/remix"; import { discordAvatarUrl, teamPage, userPage } from "~/utils/urls"; import * as ShowcaseTournaments from "../core/ShowcaseTournaments.server"; @@ -44,6 +45,7 @@ export const loader = async () => { leaderboards, rotations, weaponPool, + now: databaseTimestampNow(), }; }; diff --git a/app/features/img-upload/actions/upload.server.ts b/app/features/img-upload/actions/upload.server.ts index fe05a411d..4afa1da6f 100644 --- a/app/features/img-upload/actions/upload.server.ts +++ b/app/features/img-upload/actions/upload.server.ts @@ -18,7 +18,10 @@ import { import { teamPage, tournamentOrganizationPage } from "~/utils/urls"; import * as ImageRepository from "../ImageRepository.server"; import { uploadStreamToS3 } from "../s3.server"; -import { MAX_UNVALIDATED_IMG_COUNT } from "../upload-constants"; +import { + ALLOWED_IMAGE_EXTENSIONS, + MAX_UNVALIDATED_IMG_COUNT, +} from "../upload-constants"; import { requestToImgType } from "../upload-utils"; export const action = async ({ request }: ActionFunctionArgs) => { @@ -44,8 +47,12 @@ export const action = async ({ request }: ActionFunctionArgs) => { const uploadHandler = async (fileUpload: FileUpload) => { if (fileUpload.fieldName === "img") { - const [, ending] = fileUpload.name.split("."); - invariant(ending); + const ending = fileUpload.name.split(".").pop()?.toLowerCase(); + invariant(ending && ending !== fileUpload.name); + invariant( + ALLOWED_IMAGE_EXTENSIONS.includes(ending), + `Invalid file extension: "${ending}"`, + ); const newFilename = `img-${Date.now()}.${ending}`; const uploadedFileLocation = await uploadStreamToS3( diff --git a/app/features/img-upload/upload-constants.ts b/app/features/img-upload/upload-constants.ts index ed6a66ef5..c52223702 100644 --- a/app/features/img-upload/upload-constants.ts +++ b/app/features/img-upload/upload-constants.ts @@ -1,5 +1,7 @@ import type { ImageUploadType } from "./upload-types"; +export const ALLOWED_IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "webp"]; + export const MAX_UNVALIDATED_IMG_COUNT = 5; export const IMAGES_TO_VALIDATE_AT_ONCE = 5; diff --git a/app/features/info/routes/contributions.tsx b/app/features/info/routes/contributions.tsx index 106fb2b21..dfcf6e1ad 100644 --- a/app/features/info/routes/contributions.tsx +++ b/app/features/info/routes/contributions.tsx @@ -23,6 +23,7 @@ export const handle: SendouRouteHandle = { }; const PROGRAMMERS = [ + "hfcRed", "DoubleCookies", "ElementUser", "remmycat", diff --git a/app/features/leaderboards/LeaderboardRepository.server.ts b/app/features/leaderboards/LeaderboardRepository.server.ts index 9667c824e..435e5db9c 100644 --- a/app/features/leaderboards/LeaderboardRepository.server.ts +++ b/app/features/leaderboards/LeaderboardRepository.server.ts @@ -180,6 +180,45 @@ async function userIdsWithEnoughSqMatchesForTeamLeaderboard(seasonNth: number) { .map(([userId]) => userId); } +export async function userHasEnoughSqMatches(userId: number) { + const season = Seasons.currentOrPrevious(); + if (!season) return false; + + const dateRange = Seasons.nthToDateRange(season.nth); + if (!dateRange) return false; + + const rows = await db + .selectFrom("GroupMatch") + .innerJoin("GroupMember", (join) => + join.on((eb) => + eb.or([ + eb("GroupMatch.alphaGroupId", "=", eb.ref("GroupMember.groupId")), + eb("GroupMatch.bravoGroupId", "=", eb.ref("GroupMember.groupId")), + ]), + ), + ) + .innerJoin("Skill", (join) => + join + .onRef("Skill.groupMatchId", "=", "GroupMatch.id") + .onRef("Skill.userId", "=", "GroupMember.userId"), + ) + .where("GroupMember.userId", "=", userId) + .where( + "GroupMatch.createdAt", + ">", + dateToDatabaseTimestamp(dateRange.starts), + ) + .where( + "GroupMatch.createdAt", + "<", + dateToDatabaseTimestamp(add(dateRange.ends, { days: 1 })), + ) + .select(db.fn.countAll().as("count")) + .executeTakeFirstOrThrow(); + + return rows.count >= MATCHES_COUNT_NEEDED_FOR_LEADERBOARD; +} + function filterOneEntryPerUser( entries: TeamLeaderboardBySeasonQueryReturnType, ) { diff --git a/app/features/map-planner/components/Planner.module.css b/app/features/map-planner/components/Planner.module.css index a58eb34fe..eb1c69405 100644 --- a/app/features/map-planner/components/Planner.module.css +++ b/app/features/map-planner/components/Planner.module.css @@ -1,7 +1,7 @@ .topWrapper { position: fixed; z-index: 10; - top: 0; + top: env(safe-area-inset-top, 0); left: 50%; transform: translateX(-50%); display: flex; diff --git a/app/features/map-planner/components/Planner.tsx b/app/features/map-planner/components/Planner.tsx index fec1133b0..9825679e3 100644 --- a/app/features/map-planner/components/Planner.tsx +++ b/app/features/map-planner/components/Planner.tsx @@ -28,15 +28,23 @@ import { ChevronRight, ChevronUp, LogOut, + Radius, + Square, } from "lucide-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; +import { getWeaponRange } from "~/features/comp-analyzer/core/weapon-range"; import { useTheme } from "~/features/theme/core/provider"; import type { LanguageCode } from "~/modules/i18n/config"; import { modesShort } from "~/modules/in-game-lists/modes"; import { stageIds } from "~/modules/in-game-lists/stage-ids"; -import type { ModeShort, StageId } from "~/modules/in-game-lists/types"; +import type { + MainWeaponId, + ModeShort, + StageId, +} from "~/modules/in-game-lists/types"; import { + mainWeaponIds, specialWeaponIds, subWeaponIds, weaponCategories, @@ -53,12 +61,16 @@ import { } from "~/utils/urls"; import { LinkButton, SendouButton } from "../../../components/elements/Button"; import { Image } from "../../../components/Image"; -import type { StageBackgroundStyle } from "../plans-types"; import styles from "./Planner.module.css"; const DROPPED_IMAGE_SIZE_PX = 45; const BACKGROUND_WIDTH = 1127; const BACKGROUND_HEIGHT = 634; +const GAME_UNITS_TO_PX: Record<"MINI" | "OVER", number> = { + MINI: 4.4, + OVER: 8.4, +}; +const MAIN_WEAPON_URL_PATTERN = /main-weapons-outlined\/(\d+)/; export default function Planner() { const { t, i18n } = useTranslation(["common"]); @@ -70,6 +82,11 @@ export default function Planner() { const [imgOutlined, setImgOutlined] = React.useState(false); const [topCollapsed, setTopCollapsed] = React.useState(false); const [weaponsCollapsed, setWeaponsCollapsed] = React.useState(false); + const [rangesVisible, setRangesVisible] = React.useState(false); + const [backgroundStyle, setBackgroundStyle] = React.useState<"MINI" | "OVER">( + "MINI", + ); + const rangeCleanupRef = React.useRef<(() => void) | null>(null); const [activeDragItem, setActiveDragItem] = React.useState<{ src: string; previewPath: string; @@ -200,11 +217,103 @@ export default function Planner() { handleAddWeaponAtPosition(src, [pagePoint.x, pagePoint.y]); }; + const handleRangeToggle = () => { + if (!editor) return; + + if (rangesVisible) { + rangeCleanupRef.current?.(); + rangeCleanupRef.current = null; + removeRangeCircles(editor); + setRangesVisible(false); + } else { + const gameUnitsToPx = GAME_UNITS_TO_PX[backgroundStyle]; + removeRangeCircles(editor); + for (const shape of editor.getCurrentPageShapes()) { + createRangeCircleForShape(editor, shape, gameUnitsToPx); + } + + const unsubCreate = editor.sideEffects.registerAfterCreateHandler( + "shape", + (shape) => { + if (shape.meta.isRangeCircle) return; + createRangeCircleForShape(editor, shape, gameUnitsToPx); + }, + ); + + const unsubChange = editor.sideEffects.registerAfterChangeHandler( + "shape", + (_prev, next) => { + if (next.meta.isRangeCircle) return; + + const rangeCircles = editor + .getCurrentPageShapes() + .filter( + (s) => + s.meta.isRangeCircle === true && + s.meta.weaponShapeId === next.id, + ); + if (rangeCircles.length === 0) return; + + const centerX = next.x + (next.props as { w: number }).w / 2; + const centerY = next.y + (next.props as { h: number }).h / 2; + + for (const rangeCircle of rangeCircles) { + const radiusPx = (rangeCircle.props as { w: number }).w / 2; + editor.updateShape({ + id: rangeCircle.id, + type: rangeCircle.type, + isLocked: false, + }); + editor.updateShape({ + id: rangeCircle.id, + type: rangeCircle.type, + x: centerX - radiusPx, + y: centerY - radiusPx, + isLocked: true, + }); + } + }, + ); + + const unsubDelete = editor.sideEffects.registerAfterDeleteHandler( + "shape", + (shape) => { + if (shape.meta.isRangeCircle) return; + + const rangeCircles = editor + .getCurrentPageShapes() + .filter( + (s) => + s.meta.isRangeCircle === true && + s.meta.weaponShapeId === shape.id, + ); + if (rangeCircles.length === 0) return; + + for (const rangeCircle of rangeCircles) { + editor.updateShape({ + id: rangeCircle.id, + type: rangeCircle.type, + isLocked: false, + }); + } + editor.deleteShapes(rangeCircles); + }, + ); + + rangeCleanupRef.current = () => { + unsubCreate(); + unsubChange(); + unsubDelete(); + }; + setRangesVisible(true); + } + }; + const handleAddBackgroundImage = React.useCallback( (urlArgs: { stageId: StageId; mode: ModeShort; - style: StageBackgroundStyle; + style: "MINI" | "OVER"; }) => { if (!editor) return; @@ -225,6 +334,10 @@ export default function Planner() { }); editor.zoomToFit(); + rangeCleanupRef.current?.(); + rangeCleanupRef.current = null; + setRangesVisible(false); + setBackgroundStyle(urlArgs.style); }, [editor, handleAddImage], ); @@ -293,6 +406,7 @@ export default function Planner() { outlined={imgOutlined} setImgOutlined={setImgOutlined} /> +
+ {number}. + { + if (key === null) return; + const [mode, stageId] = String(key).split("-"); + onMapChange({ + mode: mode as ModeShort, + stageId: Number(stageId) as StageId, + }); + }} + search={{ + placeholder: t("common:forms.stageSearch.search.placeholder"), + }} + className={styles.mapRowSelect} + popoverClassName={styles.mapRowSelectPopover} + > + {(group) => ( + + {group.maps.map((m) => ( + +
+ + + {m.name} +
+
+ ))} +
+ )} +
); } diff --git a/app/features/tournament-bracket/components/MatchMapInfo.tsx b/app/features/tournament-bracket/components/MatchMapInfo.tsx index e1a1f5c82..a53055e93 100644 --- a/app/features/tournament-bracket/components/MatchMapInfo.tsx +++ b/app/features/tournament-bracket/components/MatchMapInfo.tsx @@ -3,6 +3,7 @@ import { useLoaderData } from "react-router"; import { ModeImage, StageImage } from "~/components/Image"; import type { CustomPickBanStep } from "~/db/tables"; import { useTournament } from "~/features/tournament/routes/to.$id"; +import * as PickBan from "~/features/tournament-bracket/core/PickBan"; import type { ModeShort, StageId } from "~/modules/in-game-lists/types"; import type { TournamentMatchLoaderData } from "../loaders/to.$id.matches.$mid.server"; import styles from "./MatchMapInfo.module.css"; @@ -17,6 +18,11 @@ export function MatchMapInfo({ teams }: { teams: [number, number] }) { const customFlow = data.match.roundMaps?.customFlow; if (!customFlow) return null; + const pickBanTeams: [PickBan.PickBanTeam, PickBan.PickBanTeam] = [ + { id: teams[0], seed: teamOne?.seed ?? 0 }, + { id: teams[1], seed: teamTwo?.seed ?? 0 }, + ]; + const teamOneBans: BanEvent[] = []; const teamTwoBans: BanEvent[] = []; @@ -28,7 +34,7 @@ export function MatchMapInfo({ teams }: { teams: [number, number] }) { eventIndex: i, preSet: customFlow.preSet, postGame: customFlow.postGame, - teams, + teams: pickBanTeams, results: data.results, }); @@ -60,7 +66,7 @@ function resolveTeamForEvent({ eventIndex: number; preSet: CustomPickBanStep[]; postGame: CustomPickBanStep[]; - teams: [number, number]; + teams: [PickBan.PickBanTeam, PickBan.PickBanTeam]; results: Array<{ winnerTeamId: number }>; }): number | null { const step = @@ -70,27 +76,27 @@ function resolveTeamForEvent({ if (!step?.side) return null; - switch (step.side) { - case "ALPHA": - return teams[0]; - case "BRAVO": - return teams[1]; - case "HIGHER_SEED": - return teams[1]; - case "LOWER_SEED": - return teams[0]; - case "WINNER": - case "LOSER": { - const cycleIndex = Math.floor( - (eventIndex - preSet.length) / postGame.length, - ); - const result = results[cycleIndex]; - if (!result) return null; + // PickBan.resolveTeamFromSide uses the last element of results for WINNER/LOSER, + // but here we iterate over all historical events so we need to slice + // results to the correct post-game cycle + if (step.side === "WINNER" || step.side === "LOSER") { + const cycleIndex = Math.floor( + (eventIndex - preSet.length) / postGame.length, + ); + if (!results[cycleIndex]) return null; - if (step.side === "WINNER") return result.winnerTeamId; - return teams.find((t) => t !== result.winnerTeamId) ?? null; - } + return PickBan.resolveTeamFromSide({ + side: step.side, + teams, + results: results.slice(0, cycleIndex + 1), + }); } + + return PickBan.resolveTeamFromSide({ + side: step.side, + teams, + results, + }); } interface BanEvent { diff --git a/app/features/tournament-bracket/core/AbDivisions.test.ts b/app/features/tournament-bracket/core/AbDivisions.test.ts new file mode 100644 index 000000000..9c62b82bd --- /dev/null +++ b/app/features/tournament-bracket/core/AbDivisions.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from "vitest"; +import * as AbDivisions from "./AbDivisions"; + +describe("AbDivisions.validate", () => { + it("accepts a balanced 12-team single-group configuration", () => { + const result = AbDivisions.validate({ + abDivisionsBySeedOrder: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], + groupCount: 1, + }); + + expect(result.isOk()).toBe(true); + expect(result._unsafeUnwrap()).toEqual([ + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + ]); + }); + + it("accepts a balanced 12-team two-group configuration", () => { + const result = AbDivisions.validate({ + abDivisionsBySeedOrder: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1], + groupCount: 2, + }); + + expect(result.isOk()).toBe(true); + }); + + it("rejects any unassigned team", () => { + const result = AbDivisions.validate({ + abDivisionsBySeedOrder: [0, 1, 0, 1, 0, null, 0, 1, 0, 1, 0, 1], + groupCount: 1, + }); + + expect(result.isErr()).toBe(true); + expect(result._unsafeUnwrapErr()).toMatch(/assigned/); + }); + + it("rejects invalid division values", () => { + const result = AbDivisions.validate({ + abDivisionsBySeedOrder: [0, 1, 0, 1, 0, 2, 0, 1, 0, 1, 0, 1], + groupCount: 1, + }); + + expect(result.isErr()).toBe(true); + }); + + it("rejects A/B counts differing by more than 1", () => { + const result = AbDivisions.validate({ + abDivisionsBySeedOrder: [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1], + groupCount: 1, + }); + + expect(result.isErr()).toBe(true); + expect(result._unsafeUnwrapErr()).toMatch(/7 A, 5 B/); + }); + + it("accepts a ±1 uneven configuration with a single group", () => { + const result = AbDivisions.validate({ + abDivisionsBySeedOrder: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0], + groupCount: 1, + }); + + expect(result.isOk()).toBe(true); + }); + + it("rejects a ±1 uneven configuration when there are multiple groups", () => { + const result = AbDivisions.validate({ + abDivisionsBySeedOrder: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0], + groupCount: 2, + }); + + expect(result.isErr()).toBe(true); + expect(result._unsafeUnwrapErr()).toMatch(/single group/); + }); + + it("rejects team counts not divisible by group count", () => { + const result = AbDivisions.validate({ + abDivisionsBySeedOrder: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1], + groupCount: 3, + }); + + expect(result.isErr()).toBe(true); + expect(result._unsafeUnwrapErr()).toMatch(/10 checked-in teams into 3/); + }); + + it("rejects odd per-group team counts", () => { + const result = AbDivisions.validate({ + abDivisionsBySeedOrder: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1], + groupCount: 2, + }); + + expect(result.isErr()).toBe(true); + expect(result._unsafeUnwrapErr()).toMatch(/5 teams/); + }); + + it("preserves the original order of the divisions", () => { + const divisions = [1, 0, 1, 0, 0, 1, 1, 0]; + + const result = AbDivisions.validate({ + abDivisionsBySeedOrder: divisions, + groupCount: 2, + }); + + expect(result._unsafeUnwrap()).toEqual(divisions); + }); +}); + +describe("AbDivisions.countByDivision", () => { + it("counts A, B, and unassigned separately", () => { + const counts = AbDivisions.countByDivision([ + { abDivision: 0 }, + { abDivision: 0 }, + { abDivision: 1 }, + { abDivision: null }, + { abDivision: null }, + { abDivision: null }, + ]); + + expect(counts).toEqual({ a: 2, b: 1, unassigned: 3 }); + }); +}); diff --git a/app/features/tournament-bracket/core/AbDivisions.ts b/app/features/tournament-bracket/core/AbDivisions.ts new file mode 100644 index 000000000..936c7e632 --- /dev/null +++ b/app/features/tournament-bracket/core/AbDivisions.ts @@ -0,0 +1,85 @@ +import { err, ok, type Result } from "neverthrow"; + +interface ValidateArgs { + abDivisionsBySeedOrder: (number | null | undefined)[]; + groupCount: number; +} + +/** + * Validates that the checked-in teams are ready to start a bipartite (A/B) round robin bracket. + * + * Returns the division assignments parallel to the seeding order on success, or an error message + * suitable for surfacing to the organizer if any of the following are violated: + * + * - Every team has an A (0) or B (1) assignment + * - The counts of A and B teams differ by at most 1 + * - When the counts differ by 1, there must be only one group (uneven divisions can't be split + * evenly across multiple groups) + * - When the counts are equal, the total team count splits evenly across the groups and each + * group's team count is even (so A and B can be balanced within the group) + */ +export function validate({ + abDivisionsBySeedOrder, + groupCount, +}: ValidateArgs): Result<(0 | 1)[], string> { + const teamCount = abDivisionsBySeedOrder.length; + + const missingAssignment = abDivisionsBySeedOrder.some( + (division) => division !== 0 && division !== 1, + ); + if (missingAssignment) { + return err( + "Every checked-in team must be assigned to A or B before starting the bracket", + ); + } + + const aCount = abDivisionsBySeedOrder.filter( + (division) => division === 0, + ).length; + const bCount = abDivisionsBySeedOrder.filter( + (division) => division === 1, + ).length; + const diff = Math.abs(aCount - bCount); + + if (diff > 1) { + return err( + `Unbalanced A/B divisions (${aCount} A, ${bCount} B) — counts can differ by at most 1`, + ); + } + + if (diff === 1) { + if (groupCount !== 1) { + return err( + `Uneven A/B divisions (${aCount} A, ${bCount} B) are only supported with a single group`, + ); + } + + return ok(abDivisionsBySeedOrder as (0 | 1)[]); + } + + if (teamCount % groupCount !== 0) { + return err( + `Can't evenly distribute ${teamCount} checked-in teams into ${groupCount} groups`, + ); + } + + const teamsPerGroup = teamCount / groupCount; + if (teamsPerGroup % 2 !== 0) { + return err( + `Each group would have ${teamsPerGroup} teams — must be even for A/B divisions`, + ); + } + + return ok(abDivisionsBySeedOrder as (0 | 1)[]); +} + +/** Counts checked-in teams by division. Unassigned teams are excluded. */ +export function countByDivision(teams: { abDivision: number | null }[]) { + const a = teams.filter((team) => team.abDivision === 0).length; + const b = teams.filter((team) => team.abDivision === 1).length; + const unassigned = teams.filter( + (team) => team.abDivision !== 0 && team.abDivision !== 1, + ).length; + + return { a, b, unassigned }; +} diff --git a/app/features/tournament-bracket/core/Bracket.test.ts b/app/features/tournament-bracket/core/Bracket.test.ts index d0e2d9e41..88d1ef665 100644 --- a/app/features/tournament-bracket/core/Bracket.test.ts +++ b/app/features/tournament-bracket/core/Bracket.test.ts @@ -1,11 +1,13 @@ import * as R from "remeda"; import { describe, expect, it } from "vitest"; +import { BracketsManager } from "~/modules/brackets-manager"; +import { InMemoryDatabase } from "~/modules/brackets-memory-db"; import invariant from "../../../utils/invariant"; import * as Swiss from "../core/Swiss"; import { Tournament } from "./Tournament"; import { PADDLING_POOL_255 } from "./tests/mocks"; import { LOW_INK_DECEMBER_2024 } from "./tests/mocks-li"; -import { testTournament } from "./tests/test-utils"; +import { testTournament, tournamentCtxTeam } from "./tests/test-utils"; const TEAM_ERROR_404_ID = 17354; const TEAM_THIS_IS_FINE_ID = 17513; @@ -168,3 +170,124 @@ describe("round robin standings", () => { } }); }); + +describe("round robin A/B divisions standings", () => { + const abDivisionsTournament = () => { + const storage = new InMemoryDatabase(); + const manager = new BracketsManager(storage); + + manager.create({ + name: "AB RR", + tournamentId: 1, + type: "round_robin", + seeding: [1, 2, 3, 4], + abDivisions: [0, 1, 0, 1], + settings: { + groupCount: 1, + hasAbDivisions: true, + seedOrdering: ["groups.seed_optimized"], + }, + }); + + const setResult = ( + matchId: number, + winnerId: number, + winnerScore: number, + loserScore: number, + ) => { + const match = storage.select("match", matchId); + invariant(match, `match ${matchId} not found`); + const winnerIsOpp1 = match.opponent1.id === winnerId; + manager.update.match({ + id: match.id, + opponent1: winnerIsOpp1 + ? { score: winnerScore, result: "win" } + : { score: loserScore }, + opponent2: winnerIsOpp1 + ? { score: loserScore } + : { score: winnerScore, result: "win" }, + }); + }; + + const winnerByMatchup: Record = { + "1-2": 1, + "1-4": 1, + "2-3": 2, + "3-4": 3, + }; + for (const match of storage.select("match")!) { + const a = match.opponent1.id as number; + const b = match.opponent2.id as number; + const key = a < b ? `${a}-${b}` : `${b}-${a}`; + const winnerId = winnerByMatchup[key]; + invariant(winnerId, `unexpected matchup ${key}`); + const loserScore = key === "2-3" || key === "3-4" ? 1 : 0; + setResult(match.id, winnerId, 2, loserScore); + } + + const data = manager.get.tournamentData(1); + + return testTournament({ + ctx: { + settings: { + bracketProgression: [ + { + type: "round_robin", + name: "AB RR", + requiresCheckIn: false, + settings: { hasAbDivisions: true }, + }, + ], + }, + teams: [ + tournamentCtxTeam(1, { abDivision: 0, seed: 1 }), + tournamentCtxTeam(2, { abDivision: 1, seed: 2 }), + tournamentCtxTeam(3, { abDivision: 0, seed: 3 }), + tournamentCtxTeam(4, { abDivision: 1, seed: 4 }), + ], + }, + data, + }); + }; + + it("filtering by abDivision preserves standard tiebreaker order within each division", () => { + const tournament = abDivisionsTournament(); + const standings = tournament.bracketByIdx(0)!.currentStandings(true); + + expect(standings.map((s) => s.team.id)).toEqual([1, 2, 3, 4]); + + const divisionA = standings.filter((s) => s.team.abDivision === 0); + const divisionB = standings.filter((s) => s.team.abDivision === 1); + + expect(divisionA.map((s) => s.team.id)).toEqual([1, 3]); + expect(divisionB.map((s) => s.team.id)).toEqual([2, 4]); + }); + + it("source({ placements: [1] }) returns top team from each division", () => { + const tournament = abDivisionsTournament(); + const { teams } = tournament.bracketByIdx(0)!.source({ placements: [1] }); + + expect(teams).toEqual([1, 2]); + }); + + it("source({ placements: [1, 2] }) returns top two teams from each division", () => { + const tournament = abDivisionsTournament(); + const { teams } = tournament + .bracketByIdx(0)! + .source({ placements: [1, 2] }); + + expect(teams).toHaveLength(4); + expect(new Set(teams)).toEqual(new Set([1, 2, 3, 4])); + expect(teams.slice(0, 2)).toEqual([1, 3]); + expect(teams.slice(2, 4)).toEqual([2, 4]); + }); + + it("source ignores placements beyond division size", () => { + const tournament = abDivisionsTournament(); + const { teams } = tournament + .bracketByIdx(0)! + .source({ placements: [1, 5] }); + + expect(teams).toEqual([1, 2]); + }); +}); diff --git a/app/features/tournament-bracket/core/Bracket/Bracket.ts b/app/features/tournament-bracket/core/Bracket/Bracket.ts index 3fb29f9ba..56634639a 100644 --- a/app/features/tournament-bracket/core/Bracket/Bracket.ts +++ b/app/features/tournament-bracket/core/Bracket/Bracket.ts @@ -7,6 +7,7 @@ import type { Round } from "~/modules/brackets-model"; import invariant from "~/utils/invariant"; import { logger } from "~/utils/logger"; import { fillWithNullTillPowerOfTwo } from "../../tournament-bracket-utils"; +import * as AbDivisions from "../AbDivisions"; import { getTournamentManager } from "../brackets-manager"; import * as Progression from "../Progression"; import type { OptionalIdObject, Tournament } from "../Tournament"; @@ -294,6 +295,16 @@ export abstract class Bracket { const virtualTournamentId = 1; if (teams.length >= TOURNAMENT.ENOUGH_TEAMS_TO_START) { + const settings = this.tournament.bracketManagerSettings( + this.settings, + this.type, + teams.length, + ); + const abDivisions = + this.type === "round_robin" && this.settings?.hasAbDivisions === true + ? this.abDivisionsForPreview(teams, settings.groupCount) + : undefined; + manager.create({ tournamentId: virtualTournamentId, name: "Virtual", @@ -302,17 +313,58 @@ export abstract class Bracket { this.type === "round_robin" ? teams : fillWithNullTillPowerOfTwo(teams), - settings: this.tournament.bracketManagerSettings( - this.settings, - this.type, - teams.length, - ), + settings: abDivisions + ? settings + : { + ...settings, + hasAbDivisions: false, + }, + abDivisions, }); } return manager.get.tournamentData(virtualTournamentId); } + private abDivisionsForPreview( + teams: number[], + groupCount: number | undefined, + ): (0 | 1)[] | undefined { + if (!groupCount) return undefined; + + const assignments = teams.map((teamId) => { + const team = this.tournament.teamById(teamId); + return team?.abDivision ?? null; + }); + + const allAssigned = assignments.every( + (value) => value === 0 || value === 1, + ); + if ( + allAssigned && + AbDivisions.validate({ + abDivisionsBySeedOrder: assignments, + groupCount, + }).isOk() + ) { + return assignments as (0 | 1)[]; + } + + const fakeAssignments: (0 | 1)[] = teams.map((_, index) => + index % 2 === 0 ? 0 : 1, + ); + if ( + AbDivisions.validate({ + abDivisionsBySeedOrder: fakeAssignments, + groupCount, + }).isOk() + ) { + return fakeAssignments; + } + + return undefined; + } + get isUnderground() { return Progression.isUnderground( this.idx, diff --git a/app/features/tournament-bracket/core/Bracket/RoundRobinBracket.ts b/app/features/tournament-bracket/core/Bracket/RoundRobinBracket.ts index d3d612bcc..332517d0f 100644 --- a/app/features/tournament-bracket/core/Bracket/RoundRobinBracket.ts +++ b/app/features/tournament-bracket/core/Bracket/RoundRobinBracket.ts @@ -1,5 +1,6 @@ import * as R from "remeda"; import type { Tables } from "~/db/tables"; +import * as Standings from "~/features/tournament/core/Standings"; import type { TournamentManagerDataSet } from "~/modules/brackets-manager/types"; import invariant from "~/utils/invariant"; import { logger } from "~/utils/logger"; @@ -23,6 +24,13 @@ export class RoundRobinBracket extends Bracket { const relevantMatchesFinished = standings.length === this.participantTournamentTeamIds.length; + if (this.settings?.hasAbDivisions) { + return { + relevantMatchesFinished, + teams: this.teamsFromPlacementsPerAbDivision(standings, placements), + }; + } + const uniquePlacements = R.unique(standings.map((s) => s.placement)); // 1,3,5 -> 1,2,3 e.g. @@ -38,6 +46,30 @@ export class RoundRobinBracket extends Bracket { }; } + private teamsFromPlacementsPerAbDivision( + standings: Standing[], + placements: number[], + ): number[] { + const groupIds = R.unique( + standings + .map((s) => s.groupId) + .filter((id): id is number => typeof id === "number"), + ); + const teams: number[] = []; + for (const groupId of groupIds) { + for (const division of [0, 1] as const) { + const divisionStandings = standings.filter( + (s) => s.groupId === groupId && s.team.abDivision === division, + ); + for (const placement of placements) { + const standing = divisionStandings[placement - 1]; + if (standing) teams.push(standing.team.id); + } + } + } + return teams; + } + get standings(): Standing[] { return this.currentStandings(); } @@ -247,22 +279,8 @@ export class RoundRobinBracket extends Bracket { return 0; }); - let lastPlacement = 0; - let currentPlacement = 1; - let teamsEncountered = 0; return this.standingsWithoutNonParticipants( - sorted.map((team) => { - if (team.placement !== lastPlacement) { - lastPlacement = team.placement; - currentPlacement = teamsEncountered + 1; - } - teamsEncountered++; - return { - ...team, - placement: currentPlacement, - stats: team.stats, - }; - }), + Standings.reNumberPlacements(sorted), ); } diff --git a/app/features/tournament-bracket/core/Bracket/SwissBracket.ts b/app/features/tournament-bracket/core/Bracket/SwissBracket.ts index f17006a6b..b1a6294eb 100644 --- a/app/features/tournament-bracket/core/Bracket/SwissBracket.ts +++ b/app/features/tournament-bracket/core/Bracket/SwissBracket.ts @@ -1,5 +1,6 @@ import * as R from "remeda"; import type { Tables } from "~/db/tables"; +import * as Standings from "~/features/tournament/core/Standings"; import { TOURNAMENT } from "~/features/tournament/tournament-constants"; import type { TournamentManagerDataSet } from "~/modules/brackets-manager/types"; import invariant from "~/utils/invariant"; @@ -443,22 +444,8 @@ export class SwissBracket extends Bracket { return 0; }); - let lastPlacement = 0; - let currentPlacement = 1; - let teamsEncountered = 0; return this.standingsWithoutNonParticipants( - sorted.map((team) => { - if (team.placement !== lastPlacement) { - lastPlacement = team.placement; - currentPlacement = teamsEncountered + 1; - } - teamsEncountered++; - return { - ...team, - placement: currentPlacement, - stats: team.stats, - }; - }), + Standings.reNumberPlacements(sorted), ); } diff --git a/app/features/tournament-bracket/core/Progression.test.ts b/app/features/tournament-bracket/core/Progression.test.ts index 02f913b8d..85b5638bb 100644 --- a/app/features/tournament-bracket/core/Progression.test.ts +++ b/app/features/tournament-bracket/core/Progression.test.ts @@ -274,22 +274,36 @@ const getValidatedBrackets = ( ); describe("validatedSources - other rules", () => { - it("handles NOT_RESOLVING_WINNER (only round robin)", () => { - const error = getValidatedBrackets([ + it("accepts a single round robin with no follow-ups", () => { + const result = getValidatedBrackets([ { settings: {}, type: "round_robin", }, - ]) as Progression.ValidationError; + ]); - expect(error.type).toBe("NOT_RESOLVING_WINNER"); + expect(Array.isArray(result)).toBe(true); }); - it("handles NOT_RESOLVING_WINNER (ends in round robin)", () => { - const error = getValidatedBrackets([ + it("accepts a single A/B round robin with no follow-ups", () => { + const result = getValidatedBrackets([ + { + settings: { + hasAbDivisions: true, + teamsPerGroup: 6, + }, + type: "round_robin", + }, + ]); + + expect(Array.isArray(result)).toBe(true); + }); + + it("accepts a swiss to round robin progression", () => { + const result = getValidatedBrackets([ { settings: {}, - type: "single_elimination", + type: "swiss", }, { settings: {}, @@ -301,9 +315,30 @@ describe("validatedSources - other rules", () => { }, ], }, - ]) as Progression.ValidationError; + ]); - expect(error.type).toBe("NOT_RESOLVING_WINNER"); + expect(Array.isArray(result)).toBe(true); + }); + + it("accepts a round robin to round robin progression", () => { + const result = getValidatedBrackets([ + { + settings: {}, + type: "round_robin", + }, + { + settings: {}, + type: "round_robin", + sources: [ + { + bracketId: "0", + placements: "1,2", + }, + ], + }, + ]); + + expect(Array.isArray(result)).toBe(true); }); it("handles NOT_RESOLVING_WINNER (swiss with many groups)", () => { @@ -427,6 +462,55 @@ describe("validatedSources - other rules", () => { expect(Array.isArray(result)).toBe(true); }); + it("flags TOO_MANY_PLACEMENTS on A/B divisions when placement exceeds per-division size", () => { + const error = getValidatedBrackets([ + { + settings: { + hasAbDivisions: true, + teamsPerGroup: 6, + }, + type: "round_robin", + }, + { + settings: {}, + type: "single_elimination", + sources: [ + { + bracketId: "0", + placements: "1,2,3,4", + }, + ], + }, + ]) as Progression.ValidationError; + + expect(error.type).toBe("TOO_MANY_PLACEMENTS"); + expect((error as any).bracketIdx).toEqual(1); + }); + + it("accepts A/B divisions placements up to per-division size", () => { + const result = getValidatedBrackets([ + { + settings: { + hasAbDivisions: true, + teamsPerGroup: 6, + }, + type: "round_robin", + }, + { + settings: {}, + type: "single_elimination", + sources: [ + { + bracketId: "0", + placements: "1,2,3", + }, + ], + }, + ]); + + expect(Array.isArray(result)).toBe(true); + }); + it("handles DUPLICATE_BRACKET_NAME", () => { const error = getValidatedBrackets([ { @@ -589,6 +673,165 @@ describe("validatedSources - other rules", () => { // Should be valid (no error returned) expect(Array.isArray(result)).toBe(true); }); + + it("accepts A/B divisions on a round robin starting bracket with even teamsPerGroup", () => { + const result = getValidatedBrackets([ + { + settings: { + hasAbDivisions: true, + teamsPerGroup: 6, + }, + type: "round_robin", + }, + { + settings: {}, + type: "single_elimination", + sources: [ + { + bracketId: "0", + placements: "1-2", + }, + ], + }, + ]); + + expect(Array.isArray(result)).toBe(true); + }); + + it("handles AB_DIVISIONS_NOT_ROUND_ROBIN", () => { + const error = getValidatedBrackets([ + { + settings: { + hasAbDivisions: true, + }, + type: "swiss", + name: "Swiss", + }, + { + settings: {}, + type: "single_elimination", + name: "Finals", + sources: [ + { + bracketId: "0", + placements: "1-2", + }, + ], + }, + ]) as Progression.ValidationError; + + expect(error.type).toBe("AB_DIVISIONS_NOT_ROUND_ROBIN"); + expect((error as any).bracketIdx).toEqual(0); + }); + + it("handles AB_DIVISIONS_NOT_STARTING", () => { + const error = getValidatedBrackets([ + { + settings: {}, + type: "round_robin", + name: "Group stage", + }, + { + settings: { + hasAbDivisions: true, + teamsPerGroup: 4, + }, + type: "round_robin", + name: "Second RR", + sources: [ + { + bracketId: "0", + placements: "1-2", + }, + ], + }, + { + settings: {}, + type: "single_elimination", + name: "Finals", + sources: [ + { + bracketId: "1", + placements: "1-2", + }, + ], + }, + ]) as Progression.ValidationError; + + expect(error.type).toBe("AB_DIVISIONS_NOT_STARTING"); + expect((error as any).bracketIdx).toEqual(1); + }); + + it("handles AB_DIVISIONS_ODD_TEAMS_PER_GROUP", () => { + const error = getValidatedBrackets([ + { + settings: { + hasAbDivisions: true, + teamsPerGroup: 5, + }, + type: "round_robin", + }, + { + settings: {}, + type: "single_elimination", + sources: [ + { + bracketId: "0", + placements: "1-2", + }, + ], + }, + ]) as Progression.ValidationError; + + expect(error.type).toBe("AB_DIVISIONS_ODD_TEAMS_PER_GROUP"); + expect((error as any).bracketIdx).toEqual(0); + }); + + it("accepts A/B divisions when teamsPerGroup is unset (default is even)", () => { + const result = getValidatedBrackets([ + { + settings: { + hasAbDivisions: true, + }, + type: "round_robin", + }, + { + settings: {}, + type: "single_elimination", + sources: [ + { + bracketId: "0", + placements: "1-2", + }, + ], + }, + ]); + + expect(Array.isArray(result)).toBe(true); + }); + + it("does not apply A/B validation when hasAbDivisions is absent", () => { + const result = getValidatedBrackets([ + { + settings: { + teamsPerGroup: 5, + }, + type: "round_robin", + }, + { + settings: {}, + type: "single_elimination", + sources: [ + { + bracketId: "0", + placements: "1-2", + }, + ], + }, + ]); + + expect(Array.isArray(result)).toBe(true); + }); }); describe("isFinals", () => { diff --git a/app/features/tournament-bracket/core/Progression.ts b/app/features/tournament-bracket/core/Progression.ts index 60117da01..746d21ce1 100644 --- a/app/features/tournament-bracket/core/Progression.ts +++ b/app/features/tournament-bracket/core/Progression.ts @@ -97,6 +97,21 @@ export type ValidationError = | { type: "SWISS_EARLY_ADVANCE_NO_DESTINATION"; bracketIdx: number; + } + // A/B divisions setting is only valid on round robin brackets + | { + type: "AB_DIVISIONS_NOT_ROUND_ROBIN"; + bracketIdx: number; + } + // A/B divisions setting is only valid on starting brackets (no sources) + | { + type: "AB_DIVISIONS_NOT_STARTING"; + bracketIdx: number; + } + // A/B divisions requires an even teamsPerGroup so each group can be split equally + | { + type: "AB_DIVISIONS_ODD_TEAMS_PER_GROUP"; + bracketIdx: number; }; /** Takes validated brackets and returns them in the format that is ready for user input. */ @@ -274,6 +289,30 @@ export function bracketsToValidationError( }; } + faultyBracketIdx = abDivisionsOnNonRoundRobin(brackets); + if (typeof faultyBracketIdx === "number") { + return { + type: "AB_DIVISIONS_NOT_ROUND_ROBIN", + bracketIdx: faultyBracketIdx, + }; + } + + faultyBracketIdx = abDivisionsOnNonStartingBracket(brackets); + if (typeof faultyBracketIdx === "number") { + return { + type: "AB_DIVISIONS_NOT_STARTING", + bracketIdx: faultyBracketIdx, + }; + } + + faultyBracketIdx = abDivisionsOddTeamsPerGroup(brackets); + if (typeof faultyBracketIdx === "number") { + return { + type: "AB_DIVISIONS_ODD_TEAMS_PER_GROUP", + bracketIdx: faultyBracketIdx, + }; + } + return null; } @@ -366,7 +405,6 @@ function resolvesWinner(brackets: ParsedBracket[]) { const finals = brackets.find((_, idx) => isFinals(idx, brackets)); if (!finals) return false; - if (finals?.type === "round_robin") return false; if ( finals.type === "swiss" && (finals.settings.groupCount ?? TOURNAMENT.SWISS_DEFAULT_GROUP_COUNT) > 1 @@ -474,9 +512,13 @@ function tooManyPlacements(brackets: ParsedBracket[]) { for (const source of bracket.sources ?? []) { if (!roundRobins.includes(source.bracketIdx)) continue; - const size = - brackets[source.bracketIdx].settings.teamsPerGroup ?? + const sourceSettings = brackets[source.bracketIdx].settings; + const teamsPerGroup = + sourceSettings.teamsPerGroup ?? TOURNAMENT.RR_DEFAULT_TEAM_COUNT_PER_GROUP; + const size = sourceSettings.hasAbDivisions + ? teamsPerGroup / 2 + : teamsPerGroup; if (source.placements.some((placement) => placement > size)) { return bracketIdx; @@ -546,6 +588,46 @@ function noDoubleEliminationPositive(brackets: ParsedBracket[]) { return null; } +function abDivisionsOnNonRoundRobin(brackets: ParsedBracket[]) { + for (const [bracketIdx, bracket] of brackets.entries()) { + if (bracket.settings.hasAbDivisions && bracket.type !== "round_robin") { + return bracketIdx; + } + } + + return null; +} + +function abDivisionsOnNonStartingBracket(brackets: ParsedBracket[]) { + for (const [bracketIdx, bracket] of brackets.entries()) { + if ( + bracket.settings.hasAbDivisions && + bracket.sources && + bracket.sources.length > 0 + ) { + return bracketIdx; + } + } + + return null; +} + +function abDivisionsOddTeamsPerGroup(brackets: ParsedBracket[]) { + for (const [bracketIdx, bracket] of brackets.entries()) { + if (!bracket.settings.hasAbDivisions) continue; + + const teamsPerGroup = + bracket.settings.teamsPerGroup ?? + TOURNAMENT.RR_DEFAULT_TEAM_COUNT_PER_GROUP; + + if (teamsPerGroup % 2 !== 0) { + return bracketIdx; + } + } + + return null; +} + function swissEarlyAdvanceWithoutDestination(brackets: ParsedBracket[]) { for (const [bracketIdx, bracket] of brackets.entries()) { if (bracket.type === "swiss" && bracket.settings.advanceThreshold) { @@ -585,6 +667,16 @@ export function isFinals(idx: number, brackets: ParsedBracket[]) { return resolveMainBracketProgression(brackets).at(-1) === idx; } +/** Returns true if the finals bracket of the tournament is an A/B divisions round robin. */ +export function hasAbDivisionsFinals(brackets: ParsedBracket[]): boolean { + const finals = brackets.find((_, idx) => isFinals(idx, brackets)); + if (!finals) return false; + + return ( + finals.type === "round_robin" && finals.settings?.hasAbDivisions === true + ); +} + /** Given bracketIdx and bracketProgression will resolve if this an "underground bracket". * Underground bracket is defined as a bracket that is not part of the main tournament progression e.g. optional bracket for early losers */ diff --git a/app/features/tournament-bracket/core/Tournament.ts b/app/features/tournament-bracket/core/Tournament.ts index 63e08528f..6b81a5937 100644 --- a/app/features/tournament-bracket/core/Tournament.ts +++ b/app/features/tournament-bracket/core/Tournament.ts @@ -556,6 +556,7 @@ export class Tournament { return { groupCount: Math.ceil(participantsCount / teamsPerGroup), seedOrdering: ["groups.seed_optimized"], + hasAbDivisions: selectedSettings?.hasAbDivisions ?? false, }; } case "swiss": { @@ -827,6 +828,7 @@ export class Tournament { /** Can a new sub post be made at this time? */ get canAddNewSubPost() { if (!this.lfgEnabled) return false; + if (this.isInvitational) return false; if (this.ctx.isFinalized) return false; return ( diff --git a/app/features/tournament-bracket/core/summarizer.server.ts b/app/features/tournament-bracket/core/summarizer.server.ts index 9d3cb9c98..c1a1b952e 100644 --- a/app/features/tournament-bracket/core/summarizer.server.ts +++ b/app/features/tournament-bracket/core/summarizer.server.ts @@ -18,6 +18,7 @@ import { } from "../tournament-bracket-utils"; import type { Standing } from "./Bracket"; import type { ParsedBracket } from "./Progression"; +import * as Progression from "./Progression"; export interface TournamentSummary { skills: Omit< @@ -41,6 +42,7 @@ type TeamsArg = Array<{ id: number; members: Array<{ userId: number }>; startingBracketIdx?: number | null; + abDivision?: number | null; }>; type Rating = Pick; @@ -574,25 +576,31 @@ function tournamentResults({ }) { const result: TournamentSummary["tournamentResults"] = []; - const firstPlaceFinishesCount = finalStandings.filter( - (s) => s.placement === 1, - ).length; - const isMultiStartingBracket = firstPlaceFinishesCount > 1; + const isMultiStartingBracket = + Progression.startingBrackets(progression).length > 1; + const isAbDivisionsFinals = Progression.hasAbDivisionsFinals(progression); for (const standing of finalStandings) { const team = teams.find((t) => t.id === standing.team.id); invariant(team); - const div = - // second check should be redundant, but just here in case - typeof team.startingBracketIdx === "number" && isMultiStartingBracket - ? getBracketProgressionLabel(team.startingBracketIdx, progression) - : null; - const divisionParticipantCount = - div !== null - ? teams.filter((t) => t.startingBracketIdx === team.startingBracketIdx) - .length - : participantCount; + let div: string | null = null; + let divisionParticipantCount = participantCount; + + if (isAbDivisionsFinals && typeof team.abDivision === "number") { + div = team.abDivision === 0 ? "A" : "B"; + divisionParticipantCount = teams.filter( + (t) => t.abDivision === team.abDivision, + ).length; + } else if ( + isMultiStartingBracket && + typeof team.startingBracketIdx === "number" + ) { + div = getBracketProgressionLabel(team.startingBracketIdx, progression); + divisionParticipantCount = teams.filter( + (t) => t.startingBracketIdx === team.startingBracketIdx, + ).length; + } for (const player of standing.team.members) { result.push({ diff --git a/app/features/tournament-bracket/core/summarizer.test.ts b/app/features/tournament-bracket/core/summarizer.test.ts index 901d06f09..5687b80b0 100644 --- a/app/features/tournament-bracket/core/summarizer.test.ts +++ b/app/features/tournament-bracket/core/summarizer.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test } from "vitest"; import invariant from "~/utils/invariant"; import type { Tables } from "../../../db/tables"; import type { AllMatchResult } from "../queries/allMatchResultsByTournamentId.server"; +import type { ParsedBracket } from "./Progression"; import { tournamentSummary } from "./summarizer.server"; import type { TournamentDataTeam } from "./Tournament.server"; @@ -33,6 +34,7 @@ describe("tournamentSummary()", () => { inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, mapPool: [], members: userIds.map((userId) => ({ country: null, @@ -64,6 +66,7 @@ describe("tournamentSummary()", () => { seedingSkillCountsFor, withMemberInTwoTeams = false, teamsWithStartingBrackets, + teamsWithAbDivisions, progression, finalStandings, }: { @@ -74,13 +77,11 @@ describe("tournamentSummary()", () => { id: number; startingBracketIdx: number | null; }>; - progression?: Array<{ - name: string; - type: "single_elimination"; - settings: Record; - requiresCheckIn: boolean; - sources?: Array<{ bracketIdx: number; placements: number[] }>; + teamsWithAbDivisions?: Array<{ + id: number; + abDivision: 0 | 1; }>; + progression?: ParsedBracket[]; finalStandings?: Array<{ placement: number; team: TournamentDataTeam; @@ -121,17 +122,19 @@ describe("tournamentSummary()", () => { }, ]; - const teams = teamsWithStartingBrackets - ? defaultTeams.map((team) => { - const startingBracket = teamsWithStartingBrackets.find( - (t) => t.id === team.id, - ); - return { - ...team, - startingBracketIdx: startingBracket?.startingBracketIdx ?? null, - }; - }) - : defaultTeams; + const teams = defaultTeams.map((team) => { + const startingBracket = teamsWithStartingBrackets?.find( + (t) => t.id === team.id, + ); + const abDivisionEntry = teamsWithAbDivisions?.find( + (t) => t.id === team.id, + ); + return { + ...team, + startingBracketIdx: startingBracket?.startingBracketIdx ?? null, + abDivision: abDivisionEntry?.abDivision ?? null, + }; + }); return tournamentSummary({ finalStandings: finalStandings ?? [ @@ -778,6 +781,102 @@ describe("tournamentSummary()", () => { expect(team4Results.every((r) => r.participantCount === 2)).toBeTruthy(); }); + test("div is set from abDivision when finals is an A/B divisions round robin", () => { + const summary = summarize({ + teamsWithAbDivisions: [ + { id: 1, abDivision: 0 }, + { id: 2, abDivision: 1 }, + { id: 3, abDivision: 0 }, + { id: 4, abDivision: 1 }, + ], + progression: [ + { + name: "Groups stage", + type: "round_robin", + settings: { hasAbDivisions: true, teamsPerGroup: 4 }, + requiresCheckIn: false, + }, + ], + finalStandings: [ + { + placement: 1, + team: createTeam(1, [1, 2, 3, 4]), + }, + { + placement: 1, + team: createTeam(2, [5, 6, 7, 8]), + }, + { + placement: 2, + team: createTeam(3, [9, 10, 11, 12]), + }, + { + placement: 2, + team: createTeam(4, [13, 14, 15, 16]), + }, + ], + }); + + const team1Results = summary.tournamentResults.filter( + (r) => r.tournamentTeamId === 1, + ); + const team2Results = summary.tournamentResults.filter( + (r) => r.tournamentTeamId === 2, + ); + const team3Results = summary.tournamentResults.filter( + (r) => r.tournamentTeamId === 3, + ); + const team4Results = summary.tournamentResults.filter( + (r) => r.tournamentTeamId === 4, + ); + + expect(team1Results.every((r) => r.div === "A")).toBeTruthy(); + expect(team2Results.every((r) => r.div === "B")).toBeTruthy(); + expect(team3Results.every((r) => r.div === "A")).toBeTruthy(); + expect(team4Results.every((r) => r.div === "B")).toBeTruthy(); + }); + + test("participantCount counts teams per abDivision for A/B finals", () => { + const summary = summarize({ + teamsWithAbDivisions: [ + { id: 1, abDivision: 0 }, + { id: 2, abDivision: 1 }, + { id: 3, abDivision: 0 }, + { id: 4, abDivision: 1 }, + ], + progression: [ + { + name: "Groups stage", + type: "round_robin", + settings: { hasAbDivisions: true, teamsPerGroup: 4 }, + requiresCheckIn: false, + }, + ], + finalStandings: [ + { + placement: 1, + team: createTeam(1, [1, 2, 3, 4]), + }, + { + placement: 1, + team: createTeam(2, [5, 6, 7, 8]), + }, + { + placement: 2, + team: createTeam(3, [9, 10, 11, 12]), + }, + { + placement: 2, + team: createTeam(4, [13, 14, 15, 16]), + }, + ], + }); + + for (const result of summary.tournamentResults) { + expect(result.participantCount).toBe(2); + } + }); + test("excludes matches ended early by organizer from calculations", () => { const summary = summarize({ results: [ diff --git a/app/features/tournament-bracket/core/tests/mocks-li.ts b/app/features/tournament-bracket/core/tests/mocks-li.ts index 83e4b2223..9fbbbbe56 100644 --- a/app/features/tournament-bracket/core/tests/mocks-li.ts +++ b/app/features/tournament-bracket/core/tests/mocks-li.ts @@ -7255,6 +7255,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733157607, activeRosterUserIds: [25875, 21063, 11226, 31597], pickupAvatarUrl: null, @@ -7364,6 +7365,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733157629, activeRosterUserIds: [14837, 27260, 42704, 9379], pickupAvatarUrl: null, @@ -7473,6 +7475,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733161494, activeRosterUserIds: [34424, 31195, 31395, 26103], pickupAvatarUrl: null, @@ -7582,6 +7585,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733166918, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -7675,6 +7679,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733166213, activeRosterUserIds: [32160, 29267, 25591, 36962], pickupAvatarUrl: null, @@ -7779,6 +7784,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733189945, activeRosterUserIds: [12418, 34355, 2319, 7430], pickupAvatarUrl: null, @@ -7888,6 +7894,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733244862, activeRosterUserIds: [29425, 31524, 35674, 26285], pickupAvatarUrl: null, @@ -7997,6 +8004,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733282085, activeRosterUserIds: [26747, 27292, 5708, 6309], pickupAvatarUrl: null, @@ -8122,6 +8130,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733291438, activeRosterUserIds: [24459, 40851, 23974, 43608], pickupAvatarUrl: null, @@ -8247,6 +8256,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733439755, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -8340,6 +8350,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733485884, activeRosterUserIds: [30686, 1961, 30685, 22396], pickupAvatarUrl: null, @@ -8465,6 +8476,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733937993, activeRosterUserIds: [12434, 30263, 5861, 24275], pickupAvatarUrl: null, @@ -8590,6 +8602,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733166818, activeRosterUserIds: [32670, 38046, 42638, 34589], pickupAvatarUrl: "pickup-logo-Hj-Us_Roj5Ksfv000ceBo-1733166818832.webp", @@ -8699,6 +8712,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733167616, activeRosterUserIds: [45102, 26711, 41739, 4533], pickupAvatarUrl: null, @@ -8808,6 +8822,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733201503, activeRosterUserIds: [20807, 31556, 33373, 42703], pickupAvatarUrl: null, @@ -8933,6 +8948,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733218069, activeRosterUserIds: [26509, 7959, 7690, 7958], pickupAvatarUrl: null, @@ -9042,6 +9058,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733319202, activeRosterUserIds: [10714, 21685, 8840, 10028], pickupAvatarUrl: null, @@ -9167,6 +9184,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733471556, activeRosterUserIds: [17532, 30204, 36007, 38896], pickupAvatarUrl: null, @@ -9276,6 +9294,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733501938, activeRosterUserIds: [30495, 43073, 30488, 45295], pickupAvatarUrl: null, @@ -9385,6 +9404,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733622364, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -9472,6 +9492,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733635706, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -9565,6 +9586,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733671856, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -9653,6 +9675,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733810204, activeRosterUserIds: [1959, 17352, 33954, 22403], pickupAvatarUrl: "pickup-logo-3KZntw8OZ9LkW4XqZRLS9-1733810204048.webp", @@ -9757,6 +9780,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733889961, activeRosterUserIds: [6696, 32107, 33402, 30619], pickupAvatarUrl: null, @@ -9861,6 +9885,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733892132, activeRosterUserIds: [21670, 8993, 8395, 3566], pickupAvatarUrl: null, @@ -9970,6 +9995,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734035170, activeRosterUserIds: [24510, 10670, 22577, 31143], pickupAvatarUrl: null, @@ -10079,6 +10105,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734107844, activeRosterUserIds: [28170, 14309, 17310, 23164], pickupAvatarUrl: null, @@ -10188,6 +10215,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734132225, activeRosterUserIds: null, pickupAvatarUrl: "pickup-logo-_asHjlVchhJ50PH_mDBtw-1734132224819.webp", @@ -10276,6 +10304,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733194304, activeRosterUserIds: [40505, 29011, 23082, 45036], pickupAvatarUrl: null, @@ -10385,6 +10414,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733195091, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -10478,6 +10508,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733364647, activeRosterUserIds: [22801, 31150, 35354, 27747], pickupAvatarUrl: null, @@ -10587,6 +10618,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733374295, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -10680,6 +10712,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733433864, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -10799,6 +10832,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733513814, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -10892,6 +10926,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733602400, activeRosterUserIds: [10826, 4248, 20419, 11180], pickupAvatarUrl: null, @@ -11001,6 +11036,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733753214, activeRosterUserIds: [27903, 28446, 34634, 30728], pickupAvatarUrl: null, @@ -11110,6 +11146,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733914001, activeRosterUserIds: [32909, 10190, 35922, 40304], pickupAvatarUrl: null, @@ -11235,6 +11272,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733966548, activeRosterUserIds: [35617, 37669, 37436, 35811], pickupAvatarUrl: null, @@ -11344,6 +11382,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734032213, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -11437,6 +11476,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734106606, activeRosterUserIds: [37173, 43269, 43623, 16054], pickupAvatarUrl: null, @@ -11546,6 +11586,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734116765, activeRosterUserIds: [25312, 10378, 46771, 26044], pickupAvatarUrl: null, @@ -11671,6 +11712,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734125312, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -11764,6 +11806,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734134382, activeRosterUserIds: [26758, 25689, 42164, 44475], pickupAvatarUrl: null, @@ -11868,6 +11911,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733156802, activeRosterUserIds: [9036, 7434, 3738, 9112], pickupAvatarUrl: null, @@ -11977,6 +12021,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733157391, activeRosterUserIds: [5935, 38204, 3741, 8080], pickupAvatarUrl: null, @@ -12102,6 +12147,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733162274, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -12195,6 +12241,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733367806, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -12288,6 +12335,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733456080, activeRosterUserIds: [10386, 33369, 29617, 22942], pickupAvatarUrl: null, @@ -12397,6 +12445,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733579092, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -12484,6 +12533,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733769667, activeRosterUserIds: [3481, 38022, 41269, 43551], pickupAvatarUrl: null, @@ -12609,6 +12659,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733794148, activeRosterUserIds: [22820, 29636, 27036, 28959], pickupAvatarUrl: null, @@ -12734,6 +12785,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733820540, activeRosterUserIds: null, pickupAvatarUrl: "pickup-logo-t2-mrQNINFqIoFNYuxbmW-1733820600291.webp", @@ -12822,6 +12874,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733825084, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -12915,6 +12968,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733865890, activeRosterUserIds: [15425, 41975, 28938, 8587], pickupAvatarUrl: null, @@ -13024,6 +13078,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733873149, activeRosterUserIds: [40550, 7115, 29674, 30031], pickupAvatarUrl: null, @@ -13149,6 +13204,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733875608, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -13242,6 +13298,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733888417, activeRosterUserIds: null, pickupAvatarUrl: "pickup-logo-c9a1igcMT4m2otyRdTs_0-1733888672873.webp", @@ -13330,6 +13387,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734008857, activeRosterUserIds: [30266, 37341, 22699, 28145], pickupAvatarUrl: null, @@ -13439,6 +13497,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734018352, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -13532,6 +13591,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734019701, activeRosterUserIds: [35421, 33524, 22500, 32802], pickupAvatarUrl: "pickup-logo-u4oKxXYjamTXZ1x-bgNFp-1734019701188.webp", @@ -13652,6 +13712,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734023441, activeRosterUserIds: [1852, 2898, 25763, 3466], pickupAvatarUrl: null, @@ -13761,6 +13822,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734099744, activeRosterUserIds: [39098, 22624, 28137, 2769], pickupAvatarUrl: null, @@ -13870,6 +13932,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734109256, activeRosterUserIds: [29661, 15158, 35067, 31655], pickupAvatarUrl: "pickup-logo-An13SrR78qDNIM2t95ujb-1734109256283.webp", @@ -13990,6 +14053,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734125682, activeRosterUserIds: [36575, 30425, 32430, 24290], pickupAvatarUrl: null, @@ -14115,6 +14179,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733515005, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -14208,6 +14273,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733521735, activeRosterUserIds: [44772, 38912, 36853, 42599], pickupAvatarUrl: null, @@ -14317,6 +14383,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733525617, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -14410,6 +14477,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733847805, activeRosterUserIds: [33615, 32015, 45778, 32970], pickupAvatarUrl: null, @@ -14519,6 +14587,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733858126, activeRosterUserIds: [34545, 35567, 41108, 41255], pickupAvatarUrl: null, @@ -14628,6 +14697,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733966096, activeRosterUserIds: [39470, 42874, 32878, 25741], pickupAvatarUrl: null, @@ -14737,6 +14807,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734021147, activeRosterUserIds: [45250, 45174, 6976, 10222], pickupAvatarUrl: "pickup-logo-v3boyVjbFsTyMlQylz4Dn-1734021152539.webp", @@ -14846,6 +14917,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734040772, activeRosterUserIds: null, pickupAvatarUrl: "pickup-logo-Jx6JnhFJQjOnM10s_79ld-1734041234919.webp", @@ -14939,6 +15011,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734033803, activeRosterUserIds: [27800, 12235, 30044, 29531], pickupAvatarUrl: null, @@ -15048,6 +15121,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734099612, activeRosterUserIds: [24572, 7058, 37641, 33913], pickupAvatarUrl: "pickup-logo-RrPQW5kG_K1cvjdU5TKcF-1734099611923.webp", @@ -15152,6 +15226,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734113463, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -15245,6 +15320,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734118202, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -15332,6 +15408,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734134334, activeRosterUserIds: [11186, 27611, 25952, 23481], pickupAvatarUrl: null, @@ -15441,6 +15518,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733169181, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -15534,6 +15612,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733247691, activeRosterUserIds: null, pickupAvatarUrl: "pickup-logo--fZF6IGlzuuHeotc6Z00p-1733762912138.webp", @@ -15627,6 +15706,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733452618, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -15725,6 +15805,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733481710, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -15828,6 +15909,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733508949, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -15921,6 +16003,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733611261, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -16009,6 +16092,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 0, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733841846, activeRosterUserIds: [41943, 46289, 45290, 46394], pickupAvatarUrl: null, @@ -16118,6 +16202,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1733878153, activeRosterUserIds: null, pickupAvatarUrl: null, @@ -16211,6 +16296,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ droppedOut: 1, inviteCode: null, startingBracketIdx: null, + abDivision: null, createdAt: 1734135144, activeRosterUserIds: null, pickupAvatarUrl: "pickup-logo-obQfxdRnJg0CsbrE6OXdl-1734135144301.webp", diff --git a/app/features/tournament-bracket/core/tests/mocks-sos.ts b/app/features/tournament-bracket/core/tests/mocks-sos.ts index 86f95b499..8004c0919 100644 --- a/app/features/tournament-bracket/core/tests/mocks-sos.ts +++ b/app/features/tournament-bracket/core/tests/mocks-sos.ts @@ -2498,6 +2498,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14800, @@ -2606,6 +2607,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14743, @@ -2730,6 +2732,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14801, @@ -2838,6 +2841,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14792, @@ -2946,6 +2950,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14670, @@ -3091,6 +3096,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14661, @@ -3220,6 +3226,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14804, @@ -3349,6 +3356,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14732, @@ -3462,6 +3470,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14747, @@ -3586,6 +3595,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14748, @@ -3710,6 +3720,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14803, @@ -3818,6 +3829,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14795, @@ -3947,6 +3959,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14445, @@ -4092,6 +4105,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14810, @@ -4158,6 +4172,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14777, @@ -4266,6 +4281,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14805, @@ -4374,6 +4390,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14806, @@ -4482,6 +4499,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14653, @@ -4627,6 +4645,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14708, @@ -4756,6 +4775,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14715, @@ -4869,6 +4889,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14796, @@ -4977,6 +4998,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14797, @@ -5085,6 +5107,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14750, @@ -5188,6 +5211,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14809, @@ -5317,6 +5341,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14709, @@ -5430,6 +5455,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14783, @@ -5485,6 +5511,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14702, @@ -5593,6 +5620,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14735, @@ -5722,6 +5750,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14798, @@ -5846,6 +5875,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14663, @@ -5959,6 +5989,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14517, @@ -6088,6 +6119,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14741, @@ -6217,6 +6249,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14711, @@ -6320,6 +6353,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14655, @@ -6465,6 +6499,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14634, @@ -6552,6 +6587,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14799, @@ -6665,6 +6701,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14784, @@ -6747,6 +6784,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14733, @@ -6855,6 +6893,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14611, @@ -6958,6 +6997,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14713, @@ -7082,6 +7122,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14687, @@ -7211,6 +7252,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14742, @@ -7356,6 +7398,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14764, @@ -7464,6 +7507,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14802, @@ -7588,6 +7632,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14696, @@ -7717,6 +7762,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14808, @@ -7825,6 +7871,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14620, @@ -7965,6 +8012,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14737, @@ -8094,6 +8142,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14503, @@ -8181,6 +8230,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14739, @@ -8289,6 +8339,7 @@ export const SWIM_OR_SINK_167 = ( team: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, { id: 14607, @@ -8402,6 +8453,7 @@ export const SWIM_OR_SINK_167 = ( }, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, }, ], tieBreakerMapPool: [], diff --git a/app/features/tournament-bracket/core/tests/mocks-zones-weekly.ts b/app/features/tournament-bracket/core/tests/mocks-zones-weekly.ts index d714c0302..a9718d5e4 100644 --- a/app/features/tournament-bracket/core/tests/mocks-zones-weekly.ts +++ b/app/features/tournament-bracket/core/tests/mocks-zones-weekly.ts @@ -389,6 +389,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({ createdAt: 1734656039, activeRosterUserIds: [5662, 2899, 6114, 30176], startingBracketIdx: null, + abDivision: null, pickupAvatarUrl: null, members: [ { @@ -498,6 +499,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({ createdAt: 1734423187, activeRosterUserIds: null, startingBracketIdx: null, + abDivision: null, pickupAvatarUrl: "pickup-logo-rZYQMu8ELjiFkeiAVGJUt-1734424882431.webp", members: [ { @@ -586,6 +588,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({ createdAt: 1734660846, activeRosterUserIds: null, startingBracketIdx: null, + abDivision: null, pickupAvatarUrl: null, members: [ { @@ -674,6 +677,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({ createdAt: 1734683349, activeRosterUserIds: [37632, 13590, 10757, 33047], startingBracketIdx: null, + abDivision: null, pickupAvatarUrl: null, members: [ { @@ -783,6 +787,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({ createdAt: 1734608907, activeRosterUserIds: [11780, 46006, 43518, 33483], startingBracketIdx: null, + abDivision: null, pickupAvatarUrl: "pickup-logo-FOfFcEbo2OJxIJIJxNJqu-1734608907317.webp", members: [ { @@ -903,6 +908,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({ createdAt: 1734397954, activeRosterUserIds: [46467, 46813, 33491, 43662], startingBracketIdx: null, + abDivision: null, pickupAvatarUrl: "pickup-logo-y79k_HOVmjv4KfhTjuSqh-1734398099266.webp", members: [ { @@ -1012,6 +1018,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({ createdAt: 1734598652, activeRosterUserIds: null, startingBracketIdx: null, + abDivision: null, pickupAvatarUrl: "pickup-logo-IGXFtjFMa_dxQqAe2dqIR-1734598652684.webp", members: [ { diff --git a/app/features/tournament-bracket/core/tests/mocks.ts b/app/features/tournament-bracket/core/tests/mocks.ts index 6b405608a..eacb0cad1 100644 --- a/app/features/tournament-bracket/core/tests/mocks.ts +++ b/app/features/tournament-bracket/core/tests/mocks.ts @@ -1527,6 +1527,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709743534, @@ -1653,6 +1654,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709737918, @@ -1795,6 +1797,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709743523, @@ -1921,6 +1924,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709743262, @@ -2047,6 +2051,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709741396, @@ -2189,6 +2194,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709711811, @@ -2331,6 +2337,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709738831, @@ -2473,6 +2480,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709737837, @@ -2599,6 +2607,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709741719, @@ -2757,6 +2766,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709730354, @@ -2899,6 +2909,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709745630, @@ -3039,6 +3050,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709592381, @@ -3197,6 +3209,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709723749, @@ -3339,6 +3352,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709668399, @@ -3497,6 +3511,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709735267, @@ -3628,6 +3643,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709745849, @@ -3759,6 +3775,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709742258, @@ -3899,6 +3916,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709738744, @@ -4041,6 +4059,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709746054, @@ -4170,6 +4189,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709744894, @@ -4296,6 +4316,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709728278, @@ -4422,6 +4443,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709715006, @@ -4548,6 +4570,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709660578, @@ -4679,6 +4702,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709721869, @@ -4824,6 +4848,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709743633, @@ -4955,6 +4980,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709738747, @@ -5102,6 +5128,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709626047, @@ -5244,6 +5271,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709727951, @@ -5370,6 +5398,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709741482, @@ -5526,6 +5555,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709744451, @@ -5657,6 +5687,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709726536, @@ -5783,6 +5814,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709558706, @@ -5914,6 +5946,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709744323, @@ -6061,6 +6094,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709677397, @@ -6187,6 +6221,7 @@ export const PADDLING_POOL_257 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1709618711, @@ -8118,6 +8153,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708476597, @@ -8244,6 +8280,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708535137, @@ -8370,6 +8407,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708533764, @@ -8510,6 +8548,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708537512, @@ -8652,6 +8691,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708533309, @@ -8778,6 +8818,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708430641, @@ -8920,6 +8961,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708536306, @@ -9044,6 +9086,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708526368, @@ -9170,6 +9213,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708506060, @@ -9328,6 +9372,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708526814, @@ -9452,6 +9497,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708466421, @@ -9594,6 +9640,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708377426, @@ -9734,6 +9781,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708448289, @@ -9892,6 +9940,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708532602, @@ -10018,6 +10067,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708535205, @@ -10160,6 +10210,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708515945, @@ -10286,6 +10337,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708453334, @@ -10410,6 +10462,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708522730, @@ -10552,6 +10605,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708375443, @@ -10694,6 +10748,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708532665, @@ -10825,6 +10880,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708364254, @@ -10972,6 +11028,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708464101, @@ -11117,6 +11174,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708520249, @@ -11259,6 +11317,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708535804, @@ -11385,6 +11444,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708535891, @@ -11527,6 +11587,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708521749, @@ -11669,6 +11730,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708536584, @@ -11795,6 +11857,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708537772, @@ -11958,6 +12021,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708379916, @@ -12100,6 +12164,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708519753, @@ -12247,6 +12312,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708534312, @@ -12392,6 +12458,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708531929, @@ -12518,6 +12585,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708477155, @@ -12649,6 +12717,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708531564, @@ -12828,6 +12897,7 @@ export const PADDLING_POOL_255 = () => inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1708503356, @@ -15021,6 +15091,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707443313, @@ -15134,6 +15205,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707366405, @@ -15247,6 +15319,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1706912643, @@ -15360,6 +15433,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707359335, @@ -15505,6 +15579,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707171426, @@ -15634,6 +15709,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707342696, @@ -15763,6 +15839,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707513942, @@ -15908,6 +15985,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707526815, @@ -16053,6 +16131,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707583385, @@ -16166,6 +16245,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707486395, @@ -16279,6 +16359,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707513290, @@ -16392,6 +16473,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707531084, @@ -16505,6 +16587,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707568466, @@ -16634,6 +16717,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707481625, @@ -16747,6 +16831,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707530166, @@ -16860,6 +16945,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707181792, @@ -16989,6 +17075,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707550321, @@ -17123,6 +17210,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707575096, @@ -17252,6 +17340,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707569490, @@ -17397,6 +17486,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707537425, @@ -17510,6 +17600,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707564691, @@ -17660,6 +17751,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707145818, @@ -17783,6 +17875,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707558330, @@ -17896,6 +17989,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707586842, @@ -18009,6 +18103,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707583597, @@ -18154,6 +18249,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707429804, @@ -18283,6 +18379,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707539973, @@ -18417,6 +18514,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707507831, @@ -18546,6 +18644,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707586297, @@ -18673,6 +18772,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707583885, @@ -18818,6 +18918,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707578076, @@ -18950,6 +19051,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707582953, @@ -19063,6 +19165,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707575330, @@ -19199,6 +19302,7 @@ export const IN_THE_ZONE_32 = ({ inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, activeRosterUserIds: [], pickupAvatarUrl: null, createdAt: 1707527645, diff --git a/app/features/tournament-bracket/core/tests/test-utils.ts b/app/features/tournament-bracket/core/tests/test-utils.ts index b06238715..05afc0675 100644 --- a/app/features/tournament-bracket/core/tests/test-utils.ts +++ b/app/features/tournament-bracket/core/tests/test-utils.ts @@ -15,6 +15,7 @@ export const tournamentCtxTeam = ( inviteCode: null, avgSeedingSkillOrdinal: null, startingBracketIdx: null, + abDivision: null, team: null, mapPool: [], members: [], diff --git a/app/features/tournament-bracket/loaders/to.$id.matches.$mid.server.ts b/app/features/tournament-bracket/loaders/to.$id.matches.$mid.server.ts index 04eb9c67b..06f8aed54 100644 --- a/app/features/tournament-bracket/loaders/to.$id.matches.$mid.server.ts +++ b/app/features/tournament-bracket/loaders/to.$id.matches.$mid.server.ts @@ -15,10 +15,13 @@ import { executeRoll } from "../core/executeRoll.server"; import { mapListFromResults, resolveMapList } from "../core/mapList.server"; import * as PickBan from "../core/PickBan"; import { tournamentFromDBCached } from "../core/Tournament.server"; -import { findMatchById } from "../queries/findMatchById.server"; import { findResultsByMatchId } from "../queries/findResultsByMatchId.server"; +import * as TournamentMatchRepository from "../TournamentMatchRepository.server"; import { matchPageParamsSchema } from "../tournament-bracket-schemas.server"; -import { matchEndedEarly } from "../tournament-bracket-utils"; +import { + matchEndedEarly, + tournamentTeamToActiveRosterUserIds, +} from "../tournament-bracket-utils"; export type TournamentMatchLoaderData = typeof loader; @@ -33,7 +36,9 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { user: undefined, }); - const match = notFoundIfFalsy(findMatchById(matchId)); + const match = notFoundIfFalsy( + await TournamentMatchRepository.findMatchById(matchId), + ); const isBye = !match.opponentOne || !match.opponentTwo; if (isBye) { @@ -142,10 +147,26 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { if ( match.chatCode && !matchIsOver && - match.opponentOne && - match.opponentTwo + match.opponentOne?.id && + match.opponentTwo?.id ) { - const playerIds = match.players.map((p) => p.id); + // only add global chat for active roster (or all if not yet set i.e. first match) + // if roster changed mid-set the subs can still see the chat on the match page + const teamAlpha = tournament.teamById(match.opponentOne.id)!; + const teamAlphaActiveRoster = + tournamentTeamToActiveRosterUserIds( + teamAlpha, + tournament.minMembersPerTeam, + ) ?? teamAlpha.members.map((m) => m.userId); + const teamBravo = tournament.teamById(match.opponentTwo.id)!; + const teamBravoActiveRoster = + tournamentTeamToActiveRosterUserIds( + teamBravo, + tournament.minMembersPerTeam, + ) ?? teamBravo.members.map((m) => m.userId); + + const playerIds = [...teamAlphaActiveRoster, ...teamBravoActiveRoster]; + const matchContext = tournament.matchContextNamesById(matchId); ChatSystemMessage.setMetadata({ @@ -163,14 +184,15 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { tournament.isOrganizerOrStreamer(user) || match.players.some((p) => p.id === user?.id); - const isStaff = user?.roles.includes("STAFF") ?? false; - const chatCodeExpired = tournament.ctx.isFinalized - ? true - : !chatAccessible({ - isStaff, - expiresAfterDays: 90, - comparedTo: tournament.ctx.startTime, - }); + const isSiteStaff = user?.roles.includes("STAFF") ?? false; + const isTournamentStaff = tournament.isOrganizer(user); + const chatCodeExpired = + tournament.ctx.isFinalized && !isSiteStaff && !isTournamentStaff + ? true + : !chatAccessible({ + expiresAfterDays: tournament.isLeagueDivision ? 30 : 7, + comparedTo: tournament.ctx.startTime, + }); const visibleChatCode = shouldSeeChat && !chatCodeExpired ? match.chatCode : undefined; diff --git a/app/features/tournament-bracket/queries/findMatchById.server.ts b/app/features/tournament-bracket/queries/findMatchById.server.ts deleted file mode 100644 index 74bdc5a39..000000000 --- a/app/features/tournament-bracket/queries/findMatchById.server.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { sql } from "~/db/sql"; -import type { Tables, TournamentRoundMaps } from "~/db/tables"; -import type { Match } from "~/modules/brackets-model"; -import { parseDBArray } from "~/utils/sql"; - -const stm = sql.prepare(/* sql */ ` - select - "TournamentMatch"."id", - "TournamentMatch"."groupId", - "TournamentMatch"."opponentOne", - "TournamentMatch"."opponentTwo", - "TournamentMatch"."chatCode", - "TournamentMatch"."startedAt", - "TournamentMatch"."status", - "Tournament"."mapPickingStyle", - "TournamentRound"."id" as "roundId", - "TournamentRound"."maps" as "roundMaps", - json_group_array( - json_object( - 'id', - "User"."id", - 'username', - "User"."username", - 'tournamentTeamId', - "TournamentTeamMember"."tournamentTeamId", - 'inGameName', - COALESCE("TournamentTeamMember"."inGameName", "User"."inGameName"), - 'discordId', - "User"."discordId", - 'customUrl', - "User"."customUrl", - 'discordAvatar', - "User"."discordAvatar", - 'pronouns', json("User"."pronouns") - ) - ) as "players" - from "TournamentMatch" - left join "TournamentStage" on "TournamentStage"."id" = "TournamentMatch"."stageId" - left join "TournamentRound" on "TournamentRound"."id" = "TournamentMatch"."roundId" - left join "Tournament" on "Tournament"."id" = "TournamentStage"."tournamentId" - left join "TournamentTeamMember" on - "TournamentTeamMember"."tournamentTeamId" = "TournamentMatch"."opponentOne" ->> '$.id' - or - "TournamentTeamMember"."tournamentTeamId" = "TournamentMatch"."opponentTwo" ->> '$.id' - left join "User" on "User"."id" = "TournamentTeamMember"."userId" - where "TournamentMatch"."id" = @id - group by "TournamentMatch"."id" -`); - -export type FindMatchById = ReturnType; - -export const findMatchById = (id: number) => { - const row = stm.get({ id }) as - | ((Pick< - Tables["TournamentMatch"], - "id" | "groupId" | "chatCode" | "startedAt" | "status" - > & - Pick & { players: string }) & { - opponentOne: string; - opponentTwo: string; - roundId: number; - roundMaps: string; - }) - | undefined; - - if (!row) return; - - const roundMaps = JSON.parse(row.roundMaps) as TournamentRoundMaps; - - return { - ...row, - bestOf: roundMaps.count, - roundId: row.roundId, - roundMaps, - opponentOne: JSON.parse(row.opponentOne) as Match["opponent1"], - opponentTwo: JSON.parse(row.opponentTwo) as Match["opponent2"], - status: row.status, - players: ( - parseDBArray(row.players) as Array<{ - id: Tables["User"]["id"]; - username: Tables["User"]["username"]; - tournamentTeamId: Tables["TournamentTeamMember"]["tournamentTeamId"]; - inGameName: Tables["User"]["inGameName"]; - discordId: Tables["User"]["discordId"]; - customUrl: Tables["User"]["customUrl"]; - discordAvatar: Tables["User"]["discordAvatar"]; - pronouns: Tables["User"]["pronouns"]; - }> - ).filter((player) => player.id), - }; -}; diff --git a/app/features/tournament-bracket/routes/to.$id.brackets.tsx b/app/features/tournament-bracket/routes/to.$id.brackets.tsx index 81dc1284f..7b21d0b9f 100644 --- a/app/features/tournament-bracket/routes/to.$id.brackets.tsx +++ b/app/features/tournament-bracket/routes/to.$id.brackets.tsx @@ -40,8 +40,10 @@ import { Bracket } from "../components/Bracket"; import { useBracketSpoilerCensor } from "../components/Bracket/useBracketSpoilerCensor"; import { BracketMapListDialog } from "../components/BracketMapListDialog"; import { TournamentTeamActions } from "../components/TournamentTeamActions"; +import * as AbDivisions from "../core/AbDivisions"; import type { Bracket as BracketType } from "../core/Bracket"; import * as PreparedMaps from "../core/PreparedMaps"; +import type { Tournament } from "../core/Tournament"; export { action }; @@ -181,10 +183,24 @@ export default function TournamentBracketsPage() { return null; } + const abDivisionsStartError = getAbDivisionsStartError(bracket, tournament); + return (
{bracket.preview && + tournament.isOrganizer(user) && + tournament.regularCheckInHasEnded && + abDivisionsStartError ? ( +
+ +
+ {abDivisionsStartError} +
+
+
+ ) : null} + {bracket.preview && bracket.enoughTeams && tournament.isOrganizer(user) && tournament.regularCheckInStartInThePast ? ( @@ -201,7 +217,11 @@ export default function TournamentBracketsPage() { tournament.isDraft ? ( ) : ( - + ) ) : null} @@ -269,12 +289,41 @@ export default function TournamentBracketsPage() { ); } +function getAbDivisionsStartError( + bracket: BracketType, + tournament: Tournament, +): string | null { + if ( + bracket.type !== "round_robin" || + !bracket.settings?.hasAbDivisions || + !bracket.isStartingBracket || + !bracket.seeding || + bracket.seeding.length === 0 + ) { + return null; + } + + const groupCount = new Set(bracket.data.round.map((r) => r.group_id)).size; + const abDivisionsBySeedOrder = bracket.seeding.map( + (teamId) => tournament.teamById(teamId)?.abDivision, + ); + + const result = AbDivisions.validate({ + abDivisionsBySeedOrder, + groupCount, + }); + + return result.isErr() ? result.error : null; +} + function BracketStarter({ bracket, bracketIdx, + isDisabled, }: { bracket: BracketType; bracketIdx: number; + isDisabled?: boolean; }) { const [dialogOpen, setDialogOpen] = React.useState(false); const isHydrated = useHydrated(); @@ -297,6 +346,7 @@ function BracketStarter({ size="small" data-testid="finalize-bracket-button" onPress={() => setDialogOpen(true)} + isDisabled={isDisabled} > Start the bracket diff --git a/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts b/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts index 1d3fc7331..665ac0e72 100644 --- a/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts +++ b/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts @@ -216,6 +216,94 @@ describe("allLikesByTeamId", () => { }); }); +describe("startLooking", () => { + beforeEach(async () => { + await dbInsertUsers(3); + }); + + afterEach(() => { + dbReset(); + }); + + const createRegisteredTeam = async ( + tournamentId: number, + memberUserIds: number[], + ) => { + const team = await db + .insertInto("TournamentTeam") + .values({ + tournamentId, + name: "Real Team", + inviteCode: `inv-${tournamentId}-${memberUserIds.join("-")}`, + isLooking: 0, + isPlaceholder: 0, + }) + .returning("id") + .executeTakeFirstOrThrow(); + + for (const [idx, userId] of memberUserIds.entries()) { + await db + .insertInto("TournamentTeamMember") + .values({ + tournamentTeamId: team.id, + userId, + role: idx === 0 ? "OWNER" : "REGULAR", + }) + .execute(); + } + + return team; + }; + + test("generates chatCode for a 2+ member team", async () => { + const tournament = await createTournament(); + const team = await createRegisteredTeam(tournament.id, [1, 2]); + + const pickup = await TournamentLFGRepository.startLooking(team.id); + + expect(pickup).not.toBeNull(); + expect(pickup?.chatCode).toMatch(/.+/); + expect(pickup?.memberUserIds.sort()).toEqual([1, 2]); + + const row = await db + .selectFrom("TournamentTeam") + .select("chatCode") + .where("id", "=", team.id) + .executeTakeFirstOrThrow(); + expect(row.chatCode).toBe(pickup?.chatCode); + }); + + test("returns null when team has only 1 member", async () => { + const tournament = await createTournament(); + const team = await createRegisteredTeam(tournament.id, [1]); + + const pickup = await TournamentLFGRepository.startLooking(team.id); + + expect(pickup).toBeNull(); + + const row = await db + .selectFrom("TournamentTeam") + .select("chatCode") + .where("id", "=", team.id) + .executeTakeFirstOrThrow(); + expect(row.chatCode).toBeNull(); + }); + + test("reuses existing chatCode if already set", async () => { + const tournament = await createTournament(); + const team = await createRegisteredTeam(tournament.id, [1, 2]); + await db + .updateTable("TournamentTeam") + .set({ chatCode: "existing-code" }) + .where("id", "=", team.id) + .execute(); + + const pickup = await TournamentLFGRepository.startLooking(team.id); + + expect(pickup?.chatCode).toBe("existing-code"); + }); +}); + describe("mergeTeams", () => { beforeEach(async () => { await dbInsertUsers(5); @@ -298,6 +386,50 @@ describe("mergeTeams", () => { expect(groups).toHaveLength(0); }); + test("survivor gets a chatCode when merged size is 2+", async () => { + const tournament = await createTournament(); + const team1 = await createPlaceholder(tournament.id, 1); + const team2 = await createPlaceholder(tournament.id, 2); + + const result = await TournamentLFGRepository.mergeTeams({ + survivingTeamId: team1.id, + otherTeamId: team2.id, + maxGroupSize: 4, + }); + + expect(result.survivor).not.toBeNull(); + expect(result.survivor?.chatCode).toMatch(/.+/); + expect(result.survivor?.memberUserIds.sort()).toEqual([1, 2]); + expect(result.removedChatCode).toBeNull(); + + const row = await db + .selectFrom("TournamentTeam") + .select("chatCode") + .where("id", "=", team1.id) + .executeTakeFirstOrThrow(); + expect(row.chatCode).toBe(result.survivor?.chatCode); + }); + + test("returns removedChatCode when other team had a chatCode", async () => { + const tournament = await createTournament(); + const team1 = await createPlaceholder(tournament.id, 1); + const team2 = await createPlaceholder(tournament.id, 2); + + await db + .updateTable("TournamentTeam") + .set({ chatCode: "other-code" }) + .where("id", "=", team2.id) + .execute(); + + const result = await TournamentLFGRepository.mergeTeams({ + survivingTeamId: team1.id, + otherTeamId: team2.id, + maxGroupSize: 4, + }); + + expect(result.removedChatCode).toBe("other-code"); + }); + test("clears likes on surviving team after merge", async () => { const tournament = await createTournament(); const team1 = await createPlaceholder(tournament.id, 1); diff --git a/app/features/tournament-lfg/TournamentLFGRepository.server.ts b/app/features/tournament-lfg/TournamentLFGRepository.server.ts index 0cadb1776..26fd51415 100644 --- a/app/features/tournament-lfg/TournamentLFGRepository.server.ts +++ b/app/features/tournament-lfg/TournamentLFGRepository.server.ts @@ -9,11 +9,15 @@ import { errorIsSqliteForeignKeyConstraintFailure } from "~/utils/sql"; import { randomTeamName } from "~/utils/team-name"; export function startLooking(teamId: number) { - return db - .updateTable("TournamentTeam") - .set({ isLooking: 1 }) - .where("id", "=", teamId) - .execute(); + return db.transaction().execute(async (trx) => { + await trx + .updateTable("TournamentTeam") + .set({ isLooking: 1 }) + .where("id", "=", teamId) + .execute(); + + return ensurePickupChatCode(teamId, trx); + }); } type CreatePlaceholderTeamArgs = { @@ -167,6 +171,12 @@ export function mergeTeams({ maxGroupSize: number; }) { return db.transaction().execute(async (trx) => { + const otherTeam = await trx + .selectFrom("TournamentTeam") + .select("chatCode") + .where("id", "=", otherTeamId) + .executeTakeFirst(); + const otherMembers = await trx .selectFrom("TournamentTeamMember") .select(["TournamentTeamMember.userId", "TournamentTeamMember.role"]) @@ -207,6 +217,13 @@ export function mergeTeams({ }) .where("id", "=", survivingTeamId) .execute(); + + const survivor = await ensurePickupChatCode(survivingTeamId, trx); + + return { + survivor, + removedChatCode: otherTeam?.chatCode ?? null, + }; }); } @@ -409,3 +426,44 @@ async function getMemberCount( return members.length; } + +export type PickupChatTeam = { + chatCode: string; + name: string; + memberUserIds: number[]; +}; + +async function ensurePickupChatCode( + teamId: number, + trx: Transaction, +): Promise { + const team = await trx + .selectFrom("TournamentTeam") + .select(["name", "chatCode"]) + .where("id", "=", teamId) + .executeTakeFirstOrThrow(); + + const members = await trx + .selectFrom("TournamentTeamMember") + .select("userId") + .where("tournamentTeamId", "=", teamId) + .execute(); + + if (members.length < 2) return null; + + let chatCode = team.chatCode; + if (!chatCode) { + chatCode = shortNanoid(); + await trx + .updateTable("TournamentTeam") + .set({ chatCode }) + .where("id", "=", teamId) + .execute(); + } + + return { + chatCode, + name: team.name, + memberUserIds: members.map((m) => m.userId), + }; +} diff --git a/app/features/tournament-lfg/actions/to.$id.looking.server.ts b/app/features/tournament-lfg/actions/to.$id.looking.server.ts index 5c6b1f9d9..b9b128976 100644 --- a/app/features/tournament-lfg/actions/to.$id.looking.server.ts +++ b/app/features/tournament-lfg/actions/to.$id.looking.server.ts @@ -1,5 +1,6 @@ import type { ActionFunctionArgs } from "react-router"; import { requireUser } from "~/features/auth/core/user.server"; +import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server"; import { notify } from "~/features/notifications/core/notify.server"; import { requireNotBannedByOrganization } from "~/features/tournament/tournament-utils.server"; import { @@ -16,6 +17,7 @@ import { idObject } from "~/utils/zod"; import * as TournamentLFGRepository from "../TournamentLFGRepository.server"; import { lookingSchema } from "../tournament-lfg-schemas"; import { survivingTeamId } from "../tournament-lfg-utils"; +import { setPickupChatMetadata } from "../tournament-lfg-utils.server"; export const action = async ({ request, params }: ActionFunctionArgs) => { const user = requireUser(); @@ -72,7 +74,18 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { team.members.length < tournament.maxMembersPerTeam, "Team is already at max capacity", ); - await TournamentLFGRepository.startLooking(team.id); + const pickup = await TournamentLFGRepository.startLooking(team.id); + if (pickup) { + setPickupChatMetadata({ + team: pickup, + tournament: { + id: tournamentId, + name: tournament.ctx.name, + logoUrl: tournament.ctx.logoUrl, + startTime: tournament.ctx.startTime, + }, + }); + } } else { await TournamentLFGRepository.createPlaceholderTeam({ tournamentId, @@ -181,12 +194,28 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { user, }); - await TournamentLFGRepository.mergeTeams({ + const mergeResult = await TournamentLFGRepository.mergeTeams({ survivingTeamId: surviving, otherTeamId: otherGroup.id, maxGroupSize: tournament.maxMembersPerTeam, }); + if (mergeResult.removedChatCode) { + ChatSystemMessage.removeRoom(mergeResult.removedChatCode); + } + + if (mergeResult.survivor) { + setPickupChatMetadata({ + team: mergeResult.survivor, + tournament: { + id: tournamentId, + name: tournament.ctx.name, + logoUrl: tournament.ctx.logoUrl, + startTime: tournament.ctx.startTime, + }, + }); + } + notify({ userIds: theirGroup.members.map((m) => m.id), notification: { diff --git a/app/features/tournament-lfg/loaders/to.$id.looking.server.ts b/app/features/tournament-lfg/loaders/to.$id.looking.server.ts index 2e8e055e2..cbf51fe81 100644 --- a/app/features/tournament-lfg/loaders/to.$id.looking.server.ts +++ b/app/features/tournament-lfg/loaders/to.$id.looking.server.ts @@ -29,6 +29,10 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { throw new Response(null, { status: 404 }); } + if (tournament.isInvitational) { + throw new Response(null, { status: 404 }); + } + if (tournament.isLeagueSignup && !tournament.registrationOpen) { throw new Response(null, { status: 404 }); } diff --git a/app/features/tournament-lfg/tournament-lfg-utils.server.ts b/app/features/tournament-lfg/tournament-lfg-utils.server.ts new file mode 100644 index 000000000..902676a02 --- /dev/null +++ b/app/features/tournament-lfg/tournament-lfg-utils.server.ts @@ -0,0 +1,34 @@ +import { add } from "date-fns"; +import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server"; +import { tournamentSubsPage } from "~/utils/urls"; + +const PICKUP_CHAT_EXPIRES_AFTER_DAYS = 7; + +export function setPickupChatMetadata({ + team, + tournament, +}: { + team: { + chatCode: string; + name: string; + memberUserIds: number[]; + }; + tournament: { + id: number; + name: string; + logoUrl: string | null; + startTime: Date; + }; +}) { + return ChatSystemMessage.setMetadata({ + chatCode: team.chatCode, + header: team.name, + subtitle: tournament.name, + url: tournamentSubsPage(tournament.id), + imageUrl: tournament.logoUrl ?? undefined, + participantUserIds: team.memberUserIds, + expiresAt: add(tournament.startTime, { + days: PICKUP_CHAT_EXPIRES_AFTER_DAYS, + }), + }); +} diff --git a/app/features/tournament/TournamentRepository.server.ts b/app/features/tournament/TournamentRepository.server.ts index 979353454..5b76db9cb 100644 --- a/app/features/tournament/TournamentRepository.server.ts +++ b/app/features/tournament/TournamentRepository.server.ts @@ -150,6 +150,7 @@ export async function findById(id: number) { "TournamentTeam.createdAt", "TournamentTeam.activeRosterUserIds", "TournamentTeam.startingBracketIdx", + "TournamentTeam.abDivision", concatUserSubmittedImagePrefix( innerEb.ref("UserSubmittedImage.url"), ).as("pickupAvatarUrl"), @@ -464,6 +465,7 @@ export function forShowcase() { "Tournament.id", "Tournament.settings", "Tournament.tier", + "Tournament.isFinalized", "CalendarEvent.authorId", "CalendarEvent.name", "CalendarEvent.organizationId", @@ -646,68 +648,6 @@ export async function friendCodesByTournamentId(tournamentId: number) { ); } -export function checkIn({ - tournamentTeamId, - bracketIdx, -}: { - tournamentTeamId: number; - bracketIdx: number | null; -}) { - return db.transaction().execute(async (trx) => { - let query = trx - .deleteFrom("TournamentTeamCheckIn") - .where("TournamentTeamCheckIn.tournamentTeamId", "=", tournamentTeamId) - .where("TournamentTeamCheckIn.isCheckOut", "=", 1); - - if (typeof bracketIdx === "number") { - query = query.where("TournamentTeamCheckIn.bracketIdx", "=", bracketIdx); - } - - await query.execute(); - - await trx - .insertInto("TournamentTeamCheckIn") - .values({ - checkedInAt: dateToDatabaseTimestamp(new Date()), - tournamentTeamId, - bracketIdx, - }) - .execute(); - }); -} - -export function checkOut({ - tournamentTeamId, - bracketIdx, -}: { - tournamentTeamId: number; - bracketIdx: number | null; -}) { - return db.transaction().execute(async (trx) => { - let query = trx - .deleteFrom("TournamentTeamCheckIn") - .where("TournamentTeamCheckIn.tournamentTeamId", "=", tournamentTeamId); - - if (typeof bracketIdx === "number") { - query = query.where("TournamentTeamCheckIn.bracketIdx", "=", bracketIdx); - } - - await query.execute(); - - if (typeof bracketIdx === "number") { - await trx - .insertInto("TournamentTeamCheckIn") - .values({ - checkedInAt: dateToDatabaseTimestamp(new Date()), - tournamentTeamId, - bracketIdx, - isCheckOut: 1, - }) - .execute(); - } - }); -} - export function updateProgression({ tournamentId, bracketProgression, @@ -800,56 +740,6 @@ export function overrideTeamBracketProgression({ .execute(); } -export function updateTeamName({ - tournamentTeamId, - name, -}: { - tournamentTeamId: number; - name: string; -}) { - return db - .updateTable("TournamentTeam") - .set({ - name, - }) - .where("id", "=", tournamentTeamId) - .execute(); -} - -export function dropTeamOut({ - tournamentTeamId, - previewBracketIdxs, -}: { - tournamentTeamId: number; - previewBracketIdxs: number[]; -}) { - return db.transaction().execute(async (trx) => { - await trx - .deleteFrom("TournamentTeamCheckIn") - .where("tournamentTeamId", "=", tournamentTeamId) - .where("TournamentTeamCheckIn.bracketIdx", "in", previewBracketIdxs) - .execute(); - - await trx - .updateTable("TournamentTeam") - .set({ - droppedOut: 1, - }) - .where("id", "=", tournamentTeamId) - .execute(); - }); -} - -export function undoDropTeamOut(tournamentTeamId: number) { - return db - .updateTable("TournamentTeam") - .set({ - droppedOut: 0, - }) - .where("id", "=", tournamentTeamId) - .execute(); -} - export function addStaff({ tournamentId, userId, diff --git a/app/features/tournament/TournamentTeamRepository.server.ts b/app/features/tournament/TournamentTeamRepository.server.ts index 3ef2f11c5..0661e20e4 100644 --- a/app/features/tournament/TournamentTeamRepository.server.ts +++ b/app/features/tournament/TournamentTeamRepository.server.ts @@ -1,12 +1,11 @@ -// TODO: add rest of the functions here that relate more to tournament teams than tournament/bracket - import type { Transaction } from "kysely"; import { sql } from "kysely"; import { db } from "~/db/sql"; import type { DB, Tables } from "~/db/tables"; +import type { MapPool } from "~/features/map-list-generator/core/map-pool"; import type { ModeShort, StageId } from "~/modules/in-game-lists/types"; import { flatZip } from "~/utils/arrays"; -import { databaseTimestampNow } from "~/utils/dates"; +import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates"; import { shortNanoid } from "~/utils/id"; import invariant from "~/utils/invariant"; @@ -113,13 +112,11 @@ export function create({ avatarFileName, userId, tournamentId, - ownerInGameName, }: { team: Pick; avatarFileName?: string; userId: number; tournamentId: number; - ownerInGameName: string | null; }) { return db.transaction().execute(async (trx) => { const avatarImgId = avatarFileName @@ -143,13 +140,15 @@ export function create({ .returning("id") .executeTakeFirstOrThrow(); + const inGameName = await resolveInGameName(trx, tournamentId, userId); + await trx .insertInto("TournamentTeamMember") .values({ tournamentTeamId: tournamentTeam.id, userId, role: "OWNER", - inGameName: ownerInGameName, + inGameName, }) .execute(); @@ -157,6 +156,30 @@ export function create({ }); } +async function resolveInGameName( + trx: Transaction, + tournamentId: number, + userId: number, +) { + const tournament = await trx + .selectFrom("Tournament") + .select("Tournament.settings") + .where("Tournament.id", "=", tournamentId) + .executeTakeFirstOrThrow(); + + if (!tournament.settings.requireInGameNames) return null; + + const user = await trx + .selectFrom("User") + .select("User.inGameName") + .where("User.id", "=", userId) + .executeTakeFirstOrThrow(); + + invariant(user.inGameName, "In-game name is required but not set"); + + return user.inGameName; +} + export function copyFromAnotherTournament({ tournamentTeamId, destinationTournamentId, @@ -357,6 +380,284 @@ export function updateStartingBrackets( }); } +export function updateAbDivisions( + abDivisions: { + tournamentTeamId: number; + abDivision: 0 | 1 | null; + }[], +) { + const grouped = Object.groupBy(abDivisions, (ab) => String(ab.abDivision)); + + return db.transaction().execute(async (trx) => { + for (const [abDivisionKey, tournamentTeams = []] of Object.entries( + grouped, + )) { + if (tournamentTeams.length === 0) continue; + + await trx + .updateTable("TournamentTeam") + .set({ + abDivision: abDivisionKey === "null" ? null : Number(abDivisionKey), + }) + .where( + "TournamentTeam.id", + "in", + tournamentTeams.map((t) => t.tournamentTeamId), + ) + .execute(); + } + }); +} + +/** + * Checks in a tournament team. Clears any existing check-out records before inserting the check-in. + * When called without `bracketIdx`, checks in for the whole tournament. + * When called with `bracketIdx`, checks in for a specific bracket (e.g. after progression). + */ +export function checkIn( + tournamentTeamId: number, + options?: { bracketIdx: number }, +) { + const bracketIdx = options?.bracketIdx ?? null; + + return db.transaction().execute(async (trx) => { + let query = trx + .deleteFrom("TournamentTeamCheckIn") + .where("TournamentTeamCheckIn.tournamentTeamId", "=", tournamentTeamId) + .where("TournamentTeamCheckIn.isCheckOut", "=", 1); + + if (typeof bracketIdx === "number") { + query = query.where("TournamentTeamCheckIn.bracketIdx", "=", bracketIdx); + } + + await query.execute(); + + await trx + .insertInto("TournamentTeamCheckIn") + .values({ + checkedInAt: dateToDatabaseTimestamp(new Date()), + tournamentTeamId, + bracketIdx, + }) + .execute(); + }); +} + +export function checkOut({ + tournamentTeamId, + bracketIdx, +}: { + tournamentTeamId: number; + bracketIdx: number | null; +}) { + return db.transaction().execute(async (trx) => { + let query = trx + .deleteFrom("TournamentTeamCheckIn") + .where("TournamentTeamCheckIn.tournamentTeamId", "=", tournamentTeamId); + + if (typeof bracketIdx === "number") { + query = query.where("TournamentTeamCheckIn.bracketIdx", "=", bracketIdx); + } + + await query.execute(); + + if (typeof bracketIdx === "number") { + await trx + .insertInto("TournamentTeamCheckIn") + .values({ + checkedInAt: dateToDatabaseTimestamp(new Date()), + tournamentTeamId, + bracketIdx, + isCheckOut: 1, + }) + .execute(); + } + }); +} + +export function updateName({ + tournamentTeamId, + name, +}: { + tournamentTeamId: number; + name: string; +}) { + return db + .updateTable("TournamentTeam") + .set({ + name, + }) + .where("id", "=", tournamentTeamId) + .execute(); +} + +export function dropOut({ + tournamentTeamId, + previewBracketIdxs, +}: { + tournamentTeamId: number; + previewBracketIdxs: number[]; +}) { + return db.transaction().execute(async (trx) => { + await trx + .deleteFrom("TournamentTeamCheckIn") + .where("tournamentTeamId", "=", tournamentTeamId) + .where("TournamentTeamCheckIn.bracketIdx", "in", previewBracketIdxs) + .execute(); + + await trx + .updateTable("TournamentTeam") + .set({ + droppedOut: 1, + }) + .where("id", "=", tournamentTeamId) + .execute(); + }); +} + +export function undoDropOut(tournamentTeamId: number) { + return db + .updateTable("TournamentTeam") + .set({ + droppedOut: 0, + }) + .where("id", "=", tournamentTeamId) + .execute(); +} + +export function join({ + previousTeamId, + whatToDoWithPreviousTeam, + newTeamId, + userId, + checkOutTeam = false, +}: { + previousTeamId?: number; + whatToDoWithPreviousTeam?: "LEAVE" | "DELETE"; + newTeamId: number; + userId: number; + checkOutTeam?: boolean; +}) { + return db.transaction().execute(async (trx) => { + if (whatToDoWithPreviousTeam === "DELETE") { + await trx + .deleteFrom("TournamentTeam") + .where("TournamentTeam.id", "=", previousTeamId!) + .execute(); + } else if (whatToDoWithPreviousTeam === "LEAVE") { + await trx + .deleteFrom("TournamentTeamMember") + .where("TournamentTeamMember.tournamentTeamId", "=", previousTeamId!) + .where("TournamentTeamMember.userId", "=", userId) + .execute(); + } + + if (checkOutTeam) { + invariant( + previousTeamId, + "previousTeamId is required when checking out team", + ); + await trx + .deleteFrom("TournamentTeamCheckIn") + .where("TournamentTeamCheckIn.tournamentTeamId", "=", previousTeamId) + .execute(); + } + + const tournamentId = ( + await trx + .selectFrom("TournamentTeam") + .select("TournamentTeam.tournamentId") + .where("TournamentTeam.id", "=", newTeamId) + .executeTakeFirstOrThrow() + ).tournamentId; + + const inGameName = await resolveInGameName(trx, tournamentId, userId); + + await trx + .insertInto("TournamentTeamMember") + .values({ + tournamentTeamId: newTeamId, + userId, + inGameName, + }) + .execute(); + }); +} + +export function del(tournamentTeamId: number) { + return db.transaction().execute(async (trx) => { + await trx + .deleteFrom("MapPoolMap") + .where("MapPoolMap.tournamentTeamId", "=", tournamentTeamId) + .execute(); + + await trx + .deleteFrom("TournamentTeam") + .where("TournamentTeam.id", "=", tournamentTeamId) + .execute(); + }); +} + +export function leave({ teamId, userId }: { teamId: number; userId: number }) { + return db + .deleteFrom("TournamentTeamMember") + .where("TournamentTeamMember.tournamentTeamId", "=", teamId) + .where("TournamentTeamMember.userId", "=", userId) + .execute(); +} + +export function transferOwnership( + tournamentTeamId: number, + { + oldCaptainId, + newCaptainId, + }: { oldCaptainId: number; newCaptainId: number }, +) { + return db.transaction().execute(async (trx) => { + await trx + .updateTable("TournamentTeamMember") + .set({ role: "REGULAR" }) + .where("TournamentTeamMember.tournamentTeamId", "=", tournamentTeamId) + .where("TournamentTeamMember.userId", "=", oldCaptainId) + .execute(); + + await trx + .updateTable("TournamentTeamMember") + .set({ role: "OWNER" }) + .where("TournamentTeamMember.tournamentTeamId", "=", tournamentTeamId) + .where("TournamentTeamMember.userId", "=", newCaptainId) + .execute(); + }); +} + +export function upsertCounterpickMaps({ + tournamentTeamId, + mapPool, +}: { + tournamentTeamId: Tables["TournamentTeam"]["id"]; + mapPool: MapPool; +}) { + return db.transaction().execute(async (trx) => { + await trx + .deleteFrom("MapPoolMap") + .where("MapPoolMap.tournamentTeamId", "=", tournamentTeamId) + .execute(); + + if (mapPool.stageModePairs.length > 0) { + await trx + .insertInto("MapPoolMap") + .values( + mapPool.stageModePairs.map(({ stageId, mode }) => ({ + tournamentTeamId, + stageId, + mode, + })), + ) + .execute(); + } + }); +} + async function findTeamRecentMaps(teamId: number, limit: number) { return db .selectFrom("TournamentMatchGameResult") @@ -375,6 +676,14 @@ async function findTeamRecentMaps(teamId: number, limit: number) { .execute(); } +export function findByInviteCode(inviteCode: string) { + return db + .selectFrom("TournamentTeam") + .select(["TournamentTeam.id", "TournamentTeam.tournamentId"]) + .where("TournamentTeam.inviteCode", "=", inviteCode) + .executeTakeFirst(); +} + export async function findRecentlyPlayedMapsByIds({ teamIds, limit = 5, diff --git a/app/features/tournament/actions/to.$id.admin.server.ts b/app/features/tournament/actions/to.$id.admin.server.ts index 31d5addf9..729e4ca66 100644 --- a/app/features/tournament/actions/to.$id.admin.server.ts +++ b/app/features/tournament/actions/to.$id.admin.server.ts @@ -3,6 +3,7 @@ import * as R from "remeda"; import { DANGEROUS_CAN_ACCESS_DEV_CONTROLS } from "~/features/admin/core/dev-controls"; import { requireUser } from "~/features/auth/core/user.server"; import { userIsBanned } from "~/features/ban/core/banned.server"; +import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server"; import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server"; import { notify } from "~/features/notifications/core/notify.server"; import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server"; @@ -12,6 +13,10 @@ import { clearTournamentDataCache, tournamentFromDB, } from "~/features/tournament-bracket/core/Tournament.server"; +import { + tournamentMatchWebsocketRoom, + tournamentWebsocketRoom, +} from "~/features/tournament-bracket/tournament-bracket-utils"; import * as TournamentLFGRepository from "~/features/tournament-lfg/TournamentLFGRepository.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; import invariant from "~/utils/invariant"; @@ -25,15 +30,9 @@ import { } from "~/utils/remix.server"; import { assertUnreachable } from "~/utils/types"; import { idObject } from "../../../utils/zod"; -import { changeTeamOwner } from "../queries/changeTeamOwner.server"; -import { deleteTeam } from "../queries/deleteTeam.server"; -import { joinTeam, leaveTeam } from "../queries/joinLeaveTeam.server"; import * as TournamentRepository from "../TournamentRepository.server"; import { adminActionSchema } from "../tournament-schemas.server"; -import { - endDroppedTeamMatches, - inGameNameIfNeeded, -} from "../tournament-utils.server"; +import { endDroppedTeamMatches } from "../tournament-utils.server"; export const action: ActionFunction = async ({ request, params }) => { const user = requireUser(); @@ -65,16 +64,14 @@ export const action: ActionFunction = async ({ request, params }) => { !tournament.teamMemberOfByUser({ id: data.userId }), "User already on a team", ); + const addTeamUser = await UserRepository.findLeanById(data.userId); + errorToastIfFalsy(addTeamUser?.friendCode, "User has no friend code set"); errorToastIfFalsy( - (await UserRepository.findLeanById(data.userId))?.friendCode, - "User has no friend code set", + !tournament.ctx.settings.requireInGameNames || addTeamUser?.inGameName, + "User has no in-game name set", ); await TournamentTeamRepository.create({ - ownerInGameName: await inGameNameIfNeeded({ - tournament, - userId: data.userId, - }), team: { name: data.teamName, prefersNotToHost: 0, @@ -107,10 +104,9 @@ export const action: ActionFunction = async ({ request, params }) => { const newCaptain = team.members.find((m) => m.userId === data.memberId); errorToastIfFalsy(newCaptain, "Invalid member id"); - changeTeamOwner({ - newCaptainId: data.memberId, + await TournamentTeamRepository.transferOwnership(data.teamId, { oldCaptainId: oldCaptain.userId, - tournamentTeamId: data.teamId, + newCaptainId: data.memberId, }); message = "Team owner changed"; @@ -121,7 +117,7 @@ export const action: ActionFunction = async ({ request, params }) => { const team = tournament.teamById(data.teamId); errorToastIfFalsy(team, "Invalid team id"); - await TournamentRepository.updateTeamName({ + await TournamentTeamRepository.updateName({ tournamentTeamId: data.teamId, name: data.teamName, }); @@ -147,11 +143,11 @@ export const action: ActionFunction = async ({ request, params }) => { invariant(bracket, "Invalid bracket idx"); errorToastIfFalsy(bracket.preview, "Bracket has been started"); - await TournamentRepository.checkIn({ - tournamentTeamId: data.teamId, + await TournamentTeamRepository.checkIn( + data.teamId, // no sources = regular check in - bracketIdx: !bracket.sources ? null : data.bracketIdx, - }); + bracket.sources ? { bracketIdx: data.bracketIdx } : undefined, + ); message = "Checked team in"; break; @@ -169,7 +165,7 @@ export const action: ActionFunction = async ({ request, params }) => { invariant(bracket, "Invalid bracket idx"); errorToastIfFalsy(bracket.preview, "Bracket has been started"); - await TournamentRepository.checkOut({ + await TournamentTeamRepository.checkOut({ tournamentTeamId: data.teamId, // no sources = regular check in bracketIdx: !bracket.sources ? null : data.bracketIdx, @@ -209,7 +205,7 @@ export const action: ActionFunction = async ({ request, params }) => { }); } - leaveTeam({ + await TournamentTeamRepository.leave({ userId: data.memberId, teamId: team.id, }); @@ -245,16 +241,22 @@ export const action: ActionFunction = async ({ request, params }) => { "User trying to be added currently has an active ban from sendou.ink", ); + const addMemberUser = await UserRepository.findLeanById(data.userId); errorToastIfFalsy( - (await UserRepository.findLeanById(data.userId))?.friendCode, + addMemberUser?.friendCode, "User has no friend code set", ); + errorToastIfFalsy( + !tournament.ctx.settings.requireInGameNames || + addMemberUser?.inGameName, + "User has no in-game name set", + ); await TournamentLFGRepository.leaveLfg({ userId: data.userId, tournamentId, }); - joinTeam({ + await TournamentTeamRepository.join({ userId: data.userId, newTeamId: team.id, previousTeamId: previousTeam?.id, @@ -265,11 +267,6 @@ export const action: ActionFunction = async ({ request, params }) => { tournament.hasStarted ? "DELETE" : undefined, - tournamentId, - inGameName: await inGameNameIfNeeded({ - tournament, - userId: data.userId, - }), }); ShowcaseTournaments.addToCached({ @@ -305,7 +302,7 @@ export const action: ActionFunction = async ({ request, params }) => { errorToastIfFalsy(team, "Invalid team id"); errorToastIfFalsy(!tournament.hasStarted, "Tournament has started"); - deleteTeam(team.id); + await TournamentTeamRepository.del(team.id); for (const member of team.members) { ShowcaseTournaments.removeFromCached({ @@ -396,26 +393,41 @@ export const action: ActionFunction = async ({ request, params }) => { }); } - endDroppedTeamMatches({ + const endedMatchIds = endDroppedTeamMatches({ tournament, manager: getServerTournamentManager(), droppedTeamId: data.teamId, }); - await TournamentRepository.dropTeamOut({ + await TournamentTeamRepository.dropOut({ tournamentTeamId: data.teamId, previewBracketIdxs: tournament.brackets.flatMap((b, idx) => b.preview ? idx : [], ), }); + if (endedMatchIds.length > 0) { + ChatSystemMessage.send([ + ...endedMatchIds.map((matchId) => ({ + room: tournamentMatchWebsocketRoom(matchId), + type: "TOURNAMENT_MATCH_UPDATED" as const, + revalidateOnly: true as const, + })), + { + room: tournamentWebsocketRoom(tournament.ctx.id), + type: "TOURNAMENT_UPDATED" as const, + revalidateOnly: true as const, + }, + ]); + } + message = "Team dropped out"; break; } case "UNDO_DROP_TEAM_OUT": { validateIsTournamentOrganizer(); - await TournamentRepository.undoDropTeamOut(data.teamId); + await TournamentTeamRepository.undoDropOut(data.teamId); message = "Team drop out undone"; break; diff --git a/app/features/tournament/actions/to.$id.join.server.ts b/app/features/tournament/actions/to.$id.join.server.ts index 4dc5f70bb..e3f936462 100644 --- a/app/features/tournament/actions/to.$id.join.server.ts +++ b/app/features/tournament/actions/to.$id.join.server.ts @@ -2,6 +2,7 @@ import type { ActionFunction } from "react-router"; import { redirect } from "react-router"; import { requireUser } from "~/features/auth/core/user.server"; import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server"; +import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server"; import { clearTournamentDataCache, tournamentFromDB, @@ -16,12 +17,10 @@ import { } from "~/utils/remix.server"; import { tournamentPage } from "~/utils/urls"; import { idObject } from "~/utils/zod"; -import { findByInviteCode } from "../queries/findTeamByInviteCode.server"; -import { joinTeam } from "../queries/joinLeaveTeam.server"; import { validateCanJoinTeam } from "../tournament-utils"; import { - inGameNameIfNeeded, requireNotBannedByOrganization, + requireSendouQParticipationIfNeeded, } from "../tournament-utils.server"; export const action: ActionFunction = async ({ request, params }) => { @@ -34,7 +33,9 @@ export const action: ActionFunction = async ({ request, params }) => { const inviteCode = url.searchParams.get("code"); invariant(inviteCode, "code is missing"); - const leanTeam = notFoundIfFalsy(findByInviteCode(inviteCode)); + const leanTeam = notFoundIfFalsy( + await TournamentTeamRepository.findByInviteCode(inviteCode), + ); const tournament = await tournamentFromDB({ tournamentId, user }); @@ -42,6 +43,10 @@ export const action: ActionFunction = async ({ request, params }) => { tournament, user, }); + await requireSendouQParticipationIfNeeded({ + tournament, + userId: user.id, + }); const teamToJoin = tournament.ctx.teams.find( (team) => team.id === leanTeam.id, @@ -83,7 +88,7 @@ export const action: ActionFunction = async ({ request, params }) => { : "LEAVE"; await TournamentLFGRepository.leaveLfg({ userId: user.id, tournamentId }); - joinTeam({ + await TournamentTeamRepository.join({ userId: user.id, newTeamId: teamToJoin.id, previousTeamId: previousTeam?.id, @@ -94,11 +99,6 @@ export const action: ActionFunction = async ({ request, params }) => { previousTeam && previousTeam.members.length <= tournament.minMembersPerTeam, whatToDoWithPreviousTeam, - tournamentId, - inGameName: await inGameNameIfNeeded({ - tournament, - userId: user.id, - }), }); ShowcaseTournaments.addToCached({ diff --git a/app/features/tournament/actions/to.$id.register.server.ts b/app/features/tournament/actions/to.$id.register.server.ts index 77ac0a1fd..07c0c7eb8 100644 --- a/app/features/tournament/actions/to.$id.register.server.ts +++ b/app/features/tournament/actions/to.$id.register.server.ts @@ -22,12 +22,6 @@ import { } from "~/utils/remix.server"; import { assertUnreachable } from "~/utils/types"; import { idObject } from "~/utils/zod"; -import { checkIn } from "../queries/checkIn.server"; -import { deleteTeam } from "../queries/deleteTeam.server"; -import deleteTeamMember from "../queries/deleteTeamMember.server"; -import { findOwnTournamentTeam } from "../queries/findOwnTournamentTeam.server"; -import { joinTeam } from "../queries/joinLeaveTeam.server"; -import { upsertCounterpickMaps } from "../queries/upsertCounterpickMaps.server"; import { TOURNAMENT } from "../tournament-constants"; import { registerSchema } from "../tournament-schemas.server"; import { @@ -35,8 +29,8 @@ import { validateCounterPickMapPool, } from "../tournament-utils"; import { - inGameNameIfNeeded, requireNotBannedByOrganization, + requireSendouQParticipationIfNeeded, } from "../tournament-utils.server"; export const action: ActionFunction = async ({ request, params }) => { @@ -101,6 +95,10 @@ export const action: ActionFunction = async ({ request, params }) => { tournament, user, }); + await requireSendouQParticipationIfNeeded({ + tournament, + userId: user.id, + }); errorToastIfFalsy(!tournament.isInvitational, "Event is invite only"); errorToastIfFalsy( @@ -125,10 +123,6 @@ export const action: ActionFunction = async ({ request, params }) => { tournamentId, }); await TournamentTeamRepository.create({ - ownerInGameName: await inGameNameIfNeeded({ - tournament, - userId: user.id, - }), team: { name: data.teamName, prefersNotToHost: Number(data.prefersNotToHost), @@ -160,20 +154,18 @@ export const action: ActionFunction = async ({ request, params }) => { ); errorToastIfFalsy(data.userId !== user.id, "Can't kick yourself"); - const detailedOwnTeam = findOwnTournamentTeam({ - tournamentId, - userId: user.id, - }); // making sure they aren't unfilling one checking in condition i.e. having full roster // and then having members kicked without it affecting the checking in status errorToastIfFalsy( - detailedOwnTeam && - (!detailedOwnTeam.checkedInAt || - ownTeam.members.length > tournament.minMembersPerTeam), + !ownTeamCheckedIn || + ownTeam.members.length > tournament.minMembersPerTeam, "Can't kick a member after checking in", ); - deleteTeamMember({ tournamentTeamId: ownTeam.id, userId: data.userId }); + await TournamentTeamRepository.leave({ + teamId: ownTeam.id, + userId: data.userId, + }); ShowcaseTournaments.removeFromCached({ tournamentId, @@ -192,8 +184,8 @@ export const action: ActionFunction = async ({ request, params }) => { "You cannot leave after checking in", ); - deleteTeamMember({ - tournamentTeamId: teamMemberOf.id, + await TournamentTeamRepository.leave({ + teamId: teamMemberOf.id, userId: user.id, }); @@ -220,7 +212,7 @@ export const action: ActionFunction = async ({ request, params }) => { "Invalid map pool", ); - upsertCounterpickMaps({ + await TournamentTeamRepository.upsertCounterpickMaps({ tournamentTeamId: ownTeam.id, mapPool: new MapPool(data.mapPool), }); @@ -248,7 +240,7 @@ export const action: ActionFunction = async ({ request, params }) => { `Can't check-in - ${tournament.checkInConditionsFulfilledByTeamId(teamMemberOf.id).reason}`, ); - checkIn(teamMemberOf.id); + await TournamentTeamRepository.checkIn(teamMemberOf.id); logger.info( `Checking in (success): tournament team id: ${teamMemberOf.id} - user id: ${user.id} - tournament id: ${tournamentId}`, ); @@ -279,19 +271,18 @@ export const action: ActionFunction = async ({ request, params }) => { user: { id: data.userId }, message: "The user is banned from events hosted by this organization", }); + await requireSendouQParticipationIfNeeded({ + tournament, + userId: data.userId, + }); await TournamentLFGRepository.leaveLfg({ userId: data.userId, tournamentId, }); - joinTeam({ + await TournamentTeamRepository.join({ userId: data.userId, newTeamId: ownTeam.id, - tournamentId, - inGameName: await inGameNameIfNeeded({ - tournament, - userId: data.userId, - }), }); await SavedCalendarEventRepository.unsave({ @@ -335,7 +326,7 @@ export const action: ActionFunction = async ({ request, params }) => { "Unregistering from leagues is not possible after registration has closed", ); - deleteTeam(ownTeam.id); + await TournamentTeamRepository.del(ownTeam.id); for (const member of ownTeam.members) { ShowcaseTournaments.removeFromCached({ diff --git a/app/features/tournament/actions/to.$id.seeds.server.ts b/app/features/tournament/actions/to.$id.seeds.server.ts index d27501387..df4539e07 100644 --- a/app/features/tournament/actions/to.$id.seeds.server.ts +++ b/app/features/tournament/actions/to.$id.seeds.server.ts @@ -68,6 +68,23 @@ export const action: ActionFunction = async ({ request, params }) => { ); break; } + case "UPDATE_AB_DIVISIONS": { + errorToastIfFalsy( + tournament.ctx.settings.bracketProgression.some( + (bracket) => !bracket.sources && bracket.settings?.hasAbDivisions, + ), + "No starting bracket has A/B divisions enabled", + ); + + const validTeamIds = new Set(tournament.ctx.teams.map((t) => t.id)); + errorToastIfFalsy( + data.abDivisions.every((t) => validTeamIds.has(t.tournamentTeamId)), + "Invalid tournament team id", + ); + + await TournamentTeamRepository.updateAbDivisions(data.abDivisions); + break; + } } clearTournamentDataCache(tournamentId); diff --git a/app/features/tournament/core/Standings.test.ts b/app/features/tournament/core/Standings.test.ts new file mode 100644 index 000000000..4a6193390 --- /dev/null +++ b/app/features/tournament/core/Standings.test.ts @@ -0,0 +1,253 @@ +import { describe, expect, it } from "vitest"; +import { + progressions, + testTournament, + tournamentCtxTeam, +} from "~/features/tournament-bracket/core/tests/test-utils"; +import { BracketsManager } from "~/modules/brackets-manager"; +import { InMemoryDatabase } from "~/modules/brackets-memory-db"; +import invariant from "~/utils/invariant"; +import { reNumberPlacements, tournamentStandings } from "./Standings"; + +describe("tournamentStandings", () => { + it("returns single-division standings for a tournament with one starting bracket", () => { + const tournament = singleEliminationTournament(); + + const result = tournamentStandings(tournament); + + expect(result.type).toBe("single"); + invariant(result.type === "single"); + expect(result.standings.length).toBeGreaterThan(0); + expect(result.standings[0].placement).toBe(1); + expect(result.standings[0].team.id).toBe(1); + }); + + it("returns one div per starting bracket for a tournament with multiple starting brackets", () => { + const tournament = testTournament({ + ctx: { + settings: { bracketProgression: progressions.manyStartBrackets }, + teams: [ + tournamentCtxTeam(1, { startingBracketIdx: 0, seed: 1 }), + tournamentCtxTeam(2, { startingBracketIdx: 0, seed: 2 }), + tournamentCtxTeam(3, { startingBracketIdx: 1, seed: 3 }), + tournamentCtxTeam(4, { startingBracketIdx: 1, seed: 4 }), + ], + }, + }); + + const result = tournamentStandings(tournament); + + expect(result.type).toBe("multi"); + invariant(result.type === "multi"); + expect(result.standings).toHaveLength(2); + for (const { div } of result.standings) { + expect(typeof div).toBe("string"); + expect(div.length).toBeGreaterThan(0); + } + const divs = result.standings.map((s) => s.div); + expect(new Set(divs).size).toBe(2); + }); + + it("splits A/B divisions finals into 'A' and 'B' divs with teams partitioned by abDivision", () => { + const tournament = abDivisionsTournament(); + + const result = tournamentStandings(tournament); + + expect(result.type).toBe("multi"); + invariant(result.type === "multi"); + expect(result.standings.map((s) => s.div)).toEqual(["A", "B"]); + + const [a, b] = result.standings; + expect(a.standings.map((s) => s.team.id)).toEqual([1, 3]); + expect(b.standings.map((s) => s.team.id)).toEqual([2, 4]); + expect(a.standings.every((s) => s.team.abDivision === 0)).toBe(true); + expect(b.standings.every((s) => s.team.abDivision === 1)).toBe(true); + }); + + it("re-numbers placements within each A/B division starting from 1", () => { + const tournament = abDivisionsTournament(); + + const result = tournamentStandings(tournament); + + invariant(result.type === "multi"); + const [a, b] = result.standings; + expect(a.standings.map((s) => s.placement)).toEqual([1, 2]); + expect(b.standings.map((s) => s.placement)).toEqual([1, 2]); + }); +}); + +describe("reNumberPlacements", () => { + it("keeps already contiguous placements unchanged", () => { + const result = reNumberPlacements([ + { placement: 1 }, + { placement: 2 }, + { placement: 3 }, + ]); + + expect(result.map((s) => s.placement)).toEqual([1, 2, 3]); + }); + + it("groups tied placements and skips numbers to match team count", () => { + const result = reNumberPlacements([ + { placement: 1 }, + { placement: 1 }, + { placement: 3 }, + { placement: 3 }, + { placement: 5 }, + ]); + + expect(result.map((s) => s.placement)).toEqual([1, 1, 3, 3, 5]); + }); + + it("re-numbers from 1 when the input has been filtered (e.g. top finishers removed)", () => { + const result = reNumberPlacements([ + { placement: 3 }, + { placement: 3 }, + { placement: 5 }, + { placement: 7 }, + ]); + + expect(result.map((s) => s.placement)).toEqual([1, 1, 3, 4]); + }); + + it("adds the offset to every placement", () => { + const result = reNumberPlacements( + [{ placement: 1 }, { placement: 1 }, { placement: 3 }], + 10, + ); + + expect(result.map((s) => s.placement)).toEqual([11, 11, 13]); + }); + + it("preserves non-placement fields on each standing", () => { + const result = reNumberPlacements([ + { placement: 1, team: { id: 7 }, note: "a" }, + { placement: 2, team: { id: 8 }, note: "b" }, + ]); + + expect(result).toEqual([ + { placement: 1, team: { id: 7 }, note: "a" }, + { placement: 2, team: { id: 8 }, note: "b" }, + ]); + }); + + it("returns an empty array when given an empty array", () => { + expect(reNumberPlacements([])).toEqual([]); + expect(reNumberPlacements([], 5)).toEqual([]); + }); +}); + +function singleEliminationTournament() { + const storage = new InMemoryDatabase(); + const manager = new BracketsManager(storage); + + manager.create({ + name: "Main Bracket", + tournamentId: 1, + type: "single_elimination", + seeding: [1, 2, 3, 4], + settings: { seedOrdering: ["natural"] }, + }); + + while (true) { + const pending = storage + .select("match")! + .find( + (m) => + typeof m.opponent1?.id === "number" && + typeof m.opponent2?.id === "number" && + m.opponent1.result !== "win" && + m.opponent2.result !== "win", + ); + if (!pending) break; + + const winnerIsOpp1 = pending.opponent1.id < pending.opponent2.id; + manager.update.match({ + id: pending.id, + opponent1: winnerIsOpp1 ? { score: 2, result: "win" } : { score: 0 }, + opponent2: winnerIsOpp1 ? { score: 0 } : { score: 2, result: "win" }, + }); + } + + return testTournament({ + ctx: { + settings: { + bracketProgression: progressions.singleElimination, + }, + teams: [ + tournamentCtxTeam(1, { seed: 1 }), + tournamentCtxTeam(2, { seed: 2 }), + tournamentCtxTeam(3, { seed: 3 }), + tournamentCtxTeam(4, { seed: 4 }), + ], + }, + data: manager.get.tournamentData(1), + }); +} + +function abDivisionsTournament() { + const storage = new InMemoryDatabase(); + const manager = new BracketsManager(storage); + + manager.create({ + name: "AB RR", + tournamentId: 1, + type: "round_robin", + seeding: [1, 2, 3, 4], + abDivisions: [0, 1, 0, 1], + settings: { + groupCount: 1, + hasAbDivisions: true, + seedOrdering: ["groups.seed_optimized"], + }, + }); + + const winnerByMatchup: Record = { + "1-2": 1, + "1-4": 1, + "2-3": 2, + "3-4": 3, + }; + for (const match of storage.select("match")!) { + const a = match.opponent1.id as number; + const b = match.opponent2.id as number; + const key = a < b ? `${a}-${b}` : `${b}-${a}`; + const winnerId = winnerByMatchup[key]; + invariant(winnerId, `unexpected matchup ${key}`); + const loserScore = key === "2-3" || key === "3-4" ? 1 : 0; + const winnerIsOpp1 = match.opponent1.id === winnerId; + manager.update.match({ + id: match.id, + opponent1: winnerIsOpp1 + ? { score: 2, result: "win" } + : { score: loserScore }, + opponent2: winnerIsOpp1 + ? { score: loserScore } + : { score: 2, result: "win" }, + }); + } + + const data = manager.get.tournamentData(1); + + return testTournament({ + ctx: { + settings: { + bracketProgression: [ + { + type: "round_robin", + name: "AB RR", + requiresCheckIn: false, + settings: { hasAbDivisions: true }, + }, + ], + }, + teams: [ + tournamentCtxTeam(1, { abDivision: 0, seed: 1 }), + tournamentCtxTeam(2, { abDivision: 1, seed: 2 }), + tournamentCtxTeam(3, { abDivision: 0, seed: 3 }), + tournamentCtxTeam(4, { abDivision: 1, seed: 4 }), + ], + }, + data, + }); +} diff --git a/app/features/tournament/core/Standings.ts b/app/features/tournament/core/Standings.ts index a2fa012ce..d6894df4a 100644 --- a/app/features/tournament/core/Standings.ts +++ b/app/features/tournament/core/Standings.ts @@ -24,6 +24,34 @@ export function flattenStandings( : standingsResult.standings.flatMap((div) => div.standings); } +/** + * Re-numbers placements in a sorted standings array so that tied placements stay + * grouped (e.g. `[1, 1, 3, 3, 5]`) while non-tied positions reflect the true + * number of teams above them. Useful after filtering or merging standings where + * the original placement numbers no longer match the team count. + * + * Pass `offset` to shift every placement downwards — used when the returned + * standings will be appended below standings from another bracket. + */ +export function reNumberPlacements( + standings: T[], + offset = 0, +): T[] { + let lastOriginalPlacement = 0; + let currentPlacement = 0; + + return standings.map((standing, index) => { + if (standing.placement !== lastOriginalPlacement) { + lastOriginalPlacement = standing.placement; + currentPlacement = index + 1; + } + return { + ...standing, + placement: currentPlacement + offset, + }; + }); +} + /** Calculates SPR (Seed Performance Rating) - see https://web.archive.org/web/20250513034545/https://www.pgstats.com/articles/introducing-spr-and-uf */ export function calculateSPR({ standings, @@ -143,24 +171,42 @@ export function matchesPlayed({ export function tournamentStandings( tournament: Tournament, ): TournamentStandingsResult { - const startingBracketIdxs = Progression.startingBrackets( - tournament.ctx.settings.bracketProgression, - ); + const progression = tournament.ctx.settings.bracketProgression; + const startingBracketIdxs = Progression.startingBrackets(progression); if (startingBracketIdxs.length <= 1) { + const standings = tournamentStandingsForBracket(tournament, undefined); + + if (Progression.hasAbDivisionsFinals(progression)) { + return { + type: "multi", + standings: [ + { + div: "A", + standings: reNumberPlacements( + standings.filter((s) => s.team.abDivision === 0), + ), + }, + { + div: "B", + standings: reNumberPlacements( + standings.filter((s) => s.team.abDivision === 1), + ), + }, + ], + }; + } + return { type: "single", - standings: tournamentStandingsForBracket(tournament, undefined), + standings, }; } return { type: "multi", standings: startingBracketIdxs.map((bracketIdx) => ({ - div: getBracketProgressionLabel( - bracketIdx, - tournament.ctx.settings.bracketProgression, - ), + div: getBracketProgressionLabel(bracketIdx, progression), standings: tournamentStandingsForBracket(tournament, bracketIdx), })), }; @@ -241,8 +287,6 @@ function standingsToMergeable< standings: T[]; teamsAboveFromAnotherBracketsCount: number; }) { - const result: T[] = []; - const filtered = standings.filter( (standing) => !alreadyIncludedTeamIds.has(standing.team.id), ); @@ -250,22 +294,8 @@ function standingsToMergeable< // e.g. if standings start at 3rd place, this must mean there is 2 teams left to finish _this_ bracket const unfinishedTeamsCount = (standings.at(0)?.placement ?? 1) - 1; - let placement = 1; - - for (const [i, standing] of filtered.entries()) { - const placementChanged = - i !== 0 && standing.placement !== filtered[i - 1].placement; - - if (placementChanged) { - placement = i + 1; - } - - result.push({ - ...standing, - placement: - placement + teamsAboveFromAnotherBracketsCount + unfinishedTeamsCount, - }); - } - - return result; + return reNumberPlacements( + filtered, + teamsAboveFromAnotherBracketsCount + unfinishedTeamsCount, + ); } diff --git a/app/features/tournament/loaders/to.$id.join.server.ts b/app/features/tournament/loaders/to.$id.join.server.ts index e22a0e73c..7a092b567 100644 --- a/app/features/tournament/loaders/to.$id.join.server.ts +++ b/app/features/tournament/loaders/to.$id.join.server.ts @@ -1,12 +1,16 @@ import type { LoaderFunctionArgs } from "react-router"; -import { findByInviteCode } from "../queries/findTeamByInviteCode.server"; +import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server"; -export const loader = ({ request }: LoaderFunctionArgs) => { +export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const inviteCode = url.searchParams.get("code"); + const team = inviteCode + ? await TournamentTeamRepository.findByInviteCode(inviteCode) + : null; + return { - teamId: inviteCode ? findByInviteCode(inviteCode)?.id : null, + teamId: team?.id ?? null, inviteCode, }; }; diff --git a/app/features/tournament/loaders/to.$id.register.server.ts b/app/features/tournament/loaders/to.$id.register.server.ts index 378ce2472..98ca13c1f 100644 --- a/app/features/tournament/loaders/to.$id.register.server.ts +++ b/app/features/tournament/loaders/to.$id.register.server.ts @@ -3,10 +3,10 @@ import { getUser } from "~/features/auth/core/user.server"; import * as SQGroupRepository from "~/features/sendouq/SQGroupRepository.server"; import * as TeamRepository from "~/features/team/TeamRepository.server"; import * as SavedCalendarEventRepository from "~/features/tournament/SavedCalendarEventRepository.server"; +import { tournamentFromDBCached } from "~/features/tournament-bracket/core/Tournament.server"; import { findMapPoolByTeamId } from "~/features/tournament-bracket/queries/findMapPoolByTeamId.server"; import { parseParams } from "~/utils/remix.server"; import { idObject } from "~/utils/zod"; -import { findOwnTournamentTeam } from "../queries/findOwnTournamentTeam.server"; export const loader = async ({ params }: LoaderFunctionArgs) => { const user = getUser(); @@ -17,11 +17,10 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { schema: idObject, }); - const ownTournamentTeam = findOwnTournamentTeam({ - tournamentId, - userId: user.id, - }); - if (!ownTournamentTeam) { + const tournament = await tournamentFromDBCached({ tournamentId, user }); + const ownTeam = tournament.ownedTeamByUser(user); + + if (!ownTeam) { return { mapPool: null, friendPlayers: null, @@ -34,7 +33,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { } return { - mapPool: findMapPoolByTeamId(ownTournamentTeam.id), + mapPool: findMapPoolByTeamId(ownTeam.id), friendPlayers: await SQGroupRepository.friendsAndTeammates(user.id), teams: await TeamRepository.findAllMemberOfByUserId(user.id), isSaved: false, diff --git a/app/features/tournament/queries/changeTeamOwner.server.ts b/app/features/tournament/queries/changeTeamOwner.server.ts deleted file mode 100644 index 062c320f0..000000000 --- a/app/features/tournament/queries/changeTeamOwner.server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { sql } from "~/db/sql"; -import type { Tables } from "~/db/tables"; - -const stm = sql.prepare(/* sql */ ` - update TournamentTeamMember - set "role" = @role - where - "tournamentTeamId" = @tournamentTeamId and - "userId" = @userId -`); - -export const changeTeamOwner = sql.transaction( - (args: { - tournamentTeamId: Tables["TournamentTeam"]["id"]; - oldCaptainId: Tables["User"]["id"]; - newCaptainId: Tables["User"]["id"]; - }) => { - stm.run({ - tournamentTeamId: args.tournamentTeamId, - userId: args.oldCaptainId, - role: "REGULAR", - }); - - stm.run({ - tournamentTeamId: args.tournamentTeamId, - userId: args.newCaptainId, - role: "OWNER", - }); - }, -); diff --git a/app/features/tournament/queries/checkIn.server.ts b/app/features/tournament/queries/checkIn.server.ts deleted file mode 100644 index 24fa1888e..000000000 --- a/app/features/tournament/queries/checkIn.server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { sql } from "~/db/sql"; - -const stm = sql.prepare(/* sql */ ` - insert into "TournamentTeamCheckIn" - ("tournamentTeamId", "checkedInAt") - values - (@tournamentTeamId, strftime('%s', 'now')) -`); - -export function checkIn(tournamentTeamId: number) { - stm.run({ tournamentTeamId }); -} diff --git a/app/features/tournament/queries/checkOut.server.ts b/app/features/tournament/queries/checkOut.server.ts deleted file mode 100644 index c26068e86..000000000 --- a/app/features/tournament/queries/checkOut.server.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { sql } from "~/db/sql"; - -const stm = sql.prepare(/* sql */ ` - delete from "TournamentTeamCheckIn" - where "tournamentTeamId" = @tournamentTeamId -`); - -export function checkOut(tournamentTeamId: number) { - stm.run({ tournamentTeamId }); -} diff --git a/app/features/tournament/queries/deleteTeam.server.ts b/app/features/tournament/queries/deleteTeam.server.ts deleted file mode 100644 index fb2a6adc3..000000000 --- a/app/features/tournament/queries/deleteTeam.server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { sql } from "~/db/sql"; - -const deleteTeamStm = sql.prepare(/*sql*/ ` - delete from "TournamentTeam" - where "id" = @tournamentTeamId -`); - -const deleteMapPoolStm = sql.prepare(/*sql*/ ` - delete from "MapPoolMap" - where "tournamentTeamId" = @tournamentTeamId -`); - -export const deleteTeam = sql.transaction((tournamentTeamId: number) => { - deleteMapPoolStm.run({ tournamentTeamId }); - deleteTeamStm.run({ tournamentTeamId }); -}); diff --git a/app/features/tournament/queries/deleteTeamMember.server.ts b/app/features/tournament/queries/deleteTeamMember.server.ts deleted file mode 100644 index dfca7b6bb..000000000 --- a/app/features/tournament/queries/deleteTeamMember.server.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { sql } from "~/db/sql"; - -const stm = sql.prepare(/*sql*/ ` - delete from "TournamentTeamMember" - where userId = @userId - and tournamentTeamId = @tournamentTeamId -`); - -export default function deleteTeamMember({ - userId, - tournamentTeamId, -}: { - userId: number; - tournamentTeamId: number; -}) { - stm.run({ userId, tournamentTeamId }); -} diff --git a/app/features/tournament/queries/findOwnTournamentTeam.server.ts b/app/features/tournament/queries/findOwnTournamentTeam.server.ts deleted file mode 100644 index 5c04499a4..000000000 --- a/app/features/tournament/queries/findOwnTournamentTeam.server.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { sql } from "~/db/sql"; -import type { Tables } from "~/db/tables"; - -const stm = sql.prepare(/*sql*/ ` - select - "TournamentTeam"."id", - "TournamentTeam"."name", - "TournamentTeamCheckIn"."checkedInAt", - "TournamentTeam"."inviteCode" - from - "TournamentTeam" - left join "TournamentTeamCheckIn" on - "TournamentTeamCheckIn"."tournamentTeamId" = "TournamentTeam"."id" - left join "TournamentTeamMember" on - "TournamentTeamMember"."tournamentTeamId" = "TournamentTeam"."id" - and "TournamentTeamMember"."role" = 'OWNER' - where - "TournamentTeam"."tournamentId" = @tournamentId - and "TournamentTeam"."isPlaceholder" = 0 - and "TournamentTeamMember"."userId" = @userId -`); - -type FindOwnTeam = - | (Pick & - Pick) - | null; - -export function findOwnTournamentTeam({ - tournamentId, - userId, -}: { - tournamentId: number; - userId: number; -}) { - return stm.get({ - tournamentId, - userId, - }) as FindOwnTeam; -} diff --git a/app/features/tournament/queries/findTeamByInviteCode.server.ts b/app/features/tournament/queries/findTeamByInviteCode.server.ts deleted file mode 100644 index 8dcd6fa48..000000000 --- a/app/features/tournament/queries/findTeamByInviteCode.server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { sql } from "~/db/sql"; - -const stm = sql.prepare(/*sql */ ` - select - "TournamentTeam"."id", - "TournamentTeam"."tournamentId" - from "TournamentTeam" - where "TournamentTeam"."inviteCode" = @inviteCode -`); - -export function findByInviteCode(inviteCode: string) { - return stm.get({ inviteCode }) as { - id: number; - tournamentId: number; - } | null; -} diff --git a/app/features/tournament/queries/hasTournamentFinalized.server.ts b/app/features/tournament/queries/hasTournamentFinalized.server.ts deleted file mode 100644 index af5a232aa..000000000 --- a/app/features/tournament/queries/hasTournamentFinalized.server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { sql } from "~/db/sql"; -import type { Tables } from "~/db/tables"; - -const stm = sql.prepare(/*sql*/ ` - select 1 - from "TournamentResult" - where "TournamentResult"."tournamentId" = @tournamentId -`); - -export default function hasTournamentFinalized( - tournamentId: Tables["Tournament"]["id"], -) { - return Boolean(stm.get({ tournamentId })); -} diff --git a/app/features/tournament/queries/hasTournamentStarted.server.ts b/app/features/tournament/queries/hasTournamentStarted.server.ts deleted file mode 100644 index a1047d0a3..000000000 --- a/app/features/tournament/queries/hasTournamentStarted.server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { sql } from "~/db/sql"; -import type { Tables } from "~/db/tables"; - -const stm = sql.prepare(/*sql*/ ` - select 1 - from "TournamentStage" - where "TournamentStage"."tournamentId" = @tournamentId -`); - -export default function hasTournamentStarted( - tournamentId: Tables["Tournament"]["id"], -) { - return Boolean(stm.get({ tournamentId })); -} diff --git a/app/features/tournament/queries/joinLeaveTeam.server.ts b/app/features/tournament/queries/joinLeaveTeam.server.ts deleted file mode 100644 index 8c81e01f2..000000000 --- a/app/features/tournament/queries/joinLeaveTeam.server.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { sql } from "~/db/sql"; -import invariant from "~/utils/invariant"; -import { checkOut } from "./checkOut.server"; - -const createTeamMemberStm = sql.prepare(/*sql*/ ` - insert into "TournamentTeamMember" ( - "tournamentTeamId", - "inGameName", - "userId" - ) values ( - @tournamentTeamId, - @inGameName, - @userId - ) -`); - -const deleteTeamStm = sql.prepare(/*sql*/ ` - delete from "TournamentTeam" - where "id" = @tournamentTeamId -`); - -const deleteMemberStm = sql.prepare(/*sql*/ ` - delete from "TournamentTeamMember" - where "tournamentTeamId" = @tournamentTeamId - and "userId" = @userId -`); - -export const joinTeam = sql.transaction( - ({ - previousTeamId, - whatToDoWithPreviousTeam, - newTeamId, - userId, - inGameName, - tournamentId: _tournamentId, - checkOutTeam = false, - }: { - previousTeamId?: number; - whatToDoWithPreviousTeam?: "LEAVE" | "DELETE"; - newTeamId: number; - userId: number; - inGameName: string | null; - tournamentId: number; - checkOutTeam?: boolean; - }) => { - if (whatToDoWithPreviousTeam === "DELETE") { - deleteTeamStm.run({ tournamentTeamId: previousTeamId ?? null }); - } else if (whatToDoWithPreviousTeam === "LEAVE") { - deleteMemberStm.run({ tournamentTeamId: previousTeamId ?? null, userId }); - } - - if (checkOutTeam) { - invariant( - previousTeamId, - "previousTeamId is required when checking out team", - ); - checkOut(previousTeamId); - } - - createTeamMemberStm.run({ - tournamentTeamId: newTeamId, - userId, - inGameName, - }); - }, -); - -export const leaveTeam = ({ - teamId, - userId, -}: { - teamId: number; - userId: number; -}) => { - deleteMemberStm.run({ tournamentTeamId: teamId, userId }); -}; diff --git a/app/features/tournament/queries/upsertCounterpickMaps.server.ts b/app/features/tournament/queries/upsertCounterpickMaps.server.ts deleted file mode 100644 index c123c4ec2..000000000 --- a/app/features/tournament/queries/upsertCounterpickMaps.server.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { sql } from "~/db/sql"; -import type { Tables } from "~/db/tables"; -import type { MapPool } from "~/features/map-list-generator/core/map-pool"; - -const deleteCounterpickMapsByTeamIdStm = sql.prepare(/* sql */ ` - delete from - "MapPoolMap" - where - "tournamentTeamId" = @tournamentTeamId -`); - -const addCounterpickMapStm = sql.prepare(/* sql */ ` - insert into - "MapPoolMap" ("tournamentTeamId", "stageId", "mode") - values - (@tournamentTeamId, @stageId, @mode) -`); - -export const upsertCounterpickMaps = sql.transaction( - ({ - tournamentTeamId, - mapPool, - }: { - tournamentTeamId: Tables["TournamentTeam"]["id"]; - mapPool: MapPool; - }) => { - deleteCounterpickMapsByTeamIdStm.run({ tournamentTeamId }); - - for (const { stageId, mode } of mapPool.stageModePairs) { - addCounterpickMapStm.run({ - tournamentTeamId, - stageId, - mode, - }); - } - }, -); diff --git a/app/features/tournament/routes/to.$id.index.ts b/app/features/tournament/routes/to.$id.index.ts index 7ee47f7f1..4c5d2c1c0 100644 --- a/app/features/tournament/routes/to.$id.index.ts +++ b/app/features/tournament/routes/to.$id.index.ts @@ -1,4 +1,5 @@ import { type LoaderFunctionArgs, redirect } from "react-router"; +import { tournamentFromDBCached } from "~/features/tournament-bracket/core/Tournament.server"; import { parseParams } from "~/utils/remix.server"; import { tournamentBracketsPage, @@ -6,20 +7,23 @@ import { tournamentResultsPage, } from "~/utils/urls"; import { idObject } from "~/utils/zod"; -import hasTournamentFinalized from "../queries/hasTournamentFinalized.server"; -import hasTournamentStarted from "../queries/hasTournamentStarted.server"; -export const loader = ({ params }: LoaderFunctionArgs) => { +export const loader = async ({ params }: LoaderFunctionArgs) => { const { id: tournamentId } = parseParams({ params, schema: idObject, }); - if (!hasTournamentStarted(tournamentId)) { + const tournament = await tournamentFromDBCached({ + tournamentId, + user: undefined, + }); + + if (!tournament.hasStarted) { return redirect(tournamentRegisterPage(tournamentId)); } - if (!hasTournamentFinalized(tournamentId)) { + if (!tournament.ctx.isFinalized) { return redirect(tournamentBracketsPage({ tournamentId })); } diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx index 2e58cc7f5..11d44cbb9 100644 --- a/app/features/tournament/routes/to.$id.register.tsx +++ b/app/features/tournament/routes/to.$id.register.tsx @@ -1381,15 +1381,12 @@ function TOPickedMapPoolInfo() {
- - - {t("calendar:createMapList")} - +
+ + + {t("calendar:createMapList")} + +
); diff --git a/app/features/tournament/routes/to.$id.seeds.tsx b/app/features/tournament/routes/to.$id.seeds.tsx index df87d5f6c..b74416eca 100644 --- a/app/features/tournament/routes/to.$id.seeds.tsx +++ b/app/features/tournament/routes/to.$id.seeds.tsx @@ -23,23 +23,36 @@ import { Alert } from "~/components/Alert"; import { Avatar } from "~/components/Avatar"; import { Catcher } from "~/components/Catcher"; import { SendouButton } from "~/components/elements/Button"; +import { + SendouChipRadio, + SendouChipRadioGroup, +} from "~/components/elements/ChipRadio"; import { SendouDialog } from "~/components/elements/Dialog"; import { Image } from "~/components/Image"; import { InfoPopover } from "~/components/InfoPopover"; import { SubmitButton } from "~/components/SubmitButton"; import { Table } from "~/components/Table"; import type { SeedingSnapshot } from "~/db/tables"; +import * as AbDivisions from "~/features/tournament-bracket/core/AbDivisions"; +import type { Tournament } from "~/features/tournament-bracket/core/Tournament"; import type { TournamentDataTeam } from "~/features/tournament-bracket/core/Tournament.server"; import invariant from "~/utils/invariant"; import { navIconUrl, userResultsPage } from "~/utils/urls"; import { ordinalToRoundedSp } from "../../mmr/mmr-utils"; import { action } from "../actions/to.$id.seeds.server"; import { loader } from "../loaders/to.$id.seeds.server"; +import { TOURNAMENT } from "../tournament-constants"; import { useTournament } from "./to.$id"; import styles from "./to.$id.seeds.module.css"; export { action, loader }; +const AB_DIVISION_RADIO_OPTIONS = [ + { value: "unassigned", label: "Unassigned" }, + { value: "0", label: "A" }, + { value: "1", label: "B" }, +] as const; + export default function TournamentSeedsPage() { const tournament = useTournament(); const navigation = useNavigation(); @@ -161,6 +174,16 @@ export default function TournamentSeedsPage() { .join()} /> ) : null} + {hasAbDivisionsStartingBracket(tournament) ? ( + <> + team.abDivision ?? -1) + .join()} + /> + + + ) : null}
  • @@ -431,6 +454,183 @@ function StartingBracketDialog() { ); } +function hasAbDivisionsStartingBracket(tournament: Tournament) { + return tournament.ctx.settings.bracketProgression.some( + (bracket) => !bracket.sources && bracket.settings?.hasAbDivisions, + ); +} + +function AbDivisionImbalanceWarning() { + const tournament = useTournament(); + + const warnings = tournament.ctx.settings.bracketProgression + .map((bracket, bracketIdx) => { + if (bracket.sources || !bracket.settings?.hasAbDivisions) return null; + + const bracketTeams = tournament.isMultiStartingBracket + ? tournament.ctx.teams.filter( + (team) => (team.startingBracketIdx ?? 0) === bracketIdx, + ) + : tournament.ctx.teams; + const checkedInTeams = bracketTeams.filter( + (team) => team.checkIns.length > 0, + ); + + const { a, b } = AbDivisions.countByDivision(checkedInTeams); + const diff = Math.abs(a - b); + + const teamsPerGroup = + bracket.settings.teamsPerGroup ?? + TOURNAMENT.RR_DEFAULT_TEAM_COUNT_PER_GROUP; + const groupCount = Math.max( + 1, + Math.ceil(checkedInTeams.length / teamsPerGroup), + ); + + const tooImbalanced = diff > 1; + const unevenWithMultipleGroups = diff === 1 && groupCount > 1; + if (!tooImbalanced && !unevenWithMultipleGroups) return null; + + const prefix = tournament.isMultiStartingBracket + ? `${bracket.name}: ` + : ""; + const reason = tooImbalanced + ? "counts can differ by at most 1 to start bracket" + : "uneven A/B is only allowed with a single group"; + + return `${prefix}${a} checked-in A teams, ${b} checked-in B teams — ${reason}.`; + }) + .filter((warning): warning is string => warning !== null); + + if (warnings.length === 0) return null; + + return ( + +
    + {warnings.map((warning) => ( +
    {warning}
    + ))} +
    +
    + ); +} + +type AbDivisionValue = 0 | 1 | null; + +function AbDivisionsDialog() { + const fetcher = useFetcher(); + const tournament = useTournament(); + + const [isOpen, setIsOpen] = React.useState(false); + const [teamAbDivisions, setTeamAbDivisions] = React.useState< + { tournamentTeamId: number; abDivision: AbDivisionValue }[] + >( + tournament.ctx.teams.map((team) => ({ + tournamentTeamId: team.id, + abDivision: + team.abDivision === 0 || team.abDivision === 1 ? team.abDivision : null, + })), + ); + + const counts = AbDivisions.countByDivision(teamAbDivisions); + + return ( +
    + setIsOpen(true)} + data-testid="set-ab-divisions" + > + Set A/B divisions + + setIsOpen(false)} + isFullScreen + > + +
    + A: {counts.a} + B: {counts.b} + Unassigned: {counts.unassigned} +
    + + + + + + + + + + + + {tournament.ctx.teams.map((team) => { + const { abDivision } = teamAbDivisions.find( + ({ tournamentTeamId }) => tournamentTeamId === team.id, + )!; + + return ( + + + + + ); + })} + +
    TeamDivision
    {team.name} + + {AB_DIVISION_RADIO_OPTIONS.map(({ value, label }) => ( + { + const newDivision: AbDivisionValue = + rawValue === "unassigned" + ? null + : (Number(rawValue) as 0 | 1); + setTeamAbDivisions((teamAbDivisions) => + teamAbDivisions.map((t) => + t.tournamentTeamId === team.id + ? { ...t, abDivision: newDivision } + : t, + ), + ); + }} + > + {label} + + ))} + +
    + + Save + +
    +
    +
    + ); +} + function SeedAlert({ teamOrder }: { teamOrder: number[] }) { const tournament = useTournament(); const fetcher = useFetcher(); diff --git a/app/features/tournament/tournament-constants.ts b/app/features/tournament/tournament-constants.ts index cad0b1ab0..00169d2ec 100644 --- a/app/features/tournament/tournament-constants.ts +++ b/app/features/tournament/tournament-constants.ts @@ -13,6 +13,8 @@ export const TOURNAMENT = { BRACKET_NAME_MAX_LENGTH: 32, // just a fallback, normally this should be set by user explicitly RR_DEFAULT_TEAM_COUNT_PER_GROUP: 4, + RR_TEAMS_PER_GROUP_OPTIONS: [3, 4, 5, 6], + RR_AB_DIVISIONS_TEAMS_PER_GROUP_OPTIONS: [4, 6, 8, 10, 12], SWISS_DEFAULT_GROUP_COUNT: 1, SWISS_DEFAULT_ROUND_COUNT: 5, SE_DEFAULT_HAS_THIRD_PLACE_MATCH: true, diff --git a/app/features/tournament/tournament-schemas.server.ts b/app/features/tournament/tournament-schemas.server.ts index 09c4b4f71..92abdff25 100644 --- a/app/features/tournament/tournament-schemas.server.ts +++ b/app/features/tournament/tournament-schemas.server.ts @@ -77,6 +77,18 @@ export const seedsActionSchema = z.union([ ), ), }), + z.object({ + _action: _action("UPDATE_AB_DIVISIONS"), + abDivisions: z.preprocess( + safeJSONParse, + z.array( + z.object({ + tournamentTeamId: id, + abDivision: z.union([z.literal(0), z.literal(1), z.null()]), + }), + ), + ), + }), ]); export const tournamentSearchSearchParamsSchema = z.object({ diff --git a/app/features/tournament/tournament-test-utils.ts b/app/features/tournament/tournament-test-utils.ts index d35a12119..8a903b025 100644 --- a/app/features/tournament/tournament-test-utils.ts +++ b/app/features/tournament/tournament-test-utils.ts @@ -3,9 +3,7 @@ import { databaseTimestampNow } from "~/utils/dates"; import invariant from "~/utils/invariant"; import { getServerTournamentManager } from "../tournament-bracket/core/brackets-manager/manager.server"; import { tournamentFromDB } from "../tournament-bracket/core/Tournament.server"; -import { joinTeam } from "./queries/joinLeaveTeam.server"; import { updateRoundMaps } from "./queries/updateRoundMaps.server"; -import * as TournamentRepository from "./TournamentRepository.server"; import * as TournamentTeamRepository from "./TournamentTeamRepository.server"; /** @@ -58,7 +56,6 @@ export async function dbInsertTournamentTeam({ tournamentId?: number; }) { const tournamentTeam = await TournamentTeamRepository.create({ - ownerInGameName: null, team: { name: `Test Team ${ownerId}`, prefersNotToHost: 0, @@ -71,19 +68,13 @@ export async function dbInsertTournamentTeam({ for (let i = 1; i < membersCount; i++) { const memberId = ownerId + i; - joinTeam({ + await TournamentTeamRepository.join({ userId: memberId, newTeamId: tournamentTeam.id, - tournamentId, - inGameName: null, }); } - await TournamentRepository.checkIn({ - tournamentTeamId: tournamentTeam.id, - // no sources = regular check in - bracketIdx: null, - }); + await TournamentTeamRepository.checkIn(tournamentTeam.id); } /** diff --git a/app/features/tournament/tournament-utils.server.ts b/app/features/tournament/tournament-utils.server.ts index 5ee29b5f8..349942302 100644 --- a/app/features/tournament/tournament-utils.server.ts +++ b/app/features/tournament/tournament-utils.server.ts @@ -1,26 +1,11 @@ +import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepository.server"; import type { getServerTournamentManager } from "~/features/tournament-bracket/core/brackets-manager/manager.server"; import * as TournamentOrganizationRepository from "~/features/tournament-organization/TournamentOrganizationRepository.server"; -import * as UserRepository from "~/features/user-page/UserRepository.server"; import { logger } from "~/utils/logger"; import { errorToast, errorToastIfFalsy } from "~/utils/remix.server"; +import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "../leaderboards/leaderboards-constants"; import type { Tournament } from "../tournament-bracket/core/Tournament"; -export const inGameNameIfNeeded = async ({ - tournament, - userId, -}: { - tournament: Tournament; - userId: number; -}) => { - if (!tournament.ctx.settings.requireInGameNames) return null; - - const inGameName = await UserRepository.inGameNameByUserId(userId); - - errorToastIfFalsy(inGameName, "No in-game name"); - - return inGameName; -}; - export async function requireNotBannedByOrganization({ tournament, user, @@ -43,6 +28,23 @@ export async function requireNotBannedByOrganization({ } } +export async function requireSendouQParticipationIfNeeded({ + tournament, + userId, +}: { + tournament: Tournament; + userId: number; +}) { + if (!tournament.ctx.settings.requireSendouQParticipation) return; + + const hasEnough = await LeaderboardRepository.userHasEnoughSqMatches(userId); + + errorToastIfFalsy( + hasEnough, + `Must have played ${MATCHES_COUNT_NEEDED_FOR_LEADERBOARD} SendouQ matches this season to join`, + ); +} + /** * Ends all unfinished matches involving dropped teams by awarding wins to their opponents. * If both teams in a match have dropped, a random winner is selected. @@ -63,6 +65,8 @@ export function endDroppedTeamMatches({ }) { const stageData = manager.get.tournamentData(tournament.ctx.id); + const endedMatchIds: number[] = []; + for (const match of stageData.match) { if (!match.opponent1?.id || !match.opponent2?.id) continue; if (match.opponent1.result === "win" || match.opponent2.result === "win") @@ -100,5 +104,9 @@ export function endDroppedTeamMatches({ }, true, ); + + endedMatchIds.push(match.id); } + + return endedMatchIds; } diff --git a/app/features/tournament/tournament-utils.test.ts b/app/features/tournament/tournament-utils.test.ts index c81afec69..90ea09166 100644 --- a/app/features/tournament/tournament-utils.test.ts +++ b/app/features/tournament/tournament-utils.test.ts @@ -23,7 +23,10 @@ const createTeam = ( id, seed: options.seed ?? null, members: { length: options.members ?? 4 }, - avgSeedingSkillOrdinal: options.avgSeedingSkillOrdinal ?? 100, + avgSeedingSkillOrdinal: + options.avgSeedingSkillOrdinal === undefined + ? 100 + : options.avgSeedingSkillOrdinal, createdAt: options.createdAt ?? id, startingBracketIdx: options.startingBracketIdx ?? null, }); @@ -146,7 +149,7 @@ describe("compareTeamsForOrdering", () => { it("places team with skill before team without skill", () => { const withSkill = createTeam(1, { avgSeedingSkillOrdinal: 100 }); - const withoutSkill = createTeam(2); + const withoutSkill = createTeam(2, { avgSeedingSkillOrdinal: null }); const result = compareTeamsForOrdering( withSkill, @@ -156,6 +159,21 @@ describe("compareTeamsForOrdering", () => { expect(result).toBeLessThan(0); }); + + it("places rated team before unrated team even when unrated was created earlier", () => { + const rated = createTeam(1, { + avgSeedingSkillOrdinal: 100, + createdAt: 200, + }); + const unrated = createTeam(2, { + avgSeedingSkillOrdinal: null, + createdAt: 100, + }); + + const sorted = sortTeamsBySeeding([unrated, rated], MIN_MEMBERS); + + expect(sorted.map((t) => t.id)).toEqual([1, 2]); + }); }); describe("createdAt tiebreaker", () => { diff --git a/app/features/tournament/tournament-utils.ts b/app/features/tournament/tournament-utils.ts index c9017b864..f6dc8ebb3 100644 --- a/app/features/tournament/tournament-utils.ts +++ b/app/features/tournament/tournament-utils.ts @@ -312,10 +312,16 @@ export function compareTeamsForOrdering( return 1; } + if (a.avgSeedingSkillOrdinal !== null && b.avgSeedingSkillOrdinal === null) { + return -1; + } + if (a.avgSeedingSkillOrdinal === null && b.avgSeedingSkillOrdinal !== null) { + return 1; + } if ( - a.avgSeedingSkillOrdinal !== b.avgSeedingSkillOrdinal && a.avgSeedingSkillOrdinal !== null && - b.avgSeedingSkillOrdinal !== null + b.avgSeedingSkillOrdinal !== null && + a.avgSeedingSkillOrdinal !== b.avgSeedingSkillOrdinal ) { return b.avgSeedingSkillOrdinal - a.avgSeedingSkillOrdinal; } diff --git a/app/features/tournament/tournament.module.css b/app/features/tournament/tournament.module.css index fcf6b6943..646d0e360 100644 --- a/app/features/tournament/tournament.module.css +++ b/app/features/tournament/tournament.module.css @@ -33,6 +33,7 @@ display: grid; gap: var(--s-1-5); grid-template-columns: max-content minmax(0, max-content); + align-items: center; } .teamWithRosterMemberInactive { @@ -91,6 +92,7 @@ text-overflow: ellipsis; white-space: nowrap; display: block; + padding-block: 1px; } .teamMemberNameRole { diff --git a/app/features/user-page/UserRepository.server.ts b/app/features/user-page/UserRepository.server.ts index 9675050e0..60b8284ab 100644 --- a/app/features/user-page/UserRepository.server.ts +++ b/app/features/user-page/UserRepository.server.ts @@ -544,13 +544,27 @@ const baseTournamentResultsQuery = (userId: number) => .innerJoin("Tournament", "Tournament.id", "TournamentResult.tournamentId") .where("TournamentResult.userId", "=", userId); +const escapeLikePattern = (value: string) => + value.replace(/[\\%_]/g, (char) => `\\${char}`); + +const tournamentNameLikeExpr = (tournamentName: string) => { + const pattern = `%${escapeLikePattern(tournamentName)}%`; + return sql`${sql.ref("CalendarEvent.name")} like ${pattern} escape '\\'`; +}; + export function findResultsByUserId( userId: number, { showHighlightsOnly = false, limit, offset, - }: { showHighlightsOnly?: boolean; limit?: number; offset?: number } = {}, + tournamentName, + }: { + showHighlightsOnly?: boolean; + limit?: number; + offset?: number; + tournamentName?: string; + } = {}, ) { let calendarEventResultsQuery = baseCalendarEventResultsQuery(userId).select( ({ eb, fn }) => [ @@ -634,6 +648,15 @@ export function findResultsByUserId( ); } + if (tournamentName) { + calendarEventResultsQuery = calendarEventResultsQuery.where( + tournamentNameLikeExpr(tournamentName), + ); + tournamentResultsQuery = tournamentResultsQuery.where( + tournamentNameLikeExpr(tournamentName), + ); + } + let query = calendarEventResultsQuery .unionAll(tournamentResultsQuery) .orderBy("startTime", "desc") @@ -652,7 +675,10 @@ export function findResultsByUserId( export async function countResultsByUserId( userId: number, - { showHighlightsOnly = false }: { showHighlightsOnly?: boolean } = {}, + { + showHighlightsOnly = false, + tournamentName, + }: { showHighlightsOnly?: boolean; tournamentName?: string } = {}, ) { let calendarEventResultsQuery = baseCalendarEventResultsQuery(userId).select( ({ fn }) => [fn.countAll().as("count")], @@ -675,6 +701,15 @@ export async function countResultsByUserId( ); } + if (tournamentName) { + calendarEventResultsQuery = calendarEventResultsQuery.where( + tournamentNameLikeExpr(tournamentName), + ); + tournamentResultsQuery = tournamentResultsQuery.where( + tournamentNameLikeExpr(tournamentName), + ); + } + const [calendarEventResults, tournamentResults] = await Promise.all([ calendarEventResultsQuery.executeTakeFirst(), tournamentResultsQuery.executeTakeFirst(), diff --git a/app/features/user-page/actions/u.$identifier.builds.server.ts b/app/features/user-page/actions/u.$identifier.builds.server.ts index 37729b879..54da58a64 100644 --- a/app/features/user-page/actions/u.$identifier.builds.server.ts +++ b/app/features/user-page/actions/u.$identifier.builds.server.ts @@ -26,15 +26,9 @@ export const action: ActionFunction = async ({ request }) => { switch (data._action) { case "DELETE_BUILD": { - const usersBuilds = await BuildRepository.allByUserId(user.id, { - showPrivate: true, - }); + const ownerId = await BuildRepository.ownerIdById(data.buildToDeleteId); - const buildToDelete = usersBuilds.find( - (build) => build.id === data.buildToDeleteId, - ); - - errorToastIfFalsy(buildToDelete, "Build to delete not found"); + errorToastIfFalsy(ownerId === user.id, "Build to delete not found"); await BuildRepository.deleteById(data.buildToDeleteId); diff --git a/app/features/user-page/core/build-sorting.server.test.ts b/app/features/user-page/core/build-sorting.server.test.ts index 12156fae6..83e8ad44e 100644 --- a/app/features/user-page/core/build-sorting.server.test.ts +++ b/app/features/user-page/core/build-sorting.server.test.ts @@ -329,6 +329,41 @@ describe("sortBuilds()", () => { expect(sortedBuilds[2].id).toBe(1); }); + + it(`sorts ${identifier} with null gear last`, () => { + const key = ( + { + HEADGEAR_ID: "headGearSplId", + CLOTHES_ID: "clothesGearSplId", + SHOES_ID: "shoesGearSplId", + } as const + )[identifier]!; + + const builds = [ + mockBuild({ + id: 1, + [key]: null, + }), + mockBuild({ + id: 2, + [key]: 5, + }), + mockBuild({ + id: 3, + [key]: 1, + }), + ]; + + const sortedBuilds = sortBuilds({ + builds, + buildSorting: [identifier as any], + weaponPool: [], + }); + + expect(sortedBuilds[0].id).toBe(3); + expect(sortedBuilds[1].id).toBe(2); + expect(sortedBuilds[2].id).toBe(1); + }); } it("sorts when buildSort not given", () => { diff --git a/app/features/user-page/core/build-sorting.server.ts b/app/features/user-page/core/build-sorting.server.ts index b2ee5b8d6..b47880c4a 100644 --- a/app/features/user-page/core/build-sorting.server.ts +++ b/app/features/user-page/core/build-sorting.server.ts @@ -28,9 +28,12 @@ export function sortBuilds({ Math.min(...a.weapons.map((wpn) => wpn.weaponSplId)) - Math.min(...b.weapons.map((wpn) => wpn.weaponSplId)), UPDATED_AT: (a, b) => b.updatedAt - a.updatedAt, - HEADGEAR_ID: (a, b) => a.headGearSplId - b.headGearSplId, - CLOTHES_ID: (a, b) => a.clothesGearSplId - b.clothesGearSplId, - SHOES_ID: (a, b) => a.shoesGearSplId - b.shoesGearSplId, + HEADGEAR_ID: (a, b) => + compareNullableNumbers(a.headGearSplId, b.headGearSplId), + CLOTHES_ID: (a, b) => + compareNullableNumbers(a.clothesGearSplId, b.clothesGearSplId), + SHOES_ID: (a, b) => + compareNullableNumbers(a.shoesGearSplId, b.shoesGearSplId), MODE: (a, b) => { const aLowestModeIdx = modesShort.findIndex((mode) => a.modes?.includes(mode), @@ -104,3 +107,10 @@ export function sortBuilds({ return 0; }); } + +function compareNullableNumbers(a: number | null, b: number | null) { + if (a === null && b === null) return 0; + if (a === null) return 1; + if (b === null) return -1; + return a - b; +} diff --git a/app/features/user-page/loaders/u.$identifier.builds.new.server.ts b/app/features/user-page/loaders/u.$identifier.builds.new.server.ts index fc7170648..30edf1315 100644 --- a/app/features/user-page/loaders/u.$identifier.builds.new.server.ts +++ b/app/features/user-page/loaders/u.$identifier.builds.new.server.ts @@ -68,13 +68,9 @@ function resolveDefaultValues( return { buildToEditId: buildToEdit?.id, weapons, - head: buildToEdit?.headGearSplId === -1 ? null : buildToEdit?.headGearSplId, - clothes: - buildToEdit?.clothesGearSplId === -1 - ? null - : buildToEdit?.clothesGearSplId, - shoes: - buildToEdit?.shoesGearSplId === -1 ? null : buildToEdit?.shoesGearSplId, + head: buildToEdit?.headGearSplId, + clothes: buildToEdit?.clothesGearSplId, + shoes: buildToEdit?.shoesGearSplId, abilities, title: buildToEdit?.title, description: buildToEdit?.description ?? null, diff --git a/app/features/user-page/loaders/u.$identifier.builds.server.ts b/app/features/user-page/loaders/u.$identifier.builds.server.ts index 77c51464a..4c4ea0c97 100644 --- a/app/features/user-page/loaders/u.$identifier.builds.server.ts +++ b/app/features/user-page/loaders/u.$identifier.builds.server.ts @@ -1,8 +1,8 @@ import type { LoaderFunctionArgs } from "react-router"; +import * as R from "remeda"; import { getUser } from "~/features/auth/core/user.server"; import * as BuildRepository from "~/features/builds/BuildRepository.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; -import type { MainWeaponId } from "~/modules/in-game-lists/types"; import type { SerializeFrom } from "~/utils/remix"; import { notFoundIfFalsy, privatelyCachedJson } from "~/utils/remix.server"; import { sortBuilds } from "../core/build-sorting.server"; @@ -37,19 +37,9 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { return privatelyCachedJson({ buildSorting: user.buildSorting, builds: sortedBuilds, - weaponCounts: calculateWeaponCounts(), + weaponCounts: R.countBy( + builds.flatMap((build) => build.weapons), + (weapon) => weapon.weaponSplId, + ), }); - - function calculateWeaponCounts() { - return builds.reduce( - (acc, build) => { - for (const weapon of build.weapons) { - acc[weapon.weaponSplId] = (acc[weapon.weaponSplId] ?? 0) + 1; - } - - return acc; - }, - {} as Record, - ); - } }; diff --git a/app/features/user-page/loaders/u.$identifier.results.server.ts b/app/features/user-page/loaders/u.$identifier.results.server.ts index 7d15bf425..5be3ea471 100644 --- a/app/features/user-page/loaders/u.$identifier.results.server.ts +++ b/app/features/user-page/loaders/u.$identifier.results.server.ts @@ -1,9 +1,13 @@ import type { LoaderFunctionArgs } from "react-router"; import { redirect } from "react-router"; +import { getUser } from "~/features/auth/core/user.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; import type { SerializeFrom } from "~/utils/remix"; import { notFoundIfFalsy, parseSafeSearchParams } from "~/utils/remix.server"; -import { RESULTS_PER_PAGE } from "../user-page-constants"; +import { + HIGHLIGHTS_RESULTS_MAX, + RESULTS_PER_PAGE, +} from "../user-page-constants"; import { userResultsPageSearchParamsSchema } from "../user-page-schemas"; export type UserResultsLoaderData = SerializeFrom; @@ -34,15 +38,23 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => { } const page = parsedSearchParams.success ? parsedSearchParams.data.page : 1; + const tournamentName = + !isChoosingHighlights && getUser() && parsedSearchParams.success + ? parsedSearchParams.data.tournament + : undefined; const [results, totalCount] = await Promise.all([ UserRepository.findResultsByUserId(userId, { showHighlightsOnly, + tournamentName, ...(isChoosingHighlights - ? {} + ? { limit: HIGHLIGHTS_RESULTS_MAX } : { limit: RESULTS_PER_PAGE, offset: (page - 1) * RESULTS_PER_PAGE }), }), - UserRepository.countResultsByUserId(userId, { showHighlightsOnly }), + UserRepository.countResultsByUserId(userId, { + showHighlightsOnly, + tournamentName, + }), ]); const maxPage = Math.ceil(totalCount / RESULTS_PER_PAGE); @@ -72,6 +84,6 @@ function redirectIfPageOutOfBounds({ const url = new URL(request.url); const searchParams = new URLSearchParams(url.searchParams); - searchParams.set("page", String(maxPage)); + searchParams.set("page", String(Math.max(maxPage, 1))); throw redirect(`${url.pathname}?${searchParams.toString()}`); } diff --git a/app/features/user-page/routes/u.$identifier.results.tsx b/app/features/user-page/routes/u.$identifier.results.tsx index e6be55a7e..fe0baafcf 100644 --- a/app/features/user-page/routes/u.$identifier.results.tsx +++ b/app/features/user-page/routes/u.$identifier.results.tsx @@ -1,6 +1,10 @@ +import { Search } from "lucide-react"; +import * as React from "react"; import { useTranslation } from "react-i18next"; import { useLoaderData, useMatches, useSearchParams } from "react-router"; +import { useDebounce } from "react-use"; import { LinkButton } from "~/components/elements/Button"; +import { Input } from "~/components/Input"; import { Pagination } from "~/components/Pagination"; import { useUser } from "~/features/auth/core/user"; import { UserResultsTable } from "~/features/user-page/components/UserResultsTable"; @@ -10,6 +14,7 @@ import { SendouButton } from "../../../components/elements/Button"; import { SubPageHeader } from "../components/SubPageHeader"; import { loader } from "../loaders/u.$identifier.results.server"; import type { UserPageLoaderData } from "../loaders/u.$identifier.server"; +import styles from "../user-page.module.css"; export { loader }; @@ -25,6 +30,34 @@ export default function UserResultsPage() { const [searchParams, setSearchParams] = useSearchParams(); const showAll = searchParams.get("all") === "true"; + const urlTournamentQuery = searchParams.get("tournament") ?? ""; + const [tournamentQuery, setTournamentQuery] = + React.useState(urlTournamentQuery); + const [prevUrlTournamentQuery, setPrevUrlTournamentQuery] = + React.useState(urlTournamentQuery); + + if (urlTournamentQuery !== prevUrlTournamentQuery) { + setPrevUrlTournamentQuery(urlTournamentQuery); + setTournamentQuery(urlTournamentQuery); + } + + useDebounce( + () => { + if (urlTournamentQuery === tournamentQuery) return; + setSearchParams((params) => { + if (tournamentQuery) { + params.set("tournament", tournamentQuery); + } else { + params.delete("tournament"); + } + params.delete("page"); + return params; + }); + }, + 300, + [tournamentQuery], + ); + const setPage = (page: number) => { setSearchParams((params) => { params.set("page", String(page)); @@ -44,15 +77,23 @@ export default function UserResultsPage() { ? t("results.title") : t("results.highlights")} - {user?.id === layoutData.user.id ? ( - - {t("results.highlights.choose")} - - ) : null} +
    + {user ? ( + setTournamentQuery(e.target.value)} + placeholder={t("results.filter.placeholder")} + aria-label={t("results.filter.placeholder")} + icon={} + /> + ) : null} + {user?.id === layoutData.user.id ? ( + + {t("results.highlights.choose")} + + ) : null} +
    {data.results.pages > 1 ? ( diff --git a/app/features/user-page/user-page-constants.ts b/app/features/user-page/user-page-constants.ts index bd87c25ed..92d16b3bf 100644 --- a/app/features/user-page/user-page-constants.ts +++ b/app/features/user-page/user-page-constants.ts @@ -24,6 +24,7 @@ export const IN_GAME_NAME_REGEXP = /^.{1,10}#[0-9a-z]{4,5}$/u; export const MATCHES_PER_SEASONS_PAGE = 8; export const RESULTS_PER_PAGE = 25; +export const HIGHLIGHTS_RESULTS_MAX = 500; export const DEFAULT_BUILD_SORT = ["WEAPON_POOL", "UPDATED_AT"] as const; /** diff --git a/app/features/user-page/user-page-schemas.ts b/app/features/user-page/user-page-schemas.ts index af9e516a7..642edf4a7 100644 --- a/app/features/user-page/user-page-schemas.ts +++ b/app/features/user-page/user-page-schemas.ts @@ -199,6 +199,7 @@ export const adminTabActionSchema = z.union([ export const userResultsPageSearchParamsSchema = z.object({ all: z.stringbool().catch(false), page: z.coerce.number().min(1).max(1_000).catch(1), + tournament: z.string().trim().min(1).max(100).optional().catch(undefined), }); const widgetSettingsSchemas = allWidgetsFlat().map((widget) => { diff --git a/app/features/user-page/user-page.module.css b/app/features/user-page/user-page.module.css index a615f2bbb..88a6b57c8 100644 --- a/app/features/user-page/user-page.module.css +++ b/app/features/user-page/user-page.module.css @@ -126,6 +126,11 @@ overflow-x: auto; } +.resultsFilterInput { + width: 12rem; + max-width: 100%; +} + .resultsTableHighlights { border: var(--s-2) solid var(--color-bg-high); padding-inline: 0 !important; diff --git a/app/modules/brackets-manager/base/getter.ts b/app/modules/brackets-manager/base/getter.ts index 0818cbfc7..dcb2918f1 100644 --- a/app/modules/brackets-manager/base/getter.ts +++ b/app/modules/brackets-manager/base/getter.ts @@ -3,11 +3,9 @@ import type { GroupType, Match, Round, - SeedOrdering, Stage, StageType, } from "~/modules/brackets-model"; -import type { Create } from "../create"; import * as helpers from "../helpers"; import type { RoundPositionalInfo, Storage } from "../types"; @@ -538,38 +536,6 @@ export class BaseGetter { return [this.getDiagonalMatch(match.group_id, roundNumber, match.number)]; } - /** - * Returns the good seeding ordering based on the stage's type. - * - * @param stageType The type of the stage. - * @param create A reference to a Create instance. - */ - protected static getSeedingOrdering( - stageType: StageType, - create: Create, - ): SeedOrdering { - return stageType === "round_robin" - ? create.getRoundRobinOrdering() - : create.getStandardBracketFirstRoundOrdering(); - } - - /** - * Returns the matches which contain the seeding of a stage based on its type. - * - * @param stageId ID of the stage. - * @param stageType The type of the stage. - */ - protected getSeedingMatches( - stageId: number, - stageType: StageType, - ): Match[] | null { - if (stageType === "round_robin") - return this.storage.select("match", { stage_id: stageId }); - - const firstRound = this.getUpperBracketFirstRound(stageId); - return this.storage.select("match", { round_id: firstRound.id }); - } - /** * Gets the first round of the upper bracket. * diff --git a/app/modules/brackets-manager/create.ts b/app/modules/brackets-manager/create.ts index 56070db93..f4ab402e7 100644 --- a/app/modules/brackets-manager/create.ts +++ b/app/modules/brackets-manager/create.ts @@ -30,7 +30,7 @@ export function create(this: BracketsManager, stage: InputStage): Stage { return instance.run(); } -export class Create { +class Create { private storage: Storage; private stage: InputStage; private readonly seedOrdering: SeedOrdering[]; @@ -100,6 +100,8 @@ export class Create { * Group count must be given. It will distribute participants in groups and rounds. */ private roundRobin(): Stage { + if (this.stage.settings?.hasAbDivisions) return this.abDivisionRoundRobin(); + const groups = this.getRoundRobinGroups(); const stage = this.createStage(); @@ -109,6 +111,27 @@ export class Create { return stage; } + /** + * Creates a bipartite (A/B divisions) round-robin stage. + * + * Participants are partitioned into two pools by `abDivisions` (parallel to the seeding). + * Each group receives equal A and B teams, and matches only pair A against B. + */ + private abDivisionRoundRobin(): Stage { + const groups = this.getAbDivisionGroups(); + const stage = this.createStage(); + + for (let i = 0; i < groups.length; i++) + this.createAbDivisionRoundRobinGroup( + stage.id, + i + 1, + groups[i].a, + groups[i].b, + ); + + return stage; + } + /** * Creates a single elimination stage. * @@ -238,6 +261,33 @@ export class Create { this.createRound(stageId, groupId, i + 1, rounds[0].length, rounds[i]); } + /** + * Creates a bipartite round-robin group where every A team plays every B team exactly once. + * + * @param stageId ID of the parent stage. + * @param number Number in the stage. + * @param slotsA Slots in division A (ordered by seed). + * @param slotsB Slots in division B (ordered by seed). + */ + private createAbDivisionRoundRobinGroup( + stageId: number, + number: number, + slotsA: ParticipantSlot[], + slotsB: ParticipantSlot[], + ): void { + const groupId = this.insertGroup({ + stage_id: stageId, + number, + }); + + if (groupId === -1) throw Error("Could not insert the group."); + + const rounds = helpers.makeAbDivisionRoundRobinMatches(slotsA, slotsB); + + for (let i = 0; i < rounds.length; i++) + this.createRound(stageId, groupId, i + 1, rounds[0].length, rounds[i]); + } + /** * Creates a standard bracket, which is the only one in single elimination and the upper one in double elimination. * @@ -680,6 +730,58 @@ export class Create { return helpers.makeGroups(ordered, this.stage.settings.groupCount); } + /** + * Partitions the seeded slots into A and B pools then distributes them into groups + * such that each group has an equal number of A and B participants. + */ + private getAbDivisionGroups(): { + a: ParticipantSlot[]; + b: ParticipantSlot[]; + }[] { + if ( + this.stage.settings?.groupCount === undefined || + !Number.isInteger(this.stage.settings.groupCount) + ) + throw Error("You must specify a group count for round-robin stages."); + + if (this.stage.settings.groupCount <= 0) + throw Error("You must provide a strictly positive group count."); + + const abDivisions = this.stage.abDivisions; + if (!abDivisions) + throw Error( + "abDivisions must be provided when hasAbDivisions is enabled.", + ); + + const slots = this.getSlots(); + + if (abDivisions.length !== slots.length) + throw Error("abDivisions length must match the seeding length."); + + const divisionA: ParticipantSlot[] = []; + const divisionB: ParticipantSlot[] = []; + + for (let i = 0; i < slots.length; i++) { + const slot = slots[i]; + if (slot === null) + throw Error("BYEs are not supported with A/B divisions."); + + const division = abDivisions[i]; + if (division === 0) divisionA.push(slot); + else if (division === 1) divisionB.push(slot); + else + throw Error( + `Participant at seed ${i + 1} is missing an A/B division assignment.`, + ); + } + + return helpers.makeAbDivisionGroups( + divisionA, + divisionB, + this.stage.settings.groupCount, + ); + } + /** * Returns the ordering method for the groups in a round-robin stage. */ diff --git a/app/modules/brackets-manager/helpers.ts b/app/modules/brackets-manager/helpers.ts index ee00d77ed..f77801cbd 100644 --- a/app/modules/brackets-manager/helpers.ts +++ b/app/modules/brackets-manager/helpers.ts @@ -56,6 +56,99 @@ export function makeRoundRobinMatches( return [...distribution, ...symmetry]; } +/** + * Makes a list of rounds containing the matches of a bipartite (A/B divisions) round-robin group. + * + * Every A team plays every B team exactly once; there are no A-vs-A or B-vs-B matches. + * Round 1 is cross-seeded (strongest A vs weakest B), and B is rotated cyclically downward + * in each subsequent round. + * + * When the divisions have different sizes, the shorter side is padded with bye slots so the + * rotation still works. Those bye pairings are filtered out of the output, so each round has + * exactly `min(|A|, |B|)` real matches and the total is `|A| * |B|`. + * + * @param divisionA Participants in division A, ordered by seed. + * @param divisionB Participants in division B, ordered by seed. + */ +export function makeAbDivisionRoundRobinMatches( + divisionA: T[], + divisionB: T[], +): [T, T][][] { + const n = Math.max(divisionA.length, divisionB.length); + const paddedA: (T | null)[] = [ + ...divisionA, + ...Array(n - divisionA.length).fill(null), + ]; + const paddedB: (T | null)[] = [ + ...divisionB, + ...Array(n - divisionB.length).fill(null), + ]; + const rounds: [T, T][][] = []; + + for (let roundIdx = 0; roundIdx < n; roundIdx++) { + const matches: [T, T][] = []; + + for (let i = 0; i < n; i++) { + const bIdx = (((n - 1 - i - roundIdx) % n) + n) % n; + const a = paddedA[i]; + const b = paddedB[bIdx]; + if (a === null || b === null) continue; + matches.push([a, b]); + } + + rounds.push(matches); + } + + return rounds; +} + +/** + * Distributes A/B division participants into groups such that each group has an + * equal number of A and B participants. + * + * The snake ordering used by `groups.seed_optimized` is applied independently to + * each pool, so that relative seed order within each pool is preserved within + * every group. + * + * @param divisionA Participants in division A, ordered by seed. + * @param divisionB Participants in division B, ordered by seed. + * @param groupCount Number of groups to distribute into. + */ +export function makeAbDivisionGroups( + divisionA: T[], + divisionB: T[], + groupCount: number, +): { a: T[]; b: T[] }[] { + if (groupCount <= 0) throw Error("Group count must be strictly positive."); + + if (divisionA.length !== divisionB.length) { + if (groupCount !== 1) + throw Error( + "Uneven A/B divisions are only supported with a single group.", + ); + + return [{ a: divisionA, b: divisionB }]; + } + + if (divisionA.length % groupCount !== 0) + throw Error("Pool size must be divisible by group count."); + + const aOrdered = ordering["groups.seed_optimized"](divisionA, groupCount); + const bOrdered = ordering["groups.seed_optimized"](divisionB, groupCount); + + const perPoolGroupSize = divisionA.length / groupCount; + const groups: { a: T[]; b: T[] }[] = []; + + for (let i = 0; i < groupCount; i++) { + groups.push({ + a: aOrdered.slice(i * perPoolGroupSize, (i + 1) * perPoolGroupSize), + b: bOrdered.slice(i * perPoolGroupSize, (i + 1) * perPoolGroupSize), + }); + } + + return groups; +} + /** * Distributes participants in rounds for a round-robin group. * @@ -143,6 +236,57 @@ export function assertRoundRobin( } } +/** + * A helper to assert our generated bipartite round-robin is correct. + * + * @param divisionA Seeds in division A (ordered by seed). + * @param divisionB Seeds in division B (ordered by seed). + * @param output The resulting rounds of matches. + */ +export function assertAbDivisionRoundRobin( + divisionA: number[], + divisionB: number[], + output: [number, number][][], +): void { + const roundCount = Math.max(divisionA.length, divisionB.length); + const matchesPerRound = Math.min(divisionA.length, divisionB.length); + + if (output.length !== roundCount) throw Error("Round count is wrong"); + if (!output.every((round) => round.length === matchesPerRound)) + throw Error("Not every round has the good number of matches"); + + const aSet = new Set(divisionA); + const bSet = new Set(divisionB); + const seenPairings = new Set(); + + for (const round of output) { + const playingInRound = new Set(); + + for (const match of round) { + if (match.length !== 2) throw Error("One match is not a pair"); + + const [a, b] = match; + + if (!aSet.has(a)) throw Error(`${a} is not a division A participant`); + if (!bSet.has(b)) throw Error(`${b} is not a division B participant`); + + if (playingInRound.has(a)) throw Error("This team is already playing"); + playingInRound.add(a); + + if (playingInRound.has(b)) throw Error("This team is already playing"); + playingInRound.add(b); + + const pairingKey = `${a}-${b}`; + if (seenPairings.has(pairingKey)) + throw Error("The teams have already been paired"); + seenPairings.add(pairingKey); + } + } + + if (seenPairings.size !== divisionA.length * divisionB.length) + throw Error("Not every A vs B pairing was generated"); +} + /** * Distributes elements in groups of equal size. * diff --git a/app/modules/brackets-manager/index.ts b/app/modules/brackets-manager/index.ts index 3a0fd160b..3cc3935c8 100644 --- a/app/modules/brackets-manager/index.ts +++ b/app/modules/brackets-manager/index.ts @@ -1,30 +1,11 @@ import { BracketsManager } from "./manager"; -import type { - CrudInterface, - Database, - Duel, - OmitId, - OrderingMap, - ParticipantSlot, - Scores, - Side, - StandardBracketResults, - Storage, - Table, -} from "./types"; +import type { CrudInterface, Database, OmitId, Table } from "./types"; export { BracketsManager, type CrudInterface, type Database, - type Duel, type OmitId, - type OrderingMap, - type ParticipantSlot, - type Scores, - type Side, - type StandardBracketResults, - type Storage, type Table, }; diff --git a/app/modules/brackets-manager/test/helpers.test.ts b/app/modules/brackets-manager/test/helpers.test.ts index d119d0832..9a8de5d86 100644 --- a/app/modules/brackets-manager/test/helpers.test.ts +++ b/app/modules/brackets-manager/test/helpers.test.ts @@ -1,7 +1,10 @@ import { describe, expect, test } from "vitest"; import { + assertAbDivisionRoundRobin, assertRoundRobin, balanceByes, + makeAbDivisionGroups, + makeAbDivisionRoundRobinMatches, makeGroups, makeRoundRobinMatches, } from "../helpers"; @@ -35,6 +38,256 @@ describe("Round-robin groups", () => { }); }); +describe("A/B divisions round-robin groups", () => { + test("should pair every A with every B exactly once for N=2..6", () => { + for (const n of [2, 3, 4, 5, 6]) { + const divisionA = Array.from({ length: n }, (_, i) => i + 1); + const divisionB = Array.from({ length: n }, (_, i) => i + 1 + n); + + assertAbDivisionRoundRobin( + divisionA, + divisionB, + makeAbDivisionRoundRobinMatches(divisionA, divisionB), + ); + } + }); + + test("should produce N rounds and N^2 matches total", () => { + for (const n of [2, 3, 4, 5, 6]) { + const divisionA = Array.from({ length: n }, (_, i) => i + 1); + const divisionB = Array.from({ length: n }, (_, i) => i + 1 + n); + + const rounds = makeAbDivisionRoundRobinMatches(divisionA, divisionB); + + expect(rounds).toHaveLength(n); + expect(rounds.flat()).toHaveLength(n * n); + expect(rounds.every((round) => round.length === n)).toBe(true); + } + }); + + test("round 1 is cross-seeded (A[i] vs B[N-1-i])", () => { + for (const n of [2, 3, 4, 5, 6]) { + const divisionA = Array.from({ length: n }, (_, i) => i + 1); + const divisionB = Array.from({ length: n }, (_, i) => i + 1 + n); + + const [firstRound] = makeAbDivisionRoundRobinMatches( + divisionA, + divisionB, + ); + + const expected = divisionA.map<[number, number]>((a, i) => [ + a, + divisionB[n - 1 - i], + ]); + expect(firstRound).toEqual(expected); + } + }); + + test("matches the spec example for N=6", () => { + const divisionA = [1, 2, 3, 4, 5, 6]; + const divisionB = [11, 12, 13, 14, 15, 16]; + + const rounds = makeAbDivisionRoundRobinMatches(divisionA, divisionB); + + expect(rounds[0]).toEqual([ + [1, 16], + [2, 15], + [3, 14], + [4, 13], + [5, 12], + [6, 11], + ]); + expect(rounds[1]).toEqual([ + [1, 15], + [2, 14], + [3, 13], + [4, 12], + [5, 11], + [6, 16], + ]); + expect(rounds[2]).toEqual([ + [1, 14], + [2, 13], + [3, 12], + [4, 11], + [5, 16], + [6, 15], + ]); + }); + + test("supports uneven divisions where |A| = |B| + 1", () => { + const divisionA = [1, 2, 3, 4, 5, 6]; + const divisionB = [11, 12, 13, 14, 15]; + + const rounds = makeAbDivisionRoundRobinMatches(divisionA, divisionB); + + assertAbDivisionRoundRobin(divisionA, divisionB, rounds); + expect(rounds).toHaveLength(6); + expect(rounds.flat()).toHaveLength(5 * 6); + expect(rounds.every((round) => round.length === 5)).toBe(true); + + const byeCountPerA = new Map(divisionA.map((a) => [a, 0])); + for (const round of rounds) { + const playingA = new Set(round.map(([a]) => a)); + for (const a of divisionA) { + if (!playingA.has(a)) byeCountPerA.set(a, byeCountPerA.get(a)! + 1); + } + } + expect([...byeCountPerA.values()]).toEqual([1, 1, 1, 1, 1, 1]); + }); + + test("supports uneven divisions where |B| = |A| + 1", () => { + const divisionA = [1, 2, 3, 4, 5]; + const divisionB = [11, 12, 13, 14, 15, 16]; + + const rounds = makeAbDivisionRoundRobinMatches(divisionA, divisionB); + + assertAbDivisionRoundRobin(divisionA, divisionB, rounds); + expect(rounds).toHaveLength(6); + expect(rounds.flat()).toHaveLength(5 * 6); + expect(rounds.every((round) => round.length === 5)).toBe(true); + + const byeCountPerB = new Map(divisionB.map((b) => [b, 0])); + for (const round of rounds) { + const playingB = new Set(round.map(([, b]) => b)); + for (const b of divisionB) { + if (!playingB.has(b)) byeCountPerB.set(b, byeCountPerB.get(b)! + 1); + } + } + expect([...byeCountPerB.values()]).toEqual([1, 1, 1, 1, 1, 1]); + }); + + test("handles non-contiguous seed identifiers in each pool", () => { + const divisionA = [1, 4, 5]; + const divisionB = [9, 10, 12]; + + const rounds = makeAbDivisionRoundRobinMatches(divisionA, divisionB); + + assertAbDivisionRoundRobin(divisionA, divisionB, rounds); + expect(rounds[0]).toEqual([ + [1, 12], + [4, 10], + [5, 9], + ]); + }); +}); + +describe("A/B division group distribution", () => { + const CASES: ReadonlyArray = [ + [6, 1], + [6, 2], + [6, 3], + [6, 6], + [4, 1], + [4, 2], + [4, 4], + [3, 1], + [3, 3], + [8, 2], + [8, 4], + ]; + + test("places all teams with equal A/B per group for supported sizes", () => { + for (const [poolSize, groupCount] of CASES) { + const divisionA = Array.from({ length: poolSize }, (_, i) => i + 1); + const divisionB = Array.from( + { length: poolSize }, + (_, i) => i + 1 + poolSize, + ); + + const groups = makeAbDivisionGroups(divisionA, divisionB, groupCount); + + expect(groups).toHaveLength(groupCount); + + const perGroupSize = poolSize / groupCount; + for (const group of groups) { + expect(group.a).toHaveLength(perGroupSize); + expect(group.b).toHaveLength(perGroupSize); + } + + const flatA = groups.flatMap((g) => g.a).sort((x, y) => x - y); + const flatB = groups.flatMap((g) => g.b).sort((x, y) => x - y); + expect(flatA).toEqual(divisionA); + expect(flatB).toEqual(divisionB); + } + }); + + test("preserves ascending seed order within each group's A and B pools", () => { + for (const [poolSize, groupCount] of CASES) { + const divisionA = Array.from({ length: poolSize }, (_, i) => i + 1); + const divisionB = Array.from( + { length: poolSize }, + (_, i) => i + 1 + poolSize, + ); + + const groups = makeAbDivisionGroups(divisionA, divisionB, groupCount); + + for (const group of groups) { + expect(group.a).toEqual([...group.a].sort((x, y) => x - y)); + expect(group.b).toEqual([...group.b].sort((x, y) => x - y)); + } + } + }); + + test("is deterministic for identical input", () => { + const divisionA = [1, 2, 3, 4, 5, 6]; + const divisionB = [7, 8, 9, 10, 11, 12]; + + const first = makeAbDivisionGroups(divisionA, divisionB, 2); + const second = makeAbDivisionGroups(divisionA, divisionB, 2); + + expect(first).toEqual(second); + }); + + test("matches the expected snake distribution for 12 teams, 2 groups", () => { + const divisionA = [1, 2, 3, 4, 5, 6]; + const divisionB = [7, 8, 9, 10, 11, 12]; + + expect(makeAbDivisionGroups(divisionA, divisionB, 2)).toEqual([ + { a: [1, 4, 5], b: [7, 10, 11] }, + { a: [2, 3, 6], b: [8, 9, 12] }, + ]); + }); + + test("matches the expected snake distribution for 12 teams, 3 groups", () => { + const divisionA = [1, 2, 3, 4, 5, 6]; + const divisionB = [7, 8, 9, 10, 11, 12]; + + expect(makeAbDivisionGroups(divisionA, divisionB, 3)).toEqual([ + { a: [1, 6], b: [7, 12] }, + { a: [2, 5], b: [8, 11] }, + { a: [3, 4], b: [9, 10] }, + ]); + }); + + test("single group contains all teams", () => { + const divisionA = [1, 2, 3, 4]; + const divisionB = [5, 6, 7, 8]; + + expect(makeAbDivisionGroups(divisionA, divisionB, 1)).toEqual([ + { a: divisionA, b: divisionB }, + ]); + }); + + test("allows uneven pools with a single group", () => { + expect(makeAbDivisionGroups([1, 2, 3], [4, 5], 1)).toEqual([ + { a: [1, 2, 3], b: [4, 5] }, + ]); + }); + + test("throws when pools have different sizes and multiple groups", () => { + expect(() => makeAbDivisionGroups([1, 2, 3], [4, 5], 2)).toThrow(); + }); + + test("throws when pool size is not divisible by group count", () => { + expect(() => makeAbDivisionGroups([1, 2, 3], [4, 5, 6], 2)).toThrow(); + }); + + test("throws when group count is not positive", () => { + expect(() => makeAbDivisionGroups([1], [2], 0)).toThrow(); + }); +}); + describe("Seed ordering methods", () => { test("should place 2 participants with inner-outer method", () => { const teams = [1, 2]; diff --git a/app/modules/brackets-manager/test/round-robin.test.ts b/app/modules/brackets-manager/test/round-robin.test.ts index f27e52938..879e56886 100644 --- a/app/modules/brackets-manager/test/round-robin.test.ts +++ b/app/modules/brackets-manager/test/round-robin.test.ts @@ -164,6 +164,98 @@ describe("Create a round-robin stage", () => { }), ).toThrow("You must provide a strictly positive group count."); }); + + test("creates an A/B divisions round-robin where every A team plays every B team once", () => { + const seeding = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + // alternate A (0) / B (1) so that seed order 1..12 gives A=[1,3,5,7,9,11], B=[2,4,6,8,10,12] + const abDivisions = seeding.map((_, i) => (i % 2 === 0 ? 0 : 1)); + + manager.create({ + name: "AB Example", + tournamentId: 0, + type: "round_robin", + seeding, + abDivisions: abDivisions as (0 | 1)[], + settings: { + groupCount: 1, + hasAbDivisions: true, + seedOrdering: ["groups.seed_optimized"], + }, + }); + + expect(storage.select("group")!.length).toBe(1); + expect(storage.select("round")!.length).toBe(6); + expect(storage.select("match")!.length).toBe(36); + + const divisionAIds = new Set([1, 3, 5, 7, 9, 11]); + const divisionBIds = new Set([2, 4, 6, 8, 10, 12]); + const pairings = new Set(); + + for (const match of storage.select("match")!) { + const aId: number = match.opponent1.id; + const bId: number = match.opponent2.id; + + expect(divisionAIds.has(aId)).toBe(true); + expect(divisionBIds.has(bId)).toBe(true); + + const key = `${aId}-${bId}`; + expect(pairings.has(key)).toBe(false); + pairings.add(key); + } + + expect(pairings.size).toBe(36); + }); + + test("throws when A/B divisions are requested but abDivisions is missing", () => { + expect(() => + manager.create({ + name: "Missing AB", + tournamentId: 0, + type: "round_robin", + seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + settings: { + groupCount: 1, + hasAbDivisions: true, + }, + }), + ).toThrow("abDivisions must be provided when hasAbDivisions is enabled."); + }); + + test("creates an A/B divisions round-robin with uneven (±1) divisions and a single group", () => { + const seeding = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; + manager.create({ + name: "Uneven AB", + tournamentId: 0, + type: "round_robin", + seeding, + abDivisions: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0], + settings: { + groupCount: 1, + hasAbDivisions: true, + seedOrdering: ["groups.seed_optimized"], + }, + }); + + expect(storage.select("group")!.length).toBe(1); + expect(storage.select("round")!.length).toBe(6); + expect(storage.select("match")!.length).toBe(30); + }); + + test("throws when A/B divisions are uneven with multiple groups", () => { + expect(() => + manager.create({ + name: "Uneven AB multi-group", + tournamentId: 0, + type: "round_robin", + seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + abDivisions: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0], + settings: { + groupCount: 2, + hasAbDivisions: true, + }, + }), + ).toThrow("Uneven A/B divisions are only supported with a single group."); + }); }); describe("Update scores in a round-robin stage", () => { diff --git a/app/modules/brackets-manager/types.ts b/app/modules/brackets-manager/types.ts index 29700c2f7..39ced56ba 100644 --- a/app/modules/brackets-manager/types.ts +++ b/app/modules/brackets-manager/types.ts @@ -44,11 +44,6 @@ export type Duel = [ParticipantSlot, ParticipantSlot]; */ export type Side = "opponent1" | "opponent2"; -/** - * The cumulated scores of the opponents in a match's child games. - */ -export type Scores = { opponent1: number; opponent2: number }; - /** * Positional information about a round. */ diff --git a/app/modules/brackets-model/input.ts b/app/modules/brackets-model/input.ts index 2a2e3745f..6651eff3d 100644 --- a/app/modules/brackets-model/input.ts +++ b/app/modules/brackets-model/input.ts @@ -43,6 +43,12 @@ export interface InputStage { /** Contains participants or `null` for BYEs. */ seeding?: Seeding; + /** + * A/B division assignment parallel to `seeding`. `0` = A, `1` = B. + * Required when `settings.hasAbDivisions` is `true`. + */ + abDivisions?: (0 | 1)[]; + /** Contains optional settings specific to each stage type. */ settings?: StageSettings; } @@ -90,6 +96,13 @@ export interface StageSettings { */ roundRobinMode?: RoundRobinMode; + /** + * Whether to generate a bipartite round-robin where teams are split into two + * A/B divisions and every match pairs one A team with one B team. + * Only valid on round-robin stages. + */ + hasAbDivisions?: boolean; + /** * A list of seeds per group for a round-robin stage to be manually ordered. * diff --git a/app/routines/setOldGroupsAsInactive.ts b/app/routines/setOldGroupsAsInactive.ts index 6389ff9c4..9f6609155 100644 --- a/app/routines/setOldGroupsAsInactive.ts +++ b/app/routines/setOldGroupsAsInactive.ts @@ -1,3 +1,4 @@ +import { refreshSendouQInstance } from "~/features/sendouq/core/SendouQ.server"; import * as SQGroupRepository from "~/features/sendouq/SQGroupRepository.server"; import { logger } from "../utils/logger"; import { Routine } from "./routine.server"; @@ -6,6 +7,7 @@ export const SetOldGroupsAsInactiveRoutine = new Routine({ name: "SetOldGroupsAsInactive", func: async () => { const { numUpdatedRows } = await SQGroupRepository.setOldGroupsAsInactive(); + await refreshSendouQInstance(); logger.info(`Set ${numUpdatedRows} as inactive`); }, }); diff --git a/app/styles/common.css b/app/styles/common.css index 4a5a6aa9d..154c013e1 100644 --- a/app/styles/common.css +++ b/app/styles/common.css @@ -461,6 +461,7 @@ height: 100%; border: none; background-color: var(--color-bg-higher); + color: var(--color-text-high); white-space: nowrap; margin-right: var(--s-2); } diff --git a/app/styles/utils.css b/app/styles/utils.css index 598e79955..f64367cf3 100644 --- a/app/styles/utils.css +++ b/app/styles/utils.css @@ -85,6 +85,7 @@ .underline { text-decoration: underline; + text-decoration-thickness: 2px; } .line-through { diff --git a/app/utils/errors.ts b/app/utils/errors.ts index 61ae95a3b..d42542ec2 100644 --- a/app/utils/errors.ts +++ b/app/utils/errors.ts @@ -4,3 +4,10 @@ export class LimitReachedError extends Error { this.name = "LimitReachedError"; } } + +export class ConcurrentModificationError extends Error { + constructor(message: string) { + super(message); + this.name = "ConcurrentModificationError"; + } +} diff --git a/app/utils/remix.server.ts b/app/utils/remix.server.ts index cf52aa0fc..58afdcaa1 100644 --- a/app/utils/remix.server.ts +++ b/app/utils/remix.server.ts @@ -302,7 +302,7 @@ export function privatelyCachedJson(dataValue: T) { }); } -const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024; +const DEFAULT_MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024; type FileUploadHandler = ( fileUpload: FileUpload, @@ -323,6 +323,11 @@ export async function safeParseMultipartFormData( optionsOrHandler?: ParseFormDataOptions | FileUploadHandler, uploadHandler?: FileUploadHandler, ): Promise { + const maxFileSize = + typeof optionsOrHandler === "object" && optionsOrHandler?.maxFileSize + ? optionsOrHandler.maxFileSize + : DEFAULT_MAX_FILE_SIZE_BYTES; + try { if (typeof optionsOrHandler === "function") { return await parseMultipartFormData(request, optionsOrHandler); @@ -335,11 +340,12 @@ export async function safeParseMultipartFormData( } catch (err) { if ( err instanceof Error && - err.cause instanceof Error && - err.cause.name === "MaxFileSizeExceededError" + (err.name === "MaxFileSizeExceededError" || + (err.cause instanceof Error && + err.cause.name === "MaxFileSizeExceededError")) ) { throw errorToastRedirect( - `File size exceeds maximum allowed size of ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB`, + `File size exceeds maximum allowed size of ${maxFileSize / 1024 / 1024}MB`, ); } throw err; diff --git a/content/articles/gamers-for-giving-2026.md b/content/articles/gamers-for-giving-2026.md new file mode 100644 index 000000000..2564bc565 --- /dev/null +++ b/content/articles/gamers-for-giving-2026.md @@ -0,0 +1,119 @@ +--- +title: "Gamers for Giving 2026" +date: 2026-04-09 +author: + - name: YELLOW + link: https://sendou.ink/u/great-hero-yellow +--- + +#### *Splatoon fans coming together to bring the joy of gaming to those in need* + +gamers-for-giving-2026 + +On Sunday, April 5, 2026, Dapple Productions and Inkling Performance Labs (IPL) partnered with Gamers Outreach to bring the Gamers for Giving 2026 tournament to the competitive community. The Splatoon 3 tournament aimed to raise money to help provide families in hospitals with video games. + +With Neato and Kyon commentating for IPL and SPF\_Dark and Chaedr on the Dapple side, the two streams gave the audience options to choose from, as both streams showed different matches until the Loser’s Semifinals, where Pat and Scep took over. + +The results of the first four rounds are below: + +**Round 1:** We Deleted Goowapa for Charity vs. Good Squiddy (2-0) \[IPL stream\] +**Round 1:** GDM vs. Last Minute Heist (0-2) \[Dapple stream\] + +**Round 2:** 👻 vs. Sailor Moon Lovers (2-0) \[IPL stream\] +**Round 2:** Unknown? vs. \[I\] Eclipse (2-1) \[Dapple stream\] + +**Round 3:** Ascension vs. Zipcat (3-2) \[IPL stream\] +**Round 3:** G.I.G vs. BlankZ (2-3) \[Dapple stream\] + +**Winner’s Semifinals:** nm vs. Ascension (3-1) \[IPL stream\] +**Winner’s Semifinals:** bread vs. BlankZ (3-0) \[Dapple stream\] + +## **Winner’s Finals: nm vs. bread (3-2)** + +Starting with the Winner’s Finals, the dual streams converged into one IPL stream. The winner of the IPL stream, nm, would face bread, the winner of the Dapple stream. Get ready to see these two teams against one another, as they would bring plenty of mileage to the table\! + +Winner’s Finals opened with Splat Zones at MakoMart. Bread held the zone all the way up to 25 before nm flipped it in their favor. Nm wiped out bread multiple times in the game, but even all of that was just barely enough for them to eke out a win in overtime, after holding the zone through their penalty to take the lead, for a final score of 93-92. + +Bread counterpicked to Splat Zones at Um’ami Ruins—nm took the lead from bread before the first minute was up, but bread quickly reclaimed the zone and their lead. Before the game clock was halfway through, a delayed wipeout on nm let bread take the knockout victory. + +GFG-WF_zonesum'ami + +*Bread, having just taken down all members of nm, stays on the zone as the ticks count down to their knockout.* + +Done with Splat Zones, the game transitioned to Tower Control at Inkblot Art Academy. Bread continued their dominating streak, not letting nm have the lead for very long, and sailed through their third checkpoint. The game did go into overtime, but ended with a wipeout on nm and another victory for bread, 88-39. + +Rainmaker at Humpback Pump Track was next, and both teams took turns clearing their first checkpoint. Bread set the lead at 17; nm responded by getting to 33\. In the final minute of the game, nm wiped out bread and took the Rainmaker all the way to 4, just shy of the knockout. The final score was 96-83 in favor of nm, and now the set was tied 2-2. + +Game five went to Splat Zones again, at Robo ROM-en. The lead swapped a couple of times in the first minute, but by the end of it, it was nm extending their lead. Bread did their best to keep the penalty points piling on nm, but were barely able to stop nm at 3 in the last minute. Ultimately, nm earned their knockout, and seed \#2 would send seed \#1 to the Loser’s Bracket. + +GFG-WFnmvictory + +*Seed \#2, team nm, composed of Jasmine, Obito (umiha), SnipeZ\! (MESMERIZER), and Mehdi.* + +### **Loser’s Semifinals: BlankZ vs. Ascension (2-1)** + +In the Loser’s Semifinals, BlankZ and Ascension met for a Bo3 set to determine who went to the Loser’s Finals to face bread. Between the prior set ending and this one starting, the donation pool surpassed its first goal of $500\! + +**Game 1**, Splat Zones at Robo ROM-en, went to BlankZ, 98-87 after overtime. +**Game 2**, Splat Zones at Um’ami Ruins, went to Ascension, winning in a knockout. +**Game 3**, Tower Control at Hagglefish Market, went to BlankZ, 76-61. + +## **Loser’s Finals: bread vs. BlankZ (3-0)** + +BlankZ had earned their rematch against bread—in Winner’s Semifinals, bread 3-0’d BlankZ, and bread wanted to keep it that way to get back to nm as soon as they could. + +The first game was fairly predictable considering the map and mode was Barnacle & Dime Splat Zones. Too often, games here end in a knockout, and that’s exactly what happened: bread won the game with over three minutes left on the game clock. + +BlankZ counterpicked to Rainmaker at Undertow Spillway, where they had previously fought bread, and came close to winning but fell short by just 4 points. This game would be far different, and not just because both teams opted to take the right-side route instead of the left. After several wipeouts on BlankZ, bread took the win, 97-40. + +Game three was Rainmaker again, at Humpback Pump Track. Bread already cleared the first checkpoint 20 seconds in. BlankZ, down two players and the others occupied in mid, had to scramble to regroup after bread snuck the Rainmaker to 4\. With less than 30 seconds remaining, BlankZ made a huge push through Triple Inkstrikes… but was stopped at 5, and wiped out. Bread took the win 96-95, and the set 3-0. + +GFG-LFrmhumpback + +*While bread keeps BlankZ busy in mid, Soulja runs the Rainmaker uncontested and almost gets the knockout.* + +## **Grand Finals: nm vs. bread (1-3)** + +And bread returns to take on nm for the second time\! Even though it’s Grand Finals, this wouldn’t be the end of the tournament if bread took the win, instead forcing a bracket reset before the winner was determined. + +Splat Zones at Mahi-Mahi Resort was another speedy knockout from bread, not giving nm the zone at all. The best nm could do was neutralize the zone; at the end, they were wiped out as the game ended one minute and 32 seconds in. + +Back at Humpback Pump Track for Rainmaker, we started to see a recurring pattern of bread falling behind after nm uses an Ink Vac to protect the objective (in this case, the Rainmaker carrier). Right as the clock reached 2:00, this tactic led nm to their own knockout win, tying the set 1-1. + +GFG-GFrmhumpback + +*nm uses three specials—Ultra Stamp, Big Bubbler, and Ink Vac—to safely escort Mehdi the Rainmaker carrier to take the lead, and eventually, the knockout.* + +Game three was Tower Control at Inkblot Art Academy. While both teams cleared their first checkpoints in the first minute and a half, it wasn’t until the last 30 seconds that anybody got through the second—belonging to bread. After overtime, nm remained 40 points behind bread, for a 35-75 loss. + +The set ended with another game of Splat Zones, at Robo ROM-en, which nm knocked out on the last time these two were there. Plenty of close saves this game, with bread barely saving their lead, 63-64, then nm taking the lead by one point just frames before bread recapped the zone. The game ended in a knockout, but this time, the win went to bread. + +And to the Grand Finals Reset we go\! + +## **Grand Finals Reset: nm vs. bread (1-3)** + +The set began with Splat Zones, like every other set, but at Hagglefish Market this time. The story starts to repeat itself: bread won the game, leaving nm with a bitter wipeout as the match closed. Though this time, no knockouts, just a 92-63 score. + +Game two went to Humpback Pump Track again, and again nm outplayed bread by keeping the Rainmaker safe from bread’s Triple Inkstrikes with an Ink Vac, taking the lead 20-26. Nm wiped out bread as a bonus, and the game ended after overtime with nm still in the lead, 80-74. + +GFG-GFRrmhumpback + +*Fearlessly charging right through Triple Inkstrikes, with an Ink Vac protecting their back, Mehdi takes the lead.* + +And for the third time, these teams met for Tower Control at Inkblot Art Academy for game three\! Nm tried a defensive approach, bringing a double Big Bubbler comp in addition to a Snipewriter 5H. It got them their checkpoint first, but bread’s momentum snowballed after their first checkpoint, putting them ahead. Even down 3 players in overtime, bread managed to sneak onto the tower to win, 45-42. + +Clam Blitz finally makes its first appearance, as the final sendoff for Gamers for Giving 2026\! Bread, over a 30-second push, brought their score from 100 to 30\. Nm’s Ink Vac and Big Bubbler only protected them long enough to get to 48\. Both teams scored again, but weren’t able to get more points due to their hefty penalties. + +Overtime went in nm’s favor, with an Ink Vac online and a Power Clam approaching the basket. Both teams were down one player. As the timer ticked down, bread’s Splash-o-matic popped out of the ink and splatted the clam holder, and by then, the clock ran out. Bread won the game 70-52, and after 13 games with nm, walked out of the tournament with the gold (and a special Sendou.ink badge)\! + +GFG-GFRbreadvictory + +*Fist bumps well-earned for bread: y0shell, phoenix (Jirachi), Soulja, and ecstacy.* + +Coming out of the tournament, one thing is certain: the people like Rainmaker at Humpback Pump Track\! Appearing four times on the merged IPL stream, beating Splat Zones at Robo ROM-en and Tower Control at Inkblot Art Academy, which appeared three times each. + +As the stream came to a close, the final donation total was $765.00 USD, over $250 more than the original donation goal of $500, and surpassing the second donation goal of $750.00\! + +Original Posting Date: April 9, 2026 at [Splatoon Stronghold](https://www.splatoonstronghold.com/news/gamers-for-giving-2026). + +Written and formatted for publication by [YELLOW](https://bsky.app/profile/great-hero-yellow.bsky.social). diff --git a/content/articles/na-lague-2026-p1e1.md b/content/articles/na-lague-2026-p1e1.md new file mode 100644 index 000000000..02cdcfbc1 --- /dev/null +++ b/content/articles/na-lague-2026-p1e1.md @@ -0,0 +1,156 @@ +--- +title: "NA League 2026 Event #1 (Preseason Week 1)" +date: 2026-04-07 +author: + - name: YELLOW + link: https://sendou.ink/u/great-hero-yellow +--- + +#### *The Splatoon 3 North American League 2026 begins, and this one has it all\!* + +NAL2026-p1e1 + +Almost four months after the end of the 2025 Splatoon 3 North American League, the NA League returns for a 2026 edition, with plenty of familiar faces, maps, and modes. Jordan, Zach, and Mellana are our hosts, with Piko on the spectator camera. + +There’s no slow starts this time: the level of action you’d expect from the last event of the Regular Season kicks off in Preseason Week 1\! Knockouts, shutouts, game five sets, and even a Grand Finals bracket reset took place on Saturday, April 4\. + +Although throughout the event, the Splattercolor Screen was used, its audio and visual effects did not show up on stream, so those who are sensitive to it can safely watch the replay. + +These teams earned their way to the top cut this week: + +1. TZS (better known as Triggerfish Zones Supremacy) +2. Windsurfers +3. InkThys +4. 11 ft 8 Bridge +5. triple dippers +6. bunnybunny +7. Broke Birds +8. Reignfall + +Only a handful of players in this lineup have made it to a top cut stream in the previous NA League, so with plenty of fresh talent on display, we dive in\! + +## **Winner’s Quarterfinals: InkThys vs. 11 ft 8 Bridge (0-3)** + +Starting off the Splatoon 3 North American League’s 2026 season is the classic Splat Zones pick on one of the new map options, Robo ROM-en. For most of the game, the teams traded the lead, settling at a close 28-27 favoring InkThys halfway through. With 19 seconds remaining, 11 ft 8 Bridge took the lead and brought it all the way to a knockout victory before the clock reached zero. + +Next, the teams went to Crableg Capital for Rainmaker. They kept the score close, first 61-62, and then 40-41. InkThys would remain at 40, while 11 ft 8 Bridge pressed forward with less than a minute and a half remaining, taking the Rainmaker all the way to 1 tick left. The last 15 seconds in the game had all players in mid throwing ink at the Rainmaker shield. The victor was 11 ft 8 Bridge, 99-60. + +WQF_RMcrableg + +Game three was Tower Control at Hagglefish Market. Wasting no time, 11 ft 8 Bridge already had the tower at 46 by the time the first minute passed. Halfway through the game, they scored a wipeout on InkThys. The final score was 64-28, to 11 ft 8 Bridge, securing them the set shutout, 3-0\! + +## **Winner’s Semifinals: triple dippers vs. Broke Birds (3-1)** + +With 11 ft 8 Bridge advancing to face TZS next, the stream shifted to the other bracket for Winner’s Semi-Finals: triple dippers vs. Broke Birds. Triple dippers would have an advantage going into this match, having a few players on their side from team FOAMS NA, who were often seen in the top 16 of the 2025 NA League. + +Game one started at MakoMart for Tower Control. Both teams took their turn with the lead in the first two minutes. Triple dippers, behind in score, wiped out Broke Birds and drove to surpass Broke Birds’ 43 points. They got to 42—one point behind Broke Birds—before getting wiped out themselves. The game would end with that one point difference: Broke Birds winning 58-57. + +WSF_TCmakomart + +*Broke Birds’ silver., signaling This Way\! to their teammates after finishing the wipeout.* + +Back to Hagglefish Market, for Splat Zones this time, Broke Birds took an early advantage by wiping out triple dippers 20 seconds in. They kept the zone for a minute longer before triple dippers threw every special they had at the zone to stop Broke Birds at 6\. Their momentum kept them going, for a wipeout on Broke Birds, then a lead switch, and finally a knockout victory with a little less than a minute and a half left. + +Game three was Rainmaker at Undertow Spillway; triple dippers cleared their first checkpoint fairly quickly, and Broke Birds responded by taking the lead back… At the cost of being wiped out. A lead to 46 isn’t anything to rest on, and soon triple dippers had the lead again. They lost the Rainmaker at 7; upon popping it, they wiped out Broke Birds, allowing them to knockout with 1:18 to spare\! + +It didn’t take long for Turf War to show up in the 2026 NA League, and it debuted at Museum d’Alfonsino. The teams adjusted their comps for the mode—a Colorz Aerospray made an appearance. As for the gameplay, as one would expect from Turf War, things remained even up to the final seconds. In the last 20 seconds, Broke Birds faltered, and the final score was 56.1% to 38.5%, to triple dippers. + +## **Winner’s Finals: TZS vs. triple dippers (3-2)** + +Up until now we’ve seen knockouts, 3-0 set shut-outs, and victories decided by just one point. It’s about time we get two more events to have everyone on the edge of their seats: 100-to-0 games and a game five set\! + +Opening Winner’s Finals was Turf War at Robo ROM-en; while teams focused on painting over splatting, as the game reached the halfway point, TZS fell behind, having the Danger\! warning above their heads. In the last 45 seconds, TZS was wiped out, and triple dippers won 64% to 32%, for a clean 96% total ink coverage. + +Game two went to Splat Zones at MakoMart, for the first of two 100-0 knockouts. TZS took the zone in 15 seconds, and the story ends there. Triple dippers had no chance to touch the zone, and at 3:45, TZS had their knockout win. + +TZS_zonesmakomartWF + +*TZS, just ticks from knocking out, down two players while triple dippers is down 3\.* + +Game three, Tower Control at Inkblot Art Academy, was the same story, only with triple dippers as the lead. At 4:35, triple dippers hopped on the tower, and rode it all the way to the end without pause, even wiping out TZS just a tick before knocking out, at 3:43. + +Our first repeat map and mode, Rainmaker at Undertow Spillway, had the same flow as a game of Turf War: mostly stalemate-y until the final seconds. Triple dippers cleared their first checkpoint, then TZS followed. No new points were scored until the last 8 seconds, where KrakenMare grabbed the Rainmaker and leapt forward, taking the lead by two points. Although the game went into overtime, TZS remained victorious, 68-40. + +Finally, Clam Blitz at Museum d’Alfonsino was forced out by a game five to determine who went to Grand Finals and who went to the Loser’s Bracket. TZS was already scoring within the first minute, and they kept doing so, frequently. While all eyes were on triple dippers advancing into TZS’s plat, all the way across the map, TZS snuck in and scored to a knockout, ending the game 30 seconds early. + +WF_clamsmuseum + +*Triple dippers on their opponent’s plat, having to 180° after seeing TZS’s Power Clam super jump all the way to their basket.* + +## **Loser’s Semifinals: Broke Birds vs. Reignfall (3-0)** + +Loser’s Semifinals repeats a match in Winner’s Quarterfinals between Broke Birds and Reignfall, which we didn’t see on stream earlier. Broke Birds won 3-1 over Reignfall initially, and if they do it again, they will meet triple dippers in Loser’s Finals for another repeat set. + +Again, Splat Zones at Robo ROM-en would make an appearance. Broke Birds’ initial push left them at 45, which would be a tough hill for Reignfall to climb. By the time they got close, Broke Birds moved the goal to 8\. Reignfall put forward their best effort, but the clock ran out while the zone was neutral, leaving Reignfall just shy of the win, 82-92. + +Game two, Inkblot Art Academy for Tower Control, saw Broke Birds trade Chai for Skaboop, but the focus was all on Golias and the Cometz Octobrush. Reignfall’s downfall was focusing on Golias while the rest of Broke Birds was clearing checkpoints and eventually, taking the tower to the knockout, for a second Broke Birds victory. + +Game three had another “chai swap”, as this time Reignfall swapped out demONdaz for evil chai, doubling the amount of Brushes at Scorch Gorge for Rainmaker. + +LS_twochai + +*Yes, “Chai vs. evil chai” was a joke both teams worked together to provide to the stream.* + +Broke Birds forged their way to an early advantage of 32 shortly after clearing their first checkpoint. Reignfall fought back, being met with two wipeouts. Broke Birds would take the third game, 68-30, and go on to rematch triple dippers in Loser’s Finals. + +## **Loser’s Finals: triple dippers vs. Broke Birds (3-1)** + +Triple dippers and Broke Birds back at it again, both ready to see a different result from Winner’s Semifinals. There, triple dippers won 3-1, and both teams were hoping for a 3-0 the second time around. + +Starting off at Splat Zones at Hagglefish Market, triple dippers took the zone first, and their initial push brought them all the way down to 19\. What sounds like a tall order, Broke Birds made look easy, surpassing 19 in one go. They were halted just short of the knockout with a neutralized zone, but silver. clutched out a quad and brought Broke Birds to a knockout. + +LF_zoneshagglefish + +Clam Blitz at Museum d’Alfonsino returned—triple dippers went from 100 to 44 in one 30-second push. Broke Birds struggled to get forward, only getting their first points in overtime, and wouldn’t score enough to take the lead. Triple dippers won 56-20. + +With the set tied, Tower Control at MakoMart was next, and it took nearly two minutes for the tower to finally pass the 90-point mark. But when it did, it was triple dippers on a roll to the end. Broke Birds tried to stop triple dippers from knocking out by sending in the Kraken Royale, but two specials were enough to keep it at bay, and triple dippers had the knockout secured. + +LF_TCmakomart + +*Golias’s Kraken Royale on the tower, about to tank FOAMArcher’s Ink Vac burst.* + +Rainmaker at Scorch Gorge set game four. Broke Birds took their checkpoint first and dove all the way to 36\. They held onto this lead for about two minutes, but after triple dippers cleared their first checkpoint, they deployed three specials to protect their Rainmaker carrier as they swam to a new lead. The game went into overtime, but once more Broke Birds couldn’t make the play they needed, and triple dippers won 83-64. + +## **Grand Finals: TZS vs. triple dippers (0-3)** + +The Grand Finals set that everything else was leading up to brings another face-off between Triggerfish Zones Supremacy and triple dippers. The set was sure to be tense, given that their last set went to a game five. + +Grand Finals began with Clam Blitz at Scorch Gorge. TZS scored minimally, only getting Power Clams in one at a time. Triple dippers, meanwhile, brought their score to 51 in one big push, taking the lead, and defended it through overtime, for a game one victory 49-30. + +Urchin Underpass appeared for the first time, for Tower Control; TZS swapped out KrakenMare for DRF, hoping the Tri-Slosher ASH-N’s Splattercolor Screen would give them an edge. Unfortunately as we’ve seen from triple dippers, once they get going, it’s hard to stop them; after they cleared their first checkpoint, they went all the way through the second. TZS tried catching up, but fell short, 53-75. + +Manta Maria also appeared for the first time in the 2026 NA League, for Rainmaker. TZS denied triple dippers their checkpoint and wiped them out, which led to TZS getting their lead all the way to 22\. It took until the last minute for triple dippers to clear that checkpoint, and taking a page from TZS’s Winner’s Finals playbook, they dove ahead to take the lead by two points in the final seconds, winning 80-78. + +GF_RMmantamaria + +*Nicochico making a game-winning dive for points, dodging everything TZS threw at them.* + +With triple dippers coming from the Loser’s Bracket and having won Grand Finals 3-0, it was time for a Grand Finals Reset\! + +## **Grand Finals Reset: TZS vs. triple dippers (3-1)** + +The first event of the 2026 NA League keeps on giving, and we find ourselves with a Grand Finals Reset\! From a 3-2 set, to getting 0-3’d, the storyline between these two teams has enough gas for one more rematch. + +Kicking off with Turf War at Urchin Underpass, triple dippers brought out both the N-ZAP ‘85 *and* ‘89. TZS was much more aggressive this game, constantly being found in the walls of their opponent’s base. In the last minute, the map was looking to favor TZS just slightly, but they went down three players in the final 10 seconds, giving triple dippers enough time to ink ahead for a 48.6% \- 42.1% win. + +Game two was Tower Control at Undertow Spillway, and TZS swiftly rode the tower through two checkpoints, to get to 43\. Triple dippers got to 55, and after that, the game stalled, with no more points gained until close to the end, where TZS extended their lead. In overtime, triple dippers went down three players; TZS reclaimed the tower and tied the set 1-1 with their 71-51 win. + +The next game was Rainmaker at Scorch Gorge. Triple dippers cleared the checkpoint first; TZS followed and took a narrow lead. They continued pushing, getting all the way to 16 after a series of small pushes. Unleashing specials one by one, triple dippers ran the Rainmaker down the hallway, but was stopped just short of the lead. They couldn’t put together another push in overtime, and TZS won another game, 84-82. + +The Grand Finals Reset was at set point for TZS, being a repeat of Clam Blitz at Museum d’Alfonsino. This was the same map and mode combination which TZS won the tiebreaker during Winner’s Finals, so it wasn’t looking good for triple dippers from the outset. + +It took halfway through the game for anyone to score; triple dippers took down three of TZS’s players to end up with a solid first push to 47\. TZS consistently had plenty of clams, but a lack of ink made it difficult for them to approach the other basket. By overtime, they hadn’t scored once. The chance arose when triple dippers went down two; players jumped in, and in less than three seconds, TZS took the lead, 56-53. + +GFR_clamsmuseum + +*In overtime, the score is tied 47-47; the final clam soars through the air as TZS is mid-Triple Splashdown.* + +After facing triple dippers three times across 12 games, TZS comes out as the winner of the first event in the Splatoon 3 North American League 2026\! + +*And there wasn’t even a single game of Splat Zones in all seven Grand Finals matches.* + +finalbracket + +Original Posting Date: April 7, 2026 at [Splatoon Stronghold](https://www.splatoonstronghold.com/news/na-league-2026-e1-p1) + +Written and formatted for publication by [YELLOW](https://bsky.app/profile/great-hero-yellow.bsky.social). diff --git a/content/articles/na-league-2026-p2e2.md b/content/articles/na-league-2026-p2e2.md new file mode 100644 index 000000000..5b5331df4 --- /dev/null +++ b/content/articles/na-league-2026-p2e2.md @@ -0,0 +1,122 @@ +--- +title: "NA League 2026 Event #2 (Preseason Week 2) " +date: 2026-04-14 +author: + - name: YELLOW + link: https://sendou.ink/u/great-hero-yellow +--- + +#### *Preseason Week 2 means one more chance for unsung talent to get their big break on Nintendo’s livestream* + +NAL2026-p2e2 + +On Saturday, April 11, the Splatoon 3 North American League 2026’s second and final Preseason event ran amid several other major Splatoon events, including LANs, a Global Gauntlet qualifier, and Springfest. + +Zach led the commentator cast, joined by Kbot and Falco. Operating the spectator camera was CzarDB. + +The teams who made it to the Top Cut during the Preseason Week 2 event were: +1. Xiphias +2. Acute team +3. Not For Radio +4. C4K +5. Honeybadgers +6. Wave Riders +7. InkThys +8. SmoothMoves + +The top two teams, Xiphias and Acute team, were no newcomers to the NA League. Xiphias appeared in a Top Cut stream last year, and the team was partly built with players from DPS Only, a two-time NA League 2025 Top Cut stream participant. Acute team, meanwhile, included two players from Broke Birds, last week’s third place team. + +This week’s action did see use of the Splattercolor Screen, and its audio and visual effects appeared on stream in several matches. Take caution watching replays if you are sensitive to them. + +## **Winner’s Quarterfinals: C4K vs. Honeybadgers (3–0)** + +To start the Top Cut stream, Winner’s Quarterfinals began with C4K vs. Honeybadgers, two teams new to the NA League Top Cut. + +Game one was at Hagglefish Market for Rainmaker. Neither team had a Charger, opting for choices like Heavy Edit Splatling and Dynamo Roller. C4K cleared their checkpoint first, but received a delayed wipe, enabling Honeybadgers to quickly follow up with their own checkpoint. In the final seconds, C4K took Honeybadgers down three players, and the final score was 73–58, in C4K’s favor. + +Game two, Tower Control at MakoMart, was perhaps the fastest game of Tower Control the NA League may ever see, lasting only 52 seconds. At 4:48, C4K got on the tower, and rode it without slowing down straight to the knockout, at 4:08\! + +WQF_TC_makomart + +Splat Zones at Manta Maria was the final game in the set, and it was rough for Honeybadgers. They were frequently down three players, even being caught in a delayed wipeout more than once. C4K had another 100 to 0 knockout, and took the set shutout 3–0 to advance to Winner’s Semifinals. + +## **Winner’s Semifinals: Acute team vs. Not For Radio (3–0)** + +With C4K advancing to Winner’s Semifinals to face Xiphias, on the other side of the Winner’s Bracket, Acute team was about to face Not For Radio. SmoothMoves, Honeybadgers, InkThys, and Wave Riders were all sent to the Loser’s Bracket. + +The first game went to Hagglefish Market for Splat Zones. In the tense game, Acute team was able to wipe out Not For Radio twice, but lagged behind in points for most of the match. In the last 15 seconds, Not For Radio tried to retake the zone with a Reefslider, which Acute team’s Ultra Stamp shut down, allowing them to get the lead. Acute team held their lead through overtime, winning 87–77. + +WSF_SZ_hagglefish + +*Weatherman’s Ultra Stamp flying in to meet KAGOME’s Reefslider and shut it down before it gets to the Splat Zone.* + +Up next was Tower Control at Shipshape Cargo Co., a very different story than the previous game. After 30 seconds, Acute team passed the first checkpoint, and not slowing down, they got to 15 before both teams went down three players. Another minute passed, and Acute team inched closer to victory, to 4\. They wiped out Not For Radio three times in a minute and a half, and knocked out with over a minute left. + +Rainmaker at Robo ROM-en was another strong showing from Acute team, getting all the way to 8 before 60 seconds passed. Acute team kept constant pressure on Not For Radio, either keeping them down two, three, four players, or bringing the Rainmaker into their spawn. Not For Radio had the Rainmaker in overtime, but was taken out by an Ultra Stamp toss, giving Acute team the set shutout after the 92–38 game. + +## **Winner’s Finals: Xiphias vs. Acute team (3–0)** + +Xiphias and Acute team met in Winner’s Finals already having a storyline develop throughout the day. Xiphias hadn’t lost a single game yet, including during the Ladder round of the NA League. Acute team, meanwhile, had almost the same perfect streak—their only loss was to Xiphias, in the Ladder round. + +WF_XiphiasOpener + +*Team Xiphias, from left to right: vheavy, jose, Jay, and Droid.* + +Splat Zones at Hagglefish Market for game one again\! In the first 20 seconds, Xiphias wiped out Acute team, and from there, their points started burning. Acute team blocked Xiphias’s 100 to 0 knockout by stopping them at 4\. Ultimately, they could only delay the knockout, and Xiphias won the game 30 seconds early. + +The stream saw its second Tower Control at MakoMart game, and while not as fast as the first one, still ended rather early. Acute team went down three players in the first 15 seconds, leading to Xiphias making a huge push to 45 in one go. They kept driving forward, taking Acute team down in pairs, until they knocked out again, at 2:48. + +Rainmaker at Museum d’Alfonsino was next, and Acute team has now used one of each type of Splatana: Wiper, Stamper, and Decavitator. Xiphias got the Rainmaker first, but couldn’t get the checkpoint. Acute team took the lead, but still not the check yet. Halfway through, Xiphias finally cleared the checkpoint, and carried it to 35, their furthest score. Xiphias won the game 65–40, and advanced to Grand Finals undefeated\! + +## **Loser’s Semifinals: Honeybadgers vs. C4K (1–3)** + +Well, it’s back to Honeybadgers vs. C4K, just like Winner’s Quarterfinals\! Honeybadgers had won their way up from Loser’s Top 8, 3–0’ing SmoothMoves and Not For Radio; meanwhile, C4K was coming off of a 3–0 win over InkThys in Loser’s Quarterfinals. + +At Inkblot Art Academy for Rainmaker, C4K nailed an early lead, getting to 31 before Honeybadgers got any points. By the two minute mark, Honeybadgers were wiped out as C4K had one player disconnect, and it was just their S-BLAST ‘91 left alive. C4K defended their lead in the 3v4, not losing it until the last 45 seconds; in the final second, when Honeybadgers were down 3, C4K slipped back into the lead, winning the 3v4 72–71. + +LSF_RM_inkblot + +Tower Control at Undertow Spillway was another quick knockout from C4K, smoothly sailing past their first and second checkpoint. As they cleared their third, Honeybadgers were wiped out, and seconds later, C4K won the game, at 3:40. + +Game three went to Urchin Underpass for Splat Zones, where the Splattercolor Screen was used, and its effects were on stream. C4K brought a double Triple Splashdown comp, and made full use of it to cap the zone whenever possible. Despite this, Honeybadgers took a strong lead early, and only bolstered it throughout the game, going as far as knocking out and taking their first win from C4K. + +Clam Blitz was finally up, at Scorch Gorge. From the 3-minute mark, C4K scored three times in one minute, but until the third time, it was only one Power Clam, with their score ending at 51\. Honeybadgers missed their first throw, but on the second try, scored, and took the lead, all the way to 27\! In overtime, C4K used every special at their disposal to protect jump-ins and wipe out Honeybadgers, to take the lead, 84–73. + +## **Loser’s Finals: Acute team vs. C4K (3–0)** + +Both Acute team and C4K had been sent to the Loser’s Bracket courtesy of Xiphias, and now they were set to fight over who got the rerun. This was not the first time these teams faced one another that day either, as Acute team won against C4K during the Ladder round as well. + +For the third time, game one was Splat Zones at Hagglefish Market. C4K took a big lead at the start, but before the first minute ended, Acute team wiped out C4K. This led to Acute team taking the lead, but barely a minute passed before C4K had it back thanks to a Splattercolor Screen blocking the zone. Again, Acute team wiped out C4K to get the lead one last time before knocking out close to the one minute mark. + +LF_TW_loadout + +*Each team’s Turf War comp—Acute team with a REEF-LUX MIL-K, .52 Gal, Snipewriter 5H, and Painbrush; C4K with a .52 Gal, Heavy Edit Splatling, Splash-o-matic, and Glamorz Splattershot.* + +Turf War made its first appearance on stream at Inkblot Art Academy. The teams didn’t adjust their weapons too much to accommodate the mode. Halfway through, the Danger warning showed up for C4K, and at the one minute point, both teams had only two players up. In the last seconds, C4K, with Danger back in the overhead, went down three players, sealing their loss, 35.5% to 53.1%. + +Tower Control at MakoMart, a combo which C4K had been dominant on earlier on stream, began with Acute team wiping out C4K and themselves only being down to their last player, who rode the tower through the first checkpoint while everyone respawned. The game felt like a bloodbath, with players splatted left and right, and by the end of it, Acute team proved to be the stronger team, winning 65–40. + +## **Grand Finals: Xiphias vs. Acute team (3–0)** + +The runback between Xiphias and Acute team, to conclude their storyline for Saturday’s events, had both teams sticking to weapons they were most comfortable with. Especially in Xiphias’s case, who hardly changed their weapons at all during the entire Top Cut. + +Splat Zones at Crableg Capital started with Acute team falling into a delayed wipe little more than 10 seconds in. They were able to cap the zone, stopping Xiphias at 30\. It wasn’t long before Xiphias was back in control, and before the clock reached the halfway point, Xiphias had the knockout under their belts. + +Game two went to Tower Control at Robo ROM-en. Both teams were quick to be cut in half, and shortly after, Acute team was wiped out, giving Xiphias time to clear their first checkpoint. Xiphias’s Blaster was Acute team’s biggest obstacle, always popping up to take players down in droves. Xiphias beat the clock, wiping out Acute team and driving the tower to a knockout with a handful of seconds to spare. + +Rainmaker at Scorch Gorge closed out the set; Acute team brought a .52 Gal and .96 Gal, but the Ink Vac didn’t stop them from struggling most of the game to keep Xiphias out of their base. Xiphias’s Rainmaker carrier dodged past a Kraken Royale and took it all the way to 17, and from there, the rest of the game was Acute team being denied any momentum. + +GF_RM_scorchgorge + +*Jay brings the Rainmaker as far as it can go, pincered by a Kraken Royale and two opponents.* + +Xiphias went on to win the game 86–0, and made it to the top of the podium at the Splatoon 3 North American League 2026 Preseason Week 2 event completely undefeated all day, from the Ladder round through Top Cut\! + +FinalBracket_e2p-p2 + +By the end of the Preseason event, there was still more Splatoon for everyone to enjoy, with Springfest only half over. With big action waiting for next Saturday, between the start of the NA League Regular season—where points begin to count toward Playoffs—and the Splat World Series’ First Edition Global Gauntlet Qualifier 2, April remains a strong month for the best Competitive Splatoon has to offer\! + + +Original Posting Date: April 14, 2026 at [Splatoon Stronghold](https://www.splatoonstronghold.com/news/na-league-2026-e2-p2). + +Written and formatted for publication by [YELLOW](https://bsky.app/profile/great-hero-yellow.bsky.social). diff --git a/content/articles/sws26-recap-gg1-q1.md b/content/articles/sws26-recap-gg1-q1.md new file mode 100644 index 000000000..2ce754a14 --- /dev/null +++ b/content/articles/sws26-recap-gg1-q1.md @@ -0,0 +1,184 @@ +--- +title: "SWS26 Recap: Global Gauntlet 1, Qualifier 1" +date: 2026-04-16 +author: + - name: YELLOW + link: https://sendou.ink/u/great-hero-yellow +--- + +#### *The Global Gauntlet’s first stop is Knockout City, determining the first three qualifiers in the most decisive matches back-to-back we’ve seen* + +SWS26Recap_GG1Q1 + +On Saturday, April 11, 2026, Qualifier 1 for the First Edition Global Gauntlet took place, the first of six total events in IPL and AREA CUP’s Global Gauntlet sub-series, leading up to the Splat World Series 2026\. + +With the day already packed with Splatoon, between this event, the Splatoon 3 North American League 2026, and LANs all taking place, the competitive spirit was in the air. The GG Qualifier 1 attracted 20 teams all vying for one of three available tickets to the First Edition Global Gauntlet Finals later in the month. + +Teams who didn’t make the cut this week can try again, same time and place, in the GG Qualifier 2, which will determine the West’s final three teams for the First Edition Finals. + +As a refresher on the format of the tournament, all SWS26 events will be Splat Zones-only. The Global Gauntlets are double-elimination, with qualifiers taking place during the Western Timeslot (10 AM PT / 1 PM ET / 7 PM CET), and follow a strike/select system: + +* When the set begins, both teams take turns banning two maps each. The higher seed gets to choose their two maps of choice to ban first. +* The first map that the teams play on will be randomly selected out of the pool of non-banned maps. +* After the first game, the **winning team** bans two more stages. The **losing team** selects the map for the next game. + * This continues after each game until the set ends. +* Map bans reset after the set ends and will not carry into the next set. + +With Cyren and Magic commentating all of the action, and Ely on the spectator camera, and some of the most visually-stunning production quality Competitive Splatoon has ever seen, this qualifier isn’t a tournament to skip\! + +GG1Q1_MakoMartLoadingScreen + +While the Splattercolor Screen did make an appearance in the final set, its effects were not on stream, so viewers sensitive to its audio and visual disruption can safely watch. + +## **Winner’s Round 2: fred vs. NEVER BACK DOWN NEVER WHAT (1-2)** + +The first set of the First Edition Global Gauntlet Qualifier 1 began with Winner’s Round 2, between fred and NEVER BACK DOWN NEVER WHAT. The first set would be a Best of 3, and all others would be a Best of 5\. There was a slight mishap in the first round with the strike/select system, but it was fixed for all teams once the round ended. + +Game one was at Inkblot Art Academy, after NEVER BACK DOWN NEVER WHAT banned Urchin Underpass and Brinewater Springs. Fred banned Bluefin Depot and Shipshape Cargo Co. + +One minute in, fred had the lead, but it was NEVER BACK DOWN NEVER WHAT who was taking points free of penalty. The two teams had a stalemate around a score of 35 (fred) \- 59 (NEVER BACK DOWN NEVER WHAT), but a crucial double from fred’s E-liter 4K Scope taking the opponent down to their last player sealed the knockout victory for fred, with a minute and a half remaining. + +Game two was at Mahi-Mahi Resort, after fred banned Flounder Heights and Undertow Spillway. + +WR2_strikeselect + +The match began with NEVER BACK DOWN NEVER WHAT taking the zone, and already one minute in, they were just 34 ticks from winning, still in charge. Fred hardly had a chance to even neutralize the zone; by 3:40, NEVER BACK DOWN NEVER WHAT had gone from 100 to 0, for a fast knockout win. + +Game three was at Barnacle & Dime, after fred banned Robo ROM-en and Crableg Capital. It was set point for both teams now—whoever won would be advancing\! + +The zone flipped possession a few times in the first minute, with each team having the lead for a brief moment. Fred was extending their lead, getting all the way to 38 before being stopped. From 83 and 14 penalty points, NEVER BACK DOWN NEVER WHAT rose to the occasion and went all the way to the KO. The team collectively Booyah’d as the zone reached zero, with two and a half minutes to spare. + +The first set ended in an upset, with seed 13 sending seed 5 to the Loser’s Bracket\! + +## **Winner’s Quarterfinals: FTWin vs. healthy diet food groups (3-0)** + +With the bracket shifting to a Best of 5, FTWin would be set to take on healthy diet food groups—two well-known names in the tournament scene, with FTWin returning after their hiatus from the 2025 North American League. + +Game one went to Eeltail Alley, with FTWin’s map bans being Lemuria Hub and Mincemeat Metalworks, and healthy diet food groups’ bans being Mahi-Mahi Resort and Urchin Underpass. + +It took nearly 30 seconds for the zone to be capped, but FTWin took point first. A minute into the game, \[K\]yo got a double and popped aura as healthy diet food groups suffered a wipeout. Less than a minute later, FTWin knocked out, having gone 100 to 0\. + +Game two went to Museum d’Alfonsino, after FTWin banned Hammerhead Bridge and Sturgeon Shipyard. + +FTWin claimed the zone almost immediately, kickstarting the intense matchup between teams. FTWin went down two players, giving healthy diet food groups the chance to take the zone, stopping them at 45\. Shortly after, healthy diet food groups took the lead, until eventually they went down three players and FTWin took the zone. + +WQF_museum + +*Down three teammates, Zyf holds the zone in neutral against FTWin long enough for healthy diet food groups to respawn and take the zone.* + +FTWin did reclaim the zone, and with one minute remaining, they had the lead again. But they kept going down multiple players, which healthy diet food groups capitalized on to get the zone. Their points burned fast; in the final seconds, teams fought over a neutral zone. The clock ran out, and FTWin emerged as the victors, 87-78. + +Game three went to MakoMart, after FTWin banned Um’ami Ruins and Scorch Gorge. + +Healthy diet food groups went for the zone first, having it for a short time before FTWin took it. A minute and a half in, FTWin wiped out their opponent and extended their lead. Next, both teams inched their points forward, precariously taking small leads from one another. + +First FTWin was stopped at 35\. Healthy diet food groups took the lead—FTWin stopped them at 7 points remaining. Then FTWin held the zone long enough to get to 3, before healthy diet food groups applied a penalty to them. FTWin eventually made their way to a knockout, with less than half a minute remaining. + +## **Winner’s Semifinals Part 1: FTWin vs. NEVER BACK DOWN NEVER WHAT (3-0)** + +Both Winner’s Semifinals sets would be on stream, as the winner of each set would earn their ticket to the 1st Edition Global Gauntlet Finals. The first ticket would go to the winner between FTWin and NEVER BACK DOWN NEVER WHAT, each having their second time on stream\! + +Game one went to Inkblot Art Academy again; FTWin banned Urchin Underpass and Sturgeon Shipyard, while NEVER BACK DOWN NEVER WHAT banned MakoMart and Robo ROM-en. + +FTWin had some interesting choices with their comp, bringing a Luna Blaster Neo and double Wave Breaker. NEVER BACK DOWN NEVER WHAT was able to get halfway through their objective before FTWin locked in and took the zone for the first time. FTWin’s paint control and constant Wave Breakers led them to the knockout with 50 seconds left on the clock. + +Game two was at Mahi-Mahi Resort, after FTWin banned Um’ami Ruins and Manta Maria. + +The match slowly snowballed for FTWin, really gaining momentum after the first minute when NEVER BACK DOWN NEVER WHAT went down three players. FTWin was able to lock their opponent out and get a knockout, with over two and a half minutes to spare. + +Game three was at Brinewater Springs, after FTWin banned Undertow Spillway and Hammerhead Bridge. Again, FTWin brought a comp that made the commentators double-take, having Custom Blaster and Custom E-liter 4K, for double Kraken Royale. + +FTWin was ambitious, taking the zone and moving right up to their opponent’s spawn. The lockout crumbled, and NEVER BACK DOWN NEVER WHAT halted FTWin’s advance at 33\. While NEVER BACK DOWN NEVER WHAT occupied the zone and FTWin was down three, azealia (Sam) snuck into the opponent’s spawn and popped the Kraken Royale, safely offering a jump to \[K\]yo for the team to pincer and retake zone. + +WSF_brinewater + +*FTWin’s pincer, with \[K\]yo and Sam in the opponent’s spawn area.* + +The teams continued to fight until their penalties were nearly gone, before the zone flipped and reapplied points. With 1:25 remaining, FTWin wiped out NEVER GIVE UP NEVER WHAT, took the lead, and came close to knocking out, winning 94-66 and taking the first seat to the First Edition Global Gauntlet Finals\! + +## **Winner’s Semifinals Part 2: hi vs. ezmd (1-3)** + +The second ticket to the first GG Finals would go to the winner between hi and ezmd; the loser of the set would have one last chance to try to earn their spot by winning in Loser’s Semifinals. + +Game one went to Marlin Airport after ezmd banned Humpback Pump Track and Urchin Underpass, while hi banned Lemuria Hub and Crableg Capital. The first start of the game saw a disconnect, but was quickly back in action after a reset. + +Ezmd started strong the first minute; even after going down three players, Ant kept the dream alive and stalled hi long enough that even at the numbers disadvantage, ezmd didn’t lose the zone. Ezmd was stopped at 7, then once more at 5, but in a twist, all the way from 34 and a handful of penalty points, hi swept in for the knockout. + +Game two went to Barnacle & Dime; hi’s banned stages were Hammerhead Bridge and Um’ami Ruins. + +Both teams traded multiple times over the course of the match: splats, zone, leads, specials. Halfway through, hi stopped ezmd at 5 using a critical Ink Vac to take away a Booyah Bomb and any ink ezmd threw at the zone. Ezmd came back with a wipeout on hi, and as the objective ticked down, Devin4K on the E-liter 4K got a double right before they knocked out. + +Game three went to Sturgeon Shipyard after ezmd banned Flounder Heights and Manta Maria. + +Hi had the zone first and held it for over a minute, but going down three let ezmd take possession, and shortly after, the lead. The teams clustered together at both ends of the wide zone, throwing special after special to stop one another. Amid the chaos, the game ended a little after two minutes had passed, with ezmd walking out as the winner. + +WSF_Sturgeon + +*Ezmd’s Booyah Bomb targets hi’s Crab Tank in the corner, while their Killer Wail 5.1 and Ultra Stamp (chrome) target the Booyah Bomb.* + +Game four’s map was Scorch Gorge, after ezmd banned Inkblot Art Academy and Undertow Spillway, seemingly favoring to ban the double-zone maps, as none were left in the pool after Undertow Spillway was out. + +Ezmd was quick to take the zone, and quicker to go down 3, giving hi the lead 30 seconds in. The large zone with obtrusive center pillar made keeping the zone capped difficult; after a 34-second neutral state, ezmd claimed it and regained the lead. The Charger players shined in this match, with the PoV capturing a handful of excellent shots. Despite being wiped out with 47 seconds left, ezmd clutched the win, 87-83. + +The second seat at the first Global Gauntlet Finals belonged to ezmd\! One spot remains to be claimed in this week’s qualifying event\! + +## **Loser’s Semifinals: Azure vs. hi (3-2)** + +The stage to earn the last ticket to the 1st Edition Global Gauntlet Finals in this qualifier went to perhaps the most deserving storyline, Azure vs. hi. Not on stream, the two met earlier in Winner’s Round 3, where the winner, hi, was decided in a game 5\. The two sets between these two were the only sets in the entire event that went as far as a game 5\. + +Game one was set at Scorch Gorge for the second time in a row; Azure banned Flounder Heights and Humpback Pump Track, while hi banned Crableg Capital and Robo ROM-en, despite the latter having previously won against Azure on Robo ROM-en. + +Azure took the first extended lead when hi went down two players in the first minute. As soon as Volty made it into hi’s side of the map, the game was hers; immediately getting two-for-one at a ledge, followed by a third for a triple and aura. Looping back around to the zone, Volty sniped another with a Trizooka, and caught an Ultra Stamp below a ledge, bringing Azure to the first knockout of the set after a minute and a half. + +LF_scorch + +*Volty’s two-for-one splat on hi’s Zink Mini Splatling and Snipewriter 5H.* + +Game two was at Sturgeon Shipyard; Azure banned Lemuria Hub and Museum d’Alfonsino. In Winner’s Round 3, these teams played on Sturgeon Shipyard, where hi took the win. + +The game went nearly as fast as the previous one, only in the other direction. Just 20 seconds in, Azure was down three. Hi took the zone and tried locking out, but was unsuccessful. Both teams ended up down three while hi’s points ticked down—Azure was wiped out, and hi tied the set after a knockout just after two minutes in. + +Game three went to Brinewater Springs. Hi banned Manta Maria and Um’ami Ruins, likely due to losing to Azure at Um’ami Ruins in their last set. + +Azure took the zone in a swift 10 seconds. After some lead shuffling, Azure was once again ahead, finally stopped at 29 after hi disrupted their flow with a Big Bubbler. While hi was down three, Azure, despite being down two themselves, pressed forward to lock out, and with about two and a half minutes left, Azure took the knockout. + +Game four went to Marlin Airport. Azure banned Hagglefish Market and Shipshape Cargo Co., crushing Cyren’s dream of getting to commentate a set on Hagglefish Market during this event. + +Not even 20 seconds in, hi wiped Azure and were locking them out; by the one minute mark, they had the zone down to 30\. While Azure managed to cap the zone, they struggled to maintain control for long and suffered from a lack of paint on the ground. Hi knocked out, bringing these two teams back to another game five set\! + +Game five, to determine the third qualifier for the GG Finals, was at… Bluefin Depot. In a burst of sarcastic commentator’s cursing (or perhaps clairvoyance?), Cyren and Magic predicted hi’s map bans, being Mahi-Mahi Resort and MakoMart. + +Azure had the first lead in the game, able to get it down to 48\. Even after being down three players twice, they regrouped well enough to prevent hi from taking the lead from them during their vulnerability. Hi did eventually take the lead, by just two points, but 30 seconds later, Azure was ahead, and after the two-minute mark, had their hard-fought victory, once more in a knockout\! + +After ten games across two sets, Azure rose from their Loser’s Bracket run to overtake hi and claim the final ticket from the qualifier to the First Edition Global Gauntlet Finals\! + +GG1Q1_finalbracket + +*Winner’s Finals, Loser’s Finals, and Grand Finals were not played in this event, as the top 3 teams were already decided and earned their place at the GG1 Finals.* + +The event attracted several of the West’s best talent, and the outcome shows it. On stream, the overwhelming majority of matches ended in a knockout, with only three not wrapping up early, and even in those games, the scores were close, and none of them incurred overtime. + +With the entire event featuring map bans, what were the event’s favorite maps to throw out? Urchin Underpass was the most banned map (20 strikes), with Flounder Heights (19 strikes) and Crableg Capital (17 strikes) taking second and third place. The top three selected stages were Hagglefish Market (8 selects), Undertow Spillway (5 selects), and Museum d’Alfonsino/Sturgeon Shipyard/MakoMart (3 selects each). + +### **Who Has Qualified So Far?** + +Three and a half hours of intense Splatoon gameplay later, and the West has its first three contenders for the First Edition Global Gauntlet Finals on April 24, 2026: + +1. FTWin +2. ezmd +3. Azure + +On Japan’s side, AREA CUP will be selecting teams to participate in the Global Gauntlet Finals. So far, three of them have been revealed: + +1. Zest +2. Utopia +3. 07 Quartet + +Last year, both FTWin and Utopia participated in the premiere Splat World Series tournament and will be returning faces, at least in full by FTWin—Utopia’s roster has changed slightly, replacing Kyamyi and Niru with Inaten and Kasiwa. Samurai Kasato, captain of 07 Quartet, also competed in SWS 2025 as part of Samurai Cat, the second-place team. + +On April 18th, the action returns for a second qualifier to determine the West’s final three teams who will go head-to-head against Japan’s handful of chosen teams. Which teams will we see come out on top? What new surprises lie in store? Make sure you’re there to see the spectacle\! + + +Original Posting Date: April 16, 2026 at [Splatoon Stronghold](https://www.splatoonstronghold.com/news/sws26-recap-gg1-q1). + +Written and formatted for publication by [YELLOW](https://bsky.app/profile/great-hero-yellow.bsky.social). diff --git a/content/articles/sws26-recap-gg1-q2.md b/content/articles/sws26-recap-gg1-q2.md new file mode 100644 index 000000000..2b8e4a034 --- /dev/null +++ b/content/articles/sws26-recap-gg1-q2.md @@ -0,0 +1,150 @@ +--- +title: "NA League 2026 Event #3 (Regular Season Week 1) " +date: 2026-04-21 +author: + - name: YELLOW + link: https://sendou.ink/u/great-hero-yellow +--- + +#### *The first week of the Regular Season was so intense that by the end of it, you forget that it’s not the Playoffs yet.* + +NAL2026-r1e3 + +On Saturday, April 18, 2026, the Splatoon 3 North American League began its Regular Season. The training wheels were gone, and now wins started counting, earning points to qualify for the Playoffs in June. Teams only need to play in four weeks to be eligible for the Playoffs, but needed to ensure a high amount of points to make the top eight. + +Nine, Falco, and Nox led the commentator panel, and Power was operating the spectator camera. + +Despite the day being filled with competing events like the Global Gauntlet Qualifier 2, LUTI Playoffs, and two LANs in Sunset Surge and Don’t Flounder at the Function, big names in the North American scene showed up to throw down: + +1. Milky Way +2. SIZA +3. Content Cat +4. usagi fanclub +5. NME +6. Moonlight +7. Hypernova +8. lfn + +The Splattercolor Screen was used in Loser’s Finals, and its effects were briefly on screen. As we dive into the details, if you plan on rewatching, take caution if you are sensitive to its effects. + +## **Winner’s Quarterfinals: Content Cat vs. Moonlight (1–3)** + +While Content Cat is a new team to the NA League, its players aren’t, with most having appeared in the Top Cut stream in last year’s Regular Season. And Moonlight needs no introduction by now; you know them, you love them\! + +Game one, Tower Control at MakoMart; it took nearly half of the game before anyone cleared a single checkpoint. Once Moonlight started rolling, they didn’t stop, giving Content Cat no points and taking the knockout. + +Game two, Clam Blitz at Urchin Underpass, notorious for its difficulty to score, saw Moonlight opening the basket within 30 seconds. Held up in mid, Content Cat had to backtrack once Moonlight snuck in and scored again. Content Cat got points on the board, but after overtime, lost 52–86. + +Turf War at Scorch Gorge was next, and it only saw three splats in the first 60 seconds. It took until the latter half for things to pick up, with Content Cat losing players and seeing the Danger\! warning. At the one minute pan out, the map looked favorable to Content Cat, and true to appearances, they won, 53.8% – 44.2%. + +WQF_TWscorch + +*An even 98.0% total ink coverage—can you spot the 2% left uninked?* + +Already, the 2026 NA League was setting new records, as the total ink coverage percent from this game, 98%, beat the 2025 NA League’s record of 97.3%, set by Gourmet Race vs. BEt in last year’s Event \#10\! + +Splat Zones at Mahi-Mahi Resort saw Content Cat take the first lead when both teams went down two players. Moonlight had the lead when the first minute ended, never giving it up and stopping Content Cat when they were one point from taking it. Moonlight knocked out at the halfway point, sending Content Cat to the Loser’s Bracket. + +## **Winner’s Semifinals: Milky Way vs. NME (3–1)** + +Advancing to Winner’s Semifinals were Moonlight, Hypernova, Milky Way, and NME, which is a team three parts fofofo (last NA League’s \#3 team). Content Cat, SIZA, usagi fanclub, and lfn were in the Loser’s Bracket. + +The set began on Undertow Spillway for Splat Zones. NME struggled to keep players up, almost always down at least one. They still gave Milky Way a tough fight, keeping the zone neutral to stall the points gain. In overtime, NME was ticking close to Milky Way’s lead, but was wiped out, losing 94–97. + +Tower Control at Robo ROM-en was second, another difficult game for NME. Within the first two minutes, Milky Way’s push ended at just 4 points to knockout. NME was able to get some points to their name, but Milky Way’s lead persevered, 96–7. + +Rainmaker at Mahi-Mahi Resort would become NME’s bread and butter—Milky Way cleared their first checkpoint in 30 seconds, but NME retaliated by taking them down three players, dunking the checkpoint, taking Milky Way down in a delayed wipe, and knocking out just after a minute into the game\! + +WSF_RMmahi-mahi + +*What could have been a sub-minute knockout, if only the pedestal had been inked\!* + +Game four went to Turf War at Crableg Capital. At the start, Milky Way didn’t paint their base much, but it was NME who had the Danger\! indicator sooner. Milky Way had their turn with it as Now or Never started to play, and were even down three players in the last 20 seconds, but still walked out 55.3% – 40.8%, headed to Winner’s Finals. + +## **Winner’s Finals: Milky Way vs. Moonlight (3–2)** + +Those who paid attention to last year’s NA League Playoffs will recognize Milky Way vs. Moonlight as the Grand Finals set. The two teams have historically clashed multiple times, and the hosts were stoked to commentate it so early in the Regular Season. + +Splat Zones at Mahi-Mahi Resort saw Moonlight take a strong offense, getting the zone in 12 seconds and Milky Way down two players. They were stopped at 37, and Milky Way inched ahead, but Moonlight stopped them at 26\. Painting through Triple Inkstrikes and a Booyah Bomb, Moonlight took the lead and knocked out at 2:24. + +Turf War at MakoMart didn’t bring out anything unexpected from these teams; both had a REEF-LUX 450, both had a Snipewriter 5H. Danger\! popped up in Moonlight’s overhead a few times, critically in the last minute, and they lost 39.4% – 56.6%. + +Game three was Tower Control at Inkblot Art Academy. Moonlight moved the tower one point, then Milky Way, and they were pushed back to 100\. Moonlight cleared the first checkpoint before Milky Way, but 20 seconds later, Milky Way had the lead; soon after, more checkpoints, and before three minutes had passed, the knockout. + +Clam Blitz at Crableg Capital saw no points gained until the last 40 seconds, where Milky Way scored just one Power Clam. Moonlight ran in and scored in the final six seconds, took the lead, and in overtime, a jump in sealed the basket and the win, 44–20. + +WF_CBcrableg + +*SSNolan with the game-winning play, getting the wipeout, providing jump-ins, and grabbing the last clam needed to close the basket.* + +Game five went to Rainmaker at Museum d’Alfonsino. Moonlight entered with a double Ink Vac comp, and it truly felt like there was one online at all times. At the halfway point, with both teams down two, Moonlight took the lead, but needed another 30 seconds to clear the first checkpoint. + +Milky Way only needed 25 seconds to get their first checkpoint from there, and shortly after, took the lead to 18\. Approaching overtime, things looked good for Moonlight with Milky Way down 3, but trapped between an enemy and a Suction Bomb, they fell short of the lead, 62–82, and were sent to the Loser’s Bracket. + +## **Loser’s Semifinals: Hypernova vs. NME (2–3)** + +After both Hypernova and NME went 3–1 over their opponents in Loser’s Quarterfinals, they squared up to decide who advanced to fight Moonlight in Loser’s Finals. + +Game one was Tower Control at Museum d’Alfonsino. NME went all the way to 16 in a minute and a half. It took Hypernova two minutes, but they got the lead and third checkpoint, then jumped out to play defense for the last minute. In the final 40 seconds, they ended up wiped out twice, and NME beat the clock for a knockout. + +For Rainmaker at Crableg Capital, NME brought the uncommon Ballpoint Splatling Nouveau for Ink Vac coverage. NME was at a numbers disadvantage frequently, but Hypernova played cautiously and hesitated to move forward. They held the lead at 9, and NME crept closer, to 14, but two wipeouts in the last 20 seconds kept them at bay, and Hypernova kept the win, 91–86. + +LSF_RMcrableg + +*In the background, Synapse trades splats with DATKID to get a second wipeout over NME, six seconds after the previous one.* + +Game three was Clam Blitz at Scorch Gorge. Across two pushes in the first half of the game, NME brought their score down to 27\. Hypernova’s first push got them to 54, but they were caught in a delayed wipeout. At the one minute mark, NME scored again, wiped out Hypernova, and knocked out. + +Shipshape Cargo Co. was chosen for Turf War—the Splattercolor Screen was used in this game, so be careful watching replays. The game was solidly in Hypernova’s hands for a majority of the three minutes, taking NME down two, three players multiple times. By the final minute, they had set up a lockout, keeping NME contained in 1/3rd of the map. + +Hypernova won 63.6% – 33.7%, tying with the 2025 NA League’s total ink coverage percentage record, 97.3%\! + +A game five Splat Zones is rare, and unexpected at Undertow Spillway. Early in the game, NME set the goal to reach at 34\. The teams traded wipeouts as Hypernova fought to keep up with NME’s score. In the last five seconds, Hypernova wiped out NME, and in overtime, kept them down three. Just one tick to tie, the zone was neutralized. Hypernova was wiped out, and NME took the win, 88–87. + +## **Loser’s Finals: Moonlight vs. NME (3–1)** + +A brief respite from game five sets, Moonlight and NME would face off to determine who got their runback against Milky Way at Grand Finals. + +The set started with Tower Control at MakoMart, and already 40 seconds in, Moonlight suffered a delayed wipeout and NME was down two players. It took until the halfway point for a checkpoint to be cleared, which was Moonlight’s, but they didn’t get much further after half of their team was splatted. NME took the game into overtime, but lost with a wipeout, and their score was 7–55. + +The most popular Turf War stage from 2025’s NA League, Urchin Underpass, was back, and one minute in, Moonlight was the team with Danger\! in their overhead. NME went down two, but Moonlight was still behind in score. In the last ten seconds, NME was again down two, then three, and Moonlight earned their comeback, 53.8% – 39.8%. + +Next, NME brought Moonlight to their turf, Rainmaker at Mahi-Mahi Resort. Running a double-Ultra Stamp comp, NME replayed the same story as before: their opponent got the first checkpoint, but it was NME going directly from their first checkpoint right into the knockout, leaving two minutes and 19 seconds to spare. + +LF_RMmahi-mahi + +*Pedestal painted this time, 3z is able to squid roll to the win, protected by an Ultra Stamp.* + +Game four was Splat Zones at Crableg Capital. The opening favored NME, overtaking Moonlight’s score of 34, and Moonlight had to use an Ink Vac and Big Bubbler to stop NME at 4, before the knockout. Moonlight rebounded, wiping out NME a minute later, and in the last seconds, Omega got a triple, securing their knockout and set win. + +## **Grand Finals: Milky Way vs. Moonlight (3–2)** + +Back to knocking on Milky Way’s door, Moonlight was ready to take on their biggest opponent and deepen the storyline between the \#1 and \#2 teams from the 2025 NA League. + +Repeating game one from Winner’s Finals, Splat Zones at Mahi-Mahi Resort, the game could be neatly divided into one-minute segments: at 4:11, Milky Way took the lead. At 3:11, Moonlight took the lead. At 2:11, Milky Way was back in the lead. Fast forward to 0:39, Moonlight was ahead once again, and the game went into overtime. Just as Milky Way was about to tie, Moonlight’s Ink Vac protected the zone, ending the game 92–91. + +Repeating game two, Turf War at MakoMart, the first minute was advantageous for Moonlight, with only one player being splatted on their side. Overall, splats were few in the game until the end. Milky Way threw out Triple Inkstrikes at the last second, which was the most crucial play, giving them just enough ink to eke ahead, 49.4% – 47.4%. + +Game three was Tower Control again, but at Crableg Capital rather than Inkblot Art Academy. The result was still the same, as Milky Way rode the tower through checkpoints one, two, and three with speed, knocking out by the three minute mark. + +Interestingly, game four was Clam Blitz… at Shipshape Cargo Co. But it worked out for Moonlight, scoring in the first 40 seconds. At the halfway point, as Milky Way was throwing clams to open Moonlight’s basket, Moonlight beat them to it, forcing Milky Way to turn around. Moonlight was wiped out for this, but they had the lead at 58\. + +GF_CBshipshape + +*Milky Way nowhere to be seen near their basket, instead set up by Moonlight’s basket in anticipation of scoring first.* + +Once the basket closed, Milky Way was already set up and immediately scored. However, they only scored to 60\. Moonlight scored again as overtime began, ending it as soon as it started, and won the game 42–40, and tied the set again. + +Game five again. Rainmaker at Museum d’Alfonsino again. Moonlight had a double Ink Vac comp again. Moonlight cleared their checkpoint, and went to extend their lead, but the Rainmaker reset faster. Milky Way baited out an Ink Vac; taking Moonlight down two, they claimed the lead at 47\. Moonlight lost the Rainmaker in the final seconds and couldn’t pop the shield in time, and without overtime, their run ended short, 42–57. + +Moonlight sitting at second place to Milky Way after two down-to-the-wire game five sets is building a strong storyline so early in the Regular Season, and as more teams end up in Top Cut, who else will enrich the narrative? When will the newly-revitalized FTWin make their entrance? + +With no 3–0 sets on stream, in a strangely Hagglefish Market-less event, the first week of the Regular Season has set a high standard for successive weeks to live up to. + +Make sure you’re there to see it\! + +R1E3_finalbracket + +Original Posting Date: April 21, 2026 at [Splatoon Stronghold](https://www.splatoonstronghold.com/news/na-league-2026-e3-rs1). + +Written and formatted for publication by [YELLOW](https://bsky.app/profile/great-hero-yellow.bsky.social). diff --git a/db-test.sqlite3 b/db-test.sqlite3 index 910c5f0b2..75ffffbc7 100644 Binary files a/db-test.sqlite3 and b/db-test.sqlite3 differ diff --git a/desktop-bracket.png b/desktop-bracket.png new file mode 100644 index 000000000..97e74bec6 Binary files /dev/null and b/desktop-bracket.png differ diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index 6dcaa4be6..c02540948 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -50,10 +50,12 @@ sendou.ink/ │ ├── modules/ -- "node_modules but part of the app" │ ├── styles/ -- Global .css files │ ├── utils/ -- Helper functions grouped by domain used by many features -│ ├── entry.client.tsx -- Client entry point (Remix concept) -│ ├── entry.server.tsx -- Server entry point (Remix concept) +│ ├── entry.client.tsx -- Client entry point (React Router concept) +│ ├── entry.server.tsx -- Server entry point (React Router concept) +│ ├── form/ -- Form helpers shared across features │ ├── root.tsx -- Basic HTML structure, React context providers & root data loader -│ └── routes.ts -- Route manifest +│ ├── routes.ts -- Route manifest +│ └── routines/ -- Cron job definitions (see "Routines" below) ├── content/ -- Markdown files containing articles ├── docs/ -- Documentation to developers and users ├── e2e/ -- Playwright tests @@ -72,12 +74,12 @@ You should aim to colocate code that "changes together" as much as possible. Fea ### Feature folder files & folders -- **actions/**: Remix actions per route +- **actions/**: React Router actions per route - **components/**: React components - **core/**: "Core logic" meaning modules (see below) or other logic that is not typically rendering components or calling database - **queries/**: (deprecated) Database queries, should use repository instead -- **loaders/**: Remix loaders per route -- **routes/**: Remix actions per route +- **loaders/**: React Router loaders per route +- **routes/**: React Router route files (re-export the action/loader & default export the route component) - **FeatureRepository.server.ts**: Database queries & mappers - **feature-constants.ts**: Constant values - **feature-hooks**: React hooks @@ -177,13 +179,13 @@ Check `database-relations.md` for more information about the database relations. ### State management -We are not using a state management library such as Redux. Instead use React Context for the few global state needed and Remix's data loading hooks to share the state loaded from server. See also "Search params" section below. +We are not using a state management library such as Redux. Instead use React Context for the few global state needed and React Router's data loading hooks to share the state loaded from server. See also "Search params" section below. ### Search params Often it's convenient to store state in search params. This allows for nice features like users to deep link to the view they are seeing. You have two options to achieve this: -1) Use Remix's built-in solution. Use this if data loaders should rerun once search params are changed. +1) Use React Router's built-in solution. Use this if data loaders should rerun once search params are changed. 2) `useSearchParamState` hook. Use this if it is not needed. ### Routines @@ -192,7 +194,7 @@ Cron jobs to perform actions on the server at certain intervals. To add a new on ### Real time -Webhooks via Skalop service (see logic in the Chat module). In short an action file can send an update via the `ChatSystemMessage` module: +Webhooks via the Skalop service (see logic in the Chat module). In short an action file can send an update via the `ChatSystemMessage` module: ```ts ChatSystemMessage.send([ @@ -204,19 +206,22 @@ ChatSystemMessage.send([ ]); ``` -which then can be listed to in a React component via the `useChat` hook: +#### Global chat -```tsx -const chatRooms = React.useMemo( - () => [ - { code: `tournament__${tournament.id}`, label: "Tournament" }, - ], - [tournament.ctx.id], -); -useChat({ - rooms: chatRooms, - connected: !tournament.ctx.isFinalized, -}); +There is a single `ChatProvider` mounted near the root that owns the WebSocket connection for the whole app. + +Two things drive which rooms the provider cares about: + +1) **Server-managed participant rooms.** Rooms where the user is a `participantUserId` in the room metadata are pushed to the client by Skalop automatically on connect (and via `ROOM_JOINED` / `ROOM_REMOVED` events). These are the rooms that show up in the chat list regardless of which page the user is on (e.g. a SendouQ group chat, a tournament match chat). +2) **Route-exposed `chatCode`.** A loader can expose a `chatCode` (string or `string[]`) in its returned data. The provider reads this out of `useMatches()` and subscribes to that room for the duration of the route being active — used for rooms the user is viewing but not necessarily a participant of (e.g. a tournament match chat viewed by a TO). + +Example loader: + +```ts +return { + // ...other loader data + chatCode: match.chatCode, +}; ``` ### Notifications diff --git a/e2e/builds.spec.ts b/e2e/builds.spec.ts index 8d8f21cbc..4b8662188 100644 --- a/e2e/builds.spec.ts +++ b/e2e/builds.spec.ts @@ -1,8 +1,9 @@ -import type { Page } from "@playwright/test"; +import type { Locator, Page } from "@playwright/test"; import { NZAP_TEST_DISCORD_ID, NZAP_TEST_ID } from "~/db/seed/constants"; import type { GearType } from "~/db/tables"; import { ADMIN_DISCORD_ID } from "~/features/admin/admin-constants"; import { newBuildBaseSchema } from "~/features/user-page/user-page-schemas"; +import invariant from "~/utils/invariant"; import { expect, impersonate, navigate, seed, test } from "~/utils/playwright"; import { createFormHelpers } from "~/utils/playwright-form"; import { BUILDS_PAGE, userBuildsPage, userNewBuildPage } from "~/utils/urls"; @@ -69,6 +70,10 @@ test.describe("Builds", () => { url: userBuildsPage({ discordId: ADMIN_DISCORD_ID }), }); + const buildIdBefore = await buildIdFromEditLink( + page.getByTestId("edit-build").first(), + ); + await page.getByTestId("edit-build").first().click(); const form = createFormHelpers(page, newBuildBaseSchema); @@ -83,6 +88,11 @@ test.describe("Builds", () => { "Private", ); + const buildIdAfter = await buildIdFromEditLink( + page.getByTestId("edit-build").first(), + ); + expect(buildIdAfter).toBe(buildIdBefore); + await impersonate(page, NZAP_TEST_ID); await navigate({ page, @@ -157,3 +167,11 @@ async function selectGear({ .getByTestId(`gear-select-option-${name}`) .click(); } + +async function buildIdFromEditLink(locator: Locator) { + const href = await locator.getAttribute("href"); + invariant(href, "edit-build link missing href"); + const match = href.match(/buildId=(\d+)/); + invariant(match, `buildId not found in href: ${href}`); + return Number(match[1]); +} diff --git a/e2e/seeds/db-seed-AB_RR.sqlite3 b/e2e/seeds/db-seed-AB_RR.sqlite3 new file mode 100644 index 000000000..ef8a147d7 Binary files /dev/null and b/e2e/seeds/db-seed-AB_RR.sqlite3 differ diff --git a/e2e/seeds/db-seed-DEFAULT.sqlite3 b/e2e/seeds/db-seed-DEFAULT.sqlite3 index a7c61ba5d..84e45b86b 100644 Binary files a/e2e/seeds/db-seed-DEFAULT.sqlite3 and b/e2e/seeds/db-seed-DEFAULT.sqlite3 differ diff --git a/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 b/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 index d4f0b96bc..cbf1fbcc4 100644 Binary files a/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 and b/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 b/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 index acbea5962..e200e2f47 100644 Binary files a/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 and b/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 b/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 index 6aa8c7ea8..2aa80b740 100644 Binary files a/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 and b/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 b/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 index b5110d850..52c27d975 100644 Binary files a/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 and b/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 b/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 index ebc2defa1..211fbe48b 100644 Binary files a/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 and b/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 differ diff --git a/e2e/seeds/db-seed-REG_OPEN.sqlite3 b/e2e/seeds/db-seed-REG_OPEN.sqlite3 index 956204816..92388cb5f 100644 Binary files a/e2e/seeds/db-seed-REG_OPEN.sqlite3 and b/e2e/seeds/db-seed-REG_OPEN.sqlite3 differ diff --git a/e2e/seeds/db-seed-SMALL_SOS.sqlite3 b/e2e/seeds/db-seed-SMALL_SOS.sqlite3 index a40a1ca4d..fdaf1c892 100644 Binary files a/e2e/seeds/db-seed-SMALL_SOS.sqlite3 and b/e2e/seeds/db-seed-SMALL_SOS.sqlite3 differ diff --git a/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 b/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 index 7f108342b..79ca1fe6b 100644 Binary files a/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 and b/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 differ diff --git a/e2e/tournament-ab-divisions.spec.ts b/e2e/tournament-ab-divisions.spec.ts new file mode 100644 index 000000000..130f27593 --- /dev/null +++ b/e2e/tournament-ab-divisions.spec.ts @@ -0,0 +1,68 @@ +import { + expect, + impersonate, + navigate, + seed, + submit, + test, +} from "~/utils/playwright"; +import { tournamentBracketsPage } from "~/utils/urls"; + +const AB_RR_TOURNAMENT_ID = 8; +const TEAMS_PER_DIVISION = 6; + +test.describe("Tournament A/B divisions", () => { + test("assigns 6A/6B, starts bracket, renders 36 matches across 6 rounds and two standings tables", async ({ + page, + }) => { + test.slow(); + + await seed(page, "AB_RR"); + await impersonate(page); + + await navigate({ + page, + url: `/to/${AB_RR_TOURNAMENT_ID}/seeds`, + }); + + await page.getByTestId("set-ab-divisions").click(); + + const divisionRadioGroups = page.getByTestId("ab-division-radio-group"); + await expect(divisionRadioGroups).toHaveCount(TEAMS_PER_DIVISION * 2); + + for (let i = 0; i < TEAMS_PER_DIVISION; i++) { + await divisionRadioGroups.nth(i).getByText("A", { exact: true }).click(); + } + for (let i = TEAMS_PER_DIVISION; i < TEAMS_PER_DIVISION * 2; i++) { + await divisionRadioGroups.nth(i).getByText("B", { exact: true }).click(); + } + + await submit(page, "set-ab-divisions-submit-button"); + + await navigate({ + page, + url: tournamentBracketsPage({ tournamentId: AB_RR_TOURNAMENT_ID }), + }); + + await page.getByTestId("finalize-bracket-button").click(); + await submit(page, "confirm-finalize-bracket-button"); + + await expect(page.getByTestId("brackets-viewer")).toBeVisible(); + + await expect(page.locator("[data-match-id]")).toHaveCount( + TEAMS_PER_DIVISION * TEAMS_PER_DIVISION, + ); + + for ( + let roundNumber = 1; + roundNumber <= TEAMS_PER_DIVISION; + roundNumber++ + ) { + await expect( + page.getByText(`Round ${roundNumber}`, { exact: true }).first(), + ).toBeVisible(); + } + + await expect(page.getByTestId("rr-standings-table")).toHaveCount(2); + }); +}); diff --git a/e2e/tournament-bracket.spec.ts b/e2e/tournament-bracket.spec.ts index 8d7069d18..4e8facf19 100644 --- a/e2e/tournament-bracket.spec.ts +++ b/e2e/tournament-bracket.spec.ts @@ -480,6 +480,7 @@ test.describe("Tournament bracket", () => { test("shows tournament results on user profile after finalized tournament", async ({ page, }) => { + test.slow(); const tournamentId = 4; await seed(page, "SMALL_SOS"); diff --git a/locales/da/analyzer.json b/locales/da/analyzer.json index 354dc54f5..c391cc306 100644 --- a/locales/da/analyzer.json +++ b/locales/da/analyzer.json @@ -181,6 +181,7 @@ "comp.groupBy.special": "", "comp.selectWeapons": "", "comp.removeWeapon": "", + "comp.reorderWeapon": "", "comp.pickWeapon": "", "comp.showWeaponGrid": "", "comp.hideWeaponGrid": "", diff --git a/locales/da/common.json b/locales/da/common.json index 078054c3b..ae76e065d 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "logindforsøg afbrudt", "auth.errors.failed": "Loginforsøg fejlet", "auth.errors.discordPermissions": "Før at du kan oprette en profil på sendou.ink, skal sendou.ink have adgang til din Discordprofils navn, brugerbillede og sociale forbindelser (de sociale medier, som du har tilknyttet din discordprofil).", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": "", "toasts.error": "", "toasts.success": "", @@ -227,6 +230,7 @@ "plans.bgStyle.OVER": "Set ovenfra", "plans.bgStyle.MINI": "", "plans.adder.objective": "", + "plans.ranges": "", "articles.by": "af {{author}}", "theme.light": "Lyst", "theme.dark": "Mørkt", diff --git a/locales/da/contributions.json b/locales/da/contributions.json index d3b6e918f..6d60cd1da 100644 --- a/locales/da/contributions.json +++ b/locales/da/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink er et projekt af <2>Sendou med hjælp fra bidragsydere:", + "project": "Sendou.ink er et projekt af Sendou med hjælp fra bidragsydere:", "code": "Se alle der har hjulpet med kodningen", "lean": "Hjalp med at fremvise Splatoons 'indre' og skabte Lanista-botten", "borzoic": "Lavede mærker, ikoner og kunst til forsiden", diff --git a/locales/da/tournament.json b/locales/da/tournament.json index b18afafad..a32c9c617 100644 --- a/locales/da/tournament.json +++ b/locales/da/tournament.json @@ -153,6 +153,9 @@ "progression.error.NO_SE_SOURCE": "", "progression.error.NO_DE_POSITIVE": "", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/da/user.json b/locales/da/user.json index 3f2e14e76..3fe8398b3 100644 --- a/locales/da/user.json +++ b/locales/da/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "Vælg de resultater, som du vil fremhæve", "results.button.showHighlights": "Vis højdepunkter", "results.button.showAll": "Vis alt", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "Maks antal våben nået", "search.info": "", "search.noResults": "Søgningen ’{{query}}’ fandt ingen brugere", diff --git a/locales/de/analyzer.json b/locales/de/analyzer.json index f4051aa45..e9a284e40 100644 --- a/locales/de/analyzer.json +++ b/locales/de/analyzer.json @@ -181,6 +181,7 @@ "comp.groupBy.special": "", "comp.selectWeapons": "", "comp.removeWeapon": "", + "comp.reorderWeapon": "", "comp.pickWeapon": "", "comp.showWeaponGrid": "", "comp.hideWeaponGrid": "", diff --git a/locales/de/common.json b/locales/de/common.json index a486549cc..df000ce93 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "Einloggen abgebrochen", "auth.errors.failed": "Einloggen fehlgeschlagen", "auth.errors.discordPermissions": "Für dein sendou.ink-Profil benötigt die Seite Zugriff auf den Namen, Avatar und verbundene Social-Media-Accounts in deinem Discord-Profil.", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": "", "toasts.error": "", "toasts.success": "", @@ -227,6 +230,7 @@ "plans.bgStyle.OVER": "Vogelperspektive", "plans.bgStyle.MINI": "", "plans.adder.objective": "", + "plans.ranges": "", "articles.by": "von {{author}}", "theme.light": "Hell", "theme.dark": "Dunkel", diff --git a/locales/de/contributions.json b/locales/de/contributions.json index 0341ac459..d45f323e1 100644 --- a/locales/de/contributions.json +++ b/locales/de/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink ist ein Projekt von <2>Sendou - mit der Hilfe von Mitwirkenden:", + "project": "Sendou.ink ist ein Projekt von Sendou - mit der Hilfe von Mitwirkenden:", "code": "Alle am Code Mitwirkenden ansehen", "lean": "Hilft mit der Analyse von Splatoon-Internals und hat den Lanista-Bot erstellt", "borzoic": "Erstellte Abzeichen, Icons und Grafiken auf der Homepage", diff --git a/locales/de/tournament.json b/locales/de/tournament.json index 63a7c0bca..40ef0e573 100644 --- a/locales/de/tournament.json +++ b/locales/de/tournament.json @@ -153,6 +153,9 @@ "progression.error.NO_SE_SOURCE": "", "progression.error.NO_DE_POSITIVE": "", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/de/user.json b/locales/de/user.json index 2c3873d9e..4c877750d 100644 --- a/locales/de/user.json +++ b/locales/de/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "Wähle Ergebnisse, die du hervorheben möchtest", "results.button.showHighlights": "", "results.button.showAll": "", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "Maximale Zahl an Waffen erreicht", "search.info": "", "search.noResults": "Keine Nutzer gefunden, die '{{query}}' entsprechen", diff --git a/locales/en/analyzer.json b/locales/en/analyzer.json index a1213bfb3..ae54372bb 100644 --- a/locales/en/analyzer.json +++ b/locales/en/analyzer.json @@ -181,6 +181,7 @@ "comp.groupBy.special": "Special weapon", "comp.selectWeapons": "Select up to {{max}} weapons", "comp.removeWeapon": "Remove weapon", + "comp.reorderWeapon": "Drag to reorder weapon", "comp.pickWeapon": "Pick a weapon", "comp.showWeaponGrid": "Show weapon selector", "comp.hideWeaponGrid": "Hide weapon selector", diff --git a/locales/en/common.json b/locales/en/common.json index f3f0694e2..aec67cec0 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -95,7 +95,10 @@ "auth.errors.aborted": "Login Aborted", "auth.errors.failed": "Login Failed", "auth.errors.discordPermissions": "For your sendou.ink profile, the site needs access to your Discord profile's name, avatar and social connections.", - "auth.errors.unknown": "Unknown error, try again a bit later. Verify also that your Discord account has a verified email. For help please reach out to us via the #helpdesk channel on our Discord:", + "auth.errors.discordOverloaded": "Discord is currently overloaded. Please try again later. If you are a supporter, you can also use the /login command on our Discord to get a direct login link:", + "auth.errors.unverifiedEmail": "Your Discord account does not have a verified email address. Please verify your email on Discord and try again.", + "auth.errors.browserPrivacy": "Your browser appears to be blocking cookies required for login. Please check your browser privacy settings, disable extensions that may block cookies, or try a different browser.", + "auth.errors.unknown": "Unknown error, try again a bit later. For help please reach out to us via the #helpdesk channel on our Discord:", "toasts.error": "Error", "toasts.success": "Success", "toasts.info": "Info", @@ -227,6 +230,7 @@ "plans.bgStyle.OVER": "Overhead", "plans.bgStyle.MINI": "Minimap", "plans.adder.objective": "Objective", + "plans.ranges": "Ranges", "articles.by": "by {{author}}", "theme.light": "Light", "theme.dark": "Dark", diff --git a/locales/en/contributions.json b/locales/en/contributions.json index 867b71e25..4316728b9 100644 --- a/locales/en/contributions.json +++ b/locales/en/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink is a project by <2>Sendou with help from contributors:", + "project": "Sendou.ink is a project by Sendou with help from contributors:", "code": "See all code contributors", "lean": "Helped with uncovering Splatoon internals and created the Lanista bot", "borzoic": "Made badges, icons and front page art", diff --git a/locales/en/tournament.json b/locales/en/tournament.json index a0edf91ee..8e425c287 100644 --- a/locales/en/tournament.json +++ b/locales/en/tournament.json @@ -153,6 +153,9 @@ "progression.error.NO_SE_SOURCE": "Single elimination is not a valid source bracket", "progression.error.NO_DE_POSITIVE": "Double elimination is not valid for positive progression", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "Swiss bracket with early advance/elimination must lead to another bracket", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "A/B divisions can only be enabled on round robin brackets", + "progression.error.AB_DIVISIONS_NOT_STARTING": "A/B divisions can only be enabled on starting brackets", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "A/B divisions requires an even number of teams per group", "lfg.askCaptainToJoinQueue": "Ask your team's captain or a manager to join the queue", "customFlow.beforeSet": "Before set", "customFlow.afterMap": "After map", diff --git a/locales/en/user.json b/locales/en/user.json index 28315d99f..008fe7ad3 100644 --- a/locales/en/user.json +++ b/locales/en/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "Select the results you want to highlight", "results.button.showHighlights": "Show highlights", "results.button.showAll": "Show all", + "results.filter.placeholder": "Filter by tournament", "forms.errors.maxWeapons": "Max weapon count reached", "search.info": "Search for users by Discord or Splatoon 3 name", "search.noResults": "No users found matching '{{query}}'", diff --git a/locales/es-ES/analyzer.json b/locales/es-ES/analyzer.json index cd7ebf5ac..3ba45af64 100644 --- a/locales/es-ES/analyzer.json +++ b/locales/es-ES/analyzer.json @@ -183,6 +183,7 @@ "comp.groupBy.special": "Arma especial", "comp.selectWeapons": "Selecciona hasta {{max}} armas", "comp.removeWeapon": "Eliminar arma", + "comp.reorderWeapon": "", "comp.pickWeapon": "Elige un arma", "comp.showWeaponGrid": "Mostrar selector de armas", "comp.hideWeaponGrid": "Ocultar selector de armas", diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index 1c852ec44..7963639bf 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "Inicio de Sesión Cancelado", "auth.errors.failed": "Error al Iniciar Sesión", "auth.errors.discordPermissions": "Para tu perfil de sendou.ink, el sitio necesita acceso al nombre, avatar y conexiones sociales de tu perfil de Discord.", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": "Error desconocido, inténtalo de nuevo más tarde. Verifica también que tu cuenta de Discord tenga un correo electrónico verificado. Para obtener ayuda, contáctanos en el canal #helpdesk de nuestro Discord:", "toasts.error": "Error", "toasts.success": "Éxito", @@ -228,6 +231,7 @@ "plans.bgStyle.OVER": "Vista aérea", "plans.bgStyle.MINI": "Minimapa", "plans.adder.objective": "Objetivo", + "plans.ranges": "", "articles.by": "por {{author}}", "theme.light": "Claro", "theme.dark": "Oscuro", diff --git a/locales/es-ES/contributions.json b/locales/es-ES/contributions.json index f37dabf09..8c1908be1 100644 --- a/locales/es-ES/contributions.json +++ b/locales/es-ES/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink es un proyecto hecho por <2>Sendou con ayuda de contribuidores:", + "project": "Sendou.ink es un proyecto hecho por Sendou con ayuda de contribuidores:", "code": "Ver todos los contribuidores", "lean": "Ayudó a descubrir partes internas de Splatoon y creó el bot Lanista", "borzoic": "Creó insignias, íconos y el arte de la página principal", diff --git a/locales/es-ES/tournament.json b/locales/es-ES/tournament.json index 72710251f..b4fbffccf 100644 --- a/locales/es-ES/tournament.json +++ b/locales/es-ES/tournament.json @@ -155,6 +155,9 @@ "progression.error.NO_SE_SOURCE": "La eliminación simple no es un cuadro de origen válido", "progression.error.NO_DE_POSITIVE": "La eliminación doble no es válida para progresión positiva", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "El cuadro suizo con avance/eliminación anticipada debe llevar a otro cuadro", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/es-ES/user.json b/locales/es-ES/user.json index c6b00e59d..4644f05df 100644 --- a/locales/es-ES/user.json +++ b/locales/es-ES/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "Elige los resultados que quieres resaltar", "results.button.showHighlights": "Mostrar resaltados", "results.button.showAll": "Mostrar todos", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "Máxima cantidad de armas", "search.info": "Busca usuarios por su nombre de Discord o de Splatoon 3", "search.noResults": "No se encontraron usuarios que coincidan con '{{query}}'", diff --git a/locales/es-US/analyzer.json b/locales/es-US/analyzer.json index b6f5a5adb..4bbcbc86b 100644 --- a/locales/es-US/analyzer.json +++ b/locales/es-US/analyzer.json @@ -183,6 +183,7 @@ "comp.groupBy.special": "", "comp.selectWeapons": "", "comp.removeWeapon": "", + "comp.reorderWeapon": "", "comp.pickWeapon": "", "comp.showWeaponGrid": "", "comp.hideWeaponGrid": "", diff --git a/locales/es-US/common.json b/locales/es-US/common.json index 2fa230dbc..ae90bbddb 100644 --- a/locales/es-US/common.json +++ b/locales/es-US/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "Ingreso cancelado", "auth.errors.failed": "Ingreso fallido", "auth.errors.discordPermissions": "Para tu perfil en sendou.ink, el sitio requiere aceso a tu nombre en Discord, avatar, y redes sociales.", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": "", "toasts.error": "", "toasts.success": "", @@ -228,6 +231,7 @@ "plans.bgStyle.OVER": "Vista aérea", "plans.bgStyle.MINI": "", "plans.adder.objective": "", + "plans.ranges": "", "articles.by": "por {{author}}", "theme.light": "Claro", "theme.dark": "Oscuro", diff --git a/locales/es-US/contributions.json b/locales/es-US/contributions.json index 50daba4c7..ecaae3b26 100644 --- a/locales/es-US/contributions.json +++ b/locales/es-US/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink es un proyecto hecho por <2>Sendou con ayuda de contribuidores:", + "project": "Sendou.ink es un proyecto hecho por Sendou con ayuda de contribuidores:", "code": "Ver todos los contribuidores", "lean": "Ayudó a descubrir partes internas de Splatoon y creó el bot Lanista", "borzoic": "Creó insignias, íconos y el arte de la página principal", diff --git a/locales/es-US/tournament.json b/locales/es-US/tournament.json index 3ba0494c6..7d4156dbf 100644 --- a/locales/es-US/tournament.json +++ b/locales/es-US/tournament.json @@ -155,6 +155,9 @@ "progression.error.NO_SE_SOURCE": "", "progression.error.NO_DE_POSITIVE": "", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/es-US/user.json b/locales/es-US/user.json index 061c084cc..6fc05551c 100644 --- a/locales/es-US/user.json +++ b/locales/es-US/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "Elige los resultados que quieres resaltar", "results.button.showHighlights": "Mostrar resaltos", "results.button.showAll": "Mostrar todos", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "Máxima cantidad de armas", "search.info": "", "search.noResults": "No se encontraron usuarios que coincidan con '{{query}}'", diff --git a/locales/fr-CA/analyzer.json b/locales/fr-CA/analyzer.json index cf54f2f91..93305cfd1 100644 --- a/locales/fr-CA/analyzer.json +++ b/locales/fr-CA/analyzer.json @@ -183,6 +183,7 @@ "comp.groupBy.special": "", "comp.selectWeapons": "", "comp.removeWeapon": "", + "comp.reorderWeapon": "", "comp.pickWeapon": "", "comp.showWeaponGrid": "", "comp.hideWeaponGrid": "", diff --git a/locales/fr-CA/common.json b/locales/fr-CA/common.json index a8c064c13..5f5610359 100644 --- a/locales/fr-CA/common.json +++ b/locales/fr-CA/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "Connexion abandonnée", "auth.errors.failed": "Connexion échouée", "auth.errors.discordPermissions": "Pour mettre en place votre profil, sendou.ink a besoin de votre nom de profil Discord, de votre avatar et de vos réseaux connectés.", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": "", "toasts.error": "", "toasts.success": "", @@ -228,6 +231,7 @@ "plans.bgStyle.OVER": "Vue de dessus", "plans.bgStyle.MINI": "", "plans.adder.objective": "", + "plans.ranges": "", "articles.by": "par {{author}}", "theme.light": "Clair", "theme.dark": "Sombre", diff --git a/locales/fr-CA/contributions.json b/locales/fr-CA/contributions.json index cafea0a07..c12a13366 100644 --- a/locales/fr-CA/contributions.json +++ b/locales/fr-CA/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink est un projet créé par <2>Sendou avec l'aide de contributeurs :", + "project": "Sendou.ink est un projet créé par Sendou avec l'aide de contributeurs :", "code": "Voir tous les contributeurs au code", "lean": "A aidé à découvrir les fonctionnements internes de Splatoon et a créé le bot Lanista", "borzoic": "A créé les badges, les icônes et l'illustration de la page d'accueil", diff --git a/locales/fr-CA/tournament.json b/locales/fr-CA/tournament.json index 12440c68d..d7a42f9d0 100644 --- a/locales/fr-CA/tournament.json +++ b/locales/fr-CA/tournament.json @@ -155,6 +155,9 @@ "progression.error.NO_SE_SOURCE": "", "progression.error.NO_DE_POSITIVE": "", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/fr-CA/user.json b/locales/fr-CA/user.json index 21247c963..2e59aec77 100644 --- a/locales/fr-CA/user.json +++ b/locales/fr-CA/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "Sélectionnez les résultats que vous voulez mettre en avant", "results.button.showHighlights": "", "results.button.showAll": "", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "Nombre d'armes maximum atteint", "search.info": "", "search.noResults": "Aucun utilisateur correspondant à '{{query}}' n'a été trouvé", diff --git a/locales/fr-EU/analyzer.json b/locales/fr-EU/analyzer.json index 92ddf6b8a..8e08cb89a 100644 --- a/locales/fr-EU/analyzer.json +++ b/locales/fr-EU/analyzer.json @@ -183,6 +183,7 @@ "comp.groupBy.special": "", "comp.selectWeapons": "", "comp.removeWeapon": "", + "comp.reorderWeapon": "", "comp.pickWeapon": "", "comp.showWeaponGrid": "", "comp.hideWeaponGrid": "", diff --git a/locales/fr-EU/common.json b/locales/fr-EU/common.json index ac268bdae..dc9fb372c 100644 --- a/locales/fr-EU/common.json +++ b/locales/fr-EU/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "Connexion abandonnée", "auth.errors.failed": "Connexion échouée", "auth.errors.discordPermissions": "Pour mettre en place votre profil, sendou.ink a besoin de votre nom de profil Discord, de votre avatar et de vos réseaux connectés.", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": " Erreur inconnue, essayez plus tard. Verifiez que votre compte discord aie un email vérifié. Allez sur me channel #helpdesk de notre discord pour de l'aide:", "toasts.error": "Erreur", "toasts.success": "Succès", @@ -228,6 +231,7 @@ "plans.bgStyle.OVER": "Vue de dessus", "plans.bgStyle.MINI": "Minimap", "plans.adder.objective": "Objectif", + "plans.ranges": "", "articles.by": "par {{author}}", "theme.light": "Clair", "theme.dark": "Sombre", diff --git a/locales/fr-EU/contributions.json b/locales/fr-EU/contributions.json index 5a47c1d57..fccf5576b 100644 --- a/locales/fr-EU/contributions.json +++ b/locales/fr-EU/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink est un projet créé par <2>Sendou avec l'aide de contributeurs :", + "project": "Sendou.ink est un projet créé par Sendou avec l'aide de contributeurs :", "code": "Voir tous les contributeurs au code", "lean": "A aidé à découvrir les fonctionnements internes de Splatoon et a créé le bot Lanista", "borzoic": "A créé les badges, les icônes et l'illustration de la page d'accueil", diff --git a/locales/fr-EU/tournament.json b/locales/fr-EU/tournament.json index 5fdc55e35..acaaa5ef7 100644 --- a/locales/fr-EU/tournament.json +++ b/locales/fr-EU/tournament.json @@ -155,6 +155,9 @@ "progression.error.NO_SE_SOURCE": "Single elimination is not a valid source bracket", "progression.error.NO_DE_POSITIVE": "Double elimination is not valid for positive progression", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/fr-EU/user.json b/locales/fr-EU/user.json index 92621b46c..5c70f11ac 100644 --- a/locales/fr-EU/user.json +++ b/locales/fr-EU/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "Sélectionnez les résultats que vous voulez mettre en avant", "results.button.showHighlights": "Montrer les highlights", "results.button.showAll": "Tout montrer", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "Nombre d'armes maximum atteint", "search.info": "Recherchez avec le pseudo Discord ou Splatoon 3 du compte", "search.noResults": "Aucun utilisateur correspondant à '{{query}}' n'a été trouvé", diff --git a/locales/he/analyzer.json b/locales/he/analyzer.json index 20cc93ba0..34fde8a1b 100644 --- a/locales/he/analyzer.json +++ b/locales/he/analyzer.json @@ -183,6 +183,7 @@ "comp.groupBy.special": "נשק מיוחד", "comp.selectWeapons": "בחר עד ל-{{max}} נשקים", "comp.removeWeapon": "הסר נשק", + "comp.reorderWeapon": "", "comp.pickWeapon": "בחר נשק", "comp.showWeaponGrid": "הצג בורר נשק", "comp.hideWeaponGrid": "הסתר בורר נשק", diff --git a/locales/he/common.json b/locales/he/common.json index 6db52a6e8..15a06c15e 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "הכניסה בוטלה", "auth.errors.failed": "הכניסה נכשלה", "auth.errors.discordPermissions": "עבור פרופיל sendou.ink שלך, האתר זקוק לגישה לשם, הפרופיל והקשרים החברתיים של פרופיל ה-Discord שלך.", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": "", "toasts.error": "", "toasts.success": "", @@ -227,6 +230,7 @@ "plans.bgStyle.OVER": "מבט על", "plans.bgStyle.MINI": "", "plans.adder.objective": "", + "plans.ranges": "", "articles.by": "מאת {{author}}", "theme.light": "בהיר", "theme.dark": "חשוך", diff --git a/locales/he/contributions.json b/locales/he/contributions.json index 7a7ceffd3..874e110d0 100644 --- a/locales/he/contributions.json +++ b/locales/he/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink הוא פרויקט של <2>Sendou עם עזרה מהתורמים:", + "project": "Sendou.ink הוא פרויקט של Sendou עם עזרה מהתורמים:", "code": "ראה את כל תורמי הקוד", "lean": "עזר בחשיפת מידע פנימי של Splatoon ויצירת הבוט Lanista", "borzoic": "הכין תגים, אייקונים וציור לעמוד הראשון", diff --git a/locales/he/tournament.json b/locales/he/tournament.json index d89da162a..f2c4c9278 100644 --- a/locales/he/tournament.json +++ b/locales/he/tournament.json @@ -155,6 +155,9 @@ "progression.error.NO_SE_SOURCE": "", "progression.error.NO_DE_POSITIVE": "", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/he/user.json b/locales/he/user.json index b4f9ec5ef..cfe95db7a 100644 --- a/locales/he/user.json +++ b/locales/he/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "בחרו את התוצאות שאתם רוצים להדגיש", "results.button.showHighlights": "", "results.button.showAll": "", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "הגעה לכמות מקסימלית של מספר נשקים", "search.info": "", "search.noResults": "לא נמצאו משתמשים התואמים '{{query}}'", diff --git a/locales/it/analyzer.json b/locales/it/analyzer.json index 78dc855b2..e46d1beeb 100644 --- a/locales/it/analyzer.json +++ b/locales/it/analyzer.json @@ -183,6 +183,7 @@ "comp.groupBy.special": "", "comp.selectWeapons": "", "comp.removeWeapon": "", + "comp.reorderWeapon": "", "comp.pickWeapon": "", "comp.showWeaponGrid": "", "comp.hideWeaponGrid": "", diff --git a/locales/it/common.json b/locales/it/common.json index eb9358411..b03de3778 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "Accesso cancellato", "auth.errors.failed": "Accesso fallito", "auth.errors.discordPermissions": "Per il tuo profilo di sendou.ink, il sito ha bisogno di accesso al nome utente, avatar e connessioni social del tuo profilo Discord.", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": "Errore sconosciuto, riprova più tardi. Verifica anche che il tuo account Discord abbia una email verificata. Per ulteriore aiuto, contattaci tramite il canale #helpdesk nel nostro Discord:", "toasts.error": "Errore", "toasts.success": "Successo", @@ -228,6 +231,7 @@ "plans.bgStyle.OVER": "Pianta", "plans.bgStyle.MINI": "Minimappa", "plans.adder.objective": "Obiettivo", + "plans.ranges": "", "articles.by": "da {{author}}", "theme.light": "Light", "theme.dark": "Dark", diff --git a/locales/it/contributions.json b/locales/it/contributions.json index fe2b4e0f6..b23dd94d1 100644 --- a/locales/it/contributions.json +++ b/locales/it/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink è un proggeto creato da <2>Sendou con aiuto dai contributori:", + "project": "Sendou.ink è un proggeto creato da Sendou con aiuto dai contributori:", "code": "Vedi tutti i contributori al codice sorgente", "lean": "Ha aiutato scoprendo le mecchaniche interne di Splatoon e creando il bot Lanista", "borzoic": "Ha creato le medaglie, le icone e l'arte sulla pagina principale", diff --git a/locales/it/tournament.json b/locales/it/tournament.json index 6d46c16a7..41fbead34 100644 --- a/locales/it/tournament.json +++ b/locales/it/tournament.json @@ -155,6 +155,9 @@ "progression.error.NO_SE_SOURCE": "Eliminazione singola non è un bracket sorgente valido", "progression.error.NO_DE_POSITIVE": "Doppia eliminazione non è valida per progressione positiva", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/it/user.json b/locales/it/user.json index 5741c9e9c..53c29ef19 100644 --- a/locales/it/user.json +++ b/locales/it/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "Scegli il risultato che vuoi mettere come highlight", "results.button.showHighlights": "Mostra highlight", "results.button.showAll": "Mostra tutti", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "Massimo numero di armi raggiunto", "search.info": "Cerca utenti tramite nome Discord o Splatoon 3", "search.noResults": "Nessun utente trovato per '{{query}}'", diff --git a/locales/ja/analyzer.json b/locales/ja/analyzer.json index da81ac7ee..40ddce694 100644 --- a/locales/ja/analyzer.json +++ b/locales/ja/analyzer.json @@ -177,6 +177,7 @@ "comp.groupBy.special": "", "comp.selectWeapons": "", "comp.removeWeapon": "", + "comp.reorderWeapon": "", "comp.pickWeapon": "", "comp.showWeaponGrid": "", "comp.hideWeaponGrid": "", diff --git a/locales/ja/common.json b/locales/ja/common.json index 7b8228f19..53eaf2116 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "ログインを中断しました", "auth.errors.failed": "ログインに失敗しました", "auth.errors.discordPermissions": "sendou.ink は、Discord のプロファイル名、アバター、SNS連携をサイトのプロファイルに使用します。", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": "", "toasts.error": "", "toasts.success": "", @@ -225,6 +228,7 @@ "plans.bgStyle.OVER": "頭上マップ", "plans.bgStyle.MINI": "ミニマップ", "plans.adder.objective": "目的", + "plans.ranges": "", "articles.by": "作者: {{author}}", "theme.light": "Light", "theme.dark": "Dark", diff --git a/locales/ja/contributions.json b/locales/ja/contributions.json index 320b40a11..cdcce4b19 100644 --- a/locales/ja/contributions.json +++ b/locales/ja/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink は <2>Sendou によるプロジェクトで、以下のコントリビューターに支えられています:", + "project": "Sendou.ink は Sendou によるプロジェクトで、以下のコントリビューターに支えられています:", "code": "すべてのコードコントリビューターを見る", "lean": "Splatoon 内部理解のサポート、Lanista bot の作成", "borzoic": "バッジ、アイコン、トップページアートの作成", diff --git a/locales/ja/tournament.json b/locales/ja/tournament.json index dd0b47e6e..49fbfd7f9 100644 --- a/locales/ja/tournament.json +++ b/locales/ja/tournament.json @@ -149,6 +149,9 @@ "progression.error.NO_SE_SOURCE": "シングルエリ三ネーションは妥当なブラケットではないです", "progression.error.NO_DE_POSITIVE": "ダブルエリミネーションは普通の進行(前向き)では妥当ではないです", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/ja/user.json b/locales/ja/user.json index 3e258c267..a29b526df 100644 --- a/locales/ja/user.json +++ b/locales/ja/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "戦績として選択したい結果を選ぶ", "results.button.showHighlights": "ハイライトを表示", "results.button.showAll": "全て表示", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "最大ブキ数を超えました", "search.info": "", "search.noResults": "該当ユーザーが見つかりません '{{query}}'", diff --git a/locales/ko/analyzer.json b/locales/ko/analyzer.json index 0597d0a40..bcfa8c9c3 100644 --- a/locales/ko/analyzer.json +++ b/locales/ko/analyzer.json @@ -177,6 +177,7 @@ "comp.groupBy.special": "", "comp.selectWeapons": "", "comp.removeWeapon": "", + "comp.reorderWeapon": "", "comp.pickWeapon": "", "comp.showWeaponGrid": "", "comp.hideWeaponGrid": "", diff --git a/locales/ko/common.json b/locales/ko/common.json index bfb90daa1..6deb1b74d 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "로그인 중단됨", "auth.errors.failed": "로그인 실패", "auth.errors.discordPermissions": "sendou.ink 프로필을 위해 디스코드 프로필의 이름, 아바타와 연락처에 대한 접근이 필요합니다.", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": "", "toasts.error": "", "toasts.success": "", @@ -225,6 +228,7 @@ "plans.bgStyle.OVER": "전체", "plans.bgStyle.MINI": "", "plans.adder.objective": "", + "plans.ranges": "", "articles.by": "{{author}} 작성", "theme.light": "라이트", "theme.dark": "다크", diff --git a/locales/ko/contributions.json b/locales/ko/contributions.json index d734e2e3b..b2b7ae553 100644 --- a/locales/ko/contributions.json +++ b/locales/ko/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink는 <2>Sendou가 기여자들의 도움을 받아 만든 프로젝트입니다:", + "project": "Sendou.ink는 Sendou가 기여자들의 도움을 받아 만든 프로젝트입니다:", "code": "전체 코드 기여자 확인", "lean": "스플래툰의 내부를 파헤치는 데에 도움을 주고 Lanista bot을 만들었습니다", "borzoic": "배지, 아이콘과 표지 그림을 만들었습니다", diff --git a/locales/ko/tournament.json b/locales/ko/tournament.json index 9e311c0ff..a0d03e2e6 100644 --- a/locales/ko/tournament.json +++ b/locales/ko/tournament.json @@ -149,6 +149,9 @@ "progression.error.NO_SE_SOURCE": "", "progression.error.NO_DE_POSITIVE": "", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/ko/user.json b/locales/ko/user.json index 7b9359991..e911fe35a 100644 --- a/locales/ko/user.json +++ b/locales/ko/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "", "results.button.showHighlights": "", "results.button.showAll": "", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "", "search.info": "", "search.noResults": "", diff --git a/locales/nl/analyzer.json b/locales/nl/analyzer.json index f35dc180e..ca37a315d 100644 --- a/locales/nl/analyzer.json +++ b/locales/nl/analyzer.json @@ -181,6 +181,7 @@ "comp.groupBy.special": "", "comp.selectWeapons": "", "comp.removeWeapon": "", + "comp.reorderWeapon": "", "comp.pickWeapon": "", "comp.showWeaponGrid": "", "comp.hideWeaponGrid": "", diff --git a/locales/nl/common.json b/locales/nl/common.json index 26af833b1..17367f1db 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "", "auth.errors.failed": "", "auth.errors.discordPermissions": "", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": "", "toasts.error": "", "toasts.success": "", @@ -227,6 +230,7 @@ "plans.bgStyle.OVER": "", "plans.bgStyle.MINI": "", "plans.adder.objective": "", + "plans.ranges": "", "articles.by": "", "theme.light": "", "theme.dark": "", diff --git a/locales/nl/contributions.json b/locales/nl/contributions.json index 6ff436d37..79df59745 100644 --- a/locales/nl/contributions.json +++ b/locales/nl/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink is een project door <2>Sendou met behulp van de volgende bijdragers:", + "project": "Sendou.ink is een project door Sendou met behulp van de volgende bijdragers:", "code": "", "lean": "Heeft geholpen met het onthullingen van de interne werkingen van Splatoon en heeft ook de Lanista bot ontwikkeld.", "borzoic": "Heeft badges, iconen en de voorpagina art gemaakt.", diff --git a/locales/nl/tournament.json b/locales/nl/tournament.json index 1894e4dab..9147a4b26 100644 --- a/locales/nl/tournament.json +++ b/locales/nl/tournament.json @@ -153,6 +153,9 @@ "progression.error.NO_SE_SOURCE": "", "progression.error.NO_DE_POSITIVE": "", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/nl/user.json b/locales/nl/user.json index 8a52b524f..a5ed8f29b 100644 --- a/locales/nl/user.json +++ b/locales/nl/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "", "results.button.showHighlights": "", "results.button.showAll": "", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "", "search.info": "", "search.noResults": "", diff --git a/locales/pl/analyzer.json b/locales/pl/analyzer.json index 179b4d0de..0ac2be31f 100644 --- a/locales/pl/analyzer.json +++ b/locales/pl/analyzer.json @@ -185,6 +185,7 @@ "comp.groupBy.special": "", "comp.selectWeapons": "", "comp.removeWeapon": "", + "comp.reorderWeapon": "", "comp.pickWeapon": "", "comp.showWeaponGrid": "", "comp.hideWeaponGrid": "", diff --git a/locales/pl/common.json b/locales/pl/common.json index 170533b2f..e4252c03c 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "Logowanie przerwane", "auth.errors.failed": "Logowanie nieudane", "auth.errors.discordPermissions": "Do twojego profilu sendou.ink, ta strona potrzebuje dostęp do twojej nazwy, avataru i połączeń konta Discord.", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": "", "toasts.error": "", "toasts.success": "", @@ -228,6 +231,7 @@ "plans.bgStyle.OVER": "Overhead", "plans.bgStyle.MINI": "", "plans.adder.objective": "", + "plans.ranges": "", "articles.by": "napisane przez {{author}}", "theme.light": "Jasny", "theme.dark": "Ciemny", diff --git a/locales/pl/contributions.json b/locales/pl/contributions.json index ddff3e83a..0b55d5e32 100644 --- a/locales/pl/contributions.json +++ b/locales/pl/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink jest projektem zrobionym przez <2>Sendou wraz z pomocą innych współtwórców:", + "project": "Sendou.ink jest projektem zrobionym przez Sendou wraz z pomocą innych współtwórców:", "code": "Zobacz wszystkich współtwórców kodu", "lean": "Pomógł z odkrywaniem wnętrz Splatoona oraz stworzył Lanista bot", "borzoic": "Stworzyła odznaki, ikony i ilustracje na głównej stronie", diff --git a/locales/pl/tournament.json b/locales/pl/tournament.json index 2228823ad..62af58676 100644 --- a/locales/pl/tournament.json +++ b/locales/pl/tournament.json @@ -157,6 +157,9 @@ "progression.error.NO_SE_SOURCE": "", "progression.error.NO_DE_POSITIVE": "", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/pl/user.json b/locales/pl/user.json index 63b7fb78a..564944d61 100644 --- a/locales/pl/user.json +++ b/locales/pl/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "Wybierz wyniki, które chcesz wyróżnić", "results.button.showHighlights": "", "results.button.showAll": "", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "Maksymalna ilość broni osiągnięta", "search.info": "", "search.noResults": "Nie znaleziono użytkownika o nazwie '{{query}}'", diff --git a/locales/pt-BR/analyzer.json b/locales/pt-BR/analyzer.json index 98eb5ad1e..8e90599f3 100644 --- a/locales/pt-BR/analyzer.json +++ b/locales/pt-BR/analyzer.json @@ -183,6 +183,7 @@ "comp.groupBy.special": "", "comp.selectWeapons": "", "comp.removeWeapon": "", + "comp.reorderWeapon": "", "comp.pickWeapon": "", "comp.showWeaponGrid": "", "comp.hideWeaponGrid": "", diff --git a/locales/pt-BR/common.json b/locales/pt-BR/common.json index 908777b06..47549b85b 100644 --- a/locales/pt-BR/common.json +++ b/locales/pt-BR/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "Login Abortado", "auth.errors.failed": "Login Falhou", "auth.errors.discordPermissions": "Para o seu perfil do sendou.ink, o site precisa de acesso ao nome do perfil do seu Discord, incluindo também o avatar e conexões sociais.", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": "", "toasts.error": "", "toasts.success": "", @@ -228,6 +231,7 @@ "plans.bgStyle.OVER": "Visão aérea", "plans.bgStyle.MINI": "", "plans.adder.objective": "", + "plans.ranges": "", "articles.by": "por/pela {{author}}", "theme.light": "Claro", "theme.dark": "Escuro", diff --git a/locales/pt-BR/contributions.json b/locales/pt-BR/contributions.json index 15292073d..8bf05e8ad 100644 --- a/locales/pt-BR/contributions.json +++ b/locales/pt-BR/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink é um projeto por <2>Sendou com ajuda dos contribuidores(as):", + "project": "Sendou.ink é um projeto por Sendou com ajuda dos contribuidores(as):", "code": "Ver todos os contribuidores(as) de código", "lean": "Ajudou com as descobertas do sistema interno do Splatoon e criou o bot Lanista", "borzoic": "Fez as insígnias, ícones e arte da página inicial", diff --git a/locales/pt-BR/tournament.json b/locales/pt-BR/tournament.json index 9d449ffbb..cf5869250 100644 --- a/locales/pt-BR/tournament.json +++ b/locales/pt-BR/tournament.json @@ -155,6 +155,9 @@ "progression.error.NO_SE_SOURCE": "", "progression.error.NO_DE_POSITIVE": "", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/pt-BR/user.json b/locales/pt-BR/user.json index c98d5279d..4b73f1b6e 100644 --- a/locales/pt-BR/user.json +++ b/locales/pt-BR/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "Escolha os resultados que você quer destacar", "results.button.showHighlights": "", "results.button.showAll": "", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "Número máximo de armas no perfil atingido.", "search.info": "", "search.noResults": "Nenhum usuário encontrado com o termo '{{query}}'", diff --git a/locales/ru/analyzer.json b/locales/ru/analyzer.json index c8333b1ba..4d10e4b7b 100644 --- a/locales/ru/analyzer.json +++ b/locales/ru/analyzer.json @@ -185,6 +185,7 @@ "comp.groupBy.special": "", "comp.selectWeapons": "", "comp.removeWeapon": "", + "comp.reorderWeapon": "", "comp.pickWeapon": "", "comp.showWeaponGrid": "", "comp.hideWeaponGrid": "", diff --git a/locales/ru/common.json b/locales/ru/common.json index ef5cab90c..2f2d22a5a 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "Вход отменён", "auth.errors.failed": "Ошибка входа", "auth.errors.discordPermissions": "Для вашего профиля на sendou.ink странице нужен доступ к вашему имени, аватару и привязанным аккаунтам соц. сетей в Discord.", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": "Неизвестная ошибка, повторите позже. Убедитесь, что к вашему аккаунту Discord привязана проверенная почта. За помощью обращайтесь в канал #helpdesk на нашем сервере Discord:", "toasts.error": "Ошибка", "toasts.success": "Успешно", @@ -228,6 +231,7 @@ "plans.bgStyle.OVER": "Вид сверху", "plans.bgStyle.MINI": "Миникарта", "plans.adder.objective": "Цель", + "plans.ranges": "", "articles.by": "от {{author}}", "theme.light": "Светлая", "theme.dark": "Тёмная", diff --git a/locales/ru/contributions.json b/locales/ru/contributions.json index 549db390a..d1cbf2064 100644 --- a/locales/ru/contributions.json +++ b/locales/ru/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink это проект, созданный <2>Sendou с поддержкой помощников:", + "project": "Sendou.ink это проект, созданный Sendou с поддержкой помощников:", "code": "Посмотреть всех, кто внёс вклад в код", "lean": "Помощь с исследованием внутренностей Splatoon и создатель бота Lanista", "borzoic": "Создатель рисунка на главной странице, а также значков и иконок", diff --git a/locales/ru/tournament.json b/locales/ru/tournament.json index 0a3df698c..b4bad5b52 100644 --- a/locales/ru/tournament.json +++ b/locales/ru/tournament.json @@ -157,6 +157,9 @@ "progression.error.NO_SE_SOURCE": "Single elimination не валидная сетка-исток", "progression.error.NO_DE_POSITIVE": "Double elimination не валидно для позитивной прогрессии", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/ru/user.json b/locales/ru/user.json index c8b93a193..c0ee78445 100644 --- a/locales/ru/user.json +++ b/locales/ru/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "Выберите ваш избранный результат", "results.button.showHighlights": "Показать избранные", "results.button.showAll": "Показать все", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "Достигнут максимум", "search.info": "Поиск пользователей по имени Discord или Splatoon 3", "search.noResults": "По запросу '{{query}}' пользователь не найден", diff --git a/locales/zh/analyzer.json b/locales/zh/analyzer.json index 2dc913cb7..3ca9df0a7 100644 --- a/locales/zh/analyzer.json +++ b/locales/zh/analyzer.json @@ -177,6 +177,7 @@ "comp.groupBy.special": "", "comp.selectWeapons": "", "comp.removeWeapon": "", + "comp.reorderWeapon": "", "comp.pickWeapon": "", "comp.showWeaponGrid": "", "comp.hideWeaponGrid": "", diff --git a/locales/zh/common.json b/locales/zh/common.json index 0cbb97949..8af1e24ee 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -95,6 +95,9 @@ "auth.errors.aborted": "登录中止", "auth.errors.failed": "登录失败", "auth.errors.discordPermissions": "为了完善您的sendou.ink个人资料,网站需要获取您的Discord名字、头像和社交链接。", + "auth.errors.discordOverloaded": "", + "auth.errors.unverifiedEmail": "", + "auth.errors.browserPrivacy": "", "auth.errors.unknown": "", "toasts.error": "", "toasts.success": "", @@ -225,6 +228,7 @@ "plans.bgStyle.OVER": "俯视图", "plans.bgStyle.MINI": "", "plans.adder.objective": "", + "plans.ranges": "", "articles.by": "作者 {{author}}", "theme.light": "浅色模式", "theme.dark": "深色模式", diff --git a/locales/zh/contributions.json b/locales/zh/contributions.json index e271b322d..9c53240da 100644 --- a/locales/zh/contributions.json +++ b/locales/zh/contributions.json @@ -1,5 +1,5 @@ { - "project": "Sendou.ink是<2>Sendou在以下贡献者的帮助下创建的:", + "project": "Sendou.ink是Sendou在以下贡献者的帮助下创建的:", "code": "查看所有代码贡献者", "lean": "帮助破解斯普拉遁并创建了Lanista bot", "borzoic": "制作徽章、图标,设计首页", diff --git a/locales/zh/tournament.json b/locales/zh/tournament.json index 3fb65d3c5..71f639be3 100644 --- a/locales/zh/tournament.json +++ b/locales/zh/tournament.json @@ -149,6 +149,9 @@ "progression.error.NO_SE_SOURCE": "", "progression.error.NO_DE_POSITIVE": "", "progression.error.SWISS_EARLY_ADVANCE_NO_DESTINATION": "", + "progression.error.AB_DIVISIONS_NOT_ROUND_ROBIN": "", + "progression.error.AB_DIVISIONS_NOT_STARTING": "", + "progression.error.AB_DIVISIONS_ODD_TEAMS_PER_GROUP": "", "lfg.askCaptainToJoinQueue": "", "customFlow.beforeSet": "", "customFlow.afterMap": "", diff --git a/locales/zh/user.json b/locales/zh/user.json index da4c7b5b1..b609a38d2 100644 --- a/locales/zh/user.json +++ b/locales/zh/user.json @@ -159,6 +159,7 @@ "results.highlights.explanation": "选择您想强调的高光成绩", "results.button.showHighlights": "显示高光成绩", "results.button.showAll": "显示全部成绩", + "results.filter.placeholder": "", "forms.errors.maxWeapons": "已达到武器数量上限", "search.info": "", "search.noResults": "没有符合 '{{query}}' 的用户", diff --git a/migrations/132-build-gear-nullable.js b/migrations/132-build-gear-nullable.js new file mode 100644 index 000000000..035db381b --- /dev/null +++ b/migrations/132-build-gear-nullable.js @@ -0,0 +1,53 @@ +export function up(db) { + db.pragma("foreign_keys = OFF"); + + db.transaction(() => { + db.prepare( + /*sql*/ ` + create table "Build_new" ( + "id" integer primary key, + "ownerId" integer not null, + "title" text not null, + "description" text, + "modes" text, + "headGearSplId" integer, + "clothesGearSplId" integer, + "shoesGearSplId" integer, + "updatedAt" integer default (strftime('%s', 'now')) not null, + "private" integer default 0, + foreign key ("ownerId") references "User"("id") on delete restrict + ) strict + `, + ).run(); + + db.prepare( + /*sql*/ ` + insert into "Build_new" ("id", "ownerId", "title", "description", "modes", "headGearSplId", "clothesGearSplId", "shoesGearSplId", "updatedAt", "private") + select + "id", + "ownerId", + "title", + "description", + "modes", + case when "headGearSplId" = -1 then null else "headGearSplId" end, + case when "clothesGearSplId" = -1 then null else "clothesGearSplId" end, + case when "shoesGearSplId" = -1 then null else "shoesGearSplId" end, + "updatedAt", + "private" + from "Build" + `, + ).run(); + + db.prepare(/*sql*/ `drop table "Build"`).run(); + + db.prepare(/*sql*/ `alter table "Build_new" rename to "Build"`).run(); + + db.prepare( + /*sql*/ `create index build_owner_id on "Build"("ownerId")`, + ).run(); + + db.pragma("foreign_key_check"); + })(); + + db.pragma("foreign_keys = ON"); +} diff --git a/migrations/133-tournament-team-ab-division.js b/migrations/133-tournament-team-ab-division.js new file mode 100644 index 000000000..d4042ad02 --- /dev/null +++ b/migrations/133-tournament-team-ab-division.js @@ -0,0 +1,5 @@ +export function up(db) { + db.prepare( + /* sql */ `alter table "TournamentTeam" add column "abDivision" integer`, + ).run(); +} diff --git a/mobile-analyzer.png b/mobile-analyzer.png new file mode 100644 index 000000000..dc1651725 Binary files /dev/null and b/mobile-analyzer.png differ diff --git a/package.json b/package.json index 8ed148fe1..1822f1c1b 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,8 @@ "sync-weapon-params": "tsx scripts/sync-weapon-params.ts" }, "dependencies": { - "@aws-sdk/client-s3": "3.1019.0", - "@aws-sdk/lib-storage": "3.1019.0", + "@aws-sdk/client-s3": "3.1030.0", + "@aws-sdk/lib-storage": "3.1030.0", "@date-fns/tz": "1.4.1", "@dnd-kit/core": "6.3.1", "@dnd-kit/modifiers": "9.0.0", @@ -45,82 +45,82 @@ "@dnd-kit/utilities": "3.2.2", "@epic-web/cachified": "5.6.2", "@faker-js/faker": "10.4.0", - "@formatjs/intl-durationformat": "0.10.3", - "@internationalized/date": "3.12.0", - "@react-router/node": "7.14.0", - "@react-router/serve": "7.14.0", + "@formatjs/intl-durationformat": "0.10.4", + "@internationalized/date": "3.12.1", + "@react-router/node": "7.14.1", + "@react-router/serve": "7.14.1", "@remix-run/form-data-parser": "0.16.0", "@tldraw/tldraw": "3.12.1", - "@zumer/snapdom": "2.7.0", + "@zumer/snapdom": "2.9.0", "aws-sdk": "2.1693.0", - "better-sqlite3": "12.8.0", + "better-sqlite3": "12.9.0", "clsx": "2.1.1", - "compressorjs": "1.2.1", + "compressorjs": "1.3.0", "date-fns": "4.1.0", "edmonds-blossom-fixed": "1.0.1", "gray-matter": "4.0.3", "i18next": "25.10.10", "i18next-browser-languagedetector": "8.2.1", - "i18next-http-backend": "3.0.2", + "i18next-http-backend": "3.0.5", "ics": "3.11.0", - "isbot": "5.1.36", + "isbot": "5.1.38", "jsoncrush": "1.1.8", - "kysely": "0.28.14", - "lru-cache": "11.2.7", - "lucide-react": "1.7.0", - "markdown-to-jsx": "9.7.13", - "nanoid": "5.1.7", + "kysely": "0.28.16", + "lru-cache": "11.3.5", + "lucide-react": "1.8.0", + "markdown-to-jsx": "9.7.15", + "nanoid": "5.1.9", "neverthrow": "8.2.0", "node-cron": "4.2.1", "nprogress": "0.2.0", "openskill": "4.1.1", "p-limit": "7.3.0", "partysocket": "1.1.16", - "react": "19.2.4", - "react-aria-components": "1.16.0", + "react": "19.2.5", + "react-aria-components": "1.17.0", "react-charts": "3.0.0-beta.57", - "react-dom": "19.2.4", + "react-dom": "19.2.5", "react-error-boundary": "6.1.1", "react-flip-toolkit": "7.2.4", "react-i18next": "16.6.6", - "react-router": "7.14.0", + "react-router": "7.14.1", "react-use": "17.6.0", "react-use-draggable-scroll": "0.4.7", - "remeda": "2.33.6", + "remeda": "2.33.7", "remix-auth": "4.2.0", "remix-auth-oauth2": "3.4.1", "remix-i18next": "7.4.2", - "slugify": "1.6.8", + "slugify": "1.6.9", "swr": "2.4.1", "web-push": "3.6.7", "zod": "4.3.6" }, "devDependencies": { - "@biomejs/biome": "2.4.9", - "@playwright/test": "1.58.2", - "@react-router/dev": "7.14.0", + "@biomejs/biome": "2.4.11", + "@playwright/test": "1.59.1", + "@react-router/dev": "7.14.1", "@types/better-sqlite3": "7.6.13", - "@types/node": "25.5.0", + "@types/node": "25.6.0", "@types/node-cron": "3.0.11", "@types/nprogress": "0.2.3", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@types/web-push": "3.6.4", - "@vitest/browser-playwright": "4.1.2", - "@vitest/ui": "4.1.2", + "@vitest/browser-playwright": "4.1.4", + "@vitest/ui": "4.1.4", "babel-plugin-react-compiler": "19.1.0-rc.3", "cross-env": "10.1.0", - "dotenv": "17.3.1", + "dotenv": "17.4.2", "i18next-locales-sync": "2.1.1", - "knip": "6.1.0", + "knip": "6.4.1", "ley": "0.8.1", "sql-formatter": "15.7.3", "tsx": "4.21.0", - "typescript": "5.9.3", - "vite": "8.0.4", + "typescript": "6.0.3", + "vite": "8.0.8", "vite-node": "6.0.0", "vite-plugin-babel": "1.6.0", - "vitest": "4.1.2", + "vitest": "4.1.4", "vitest-browser-react": "2.2.0" }, "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d0ccf602..0bb447dc6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,26 +9,26 @@ importers: .: dependencies: '@aws-sdk/client-s3': - specifier: 3.1019.0 - version: 3.1019.0 + specifier: 3.1030.0 + version: 3.1030.0 '@aws-sdk/lib-storage': - specifier: 3.1019.0 - version: 3.1019.0(@aws-sdk/client-s3@3.1019.0) + specifier: 3.1030.0 + version: 3.1030.0(@aws-sdk/client-s3@3.1030.0) '@date-fns/tz': specifier: 1.4.1 version: 1.4.1 '@dnd-kit/core': specifier: 6.3.1 - version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 6.3.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@dnd-kit/modifiers': specifier: 9.0.0 - version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) '@dnd-kit/sortable': specifier: 10.0.0 - version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) '@dnd-kit/utilities': specifier: 3.2.2 - version: 3.2.2(react@19.2.4) + version: 3.2.2(react@19.2.5) '@epic-web/cachified': specifier: 5.6.2 version: 5.6.2 @@ -36,38 +36,38 @@ importers: specifier: 10.4.0 version: 10.4.0 '@formatjs/intl-durationformat': - specifier: 0.10.3 - version: 0.10.3 + specifier: 0.10.4 + version: 0.10.4 '@internationalized/date': - specifier: 3.12.0 - version: 3.12.0 + specifier: 3.12.1 + version: 3.12.1 '@react-router/node': - specifier: 7.14.0 - version: 7.14.0(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + specifier: 7.14.1 + version: 7.14.1(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3) '@react-router/serve': - specifier: 7.14.0 - version: 7.14.0(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + specifier: 7.14.1 + version: 7.14.1(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3) '@remix-run/form-data-parser': specifier: 0.16.0 version: 0.16.0 '@tldraw/tldraw': specifier: 3.12.1 - version: 3.12.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 3.12.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@zumer/snapdom': - specifier: 2.7.0 - version: 2.7.0 + specifier: 2.9.0 + version: 2.9.0 aws-sdk: specifier: 2.1693.0 version: 2.1693.0 better-sqlite3: - specifier: 12.8.0 - version: 12.8.0 + specifier: 12.9.0 + version: 12.9.0 clsx: specifier: 2.1.1 version: 2.1.1 compressorjs: - specifier: 1.2.1 - version: 1.2.1 + specifier: 1.3.0 + version: 1.3.0 date-fns: specifier: 4.1.0 version: 4.1.0 @@ -79,37 +79,37 @@ importers: version: 4.0.3 i18next: specifier: 25.10.10 - version: 25.10.10(typescript@5.9.3) + version: 25.10.10(typescript@6.0.3) i18next-browser-languagedetector: specifier: 8.2.1 version: 8.2.1 i18next-http-backend: - specifier: 3.0.2 - version: 3.0.2 + specifier: 3.0.5 + version: 3.0.5 ics: specifier: 3.11.0 version: 3.11.0 isbot: - specifier: 5.1.36 - version: 5.1.36 + specifier: 5.1.38 + version: 5.1.38 jsoncrush: specifier: 1.1.8 version: 1.1.8 kysely: - specifier: 0.28.14 - version: 0.28.14 + specifier: 0.28.16 + version: 0.28.16 lru-cache: - specifier: 11.2.7 - version: 11.2.7 + specifier: 11.3.5 + version: 11.3.5 lucide-react: - specifier: 1.7.0 - version: 1.7.0(react@19.2.4) + specifier: 1.8.0 + version: 1.8.0(react@19.2.5) markdown-to-jsx: - specifier: 9.7.13 - version: 9.7.13(react@19.2.4) + specifier: 9.7.15 + version: 9.7.15(react@19.2.5) nanoid: - specifier: 5.1.7 - version: 5.1.7 + specifier: 5.1.9 + version: 5.1.9 neverthrow: specifier: 8.2.0 version: 8.2.0 @@ -127,40 +127,40 @@ importers: version: 7.3.0 partysocket: specifier: 1.1.16 - version: 1.1.16(react@19.2.4) + version: 1.1.16(react@19.2.5) react: - specifier: 19.2.4 - version: 19.2.4 + specifier: 19.2.5 + version: 19.2.5 react-aria-components: - specifier: 1.16.0 - version: 1.16.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 1.17.0 + version: 1.17.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-charts: specifier: 3.0.0-beta.57 - version: 3.0.0-beta.57(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 3.0.0-beta.57(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-dom: - specifier: 19.2.4 - version: 19.2.4(react@19.2.4) + specifier: 19.2.5 + version: 19.2.5(react@19.2.5) react-error-boundary: specifier: 6.1.1 - version: 6.1.1(react@19.2.4) + version: 6.1.1(react@19.2.5) react-flip-toolkit: specifier: 7.2.4 - version: 7.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 7.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-i18next: specifier: 16.6.6 - version: 16.6.6(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + version: 16.6.6(i18next@25.10.10(typescript@6.0.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3) react-router: - specifier: 7.14.0 - version: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 7.14.1 + version: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-use: specifier: 17.6.0 - version: 17.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 17.6.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-use-draggable-scroll: specifier: 0.4.7 - version: 0.4.7(react@19.2.4) + version: 0.4.7(react@19.2.5) remeda: - specifier: 2.33.6 - version: 2.33.6 + specifier: 2.33.7 + version: 2.33.7 remix-auth: specifier: 4.2.0 version: 4.2.0 @@ -169,13 +169,13 @@ importers: version: 3.4.1(remix-auth@4.2.0) remix-i18next: specifier: 7.4.2 - version: 7.4.2(i18next@25.10.10(typescript@5.9.3))(react-i18next@16.6.6(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 7.4.2(i18next@25.10.10(typescript@6.0.3))(react-i18next@16.6.6(i18next@25.10.10(typescript@6.0.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3))(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) slugify: - specifier: 1.6.8 - version: 1.6.8 + specifier: 1.6.9 + version: 1.6.9 swr: specifier: 2.4.1 - version: 2.4.1(react@19.2.4) + version: 2.4.1(react@19.2.5) web-push: specifier: 3.6.7 version: 3.6.7 @@ -184,20 +184,20 @@ importers: version: 4.3.6 devDependencies: '@biomejs/biome': - specifier: 2.4.9 - version: 2.4.9 + specifier: 2.4.11 + version: 2.4.11 '@playwright/test': - specifier: 1.58.2 - version: 1.58.2 + specifier: 1.59.1 + version: 1.59.1 '@react-router/dev': - specifier: 7.14.0 - version: 7.14.0(@react-router/serve@7.14.0(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3))(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) + specifier: 7.14.1 + version: 7.14.1(@react-router/serve@7.14.1(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3))(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(tsx@4.21.0)(typescript@6.0.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) '@types/better-sqlite3': specifier: 7.6.13 version: 7.6.13 '@types/node': - specifier: 25.5.0 - version: 25.5.0 + specifier: 25.6.0 + version: 25.6.0 '@types/node-cron': specifier: 3.0.11 version: 3.0.11 @@ -214,11 +214,11 @@ importers: specifier: 3.6.4 version: 3.6.4 '@vitest/browser-playwright': - specifier: 4.1.2 - version: 4.1.2(playwright@1.58.2)(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + specifier: 4.1.4 + version: 4.1.4(playwright@1.59.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4) '@vitest/ui': - specifier: 4.1.2 - version: 4.1.2(vitest@4.1.2) + specifier: 4.1.4 + version: 4.1.4(vitest@4.1.4) babel-plugin-react-compiler: specifier: 19.1.0-rc.3 version: 19.1.0-rc.3 @@ -226,14 +226,14 @@ importers: specifier: 10.1.0 version: 10.1.0 dotenv: - specifier: 17.3.1 - version: 17.3.1 + specifier: 17.4.2 + version: 17.4.2 i18next-locales-sync: specifier: 2.1.1 version: 2.1.1 knip: - specifier: 6.1.0 - version: 6.1.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + specifier: 6.4.1 + version: 6.4.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) ley: specifier: 0.8.1 version: 0.8.1 @@ -244,23 +244,23 @@ importers: specifier: 4.21.0 version: 4.21.0 typescript: - specifier: 5.9.3 - version: 5.9.3 + specifier: 6.0.3 + version: 6.0.3 vite: - specifier: 8.0.4 - version: 8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.8 + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) vite-node: specifier: 6.0.0 - version: 6.0.0(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + version: 6.0.0(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) vite-plugin-babel: specifier: 1.6.0 - version: 1.6.0(@babel/core@7.29.0)(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 1.6.0(@babel/core@7.29.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: - specifier: 4.1.2 - version: 4.1.2(@types/node@25.5.0)(@vitest/browser-playwright@4.1.2)(@vitest/ui@4.1.2)(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 4.1.4 + version: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/ui@4.1.4)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) vitest-browser-react: specifier: 2.2.0 - version: 2.2.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.2) + version: 2.2.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.4) packages: @@ -287,133 +287,133 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-s3@3.1019.0': - resolution: {integrity: sha512-0pb9x7PPhS4oEi4c0rL3vzQQoXA4cWKtPuGga/UfVYLZ68yrqdq0NDKg0fr55qzdhNvWFCpmGx73g9Iyy03kkA==} + '@aws-sdk/client-s3@3.1030.0': + resolution: {integrity: sha512-sgGb4ub0JXnHaXnok5td7A1KGwENFPwOrwgzvpkeWq9w16Sl7x2KhYtVl+Fdd/7LAvaEtm3HqrYtNmm2d0OXmQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.973.25': - resolution: {integrity: sha512-TNrx7eq6nKNOO62HWPqoBqPLXEkW6nLZQGwjL6lq1jZtigWYbK1NbCnT7mKDzbLMHZfuOECUt3n6CzxjUW9HWQ==} + '@aws-sdk/core@3.974.3': + resolution: {integrity: sha512-W3aJJm2clu8OmsrwMOMnfof13O6LGnbknnZIQeSRbxjqKah2nVvkjbUBBZVhWrt08KC69H7WsINTdrxC/2SXQw==} engines: {node: '>=20.0.0'} - '@aws-sdk/crc64-nvme@3.972.5': - resolution: {integrity: sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==} + '@aws-sdk/crc64-nvme@3.972.7': + resolution: {integrity: sha512-QUagVVBbC8gODCF6e1aV0mE2TXWB9Opz4k8EJFdNrujUVQm5R4AjJa1mpOqzwOuROBzqJU9zawzig7M96L8Ejg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.23': - resolution: {integrity: sha512-EamaclJcCEaPHp6wiVknNMM2RlsPMjAHSsYSFLNENBM8Wz92QPc6cOn3dif6vPDQt0Oo4IEghDy3NMDCzY/IvA==} + '@aws-sdk/credential-provider-env@3.972.29': + resolution: {integrity: sha512-rf+AlUxgTeSzQ/4zoS0D+Bt7XvgpY48PnWG8Yg/N9fdMgyK2Jaqa+6tLZp4MYMIMHkGrfAxnbSeb2YLMGFMg6g==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.25': - resolution: {integrity: sha512-qPymamdPcLp6ugoVocG1y5r69ScNiRzb0hogX25/ij+Wz7c7WnsgjLTaz7+eB5BfRxeyUwuw5hgULMuwOGOpcw==} + '@aws-sdk/credential-provider-http@3.972.31': + resolution: {integrity: sha512-TR2/lQ3qKFj2EOrsiASzemsNEz2uzZ/SUBf48+U4Cr9a/FZlHfH/hwAeBJNBp1gMyJNxROJZhT3dn1cO+jnYfQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.972.26': - resolution: {integrity: sha512-xKxEAMuP6GYx2y5GET+d3aGEroax3AgGfwBE65EQAUe090lzyJ/RzxPX9s8v7Z6qAk0XwfQl+LrmH05X7YvTeg==} + '@aws-sdk/credential-provider-ini@3.972.33': + resolution: {integrity: sha512-UwdbJbOrgnOxZbshaNZ4DzX35h5wQd33MNYTGzWhN3ORG9lG9KQbDX6l6tDJSAdaGTktJoZPSritmUoW1rYkRA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.26': - resolution: {integrity: sha512-EFcM8RM3TUxnZOfMJo++3PnyxFu1fL/huzmn3Vh+8IWRgqZawUD3cRwwOr+/4bE9DpyHaLOWFAjY0lfK5X9ZkQ==} + '@aws-sdk/credential-provider-login@3.972.33': + resolution: {integrity: sha512-WyZuPVoDM1HGNl41eVg8HSSXIB+FGkuuK63GhDbh4TMdfWU03AciWvF/QqOVWvJtWVYaLddANJ+aUklVr2ieuw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.27': - resolution: {integrity: sha512-jXpxSolfFnPVj6GCTtx3xIdWNoDR7hYC/0SbetGZxOC9UnNmipHeX1k6spVstf7eWJrMhXNQEgXC0pD1r5tXIg==} + '@aws-sdk/credential-provider-node@3.972.34': + resolution: {integrity: sha512-sPcisURibKU4x0PCWJkWF1KJYm49Cph9dCn/PAnG5FU0wq5Id3g2v7RuEWAtNlKv1Af4gUJYBVGOeNpSEEx41A==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.23': - resolution: {integrity: sha512-IL/TFW59++b7MpHserjUblGrdP5UXy5Ekqqx1XQkERXBFJcZr74I7VaSrQT5dxdRMU16xGK4L0RQ5fQG1pMgnA==} + '@aws-sdk/credential-provider-process@3.972.29': + resolution: {integrity: sha512-DURisqWS3bUgiwMXTmzymVNGlcRW0FnbPZ3SZknhmxnCXm3n9idkTJ6T+Uir359KRKtJNFLRViskk8HsSVLi1w==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.972.26': - resolution: {integrity: sha512-c6ghvRb6gTlMznWhGxn/bpVCcp0HRaz4DobGVD9kI4vwHq186nU2xN/S7QGkm0lo0H2jQU8+dgpUFLxfTcwCOg==} + '@aws-sdk/credential-provider-sso@3.972.33': + resolution: {integrity: sha512-9y9obU4IQWru9f+NiiscUeyCe5ZmQav4FKEb1qfUNrik/C3BzBGUnHQWyPEyXjOX9cb+vx1TYx0qZBtinKdzTA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.26': - resolution: {integrity: sha512-cXcS3+XD3iwhoXkM44AmxjmbcKueoLCINr1e+IceMmCySda5ysNIfiGBGe9qn5EMiQ9Jd7pP0AGFtcd6OV3Lvg==} + '@aws-sdk/credential-provider-web-identity@3.972.33': + resolution: {integrity: sha512-RazhlN0YAkna2T2p2v4YuuRlVBVRNo8V0SL+9JePTWDndEUAeOBAjYeQfAMbtDyCh120+zA0Op6V0jS4dw2+iw==} engines: {node: '>=20.0.0'} - '@aws-sdk/lib-storage@3.1019.0': - resolution: {integrity: sha512-btVQ5wp4VWzP1A1RpCB1MYGyblMxj+uDs07tQa8LGQEml0QTdKxfsYypcGMKGlsV0PN1es3XGnMSjlfWSFO0kQ==} + '@aws-sdk/lib-storage@3.1030.0': + resolution: {integrity: sha512-1Hn+m1sioy3OMvF/I1uDz9QjpqcE3QSsHvz0Y0UXyMthNCpvAEvN4qO9RWBDGfVqddY1Flsp0rfvjwYP4KVr+w==} engines: {node: '>=20.0.0'} peerDependencies: - '@aws-sdk/client-s3': ^3.1019.0 + '@aws-sdk/client-s3': ^3.1030.0 - '@aws-sdk/middleware-bucket-endpoint@3.972.8': - resolution: {integrity: sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw==} + '@aws-sdk/middleware-bucket-endpoint@3.972.10': + resolution: {integrity: sha512-Vbc2frZH7wXlMNd+ZZSXUEs/l1Sv8Jj4zUnIfwrYF5lwaLdXHZ9xx4U3rjUcaye3HRhFVc+E5DbBxpRAbB16BA==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-expect-continue@3.972.8': - resolution: {integrity: sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==} + '@aws-sdk/middleware-expect-continue@3.972.10': + resolution: {integrity: sha512-2Yn0f1Qiq/DjxYR3wfI3LokXnjOhFM7Ssn4LTdFDIxRMCE6I32MAsVnhPX1cUZsuVA9tiZtwwhlSLAtFGxAZlQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-flexible-checksums@3.974.5': - resolution: {integrity: sha512-SPSvF0G1t8m8CcB0L+ClNFszzQOvXaxmRj25oRWDf6aU+TuN2PXPFAJ9A6lt1IvX4oGAqqbTdMPTYs/SSHUYYQ==} + '@aws-sdk/middleware-flexible-checksums@3.974.11': + resolution: {integrity: sha512-jTrJFs4SMs9xjih45+QHtU79piovA6CAlofMt4jeknN5ef9zsVEHDtuwCnEe/3eANWewa9fd6Tvc54xEPpQ3RA==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-host-header@3.972.8': - resolution: {integrity: sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==} + '@aws-sdk/middleware-host-header@3.972.10': + resolution: {integrity: sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-location-constraint@3.972.8': - resolution: {integrity: sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw==} + '@aws-sdk/middleware-location-constraint@3.972.10': + resolution: {integrity: sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-logger@3.972.8': - resolution: {integrity: sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==} + '@aws-sdk/middleware-logger@3.972.10': + resolution: {integrity: sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-recursion-detection@3.972.9': - resolution: {integrity: sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==} + '@aws-sdk/middleware-recursion-detection@3.972.11': + resolution: {integrity: sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-sdk-s3@3.972.26': - resolution: {integrity: sha512-5q7UGSTtt7/KF0Os8wj2VZtlLxeWJVb0e2eDrDJlWot2EIxUNKDDMPFq/FowUqrwZ40rO2bu6BypxaKNvQhI+g==} + '@aws-sdk/middleware-sdk-s3@3.972.32': + resolution: {integrity: sha512-dc2O2x0V5pGJhmdQYQveUIFtMZsur7GrGuSgoKM4oQJuEcfvwnJ3sj+ip6WnxR5l6TrX5zkl4KgcgswOy3wAzQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-ssec@3.972.8': - resolution: {integrity: sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==} + '@aws-sdk/middleware-ssec@3.972.10': + resolution: {integrity: sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-user-agent@3.972.26': - resolution: {integrity: sha512-AilFIh4rI/2hKyyGN6XrB0yN96W2o7e7wyrPWCM6QjZM1mcC/pVkW3IWWRvuBWMpVP8Fg+rMpbzeLQ6dTM4gig==} + '@aws-sdk/middleware-user-agent@3.972.33': + resolution: {integrity: sha512-mqtT3Fo7xanWMk2SbAcKLGGI/q1GHWNrExBj7cnWP2W2mkTMheXB4ntJvwPZ1UxPrQobrsv2dWFXmaOJeSOiDg==} engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.996.16': - resolution: {integrity: sha512-L7Qzoj/qQU1cL5GnYLQP5LbI+wlLCLoINvcykR3htKcQ4tzrPf2DOs72x933BM7oArYj1SKrkb2lGlsJHIic3g==} + '@aws-sdk/nested-clients@3.997.1': + resolution: {integrity: sha512-Afc9hc2WZs3X4Jb8dnxyuYiZsLoWRO51roTCRf497gPnAKN2WRdXANu1vaVCTzwnDMOYFXb/cYv4ZSjxqAqcKA==} engines: {node: '>=20.0.0'} - '@aws-sdk/region-config-resolver@3.972.10': - resolution: {integrity: sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ==} + '@aws-sdk/region-config-resolver@3.972.13': + resolution: {integrity: sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==} engines: {node: '>=20.0.0'} - '@aws-sdk/signature-v4-multi-region@3.996.14': - resolution: {integrity: sha512-4nZSrBr1NO+48HCM/6BRU8mnRjuHZjcpziCvLXZk5QVftwWz5Mxqbhwdz4xf7WW88buaTB8uRO2MHklSX1m0vg==} + '@aws-sdk/signature-v4-multi-region@3.996.20': + resolution: {integrity: sha512-MEj6DhEcaO8RgVtFCJ+xpCQnZC3Iesr09avdY75qkMQfckQULu447IegK7Rs1MCGerVBfKnJQ4q+pQq9hI5lng==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1019.0': - resolution: {integrity: sha512-OF+2RfRmUKyjzrRWlDcyju3RBsuqcrYDQ8TwrJg8efcOotMzuZN4U9mpVTIdATpmEc4lWNZBMSjPzrGm6JPnAQ==} + '@aws-sdk/token-providers@3.1034.0': + resolution: {integrity: sha512-8E+KGcD4ET0H9FXJ2/ZWbfFnQNYEkTZZYJxAs1lkdJlve1AYuqaydInIFfvNgoz5GbYtzbK8/ugsSMu5wPm6kA==} engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.973.6': - resolution: {integrity: sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==} + '@aws-sdk/types@3.973.8': + resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} engines: {node: '>=20.0.0'} '@aws-sdk/util-arn-parser@3.972.3': resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-endpoints@3.996.5': - resolution: {integrity: sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==} + '@aws-sdk/util-endpoints@3.996.8': + resolution: {integrity: sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==} engines: {node: '>=20.0.0'} '@aws-sdk/util-locate-window@3.965.5': resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-user-agent-browser@3.972.8': - resolution: {integrity: sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==} + '@aws-sdk/util-user-agent-browser@3.972.10': + resolution: {integrity: sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==} - '@aws-sdk/util-user-agent-node@3.973.12': - resolution: {integrity: sha512-8phW0TS8ntENJgDcFewYT/Q8dOmarpvSxEjATu2GUBAutiHr++oEGCiBUwxslCMNvwW2cAPZNT53S/ym8zm/gg==} + '@aws-sdk/util-user-agent-node@3.973.19': + resolution: {integrity: sha512-ZAfHjpzdbrzkAftC139JoYGfXzDh5HY+AxRzw8pGJ8cULsf+l721sKAMK8mV5NvRETaW/BwghSwQhGgoNgrxMw==} engines: {node: '>=20.0.0'} peerDependencies: aws-crt: '>=1.0.0' @@ -421,8 +421,8 @@ packages: aws-crt: optional: true - '@aws-sdk/xml-builder@3.972.16': - resolution: {integrity: sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==} + '@aws-sdk/xml-builder@3.972.18': + resolution: {integrity: sha512-BMDNVG1ETXRhl1tnisQiYBef3RShJ1kfZA7x7afivTFMLirfHNTb6U71K569HNXhSXbQZsweHvSDZ6euBw8hPA==} engines: {node: '>=20.0.0'} '@aws/lambda-invoke-store@0.2.4': @@ -562,59 +562,59 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.4.9': - resolution: {integrity: sha512-wvZW92FrwitTcacvCBT8xdAbfbxWfDLwjYMmU3djjqQTh7Ni4ZdiWIT/x5VcZ+RQuxiKzIOzi5D+dcyJDFZMsA==} + '@biomejs/biome@2.4.11': + resolution: {integrity: sha512-nWxHX8tf3Opb/qRgZpBbsTOqOodkbrkJ7S+JxJAruxOReaDPPmPuLBAGQ8vigyUgo0QBB+oQltNEAvalLcjggA==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.4.9': - resolution: {integrity: sha512-d5G8Gf2RpH5pYwiHLPA+UpG3G9TLQu4WM+VK6sfL7K68AmhcEQ9r+nkj/DvR/GYhYox6twsHUtmWWWIKfcfQQA==} + '@biomejs/cli-darwin-arm64@2.4.11': + resolution: {integrity: sha512-wOt+ed+L2dgZanWyL6i29qlXMc088N11optzpo10peayObBaAshbTcxKUchzEMp9QSY8rh5h6VfAFE3WTS1rqg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.4.9': - resolution: {integrity: sha512-LNCLNgqDMG7BLdc3a8aY/dwKPK7+R8/JXJoXjCvZh2gx8KseqBdFDKbhrr7HCWF8SzNhbTaALhTBoh/I6rf9lA==} + '@biomejs/cli-darwin-x64@2.4.11': + resolution: {integrity: sha512-gZ6zR8XmZlExfi/Pz/PffmdpWOQ8Qhy7oBztgkR8/ylSRyLwfRPSadmiVCV8WQ8PoJ2MWUy2fgID9zmtgUUJmw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.4.9': - resolution: {integrity: sha512-8RCww5xnPn2wpK4L/QDGDOW0dq80uVWfppPxHIUg6mOs9B6gRmqPp32h1Ls3T8GnW8Wo5A8u7vpTwz4fExN+sw==} + '@biomejs/cli-linux-arm64-musl@2.4.11': + resolution: {integrity: sha512-+Sbo1OAmlegtdwqFE8iOxFIWLh1B3OEgsuZfBpyyN/kWuqZ8dx9ZEes6zVnDMo+zRHF2wLynRVhoQmV7ohxl2Q==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.4.9': - resolution: {integrity: sha512-4adnkAUi6K4C/emPRgYznMOcLlUqZdXWM6aIui4VP4LraE764g6Q4YguygnAUoxKjKIXIWPteKMgRbN0wsgwcg==} + '@biomejs/cli-linux-arm64@2.4.11': + resolution: {integrity: sha512-avdJaEElXrKceK0va9FkJ4P5ci3N01TGkc6ni3P8l3BElqbOz42Wg2IyX3gbh0ZLEd4HVKEIrmuVu/AMuSeFFA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.4.9': - resolution: {integrity: sha512-5TD+WS9v5vzXKzjetF0hgoaNFHMcpQeBUwKKVi3JbG1e9UCrFuUK3Gt185fyTzvRdwYkJJEMqglRPjmesmVv4A==} + '@biomejs/cli-linux-x64-musl@2.4.11': + resolution: {integrity: sha512-bexd2IklK7ZgPhrz6jXzpIL6dEAH9MlJU1xGTrypx+FICxrXUp4CqtwfiuoDKse+UlgAlWtzML3jrMqeEAHEhA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.4.9': - resolution: {integrity: sha512-L10na7POF0Ks/cgLFNF1ZvIe+X4onLkTi5oP9hY+Rh60Q+7fWzKDDCeGyiHUFf1nGIa9dQOOUPGe2MyYg8nMSQ==} + '@biomejs/cli-linux-x64@2.4.11': + resolution: {integrity: sha512-TagWV0iomp5LnEnxWFg4nQO+e52Fow349vaX0Q/PIcX6Zhk4GGBgp3qqZ8PVkpC+cuehRctMf3+6+FgQ8jCEFQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.4.9': - resolution: {integrity: sha512-aDZr0RBC3sMGJOU10BvG7eZIlWLK/i51HRIfScE2lVhfts2dQTreowLiJJd+UYg/tHKxS470IbzpuKmd0MiD6g==} + '@biomejs/cli-win32-arm64@2.4.11': + resolution: {integrity: sha512-RJhaTnY8byzxDt4bDVb7AFPHkPcjOPK3xBip4ZRTrN3TEfyhjLRm3r3mqknqydgVTB74XG8l4jMLwEACEeihVg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.4.9': - resolution: {integrity: sha512-NS4g/2G9SoQ4ktKtz31pvyc/rmgzlcIDCGU/zWbmHJAqx6gcRj2gj5Q/guXhoWTzCUaQZDIqiCQXHS7BcGYc0w==} + '@biomejs/cli-win32-x64@2.4.11': + resolution: {integrity: sha512-A8D3JM/00C2KQgUV3oj8Ba15EHEYwebAGCy5Sf9GAjr5Y3+kJIYOiESoqRDeuRZueuMdCsbLZIUqmPhpYXJE9A==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -657,14 +657,14 @@ packages: resolution: {integrity: sha512-VLhlvEPDJ0Sd0pE6sAYTQkIqZCXVonaWlgRJIQQHzfjTXCadF77qqHj5NxaPSc4wCul0DJO/0MnejVqJAXUiRg==} engines: {node: '>=20.0.0'} - '@emnapi/core@1.9.1': - resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} - '@emnapi/runtime@1.9.1': - resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} - '@emnapi/wasi-threads@1.2.0': - resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} '@epic-web/cachified@5.6.2': resolution: {integrity: sha512-dxXZBBWDTZRelGE805m1ZMua4SGwlc/tGkEAxTU4bQMlfny+zLzLbuX1UF1eqJrO2BtLekchLIA4iDPRxyk+EQ==} @@ -678,156 +678,312 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.27.4': resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.27.4': resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.27.4': resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.27.4': resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.27.4': resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.27.4': resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.4': resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.27.4': resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.27.4': resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.27.4': resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.27.4': resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.27.4': resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.27.4': resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.27.4': resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.27.4': resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.27.4': resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.27.4': resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.4': resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.27.4': resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.4': resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.27.4': resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.27.4': resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.27.4': resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.27.4': resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.27.4': resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@faker-js/faker@10.4.0': resolution: {integrity: sha512-sDBWI3yLy8EcDzgobvJTWq1MJYzAkQdpjXuPukga9wXonhpMRvd1Izuo2Qgwey2OiEoRIBr35RMU9HJRoOHzpw==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} @@ -847,47 +1003,23 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - '@formatjs/bigdecimal@0.2.0': - resolution: {integrity: sha512-GeaxHZbUoYvHL9tC5eltHLs+1zU70aPw0s7LwqgktIzF5oMhNY4o4deEtusJMsq7WFJF3Ye2zQEzdG8beVk73w==} + '@formatjs/fast-memoize@3.1.2': + resolution: {integrity: sha512-vPnriihkfK0lzoQGaXq+qXH23VsYyansRTkTgo2aTG0k1NjLFyZimFVdfj4C9JkSE5dm7CEngcQ5TTc1yAyBfQ==} - '@formatjs/ecma402-abstract@2.3.6': - resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} + '@formatjs/intl-durationformat@0.10.4': + resolution: {integrity: sha512-SUS4xT3GX43/thXTZojOXMNMB1zUIlLC+3mdyJga7evrgV4jvmgycX9bXfmMNDu7c2TC9gnXySUpUcxMghZrKg==} - '@formatjs/ecma402-abstract@3.2.0': - resolution: {integrity: sha512-dHnqHgBo6GXYGRsepaE1wmsC2etaivOWd5VaJstZd+HI2zR3DCUjbDVZRtoPGkkXZmyHvBwrdEUuqfvzhF/DtQ==} + '@formatjs/intl-localematcher@0.8.3': + resolution: {integrity: sha512-pHUjWb9NuhnMs8+PxQdzBtZRFJHlGhrURGAbm6Ltwl82BFajeuiIR3jblSa7ia3r62rXe/0YtVpUG3xWr5bFCA==} - '@formatjs/fast-memoize@2.2.7': - resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + '@internationalized/date@3.12.1': + resolution: {integrity: sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==} - '@formatjs/fast-memoize@3.1.1': - resolution: {integrity: sha512-CbNbf+tlJn1baRnPkNePnBqTLxGliG6DDgNa/UtV66abwIjwsliPMOt0172tzxABYzSuxZBZfcp//qI8AvBWPg==} + '@internationalized/number@3.6.6': + resolution: {integrity: sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==} - '@formatjs/icu-messageformat-parser@2.11.4': - resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} - - '@formatjs/icu-skeleton-parser@1.8.16': - resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} - - '@formatjs/intl-durationformat@0.10.3': - resolution: {integrity: sha512-xRS3GaOlsQLwz0n56SvaddwEnl2NLPKBvYg2M32ak/27dodmVxFJz3j7Nqj7EwKyHTu3f/e+BeoKPrIDUSXTuQ==} - - '@formatjs/intl-localematcher@0.6.2': - resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} - - '@formatjs/intl-localematcher@0.8.2': - resolution: {integrity: sha512-q05KMYGJLyqFNFtIb8NhWLF5X3aK/k0wYt7dnRFuy6aLQL+vUwQ1cg5cO4qawEiINybeCPXAWlprY2mSBjSXAQ==} - - '@internationalized/date@3.12.0': - resolution: {integrity: sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==} - - '@internationalized/message@3.1.8': - resolution: {integrity: sha512-Rwk3j/TlYZhn3HQ6PyXUV0XP9Uv42jqZGNegt0BXlxjE6G3+LwHjbQZAGHhCnCPdaA6Tvd3ma/7QzLlLkJxAWA==} - - '@internationalized/number@3.6.5': - resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==} - - '@internationalized/string@3.2.7': - resolution: {integrity: sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A==} + '@internationalized/string@3.2.8': + resolution: {integrity: sha512-NdbMQUSfXLYIQol5VyMtinm9pZDciiMfN7RtmSuSB78io1hqwJ0naYfxyW6vgxWBkzWymQa/3uLDlbfmshtCaA==} '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -911,11 +1043,8 @@ packages: '@mjackson/node-fetch-server@0.2.0': resolution: {integrity: sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==} - '@napi-rs/wasm-runtime@1.1.1': - resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} - - '@napi-rs/wasm-runtime@1.1.2': - resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -1080,8 +1209,8 @@ packages: '@oxc-project/types@0.121.0': resolution: {integrity: sha512-CGtOARQb9tyv7ECgdAlFxi0Fv7lmzvmlm2rpD/RdijOO9rfk/JvB1CjT8EnoD+tjna/IYgKKw3IV7objRb+aYw==} - '@oxc-project/types@0.122.0': - resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@oxc-project/types@0.124.0': + resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} '@oxc-resolver/binding-android-arm-eabi@11.19.1': resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} @@ -1191,8 +1320,8 @@ packages: cpu: [x64] os: [win32] - '@playwright/test@1.58.2': - resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} engines: {node: '>=18'} hasBin: true @@ -1571,307 +1700,16 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@react-aria/autocomplete@3.0.0-rc.6': - resolution: {integrity: sha512-uymUNJ8NW+dX7lmgkHE+SklAbxwktycAJcI5lBBw6KPZyc0EdMHC+/Fc5CUz3enIAhNwd2oxxogcSHknquMzQA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/breadcrumbs@3.5.32': - resolution: {integrity: sha512-S61vh5DJ2PXiXUwD7gk+pvS/b4VPrc3ZJOUZ0yVRLHkVESr5LhIZH+SAVgZkm1lzKyMRG+BH+fiRH/DZRSs7SA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/button@3.14.5': - resolution: {integrity: sha512-ZuLx+wQj9VQhH9BYe7t0JowmKnns2XrFHFNvIVBb5RwxL+CIycIOL7brhWKg2rGdxvlOom7jhVbcjSmtAaSyaQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/calendar@3.9.5': - resolution: {integrity: sha512-k0kvceYdZZu+DoeqephtlmIvh1CxqdFyoN52iqVzTz9O0pe5Xfhq7zxPGbeCp4pC61xzp8Lu/6uFA/YNfQQNag==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/checkbox@3.16.5': - resolution: {integrity: sha512-ZhUT7ELuD52hb+Zpzw0ElLQiVOd5sKYahrh+PK3vq13Wk5TedBscALpjuXetI4pwFfdmAM1Lhgcsrd8+6AmyvA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/collections@3.0.3': - resolution: {integrity: sha512-lbC5DEbHeVFvVr4ke9y8D9Nynnr8G8UjVEBoFGRylpAaScU7SX1TN84QI+EjMbsdZ0/5P2H7gUTS+MYd+6U3Rg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/color@3.1.5': - resolution: {integrity: sha512-eysWdBRzE8WDhBzh1nfjyUgzseMokXGHjIoJo880T7IPJ8tTavfQni49pU1B2qWrNOWPyrwx4Bd9pzHyboxJSA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/combobox@3.15.0': - resolution: {integrity: sha512-qSjQTFwKl3x1jCP2NRSJ6doZqAp6c2GTfoiFwWjaWg1IewwLsglaW6NnzqRDFiqFbDGgXPn4MqtC1VYEJ3NEjA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/datepicker@3.16.1': - resolution: {integrity: sha512-6BltCVWt09yefTkGjb2gViGCwoddx9HKJiZbY9u6Es/Q+VhwNJQRtczbnZ3K32p262hIknukNf/5nZaCOI1AKA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/dialog@3.5.34': - resolution: {integrity: sha512-/x53Q5ynpW5Kv9637WYu7SrDfj3woSp6jJRj8l6teGnWW/iNZWYJETgzHfbxx+HPKYATCZesRoIeO2LnYIXyEA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/disclosure@3.1.3': - resolution: {integrity: sha512-S3k7Wqrj+x0sWcP88Z1stSr5TIZmKEmx2rU7RB1O1/jPpbw5mgKnjtiriOlTh+kwdK11FkeqgxyHzAcBAR+FMQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/dnd@3.11.6': - resolution: {integrity: sha512-4YLHUeYJleF+moAYaYt8UZqujudPvpoaHR+QMkWIFzhfridVUhCr6ZjGWrzpSZY3r68k46TG7YCsi4IEiNnysw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/focus@3.21.5': - resolution: {integrity: sha512-V18fwCyf8zqgJdpLQeDU5ZRNd9TeOfBbhLgmX77Zr5ae9XwaoJ1R3SFJG1wCJX60t34AW+aLZSEEK+saQElf3Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/form@3.1.5': - resolution: {integrity: sha512-BWlONgHn8hmaMkcS6AgMSLQeNqVBwqPNLhdqjDO/PCfzvV7O8NZw/dFeIzJwfG4aBfSpbHHRdXGdfrk3d8dylQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/grid@3.14.8': - resolution: {integrity: sha512-X6rRFKDu/Kh6Sv8FBap3vjcb+z4jXkSOwkYnexIJp5kMTo5/Dqo55cCBio5B70Tanfv32Ev/6SpzYG7ryxnM9w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/gridlist@3.14.4': - resolution: {integrity: sha512-C/SbwC0qagZatoBrCjx8iZUex9apaJ8o8iRJ9eVHz0cpj7mXg6HuuotYGmDy9q67A2hve4I693RM1Cuwqwm+PQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/i18n@3.12.16': - resolution: {integrity: sha512-Km2CAz6MFQOUEaattaW+2jBdWOHUF8WX7VQoNbjlqElCP58nSaqi9yxTWUDRhAcn8/xFUnkFh4MFweNgtrHuEA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/interactions@3.27.1': - resolution: {integrity: sha512-M3wLpTTmDflI0QGNK0PJNUaBXXfeBXue8ZxLMngfc1piHNiH4G5lUvWd9W14XVbqrSCVY8i8DfGrNYpyyZu0tw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/label@3.7.25': - resolution: {integrity: sha512-oNK3Pqj4LDPwEbQaoM/uCip4QvQmmwGOh08VeW+vzSi6TAwf+KoWTyH/tiAeB0CHWNDK0k3e1iTygTAt4wzBmg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/landmark@3.0.10': - resolution: {integrity: sha512-GpNjJaI8/a6WxYDZgzTCLYSzPM6xp2pxCIQ4udiGbTCtxx13Trmm0cPABvPtzELidgolCf05em9Phr+3G0eE8A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/link@3.8.9': - resolution: {integrity: sha512-UaAFBfs84/Qq6TxlMWkREqqNY6SFLukot+z2Aa1kC+VyStv1kWG6sE5QLjm4SBn1Q3CGRsefhB/5+taaIbB4Pw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/listbox@3.15.3': - resolution: {integrity: sha512-C6YgiyrHS5sbS5UBdxGMhEs+EKJYotJgGVtl9l0ySXpBUXERiHJWLOyV7a8PwkUOmepbB4FaLD7Y9EUzGkrGlw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/live-announcer@3.4.4': - resolution: {integrity: sha512-PTTBIjNRnrdJOIRTDGNifY2d//kA7GUAwRFJNOEwSNG4FW+Bq9awqLiflw0JkpyB0VNIwou6lqKPHZVLsGWOXA==} - - '@react-aria/menu@3.21.0': - resolution: {integrity: sha512-CKTVZ4izSE1eKIti6TbTtzJAUo+WT8O4JC0XZCYDBpa0f++lD19Kz9aY+iY1buv5xGI20gAfpO474E9oEd4aQA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/meter@3.4.30': - resolution: {integrity: sha512-ZmANKW7s/Z4QGylHi46nhwtQ47T1bfMsU9MysBu7ViXXNJ03F4b6JXCJlKL5o2goQ3NbfZ68GeWamIT0BWSgtw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/numberfield@3.12.5': - resolution: {integrity: sha512-Fi41IUWXEHLFIeJ/LHuZ9Azs8J/P563fZi37GSBkIq5P1pNt1rPgJJng5CNn4KsHxwqadTRUlbbZwbZraWDtRg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/overlays@3.31.2': - resolution: {integrity: sha512-78HYI08r6LvcfD34gyv19ArRIjy1qxOKuXl/jYnjLDyQzD4pVb634IQWcm0zt10RdKgyuH6HTqvuDOgZTLet7Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/progress@3.4.30': - resolution: {integrity: sha512-S6OWVGgluSWYSd/A6O8CVjz83eeMUfkuWSra0ewAV9bmxZ7TP9pUmD3bGdqHZEl97nt5vHGjZ3eq/x8eCmzKhA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/radio@3.12.5': - resolution: {integrity: sha512-8CCJKJzfozEiWBPO9QAATG1rBGJEJ+xoqvHf9LKU2sPFGsA2/SRnLs6LB9fCG5R3spvaK1xz0any1fjWPl7x8A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/searchfield@3.8.12': - resolution: {integrity: sha512-kYlUHD/+mWzNroHoR8ojUxYBoMviRZn134WaKPFjfNUGZDOEuh4XzOoj+cjdJfe6N3mwTaYu6rJQtunSHIAfhA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/select@3.17.3': - resolution: {integrity: sha512-u0UFWw0S7q9oiSbjetDpRoLLIcC+L89uYlm+YfCrdT8ntbQgABNiJRxdVvxnhR0fR6MC9ASTTvuQnNHNn52+1A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/selection@3.27.2': - resolution: {integrity: sha512-GbUSSLX/ciXix95KW1g+SLM9np7iXpIZrFDSXkC6oNx1uhy18eAcuTkeZE25+SY5USVUmEzjI3m/3JoSUcebbg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/separator@3.4.16': - resolution: {integrity: sha512-RCUtQhDGnPxKzyG8KM79yOB0fSiEf8r/rxShidOVnGLiBW2KFmBa22/Gfc4jnqg/keN3dxvkSGoqmeXgctyp6g==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/slider@3.8.5': - resolution: {integrity: sha512-gqkJxznk141mE0JamXF5CXml9PDbPkBz8dyKlihtWHWX4yhEbVYdC9J0otE7iCR3zx69Bm7WHoTGL0BsdpKzVA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/spinbutton@3.7.2': - resolution: {integrity: sha512-adjE1wNCWlugvAtVXlXWPtIG9JWurEgYVn1Eeyh19x038+oXGvOsOAoKCXM+SnGleTWQ9J7pEZITFoEI3cVfAw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/ssr@3.9.10': - resolution: {integrity: sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==} - engines: {node: '>= 12'} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/switch@3.7.11': - resolution: {integrity: sha512-dYVX71HiepBsKyeMaQgHbhqI+MQ3MVoTd5EnTbUjefIBnmQZavYj1/e4NUiUI4Ix+/C0HxL8ibDAv4NlSW3eLQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/table@3.17.11': - resolution: {integrity: sha512-GkYmWPiW3OM+FUZxdS33teHXHXde7TjHuYgDDaG9phvg6cQTQjGilJozrzA3OfftTOq5VB8XcKTIQW3c0tpYsQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/tabs@3.11.1': - resolution: {integrity: sha512-3Ppz7yaEDW9L7p9PE9yNOl5caLwNnnLQqI+MX/dwbWlw9HluHS7uIjb21oswNl6UbSxAWyENOka45+KN4Fkh7A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/tag@3.8.1': - resolution: {integrity: sha512-VonpO++F8afXGDWc9VUxAc2wefyJpp1n9OGpbnB7zmqWiuPwO/RixjUdcH7iJkiC4vADwx9uLnhyD6kcwGV2ig==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/textfield@3.18.5': - resolution: {integrity: sha512-ttwVSuwoV3RPaG2k2QzEXKeQNQ3mbdl/2yy6I4Tjrn1ZNkYHfVyJJ26AjenfSmj1kkTQoSAfZ8p+7rZp4n0xoQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/toast@3.0.11': - resolution: {integrity: sha512-2DjZjBAvm8/CWbnZ6s7LjkYCkULKtjMve6GvhPTq98AthuEDLEiBvM1wa3xdecCRhZyRT1g6DXqVca0EfZ9fJA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/toggle@3.12.5': - resolution: {integrity: sha512-XXVFLzcV8fr9mz7y/wfxEAhWvaBZ9jSfhCMuxH2bsivO7nTcMJ1jb4g2xJNwZgne17bMWNc7mKvW5dbsdlI6BA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/toolbar@3.0.0-beta.24': - resolution: {integrity: sha512-B2Rmpko7Ghi2RbNfsGdbR7I+RQBDhPGVE4bU3/EwHz+P/vNe5LyGPTeSwqaOMsQTF9lKNCkY8424dVTCr6RUMg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/tooltip@3.9.2': - resolution: {integrity: sha512-VrgkPwHiEnAnBhoQ4W7kfry/RfVuRWrUPaJSp0+wKM6u0gg2tmn7OFRDXTxBAm/omQUguIdIjRWg7sf3zHH82A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/tree@3.1.7': - resolution: {integrity: sha512-C54yH5NmsOFa2Q+cg6B1BPr5KUlU9vLIoBnVrgrH237FRSXQPIbcM4VpmITAHq1VR7w6ayyS1hgTwFxo67ykWQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/utils@3.33.1': - resolution: {integrity: sha512-kIx1Sj6bbAT0pdqCegHuPanR9zrLn5zMRiM7LN12rgRf55S19ptd9g3ncahArifYTRkfEU9VIn+q0HjfMqS9/w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/virtualizer@4.1.13': - resolution: {integrity: sha512-d5KS+p8GXGNRbGPRE/N6jtth3et3KssQIz52h2+CAoAh7C3vvR64kkTaGdeywClvM+fSo8FxJuBrdfQvqC2ktQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-aria/visually-hidden@3.8.31': - resolution: {integrity: sha512-RTOHHa4n56a9A3criThqFHBifvZoV71+MCkSuNP2cKO662SUWjqKkd0tJt/mBRMEJPkys8K7Eirp6T8Wt5FFRA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-router/dev@7.14.0': - resolution: {integrity: sha512-/1ElF4lDTEIZ/rbEdlj6MmRY9ERRDyaTswWes+3pbqEKF2r/ixSzACueHWIfV9ULg/x5/weCvSexDD9f16ObwA==} + '@react-router/dev@7.14.1': + resolution: {integrity: sha512-ZBEwods1TxqPVY2SrXDuDCfoaE5VoTMBYrfa/+3MesprY3foSo1jhin9mh4FwmXPXhhmDYKXi2z5UR+oMj8Qjg==} engines: {node: '>=20.0.0'} hasBin: true peerDependencies: - '@react-router/serve': ^7.14.0 + '@react-router/serve': ^7.14.1 '@vitejs/plugin-rsc': ~0.5.21 - react-router: ^7.14.0 + react-router: ^7.14.1 react-server-dom-webpack: ^19.2.3 - typescript: ^5.1.0 + typescript: ^5.1.0 || ^6.0.0 vite: ^5.1.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 wrangler: ^3.28.2 || ^4.0.0 peerDependenciesMeta: @@ -1886,326 +1724,36 @@ packages: wrangler: optional: true - '@react-router/express@7.14.0': - resolution: {integrity: sha512-isrPotskov4KJ/v0GvTACaXWua/3iPs71717iZZfxix77MqVo1uW7jtuXc8ChJRRWSHZOv2NFvIOYCUFXzmNJA==} + '@react-router/express@7.14.1': + resolution: {integrity: sha512-XX/R+/JIIbwTfaXHz1WAJbiPfkd56y7PN9Czg7h6Tvos9TZlmMXmRhxWKRdzfsa8Lp8sq42JjKOBCEEPyH4V1Q==} engines: {node: '>=20.0.0'} peerDependencies: express: ^4.17.1 || ^5 - react-router: 7.14.0 - typescript: ^5.1.0 + react-router: 7.14.1 + typescript: ^5.1.0 || ^6.0.0 peerDependenciesMeta: typescript: optional: true - '@react-router/node@7.14.0': - resolution: {integrity: sha512-ZxJJLE4PX29+cHLacH3pmCHMCJQz/1dpEgFQtm8Pst2IP5GI6897rShYylLZbJ7jRBJSkskHn+opSEh+o6mmOA==} + '@react-router/node@7.14.1': + resolution: {integrity: sha512-SthTjCwW7otzEAcZwF0RAPMRrDT47B4qHDxZM45rM5K1Gp86ANK/xlXF+DgpLq9qKZf9FbKzxS9hT7FqDeBAOg==} engines: {node: '>=20.0.0'} peerDependencies: - react-router: 7.14.0 - typescript: ^5.1.0 + react-router: 7.14.1 + typescript: ^5.1.0 || ^6.0.0 peerDependenciesMeta: typescript: optional: true - '@react-router/serve@7.14.0': - resolution: {integrity: sha512-setPBP5+ci0vwx+ufGZl0inOwsCoGU1ssOJcW4fHo+Pb6GbbMTrbCOVO6yQkDsTrQju+iStp3d7FTxLHphLhcA==} + '@react-router/serve@7.14.1': + resolution: {integrity: sha512-3oSNEQqU4ekIQTMqc7c9MJMHzSUAl4knG5mF9+1HaLqvUaYAfZPidqd4JWQKeYwe6Tw6fa79lcvUXqfCSXiEUg==} engines: {node: '>=20.0.0'} hasBin: true peerDependencies: - react-router: 7.14.0 + react-router: 7.14.1 - '@react-stately/autocomplete@3.0.0-beta.4': - resolution: {integrity: sha512-K2Uy7XEdseFvgwRQ8CyrYEHMupjVKEszddOapP8deNz4hntYvT1aRm0m+sKa5Kl/4kvg9c/3NZpQcrky/vRZIg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/calendar@3.9.3': - resolution: {integrity: sha512-uw7fCZXoypSBBUsVkbNvJMQWTihZReRbyLIGG3o/ZM630N3OCZhb/h4Uxke4pNu7n527H0V1bAnZgAldIzOYqg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/checkbox@3.7.5': - resolution: {integrity: sha512-K5R5ted7AxLB3sDkuVAazUdyRMraFT1imVqij2GuAiOUFvsZvbuocnDuFkBVKojyV3GpqLBvViV8IaCMc4hNIw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/collections@3.12.10': - resolution: {integrity: sha512-wmF9VxJDyBujBuQ76vXj2g/+bnnj8fx5DdXgRmyfkkYhPB46+g2qnjbVGEvipo7bJuGxDftCUC4SN7l7xqUWfg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/color@3.9.5': - resolution: {integrity: sha512-8pZxzXWDRuglzDwyTG7mLw2LQMCHIVNbVc9YmbsxbOjAL+lOqszo60KzyaFKVxeDQczSvrNTHcQZqlbNIC0eyQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/combobox@3.13.0': - resolution: {integrity: sha512-dX9g/cK1hjLRjcbWVF6keHxTQDGhKGB2QAgPhWcBmOK3qJv+2dQqsJ6YCGWn/Y2N2acoEseLrAA7+Qe4HWV9cg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/data@3.15.2': - resolution: {integrity: sha512-BsmeeGgFwOGwo0g9Waprdyt+846n3KhKggZfpEnp5+sC4dE4uW1VIYpdyupMfr3bQcmX123q6TegfNP3eszrUA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/datepicker@3.16.1': - resolution: {integrity: sha512-BtAMDvxd1OZxkxjqq5tN5TYmp6Hm8+o3+IDA4qmem2/pfQfVbOZeWS2WitcPBImj4n4T+W1A5+PI7mT/6DUBVg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/disclosure@3.0.11': - resolution: {integrity: sha512-/KjB/0HkxGWbhFAPztCP411LUKZCx9k8cKukrlGqrUWyvrcXlmza90j0g/CuxACBoV+DJP9V+4q+8ide0x750A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/dnd@3.7.4': - resolution: {integrity: sha512-YD0TVR5JkvTqskc1ouBpVKs6t/QS4RYCIyu8Ug8RgO122iIizuf2pfKnRLjYMdu5lXzBXGaIgd49dvnLzEXHIw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/flags@3.1.2': - resolution: {integrity: sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==} - - '@react-stately/form@3.2.4': - resolution: {integrity: sha512-qNBzun8SbLdgahryhKLqL1eqP+MXY6as82sVXYOOvUYLzgU5uuN8mObxYlxJgMI5akSdQJQV3RzyfVobPRE7Kw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/grid@3.11.9': - resolution: {integrity: sha512-qQY6F+27iZRn30dt0ZOrSetUmbmNJ0pLe9Weuqw3+XDVSuWT+2O/rO1UUYeK+mO0Acjzdv+IWiYbu9RKf2wS9w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/layout@4.6.0': - resolution: {integrity: sha512-kBenEsP03nh5rKgfqlVMPcoKTJv0v92CTvrAb5gYY8t9g8LOwzdL89Yannq7f5xv8LFck/MmRQlotpMt2InETg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/list@3.13.4': - resolution: {integrity: sha512-HHYSjA9VG7FPSAtpXAjQyM/V7qFHWGg88WmMrDt5QDlTBexwPuH0oFLnW0qaVZpAIxuWIsutZfxRAnme/NhhAA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/menu@3.9.11': - resolution: {integrity: sha512-vYkpO9uV2OUecsIkrOc+Urdl/s1xw/ibNH/UXsp4PtjMnS6mK9q2kXZTM3WvMAKoh12iveUO+YkYCZQshmFLHQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/numberfield@3.11.0': - resolution: {integrity: sha512-rxfC047vL0LP4tanjinfjKAriAvdVL57Um5RUL5nHML8IOWCB3TBxegQkJ6to6goScC/oZhd0/Y2LSaiRuKbNw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/overlays@3.6.23': - resolution: {integrity: sha512-RzWxots9A6gAzQMP4s8hOAHV7SbJRTFSlQbb6ly1nkWQXacOSZSFNGsKOaS0eIatfNPlNnW4NIkgtGws5UYzfw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/radio@3.11.5': - resolution: {integrity: sha512-QxA779S4ea5icQ0ja7CeiNzY1cj7c9G9TN0m7maAIGiTSinZl2Ia8naZJ0XcbRRp+LBll7RFEdekne15TjvS/w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/searchfield@3.5.19': - resolution: {integrity: sha512-URllgjbtTQEaOCfddbHpJSPKOzG3pE3ajQHJ7Df8qCoHTjKfL6hnm/vp7X5sxPaZaN7VLZ5kAQxTE8hpo6s0+A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/select@3.9.2': - resolution: {integrity: sha512-oWn0bijuusp8YI7FRM/wgtPVqiIrgU/ZUfLKe/qJUmT8D+JFaMAJnyrAzKpx98TrgamgtXynF78ccpopPhgrKQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/selection@3.20.9': - resolution: {integrity: sha512-RhxRR5Wovg9EVi3pq7gBPK2BoKmP59tOXDMh2r1PbnGevg/7TNdR67DCEblcmXwHuBNS46ELfKdd0XGHqmS8nQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/slider@3.7.5': - resolution: {integrity: sha512-OrQMNR5xamLYH52TXtvTgyw3EMwv+JI+1istQgEj1CHBjC9eZZqn5iNCN20tzm+uDPTH0EIGULFjjPIumqYUQg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/table@3.15.4': - resolution: {integrity: sha512-fGaNyw3wv7JgRCNzgyDzpaaTFuSy5f4Qekch4UheMXDJX7dOeaMhUXeOfvnXCVg+BGM4ey/D82RvDOGvPy1Nww==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/tabs@3.8.9': - resolution: {integrity: sha512-AQ4Xrn6YzIolaVShCV9cnwOjBKPAOGP/PTp7wpSEtQbQ0HZzUDG2RG/M4baMeUB2jZ33b7ifXyPcK78o0uOftg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/toast@3.1.3': - resolution: {integrity: sha512-mT9QJKmD523lqFpOp0VWZ6QHZENFK7HrodnNJDVc7g616s5GNmemdlkITV43fSY3tHeThCVvPu+Uzh7RvQ9mpQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/toggle@3.9.5': - resolution: {integrity: sha512-PVzXc788q3jH98Kvw1LYDL+wpVC14dCEKjOku8cSaqhEof6AJGaLR9yq+EF1yYSL2dxI6z8ghc0OozY8WrcFcA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/tooltip@3.5.11': - resolution: {integrity: sha512-o8PnFXbvDCuVZ4Ht9ahfS6KHwIZjXopvoQ2vUPxv920irdgWEeC+4omgDOnJ/xFvcpmmJAmSsrQsTQrTguDUQA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/tree@3.9.6': - resolution: {integrity: sha512-JCuhGyX2A+PAMsx2pRSwArfqNFZJ9JSPkDaOQJS8MFPAsBe5HemvXsdmv9aBIMzlbCYcVq6EsrFnzbVVTBt/6w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/utils@3.11.0': - resolution: {integrity: sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-stately/virtualizer@4.4.6': - resolution: {integrity: sha512-9SfXgLFB61/8SXNLfg5ARx9jAK4m03Aw6/Cg8mdZN24SYarL4TKNRpfw8K/HHVU/bi6WHSJypk6Z/z19o/ztrg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/autocomplete@3.0.0-alpha.38': - resolution: {integrity: sha512-0XrlVC8drzcrCNzybbkZdLcTofXEzBsHuaFevt5awW1J0xBJ+SMLIQMDeUYrvKjjwXUBlCtjJJpOvitGt4Z+KA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/breadcrumbs@3.7.19': - resolution: {integrity: sha512-AnkyYYmzaM2QFi/N0P/kQLM8tHOyFi7p397B/jEMucXDfwMw5Ny1ObCXeIEqbh8KrIa2Xp8SxmQlCV+8FPs4LA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/button@3.15.1': - resolution: {integrity: sha512-M1HtsKreJkigCnqceuIT22hDJBSStbPimnpmQmsl7SNyqCFY3+DHS7y/Sl3GvqCkzxF7j9UTL0dG38lGQ3K4xQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/calendar@3.8.3': - resolution: {integrity: sha512-fpH6WNXotzH0TlKHXXxtjeLZ7ko0sbyHmwDAwmDFyP7T0Iwn1YQZ+lhceLifvynlxuOgX6oBItyUKmkHQ0FouQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/checkbox@3.10.4': - resolution: {integrity: sha512-tYCG0Pd1usEz5hjvBEYcqcA0youx930Rss1QBIse9TgMekA1c2WmPDNupYV8phpO8Zuej3DL1WfBeXcgavK8aw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/color@3.1.4': - resolution: {integrity: sha512-s+Xj4pvNBlJPpQ1Gr7bO1j4/tuwMUfdS9xIVFuiW5RvDsSybKTUJ/gqPzTxms94VDCRhLFocVn2STNdD2Erf6A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/combobox@3.14.0': - resolution: {integrity: sha512-zmSSS7BcCOD8rGT8eGbVy7UlL5qq1vm88fFn4WgFe+lfK33ne+E7yTzTxcPY2TCGSo5fY6xMj3OG79FfVNGbSg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/datepicker@3.13.5': - resolution: {integrity: sha512-j28Vz+xvbb4bj7+9Xbpc4WTvSitlBvt7YEaEGM/8ZQ5g4Jr85H2KwkmDwjzmMN2r6VMQMMYq9JEcemq5wWpfUQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/dialog@3.5.24': - resolution: {integrity: sha512-NFurEP/zV0dA/41422lV1t+0oh6f/13n+VmLHZG8R13m1J3ql/kAXZ49zBSqkqANBO1ojyugWebk99IiR4pYOw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/form@3.7.18': - resolution: {integrity: sha512-0sBJW0+I9nJcF4SmKrYFEWAlehiebSTy7xqriqAXtqfTEdvzAYLGaAK2/7gx+wlNZeDTdW43CDRJ4XAhyhBqnw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/grid@3.3.8': - resolution: {integrity: sha512-zJvXH8gc1e1VH2H3LRnHH/W2HIkLkZMH3Cu5pLcj0vDuLBSWpcr3Ikh3jZ+VUOZF0G1Jt1lO8pKIaqFzDLNmLQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/link@3.6.7': - resolution: {integrity: sha512-1apXCFJgMC1uydc2KNENrps1qR642FqDpwlNWe254UTpRZn/hEZhA6ImVr8WhomfLJu672WyWA0rUOv4HT+/pQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/listbox@3.7.6': - resolution: {integrity: sha512-335NYElKEByXMalAmeRPyulKIDd2cjOCQhLwvv2BtxO5zaJfZnBbhZs+XPd9zwU6YomyOxODKSHrwbNDx+Jf3w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/menu@3.10.7': - resolution: {integrity: sha512-+p7ixZdvPDJZhisqdtWiiuJ9pteNfK5i19NB6wzAw5XkljbEzodNhwLv6rI96DY5XpbFso2kcjw7IWi+rAAGGQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/meter@3.4.15': - resolution: {integrity: sha512-9WjNphhLLM+TA4Ev1y2MkpugJ5JjTXseHh7ZWWx2veq5DrXMZYclkRpfUrUdLVKvaBIPQCgpQIj0TcQi+quR9A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/numberfield@3.8.18': - resolution: {integrity: sha512-nLzk7YAG9yAUtSv+9R8LgCHsu8hJq8/A+m1KsKxvc8WmNJjIujSFgWvT21MWBiUgPBzJKGzAqpMDDa087mltJQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/overlays@3.9.4': - resolution: {integrity: sha512-7Z9HaebMFyYBqtv3XVNHEmVkm7AiYviV7gv0c98elEN2Co+eQcKFGvwBM9Gy/lV57zlTqFX1EX/SAqkMEbCLOA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/progress@3.5.18': - resolution: {integrity: sha512-mKeQn+KrHr1y0/k7KtrbeDGDaERH6i4f6yBwj/ZtYDCTNKMO3tPHJY6nzF0w/KKZLplIO+BjUbHXc2RVm8ovwQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/radio@3.9.4': - resolution: {integrity: sha512-TkMRY3sA1PcFZhhclu4IUzUTIir6MzNJj8h6WT8vO6Nug2kXJ72qigugVFBWJSE472mltduOErEAo0rtAYWbQA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/searchfield@3.6.8': - resolution: {integrity: sha512-M2p7OVdMTMDmlBcHd4N2uCBwg3uJSNM4lmEyf09YD44N5wDAI0yogk52QBwsnhpe+i2s65UwCYgunB+QltRX8A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/select@3.12.2': - resolution: {integrity: sha512-AseOjfr3qM1W1qIWcbAe6NFpwZluVeQX/dmu9BYxjcnVvtoBLPMbE5zX/BPbv+N5eFYjoMyj7Ug9dqnI+LrlGw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/shared@3.33.1': - resolution: {integrity: sha512-oJHtjvLG43VjwemQDadlR5g/8VepK56B/xKO2XORPHt9zlW6IZs3tZrYlvH29BMvoqC7RtE7E5UjgbnbFtDGag==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/slider@3.8.4': - resolution: {integrity: sha512-C+xFVvfKREai9S/ekBDCVaGPOQYkNUAsQhjQnNsUAATaox4I6IYLmcIgLmljpMQWqAe+gZiWsIwacRYMez2Tew==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/switch@3.5.17': - resolution: {integrity: sha512-2GTPJvBCYI8YZ3oerHtXg+qikabIXCMJ6C2wcIJ5Xn0k9XOovowghfJi10OPB2GGyOiLBU74CczP5nx8adG90Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/table@3.13.6': - resolution: {integrity: sha512-eluL+iFfnVmFm7OSZrrFG9AUjw+tcv898zbv+NsZACa8oXG1v9AimhZfd+Mo8q/5+sX/9hguWNXFkSvmTjuVPQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/tabs@3.3.22': - resolution: {integrity: sha512-HGwLD9dA3k3AGfRKGFBhNgxU9/LyRmxN0kxVj1ghA4L9S/qTOzS6GhrGNkGzsGxyVLV4JN8MLxjWN2o9QHnLEg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/textfield@3.12.8': - resolution: {integrity: sha512-wt6FcuE5AyntxsnPika/h3nf/DPmeAVbI018L9o6h+B/IL4sMWWdx663wx2KOOeHH8ejKGZQNPLhUKs4s1mVQA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - - '@react-types/tooltip@3.5.2': - resolution: {integrity: sha512-FvSuZ2WP08NEWefrpCdBYpEEZh/5TvqvGjq0wqGzWg2OPwpc14HjD8aE7I3MOuylXkD4MSlMjl7J4DlvlcCs3Q==} + '@react-types/shared@3.34.0': + resolution: {integrity: sha512-gp6xo/s2lX54AlTjOiqwDnxA7UW79BNvI9dB9pr3LZTzRKCd1ZA+ZbgKw/ReIiWuvvVw/8QFJpnqeeFyLocMcQ==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -2224,196 +1772,196 @@ packages: '@remix-run/node-fetch-server@0.13.0': resolution: {integrity: sha512-1EsNo0ZpgXu/90AWoRZf/oE3RVTUS80tiTUpt+hv5pjtAkw7icN4WskDwz/KdAw5ARbJLMhZBrO1NqThmy/McA==} - '@rolldown/binding-android-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + '@rolldown/binding-android-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.12': - resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.12': - resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': - resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': - resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': - resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': - resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': - resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': - resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.12': - resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + '@rolldown/pluginutils@1.0.0-rc.15': + resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} - '@rollup/rollup-android-arm-eabi@4.60.0': - resolution: {integrity: sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==} + '@rollup/rollup-android-arm-eabi@4.60.2': + resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.60.0': - resolution: {integrity: sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==} + '@rollup/rollup-android-arm64@4.60.2': + resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.0': - resolution: {integrity: sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==} + '@rollup/rollup-darwin-arm64@4.60.2': + resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.0': - resolution: {integrity: sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==} + '@rollup/rollup-darwin-x64@4.60.2': + resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.0': - resolution: {integrity: sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==} + '@rollup/rollup-freebsd-arm64@4.60.2': + resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.60.0': - resolution: {integrity: sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==} + '@rollup/rollup-freebsd-x64@4.60.2': + resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.0': - resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==} + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': + resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.60.0': - resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==} + '@rollup/rollup-linux-arm-musleabihf@4.60.2': + resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.60.0': - resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==} + '@rollup/rollup-linux-arm64-gnu@4.60.2': + resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.60.0': - resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==} + '@rollup/rollup-linux-arm64-musl@4.60.2': + resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.60.0': - resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==} + '@rollup/rollup-linux-loong64-gnu@4.60.2': + resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.60.0': - resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==} + '@rollup/rollup-linux-loong64-musl@4.60.2': + resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.60.0': - resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==} + '@rollup/rollup-linux-ppc64-gnu@4.60.2': + resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.60.0': - resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==} + '@rollup/rollup-linux-ppc64-musl@4.60.2': + resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.60.0': - resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==} + '@rollup/rollup-linux-riscv64-gnu@4.60.2': + resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.60.0': - resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==} + '@rollup/rollup-linux-riscv64-musl@4.60.2': + resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.60.0': - resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==} + '@rollup/rollup-linux-s390x-gnu@4.60.2': + resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} cpu: [s390x] os: [linux] libc: [glibc] @@ -2424,46 +1972,48 @@ packages: os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.60.0': - resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==} + '@rollup/rollup-linux-x64-gnu@4.60.2': + resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.2': + resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.60.0': - resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==} + '@rollup/rollup-openbsd-x64@4.60.2': + resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.60.0': - resolution: {integrity: sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==} + '@rollup/rollup-openharmony-arm64@4.60.2': + resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.60.0': - resolution: {integrity: sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==} + '@rollup/rollup-win32-arm64-msvc@4.60.2': + resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.0': - resolution: {integrity: sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==} + '@rollup/rollup-win32-ia32-msvc@4.60.2': + resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.0': - resolution: {integrity: sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==} + '@rollup/rollup-win32-x64-gnu@4.60.2': + resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.0': - resolution: {integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==} + '@rollup/rollup-win32-x64-msvc@4.60.2': + resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==} cpu: [x64] os: [win32] - '@smithy/abort-controller@4.2.12': - resolution: {integrity: sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==} - engines: {node: '>=18.0.0'} - '@smithy/chunked-blob-reader-native@4.2.3': resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==} engines: {node: '>=18.0.0'} @@ -2472,56 +2022,56 @@ packages: resolution: {integrity: sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==} engines: {node: '>=18.0.0'} - '@smithy/config-resolver@4.4.13': - resolution: {integrity: sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==} + '@smithy/config-resolver@4.4.17': + resolution: {integrity: sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==} engines: {node: '>=18.0.0'} - '@smithy/core@3.23.12': - resolution: {integrity: sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==} + '@smithy/core@3.23.16': + resolution: {integrity: sha512-JStomOrINQA1VqNEopLsgcdgwd42au7mykKqVr30XFw89wLt9sDxJDi4djVPRwQmmzyTGy/uOvTc2ultMpFi1w==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.2.12': - resolution: {integrity: sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==} + '@smithy/credential-provider-imds@4.2.14': + resolution: {integrity: sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-codec@4.2.12': - resolution: {integrity: sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==} + '@smithy/eventstream-codec@4.2.14': + resolution: {integrity: sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-browser@4.2.12': - resolution: {integrity: sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==} + '@smithy/eventstream-serde-browser@4.2.14': + resolution: {integrity: sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-config-resolver@4.3.12': - resolution: {integrity: sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==} + '@smithy/eventstream-serde-config-resolver@4.3.14': + resolution: {integrity: sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-node@4.2.12': - resolution: {integrity: sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==} + '@smithy/eventstream-serde-node@4.2.14': + resolution: {integrity: sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-universal@4.2.12': - resolution: {integrity: sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==} + '@smithy/eventstream-serde-universal@4.2.14': + resolution: {integrity: sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.3.15': - resolution: {integrity: sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==} + '@smithy/fetch-http-handler@5.3.17': + resolution: {integrity: sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==} engines: {node: '>=18.0.0'} - '@smithy/hash-blob-browser@4.2.13': - resolution: {integrity: sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==} + '@smithy/hash-blob-browser@4.2.15': + resolution: {integrity: sha512-0PJ4Al3fg2nM4qKrAIxyNcApgqHAXcBkN8FeizOz69z0rb26uZ6lMESYtxegaTlXB5Hj84JfwMPavMrwDMjucA==} engines: {node: '>=18.0.0'} - '@smithy/hash-node@4.2.12': - resolution: {integrity: sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==} + '@smithy/hash-node@4.2.14': + resolution: {integrity: sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==} engines: {node: '>=18.0.0'} - '@smithy/hash-stream-node@4.2.12': - resolution: {integrity: sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==} + '@smithy/hash-stream-node@4.2.14': + resolution: {integrity: sha512-tw4GANWkZPb6+BdD4Fgucqzey2+r73Z/GRo9zklsCdwrnxxumUV83ZIaBDdudV4Ylazw3EPTiJZhpX42105ruQ==} engines: {node: '>=18.0.0'} - '@smithy/invalid-dependency@4.2.12': - resolution: {integrity: sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==} + '@smithy/invalid-dependency@4.2.14': + resolution: {integrity: sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==} engines: {node: '>=18.0.0'} '@smithy/is-array-buffer@2.2.0': @@ -2532,76 +2082,76 @@ packages: resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} engines: {node: '>=18.0.0'} - '@smithy/md5-js@4.2.12': - resolution: {integrity: sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==} + '@smithy/md5-js@4.2.14': + resolution: {integrity: sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA==} engines: {node: '>=18.0.0'} - '@smithy/middleware-content-length@4.2.12': - resolution: {integrity: sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==} + '@smithy/middleware-content-length@4.2.14': + resolution: {integrity: sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==} engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.4.27': - resolution: {integrity: sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==} + '@smithy/middleware-endpoint@4.4.31': + resolution: {integrity: sha512-KJPdCIN2kOE2aGmqZd7eUTr4WQwOGgtLWgUkswGJggs7rBcQYQjcZMEDa3C0DwbOiXS9L8/wDoQHkfxBYLfiLw==} engines: {node: '>=18.0.0'} - '@smithy/middleware-retry@4.4.44': - resolution: {integrity: sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA==} + '@smithy/middleware-retry@4.5.4': + resolution: {integrity: sha512-/z7nIFK+ZRW3Ie/l3NEVGdy34LvmEOzBrtBAvgWZ/4PrKX0xP3kWm8pkfcwUk523SqxZhdbQP9JSXgjF77Uhpw==} engines: {node: '>=18.0.0'} - '@smithy/middleware-serde@4.2.15': - resolution: {integrity: sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==} + '@smithy/middleware-serde@4.2.19': + resolution: {integrity: sha512-Q6y+W9h3iYVMCKWDoVge+OC1LKFqbEKaq8SIWG2X2bWJRpd/6dDLyICcNLT6PbjH3Rr6bmg/SeDB25XFOFfeEw==} engines: {node: '>=18.0.0'} - '@smithy/middleware-stack@4.2.12': - resolution: {integrity: sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==} + '@smithy/middleware-stack@4.2.14': + resolution: {integrity: sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==} engines: {node: '>=18.0.0'} - '@smithy/node-config-provider@4.3.12': - resolution: {integrity: sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==} + '@smithy/node-config-provider@4.3.14': + resolution: {integrity: sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.5.0': - resolution: {integrity: sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==} + '@smithy/node-http-handler@4.6.0': + resolution: {integrity: sha512-P734cAoTFtuGfWa/R3jgBnGlURt2w9bYEBwQNMKf58sRM9RShirB2mKwLsVP+jlG/wxpCu8abv8NxdUts8tdLA==} engines: {node: '>=18.0.0'} - '@smithy/property-provider@4.2.12': - resolution: {integrity: sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==} + '@smithy/property-provider@4.2.14': + resolution: {integrity: sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==} engines: {node: '>=18.0.0'} - '@smithy/protocol-http@5.3.12': - resolution: {integrity: sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==} + '@smithy/protocol-http@5.3.14': + resolution: {integrity: sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==} engines: {node: '>=18.0.0'} - '@smithy/querystring-builder@4.2.12': - resolution: {integrity: sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==} + '@smithy/querystring-builder@4.2.14': + resolution: {integrity: sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==} engines: {node: '>=18.0.0'} - '@smithy/querystring-parser@4.2.12': - resolution: {integrity: sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==} + '@smithy/querystring-parser@4.2.14': + resolution: {integrity: sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==} engines: {node: '>=18.0.0'} - '@smithy/service-error-classification@4.2.12': - resolution: {integrity: sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==} + '@smithy/service-error-classification@4.3.0': + resolution: {integrity: sha512-9jKsBYQRPR0xBLgc2415RsA5PIcP2sis4oBdN9s0D13cg1B1284mNTjx9Yc+BEERXzuPm5ObktI96OxsKh8E9A==} engines: {node: '>=18.0.0'} - '@smithy/shared-ini-file-loader@4.4.7': - resolution: {integrity: sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==} + '@smithy/shared-ini-file-loader@4.4.9': + resolution: {integrity: sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==} engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.3.12': - resolution: {integrity: sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==} + '@smithy/signature-v4@5.3.14': + resolution: {integrity: sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==} engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.12.7': - resolution: {integrity: sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==} + '@smithy/smithy-client@4.12.12': + resolution: {integrity: sha512-daO7SJn4eM6ArbmrEs+/BTbH7af8AEbSL3OMQdcRvvn8tuUcR5rU2n6DgxIV53aXMS42uwK8NgKKCh5XgqYOPQ==} engines: {node: '>=18.0.0'} - '@smithy/types@4.13.1': - resolution: {integrity: sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==} + '@smithy/types@4.14.1': + resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==} engines: {node: '>=18.0.0'} - '@smithy/url-parser@4.2.12': - resolution: {integrity: sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==} + '@smithy/url-parser@4.2.14': + resolution: {integrity: sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==} engines: {node: '>=18.0.0'} '@smithy/util-base64@4.3.2': @@ -2628,32 +2178,32 @@ packages: resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-browser@4.3.43': - resolution: {integrity: sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ==} + '@smithy/util-defaults-mode-browser@4.3.48': + resolution: {integrity: sha512-hxVRVPYaRDWa6YQdse1aWX1qrksmLsvNyGBKdc32q4jFzSjxYVNWfstknAfR228TnzS4tzgswXRuYIbhXBuXFQ==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.2.47': - resolution: {integrity: sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ==} + '@smithy/util-defaults-mode-node@4.2.53': + resolution: {integrity: sha512-ybgCk+9JdBq8pYC8Y6U5fjyS8e4sboyAShetxPNL0rRBtaVl56GSFAxsolVBIea1tXR4LPIzL8i6xqmcf0+DCQ==} engines: {node: '>=18.0.0'} - '@smithy/util-endpoints@3.3.3': - resolution: {integrity: sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==} + '@smithy/util-endpoints@3.4.2': + resolution: {integrity: sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==} engines: {node: '>=18.0.0'} '@smithy/util-hex-encoding@4.2.2': resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} engines: {node: '>=18.0.0'} - '@smithy/util-middleware@4.2.12': - resolution: {integrity: sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==} + '@smithy/util-middleware@4.2.14': + resolution: {integrity: sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==} engines: {node: '>=18.0.0'} - '@smithy/util-retry@4.2.12': - resolution: {integrity: sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==} + '@smithy/util-retry@4.3.3': + resolution: {integrity: sha512-idjUvd4M9Jj6rXkhqw4H4reHoweuK4ZxYWyOrEp4N2rOF5VtaOlQGLDQJva/8WanNXk9ScQtsAb7o5UHGvFm4A==} engines: {node: '>=18.0.0'} - '@smithy/util-stream@4.5.20': - resolution: {integrity: sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==} + '@smithy/util-stream@4.5.24': + resolution: {integrity: sha512-na5vv2mBSDzXewLEEoWGI7LQQkfpmFEomBsmOpzLFjqGctm0iMwXY5lAwesY9pIaErkccW0qzEOUcYP+WKneXg==} engines: {node: '>=18.0.0'} '@smithy/util-uri-escape@4.2.2': @@ -2668,8 +2218,8 @@ packages: resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} engines: {node: '>=18.0.0'} - '@smithy/util-waiter@4.2.13': - resolution: {integrity: sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ==} + '@smithy/util-waiter@4.2.16': + resolution: {integrity: sha512-GtclrKoZ3Lt7jPQ7aTIYKfjY92OgceScftVnkTsG8e1KV8rkvZgN+ny6YSRhd9hxB8rZtwVbmln7NTvE5O3GmQ==} engines: {node: '>=18.0.0'} '@smithy/uuid@1.1.2': @@ -2679,8 +2229,8 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@swc/helpers@0.5.20': - resolution: {integrity: sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw==} + '@swc/helpers@0.5.21': + resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} '@tiptap/core@2.27.2': resolution: {integrity: sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ==} @@ -2913,8 +2463,8 @@ packages: '@types/node-cron@3.0.11': resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} - '@types/node@25.5.0': - resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} '@types/nprogress@0.2.3': resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==} @@ -2961,22 +2511,22 @@ packages: peerDependencies: react: '>= 16.8.0' - '@vitest/browser-playwright@4.1.2': - resolution: {integrity: sha512-N0Z2HzMLvMR6k/tWPTS6Q/DaRscrkax/f2f9DIbNQr+Cd1l4W4wTf/I6S983PAMr0tNqqoTL+xNkLh9M5vbkLg==} + '@vitest/browser-playwright@4.1.4': + resolution: {integrity: sha512-q3PchVhZINX23Pv+RERgAtDlp6wzVkID/smOPnZ5YGWpeWUe3jMNYppeVh15j4il3G7JIJty1d1Kicpm0HSMig==} peerDependencies: playwright: '*' - vitest: 4.1.2 + vitest: 4.1.4 - '@vitest/browser@4.1.2': - resolution: {integrity: sha512-CwdIf90LNf1Zitgqy63ciMAzmyb4oIGs8WZ40VGYrWkssQKeEKr32EzO8MKUrDPPcPVHFI9oQ5ni2Hp24NaNRQ==} + '@vitest/browser@4.1.4': + resolution: {integrity: sha512-TrNaY/yVOwxtrxNsDUC/wQ56xSwplpytTeRAqF/197xV/ZddxxulBsxR6TrhVMyniJmp9in8d5u0AcDaNRY30w==} peerDependencies: - vitest: 4.1.2 + vitest: 4.1.4 - '@vitest/expect@4.1.2': - resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + '@vitest/expect@4.1.4': + resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} - '@vitest/mocker@4.1.2': - resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} + '@vitest/mocker@4.1.4': + resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2986,31 +2536,31 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.2': - resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + '@vitest/pretty-format@4.1.4': + resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} - '@vitest/runner@4.1.2': - resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + '@vitest/runner@4.1.4': + resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} - '@vitest/snapshot@4.1.2': - resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + '@vitest/snapshot@4.1.4': + resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} - '@vitest/spy@4.1.2': - resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + '@vitest/spy@4.1.4': + resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==} - '@vitest/ui@4.1.2': - resolution: {integrity: sha512-/irhyeAcKS2u6Zokagf9tqZJ0t8S6kMZq4ZG9BHZv7I+fkRrYfQX4w7geYeC2r6obThz39PDxvXQzZX+qXqGeg==} + '@vitest/ui@4.1.4': + resolution: {integrity: sha512-EgFR7nlj5iTDYZYCvavjFokNYwr3c3ry0sFiCg+N7B233Nwp+NNx7eoF/XvMWDCKY71xXAG3kFkt97ZHBJVL8A==} peerDependencies: - vitest: 4.1.2 + vitest: 4.1.4 - '@vitest/utils@4.1.2': - resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + '@vitest/utils@4.1.4': + resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} '@xobotyi/scrollbar-width@1.9.5': resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} - '@zumer/snapdom@2.7.0': - resolution: {integrity: sha512-ZiELKzDszeFOazPQ/ExXzgtdoW9jADVjDjInr5XDAlVdCx0RbNsFiG7RLyM48XnA7EyCA9yTvmXSc3ElDrTRqA==} + '@zumer/snapdom@2.9.0': + resolution: {integrity: sha512-ksDM5gXQmu1isBof/YumD2HeIHf4ImTMfba9xE4JyDbARhcZCWHFdjW8oZV3hSKdWpQILEspNq5MrAfwEfzpIw==} accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} @@ -3072,8 +2622,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.11: - resolution: {integrity: sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==} + baseline-browser-mapping@2.10.20: + resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -3081,8 +2631,8 @@ packages: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} - better-sqlite3@12.8.0: - resolution: {integrity: sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==} + better-sqlite3@12.9.0: + resolution: {integrity: sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} bindings@1.5.0: @@ -3108,8 +2658,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -3125,6 +2675,9 @@ packages: buffer@5.6.0: resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -3149,8 +2702,8 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - caniuse-lite@1.0.30001781: - resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} + caniuse-lite@1.0.30001790: + resolution: {integrity: sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} @@ -3199,8 +2752,8 @@ packages: resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} engines: {node: '>= 0.8.0'} - compressorjs@1.2.1: - resolution: {integrity: sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==} + compressorjs@1.3.0: + resolution: {integrity: sha512-TsvzkRgDm/6mIRUdxJbrTH7kfSW3oJzOw8b1xU60fziQSosTML5TczpO6Z4H1LGF0yRmTotk6r5UNhuRxEwA1A==} confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} @@ -3241,8 +2794,8 @@ packages: engines: {node: '>=20'} hasBin: true - cross-fetch@4.0.0: - resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} @@ -3312,9 +2865,6 @@ packages: supports-color: optional: true - decimal.js@10.6.0: - resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -3360,8 +2910,8 @@ packages: discontinuous-range@1.0.0: resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==} - dotenv@17.3.1: - resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -3377,8 +2927,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.328: - resolution: {integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==} + electron-to-chromium@1.5.343: + resolution: {integrity: sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -3420,6 +2970,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -3490,8 +3045,8 @@ packages: fast-shallow-equal@1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} - fast-xml-builder@1.1.4: - resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + fast-xml-builder@1.1.5: + resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==} fast-xml-parser@5.5.8: resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} @@ -3672,8 +3227,8 @@ packages: i18next-browser-languagedetector@8.2.1: resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==} - i18next-http-backend@3.0.2: - resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==} + i18next-http-backend@3.0.5: + resolution: {integrity: sha512-QaWHnsxieEDcqKe+vo/RFqpiIFRi/KBqlOSPcUlvinBaISCeiTRCbtrazHAjtHtsLC66oDsROAH8frWkQzfMMQ==} i18next-locales-sync@2.1.1: resolution: {integrity: sha512-NC/Zw0kKT+VULSh6zzDSxvqDUS9BHY6tk2cj93qQ/yaNuKc3hg4VlJFDeuaxdovY1/LobcdQCAc6g1U03qe2SQ==} @@ -3701,6 +3256,9 @@ packages: ieee754@1.1.13: resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -3713,9 +3271,6 @@ packages: internmap@1.0.1: resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} - intl-messageformat@10.7.18: - resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} - ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -3771,8 +3326,8 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isbot@5.1.36: - resolution: {integrity: sha512-C/ZtXyJqDPZ7G7JPr06ApWyYoHjYexQbS6hPYD4WYCzpv2Qes6Z+CCEfTX4Owzf+1EJ933PoI2p+B9v7wpGZBQ==} + isbot@5.1.38: + resolution: {integrity: sha512-Cus2702JamTNMEY4zTP+TShgq/3qzjvGcBC4XMOV45BLaxD4iUFENkqu7ZhFeSzwNsCSZLjnGlihDQznnpnEEA==} engines: {node: '>=18'} isexe@2.0.0: @@ -3801,11 +3356,6 @@ packages: engines: {node: '>=6'} hasBin: true - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -3831,13 +3381,13 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - knip@6.1.0: - resolution: {integrity: sha512-n5eVbJP7HXmwTsiJcELWJe2O1ESxyCTNxJzRTIECDYDTM465qnqk7fL2dv6ae3NUFvFWorZvGlh9mcwxwJ5Xgw==} + knip@6.4.1: + resolution: {integrity: sha512-Ry+ywmDFSZvKp/jx7LxMgsZWRTs931alV84e60lh0Stf6kSRYqSIUTkviyyDFRcSO3yY1Kpbi83OirN+4lA2Xw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - kysely@0.28.14: - resolution: {integrity: sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==} + kysely@0.28.16: + resolution: {integrity: sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww==} engines: {node: '>=20.0.0'} ley@0.8.1: @@ -3935,22 +3485,22 @@ packages: lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - lodash@4.17.23: - resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - lru-cache@11.2.7: - resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} engines: {node: 20 || >=22} lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@1.7.0: - resolution: {integrity: sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==} + lucide-react@1.8.0: + resolution: {integrity: sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -3965,8 +3515,8 @@ packages: resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} hasBin: true - markdown-to-jsx@9.7.13: - resolution: {integrity: sha512-twSoD1A2RMx+wyYTNvHvf4JTzEr4SfkgycoSRwjbezejKhqdIXHrHaA3uEm24ET/ZLWrwSzOyWahmVULC+tTbw==} + markdown-to-jsx@9.7.15: + resolution: {integrity: sha512-e+zu57JYDDEiCYNSC8ohEeihBcAs73QdS++qd9Ffmeo6EU+yBFsfbgntkaT7lU2pRU6W4Yqh8a0cvpbmad7wHA==} engines: {node: '>= 18'} peerDependencies: react: '>= 16.0.0' @@ -4078,8 +3628,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@5.1.7: - resolution: {integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==} + nanoid@5.1.9: + resolution: {integrity: sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==} engines: {node: ^18 || >=20} hasBin: true @@ -4119,8 +3669,8 @@ packages: encoding: optional: true - node-releases@2.0.36: - resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} nprogress@0.2.0: resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} @@ -4185,8 +3735,8 @@ packages: react: optional: true - path-expression-matcher@1.2.0: - resolution: {integrity: sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} engines: {node: '>=14.0.0'} path-key@3.1.1: @@ -4216,13 +3766,13 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - playwright-core@1.58.2: - resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} engines: {node: '>=18'} hasBin: true - playwright@1.58.2: - resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} engines: {node: '>=18'} hasBin: true @@ -4234,8 +3784,8 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} engines: {node: ^10 || ^12 || >=14} prebuild-install@7.1.3: @@ -4244,8 +3794,8 @@ packages: deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true - prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} engines: {node: '>=14'} hasBin: true @@ -4361,14 +3911,14 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - react-aria-components@1.16.0: - resolution: {integrity: sha512-MjHbTLpMFzzD2Tv5KbeXoZwPczuUWZcRavVvQQlNHRtXHH38D+sToMEYpNeir7Wh3K/XWtzeX3EujfJW6QNkrw==} + react-aria-components@1.17.0: + resolution: {integrity: sha512-0EyisMgvsFJ2aML3crDYv2tW5vT2Ryf8PGzY/g63JjDdCbLshlwazhS8JNtPF1vkTkungJJ6sVJbKyX+YKSoFA==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - react-aria@3.47.0: - resolution: {integrity: sha512-nvahimIqdByl/PXk/xPkG30LPRzcin+/Uk0uFfwbbKRRFC9aa22a6BRULZLqVHwa9GaNyKe6CDUxO1Dde4v0kA==} + react-aria@3.48.0: + resolution: {integrity: sha512-jQjd4rBEIMqecBaAKYJbVGK6EqIHLa5znVQ7jwFyK5vCyljoj6KhgtiahmcIPsG5vG5vEDLw+ba+bEWn6A2P4w==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -4379,10 +3929,10 @@ packages: react: '>=16' react-dom: '>=16' - react-dom@19.2.4: - resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} peerDependencies: - react: ^19.2.4 + react: ^19.2.5 react-error-boundary@6.1.1: resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==} @@ -4439,8 +3989,8 @@ packages: '@types/react': optional: true - react-router@7.14.0: - resolution: {integrity: sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==} + react-router@7.14.1: + resolution: {integrity: sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -4449,8 +3999,8 @@ packages: react-dom: optional: true - react-stately@3.45.0: - resolution: {integrity: sha512-G3bYr0BIiookpt4H05VeZUuVS/FslQAj2TeT8vDfCiL314Y+LtPXIPe/a3eamCA0wljy7z1EDYKV50Qbz7pcJg==} + react-stately@3.46.0: + resolution: {integrity: sha512-OdxhWvHgs2L4OJGIs7hnuTr5WjjMM6enhNEAMRqiekhF8+ITvA2LRwNftOZwcogaoCslGYq5S2VQTQwnm0GbCA==} peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 @@ -4482,8 +4032,8 @@ packages: react: '*' react-dom: '*' - react@19.2.4: - resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} engines: {node: '>=0.10.0'} readable-stream@3.6.2: @@ -4497,8 +4047,8 @@ packages: rematrix@0.2.2: resolution: {integrity: sha512-agFFS3RzrLXJl5LY5xg/xYyXvUuVAnkhgKO7RaO9J1Ssth6yvbO+PIiV67V59MB5NCdAK2flvGvNT4mdKVniFA==} - remeda@2.33.6: - resolution: {integrity: sha512-tazDGH7s75kUPGBKLvhgBEHMgW+TdDFhjUAMdQj57IoWz6HsGa5D2RX5yDUz6IIqiRRvZiaEHzCzWdTeixc/Kg==} + remeda@2.33.7: + resolution: {integrity: sha512-cXlyjevWx5AcslOUEETG4o8XYi9UkoCXcJmj7XhPFVbla+ITuOBxv6ijBrmbeg+ZhzmDThkNdO+iXKUfrJep1w==} remix-auth-oauth2@3.4.1: resolution: {integrity: sha512-ZhGon1czdIsOw1/O9EcTCzapZB6FpT3u9vtXSVeEMwGNs+iWljRsibnUC1RtwJbzzCdLBSwIXTNTbiRmLZ4cZw==} @@ -4537,13 +4087,13 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown@1.0.0-rc.12: - resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + rolldown@1.0.0-rc.15: + resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rollup@4.60.0: - resolution: {integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==} + rollup@4.60.2: + resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -4629,8 +4179,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} side-channel-map@1.0.1: @@ -4658,8 +4208,8 @@ packages: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} - slugify@1.6.8: - resolution: {integrity: sha512-HVk9X1E0gz3mSpoi60h/saazLKXKaZThMLU3u/aNwoYn8/xQyX2MGxL0ui2eaokkD7tF+Zo+cKTHUbe1mmmGzA==} + slugify@1.6.9: + resolution: {integrity: sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==} engines: {node: '>=8.0.0'} smol-toml@1.6.1: @@ -4711,8 +4261,8 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@4.0.0: - resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} stream-browserify@3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} @@ -4740,8 +4290,8 @@ packages: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} - strnum@2.2.2: - resolution: {integrity: sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==} + strnum@2.2.3: + resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} @@ -4772,12 +4322,12 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.4: - resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} tinyrainbow@3.1.0: @@ -4846,8 +4396,8 @@ packages: types-ramda@0.31.0: resolution: {integrity: sha512-vaoC35CRC3xvL8Z6HkshDbi6KWM1ezK0LHN0YyxXWUn9HKzBNg/T3xSGlJZjCYspnOD3jE7bcizsp0bUXZDxnQ==} - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true @@ -4858,8 +4408,8 @@ packages: resolution: {integrity: sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==} engines: {node: '>=14'} - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} @@ -4945,8 +4495,8 @@ packages: '@babel/core': ^7.0.0 vite: ^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + vite@7.3.2: + resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4985,8 +4535,8 @@ packages: yaml: optional: true - vite@8.0.4: - resolution: {integrity: sha512-baBr4jUVSLJ0RPyZ2nK0zS2+W8hNHbM4hEzfvllukmRPVS3xDG5ATTNtbRXrKIOE2b8/FsPWJAOnuIxcs7g3cw==} + vite@8.0.8: + resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -5042,18 +4592,20 @@ packages: '@types/react-dom': optional: true - vitest@4.1.2: - resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + vitest@4.1.4: + resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.2 - '@vitest/browser-preview': 4.1.2 - '@vitest/browser-webdriverio': 4.1.2 - '@vitest/ui': 4.1.2 + '@vitest/browser-playwright': 4.1.4 + '@vitest/browser-preview': 4.1.4 + '@vitest/browser-webdriverio': 4.1.4 + '@vitest/coverage-istanbul': 4.1.4 + '@vitest/coverage-v8': 4.1.4 + '@vitest/ui': 4.1.4 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -5070,6 +4622,10 @@ packages: optional: true '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -5175,20 +4731,20 @@ snapshots: '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 tslib: 2.8.1 '@aws-crypto/crc32c@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 tslib: 2.8.1 '@aws-crypto/sha1-browser@5.2.0': dependencies: '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 '@aws-sdk/util-locate-window': 3.965.5 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -5198,7 +4754,7 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 '@aws-sdk/util-locate-window': 3.965.5 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -5206,7 +4762,7 @@ snapshots: '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -5215,414 +4771,416 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-s3@3.1019.0': + '@aws-sdk/client-s3@3.1030.0': dependencies: '@aws-crypto/sha1-browser': 5.2.0 '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.25 - '@aws-sdk/credential-provider-node': 3.972.27 - '@aws-sdk/middleware-bucket-endpoint': 3.972.8 - '@aws-sdk/middleware-expect-continue': 3.972.8 - '@aws-sdk/middleware-flexible-checksums': 3.974.5 - '@aws-sdk/middleware-host-header': 3.972.8 - '@aws-sdk/middleware-location-constraint': 3.972.8 - '@aws-sdk/middleware-logger': 3.972.8 - '@aws-sdk/middleware-recursion-detection': 3.972.9 - '@aws-sdk/middleware-sdk-s3': 3.972.26 - '@aws-sdk/middleware-ssec': 3.972.8 - '@aws-sdk/middleware-user-agent': 3.972.26 - '@aws-sdk/region-config-resolver': 3.972.10 - '@aws-sdk/signature-v4-multi-region': 3.996.14 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.12 - '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.12 - '@smithy/eventstream-serde-browser': 4.2.12 - '@smithy/eventstream-serde-config-resolver': 4.3.12 - '@smithy/eventstream-serde-node': 4.2.12 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/hash-blob-browser': 4.2.13 - '@smithy/hash-node': 4.2.12 - '@smithy/hash-stream-node': 4.2.12 - '@smithy/invalid-dependency': 4.2.12 - '@smithy/md5-js': 4.2.12 - '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.27 - '@smithy/middleware-retry': 4.4.44 - '@smithy/middleware-serde': 4.2.15 - '@smithy/middleware-stack': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.5.0 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/credential-provider-node': 3.972.34 + '@aws-sdk/middleware-bucket-endpoint': 3.972.10 + '@aws-sdk/middleware-expect-continue': 3.972.10 + '@aws-sdk/middleware-flexible-checksums': 3.974.11 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-location-constraint': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-sdk-s3': 3.972.32 + '@aws-sdk/middleware-ssec': 3.972.10 + '@aws-sdk/middleware-user-agent': 3.972.33 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/signature-v4-multi-region': 3.996.20 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.19 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.16 + '@smithy/eventstream-serde-browser': 4.2.14 + '@smithy/eventstream-serde-config-resolver': 4.3.14 + '@smithy/eventstream-serde-node': 4.2.14 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-blob-browser': 4.2.15 + '@smithy/hash-node': 4.2.14 + '@smithy/hash-stream-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/md5-js': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.31 + '@smithy/middleware-retry': 4.5.4 + '@smithy/middleware-serde': 4.2.19 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.0 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.43 - '@smithy/util-defaults-mode-node': 4.2.47 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.12 - '@smithy/util-stream': 4.5.20 + '@smithy/util-defaults-mode-browser': 4.3.48 + '@smithy/util-defaults-mode-node': 4.2.53 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.3 + '@smithy/util-stream': 4.5.24 '@smithy/util-utf8': 4.2.2 - '@smithy/util-waiter': 4.2.13 + '@smithy/util-waiter': 4.2.16 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/core@3.973.25': + '@aws-sdk/core@3.974.3': dependencies: - '@aws-sdk/types': 3.973.6 - '@aws-sdk/xml-builder': 3.972.16 - '@smithy/core': 3.23.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/property-provider': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/signature-v4': 5.3.12 - '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/xml-builder': 3.972.18 + '@smithy/core': 3.23.16 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 '@smithy/util-base64': 4.3.2 - '@smithy/util-middleware': 4.2.12 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.3 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@aws-sdk/crc64-nvme@3.972.5': + '@aws-sdk/crc64-nvme@3.972.7': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.972.23': + '@aws-sdk/credential-provider-env@3.972.29': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.972.25': + '@aws-sdk/credential-provider-http@3.972.31': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/types': 3.973.6 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/node-http-handler': 4.5.0 - '@smithy/property-provider': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.13.1 - '@smithy/util-stream': 4.5.20 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/types': 3.973.8 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/node-http-handler': 4.6.0 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 + '@smithy/util-stream': 4.5.24 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.972.26': + '@aws-sdk/credential-provider-ini@3.972.33': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/credential-provider-env': 3.972.23 - '@aws-sdk/credential-provider-http': 3.972.25 - '@aws-sdk/credential-provider-login': 3.972.26 - '@aws-sdk/credential-provider-process': 3.972.23 - '@aws-sdk/credential-provider-sso': 3.972.26 - '@aws-sdk/credential-provider-web-identity': 3.972.26 - '@aws-sdk/nested-clients': 3.996.16 - '@aws-sdk/types': 3.973.6 - '@smithy/credential-provider-imds': 4.2.12 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/credential-provider-env': 3.972.29 + '@aws-sdk/credential-provider-http': 3.972.31 + '@aws-sdk/credential-provider-login': 3.972.33 + '@aws-sdk/credential-provider-process': 3.972.29 + '@aws-sdk/credential-provider-sso': 3.972.33 + '@aws-sdk/credential-provider-web-identity': 3.972.33 + '@aws-sdk/nested-clients': 3.997.1 + '@aws-sdk/types': 3.973.8 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-login@3.972.26': + '@aws-sdk/credential-provider-login@3.972.33': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/nested-clients': 3.996.16 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/nested-clients': 3.997.1 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.972.27': + '@aws-sdk/credential-provider-node@3.972.34': dependencies: - '@aws-sdk/credential-provider-env': 3.972.23 - '@aws-sdk/credential-provider-http': 3.972.25 - '@aws-sdk/credential-provider-ini': 3.972.26 - '@aws-sdk/credential-provider-process': 3.972.23 - '@aws-sdk/credential-provider-sso': 3.972.26 - '@aws-sdk/credential-provider-web-identity': 3.972.26 - '@aws-sdk/types': 3.973.6 - '@smithy/credential-provider-imds': 4.2.12 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/credential-provider-env': 3.972.29 + '@aws-sdk/credential-provider-http': 3.972.31 + '@aws-sdk/credential-provider-ini': 3.972.33 + '@aws-sdk/credential-provider-process': 3.972.29 + '@aws-sdk/credential-provider-sso': 3.972.33 + '@aws-sdk/credential-provider-web-identity': 3.972.33 + '@aws-sdk/types': 3.973.8 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.972.23': + '@aws-sdk/credential-provider-process@3.972.29': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.972.26': + '@aws-sdk/credential-provider-sso@3.972.33': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/nested-clients': 3.996.16 - '@aws-sdk/token-providers': 3.1019.0 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/nested-clients': 3.997.1 + '@aws-sdk/token-providers': 3.1034.0 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.972.26': + '@aws-sdk/credential-provider-web-identity@3.972.33': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/nested-clients': 3.996.16 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/nested-clients': 3.997.1 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/lib-storage@3.1019.0(@aws-sdk/client-s3@3.1019.0)': + '@aws-sdk/lib-storage@3.1030.0(@aws-sdk/client-s3@3.1030.0)': dependencies: - '@aws-sdk/client-s3': 3.1019.0 - '@smithy/middleware-endpoint': 4.4.27 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.13.1 + '@aws-sdk/client-s3': 3.1030.0 + '@smithy/middleware-endpoint': 4.4.31 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 tslib: 2.8.1 - '@aws-sdk/middleware-bucket-endpoint@3.972.8': + '@aws-sdk/middleware-bucket-endpoint@3.972.10': dependencies: - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 '@aws-sdk/util-arn-parser': 3.972.3 - '@smithy/node-config-provider': 4.3.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 '@smithy/util-config-provider': 4.2.2 tslib: 2.8.1 - '@aws-sdk/middleware-expect-continue@3.972.8': + '@aws-sdk/middleware-expect-continue@3.972.10': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-flexible-checksums@3.974.5': + '@aws-sdk/middleware-flexible-checksums@3.974.11': dependencies: '@aws-crypto/crc32': 5.2.0 '@aws-crypto/crc32c': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/core': 3.973.25 - '@aws-sdk/crc64-nvme': 3.972.5 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/crc64-nvme': 3.972.7 + '@aws-sdk/types': 3.973.8 '@smithy/is-array-buffer': 4.2.2 - '@smithy/node-config-provider': 4.3.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-stream': 4.5.20 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-stream': 4.5.24 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@aws-sdk/middleware-host-header@3.972.8': + '@aws-sdk/middleware-host-header@3.972.10': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-location-constraint@3.972.8': + '@aws-sdk/middleware-location-constraint@3.972.10': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-logger@3.972.8': + '@aws-sdk/middleware-logger@3.972.10': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.972.9': + '@aws-sdk/middleware-recursion-detection@3.972.11': dependencies: - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 '@aws/lambda-invoke-store': 0.2.4 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.972.26': + '@aws-sdk/middleware-sdk-s3@3.972.32': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/types': 3.973.8 '@aws-sdk/util-arn-parser': 3.972.3 - '@smithy/core': 3.23.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/signature-v4': 5.3.12 - '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.13.1 + '@smithy/core': 3.23.16 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 '@smithy/util-config-provider': 4.2.2 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-stream': 4.5.20 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-stream': 4.5.24 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@aws-sdk/middleware-ssec@3.972.8': + '@aws-sdk/middleware-ssec@3.972.10': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.972.26': + '@aws-sdk/middleware-user-agent@3.972.33': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@smithy/core': 3.23.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-retry': 4.2.12 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@smithy/core': 3.23.16 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-retry': 4.3.3 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.996.16': + '@aws-sdk/nested-clients@3.997.1': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.25 - '@aws-sdk/middleware-host-header': 3.972.8 - '@aws-sdk/middleware-logger': 3.972.8 - '@aws-sdk/middleware-recursion-detection': 3.972.9 - '@aws-sdk/middleware-user-agent': 3.972.26 - '@aws-sdk/region-config-resolver': 3.972.10 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.12 - '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.12 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/hash-node': 4.2.12 - '@smithy/invalid-dependency': 4.2.12 - '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.27 - '@smithy/middleware-retry': 4.4.44 - '@smithy/middleware-serde': 4.2.15 - '@smithy/middleware-stack': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.5.0 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-user-agent': 3.972.33 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/signature-v4-multi-region': 3.996.20 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.19 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.16 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.31 + '@smithy/middleware-retry': 4.5.4 + '@smithy/middleware-serde': 4.2.19 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.0 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.43 - '@smithy/util-defaults-mode-node': 4.2.47 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.12 + '@smithy/util-defaults-mode-browser': 4.3.48 + '@smithy/util-defaults-mode-node': 4.2.53 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.3 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/region-config-resolver@3.972.10': + '@aws-sdk/region-config-resolver@3.972.13': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/config-resolver': 4.4.13 - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@smithy/config-resolver': 4.4.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.996.14': + '@aws-sdk/signature-v4-multi-region@3.996.20': dependencies: - '@aws-sdk/middleware-sdk-s3': 3.972.26 - '@aws-sdk/types': 3.973.6 - '@smithy/protocol-http': 5.3.12 - '@smithy/signature-v4': 5.3.12 - '@smithy/types': 4.13.1 + '@aws-sdk/middleware-sdk-s3': 3.972.32 + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1019.0': + '@aws-sdk/token-providers@3.1034.0': dependencies: - '@aws-sdk/core': 3.973.25 - '@aws-sdk/nested-clients': 3.996.16 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.3 + '@aws-sdk/nested-clients': 3.997.1 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/types@3.973.6': + '@aws-sdk/types@3.973.8': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 '@aws-sdk/util-arn-parser@3.972.3': dependencies: tslib: 2.8.1 - '@aws-sdk/util-endpoints@3.996.5': + '@aws-sdk/util-endpoints@3.996.8': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-endpoints': 3.3.3 + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-endpoints': 3.4.2 tslib: 2.8.1 '@aws-sdk/util-locate-window@3.965.5': dependencies: tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.972.8': + '@aws-sdk/util-user-agent-browser@3.972.10': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.973.12': + '@aws-sdk/util-user-agent-node@3.973.19': dependencies: - '@aws-sdk/middleware-user-agent': 3.972.26 - '@aws-sdk/types': 3.973.6 - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 + '@aws-sdk/middleware-user-agent': 3.972.33 + '@aws-sdk/types': 3.973.8 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 '@smithy/util-config-provider': 4.2.2 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.16': + '@aws-sdk/xml-builder@3.972.18': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 fast-xml-parser: 5.5.8 tslib: 2.8.1 @@ -5662,7 +5220,7 @@ snapshots: '@babel/types': 7.29.0 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 + jsesc: 3.0.2 '@babel/helper-annotate-as-pure@7.27.3': dependencies: @@ -5672,7 +5230,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 + browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -5816,91 +5374,91 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@biomejs/biome@2.4.9': + '@biomejs/biome@2.4.11': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.4.9 - '@biomejs/cli-darwin-x64': 2.4.9 - '@biomejs/cli-linux-arm64': 2.4.9 - '@biomejs/cli-linux-arm64-musl': 2.4.9 - '@biomejs/cli-linux-x64': 2.4.9 - '@biomejs/cli-linux-x64-musl': 2.4.9 - '@biomejs/cli-win32-arm64': 2.4.9 - '@biomejs/cli-win32-x64': 2.4.9 + '@biomejs/cli-darwin-arm64': 2.4.11 + '@biomejs/cli-darwin-x64': 2.4.11 + '@biomejs/cli-linux-arm64': 2.4.11 + '@biomejs/cli-linux-arm64-musl': 2.4.11 + '@biomejs/cli-linux-x64': 2.4.11 + '@biomejs/cli-linux-x64-musl': 2.4.11 + '@biomejs/cli-win32-arm64': 2.4.11 + '@biomejs/cli-win32-x64': 2.4.11 - '@biomejs/cli-darwin-arm64@2.4.9': + '@biomejs/cli-darwin-arm64@2.4.11': optional: true - '@biomejs/cli-darwin-x64@2.4.9': + '@biomejs/cli-darwin-x64@2.4.11': optional: true - '@biomejs/cli-linux-arm64-musl@2.4.9': + '@biomejs/cli-linux-arm64-musl@2.4.11': optional: true - '@biomejs/cli-linux-arm64@2.4.9': + '@biomejs/cli-linux-arm64@2.4.11': optional: true - '@biomejs/cli-linux-x64-musl@2.4.9': + '@biomejs/cli-linux-x64-musl@2.4.11': optional: true - '@biomejs/cli-linux-x64@2.4.9': + '@biomejs/cli-linux-x64@2.4.11': optional: true - '@biomejs/cli-win32-arm64@2.4.9': + '@biomejs/cli-win32-arm64@2.4.11': optional: true - '@biomejs/cli-win32-x64@2.4.9': + '@biomejs/cli-win32-x64@2.4.11': optional: true '@blazediff/core@1.9.1': {} '@date-fns/tz@1.4.1': {} - '@dnd-kit/accessibility@3.1.1(react@19.2.4)': + '@dnd-kit/accessibility@3.1.1(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 tslib: 2.8.1 - '@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@dnd-kit/core@6.3.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@dnd-kit/accessibility': 3.1.1(react@19.2.4) - '@dnd-kit/utilities': 3.2.2(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@dnd-kit/accessibility': 3.1.1(react@19.2.5) + '@dnd-kit/utilities': 3.2.2(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) tslib: 2.8.1 - '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)': dependencies: - '@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@dnd-kit/utilities': 3.2.2(react@19.2.4) - react: 19.2.4 + '@dnd-kit/core': 6.3.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@dnd-kit/utilities': 3.2.2(react@19.2.5) + react: 19.2.5 tslib: 2.8.1 - '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)': dependencies: - '@dnd-kit/core': 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@dnd-kit/utilities': 3.2.2(react@19.2.4) - react: 19.2.4 + '@dnd-kit/core': 6.3.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@dnd-kit/utilities': 3.2.2(react@19.2.5) + react: 19.2.5 tslib: 2.8.1 - '@dnd-kit/utilities@3.2.2(react@19.2.4)': + '@dnd-kit/utilities@3.2.2(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 tslib: 2.8.1 '@edgefirst-dev/data@0.0.4': {} - '@emnapi/core@1.9.1': + '@emnapi/core@1.9.2': dependencies: - '@emnapi/wasi-threads': 1.2.0 + '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.9.1': + '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.0': + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true @@ -5912,81 +5470,159 @@ snapshots: '@esbuild/aix-ppc64@0.27.4': optional: true + '@esbuild/aix-ppc64@0.27.7': + optional: true + '@esbuild/android-arm64@0.27.4': optional: true + '@esbuild/android-arm64@0.27.7': + optional: true + '@esbuild/android-arm@0.27.4': optional: true + '@esbuild/android-arm@0.27.7': + optional: true + '@esbuild/android-x64@0.27.4': optional: true + '@esbuild/android-x64@0.27.7': + optional: true + '@esbuild/darwin-arm64@0.27.4': optional: true + '@esbuild/darwin-arm64@0.27.7': + optional: true + '@esbuild/darwin-x64@0.27.4': optional: true + '@esbuild/darwin-x64@0.27.7': + optional: true + '@esbuild/freebsd-arm64@0.27.4': optional: true + '@esbuild/freebsd-arm64@0.27.7': + optional: true + '@esbuild/freebsd-x64@0.27.4': optional: true + '@esbuild/freebsd-x64@0.27.7': + optional: true + '@esbuild/linux-arm64@0.27.4': optional: true + '@esbuild/linux-arm64@0.27.7': + optional: true + '@esbuild/linux-arm@0.27.4': optional: true + '@esbuild/linux-arm@0.27.7': + optional: true + '@esbuild/linux-ia32@0.27.4': optional: true + '@esbuild/linux-ia32@0.27.7': + optional: true + '@esbuild/linux-loong64@0.27.4': optional: true + '@esbuild/linux-loong64@0.27.7': + optional: true + '@esbuild/linux-mips64el@0.27.4': optional: true + '@esbuild/linux-mips64el@0.27.7': + optional: true + '@esbuild/linux-ppc64@0.27.4': optional: true + '@esbuild/linux-ppc64@0.27.7': + optional: true + '@esbuild/linux-riscv64@0.27.4': optional: true + '@esbuild/linux-riscv64@0.27.7': + optional: true + '@esbuild/linux-s390x@0.27.4': optional: true + '@esbuild/linux-s390x@0.27.7': + optional: true + '@esbuild/linux-x64@0.27.4': optional: true + '@esbuild/linux-x64@0.27.7': + optional: true + '@esbuild/netbsd-arm64@0.27.4': optional: true + '@esbuild/netbsd-arm64@0.27.7': + optional: true + '@esbuild/netbsd-x64@0.27.4': optional: true + '@esbuild/netbsd-x64@0.27.7': + optional: true + '@esbuild/openbsd-arm64@0.27.4': optional: true + '@esbuild/openbsd-arm64@0.27.7': + optional: true + '@esbuild/openbsd-x64@0.27.4': optional: true + '@esbuild/openbsd-x64@0.27.7': + optional: true + '@esbuild/openharmony-arm64@0.27.4': optional: true + '@esbuild/openharmony-arm64@0.27.7': + optional: true + '@esbuild/sunos-x64@0.27.4': optional: true + '@esbuild/sunos-x64@0.27.7': + optional: true + '@esbuild/win32-arm64@0.27.4': optional: true + '@esbuild/win32-arm64@0.27.7': + optional: true + '@esbuild/win32-ia32@0.27.4': optional: true + '@esbuild/win32-ia32@0.27.7': + optional: true + '@esbuild/win32-x64@0.27.4': optional: true + '@esbuild/win32-x64@0.27.7': + optional: true + '@faker-js/faker@10.4.0': {} '@floating-ui/core@1.7.5': @@ -5998,75 +5634,35 @@ snapshots: '@floating-ui/core': 1.7.5 '@floating-ui/utils': 0.2.11 - '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@floating-ui/react-dom@2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@floating-ui/dom': 1.7.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) '@floating-ui/utils@0.2.11': {} - '@formatjs/bigdecimal@0.2.0': {} + '@formatjs/fast-memoize@3.1.2': {} - '@formatjs/ecma402-abstract@2.3.6': + '@formatjs/intl-durationformat@0.10.4': dependencies: - '@formatjs/fast-memoize': 2.2.7 - '@formatjs/intl-localematcher': 0.6.2 - decimal.js: 10.6.0 - tslib: 2.8.1 + '@formatjs/intl-localematcher': 0.8.3 - '@formatjs/ecma402-abstract@3.2.0': + '@formatjs/intl-localematcher@0.8.3': dependencies: - '@formatjs/bigdecimal': 0.2.0 - '@formatjs/fast-memoize': 3.1.1 - '@formatjs/intl-localematcher': 0.8.2 + '@formatjs/fast-memoize': 3.1.2 - '@formatjs/fast-memoize@2.2.7': + '@internationalized/date@3.12.1': dependencies: - tslib: 2.8.1 + '@swc/helpers': 0.5.21 - '@formatjs/fast-memoize@3.1.1': {} - - '@formatjs/icu-messageformat-parser@2.11.4': + '@internationalized/number@3.6.6': dependencies: - '@formatjs/ecma402-abstract': 2.3.6 - '@formatjs/icu-skeleton-parser': 1.8.16 - tslib: 2.8.1 + '@swc/helpers': 0.5.21 - '@formatjs/icu-skeleton-parser@1.8.16': + '@internationalized/string@3.2.8': dependencies: - '@formatjs/ecma402-abstract': 2.3.6 - tslib: 2.8.1 - - '@formatjs/intl-durationformat@0.10.3': - dependencies: - '@formatjs/ecma402-abstract': 3.2.0 - '@formatjs/intl-localematcher': 0.8.2 - - '@formatjs/intl-localematcher@0.6.2': - dependencies: - tslib: 2.8.1 - - '@formatjs/intl-localematcher@0.8.2': - dependencies: - '@formatjs/fast-memoize': 3.1.1 - - '@internationalized/date@3.12.0': - dependencies: - '@swc/helpers': 0.5.20 - - '@internationalized/message@3.1.8': - dependencies: - '@swc/helpers': 0.5.20 - intl-messageformat: 10.7.18 - - '@internationalized/number@3.6.5': - dependencies: - '@swc/helpers': 0.5.20 - - '@internationalized/string@3.2.7': - dependencies: - '@swc/helpers': 0.5.20 + '@swc/helpers': 0.5.21 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -6091,17 +5687,10 @@ snapshots: '@mjackson/node-fetch-server@0.2.0': {} - '@napi-rs/wasm-runtime@1.1.1': + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@emnapi/core': 1.9.1 - '@emnapi/runtime': 1.9.1 - '@tybys/wasm-util': 0.10.1 - optional: true - - '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': - dependencies: - '@emnapi/core': 1.9.1 - '@emnapi/runtime': 1.9.1 + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 '@tybys/wasm-util': 0.10.1 optional: true @@ -6184,9 +5773,9 @@ snapshots: '@oxc-parser/binding-openharmony-arm64@0.121.0': optional: true - '@oxc-parser/binding-wasm32-wasi@0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + '@oxc-parser/binding-wasm32-wasi@0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -6203,7 +5792,7 @@ snapshots: '@oxc-project/types@0.121.0': {} - '@oxc-project/types@0.122.0': {} + '@oxc-project/types@0.124.0': {} '@oxc-resolver/binding-android-arm-eabi@11.19.1': optional: true @@ -6253,9 +5842,9 @@ snapshots: '@oxc-resolver/binding-openharmony-arm64@11.19.1': optional: true - '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -6270,9 +5859,9 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.19.1': optional: true - '@playwright/test@1.58.2': + '@playwright/test@1.59.1': dependencies: - playwright: 1.58.2 + playwright: 1.59.1 '@polka/url@1.0.0-next.29': {} @@ -6282,1007 +5871,372 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) '@radix-ui/rect': 1.1.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.5)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.5)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.5)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: '@radix-ui/rect': 1.1.1 - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) '@radix-ui/rect@1.1.1': {} - '@react-aria/autocomplete@3.0.0-rc.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/combobox': 3.15.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/listbox': 3.15.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/searchfield': 3.8.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/textfield': 3.18.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/autocomplete': 3.0.0-beta.4(react@19.2.4) - '@react-stately/combobox': 3.13.0(react@19.2.4) - '@react-types/autocomplete': 3.0.0-alpha.38(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/breadcrumbs@3.5.32(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/link': 3.8.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/breadcrumbs': 3.7.19(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/button@3.14.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/toolbar': 3.0.0-beta.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/toggle': 3.9.5(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/calendar@3.9.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@internationalized/date': 3.12.0 - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/calendar': 3.9.3(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/calendar': 3.8.3(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/checkbox@3.16.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/form': 3.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/label': 3.7.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/toggle': 3.12.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/checkbox': 3.7.5(react@19.2.4) - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-stately/toggle': 3.9.5(react@19.2.4) - '@react-types/checkbox': 3.10.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/collections@3.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/ssr': 3.9.10(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - use-sync-external-store: 1.6.0(react@19.2.4) - - '@react-aria/color@3.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/numberfield': 3.12.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/slider': 3.8.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/spinbutton': 3.7.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/textfield': 3.18.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/visually-hidden': 3.8.31(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/color': 3.9.5(react@19.2.4) - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-types/color': 3.1.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/combobox@3.15.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/listbox': 3.15.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/menu': 3.21.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/overlays': 3.31.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/selection': 3.27.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/textfield': 3.18.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/collections': 3.12.10(react@19.2.4) - '@react-stately/combobox': 3.13.0(react@19.2.4) - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/combobox': 3.14.0(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/datepicker@3.16.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@internationalized/date': 3.12.0 - '@internationalized/number': 3.6.5 - '@internationalized/string': 3.2.7 - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/form': 3.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/label': 3.7.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/spinbutton': 3.7.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/datepicker': 3.16.1(react@19.2.4) - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/calendar': 3.8.3(react@19.2.4) - '@react-types/datepicker': 3.13.5(react@19.2.4) - '@react-types/dialog': 3.5.24(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/dialog@3.5.34(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/overlays': 3.31.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/dialog': 3.5.24(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/disclosure@3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/ssr': 3.9.10(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/disclosure': 3.0.11(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/dnd@3.11.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@internationalized/string': 3.2.7 - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/overlays': 3.31.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/collections': 3.12.10(react@19.2.4) - '@react-stately/dnd': 3.7.4(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/focus@3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - clsx: 2.1.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/form@3.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/grid@3.14.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/selection': 3.27.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/collections': 3.12.10(react@19.2.4) - '@react-stately/grid': 3.11.9(react@19.2.4) - '@react-stately/selection': 3.20.9(react@19.2.4) - '@react-types/checkbox': 3.10.4(react@19.2.4) - '@react-types/grid': 3.3.8(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/gridlist@3.14.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/grid': 3.14.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/selection': 3.27.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/list': 3.13.4(react@19.2.4) - '@react-stately/tree': 3.9.6(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/i18n@3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@internationalized/date': 3.12.0 - '@internationalized/message': 3.1.8 - '@internationalized/number': 3.6.5 - '@internationalized/string': 3.2.7 - '@react-aria/ssr': 3.9.10(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/interactions@3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/ssr': 3.9.10(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/flags': 3.1.2 - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/label@3.7.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/landmark@3.0.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - use-sync-external-store: 1.6.0(react@19.2.4) - - '@react-aria/link@3.8.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/link': 3.6.7(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/listbox@3.15.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/label': 3.7.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/selection': 3.27.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/collections': 3.12.10(react@19.2.4) - '@react-stately/list': 3.13.4(react@19.2.4) - '@react-types/listbox': 3.7.6(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/live-announcer@3.4.4': - dependencies: - '@swc/helpers': 0.5.20 - - '@react-aria/menu@3.21.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/overlays': 3.31.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/selection': 3.27.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/collections': 3.12.10(react@19.2.4) - '@react-stately/menu': 3.9.11(react@19.2.4) - '@react-stately/selection': 3.20.9(react@19.2.4) - '@react-stately/tree': 3.9.6(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/menu': 3.10.7(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/meter@3.4.30(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/progress': 3.4.30(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/meter': 3.4.15(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/numberfield@3.12.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/spinbutton': 3.7.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/textfield': 3.18.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-stately/numberfield': 3.11.0(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/numberfield': 3.8.18(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/overlays@3.31.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/ssr': 3.9.10(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/visually-hidden': 3.8.31(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/flags': 3.1.2 - '@react-stately/overlays': 3.6.23(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/overlays': 3.9.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/progress@3.4.30(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/label': 3.7.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/progress': 3.5.18(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/radio@3.12.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/form': 3.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/label': 3.7.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/radio': 3.11.5(react@19.2.4) - '@react-types/radio': 3.9.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/searchfield@3.8.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/textfield': 3.18.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/searchfield': 3.5.19(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/searchfield': 3.6.8(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/select@3.17.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/form': 3.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/label': 3.7.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/listbox': 3.15.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/menu': 3.21.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/selection': 3.27.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/visually-hidden': 3.8.31(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/select': 3.9.2(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/select': 3.12.2(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/selection@3.27.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/selection': 3.20.9(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/separator@3.4.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/slider@3.8.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/label': 3.7.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/slider': 3.7.5(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@react-types/slider': 3.8.4(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/spinbutton@3.7.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/ssr@3.9.10(react@19.2.4)': - dependencies: - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-aria/switch@3.7.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/toggle': 3.12.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/toggle': 3.9.5(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@react-types/switch': 3.5.17(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/table@3.17.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/grid': 3.14.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/visually-hidden': 3.8.31(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/collections': 3.12.10(react@19.2.4) - '@react-stately/flags': 3.1.2 - '@react-stately/table': 3.15.4(react@19.2.4) - '@react-types/checkbox': 3.10.4(react@19.2.4) - '@react-types/grid': 3.3.8(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@react-types/table': 3.13.6(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/tabs@3.11.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/selection': 3.27.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/tabs': 3.8.9(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@react-types/tabs': 3.3.22(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/tag@3.8.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/gridlist': 3.14.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/label': 3.7.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/selection': 3.27.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/list': 3.13.4(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/textfield@3.18.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/form': 3.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/label': 3.7.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@react-types/textfield': 3.12.8(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/toast@3.0.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/landmark': 3.0.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/toast': 3.1.3(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/toggle@3.12.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/toggle': 3.9.5(react@19.2.4) - '@react-types/checkbox': 3.10.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/toolbar@3.0.0-beta.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/tooltip@3.9.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/tooltip': 3.5.11(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@react-types/tooltip': 3.5.2(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/tree@3.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/gridlist': 3.14.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/selection': 3.27.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/tree': 3.9.6(react@19.2.4) - '@react-types/button': 3.15.1(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/utils@3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/ssr': 3.9.10(react@19.2.4) - '@react-stately/flags': 3.1.2 - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - clsx: 2.1.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/virtualizer@4.1.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/virtualizer': 4.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-aria/visually-hidden@3.8.31(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-router/dev@7.14.0(@react-router/serve@7.14.0(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3))(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(tsx@4.21.0)(typescript@5.9.3)(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)': + '@react-router/dev@7.14.1(@react-router/serve@7.14.1(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3))(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(tsx@4.21.0)(typescript@6.0.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -7291,7 +6245,7 @@ snapshots: '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 - '@react-router/node': 7.14.0(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + '@react-router/node': 7.14.1(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3) '@remix-run/node-fetch-server': 0.13.0 arg: 5.0.2 babel-dead-code-elimination: 1.0.12 @@ -7299,24 +6253,24 @@ snapshots: dedent: 1.7.2 es-module-lexer: 1.7.0 exit-hook: 2.2.1 - isbot: 5.1.36 + isbot: 5.1.38 jsesc: 3.0.2 - lodash: 4.17.23 + lodash: 4.18.1 p-map: 7.0.4 pathe: 1.1.2 picocolors: 1.1.1 pkg-types: 2.3.0 - prettier: 3.8.1 + prettier: 3.8.3 react-refresh: 0.14.2 - react-router: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-router: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) semver: 7.7.4 - tinyglobby: 0.2.15 - valibot: 1.3.1(typescript@5.9.3) - vite: 8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) - vite-node: 3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + tinyglobby: 0.2.16 + valibot: 1.3.1(typescript@6.0.3) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite-node: 3.2.4(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) optionalDependencies: - '@react-router/serve': 7.14.0(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) - typescript: 5.9.3 + '@react-router/serve': 7.14.1(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -7332,445 +6286,39 @@ snapshots: - tsx - yaml - '@react-router/express@7.14.0(express@4.22.1)(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': + '@react-router/express@7.14.1(express@4.22.1)(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3)': dependencies: - '@react-router/node': 7.14.0(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + '@react-router/node': 7.14.1(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3) express: 4.22.1 - react-router: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-router: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 - '@react-router/node@7.14.0(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': + '@react-router/node@7.14.1(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3)': dependencies: '@mjackson/node-fetch-server': 0.2.0 - react-router: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-router: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 - '@react-router/serve@7.14.0(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)': + '@react-router/serve@7.14.1(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3)': dependencies: '@mjackson/node-fetch-server': 0.2.0 - '@react-router/express': 7.14.0(express@4.22.1)(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) - '@react-router/node': 7.14.0(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + '@react-router/express': 7.14.1(express@4.22.1)(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3) + '@react-router/node': 7.14.1(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.3) compression: 1.8.1 express: 4.22.1 get-port: 5.1.1 morgan: 1.10.1 - react-router: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-router: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) source-map-support: 0.5.21 transitivePeerDependencies: - supports-color - typescript - '@react-stately/autocomplete@3.0.0-beta.4(react@19.2.4)': + '@react-types/shared@3.34.0(react@19.2.5)': dependencies: - '@react-stately/utils': 3.11.0(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/calendar@3.9.3(react@19.2.4)': - dependencies: - '@internationalized/date': 3.12.0 - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/calendar': 3.8.3(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/checkbox@3.7.5(react@19.2.4)': - dependencies: - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/checkbox': 3.10.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/collections@3.12.10(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/color@3.9.5(react@19.2.4)': - dependencies: - '@internationalized/number': 3.6.5 - '@internationalized/string': 3.2.7 - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-stately/numberfield': 3.11.0(react@19.2.4) - '@react-stately/slider': 3.7.5(react@19.2.4) - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/color': 3.1.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/combobox@3.13.0(react@19.2.4)': - dependencies: - '@react-stately/collections': 3.12.10(react@19.2.4) - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-stately/list': 3.13.4(react@19.2.4) - '@react-stately/overlays': 3.6.23(react@19.2.4) - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/combobox': 3.14.0(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/data@3.15.2(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/datepicker@3.16.1(react@19.2.4)': - dependencies: - '@internationalized/date': 3.12.0 - '@internationalized/number': 3.6.5 - '@internationalized/string': 3.2.7 - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-stately/overlays': 3.6.23(react@19.2.4) - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/datepicker': 3.13.5(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/disclosure@3.0.11(react@19.2.4)': - dependencies: - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/dnd@3.7.4(react@19.2.4)': - dependencies: - '@react-stately/selection': 3.20.9(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/flags@3.1.2': - dependencies: - '@swc/helpers': 0.5.20 - - '@react-stately/form@3.2.4(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/grid@3.11.9(react@19.2.4)': - dependencies: - '@react-stately/collections': 3.12.10(react@19.2.4) - '@react-stately/selection': 3.20.9(react@19.2.4) - '@react-types/grid': 3.3.8(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/layout@4.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-stately/collections': 3.12.10(react@19.2.4) - '@react-stately/table': 3.15.4(react@19.2.4) - '@react-stately/virtualizer': 4.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/grid': 3.3.8(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@react-types/table': 3.13.6(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-stately/list@3.13.4(react@19.2.4)': - dependencies: - '@react-stately/collections': 3.12.10(react@19.2.4) - '@react-stately/selection': 3.20.9(react@19.2.4) - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/menu@3.9.11(react@19.2.4)': - dependencies: - '@react-stately/overlays': 3.6.23(react@19.2.4) - '@react-types/menu': 3.10.7(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/numberfield@3.11.0(react@19.2.4)': - dependencies: - '@internationalized/number': 3.6.5 - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/numberfield': 3.8.18(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/overlays@3.6.23(react@19.2.4)': - dependencies: - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/overlays': 3.9.4(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/radio@3.11.5(react@19.2.4)': - dependencies: - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/radio': 3.9.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/searchfield@3.5.19(react@19.2.4)': - dependencies: - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/searchfield': 3.6.8(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/select@3.9.2(react@19.2.4)': - dependencies: - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-stately/list': 3.13.4(react@19.2.4) - '@react-stately/overlays': 3.6.23(react@19.2.4) - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/select': 3.12.2(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/selection@3.20.9(react@19.2.4)': - dependencies: - '@react-stately/collections': 3.12.10(react@19.2.4) - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/slider@3.7.5(react@19.2.4)': - dependencies: - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@react-types/slider': 3.8.4(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/table@3.15.4(react@19.2.4)': - dependencies: - '@react-stately/collections': 3.12.10(react@19.2.4) - '@react-stately/flags': 3.1.2 - '@react-stately/grid': 3.11.9(react@19.2.4) - '@react-stately/selection': 3.20.9(react@19.2.4) - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/grid': 3.3.8(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@react-types/table': 3.13.6(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/tabs@3.8.9(react@19.2.4)': - dependencies: - '@react-stately/list': 3.13.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@react-types/tabs': 3.3.22(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/toast@3.1.3(react@19.2.4)': - dependencies: - '@swc/helpers': 0.5.20 - react: 19.2.4 - use-sync-external-store: 1.6.0(react@19.2.4) - - '@react-stately/toggle@3.9.5(react@19.2.4)': - dependencies: - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/checkbox': 3.10.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/tooltip@3.5.11(react@19.2.4)': - dependencies: - '@react-stately/overlays': 3.6.23(react@19.2.4) - '@react-types/tooltip': 3.5.2(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/tree@3.9.6(react@19.2.4)': - dependencies: - '@react-stately/collections': 3.12.10(react@19.2.4) - '@react-stately/selection': 3.20.9(react@19.2.4) - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/utils@3.11.0(react@19.2.4)': - dependencies: - '@swc/helpers': 0.5.20 - react: 19.2.4 - - '@react-stately/virtualizer@4.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@react-types/autocomplete@3.0.0-alpha.38(react@19.2.4)': - dependencies: - '@react-types/combobox': 3.14.0(react@19.2.4) - '@react-types/searchfield': 3.6.8(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/breadcrumbs@3.7.19(react@19.2.4)': - dependencies: - '@react-types/link': 3.6.7(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/button@3.15.1(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/calendar@3.8.3(react@19.2.4)': - dependencies: - '@internationalized/date': 3.12.0 - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/checkbox@3.10.4(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/color@3.1.4(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - '@react-types/slider': 3.8.4(react@19.2.4) - react: 19.2.4 - - '@react-types/combobox@3.14.0(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/datepicker@3.13.5(react@19.2.4)': - dependencies: - '@internationalized/date': 3.12.0 - '@react-types/calendar': 3.8.3(react@19.2.4) - '@react-types/overlays': 3.9.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/dialog@3.5.24(react@19.2.4)': - dependencies: - '@react-types/overlays': 3.9.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/form@3.7.18(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/grid@3.3.8(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/link@3.6.7(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/listbox@3.7.6(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/menu@3.10.7(react@19.2.4)': - dependencies: - '@react-types/overlays': 3.9.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/meter@3.4.15(react@19.2.4)': - dependencies: - '@react-types/progress': 3.5.18(react@19.2.4) - react: 19.2.4 - - '@react-types/numberfield@3.8.18(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/overlays@3.9.4(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/progress@3.5.18(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/radio@3.9.4(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/searchfield@3.6.8(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - '@react-types/textfield': 3.12.8(react@19.2.4) - react: 19.2.4 - - '@react-types/select@3.12.2(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/shared@3.33.1(react@19.2.4)': - dependencies: - react: 19.2.4 - - '@react-types/slider@3.8.4(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/switch@3.5.17(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/table@3.13.6(react@19.2.4)': - dependencies: - '@react-types/grid': 3.3.8(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/tabs@3.3.22(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/textfield@3.12.8(react@19.2.4)': - dependencies: - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - - '@react-types/tooltip@3.5.2(react@19.2.4)': - dependencies: - '@react-types/overlays': 3.9.4(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 + react: 19.2.5 '@remirror/core-constants@3.0.0': {} @@ -7786,134 +6334,134 @@ snapshots: '@remix-run/node-fetch-server@0.13.0': {} - '@rolldown/binding-android-arm64@1.0.0-rc.12': + '@rolldown/binding-android-arm64@1.0.0-rc.15': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.12': + '@rolldown/binding-darwin-x64@1.0.0-rc.15': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': optional: true - '@rolldown/pluginutils@1.0.0-rc.12': {} + '@rolldown/pluginutils@1.0.0-rc.15': {} - '@rollup/rollup-android-arm-eabi@4.60.0': + '@rollup/rollup-android-arm-eabi@4.60.2': optional: true - '@rollup/rollup-android-arm64@4.60.0': + '@rollup/rollup-android-arm64@4.60.2': optional: true - '@rollup/rollup-darwin-arm64@4.60.0': + '@rollup/rollup-darwin-arm64@4.60.2': optional: true - '@rollup/rollup-darwin-x64@4.60.0': + '@rollup/rollup-darwin-x64@4.60.2': optional: true - '@rollup/rollup-freebsd-arm64@4.60.0': + '@rollup/rollup-freebsd-arm64@4.60.2': optional: true - '@rollup/rollup-freebsd-x64@4.60.0': + '@rollup/rollup-freebsd-x64@4.60.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.0': + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.0': + '@rollup/rollup-linux-arm-musleabihf@4.60.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.0': + '@rollup/rollup-linux-arm64-gnu@4.60.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.60.0': + '@rollup/rollup-linux-arm64-musl@4.60.2': optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.0': + '@rollup/rollup-linux-loong64-gnu@4.60.2': optional: true - '@rollup/rollup-linux-loong64-musl@4.60.0': + '@rollup/rollup-linux-loong64-musl@4.60.2': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.0': + '@rollup/rollup-linux-ppc64-gnu@4.60.2': optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.0': + '@rollup/rollup-linux-ppc64-musl@4.60.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.0': + '@rollup/rollup-linux-riscv64-gnu@4.60.2': optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.0': + '@rollup/rollup-linux-riscv64-musl@4.60.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.0': + '@rollup/rollup-linux-s390x-gnu@4.60.2': optional: true '@rollup/rollup-linux-x64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-x64-musl@4.60.0': + '@rollup/rollup-linux-x64-gnu@4.60.2': optional: true - '@rollup/rollup-openbsd-x64@4.60.0': + '@rollup/rollup-linux-x64-musl@4.60.2': optional: true - '@rollup/rollup-openharmony-arm64@4.60.0': + '@rollup/rollup-openbsd-x64@4.60.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.0': + '@rollup/rollup-openharmony-arm64@4.60.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.0': + '@rollup/rollup-win32-arm64-msvc@4.60.2': optional: true - '@rollup/rollup-win32-x64-gnu@4.60.0': + '@rollup/rollup-win32-ia32-msvc@4.60.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.60.0': + '@rollup/rollup-win32-x64-gnu@4.60.2': optional: true - '@smithy/abort-controller@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 + '@rollup/rollup-win32-x64-msvc@4.60.2': + optional: true '@smithy/chunked-blob-reader-native@4.2.3': dependencies: @@ -7924,97 +6472,97 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/config-resolver@4.4.13': + '@smithy/config-resolver@4.4.17': dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 '@smithy/util-config-provider': 4.2.2 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 tslib: 2.8.1 - '@smithy/core@3.23.12': + '@smithy/core@3.23.16': dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-stream': 4.5.20 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-stream': 4.5.24 '@smithy/util-utf8': 4.2.2 '@smithy/uuid': 1.1.2 tslib: 2.8.1 - '@smithy/credential-provider-imds@4.2.12': + '@smithy/credential-provider-imds@4.2.14': dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/property-provider': 4.2.12 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 tslib: 2.8.1 - '@smithy/eventstream-codec@4.2.12': + '@smithy/eventstream-codec@4.2.14': dependencies: '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 '@smithy/util-hex-encoding': 4.2.2 tslib: 2.8.1 - '@smithy/eventstream-serde-browser@4.2.12': + '@smithy/eventstream-serde-browser@4.2.14': dependencies: - '@smithy/eventstream-serde-universal': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/eventstream-serde-universal': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/eventstream-serde-config-resolver@4.3.12': + '@smithy/eventstream-serde-config-resolver@4.3.14': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/eventstream-serde-node@4.2.12': + '@smithy/eventstream-serde-node@4.2.14': dependencies: - '@smithy/eventstream-serde-universal': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/eventstream-serde-universal': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/eventstream-serde-universal@4.2.12': + '@smithy/eventstream-serde-universal@4.2.14': dependencies: - '@smithy/eventstream-codec': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/eventstream-codec': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/fetch-http-handler@5.3.15': + '@smithy/fetch-http-handler@5.3.17': dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 '@smithy/util-base64': 4.3.2 tslib: 2.8.1 - '@smithy/hash-blob-browser@4.2.13': + '@smithy/hash-blob-browser@4.2.15': dependencies: '@smithy/chunked-blob-reader': 5.2.2 '@smithy/chunked-blob-reader-native': 4.2.3 - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/hash-node@4.2.12': + '@smithy/hash-node@4.2.14': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 '@smithy/util-buffer-from': 4.2.2 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@smithy/hash-stream-node@4.2.12': + '@smithy/hash-stream-node@4.2.14': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@smithy/invalid-dependency@4.2.12': + '@smithy/invalid-dependency@4.2.14': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 '@smithy/is-array-buffer@2.2.0': @@ -8025,127 +6573,127 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/md5-js@4.2.12': + '@smithy/md5-js@4.2.14': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@smithy/middleware-content-length@4.2.12': + '@smithy/middleware-content-length@4.2.14': dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/middleware-endpoint@4.4.27': + '@smithy/middleware-endpoint@4.4.31': dependencies: - '@smithy/core': 3.23.12 - '@smithy/middleware-serde': 4.2.15 - '@smithy/node-config-provider': 4.3.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-middleware': 4.2.12 + '@smithy/core': 3.23.16 + '@smithy/middleware-serde': 4.2.19 + '@smithy/node-config-provider': 4.3.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-middleware': 4.2.14 tslib: 2.8.1 - '@smithy/middleware-retry@4.4.44': + '@smithy/middleware-retry@4.5.4': dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/service-error-classification': 4.2.12 - '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.13.1 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.12 + '@smithy/core': 3.23.16 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/service-error-classification': 4.3.0 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.3 '@smithy/uuid': 1.1.2 tslib: 2.8.1 - '@smithy/middleware-serde@4.2.15': + '@smithy/middleware-serde@4.2.19': dependencies: - '@smithy/core': 3.23.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/core': 3.23.16 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/middleware-stack@4.2.12': + '@smithy/middleware-stack@4.2.14': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/node-config-provider@4.3.12': + '@smithy/node-config-provider@4.3.14': dependencies: - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/node-http-handler@4.5.0': + '@smithy/node-http-handler@4.6.0': dependencies: - '@smithy/abort-controller': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/property-provider@4.2.12': + '@smithy/property-provider@4.2.14': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/protocol-http@5.3.12': + '@smithy/protocol-http@5.3.14': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/querystring-builder@4.2.12': + '@smithy/querystring-builder@4.2.14': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 '@smithy/util-uri-escape': 4.2.2 tslib: 2.8.1 - '@smithy/querystring-parser@4.2.12': + '@smithy/querystring-parser@4.2.14': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/service-error-classification@4.2.12': + '@smithy/service-error-classification@4.3.0': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 - '@smithy/shared-ini-file-loader@4.4.7': + '@smithy/shared-ini-file-loader@4.4.9': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/signature-v4@5.3.12': + '@smithy/signature-v4@5.3.14': dependencies: '@smithy/is-array-buffer': 4.2.2 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 '@smithy/util-hex-encoding': 4.2.2 - '@smithy/util-middleware': 4.2.12 + '@smithy/util-middleware': 4.2.14 '@smithy/util-uri-escape': 4.2.2 '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - '@smithy/smithy-client@4.12.7': + '@smithy/smithy-client@4.12.12': dependencies: - '@smithy/core': 3.23.12 - '@smithy/middleware-endpoint': 4.4.27 - '@smithy/middleware-stack': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-stream': 4.5.20 + '@smithy/core': 3.23.16 + '@smithy/middleware-endpoint': 4.4.31 + '@smithy/middleware-stack': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-stream': 4.5.24 tslib: 2.8.1 - '@smithy/types@4.13.1': + '@smithy/types@4.14.1': dependencies: tslib: 2.8.1 - '@smithy/url-parser@4.2.12': + '@smithy/url-parser@4.2.14': dependencies: - '@smithy/querystring-parser': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/querystring-parser': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 '@smithy/util-base64@4.3.2': @@ -8176,49 +6724,49 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.3.43': + '@smithy/util-defaults-mode-browser@4.3.48': dependencies: - '@smithy/property-provider': 4.2.12 - '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.13.1 + '@smithy/property-provider': 4.2.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.2.47': + '@smithy/util-defaults-mode-node@4.2.53': dependencies: - '@smithy/config-resolver': 4.4.13 - '@smithy/credential-provider-imds': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/property-provider': 4.2.12 - '@smithy/smithy-client': 4.12.7 - '@smithy/types': 4.13.1 + '@smithy/config-resolver': 4.4.17 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/smithy-client': 4.12.12 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/util-endpoints@3.3.3': + '@smithy/util-endpoints@3.4.2': dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 '@smithy/util-hex-encoding@4.2.2': dependencies: tslib: 2.8.1 - '@smithy/util-middleware@4.2.12': + '@smithy/util-middleware@4.2.14': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/util-retry@4.2.12': + '@smithy/util-retry@4.3.3': dependencies: - '@smithy/service-error-classification': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/service-error-classification': 4.3.0 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@smithy/util-stream@4.5.20': + '@smithy/util-stream@4.5.24': dependencies: - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/node-http-handler': 4.5.0 - '@smithy/types': 4.13.1 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/node-http-handler': 4.6.0 + '@smithy/types': 4.14.1 '@smithy/util-base64': 4.3.2 '@smithy/util-buffer-from': 4.2.2 '@smithy/util-hex-encoding': 4.2.2 @@ -8239,10 +6787,9 @@ snapshots: '@smithy/util-buffer-from': 4.2.2 tslib: 2.8.1 - '@smithy/util-waiter@4.2.13': + '@smithy/util-waiter@4.2.16': dependencies: - '@smithy/abort-controller': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 '@smithy/uuid@1.1.2': @@ -8251,7 +6798,7 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@swc/helpers@0.5.20': + '@swc/helpers@0.5.21': dependencies: tslib: 2.8.1 @@ -8383,7 +6930,7 @@ snapshots: prosemirror-transform: 1.11.0 prosemirror-view: 1.41.7 - '@tiptap/react@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tiptap/react@2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) '@tiptap/extension-bubble-menu': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) @@ -8391,9 +6938,9 @@ snapshots: '@tiptap/pm': 2.27.2 '@types/use-sync-external-store': 0.0.6 fast-deep-equal: 3.1.3 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - use-sync-external-store: 1.6.0(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + use-sync-external-store: 1.6.0(react@19.2.5) '@tiptap/starter-kit@2.27.2': dependencies: @@ -8419,63 +6966,63 @@ snapshots: '@tiptap/extension-text-style': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) '@tiptap/pm': 2.27.2 - '@tldraw/editor@3.12.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tldraw/editor@3.12.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) '@tiptap/pm': 2.27.2 - '@tiptap/react': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tiptap/react': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tldraw/state': 3.12.1 - '@tldraw/state-react': 3.12.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tldraw/store': 3.12.1(react@19.2.4) - '@tldraw/tlschema': 3.12.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tldraw/state-react': 3.12.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tldraw/store': 3.12.1(react@19.2.5) + '@tldraw/tlschema': 3.12.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tldraw/utils': 3.12.1 '@tldraw/validate': 3.12.1 '@types/core-js': 2.5.8 - '@use-gesture/react': 10.3.1(react@19.2.4) + '@use-gesture/react': 10.3.1(react@19.2.5) classnames: 2.5.1 core-js: 3.49.0 eventemitter3: 4.0.7 idb: 7.1.1 is-plain-object: 5.0.0 lodash.isequal: 4.5.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) - '@tldraw/state-react@3.12.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tldraw/state-react@3.12.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@tldraw/state': 3.12.1 '@tldraw/utils': 3.12.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) '@tldraw/state@3.12.1': dependencies: '@tldraw/utils': 3.12.1 - '@tldraw/store@3.12.1(react@19.2.4)': + '@tldraw/store@3.12.1(react@19.2.5)': dependencies: '@tldraw/state': 3.12.1 '@tldraw/utils': 3.12.1 lodash.isequal: 4.5.0 - react: 19.2.4 + react: 19.2.5 - '@tldraw/tldraw@3.12.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tldraw/tldraw@3.12.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - tldraw: 3.12.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + tldraw: 3.12.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) transitivePeerDependencies: - '@types/react' - '@types/react-dom' - '@tldraw/tlschema@3.12.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tldraw/tlschema@3.12.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@tldraw/state': 3.12.1 - '@tldraw/store': 3.12.1(react@19.2.4) + '@tldraw/store': 3.12.1(react@19.2.5) '@tldraw/utils': 3.12.1 '@tldraw/validate': 3.12.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) '@tldraw/utils@3.12.1': dependencies: @@ -8494,7 +7041,7 @@ snapshots: '@types/better-sqlite3@7.6.13': dependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 '@types/chai@5.2.3': dependencies: @@ -8536,9 +7083,9 @@ snapshots: '@types/node-cron@3.0.11': {} - '@types/node@25.5.0': + '@types/node@25.6.0': dependencies: - undici-types: 7.18.2 + undici-types: 7.19.2 '@types/nprogress@0.2.3': {} @@ -8574,38 +7121,38 @@ snapshots: '@types/web-push@3.6.4': dependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 '@use-gesture/core@10.3.1': {} - '@use-gesture/react@10.3.1(react@19.2.4)': + '@use-gesture/react@10.3.1(react@19.2.5)': dependencies: '@use-gesture/core': 10.3.1 - react: 19.2.4 + react: 19.2.5 - '@vitest/browser-playwright@4.1.2(playwright@1.58.2)(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': + '@vitest/browser-playwright@4.1.4(playwright@1.59.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)': dependencies: - '@vitest/browser': 4.1.2(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/mocker': 4.1.2(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) - playwright: 1.58.2 + '@vitest/browser': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4) + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + playwright: 1.59.1 tinyrainbow: 3.1.0 - vitest: 4.1.2(@types/node@25.5.0)(@vitest/browser-playwright@4.1.2)(@vitest/ui@4.1.2)(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/ui@4.1.4)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.2(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': + '@vitest/browser@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.2(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/utils': 4.1.2 + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 4.1.4 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.2(@types/node@25.5.0)(@vitest/browser-playwright@4.1.2)(@vitest/ui@4.1.2)(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/ui@4.1.4)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) ws: 8.20.0 transitivePeerDependencies: - bufferutil @@ -8613,61 +7160,61 @@ snapshots: - utf-8-validate - vite - '@vitest/expect@4.1.2': + '@vitest/expect@4.1.4': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.2(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.2 + '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/pretty-format@4.1.2': + '@vitest/pretty-format@4.1.4': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.2': + '@vitest/runner@4.1.4': dependencies: - '@vitest/utils': 4.1.2 + '@vitest/utils': 4.1.4 pathe: 2.0.3 - '@vitest/snapshot@4.1.2': + '@vitest/snapshot@4.1.4': dependencies: - '@vitest/pretty-format': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/pretty-format': 4.1.4 + '@vitest/utils': 4.1.4 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.2': {} + '@vitest/spy@4.1.4': {} - '@vitest/ui@4.1.2(vitest@4.1.2)': + '@vitest/ui@4.1.4(vitest@4.1.4)': dependencies: - '@vitest/utils': 4.1.2 + '@vitest/utils': 4.1.4 fflate: 0.8.2 flatted: 3.4.2 pathe: 2.0.3 sirv: 3.0.2 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vitest: 4.1.2(@types/node@25.5.0)(@vitest/browser-playwright@4.1.2)(@vitest/ui@4.1.2)(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/ui@4.1.4)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/utils@4.1.2': + '@vitest/utils@4.1.4': dependencies: - '@vitest/pretty-format': 4.1.2 + '@vitest/pretty-format': 4.1.4 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 '@xobotyi/scrollbar-width@1.9.5': {} - '@zumer/snapdom@2.7.0': {} + '@zumer/snapdom@2.9.0': {} accepts@1.3.8: dependencies: @@ -8743,13 +7290,13 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.11: {} + baseline-browser-mapping@2.10.20: {} basic-auth@2.0.1: dependencies: safe-buffer: 5.1.2 - better-sqlite3@12.8.0: + better-sqlite3@12.9.0: dependencies: bindings: 1.5.0 prebuild-install: 7.1.3 @@ -8760,7 +7307,7 @@ snapshots: bl@4.1.0: dependencies: - buffer: 5.6.0 + buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 @@ -8791,13 +7338,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.1: + browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.11 - caniuse-lite: 1.0.30001781 - electron-to-chromium: 1.5.328 - node-releases: 2.0.36 - update-browserslist-db: 1.2.3(browserslist@4.28.1) + baseline-browser-mapping: 2.10.20 + caniuse-lite: 1.0.30001790 + electron-to-chromium: 1.5.343 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) buffer-equal-constant-time@1.0.1: {} @@ -8812,7 +7359,12 @@ snapshots: buffer@5.6.0: dependencies: base64-js: 1.5.1 - ieee754: 1.1.13 + ieee754: 1.2.1 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 bytes@3.1.2: {} @@ -8837,7 +7389,7 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - caniuse-lite@1.0.30001781: {} + caniuse-lite@1.0.30001790: {} chai@6.2.2: {} @@ -8888,7 +7440,7 @@ snapshots: transitivePeerDependencies: - supports-color - compressorjs@1.2.1: + compressorjs@1.3.0: dependencies: blueimp-canvas-to-blob: 3.29.0 is-blob: 2.1.0 @@ -8922,7 +7474,7 @@ snapshots: '@epic-web/invariant': 1.0.0 cross-spawn: 7.0.6 - cross-fetch@4.0.0: + cross-fetch@4.1.0: dependencies: node-fetch: 2.7.0 transitivePeerDependencies: @@ -8997,8 +7549,6 @@ snapshots: dependencies: ms: 2.1.3 - decimal.js@10.6.0: {} - decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -9027,7 +7577,7 @@ snapshots: discontinuous-range@1.0.0: {} - dotenv@17.3.1: {} + dotenv@17.4.2: {} dunder-proto@1.0.1: dependencies: @@ -9043,7 +7593,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.328: {} + electron-to-chromium@1.5.343: {} emoji-regex@8.0.0: {} @@ -9100,6 +7650,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.4 '@esbuild/win32-x64': 0.27.4 + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -9182,15 +7761,15 @@ snapshots: fast-shallow-equal@1.0.0: {} - fast-xml-builder@1.1.4: + fast-xml-builder@1.1.5: dependencies: - path-expression-matcher: 1.2.0 + path-expression-matcher: 1.5.0 fast-xml-parser@5.5.8: dependencies: - fast-xml-builder: 1.1.4 - path-expression-matcher: 1.2.0 - strnum: 2.2.2 + fast-xml-builder: 1.1.5 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 fastest-stable-stringify@2.0.2: {} @@ -9358,9 +7937,9 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 - i18next-http-backend@3.0.2: + i18next-http-backend@3.0.5: dependencies: - cross-fetch: 4.0.0 + cross-fetch: 4.1.0 transitivePeerDependencies: - encoding @@ -9372,11 +7951,11 @@ snapshots: picomatch: 4.0.4 yargs: 17.7.2 - i18next@25.10.10(typescript@5.9.3): + i18next@25.10.10(typescript@6.0.3): dependencies: '@babel/runtime': 7.29.2 optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 iconv-lite@0.4.24: dependencies: @@ -9392,6 +7971,8 @@ snapshots: ieee754@1.1.13: {} + ieee754@1.2.1: {} + inherits@2.0.4: {} ini@1.3.8: {} @@ -9402,13 +7983,6 @@ snapshots: internmap@1.0.1: {} - intl-messageformat@10.7.18: - dependencies: - '@formatjs/ecma402-abstract': 2.3.6 - '@formatjs/fast-memoize': 2.2.7 - '@formatjs/icu-messageformat-parser': 2.11.4 - tslib: 2.8.1 - ipaddr.js@1.9.1: {} is-arguments@1.2.0: @@ -9455,7 +8029,7 @@ snapshots: isarray@1.0.0: {} - isbot@5.1.36: {} + isbot@5.1.38: {} isexe@2.0.0: {} @@ -9474,8 +8048,6 @@ snapshots: jsesc@3.0.2: {} - jsesc@3.1.0: {} - json5@2.2.3: {} jsoncrush@1.1.8: {} @@ -9501,7 +8073,7 @@ snapshots: kleur@4.1.5: {} - knip@6.1.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + knip@6.4.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): dependencies: '@nodelib/fs.walk': 1.2.8 fast-glob: 3.3.3 @@ -9509,8 +8081,8 @@ snapshots: get-tsconfig: 4.13.7 jiti: 2.6.1 minimist: 1.2.8 - oxc-parser: 0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) - oxc-resolver: 11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + oxc-parser: 0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) picocolors: 1.1.1 picomatch: 4.0.4 smol-toml: 1.6.1 @@ -9522,7 +8094,7 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - kysely@0.28.14: {} + kysely@0.28.16: {} ley@0.8.1: dependencies: @@ -9592,21 +8164,21 @@ snapshots: lodash.uniq@4.5.0: {} - lodash@4.17.23: {} + lodash@4.18.1: {} loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 - lru-cache@11.2.7: {} + lru-cache@11.3.5: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 - lucide-react@1.7.0(react@19.2.4): + lucide-react@1.8.0(react@19.2.5): dependencies: - react: 19.2.4 + react: 19.2.5 lz-string@1.5.0: {} @@ -9623,9 +8195,9 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 - markdown-to-jsx@9.7.13(react@19.2.4): + markdown-to-jsx@9.7.15(react@19.2.5): optionalDependencies: - react: 19.2.4 + react: 19.2.5 math-intrinsics@1.1.0: {} @@ -9686,22 +8258,22 @@ snapshots: ms@2.1.3: {} - nano-css@5.6.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + nano-css@5.6.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@jridgewell/sourcemap-codec': 1.5.5 css-tree: 1.1.3 csstype: 3.2.3 fastest-stable-stringify: 2.0.2 inline-style-prefixer: 7.0.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) rtl-css-js: 1.16.1 stacktrace-js: 2.0.2 stylis: 4.3.6 nanoid@3.3.11: {} - nanoid@5.1.7: {} + nanoid@5.1.9: {} napi-build-utils@2.0.0: {} @@ -9730,7 +8302,7 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-releases@2.0.36: {} + node-releases@2.0.38: {} nprogress@0.2.0: {} @@ -9764,7 +8336,7 @@ snapshots: orderedmap@2.1.1: {} - oxc-parser@0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + oxc-parser@0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): dependencies: '@oxc-project/types': 0.121.0 optionalDependencies: @@ -9784,7 +8356,7 @@ snapshots: '@oxc-parser/binding-linux-x64-gnu': 0.121.0 '@oxc-parser/binding-linux-x64-musl': 0.121.0 '@oxc-parser/binding-openharmony-arm64': 0.121.0 - '@oxc-parser/binding-wasm32-wasi': 0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@oxc-parser/binding-wasm32-wasi': 0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) '@oxc-parser/binding-win32-arm64-msvc': 0.121.0 '@oxc-parser/binding-win32-ia32-msvc': 0.121.0 '@oxc-parser/binding-win32-x64-msvc': 0.121.0 @@ -9792,7 +8364,7 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - oxc-resolver@11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): optionalDependencies: '@oxc-resolver/binding-android-arm-eabi': 11.19.1 '@oxc-resolver/binding-android-arm64': 11.19.1 @@ -9810,7 +8382,7 @@ snapshots: '@oxc-resolver/binding-linux-x64-gnu': 11.19.1 '@oxc-resolver/binding-linux-x64-musl': 11.19.1 '@oxc-resolver/binding-openharmony-arm64': 11.19.1 - '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1 '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 @@ -9826,13 +8398,13 @@ snapshots: parseurl@1.3.3: {} - partysocket@1.1.16(react@19.2.4): + partysocket@1.1.16(react@19.2.5): dependencies: event-target-polyfill: 0.0.4 optionalDependencies: - react: 19.2.4 + react: 19.2.5 - path-expression-matcher@1.2.0: {} + path-expression-matcher@1.5.0: {} path-key@3.1.1: {} @@ -9854,11 +8426,11 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 - playwright-core@1.58.2: {} + playwright-core@1.59.1: {} - playwright@1.58.2: + playwright@1.59.1: dependencies: - playwright-core: 1.58.2 + playwright-core: 1.59.1 optionalDependencies: fsevents: 2.3.2 @@ -9866,7 +8438,7 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss@8.5.8: + postcss@8.5.10: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -9887,7 +8459,7 @@ snapshots: tar-fs: 2.1.4 tunnel-agent: 0.6.0 - prettier@3.8.1: {} + prettier@3.8.3: {} prop-types@15.8.1: dependencies: @@ -10047,88 +8619,32 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-aria-components@1.16.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-aria-components@1.17.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - '@internationalized/date': 3.12.0 - '@internationalized/string': 3.2.7 - '@react-aria/autocomplete': 3.0.0-rc.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/collections': 3.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/dnd': 3.11.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/live-announcer': 3.4.4 - '@react-aria/overlays': 3.31.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/ssr': 3.9.10(react@19.2.4) - '@react-aria/textfield': 3.18.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/toolbar': 3.0.0-beta.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/virtualizer': 4.1.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/autocomplete': 3.0.0-beta.4(react@19.2.4) - '@react-stately/layout': 4.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-stately/selection': 3.20.9(react@19.2.4) - '@react-stately/table': 3.15.4(react@19.2.4) - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-stately/virtualizer': 4.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/form': 3.7.18(react@19.2.4) - '@react-types/grid': 3.3.8(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - '@react-types/table': 3.13.6(react@19.2.4) - '@swc/helpers': 0.5.20 + '@internationalized/date': 3.12.1 + '@react-types/shared': 3.34.0(react@19.2.5) + '@swc/helpers': 0.5.21 client-only: 0.0.1 - react: 19.2.4 - react-aria: 3.47.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react-dom: 19.2.4(react@19.2.4) - react-stately: 3.45.0(react@19.2.4) - use-sync-external-store: 1.6.0(react@19.2.4) + react: 19.2.5 + react-aria: 3.48.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react-dom: 19.2.5(react@19.2.5) + react-stately: 3.46.0(react@19.2.5) - react-aria@3.47.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-aria@3.48.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - '@internationalized/string': 3.2.7 - '@react-aria/breadcrumbs': 3.5.32(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/button': 3.14.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/calendar': 3.9.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/checkbox': 3.16.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/color': 3.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/combobox': 3.15.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/datepicker': 3.16.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/dialog': 3.5.34(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/disclosure': 3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/dnd': 3.11.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/gridlist': 3.14.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/i18n': 3.12.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/label': 3.7.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/landmark': 3.0.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/link': 3.8.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/listbox': 3.15.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/menu': 3.21.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/meter': 3.4.30(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/numberfield': 3.12.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/overlays': 3.31.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/progress': 3.4.30(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/radio': 3.12.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/searchfield': 3.8.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/select': 3.17.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/selection': 3.27.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/separator': 3.4.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/slider': 3.8.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/ssr': 3.9.10(react@19.2.4) - '@react-aria/switch': 3.7.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/table': 3.17.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/tabs': 3.11.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/tag': 3.8.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/textfield': 3.18.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/toast': 3.0.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/tooltip': 3.9.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/tree': 3.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/visually-hidden': 3.8.31(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@internationalized/date': 3.12.1 + '@internationalized/number': 3.6.6 + '@internationalized/string': 3.2.8 + '@react-types/shared': 3.34.0(react@19.2.5) + '@swc/helpers': 0.5.21 + aria-hidden: 1.2.6 + clsx: 2.1.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-stately: 3.46.0(react@19.2.5) + use-sync-external-store: 1.6.0(react@19.2.5) - react-charts@3.0.0-beta.57(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-charts@3.0.0-beta.57(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@babel/runtime': 7.29.2 '@types/d3-array': 3.2.2 @@ -10143,116 +8659,96 @@ snapshots: d3-shape: 2.1.0 d3-time: 2.1.1 d3-time-format: 4.1.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) ts-toolbelt: 9.6.0 - react-dom@19.2.4(react@19.2.4): + react-dom@19.2.5(react@19.2.5): dependencies: - react: 19.2.4 + react: 19.2.5 scheduler: 0.27.0 - react-error-boundary@6.1.1(react@19.2.4): + react-error-boundary@6.1.1(react@19.2.5): dependencies: - react: 19.2.4 + react: 19.2.5 - react-flip-toolkit@7.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-flip-toolkit@7.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: flip-toolkit: 7.2.4 prop-types: 15.8.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) - react-i18next@16.6.6(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3): + react-i18next@16.6.6(i18next@25.10.10(typescript@6.0.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3): dependencies: '@babel/runtime': 7.29.2 html-parse-stringify: 3.0.1 - i18next: 25.10.10(typescript@5.9.3) - react: 19.2.4 - use-sync-external-store: 1.6.0(react@19.2.4) + i18next: 25.10.10(typescript@6.0.3) + react: 19.2.5 + use-sync-external-store: 1.6.0(react@19.2.5) optionalDependencies: - react-dom: 19.2.4(react@19.2.4) - typescript: 5.9.3 + react-dom: 19.2.5(react@19.2.5) + typescript: 6.0.3 react-is@16.13.1: {} react-refresh@0.14.2: {} - react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.5): dependencies: - react: 19.2.4 - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.5 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5) tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 - react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.4): + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.5): dependencies: - react: 19.2.4 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4) - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.5 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.5) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4) - use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4) + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.5) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 - react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: cookie: 1.1.1 - react: 19.2.4 + react: 19.2.5 set-cookie-parser: 2.7.2 optionalDependencies: - react-dom: 19.2.4(react@19.2.4) + react-dom: 19.2.5(react@19.2.5) - react-stately@3.45.0(react@19.2.4): + react-stately@3.46.0(react@19.2.5): dependencies: - '@react-stately/calendar': 3.9.3(react@19.2.4) - '@react-stately/checkbox': 3.7.5(react@19.2.4) - '@react-stately/collections': 3.12.10(react@19.2.4) - '@react-stately/color': 3.9.5(react@19.2.4) - '@react-stately/combobox': 3.13.0(react@19.2.4) - '@react-stately/data': 3.15.2(react@19.2.4) - '@react-stately/datepicker': 3.16.1(react@19.2.4) - '@react-stately/disclosure': 3.0.11(react@19.2.4) - '@react-stately/dnd': 3.7.4(react@19.2.4) - '@react-stately/form': 3.2.4(react@19.2.4) - '@react-stately/list': 3.13.4(react@19.2.4) - '@react-stately/menu': 3.9.11(react@19.2.4) - '@react-stately/numberfield': 3.11.0(react@19.2.4) - '@react-stately/overlays': 3.6.23(react@19.2.4) - '@react-stately/radio': 3.11.5(react@19.2.4) - '@react-stately/searchfield': 3.5.19(react@19.2.4) - '@react-stately/select': 3.9.2(react@19.2.4) - '@react-stately/selection': 3.20.9(react@19.2.4) - '@react-stately/slider': 3.7.5(react@19.2.4) - '@react-stately/table': 3.15.4(react@19.2.4) - '@react-stately/tabs': 3.8.9(react@19.2.4) - '@react-stately/toast': 3.1.3(react@19.2.4) - '@react-stately/toggle': 3.9.5(react@19.2.4) - '@react-stately/tooltip': 3.5.11(react@19.2.4) - '@react-stately/tree': 3.9.6(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) - react: 19.2.4 + '@internationalized/date': 3.12.1 + '@internationalized/number': 3.6.6 + '@internationalized/string': 3.2.8 + '@react-types/shared': 3.34.0(react@19.2.5) + '@swc/helpers': 0.5.21 + react: 19.2.5 + use-sync-external-store: 1.6.0(react@19.2.5) - react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4): + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.5): dependencies: get-nonce: 1.0.1 - react: 19.2.4 + react: 19.2.5 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 - react-universal-interface@0.6.2(react@19.2.4)(tslib@2.8.1): + react-universal-interface@0.6.2(react@19.2.5)(tslib@2.8.1): dependencies: - react: 19.2.4 + react: 19.2.5 tslib: 2.8.1 - react-use-draggable-scroll@0.4.7(react@19.2.4): + react-use-draggable-scroll@0.4.7(react@19.2.5): dependencies: - react: 19.2.4 + react: 19.2.5 - react-use@17.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-use@17.6.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@types/js-cookie': 2.2.7 '@xobotyi/scrollbar-width': 1.9.5 @@ -10260,10 +8756,10 @@ snapshots: fast-deep-equal: 3.1.3 fast-shallow-equal: 1.0.0 js-cookie: 2.2.1 - nano-css: 5.6.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-universal-interface: 0.6.2(react@19.2.4)(tslib@2.8.1) + nano-css: 5.6.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-universal-interface: 0.6.2(react@19.2.5)(tslib@2.8.1) resize-observer-polyfill: 1.5.1 screenfull: 5.2.0 set-harmonic-interval: 1.0.1 @@ -10271,7 +8767,7 @@ snapshots: ts-easing: 0.2.0 tslib: 2.8.1 - react@19.2.4: {} + react@19.2.5: {} readable-stream@3.6.2: dependencies: @@ -10283,7 +8779,7 @@ snapshots: rematrix@0.2.2: {} - remeda@2.33.6: {} + remeda@2.33.7: {} remix-auth-oauth2@3.4.1(remix-auth@4.2.0): dependencies: @@ -10294,12 +8790,12 @@ snapshots: remix-auth@4.2.0: {} - remix-i18next@7.4.2(i18next@25.10.10(typescript@5.9.3))(react-i18next@16.6.6(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): + remix-i18next@7.4.2(i18next@25.10.10(typescript@6.0.3))(react-i18next@16.6.6(i18next@25.10.10(typescript@6.0.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3))(react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5): dependencies: - i18next: 25.10.10(typescript@5.9.3) - react: 19.2.4 - react-i18next: 16.6.6(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) - react-router: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + i18next: 25.10.10(typescript@6.0.3) + react: 19.2.5 + react-i18next: 16.6.6(i18next@25.10.10(typescript@6.0.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3) + react-router: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) require-directory@2.1.1: {} @@ -10311,56 +8807,56 @@ snapshots: reusify@1.1.0: {} - rolldown@1.0.0-rc.12: + rolldown@1.0.0-rc.15: dependencies: - '@oxc-project/types': 0.122.0 - '@rolldown/pluginutils': 1.0.0-rc.12 + '@oxc-project/types': 0.124.0 + '@rolldown/pluginutils': 1.0.0-rc.15 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.12 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 - '@rolldown/binding-darwin-x64': 1.0.0-rc.12 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + '@rolldown/binding-android-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-x64': 1.0.0-rc.15 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 - rollup@4.60.0: + rollup@4.60.2: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.0 - '@rollup/rollup-android-arm64': 4.60.0 - '@rollup/rollup-darwin-arm64': 4.60.0 - '@rollup/rollup-darwin-x64': 4.60.0 - '@rollup/rollup-freebsd-arm64': 4.60.0 - '@rollup/rollup-freebsd-x64': 4.60.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.0 - '@rollup/rollup-linux-arm-musleabihf': 4.60.0 - '@rollup/rollup-linux-arm64-gnu': 4.60.0 - '@rollup/rollup-linux-arm64-musl': 4.60.0 - '@rollup/rollup-linux-loong64-gnu': 4.60.0 - '@rollup/rollup-linux-loong64-musl': 4.60.0 - '@rollup/rollup-linux-ppc64-gnu': 4.60.0 - '@rollup/rollup-linux-ppc64-musl': 4.60.0 - '@rollup/rollup-linux-riscv64-gnu': 4.60.0 - '@rollup/rollup-linux-riscv64-musl': 4.60.0 - '@rollup/rollup-linux-s390x-gnu': 4.60.0 - '@rollup/rollup-linux-x64-gnu': 4.60.0 - '@rollup/rollup-linux-x64-musl': 4.60.0 - '@rollup/rollup-openbsd-x64': 4.60.0 - '@rollup/rollup-openharmony-arm64': 4.60.0 - '@rollup/rollup-win32-arm64-msvc': 4.60.0 - '@rollup/rollup-win32-ia32-msvc': 4.60.0 - '@rollup/rollup-win32-x64-gnu': 4.60.0 - '@rollup/rollup-win32-x64-msvc': 4.60.0 + '@rollup/rollup-android-arm-eabi': 4.60.2 + '@rollup/rollup-android-arm64': 4.60.2 + '@rollup/rollup-darwin-arm64': 4.60.2 + '@rollup/rollup-darwin-x64': 4.60.2 + '@rollup/rollup-freebsd-arm64': 4.60.2 + '@rollup/rollup-freebsd-x64': 4.60.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.2 + '@rollup/rollup-linux-arm-musleabihf': 4.60.2 + '@rollup/rollup-linux-arm64-gnu': 4.60.2 + '@rollup/rollup-linux-arm64-musl': 4.60.2 + '@rollup/rollup-linux-loong64-gnu': 4.60.2 + '@rollup/rollup-linux-loong64-musl': 4.60.2 + '@rollup/rollup-linux-ppc64-gnu': 4.60.2 + '@rollup/rollup-linux-ppc64-musl': 4.60.2 + '@rollup/rollup-linux-riscv64-gnu': 4.60.2 + '@rollup/rollup-linux-riscv64-musl': 4.60.2 + '@rollup/rollup-linux-s390x-gnu': 4.60.2 + '@rollup/rollup-linux-x64-gnu': 4.60.2 + '@rollup/rollup-linux-x64-musl': 4.60.2 + '@rollup/rollup-openbsd-x64': 4.60.2 + '@rollup/rollup-openharmony-arm64': 4.60.2 + '@rollup/rollup-win32-arm64-msvc': 4.60.2 + '@rollup/rollup-win32-ia32-msvc': 4.60.2 + '@rollup/rollup-win32-x64-gnu': 4.60.2 + '@rollup/rollup-win32-x64-msvc': 4.60.2 fsevents: 2.3.3 rope-sequence@1.3.4: {} @@ -10454,7 +8950,7 @@ snapshots: shebang-regex@3.0.0: {} - side-channel-list@1.0.0: + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 @@ -10478,7 +8974,7 @@ snapshots: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 - side-channel-list: 1.0.0 + side-channel-list: 1.0.1 side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 @@ -10498,7 +8994,7 @@ snapshots: mrmime: 2.0.1 totalist: 3.0.1 - slugify@1.6.8: {} + slugify@1.6.9: {} smol-toml@1.6.1: {} @@ -10543,7 +9039,7 @@ snapshots: statuses@2.0.2: {} - std-env@4.0.0: {} + std-env@4.1.0: {} stream-browserify@3.0.0: dependencies: @@ -10570,7 +9066,7 @@ snapshots: strip-json-comments@5.0.3: {} - strnum@2.2.2: {} + strnum@2.2.3: {} stylis@4.3.6: {} @@ -10578,11 +9074,11 @@ snapshots: dependencies: has-flag: 4.0.0 - swr@2.4.1(react@19.2.4): + swr@2.4.1(react@19.2.5): dependencies: dequal: 2.0.3 - react: 19.2.4 - use-sync-external-store: 1.6.0(react@19.2.4) + react: 19.2.5 + use-sync-external-store: 1.6.0(react@19.2.5) tar-fs@2.1.4: dependencies: @@ -10605,9 +9101,9 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.0.4: {} + tinyexec@1.1.1: {} - tinyglobby@0.2.15: + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 @@ -10618,31 +9114,31 @@ snapshots: dependencies: '@popperjs/core': 2.11.8 - tldraw@3.12.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + tldraw@3.12.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tiptap/core': 2.27.2(@tiptap/pm@2.27.2) '@tiptap/extension-code': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) '@tiptap/extension-highlight': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) '@tiptap/extension-link': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2) '@tiptap/pm': 2.27.2 - '@tiptap/react': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tiptap/react': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tiptap/starter-kit': 2.27.2 - '@tldraw/editor': 3.12.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tldraw/store': 3.12.1(react@19.2.4) + '@tldraw/editor': 3.12.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tldraw/store': 3.12.1(react@19.2.5) classnames: 2.5.1 hotkeys-js: 3.13.15 idb: 7.1.1 lodash.isequal: 4.5.0 lz-string: 1.5.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -10691,21 +9187,21 @@ snapshots: dependencies: ts-toolbelt: 9.6.0 - typescript@5.9.3: {} + typescript@6.0.3: {} uc.micro@2.1.0: {} unbash@2.2.0: {} - undici-types@7.18.2: {} + undici-types@7.19.2: {} universalify@2.0.1: {} unpipe@1.0.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.1): + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: - browserslist: 4.28.1 + browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1 @@ -10714,24 +9210,24 @@ snapshots: punycode: 1.3.2 querystring: 0.2.0 - use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.5): dependencies: - react: 19.2.4 + react: 19.2.5 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 - use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4): + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.5): dependencies: detect-node-es: 1.1.0 - react: 19.2.4 + react: 19.2.5 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 - use-sync-external-store@1.6.0(react@19.2.4): + use-sync-external-store@1.6.0(react@19.2.5): dependencies: - react: 19.2.4 + react: 19.2.5 util-deprecate@1.0.2: {} @@ -10747,19 +9243,19 @@ snapshots: uuid@8.0.0: {} - valibot@1.3.1(typescript@5.9.3): + valibot@1.3.1(typescript@6.0.3): optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 vary@1.1.2: {} - vite-node@3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3): + vite-node@3.2.4(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -10774,13 +9270,13 @@ snapshots: - tsx - yaml - vite-node@6.0.0(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3): + vite-node@6.0.0(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: cac: 7.0.0 es-module-lexer: 2.0.0 obug: 2.1.1 pathe: 2.0.3 - vite: 8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - '@vitejs/devtools' @@ -10795,77 +9291,77 @@ snapshots: - tsx - yaml - vite-plugin-babel@1.6.0(@babel/core@7.29.0)(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): + vite-plugin-babel@1.6.0(@babel/core@7.29.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@babel/core': 7.29.0 - vite: 8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) - vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3): + vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: - esbuild: 0.27.4 + esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.8 - rollup: 4.60.0 - tinyglobby: 0.2.15 + postcss: 8.5.10 + rollup: 4.60.2 + tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.32.0 tsx: 4.21.0 yaml: 2.8.3 - vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3): + vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.8 - rolldown: 1.0.0-rc.12 - tinyglobby: 0.2.15 + postcss: 8.5.10 + rolldown: 1.0.0-rc.15 + tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 25.5.0 - esbuild: 0.27.4 + '@types/node': 25.6.0 + esbuild: 0.27.7 fsevents: 2.3.3 jiti: 2.6.1 tsx: 4.21.0 yaml: 2.8.3 - vitest-browser-react@2.2.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.2): + vitest-browser-react@2.2.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(vitest@4.1.4): dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - vitest: 4.1.2(@types/node@25.5.0)(@vitest/browser-playwright@4.1.2)(@vitest/ui@4.1.2)(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + vitest: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/ui@4.1.4)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - vitest@4.1.2(@types/node@25.5.0)(@vitest/browser-playwright@4.1.2)(@vitest/ui@4.1.2)(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/ui@4.1.4)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.2 - '@vitest/runner': 4.1.2 - '@vitest/snapshot': 4.1.2 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/expect': 4.1.4 + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.4 + '@vitest/runner': 4.1.4 + '@vitest/snapshot': 4.1.4 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.4 - std-env: 4.0.0 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.5.0 - '@vitest/browser-playwright': 4.1.2(playwright@1.58.2)(vite@8.0.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/ui': 4.1.2(vitest@4.1.2) + '@types/node': 25.6.0 + '@vitest/browser-playwright': 4.1.4(playwright@1.59.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4) + '@vitest/ui': 4.1.4(vitest@4.1.4) transitivePeerDependencies: - msw diff --git a/public/static-assets/badges/bubblyb.avif b/public/static-assets/badges/bubblyb.avif new file mode 100644 index 000000000..ef18df3ad Binary files /dev/null and b/public/static-assets/badges/bubblyb.avif differ diff --git a/public/static-assets/badges/bubblyb.gif b/public/static-assets/badges/bubblyb.gif new file mode 100644 index 000000000..de9be0967 Binary files /dev/null and b/public/static-assets/badges/bubblyb.gif differ diff --git a/public/static-assets/badges/dreamteam.avif b/public/static-assets/badges/dreamteam.avif new file mode 100644 index 000000000..26d81af3c Binary files /dev/null and b/public/static-assets/badges/dreamteam.avif differ diff --git a/public/static-assets/badges/dreamteam.gif b/public/static-assets/badges/dreamteam.gif new file mode 100644 index 000000000..4eed107df Binary files /dev/null and b/public/static-assets/badges/dreamteam.gif differ diff --git a/public/static-assets/badges/oocee.avif b/public/static-assets/badges/oocee.avif new file mode 100644 index 000000000..2e1c5da9f Binary files /dev/null and b/public/static-assets/badges/oocee.avif differ diff --git a/public/static-assets/badges/oocee.gif b/public/static-assets/badges/oocee.gif new file mode 100644 index 000000000..7687b7d3c Binary files /dev/null and b/public/static-assets/badges/oocee.gif differ diff --git a/public/static-assets/badges/wwarning.avif b/public/static-assets/badges/wwarning.avif new file mode 100644 index 000000000..40e7e481d Binary files /dev/null and b/public/static-assets/badges/wwarning.avif differ diff --git a/public/static-assets/badges/wwarning.gif b/public/static-assets/badges/wwarning.gif new file mode 100644 index 000000000..de2035fe5 Binary files /dev/null and b/public/static-assets/badges/wwarning.gif differ diff --git a/screenshot-1.png b/screenshot-1.png deleted file mode 100644 index 4189a6309..000000000 Binary files a/screenshot-1.png and /dev/null differ diff --git a/screenshot-2.png b/screenshot-2.png deleted file mode 100644 index f7e50aaee..000000000 Binary files a/screenshot-2.png and /dev/null differ diff --git a/screenshot-3.png b/screenshot-3.png deleted file mode 100644 index fb72458da..000000000 Binary files a/screenshot-3.png and /dev/null differ diff --git a/scripts/add-leaderboard-teams-to-tournament.ts b/scripts/add-leaderboard-teams-to-tournament.ts index 36311e22b..1e6bb9830 100644 --- a/scripts/add-leaderboard-teams-to-tournament.ts +++ b/scripts/add-leaderboard-teams-to-tournament.ts @@ -1,7 +1,6 @@ import "dotenv/config"; import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepository.server"; import * as Seasons from "~/features/mmr/core/Seasons"; -import { joinTeam } from "~/features/tournament/queries/joinLeaveTeam.server"; import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server"; import { tournamentFromDB } from "~/features/tournament-bracket/core/Tournament.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; @@ -101,10 +100,6 @@ async function main() { const teamName = resolvedNames[i]; const owner = entry.members[0]; - const ownerInGameName = tournament.ctx.settings.requireInGameNames - ? await UserRepository.inGameNameByUserId(owner.id) - : null; - const tournamentTeam = await TournamentTeamRepository.create({ team: { name: teamName, @@ -113,19 +108,12 @@ async function main() { }, userId: owner.id, tournamentId, - ownerInGameName: ownerInGameName ?? null, }); for (const member of entry.members.slice(1)) { - const memberInGameName = tournament.ctx.settings.requireInGameNames - ? await UserRepository.inGameNameByUserId(member.id) - : null; - - joinTeam({ + await TournamentTeamRepository.join({ newTeamId: tournamentTeam.id, userId: member.id, - inGameName: memberInGameName ?? null, - tournamentId, }); } diff --git a/tsconfig.json b/tsconfig.json index 1310f6fdd..95da52d0c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,6 @@ "resolveJsonModule": true, "target": "ES2022", "strict": true, - "baseUrl": ".", "paths": { "~/*": ["./app/*"] },