A kódbázis kommentek nélkül marad
ci/woodpecker/push/woodpecker Pipeline was successful

Kérésre: minden magyarázó komment kikerült a forrásfájlokból — 89 Ruby, 16
TypeScript, 14 Vue, plusz a CSS/JS/CJS. Nem soralapú kereséssel: a Ruby-t a
Ripper tokenizálta, a JS/TS/CSS-t állapotgép járta végig, hogy az URL-ekben,
reguláris kifejezésekben és heredocokban álló // és # jelek helyükön
maradjanak.

Három komment maradt, mert nélkülük nem indul a kód: az entrypoint.sh
shebangja, a vite-env.d.ts hármas perjeles referenciája, és a sanitize
teszt @vitest-environment direktívája (ez utóbbi a magyarázó része nélkül).

Egy helyen kódot is kellett írni: a CommandBlock másolás-hibaágán a komment
volt a catch egyetlen tartalma, és üres blokkot az eslint nem enged — a
copied jelző visszaállítása került a helyére.

A yaml, Dockerfile, Makefile, erb és markdown fájlokat nem érintettem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-20 12:52:47 +02:00
co-authored by Claude Opus 5
parent e431e2b85b
commit ebe3684d44
126 changed files with 117 additions and 1107 deletions
-3
View File
@@ -2,7 +2,6 @@ import type { WikiPage, WikiPageWithContent, WikiPageContent } from '../lib/inte
import { CONFIG } from '../lib/config'
// Public content base — used only for building external links to wiki pages.
const WIKI_BASE = CONFIG.wikiBase
interface RawWikiPage {
@@ -19,8 +18,6 @@ interface RawWikiPage {
tags?: string[]
}
// The engines page lists the flagship engines only; the wiki marks those with
// a `highlighted` tag next to `engine`.
const HIGHLIGHTED_TAG = 'highlighted'
async function fetchPages(
@@ -1,7 +1,3 @@
<!-- 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">
@@ -3,7 +3,6 @@
</template>
<script setup lang="ts">
// 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>
@@ -1,9 +1,3 @@
<!-- 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"
@@ -1,9 +1,3 @@
// Márkajelek SVG-útvonalai, 24×24-es rácson. A többségük a simple-icons
// készletéből származik (CC0); a Windows- és a LinkedIn-glifet a készlet
// védjegyokokból nem tartja, ezek saját, egyszerűsített rajzok.
//
// Azért inline útvonalak és nem külön csomag, mert tíz ikonért nem éri meg
// futásidejű függőség, és így a currentColor-ral együtt követik a témát.
export const BRAND_PATHS: Record<string, string[]> = {
apple: [
"M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701",
@@ -1,17 +1,5 @@
// 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'
@@ -36,7 +36,7 @@ describe('useLoadable', () => {
it('re-executes after TTL expires', async () => {
vi.useFakeTimers()
const { withCache } = useLoadable(100) // 100ms TTL
const { withCache } = useLoadable(100)
const fn = vi.fn()
await withCache(fn)
+1 -1
View File
@@ -1,6 +1,6 @@
import { ref } from 'vue'
const DEFAULT_TTL = 15 * 60 * 1000 // 15 perc
const DEFAULT_TTL = 15 * 60 * 1000
export function useLoadable(ttlMs?: number) {
const loading = ref(false)
@@ -2,14 +2,6 @@ import { describe, expect, it } from 'vitest'
import en from '../locales/en'
import hu from '../locales/hu'
/**
* The two bundles have to carry the same keys.
*
* A missing translation is not an error at runtime — vue-i18n falls back and the page
* renders the key itself, so `home.store.title` appears on screen looking like a bug
* somebody forgot to finish. That is exactly the kind of thing a person only notices
* after switching language, which is to say: after it has shipped.
*/
function paths (value: unknown, prefix = ''): string[] {
if (typeof value !== 'object' || value === null) return [prefix]
return Object.entries(value as Record<string, unknown>)
+1 -7
View File
@@ -4,8 +4,6 @@
<div class="app-header-inner">
<RouterLink to="/" class="app-logo">Teletype Games</RouterLink>
<!-- 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) }}
@@ -136,7 +134,6 @@ function localeBtnClass(lang: string) {
@apply flex-1;
}
/* ---- Fejléc ---- */
.app-header {
@apply sticky top-0 z-40 border-b border-line bg-page/90 backdrop-blur;
}
@@ -176,7 +173,6 @@ function localeBtnClass(lang: string) {
@apply lg:hidden;
}
/* ---- Mobil menü ---- */
.nav-mobile {
@apply flex flex-col border-t border-line bg-page px-4 py-2 lg:hidden;
}
@@ -187,7 +183,6 @@ function localeBtnClass(lang: string) {
@apply text-accent;
}
/* ---- Lábléc ---- */
.app-footer {
@apply mt-16 border-t border-line bg-surface;
}
@@ -218,8 +213,7 @@ function localeBtnClass(lang: string) {
.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
@@ -5,12 +5,6 @@ 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 />' } }],
@@ -54,7 +48,6 @@ describe('AppLayout', () => {
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')
@@ -1,10 +1,4 @@
/**
* 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
*/
/** @vitest-environment jsdom */
import { describe, it, expect } from 'vitest'
import { sanitize } from '../sanitize'
-4
View File
@@ -30,10 +30,6 @@ 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',
-3
View File
@@ -6,7 +6,4 @@ import App from './App.vue'
import './styles/tokens.css'
import './styles/global.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')
@@ -64,7 +64,7 @@ onMounted(() => store.fetch())
.blog-container {
@apply min-h-screen bg-page;
}
/* Olvasnivaló, nem kártyafal: egy hasáb, vonallal elválasztott bejegyzésekkel. */
.blog-list {
@apply flex max-w-[68ch] flex-col;
}
@@ -14,8 +14,6 @@
<SkeletonCard v-else-if="!data" :count="3" />
<!-- 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="table-wrap">
<table class="builds-table">
@@ -58,7 +56,6 @@
</div>
</div>
<!-- Mobile cards -->
<div v-if="data" class="builds-mobile">
<div v-for="(info, platform) in data.platforms" :key="platform" class="builds-card">
<h2 class="builds-card-title">{{ info.label }}</h2>
@@ -163,7 +160,7 @@ onMounted(() => store.fetch())
.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;
}
@@ -179,12 +176,11 @@ onMounted(() => store.fetch())
.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;
}
/* ---- Mobil ---- */
.builds-mobile {
@apply flex flex-col gap-4 md:hidden;
}
@@ -2,8 +2,6 @@
<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. -->
<p class="hero-subtitle">{{ t('catalog.subtitle') }}</p>
<div class="hero-actions">
<RouterLink to="/stores" class="btn-accent">{{ t('catalog.getTheStore') }}</RouterLink>
@@ -13,8 +11,6 @@
</header>
<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"
@@ -131,8 +127,7 @@ onMounted(() => store.fetch())
.software-card {
@apply flex flex-col overflow-hidden rounded-card border border-line bg-surface transition-colors hover:border-line-strong;
}
/* 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;
}
@@ -17,7 +17,6 @@
<main v-if="software" class="main-container">
<div class="catalog-layout">
<!-- Oldalsáv: kép és tények -->
<aside class="catalog-sidebar">
<div v-if="defaultImageUrl" class="catalog-image-panel">
<img
@@ -75,7 +74,6 @@
</div>
</aside>
<!-- oszlop -->
<div class="catalog-main">
<section v-if="software.story" class="card">
<h2 class="catalog-section-heading">{{ t('catalogShow.about') }}</h2>
@@ -105,8 +103,6 @@
</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">
@@ -206,7 +202,6 @@ const latestStable = computed(() => stableReleases.value[0] ?? null)
const downloadUrl = (path: string) =>
`/api/download?path=${encodeURIComponent(path.replace('/file/', ''))}`
// az asset-táblázat sorai és sorrendjük; a html nem letöltés, azt a Play gomb viszi
const TABLE_KIND_ORDER = [
'cartridge', 'source', 'docs',
'win_x64', 'win_x86', 'linux_x64', 'linux_arm64', 'linux_x86',
@@ -252,7 +247,7 @@ onMounted(async () => {
.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;
}
@@ -281,7 +276,6 @@ onMounted(async () => {
@apply max-w-[68ch] whitespace-pre-line leading-relaxed text-fg-muted;
}
/* ---- Kiadások ---- */
.release-headline {
@apply flex flex-wrap items-start justify-between gap-4;
}
@@ -307,7 +301,6 @@ onMounted(async () => {
@apply text-xs text-fg-subtle;
}
/* ---- Letöltések ---- */
.asset-list {
@apply mt-4 flex flex-col;
}
@@ -17,7 +17,6 @@
<SkeletonCard v-if="!error && publicRepos.length === 0 && !recentCommits.length" :count="4" />
<!-- 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"
@@ -12,14 +12,12 @@
</header>
<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>
@@ -53,8 +51,6 @@
</div>
</section>
<!-- 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>
@@ -68,7 +64,6 @@
</div>
</section>
<!-- Spotify -->
<MediaBand label="Teletype Games Party">
<template #action>
<a :href="SPOTIFY_PLAYLIST_URL" target="_blank" rel="noopener noreferrer" class="link text-sm">
@@ -78,8 +78,6 @@ const cards = computed(() =>
enginePages.value.map((page) => ({ page, digest: getEngineDigest(page.content) })),
)
// Explore points at the engine's git repository (from wiki metadata);
// pages without one fall back to their wiki page.
const exploreUrl = (page: WikiPageWithContent): string =>
page.repo || `${WIKI_BASE}/${page.path}`
@@ -91,8 +89,6 @@ onMounted(() => store.fetch())
@apply min-h-screen bg-page;
}
/* 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-6;
}
-16
View File
@@ -9,8 +9,6 @@
</header>
<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>
@@ -29,7 +27,6 @@
</div>
</section>
<!-- 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" />
@@ -59,8 +56,6 @@
</div>
</section>
<!-- 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>
@@ -98,7 +93,6 @@
</div>
</MediaBand>
<!-- Legutóbbi YouTube-videó -->
<MediaBand :label="t('home.latestFromYoutube')">
<template #action>
<a href="https://www.youtube.com/@teletypegames" target="_blank" class="link text-sm">
@@ -134,7 +128,6 @@
</div>
</MediaBand>
<!-- Spotify-lejátszási lista -->
<MediaBand label="Teletype Games Party">
<template #action>
<a :href="SPOTIFY_PLAYLIST_URL" target="_blank" class="link text-sm">
@@ -170,8 +163,6 @@ 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'
@@ -206,7 +197,6 @@ onUnmounted(() => eventStore.cleanup())
@apply min-h-screen bg-page;
}
/* ---- Visszaszámláló ---- */
.countdown {
@apply rounded-card border border-line bg-surface;
}
@@ -232,7 +222,6 @@ onUnmounted(() => eventStore.cleanup())
@apply flex items-center justify-between py-1 text-sm;
}
/* ---- Kiemelt játék ---- */
.featured {
@apply flex flex-col overflow-hidden rounded-card border border-line bg-surface md:flex-row;
}
@@ -258,7 +247,6 @@ onUnmounted(() => eventStore.cleanup())
@apply mt-6 flex flex-wrap gap-3;
}
/* ---- Bolt ---- */
.store-grid {
@apply grid grid-cols-1 gap-6 md:grid-cols-2 md:items-center;
}
@@ -284,7 +272,6 @@ onUnmounted(() => eventStore.cleanup())
@apply mt-4 flex items-center gap-2 text-xs text-fg-subtle;
}
/* ---- YouTube ---- */
.yt-grid {
@apply grid grid-cols-1 gap-6 lg:grid-cols-[1.6fr_1fr];
}
@@ -310,9 +297,6 @@ onUnmounted(() => eventStore.cleanup())
@apply flex flex-wrap gap-4 text-xs text-fg-subtle;
}
/* ---- 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;
}
@@ -9,8 +9,7 @@
</template>
<script setup lang="ts">
// The page shows five of these — install, restart, CLI, uninstall — so the
// 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'
@@ -26,7 +25,7 @@ async function copy() {
copied.value = true
setTimeout(() => { copied.value = false }, 2000)
} catch {
// Clipboard API needs a secure context — the command stays selectable anyway.
copied.value = false
}
}
</script>
@@ -35,8 +34,7 @@ async function copy() {
.cb {
@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. */
.cb pre {
@apply m-0 p-4 pr-14 font-mono text-sm leading-relaxed;
}
@@ -7,8 +7,6 @@
</header>
<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')">
<button
class="st-view" :class="{ 'st-view-active': view === 'client' }"
@@ -45,8 +43,6 @@
</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="1536" height="1110">
</figure>
@@ -70,8 +66,6 @@
</div>
<section class="st-section">
<!-- The spacing lives on this one container. Margins on the labels and none on
the button row is what let the buttons sit flush against the last block. -->
<div class="st-cmds">
<p class="st-cmd-label">{{ t('stores.installTitle') }}</p>
<CommandBlock :command="current.installCmd" />
@@ -100,9 +94,6 @@
</div>
</section>
<!-- Which catalog platforms a store engine can put on a device. The app has no
such list to show: it installs whatever the catalog offers, and says so on the
cards themselves. -->
<section class="st-section">
<h2 class="st-section-title">{{ t('stores.platformsTitle') }}</h2>
<ul class="st-platforms">
@@ -135,9 +126,6 @@ const router = useRouter()
const FORGE = 'https://git.teletypegames.org/stores'
// The graphical client. Its own thing rather than a link on the desktop store: it
// drives any WarpEngine store the site's registry offers, and on Windows it is the
// only way in — there is no `curl … | sh` there.
const CLIENT = {
repoUrl: `${FORGE}/warp-engine-client`,
releasesUrl: `${FORGE}/warp-engine-client/releases`,
@@ -146,9 +134,6 @@ const CLIENT = {
type DeviceId = 'batocera' | 'retroarch'
// The devices below are the ones a store reaches through a shell installer. An
// ordinary computer is not among them any more: there the store *is* the app in the
// section above, which carries its own engine and needs nothing installed first.
const BATOCERA_CLI = '/userdata/system/batocera-store/ttg-store'
const RETROARCH_CLI = '~/.local/bin/ttg-retroarch-store'
@@ -158,8 +143,7 @@ const devices = [
projectUrl: 'https://batocera.org',
repoUrl: `${FORGE}/ttg-batocera-store`,
wikiUrl: `${CONFIG.wikiBase}/stores/ttg-batocera-store`,
// The installer is served straight from the forge, so this one line is the
// whole install on the device.
installCmd: `curl -fsSL ${FORGE}/ttg-batocera-store/raw/branch/master/install.sh | sh`,
afterInstallCmd: 'batocera-es-swissknife --restart',
uninstallCmd: `curl -fsSL ${FORGE}/ttg-batocera-store/raw/branch/master/uninstall.sh | sh`,
@@ -188,24 +172,18 @@ const devices = [
type ViewId = 'client' | 'others'
// `?view=` and `?device=` keep a link shareable, and let /batocera redirect here without
// losing which store the visitor came for. Query only: the router does not scroll for a
// query-only change, so switching does not throw the page back to the top.
const initial = devices.some((d) => d.id === route.query.device)
? (route.query.device as DeviceId)
: 'batocera'
const device = ref<DeviceId>(initial)
// The client view is what most visitors want, so it is the default — except when the URL
// names a device, which is how /batocera and /retroarch redirect here. Somebody arriving
// from those wants the integrations, not the app.
const view = ref<ViewId>(
route.query.view === 'others' || (route.query.view === undefined && route.query.device !== undefined)
? 'others'
: 'client',
)
const current = computed(() => devices.find((d) => d.id === device.value) ?? devices[0])
// The app is not tied to a device tab: it drives whichever store the site's registry
// offers, so it stands above the chooser rather than inside it.
const client = CLIENT
function select(id: DeviceId) {
@@ -255,7 +233,6 @@ const platforms = [
@apply text-accent;
}
/* The switcher is meant to be almost nothing: two words and a line under the active one. */
.st-views {
@apply mb-6 flex gap-1 border-b border-line;
}
@@ -266,8 +243,7 @@ const platforms = [
.st-view-active {
@apply border-accent text-fg;
}
/* One rhythm for the whole command list, so nothing depends on a margin someone
remembered to add. */
.st-cmds {
@apply space-y-2;
}
+1 -4
View File
@@ -25,11 +25,8 @@ export const router = createRouter({
...storesRouter,
],
scrollBehavior(to, from, savedPosition) {
// Back and forward land where the visitor was.
if (savedPosition) return savedPosition
// A query-only change is not a new page. The stores page writes its switcher into the
// URL so a link stays shareable, and throwing the reader back to the top for that
// felt like a reload.
if (to.path === from.path) return false
return { top: 0 }
},
+2 -5
View File
@@ -2,12 +2,9 @@ import type { RouteRecordRaw } from 'vue-router'
export const storesRouter: RouteRecordRaw[] = [
{ path: '/stores', name: 'storesIndex', component: () => import('../page/stores/StoresIndexPage.vue') },
// The Batocera store had a page to itself until the RetroArch store joined it.
// Both device names keep working as URLs — the old links, and the guess someone
// makes after reading "RetroArch store" — and each lands on its own tab.
{ path: '/batocera', redirect: { name: 'storesIndex', query: { device: 'batocera' } } },
{ path: '/retroarch', redirect: { name: 'storesIndex', query: { device: 'retroarch' } } },
// `/desktop` used to name a shell store for ordinary computers. That store is the
// app now, so the old URL lands on the app rather than on a device tab.
{ path: '/desktop', redirect: { name: 'storesIndex' } },
]
@@ -1,9 +1,6 @@
import { describe, it, expect } from 'vitest'
import { getEngineDigest } from '../engines.store'
// Generic markdown in the shape every engine-tagged wiki page must follow:
// intro paragraph, then "What You Get" as the first heading with bullets.
// Dummy text only — real content always comes from the wiki at runtime.
const SAMPLE_MARKDOWN = `
> Example Engine is a **sample** framework that turns markdown into landing cards.
@@ -9,9 +9,6 @@ export interface EngineHighlight {
text: string
}
// Landing-page digest of a wiki engine page. Convention: every engine-tagged
// page opens with an intro paragraph, then a "What You Get" heading as its
// first heading, with the feature bullets underneath.
export interface EngineDigest {
intro: string
highlightsTitle: string
@@ -21,7 +18,6 @@ export interface EngineDigest {
const HIGHLIGHTS_HEADING = 'what you get'
const MAX_HIGHLIGHTS = 6
// Inline markdown (links, bold, code) stripped so the text reads as plain prose.
function stripInline(md: string): string {
return md
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
@@ -41,8 +37,6 @@ export function getEngineDigest(content: string): EngineDigest {
const heading = line.match(/^#{1,6}\s+(.*)$/)
if (heading) {
// The section ends at the next heading; and if the page's first heading
// is not "What You Get", it doesn't follow the convention — no highlights.
if (inHighlights) break
const title = stripInline(heading[1])
if (title.toLowerCase() !== HIGHLIGHTS_HEADING) break
@@ -63,7 +57,6 @@ export function getEngineDigest(content: string): EngineDigest {
continue
}
// First prose line (paragraph or blockquote) before the heading is the intro.
if (!digest.intro) {
digest.intro = stripInline(line.replace(/^>\s*/, ''))
}
-3
View File
@@ -14,9 +14,6 @@ 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
const file = member.avatarFilename.replace(/\.(png|jpe?g)$/i, '.webp')
-9
View File
@@ -6,8 +6,6 @@ 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'
}
@@ -16,15 +14,10 @@ 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> {
@@ -42,8 +35,6 @@ export const useUiStore = defineStore('ui', () => {
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)
-22
View File
@@ -9,15 +9,11 @@ body {
height: 100%;
}
/* 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));
}
/* 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,
@@ -29,8 +25,6 @@ body {
}
}
/* 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));
@@ -39,7 +33,6 @@ button:focus-visible {
}
@layer components {
/* ---- Elrendezés ---------------------------------------------------- */
.main-container {
@apply mx-auto w-full max-w-6xl px-4 py-10 md:px-6 md:py-14;
}
@@ -48,9 +41,6 @@ button:focus-visible {
@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;
}
@@ -75,7 +65,6 @@ button:focus-visible {
@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;
}
@@ -84,7 +73,6 @@ button:focus-visible {
@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;
@@ -106,7 +94,6 @@ button:focus-visible {
@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;
}
@@ -115,8 +102,6 @@ button:focus-visible {
@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;
}
@@ -129,7 +114,6 @@ button:focus-visible {
@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;
}
@@ -154,17 +138,11 @@ button:focus-visible {
@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;
}
+4 -37
View File
@@ -1,45 +1,25 @@
/* ===================================================================
RETRO MODE — TuiCss-stílusú megjelenés a site fölött.
A facelift óta ez a fájl nem fest újra minden komponenst egyenként:
a retro egy harmadik téma. Először felülírja a szerep-tokeneket
(tokens.css), és csak azt szabályozza külön, ami formában és nem
színben más — betűtípus, kettős keret, blokkárnyék, tui-sáv.
Ezért is rövidült 1153 sorról ennyire: a korábbi változat ~500
`!important`-tal küzdött a Tailwind-osztályok ellen, és a szelektorai
nagyrészt olyan markupra épültek, ami már nem létezik.
A fájl (a TuiCSS-szel együtt) csak akkor töltődik be, ha valaki
bekapcsolja a retro módot — lásd ui.store.
=================================================================== */
/* A gyökéren a tokenek: a body háttere is innen jön. Az attribútumot az
ui.store teszi ki, ugyanakkor, amikor a .retro-mode osztályt. */
[data-retro='1'] {
--c-page: 0 0 0;
--c-surface: 0 0 168; /* tui blue-168 */
--c-surface: 0 0 168;
--c-raised: 0 0 168;
--c-line: 255 255 255;
--c-line-strong: 255 255 255;
--c-fg: 255 255 255;
--c-fg-muted: 200 200 200;
--c-fg-subtle: 168 168 168;
--c-accent: 0 168 0; /* tui green-168 */
--c-accent-hover: 0 168 168; /* tui cyan-168 */
--c-accent: 0 168 0;
--c-accent-hover: 0 168 168;
--c-accent-fg: 0 0 0;
--c-accent-quiet: 0 0 168;
--c-warn: 168 168 0;
--c-danger: 168 0 0;
}
/* ---- ANIMÁCIÓK ---- */
@keyframes retro-blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
/* ---- ALAP ---- */
.retro-mode {
font-family: "Lucida Console", monospace !important;
background-color: #000 !important;
@@ -58,7 +38,6 @@
animation: retro-blink 0.9s step-start infinite;
}
/* ---- FEJLÉC — tui-nav ---- */
.retro-mode .app-header {
background-color: #a8a8a8 !important;
border-bottom: none !important;
@@ -85,7 +64,6 @@
text-decoration: none !important;
}
/* tui-shortcut: az első betű piros, mint a DOS-menükben */
.retro-mode .nav-link::first-letter {
color: #a80000 !important;
font-weight: bold !important;
@@ -118,7 +96,6 @@
padding: 2px 6px !important;
}
/* Mobil menü — tui-window */
.retro-mode .nav-mobile {
background-color: #a8a8a8 !important;
border: 2px solid #000 !important;
@@ -134,7 +111,6 @@
background-color: #00a800 !important;
}
/* ---- HERO — tui-bg-blue-black ---- */
.retro-mode .hero-section {
background-color: #0000a8 !important;
background-image: url("../../node_modules/tuicss/dist/images/bg-blue-black.png") !important;
@@ -164,7 +140,6 @@
font-size: 14px !important;
}
/* ---- FELÜLETEK — tui-window / tui-panel (6px kettős fehér keret) ---- */
.retro-mode .card,
.retro-mode .card-bare,
.retro-mode .card-link,
@@ -186,7 +161,7 @@
background-image: none !important;
color: #fff !important;
border: 6px white double !important;
box-shadow: 10px 10px 0 #000 !important; /* tui-shadow */
box-shadow: 10px 10px 0 #000 !important;
transform: none !important;
}
@@ -200,7 +175,6 @@
color: #00a800 !important;
}
/* ---- GOMBOK — tui-button ---- */
.retro-mode .btn,
.retro-mode .btn-accent,
.retro-mode .btn-ghost,
@@ -230,7 +204,6 @@
box-shadow: none !important;
}
/* ---- LINKEK ---- */
.retro-mode .link,
.retro-mode a.link {
color: #00a8a8 !important;
@@ -241,7 +214,6 @@
color: #00a8a8 !important;
}
/* ---- JELÖLŐK — 168-as paletta ---- */
.retro-mode .badge,
.retro-mode .badge-accent,
.retro-mode .status-badge,
@@ -257,7 +229,6 @@
background-color: #00a800 !important;
}
/* ---- TÁBLÁZATOK — tui-table, striped-blue ---- */
.retro-mode table {
border: 2px solid #a8a8a8 !important;
border-collapse: collapse !important;
@@ -294,7 +265,6 @@
opacity: 1 !important;
}
/* ---- PARANCSBLOKK ---- */
.retro-mode .cb {
background-color: #000 !important;
border: 2px solid #a8a8a8 !important;
@@ -306,7 +276,6 @@
color: #00a800 !important;
}
/* ---- WIKI-TARTALOM ---- */
.retro-mode .wiki-content,
.retro-mode .wiki-content p,
.retro-mode .wiki-content li {
@@ -337,7 +306,6 @@
color: #000 !important;
}
/* ---- LÁBLÉC — tui-statusbar ---- */
.retro-mode .app-footer {
background-color: #a8a8a8 !important;
border-top: none !important;
@@ -364,7 +332,6 @@
background-color: #00a800 !important;
}
/* ---- KÉPEK ---- */
.retro-mode img {
border: 2px solid #a8a8a8 !important;
image-rendering: pixelated;
+27 -48
View File
@@ -1,62 +1,41 @@
/* ===================================================================
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 */
--c-surface: 250 250 250;
--c-raised: 244 244 245;
/* keretek */
--c-line: 228 228 231; /* zinc-200 */
--c-line-strong: 212 212 216; /* zinc-300 */
--c-line: 228 228 231;
--c-line-strong: 212 212 216;
/* 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 */
--c-fg: 24 24 27;
--c-fg-muted: 82 82 91;
--c-fg-subtle: 113 113 122;
/* 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 */
--c-accent: 21 128 61;
--c-accent-hover: 22 101 52;
--c-accent-fg: 255 255 255;
--c-accent-quiet: 220 252 231;
/* állapotok */
--c-warn: 180 83 9; /* amber-700 · 5.0:1 */
--c-danger: 185 28 28; /* red-700 */
--c-warn: 180 83 9;
--c-danger: 185 28 28;
}
[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-page: 9 9 11;
--c-surface: 24 24 27;
--c-raised: 39 39 42;
--c-line: 39 39 42; /* zinc-800 */
--c-line-strong: 63 63 70; /* zinc-700 */
--c-line: 39 39 42;
--c-line-strong: 63 63 70;
--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-fg: 250 250 250;
--c-fg-muted: 161 161 170;
--c-fg-subtle: 113 113 122;
--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-accent: 74 222 128;
--c-accent-hover: 134 239 172;
--c-accent-fg: 9 9 11;
--c-accent-quiet: 20 83 45;
--c-warn: 255 176 0; /* retro.amber · 10.9:1 */
--c-danger: 248 113 113; /* red-400 */
--c-warn: 255 176 0;
--c-danger: 248 113 113;
}
+3 -7
View File
@@ -1,18 +1,14 @@
// 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'),
@@ -46,12 +42,12 @@ 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)',