From 540da303c88e9ad4cc3691c71e032b37b44d66a0 Mon Sep 17 00:00:00 2001 From: mr150 Date: Wed, 29 Jul 2026 19:38:14 +0300 Subject: [PATCH 1/4] chore: add agents.md --- AGENTS.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..16b277f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# AGENTS Guide + +## Project Overview + +- **Name:** mlut website +- **Stack:** Eleventy, mlut, ejs, Sass, Javascript +- **Architecture:** SSG (Static site generation) + +## Project Structure + +- `src/` - editable sources +- `src/_includes/` - partials and reusable templates +- `src/_data/` - global Eleventy data +- `src/assets/` - styles, scripts, images, and other static assets +- `src/layouts/` - reusable generic layouts for pages +- `src/arts/` - pure CSS arts +- `dist/` - generated output + +## Setup and commands + +- **Install dependencies:** `npm i` +- **Build project:** `npm run build` +- **Start dev server:** `npm start` +- **Start mlut CLI in watch mode:** `npm run mlut` + +## Working rules + +- Always try to address the cause, not the effect +- Use modern vanilla JS with custom elements and without frameworks. See examples of components in `src/assets/script/` +- Use Atomic CSS aproach with mlut for writing styles +- Do not install new third-party packages without asking the user first From 9b293d9e91438b76541bf96687458b22569426bc Mon Sep 17 00:00:00 2001 From: mr150 Date: Wed, 29 Jul 2026 19:39:31 +0300 Subject: [PATCH 2/4] feat(art): add masonry layout --- src/_data/arts.json | 33 ++++++++++++++--------- src/arts.ejs | 52 +++++++++++++++++++++++++++++++------ src/assets/style/style.scss | 21 +++++++++++++++ 3 files changed, 86 insertions(+), 20 deletions(-) diff --git a/src/_data/arts.json b/src/_data/arts.json index daa21d9..0199483 100644 --- a/src/_data/arts.json +++ b/src/_data/arts.json @@ -2,52 +2,61 @@ { "artId":"ghost", "bgColor":"Bgc-$artGhost", - "showInMain": true + "showInMain": true, + "size":"standard" }, { "artId":"flushed-emoji", "bgColor":"Bgc-$artFlushedEmoji", - "showInMain": true + "showInMain": true, + "size":"standard" }, { "artId":"penrose-triangle", "bgColor":"Bgc-$artPenroseTriangle", - "showInMain": true + "showInMain": true, + "size":"wide" }, { "artId":"exploding-head", "bgColor":"Bgc-$artExplodingHead", - "showInMain": true + "showInMain": true, + "size":"wide" }, { "artId":"clown-emoji", "bgColor":"Bgc-$artClownEmoji", - "showInMain": false + "showInMain": false, + "size":"standard" }, { "artId":"mushroom", "bgColor":"Bgc-$artMushroom", - "showInMain": false + "showInMain": false, + "size":"tall" }, { "artId":"robot-hare", "bgColor":"Bgc-$artRobotHare", - "showInMain": true + "showInMain": true, + "size":"tall" }, { "artId":"funny-cake", "bgColor":"Bgc-$artFunnyCake", - "showInMain": false + "showInMain": false, + "size":"wide" }, { "artId":"cat", "bgColor":"Bgc-$artCat", - "showInMain": false + "showInMain": false, + "size":"wide" }, { "artId":"corgi", "bgColor":"Bgc-$artCorgi", - "showInMain": true + "showInMain": true, + "size":"wide" } - -] \ No newline at end of file +] diff --git a/src/arts.ejs b/src/arts.ejs index 6e849c4..80efb3f 100644 --- a/src/arts.ejs +++ b/src/arts.ejs @@ -4,23 +4,59 @@ area: Arts --- <% const artsPageCSS = { - card:"W100p H20gg Tsd-$shortTs @:hv_Tsd-$longTs", - artWrapper:"-Sz100p Ov-h Plcc-c Plci-c D-f", + card:"D H20gg Bgc-$core800 Tsd-$shortTs @:hv_Tsd-$longTs", button: "W12gg lg_W9gg Mxw50u xl_Mxw55u Ai-str" } + const tileSizes = { + standard: [1, 2], + wide: [2, 2], + tall: [1, 4] + } + const masonryItems = [...it.arts, { artId: 'add-art', size: 'standard' }] + const getCells = (row, column, width, height) => Array.from( + { length: width * height }, + (_, index) => `${row + Math.floor(index / width)}:${column + (index % width)}` + ) + + function packMasonry(columns) { + const occupied = new Set() + + return masonryItems.reduce((layout, item) => { + const [itemWidth, height] = tileSizes[item.size] || tileSizes.standard + const width = Math.min(itemWidth, columns) + + for (let row = 1; ; row += 1) { + for (let column = 1; column <= columns - width + 1; column += 1) { + const cells = getCells(row, column, width, height) + if (cells.some((cell) => occupied.has(cell))) continue - function setCtaLength(){ - return it.arts.length % 2 != 0 ? 'md_W50p' : '' + cells.forEach((cell) => occupied.add(cell)) + layout[item.artId] = { column, row, width, height } + return layout + } + } + }, {}) } + const masonryLayouts = { + md: packMasonry(2), + xl: packMasonry(4) + } + + function getMasonryStyle(artId) { + return Object.entries(masonryLayouts).map(([breakpoint, layout]) => { + const { column, row, width, height } = layout[artId] + return `--masonry-column-${breakpoint}:${column}/span ${width};--masonry-row-${breakpoint}:${row}/span ${height};` + }).join('') + } %>

