From e346db862c3c8e292f4d5bebbaf9010cb9243518 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Sat, 13 Dec 2025 18:33:11 +0300 Subject: [PATCH 01/53] Moves theme picker style sto CSS module --- _TODO.md | 13 +- src/components/ThemePicker/index.astro | 118 ++++++++---------- .../ThemePicker/themePicker.module.css | 61 +++++++++ 3 files changed, 115 insertions(+), 77 deletions(-) create mode 100644 src/components/ThemePicker/themePicker.module.css diff --git a/_TODO.md b/_TODO.md index 9ca677f4a..cfefe0d03 100644 --- a/_TODO.md +++ b/_TODO.md @@ -17,16 +17,17 @@ continue `/domain` -- Entities are part of the domain. They represent state and core business rules, but not persistence logic. Defined primarily by its unique identity, rather than its attributes or properties. - -Is an entry point to the domain logic forming the core of the application, modifying state and persistence as needed. This may be a Transaction Script, Service Layer, Application Service, or something similar. +- The Domain is an entry point to the domain logic forming the core of the application, modifying state and persistence as needed. This may be a Transaction Script, Service Layer, Application Service, or something similar. The Domain in ADR relates to the whole of the domain objects, all the entities and their relations as a whole `/entities` or `entities.ts` -- The responder builds the entire HTTP response from the domain's output which is given to it by the action. The Responder is responsible solely for formatting the final response (e.g., JSON, HTML) to be sent back to the client. +- Entities are part of the domain. They represent state and core business rules, but not persistence logic. Defined primarily by its unique identity, rather than its attributes or properties. + +- By any name the Domain Payload Object is a specialized Data Transfer Object. A Data Transfer Object is a fine-grained object, providing properties that mirror or at least shadow properties found on the domain objects that they replicate. A Domain Payload Object is a coarse-grained object that transfers whole domain object instances to the client. `/responders` or `responders.ts` +- The responder builds the entire HTTP response from the domain's output which is given to it by the action. The Responder is responsible solely for formatting the final response (e.g., JSON, HTML) to be sent back to the client. ### Endpoints: @@ -145,10 +146,6 @@ cat.structure: Rules related to the document's overall structure, like the prope cat.tables: Rules for data tables, including headers and associations. cat.text-alternatives: Rules for ensuring that text alternatives are provided for non-text content, such as images. -## Use the Page Visibility API to pause videos, image carousels, and animations - -Stop unnecessary processes when the user doesn't see the page or inversely to perform background actions. - ## "Add to Calendar" button Google Calendar, Apple Calendar, Yahoo Calender, Microsoft 365, Outlook, and Teams, and generate iCal/ics files (for all other calendars and cases). diff --git a/src/components/ThemePicker/index.astro b/src/components/ThemePicker/index.astro index ff96070bf..bdd853bd7 100644 --- a/src/components/ThemePicker/index.astro +++ b/src/components/ThemePicker/index.astro @@ -6,6 +6,7 @@ */ import { themes } from '@components/ThemePicker/server' import Icon from '@components/Icon/index.astro' +import themePickerStyles from './themePicker.module.css' export interface Props { closeComponent?: string @@ -15,69 +16,9 @@ export interface Props { const { closeComponent = 'close', label = 'Select theme' } = Astro.props --- - - From 7927ac1ef16a1d86914237e4d9d1c93e00215db0 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Sat, 13 Dec 2025 23:27:40 +0300 Subject: [PATCH 06/53] ARIA improvements to Consent Preferences component --- .../client/__tests__/index.spec.ts | 32 ++++++++++ .../Consent/Preferences/client/index.ts | 42 ------------- .../Consent/Preferences/index.astro | 59 +++++++++++++------ 3 files changed, 73 insertions(+), 60 deletions(-) diff --git a/src/components/Consent/Preferences/client/__tests__/index.spec.ts b/src/components/Consent/Preferences/client/__tests__/index.spec.ts index 6983e1dbe..3edecb93c 100644 --- a/src/components/Consent/Preferences/client/__tests__/index.spec.ts +++ b/src/components/Consent/Preferences/client/__tests__/index.spec.ts @@ -123,6 +123,38 @@ describe('ConsentPreferencesElement', () => { window.localStorage.clear() }) }) + it('wires the preferences center labels and switch semantics', async () => { + await renderConsentPreferences(({ window }) => { + const section = window.document.querySelector('consent-preferences section') as HTMLElement | null + expect(section).not.toBeNull() + expect(section!.getAttribute('aria-labelledby')).toBe('consent-preferences__title') + expect(section!.getAttribute('aria-describedby')).toBe('consent-preferences__settings-desc') + + const pageTitle = window.document.getElementById('consent-preferences__title') + expect(pageTitle?.textContent).toContain('Privacy Preference Center') + + const settingsDesc = window.document.getElementById('consent-preferences__settings-desc') + expect(settingsDesc).not.toBeNull() + + const analyticsSwitch = window.document.getElementById('analytics-cookies') as HTMLInputElement | null + expect(analyticsSwitch).not.toBeNull() + expect(analyticsSwitch!.getAttribute('role')).toBe('switch') + expect(analyticsSwitch!.getAttribute('aria-labelledby')).toBe('consent-preferences__analytics-title') + expect(analyticsSwitch!.getAttribute('aria-describedby')).toBe('consent-preferences__analytics-desc') + + const functionalSwitch = window.document.getElementById('functional-cookies') as HTMLInputElement | null + expect(functionalSwitch).not.toBeNull() + expect(functionalSwitch!.getAttribute('role')).toBe('switch') + expect(functionalSwitch!.getAttribute('aria-labelledby')).toBe('consent-preferences__functional-title') + expect(functionalSwitch!.getAttribute('aria-describedby')).toBe('consent-preferences__functional-desc') + + const marketingSwitch = window.document.getElementById('marketing-cookies') as HTMLInputElement | null + expect(marketingSwitch).not.toBeNull() + expect(marketingSwitch!.getAttribute('role')).toBe('switch') + expect(marketingSwitch!.getAttribute('aria-labelledby')).toBe('consent-preferences__marketing-title') + expect(marketingSwitch!.getAttribute('aria-describedby')).toBe('consent-preferences__marketing-desc') + }) + }) it('grants all consent when Allow All is clicked', async () => { await renderConsentPreferences(({ window }) => { diff --git a/src/components/Consent/Preferences/client/index.ts b/src/components/Consent/Preferences/client/index.ts index 34c99fe9f..cff294f99 100644 --- a/src/components/Consent/Preferences/client/index.ts +++ b/src/components/Consent/Preferences/client/index.ts @@ -32,7 +32,6 @@ export class ConsentPreferencesElement extends HTMLElement { private beforePreparationHandler: (() => void) | null = null private afterSwapHandler: (() => void) | null = null private unsubscribeConsent: (() => void) | null = null - private toggleLabelHandler: ((_event: Event) => void) | null = null private isInitialized = false connectedCallback(): void { @@ -67,7 +66,6 @@ export class ConsentPreferencesElement extends HTMLElement { } this.removeViewTransitionsHandlers() - this.cleanupToggleLabelListeners() if (this.unsubscribeConsent) { this.unsubscribeConsent() @@ -141,7 +139,6 @@ export class ConsentPreferencesElement extends HTMLElement { this.afterSwapHandler = () => { this.isInitialized = false - this.cleanupToggleLabelListeners() this.initialize() } @@ -172,45 +169,6 @@ export class ConsentPreferencesElement extends HTMLElement { addButtonEventListeners(this.allowAllBtn, () => this.allowAll()) addButtonEventListeners(this.denyAllBtn, () => this.denyAll()) addButtonEventListeners(this.saveBtn, () => this.savePreferences()) - this.bindToggleLabelListeners() - } - - private bindToggleLabelListeners(): void { - if (this.toggleLabelHandler) { - return - } - - this.toggleLabelHandler = (event: Event) => { - const target = event.target - if (!(target instanceof HTMLElement)) { - return - } - - const label = target.closest('[data-consent-toggle]') - if (!label || !this.contains(label)) { - return - } - - event.preventDefault() - const checkboxId = label.getAttribute('data-consent-toggle') - if (!checkboxId) { - return - } - - const checkbox = document.getElementById(checkboxId) - if (isInputElement(checkbox)) { - checkbox.checked = !checkbox.checked - } - } - - document.addEventListener('click', this.toggleLabelHandler) - } - - private cleanupToggleLabelListeners(): void { - if (this.toggleLabelHandler) { - document.removeEventListener('click', this.toggleLabelHandler) - this.toggleLabelHandler = null - } } private updateCheckboxes(preferences: ConsentState): void { diff --git a/src/components/Consent/Preferences/index.astro b/src/components/Consent/Preferences/index.astro index 411f6e2b4..8a2948c3a 100644 --- a/src/components/Consent/Preferences/index.astro +++ b/src/components/Consent/Preferences/index.astro @@ -4,18 +4,22 @@ import logoSvg from '@assets/images/site/logo.svg' --- -
+
Webstack Builders corporate logo. -

