// ── Design 5 styles, as a CSS string ──────────────────────────── // // Single export — the SDK's stylesheet, exported as a plain string so it can // be injected into either the document (React in WC Blocks) or a shadowRoot's // adoptedStyleSheets (web-component layer). All class names are prefixed // `wcp-pf__` to avoid collision with host theme styles when used outside Shadow DOM. // // Mirrors the look of `design-previews/05-card-only-minimal.html` (post-feedback // version with inline GPay button + gpay_ready state). export const PAYMENT_FIELDS_CSS = ` .wcp-pf { --wcp-green: #59B210; --wcp-clover-green: #228800; --wcp-warm: #c4621a; --wcp-error: #d64545; --wcp-focus: #4a7c6a; --wcp-border: #d6d8db; --wcp-border-soft: #e5e7eb; --wcp-bg: #ffffff; --wcp-text: #1a1d21; --wcp-text-muted: #6b7280; --wcp-text-soft: #8a9199; font-family: 'DM Sans', system-ui, -apple-system, sans-serif; color: var(--wcp-text); line-height: 1.5; -webkit-font-smoothing: antialiased; max-width: 480px; margin: 0 auto; } .wcp-pf, .wcp-pf * { box-sizing: border-box; } /* ── Status strip (top of widget when state !== idle) ── */ .wcp-pf__strip { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-radius: 8px; font-size: 13px; font-weight: 500; margin-bottom: 16px; animation: wcp-pf-strip-in 240ms cubic-bezier(.2,.7,.2,1); } .wcp-pf__strip svg { flex: none; } .wcp-pf__strip--info { background: #eef4f1; color: var(--wcp-focus); border: 1px solid rgba(74,124,106,0.18); } .wcp-pf__strip--warning { background: #fdf5ed; color: var(--wcp-warm); border: 1px solid rgba(196,98,26,0.22); } .wcp-pf__strip--success { background: #f1f8e8; color: var(--wcp-clover-green); border: 1px solid rgba(34,136,0,0.22); } .wcp-pf__strip--error { background: #fbeded; color: var(--wcp-error); border: 1px solid rgba(214,69,69,0.22); } .wcp-pf__spinner { width: 14px; height: 14px; border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%; animation: wcp-pf-spin 700ms linear infinite; flex: none; opacity: 0.7; } /* ── Form stack ── - Idle: visible. - In-progress (tokenizing/submitting/3DS): dimmed so user still sees what they entered. - GPay ready: collapsed (max-height + opacity + margin → 0) so the GPay-ready card stands alone. 540px is a safe ceiling — accommodates cardholder name + email + the 4 card fields + per-field error text. */ .wcp-pf__form { max-height: 540px; overflow: hidden; transition: max-height 320ms cubic-bezier(.4,0,.2,1), opacity 220ms ease, margin 280ms ease, filter 240ms ease; } .wcp-pf__form--dimmed { opacity: 0.42; filter: saturate(0.7); pointer-events: none; } .wcp-pf__form--collapsed { max-height: 0; opacity: 0; margin-top: 0; margin-bottom: 0; pointer-events: none; } /* ── Field rows ── */ .wcp-pf__row { margin-bottom: 10px; } .wcp-pf__row:last-child { margin-bottom: 0; } .wcp-pf__grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; align-items: start; } .wcp-pf__grid-3 { display: grid; grid-template-columns: 1.45fr 1fr 1.1fr; gap: 10px; align-items: start; } /* Vertical wrapper: field (46px) + optional error text underneath. */ .wcp-pf__field-slot { display: flex; flex-direction: column; min-width: 0; } /* The host div for Clover's iframe. * * Layout strategy: the wrapper has a fixed visual height (46px) for border * + focus ring + theme resistance, BUT it uses flex centering so the iframe * is positioned by Clover's own auto-resize, not stretched to fill the wrapper. * * Clover's SDK does this internally (see sdk.js updateFrameStyles): the * iframe document measures its rendered content and posts {style, width, * height} back to the parent, which Clover then applies to the iframe element * inline. If we force height:100% with !important on the iframe, we override * Clover's measurement and force the viewport to 46px even when the iframe * document needs slightly more (e.g., sub-pixel rounding, focus ring inside * the iframe, masked-input wrappers Clover adds around the input element). * That is what produces the per-field scrollbars. * * Instead: let Clover size the iframe to its natural content height (the * input is height:46px via theme.ts), center it within our 46px wrapper * via flex, and clip any sub-pixel overshoot with overflow:hidden. */ .wcp-pf__field { width: 100%; height: 46px; padding: 0; background: #fff; border: 1.5px solid var(--wcp-border); border-radius: 8px; overflow: hidden; transition: border-color 160ms ease, box-shadow 160ms ease; cursor: text; position: relative; display: flex; align-items: center; justify-content: stretch; } .wcp-pf__field:hover { border-color: #b9bcc1; } /* :focus-within bubbles focus across iframe boundaries in modern browsers, which is how we get a focus ring on the wrapper when the user clicks into the Clover-rendered input inside the cross-origin iframe. */ .wcp-pf__field:focus-within, .wcp-pf__field--focused { border-color: var(--wcp-focus); box-shadow: 0 0 0 3px rgba(74,124,106,0.08); } .wcp-pf__field--error { border-color: var(--wcp-error); box-shadow: 0 0 0 3px rgba(214,69,69,0.08); animation: wcp-pf-shake 360ms cubic-bezier(.36,.07,.19,.97); } /* Touched + no error → positive visual confirmation. The error state's counterpart so the user can see "yes, this is accepted" without needing to read the form. Border + ring use Clover's accent green. */ .wcp-pf__field--valid { border-color: var(--wcp-clover-green); box-shadow: 0 0 0 3px rgba(34,136,0,0.08); } .wcp-pf__field-check { position: absolute; top: 50%; right: 10px; transform: translateY(-50%); width: 18px; height: 18px; border-radius: 50%; background: var(--wcp-clover-green); color: #fff; display: inline-flex; align-items: center; justify-content: center; pointer-events: none; animation: wcp-pf-pop 240ms cubic-bezier(.2,.7,.2,1); } .wcp-pf__field-check svg { display: block; } /* No !important on height — let Clover's updateFrameStyles set the iframe height to the measured content size. Width is !important because Clover sets width:100% inline at creation and we don't want themes overriding. scrolling="no" is set programmatically in element-registry.ts after mount — this is the only fully-reliable way to suppress the cross-origin iframe overlay scrollbar (CSS overflow on iframes is browser-spotty). */ .wcp-pf__field iframe { width: 100% !important; border: 0 !important; display: block !important; background: transparent !important; flex: 0 0 auto; overflow: hidden; } .wcp-pf__error-text { font-size: 12px; color: var(--wcp-error); margin-top: 6px; display: flex; align-items: center; gap: 5px; } .wcp-pf__error-text svg { flex: none; } /* ── GPay block (Clover iframe lives in .wcp-pf__gpay-mount) ── * The "Or pay instantly" label provides its own horizontal rule via the * ::before / ::after pseudo-elements below. The block itself must NOT add * a second border-top — two stacked rules read as an extra empty line. */ .wcp-pf__gpay-block { margin-top: 18px; } .wcp-pf__gpay-label { display: flex; align-items: center; gap: 12px; color: var(--wcp-text-soft); font-size: 12px; margin-bottom: 12px; } .wcp-pf__gpay-label::before, .wcp-pf__gpay-label::after { content: ""; flex: 1; height: 1px; background: var(--wcp-border-soft); } /* The host div for Clover's PAYMENT_REQUEST_BUTTON iframe. Constrain it the same way the legacy PaymentFields.php template did (inline style at line 167: width:100%; max-width:200px; margin:0 auto; height:50px). Without this, the iframe expands to its container's full width. */ .wcp-pf__gpay-mount { width: 100%; max-width: 240px; height: 48px; margin: 0 auto; overflow: hidden; } .wcp-pf__gpay-mount iframe { width: 100% !important; height: 100% !important; min-height: 40px !important; min-width: 90px !important; border: 0 !important; display: block !important; } /* ── GPay-ready card (Variation 1: refined receipt-card) ── Single unified card; amount is the dominant element. Replaces the previous "ready strip + details card + greyed fields" triplet. Card fields collapse out via .wcp-pf__form--collapsed so this card stands alone. */ .wcp-pf__gpay-card { background: #ffffff; border: 1px solid var(--wcp-border-soft); border-radius: 12px; padding: 22px 22px 18px; margin-bottom: 10px; box-shadow: 0 1px 2px rgba(20, 20, 40, 0.04); animation: wcp-pf-rise 360ms cubic-bezier(.2,.7,.2,1); } .wcp-pf__gpay-card-head { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 18px; } .wcp-pf__gpay-card-check { width: 28px; height: 28px; border-radius: 50%; background: var(--wcp-clover-green); color: #fff; display: inline-flex; align-items: center; justify-content: center; flex: none; box-shadow: 0 0 0 4px #ecfdf5; } .wcp-pf__gpay-card-check svg { width: 14px; height: 14px; } .wcp-pf__gpay-card-head-text { flex: 1; min-width: 0; } .wcp-pf__gpay-card-title { font-size: 15px; font-weight: 600; color: var(--wcp-text); line-height: 1.3; } .wcp-pf__gpay-card-sub { font-size: 12.5px; color: var(--wcp-text-muted); margin-top: 2px; line-height: 1.4; } .wcp-pf__gpay-card-detail { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; padding-bottom: 14px; border-bottom: 1px dashed var(--wcp-border-soft); margin-bottom: 14px; } .wcp-pf__gpay-card-amount { font-size: 32px; font-weight: 600; color: var(--wcp-text); letter-spacing: -0.02em; line-height: 1; font-variant-numeric: tabular-nums; } .wcp-pf__gpay-card-info { font-size: 11px; color: var(--wcp-text-muted); text-transform: uppercase; letter-spacing: 0.06em; text-align: right; line-height: 1.5; } .wcp-pf__gpay-card-info-row { display: block; } .wcp-pf__gpay-card-info b { font-weight: 700; color: var(--wcp-text); } .wcp-pf__gpay-card-cta { font-size: 13px; color: var(--wcp-text-muted); margin: 0; line-height: 1.5; } .wcp-pf__gpay-card-cta b { color: var(--wcp-text); font-weight: 600; } .wcp-pf__gpay-card-switch { display: inline-block; margin-top: 8px; padding: 0; font: inherit; font-size: 12.5px; color: var(--wcp-text-muted); background: transparent; border: none; text-decoration: underline; text-decoration-color: var(--wcp-text-soft); text-underline-offset: 3px; cursor: pointer; transition: color 140ms ease; } .wcp-pf__gpay-card-switch:hover { color: var(--wcp-text); } .wcp-pf__gpay-card-switch:disabled { cursor: default; opacity: 0.55; } /* ── Trust footer (mirrors WP-classic CSS at lines 140-167) ── */ .wcp-pf__trust { margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--wcp-border-soft); display: flex; align-items: center; justify-content: center; } .wcp-pf__trust-lock { padding-right: 10px; flex: none; display: flex; } .wcp-pf__trust-lock img { display: block; width: 20px; height: 20px; } .wcp-pf__trust-display { display: flex; align-items: center; flex-wrap: wrap; justify-content: space-around; min-width: 180px; gap: 4px 0; } .wcp-pf__trust-text { white-space: nowrap; padding-right: 5px; font-size: 12.5px; color: #4a4a4a; font-weight: 500; } .wcp-pf__trust-logos { vertical-align: middle; margin: 0; display: block; height: auto; max-width: 100%; } /* ── Animations ── */ @keyframes wcp-pf-spin { to { transform: rotate(360deg); } } @keyframes wcp-pf-strip-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } } @keyframes wcp-pf-rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } } @keyframes wcp-pf-shake { 10%, 90% { transform: translate3d(-1px, 0, 0); } 20%, 80% { transform: translate3d(2px, 0, 0); } 30%, 50%, 70% { transform: translate3d(-3px, 0, 0); } 40%, 60% { transform: translate3d(3px, 0, 0); } } @keyframes wcp-pf-pop { from { opacity: 0; transform: translateY(-50%) scale(0.6); } to { opacity: 1; transform: translateY(-50%) scale(1); } } @media (prefers-reduced-motion: reduce) { .wcp-pf__strip, .wcp-pf__gpay-ready, .wcp-pf__field { animation: none; } .wcp-pf__spinner { animation-duration: 1.4s; } } `;