Updates, inlining, fix for container restarts
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/push/deploy unknown status

This commit is contained in:
2026-02-14 19:03:32 -03:00
parent bb0fb34656
commit c01ef1efcc
21 changed files with 1131 additions and 606 deletions

View File

@@ -1 +1,6 @@
.pnpm-store
node_modules node_modules
pnpm-lock.yaml
dist

View File

@@ -1,6 +1,15 @@
{ {
"semi": true, "semi": false,
"singleQuote": true, "singleQuote": true,
"tabWidth": 2, "tabWidth": 4,
"trailingComma": "es5" "trailingComma": "none",
"overrides": [
{
"files": "*.yml",
"options": {
"tabWidth": 4,
"proseWrap": "preserve"
}
}
]
} }

View File

@@ -1,4 +0,0 @@
{
"extends": ["stylelint-config-recommended"],
"ignoreFiles": ["node_modules/**"]
}

View File

@@ -10,6 +10,14 @@ depends_on:
- ci - ci
steps: steps:
- name: Site build
image: node:22-alpine
commands:
- corepack enable
- corepack prepare pnpm@10.29.2 --activate
- pnpm install --frozen-lockfile
- pnpm build
- name: Docker image build - name: Docker image build
image: docker:latest image: docker:latest
environment: environment:

View File

@@ -1,10 +1,4 @@
# Armandine gallery static site served by Nginx
FROM nginx:alpine FROM nginx:alpine
# Copy static site into default Nginx docroot COPY nginx/conf.d/ /etc/nginx/conf.d/
COPY src/ /usr/share/nginx/html/ COPY dist/ /usr/share/nginx/html/
# Optional: custom nginx config could be COPY'd here
# COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

View File

@@ -2,6 +2,7 @@ services:
mifi-holdings-armandine-gallery: mifi-holdings-armandine-gallery:
image: git.mifi.dev/mifi-holdings/armandine:latest image: git.mifi.dev/mifi-holdings/armandine:latest
container_name: mifi-holdings-armandine container_name: mifi-holdings-armandine
restart: unless-stopped
networks: networks:
- marina-net - marina-net
labels: labels:
@@ -9,7 +10,7 @@ services:
- "traefik.docker.network=marina-net" - "traefik.docker.network=marina-net"
- "traefik.http.routers.armandine-gallery.rule=Host(`armandine.mifi.holdings`)" - "traefik.http.routers.armandine-gallery.rule=Host(`armandine.mifi.holdings`)"
- "traefik.http.routers.armandine-gallery.entrypoints=websecure" - "traefik.http.routers.armandine-gallery.entrypoints=websecure"
- "traefik.http.routers.armandine-gallery.middlewares=security-prison@file" - "traefik.http.routers.armandine-gallery.middlewares=security-supermax-with-analytics@file"
- "traefik.http.routers.armandine-gallery.tls=true" - "traefik.http.routers.armandine-gallery.tls=true"
- "traefik.http.routers.armandine-gallery.tls.certresolver=letsencrypt" - "traefik.http.routers.armandine-gallery.tls.certresolver=letsencrypt"
- "traefik.http.services.armandine-gallery.loadbalancer.server.port=80" - "traefik.http.services.armandine-gallery.loadbalancer.server.port=80"

View File

@@ -1,26 +1,20 @@
import js from '@eslint/js'; import prettierConfig from 'eslint-config-prettier/flat'
import prettier from 'eslint-config-prettier';
export default [ export default [
{ {
files: ['src/**/*.js'], files: ['src/**/*.js'],
...js.configs.recommended, languageOptions: {
languageOptions: { ecmaVersion: 'latest',
ecmaVersion: 'latest', sourceType: 'script',
sourceType: 'script', globals: {
globals: { window: 'readonly',
document: 'readonly', document: 'readonly',
window: 'readonly', dataLayer: 'writable'
localStorage: 'readonly', }
console: 'readonly', },
fetch: 'readonly', rules: {
Image: 'readonly', 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }]
CustomEvent: 'readonly', }
},
}, },
rules: { prettierConfig
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], ]
},
},
prettier,
];

