Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f56477c
WIP: Start working on rating component.
Carifio24 Sep 16, 2025
3226434
Use textarea in rating component and export from package. Add notific…
Carifio24 Sep 19, 2025
665bd75
Add storybook story.
Carifio24 Sep 19, 2025
15b9753
More work on updating component and story.
Carifio24 Sep 20, 2025
4bad294
WIP: Add 'attention hook' component.
Carifio24 Sep 24, 2025
50a32ca
Work out details of attention hook animation.
Carifio24 Sep 25, 2025
7a37dcd
Create story for using attention hook with user experience. Set conte…
Carifio24 Sep 25, 2025
df7d9d0
Make rating item into a slot; keep current content as default.
Carifio24 Sep 25, 2025
fde499b
Make component more flexible and update story.
Carifio24 Sep 25, 2025
887b7c9
Allow specifying bounce count.
Carifio24 Sep 25, 2025
60190a0
Bounce up rather than down.
Carifio24 Sep 25, 2025
f0a98c7
Add type annotations to fix build.
Carifio24 Sep 25, 2025
9b36d04
Allow changing default icon size and add docstring comments.
Carifio24 Sep 25, 2025
36efd3c
Add empty event to user experience component.
Carifio24 Sep 25, 2025
5a9406e
WIP: Start working on making user experience component two-part.
Carifio24 Sep 26, 2025
877feac
Transition in the text area.
Carifio24 Sep 27, 2025
f650183
Updates to stories.
Carifio24 Sep 29, 2025
9f43a54
Remove debugging log statements.
Carifio24 Sep 29, 2025
3e11b31
Fix linting issue.
Carifio24 Sep 29, 2025
0fcaeae
Remove medium rating. Add a footer slot.
Carifio24 Sep 29, 2025
b312931
Add default color to user experience. Fix issue with building component.
Carifio24 Sep 29, 2025
3669927
WIP: Work on hiding footer if empty.
Carifio24 Sep 29, 2025
1ddbb5f
Hide footer in a build-compatible way.
Carifio24 Sep 29, 2025
f27175d
Updates to user experience component layout.
Carifio24 Sep 30, 2025
5b3022a
Merge branch 'main' into rating-component
Carifio24 Oct 1, 2025
d7437ab
Add imports to user experience component.
Carifio24 Oct 1, 2025
bdbc35f
Remove notification.
Carifio24 Oct 1, 2025
9cd0000
Styling updates.
Carifio24 Oct 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { wwtPinia } from "@wwtelescope/engine-pinia";
import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';
import Notifications from "@kyvg/vue3-notification";

