diff --git a/.github/instructions/general.instructions.md b/.github/instructions/general.instructions.md index 7d8c91020..4f63ab973 100644 --- a/.github/instructions/general.instructions.md +++ b/.github/instructions/general.instructions.md @@ -23,6 +23,14 @@ applyTo: "**" - Preact exists only for the Markdown E2E harness under `src/lib/markdown/__tests__/e2e`; all production UI components must use Lit web components instead of Preact. - Z-index: never hard-code numeric z-index values (including Tailwind `z-*` utilities). Always use the CSS variables in `src/styles/index.css` (e.g., `z-index: var(--z-nav)` or `z-(--z-nav)`). If no existing token fits, ask the user what z-layer to use before adding/changing tokens. +## Component Reuse Policy + +- Prefer existing `List`, `Icon`, and `Button` components whenever possible. +- `List`: use an existing list layout first. If no layout fits, ask the user before adding a new layout. +- `Icon`: use existing icons first. If a new icon is needed, ask the user first and describe the icon you plan to add. +- `Button`: use the shared `Button` component when the current API supports the use case. +- If a button would require adding `Button` props or forcing behavior that does not match the component, ask the user before using a raw HTML ` diff --git a/src/components/Button/server/__tests__/index.spec.ts b/src/components/Button/server/__tests__/index.spec.ts index 529c37b93..9782b0ca2 100644 --- a/src/components/Button/server/__tests__/index.spec.ts +++ b/src/components/Button/server/__tests__/index.spec.ts @@ -39,19 +39,6 @@ describe('Button server helpers', () => { expect(classList['custom']).toBe(true) expect(classList['extra-class']).toBe(true) }) - - it('supports the spotlight variant', () => { - const classList = buildButtonClassList({ - variant: 'spotlight', - }) - - expect(classList).toMatchObject({ - 'bg-spotlight': true, - 'hover:bg-spotlight-offset': true, - 'focus-visible:bg-spotlight-offset': true, - 'active:bg-spotlight-offset': true, - }) - }) }) describe('resolveAriaLabel', () => { diff --git a/src/components/Button/server/index.ts b/src/components/Button/server/index.ts index a6a1d9c43..28ad59a87 100644 --- a/src/components/Button/server/index.ts +++ b/src/components/Button/server/index.ts @@ -1,4 +1,13 @@ -export type ButtonVariant = 'primary' | 'secondary' | 'twitter' | 'success' | 'warning' | 'spotlight' | 'icon' +export type ButtonVariant = + | 'danger' + | 'note' + | 'primary' + | 'secondary' + | 'twitter' + | 'success' + | 'info' + | 'warning' + | 'icon' export type ButtonSize = 'small' | 'medium' | 'large' export type ButtonType = 'button' | 'submit' | 'reset' export type IconPosition = 'left' | 'right' | 'only' @@ -34,15 +43,14 @@ function addClassNames(classList: ClassList, classNames: string) { const baseButtonClasses = 'inline-flex items-center justify-center text-center align-middle whitespace-nowrap select-none ' + - 'no-underline ' + + 'no-underline hover:no-underline focus:no-underline focus-visible:no-underline decoration-transparent ' + '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 after:opacity-0 after:transition-opacity after:duration-150 after:ease-out " + 'focus-visible:after:opacity-100 focus-visible:after:[inset:-6px] focus-visible:after:border-spotlight ' + 'border-2 border-solid border-transparent rounded-md ' + 'font-bold uppercase tracking-[0.08em] ' + 'text-sm leading-5 ' + - '[box-shadow:var(--shadow-sm)] [text-shadow:var(--shadow-text)] ' + - 'transition-[color,background-color,border-color,box-shadow,transform] duration-200 ease-in-out ' + + 'transition-[color,background-color,border-color] duration-200 ease-in-out ' + 'disabled:cursor-not-allowed disabled:opacity-[0.65] disabled:pointer-events-none ' + 'aria-disabled:cursor-not-allowed aria-disabled:opacity-[0.65] aria-disabled:pointer-events-none' @@ -56,38 +64,52 @@ const sizeClasses: Record = { } const variantClasses: Record = { + danger: + 'bg-danger text-white ' + + 'hover:bg-danger-offset ' + + 'focus-visible:bg-danger-offset ' + + 'active:bg-danger-offset', + icon: + 'aspect-square bg-transparent text-white p-2 ' + + 'hover:bg-page-offset hover:text-primary ' + + 'focus-visible:bg-page-offset focus-visible:text-primary ' + + 'active:bg-page-offset', + info: + 'bg-info text-white ' + + 'hover:bg-info-offset ' + + 'focus-visible:bg-info-offset ' + + 'active:bg-info-offset', + note: + 'bg-note text-white ' + + 'hover:bg-note-offset ' + + 'focus-visible:bg-note-offset ' + + 'active:bg-note-offset', primary: - 'bg-primary text-content ' + - 'hover:bg-primary-offset hover:[box-shadow:var(--shadow-hover)] hover:-translate-y-px ' + - 'focus-visible:bg-primary-offset focus-visible:[box-shadow:var(--shadow-hover)] focus-visible:-translate-y-px ' + - 'active:bg-primary-offset active:[box-shadow:var(--shadow-active)] active:translate-y-px', + 'bg-primary text-primary-inverse ' + + 'hover:bg-primary-offset ' + + 'focus-visible:bg-primary-offset ' + + 'active:bg-primary-offset', secondary: - 'bg-secondary text-content ' + - 'hover:bg-secondary-offset hover:[box-shadow:var(--shadow-hover)] hover:-translate-y-px ' + - 'focus-visible:bg-secondary-offset focus-visible:[box-shadow:var(--shadow-hover)] focus-visible:-translate-y-px ' + - 'active:bg-secondary-offset active:[box-shadow:var(--shadow-active)] active:translate-y-px', - twitter: 'bg-x border-x text-content hover:bg-transparent hover:text-x focus-visible:bg-transparent focus-visible:text-x', + 'bg-secondary text-secondary-inverse ' + + 'hover:bg-secondary-offset ' + + 'focus-visible:bg-secondary-offset ' + + 'active:bg-secondary-offset', success: 'bg-success text-white ' + - 'hover:bg-success-offset hover:[box-shadow:var(--shadow-hover)] hover:-translate-y-px ' + - 'focus-visible:bg-success-offset focus-visible:[box-shadow:var(--shadow-hover)] focus-visible:-translate-y-px ' + - 'active:bg-success-offset active:[box-shadow:var(--shadow-active)] active:translate-y-px', + 'hover:bg-success-offset ' + + 'focus-visible:bg-success-offset ' + + 'active:bg-success-offset', + twitter: + 'bg-x border-x text-white ' + + 'hover:bg-transparent hover:text-x ' + + 'focus-visible:bg-transparent ' + + 'focus-visible:text-x', warning: - 'bg-warning text-content ' + - 'hover:bg-warning-offset hover:[box-shadow:var(--shadow-hover)] hover:-translate-y-px ' + - 'focus-visible:bg-warning-offset focus-visible:[box-shadow:var(--shadow-hover)] focus-visible:-translate-y-px ' + - 'active:bg-warning-offset active:[box-shadow:var(--shadow-active)] active:translate-y-px', - spotlight: - 'bg-spotlight text-black ' + - 'hover:bg-spotlight-offset hover:[box-shadow:var(--shadow-hover)] hover:-translate-y-px ' + - 'focus-visible:bg-spotlight-offset focus-visible:[box-shadow:var(--shadow-hover)] focus-visible:-translate-y-px ' + - 'active:bg-spotlight-offset active:[box-shadow:var(--shadow-active)] active:translate-y-px', - icon: - 'aspect-square bg-transparent text-content p-2 ![box-shadow:none] ' + - 'hover:bg-page-offset hover:text-primary ' + - 'focus-visible:bg-page-offset focus-visible:text-primary ' + - 'active:bg-page-offset', -} + 'bg-warning-offset text-white ' + + 'hover:bg-warning ' + + 'focus-visible:bg-warning ' + + 'active:bg-warning', + } export interface ButtonClassOptions { variant?: ButtonVariant @@ -101,10 +123,15 @@ export function buildButtonClassList({ additionalClasses, }: ButtonClassOptions) { const classList: ClassList = {} + const selectedVariant = variant as string + + if (!(selectedVariant in variantClasses)) { + throw new Error(`Button: unsupported variant '${selectedVariant}'`) + } addClassNames(classList, baseButtonClasses) addClassNames(classList, sizeClasses[size]) - addClassNames(classList, variantClasses[variant]) + addClassNames(classList, variantClasses[selectedVariant as ButtonVariant]) if (additionalClasses?.trim()) { addClassNames(classList, additionalClasses) diff --git a/src/components/CallToAction/Download/index.astro b/src/components/CallToAction/Download/index.astro index f0c96c2d2..8afe09c8a 100644 --- a/src/components/CallToAction/Download/index.astro +++ b/src/components/CallToAction/Download/index.astro @@ -4,6 +4,7 @@ * download page, which is implemented by Forms/Download. */ import { BuildError } from '@lib/errors/BuildError' +import Button from '@components/Button/index.astro' import List from '@components/List/index.astro' import Icon from '@components/Icon/index.astro' @@ -74,7 +75,7 @@ const downloadUrl = `/downloads/${normalizedResource}` -