View File

@@ -3,25 +3,35 @@
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"private": true, "private": true,
"packageManager": "pnpm@10.29.2", "packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc",
"description": "Armandine gallery static Nginx site", "description": "Armandine gallery static Nginx site",
"scripts": { "scripts": {
"build": "docker build -t git.mifi.dev/mifi-holdings/armandine:latest .", "build": "node scripts/build.js",
"push": "docker push git.mifi.dev/mifi-holdings/armandine:latest", "docker:build": "docker build --platform linux/amd64 -t git.mifi.dev/mifi-holdings/landing:latest .",
"lint": "pnpm lint:js && pnpm lint:css", "docker:push": "docker push git.mifi.dev/mifi-holdings/landing:latest",
"lint:js": "eslint src",
"lint:css": "stylelint \"src/**/*.css\"",
"format": "prettier --write \"src/**/*.{html,css,js,json}\"", "format": "prettier --write \"src/**/*.{html,css,js,json}\"",
"format:check": "prettier --check \"src/**/*.{html,css,js,json}\"", "format:check": "prettier --check \"src/**/*.{html,css,js,json}\"",
"serve": "pnpm exec serve src -p 3000" "lint": "pnpm run lint:yaml && pnpm run lint:js && pnpm run lint:css",
"lint:css": "stylelint \"src/**/*.css\"",
"lint:js": "eslint src/",
"lint:yaml": "yamllint .woodpecker/ci.yml .woodpecker/build.yml .woodpecker/deploy.yml docker-compose.yml",
"lint:fix": "pnpm run lint:fix:js && pnpm run lint:fix:css && pnpm run lint:fix:yaml",
"lint:fix:js": "eslint src/ --fix",
"lint:fix:css": "stylelint \"src/**/*.css\" --fix",
"lint:fix:yaml": "yamllint .woodpecker/ci.yml .woodpecker/build.yml .woodpecker/deploy.yml docker-compose.yml --fix",
"preview": "serve src -l 3000",
"preview:prod": "pnpm build && serve dist -l 3000"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.15.0", "clean-css": "^5.3.3",
"eslint": "^9.15.0", "beasties": "^0.4.1",
"eslint-config-prettier": "^9.1.0", "eslint": "^10.0.0",
"prettier": "^3.3.3", "eslint-config-prettier": "^10.1.8",
"prettier": "^3.4.2",
"serve": "^14.2.4", "serve": "^14.2.4",
"stylelint": "^16.10.0", "stylelint": "^17.3.0",
"stylelint-config-recommended": "^14.0.0" "stylelint-config-standard": "^40.0.0",
"terser": "^5.46.0",
"yaml-lint": "^1.7.0"
} }
} }

784
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

78
scripts/build.js Normal file
View File

