petition styles update

This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-04-30 23:32:29 -03:00
parent aff72f26af
commit 12cc55b663
9 changed files with 156 additions and 31 deletions

View file

@ -19,31 +19,39 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
return notFound()
}
const consentHTML =
typeof petition.form.consent !== 'number'
? convertLexicalToHTML({ data: petition.form.consent.text })
: ''
return (
<main className="bg-background text-foreground">
<section className="relative overflow-hidden border-b border-border">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_right,oklch(var(--primary)/0.2),transparent_40%),radial-gradient(circle_at_bottom_left,oklch(var(--accent)/0.18),transparent_45%)]" />
<div className="relative mx-auto flex min-h-[75vh] w-full max-w-4xl items-center justify-center px-4 py-20 sm:px-6 lg:px-8">
<main
data-theme="petition"
className="h-screen w-screen overflow-y-auto bg-background text-foreground"
>
<section className="relative overflow-hidden border-b border-border bg-[var(--petition-hero)] text-[var(--petition-hero-foreground)]">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_right,var(--petition-hero-glow),transparent_40%),radial-gradient(circle_at_bottom_left,var(--petition-hero-depth),transparent_46%)]" />
<div className="relative mx-auto flex min-h-[72vh] w-full max-w-5xl items-center justify-center px-4 py-16 sm:px-6 sm:py-20 lg:px-8">
<div className="mx-auto flex max-w-3xl flex-col items-center space-y-6 text-center">
<span className="inline-flex w-fit rounded-full border border-border bg-card px-3 py-1 text-xs font-medium uppercase tracking-wider text-muted-foreground">
<span className="inline-flex w-fit rounded-full border border-white/20 bg-[var(--petition-hero-chip)] px-3 py-1 text-xs font-medium uppercase tracking-wider text-[var(--petition-hero-chip-foreground)]">
Abaixo-assinado
</span>
<h1 className="text-4xl font-bold leading-tight tracking-tight sm:text-5xl lg:text-6xl">
<h1 className="text-4xl font-bold leading-tight tracking-tight text-balance sm:text-5xl lg:text-6xl">
{petition.title}
</h1>
<h2 className="max-w-2xl text-lg leading-relaxed text-muted-foreground sm:text-xl">
<h2 className="max-w-2xl border-none text-lg leading-relaxed text-[var(--petition-hero-muted)] sm:text-xl">
{petition.subtitle}
</h2>
<div className="flex flex-wrap justify-center gap-3">
<div className="flex flex-wrap justify-center gap-3 border-t border-white/15 pt-6">
<Link
href="#formulario"
className="inline-flex items-center justify-center rounded-md bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
className="inline-flex items-center justify-center rounded-md bg-[var(--petition-hero-cta)] px-5 py-2.5 text-sm font-semibold text-[var(--petition-hero-cta-foreground)] transition-colors hover:bg-[color-mix(in_oklab,var(--petition-hero-cta)_90%,white)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
>
Assinar agora
</Link>
<Link
href="#detalhes"
className="inline-flex items-center justify-center rounded-md border border-border bg-background px-5 py-2.5 text-sm font-semibold text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
className="inline-flex items-center justify-center rounded-md border border-white/30 bg-transparent px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70"
>
Ler detalhes
</Link>
@ -54,18 +62,30 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
<section
id="detalhes"
className="mx-auto w-full max-w-4xl px-4 py-14 sm:px-6 lg:px-8 lg:py-16"
className="mx-auto w-full max-w-3xl px-4 py-14 sm:px-6 lg:px-8 lg:py-16"
>
<article
className="space-y-5 leading-relaxed text-foreground/90 [&_a]:font-medium [&_a]:text-primary [&_a]:underline-offset-4 [&_a:hover]:underline [&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_h1]:text-3xl [&_h1]:font-bold [&_h2]:text-2xl [&_h2]:font-semibold [&_h3]:text-xl [&_h3]:font-semibold [&_li]:ml-5 [&_li]:list-disc [&_p]:text-base [&_strong]:text-foreground"
className="space-y-6 text-lg leading-8 text-foreground/90 [&_a]:font-medium [&_a]:text-primary [&_a]:underline-offset-4 [&_a:hover]:underline [&_blockquote]:border-l-4 [&_blockquote]:border-primary/40 [&_blockquote]:pl-4 [&_h1]:text-3xl [&_h1]:font-bold [&_h2]:border-none [&_h2]:text-2xl [&_h2]:font-semibold [&_h2]:text-primary [&_h3]:text-xl [&_h3]:font-semibold [&_li]:ml-5 [&_li]:list-disc [&_p]:text-lg [&_strong]:font-semibold [&_strong]:text-foreground"
dangerouslySetInnerHTML={{ __html: convertLexicalToHTML({ data: petition.body }) }}
/>
</section>
<section className="border-t border-border bg-muted/30">
<div className="mx-auto w-full max-w-3xl px-4 py-14 sm:px-6 lg:px-8 lg:py-16">
<div className="rounded-xl border border-border bg-card p-5 shadow-sm sm:p-7">
<PetitionForm id="formulario" petition={petition} />
<section className="border-t border-border bg-[var(--petition-form-section)]">
<div className="mx-auto grid w-full max-w-5xl gap-8 px-4 py-14 sm:px-6 lg:grid-cols-[0.82fr_1.18fr] lg:px-8 lg:py-16">
<div className="space-y-4 self-start">
<span className="text-sm font-semibold uppercase tracking-wider text-primary">
Participe da mobilização
</span>
<h2 className="border-none text-left text-3xl font-bold leading-tight text-foreground sm:text-4xl">
Sua assinatura fortalece a pressão pela jornada 5x2.
</h2>
<p className="text-base leading-7 text-muted-foreground">
Preencha o formulário para somar apoio público à proposta. Os dados serão usados
apenas para registrar sua participação nesta campanha.
</p>
</div>
<div className="rounded-xl border border-border bg-card p-5 text-card-foreground shadow-[0_24px_60px_rgb(122_25_18/0.12)] sm:p-7">
<PetitionForm id="formulario" petition={petition} consentHTML={consentHTML} />
</div>
</div>
</section>

