From a621bce692619c815ef0331b8c57826eaaaa5edf Mon Sep 17 00:00:00 2001
From: shayla-develops-webs
Date: Tue, 25 Aug 2026 16:42:49 -0500
Subject: [PATCH 01/12] Add Secondary color CSS vars for What We Offer
pills/dialog
---
styles/globals.scss | 2 ++
1 file changed, 2 insertions(+)
diff --git a/styles/globals.scss b/styles/globals.scss
index b8be84c9..718b9de7 100644
--- a/styles/globals.scss
+++ b/styles/globals.scss
@@ -12,6 +12,8 @@
--color-light-bg: #8cd5e8;
--color-primary-accent: #ffcc4c;
--color-primary-content: #292929;
+ --color-secondary-accent: #19aad1;
+ --color-secondary-surface: #75cce3;
--color-transparent: transparent;
--color-box-shadow: rgba(0, 0, 0, 0.08);
--dark-bg: #023047;
From ab37b1d4825f3da4c9aa5510607ced254c4d6f5e Mon Sep 17 00:00:00 2001
From: shayla-develops-webs
Date: Tue, 25 Aug 2026 16:54:20 -0500
Subject: [PATCH 02/12] Add static Pill component and services test harness
page
---
components/services/Pill/Pill.module.scss | 29 +++++++++++++++++++++++
components/services/Pill/index.js | 12 ++++++++++
pages/services.js | 11 +++++++++
3 files changed, 52 insertions(+)
create mode 100644 components/services/Pill/Pill.module.scss
create mode 100644 components/services/Pill/index.js
create mode 100644 pages/services.js
diff --git a/components/services/Pill/Pill.module.scss b/components/services/Pill/Pill.module.scss
new file mode 100644
index 00000000..2c76a29f
--- /dev/null
+++ b/components/services/Pill/Pill.module.scss
@@ -0,0 +1,29 @@
+@use '@/styles/index' as *;
+
+.pill {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 999px;
+ border: 2px solid var(--color-primary-accent);
+ background-color: var(--color-transparent);
+ color: var(--color-black);
+ font-family: $font-family-primary;
+ font-weight: $font-weight-regular;
+ white-space: nowrap;
+ cursor: pointer;
+ padding: 0.5rem 1rem;
+ font-size: 1rem;
+ line-height: 1.5rem;
+ @include transition(all 0.3s ease);
+
+ @include desktop {
+ padding: 0.75rem 2rem;
+ font-size: 1.5rem;
+ line-height: 2rem;
+ }
+
+ &.secondary {
+ border-color: var(--color-secondary-accent);
+ }
+}
diff --git a/components/services/Pill/index.js b/components/services/Pill/index.js
new file mode 100644
index 00000000..e16b4816
--- /dev/null
+++ b/components/services/Pill/index.js
@@ -0,0 +1,12 @@
+import styles from './Pill.module.scss';
+import { combineClasses } from '@/utils/classnames';
+
+export default function Pill({ label, variant = 'primary', onClick }) {
+ const pillClass = combineClasses(styles.pill, variant, styles);
+
+ return (
+
+ {label}
+
+ );
+}
diff --git a/pages/services.js b/pages/services.js
new file mode 100644
index 00000000..f5738db7
--- /dev/null
+++ b/pages/services.js
@@ -0,0 +1,11 @@
+import Pill from '@/components/services/Pill';
+
+export default function Services() {
+ return (
+
+ );
+}
From 6fda0d52c39720dddbddb3d2949d579d3b085f89 Mon Sep 17 00:00:00 2001
From: shayla-develops-webs
Date: Tue, 25 Aug 2026 17:55:11 -0500
Subject: [PATCH 03/12] Add static What We Offer section layout
---
.../WhatWeOffer/WhatWeOffer.module.scss | 54 +++++++++++++++++++
components/services/WhatWeOffer/index.js | 48 +++++++++++++++++
pages/services.js | 10 +---
3 files changed, 104 insertions(+), 8 deletions(-)
create mode 100644 components/services/WhatWeOffer/WhatWeOffer.module.scss
create mode 100644 components/services/WhatWeOffer/index.js
diff --git a/components/services/WhatWeOffer/WhatWeOffer.module.scss b/components/services/WhatWeOffer/WhatWeOffer.module.scss
new file mode 100644
index 00000000..fc7bad8d
--- /dev/null
+++ b/components/services/WhatWeOffer/WhatWeOffer.module.scss
@@ -0,0 +1,54 @@
+@use '@/styles/index' as *;
+
+.whatWeOffer {
+ width: 100%;
+ background-color: var(--color-white);
+ padding: 4rem 0;
+
+ @include desktop {
+ padding: 6rem 0;
+ }
+}
+
+.heading {
+ font-family: $font-family-secondary;
+ font-weight: $font-weight-bold;
+ font-size: 2.25rem;
+ line-height: 2.75rem;
+ text-align: center;
+ margin: 0 0 1.5rem;
+
+ @include desktop {
+ font-size: 4rem;
+ line-height: 4.5rem;
+ }
+}
+
+.body {
+ font-family: $font-family-primary;
+ font-size: 1rem;
+ line-height: 1.5rem;
+ text-align: center;
+ max-width: 60rem;
+ margin: 0 auto 2.5rem;
+
+ @include desktop {
+ font-size: 1.5rem;
+ line-height: 2rem;
+ }
+}
+
+.pillRows {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+}
+
+.pillRow {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 1rem;
+ width: 100%;
+}
diff --git a/components/services/WhatWeOffer/index.js b/components/services/WhatWeOffer/index.js
new file mode 100644
index 00000000..93c26573
--- /dev/null
+++ b/components/services/WhatWeOffer/index.js
@@ -0,0 +1,48 @@
+import Pill from '@/components/services/Pill';
+import Container from '@/components/containers/Container';
+import styles from './WhatWeOffer.module.scss';
+
+const offerPills = [
+ { id: 'web-development', label: 'Web Development', variant: 'primary' },
+ {
+ id: 'product-project-management',
+ label: 'Product & Project Management',
+ variant: 'secondary',
+ },
+ { id: 'design', label: 'Design', variant: 'primary' },
+ {
+ id: 'startup-nonprofit-tech-support',
+ label: 'Startup & Nonprofit Tech Support',
+ variant: 'secondary',
+ },
+ {
+ id: 'optimization-growth',
+ label: 'Optimization & Growth',
+ variant: 'primary',
+ },
+];
+
+export default function WhatWeOffer() {
+ return (
+
+
+ What We Offer
+
+ We deliver high-end digital solutions — from{' '}
+ design and development to{' '}
+ project management and strategy — tailored to your
+ goals and budget, with no compromise on quality.
+
+
+ {[0, 1, 2].map(rowIndex => (
+
+ {offerPills.map(pill => (
+
+ ))}
+
+ ))}
+
+
+
+ );
+}
diff --git a/pages/services.js b/pages/services.js
index f5738db7..56c1509c 100644
--- a/pages/services.js
+++ b/pages/services.js
@@ -1,11 +1,5 @@
-import Pill from '@/components/services/Pill';
+import WhatWeOffer from '@/components/services/WhatWeOffer';
export default function Services() {
- return (
-
- );
+ return ;
}
From fb69ad954f2b3d906a9eba182559763ad3b5c878 Mon Sep 17 00:00:00 2001
From: shayla-develops-webs
Date: Tue, 25 Aug 2026 18:35:43 -0500
Subject: [PATCH 04/12] Add auto-scrolling marquee rows with
keyboard-accessible focus handling
---
components/services/Pill/index.js | 14 ++++-
.../WhatWeOffer/WhatWeOffer.module.scss | 24 +++++++--
components/services/WhatWeOffer/index.js | 46 +++++++++++++---
.../services/WhatWeOffer/useAutoScroll.js | 52 +++++++++++++++++++
4 files changed, 123 insertions(+), 13 deletions(-)
create mode 100644 components/services/WhatWeOffer/useAutoScroll.js
diff --git a/components/services/Pill/index.js b/components/services/Pill/index.js
index e16b4816..d4a94e6d 100644
--- a/components/services/Pill/index.js
+++ b/components/services/Pill/index.js
@@ -1,11 +1,21 @@
import styles from './Pill.module.scss';
import { combineClasses } from '@/utils/classnames';
-export default function Pill({ label, variant = 'primary', onClick }) {
+export default function Pill({
+ label,
+ variant = 'primary',
+ onClick,
+ tabIndex,
+}) {
const pillClass = combineClasses(styles.pill, variant, styles);
return (
-
+
{label}
);
diff --git a/components/services/WhatWeOffer/WhatWeOffer.module.scss b/components/services/WhatWeOffer/WhatWeOffer.module.scss
index fc7bad8d..dae0b52f 100644
--- a/components/services/WhatWeOffer/WhatWeOffer.module.scss
+++ b/components/services/WhatWeOffer/WhatWeOffer.module.scss
@@ -41,14 +41,28 @@
.pillRows {
display: flex;
flex-direction: column;
- align-items: center;
gap: 1rem;
}
-.pillRow {
+.pillRowViewport {
+ width: 100%;
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+}
+
+.pillTrack {
+ display: flex;
+ gap: 1rem;
+ width: max-content;
+}
+
+.pillGroup {
display: flex;
- flex-wrap: wrap;
- justify-content: center;
gap: 1rem;
- width: 100%;
}
diff --git a/components/services/WhatWeOffer/index.js b/components/services/WhatWeOffer/index.js
index 93c26573..609581b5 100644
--- a/components/services/WhatWeOffer/index.js
+++ b/components/services/WhatWeOffer/index.js
@@ -1,7 +1,45 @@
import Pill from '@/components/services/Pill';
import Container from '@/components/containers/Container';
+import { useAutoScroll } from './useAutoScroll';
import styles from './WhatWeOffer.module.scss';
+const rowDirections = ['left', 'right', 'left'];
+
+function PillRow({ pills, direction }) {
+ const { viewportRef, groupRef, pause, resume } = useAutoScroll({
+ direction,
+ });
+
+ return (
+
+
+
+ {pills.map(pill => (
+
+ ))}
+
+
+ {pills.map(pill => (
+
+ ))}
+
+
+
+ );
+}
+
const offerPills = [
{ id: 'web-development', label: 'Web Development', variant: 'primary' },
{
@@ -34,12 +72,8 @@ export default function WhatWeOffer() {
goals and budget, with no compromise on quality.
- {[0, 1, 2].map(rowIndex => (
-
- {offerPills.map(pill => (
-
- ))}
-
+ {rowDirections.map((direction, rowIndex) => (
+
))}
diff --git a/components/services/WhatWeOffer/useAutoScroll.js b/components/services/WhatWeOffer/useAutoScroll.js
new file mode 100644
index 00000000..f6b085e1
--- /dev/null
+++ b/components/services/WhatWeOffer/useAutoScroll.js
@@ -0,0 +1,52 @@
+import { useEffect, useRef } from 'react';
+
+export function useAutoScroll({ direction = 'left', speed = 60 } = {}) {
+ const viewportRef = useRef(null);
+ const groupRef = useRef(null);
+ const pausedRef = useRef(false);
+
+ useEffect(() => {
+ const viewport = viewportRef.current;
+ const group = groupRef.current;
+ if (!viewport || !group) return undefined;
+
+ const prefersReducedMotion = window.matchMedia(
+ '(prefers-reduced-motion: reduce)',
+ ).matches;
+ if (prefersReducedMotion) return undefined;
+
+ const dir = direction === 'right' ? -1 : 1;
+ const groupWidth = group.offsetWidth;
+ viewport.scrollLeft = dir === -1 ? groupWidth : 0;
+
+ let frameId;
+ let lastTimestamp;
+
+ const step = timestamp => {
+ if (lastTimestamp !== undefined && !pausedRef.current) {
+ const delta = ((timestamp - lastTimestamp) / 1000) * speed * dir;
+ viewport.scrollLeft += delta;
+
+ if (viewport.scrollLeft >= groupWidth) {
+ viewport.scrollLeft -= groupWidth;
+ } else if (viewport.scrollLeft <= 0) {
+ viewport.scrollLeft += groupWidth;
+ }
+ }
+ lastTimestamp = timestamp;
+ frameId = requestAnimationFrame(step);
+ };
+
+ frameId = requestAnimationFrame(step);
+ return () => cancelAnimationFrame(frameId);
+ }, [direction, speed]);
+
+ const pause = () => {
+ pausedRef.current = true;
+ };
+ const resume = () => {
+ pausedRef.current = false;
+ };
+
+ return { viewportRef, groupRef, pause, resume };
+}
From cfd69cec74f11477d095f087905b3575b14818a4 Mon Sep 17 00:00:00 2001
From: shayla-develops-webs
Date: Tue, 25 Aug 2026 18:41:49 -0500
Subject: [PATCH 05/12] Add accessible Dialog primitive with focus trap and
escape/backdrop close
---
components/overlays/Dialog/Dialog.module.scss | 20 +++++
components/overlays/Dialog/index.js | 74 +++++++++++++++++++
pages/services.js | 21 +++++-
3 files changed, 114 insertions(+), 1 deletion(-)
create mode 100644 components/overlays/Dialog/Dialog.module.scss
create mode 100644 components/overlays/Dialog/index.js
diff --git a/components/overlays/Dialog/Dialog.module.scss b/components/overlays/Dialog/Dialog.module.scss
new file mode 100644
index 00000000..2c674dcd
--- /dev/null
+++ b/components/overlays/Dialog/Dialog.module.scss
@@ -0,0 +1,20 @@
+.backdrop {
+ position: fixed;
+ inset: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1.5rem;
+ z-index: 100;
+}
+
+.panel {
+ background-color: var(--color-white);
+ border-radius: 1rem;
+ max-width: 40rem;
+ width: 100%;
+ max-height: 90vh;
+ overflow-y: auto;
+ padding: 2rem;
+}
diff --git a/components/overlays/Dialog/index.js b/components/overlays/Dialog/index.js
new file mode 100644
index 00000000..21d89c8c
--- /dev/null
+++ b/components/overlays/Dialog/index.js
@@ -0,0 +1,74 @@
+import { useEffect, useRef, useState } from 'react';
+import { createPortal } from 'react-dom';
+import styles from './Dialog.module.scss';
+
+const FOCUSABLE_SELECTOR =
+ 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
+
+export default function Dialog({ open, onClose, titleId, children }) {
+ const [mounted, setMounted] = useState(false);
+ const panelRef = useRef(null);
+ const previouslyFocusedRef = useRef(null);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ useEffect(() => {
+ if (!open) return undefined;
+
+ previouslyFocusedRef.current = document.activeElement;
+ const panel = panelRef.current;
+ const focusable = panel
+ ? Array.from(panel.querySelectorAll(FOCUSABLE_SELECTOR))
+ : [];
+ (focusable[0] || panel)?.focus();
+
+ const handleKeyDown = event => {
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ onClose();
+ return;
+ }
+
+ if (event.key !== 'Tab' || focusable.length === 0) return;
+
+ const first = focusable[0];
+ const last = focusable[focusable.length - 1];
+
+ if (event.shiftKey && document.activeElement === first) {
+ event.preventDefault();
+ last.focus();
+ } else if (!event.shiftKey && document.activeElement === last) {
+ event.preventDefault();
+ first.focus();
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown);
+ previouslyFocusedRef.current?.focus();
+ };
+ }, [open, onClose]);
+
+ if (!mounted || !open) return null;
+
+ return createPortal(
+
+
event.stopPropagation()}
+ >
+ {children}
+
+
,
+ document.body,
+ );
+}
diff --git a/pages/services.js b/pages/services.js
index 56c1509c..881c028e 100644
--- a/pages/services.js
+++ b/pages/services.js
@@ -1,5 +1,24 @@
+import { useId, useState } from 'react';
import WhatWeOffer from '@/components/services/WhatWeOffer';
+import Dialog from '@/components/overlays/Dialog';
export default function Services() {
- return ;
+ const [open, setOpen] = useState(false);
+ const titleId = useId();
+
+ return (
+ <>
+
+ setOpen(true)}>
+ Open test dialog
+
+ setOpen(false)} titleId={titleId}>
+ Test dialog
+ This is a throwaway dialog to verify the Dialog primitive.
+ setOpen(false)}>
+ Close
+
+
+ >
+ );
}
From 21e0f95ac18019cdf02e7011d2e47402f8913b2b Mon Sep 17 00:00:00 2001
From: shayla-develops-webs
Date: Tue, 25 Aug 2026 18:52:29 -0500
Subject: [PATCH 06/12] Add ServiceDialog with hand-built CloseIcon and
Primary/Secondary variants
---
components/icons/CloseIcon/index.js | 18 +++++
components/overlays/Dialog/Dialog.module.scss | 1 -
components/overlays/Dialog/index.js | 10 ++-
.../ServiceDialog/ServiceDialog.module.scss | 68 +++++++++++++++++++
components/services/ServiceDialog/index.js | 39 +++++++++++
pages/services.js | 38 +++++++----
6 files changed, 159 insertions(+), 15 deletions(-)
create mode 100644 components/icons/CloseIcon/index.js
create mode 100644 components/services/ServiceDialog/ServiceDialog.module.scss
create mode 100644 components/services/ServiceDialog/index.js
diff --git a/components/icons/CloseIcon/index.js b/components/icons/CloseIcon/index.js
new file mode 100644
index 00000000..e83521de
--- /dev/null
+++ b/components/icons/CloseIcon/index.js
@@ -0,0 +1,18 @@
+export default function CloseIcon(props) {
+ const { className, fill = 'var(--color-black)' } = props;
+
+ return (
+
+
+
+ );
+}
diff --git a/components/overlays/Dialog/Dialog.module.scss b/components/overlays/Dialog/Dialog.module.scss
index 2c674dcd..1b90108a 100644
--- a/components/overlays/Dialog/Dialog.module.scss
+++ b/components/overlays/Dialog/Dialog.module.scss
@@ -10,7 +10,6 @@
}
.panel {
- background-color: var(--color-white);
border-radius: 1rem;
max-width: 40rem;
width: 100%;
diff --git a/components/overlays/Dialog/index.js b/components/overlays/Dialog/index.js
index 21d89c8c..5152891d 100644
--- a/components/overlays/Dialog/index.js
+++ b/components/overlays/Dialog/index.js
@@ -5,7 +5,13 @@ import styles from './Dialog.module.scss';
const FOCUSABLE_SELECTOR =
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
-export default function Dialog({ open, onClose, titleId, children }) {
+export default function Dialog({
+ open,
+ onClose,
+ titleId,
+ className,
+ children,
+}) {
const [mounted, setMounted] = useState(false);
const panelRef = useRef(null);
const previouslyFocusedRef = useRef(null);
@@ -58,7 +64,7 @@ export default function Dialog({ open, onClose, titleId, children }) {
return createPortal(
+
+
+ {label}
+
+
+
+
+
+
{description}
+
+ );
+}
diff --git a/pages/services.js b/pages/services.js
index 881c028e..4a8d0179 100644
--- a/pages/services.js
+++ b/pages/services.js
@@ -1,24 +1,38 @@
import { useId, useState } from 'react';
import WhatWeOffer from '@/components/services/WhatWeOffer';
-import Dialog from '@/components/overlays/Dialog';
+import ServiceDialog from '@/components/services/ServiceDialog';
export default function Services() {
- const [open, setOpen] = useState(false);
- const titleId = useId();
+ const [primaryOpen, setPrimaryOpen] = useState(false);
+ const [secondaryOpen, setSecondaryOpen] = useState(false);
+ const primaryTitleId = useId();
+ const secondaryTitleId = useId();
return (
<>
-
setOpen(true)}>
- Open test dialog
+ setPrimaryOpen(true)}>
+ Open primary test dialog
- setOpen(false)} titleId={titleId}>
- Test dialog
- This is a throwaway dialog to verify the Dialog primitive.
- setOpen(false)}>
- Close
-
-
+ setSecondaryOpen(true)}>
+ Open secondary test dialog
+
+ setPrimaryOpen(false)}
+ titleId={primaryTitleId}
+ variant='primary'
+ label='Web Development'
+ description='This is placeholder dialog content for testing the primary variant.'
+ />
+ setSecondaryOpen(false)}
+ titleId={secondaryTitleId}
+ variant='secondary'
+ label='Product & Project Management'
+ description='This is placeholder dialog content for testing the secondary variant.'
+ />
>
);
}
From d5d6c08111ec0e86bc01d2ceaf4803ac039980ea Mon Sep 17 00:00:00 2001
From: shayla-develops-webs
Date: Tue, 25 Aug 2026 19:12:31 -0500
Subject: [PATCH 07/12] Add services data file and wire pill clicks to open
ServiceDialog
---
components/services/WhatWeOffer/index.js | 51 ++++++++++++----------
pages/services.js | 35 +--------------
utils/services.js | 54 ++++++++++++++++++++++++
3 files changed, 83 insertions(+), 57 deletions(-)
create mode 100644 utils/services.js
diff --git a/components/services/WhatWeOffer/index.js b/components/services/WhatWeOffer/index.js
index 609581b5..e4261c24 100644
--- a/components/services/WhatWeOffer/index.js
+++ b/components/services/WhatWeOffer/index.js
@@ -1,11 +1,14 @@
+import { useId, useState } from 'react';
import Pill from '@/components/services/Pill';
import Container from '@/components/containers/Container';
+import ServiceDialog from '@/components/services/ServiceDialog';
+import { services } from '@/utils/services';
import { useAutoScroll } from './useAutoScroll';
import styles from './WhatWeOffer.module.scss';
const rowDirections = ['left', 'right', 'left'];
-function PillRow({ pills, direction }) {
+function PillRow({ pills, direction, onSelect }) {
const { viewportRef, groupRef, pause, resume } = useAutoScroll({
direction,
});
@@ -22,7 +25,12 @@ function PillRow({ pills, direction }) {
{pills.map(pill => (
-
+
onSelect(pill)}
+ />
))}
@@ -32,6 +40,7 @@ function PillRow({ pills, direction }) {
label={pill.label}
variant={pill.variant}
tabIndex={-1}
+ onClick={() => onSelect(pill)}
/>
))}
@@ -40,27 +49,10 @@ function PillRow({ pills, direction }) {
);
}
-const offerPills = [
- { id: 'web-development', label: 'Web Development', variant: 'primary' },
- {
- id: 'product-project-management',
- label: 'Product & Project Management',
- variant: 'secondary',
- },
- { id: 'design', label: 'Design', variant: 'primary' },
- {
- id: 'startup-nonprofit-tech-support',
- label: 'Startup & Nonprofit Tech Support',
- variant: 'secondary',
- },
- {
- id: 'optimization-growth',
- label: 'Optimization & Growth',
- variant: 'primary',
- },
-];
-
export default function WhatWeOffer() {
+ const [activeService, setActiveService] = useState(null);
+ const titleId = useId();
+
return (
@@ -73,10 +65,23 @@ export default function WhatWeOffer() {
{rowDirections.map((direction, rowIndex) => (
-
+
))}
+ setActiveService(null)}
+ titleId={titleId}
+ label={activeService?.label}
+ description={activeService?.description}
+ variant={activeService?.variant}
+ />
);
}
diff --git a/pages/services.js b/pages/services.js
index 4a8d0179..56c1509c 100644
--- a/pages/services.js
+++ b/pages/services.js
@@ -1,38 +1,5 @@
-import { useId, useState } from 'react';
import WhatWeOffer from '@/components/services/WhatWeOffer';
-import ServiceDialog from '@/components/services/ServiceDialog';
export default function Services() {
- const [primaryOpen, setPrimaryOpen] = useState(false);
- const [secondaryOpen, setSecondaryOpen] = useState(false);
- const primaryTitleId = useId();
- const secondaryTitleId = useId();
-
- return (
- <>
-
-
setPrimaryOpen(true)}>
- Open primary test dialog
-
-
setSecondaryOpen(true)}>
- Open secondary test dialog
-
-
setPrimaryOpen(false)}
- titleId={primaryTitleId}
- variant='primary'
- label='Web Development'
- description='This is placeholder dialog content for testing the primary variant.'
- />
- setSecondaryOpen(false)}
- titleId={secondaryTitleId}
- variant='secondary'
- label='Product & Project Management'
- description='This is placeholder dialog content for testing the secondary variant.'
- />
- >
- );
+ return ;
}
diff --git a/utils/services.js b/utils/services.js
new file mode 100644
index 00000000..3dd38929
--- /dev/null
+++ b/utils/services.js
@@ -0,0 +1,54 @@
+export const services = [
+ {
+ id: 'web-development',
+ label: 'Web Development',
+ variant: 'primary',
+ subServices: ['Custom Websites', 'CMS Development (WordPress)'],
+ description:
+ 'We create modern, responsive websites that are fast, secure, and tailored to your business—designed to engage users, drive results, and grow your online presence.',
+ },
+ {
+ id: 'product-project-management',
+ label: 'Product & Project Management',
+ variant: 'secondary',
+ subServices: [
+ 'Agile Project Management',
+ 'Product Discovery',
+ 'Technical Roadmapping',
+ ],
+ description:
+ 'Our service ensures every project is strategically planned, efficiently executed, and successfully delivered—on time, within budget, and aligned with your business goals.',
+ },
+ {
+ id: 'design',
+ label: 'Design',
+ variant: 'primary',
+ subServices: ['UI/UX Design', 'Product Design', 'UX Audits'],
+ description:
+ 'We craft intuitive, user-centered designs — from UI/UX and product design to UX audits — that make every digital experience clear, engaging, and easy to use.',
+ },
+ {
+ id: 'startup-nonprofit-tech-support',
+ label: 'Startup & Nonprofit Tech Support',
+ variant: 'secondary',
+ subServices: [
+ 'MVP Development',
+ 'Technical Advisory',
+ 'Digital Transformation for Nonprofits',
+ ],
+ description:
+ 'We support startups and nonprofits with MVP development, technical advisory, and digital transformation — helping mission-driven teams build smart and move fast, without compromising on quality.',
+ },
+ {
+ id: 'optimization-growth',
+ label: 'Optimization & Growth',
+ variant: 'primary',
+ subServices: [
+ 'SEO Optimization',
+ 'Website Performance Optimization',
+ 'Accessibility Improvements',
+ ],
+ description:
+ 'We help your website perform at its best through SEO optimization, performance tuning, and accessibility improvements — keeping you visible, fast, and inclusive for every user.',
+ },
+];
From a0c45d939ed4c7de5ed616cc62df82c2dacba840 Mon Sep 17 00:00:00 2001
From: shayla-develops-webs
Date: Thu, 3 Sep 2026 16:31:01 -0500
Subject: [PATCH 08/12] Fix ServiceDialog mobile/desktop sizing, decoration
assets, and overflow bugs
---
components/overlays/Dialog/Dialog.module.scss | 11 +++-
components/overlays/Dialog/index.js | 34 +++++++++++-
components/services/Pill/Pill.module.scss | 4 +-
.../ServiceDialog/ServiceDialog.module.scss | 47 ++++++++++++++--
components/services/ServiceDialog/index.js | 1 +
.../WhatWeOffer/WhatWeOffer.module.scss | 53 +++++++++++++++++--
components/services/WhatWeOffer/index.js | 47 ++++++++++------
styles/globals.scss | 1 +
8 files changed, 169 insertions(+), 29 deletions(-)
diff --git a/components/overlays/Dialog/Dialog.module.scss b/components/overlays/Dialog/Dialog.module.scss
index 1b90108a..15bb251a 100644
--- a/components/overlays/Dialog/Dialog.module.scss
+++ b/components/overlays/Dialog/Dialog.module.scss
@@ -1,7 +1,9 @@
+@use '@/styles/index' as *;
+
.backdrop {
position: fixed;
inset: 0;
- background-color: rgba(0, 0, 0, 0.5);
+ background-color: var(--overlay-backdrop);
display: flex;
align-items: center;
justify-content: center;
@@ -13,7 +15,14 @@
border-radius: 1rem;
max-width: 40rem;
width: 100%;
+ min-width: 0;
max-height: 90vh;
overflow-y: auto;
padding: 2rem;
}
+
+.containedPanel {
+ border-radius: 1rem;
+ min-width: 0;
+ padding: 2rem;
+}
diff --git a/components/overlays/Dialog/index.js b/components/overlays/Dialog/index.js
index 5152891d..bd29196a 100644
--- a/components/overlays/Dialog/index.js
+++ b/components/overlays/Dialog/index.js
@@ -10,6 +10,7 @@ export default function Dialog({
onClose,
titleId,
className,
+ contained = false,
children,
}) {
const [mounted, setMounted] = useState(false);
@@ -51,15 +52,44 @@ export default function Dialog({
}
};
+ const handleOutsideClick = event => {
+ if (panel && !panel.contains(event.target)) {
+ onClose();
+ }
+ };
+
document.addEventListener('keydown', handleKeyDown);
+ if (contained) {
+ document.addEventListener('mousedown', handleOutsideClick);
+ }
return () => {
document.removeEventListener('keydown', handleKeyDown);
+ if (contained) {
+ document.removeEventListener('mousedown', handleOutsideClick);
+ }
previouslyFocusedRef.current?.focus();
};
- }, [open, onClose]);
+ }, [open, onClose, contained]);
+
+ if (!open) return null;
+
+ if (contained) {
+ return (
+
+ {children}
+
+ );
+ }
- if (!mounted || !open) return null;
+ if (!mounted) return null;
return createPortal(
diff --git a/components/services/Pill/Pill.module.scss b/components/services/Pill/Pill.module.scss
index 2c76a29f..7c295e97 100644
--- a/components/services/Pill/Pill.module.scss
+++ b/components/services/Pill/Pill.module.scss
@@ -13,13 +13,13 @@
white-space: nowrap;
cursor: pointer;
padding: 0.5rem 1rem;
- font-size: 1rem;
+ font-size: $font-size-2xs;
line-height: 1.5rem;
@include transition(all 0.3s ease);
@include desktop {
padding: 0.75rem 2rem;
- font-size: 1.5rem;
+ font-size: $font-size-sm;
line-height: 2rem;
}
diff --git a/components/services/ServiceDialog/ServiceDialog.module.scss b/components/services/ServiceDialog/ServiceDialog.module.scss
index 5fded2a9..3d3d59f4 100644
--- a/components/services/ServiceDialog/ServiceDialog.module.scss
+++ b/components/services/ServiceDialog/ServiceDialog.module.scss
@@ -1,10 +1,33 @@
@use '@/styles/index' as *;
.panel {
+ position: absolute;
+ min-height: 100%;
+ z-index: 1;
display: flex;
flex-direction: column;
gap: 1.5rem;
+ @include mobile {
+ top: -0.5rem;
+ right: -2rem;
+ bottom: -4rem;
+ left: -2rem;
+ }
+
+ @include tablet {
+ top: -1rem;
+ right: -2rem;
+ left: -2rem;
+ }
+
+ @include desktop {
+ top: -1rem;
+ right: -2rem;
+ bottom: -2rem;
+ left: -2rem;
+ }
+
&.primary {
background-color: var(--color-primary-accent);
}
@@ -19,6 +42,7 @@
align-items: center;
justify-content: space-between;
gap: 1rem;
+ min-width: 0;
}
.echoPill {
@@ -30,10 +54,17 @@
color: var(--color-white);
font-family: $font-family-primary;
font-weight: $font-weight-regular;
- padding: 0.75rem 2rem;
- font-size: 1.5rem;
- line-height: 2rem;
+ padding: 0.25rem 0.75rem;
+ font-size: 0.75rem;
+ line-height: 1rem;
white-space: nowrap;
+ flex-shrink: 0;
+
+ @include desktop {
+ padding: 0.5rem 1.25rem;
+ font-size: 1rem;
+ line-height: 1.5rem;
+ }
}
.closeButton {
@@ -61,8 +92,14 @@
.description {
font-family: $font-family-primary;
- font-size: 1.5rem;
- line-height: 2rem;
+ font-size: $font-size-2xs;
+ line-height: 1.5rem;
color: var(--color-primary-content);
+ word-break: break-word;
margin: 0;
+
+ @include desktop {
+ font-size: $font-size-sm;
+ line-height: 2rem;
+ }
}
diff --git a/components/services/ServiceDialog/index.js b/components/services/ServiceDialog/index.js
index a8f118c0..cae3d60c 100644
--- a/components/services/ServiceDialog/index.js
+++ b/components/services/ServiceDialog/index.js
@@ -19,6 +19,7 @@ export default function ServiceDialog({
onClose={onClose}
titleId={titleId}
className={panelClass}
+ contained
>
diff --git a/components/services/WhatWeOffer/WhatWeOffer.module.scss b/components/services/WhatWeOffer/WhatWeOffer.module.scss
index dae0b52f..0b1ad963 100644
--- a/components/services/WhatWeOffer/WhatWeOffer.module.scss
+++ b/components/services/WhatWeOffer/WhatWeOffer.module.scss
@@ -1,7 +1,10 @@
@use '@/styles/index' as *;
.whatWeOffer {
+ position: relative;
width: 100%;
+ overflow-x: hidden;
+ overflow-y: visible;
background-color: var(--color-white);
padding: 4rem 0;
@@ -10,10 +13,50 @@
}
}
+.leftDecoration {
+ display: none;
+ pointer-events: none;
+
+ @include desktop {
+ display: block;
+ position: absolute;
+ top: 50%;
+ left: -17rem;
+ transform: translateY(-50%);
+ height: 14rem;
+ width: auto;
+ }
+}
+
+.rightDecoration {
+ display: none;
+ pointer-events: none;
+
+ @include desktop {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ position: absolute;
+ top: 50%;
+ right: -24rem;
+ transform: translateY(-50%);
+ }
+}
+
+.slash {
+ height: 19rem;
+ width: auto;
+}
+
+.angleBracket {
+ height: 14rem;
+ width: auto;
+}
+
.heading {
font-family: $font-family-secondary;
font-weight: $font-weight-bold;
- font-size: 2.25rem;
+ font-size: $font-size-md;
line-height: 2.75rem;
text-align: center;
margin: 0 0 1.5rem;
@@ -26,18 +69,22 @@
.body {
font-family: $font-family-primary;
- font-size: 1rem;
+ font-size: $font-size-2xs;
line-height: 1.5rem;
text-align: center;
max-width: 60rem;
margin: 0 auto 2.5rem;
@include desktop {
- font-size: 1.5rem;
+ font-size: $font-size-sm;
line-height: 2rem;
}
}
+.pillRowsWrapper {
+ position: relative;
+}
+
.pillRows {
display: flex;
flex-direction: column;
diff --git a/components/services/WhatWeOffer/index.js b/components/services/WhatWeOffer/index.js
index e4261c24..87c3667d 100644
--- a/components/services/WhatWeOffer/index.js
+++ b/components/services/WhatWeOffer/index.js
@@ -63,25 +63,40 @@ export default function WhatWeOffer() {
project management and strategy — tailored to your
goals and budget, with no compromise on quality.
-
- {rowDirections.map((direction, rowIndex) => (
-
+
+
+
+
- ))}
+
+
+ {rowDirections.map((direction, rowIndex) => (
+
+ ))}
+
+ setActiveService(null)}
+ titleId={titleId}
+ label={activeService?.label}
+ description={activeService?.description}
+ variant={activeService?.variant}
+ />
- setActiveService(null)}
- titleId={titleId}
- label={activeService?.label}
- description={activeService?.description}
- variant={activeService?.variant}
- />
);
}
diff --git a/styles/globals.scss b/styles/globals.scss
index 718b9de7..34745cba 100644
--- a/styles/globals.scss
+++ b/styles/globals.scss
@@ -16,6 +16,7 @@
--color-secondary-surface: #75cce3;
--color-transparent: transparent;
--color-box-shadow: rgba(0, 0, 0, 0.08);
+ --overlay-backdrop: rgba(0, 0, 0, 0.5);
--dark-bg: #023047;
--error: #be1313;
--shadow-default: 0px 8px 24px rgba(0, 0, 0, 0.15);
From 5aad9c943abbf5825d5df7af466a57d9aab02cb6 Mon Sep 17 00:00:00 2001
From: shayla-develops-webs
Date: Thu, 3 Sep 2026 16:39:14 -0500
Subject: [PATCH 09/12] move useAutoScroll to hooks
---
components/services/WhatWeOffer/index.js | 2 +-
{components/services/WhatWeOffer => hooks}/useAutoScroll.js | 0
2 files changed, 1 insertion(+), 1 deletion(-)
rename {components/services/WhatWeOffer => hooks}/useAutoScroll.js (100%)
diff --git a/components/services/WhatWeOffer/index.js b/components/services/WhatWeOffer/index.js
index 87c3667d..6b9b7a5d 100644
--- a/components/services/WhatWeOffer/index.js
+++ b/components/services/WhatWeOffer/index.js
@@ -3,7 +3,7 @@ import Pill from '@/components/services/Pill';
import Container from '@/components/containers/Container';
import ServiceDialog from '@/components/services/ServiceDialog';
import { services } from '@/utils/services';
-import { useAutoScroll } from './useAutoScroll';
+import { useAutoScroll } from '@/hooks/useAutoScroll';
import styles from './WhatWeOffer.module.scss';
const rowDirections = ['left', 'right', 'left'];
diff --git a/components/services/WhatWeOffer/useAutoScroll.js b/hooks/useAutoScroll.js
similarity index 100%
rename from components/services/WhatWeOffer/useAutoScroll.js
rename to hooks/useAutoScroll.js
From 90a1b5c966023394d62fe112d954f5419b9e76de Mon Sep 17 00:00:00 2001
From: shayla-develops-webs
Date: Thu, 3 Sep 2026 17:08:30 -0500
Subject: [PATCH 10/12] changelog updated
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f27fb910..8f196699 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -234,6 +234,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Introduced SDD structure for Services Page
- Updated SDD docs to include new Service page issues
+- Added "What We Offer" section to the Services page
### Fixed
From 1c6c53eeb197884de6691b813c55314a68673560 Mon Sep 17 00:00:00 2001
From: shayla-develops-webs
Date: Thu, 3 Sep 2026 18:04:32 -0500
Subject: [PATCH 11/12] refactor pill movement based on row index
---
components/services/WhatWeOffer/index.js | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/components/services/WhatWeOffer/index.js b/components/services/WhatWeOffer/index.js
index 6b9b7a5d..d4ac8df2 100644
--- a/components/services/WhatWeOffer/index.js
+++ b/components/services/WhatWeOffer/index.js
@@ -6,7 +6,12 @@ import { services } from '@/utils/services';
import { useAutoScroll } from '@/hooks/useAutoScroll';
import styles from './WhatWeOffer.module.scss';
-const rowDirections = ['left', 'right', 'left'];
+const ROW_COUNT = 3;
+const rowIndexes = Array.from({ length: ROW_COUNT }, (_, index) => index);
+
+function getRowDirection(rowIndex) {
+ return rowIndex % 2 === 0 ? 'left' : 'right';
+}
function PillRow({ pills, direction, onSelect }) {
const { viewportRef, groupRef, pause, resume } = useAutoScroll({
@@ -78,11 +83,11 @@ export default function WhatWeOffer() {
/>
- {rowDirections.map((direction, rowIndex) => (
+ {rowIndexes.map(rowIndex => (
))}
From 4d33c9809d8dc3a4860bf8d92b6abf34060744e2 Mon Sep 17 00:00:00 2001
From: shayla-develops-webs
Date: Thu, 3 Sep 2026 19:58:27 -0500
Subject: [PATCH 12/12] Add staged entrance animation
---
.../WhatWeOffer/WhatWeOffer.module.scss | 29 +++++++-
components/services/WhatWeOffer/index.js | 66 +++++++++++++++----
hooks/useAutoScroll.js | 10 ++-
3 files changed, 88 insertions(+), 17 deletions(-)
diff --git a/components/services/WhatWeOffer/WhatWeOffer.module.scss b/components/services/WhatWeOffer/WhatWeOffer.module.scss
index 0b1ad963..3d32c6fb 100644
--- a/components/services/WhatWeOffer/WhatWeOffer.module.scss
+++ b/components/services/WhatWeOffer/WhatWeOffer.module.scss
@@ -13,6 +13,17 @@
}
}
+.content {
+ opacity: 0;
+ transform: translateY(-16rem);
+ @include transition(transform 1s, opacity 1s);
+
+ &.revealed {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
.leftDecoration {
display: none;
pointer-events: none;
@@ -22,9 +33,16 @@
position: absolute;
top: 50%;
left: -17rem;
- transform: translateY(-50%);
height: 14rem;
width: auto;
+ opacity: 0;
+ transform: translateY(-50%) translateX(-13rem);
+ @include transition(transform 1s, opacity 1s);
+
+ &.revealed {
+ opacity: 1;
+ transform: translateY(-50%) translateX(0);
+ }
}
}
@@ -39,7 +57,14 @@
position: absolute;
top: 50%;
right: -24rem;
- transform: translateY(-50%);
+ opacity: 0;
+ transform: translateY(-50%) translateX(13rem);
+ @include transition(transform 1s, opacity 1s);
+
+ &.revealed {
+ opacity: 1;
+ transform: translateY(-50%) translateX(0);
+ }
}
}
diff --git a/components/services/WhatWeOffer/index.js b/components/services/WhatWeOffer/index.js
index d4ac8df2..45835c9b 100644
--- a/components/services/WhatWeOffer/index.js
+++ b/components/services/WhatWeOffer/index.js
@@ -1,21 +1,25 @@
-import { useId, useState } from 'react';
+import { useEffect, useId, useState } from 'react';
import Pill from '@/components/services/Pill';
import Container from '@/components/containers/Container';
import ServiceDialog from '@/components/services/ServiceDialog';
import { services } from '@/utils/services';
import { useAutoScroll } from '@/hooks/useAutoScroll';
+import { useIntersect } from '@/hooks/useIntersect';
+import { combineClasses } from '@/utils/classnames';
import styles from './WhatWeOffer.module.scss';
const ROW_COUNT = 3;
const rowIndexes = Array.from({ length: ROW_COUNT }, (_, index) => index);
+const REVEAL_TRANSITION_MS = 1000;
function getRowDirection(rowIndex) {
return rowIndex % 2 === 0 ? 'left' : 'right';
}
-function PillRow({ pills, direction, onSelect }) {
+function PillRow({ pills, direction, enabled, onSelect }) {
const { viewportRef, groupRef, pause, resume } = useAutoScroll({
direction,
+ enabled,
});
return (
@@ -57,24 +61,61 @@ function PillRow({ pills, direction, onSelect }) {
export default function WhatWeOffer() {
const [activeService, setActiveService] = useState(null);
const titleId = useId();
+ const [setSectionNode, entry] = useIntersect({});
+ const [revealed, setRevealed] = useState(false);
+ const [marqueeEnabled, setMarqueeEnabled] = useState(false);
+
+ useEffect(() => {
+ if (entry.isIntersecting && !revealed) {
+ setRevealed(true);
+ }
+ }, [entry.isIntersecting, revealed]);
+
+ useEffect(() => {
+ if (!revealed) return undefined;
+
+ const timeoutId = setTimeout(
+ () => setMarqueeEnabled(true),
+ REVEAL_TRANSITION_MS,
+ );
+ return () => clearTimeout(timeoutId);
+ }, [revealed]);
+
+ const contentClass = combineClasses(
+ styles.content,
+ revealed ? 'revealed' : null,
+ styles,
+ );
+ const leftDecorationClass = combineClasses(
+ styles.leftDecoration,
+ revealed ? 'revealed' : null,
+ styles,
+ );
+ const rightDecorationClass = combineClasses(
+ styles.rightDecoration,
+ revealed ? 'revealed' : null,
+ styles,
+ );
return (
-
+
- What We Offer
-
- We deliver high-end digital solutions — from{' '}
- design and development to{' '}
- project management and strategy — tailored to your
- goals and budget, with no compromise on quality.
-
+
+
What We Offer
+
+ We deliver high-end digital solutions — from{' '}
+ design and development to{' '}
+ project management and strategy — tailored to your
+ goals and budget, with no compromise on quality.
+
+
-
+
))}
diff --git a/hooks/useAutoScroll.js b/hooks/useAutoScroll.js
index f6b085e1..3ba5b548 100644
--- a/hooks/useAutoScroll.js
+++ b/hooks/useAutoScroll.js
@@ -1,6 +1,10 @@
import { useEffect, useRef } from 'react';
-export function useAutoScroll({ direction = 'left', speed = 60 } = {}) {
+export function useAutoScroll({
+ direction = 'left',
+ speed = 60,
+ enabled = true,
+} = {}) {
const viewportRef = useRef(null);
const groupRef = useRef(null);
const pausedRef = useRef(false);
@@ -8,7 +12,7 @@ export function useAutoScroll({ direction = 'left', speed = 60 } = {}) {
useEffect(() => {
const viewport = viewportRef.current;
const group = groupRef.current;
- if (!viewport || !group) return undefined;
+ if (!viewport || !group || !enabled) return undefined;
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)',
@@ -39,7 +43,7 @@ export function useAutoScroll({ direction = 'left', speed = 60 } = {}) {
frameId = requestAnimationFrame(step);
return () => cancelAnimationFrame(frameId);
- }, [direction, speed]);
+ }, [direction, speed, enabled]);
const pause = () => {
pausedRef.current = true;