dummy content scaffolding

This commit is contained in:
nicwands 2026-06-08 17:09:42 -04:00
parent 5c38cd9bdd
commit 750af00e08
9 changed files with 292 additions and 83 deletions

3
src/components.d.ts vendored
View file

@ -18,10 +18,13 @@ declare module 'vue' {
Layout: typeof import('./components/Layout.vue')['default'] Layout: typeof import('./components/Layout.vue')['default']
Lenis: typeof import('./components/Lenis.vue')['default'] Lenis: typeof import('./components/Lenis.vue')['default']
LoadingSpinner: typeof import('./components/LoadingSpinner.vue')['default'] LoadingSpinner: typeof import('./components/LoadingSpinner.vue')['default']
MosaicModal: typeof import('./components/MosaicModal.vue')['default']
MosaicScroll: typeof import('./components/MosaicScroll.vue')['default']
MosaicViewport: typeof import('./components/MosaicViewport.vue')['default'] MosaicViewport: typeof import('./components/MosaicViewport.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SiteHeader: typeof import('./components/site/Header.vue')['default'] SiteHeader: typeof import('./components/site/Header.vue')['default']
StrapiContent: typeof import('./components/strapi/Content.vue')['default'] StrapiContent: typeof import('./components/strapi/Content.vue')['default']
StrapiMedia: typeof import('./components/strapi/Media.vue')['default']
} }
} }

View file

