mirror of
https://github.com/rommapp/romm.git
synced 2026-10-12 02:21:58 +00:00
Remove the no-emdash-in-comment ESLint rule after a one-time sweep
Replace the em-dashes in frontend comments outside the frozen v1 UI (script, template and style comments, plus the header build-tokens.ts writes into tokens.css), then drop the rule. String literals keep their em-dash glyphs. The AGENTS.md rule stays, and pr-ready's humanizer pass catches new ones in prose. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01RyamrxVw5BzEidVzm68LY7
This commit is contained in:
@@ -2,7 +2,7 @@ import type { StorybookConfig } from "@storybook/vue3-vite";
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
// Only pick up v2 stories — the v1 UI is frozen and does not ship stories.
|
||||
// Only pick up v2 stories: the v1 UI is frozen and does not ship stories.
|
||||
stories: ["../src/v2/**/*.stories.@(js|jsx|ts|tsx)", "../src/v2/**/*.mdx"],
|
||||
addons: [
|
||||
"@storybook/addon-docs",
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
installBreakpointAttribute();
|
||||
|
||||
// Each story runs inside a Vue app with Pinia + i18n + Vuetify registered.
|
||||
// v2 primitives are Vuetify-free at runtime — Vuetify stays registered only
|
||||
// v2 primitives are Vuetify-free at runtime: Vuetify stays registered only
|
||||
// because some shared dependencies still pull it in. The visible theme for
|
||||
// v2 stories comes from the `.r-v2-dark` / `.r-v2-light` class toggled on
|
||||
// <html> by the theme switcher decorator below.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @ts-check
|
||||
import noColorLiteral from "./rules/no-color-literal.js";
|
||||
import noEmdashInComment from "./rules/no-emdash-in-comment.js";
|
||||
import noLayoutMediaQuery from "./rules/no-layout-media-query.js";
|
||||
|
||||
/** @type {import("eslint").ESLint.Plugin} */
|
||||
@@ -8,7 +7,6 @@ export default {
|
||||
meta: { name: "eslint-plugin-romm" },
|
||||
rules: {
|
||||
"no-color-literal": noColorLiteral,
|
||||
"no-emdash-in-comment": noEmdashInComment,
|
||||
"no-layout-media-query": noLayoutMediaQuery,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
// @ts-check
|
||||
import { reportAt } from "../utils/report.js";
|
||||
import { rangeOf, sfcFragment, sfcStyleRoots } from "../utils/sfcStyles.js";
|
||||
|
||||
const EM_DASH = "—";
|
||||
|
||||
/**
|
||||
* Source ranges of every comment in the file: script, SFC template, and SFC style.
|
||||
* @param {import("eslint").Rule.RuleContext} context
|
||||
* @returns {[number, number][]}
|
||||
*/
|
||||
function commentRanges(context) {
|
||||
/** @type {[number, number][]} */
|
||||
const ranges = [];
|
||||
for (const c of context.sourceCode.getAllComments()) {
|
||||
if (c.range) ranges.push(c.range);
|
||||
}
|
||||
for (const c of sfcFragment(context)?.comments ?? []) ranges.push(c.range);
|
||||
for (const block of sfcStyleRoots(context)) {
|
||||
block.root.walkComments((c) => {
|
||||
ranges.push(rangeOf(block, c));
|
||||
});
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
/** @type {import("eslint").Rule.RuleModule} */
|
||||
export default {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Disallow the em dash (U+2014) in comments (script, template, and style). Strings and template text are code and stay allowed.",
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
emDash:
|
||||
"Replace the em dash with a comma, parentheses, or a separate sentence.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
Program() {
|
||||
const { text } = context.sourceCode;
|
||||
if (!text.includes(EM_DASH)) return;
|
||||
for (const [start, end] of commentRanges(context)) {
|
||||
const comment = text.slice(start, end);
|
||||
let offset = comment.indexOf(EM_DASH);
|
||||
while (offset !== -1) {
|
||||
reportAt(context, start + offset, 1, "emDash");
|
||||
offset = comment.indexOf(EM_DASH, offset + 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import rule from "./no-emdash-in-comment.js";
|
||||
import { ruleTester, sfc } from "./testing";
|
||||
|
||||
const error = { messageId: "emDash" };
|
||||
|
||||
ruleTester.run("no-emdash-in-comment", rule, {
|
||||
valid: [
|
||||
{ code: "// A comma, then (parentheses) - and a hyphen.\nconst a = 1;" },
|
||||
{ code: 'const empty = "—";' },
|
||||
{ code: "const label = `Save — now`;" },
|
||||
{ code: 'describe("criteria — tags", () => {});' },
|
||||
{ code: "const foo = `${this}—${that}`;" },
|
||||
{ code: "const foo = `${a} — ${b}`; // joined with a separator" },
|
||||
{ code: 'const url = "https://example.com/a—b";' },
|
||||
{ code: 'const s = "// not a comment — just text";' },
|
||||
{ code: 'const s = "/* also — not a comment */";' },
|
||||
{ code: "const s = `<!-- — -->`;" },
|
||||
{ code: "const dash = /—/g;" },
|
||||
{
|
||||
code: '<script setup lang="ts"></script>\n<template><p :title="`${a}—${b}`">{{ `${a} — ${b}` }}</p></template>\n',
|
||||
filename: "A.vue",
|
||||
},
|
||||
{
|
||||
code: '<script setup lang="ts"></script>\n<template><span>—</span><p>Save — now</p></template>\n',
|
||||
filename: "A.vue",
|
||||
},
|
||||
{
|
||||
code: '<script setup lang="ts"></script>\n<template><RTag text="—" /></template>\n',
|
||||
filename: "A.vue",
|
||||
},
|
||||
sfc('.a::after { content: "—"; }'),
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "// FilterDrawer (v2) — gallery filter side panel.\nconst a = 1;",
|
||||
errors: [{ ...error, line: 1, column: 22 }],
|
||||
},
|
||||
{
|
||||
code: "/* No card surface — the cover is the card. */\nconst a = 1;",
|
||||
errors: [error],
|
||||
},
|
||||
{
|
||||
code: "/**\n * First line.\n * more text — continues here.\n */\nexport const x = 1;",
|
||||
errors: [{ ...error, line: 3 }],
|
||||
},
|
||||
{
|
||||
code: "/** One — two — three. */\nexport const x = 1;",
|
||||
errors: [error, error],
|
||||
},
|
||||
{
|
||||
code: 'const empty = "—"; // placeholder — no value',
|
||||
errors: [{ ...error, column: 35 }],
|
||||
},
|
||||
{
|
||||
code: '<script setup lang="ts">\n// Header — detail\nconst a = "—";\n</script>\n<template><div /></template>\n',
|
||||
filename: "A.vue",
|
||||
errors: [{ ...error, line: 2 }],
|
||||
},
|
||||
{
|
||||
code: '<script setup lang="ts"></script>\n<template><!-- Row — note --><p>—</p></template>\n',
|
||||
filename: "A.vue",
|
||||
errors: [{ ...error, line: 2 }],
|
||||
},
|
||||
{
|
||||
...sfc('/* Glass — dark */\n.a { content: "—"; }'),
|
||||
errors: [{ ...error, line: 4 }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -208,7 +208,6 @@ export default tseslint.config(
|
||||
files: ["src/v2/**/*.ts", "src/v2/**/*.vue"],
|
||||
plugins: { romm },
|
||||
rules: {
|
||||
"romm/no-emdash-in-comment": "error",
|
||||
"romm/no-color-literal": "error",
|
||||
"romm/no-layout-media-query": "error",
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* build-tokens — emits src/v2/styles/tokens.css from src/v2/tokens/index.ts.
|
||||
* build-tokens: emits src/v2/styles/tokens.css from src/v2/tokens/index.ts.
|
||||
*
|
||||
* The TypeScript module is the single source of truth. This script declares
|
||||
* the JS-path → CSS-variable mapping and writes the CSS file. Run it via
|
||||
@@ -102,7 +102,7 @@ const NAME_OVERRIDES = {
|
||||
rommGold: "--r-color-romm-gold",
|
||||
},
|
||||
// Stacking-layer tokens drop the "-index" suffix the default generator
|
||||
// would produce — components consume `--r-z-menu` etc., shorter and
|
||||
// would produce: components consume `--r-z-menu` etc., shorter and
|
||||
// matches the convention from §VI of the constitution.
|
||||
zIndex: {
|
||||
drawer: "--r-z-drawer",
|
||||
@@ -192,15 +192,15 @@ const LIGHT: Entry[] = [
|
||||
];
|
||||
|
||||
const HEADER = `/*
|
||||
* RomM v2 Design Tokens — CSS Custom Properties
|
||||
* RomM v2 Design Tokens, CSS Custom Properties
|
||||
*
|
||||
* GENERATED FILE — do not hand-edit. Source: src/v2/tokens/index.ts
|
||||
* GENERATED FILE, do not hand-edit. Source: src/v2/tokens/index.ts
|
||||
* Regenerate with: npm run build:tokens
|
||||
*
|
||||
* Scoped under .r-v2 so v1 styling is unaffected. Theme palettes live under
|
||||
* .r-v2.r-v2-dark and .r-v2.r-v2-light. The classes are toggled on <html>
|
||||
* by RomM.vue so teleported overlays (RDialog, RMenu, RTooltip) — which
|
||||
* land in <body> outside the app root — still resolve var(--r-color-*).
|
||||
* by RomM.vue so teleported overlays (RDialog, RMenu, RTooltip), which
|
||||
* land in <body> outside the app root, still resolve var(--r-color-*).
|
||||
*/
|
||||
`;
|
||||
|
||||
@@ -218,12 +218,12 @@ const css = [
|
||||
block(
|
||||
".r-v2.r-v2-dark",
|
||||
DARK,
|
||||
"Dark surface palette — translucent white over the near-black base.",
|
||||
"Dark surface palette: translucent white over the near-black base.",
|
||||
),
|
||||
block(
|
||||
".r-v2.r-v2-light",
|
||||
LIGHT,
|
||||
"Light surface palette — translucent black over the off-white base.",
|
||||
"Light surface palette: translucent black over the off-white base.",
|
||||
),
|
||||
].join("\n");
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ languageStore.setLanguage(selectedLanguage.value);
|
||||
// NOTE: uiVersion uses a module-level singleton ref (useUiVersion) so a write
|
||||
// from the settings page is the SAME ref RomM.vue reads, triggering a gate
|
||||
// re-evaluation and an instant v1 ↔ v2 swap with no reload. Theme likewise
|
||||
// reads the raw localStorage ref — we stay off useUISettings here because it
|
||||
// reads the raw localStorage ref: we stay off useUISettings here because it
|
||||
// imports the API layer and would trigger an API-client ↔ router circular-
|
||||
// import TDZ during bootstrap (RomM.vue is the first module main.ts loads).
|
||||
const uiVersion = useUiVersion();
|
||||
@@ -56,7 +56,7 @@ const { idle: mouseIdle } = useIdle(100, {
|
||||
events: ["mousemove", "mousedown", "wheel", "touchstart"],
|
||||
});
|
||||
|
||||
// Centralized theme resolution — Vuetify only knows the "dark" / "light"
|
||||
// Centralized theme resolution: Vuetify only knows the "dark" / "light"
|
||||
// pair (used by v1 surfaces and any remaining v1 components rendered
|
||||
// inside v2). v2's own colour story is driven by tokens on the .r-v2-*
|
||||
// classes below, not by Vuetify's runtime theme.
|
||||
@@ -98,7 +98,7 @@ watch(
|
||||
const isV2 = computed(() => uiVersion.value === "v2");
|
||||
|
||||
// Apply the v2 token scope to <html> when v2 is active. Vuetify teleports
|
||||
// overlays (VDialog, VMenu) into `<body> > .v-overlay-container` — which
|
||||
// overlays (VDialog, VMenu) into `<body> > .v-overlay-container`: which
|
||||
// sits OUTSIDE both the AppLayout `.r-v2` wrapper AND <v-app>. Putting the
|
||||
// classes on <html> means the entire document inherits the v2 CSS custom
|
||||
// properties so `var(--r-color-...)` resolves inside any teleported
|
||||
|
||||
@@ -59,7 +59,7 @@ export const UI_SETTINGS_KEYS = {
|
||||
key: "settings.libraryStatsMode",
|
||||
default: "compact",
|
||||
},
|
||||
// Widget render order — comma-separated list of widget IDs.
|
||||
// Widget render order: comma-separated list of widget IDs.
|
||||
// Persisted as a string in localStorage (the useUISettings helper
|
||||
// only handles primitives; consumers parse/serialize at the edges).
|
||||
// Unknown IDs are filtered out on read so removing a widget from
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// useUiVersion — singleton ref for the v1/v2 UI toggle.
|
||||
// useUiVersion: singleton ref for the v1/v2 UI toggle.
|
||||
//
|
||||
// vueuse's useLocalStorage creates an INDEPENDENT ref per call. Two callers
|
||||
// with the same key share localStorage but not Vue reactivity within the
|
||||
|
||||
@@ -275,7 +275,7 @@ const routes = [
|
||||
v2: v2For(ROUTES.STREAM_DESKTOP),
|
||||
},
|
||||
},
|
||||
// Settings group — every settings route shares the same v2
|
||||
// Settings group: every settings route shares the same v2
|
||||
// sub-layout (sidebar + content panel). Library Tools (Scan /
|
||||
// Upload / Patcher) live here too so they share the settings
|
||||
// sidebar shell. v1 keeps its existing per-view structure via the
|
||||
@@ -441,7 +441,7 @@ const routes = [
|
||||
title: "common.logs",
|
||||
bare: true,
|
||||
// The log panel fills the viewport and scrolls internally
|
||||
// instead of growing the document — see SettingsLayout `fill`.
|
||||
// instead of growing the document: see SettingsLayout `fill`.
|
||||
fill: true,
|
||||
},
|
||||
components: {
|
||||
@@ -510,7 +510,7 @@ const routes = [
|
||||
name: ROUTES.PAIR,
|
||||
component: () => import("@/v2/views/PairDispatcher.vue"),
|
||||
},
|
||||
// Console mode (separate UI namespace under /console) — v1 only; v2 merges
|
||||
// Console mode (separate UI namespace under /console): v1 only; v2 merges
|
||||
// console behavior into the main UI via the universal input system.
|
||||
{
|
||||
path: "/console",
|
||||
@@ -564,14 +564,14 @@ const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// popstate (back/forward) — restore the saved offset.
|
||||
// popstate (back/forward): restore the saved offset.
|
||||
if (savedPosition) return savedPosition;
|
||||
// Same path → only query/hash changed (e.g., the v2 GameDetails
|
||||
// tab/subtab params, gallery filter syncs). The user's view should
|
||||
// stay where it is; scrolling to top would make the URL update
|
||||
// visible as a UX jump.
|
||||
if (to.path === from.path) return false;
|
||||
// Genuine route change — start fresh from the top.
|
||||
// Genuine route change: start fresh from the top.
|
||||
return { left: 0, top: 0 };
|
||||
},
|
||||
});
|
||||
@@ -629,7 +629,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
const currentRoute = to.name?.toString();
|
||||
|
||||
try {
|
||||
// Backend unreachable/broken — we can't trust the setup/auth state, and
|
||||
// Backend unreachable/broken: we can't trust the setup/auth state, and
|
||||
// bouncing to /login would just strand the user on a page that can't work
|
||||
// either. Let them stay on (and navigate within) whatever the cached state
|
||||
// allows; the offline notice explains it and the connection layer
|
||||
@@ -644,7 +644,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
return currentRoute !== "setup" ? next({ name: ROUTES.SETUP }) : next();
|
||||
}
|
||||
|
||||
// Handle authentication — unauth'd users visiting a non-exempt route
|
||||
// Handle authentication: unauth'd users visiting a non-exempt route
|
||||
// land on /login. Without this branch, they fall through to the
|
||||
// permission check below, fail it, get redirected to the catch-all 404
|
||||
// (which matches /), and the guard re-runs forever.
|
||||
@@ -657,7 +657,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
});
|
||||
}
|
||||
|
||||
// SHOW_SETUP_WIZARD is false here, so setup is already done — nobody
|
||||
// SHOW_SETUP_WIZARD is false here, so setup is already done: nobody
|
||||
// belongs on /setup anymore. `/setup` is auth-exempt (so the block above
|
||||
// won't bounce an unauthenticated visitor), so redirect both cases:
|
||||
// authenticated users go home, everyone else to login. Without covering
|
||||
@@ -701,11 +701,11 @@ watch(i18n.global.locale, async (locale) => {
|
||||
});
|
||||
|
||||
router.beforeResolve(async (to, from) => {
|
||||
// Query/hash-only changes (same path — e.g. the v2 GameDetails `?tab=`
|
||||
// Query/hash-only changes (same path: e.g. the v2 GameDetails `?tab=`
|
||||
// param) aren't a real view change. Running a view transition would
|
||||
// snapshot every `view-transition-name` element (like the details cover)
|
||||
// into the browser's top layer for the crossfade, briefly floating it over
|
||||
// the fixed navbar. Skip them — matching `scrollBehavior` above.
|
||||
// the fixed navbar. Skip them: matching `scrollBehavior` above.
|
||||
if (to.path === from.path) return;
|
||||
const viewTransition = startViewTransition();
|
||||
await viewTransition.captured;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { VDateInput } from "vuetify/labs/VDateInput";
|
||||
import "vuetify/styles";
|
||||
import { dark, light } from "@/styles/themes";
|
||||
|
||||
// Initial theme resolution only — runtime theme changes are owned by
|
||||
// Initial theme resolution only: runtime theme changes are owned by
|
||||
// RomM.vue, which keeps Vuetify's name in sync with user preference.
|
||||
// v2 surfaces don't read from Vuetify's runtime theme; they read tokens
|
||||
// off `.r-v2-dark` / `.r-v2-light` on <html>.
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface ActivityEntry {
|
||||
rom_name: string;
|
||||
rom_cover_path: string;
|
||||
// "Where they are" image (latest save screenshot / title / first
|
||||
// screenshot); empty when none — the card then shows cover art.
|
||||
// screenshot); empty when none: the card then shows cover art.
|
||||
screenshot_path: string;
|
||||
platform_slug: string;
|
||||
platform_name: string;
|
||||
|
||||
@@ -66,8 +66,8 @@ api.interceptors.response.use(
|
||||
return response;
|
||||
},
|
||||
async (error) => {
|
||||
// Mirror the success path's bookkeeping: a settled request — even a failed
|
||||
// or canceled one — leaves the inflight set so `network-quiesced` can still
|
||||
// Mirror the success path's bookkeeping: a settled request, even a failed
|
||||
// or canceled one: leaves the inflight set so `network-quiesced` can still
|
||||
// fire once the network goes quiet.
|
||||
inflightRequests.delete(error.config?.url);
|
||||
if (inflightRequests.size === 0) {
|
||||
@@ -86,7 +86,7 @@ api.interceptors.response.use(
|
||||
// * 5xx from a non-heartbeat endpoint → "suspect"; ask the connection
|
||||
// layer to confirm via the authoritative /heartbeat probe (so one
|
||||
// buggy endpoint doesn't flash the banner, and the heartbeat request
|
||||
// itself never re-triggers this — which would loop).
|
||||
// itself never re-triggers this: which would loop).
|
||||
// * 4xx → backend is alive; emit backend-online so a stale offline banner
|
||||
// clears immediately instead of waiting for the next heartbeat poll.
|
||||
const status = error.response?.status as number | undefined;
|
||||
|
||||
@@ -529,7 +529,7 @@ async function getRomSimple({
|
||||
romId: number;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
// `/roms/{id}/simple` — returns `SimpleRomSchema` with no eager-loaded
|
||||
// `/roms/{id}/simple`: returns `SimpleRomSchema` with no eager-loaded
|
||||
// notes / saves / states / screenshots / collections arrays. Designed
|
||||
// for the v2 gallery card's per-card fetch path. Detail-level data is
|
||||
// pulled on demand (game details page, quick-note dialog open).
|
||||
|
||||
@@ -19,7 +19,7 @@ export type FilterType =
|
||||
|
||||
export type FilterLogicOperator = "any" | "all" | "none";
|
||||
|
||||
// Built lazily so `romStatusMap` isn't read at module-evaluation time —
|
||||
// Built lazily so `romStatusMap` isn't read at module-evaluation time:
|
||||
// utils ↔ stores have a circular import chain (utils → navigation → router →
|
||||
// roms → galleryFilter → utils) that's harmless in the app shell because
|
||||
// main.ts loads router first, but trips Storybook (no router boot) with a
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// permissionGroups — Pinia store holding the admin list of permission groups.
|
||||
// permissionGroups: Pinia store holding the admin list of permission groups.
|
||||
//
|
||||
// Single source of truth shared by the groups table, the user create/edit
|
||||
// dialogs and the users table, so creating or deleting a group is reflected
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// permissions — Pinia store holding the current user's effective grants.
|
||||
// permissions: Pinia store holding the current user's effective grants.
|
||||
//
|
||||
// Hydrated from the backend's `/permissions/me` (the source of truth) via
|
||||
// `installPermissionsHydration()` in AppLayout, and refreshed live on the
|
||||
// `permissions:changed` socket event. v2 features consume this store via
|
||||
// `useCan(action, scope?)`. v1 does not import from here — its existing inline
|
||||
// `useCan(action, scope?)`. v1 does not import from here: its existing inline
|
||||
// scope checks keep working.
|
||||
import { defineStore } from "pinia";
|
||||
import type {
|
||||
|
||||
Vendored
+6
-6
@@ -23,21 +23,21 @@ export type Events = {
|
||||
showDeletePlatformDialog: Platform;
|
||||
showCreateCollectionDialog: null;
|
||||
showCreateSmartCollectionDialog: null;
|
||||
/** v1 — opens the legacy AddRoms picker. The v2 equivalent is
|
||||
/** v1: opens the legacy AddRoms picker. The v2 equivalent is
|
||||
* `showManageCollectionsDialog`; this entry stays while v1 still
|
||||
* consumes it.
|
||||
* @deprecated v2 → use `showManageCollectionsDialog`. */
|
||||
showAddToCollectionDialog: SimpleRom[];
|
||||
/** v2 — opens ManageCollectionsDialog with the given selection. */
|
||||
/** v2: opens ManageCollectionsDialog with the given selection. */
|
||||
showManageCollectionsDialog: SimpleRom[];
|
||||
/** v2 — fired by ManageCollectionsDialog when it closes, so the
|
||||
/** v2: fired by ManageCollectionsDialog when it closes, so the
|
||||
* GameActionBtn that opened it can drop its pinned-hover state. */
|
||||
closeManageCollectionsDialog: null;
|
||||
showRemoveFromCollectionDialog: SimpleRom[];
|
||||
showDeleteCollectionDialog: Collection;
|
||||
showDeleteSmartCollectionDialog: SmartCollection;
|
||||
showMatchRomDialog: SimpleRom;
|
||||
/** v2 — `rom` is optional; when provided, the dialog also fetches
|
||||
/** v2: `rom` is optional; when provided, the dialog also fetches
|
||||
* per-provider covers via `/search/roms` so the user can pick the
|
||||
* IGDB / MobyGames / Screenscraper / … artwork without going
|
||||
* through the manual-match flow. Collection-cover edits omit it. */
|
||||
@@ -49,7 +49,7 @@ export type Events = {
|
||||
updateUrlCover: string;
|
||||
showEditRomDialog: SimpleRom;
|
||||
showRefreshMetadataDialog: SimpleRom;
|
||||
/** v2-only — bulk refresh of multiple ROMs from the SelectionBar.
|
||||
/** v2-only: bulk refresh of multiple ROMs from the SelectionBar.
|
||||
* The v2 RefreshMetadataDialog listens to both this and the single
|
||||
* event; v1 keeps the single-rom contract. */
|
||||
showRefreshMetadataDialogBulk: SimpleRom[];
|
||||
@@ -125,7 +125,7 @@ export type Events = {
|
||||
files: File[];
|
||||
};
|
||||
playGame: number;
|
||||
// v2 only — generic confirmation dialog. Consumers go through
|
||||
// v2 only: generic confirmation dialog. Consumers go through
|
||||
// `useConfirm()`; the payload carries an id used to route the result
|
||||
// back via the matching `confirmResolved` event.
|
||||
showConfirm: {
|
||||
|
||||
@@ -805,7 +805,7 @@ export function getNintendoDSFiles(
|
||||
rom: DetailedRom | SimpleRom,
|
||||
): RomFileSchema[] {
|
||||
// `files` only ships on DetailedRom. Gallery surfaces pass SimpleRom,
|
||||
// where the inner-file check is impossible — return an empty list so
|
||||
// where the inner-file check is impossible: return an empty list so
|
||||
// the caller falls back to the extension check.
|
||||
const files = (rom as DetailedRom).files;
|
||||
if (!files) return [];
|
||||
@@ -824,8 +824,8 @@ export function getNintendoDSFiles(
|
||||
* Check if a ROM is a valid NDS/3DS/DSi game.
|
||||
*
|
||||
* Accepts both SimpleRom (gallery cards) and DetailedRom (detail view).
|
||||
* With SimpleRom the inner-file check is skipped — only the root
|
||||
* extension counts — which is what every gallery surface can ever see.
|
||||
* With SimpleRom the inner-file check is skipped, only the root
|
||||
* extension counts, which is what every gallery surface can ever see.
|
||||
*/
|
||||
export function isNintendoDSRom(rom: DetailedRom | SimpleRom): boolean {
|
||||
if (
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// The whole card is a RouterLink to the game detail view, so it participates
|
||||
// in spatial / gamepad nav (useWrapGridNav discovers it via the `a[href]` it
|
||||
// renders). No "live" badge: every card in this view is an active session by
|
||||
// definition, so the badge carried no per-card information — the page's
|
||||
// definition, so the badge carried no per-card information: the page's
|
||||
// session counter is the single live indicator.
|
||||
import { RAvatar, RBtn, RIcon } from "@v2/lib";
|
||||
import { useI18n } from "vue-i18n";
|
||||
@@ -17,7 +17,7 @@ import GameCover from "@/v2/components/shared/GameCover.vue";
|
||||
interface Props {
|
||||
to: RouteLocationRaw;
|
||||
coverSrc: string | null;
|
||||
/** Cover-art URL for the corner PIP — set only when `coverSrc` is a
|
||||
/** Cover-art URL for the corner PIP: set only when `coverSrc` is a
|
||||
* screenshot, so the game stays identifiable. Null shows no PIP. */
|
||||
pipCoverSrc?: string | null;
|
||||
romName: string;
|
||||
@@ -26,7 +26,7 @@ interface Props {
|
||||
avatarSrc: string;
|
||||
elapsedLabel: string;
|
||||
deviceType: string;
|
||||
/** Emulator core running the session, e.g. "RA Snes9x" — streaming
|
||||
/** Emulator core running the session, e.g. "RA Snes9x": streaming
|
||||
* sessions only. */
|
||||
emulatorLabel?: string | null;
|
||||
/** Admin-only: this is a streaming session an admin can force-release. */
|
||||
@@ -52,7 +52,7 @@ const { t } = useI18n();
|
||||
:title="romName"
|
||||
class="activity-card__art"
|
||||
>
|
||||
<!-- Cover-art PIP — shown when the main image is a screenshot, so the
|
||||
<!-- Cover-art PIP: shown when the main image is a screenshot, so the
|
||||
game stays identifiable. -->
|
||||
<CoverArtPip
|
||||
v-if="pipCoverSrc"
|
||||
@@ -105,7 +105,7 @@ const { t } = useI18n();
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* No card surface — the cover is the card (gallery-card vocabulary): a
|
||||
/* No card surface; the cover is the card (gallery-card vocabulary): a
|
||||
rounded art box that lifts on hover, with the title/meta stacked below on
|
||||
the bare page. */
|
||||
.activity-card {
|
||||
@@ -114,7 +114,7 @@ const { t } = useI18n();
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
/* Each card keeps its cover's natural width and wraps — no grow/shrink. */
|
||||
/* Each card keeps its cover's natural width and wraps: no grow/shrink. */
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// AppNav — the top navigation. Logo on the left, centred tab pill of
|
||||
// AppNav: the top navigation. Logo on the left, centred tab pill of
|
||||
// content destinations (Home / Platforms / Collections / Search), and
|
||||
// a right cluster of utility chrome (scanning indicator, the mini player on
|
||||
// phones, user menu).
|
||||
@@ -8,7 +8,7 @@
|
||||
// light up the Home tab.
|
||||
//
|
||||
// Library tools (Scan / Upload) are administrative actions, not content
|
||||
// destinations — they live in the user menu's Library group, keeping the
|
||||
// destinations: they live in the user menu's Library group, keeping the
|
||||
// primary nav focused on browsing destinations.
|
||||
import { RSliderBtnGroup, RImg } from "@v2/lib";
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
@@ -32,7 +32,7 @@ const { destinations: tabs, activeId: activeTab } = useNavDestinations();
|
||||
// bg / cover art reads cleanly. Once the user scrolls, a `::before`
|
||||
// pseudo-element fades in carrying the glass surface (bg + blur). The
|
||||
// blur is **static** on the pseudo, with only `opacity` transitioning
|
||||
// — transitioning `backdrop-filter` directly kept the blur layer alive
|
||||
// transitioning `backdrop-filter` directly kept the blur layer alive
|
||||
// and any hover repaint nearby would flash it.
|
||||
const { innerScrolled, innerGlass, handoff, threshold } = useNavGlass();
|
||||
const windowScrolled = ref(false);
|
||||
@@ -103,7 +103,7 @@ onBeforeUnmount(() => {
|
||||
The glass (bg + blur) lives on a `::before` pseudo-element so we
|
||||
can transition only its **opacity** between scrolled / not-scrolled
|
||||
instead of transitioning `backdrop-filter` directly. Transitioning
|
||||
backdrop-filter is notoriously expensive — the browser keeps the
|
||||
backdrop-filter is notoriously expensive: the browser keeps the
|
||||
blur layer "alive" the whole time and any nearby hover repaint
|
||||
invalidates it, producing a one-frame flicker. Static blur +
|
||||
opacity-only transition keeps the compositor layer stable. */
|
||||
@@ -211,7 +211,7 @@ onBeforeUnmount(() => {
|
||||
centre pill is dropped from the top nav and the bar keeps only the
|
||||
logo (far left) + user cluster (far right). With just those two
|
||||
children, collapse to a 2-column grid so the cluster tracks the right
|
||||
edge — the default `1fr auto 1fr` would auto-place it into the now-
|
||||
edge: the default `1fr auto 1fr` would auto-place it into the now-
|
||||
empty centre column instead of the trailing one. The gutter follows
|
||||
`--r-row-pad` (20px sm, 14px xs) so logo + user line up with the Home
|
||||
sections' content edge. */
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
// BackendStatusBanner — soft-red connection notice. Invisible while the
|
||||
// BackendStatusBanner: soft-red connection notice. Invisible while the
|
||||
// backend is healthy; surfaces a compact, centred toast-style card near the
|
||||
// top the moment the heartbeat probe (or a request) reports the backend as
|
||||
// down/broken. Unlike a redirect to /login, the user can keep navigating
|
||||
// wherever the cached state allows — the card just explains the degraded
|
||||
// wherever the cached state allows: the card just explains the degraded
|
||||
// state and the connection layer auto-recovers.
|
||||
//
|
||||
// Calling `useServerConnection()` here in <script setup> both gives us the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// BackgroundArt — the two-layer cross-fading backdrop. Views paint a cover
|
||||
// BackgroundArt: the two-layer cross-fading backdrop. Views paint a cover
|
||||
// URL via the injected `r-v2-set-background-art` provider; we swap between
|
||||
// layer A and layer B so there's always a smooth transition, never a flash.
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
// BottomNav — the primary-navigation pill, relocated to the bottom edge
|
||||
// BottomNav: the primary-navigation pill, relocated to the bottom edge
|
||||
// on phones (xs) so the four destinations sit within thumb reach. AppNav
|
||||
// drops its centre pill on xs in turn (see AppNav.vue).
|
||||
//
|
||||
// This is the SAME RSliderBtnGroup tab pill the top nav uses — same glass
|
||||
// surface, same border, same sliding active indicator — just pinned to
|
||||
// This is the SAME RSliderBtnGroup tab pill the top nav uses: same glass
|
||||
// surface, same border, same sliding active indicator: just pinned to
|
||||
// the bottom and stretched full-width. No bespoke nav surface and no
|
||||
// alternate layout, so the brand look and feel stay identical top vs
|
||||
// bottom. Routing and universal input (keyboard / gamepad roving,
|
||||
@@ -52,7 +52,7 @@ const { destinations, activeId } = useNavDestinations();
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Transparent — the pill itself carries the glass surface (tab variant),
|
||||
/* Transparent: the pill itself carries the glass surface (tab variant),
|
||||
so it reads as a floating control matching the top nav. Side gutters
|
||||
follow the responsive `--r-row-pad`; the safe-area inset keeps the pill
|
||||
above a notched phone's home indicator. `pointer-events: none` on the
|
||||
@@ -81,7 +81,7 @@ const { destinations, activeId } = useNavDestinations();
|
||||
|
||||
/* Each destination stretches to an equal quarter of the pill so the
|
||||
active indicator spans its full cell and reaches close to the pill's
|
||||
rounded background edges — and keeps doing so as the viewport widens,
|
||||
rounded background edges: and keeps doing so as the viewport widens,
|
||||
instead of leaving the items bunched in the centre. The indicator sits
|
||||
inside the pill's 4px padding, so even with sub-pixel cell rounding it
|
||||
never grazes the border. */
|
||||
@@ -90,7 +90,7 @@ const { destinations, activeId } = useNavDestinations();
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Icon-only on phones — four full-width inline labels won't fit, and the
|
||||
/* Icon-only on phones: four full-width inline labels won't fit, and the
|
||||
icon-only treatment matches the top-nav-on-xs precedent. The
|
||||
router-link `aria-label` (set by RSliderBtnGroup from each item's
|
||||
`ariaLabel`) stays the source of accessible naming. */
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
// CrtWarmup — a purely cosmetic "CRT phosphor warm-up" easter egg, fired when
|
||||
// CrtWarmup: a purely cosmetic "CRT phosphor warm-up" easter egg, fired when
|
||||
// CRT mode is switched ON (see useCrtMode / Settings → User Interface → Theme). Plays a fast, glitchy,
|
||||
// old-television power-on flash: instant black-out, a bright collapsing
|
||||
// scanline, a phosphor bloom with chromatic-aberration tearing, a sync-roll
|
||||
// band and jittering scanlines, then a quick settle back to the live app.
|
||||
//
|
||||
// Chrome-only — no stores/services/domain, no user-visible text (so it stays
|
||||
// Chrome-only: no stores/services/domain, no user-visible text (so it stays
|
||||
// trivially theme-agnostic). Teleported to <body> so it covers teleported
|
||||
// overlays too, and rendered pointer-events:none so it never traps input.
|
||||
// Auto-removes when the master timeline ends. Honours prefers-reduced-motion
|
||||
@@ -14,7 +14,7 @@ import { nextTick, ref } from "vue";
|
||||
|
||||
const visible = ref(false);
|
||||
// Bumping the key remounts the layer so the CSS animations restart from
|
||||
// frame 0 — lets the user re-trigger the gimmick mid-flash.
|
||||
// frame 0: lets the user re-trigger the gimmick mid-flash.
|
||||
const runId = ref(0);
|
||||
|
||||
async function play() {
|
||||
@@ -24,7 +24,7 @@ async function play() {
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
// `.self` so only the container's own master animation ends the run — child
|
||||
// `.self` so only the container's own master animation ends the run: child
|
||||
// layer animations bubble their animationend but must not cut the timeline
|
||||
// short. The master animation is the longest, so it fires last.
|
||||
function onEnded() {
|
||||
@@ -78,7 +78,7 @@ defineExpose({ play });
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
/* White-hot core fading out to phosphor green — the main "flash". */
|
||||
/* White-hot core fading out to phosphor green: the main "flash". */
|
||||
.r-crt__bloom {
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
@@ -105,7 +105,7 @@ defineExpose({ play });
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
|
||||
/* Chromatic-aberration tearing — red/cyan RGB-split fringes that jump
|
||||
/* Chromatic-aberration tearing: red/cyan RGB-split fringes that jump
|
||||
sideways in hard steps while the tube "locks on". */
|
||||
.r-crt__glitch {
|
||||
box-shadow:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
// DebugOverlay — developer-facing diagnostic panel pinned bottom-right
|
||||
// DebugOverlay: developer-facing diagnostic panel pinned bottom-right
|
||||
// (right side avoids clashing with the browser's own link-preview tooltip).
|
||||
//
|
||||
// Gated by `useDebugMode().enabled` (a per-device localStorage toggle in
|
||||
// Settings → Developer), NOT by `import.meta.env.DEV` — the whole point is
|
||||
// Settings → Developer), NOT by `import.meta.env.DEV`: the whole point is
|
||||
// being able to flip it on against the deployed build to debug the live
|
||||
// webui fast. It absorbs the old dev-only BreakpointBadge as its first row.
|
||||
//
|
||||
@@ -31,7 +31,7 @@ import { useInputModality } from "@/v2/composables/useInputModality";
|
||||
import { useThemeMode } from "@/v2/composables/useThemeMode";
|
||||
import { useVirtualScrollDebug } from "@/v2/composables/useVirtualScrollDebug";
|
||||
|
||||
// — Breakpoint + viewport ------------------------------------------------
|
||||
// Breakpoint + viewport ------------------------------------------------
|
||||
const { xs, smAndUp, mdAndUp, lgAndUp, xlAndUp } = useBreakpoint();
|
||||
const { width, height } = useWindowSize();
|
||||
const tier = computed(() => {
|
||||
@@ -43,20 +43,20 @@ const tier = computed(() => {
|
||||
return "—";
|
||||
});
|
||||
|
||||
// — Input modality -------------------------------------------------------
|
||||
// Input modality -------------------------------------------------------
|
||||
const { modality } = useInputModality();
|
||||
|
||||
// — Theme ----------------------------------------------------------------
|
||||
// Theme ----------------------------------------------------------------
|
||||
const { isDark } = useThemeMode();
|
||||
const themeLabel = computed(() => (isDark.value ? "v2-dark" : "v2-light"));
|
||||
|
||||
// — Route ----------------------------------------------------------------
|
||||
// Route ----------------------------------------------------------------
|
||||
const route = useRoute();
|
||||
const routeName = computed(() =>
|
||||
typeof route.name === "string" ? route.name : "—",
|
||||
);
|
||||
|
||||
// — Gamepad + focus ------------------------------------------------------
|
||||
// Gamepad + focus ------------------------------------------------------
|
||||
const padConnected = ref(false);
|
||||
const lastButton = ref("—");
|
||||
|
||||
@@ -77,7 +77,7 @@ useEventListener(
|
||||
},
|
||||
);
|
||||
|
||||
// Currently focused element — track via focusin (bubbles, unlike focus).
|
||||
// Currently focused element: track via focusin (bubbles, unlike focus).
|
||||
const focusEl = ref<Element | null>(null);
|
||||
function describeFocus(el: Element | null): string {
|
||||
if (!el || el === document.body || el === document.documentElement)
|
||||
@@ -101,13 +101,13 @@ useEventListener(document, "focusout", () => {
|
||||
focusEl.value = document.activeElement;
|
||||
});
|
||||
|
||||
// — Virtual scroll -------------------------------------------------------
|
||||
// Virtual scroll -------------------------------------------------------
|
||||
// Published by the active virtualised gallery (null elsewhere). If windowing
|
||||
// is healthy, `rendered` stays ~constant (viewport + 2×overscan) however far
|
||||
// you scroll; a climbing value means the DOM isn't being trimmed.
|
||||
const { stats: vstats } = useVirtualScrollDebug();
|
||||
|
||||
// — Performance ----------------------------------------------------------
|
||||
// Performance ----------------------------------------------------------
|
||||
const fps = useFps();
|
||||
const { isSupported: memSupported, memory } = useMemory();
|
||||
const heapMb = computed(() =>
|
||||
|
||||
@@ -85,7 +85,7 @@ const counterLabel = computed(() => {
|
||||
|
||||
<!-- Live progress bar pinned to the pill's bottom edge.
|
||||
Determinate when totals are known, indeterminate
|
||||
otherwise — both modes keep the bar visually busy. -->
|
||||
otherwise: both modes keep the bar visually busy. -->
|
||||
<RProgressLinear
|
||||
class="r-scan-indicator__progress"
|
||||
:indeterminate="!hasTotal"
|
||||
@@ -156,7 +156,7 @@ const counterLabel = computed(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Radar glyph stands in for the label on phones — hidden by default so
|
||||
/* Radar glyph stands in for the label on phones: hidden by default so
|
||||
the desktop pill keeps its word. */
|
||||
.r-scan-indicator__icon {
|
||||
display: none;
|
||||
@@ -178,7 +178,7 @@ html[data-bp~="xs"] .r-scan-indicator__icon {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
/* Enter / leave — opacity + slide-in from the right side, matching
|
||||
/* Enter / leave: opacity + slide-in from the right side, matching
|
||||
the prior implementation so navbar micro-interactions feel related. */
|
||||
.r-scan-indicator-enter-active {
|
||||
transition:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// LoginForm — username + password form. Emits `submit` on valid submission;
|
||||
// LoginForm: username + password form. Emits `submit` on valid submission;
|
||||
// the parent owns the API call + snackbars so this stays purely presentational.
|
||||
import { RBtn, RTextField } from "@v2/lib";
|
||||
import { ref } from "vue";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// OIDCButton — OIDC / SSO login button with the provider's dashboard-icon
|
||||
// OIDCButton: OIDC / SSO login button with the provider's dashboard-icon
|
||||
// (falls back to a generic key if the icon can't be found).
|
||||
import { RBtn, RIcon, RImg } from "@v2/lib";
|
||||
import { ref } from "vue";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// ResetForm — username-only form that triggers a password-reset email.
|
||||
// ResetForm: username-only form that triggers a password-reset email.
|
||||
// Emits `done` on success and `cancel` when the user backs out.
|
||||
import { RBtn, RTextField } from "@v2/lib";
|
||||
import { ref } from "vue";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// SetupStepAdmin — Step 2 of the setup wizard. Creates the first admin
|
||||
// SetupStepAdmin: Step 2 of the setup wizard. Creates the first admin
|
||||
// account that owns the library and manages other users.
|
||||
//
|
||||
// Two-column layout: left side carries the avatar picker + context (what
|
||||
@@ -233,7 +233,7 @@ html[data-bp~="sm-and-down"] .r-setup-admin {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--r-space-5);
|
||||
/* The body clips (overflow: hidden) so this step scrolls its own content,
|
||||
like the other steps — otherwise the stacked intro + form overflow and
|
||||
like the other steps: otherwise the stacked intro + form overflow and
|
||||
the last field (with its bottom margin) gets cut off. Align to the top so
|
||||
the scroll starts at the first row; the layer promotion keeps the region
|
||||
from collapsing when the form re-renders on input. */
|
||||
@@ -258,7 +258,7 @@ html[data-bp~="sm-and-down"] .r-setup-admin__intro {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Avatar — clickable round button sits next to a small trash icon that
|
||||
/* Avatar: clickable round button sits next to a small trash icon that
|
||||
removes the picked photo. The trash button stays disabled until a
|
||||
photo is picked so the affordance is discoverable without inviting a
|
||||
no-op click. It bottom-aligns next to the avatar so the danger glyph
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// CollectionListHeader — column-header strip for the Collections
|
||||
// CollectionListHeader: column-header strip for the Collections
|
||||
// list-mode view. Mirrors GameListHeader's anatomy: each sortable
|
||||
// column is a button that toggles asc → desc → asc on the parent's
|
||||
// sort state via the `sort` event.
|
||||
@@ -46,7 +46,7 @@ const sortOptions = computed(() =>
|
||||
function handleClick(col: CollectionListColumn) {
|
||||
if (!col.sortKey) return;
|
||||
// Toggle direction when re-clicking the active column; otherwise
|
||||
// start the new column at ascending — same rule as GameListHeader.
|
||||
// start the new column at ascending: same rule as GameListHeader.
|
||||
const nextDir: "asc" | "desc" =
|
||||
props.sortKey === col.sortKey && props.sortDir === "asc" ? "desc" : "asc";
|
||||
emit("sort", { key: col.sortKey, dir: nextDir });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// CollectionListRow — single row of the Collections list-mode index.
|
||||
// CollectionListRow: single row of the Collections list-mode index.
|
||||
//
|
||||
// Anatomy mirrors GameListRow: cover mosaic + name on the left, kind
|
||||
// chip in the middle, game count on the right. A public collection's
|
||||
@@ -64,7 +64,7 @@ const morphStyle = computed(() =>
|
||||
: undefined,
|
||||
);
|
||||
|
||||
// Kind cell — icon + label for every kind (the list view can afford the
|
||||
// Kind cell: icon + label for every kind (the list view can afford the
|
||||
// text). Icons match the grid cards.
|
||||
const kindBadge = computed<{ icon: string; label: string }>(() => {
|
||||
if (props.kind === "smart") {
|
||||
@@ -277,7 +277,7 @@ function onRowClick(e: MouseEvent) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Smart = accent (warm), virtual = brand (cool) — so the two computed
|
||||
/* Smart = accent (warm), virtual = brand (cool): so the two computed
|
||||
kinds read apart at a glance. */
|
||||
.coll-list-row__kind--smart {
|
||||
color: var(--r-color-brand-accent);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// CollectionMosaic — 2×2 cover grid for collection artwork. Feature
|
||||
// CollectionMosaic: 2×2 cover grid for collection artwork. Feature
|
||||
// component used by CollectionTile and the collection info panel.
|
||||
// Behaviour:
|
||||
// * 0 covers → empty state with a bookmark glyph
|
||||
@@ -30,7 +30,7 @@ const displayCovers = computed(() =>
|
||||
(props.covers ?? []).filter((c): c is string => Boolean(c)),
|
||||
);
|
||||
|
||||
// Mosaic slots — if we have 2 or 3 covers, cycle through them so every
|
||||
// Mosaic slots: if we have 2 or 3 covers, cycle through them so every
|
||||
// slot is filled. Deterministic (index % len) instead of random so the
|
||||
// mosaic doesn't shuffle between re-renders.
|
||||
const mosaicSlots = computed(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// CollectionPickerRow — one row in the ManageCollectionsDialog. Portrait
|
||||
// CollectionPickerRow: one row in the ManageCollectionsDialog. Portrait
|
||||
// thumb (globed when public) + name + rom-count + brand-primary circular
|
||||
// tick when checked.
|
||||
// Click toggles; parent owns the pending/checked state and handles the
|
||||
@@ -111,7 +111,7 @@ const isFull = computed(() => props.state === "all");
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
/* Portrait thumb — width tracks the configurable tile size; height is
|
||||
/* Portrait thumb: width tracks the configurable tile size; height is
|
||||
computed by CollectionMosaic from its 140/188 aspectRatio. */
|
||||
.pick-row__thumb {
|
||||
position: relative;
|
||||
@@ -161,7 +161,7 @@ const isFull = computed(() => props.state === "all");
|
||||
color: var(--r-color-overlay-fg);
|
||||
}
|
||||
|
||||
/* Indeterminate (`some`) — the brand colour reads as a hint but the
|
||||
/* Indeterminate (`some`): the brand colour reads as a hint but the
|
||||
tick swaps to a horizontal dash so users can tell "partially in" from
|
||||
"fully in" at a glance. */
|
||||
.pick-row--partial .pick-row__tick {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// CollectionTile — collection card (Home row + /collections grid). Feature
|
||||
// CollectionTile: collection card (Home row + /collections grid). Feature
|
||||
// composite that wraps CollectionMosaic + name + count. `kind` overlays
|
||||
// "Smart" or "Virtual" badge. `variant` controls sizing ("row" = 150px
|
||||
// fixed, "grid" = fills cell).
|
||||
@@ -29,7 +29,7 @@ interface Props {
|
||||
/** Owner-set visibility; public ones carry the globe. Virtual
|
||||
* collections are autogenerated and never carry a public/private state. */
|
||||
isPublic?: boolean;
|
||||
/** Stable id used to derive the shared-element morph tag. Optional —
|
||||
/** Stable id used to derive the shared-element morph tag. Optional:
|
||||
* if absent the tile still navigates but won't participate in the
|
||||
* morph (some callers pass a virtual/smart wrapper object). */
|
||||
id?: number | string;
|
||||
@@ -46,7 +46,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Kind badge — smart/virtual collapse to an icon chip; the tooltip
|
||||
// Kind badge: smart/virtual collapse to an icon chip; the tooltip
|
||||
// carries the word. Regular collections show no kind badge.
|
||||
const kindBadge = computed<{ icon: string; label: string } | null>(() => {
|
||||
if (props.kind === "smart") {
|
||||
@@ -149,7 +149,7 @@ const morphStyle = computed(() =>
|
||||
box-shadow var(--r-motion-fast);
|
||||
}
|
||||
|
||||
/* Hover gated to mouse/touch — keeps a parked cursor from competing
|
||||
/* Hover gated to mouse/touch: keeps a parked cursor from competing
|
||||
with the focused tile when driving with a gamepad. */
|
||||
html:not([data-input="pad"]) .coll-tile:hover .coll-tile__cover,
|
||||
.coll-tile:focus-visible .coll-tile__cover {
|
||||
@@ -157,7 +157,7 @@ html:not([data-input="pad"]) .coll-tile:hover .coll-tile__cover,
|
||||
box-shadow: var(--r-elev-3);
|
||||
}
|
||||
|
||||
/* Keyboard / gamepad focus — ring + brand glow on the cover, brighten
|
||||
/* Keyboard / gamepad focus: ring + brand glow on the cover, brighten
|
||||
the name. Matches the v1 console card visual language. */
|
||||
.coll-tile:focus-visible {
|
||||
outline: none;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// NewCollectionRow — the "create collection" CTA row inside the
|
||||
// NewCollectionRow: the "create collection" CTA row inside the
|
||||
// ManageCollectionsDialog. Collapsed: purple-tinted plus-tile + label.
|
||||
// Expanded: inline name input and visibility switch with Create / Cancel
|
||||
// actions.
|
||||
@@ -157,13 +157,13 @@ function onInput(e: Event) {
|
||||
/* Edge-to-edge CTA row matching the picker rows below. The host
|
||||
dialog (`ManageCollectionsDialog`) drops the standard RDialog body
|
||||
padding AND we drop the row's horizontal padding too, so the tile
|
||||
and actions go flush with the dialog walls — same "menu items, not
|
||||
and actions go flush with the dialog walls: same "menu items, not
|
||||
padded cards" idiom as RMenu. */
|
||||
.new-row {
|
||||
display: grid;
|
||||
/* First column tracks the configurable tile width so the label always
|
||||
starts past the tile, even when the consumer makes the tile bigger.
|
||||
Third column is `auto` — sizes to the actions when present, collapses
|
||||
Third column is `auto`: sizes to the actions when present, collapses
|
||||
to 0 when the Transition unmounts them. */
|
||||
grid-template-columns: var(--tile-w, 36px) 1fr auto;
|
||||
align-items: center;
|
||||
@@ -184,7 +184,7 @@ function onInput(e: Event) {
|
||||
background: color-mix(in srgb, var(--r-color-brand-primary) 5%, transparent);
|
||||
}
|
||||
|
||||
/* Portrait plus-tile — matches the CollectionMosaic footprint of the
|
||||
/* Portrait plus-tile: matches the CollectionMosaic footprint of the
|
||||
picker rows so the first column aligns whether you're looking at the
|
||||
CTA or an existing collection thumb. */
|
||||
.new-row__tile {
|
||||
@@ -207,7 +207,7 @@ function onInput(e: Event) {
|
||||
color: var(--r-color-overlay-fg);
|
||||
}
|
||||
|
||||
/* Label-as-button — left-aligned, no chrome so it reads as a row label
|
||||
/* Label-as-button: left-aligned, no chrome so it reads as a row label
|
||||
that happens to be focusable. */
|
||||
.new-row__cta {
|
||||
appearance: none;
|
||||
@@ -262,7 +262,7 @@ function onInput(e: Event) {
|
||||
color: color-mix(in srgb, var(--r-color-fg) 35%, transparent);
|
||||
}
|
||||
|
||||
/* Cancel/Create transition — slides in from the right on expand and
|
||||
/* Cancel/Create transition: slides in from the right on expand and
|
||||
slides back out on cancel. Both directions animate because the
|
||||
actions cluster lives outside the form, so its Vue Transition leave
|
||||
isn't cut short by a form unmount. */
|
||||
|
||||
@@ -11,7 +11,7 @@ export type CollectionListSortKey = "name" | "kind" | "rom_count";
|
||||
export interface CollectionListColumn {
|
||||
/** Unique column id. */
|
||||
key: CollectionListSortKey;
|
||||
/** i18n key for the header label — resolved in CollectionListHeader. */
|
||||
/** i18n key for the header label: resolved in CollectionListHeader. */
|
||||
labelKey: string;
|
||||
/** Sort axis when the column is sortable; omitted for display-only columns. */
|
||||
sortKey?: CollectionListSortKey;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// ControllerPad — renders a single connected Gamepad as the silhouette of
|
||||
// ControllerPad: renders a single connected Gamepad as the silhouette of
|
||||
// a generic controller: triggers + shoulders top corners, D-pad bottom-
|
||||
// left, face buttons in a diamond bottom-right, sticks below their
|
||||
// respective halves, and Back / Home / Start in the centre. Each element
|
||||
@@ -277,7 +277,7 @@ function magnitude(x: number, y: number) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Outer wrapper centres the silhouette and caps its width — at full
|
||||
/* Outer wrapper centres the silhouette and caps its width: at full
|
||||
container width the layout starts looking sparse. */
|
||||
.r-v2-pad__wrap {
|
||||
padding: 24px 16px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// v2 AboutDialog — emitter-driven. Replaces the v1 AboutDialog in the v2
|
||||
// v2 AboutDialog: emitter-driven. Replaces the v1 AboutDialog in the v2
|
||||
// GlobalDialogs stack so the "About" entry in UserMenu renders the v2 glass
|
||||
// panel instead of the legacy card.
|
||||
import { RDialog, RIcon, RImg, RTooltip } from "@v2/lib";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// AddPhysicalGameDialog — manually add a game you own physically (cartridge,
|
||||
// AddPhysicalGameDialog: manually add a game you own physically (cartridge,
|
||||
// disc, boxed copy) that has no file on disk. The backend stores it as a
|
||||
// file-less Rom (is_physical=true) and auto-links metadata by name/UPC in a
|
||||
// single quick scan.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// v2 ChangelogDialog — emitter-driven. Fetches the latest releases from
|
||||
// v2 ChangelogDialog: emitter-driven. Fetches the latest releases from
|
||||
// the public GitHub API on first open and reuses the cached payload on
|
||||
// subsequent opens within the same session. Each release renders as a
|
||||
// glass-panel block (tag + date) with the release body rendered through
|
||||
@@ -269,7 +269,7 @@ function closeDialog() {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* md-editor surface tweaks — the preview ships its own white card; we
|
||||
/* md-editor surface tweaks: the preview ships its own white card; we
|
||||
strip it and let the tinted body wrapper provide the surface. Same
|
||||
approach NotesTab uses. */
|
||||
.r-v2-changelog__body :deep(.md-editor),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// CopyDownloadLinkDialog — fallback surface for `useGameActions.copyDownloadLink`
|
||||
// CopyDownloadLinkDialog: fallback surface for `useGameActions.copyDownloadLink`
|
||||
// when the Clipboard API isn't usable (insecure context, denied
|
||||
// permission, older browsers). Renders the link in a selectable box so
|
||||
// the user can copy it by hand, and offers a Retry button that tries
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// CreateSmartCollectionDialog — captures the current galleryFilter state
|
||||
// CreateSmartCollectionDialog: captures the current galleryFilter state
|
||||
// as a smart collection. v2 replacement for v1's
|
||||
// `components/common/Collection/Dialog/CreateSmartCollection.vue`.
|
||||
//
|
||||
@@ -7,7 +7,7 @@
|
||||
// • Emitter fires `showCreateSmartCollectionDialog` (typically from
|
||||
// the FilterDrawer footer CTA).
|
||||
// • If no filters are active we refuse to open and surface a snackbar
|
||||
// hint — same guard v1 had, just routed through the v2 channel.
|
||||
// hint: same guard v1 had, just routed through the v2 channel.
|
||||
// • The dialog snapshots `galleryFilter` once on open (so further
|
||||
// toggles in the gallery don't leak into the preview) and lets the
|
||||
// user name / describe / mark public.
|
||||
@@ -65,14 +65,14 @@ const isPublic = ref(false);
|
||||
|
||||
// Snapshot the criteria once when the dialog opens. We don't want the
|
||||
// preview to keep updating as the user toggles filters in the gallery
|
||||
// behind the dialog — what the user saw at the moment they clicked the
|
||||
// behind the dialog: what the user saw at the moment they clicked the
|
||||
// CTA is what gets saved.
|
||||
const snapshot = ref<SmartFilterCriteria>({});
|
||||
|
||||
const nameRules = [required(t("common.required"))];
|
||||
|
||||
const openHandler = () => {
|
||||
// v1 guarded with a snackbar warning here — keep that behaviour so
|
||||
// v1 guarded with a snackbar warning here: keep that behaviour so
|
||||
// the user gets a hint instead of an empty preview.
|
||||
const next = buildSmartFilterCriteria(
|
||||
{
|
||||
@@ -361,7 +361,7 @@ html[data-bp~="sm-and-up"] .r-v2-csc__grid {
|
||||
border-radius: var(--r-radius-md);
|
||||
min-height: 200px;
|
||||
/* Bound the panel so a filter-heavy snapshot can't stretch the dialog
|
||||
past the viewport — the head stays pinned, the list scrolls. */
|
||||
past the viewport: the head stays pinned, the list scrolls. */
|
||||
max-height: clamp(200px, 48vh, 420px);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// DeleteManualDialog — confirms removing the primary manual from a ROM or
|
||||
// DeleteManualDialog: confirms removing the primary manual from a ROM or
|
||||
// a single manual file (multi-manual ROMs). The emitter payload picks the
|
||||
// scope.
|
||||
import { RBtn, RDialog, RIcon } from "@v2/lib";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
// ManageCollectionsDialog — collection picker that matches the mockup's
|
||||
// ManageCollectionsDialog: collection picker that matches the mockup's
|
||||
// menu-panel shape. Driven by the `showManageCollectionsDialog` emitter.
|
||||
//
|
||||
// Uses the v2 RDialog primitive — RDialog shares the RMenu visual
|
||||
// Uses the v2 RDialog primitive: RDialog shares the RMenu visual
|
||||
// language (14px radius, deep glass, menu-style shadow) so this picker
|
||||
// reads as a sibling of the user menu / ROM context menu.
|
||||
//
|
||||
@@ -14,7 +14,7 @@
|
||||
// "New Collection" label. Click expands to an inline Create /
|
||||
// Cancel input.
|
||||
// * Divider.
|
||||
// * List of owned collections — avatar, name, count, brand-primary
|
||||
// * List of owned collections: avatar, name, count, brand-primary
|
||||
// circular tick when on. Clicking toggles INSTANTLY (optimistic
|
||||
// update + API call; reverts on failure).
|
||||
// * Empty state if no collections exist.
|
||||
@@ -60,7 +60,7 @@ const roms = ref<SimpleRom[]>([]);
|
||||
const pendingCollections = ref(new Set<number>());
|
||||
// Optimistic state is now tri-state (off | some | all). When the user
|
||||
// clicks a row we paint the *resolved* next state immediately and
|
||||
// reconcile when the API responds — `undefined` means "fall back to
|
||||
// reconcile when the API responds: `undefined` means "fall back to
|
||||
// the collection's actual membership computed against `roms.value`".
|
||||
const optimistic = ref(new Map<number, "off" | "some" | "all">());
|
||||
|
||||
@@ -86,7 +86,7 @@ emitter?.on("showManageCollectionsDialog", openHandler);
|
||||
onBeforeUnmount(() => emitter?.off("showManageCollectionsDialog", openHandler));
|
||||
|
||||
// Notify any GameActionBtn that opened us so it can drop its pinned-hover
|
||||
// state — covers every close path (X, scrim, Escape, programmatic close).
|
||||
// state: covers every close path (X, scrim, Escape, programmatic close).
|
||||
watch(show, (open) => {
|
||||
if (!open) emitter?.emit("closeManageCollectionsDialog", null);
|
||||
});
|
||||
@@ -94,7 +94,7 @@ watch(show, (open) => {
|
||||
/** Resolve a collection's membership against the open dialog's
|
||||
* selection. "all" when every selected rom is in the collection,
|
||||
* "some" when at least one (but not all) is, "off" otherwise. The
|
||||
* "some" state only appears for bulk dialogs — a single-rom dialog
|
||||
* "some" state only appears for bulk dialogs: a single-rom dialog
|
||||
* collapses to off / all. */
|
||||
function membershipState(collection: Collection): "off" | "some" | "all" {
|
||||
const override = optimistic.value.get(collection.id);
|
||||
@@ -125,7 +125,7 @@ async function toggle(collection: Collection) {
|
||||
pendingCollections.value.add(collection.id);
|
||||
|
||||
// For the "some → all" transition we still issue an add against all
|
||||
// selected ids — the backend de-dupes against existing membership,
|
||||
// selected ids: the backend de-dupes against existing membership,
|
||||
// so this is safe and saves a per-id diff round-trip from the
|
||||
// frontend.
|
||||
//
|
||||
@@ -205,7 +205,7 @@ const subtitle = computed(() => {
|
||||
|
||||
const ownedCollections = computed(() => collectionsStore.ownedCollections);
|
||||
|
||||
// Header cover thumbnail — single-rom invocations get the game's cover
|
||||
// Header cover thumbnail: single-rom invocations get the game's cover
|
||||
// alongside the title (same `<GameCard size="xs" decorative>` shape
|
||||
// the list-mode row uses, so the header reads as a sibling of the
|
||||
// gallery surface). Bulk invocations stay text-only since one cover
|
||||
@@ -268,7 +268,7 @@ function closeDialog() {
|
||||
|
||||
<RDivider v-if="ownedCollections.length > 0" full-width />
|
||||
|
||||
<!-- Existing collection rows — instant toggle, no commit step. -->
|
||||
<!-- Existing collection rows: instant toggle, no commit step. -->
|
||||
<ul v-if="ownedCollections.length" class="r-v2-mng-coll__list">
|
||||
<li v-for="collection in ownedCollections" :key="collection.id">
|
||||
<CollectionPickerRow
|
||||
@@ -288,7 +288,7 @@ function closeDialog() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Header — optional GameCard thumbnail + stacked title (14px bold) +
|
||||
/* Header: optional GameCard thumbnail + stacked title (14px bold) +
|
||||
subtitle (11.5px muted) inside RDialog's single header slot. */
|
||||
.r-v2-mng-coll__head {
|
||||
display: flex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// RefreshMetadataDialog — kicks off a per-ROM (or bulk) metadata
|
||||
// RefreshMetadataDialog: kicks off a per-ROM (or bulk) metadata
|
||||
// re-scan. Shares the visual vocabulary of the Scan view config card:
|
||||
// provider selects split into General / Specific, hash-matcher proxies
|
||||
// rendered as switch pills, and a scan-type select with two per-ROM-
|
||||
@@ -34,7 +34,7 @@ const { t } = useI18n();
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const snackbar = useSnackbar();
|
||||
const show = ref(false);
|
||||
// Accept either a single rom or an array — the SelectionBar passes
|
||||
// Accept either a single rom or an array: the SelectionBar passes
|
||||
// many at once, individual menus pass one. Internally we always
|
||||
// normalise to an array so the scan emit groups by platform without
|
||||
// branching on the input shape.
|
||||
@@ -147,7 +147,7 @@ const singleRomTitle = computed(() => {
|
||||
function onScan() {
|
||||
if (roms.value.length === 0) return;
|
||||
|
||||
// Group rom ids by platform — the scan socket event accepts one
|
||||
// Group rom ids by platform: the scan socket event accepts one
|
||||
// platform list + one rom-id list, so a selection that spans
|
||||
// multiple platforms is fanned into N events, one per platform.
|
||||
const byPlatform = new Map<number, number[]>();
|
||||
@@ -205,7 +205,7 @@ function closeDialog() {
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="r-v2-refresh">
|
||||
<!-- ROM identity row — cover + name for the single-ROM case
|
||||
<!-- ROM identity row: cover + name for the single-ROM case
|
||||
(visual consistency with EditRomDialog / DeleteRomDialog),
|
||||
count chip for bulk. -->
|
||||
<div v-if="singleRom" class="r-v2-refresh__rom">
|
||||
@@ -237,7 +237,7 @@ function closeDialog() {
|
||||
<span>{{ t("rom.selection-count", { n: roms.length }) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 1. Providers section — General + Specific RSelects, sharing
|
||||
<!-- 1. Providers section: General + Specific RSelects, sharing
|
||||
one `metadataSources` model. Both render icon-only chips so
|
||||
a multi-select stays visually quiet in the activator. -->
|
||||
<section class="r-v2-refresh__section">
|
||||
@@ -298,7 +298,7 @@ function closeDialog() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LaunchBox Local/Cloud toggle — inline inside its
|
||||
<!-- LaunchBox Local/Cloud toggle: inline inside its
|
||||
dropdown row, disabled until LaunchBox itself is
|
||||
selected. Same pattern as Scan.vue. -->
|
||||
<div
|
||||
@@ -394,7 +394,7 @@ function closeDialog() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Proxies (hash matchers) — compact switch pills, same as
|
||||
<!-- 2. Proxies (hash matchers): compact switch pills, same as
|
||||
the Scan view. -->
|
||||
<section class="r-v2-refresh__section">
|
||||
<h3 class="r-v2-refresh__section-title">
|
||||
@@ -443,7 +443,7 @@ function closeDialog() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. Scan type — per-ROM friendly options. -->
|
||||
<!-- 3. Scan type: per-ROM friendly options. -->
|
||||
<section class="r-v2-refresh__section">
|
||||
<h3 class="r-v2-refresh__section-title">
|
||||
{{ t("scan.section-scan-type") }}
|
||||
@@ -508,7 +508,7 @@ function closeDialog() {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* ROM identity row — cover + name + filename. Mirrors the row layout
|
||||
/* ROM identity row: cover + name + filename. Mirrors the row layout
|
||||
in DeleteRomDialog so the two "do-something-with-this-ROM" dialogs
|
||||
read as siblings. */
|
||||
.r-v2-refresh__rom {
|
||||
@@ -576,7 +576,7 @@ function closeDialog() {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
/* Section vocabulary — small uppercase label above the controls,
|
||||
/* Section vocabulary: small uppercase label above the controls,
|
||||
hairline divider between sections. Same rhythm as Scan.vue. */
|
||||
.r-v2-refresh__section {
|
||||
display: flex;
|
||||
@@ -598,7 +598,7 @@ function closeDialog() {
|
||||
color: var(--r-color-fg-muted);
|
||||
}
|
||||
|
||||
/* Provider groups (General / Specific) — same layout as Scan.vue: a
|
||||
/* Provider groups (General / Specific) use the same layout as Scan.vue: a
|
||||
small caption above each RSelect, two groups stacked with a tight
|
||||
inter-group margin. */
|
||||
.r-v2-refresh__providers-group {
|
||||
@@ -616,7 +616,7 @@ function closeDialog() {
|
||||
color: var(--r-color-fg-faint);
|
||||
}
|
||||
|
||||
/* Icon-only chip rendered in the activator — keeps the multi-select
|
||||
/* Icon-only chip rendered in the activator: keeps the multi-select
|
||||
visually quiet when many providers are picked. */
|
||||
.r-v2-refresh__provider-chip {
|
||||
display: inline-flex;
|
||||
@@ -624,7 +624,7 @@ function closeDialog() {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Hash matcher pills — same compact icon + switch rows as Scan.vue. */
|
||||
/* Hash matcher pills: same compact icon + switch rows as Scan.vue. */
|
||||
.r-v2-refresh__matchers {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -69,7 +69,7 @@ const show = ref(false);
|
||||
const searching = ref(false);
|
||||
const searchText = ref("");
|
||||
const covers = ref<SearchCoverSchema[]>([]);
|
||||
// Source rom for the in-flight search — drives the optional
|
||||
// Source rom for the in-flight search: drives the optional
|
||||
// `/search/roms` companion call. Reset on close so a follow-up open
|
||||
// without a `rom` payload (e.g. from CollectionSettingsDrawer) doesn't
|
||||
// inherit a stale rom.
|
||||
@@ -101,7 +101,7 @@ function gridProviderLogo(key: CoverProvider): string {
|
||||
// Client-side filtering + sorting over the two fetched lists. The
|
||||
// backend returns every content variant (NSFW / humor / epilepsy) with
|
||||
// its per-cover metadata in one call, so the controls just narrow the
|
||||
// already-loaded results — no re-fetch.
|
||||
// already-loaded results: no re-fetch.
|
||||
const {
|
||||
coverType,
|
||||
resolutionFilter,
|
||||
@@ -138,7 +138,7 @@ const sortLabel = computed(
|
||||
() => sortItems.value.find((item) => item.id === sortMode.value)?.label,
|
||||
);
|
||||
|
||||
// SGDB animated covers ship their `thumb` as a `.webm` clip — an `<img>`
|
||||
// SGDB animated covers ship their `thumb` as a `.webm` clip: an `<img>`
|
||||
// element can't render those (broken-image icon). Detect by type or
|
||||
// extension and swap to a `<video>` for those resources so the preview
|
||||
// actually plays.
|
||||
@@ -247,8 +247,8 @@ async function doSearch() {
|
||||
) {
|
||||
const results = providersResult.value.data;
|
||||
// Prefer the result that shares the most IDs with our rom (the
|
||||
// same identified game). When no result matches by ID — e.g. an
|
||||
// unidentified rom — fall back to the first one so the user
|
||||
// same identified game). When no result matches by ID: e.g. an
|
||||
// unidentified rom: fall back to the first one so the user
|
||||
// still gets something. `0` score with a populated `results`
|
||||
// array still picks `results[0]`.
|
||||
const best = [...results]
|
||||
@@ -291,7 +291,7 @@ function pickCover(url: string, provider: CoverProvider) {
|
||||
}
|
||||
|
||||
// Provider covers (IGDB / Moby / SS / …) already come at full
|
||||
// resolution from `/search/roms`, so no thumb-→-grid swap — just hand
|
||||
// resolution from `/search/roms`, so no thumb-→-grid swap: just hand
|
||||
// the URL off to the consumer.
|
||||
function pickProviderCover(url: string) {
|
||||
emitter?.emit("updateUrlCover", url);
|
||||
@@ -505,7 +505,7 @@ function closeDialog() {
|
||||
</div>
|
||||
|
||||
<div v-else-if="hasResults" class="r-v2-sgdb__results">
|
||||
<!-- Provider covers — one card per metadata source that
|
||||
<!-- Provider covers: one card per metadata source that
|
||||
returned a URL for this rom. Renders only when the
|
||||
dialog was opened with a `rom` payload; the provider
|
||||
logo overlays the card so the user can read at a
|
||||
@@ -696,7 +696,7 @@ html[data-bp~="xs"] .r-v2-sgdb__content-toggles {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Flow-pack of cover cards — each tile adopts its cover's natural aspect
|
||||
/* Flow-pack of cover cards: each tile adopts its cover's natural aspect
|
||||
(fixed height, width follows the art), like the gallery cards, instead of
|
||||
a rigid uniform-width grid. */
|
||||
.r-v2-sgdb__grid {
|
||||
@@ -730,7 +730,7 @@ html[data-bp~="xs"] .r-v2-sgdb__content-toggles {
|
||||
}
|
||||
.r-v2-sgdb__cover-img {
|
||||
display: block;
|
||||
/* Fixed height, natural width — the card takes the cover's true aspect,
|
||||
/* Fixed height, natural width: the card takes the cover's true aspect,
|
||||
never cropped. `max-width` caps the rare ultra-wide cover to the tile so
|
||||
it letterboxes instead of overflowing past its rounded corners. */
|
||||
height: 180px;
|
||||
@@ -739,7 +739,7 @@ html[data-bp~="xs"] .r-v2-sgdb__content-toggles {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Provider logo overlay — top-right corner badge on the cover so the
|
||||
/* Provider logo overlay: top-right corner badge on the cover so the
|
||||
user sees at a glance which metadata source the artwork belongs to. */
|
||||
.r-v2-sgdb__cover-provider {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// ShowQRCodeDialog — emitter-driven QR for downloading a single ROM from a
|
||||
// ShowQRCodeDialog: emitter-driven QR for downloading a single ROM from a
|
||||
// handheld/phone. The qrcode library renders into the canvas directly
|
||||
// after the dialog opens (nextTick so the canvas is in the DOM).
|
||||
import { RDialog } from "@v2/lib";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
// AdditionalDetails (v2) — manual metadata overrides for the edit-ROM
|
||||
// AdditionalDetails (v2): manual metadata overrides for the edit-ROM
|
||||
// dialog. Companies / genres / franchises / game modes / age ratings
|
||||
// are multi-tag combobox fields; first release date is a date picker;
|
||||
// YouTube video ID is a plain text field.
|
||||
//
|
||||
// All writes go through `manual_metadata` — the backend layer where
|
||||
// All writes go through `manual_metadata`: the backend layer where
|
||||
// admins override automatic scraper output. Emitting `update:rom` with
|
||||
// the merged shape keeps the parent in charge of when to actually
|
||||
// commit (Save button on EditRomDialog).
|
||||
@@ -54,7 +54,7 @@ const GAME_MODE_ITEMS = computed(() => [
|
||||
|
||||
const manual = computed(() => props.rom.manual_metadata || {});
|
||||
|
||||
// Age ratings are stored as `SYSTEM:RATING` strings — convert to the
|
||||
// Age ratings are stored as `SYSTEM:RATING` strings: convert to the
|
||||
// human-readable `SYSTEM - RATING` shape the combobox uses, and back.
|
||||
const ageRatingsView = computed(() =>
|
||||
(manual.value.age_ratings ?? []).map((r) => r.replace(":", " - ")),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
// MetadataIdSection (v2) — provider ID inputs for the edit-ROM dialog.
|
||||
// MetadataIdSection (v2): provider ID inputs for the edit-ROM dialog.
|
||||
// Each field maps a single metadata provider's ID (IGDB / MobyGames /
|
||||
// ScreenScraper / RetroAchievements / LaunchBox / SteamGridDB /
|
||||
// Hasheous / Flashpoint / HowLongToBeat) so admins can manually fix
|
||||
// up mismatched scrapes.
|
||||
//
|
||||
// Feature composite — knows UpdateRom; lives under
|
||||
// Feature composite: knows UpdateRom; lives under
|
||||
// `components/EditRom/` next to its siblings.
|
||||
import { RTextField } from "@v2/lib";
|
||||
import { computed } from "vue";
|
||||
@@ -22,7 +22,7 @@ const emit = defineEmits<{
|
||||
const { t } = useI18n();
|
||||
|
||||
// `field` is keyof UpdateRom; the parsed value is `number | string |
|
||||
// null` — most providers use numeric ids, Flashpoint stores a string.
|
||||
// null`: most providers use numeric ids, Flashpoint stores a string.
|
||||
function updateField(field: keyof UpdateRom, value: string | number | null) {
|
||||
emit("update:rom", { ...props.rom, [field]: value });
|
||||
}
|
||||
@@ -41,7 +41,7 @@ function parseIdValue(
|
||||
return Number.isNaN(n) ? null : n;
|
||||
}
|
||||
|
||||
// Field config — keeps the markup tight and lets us tweak the layout
|
||||
// Field config: keeps the markup tight and lets us tweak the layout
|
||||
// without re-hand-writing nine identical RTextField blocks.
|
||||
interface IdField {
|
||||
key: keyof UpdateRom;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
// RawMetadataPanel (v2) — textarea + action row for one provider's raw
|
||||
// RawMetadataPanel (v2): textarea + action row for one provider's raw
|
||||
// metadata payload. The visible "what provider is this" affordance is
|
||||
// owned by whoever hosts the panel (today: the active tab in
|
||||
// EditRomDialog's RTabNav, which shows the provider logo + label).
|
||||
// That lets this component drop a redundant title and keep the surface
|
||||
// quiet — the JSON is the content.
|
||||
// quiet: the JSON is the content.
|
||||
//
|
||||
// Feature composite — knows the UpdateRom shape and the emitter event
|
||||
// Feature composite: knows the UpdateRom shape and the emitter event
|
||||
// bus. Renders nothing if the rom has no stored data for the configured
|
||||
// provider.
|
||||
import { RBtn, RTextField } from "@v2/lib";
|
||||
@@ -57,7 +57,7 @@ function cancelEdit() {
|
||||
}
|
||||
|
||||
function saveMetadata() {
|
||||
// Empty wipe — reset this provider's slot to `{}` in raw_metadata.
|
||||
// Empty wipe: reset this provider's slot to `{}` in raw_metadata.
|
||||
if (!metadataJson.value || metadataJson.value.trim() === "") {
|
||||
emit("update:rom", {
|
||||
...props.rom,
|
||||
@@ -90,7 +90,7 @@ function saveMetadata() {
|
||||
}
|
||||
}
|
||||
|
||||
// aria-only — RTextField turns `label` into the input's aria-label when
|
||||
// aria-only: RTextField turns `label` into the input's aria-label when
|
||||
// no `prefix-label` is set. No visible chrome rendered for it.
|
||||
const ariaLabel = computed(() => `${props.label} ${t("rom.metadata")} JSON`);
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
// AlphaStrip — A-Z-# jump sidebar for letter-grouped grids (Platform and
|
||||
// Collection gallery). Feature composite — not a design-system primitive.
|
||||
// AlphaStrip: A-Z-# jump sidebar for letter-grouped grids (Platform and
|
||||
// Collection gallery). Feature composite: not a design-system primitive.
|
||||
//
|
||||
// Two highlight signals are supported:
|
||||
// * `current` — single letter used to mark a deliberate jump (e.g. the
|
||||
// * `current`: single letter used to mark a deliberate jump (e.g. the
|
||||
// user clicked "F"). One active at a time.
|
||||
// * `visible` — a Set of every letter whose section currently intersects
|
||||
// * `visible`: a Set of every letter whose section currently intersects
|
||||
// the viewport. Multiple letters light up together when the first row
|
||||
// of the grid spans several groups (A, B, C, …).
|
||||
//
|
||||
@@ -22,9 +22,9 @@ defineOptions({ inheritAttrs: false });
|
||||
const { t } = useI18n();
|
||||
|
||||
// Two non-alphabetic buckets bookend the alphabet:
|
||||
// * `#` — digits (0-9) — sits BEFORE A
|
||||
// * `@` — any other non-alphanumeric first character — sits AFTER Z
|
||||
// Order in asc: `# A B … Z @` — `#` at the top, `@` at the bottom.
|
||||
// * `#`, digits (0-9), sits BEFORE A
|
||||
// * `@`, any other non-alphanumeric first character, sits AFTER Z
|
||||
// Order in asc: `# A B … Z @`, `#` at the top, `@` at the bottom.
|
||||
// When the gallery sorts desc the whole array reverses (`@` to the top,
|
||||
// `#` to the bottom) so the strip's visual order tracks the data's
|
||||
// order and the scroll-spy highlight follows the scroll direction.
|
||||
@@ -34,7 +34,7 @@ interface Props {
|
||||
available?: Set<string> | string[];
|
||||
current?: string;
|
||||
visible?: Set<string> | string[];
|
||||
/** Render order — reversed when the gallery sorts descending. */
|
||||
/** Render order: reversed when the gallery sorts descending. */
|
||||
direction?: "asc" | "desc";
|
||||
/** Lay the letters out as a wrapping grid instead of a column. */
|
||||
grid?: boolean;
|
||||
@@ -140,7 +140,7 @@ watch(
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: 8px 0;
|
||||
/* Breathe away from the viewport edge — the strip shouldn't touch the
|
||||
/* Breathe away from the viewport edge: the strip shouldn't touch the
|
||||
right border of the gallery section. */
|
||||
margin-right: var(--r-alpha-strip-gap, 12px);
|
||||
user-select: none;
|
||||
@@ -175,7 +175,7 @@ watch(
|
||||
background: var(--r-color-surface);
|
||||
}
|
||||
|
||||
/* Scroll-spied letter — primary brand colour to stand out against the
|
||||
/* Scroll-spied letter: primary brand colour to stand out against the
|
||||
plain-white `--has` letters. */
|
||||
.alpha-strip__btn--current,
|
||||
.alpha-strip__btn--has.alpha-strip__btn--current {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
// CollectionHead — the collection-context strip that sits above the
|
||||
// CollectionHead: the collection-context strip that sits above the
|
||||
// gallery / settings tab body. Mirrors `PlatformHead` in shape: an
|
||||
// InfoPanel (4-cover mosaic + eyebrow + name + description chip +
|
||||
// stats) plus the RTabNav underneath. Re-used in two render branches
|
||||
// inside Collection.vue:
|
||||
//
|
||||
// 1. Library tab — passed to `GalleryShell`'s `#header` slot so the
|
||||
// 1. Library tab: passed to `GalleryShell`'s `#header` slot so the
|
||||
// head scrolls naturally with the cards (and the toolbar pins
|
||||
// below it).
|
||||
//
|
||||
@@ -121,7 +121,7 @@ defineEmits<{
|
||||
}
|
||||
|
||||
/* Match the collection-index card covers (which fill a ~150px grid cell on
|
||||
phones) — the InfoPanel stacks and centres the cover on xs, so there's room
|
||||
phones): the InfoPanel stacks and centres the cover on xs, so there's room
|
||||
for a proper hero instead of the old cramped 100px thumbnail. */
|
||||
html[data-bp~="xs"] .r-v2-coll__panel-cover {
|
||||
width: var(--r-coll-cover-w-xs);
|
||||
@@ -130,7 +130,7 @@ html[data-bp~="xs"] .r-v2-coll__panel-cover {
|
||||
|
||||
.r-v2-coll__tabs {
|
||||
/* Tuck the nav up against the InfoPanel's bottom padding so the
|
||||
two read as a single head band — same vocabulary as PlatformHead. */
|
||||
two read as a single head band: same vocabulary as PlatformHead. */
|
||||
margin-top: -8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// CollectionSettingsTab — collection-scoped settings rendered as the
|
||||
// CollectionSettingsTab: collection-scoped settings rendered as the
|
||||
// `Settings` tab inside Collection.vue. Same content surface as the
|
||||
// previous `CollectionSettingsDrawer`, now lives inline in the
|
||||
// collection view. One component, two branches (drawer-era split):
|
||||
@@ -8,12 +8,12 @@
|
||||
// (SteamGridDB search · file upload · remove), filter_criteria
|
||||
// not applicable.
|
||||
// * smart → name / description / public, filter_criteria displayed
|
||||
// read-only (editing the criteria isn't surfaced here — happens
|
||||
// read-only (editing the criteria isn't surfaced here: happens
|
||||
// via the create-smart-collection flow).
|
||||
//
|
||||
// Delete moved into this tab's danger zone (was a kebab menu item in
|
||||
// the pre-tabs design). The destructive flow is still owned by the
|
||||
// parent view — `@delete` emit triggers the confirm + router
|
||||
// parent view: `@delete` emit triggers the confirm + router
|
||||
// navigation in `Collection.vue`.
|
||||
//
|
||||
// Cover artwork flow:
|
||||
@@ -127,7 +127,7 @@ const dirty = computed(() => {
|
||||
// changes (route swap, socket-driven backend update). Keeps the form
|
||||
// in sync with the canonical record without trampling in-flight edits
|
||||
// while a save is running. Accepts an explicit source so `save()` can
|
||||
// re-sync from the update response — `props.collection` only receives
|
||||
// re-sync from the update response: `props.collection` only receives
|
||||
// the fresh value asynchronously (store swap + parent reassign), so it
|
||||
// still holds the pre-save object at the moment `save()` snapshots.
|
||||
function snapshot(source: Collection | SmartCollection = props.collection) {
|
||||
@@ -180,7 +180,7 @@ const mosaicFallback = computed<string[]>(() => {
|
||||
return (c.path_covers_small ?? []).slice(0, 4).map(toWebp);
|
||||
});
|
||||
|
||||
// Smart-collection filter criteria — read-only display. The summary
|
||||
// Smart-collection filter criteria: read-only display. The summary
|
||||
// helper translates the raw JSON into a structured list of rows.
|
||||
function platformLookup(id: number): string | null {
|
||||
return allPlatforms.value.find((p) => p.id === id)?.display_name ?? null;
|
||||
@@ -283,7 +283,7 @@ async function save() {
|
||||
snackbar.success(t("collection.updated", "Collection updated"), {
|
||||
icon: "mdi-check-bold",
|
||||
});
|
||||
// Re-sync from the response, not the prop — the prop hasn't been
|
||||
// Re-sync from the response, not the prop: the prop hasn't been
|
||||
// updated yet (see `snapshot` note), so snapshotting it would revert
|
||||
// the form to the pre-save values and keep `dirty` true.
|
||||
if (syncSaved(saved, target)) snapshot(saved);
|
||||
@@ -375,7 +375,7 @@ async function setVisibility(next: boolean) {
|
||||
|
||||
<template>
|
||||
<div class="r-settings-column">
|
||||
<!-- Cover artwork — regular collections only. Smart collections
|
||||
<!-- Cover artwork: regular collections only. Smart collections
|
||||
derive their cover from the contained ROMs at runtime, so an
|
||||
upload UI here would be misleading. -->
|
||||
<section v-if="kind === 'regular'" class="r-v2-coll-set__section">
|
||||
@@ -432,7 +432,7 @@ async function setVisibility(next: boolean) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Details (edit form) — both kinds. -->
|
||||
<!-- Details (edit form): both kinds. -->
|
||||
<section class="r-v2-coll-set__section">
|
||||
<header class="r-section-head">
|
||||
<RIcon icon="mdi-information-outline" size="14" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// DeleteFirmwareDialog — RDialog that confirms deletion of one or more
|
||||
// DeleteFirmwareDialog: RDialog that confirms deletion of one or more
|
||||
// firmware files and lets the user opt-in (per item) to also delete
|
||||
// the underlying file from disk. The actual deletion is performed by
|
||||
// the parent (FirmwareDrawer) via the `onConfirm` prop so the dialog
|
||||
@@ -48,7 +48,7 @@ const {
|
||||
clear: clearFs,
|
||||
} = useIdSelection(() => props.firmware);
|
||||
|
||||
// Each open trip starts with no filesystem deletes selected — the
|
||||
// Each open trip starts with no filesystem deletes selected: the
|
||||
// user has to opt in deliberately. Resetting when modelValue flips
|
||||
// from false → true prevents a previous-session selection from
|
||||
// resurfacing.
|
||||
@@ -143,7 +143,7 @@ async function confirm() {
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Warning strip — only painted when at least one row will hit
|
||||
<!-- Warning strip: only painted when at least one row will hit
|
||||
the filesystem, so the destructive consequence is impossible
|
||||
to miss. -->
|
||||
<p v-if="fsCount > 0" class="r-v2-del-fw__warn">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// FilterDrawer (v2) — gallery filter side panel. v2-native replacement
|
||||
// FilterDrawer (v2): gallery filter side panel. v2-native replacement
|
||||
// for `src/components/Gallery/AppBar/common/FilterDrawer/Base.vue`.
|
||||
//
|
||||
// Surface area (matches v1 1:1 so URLs stay compatible):
|
||||
@@ -10,15 +10,15 @@
|
||||
// where you can mix platforms).
|
||||
// • 11 multi-select filter groups (genres / franchises / collections /
|
||||
// companies / age-ratings / regions / languages / tags /
|
||||
// player-counts / metadata-providers / statuses) — each paired with
|
||||
// player-counts / metadata-providers / statuses): each paired with
|
||||
// an AND/OR/NONE logic toggle.
|
||||
// • Game length: an open-ended HowLongToBeat main-story range in hours.
|
||||
// • Reset button at the bottom.
|
||||
//
|
||||
// Apply is implicit — the URL composable + galleryRoms watcher refresh
|
||||
// Apply is implicit: the URL composable + galleryRoms watcher refresh
|
||||
// results on every store change. There is no "Apply" button.
|
||||
//
|
||||
// Feature composite — knows the galleryFilter store layout and v2
|
||||
// Feature composite: knows the galleryFilter store layout and v2
|
||||
// primitives. Mounted by GalleryShell so it's available everywhere a
|
||||
// gallery is rendered. The shell controls `modelValue` and forwards
|
||||
// `showPlatformsFilter`.
|
||||
@@ -503,7 +503,7 @@ function resetAll() {
|
||||
filter.setSelectedFilterLengthHours(null, null);
|
||||
}
|
||||
|
||||
// Hand off to CreateSmartCollectionDialog — closing the drawer first
|
||||
// Hand off to CreateSmartCollectionDialog: closing the drawer first
|
||||
// keeps focus management clean (the drawer's escape stack pops before
|
||||
// the dialog pushes its own).
|
||||
function saveAsSmartCollection() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// FirmwareTab — platform-scoped firmware manager rendered as the
|
||||
// FirmwareTab: platform-scoped firmware manager rendered as the
|
||||
// `Firmware` tab inside Platform.vue. Lists every firmware file
|
||||
// associated with the platform and lets admins upload, download, or
|
||||
// delete files. Same content surface as the previous
|
||||
@@ -65,7 +65,7 @@ const {
|
||||
toggleAll,
|
||||
} = useIdSelection(() => firmwareList.value);
|
||||
|
||||
// ── Inline upload (no dialog — the dropzone lives in the tab) ──────
|
||||
// ── Inline upload (no dialog: the dropzone lives in the tab) ──────
|
||||
const firmwareDz = ref<InstanceType<typeof RDropzone> | null>(null);
|
||||
const pendingFiles = ref<File[]>([]);
|
||||
const uploading = ref(false);
|
||||
@@ -141,7 +141,7 @@ function onDeleted(deletedIds: number[]) {
|
||||
syncFirmware(firmwareList.value.filter((f) => !deletedIds.includes(f.id)));
|
||||
}
|
||||
|
||||
// `firmware_count` is a readonly derived field on PlatformSchema —
|
||||
// `firmware_count` is a readonly derived field on PlatformSchema:
|
||||
// patched locally so the InfoPanel stat reacts instantly; a future
|
||||
// refetch reconciles.
|
||||
function syncFirmware(next: FirmwareSchema[]) {
|
||||
@@ -201,7 +201,7 @@ async function performDelete(
|
||||
@files="addFiles"
|
||||
/>
|
||||
|
||||
<!-- Otherwise the whole tab is a drop target (overlay) — dropping anywhere
|
||||
<!-- Otherwise the whole tab is a drop target (overlay): dropping anywhere
|
||||
adds files to the pending list. -->
|
||||
<RDropzone
|
||||
v-else
|
||||
@@ -215,7 +215,7 @@ async function performDelete(
|
||||
@files="addFiles"
|
||||
>
|
||||
<div class="r-v2-fw">
|
||||
<!-- Pending upload — the files selected / dropped, awaiting upload. -->
|
||||
<!-- Pending upload: the files selected / dropped, awaiting upload. -->
|
||||
<section v-if="pendingFiles.length > 0" class="r-v2-fw__pending">
|
||||
<header class="r-v2-fw__pending-head">
|
||||
<span>{{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// GalleryShell — shared layout for Platform / Search / Collection.
|
||||
// GalleryShell: shared layout for Platform / Search / Collection.
|
||||
//
|
||||
// Three structural sections, top to bottom, all sharing one scrollbar:
|
||||
// 1. HEADER: view-supplied via `#header` slot. Whatever the view
|
||||
@@ -9,7 +9,7 @@
|
||||
// below the top bar; once pinned it shares one glass surface
|
||||
// with the top bar, so cards blur behind both.
|
||||
// 3. GRID / TABLE: the row-virtualised content (cards in grid mode,
|
||||
// div-based rows in list mode — same shell scroller, same
|
||||
// div-based rows in list mode: same shell scroller, same
|
||||
// AlphaStrip wiring; the list column header lives in the
|
||||
// prepend, sticky below the toolbar).
|
||||
//
|
||||
@@ -96,12 +96,12 @@ interface Props {
|
||||
emptyMessage: string;
|
||||
/** Empty-state icon, shared with not-found mode. */
|
||||
emptyIcon?: string;
|
||||
/** "Not found" mode — replaces all body items with a single empty row. */
|
||||
/** "Not found" mode: replaces all body items with a single empty row. */
|
||||
notFound?: boolean;
|
||||
/** Override the empty-state message in not-found mode. */
|
||||
notFoundMessage?: string;
|
||||
/** Whether GameCards should display the platform badge corner (Search /
|
||||
* Collection: yes; Platform: no — the cards already share a platform). */
|
||||
* Collection: yes; Platform: no, the cards already share a platform). */
|
||||
showPlatformBadge?: boolean;
|
||||
/** Skeleton row count painted while the very first window is loading. */
|
||||
skeletonRowCount?: number;
|
||||
@@ -128,7 +128,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
defineSlots<{
|
||||
/** View-specific header (InfoPanel / PageHeader / etc). Rendered in
|
||||
* the scroller's `#prepend` slot — scrolls naturally with the rest
|
||||
* the scroller's `#prepend` slot: scrolls naturally with the rest
|
||||
* of the content. Must NOT carry a divider of its own; the shell
|
||||
* paints the single divider at the bottom of the prepend band. */
|
||||
header(): unknown;
|
||||
@@ -189,10 +189,10 @@ const {
|
||||
selectedLengthMaxHours,
|
||||
} = storeToRefs(galleryFilterStore);
|
||||
|
||||
// Drawer open state — bound to FilterDrawer via v-model.
|
||||
// Drawer open state: bound to FilterDrawer via v-model.
|
||||
const filterDrawerOpen = ref(false);
|
||||
|
||||
// Active filter count — drives the toolbar badge. Counts each
|
||||
// Active filter count: drives the toolbar badge. Counts each
|
||||
// boolean/tri-state filter that's set, plus each multi-select group
|
||||
// with at least one selection. Mirrors `FilterDrawer`'s own count so
|
||||
// the badge agrees with the drawer header.
|
||||
@@ -298,7 +298,7 @@ const { total, charIndex, initialFetching, orderBy, orderDir } =
|
||||
|
||||
const { groupBy, layout, toolbarPosition } = useGalleryMode();
|
||||
|
||||
// Responsive columns — measure the section to chunk roms into rows.
|
||||
// Responsive columns: measure the section to chunk roms into rows.
|
||||
// Card width and inset track the breakpoint so phones pack more, smaller
|
||||
// cards instead of one stretched card per row:
|
||||
// inset = scroller padding (--r-row-pad × 2), plus the AlphaStrip column
|
||||
@@ -331,7 +331,7 @@ const { columns, usableWidth } = useResponsiveColumns(sectionEl, {
|
||||
(stripVisible.value ? STRIP_INSET_PX : 0),
|
||||
});
|
||||
|
||||
// Fallback cover ratio (boxart style) — the per-card `--r-cover-ratio` seed
|
||||
// Fallback cover ratio (boxart style): the per-card `--r-cover-ratio` seed
|
||||
// before GameCover measures the real image, plus the bootstrap skeletons.
|
||||
// The flow-packer takes it too (`fallbackRatio`): a card with no artwork
|
||||
// paints its placeholder at this ratio and never reports a measured one, so
|
||||
@@ -343,7 +343,7 @@ const coverAspectRatio = computed(() =>
|
||||
),
|
||||
);
|
||||
|
||||
// Measured natural cover ratios feeding the flow-packer — GameCard reports
|
||||
// Measured natural cover ratios feeding the flow-packer: GameCard reports
|
||||
// each cover's ratio on load (`onCardRatio`), the packer reads `ratioAt`,
|
||||
// and `ratioVersion` bumps (debounced) to trigger a single re-pack.
|
||||
const { ratioVersion, ratioAt, onCardRatio } = useGalleryCoverRatios();
|
||||
@@ -372,15 +372,15 @@ watch(
|
||||
);
|
||||
|
||||
// 2D arrow / gamepad nav for both layouts of the gallery. Two passes:
|
||||
// * Grid mode — rows are `.r-v2-shell__row` (the per-virtualizer-item
|
||||
// * Grid mode: rows are `.r-v2-shell__row` (the per-virtualizer-item
|
||||
// wrapper around the row's GameCards). ArrowLeft/Right within a row,
|
||||
// ArrowUp/Down jumps to the same column in the next row.
|
||||
// * List mode — each `.game-list-row` is both row and cell. ArrowLeft/
|
||||
// * List mode: each `.game-list-row` is both row and cell. ArrowLeft/
|
||||
// Right is no-op (single cell per row); ArrowUp/Down moves between
|
||||
// rows.
|
||||
// Both call sites resolve `current()` against the same focused element
|
||||
// and only the matching one actually moves focus, so they don't fight.
|
||||
// Virtualised rows past the overscan window simply aren't in the DOM —
|
||||
// Virtualised rows past the overscan window simply aren't in the DOM:
|
||||
// nav clamps at the boundary; scrolling past mounts more rows.
|
||||
useGridNav(sectionEl, { rowSelector: ".r-v2-shell__row" });
|
||||
useGridNav(sectionEl, {
|
||||
@@ -536,7 +536,7 @@ watchEffect(() => {
|
||||
const overscan = virtualOverscan.value;
|
||||
const first = empty ? 0 : Math.max(0, vr.first - overscan);
|
||||
const last = empty ? -1 : Math.min(total - 1, vr.last + overscan);
|
||||
// Count the cards actually mounted in the rendered window — grid rows fan
|
||||
// Count the cards actually mounted in the rendered window: grid rows fan
|
||||
// out into many cards, so this is the real DOM weight (not just row count).
|
||||
let renderedCards = 0;
|
||||
for (let i = first; i <= last; i++) {
|
||||
@@ -594,11 +594,11 @@ const currentLetter = computed<string>(() => {
|
||||
// aligns each to its shared 72-item window, dedupes, starts the windows
|
||||
// covering the viewport, and cancels any that scrolled out of view.
|
||||
// Batching visible cards into a handful of paginated `getRoms` requests
|
||||
// (instead of one request per card) is what keeps a fast scroll — or two
|
||||
// users scrolling at once — from flooding the single-worker backend.
|
||||
// (instead of one request per card) is what keeps a fast scroll: or two
|
||||
// users scrolling at once: from flooding the single-worker backend.
|
||||
//
|
||||
// A small debounce on viewport changes prevents fire-and-cancel storms
|
||||
// during smooth scrolling — only when the viewport settles for
|
||||
// during smooth scrolling: only when the viewport settles for
|
||||
// `FETCH_DEBOUNCE_MS` do we sync. Both layouts share this one path, so the
|
||||
// list is debounced too (list rows no longer self-fetch on mount).
|
||||
const FETCH_DEBOUNCE_MS = 80;
|
||||
@@ -653,7 +653,7 @@ watch(virtualItems, () => {
|
||||
}
|
||||
pendingRange = null;
|
||||
// Re-sync against the current viewport so visible rows in the new
|
||||
// context start fetching immediately (no debounce — items just
|
||||
// context start fetching immediately (no debounce: items just
|
||||
// changed, the user is staring at skeletons).
|
||||
syncFetches(viewportRange.value);
|
||||
});
|
||||
@@ -684,7 +684,7 @@ function scrollToLetter(letter: string) {
|
||||
jumpLetter.value = letter;
|
||||
jumpDeadline = Date.now() + LETTER_JUMP_MAX_MS;
|
||||
anchorLetter(letter, true);
|
||||
// The viewport-driven fetch sync handles the destination — once the
|
||||
// The viewport-driven fetch sync handles the destination: once the
|
||||
// smooth scroll settles, `update:viewportRange` fires and the windows at
|
||||
// the landing zone start loading via `syncFetches` (both layouts). No
|
||||
// manual prefetch needed.
|
||||
@@ -902,7 +902,7 @@ const itemKind = (i: GalleryItem) => i.kind;
|
||||
// (keyed by `:key="p"`) patch in place. `unknown` matches the prop signature.
|
||||
const galleryItemKey = (item: unknown): string => (item as GalleryItem).key;
|
||||
|
||||
// View-facing surface. Methods only — internal state stays internal.
|
||||
// View-facing surface. Methods only: internal state stays internal.
|
||||
defineExpose({
|
||||
/** Re-apply the previously-saved scroll position for the current route
|
||||
* (typically called by the view at the end of its load flow). */
|
||||
@@ -1000,7 +1000,7 @@ defineExpose({
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- LIST COLUMN HEADER — sticky below the toolbar in list mode.
|
||||
<!-- LIST COLUMN HEADER: sticky below the toolbar in list mode.
|
||||
Shares `LIST_GRID_TEMPLATE` with every GameListRow underneath
|
||||
so columns align. Header click cycles asc/desc into the
|
||||
store's orderBy/orderDir. -->
|
||||
@@ -1021,7 +1021,7 @@ defineExpose({
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- GRID / TABLE (Section 3) — letter-headers + rows of cards in
|
||||
<!-- GRID / TABLE (Section 3): letter-headers + rows of cards in
|
||||
grid/grouped mode, or a single RTable in list mode. Skeleton
|
||||
rows render while the first window is in flight. The empty
|
||||
/ not-found state replaces everything below the toolbar
|
||||
@@ -1115,7 +1115,7 @@ defineExpose({
|
||||
@pick="scrollToLetter"
|
||||
/>
|
||||
|
||||
<!-- FLOATING-DOCK TOOLBAR — the alternative dock; sits permanently
|
||||
<!-- FLOATING-DOCK TOOLBAR: the alternative dock; sits permanently
|
||||
in the top-right and never scrolls. Mutually exclusive with
|
||||
the in-scroller header dock above. -->
|
||||
<GalleryToolbar
|
||||
@@ -1146,7 +1146,7 @@ defineExpose({
|
||||
</template>
|
||||
</GalleryToolbar>
|
||||
|
||||
<!-- FILTER DRAWER — owned by the shell so every gallery view gets
|
||||
<!-- FILTER DRAWER: owned by the shell so every gallery view gets
|
||||
it for free. Forwards `showPlatformsInFilter` from the view so
|
||||
single-platform pages can hide the platform multi-select. -->
|
||||
<FilterDrawer
|
||||
@@ -1154,7 +1154,7 @@ defineExpose({
|
||||
:show-platforms-filter="showPlatformsInFilter"
|
||||
/>
|
||||
|
||||
<!-- SELECTION BAR — floating bottom panel surfaced whenever the
|
||||
<!-- SELECTION BAR: floating bottom panel surfaced whenever the
|
||||
user has selected at least one ROM. Owns the bulk actions
|
||||
(favorite, collections, download, refresh, delete). Stays
|
||||
outside the scroller so it never scrolls away. -->
|
||||
@@ -1177,7 +1177,7 @@ defineExpose({
|
||||
/* Explicit viewport-relative height instead of `height: 100%`.
|
||||
The parent `<main>` is a flex item, and percentage heights on
|
||||
descendants of flex-computed boxes don't always resolve in every
|
||||
browser / stacking context — when they fail to resolve the
|
||||
browser / stacking context: when they fail to resolve the
|
||||
section becomes content-sized, the scroller inside ends up with
|
||||
`height: auto`, and overflow-y stops doing anything because
|
||||
there's nothing to overflow. A viewport height bypasses that
|
||||
@@ -1196,7 +1196,7 @@ defineExpose({
|
||||
scroll UNDER the translucent bottom tab bar (the glass effect). The layout
|
||||
<main> adds a bottom padding for the bar (natural-flow views need it);
|
||||
cancel it here with a matching negative margin so this full-height section
|
||||
doesn't push the document past one viewport — otherwise a second, global
|
||||
doesn't push the document past one viewport: otherwise a second, global
|
||||
scroll stacks on top of the internal one. The scroller's bottom spacer
|
||||
(below) lifts the last row clear of the bar. */
|
||||
html[data-bp~="sm-and-down"] .r-v2-shell {
|
||||
@@ -1282,7 +1282,7 @@ html[data-bp~="md-and-up"] .r-v2-shell__list-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Card reveal animation (.r-v2-card-fade) lives in global.css — shared
|
||||
/* Card reveal animation (.r-v2-card-fade) lives in global.css: shared
|
||||
with the Home dashboard rows. */
|
||||
|
||||
.r-v2-shell__empty {
|
||||
@@ -1298,7 +1298,7 @@ html[data-bp~="md-and-up"] .r-v2-shell__list-header {
|
||||
}
|
||||
|
||||
/* When the list scrolls horizontally (columns wider than the viewport), the
|
||||
page chrome — view header, divider and in-flow toolbar — belongs to the
|
||||
page chrome, view header, divider and in-flow toolbar, belongs to the
|
||||
page, not the table, so pin them to the left (`left: 0`). They stay in place
|
||||
while only the column header and rows scroll sideways. `left` engages only
|
||||
while the scroller has horizontal overflow, i.e. in list mode. */
|
||||
@@ -1389,7 +1389,7 @@ html[data-bp~="xs"] .r-v2-shell {
|
||||
}
|
||||
|
||||
/* Last row rests clear of the bottom tab bar (the rest of the scroll passes
|
||||
under its glass). A real in-flow spacer, not `padding-bottom` — Safari /
|
||||
under its glass). A real in-flow spacer, not `padding-bottom`: Safari /
|
||||
older Chromium drop a scroll container's bottom padding from its
|
||||
scrollable overflow, trapping the last row behind the bar. */
|
||||
html[data-bp~="sm-and-down"] .r-v2-shell__scroller::after {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// GalleryToolbar — the "how should I see these games?" control strip for
|
||||
// GalleryToolbar: the "how should I see these games?" control strip for
|
||||
// Platform / Collection / Search views.
|
||||
//
|
||||
// Controlled externally via `useGalleryMode()`; the parent passes the refs
|
||||
@@ -70,7 +70,7 @@ const props = withDefaults(
|
||||
position?: ToolbarPosition;
|
||||
/** Show the GroupBy toggle (hide it on views where grouping doesn't make sense). */
|
||||
showGroupBy?: boolean;
|
||||
/** Override the GroupBy options — used by index views (Platforms)
|
||||
/** Override the GroupBy options: used by index views (Platforms)
|
||||
* to expose richer modes (family / category / generation) beyond
|
||||
* the default flat / letter pair. */
|
||||
groupByItems?: GroupByItem[];
|
||||
@@ -137,7 +137,7 @@ defineSlots<{
|
||||
actions?(): unknown;
|
||||
}>();
|
||||
|
||||
// Support both a Ref or a plain value — keeps consumption flexible.
|
||||
// Support both a Ref or a plain value: keeps consumption flexible.
|
||||
function toValue<T>(source: Ref<T> | T): T {
|
||||
return source && typeof source === "object" && "value" in (source as object)
|
||||
? ((source as Ref<T>).value as T)
|
||||
@@ -277,7 +277,7 @@ const { smAndUp } = useBreakpoint();
|
||||
</RTextField>
|
||||
<!-- eslint-enable vuejs-accessibility/no-autofocus -->
|
||||
|
||||
<!-- Filter button — sits flush against the search field. Same disc
|
||||
<!-- Filter button: sits flush against the search field. Same disc
|
||||
shape as the kebab (outlined icon-only RBtn); the active-count
|
||||
chip is `RBadge` anchored top-end, slightly overlapping the disc. -->
|
||||
<RBadge
|
||||
@@ -312,7 +312,7 @@ const { smAndUp } = useBreakpoint();
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- View controls cluster — pushed right via margin-left: auto.
|
||||
<!-- View controls cluster: pushed right via margin-left: auto.
|
||||
At ≥ smAndUp the inline sliders carry every option; below that
|
||||
they collapse into the kebab menu so the toolbar fits on phones
|
||||
without overflowing. -->
|
||||
@@ -471,7 +471,7 @@ const { smAndUp } = useBreakpoint();
|
||||
padding: var(--r-space-2) 0 var(--r-space-5);
|
||||
}
|
||||
|
||||
/* Floating variant — fixed top-right of the gallery body. */
|
||||
/* Floating variant: fixed top-right of the gallery body. */
|
||||
.gallery-toolbar--floating {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
@@ -484,7 +484,7 @@ const { smAndUp } = useBreakpoint();
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
/* Controls cluster — gets pushed right by its own margin. */
|
||||
/* Controls cluster: gets pushed right by its own margin. */
|
||||
.gallery-toolbar__controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -492,7 +492,7 @@ const { smAndUp } = useBreakpoint();
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Search — bounded width so the pills stay visible on wide screens. */
|
||||
/* Search: bounded width so the pills stay visible on wide screens. */
|
||||
.gallery-toolbar__search {
|
||||
flex: 0 1 360px;
|
||||
min-width: 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// GameListHeader — sticky column-header row for list-mode galleries.
|
||||
// GameListHeader: sticky column-header row for list-mode galleries.
|
||||
//
|
||||
// Layout: a single CSS-grid div sharing `LIST_GRID_TEMPLATE` with every
|
||||
// `GameListRow` underneath, so columns line up regardless of viewport.
|
||||
@@ -8,7 +8,7 @@
|
||||
//
|
||||
// Sticky positioning is owned by the parent (`GalleryShell` pins this
|
||||
// below the toolbar at `top: --r-v2-shell-toolbar-h`). The header
|
||||
// itself only paints — it doesn't manage scroll.
|
||||
// itself only paints: it doesn't manage scroll.
|
||||
import { RCheckbox, RIcon } from "@v2/lib";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
// GameListRow — single row of the list-mode gallery.
|
||||
// GameListRow: single row of the list-mode gallery.
|
||||
//
|
||||
// Owns:
|
||||
// * Skeleton ↔ real swap — when `getRomAt(position)` returns null the
|
||||
// * Skeleton ↔ real swap: when `getRomAt(position)` returns null the
|
||||
// row paints skeleton placeholders in every column; once the fetch
|
||||
// resolves it flips to the real cells. Same row height in both
|
||||
// states (no scroll reflow on hydration). Skeleton cells iterate
|
||||
@@ -64,11 +64,11 @@ interface Props {
|
||||
* lookup key into the store's `byPosition` map; the shell drives the
|
||||
* windowed fetch that fills it. Pass either this or `rom`, not both. */
|
||||
position?: number;
|
||||
/** Static ROM data — used by non-gallery surfaces (Settings → Missing
|
||||
/** Static ROM data: used by non-gallery surfaces (Settings → Missing
|
||||
* games) that already own the rom list. When provided, the row skips
|
||||
* the galleryRoms position lookup. */
|
||||
rom?: SimpleRom | null;
|
||||
/** Cover variant — when the browser supports webp the thumb URL is
|
||||
/** Cover variant: when the browser supports webp the thumb URL is
|
||||
* rewritten to .webp before the request. Wired from the shell so the
|
||||
* choice is decided once per gallery render, not per row. */
|
||||
webp?: boolean;
|
||||
@@ -218,7 +218,7 @@ const providers = computed(() => {
|
||||
});
|
||||
|
||||
// Status badge surfaces only when the rom actually has a play status
|
||||
// set — otherwise GameActionBtn would render the dashed-circle
|
||||
// set: otherwise GameActionBtn would render the dashed-circle
|
||||
// "no status set" placeholder on every row, which reads as visual
|
||||
// noise across a tall list. Mirrors the flags `useGameActions`
|
||||
// inspects in `currentStatusKey`.
|
||||
@@ -269,7 +269,7 @@ function navigateTo(item: SimpleRom, currentTarget: HTMLElement | null) {
|
||||
const navigate = async () => {
|
||||
await router.push(`/rom/${item.id}`);
|
||||
};
|
||||
// The thumb is the `<GameCard decorative>`'s inner art element —
|
||||
// The thumb is the `<GameCard decorative>`'s inner art element:
|
||||
// querying `.r-gc__art` reaches it through the GameCard wrapper.
|
||||
// GameCard's own `morphStyle` computed paints the reverse-paint name
|
||||
// on the same element when we come back from the detail page, so the
|
||||
@@ -299,7 +299,7 @@ function onRowClick(e: MouseEvent) {
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
// Default click — prevent the anchor's native navigation, run the
|
||||
// Default click: prevent the anchor's native navigation, run the
|
||||
// morph, then push the route.
|
||||
e.preventDefault();
|
||||
navigateTo(item, e.currentTarget as HTMLElement | null);
|
||||
@@ -894,7 +894,7 @@ function onRowPointerDown(e: PointerEvent) {
|
||||
background: var(--r-color-bg-elevated);
|
||||
}
|
||||
|
||||
/* Selected row — brand-tinted background so the selection reads at
|
||||
/* Selected row: brand-tinted background so the selection reads at
|
||||
a glance without competing with the per-row hover (`bg-elevated`).
|
||||
The two states can overlap (hover on a selected row); we stack the
|
||||
hover delta on top of the selected tint via `color-mix`. */
|
||||
@@ -914,7 +914,7 @@ html[data-input="pad"] .game-list-row:focus-visible {
|
||||
box-shadow: inset 0 0 0 var(--r-focus-ring-width) var(--r-color-focus);
|
||||
}
|
||||
|
||||
/* Select cell — checkbox column. Empty when the row is in skeleton
|
||||
/* Select cell: checkbox column. Empty when the row is in skeleton
|
||||
mode so the chrome only appears once a real row is loaded. */
|
||||
.game-list-row__select {
|
||||
display: flex;
|
||||
@@ -924,7 +924,7 @@ html[data-input="pad"] .game-list-row:focus-visible {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* RCheckbox carries its own visual language — we only own the row's
|
||||
/* RCheckbox carries its own visual language: we only own the row's
|
||||
reveal behaviour. Hidden at rest, visible on row hover/focus and
|
||||
whenever the row is selected. */
|
||||
.game-list-row__check {
|
||||
@@ -972,7 +972,7 @@ html[data-input="pad"] .game-list-row:focus-visible {
|
||||
and let the cover letterbox (contain) within, so the whole cover still
|
||||
shows at its true aspect. Portrait/square covers are unaffected (their
|
||||
natural width already fits, and contain renders identically to cover when
|
||||
the box matches the cover's ratio). Scoped to the list cover only — the
|
||||
the box matches the cover's ratio). Scoped to the list cover only: the
|
||||
gallery grid keeps its natural-width flow. */
|
||||
.game-list-row__cover :deep(.r-gc__art) {
|
||||
max-width: var(--r-card-art-h);
|
||||
@@ -1013,7 +1013,7 @@ html[data-input="pad"] .game-list-row:focus-visible {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
/* Inline badges next to the title — status, sibling count, etc.
|
||||
/* Inline badges next to the title: status, sibling count, etc.
|
||||
`flex-shrink: 0` keeps them visible when the name truncates. The
|
||||
SiblingBadge in GameCard absolute-positions itself over the cover;
|
||||
inline here it falls back to its natural pill layout. */
|
||||
@@ -1038,7 +1038,7 @@ html[data-input="pad"] .game-list-row:focus-visible {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Pills cell — chips wrap to multiple lines inside the cell when they
|
||||
/* Pills cell: chips wrap to multiple lines inside the cell when they
|
||||
don't fit on a single row, so short language/region lists read as a
|
||||
single horizontal strip and longer lists stack vertically. The cell
|
||||
keeps `overflow: hidden` (defense against extreme lists) and the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// GameListSkeletonRow — bootstrap-phase placeholder row for list-mode.
|
||||
// GameListSkeletonRow: bootstrap-phase placeholder row for list-mode.
|
||||
//
|
||||
// Painted by `GalleryShell` while the first metadata window is in
|
||||
// flight (no `total` yet). Self-contained: owns its row geometry +
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
// InfoPanel — hero strip at the top of Platform / Collection gallery
|
||||
// InfoPanel: hero strip at the top of Platform / Collection gallery
|
||||
// pages (matches the artist mockup's `.plat-info-panel`). Feature
|
||||
// composite around Stat, chips, and callsite-provided cover art.
|
||||
//
|
||||
// Slots: cover, eyebrow, title (defaults to `title` prop), tags, stats,
|
||||
// providers, actions.
|
||||
//
|
||||
// `providers` sits below stats — used by Platform.vue to surface the
|
||||
// `providers` sits below stats: used by Platform.vue to surface the
|
||||
// external-metadata chip cluster (IGDB / SS / MobyGames / RA / …)
|
||||
// inline rather than buried in a drawer.
|
||||
//
|
||||
// IMPORTANT — no divider here. The gallery shell owns the divider
|
||||
// IMPORTANT: no divider here. The gallery shell owns the divider
|
||||
// between the hero (header) and the toolbar so all three views
|
||||
// (Platform / Collection / Search) get the same separator regardless
|
||||
// of which header they render. The InfoPanel just provides the inner
|
||||
@@ -123,7 +123,7 @@ html[data-bp~="xs"] .info-panel {
|
||||
gap: 14px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
/* Drive the vertical rhythm purely from one flex gap on xs — the
|
||||
/* Drive the vertical rhythm purely from one flex gap on xs: the
|
||||
per-child margins (title / tags / providers) compound with the column
|
||||
gap otherwise and the stack reads cramped + uneven. Centre the whole
|
||||
hero (cover, title, chip clusters) so it reads as a balanced mobile
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
// PlatformHead — the platform-context strip that sits above the
|
||||
// PlatformHead: the platform-context strip that sits above the
|
||||
// gallery / firmware / settings tab body. Composes the InfoPanel
|
||||
// (icon + name + stats + provider chips + action ribbon) with the
|
||||
// RTabNav. Re-used in two render branches inside Platform.vue:
|
||||
//
|
||||
// 1. Library tab — passed to `GalleryShell`'s `#header` slot so the
|
||||
// 1. Library tab: passed to `GalleryShell`'s `#header` slot so the
|
||||
// head scrolls naturally with the cards (and the toolbar pins
|
||||
// below it). Matches the pre-tabs experience.
|
||||
//
|
||||
@@ -74,7 +74,7 @@ defineEmits<{
|
||||
|
||||
// A square icon sized per breakpoint (smaller on phones). Driving the size
|
||||
// in JS keeps the box able to grow to the icon (reflow) instead of forcing
|
||||
// the icon to a fixed box height, which — for a tall icon — either overflowed
|
||||
// the icon to a fixed box height, which, for a tall icon, either overflowed
|
||||
// (overlap) or, once clipped, cut it off.
|
||||
const { xs } = useBreakpoint();
|
||||
const { t } = useI18n();
|
||||
@@ -170,13 +170,13 @@ const iconSize = computed(() => (xs.value ? 116 : 148));
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<!-- Action ribbon — Random / Upload / Scan. Edit and Delete moved
|
||||
<!-- Action ribbon: Random / Upload / Scan. Edit and Delete moved
|
||||
inline into the Settings tab (the editable `custom_name` lives
|
||||
next to the read-only details there, and Delete sits in the
|
||||
tab's danger zone). Same circular icon-button vocabulary as
|
||||
the GameDetails action row. Random is open to anyone who can
|
||||
view the platform — it's a navigation shortcut, not an admin
|
||||
action — so it sits left of the gated Upload/Scan buttons. -->
|
||||
view the platform, it's a navigation shortcut, not an admin
|
||||
action, so it sits left of the gated Upload/Scan buttons. -->
|
||||
<template #actions>
|
||||
<RBtn
|
||||
variant="outlined"
|
||||
@@ -251,7 +251,7 @@ const iconSize = computed(() => (xs.value ? 116 : 148));
|
||||
}
|
||||
|
||||
/* Keep the platform icon prominent on phones (it's the page's identity)
|
||||
rather than shrinking it to a thumbnail — just a touch smaller than the
|
||||
rather than shrinking it to a thumbnail: just a touch smaller than the
|
||||
desktop size (see `iconSize`) so it leaves room for the centred title. */
|
||||
html[data-bp~="xs"] .r-v2-plat__panel-icon {
|
||||
width: 150px;
|
||||
@@ -266,7 +266,7 @@ html[data-bp~="xs"] .r-v2-plat__panel-icon {
|
||||
|
||||
/* ── Provider chip cluster ───────────────────────────────────────
|
||||
Compact pill that pairs the provider's logo with its remote ID.
|
||||
`--passive` (Flashpoint / HLTB / Libretro — no public lookup URL)
|
||||
`--passive` (Flashpoint / HLTB / Libretro: no public lookup URL)
|
||||
drops the hover lift since clicking does nothing. */
|
||||
.r-v2-plat__provider {
|
||||
display: inline-flex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// ScanPlatformDialog — kicks off a scan for a single platform with
|
||||
// ScanPlatformDialog: kicks off a scan for a single platform with
|
||||
// the user's choice of providers, hash-matchers, and scan type.
|
||||
//
|
||||
// Visual + interaction language mirrors `RefreshMetadataDialog`
|
||||
@@ -7,7 +7,7 @@
|
||||
// proxies as switch pills, scan-type select), but the identity row
|
||||
// at the top shows the platform instead of a ROM, and the scan-type
|
||||
// list mirrors the Scan view's per-platform options (no "new
|
||||
// platforms" — that's a discovery scan against the whole library,
|
||||
// platforms": that's a discovery scan against the whole library,
|
||||
// not a single platform).
|
||||
import {
|
||||
RAlert,
|
||||
@@ -59,7 +59,7 @@ const {
|
||||
persistSelection,
|
||||
} = useScanProviders();
|
||||
|
||||
// Per-platform scan types — the full Scan-view list minus
|
||||
// Per-platform scan types: the full Scan-view list minus
|
||||
// `new_platforms` (a discovery scan against fs_slugs not yet in the
|
||||
// DB, which can't be scoped to a known platform).
|
||||
type ScanType = Exclude<SharedScanType, "new_platforms">;
|
||||
@@ -135,7 +135,7 @@ function onScan() {
|
||||
|
||||
<template #content>
|
||||
<div class="r-v2-scan-plat">
|
||||
<!-- Platform identity row — icon + name. Mirrors the
|
||||
<!-- Platform identity row: icon + name. Mirrors the
|
||||
ROM-identity row in RefreshMetadataDialog so the two
|
||||
scan-launching surfaces read as siblings. -->
|
||||
<div class="r-v2-scan-plat__head">
|
||||
@@ -157,7 +157,7 @@ function onScan() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. Providers — General + Specific RSelects. -->
|
||||
<!-- 1. Providers: General + Specific RSelects. -->
|
||||
<section class="r-v2-scan-plat__section">
|
||||
<h3 class="r-v2-scan-plat__section-title">
|
||||
{{ t("scan.section-providers") }}
|
||||
@@ -309,7 +309,7 @@ function onScan() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. Hash-matcher proxies — same compact switch pills as
|
||||
<!-- 2. Hash-matcher proxies: same compact switch pills as
|
||||
RefreshMetadataDialog. -->
|
||||
<section class="r-v2-scan-plat__section">
|
||||
<h3 class="r-v2-scan-plat__section-title">
|
||||
@@ -358,8 +358,8 @@ function onScan() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. Scan type — full per-platform option list (no "new
|
||||
platforms" — that's a library-wide discovery scan). -->
|
||||
<!-- 3. Scan type, full per-platform option list (no "new
|
||||
platforms", that's a library-wide discovery scan). -->
|
||||
<section class="r-v2-scan-plat__section">
|
||||
<h3 class="r-v2-scan-plat__section-title">
|
||||
{{ t("scan.section-scan-type") }}
|
||||
@@ -421,7 +421,7 @@ function onScan() {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Platform identity row — sibling of `.r-v2-refresh__rom` in
|
||||
/* Platform identity row: sibling of `.r-v2-refresh__rom` in
|
||||
RefreshMetadataDialog. */
|
||||
.r-v2-scan-plat__head {
|
||||
display: flex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// SelectionBar — floating bottom panel that surfaces bulk actions
|
||||
// SelectionBar: floating bottom panel that surfaces bulk actions
|
||||
// over the currently-selected ROMs. Replaces v1's `FabOverlay`
|
||||
// speed-dial: the bar sits below the gallery, never inside the
|
||||
// toolbar, so filter/search/sort all stay reachable while the user
|
||||
@@ -7,29 +7,29 @@
|
||||
//
|
||||
// Visibility: bound to `gallerySelection.enabled`. Slides up from
|
||||
// `bottom: 0` when the first ROM is selected and slides back down
|
||||
// when the count drops to zero — the panel stays mounted so its
|
||||
// when the count drops to zero: the panel stays mounted so its
|
||||
// buttons keep their ripple state cleanly between cycles.
|
||||
//
|
||||
// Actions wire-up:
|
||||
// * favorite / unfavorite — direct collectionApi bulk call against
|
||||
// * favorite / unfavorite: direct collectionApi bulk call against
|
||||
// the favorite collection. The Card/Row's per-rom favourite
|
||||
// toggle still routes through `useGameActions` per-rom; here we
|
||||
// bypass only its per-rom write to issue a single add/remove call
|
||||
// for the whole set, while still reusing its
|
||||
// `ensureFavoriteCollection` so a fresh instance gets one.
|
||||
// * manage collections — re-uses the existing
|
||||
// * manage collections: re-uses the existing
|
||||
// `ManageCollectionsDialog` (already accepts SimpleRom[]) via
|
||||
// the `showManageCollectionsDialog` emitter event.
|
||||
// * download — a single selected ROM downloads directly (like the
|
||||
// * download: a single selected ROM downloads directly (like the
|
||||
// per-rom `useGameActions.download`); multi-selections go through
|
||||
// the bulk endpoint so the server bundles them into one zip.
|
||||
// `hideDownload` drops the action for hosts whose rows have no
|
||||
// file to serve (the Missing games tab).
|
||||
// * refresh metadata — emits `showRefreshMetadataDialog` for each
|
||||
// * refresh metadata: emits `showRefreshMetadataDialog` for each
|
||||
// ROM in turn. (Phase-2 follow-up: the dialog will accept arrays
|
||||
// so the user only sees the scan-type picker once for the whole
|
||||
// batch instead of N dialogs.)
|
||||
// * delete — emits `showDeleteRomDialog` with the full selection;
|
||||
// * delete: emits `showDeleteRomDialog` with the full selection;
|
||||
// the dialog already paints a per-ROM "remove from disk"
|
||||
// checklist for bulk deletes.
|
||||
//
|
||||
@@ -117,7 +117,7 @@ const canEditRom = useCan("rom.edit");
|
||||
// Bulk delete hits `POST /roms/delete`, which gates on ROMS_WRITE
|
||||
const canDelete = computed(() => hasDeleteGrant.value && canEditRom.value);
|
||||
|
||||
// `favorite` is the favourite collection — used to compute "are all
|
||||
// `favorite` is the favourite collection: used to compute "are all
|
||||
// selected ROMs in favorites?" so the button can toggle between
|
||||
// "add to" / "remove from" instead of forcing a separate unfavorite
|
||||
// action. Same model as the per-card favourite button.
|
||||
@@ -163,7 +163,7 @@ async function bulkFavorite() {
|
||||
collectionsStore.setFavoriteCollection(data);
|
||||
if (wasAllFavorited && galleryRomsStore.currentCollection?.id === fav.id) {
|
||||
// We were on the favourites collection view and just removed
|
||||
// every selected rom from it — drop them from the visible
|
||||
// every selected rom from it: drop them from the visible
|
||||
// roms so the UI reflects the new membership immediately.
|
||||
removeCachedRoms(roms);
|
||||
}
|
||||
@@ -182,7 +182,7 @@ async function bulkFavorite() {
|
||||
}
|
||||
}
|
||||
|
||||
// Bulk status — toggles one play-status across the whole selection,
|
||||
// Bulk status: toggles one play-status across the whole selection,
|
||||
// mirroring the favourite button's all-or-nothing model: a status reads
|
||||
// as "active" only when *every* selected ROM already has it. Clicking an
|
||||
// active status clears it on all; clicking an inactive one (at least one
|
||||
@@ -410,7 +410,7 @@ function clear() {
|
||||
<!-- Default slot: action buttons. Order mirrors the v1 FAB:
|
||||
download → favourite → collections → refresh → delete.
|
||||
Every button is wrapped in RTooltip so the user gets a
|
||||
consistent hover hint and gamepad users see the label —
|
||||
consistent hover hint and gamepad users see the label:
|
||||
the v2 visual vocabulary for icon-only buttons. -->
|
||||
<RTooltip
|
||||
v-if="canDownload && !hideDownload"
|
||||
@@ -440,7 +440,7 @@ function clear() {
|
||||
</template>
|
||||
</RTooltip>
|
||||
|
||||
<!-- Set status — opens a picker that toggles one play-status across
|
||||
<!-- Set status: opens a picker that toggles one play-status across
|
||||
the whole selection. Mirrors the per-ROM status menu
|
||||
(GameActionBtn) and the favourite button's all-or-nothing
|
||||
model: a row reads active only when every selected ROM already
|
||||
@@ -463,7 +463,7 @@ function clear() {
|
||||
:aria-label="t('gallery.selection-status')"
|
||||
/>
|
||||
</template>
|
||||
<!-- Enum statuses — single play-status per ROM. Active row tints
|
||||
<!-- Enum statuses: single play-status per ROM. Active row tints
|
||||
brand when all selected share it. -->
|
||||
<RMenuItem
|
||||
v-for="key in ENUM_KEYS"
|
||||
@@ -477,7 +477,7 @@ function clear() {
|
||||
|
||||
<RDivider />
|
||||
|
||||
<!-- Play-status flags — independent toggles. Active rows tint
|
||||
<!-- Play-status flags: independent toggles. Active rows tint
|
||||
brand and show a trailing check when every selected ROM has
|
||||
the flag set. -->
|
||||
<RMenuItem
|
||||
@@ -496,7 +496,7 @@ function clear() {
|
||||
|
||||
<RDivider />
|
||||
|
||||
<!-- Visibility flag — distinct category (library visibility,
|
||||
<!-- Visibility flag: distinct category (library visibility,
|
||||
not play state) so it lives in its own section. -->
|
||||
<RMenuItem
|
||||
v-for="key in VISIBILITY_FLAG_KEYS"
|
||||
@@ -564,7 +564,7 @@ function clear() {
|
||||
</template>
|
||||
</RTooltip>
|
||||
|
||||
<!-- Append region: clear button — floats to the right edge via
|
||||
<!-- Append region: clear button, floats to the right edge via
|
||||
RToolbar's built-in spacer. -->
|
||||
<template #append>
|
||||
<RTooltip :text="t('gallery.selection-clear')">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
// SettingsTab — platform-scoped settings rendered as the `Settings`
|
||||
// SettingsTab: platform-scoped settings rendered as the `Settings`
|
||||
// tab inside Platform.vue. Details (editable name + read-only platform
|
||||
// fields) with a danger zone underneath holding the destructive
|
||||
// "Delete platform" action.
|
||||
//
|
||||
// Mutation path:
|
||||
// • `custom_name` / `description` → `platformApi.updatePlatform(...)`
|
||||
// Optimistic — the form updates the local platform reactively, a
|
||||
// Optimistic: the form updates the local platform reactively, a
|
||||
// snackbar fires on success/failure.
|
||||
//
|
||||
// Delete: emitted upward (`@delete`) so the view orchestrator can
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// and `GameListSkeletonRow` (bootstrap-phase placeholder rows).
|
||||
//
|
||||
// CSS grid template lives here so all three components pick up the same
|
||||
// column geometry — change column widths in one place and the header
|
||||
// column geometry: change column widths in one place and the header
|
||||
// stays aligned with every row underneath. The fr units in the title
|
||||
// column let the row stretch to fill remaining width while the metric
|
||||
// columns hold fixed pixel widths so numbers align cleanly.
|
||||
@@ -49,7 +49,7 @@ export interface ListColumn {
|
||||
* as a grid. */
|
||||
numeric?: boolean;
|
||||
/** Skeleton placeholder width (px) for this column's loading state.
|
||||
* `undefined` means the column owns a custom skeleton shape — the
|
||||
* `undefined` means the column owns a custom skeleton shape: the
|
||||
* title column paints cover + meta lines, the actions column paints
|
||||
* nothing. */
|
||||
skeletonWidth?: number;
|
||||
@@ -141,7 +141,7 @@ export function getListColumns(showPlatform: boolean): readonly ListColumn[] {
|
||||
return cols;
|
||||
}
|
||||
|
||||
// Fixed track widths (px) — kept as data so the grid template AND the row's
|
||||
// Fixed track widths (px): kept as data so the grid template AND the row's
|
||||
// natural min-width (below) derive from the same numbers.
|
||||
const LIST_SELECT_TRACK_PX = parseInt(layout.listSelectWidth, 10);
|
||||
const LIST_PLATFORM_TRACK_PX = 200;
|
||||
@@ -155,7 +155,7 @@ export const LIST_TITLE_MIN_PX = 200;
|
||||
// CSS. Token values are guaranteed to be `<number>px` strings.
|
||||
//
|
||||
// The list-row avatar is rendered by `<GameCard size="xs" />`, so the
|
||||
// cover dimensions come from the shared xs tokens — there's no
|
||||
// cover dimensions come from the shared xs tokens: there's no
|
||||
// dedicated "list cover" token any more. Keep these JS mirrors so the
|
||||
// skeleton placeholder paints at the same footprint as the real card.
|
||||
export const LIST_ROW_HEIGHT_PX = parseInt(layout.listRowHeight, 10);
|
||||
@@ -224,7 +224,7 @@ export function getListMinWidth(showPlatform: boolean): number {
|
||||
return tracks + (columnCount - 1) * LIST_GRID_GAP_PX + 2 * LIST_ROW_PAD_X_PX;
|
||||
}
|
||||
|
||||
/** Default exports — the cross-platform variant. Used by the bootstrap-
|
||||
/** Default exports: the cross-platform variant. Used by the bootstrap-
|
||||
* phase skeleton/header pair when no consumer-specific override is
|
||||
* available; per-view code should always go through the explicit
|
||||
* `getListColumns(...)` / `getListGridTemplate(...)` so the column set
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// GameActionBtn — single icon button for the per-ROM action set
|
||||
// GameActionBtn: single icon button for the per-ROM action set
|
||||
// shared between the GameCard hover overlay and the GameDetails
|
||||
// header. One component, three sizes, auto-wires to `useGameActions`
|
||||
// so both surfaces stay in sync.
|
||||
@@ -18,12 +18,12 @@
|
||||
// current status icon when set, dashed border when empty
|
||||
// more → open MoreMenu (GameActionsList dropdown)
|
||||
//
|
||||
// Sizes (controls diameter + icon size + padding) — same vocabulary as
|
||||
// Sizes (controls diameter + icon size + padding): same vocabulary as
|
||||
// RBtn / RChip / RTag:
|
||||
// x-small → 22px
|
||||
// small → 28px — GameCard hover overlay
|
||||
// default → 40px — GameDetails header
|
||||
// large → 44px — larger emphasis (GameActions row)
|
||||
// small → 28px: GameCard hover overlay
|
||||
// default → 40px: GameDetails header
|
||||
// large → 44px: larger emphasis (GameActions row)
|
||||
// x-large → 52px
|
||||
//
|
||||
// Variants:
|
||||
@@ -133,7 +133,7 @@ const { smAndDown } = useBreakpoint();
|
||||
|
||||
const romRef = toRef(props, "rom");
|
||||
// Reuse the host's shared instance when one is provided (GameCard provides a
|
||||
// single `useGameActions` for all its buttons — see GAME_ACTIONS_KEY). Falls
|
||||
// single `useGameActions` for all its buttons: see GAME_ACTIONS_KEY). Falls
|
||||
// back to an own instance for standalone use (GameDetails header, list rows).
|
||||
// Safe to call conditionally: useGameActions registers no lifecycle hooks, and
|
||||
// setup decides the branch once at mount. The provided instance is bound to
|
||||
@@ -169,7 +169,7 @@ type Preset = {
|
||||
image?: string;
|
||||
};
|
||||
|
||||
// Presentation metadata per action — icon swaps when active, different
|
||||
// Presentation metadata per action: icon swaps when active, different
|
||||
// aria labels, different click handlers. Written as an if-chain instead
|
||||
// of a switch so the linter can see every path returns.
|
||||
const preset = computed<Preset>(() => {
|
||||
@@ -286,7 +286,7 @@ const preset = computed<Preset>(() => {
|
||||
active: count > 0,
|
||||
};
|
||||
}
|
||||
// "more" — the RMenu owns activation; no direct click handler.
|
||||
// "more": the RMenu owns activation; no direct click handler.
|
||||
return {
|
||||
icon: "mdi-dots-horizontal",
|
||||
activeIcon: null,
|
||||
@@ -326,7 +326,7 @@ onBeforeUnmount(() =>
|
||||
);
|
||||
|
||||
// Single signal the GameCard `:has()` selectors watch to keep the card's
|
||||
// hover state painted while an action is in flight — more menu, status
|
||||
// hover state painted while an action is in flight: more menu, status
|
||||
// picker, or collection-manage dialog.
|
||||
const pinned = computed(() => {
|
||||
if (props.action === "more") return moreOpen.value;
|
||||
@@ -336,14 +336,14 @@ const pinned = computed(() => {
|
||||
});
|
||||
|
||||
function pickEnum(key: RomUserStatus) {
|
||||
// Re-clicking the active enum clears just the enum — gives the user a
|
||||
// Re-clicking the active enum clears just the enum: gives the user a
|
||||
// way to drop the status without also wiping the flags via "Clear all".
|
||||
void actions.setStatusEnum(enumStatus.value === key ? null : key);
|
||||
statusOpen.value = false;
|
||||
}
|
||||
|
||||
function toggleFlag(key: StatusFlagKey) {
|
||||
// Don't close — flags are independent toggles, the user may flip several.
|
||||
// Don't close: flags are independent toggles, the user may flip several.
|
||||
void actions.setStatus(key);
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ function onClick(e: MouseEvent) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- More — opens the shared GameActionsList dropdown. On phones it docks
|
||||
<!-- More: opens the shared GameActionsList dropdown. On phones it docks
|
||||
as a bottom sheet (content-height) instead of a floating dropdown. -->
|
||||
<RMenu
|
||||
v-if="action === 'more'"
|
||||
@@ -407,7 +407,7 @@ function onClick(e: MouseEvent) {
|
||||
<GameActionsList :rom="rom" @close="moreOpen = false" />
|
||||
</RMenu>
|
||||
|
||||
<!-- Status — enum picker; icon mirrors the current value, dashed
|
||||
<!-- Status: enum picker; icon mirrors the current value, dashed
|
||||
border when no status is set. Keeps the per-ROM action set in
|
||||
one place instead of a parallel widget. With several states
|
||||
active the activator stretches into a multi-icon pill; the
|
||||
@@ -492,7 +492,7 @@ function onClick(e: MouseEvent) {
|
||||
|
||||
<RDivider />
|
||||
|
||||
<!-- Visibility flag — distinct category (controls library
|
||||
<!-- Visibility flag: distinct category (controls library
|
||||
visibility, not play state) so it lives in its own section. -->
|
||||
<RMenuItem
|
||||
v-for="key in VISIBILITY_FLAG_KEYS"
|
||||
@@ -527,7 +527,7 @@ function onClick(e: MouseEvent) {
|
||||
</template>
|
||||
</RMenu>
|
||||
|
||||
<!-- Plain action — direct click, or a real anchor for a linked launch. -->
|
||||
<!-- Plain action: direct click, or a real anchor for a linked launch. -->
|
||||
<component
|
||||
:is="linkHref ? 'a' : 'button'"
|
||||
v-else
|
||||
@@ -575,7 +575,7 @@ function onClick(e: MouseEvent) {
|
||||
/* Dark glass so the button still reads when sitting on top of a bright
|
||||
or busy cover image in the GameCard overlay. In GameDetails the
|
||||
backdrop is already a dark blurred cover so this tone lands neutral
|
||||
there too. Overlay tokens never theme-flip — they stay dark over
|
||||
there too. Overlay tokens never theme-flip: they stay dark over
|
||||
any cover artwork. */
|
||||
border: 1px solid var(--r-color-overlay-border);
|
||||
background: var(--r-color-overlay-scrim-soft);
|
||||
@@ -605,7 +605,7 @@ function onClick(e: MouseEvent) {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
/* Size ladder — circular unless `--labelled`. Matches the
|
||||
/* Size ladder: circular unless `--labelled`. Matches the
|
||||
x-small/small/default/large/x-large vocabulary used across primitives. */
|
||||
.r-v2-game-btn--x-small {
|
||||
width: 22px;
|
||||
@@ -668,7 +668,7 @@ function onClick(e: MouseEvent) {
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
/* Labelled — expands to a pill with text. Used by Play in the
|
||||
/* Labelled: expands to a pill with text. Used by Play in the
|
||||
GameDetails header. Height stays the same as the circular variant so
|
||||
it can live in the same row without visual jumps. */
|
||||
.r-v2-game-btn--labelled {
|
||||
@@ -688,9 +688,9 @@ function onClick(e: MouseEvent) {
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
/* Surface — RTag-style translucent grey. Used in the GameDetails
|
||||
/* Surface: RTag-style translucent grey. Used in the GameDetails
|
||||
header where buttons sit on the page background (not over cover
|
||||
art) — matches the visual vocabulary of RTag and RSelect there. */
|
||||
art): matches the visual vocabulary of RTag and RSelect there. */
|
||||
.r-v2-game-btn--surface {
|
||||
background: var(--r-color-surface);
|
||||
border-color: var(--r-color-border-strong);
|
||||
@@ -703,7 +703,7 @@ function onClick(e: MouseEvent) {
|
||||
color: var(--r-color-fg);
|
||||
}
|
||||
|
||||
/* Bare — no chrome. The active-state colour swap (`--active-favorite`
|
||||
/* Bare: no chrome. The active-state colour swap (`--active-favorite`
|
||||
etc.) still applies because it overrides `color` only. Hover paints a
|
||||
subtle surface tint so the hit-target reads. */
|
||||
.r-v2-game-btn--bare {
|
||||
@@ -718,7 +718,7 @@ function onClick(e: MouseEvent) {
|
||||
color: var(--r-color-fg);
|
||||
}
|
||||
|
||||
/* Emphasized — the primary-action look (white on dark). Used by Play. */
|
||||
/* Emphasized: the primary-action look (white on dark). Used by Play. */
|
||||
.r-v2-game-btn--emphasized {
|
||||
background: var(--r-color-overlay-emphasis-bg) !important;
|
||||
border-color: var(--r-color-overlay-emphasis-bg) !important;
|
||||
@@ -755,14 +755,14 @@ function onClick(e: MouseEvent) {
|
||||
color: var(--r-color-brand-primary) !important;
|
||||
}
|
||||
|
||||
/* Status — dashed border when no status is set, signals "click to
|
||||
/* Status: dashed border when no status is set, signals "click to
|
||||
pick". Once set, the button uses the regular solid border + the
|
||||
status icon shows the choice. */
|
||||
.r-v2-game-btn--action-status:not(.r-v2-game-btn--active-status) {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
/* Multi-status — the button stretches to fit every active state's
|
||||
/* Multi-status: the button stretches to fit every active state's
|
||||
icon. Width/height go auto with a min that keeps the single-state
|
||||
diameter, so 0/1 active still reads as a circle. The icon row/column
|
||||
lives in `__icons`. */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// GameActions — the action row in the game-details header.
|
||||
// GameActions: the action row in the game-details header.
|
||||
// Composes GameActionBtn atoms that are shared with the GameCard hover
|
||||
// overlay so both surfaces stay visually and behaviourally in sync.
|
||||
// The Play button uses the emphasized + withLabel variant to match the
|
||||
@@ -8,7 +8,7 @@
|
||||
//
|
||||
// Right-side group (desktop only): completion + rating + difficulty
|
||||
// pickers, separated from the main ribbon by a spacer. All three share
|
||||
// MetricMenuBtn — the rating/difficulty trigger an RRating popup,
|
||||
// MetricMenuBtn: the rating/difficulty trigger an RRating popup,
|
||||
// completion triggers an RSlider popup. On phones these move into the
|
||||
// status button's sheet (GameActionBtn `withMetrics`) to save a row.
|
||||
// Writes are optimistic via useGameActions.setScore.
|
||||
@@ -32,7 +32,7 @@ const { t } = useI18n();
|
||||
const romRef = toRef(props, "rom");
|
||||
const actions = useGameActions(() => romRef.value);
|
||||
|
||||
// Shrink the ribbon on phones — the large (44px) buttons crowd the narrow
|
||||
// Shrink the ribbon on phones: the large (44px) buttons crowd the narrow
|
||||
// column; the default (36px) size fits more per row and reads cleaner.
|
||||
const { smAndDown } = useBreakpoint();
|
||||
const btnSize = computed<"default" | "large">(() =>
|
||||
@@ -43,7 +43,7 @@ const btnSize = computed<"default" | "large">(() =>
|
||||
// itself the row; cells are every action button (`.r-v2-game-btn`) plus
|
||||
// the right-side metrics (`.r-v2-metric-btn`), skipping the layout
|
||||
// spacer. On pad-modality autofocus, `focusFirst` lands on the first
|
||||
// rendered button — Play if available (template renders it first when
|
||||
// rendered button: Play if available (template renders it first when
|
||||
// `canPlay`), otherwise Download.
|
||||
const rootEl = ref<HTMLElement | null>(null);
|
||||
useGridNav(rootEl, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// GameMetricsSections — the three per-user metric editors (completion,
|
||||
// GameMetricsSections: the three per-user metric editors (completion,
|
||||
// rating, difficulty) stacked as sections under a "Your progress"
|
||||
// header. Mounted inside the Status button's mobile sheet (see
|
||||
// GameActionBtn `withMetrics`) so phones edit scores there instead of
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// MetricMenuBtn — picker for a per-user numeric metric on a ROM.
|
||||
// MetricMenuBtn: picker for a per-user numeric metric on a ROM.
|
||||
// Powers all three score controls in the GameActions ribbon: rating,
|
||||
// difficulty (kind='rating'), and completion (kind='percent').
|
||||
//
|
||||
@@ -88,7 +88,7 @@ const valueLabel = computed(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Trigger — parallels .r-v2-game-btn--surface so it sits in the
|
||||
/* Trigger: parallels .r-v2-game-btn--surface so it sits in the
|
||||
action row without visual breaks. Translucent grey RTag-style
|
||||
surface, not the dark cover-overlay scrim. */
|
||||
.r-v2-metric-btn {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// MetricSection — inline editor body for a single per-user metric
|
||||
// MetricSection: inline editor body for a single per-user metric
|
||||
// (rating, difficulty, completion). Shared between MetricMenuBtn's
|
||||
// popup (desktop ribbon) and GameMetricsSections' mobile status sheet.
|
||||
// Renders a header (icon + label + live value), an RRating for 1..max
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// MoreMenu — dropdown "more actions" for a ROM, anchored to the trigger
|
||||
// MoreMenu: dropdown "more actions" for a ROM, anchored to the trigger
|
||||
// button you pass in via the `#activator` slot. RMenu hosts the shared
|
||||
// GameActionsList so the items match the right-click context menu
|
||||
// exactly.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// CardFlags — region / language emoji chips over the cover's bottom-left
|
||||
// CardFlags: region / language emoji chips over the cover's bottom-left
|
||||
// corner, the v2 counterpart of v1's cover flags. Visibility is driven by
|
||||
// the showRegions / showLanguages UI settings; each chip caps at three
|
||||
// emoji and carries the full list in its hover title. Purely informational:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
// GameCard — portrait game cover with hover overlay.
|
||||
// GameCard: portrait game cover with hover overlay.
|
||||
//
|
||||
// Feature composite (not a lib primitive): depends on SimpleRom,
|
||||
// useGameActions (via GameActionBtn), useBackgroundArt, and the store
|
||||
// layer. Lives under `components/` instead of `lib/` for
|
||||
// that reason — the library is reserved for truly generic primitives
|
||||
// that reason: the library is reserved for truly generic primitives
|
||||
// that a Storybook reader can drop into a page without wiring stores or
|
||||
// a router.
|
||||
//
|
||||
@@ -12,15 +12,15 @@
|
||||
// * 158×213 card art, 8px radius
|
||||
// * Rating badge top-right (appear on hover)
|
||||
// * Platform icon (TR, always visible): semi-transparent circle with the
|
||||
// platform's icon — toggle via `showPlatformIcon`.
|
||||
// platform's icon: toggle via `showPlatformIcon`.
|
||||
// * Hover overlay: play (center) · action row (BL: download, collection,
|
||||
// favorite, more) — action buttons are the shared GameActionBtn atom
|
||||
// favorite, more): action buttons are the shared GameActionBtn atom
|
||||
// so the card and the GameDetails header stay visually + behaviourally
|
||||
// in sync.
|
||||
// * Label below the card, 11.5px, truncated
|
||||
// * Optional `hero` variant: 300×169 (16:9) + larger multi-line label
|
||||
//
|
||||
// Static mode (`static`) strips the gallery chrome — no router-link,
|
||||
// Static mode (`static`) strips the gallery chrome: no router-link,
|
||||
// no action overlay, no rating / status / platform-icon badges, no
|
||||
// background-art highlight on hover. Click emits `@click` instead of
|
||||
// navigating. Hover scale stays by default (suppress with `noHover` for
|
||||
@@ -30,9 +30,9 @@
|
||||
// they have no destination.
|
||||
//
|
||||
// Other static-friendly props:
|
||||
// * `coverSrc` overrides the cover URL chain — used for preview blobs
|
||||
// * `coverSrc` overrides the cover URL chain: used for preview blobs
|
||||
// (edit dialog) and external provider URLs (match dialog).
|
||||
// * `showTitle` toggles the label below the cover — defaults true.
|
||||
// * `showTitle` toggles the label below the cover: defaults true.
|
||||
// * `selected` paints a brand-coloured outline on the cover art for
|
||||
// pickers (match-flow source variants, multi-select galleries).
|
||||
// * `#overlay` slot renders content on top of the cover for badges
|
||||
@@ -70,11 +70,11 @@ interface Props {
|
||||
/** Card scale tier. Drives the cover art width/height via the shared
|
||||
* `--r-card-art-w/h` tokens, and the hero variant's `--r-hero-w/h`
|
||||
* when `hero` is true.
|
||||
* xs (48 × 64) — list-row avatars
|
||||
* sm (120 × 162) — dense pickers
|
||||
* md (158 × 213) — gallery default (no class — keeps the global token)
|
||||
* lg (200 × 270) — edit-dialog preview
|
||||
* xl (240 × 324) — detail page cover
|
||||
* xs (48 × 64) , list-row avatars
|
||||
* sm (120 × 162), dense pickers
|
||||
* md (158 × 213), gallery default (no class, keeps the global token)
|
||||
* lg (200 × 270), edit-dialog preview
|
||||
* xl (240 × 324), detail page cover
|
||||
* Hero scales linearly with size (it's just an aspect-ratio change,
|
||||
* not a separate scale), so `hero` + any `size` paints the 16:9
|
||||
* shape at that tier's footprint. */
|
||||
@@ -82,12 +82,12 @@ interface Props {
|
||||
focused?: boolean;
|
||||
webp?: boolean;
|
||||
showPlatformIcon?: boolean;
|
||||
/** Passive mode — root is `<article role="button">` instead of a
|
||||
/** Passive mode: root is `<article role="button">` instead of a
|
||||
* router-link; action overlay / rating / status / platform-icon /
|
||||
* bg-art highlight are all suppressed. Click emits `@click` for the
|
||||
* consumer to handle. Hover scale stays unless `noHover` is set. */
|
||||
static?: boolean;
|
||||
/** Fully decorative — root is a plain `<div>` with no role, no
|
||||
/** Fully decorative: root is a plain `<div>` with no role, no
|
||||
* tabindex, no router wiring, no own click handlers. The whole
|
||||
* card is visual content; the parent (typically an `<a>` or row
|
||||
* button) owns interactivity and navigation. Avoids the nested-
|
||||
@@ -112,7 +112,7 @@ interface Props {
|
||||
coverPip?: boolean;
|
||||
/** Paint a brand-coloured outline to mark the card as selected
|
||||
* (cover-variant picker, multi-select gallery). When `selectable`
|
||||
* is true this prop is ignored — the card subscribes directly to
|
||||
* is true this prop is ignored: the card subscribes directly to
|
||||
* `gallerySelection` so a single source of truth (the store)
|
||||
* drives every selected card across the gallery. */
|
||||
selected?: boolean;
|
||||
@@ -125,7 +125,7 @@ interface Props {
|
||||
* gallery consumers (pickers, edit-dialog previews) keep their
|
||||
* prop-driven behaviour. */
|
||||
selectable?: boolean;
|
||||
/** Sparse position of the rom inside the gallery — the anchor that
|
||||
/** Sparse position of the rom inside the gallery: the anchor that
|
||||
* shift-range selection uses. Provided by `GalleryShell`. Required
|
||||
* when `selectable` is true (the composable needs a position to
|
||||
* store/restore the range anchor). */
|
||||
@@ -159,11 +159,11 @@ const art = useCoverArt(() => props.rom, {
|
||||
const coverUrl = art.coverUrl;
|
||||
const fallbackUrl = art.fallbackUrl;
|
||||
const coverAspectRatio = art.ratio;
|
||||
// Cover-art PIP — only when an override image (a screenshot) is actually
|
||||
// Cover-art PIP: only when an override image (a screenshot) is actually
|
||||
// covering the rom's own art, and the consumer opted in.
|
||||
const showCoverPip = computed(() => props.coverPip && !!props.coverSrc);
|
||||
// Alt-art styles (box3d / physical / miximage) drop the card frame so the
|
||||
// artwork floats — but only while a real image renders; with no cover the
|
||||
// artwork floats: but only while a real image renders; with no cover the
|
||||
// placeholder keeps its grey box so the title stays readable.
|
||||
const isAltStyle = computed(
|
||||
() =>
|
||||
@@ -172,7 +172,7 @@ const isAltStyle = computed(
|
||||
// Forward view-transition morph targets the GameCover's box element.
|
||||
const coverRef = ref<InstanceType<typeof GameCover> | null>(null);
|
||||
|
||||
// Synthetic roms (id = 0 / null / undefined) have no destination — skip
|
||||
// Synthetic roms (id = 0 / null / undefined) have no destination: skip
|
||||
// the entire view-transition wiring so we never try to morph to a
|
||||
// non-existent /rom/0 detail page.
|
||||
const isSynthetic = computed(() => !props.rom.id);
|
||||
@@ -187,7 +187,7 @@ const href = computed(() => props.to ?? `/rom/${props.rom.id}`);
|
||||
|
||||
const setBgArt = useBackgroundArt();
|
||||
// Same handler fires on hover AND focus so keyboard/gamepad users get
|
||||
// the background cross-fade to the focused cover — mirrors what mouse
|
||||
// the background cross-fade to the focused cover: mirrors what mouse
|
||||
// users see when they rest a pointer on the card. Suppressed in static
|
||||
// mode (the card isn't part of a gallery surface where the background
|
||||
// reads as the focused cover).
|
||||
@@ -239,7 +239,7 @@ const { morphTransition } = useViewTransition();
|
||||
// thumbnails, dialog tiles) render no overlay, so creating the composable for
|
||||
// them is pure waste. When present, share this single instance with every
|
||||
// GameActionBtn in the overlay (play / download / collection / favorite /
|
||||
// status / more) instead of each button spinning up its own — the difference
|
||||
// status / more) instead of each button spinning up its own: the difference
|
||||
// between ~1 and ~7 live `useGameActions` per card, which dominates a
|
||||
// virtualised grid's cost. Safe to call conditionally (no lifecycle hooks).
|
||||
const actions =
|
||||
@@ -261,12 +261,12 @@ function onPlatformClick(e: MouseEvent) {
|
||||
const emit = defineEmits<{
|
||||
(e: "click", event: MouseEvent): void;
|
||||
/** The cover's natural aspect ratio (width / height) once its image
|
||||
* loads — forwarded from GameCover so the gallery can flow-pack cards
|
||||
* loads: forwarded from GameCover so the gallery can flow-pack cards
|
||||
* at their true shape. Carries the rom id so the consumer can key it. */
|
||||
(e: "ratio", payload: { romId: number; ratio: number }): void;
|
||||
}>();
|
||||
|
||||
// Gallery selection — only wired when the consumer opts in via
|
||||
// Gallery selection: only wired when the consumer opts in via
|
||||
// `selectable`. Outside the gallery (pickers, previews) the store
|
||||
// stays untouched so a stray match-dialog selection can't leak into
|
||||
// the gallery's selection bar.
|
||||
@@ -293,7 +293,7 @@ const checkboxAlwaysOn = computed(
|
||||
|
||||
function onCheckboxClick(e: MouseEvent) {
|
||||
// The checkbox itself is a deliberate selection gesture. We bind
|
||||
// RCheckbox in "controlled" mode (only `:model-value` — no v-model),
|
||||
// RCheckbox in "controlled" mode (only `:model-value`: no v-model),
|
||||
// so the native input change is purely visual; `preventDefault`
|
||||
// here cancels the label → input click default so the input never
|
||||
// toggles itself, and our store mutation drives the next render.
|
||||
@@ -313,10 +313,10 @@ function onCardClickCapture(e: MouseEvent) {
|
||||
function onCardClick(e: MouseEvent) {
|
||||
// Decorative mode: the card has no behaviour of its own. Let the
|
||||
// event bubble untouched so the parent (typically an anchor or row
|
||||
// button) handles navigation / selection — no emit, no morph.
|
||||
// button) handles navigation / selection: no emit, no morph.
|
||||
if (props.decorative) return;
|
||||
// Static mode: consumer owns the click. No router push, no forward
|
||||
// morph — just hand the event off.
|
||||
// morph: just hand the event off.
|
||||
if (props.static) {
|
||||
emit("click", e);
|
||||
return;
|
||||
@@ -409,7 +409,7 @@ function onStaticKeydown(e: KeyboardEvent) {
|
||||
:morph-id="isSynthetic ? null : rom.id"
|
||||
@ratio="emit('ratio', { romId: rom.id, ratio: $event })"
|
||||
>
|
||||
<!-- Selection checkbox — top-left, drawn over the cover. Hidden
|
||||
<!-- Selection checkbox: top-left, drawn over the cover. Hidden
|
||||
at rest; appears on hover for discoverability, and stays
|
||||
pinned whenever the gallery is in selection mode so the
|
||||
user always knows which cards are picked. Uses RCheckbox
|
||||
@@ -430,19 +430,19 @@ function onStaticKeydown(e: KeyboardEvent) {
|
||||
@click="onCheckboxClick"
|
||||
/>
|
||||
|
||||
<!-- Consumer-driven overlay slot — sits above the cover, below
|
||||
<!-- Consumer-driven overlay slot: sits above the cover, below
|
||||
the gallery chrome. Use for badges that aren't part of the
|
||||
default overlay set (provider logos, custom status pills). -->
|
||||
<div v-if="$slots.overlay" class="r-gc__overlay-slot">
|
||||
<slot name="overlay" />
|
||||
</div>
|
||||
|
||||
<!-- Cover-art PIP — small box-art thumbnail floated bottom-right while a
|
||||
<!-- Cover-art PIP: small box-art thumbnail floated bottom-right while a
|
||||
screenshot covers the rom's own art. Fades out on hover (below) so
|
||||
it never collides with the action overlay. -->
|
||||
<CoverArtPip v-if="showCoverPip" :rom="rom" :title="title" :webp="webp" />
|
||||
|
||||
<!-- Gallery chrome — all suppressed in static / decorative mode. -->
|
||||
<!-- Gallery chrome: all suppressed in static / decorative mode. -->
|
||||
<template v-if="!static && !decorative">
|
||||
<div v-if="ratingLabel" class="r-gc__rating">★ {{ ratingLabel }}</div>
|
||||
|
||||
@@ -474,11 +474,11 @@ function onStaticKeydown(e: KeyboardEvent) {
|
||||
|
||||
<SiblingBadge :rom="rom" />
|
||||
|
||||
<!-- Region / language flags — bottom-left, gated by the
|
||||
<!-- Region / language flags: bottom-left, gated by the
|
||||
showRegions / showLanguages UI settings. -->
|
||||
<CardFlags :rom="rom" />
|
||||
|
||||
<!-- Hover overlay — action buttons are the shared GameActionBtn. -->
|
||||
<!-- Hover overlay: action buttons are the shared GameActionBtn. -->
|
||||
<div class="r-gc__overlay">
|
||||
<div class="r-gc__overlay-center">
|
||||
<GameActionBtn
|
||||
@@ -512,7 +512,7 @@ function onStaticKeydown(e: KeyboardEvent) {
|
||||
<div v-if="showTitle" class="r-gc__label">
|
||||
{{ title }}
|
||||
</div>
|
||||
<!-- Full-name tooltip — the label below the cover truncates at one
|
||||
<!-- Full-name tooltip: the label below the cover truncates at one
|
||||
line (two on hero variant), so a hover reveal is the only way
|
||||
to see the full title without navigating in. `activator="parent"`
|
||||
attaches to the root card element so the tooltip fires no
|
||||
@@ -537,7 +537,7 @@ function onStaticKeydown(e: KeyboardEvent) {
|
||||
position: relative;
|
||||
outline: none;
|
||||
width: var(--r-card-art-w);
|
||||
/* Width is the card-art token exactly — never let a stray border /
|
||||
/* Width is the card-art token exactly: never let a stray border /
|
||||
padding push the rendered box past it (which would eat the grid gap
|
||||
and make neighbouring covers appear to touch). */
|
||||
box-sizing: border-box;
|
||||
@@ -545,7 +545,7 @@ function onStaticKeydown(e: KeyboardEvent) {
|
||||
color: var(--r-color-fg);
|
||||
}
|
||||
|
||||
/* Non-hero cards render at a FIXED HEIGHT with NATURAL WIDTH — same height,
|
||||
/* Non-hero cards render at a FIXED HEIGHT with NATURAL WIDTH: same height,
|
||||
varying widths, never cropped. `size` just picks the height (the default
|
||||
derives it from the reference width; tiers set it directly). Only `hero`
|
||||
(16:9) keeps a fixed w×h footprint. */
|
||||
@@ -575,7 +575,7 @@ function onStaticKeydown(e: KeyboardEvent) {
|
||||
|
||||
/* The art box IS the shared <GameCover> (this class lands on its root).
|
||||
GameCover owns the radius / overflow / background / image; the card
|
||||
keeps only the gallery-level box concerns: footprint (height — width
|
||||
keeps only the gallery-level box concerns: footprint (height, width
|
||||
comes from `.r-gc`), the focus outline, and the hover/focus transition. */
|
||||
.r-gc__art {
|
||||
height: var(--r-card-art-h);
|
||||
@@ -591,14 +591,14 @@ function onStaticKeydown(e: KeyboardEvent) {
|
||||
artwork floats: GameCover already makes its own background transparent;
|
||||
here we kill the hover / focus / selected DROP SHADOW so a transparent
|
||||
disc doesn't get a rectangular shadow around empty space (most obvious
|
||||
on the wide hero — Home "continue playing"). The hover scale + the
|
||||
on the wide hero: Home "continue playing"). The hover scale + the
|
||||
focus/selected brand OUTLINE (the `outline` property, not the shadow)
|
||||
stay. `!important` beats the multi-state shadow selectors without
|
||||
duplicating their whole list. */
|
||||
.r-gc--alt-art .r-gc__art {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
/* …and no darkening scrim on hover — the gradient would paint a dark
|
||||
/* …and no darkening scrim on hover: the gradient would paint a dark
|
||||
rectangle around a floating disc / cartridge. The overlay's controls
|
||||
still appear (the emphasized Play CTA carries its own background). */
|
||||
.r-gc--alt-art .r-gc__overlay {
|
||||
@@ -648,7 +648,7 @@ function onStaticKeydown(e: KeyboardEvent) {
|
||||
right: 7px;
|
||||
z-index: 2;
|
||||
/* Shrink-wrap around the platform icon. RBtn's size classes try to
|
||||
impose a fixed `height` / `width: var(--r-btn-rest-h)` — we have
|
||||
impose a fixed `height` / `width: var(--r-btn-rest-h)`, we have
|
||||
to defeat those so the badge always fits whatever `:size` the
|
||||
PlatformIcon was given (icon + 3px padding all around). */
|
||||
width: auto !important;
|
||||
@@ -661,7 +661,7 @@ function onStaticKeydown(e: KeyboardEvent) {
|
||||
border: 1px solid var(--r-color-overlay-border) !important;
|
||||
color: var(--r-color-overlay-fg) !important;
|
||||
/* Override RBtn's at-rest opacity (0.7) so the 78% scrim reads at
|
||||
full strength — without this the bg modulates down to ~55% and
|
||||
full strength: without this the bg modulates down to ~55% and
|
||||
reads faint over busy cover art. */
|
||||
opacity: 1 !important;
|
||||
transition:
|
||||
@@ -720,7 +720,7 @@ function onStaticKeydown(e: KeyboardEvent) {
|
||||
transition: opacity 0.12s ease;
|
||||
}
|
||||
|
||||
/* Hover-driven art scale gated to mouse/touch — a cursor parked from a
|
||||
/* Hover-driven art scale gated to mouse/touch: a cursor parked from a
|
||||
previous mouse session shouldn't compete with the focused card when
|
||||
the user is on a gamepad. Focus / pinned / sibling-pinned states
|
||||
trigger the same effect in every modality. */
|
||||
@@ -752,7 +752,7 @@ html:not([data-input="pad"]) .r-gc:hover .r-gc__overlay,
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Consumer-driven `#overlay` slot — top-right anchor by default so
|
||||
/* Consumer-driven `#overlay` slot: top-right anchor by default so
|
||||
provider logos / status pills sit out of the way of the rating
|
||||
centred above. `pointer-events: none` lets clicks pass through to
|
||||
the card so the consumer doesn't have to opt out per-element. */
|
||||
@@ -767,7 +767,7 @@ html:not([data-input="pad"]) .r-gc:hover .r-gc__overlay,
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Cover-art PIP (CoverArtPip) — fades out under the hover overlay so it never
|
||||
/* Cover-art PIP (CoverArtPip): fades out under the hover overlay so it never
|
||||
overlaps the action row. The footprint / chrome live in the component. */
|
||||
html:not([data-input="pad"]) .r-gc:hover :deep(.cover-art-pip),
|
||||
.r-gc:focus-visible :deep(.cover-art-pip),
|
||||
@@ -795,7 +795,7 @@ html:not([data-input="pad"]) .r-gc:hover .r-gc__rating,
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Status badge — corner of the cover that mirrors the platform
|
||||
/* Status badge: corner of the cover that mirrors the platform
|
||||
icon. The actual <button> lives inside GameActionBtn's RMenu
|
||||
activator slot, so we reach it via `:deep` matching the action
|
||||
class. Default position (no platform icon shown): top-left.
|
||||
@@ -829,7 +829,7 @@ html:not([data-input="pad"]) .r-gc:hover :deep(.r-v2-game-btn--action-status),
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Sibling-versions badge — right-side stack with the platform icon
|
||||
/* Sibling-versions badge: right-side stack with the platform icon
|
||||
and status badge. With a platform icon present (the normal gallery
|
||||
layout) the chip drops below it; without one it takes the top-right
|
||||
corner. When the sibling badge IS present, the status badge slides
|
||||
@@ -852,7 +852,7 @@ html:not([data-input="pad"]) .r-gc:hover :deep(.r-v2-game-btn--action-status),
|
||||
top: 80px;
|
||||
}
|
||||
|
||||
/* Keyboard / gamepad focus — paint the outline in the brand colour and
|
||||
/* Keyboard / gamepad focus: paint the outline in the brand colour and
|
||||
stack a drop-shadow + outer bloom on top so the focused card reads
|
||||
distinctly from hover. Mirrors the v1 console GameCard pattern. */
|
||||
.r-gc:focus-visible {
|
||||
@@ -866,7 +866,7 @@ html:not([data-input="pad"]) .r-gc:hover :deep(.r-v2-game-btn--action-status),
|
||||
0 0 18px color-mix(in srgb, var(--r-color-brand-primary) 60%, transparent);
|
||||
}
|
||||
|
||||
/* `selected` — same brand-outline language as focus, but persistent.
|
||||
/* `selected`: same brand-outline language as focus, but persistent.
|
||||
Used by cover-variant pickers and multi-select galleries to mark the
|
||||
currently-picked card without relying on focus state. No flush `0 0 0 Npx`
|
||||
ring here: the `outline` above already draws that border, and stacking a
|
||||
@@ -923,7 +923,7 @@ html:not([data-input="pad"]) .r-gc:hover .r-gc__label,
|
||||
|
||||
/* ── Selection checkbox ────────────────────────────────────
|
||||
Top-left affordance. The visible chrome (box, fill, draw-in
|
||||
tick, press squash) all comes from RCheckbox — this rule
|
||||
tick, press squash) all comes from RCheckbox: this rule
|
||||
only owns the positioning and the visibility fade.
|
||||
At rest the checkbox is invisible; it fades in on hover or
|
||||
focus, and stays pinned whenever the gallery is in selection
|
||||
@@ -947,7 +947,7 @@ html:not([data-input="pad"]) .r-gc:hover .r-gc__check,
|
||||
}
|
||||
|
||||
/* Glassy scrim under the unchecked box so the outline reads over
|
||||
busy cover art — RCheckbox's default border colour assumes a
|
||||
busy cover art: RCheckbox's default border colour assumes a
|
||||
plain page background, here it sits on photos. Once checked the
|
||||
primitive's brand fill takes over and we step out of the way. */
|
||||
.r-gc__check :deep(.r-checkbox__box) {
|
||||
@@ -956,7 +956,7 @@ html:not([data-input="pad"]) .r-gc:hover .r-gc__check,
|
||||
}
|
||||
.r-gc__check.r-checkbox--checked :deep(.r-checkbox__box),
|
||||
.r-gc__check.r-checkbox--indeterminate :deep(.r-checkbox__box) {
|
||||
/* Let RCheckbox's checked styles win — clear our scrim overrides
|
||||
/* Let RCheckbox's checked styles win: clear our scrim overrides
|
||||
so the brand fill + glow apply unmodified. */
|
||||
background: var(--r-color-brand-primary);
|
||||
border-color: var(--r-color-brand-primary);
|
||||
@@ -967,7 +967,7 @@ html:not([data-input="pad"]) .r-gc:hover .r-gc__check,
|
||||
locally on the card, so every downstream rule that already reads
|
||||
those vars (`.r-gc { width }`, `.r-gc__art { width / height }`,
|
||||
`.r-gc--hero .r-gc__art`) picks up the new values without
|
||||
duplication. "md" is the default — no class — so the gallery
|
||||
duplication. "md" is the default, no class, so the gallery
|
||||
grid (which reads the global `--r-card-art-w`) stays in lock-step
|
||||
with the un-tiered card. */
|
||||
.r-gc--size-xs {
|
||||
@@ -997,7 +997,7 @@ html:not([data-input="pad"]) .r-gc:hover .r-gc__check,
|
||||
|
||||
/* ── Mobile ───────────────────────────────────────────────────
|
||||
Forces a tighter footprint on phones. Only applies to the
|
||||
default (md) card — explicit size tiers are intentional choices
|
||||
default (md) card: explicit size tiers are intentional choices
|
||||
from the consumer (an `xs` list-row avatar should stay 48px even
|
||||
on mobile). Sets the vars instead of `width` directly so the
|
||||
inner `.r-gc__art` rule picks them up too. */
|
||||
|
||||
@@ -11,10 +11,10 @@ import { RSkeletonBlock } from "@v2/lib";
|
||||
import { computed } from "vue";
|
||||
|
||||
interface Props {
|
||||
/** Hero variant — 16:9 art with a larger label, matching `GameCard`'s
|
||||
/** Hero variant: 16:9 art with a larger label, matching `GameCard`'s
|
||||
* `hero` prop. Scales with `size` (same as GameCard). */
|
||||
hero?: boolean;
|
||||
/** Size tier — mirrors `GameCard`'s `size` prop so a skeleton swapped
|
||||
/** Size tier: mirrors `GameCard`'s `size` prop so a skeleton swapped
|
||||
* into a tiered card slot keeps the same footprint and the
|
||||
* virtualiser's measurements stay stable. */
|
||||
size?: "xs" | "sm" | "md" | "lg" | "xl";
|
||||
@@ -72,7 +72,7 @@ const artHeight = computed(() =>
|
||||
--r-card-art-h: calc(var(--r-card-art-w) / var(--r-cover-ratio, 0.6667));
|
||||
}
|
||||
|
||||
/* Hero (16:9) variant — mirrors `GameCard`'s `.r-gc--hero` block. The
|
||||
/* Hero (16:9) variant: mirrors `GameCard`'s `.r-gc--hero` block. The
|
||||
art skeleton picks up its own larger size via the prop-bound CSS
|
||||
vars; here we just widen the container so the centred label sits at
|
||||
the right horizontal width, and bump the label's reserved height to
|
||||
@@ -85,7 +85,7 @@ const artHeight = computed(() =>
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Size tiers — mirror GameCard's `.r-gc--size-*` overrides exactly so
|
||||
/* Size tiers: mirror GameCard's `.r-gc--size-*` overrides exactly so
|
||||
skeleton + real card occupy the same footprint when one swaps for
|
||||
the other in a virtualiser slot. */
|
||||
.r-gcs--size-xs {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// SiblingBadge — gallery overlay that exposes a ROM's grouped sibling
|
||||
// SiblingBadge: gallery overlay that exposes a ROM's grouped sibling
|
||||
// versions. Visible only when `groupRoms` is on (the gallery is showing
|
||||
// one card per sibling group); with grouping off every version is its
|
||||
// own card and the chip would be redundant noise.
|
||||
@@ -10,7 +10,7 @@
|
||||
// a router-link to that sibling's detail page. The current rom is
|
||||
// marked as the active row (radio-like) so the user can see which
|
||||
// one they're looking at.
|
||||
// * Click on the chip itself doesn't navigate — the rom's card
|
||||
// * Click on the chip itself doesn't navigate: the rom's card
|
||||
// already handles its own click; the chip is a popover trigger only.
|
||||
import { RIcon, RMenu, RMenuItem } from "@v2/lib";
|
||||
import { computed, ref } from "vue";
|
||||
@@ -25,7 +25,7 @@ const props = withDefaults(
|
||||
rom: SimpleRom;
|
||||
/** Layout direction. Mirrors GameActionBtn's `orientation` prop:
|
||||
* `vertical` stacks icon over count (the GameCard cover-overlay
|
||||
* default — fits the right-side column of badges), `horizontal`
|
||||
* default: fits the right-side column of badges), `horizontal`
|
||||
* places them side-by-side (used inline in list rows where the
|
||||
* badge sits next to the title). */
|
||||
orientation?: "horizontal" | "vertical";
|
||||
@@ -36,7 +36,7 @@ const props = withDefaults(
|
||||
const { t } = useI18n();
|
||||
const { groupRoms } = useUISettings();
|
||||
|
||||
// Menu-open state — exposed to the parent surface via the
|
||||
// Menu-open state: exposed to the parent surface via the
|
||||
// `.sibling-badge--pinned` class on the chip, so the GameCard can keep
|
||||
// its hover chrome painted while the version list is open (same
|
||||
// "pinned" pattern GameActionBtn uses for `more` / `status` menus).
|
||||
@@ -44,7 +44,7 @@ const menuOpen = ref(false);
|
||||
|
||||
// The scan socket (`scan:scanning_rom`) emits roms with `sibling_roms`
|
||||
// stripped from the payload, so it can be undefined here even though the
|
||||
// type marks it required. Default to an empty list — otherwise reading
|
||||
// type marks it required. Default to an empty list: otherwise reading
|
||||
// `.length`/`.map` throws while a scan streams roms and `groupRoms` is on.
|
||||
// REST payloads always include it (possibly empty).
|
||||
const siblings = computed(() => props.rom.sibling_roms ?? []);
|
||||
@@ -62,7 +62,7 @@ const tooltipText = computed(() =>
|
||||
// `[rom, ...siblings]` mirrors the order v1's VersionSwitcher uses. The
|
||||
// current rom always leads so the user immediately reads "the one I'm
|
||||
// looking at" before the alternates. `main` flags the user-marked
|
||||
// default version — read off `rom_user` for this rom and off the
|
||||
// default version: read off `rom_user` for this rom and off the
|
||||
// per-sibling `is_main_sibling` field for the rest (the backend
|
||||
// resolves it from the request user's RomUser).
|
||||
const versions = computed(() => [
|
||||
@@ -83,7 +83,7 @@ const versions = computed(() => [
|
||||
const mainTooltip = computed(() => t("rom.default-version"));
|
||||
|
||||
// Prevent the parent card's `router-link` from intercepting clicks on
|
||||
// the chip / menu — without this the gallery would navigate to the
|
||||
// the chip / menu: without this the gallery would navigate to the
|
||||
// current rom instead of opening the menu.
|
||||
function stopCard(e: Event) {
|
||||
e.stopPropagation();
|
||||
@@ -138,7 +138,7 @@ function stopCard(e: Event) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Vertical pill — same visual idiom as `GameActionBtn` with
|
||||
/* Vertical pill: same visual idiom as `GameActionBtn` with
|
||||
`orientation="vertical"` (icon stacked over the count) so the
|
||||
sibling badge reads as a sibling of the status badge in the
|
||||
right-side column. Positioning lives on the consuming surface
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// AchievementsTab — RetroAchievements summary + filter row + per-achievement
|
||||
// AchievementsTab: RetroAchievements summary + filter row + per-achievement
|
||||
// list. The "earned" set comes from the parent (computed off
|
||||
// auth.user.ra_progression so it stays reactive); rows look up by
|
||||
// `badge_id` against the set in O(1).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// AgeRatingBadges — renders the ROM's age ratings as visual badges.
|
||||
// AgeRatingBadges: renders the ROM's age ratings as visual badges.
|
||||
// Mirrors v1's matching logic (Details/Info/GameInfo.vue): the merged
|
||||
// list of rating strings on `metadatum.age_ratings` is cross-referenced
|
||||
// with `igdb_metadata.age_ratings` and `ss_metadata.age_ratings` to
|
||||
@@ -13,7 +13,7 @@ import { computed, reactive } from "vue";
|
||||
import type { DetailedRom } from "@/stores/roms";
|
||||
|
||||
// IGDB hosts every rating icon at a conventional URL, but not every
|
||||
// `category_rating` combo is actually populated — old/regional ratings
|
||||
// `category_rating` combo is actually populated: old/regional ratings
|
||||
// 404 even though `igdbIconUrl` can build the URL. We track every URL
|
||||
// that 404s in this Set so the matching badge swaps to the text chip
|
||||
// fallback on the next paint, matching GameCard's "load image →
|
||||
@@ -68,7 +68,7 @@ const badges = computed<Badge[]>(() => {
|
||||
);
|
||||
|
||||
return ratings.map<Badge>((entry) => {
|
||||
// Manually entered "CATEGORY:RATING" — reconstruct the icon URL
|
||||
// Manually entered "CATEGORY:RATING": reconstruct the icon URL
|
||||
// by convention since there's no provider object to look up.
|
||||
if (entry.includes(":")) {
|
||||
const [categoryRaw, ratingRaw] = entry.split(":");
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
// CoverColumn — fixed-width left column wrapping the shared GameCover. The
|
||||
// CoverColumn: fixed-width left column wrapping the shared GameCover. The
|
||||
// cover honours the details-page boxart style (falling back to the
|
||||
// gallery-wide one), animates on hover, paints the procedural placeholder
|
||||
// when empty, and is the destination of the shared-element morph from the
|
||||
// GameCard the user clicked through from — all of that lives in GameCover
|
||||
// GameCard the user clicked through from: all of that lives in GameCover
|
||||
// now; this just sizes the column.
|
||||
//
|
||||
// When the resolved boxart style is the 3D box AND the rom has the full set
|
||||
// of flat scans (front + back + spine, from ScreenScraper), the hero
|
||||
// upgrades to the interactive RBox3D the user can spin. Anything missing —
|
||||
// a different style, an incomplete set, or a failed image — falls straight
|
||||
// upgrades to the interactive RBox3D the user can spin. Anything missing:
|
||||
// a different style, an incomplete set, or a failed image: falls straight
|
||||
// back to the flat GameCover.
|
||||
import { RBox3D } from "@v2/lib";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// FileRow — a single file entry inside the FilesTab list.
|
||||
// FileRow: a single file entry inside the FilesTab list.
|
||||
//
|
||||
// Owns the row layout: leading selection checkbox, filename block
|
||||
// (icon + path), meta line (category chip + size + audio metadata),
|
||||
@@ -9,7 +9,7 @@
|
||||
// API calls.
|
||||
//
|
||||
// The parent also decides whether to render the category-icon at the
|
||||
// row's leading edge and the category chip in the meta line — both
|
||||
// row's leading edge and the category chip in the meta line: both
|
||||
// are redundant inside folder subtabs where every row shares the same
|
||||
// category, so we toggle them off there.
|
||||
import { RBtn, RCheckbox, RChip, RIcon } from "@v2/lib";
|
||||
@@ -31,7 +31,7 @@ const props = defineProps<{
|
||||
/** Filename rendered in the row; the parent strips the folder
|
||||
* prefix inside folder subtabs and passes the result here. */
|
||||
displayPath: string;
|
||||
/** Full relative path — used for native title / aria-label so the
|
||||
/** Full relative path: used for native title / aria-label so the
|
||||
* context isn't lost when the displayed name is short. */
|
||||
relativePath: string;
|
||||
selected: boolean;
|
||||
@@ -52,7 +52,7 @@ const emit = defineEmits<{
|
||||
(e: "delete"): void;
|
||||
}>();
|
||||
|
||||
// Shared category metadata — kept inline (instead of importing from
|
||||
// Shared category metadata: kept inline (instead of importing from
|
||||
// FilesTab) so this row is self-contained. The chip / icon are only
|
||||
// rendered when the parent says so, so the lookup stays cheap.
|
||||
const CATEGORY_META = computed<
|
||||
@@ -294,7 +294,7 @@ const hasAnyHash = computed(
|
||||
.r-v2-file-row__check {
|
||||
/* Mirror the filename's line box so the checkbox aligns with the
|
||||
filename's optical centre instead of the row's top edge. The
|
||||
filename uses `font-size: 13px` (see __row-path) — multiplying
|
||||
filename uses `font-size: 13px` (see __row-path), multiplying
|
||||
by the inherited body line-height (~1.5) yields ~19.5px, which
|
||||
we round to 20px here. `align-items: center` then drops the
|
||||
16px sm-checkbox box on the line centre. */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// FilesSummary — top-of-FilesTab card showing the ROM's name + total
|
||||
// FilesSummary: top-of-FilesTab card showing the ROM's name + total
|
||||
// size + revision + ROM-level hashes (click-to-copy via HashChip).
|
||||
// Carries the missing-from-fs flag when applicable.
|
||||
import { RIcon } from "@v2/lib";
|
||||
@@ -24,7 +24,7 @@ interface RomHash {
|
||||
value: string | null;
|
||||
}
|
||||
const hashes = computed<RomHash[]>(() => {
|
||||
// CHD SHA-1 lives on the file, not the ROM — surface it at ROM level
|
||||
// CHD SHA-1 lives on the file, not the ROM: surface it at ROM level
|
||||
// only when the ROM is a single CHD file (mirrors v1's FileInfo).
|
||||
const chdSha1 = props.rom.has_simple_single_file
|
||||
? (props.rom.files[0]?.chd_sha1_hash ?? null)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
// GameHeader — right-column header for the details view.
|
||||
// GameHeader: right-column header for the details view.
|
||||
// Four rows, top to bottom:
|
||||
// 1. Title (+ previous / next game arrows on the right, desktop only)
|
||||
// 2. Meta (year · platform-icon + platform · verified RTag)
|
||||
// 3. Tags (regions + languages + custom tags) — RTag primitive,
|
||||
// 3. Tags (regions + languages + custom tags): RTag primitive,
|
||||
// each a `searchLocation` pivot into the filtered search
|
||||
// 4. GameActions (Play · Download · Favorite · Share · More)
|
||||
//
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// InfoGrid — two-column section grid used in the Overview tab.
|
||||
// InfoGrid: two-column section grid used in the Overview tab.
|
||||
// Each section is `icon + label` on the header line and a row of
|
||||
// small chips below (genres, companies, franchises, collections, …).
|
||||
// The icon gives each section a semantic anchor instead of relying
|
||||
@@ -19,7 +19,7 @@ defineOptions({ inheritAttrs: false });
|
||||
export type InfoGridSection = {
|
||||
label: string;
|
||||
items: string[];
|
||||
/** Leading icon for the section header — gives each category a
|
||||
/** Leading icon for the section header: gives each category a
|
||||
* semantic cue (e.g. tags for genres, building for companies). */
|
||||
icon?: string;
|
||||
/** Gallery filter these items map to. Set it to make the chips
|
||||
@@ -65,7 +65,7 @@ const visible = () => props.sections.filter((s) => s.items.length > 0);
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Auto-fill grid spanning the whole details body — sections reflow to a
|
||||
/* Auto-fill grid spanning the whole details body: sections reflow to a
|
||||
new row when the column would shrink below 240px. Mirrors the
|
||||
responsive pattern used by ProviderGrid in the Metadata tab so both
|
||||
surfaces feel like siblings. */
|
||||
@@ -89,7 +89,7 @@ const visible = () => props.sections.filter((s) => s.items.length > 0);
|
||||
}
|
||||
.r-v2-det-infogrid__label-icon {
|
||||
/* Brand-primary keeps the section header readable as a "label" cue
|
||||
against the muted eyebrow text — the icon is the focal point. */
|
||||
against the muted eyebrow text: the icon is the focal point. */
|
||||
color: var(--r-color-brand-primary);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// MainSiblingToggle — marks this rom as the "default version" of the
|
||||
// MainSiblingToggle: marks this rom as the "default version" of the
|
||||
// sibling group: when the gallery groups siblings, the main one is the
|
||||
// card that's shown in the user's library. Per-user state — every user
|
||||
// card that's shown in the user's library. Per-user state: every user
|
||||
// picks their own preferred version.
|
||||
//
|
||||
// The backend enforces the exclusive constraint (setting this one to
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// MarkdownViewer (v2) — renders a Markdown (.md) manual with the same v2
|
||||
// MarkdownViewer (v2): renders a Markdown (.md) manual with the same v2
|
||||
// chrome as PdfViewer. Manuals can be PDF or Markdown; MediaTab picks the
|
||||
// viewer by extension. The file is fetched as text and handed to MdPreview
|
||||
// (md-editor-v3), the same renderer used by NotesTab.
|
||||
@@ -43,7 +43,7 @@ const mdTheme = computed<"light" | "dark">(() =>
|
||||
isLight.value ? "light" : "dark",
|
||||
);
|
||||
|
||||
// Filename for the download button — last path segment without the cache-bust
|
||||
// Filename for the download button: last path segment without the cache-bust
|
||||
// query, decoded back to its human form (e.g. `README.md`).
|
||||
const fileName = computed(() => {
|
||||
const path = props.url.split("?")[0];
|
||||
@@ -187,7 +187,7 @@ watch(() => props.url, load, { immediate: true });
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Toolbar inherits the parent's bg-elevated — no separate background or
|
||||
/* Toolbar inherits the parent's bg-elevated: no separate background or
|
||||
divider so the surface reads as one continuous panel (mirrors PdfViewer). */
|
||||
.r-v2-mdv__toolbar {
|
||||
display: flex;
|
||||
@@ -258,7 +258,7 @@ watch(() => props.url, load, { immediate: true });
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* MdPreview paints its own surface — blend it with the surrounding
|
||||
/* MdPreview paints its own surface: blend it with the surrounding
|
||||
bg-elevated container so there's no visible seam under the toolbar. */
|
||||
.r-v2-mdv__preview {
|
||||
background: transparent;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// PlayerCountBadge — renders `metadatum.player_count` as a single
|
||||
// PlayerCountBadge: renders `metadatum.player_count` as a single
|
||||
// semantic badge. The icon swaps with the maximum number of players
|
||||
// the string mentions:
|
||||
// 1 → mdi-account-outline ("Single player")
|
||||
@@ -7,7 +7,7 @@
|
||||
// 3–4 → mdi-account-group-outline
|
||||
// 5+ → mdi-account-multiple-plus-outline
|
||||
// The string the API returns is free-form (the providers feed it as
|
||||
// a bare number, a range, or a sentence — "1", "1-4", "Up to 4
|
||||
// a bare number, a range, or a sentence: "1", "1-4", "Up to 4
|
||||
// players", "Single Player", "Massively Multiplayer" …). We pull
|
||||
// the largest integer out as the signal and fall back to a generic
|
||||
// group icon when no digit is parseable, so non-numeric descriptors
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//
|
||||
// Two conditions gate the arrows, and both are needed. The ROM has to be
|
||||
// in that list, and the user has to have arrived from the gallery that
|
||||
// built it (see useGalleryProvenance) — the store keeps its index alive
|
||||
// built it (see useGalleryProvenance): the store keeps its index alive
|
||||
// after the gallery is left, so membership alone would hand a stale list
|
||||
// to a ROM opened from Home, Activity or a scan result.
|
||||
import { RBtn } from "@v2/lib";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// ProviderCard — single metadata-source card. Provider name + logo
|
||||
// ProviderCard: single metadata-source card. Provider name + logo
|
||||
// + linked external ID (or "Not linked"), plus the provider's rating
|
||||
// when it publishes one. When `url` is non-null and the ID is set, the
|
||||
// card renders as an `<a target=_blank>`; otherwise it's a static div.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// ProviderGrid — Metadata-tab grid showing every configured provider.
|
||||
// ProviderGrid: Metadata-tab grid showing every configured provider.
|
||||
// Linked providers come first, unlinked ones follow in a muted row so
|
||||
// users can see what's missing. Card visuals are owned by ProviderCard.
|
||||
import { computed } from "vue";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
// RelatedGamesGrid — one labelled grid of IGDBRelatedGame tiles.
|
||||
// RelatedGamesGrid: one labelled grid of IGDBRelatedGame tiles.
|
||||
//
|
||||
// Used by both the "Additional content" tab (Expansions / DLC) and the
|
||||
// "Related" tab (Remakes / Remasters / Similar). Multiple sections per
|
||||
// tab? Render this component multiple times — one per section. Each
|
||||
// tab? Render this component multiple times: one per section. Each
|
||||
// card is a RelatedGameCard, which owns the per-card cross-reference
|
||||
// against the local RomM library — so this grid stays a thin renderer.
|
||||
// against the local RomM library: so this grid stays a thin renderer.
|
||||
import type { IGDBRelatedGame } from "@/__generated__";
|
||||
import RelatedGameCard from "@/v2/components/GameDetails/RelatedGameCard.vue";
|
||||
|
||||
@@ -38,7 +38,7 @@ defineProps<{
|
||||
color: var(--r-color-fg-faint);
|
||||
}
|
||||
|
||||
/* Flex-wrap (not auto-fill grid) — GameCard has a fixed 158px width
|
||||
/* Flex-wrap (not auto-fill grid): GameCard has a fixed 158px width
|
||||
and a hard-coded `flex-shrink: 0`, so a `1fr` track would tear the
|
||||
layout: the column would compute below 158px on narrow viewports
|
||||
and the cards would overflow into the next column. Flex-wrap lets
|
||||
|
||||
@@ -247,7 +247,7 @@ async function onStateUpload(files: File[], emulator: string | null) {
|
||||
}
|
||||
|
||||
// ---------- Per-row actions ----------
|
||||
// Both saves and states ship `download_path` from the backend — fire
|
||||
// Both saves and states ship `download_path` from the backend: fire
|
||||
// a synthesized anchor click rather than a window.open so the browser
|
||||
// uses the right filename and skips the new-tab affordance.
|
||||
function downloadAsset(asset: { download_path: string; file_name: string }) {
|
||||
@@ -591,7 +591,7 @@ const labelSuggestions = computed(() =>
|
||||
</aside>
|
||||
|
||||
<div class="r-v2-saves__content">
|
||||
<!-- Saves subtab — vertical info list -->
|
||||
<!-- Saves subtab: vertical info list -->
|
||||
<section v-show="subTab === 'saves'" class="r-v2-saves__panel">
|
||||
<!-- Mine -->
|
||||
<div class="r-v2-saves__section">
|
||||
@@ -699,7 +699,7 @@ const labelSuggestions = computed(() =>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- States subtab — tile grid (screenshot is the point) -->
|
||||
<!-- States subtab: tile grid (screenshot is the point) -->
|
||||
<section v-show="subTab === 'states'" class="r-v2-saves__panel">
|
||||
<!-- Mine -->
|
||||
<div class="r-v2-saves__section">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// TextViewer (v2) — renders a plain-text (.txt) or HTML (.html/.htm) document
|
||||
// TextViewer (v2): renders a plain-text (.txt) or HTML (.html/.htm) document
|
||||
// with the same v2 chrome as MarkdownViewer/PdfViewer. Used for manuals and
|
||||
// walkthroughs. Plain text is fetched and shown in a <pre> (so reading
|
||||
// progress can track its scroll); HTML is shown in a sandboxed iframe (the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// VersionSwitcher — RBtn + RMenu pair that lets the user navigate
|
||||
// VersionSwitcher: RBtn + RMenu pair that lets the user navigate
|
||||
// between sibling ROMs of the same game (different region / revision /
|
||||
// language). Renders nothing when the rom has no siblings.
|
||||
//
|
||||
|
||||
@@ -8,7 +8,7 @@ export type Provider = {
|
||||
/** Field on DetailedRom that holds the provider's external ID. */
|
||||
key: keyof DetailedRom;
|
||||
name: string;
|
||||
/** CSS color value (token reference) — kept inline because tokens
|
||||
/** CSS color value (token reference): kept inline because tokens
|
||||
* resolve at the consumer's CSS context, not at this file. */
|
||||
color: string;
|
||||
/** Optional favicon for the provider's identity. */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// LiveSessionCard — a multiplayer streaming session another user is hosting
|
||||
// LiveSessionCard: a multiplayer streaming session another user is hosting
|
||||
// right now, shown on the Home row. Clicking asks to join and then opens
|
||||
// the stream as a viewer.
|
||||
import { RChip, RIcon } from "@v2/lib";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
// LibraryStatsWidget — counts overview of what's in RomM.
|
||||
// LibraryStatsWidget: counts overview of what's in RomM.
|
||||
//
|
||||
// Two display modes (driven by `libraryStatsMode` UI setting):
|
||||
// • compact — games / platforms / favorites (3 rows, mock parity)
|
||||
// • extended — adds saves / states / screenshots / disk size, the
|
||||
// • compact - games / platforms / favorites (3 rows, mock parity)
|
||||
// • extended: adds saves / states / screenshots / disk size, the
|
||||
// full v1 "Home/Stats" surface, condensed into the
|
||||
// widget vocabulary. The card grows wider (not taller)
|
||||
// and renders the rows in a 2-column grid so every
|
||||
@@ -143,7 +143,7 @@ onMounted(async () => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Extended mode — same card height as compact, but the card itself
|
||||
/* Extended mode: same card height as compact, but the card itself
|
||||
grows wider and the rows lay out in a 3-column grid so all 7 stats
|
||||
(3 base + 4 extended) fit in 3 rows inside the shared rail height.
|
||||
Columns size to their content (`auto`, paired with the card's
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user