diff --git a/app/components/Common/Buttons.vue b/app/components/Common/Buttons.vue
new file mode 100644
index 0000000..abf7c7d
--- /dev/null
+++ b/app/components/Common/Buttons.vue
@@ -0,0 +1,71 @@
+
diff --git a/app/components/Common/ConfirmationModal.vue b/app/components/Common/ConfirmationModal.vue
new file mode 100644
index 0000000..c57b2a9
--- /dev/null
+++ b/app/components/Common/ConfirmationModal.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
diff --git a/app/composables/useButtonNavigation.ts b/app/composables/useButtonNavigation.ts
index ddd85aa..4413e5e 100644
--- a/app/composables/useButtonNavigation.ts
+++ b/app/composables/useButtonNavigation.ts
@@ -20,12 +20,16 @@ export const useButtonNavigation = >({
}
>;
}) => {
+ const { state: modalState } = useConfirmationModal();
+
const selectedButton = ref(initialButton);
const selectorPosition = computed(() => buttons[selectedButton.value]!);
const nextButton = ref();
useScreenClick((x: number, y: number) => {
+ if (modalState.value.isOpen) return;
+
for (const [buttonName, config] of Object.entries(buttons) as [
keyof T,
ButtonConfig,
@@ -52,6 +56,8 @@ export const useButtonNavigation = >({
});
useKeyDown((key) => {
+ if (modalState.value.isOpen) return;
+
const currentButton = selectedButton.value as keyof T;
const currentNav = navigation[currentButton];
diff --git a/app/composables/useConfirmationModal.ts b/app/composables/useConfirmationModal.ts
new file mode 100644
index 0000000..32adb3e
--- /dev/null
+++ b/app/composables/useConfirmationModal.ts
@@ -0,0 +1,93 @@
+import gsap from "gsap";
+
+const MODAL_MAX_Y_OFFSET = 106;
+const BUTTONS_MAX_Y_OFFSET = 20;
+const BUTTONS_TIME = 0.1;
+const MODAL_TIME = 0.225;
+
+const state = useState("confirmationModal", () =>
+ reactive({
+ isOpen: false,
+ text: "",
+ onConfirm: null as (() => void) | null,
+ offsetY: MODAL_MAX_Y_OFFSET,
+ buttonsYOffset: 0,
+ modalButtonsYOffset: BUTTONS_MAX_Y_OFFSET,
+ isVisible: false,
+ isClosing: false,
+ }),
+);
+
+const open = (text: string, onConfirm?: (() => void) | null) => {
+ gsap.killTweensOf(state.value);
+ state.value.text = text;
+ state.value.onConfirm = onConfirm || null;
+ state.value.isVisible = true;
+ state.value.isClosing = false;
+ state.value.isOpen = true;
+
+ gsap
+ .timeline()
+ // standard buttons down
+ .fromTo(
+ state.value,
+ { buttonsYOffset: 0 },
+ {
+ buttonsYOffset: BUTTONS_MAX_Y_OFFSET,
+ duration: BUTTONS_TIME,
+ ease: "none",
+ },
+ )
+ // modal up
+ .fromTo(
+ state.value,
+ { offsetY: MODAL_MAX_Y_OFFSET },
+ { offsetY: 0, duration: MODAL_TIME, ease: "none" },
+ )
+ // modal buttons up
+ .fromTo(
+ state.value,
+ { modalButtonsYOffset: BUTTONS_MAX_Y_OFFSET },
+ { modalButtonsYOffset: 0, duration: BUTTONS_TIME, ease: "none" },
+ );
+};
+
+const close = () => {
+ if (!state.value.isVisible || state.value.isClosing) return;
+
+ state.value.isClosing = true;
+
+ gsap
+ .timeline()
+ // modal buttons down
+ .to(state.value, {
+ modalButtonsYOffset: BUTTONS_MAX_Y_OFFSET,
+ duration: BUTTONS_TIME,
+ ease: "none",
+ })
+ // modal down
+ .to(state.value, {
+ offsetY: MODAL_MAX_Y_OFFSET,
+ duration: MODAL_TIME,
+ ease: "none",
+ })
+ // standard buttons up
+ .to(state.value, {
+ buttonsYOffset: 0,
+ duration: BUTTONS_TIME,
+ ease: "none",
+ })
+ .call(() => {
+ state.value.isVisible = false;
+ state.value.isClosing = false;
+ state.value.isOpen = false;
+ state.value.text = "";
+ state.value.onConfirm = null;
+ });
+};
+
+export const useConfirmationModal = () => ({
+ open,
+ close,
+ state: readonly(state),
+});
diff --git a/app/utils/async.ts b/app/utils/async.ts
new file mode 100644
index 0000000..7ce4b40
--- /dev/null
+++ b/app/utils/async.ts
@@ -0,0 +1,2 @@
+export const sleep = (ms: number) =>
+ new Promise((resolve) => setTimeout(resolve, ms));
diff --git a/public/images/common/A.webp b/public/images/common/A.webp
new file mode 100644
index 0000000..2b1889c
Binary files /dev/null and b/public/images/common/A.webp differ
diff --git a/public/images/common/B.webp b/public/images/common/B.webp
new file mode 100644
index 0000000..e09d99f
Binary files /dev/null and b/public/images/common/B.webp differ
diff --git a/public/images/common/button.webp b/public/images/common/button.webp
new file mode 100644
index 0000000..85e6f03
Binary files /dev/null and b/public/images/common/button.webp differ
diff --git a/public/images/common/confirmation-modal.webp b/public/images/common/confirmation-modal.webp
new file mode 100644
index 0000000..deeed97
Binary files /dev/null and b/public/images/common/confirmation-modal.webp differ