@mundogamernetwork/shared-ui 1.1.95 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/playtest/PlaytestBrief.vue +152 -0
- package/components/playtest/PlaytestPendingConfirmation.vue +84 -0
- package/components/playtest/PlaytestResponseFlow.vue +371 -0
- package/components/playtest/StarRatingInput.vue +105 -0
- package/components/playtest/accessibility/CriterionRow.vue +167 -0
- package/components/playtest/baseline/BaselineSectionStep.vue +106 -0
- package/components/playtest/compliance/ChecklistItemRow.vue +200 -0
- package/components/playtest/concept-poll/EmojiReactionVote.vue +145 -0
- package/components/playtest/concept-poll/FiveSecondTestVote.vue +197 -0
- package/components/playtest/concept-poll/HeadToHeadVote.vue +98 -0
- package/components/playtest/concept-poll/MultiSelectVote.vue +104 -0
- package/components/playtest/concept-poll/RankedChoiceVote.vue +169 -0
- package/components/playtest/concept-poll/SingleSelectVote.vue +98 -0
- package/components/playtest/concept-poll/StarRatingVote.vue +110 -0
- package/components/playtest/concept-poll/VoteReasonField.vue +74 -0
- package/components/playtest/lqa/FileIssueForm.vue +181 -0
- package/components/playtest/lqa/IssueList.vue +99 -0
- package/components/playtest/lqa/LocaleSelector.vue +73 -0
- package/components/playtest/moderated/JoinCallPanel.vue +135 -0
- package/components/playtest/moderated/SessionCard.vue +146 -0
- package/composables/usePlaytestResponse.ts +446 -0
- package/locales/de.json +1 -0
- package/locales/en.json +1 -0
- package/locales/pt-BR.json +1 -0
- package/locales/ro.json +1 -0
- package/package.json +10 -3
- package/pages/key-campaigns/redeem-key.vue +36 -5
- package/services/campaignService.ts +10 -0
- package/services/playtestTesterService.ts +416 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Reusable 1–5 star rating input. No star-rating component existed anywhere
|
|
3
|
+
// in shared-ui before this (community-frontend currently copy-pastes inline
|
|
4
|
+
// star SVG per usage per this session's research) — built fresh here so
|
|
5
|
+
// every playtest flow (baseline "scale" sections, concept-poll star_rating
|
|
6
|
+
// votes) shares one implementation.
|
|
7
|
+
const props = withDefaults(defineProps<{
|
|
8
|
+
modelValue: number | null
|
|
9
|
+
max?: number
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
size?: number
|
|
12
|
+
}>(), {
|
|
13
|
+
max: 5,
|
|
14
|
+
disabled: false,
|
|
15
|
+
size: 22,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
(e: "update:modelValue", value: number): void
|
|
20
|
+
}>();
|
|
21
|
+
|
|
22
|
+
import { ref } from "vue";
|
|
23
|
+
|
|
24
|
+
const hovered = ref<number | null>(null);
|
|
25
|
+
|
|
26
|
+
function select(n: number) {
|
|
27
|
+
if (props.disabled) return;
|
|
28
|
+
emit("update:modelValue", n);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function starClass(n: number): string {
|
|
32
|
+
const active = hovered.value ?? props.modelValue ?? 0;
|
|
33
|
+
return n <= active ? "star-rating__star star-rating__star--active" : "star-rating__star";
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<div
|
|
39
|
+
class="star-rating"
|
|
40
|
+
:class="{ 'star-rating--disabled': disabled }"
|
|
41
|
+
role="radiogroup"
|
|
42
|
+
:aria-label="`Rating out of ${max}`"
|
|
43
|
+
>
|
|
44
|
+
<button
|
|
45
|
+
v-for="n in max"
|
|
46
|
+
:key="n"
|
|
47
|
+
type="button"
|
|
48
|
+
class="star-rating__btn"
|
|
49
|
+
:class="starClass(n)"
|
|
50
|
+
:disabled="disabled"
|
|
51
|
+
:aria-checked="modelValue === n"
|
|
52
|
+
role="radio"
|
|
53
|
+
@click="select(n)"
|
|
54
|
+
@mouseenter="hovered = n"
|
|
55
|
+
@mouseleave="hovered = null"
|
|
56
|
+
>
|
|
57
|
+
<svg
|
|
58
|
+
:width="size"
|
|
59
|
+
:height="size"
|
|
60
|
+
viewBox="0 0 24 24"
|
|
61
|
+
:fill="n <= (hovered ?? modelValue ?? 0) ? 'currentColor' : 'none'"
|
|
62
|
+
stroke="currentColor"
|
|
63
|
+
stroke-width="1.5"
|
|
64
|
+
>
|
|
65
|
+
<path
|
|
66
|
+
d="M12 2.5l2.9 6.06 6.6.77-4.85 4.6 1.24 6.57L12 17.3l-5.89 3.2 1.24-6.57-4.85-4.6 6.6-.77L12 2.5z"
|
|
67
|
+
stroke-linejoin="round"
|
|
68
|
+
/>
|
|
69
|
+
</svg>
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
</template>
|
|
73
|
+
|
|
74
|
+
<style scoped lang="scss">
|
|
75
|
+
.star-rating {
|
|
76
|
+
display: inline-flex;
|
|
77
|
+
gap: 4px;
|
|
78
|
+
|
|
79
|
+
&--disabled {
|
|
80
|
+
opacity: 0.6;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
&__btn {
|
|
84
|
+
background: none;
|
|
85
|
+
border: none;
|
|
86
|
+
padding: 2px;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
line-height: 0;
|
|
89
|
+
color: var(--secondary-info-fg, #666);
|
|
90
|
+
transition: color 0.12s, transform 0.12s;
|
|
91
|
+
|
|
92
|
+
&:hover:not(:disabled) {
|
|
93
|
+
transform: scale(1.08);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&:disabled {
|
|
97
|
+
cursor: not-allowed;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
&__star--active {
|
|
102
|
+
color: var(--primary, #D297FF);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
</style>
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// One accessibility criterion's evaluation row — result enum + conditional
|
|
3
|
+
// notes (required when result==='fail', per StoreAccessibilityEvaluationRequest.php)
|
|
4
|
+
// + tester_uses_accommodation checkbox (weights testers who actually rely on
|
|
5
|
+
// the accommodation). Server upserts per-criterion — safe to auto-save each
|
|
6
|
+
// row as the tester completes it, not just at the end.
|
|
7
|
+
import { ref, computed, watch } from "vue";
|
|
8
|
+
import type { AccessibilityEvaluationResult, PlaytestAccessibilityCriterionMeta } from "../../../services/playtestTesterService";
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
criterionKey: string;
|
|
12
|
+
meta: PlaytestAccessibilityCriterionMeta;
|
|
13
|
+
autoSave?: boolean;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits<{
|
|
17
|
+
(e: "save", payload: { criterion: string; result: AccessibilityEvaluationResult; notes?: string | null; tester_uses_accommodation?: boolean }): void
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
const RESULTS: AccessibilityEvaluationResult[] = ["pass", "fail", "partial", "not_applicable"];
|
|
21
|
+
|
|
22
|
+
const result = ref<AccessibilityEvaluationResult | "">("");
|
|
23
|
+
const notes = ref("");
|
|
24
|
+
const usesAccommodation = ref(false);
|
|
25
|
+
|
|
26
|
+
const notesRequired = computed(() => result.value === "fail");
|
|
27
|
+
const isComplete = computed(() => !!result.value && (!notesRequired.value || notes.value.trim().length > 0));
|
|
28
|
+
|
|
29
|
+
function save() {
|
|
30
|
+
if (!result.value) return;
|
|
31
|
+
if (notesRequired.value && !notes.value.trim()) return;
|
|
32
|
+
emit("save", {
|
|
33
|
+
criterion: props.criterionKey,
|
|
34
|
+
result: result.value,
|
|
35
|
+
notes: notes.value || null,
|
|
36
|
+
tester_uses_accommodation: usesAccommodation.value,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Auto-save whenever the row becomes complete (per-criterion upsert is safe
|
|
41
|
+
// server-side) — opt-out via autoSave=false if the orchestrator wants an
|
|
42
|
+
// explicit "save" button flow instead.
|
|
43
|
+
watch([result, notes, usesAccommodation], () => {
|
|
44
|
+
if (props.autoSave !== false && isComplete.value) save();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
defineExpose({ isComplete, save });
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<div class="criterion-row">
|
|
52
|
+
<div class="criterion-row__header">
|
|
53
|
+
<h4 class="criterion-row__label">{{ meta.label }}</h4>
|
|
54
|
+
<label class="criterion-row__accommodation">
|
|
55
|
+
<input type="checkbox" v-model="usesAccommodation">
|
|
56
|
+
{{ $t("playtest.accessibility.uses_accommodation", "I personally rely on this accommodation") }}
|
|
57
|
+
</label>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="criterion-row__results">
|
|
61
|
+
<label
|
|
62
|
+
v-for="r in RESULTS"
|
|
63
|
+
:key="r"
|
|
64
|
+
class="criterion-row__result"
|
|
65
|
+
:class="{ 'criterion-row__result--selected': result === r }"
|
|
66
|
+
>
|
|
67
|
+
<input type="radio" :value="r" v-model="result" :name="`criterion-${criterionKey}`">
|
|
68
|
+
{{ r.replace('_', ' ') }}
|
|
69
|
+
</label>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div v-if="notesRequired" class="criterion-row__notes">
|
|
73
|
+
<label class="criterion-row__notes-label">
|
|
74
|
+
{{ $t("playtest.accessibility.notes_required", "Notes (required when failing)") }}
|
|
75
|
+
</label>
|
|
76
|
+
<textarea v-model="notes" rows="2" class="criterion-row__textarea" />
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<style scoped lang="scss">
|
|
82
|
+
.criterion-row {
|
|
83
|
+
padding: 14px 0;
|
|
84
|
+
border-bottom: 1px solid var(--button-secondary-default-bg, #2a2a2a);
|
|
85
|
+
|
|
86
|
+
&:last-child {
|
|
87
|
+
border-bottom: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
&__header {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
justify-content: space-between;
|
|
94
|
+
flex-wrap: wrap;
|
|
95
|
+
gap: 8px;
|
|
96
|
+
margin-bottom: 10px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
&__label {
|
|
100
|
+
font-size: 14px;
|
|
101
|
+
font-weight: 700;
|
|
102
|
+
color: var(--card-article-title, #fff);
|
|
103
|
+
margin: 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
&__accommodation {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
gap: 6px;
|
|
110
|
+
font-size: 11px;
|
|
111
|
+
color: var(--secondary-info-fg, #aaa);
|
|
112
|
+
|
|
113
|
+
input {
|
|
114
|
+
accent-color: var(--primary, #D297FF);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
&__results {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-wrap: wrap;
|
|
121
|
+
gap: 8px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
&__result {
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
gap: 6px;
|
|
128
|
+
padding: 6px 12px;
|
|
129
|
+
background: var(--bg-app-badge, #1a1a1a);
|
|
130
|
+
border: 1px solid var(--button-secondary-default-bg, #2a2a2a);
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
font-size: 12px;
|
|
133
|
+
text-transform: capitalize;
|
|
134
|
+
color: var(--text-primary, #fff);
|
|
135
|
+
|
|
136
|
+
&--selected {
|
|
137
|
+
border-color: var(--primary, #D297FF);
|
|
138
|
+
background: rgba(210, 151, 255, 0.08);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
input {
|
|
142
|
+
accent-color: var(--primary, #D297FF);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
&__notes {
|
|
147
|
+
margin-top: 10px;
|
|
148
|
+
|
|
149
|
+
&-label {
|
|
150
|
+
display: block;
|
|
151
|
+
font-size: 11px;
|
|
152
|
+
color: var(--danger, #e53935);
|
|
153
|
+
margin-bottom: 6px;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
&__textarea {
|
|
158
|
+
width: 100%;
|
|
159
|
+
background: var(--bg-app-badge, #1a1a1a);
|
|
160
|
+
border: 1px solid var(--button-secondary-default-bg, #2a2a2a);
|
|
161
|
+
color: var(--text-primary, #fff);
|
|
162
|
+
padding: 8px;
|
|
163
|
+
font-size: 12px;
|
|
164
|
+
resize: vertical;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
</style>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// One baseline section's rating+text inputs, driven entirely by the
|
|
3
|
+
// section's catalog metadata (scale/text requirement) from
|
|
4
|
+
// GET /playtest/catalog — never hardcodes per-section behavior, since the
|
|
5
|
+
// catalog is the single source of truth (PlaytestFormSection::all() on the
|
|
6
|
+
// backend, mirrored here).
|
|
7
|
+
import { computed } from "vue";
|
|
8
|
+
import StarRatingInput from "../StarRatingInput.vue";
|
|
9
|
+
import type { PlaytestFormSectionMeta, PlaytestAnswerEntry } from "../../../services/playtestTesterService";
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
sectionKey: string;
|
|
13
|
+
meta: PlaytestFormSectionMeta;
|
|
14
|
+
modelValue: PlaytestAnswerEntry;
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
const emit = defineEmits<{
|
|
18
|
+
(e: "update:modelValue", value: PlaytestAnswerEntry): void
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
const rating = computed({
|
|
22
|
+
get: () => props.modelValue.rating ?? null,
|
|
23
|
+
set: (v: number | null) => emit("update:modelValue", { ...props.modelValue, rating: v ?? undefined }),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const text = computed({
|
|
27
|
+
get: () => props.modelValue.text ?? "",
|
|
28
|
+
set: (v: string) => emit("update:modelValue", { ...props.modelValue, text: v }),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const textRequired = computed(() => props.meta.text === "required");
|
|
32
|
+
const textOptional = computed(() => props.meta.text === "optional");
|
|
33
|
+
const showText = computed(() => props.meta.text !== "none");
|
|
34
|
+
|
|
35
|
+
const isValid = computed(() => {
|
|
36
|
+
if (props.meta.scale && !rating.value) return false;
|
|
37
|
+
if (textRequired.value && !text.value.trim()) return false;
|
|
38
|
+
return true;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
defineExpose({ isValid });
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<div class="baseline-step">
|
|
46
|
+
<h4 class="baseline-step__label">{{ meta.label }}</h4>
|
|
47
|
+
|
|
48
|
+
<div v-if="meta.scale" class="baseline-step__rating">
|
|
49
|
+
<StarRatingInput v-model="rating as any" />
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div v-if="showText" class="baseline-step__text">
|
|
53
|
+
<label :for="`baseline-text-${sectionKey}`" class="baseline-step__text-label">
|
|
54
|
+
{{ textRequired
|
|
55
|
+
? $t("playtest.baseline.feedback_required", "Your feedback (required)")
|
|
56
|
+
: $t("playtest.baseline.feedback_optional", "Your feedback (optional)") }}
|
|
57
|
+
</label>
|
|
58
|
+
<textarea
|
|
59
|
+
:id="`baseline-text-${sectionKey}`"
|
|
60
|
+
v-model="text"
|
|
61
|
+
class="baseline-step__textarea"
|
|
62
|
+
rows="4"
|
|
63
|
+
:required="textRequired"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<style scoped lang="scss">
|
|
70
|
+
.baseline-step {
|
|
71
|
+
padding: 16px 0;
|
|
72
|
+
border-bottom: 1px solid var(--button-secondary-default-bg, #2a2a2a);
|
|
73
|
+
|
|
74
|
+
&:last-child {
|
|
75
|
+
border-bottom: none;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
&__label {
|
|
79
|
+
font-size: 15px;
|
|
80
|
+
font-weight: 700;
|
|
81
|
+
color: var(--card-article-title, #fff);
|
|
82
|
+
margin: 0 0 10px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
&__rating {
|
|
86
|
+
margin-bottom: 12px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
&__text-label {
|
|
90
|
+
display: block;
|
|
91
|
+
font-size: 12px;
|
|
92
|
+
color: var(--secondary-info-fg, #aaa);
|
|
93
|
+
margin-bottom: 6px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&__textarea {
|
|
97
|
+
width: 100%;
|
|
98
|
+
background: var(--bg-app-badge, #1a1a1a);
|
|
99
|
+
border: 1px solid var(--button-secondary-default-bg, #2a2a2a);
|
|
100
|
+
color: var(--text-primary, #fff);
|
|
101
|
+
padding: 10px;
|
|
102
|
+
font-size: 13px;
|
|
103
|
+
resize: vertical;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// One compliance checklist item's result row — status enum (only 3: no
|
|
3
|
+
// 'partial', unlike accessibility) + conditional notes (required when
|
|
4
|
+
// status==='fail', per SubmitComplianceResultRequest.php) + optional
|
|
5
|
+
// evidence_url (populated via POST /playtest/media beforehand). IMPORTANT:
|
|
6
|
+
// unlike accessibility's per-criterion upsert, the parent submit endpoint
|
|
7
|
+
// REPLACES the whole results array for the checklist on every call — this
|
|
8
|
+
// row only emits its own entry; the orchestrator/composable is responsible
|
|
9
|
+
// for merging it into the full accumulated set before calling submit.
|
|
10
|
+
import { ref, computed, watch } from "vue";
|
|
11
|
+
import type { ComplianceResultStatus, PlaytestComplianceChecklistItem } from "../../../services/playtestTesterService";
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
item: PlaytestComplianceChecklistItem;
|
|
15
|
+
uploading?: boolean;
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
(e: "change", payload: { key: string; status: ComplianceResultStatus; notes?: string | null; evidence_url?: string | null }): void
|
|
20
|
+
(e: "upload-evidence", file: File): void
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
const STATUSES: ComplianceResultStatus[] = ["pass", "fail", "not_applicable"];
|
|
24
|
+
|
|
25
|
+
const status = ref<ComplianceResultStatus | "">("");
|
|
26
|
+
const notes = ref("");
|
|
27
|
+
const evidenceUrl = ref<string | null>(null);
|
|
28
|
+
|
|
29
|
+
const notesRequired = computed(() => status.value === "fail");
|
|
30
|
+
const isComplete = computed(() => !!status.value && (!notesRequired.value || notes.value.trim().length > 0));
|
|
31
|
+
|
|
32
|
+
function emitChange() {
|
|
33
|
+
if (!status.value) return;
|
|
34
|
+
if (notesRequired.value && !notes.value.trim()) return;
|
|
35
|
+
emit("change", {
|
|
36
|
+
key: props.item.key,
|
|
37
|
+
status: status.value,
|
|
38
|
+
notes: notes.value || null,
|
|
39
|
+
evidence_url: evidenceUrl.value,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
watch([status, notes, evidenceUrl], emitChange);
|
|
44
|
+
|
|
45
|
+
function onFileChange(e: Event) {
|
|
46
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
47
|
+
if (file) emit("upload-evidence", file);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function setEvidenceUrl(url: string | null) {
|
|
51
|
+
evidenceUrl.value = url;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
defineExpose({ isComplete, setEvidenceUrl });
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<div class="checklist-item-row">
|
|
59
|
+
<div class="checklist-item-row__header">
|
|
60
|
+
<h4 class="checklist-item-row__label">{{ item.label }}</h4>
|
|
61
|
+
<span v-if="item.severity" class="checklist-item-row__severity">{{ item.severity }}</span>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<p v-if="item.description" class="checklist-item-row__description">{{ item.description }}</p>
|
|
65
|
+
|
|
66
|
+
<div class="checklist-item-row__statuses">
|
|
67
|
+
<label
|
|
68
|
+
v-for="s in STATUSES"
|
|
69
|
+
:key="s"
|
|
70
|
+
class="checklist-item-row__status"
|
|
71
|
+
:class="{ 'checklist-item-row__status--selected': status === s }"
|
|
72
|
+
>
|
|
73
|
+
<input type="radio" :value="s" v-model="status" :name="`compliance-${item.key}`">
|
|
74
|
+
{{ s.replace('_', ' ') }}
|
|
75
|
+
</label>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div v-if="notesRequired" class="checklist-item-row__notes">
|
|
79
|
+
<label class="checklist-item-row__notes-label">
|
|
80
|
+
{{ $t("playtest.compliance.notes_required", "Notes (required when failing)") }}
|
|
81
|
+
</label>
|
|
82
|
+
<textarea v-model="notes" rows="2" class="checklist-item-row__textarea" />
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div v-if="item.evidence_required || status === 'fail'" class="checklist-item-row__evidence">
|
|
86
|
+
<label class="checklist-item-row__evidence-label">
|
|
87
|
+
{{ $t("playtest.compliance.attach_evidence", "Attach evidence (screenshot/video)") }}
|
|
88
|
+
</label>
|
|
89
|
+
<input type="file" accept="image/*,video/mp4,video/quicktime,video/webm" @change="onFileChange">
|
|
90
|
+
<span v-if="uploading" class="checklist-item-row__uploading">{{ $t("playtest.compliance.uploading", "Uploading…") }}</span>
|
|
91
|
+
<span v-else-if="evidenceUrl" class="checklist-item-row__uploaded">{{ $t("playtest.compliance.uploaded", "Attached") }}</span>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<style scoped lang="scss">
|
|
97
|
+
.checklist-item-row {
|
|
98
|
+
padding: 14px 0;
|
|
99
|
+
border-bottom: 1px solid var(--button-secondary-default-bg, #2a2a2a);
|
|
100
|
+
|
|
101
|
+
&:last-child {
|
|
102
|
+
border-bottom: none;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&__header {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: space-between;
|
|
109
|
+
margin-bottom: 6px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
&__label {
|
|
113
|
+
font-size: 14px;
|
|
114
|
+
font-weight: 700;
|
|
115
|
+
color: var(--card-article-title, #fff);
|
|
116
|
+
margin: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
&__severity {
|
|
120
|
+
font-size: 10px;
|
|
121
|
+
font-weight: 700;
|
|
122
|
+
text-transform: uppercase;
|
|
123
|
+
color: var(--secondary-info-fg, #aaa);
|
|
124
|
+
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.2));
|
|
125
|
+
padding: 2px 8px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
&__description {
|
|
129
|
+
font-size: 12px;
|
|
130
|
+
color: var(--secondary-info-fg, #aaa);
|
|
131
|
+
margin: 0 0 10px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
&__statuses {
|
|
135
|
+
display: flex;
|
|
136
|
+
gap: 8px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
&__status {
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: 6px;
|
|
143
|
+
padding: 6px 12px;
|
|
144
|
+
background: var(--bg-app-badge, #1a1a1a);
|
|
145
|
+
border: 1px solid var(--button-secondary-default-bg, #2a2a2a);
|
|
146
|
+
cursor: pointer;
|
|
147
|
+
font-size: 12px;
|
|
148
|
+
text-transform: capitalize;
|
|
149
|
+
color: var(--text-primary, #fff);
|
|
150
|
+
|
|
151
|
+
&--selected {
|
|
152
|
+
border-color: var(--primary, #D297FF);
|
|
153
|
+
background: rgba(210, 151, 255, 0.08);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
input {
|
|
157
|
+
accent-color: var(--primary, #D297FF);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
&__notes,
|
|
162
|
+
&__evidence {
|
|
163
|
+
margin-top: 10px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
&__notes-label,
|
|
167
|
+
&__evidence-label {
|
|
168
|
+
display: block;
|
|
169
|
+
font-size: 11px;
|
|
170
|
+
color: var(--secondary-info-fg, #aaa);
|
|
171
|
+
margin-bottom: 6px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
&__notes-label {
|
|
175
|
+
color: var(--danger, #e53935);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
&__textarea {
|
|
179
|
+
width: 100%;
|
|
180
|
+
background: var(--bg-app-badge, #1a1a1a);
|
|
181
|
+
border: 1px solid var(--button-secondary-default-bg, #2a2a2a);
|
|
182
|
+
color: var(--text-primary, #fff);
|
|
183
|
+
padding: 8px;
|
|
184
|
+
font-size: 12px;
|
|
185
|
+
resize: vertical;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
&__uploading,
|
|
189
|
+
&__uploaded {
|
|
190
|
+
display: inline-block;
|
|
191
|
+
margin-left: 8px;
|
|
192
|
+
font-size: 11px;
|
|
193
|
+
color: var(--secondary-info-fg, #888);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
&__uploaded {
|
|
197
|
+
color: var(--primary, #D297FF);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
</style>
|