-
-
Notifications
You must be signed in to change notification settings - Fork 601
Expand file tree
/
Copy pathGameActions.vue
More file actions
168 lines (159 loc) · 5.11 KB
/
Copy pathGameActions.vue
File metadata and controls
168 lines (159 loc) · 5.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
<script setup lang="ts">
// 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
// original white pill CTA; every other button is a circular glass icon
// button. The `more` action opens the shared GameActionsList.
//
// 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,
// 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.
import { computed, ref, toRef } from "vue";
import { useI18n } from "vue-i18n";
import type { SimpleRom } from "@/stores/roms";
import GameActionBtn from "@/v2/components/GameActions/GameActionBtn.vue";
import MetricMenuBtn from "@/v2/components/GameActions/MetricMenuBtn.vue";
import { METRICS } from "@/v2/components/GameActions/metrics";
import { useBreakpoint } from "@/v2/composables/useBreakpoint";
import { useGameActions } from "@/v2/composables/useGameActions";
import { useGridNav } from "@/v2/composables/useGridNav";
defineOptions({ inheritAttrs: false });
const props = defineProps<{
rom: SimpleRom;
}>();
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
// column; the default (36px) size fits more per row and reads cleaner.
const { smAndDown } = useBreakpoint();
const btnSize = computed<"default" | "large">(() =>
smAndDown.value ? "default" : "large",
);
// Single-row gamepad/keyboard nav across the action ribbon. The root is
// 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
// `canPlay`), otherwise Download.
const rootEl = ref<HTMLElement | null>(null);
useGridNav(rootEl, {
getRows: () => (rootEl.value ? [rootEl.value] : []),
getCells: (row) =>
Array.from(
row.querySelectorAll<HTMLElement>(".r-v2-game-btn, .r-v2-metric-btn"),
),
});
</script>
<template>
<div ref="rootEl" class="game-actions">
<GameActionBtn
v-if="actions.canPlay.value"
:rom="rom"
action="play"
:size="btnSize"
variant="emphasized"
with-label
/>
<div v-if="actions.canPlay.value" class="game-actions__break" />
<GameActionBtn
:rom="rom"
action="download"
:size="btnSize"
variant="surface"
/>
<GameActionBtn
:rom="rom"
action="copy-link"
:size="btnSize"
variant="surface"
/>
<GameActionBtn
v-if="actions.canShareQR.value"
:rom="rom"
action="qr"
:size="btnSize"
variant="surface"
/>
<GameActionBtn
v-if="actions.canOpenInFlashpoint.value"
:rom="rom"
action="flashpoint"
:size="btnSize"
variant="surface"
with-label
/>
<GameActionBtn
:rom="rom"
action="favorite"
:size="btnSize"
variant="surface"
/>
<GameActionBtn
:rom="rom"
action="collection"
:size="btnSize"
variant="surface"
/>
<GameActionBtn
:rom="rom"
action="status"
:size="btnSize"
variant="surface"
with-metrics
/>
<GameActionBtn :rom="rom" action="more" :size="btnSize" variant="surface" />
<!-- Desktop only: the metric pills sit in the ribbon. On phones they
move into the status sheet (see the status button's `withMetrics`). -->
<template v-if="rom.rom_user && !smAndDown">
<div class="game-actions__spacer" />
<MetricMenuBtn
v-for="m in METRICS"
:key="m.field"
:kind="m.kind"
:label="t(m.labelKey)"
:icon-full="m.iconFull"
:icon-empty="m.iconEmpty"
:accent="m.accent"
:step="m.step"
:size="btnSize"
:value="rom.rom_user?.[m.field] ?? 0"
@update:value="(v) => actions.setScore(m.field, v)"
/>
</template>
</div>
</template>
<style scoped>
.game-actions {
display: flex;
align-items: center;
gap: 10px;
margin: 6px 0 4px;
flex-wrap: wrap;
}
.game-actions__spacer {
flex: 1;
min-width: 16px;
}
/* Mobile: centre the ribbon. The metrics move into the status sheet on
phones (they aren't rendered here), so no spacer/hairline is needed. */
html[data-bp~="sm-and-down"] .game-actions {
justify-content: center;
gap: 8px;
}
/* Full-width break after the Play CTA so it keeps its natural width but
sits alone (centred) on its own row above the icon ribbon on phones.
Collapsed on wider viewports so it has no effect there. */
.game-actions__break {
display: none;
}
html[data-bp~="sm-and-down"] .game-actions__break {
display: block;
flex: 0 0 100%;
height: 0;
}
</style>