Pure CSS art

-
- <% it.arts.forEach((art, index) => {%> - +
+ <% it.arts.forEach((art) => {%> +
<%- include(`./arts/${art.artId}.ejs`) %>
@@ -45,7 +81,7 @@ area: Arts
<% }) %> -
+
diff --git a/src/assets/style/style.scss b/src/assets/style/style.scss index 2f8c2e8..79dce5b 100644 --- a/src/assets/style/style.scss +++ b/src/assets/style/style.scss @@ -240,3 +240,24 @@ html, .theme-dark { @include light-theme; } } + +art-panel[size], +.arts-masonry-cta { + min-width: 0; +} + +@media (min-width: 768px) { + art-panel[size], + .arts-masonry-cta { + grid-column: var(--masonry-column-md); + grid-row: var(--masonry-row-md); + } +} + +@media (min-width: 1200px) { + art-panel[size], + .arts-masonry-cta { + grid-column: var(--masonry-column-xl); + grid-row: var(--masonry-row-xl); + } +} From ddc5ee35cd5841cdeb354e72b7c41e6080668286 Mon Sep 17 00:00:00 2001 From: mr150 Date: Thu, 30 Jul 2026 13:27:09 +0300 Subject: [PATCH 3/4] refactor(art): change `wide` art size on `md` screen --- src/arts.ejs | 59 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/src/arts.ejs b/src/arts.ejs index 80efb3f..db84dfe 100644 --- a/src/arts.ejs +++ b/src/arts.ejs @@ -6,48 +6,65 @@ area: Arts const artsPageCSS = { card:"D H20gg Bgc-$core800 Tsd-$shortTs @:hv_Tsd-$longTs", button: "W12gg lg_W9gg Mxw50u xl_Mxw55u Ai-str" - } + }; + + // Grid spans are expressed as [columns, rows]. const tileSizes = { standard: [1, 2], wide: [2, 2], tall: [1, 4] - } - const masonryItems = [...it.arts, { artId: 'add-art', size: 'standard' }] + }; + const colsOnScreen = { + md: 2, + xl: 4, + }; + + const masonryItems = [ + ...it.arts, + { artId: 'add-art', size: 'standard' } + ]; + + // Represent every occupied grid cell with a stable row:column key. const getCells = (row, column, width, height) => Array.from( { length: width * height }, (_, index) => `${row + Math.floor(index / width)}:${column + (index % width)}` - ) + ); function packMasonry(columns) { - const occupied = new Set() + const occupied = new Set(); + const isMdScreen = columns === colsOnScreen.md; + // Scan rows left-to-right and place each tile in the first free fitting area. return masonryItems.reduce((layout, item) => { - const [itemWidth, height] = tileSizes[item.size] || tileSizes.standard - const width = Math.min(itemWidth, columns) + const [itemWidth, height] = tileSizes[item.size] || tileSizes.standard; + const width = isMdScreen && item.size === 'wide' ? + 1 : Math.min(itemWidth, columns); for (let row = 1; ; row += 1) { for (let column = 1; column <= columns - width + 1; column += 1) { - const cells = getCells(row, column, width, height) - if (cells.some((cell) => occupied.has(cell))) continue + const cells = getCells(row, column, width, height); + if (cells.some((cell) => occupied.has(cell))) continue; - cells.forEach((cell) => occupied.add(cell)) - layout[item.artId] = { column, row, width, height } - return layout + cells.forEach((cell) => occupied.add(cell)); + layout[item.artId] = { column, row, width, height }; + return layout; } } - }, {}) + }, {}); } + // Save positions for the two responsive grid configurations. const masonryLayouts = { - md: packMasonry(2), - xl: packMasonry(4) - } + md: packMasonry(colsOnScreen.md), + xl: packMasonry(colsOnScreen.xl) + }; function getMasonryStyle(artId) { + // Serialize server-calculated positions as CSS custom properties. return Object.entries(masonryLayouts).map(([breakpoint, layout]) => { - const { column, row, width, height } = layout[artId] - return `--masonry-column-${breakpoint}:${column}/span ${width};--masonry-row-${breakpoint}:${row}/span ${height};` - }).join('') + const { column, row, width, height } = layout[artId]; + return `--masonry-column-${breakpoint}:${column}/span ${width};--masonry-row-${breakpoint}:${row}/span ${height};`; + }).join(''); } %>
@@ -55,7 +72,7 @@ area: Arts