@ -5,7 +5,7 @@
</suspense> </suspense>
<main class="main-content"> <main class="main-content">
<infinite-scroll-demo /> <mosaic-scroll :items="mosaicItems" />
</main> </main>
</div> </div>
</template> </template>
@ -13,9 +13,12 @@
<script setup> <script setup>
import { useStrapiSingle } from '@/composables/useStrapi' import { useStrapiSingle } from '@/composables/useStrapi'
import useStrapiHead from '@/composables/useStrapiHead' import useStrapiHead from '@/composables/useStrapiHead'
import { computed } from 'vue'
const { data } = await useStrapiSingle('home') const { data } = await useStrapiSingle('home')
useStrapiHead() useStrapiHead()
const mosaicItems = computed(() => data.value?.mosaic_items || [])
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -0,0 +1,75 @@
<template>
<div
class="mosaic-modal theme-dark layout-grid-inner"
@click="emit('close')"
>
<div class="window dl" @click.stop>
<dl-row v-for="row in item.dl" :row="row" />
</div>
<div class="window content" @click.stop data-lenis-prevent>
<h1 class="title">{{ item.title }}</h1>
<strapi-content :content="item.content" />
</div>
<button class="window close" @click="emit('close')">Close X</button>
</div>
</template>
<script setup>
const props = defineProps({ item: Object })
const emit = defineEmits(['close'])
</script>
<style lang="scss">
.mosaic-modal {
position: fixed;
inset: 0;
align-items: flex-start;
padding-top: desktop-vh(27px);
.window {
background: var(--theme-bg);
color: var(--theme-fg);
&.dl {
padding: desktop-vw(12px) desktop-vw(10px);
border-radius: desktop-vw(10px);
display: flex;
flex-direction: column;
gap: desktop-vw(8px);
grid-column: 4 / span 2;
.dl-row:first-child {
border-top: none;
}
}
&.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;
.title {
text-align: center;
margin-bottom: 1em;
}
}
&.close {
padding: desktop-vw(9px);
border-radius: desktop-vw(16px);
cursor: pointer;
/* position: absolute;
right: calc(
(var(--layout-column-width) * 3) +
(var(--layout-column-gap) * 3) + var(--layout-margin)
);
top: desktop-vw(40px); */
margin-right: auto;
@include label;
}
}
}
</style>

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="infinite-scroll-demo"> <div class="mosaic-scroll">
<infinite-scroll-container <infinite-scroll-container
:bufferSize="2" :bufferSize="2"
:initialIndex="0" :initialIndex="0"
@ -21,20 +21,28 @@
import _shuffle from 'lodash/shuffle' import _shuffle from 'lodash/shuffle'
import { useMosaicLayout } from '@/composables/useMosaicLayout' import { useMosaicLayout } from '@/composables/useMosaicLayout'
const props = defineProps({
items: Array,
})
// Initialize mosaic layout generator // Initialize mosaic layout generator
const { generateLayout, populateCells } = useMosaicLayout() const { generateLayout, populateCells } = useMosaicLayout()
const generateGifContent = (cell) => { // const generateGifContent = (cell) => {
return { // return {
id: `gif-${cell.id}`, // id: `gif-${cell.id}`,
width: Math.floor(cell.width), // width: Math.floor(cell.width),
height: Math.floor(cell.height), // height: Math.floor(cell.height),
note: _shuffle( // 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( // '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(' '), // ).join(' '),
} // }
// }
const generateCellContent = () => {
const itemIndex = Math.floor(Math.random() * props.items.length)
return props.items[itemIndex]
} }
// Generate mosaic content for each viewport // Generate mosaic content for each viewport
@ -44,8 +52,6 @@ const generateMosaicContent = async (index) => {
const variance = Math.abs(index % 2) * 2 const variance = Math.abs(index % 2) * 2
const targetCells = baseCellCount + variance const targetCells = baseCellCount + variance
// Generate layout with different parameters based on viewport index
// This adds variety to each viewport
const layoutOptions = { const layoutOptions = {
minCellWidth: 200, minCellWidth: 200,
minCellHeight: 200, minCellHeight: 200,
@ -56,17 +62,16 @@ const generateMosaicContent = async (index) => {
const cells = generateLayout(layoutOptions) const cells = generateLayout(layoutOptions)
// Populate cells with GIF content // Populate cells with GIF content
const populatedCells = populateCells(cells, generateGifContent) const populatedCells = populateCells(cells, generateCellContent)
return { return {
cells: populatedCells, cells: populatedCells,
viewportIndex: index,
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.infinite-scroll-demo { .mosaic-scroll {
position: relative; position: relative;
} }
</style> </style>

View file

@ -3,7 +3,10 @@
<div class="mosaic-container"> <div class="mosaic-container">
<div <div
v-for="cell in content.cells" v-for="cell in content.cells"
class="mosaic-cell" :class="[
'mosaic-cell',
`layout-${cell.width >= cell.height ? 'landscape' : 'portrait'}`,
]"
:style="{ :style="{
left: cell.x + 'px', left: cell.x + 'px',
top: cell.y + 'px', top: cell.y + 'px',
@ -11,38 +14,31 @@
height: cell.height + 'px', height: cell.height + 'px',
}" }"
:key="`${viewport.index}-${cell.id}`" :key="`${viewport.index}-${cell.id}`"
@click="openNote(cell.content)" @click="openModal(cell.content)"
> >
<div class="gif-placeholder"> <strapi-media
<div class="gif-info"> :field="cell.content.image"
<span class="gif-id">{{ cell.content.id }}</span> :aspect="100"
<span class="gif-size" fit="contain"
>{{ Math.round(cell.width) }}x{{ />
Math.round(cell.height)
}}</span
>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Note modal --> <!-- Content modal -->
<teleport to="#container"> <teleport to="#container">
<div v-if="selectedNote" class="note-modal-backdrop" @click="closeNote"> <mosaic-modal
<div class="note-modal" @click.stop> v-if="selectedItem"
<button class="note-close" @click="closeNote">×</button> :item="selectedItem"
<div class="note-content"> @close="closeModal"
<p>{{ selectedNote.note }}</p> />
</div>
</div>
</div>
</teleport> </teleport>
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue'
import { useEventListener } from '@vueuse/core' import { useEventListener } from '@vueuse/core'
import { ref } from 'vue' import useLenis from '@/composables/useLenis'
const props = defineProps({ const props = defineProps({
viewport: { viewport: {
@ -63,26 +59,33 @@ const props = defineProps({
}, },
}) })
const selectedNote = ref(null) const selectedItem = ref(null)
const openNote = (cellContent) => { const openModal = (cell) => {
if (cellContent.note) { selectedItem.value = cell
selectedNote.value = cellContent
}
} }
const closeNote = () => { const closeModal = () => {
selectedNote.value = null selectedItem.value = null
} }
// Close note on Escape key // Close note on Escape key
const handleKeydown = (event) => { const handleKeydown = (event) => {
if (event.key === 'Escape' && selectedNote.value) { if (event.key === 'Escape' && selectedItem.value) {
closeNote() closeModal()
} }
} }
useEventListener('keydown', handleKeydown) useEventListener('keydown', handleKeydown)
// Pause scroll when modal is open
const lenis = useLenis()
watch(selectedItem, (newItem, oldItem) => {
if (newItem && !oldItem) {
lenis.value.stop()
} else if (oldItem && !newItem) {
lenis.value.start()
}
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -101,37 +104,22 @@ useEventListener('keydown', handleKeydown)
overflow: hidden; overflow: hidden;
border: 1px solid var(--theme-fg); border: 1px solid var(--theme-fg);
cursor: pointer; cursor: pointer;
.gif-placeholder {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.gif-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
}
}
}
.note-modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
.note-modal { &.layout-portrait {
background: var(--theme-bg); .strapi-media {
width: 50vw; width: 50%;
padding: 2rem; }
}
&.layout-landscape {
.strapi-media {
height: 50%;
aspect-ratio: 1;
}
}
}
} }
} }
</style> </style>

View file

@ -1,5 +1,5 @@
<template> <template>
<header v-if="data" class="site-header"> <header v-if="data" class="site-header layout-grid-inner">
<img class="logo" src="/images/output_pixelated.gif" /> <img class="logo" src="/images/output_pixelated.gif" />
<div class="info theme-red"> <div class="info theme-red">
@ -24,7 +24,6 @@ const { data } = await useStrapiGlobal()
right: 0; right: 0;
pointer-events: none; pointer-events: none;
padding: desktop-vw(27px); padding: desktop-vw(27px);
display: flex;
align-items: flex-start; align-items: flex-start;
> * { > * {
@ -39,9 +38,9 @@ const { data } = await useStrapiGlobal()
.info { .info {
background: var(--theme-bg); background: var(--theme-bg);
color: var(--theme-fg); color: var(--theme-fg);
width: desktop-vw(173px);
border-radius: desktop-vw(10px); border-radius: desktop-vw(10px);
padding: desktop-vw(12px) desktop-vw(10px); padding: desktop-vw(12px) desktop-vw(10px);
grid-column: 2 / span 2;
.copy { .copy {
margin-bottom: desktop-vw(18px); margin-bottom: desktop-vw(18px);

View file

@ -14,6 +14,11 @@ const props = defineProps({
<style lang="scss"> <style lang="scss">
.strapi-content { .strapi-content {
> * {
margin-top: 1em;
margin-bottom: 1em;
}
> *:first-child { > *:first-child {
margin-top: 0 !important; margin-top: 0 !important;
} }

View file

@ -0,0 +1,131 @@
<template>
<div
v-if="image"
:class="['strapi-media', { 'fill-space': fillSpace }, `fit-${fit}`]"
:style="styles"
>
<div class="image-sizer">
<img
:src="addURLBase(image.url)"
:srcset="srcset"
:sizes="sizes"
:alt="image.alternativeText || ''"
:width="image.width"
:height="image.height"
loading="lazy"
decoding="async"
/>
</div>
</div>
</template>
<script setup>
import { breakpoints } from '@/libs/theme'
import { computed } from 'vue'
const props = defineProps({
field: Object,
aspect: {
type: [String, Number],
default: () => -1,
},
fillSpace: {
type: Boolean,
default: () => false,
},
fit: {
type: String,
default: () => 'cover',
},
mobileSize: {
type: String,
default: () => '100vw',
},
desktopSize: {
type: String,
default: () => '100vw',
},
})
const addURLBase = (path) => `https://cms.nicwands.com${path}`
// const image = computed(() => props.field.image)
const image = computed(() => props.field)
const formats = computed(() => image.value?.formats || {})
const ratio = computed(() => {
if (!image.value) return 1
return image.value.height / image.value.width
})
const aspect = computed(() => {
// calculate if no aspect provided
if (props.aspect === -1) {
return ratio.value * 100
}
// otherwise, parse provided aspect, handling both 56.25 and 0.5625 style
const toParse = parseFloat(props.aspect)
return toParse <= 1 ? toParse * 100 : toParse
})
const styles = computed(() => ({ '--aspect': aspect.value + '%' }))
const srcset = computed(() =>
Object.values(formats.value)
.map((f) => `${addURLBase(f.url)} ${f.width}w`)
.join(', '),
)
const sizes = computed(
() =>
`(max-width: ${breakpoints.mobile}) ${props.mobileSize}, ${props.desktopSize}`,
)
</script>
<style lang="scss">
.strapi-media {
position: relative;
width: 100%;
overflow: hidden;
.image-sizer {
overflow: hidden;
padding-bottom: var(--aspect);
& > * {
display: block;
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
}
.svg {
background-size: cover;
background-repeat: no-repeat;
}
}
// fill space
&.fill-space {
position: absolute;
bottom: 0;
right: 0;
left: 0;
top: 0;
.image-sizer {
padding-bottom: 0;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
// fits
&.fit-cover .image-sizer > * {
object-fit: cover;
}
&.fit-contain .image-sizer > * {
object-fit: contain;
}
}
</style>

View file

@ -21,15 +21,15 @@
$layout: ( $layout: (
'columns-count': ( 'columns-count': (
5, 5,
18, 15,
), ),
'columns-gap': ( 'columns-gap': (
20px, 20px,
20px, 20px,
), ),
'margin': ( 'margin': (
30px, 20px,
60px, 20px,
), ),
); );