Skip to content

Commit

Permalink
added nested modals
Browse files Browse the repository at this point in the history
  • Loading branch information
onmax committed Apr 19, 2024
1 parent c3c6e90 commit 7ca2240
Show file tree
Hide file tree
Showing 11 changed files with 144 additions and 48 deletions.
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "pnpm i18n:extract && pnpm build-only",
"build": "pnpm i18n:extract && type-check && pnpm build-only",
"build:dev": "pnpm i18n:extract && pnpm type-check && vite build --mode development --no-minify",
"preview": "vite preview --port 4173",
"build-only": "vite build",
Expand Down Expand Up @@ -45,6 +45,7 @@
"@types/google.maps": "^3.55.7",
"@types/grecaptcha": "^3.0.9",
"@types/node": "^20.12.7",
"@unocss/preset-mini": "^0.59.4",
"@unocss/preset-rem-to-px": "^0.59.2",
"@unocss/reset": "^0.59.2",
"@vitejs/plugin-vue": "^5.0.4",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ declare module 'vue' {
ComboboxViewport: typeof import('radix-vue')['ComboboxViewport']
Computer_electronics: typeof import('./components/icons/categories/computer_electronics.vue')['default']
Controls: typeof import('./components/elements/Controls.vue')['default']
copy: typeof import('./components/atoms/Modal copy.vue')['default']
CryptocityCard: typeof import('./components/cards/cryptocity/CryptocityCard.vue')['default']
'CryptocityCard.story': typeof import('./components/cards/cryptocity/CryptocityCard.story.vue')['default']
CryptocityMarker: typeof import('./components/markers/CryptocityMarker.vue')['default']
Expand Down Expand Up @@ -107,6 +108,7 @@ declare module 'vue' {
MobileView: typeof import('./components/MobileView.vue')['default']
Modal: typeof import('./components/atoms/Modal.vue')['default']
Naka: typeof import('./components/icons/providers/naka.vue')['default']
NestedModal: typeof import('./components/atoms/NestedModal.vue')['default']
NewCandidate: typeof import('./components/forms/NewCandidate.vue')['default']
Nimiq: typeof import('./components/icons/providers/nimiq.vue')['default']
NimiqPay: typeof import('./components/icons/providers/nimiq-pay.vue')['default']
Expand Down
11 changes: 5 additions & 6 deletions apps/web/src/components/DesktopView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ const toggleList = useToggle(isListShown)
<div id="shadow-left" absolute inset-0 max-w-368 pointer-events-none bg-gradient-to-r from-neutral to-transparent />
<aside absolute max-w-384 inset-24 right-initial h-max pointer-events-none children:pointer-events-auto flex="~ col">
<!-- This element if for the shadow in the header. We cannot use a normal shadow because the use of mask-image restrict us of using shadows -->
<div absolute inset-0 shadow pointer-events-none id="shadow"
<div absolute inset-0 shadow ring="1.5 neutral/3" pointer-events-none id="shadow"
style="height: calc(66px + (88px * var(--search-box-hint)))" />
<div w-max bg-neutral-0 ring-neutral-100 id="wrapper">
<div w-max bg-neutral-0 id="wrapper">
<InteractionBar />
<DesktopList :singles="singlesInView" :clusters="clustersInView" :list-is-shown="isListShown" />
</div>
<button mt-12 pill-tertiary pill-sm ring-neutral-50 z-10 flex="~ gap-8" @click="() => toggleList()">
<button mt-12 pill-tertiary border-none pill-sm ring="1.5 neutral/3" z-10 flex="~ gap-8" @click="() => toggleList()">
<div i-nimiq:chevron-down :class="{ 'rotate-180': isListShown }" text="10 op-70"
transition="transform delay-500" />
{{ $t(isListShown ? 'Hide list' : 'Show list') }}
Expand All @@ -36,7 +36,7 @@ const toggleList = useToggle(isListShown)
transition: transform 1000ms 75ms, opacity 300ms 75ms;
opacity: 0;
&:has(+ aside [data-state="open"]) {
&:has(+ aside :is([data-state="open"]:not(#crypto-map-modal))) {
/* List or suggestions opened */
transform: translateX(0);
transition: transform 500ms 100ms, opacity 300ms 100ms;
Expand All @@ -58,13 +58,12 @@ aside {
#wrapper {
transition: border-radius 75ms;
border-radius: 16px;
box-shadow: 0px 0.337px 2px 0px rgba(0, 0, 0, 0.03), 0px 1.5px 3px 0px rgba(0, 0, 0, 0.05), 0px 4px 16px 0px rgba(0, 0, 0, 0.07);
&:not(:has([data-suggestions])) {
mask-image: linear-gradient(white, white);
}
/*
/*
If the list is closed and there are suggestions, we need to remove the border-radius.
We use double :has to make an AND gate
in other words: If we have suggestions AND the list is closed.
Expand Down
102 changes: 73 additions & 29 deletions apps/web/src/components/atoms/Modal.vue
Original file line number Diff line number Diff line change
@@ -1,40 +1,84 @@
<script setup lang="ts">
defineEmits({ open: Function, close: Function })
const open = defineModel<boolean>()
function hasSlot(slot: 'pre-title' | 'title' | 'description' | 'content') {
return !!useSlots()[slot]
}
const open = defineModel<boolean>('open')
const slots = useSlots()
</script>

<template>
<DialogRoot v-model:open="open" @update:open="$event ? $emit('open') : $emit('close')" group>
<DialogRoot v-model:open="open" group>
<DialogTrigger v-bind="$attrs">
<slot name="trigger" />
</DialogTrigger>
<DialogPortal>
<DialogOverlay bg-darkblue bg-op-60 fixed inset-0 z-20 item-open:animate-fade-in item-closed:animate-fade-out />
<DialogContent fixed max-desktop="bottom-0 rounded-t-8" desktop="top-1/2 left-1/2 -translate-1/2 rounded-8" max-h-85dvh w-full max-w-512 py-32 rounded-md z-20 of-y-auto ring-neutral-100 shadow-lg bg-neutral-0
class="data-[state=open]:animate-fade md:rounded-lg md:max-w512"
>
<div v-if="hasSlot('pre-title')" px-24 desktop:px-40 mb-16>
<slot name="pre-title" />
</div>

<DialogTitle v-if="hasSlot('title')" px-24 desktop:px-40 mb-8 text-18 font-bold text-neutral lh-none as="h2">
<slot name="title" />
</DialogTitle>
<DialogDescription v-if="hasSlot('description')" as="div" px-24 desktop:px-40 text-neutral-800>
<slot name="description" />
</DialogDescription>

<div v-if="hasSlot('content')" px-24 desktop:px-40>
<slot name="content" />
</div>

<DialogClose :aria-label="$t('Close')" close-btn absolute right-16 top-16 text-28 />
</DialogContent>
<Transition name="backdrop">
<DialogOverlay bg-darkblue op-60 fixed inset-0 z-20 />
</Transition>
<Transition name="zoom">
<DialogContent fixed bottom-0 desktop="top-1/2 left-1/2 translate--1/2" op-100 max-h-85dvh w-full max-w-512
py-32 z-20 of-y-auto ring="1.5 neutral-50" shadow-lg bg-neutral-0 rounded="t-8 desktop:8" h-max class="content">
<div v-if="slots['pre-title']" px-24 desktop:px-40 mb-16>
<slot name="pre-title" />
</div>

<DialogTitle v-if="slots.title" px-24 desktop:px-40 mb-8 text-18 font-bold text-neutral lh-none as="h2">
<slot name="title" />
</DialogTitle>
<DialogDescription v-if="slots.description" as="div" px-24 desktop:px-40 text-neutral-800>
<slot name="description" />
</DialogDescription>

<div v-if="slots.content" px-24 desktop:px-40>
<slot name="content" />
</div>

<DialogClose :aria-label="$t('Close')" close-btn absolute right-16 top-16 text-28 />
</DialogContent>
</Transition>
</DialogPortal>
</DialogRoot>
</template>

<style>
/* https://github.com/nimiq/wallet/blob/master/src/components/modals/Modal.vue */
.backdrop-enter-active {
transition: opacity 650ms cubic-bezier(.3, 1, .2, 1);
}
.backdrop-leave-active {
transition: opacity 650ms cubic-bezier(.3, 0, 0, 1);
}
.backdrop-enter-from,
.backdrop-leave-to {
opacity: 0;
}
.zoom-enter-active,
.zoom-leave-active {
transition:
opacity 250ms cubic-bezier(.4, 0, .2, 1),
transform 450ms var(--nq-ease);
}
.zoom-enter-from,
.zoom-leave-to {
opacity: 0;
--un-scale-x: 0.96;
--un-scale-y: 0.96;
--un-translate-y: calc(-50% - 0.5rem);
}
.content {
transition:
transform 250ms cubic-bezier(.4, 0, .2, 1),
filter 450ms cubic-bezier(.3, 0, 0, 1);
/* Radix will set all the modals in the root of the body */
&:has(+ [data-nested][data-state="open"]) {
--un-scale-x: 0.94;
--un-scale-y: 0.94;
/* background-color: rgb(var(--nq-neutral-700)); */
filter: brightness(0.75);
}
}
</style>
2 changes: 1 addition & 1 deletion apps/web/src/components/atoms/SearchBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ watchOnce(q, useApp().hideSearchBoxHint)
<ComboboxCancel v-else i-nimiq:cross absolute right-16 text="10 neutral-700 peer-focus-visible:blue/80" />
</ComboboxAnchor>

<ComboboxContent absolute bg-neutral-0 rounded-b-16 top-66 inset-x-0 data-suggestions>
<ComboboxContent absolute bg-neutral-0 shadow rounded-b-16 top-66 inset-x-0 data-suggestions>
<ComboboxViewport>
<div p-16 text-neutral-800 v-if="status !== AutocompleteStatus.WithResults">
<ComboboxEmpty v-if="status === AutocompleteStatus.NoResults">
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/elements/Controls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ function clearStorage() {
</PopoverPortal>
</PopoverRoot>
<button size-32 shadow ring-neutral-100 rounded-full bg="neutral-0 hover:neutral-100" text-14 flex="~ items-center justify-center"
<button size-32 shadow ring="1.5 neutral/3" rounded-full bg="neutral-0 hover:neutral-100" text-14 flex="~ items-center justify-center"
v-if="browserPositionIsSupported" :disabled="geolocatingUserBrowser" :aria-label="$t('Show your location')"
:title="$t('Show your location')" @click="setBrowserPosition">
<div i-nimiq:gps />
</button>
<div flex="~ col" rounded-full shadow max-desktop:hidden w-32 ring-neutral-100 text-12>
<div flex="~ col" rounded-full shadow max-desktop:hidden w-32 ring="1.5 neutral/3" text-12>
<button size-32 rounded-t-full bg="neutral-0 hover:neutral-100" transition-colors flex="~ justify-center items-center" :aria-label="$t('Increase zoom')"
:title="$t('Increase zoom')" @click="useMap().increaseZoom">
<div i-nimiq:plus />
Expand Down
20 changes: 18 additions & 2 deletions apps/web/src/components/elements/CryptoMapModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,25 @@ watch(lang, () => setLanguage(lang.value))

<template #content>
<div flex="~ items-center justify-between" mt-32>
<a href="/location/add" pill-blue pill-sm>
<NestedModal>
<template #trigger>
<button pill-blue pill-sm>
{{ $t('Add Crypto location') }}
</button>
</template>
<template #title>
Lo
</template>
<template #description>
jj
</template>
<template #content>
Your content
</template>
</NestedModal>
<!-- <a href="/location/add" pill-blue pill-sm>
{{ $t('Add Crypto location') }}
</a>
</a> -->

<TriangleSelector v-model:selected="lang" :options="SUPPORTED_LANGUAGES" />
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/elements/InteractionBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const showHint = computed(() => shouldShowSearchBoxHint.value && useBreakpoints(
<header relative z-10 flex="~ items-center gap-8 desktop:gap-16" w-full p-24 desktop:p-16 pl-16 z-100>
<div i-nimiq:logos-crypto-map text-24 aria-hidden shrink-0 />
<SearchBox />
<CryptoMapModal />
<CryptoMapModal id="crypto-map-modal" />
</header>

<!-- We need to hardcode the height, otherwise the desktop list will break -->
Expand Down
7 changes: 4 additions & 3 deletions apps/web/src/composables/useAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@ interface UseAutocompleteOptions {
export function useAutocomplete({ autocomplete }: UseAutocompleteOptions) {
const status = ref<AutocompleteStatus>(AutocompleteStatus.Initial)
const query = ref<string>('')
watch(() => query.value, () => {
watch(() => query.value, ([newValue, oldValue]) => {
if (newValue === oldValue) return
if (!query.value) {
status.value = AutocompleteStatus.Initial
clearSuggestions()
} else {
if(newValue !== '' && (oldValue === '') || (googleSuggestions.value.length === 0 && locationSuggestions.value.length === 0)) status.value = AutocompleteStatus.Loading
if(newValue === '' && oldValue !== '') status.value = AutocompleteStatus.Initial
querySearch()
}
})
Expand Down Expand Up @@ -97,12 +100,10 @@ export function useAutocomplete({ autocomplete }: UseAutocompleteOptions) {
// eslint-disable-next-line no-console
console.group(`🔍 Autocomplete "${query}"`)

status.value = query.value === '' ? AutocompleteStatus.Loading : status.value
if (!query.value) {
clearSuggestions()
return
}
console.log('ayaya', query.value)

const result = await Promise.allSettled([autocompleteLocations(), autocompleteGoogle()])

Expand Down
4 changes: 1 addition & 3 deletions apps/web/unocss.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { defineConfig, presetAttributify, presetUno, presetIcons } from 'unocss'
import { presetRemToPx } from '@unocss/preset-rem-to-px'
import presetAnimations from 'unocss-preset-animations'
import { Provider } from 'types'
import { match } from 'assert'
import { matchedRouteKey } from 'vue-router'


export default defineConfig({
Expand Down Expand Up @@ -42,7 +40,7 @@ export default defineConfig({
return matcher
return {
matcher: matcher.replace(re, ''),
selector: s => `[data-state="${match[1]}"] :is(& ${s}, &:is(${s}))`,
selector: s => `[data-state="${match[1]}"]:is(${s}, & ${s})`,
}
},
]
Expand Down
35 changes: 35 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7ca2240

Please sign in to comment.