Pure CSS art

- <% it.arts.forEach((art) => {%> + <% it.arts.forEach((art) => { %>
<%- include(`./arts/${art.artId}.ejs`) %> @@ -79,7 +96,7 @@ area: Arts
- <% }) %> + <% }); %>
Date: Thu, 30 Jul 2026 14:07:36 +0300 Subject: [PATCH 4/4] refactor(art): make `packMasonry` more readable --- src/arts.ejs | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/arts.ejs b/src/arts.ejs index db84dfe..bd08705 100644 --- a/src/arts.ejs +++ b/src/arts.ejs @@ -30,26 +30,41 @@ area: Arts (_, index) => `${row + Math.floor(index / width)}:${column + (index % width)}` ); + function getTileDimensions(size, columns) { + const [preferredWidth, height] = tileSizes[size] || tileSizes.standard; + const wideTileOnMdScreen = size === 'wide' && columns === colsOnScreen.md; + const width = wideTileOnMdScreen ? 1 : Math.min(preferredWidth, columns); + + return { width, height }; + } + + function findFirstAvailableArea(occupied, columns, width, height) { + // Preserve the row-first, then column-first placement order. + for (let row = 1; ; row += 1) { + for (let column = 1; column <= columns - width + 1; column += 1) { + const cells = getCells(row, column, width, height); + const isAvailable = cells.every((cell) => !occupied.has(cell)); + + if (isAvailable) return { column, row, cells }; + } + } + } + function packMasonry(columns) { const occupied = new Set(); - const isMdScreen = columns === colsOnScreen.md; - // Scan rows left-to-right and place each tile in the first free fitting area. return masonryItems.reduce((layout, item) => { - const [itemWidth, height] = tileSizes[item.size] || tileSizes.standard; - const width = isMdScreen && item.size === 'wide' ? - 1 : Math.min(itemWidth, columns); + const { width, height } = getTileDimensions(item.size, columns); + const { column, row, cells } = findFirstAvailableArea( + occupied, + columns, + width, + height + ); - for (let row = 1; ; row += 1) { - for (let column = 1; column <= columns - width + 1; column += 1) { - const cells = getCells(row, column, width, height); - if (cells.some((cell) => occupied.has(cell))) continue; - - cells.forEach((cell) => occupied.add(cell)); - layout[item.artId] = { column, row, width, height }; - return layout; - } - } + cells.forEach((cell) => occupied.add(cell)); + layout[item.artId] = { column, row, width, height }; + return layout; }, {}); }