The stars ask. One rating, one optional comment.
Four stars picked; the fifth sits dimmed. Submit sends the rating alone. Add a comment opens card B in place, never a second modal.
PopModal · ui/components/pop/PopModal.vue — standard chassis, standard dismissalTemplateModalMessage · ui/components/pop/templates/ModalMessage.vue — the same message template the beta survey used; footer slot carries the buttonsPrimaryButton · ui/buttons/PrimaryButton.vue — gray + green pair, size="md", renders f:body/m-heavyuseFeedbackStore · ui/store/feedback.js — the modal.show visibility state carries over; submit() is reworked to take { rating, comment } and the question flow retiresStarRating · catalog — the system's green stars, already in progress with the dev; v-model bound to Number.fb-modal column · title, stars, footer on one axis — gap owns every space (R10, R11)<script setup>
import { ref, computed } from 'vue'
import PopModal from '@/components/pop/PopModal.vue'
import TemplateModalMessage from '@/components/pop/templates/ModalMessage.vue'
import PrimaryButton from '@/buttons/PrimaryButton.vue'
import StarRating from '@/components/StarRating.vue' // new — five JoyPixels stars
import ComboField from '@/input/ComboField.vue'
import FieldRegion from '@/input/FieldRegion.vue'
import { useFeedbackStore } from '@/store/feedback'
const props = defineProps({ popped: Boolean })
const feedback = useFeedbackStore()
const rating = ref(null)
const comment = ref('')
const commenting = ref(false)
const submitted = ref(false)
const canSubmit = computed(() => rating.value != null)
const send = () => {
feedback.submit({ rating: rating.value, comment: comment.value.trim() })
submitted.value = true
}
</script>
<template>
<PopModal :popped="props.popped" width="420px" @unpop="feedback.hideModal()">
<TemplateModalMessage v-if="!submitted" emoji="">
<div class="__title">How are you liking EmojiCopy?</div>
<StarRating v-model="rating" />
<FieldRegion v-if="commenting" style="text-align: left">
<ComboField type="textarea" placeholder="What's working? What's missing?"
v-model="comment" />
</FieldRegion>
<template #footer>
<PrimaryButton v-if="!commenting" variant="gray" size="md"
label="Add a comment" @click="commenting = true" />
<PrimaryButton variant="green" size="md"
:label="commenting ? 'Submit feedback' : 'Submit'"
:disabled="!canSubmit" @click="send" />
</template>
</TemplateModalMessage>
<TemplateModalMessage v-else emoji="🙌">
<div class="__title">Thanks for the feedback.</div>
<PrimaryButton variant="green" size="md" label="Done" @click="feedback.hideModal()" />
</TemplateModalMessage>
</PopModal>
</template>
<!-- App.vue:132 already mounts on this state — the relaunch keeps it --> <FeedbackModal :popped="feedback.modal.show" /> // visibility state is feedback.modal.show — showModal()/hideModal() // are the store ACTIONS that flip it; never guard on the action. // openers: the bottom-left feedback prompt // (ui/components/Upsell/FeedbackPrompt.vue) and the Help menu's // "Give feedback" row. Never self-opens on a timer.
/* theme-aware app surface — feedback is not a sales modal, so no
fixed-dark shell and no upsell background class */
.fb-modal {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp-5);
padding: 40px 28px 28px;
text-align: center;
}
/* 🟡 StarRating — the system component; CSS verbatim from the
catalog (components.html#star-rating), Vue file there too. */
.star-rating {
display: inline-flex;
gap: 4px;
padding: 4px 0;
justify-content: center;
width: 100%;
}
.star-rating__star {
appearance: none;
border: none;
background: none;
padding: 4px 2px;
font-size: 3.2rem;
line-height: 1;
color: color-mix(in srgb, currentColor 22%, transparent);
cursor: pointer;
transition: color .12s, transform .08s;
&::before { content: '★'; }
&:hover,
&:focus-visible {
outline: none;
transform: scale(1.08);
color: color-mix(in srgb, var(--green-500) 60%, transparent);
}
&.is-active { color: var(--green-500); }
}
/* data-hover="N" highlights every star <= N — see the catalog */
The textarea rides ComboField's textarea type inside FieldRegion — the app's own input, not a new atom. The comment stays optional even here.
FeedbackModal as card A — commenting = true is the whole differenceComboField · ui/input/ComboField.vue — type="textarea", the beta survey's own field, reused as-isFieldRegion · ui/input/FieldRegion.vue — left-aligns the field inside the centered column.fb-modal column · the field joins the same axis between stars and footer — gap absorbs it, nothing else moves (R11)// card A's "Add a comment" button:
<PrimaryButton v-if="!commenting" variant="gray" size="md"
label="Add a comment" @click="commenting = true" />
// commenting swaps the gray button out and the field in; the green
// label becomes "Submit feedback". No second modal, no route.
.fb-modal {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp-5);
padding: 40px 28px 28px;
text-align: center;
}
/* DM Sans display title, sized for the 420px card */
.fb-modal .__title {
font-family: var(--ff-dm-sans);
font-weight: 700;
font-size: 2rem;
line-height: 1.25;
color: var(--c-text-primary);
max-width: 20ch;
}
/* StarRating = the system component — CSS verbatim in card A's
drawer and the catalog (components.html#star-rating) */
.star-rating { display: inline-flex; gap: 4px; padding: 4px 0; justify-content: center; width: 100%; }
.star-rating__star { font-size: 3.2rem; color: color-mix(in srgb, currentColor 22%, transparent); }
.star-rating__star.is-active { color: var(--green-500); }
/* the field is ComboField's own textarea rendering — no new CSS;
FieldRegion supplies the left alignment */
One line, one exit. No credit, no follow-up ask.
FeedbackModal as card A — the submitted branch of the same componentTemplateModalMessage · ui/components/pop/templates/ModalMessage.vue — emoji="🙌" renders the art slot1f64c<TemplateModalMessage v-else emoji="🙌">
<Text type="body/xxl" color="text-primary" text="Thanks for the feedback." />
<PrimaryButton variant="green" size="md" label="Done" @click="feedback.hideModal()" />
</TemplateModalMessage>
.fb-modal {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp-5);
padding: 40px 28px 28px;
text-align: center;
}
/* DM Sans display title, sized for the 420px card */
.fb-modal .__title {
font-family: var(--ff-dm-sans);
font-weight: 700;
font-size: 2rem;
line-height: 1.25;
color: var(--c-text-primary);
max-width: 20ch;
}
.fb-modal .__thanks-art { width: 56px; height: 56px; }