auto: 2026-08-01 08:55 (EOMSANGDON-HOME)

This commit is contained in:
2026-08-01 08:55:01 +09:00
parent 4aa211cead
commit 4501f3b19e
20 changed files with 1205 additions and 105 deletions
+78
View File
@@ -0,0 +1,78 @@
/* 공통 프로그레스 서클 — 뷰포트 위에 얹는 원형 진행 표시. */
.ui-progress-circle {
--ui-progress-size: 96px;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-8);
pointer-events: none;
user-select: none;
}
.ui-progress-circle__dial {
position: relative;
width: var(--ui-progress-size);
height: var(--ui-progress-size);
}
.ui-progress-circle__svg {
width: 100%;
height: 100%;
/* 12시 방향에서 시계 방향으로 채운다. */
transform: rotate(-90deg);
}
.ui-progress-circle__track {
fill: none;
stroke: color-mix(in srgb, var(--color-border) 70%, transparent);
stroke-width: 8;
}
.ui-progress-circle__bar {
fill: none;
stroke: var(--color-royal-amethyst);
stroke-width: 8;
stroke-linecap: round;
transition: stroke-dashoffset var(--transition-base);
}
/* 진행률을 모르는 구간 — 호 하나를 계속 돌린다. */
.ui-progress-circle.is-indeterminate .ui-progress-circle__svg {
animation: ui-progress-spin 1s linear infinite;
}
@keyframes ui-progress-spin {
from {
transform: rotate(-90deg);
}
to {
transform: rotate(270deg);
}
}
.ui-progress-circle__percent {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-body);
font-family: var(--font-body);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-semibold);
}
.ui-progress-circle__label {
max-width: 22ch;
padding: var(--spacing-4) var(--spacing-8);
border-radius: var(--radius-inputs);
background: color-mix(in srgb, var(--color-surface-raised) 82%, transparent);
color: var(--color-text-secondary);
font-size: var(--text-caption);
text-align: center;
word-break: keep-all;
}
.ui-progress-circle__label:empty {
display: none;
}
+89
View File
@@ -0,0 +1,89 @@
import "./ui_template_progress.css";
/* =============================================================================
* ui_template_progress.ts
* 공통 프로그레스 서클 (원형 진행 표시)
*
* 뷰포트 위에 얹어 "지금 무엇을 얼마나 불러왔는지"를 보여 주는 용도.
* 진행률을 모르는 구간은 set()에 ratio를 주지 않으면 회전 애니메이션으로 표시한다.
* 색상/치수는 theme.css 변수만 사용한다.
* ========================================================================== */
const SVG_NS = "http://www.w3.org/2000/svg";
/** viewBox 기준 반지름 — 실제 크기는 CSS(--ui-progress-size)로 정한다. */
const RADIUS = 42;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
export interface ProgressCircleOptions {
/** 지름(px). 기본 96. */
size?: number;
/** 서클 아래 안내 문구. */
label?: string;
}
export interface ProgressCircleHandle {
root: HTMLDivElement;
/** 진행률(0~1)과 문구 갱신. ratio가 null이면 진행률 미상(회전 표시). */
set: (ratio: number | null, label?: string) => void;
/** 화면에서 제거. */
remove: () => void;
}
export function createProgressCircle(options: ProgressCircleOptions = {}): ProgressCircleHandle {
const root = document.createElement("div");
root.className = "ui-progress-circle";
root.setAttribute("role", "status");
root.setAttribute("aria-live", "polite");
if (options.size) root.style.setProperty("--ui-progress-size", `${options.size}px`);
const svg = document.createElementNS(SVG_NS, "svg");
svg.setAttribute("class", "ui-progress-circle__svg");
svg.setAttribute("viewBox", "0 0 100 100");
const track = document.createElementNS(SVG_NS, "circle");
track.setAttribute("class", "ui-progress-circle__track");
track.setAttribute("cx", "50");
track.setAttribute("cy", "50");
track.setAttribute("r", String(RADIUS));
const bar = document.createElementNS(SVG_NS, "circle");
bar.setAttribute("class", "ui-progress-circle__bar");
bar.setAttribute("cx", "50");
bar.setAttribute("cy", "50");
bar.setAttribute("r", String(RADIUS));
bar.setAttribute("stroke-dasharray", String(CIRCUMFERENCE));
bar.setAttribute("stroke-dashoffset", String(CIRCUMFERENCE));
svg.append(track, bar);
const percent = document.createElement("span");
percent.className = "ui-progress-circle__percent";
const label = document.createElement("span");
label.className = "ui-progress-circle__label";
label.textContent = options.label ?? "";
const dial = document.createElement("div");
dial.className = "ui-progress-circle__dial";
dial.append(svg, percent);
root.append(dial, label);
function set(ratio: number | null, nextLabel?: string): void {
if (nextLabel !== undefined) label.textContent = nextLabel;
if (ratio === null) {
// 진행률 미상 — 4분의 1 호를 돌려 "돌아가는 중"만 알린다.
root.classList.add("is-indeterminate");
bar.setAttribute("stroke-dashoffset", String(CIRCUMFERENCE * 0.75));
percent.textContent = "";
return;
}
const clamped = Math.min(1, Math.max(0, ratio));
root.classList.remove("is-indeterminate");
bar.setAttribute("stroke-dashoffset", String(CIRCUMFERENCE * (1 - clamped)));
percent.textContent = `${Math.round(clamped * 100)}%`;
}
set(0, options.label);
return {
root,
set,
remove: () => root.remove(),
};
}