View file

@ -104,6 +104,51 @@ body,
--sidebar-ring: oklch(0.9 0.14 143);
--background: #ae1603;
--foreground: oklch(0.98 0.004 197.1);
--field-background: #ffffff;
--field-foreground: #000000;
--field-placeholder: rgb(0 0 0 / 80%);
--field-border: rgb(255 255 255 / 80%);
--field-disabled-background: rgb(255 255 255 / 70%);
--field-disabled-foreground: rgb(0 0 0 / 70%);
--field-icon: rgb(0 0 0 / 60%);
}
[data-theme="petition"] {
--background: #fff8f2;
--foreground: #2c2522;
--card: #ffffff;
--card-foreground: #2c2522;
--popover: #ffffff;
--popover-foreground: #2c2522;
--primary: #c51414;
--primary-foreground: #ffffff;
--secondary: #ffe8df;
--secondary-foreground: #5f1715;
--muted: #f4e7de;
--muted-foreground: #675c56;
--accent: #ffcf2e;
--accent-foreground: #332300;
--border: rgb(189 119 104 / 24%);
--input: rgb(189 119 104 / 32%);
--ring: #c51414;
--destructive: #b42318;
--petition-hero: #b6120f;
--petition-hero-foreground: #fffaf5;
--petition-hero-muted: #ffe2d6;
--petition-hero-chip: rgb(255 255 255 / 12%);
--petition-hero-chip-foreground: #fff7ed;
--petition-hero-glow: rgb(255 207 46 / 24%);
--petition-hero-depth: rgb(122 0 0 / 35%);
--petition-hero-cta: #ffcf2e;
--petition-hero-cta-foreground: #332300;
--petition-form-section: #f6ebe4;
--field-background: #ffffff;
--field-foreground: #2c2522;
--field-placeholder: rgb(44 37 34 / 58%);
--field-border: #ddc7bc;
--field-disabled-background: #ece0d9;
--field-disabled-foreground: rgb(44 37 34 / 55%);
--field-icon: rgb(44 37 34 / 58%);
}
.dark {

View file

@ -22,14 +22,14 @@ import { PhoneInput } from './PhoneInput'
import { StateSelect } from './StateSelect'
import { CitySelect } from './CitySelect'
import { PostalCodeInput } from './PostalCodeInput'
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'
interface PetitionFormProps {
id: string
petition: Petition
consentHTML: string
}
export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
export const PetitionForm = ({ id, petition, consentHTML }: PetitionFormProps) => {
const methods = useForm<PetitionFormInput>({
resolver: zodResolver(petitionFormSchema),
defaultValues: {
@ -71,9 +71,7 @@ export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
<FieldSet>
{petition.form.title ? <FieldLegend>{petition.form.title}</FieldLegend> : null}
{petition.form.subtitle ? (
<FieldDescription>
Assine esta petição e ajude a manter a orla de Salvador para todos nós!
</FieldDescription>
<FieldDescription>{petition.form.subtitle}</FieldDescription>
) : null}
<FieldGroup>
<Field>
@ -105,10 +103,11 @@ export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
<Textarea id="comment" {...methods.register('comment')} />
</Field>
<Field>
{typeof petition.form.consent !== 'number' ? (
<FieldDescription
{consentHTML ? (
<div
className="petition-consent text-left text-sm leading-normal font-normal text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:text-primary [&_p]:m-0 [&_strong]:font-semibold [&_strong]:text-foreground"
dangerouslySetInnerHTML={{
__html: convertLexicalToHTML({ data: petition.form.consent.text }),
__html: consentHTML,
}}
/>
) : null}
@ -117,7 +116,11 @@ export const PetitionForm = ({ id, petition }: PetitionFormProps) => {
{methods.formState.errors.root.message}
</FieldDescription>
) : null}
<Button type="submit" disabled={isSubmitting}>
<Button
type="submit"
disabled={isSubmitting}
className="text-primary-foreground hover:text-primary-foreground"
>
{isSubmitting ? 'Enviando...' : 'Assinar'}
</Button>
</Field>

View file

@ -7,7 +7,7 @@ function Input({ className, type, ...props }: ComponentProps<'input'>) {
type={type}
data-slot="input"
className={cn(
'h-8 w-full min-w-0 rounded-full border border-white/80 bg-white p-4 py-5 text-base text-black transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-black placeholder:text-black/80 focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-white/70 disabled:text-black/70 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm',
'h-8 w-full min-w-0 rounded-full border border-[var(--field-border)] bg-[var(--field-background)] p-4 py-5 text-base text-[var(--field-foreground)] transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-[var(--field-foreground)] placeholder:text-[var(--field-placeholder)] focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-[var(--field-disabled-background)] disabled:text-[var(--field-disabled-foreground)] disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm',
className,
)}
{...props}

View file

@ -20,11 +20,11 @@ function NativeSelect({ className, size = 'default', ...props }: NativeSelectPro
<select
data-slot="native-select"
data-size={size}
className="w-full min-w-0 appearance-none rounded-full border border-white/80 bg-white px-4 py-2 text-sm text-black transition-colors outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-black/80 focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-white/70 disabled:text-black/70 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5"
className="w-full min-w-0 appearance-none rounded-full border border-[var(--field-border)] bg-[var(--field-background)] px-4 py-2 text-sm text-[var(--field-foreground)] transition-colors outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-[var(--field-placeholder)] focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-[var(--field-disabled-background)] disabled:text-[var(--field-disabled-foreground)] aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5"
{...props}
/>
<ChevronDownIcon
className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-black/60 select-none"
className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-[var(--field-icon)] select-none"
aria-hidden="true"
data-slot="native-select-icon"
/>

View file

@ -37,14 +37,14 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-white/80 bg-white py-2 pr-2 pl-2.5 text-sm whitespace-nowrap text-black transition-colors outline-none select-none focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-black/50 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-[var(--field-border)] bg-[var(--field-background)] py-2 pr-2 pl-2.5 text-sm whitespace-nowrap text-[var(--field-foreground)] transition-colors outline-none select-none focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-[var(--field-placeholder)] data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="pointer-events-none size-4 text-black/60" />
<ChevronDownIcon className="pointer-events-none size-4 text-[var(--field-icon)]" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)

View file

@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
<textarea
data-slot="textarea"
className={cn(
'flex field-sizing-content min-h-16 w-full rounded-xl border border-white/80 bg-white px-4 py-2 text-base text-black transition-colors outline-none placeholder:text-black/80 focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:bg-white/70 disabled:text-black/70 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm',
'flex field-sizing-content min-h-16 w-full rounded-xl border border-[var(--field-border)] bg-[var(--field-background)] px-4 py-2 text-base text-[var(--field-foreground)] transition-colors outline-none placeholder:text-[var(--field-placeholder)] focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:bg-[var(--field-disabled-background)] disabled:text-[var(--field-disabled-foreground)] disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm',
className,
)}
{...props}

View file

@ -43,6 +43,8 @@ export default buildConfig({
pool: {
connectionString: process.env.DATABASE_URL || '',
},
// Local dev can point at the shared production DB; never auto-push schema changes.
push: false,
}),
sharp,
plugins: [

View file

@ -0,0 +1,55 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
describe('petition page layout', () => {
const pageSource = readFileSync(
resolve(process.cwd(), 'src/app/(frontend)/abaixo-assinado/[id]/page.tsx'),
'utf8',
)
const formSource = readFileSync(resolve(process.cwd(), 'src/components/PetitionForm.tsx'), 'utf8')
const stylesSource = readFileSync(
resolve(process.cwd(), 'src/app/(frontend)/styles.css'),
'utf8',
)
const inputSource = readFileSync(resolve(process.cwd(), 'src/components/ui/input.tsx'), 'utf8')
const textareaSource = readFileSync(
resolve(process.cwd(), 'src/components/ui/textarea.tsx'),
'utf8',
)
const nativeSelectSource = readFileSync(
resolve(process.cwd(), 'src/components/ui/native-select.tsx'),
'utf8',
)
it('uses the main element as the vertical scroll container', () => {
const mainClass = pageSource.match(/<main[\s\S]*?className="([^"]+)"/)?.[1]
expect(mainClass?.split(/\s+/)).toEqual(
expect.arrayContaining(['h-screen', 'w-screen', 'overflow-y-auto']),
)
})
it('scopes the petition color system to the petition page', () => {
expect(pageSource).toContain('data-theme="petition"')
expect(stylesSource).toContain('[data-theme="petition"]')
expect(stylesSource).toContain('--petition-hero')
expect(stylesSource).toContain('--petition-form-section')
})
it('keeps petition form copy and consent HTML data-driven', () => {
expect(formSource).toContain('petition.form.subtitle')
expect(formSource).not.toContain('manter a orla de Salvador')
expect(pageSource).toContain('consentHTML')
expect(formSource).not.toContain("convertLexicalToHTML")
})
it('uses semantic field color variables for shared form controls', () => {
for (const source of [inputSource, textareaSource, nativeSelectSource]) {
expect(source).toContain('--field-background')
expect(source).toContain('--field-foreground')
expect(source).toContain('--field-border')
}
})
})