7 Commits

8 changed files with 4260 additions and 116 deletions

View File

@@ -1,92 +0,0 @@
/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// If the loader is already loaded, just stop.
if (!self.define) {
let registry = {};
// Used for `eval` and `importScripts` where we can't get script URL by other means.
// In both cases, it's safe to use a global var because those functions are synchronous.
let nextDefineUri;
const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || (
new Promise(resolve => {
if ("document" in self) {
const script = document.createElement("script");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
} else {
nextDefineUri = uri;
importScripts(uri);
resolve();
}
})
.then(() => {
let promise = registry[uri];
if (!promise) {
throw new Error(`Module ${uri} didnt register its module`);
}
return promise;
})
);
};
self.define = (depsNames, factory) => {
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
if (registry[uri]) {
// Module is already loading or loaded.
return;
}
let exports = {};
const require = depUri => singleRequire(depUri, uri);
const specialDeps = {
module: { uri },
exports,
require
};
registry[uri] = Promise.all(depsNames.map(
depName => specialDeps[depName] || require(depName)
)).then(deps => {
factory(...deps);
return exports;
});
};
}
define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
self.skipWaiting();
workbox.clientsClaim();
/**
* The precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
workbox.precacheAndRoute([{
"url": "registerSW.js",
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
"revision": "0.eh0stsihuc8"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
allowlist: [/^\/$/]
}));
}));

2046
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,8 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest"
},
"dependencies": {
"fireworks-js": "^2.10.8",
@@ -16,7 +17,10 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"@vue/test-utils": "^2.4.6",
"jsdom": "^28.0.0",
"vite": "^5.1.4",
"vite-plugin-pwa": "^0.20.5"
"vite-plugin-pwa": "^0.20.5",
"vitest": "^4.0.18"
}
}

View File

@@ -2,7 +2,7 @@
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { usePuzzleStore } from '@/stores/puzzle';
import { useI18n } from '@/composables/useI18n';
import { Gamepad2, Palette, CircleHelp, Sun, Moon, Menu, X, ChevronDown, ChevronUp } from 'lucide-vue-next';
import { Gamepad2, Palette, CircleHelp, Sun, Moon, Menu, X, ChevronDown, ChevronUp, Monitor } from 'lucide-vue-next';
const store = usePuzzleStore();
const { t, locale, setLocale, locales } = useI18n();
@@ -61,7 +61,59 @@ const langToCountry = {
gl: 'es-ga',
cy: 'gb-wls',
gd: 'gb-sct',
eu: 'es-pv'
eu: 'es-pv',
af: 'za',
am: 'et',
hy: 'am',
az: 'az',
my: 'mm',
km: 'kh',
ceb: 'ph',
fa: 'ir',
gu: 'in',
ht: 'ht',
he: 'il',
ig: 'ng',
ilo: 'ph',
id: 'id',
ja: 'jp',
jv: 'id',
kn: 'in',
kk: 'kz',
rw: 'rw',
rn: 'bi',
ko: 'kr',
ku: 'tr',
ckb: 'iq',
ky: 'kg',
lo: 'la',
ms: 'my',
mr: 'in',
mn: 'mn',
ne: 'np',
om: 'et',
ps: 'af',
pa: 'in',
so: 'so',
sw: 'tz',
tl: 'ph',
ta: 'in',
te: 'in',
th: 'th',
bo: 'cn',
ti: 'er',
uz: 'uz',
vi: 'vn',
wo: 'sn',
yo: 'ng',
'pt-br': 'br',
'pt-pt': 'pt',
'fr-ca': 'ca',
'nl-be': 'be',
'es-es': 'es',
'es-419': 'mx',
'zh-hant': 'tw',
'zh-hans': 'cn'
};
const getFlagClass = (code) => {
@@ -210,15 +262,18 @@ watch(isMobileMenuOpen, (val) => {
<!-- Theme Menu -->
<div class="nav-dropdown">
<button class="btn-neon nav-btn" @click.stop="toggleThemeMenu">
<Palette :size="18" /> Theme
<Palette :size="18" /> {{ t('theme.label') }}
</button>
<transition name="slide-fade">
<div v-if="isThemeOpen" class="dropdown-menu theme-menu">
<button class="dropdown-item" @click="setTheme('system')">
<Monitor :size="16" /> {{ t('theme.system') }}
</button>
<button class="dropdown-item" @click="setTheme('light')">
<Sun :size="16" /> Light
<Sun :size="16" /> {{ t('theme.light') }}
</button>
<button class="dropdown-item" @click="setTheme('dark')">
<Moon :size="16" /> Dark
<Moon :size="16" /> {{ t('theme.dark') }}
</button>
</div>
</transition>
@@ -301,15 +356,18 @@ watch(isMobileMenuOpen, (val) => {
<!-- Mobile Theme Menu -->
<div class="mobile-group">
<button class="mobile-item-trigger" @click="toggleThemeMenu">
<span class="flex-center gap-10"><Palette :size="20" /> Theme</span>
<span class="flex-center gap-10"><Palette :size="20" /> {{ t('theme.label') }}</span>
<component :is="isThemeOpen ? ChevronUp : ChevronDown" :size="16" />
</button>
<div v-if="isThemeOpen" class="mobile-sub-menu">
<button class="mobile-sub-item" @click="setTheme('system')">
<Monitor :size="16" /> {{ t('theme.system') }}
</button>
<button class="mobile-sub-item" @click="setTheme('light')">
<Sun :size="16" /> Light
<Sun :size="16" /> {{ t('theme.light') }}
</button>
<button class="mobile-sub-item" @click="setTheme('dark')">
<Moon :size="16" /> Dark
<Moon :size="16" /> {{ t('theme.dark') }}
</button>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -121,6 +121,11 @@
-webkit-user-select: none;
}
html {
overflow-x: hidden;
width: 100%;
}
body {
margin: 0;
padding: 20px;

View File

@@ -0,0 +1,51 @@
import { describe, it, expect } from 'vitest'
import { calculateHints } from './puzzleUtils'
describe('puzzleUtils', () => {
it('calculateHints correctly calculates hints for a simple grid', () => {
const grid = [
[1, 0, 1],
[1, 1, 1],
[0, 1, 0]
]
// Row 0: 1, then space, then 1 -> [1, 1]
// Row 1: 1, 1, 1 -> [3]
// Row 2: space, 1, space -> [1]
// Col 0: 1, 1, 0 -> [2]
// Col 1: 0, 1, 1 -> [2] ? Wait. Col 1 is 0, 1, 1. So space, 1, 1 -> [2].
// Let's trace col 1 manually:
// r0,c1 = 0
// r1,c1 = 1 -> count=1
// r2,c1 = 1 -> count=2
// end -> push 2.
// So Col 1 is [2].
// Wait, my manual trace above for col 1:
// grid[0][1] is 0.
// grid[1][1] is 1.
// grid[2][1] is 1.
// Yes, [2].
// Col 2: 1, 1, 0 -> [2].
const expected = {
rowHints: [[1, 1], [3], [1]],
colHints: [[2], [2], [2]]
}
expect(calculateHints(grid)).toEqual(expected)
})
it('calculateHints handles empty rows/cols', () => {
const grid = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]
]
const expected = {
rowHints: [[0], [0], [0]],
colHints: [[0], [0], [0]]
}
expect(calculateHints(grid)).toEqual(expected)
})
})

16
vitest.config.js Normal file
View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})