const vuetify = createVuetify({
components,
Expand All @@ -13,6 +14,7 @@ const vuetify = createVuetify({
setup((app) => {
app.use(wwtPinia);
app.use(vuetify);
app.use(Notifications);
});

const preview: Preview = {
Expand Down
110 changes: 110 additions & 0 deletions src/components/AttentionHook.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<template>
<div
class="attention-hook"
v-show="visible"
ref="hook"
>
<slot>
<font-awesome-icon
icon="chevron-up"
class="attention-hook-icon"
@click="emit('open')"
>
</font-awesome-icon>
</slot>
</div>
</template>

<script setup lang="ts">
import { watch, onMounted, useTemplateRef } from "vue";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faChevronUp } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";

import { createBounceAnimation } from "../utils";

library.add(faChevronUp);

interface Props {
visible?: boolean;
bounceAmount?: string;
bounceDuration?: number;
betweenBouncesDuration?: number;
bounceCount?: number;
popupTime?: number;
}

const props = withDefaults(defineProps<Props>(), {
visible: true,
bounceAmount: "10%",
bounceDuration: 500,
betweenBouncesDuration: 1000,
bounceCount: Infinity,
popupTime: 500,
});

const root = useTemplateRef<HTMLElement>("hook");

onMounted(() => {
setupAnimation();
});

function setupAnimation() {
const element = root.value;
if (!element) { return; }
const popupAnimation = element.animate([
// Remember: +Y is down, so we're moving up
{ transform: "translateY(100%)", offset: 0 },
{ transform: "translateY(0)", offset: 1 },
],
{ duration: props.popupTime });
popupAnimation.finished.then(() => {
const bounceAnimation = createBounceAnimation(element, props);
bounceAnimation.play();
});
popupAnimation.play();
}

watch(() => props.visible, (visible: boolean) => {
if (visible) {
setupAnimation();
}
});

const emit = defineEmits<{
(event: "open"): void;
}>();
</script>

<style lang="less">
.attention-hook {
position: absolute;
bottom: 0;
right: 10px;
width: 40px;
height: 40px;
background: #808080;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
overscroll-behavior-y: contain;

&:hover {
cursor: pointer;
}

& .attention-hook-icon {
color: white;
}
}

@keyframes pop-up {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
</style>
221 changes: 221 additions & 0 deletions src/components/UserExperience.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<template>
<v-card
class="rating-root"
:color="color"
:style="css"
>
<template #title>
<div class="rating-title">
<span>{{ question }}</span>
<v-spacer></v-spacer>
<v-btn
density="compact"
class="close-button"
icon="mdi-close"
@click="emit('dismiss', currentRating, comments)"
></v-btn>
</div>
</template>
<v-card-text>
<v-form
@submit.prevent="emit('finish', currentRating, comments)"
>
<div class="rating-icon-row">
<template
v-for="rating in ratings"
>
<slot
:rating="rating"
>
<v-hover
:key="rating"
>
<template #default="{ isHovering, props }: { isHovering: boolean | null, props: Record<string, unknown> }">
<FontAwesomeIcon
v-bind="props"
:size="iconSize"
:class="['rating', rating, {'hovered': isHovering}, {'selected': rating === currentRating}]"
:icon="ratingIcons[rating as UserExperienceRating][0]"
:color="(isHovering || rating === currentRating) ? ratingIcons[rating as UserExperienceRating][1]: baseColor"
@click="currentRating = rating as UserExperienceRating"
>
</FontAwesomeIcon>
</template>
</v-hover>
</slot>
</template>
</div>
<v-expand-transition>
<VTextarea
v-if="showComments"
v-model="comments"
class="comments-box text-body-2"
:placeholder="commentPlaceholder"
auto-grow
max-rows="4"
density="compact"
width="100%"
@keydown.stop
>
</VTextarea>
</v-expand-transition>
<v-expand-transition>
<v-btn
v-if="currentRating || showComments"
type="submit"
width="fit-content"
color="success"
>
Submit
</v-btn>
</v-expand-transition>
</v-form>
</v-card-text>
<template #actions>
<slot name="footer"></slot>
</template>
</v-card>
</template>

<script setup lang="ts">
/* eslint-disable @typescript-eslint/naming-convention */
import { computed, ref, watch, useSlots } from "vue";
import { useTheme } from "vuetify";
import { DEFAULT_RATING_COLORS, type UserExperienceRating } from "../utils";
import { UserExperienceProps } from "../types";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import {
faFaceGrinStars,
faFaceSmile,
faFaceMeh,
faFaceFrownOpen,
faFaceFrown,
} from "@fortawesome/free-solid-svg-icons";

import { VTextarea, VExpandTransition, VCard, VForm, VSpacer, VCardText, VBtn, VHover } from "vuetify/components";

const { current: currentTheme } = useTheme();

const props = withDefaults(defineProps<UserExperienceProps>(), {
ratingColors: () => DEFAULT_RATING_COLORS,
question: "How would you rate your experience?",
commentPlaceholder: "Please tell us more if you like",
askForComments: true,
iconSize: "5x",
color: "surface",
});

const emit = defineEmits<{
(event: "rating", rating: UserExperienceRating | null): void;
(event: "finish", rating: UserExperienceRating | null, comments: string | null): void;
(event: "dismiss", rating: UserExperienceRating | null, comments: string | null): void;
}>();

const slots = useSlots();
const showFooter = computed(() => !!slots.footer);

const css = computed(() => ({
"--footer-visible": showFooter.value ? "visible" : "none",
}));

library.add(faFaceGrinStars);
library.add(faFaceSmile);
library.add(faFaceMeh);
library.add(faFaceFrownOpen);
library.add(faFaceFrown);


const ratingIcons: Record<UserExperienceRating, [string, string]> = {
// eslint-disable-next-line @typescript-eslint/naming-convention
"very_bad": ["fa-face-frown", "red"],
"poor": ["fa-face-frown-open", "orange"],
"good": ["fa-face-smile", "lightgreen"],
"excellent": ["fa-face-grin-stars", "green"],
};

const ratings = Object.keys(ratingIcons) as UserExperienceRating[];

const currentRating = ref<UserExperienceRating | null>(null);
const baseColor = computed(() => props.baseColor ?? (currentTheme.value.dark ? 'white' : 'black'));
const comments = ref<string | null>(null);
const showComments = ref(false);

watch(currentRating, (rating: UserExperienceRating | null) => {
if (rating) {
if (props.askForComments) {
showComments.value = true;
}
emit("rating", rating);
}
});
</script>

<style lang="less">
.rating-root {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
padding: 5px;
}

.rating-icon-row {
display: flex;
flex-direction: row;
gap: 10px;
padding: 20px;
justify-content: center;
}

.rating {
transition: color 0.1s;
}

.selected {
border-radius: 50%;
box-shadow: 0 0 0 5px silver;
}

.rating-notification {
border-radius: 5px;
font-size: calc(1.1 * var(--default-font-size));
padding: 1em;
color: white;

&.success {
background-color: #9a009a;
}
&.error {
background-color: #b30000;
}
}

.comments-box {
width: 75%;
}

.v-card-actions {
display: var(--footer-visible);
}

.close-button {
display: inline;
}

.rating-title {
width: 100%;
text-align: center;
}

.close-button {
position: absolute !important;
top: 5px;
right: 5px;
}

.rating-title {
padding: 5px 10px;
}
</style>

4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { usePlaybackControl } from "./composables/playbackControl";
import { useWindowShape, WindowShape } from "./composables/windowShape";
import { useWWTKeyboardControls } from "./composables/wwtKeyboard";

import AttentionHook from "./components/AttentionHook.vue";
import CreditLogos from "./components/CreditLogos.vue";
import DateTimePicker from "./components/DateTimePicker.vue";
import FolderView from "./components/FolderView.vue";
Expand All @@ -20,6 +21,7 @@ import PlaybackControl from "./components/PlaybackControl.vue";
import ShareButton from "./components/ShareButton.vue";
import SpeedControl from "./components/SpeedControl.vue";
import TapToInput from "./components/TapToInput.vue";
import UserExperience from "./components/UserExperience.vue";
import WwtHud from "./components/WwtHud.vue";

export {
Expand All @@ -37,6 +39,7 @@ export {
useWindowShape,
useWWTKeyboardControls,

AttentionHook,
CreditLogos,
DateTimePicker,
FolderView,
Expand All @@ -50,6 +53,7 @@ export {
ShareButton,
SpeedControl,
TapToInput,
UserExperience,
WwtHud,
};

Expand Down
Loading