teqo-backup/src/lib/cardModels.ts
Francisco Solla 759b2d71cc feat(S14): cards personalizados — ajustes de fidelidade e experiência
A home abre o mesmo compositor do /cards ali mesmo (dialog no desktop,
drawer no mobile) e perde o CTA: os três modelos viram botões e nada
navega. A seção sobe para imediatamente antes da captura de novidades.
O nome no card e no tile alinha à esquerda com o SOU (x=213) via leftX
+ drawCardName extraído; o tile passa a desenhar SEU NOME com o próprio
pipeline real (canvas transparente sobre a base). Modal sem o vão
interno (p-0 sm:p-0) e ajuda legível no tema campaign-site
(--muted-foreground). Remove a variante link e o cardModelHref sem call
site. Unit atualizado (+ drawCardName) e e2e com alinhamento por pixel;
sem migration, sem PII em rede.
2026-09-12 20:43:22 -03:00

90 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* S13 — the three personalized-card models of the campaign site. Pure data so
* the home section, the `/cards` gallery and the canvas renderer share one
* catalog (no second editor, no CMS): each entry carries the master asset, the
* real output size in pixels and, for photo models, the transparent window the
* visitor's photo must cover.
*
* Geometry comes from the master files (pixel-measured, keep in sync with
* `docs/plans/cards-personalizados-campanha-impl.md`): the photo frames are
* 1000×1000 and 1000×1440 despite the `1080x` in the original file names.
*/
const CARD_MODEL_IDS = ['eu-sou-solla', 'perfil-quadrado', 'perfil-retangular'] as const
export type CardModelId = (typeof CARD_MODEL_IDS)[number]
type CardModelKind = 'name' | 'photo'
export type CardRect = {
x: number
y: number
width: number
height: number
}
export type CardModel = {
id: CardModelId
kind: CardModelKind
label: string
assetSrc: string
width: number
height: number
/** Photo models only: the transparent window the photo must cover. */
photoWindow?: CardRect
}
export const CARD_MODELS: readonly CardModel[] = [
{
id: 'eu-sou-solla',
kind: 'name',
label: 'Card com seu nome',
assetSrc: '/cards/name-card-base.jpg',
width: 1080,
height: 1440,
},
{
id: 'perfil-quadrado',
kind: 'photo',
label: 'Moldura quadrada',
assetSrc: '/cards/photo-square-frame.png',
width: 1000,
height: 1000,
photoWindow: { x: 0, y: 0, width: 1000, height: 740 },
},
{
id: 'perfil-retangular',
kind: 'photo',
label: 'Moldura vertical',
assetSrc: '/cards/photo-portrait-frame.png',
width: 1000,
height: 1440,
photoWindow: { x: 0, y: 0, width: 1000, height: 1044 },
},
]
/**
* Name model slot, pixel-measured from the filled master (`FULANO` example):
* the `#SOU` line and the visitor's name share the same left border at x=213
* (S14 — the name is left-aligned, not centered); the cap band tops at y=430
* with a 106px cap height and the master's ink spans x[214..925] (712px), so a
* fitted name never crosses 714px. The visitor's name is the only thing drawn
* over the base; the fixed lines already live in the art.
*/
export const NAME_CARD_SLOT = {
leftX: 213,
capTop: 430,
capHeight: 106,
maxInkWidth: 714,
maxBlockBottom: 630,
minCapHeight: 48,
fill: '#ffec01',
maxLines: 2,
} as const
export const BRAND_FALLBACK_FONT = 'sans-serif'
export const isCardModelId = (value: unknown): value is CardModelId =>
typeof value === 'string' && (CARD_MODEL_IDS as readonly string[]).includes(value)
export const getCardModel = (id: CardModelId): CardModel | undefined =>
CARD_MODELS.find((model) => model.id === id)