08빈 피드가 첫 활동으로 바뀌는 순간
빈 상태 안내는 steps(1, end) 컷으로 한 프레임에 물러나고 첫 활동 항목이 translateY 로 떠오르며, 그 자리를 aria-live 로 지정해 처음 생긴 활동이 낭독기에 전해집니다.
쓰임막 열은 서비스의 초기 화면, 아직 로그가 없는 새 대시보드
steps(1, end)translateYaria-live

<main class="stage">
<section class="ef is-demo" id="ef" aria-label="활동 피드">
<header class="ef__h">
<h2 class="ef__t">활동 피드</h2>
<span class="ef__phase" id="ef-phase" data-t1="활동 {n}건" data-t2="비어 있음" aria-live="polite">비어 있음</span>
</header>
<div class="ef__empty" id="ef-empty">
<span class="ef__ic" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor"><g><path d="M216 48v112h-36.69a8 8 0 0 0-5.66 2.34l-19.31 19.32a8 8 0 0 1-5.66 2.34h-41.37a8 8 0 0 1-5.66-2.34l-19.31-19.32a8 8 0 0 0-5.66-2.34H40V48a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8" opacity=".2"/><path d="M208 32H48a16 16 0 0 0-16 16v160a16 16 0 0 0 16 16h160a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16m0 16v104h-28.7a15.86 15.86 0 0 0-11.3 4.69L148.69 176h-41.38L88 156.68A15.9 15.9 0 0 0 76.69 152H48V48Zm0 160H48v-40h28.69L96 187.32a15.9 15.9 0 0 0 11.31 4.68h41.38a15.86 15.86 0 0 0 11.31-4.69L179.31 168H208z"/></g></svg></span>
<p class="ef__e1">아직 활동이 없습니다</p>
<p class="ef__e2">첫 활동이 생기면 여기에 쌓입니다</p>
</div>
<ul class="ef__list" role="log" aria-live="polite" id="ef-list">
<li class="ef__row ef__row--first" data-row="first">
<div class="ef__fold"><div class="ef__cell">
<span class="ef__av" style="--a:#93a4ca">김</span>
<p class="ef__line"><b>김하늘</b>이 팀에 들어왔습니다</p>
<time class="ef__tm">방금</time>
</div></div>
</li>
<li class="ef__row ef__row--second" data-row="second">
<div class="ef__fold"><div class="ef__cell">
<span class="ef__av" style="--a:#ff9ebb">이</span>
<p class="ef__line"><b>이준</b>이 시작 가이드를 열었습니다</p>
<time class="ef__tm">방금</time>
</div></div>
</li>
</ul>
<div class="ef__btns">
<button class="ef__b ef__b--main" type="button" id="ef-add">새 활동</button>
<button class="ef__b" type="button" id="ef-clear">비우기</button>
</div>
</section>
</main>
<template id="ef-tpl"><li class="ef__row is-in"><div class="ef__fold"><div class="ef__cell">
<span class="ef__av" style="--a:#90c8b5">박</span>
<p class="ef__line"><b>박서아</b>가 첫 문서를 만들었습니다</p>
<time class="ef__tm">방금</time>
</div></div></li></template>@use "sass:color";
@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-yellow);
$ink: $subject-ink;
$line: rgba(23, 20, 26, .22);
$soft: rgba(255, 255, 255, .55);
.ef {
box-sizing: border-box;
width: min(304px, 92vw);
padding: $sp-3;
border: 1px solid $ink;
border-radius: $r-card;
background: $stage-yellow;
color: $ink;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 12px;
line-height: 1.3;
}
.ef__h {
display: flex;
align-items: center;
gap: $sp-2;
margin: 0 0 $sp-2;
}
.ef__t { margin: 0; font-size: 12px; font-weight: 700; }
.ef__phase {
margin-left: auto;
padding: 0 $sp-2;
border-radius: $r-pill;
background: rgba(23, 20, 26, .1);
color: $ink;
font-size: 10px;
font-weight: 700;
line-height: 1.7;
}
// ── 빈 상태 ───────────────────────────────────────
.ef__empty {
display: grid;
justify-items: center;
gap: $sp-1;
padding: $sp-4 0 $sp-2;
}
.ef__ic {
display: inline-grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(255, 255, 255, .6);
color: $ink;
}
.ef__ic svg { width: 24px; height: 24px; }
.ef__e1 { margin: 0; font-size: 12px; font-weight: 700; }
.ef__e2 { margin: 0; color: rgba(23, 20, 26, .72); font-size: 10px; }
// 정적 경로 — 활동이 생기면 빈 상태는 사라진다
.ef.has-act .ef__empty { display: none; }
// ── 활동 줄 ───────────────────────────────────────
.ef__list {
display: grid;
gap: $sp-1;
margin: 0;
padding: 0;
list-style: none;
}
.ef__fold { display: grid; grid-template-rows: 1fr; }
.ef__row--first .ef__fold,
.ef__row--second .ef__fold { grid-template-rows: 0fr; }
.ef.has-act .ef__row--first .ef__fold { grid-template-rows: 1fr; } // 첫 활동 자리
.ef__cell {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: $sp-2;
min-height: 0;
overflow: hidden;
padding: $sp-1 $sp-2;
border-radius: $r-sm;
background: #ffffff;
color: $ink;
}
.ef__av {
display: inline-grid;
place-items: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--a);
color: $ink;
font-size: 11px;
font-weight: 700;
}
.ef__line { margin: 0; }
.ef__line b { font-weight: 700; }
.ef__tm { color: rgba(23, 20, 26, .72); font-size: 10px; }
// 사람이 눌러 넣은 줄: 아래에서 떠오른다 (is-in 동안만)
.ef__row.is-in .ef__cell { animation: ef-rise $dur-base $ease-pop both; }
@keyframes ef-rise {
from { transform: translateY(8px); }
to { transform: translateY(0); }
}
.ef__btns {
display: flex;
gap: $sp-1;
margin: $sp-2 0 0;
}
.ef__b {
padding: $sp-1 $sp-3;
border: 1px solid $ink;
border-radius: $r-pill;
background: transparent;
color: $ink;
font-family: inherit;
font-size: 12px;
font-weight: 700;
line-height: 1.6;
cursor: pointer;
}
.ef__b:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
.ef__b:active { transform: scale(.94); }
.ef__b--main { background: $ink; color: $stage-yellow; }
// ── 격자 미리보기 전용 2초 루프 ─────────────────────
// 빈 상태 → 첫 활동: 상태 교체는 steps 컷, 등장은 fold + translateY
@keyframes ef-empty-out {
0%, 17% { visibility: visible; }
18%, 86% { visibility: hidden; }
87%, 100% { visibility: visible; }
}
@keyframes ef-first-in {
0%, 17% { grid-template-rows: 0fr; }
18% { grid-template-rows: 1fr; }
18.1%, 86% { grid-template-rows: 1fr; }
87%, 100% { grid-template-rows: 0fr; }
}
@keyframes ef-first-rise {
0%, 18% { transform: translateY(8px); }
25%, 86% { transform: translateY(0); }
87%, 100% { transform: translateY(8px); }
}
@keyframes ef-av-pop {
0%, 18% { transform: scale(.4); }
24%, 86% { transform: scale(1); }
87%, 100% { transform: scale(.4); }
}
@keyframes ef-second-in {
0%, 54% { grid-template-rows: 0fr; }
55% { grid-template-rows: 1fr; }
55.1%, 86% { grid-template-rows: 1fr; }
87%, 100% { grid-template-rows: 0fr; }
}
@keyframes ef-second-rise {
0%, 55% { transform: translateY(8px); }
62%, 86% { transform: translateY(0); }
87%, 100% { transform: translateY(8px); }
}
@keyframes ef-ic-bob {
0%, 17% { transform: translateY(0); }
6% { transform: translateY(-3px); }
12% { transform: translateY(0); }
18%, 100% { transform: translateY(0); }
}
@keyframes ef-add-pop {
0%, 17% { transform: translateY(0); }
20% { transform: scale(.92); }
24%, 86% { transform: scale(1); }
87%, 100% { transform: scale(1); }
}
@keyframes ef-phase-cut {
0%, 17% { transform: scale(1); }
18%, 86% { transform: scale(1.08); }
87%, 100% { transform: scale(1); }
}
.ef.is-demo {
.ef__empty {
animation-name: ef-empty-out, ef-ic-bob;
animation-duration: $dur-loop, $dur-loop;
animation-timing-function: steps(1, end), $easing;
animation-iteration-count: infinite, infinite;
}
.ef__row--first .ef__fold {
animation-name: ef-first-in;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.ef__row--first .ef__cell {
animation-name: ef-first-rise;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.ef__row--first .ef__av {
animation-name: ef-av-pop;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.ef__row--second .ef__fold {
animation-name: ef-second-in;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.ef__row--second .ef__cell {
animation-name: ef-second-rise;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.ef__b--main {
animation-name: ef-add-pop;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
.ef__phase {
animation-name: ef-phase-cut;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
}
@media (max-width: 340px) {
.ef { padding: $sp-2; font-size: 11px; }
.ef__h { margin-bottom: $sp-1; }
.ef__empty { padding: $sp-3 0 $sp-1; }
.ef__ic { width: 36px; height: 36px; }
.ef__ic svg { width: 20px; height: 20px; }
.ef__cell { padding: 0 $sp-2; }
.ef__b { padding: 0 $sp-3; font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.ef__row.is-in .ef__cell { animation: none !important; }
.ef__b:active { transform: none; }
.ef.is-demo .ef__empty,
.ef.is-demo .ef__row .ef__fold,
.ef.is-demo .ef__row .ef__cell,
.ef.is-demo .ef__av,
.ef.is-demo .ef__b--main,
.ef.is-demo .ef__phase { animation: none !important; transform: none !important; }
// 움직임만 끄고 결과 상태는 남긴다 — 첫 활동이 들어온 뒤의 모습
.ef.is-demo .ef__empty { visibility: hidden !important; }
.ef.is-demo .ef__row--first .ef__fold,
.ef.is-demo .ef__row--second .ef__fold { grid-template-rows: 1fr !important; }
.ef.is-demo .ef__row .ef__av { transform: scale(1) !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ef {
box-sizing: border-box;
width: min(304px, 92vw);
padding: 12px;
border: 1px solid #17141a;
border-radius: 16px;
background: #ffd23f;
color: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-size: 12px;
line-height: 1.3;
}
.ef__h {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 8px;
}
.ef__t {
margin: 0;
font-size: 12px;
font-weight: 700;
}
.ef__phase {
margin-left: auto;
padding: 0 8px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.1);
color: #17141a;
font-size: 10px;
font-weight: 700;
line-height: 1.7;
}
.ef__empty {
display: grid;
justify-items: center;
gap: 4px;
padding: 16px 0 8px;
}
.ef__ic {
display: inline-grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.6);
color: #17141a;
}
.ef__ic svg {
width: 24px;
height: 24px;
}
.ef__e1 {
margin: 0;
font-size: 12px;
font-weight: 700;
}
.ef__e2 {
margin: 0;
color: rgba(23, 20, 26, 0.72);
font-size: 10px;
}
.ef.has-act .ef__empty {
display: none;
}
.ef__list {
display: grid;
gap: 4px;
margin: 0;
padding: 0;
list-style: none;
}
.ef__fold {
display: grid;
grid-template-rows: 1fr;
}
.ef__row--first .ef__fold,
.ef__row--second .ef__fold {
grid-template-rows: 0fr;
}
.ef.has-act .ef__row--first .ef__fold {
grid-template-rows: 1fr;
}
.ef__cell {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 8px;
min-height: 0;
overflow: hidden;
padding: 4px 8px;
border-radius: 8px;
background: #ffffff;
color: #17141a;
}
.ef__av {
display: inline-grid;
place-items: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--a);
color: #17141a;
font-size: 11px;
font-weight: 700;
}
.ef__line {
margin: 0;
}
.ef__line b {
font-weight: 700;
}
.ef__tm {
color: rgba(23, 20, 26, 0.72);
font-size: 10px;
}
.ef__row.is-in .ef__cell {
animation: ef-rise 300ms cubic-bezier(0.2, 1.4, 0.4, 1) both;
}
@keyframes ef-rise {
from {
transform: translateY(8px);
}
to {
transform: translateY(0);
}
}
.ef__btns {
display: flex;
gap: 4px;
margin: 8px 0 0;
}
.ef__b {
padding: 4px 12px;
border: 1px solid #17141a;
border-radius: 999px;
background: transparent;
color: #17141a;
font-family: inherit;
font-size: 12px;
font-weight: 700;
line-height: 1.6;
cursor: pointer;
}
.ef__b:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.ef__b:active {
transform: scale(0.94);
}
.ef__b--main {
background: #17141a;
color: #ffd23f;
}
@keyframes ef-empty-out {
0%, 17% {
visibility: visible;
}
18%, 86% {
visibility: hidden;
}
87%, 100% {
visibility: visible;
}
}
@keyframes ef-first-in {
0%, 17% {
grid-template-rows: 0fr;
}
18% {
grid-template-rows: 1fr;
}
18.1%, 86% {
grid-template-rows: 1fr;
}
87%, 100% {
grid-template-rows: 0fr;
}
}
@keyframes ef-first-rise {
0%, 18% {
transform: translateY(8px);
}
25%, 86% {
transform: translateY(0);
}
87%, 100% {
transform: translateY(8px);
}
}
@keyframes ef-av-pop {
0%, 18% {
transform: scale(0.4);
}
24%, 86% {
transform: scale(1);
}
87%, 100% {
transform: scale(0.4);
}
}
@keyframes ef-second-in {
0%, 54% {
grid-template-rows: 0fr;
}
55% {
grid-template-rows: 1fr;
}
55.1%, 86% {
grid-template-rows: 1fr;
}
87%, 100% {
grid-template-rows: 0fr;
}
}
@keyframes ef-second-rise {
0%, 55% {
transform: translateY(8px);
}
62%, 86% {
transform: translateY(0);
}
87%, 100% {
transform: translateY(8px);
}
}
@keyframes ef-ic-bob {
0%, 17% {
transform: translateY(0);
}
6% {
transform: translateY(-3px);
}
12% {
transform: translateY(0);
}
18%, 100% {
transform: translateY(0);
}
}
@keyframes ef-add-pop {
0%, 17% {
transform: translateY(0);
}
20% {
transform: scale(0.92);
}
24%, 86% {
transform: scale(1);
}
87%, 100% {
transform: scale(1);
}
}
@keyframes ef-phase-cut {
0%, 17% {
transform: scale(1);
}
18%, 86% {
transform: scale(1.08);
}
87%, 100% {
transform: scale(1);
}
}
.ef.is-demo .ef__empty {
animation-name: ef-empty-out, ef-ic-bob;
animation-duration: 2s, 2s;
animation-timing-function: steps(1, end), cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite, infinite;
}
.ef.is-demo .ef__row--first .ef__fold {
animation-name: ef-first-in;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.ef.is-demo .ef__row--first .ef__cell {
animation-name: ef-first-rise;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.ef.is-demo .ef__row--first .ef__av {
animation-name: ef-av-pop;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.ef.is-demo .ef__row--second .ef__fold {
animation-name: ef-second-in;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.ef.is-demo .ef__row--second .ef__cell {
animation-name: ef-second-rise;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.ef.is-demo .ef__b--main {
animation-name: ef-add-pop;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
.ef.is-demo .ef__phase {
animation-name: ef-phase-cut;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@media (max-width: 340px) {
.ef {
padding: 8px;
font-size: 11px;
}
.ef__h {
margin-bottom: 4px;
}
.ef__empty {
padding: 12px 0 4px;
}
.ef__ic {
width: 36px;
height: 36px;
}
.ef__ic svg {
width: 20px;
height: 20px;
}
.ef__cell {
padding: 0 8px;
}
.ef__b {
padding: 0 12px;
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.ef__row.is-in .ef__cell {
animation: none !important;
}
.ef__b:active {
transform: none;
}
.ef.is-demo .ef__empty,
.ef.is-demo .ef__row .ef__fold,
.ef.is-demo .ef__row .ef__cell,
.ef.is-demo .ef__av,
.ef.is-demo .ef__b--main,
.ef.is-demo .ef__phase {
animation: none !important;
transform: none !important;
}
.ef.is-demo .ef__empty {
visibility: hidden !important;
}
.ef.is-demo .ef__row--first .ef__fold,
.ef.is-demo .ef__row--second .ef__fold {
grid-template-rows: 1fr !important;
}
.ef.is-demo .ef__row .ef__av {
transform: scale(1) !important;
}
}// 실제 동작: 새 활동을 누르면 빈 상태가 물러나고 첫 활동이 자라 나타난다.
// 비우기는 반대로 — 줄을 전부 치우고 빈 상태로 돌아간다. 상태 문구는 aria-live 로 읽힌다.
var ef = document.getElementById('ef');
var efEmpty = document.getElementById('ef-empty');
var efList = document.getElementById('ef-list');
var efPhase = document.getElementById('ef-phase');
var efCount = 0; // 접힌 미리보기 줄은 세지 않는다
function efSync(on) {
ef.classList.toggle('has-act', on);
efPhase.textContent = on
? efPhase.getAttribute('data-t1').replace('{n}', efCount)
: efPhase.getAttribute('data-t2');
}
document.getElementById('ef-add').addEventListener('click', function () {
ef.classList.remove('is-demo');
var row = document.getElementById('ef-tpl').content.firstElementChild.cloneNode(true);
efList.insertBefore(row, efList.firstChild);
efCount++;
efSync(true);
});
document.getElementById('ef-clear').addEventListener('click', function () {
ef.classList.remove('is-demo');
efList.innerHTML = '';
efCount = 0;
efSync(false);
});
efSync(false);import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./EmptyFirstActivity.module.scss";
export type EmptyFirstActivityProps = {
groupLabel?: string;
heading?: string;
emptyTitle?: string;
emptySub?: string;
emptyPhase?: string;
countPhase?: (n: number) => string;
addLabel?: string;
clearLabel?: string;
nowLabel?: string;
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type Item = { key: number; name: string; initial: string; av: string; text: string };
const NEXT: Item[] = [
{ key: 0, name: "Park Seo-a", initial: "P", av: "#4cd4a6", text: " created the first doc" },
];
function TrayIcon() {
return (
<svg viewBox="0 0 256 256" width="1em" height="1em" fill="currentColor">
<g>
<path d="M216 48v112h-36.69a8 8 0 0 0-5.66 2.34l-19.31 19.32a8 8 0 0 1-5.66 2.34h-41.37a8 8 0 0 1-5.66-2.34l-19.31-19.32a8 8 0 0 0-5.66-2.34H40V48a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8" opacity=".2" />
<path d="M208 32H48a16 16 0 0 0-16 16v160a16 16 0 0 0 16 16h160a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16m0 16v104h-28.7a15.86 15.86 0 0 0-11.3 4.69L148.69 176h-41.38L88 156.68A15.9 15.9 0 0 0 76.69 152H48V48Zm0 160H48v-40h28.69L96 187.32a15.9 15.9 0 0 0 11.31 4.68h41.38a15.86 15.86 0 0 0 11.31-4.69L179.31 168H208z" />
</g>
</svg>
);
}
export default function EmptyFirstActivity({
groupLabel = "Activity feed",
heading = "Activity feed",
emptyTitle = "No activity yet",
emptySub = "The first activity will stack up here",
emptyPhase = "Empty",
countPhase = (n) => `${n} activities`,
addLabel = "New activity",
clearLabel = "Clear",
nowLabel = "now",
demo = true,
duration = 220,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: EmptyFirstActivityProps) {
const [live, setLive] = useState(demo);
const [items, setItems] = useState<Item[]>([]);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
function add() {
setLive(false);
setItems((r) => [{ ...NEXT[0], key: Date.now() }, ...r]);
}
function clear() {
setLive(false);
setItems([]);
}
const has = items.length > 0;
const cls = [styles.ef, live ? styles.isDemo : "", has ? styles.hasAct : ""].filter(Boolean).join(" ");
return (
<section className={cls} style={vars} aria-label={groupLabel}>
<header className={styles.h}>
<h2 className={styles.t}>{heading}</h2>
<span className={styles.phase} aria-live="polite">
{live ? (
<>
<i className={styles.pEmpty} aria-hidden="true">{emptyPhase}</i>
<i className={styles.pCount} aria-hidden="true">{countPhase(2)}</i>
</>
) : (
(has ? countPhase(items.length) : emptyPhase)
)}
</span>
</header>
<div className={styles.empty}>
<span className={styles.ic} aria-hidden="true"><TrayIcon /></span>
<p className={styles.e1}>{emptyTitle}</p>
<p className={styles.e2}>{emptySub}</p>
</div>
<ul className={styles.list} role="log" aria-live="polite">
<li className={styles.rowFirst}>
<div className={styles.fold}>
<div className={styles.cell}>
<span className={styles.av} style={{ background: color }}>K</span>
<p className={styles.line}><b>Kim Haneul</b> joined the team</p>
<time className={styles.tm}>{nowLabel}</time>
</div>
</div>
</li>
<li className={styles.rowSecond}>
<div className={styles.fold}>
<div className={styles.cell}>
<span className={styles.av} style={{ background: "#ff9ebb" }}>L</span>
<p className={styles.line}><b>Lee Jun</b> opened the getting-started guide</p>
<time className={styles.tm}>{nowLabel}</time>
</div>
</div>
</li>
{items.map((it) => (
<li key={it.key} className={styles.rowIn}>
<div className={styles.fold}>
<div className={styles.cell}>
<span className={styles.av} style={{ background: it.av }}>{it.initial}</span>
<p className={styles.line}><b>{it.name}</b>{it.text}</p>
<time className={styles.tm}>{nowLabel}</time>
</div>
</div>
</li>
))}
</ul>
<div className={styles.btns}>
<button className={styles.bMain} type="button" onClick={add}>{addLabel}</button>
<button className={styles.b} type="button" onClick={clear}>{clearLabel}</button>
</div>
</section>
);
}08 빈 피드가 첫 활동으로 바뀌는 순간 — 측정 누적면적 18.88% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 알림 목록 ui 9가지 — 사이드바에 박히는 활동 피드와 이력 위젯
이 데모가 속한 글
알림 목록 ui 9가지 — 사이드바에 박히는 활동 피드와 이력 위젯알림 목록 ui는 떴다 사라지는 토스트가 아니라 화면 한구석에 자리를 잡고 계속 쌓이는 목록을 말합니다. 사이드바에 박히는 이 아홉 개 위젯은 읽음 표시, 고치기 전 값, 로그 수준, 오늘의 건수까지 전부 실제 상태 이 글의 데모 전부 보기


