From 5775875cdd8d8e876e59dbdb74d1f98a2d4dc77c Mon Sep 17 00:00:00 2001 From: Sadeesha Perera <121984253+SadeeshaPerera@users.noreply.github.com> Date: Sun, 19 Oct 2025 20:59:22 +0000 Subject: [PATCH 1/2] Feat: gallery structure with sample data --- components.json | 22 +++ package.json | 8 +- pnpm-lock.yaml | 55 ++++++++ src/app/globals.css | 124 +++++++++++++++-- src/app/page.tsx | 12 +- src/components/Masonry.tsx | 275 +++++++++++++++++++++++++++++++++++++ src/data/gallery.json | 38 +++++ src/lib/utils.ts | 6 + 8 files changed, 524 insertions(+), 16 deletions(-) create mode 100644 components.json create mode 100644 src/components/Masonry.tsx create mode 100644 src/data/gallery.json create mode 100644 src/lib/utils.ts diff --git a/components.json b/components.json new file mode 100644 index 0000000..edcaef2 --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/package.json b/package.json index 2311767..99afc0f 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,14 @@ "format:check": "prettier --check --cache \"**/*.{js,jsx,ts,tsx,md,mdx}\"" }, "dependencies": { + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "gsap": "^3.13.0", + "lucide-react": "^0.546.0", "next": "15.5.4", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "tailwind-merge": "^3.3.1" }, "devDependencies": { "@commitlint/cli": "^20.1.0", @@ -30,6 +35,7 @@ "lint-staged": "^16.2.3", "prettier": "^3.6.2", "tailwindcss": "^4", + "tw-animate-css": "^1.4.0", "typescript": "^5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eae9ebd..4416f8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,18 @@ importers: .: dependencies: + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + gsap: + specifier: ^3.13.0 + version: 3.13.0 + lucide-react: + specifier: ^0.546.0 + version: 0.546.0(react@19.1.0) next: specifier: 15.5.4 version: 15.5.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -17,6 +29,9 @@ importers: react-dom: specifier: 19.1.0 version: 19.1.0(react@19.1.0) + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 devDependencies: '@commitlint/cli': specifier: ^20.1.0 @@ -57,6 +72,9 @@ importers: tailwindcss: specifier: ^4 version: 4.1.14 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 typescript: specifier: ^5 version: 5.9.3 @@ -848,6 +866,9 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -863,6 +884,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1297,6 +1322,9 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gsap@3.13.0: + resolution: {integrity: sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -1659,6 +1687,11 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lucide-react@0.546.0: + resolution: {integrity: sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} @@ -2096,6 +2129,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwindcss@4.1.14: resolution: {integrity: sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==} @@ -2137,6 +2173,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -3027,6 +3066,10 @@ snapshots: chownr@3.0.0: {} + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -3044,6 +3087,8 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3623,6 +3668,8 @@ snapshots: graphemer@1.4.0: {} + gsap@3.13.0: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -3957,6 +4004,10 @@ snapshots: dependencies: js-tokens: 4.0.0 + lucide-react@0.546.0(react@19.1.0): + dependencies: + react: 19.1.0 + magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4440,6 +4491,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tailwind-merge@3.3.1: {} + tailwindcss@4.1.14: {} tapable@2.3.0: {} @@ -4480,6 +4533,8 @@ snapshots: tslib@2.8.1: {} + tw-animate-css@1.4.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..31ec2d7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,122 @@ -@import "tailwindcss"; +@import 'tailwindcss'; +@import 'tw-animate-css'; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } } diff --git a/src/app/page.tsx b/src/app/page.tsx index 7bcd29e..41beba7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,13 @@ +import Masonry from '@/components/Masonry'; +import gallery from '@/data/gallery.json'; + export default function Home() { - return
; + return ( +
+

Gallery

+
+ +
+
+ ); } diff --git a/src/components/Masonry.tsx b/src/components/Masonry.tsx new file mode 100644 index 0000000..1483fa9 --- /dev/null +++ b/src/components/Masonry.tsx @@ -0,0 +1,275 @@ +'use client'; + +import React, { + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { gsap } from 'gsap'; + +const useMedia = ( + queries: string[], + values: number[], + defaultValue: number +): number => { + const get = () => { + if ( + typeof window === 'undefined' || + typeof window.matchMedia !== 'function' + ) + return defaultValue; + const idx = queries.findIndex((q) => window.matchMedia(q).matches); + return values[idx] ?? defaultValue; + }; + + const [value, setValue] = useState(get); + + useEffect(() => { + if ( + typeof window === 'undefined' || + typeof window.matchMedia !== 'function' + ) + return; + const handler = () => setValue(get); + const mqs = queries.map((q) => window.matchMedia(q)); + mqs.forEach((mq) => mq.addEventListener('change', handler)); + return () => mqs.forEach((mq) => mq.removeEventListener('change', handler)); + }, [queries]); + + return value; +}; + +const useMeasure = () => { + const ref = useRef(null); + const [size, setSize] = useState({ width: 0, height: 0 }); + + useLayoutEffect(() => { + if (!ref.current) return; + const ro = new ResizeObserver(([entry]) => { + const { width, height } = entry.contentRect; + setSize({ width, height }); + }); + ro.observe(ref.current); + return () => ro.disconnect(); + }, []); + + return [ref, size] as const; +}; + +const preloadImages = async (urls: string[]): Promise => { + await Promise.all( + urls.map( + (src) => + new Promise((resolve) => { + const img = new Image(); + img.src = src; + img.onload = img.onerror = () => resolve(); + }) + ) + ); +}; + +interface Item { + id: string; + img: string; + url: string; + height: number; +} + +interface GridItem extends Item { + x: number; + y: number; + w: number; + h: number; +} + +interface MasonryProps { + items: Item[]; + ease?: string; + duration?: number; + stagger?: number; + animateFrom?: 'bottom' | 'top' | 'left' | 'right' | 'center' | 'random'; + scaleOnHover?: boolean; + hoverScale?: number; + blurToFocus?: boolean; + colorShiftOnHover?: boolean; +} + +const Masonry: React.FC = ({ + items, + ease = 'power3.out', + duration = 0.6, + stagger = 0.05, + animateFrom = 'bottom', + scaleOnHover = true, + hoverScale = 0.95, + blurToFocus = true, + colorShiftOnHover = false, +}) => { + const columns = useMedia( + [ + '(min-width:1500px)', + '(min-width:1000px)', + '(min-width:600px)', + '(min-width:400px)', + ], + [5, 4, 3, 2], + 1 + ); + + const [containerRef, { width }] = useMeasure(); + const [imagesReady, setImagesReady] = useState(false); + + const getInitialPosition = (item: GridItem) => { + const containerRect = containerRef.current?.getBoundingClientRect(); + if (!containerRect) return { x: item.x, y: item.y }; + + let direction = animateFrom; + if (animateFrom === 'random') { + const dirs = ['top', 'bottom', 'left', 'right']; + direction = dirs[ + Math.floor(Math.random() * dirs.length) + ] as typeof animateFrom; + } + + switch (direction) { + case 'top': + return { x: item.x, y: -200 }; + case 'bottom': + return { x: item.x, y: window.innerHeight + 200 }; + case 'left': + return { x: -200, y: item.y }; + case 'right': + return { x: window.innerWidth + 200, y: item.y }; + case 'center': + return { + x: containerRect.width / 2 - item.w / 2, + y: containerRect.height / 2 - item.h / 2, + }; + default: + return { x: item.x, y: item.y + 100 }; + } + }; + + useEffect(() => { + preloadImages(items.map((i) => i.img)).then(() => setImagesReady(true)); + }, [items]); + + const grid = useMemo(() => { + if (!width) return []; + const colHeights = new Array(columns).fill(0); + const gap = 16; + const totalGaps = (columns - 1) * gap; + const columnWidth = (width - totalGaps) / columns; + + return items.map((child) => { + const col = colHeights.indexOf(Math.min(...colHeights)); + const x = col * (columnWidth + gap); + const height = child.height / 2; + const y = colHeights[col]; + + colHeights[col] += height + gap; + return { ...child, x, y, w: columnWidth, h: height }; + }); + }, [columns, items, width]); + + const hasMounted = useRef(false); + + useLayoutEffect(() => { + if (!imagesReady) return; + + grid.forEach((item, index) => { + const selector = `[data-key="${item.id}"]`; + const animProps = { x: item.x, y: item.y, width: item.w, height: item.h }; + + if (!hasMounted.current) { + const start = getInitialPosition(item); + gsap.fromTo( + selector, + { + opacity: 0, + x: start.x, + y: start.y, + width: item.w, + height: item.h, + ...(blurToFocus && { filter: 'blur(10px)' }), + }, + { + opacity: 1, + ...animProps, + ...(blurToFocus && { filter: 'blur(0px)' }), + duration: 0.8, + ease: 'power3.out', + delay: index * stagger, + } + ); + } else { + gsap.to(selector, { + ...animProps, + duration, + ease, + overwrite: 'auto', + }); + } + }); + + hasMounted.current = true; + }, [grid, imagesReady, stagger, animateFrom, blurToFocus, duration, ease]); + + const handleMouseEnter = (id: string, element: HTMLElement) => { + if (scaleOnHover) { + gsap.to(`[data-key="${id}"]`, { + scale: hoverScale, + duration: 0.3, + ease: 'power2.out', + }); + } + if (colorShiftOnHover) { + const overlay = element.querySelector('.color-overlay') as HTMLElement; + if (overlay) gsap.to(overlay, { opacity: 0.3, duration: 0.3 }); + } + }; + + const handleMouseLeave = (id: string, element: HTMLElement) => { + if (scaleOnHover) { + gsap.to(`[data-key="${id}"]`, { + scale: 1, + duration: 0.3, + ease: 'power2.out', + }); + } + if (colorShiftOnHover) { + const overlay = element.querySelector('.color-overlay') as HTMLElement; + if (overlay) gsap.to(overlay, { opacity: 0, duration: 0.3 }); + } + }; + + return ( +
+ {grid.map((item) => ( +
window.open(item.url, '_blank', 'noopener')} + onMouseEnter={(e) => handleMouseEnter(item.id, e.currentTarget)} + onMouseLeave={(e) => handleMouseLeave(item.id, e.currentTarget)} + > +
+ {colorShiftOnHover && ( +
+ )} +
+
+ ))} +
+ ); +}; + +export default Masonry; diff --git a/src/data/gallery.json b/src/data/gallery.json new file mode 100644 index 0000000..07e21c5 --- /dev/null +++ b/src/data/gallery.json @@ -0,0 +1,38 @@ +[ + { + "id": "1", + "img": "https://picsum.photos/600/800?random=1", + "url": "https://picsum.photos", + "height": 800 + }, + { + "id": "2", + "img": "https://picsum.photos/600/400?random=2", + "url": "https://picsum.photos", + "height": 400 + }, + { + "id": "3", + "img": "https://picsum.photos/600/1200?random=3", + "url": "https://picsum.photos", + "height": 1200 + }, + { + "id": "4", + "img": "https://picsum.photos/600/600?random=4", + "url": "https://picsum.photos", + "height": 600 + }, + { + "id": "5", + "img": "https://picsum.photos/600/700?random=5", + "url": "https://picsum.photos", + "height": 700 + }, + { + "id": "6", + "img": "https://picsum.photos/600/500?random=6", + "url": "https://picsum.photos", + "height": 500 + } +] diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..2819a83 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} From 5aff35cacf4a9e2ae751dbe2bcf6bf6428342616 Mon Sep 17 00:00:00 2001 From: Sadeesha Perera <121984253+SadeeshaPerera@users.noreply.github.com> Date: Sun, 19 Oct 2025 21:37:23 +0000 Subject: [PATCH 2/2] Feat: add gallery heading gradient and font --- src/app/globals.css | 35 +++++++++++++++++++++++++++++++++++ src/app/page.tsx | 4 +++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/app/globals.css b/src/app/globals.css index 31ec2d7..da82bfd 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -120,3 +120,38 @@ @apply bg-background text-foreground; } } + +/* Bricolage Grotesque font (add font files under /public/fonts or update URLs) */ +@font-face { + font-family: 'Bricolage Grotesque'; + src: + local('Bricolage Grotesque'), + url('/fonts/BricolageGrotesque-Regular.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +.font-bricolage { + font-family: + 'Bricolage Grotesque', + system-ui, + -apple-system, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + Arial; +} + +/* Reusable heading gradient (horizontal) */ +:root { + --heading-gradient: linear-gradient(90deg, #2fa5ff 0%, #9c09ff 100%); +} + +.heading-gradient { + background-image: var(--heading-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + color: transparent; +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 41beba7..f5cdc68 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,7 +4,9 @@ import gallery from '@/data/gallery.json'; export default function Home() { return (
-

Gallery

+

+ Gallery +