Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/constanza101/borrissol/llms.txt

Use this file to discover all available pages before exploring further.

The gallery page showcases participant creations and workshop moments at Borrissol Espai Creatiu. Images are displayed in a honeycomb layout — a CSS clip-path grid where diamond-clipped tiles morph into rounded squares on hover or tap, highlighting the selected image while dimming its neighbours. The page is intentionally minimal: a heading, an eyebrow label, and the grid — no sidebar, no filters, no pagination.

Routes

The gallery is served at four URLs, one per locale. All four are statically pre-rendered at build time:
LocaleURL
Catalan (default)/gallery
Spanish/es/gallery
English/en/gallery
French/fr/gallery
The Catalan default route (gallery.astro) hardcodes useTranslations('ca'). The three non-default locale routes ([lang]/gallery.astro) call useTranslations(lang) with the locale derived from getStaticPaths, resolving the page title, meta description, eyebrow label, and image alt text prefix from src/i18n/ui.ts.

SEO metadata

The page title and description are driven by translation keys. In English:
KeyValue
page.gallery.titleGallery
page.gallery.descriptionPhoto gallery of Borrissol Espai Creatiu: tufting, punch needle, needle felting, weaving and textile creations in Mataró, Barcelona.
gallery.eyebrowGallery
gallery.headlineWorkshops in pictures
gallery.image.altTufting and textile techniques workshop at Borrissol Espai Creatiu, Mataró
Each image receives an auto-numbered alt attribute: ${t('gallery.image.alt')} 1, ${t('gallery.image.alt')} 2, and so on, based on sort order.

Image pipeline

Gallery images live in src/assets/images/gallery/ and are processed by the Astro asset pipeline at build time. The gallery.astro page uses import.meta.glob to load all .jpg files eagerly:
const galleryModules = import.meta.glob<{ default: ImageMetadata }>(
  '../assets/images/gallery/*.jpg',
  { eager: true }
);

const images = Object.entries(galleryModules)
  .sort(([a], [b]) => a.localeCompare(b))
  .map(([, module], i) => ({
    src: module.default,
    alt: `${altPrefix} ${i + 1}`,
  }));
Images are sorted alphabetically by filename so the display order is deterministic. The sorted array is passed as the images prop to Gallery.astro.
To add a new image to the gallery, drop a .jpg file into src/assets/images/gallery/. Name it with a numeric or alphabetic prefix (e.g. 040-new-piece.jpg) to control its position in the grid. The Astro build will automatically generate AVIF and WebP variants.
Gallery.astro renders the honeycomb grid. It accepts a typed images prop:
interface GalleryImage {
  src: ImageMetadata;
  alt: string;
}

interface Props {
  images: GalleryImage[];
}
Inside the component, each image is rendered with <Picture> from astro:assets, outputting responsive <source> elements in AVIF and WebP:
<Picture
  src={src}
  widths={[240, 360, 540, 720]}
  sizes="(max-width: 540px) 40vw, (max-width: 1024px) 22vw, 16vw"
  formats={['avif', 'webp']}
  alt={alt}
  class="gallery-img"
  loading="lazy"
/>
All images are loading="lazy" — the gallery has no above-the-fold hero image.

Honeycomb layout mechanics

The interlocking honeycomb pattern is achieved entirely in CSS using a grid + clip-path: path() approach. There is no canvas, SVG, or layout library involved.

Grid structure

Each photo cell (grid-column: span 2) is a square sized 2 × --cell + gap. Grid rows are only --cell tall, so each cell visually overflows downward by one row. This vertical overflow, combined with the horizontal half-tile offset applied to every nth-child(7n+5) cell, produces the honeycomb interlock.

Diamond clip

The initial clip-path: path(...) clips each tile to a diamond shape using an 8-curve Bézier. The hover/active state morphs the same path to a rounded square. Both shapes share the same 8-control-point structure so the browser can interpolate smoothly.

Breakpoint grid configuration

The --cell CSS custom property controls the tile size at each breakpoint:
BreakpointColumns--cellNotes
Default (≥ 1367px)8150pxOffset pattern: nth-child(7n+5)
Large laptop (≤ 1366px)8130pxSame offset pattern
Tablet (≤ 1024px)6110pxOffset pattern: nth-child(5n+4)
Small tablet (≤ 700px)690pxSame 6-col offset
Mobile (≤ 540px)480pxOffset pattern: nth-child(3n)
Each breakpoint has its own hardcoded clip-path: path() string because CSS path() uses absolute pixel coordinates — percentages are not supported — so the path must be rescaled for each tile size.

Tap-to-reveal interaction

No JavaScript framework is used. The interaction is handled by a small inline script (is:inline) injected into the page:
(function () {
  var grid = document.querySelector('.gallery-grid');
  var tiles = grid.querySelectorAll('.gallery-tile');

  // Click / tap: toggle .is-active on the clicked tile,
  // clear any previously active tile first.
  tiles.forEach(function (btn) {
    btn.addEventListener('click', function (e) {
      e.stopPropagation();
      var wasActive = btn.classList.contains('is-active');
      grid.querySelectorAll('.gallery-tile.is-active').forEach(function (b) {
        b.classList.remove('is-active');
      });
      if (!wasActive) btn.classList.add('is-active');
    });
  });

  // Click outside grid: deactivate all tiles.
  document.addEventListener('click', function (e) {
    if (!grid.contains(e.target)) {
      grid.querySelectorAll('.gallery-tile.is-active').forEach(function (b) {
        b.classList.remove('is-active');
      });
    }
  });

  // Escape key: deactivate all tiles.
  document.addEventListener('keydown', function (e) {
    if (e.key === 'Escape') {
      grid.querySelectorAll('.gallery-tile.is-active').forEach(function (b) {
        b.classList.remove('is-active');
      });
    }
  });
})();
The CSS :has() selector handles the dimming logic: when any tile has .is-active, all other tiles receive filter: brightness(0.55) saturate(0.6). The active tile itself gets filter: brightness(1) saturate(1.15) and morphs to the rounded-square clip path. On hover-capable devices (@media (hover: hover)), the hover state provides the same morph without requiring a tap. The hover and tap states use identical clip-path values.

Accessibility

  • The grid is a <ul role="list"> with <li> cells, making the image count available to screen readers.
  • Each tile is a <button type="button"> with an aria-label set to the image’s alt text.
  • focus-visible outline is applied to focused tiles for keyboard navigation.
  • @media (prefers-reduced-motion: reduce) disables the clip-path morph transition, leaving only the filter transition at a reduced 200ms duration.

Page layout

The gallery page uses the shared Layout.astro wrapper (which injects GA4, consent mode, skip link, hreflang tags, and Open Graph meta) with ogType="website". It includes <Navbar> and <Footer> but no PrivacySection — the privacy accordion is only included on the home page.

Build docs developers (and LLMs) love