From 9fafa6f735b9090eaa7009387eee8b6e5d378363 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Tue, 21 Apr 2026 16:31:15 +0300 Subject: [PATCH 1/7] Add 'my-data' sample output --- _TODO.md | 20 +- my-data-1776734445651.json | 76 ++++++++ .../MyData/client/__tests__/index.spec.ts | 41 ++++ src/components/Pages/MyData/client/index.ts | 178 +++++++++++++++++- src/components/Pages/MyData/index.astro | 31 ++- .../Pages/Resume}/index.css | 0 .../Pages/Resume/partials/Controls.astro | 4 +- .../Pages/Resume/partials/Persona.astro | 4 +- src/pages/resume/index.astro | 3 - 9 files changed, 345 insertions(+), 12 deletions(-) create mode 100644 my-data-1776734445651.json rename src/{pages/resume => components/Pages/Resume}/index.css (100%) diff --git a/_TODO.md b/_TODO.md index 936b66832..bf2ac7853 100644 --- a/_TODO.md +++ b/_TODO.md @@ -23,6 +23,24 @@ https://aws.plainenglish.io/how-to-build-a-chatbot-using-aws-lex-and-lambda-in-2 - Moving the scroll bar up quickly with the mouse seems to make the header logic break - the Switcher component and Breadcrumbs are hidden under the header - Themepicker and search icon are too big in non-squished header. Logo too - the initial presentation should be smaller. +## GDPR Compliance for "My Data" + +**Weaknesses: Missing Information for Right of Access (Article 15)** + +The Right of Access is broader than just a data dump. A complete response must include specific supplementary information that your JSON currently lacks: + +- Retention Periods: You must state how long you intend to keep each category of data. +- Third-Party Recipients: You must list any recipients or categories of recipients (like email service providers or analytics tools) with whom this data has been shared. +- Data Subject Rights: The response should remind the user of their other rights, such as the right to request erasure, rectification, or to lodge a complaint with a supervisory authority. +- Source of Data: While your JSON shows "source," it should explicitly confirm if any data was collected from third parties rather than the subject themselves. + +**Technical Observations** + +- Privacy Policy Version: Your output shows "privacyPolicyVersion": "1970-01-01". This looks like a placeholder or a Unix epoch default; ensure this reflects the actual policy version the user agreed to for each record to maintain a valid audit trail. +- Consent Text: Most records show "consentText": null. GDPR requires you to be able to demonstrate what the subject consented to; providing the actual text shown to the user at the time is a best practice for documenting valid consent. + +Next Step: To be fully compliant, you should pair this JSON file with a summary document (often a PDF or HTML page) that includes the missing legal disclosures (retention, recipients, and rights) or include those fields directly in your JSON schema. + ## Resume - Finish styling @@ -30,9 +48,9 @@ https://aws.plainenglish.io/how-to-build-a-chatbot-using-aws-lex-and-lambda-in-2 ## Contact Form - `0/2000` characters should show number of characters left instead +- Workflow right now puts the "Success" toast under the submit button when the submit button returns to normal after a submission. It seems like the button should have some time out after a successful submission to make sure it's not hammered, like five seconds. And it just looks visually odd - maybe the button should be part of the layout of the success toast, or moved down under it. ## Newsletter / MJML Templates -- We need to make sure the images point to the full production URL, not a relative import - Need to move the unsubscribe link into an Action and handle it entirely within our website instead of on Hubspot - Need to add a newsletter publishing workflow as an action, using the newsletter static segment imported from Hubspot diff --git a/my-data-1776734445651.json b/my-data-1776734445651.json new file mode 100644 index 000000000..9155143ca --- /dev/null +++ b/my-data-1776734445651.json @@ -0,0 +1,76 @@ +{ + "email": "kevinbrown2354@gmail.com", + "requestDate": "2026-04-21T01:19:13.111Z", + "consentRecords": [ + { + "id": "f71945fc-abfe-467b-abb9-a7a904c9df6d", + "dataSubjectId": "ea27f0b9-57b3-4f4d-874d-e35438cd9adb", + "email": "kevinbrown2354@gmail.com", + "purposes": [ + "marketing" + ], + "source": "newsletter_form", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", + "privacyPolicyVersion": "1970-01-01", + "consentText": null, + "verified": false, + "createdAt": "2026-04-21T01:17:54.008Z" + }, + { + "id": "59578095-feb3-41bd-bb74-b54cdf7ae920", + "dataSubjectId": "ea27f0b9-57b3-4f4d-874d-e35438cd9adb", + "email": "kevinbrown2354@gmail.com", + "purposes": [ + "contact" + ], + "source": "contact_form", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", + "privacyPolicyVersion": "1970-01-01", + "consentText": null, + "verified": true, + "createdAt": "2026-04-21T01:15:01.940Z" + }, + { + "id": "a38d16a9-7f93-4e56-a456-76d042836cea", + "dataSubjectId": "ea27f0b9-57b3-4f4d-874d-e35438cd9adb", + "email": "kevinbrown2354@gmail.com", + "purposes": [ + "marketing" + ], + "source": "newsletter_form", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", + "privacyPolicyVersion": "1970-01-01", + "consentText": null, + "verified": true, + "createdAt": "2026-04-14T22:47:25.830Z" + }, + { + "id": "7a8a87fa-e4bb-4eb5-98c6-7006e97c1ef4", + "dataSubjectId": "ea27f0b9-57b3-4f4d-874d-e35438cd9adb", + "email": "kevinbrown2354@gmail.com", + "purposes": [ + "contact" + ], + "source": "contact_form", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", + "privacyPolicyVersion": "1970-01-01", + "consentText": null, + "verified": true, + "createdAt": "2026-04-14T12:33:07.373Z" + }, + { + "id": "3d81f4bd-f0d6-4618-a79e-7da633cde947", + "dataSubjectId": "ea27f0b9-57b3-4f4d-874d-e35438cd9adb", + "email": "kevinbrown2354@gmail.com", + "purposes": [ + "contact" + ], + "source": "contact_form", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", + "privacyPolicyVersion": "1970-01-01", + "consentText": null, + "verified": true, + "createdAt": "2026-04-14T00:03:07.112Z" + } + ] +} \ No newline at end of file diff --git a/src/components/Pages/MyData/client/__tests__/index.spec.ts b/src/components/Pages/MyData/client/__tests__/index.spec.ts index 2e3f65a5c..df7df728f 100644 --- a/src/components/Pages/MyData/client/__tests__/index.spec.ts +++ b/src/components/Pages/MyData/client/__tests__/index.spec.ts @@ -114,6 +114,47 @@ describe('PrivacyForm behavior', () => { }) }) + it('renders preview states from query parameters for both workflows', async () => { + await executeRender({ + container, + component: PrivacyForm, + moduleSpecifier: '@components/Pages/MyData/client/index', + args: { + props: { + content: myDataContent, + }, + }, + waitForReady: async (element: PrivacyFormElementInstance) => { + window.history.replaceState( + {}, + '', + 'http://localhost/privacy/my-data?accessState=loading&deleteState=validation' + ) + element.initialize() + }, + assert: async ({ element }) => { + const elements = getPrivacyFormElements(element) + const accessSubmitButton = elements.accessForm.querySelector('button[type="submit"]') + + expect(elements.accessForm.dataset.privacyState).toBe('loading') + expect(elements.accessForm.getAttribute('aria-busy')).toBe('true') + expect(elements.accessMessage.textContent).toBe('Sending request...') + expect(elements.accessMessage.classList.contains('border-info')).toBe(true) + expect(accessSubmitButton).toBeInstanceOf(HTMLButtonElement) + expect((accessSubmitButton as HTMLButtonElement).disabled).toBe(true) + + expect(elements.deleteForm.dataset.privacyState).toBe('validation') + expect(elements.deleteMessage.textContent).toBe( + 'Please enter a valid email address and confirm the deletion request.' + ) + expect(elements.deleteMessage.classList.contains('border-danger')).toBe(true) + expect(elements.deleteEmailInput.getAttribute('aria-invalid')).toBe('true') + expect(elements.deleteConfirmCheckbox.getAttribute('aria-invalid')).toBe('true') + expect(requestDataMock).not.toHaveBeenCalled() + }, + }) + }) + it('blocks delete submit when confirmation is not checked', async () => { requestDataMock.mockResolvedValue({ data: { message: 'Delete request sent.' } }) diff --git a/src/components/Pages/MyData/client/index.ts b/src/components/Pages/MyData/client/index.ts index f6d3cd262..dc04ef384 100644 --- a/src/components/Pages/MyData/client/index.ts +++ b/src/components/Pages/MyData/client/index.ts @@ -8,6 +8,8 @@ import { getPrivacyFormElements } from './selectors' type MessageType = 'success' | 'error' | 'info' type RequestType = 'ACCESS' | 'DELETE' +type RequestPreviewState = 'loading' | 'success' | 'error' | 'validation' +type RequestFormType = 'access' | 'delete' type DsarVerifyResult = | { status: 'download'; filename: string; json: string } @@ -16,6 +18,13 @@ type DsarVerifyResult = type RequestDataResult = { message: string } +const requestPreviewStates = ['loading', 'success', 'error', 'validation'] as const + +const requestPreviewQueryParams: Record = { + access: 'accessState', + delete: 'deleteState', +} + const statusMessages: Record = { sent: { type: 'success', @@ -89,6 +98,7 @@ export class PrivacyFormElement extends LitElement { this.bindEvents() this.isInitialized = true + this.renderRequestPreviewStatesFromQueryString() this.renderStatusFromQueryString() void this.handleVerificationToken() } catch (error) { @@ -136,7 +146,66 @@ export class PrivacyFormElement extends LitElement { }) } - private setMessage(target: HTMLElement, message: string, type: MessageType): void { + private setRequestState(form: HTMLFormElement, state: RequestPreviewState | 'idle'): void { + form.dataset.privacyState = state + form.setAttribute('aria-busy', String(state === 'loading')) + } + + private setSubmitLoading(form: HTMLFormElement, loading: boolean): void { + const submitButton = form.querySelector('button[type="submit"]') + if (submitButton instanceof HTMLButtonElement) { + submitButton.disabled = loading + } + } + + private setEmailInvalid(input: HTMLInputElement, invalid: boolean): void { + input.setAttribute('aria-invalid', String(invalid)) + input.classList.toggle('border-danger', invalid) + input.classList.toggle('focus:border-danger', invalid) + } + + private setDeleteConfirmationInvalid(invalid: boolean): void { + this.deleteConfirmCheckbox.setAttribute('aria-invalid', String(invalid)) + } + + private resetMessage(target: HTMLElement): void { + target.textContent = '' + target.classList.add('hidden') + target.classList.remove( + 'border-success', + 'bg-success-inverse', + 'text-success', + 'border-danger', + 'bg-danger-offset', + 'text-danger', + 'border-info', + 'bg-info-inverse', + 'text-info' + ) + } + + private resetRequestState(requestType: RequestType): void { + const isAccessRequest = requestType === 'ACCESS' + const form = isAccessRequest ? this.accessForm : this.deleteForm + const emailInput = isAccessRequest ? this.accessEmailInput : this.deleteEmailInput + const message = isAccessRequest ? this.accessMessage : this.deleteMessage + + this.resetMessage(message) + this.setRequestState(form, 'idle') + this.setSubmitLoading(form, false) + this.setEmailInvalid(emailInput, false) + + if (!isAccessRequest) { + this.setDeleteConfirmationInvalid(false) + } + } + + private setMessage( + target: HTMLElement, + message: string, + type: MessageType, + options: { focus?: boolean } = {} + ): void { target.setAttribute('role', type === 'error' ? 'alert' : 'status') target.setAttribute('aria-live', type === 'error' ? 'assertive' : 'polite') target.textContent = message @@ -167,7 +236,100 @@ export class PrivacyFormElement extends LitElement { target.classList.add('border-info', 'bg-info-inverse', 'text-info') } - target.focus() + if (options.focus ?? true) { + target.focus() + } + } + + private resolvePreviewState( + params: URLSearchParams, + requestFormType: RequestFormType + ): RequestPreviewState | null { + const previewState = params.get(requestPreviewQueryParams[requestFormType])?.trim().toLowerCase() + + if (!previewState) { + return null + } + + return requestPreviewStates.includes(previewState as RequestPreviewState) + ? (previewState as RequestPreviewState) + : null + } + + private applyRequestPreviewState( + requestType: RequestType, + previewState: RequestPreviewState + ): void { + this.resetRequestState(requestType) + + const isAccessRequest = requestType === 'ACCESS' + const form = isAccessRequest ? this.accessForm : this.deleteForm + const emailInput = isAccessRequest ? this.accessEmailInput : this.deleteEmailInput + const message = isAccessRequest ? this.accessMessage : this.deleteMessage + + switch (previewState) { + case 'loading': + this.setRequestState(form, 'loading') + this.setSubmitLoading(form, true) + this.setMessage(message, 'Sending request...', 'info', { focus: false }) + return + + case 'success': + this.setRequestState(form, 'success') + this.setMessage( + message, + isAccessRequest + ? 'Data access request sent. Please check your inbox to verify the request.' + : 'Deletion request sent. Please check your inbox to verify the request.', + 'success', + { focus: false } + ) + return + + case 'error': + this.setRequestState(form, 'error') + this.setMessage( + message, + isAccessRequest + ? 'Unable to submit your data request. Please try again.' + : 'Unable to submit your deletion request. Please try again.', + 'error', + { focus: false } + ) + return + + case 'validation': + this.setRequestState(form, 'validation') + this.setEmailInvalid(emailInput, true) + + if (isAccessRequest) { + this.setMessage(message, 'Please enter a valid email address.', 'error', { focus: false }) + return + } + + this.setDeleteConfirmationInvalid(true) + this.setMessage( + message, + 'Please enter a valid email address and confirm the deletion request.', + 'error', + { focus: false } + ) + return + } + } + + private renderRequestPreviewStatesFromQueryString(): void { + const params = new URLSearchParams(window.location.search) + const accessPreviewState = this.resolvePreviewState(params, 'access') + const deletePreviewState = this.resolvePreviewState(params, 'delete') + + if (accessPreviewState) { + this.applyRequestPreviewState('ACCESS', accessPreviewState) + } + + if (deletePreviewState) { + this.applyRequestPreviewState('DELETE', deletePreviewState) + } } private renderStatusFromQueryString(): void { @@ -240,25 +402,37 @@ export class PrivacyFormElement extends LitElement { const formEl = requestType === 'ACCESS' ? this.accessForm : this.deleteForm const emailInput = requestType === 'ACCESS' ? this.accessEmailInput : this.deleteEmailInput + this.resetRequestState(requestType) + if (requestType === 'DELETE' && !this.deleteConfirmCheckbox.checked) { + this.setRequestState(formEl, 'validation') + this.setDeleteConfirmationInvalid(true) this.setMessage(messageEl, 'Please confirm you understand the deletion request.', 'error') return } const email = emailInput.value + this.setRequestState(formEl, 'loading') + this.setSubmitLoading(formEl, true) this.setMessage(messageEl, 'Sending request...', 'info') try { const { data, error } = await actions.gdpr.requestData({ email, requestType }) if (error || !data) { + this.setRequestState(formEl, 'error') + this.setSubmitLoading(formEl, false) this.setMessage(messageEl, error?.message || 'Request failed', 'error') return } const resultData = data as RequestDataResult + this.setRequestState(formEl, 'success') + this.setSubmitLoading(formEl, false) this.setMessage(messageEl, resultData.message, 'success') formEl.reset() } catch (error) { + this.setRequestState(formEl, 'error') + this.setSubmitLoading(formEl, false) this.setMessage( messageEl, error instanceof Error ? error.message : 'Network or server error', diff --git a/src/components/Pages/MyData/index.astro b/src/components/Pages/MyData/index.astro index c2bbe6de3..cc40eb0f3 100644 --- a/src/components/Pages/MyData/index.astro +++ b/src/components/Pages/MyData/index.astro @@ -3,6 +3,19 @@ import Button from '@components/Button/index.astro' import Icon from '@components/Icon/index.astro' import List from '@components/List/index.astro' +/** + * Privacy request preview states for styling: + * + * - /privacy/my-data?accessState=loading + * - /privacy/my-data?accessState=success + * - /privacy/my-data?accessState=error + * - /privacy/my-data?accessState=validation + * - /privacy/my-data?deleteState=loading + * - /privacy/my-data?deleteState=success + * - /privacy/my-data?deleteState=error + * - /privacy/my-data?deleteState=validation + */ + export type Props = { content: { header: { @@ -87,7 +100,14 @@ const { content } = Astro.props {content.accessData.description}