@@ -0,0 +1,78 @@
/**
* Build script: copy src → dist, minify JS/CSS, inline critical CSS (Beasties).
* Run with: pnpm build
*/
import {
rmSync,
mkdirSync,
readFileSync,
writeFileSync,
cpSync,
readdirSync
} from 'fs'
import { join, dirname, extname } from 'path'
import { fileURLToPath } from 'url'
import Beasties from 'beasties'
import { minify as minifyJs } from 'terser'
import CleanCSS from 'clean-css'
const __dirname = dirname(fileURLToPath(import.meta.url))
const root = join(__dirname, '..')
const srcDir = join(root, 'src')
const distDir = join(root, 'dist')
function getFiles(dir, files = []) {
const entries = readdirSync(dir, { withFileTypes: true })
for (const e of entries) {
const full = join(dir, e.name)
if (e.isDirectory()) getFiles(full, files)
else files.push(full)
}
return files
}
async function main() {
// 1. Clean and copy src → dist
rmSync(distDir, { recursive: true, force: true })
mkdirSync(distDir, { recursive: true })
cpSync(srcDir, distDir, { recursive: true })
const distFiles = getFiles(distDir)
// 2. Minify JS
const jsFiles = distFiles.filter((f) => extname(f) === '.js')
for (const f of jsFiles) {
const code = readFileSync(f, 'utf8')
const result = await minifyJs(code, { format: { comments: false } })
if (result.code) writeFileSync(f, result.code)
}
// 3. Minify CSS
const cleanCss = new CleanCSS({ level: 2 })
const cssFiles = distFiles.filter((f) => extname(f) === '.css')
for (const f of cssFiles) {
const code = readFileSync(f, 'utf8')
const result = cleanCss.minify(code)
if (!result.errors.length) writeFileSync(f, result.styles)
}
// 4. Inline critical CSS with Beasties for all HTML files (no browser; works in CI)
const htmlFiles = distFiles.filter((f) => extname(f) === '.html')
const beasties = new Beasties({
path: distDir,
preload: 'default',
logLevel: 'warn'
})
for (const htmlFile of htmlFiles) {
const html = readFileSync(htmlFile, 'utf8')
const inlined = await beasties.process(html)
writeFileSync(htmlFile, inlined)
}
console.log('Build complete: dist/')
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -1,129 +1,145 @@
:root { :root {
--bg: #fff; --bg: #fff;
--fg: #222; --fg: #222;
--accent: #007acc; --accent: #007acc;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--bg: #111;
--fg: #eee;
--accent: #46c;
}
}
/* Explicit theme toggle overrides (win over media query when set) */
html.dark {
--bg: #111; --bg: #111;
--fg: #eee; --fg: #eee;
--accent: #46c; --accent: #46c;
}
}
/* Explicit theme toggle overrides (win over media query when set) */
html.dark {
--bg: #111;
--fg: #eee;
--accent: #46c;
} }
html.light { html.light {
--bg: #fff; --bg: #fff;
--fg: #222; --fg: #222;
--accent: #007acc; --accent: #007acc;
} }
body { body {
margin: 0; margin: 0;
font-family: sans-serif; font-family: sans-serif;
background-color: var(--bg); background-color: var(--bg);
color: var(--fg); color: var(--fg);
} }
.lightbox-open { .lightbox-open {
overflow: hidden; overflow: hidden;
} }
.site-header { .site-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 1rem; padding: 1rem;
background: var(--bg); background: var(--bg);
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
} }
.emoji-button { .emoji-button {
background: none; background: none;
border: none; border: none;
font-size: 1.5rem; font-size: 1.5rem;
cursor: pointer; cursor: pointer;
color: var(--accent); color: var(--accent);
} }
.gallery-grid { .gallery-grid {
column-count: 1; column-count: 1;
column-gap: 1rem; column-gap: 1rem;
padding: 1rem; padding: 1rem;
} }
@media (min-width: 768px) {
.gallery-grid { @media (width >= 768px) {
column-count: 2; .gallery-grid {
} column-count: 2;
}
} }
@media (min-width: 1024px) {
.gallery-grid { @media (width >= 1024px) {
column-count: 3; .gallery-grid {
} column-count: 3;
}
} }
.gallery-item { .gallery-item {
margin: 0 0 1rem; margin: 0 0 1rem;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
} }
.gallery-item img { .gallery-item img {
width: 100%; width: 100%;
display: block; display: block;
border-radius: 8px; border-radius: 8px;
} }
.gallery-item figcaption { .gallery-item figcaption {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
width: 100%; width: 100%;
padding: 0.5rem; padding: 0.5rem;
background: rgba(0, 0, 0, 0.6); background: rgb(0 0 0 / 60%);
color: #fff; color: #fff;
font-size: 0.9rem; font-size: 0.9rem;
opacity: 0; opacity: 0;
transition: opacity 0.2s; transition: opacity 0.2s;
} }
.gallery-item:focus figcaption, .gallery-item:focus figcaption,
.gallery-item:hover figcaption { .gallery-item:hover figcaption {
opacity: 1; opacity: 1;
} }
#lightbox { #lightbox {
position: fixed; position: fixed;
top: 0; inset: 0;
left: 0; background: rgb(0 0 0 / 90%);
right: 0; display: flex;
bottom: 0; flex-direction: column;
background: rgba(0, 0, 0, 0.9); align-items: center;
display: flex; justify-content: center;
flex-direction: column; visibility: hidden;
align-items: center; opacity: 0;
justify-content: center; transition: opacity 0.3s;
visibility: hidden;
opacity: 0;
transition: opacity 0.3s;
} }
#lightbox[aria-hidden='false'] { #lightbox[aria-hidden='false'] {
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
} }
#lb-content img, #lb-content img,
#lb-content video { #lb-content video {
max-width: 90vw; max-width: 90vw;
max-height: 80vh; max-height: 80vh;
border-radius: 8px; border-radius: 8px;
} }
#lb-caption { #lb-caption {
color: #fff; color: #fff;
margin-top: 0.5rem; margin-top: 0.5rem;
text-align: center; text-align: center;
max-width: 90vw; max-width: 90vw;
} }
#lb-close { #lb-close {
position: absolute; position: absolute;
top: 1rem; top: 1rem;
right: 1rem; right: 1rem;
background: none; background: none;
border: none; border: none;
font-size: 2rem; font-size: 2rem;
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
src/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

