A site megtanul témát váltani, és leszokik az ikonokról
Nyolc akcentszín volt, oldalanként másik hero-gradienssel, tizenegy blur-blobbal és 146 ikonhasználattal — ettől nézett ki réginek. Most egy vizuális nyelv van: semleges zinc-skála, egyetlen akcens (a saját foszforzöldünk), 1px keretek árnyék helyett, és ikon csak ott, ahol nincs mellette szöveg. A választható világos/sötét téma miatt minden szín tokenen keresztül megy. A szerepeket a tokens.css írja le mindkét témára, a Tailwind szemantikus színei (bg-surface, text-fg-muted, border-line, bg-accent) ezekre mutatnak, és a <html data-theme> választ. A sütit az index.html inline scriptje olvassa ki a bundle előtt, különben minden sötét témás betöltés villanna egy világosat. A világos téma zöldje green-700: a green-600 fehéren csak 3,3:1, linkszínnek kevés. A retro mód ebből lett a legtöbbet nyerő: mostantól ő is csak a tokeneket írja felül, és azt szabályozza külön, ami formában más — betűtípus, kettős keret, tui-sáv. 1153 sor és 535 !important helyett 300 sor. Menet közben kiderült, hogy a fejléc- és menüstílusai régóta nem is fogtak: a @apply óta nincsenek a DOM-ban azok a Tailwind-osztályok, amikre épültek. A FontAwesome kikerült (258 KB webfont ~50 glifért), helyette Lucide, meg tíz saját inline márkajel. A csapatképek WebP-ben: 1,6 MB helyett 30 KB darabja. A TuiCSS és a retro.css már csak akkor töltődik be, ha valaki bekapcsolja. A dist 8,2 MB-ról 784 KB-ra, a fő CSS 237 KB-ról 41 KB-ra. Nem a faceliftből: a sanitize.test.ts jsdom alatt fut, és a jsdom függőségfája ESM-only lett — Node 20-on a fájl el sem indult, a tesztjei csendben kimaradtak, az npm run test meg hibakóddal lépett ki. jsdom 26 az utolsó, ami ezen a Node-on megy; a négy teszt így újra fut. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@@ -7,6 +7,18 @@
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<title>Teletype Games</title>
|
||||
<script>
|
||||
/* A témát a bundle betöltése ELŐTT tesszük a gyökérre, különben minden
|
||||
sötét témás oldalbetöltés villan egy világosat. A sütit ezután az
|
||||
ui.store tartja karban; süti nélkül az alapértelmezés a világos. */
|
||||
(function () {
|
||||
var m = document.cookie.match(/(?:^|; )theme=([^;]*)/)
|
||||
var theme = m && decodeURIComponent(m[1]) === 'dark' ? 'dark' : 'light'
|
||||
document.documentElement.setAttribute('data-theme', theme)
|
||||
var r = document.cookie.match(/(?:^|; )retro-mode=([^;]*)/)
|
||||
document.documentElement.setAttribute('data-retro', r && r[1] === '1' ? '1' : '0')
|
||||
})()
|
||||
</script>
|
||||
<link rel="alternate" type="application/rss+xml" title="Teletype Games Blog" href="/api/rss/blog" />
|
||||
<link rel="alternate" type="application/rss+xml" title="Teletype Games Releases" href="/api/rss/releases" />
|
||||
<link rel="alternate" type="application/rss+xml" title="Teletype Games HowTos" href="/api/rss/howtos" />
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"name": "ttglanding",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^7.3.1",
|
||||
"dompurify": "^3.4.12",
|
||||
"lucide-vue-next": "^1.0.0",
|
||||
"pinia": "^2.2.0",
|
||||
"tuicss": "^2.1.2",
|
||||
"vue": "^3.5.0",
|
||||
@@ -25,7 +25,7 @@
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-plugin-vue": "^9.28.0",
|
||||
"happy-dom": "^20.11.1",
|
||||
"jsdom": "^29.1.1",
|
||||
"jsdom": "^26.1.0",
|
||||
"postcss": "^8.4.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.6.0",
|
||||
@@ -49,56 +49,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "5.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
|
||||
"integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
||||
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/generational-cache": "^1.0.1",
|
||||
"@csstools/css-calc": "^3.2.0",
|
||||
"@csstools/css-color-parser": "^4.1.0",
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
"@csstools/css-calc": "^2.1.3",
|
||||
"@csstools/css-color-parser": "^3.0.9",
|
||||
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||
"@csstools/css-tokenizer": "^3.0.3",
|
||||
"lru-cache": "^10.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
|
||||
"integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/generational-cache": "^1.0.1",
|
||||
"@asamuzakjp/nwsapi": "^2.3.9",
|
||||
"bidi-js": "^1.0.3",
|
||||
"css-tree": "^3.2.1",
|
||||
"is-potential-custom-element-name": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/generational-cache": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
|
||||
"integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/nwsapi": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
|
||||
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
@@ -145,23 +108,10 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bramus/specificity": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-tree": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"specificity": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz",
|
||||
"integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -175,13 +125,13 @@
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz",
|
||||
"integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -195,17 +145,17 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz",
|
||||
"integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
||||
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -219,21 +169,21 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^6.1.0",
|
||||
"@csstools/css-calc": "^3.3.0"
|
||||
"@csstools/color-helpers": "^5.1.0",
|
||||
"@csstools/css-calc": "^2.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
|
||||
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -247,41 +197,16 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz",
|
||||
"integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"peerDependencies": {
|
||||
"css-tree": "^3.2.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"css-tree": {
|
||||
"optional": true
|
||||
}
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
|
||||
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -295,7 +220,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
@@ -887,33 +812,6 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@exodus/bytes": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
|
||||
"integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@noble/hashes": "^1.8.0 || ^2.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@noble/hashes": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-free": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.3.1.tgz",
|
||||
"integrity": "sha512-wmglKKPDIkgV3aWlZzWECCPoGIkYCulzBwxG9+w7rc5BGapZ6cPMpoPOT8k36J0Ni7PPX6c/rsoMWfS4d1MUMg==",
|
||||
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||
@@ -2472,6 +2370,16 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
||||
@@ -2627,16 +2535,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -2904,20 +2802,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.27.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@@ -2931,6 +2815,20 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/cssstyle": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
||||
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^3.2.0",
|
||||
"rrweb-cssom": "^0.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@@ -2938,27 +2836,27 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
||||
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^16.0.0"
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls/node_modules/whatwg-mimetype": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
||||
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
@@ -3706,17 +3604,45 @@
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
||||
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.6.0"
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
@@ -3928,36 +3854,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "29.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
|
||||
"integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
|
||||
"version": "26.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^5.1.11",
|
||||
"@asamuzakjp/dom-selector": "^7.1.1",
|
||||
"@bramus/specificity": "^2.4.2",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.3",
|
||||
"@exodus/bytes": "^1.15.0",
|
||||
"css-tree": "^3.2.1",
|
||||
"data-urls": "^7.0.0",
|
||||
"decimal.js": "^10.6.0",
|
||||
"html-encoding-sniffer": "^6.0.0",
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
"decimal.js": "^10.5.0",
|
||||
"html-encoding-sniffer": "^4.0.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.3.5",
|
||||
"parse5": "^8.0.1",
|
||||
"nwsapi": "^2.2.16",
|
||||
"parse5": "^7.2.1",
|
||||
"rrweb-cssom": "^0.8.0",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^6.0.1",
|
||||
"undici": "^7.25.0",
|
||||
"tough-cookie": "^5.1.1",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^8.0.1",
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^16.0.1",
|
||||
"webidl-conversions": "^7.0.0",
|
||||
"whatwg-encoding": "^3.1.1",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.1.1",
|
||||
"ws": "^8.18.0",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.0.0"
|
||||
@@ -3968,14 +3893,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/html-encoding-sniffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/whatwg-mimetype": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
||||
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/xml-name-validator": {
|
||||
@@ -4345,13 +4283,20 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
||||
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/lucide-vue-next": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-1.0.0.tgz",
|
||||
"integrity": "sha512-V6SPvx1IHTj/UY+FrIYWV5faISsPSb8BnWSFDxAtezWKvWc9ZZ40PDrdu1/Qb5vg4lHWr1hs1BAMGVGm6V1Xdg==",
|
||||
"deprecated": "Package deprecated. Please use @lucide/vue instead.",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
@@ -4363,13 +4308,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.27.1",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
||||
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -4517,6 +4455,13 @@
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/nwsapi": {
|
||||
"version": "2.2.24",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
|
||||
"integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -4622,26 +4567,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
|
||||
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^8.0.0"
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5/node_modules/entities": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
|
||||
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
@@ -4698,13 +4643,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry/node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
@@ -5006,16 +4944,6 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.12",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||
@@ -5138,6 +5066,13 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb-cssom": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@@ -5162,6 +5097,13 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
@@ -5574,22 +5516,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.4.10",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz",
|
||||
"integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==",
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.4.10"
|
||||
"tldts-core": "^6.1.86"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.4.10",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz",
|
||||
"integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==",
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -5607,29 +5549,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
|
||||
"integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^7.0.5"
|
||||
"tldts": "^6.1.32"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
|
||||
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
@@ -5722,16 +5664,6 @@
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
|
||||
"integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||
@@ -6734,13 +6666,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
@@ -6754,18 +6700,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
|
||||
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.11.0",
|
||||
"tr46": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1"
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^7.3.1",
|
||||
"dompurify": "^3.4.12",
|
||||
"lucide-vue-next": "^1.0.0",
|
||||
"pinia": "^2.2.0",
|
||||
"tuicss": "^2.1.2",
|
||||
"vue": "^3.5.0",
|
||||
@@ -29,7 +29,7 @@
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-plugin-vue": "^9.28.0",
|
||||
"happy-dom": "^20.11.1",
|
||||
"jsdom": "^29.1.1",
|
||||
"jsdom": "^26.1.0",
|
||||
"postcss": "^8.4.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.6.0",
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 878 KiB |
|
After Width: | Height: | Size: 106 KiB |
@@ -0,0 +1,34 @@
|
||||
<!-- A főoldal három beágyazott blokkja — bolt, YouTube, Spotify — ugyanaz a
|
||||
forma volt három külön osztálykészlettel megírva. Ez az egy komponens
|
||||
váltja ki mindhármat: bal oldalt a címke, jobbra egy kivezető link, alatta
|
||||
a tartalom. -->
|
||||
<template>
|
||||
<section class="media-band">
|
||||
<header class="media-band-header">
|
||||
<h2 class="media-band-label">{{ label }}</h2>
|
||||
<slot name="action" />
|
||||
</header>
|
||||
<div class="media-band-body">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ label: string }>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.media-band {
|
||||
@apply overflow-hidden rounded-card border border-line bg-surface;
|
||||
}
|
||||
.media-band-header {
|
||||
@apply flex flex-wrap items-baseline justify-between gap-3 border-b border-line px-5 py-3 md:px-6;
|
||||
}
|
||||
.media-band-label {
|
||||
@apply font-mono text-xs uppercase tracking-[0.14em] text-fg-subtle;
|
||||
}
|
||||
.media-band-body {
|
||||
@apply p-5 md:p-6;
|
||||
}
|
||||
</style>
|
||||
@@ -1,38 +1,9 @@
|
||||
<template>
|
||||
<span :class="badgeClass"><i :class="iconClass"></i> {{ platform }}</span>
|
||||
<span class="badge">{{ platform }}</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{ platform: string }>()
|
||||
|
||||
const knownPlatforms = ['tic80', 'ebitengine', 'love', 'godot']
|
||||
|
||||
const platformIcons: Record<string, string> = {
|
||||
tic80: 'fa-solid fa-tv',
|
||||
ebitengine: 'fa-brands fa-golang',
|
||||
love: 'fa-solid fa-heart',
|
||||
godot: 'fa-solid fa-robot',
|
||||
other: 'fa-solid fa-cube',
|
||||
}
|
||||
|
||||
const platformKey = computed(() => {
|
||||
const key = props.platform.toLowerCase().replace(/[^a-z0-9]/g, '')
|
||||
return knownPlatforms.includes(key) ? key : 'other'
|
||||
})
|
||||
|
||||
const badgeClass = computed(() => `platform-badge platform-${platformKey.value}`)
|
||||
const iconClass = computed(() => platformIcons[platformKey.value])
|
||||
// A platform neve önmagában elmond mindent, amit egy ikon is elmondana, ezért a
|
||||
// jelölő itt szöveg. Ikon ott marad, ahol nincs mellette szó — a build-mátrixban.
|
||||
defineProps<{ platform: string }>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.platform-badge {
|
||||
@apply px-2 py-0.5 rounded text-xs font-bold uppercase;
|
||||
}
|
||||
.platform-tic80 { @apply bg-cyan-100 text-cyan-800 border border-cyan-200; }
|
||||
.platform-ebitengine { @apply bg-red-100 text-red-800 border border-red-200; }
|
||||
.platform-love { @apply bg-pink-100 text-pink-800 border border-pink-200; }
|
||||
.platform-godot { @apply bg-sky-100 text-sky-800 border border-sky-200; }
|
||||
.platform-other { @apply bg-purple-100 text-purple-800 border border-purple-200; }
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<template>
|
||||
<div class="animate-pulse">
|
||||
<div class="animate-pulse space-y-4">
|
||||
<div v-for="n in count" :key="n" class="skeleton-card">
|
||||
<div class="skeleton-card-inner">
|
||||
<div class="skeleton-line w-1/4 mb-4"></div>
|
||||
<div class="skeleton-line-lg w-3/4 mb-3"></div>
|
||||
<div class="skeleton-line w-full mb-2"></div>
|
||||
<div class="skeleton-line w-5/6"></div>
|
||||
</div>
|
||||
<div class="skeleton-line w-1/4"></div>
|
||||
<div class="skeleton-line-lg w-3/4"></div>
|
||||
<div class="skeleton-line w-full"></div>
|
||||
<div class="skeleton-line w-5/6"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,15 +15,12 @@ withDefaults(defineProps<{ count?: number }>(), { count: 3 })
|
||||
|
||||
<style scoped>
|
||||
.skeleton-card {
|
||||
@apply bg-white rounded-2xl shadow-xl border border-gray-100 overflow-hidden mb-6;
|
||||
}
|
||||
.skeleton-card-inner {
|
||||
@apply p-6 md:p-8;
|
||||
@apply flex flex-col gap-3 rounded-card border border-line bg-surface p-5 md:p-6;
|
||||
}
|
||||
.skeleton-line {
|
||||
@apply h-4 bg-gray-200 rounded;
|
||||
@apply h-4 rounded bg-raised;
|
||||
}
|
||||
.skeleton-line-lg {
|
||||
@apply h-6 bg-gray-200 rounded;
|
||||
@apply h-6 rounded bg-raised;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<!-- Márkajel (Discord, Windows, Linux, …). A Lucide készletében nincsenek
|
||||
brand-ikonok, ezért ez a tíz saját inline SVG. currentColor-ral rajzol,
|
||||
tehát a témát külön beállítás nélkül követi.
|
||||
|
||||
Címke nélkül dekoratív (aria-hidden); ha az ikon önmagában áll szöveg
|
||||
nélkül, adj neki `label`-t. -->
|
||||
<template>
|
||||
<svg
|
||||
:width="size" :height="size" viewBox="0 0 24 24"
|
||||
fill="currentColor" :aria-hidden="label ? undefined : 'true'"
|
||||
:role="label ? 'img' : undefined" :aria-label="label || undefined"
|
||||
class="inline-block shrink-0 align-[-0.125em]"
|
||||
>
|
||||
<path v-for="(d, i) in paths" :key="i" :d="d" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { BRAND_PATHS } from './brandPaths'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
name: string
|
||||
size?: number | string
|
||||
label?: string
|
||||
}>(), { size: 16, label: undefined })
|
||||
|
||||
const paths = computed(() => BRAND_PATHS[props.name] ?? [])
|
||||
</script>
|
||||
@@ -0,0 +1,19 @@
|
||||
// Ikonok a faceliftben: a szöveg mellett álló, díszítő ikonok elmaradnak —
|
||||
// ikon csak akkor kerül a felületre, ha nincs mellette szöveg, vagy ha maga az
|
||||
// ikon az adat (build-mátrix pipa, platformjelölő).
|
||||
//
|
||||
// A készlet a Lucide (`lucide-vue-next`), névre szóló importtal a tree-shake
|
||||
// miatt; a márkajelek a BrandIcon-ban vannak, mert a Lucide-ban nincsenek.
|
||||
//
|
||||
// import { Download } from 'lucide-vue-next'
|
||||
// import { ICON } from '../components/icons'
|
||||
// <Download v-bind="ICON" />
|
||||
//
|
||||
export const ICON = { size: 16, strokeWidth: 1.75 } as const
|
||||
|
||||
// Önállóan álló vezérlők (hamburger, bezárás, téma-kapcsoló).
|
||||
export const ICON_CONTROL = { size: 20, strokeWidth: 1.75 } as const
|
||||
|
||||
export { default as BrandIcon } from './BrandIcon.vue'
|
||||
export { BRAND_PATHS } from './brandPaths'
|
||||
export type { BrandName } from './brandPaths'
|
||||
@@ -11,6 +11,17 @@ export default {
|
||||
retro: 'Retro Mode',
|
||||
exitRetro: 'Exit Retro Mode',
|
||||
rss: 'RSS',
|
||||
stores: 'Stores',
|
||||
builds: 'Builds',
|
||||
theme: 'Switch theme',
|
||||
menu: 'Menu',
|
||||
},
|
||||
footer: {
|
||||
wiki: 'Wiki',
|
||||
git: 'Git repositories',
|
||||
releases: 'Releases',
|
||||
license: 'All content is available under the',
|
||||
tagline: 'Built in the open — we love open source, and the freedom to learn, fork and share.',
|
||||
},
|
||||
common: {
|
||||
status: {
|
||||
@@ -37,26 +48,6 @@ export default {
|
||||
newRelease: 'New Release',
|
||||
watchOnYoutube: 'Watch on YouTube',
|
||||
note: 'Our HowTo-s and blog posts are written by AI with human supervision.',
|
||||
catalog: {
|
||||
title: 'Catalog',
|
||||
desc: 'Here you can find our games. Discover our creations for TIC-80 and other platforms, many of which you can try directly in your browser.',
|
||||
btn: 'Browse Games',
|
||||
},
|
||||
wiki: {
|
||||
title: 'Wiki',
|
||||
desc: 'We gather our public interest development knowledge here. Read through technical descriptions and development logs.',
|
||||
btn: 'Knowledge Base',
|
||||
},
|
||||
git: {
|
||||
title: 'Git Repos',
|
||||
desc: 'All our projects are open source. Browse our code, study our solutions, or even participate in the development.',
|
||||
btn: 'Gitea',
|
||||
},
|
||||
youtube: {
|
||||
title: 'YouTube',
|
||||
desc: 'Watch our development vlogs, gameplay videos, and tutorials on our official YouTube channel.',
|
||||
btn: 'Watch on YouTube',
|
||||
},
|
||||
spotify: {
|
||||
openInSpotify: 'Open in Spotify ↗',
|
||||
},
|
||||
@@ -111,6 +102,8 @@ export default {
|
||||
},
|
||||
},
|
||||
blog: {
|
||||
back: 'Back to the blog',
|
||||
noContent: 'No content available for this post.',
|
||||
badge: 'Community Stories',
|
||||
title: 'Developer Blog',
|
||||
subtitle: 'News, updates, and behind-the-scenes stories from the Teletype Games community.',
|
||||
@@ -139,6 +132,9 @@ export default {
|
||||
},
|
||||
catalogShow: {
|
||||
back: 'Back to Catalog',
|
||||
close: 'Close',
|
||||
prevImage: 'Previous image',
|
||||
nextImage: 'Next image',
|
||||
projectInfo: 'Project Info',
|
||||
author: 'Author',
|
||||
platform: 'Platform',
|
||||
|
||||
@@ -11,6 +11,17 @@ export default {
|
||||
retro: 'Retró mód',
|
||||
exitRetro: 'Retró mód kikapcsolása',
|
||||
rss: 'RSS',
|
||||
stores: 'Boltok',
|
||||
builds: 'Buildek',
|
||||
theme: 'Téma váltása',
|
||||
menu: 'Menü',
|
||||
},
|
||||
footer: {
|
||||
wiki: 'Wiki',
|
||||
git: 'Git tárolók',
|
||||
releases: 'Kiadások',
|
||||
license: 'A tartalom elérhető',
|
||||
tagline: 'Nyíltan fejlesztünk — szeretjük a nyílt forráskódot, és a szabadságot, hogy tanulni, forkolni és megosztani lehessen.',
|
||||
},
|
||||
common: {
|
||||
status: {
|
||||
@@ -37,26 +48,6 @@ export default {
|
||||
newRelease: 'Új megjelenés',
|
||||
watchOnYoutube: 'Nézd meg YouTube-on',
|
||||
note: 'A HowTo-k és blogbejegyzések mesterséges intelligenciával íródtak, emberi felügyelet mellett.',
|
||||
catalog: {
|
||||
title: 'Katalógus',
|
||||
desc: 'Itt megtalálod a játékainkat. Fedezd fel alkotásainkat TIC-80-ra és más platformokra, amelyek közül sokat közvetlenül a böngésződben is kipróbálhatsz.',
|
||||
btn: 'Játékok böngészése',
|
||||
},
|
||||
wiki: {
|
||||
title: 'Wiki',
|
||||
desc: 'Itt gyűjtjük össze a közérdekű fejlesztési tudásunkat. Olvasd át a technikai leírásokat és fejlesztési naplókat.',
|
||||
btn: 'Tudásbázis',
|
||||
},
|
||||
git: {
|
||||
title: 'Git tárolók',
|
||||
desc: 'Minden projektünk nyílt forráskódú. Böngészd a kódunkat, tanulmányozd a megoldásainkat, vagy akár részt is vehetsz a fejlesztésben.',
|
||||
btn: 'Gitea',
|
||||
},
|
||||
youtube: {
|
||||
title: 'YouTube',
|
||||
desc: 'Nézd meg fejlesztési vlogjainkat, gameplay videóinkat és tutorialainkat a hivatalos YouTube-csatornánkon.',
|
||||
btn: 'Nézd meg YouTube-on',
|
||||
},
|
||||
spotify: {
|
||||
openInSpotify: 'Megnyitás Spotify-ban ↗',
|
||||
},
|
||||
@@ -111,6 +102,8 @@ export default {
|
||||
},
|
||||
},
|
||||
blog: {
|
||||
back: 'Vissza a bloghoz',
|
||||
noContent: 'Ehhez a bejegyzéshez nincs tartalom.',
|
||||
badge: 'Közösségi történetek',
|
||||
title: 'Fejlesztői Blog',
|
||||
subtitle: 'Hírek, frissítések és kulisszák mögötti történetek a Teletype Games közösségből.',
|
||||
@@ -139,6 +132,9 @@ export default {
|
||||
},
|
||||
catalogShow: {
|
||||
back: '← Vissza a katalógushoz',
|
||||
close: 'Bezárás',
|
||||
prevImage: 'Előző kép',
|
||||
nextImage: 'Következő kép',
|
||||
projectInfo: 'Projekt infó',
|
||||
author: 'Szerző',
|
||||
platform: 'Platform',
|
||||
|
||||
@@ -1,76 +1,84 @@
|
||||
<template>
|
||||
<div :class="{ 'retro-mode': isRetroMode }" class="app-root">
|
||||
<header class="app-header">
|
||||
<RouterLink to="/" class="app-logo">Teletype Games</RouterLink>
|
||||
<div class="app-header-inner">
|
||||
<RouterLink to="/" class="app-logo">Teletype Games</RouterLink>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<nav class="nav-desktop">
|
||||
<RouterLink to="/" class="nav-link"><i class="fa-solid fa-house nav-icon"></i>{{ t('nav.home') }}</RouterLink>
|
||||
<RouterLink to="/catalog" class="nav-link"><i class="fa-solid fa-gamepad nav-icon"></i>{{ t('nav.catalog') }}</RouterLink>
|
||||
<RouterLink to="/blog" class="nav-link"><i class="fa-solid fa-newspaper nav-icon"></i>{{ t('nav.blog') }}</RouterLink>
|
||||
<RouterLink to="/howtos" class="nav-link"><i class="fa-solid fa-lightbulb nav-icon"></i>{{ t('nav.howtos') }}</RouterLink>
|
||||
<RouterLink to="/engines" class="nav-link"><i class="fa-solid fa-cubes nav-icon"></i>{{ t('nav.engines') }}</RouterLink>
|
||||
<RouterLink to="/code" class="nav-link"><i class="fa-solid fa-code nav-icon"></i>{{ t('nav.code') }}</RouterLink>
|
||||
<RouterLink to="/team" class="nav-link"><i class="fa-solid fa-users nav-icon"></i>{{ t('nav.team') }}</RouterLink>
|
||||
<RouterLink to="/contact" class="nav-link"><i class="fa-solid fa-envelope nav-icon"></i>{{ t('nav.contact') }}</RouterLink>
|
||||
<a v-if="isAdmin" href="/admin" class="nav-link-admin"><i class="fa-solid fa-wrench nav-icon"></i>Admin</a>
|
||||
<!-- Ikonok nélkül: a nyolc menüpont szövege megmondja, hova visz.
|
||||
Az aktív pontot aláhúzás jelöli, nem külön szín. -->
|
||||
<nav class="nav-desktop">
|
||||
<RouterLink v-for="item in navItems" :key="item.to" :to="item.to" class="nav-link">
|
||||
{{ t(item.label) }}
|
||||
</RouterLink>
|
||||
<a v-if="isAdmin" href="/admin" class="nav-link">Admin</a>
|
||||
</nav>
|
||||
|
||||
<div class="locale-switcher">
|
||||
<i class="fa-solid fa-globe text-gray-500 text-xs"></i>
|
||||
<button :class="locale === 'en' ? 'text-white' : 'text-gray-400 hover:text-white'" class="locale-btn" @click="switchLocale('en')">EN</button>
|
||||
<span class="locale-separator">|</span>
|
||||
<button :class="locale === 'hu' ? 'text-white' : 'text-gray-400 hover:text-white'" class="locale-btn" @click="switchLocale('hu')">HU</button>
|
||||
<div class="header-tools">
|
||||
<div class="locale-switcher">
|
||||
<button :class="localeBtnClass('en')" class="locale-btn" @click="switchLocale('en')">EN</button>
|
||||
<span class="locale-separator">/</span>
|
||||
<button :class="localeBtnClass('hu')" class="locale-btn" @click="switchLocale('hu')">HU</button>
|
||||
</div>
|
||||
|
||||
<button class="icon-btn" :aria-label="t('nav.theme')" :title="t('nav.theme')" @click="toggleTheme">
|
||||
<Sun v-if="theme === 'dark'" v-bind="ICON_CONTROL" />
|
||||
<Moon v-else v-bind="ICON_CONTROL" />
|
||||
</button>
|
||||
|
||||
<button class="icon-btn hamburger-btn" :aria-label="t('nav.menu')" :aria-expanded="menuOpen" @click="toggleMenu">
|
||||
<X v-if="menuOpen" v-bind="ICON_CONTROL" />
|
||||
<Menu v-else v-bind="ICON_CONTROL" />
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger Button (Mobile only) -->
|
||||
<button class="hamburger-btn" @click="toggleMenu">
|
||||
<i class="fa-solid fa-bars text-xl"></i>
|
||||
</button>
|
||||
|
||||
<!-- Mobile Navigation -->
|
||||
<nav v-show="menuOpen" class="nav-mobile">
|
||||
<RouterLink to="/" class="nav-mobile-link"><i class="fa-solid fa-house nav-icon"></i>{{ t('nav.home') }}</RouterLink>
|
||||
<RouterLink to="/catalog" class="nav-mobile-link"><i class="fa-solid fa-gamepad nav-icon"></i>{{ t('nav.catalog') }}</RouterLink>
|
||||
<RouterLink to="/blog" class="nav-mobile-link"><i class="fa-solid fa-newspaper nav-icon"></i>{{ t('nav.blog') }}</RouterLink>
|
||||
<RouterLink to="/howtos" class="nav-mobile-link"><i class="fa-solid fa-lightbulb nav-icon"></i>{{ t('nav.howtos') }}</RouterLink>
|
||||
<RouterLink to="/engines" class="nav-mobile-link"><i class="fa-solid fa-cubes nav-icon"></i>{{ t('nav.engines') }}</RouterLink>
|
||||
<RouterLink to="/code" class="nav-mobile-link"><i class="fa-solid fa-code nav-icon"></i>{{ t('nav.code') }}</RouterLink>
|
||||
<RouterLink to="/team" class="nav-mobile-link"><i class="fa-solid fa-users nav-icon"></i>{{ t('nav.team') }}</RouterLink>
|
||||
<RouterLink to="/contact" class="nav-mobile-link"><i class="fa-solid fa-envelope nav-icon"></i>{{ t('nav.contact') }}</RouterLink>
|
||||
<a v-if="isAdmin" href="/admin" class="nav-mobile-link"><i class="fa-solid fa-wrench nav-icon"></i>Admin</a>
|
||||
<div class="locale-switcher-mobile">
|
||||
<button :class="locale === 'en' ? 'text-white' : 'text-gray-400'" class="locale-btn-mobile" @click="switchLocale('en')">EN</button>
|
||||
<span class="locale-separator">|</span>
|
||||
<button :class="locale === 'hu' ? 'text-white' : 'text-gray-400'" class="locale-btn-mobile" @click="switchLocale('hu')">HU</button>
|
||||
</div>
|
||||
<RouterLink v-for="item in navItems" :key="item.to" :to="item.to" class="nav-mobile-link">
|
||||
{{ t(item.label) }}
|
||||
</RouterLink>
|
||||
<a v-if="isAdmin" href="/admin" class="nav-mobile-link">Admin</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<main class="app-main">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<div class="retro-toggle-bar">
|
||||
<div class="retro-toggle-content">
|
||||
<button class="retro-toggle-btn" @click="toggleRetroMode">
|
||||
<i class="fa-solid fa-desktop"></i> {{ isRetroMode ? t('nav.exitRetro') : t('nav.retro') }}
|
||||
</button>
|
||||
<div class="rss-links">
|
||||
<a href="/api/rss/blog" target="_blank" class="rss-link" title="Blog RSS">
|
||||
<i class="fa-solid fa-rss"></i> Blog
|
||||
</a>
|
||||
<span class="locale-separator">|</span>
|
||||
<a href="/api/rss/releases" target="_blank" class="rss-link" title="Releases RSS">
|
||||
<i class="fa-solid fa-rss"></i> Releases
|
||||
</a>
|
||||
<span class="locale-separator">|</span>
|
||||
<a href="/api/rss/howtos" target="_blank" class="rss-link" title="HowTos RSS">
|
||||
<i class="fa-solid fa-rss"></i> HowTos
|
||||
</a>
|
||||
<footer class="app-footer">
|
||||
<div class="footer-inner">
|
||||
<nav class="footer-nav">
|
||||
<RouterLink v-for="item in navItems" :key="item.to" :to="item.to" class="footer-link">
|
||||
{{ t(item.label) }}
|
||||
</RouterLink>
|
||||
<RouterLink to="/stores" class="footer-link">{{ t('nav.stores') }}</RouterLink>
|
||||
<RouterLink to="/builds" class="footer-link">{{ t('nav.builds') }}</RouterLink>
|
||||
</nav>
|
||||
|
||||
<div class="footer-meta">
|
||||
<a :href="WIKI_BASE" target="_blank" rel="noopener noreferrer" class="footer-link">{{ t('footer.wiki') }}</a>
|
||||
<a :href="GIT_BASE" target="_blank" rel="noopener noreferrer" class="footer-link">{{ t('footer.git') }}</a>
|
||||
<span class="footer-rss">
|
||||
<Rss v-bind="ICON" aria-hidden="true" />
|
||||
<a href="/api/rss/blog" target="_blank" class="footer-link">{{ t('nav.blog') }}</a>
|
||||
<a href="/api/rss/releases" target="_blank" class="footer-link">{{ t('footer.releases') }}</a>
|
||||
<a href="/api/rss/howtos" target="_blank" class="footer-link">{{ t('nav.howtos') }}</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="footer-note">{{ t('home.note') }}</p>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p class="footer-license">
|
||||
{{ t('footer.license') }}
|
||||
<a href="https://opensource.org/license/mit" target="_blank" rel="noopener noreferrer" class="footer-link">MIT</a>.
|
||||
{{ t('footer.tagline') }}
|
||||
</p>
|
||||
<button class="retro-toggle-btn" @click="toggleRetroMode">
|
||||
{{ isRetroMode ? t('nav.exitRetro') : t('nav.retro') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -79,14 +87,30 @@ import { computed, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Menu, Moon, Rss, Sun, X } from 'lucide-vue-next'
|
||||
import { useUiStore } from '../stores/ui.store'
|
||||
import { getCookie } from '../lib/cookie'
|
||||
import { ICON, ICON_CONTROL } from '../components/icons'
|
||||
import { WIKI_BASE } from '../api/wiki.api'
|
||||
|
||||
const GIT_BASE = 'https://git.teletypegames.org'
|
||||
|
||||
const navItems = [
|
||||
{ to: '/', label: 'nav.home' },
|
||||
{ to: '/catalog', label: 'nav.catalog' },
|
||||
{ to: '/blog', label: 'nav.blog' },
|
||||
{ to: '/howtos', label: 'nav.howtos' },
|
||||
{ to: '/engines', label: 'nav.engines' },
|
||||
{ to: '/code', label: 'nav.code' },
|
||||
{ to: '/team', label: 'nav.team' },
|
||||
{ to: '/contact', label: 'nav.contact' },
|
||||
]
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const ui = useUiStore()
|
||||
const { isRetroMode, menuOpen } = storeToRefs(ui)
|
||||
const { toggleMenu, toggleRetroMode } = ui
|
||||
const { isRetroMode, menuOpen, theme } = storeToRefs(ui)
|
||||
const { toggleMenu, toggleRetroMode, toggleTheme } = ui
|
||||
const route = useRoute()
|
||||
const isAdmin = computed(() => !!getCookie('is_admin'))
|
||||
|
||||
@@ -98,71 +122,107 @@ function switchLocale(lang: string) {
|
||||
locale.value = lang
|
||||
localStorage.setItem('locale', lang)
|
||||
}
|
||||
|
||||
function localeBtnClass(lang: string) {
|
||||
return locale.value === lang ? 'text-fg' : 'text-fg-subtle hover:text-fg'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-root {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@apply flex min-h-screen flex-col bg-page;
|
||||
}
|
||||
.app-root > main {
|
||||
flex: 1;
|
||||
.app-main {
|
||||
@apply flex-1;
|
||||
}
|
||||
|
||||
/* ---- Fejléc ---- */
|
||||
.app-header {
|
||||
@apply bg-gray-800 text-white p-4 flex justify-between items-center relative;
|
||||
@apply sticky top-0 z-40 border-b border-line bg-page/90 backdrop-blur;
|
||||
}
|
||||
.app-header-inner {
|
||||
@apply mx-auto flex w-full max-w-6xl items-center gap-4 px-4 py-3 md:px-6;
|
||||
}
|
||||
.app-logo {
|
||||
@apply text-xl font-bold;
|
||||
@apply text-sm font-semibold tracking-tight text-fg;
|
||||
}
|
||||
.nav-desktop {
|
||||
@apply hidden lg:flex items-center space-x-1;
|
||||
@apply ml-4 hidden flex-1 items-center gap-1 lg:flex;
|
||||
}
|
||||
.nav-link {
|
||||
@apply px-2.5 py-2 text-sm hover:text-purple-300 transition-colors duration-200 whitespace-nowrap;
|
||||
@apply rounded-control px-2 py-1.5 text-sm text-fg-muted transition-colors hover:text-fg;
|
||||
}
|
||||
.nav-link-admin {
|
||||
@apply px-2.5 py-2 text-sm hover:text-yellow-300 transition-colors duration-200 whitespace-nowrap;
|
||||
.nav-link.router-link-exact-active {
|
||||
@apply text-fg;
|
||||
box-shadow: inset 0 -2px 0 0 rgb(var(--c-accent));
|
||||
}
|
||||
.header-tools {
|
||||
@apply ml-auto flex items-center gap-1;
|
||||
}
|
||||
.locale-switcher {
|
||||
@apply flex items-center gap-1 text-sm font-bold ml-2 border-l border-gray-600 pl-4;
|
||||
@apply flex items-center gap-1 text-xs font-medium;
|
||||
}
|
||||
.locale-btn {
|
||||
@apply transition-colors px-1;
|
||||
@apply px-1 transition-colors;
|
||||
}
|
||||
.locale-separator {
|
||||
@apply text-gray-600;
|
||||
@apply text-fg-subtle;
|
||||
}
|
||||
/* Az ikonok viszik a legtöbb helyet a sok menüpont mellett — desktopon csak
|
||||
extra széles kijelzőn jelennek meg, a mobil menüben mindig. */
|
||||
.nav-desktop .nav-icon {
|
||||
@apply hidden xl:inline-block;
|
||||
}
|
||||
.nav-icon {
|
||||
@apply mr-1.5 text-xs opacity-70;
|
||||
.icon-btn {
|
||||
@apply rounded-control p-2 text-fg-muted transition-colors hover:bg-raised hover:text-fg
|
||||
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent;
|
||||
}
|
||||
.hamburger-btn {
|
||||
@apply lg:hidden p-2 focus:outline-none focus:ring-2 focus:ring-purple-500 rounded-md;
|
||||
@apply lg:hidden;
|
||||
}
|
||||
|
||||
/* ---- Mobil menü ---- */
|
||||
.nav-mobile {
|
||||
@apply lg:hidden absolute top-full left-0 w-full bg-gray-800 flex flex-col items-center py-4 space-y-2 z-50;
|
||||
@apply flex flex-col border-t border-line bg-page px-4 py-2 lg:hidden;
|
||||
}
|
||||
.nav-mobile-link {
|
||||
@apply block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200;
|
||||
@apply border-b border-line py-2.5 text-sm text-fg-muted last:border-b-0 hover:text-fg;
|
||||
}
|
||||
.locale-switcher-mobile {
|
||||
@apply flex items-center gap-2 pt-2 border-t border-gray-700 w-full justify-center text-sm font-bold;
|
||||
.nav-mobile-link.router-link-exact-active {
|
||||
@apply text-accent;
|
||||
}
|
||||
.locale-btn-mobile {
|
||||
@apply transition-colors px-2 py-1;
|
||||
|
||||
/* ---- Lábléc ---- */
|
||||
.app-footer {
|
||||
@apply mt-16 border-t border-line bg-surface;
|
||||
}
|
||||
.retro-toggle-content {
|
||||
@apply flex items-center justify-center gap-4;
|
||||
.footer-inner {
|
||||
@apply mx-auto flex w-full max-w-6xl flex-col gap-4 px-4 py-8 md:px-6;
|
||||
}
|
||||
.rss-links {
|
||||
@apply flex items-center gap-2 text-xs text-gray-400;
|
||||
.footer-nav {
|
||||
@apply flex flex-wrap gap-x-5 gap-y-2;
|
||||
}
|
||||
.rss-link {
|
||||
@apply hover:text-orange-400 transition-colors;
|
||||
.footer-meta {
|
||||
@apply flex flex-wrap items-center gap-x-5 gap-y-2;
|
||||
}
|
||||
.footer-rss {
|
||||
@apply flex items-center gap-2 text-fg-subtle;
|
||||
}
|
||||
.footer-link {
|
||||
@apply text-sm text-fg-muted underline-offset-4 transition-colors hover:text-fg hover:underline;
|
||||
}
|
||||
.footer-note {
|
||||
@apply max-w-[68ch] text-xs italic text-fg-subtle;
|
||||
}
|
||||
.footer-bottom {
|
||||
@apply flex flex-wrap items-center justify-between gap-3 border-t border-line pt-4;
|
||||
}
|
||||
.footer-license {
|
||||
@apply text-xs text-fg-subtle;
|
||||
}
|
||||
.footer-license a {
|
||||
@apply text-xs;
|
||||
}
|
||||
/* A retro-kapcsoló stílusa itt él, nem a retro.css-ben: az a fájl már csak
|
||||
akkor töltődik be, amikor valaki bekapcsolja a retro módot. */
|
||||
.retro-toggle-btn {
|
||||
@apply rounded-control border border-line px-3 py-1.5 text-xs font-medium text-fg-muted
|
||||
transition-colors hover:border-line-strong hover:text-fg
|
||||
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import { i18n } from '../../i18n'
|
||||
import AppLayout from '../AppLayout.vue'
|
||||
|
||||
/**
|
||||
* A váz az egyetlen darab, ami minden oldalon ott van, és a facelift éppen ezt
|
||||
* írta át: ikonok nélküli navigáció, valódi lábléc, téma-kapcsoló. Ezek olyan
|
||||
* dolgok, amiket kézzel könnyű elrontani és nehéz észrevenni — a nyelvváltó
|
||||
* mellől eltűnő kapcsoló nem dob hibát, csak nincs ott.
|
||||
*/
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [{ path: '/:pathMatch(.*)*', component: { template: '<div />' } }],
|
||||
})
|
||||
|
||||
async function mountLayout() {
|
||||
await router.push('/')
|
||||
await router.isReady()
|
||||
return mount(AppLayout, { global: { plugins: [router, i18n] } })
|
||||
}
|
||||
|
||||
describe('AppLayout', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
document.cookie = 'theme=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/'
|
||||
document.cookie = 'retro-mode=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/'
|
||||
})
|
||||
|
||||
it('renders every navigation entry', async () => {
|
||||
const wrapper = await mountLayout()
|
||||
const links = wrapper.findAll('.nav-desktop .nav-link')
|
||||
expect(links).toHaveLength(8)
|
||||
expect(links.map((l) => l.text())).toContain('Catalog')
|
||||
})
|
||||
|
||||
it('carries no FontAwesome markup any more', async () => {
|
||||
const wrapper = await mountLayout()
|
||||
expect(wrapper.html()).not.toMatch(/\bfa-(solid|brands|regular)\b/)
|
||||
})
|
||||
|
||||
it('has a footer with the RSS feeds and the retro switch', async () => {
|
||||
const wrapper = await mountLayout()
|
||||
const footer = wrapper.get('.app-footer')
|
||||
expect(footer.html()).toContain('/api/rss/blog')
|
||||
expect(footer.html()).toContain('/api/rss/releases')
|
||||
expect(footer.html()).toContain('/api/rss/howtos')
|
||||
expect(footer.find('.retro-toggle-btn').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('switches the theme from the header, and writes it to the root element', async () => {
|
||||
const wrapper = await mountLayout()
|
||||
expect(document.documentElement.getAttribute('data-theme')).toBe('light')
|
||||
|
||||
// Az első ikongomb a téma-kapcsoló, a második a hamburger.
|
||||
await wrapper.findAll('.icon-btn')[0].trigger('click')
|
||||
|
||||
expect(document.documentElement.getAttribute('data-theme')).toBe('dark')
|
||||
expect(document.cookie).toContain('theme=dark')
|
||||
})
|
||||
|
||||
it('marks retro mode on the root element too, so the page background follows', async () => {
|
||||
const wrapper = await mountLayout()
|
||||
expect(document.documentElement.getAttribute('data-retro')).toBe('0')
|
||||
|
||||
await wrapper.get('.retro-toggle-btn').trigger('click')
|
||||
|
||||
expect(document.documentElement.getAttribute('data-retro')).toBe('1')
|
||||
expect(wrapper.get('.app-root').classes()).toContain('retro-mode')
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,8 @@
|
||||
/**
|
||||
* jsdom, és nem a projekt alapértelmezett happy-dom-ja: a DOMPurify happy-dom
|
||||
* alatt nem ismeri fel a környezetet, és változatlanul visszaadja a bemenetet —
|
||||
* a teszt így zöld lenne úgy, hogy közben semmit nem tisztít.
|
||||
*
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
@@ -29,3 +29,18 @@ export function filterDevReleases(releases: Release[]): Release[] {
|
||||
export function getLatestStable(releases: Release[]): Release | undefined {
|
||||
return filterStableReleases(releases)[0]
|
||||
}
|
||||
|
||||
// A négy státusz közül csak a megjelent kap kiemelést, a többi semleges jelölő.
|
||||
// Az osztálynevek szándékosan teljes szövegként állnak itt: ha a sablon
|
||||
// `status-${status}`-t interpolálna, a Tailwind nem látná őket, és kiszedné a
|
||||
// bundle-ből.
|
||||
const STATUS_BADGE_CLASS: Record<string, string> = {
|
||||
released: 'status-badge status-released',
|
||||
demo: 'status-badge',
|
||||
development: 'status-badge',
|
||||
archived: 'status-badge',
|
||||
}
|
||||
|
||||
export function statusBadgeClass(status: string): string {
|
||||
return STATUS_BADGE_CLASS[status] ?? 'status-badge'
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ import { createPinia } from 'pinia'
|
||||
import { router } from './router/index.router'
|
||||
import { i18n } from './i18n'
|
||||
import App from './App.vue'
|
||||
import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||
import 'tuicss/dist/tuicss.min.css'
|
||||
import './styles/tokens.css'
|
||||
import './styles/global.css'
|
||||
import './styles/retro.css'
|
||||
|
||||
// A retro mód stílusai (tuicss + retro.css) az ui.store-ból töltődnek be,
|
||||
// akkor és csak akkor, ha valaki bekapcsolja.
|
||||
|
||||
createApp(App).use(createPinia()).use(router).use(i18n).mount('#app')
|
||||
|
||||
@@ -1,70 +1,41 @@
|
||||
<template>
|
||||
<div class="blog-container">
|
||||
<header class="blog-header">
|
||||
<div class="blog-header-decor">
|
||||
<div class="hero-decor-blob -top-24 -left-24 h-96 w-96 bg-purple-600"></div>
|
||||
<div class="hero-decor-blob -bottom-24 -right-24 h-96 w-96 bg-blue-600"></div>
|
||||
</div>
|
||||
|
||||
<header class="hero-section">
|
||||
<div class="hero-container">
|
||||
<div class="hero-badge">{{ t('blog.badge') }}</div>
|
||||
<h1 class="hero-title">
|
||||
Developer <span class="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">Blog</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle mb-10">{{ t('blog.subtitle') }}</p>
|
||||
<p class="hero-badge">{{ t('blog.badge') }}</p>
|
||||
<h1 class="hero-title">{{ t('blog.title') }}</h1>
|
||||
<p class="hero-subtitle">{{ t('blog.subtitle') }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="blog-main">
|
||||
<main class="main-container">
|
||||
<div v-if="error" class="error-banner" role="alert">
|
||||
<i class="fa-solid fa-triangle-exclamation text-2xl text-yellow-500"></i>
|
||||
<div>
|
||||
<h3 class="error-banner-title">{{ t('blog.errorTitle') }}</h3>
|
||||
<p class="error-banner-desc">{{ error }}</p>
|
||||
</div>
|
||||
<h2 class="error-banner-title">{{ t('blog.errorTitle') }}</h2>
|
||||
<p class="error-banner-desc">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loading && blogPages.length === 0" class="empty-state">
|
||||
<div class="empty-state-icon"><i class="fa-solid fa-inbox"></i></div>
|
||||
<h2 class="empty-state-title">{{ t('blog.noPostsTitle') }}</h2>
|
||||
<p class="empty-state-desc">{{ t('blog.noPostsDesc') }}</p>
|
||||
</div>
|
||||
|
||||
<SkeletonCard v-else-if="loading" :count="3" />
|
||||
|
||||
<div v-else class="blog-posts-list">
|
||||
<article v-for="page in blogPages" :key="page.id" class="blog-post-card">
|
||||
<div class="blog-post-content">
|
||||
<div class="blog-post-meta">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="blog-post-timestamp">
|
||||
<span v-if="isNew(page.createdAt)" class="new-post-badge">
|
||||
<span class="new-post-dot"></span>
|
||||
{{ t('blog.newPost') }}
|
||||
</span>
|
||||
<span><i class="fa-solid fa-calendar text-gray-300 mr-1"></i>{{ formatDateTime(page.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="blog-list">
|
||||
<article v-for="page in blogPages" :key="page.id" class="blog-post">
|
||||
<p class="blog-post-meta">
|
||||
<span class="mono">{{ formatDateTime(page.createdAt) }}</span>
|
||||
<span v-if="isNew(page.createdAt)" class="badge-accent">{{ t('blog.newPost') }}</span>
|
||||
</p>
|
||||
|
||||
<h2 class="blog-post-title">
|
||||
<RouterLink :to="getPermalink(page.path)" class="hover:text-indigo-600 transition-colors">
|
||||
{{ page.title }}
|
||||
</RouterLink>
|
||||
</h2>
|
||||
<h2 class="blog-post-title">
|
||||
<RouterLink :to="getPermalink(page.path)">{{ page.title }}</RouterLink>
|
||||
</h2>
|
||||
|
||||
<div class="blog-post-preview">
|
||||
<p v-if="page.description" class="blog-post-description">{{ page.description }}</p>
|
||||
<p v-if="page.content" class="blog-post-body">{{ getCleanPreview(page.content) }}</p>
|
||||
</div>
|
||||
<p v-if="page.description" class="blog-post-description">{{ page.description }}</p>
|
||||
<p v-if="page.content" class="blog-post-body">{{ getCleanPreview(page.content) }}</p>
|
||||
|
||||
<div class="mt-6">
|
||||
<RouterLink :to="getPermalink(page.path)" class="read-more-btn">
|
||||
{{ t('blog.readMore') }}
|
||||
<i class="fa-solid fa-arrow-right text-sm ml-1"></i>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
<RouterLink :to="getPermalink(page.path)" class="link text-sm">{{ t('blog.readMore') }}</RouterLink>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
@@ -91,68 +62,28 @@ onMounted(() => store.fetch())
|
||||
|
||||
<style scoped>
|
||||
.blog-container {
|
||||
@apply bg-gray-50 min-h-screen pb-20;
|
||||
@apply min-h-screen bg-page;
|
||||
}
|
||||
.blog-header {
|
||||
@apply relative overflow-hidden py-24 text-white text-center bg-indigo-900;
|
||||
/* Olvasnivaló, nem kártyafal: egy hasáb, vonallal elválasztott bejegyzésekkel. */
|
||||
.blog-list {
|
||||
@apply flex max-w-[68ch] flex-col;
|
||||
}
|
||||
.blog-header-decor {
|
||||
@apply absolute inset-0 opacity-30;
|
||||
}
|
||||
.blog-main {
|
||||
@apply max-w-5xl mx-auto px-4 md:px-8 -mt-12 relative z-10;
|
||||
}
|
||||
.banner-base {
|
||||
@apply max-w-7xl mx-auto border-l-4 p-6 rounded-r-xl shadow-lg mb-12 flex items-start gap-4;
|
||||
}
|
||||
.error-banner { @apply banner-base bg-red-50 border-red-500; }
|
||||
.error-banner-title { @apply text-red-800 font-bold text-lg; }
|
||||
.error-banner-desc { @apply text-red-700 mt-1; }
|
||||
.empty-state {
|
||||
@apply max-w-7xl mx-auto bg-white rounded-2xl shadow-xl p-12 text-center border border-gray-100;
|
||||
}
|
||||
.empty-state-icon { @apply text-6xl mb-4; }
|
||||
.empty-state-title { @apply text-2xl font-bold text-gray-900 mb-2; }
|
||||
.empty-state-desc { @apply text-gray-500 max-w-md mx-auto; }
|
||||
.blog-posts-list {
|
||||
@apply flex flex-col gap-12;
|
||||
}
|
||||
.blog-post-card {
|
||||
@apply bg-white rounded-2xl shadow-xl border border-gray-100 overflow-hidden;
|
||||
}
|
||||
.blog-post-content {
|
||||
@apply p-6 md:p-10;
|
||||
.blog-post {
|
||||
@apply flex flex-col items-start gap-2 border-b border-line py-8 first:pt-0 last:border-b-0;
|
||||
}
|
||||
.blog-post-meta {
|
||||
@apply flex items-center justify-between mb-4;
|
||||
}
|
||||
.blog-post-timestamp {
|
||||
@apply flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-gray-400;
|
||||
}
|
||||
.new-post-badge {
|
||||
@apply flex items-center gap-1 text-green-600 bg-green-50 px-2 py-0.5 rounded-full border border-green-100;
|
||||
}
|
||||
.new-post-dot {
|
||||
@apply w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse;
|
||||
@apply flex flex-wrap items-center gap-3 text-xs text-fg-subtle;
|
||||
}
|
||||
.blog-post-title {
|
||||
@apply text-2xl md:text-3xl font-black text-gray-900 mb-4 leading-tight;
|
||||
@apply text-xl font-semibold leading-snug tracking-tight text-fg md:text-2xl;
|
||||
}
|
||||
.blog-post-preview {
|
||||
@apply relative overflow-hidden;
|
||||
max-height: 8rem;
|
||||
}
|
||||
.blog-post-preview::after {
|
||||
content: "";
|
||||
@apply absolute bottom-0 left-0 w-full h-16 bg-gradient-to-b from-transparent to-white pointer-events-none;
|
||||
.blog-post-title a {
|
||||
@apply transition-colors hover:text-accent;
|
||||
}
|
||||
.blog-post-description {
|
||||
@apply text-lg text-gray-800 font-bold mb-2 leading-relaxed;
|
||||
@apply text-base leading-relaxed text-fg;
|
||||
}
|
||||
.blog-post-body {
|
||||
@apply text-base text-gray-500 font-medium leading-relaxed;
|
||||
}
|
||||
.read-more-btn {
|
||||
@apply inline-flex items-center text-indigo-600 font-bold hover:text-indigo-800 transition-colors;
|
||||
@apply line-clamp-3 text-sm leading-relaxed text-fg-muted;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,35 +1,25 @@
|
||||
<template>
|
||||
<div class="blog-container">
|
||||
<header class="blog-header">
|
||||
<div class="blog-header-decor">
|
||||
<div class="hero-decor-blob -top-24 -left-24 h-96 w-96 bg-purple-600"></div>
|
||||
<div class="hero-decor-blob -bottom-24 -right-24 h-96 w-96 bg-blue-600"></div>
|
||||
</div>
|
||||
|
||||
<div class="post-hero-container">
|
||||
<RouterLink to="/blog" class="back-link"><i class="fa-solid fa-arrow-left mr-1"></i> Back to Blog</RouterLink>
|
||||
<header class="hero-section">
|
||||
<div class="hero-container">
|
||||
<RouterLink to="/blog" class="link text-sm">{{ t('blog.back') }}</RouterLink>
|
||||
<template v-if="pageContent">
|
||||
<h1 class="hero-title mt-4">{{ pageContent.title }}</h1>
|
||||
<p class="hero-subtitle mb-4">{{ pageContent.description }}</p>
|
||||
<div class="blog-post-timestamp text-indigo-200"><i class="fa-solid fa-calendar mr-1"></i>{{ formatDateTime(pageContent.createdAt) }}</div>
|
||||
<p class="hero-subtitle">{{ pageContent.description }}</p>
|
||||
<p class="post-timestamp mono">{{ formatDateTime(pageContent.createdAt) }}</p>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="blog-main">
|
||||
<main class="main-container">
|
||||
<div v-if="error" class="error-banner" role="alert">
|
||||
<i class="fa-solid fa-triangle-exclamation text-2xl text-yellow-500"></i>
|
||||
<div>
|
||||
<h3 class="error-banner-title">Error</h3>
|
||||
<p class="error-banner-desc">{{ error }}</p>
|
||||
</div>
|
||||
<h2 class="error-banner-title">{{ t('common.error') }}</h2>
|
||||
<p class="error-banner-desc">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<article v-else-if="pageContent" class="blog-post-card">
|
||||
<div class="blog-post-content">
|
||||
<div v-if="pageContent.render" class="wiki-content max-w-none text-gray-700 leading-relaxed" v-html="sanitize(pageContent.render)" />
|
||||
<div v-else class="blog-post-fallback">No content available for this post.</div>
|
||||
</div>
|
||||
<article v-else-if="pageContent">
|
||||
<div v-if="pageContent.render" class="wiki-content" v-html="sanitize(pageContent.render)" />
|
||||
<p v-else class="text-fg-muted">{{ t('blog.noContent') }}</p>
|
||||
</article>
|
||||
|
||||
<SkeletonCard v-else-if="loading" :count="1" />
|
||||
@@ -41,11 +31,13 @@
|
||||
import { onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { formatDateTime } from '../../lib/dateFormat'
|
||||
import { sanitize } from '../../lib/sanitize'
|
||||
import { useBlogStore } from '../../stores/blog.store'
|
||||
import SkeletonCard from '../../components/SkeletonCard.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const store = useBlogStore()
|
||||
const { currentPage: pageContent, pageLoading: loading, pageError: error } = storeToRefs(store)
|
||||
@@ -55,41 +47,9 @@ onMounted(() => store.fetchPage(route.params.slug as string))
|
||||
|
||||
<style scoped>
|
||||
.blog-container {
|
||||
@apply bg-gray-50 min-h-screen pb-20;
|
||||
@apply min-h-screen bg-page;
|
||||
}
|
||||
.blog-header {
|
||||
@apply relative overflow-hidden py-24 text-white text-center bg-indigo-900;
|
||||
.post-timestamp {
|
||||
@apply mt-4 text-xs text-fg-subtle;
|
||||
}
|
||||
.blog-header-decor {
|
||||
@apply absolute inset-0 opacity-30;
|
||||
}
|
||||
.post-hero-container {
|
||||
@apply relative z-10 max-w-4xl mx-auto px-4;
|
||||
}
|
||||
.back-link {
|
||||
@apply inline-block text-indigo-300 hover:text-white transition-colors font-medium;
|
||||
}
|
||||
.hero-title {
|
||||
@apply text-4xl md:text-5xl font-black mb-4 leading-tight;
|
||||
}
|
||||
.hero-subtitle {
|
||||
@apply text-xl text-indigo-100 font-medium;
|
||||
}
|
||||
.blog-post-timestamp {
|
||||
@apply text-sm font-semibold uppercase tracking-wider;
|
||||
}
|
||||
.blog-main {
|
||||
@apply max-w-4xl mx-auto px-4 md:px-8 -mt-12 relative z-10;
|
||||
}
|
||||
.blog-post-card {
|
||||
@apply bg-white rounded-2xl shadow-xl border border-gray-100 overflow-hidden;
|
||||
}
|
||||
.blog-post-content {
|
||||
@apply p-6 md:p-12;
|
||||
}
|
||||
.error-banner {
|
||||
@apply max-w-7xl mx-auto bg-red-50 border-l-4 border-red-500 p-6 rounded-r-xl shadow-lg mb-12 flex items-start gap-4;
|
||||
}
|
||||
.error-banner-title { @apply text-red-800 font-bold text-lg; }
|
||||
.error-banner-desc { @apply text-red-700 mt-1; }
|
||||
</style>
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
<template>
|
||||
<header class="hero-section-gradient from-slate-700 to-gray-800 py-16">
|
||||
<header class="hero-section">
|
||||
<div class="hero-container">
|
||||
<h1 class="hero-title">{{ t('builds.title') }}</h1>
|
||||
<p class="hero-subtitle text-slate-300">{{ t('builds.subtitle') }}</p>
|
||||
<p class="hero-subtitle">{{ t('builds.subtitle') }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container py-12 px-4 mt-0">
|
||||
<div v-if="error" class="builds-error">{{ error }}</div>
|
||||
<main class="main-container">
|
||||
<div v-if="error" class="error-banner">
|
||||
<h2 class="error-banner-title">{{ t('common.error') }}</h2>
|
||||
<p class="error-banner-desc">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<SkeletonCard v-else-if="!data" :count="3" />
|
||||
|
||||
<!-- Desktop table -->
|
||||
<!-- Desktop table — a pipa és az ikssz maga az adat, ezért marad ikon.
|
||||
A csoportokat vonal választja el, nem hat különböző háttérszín. -->
|
||||
<div v-else class="builds-desktop">
|
||||
<div class="builds-table-wrap">
|
||||
<div class="table-wrap">
|
||||
<table class="builds-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2" class="builds-th-engine">Engine</th>
|
||||
<th v-for="group in columnGroups" :key="group.label" :colspan="group.kinds.length" :class="['builds-th-group', group.color]">
|
||||
<i :class="group.icon" class="builds-group-icon"></i>
|
||||
<span class="builds-group-label">{{ group.label }}</span>
|
||||
<th
|
||||
v-for="group in columnGroups" :key="group.label"
|
||||
:colspan="group.kinds.length" class="builds-th-group builds-group-start"
|
||||
>
|
||||
{{ group.label }}
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<template v-for="group in columnGroups" :key="group.label">
|
||||
<th v-for="(col, colIdx) in group.kinds" :key="col.kind" :class="['builds-th-sub', group.color, { 'builds-group-start': colIdx === 0 }]">
|
||||
<i :class="group.icon" class="builds-sub-icon"></i>
|
||||
<th
|
||||
v-for="(col, colIdx) in group.kinds" :key="col.kind"
|
||||
:class="['builds-th-sub', { 'builds-group-start': colIdx === 0 }]"
|
||||
>
|
||||
{{ col.sub }}
|
||||
</th>
|
||||
</template>
|
||||
@@ -34,14 +42,14 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(info, platform) in data.platforms" :key="platform" class="builds-row">
|
||||
<td class="builds-td-engine">
|
||||
<i :class="engineIcons[platform as string] || defaultEngineIcon" class="builds-engine-icon"></i>
|
||||
{{ info.label }}
|
||||
</td>
|
||||
<td class="builds-td-engine">{{ info.label }}</td>
|
||||
<template v-for="group in columnGroups" :key="group.label">
|
||||
<td v-for="(col, colIdx) in group.kinds" :key="col.kind" :class="['builds-td-kind', group.color, { 'builds-group-start': colIdx === 0 }]">
|
||||
<i v-if="info.kinds.includes(col.kind)" class="fa-solid fa-check builds-check"></i>
|
||||
<i v-else class="fa-solid fa-xmark builds-xmark"></i>
|
||||
<td
|
||||
v-for="(col, colIdx) in group.kinds" :key="col.kind"
|
||||
:class="['builds-td-kind', { 'builds-group-start': colIdx === 0 }]"
|
||||
>
|
||||
<Check v-if="info.kinds.includes(col.kind)" class="builds-check" :size="18" :stroke-width="2" />
|
||||
<X v-else class="builds-xmark" :size="16" :stroke-width="1.75" />
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
@@ -53,18 +61,13 @@
|
||||
<!-- Mobile cards -->
|
||||
<div v-if="data" class="builds-mobile">
|
||||
<div v-for="(info, platform) in data.platforms" :key="platform" class="builds-card">
|
||||
<h3 class="builds-card-title">
|
||||
<i :class="engineIcons[platform as string] || defaultEngineIcon" class="builds-engine-icon"></i>
|
||||
{{ info.label }}
|
||||
</h3>
|
||||
<div v-for="group in columnGroups" :key="group.label" :class="['builds-card-group', group.color]">
|
||||
<div class="builds-card-group-header">
|
||||
<i :class="group.icon" class="builds-card-group-icon"></i>
|
||||
<span>{{ group.label }}</span>
|
||||
</div>
|
||||
<h2 class="builds-card-title">{{ info.label }}</h2>
|
||||
<div v-for="group in columnGroups" :key="group.label" class="builds-card-group">
|
||||
<h3 class="builds-card-group-header">{{ group.label }}</h3>
|
||||
<div class="builds-card-kinds">
|
||||
<div v-for="col in group.kinds" :key="col.kind" class="builds-card-kind">
|
||||
<i :class="info.kinds.includes(col.kind) ? 'fa-solid fa-check builds-check' : 'fa-solid fa-xmark builds-xmark'"></i>
|
||||
<Check v-if="info.kinds.includes(col.kind)" class="builds-check" :size="16" :stroke-width="2" />
|
||||
<X v-else class="builds-xmark" :size="14" :stroke-width="1.75" />
|
||||
<span class="builds-card-kind-label">{{ col.sub }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,9 +76,7 @@
|
||||
</div>
|
||||
|
||||
<div class="builds-back">
|
||||
<RouterLink to="/catalog" class="builds-back-link">
|
||||
{{ t('catalogShow.back') }}
|
||||
</RouterLink>
|
||||
<RouterLink to="/catalog" class="link text-sm">{{ t('catalogShow.back') }}</RouterLink>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
@@ -85,6 +86,7 @@ import { onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Check, X } from 'lucide-vue-next'
|
||||
import { useBuildsStore } from '../../stores/builds.store'
|
||||
import SkeletonCard from '../../components/SkeletonCard.vue'
|
||||
|
||||
@@ -93,22 +95,9 @@ const { t } = useI18n()
|
||||
const store = useBuildsStore()
|
||||
const { data, error } = storeToRefs(store)
|
||||
|
||||
const engineIcons: Record<string, string> = {
|
||||
tic80: 'fa-solid fa-tv',
|
||||
ebitengine: 'fa-brands fa-golang',
|
||||
love: 'fa-solid fa-heart',
|
||||
godot: 'fa-solid fa-robot',
|
||||
c64: 'fa-solid fa-floppy-disk',
|
||||
bevy: 'fa-solid fa-cubes',
|
||||
phaser: 'fa-solid fa-bolt',
|
||||
}
|
||||
const defaultEngineIcon = 'fa-solid fa-cube'
|
||||
|
||||
const columnGroups = [
|
||||
{
|
||||
label: 'Windows',
|
||||
icon: 'fa-brands fa-windows',
|
||||
color: 'group-blue',
|
||||
kinds: [
|
||||
{ kind: 'win_x64', sub: '64 bit' },
|
||||
{ kind: 'win_x86', sub: '32 bit' },
|
||||
@@ -116,8 +105,6 @@ const columnGroups = [
|
||||
},
|
||||
{
|
||||
label: 'Linux',
|
||||
icon: 'fa-brands fa-linux',
|
||||
color: 'group-amber',
|
||||
kinds: [
|
||||
{ kind: 'linux_x64', sub: '64 bit' },
|
||||
{ kind: 'linux_arm64', sub: 'ARM 64 bit' },
|
||||
@@ -125,8 +112,6 @@ const columnGroups = [
|
||||
},
|
||||
{
|
||||
label: 'Mac',
|
||||
icon: 'fa-brands fa-apple',
|
||||
color: 'group-slate',
|
||||
kinds: [
|
||||
{ kind: 'mac_x64', sub: 'Intel' },
|
||||
{ kind: 'mac_arm64', sub: 'ARM' },
|
||||
@@ -135,24 +120,18 @@ const columnGroups = [
|
||||
},
|
||||
{
|
||||
label: 'Web',
|
||||
icon: 'fa-solid fa-globe',
|
||||
color: 'group-green',
|
||||
kinds: [
|
||||
{ kind: 'html', sub: 'HTML5' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Cartridge',
|
||||
icon: 'fa-solid fa-gamepad',
|
||||
color: 'group-purple',
|
||||
kinds: [
|
||||
{ kind: 'cartridge', sub: 'Native' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Others',
|
||||
icon: 'fa-solid fa-folder-open',
|
||||
color: 'group-gray',
|
||||
kinds: [
|
||||
{ kind: 'source', sub: 'Source' },
|
||||
{ kind: 'docs', sub: 'Docs' },
|
||||
@@ -164,150 +143,70 @@ onMounted(() => store.fetch())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.builds-error {
|
||||
@apply text-center text-red-500 font-bold py-12;
|
||||
}
|
||||
.builds-loading {
|
||||
@apply text-center text-gray-400 py-12;
|
||||
}
|
||||
.builds-table-wrap {
|
||||
@apply bg-white rounded-2xl shadow-xl border border-gray-100 overflow-x-auto;
|
||||
}
|
||||
.builds-table {
|
||||
@apply w-full text-sm;
|
||||
}
|
||||
.builds-th-engine {
|
||||
@apply px-6 py-4 text-left text-xs font-bold text-gray-400 uppercase tracking-widest bg-gray-50 border-b border-gray-200;
|
||||
}
|
||||
.builds-th-group {
|
||||
@apply px-2 py-3 text-center border-b border-gray-200;
|
||||
}
|
||||
.builds-group-icon {
|
||||
@apply text-base;
|
||||
}
|
||||
.builds-group-label {
|
||||
@apply ml-1.5 text-xs font-bold uppercase tracking-wider;
|
||||
}
|
||||
.builds-th-sub {
|
||||
@apply px-3 py-2 text-center text-[10px] font-bold uppercase tracking-wider;
|
||||
}
|
||||
.builds-sub-icon {
|
||||
@apply text-[9px] mr-0.5 opacity-60;
|
||||
}
|
||||
.builds-group-start {
|
||||
@apply border-l-2;
|
||||
}
|
||||
|
||||
/* Group color themes */
|
||||
.group-blue .builds-group-icon,
|
||||
.group-blue .builds-group-label { @apply text-blue-500; }
|
||||
.th-group.group-blue, th.group-blue { background-color: rgb(239 246 255); }
|
||||
td.group-blue { background-color: rgb(239 246 255 / 0.4); }
|
||||
th.group-blue.builds-group-start, td.group-blue.builds-group-start { @apply border-l-blue-300; }
|
||||
|
||||
.group-amber .builds-group-icon,
|
||||
.group-amber .builds-group-label { @apply text-amber-600; }
|
||||
th.group-amber { background-color: rgb(255 251 235); }
|
||||
td.group-amber { background-color: rgb(255 251 235 / 0.4); }
|
||||
th.group-amber.builds-group-start, td.group-amber.builds-group-start { @apply border-l-amber-300; }
|
||||
|
||||
.group-slate .builds-group-icon,
|
||||
.group-slate .builds-group-label { @apply text-slate-500; }
|
||||
th.group-slate { background-color: rgb(248 250 252); }
|
||||
td.group-slate { background-color: rgb(248 250 252 / 0.4); }
|
||||
th.group-slate.builds-group-start, td.group-slate.builds-group-start { @apply border-l-slate-300; }
|
||||
|
||||
.group-green .builds-group-icon,
|
||||
.group-green .builds-group-label { @apply text-emerald-500; }
|
||||
th.group-green { background-color: rgb(236 253 245); }
|
||||
td.group-green { background-color: rgb(236 253 245 / 0.4); }
|
||||
th.group-green.builds-group-start, td.group-green.builds-group-start { @apply border-l-emerald-300; }
|
||||
|
||||
.group-purple .builds-group-icon,
|
||||
.group-purple .builds-group-label { @apply text-purple-500; }
|
||||
th.group-purple { background-color: rgb(250 245 255); }
|
||||
td.group-purple { background-color: rgb(250 245 255 / 0.4); }
|
||||
th.group-purple.builds-group-start, td.group-purple.builds-group-start { @apply border-l-purple-300; }
|
||||
|
||||
.group-gray .builds-group-icon,
|
||||
.group-gray .builds-group-label { @apply text-gray-500; }
|
||||
th.group-gray { background-color: rgb(249 250 251); }
|
||||
td.group-gray { background-color: rgb(249 250 251 / 0.4); }
|
||||
th.group-gray.builds-group-start, td.group-gray.builds-group-start { @apply border-l-gray-300; }
|
||||
|
||||
/* Sub-header inherits group tint */
|
||||
.builds-th-sub.group-blue { @apply text-blue-400; }
|
||||
.builds-th-sub.group-amber { @apply text-amber-500; }
|
||||
.builds-th-sub.group-slate { @apply text-slate-400; }
|
||||
.builds-th-sub.group-green { @apply text-emerald-400; }
|
||||
.builds-th-sub.group-purple { @apply text-purple-400; }
|
||||
.builds-th-sub.group-gray { @apply text-gray-400; }
|
||||
.builds-row {
|
||||
@apply border-t border-gray-100 transition-colors;
|
||||
}
|
||||
.builds-row:hover td {
|
||||
filter: brightness(0.97);
|
||||
}
|
||||
.builds-td-engine {
|
||||
@apply px-6 py-4 font-bold text-gray-900 whitespace-nowrap;
|
||||
}
|
||||
.builds-engine-icon {
|
||||
@apply text-gray-400 text-sm mr-2;
|
||||
}
|
||||
.builds-td-kind {
|
||||
@apply px-3 py-4 text-center;
|
||||
}
|
||||
.builds-check {
|
||||
@apply text-green-500 text-lg;
|
||||
}
|
||||
.builds-xmark {
|
||||
@apply text-gray-200 text-lg;
|
||||
}
|
||||
/* Responsive visibility */
|
||||
.builds-desktop {
|
||||
@apply hidden md:block;
|
||||
}
|
||||
.builds-mobile {
|
||||
@apply md:hidden space-y-4;
|
||||
.builds-table {
|
||||
@apply w-full border-collapse text-sm;
|
||||
}
|
||||
.builds-th-engine,
|
||||
.builds-th-group,
|
||||
.builds-th-sub {
|
||||
@apply bg-raised px-3 py-2.5 font-mono text-xs font-medium uppercase tracking-[0.1em] text-fg-subtle;
|
||||
}
|
||||
.builds-th-engine {
|
||||
@apply px-5 text-left;
|
||||
}
|
||||
.builds-th-group {
|
||||
@apply text-center text-fg;
|
||||
}
|
||||
.builds-th-sub {
|
||||
@apply text-center text-[10px];
|
||||
}
|
||||
/* A csoportokat egyetlen vonal választja el — a hat háttérszín helyett. */
|
||||
.builds-group-start {
|
||||
@apply border-l border-line;
|
||||
}
|
||||
.builds-row {
|
||||
@apply border-t border-line transition-colors hover:bg-surface;
|
||||
}
|
||||
.builds-td-engine {
|
||||
@apply whitespace-nowrap px-5 py-3 font-medium text-fg;
|
||||
}
|
||||
.builds-td-kind {
|
||||
@apply px-3 py-3 text-center;
|
||||
}
|
||||
.builds-check {
|
||||
@apply mx-auto text-accent;
|
||||
}
|
||||
/* A hiányzó build nem hiba, ezért nem piros. */
|
||||
.builds-xmark {
|
||||
@apply mx-auto text-fg-subtle opacity-50;
|
||||
}
|
||||
|
||||
/* Mobile cards */
|
||||
/* ---- Mobil ---- */
|
||||
.builds-mobile {
|
||||
@apply flex flex-col gap-4 md:hidden;
|
||||
}
|
||||
.builds-card {
|
||||
@apply bg-white rounded-2xl shadow-lg border border-gray-100 p-5;
|
||||
@apply rounded-card border border-line bg-surface p-5;
|
||||
}
|
||||
.builds-card-title {
|
||||
@apply text-lg font-bold text-gray-900 mb-4 pb-3 border-b border-gray-100;
|
||||
@apply mb-3 border-b border-line pb-3 text-base font-semibold text-fg;
|
||||
}
|
||||
.builds-card-group {
|
||||
@apply py-2 px-3 rounded-lg my-1;
|
||||
@apply py-1.5;
|
||||
}
|
||||
.builds-card-group.group-blue { background-color: rgb(239 246 255 / 0.5); }
|
||||
.builds-card-group.group-amber { background-color: rgb(255 251 235 / 0.5); }
|
||||
.builds-card-group.group-slate { background-color: rgb(248 250 252 / 0.5); }
|
||||
.builds-card-group.group-green { background-color: rgb(236 253 245 / 0.5); }
|
||||
.builds-card-group.group-purple { background-color: rgb(250 245 255 / 0.5); }
|
||||
.builds-card-group.group-gray { background-color: rgb(249 250 251 / 0.5); }
|
||||
|
||||
.builds-card-group-header {
|
||||
@apply flex items-center gap-2 text-xs font-bold uppercase tracking-wider mb-2;
|
||||
}
|
||||
.builds-card-group-icon {
|
||||
@apply text-sm;
|
||||
@apply mb-1 font-mono text-xs uppercase tracking-[0.1em] text-fg-subtle;
|
||||
}
|
||||
.builds-card-kinds {
|
||||
@apply flex flex-wrap gap-x-5 gap-y-1 pl-5 mb-2;
|
||||
@apply flex flex-wrap gap-x-4 gap-y-1;
|
||||
}
|
||||
.builds-card-kind {
|
||||
@apply flex items-center gap-1.5 text-sm;
|
||||
@apply flex items-center gap-1.5 text-sm text-fg-muted;
|
||||
}
|
||||
.builds-card-kind-label {
|
||||
@apply text-gray-600;
|
||||
}
|
||||
|
||||
.builds-back {
|
||||
@apply mt-8 text-center;
|
||||
}
|
||||
.builds-back-link {
|
||||
@apply text-gray-400 hover:text-gray-600 text-sm font-medium transition-colors;
|
||||
@apply mt-8;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
<template>
|
||||
<header class="hero-section-gradient from-purple-600 to-indigo-600 py-16">
|
||||
<header class="hero-section">
|
||||
<div class="hero-container">
|
||||
<h1 class="hero-title">{{ t('catalog.title') }}</h1>
|
||||
<!-- Playing a game needs the store, so the way to it belongs where somebody
|
||||
arrives rather than under the list they have to scroll past first. Same
|
||||
shape as the Code and HowTo heroes: subtitle, breathing room, one button. -->
|
||||
<p class="hero-subtitle text-purple-50 mb-10">{{ t('catalog.subtitle') }}</p>
|
||||
<RouterLink to="/stores" class="btn-hero-purple">
|
||||
<i class="fa-solid fa-download mr-2"></i>{{ t('catalog.getTheStore') }}
|
||||
</RouterLink>
|
||||
arrives rather than under the list they have to scroll past first. -->
|
||||
<p class="hero-subtitle">{{ t('catalog.subtitle') }}</p>
|
||||
<div class="hero-actions">
|
||||
<RouterLink to="/stores" class="btn-accent">{{ t('catalog.getTheStore') }}</RouterLink>
|
||||
<RouterLink to="/builds" class="link text-sm">{{ t('builds.title') }}</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container py-12 px-4 mt-0">
|
||||
<div class="filter-buttons">
|
||||
<main class="main-container">
|
||||
<!-- Szegmentált szűrő: egy sáv, egy kiemelt állapottal — nem hat különböző
|
||||
színű pirula. -->
|
||||
<div class="filter-bar" role="tablist">
|
||||
<button
|
||||
v-for="f in filters"
|
||||
:key="f"
|
||||
:class="['filter-btn', { active: activeFilter === f }]"
|
||||
role="tab"
|
||||
:aria-selected="activeFilter === f"
|
||||
@click="activeFilter = f"
|
||||
>
|
||||
{{ t(`catalog.filter.${f}`) }}
|
||||
@@ -26,55 +30,50 @@
|
||||
|
||||
<SkeletonCard v-if="loading" :count="4" />
|
||||
|
||||
<div v-else class="software-list">
|
||||
<div v-else class="software-grid">
|
||||
<template v-for="{ software, releases } in softwares" :key="software.name">
|
||||
<div v-show="activeFilter === 'all' || software.status === activeFilter" class="software-card group">
|
||||
<div class="software-card-content">
|
||||
<div class="software-card-layout">
|
||||
<div v-if="getDefaultImageUrl(software)" class="flex-shrink-0">
|
||||
<img :src="getDefaultImageUrl(software)!" :alt="software.title" class="software-thumb" />
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<RouterLink :to="`/catalog/${software.name}`" class="hover:text-purple-600 transition-colors">
|
||||
<h2 class="software-title mb-0">{{ software.title }}</h2>
|
||||
</RouterLink>
|
||||
<div class="software-badges">
|
||||
<span :class="`status-badge status-${software.status}`">{{ t(`common.status.${software.status}`) }}</span>
|
||||
<PlatformBadge :platform="software.platform" />
|
||||
</div>
|
||||
<p class="software-desc max-w-2xl">{{ software.desc }}</p>
|
||||
<div class="software-meta">
|
||||
<span>{{ t('catalog.author') }} <span class="text-gray-900 font-medium">{{ software.author }}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<article v-show="activeFilter === 'all' || software.status === activeFilter" class="software-card">
|
||||
<RouterLink :to="`/catalog/${software.name}`" class="software-thumb-link">
|
||||
<img
|
||||
v-if="getDefaultImageUrl(software)"
|
||||
:src="getDefaultImageUrl(software)!" :alt="software.title" class="software-thumb" loading="lazy"
|
||||
/>
|
||||
<span v-else class="software-thumb-empty">{{ software.title.slice(0, 1) }}</span>
|
||||
</RouterLink>
|
||||
|
||||
<div class="software-actions">
|
||||
<a
|
||||
v-if="getLatestStable(releases)?.htmlFolderPath"
|
||||
:href="getLatestStable(releases)?.htmlFolderPath"
|
||||
target="_blank"
|
||||
class="btn-play-sm"
|
||||
><i class="fa-solid fa-play mr-1.5 text-xs"></i>{{ t('catalog.play') }}</a>
|
||||
<RouterLink :to="`/catalog/${software.name}`" class="btn-info-sm">
|
||||
<i class="fa-solid fa-circle-info"></i> {{ t('catalog.moreInfo') }}
|
||||
</RouterLink>
|
||||
<span v-if="getTotalDownloads(software.name)" class="software-download-count">
|
||||
<i class="fa-solid fa-download text-[10px]"></i> {{ getTotalDownloads(software.name) }} {{ t('catalog.downloads') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="software-body">
|
||||
<div class="software-badges">
|
||||
<span :class="statusBadgeClass(software.status)">{{ t(`common.status.${software.status}`) }}</span>
|
||||
<PlatformBadge :platform="software.platform" />
|
||||
</div>
|
||||
|
||||
<h2 class="software-title">
|
||||
<RouterLink :to="`/catalog/${software.name}`">{{ software.title }}</RouterLink>
|
||||
</h2>
|
||||
<p class="software-desc">{{ software.desc }}</p>
|
||||
|
||||
<p class="software-meta">
|
||||
{{ t('catalog.author') }} <span class="text-fg">{{ software.author }}</span>
|
||||
<template v-if="getTotalDownloads(software.name)">
|
||||
· <span class="mono">{{ getTotalDownloads(software.name) }}</span> {{ t('catalog.downloads') }}
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<div class="software-actions">
|
||||
<a
|
||||
v-if="getLatestStable(releases)?.htmlFolderPath"
|
||||
:href="getLatestStable(releases)?.htmlFolderPath"
|
||||
target="_blank"
|
||||
class="btn-accent btn-sm"
|
||||
>{{ t('catalog.play') }}</a>
|
||||
<RouterLink :to="`/catalog/${software.name}`" class="btn-ghost btn-sm">
|
||||
{{ t('catalog.moreInfo') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Stores moved up into the hero; the build matrix stays down here, which is
|
||||
where somebody who has already read the catalog goes looking for it. -->
|
||||
<div class="builds-link-row">
|
||||
<RouterLink to="/builds" class="builds-link">
|
||||
<i class="fa-solid fa-table-cells"></i> {{ t('builds.title') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -84,7 +83,7 @@ import { storeToRefs } from 'pinia'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSoftwareStore } from '../../stores/software.store'
|
||||
import { getDefaultImageUrl, getLatestStable } from '../../lib/softwareUtils'
|
||||
import { getDefaultImageUrl, getLatestStable, statusBadgeClass } from '../../lib/softwareUtils'
|
||||
import PlatformBadge from '../../components/PlatformBadge.vue'
|
||||
import SkeletonCard from '../../components/SkeletonCard.vue'
|
||||
import type { SoftwareEntry } from '../../lib/interfaces/software.interface'
|
||||
@@ -115,64 +114,53 @@ onMounted(() => store.fetch())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-buttons {
|
||||
@apply flex flex-wrap gap-4 mb-8 justify-center pb-4;
|
||||
.filter-bar {
|
||||
@apply mb-8 inline-flex flex-wrap gap-1 rounded-control border border-line bg-surface p-1;
|
||||
}
|
||||
.filter-btn {
|
||||
@apply px-4 py-2 rounded-full border-2 border-purple-600 text-purple-600 font-bold transition-all hover:bg-purple-600 hover:text-white;
|
||||
@apply rounded-control px-3 py-1.5 text-sm text-fg-muted transition-colors hover:text-fg
|
||||
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent;
|
||||
}
|
||||
.filter-btn.active {
|
||||
@apply bg-purple-600 text-white;
|
||||
@apply bg-raised text-fg;
|
||||
}
|
||||
.status-badge {
|
||||
@apply px-2 py-0.5 rounded text-xs font-bold uppercase;
|
||||
|
||||
.software-grid {
|
||||
@apply grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3;
|
||||
}
|
||||
.status-released { @apply bg-green-100 text-green-800 border border-green-200; }
|
||||
.status-demo { @apply bg-blue-100 text-blue-800 border border-blue-200; }
|
||||
.status-development { @apply bg-yellow-100 text-yellow-800 border border-yellow-200; }
|
||||
.status-archived { @apply bg-gray-100 text-gray-800 border border-gray-200; }
|
||||
.software-card {
|
||||
@apply bg-white shadow-lg relative rounded-xl overflow-hidden hover:shadow-2xl transition-shadow duration-300;
|
||||
@apply flex flex-col overflow-hidden rounded-card border border-line bg-surface transition-colors hover:border-line-strong;
|
||||
}
|
||||
.software-card-content {
|
||||
@apply p-6;
|
||||
/* A borítók egy része fehér hátterű; a semleges alap és a keret miatt sötét
|
||||
témában sem ütnek lyukat az oldalba. */
|
||||
.software-thumb-link {
|
||||
@apply flex aspect-video items-center justify-center overflow-hidden border-b border-line bg-raised;
|
||||
}
|
||||
.software-title {
|
||||
@apply text-2xl font-bold mb-0;
|
||||
}
|
||||
.software-desc {
|
||||
@apply text-gray-600 mb-3;
|
||||
}
|
||||
.software-meta {
|
||||
@apply text-sm text-gray-500;
|
||||
}
|
||||
.btn-base-sm {
|
||||
@apply inline-flex items-center justify-center font-bold py-2.5 px-5 rounded-xl text-sm transition-all active:scale-95;
|
||||
}
|
||||
.btn-play-sm { @apply btn-base-sm bg-purple-600 hover:bg-purple-700 text-white shadow-lg shadow-purple-600/20; }
|
||||
.btn-info-sm { @apply btn-base-sm bg-gray-100 hover:bg-gray-200 text-gray-700; }
|
||||
.software-thumb {
|
||||
@apply w-24 h-24 object-cover rounded-xl border border-gray-100 shadow-sm;
|
||||
@apply h-full w-full object-contain p-4;
|
||||
}
|
||||
.software-list {
|
||||
@apply grid grid-cols-1 gap-8;
|
||||
.software-thumb-empty {
|
||||
@apply font-mono text-4xl text-fg-subtle;
|
||||
}
|
||||
.software-card-layout {
|
||||
@apply flex flex-col md:flex-row md:items-center justify-between gap-4;
|
||||
.software-body {
|
||||
@apply flex flex-1 flex-col p-5;
|
||||
}
|
||||
.software-badges {
|
||||
@apply flex items-center gap-2 mt-1 mb-2;
|
||||
@apply mb-3 flex flex-wrap items-center gap-2;
|
||||
}
|
||||
.software-title {
|
||||
@apply text-lg font-semibold tracking-tight text-fg;
|
||||
}
|
||||
.software-title a {
|
||||
@apply transition-colors hover:text-accent;
|
||||
}
|
||||
.software-desc {
|
||||
@apply mt-2 line-clamp-3 text-sm leading-relaxed text-fg-muted;
|
||||
}
|
||||
.software-meta {
|
||||
@apply mt-3 text-xs text-fg-subtle;
|
||||
}
|
||||
.software-actions {
|
||||
@apply flex flex-wrap gap-2 items-center;
|
||||
}
|
||||
.software-download-count {
|
||||
@apply text-xs text-gray-400 font-medium ml-1;
|
||||
}
|
||||
.builds-link-row {
|
||||
@apply flex flex-wrap items-center justify-center gap-3 mt-10;
|
||||
}
|
||||
.builds-link {
|
||||
@apply text-sm text-gray-400 hover:text-purple-600 font-medium transition-colors;
|
||||
@apply mt-5 flex flex-wrap items-center gap-2 pt-1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,250 +1,143 @@
|
||||
<template>
|
||||
<div class="catalog-show-page">
|
||||
<header class="hero-section-gradient from-purple-700 to-indigo-800 py-12 md:py-20">
|
||||
<header class="hero-section">
|
||||
<div class="hero-container">
|
||||
<nav class="mb-8">
|
||||
<RouterLink to="/catalog" class="catalog-back-link">
|
||||
<i class="fa-solid fa-arrow-left"></i> {{ t('catalogShow.back') }}
|
||||
</RouterLink>
|
||||
</nav>
|
||||
<RouterLink to="/catalog" class="link text-sm">{{ t('catalogShow.back') }}</RouterLink>
|
||||
|
||||
<div v-if="software" class="catalog-header-content">
|
||||
<div class="flex-grow">
|
||||
<div class="catalog-badges-group">
|
||||
<span :class="`status-badge status-${software.status} px-3 py-1`">{{ t(`common.status.${software.status}`) }}</span>
|
||||
<PlatformBadge :platform="software.platform" />
|
||||
</div>
|
||||
<h1 class="catalog-title">{{ software.title }}</h1>
|
||||
<p class="catalog-subtitle">{{ software.desc }}</p>
|
||||
<template v-if="software">
|
||||
<div class="catalog-badges">
|
||||
<span :class="statusBadgeClass(software.status)">{{ t(`common.status.${software.status}`) }}</span>
|
||||
<PlatformBadge :platform="software.platform" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="hero-title">{{ software.title }}</h1>
|
||||
<p class="hero-subtitle">{{ software.desc }}</p>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main v-if="software" class="main-container -mt-10">
|
||||
<main v-if="software" class="main-container">
|
||||
<div class="catalog-layout">
|
||||
<!-- Sidebar Info -->
|
||||
<div class="catalog-sidebar">
|
||||
<div class="catalog-card">
|
||||
<div v-if="defaultImageUrl" class="mb-6">
|
||||
<div class="flex justify-center">
|
||||
<img
|
||||
:src="defaultImageUrl"
|
||||
:alt="software.title"
|
||||
:class="['catalog-featured-image', softwareImages.length > 0 ? 'cursor-pointer hover:opacity-80 transition-opacity' : '']"
|
||||
@click="openLightbox(defaultImageIndex)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="softwareImages.length > 1" class="catalog-thumbs">
|
||||
<img
|
||||
v-for="(img, idx) in softwareImages"
|
||||
:key="idx"
|
||||
:src="img.url"
|
||||
:alt="`${software.title} ${idx + 1}`"
|
||||
:class="['catalog-thumb', img.isDefault ? 'border-purple-400' : 'border-gray-200 hover:border-purple-300']"
|
||||
@click="openLightbox(idx)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="catalog-section-heading">
|
||||
<i class="fa-solid fa-circle-info text-purple-600"></i> {{ t('catalogShow.projectInfo') }}
|
||||
</h2>
|
||||
<dl class="space-y-4">
|
||||
<div>
|
||||
<dt class="catalog-info-label">{{ t('catalogShow.author') }}</dt>
|
||||
<dd class="catalog-info-value">{{ software.author }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="catalog-info-label">{{ t('catalogShow.platform') }}</dt>
|
||||
<dd><PlatformBadge :platform="software.platform" /></dd>
|
||||
</div>
|
||||
<div v-if="software.license">
|
||||
<dt class="catalog-info-label">{{ t('catalogShow.license') }}</dt>
|
||||
<dd class="catalog-info-value">{{ software.license }}</dd>
|
||||
</div>
|
||||
<div v-if="software.externalLinks?.length">
|
||||
<dt class="catalog-info-label">{{ t('catalogShow.links') }}</dt>
|
||||
<dd class="flex flex-col gap-1.5">
|
||||
<a v-for="link in software.externalLinks" :key="link.url" :href="link.url" target="_blank" class="catalog-external-link">
|
||||
{{ link.label }} <i class="fa-solid fa-up-right-from-square text-xs"></i>
|
||||
<!-- Oldalsáv: kép és tények -->
|
||||
<aside class="catalog-sidebar">
|
||||
<div v-if="defaultImageUrl" class="catalog-image-panel">
|
||||
<img
|
||||
:src="defaultImageUrl"
|
||||
:alt="software.title"
|
||||
class="catalog-featured-image"
|
||||
:class="softwareImages.length > 0 ? 'cursor-pointer' : ''"
|
||||
@click="openLightbox(defaultImageIndex)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="softwareImages.length > 1" class="catalog-thumbs">
|
||||
<img
|
||||
v-for="(img, idx) in softwareImages"
|
||||
:key="idx"
|
||||
:src="img.url"
|
||||
:alt="`${software.title} ${idx + 1}`"
|
||||
class="catalog-thumb"
|
||||
:class="img.isDefault ? 'border-accent' : 'border-line'"
|
||||
@click="openLightbox(idx)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="catalog-section-heading">{{ t('catalogShow.projectInfo') }}</h2>
|
||||
<dl class="catalog-facts">
|
||||
<dt>{{ t('catalogShow.author') }}</dt>
|
||||
<dd>{{ software.author }}</dd>
|
||||
|
||||
<dt>{{ t('catalogShow.platform') }}</dt>
|
||||
<dd>{{ software.platform }}</dd>
|
||||
|
||||
<template v-if="software.license">
|
||||
<dt>{{ t('catalogShow.license') }}</dt>
|
||||
<dd>{{ software.license }}</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="software.externalLinks?.length">
|
||||
<dt>{{ t('catalogShow.links') }}</dt>
|
||||
<dd class="flex flex-col items-start gap-1">
|
||||
<a v-for="link in software.externalLinks" :key="link.url" :href="link.url" target="_blank" rel="noopener" class="link">
|
||||
{{ link.label }}<ExternalLink v-bind="ICON" class="ml-1 inline" />
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="software.platformLinks?.length">
|
||||
<dt class="catalog-info-label">{{ t('catalogShow.platformLinks') }}</dt>
|
||||
<dd class="flex flex-col gap-1.5">
|
||||
<a v-for="pl in software.platformLinks" :key="pl.url" :href="pl.url" target="_blank" class="catalog-external-link">
|
||||
{{ pl.name }} <i class="fa-solid fa-up-right-from-square text-xs"></i>
|
||||
</template>
|
||||
|
||||
<template v-if="software.platformLinks?.length">
|
||||
<dt>{{ t('catalogShow.platformLinks') }}</dt>
|
||||
<dd class="flex flex-col items-start gap-1">
|
||||
<a v-for="pl in software.platformLinks" :key="pl.url" :href="pl.url" target="_blank" rel="noopener" class="link">
|
||||
{{ pl.name }}<ExternalLink v-bind="ICON" class="ml-1 inline" />
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
</template>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<!-- Fő oszlop -->
|
||||
<div class="catalog-main">
|
||||
<div v-if="software.story" class="catalog-card">
|
||||
<h2 class="catalog-about-title">{{ t('catalogShow.about') }}</h2>
|
||||
<section v-if="software.story" class="card">
|
||||
<h2 class="catalog-section-heading">{{ t('catalogShow.about') }}</h2>
|
||||
<p class="catalog-about-text">{{ software.story }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="latestStable" class="latest-release-card">
|
||||
<div class="latest-release-header">
|
||||
<section v-if="latestStable" class="card">
|
||||
<div class="release-headline">
|
||||
<div>
|
||||
<div class="latest-release-label-row">
|
||||
<span class="latest-release-badge">{{ t('catalogShow.latestStable') }}</span>
|
||||
</div>
|
||||
<h2 class="latest-release-version">{{ latestStable.version }}</h2>
|
||||
<p class="latest-release-date"><i class="fa-solid fa-calendar mr-1"></i>{{ t('catalogShow.released') }} {{ formatDateTime(latestStable.updatedAt) }}</p>
|
||||
<span class="badge-accent">{{ t('catalogShow.latestStable') }}</span>
|
||||
<h2 class="release-version-lead mono">{{ latestStable.version }}</h2>
|
||||
<p class="release-date">{{ t('catalogShow.released') }} {{ formatDateTime(latestStable.updatedAt) }}</p>
|
||||
</div>
|
||||
|
||||
<a v-if="latestStable.htmlFolderPath" :href="latestStable.htmlFolderPath" target="_blank" class="latest-release-play-btn"><i class="fa-solid fa-play mr-2"></i>{{ t('catalogShow.playNow') }}</a>
|
||||
<a v-if="latestStable.htmlFolderPath" :href="latestStable.htmlFolderPath" target="_blank" class="btn-accent">
|
||||
{{ t('catalogShow.playNow') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="tableAssets(latestStable).length" class="latest-release-assets">
|
||||
<div class="assets-section-label">{{ t('catalogShow.assetsTitle') }}</div>
|
||||
<!-- Desktop asset table -->
|
||||
<table class="asset-table-desktop">
|
||||
<tbody>
|
||||
<tr v-for="asset in tableAssets(latestStable)" :key="asset.kind" class="asset-table-row-light">
|
||||
<td class="asset-kind-col-light"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</td>
|
||||
<td class="asset-path-col-light">{{ assetFileName(asset.path) }}</td>
|
||||
<td class="asset-action-col">
|
||||
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="asset-link-white">{{ t('catalogShow.assetOpen') }}</a>
|
||||
<a v-else :href="downloadUrl(asset.path)" class="asset-link-white">{{ t('catalogShow.assetDownload') }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Mobile asset list -->
|
||||
<div class="asset-mobile-list">
|
||||
<div v-for="asset in tableAssets(latestStable)" :key="asset.kind" class="asset-mobile-item-light">
|
||||
<div class="asset-mobile-row">
|
||||
<span class="asset-mobile-kind-light"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</span>
|
||||
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="asset-mobile-link-white">{{ t('catalogShow.assetOpen') }}</a>
|
||||
<a v-else :href="downloadUrl(asset.path)" class="asset-mobile-link-white">{{ t('catalogShow.assetDownload') }}</a>
|
||||
</div>
|
||||
<div class="asset-mobile-path-light">{{ assetFileName(asset.path) }}</div>
|
||||
<div v-if="tableAssets(latestStable).length" class="asset-list">
|
||||
<h3 class="asset-list-title">{{ t('catalogShow.assetsTitle') }}</h3>
|
||||
<div v-for="asset in tableAssets(latestStable)" :key="asset.kind" class="asset-row">
|
||||
<span class="asset-kind">{{ t('catalogShow.assetKind.' + asset.kind) }}</span>
|
||||
<span class="asset-path mono">{{ assetFileName(asset.path) }}</span>
|
||||
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="link text-sm">{{ t('catalogShow.assetOpen') }}</a>
|
||||
<a v-else :href="downloadUrl(asset.path)" class="link text-sm">{{ t('catalogShow.assetDownload') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Egy lista minden kiadásnak; a fejlesztői verziók jelölve. Korábban
|
||||
ugyanez két külön táblázatban élt, asztali és mobil változatban. -->
|
||||
<section class="card-bare">
|
||||
<h2 class="releases-title">{{ t('catalogShow.allReleases') }}</h2>
|
||||
<article v-for="release in allReleases" :key="release.version" class="release-item">
|
||||
<div class="release-item-head">
|
||||
<span class="release-version mono">{{ release.version }}</span>
|
||||
<span v-if="release.version.startsWith('dev-')" class="badge">dev</span>
|
||||
<span v-if="release.downloadCount" class="release-downloads mono">
|
||||
{{ release.downloadCount }} {{ t('catalogShow.downloads') }}
|
||||
</span>
|
||||
<span class="release-date mono ml-auto">{{ formatDateTime(release.updatedAt) }}</span>
|
||||
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="link text-sm">
|
||||
{{ t('catalogShow.play') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="tableAssets(release).length" class="asset-list">
|
||||
<div v-for="asset in tableAssets(release)" :key="asset.kind" class="asset-row">
|
||||
<span class="asset-kind">{{ t('catalogShow.assetKind.' + asset.kind) }}</span>
|
||||
<span class="asset-path mono">{{ assetFileName(asset.path) }}</span>
|
||||
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="link text-sm">{{ t('catalogShow.assetOpen') }}</a>
|
||||
<a v-else :href="downloadUrl(asset.path)" class="link text-sm">{{ t('catalogShow.assetDownload') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="releases-card">
|
||||
<div class="releases-header">
|
||||
<h2 class="releases-title">{{ t('catalogShow.allReleases') }}</h2>
|
||||
</div>
|
||||
<div class="p-0">
|
||||
<!-- Desktop Table -->
|
||||
<div class="hidden md:block">
|
||||
<table class="w-full text-left">
|
||||
<thead class="release-table-head">
|
||||
<tr>
|
||||
<th class="px-8 py-4">{{ t('catalogShow.version') }}</th>
|
||||
<th class="px-8 py-4">{{ t('catalogShow.releaseDate') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="release in stableReleases" :key="release.version">
|
||||
<tr class="release-table-row">
|
||||
<td class="px-8 py-4">
|
||||
<div class="release-version-row">
|
||||
<span class="release-version">{{ release.version }}</span>
|
||||
<span v-if="release.downloadCount" class="release-download-count"><i class="fa-solid fa-download text-[10px]"></i> {{ release.downloadCount }}</span>
|
||||
</div>
|
||||
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="release-play-link">{{ t('catalogShow.play') }}</a>
|
||||
</td>
|
||||
<td class="release-date-col">{{ formatDateTime(release.updatedAt) }}</td>
|
||||
</tr>
|
||||
<tr v-if="tableAssets(release).length">
|
||||
<td colspan="2" class="px-8 pb-4 pt-0">
|
||||
<table class="w-full text-sm">
|
||||
<tbody>
|
||||
<tr v-for="asset in tableAssets(release)" :key="asset.kind" class="release-asset-row">
|
||||
<td class="release-asset-kind"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</td>
|
||||
<td class="release-asset-path">{{ assetFileName(asset.path) }}</td>
|
||||
<td class="release-asset-action">
|
||||
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="release-asset-link">{{ t('catalogShow.assetOpen') }}</a>
|
||||
<a v-else :href="downloadUrl(asset.path)" class="release-asset-link">{{ t('catalogShow.assetDownload') }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<tr v-if="devReleases.length > 0" class="dev-versions-header">
|
||||
<td colspan="2" class="dev-versions-label">{{ t('catalogShow.devVersions') }}</td>
|
||||
</tr>
|
||||
|
||||
<template v-for="release in devReleases" :key="release.version">
|
||||
<tr class="dev-release-row">
|
||||
<td class="px-8 py-4">
|
||||
<div class="dev-release-version">{{ release.version }}</div>
|
||||
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="release-play-link">{{ t('catalogShow.play') }}</a>
|
||||
</td>
|
||||
<td class="release-date-col">{{ formatDateTime(release.updatedAt) }}</td>
|
||||
</tr>
|
||||
<tr v-if="tableAssets(release).length" class="bg-yellow-50/10">
|
||||
<td colspan="2" class="px-8 pb-4 pt-0">
|
||||
<table class="w-full text-sm">
|
||||
<tbody>
|
||||
<tr v-for="asset in tableAssets(release)" :key="asset.kind" class="release-asset-row">
|
||||
<td class="release-asset-kind"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</td>
|
||||
<td class="release-asset-path">{{ assetFileName(asset.path) }}</td>
|
||||
<td class="release-asset-action">
|
||||
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="release-asset-link">{{ t('catalogShow.assetOpen') }}</a>
|
||||
<a v-else :href="downloadUrl(asset.path)" class="release-asset-link">{{ t('catalogShow.assetDownload') }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mobile View -->
|
||||
<div class="md:hidden divide-y divide-gray-100">
|
||||
<div
|
||||
v-for="release in allReleases"
|
||||
:key="release.version"
|
||||
:class="['p-6', release.version.startsWith('dev-') ? 'bg-yellow-50/20' : '']"
|
||||
>
|
||||
<div class="mobile-release-header">
|
||||
<div>
|
||||
<div class="mobile-release-version">{{ release.version }}</div>
|
||||
<div class="mobile-release-date">{{ formatDateTime(release.updatedAt) }}</div>
|
||||
</div>
|
||||
<span v-if="release.version.startsWith('dev-')" class="dev-badge">Dev</span>
|
||||
</div>
|
||||
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="mobile-play-btn">{{ t('catalogShow.play') }}</a>
|
||||
<div v-if="tableAssets(release).length" class="mt-3 space-y-1">
|
||||
<div v-for="asset in tableAssets(release)" :key="asset.kind" class="asset-mobile-item">
|
||||
<div class="asset-mobile-row">
|
||||
<span class="asset-mobile-kind"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</span>
|
||||
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="asset-mobile-link">{{ t('catalogShow.assetOpen') }}</a>
|
||||
<a v-else :href="downloadUrl(asset.path)" class="asset-mobile-link">{{ t('catalogShow.assetDownload') }}</a>
|
||||
</div>
|
||||
<div class="asset-mobile-path">{{ assetFileName(asset.path) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<main v-else class="main-container -mt-10">
|
||||
<div class="loading-card">
|
||||
{{ error || t('catalogShow.loading') }}
|
||||
</div>
|
||||
<main v-else class="main-container">
|
||||
<p class="text-fg-muted">{{ error || t('catalogShow.loading') }}</p>
|
||||
</main>
|
||||
|
||||
<ImageLightbox
|
||||
@@ -262,10 +155,12 @@ import { RouterLink, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { formatDateTime } from '../../lib/dateFormat'
|
||||
import { useSoftwareStore } from '../../stores/software.store'
|
||||
import { filterStableReleases, filterDevReleases } from '../../lib/softwareUtils'
|
||||
import { filterStableReleases, filterDevReleases, statusBadgeClass } from '../../lib/softwareUtils'
|
||||
import type { Release, Software, SoftwareImage } from '../../lib/interfaces/software.interface'
|
||||
import { ExternalLink } from 'lucide-vue-next'
|
||||
import ImageLightbox from './ImageLightbox.vue'
|
||||
import PlatformBadge from '../../components/PlatformBadge.vue'
|
||||
import { ICON } from '../../components/icons'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
@@ -325,15 +220,6 @@ const tableAssets = (release: Release) =>
|
||||
|
||||
const assetFileName = (path: string) => path.replace(/\/+$/, '').split('/').pop() ?? ''
|
||||
|
||||
const assetKindIcon = (kind: string) =>
|
||||
kind.startsWith('win_') ? 'fa-brands fa-windows'
|
||||
: kind.startsWith('linux_') ? 'fa-brands fa-linux'
|
||||
: kind.startsWith('mac_') ? 'fa-brands fa-apple'
|
||||
: kind === 'cartridge' ? 'fa-solid fa-gamepad'
|
||||
: kind === 'source' ? 'fa-solid fa-code'
|
||||
: kind === 'docs' ? 'fa-solid fa-book'
|
||||
: ''
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await store.fetch()
|
||||
@@ -351,105 +237,90 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Page */
|
||||
.catalog-show-page { @apply bg-gray-50 min-h-screen pb-20; }
|
||||
.catalog-back-link { @apply text-purple-200 hover:text-white transition-colors flex items-center gap-2 font-medium; }
|
||||
.catalog-header-content { @apply flex flex-col md:flex-row md:items-end justify-between gap-6; }
|
||||
.catalog-badges-group { @apply flex items-center justify-center gap-3 mb-4; }
|
||||
.catalog-title { @apply text-4xl md:text-6xl font-black text-white mb-4 leading-tight; }
|
||||
.catalog-subtitle { @apply text-xl text-purple-100 max-w-3xl mx-auto leading-relaxed; }
|
||||
.catalog-show-page {
|
||||
@apply min-h-screen bg-page;
|
||||
}
|
||||
.catalog-badges {
|
||||
@apply mb-3 mt-6 flex flex-wrap items-center gap-2;
|
||||
}
|
||||
.catalog-layout {
|
||||
@apply grid grid-cols-1 gap-6 lg:grid-cols-[320px_minmax(0,1fr)];
|
||||
}
|
||||
.catalog-sidebar {
|
||||
@apply flex flex-col gap-4;
|
||||
}
|
||||
.catalog-main {
|
||||
@apply flex flex-col gap-6;
|
||||
}
|
||||
/* A borítók egy része fehér hátterű, ezért semleges alap és keret kerül alájuk. */
|
||||
.catalog-image-panel {
|
||||
@apply flex items-center justify-center rounded-card border border-line bg-raised p-6;
|
||||
}
|
||||
.catalog-featured-image {
|
||||
@apply max-h-64 w-auto object-contain transition-opacity hover:opacity-80;
|
||||
}
|
||||
.catalog-thumbs {
|
||||
@apply flex flex-wrap gap-2;
|
||||
}
|
||||
.catalog-thumb {
|
||||
@apply h-14 w-14 cursor-pointer rounded-control border object-cover transition-colors hover:border-accent;
|
||||
}
|
||||
.catalog-section-heading {
|
||||
@apply mb-4 text-base font-semibold tracking-tight text-fg;
|
||||
}
|
||||
.catalog-facts {
|
||||
@apply grid grid-cols-1 gap-y-3 text-sm;
|
||||
}
|
||||
.catalog-facts dt {
|
||||
@apply text-xs uppercase tracking-wide text-fg-subtle;
|
||||
}
|
||||
.catalog-facts dd {
|
||||
@apply m-0 mb-2 text-fg;
|
||||
}
|
||||
.catalog-about-text {
|
||||
@apply max-w-[68ch] whitespace-pre-line leading-relaxed text-fg-muted;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.catalog-layout { @apply grid grid-cols-1 lg:grid-cols-3 gap-8; }
|
||||
.catalog-sidebar { @apply lg:col-span-1 space-y-6; }
|
||||
.catalog-main { @apply lg:col-span-2 space-y-8; }
|
||||
.catalog-card { @apply bg-white rounded-3xl shadow-xl p-8 border border-gray-100; }
|
||||
/* ---- Kiadások ---- */
|
||||
.release-headline {
|
||||
@apply flex flex-wrap items-start justify-between gap-4;
|
||||
}
|
||||
.release-version-lead {
|
||||
@apply mt-2 text-2xl font-semibold text-fg;
|
||||
}
|
||||
.release-date {
|
||||
@apply text-sm text-fg-subtle;
|
||||
}
|
||||
.releases-title {
|
||||
@apply border-b border-line px-5 py-3 text-base font-semibold tracking-tight text-fg md:px-6;
|
||||
}
|
||||
.release-item {
|
||||
@apply border-b border-line px-5 py-4 last:border-b-0 md:px-6;
|
||||
}
|
||||
.release-item-head {
|
||||
@apply flex flex-wrap items-center gap-3;
|
||||
}
|
||||
.release-version {
|
||||
@apply text-sm font-medium text-fg;
|
||||
}
|
||||
.release-downloads {
|
||||
@apply text-xs text-fg-subtle;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.catalog-featured-image { @apply max-h-48 w-auto object-contain border-4 border-gray-100 shadow-sm; }
|
||||
.catalog-thumbs { @apply flex gap-2 mt-3 justify-center; }
|
||||
.catalog-thumb { @apply w-12 h-12 object-cover rounded border-2 cursor-pointer transition-all; }
|
||||
.catalog-section-heading { @apply text-xl font-bold text-gray-900 mb-6 flex items-center gap-2; }
|
||||
.catalog-info-label { @apply text-xs font-bold text-gray-400 uppercase tracking-widest mb-1; }
|
||||
.catalog-info-value { @apply text-gray-900 font-medium; }
|
||||
.catalog-external-link { @apply text-purple-600 hover:text-purple-800 font-bold inline-flex items-center gap-1; }
|
||||
|
||||
/* About */
|
||||
.catalog-about-title { @apply text-xl font-bold text-gray-900 mb-4; }
|
||||
.catalog-about-text { @apply text-gray-700 leading-relaxed whitespace-pre-line; }
|
||||
|
||||
/* Latest Release */
|
||||
.latest-release-card { @apply bg-gradient-to-br from-indigo-600 to-purple-700 rounded-3xl shadow-xl p-8 text-white; }
|
||||
.latest-release-header { @apply flex flex-col md:flex-row justify-between items-start md:items-center gap-6; }
|
||||
.latest-release-label-row { @apply flex items-center gap-2 mb-2; }
|
||||
.latest-release-badge { @apply px-2 py-0.5 bg-white/20 text-white text-[10px] font-black uppercase rounded tracking-widest; }
|
||||
.latest-release-version { @apply text-4xl font-black mb-1; }
|
||||
.latest-release-date { @apply text-indigo-100 text-sm; }
|
||||
.latest-release-play-btn { @apply flex items-center justify-center py-3 px-6 bg-white text-indigo-900 font-bold rounded-xl transition-all hover:bg-indigo-50 shadow-lg shadow-black/10 text-sm whitespace-nowrap; }
|
||||
.latest-release-assets { @apply mt-6 pt-6 border-t border-white/15; }
|
||||
.assets-section-label { @apply text-[10px] font-black uppercase tracking-widest text-indigo-200 mb-3; }
|
||||
|
||||
/* Asset table - light (hero) */
|
||||
.asset-table-desktop { @apply hidden md:table w-full text-sm; }
|
||||
.asset-table-row-light { @apply border-b border-white/10 last:border-0; }
|
||||
.asset-kind-col-light { @apply py-1.5 pr-6 text-indigo-100 whitespace-nowrap; }
|
||||
.asset-path-col-light { @apply py-1.5 pr-6 w-full font-mono text-xs text-indigo-200/80 break-all; }
|
||||
.asset-action-col { @apply py-1.5 text-right whitespace-nowrap; }
|
||||
.asset-link-white { @apply text-white font-bold hover:underline; }
|
||||
|
||||
/* Asset mobile - light (hero) */
|
||||
.asset-mobile-list { @apply md:hidden space-y-2; }
|
||||
.asset-mobile-item-light { @apply flex flex-col gap-0.5 py-2 border-b border-white/10 last:border-0; }
|
||||
.asset-mobile-row { @apply flex justify-between items-center; }
|
||||
.asset-mobile-kind-light { @apply text-indigo-100 text-sm whitespace-nowrap; }
|
||||
.asset-mobile-link-white { @apply text-white font-bold text-sm hover:underline ml-4 shrink-0; }
|
||||
.asset-mobile-path-light { @apply font-mono text-xs text-indigo-200/80 truncate; }
|
||||
|
||||
/* Releases section */
|
||||
.releases-card { @apply bg-white rounded-3xl shadow-xl overflow-hidden border border-gray-100; }
|
||||
.releases-header { @apply p-8 border-b border-gray-100; }
|
||||
.releases-title { @apply text-2xl font-bold text-gray-900; }
|
||||
.release-table-head { @apply bg-gray-50 text-gray-500 text-xs font-bold uppercase tracking-widest; }
|
||||
.release-table-row { @apply border-t border-gray-100 first:border-t-0 hover:bg-gray-50/50 transition-colors; }
|
||||
.release-version-row { @apply flex items-center gap-2 mb-2; }
|
||||
.release-version { @apply font-bold text-gray-900 text-lg; }
|
||||
.release-download-count { @apply text-xs text-gray-400 font-medium; }
|
||||
.release-play-link { @apply text-purple-600 hover:text-purple-900 font-bold text-sm; }
|
||||
.release-date-col { @apply px-8 py-4 text-gray-500 align-top pt-5; }
|
||||
|
||||
/* Release asset table */
|
||||
.release-asset-row { @apply border-b border-gray-100 last:border-0; }
|
||||
.release-asset-kind { @apply py-1 pr-6 text-gray-500 whitespace-nowrap; }
|
||||
.release-asset-path { @apply py-1 pr-6 w-full font-mono text-xs text-gray-400 break-all; }
|
||||
.release-asset-action { @apply py-1 text-right whitespace-nowrap; }
|
||||
.release-asset-link { @apply text-blue-600 hover:text-blue-900 font-bold; }
|
||||
|
||||
/* Dev versions */
|
||||
.dev-versions-header { @apply border-t border-gray-100 bg-yellow-50/30; }
|
||||
.dev-versions-label { @apply px-8 py-3 text-[10px] font-black text-yellow-700 uppercase tracking-tighter; }
|
||||
.dev-release-row { @apply border-t border-gray-100 bg-yellow-50/10 hover:bg-yellow-50/30 transition-colors; }
|
||||
.dev-release-version { @apply font-bold text-yellow-800 text-lg mb-2; }
|
||||
|
||||
/* Mobile releases */
|
||||
.mobile-release-header { @apply flex justify-between items-start mb-4; }
|
||||
.mobile-release-version { @apply font-bold text-gray-900 text-lg; }
|
||||
.mobile-release-date { @apply text-gray-500 text-xs mt-1; }
|
||||
.dev-badge { @apply px-2 py-0.5 bg-yellow-100 text-yellow-800 text-[10px] font-black uppercase rounded; }
|
||||
.mobile-play-btn { @apply flex items-center justify-center py-2 bg-purple-100 text-purple-700 font-bold rounded-lg text-sm; }
|
||||
|
||||
/* Asset mobile - dark (releases) */
|
||||
.asset-mobile-item { @apply flex flex-col gap-0.5 py-1.5 border-b border-gray-100 last:border-0; }
|
||||
.asset-mobile-kind { @apply text-gray-500 text-sm whitespace-nowrap; }
|
||||
.asset-mobile-link { @apply text-blue-600 font-bold text-sm ml-4 shrink-0; }
|
||||
.asset-mobile-path { @apply font-mono text-xs text-gray-400 truncate; }
|
||||
|
||||
/* Status badges */
|
||||
.status-badge { @apply rounded text-[10px] font-black uppercase tracking-wider; }
|
||||
.status-released { @apply bg-green-500 text-white shadow-lg shadow-green-500/20; }
|
||||
.status-demo { @apply bg-blue-500 text-white shadow-lg shadow-blue-500/20; }
|
||||
.status-development { @apply bg-yellow-500 text-white shadow-lg shadow-yellow-500/20; }
|
||||
.status-archived { @apply bg-gray-500 text-white shadow-lg shadow-gray-500/20; }
|
||||
|
||||
/* Loading */
|
||||
.loading-card { @apply bg-white rounded-3xl shadow-xl p-12 text-center text-gray-400; }
|
||||
/* ---- Letöltések ---- */
|
||||
.asset-list {
|
||||
@apply mt-4 flex flex-col;
|
||||
}
|
||||
.asset-list-title {
|
||||
@apply mb-1 font-mono text-xs uppercase tracking-[0.1em] text-fg-subtle;
|
||||
}
|
||||
.asset-row {
|
||||
@apply flex flex-wrap items-baseline gap-x-4 gap-y-1 border-t border-line py-2 text-sm;
|
||||
}
|
||||
.asset-kind {
|
||||
@apply min-w-[9rem] text-fg;
|
||||
}
|
||||
.asset-path {
|
||||
@apply flex-grow truncate text-xs text-fg-subtle;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
@keydown.left="prev"
|
||||
@keydown.right="next"
|
||||
>
|
||||
<button class="lightbox-close-btn" @click="close"><i class="fa-solid fa-xmark"></i></button>
|
||||
<button class="lightbox-close-btn" :aria-label="t('catalogShow.close')" @click="close"><X :size="24" :stroke-width="1.75" /></button>
|
||||
|
||||
<button
|
||||
v-if="images.length > 1" class="lightbox-nav-btn left-4"
|
||||
@click="prev"
|
||||
:aria-label="t('catalogShow.prevImage')" @click="prev"
|
||||
>
|
||||
<i class="fa-solid fa-chevron-left"></i>
|
||||
<ChevronLeft :size="28" :stroke-width="1.75" />
|
||||
</button>
|
||||
|
||||
<img
|
||||
@@ -27,9 +27,9 @@
|
||||
|
||||
<button
|
||||
v-if="images.length > 1" class="lightbox-nav-btn right-4"
|
||||
@click="next"
|
||||
:aria-label="t('catalogShow.nextImage')" @click="next"
|
||||
>
|
||||
<i class="fa-solid fa-chevron-right"></i>
|
||||
<ChevronRight :size="28" :stroke-width="1.75" />
|
||||
</button>
|
||||
|
||||
<div v-if="images.length > 1" class="lightbox-counter">
|
||||
@@ -41,8 +41,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ChevronLeft, ChevronRight, X } from 'lucide-vue-next'
|
||||
import type { SoftwareImage } from '../../lib/interfaces/software.interface'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
images: SoftwareImage[]
|
||||
isOpen: boolean
|
||||
@@ -77,15 +81,15 @@ function prev() { currentIndex.value = (currentIndex.value - 1 + props.images.le
|
||||
@apply fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm;
|
||||
}
|
||||
.lightbox-close-btn {
|
||||
@apply absolute top-4 right-4 text-white/70 hover:text-white text-3xl font-bold z-10;
|
||||
@apply absolute right-4 top-4 z-10 rounded-control p-2 text-white/70 transition-colors hover:bg-white/10 hover:text-white;
|
||||
}
|
||||
.lightbox-nav-btn {
|
||||
@apply absolute text-white/70 hover:text-white text-5xl font-bold z-10 select-none;
|
||||
@apply absolute z-10 select-none rounded-control p-2 text-white/70 transition-colors hover:bg-white/10 hover:text-white;
|
||||
}
|
||||
.lightbox-image {
|
||||
@apply max-h-[85vh] max-w-[90vw] object-contain rounded-lg shadow-2xl;
|
||||
@apply max-h-[85vh] max-w-[90vw] rounded-card object-contain;
|
||||
}
|
||||
.lightbox-counter {
|
||||
@apply absolute bottom-6 text-white/60 text-sm font-mono;
|
||||
@apply absolute bottom-6 font-mono text-sm text-white/60;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,60 +1,62 @@
|
||||
<template>
|
||||
<header class="hero-section-gradient from-teal-600 to-green-600 py-20">
|
||||
<div class="hero-container max-w-4xl">
|
||||
<header class="hero-section">
|
||||
<div class="hero-container">
|
||||
<h1 class="hero-title">{{ t('code.title') }}</h1>
|
||||
<p class="hero-subtitle text-teal-50 mb-10">{{ t('code.subtitle') }}</p>
|
||||
<a href="https://git.teletype.hu" target="_blank" class="btn-hero-teal">
|
||||
<i class="fa-solid fa-code-branch mr-2"></i>{{ t('code.openGitea') }}
|
||||
</a>
|
||||
<p class="hero-subtitle">{{ t('code.subtitle') }}</p>
|
||||
<div class="hero-actions">
|
||||
<a :href="GIT_BASE" target="_blank" rel="noopener noreferrer" class="btn-accent">{{ t('code.openGitea') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container">
|
||||
<div v-if="error" class="error-alert" role="alert">
|
||||
<strong class="font-bold">{{ t('common.error') }}</strong>
|
||||
<span class="block sm:inline"> {{ error }}</span>
|
||||
<main class="main-container flex flex-col gap-12">
|
||||
<div v-if="error" class="error-banner" role="alert">
|
||||
<h2 class="error-banner-title">{{ t('common.error') }}</h2>
|
||||
<p class="error-banner-desc">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<SkeletonCard v-if="!error && publicRepos.length === 0 && !recentCommits.length" :count="4" />
|
||||
|
||||
<div v-if="!error && publicRepos.length > 0" class="card-grid mb-16">
|
||||
<div v-for="repo in publicRepos" :key="repo.name" class="card-base">
|
||||
<h3 class="repo-card-title">
|
||||
<a :href="repo.html_url" target="_blank" class="repo-link">
|
||||
<i class="fa-solid fa-code-branch mr-1 text-sm opacity-60"></i>{{ repo.owner.login }}/{{ repo.name }}
|
||||
</a>
|
||||
</h3>
|
||||
<p v-if="repo.description" class="repo-card-desc">{{ repo.description }}</p>
|
||||
<p v-if="repo.language" class="repo-card-meta"><i class="fa-solid fa-circle text-[6px] mr-1 align-middle"></i>{{ t('code.language') }} {{ repo.language }}</p>
|
||||
<p class="repo-card-meta"><i class="fa-solid fa-clock text-[10px] mr-1 opacity-50"></i>{{ t('code.updated') }} {{ formatDateTime(repo.updated_at) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Kártyafal helyett lista: a tárolók neve és leírása sorokban olvasható. -->
|
||||
<section v-if="!error && publicRepos.length > 0" class="repo-list">
|
||||
<a
|
||||
v-for="repo in publicRepos" :key="repo.name"
|
||||
:href="repo.html_url" target="_blank" rel="noopener noreferrer" class="repo-row"
|
||||
>
|
||||
<span class="repo-name">{{ repo.owner.login }}/{{ repo.name }}</span>
|
||||
<span v-if="repo.description" class="repo-desc">{{ repo.description }}</span>
|
||||
<span class="repo-meta">
|
||||
<span v-if="repo.language">{{ t('code.language') }} {{ repo.language }}</span>
|
||||
<span class="mono">{{ formatDateTime(repo.updated_at) }}</span>
|
||||
</span>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<h2 class="commits-section-title">{{ t('code.recentCommits') }}</h2>
|
||||
<section>
|
||||
<h2 class="section-title">{{ t('code.recentCommits') }}</h2>
|
||||
|
||||
<p v-if="!error && recentCommits.length === 0" class="empty-commits-msg">
|
||||
{{ t('code.noCommits') }}
|
||||
</p>
|
||||
<p v-if="!error && recentCommits.length === 0" class="text-sm text-fg-muted">
|
||||
{{ t('code.noCommits') }}
|
||||
</p>
|
||||
|
||||
<div class="commits-list">
|
||||
<div v-for="commit in recentCommits" :key="commit.sha" class="commit-item">
|
||||
<div class="commit-icon"><i class="fa-solid fa-rocket"></i></div>
|
||||
<div class="commit-content">
|
||||
<div class="commit-list">
|
||||
<article v-for="commit in recentCommits" :key="commit.sha" class="commit-item">
|
||||
<p class="commit-message">
|
||||
<a :href="commit.repo.html_url" target="_blank" class="commit-repo-link">
|
||||
<span class="font-bold">{{ commit.repo.owner }}</span> / <span class="font-bold">{{ commit.repo.name }}</span>
|
||||
<a :href="commit.repo.html_url" target="_blank" rel="noopener noreferrer" class="commit-repo-link">
|
||||
{{ commit.repo.owner }}/{{ commit.repo.name }}
|
||||
</a>
|
||||
: <span class="text-gray-700">{{ commit.message.split('\n')[0] }}</span>
|
||||
<span class="text-fg">{{ commit.message.split('\n')[0] }}</span>
|
||||
</p>
|
||||
<p class="commit-meta">
|
||||
<i class="fa-solid fa-user text-[10px] mr-0.5 opacity-50"></i>{{ t('code.authoredBy') }} <span class="font-bold">{{ commit.author.name }}</span> <i class="fa-solid fa-clock text-[10px] mx-0.5 opacity-50"></i>{{ t('code.on') }} {{ formatDateTime(commit.date) }}
|
||||
<a :href="commit.url" target="_blank" class="commit-sha-link">
|
||||
<span class="font-mono text-xs">{{ commit.sha.substring(0, 7) }}</span>
|
||||
{{ t('code.authoredBy') }} <span class="text-fg-muted">{{ commit.author.name }}</span>
|
||||
{{ t('code.on') }} <span class="mono">{{ formatDateTime(commit.date) }}</span>
|
||||
<a :href="commit.url" target="_blank" rel="noopener noreferrer" class="commit-sha mono">
|
||||
{{ commit.sha.substring(0, 7) }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -66,6 +68,8 @@ import { formatDateTime } from '../../lib/dateFormat'
|
||||
import { useGitStore } from '../../stores/git.store'
|
||||
import SkeletonCard from '../../components/SkeletonCard.vue'
|
||||
|
||||
const GIT_BASE = 'https://git.teletypegames.org'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const store = useGitStore()
|
||||
@@ -75,49 +79,40 @@ onMounted(() => store.fetch())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error-alert {
|
||||
@apply bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-8;
|
||||
.section-title {
|
||||
@apply mb-4 text-xl font-semibold tracking-tight text-fg;
|
||||
}
|
||||
.repo-card-title {
|
||||
@apply text-xl font-semibold mb-2;
|
||||
.repo-list {
|
||||
@apply overflow-hidden rounded-card border border-line bg-surface;
|
||||
}
|
||||
.repo-link {
|
||||
@apply text-blue-600 hover:text-blue-800;
|
||||
.repo-row {
|
||||
@apply flex flex-col gap-1 border-b border-line px-5 py-4 transition-colors last:border-b-0 hover:bg-raised;
|
||||
}
|
||||
.repo-card-desc {
|
||||
@apply text-gray-700 text-sm;
|
||||
.repo-name {
|
||||
@apply font-mono text-sm text-fg;
|
||||
}
|
||||
.repo-card-meta {
|
||||
@apply text-gray-500 text-xs mt-1;
|
||||
.repo-desc {
|
||||
@apply text-sm text-fg-muted;
|
||||
}
|
||||
.commits-section-title {
|
||||
@apply text-3xl font-bold mb-8 text-center text-gray-800;
|
||||
.repo-meta {
|
||||
@apply flex flex-wrap gap-4 text-xs text-fg-subtle;
|
||||
}
|
||||
.empty-commits-msg {
|
||||
@apply text-center text-gray-600 text-lg;
|
||||
}
|
||||
.commits-list {
|
||||
@apply space-y-4 mb-12;
|
||||
.commit-list {
|
||||
@apply flex flex-col;
|
||||
}
|
||||
.commit-item {
|
||||
@apply bg-white shadow-lg rounded-lg p-4 flex items-center space-x-4 border border-gray-100;
|
||||
}
|
||||
.commit-icon {
|
||||
@apply flex-shrink-0 text-3xl;
|
||||
}
|
||||
.commit-content {
|
||||
@apply flex-grow;
|
||||
@apply border-b border-line py-3 last:border-b-0;
|
||||
}
|
||||
.commit-message {
|
||||
@apply text-gray-800 font-semibold;
|
||||
@apply flex flex-wrap items-baseline gap-2 text-sm;
|
||||
}
|
||||
.commit-repo-link {
|
||||
@apply text-blue-600 hover:text-blue-800;
|
||||
@apply font-mono text-xs text-accent;
|
||||
}
|
||||
.commit-meta {
|
||||
@apply text-gray-600 text-sm mt-1;
|
||||
@apply mt-1 flex flex-wrap items-baseline gap-1.5 text-xs text-fg-subtle;
|
||||
}
|
||||
.commit-sha-link {
|
||||
@apply ml-2 text-blue-500 hover:text-blue-700;
|
||||
.commit-sha {
|
||||
@apply text-accent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,227 +1,143 @@
|
||||
<template>
|
||||
<header class="hero-section-gradient from-blue-600 to-indigo-700 py-20 text-white">
|
||||
<div class="hero-container text-center">
|
||||
<h1 class="hero-title mb-6">{{ t('contact.title') }}</h1>
|
||||
<p class="hero-subtitle mb-10 text-blue-100">{{ t('contact.subtitle') }}</p>
|
||||
<a :href="DISCORD_INVITE_LINK" target="_blank" class="discord-hero-btn">
|
||||
<i class="fa-brands fa-discord text-2xl"></i>
|
||||
{{ t('contact.joinDiscord') }}
|
||||
</a>
|
||||
<header class="hero-section">
|
||||
<div class="hero-container">
|
||||
<h1 class="hero-title">{{ t('contact.title') }}</h1>
|
||||
<p class="hero-subtitle">{{ t('contact.subtitle') }}</p>
|
||||
<div class="hero-actions">
|
||||
<a :href="DISCORD_INVITE_LINK" target="_blank" rel="noopener noreferrer" class="btn-accent">
|
||||
<BrandIcon name="discord" />{{ t('contact.joinDiscord') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container py-16 px-4">
|
||||
<div class="contact-grid">
|
||||
<div class="contact-card">
|
||||
<h2 class="contact-card-title">
|
||||
<i class="fa-solid fa-envelope text-3xl text-indigo-500"></i> {{ t('contact.emailUs') }}
|
||||
</h2>
|
||||
<p class="contact-card-desc">{{ t('contact.emailDesc') }}</p>
|
||||
<div class="email-link cursor-pointer" @click="openEmail">
|
||||
contact@teletype.hu
|
||||
</div>
|
||||
<main class="main-container flex flex-col gap-12">
|
||||
<!-- E-mail -->
|
||||
<section>
|
||||
<h2 class="section-title">{{ t('contact.emailUs') }}</h2>
|
||||
<p class="section-desc">{{ t('contact.emailDesc') }}</p>
|
||||
<a :href="`mailto:${EMAIL}`" class="link text-lg">{{ EMAIL }}</a>
|
||||
</section>
|
||||
|
||||
<!-- Közösség -->
|
||||
<section>
|
||||
<h2 class="section-title">{{ t('contact.community') }}</h2>
|
||||
<p class="section-desc">{{ t('contact.communityDesc') }}</p>
|
||||
|
||||
<div class="channel-grid">
|
||||
<article class="channel">
|
||||
<h3 class="channel-title"><BrandIcon name="discord" />Discord</h3>
|
||||
<p class="channel-desc">{{ t('contact.discord.desc') }}</p>
|
||||
<a :href="DISCORD_INVITE_LINK" target="_blank" rel="noopener noreferrer" class="link text-sm">
|
||||
{{ t('contact.discord.link') }}
|
||||
</a>
|
||||
</article>
|
||||
|
||||
<article class="channel">
|
||||
<h3 class="channel-title">IRC</h3>
|
||||
<p class="channel-desc">{{ t('contact.irc.desc') }}</p>
|
||||
<dl class="channel-facts">
|
||||
<dt>Network</dt><dd class="mono">Libera.Chat</dd>
|
||||
<dt>Channel</dt><dd class="mono">#teletypegames</dd>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="channel">
|
||||
<h3 class="channel-title">BBS</h3>
|
||||
<p class="channel-desc">{{ t('contact.bbs.desc') }}</p>
|
||||
<dl class="channel-facts">
|
||||
<dt>Host</dt><dd class="mono">games.teletype.hu</dd>
|
||||
<dt>Port</dt><dd class="mono">2323</dd>
|
||||
</dl>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="contact-card">
|
||||
<h2 class="contact-card-title">
|
||||
<i class="fa-solid fa-comments text-3xl text-indigo-500"></i> {{ t('contact.community') }}
|
||||
</h2>
|
||||
<p class="contact-card-desc">{{ t('contact.communityDesc') }}</p>
|
||||
<!-- Közösségi média — itt a márkajel az, ami felismerhetővé tesz, de a
|
||||
márkaszínek nem: azok egyik témában sem viselkednek jól. -->
|
||||
<section>
|
||||
<h2 class="section-title">{{ t('contact.social') }}</h2>
|
||||
<p class="section-desc">{{ t('contact.socialDesc') }}</p>
|
||||
|
||||
<div class="community-grid-3">
|
||||
<div class="community-section border-[#5865F2]">
|
||||
<h3 class="community-section-title text-[#5865F2]"><i class="fa-brands fa-discord"></i> Discord</h3>
|
||||
<p class="community-section-desc">{{ t('contact.discord.desc') }}</p>
|
||||
<a :href="DISCORD_INVITE_LINK" target="_blank" class="community-link-discord">
|
||||
{{ t('contact.discord.link') }} <i class="fa-solid fa-arrow-right text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="community-section border-green-500">
|
||||
<h3 class="community-section-title text-green-700"><i class="fa-solid fa-terminal"></i> IRC</h3>
|
||||
<p class="community-section-desc">{{ t('contact.irc.desc') }}</p>
|
||||
<div class="irc-box">
|
||||
<span class="irc-network">Network: Libera.Chat</span>
|
||||
<code class="irc-channel">#teletypegames</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="community-section border-amber-500">
|
||||
<h3 class="community-section-title text-amber-700"><i class="fa-solid fa-tower-broadcast"></i> BBS</h3>
|
||||
<p class="community-section-desc">{{ t('contact.bbs.desc') }}</p>
|
||||
<div class="bbs-box">
|
||||
<span class="bbs-host">Host: games.teletype.hu</span>
|
||||
<code class="bbs-port">Port: 2323</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="channel-grid">
|
||||
<article v-for="item in socials" :key="item.brand" class="channel">
|
||||
<h3 class="channel-title"><BrandIcon :name="item.brand" />{{ item.name }}</h3>
|
||||
<p class="channel-desc">{{ t(item.desc) }}</p>
|
||||
<a :href="item.href" target="_blank" rel="noopener noreferrer" class="link text-sm">{{ t(item.link) }}</a>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="contact-card">
|
||||
<h2 class="contact-card-title">
|
||||
<i class="fa-solid fa-share-nodes text-3xl text-indigo-500"></i> {{ t('contact.social') }}
|
||||
</h2>
|
||||
<p class="contact-card-desc">{{ t('contact.socialDesc') }}</p>
|
||||
|
||||
<div class="social-grid">
|
||||
<div class="community-section border-red-600">
|
||||
<h3 class="community-section-title text-red-600"><i class="fa-brands fa-youtube"></i> YouTube</h3>
|
||||
<p class="community-section-desc">{{ t('contact.youtube.desc') }}</p>
|
||||
<a href="https://www.youtube.com/@teletypegames" target="_blank" class="community-link-youtube">
|
||||
{{ t('contact.youtube.link') }} <i class="fa-solid fa-arrow-right text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="community-section border-[#1877F2]">
|
||||
<h3 class="community-section-title text-[#1877F2]"><i class="fa-brands fa-facebook"></i> Facebook</h3>
|
||||
<p class="community-section-desc">{{ t('contact.facebook.desc') }}</p>
|
||||
<a href="https://www.facebook.com/teletypegames" target="_blank" class="community-link-facebook">
|
||||
{{ t('contact.facebook.link') }} <i class="fa-solid fa-arrow-right text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="community-section border-[#0A66C2]">
|
||||
<h3 class="community-section-title text-[#0A66C2]"><i class="fa-brands fa-linkedin"></i> LinkedIn</h3>
|
||||
<p class="community-section-desc">{{ t('contact.linkedin.desc') }}</p>
|
||||
<a href="https://www.linkedin.com/company/teletypegames/" target="_blank" class="community-link-linkedin">
|
||||
{{ t('contact.linkedin.link') }} <i class="fa-solid fa-arrow-right text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="community-section border-[#1DB954]">
|
||||
<h3 class="community-section-title text-[#1DB954]"><i class="fa-brands fa-spotify"></i> Spotify</h3>
|
||||
<p class="community-section-desc">{{ t('contact.spotify.desc') }}</p>
|
||||
<a href="https://open.spotify.com/playlist/4T7hMkdiERkkW7HaPXqgfX?si=7cdaa091c3274422" target="_blank" class="community-link-spotify">
|
||||
{{ t('contact.spotify.link') }} <i class="fa-solid fa-arrow-right text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-card spotify-card">
|
||||
<div class="spotify-card-header">
|
||||
<div class="spotify-icon-wrap">
|
||||
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="spotify-card-title">Teletype Games Party</h2>
|
||||
<p class="spotify-card-subtitle">Spotify Playlist</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spotify-embed-wrap">
|
||||
<iframe
|
||||
style="border-radius: 12px"
|
||||
src="https://open.spotify.com/embed/playlist/4T7hMkdiERkkW7HaPXqgfX?utm_source=generator&theme=0"
|
||||
width="100%"
|
||||
height="352"
|
||||
frameBorder="0"
|
||||
allowfullscreen
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spotify -->
|
||||
<MediaBand label="Teletype Games Party">
|
||||
<template #action>
|
||||
<a :href="SPOTIFY_PLAYLIST_URL" target="_blank" rel="noopener noreferrer" class="link text-sm">
|
||||
{{ t('contact.spotify.link') }}
|
||||
</a>
|
||||
</template>
|
||||
<iframe
|
||||
class="spotify-embed"
|
||||
src="https://open.spotify.com/embed/playlist/4T7hMkdiERkkW7HaPXqgfX?utm_source=generator"
|
||||
width="100%"
|
||||
height="352"
|
||||
frameBorder="0"
|
||||
allowfullscreen
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
</MediaBand>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CONFIG } from '../../lib/config'
|
||||
import MediaBand from '../../components/MediaBand.vue'
|
||||
import { BrandIcon } from '../../components/icons'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const EMAIL = 'contact@teletype.hu'
|
||||
const DISCORD_INVITE_LINK = CONFIG.discordInvite
|
||||
const SPOTIFY_PLAYLIST_URL = 'https://open.spotify.com/playlist/4T7hMkdiERkkW7HaPXqgfX?si=7cdaa091c3274422'
|
||||
|
||||
function openEmail() {
|
||||
window.location.href = 'mailto:contact@teletype.hu'
|
||||
}
|
||||
const socials = [
|
||||
{ brand: 'youtube', name: 'YouTube', href: 'https://www.youtube.com/@teletypegames', desc: 'contact.youtube.desc', link: 'contact.youtube.link' },
|
||||
{ brand: 'facebook', name: 'Facebook', href: 'https://www.facebook.com/teletypegames', desc: 'contact.facebook.desc', link: 'contact.facebook.link' },
|
||||
{ brand: 'linkedin', name: 'LinkedIn', href: 'https://www.linkedin.com/company/teletypegames/', desc: 'contact.linkedin.desc', link: 'contact.linkedin.link' },
|
||||
{ brand: 'spotify', name: 'Spotify', href: SPOTIFY_PLAYLIST_URL, desc: 'contact.spotify.desc', link: 'contact.spotify.link' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.discord-hero-btn {
|
||||
@apply inline-flex items-center gap-3 bg-[#5865F2] hover:bg-[#4752C4] text-white font-black py-4 px-8 rounded-full text-lg transition-all transform hover:scale-105 shadow-xl;
|
||||
.section-title {
|
||||
@apply text-xl font-semibold tracking-tight text-fg;
|
||||
}
|
||||
.contact-grid {
|
||||
@apply grid grid-cols-1 gap-12 max-w-5xl mx-auto;
|
||||
.section-desc {
|
||||
@apply mb-5 mt-2 max-w-[68ch] text-sm leading-relaxed text-fg-muted;
|
||||
}
|
||||
.contact-card {
|
||||
@apply bg-white p-8 rounded-2xl shadow-xl border border-gray-100;
|
||||
.channel-grid {
|
||||
@apply grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4;
|
||||
}
|
||||
.contact-card-title {
|
||||
@apply text-2xl font-black text-gray-900 mb-6 flex items-center gap-3;
|
||||
.channel {
|
||||
@apply flex flex-col gap-2 rounded-card border border-line bg-surface p-5;
|
||||
}
|
||||
.contact-card-desc {
|
||||
@apply text-gray-600 mb-6 leading-relaxed;
|
||||
.channel-title {
|
||||
@apply flex items-center gap-2 text-base font-semibold text-fg;
|
||||
}
|
||||
.email-link {
|
||||
@apply text-indigo-600 font-bold text-xl hover:underline transition-all;
|
||||
.channel-desc {
|
||||
@apply flex-grow text-sm leading-relaxed text-fg-muted;
|
||||
}
|
||||
.community-section {
|
||||
@apply border-l-4 pl-4;
|
||||
.channel-facts {
|
||||
@apply grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-sm;
|
||||
}
|
||||
.community-section-title {
|
||||
@apply text-lg font-bold mb-2;
|
||||
.channel-facts dt {
|
||||
@apply text-xs uppercase tracking-wide text-fg-subtle;
|
||||
}
|
||||
.community-section-desc {
|
||||
@apply text-gray-600 text-sm mb-3;
|
||||
.channel-facts dd {
|
||||
@apply m-0 text-fg;
|
||||
}
|
||||
.community-link-discord {
|
||||
@apply text-[#5865F2] font-bold hover:underline inline-flex items-center gap-1;
|
||||
}
|
||||
.community-link-youtube {
|
||||
@apply text-red-600 font-bold hover:underline inline-flex items-center gap-1;
|
||||
}
|
||||
.community-link-facebook {
|
||||
@apply text-[#1877F2] font-bold hover:underline inline-flex items-center gap-1;
|
||||
}
|
||||
.community-link-linkedin {
|
||||
@apply text-[#0A66C2] font-bold hover:underline inline-flex items-center gap-1;
|
||||
}
|
||||
.community-link-spotify {
|
||||
@apply text-[#1DB954] font-bold hover:underline inline-flex items-center gap-1;
|
||||
}
|
||||
.spotify-card {
|
||||
@apply bg-gradient-to-br from-slate-900 via-slate-900 to-emerald-950 border-slate-800 text-white;
|
||||
}
|
||||
.spotify-card-header {
|
||||
@apply flex items-center gap-4 mb-6 text-[#1DB954];
|
||||
}
|
||||
.spotify-icon-wrap {
|
||||
@apply w-14 h-14 rounded-2xl bg-[#1DB954]/10 border border-[#1DB954]/20 flex items-center justify-center;
|
||||
}
|
||||
.spotify-card-title {
|
||||
@apply text-2xl font-black text-white;
|
||||
}
|
||||
.spotify-card-subtitle {
|
||||
@apply text-[#1DB954] text-sm font-bold;
|
||||
}
|
||||
.spotify-embed-wrap {
|
||||
@apply rounded-xl overflow-hidden;
|
||||
}
|
||||
.community-grid-3 {
|
||||
@apply grid grid-cols-1 sm:grid-cols-3 gap-4;
|
||||
}
|
||||
.social-grid {
|
||||
@apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4;
|
||||
}
|
||||
.irc-box {
|
||||
@apply bg-green-100 p-4 rounded-xl border border-green-200 mt-3 inline-block;
|
||||
}
|
||||
.irc-network {
|
||||
@apply block text-xs font-semibold text-green-500 uppercase tracking-wider mb-1;
|
||||
}
|
||||
.irc-channel {
|
||||
@apply text-green-800 font-mono text-lg font-bold;
|
||||
}
|
||||
.bbs-box {
|
||||
@apply bg-amber-50 p-4 rounded-xl border border-amber-100 mt-3 inline-block;
|
||||
}
|
||||
.bbs-host {
|
||||
@apply block text-xs font-semibold text-amber-500 uppercase tracking-wider mb-1;
|
||||
}
|
||||
.bbs-port {
|
||||
@apply text-amber-700 font-mono text-lg font-bold;
|
||||
.spotify-embed {
|
||||
@apply block overflow-hidden rounded-card border border-line bg-raised;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,39 +1,28 @@
|
||||
<template>
|
||||
<div class="engines-container">
|
||||
<header class="hero-section-slate">
|
||||
<div class="engines-header-decor">
|
||||
<div class="hero-decor-blob -top-24 -left-24 h-96 w-96 bg-emerald-600"></div>
|
||||
<div class="hero-decor-blob -bottom-24 -right-24 h-96 w-96 bg-teal-600"></div>
|
||||
</div>
|
||||
|
||||
<header class="hero-section">
|
||||
<div class="hero-container">
|
||||
<div class="hero-badge">{{ t('engines.badge') }}</div>
|
||||
<h1 class="hero-title">
|
||||
{{ t('engines.titleLead') }} <span class="text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-teal-300">{{ t('engines.titleAccent') }}</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle mb-10">{{ t('engines.subtitle') }}</p>
|
||||
<p class="hero-badge">{{ t('engines.badge') }}</p>
|
||||
<h1 class="hero-title">{{ t('engines.titleLead') }} {{ t('engines.titleAccent') }}</h1>
|
||||
<p class="hero-subtitle">{{ t('engines.subtitle') }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="engines-main">
|
||||
<main class="main-container">
|
||||
<div v-if="error" class="error-banner" role="alert">
|
||||
<i class="fa-solid fa-triangle-exclamation text-2xl text-yellow-500"></i>
|
||||
<div>
|
||||
<h3 class="error-banner-title">{{ t('engines.errorTitle') }}</h3>
|
||||
<p class="error-banner-desc">{{ error }}</p>
|
||||
</div>
|
||||
<h2 class="error-banner-title">{{ t('engines.errorTitle') }}</h2>
|
||||
<p class="error-banner-desc">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<SkeletonCard v-else-if="loading" :count="3" />
|
||||
|
||||
<div v-else-if="enginePages.length === 0" class="empty-state">
|
||||
<div class="empty-state-icon"><i class="fa-solid fa-inbox"></i></div>
|
||||
<h2 class="empty-state-title">{{ t('engines.noPagesTitle') }}</h2>
|
||||
<p class="empty-state-desc">{{ t('engines.noPagesDesc') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="engine-list">
|
||||
<article v-for="({ page, digest }, index) in cards" :key="page.id" class="engine-card group">
|
||||
<article v-for="({ page, digest }, index) in cards" :key="page.id" class="engine-card">
|
||||
<div class="engine-card-index">{{ String(index + 1).padStart(2, '0') }}</div>
|
||||
|
||||
<div class="engine-card-body">
|
||||
@@ -49,23 +38,18 @@
|
||||
<h3 v-if="digest.highlightsTitle" class="engine-highlights-title">{{ digest.highlightsTitle }}</h3>
|
||||
<ul class="engine-highlights-grid">
|
||||
<li v-for="(item, i) in digest.highlights" :key="i" class="engine-highlight">
|
||||
<span class="engine-highlight-icon"><i class="fa-solid fa-check"></i></span>
|
||||
<p class="engine-highlight-body">
|
||||
<strong v-if="item.title" class="engine-highlight-lead">{{ item.title }}</strong>
|
||||
<template v-if="item.title && item.text"> — </template>{{ item.text }}
|
||||
</p>
|
||||
<strong v-if="item.title" class="engine-highlight-lead">{{ item.title }}</strong>
|
||||
<template v-if="item.title && item.text"> — </template>{{ item.text }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div class="engine-card-actions">
|
||||
<a :href="exploreUrl(page)" target="_blank" rel="noopener" class="engine-explore-btn">
|
||||
<i class="fa-solid fa-code-branch text-sm"></i>
|
||||
<a :href="exploreUrl(page)" target="_blank" rel="noopener" class="btn-accent">
|
||||
{{ t('engines.explore') }}
|
||||
<i class="fa-solid fa-arrow-right text-sm"></i>
|
||||
</a>
|
||||
<a :href="`${WIKI_BASE}/${page.path}`" target="_blank" rel="noopener" class="engine-wiki-link">
|
||||
<i class="fa-solid fa-book mr-1"></i>{{ t('engines.openWiki') }}
|
||||
<a :href="`${WIKI_BASE}/${page.path}`" target="_blank" rel="noopener" class="btn-ghost">
|
||||
{{ t('engines.openWiki') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,83 +88,51 @@ onMounted(() => store.fetch())
|
||||
|
||||
<style scoped>
|
||||
.engines-container {
|
||||
@apply bg-slate-950 min-h-screen pb-24;
|
||||
@apply min-h-screen bg-page;
|
||||
}
|
||||
.engines-header-decor {
|
||||
@apply absolute inset-0 opacity-30;
|
||||
}
|
||||
.engines-main {
|
||||
@apply max-w-5xl mx-auto px-4 md:px-8 mt-16 relative z-10;
|
||||
}
|
||||
.banner-base {
|
||||
@apply max-w-7xl mx-auto border-l-4 p-6 rounded-r-xl shadow-lg mb-12 flex items-start gap-4;
|
||||
}
|
||||
.error-banner { @apply banner-base bg-red-50 border-red-500; }
|
||||
.error-banner-title { @apply text-red-800 font-bold text-lg; }
|
||||
.error-banner-desc { @apply text-red-700 mt-1; }
|
||||
.empty-state {
|
||||
@apply max-w-7xl mx-auto bg-slate-900 rounded-2xl shadow-xl p-12 text-center border border-slate-800;
|
||||
}
|
||||
.empty-state-icon { @apply text-6xl mb-4; }
|
||||
.empty-state-title { @apply text-2xl font-bold text-white mb-2; }
|
||||
.empty-state-desc { @apply text-slate-400 max-w-md mx-auto; }
|
||||
|
||||
/* Few engines, so every one of them gets a full-width, poster-like card. */
|
||||
/* Kevés motor van, ezért mindegyik teljes szélességű kártyát kap. A sorszám az
|
||||
oldal karaktere — csak halványabban, hogy ne vigye el a szöveg elől a helyet. */
|
||||
.engine-list {
|
||||
@apply flex flex-col gap-10;
|
||||
@apply flex flex-col gap-6;
|
||||
}
|
||||
.engine-card {
|
||||
@apply relative overflow-hidden bg-slate-900 rounded-3xl border border-slate-800 border-l-4 border-l-emerald-500 shadow-2xl transition-all hover:border-l-teal-300 hover:-translate-y-1;
|
||||
@apply relative overflow-hidden rounded-card border border-line bg-surface transition-colors hover:border-line-strong;
|
||||
}
|
||||
.engine-card-index {
|
||||
@apply absolute -top-6 right-4 text-[9rem] leading-none font-black text-slate-800/60 select-none pointer-events-none transition-colors;
|
||||
}
|
||||
.engine-card:hover .engine-card-index {
|
||||
@apply text-slate-800;
|
||||
@apply pointer-events-none absolute -top-6 right-4 select-none font-mono text-[7rem] leading-none text-line;
|
||||
}
|
||||
.engine-card-body {
|
||||
@apply relative p-8 md:p-12;
|
||||
@apply relative p-6 md:p-8;
|
||||
}
|
||||
.engine-card-title {
|
||||
@apply text-3xl md:text-5xl font-black text-white mb-4 leading-tight;
|
||||
@apply text-2xl font-semibold tracking-tight text-fg md:text-3xl;
|
||||
}
|
||||
.engine-card-title a {
|
||||
@apply hover:text-emerald-400 transition-colors;
|
||||
@apply transition-colors hover:text-accent;
|
||||
}
|
||||
.engine-card-desc {
|
||||
@apply text-lg md:text-xl text-emerald-100/90 font-semibold mb-3 leading-relaxed max-w-3xl;
|
||||
@apply mt-3 max-w-3xl text-base leading-relaxed text-fg;
|
||||
}
|
||||
.engine-card-preview {
|
||||
@apply text-base text-slate-400 leading-relaxed mb-8 max-w-3xl;
|
||||
@apply mt-2 max-w-3xl text-sm leading-relaxed text-fg-muted;
|
||||
}
|
||||
.engine-highlights {
|
||||
@apply mb-10;
|
||||
@apply mt-6 border-t border-line pt-5;
|
||||
}
|
||||
.engine-highlights-title {
|
||||
@apply text-xs font-bold uppercase tracking-[0.2em] text-emerald-400 mb-5;
|
||||
@apply mb-3 font-mono text-xs uppercase tracking-[0.14em] text-fg-subtle;
|
||||
}
|
||||
.engine-highlights-grid {
|
||||
@apply grid md:grid-cols-2 gap-x-10 gap-y-4 max-w-4xl;
|
||||
@apply grid max-w-4xl list-disc grid-cols-1 gap-x-8 gap-y-2 pl-5 md:grid-cols-2;
|
||||
}
|
||||
.engine-highlight {
|
||||
@apply flex items-start gap-3;
|
||||
}
|
||||
.engine-highlight-icon {
|
||||
@apply mt-1 flex h-5 w-5 flex-none items-center justify-center rounded-md bg-emerald-500/15 text-emerald-400 text-[10px];
|
||||
}
|
||||
.engine-highlight-body {
|
||||
@apply text-sm text-slate-400 leading-relaxed;
|
||||
@apply text-sm leading-relaxed text-fg-muted;
|
||||
}
|
||||
.engine-highlight-lead {
|
||||
@apply text-white font-semibold;
|
||||
@apply font-medium text-fg;
|
||||
}
|
||||
.engine-card-actions {
|
||||
@apply flex flex-wrap items-center gap-6;
|
||||
}
|
||||
.engine-explore-btn {
|
||||
@apply inline-flex items-center gap-2 bg-emerald-500 text-slate-950 px-6 py-3 rounded-xl font-bold hover:bg-emerald-400 transition-all shadow-lg shadow-emerald-900/40 group-hover:translate-x-1;
|
||||
}
|
||||
.engine-wiki-link {
|
||||
@apply text-slate-400 font-semibold hover:text-white transition-colors;
|
||||
@apply mt-6 flex flex-wrap items-center gap-3;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,272 +1,158 @@
|
||||
<template>
|
||||
<div class="home-container">
|
||||
<!-- Header Section -->
|
||||
<header class="hero-section-slate">
|
||||
<div class="home-header-decor">
|
||||
<div class="hero-decor-blob -top-24 -left-24 h-96 w-96 bg-indigo-600"></div>
|
||||
<div class="hero-decor-blob -bottom-24 -right-24 h-96 w-96 bg-purple-600"></div>
|
||||
</div>
|
||||
|
||||
<header class="hero-section">
|
||||
<div class="hero-container">
|
||||
<div class="hero-badge">{{ t('home.welcomeTo') }}</div>
|
||||
<h1 class="hero-title">
|
||||
Teletype <span class="text-transparent bg-clip-text bg-gradient-to-r from-violet-400 to-fuchsia-400">Games</span>
|
||||
</h1>
|
||||
<p class="hero-badge">{{ t('home.welcomeTo') }}</p>
|
||||
<h1 class="hero-title">Teletype <span class="text-accent">Games</span></h1>
|
||||
<p class="hero-subtitle">{{ t('home.subtitle') }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container max-w-6xl">
|
||||
<!-- Countdown Section -->
|
||||
<section v-if="nextEvent" class="mb-12">
|
||||
<div class="countdown-card">
|
||||
<h2 class="countdown-event-name">{{ nextEvent.name }}</h2>
|
||||
<div class="countdown-timer">{{ countdownText }}</div>
|
||||
<p class="countdown-date"><i class="fa-solid fa-calendar-days mr-1"></i>{{ t('home.startsOn') }} <span class="font-semibold">{{ nextEvent.dateText }}</span></p>
|
||||
<main class="main-container flex flex-col gap-10">
|
||||
<!-- Visszaszámláló — vékony sáv, nem féloldalnyi kártya: egy dátum és egy
|
||||
szám annyi információ, amennyi elfér egy sorban. -->
|
||||
<section v-if="nextEvent" class="countdown">
|
||||
<div class="countdown-main">
|
||||
<div>
|
||||
<h2 class="countdown-event-name">{{ nextEvent.name }}</h2>
|
||||
<p class="countdown-date">{{ t('home.startsOn') }} {{ nextEvent.dateText }}</p>
|
||||
</div>
|
||||
<p class="countdown-timer">{{ countdownText }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="followingEvents.length > 0" class="upcoming-events-section">
|
||||
<h3 class="upcoming-events-title">{{ t('home.upcomingEvents') }}</h3>
|
||||
<div class="space-y-4">
|
||||
<div v-for="event in followingEvents" :key="event.name" class="upcoming-event-item">
|
||||
<span class="font-bold text-gray-800">{{ event.name }}</span>
|
||||
<span class="text-sm text-gray-500 font-mono">{{ event.dateText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="followingEvents.length > 0" class="countdown-upcoming">
|
||||
<h3 class="countdown-upcoming-title">{{ t('home.upcomingEvents') }}</h3>
|
||||
<div v-for="event in followingEvents" :key="event.name" class="countdown-upcoming-row">
|
||||
<span class="text-fg">{{ event.name }}</span>
|
||||
<span class="mono text-fg-subtle">{{ event.dateText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Software Section -->
|
||||
<section v-if="highlightedSoftware" class="mb-16">
|
||||
<div class="featured-card group">
|
||||
<div v-if="highlightedImageUrl" class="featured-image-panel">
|
||||
<img :src="highlightedImageUrl" :alt="highlightedSoftware.software.title" class="featured-image" />
|
||||
<!-- Kiemelt játék -->
|
||||
<section v-if="highlightedSoftware" class="featured">
|
||||
<div v-if="highlightedImageUrl" class="featured-image-panel">
|
||||
<img :src="highlightedImageUrl" :alt="highlightedSoftware.software.title" class="featured-image" />
|
||||
</div>
|
||||
<div class="featured-content">
|
||||
<div class="featured-badges">
|
||||
<span class="badge-accent">{{ t('home.featuredGame') }}</span>
|
||||
<span :class="statusBadgeClass(highlightedSoftware.software.status)">
|
||||
{{ t(`common.status.${highlightedSoftware.software.status}`) }}
|
||||
</span>
|
||||
<span v-if="highlightedStableRelease" class="badge mono">{{ highlightedStableRelease.version }}</span>
|
||||
</div>
|
||||
<div class="featured-content">
|
||||
<div class="featured-badges-group">
|
||||
<span class="featured-badge">{{ t('home.featuredGame') }}</span>
|
||||
<span :class="`status-badge status-${highlightedSoftware.software.status}`">{{ t(`common.status.${highlightedSoftware.software.status}`) }}</span>
|
||||
<span v-if="highlightedStableRelease" class="featured-version-badge">{{ highlightedStableRelease.version }}</span>
|
||||
</div>
|
||||
<h2 class="featured-title">{{ highlightedSoftware.software.title }}</h2>
|
||||
<p class="featured-desc">{{ highlightedSoftware.software.desc }}</p>
|
||||
<h2 class="featured-title">{{ highlightedSoftware.software.title }}</h2>
|
||||
<p class="featured-desc">{{ highlightedSoftware.software.desc }}</p>
|
||||
|
||||
<div class="featured-actions">
|
||||
<a
|
||||
v-if="highlightedStableRelease?.htmlFolderPath"
|
||||
:href="highlightedStableRelease.htmlFolderPath"
|
||||
target="_blank"
|
||||
class="featured-btn-play"
|
||||
><i class="fa-solid fa-play mr-2"></i>{{ t('home.playInBrowser') }}</a>
|
||||
<RouterLink :to="`/catalog/${highlightedSoftware.software.name}`" class="featured-btn-secondary">
|
||||
<i class="fa-solid fa-circle-info mr-2"></i>{{ t('home.viewProjectDetails') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="featured-actions">
|
||||
<a
|
||||
v-if="highlightedStableRelease?.htmlFolderPath"
|
||||
:href="highlightedStableRelease.htmlFolderPath"
|
||||
target="_blank"
|
||||
class="btn-accent"
|
||||
>{{ t('home.playInBrowser') }}</a>
|
||||
<RouterLink :to="`/catalog/${highlightedSoftware.software.name}`" class="btn-ghost">
|
||||
{{ t('home.viewProjectDetails') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- The store. Placed under the featured game on purpose: somebody who has just
|
||||
read about a title is one step from wanting to run it, and this is that step.
|
||||
Same dark band as the two media sections below, so the page keeps one rhythm. -->
|
||||
<section class="mb-16">
|
||||
<div class="store-wrapper group">
|
||||
<div class="store-glow"></div>
|
||||
<!-- A bolt a kiemelt játék alatt: aki épp olvasott egy címről, egy lépésre
|
||||
van attól, hogy futtatni akarja, és ez az a lépés. -->
|
||||
<MediaBand :label="t('home.store.label')">
|
||||
<template #action>
|
||||
<RouterLink to="/stores" class="link text-sm">{{ t('home.store.allStores') }}</RouterLink>
|
||||
</template>
|
||||
|
||||
<div class="store-header">
|
||||
<div class="store-header-inner">
|
||||
<div class="store-status-blob">
|
||||
<div class="store-status-dot"></div>
|
||||
</div>
|
||||
<span class="store-label">{{ t('home.store.label') }}</span>
|
||||
<div class="store-grid">
|
||||
<figure class="store-shot">
|
||||
<img
|
||||
:src="clientScreenshot" :alt="t('stores.clientShotAlt')"
|
||||
width="1536" height="1110" loading="lazy"
|
||||
>
|
||||
</figure>
|
||||
|
||||
<div class="store-info">
|
||||
<h3 class="store-title">{{ t('home.store.title') }}</h3>
|
||||
<p class="store-desc">{{ t('home.store.desc') }}</p>
|
||||
<ul class="store-points">
|
||||
<li>{{ t('home.store.point1') }}</li>
|
||||
<li>{{ t('home.store.point2') }}</li>
|
||||
<li>{{ t('home.store.point3') }}</li>
|
||||
</ul>
|
||||
|
||||
<div class="store-actions">
|
||||
<a :href="CLIENT_RELEASES_URL" target="_blank" rel="noopener noreferrer" class="btn-accent">
|
||||
{{ t('home.store.download') }}
|
||||
</a>
|
||||
<RouterLink to="/stores" class="btn-ghost">{{ t('home.store.more') }}</RouterLink>
|
||||
</div>
|
||||
<RouterLink to="/stores" class="store-all-link">{{ t('home.store.allStores') }}</RouterLink>
|
||||
</div>
|
||||
|
||||
<div class="store-grid">
|
||||
<figure class="store-shot">
|
||||
<img
|
||||
:src="clientScreenshot" :alt="t('stores.clientShotAlt')"
|
||||
width="2304" height="1664" loading="lazy"
|
||||
>
|
||||
</figure>
|
||||
|
||||
<div class="store-info">
|
||||
<h3 class="store-title">{{ t('home.store.title') }}</h3>
|
||||
<p class="store-desc">{{ t('home.store.desc') }}</p>
|
||||
<ul class="store-points">
|
||||
<li><i class="fa-solid fa-check"></i> {{ t('home.store.point1') }}</li>
|
||||
<li><i class="fa-solid fa-check"></i> {{ t('home.store.point2') }}</li>
|
||||
<li><i class="fa-solid fa-check"></i> {{ t('home.store.point3') }}</li>
|
||||
</ul>
|
||||
|
||||
<div class="store-actions">
|
||||
<a :href="CLIENT_RELEASES_URL" target="_blank" rel="noopener noreferrer" class="store-btn-primary">
|
||||
<i class="fa-solid fa-download"></i> {{ t('home.store.download') }}
|
||||
</a>
|
||||
<RouterLink to="/stores" class="store-btn-secondary">
|
||||
<i class="fa-solid fa-circle-info"></i> {{ t('home.store.more') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<p class="store-platforms">
|
||||
<i class="fa-brands fa-linux"></i> <i class="fa-brands fa-apple"></i>
|
||||
<i class="fa-brands fa-windows"></i> {{ t('home.store.platforms') }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="store-platforms">
|
||||
<BrandIcon name="linux" /><BrandIcon name="apple" /><BrandIcon name="windows" />
|
||||
{{ t('home.store.platforms') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</MediaBand>
|
||||
|
||||
<!-- Latest YouTube Video -->
|
||||
<section class="mb-16">
|
||||
<div class="yt-featured-wrapper group">
|
||||
<div class="yt-glow"></div>
|
||||
<!-- Legutóbbi YouTube-videó -->
|
||||
<MediaBand :label="t('home.latestFromYoutube')">
|
||||
<template #action>
|
||||
<a href="https://www.youtube.com/@teletypegames" target="_blank" class="link text-sm">
|
||||
{{ t('home.visitChannel') }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<div class="yt-header-area">
|
||||
<div class="yt-header-inner">
|
||||
<div class="yt-status-blob">
|
||||
<div class="yt-status-dot"></div>
|
||||
</div>
|
||||
<span class="yt-label-text">{{ t('home.latestFromYoutube') }}</span>
|
||||
</div>
|
||||
<a href="https://www.youtube.com/@teletypegames" target="_blank" class="yt-channel-link">
|
||||
{{ t('home.visitChannel') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="yt-content-grid">
|
||||
<div class="yt-player-container">
|
||||
<div class="yt-player-wrap">
|
||||
<div v-if="ytLoading" class="yt-state-overlay">
|
||||
<div class="yt-loading-spinner"></div>
|
||||
</div>
|
||||
<div v-else-if="ytError" class="yt-state-overlay flex-col gap-2">
|
||||
<div class="yt-error-icon"><i class="fa-solid fa-triangle-exclamation"></i></div>
|
||||
<div class="yt-error-text">{{ ytError }}</div>
|
||||
</div>
|
||||
<div v-else-if="ytNoConfig" class="yt-state-overlay">
|
||||
<div class="yt-noconfig-text">{{ t('common.missingApiConfig') }}</div>
|
||||
</div>
|
||||
<iframe
|
||||
v-else-if="ytVideoId"
|
||||
:src="`https://www.youtube.com/embed/${ytVideoId}?autoplay=0&rel=0&modestbranding=1`"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
class="yt-iframe"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="ytVideoId" class="yt-info-sidebar">
|
||||
<div class="mb-auto">
|
||||
<span class="yt-new-tag">{{ t('home.newRelease') }}</span>
|
||||
<h3 class="yt-video-title">{{ ytTitle }}</h3>
|
||||
<div class="yt-stats-row">
|
||||
<span v-if="ytPublishDate"><i class="fa-solid fa-calendar-days"></i> {{ ytPublishDate }}</span>
|
||||
<span v-if="ytViewCount"><i class="fa-solid fa-eye"></i> {{ ytViewCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<a :href="`https://www.youtube.com/watch?v=${ytVideoId}`" target="_blank" class="yt-play-button">
|
||||
<i class="fa-brands fa-youtube text-xl text-red-500"></i> {{ t('home.watchOnYoutube') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Spotify Playlist -->
|
||||
<section class="mb-16">
|
||||
<div class="spotify-wrapper group">
|
||||
<div class="spotify-glow"></div>
|
||||
|
||||
<div class="spotify-header">
|
||||
<div class="spotify-header-inner">
|
||||
<div class="spotify-pulse-wrap">
|
||||
<div class="spotify-pulse-bar"></div>
|
||||
<div class="spotify-pulse-bar bar-2"></div>
|
||||
<div class="spotify-pulse-bar bar-3"></div>
|
||||
<div class="spotify-pulse-bar bar-4"></div>
|
||||
</div>
|
||||
<span class="spotify-label">Teletype Games Party</span>
|
||||
</div>
|
||||
<a href="https://open.spotify.com/playlist/4T7hMkdiERkkW7HaPXqgfX?si=7cdaa091c3274422" target="_blank" class="spotify-open-link">
|
||||
{{ t('home.spotify.openInSpotify') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="spotify-embed-container">
|
||||
<div class="yt-grid">
|
||||
<div class="yt-player">
|
||||
<div v-if="ytLoading" class="yt-state"><span class="yt-spinner"></span></div>
|
||||
<div v-else-if="ytError" class="yt-state">{{ ytError }}</div>
|
||||
<div v-else-if="ytNoConfig" class="yt-state">{{ t('common.missingApiConfig') }}</div>
|
||||
<iframe
|
||||
style="border-radius: 12px"
|
||||
src="https://open.spotify.com/embed/playlist/4T7hMkdiERkkW7HaPXqgfX?utm_source=generator&theme=0"
|
||||
width="100%"
|
||||
height="352"
|
||||
frameBorder="0"
|
||||
v-else-if="ytVideoId"
|
||||
:src="`https://www.youtube.com/embed/${ytVideoId}?autoplay=0&rel=0&modestbranding=1`"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
class="yt-iframe"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Navigation Grid -->
|
||||
<section class="nav-grid">
|
||||
<div class="nav-card group card-interactive">
|
||||
<div class="nav-card-icon bg-purple-50 group-hover:bg-purple-100">
|
||||
<i class="fa-solid fa-gamepad text-3xl text-purple-600"></i>
|
||||
<div v-if="ytVideoId" class="yt-info">
|
||||
<span class="badge-accent">{{ t('home.newRelease') }}</span>
|
||||
<h3 class="yt-title">{{ ytTitle }}</h3>
|
||||
<p class="yt-stats">
|
||||
<span v-if="ytPublishDate" class="mono">{{ ytPublishDate }}</span>
|
||||
<span v-if="ytViewCount" class="mono">{{ ytViewCount }}</span>
|
||||
</p>
|
||||
<a :href="`https://www.youtube.com/watch?v=${ytVideoId}`" target="_blank" class="btn-ghost mt-auto">
|
||||
<BrandIcon name="youtube" />{{ t('home.watchOnYoutube') }}
|
||||
</a>
|
||||
</div>
|
||||
<h2 class="nav-card-title">{{ t('home.catalog.title') }}</h2>
|
||||
<p class="nav-card-desc">{{ t('home.catalog.desc') }}</p>
|
||||
<RouterLink to="/catalog" class="btn-primary bg-purple-600 hover:bg-purple-700 text-white hover:shadow-purple-200">
|
||||
{{ t('home.catalog.btn') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</MediaBand>
|
||||
|
||||
<div class="nav-card group card-interactive">
|
||||
<div class="nav-card-icon bg-green-50 group-hover:bg-green-100">
|
||||
<i class="fa-solid fa-book text-3xl text-green-600"></i>
|
||||
</div>
|
||||
<h2 class="nav-card-title">{{ t('home.wiki.title') }}</h2>
|
||||
<p class="nav-card-desc">{{ t('home.wiki.desc') }}</p>
|
||||
<a href="https://wiki.teletype.hu" target="_blank" rel="noopener noreferrer" class="btn-primary bg-green-600 hover:bg-green-700 text-white hover:shadow-green-200">
|
||||
{{ t('home.wiki.btn') }}
|
||||
<!-- Spotify-lejátszási lista -->
|
||||
<MediaBand label="Teletype Games Party">
|
||||
<template #action>
|
||||
<a :href="SPOTIFY_PLAYLIST_URL" target="_blank" class="link text-sm">
|
||||
{{ t('home.spotify.openInSpotify') }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="nav-card group card-interactive">
|
||||
<div class="nav-card-icon bg-slate-50 group-hover:bg-slate-100">
|
||||
<i class="fa-solid fa-laptop-code text-3xl text-slate-700"></i>
|
||||
</div>
|
||||
<h2 class="nav-card-title">{{ t('home.git.title') }}</h2>
|
||||
<p class="nav-card-desc">{{ t('home.git.desc') }}</p>
|
||||
<a href="https://git.teletype.hu" target="_blank" rel="noopener noreferrer" class="btn-primary bg-slate-800 hover:bg-slate-900 text-white hover:shadow-slate-200">
|
||||
{{ t('home.git.btn') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-card group card-interactive">
|
||||
<div class="nav-card-icon bg-red-50 group-hover:bg-red-100">
|
||||
<i class="fa-solid fa-tv text-3xl text-red-600"></i>
|
||||
</div>
|
||||
<h2 class="nav-card-title">{{ t('home.youtube.title') }}</h2>
|
||||
<p class="nav-card-desc">{{ t('home.youtube.desc') }}</p>
|
||||
<a href="https://www.youtube.com/@teletypegames" target="_blank" rel="noopener noreferrer" class="btn-primary bg-red-600 hover:bg-red-700 text-white hover:shadow-red-200">
|
||||
{{ t('home.youtube.btn') }}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- AI Content Notice -->
|
||||
<div class="ai-notice-card">
|
||||
<div class="ai-notice-icon"><i class="fa-solid fa-robot text-indigo-500"></i></div>
|
||||
<div class="ai-notice-text">
|
||||
<strong>Note:</strong> {{ t('home.note') }}
|
||||
</div>
|
||||
</div>
|
||||
<iframe
|
||||
class="spotify-embed"
|
||||
src="https://open.spotify.com/embed/playlist/4T7hMkdiERkkW7HaPXqgfX?utm_source=generator"
|
||||
width="100%"
|
||||
height="352"
|
||||
frameBorder="0"
|
||||
allowfullscreen
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
</MediaBand>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -279,12 +165,15 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useEventStore } from '../../stores/event.store'
|
||||
import { useSoftwareStore } from '../../stores/software.store'
|
||||
import { useYoutubeStore } from '../../stores/youtube.store'
|
||||
import { getDefaultImageUrl } from '../../lib/softwareUtils'
|
||||
import clientScreenshot from '../../assets/warpengine-client.png'
|
||||
import { getDefaultImageUrl, statusBadgeClass } from '../../lib/softwareUtils'
|
||||
import MediaBand from '../../components/MediaBand.vue'
|
||||
import { BrandIcon } from '../../components/icons'
|
||||
import clientScreenshot from '../../assets/warpengine-client.webp'
|
||||
|
||||
// The releases page, not a versioned file: whoever follows it should get whatever is
|
||||
// newest, and this page would otherwise need a release every time the client has one.
|
||||
const CLIENT_RELEASES_URL = 'https://git.teletypegames.org/stores/warp-engine-client/releases'
|
||||
const SPOTIFY_PLAYLIST_URL = 'https://open.spotify.com/playlist/4T7hMkdiERkkW7HaPXqgfX?si=7cdaa091c3274422'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -314,286 +203,117 @@ onUnmounted(() => eventStore.cleanup())
|
||||
|
||||
<style scoped>
|
||||
.home-container {
|
||||
@apply bg-gray-50 min-h-screen pb-20;
|
||||
}
|
||||
.home-header-decor {
|
||||
@apply absolute inset-0 opacity-30;
|
||||
@apply min-h-screen bg-page;
|
||||
}
|
||||
|
||||
/* Featured Card */
|
||||
.featured-card {
|
||||
@apply relative overflow-hidden bg-gradient-to-br from-indigo-900 to-slate-900 rounded-3xl shadow-2xl flex flex-col md:flex-row min-h-[400px];
|
||||
/* ---- Visszaszámláló ---- */
|
||||
.countdown {
|
||||
@apply rounded-card border border-line bg-surface;
|
||||
}
|
||||
.featured-content {
|
||||
@apply p-8 md:p-12 flex flex-col justify-center flex-1 z-10;
|
||||
}
|
||||
.featured-badges-group {
|
||||
@apply flex items-center gap-2 mb-4;
|
||||
}
|
||||
.featured-actions {
|
||||
@apply flex flex-wrap gap-4 mt-8;
|
||||
}
|
||||
.featured-badge {
|
||||
@apply px-3 py-1 bg-indigo-500/20 text-indigo-300 border border-indigo-400/30 rounded-full text-xs font-bold uppercase tracking-wider;
|
||||
}
|
||||
.featured-title {
|
||||
@apply text-4xl md:text-5xl font-black text-white mb-6;
|
||||
}
|
||||
.featured-desc {
|
||||
@apply text-indigo-100/80 text-lg leading-relaxed max-w-xl;
|
||||
}
|
||||
.featured-btn-play {
|
||||
@apply px-8 py-4 bg-white text-indigo-900 font-bold rounded-xl transition-all hover:scale-105 hover:shadow-xl hover:shadow-indigo-500/20 active:scale-95;
|
||||
}
|
||||
.featured-btn-secondary {
|
||||
@apply px-8 py-4 bg-indigo-500/10 text-white border border-indigo-400/30 font-bold rounded-xl transition-all hover:bg-indigo-500/20;
|
||||
}
|
||||
.featured-image-panel {
|
||||
@apply flex-shrink-0 flex items-center justify-center p-6 md:p-8;
|
||||
}
|
||||
.featured-image {
|
||||
@apply max-h-48 w-auto object-contain border-4 border-white shadow-xl opacity-90 group-hover:opacity-100 transition-opacity;
|
||||
}
|
||||
.featured-version-badge {
|
||||
@apply px-2 py-0.5 rounded text-[10px] font-bold bg-white/10 text-white/70 border border-white/20;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
@apply px-2 py-0.5 rounded text-[10px] font-bold uppercase;
|
||||
}
|
||||
.status-released { @apply bg-green-500/20 text-green-300 border border-green-500/30; }
|
||||
.status-demo { @apply bg-blue-500/20 text-blue-300 border border-blue-500/30; }
|
||||
.status-development { @apply bg-yellow-500/20 text-yellow-300 border border-yellow-500/30; }
|
||||
.status-archived { @apply bg-gray-500/20 text-gray-300 border border-gray-500/30; }
|
||||
|
||||
/* Countdown Card */
|
||||
.countdown-card {
|
||||
@apply bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-100 p-8 md:p-12 text-center transition-all duration-300;
|
||||
.countdown-main {
|
||||
@apply flex flex-wrap items-center justify-between gap-4 px-5 py-4 md:px-6;
|
||||
}
|
||||
.countdown-event-name {
|
||||
@apply text-2xl font-bold text-gray-400 uppercase tracking-widest mb-2;
|
||||
}
|
||||
.countdown-timer {
|
||||
@apply text-5xl md:text-7xl font-mono font-black text-transparent bg-clip-text bg-gradient-to-r from-violet-600 to-fuchsia-600 my-6;
|
||||
@apply text-lg font-semibold tracking-tight text-fg;
|
||||
}
|
||||
.countdown-date {
|
||||
@apply text-gray-500 text-lg;
|
||||
@apply text-sm text-fg-muted;
|
||||
}
|
||||
.upcoming-events-section {
|
||||
@apply mt-12 pt-12 border-t border-gray-100 text-left max-w-2xl mx-auto;
|
||||
.countdown-timer {
|
||||
@apply font-mono text-2xl tabular-nums text-accent md:text-3xl;
|
||||
}
|
||||
.upcoming-events-title {
|
||||
@apply text-xl font-bold text-gray-900 mb-6;
|
||||
.countdown-upcoming {
|
||||
@apply border-t border-line px-5 py-4 md:px-6;
|
||||
}
|
||||
.upcoming-event-item {
|
||||
@apply flex items-center justify-between p-4 bg-gray-50 rounded-xl border border-gray-100;
|
||||
.countdown-upcoming-title {
|
||||
@apply mb-2 font-mono text-xs uppercase tracking-[0.14em] text-fg-subtle;
|
||||
}
|
||||
.countdown-upcoming-row {
|
||||
@apply flex items-center justify-between py-1 text-sm;
|
||||
}
|
||||
|
||||
/* Nav Cards */
|
||||
.nav-card {
|
||||
@apply flex flex-col h-full;
|
||||
/* ---- Kiemelt játék ---- */
|
||||
.featured {
|
||||
@apply flex flex-col overflow-hidden rounded-card border border-line bg-surface md:flex-row;
|
||||
}
|
||||
.nav-card-icon {
|
||||
@apply w-14 h-14 rounded-xl flex items-center justify-center mb-6 transition-colors;
|
||||
.featured-image-panel {
|
||||
@apply flex flex-shrink-0 items-center justify-center border-b border-line bg-raised p-8 md:border-b-0 md:border-r;
|
||||
}
|
||||
.nav-card-title {
|
||||
@apply text-2xl font-bold text-gray-900 mb-3;
|
||||
.featured-image {
|
||||
@apply max-h-48 w-auto object-contain;
|
||||
}
|
||||
.nav-card-desc {
|
||||
@apply text-gray-600 mb-6 flex-grow leading-relaxed;
|
||||
.featured-content {
|
||||
@apply flex flex-1 flex-col justify-center p-6 md:p-8;
|
||||
}
|
||||
.featured-badges {
|
||||
@apply mb-3 flex flex-wrap items-center gap-2;
|
||||
}
|
||||
.featured-title {
|
||||
@apply text-2xl font-semibold tracking-tight text-fg md:text-3xl;
|
||||
}
|
||||
.featured-desc {
|
||||
@apply mt-2 max-w-2xl leading-relaxed text-fg-muted;
|
||||
}
|
||||
.featured-actions {
|
||||
@apply mt-6 flex flex-wrap gap-3;
|
||||
}
|
||||
|
||||
/* YouTube Featured Wrapper */
|
||||
.yt-featured-wrapper {
|
||||
@apply relative overflow-hidden bg-gradient-to-br from-slate-900 via-slate-900 to-indigo-950 rounded-3xl shadow-2xl border border-slate-800 p-6 md:p-8;
|
||||
}
|
||||
.yt-glow {
|
||||
@apply absolute -top-24 -right-24 w-96 h-96 bg-indigo-500/10 rounded-full blur-[100px] pointer-events-none;
|
||||
}
|
||||
.yt-header-area {
|
||||
@apply flex items-center justify-between mb-6 relative z-10;
|
||||
}
|
||||
.yt-status-blob {
|
||||
@apply w-3 h-3 bg-red-500/20 rounded-full flex items-center justify-center;
|
||||
}
|
||||
.yt-status-dot {
|
||||
@apply w-1.5 h-1.5 bg-red-500 rounded-full animate-pulse;
|
||||
}
|
||||
.yt-label-text {
|
||||
@apply text-xs font-bold uppercase tracking-widest text-slate-400;
|
||||
}
|
||||
.yt-channel-link {
|
||||
@apply text-xs font-semibold text-indigo-400 hover:text-indigo-300 transition-colors;
|
||||
}
|
||||
.yt-content-grid {
|
||||
@apply flex flex-col lg:flex-row gap-8 relative z-10;
|
||||
}
|
||||
.yt-player-container {
|
||||
@apply flex-[1.6] overflow-hidden rounded-2xl border border-white/5 shadow-inner bg-black;
|
||||
}
|
||||
.yt-player-wrap {
|
||||
@apply relative w-full aspect-video;
|
||||
}
|
||||
.yt-state-overlay {
|
||||
@apply absolute inset-0 flex items-center justify-center bg-slate-950;
|
||||
}
|
||||
.yt-loading-spinner {
|
||||
@apply w-10 h-10 border-4 border-white/5 border-t-indigo-500 rounded-full animate-spin;
|
||||
}
|
||||
.yt-error-icon {
|
||||
@apply text-xl text-yellow-500;
|
||||
}
|
||||
.yt-error-text {
|
||||
@apply text-[11px] text-slate-500 text-center px-4 font-bold uppercase tracking-tighter;
|
||||
}
|
||||
.yt-noconfig-text {
|
||||
@apply text-slate-500 text-xs;
|
||||
}
|
||||
.yt-iframe {
|
||||
@apply w-full h-full border-none block;
|
||||
}
|
||||
.yt-header-inner {
|
||||
@apply flex items-center gap-3;
|
||||
}
|
||||
.yt-info-sidebar {
|
||||
@apply flex-1 flex flex-col justify-center;
|
||||
}
|
||||
.yt-new-tag {
|
||||
@apply inline-block px-2 py-1 bg-red-500/10 text-red-400 border border-red-500/20 rounded-md text-[10px] font-black uppercase tracking-tighter mb-4;
|
||||
}
|
||||
.yt-video-title {
|
||||
@apply text-2xl font-black text-white mb-4 leading-tight group-hover:text-indigo-200 transition-colors;
|
||||
}
|
||||
.yt-stats-row {
|
||||
@apply flex gap-4 text-sm text-slate-400 font-medium;
|
||||
}
|
||||
.yt-play-button {
|
||||
@apply flex items-center justify-center gap-2 w-full py-4 bg-white text-slate-900 font-black rounded-xl transition-all hover:scale-[1.02] hover:bg-indigo-50 active:scale-95 shadow-xl;
|
||||
}
|
||||
|
||||
/* Spotify Section */
|
||||
.spotify-wrapper {
|
||||
@apply relative overflow-hidden bg-gradient-to-br from-slate-900 via-slate-900 to-emerald-950 rounded-3xl shadow-2xl border border-slate-800 p-6 md:p-8;
|
||||
}
|
||||
.spotify-glow {
|
||||
@apply absolute -top-24 -left-24 w-96 h-96 bg-[#1DB954]/10 rounded-full blur-[100px] pointer-events-none;
|
||||
}
|
||||
.spotify-header {
|
||||
@apply flex items-center justify-between mb-6 relative z-10;
|
||||
}
|
||||
.spotify-pulse-wrap {
|
||||
@apply flex items-end gap-[3px] h-4;
|
||||
}
|
||||
.spotify-pulse-bar {
|
||||
@apply w-[3px] bg-[#1DB954] rounded-full animate-pulse;
|
||||
height: 100%;
|
||||
}
|
||||
.spotify-pulse-bar.bar-2 {
|
||||
height: 60%;
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
.spotify-pulse-bar.bar-3 {
|
||||
height: 80%;
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
.spotify-pulse-bar.bar-4 {
|
||||
height: 40%;
|
||||
animation-delay: 0.45s;
|
||||
}
|
||||
.spotify-label {
|
||||
@apply text-xs font-bold uppercase tracking-widest text-slate-400;
|
||||
}
|
||||
.spotify-open-link {
|
||||
@apply text-xs font-semibold text-[#1DB954] hover:text-[#1ed760] transition-colors;
|
||||
}
|
||||
.spotify-header-inner {
|
||||
@apply flex items-center gap-3;
|
||||
}
|
||||
.spotify-embed-container {
|
||||
@apply relative z-10 rounded-xl overflow-hidden;
|
||||
}
|
||||
|
||||
/* Store Promo */
|
||||
.store-wrapper {
|
||||
@apply relative overflow-hidden bg-gradient-to-br from-slate-900 via-slate-900 to-violet-950 rounded-3xl shadow-2xl border border-slate-800 p-6 md:p-8;
|
||||
}
|
||||
.store-glow {
|
||||
@apply absolute -bottom-24 -right-24 w-96 h-96 bg-violet-500/10 rounded-full blur-[100px] pointer-events-none;
|
||||
}
|
||||
.store-header {
|
||||
@apply flex items-center justify-between mb-6 relative z-10;
|
||||
}
|
||||
.store-header-inner {
|
||||
@apply flex items-center gap-3;
|
||||
}
|
||||
.store-status-blob {
|
||||
@apply w-3 h-3 bg-violet-500/20 rounded-full flex items-center justify-center;
|
||||
}
|
||||
.store-status-dot {
|
||||
@apply w-1.5 h-1.5 bg-violet-400 rounded-full animate-pulse;
|
||||
}
|
||||
.store-label {
|
||||
@apply text-xs font-bold uppercase tracking-widest text-slate-400;
|
||||
}
|
||||
.store-all-link {
|
||||
@apply text-xs font-semibold text-violet-400 hover:text-violet-300 transition-colors;
|
||||
}
|
||||
/* ---- Bolt ---- */
|
||||
.store-grid {
|
||||
@apply flex flex-col lg:flex-row gap-8 relative z-10;
|
||||
@apply grid grid-cols-1 gap-6 md:grid-cols-2 md:items-center;
|
||||
}
|
||||
/* The screenshot leads on a wide screen and follows on a narrow one: it is what makes
|
||||
the section legible at a glance, but it is not what somebody reads first on a phone. */
|
||||
.store-shot {
|
||||
@apply flex-[1.4] order-last lg:order-first overflow-hidden rounded-2xl border border-white/5 shadow-inner bg-black;
|
||||
@apply m-0 overflow-hidden rounded-card border border-line;
|
||||
}
|
||||
.store-shot img {
|
||||
@apply w-full h-auto block;
|
||||
}
|
||||
.store-info {
|
||||
@apply flex-1 flex flex-col justify-center;
|
||||
@apply block h-auto w-full;
|
||||
}
|
||||
.store-title {
|
||||
@apply text-2xl font-black text-white mb-3 leading-tight group-hover:text-violet-200 transition-colors;
|
||||
@apply text-xl font-semibold tracking-tight text-fg;
|
||||
}
|
||||
.store-desc {
|
||||
@apply text-slate-300 leading-relaxed mb-5;
|
||||
@apply mt-2 leading-relaxed text-fg-muted;
|
||||
}
|
||||
.store-points {
|
||||
@apply space-y-2 mb-6 text-sm text-slate-400;
|
||||
}
|
||||
.store-points li {
|
||||
@apply flex items-start gap-2;
|
||||
}
|
||||
.store-points i {
|
||||
@apply text-violet-400 mt-1 text-xs;
|
||||
@apply mt-4 flex list-disc flex-col gap-1.5 pl-5 text-sm text-fg-muted;
|
||||
}
|
||||
.store-actions {
|
||||
@apply flex flex-wrap gap-3;
|
||||
}
|
||||
.store-btn-primary {
|
||||
@apply inline-flex items-center gap-2 px-6 py-3 bg-white text-slate-900 font-black rounded-xl transition-all hover:scale-[1.02] hover:bg-violet-50 active:scale-95 shadow-xl;
|
||||
}
|
||||
.store-btn-secondary {
|
||||
@apply inline-flex items-center gap-2 px-6 py-3 bg-violet-500/10 text-white border border-violet-400/30 font-bold rounded-xl transition-all hover:bg-violet-500/20;
|
||||
@apply mt-5 flex flex-wrap gap-3;
|
||||
}
|
||||
.store-platforms {
|
||||
@apply flex items-center gap-2 mt-5 text-xs text-slate-500;
|
||||
@apply mt-4 flex items-center gap-2 text-xs text-fg-subtle;
|
||||
}
|
||||
|
||||
/* Nav Grid */
|
||||
.nav-grid {
|
||||
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6;
|
||||
/* ---- YouTube ---- */
|
||||
.yt-grid {
|
||||
@apply grid grid-cols-1 gap-6 lg:grid-cols-[1.6fr_1fr];
|
||||
}
|
||||
.yt-player {
|
||||
@apply relative aspect-video overflow-hidden rounded-card border border-line bg-raised;
|
||||
}
|
||||
.yt-iframe {
|
||||
@apply absolute inset-0 h-full w-full;
|
||||
}
|
||||
.yt-state {
|
||||
@apply absolute inset-0 flex items-center justify-center p-4 text-center text-sm text-fg-muted;
|
||||
}
|
||||
.yt-spinner {
|
||||
@apply h-8 w-8 animate-spin rounded-full border-2 border-line border-t-accent;
|
||||
}
|
||||
.yt-info {
|
||||
@apply flex flex-col items-start gap-3;
|
||||
}
|
||||
.yt-title {
|
||||
@apply text-lg font-semibold leading-snug tracking-tight text-fg;
|
||||
}
|
||||
.yt-stats {
|
||||
@apply flex flex-wrap gap-4 text-xs text-fg-subtle;
|
||||
}
|
||||
|
||||
/* AI Notice */
|
||||
.ai-notice-card {
|
||||
@apply mb-12 mt-12 bg-white rounded-2xl border border-indigo-100 p-6 shadow-sm flex items-center gap-4;
|
||||
}
|
||||
.ai-notice-icon {
|
||||
@apply w-12 h-12 rounded-full bg-indigo-50 flex items-center justify-center text-2xl flex-shrink-0;
|
||||
}
|
||||
.ai-notice-text {
|
||||
@apply flex-grow text-slate-600 text-sm md:text-base italic;
|
||||
/* ---- Spotify ----
|
||||
A beágyazás a saját sötét felületét hozza; keretbe tesszük, hogy világos
|
||||
témában is szándékosnak látsszon, ne foltnak. */
|
||||
.spotify-embed {
|
||||
@apply block overflow-hidden rounded-card border border-line bg-raised;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,36 +1,27 @@
|
||||
<template>
|
||||
<div class="howtos-container">
|
||||
<header class="hero-section-slate">
|
||||
<div class="howtos-header-decor">
|
||||
<div class="hero-decor-blob -top-24 -left-24 h-96 w-96 bg-indigo-600"></div>
|
||||
<div class="hero-decor-blob -bottom-24 -right-24 h-96 w-96 bg-purple-600"></div>
|
||||
</div>
|
||||
|
||||
<header class="hero-section">
|
||||
<div class="hero-container">
|
||||
<div class="hero-badge">{{ t('howtos.badge') }}</div>
|
||||
<h1 class="hero-title">
|
||||
Tech <span class="text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 to-purple-400">HowTo Center</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle mb-10">{{ t('howtos.subtitle') }}</p>
|
||||
<a :href="WIKI_BASE" target="_blank" class="btn-hero-indigo">
|
||||
<i class="fa-solid fa-book mr-2"></i>{{ t('howtos.knowledgeBase') }}
|
||||
</a>
|
||||
<p class="hero-badge">{{ t('howtos.badge') }}</p>
|
||||
<h1 class="hero-title">{{ t('howtos.title') }}</h1>
|
||||
<p class="hero-subtitle">{{ t('howtos.subtitle') }}</p>
|
||||
<div class="hero-actions">
|
||||
<a :href="WIKI_BASE" target="_blank" rel="noopener noreferrer" class="btn-accent">
|
||||
{{ t('howtos.knowledgeBase') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container">
|
||||
<div v-if="error" class="error-banner" role="alert">
|
||||
<i class="fa-solid fa-triangle-exclamation text-2xl text-yellow-500"></i>
|
||||
<div>
|
||||
<h3 class="error-banner-title">{{ t('howtos.errorTitle') }}</h3>
|
||||
<p class="error-banner-desc">{{ error }}</p>
|
||||
</div>
|
||||
<h2 class="error-banner-title">{{ t('howtos.errorTitle') }}</h2>
|
||||
<p class="error-banner-desc">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<SkeletonCard v-else-if="loading" :count="4" />
|
||||
|
||||
<div v-else-if="recentPages.length === 0" class="empty-state">
|
||||
<div class="empty-state-icon"><i class="fa-solid fa-inbox"></i></div>
|
||||
<h2 class="empty-state-title">{{ t('howtos.noPagesTitle') }}</h2>
|
||||
<p class="empty-state-desc">{{ t('howtos.noPagesDesc') }}</p>
|
||||
</div>
|
||||
@@ -41,34 +32,26 @@
|
||||
:key="page.id"
|
||||
:href="`${WIKI_BASE}/${page.path}`"
|
||||
target="_blank"
|
||||
class="group card-interactive flex flex-col"
|
||||
rel="noopener noreferrer"
|
||||
class="howto-card"
|
||||
>
|
||||
<div class="howto-card-meta">
|
||||
<div class="howto-card-timestamp">
|
||||
<span v-if="isNew(page.createdAt)" class="new-badge">
|
||||
<span class="new-dot"></span>
|
||||
{{ t('howtos.new') }}
|
||||
</span>
|
||||
<span><i class="fa-solid fa-calendar text-gray-300 mr-1"></i>{{ formatDateTime(page.updatedAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="howto-card-meta">
|
||||
<span class="mono">{{ formatDateTime(page.updatedAt) }}</span>
|
||||
<span v-if="isNew(page.createdAt)" class="badge-accent">{{ t('howtos.new') }}</span>
|
||||
</p>
|
||||
|
||||
<h2 class="howto-card-title">{{ page.title }}</h2>
|
||||
|
||||
<p v-if="page.description" class="howto-card-desc">{{ page.description }}</p>
|
||||
<div v-else class="howto-card-no-desc">{{ t('howtos.noDesc') }}</div>
|
||||
<p v-else class="howto-card-no-desc">{{ t('howtos.noDesc') }}</p>
|
||||
|
||||
<div class="howto-card-footer">
|
||||
<span class="howto-card-path"><i class="fa-solid fa-file-lines mr-1 opacity-50"></i>{{ page.path }}</span>
|
||||
<span class="howto-card-read-link">{{ t('howtos.read') }} <i class="fa-solid fa-arrow-right text-sm"></i></span>
|
||||
</div>
|
||||
<p class="howto-card-footer">{{ page.path }}</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="!error && recentPages.length > 0" class="howtos-footer">
|
||||
<a :href="WIKI_BASE" target="_blank" class="browse-wiki-btn">
|
||||
<a :href="WIKI_BASE" target="_blank" rel="noopener noreferrer" class="link text-sm">
|
||||
{{ t('howtos.browseWiki') }}
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
@@ -95,57 +78,30 @@ onMounted(() => store.fetch())
|
||||
|
||||
<style scoped>
|
||||
.howtos-container {
|
||||
@apply bg-gray-50 min-h-screen pb-20;
|
||||
@apply min-h-screen bg-page;
|
||||
}
|
||||
.howtos-header-decor {
|
||||
@apply absolute inset-0 opacity-30;
|
||||
.howto-card {
|
||||
@apply flex flex-col gap-2 rounded-card border border-line bg-surface p-5 transition-colors hover:border-line-strong;
|
||||
}
|
||||
.banner-base {
|
||||
@apply max-w-7xl mx-auto border-l-4 p-6 rounded-r-xl shadow-lg mb-12 flex items-start gap-4;
|
||||
}
|
||||
.error-banner { @apply banner-base bg-red-50 border-red-500; }
|
||||
.error-banner-title { @apply text-red-800 font-bold text-lg; }
|
||||
.error-banner-desc { @apply text-red-700 mt-1; }
|
||||
.empty-state {
|
||||
@apply max-w-7xl mx-auto bg-white rounded-2xl shadow-xl p-12 text-center border border-gray-100;
|
||||
}
|
||||
.empty-state-icon { @apply text-6xl mb-4; }
|
||||
.empty-state-title { @apply text-2xl font-bold text-gray-900 mb-2; }
|
||||
.empty-state-desc { @apply text-gray-500 max-w-md mx-auto; }
|
||||
.howto-card-meta {
|
||||
@apply flex items-center justify-end mb-4;
|
||||
}
|
||||
.howto-card-timestamp {
|
||||
@apply flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-gray-400;
|
||||
}
|
||||
.new-badge {
|
||||
@apply flex items-center gap-1 text-green-600 bg-green-50 px-2 py-0.5 rounded-full border border-green-100;
|
||||
}
|
||||
.new-dot {
|
||||
@apply w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse;
|
||||
@apply flex flex-wrap items-center gap-3 text-xs text-fg-subtle;
|
||||
}
|
||||
.howto-card-title {
|
||||
@apply text-xl font-bold text-gray-900 group-hover:text-indigo-600 transition-colors mb-2 line-clamp-2 leading-tight;
|
||||
@apply line-clamp-2 text-base font-semibold leading-snug tracking-tight text-fg;
|
||||
}
|
||||
.howto-card:hover .howto-card-title {
|
||||
@apply text-accent;
|
||||
}
|
||||
.howto-card-desc {
|
||||
@apply text-gray-600 text-sm mb-4 line-clamp-3 leading-relaxed flex-grow;
|
||||
@apply line-clamp-3 flex-grow text-sm leading-relaxed text-fg-muted;
|
||||
}
|
||||
.howto-card-no-desc {
|
||||
@apply flex-grow mb-4 italic text-gray-400 text-sm;
|
||||
@apply flex-grow text-sm italic text-fg-subtle;
|
||||
}
|
||||
.howto-card-footer {
|
||||
@apply pt-4 border-t border-gray-100 mt-auto flex items-center justify-between;
|
||||
}
|
||||
.howto-card-path {
|
||||
@apply text-xs font-mono text-gray-400 truncate max-w-[180px];
|
||||
}
|
||||
.howto-card-read-link {
|
||||
@apply text-indigo-600 text-sm font-bold flex items-center gap-1 group-hover:translate-x-1 transition-transform;
|
||||
@apply mt-auto truncate border-t border-line pt-3 font-mono text-xs text-fg-subtle;
|
||||
}
|
||||
.howtos-footer {
|
||||
@apply mt-16 text-center;
|
||||
}
|
||||
.browse-wiki-btn {
|
||||
@apply inline-flex items-center gap-2 bg-slate-900 text-white px-8 py-4 rounded-xl hover:bg-slate-800 transition-all font-bold shadow-lg hover:shadow-indigo-200/50;
|
||||
@apply mt-10;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<div class="cb">
|
||||
<pre><code>{{ command }}</code></pre>
|
||||
<button class="cb-copy" :title="t('stores.copy')" @click="copy">
|
||||
<i :class="copied ? 'fa-solid fa-check' : 'fa-regular fa-copy'"></i>
|
||||
<button class="cb-copy" :aria-label="t('stores.copy')" :title="t('stores.copy')" @click="copy">
|
||||
<Check v-if="copied" v-bind="ICON" />
|
||||
<Copy v-else v-bind="ICON" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -12,6 +13,8 @@
|
||||
// block and its copy button live in one place rather than being repeated.
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Check, Copy } from 'lucide-vue-next'
|
||||
import { ICON } from '../../components/icons'
|
||||
|
||||
const { command } = defineProps<{ command: string }>()
|
||||
const { t } = useI18n()
|
||||
@@ -30,7 +33,7 @@ async function copy() {
|
||||
|
||||
<style scoped>
|
||||
.cb {
|
||||
@apply relative block bg-gray-900 text-gray-100 rounded-xl overflow-x-auto;
|
||||
@apply relative block overflow-x-auto rounded-card border border-line bg-raised text-fg;
|
||||
}
|
||||
/* Explicitly margin-free: a `pre` carries a browser margin of its own, and the block is
|
||||
spaced by whatever contains it — otherwise what follows ends up against it. */
|
||||
@@ -38,6 +41,8 @@ async function copy() {
|
||||
@apply m-0 p-4 pr-14 font-mono text-sm leading-relaxed;
|
||||
}
|
||||
.cb-copy {
|
||||
@apply absolute top-2 right-2 w-9 h-9 rounded-lg bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors;
|
||||
@apply absolute right-2 top-2 rounded-control border border-line bg-surface p-2 text-fg-muted
|
||||
transition-colors hover:text-fg
|
||||
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<header class="hero-section-gradient from-emerald-700 to-teal-800 py-10">
|
||||
<header class="hero-section">
|
||||
<div class="hero-container">
|
||||
<h1 class="hero-title">{{ t('stores.title') }}</h1>
|
||||
<p class="hero-subtitle text-emerald-50">{{ t('stores.subtitle') }}</p>
|
||||
<p class="hero-subtitle">{{ t('stores.subtitle') }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container py-8 px-4 mt-0">
|
||||
<main class="main-container">
|
||||
<!-- Two ways in, and almost everybody wants the first one. A thin switcher rather
|
||||
than a page of prose: whoever needs the detail follows a link to the wiki. -->
|
||||
<nav class="st-views" role="tablist" :aria-label="t('stores.title')">
|
||||
@@ -14,44 +14,41 @@
|
||||
class="st-view" :class="{ 'st-view-active': view === 'client' }"
|
||||
role="tab" :aria-selected="view === 'client'" @click="selectView('client')"
|
||||
>
|
||||
<i class="fa-solid fa-window-maximize"></i> {{ t('stores.viewClient') }}
|
||||
{{ t('stores.viewClient') }}
|
||||
</button>
|
||||
<button
|
||||
class="st-view" :class="{ 'st-view-active': view === 'others' }"
|
||||
role="tab" :aria-selected="view === 'others'" @click="selectView('others')"
|
||||
>
|
||||
<i class="fa-solid fa-terminal"></i> {{ t('stores.viewOthers') }}
|
||||
{{ t('stores.viewOthers') }}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<section v-if="view === 'client'" class="st-section st-app">
|
||||
<section v-if="view === 'client'" class="st-section">
|
||||
<div class="st-app-grid">
|
||||
<div>
|
||||
<p class="st-app-lead">{{ t('stores.clientDesc') }}</p>
|
||||
<div class="st-links">
|
||||
<a
|
||||
:href="client.releasesUrl" target="_blank" rel="noopener noreferrer"
|
||||
class="st-link st-link-cta"
|
||||
>
|
||||
<i class="fa-solid fa-download"></i> {{ t('stores.clientDownload') }}
|
||||
<a :href="client.releasesUrl" target="_blank" rel="noopener noreferrer" class="btn-accent">
|
||||
{{ t('stores.clientDownload') }}
|
||||
</a>
|
||||
<a :href="client.wikiUrl" target="_blank" rel="noopener noreferrer" class="st-link st-link-indigo">
|
||||
<i class="fa-solid fa-book"></i> {{ t('stores.docs') }}
|
||||
<a :href="client.wikiUrl" target="_blank" rel="noopener noreferrer" class="btn-ghost">
|
||||
{{ t('stores.docs') }}
|
||||
</a>
|
||||
<a :href="client.repoUrl" target="_blank" rel="noopener noreferrer" class="st-link st-link-dark">
|
||||
<i class="fa-solid fa-code-branch"></i> {{ t('stores.repo') }}
|
||||
<a :href="client.repoUrl" target="_blank" rel="noopener noreferrer" class="btn-ghost">
|
||||
{{ t('stores.repo') }}
|
||||
</a>
|
||||
</div>
|
||||
<p class="st-app-platforms">
|
||||
<i class="fa-brands fa-linux"></i> <i class="fa-brands fa-apple"></i>
|
||||
<i class="fa-brands fa-windows"></i> {{ t('stores.clientPlatforms') }}
|
||||
<BrandIcon name="linux" /><BrandIcon name="apple" /><BrandIcon name="windows" />
|
||||
{{ t('stores.clientPlatforms') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Eager, with its dimensions given: it sits in the first screen, so lazy
|
||||
loading it would only buy a reflow. -->
|
||||
<figure class="st-app-shot">
|
||||
<img :src="clientScreenshot" :alt="t('stores.clientShotAlt')" width="2304" height="1664">
|
||||
<img :src="clientScreenshot" :alt="t('stores.clientShotAlt')" width="1536" height="1110">
|
||||
</figure>
|
||||
</div>
|
||||
</section>
|
||||
@@ -67,7 +64,6 @@
|
||||
:aria-selected="d.id === device"
|
||||
@click="select(d.id)"
|
||||
>
|
||||
<i :class="d.icon" class="st-device-icon"></i>
|
||||
<span class="st-device-name">{{ t(`stores.${d.id}.name`) }}</span>
|
||||
<span class="st-device-tagline">{{ t(`stores.${d.id}.tagline`) }}</span>
|
||||
</button>
|
||||
@@ -92,14 +88,14 @@
|
||||
</div>
|
||||
|
||||
<div class="st-links st-links-after">
|
||||
<a :href="current.wikiUrl" target="_blank" rel="noopener noreferrer" class="st-link st-link-indigo">
|
||||
<i class="fa-solid fa-book"></i> {{ t('stores.docs') }}
|
||||
<a :href="current.wikiUrl" target="_blank" rel="noopener noreferrer" class="btn-ghost">
|
||||
{{ t('stores.docs') }}
|
||||
</a>
|
||||
<a :href="current.repoUrl" target="_blank" rel="noopener noreferrer" class="st-link st-link-dark">
|
||||
<i class="fa-solid fa-code-branch"></i> {{ t('stores.repo') }}
|
||||
<a :href="current.repoUrl" target="_blank" rel="noopener noreferrer" class="btn-ghost">
|
||||
{{ t('stores.repo') }}
|
||||
</a>
|
||||
<a :href="current.projectUrl" target="_blank" rel="noopener noreferrer" class="st-link st-link-emerald">
|
||||
<i :class="current.icon"></i> {{ t(`stores.${device}.site`) }}
|
||||
<a :href="current.projectUrl" target="_blank" rel="noopener noreferrer" class="btn-ghost">
|
||||
{{ t(`stores.${device}.site`) }}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
@@ -111,15 +107,15 @@
|
||||
<h2 class="st-section-title">{{ t('stores.platformsTitle') }}</h2>
|
||||
<ul class="st-platforms">
|
||||
<li v-for="p in platforms" :key="p.platform" class="st-platform">
|
||||
<i :class="p.icon" class="st-platform-icon"></i>
|
||||
<span class="st-platform-name">{{ p.label }}</span>
|
||||
<code class="st-platform-ext">{{ p.ext }}</code>
|
||||
<code class="st-platform-ext mono">{{ p.ext }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<div class="st-back">
|
||||
<RouterLink to="/catalog" class="st-back-link">{{ t('catalogShow.back') }}</RouterLink>
|
||||
<RouterLink to="/catalog" class="link text-sm">{{ t('catalogShow.back') }}</RouterLink>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
@@ -130,7 +126,8 @@ import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CONFIG } from '../../lib/config'
|
||||
import CommandBlock from './CommandBlock.vue'
|
||||
import clientScreenshot from '../../assets/warpengine-client.png'
|
||||
import { BrandIcon } from '../../components/icons'
|
||||
import clientScreenshot from '../../assets/warpengine-client.webp'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
@@ -158,7 +155,6 @@ const RETROARCH_CLI = '~/.local/bin/ttg-retroarch-store'
|
||||
const devices = [
|
||||
{
|
||||
id: 'batocera' as DeviceId,
|
||||
icon: 'fa-solid fa-tv',
|
||||
projectUrl: 'https://batocera.org',
|
||||
repoUrl: `${FORGE}/ttg-batocera-store`,
|
||||
wikiUrl: `${CONFIG.wikiBase}/stores/ttg-batocera-store`,
|
||||
@@ -175,7 +171,6 @@ const devices = [
|
||||
},
|
||||
{
|
||||
id: 'retroarch' as DeviceId,
|
||||
icon: 'fa-solid fa-gamepad',
|
||||
projectUrl: 'https://www.retroarch.com',
|
||||
repoUrl: `${FORGE}/ttg-retroarch-store`,
|
||||
wikiUrl: `${CONFIG.wikiBase}/stores/ttg-retroarch-store`,
|
||||
@@ -224,8 +219,8 @@ function selectView(id: ViewId) {
|
||||
}
|
||||
|
||||
const platforms = [
|
||||
{ platform: 'c64', label: 'Commodore 64 (VICE)', ext: '.prg', icon: 'fa-solid fa-floppy-disk' },
|
||||
{ platform: 'tic80', label: 'TIC-80', ext: '.tic', icon: 'fa-solid fa-tv' },
|
||||
{ platform: 'c64', label: 'Commodore 64 (VICE)', ext: '.prg' },
|
||||
{ platform: 'tic80', label: 'TIC-80', ext: '.tic' },
|
||||
]
|
||||
</script>
|
||||
|
||||
@@ -233,51 +228,43 @@ const platforms = [
|
||||
.st-links {
|
||||
@apply flex flex-wrap gap-3;
|
||||
}
|
||||
.st-link {
|
||||
@apply inline-flex items-center gap-2 font-bold py-2.5 px-5 rounded-xl text-sm text-white transition-all active:scale-95;
|
||||
}
|
||||
.st-link-dark { @apply bg-slate-800 hover:bg-slate-900 shadow-lg shadow-slate-800/20; }
|
||||
.st-link-indigo { @apply bg-indigo-600 hover:bg-indigo-700 shadow-lg shadow-indigo-600/20; }
|
||||
.st-link-emerald { @apply bg-emerald-600 hover:bg-emerald-700 shadow-lg shadow-emerald-600/20; }
|
||||
|
||||
.st-section {
|
||||
@apply bg-white rounded-2xl shadow-xl border border-gray-100 p-6 md:p-8 mt-6;
|
||||
@apply mt-6 rounded-card border border-line bg-surface p-5 md:p-8;
|
||||
}
|
||||
.st-section-title {
|
||||
@apply flex items-center gap-3 text-xl font-bold text-gray-900 mb-2;
|
||||
@apply mb-3 text-lg font-semibold tracking-tight text-fg;
|
||||
}
|
||||
|
||||
.st-devices {
|
||||
@apply grid grid-cols-1 sm:grid-cols-3 gap-3 mb-6;
|
||||
@apply mb-6 grid grid-cols-1 gap-3 sm:grid-cols-3;
|
||||
}
|
||||
.st-device {
|
||||
@apply flex flex-col items-start gap-1 text-left bg-gray-50 border-2 border-gray-100 rounded-xl px-4 py-3 transition-all hover:border-emerald-200 active:scale-95;
|
||||
@apply flex flex-col items-start gap-0.5 rounded-card border border-line bg-surface px-4 py-3 text-left transition-colors hover:border-line-strong
|
||||
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent;
|
||||
}
|
||||
.st-device-active {
|
||||
@apply bg-emerald-50 border-emerald-500;
|
||||
}
|
||||
.st-device-icon {
|
||||
@apply text-gray-400;
|
||||
}
|
||||
.st-device-active .st-device-icon {
|
||||
@apply text-emerald-600;
|
||||
@apply border-accent bg-accent-quiet;
|
||||
}
|
||||
.st-device-name {
|
||||
@apply font-bold text-gray-900;
|
||||
@apply font-medium text-fg;
|
||||
}
|
||||
.st-device-tagline {
|
||||
@apply text-sm text-gray-500;
|
||||
@apply text-sm text-fg-muted;
|
||||
}
|
||||
.st-device-active .st-device-tagline {
|
||||
@apply text-accent;
|
||||
}
|
||||
|
||||
/* The switcher is meant to be almost nothing: two words and a line under the active one. */
|
||||
.st-views {
|
||||
@apply flex gap-1 border-b border-gray-200 mb-6;
|
||||
@apply mb-6 flex gap-1 border-b border-line;
|
||||
}
|
||||
.st-view {
|
||||
@apply flex items-center gap-2 px-4 py-2 -mb-px text-sm font-semibold text-gray-500 border-b-2 border-transparent transition-colors hover:text-gray-800;
|
||||
@apply -mb-px border-b-2 border-transparent px-4 py-2 text-sm font-medium text-fg-muted transition-colors hover:text-fg
|
||||
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent;
|
||||
}
|
||||
.st-view-active {
|
||||
@apply text-emerald-700 border-emerald-500;
|
||||
@apply border-accent text-fg;
|
||||
}
|
||||
/* One rhythm for the whole command list, so nothing depends on a margin someone
|
||||
remembered to add. */
|
||||
@@ -285,59 +272,39 @@ const platforms = [
|
||||
@apply space-y-2;
|
||||
}
|
||||
.st-cmd-label {
|
||||
@apply text-xs font-semibold uppercase tracking-wide text-gray-400 pt-3;
|
||||
}
|
||||
.st-cmd-label:first-child {
|
||||
@apply pt-0;
|
||||
@apply pt-3 font-mono text-xs uppercase tracking-[0.1em] text-fg-subtle first:pt-0;
|
||||
}
|
||||
.st-links-after {
|
||||
@apply mt-6;
|
||||
}
|
||||
.st-engine-link {
|
||||
@apply text-indigo-600 hover:underline font-mono text-xs;
|
||||
}
|
||||
/* The app leads the page, so it is the one section that looks like an offer rather
|
||||
than a paragraph. */
|
||||
.st-app {
|
||||
@apply bg-gradient-to-br from-emerald-50 to-teal-50 border-emerald-200;
|
||||
}
|
||||
.st-app-lead {
|
||||
@apply text-gray-700 text-lg leading-relaxed mb-4;
|
||||
@apply mb-4 text-base leading-relaxed text-fg-muted;
|
||||
}
|
||||
.st-app-platforms {
|
||||
@apply flex items-center gap-2 text-sm text-gray-500 mt-4;
|
||||
@apply mt-4 flex items-center gap-2 text-sm text-fg-subtle;
|
||||
}
|
||||
.st-app-grid {
|
||||
@apply grid gap-6 items-start lg:grid-cols-2;
|
||||
@apply grid items-start gap-6 lg:grid-cols-2;
|
||||
}
|
||||
.st-app-shot {
|
||||
@apply rounded-xl overflow-hidden border border-emerald-200/70 shadow-lg bg-gray-900;
|
||||
@apply m-0 overflow-hidden rounded-card border border-line;
|
||||
}
|
||||
.st-app-shot img {
|
||||
@apply block w-full h-auto;
|
||||
}
|
||||
.st-link-cta {
|
||||
@apply bg-emerald-600 text-white hover:bg-emerald-700 text-base px-5 py-2.5 font-semibold;
|
||||
@apply block h-auto w-full;
|
||||
}
|
||||
.st-platforms {
|
||||
@apply grid grid-cols-1 sm:grid-cols-2 gap-3;
|
||||
@apply grid grid-cols-1 gap-3 sm:grid-cols-2;
|
||||
}
|
||||
.st-platform {
|
||||
@apply flex items-center gap-3 bg-gray-50 border border-gray-100 rounded-xl px-4 py-3;
|
||||
}
|
||||
.st-platform-icon {
|
||||
@apply text-gray-400;
|
||||
@apply flex items-center gap-3 rounded-card border border-line bg-raised px-4 py-3;
|
||||
}
|
||||
.st-platform-name {
|
||||
@apply font-medium text-gray-800 flex-grow;
|
||||
@apply flex-grow text-sm text-fg;
|
||||
}
|
||||
.st-platform-ext {
|
||||
@apply font-mono text-xs text-purple-600 bg-purple-50 px-2 py-0.5 rounded;
|
||||
@apply rounded border border-line bg-surface px-2 py-0.5 text-xs text-accent;
|
||||
}
|
||||
.st-back {
|
||||
@apply mt-8 text-center;
|
||||
}
|
||||
.st-back-link {
|
||||
@apply text-gray-400 hover:text-gray-600 text-sm font-medium transition-colors;
|
||||
@apply mt-8;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
<template>
|
||||
<header class="hero-section-gradient from-purple-600 to-blue-700 py-20 text-white">
|
||||
<div class="hero-container text-center">
|
||||
<h1 class="hero-title mb-6">{{ t('team.title') }}</h1>
|
||||
<p class="hero-subtitle mb-10 text-purple-100">{{ t('team.subtitle') }}</p>
|
||||
<header class="hero-section">
|
||||
<div class="hero-container">
|
||||
<h1 class="hero-title">{{ t('team.title') }}</h1>
|
||||
<p class="hero-subtitle">{{ t('team.subtitle') }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-container py-16 px-4">
|
||||
<main class="main-container">
|
||||
<SkeletonCard v-if="loading" :count="4" />
|
||||
|
||||
<div v-else class="team-grid">
|
||||
<div v-for="member in members" :key="member.nick" class="team-card">
|
||||
<div class="team-card-image-container">
|
||||
<img :src="resolvedAvatarUrl(member)" :alt="member.nick" class="team-card-image" />
|
||||
</div>
|
||||
<div class="team-card-content">
|
||||
<h2 class="team-card-title">{{ member.nick }}: {{ member.realNick }}</h2>
|
||||
<p class="team-card-desc">{{ member.motto }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<article v-for="member in members" :key="member.nick" class="team-card">
|
||||
<img :src="resolvedAvatarUrl(member)" :alt="member.nick" class="team-avatar" width="512" height="512" loading="lazy" />
|
||||
<h2 class="team-name">{{ member.nick }}: {{ member.realNick }}</h2>
|
||||
<p class="team-motto">{{ member.motto }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
@@ -41,24 +37,18 @@ onMounted(() => store.fetch())
|
||||
|
||||
<style scoped>
|
||||
.team-grid {
|
||||
@apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8 max-w-7xl mx-auto;
|
||||
@apply grid grid-cols-2 gap-8 lg:grid-cols-4;
|
||||
}
|
||||
.team-card {
|
||||
@apply bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-100 transition-all hover:scale-105;
|
||||
@apply flex flex-col items-center text-center;
|
||||
}
|
||||
.team-card-image-container {
|
||||
@apply aspect-square overflow-hidden bg-gray-200;
|
||||
.team-avatar {
|
||||
@apply mb-4 h-28 w-28 rounded-full border border-line bg-raised object-cover;
|
||||
}
|
||||
.team-card-image {
|
||||
@apply w-full h-full object-cover;
|
||||
.team-name {
|
||||
@apply text-base font-semibold tracking-tight text-fg;
|
||||
}
|
||||
.team-card-content {
|
||||
@apply p-6;
|
||||
}
|
||||
.team-card-title {
|
||||
@apply text-xl font-black text-gray-900 mb-2;
|
||||
}
|
||||
.team-card-desc {
|
||||
@apply text-gray-600 leading-relaxed;
|
||||
.team-motto {
|
||||
@apply mt-1 text-sm leading-relaxed text-fg-muted;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,6 +23,30 @@ describe('useUiStore', () => {
|
||||
expect(store.isRetroMode).toBe(false)
|
||||
})
|
||||
|
||||
it('starts in the light theme when no cookie is set', () => {
|
||||
const store = useUiStore()
|
||||
expect(store.theme).toBe('light')
|
||||
expect(document.documentElement.getAttribute('data-theme')).toBe('light')
|
||||
})
|
||||
|
||||
it('reads the theme back from the cookie', () => {
|
||||
document.cookie = 'theme=dark;path=/'
|
||||
const store = useUiStore()
|
||||
expect(store.theme).toBe('dark')
|
||||
expect(document.documentElement.getAttribute('data-theme')).toBe('dark')
|
||||
})
|
||||
|
||||
it('toggling the theme writes the cookie and the root attribute', () => {
|
||||
const store = useUiStore()
|
||||
store.toggleTheme()
|
||||
expect(store.theme).toBe('dark')
|
||||
expect(document.cookie).toContain('theme=dark')
|
||||
expect(document.documentElement.getAttribute('data-theme')).toBe('dark')
|
||||
store.toggleTheme()
|
||||
expect(store.theme).toBe('light')
|
||||
expect(document.cookie).toContain('theme=light')
|
||||
})
|
||||
|
||||
it('toggles menu', () => {
|
||||
const store = useUiStore()
|
||||
expect(store.menuOpen).toBe(false)
|
||||
|
||||
@@ -14,9 +14,13 @@ export const useMemberStore = defineStore('member', () => {
|
||||
})
|
||||
}
|
||||
|
||||
// A tartalék avatarok a bundle-ben WebP-ként élnek (a PNG-k fejenként ~1,6 MB-ot
|
||||
// vittek egy ~300 px-es képért), a fájlnév viszont az adatbázisból jön, még
|
||||
// .png végződéssel — ezért itt cseréljük a kiterjesztést.
|
||||
function resolvedAvatarUrl(member: Member): string {
|
||||
if (member.imageUrl) return member.imageUrl
|
||||
return new URL(`../assets/team/${member.avatarFilename}`, import.meta.url).href
|
||||
const file = member.avatarFilename.replace(/\.(png|jpe?g)$/i, '.webp')
|
||||
return new URL(`../assets/team/${file}`, import.meta.url).href
|
||||
}
|
||||
|
||||
return { members, loading, error, fetch, resolvedAvatarUrl, invalidate }
|
||||
|
||||
@@ -2,13 +2,68 @@ import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { getCookie, setCookie } from '../lib/cookie'
|
||||
|
||||
export type Theme = 'light' | 'dark'
|
||||
|
||||
const THEME_COOKIE = 'theme'
|
||||
|
||||
// Süti nélkül világos: az alapértelmezés legyen kiszámítható, ezért a
|
||||
// prefers-color-scheme-et szándékosan nem nézzük.
|
||||
function readTheme(): Theme {
|
||||
return getCookie(THEME_COOKIE) === 'dark' ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme)
|
||||
}
|
||||
|
||||
// A retro mód a gyökéren is jelöli magát: a body háttere a tokenekből jön, és a
|
||||
// retro.css ott írja felül őket. A .retro-mode osztály az app gyökerén marad,
|
||||
// az viszi a formát (betűtípus, keretek).
|
||||
function applyRetro(on: boolean) {
|
||||
document.documentElement.setAttribute('data-retro', on ? '1' : '0')
|
||||
}
|
||||
|
||||
// A TuiCSS és a retro.css együtt ~500 KB, és a látogatók töredéke kapcsolja be
|
||||
// a retro módot — ezért csak akkor töltjük be, amikor tényleg kell.
|
||||
let retroStyles: Promise<unknown> | null = null
|
||||
|
||||
function loadRetroStyles(): Promise<unknown> {
|
||||
if (!retroStyles) {
|
||||
retroStyles = Promise.all([
|
||||
import('tuicss/dist/tuicss.min.css'),
|
||||
import('../styles/retro.css'),
|
||||
])
|
||||
}
|
||||
return retroStyles
|
||||
}
|
||||
|
||||
export const useUiStore = defineStore('ui', () => {
|
||||
const isRetroMode = ref(getCookie('retro-mode') === '1')
|
||||
const theme = ref<Theme>(readTheme())
|
||||
const menuOpen = ref(false)
|
||||
|
||||
// Az index.html inline scriptje már beállította ugyanezt; itt csak azért
|
||||
// ismételjük meg, hogy a store maradjon az állapot egyetlen gazdája.
|
||||
applyTheme(theme.value)
|
||||
applyRetro(isRetroMode.value)
|
||||
|
||||
if (isRetroMode.value) loadRetroStyles()
|
||||
|
||||
function setTheme(next: Theme) {
|
||||
theme.value = next
|
||||
setCookie(THEME_COOKIE, next)
|
||||
applyTheme(next)
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
setTheme(theme.value === 'dark' ? 'light' : 'dark')
|
||||
}
|
||||
|
||||
function toggleRetroMode() {
|
||||
isRetroMode.value = !isRetroMode.value
|
||||
setCookie('retro-mode', isRetroMode.value ? '1' : '0')
|
||||
applyRetro(isRetroMode.value)
|
||||
if (isRetroMode.value) loadRetroStyles()
|
||||
window.scrollTo({ top: 0, behavior: 'instant' })
|
||||
}
|
||||
|
||||
@@ -20,5 +75,5 @@ export const useUiStore = defineStore('ui', () => {
|
||||
menuOpen.value = false
|
||||
}
|
||||
|
||||
return { isRetroMode, menuOpen, toggleRetroMode, toggleMenu, closeMenu }
|
||||
return { isRetroMode, theme, menuOpen, setTheme, toggleTheme, toggleRetroMode, toggleMenu, closeMenu }
|
||||
})
|
||||
|
||||
@@ -9,95 +9,184 @@ body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Hero Section */
|
||||
.hero-section {
|
||||
@apply relative overflow-hidden py-24 text-white text-center;
|
||||
}
|
||||
/* A felület alapja tokenből jön, hogy a témaváltás egyetlen attribútumon
|
||||
múljon — lásd src/styles/tokens.css. */
|
||||
body {
|
||||
background-color: rgb(var(--c-page));
|
||||
color: rgb(var(--c-fg));
|
||||
}
|
||||
|
||||
.hero-section-gradient {
|
||||
@apply hero-section bg-gradient-to-r;
|
||||
}
|
||||
|
||||
.hero-section-slate {
|
||||
@apply hero-section bg-slate-900;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
@apply relative max-w-5xl mx-auto px-4 z-10;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
@apply inline-block px-3 py-1 rounded-full bg-indigo-500/20 border border-indigo-400/30 text-indigo-300 text-sm font-medium mb-6 backdrop-blur-sm;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
@apply text-5xl md:text-6xl font-extrabold tracking-tight mb-6;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
@apply text-lg md:text-xl text-slate-300 max-w-2xl mx-auto leading-relaxed;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-hero {
|
||||
@apply inline-flex items-center gap-3 bg-white px-8 py-4 rounded-xl transition-all font-bold shadow-xl hover:scale-105 active:scale-95;
|
||||
}
|
||||
|
||||
.btn-hero-teal {
|
||||
@apply btn-hero text-teal-700 hover:bg-teal-50;
|
||||
}
|
||||
|
||||
.btn-hero-indigo {
|
||||
@apply btn-hero text-indigo-900 hover:bg-indigo-50;
|
||||
}
|
||||
|
||||
.btn-hero-purple {
|
||||
@apply btn-hero text-purple-700 hover:bg-purple-50;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply inline-flex items-center justify-center font-bold py-3 px-8 rounded-xl transition-all shadow-lg;
|
||||
}
|
||||
|
||||
/* Grid and Containers */
|
||||
.main-container {
|
||||
@apply max-w-7xl mx-auto px-4 -mt-12 relative z-10;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card-base {
|
||||
@apply bg-white rounded-2xl border border-gray-200 p-6 shadow-sm;
|
||||
}
|
||||
|
||||
.card-interactive {
|
||||
@apply bg-white rounded-2xl border border-gray-200 p-6 shadow-sm hover:shadow-xl transition-all duration-300 ease-out hover:-translate-y-1;
|
||||
}
|
||||
|
||||
/* Decorative Background Elements */
|
||||
.hero-decor-blob {
|
||||
@apply absolute rounded-full blur-3xl opacity-30;
|
||||
/* A témaváltás nem animált: a keresztfade olcsó trükknek látszik, és a
|
||||
sok elem egyszerre úszó színe zajos. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Wiki content styles (used with v-html) */
|
||||
.wiki-content h1 { @apply text-2xl font-bold mt-8 mb-4 text-gray-900; }
|
||||
.wiki-content h2 { @apply text-xl font-bold mt-6 mb-3 text-gray-800; }
|
||||
.wiki-content h3 { @apply text-lg font-bold mt-4 mb-2 text-gray-800; }
|
||||
.wiki-content p { @apply mb-4 text-gray-700 leading-relaxed; }
|
||||
.wiki-content ul { @apply list-disc ml-6 mb-4 text-gray-700; }
|
||||
.wiki-content ol { @apply list-decimal ml-6 mb-4 text-gray-700; }
|
||||
/* A billentyűzetes fókusz mindenhol látszik — a linkeknek nincs saját
|
||||
komponensosztályuk, ezért az alapot itt kapják. */
|
||||
a:focus-visible,
|
||||
button:focus-visible {
|
||||
outline: 2px solid rgb(var(--c-accent));
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* ---- Elrendezés ---------------------------------------------------- */
|
||||
.main-container {
|
||||
@apply mx-auto w-full max-w-6xl px-4 py-10 md:px-6 md:py-14;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
@apply grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3;
|
||||
}
|
||||
|
||||
/* ---- Hero ----------------------------------------------------------
|
||||
Egy hero minden oldalon: cím, egy alcím, legfeljebb egy gomb. A
|
||||
gradiensek, a blur-blobok és az oldalanként külön akcentszín kiestek. */
|
||||
.hero-section {
|
||||
@apply border-b border-line bg-surface py-10 md:py-14;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
@apply mx-auto w-full max-w-6xl px-4 md:px-6;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
@apply mb-3 inline-block font-mono text-xs uppercase tracking-[0.14em] text-accent;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
@apply text-3xl font-semibold tracking-tight text-fg md:text-5xl;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
@apply mt-3 max-w-2xl text-base leading-relaxed text-fg-muted md:text-lg;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
@apply mt-6 flex flex-wrap items-center gap-3;
|
||||
}
|
||||
|
||||
/* ---- Felületek — a kártyát keret határolja, nem árnyék -------------- */
|
||||
.card {
|
||||
@apply rounded-card border border-line bg-surface p-5 md:p-6;
|
||||
}
|
||||
|
||||
.card-bare {
|
||||
@apply rounded-card border border-line bg-surface;
|
||||
}
|
||||
|
||||
/* ---- Vezérlők ------------------------------------------------------- */
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-control px-4 py-2 text-sm font-medium transition-colors
|
||||
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent;
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
@apply btn bg-accent text-accent-fg hover:bg-accent-hover;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply btn border border-line text-fg hover:border-line-strong hover:bg-raised;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
@apply px-3 py-1.5 text-xs;
|
||||
}
|
||||
|
||||
.link {
|
||||
@apply text-accent underline-offset-4 transition-colors hover:underline;
|
||||
}
|
||||
|
||||
/* ---- Jelölők -------------------------------------------------------- */
|
||||
.badge {
|
||||
@apply inline-flex items-center rounded-control border border-line bg-raised px-2 py-0.5 text-xs font-medium text-fg-muted;
|
||||
}
|
||||
|
||||
.badge-accent {
|
||||
@apply badge border-accent/40 bg-accent-quiet text-accent;
|
||||
}
|
||||
|
||||
/* A négy státusz nem kap négy színcsaládot: a megjelent cím az egyetlen,
|
||||
ami kiemelést érdemel, a többi semleges. */
|
||||
.status-badge {
|
||||
@apply badge uppercase tracking-wide;
|
||||
}
|
||||
|
||||
.status-released {
|
||||
@apply border-accent/40 bg-accent-quiet text-accent;
|
||||
}
|
||||
|
||||
.mono {
|
||||
@apply font-mono tabular-nums;
|
||||
}
|
||||
|
||||
/* ---- Üres és hibaállapotok ------------------------------------------ */
|
||||
.empty-state {
|
||||
@apply rounded-card border border-line bg-surface px-6 py-16 text-center;
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
@apply text-lg font-semibold text-fg;
|
||||
}
|
||||
|
||||
.empty-state-desc {
|
||||
@apply mx-auto mt-2 max-w-md text-sm text-fg-muted;
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
@apply rounded-card border border-line border-l-2 border-l-warn bg-surface px-5 py-4;
|
||||
}
|
||||
|
||||
.error-banner-title {
|
||||
@apply font-semibold text-fg;
|
||||
}
|
||||
|
||||
.error-banner-desc {
|
||||
@apply mt-1 text-sm text-fg-muted;
|
||||
}
|
||||
|
||||
/* ---- Táblázat ------------------------------------------------------- */
|
||||
.table-wrap {
|
||||
@apply overflow-x-auto rounded-card border border-line;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Wiki-tartalom (v-html-lel érkezik a wikiből, ezért nem lehet utility-kkel
|
||||
megcímezni — minden szabály tokenre hivatkozik, hogy mindkét témában álljon)
|
||||
========================================================================== */
|
||||
.wiki-content {
|
||||
@apply max-w-[68ch] text-fg-muted;
|
||||
}
|
||||
|
||||
.wiki-content h1 { @apply mb-4 mt-10 text-2xl font-semibold tracking-tight text-fg; }
|
||||
.wiki-content h2 { @apply mb-3 mt-8 text-xl font-semibold tracking-tight text-fg; }
|
||||
.wiki-content h3 { @apply mb-2 mt-6 text-lg font-semibold text-fg; }
|
||||
.wiki-content p { @apply mb-4 leading-relaxed; }
|
||||
.wiki-content ul { @apply mb-4 ml-5 list-disc; }
|
||||
.wiki-content ol { @apply mb-4 ml-5 list-decimal; }
|
||||
.wiki-content li { @apply mb-1; }
|
||||
.wiki-content a { @apply text-indigo-600 hover:underline; }
|
||||
.wiki-content img { @apply max-w-full h-auto rounded-lg my-6 shadow-md; }
|
||||
.wiki-content pre { @apply bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto my-6 font-mono text-sm; }
|
||||
.wiki-content code { @apply bg-gray-100 text-pink-600 px-1 rounded font-mono text-sm; }
|
||||
.wiki-content blockquote { @apply border-l-4 border-gray-300 pl-4 italic my-6 text-gray-600; }
|
||||
.wiki-content table { @apply w-full border-collapse my-6; }
|
||||
.wiki-content a { @apply text-accent underline-offset-4 hover:underline; }
|
||||
.wiki-content strong { @apply font-semibold text-fg; }
|
||||
.wiki-content img { @apply my-6 h-auto max-w-full rounded-card border border-line; }
|
||||
.wiki-content hr { @apply my-8 border-line; }
|
||||
.wiki-content pre {
|
||||
@apply my-6 overflow-x-auto rounded-card border border-line bg-raised p-4 font-mono text-sm text-fg;
|
||||
}
|
||||
.wiki-content code { @apply rounded border border-line bg-raised px-1 py-0.5 font-mono text-[0.9em] text-fg; }
|
||||
.wiki-content pre code { @apply border-0 bg-transparent p-0; }
|
||||
.wiki-content blockquote { @apply my-6 border-l-2 border-accent pl-4 italic; }
|
||||
.wiki-content table { @apply my-6 w-full border-collapse text-sm; }
|
||||
.wiki-content th,
|
||||
.wiki-content td { @apply border border-gray-200 p-3 text-left; }
|
||||
.wiki-content th { @apply bg-gray-50 font-bold; }
|
||||
.wiki-content td { @apply border border-line px-3 py-2 text-left; }
|
||||
.wiki-content th { @apply bg-raised font-semibold text-fg; }
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/* ===================================================================
|
||||
Design tokenek — a világos és a sötét téma ugyanazokat a szerepeket
|
||||
tölti ki, csak más értékkel. A komponensek soha nem hivatkoznak nyers
|
||||
palettanévre, csak ezekre a szerepekre (Tailwindből: bg-surface,
|
||||
text-fg-muted, border-line, bg-accent, …).
|
||||
|
||||
A témát a <html data-theme="light|dark"> attribútuma választja; azt az
|
||||
index.html inline scriptje állítja be a bundle betöltése előtt, és
|
||||
utána az ui.store tartja karban. Alapértelmezés (süti nélkül): világos.
|
||||
A prefers-color-scheme-et szándékosan nem követjük.
|
||||
|
||||
Az értékek szóközzel elválasztott RGB-csatornák, hogy a Tailwind
|
||||
alfa-variánsai (bg-accent/15) is működjenek.
|
||||
=================================================================== */
|
||||
|
||||
:root {
|
||||
/* felületek */
|
||||
--c-page: 255 255 255;
|
||||
--c-surface: 250 250 250; /* zinc-50 */
|
||||
--c-raised: 244 244 245; /* zinc-100 */
|
||||
|
||||
/* keretek */
|
||||
--c-line: 228 228 231; /* zinc-200 */
|
||||
--c-line-strong: 212 212 216; /* zinc-300 */
|
||||
|
||||
/* szöveg */
|
||||
--c-fg: 24 24 27; /* zinc-900 · 17.7:1 */
|
||||
--c-fg-muted: 82 82 91; /* zinc-600 · 7.7:1 */
|
||||
--c-fg-subtle: 113 113 122; /* zinc-500 · 4.8:1 — csak metaadatra */
|
||||
|
||||
/* akcens: a foszforzöld világos alapon olvasható változata.
|
||||
A green-600 fehéren csak 3.3:1, ezért green-700. */
|
||||
--c-accent: 21 128 61; /* green-700 · 5.0:1 fehéren */
|
||||
--c-accent-hover: 22 101 52; /* green-800 */
|
||||
--c-accent-fg: 255 255 255; /* szöveg akcens-kitöltésen */
|
||||
--c-accent-quiet: 220 252 231; /* green-100 — halvány alap, badge */
|
||||
|
||||
/* állapotok */
|
||||
--c-warn: 180 83 9; /* amber-700 · 5.0:1 */
|
||||
--c-danger: 185 28 28; /* red-700 */
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--c-page: 9 9 11; /* zinc-950 */
|
||||
--c-surface: 24 24 27; /* zinc-900 */
|
||||
--c-raised: 39 39 42; /* zinc-800 */
|
||||
|
||||
--c-line: 39 39 42; /* zinc-800 */
|
||||
--c-line-strong: 63 63 70; /* zinc-700 */
|
||||
|
||||
--c-fg: 250 250 250; /* 19.1:1 */
|
||||
--c-fg-muted: 161 161 170; /* zinc-400 · 7.8:1 */
|
||||
--c-fg-subtle: 113 113 122; /* zinc-500 · 4.1:1 — csak metaadatra */
|
||||
|
||||
--c-accent: 74 222 128; /* green-400 · 11.4:1 feketén */
|
||||
--c-accent-hover: 134 239 172; /* green-300 */
|
||||
--c-accent-fg: 9 9 11; /* akcens-kitöltésen sötét szöveg kell */
|
||||
--c-accent-quiet: 20 83 45; /* green-900 */
|
||||
|
||||
--c-warn: 255 176 0; /* retro.amber · 10.9:1 */
|
||||
--c-danger: 248 113 113; /* red-400 */
|
||||
}
|
||||
@@ -1,10 +1,33 @@
|
||||
// A szemantikus színek a src/styles/tokens.css változóira mutatnak, hogy a
|
||||
// témaváltás (világos / sötét) egyetlen attribútumon múljon, és az
|
||||
// alfa-variánsok (bg-accent/15) is működjenek.
|
||||
const token = (name) => `rgb(var(${name}) / <alpha-value>)`
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{astro,html,js,ts,jsx,tsx,vue,svelte}"
|
||||
],
|
||||
// A témát a <html data-theme="dark"> választja, nem a prefers-color-scheme.
|
||||
darkMode: ['selector', '[data-theme="dark"]'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Szerepek — ezeket használja a markup.
|
||||
page: token('--c-page'),
|
||||
surface: token('--c-surface'),
|
||||
raised: token('--c-raised'),
|
||||
line: token('--c-line'),
|
||||
'line-strong': token('--c-line-strong'),
|
||||
fg: token('--c-fg'),
|
||||
'fg-muted': token('--c-fg-muted'),
|
||||
'fg-subtle': token('--c-fg-subtle'),
|
||||
accent: token('--c-accent'),
|
||||
'accent-hover': token('--c-accent-hover'),
|
||||
'accent-fg': token('--c-accent-fg'),
|
||||
'accent-quiet': token('--c-accent-quiet'),
|
||||
warn: token('--c-warn'),
|
||||
danger: token('--c-danger'),
|
||||
|
||||
primary: {
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
@@ -23,6 +46,16 @@ module.exports = {
|
||||
amber: '#ffb000',
|
||||
},
|
||||
},
|
||||
// Egy sugárskála: kártya, vezérlő. A kör (rounded-full) marad a Tailwindé.
|
||||
borderRadius: {
|
||||
card: '8px',
|
||||
control: '6px',
|
||||
},
|
||||
// Árnyék csak lebegő elemre; a kártyát keret határolja, nem árnyék.
|
||||
boxShadow: {
|
||||
pop: '0 1px 2px rgb(0 0 0 / 0.06), 0 1px 1px rgb(0 0 0 / 0.04)',
|
||||
float: '0 8px 24px rgb(0 0 0 / 0.12), 0 2px 6px rgb(0 0 0 / 0.08)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
||||