+

{title}

@@ -99,37 +100,38 @@ const downloadUrl = `/downloads/${normalizedResource}` -

Free resource · Instant access

+

Free resource · Instant access

- - {buttonText} - + {buttonText} + { secondaryLink && ( - - {secondaryLink.text} - + variant="secondary" + class="w-full mt-2 px-6 py-3 bg-page-offset text-content-inverse hover:bg-page-offset focus-visible:bg-page-offset" + text={secondaryLink.text} + /> ) }
-

No credit card required.

+

No credit card required.

diff --git a/src/components/CallToAction/Newsletter/layouts/article.astro b/src/components/CallToAction/Newsletter/layouts/article.astro index 0b73c44f7..4de74884e 100644 --- a/src/components/CallToAction/Newsletter/layouts/article.astro +++ b/src/components/CallToAction/Newsletter/layouts/article.astro @@ -1,6 +1,7 @@ --- import GDPRConsent from '@components/Consent/Checkbox/index.astro' import Confetti from '@components/Animations/Confetti/index.astro' +import Icon from '@components/Icon/index.astro' import type { NewsletterProps } from '@components/CallToAction/Newsletter/client/@types' export type Props = Omit @@ -57,44 +58,18 @@ const { class="relative px-6 py-2 bg-primary hover:bg-primary-offset text-page-base font-semibold rounded text-sm transition-colors whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed 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" > {buttonText} - - +