-
+
- +
+ + +
+ + + + + ) +} diff --git a/src/components/Inset/__tests__/index.spec.ts b/src/components/Inset/__tests__/index.spec.ts index 4cce1867e..fbbd84d2a 100644 --- a/src/components/Inset/__tests__/index.spec.ts +++ b/src/components/Inset/__tests__/index.spec.ts @@ -90,4 +90,4 @@ describe('Inset (Astro)', () => { ) }) }) -}) \ No newline at end of file +}) diff --git a/src/components/Inset/index.astro b/src/components/Inset/index.astro index daac5a6dd..d8ff5ac54 100644 --- a/src/components/Inset/index.astro +++ b/src/components/Inset/index.astro @@ -26,12 +26,12 @@ const defaultInsetProps = { ---
- {variant === 'default' && ( - - - - )} - {figure && ( -
- )} + { + variant === 'default' && ( + + + + ) + } + {figure &&
}
diff --git a/src/components/Layout/Copyright/index.astro b/src/components/Layout/Copyright/index.astro index add75b367..37cc2a2d0 100644 --- a/src/components/Layout/Copyright/index.astro +++ b/src/components/Layout/Copyright/index.astro @@ -12,6 +12,5 @@ const { publishDate, variant = 'default', link } = Astro.props const publishYear = publishDate.getFullYear() --- - {variant === 'default' && } {variant === 'print' && } diff --git a/src/components/Layout/Copyright/layouts/default.astro b/src/components/Layout/Copyright/layouts/default.astro index ff51caf94..af5c13351 100644 --- a/src/components/Layout/Copyright/layouts/default.astro +++ b/src/components/Layout/Copyright/layouts/default.astro @@ -11,12 +11,7 @@ const { publishYear } = Astro.props
Copyright - + diff --git a/src/components/Layout/Copyright/layouts/print.astro b/src/components/Layout/Copyright/layouts/print.astro index f2b7f9bd5..9c6f860a0 100644 --- a/src/components/Layout/Copyright/layouts/print.astro +++ b/src/components/Layout/Copyright/layouts/print.astro @@ -9,19 +9,16 @@ export interface Props { const { link, publishYear } = Astro.props const companyName = 'Webstack Builders, Inc.' -const creativeCommonsLicense = 'The text, diagrams, and images in this work are licensed under CC BY-NC 4.0' -const mitLicense = 'All code samples in this article are licensed under the MIT License. Feel free to use, modify, and distribute them in any project.' +const creativeCommonsLicense = + 'The text, diagrams, and images in this work are licensed under CC BY-NC 4.0' +const mitLicense = + 'All code samples in this article are licensed under the MIT License. Feel free to use, modify, and distribute them in any project.' ---
Copyright - + diff --git a/src/components/Layout/Markdown/Lead/__tests__/index.spec.ts b/src/components/Layout/Markdown/Lead/__tests__/index.spec.ts index 815ff60c4..bafe03f53 100644 --- a/src/components/Layout/Markdown/Lead/__tests__/index.spec.ts +++ b/src/components/Layout/Markdown/Lead/__tests__/index.spec.ts @@ -7,7 +7,7 @@ import Lead from '../index.astro' const fixtureModules = import.meta.glob( '../../../../Avatar/server/__fixtures__/avatars/*.{jpg,jpeg,png,webp}', - { eager: true, import: 'default' }, + { eager: true, import: 'default' } ) vi.mock('@components/Avatar/server/avatarImports', () => ({ @@ -36,7 +36,9 @@ describe('Lead', () => { const document = dom.window.document const normalizeWhitespace = (value: string) => value.replace(/\s+/g, ' ').trim() - const primaryLineText = normalizeWhitespace(document.querySelector('.text-primary')?.textContent ?? '') + const primaryLineText = normalizeWhitespace( + document.querySelector('.text-primary')?.textContent ?? '' + ) expect(html).toContain('href="/about"') expect(html).toContain('Kevin Brown') diff --git a/src/components/Layout/Markdown/Lead/index.astro b/src/components/Layout/Markdown/Lead/index.astro index 8ce74176b..bdbb722c0 100644 --- a/src/components/Layout/Markdown/Lead/index.astro +++ b/src/components/Layout/Markdown/Lead/index.astro @@ -21,8 +21,11 @@ const formatShortDate = (date: Date) => }) const publishDateText = publishDate ? formatShortDate(publishDate) : null -const shouldShowModifiedDate = Boolean(modifiedDate && publishDate && modifiedDate.getTime() !== publishDate.getTime()) -const modifiedDateText = shouldShowModifiedDate && modifiedDate ? formatShortDate(modifiedDate) : null +const shouldShowModifiedDate = Boolean( + modifiedDate && publishDate && modifiedDate.getTime() !== publishDate.getTime() +) +const modifiedDateText = + shouldShowModifiedDate && modifiedDate ? formatShortDate(modifiedDate) : null ---
@@ -32,35 +35,47 @@ const modifiedDateText = shouldShowModifiedDate && modifiedDate ? formatShortDat
- {modifiedDateText && ( - - )} + { + modifiedDateText && ( + + ) + } {readingTime &&
{readingTime}
}
- {tags && tags.length > 0 && ( - - )} + { + tags && tags.length > 0 && ( + + ) + }
diff --git a/src/components/Layout/Markdown/Tags/index.astro b/src/components/Layout/Markdown/Tags/index.astro index 3407f3e73..3573dc7d9 100644 --- a/src/components/Layout/Markdown/Tags/index.astro +++ b/src/components/Layout/Markdown/Tags/index.astro @@ -61,9 +61,9 @@ const resolveTagMeta = (rawTag: string) => { data-tooltip title={`See All ${label} Articles`} class:list={[ - "inline-block rounded-full transition-colors px-3 py-1 ml-2", - "uppercase text-content-inverse text-xs no-underline", - "bg-warning-offset hover:bg-warning", + 'inline-block rounded-full transition-colors px-3 py-1 ml-2', + 'uppercase text-content-inverse text-xs no-underline', + 'bg-warning-offset hover:bg-warning', "relative focus-visible:outline-none after:pointer-events-none after:absolute after:content-[''] after:inset-0 after:rounded-none after:border-2 after:border-transparent focus-visible:after:-inset-1 focus-visible:after:border-spotlight", ]} > diff --git a/src/components/Layout/Print/Cover/index.astro b/src/components/Layout/Print/Cover/index.astro index 1d859c06b..6afa5fe25 100644 --- a/src/components/Layout/Print/Cover/index.astro +++ b/src/components/Layout/Print/Cover/index.astro @@ -19,27 +19,23 @@ const publishDateLabel = article.data.publishDate.toLocaleDateString(undefined, }) --- -
-
- +
+
+
-

+

{article.data.title}

- {article.data.coverAlt} + {article.data.coverAlt}
@@ -57,62 +53,62 @@ const publishDateLabel = article.data.publishDate.toLocaleDateString(undefined,
diff --git a/src/components/Layout/Print/Toc/index.astro b/src/components/Layout/Print/Toc/index.astro index fe307ecbc..884eb114f 100644 --- a/src/components/Layout/Print/Toc/index.astro +++ b/src/components/Layout/Print/Toc/index.astro @@ -17,28 +17,30 @@ const { headings } = Astro.props const tocTree = buildTocTree(headings) --- -{tocTree.length > 0 && ( - + ) +} diff --git a/src/components/Layout/SubHeader/index.astro b/src/components/Layout/SubHeader/index.astro index bc9b7befd..ce26b7ce6 100644 --- a/src/components/Layout/SubHeader/index.astro +++ b/src/components/Layout/SubHeader/index.astro @@ -12,13 +12,13 @@ const { title, description, icon, subtitle } = Astro.props
- +

{title}

- {description && (

)} - {subtitle && (

)} + { + description && ( +

+ ) + } + {subtitle &&

}

diff --git a/src/components/List/ListItem.astro b/src/components/List/ListItem.astro index d47944b92..7478d10af 100644 --- a/src/components/List/ListItem.astro +++ b/src/components/List/ListItem.astro @@ -6,4 +6,4 @@ export type Props = { const { lead } = Astro.props as Props --- - \ No newline at end of file + diff --git a/src/components/List/__fixtures__/mixedApi.fixture.astro b/src/components/List/__fixtures__/mixedApi.fixture.astro index 5824f4e0c..b67d3c3e5 100644 --- a/src/components/List/__fixtures__/mixedApi.fixture.astro +++ b/src/components/List/__fixtures__/mixedApi.fixture.astro @@ -13,4 +13,4 @@ import ListItem from '@components/List/ListItem.astro' ]} > This should never render. - \ No newline at end of file + diff --git a/src/components/List/__fixtures__/richItems.fixture.astro b/src/components/List/__fixtures__/richItems.fixture.astro index 0c5d84c07..254ba8b69 100644 --- a/src/components/List/__fixtures__/richItems.fixture.astro +++ b/src/components/List/__fixtures__/richItems.fixture.astro @@ -7,7 +7,5 @@ import ListItem from '@components/List/ListItem.astro' Separate VPCs1 can provide stronger boundaries. - - Dedicated node pools reduce resource contention. - - \ No newline at end of file + Dedicated node pools reduce resource contention. + diff --git a/src/components/List/__tests__/index.spec.ts b/src/components/List/__tests__/index.spec.ts index e9190d19d..d9bbae4aa 100644 --- a/src/components/List/__tests__/index.spec.ts +++ b/src/components/List/__tests__/index.spec.ts @@ -36,8 +36,8 @@ describe('List (Astro)', () => { const definitionList = window.document.querySelector('dl') expect(definitionList).toBeTruthy() - expect(definitionList?.className).toContain('max-w-2xl') - expect(definitionList?.className).toContain('mx-auto') + expect(definitionList?.className).toContain('max-w-2xl') + expect(definitionList?.className).toContain('mx-auto') const questions = window.document.querySelectorAll('dt') const answers = window.document.querySelectorAll('dd') @@ -227,7 +227,9 @@ describe('List (Astro)', () => { expect(list?.className).toContain('text-content-offset') expect(item?.className).toContain('flex') expect(svg).toBeTruthy() - expect(item?.textContent).toContain('Built self-service infrastructure provisioning workflows.') + expect(item?.textContent).toContain( + 'Built self-service infrastructure provisioning workflows.' + ) }) }) @@ -284,4 +286,4 @@ describe('List (Astro)', () => { 'List: received both the `items` prop and ListItem children. Use one API or the other.' ) }) -}) \ No newline at end of file +}) diff --git a/src/components/List/index.astro b/src/components/List/index.astro index f5a505c37..ce658a246 100644 --- a/src/components/List/index.astro +++ b/src/components/List/index.astro @@ -48,7 +48,15 @@ export type Props = { style?: Record } -const { items, size, color, startNumber, variant = 'default', classes, style } = Astro.props as Props +const { + items, + size, + color, + startNumber, + variant = 'default', + classes, + style, +} = Astro.props as Props const hasItemsProp = Object.prototype.hasOwnProperty.call(Astro.props, 'items') const hasDefaultSlot = Astro.slots.has('default') @@ -90,26 +98,119 @@ const itemsWithIconAndColor = normalizedItems as Array<{ inverseColor?: string bgColor?: string }> -const plainIconItems = normalizedItems.filter((item): item is NonNullable[number] & { icon: string } => { - return typeof item.icon === 'string' && item.icon.trim().length > 0 -}) +const plainIconItems = normalizedItems.filter( + (item): item is NonNullable[number] & { icon: string } => { + return typeof item.icon === 'string' && item.icon.trim().length > 0 + } +) ---
- {variant === 'accent-border-left-list' && } - {variant === 'badge-list' && } - {variant === 'card-grid-list' && } - {variant === 'chat-bubbles' && } - {variant === 'check-icons-list' && } - {variant === 'chevron-list' && } - {variant === 'colored-marker-list' && } + { + variant === 'accent-border-left-list' && ( + + ) + } + { + variant === 'badge-list' && ( + + ) + } + { + variant === 'card-grid-list' && ( + + ) + } + { + variant === 'chat-bubbles' && ( + + ) + } + { + variant === 'check-icons-list' && ( + + ) + } + { + variant === 'chevron-list' && ( + + ) + } + { + variant === 'colored-marker-list' && ( + + ) + } {variant === 'experience-list' && } - {variant === 'numbered-with-background-list' && } - {variant === 'plain-icon-list' && } - {variant === 'side-by-side-list' && } - {variant === 'timeline-list' && } - {variant === 'two-column-check-icons-list' && } - {variant === 'two-column-icon-list' && } - {variant === 'three-column-icon-list' && } - {variant === 'zebra-list' && } + { + variant === 'numbered-with-background-list' && ( + + ) + } + { + variant === 'plain-icon-list' && ( + + ) + } + { + variant === 'side-by-side-list' && ( + + ) + } + { + variant === 'timeline-list' && ( + + ) + } + { + variant === 'two-column-check-icons-list' && ( + + ) + } + { + variant === 'two-column-icon-list' && ( + + ) + } + { + variant === 'three-column-icon-list' && ( + + ) + } + { + variant === 'zebra-list' && ( + + ) + }
diff --git a/src/components/List/layouts/AccentBorderLeftList.astro b/src/components/List/layouts/AccentBorderLeftList.astro index 57a706377..979edb8b3 100644 --- a/src/components/List/layouts/AccentBorderLeftList.astro +++ b/src/components/List/layouts/AccentBorderLeftList.astro @@ -14,25 +14,25 @@ export type Props = { const { items, classes }: Props = Astro.props -const ulClass = "space-y-4 list-none pl-0" -const liClass = "border-l-4 pl-4" -const headerClass = "text-page-inverse font-semibold mb-2" -const textClass = "text-content-offset text-sm" +const ulClass = 'space-y-4 list-none pl-0' +const liClass = 'border-l-4 pl-4' +const headerClass = 'text-page-inverse font-semibold mb-2' +const textClass = 'text-content-offset text-sm' ---
    { - items.map((item) => ( + items.map(item => (
  • - {item.lead &&

    } + {item.lead &&

    } {Array.isArray(item.text) ? (
    - {item.text.map((paragraph) => ( -

    + {item.text.map(paragraph => ( +

    ))}

    ) : ( -

    +

    )}

  • )) diff --git a/src/components/List/layouts/BadgeList.astro b/src/components/List/layouts/BadgeList.astro index 08fa2c950..1625c6222 100644 --- a/src/components/List/layouts/BadgeList.astro +++ b/src/components/List/layouts/BadgeList.astro @@ -17,32 +17,34 @@ export type Props = { const { items, classes }: Props = Astro.props -const ulClass = ["space-y-4 sm:space-y-0 sm:table sm:border-separate sm:border-spacing-x-4 sm:border-spacing-y-4", classes?.ul] -const liClass = ["flex flex-col gap-2 sm:table-row", classes?.li] -const titleCellClass = "sm:table-cell sm:align-top" +const ulClass = [ + 'space-y-4 sm:space-y-0 sm:table sm:border-separate sm:border-spacing-x-4 sm:border-spacing-y-4', + classes?.ul, +] +const liClass = ['flex flex-col gap-2 sm:table-row', classes?.li] +const titleCellClass = 'sm:table-cell sm:align-top' const titleClass = [ - "inline-block px-2 py-1 rounded bg-content text-page-base font-mono text-xs font-bold sm:mt-1", + 'inline-block px-2 py-1 rounded bg-content text-page-base font-mono text-xs font-bold sm:mt-1', classes?.titleClass, ] -const bodyClass = ["sm:table-cell sm:align-top", classes?.content] -const emClass = ["text-content font-bold not-italic mr-2", classes?.em] +const bodyClass = ['sm:table-cell sm:align-top', classes?.content] +const emClass = ['text-content font-bold not-italic mr-2', classes?.em] ---
      { - items.map((item) => ( + items.map(item => (
    • {item.title && ( - + )} - {item.lead && } - + {item.lead && } +
    • )) }
    - diff --git a/src/components/List/layouts/CardGridList.astro b/src/components/List/layouts/CardGridList.astro index 5e27765cf..feb30571a 100644 --- a/src/components/List/layouts/CardGridList.astro +++ b/src/components/List/layouts/CardGridList.astro @@ -16,25 +16,25 @@ export type Props = { const { items, classes }: Props = Astro.props -const ulClass = ["grid grid-cols-1 md:grid-cols-2 gap-4", classes?.ul] +const ulClass = ['grid grid-cols-1 md:grid-cols-2 gap-4', classes?.ul] const liClass = [ - "bg-page-offset border border-trim rounded-lg p-5 transition-all duration-200 ease-out hover:-translate-y-1 hover:border-primary hover:bg-page-base hover:shadow-md focus-within:-translate-y-1 focus-within:border-primary focus-within:bg-page-base focus-within:shadow-md", + 'bg-page-offset border border-trim rounded-lg p-5 transition-all duration-200 ease-out hover:-translate-y-1 hover:border-primary hover:bg-page-base hover:shadow-md focus-within:-translate-y-1 focus-within:border-primary focus-within:bg-page-base focus-within:shadow-md', classes?.li, ] -const emClass = ["block text-page-inverse font-semibold not-italic mb-2", classes?.em] +const emClass = ['block text-page-inverse font-semibold not-italic mb-2', classes?.em] ---
      { - items.map((item) => ( + items.map(item => (
    • {item.icon && ( )} - {item.lead && } + {item.lead && }
      - +
    • )) } diff --git a/src/components/List/layouts/ChatBubbles.astro b/src/components/List/layouts/ChatBubbles.astro index 15cbae1a1..88006249b 100644 --- a/src/components/List/layouts/ChatBubbles.astro +++ b/src/components/List/layouts/ChatBubbles.astro @@ -37,19 +37,23 @@ const ddClass = [
      { - items.map((item) => ( + items.map(item => (
      {item.lead && (
      - - + +
      )}
      - - + +
      )) } -
      \ No newline at end of file + diff --git a/src/components/List/layouts/CheckIconsList.astro b/src/components/List/layouts/CheckIconsList.astro index c8268da55..e3482cc63 100644 --- a/src/components/List/layouts/CheckIconsList.astro +++ b/src/components/List/layouts/CheckIconsList.astro @@ -1,5 +1,5 @@ --- -import Icon from "@components/Icon/index.astro" +import Icon from '@components/Icon/index.astro' export type Props = { items: { @@ -17,30 +17,25 @@ export type Props = { const { items, classes }: Props = Astro.props -const ulClass = ["space-y-3", classes?.ul] -const liClass = ["flex items-start gap-3", classes?.li] -const emClass = ["text-content font-semibold not-italic", classes?.em] -const textClass = ["text-content-offset", classes?.text] +const ulClass = ['space-y-3', classes?.ul] +const liClass = ['flex items-start gap-3', classes?.li] +const emClass = ['text-content font-semibold not-italic', classes?.em] +const textClass = ['text-content-offset', classes?.text] ---
        { - items.map((item) => ( + items.map(item => (
      • - + {item.lead && ( <> - + )} - +
      • )) diff --git a/src/components/List/layouts/ChevronList.astro b/src/components/List/layouts/ChevronList.astro index 0c62918ea..f1148a640 100644 --- a/src/components/List/layouts/ChevronList.astro +++ b/src/components/List/layouts/ChevronList.astro @@ -1,5 +1,5 @@ --- -import Icon from "@components/Icon/index.astro" +import Icon from '@components/Icon/index.astro' export type Props = { items: { @@ -16,28 +16,28 @@ export type Props = { const { items, classes }: Props = Astro.props -const ulClass = ["space-y-3", classes?.ul] -const liClass = ["group", classes?.li] -const emClass = ["text-content font-semibold not-italic", classes?.em] -const textClass = "block text-content-offset mt-0.5" +const ulClass = ['space-y-3', classes?.ul] +const liClass = ['group', classes?.li] +const emClass = ['text-content font-semibold not-italic', classes?.em] +const textClass = 'block text-content-offset mt-0.5' ---
          { - items.map((item) => ( + items.map(item => (
        • - {item.lead && } - + {item.lead && } +
        • diff --git a/src/components/List/layouts/ColoredMarkerList.astro b/src/components/List/layouts/ColoredMarkerList.astro index bcea5ede6..838f8a711 100644 --- a/src/components/List/layouts/ColoredMarkerList.astro +++ b/src/components/List/layouts/ColoredMarkerList.astro @@ -17,23 +17,23 @@ export type Props = { const { items, classes, size = 2 }: Props = Astro.props -const ulClass = ["list-none pl-0 text-content mt-4 mb-2 space-y-2", classes?.ul] -const liClass = ["flex items-start", classes?.li] -const emClass = ["not-italic font-bold", classes?.em] +const ulClass = ['list-none pl-0 text-content mt-4 mb-2 space-y-2', classes?.ul] +const liClass = ['flex items-start', classes?.li] +const emClass = ['not-italic font-bold', classes?.em] const markerSizeInRem = `${size * 0.25}rem` const markerStyle = `width: ${markerSizeInRem}; height: ${markerSizeInRem};` -const iconClass = ["rounded-full mt-2 mr-3 shrink-0", classes?.icon] +const iconClass = ['rounded-full mt-2 mr-3 shrink-0', classes?.icon] const textClass = classes?.text ? [classes.text] : [] ---
            { - items.map((item) => ( + items.map(item => (
          • - {item.lead && } - + {item.lead && } +
          • )) diff --git a/src/components/List/layouts/ExperienceList.astro b/src/components/List/layouts/ExperienceList.astro index c0264a938..0befb381c 100644 --- a/src/components/List/layouts/ExperienceList.astro +++ b/src/components/List/layouts/ExperienceList.astro @@ -1,4 +1,3 @@ - --- import Icon from '@components/Icon/index.astro' @@ -27,14 +26,17 @@ const iconClass = ['mt-0.5 shrink-0', classes?.icon]
              { - items.map((item) => ( + items.map(item => (
            • - {item.lead && } - + {item.lead && } +
            • )) diff --git a/src/components/List/layouts/NumberedWithBackgroundList.astro b/src/components/List/layouts/NumberedWithBackgroundList.astro index 62dc9d228..b40274c5f 100644 --- a/src/components/List/layouts/NumberedWithBackgroundList.astro +++ b/src/components/List/layouts/NumberedWithBackgroundList.astro @@ -14,14 +14,16 @@ export type Props = { } const { items, classes, color, startNumber = 1 }: Props = Astro.props -const resolvedColor = typeof color === 'string' && color.trim().length > 0 ? color.trim() : undefined -const safeColorToken = resolvedColor && /^[-a-z0-9]+$/i.test(resolvedColor) ? resolvedColor : undefined +const resolvedColor = + typeof color === 'string' && color.trim().length > 0 ? color.trim() : undefined +const safeColorToken = + resolvedColor && /^[-a-z0-9]+$/i.test(resolvedColor) ? resolvedColor : undefined const pillStyle = safeColorToken ? `background-color: var(--color-${safeColorToken});` : undefined const leadStyle = safeColorToken ? `color: var(--color-${safeColorToken});` : undefined -const olClass = ["space-y-3", classes?.ol] -const liClass = ["flex items-start gap-4 bg-page-offset/50 rounded-lg p-4", classes?.li] -const emClass = ["font-semibold not-italic", !safeColorToken && 'text-primary-offset', classes?.em] +const olClass = ['space-y-3', classes?.ol] +const liClass = ['flex items-start gap-4 bg-page-offset/50 rounded-lg p-4', classes?.li] +const emClass = ['font-semibold not-italic', !safeColorToken && 'text-primary-offset', classes?.em] ---
                @@ -30,7 +32,7 @@ const emClass = ["font-semibold not-italic", !safeColorToken && 'text-primary-of
              1. - {item.lead && } -
                + {item.lead && } +
              2. )) diff --git a/src/components/List/layouts/PlainIconList.astro b/src/components/List/layouts/PlainIconList.astro index c63456dcb..122748f60 100644 --- a/src/components/List/layouts/PlainIconList.astro +++ b/src/components/List/layouts/PlainIconList.astro @@ -18,8 +18,8 @@ export type Props = { type PlainIconListItem = Props['items'][number] const props: Props = Astro.props -const ulClass = ["list-none pl-0 space-y-3 mb-0 mt-2", props.classes?.ul] -const liClass = ["flex items-start gap-3", props.classes?.li] +const ulClass = ['list-none pl-0 space-y-3 mb-0 mt-2', props.classes?.ul] +const liClass = ['flex items-start gap-3', props.classes?.li] const markerClasses = props.classes?.svg --- @@ -29,10 +29,15 @@ const markerClasses = props.classes?.svg const resolvedColor = color?.trim() ? color : 'currentColor' return ( -
              3. - - -
              4. +
              5. + + +
              6. ) }) } diff --git a/src/components/List/layouts/ThreeColumnIconList.astro b/src/components/List/layouts/ThreeColumnIconList.astro index a7543d4ee..a07c9f50f 100644 --- a/src/components/List/layouts/ThreeColumnIconList.astro +++ b/src/components/List/layouts/ThreeColumnIconList.astro @@ -1,5 +1,5 @@ --- -import Icon from "@components/Icon/index.astro" +import Icon from '@components/Icon/index.astro' export type Props = { items: { @@ -24,31 +24,42 @@ export type Props = { const { items, classes, size }: Props = Astro.props -const ulClass = ["grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6", classes?.ul].filter(Boolean).join(" ") -const liClass = ["flex items-start gap-2 bg-white rounded-xl p-6 shadow-md transition-all duration-200 ease-out hover:-translate-y-1 hover:shadow-lg", classes?.li].filter(Boolean).join(" ") -const iconWrapper = ["shrink-0 rounded-lg flex items-center justify-center mt-1 p-2", classes?.icon].filter(Boolean).join(" ") -const contentWrapper = ["text-page-inverse mb-1 ml-2", classes?.content].filter(Boolean).join(" ") -const headerClass = ["font-sans text-lg font-semibold text-gray-900 mt-0 mb-2", classes?.header].filter(Boolean).join(" ") -const textClass = ["text-sm text-note-offset", classes?.text].filter(Boolean).join(" ") +const ulClass = ['grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6', classes?.ul] + .filter(Boolean) + .join(' ') +const liClass = [ + 'flex items-start gap-2 bg-white rounded-xl p-6 shadow-md transition-all duration-200 ease-out hover:-translate-y-1 hover:shadow-lg', + classes?.li, +] + .filter(Boolean) + .join(' ') +const iconWrapper = ['shrink-0 rounded-lg flex items-center justify-center mt-1 p-2', classes?.icon] + .filter(Boolean) + .join(' ') +const contentWrapper = ['text-page-inverse mb-1 ml-2', classes?.content].filter(Boolean).join(' ') +const headerClass = ['font-sans text-lg font-semibold text-gray-900 mt-0 mb-2', classes?.header] + .filter(Boolean) + .join(' ') +const textClass = ['text-sm text-note-offset', classes?.text].filter(Boolean).join(' ') ---
                  { - items.map((item) => { - const icon = item.icon ?? "check" + items.map(item => { + const icon = item.icon ?? 'check' return (
                • -
                  - +
                  +
                  - {item.title &&

                  } -

                  + {item.title &&

                  } +

                • ) diff --git a/src/components/List/layouts/TimelineList.astro b/src/components/List/layouts/TimelineList.astro index be9de5e76..a044e0807 100644 --- a/src/components/List/layouts/TimelineList.astro +++ b/src/components/List/layouts/TimelineList.astro @@ -21,25 +21,30 @@ const timelineItems = items.map((item: TimelineItem, index: number) => ({ showConnector: index < items.length - 1, })) -const ulClass = ["space-y-0", classes?.ul] -const liClass = ["relative grid grid-cols-[1.5rem_1fr] gap-4", classes?.li] -const emClass = ["block text-content font-bold not-italic mb-1", classes?.em] -const wrapperClass = "mb-4" -const iconClass = ["relative h-5 w-5 mt-0.5 rounded-full border-2 border-primary bg-page-base z-10", classes?.icon] -const connectorClass = ["absolute left-1/2 top-[1.375rem] -bottom-1 -translate-x-1/2 w-0.5 bg-content-offset"] +const ulClass = ['space-y-0', classes?.ul] +const liClass = ['relative grid grid-cols-[1.5rem_1fr] gap-4', classes?.li] +const emClass = ['block text-content font-bold not-italic mb-1', classes?.em] +const wrapperClass = 'mb-4' +const iconClass = [ + 'relative h-5 w-5 mt-0.5 rounded-full border-2 border-primary bg-page-base z-10', + classes?.icon, +] +const connectorClass = [ + 'absolute left-1/2 top-[1.375rem] -bottom-1 -translate-x-1/2 w-0.5 bg-content-offset', +] ---
                    { - timelineItems.map((item) => ( + timelineItems.map(item => (
                  • - {item.showConnector && } - + {item.showConnector &&
                    -
                    - {item.lead && } - +
                    + {item.lead && } +
                  • )) diff --git a/src/components/List/layouts/TwoColumnCheckIconsList.astro b/src/components/List/layouts/TwoColumnCheckIconsList.astro index 6984856a4..1984dd73b 100644 --- a/src/components/List/layouts/TwoColumnCheckIconsList.astro +++ b/src/components/List/layouts/TwoColumnCheckIconsList.astro @@ -1,5 +1,5 @@ --- -import Icon from "@components/Icon/index.astro" +import Icon from '@components/Icon/index.astro' export type Props = { items: { @@ -16,21 +16,17 @@ export type Props = { const { items, classes, size }: Props = Astro.props -const ulClass = ["grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm text-content mb-1", classes?.ul] -const liClass = ["flex items-center gap-2", classes?.li] +const ulClass = ['grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm text-content mb-1', classes?.ul] +const liClass = ['flex items-center gap-2', classes?.li] ---
                      { - items.map((item) => ( + items.map(item => (
                    • - + - +
                    • )) diff --git a/src/components/List/layouts/TwoColumnIconList.astro b/src/components/List/layouts/TwoColumnIconList.astro index 34ee6812c..fcdceaf9c 100644 --- a/src/components/List/layouts/TwoColumnIconList.astro +++ b/src/components/List/layouts/TwoColumnIconList.astro @@ -1,5 +1,5 @@ --- -import Icon from "@components/Icon/index.astro" +import Icon from '@components/Icon/index.astro' export type Props = { items: { @@ -23,31 +23,35 @@ export type Props = { const { items, classes, size }: Props = Astro.props -const ulClass = ["grid grid-cols-1 md:grid-cols-2 gap-6", classes?.ul].filter(Boolean).join(" ") -const liClass = ["flex items-start gap-2", classes?.li].filter(Boolean).join(" ") -const iconWrapper = ["shrink-0 rounded-md flex items-center justify-center p-2 mt-1", classes?.icon].filter(Boolean).join(" ") -const contentWrapper = ["text-page-inverse mb-1 ml-2", classes?.content].filter(Boolean).join(" ") -const headerClass = ["font-sans text-xl text-content mt-1 mb-1", classes?.header].filter(Boolean).join(" ") -const textClass = ["text-sm text-note-offset", classes?.text].filter(Boolean).join(" ") +const ulClass = ['grid grid-cols-1 md:grid-cols-2 gap-6', classes?.ul].filter(Boolean).join(' ') +const liClass = ['flex items-start gap-2', classes?.li].filter(Boolean).join(' ') +const iconWrapper = ['shrink-0 rounded-md flex items-center justify-center p-2 mt-1', classes?.icon] + .filter(Boolean) + .join(' ') +const contentWrapper = ['text-page-inverse mb-1 ml-2', classes?.content].filter(Boolean).join(' ') +const headerClass = ['font-sans text-xl text-content mt-1 mb-1', classes?.header] + .filter(Boolean) + .join(' ') +const textClass = ['text-sm text-note-offset', classes?.text].filter(Boolean).join(' ') ---
                        { - items.map((item) => { - const icon = item.icon ?? "check" + items.map(item => { + const icon = item.icon ?? 'check' return (
                      • -
                        - +
                        +
                        - {item.title &&

                        } -

                        + {item.title &&

                        } +

                      • ) diff --git a/src/components/List/layouts/ZebraList.astro b/src/components/List/layouts/ZebraList.astro index f8ddb0e45..81b131588 100644 --- a/src/components/List/layouts/ZebraList.astro +++ b/src/components/List/layouts/ZebraList.astro @@ -1,5 +1,5 @@ --- -import Icon from "@components/Icon/index.astro" +import Icon from '@components/Icon/index.astro' export type Props = { items: { @@ -19,27 +19,33 @@ export type Props = { const { items, classes, style }: Props = Astro.props -const ulClass = ["print-zebra-list border border-trim rounded-lg overflow-hidden divide-y divide-trim", classes?.ul] -const liClass = ["print-zebra-list-item p-4 bg-page-base flex items-center justify-between group hover:bg-page-offset transition-colors", classes?.li] -const emClass = ["text-page-inverse font-bold not-italic block mb-0.5", classes?.em] -const textClass = ["text-content"] +const ulClass = [ + 'print-zebra-list border border-trim rounded-lg overflow-hidden divide-y divide-trim', + classes?.ul, +] +const liClass = [ + 'print-zebra-list-item p-4 bg-page-base flex items-center justify-between group hover:bg-page-offset transition-colors', + classes?.li, +] +const emClass = ['text-page-inverse font-bold not-italic block mb-0.5', classes?.em] +const textClass = ['text-content'] ---
                          { - items.map((item) => ( + items.map(item => (
                        • - {item.lead && } - + {item.lead && } +
                          {!style?.hideIcon && (
                          )} diff --git a/src/components/List/server/selectors.ts b/src/components/List/server/selectors.ts index e0935de95..54d3ba71d 100644 --- a/src/components/List/server/selectors.ts +++ b/src/components/List/server/selectors.ts @@ -2,4 +2,4 @@ import type { HTMLElement as NHPElement } from 'node-html-parser' export const queryListItemElements = (context: NHPElement): NHPElement[] => { return context.querySelectorAll('wsb-list-item') -} \ No newline at end of file +} diff --git a/src/components/List/server/slotItems.ts b/src/components/List/server/slotItems.ts index fe3d4d856..e9283f3ae 100644 --- a/src/components/List/server/slotItems.ts +++ b/src/components/List/server/slotItems.ts @@ -46,7 +46,7 @@ export function getListItemsFromSlotMarkup(markup: string, variant: string): Lis ) } - return listItemElements.map((element) => { + return listItemElements.map(element => { const lead = element.getAttribute('data-lead') return { @@ -54,4 +54,4 @@ export function getListItemsFromSlotMarkup(markup: string, variant: string): Lis text: element.innerHTML.trim(), } }) -} \ No newline at end of file +} diff --git a/src/components/Map/client/index.ts b/src/components/Map/client/index.ts index 95152df9d..edd9d8095 100644 --- a/src/components/Map/client/index.ts +++ b/src/components/Map/client/index.ts @@ -1,7 +1,11 @@ import { APILoader } from '@googlemaps/extended-component-library/api_loader.js' import { addScriptBreadcrumb } from '@components/scripts/errors' import { handleScriptError } from '@components/scripts/errors/handler' -import { getGoogleMapId, getGoogleMapsApiKey, isE2eTest } from '@components/scripts/utils/environmentClient' +import { + getGoogleMapId, + getGoogleMapsApiKey, + isE2eTest, +} from '@components/scripts/utils/environmentClient' import { getCompanyMapAddress, queryCompanyMapElement, @@ -20,9 +24,7 @@ async function geocodeAddress(address: string): Promise<{ lat: number; lng: numb const Geocoder = ( geocodingLibrary as unknown as { Geocoder: new () => { - geocode: (_request: { - address: string - }) => Promise<{ + geocode: (_request: { address: string }) => Promise<{ results?: Array<{ geometry?: { location?: { lat: () => number; lng: () => number } } }> }> } diff --git a/src/components/Navigation/index.astro b/src/components/Navigation/index.astro index 5110b1683..0e9b15f8d 100644 --- a/src/components/Navigation/index.astro +++ b/src/components/Navigation/index.astro @@ -13,10 +13,7 @@ const { path } = Astro.props
                          {/* Main Navigation Menu */} - + {/* Mobile navigation toggle button to show menu on full-page splash screen */} diff --git a/src/components/Pages/About/index.astro b/src/components/Pages/About/index.astro index 2fc49787e..39813cb88 100644 --- a/src/components/Pages/About/index.astro +++ b/src/components/Pages/About/index.astro @@ -63,12 +63,11 @@ const resumeLink = `${getSiteUrl()}/resume` ---
                          - {/* ── Journey ── */}

                          {content.journey.header} - +

                          @@ -76,36 +75,39 @@ const resumeLink = `${getSiteUrl()}/resume`
                          - {content.journey.itemlist.map((item, index) => ( -
                          - {/* Timeline node */} -
                          - -
                          + { + content.journey.itemlist.map((item, index) => ( +
                          + {/* Timeline node */} +
                          + +
                          - {/* Card */} -
                          -

                          {item.title}

                          -

                          {item.description}

                          - {index === 0 && ( -
                          -
                          - )} + {/* Card */} +
                          +

                          {item.title}

                          +

                          {item.description}

                          + {index === 0 && ( +
                          +
                          + )} +
                          -
                          - ))} + )) + }
                          @@ -114,31 +116,27 @@ const resumeLink = `${getSiteUrl()}/resume`

                          {content.expertise.header} - +

                          - {content.expertise.itemlist.map((item, index) => ( -
                          - {/* Accent top border on hover */} -
                          @@ -146,34 +144,40 @@ const resumeLink = `${getSiteUrl()}/resume`

                          {content.proficiencies.header} - +

                          - {content.proficiencies.cards.map((card) => ( -
                          -

                          - {card.subheader} -

                          - -
                          - ))} + { + content.proficiencies.cards.map(card => ( +
                          +

                          + {card.subheader} +

                          + +
                          + )) + }
                          @@ -181,7 +185,7 @@ const resumeLink = `${getSiteUrl()}/resume`

                          {content.proficiencies.philosophy.subheader} - +

                          - {content.proficiencies.philosophy.itemlist.map((item) => ( -
                          -
                          - + { + content.proficiencies.philosophy.itemlist.map(item => ( +
                          +
                          + +
                          +

                          {item.name}

                          +

                          {item.text}

                          -

                          - {item.name} -

                          -

                          - {item.text} -

                          -
                          - ))} + )) + }
                          diff --git a/src/components/Pages/Consent/client/__tests__/index.spec.ts b/src/components/Pages/Consent/client/__tests__/index.spec.ts index 9a9984592..2137384e3 100644 --- a/src/components/Pages/Consent/client/__tests__/index.spec.ts +++ b/src/components/Pages/Consent/client/__tests__/index.spec.ts @@ -16,7 +16,7 @@ const CONSENT_PREFERENCES_READY_EVENT = 'consent-preferences:ready' const SAVE_DELAY_SETTLE_MS = 450 const waitForSaveDelay = async () => { - await new Promise((resolve) => { + await new Promise(resolve => { setTimeout(resolve, SAVE_DELAY_SETTLE_MS) }) } @@ -385,7 +385,9 @@ describe('ConsentPreferencesElement', () => { contactLink.href = '/contact/' contactLink.textContent = 'Contact' window.document.body.append(contactLink) - const discardBtn = window.document.getElementById('consent-unsaved-discard') as HTMLButtonElement | null + const discardBtn = window.document.getElementById( + 'consent-unsaved-discard' + ) as HTMLButtonElement | null const dialog = window.document.getElementById('consent-unsaved-dialog') as HTMLElement | null expect(functionalCheckbox).not.toBeNull() diff --git a/src/components/Pages/Consent/client/index.ts b/src/components/Pages/Consent/client/index.ts index 0a433503a..db72840f4 100644 --- a/src/components/Pages/Consent/client/index.ts +++ b/src/components/Pages/Consent/client/index.ts @@ -39,7 +39,10 @@ export class ConsentPreferencesElement extends LitElement { 'hover:bg-secondary', ] - private static readonly saveButtonEnabledClasses = ['bg-page-inverse', 'hover:bg-secondary-offset'] + private static readonly saveButtonEnabledClasses = [ + 'bg-page-inverse', + 'hover:bg-secondary-offset', + ] private static readonly saveButtonSavingClasses = [ 'bg-secondary-offset', @@ -246,7 +249,12 @@ export class ConsentPreferencesElement extends LitElement { private bindUnsavedDialogListeners(): void { this.removeUnsavedDialogListeners() - if (!this.unsavedDialog || !this.unsavedSaveBtn || !this.unsavedDiscardBtn || !this.unsavedStayBtn) { + if ( + !this.unsavedDialog || + !this.unsavedSaveBtn || + !this.unsavedDiscardBtn || + !this.unsavedStayBtn + ) { return } @@ -518,7 +526,7 @@ export class ConsentPreferencesElement extends LitElement { this.updateSaveButtonState() try { - await new Promise((resolve) => { + await new Promise(resolve => { window.setTimeout(resolve, SAVE_PREFERENCES_DELAY_MS) }) diff --git a/src/components/Pages/Consent/index.astro b/src/components/Pages/Consent/index.astro index 8ce45795f..3be867e52 100644 --- a/src/components/Pages/Consent/index.astro +++ b/src/components/Pages/Consent/index.astro @@ -42,7 +42,7 @@ export type Props = { save: string acceptAll: string rejectAll: string - }, + } cookies: { heading: string description: string @@ -126,7 +126,9 @@ const { content } = Astro.props
                          -

                          {content.essential.heading}

                          +

                          + {content.essential.heading} +

                          {content.functional.description}

                          -

                          +

                          @@ -328,7 +333,7 @@ const { content } = Astro.props class="text-2xl md:text-3xl font-bold text-primary-offset mb-2" > {content.cookies.heading} - +

                          @@ -339,10 +344,10 @@ const { content } = Astro.props items={content.cookies.types} size={6} classes={{ - ul: "mt-6 mb-0 pl-0", - icon: "w-12 h-12 bg-blue-100 mr-2", - li: "border-2 border-page-offset p-4 rounded-xl", - text: "mb-0", + ul: 'mt-6 mb-0 pl-0', + icon: 'w-12 h-12 bg-blue-100 mr-2', + li: 'border-2 border-page-offset p-4 rounded-xl', + text: 'mb-0', }} />

                          @@ -354,7 +359,7 @@ const { content } = Astro.props class="text-2xl md:text-3xl font-bold text-primary-offset mb-4" > {content.management.heading} - +

                          {content.management.subheading} @@ -364,11 +369,11 @@ const { content } = Astro.props items={content.management.types} size={6} classes={{ - ul: "mt-6 mb-0 pl-0", - icon: "w-12 h-12 bg-blue-100 mr-2", - li: "border-2 border-page-offset p-4 rounded-xl", - header: "mb-1 mt-0", - text: "mb-0", + ul: 'mt-6 mb-0 pl-0', + icon: 'w-12 h-12 bg-blue-100 mr-2', + li: 'border-2 border-page-offset p-4 rounded-xl', + header: 'mb-1 mt-0', + text: 'mb-0', }} />

@@ -379,35 +384,31 @@ const { content } = Astro.props class="text-2xl md:text-3xl font-bold text-primary-offset mb-4" > {content.questions.heading} - +

{content.questions.subheading}

- {content.questions.methods.map((item) => { - return ( -
-
- -
-
-

{item.lead}

- - {item.text} - + { + content.questions.methods.map(item => { + return ( +
+
+ +
+
+

{item.lead}

+ + {item.text} + +
-
- ) - })} + ) + }) + }
diff --git a/src/components/Pages/Consent/unsaved.astro b/src/components/Pages/Consent/unsaved.astro index 5f0b87b09..a53d7a70e 100644 --- a/src/components/Pages/Consent/unsaved.astro +++ b/src/components/Pages/Consent/unsaved.astro @@ -3,38 +3,25 @@ import Button from '@components/Button/index.astro' --- -
- -
+
+ +
- + -
-
+
diff --git a/src/components/Pages/Contact/client/__fixtures__/contactForm.fixture.astro b/src/components/Pages/Contact/client/__fixtures__/contactForm.fixture.astro index e242e5379..153f2d82a 100644 --- a/src/components/Pages/Contact/client/__fixtures__/contactForm.fixture.astro +++ b/src/components/Pages/Contact/client/__fixtures__/contactForm.fixture.astro @@ -3,76 +3,76 @@ import ContactFormComponent from '../../index.astro' import type { Props as ContactPageProps } from '../../index.astro' const content = { - header: { - title: 'Contact', - description: 'Get in touch', - }, - contact: { - header: 'Contact Details', - details: [ - { - text: 'Email', - value: 'test@example.com', - icon: 'email', - }, - ], - expertise: { - header: 'Expertise', - areas: [{ text: 'Platform Engineering' }], - }, - chooseUs: { - header: 'Why Us', - areas: [{ text: 'Fast response' }], - }, - }, - form: { - header: 'Send a message', - description: 'Tell us about your project', - formErrorMssg: 'Please fix form errors.', - contact: { - header: 'Your Contact Info', - company: 'Company', - companyPlaceholder: 'Acme Inc', - email: 'Email', - emailPlaceholder: 'you@example.com', - fullName: 'Full Name', - fullNamePlaceholder: 'Jane Doe', - phone: 'Phone', - phonePlaceholder: '+1 555 555 5555', - }, - project: { - header: 'Project Details', - description: 'Description', - descriptionPlaceholder: 'Project details', - timeline: 'Timeline', - timelineOptions: [ - { text: 'Select timeline', value: '' }, - { text: 'Within 1 month', value: 'within-1-month' }, - ], - timelinePlaceholder: 'Timeline', - type: 'Project Type', - typeOptions: [ - { text: 'Select type', value: '' }, - { text: 'Infrastructure', value: 'infrastructure' }, - ], - typePlaceholder: 'Project Type', - }, - files: { - header: 'Attachments', - subheader: 'Optional uploads', - supportedFormats: 'PDF, DOCX', - maxFileSizeText: 'Max file size:', - maxFileSize: '10MB', - maxFilesText: 'Max files:', - maxFiles: '3', - }, - info: [{ lead: 'Note', text: 'We reply quickly.' }], - submitButtonText: 'Send', - waitingText: 'Sending...', - successText: 'Sent successfully.', - errorText: 'Submission failed.', - errorEmail: 'Please provide a valid email.', - }, + header: { + title: 'Contact', + description: 'Get in touch', + }, + contact: { + header: 'Contact Details', + details: [ + { + text: 'Email', + value: 'test@example.com', + icon: 'email', + }, + ], + expertise: { + header: 'Expertise', + areas: [{ text: 'Platform Engineering' }], + }, + chooseUs: { + header: 'Why Us', + areas: [{ text: 'Fast response' }], + }, + }, + form: { + header: 'Send a message', + description: 'Tell us about your project', + formErrorMssg: 'Please fix form errors.', + contact: { + header: 'Your Contact Info', + company: 'Company', + companyPlaceholder: 'Acme Inc', + email: 'Email', + emailPlaceholder: 'you@example.com', + fullName: 'Full Name', + fullNamePlaceholder: 'Jane Doe', + phone: 'Phone', + phonePlaceholder: '+1 555 555 5555', + }, + project: { + header: 'Project Details', + description: 'Description', + descriptionPlaceholder: 'Project details', + timeline: 'Timeline', + timelineOptions: [ + { text: 'Select timeline', value: '' }, + { text: 'Within 1 month', value: 'within-1-month' }, + ], + timelinePlaceholder: 'Timeline', + type: 'Project Type', + typeOptions: [ + { text: 'Select type', value: '' }, + { text: 'Infrastructure', value: 'infrastructure' }, + ], + typePlaceholder: 'Project Type', + }, + files: { + header: 'Attachments', + subheader: 'Optional uploads', + supportedFormats: 'PDF, DOCX', + maxFileSizeText: 'Max file size:', + maxFileSize: '10MB', + maxFilesText: 'Max files:', + maxFiles: '3', + }, + info: [{ lead: 'Note', text: 'We reply quickly.' }], + submitButtonText: 'Send', + waitingText: 'Sending...', + successText: 'Sent successfully.', + errorText: 'Submission failed.', + errorEmail: 'Please provide a valid email.', + }, } satisfies ContactPageProps['content'] --- diff --git a/src/components/Pages/Contact/client/__tests__/formSubmission.spec.ts b/src/components/Pages/Contact/client/__tests__/formSubmission.spec.ts index 6404da74b..ee5638404 100644 --- a/src/components/Pages/Contact/client/__tests__/formSubmission.spec.ts +++ b/src/components/Pages/Contact/client/__tests__/formSubmission.spec.ts @@ -161,7 +161,9 @@ describe('ContactForm submission', () => { expect(context.elements.fields.email.feedback.textContent).toBe( 'Please enter a valid email address.' ) - expect(context.elements.fields.message.feedback.textContent).toBe('Please describe your project') + expect(context.elements.fields.message.feedback.textContent).toBe( + 'Please describe your project' + ) }) }) @@ -306,7 +308,9 @@ describe('ContactForm submission', () => { ) expect(context.elements.fields.email.feedback.classList.contains('hidden')).toBe(false) - expect(context.elements.fields.email.feedback.textContent).toBe('Enter a valid email address.') + expect(context.elements.fields.email.feedback.textContent).toBe( + 'Enter a valid email address.' + ) expect(context.elements.fields.email.input.getAttribute('aria-invalid')).toBe('true') expect(context.elements.fields.email.input.classList.contains('error')).toBe(true) }) diff --git a/src/components/Pages/Contact/client/__tests__/upload.spec.ts b/src/components/Pages/Contact/client/__tests__/upload.spec.ts index 1f1e10f4f..54119fe2a 100644 --- a/src/components/Pages/Contact/client/__tests__/upload.spec.ts +++ b/src/components/Pages/Contact/client/__tests__/upload.spec.ts @@ -56,4 +56,4 @@ describe('Contact upload accessibility', () => { observer.disconnect() }) }) -}) \ No newline at end of file +}) diff --git a/src/components/Pages/Contact/client/formSubmission.ts b/src/components/Pages/Contact/client/formSubmission.ts index 89d52ce51..c936335ce 100644 --- a/src/components/Pages/Contact/client/formSubmission.ts +++ b/src/components/Pages/Contact/client/formSubmission.ts @@ -17,7 +17,13 @@ import { queryContactFormGeneratedFieldError, queryContactFormGenericFields } fr export type ContactUiState = 'idle' | 'loading' | 'success' | 'error' | 'validation' -export const contactPreviewStates = ['loading', 'success', 'error', 'validation', 'confetti'] as const +export const contactPreviewStates = [ + 'loading', + 'success', + 'error', + 'validation', + 'confetti', +] as const type ContactPreviewState = (typeof contactPreviewStates)[number] type ContactPreviewMode = 'confetti' diff --git a/src/components/Pages/Contact/client/index.ts b/src/components/Pages/Contact/client/index.ts index e8f7a7fd9..12808cf6a 100644 --- a/src/components/Pages/Contact/client/index.ts +++ b/src/components/Pages/Contact/client/index.ts @@ -8,12 +8,20 @@ import { LitElement } from 'lit' import { addScriptBreadcrumb } from '@components/scripts/errors' import { handleScriptError } from '@components/scripts/errors/handler' import { initStickySidebar } from '@components/scripts/stickySidebar' -import { getContactFormElements, queryContactProjectTypeSelect, queryContactStickySidebar } from './selectors' +import { + getContactFormElements, + queryContactProjectTypeSelect, + queryContactStickySidebar, +} from './selectors' import type { ContactFormConfig } from './@types' import { initCharacterCounter, initUploadPlaceholder } from './utils' import { initLabelHandlers, type LabelController } from './feedback' import { initEmailValidationHandler } from './email' -import { applyContactPreviewState, initFormSubmission, resolveContactPreviewState } from './formSubmission' +import { + applyContactPreviewState, + initFormSubmission, + resolveContactPreviewState, +} from './formSubmission' import { initGenericValidation, initNameLengthHandler, initMssgLengthHandler } from './validation' import { defineCustomElement } from '@components/scripts/utils' import { isProd } from '@components/scripts/utils/environmentClient' @@ -91,7 +99,9 @@ export class ContactFormElement extends LitElement { return } - const projectType = window.location.search ? new URLSearchParams(window.location.search).get('type') : null + const projectType = window.location.search + ? new URLSearchParams(window.location.search).get('type') + : null if (!projectType) { return } @@ -101,7 +111,9 @@ export class ContactFormElement extends LitElement { return } - const hasOption = Array.from(projectTypeSelect.options).some(option => option.value === projectType) + const hasOption = Array.from(projectTypeSelect.options).some( + option => option.value === projectType + ) if (!hasOption) { return } diff --git a/src/components/Pages/Contact/client/selectors.ts b/src/components/Pages/Contact/client/selectors.ts index 3f7771803..8d55477a3 100644 --- a/src/components/Pages/Contact/client/selectors.ts +++ b/src/components/Pages/Contact/client/selectors.ts @@ -155,16 +155,18 @@ const isUppyDashboardRoot = (element: Element): element is UppyDashboardRoot => } export const queryAccessibilityLabelTargets = (root: ParentNode): AccessibilityLabelTarget[] => { - return Array.from(root.querySelectorAll('input[type="text"], input[type="email"], textarea')).filter( - isAccessibilityLabelTarget - ) + return Array.from( + root.querySelectorAll('input[type="text"], input[type="email"], textarea') + ).filter(isAccessibilityLabelTarget) } export const queryUppyDashboardRoots = (root: ParentNode): UppyDashboardRoot[] => { return Array.from(root.querySelectorAll('.uppy-Dashboard')).filter(isUppyDashboardRoot) } -export const queryContactProjectTypeSelect = (root: ParentNode = document): HTMLSelectElement | null => { +export const queryContactProjectTypeSelect = ( + root: ParentNode = document +): HTMLSelectElement | null => { const projectTypeSelect = root.querySelector('#project_type') return projectTypeSelect instanceof HTMLSelectElement ? projectTypeSelect : null } diff --git a/src/components/Pages/Contact/index.astro b/src/components/Pages/Contact/index.astro index d54eca742..400a67981 100644 --- a/src/components/Pages/Contact/index.astro +++ b/src/components/Pages/Contact/index.astro @@ -49,11 +49,11 @@ export type Props = { expertise: { header: string areas: { text: string }[] - }, + } chooseUs: { - header: string, - areas: { text: string }[], - }, + header: string + areas: { text: string }[] + } } form: { header: string @@ -69,7 +69,7 @@ export type Props = { fullNamePlaceholder: string phone: string phonePlaceholder: string - }, + } project: { header: string description: string @@ -86,7 +86,7 @@ export type Props = { value: string }[] typePlaceholder: string - }, + } files: { header: string subheader: string @@ -95,7 +95,7 @@ export type Props = { maxFileSize: string maxFilesText: string maxFiles: string - }, + } info: { lead: string text: string @@ -121,323 +121,321 @@ const { content, prefilledProjectType = '' } = Astro.props {/** Contact Form Section */}
-
+
{/** Contact Form */}
- {/** Contact FormHeader */} -
-

- {content.form.header} -

-

{content.form.description}

-
+ {/** Contact FormHeader */} +
+

+ {content.form.header} +

+

{content.form.description}

+
- + - {/** Personal Information */} -
-

-
- {content.form.contact.header} -

+ {/** Personal Information */} +
+

+
+ {content.form.contact.header} +

-
-
- - - -
+
+
+ + + +
-
- - - -
+
+ + +
+
-
-
- - -
+
+
+ + +
-
- - -
+
+ +
+
- {/** Project Information */} -
-

-
- {content.form.project.header} -

+ {/** Project Information */} +
+

+
+ {content.form.project.header} +

-
-
- - + { + content.form.project.typeOptions.map(item => ( + - ))} - -
- -
- - -
+ )) + } +
-
-
- {/** File Upload Section */} -
-

-
- {content.form.files.header} -

-

{content.form.files.subheader}

+
+ + + +
+ 0/2000 characters +
+
+
-
- -
-

- Supported formats: {content.form.files.supportedFormats} -

-

- {content.form.files.maxFileSizeText} {content.form.files.maxFileSize} • {content.form.files.maxFilesText} {content.form.files.maxFiles} -

-
+ {/** File Upload Section */} +
+

+
+ {content.form.files.header} +

+

{content.form.files.subheader}

+ +
+ +
+

+ Supported formats: + {content.form.files.supportedFormats} +

+

+ {content.form.files.maxFileSizeText} + {content.form.files.maxFileSize} • {content.form.files.maxFilesText} + {content.form.files.maxFiles} +

+
- {/** GDPR Consent */} - + {/** GDPR Consent */} + - {/** Data Retention Notice */} -
- {content.form.info.map((item) => ( + {/** Data Retention Notice */} +
+ { + content.form.info.map(item => (

- {item.lead}{item.text} + {item.lead} + {item.text} {item.email && ( - {item.email} + + {item.email} + )}

- ))} + )) + } +
+ + +
+ {/** Intentionally not using Button component. */} +
+
+ - -
- {/** Intentionally not using Button component. */} - + {/** Form Messages */} +
@@ -126,7 +120,9 @@ const normalizedDownloadUrl = `/downloads/${fileName}`
-

+

Instant Access

Get The Ebook

@@ -238,7 +234,12 @@ const normalizedDownloadUrl = `/downloads/${fileName}` class="block w-full bg-success text-primary-inverse font-semibold py-3 px-6 rounded-xl hover:bg-success-offset focus:outline-none focus:ring-2 focus:ring-success focus:ring-offset-2 focus:ring-offset-page-active transition-all duration-200 text-center" id="downloadBtn" > - + Download {fileType}
diff --git a/src/components/Pages/MyData/client/__tests__/index.spec.ts b/src/components/Pages/MyData/client/__tests__/index.spec.ts index df7df728f..885d8fff6 100644 --- a/src/components/Pages/MyData/client/__tests__/index.spec.ts +++ b/src/components/Pages/MyData/client/__tests__/index.spec.ts @@ -15,9 +15,13 @@ type VerifyResult = | { status: 'deleted' } | { status: 'expired' } -const requestDataMock = vi.fn< - (_input: { email: string; requestType: 'ACCESS' | 'DELETE' }) => Promise> ->() +const requestDataMock = + vi.fn< + (_input: { + email: string + requestType: 'ACCESS' | 'DELETE' + }) => Promise> + >() const verifyDsarMock = vi.fn<(_input: { token: string }) => Promise>>() @@ -105,7 +109,10 @@ describe('PrivacyForm behavior', () => { elements.accessForm.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })) await flushMicrotasks() - expect(requestDataMock).toHaveBeenCalledWith({ email: 'test@example.com', requestType: 'ACCESS' }) + expect(requestDataMock).toHaveBeenCalledWith({ + email: 'test@example.com', + requestType: 'ACCESS', + }) expect(elements.accessMessage.textContent).toBe('Access request sent.') expect(elements.accessMessage.classList.contains('hidden')).toBe(false) expect(elements.accessMessage.classList.contains('border-success')).toBe(true) @@ -180,7 +187,9 @@ describe('PrivacyForm behavior', () => { await flushMicrotasks() expect(requestDataMock).not.toHaveBeenCalled() - expect(elements.deleteMessage.textContent).toBe('Please confirm you understand the deletion request.') + expect(elements.deleteMessage.textContent).toBe( + 'Please confirm you understand the deletion request.' + ) expect(elements.deleteMessage.classList.contains('border-danger')).toBe(true) }, }) @@ -204,8 +213,13 @@ describe('PrivacyForm behavior', () => { }, }, waitForReady: async (element: PrivacyFormElementInstance) => { - window.history.replaceState({}, '', 'http://localhost/privacy/my-data?token=unit-test-token') - ;(element as unknown as { downloadJson: typeof downloadJsonSpy }).downloadJson = downloadJsonSpy + window.history.replaceState( + {}, + '', + 'http://localhost/privacy/my-data?token=unit-test-token' + ) + ;(element as unknown as { downloadJson: typeof downloadJsonSpy }).downloadJson = + downloadJsonSpy ;(element as unknown as { navigateTo: typeof navigateToSpy }).navigateTo = navigateToSpy element.initialize() }, diff --git a/src/components/Pages/MyData/client/__tests__/selectors.spec.ts b/src/components/Pages/MyData/client/__tests__/selectors.spec.ts index e84bef66e..4a16e04de 100644 --- a/src/components/Pages/MyData/client/__tests__/selectors.spec.ts +++ b/src/components/Pages/MyData/client/__tests__/selectors.spec.ts @@ -10,11 +10,16 @@ type PrivacyFormModule = WebComponentModule type ActionResult = { data?: TData; error?: { message?: string } } -const requestDataMock = vi.fn< - (_input: { email: string; requestType: 'ACCESS' | 'DELETE' }) => Promise> ->() +const requestDataMock = + vi.fn< + (_input: { + email: string + requestType: 'ACCESS' | 'DELETE' + }) => Promise> + >() -const verifyDsarMock = vi.fn<(_input: { token: string }) => Promise>>() +const verifyDsarMock = + vi.fn<(_input: { token: string }) => Promise>>() const myDataContent = { header: { diff --git a/src/components/Pages/MyData/client/index.ts b/src/components/Pages/MyData/client/index.ts index dc04ef384..ca55f75cf 100644 --- a/src/components/Pages/MyData/client/index.ts +++ b/src/components/Pages/MyData/client/index.ts @@ -245,7 +245,10 @@ export class PrivacyFormElement extends LitElement { params: URLSearchParams, requestFormType: RequestFormType ): RequestPreviewState | null { - const previewState = params.get(requestPreviewQueryParams[requestFormType])?.trim().toLowerCase() + const previewState = params + .get(requestPreviewQueryParams[requestFormType]) + ?.trim() + .toLowerCase() if (!previewState) { return null diff --git a/src/components/Pages/MyData/index.astro b/src/components/Pages/MyData/index.astro index cc40eb0f3..dc5f37385 100644 --- a/src/components/Pages/MyData/index.astro +++ b/src/components/Pages/MyData/index.astro @@ -32,14 +32,14 @@ export type Props = { description: string label: string buttonText: string - }, - deleteData: { + } + deleteData: { heading: string description: string label: string buttonText: string confirmText: string - }, + } next: { heading: string items: { @@ -62,7 +62,8 @@ const { content } = Astro.props aria-live="polite" aria-atomic="true" class="hidden rounded-xl border px-4 py-3 text-sm" - >
+ > +
+ > +
@@ -148,13 +150,11 @@ const { content } = Astro.props {content.deleteData.heading} -
+
- + {content.deleteData.description}
@@ -205,7 +205,8 @@ const { content } = Astro.props aria-live="polite" aria-atomic="true" tabindex="-1" - >
+ > +
@@ -214,10 +215,7 @@ const { content } = Astro.props

{content.next.heading}

- + diff --git a/src/components/Pages/MyData/server/index.ts b/src/components/Pages/MyData/server/index.ts index ac9974fb4..a21917842 100644 --- a/src/components/Pages/MyData/server/index.ts +++ b/src/components/Pages/MyData/server/index.ts @@ -1,4 +1,7 @@ -export const statusMessages: Record = { +export const statusMessages: Record< + string, + { type: 'success' | 'error' | 'info'; message: string } +> = { sent: { type: 'success', message: diff --git a/src/components/Pages/Newsletter/Confirm/client/__tests__/index.spec.ts b/src/components/Pages/Newsletter/Confirm/client/__tests__/index.spec.ts index cd335891e..5a9bed5ca 100644 --- a/src/components/Pages/Newsletter/Confirm/client/__tests__/index.spec.ts +++ b/src/components/Pages/Newsletter/Confirm/client/__tests__/index.spec.ts @@ -17,9 +17,7 @@ type ConfirmActionData = { message?: string } -const confirmMock = vi.fn< - (_input: { token: string }) => Promise> ->() +const confirmMock = vi.fn<(_input: { token: string }) => Promise>>() vi.mock('astro:actions', () => ({ actions: { diff --git a/src/components/Pages/Newsletter/Confirm/client/__tests__/selectors.spec.ts b/src/components/Pages/Newsletter/Confirm/client/__tests__/selectors.spec.ts index 27072b6e0..dc59e2bfe 100644 --- a/src/components/Pages/Newsletter/Confirm/client/__tests__/selectors.spec.ts +++ b/src/components/Pages/Newsletter/Confirm/client/__tests__/selectors.spec.ts @@ -17,9 +17,7 @@ type ConfirmActionData = { message?: string } -const confirmMock = vi.fn< - (_input: { token: string }) => Promise> ->() +const confirmMock = vi.fn<(_input: { token: string }) => Promise>>() vi.mock('astro:actions', () => ({ actions: { diff --git a/src/components/Pages/Newsletter/Confirm/client/selectors.ts b/src/components/Pages/Newsletter/Confirm/client/selectors.ts index 3673ada41..abb28dae8 100644 --- a/src/components/Pages/Newsletter/Confirm/client/selectors.ts +++ b/src/components/Pages/Newsletter/Confirm/client/selectors.ts @@ -73,8 +73,18 @@ export function getNewsletterConfirmElements(root?: SelectorRoot): NewsletterCon 'Newsletter confirm status announcer not found', root ), - userEmail: queryRequiredElement('#user-email', isHtmlElement, 'Newsletter confirm email not found', root), - errorTitle: queryRequiredElement('#error-title', isHtmlElement, 'Newsletter confirm error title not found', root), + userEmail: queryRequiredElement( + '#user-email', + isHtmlElement, + 'Newsletter confirm email not found', + root + ), + errorTitle: queryRequiredElement( + '#error-title', + isHtmlElement, + 'Newsletter confirm error title not found', + root + ), errorMessage: queryRequiredElement( '#error-message', isHtmlElement, diff --git a/src/components/Pages/Newsletter/Confirm/index.astro b/src/components/Pages/Newsletter/Confirm/index.astro index 8e6a5ccf6..12b9f5175 100644 --- a/src/components/Pages/Newsletter/Confirm/index.astro +++ b/src/components/Pages/Newsletter/Confirm/index.astro @@ -24,28 +24,23 @@ const { token } = Astro.props
-

+

-
- +
+
-

+

Confirming Your Subscription

@@ -53,9 +48,7 @@ const { token } = Astro.props

Please wait while we verify your confirmation link.

-

- This usually takes just a moment. -

+

This usually takes just a moment.

@@ -64,14 +57,16 @@ const { token } = Astro.props @@ -133,14 +124,16 @@ const { token } = Astro.props