Privacy Preference Center

+
{/* Introduction */}
-
-

+

-

+

-
From 12471f2bf1548a66f6b64c9562bcc661b4889b1a Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Sun, 14 Dec 2025 03:35:49 +0300 Subject: [PATCH 36/53] ARIA improvements to Case Studies item page layout --- src/pages/case-studies/index.astro | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/case-studies/index.astro b/src/pages/case-studies/index.astro index f66623ebb..7cb607188 100644 --- a/src/pages/case-studies/index.astro +++ b/src/pages/case-studies/index.astro @@ -23,7 +23,7 @@ const section = 'Case Studies' section={section} subtitle="Explore our successful projects and client outcomes" > -
+
{ sortedCaseStudies.map(caseStudy => { return ( @@ -64,7 +64,7 @@ const section = 'Case Studies' )} -
-
-

About Webstack Builders

+
+

+ About Webstack Builders +

We're a client-focused web application development team specializing in custom software solutions, modern web applications, and digital transformation for businesses of all @@ -200,13 +215,16 @@ const description = 'Offline fallback for Webstack Builders with guidance on wha

Support Available
- +
-
-

+
+

Get In Touch When You're Back Online -

+

Email: @@ -217,10 +235,11 @@ const description = 'Offline fallback for Webstack Builders with guidance on wha webstackbuilders.com
-
+
-
+
+

Try again

-
+
From 2bf4e32ff4f690b68a0f65a7f0e6dd49c35fa0ce Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Sun, 14 Dec 2025 04:17:16 +0300 Subject: [PATCH 42/53] ARIA improvements to Privacy page layout --- src/pages/privacy/index.astro | 152 ++++++++++++++++++++++++++-------- 1 file changed, 116 insertions(+), 36 deletions(-) diff --git a/src/pages/privacy/index.astro b/src/pages/privacy/index.astro index faf7df737..5c0804592 100644 --- a/src/pages/privacy/index.astro +++ b/src/pages/privacy/index.astro @@ -5,6 +5,13 @@ import contactData from '@content/contact.json' const pageTitle = 'Privacy Policy' const path = '/privacy' const description = 'Learn how Webstack Builders collects, uses, and protects your personal information.' +const lastUpdatedDate = new Date() +const lastUpdatedIso = lastUpdatedDate.toISOString() +const lastUpdatedText = lastUpdatedDate.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', +}) --- @@ -22,18 +29,12 @@ const description = 'Learn how Webstack Builders collects, uses, and protects yo

Last updated: { - new Date().toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - }) - }Last updated:

-
-

+
+

Information We Collect

@@ -69,8 +70,8 @@ const description = 'Learn how Webstack Builders collects, uses, and protects yo
-
-

+
+

How We Use Your Information

@@ -84,6 +85,8 @@ const description = 'Learn how Webstack Builders collects, uses, and protects yo fill="none" stroke="currentColor" viewBox="0 0 24 24" + aria-hidden="true" + focusable="false" >
-
-

+
+

Information Sharing

-
-

+
+

Data Security

@@ -241,6 +258,8 @@ const description = 'Learn how Webstack Builders collects, uses, and protects yo fill="none" stroke="currentColor" viewBox="0 0 24 24" + aria-hidden="true" + focusable="false" >

- +
- +
-
-

+
+

Cookies and Tracking

@@ -447,8 +476,11 @@ const description = 'Learn how Webstack Builders collects, uses, and protects yo

-
-

+
+

Strictly Necessary Storage

@@ -540,8 +572,11 @@ const description = 'Learn how Webstack Builders collects, uses, and protects yo

-
-

+
+

Error Monitoring and Diagnostics

@@ -587,32 +622,67 @@ const description = 'Learn how Webstack Builders collects, uses, and protects yo

- + Privacy Protections:

  • - + No IP addresses collected or stored
  • - + No session replay or screen recording
  • - + No performance tracking or monitoring
  • - + Data is used solely for technical troubleshooting and bug fixes @@ -633,8 +703,11 @@ const description = 'Learn how Webstack Builders collects, uses, and protects yo
-
-

+
+

Contact Us

@@ -652,6 +725,8 @@ const description = 'Learn how Webstack Builders collects, uses, and protects yo fill="none" stroke="currentColor" viewBox="0 0 24 24" + aria-hidden="true" + focusable="false" >
-
-

+
+

Changes to This Policy

From 3615a4165d1bf479d9db2a9ae78b888379a53edc Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Sun, 14 Dec 2025 04:21:30 +0300 Subject: [PATCH 43/53] ARIA improvements to Privacy 'my-data' page layout --- src/pages/privacy/my-data.astro | 91 +++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 27 deletions(-) diff --git a/src/pages/privacy/my-data.astro b/src/pages/privacy/my-data.astro index f7fbcc537..1c5d3eb78 100644 --- a/src/pages/privacy/my-data.astro +++ b/src/pages/privacy/my-data.astro @@ -23,7 +23,13 @@ const statusMessages: RecordMy Data & Privacy Rights