11
src/assets/js/ga-init.js Normal file
View File

@@ -0,0 +1,11 @@
;(function () {
var script = document.currentScript
var id = script && script.getAttribute('data-ga-id')
if (!id) return
window.dataLayer = window.dataLayer || []
function gtag() {
window.dataLayer.push(arguments)
}
gtag('js', new Date())
gtag('config', id, { anonymize_ip: true })
})()

View File

@@ -1,115 +1,115 @@
// --- theme toggle --- // --- theme toggle ---
const toggle = document.getElementById('theme-toggle'); const toggle = document.getElementById('theme-toggle')
const root = document.documentElement; const root = document.documentElement
const saved = localStorage.getItem('dark-mode'); const saved = localStorage.getItem('dark-mode')
const sysDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const sysDark = window.matchMedia('(prefers-color-scheme: dark)').matches
if (saved === 'true') { if (saved === 'true') {
root.classList.add('dark'); root.classList.add('dark')
root.classList.remove('light'); root.classList.remove('light')
} else if (saved === 'false') { } else if (saved === 'false') {
root.classList.add('light'); root.classList.add('light')
root.classList.remove('dark'); root.classList.remove('dark')
} else { } else {
if (sysDark) { if (sysDark) {
root.classList.add('dark'); root.classList.add('dark')
root.classList.remove('light'); root.classList.remove('light')
} else { } else {
root.classList.add('light'); root.classList.add('light')
root.classList.remove('dark'); root.classList.remove('dark')
} }
} }
toggle.addEventListener('click', () => { toggle.addEventListener('click', () => {
const isDark = root.classList.contains('dark'); const isDark = root.classList.contains('dark')
root.classList.toggle('dark', !isDark); root.classList.toggle('dark', !isDark)
root.classList.toggle('light', isDark); root.classList.toggle('light', isDark)
localStorage.setItem('dark-mode', !isDark); localStorage.setItem('dark-mode', !isDark)
}); })
const body = document.body; const body = document.body
// --- lightbox base --- // --- lightbox base ---
const lb = document.getElementById('lightbox'); const lb = document.getElementById('lightbox')
const lbCnt = document.getElementById('lb-content'); const lbCnt = document.getElementById('lb-content')
const lbCap = document.getElementById('lb-caption'); const lbCap = document.getElementById('lb-caption')
document.getElementById('lb-close').addEventListener('click', () => { document.getElementById('lb-close').addEventListener('click', () => {
lb.setAttribute('aria-hidden', 'true'); lb.setAttribute('aria-hidden', 'true')
body.classList.remove('lightbox-open'); body.classList.remove('lightbox-open')
lbCnt.innerHTML = ''; lbCnt.innerHTML = ''
}); })
// --- build gallery --- // --- build gallery ---
const gallery = document.getElementById('gallery'); const gallery = document.getElementById('gallery')
const mediaData = JSON.parse(document.getElementById('media-data').textContent); const mediaData = JSON.parse(document.getElementById('media-data').textContent)
const createPicture = (item) => { const createPicture = (item) => {
const pic = document.createElement('picture'); const pic = document.createElement('picture')
['desktop', 'tablet', 'mobile'].forEach((bp) => { ;['desktop', 'tablet', 'mobile'].forEach((bp) => {
const src = document.createElement('source'); const src = document.createElement('source')
const widthQuery = bp === 'desktop' ? 1024 : bp === 'tablet' ? 768 : 0; const widthQuery = bp === 'desktop' ? 1024 : bp === 'tablet' ? 768 : 0
src.media = `(min-width:${widthQuery}px)`; src.media = `(min-width:${widthQuery}px)`
if (item.type === 'image') { if (item.type === 'image') {
src.srcset = src.srcset =
`assets/media/${bp}/${item.name}@1x.webp 1x, ` + `assets/media/${bp}/${item.name}@1x.webp 1x, ` +
`assets/media/${bp}/${item.name}.webp 2x`; `assets/media/${bp}/${item.name}.webp 2x`
} else { } else {
// video poster still // video poster still
src.srcset = src.srcset =
`assets/media/${bp}/${item.name}_still@1x.webp 1x, ` + `assets/media/${bp}/${item.name}_still@1x.webp 1x, ` +
`assets/media/${bp}/${item.name}_still.webp 2x`; `assets/media/${bp}/${item.name}_still.webp 2x`
} }
pic.appendChild(src); pic.appendChild(src)
}); })
// thumbnail fallback (always 300px/2×) // thumbnail fallback (always 300px/2×)
const img = document.createElement('img'); const img = document.createElement('img')
img.src = `assets/media/thumbnail/${item.name}.webp`; img.src = `assets/media/thumbnail/${item.name}.webp`
img.alt = item.alt.replace(/[]/g, ''); img.alt = item.alt.replace(/[]/g, '')
pic.appendChild(img); pic.appendChild(img)
return pic; return pic
}; }
mediaData.forEach((item) => { mediaData.forEach((item) => {
const fig = document.createElement('figure'); const fig = document.createElement('figure')
fig.className = `gallery-item${item.type === 'video' ? ' video' : ''}`; fig.className = `gallery-item${item.type === 'video' ? ' video' : ''}`
fig.tabIndex = 0; fig.tabIndex = 0
fig.dataset.name = item.name; fig.dataset.name = item.name
fig.dataset.type = item.type; fig.dataset.type = item.type
fig.dataset.caption = item.caption; fig.dataset.caption = item.caption
fig.appendChild(createPicture(item)); fig.appendChild(createPicture(item))
// overlay caption // overlay caption
const cap = document.createElement('figcaption'); const cap = document.createElement('figcaption')
cap.textContent = item.caption; cap.textContent = item.caption
fig.appendChild(cap); fig.appendChild(cap)
// events // events
fig.addEventListener('click', () => openLightbox(item)); fig.addEventListener('click', () => openLightbox(item))
fig.addEventListener( fig.addEventListener(
'keypress', 'keypress',
(e) => e.key === 'Enter' && openLightbox(item) (e) => e.key === 'Enter' && openLightbox(item)
); )
gallery.appendChild(fig); gallery.appendChild(fig)
}); })
// --- video toggle --- // --- video toggle ---
const videoTgl = document.getElementById('show_video'); const videoTgl = document.getElementById('show_video')
videoTgl.addEventListener('click', () => { videoTgl.addEventListener('click', () => {
openLightbox(mediaData.find((i) => i.type === 'video')); openLightbox(mediaData.find((i) => i.type === 'video'))
}); })
function openLightbox(item) { function openLightbox(item) {
lbCnt.innerHTML = ''; lbCnt.innerHTML = ''
if (item.type === 'video') { if (item.type === 'video') {
const v = document.createElement('video'); const v = document.createElement('video')
v.src = `assets/media/videos/${item.name}.mp4`; v.src = `assets/media/videos/${item.name}.mp4`
v.controls = true; v.controls = true
v.autoplay = true; v.autoplay = true
lbCnt.appendChild(v); lbCnt.appendChild(v)
} else { } else {
lbCnt.appendChild(createPicture(item)); lbCnt.appendChild(createPicture(item))
} }
lbCap.textContent = item.caption; lbCap.textContent = item.caption
body.classList.add('lightbox-open'); body.classList.add('lightbox-open')
lb.setAttribute('aria-hidden', 'false'); lb.setAttribute('aria-hidden', 'false')
} }

