modal query string trigger

This commit is contained in:
nicwands 2026-06-09 14:26:06 -04:00
parent 750af00e08
commit 499c23994b
15 changed files with 119 additions and 144 deletions

BIN
public/images/output.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View file

@ -2,7 +2,11 @@
<lenis root :options="{ duration: 1 }">
<div :class="classes" :style="styles" id="container">
<suspense>
<layout />
<site-header />
</suspense>
<suspense>
<router-view />
</suspense>
</div>
</lenis>

3
src/components.d.ts vendored
View file

@ -11,11 +11,8 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
Dl: typeof import('./components/Dl.vue')['default']
DlRow: typeof import('./components/DlRow.vue')['default']
InfiniteScrollContainer: typeof import('./components/InfiniteScrollContainer.vue')['default']
InfiniteScrollDemo: typeof import('./components/InfiniteScrollDemo.vue')['default']
Layout: typeof import('./components/Layout.vue')['default']
Lenis: typeof import('./components/Lenis.vue')['default']
LoadingSpinner: typeof import('./components/LoadingSpinner.vue')['default']
MosaicModal: typeof import('./components/MosaicModal.vue')['default']

View file

@ -14,9 +14,7 @@
<slot
name="viewport"
:viewport="viewport"
:index="viewport.index"
:content="viewport.content"
:isActive="viewport.index === currentViewportIndex"
>
<!-- Default viewport content -->
<div class="default-viewport">
@ -117,7 +115,7 @@ defineExpose({
})
</script>
<style lang="scss" scoped>
<style lang="scss">
.infinite-scroll-container {
position: relative;
width: 100%;

View file

@ -1,43 +0,0 @@
<template>
<div class="layout">
<suspense>
<site-header />
</suspense>
<main class="main-content">
<mosaic-scroll :items="mosaicItems" />
</main>
</div>
</template>
<script setup>
import { useStrapiSingle } from '@/composables/useStrapi'
import useStrapiHead from '@/composables/useStrapiHead'
import { computed } from 'vue'
const { data } = await useStrapiSingle('home')
useStrapiHead()
const mosaicItems = computed(() => data.value?.mosaic_items || [])
</script>
<style lang="scss" scoped>
.layout {
position: relative;
min-height: 100vh;
.main-content {
position: relative;
width: 100%;
}
div {
background: var(--theme-bg);
color: var(--theme-fg);
code {
background: var(--grey);
}
}
}
</style>

View file

@ -8,8 +8,10 @@
</div>
<div class="window content" @click.stop data-lenis-prevent>
<h1 class="title">{{ item.title }}</h1>
<strapi-content :content="item.content" />
<lenis instance="mosaic-modal">
<h1 class="title">{{ item.title }}</h1>
<strapi-content :content="item.content" />
</lenis>
</div>
<button class="window close" @click="emit('close')">Close X</button>
@ -27,7 +29,7 @@ const emit = defineEmits(['close'])
position: fixed;
inset: 0;
align-items: flex-start;
padding-top: desktop-vh(27px);
padding-top: desktop-vw(27px);
.window {
background: var(--theme-bg);
@ -43,15 +45,18 @@ const emit = defineEmits(['close'])
.dl-row:first-child {
border-top: none;
padding-top: 0;
}
}
&.content {
padding: desktop-vw(34px) desktop-vw(24px);
border-radius: desktop-vw(10px);
grid-column: 6 / span 7;
max-height: desktop-vh(879px);
overflow: auto;
.lenis {
max-height: desktop-vh(900px);
overflow: auto;
padding: desktop-vw(34px) desktop-vw(24px);
}
.title {
text-align: center;
margin-bottom: 1em;

View file

@ -5,13 +5,8 @@
:initialIndex="0"
:generateContent="generateMosaicContent"
>
<template #viewport="{ viewport, content, isActive }">
<mosaic-viewport
:viewport="viewport"
:index="viewport.index"
:content="content"
:isActive="isActive"
/>
<template #viewport="{ viewport, content }">
<mosaic-viewport :viewport="viewport" :content="content" />
</template>
</infinite-scroll-container>
</div>
@ -28,18 +23,6 @@ const props = defineProps({
// Initialize mosaic layout generator
const { generateLayout, populateCells } = useMosaicLayout()
// const generateGifContent = (cell) => {
// return {
// id: `gif-${cell.id}`,
// width: Math.floor(cell.width),
// height: Math.floor(cell.height),
// note: _shuffle(
// 'curabitur euismod ultrices porttitor. Vivamus magna sapien, pretium at ullamcorper semper, sollicitudin et magna curabitur eget dolor a neque facilisis semper pellentesque euismod luctus urna, non aliquam justo aliquet id vestibulum quis lobortis diam nulla facilisi phasellus et rutrum lectus vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Praesent mattis felis condimentum, gravida lacus id, pretium neque. Donec cursus augue a dui placerat porttitor.Vestibulum rhoncus lectus nisl, nec gravida ex sollicitudin vel etiam dictum ac lacus ac elementum donec turpis eros, aliquam in nibh a, posuere tristique neque. Nunc cursus molestie consectetur.Integer placerat lorem eu nunc semper porta.'.split(
// ' ',
// ),
// ).join(' '),
// }
// }
const generateCellContent = () => {
const itemIndex = Math.floor(Math.random() * props.items.length)
return props.items[itemIndex]
@ -53,8 +36,6 @@ const generateMosaicContent = async (index) => {
const targetCells = baseCellCount + variance
const layoutOptions = {
minCellWidth: 200,
minCellHeight: 200,
targetCells: targetCells,
}
@ -62,7 +43,7 @@ const generateMosaicContent = async (index) => {
const cells = generateLayout(layoutOptions)
// Populate cells with GIF content
const populatedCells = populateCells(cells, generateCellContent)
const populatedCells = populateCells(cells.value, generateCellContent)
return {
cells: populatedCells,
@ -70,7 +51,7 @@ const generateMosaicContent = async (index) => {
}
</script>
<style lang="scss" scoped>
<style lang="scss">
.mosaic-scroll {
position: relative;
}

View file

@ -1,5 +1,5 @@
<template>
<div class="mosaic-viewport" :class="{ active: isActive }">
<div class="mosaic-viewport">
<div class="mosaic-container">
<div
v-for="cell in content.cells"
@ -8,10 +8,10 @@
`layout-${cell.width >= cell.height ? 'landscape' : 'portrait'}`,
]"
:style="{
left: cell.x + 'px',
top: cell.y + 'px',
width: cell.width + 'px',
height: cell.height + 'px',
left: cell.x * 100 + 'vw',
top: cell.y * 100 + 'vh',
width: cell.width * 100 + 'vw',
height: cell.height * 100 + 'vh',
}"
:key="`${viewport.index}-${cell.id}`"
@click="openModal(cell.content)"
@ -36,37 +36,45 @@
</template>
<script setup>
import { ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { useEventListener } from '@vueuse/core'
import useLenis from '@/composables/useLenis'
import _kebabCase from 'lodash/kebabCase'
import { useRoute, useRouter } from 'vue-router'
const props = defineProps({
viewport: {
type: Object,
required: true,
},
index: {
type: Number,
required: true,
},
content: {
type: Object,
required: true,
},
isActive: {
type: Boolean,
default: false,
},
})
const selectedItem = ref(null)
const route = useRoute()
const router = useRouter()
const openModal = (cell) => {
selectedItem.value = cell
const selectedItem = computed(() => {
const { item } = route.query
return props.content.cells.find((cell) => {
return _kebabCase(cell?.content?.title || '') === item
})?.content
})
const openModal = (cellContent) => {
router.push({
query: {
item: _kebabCase(cellContent.title),
},
})
}
const closeModal = () => {
selectedItem.value = null
router.push({
query: null,
})
}
// Close note on Escape key
@ -88,7 +96,7 @@ watch(selectedItem, (newItem, oldItem) => {
})
</script>
<style lang="scss" scoped>
<style lang="scss">
.mosaic-viewport {
height: 100vh;
display: flex;
@ -110,12 +118,12 @@ watch(selectedItem, (newItem, oldItem) => {
&.layout-portrait {
.strapi-media {
width: 50%;
width: 40%;
}
}
&.layout-landscape {
.strapi-media {
height: 50%;
height: 40%;
aspect-ratio: 1;
}
}

View file

@ -1,6 +1,6 @@
<template>
<header v-if="data" class="site-header layout-grid-inner">
<img class="logo" src="/images/output_pixelated.gif" />
<img class="logo" src="/images/output.gif" />
<div class="info theme-red">
<strapi-content class="copy" :content="data.header_copy" />
@ -23,7 +23,7 @@ const { data } = await useStrapiGlobal()
left: 0;
right: 0;
pointer-events: none;
padding: desktop-vw(27px);
padding: desktop-vw(27px) var(--layout-margin);
align-items: flex-start;
> * {
@ -33,6 +33,7 @@ const { data } = await useStrapiGlobal()
.logo {
width: desktop-vw(93px);
height: auto;
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));
}
.info {

View file

@ -1,4 +1,6 @@
import { useWindowSize } from '@vueuse/core'
import { computed } from 'vue'
import { viewports } from '@/libs/theme'
/**
* Mosaic Layout Generator
@ -8,6 +10,9 @@ import { useWindowSize } from '@vueuse/core'
const { width: wWidth, height: wHeight } = useWindowSize()
export const useMosaicLayout = () => {
const dvw = (pixels) => (pixels / viewports.desktop.width) * wWidth.value
const dvh = (pixels) => (pixels / viewports.desktop.height) * wHeight.value
/**
* Generate a space-filling mosaic layout
* @param {Object} options Layout configuration
@ -19,24 +24,26 @@ export const useMosaicLayout = () => {
* @param {number} options.maxCellHeight Maximum cell height
* @param {number} options.targetCells Target number of cells to create (if specified, overrides subdivisionProbability)
* @param {number} options.subdivisionProbability Probability of subdivision (0-1) - used when targetCells is not specified
* @returns {Array} Array of cell objects with x, y, width, height, id
* @returns {ComputedRef} Computed ref of array of cell objects with x, y, width, height, id
*/
const generateLayout = (options = {}) => {
const {
width = wWidth.value,
height = wHeight.value,
minCellWidth = 150,
minCellHeight = 150,
targetCells = 5,
} = options
return computed(() => {
const {
width = wWidth.value,
height = wHeight.value,
minCellWidth = dvw(150),
minCellHeight = dvh(150),
targetCells = 5,
} = typeof options === 'function' ? options() : options
return generateLayoutWithCellCount(
width,
height,
targetCells,
minCellWidth,
minCellHeight,
)
return generateLayoutWithCellCount(
width,
height,
targetCells,
minCellWidth,
minCellHeight,
)
})
}
/**
@ -92,13 +99,7 @@ export const useMosaicLayout = () => {
let splitVertical = true
if (canSubdivideWidth && canSubdivideHeight) {
const aspectRatio = areaToSplit.width / areaToSplit.height
if (aspectRatio > 1.5) {
splitVertical = true // Split wide areas vertically
} else if (aspectRatio < 0.67) {
splitVertical = false // Split tall areas horizontally
} else {
splitVertical = Math.random() > 0.5 // Random for square-ish areas
}
splitVertical = aspectRatio > 1
} else {
splitVertical = canSubdivideWidth
}
@ -157,13 +158,13 @@ export const useMosaicLayout = () => {
}
}
// Convert areas to cells
// Convert areas to cells, normalise to 0-1 so consumers can use vw/vh
return areas.map((area) => ({
id: area.id,
x: area.x,
y: area.y,
width: area.width,
height: area.height,
x: area.x / width,
y: area.y / height,
width: area.width / width,
height: area.height / height,
}))
}

View file

@ -9,10 +9,6 @@ import useLenis from '@/composables/useLenis'
const { height: wHeight } = useWindowSize()
export const useScrollProgress = (el, callback, entry = 0.5, exit = 0.5) => {
const isActive = ref(true)
const smoothProgress = ref(0)
const { height, top } = useElementBounding(el)
const isIntersected = ref(false)
@ -20,10 +16,6 @@ export const useScrollProgress = (el, callback, entry = 0.5, exit = 0.5) => {
isIntersected.value = isIntersecting
})
useLenis(({ scroll }) => {
if (!isActive.value) return
if (!height.value || !wHeight.value) return
if (!isIntersected.value) return
const pageTop = scroll + top.value
@ -37,10 +29,6 @@ export const useScrollProgress = (el, callback, entry = 0.5, exit = 0.5) => {
callback?.(smoothProgress.value)
})
const destroy = () => {
isActive.value = false
stop?.()
}
return { destroy }
}

View file

@ -1,9 +1,10 @@
import './styles/main.scss'
import App from './App.vue'
import { ViteSSG } from 'vite-ssg/single-page'
import { ViteSSG } from 'vite-ssg'
import routes from './routes'
import strapi from './plugins/strapi'
export const createApp = ViteSSG(App, ({ app }) => {
export const createApp = ViteSSG(App, routes, ({ app }) => {
// Plugins
app.use(strapi)
})

23
src/pages/index.vue Normal file
View file

@ -0,0 +1,23 @@
<template>
<main class="home">
<mosaic-scroll :items="mosaicItems" />
</main>
</template>
<script setup>
import { useStrapiSingle } from '@/composables/useStrapi'
import useStrapiHead from '@/composables/useStrapiHead'
import { computed } from 'vue'
const { data } = await useStrapiSingle('home')
useStrapiHead()
const mosaicItems = computed(() => data.value?.mosaic_items || [])
</script>
<style lang="scss">
main.home {
position: relative;
width: 100%;
}
</style>

11
src/routes.js Normal file
View file

@ -0,0 +1,11 @@
import index from './pages/index.vue'
export default {
routes: [
{
path: '/',
component: index,
name: 'home',
},
],
}