04첫 방문 모서리 카드
오른쪽 아래 카드가 4도 기운 채로 들어와 제 각도를 찾고, 목적별 설명 세 줄이 카드보다 한 박자 늦게 떠오릅니다.
쓰임GDPR 고지가 긴 사이트의 첫 방문 안내, 지역별 개인정보 안내 카드
rotate 4deg→00.1s 시차radius 16

<main class="stage">
<div class="fv">
<div class="fv__top" aria-hidden="true">
<i class="fv__dot"></i><i class="fv__dot"></i><i class="fv__dot"></i>
</div>
<div class="fv__lines" aria-hidden="true">
<i class="fv__line fv__line--title"></i>
<i class="fv__line"></i>
<i class="fv__line"></i>
<i class="fv__line fv__line--short"></i>
</div>
<div class="fv__card is-demo" role="dialog" aria-label="쿠키 동의">
<div class="fv__head">
<span class="fv__shield" aria-hidden="true"><svg viewBox="0 0 256 256" width="16" height="16"><g fill="currentColor"><path d="M216 56v56c0 96-88 120-88 120s-88-24-88-120V56a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8" opacity=".2"/><path d="M208 40H48a16 16 0 0 0-16 16v56c0 52.72 25.52 84.67 46.93 102.19c23.06 18.86 46 25.26 47 25.53a8 8 0 0 0 4.2 0c1-.27 23.91-6.67 47-25.53C198.48 196.67 224 164.72 224 112V56a16 16 0 0 0-16-16m0 72c0 37.07-13.66 67.16-40.6 89.42a129.3 129.3 0 0 1-39.4 22.2a128.3 128.3 0 0 1-38.92-21.81C61.82 179.51 48 149.3 48 112V56h160ZM82.34 141.66a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 11.32l-56 56a8 8 0 0 1-11.32 0Z"/></g></svg></span>
<b class="fv__title">개인정보와 쿠키</b>
</div>
<p class="fv__desc">아래 목적으로 쿠키를 씁니다.</p>
<ul class="fv__list">
<li class="fv__row fv__row--a">
<span class="fv__ic" aria-hidden="true"><svg viewBox="0 0 256 256" width="12" height="12"><g fill="currentColor"><path d="m230.1 108.76l-31.85-18.14c-.64-1.16-1.31-2.29-2-3.41l-.12-36A104.6 104.6 0 0 0 162 32l-32 17.89h-4L94 32a104.6 104.6 0 0 0-34.11 19.25l-.16 36c-.7 1.12-1.37 2.26-2 3.41l-31.84 18.1a99.2 99.2 0 0 0 0 38.46l31.85 18.14c.64 1.16 1.31 2.29 2 3.41l.12 36A104.6 104.6 0 0 0 94 224l32-17.87h4L162 224a104.6 104.6 0 0 0 34.08-19.25l.16-36c.7-1.12 1.37-2.26 2-3.41l31.84-18.1a99.2 99.2 0 0 0 .02-38.48M128 168a40 40 0 1 1 40-40a40 40 0 0 1-40 40" opacity=".2"/><path d="M128 80a48 48 0 1 0 48 48a48.05 48.05 0 0 0-48-48m0 80a32 32 0 1 1 32-32a32 32 0 0 1-32 32m109.94-52.79a8 8 0 0 0-3.89-5.4l-29.83-17l-.12-33.62a8 8 0 0 0-2.83-6.08a111.9 111.9 0 0 0-36.72-20.67a8 8 0 0 0-6.46.59L128 41.85L97.88 25a8 8 0 0 0-6.47-.6a111.9 111.9 0 0 0-36.68 20.75a8 8 0 0 0-2.83 6.07l-.15 33.65l-29.83 17a8 8 0 0 0-3.89 5.4a106.5 106.5 0 0 0 0 41.56a8 8 0 0 0 3.89 5.4l29.83 17l.12 33.63a8 8 0 0 0 2.83 6.08a111.9 111.9 0 0 0 36.72 20.67a8 8 0 0 0 6.46-.59L128 214.15L158.12 231a7.9 7.9 0 0 0 3.9 1a8.1 8.1 0 0 0 2.57-.42a112.1 112.1 0 0 0 36.68-20.73a8 8 0 0 0 2.83-6.07l.15-33.65l29.83-17a8 8 0 0 0 3.89-5.4a106.5 106.5 0 0 0-.03-41.52m-15 34.91l-28.57 16.25a8 8 0 0 0-3 3c-.58 1-1.19 2.06-1.81 3.06a7.94 7.94 0 0 0-1.22 4.21l-.15 32.25a95.9 95.9 0 0 1-25.37 14.3L134 199.13a8 8 0 0 0-3.91-1h-3.83a8.1 8.1 0 0 0-4.1 1l-28.84 16.1A96 96 0 0 1 67.88 201l-.11-32.2a8 8 0 0 0-1.22-4.22c-.62-1-1.23-2-1.8-3.06a8.1 8.1 0 0 0-3-3.06l-28.6-16.29a90.5 90.5 0 0 1 0-28.26l28.52-16.28a8 8 0 0 0 3-3c.58-1 1.19-2.06 1.81-3.06a7.94 7.94 0 0 0 1.22-4.21l.15-32.25a95.9 95.9 0 0 1 25.37-14.3L122 56.87a8 8 0 0 0 4.1 1h3.64a8 8 0 0 0 4.1-1l28.84-16.1A96 96 0 0 1 188.12 55l.11 32.2a8 8 0 0 0 1.22 4.22c.62 1 1.23 2 1.8 3.06a8.1 8.1 0 0 0 3 3.06l28.6 16.29a90.5 90.5 0 0 1 .05 28.29Z"/></g></svg></span>
<span class="fv__label">사이트 기능 유지</span>
</li>
<li class="fv__row fv__row--b">
<span class="fv__ic" aria-hidden="true"><svg viewBox="0 0 256 256" width="12" height="12"><g fill="currentColor"><path d="M208 40v168h-56V40Z" opacity=".2"/><path d="M224 200h-8V40a8 8 0 0 0-8-8h-56a8 8 0 0 0-8 8v40H96a8 8 0 0 0-8 8v40H48a8 8 0 0 0-8 8v64h-8a8 8 0 0 0 0 16h192a8 8 0 0 0 0-16M160 48h40v152h-40Zm-56 48h40v104h-40Zm-48 48h32v56H56Z"/></g></svg></span>
<span class="fv__label">방문 통계</span>
</li>
<li class="fv__row fv__row--c">
<span class="fv__ic" aria-hidden="true"><svg viewBox="0 0 256 256" width="12" height="12"><g fill="currentColor"><path d="M240 120a40 40 0 0 1-40 40h-40V80h40a40 40 0 0 1 40 40" opacity=".2"/><path d="M248 120a48.05 48.05 0 0 0-48-48h-39.8c-2.91-.17-53.62-3.74-101.91-44.24A16 16 0 0 0 32 40v160a16 16 0 0 0 26.29 12.25c37.77-31.68 77-40.76 93.71-43.3v31.72a16 16 0 0 0 7.12 13.33l11 7.33A16 16 0 0 0 194.5 212l11.77-44.36A48.07 48.07 0 0 0 248 120M48 199.93V40c42.81 35.91 86.63 45 104 47.24v65.48c-17.35 2.28-61.16 11.35-104 47.21m131 8v.11l-11-7.33V168h21.6ZM200 152h-32V88h32a32 32 0 1 1 0 64"/></g></svg></span>
<span class="fv__label">맞춤 광고</span>
</li>
</ul>
<ul class="fv__prefs" hidden>
<li><label class="fv__row"><input class="fv__check" type="checkbox" checked disabled><span class="fv__label">사이트 기능 유지</span></label></li>
<li><label class="fv__row"><input class="fv__check" type="checkbox" checked><span class="fv__label">방문 통계</span></label></li>
<li><label class="fv__row"><input class="fv__check" type="checkbox"><span class="fv__label">맞춤 광고</span></label></li>
</ul>
<div class="fv__btns">
<button class="fv__btn fv__more" type="button" aria-expanded="false">설정</button>
<button class="fv__btn fv__btn--primary fv__accept" type="button">동의합니다</button>
</div>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-paper);
[hidden] { display: none !important; }
// ── 가짜 페이지 목업 (움직이지 않는다) ──
.fv {
position: relative;
box-sizing: border-box;
width: clamp(272px, 90vw, 420px);
height: clamp(170px, 74vh, 236px); // 카드(en·ja 는 설명이 두 줄) + 아래 여백 24px 이 다 들어가는 높이
overflow: hidden; // 카드가 들어올 때의 회전·이동분을 목업 안에서 자른다
border: 1px solid rgba(23, 20, 26, .10);
border-radius: $r-card;
background: $stage-paper;
box-shadow: 0 2px 8px rgba(23, 20, 26, .10);
color: $subject-ink;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.fv__top {
display: flex;
align-items: center;
gap: $sp-1;
height: 24px;
padding: 0 $sp-3;
background: rgba(23, 20, 26, .06);
}
.fv__dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(23, 20, 26, .20); }
.fv__lines { display: grid; gap: $sp-3; padding: $sp-4; }
.fv__line { height: 8px; border-radius: $r-xs; background: rgba(23, 20, 26, .08); }
.fv__line--title { height: 12px; width: 56%; background: rgba(23, 20, 26, .14); }
.fv__line--short { width: 62%; }
// ── 오른쪽 아래 세로 카드 ──
.fv__card {
position: absolute;
right: $sp-4;
bottom: $sp-6;
box-sizing: border-box;
width: clamp(148px, 42vw, 196px);
display: grid;
gap: $sp-2;
padding: $sp-3;
border: 1px solid rgba(23, 20, 26, .10);
border-radius: $r-card;
background: $stage-paper;
box-shadow: 0 2px 8px rgba(23, 20, 26, .10);
transform-origin: 100% 100%; // 오른쪽 아래를 축으로 — 회전분이 목업 안쪽으로 자란다
transition: opacity $dur-base $easing, transform $dur-base $easing;
}
.fv__card.is-gone {
opacity: 0;
transform: translateY(16px) rotate(-4deg) scale(.96);
pointer-events: none;
}
.fv__head { display: flex; align-items: center; gap: $sp-2; }
.fv__shield { display: inline-flex; color: $color; }
.fv__title { font-size: clamp(11px, 3.2vw, 13px); font-weight: 700; letter-spacing: -.01em; }
.fv__desc {
margin: 0;
font-size: clamp(9px, 2.6vw, 11px);
line-height: 1.4;
color: rgba($stage-ink, .72);
}
.fv__list, .fv__prefs { list-style: none; margin: 0; padding: 0; display: grid; gap: $sp-1; }
.fv__row {
display: flex;
align-items: center;
gap: $sp-2;
min-width: 0;
font-size: clamp(9px, 2.6vw, 11px);
font-weight: 600;
color: rgba(23, 20, 26, .72);
}
.fv__ic { display: inline-flex; flex: 0 0 auto; color: $color; }
.fv__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fv__check { width: 12px; height: 12px; margin: 0; flex: 0 0 auto; accent-color: $color; }
.fv__btns { display: grid; grid-template-columns: auto 1fr; gap: $sp-2; }
.fv__btn {
min-width: 0;
border: 1px solid rgba(23, 20, 26, .20);
border-radius: $r-control;
padding: $sp-2 $sp-3;
background: $stage-paper;
color: rgba(23, 20, 26, .72);
font-family: inherit;
font-size: clamp(9px, 2.6vw, 11px);
font-weight: 700;
white-space: nowrap;
cursor: pointer;
transition: background $dur-quick $ease-out, transform $dur-instant $ease-out;
}
.fv__btn:hover, .fv__btn:focus-visible { background: rgba(23, 20, 26, .06); }
.fv__btn:active { transform: scale(.97); }
.fv__btn--primary {
border-color: $color;
background: $color;
color: $stage-paper;
}
.fv__btn--primary:hover, .fv__btn--primary:focus-visible { background: $color; filter: brightness(.94); }
// ── 자동 루프: 들어왔다 머물다 같은 경로로 나간다 (0% = 100%) ──
.fv__card.is-demo { animation: fv-card $duration $easing infinite; }
@keyframes fv-card {
0%, 100% { opacity: 0; transform: translateY(16px) rotate(-4deg) scale(.96); }
18%, 78% { opacity: 1; transform: translateY(0) rotate(0deg) scale(1); }
}
// 목적 3줄은 카드보다 한 박자 늦게 뜬다
.fv__card.is-demo .fv__row { animation: fv-row $duration $easing infinite; }
.fv__card.is-demo .fv__row--a { animation-delay: .10s; }
.fv__card.is-demo .fv__row--b { animation-delay: .16s; }
.fv__card.is-demo .fv__row--c { animation-delay: .22s; }
@keyframes fv-row {
0%, 100% { opacity: 0; transform: translateY(8px); }
16%, 74% { opacity: 1; transform: translateY(0); }
}
@media (max-width: 340px) {
.fv__card { bottom: $sp-5; gap: $sp-1; padding: $sp-2; }
.fv__lines { gap: $sp-2; padding: $sp-3; }
.fv__btn { padding: $sp-1 $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.fv__card.is-demo, .fv__card.is-demo .fv__row { animation: none !important; }
.fv__card.is-demo { opacity: 1; transform: none; } // 들어와 있는 결과 상태로 둔다
.fv__card.is-demo .fv__row { opacity: 1; transform: none; }
.fv__card, .fv__btn { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
[hidden] {
display: none !important;
}
.fv {
position: relative;
box-sizing: border-box;
width: clamp(272px, 90vw, 420px);
height: clamp(170px, 74vh, 236px);
overflow: hidden;
border: 1px solid rgba(23, 20, 26, 0.1);
border-radius: 16px;
background: #ffffff;
box-shadow: 0 2px 8px rgba(23, 20, 26, 0.1);
color: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.fv__top {
display: flex;
align-items: center;
gap: 4px;
height: 24px;
padding: 0 12px;
background: rgba(23, 20, 26, 0.06);
}
.fv__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(23, 20, 26, 0.2);
}
.fv__lines {
display: grid;
gap: 12px;
padding: 16px;
}
.fv__line {
height: 8px;
border-radius: 4px;
background: rgba(23, 20, 26, 0.08);
}
.fv__line--title {
height: 12px;
width: 56%;
background: rgba(23, 20, 26, 0.14);
}
.fv__line--short {
width: 62%;
}
.fv__card {
position: absolute;
right: 16px;
bottom: 24px;
box-sizing: border-box;
width: clamp(148px, 42vw, 196px);
display: grid;
gap: 8px;
padding: 12px;
border: 1px solid rgba(23, 20, 26, 0.1);
border-radius: 16px;
background: #ffffff;
box-shadow: 0 2px 8px rgba(23, 20, 26, 0.1);
transform-origin: 100% 100%;
transition: opacity 300ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.fv__card.is-gone {
opacity: 0;
transform: translateY(16px) rotate(-4deg) scale(0.96);
pointer-events: none;
}
.fv__head {
display: flex;
align-items: center;
gap: 8px;
}
.fv__shield {
display: inline-flex;
color: #2f6df6;
}
.fv__title {
font-size: clamp(11px, 3.2vw, 13px);
font-weight: 700;
letter-spacing: -0.01em;
}
.fv__desc {
margin: 0;
font-size: clamp(9px, 2.6vw, 11px);
line-height: 1.4;
color: rgba(23, 20, 26, 0.72);
}
.fv__list, .fv__prefs {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 4px;
}
.fv__row {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
font-size: clamp(9px, 2.6vw, 11px);
font-weight: 600;
color: rgba(23, 20, 26, 0.72);
}
.fv__ic {
display: inline-flex;
flex: 0 0 auto;
color: #2f6df6;
}
.fv__label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fv__check {
width: 12px;
height: 12px;
margin: 0;
flex: 0 0 auto;
accent-color: #2f6df6;
}
.fv__btns {
display: grid;
grid-template-columns: auto 1fr;
gap: 8px;
}
.fv__btn {
min-width: 0;
border: 1px solid rgba(23, 20, 26, 0.2);
border-radius: 12px;
padding: 8px 12px;
background: #ffffff;
color: rgba(23, 20, 26, 0.72);
font-family: inherit;
font-size: clamp(9px, 2.6vw, 11px);
font-weight: 700;
white-space: nowrap;
cursor: pointer;
transition: background 220ms ease-out, transform 140ms ease-out;
}
.fv__btn:hover, .fv__btn:focus-visible {
background: rgba(23, 20, 26, 0.06);
}
.fv__btn:active {
transform: scale(0.97);
}
.fv__btn--primary {
border-color: #2f6df6;
background: #2f6df6;
color: #ffffff;
}
.fv__btn--primary:hover, .fv__btn--primary:focus-visible {
background: #2f6df6;
filter: brightness(0.94);
}
.fv__card.is-demo {
animation: fv-card 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes fv-card {
0%, 100% {
opacity: 0;
transform: translateY(16px) rotate(-4deg) scale(0.96);
}
18%, 78% {
opacity: 1;
transform: translateY(0) rotate(0deg) scale(1);
}
}
.fv__card.is-demo .fv__row {
animation: fv-row 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.fv__card.is-demo .fv__row--a {
animation-delay: 0.1s;
}
.fv__card.is-demo .fv__row--b {
animation-delay: 0.16s;
}
.fv__card.is-demo .fv__row--c {
animation-delay: 0.22s;
}
@keyframes fv-row {
0%, 100% {
opacity: 0;
transform: translateY(8px);
}
16%, 74% {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 340px) {
.fv__card {
bottom: 20px;
gap: 4px;
padding: 8px;
}
.fv__lines {
gap: 8px;
padding: 12px;
}
.fv__btn {
padding: 4px 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.fv__card.is-demo, .fv__card.is-demo .fv__row {
animation: none !important;
}
.fv__card.is-demo {
opacity: 1;
transform: none;
}
.fv__card.is-demo .fv__row {
opacity: 1;
transform: none;
}
.fv__card, .fv__btn {
transition: none;
}
}// 실제 동작: 동의하면 카드가 같은 경로로 빠져나가고, 설정은 같은 카드 안에서 목적 줄을 체크박스 줄로 바꾼다
var card = document.querySelector('.fv__card');
var list = document.querySelector('.fv__list');
var prefs = document.querySelector('.fv__prefs');
var more = document.querySelector('.fv__more');
function save(v) {
try { localStorage.setItem('gd-consent-04', v); } catch (e) {}
}
function stop() {
card.classList.remove('is-demo');
}
document.querySelector('.fv__accept').addEventListener('click', function () {
stop();
card.classList.add('is-gone');
save(prefs.hidden ? 'all' : 'custom');
});
more.addEventListener('click', function () {
stop();
var open = prefs.hidden;
prefs.hidden = !open;
list.hidden = open;
more.setAttribute('aria-expanded', open ? 'true' : 'false');
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./FirstVisitCornerCard.module.scss";
export type FirstVisitCornerCardProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
};
function ShieldIcon() {
return (
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true">
<g fill="currentColor">
<path d="M216 56v56c0 96-88 120-88 120s-88-24-88-120V56a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8" opacity=".2" />
<path d="M208 40H48a16 16 0 0 0-16 16v56c0 52.72 25.52 84.67 46.93 102.19c23.06 18.86 46 25.26 47 25.53a8 8 0 0 0 4.2 0c1-.27 23.91-6.67 47-25.53C198.48 196.67 224 164.72 224 112V56a16 16 0 0 0-16-16m0 72c0 37.07-13.66 67.16-40.6 89.42a129.3 129.3 0 0 1-39.4 22.2a128.3 128.3 0 0 1-38.92-21.81C61.82 179.51 48 149.3 48 112V56h160ZM82.34 141.66a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 11.32l-56 56a8 8 0 0 1-11.32 0Z" />
</g>
</svg>
);
}
function GearIcon() {
return (
<svg viewBox="0 0 256 256" width="12" height="12" aria-hidden="true">
<g fill="currentColor">
<path d="m230.1 108.76l-31.85-18.14c-.64-1.16-1.31-2.29-2-3.41l-.12-36A104.6 104.6 0 0 0 162 32l-32 17.89h-4L94 32a104.6 104.6 0 0 0-34.11 19.25l-.16 36c-.7 1.12-1.37 2.26-2 3.41l-31.84 18.1a99.2 99.2 0 0 0 0 38.46l31.85 18.14c.64 1.16 1.31 2.29 2 3.41l.12 36A104.6 104.6 0 0 0 94 224l32-17.87h4L162 224a104.6 104.6 0 0 0 34.08-19.25l.16-36c.7-1.12 1.37-2.26 2-3.41l31.84-18.1a99.2 99.2 0 0 0 .02-38.48M128 168a40 40 0 1 1 40-40a40 40 0 0 1-40 40" opacity=".2" />
<path d="M128 80a48 48 0 1 0 48 48a48.05 48.05 0 0 0-48-48m0 80a32 32 0 1 1 32-32a32 32 0 0 1-32 32m109.94-52.79a8 8 0 0 0-3.89-5.4l-29.83-17l-.12-33.62a8 8 0 0 0-2.83-6.08a111.9 111.9 0 0 0-36.72-20.67a8 8 0 0 0-6.46.59L128 41.85L97.88 25a8 8 0 0 0-6.47-.6a111.9 111.9 0 0 0-36.68 20.75a8 8 0 0 0-2.83 6.07l-.15 33.65l-29.83 17a8 8 0 0 0-3.89 5.4a106.5 106.5 0 0 0 0 41.56a8 8 0 0 0 3.89 5.4l29.83 17l.12 33.63a8 8 0 0 0 2.83 6.08a111.9 111.9 0 0 0 36.72 20.67a8 8 0 0 0 6.46-.59L128 214.15L158.12 231a7.9 7.9 0 0 0 3.9 1a8.1 8.1 0 0 0 2.57-.42a112.1 112.1 0 0 0 36.68-20.73a8 8 0 0 0 2.83-6.07l.15-33.65l29.83-17a8 8 0 0 0 3.89-5.4a106.5 106.5 0 0 0-.03-41.52m-15 34.91l-28.57 16.25a8 8 0 0 0-3 3c-.58 1-1.19 2.06-1.81 3.06a7.94 7.94 0 0 0-1.22 4.21l-.15 32.25a95.9 95.9 0 0 1-25.37 14.3L134 199.13a8 8 0 0 0-3.91-1h-3.83a8.1 8.1 0 0 0-4.1 1l-28.84 16.1A96 96 0 0 1 67.88 201l-.11-32.2a8 8 0 0 0-1.22-4.22c-.62-1-1.23-2-1.8-3.06a8.1 8.1 0 0 0-3-3.06l-28.6-16.29a90.5 90.5 0 0 1 0-28.26l28.52-16.28a8 8 0 0 0 3-3c.58-1 1.19-2.06 1.81-3.06a7.94 7.94 0 0 0 1.22-4.21l.15-32.25a95.9 95.9 0 0 1 25.37-14.3L122 56.87a8 8 0 0 0 4.1 1h3.64a8 8 0 0 0 4.1-1l28.84-16.1A96 96 0 0 1 188.12 55l.11 32.2a8 8 0 0 0 1.22 4.22c.62 1 1.23 2 1.8 3.06a8.1 8.1 0 0 0 3 3.06l28.6 16.29a90.5 90.5 0 0 1 .05 28.29Z" />
</g>
</svg>
);
}
function ChartIcon() {
return (
<svg viewBox="0 0 256 256" width="12" height="12" aria-hidden="true">
<g fill="currentColor">
<path d="M208 40v168h-56V40Z" opacity=".2" />
<path d="M224 200h-8V40a8 8 0 0 0-8-8h-56a8 8 0 0 0-8 8v40H96a8 8 0 0 0-8 8v40H48a8 8 0 0 0-8 8v64h-8a8 8 0 0 0 0 16h192a8 8 0 0 0 0-16M160 48h40v152h-40Zm-56 48h40v104h-40Zm-48 48h32v56H56Z" />
</g>
</svg>
);
}
function MegaphoneIcon() {
return (
<svg viewBox="0 0 256 256" width="12" height="12" aria-hidden="true">
<g fill="currentColor">
<path d="M240 120a40 40 0 0 1-40 40h-40V80h40a40 40 0 0 1 40 40" opacity=".2" />
<path d="M248 120a48.05 48.05 0 0 0-48-48h-39.8c-2.91-.17-53.62-3.74-101.91-44.24A16 16 0 0 0 32 40v160a16 16 0 0 0 26.29 12.25c37.77-31.68 77-40.76 93.71-43.3v31.72a16 16 0 0 0 7.12 13.33l11 7.33A16 16 0 0 0 194.5 212l11.77-44.36A48.07 48.07 0 0 0 248 120M48 199.93V40c42.81 35.91 86.63 45 104 47.24v65.48c-17.35 2.28-61.16 11.35-104 47.21m131 8v.11l-11-7.33V168h21.6ZM200 152h-32V88h32a32 32 0 1 1 0 64" />
</g>
</svg>
);
}
const LABELS = ["Site functionality", "Visit analytics", "Personalized ads"];
function save(value: string): void {
try {
localStorage.setItem("gd-consent-04", value);
} catch (e) {
/* 샌드박스에서 던질 수 있다 */
}
}
export default function FirstVisitCornerCard({
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: FirstVisitCornerCardProps) {
const [live, setLive] = useState(true);
const [gone, setGone] = useState(false);
const [open, setOpen] = useState(false);
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
const rowClass = [styles.rowA, styles.rowB, styles.rowC];
const icons = [<GearIcon key="a" />, <ChartIcon key="b" />, <MegaphoneIcon key="c" />];
return (
<div className={styles.fv} style={vars}>
<div className={styles.top} aria-hidden="true">
<i className={styles.dot} />
<i className={styles.dot} />
<i className={styles.dot} />
</div>
<div className={styles.lines} aria-hidden="true">
<i className={`${styles.line} ${styles.lineTitle}`} />
<i className={styles.line} />
<i className={styles.line} />
<i className={`${styles.line} ${styles.lineShort}`} />
</div>
<div
className={`${styles.card} ${live ? styles.demo : ""} ${gone ? styles.gone : ""}`}
role="dialog"
aria-label="Cookie consent"
>
<div className={styles.head}>
<span className={styles.shield}>
<ShieldIcon />
</span>
<b className={styles.title}>Privacy and cookies</b>
</div>
<p className={styles.desc}>We use cookies for the purposes below.</p>
{open ? (
<ul className={styles.prefs}>
{LABELS.map((label, i) => (
<li key={label}>
<label className={styles.row}>
<input
className={styles.check}
type="checkbox"
defaultChecked={i < 2}
disabled={i === 0}
/>
<span className={styles.label}>{label}</span>
</label>
</li>
))}
</ul>
) : (
<ul className={styles.list}>
{LABELS.map((label, i) => (
<li className={`${styles.row} ${rowClass[i]}`} key={label}>
<span className={styles.ic}>{icons[i]}</span>
<span className={styles.label}>{label}</span>
</li>
))}
</ul>
)}
<div className={styles.btns}>
<button
className={styles.btn}
type="button"
aria-expanded={open}
onClick={() => {
setLive(false);
setOpen(!open);
}}
>
Options
</button>
<button
className={`${styles.btn} ${styles.btnPrimary}`}
type="button"
onClick={() => {
setLive(false);
setGone(true);
save(open ? "custom" : "all");
}}
>
Accept
</button>
</div>
</div>
</div>
);
}04 첫 방문 모서리 카드 — 측정 누적면적 12.553% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 쿠키 동의 배너 UI 9종 — 그대로 붙이는 동의 관리 코드
이 데모가 속한 글
쿠키 동의 배너 UI 9종 — 그대로 붙이는 동의 관리 코드쿠키 동의 배너는 사이트가 쿠키를 쓴다는 사실을 알리고 방문자의 선택을 받아 두는 UI입니다. 여기 아홉 개는 배너·패널·모달·토스트를 모두 실제로 눌러 볼 수 있는 상태로 만든 것입니다. 그중 하나는 끝까지 읽기 이 글의 데모 전부 보기