View File

@@ -1,184 +1,195 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <!-- Google tag (gtag.js) -->
<meta name="viewport" content="width=device-width,initial-scale=1" /> <script
<title>64 Armandine St #3 Boston, Massachusetts</title> async
<link src="https://www.googletagmanager.com/gtag/js?id=G-QZGFK4MDT4"
rel="icon" ></script>
type="image/png" <script
sizes="32x32" defer
href="assets/favicon-32x32.png" src="/assets/js/ga-init.js"
/> data-ga-id="G-QZGFK4MDT4"
<link ></script>
rel="icon"
type="image/png"
sizes="16x16"
href="assets/favicon-16x16.png"
/>
<link rel="icon" type="image/x-icon" href="assets/favicon.ico" />
<link rel="stylesheet" href="assets/css/style.css" />
</head>
<body>
<header class="site-header">
<h1>64 Armandine St #3 Boston, Massachusetts</h1>
<div class="buttons">
<button
id="show_video"
class="emoji-button"
aria-label="Show video tour"
>
🎥
</button>
<button
id="theme-toggle"
class="emoji-button"
aria-label="Toggle light/dark theme"
>
🌓
</button>
</div>
</header>
<main> <meta charset="UTF-8" />
<section id="gallery" class="gallery-grid"> <meta name="viewport" content="width=device-width,initial-scale=1" />
<!-- gallery items are injected here by script.js --> <title>64 Armandine St #3 Boston, Massachusetts</title>
</section> <link
</main> rel="icon"
type="image/png"
sizes="32x32"
href="/assets/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/assets/favicon-16x16.png"
/>
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
<link rel="stylesheet" href="/assets/css/style.css" />
</head>
<body>
<header class="site-header">
<h1>64 Armandine St #3 Boston, Massachusetts</h1>
<div class="buttons">
<button
id="show_video"
class="emoji-button"
aria-label="Show video tour"
>
🎥
</button>
<button
id="theme-toggle"
class="emoji-button"
aria-label="Toggle light/dark theme"
>
🌓
</button>
</div>
</header>
<!-- Lightbox --> <main>
<div id="lightbox" aria-hidden="true"> <section id="gallery" class="gallery-grid">
<button id="lb-close" aria-label="Close">&times;</button> <!-- gallery items are injected here by script.js -->
<div id="lb-content"></div> </section>
<p id="lb-caption"></p> </main>
</div>
<!-- Your media manifest: list each file (no extension), type, and caption --> <!-- Lightbox -->
<script id="media-data" type="application/json"> <div id="lightbox" aria-hidden="true">
[ <button id="lb-close" aria-label="Close">&times;</button>
{ <div id="lb-content"></div>
"type": "image", <p id="lb-caption"></p>
"name": "living_room_1", </div>
"caption": "An inviting blend of comfort and curated art—relaxation guaranteed.",
"alt": "Sunny living room with stylish seating and vibrant artwork."
},
{
"type": "image",
"name": "living_room_2",
"caption": "Relaxation elevated—your stylish living space awaits.",
"alt": "Spacious living area featuring elegant furniture and tasteful decor."
},
{
"type": "image",
"name": "kitchen",
"caption": "The culinary stage is set—snacking encouraged, style required.",
"alt": "Modern kitchen showcasing sleek appliances and contemporary design."
},
{
"type": "image",
"name": "bedroom_suite_1",
"caption": "A bedroom suite designed to make snoozing irresistible.",
"alt": "Inviting bedroom suite with cozy bedding and warm lighting."
},
{
"type": "image",
"name": "bedroom_suite_2",
"caption": "Style meets comfort—sleeping in has never been easier.",
"alt": "Comfortable bedroom suite with elegant decor and soft tones."
},
{
"type": "image",
"name": "bedroom_suite_3",
"caption": "Where dreams get stylish—a bedroom that feels like home.",
"alt": "Welcoming bedroom with soothing colors and inviting ambiance."
},
{
"type": "image",
"name": "guest_bath",
"caption": "Your personal spa experience—right down the hall.",
"alt": "Sophisticated guest bathroom with modern fixtures and clean lines."
},
{
"type": "image",
"name": "onsuite_1",
"caption": "Luxury meets practicality—your private ensuite awaits.",
"alt": "Private ensuite bathroom featuring contemporary design and premium finishes."
},
{
"type": "image",
"name": "onsuite_2",
"caption": "Everyday luxury, right at home—your ensuite oasis.",
"alt": "Elegant ensuite with sleek fixtures and stylish decor."
},
{
"type": "image",
"name": "laundry",
"caption": "Laundry day reimagined—functional never looked so good.",
"alt": "Modern laundry room with washer, dryer, and organized storage."
},
{
"type": "image",
"name": "coat_closet",
"caption": "Organized and chic—your entryway's best friend.",
"alt": "Convenient coat closet with tidy storage solutions."
},
{
"type": "image",
"name": "deck_1",
"caption": "Outdoor comfort, just steps away—morning coffee optional.",
"alt": "Sunny deck with cozy seating and pleasant outdoor views."
},
{
"type": "image",
"name": "deck_2",
"caption": "Your fresh-air escape—ideal for relaxing evenings.",
"alt": "Comfortable deck area perfect for unwinding or entertaining."
},
{
"type": "image",
"name": "exterior",
"caption": "Curb appeal perfected—your new favorite place starts here.",
"alt": "Attractive home exterior with inviting architecture."
},
{
"type": "image",
"name": "backyard_parking",
"caption": "Convenience meets privacy—your personal backyard parking spot.",
"alt": "Private backyard parking area offering secure convenience."
},
{
"type": "image",
"name": "office_fitness_guest_1",
"caption": "Productivity zone meets fitness corner—multitasking done right.",
"alt": "Dual-purpose room featuring office setup and fitness equipment."
},
{
"type": "image",
"name": "office_fitness_guest_2",
"caption": "Work, workout, or unwind—the room of endless possibilities.",
"alt": "Versatile office and fitness area with modern amenities."
},
{
"type": "image",
"name": "office_fitness_guest_3",
"caption": "Stay focused or get fit—you decide.",
"alt": "Functional space combining a workspace and home fitness area."
},
{
"type": "image",
"name": "office_fitness_guest_4",
"caption": "Room for every routine—your workspace meets wellness.",
"alt": "Stylish office area seamlessly integrated with fitness features."
},
{
"type": "video",
"name": "tour",
"caption": "Take the scenic route—explore your the home's highlights with a virtual walkthrough.",
"alt": "Video tour showcasing the property."
}
]
</script>
<script src="assets/js/script.js"></script> <!-- Your media manifest: list each file (no extension), type, and caption -->
</body> <script id="media-data" type="application/json">
[
{
"type": "image",
"name": "living_room_1",
"caption": "An inviting blend of comfort and curated art—relaxation guaranteed.",
"alt": "Sunny living room with stylish seating and vibrant artwork."
},
{
"type": "image",
"name": "living_room_2",
"caption": "Relaxation elevated—your stylish living space awaits.",
"alt": "Spacious living area featuring elegant furniture and tasteful decor."
},
{
"type": "image",
"name": "kitchen",
"caption": "The culinary stage is set—snacking encouraged, style required.",
"alt": "Modern kitchen showcasing sleek appliances and contemporary design."
},
{
"type": "image",
"name": "bedroom_suite_1",
"caption": "A bedroom suite designed to make snoozing irresistible.",
"alt": "Inviting bedroom suite with cozy bedding and warm lighting."
},
{
"type": "image",
"name": "bedroom_suite_2",
"caption": "Style meets comfort—sleeping in has never been easier.",
"alt": "Comfortable bedroom suite with elegant decor and soft tones."
},
{
"type": "image",
"name": "bedroom_suite_3",
"caption": "Where dreams get stylish—a bedroom that feels like home.",
"alt": "Welcoming bedroom with soothing colors and inviting ambiance."
},
{
"type": "image",
"name": "guest_bath",
"caption": "Your personal spa experience—right down the hall.",
"alt": "Sophisticated guest bathroom with modern fixtures and clean lines."
},
{
"type": "image",
"name": "onsuite_1",
"caption": "Luxury meets practicality—your private ensuite awaits.",
"alt": "Private ensuite bathroom featuring contemporary design and premium finishes."
},
{
"type": "image",
"name": "onsuite_2",
"caption": "Everyday luxury, right at home—your ensuite oasis.",
"alt": "Elegant ensuite with sleek fixtures and stylish decor."
},
{
"type": "image",
"name": "laundry",
"caption": "Laundry day reimagined—functional never looked so good.",
"alt": "Modern laundry room with washer, dryer, and organized storage."
},
{
"type": "image",
"name": "coat_closet",
"caption": "Organized and chic—your entryway's best friend.",
"alt": "Convenient coat closet with tidy storage solutions."
},
{
"type": "image",
"name": "deck_1",
"caption": "Outdoor comfort, just steps away—morning coffee optional.",
"alt": "Sunny deck with cozy seating and pleasant outdoor views."
},
{
"type": "image",
"name": "deck_2",
"caption": "Your fresh-air escape—ideal for relaxing evenings.",
"alt": "Comfortable deck area perfect for unwinding or entertaining."
},
{
"type": "image",
"name": "exterior",
"caption": "Curb appeal perfected—your new favorite place starts here.",
"alt": "Attractive home exterior with inviting architecture."
},
{
"type": "image",
"name": "backyard_parking",
"caption": "Convenience meets privacy—your personal backyard parking spot.",
"alt": "Private backyard parking area offering secure convenience."
},
{
"type": "image",
"name": "office_fitness_guest_1",
"caption": "Productivity zone meets fitness corner—multitasking done right.",
"alt": "Dual-purpose room featuring office setup and fitness equipment."
},
{
"type": "image",
"name": "office_fitness_guest_2",
"caption": "Work, workout, or unwind—the room of endless possibilities.",
"alt": "Versatile office and fitness area with modern amenities."
},
{
"type": "image",
"name": "office_fitness_guest_3",
"caption": "Stay focused or get fit—you decide.",
"alt": "Functional space combining a workspace and home fitness area."
},
{
"type": "image",
"name": "office_fitness_guest_4",
"caption": "Room for every routine—your workspace meets wellness.",
"alt": "Stylish office area seamlessly integrated with fitness features."
},
{
"type": "video",
"name": "tour",
"caption": "Take the scenic route—explore your the home's highlights with a virtual walkthrough.",
"alt": "Video tour showcasing the property."
}
]
</script>
<script src="assets/js/script.js"></script>
</body>
</html> </html>

8
stylelint.config.js Normal file
View File

@@ -0,0 +1,8 @@
export default {
extends: ['stylelint-config-standard'],
overrides: [
{
files: ['src/**/*.css']
}
]
}