{status && statusMessages[status] && ( -
)} -
+
+

Overview

Under GDPR, you have the right to access and control your personal data. Use the forms below to request a copy of your data or request its deletion. @@ -42,8 +49,10 @@ const statusMessages: Record -

-

📥 Access My Data

+
+

+ Access My Data +

Request a copy of all personal data we have about you, including consent records and newsletter subscriptions.

@@ -62,15 +71,24 @@ const statusMessages: Record - +
-
-

🗑️ Delete My Data

+
+

+ Delete My Data +

- ⚠️ Warning: This will permanently delete all your data from our systems. This action cannot be undone. + Warning: This will permanently delete all your data from our systems. This action cannot be undone.

@@ -96,13 +114,20 @@ const statusMessages: Record - +
-
-

What Happens Next?

+
+

What Happens Next?

  1. Verification Email: We'll send a verification link to your email address.
  2. Click the Link: Click the link in the email to verify your request (expires in 24 hours).
  3. @@ -114,6 +139,26 @@ const statusMessages: Record From aacb796f78f132c3a9ab45e63df10f91699431d7 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Sun, 14 Dec 2025 04:33:27 +0300 Subject: [PATCH 44/53] ARIA improvements to Services list page layout --- src/pages/services/[slug].astro | 126 +++++++++++++++++--------------- 1 file changed, 68 insertions(+), 58 deletions(-) diff --git a/src/pages/services/[slug].astro b/src/pages/services/[slug].astro index 36a9d6fa1..06e3dcf31 100644 --- a/src/pages/services/[slug].astro +++ b/src/pages/services/[slug].astro @@ -55,65 +55,71 @@ const section = 'Downloads' ) } -
    - { - service.data.category && ( -
    -
    - Category +
    +
    + { + service.data.category && ( +
    +
    + Category +
    +
    {service.data.category}
    -
    {service.data.category}
    -
    - ) - } - { - service.data.pricing && ( -
    -
    - Pricing + ) + } + { + service.data.pricing && ( +
    +
    + Pricing +
    +
    {service.data.pricing}
    -
    {service.data.pricing}
    -
    - ) - } - { - service.data.duration && ( -
    -
    - Typical Duration + ) + } + { + service.data.duration && ( +
    +
    + Typical Duration +
    +
    {service.data.duration}
    -
    {service.data.duration}
    -
    - ) - } - { - service.data.deliverables && service.data.deliverables.length > 0 && ( -
    -
    - Deliverables + ) + } + { + service.data.deliverables && service.data.deliverables.length > 0 && ( +
    +
    + Deliverables +
    +
    +
      + {service.data.deliverables.map((deliverable: string) => ( +
    • + + {deliverable} +
    • + ))} +
    +
    -
      - {service.data.deliverables.map((deliverable: string) => ( -
    • - - - - {deliverable} -
    • - ))} -
    -
    - ) - } -
    + ) + } + +
{/** SLOT: after-content */}
@@ -122,11 +128,15 @@ const section = 'Downloads'
{/** SLOT: related-content */} -
+
-

Other Services I Offer

+
-
+
From afa5b0481c5a4ba443cfd25325c4905b80d89701 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Sun, 14 Dec 2025 04:35:58 +0300 Subject: [PATCH 45/53] ARIA improvements to Services item page layout --- src/pages/services/index.astro | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/services/index.astro b/src/pages/services/index.astro index 7a93e4bda..2818074c3 100644 --- a/src/pages/services/index.astro +++ b/src/pages/services/index.astro @@ -26,9 +26,12 @@ const section = 'Services' > { services.length > 0 && ( -
+
-

+

Our Services

@@ -73,6 +76,8 @@ const section = 'Services' fill="none" stroke="currentColor" viewBox="0 0 24 24" + aria-hidden="true" + focusable="false" > Date: Sun, 14 Dec 2025 04:38:49 +0300 Subject: [PATCH 46/53] ARIA improvements to Tags list page layout --- src/pages/tags/[tag].astro | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/pages/tags/[tag].astro b/src/pages/tags/[tag].astro index c80a04496..b40c3d754 100644 --- a/src/pages/tags/[tag].astro +++ b/src/pages/tags/[tag].astro @@ -155,7 +155,11 @@ const section = 'Tags' { paginatedContent.length > 0 ? ( <> -
+
+

Tagged content

{paginatedContent.map(item => { const typeMeta = typeMetadata[item.type] const href = `/${typeMeta.hrefBase}/${item.id}` @@ -188,7 +192,10 @@ const section = 'Tags' > {typeMeta.label} -