Add a thumbnailScale field (xs/sm/md/lg/xl) to gallery layout settings that adjusts column counts for Grid, Masonry (columns mode), and Mosaic layouts. Each scale maps to a column offset applied on top of the layout's base columns, letting photographers control photo density. - Add thumbnailScale to GalleryLayoutSettings type - Apply scale offset in Grid, Masonry, and Mosaic layout components - Add thumbnail scale dropdown to admin theme customizer - Conditionally show dropdown only for applicable layouts - Safelist dynamic grid-cols classes in Tailwind config - Add i18n keys for EN, DE, PT, RU locales Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
co-authored by
Paul Nothaft
parent
3742d71535
commit
ee46088985
@@ -312,6 +312,7 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
|
|||||||
|
|
||||||
{/* Grid specific */}
|
{/* Grid specific */}
|
||||||
{localTheme.galleryLayout === 'grid' && (
|
{localTheme.galleryLayout === 'grid' && (
|
||||||
|
<>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
|
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
|
||||||
{t('branding.columns')}
|
{t('branding.columns')}
|
||||||
@@ -358,6 +359,26 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
|
||||||
|
{t('branding.thumbnailScale', 'Thumbnail Scale')}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={localTheme.gallerySettings?.thumbnailScale || 'md'}
|
||||||
|
onChange={(e) => updateGallerySettings('thumbnailScale', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100"
|
||||||
|
>
|
||||||
|
<option value="xs">{t('branding.thumbnailScaleOptions.xs', 'XS — Most photos')}</option>
|
||||||
|
<option value="sm">{t('branding.thumbnailScaleOptions.sm', 'SM — More photos')}</option>
|
||||||
|
<option value="md">{t('branding.thumbnailScaleOptions.md', 'MD — Default')}</option>
|
||||||
|
<option value="lg">{t('branding.thumbnailScaleOptions.lg', 'LG — Larger photos')}</option>
|
||||||
|
<option value="xl">{t('branding.thumbnailScaleOptions.xl', 'XL — Largest photos')}</option>
|
||||||
|
</select>
|
||||||
|
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
|
||||||
|
{t('branding.thumbnailScaleHint', 'Adjusts column count relative to the base grid columns')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Carousel specific */}
|
{/* Carousel specific */}
|
||||||
@@ -437,6 +458,29 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Thumbnail scale - only for columns mode */}
|
||||||
|
{(!localTheme.gallerySettings?.masonryMode || localTheme.gallerySettings?.masonryMode === 'columns') && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
|
||||||
|
{t('branding.thumbnailScale', 'Thumbnail Scale')}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={localTheme.gallerySettings?.thumbnailScale || 'md'}
|
||||||
|
onChange={(e) => updateGallerySettings('thumbnailScale', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100"
|
||||||
|
>
|
||||||
|
<option value="xs">{t('branding.thumbnailScaleOptions.xs', 'XS — Most photos')}</option>
|
||||||
|
<option value="sm">{t('branding.thumbnailScaleOptions.sm', 'SM — More photos')}</option>
|
||||||
|
<option value="md">{t('branding.thumbnailScaleOptions.md', 'MD — Default')}</option>
|
||||||
|
<option value="lg">{t('branding.thumbnailScaleOptions.lg', 'LG — Larger photos')}</option>
|
||||||
|
<option value="xl">{t('branding.thumbnailScaleOptions.xl', 'XL — Largest photos')}</option>
|
||||||
|
</select>
|
||||||
|
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
|
||||||
|
{t('branding.thumbnailScaleHint', 'Adjusts column count relative to the base grid columns')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Row-specific settings - show for all row-based modes */}
|
{/* Row-specific settings - show for all row-based modes */}
|
||||||
{['rows', 'flickr', 'justified'].includes(localTheme.gallerySettings?.masonryMode || '') && (
|
{['rows', 'flickr', 'justified'].includes(localTheme.gallerySettings?.masonryMode || '') && (
|
||||||
<>
|
<>
|
||||||
@@ -476,6 +520,29 @@ export const ThemeCustomizerEnhanced: React.FC<ThemeCustomizerEnhancedProps> = (
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Mosaic specific */}
|
||||||
|
{localTheme.galleryLayout === 'mosaic' && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
|
||||||
|
{t('branding.thumbnailScale', 'Thumbnail Scale')}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={localTheme.gallerySettings?.thumbnailScale || 'md'}
|
||||||
|
onChange={(e) => updateGallerySettings('thumbnailScale', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100"
|
||||||
|
>
|
||||||
|
<option value="xs">{t('branding.thumbnailScaleOptions.xs', 'XS — Most photos')}</option>
|
||||||
|
<option value="sm">{t('branding.thumbnailScaleOptions.sm', 'SM — More photos')}</option>
|
||||||
|
<option value="md">{t('branding.thumbnailScaleOptions.md', 'MD — Default')}</option>
|
||||||
|
<option value="lg">{t('branding.thumbnailScaleOptions.lg', 'LG — Larger photos')}</option>
|
||||||
|
<option value="xl">{t('branding.thumbnailScaleOptions.xl', 'XL — Largest photos')}</option>
|
||||||
|
</select>
|
||||||
|
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
|
||||||
|
{t('branding.thumbnailScaleHint', 'Adjusts column count relative to the base grid columns')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -374,6 +374,10 @@ export const GridGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
|||||||
const columns = gallerySettings.gridColumns || { mobile: 2, tablet: 3, desktop: 4 };
|
const columns = gallerySettings.gridColumns || { mobile: 2, tablet: 3, desktop: 4 };
|
||||||
const spacing = gallerySettings.spacing || 'normal';
|
const spacing = gallerySettings.spacing || 'normal';
|
||||||
const animation = gallerySettings.photoAnimation || 'fade';
|
const animation = gallerySettings.photoAnimation || 'fade';
|
||||||
|
const scale = gallerySettings.thumbnailScale || 'md';
|
||||||
|
|
||||||
|
const scaleOffsets: Record<string, number> = { xs: 3, sm: 1, md: 0, lg: -1, xl: -2 };
|
||||||
|
const applyScale = (cols: number) => Math.max(1, cols + (scaleOffsets[scale] ?? 0));
|
||||||
|
|
||||||
const [showIdentityModal, setShowIdentityModal] = React.useState(false);
|
const [showIdentityModal, setShowIdentityModal] = React.useState(false);
|
||||||
const [pendingAction, setPendingAction] = React.useState<null | { type: 'like'; photoId: number }>(null);
|
const [pendingAction, setPendingAction] = React.useState<null | { type: 'like'; photoId: number }>(null);
|
||||||
@@ -383,10 +387,10 @@ export const GridGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
|||||||
const spacingClass = spacing === 'tight' ? 'gap-2' : spacing === 'relaxed' ? 'gap-6' : 'gap-4';
|
const spacingClass = spacing === 'tight' ? 'gap-2' : spacing === 'relaxed' ? 'gap-6' : 'gap-4';
|
||||||
|
|
||||||
const gridClass = `photo-grid grid ${spacingClass}
|
const gridClass = `photo-grid grid ${spacingClass}
|
||||||
grid-cols-${columns.mobile}
|
grid-cols-${applyScale(columns.mobile)}
|
||||||
sm:grid-cols-${columns.tablet}
|
sm:grid-cols-${applyScale(columns.tablet)}
|
||||||
lg:grid-cols-${columns.desktop}
|
lg:grid-cols-${applyScale(columns.desktop)}
|
||||||
xl:grid-cols-${columns.desktop + 1}`;
|
xl:grid-cols-${applyScale(columns.desktop + 1)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={gridClass}>
|
<div className={gridClass}>
|
||||||
|
|||||||
@@ -241,6 +241,13 @@ export const MasonryGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
|||||||
const mode = gallerySettings.masonryMode || 'columns';
|
const mode = gallerySettings.masonryMode || 'columns';
|
||||||
const targetRowHeight = gallerySettings.masonryRowHeight || 250;
|
const targetRowHeight = gallerySettings.masonryRowHeight || 250;
|
||||||
const lastRowBehavior = gallerySettings.masonryLastRowBehavior || 'left';
|
const lastRowBehavior = gallerySettings.masonryLastRowBehavior || 'left';
|
||||||
|
const scale = gallerySettings.thumbnailScale || 'md';
|
||||||
|
|
||||||
|
const scaleOffsets: Record<string, number> = { xs: 3, sm: 1, md: 0, lg: -1, xl: -2 };
|
||||||
|
const applyScale = (cols: number) => Math.max(1, cols + (scaleOffsets[scale] ?? 0));
|
||||||
|
|
||||||
|
// Apply scale to columns only in columns mode
|
||||||
|
const scaledColumns = mode === 'columns' ? applyScale(columns) : columns;
|
||||||
|
|
||||||
|
|
||||||
// Calculate number of columns based on container width (for columns mode)
|
// Calculate number of columns based on container width (for columns mode)
|
||||||
@@ -339,20 +346,20 @@ export const MasonryGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
|||||||
// This creates a more balanced masonry layout instead of round-robin
|
// This creates a more balanced masonry layout instead of round-robin
|
||||||
const photoColumns: Photo[][] = useMemo(() => {
|
const photoColumns: Photo[][] = useMemo(() => {
|
||||||
if (mode !== 'columns' || photos.length === 0) {
|
if (mode !== 'columns' || photos.length === 0) {
|
||||||
return Array.from({ length: columns }, () => []);
|
return Array.from({ length: scaledColumns }, () => []);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cols: Photo[][] = Array.from({ length: columns }, () => []);
|
const cols: Photo[][] = Array.from({ length: scaledColumns }, () => []);
|
||||||
const colHeights: number[] = Array(columns).fill(0);
|
const colHeights: number[] = Array(scaledColumns).fill(0);
|
||||||
|
|
||||||
// Calculate approximate column width for height estimation
|
// Calculate approximate column width for height estimation
|
||||||
const approxColWidth = containerWidth > 0 ? (containerWidth - (columns - 1) * gutter) / columns : 300;
|
const approxColWidth = containerWidth > 0 ? (containerWidth - (scaledColumns - 1) * gutter) / scaledColumns : 300;
|
||||||
|
|
||||||
photos.forEach((photo) => {
|
photos.forEach((photo) => {
|
||||||
// Find the shortest column
|
// Find the shortest column
|
||||||
let shortestCol = 0;
|
let shortestCol = 0;
|
||||||
let minHeight = colHeights[0];
|
let minHeight = colHeights[0];
|
||||||
for (let i = 1; i < columns; i++) {
|
for (let i = 1; i < scaledColumns; i++) {
|
||||||
if (colHeights[i] < minHeight) {
|
if (colHeights[i] < minHeight) {
|
||||||
minHeight = colHeights[i];
|
minHeight = colHeights[i];
|
||||||
shortestCol = i;
|
shortestCol = i;
|
||||||
@@ -373,15 +380,15 @@ export const MasonryGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return cols;
|
return cols;
|
||||||
}, [mode, photos, columns, containerWidth, gutter]);
|
}, [mode, photos, scaledColumns, containerWidth, gutter]);
|
||||||
|
|
||||||
// Calculate approximate column width for aspect ratio calculations
|
// Calculate approximate column width for aspect ratio calculations
|
||||||
const columnWidth = useMemo(() => {
|
const columnWidth = useMemo(() => {
|
||||||
if (containerWidth <= 0 || columns <= 0) return 300;
|
if (containerWidth <= 0 || scaledColumns <= 0) return 300;
|
||||||
// Account for gaps between columns
|
// Account for gaps between columns
|
||||||
const totalGaps = (columns - 1) * gutter;
|
const totalGaps = (scaledColumns - 1) * gutter;
|
||||||
return (containerWidth - totalGaps) / columns;
|
return (containerWidth - totalGaps) / scaledColumns;
|
||||||
}, [containerWidth, columns, gutter]);
|
}, [containerWidth, scaledColumns, gutter]);
|
||||||
|
|
||||||
// ROWS MODE - Google Photos style justified layout
|
// ROWS MODE - Google Photos style justified layout
|
||||||
if (mode === 'rows') {
|
if (mode === 'rows') {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Download, Maximize2, Check, Heart, MessageSquare } from 'lucide-react';
|
import { Download, Maximize2, Check, Heart, MessageSquare } from 'lucide-react';
|
||||||
|
import { useTheme } from '../../../contexts/ThemeContext';
|
||||||
import { AuthenticatedImage } from '../../common';
|
import { AuthenticatedImage } from '../../common';
|
||||||
import { FeedbackIdentityModal } from '../../gallery/FeedbackIdentityModal';
|
import { FeedbackIdentityModal } from '../../gallery/FeedbackIdentityModal';
|
||||||
import { feedbackService } from '../../../services/feedback.service';
|
import { feedbackService } from '../../../services/feedback.service';
|
||||||
@@ -210,23 +211,33 @@ export const MosaicGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
|||||||
feedbackEnabled = false,
|
feedbackEnabled = false,
|
||||||
feedbackOptions
|
feedbackOptions
|
||||||
}) => {
|
}) => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const scale = theme.gallerySettings?.thumbnailScale || 'md';
|
||||||
|
const scaleOffsets: Record<string, number> = { xs: 3, sm: 1, md: 0, lg: -1, xl: -2 };
|
||||||
|
const applyScale = (cols: number, min = 1) => Math.max(min, cols + (scaleOffsets[scale] ?? 0));
|
||||||
|
|
||||||
|
const desktop = applyScale(4);
|
||||||
|
const xlDown = applyScale(3);
|
||||||
|
const lgDown = applyScale(2);
|
||||||
|
const mobile = Math.min(applyScale(1), 2); // Cap mobile at 2
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="photo-grid w-full"
|
className="photo-grid w-full"
|
||||||
style={{
|
style={{
|
||||||
columnCount: 4,
|
columnCount: desktop,
|
||||||
columnGap: '8px',
|
columnGap: '8px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<style>{`
|
<style>{`
|
||||||
@media (max-width: 1280px) {
|
@media (max-width: 1280px) {
|
||||||
.photo-grid { column-count: 3 !important; }
|
.photo-grid { column-count: ${xlDown} !important; }
|
||||||
}
|
}
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.photo-grid { column-count: 2 !important; }
|
.photo-grid { column-count: ${lgDown} !important; }
|
||||||
}
|
}
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.photo-grid { column-count: 1 !important; }
|
.photo-grid { column-count: ${mobile} !important; }
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
{photos.map((photo, index) => (
|
{photos.map((photo, index) => (
|
||||||
|
|||||||
@@ -1437,6 +1437,15 @@
|
|||||||
"center": "Zentriert",
|
"center": "Zentriert",
|
||||||
"justify": "Blocksatz (gestreckt)"
|
"justify": "Blocksatz (gestreckt)"
|
||||||
},
|
},
|
||||||
|
"thumbnailScale": "Vorschau-Skalierung",
|
||||||
|
"thumbnailScaleOptions": {
|
||||||
|
"xs": "XS — Meiste Fotos",
|
||||||
|
"sm": "SM — Mehr Fotos",
|
||||||
|
"md": "MD — Standard",
|
||||||
|
"lg": "LG — Größere Fotos",
|
||||||
|
"xl": "XL — Größte Fotos"
|
||||||
|
},
|
||||||
|
"thumbnailScaleHint": "Passt die Spaltenanzahl relativ zu den Basis-Rasterspalten an",
|
||||||
"showHeroSection": "Hero-Bereich anzeigen",
|
"showHeroSection": "Hero-Bereich anzeigen",
|
||||||
"showHeroSectionHint": "Zeigt ein hervorgehobenes Titelbild über der Galerie an",
|
"showHeroSectionHint": "Zeigt ein hervorgehobenes Titelbild über der Galerie an",
|
||||||
"heroHeight": "Hero-Bereich Höhe",
|
"heroHeight": "Hero-Bereich Höhe",
|
||||||
|
|||||||
@@ -1152,6 +1152,15 @@
|
|||||||
"center": "Centered",
|
"center": "Centered",
|
||||||
"justify": "Justified (stretch)"
|
"justify": "Justified (stretch)"
|
||||||
},
|
},
|
||||||
|
"thumbnailScale": "Thumbnail Scale",
|
||||||
|
"thumbnailScaleOptions": {
|
||||||
|
"xs": "XS — Most photos",
|
||||||
|
"sm": "SM — More photos",
|
||||||
|
"md": "MD — Default",
|
||||||
|
"lg": "LG — Larger photos",
|
||||||
|
"xl": "XL — Largest photos"
|
||||||
|
},
|
||||||
|
"thumbnailScaleHint": "Adjusts column count relative to the base grid columns",
|
||||||
"showHeroSection": "Show Hero Section",
|
"showHeroSection": "Show Hero Section",
|
||||||
"showHeroSectionHint": "Display a featured hero image above the justified gallery",
|
"showHeroSectionHint": "Display a featured hero image above the justified gallery",
|
||||||
"heroHeight": "Hero Section Height",
|
"heroHeight": "Hero Section Height",
|
||||||
|
|||||||
@@ -1130,6 +1130,15 @@
|
|||||||
"center": "Centralizado",
|
"center": "Centralizado",
|
||||||
"justify": "Justified (esticado)"
|
"justify": "Justified (esticado)"
|
||||||
},
|
},
|
||||||
|
"thumbnailScale": "Escala de Miniaturas",
|
||||||
|
"thumbnailScaleOptions": {
|
||||||
|
"xs": "XS — Mais fotos",
|
||||||
|
"sm": "SM — Muitas fotos",
|
||||||
|
"md": "MD — Padrão",
|
||||||
|
"lg": "LG — Fotos maiores",
|
||||||
|
"xl": "XL — Fotos grandes"
|
||||||
|
},
|
||||||
|
"thumbnailScaleHint": "Ajusta a contagem de colunas em relação às colunas base da grade",
|
||||||
"showHeroSection": "Mostrar Seção Hero",
|
"showHeroSection": "Mostrar Seção Hero",
|
||||||
"showHeroSectionHint": "Exibir uma imagem hero em destaque acima da galeria justified",
|
"showHeroSectionHint": "Exibir uma imagem hero em destaque acima da galeria justified",
|
||||||
"heroHeight": "Altura da Seção Hero",
|
"heroHeight": "Altura da Seção Hero",
|
||||||
|
|||||||
@@ -1130,6 +1130,15 @@
|
|||||||
"center": "По центру",
|
"center": "По центру",
|
||||||
"justify": "Justified (растянуть)"
|
"justify": "Justified (растянуть)"
|
||||||
},
|
},
|
||||||
|
"thumbnailScale": "Масштаб миниатюр",
|
||||||
|
"thumbnailScaleOptions": {
|
||||||
|
"xs": "XS — Больше всего фото",
|
||||||
|
"sm": "SM — Больше фото",
|
||||||
|
"md": "MD — По умолчанию",
|
||||||
|
"lg": "LG — Крупнее фото",
|
||||||
|
"xl": "XL — Самые крупные фото"
|
||||||
|
},
|
||||||
|
"thumbnailScaleHint": "Изменяет количество колонок относительно базовых колонок сетки",
|
||||||
"showHeroSection": "Показывать секцию баннера",
|
"showHeroSection": "Показывать секцию баннера",
|
||||||
"showHeroSectionHint": "Отображать главное фото баннера над justified-галереей",
|
"showHeroSectionHint": "Отображать главное фото баннера над justified-галереей",
|
||||||
"heroHeight": "Высота секции баннера",
|
"heroHeight": "Высота секции баннера",
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ export interface GalleryLayoutSettings {
|
|||||||
|
|
||||||
// Mosaic specific
|
// Mosaic specific
|
||||||
mosaicPattern?: 'random' | 'structured' | 'alternating';
|
mosaicPattern?: 'random' | 'structured' | 'alternating';
|
||||||
|
|
||||||
|
// Thumbnail scale (applies to Grid, Masonry columns, Mosaic)
|
||||||
|
thumbnailScale?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThemeConfig {
|
export interface ThemeConfig {
|
||||||
|
|||||||
@@ -84,6 +84,13 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
safelist: [
|
||||||
|
// Dynamic grid-cols classes used by thumbnail scale offsets
|
||||||
|
...Array.from({ length: 12 }, (_, i) => `grid-cols-${i + 1}`),
|
||||||
|
...Array.from({ length: 12 }, (_, i) => `sm:grid-cols-${i + 1}`),
|
||||||
|
...Array.from({ length: 12 }, (_, i) => `lg:grid-cols-${i + 1}`),
|
||||||
|
...Array.from({ length: 12 }, (_, i) => `xl:grid-cols-${i + 1}`),
|
||||||
|
],
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user