diff --git a/assets/css/tailwind.css b/assets/css/tailwind.css index af5b092d..cc35f0bf 100644 --- a/assets/css/tailwind.css +++ b/assets/css/tailwind.css @@ -351,6 +351,37 @@ mark { transparent 4px ); } +/* Scan sweep across the top of the queue / match-confirm panels. The band is a + repeating gradient whose period is one track width, painted on a double-width + element, so shifting exactly one period (-50%) loops with no seam: the next + band enters the left edge as the previous one leaves the right. Colour is + currentColor, so callers pick it with text-*. */ +@keyframes tac-scan-sweep { + from { + transform: translateX(-50%); + } + to { + transform: translateX(0); + } +} +.tac-scan-sweep { + width: 200%; + background-image: repeating-linear-gradient( + 90deg, + transparent 0, + transparent 12.5%, + currentColor 25%, + transparent 37.5%, + transparent 50% + ); + animation: tac-scan-sweep 2s linear infinite; +} +@media (prefers-reduced-motion: reduce) { + .tac-scan-sweep { + animation: none; + } +} + /* One-shot pulse for clip share buttons — toast is far from the click target, so the button itself flashes amber + scales briefly. Used by MatchHighlightsReel queue/featured share, HighlightCard, and diff --git a/components/MatchOptions.vue b/components/MatchOptions.vue index 2333bd92..b4841f5d 100644 --- a/components/MatchOptions.vue +++ b/components/MatchOptions.vue @@ -807,6 +807,37 @@ import SettingHeader from "~/components/match/SettingHeader.vue"; + + + {{ + $t("match.options.advanced.veto_pick_timeout.label") + }} + + + + + + + + + + + {{ $t("match.options.advanced.veto_pick_timeout.range") }} + + + + + {{ diff --git a/components/draft-games/DraftClock.vue b/components/draft-games/DraftClock.vue index 1bb168f6..b1468cef 100644 --- a/components/draft-games/DraftClock.vue +++ b/components/draft-games/DraftClock.vue @@ -7,11 +7,13 @@ const props = withDefaults( total?: number; accent?: string; pulse?: boolean; + compact?: boolean; }>(), { total: 30, accent: "var(--tac-amber)", pulse: false, + compact: false, }, ); @@ -67,7 +69,11 @@ const urgent = computed(() => remaining.value <= 6 && remaining.value > 0); :class="{ pulse: pulse }" :style="{ '--accent': accent }" > - + remaining.value <= 6 && remaining.value > 0);
SEC diff --git a/components/match/MatchMapVeto.vue b/components/match/MatchMapVeto.vue index dfebd416..6fb80700 100644 --- a/components/match/MatchMapVeto.vue +++ b/components/match/MatchMapVeto.vue @@ -7,6 +7,7 @@ import MapDisplay from "~/components/MapDisplay.vue"; import MapSelector from "~/components/match/MapSelector.vue"; import { Separator } from "~/components/ui/separator"; import MatchPicksDisplay from "~/components/match/MatchPicksDisplay.vue"; +import DraftClock from "~/components/draft-games/DraftClock.vue"; @@ -320,6 +346,7 @@ export default { .string() .default(e_player_roles_enum.user), max_acceptable_latency: z.number().default(100), + veto_pick_timeout: z.number().min(0).max(600).default(60), }), }), ), @@ -331,7 +358,18 @@ export default { immediate: true, handler(newVal: Array<{ name: string; value: string | null }>) { for (const setting of newVal) { - if ( + if (setting.name === "public.veto_pick_timeout") { + // Kept out of the branch below: 0 is a meaningful value here (it + // disables the veto timer) and `|| 100` would swallow it. + const vetoPickTimeout = Number(setting.value); + + (this.form.setFieldValue as any)( + setting.name, + Number.isFinite(vetoPickTimeout) && vetoPickTimeout >= 0 + ? vetoPickTimeout + : 60, + ); + } else if ( setting.name === "public.max_acceptable_latency" || setting.name === "auto_cancel_duration" || setting.name === "live_match_timeout" @@ -503,6 +541,12 @@ export default { name: "live_match_timeout", value: String((this.form.values as any).live_match_timeout), }, + { + name: "public.veto_pick_timeout", + value: String( + (this.form.values as any).public.veto_pick_timeout, + ), + }, ], on_conflict: { constraint: settings_constraint.settings_pkey, diff --git a/stores/DraftGamesStore.ts b/stores/DraftGamesStore.ts index 0fe3f37e..faeb6cb2 100644 --- a/stores/DraftGamesStore.ts +++ b/stores/DraftGamesStore.ts @@ -275,6 +275,7 @@ export const useDraftGamesStore = defineStore("draft-games", () => { map_veto_type: true, map_veto_picking_lineup_id: true, region_veto_picking_lineup_id: true, + veto_pick_expires_at: true, region_veto_picks: { type: true, region: true, @@ -289,6 +290,7 @@ export const useDraftGamesStore = defineStore("draft-games", () => { regions: true, coaches: true, check_in_setting: true, + veto_pick_timeout: true, type: true, map_pool: { id: true, diff --git a/tailwind.config.js b/tailwind.config.js index f3420e53..7832c39e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -139,10 +139,6 @@ module.exports = { from: { strokeDashoffset: "0" }, to: { strokeDashoffset: "calc(var(--flow-length) * -1px)" }, }, - "loading-bar": { - "0%": { transform: "translateX(-100%)" }, - "100%": { transform: "translateX(100%)" }, - }, "fade-in": { from: { opacity: "0", transform: "translateY(5px)" }, to: { opacity: "1", transform: "translateY(0)" }, @@ -174,7 +170,6 @@ module.exports = { "caret-blink": "caret-blink 1.25s ease-out infinite", "spin-smooth": "spin-smooth 1s cubic-bezier(0.4, 0, 0.2, 1) infinite", "topo-flow": "topo-flow linear infinite", - "loading-bar": "loading-bar 2s infinite", "fade-in": "fade-in 0.3s ease-out", "soft-pulse": "soft-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite", "ping-slow": "ping-slow 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite", diff --git a/utilities/match-options-validator.ts b/utilities/match-options-validator.ts index 0d3c5a2e..c7889a78 100644 --- a/utilities/match-options-validator.ts +++ b/utilities/match-options-validator.ts @@ -16,6 +16,12 @@ export default function matchOptionsValidator( settings.find((setting) => setting.name === "public.default_models") ?.value === "true"; + const defaultVetoPickTimeout = Number.parseInt( + settings.find((setting) => setting.name === "public.veto_pick_timeout") + ?.value ?? "", + 10, + ); + return z.object({ mr: z.string().default("12"), map_veto: z.boolean().default(true), @@ -24,6 +30,15 @@ export default function matchOptionsValidator( lan: z.boolean().default(false), coaches: z.boolean().default(false), tv_delay: z.number().min(0).max(120).default(115), + veto_pick_timeout: z + .number() + .min(0) + .max(600) + .default( + Number.isNaN(defaultVetoPickTimeout) || defaultVetoPickTimeout < 0 + ? 60 + : defaultVetoPickTimeout, + ), round_restart_delay: z.number().min(0).max(60).nullable().optional(), halftime_pausematch: z.boolean().default(false), knife_round: z.boolean().default(true), diff --git a/utilities/setupOptions.ts b/utilities/setupOptions.ts index 0f3f004e..4e883995 100644 --- a/utilities/setupOptions.ts +++ b/utilities/setupOptions.ts @@ -36,6 +36,7 @@ export const setupOptions = ( map_pool_id: options.map_pool.id, regions: selectedRegions, tv_delay: options.tv_delay, + veto_pick_timeout: options.veto_pick_timeout ?? 60, round_restart_delay: options.round_restart_delay ?? null, halftime_pausematch: options.halftime_pausematch ?? false, check_in_setting: options.check_in_setting, @@ -71,6 +72,7 @@ export function setupOptionsVariables( match_mode: string; map_pool_id?: string; tv_delay: number; + veto_pick_timeout: number; round_restart_delay?: number | null; halftime_pausematch?: boolean; map_pool?: { @@ -156,6 +158,13 @@ export function setupOptionsVariables( throw new Error("tv_delay is required"); } + if ( + values.veto_pick_timeout === undefined || + values.veto_pick_timeout === null + ) { + throw new Error("veto_pick_timeout is required"); + } + if ( values.check_in_setting === undefined || values.check_in_setting === null @@ -184,6 +193,7 @@ export function setupOptionsVariables( ready_setting: values.ready_setting, tech_timeout_setting: values.tech_timeout_setting, tv_delay: values.tv_delay, + veto_pick_timeout: values.veto_pick_timeout, round_restart_delay: values.round_restart_delay ?? null, halftime_pausematch: values.halftime_pausematch ?? false, ...(useAuthStore().isRoleAbove(e_player_roles_enum.tournament_organizer) @@ -238,6 +248,7 @@ export function setupOptionsSetMutation(hasMapPoolId: boolean = true) { timeout_setting: $("timeout_setting", "e_timeout_settings_enum!"), tech_timeout_setting: $("tech_timeout_setting", "e_timeout_settings_enum!"), tv_delay: $("tv_delay", "Int!"), + veto_pick_timeout: $("veto_pick_timeout", "Int!"), round_restart_delay: $("round_restart_delay", "Int"), halftime_pausematch: $("halftime_pausematch", "Boolean!"), ...(useAuthStore().isRoleAbove(e_player_roles_enum.tournament_organizer)