From 5bc57f5edcd7846f4bbfa48af855f7f43a3ede74 Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Fri, 19 Dec 2025 19:15:24 +0100 Subject: [PATCH] feat(common): implement buttons and confirmation modal --- app/components/Common/Buttons.vue | 71 ++++++++++++++ app/components/Common/ConfirmationModal.vue | 57 ++++++++++++ app/composables/useButtonNavigation.ts | 6 ++ app/composables/useConfirmationModal.ts | 93 +++++++++++++++++++ app/utils/async.ts | 2 + public/images/common/A.webp | Bin 0 -> 58 bytes public/images/common/B.webp | Bin 0 -> 60 bytes public/images/common/button.webp | Bin 0 -> 126 bytes public/images/common/confirmation-modal.webp | Bin 0 -> 116 bytes 9 files changed, 229 insertions(+) create mode 100644 app/components/Common/Buttons.vue create mode 100644 app/components/Common/ConfirmationModal.vue create mode 100644 app/composables/useConfirmationModal.ts create mode 100644 app/utils/async.ts create mode 100644 public/images/common/A.webp create mode 100644 public/images/common/B.webp create mode 100644 public/images/common/button.webp create mode 100644 public/images/common/confirmation-modal.webp 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 0000000000000000000000000000000000000000..2b1889c33db7b31d2af2ab80db9197e6027f9ba5 GIT binary patch literal 58 zcmWIYbaOLeU|uD P1yL)5_V=yQI>P_}=)n_f literal 0 HcmV?d00001 diff --git a/public/images/common/button.webp b/public/images/common/button.webp new file mode 100644 index 0000000000000000000000000000000000000000..85e6f03df23ea1ac44ffd82853864727fb465cfe GIT binary patch literal 126 zcmV-^0D=EfNk&F?00012MM6+kP&iC!0000lPe23^b+CzKw+=oFZYTsoAP9oGxYSd3 z0Q{Yq|LDKbwr!GXD0oMV2Ie@wj-@&Gfl_#Z#PUJh>BT~)hd=@Q(Z9QLpr0OcVyr~m)} literal 0 HcmV?d00001 diff --git a/public/images/common/confirmation-modal.webp b/public/images/common/confirmation-modal.webp new file mode 100644 index 0000000000000000000000000000000000000000..deeed979c273a32dee940689ecd67be112575ce9 GIT binary patch literal 116 zcmV-)0E_=pNk&F&00012MM6+kP&iCr0000l&43UPbts7>$$5J(<^4Oj1aH`dM