diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index e81e94e..f8ac898 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -214,9 +214,21 @@ jobs: run: pnpm exec playwright install chromium webkit timeout-minutes: 5 + # The offline e2e plays a real local vs_ai game, so it needs the per-variant dawgs; fetch the + # release the same way the Go jobs do and point the mock preview's copy step (scripts/ + # e2e-dict.mjs, via E2E_DICT_DIR below) at it. + - name: Fetch dictionary DAWGs + run: | + mkdir -p "${GITHUB_WORKSPACE}/dawg" + curl -fsSL -o /tmp/dawg.tar.gz "https://gitea.iliadenisov.ru/developer/scrabble-dictionary/releases/download/${DICT_VERSION}/scrabble-dawg-${DICT_VERSION}.tar.gz" + tar xzf /tmp/dawg.tar.gz -C "${GITHUB_WORKSPACE}/dawg" + ls -la "${GITHUB_WORKSPACE}/dawg" + - name: E2E smoke (mock) run: pnpm run test:e2e timeout-minutes: 5 + env: + E2E_DICT_DIR: ${{ github.workspace }}/dawg # conformance proves the client's local move preview (the ported dawg reader + # validator, ui/src/lib/dict) byte-for-byte against the authoritative Go engine: @@ -252,6 +264,7 @@ jobs: run: | go run ./backend/cmd/dictgen -dawg-dir "${GITHUB_WORKSPACE}/dawg" -out /tmp/dictgold go run ./backend/cmd/validategen -dawg-dir "${GITHUB_WORKSPACE}/dawg" -out /tmp/validgold + go run ./backend/cmd/movegen -dawg-dir "${GITHUB_WORKSPACE}/dawg" -out "${GITHUB_WORKSPACE}/movegold" - name: Set up Node uses: actions/setup-node@v4 @@ -271,6 +284,7 @@ jobs: DICT_DAWG_DIR: ${{ github.workspace }}/dawg DICT_GOLD_DIR: /tmp/dictgold DICT_VALID_DIR: /tmp/validgold + DICT_MOVEGEN_DIR: ${{ github.workspace }}/movegold run: pnpm exec vitest run src/lib/dict/ # gate is the single branch-protection required check. It always runs and passes diff --git a/backend/cmd/movegen/main.go b/backend/cmd/movegen/main.go new file mode 100644 index 0000000..233e5cd --- /dev/null +++ b/backend/cmd/movegen/main.go @@ -0,0 +1,417 @@ +// Command movegen emits golden conformance fixtures for the client-side move +// generator port (ui/src/lib/dict). It is a dev tool, run by hand; its output is +// committed so the TypeScript parity tests run without a Go toolchain. +// +// For each small sample dictionary (English and Russian — the latter reaches +// alphabet index 32, exercising the 33-letter cross-set boundary) it writes: +// +// - sample_.dawg the serialized dictionary (the reader/cursor fixture) +// - sample_.words.json the stored words + their alphabet indexes +// - sample_.gen.json ranked move-generation results from the real solver, +// for a handful of positions, plus the ruleset the TS +// side rebuilds to score identically +// +// Positions are built with only the solver's public API: an empty board, and +// two-ply positions reached by applying the solver's own top move (so no internal +// encoding is needed). Regenerate with: +// +// go run ./backend/cmd/movegen -out ui/src/lib/dict/testdata +package main + +import ( + "bytes" + "encoding/json" + "flag" + "log" + "os" + "path/filepath" + "strings" + + "gitea.iliadenisov.ru/developer/scrabble-solver/board" + "gitea.iliadenisov.ru/developer/scrabble-solver/rack" + "gitea.iliadenisov.ru/developer/scrabble-solver/rules" + "gitea.iliadenisov.ru/developer/scrabble-solver/scrabble" + dawg "github.com/iliadenisov/dafsa" +) + +// sampleWordsEN is the English sample dictionary, in strictly increasing +// alphabet-index order (the builder requires it). Shared prefixes (car/care/cars), +// shared suffixes (cats/dogs), internal-final nodes (do, an) and a one-letter word. +var sampleWordsEN = []string{ + "a", "an", "and", "ant", + "car", "care", "cared", "cares", "cars", "cat", "cats", + "do", "doe", "does", "dog", "dogs", "done", "dot", +} + +// sampleWordsRU is the Russian sample dictionary, in strictly increasing index +// order. It deliberately includes words starting with я (index 32) so the ported +// cross-set handles alphabet indexes past JS's 31-bit shift boundary. +var sampleWordsRU = []string{"ад", "ар", "оса", "я", "яд", "яр"} + +// sampleFixture is the JSON committed with the .dawg so the TypeScript cursor test +// knows the exact word set (as alphabet indexes) to expect from enumeration. +type sampleFixture struct { + Alphabet string `json:"alphabet"` + NumAdded int `json:"numAdded"` + Words []string `json:"words"` + Indexes [][]int `json:"indexes"` +} + +// genFixture is the move-generation golden set for one sample dictionary. +type genFixture struct { + Ruleset genRuleset `json:"ruleset"` + Cases []genCase `json:"cases"` +} + +// genRuleset is the scoring data the TS side rebuilds so evaluate() matches the Go +// solver: letter values, premium multipliers per square, the centre, rack size and bonus. +type genRuleset struct { + Size int `json:"size"` + Cols int `json:"cols"` + Center int `json:"center"` + RackSize int `json:"rackSize"` + Bingo int `json:"bingo"` + Values []int `json:"values"` + LetterMult [][]int `json:"letterMult"` + WordMult [][]int `json:"wordMult"` +} + +// genTile is one placed tile (a board tile or a move placement). +type genTile struct { + Row int `json:"row"` + Col int `json:"col"` + Letter int `json:"letter"` + Blank bool `json:"blank"` +} + +// genRack is a rack as a multiset of letter indexes plus a blank count. +type genRack struct { + Letters []int `json:"letters"` + Blanks int `json:"blanks"` +} + +// genMove is one ranked generated play: its orientation, placed tiles and total score. +type genMove struct { + Dir int `json:"dir"` + Tiles []genTile `json:"tiles"` + Score int `json:"score"` +} + +// genCase is one generation position: the tiles already on the board (empty when +// none), the rack, the mode/rule and the ranked moves the solver returns. +type genCase struct { + Name string `json:"name"` + Placed []genTile `json:"placed"` + Rack genRack `json:"rack"` + Mode int `json:"mode"` + IgnoreCrossWords bool `json:"ignoreCrossWords"` + Moves []genMove `json:"moves"` +} + +func main() { + out := flag.String("out", "ui/src/lib/dict/testdata", "output directory for fixtures") + dawgDir := flag.String("dawg-dir", "", "when set, emit real-dictionary move-gen golden from the .dawg files in this dir (conformance mode) instead of the committed samples") + flag.Parse() + + if err := os.MkdirAll(*out, 0o755); err != nil { + log.Fatalf("movegen: mkdir %s: %v", *out, err) + } + + if *dawgDir != "" { + buildReal(*dawgDir, *out) + return + } + + emitRulesets() + + buildSample(*out, "en", rules.English(), sampleWordsEN, []genCase{ + emptyCase("empty-cared", englishRack("caredts", 0), scrabble.Both, false), + emptyCase("empty-dogs", englishRack("dogsent", 0), scrabble.Both, false), + emptyCase("empty-blank", englishRack("caret", 1), scrabble.Both, false), + emptyCase("empty-single-word", englishRack("caredts", 0), scrabble.Both, true), + }) + + buildSample(*out, "ru", rules.RussianScrabble(), sampleWordsRU, []genCase{ + emptyCase("empty-yad", russianRack("ядрасо", 0), scrabble.Both, false), + }) +} + +// buildSample writes the dawg, the word fixture and the generation golden set for +// one sample dictionary. Two-ply cases are appended: the solver's own top move from +// the first non-empty result is applied, then generation runs again on the new rack. +func buildSample(out, tag string, rs *rules.Ruleset, words []string, cases []genCase) { + idx := rs.Alphabet + b := dawg.New(idx) + indexes := make([][]int, 0, len(words)) + for _, w := range words { + if err := b.Add(w); err != nil { + log.Fatalf("movegen[%s]: add %q: %v", tag, w, err) + } + enc, err := idx.Encode(w) + if err != nil { + log.Fatalf("movegen[%s]: encode %q: %v", tag, w, err) + } + ints := make([]int, len(enc)) + for i, x := range enc { + ints[i] = int(x) + } + indexes = append(indexes, ints) + } + finder := b.Finish() + + writeJSON(filepath.Join(out, "sample_"+tag+".words.json"), sampleFixture{ + Alphabet: tag, NumAdded: finder.NumAdded(), Words: words, Indexes: indexes, + }) + dawgPath := filepath.Join(out, "sample_"+tag+".dawg") + if _, err := finder.Save(dawgPath); err != nil { + log.Fatalf("movegen[%s]: save %s: %v", tag, dawgPath, err) + } + + s := scrabble.NewSolver(rs, finder) + for i := range cases { + runCase(s, rs, &cases[i], nil) + } + // A two-ply position from the first standard case that produced a move. + if two := twoPly(s, rs, cases); two != nil { + cases = append(cases, *two) + } + + writeJSON(filepath.Join(out, "sample_"+tag+".gen.json"), genFixture{ + Ruleset: rulesetOf(rs), Cases: cases, + }) + log.Printf("movegen[%s]: %d words, %d cases", tag, finder.NumAdded(), len(cases)) +} + +// realVariant maps a shipped dictionary file to the ruleset that scores it and the racks +// the conformance positions use. smallRack/blankRack keep the first-move (empty board) +// lists bounded on a dense dictionary; fullRack drives a deep 7-tile mid-game position, +// kept small by the anchors around the already-placed word. +type realVariant struct { + file, variant, smallRack, blankRack, fullRack string + rs *rules.Ruleset +} + +// buildReal emits move-generation golden from the real shipped dictionaries in dawgDir — +// the full alphabets and deep graphs the tiny samples cannot reach — one +// .movegen.json per variant. Like the dictgen/validategen vectors it is +// regenerated in CI and never committed, so it pins no dictionary version into the repo. +func buildReal(dawgDir, out string) { + reals := []realVariant{ + {"en_sowpods", "scrabble_en", "aine", "ain", "aeinrst", rules.English()}, + {"ru_scrabble", "scrabble_ru", "аеин", "аен", "аеиноср", rules.RussianScrabble()}, + {"ru_erudit", "erudit_ru", "аеин", "аен", "аеиноср", rules.Erudit()}, + } + for _, v := range reals { + data, err := os.ReadFile(filepath.Join(dawgDir, v.file+".dawg")) + if err != nil { + log.Fatalf("movegen[%s]: read dawg: %v", v.variant, err) + } + finder, err := dawg.Read(bytes.NewReader(data), 0) + if err != nil { + log.Fatalf("movegen[%s]: parse dawg: %v", v.variant, err) + } + s := scrabble.NewSolver(v.rs, finder) + + cases := []genCase{ + emptyCase("first-move", encRack(v.rs, v.smallRack, 0), scrabble.Both, false), + emptyCase("first-move-blank", encRack(v.rs, v.blankRack, 1), scrabble.Both, false), + } + for i := range cases { + runCase(s, v.rs, &cases[i], nil) + } + // A deep 7-tile mid-game: place the top first move, then generate again. The + // anchors around the placed word bound the list while still exercising a full rack, + // deep left/right extension and wide cross-sets over the real graph. + full := encRack(v.rs, v.fullRack, 0) + b := board.New(v.rs.Rows, v.rs.Cols) + if m1 := s.GenerateMovesOpts(b, toRack(v.rs.Size(), full), scrabble.Both, scrabble.PlayOptions{}); len(m1) > 0 { + mid := genCase{Name: "mid-game", Rack: full, Mode: int(scrabble.Both)} + runCase(s, v.rs, &mid, tilesOf(m1[0].Tiles)) + cases = append(cases, mid) + } + + writeJSON(filepath.Join(out, v.variant+".movegen.json"), genFixture{Ruleset: rulesetOf(v.rs), Cases: cases}) + total := 0 + for _, c := range cases { + total += len(c.Moves) + } + _ = finder.Close() + log.Printf("movegen[%s]: %d cases, %d golden moves", v.variant, len(cases), total) + } +} + +// encRack encodes a rack given as the variant's letters (plus a blank count) into the +// index-based genRack the fixtures carry. +func encRack(rs *rules.Ruleset, letters string, blanks int) genRack { + enc, err := rs.Alphabet.Encode(letters) + if err != nil { + log.Fatalf("movegen: encode rack %q: %v", letters, err) + } + idx := make([]int, len(enc)) + for i, b := range enc { + idx[i] = int(b) + } + return genRack{Letters: idx, Blanks: blanks} +} + +// runCase fills a case's Moves by generating on a board holding the given placed +// tiles (nil = empty board). +func runCase(s *scrabble.Solver, rs *rules.Ruleset, c *genCase, placed []genTile) { + bd := board.New(rs.Rows, rs.Cols) + for _, t := range placed { + bd.Set(t.Row, t.Col, cellByte(t.Letter, t.Blank)) + } + c.Placed = placed + rk := toRack(rs.Size(), c.Rack) + moves := s.GenerateMovesOpts(bd, rk, scrabble.Mode(c.Mode), scrabble.PlayOptions{IgnoreCrossWords: c.IgnoreCrossWords}) + c.Moves = movesOf(moves) +} + +// twoPly reaches a mid-game position by applying the top move of the first standard +// case that generated one, then generates again with a fresh rack of the same tiles. +func twoPly(s *scrabble.Solver, rs *rules.Ruleset, cases []genCase) *genCase { + for _, c := range cases { + if c.IgnoreCrossWords || len(c.Moves) == 0 { + continue + } + placed := c.Moves[0].Tiles + next := genCase{Name: "two-ply", Rack: c.Rack, Mode: int(scrabble.Both)} + runCase(s, rs, &next, placed) + return &next + } + return nil +} + +// cellByte encodes a board cell the way internal/encoding.Cell does (bits 0-5 hold +// letter+1, bit 7 marks a blank). Duplicated here because that package is internal +// to the solver module and cannot be imported. +func cellByte(letter int, blank bool) byte { + v := byte(letter+1) & 0x3f + if blank { + v |= 0x80 + } + return v +} + +func toRack(size int, r genRack) rack.Rack { + rk := rack.New(size) + for _, l := range r.Letters { + rk.Add(byte(l)) + } + for i := 0; i < r.Blanks; i++ { + rk.AddBlank() + } + return rk +} + +func rulesetOf(rs *rules.Ruleset) genRuleset { + lm := make([][]int, rs.Rows) + wm := make([][]int, rs.Rows) + for r := 0; r < rs.Rows; r++ { + lm[r] = make([]int, rs.Cols) + wm[r] = make([]int, rs.Cols) + for c := 0; c < rs.Cols; c++ { + p := rs.Premium(r, c) + lm[r][c] = p.LetterMult() + wm[r][c] = p.WordMult() + } + } + return genRuleset{ + Size: rs.Size(), Cols: rs.Cols, Center: rs.Center, RackSize: rs.RackSize, + Bingo: rs.Bingo, Values: rs.Values, LetterMult: lm, WordMult: wm, + } +} + +func movesOf(ms []scrabble.Move) []genMove { + out := make([]genMove, len(ms)) + for i, m := range ms { + out[i] = genMove{Dir: int(m.Dir), Tiles: tilesOf(m.Tiles), Score: m.Score} + } + return out +} + +func tilesOf(ps []scrabble.Placement) []genTile { + out := make([]genTile, len(ps)) + for i, p := range ps { + out[i] = genTile{Row: p.Row, Col: p.Col, Letter: int(p.Letter), Blank: p.Blank} + } + return out +} + +// emptyCase builds an empty-board case (Moves filled later by runCase). +func emptyCase(name string, r genRack, mode scrabble.Mode, ignoreCross bool) genCase { + return genCase{Name: name, Rack: r, Mode: int(mode), IgnoreCrossWords: ignoreCross} +} + +// englishRack builds a rack from lowercase a-z letters (index = letter-'a'). +func englishRack(letters string, blanks int) genRack { + idx := make([]int, 0, len(letters)) + for _, ch := range letters { + idx = append(idx, int(ch-'a')) + } + return genRack{Letters: idx, Blanks: blanks} +} + +// russianRack builds a rack from the Russian sample letters used above. +func russianRack(letters string, blanks int) genRack { + m := map[rune]int{'а': 0, 'д': 4, 'о': 15, 'р': 17, 'с': 18, 'я': 32} + idx := make([]int, 0, len([]rune(letters))) + for _, ch := range letters { + i, ok := m[ch] + if !ok { + log.Fatalf("movegen: russianRack: no index for %q", string(ch)) + } + idx = append(idx, i) + } + return genRack{Letters: idx, Blanks: blanks} +} + +// emitRulesets writes the per-variant static ruleset data (tile values, bag counts, blanks, +// bingo, rack size) the offline engine mirrors in ui/src/lib/localgame/ruleset.ts, so a TS +// parity test can pin that hand-copied table to the Go rulesets (scrabble-solver/rules). +func emitRulesets() { + type rsFix struct { + Size int `json:"size"` + RackSize int `json:"rackSize"` + Bingo int `json:"bingo"` + Blanks int `json:"blanks"` + Values []int `json:"values"` + Counts []int `json:"counts"` + Letters []string `json:"letters"` + } + out := map[string]rsFix{} + for _, v := range []struct { + name string + rs *rules.Ruleset + }{ + {"scrabble_en", rules.English()}, + {"scrabble_ru", rules.RussianScrabble()}, + {"erudit_ru", rules.Erudit()}, + } { + letters := make([]string, v.rs.Size()) + for i := range letters { + ch, err := v.rs.Alphabet.Character(byte(i)) + if err != nil { + log.Fatalf("movegen: %s letter %d: %v", v.name, i, err) + } + letters[i] = strings.ToUpper(ch) + } + out[v.name] = rsFix{Size: v.rs.Size(), RackSize: v.rs.RackSize, Bingo: v.rs.Bingo, Blanks: v.rs.Blanks, Values: v.rs.Values, Counts: v.rs.Counts, Letters: letters} + } + dir := filepath.Join("ui", "src", "lib", "localgame", "testdata") + if err := os.MkdirAll(dir, 0o755); err != nil { + log.Fatalf("movegen: mkdir %s: %v", dir, err) + } + writeJSON(filepath.Join(dir, "rulesets.json"), out) + log.Printf("movegen: wrote %s (3 variants)", filepath.Join(dir, "rulesets.json")) +} + +func writeJSON(path string, v any) { + data, err := json.MarshalIndent(v, "", " ") + if err != nil { + log.Fatalf("movegen: marshal %s: %v", path, err) + } + if err := os.WriteFile(path, append(data, '\n'), 0o644); err != nil { + log.Fatalf("movegen: write %s: %v", path, err) + } +} diff --git a/backend/internal/engine/endfixture_test.go b/backend/internal/engine/endfixture_test.go new file mode 100644 index 0000000..f3490f4 --- /dev/null +++ b/backend/internal/engine/endfixture_test.go @@ -0,0 +1,123 @@ +package engine + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "gitea.iliadenisov.ru/developer/scrabble-solver/rules" +) + +// The offline engine (ui/src/lib/localgame) reproduces the end-of-game rack settlement and the +// winner rule so a local game finishes with the same scores as the server. These golden fixtures +// pin the ported pure functions (applyEndAdjustment / winner / rackValue) to the real Go engine. +// Being in-package, this emitter constructs Game values directly and drives the unexported +// end-game math on chosen positions. + +type endCaseIn struct { + Name string `json:"name"` + Variant string `json:"variant"` + Reason string `json:"reason"` + Hands [][]int `json:"hands"` + Scores []int `json:"scores"` + Resigned []bool `json:"resigned"` + ToMove int `json:"toMove"` +} + +type endCaseOut struct { + endCaseIn + ScoresAfter []int `json:"scoresAfter"` + Winner int `json:"winner"` +} + +func rulesetFor(variant string) *rules.Ruleset { + switch variant { + case "scrabble_ru": + return rules.RussianScrabble() + case "erudit_ru": + return rules.Erudit() + default: + return rules.English() + } +} + +func reasonFor(s string) EndReason { + switch s { + case "out_of_tiles": + return EndOutOfTiles + case "scoreless": + return EndScoreless + case "resign": + return EndResign + case "aborted": + return EndAborted + } + return EndNotOver +} + +func handsBytes(hands [][]int) [][]byte { + out := make([][]byte, len(hands)) + for i, h := range hands { + b := make([]byte, len(h)) + for j, x := range h { + b[j] = byte(x) + } + out[i] = b + } + return out +} + +// TestEmitEndgameFixtures regenerates ui/src/lib/localgame/testdata/endgame.json. Gated by +// EMIT_ENGINE_FIXTURES. Regenerate with: +// +// EMIT_ENGINE_FIXTURES=1 go test ./backend/internal/engine -run TestEmitEndgameFixtures +func TestEmitEndgameFixtures(t *testing.T) { + if os.Getenv("EMIT_ENGINE_FIXTURES") == "" { + t.Skip("set EMIT_ENGINE_FIXTURES=1 to regenerate ui/src/lib/localgame/testdata/endgame.json") + } + + cases := []endCaseIn{ + {"out-basic", "scrabble_en", "out_of_tiles", [][]int{{}, {0, 1, 2}}, []int{50, 40}, []bool{false, false}, 0}, + {"out-blank", "scrabble_en", "out_of_tiles", [][]int{{}, {255, 0}}, []int{30, 30}, []bool{false, false}, 0}, + {"out-erudit-yo", "erudit_ru", "out_of_tiles", [][]int{{}, {6, 32}}, []int{10, 10}, []bool{false, false}, 0}, + {"out-tie", "scrabble_en", "out_of_tiles", [][]int{{}, {}}, []int{30, 30}, []bool{false, false}, 0}, + {"out-3p", "scrabble_ru", "out_of_tiles", [][]int{{}, {0, 1}, {2}}, []int{10, 10, 10}, []bool{false, false, false}, 0}, + {"scoreless", "scrabble_en", "scoreless", [][]int{{0, 1}, {2, 3}}, []int{20, 20}, []bool{false, false}, 0}, + {"resign-2p", "scrabble_en", "resign", [][]int{{}, {0}}, []int{100, 10}, []bool{true, false}, 1}, + {"resign-3p", "scrabble_en", "resign", [][]int{{}, {}, {}}, []int{50, 60, 40}, []bool{false, true, false}, 0}, + {"aborted", "scrabble_en", "aborted", [][]int{{0}, {1}}, []int{40, 30}, []bool{false, false}, 0}, + } + + out := make([]endCaseOut, 0, len(cases)) + for _, c := range cases { + rs := rulesetFor(c.Variant) + scores := append([]int(nil), c.Scores...) + g := &Game{ + rules: rs, + hands: handsBytes(c.Hands), + scores: scores, + resigned: c.Resigned, + toMove: c.ToMove, + } + reason := reasonFor(c.Reason) + g.over = true + g.reason = reason + g.applyEndAdjustment(reason) + out = append(out, endCaseOut{endCaseIn: c, ScoresAfter: g.scores, Winner: g.winner()}) + } + + dir := filepath.Join("..", "..", "..", "ui", "src", "lib", "localgame", "testdata") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatalf("mkdir %s: %v", dir, err) + } + data, err := json.MarshalIndent(map[string]any{"cases": out}, "", " ") + if err != nil { + t.Fatalf("marshal: %v", err) + } + path := filepath.Join(dir, "endgame.json") + if err := os.WriteFile(path, append(data, '\n'), 0o644); err != nil { + t.Fatalf("write %s: %v", path, err) + } + t.Logf("wrote %s (%d cases)", path, len(out)) +} diff --git a/backend/internal/game/helpers_test.go b/backend/internal/game/helpers_test.go index 3c6d0bd..0cca088 100644 --- a/backend/internal/game/helpers_test.go +++ b/backend/internal/game/helpers_test.go @@ -69,6 +69,29 @@ func TestHintsRemaining(t *testing.T) { } } +func TestHintUnlockLeftSeconds(t *testing.T) { + now := time.Now() + // A gated vs_ai game on the caller's turn, the robot having moved 10 min ago (turn started then). + gated := Game{VsAI: true, ToMove: 0, MoveCount: 2, TurnStartedAt: now.Add(-10 * time.Minute)} + cases := []struct { + name string + g Game + seat int + want int + }{ + {"non-vs_ai is open", Game{VsAI: false, ToMove: 0, MoveCount: 2, TurnStartedAt: now.Add(-10 * time.Minute)}, 0, 0}, + {"not the caller's turn is open", gated, 1, 0}, + {"human first move (no robot move) is open", Game{VsAI: true, ToMove: 0, MoveCount: 0, TurnStartedAt: now}, 0, 0}, + {"robot moved 10 min ago leaves 20 min", gated, 0, 20 * 60}, + {"robot moved past the window is open", Game{VsAI: true, ToMove: 0, MoveCount: 2, TurnStartedAt: now.Add(-40 * time.Minute)}, 0, 0}, + } + for _, c := range cases { + if got := hintUnlockLeftSeconds(c.g, c.seat, now); got != c.want { + t.Errorf("%s: hintUnlockLeftSeconds = %d, want %d", c.name, got, c.want) + } + } +} + func TestAllowedTimeout(t *testing.T) { if !allowedTimeout(24 * time.Hour) { t.Error("24h must be allowed") diff --git a/backend/internal/game/service.go b/backend/internal/game/service.go index 162adaf..2026429 100644 --- a/backend/internal/game/service.go +++ b/backend/internal/game/service.go @@ -1058,10 +1058,31 @@ func (svc *Service) MarkChangesApplied(ctx context.Context, variant engine.Varia return svc.store.MarkChangesApplied(ctx, variant.String(), version) } -// Hint reveals the top-scoring legal play for the requesting player on their -// turn, spending one hint from their per-game allowance and then their profile -// wallet. It returns ErrHintsDisabled, ErrNoHintsLeft or ErrNoHintAvailable as -// appropriate. +// hintIdleWindow is how long a vs_ai player must be stuck on a turn (since the robot's last move) +// before the idle hint unlocks. Mirrors the offline client (lib/hints HINT_GATE_MS = 30 min). +const hintIdleWindow = 30 * time.Minute + +// hintUnlockLeftSeconds is the seconds until g's vs_ai idle hint unlocks for seat, measured from now: +// the robot's last move (the current turn's start, on the human's turn) plus the window, floored at +// 0 and ceiled to whole seconds. It is 0 for a non-vs_ai game, when it is not seat's turn, or on the +// human's first move (MoveCount 0, no robot move yet) — the gate is an anti-frustration aid, not a +// first-move tax. The client anchors a monotonic countdown to it, so a client clock cannot skew it. +func hintUnlockLeftSeconds(g Game, seat int, now time.Time) int { + if !g.VsAI || g.ToMove != seat || g.MoveCount < 1 { + return 0 + } + left := g.TurnStartedAt.Add(hintIdleWindow).Sub(now) + if left <= 0 { + return 0 + } + return int((left + time.Second - 1) / time.Second) // ceil to whole seconds (no math import) +} + +// Hint reveals the top-scoring legal play for the requesting player on their turn. For a human game +// it spends one hint from the per-game allowance then the profile wallet (ErrHintsDisabled / +// ErrNoHintsLeft / ErrNoHintAvailable). For a vs_ai game the hint is unlimited and wallet-free but +// idle-gated from the server clock (ErrHintLocked until the window elapses) and counts toward no +// hint statistic. func (svc *Service) Hint(ctx context.Context, gameID, accountID uuid.UUID) (HintResult, error) { pre, err := svc.store.GetGame(ctx, gameID) if err != nil { @@ -1080,6 +1101,26 @@ func (svc *Service) Hint(ctx context.Context, gameID, accountID uuid.UUID) (Hint if !pre.HintsAllowed { return HintResult{}, ErrHintsDisabled } + if pre.VsAI { + // vs_ai: unlimited and wallet-free, but idle-gated from the server clock — and counted toward + // no hint statistic. Enforce the gate (the client normally pre-gates from the view's + // HintUnlockLeftSeconds; this is the authoritative backstop), then serve the top move without + // touching the allowance, the wallet or hints_used. + if hintUnlockLeftSeconds(pre, seat, svc.clock()) > 0 { + return HintResult{}, ErrHintLocked + } + unlock := svc.locks.lock(gameID) + defer unlock() + g, err := svc.liveGame(ctx, pre) + if err != nil { + return HintResult{}, err + } + move, ok := g.HintView() + if !ok { + return HintResult{}, ErrNoHintAvailable + } + return HintResult{Move: move}, nil + } acc, err := svc.accounts.GetByID(ctx, accountID) if err != nil { return HintResult{}, err @@ -1208,6 +1249,8 @@ func (svc *Service) GameState(ctx context.Context, gameID, accountID uuid.UUID) BagLen: g.BagLen(), HintsRemaining: hintsRemaining(pre.HintsPerPlayer, pre.Seats[seat].HintsUsed, acc.HintBalance), WalletBalance: acc.HintBalance, + // vs_ai idle-hint gate (seconds left; 0 for a human game / first move / not your turn). + HintUnlockLeftSeconds: hintUnlockLeftSeconds(pre, seat, svc.clock()), }, nil } diff --git a/backend/internal/game/types.go b/backend/internal/game/types.go index d39f231..26b173e 100644 --- a/backend/internal/game/types.go +++ b/backend/internal/game/types.go @@ -65,6 +65,10 @@ var ( ErrNoHintsLeft = errors.New("game: no hints remaining") // ErrNoHintAvailable is returned when the player has no legal play to reveal. ErrNoHintAvailable = errors.New("game: no legal move to suggest") + // ErrHintLocked is returned when a vs_ai game's idle hint is still gated (the player has not yet + // been stuck the idle window since the robot's last move). The client normally pre-gates from + // StateView.HintUnlockLeftSeconds, so this is the authoritative server-clock backstop. + ErrHintLocked = errors.New("game: hint is not available yet") // ErrGameLimitReached is returned when an account already holds MaxActiveQuickGames // simultaneous quick games and tries to create another game (quick or by invitation). ErrGameLimitReached = errors.New("game: simultaneous game limit reached") @@ -240,6 +244,11 @@ type StateView struct { // WalletBalance is the player's global hint-wallet balance alone (HintsRemaining folds // it in with the per-game allowance), so the client keeps the wallet live across games. WalletBalance int + // HintUnlockLeftSeconds is, for a vs_ai game on the requesting player's turn, the seconds left + // until the idle hint unlocks (the robot's last move plus the idle window, from the server clock); + // 0 for the human's first move, when it is not their turn, or a non-vs_ai game. The vs_ai hint is + // unlimited and wallet-free; this idle gate replaces the allowance/wallet for it. + HintUnlockLeftSeconds int } // HistoryMove is one decoded journal row, independent of any dictionary. diff --git a/backend/internal/robot/strategyfixture_test.go b/backend/internal/robot/strategyfixture_test.go new file mode 100644 index 0000000..78f1fa6 --- /dev/null +++ b/backend/internal/robot/strategyfixture_test.go @@ -0,0 +1,146 @@ +package robot + +import ( + "encoding/json" + "os" + "path/filepath" + "strconv" + "testing" + + "scrabble/backend/internal/engine" +) + +// The client-side offline robot (ui/src/lib/robot) reproduces the robot's move choice +// so a local vs_ai game plays the same way as the server. These golden fixtures pin that +// TS port to the real Go strategy. Being in-package, this emitter can exercise the +// unexported mix / playToWin / deviates / selectMove that the port mirrors. + +type mixCase struct { + Seed string `json:"seed"` + Salt string `json:"salt"` + Nums []int `json:"nums"` + Value string `json:"value"` // the mix() uint64, as a decimal string (exceeds JS safe ints) +} + +type decisionCase struct { + Seed string `json:"seed"` + MoveCount int `json:"moveCount"` + MyScore int `json:"myScore"` + OppScore int `json:"oppScore"` + CandScores []int `json:"candScores"` + RackLen int `json:"rackLen"` + BagLen int `json:"bagLen"` + PlayToWin bool `json:"playToWin"` + Win bool `json:"win"` // the per-turn intent after the deviate flip + Kind string `json:"kind"` + Index int `json:"index"` // chosen candidate index for a play, else -1 +} + +type strategyFixture struct { + PlayToWinPercent int `json:"playToWinPercent"` + Mix []mixCase `json:"mix"` + Decisions []decisionCase `json:"decisions"` +} + +// scenario is one selectMove situation exercised across several seeds. +type scenario struct { + moveCount, myScore, oppScore, rackLen, bagLen int + cands []int +} + +// TestEmitStrategyFixtures regenerates ui/src/lib/robot/testdata/strategy.json. It is +// gated by EMIT_STRATEGY_FIXTURES so a normal `go test` skips it; the committed output is +// what the TS parity test consumes. Regenerate with: +// +// EMIT_STRATEGY_FIXTURES=1 go test ./backend/internal/robot -run TestEmitStrategyFixtures +func TestEmitStrategyFixtures(t *testing.T) { + if os.Getenv("EMIT_STRATEGY_FIXTURES") == "" { + t.Skip("set EMIT_STRATEGY_FIXTURES=1 to regenerate ui/src/lib/robot/testdata/strategy.json") + } + + seeds := []int64{1, 42, -7, 1000003, 9999999999, 123456789, -123456789} + + mixCases := []mixCase{} + addMix := func(seed int64, salt string, nums ...int) { + mixCases = append(mixCases, mixCase{ + Seed: strconv.FormatInt(seed, 10), + Salt: salt, + Nums: append([]int{}, nums...), + Value: strconv.FormatUint(mix(seed, salt, nums...), 10), + }) + } + for _, sd := range seeds { + addMix(sd, "win") + addMix(sd, "deviate", 0) + addMix(sd, "deviate", 7) + } + + scenarios := []scenario{ + {3, 40, 55, 7, 20, []int{30, 12, 8}}, // behind, mid-game + {10, 120, 90, 7, 8, []int{25, 18, 5}}, // ahead, late + {1, 0, 0, 7, 30, []int{60, 30, 15, 7}}, // first move, big spread + {20, 200, 40, 5, 2, []int{50, 20}}, // big lead, bag near empty (no deviate) + {5, 30, 30, 7, 14, []int{20, 20, 20}}, // equal margins (tie-break) + {8, 50, 60, 7, 20, []int{}}, // no play, bag can refill -> exchange + {8, 50, 60, 3, 2, []int{}}, // no play, bag can't refill -> pass + {15, 300, 10, 7, 6, []int{80, 40, 10}}, // overshoots the band -> nearest to +30 + } + + decisions := []decisionCase{} + for _, sd := range seeds { + for _, sc := range scenarios { + cands := make([]engine.MoveRecord, len(sc.cands)) + for i, s := range sc.cands { + cands[i] = engine.MoveRecord{Score: s, Player: i} + } + rack := make([]string, sc.rackLen) + for i := range rack { + rack[i] = "a" + } + + ptw := playToWin(sd) + win := ptw + if deviates(sd, sc.moveCount, sc.bagLen) { + win = !win + } + d := selectMove(cands, sc.myScore, sc.oppScore, win, defaultBand, rack, sc.bagLen) + + kind, index := "pass", -1 + switch d.kind { + case decidePlay: + kind, index = "play", d.move.Player + case decideExchange: + kind = "exchange" + } + + decisions = append(decisions, decisionCase{ + Seed: strconv.FormatInt(sd, 10), + MoveCount: sc.moveCount, + MyScore: sc.myScore, + OppScore: sc.oppScore, + CandScores: append([]int{}, sc.cands...), + RackLen: sc.rackLen, + BagLen: sc.bagLen, + PlayToWin: ptw, + Win: win, + Kind: kind, + Index: index, + }) + } + } + + fx := strategyFixture{PlayToWinPercent: playToWinPercent, Mix: mixCases, Decisions: decisions} + dir := filepath.Join("..", "..", "..", "ui", "src", "lib", "robot", "testdata") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatalf("mkdir %s: %v", dir, err) + } + data, err := json.MarshalIndent(fx, "", " ") + if err != nil { + t.Fatalf("marshal: %v", err) + } + path := filepath.Join(dir, "strategy.json") + if err := os.WriteFile(path, append(data, '\n'), 0o644); err != nil { + t.Fatalf("write %s: %v", path, err) + } + t.Logf("wrote %s (%d mix, %d decisions)", path, len(mixCases), len(decisions)) +} diff --git a/backend/internal/server/banner.go b/backend/internal/server/banner.go index f86088d..8e5934d 100644 --- a/backend/internal/server/banner.go +++ b/backend/internal/server/banner.go @@ -8,6 +8,7 @@ import ( "scrabble/backend/internal/account" "scrabble/backend/internal/ads" + "scrabble/backend/internal/engine" "scrabble/backend/internal/notify" ) @@ -56,9 +57,34 @@ func (s *Server) profileResponse(ctx context.Context, acc account.Account) profi r := profileResponseFor(acc) r.Banner = s.bannerFor(ctx, acc) s.fillLinkedIdentities(ctx, &r, acc.ID) + r.DictVersions = s.currentDictVersions() return r } +// currentDictVersions reports the current dictionary version of every game variant with a +// resident dictionary, as engine.Variant stable labels. It backs profileResponse.DictVersions +// so an offline-capable client preloads the matching dawg per variant off the cold-start +// profile. A variant without a loaded dictionary is omitted (Registry.Latest reports +// ErrUnknownVariant); the returned slice is nil only when no variant is resident. +func (s *Server) currentDictVersions() []dictVersion { + if s.registry == nil { + return nil // no dictionary registry wired (e.g. a minimal test server): advertise none + } + variants := []engine.Variant{engine.VariantEnglish, engine.VariantRussianScrabble, engine.VariantErudit} + out := make([]dictVersion, 0, len(variants)) + for _, v := range variants { + version, _, err := s.registry.Latest(v) + if err != nil { + continue + } + out = append(out, dictVersion{Variant: v.String(), Version: version}) + } + if len(out) == 0 { + return nil + } + return out +} + // fillLinkedIdentities sets the profile's confirmed email address and platform-linked // flags from the account's identities, so the client offers the right link / unlink / // change-email controls. A read failure leaves them zero (no controls), logged as a diff --git a/backend/internal/server/dto.go b/backend/internal/server/dto.go index 071e966..15a2734 100644 --- a/backend/internal/server/dto.go +++ b/backend/internal/server/dto.go @@ -63,6 +63,20 @@ type profileResponse struct { Email string `json:"email"` TelegramLinked bool `json:"telegram_linked"` VkLinked bool `json:"vk_linked"` + // DictVersions lists the current dictionary version per game variant (engine.Variant + // stable labels). An installed PWA preparing for offline play reads it to preload the + // right dawg per enabled variant off this cold-start response, without an extra request. + // Filled outside the pure projection (it reads the dictionary registry), so it is empty + // for callers that build the DTO without a Server. See Server.profileResponse. + DictVersions []dictVersion `json:"dict_versions,omitempty"` +} + +// dictVersion pairs a game variant's stable label (engine.Variant.String) with its current +// dictionary version. profileResponse.DictVersions carries the set so an offline-capable +// client preloads the matching dawg per variant. +type dictVersion struct { + Variant string `json:"variant"` + Version string `json:"version"` } // tileDTO is one placed (or to-place) tile. @@ -149,13 +163,16 @@ type alphabetEntryDTO struct { // stateDTO is a player's view of a game. Rack carries wire alphabet indices (a // blank is engine.BlankIndex). Alphabet is present only when the request asked for it. type stateDTO struct { - Game gameDTO `json:"game"` - Seat int `json:"seat"` - Rack []int `json:"rack"` - BagLen int `json:"bag_len"` - HintsRemaining int `json:"hints_remaining"` - WalletBalance int `json:"wallet_balance"` - Alphabet []alphabetEntryDTO `json:"alphabet,omitempty"` + Game gameDTO `json:"game"` + Seat int `json:"seat"` + Rack []int `json:"rack"` + BagLen int `json:"bag_len"` + HintsRemaining int `json:"hints_remaining"` + WalletBalance int `json:"wallet_balance"` + // HintUnlockLeftSeconds is the vs_ai idle-hint gate: seconds until the hint unlocks (0 for a human + // game / first move / not your turn). The client anchors a monotonic countdown to it. + HintUnlockLeftSeconds int `json:"hint_unlock_left_seconds"` + Alphabet []alphabetEntryDTO `json:"alphabet,omitempty"` } // matchDTO reports whether the caller has been paired into a game. @@ -301,12 +318,13 @@ func stateDTOFrom(v game.StateView, includeAlphabet bool) (stateDTO, error) { return stateDTO{}, err } dto := stateDTO{ - Game: gameDTOFromGame(v.Game), - Seat: v.Seat, - Rack: rack, - BagLen: v.BagLen, - HintsRemaining: v.HintsRemaining, - WalletBalance: v.WalletBalance, + Game: gameDTOFromGame(v.Game), + Seat: v.Seat, + Rack: rack, + BagLen: v.BagLen, + HintsRemaining: v.HintsRemaining, + WalletBalance: v.WalletBalance, + HintUnlockLeftSeconds: v.HintUnlockLeftSeconds, } if includeAlphabet { tab, err := engine.AlphabetTable(v.Game.Variant) diff --git a/backend/internal/server/handlers.go b/backend/internal/server/handlers.go index 0909f67..6fc7d9c 100644 --- a/backend/internal/server/handlers.go +++ b/backend/internal/server/handlers.go @@ -239,6 +239,9 @@ func statusForError(err error) (int, string) { return http.StatusConflict, "no_hint_available" case errors.Is(err, game.ErrHintsDisabled), errors.Is(err, game.ErrNoHintsLeft): return http.StatusConflict, "hint_unavailable" + case errors.Is(err, game.ErrHintLocked): + // A vs_ai idle hint is still gated (the server-clock backstop); the client shows the lock. + return http.StatusConflict, "hint_locked" case errors.Is(err, engine.ErrIllegalPlay), errors.Is(err, engine.ErrTilesNotOnRack), errors.Is(err, engine.ErrGameOver): return http.StatusUnprocessableEntity, "illegal_play" case errors.Is(err, account.ErrEmailTaken), errors.Is(err, account.ErrIdentityTaken): diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f1e113f..1e6ae33 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1238,12 +1238,74 @@ separate site; the SPA shell is `noindex`, and the landing always boots in Russi browser-language detection — crawlers render with arbitrary languages). The favicon set, `og-image.png` and `robots.txt` ship unhashed from `ui/public/` (generated by `assets/icons/`, same tile design as the VK loader). On the web (`/app/`) the SPA is an installable **PWA**: -`manifest.webmanifest` and an install-only **service worker** (`sw.js`) ship unhashed from -`ui/public/`, and the client registers the worker **web-only** — never inside a Mini App or the -mock build. The worker intercepts only top-level navigations (network-first with a cached-shell -fallback), leaving `/assets/*` and the Connect stream untouched; it exists to satisfy Chromium's -installability requirement (a registered SW, needed for install on Android) and is the single -growth point for a future opt-in offline mode. The gateway registers the `.webmanifest` MIME type +`manifest.webmanifest` ships unhashed from `ui/public/`, while the **service worker** (`sw.js`) is +built from `ui/src/sw.ts` by **vite-plugin-pwa** (injectManifest strategy) — the client registers it +**web-only**, never inside a Mini App or the mock build (the plugin is disabled there entirely). It +**precaches the app shell and the hashed assets** (Workbox) so an installed PWA cold-launches with +no network. Shell **navigations are network-first** — the fresh `no-cache` shell is fetched from the +server (so a new deploy is picked up on the very next launch, not the one after), falling back to the +precached shell only when the network is unreachable or too slow; the immutable hashed assets stay +cache-first, and the hash router resolves the route client-side. This navigation route is registered +**before** the precache route on purpose: Workbox matches in registration order, and the precache +route (its `directoryIndex` is `index.html`) would otherwise shadow it and serve the shell +cache-first — the old build's version until a second load. The landing page and the conditional +polyfill bundle are excluded, and the Connect stream and runtime API POSTs are never precached nor +intercepted, so the live app is never served stale. This satisfies Chromium's installability requirement (a registered SW, needed +for install on Android) and powers the opt-in **offline mode** (in progress): a deliberate, device-scoped Settings +toggle — distinct from the transient gateway-reachability signal — that tints the header blue with +an *Offline* chip and confines play to on-device `vs_ai` games. The **offline lobby lists only those +device-local games** (reconstructed by replaying the IndexedDB move journal) and its New-vs-AI entry +creates one through the in-browser engine — the same game screen then drives it, the robot replying +locally; online-only affordances (the Stats tab, the random-opponent option) are disabled +or hidden, and New Game's *with friends* becomes the entry to **local pass-and-play (hotseat)**. +A hotseat game is a device-local **2-4 human** game built through the same engine (`hotseat` + +per-seat/host PIN locks on the record; the seats carry names but no accounts). A **mandatory host +(referee) PIN** gates the roster at creation and, in-game, the referee overrides — **skip** the +current turn, **exclude** a player (a forced seat resign, `resignSeat`) or **terminate** the game +(deleted, no winner/loser); each seat may also carry its own **optional PIN** that withholds its rack +(the board stays visible — only the tray is gated) until it is entered, so players pass the device +around freely and lock a seat only against a distrusted tablemate. The unlock is **per turn** +(re-locks on advance). PINs are a **social lock, not cryptography** — a salted SHA-256 kept only in +the device record (`lib/pin`); a 4-digit PIN is trivially brute-forceable and the racks live in +client memory regardless, so they defeat a casual glance, not a determined peek. A hotseat game is +**not `vs_ai`**: hints (the freed control becomes the host button) and chat/self-resign are gone, and +its lobby card — active *and* finished — is master-PIN-gated to delete (so the last mover cannot +instantly wipe it); a naturally finished game is saved with its result like any local game. A `vs_ai` +hint (online and offline alike) is unlimited and wallet-free but idle-gated +(unlocked ~30 min into a stuck turn), and counts toward no hint statistic. The **source** reports the +**seconds left** (`hint_unlock_left_seconds` on the game view) — computed by the **backend from the +server clock** online (which also enforces the gate, returning `hint_locked` for an early request), +and by the offline source from the device clock (persisted, capped at the window). The **client +anchors a MONOTONIC countdown** (`performance.now()`, `lib/hints`) to that seconds-left when it lands +(on load, and to the full window when the robot moves), so a client clock change cannot skew it, and +a relaunch re-reads a fresh value so the wait is not forgotten. To have data ready before the switch, the **Profile advertises the current dictionary +version per variant** (`dict_versions`, +filled from the registry on the existing cold-start profile request — no extra round-trip), and an +eligible installed PWA (standalone web + confirmed email) **background-preloads** those dictionaries +— on lobby entry and on a variant-preference change — through the same three-tier loader, retried +with backoff and honouring the session miss-breaker; the move generator, the loader and the preload +orchestration stay in lazy chunks. A first-lobby preload failure shows a *poor-connection* notice in +the ad-banner slot. Flipping the Settings toggle **to** offline runs that same cache-first fetch +bounded by a ~5 s UI wait (`raceOfflineReady` + the lazy `dict/offlineready`): it enters offline only +once every enabled variant is ready, otherwise it stays online with a *needs internet* note while the +fetch finishes in the background (the next flip is then instant). A **cold launch already in offline +mode** boots from the persisted session and +profile (the profile is saved on every online adopt/refresh) — `bootstrap` skips the session +adoption and profile fetch that would otherwise hang with no network, and lands straight in the +offline lobby; without a cached profile (an install that was never online) it drops the sticky flag +and boots online instead. Beyond the sticky toggle the app **auto-detects connectivity**: the +reactive flag carries an *auto* bit, so a self-entered offline (no network) is transient +(session-only, never persisted) and self-heals to online when the network returns, while a deliberate +one (the toggle, or the cold-start dialog's *Switch*) persists. At cold start `navigator.onLine === +false` enters offline for the session; otherwise a single bounded reachability probe (a `profile.get`, +~3 s) decides — success boots online, a timeout on an eligible install raises a *No connection* dialog +(*Switch* = sticky offline, *Wait for network* = boot online with the reachability watcher retrying). +Mid-session the `window` `online`/`offline` events drive it — `offline` auto-enters, `online` +re-verifies reachability before returning — backed by a `navigator.onLine` poll because those events +are unreliable on some platforms (notably iOS PWAs). Offline is a real **transport kill switch** +(every gateway call is refused, so no traffic leaks); the reachability probe is the one call exempt +from it (it is the return-to-online mechanism), and the transient reachability watcher is suppressed +while offline. The gateway registers the `.webmanifest` MIME type in-process (the distroless image has no `/etc/mime.types`). Hash-named `/assets/*` are served `immutable` (a relaunch is a cache hit, not a re-download); the HTML shells are `no-cache` so a new deploy is picked up — both containers apply the same caching. An diff --git a/docs/FUNCTIONAL.md b/docs/FUNCTIONAL.md index 39ffc2b..bda5ac7 100644 --- a/docs/FUNCTIONAL.md +++ b/docs/FUNCTIONAL.md @@ -215,7 +215,16 @@ unlimited and offers a complaint on any result; for a word it finds, it also lin external reference dictionary (gramota.ru for Russian games, scrabblewordfinder.org for English) to look it up. Hints are governed per game — whether they are allowed and how many each player starts with — and draw on a -personal hint wallet once the per-game allowance is spent. The game ends when the +personal hint wallet once the per-game allowance is spent. Against the robot +(**vs_ai**) hints are instead **unlimited and wallet-free**, but idle-gated as an +anti-frustration aid: one unlocks only after the player has been **stuck ~30 minutes +on a turn** (timed from the robot's last move; the very first move, before the robot +has played, is exempt). While gated the hint button carries a small **🔒 lock** and a +tap shows how long remains; the lock lifts live at the mark. The same gate applies **online and +offline**: the countdown runs on a **steady in-app timer**, not the device clock, so changing the +clock cannot skew it, and a relaunch re-reads the remaining time so a stuck turn is not forgotten. +Online the **server enforces** it from its own clock (which the player cannot touch); a vs_ai hint +counts toward no hint statistic. The game ends when the bag empties and a player clears their rack, after 6 consecutive scoreless turns, by resignation, or by the per-game move timeout (5 minutes to 24 hours, default 24 hours): a missed turn auto-resigns, except while the player is inside their @@ -256,6 +265,44 @@ the same occasional move against its plan that fades out by the endgame), and ch add-friend are off. AI games are **practice** — they never count toward a player's statistics. +### Offline mode +An installed web PWA signed in with a confirmed email can switch to a deliberate **offline mode** +(Settings → Play mode). Offline, the app never touches the network: the header turns blue with an +*Offline* chip, the lobby lists only the games stored on the device, and online-only surfaces are +disabled or hidden (the Stats tab; the *random opponent* option in New Game). +A New Game against the robot creates a device-local **vs_ai** game — it +plays entirely in the browser with no backend. Local games are kept on the device, visible only in +offline mode, and never sync to the account. So a game can be created and played with no connection, +the app quietly preloads the dictionaries for the player's enabled variants while still online, and +(once installed) launches from a precached shell even with no network. Switching **to** offline first +checks that the enabled variants' dictionaries are on the device — if any are missing it fetches them +and waits briefly, and if they cannot be readied it stays online with a short *needs internet* note +(the download keeps running in the background, so a later switch is instant). The mode is +device-scoped and sticky across launches. + +Offline, New Game's **with friends** starts a **local pass-and-play** game for 2-4 people sharing one +device. You first set a **host (leader) password** — a 4-digit PIN entered on a lock-screen-style +keypad; until it is set the player rows stay locked. The app then asks whether you are playing too — +if so you take the first seat with your profile name. You name each player (2 to 4; add or remove +rows, where a removal asks for the leader password) and may give any seat its **own optional PIN**. +During the game the board is always visible, but a PIN-locked seat shows an **Unlock** button in place +of its rack until its owner enters the PIN — so the device passes hand to hand and only a protected +seat hides its letters; the lock returns each turn. The **leader** (the host button in the game) can, +with the leader password, **skip** the current player's turn, **remove** a player, or **end the game +early** — an early end discards the game with no winner or loser. Hints and chat are off in a +pass-and-play game. A game that finishes normally is saved to the offline lobby like any other; +deleting any pass-and-play game — running or finished — from the lobby asks for the leader password, +so no one can wipe a game the moment they have moved. + +The app also **enters offline mode on its own** when it cannot reach the network. On a cold launch +with no connection it switches to offline for that session; when the device is online but the gateway +stays silent for a few seconds it asks first — *No connection. Switch to offline mode?*, with +**Switch** (go offline) or **Wait for network** (keep retrying online). While the app is open, losing +the network — airplane mode — switches to offline automatically, and restoring it returns online by +itself. A self-entered offline is temporary: it reverts to online the moment the network is back, and +the next launch re-checks. A **deliberate** offline — the Settings toggle, or **Switch** in that +dialog — is the player's choice: it is kept, and never undone automatically. + ### Social: friends, block, chat, nudge Become friends in two ways: redeem a **one-time code** the other player issues (six digits, valid for twelve hours), or send a **request to someone you have played diff --git a/docs/FUNCTIONAL_ru.md b/docs/FUNCTIONAL_ru.md index 1c6700e..a2ca461 100644 --- a/docs/FUNCTIONAL_ru.md +++ b/docs/FUNCTIONAL_ru.md @@ -222,7 +222,16 @@ e-mail) либо ввод фразы. Активные игры форфейтя внешний справочный словарь (gramota.ru для русских игр, scrabblewordfinder.org для английских), чтобы его посмотреть. Подсказки управляются настройками партии — разрешены ли они и сколько их у каждого игрока на старте — и расходуют -личный кошелёк подсказок после исчерпания внутриигрового лимита. Партия +личный кошелёк подсказок после исчерпания внутриигрового лимита. Против робота +(**vs_ai**) подсказки, наоборот, **безлимитны и без кошелька**, но с idle-гейтом как +анти-фрустрация: подсказка открывается, только когда игрок **застрял на ходу ~30 минут** +(отсчёт от последнего хода робота; самый первый ход, до хода робота, исключён). Пока гейт +закрыт, кнопка подсказки несёт маленький **🔒 замок**, а тап показывает, сколько осталось; +замок снимается вживую в нужный момент. Один и тот же гейт работает **онлайн и офлайн**: отсчёт +идёт по **ровному внутриигровому таймеру**, а не по системным часам, поэтому их перевод его не +собьёт, а повторный вход перечитывает остаток — застрявший ход не забывается. Онлайн гейт +**принуждает сервер** по своим часам (их игрок не тронет); vs_ai-подсказка не учитывается ни в +одной статистике подсказок. Партия завершается, когда мешок пуст и игрок выложил стойку, после 6 подряд бесплодных ходов, по сдаче, либо по таймауту хода (от 5 минут до 24 часов, дефолт 24 часа): пропущенный ход означает авто-сдачу, кроме как когда игрок внутри своего @@ -261,6 +270,45 @@ e-mail) либо ввод фразы. Активные игры форфейтя паузы), сохраняет ту же силу (по-прежнему играет на победу лишь примерно в 40% партий, с теми же редкими ходами вопреки плану, затухающими к эндшпилю), а чат, nudge и «добавить в друзья» выключены. Партии с ИИ — это **тренировка**: они не идут в статистику игрока. +### Офлайн-режим +Установленный веб-PWA со входом по подтверждённой почте может переключиться в осознанный +**офлайн-режим** (Настройки → Режим игры). В офлайне приложение не обращается к сети: шапка синеет с +меткой *Офлайн*, лобби показывает только сохранённые на устройстве игры, а сетевые поверхности +отключены или скрыты (вкладка Статистика; вариант «случайный соперник» в Новой игре). +Новая игра против робота создаёт локальную **vs_ai**-игру — он ходит +целиком в браузере без бэкенда. Локальные игры хранятся на устройстве, видны только в офлайн-режиме и +никогда не синхронизируются с аккаунтом. Чтобы игру можно было создать и сыграть без связи, приложение +заранее, пока ещё онлайн, подгружает словари включённых игроком вариантов и (после установки) +запускается из прекешированного шелла даже без сети. Переключение **в** офлайн сначала проверяет, что +словари включённых вариантов есть на устройстве — если каких-то не хватает, оно их подгружает и недолго +ждёт, а если подготовить их не удаётся, остаётся онлайн с короткой заметкой *нужен интернет* (загрузка +продолжается в фоне, так что следующее переключение мгновенно). Режим привязан к устройству и +сохраняется между запусками. + +В офлайне «с друзьями» в Новой игре запускает **локальную игру по очереди (hotseat)** на 2–4 человек +за одним устройством. Сначала задаётся **пароль ведущего** — 4-значный PIN на клавиатуре в стиле +экрана блокировки; пока он не задан, строки игроков заблокированы. Затем приложение спрашивает, +играете ли вы сами — если да, вы занимаете первое место под именем из профиля. Вы называете каждого +игрока (от 2 до 4; строки можно добавлять и удалять, удаление спрашивает пароль ведущего) и можете +дать любому месту **свой необязательный PIN**. Во время игры доска видна всегда, но место с PIN +вместо стойки показывает кнопку **«Разблокировать»**, пока владелец не введёт PIN, — так устройство +передаётся из рук в руки, и только защищённое место прячет свои буквы; на каждом ходу замок +возвращается. **Ведущий** (кнопка ведущего в игре) может по паролю ведущего **пропустить** ход +текущего игрока, **исключить** игрока или **завершить игру досрочно** — досрочное завершение стирает +партию без победителей и проигравших. Подсказки и чат в игре по очереди отключены. Нормально +завершённая партия сохраняется в офлайн-лобби как любая другая; удаление любой игры по очереди — +идущей или завершённой — из лобби спрашивает пароль ведущего, чтобы никто не стёр партию сразу после +своего хода. + +Приложение также **само включает офлайн-режим**, когда не может достучаться до сети. При холодном +запуске без связи оно переходит в офлайн на текущую сессию; когда устройство онлайн, но шлюз молчит +несколько секунд, оно сперва спрашивает — *Нет связи. Включить офлайн-режим?*, с выбором **Включить** +(уйти в офлайн) или **Ждать сеть** (продолжить попытки онлайн). Пока приложение открыто, потеря сети — +режим полёта — переключает в офлайн автоматически, а её восстановление само возвращает онлайн. +Самостоятельно включённый офлайн временный: он возвращается в онлайн, как только сеть появилась, и +следующий запуск проверяет заново. **Осознанный** офлайн — тумблер в Настройках или **Включить** в том +диалоге — это выбор игрока: он сохраняется и никогда не отменяется автоматически. + ### Социальное: друзья, блок, чат, nudge Подружиться можно двумя способами: погасить **одноразовый код**, который выпускает другой игрок (шесть цифр, действует двенадцать часов), либо отправить **заявку diff --git a/docs/TESTING.md b/docs/TESTING.md index e22de9c..14c0eca 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -20,7 +20,18 @@ tests or touching CI. derivation and the GCG share/copy/download choice, plus Playwright specs against the mock for the friends screen (code issue/redeem, accept a request), the lobby invitations section, the stats screen, profile editing, and the export chooser's - finished-only visibility + its signed-URL download flow (route-intercepted). + finished-only visibility + its signed-URL download flow (route-intercepted). The + **offline mode** spec (`e2e/offline.spec.ts`) plays a full device-local `vs_ai` game + end-to-end: it forces the installed-PWA display mode, enters offline through the + Settings toggle (whose readiness check fetches the enabled variants' dawgs), then creates + and plays a local game with a **pinned bag seed** (`window.__mock.setLocalSeed`, so the + rack is deterministic and the human can tap out a precomputed opening), asserting the + robot's real reply and the IndexedDB replay after a reload. A local game needs a real + dictionary, so the mock's `fetchDict` serves the per-variant dawgs from the preview + build's `/e2edict/`, copied in by `scripts/e2e-dict.mjs` from `E2E_DICT_DIR` — the `ui` + CI job fetches the `scrabble-dictionary` release like the Go jobs; locally it defaults to + the sibling `scrabble-solver/dawg`. These dawgs are never committed and never enter the + production build. - **Render sidecar** — `renderer/` (Node + skia-canvas executing the shared `ui/src/lib/gameimage.ts`) carries a `node --test` smoke: the committed fixture (a real self-played 35-move game) must rasterize to a plausible PNG diff --git a/gateway/README.md b/gateway/README.md index a7adc18..95bcfcd 100644 --- a/gateway/README.md +++ b/gateway/README.md @@ -8,9 +8,10 @@ backend over REST/JSON, and bridges the backend's gRPC push stream to each client's in-app live channel. It **embeds the static UI build** (`go:embed`, baked in by the gateway image's node stage) and serves a **landing page** at `/` and the game **SPA** at `/app/` (web), `/telegram/` (the Telegram Mini App) and `/vk/` (the VK Mini App) — the single-origin model. -The web SPA is an installable **PWA**: it serves `manifest.webmanifest` (with the `.webmanifest` -MIME type registered in-process — the distroless image has no `/etc/mime.types`) and the -install-only `sw.js`, both shipped unhashed from `ui/public/`. +The web SPA is an installable **PWA**: it serves `manifest.webmanifest` (unhashed from `ui/public/`, +with the `.webmanifest` MIME type registered in-process — the distroless image has no +`/etc/mime.types`) and the app-shell `sw.js` (built from `ui/src/sw.ts` by vite-plugin-pwa, which +precaches the shell + assets so an installed PWA cold-launches offline). Hash-named `/assets/*` are served `immutable`; the HTML shells are `no-cache`. It can also serve the backend's admin console at `/_gm` behind HTTP Basic-Auth for a local non-caddy run; in the deployed contour the front caddy owns `/_gm` (see diff --git a/gateway/internal/backendclient/api.go b/gateway/internal/backendclient/api.go index 9aa357a..33091bd 100644 --- a/gateway/internal/backendclient/api.go +++ b/gateway/internal/backendclient/api.go @@ -42,6 +42,15 @@ type ProfileResp struct { Email string `json:"email"` TelegramLinked bool `json:"telegram_linked"` VkLinked bool `json:"vk_linked"` + // DictVersions is the current dictionary version per game variant, forwarded verbatim + // into the Profile payload so an offline-capable client preloads the matching dawg. + DictVersions []DictVersion `json:"dict_versions,omitempty"` +} + +// DictVersion pairs a game variant's stable label with its current dictionary version. +type DictVersion struct { + Variant string `json:"variant"` + Version string `json:"version"` } // BannerResp is the advertising-banner block of an eligible viewer's profile: the @@ -172,13 +181,14 @@ type AlphabetEntryJSON struct { // StateResp is a player's view of a game. Rack carries wire alphabet indices; // Alphabet is present only when the request asked for it. type StateResp struct { - Game GameResp `json:"game"` - Seat int `json:"seat"` - Rack []int `json:"rack"` - BagLen int `json:"bag_len"` - HintsRemaining int `json:"hints_remaining"` - WalletBalance int `json:"wallet_balance"` - Alphabet []AlphabetEntryJSON `json:"alphabet,omitempty"` + Game GameResp `json:"game"` + Seat int `json:"seat"` + Rack []int `json:"rack"` + BagLen int `json:"bag_len"` + HintsRemaining int `json:"hints_remaining"` + WalletBalance int `json:"wallet_balance"` + HintUnlockLeftSeconds int `json:"hint_unlock_left_seconds"` + Alphabet []AlphabetEntryJSON `json:"alphabet,omitempty"` } // MatchResp reports an auto-match outcome. diff --git a/gateway/internal/transcode/encode.go b/gateway/internal/transcode/encode.go index 3258ba4..4c7af5c 100644 --- a/gateway/internal/transcode/encode.go +++ b/gateway/internal/transcode/encode.go @@ -95,6 +95,7 @@ func encodeProfile(p backendclient.ProfileResp) []byte { banner = encodeBanner(b, *p.Banner) } prefs := buildStringVector(b, p.VariantPreferences, fb.ProfileStartVariantPreferencesVector) + dictVersions := encodeDictVersions(b, p.DictVersions) fb.ProfileStart(b) fb.ProfileAddUserId(b, uid) fb.ProfileAddDisplayName(b, name) @@ -111,6 +112,9 @@ func encodeProfile(p backendclient.ProfileResp) []byte { fb.ProfileAddEmail(b, email) fb.ProfileAddTelegramLinked(b, p.TelegramLinked) fb.ProfileAddVkLinked(b, p.VkLinked) + if dictVersions != 0 { + fb.ProfileAddDictVersions(b, dictVersions) + } if p.Banner != nil { fb.ProfileAddBanner(b, banner) } @@ -118,6 +122,31 @@ func encodeProfile(p backendclient.ProfileResp) []byte { return b.FinishedBytes() } +// encodeDictVersions builds the Profile's dict_versions vector — one DictVersion table per +// variant/version pair — and returns its offset, or 0 when there are none (the field is then +// omitted). Every child table and its strings are created before the vector is opened, per the +// FlatBuffers rule against nesting a table under one still being built; the caller invokes it +// before ProfileStart for the same reason. +func encodeDictVersions(b *flatbuffers.Builder, dvs []backendclient.DictVersion) flatbuffers.UOffsetT { + if len(dvs) == 0 { + return 0 + } + offsets := make([]flatbuffers.UOffsetT, len(dvs)) + for i, dv := range dvs { + variant := b.CreateString(dv.Variant) + version := b.CreateString(dv.Version) + fb.DictVersionStart(b) + fb.DictVersionAddVariant(b, variant) + fb.DictVersionAddVersion(b, version) + offsets[i] = fb.DictVersionEnd(b) + } + fb.ProfileStartDictVersionsVector(b, len(offsets)) + for i := len(offsets) - 1; i >= 0; i-- { + b.PrependUOffsetT(offsets[i]) + } + return b.EndVector(len(offsets)) +} + // optString creates a FlatBuffers string for a non-empty value, or 0 to omit the // optional field (the client then reads it as absent). func optString(b *flatbuffers.Builder, s string) flatbuffers.UOffsetT { @@ -270,13 +299,14 @@ func toWireState(s backendclient.StateResp) wire.StateView { alphabet[i] = wire.AlphabetEntry{Index: e.Index, Letter: e.Letter, Value: e.Value} } return wire.StateView{ - Game: toWireGame(s.Game), - Seat: s.Seat, - Rack: s.Rack, - BagLen: s.BagLen, - HintsRemaining: s.HintsRemaining, - WalletBalance: s.WalletBalance, - Alphabet: alphabet, + Game: toWireGame(s.Game), + Seat: s.Seat, + Rack: s.Rack, + BagLen: s.BagLen, + HintsRemaining: s.HintsRemaining, + WalletBalance: s.WalletBalance, + HintUnlockLeftSeconds: s.HintUnlockLeftSeconds, + Alphabet: alphabet, } } diff --git a/gateway/internal/transcode/transcode_test.go b/gateway/internal/transcode/transcode_test.go index a2545af..9429423 100644 --- a/gateway/internal/transcode/transcode_test.go +++ b/gateway/internal/transcode/transcode_test.go @@ -59,7 +59,7 @@ func TestGameStateRoundTripForwardsUserID(t *testing.T) { if r.URL.Path != "/api/v1/user/games/g-1/state" { t.Errorf("unexpected path %q", r.URL.Path) } - _, _ = w.Write([]byte(`{"game":{"id":"g-1","variant":"scrabble_en","status":"active","players":2,"to_move":1,"seats":[{"seat":0,"account_id":"u-7","score":5}]},"seat":0,"rack":[0,1],"bag_len":80,"hints_remaining":4,"wallet_balance":3}`)) + _, _ = w.Write([]byte(`{"game":{"id":"g-1","variant":"scrabble_en","status":"active","players":2,"to_move":1,"seats":[{"seat":0,"account_id":"u-7","score":5}]},"seat":0,"rack":[0,1],"bag_len":80,"hints_remaining":4,"wallet_balance":3,"hint_unlock_left_seconds":1200}`)) }) defer cleanup() @@ -77,8 +77,8 @@ func TestGameStateRoundTripForwardsUserID(t *testing.T) { t.Fatalf("handler: %v", err) } st := fb.GetRootAsStateView(payload, 0) - if st.BagLen() != 80 || st.RackLength() != 2 || st.HintsRemaining() != 4 || st.WalletBalance() != 3 { - t.Fatalf("state decoded wrong: bag=%d rack=%d hints=%d wallet=%d", st.BagLen(), st.RackLength(), st.HintsRemaining(), st.WalletBalance()) + if st.BagLen() != 80 || st.RackLength() != 2 || st.HintsRemaining() != 4 || st.WalletBalance() != 3 || st.HintUnlockLeftSeconds() != 1200 { + t.Fatalf("state decoded wrong: bag=%d rack=%d hints=%d wallet=%d unlockLeft=%d", st.BagLen(), st.RackLength(), st.HintsRemaining(), st.WalletBalance(), st.HintUnlockLeftSeconds()) } game := st.Game(nil) if game == nil || string(game.Id()) != "g-1" || string(game.Variant()) != "scrabble_en" || game.ToMove() != 1 { diff --git a/gateway/internal/webui/webui.go b/gateway/internal/webui/webui.go index d999377..549d7b4 100644 --- a/gateway/internal/webui/webui.go +++ b/gateway/internal/webui/webui.go @@ -71,6 +71,11 @@ func handlerFor(content fs.FS, stripPrefix, indexName string) http.Handler { // app (notably a relaunched Telegram Mini App) is a cache hit, not a re-download. if strings.HasPrefix(name, "assets/") { w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + } else if name == "sw.js" { + // The service worker must be revalidated on every load so a new deploy's worker (and its + // fresh precache manifest) is picked up promptly; a cached sw.js would strand clients on + // the old build. The worker's network-first navigation then serves the fresh shell online. + w.Header().Set("Cache-Control", "no-cache") } files.ServeHTTP(w, r) }) diff --git a/gateway/internal/webui/webui_test.go b/gateway/internal/webui/webui_test.go index 90c174e..66de941 100644 --- a/gateway/internal/webui/webui_test.go +++ b/gateway/internal/webui/webui_test.go @@ -94,6 +94,10 @@ func TestHandlerServesServiceWorkerAsJavaScript(t *testing.T) { if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/javascript") { t.Errorf("Content-Type = %q, want text/javascript", ct) } + // Revalidated on every load so a new deploy's worker (and its fresh precache) is picked up. + if cc := resp.Header.Get("Cache-Control"); cc != "no-cache" { + t.Errorf("sw.js Cache-Control = %q, want no-cache", cc) + } } // TestHandlerFallsBackToShellForUnknownManifestPath guards the trap: a manifest/sw path NOT emitted diff --git a/pkg/fbs/scrabble.fbs b/pkg/fbs/scrabble.fbs index 636ab8f..3cc877e 100644 --- a/pkg/fbs/scrabble.fbs +++ b/pkg/fbs/scrabble.fbs @@ -223,6 +223,13 @@ table BannerInfo { // suppresses out-of-app platform push, leaving only the in-app live stream. banner // carries the advertising-banner block for an eligible viewer, absent otherwise // (all added trailing — backward-compatible). +// DictVersion is one variant's current dictionary version, carried on the profile so an offline +// client learns it from an existing cold-start request (no extra call for a rarely-used feature). +table DictVersion { + variant:string; + version:string; +} + table Profile { user_id:string; display_name:string; @@ -245,6 +252,10 @@ table Profile { email:string; telegram_linked:bool; vk_linked:bool; + // dict_versions carries each variant's current dictionary version so an offline client can + // preload the right dictionary and pin a new local game without a separate request (added + // trailing — backward-compatible). + dict_versions:[DictVersion]; } // BlockStatus reports the caller's current manual block. The UI fetches it after any operation @@ -305,6 +316,13 @@ table StateView { // the wallet is a single global figure the client keeps live across games (added trailing — // backward-compatible). wallet_balance:int; + // hint_unlock_left_seconds is, for a vs_ai game, how many seconds until the idle hint unlocks + // (the robot's last move plus the idle window, computed from the SERVER clock online / the device + // clock offline, capped at the window and floored at 0); 0 for a human's first move (no robot move + // yet) or a non-vs_ai game. The client anchors a MONOTONIC countdown (performance.now()) to it, so a + // client clock change cannot skew it — see lib/hints. Seconds granularity is enough; sent trailing + // (additive, backward-compatible). + hint_unlock_left_seconds:int; } // GameActionRequest carries just a game id (pass / resign / hint / history). diff --git a/pkg/fbs/scrabblefb/DictVersion.go b/pkg/fbs/scrabblefb/DictVersion.go new file mode 100644 index 0000000..31e60e9 --- /dev/null +++ b/pkg/fbs/scrabblefb/DictVersion.go @@ -0,0 +1,71 @@ +// Code generated by the FlatBuffers compiler. DO NOT EDIT. + +package scrabblefb + +import ( + flatbuffers "github.com/google/flatbuffers/go" +) + +type DictVersion struct { + _tab flatbuffers.Table +} + +func GetRootAsDictVersion(buf []byte, offset flatbuffers.UOffsetT) *DictVersion { + n := flatbuffers.GetUOffsetT(buf[offset:]) + x := &DictVersion{} + x.Init(buf, n+offset) + return x +} + +func FinishDictVersionBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.Finish(offset) +} + +func GetSizePrefixedRootAsDictVersion(buf []byte, offset flatbuffers.UOffsetT) *DictVersion { + n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) + x := &DictVersion{} + x.Init(buf, n+offset+flatbuffers.SizeUint32) + return x +} + +func FinishSizePrefixedDictVersionBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.FinishSizePrefixed(offset) +} + +func (rcv *DictVersion) Init(buf []byte, i flatbuffers.UOffsetT) { + rcv._tab.Bytes = buf + rcv._tab.Pos = i +} + +func (rcv *DictVersion) Table() flatbuffers.Table { + return rcv._tab +} + +func (rcv *DictVersion) Variant() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func (rcv *DictVersion) Version() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func DictVersionStart(builder *flatbuffers.Builder) { + builder.StartObject(2) +} +func DictVersionAddVariant(builder *flatbuffers.Builder, variant flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(variant), 0) +} +func DictVersionAddVersion(builder *flatbuffers.Builder, version flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(version), 0) +} +func DictVersionEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + return builder.EndObject() +} diff --git a/pkg/fbs/scrabblefb/Profile.go b/pkg/fbs/scrabblefb/Profile.go index 3bb715b..82f33d0 100644 --- a/pkg/fbs/scrabblefb/Profile.go +++ b/pkg/fbs/scrabblefb/Profile.go @@ -211,8 +211,28 @@ func (rcv *Profile) MutateVkLinked(n bool) bool { return rcv._tab.MutateBoolSlot(34, n) } +func (rcv *Profile) DictVersions(obj *DictVersion, j int) bool { + o := flatbuffers.UOffsetT(rcv._tab.Offset(36)) + if o != 0 { + x := rcv._tab.Vector(o) + x += flatbuffers.UOffsetT(j) * 4 + x = rcv._tab.Indirect(x) + obj.Init(rcv._tab.Bytes, x) + return true + } + return false +} + +func (rcv *Profile) DictVersionsLength() int { + o := flatbuffers.UOffsetT(rcv._tab.Offset(36)) + if o != 0 { + return rcv._tab.VectorLen(o) + } + return 0 +} + func ProfileStart(builder *flatbuffers.Builder) { - builder.StartObject(16) + builder.StartObject(17) } func ProfileAddUserId(builder *flatbuffers.Builder, userId flatbuffers.UOffsetT) { builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(userId), 0) @@ -265,6 +285,12 @@ func ProfileAddTelegramLinked(builder *flatbuffers.Builder, telegramLinked bool) func ProfileAddVkLinked(builder *flatbuffers.Builder, vkLinked bool) { builder.PrependBoolSlot(15, vkLinked, false) } +func ProfileAddDictVersions(builder *flatbuffers.Builder, dictVersions flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(16, flatbuffers.UOffsetT(dictVersions), 0) +} +func ProfileStartDictVersionsVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT { + return builder.StartVector(4, numElems, 4) +} func ProfileEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() } diff --git a/pkg/fbs/scrabblefb/StateView.go b/pkg/fbs/scrabblefb/StateView.go index 9ae7566..2957e44 100644 --- a/pkg/fbs/scrabblefb/StateView.go +++ b/pkg/fbs/scrabblefb/StateView.go @@ -156,8 +156,20 @@ func (rcv *StateView) MutateWalletBalance(n int32) bool { return rcv._tab.MutateInt32Slot(16, n) } +func (rcv *StateView) HintUnlockLeftSeconds() int32 { + o := flatbuffers.UOffsetT(rcv._tab.Offset(18)) + if o != 0 { + return rcv._tab.GetInt32(o + rcv._tab.Pos) + } + return 0 +} + +func (rcv *StateView) MutateHintUnlockLeftSeconds(n int32) bool { + return rcv._tab.MutateInt32Slot(18, n) +} + func StateViewStart(builder *flatbuffers.Builder) { - builder.StartObject(7) + builder.StartObject(8) } func StateViewAddGame(builder *flatbuffers.Builder, game flatbuffers.UOffsetT) { builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(game), 0) @@ -186,6 +198,9 @@ func StateViewStartAlphabetVector(builder *flatbuffers.Builder, numElems int) fl func StateViewAddWalletBalance(builder *flatbuffers.Builder, walletBalance int32) { builder.PrependInt32Slot(6, walletBalance, 0) } +func StateViewAddHintUnlockLeftSeconds(builder *flatbuffers.Builder, hintUnlockLeftSeconds int32) { + builder.PrependInt32Slot(7, hintUnlockLeftSeconds, 0) +} func StateViewEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() } diff --git a/pkg/wire/build.go b/pkg/wire/build.go index ca58f76..51f1081 100644 --- a/pkg/wire/build.go +++ b/pkg/wire/build.go @@ -87,13 +87,14 @@ type AlphabetEntry struct { // (wire alphabet indices), bag size and hint budget. Alphabet is set only when the // recipient may not have cached the variant's display table yet. type StateView struct { - Game GameView - Seat int - Rack []int - BagLen int - HintsRemaining int - WalletBalance int - Alphabet []AlphabetEntry + Game GameView + Seat int + Rack []int + BagLen int + HintsRemaining int + WalletBalance int + HintUnlockLeftSeconds int + Alphabet []AlphabetEntry } // AccountRef is a referenced account with its display name resolved. @@ -256,6 +257,7 @@ func BuildStateView(b *flatbuffers.Builder, s StateView) flatbuffers.UOffsetT { fb.StateViewAddBagLen(b, int32(s.BagLen)) fb.StateViewAddHintsRemaining(b, int32(s.HintsRemaining)) fb.StateViewAddWalletBalance(b, int32(s.WalletBalance)) + fb.StateViewAddHintUnlockLeftSeconds(b, int32(s.HintUnlockLeftSeconds)) if hasAlphabet { fb.StateViewAddAlphabet(b, alphabet) } diff --git a/ui/e2e/hotseat.spec.ts b/ui/e2e/hotseat.spec.ts new file mode 100644 index 0000000..2e26dab --- /dev/null +++ b/ui/e2e/hotseat.spec.ts @@ -0,0 +1,126 @@ +import { test, expect, type Page } from './fixtures'; + +// Offline pass-and-play (hotseat) is offered only in offline mode, which is gated to an installed +// standalone PWA with a confirmed email. The mock account has an email; force standalone so the +// Settings offline toggle shows. +async function forceStandalone(page: Page): Promise { + await page.addInitScript(() => { + const orig = window.matchMedia.bind(window); + window.matchMedia = (q: string) => + (q.includes('display-mode: standalone') + ? { matches: true, media: q, onchange: null, addEventListener() {}, removeEventListener() {}, addListener() {}, removeListener() {}, dispatchEvent: () => false } + : orig(q)) as MediaQueryList; + }); +} + +async function enterLobby(page: Page): Promise { + await page.getByRole('button', { name: /guest|гост/i }).first().click(); + await expect(page.locator('button.tab').nth(2)).toBeVisible(); +} + +async function goOffline(page: Page): Promise { + await page.locator('button.tab').nth(2).click(); // Settings + await page.getByRole('button', { name: /^(Offline|Оффлайн)$/ }).click(); + await expect(page.locator('header.nav.offline')).toBeVisible({ timeout: 15000 }); + await page.getByRole('button', { name: /Back|Назад/i }).click(); + await expect(page.locator('button.tab').nth(2)).toBeVisible(); +} + +// typePin clicks the on-screen keypad digits (the pad has no OK button — the 4th digit is the action). +// It first waits for the dots to be empty, so a call made right after a previous entry does not lose +// digits during the 250 ms verdict pause (the 4th digit → pause → clear/advance). +async function typePin(page: Page, digits: string): Promise { + await expect(page.locator('.pad .dot.on')).toHaveCount(0); + for (const d of digits) await page.locator('.pad .key', { hasText: d }).click(); +} + +test.describe('offline hotseat (pass-and-play)', () => { + test('create with a locked seat, unlock, host-skip, terminate from the lobby', async ({ page }) => { + await forceStandalone(page); + await page.goto('/'); + await enterLobby(page); + await goOffline(page); + + // A known seed: the bag deals a full rack deterministically, AND the seeded seating shuffle keeps + // the roster order (Ann first) so the locked-seat assertions below are stable. (Seat order is + // randomised at start; the shuffle is seed-driven — seed '1' would seat Bob first, '2' Ann first.) + await page.evaluate(() => (window as unknown as { __mock: { setLocalSeed(s: string): void } }).__mock.setLocalSeed('2')); + + // New game -> the offline mode selector now offers "with friends" (hotseat). + await page.locator('button.tab').nth(0).click(); + await page.getByRole('button', { name: /With friends|друзьями/i }).click(); + + // The player rows are disabled until the mandatory host (master) PIN is set. + await expect(page.locator('.pname').first()).toBeDisabled(); + + // Set the host PIN (enter + confirm), then decline taking a seat. + await page.locator('.hostpin .plink').click(); + await typePin(page, '9999'); + await typePin(page, '9999'); + await page.getByRole('button', { name: /^(No|Нет)$/ }).click(); + + // Pick the English variant (the plaques mirror Quick Match; "Scrabble" is the Latin English name). + await page.locator('.variant', { hasText: 'Scrabble' }).click(); + + // Two players: Ann (PIN-locked) and Bob (open). + await page.locator('.pname').nth(0).fill('Ann'); + await page.locator('.pname').nth(1).fill('Bob'); + + // Row-delete: a 3rd player's kebab asks the host PIN, which ARMS a ❌ (not a silent delete); + // tapping the ❌ removes the row. + await page.getByRole('button', { name: /Add player|Добавить/i }).click(); + await expect(page.locator('.prow')).toHaveCount(3); + await page.locator('.prow').nth(2).locator('.pkebab').click(); + await typePin(page, '9999'); + const rowDel = page.locator('.prow').nth(2).locator('.prow-del'); + await expect(rowDel).toBeVisible(); + await rowDel.click(); + await expect(page.locator('.prow')).toHaveCount(2); + + // Lock Ann's seat with a PIN. + await page.locator('.prow').nth(0).locator('.plink').click(); + await typePin(page, '1234'); + await typePin(page, '1234'); + + await page.locator('button.invite').click(); + await expect(page.locator('[data-cell]').first()).toBeVisible(); + + // Ann is to move and PIN-locked: the board is visible but her rack is withheld behind Unlock. + await expect(page.locator('.unlock')).toBeVisible(); + await expect(page.locator('.rack .tile')).toHaveCount(0); + + // A wrong PIN keeps it locked; the right PIN reveals the full rack. + await page.locator('.unlock').click(); + await typePin(page, '0000'); + await expect(page.locator('.pad')).toBeVisible(); // still open after a wrong PIN + await typePin(page, '1234'); + await expect(page.locator('.rack .tile')).toHaveCount(7); + + // Opening the history reveals NO social controls on the seat plaques (a local game is + // account-less), and the Dictionary entry (the comms button) is kept for the active game. + await page.locator('.scoreboard').click(); + await expect(page.locator('.fico')).toHaveCount(0); + await expect(page.locator('.chat-ico')).toBeVisible(); + await page.locator('.scoreboard').click(); + + // Host override: 🔐 -> master PIN -> skip the current turn -> confirm. The turn passes to Bob, + // whose seat is open, so his rack shows without a lock. + await page.locator('button.tab', { hasText: /Host|Ведущий/ }).click(); + await typePin(page, '9999'); + await page.getByRole('button', { name: /Skip|Пропустить/ }).click(); + await page.getByRole('button', { name: /^(OK|ОК)$/ }).click(); + await expect(page.locator('.unlock')).toHaveCount(0); + await expect(page.locator('.rack .tile')).toHaveCount(7); + + // Back in the lobby the active hotseat game has a kebab; terminating it needs the master PIN. + await page.getByRole('button', { name: /Back|Назад/i }).click(); + await expect(page.getByText('Ann', { exact: false }).first()).toBeVisible(); + await expect(page.locator('.rowwrap')).toHaveCount(1); + await page.locator('.kebab').first().click(); + const del = page.locator('.rowwrap.revealed .del'); + await expect(del).toBeVisible(); + await del.click(); + await typePin(page, '9999'); + await expect(page.locator('.rowwrap')).toHaveCount(0); + }); +}); diff --git a/ui/e2e/offline.spec.ts b/ui/e2e/offline.spec.ts new file mode 100644 index 0000000..591ea2c --- /dev/null +++ b/ui/e2e/offline.spec.ts @@ -0,0 +1,95 @@ +import { test, expect, type Page } from './fixtures'; + +// The offline mode is gated to an installed standalone PWA with a confirmed email. The mock account +// has an email; force the standalone display mode so the Settings offline toggle is offered. Only the +// `(display-mode: standalone)` query is overridden — theme/reduce-motion queries pass through. +async function forceStandalone(page: Page): Promise { + await page.addInitScript(() => { + const orig = window.matchMedia.bind(window); + window.matchMedia = (q: string) => + (q.includes('display-mode: standalone') + ? { matches: true, media: q, onchange: null, addEventListener() {}, removeEventListener() {}, addListener() {}, removeListener() {}, dispatchEvent: () => false } + : orig(q)) as MediaQueryList; + }); +} + +// After a load, land in the lobby. The mock cold-starts on the login screen (no seeded session), so +// click through as guest. Waiting for the button (rather than a point-in-time count()) is what makes +// this deterministic: a count() sampled during the pre-login splash frame returned 0, skipped the +// click, and then hung — or latched a transient lobby tab-bar — instead of opening the real lobby. +async function enterLobby(page: Page): Promise { + await page.getByRole('button', { name: /guest|гост/i }).first().click(); + // The lobby tab bar has three tabs in a fixed order: New (0), Stats (1), Settings (2). nth() is + // robust to locale, emoji variation selectors and the coachmark anchors (which the first-run + // onboarding strips after it completes). + await expect(page.locator('button.tab').nth(2)).toBeVisible(); +} + +// Pick a rack tile by its glyph and drop it on a board square (the rack of the pinned-seed game has +// all-distinct letters, so the glyph is unambiguous). +async function placeTile(page: Page, glyph: string, row: number, col: number): Promise { + await page.locator('.rack .tile', { hasText: glyph }).first().click(); + await page.locator(`[data-cell][data-row="${row}"][data-col="${col}"]`).click(); +} + +test.describe('offline mode', () => { + test('enter via the toggle, play a local vs_ai game, persist across a reload', async ({ page }) => { + await forceStandalone(page); + await page.goto('/'); + await enterLobby(page); + + // Online: a seeded online game (vs Ann) is listed. + await expect(page.getByText('Ann', { exact: false }).first()).toBeVisible(); + + // Enter offline through the real Settings toggle (its readiness check fetches every enabled + // variant's dawg, served from /e2edict/ by the mock) — the header turns blue with the chip. + await page.locator('button.tab').nth(2).click(); + await page.getByRole('button', { name: /^(Offline|Оффлайн)$/ }).click(); + await expect(page.locator('header.nav.offline')).toBeVisible({ timeout: 15000 }); + + // Back in the (now offline) lobby: the online games are hidden and the Stats tab is disabled. + await page.getByRole('button', { name: /Back|Назад/i }).click(); + await expect(page.locator('button.tab').nth(2)).toBeVisible(); + await expect(page.getByText('Ann', { exact: false })).toHaveCount(0); + await expect(page.locator('button.tab').nth(1)).toBeDisabled(); + + // Create a device-local English vs_ai game with a pinned bag seed (deals the rack NEWYMAO). + await page.evaluate(() => (window as unknown as { __mock: { setLocalSeed(s: string): void } }).__mock.setLocalSeed('1')); + await page.locator('button.tab').nth(0).click(); + // The English variant's display name is "Scrabble" (Latin) in both locales — the Russian + // variants are "Скрэббл"/"Эрудит"/"Erudite", so this uniquely selects the English game. + await page.locator('.variant', { hasText: 'Scrabble' }).click(); + await page.locator('button.invite').click(); + await expect(page.locator('[data-cell]').first()).toBeVisible(); + + // The human's first move is not idle-gated: the hint is available at once (no 🔒 lock badge). + await expect(page.locator('.lock')).toHaveCount(0); + + // The human plays WAY horizontally across the centre (7,5)-(7,7). + await placeTile(page, 'W', 7, 5); + await placeTile(page, 'A', 7, 6); + await placeTile(page, 'Y', 7, 7); + await expect(page.locator('[data-cell].pending')).toHaveCount(3); + await page.locator('.make').click(); + + // The play commits and the local robot replies with a real move, so the board carries more than + // WAY's three tiles. + await expect(async () => { + expect(await page.locator('[data-cell].filled').count()).toBeGreaterThan(3); + }).toPass({ timeout: 15000 }); + const filled = await page.locator('[data-cell].filled').count(); + + // Now it is the human's turn again after the robot moved: the idle hint gate arms, so the hint + // button shows the 🔒 lock (it lifts after 30 idle minutes on a monotonic clock — not waited here). + await expect(page.locator('.lock')).toBeVisible(); + + // Reload: the hash router restores the /game/ route and the local game replays from + // IndexedDB with every committed tile intact. + await page.reload(); + await expect(page.locator('[data-cell]').first()).toBeVisible(); + await expect(page.locator('[data-cell].filled')).toHaveCount(filled); + // The idle-hint gate is persisted (a wall-clock unlock time), so the 🔒 survives the reload — + // the wait was not reset by relaunching. + await expect(page.locator('.lock')).toBeVisible(); + }); +}); diff --git a/ui/e2e/router.spec.ts b/ui/e2e/router.spec.ts new file mode 100644 index 0000000..c023ca6 --- /dev/null +++ b/ui/e2e/router.spec.ts @@ -0,0 +1,26 @@ +import { expect, test } from './fixtures'; + +// The hash router must update its reactive `route` rune synchronously inside navigate(), not defer it +// to the asynchronous `hashchange` event. Bootstrap flips `app.ready` in the same tick right after +// `navigate('/login')` on an unauthenticated cold start; a route that trailed the hash for one frame +// let App.svelte render the stale route (the empty-hash lobby) under the new login screen — a visible +// lobby-shell flash plus a doomed games.list. It also made the offline e2e flaky: enterLobby could +// latch that transient lobby tab-bar instead of clicking through the login screen. + +test('navigate updates the reactive route synchronously (no hashchange lag)', async ({ page }) => { + await page.goto('/'); + // The bundle has loaded and installed the mock __router seam once the login screen is up. + await expect(page.getByRole('button', { name: /guest/i })).toBeVisible(); + + // Read the route in the SAME microtask as the navigate: the fix makes it the new route at once; + // the pre-fix code returned the previous route (it waited for the async hashchange). + const routeAfterNavigate = await page.evaluate(() => { + const r = (window as unknown as { __router: { navigate(p: string): void; route(): string } }).__router; + r.navigate('/settings'); + return r.route(); + }); + expect(routeAfterNavigate).toBe('settings'); + + // The hash was written too, so a reload/back still resolves the same route. + expect(new URL(page.url()).hash).toBe('#/settings'); +}); diff --git a/ui/e2e/viewport.spec.ts b/ui/e2e/viewport.spec.ts new file mode 100644 index 0000000..46e02b8 --- /dev/null +++ b/ui/e2e/viewport.spec.ts @@ -0,0 +1,69 @@ +import { test, expect, type Page } from './fixtures'; + +// Playwright's WebKit has no real soft keyboard, so emulate iOS's visual-viewport behaviour: replace +// window.visualViewport with a controllable fake BEFORE the app boots. The app's syncViewport +// (app.svelte.ts) reads visualViewport.height/offsetTop and mirrors them into --vvh / --vv-top, which +// position the pinned app-shell (app.css html.app-shell body). Driving the fake exercises the exact +// code path the real keyboard triggers on iOS — where the layout viewport does NOT shrink and the +// visual viewport instead offsets down toward the focused field. +async function installFakeViewport(page: Page): Promise { + await page.addInitScript(() => { + const fake = new EventTarget() as EventTarget & { height: number; offsetTop: number; width: number }; + fake.height = window.innerHeight; + fake.offsetTop = 0; + fake.width = window.innerWidth; + Object.defineProperty(window, 'visualViewport', { configurable: true, value: fake }); + (window as unknown as { __vv: typeof fake }).__vv = fake; + }); +} + +async function setViewport(page: Page, height: number, offsetTop: number): Promise { + await page.evaluate( + ([h, t]) => { + const vv = (window as unknown as { __vv: { height: number; offsetTop: number; dispatchEvent(e: Event): boolean } }).__vv; + vv.height = h; + vv.offsetTop = t; + vv.dispatchEvent(new Event('resize')); + vv.dispatchEvent(new Event('scroll')); + }, + [height, offsetTop], + ); +} + +async function shell(page: Page): Promise<{ vvh: string; top: string; bodyTop: string }> { + return page.evaluate(() => ({ + vvh: getComputedStyle(document.documentElement).getPropertyValue('--vvh').trim(), + top: getComputedStyle(document.documentElement).getPropertyValue('--vv-top').trim(), + bodyTop: getComputedStyle(document.body).top, + })); +} + +test.describe('visual-viewport shell (soft-keyboard alignment)', () => { + test('the pinned shell follows the visual viewport height AND offset', async ({ page }) => { + await installFakeViewport(page); + await page.goto('/'); + await expect(page.locator('html.app-shell')).toBeAttached(); + const innerH = await page.evaluate(() => window.innerHeight); + + // Keyboard closed: full height, no offset. + await setViewport(page, innerH, 0); + let s = await shell(page); + expect(s.top).toBe('0px'); + expect(s.bodyTop).toBe('0px'); + + // Keyboard open + iOS offset: the visual viewport shrinks AND offsets down. The pinned shell must + // follow BOTH — the body's top must equal the offset (not stay at 0), else the top-anchored + // content shows empty space below (the iOS bug this fixes). + await setViewport(page, innerH - 300, 180); + s = await shell(page); + expect(s.vvh).toBe(`${innerH - 300}px`); + expect(s.top).toBe('180px'); + expect(s.bodyTop).toBe('180px'); + + // Keyboard closed again: the offset reverts to 0 — no stale shift left behind. + await setViewport(page, innerH, 0); + s = await shell(page); + expect(s.top).toBe('0px'); + expect(s.bodyTop).toBe('0px'); + }); +}); diff --git a/ui/package.json b/ui/package.json index 9138db1..448202f 100644 --- a/ui/package.json +++ b/ui/package.json @@ -32,6 +32,10 @@ "svelte-check": "^4.1.0", "typescript": "^5.7.0", "vite": "^6.0.0", - "vitest": "^3.0.0" + "vite-plugin-pwa": "^0.21.2", + "vitest": "^3.0.0", + "workbox-core": "^7.4.1", + "workbox-precaching": "^7.4.1", + "workbox-routing": "^7.4.1" } } diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index b3d34cd..e993a88 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -21,8 +21,11 @@ export default defineConfig({ // under /app/ and /telegram/): served from the preview root, an absolute base keeps assets at // /assets/ so the SPA-fallback also boots a subpath like /telegram/. Base only prefixes asset // URLs, so the minified JS under test is identical to the contour's. + // After the mock build, copy the real per-variant dawgs into the preview output (dist-e2e) so the + // offline spec can play a real local vs_ai game; the files are e2e-only (never committed, never in + // the production build). Source: E2E_DICT_DIR (CI: the fetched release; local: the sibling). command: - 'pnpm exec vite build --mode mock --base / --outDir dist-e2e --emptyOutDir && pnpm exec vite preview --outDir dist-e2e --port 4173 --strictPort', + 'pnpm exec vite build --mode mock --base / --outDir dist-e2e --emptyOutDir && node scripts/e2e-dict.mjs && pnpm exec vite preview --outDir dist-e2e --port 4173 --strictPort', url: 'http://localhost:4173', reuseExistingServer: !process.env.CI, timeout: 120_000, diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 8b4434c..996bd0e 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -32,7 +32,7 @@ importers: version: 1.60.0 '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 - version: 5.1.1(svelte@5.56.0)(vite@6.4.3(@types/node@22.19.19)) + version: 5.1.1(svelte@5.56.0)(vite@6.4.3(@types/node@22.19.19)(terser@5.48.0)) '@types/node': specifier: ^22.10.0 version: 22.19.19 @@ -50,13 +50,532 @@ importers: version: 5.9.3 vite: specifier: ^6.0.0 - version: 6.4.3(@types/node@22.19.19) + version: 6.4.3(@types/node@22.19.19)(terser@5.48.0) + vite-plugin-pwa: + specifier: ^0.21.2 + version: 0.21.2(vite@6.4.3(@types/node@22.19.19)(terser@5.48.0))(workbox-build@7.4.1)(workbox-window@7.4.1) vitest: specifier: ^3.0.0 - version: 3.2.6(@types/node@22.19.19) + version: 3.2.6(@types/node@22.19.19)(terser@5.48.0) + workbox-core: + specifier: ^7.4.1 + version: 7.4.1 + workbox-precaching: + specifier: ^7.4.1 + version: 7.4.1 + workbox-routing: + specifier: ^7.4.1 + version: 7.4.1 packages: + '@apideck/better-ajv-errors@0.3.7': + resolution: {integrity: sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw==} + engines: {node: '>=10'} + peerDependencies: + ajv: '>=8' + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.7': + resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.29.7': + resolution: {integrity: sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.8': + resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.29.7': + resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.29.7': + resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.29.7': + resolution: {integrity: sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.29.7': + resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.29.7': + resolution: {integrity: sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7': + resolution: {integrity: sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7': + resolution: {integrity: sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7': + resolution: {integrity: sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7': + resolution: {integrity: sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7': + resolution: {integrity: sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7': + resolution: {integrity: sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.29.7': + resolution: {integrity: sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.29.7': + resolution: {integrity: sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.29.7': + resolution: {integrity: sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.29.7': + resolution: {integrity: sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.29.7': + resolution: {integrity: sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.29.7': + resolution: {integrity: sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.29.7': + resolution: {integrity: sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.29.7': + resolution: {integrity: sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.29.7': + resolution: {integrity: sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.29.7': + resolution: {integrity: sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.29.7': + resolution: {integrity: sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.29.7': + resolution: {integrity: sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.29.7': + resolution: {integrity: sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.29.7': + resolution: {integrity: sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7': + resolution: {integrity: sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.29.7': + resolution: {integrity: sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-explicit-resource-management@7.29.7': + resolution: {integrity: sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.29.7': + resolution: {integrity: sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.29.7': + resolution: {integrity: sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.29.7': + resolution: {integrity: sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.29.7': + resolution: {integrity: sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.29.7': + resolution: {integrity: sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.29.7': + resolution: {integrity: sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.29.7': + resolution: {integrity: sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.29.7': + resolution: {integrity: sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.29.7': + resolution: {integrity: sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.29.7': + resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.29.7': + resolution: {integrity: sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.29.7': + resolution: {integrity: sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.7': + resolution: {integrity: sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.29.7': + resolution: {integrity: sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7': + resolution: {integrity: sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.29.7': + resolution: {integrity: sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.29.7': + resolution: {integrity: sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.29.7': + resolution: {integrity: sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.29.7': + resolution: {integrity: sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.29.7': + resolution: {integrity: sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.29.7': + resolution: {integrity: sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.29.7': + resolution: {integrity: sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.29.7': + resolution: {integrity: sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.29.7': + resolution: {integrity: sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.29.7': + resolution: {integrity: sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.29.7': + resolution: {integrity: sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.29.7': + resolution: {integrity: sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.29.7': + resolution: {integrity: sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.29.7': + resolution: {integrity: sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.29.7': + resolution: {integrity: sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.29.7': + resolution: {integrity: sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.29.7': + resolution: {integrity: sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.29.7': + resolution: {integrity: sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.29.7': + resolution: {integrity: sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.29.7': + resolution: {integrity: sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.29.7': + resolution: {integrity: sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.29.7': + resolution: {integrity: sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + '@bufbuild/protobuf@2.12.0': resolution: {integrity: sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==} @@ -240,6 +759,10 @@ packages: cpu: [x64] os: [win32] + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -250,6 +773,9 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -261,6 +787,55 @@ packages: engines: {node: '>=18'} hasBin: true + '@rollup/plugin-babel@6.1.0': + resolution: {integrity: sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.3': + resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-replace@6.0.3': + resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-terser@1.0.0': + resolution: {integrity: sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.61.0': resolution: {integrity: sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ==} cpu: [arm] @@ -422,6 +997,10 @@ packages: '@swc/helpers@0.5.23': resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + '@trickfilm400/rollup-plugin-off-main-thread@3.0.0-pre1': + resolution: {integrity: sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==} + engines: {node: '>=12'} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -434,6 +1013,9 @@ packages: '@types/node@22.19.19': resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -479,22 +1061,105 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + aria-query@5.3.1: resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + babel-plugin-polyfill-corejs2@0.4.17: + resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.14.2: + resolution: {integrity: sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.42: + resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.4: + resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001800: + resolution: {integrity: sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -511,9 +1176,42 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + core-js-bundle@3.49.0: resolution: {integrity: sha512-WXc7oOsePN3aKFOJVG5zQdi+h/Jm2W0WIPYvRc4IG3vkNcbC2w6LlSzTmnhOl6N1xmOJEzCSNieX3mwF+3zBGw==} + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -531,17 +1229,69 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + devalue@5.8.1: resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-to-chromium@1.5.387: + resolution: {integrity: sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==} + + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} + engines: {node: '>= 0.4'} + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} @@ -553,13 +1303,33 @@ packages: '@typescript-eslint/types': optional: true + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eta@4.6.0: + resolution: {integrity: sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==} + engines: {node: '>=20'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -569,9 +1339,24 @@ packages: picomatch: optional: true + filelist@1.0.6: + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} + flatbuffers@25.9.23: resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -582,25 +1367,283 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -613,6 +1656,40 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-releases@2.0.50: + resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} + engines: {node: '>=18'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -637,14 +1714,65 @@ packages: engines: {node: '>=18'} hasBin: true + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss@8.5.15: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.13.2: + resolution: {integrity: sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==} + hasBin: true + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + rollup@4.61.0: resolution: {integrity: sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -654,22 +1782,130 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + serialize-javascript@7.0.7: + resolution: {integrity: sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==} + engines: {node: '>=20.0.0'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + smob@1.6.2: + resolution: {integrity: sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==} + engines: {node: '>=20.0.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + + strip-comments@2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + svelte-check@4.5.0: resolution: {integrity: sha512-9lNwPxCLWniFvQIcEv1LFqjIxcFtO3smb5+5BKbRJ3ttL4o2lXCej5rLF4DAnfLPI66oaA81vAxw6ILdIWI7kA==} engines: {node: '>= 18.0.0'} @@ -682,6 +1918,19 @@ packages: resolution: {integrity: sha512-kTXr26t1bchFp28ROrb957LtbujpBmBDibmqMGziVpUs7awBi96TGgX6SovrA8BNoEUDVRK2Fb9FkeYlGspoVg==} engines: {node: '>=18'} + temp-dir@2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + + tempy@0.6.0: + resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} + engines: {node: '>=10'} + + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} + engines: {node: '>=10'} + hasBin: true + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -704,9 +1953,32 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-fest@0.16.0: + resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} + engines: {node: '>=10'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + typescript@5.4.5: resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} @@ -717,14 +1989,64 @@ packages: engines: {node: '>=14.17'} hasBin: true + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} + engines: {node: '>=4'} + + unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + upath@1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite-plugin-pwa@0.21.2: + resolution: {integrity: sha512-vFhH6Waw8itNu37hWUJxL50q+CBbNcMVzsKaYHQVrfxTt3ihk3PeLO22SbiP1UNWzcEPaTQv+YVxe4G0KOjAkg==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@vite-pwa/assets-generator': ^0.2.6 + vite: ^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + workbox-build: ^7.3.0 + workbox-window: ^7.3.0 + peerDependenciesMeta: + '@vite-pwa/assets-generator': + optional: true + vite@6.4.3: resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -801,16 +2123,764 @@ packages: jsdom: optional: true + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + workbox-background-sync@7.4.1: + resolution: {integrity: sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g==} + + workbox-broadcast-update@7.4.1: + resolution: {integrity: sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A==} + + workbox-build@7.4.1: + resolution: {integrity: sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw==} + engines: {node: '>=20.0.0'} + + workbox-cacheable-response@7.4.1: + resolution: {integrity: sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw==} + + workbox-core@7.4.1: + resolution: {integrity: sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==} + + workbox-expiration@7.4.1: + resolution: {integrity: sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg==} + + workbox-google-analytics@7.4.1: + resolution: {integrity: sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw==} + + workbox-navigation-preload@7.4.1: + resolution: {integrity: sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ==} + + workbox-precaching@7.4.1: + resolution: {integrity: sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==} + + workbox-range-requests@7.4.1: + resolution: {integrity: sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ==} + + workbox-recipes@7.4.1: + resolution: {integrity: sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog==} + + workbox-routing@7.4.1: + resolution: {integrity: sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==} + + workbox-strategies@7.4.1: + resolution: {integrity: sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==} + + workbox-streams@7.4.1: + resolution: {integrity: sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw==} + + workbox-sw@7.4.1: + resolution: {integrity: sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw==} + + workbox-window@7.4.1: + resolution: {integrity: sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} snapshots: + '@apideck/better-ajv-errors@0.3.7(ajv@8.20.0)': + dependencies: + ajv: 8.20.0 + jsonpointer: 5.0.1 + leven: 3.1.0 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.7 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-member-expression-to-functions@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-wrap-function': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helper-wrap-function@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + + '@babel/plugin-syntax-import-assertions@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7) + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/template': 7.29.7 + + '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-dotall-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-duplicate-keys@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-dynamic-import@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-exponentiation-operator@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-for-of@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-literals@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-logical-assignment-operators@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-member-expression-literals@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-modules-amd@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-new-target@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-numeric-separator@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-object-rest-spread@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-super@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-regenerator@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-regexp-modifiers@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-reserved-words@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-shorthand-properties@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-spread@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-template-literals@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-typeof-symbol@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-unicode-escapes@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-unicode-property-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-unicode-sets-regex@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/preset-env@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/core': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7) + '@babel/plugin-syntax-import-assertions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.7) + '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-block-scoped-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-computed-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-dotall-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-duplicate-keys': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-explicit-resource-management': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-exponentiation-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-export-namespace-from': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-function-name': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-json-strings': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-logical-assignment-operators': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-member-expression-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-amd': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-systemjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-umd': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-new-target': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-numeric-separator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-object-super': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-property-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-regenerator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-regexp-modifiers': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-reserved-words': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-spread': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-sticky-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typeof-symbol': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-escapes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-property-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-sets-regex': 7.29.7(@babel/core@7.29.7) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.7) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.7) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7) + core-js-compat: 3.49.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/types': 7.29.7 + esutils: 2.0.3 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@bufbuild/protobuf@2.12.0': {} '@bufbuild/protoc-gen-es@2.12.0(@bufbuild/protobuf@2.12.0)': @@ -916,6 +2986,8 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true + '@isaacs/cliui@9.0.0': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -928,6 +3000,11 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -939,6 +3016,49 @@ snapshots: dependencies: playwright: 1.60.0 + '@rollup/plugin-babel@6.1.0(@babel/core@7.29.7)(rollup@4.61.0)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@rollup/pluginutils': 5.4.0(rollup@4.61.0) + optionalDependencies: + rollup: 4.61.0 + transitivePeerDependencies: + - supports-color + + '@rollup/plugin-node-resolve@16.0.3(rollup@4.61.0)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@4.61.0) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.12 + optionalDependencies: + rollup: 4.61.0 + + '@rollup/plugin-replace@6.0.3(rollup@4.61.0)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@4.61.0) + magic-string: 0.30.21 + optionalDependencies: + rollup: 4.61.0 + + '@rollup/plugin-terser@1.0.0(rollup@4.61.0)': + dependencies: + serialize-javascript: 7.0.7 + smob: 1.6.2 + terser: 5.48.0 + optionalDependencies: + rollup: 4.61.0 + + '@rollup/pluginutils@5.4.0(rollup@4.61.0)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.61.0 + '@rollup/rollup-android-arm-eabi@4.61.0': optional: true @@ -1018,25 +3138,25 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.3(@types/node@22.19.19)))(svelte@5.56.0)(vite@6.4.3(@types/node@22.19.19))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.3(@types/node@22.19.19)(terser@5.48.0)))(svelte@5.56.0)(vite@6.4.3(@types/node@22.19.19)(terser@5.48.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.56.0)(vite@6.4.3(@types/node@22.19.19)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.56.0)(vite@6.4.3(@types/node@22.19.19)(terser@5.48.0)) debug: 4.4.3 svelte: 5.56.0 - vite: 6.4.3(@types/node@22.19.19) + vite: 6.4.3(@types/node@22.19.19)(terser@5.48.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.3(@types/node@22.19.19))': + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.3(@types/node@22.19.19)(terser@5.48.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.3(@types/node@22.19.19)))(svelte@5.56.0)(vite@6.4.3(@types/node@22.19.19)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.3(@types/node@22.19.19)(terser@5.48.0)))(svelte@5.56.0)(vite@6.4.3(@types/node@22.19.19)(terser@5.48.0)) debug: 4.4.3 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.21 svelte: 5.56.0 - vite: 6.4.3(@types/node@22.19.19) - vitefu: 1.1.3(vite@6.4.3(@types/node@22.19.19)) + vite: 6.4.3(@types/node@22.19.19)(terser@5.48.0) + vitefu: 1.1.3(vite@6.4.3(@types/node@22.19.19)(terser@5.48.0)) transitivePeerDependencies: - supports-color @@ -1044,6 +3164,13 @@ snapshots: dependencies: tslib: 2.8.1 + '@trickfilm400/rollup-plugin-off-main-thread@3.0.0-pre1': + dependencies: + ejs: 3.1.10 + json5: 2.2.3 + magic-string: 0.30.21 + string.prototype.matchall: 4.0.12 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -1057,6 +3184,8 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/resolve@1.20.2': {} + '@types/trusted-types@2.0.7': {} '@typescript/vfs@1.6.4(typescript@5.4.5)': @@ -1074,13 +3203,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.6(vite@6.4.3(@types/node@22.19.19))': + '@vitest/mocker@3.2.6(vite@6.4.3(@types/node@22.19.19)(terser@5.48.0))': dependencies: '@vitest/spy': 3.2.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.3(@types/node@22.19.19) + vite: 6.4.3(@types/node@22.19.19)(terser@5.48.0) '@vitest/pretty-format@3.2.6': dependencies: @@ -1114,14 +3243,113 @@ snapshots: acorn@8.16.0: {} + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + aria-query@5.3.1: {} + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + async-function@1.0.0: {} + + async@3.2.6: {} + + at-least-node@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + axobject-query@4.1.0: {} + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.7): + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + core-js-compat: 3.49.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.42: {} + + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.7: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.4: + dependencies: + baseline-browser-mapping: 2.10.42 + caniuse-lite: 1.0.30001800 + electron-to-chromium: 1.5.387 + node-releases: 2.0.50 + update-browserslist-db: 1.2.3(browserslist@4.28.4) + + buffer-from@1.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-lite@1.0.30001800: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -1138,8 +3366,44 @@ snapshots: clsx@2.1.1: {} + commander@2.20.3: {} + + common-tags@1.8.2: {} + + convert-source-map@2.0.0: {} + core-js-bundle@3.49.0: {} + core-js-compat@3.49.0: + dependencies: + browserslist: 4.28.4 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-random-string@2.0.0: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -1148,10 +3412,122 @@ snapshots: deepmerge@4.3.1: {} + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + devalue@5.8.1: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + electron-to-chromium@1.5.387: {} + + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.4 + function.prototype.name: 1.2.0 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.22 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-to-primitive@1.3.4: + dependencies: + es-abstract-get: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -1181,52 +3557,376 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + escalade@3.2.0: {} + esm-env@1.2.2: {} esrap@2.2.9: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 + esutils@2.0.3: {} + + eta@4.6.0: {} + expect-type@1.3.0: {} + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-uri@3.1.3: {} + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 + filelist@1.0.6: + dependencies: + minimatch: 5.1.9 + flatbuffers@25.9.23: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fsevents@2.3.2: optional: true fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + function.prototype.name@1.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-own-enumerable-property-symbols@3.0.2: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.5 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-bigints@1.1.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + idb@7.1.1: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-map@2.0.3: {} + + is-module@1.0.0: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-obj@1.0.1: {} + is-reference@3.0.3: dependencies: '@types/estree': 1.0.9 + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-regexp@1.0.0: {} + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@2.0.1: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.6 + picocolors: 1.1.1 + + js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + jsesc@3.1.0: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonpointer@5.0.1: {} + kleur@4.1.5: {} + leven@3.1.0: {} + locate-character@3.0.0: {} + lodash.debounce@4.0.8: {} + + lodash.sortby@4.7.0: {} + loupe@3.2.1: {} + lru-cache@11.5.1: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.7 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.1 + + minipass@7.1.3: {} + mri@1.2.0: {} ms@2.1.3: {} nanoid@3.3.12: {} + node-releases@2.0.50: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.1 + minipass: 7.1.3 + pathe@2.0.3: {} pathval@2.0.1: {} @@ -1243,14 +3943,72 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + possible-typed-array-names@1.1.0: {} + postcss@8.5.15: dependencies: nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 + pretty-bytes@5.6.0: {} + + pretty-bytes@6.1.1: {} + + punycode@2.3.1: {} + readdirp@4.1.2: {} + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regenerate-unicode-properties@10.2.2: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + regexpu-core@6.4.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.2 + regjsgen: 0.8.0 + regjsparser: 0.13.2 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.1 + + regjsgen@0.8.0: {} + + regjsparser@0.13.2: + dependencies: + jsesc: 3.1.0 + + require-from-string@2.0.2: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + rollup@4.61.0: dependencies: '@types/estree': 1.0.9 @@ -1286,18 +4044,167 @@ snapshots: dependencies: mri: 1.2.0 + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + semver@6.3.1: {} + + serialize-javascript@7.0.7: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} + + smob@1.6.2: {} + source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + stackback@0.0.2: {} std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.1 + + string.prototype.trim@1.2.11: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + stringify-object@3.3.0: + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + + strip-comments@2.0.1: {} + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 + supports-preserve-symlinks-flag@1.0.0: {} + svelte-check@4.5.0(picomatch@4.0.4)(svelte@5.56.0)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -1331,6 +4238,22 @@ snapshots: transitivePeerDependencies: - '@typescript-eslint/types' + temp-dir@2.0.0: {} + + tempy@0.6.0: + dependencies: + is-stream: 2.0.1 + temp-dir: 2.0.0 + type-fest: 0.16.0 + unique-string: 2.0.0 + + terser@5.48.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -1346,21 +4269,92 @@ snapshots: tinyspy@4.0.4: {} + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + tslib@2.8.1: {} + type-fest@0.16.0: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + typescript@5.4.5: {} typescript@5.9.3: {} + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + undici-types@6.21.0: {} - vite-node@3.2.4(@types/node@22.19.19): + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.2.0 + + unicode-match-property-value-ecmascript@2.2.1: {} + + unicode-property-aliases-ecmascript@2.2.0: {} + + unique-string@2.0.0: + dependencies: + crypto-random-string: 2.0.0 + + universalify@2.0.1: {} + + upath@1.2.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.4): + dependencies: + browserslist: 4.28.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + vite-node@3.2.4(@types/node@22.19.19)(terser@5.48.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.3(@types/node@22.19.19) + vite: 6.4.3(@types/node@22.19.19)(terser@5.48.0) transitivePeerDependencies: - '@types/node' - jiti @@ -1375,7 +4369,18 @@ snapshots: - tsx - yaml - vite@6.4.3(@types/node@22.19.19): + vite-plugin-pwa@0.21.2(vite@6.4.3(@types/node@22.19.19)(terser@5.48.0))(workbox-build@7.4.1)(workbox-window@7.4.1): + dependencies: + debug: 4.4.3 + pretty-bytes: 6.1.1 + tinyglobby: 0.2.17 + vite: 6.4.3(@types/node@22.19.19)(terser@5.48.0) + workbox-build: 7.4.1 + workbox-window: 7.4.1 + transitivePeerDependencies: + - supports-color + + vite@6.4.3(@types/node@22.19.19)(terser@5.48.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -1386,16 +4391,17 @@ snapshots: optionalDependencies: '@types/node': 22.19.19 fsevents: 2.3.3 + terser: 5.48.0 - vitefu@1.1.3(vite@6.4.3(@types/node@22.19.19)): + vitefu@1.1.3(vite@6.4.3(@types/node@22.19.19)(terser@5.48.0)): optionalDependencies: - vite: 6.4.3(@types/node@22.19.19) + vite: 6.4.3(@types/node@22.19.19)(terser@5.48.0) - vitest@3.2.6(@types/node@22.19.19): + vitest@3.2.6(@types/node@22.19.19)(terser@5.48.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.6 - '@vitest/mocker': 3.2.6(vite@6.4.3(@types/node@22.19.19)) + '@vitest/mocker': 3.2.6(vite@6.4.3(@types/node@22.19.19)(terser@5.48.0)) '@vitest/pretty-format': 3.2.6 '@vitest/runner': 3.2.6 '@vitest/snapshot': 3.2.6 @@ -1413,8 +4419,8 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.4.3(@types/node@22.19.19) - vite-node: 3.2.4(@types/node@22.19.19) + vite: 6.4.3(@types/node@22.19.19)(terser@5.48.0) + vite-node: 3.2.4(@types/node@22.19.19)(terser@5.48.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.19 @@ -1432,9 +4438,177 @@ snapshots: - tsx - yaml + webidl-conversions@4.0.2: {} + + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + workbox-background-sync@7.4.1: + dependencies: + idb: 7.1.1 + workbox-core: 7.4.1 + + workbox-broadcast-update@7.4.1: + dependencies: + workbox-core: 7.4.1 + + workbox-build@7.4.1: + dependencies: + '@apideck/better-ajv-errors': 0.3.7(ajv@8.20.0) + '@babel/core': 7.29.7 + '@babel/preset-env': 7.29.7(@babel/core@7.29.7) + '@babel/runtime': 7.29.7 + '@rollup/plugin-babel': 6.1.0(@babel/core@7.29.7)(rollup@4.61.0) + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.61.0) + '@rollup/plugin-replace': 6.0.3(rollup@4.61.0) + '@rollup/plugin-terser': 1.0.0(rollup@4.61.0) + '@trickfilm400/rollup-plugin-off-main-thread': 3.0.0-pre1 + ajv: 8.20.0 + common-tags: 1.8.2 + eta: 4.6.0 + fast-json-stable-stringify: 2.1.0 + fs-extra: 9.1.0 + glob: 11.1.0 + pretty-bytes: 5.6.0 + rollup: 4.61.0 + source-map: 0.8.0-beta.0 + stringify-object: 3.3.0 + strip-comments: 2.0.1 + tempy: 0.6.0 + upath: 1.2.0 + workbox-background-sync: 7.4.1 + workbox-broadcast-update: 7.4.1 + workbox-cacheable-response: 7.4.1 + workbox-core: 7.4.1 + workbox-expiration: 7.4.1 + workbox-google-analytics: 7.4.1 + workbox-navigation-preload: 7.4.1 + workbox-precaching: 7.4.1 + workbox-range-requests: 7.4.1 + workbox-recipes: 7.4.1 + workbox-routing: 7.4.1 + workbox-strategies: 7.4.1 + workbox-streams: 7.4.1 + workbox-sw: 7.4.1 + workbox-window: 7.4.1 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + + workbox-cacheable-response@7.4.1: + dependencies: + workbox-core: 7.4.1 + + workbox-core@7.4.1: {} + + workbox-expiration@7.4.1: + dependencies: + idb: 7.1.1 + workbox-core: 7.4.1 + + workbox-google-analytics@7.4.1: + dependencies: + workbox-background-sync: 7.4.1 + workbox-core: 7.4.1 + workbox-routing: 7.4.1 + workbox-strategies: 7.4.1 + + workbox-navigation-preload@7.4.1: + dependencies: + workbox-core: 7.4.1 + + workbox-precaching@7.4.1: + dependencies: + workbox-core: 7.4.1 + workbox-routing: 7.4.1 + workbox-strategies: 7.4.1 + + workbox-range-requests@7.4.1: + dependencies: + workbox-core: 7.4.1 + + workbox-recipes@7.4.1: + dependencies: + workbox-cacheable-response: 7.4.1 + workbox-core: 7.4.1 + workbox-expiration: 7.4.1 + workbox-precaching: 7.4.1 + workbox-routing: 7.4.1 + workbox-strategies: 7.4.1 + + workbox-routing@7.4.1: + dependencies: + workbox-core: 7.4.1 + + workbox-strategies@7.4.1: + dependencies: + workbox-core: 7.4.1 + + workbox-streams@7.4.1: + dependencies: + workbox-core: 7.4.1 + workbox-routing: 7.4.1 + + workbox-sw@7.4.1: {} + + workbox-window@7.4.1: + dependencies: + '@types/trusted-types': 2.0.7 + workbox-core: 7.4.1 + + yallist@3.1.1: {} + zimmerframe@1.1.4: {} diff --git a/ui/public/sw.js b/ui/public/sw.js deleted file mode 100644 index af2d52f..0000000 --- a/ui/public/sw.js +++ /dev/null @@ -1,53 +0,0 @@ -// Install-only service worker. -// -// Its sole job today is to satisfy Chromium's PWA installability requirement (a registered -// service worker with a fetch handler) so the web app can be installed to the home screen / -// desktop — notably on Android, where the manifest alone is not enough. It deliberately does -// NOT cache assets or the Connect-RPC stream: only top-level navigations are handled, -// network-first with a cached-shell fallback, so the live app, its immutable hashed assets and -// the live event stream are never served stale. -// -// This is the single designated growth point for the planned offline mode (a Settings toggle, -// default online — see docs/ARCHITECTURE.md). Extend the fetch router here; keep the same -// /app/ scope; gate any real caching behind the toggle and coordinate updates with the boot -// version guard. -const SHELL = 'scrabble-shell-v1'; // bump only when the SW strategy itself changes. - -self.addEventListener('install', () => { - // No cached content to migrate carefully, so activate the new worker immediately. - self.skipWaiting(); -}); - -self.addEventListener('activate', (event) => { - event.waitUntil( - (async () => { - const keys = await caches.keys(); - await Promise.all(keys.filter((k) => k !== SHELL).map((k) => caches.delete(k))); - await self.clients.claim(); - })(), - ); -}); - -self.addEventListener('fetch', (event) => { - const req = event.request; - // Only top-level navigations are handled. Hashed assets and the Connect-RPC requests are - // left untouched (no respondWith -> the browser performs its default fetch), so the worker - // can never break the live stream or serve a stale immutable asset. - if (req.mode !== 'navigate') return; - event.respondWith( - (async () => { - try { - const res = await fetch(req); - const cache = await caches.open(SHELL); - // One shell entry regardless of the deep-link path: the hash router resolves the route - // client-side, so any offline navigation can be served the same cached shell. - cache.put('shell', res.clone()); - return res; - } catch { - const cache = await caches.open(SHELL); - const cached = await cache.match('shell'); - return cached ?? Response.error(); - } - })(), - ); -}); diff --git a/ui/scripts/bundle-size.mjs b/ui/scripts/bundle-size.mjs index 0403740..ab27cb6 100644 --- a/ui/scripts/bundle-size.mjs +++ b/ui/scripts/bundle-size.mjs @@ -20,10 +20,16 @@ import { join } from 'node:path'; const DIST = 'dist'; // Per-chunk gzip budgets in KB. The app entry was raised to 110 for the local move-preview -// wiring, then to 112 for the PWA install feature (the install CTA + the pwa detection / -// service-worker registration live in the app entry; the heavy dict subsystem stays in lazy -// chunks). Its scoped CSS lands in the CSS chunk, not this JS budget. -const BUDGET = { app: 112, shared: 30, landing: 5 }; +// wiring, to 112 for the PWA install feature, to 113 for the offline-mode wiring, to 114 for +// the offline auto-detect (the cold-start reachability check and the "no connection" dialog live in +// the boot path), then to 115 for the vs_ai idle-hint gate — its monotonic clock, lock badge and +// countdown toast live in the always-loaded game screen (Game.svelte) — and to 120 for the offline +// pass-and-play (hotseat) mode: the on-screen PIN pad, the creation roster and the in-game host menu +// live in the always-loaded New Game / Game / Lobby screens (the offline engine and the tiny PIN +// hashing stay in lazy chunks / are negligible). The heavy parts — the dict loader, the move +// generator and the preload orchestration — still stay in lazy chunks. Scoped CSS lands in the CSS +// chunk, not this JS budget. +const BUDGET = { app: 120, shared: 30, landing: 5 }; // gzipped returns the gzipped byte size of a built asset, or 0 when the reference is not a // local file (e.g. the Telegram SDK loaded from a CDN) or is missing. diff --git a/ui/scripts/e2e-dict.mjs b/ui/scripts/e2e-dict.mjs new file mode 100644 index 0000000..9c8ec20 --- /dev/null +++ b/ui/scripts/e2e-dict.mjs @@ -0,0 +1,35 @@ +// Copies the real per-variant dictionary DAWGs into the mock e2e preview output (dist-e2e/e2edict/) +// so the offline spec can create and play a real local vs_ai game (the mock's fetchDict serves these +// files — see lib/mock/client.ts). The dawgs are NEVER committed and never enter the production +// build; they live only in the throwaway dist-e2e/ that `vite preview` serves for Playwright. +// +// Source directory: E2E_DICT_DIR — in CI the fetched scrabble-dictionary release +// ($GITHUB_WORKSPACE/dawg); locally it defaults to the sibling scrabble-solver/dawg checkout. A +// missing source only warns (so the other, dawg-free specs still run); the offline spec then fails +// with an obvious HTTP 404 from fetchDict. + +import { existsSync, mkdirSync, copyFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const srcDir = process.env.E2E_DICT_DIR ?? '../../scrabble-solver/dawg'; +const outDir = 'dist-e2e/e2edict'; + +// The app's Variant enum value -> the release dawg file name (matches the movegen/parity mapping). +const dawgFor = { + scrabble_en: 'en_sowpods', + scrabble_ru: 'ru_scrabble', + erudit_ru: 'ru_erudit', +}; + +mkdirSync(outDir, { recursive: true }); +let copied = 0; +for (const [variant, file] of Object.entries(dawgFor)) { + const src = join(srcDir, `${file}.dawg`); + if (!existsSync(src)) { + console.warn(`e2e-dict: missing ${src} — the offline spec will 404 (set E2E_DICT_DIR)`); + continue; + } + copyFileSync(src, join(outDir, `${variant}.dawg`)); + copied++; +} +console.log(`e2e-dict: copied ${copied}/${Object.keys(dawgFor).length} dawgs from ${srcDir} -> ${outDir}`); diff --git a/ui/src/App.svelte b/ui/src/App.svelte index 39bd17e..734fd81 100644 --- a/ui/src/App.svelte +++ b/ui/src/App.svelte @@ -1,7 +1,7 @@ -