feat(B04/B05): 프로그레스 서클을 3D·지도·그래프 전 영역에 공통 적용
- 공통 서클 양식을 로딩 스피너(.ui-spinner) 기준으로 통일하고 overlay 옵션 신설 (컨테이너 정중앙 배치를 페이지별 CSS 없이 처리) - B05 3D: 위쪽 18% → 뷰포트 정중앙 (하단 패널에 가려져도 무방) - B04 포인트클라우드 뷰어: setLoading() 신설, render() 시 자동 해제 - B04 지형 뷰어: GLTF/PLY 로더 진행 이벤트로 실제 바이트 진행률 표시 - B04 2D 지도: 도엽 레이어 n/10 진행률 - B05 종단면 그래프: body-wrap으로 감싸 서클 유지, 자료 조회 전후로 토글 - B05 배수유역도: 배경도 → 도엽 레이어 → 세부유역 산정 단계 진행률
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { createProgressCircle } from "@ui/ui_template_progress";
|
||||
import {
|
||||
fetchGisGeoJson,
|
||||
fetchVWorldMeta,
|
||||
@@ -125,13 +126,23 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
|
||||
scaleBar.className = "b04-map__scale";
|
||||
const scaleText = document.createElement("span");
|
||||
scaleBar.append(scaleText);
|
||||
// 지도 정중앙 로딩 서클 — 도엽 레이어가 10종이라 다 받을 때까지 화면이 비어 보인다.
|
||||
const progress = createProgressCircle({ overlay: true });
|
||||
progress.root.hidden = true;
|
||||
viewport.append(
|
||||
...BACKGROUND_LAYERS.map((layer) => backgroundImages.get(layer)!),
|
||||
canvas,
|
||||
empty,
|
||||
statusStack,
|
||||
scaleBar,
|
||||
progress.root,
|
||||
);
|
||||
|
||||
/** 진행률(0~1, 모르면 null)과 문구. label이 null이면 서클을 감춘다. */
|
||||
function showProgress(ratio: number | null, label: string | null): void {
|
||||
progress.root.hidden = label === null;
|
||||
if (label !== null) progress.set(ratio, label);
|
||||
}
|
||||
root.append(header, viewport);
|
||||
|
||||
let currentProjectId: string | null = null;
|
||||
@@ -366,8 +377,11 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
|
||||
preparedLayers.clear();
|
||||
resetView();
|
||||
status.textContent = L("B04_Surface_Map_Loading");
|
||||
showProgress(0, L("B04_Surface_Map_Loading"));
|
||||
try {
|
||||
const nextMeta = await fetchVWorldMeta(projectId, "satellite");
|
||||
// 레이어가 끝나는 대로 진행률을 올린다 — 10종을 다 받을 때까지 화면이 비어 있어서다.
|
||||
let done = 0;
|
||||
const loadedLayers = await Promise.all(
|
||||
GIS_LAYERS.map(async (layer) => {
|
||||
try {
|
||||
@@ -375,6 +389,11 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
|
||||
return [layer, data] as const;
|
||||
} catch {
|
||||
return [layer, null] as const;
|
||||
} finally {
|
||||
done += 1;
|
||||
if (sequence === loadSequence) {
|
||||
showProgress(done / GIS_LAYERS.length, `도엽 레이어 ${done}/${GIS_LAYERS.length}`);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -397,9 +416,11 @@ export function createSurfaceMapViewer(): SurfaceMapViewer {
|
||||
);
|
||||
resetView();
|
||||
syncLayerVisibility();
|
||||
showProgress(null, null);
|
||||
} catch (error) {
|
||||
if (sequence !== loadSequence) return;
|
||||
status.textContent = error instanceof Error ? error.message : L("B04_Surface_Map_LoadFailed");
|
||||
showProgress(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -300,6 +300,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
const projectId = getProjectId();
|
||||
if (!projectId) return;
|
||||
showLoadingOverlay();
|
||||
viewer.setLoading("포인트 데이터 로딩 중…");
|
||||
try {
|
||||
pointCloud = await fetchSurfacePointCloud(projectId, filterGroup.select.value);
|
||||
terrainViewer.setReferenceBounds(pointCloud.bounds);
|
||||
@@ -324,6 +325,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
||||
models = modelResponse.models;
|
||||
renderInputFiles(inputs.files);
|
||||
renderStatus(status);
|
||||
viewer.setLoading("포인트 데이터 로딩 중…");
|
||||
try {
|
||||
pointCloud = await fetchSurfacePointCloud(projectId, filterGroup.select.value);
|
||||
terrainViewer.setReferenceBounds(pointCloud.bounds);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { PLYLoader } from "three/examples/jsm/loaders/PLYLoader.js";
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { createProgressCircle } from "@ui/ui_template_progress";
|
||||
import type { SurfaceBounds, SurfaceModelSummary } from "./B04_wf1_Surface_Api_Fetch";
|
||||
import {
|
||||
bindCursorPivotControls,
|
||||
@@ -168,6 +169,17 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
legendBar.append(maxValSpan, gradientDiv, minValSpan);
|
||||
viewerArea.append(legendBar);
|
||||
|
||||
// 뷰포트 정중앙 로딩 서클 — 메쉬 파일은 수십 MB라 내려받는 동안 화면이 비어 보인다.
|
||||
const progress = createProgressCircle({ overlay: true });
|
||||
progress.root.hidden = true;
|
||||
viewerArea.append(progress.root);
|
||||
|
||||
/** 진행률(0~1, 모르면 null)과 문구를 표시한다. label이 null이면 서클을 감춘다. */
|
||||
function showProgress(ratio: number | null, label: string | null): void {
|
||||
progress.root.hidden = label === null;
|
||||
if (label !== null) progress.set(ratio, label);
|
||||
}
|
||||
|
||||
// Three.js context variables
|
||||
let currentProjectId = "";
|
||||
let currentModelsList: readonly SurfaceModelSummary[] = [];
|
||||
@@ -311,6 +323,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
currentModelId = null;
|
||||
scaleBar.hidden = true;
|
||||
statusSpan.textContent = "모델 조회 중...";
|
||||
showProgress(null, "모델 조회 중…");
|
||||
|
||||
// 1. Find matching model in list
|
||||
// model_type is TIN / DTM / NURBS / Implicit / Meshfree (we match activeMethod)
|
||||
@@ -327,6 +340,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
|
||||
if (!match) {
|
||||
statusSpan.textContent = "일치하는 완성된 모델을 찾을 수 없습니다.";
|
||||
showProgress(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -337,8 +351,16 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
const generation = ++loadGeneration;
|
||||
|
||||
statusSpan.textContent = "3D 메쉬 파일 다운로드 중...";
|
||||
showProgress(0, "3D 메쉬 내려받는 중…");
|
||||
const previewUrl = `${API_BASE_URL}/projects/${currentProjectId}/surface/models/${modelId}/preview?smooth=${isSmooth}`;
|
||||
|
||||
/** 로더 진행 이벤트 → 서클. 서버가 길이를 안 주면(gzip) 진행률 없이 회전만 시킨다. */
|
||||
const onDownload = (event: ProgressEvent): void => {
|
||||
if (generation !== loadGeneration) return;
|
||||
const ratio = event.lengthComputable && event.total > 0 ? event.loaded / event.total : null;
|
||||
showProgress(ratio, "3D 메쉬 내려받는 중…");
|
||||
};
|
||||
|
||||
try {
|
||||
if (activeMethod === "meshfree") {
|
||||
new PLYLoader().load(
|
||||
@@ -359,12 +381,15 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
terrainMesh = points;
|
||||
scene.add(points);
|
||||
fitCamera(points);
|
||||
showProgress(1, "등고선을 그리는 중…");
|
||||
await loadSelectedContours(modelId, isSmooth);
|
||||
showProgress(null, null);
|
||||
},
|
||||
undefined,
|
||||
onDownload,
|
||||
() => {
|
||||
if (generation !== loadGeneration) return;
|
||||
statusSpan.textContent = "3D 파일 로드에 실패했습니다.";
|
||||
showProgress(null, null);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
@@ -385,17 +410,21 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer {
|
||||
terrainMesh = gltf.scene;
|
||||
scene.add(gltf.scene);
|
||||
fitCamera(gltf.scene);
|
||||
showProgress(1, "등고선을 그리는 중…");
|
||||
await loadSelectedContours(modelId, isSmooth);
|
||||
showProgress(null, null);
|
||||
},
|
||||
undefined,
|
||||
onDownload,
|
||||
() => {
|
||||
if (generation !== loadGeneration) return;
|
||||
statusSpan.textContent = "3D 메쉬 파일이 없거나 로드할 수 없습니다.";
|
||||
showProgress(null, null);
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
statusSpan.textContent = "에러 발생";
|
||||
showProgress(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { RENDER_OPTIONS } from "@config/config_frontend";
|
||||
import { createProgressCircle } from "@ui/ui_template_progress";
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import type { SurfaceBounds, SurfacePointCloudSampleResponse } from "./B04_wf1_Surface_Api_Fetch";
|
||||
@@ -20,6 +21,8 @@ export interface SurfacePointCloudViewer {
|
||||
controlsGroup: HTMLElement;
|
||||
optionsGroup: HTMLElement;
|
||||
statusSpan: HTMLElement;
|
||||
/** 로딩 서클 표시. 문구를 주면 켜고, null이면 끈다. `render()` 시 자동으로 꺼진다. */
|
||||
setLoading: (label: string | null) => void;
|
||||
render: (data: SurfacePointCloudSampleResponse | null) => void;
|
||||
setAxesVisible: (visible: boolean) => void;
|
||||
applyCameraState: (state: SurfaceCameraState) => void;
|
||||
@@ -101,6 +104,15 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
|
||||
scaleBar.append(scaleText);
|
||||
viewerArea.append(canvas, scaleBar, statusSpan);
|
||||
root.append(viewerArea);
|
||||
// 뷰포트 정중앙 로딩 서클 — 지도·그래프·다른 3D 뷰어와 같은 공통 컴포넌트.
|
||||
const progress = createProgressCircle({ overlay: true });
|
||||
progress.root.hidden = true;
|
||||
viewerArea.append(progress.root);
|
||||
|
||||
function setLoading(label: string | null): void {
|
||||
progress.root.hidden = label === null;
|
||||
if (label !== null) progress.set(null, label);
|
||||
}
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
canvas,
|
||||
@@ -306,7 +318,9 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
|
||||
controlsGroup,
|
||||
optionsGroup,
|
||||
statusSpan,
|
||||
setLoading,
|
||||
render(data) {
|
||||
setLoading(null);
|
||||
currentData = data;
|
||||
clearPoints();
|
||||
if (!data) {
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
type BoundaryOverrideEntry,
|
||||
} from "./B05_wf2_Route_UI_Drainage_Boundary";
|
||||
import { createPipeEditor } from "./B05_wf2_Route_UI_Drainage_Pipes";
|
||||
import { createProgressCircle } from "@ui/ui_template_progress";
|
||||
|
||||
// 배수유역도 패널 — 하단 종단 패널 안쪽 우측에 도킹되는 2단 사이드 패널.
|
||||
// 하단 패널이 닫히면 이 패널도 함께 화면에서 사라진다(부모 안에 들어 있으므로 자동).
|
||||
@@ -147,7 +148,16 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
const status = document.createElement("span");
|
||||
status.className = "b05-drainage__status";
|
||||
status.textContent = "노선을 확정하면 배수유역도가 표시됩니다.";
|
||||
viewport.append(backgroundImage, canvas, status);
|
||||
// 지도 정중앙 로딩 서클 — 배경도·도엽 레이어·유역 산정이 끝날 때까지 화면이 비어 보인다.
|
||||
const progress = createProgressCircle({ overlay: true });
|
||||
progress.root.hidden = true;
|
||||
viewport.append(backgroundImage, canvas, status, progress.root);
|
||||
|
||||
/** 진행률(0~1, 모르면 null)과 문구. label이 null이면 서클을 감춘다. */
|
||||
function showProgress(ratio: number | null, label: string | null): void {
|
||||
progress.root.hidden = label === null;
|
||||
if (label !== null) progress.set(ratio, label);
|
||||
}
|
||||
// 유역 제원 목록(면적·표고·유하거리·관경). 관경 수식 미확정이라 당분간 "미정"으로 나온다.
|
||||
const basinList = document.createElement("div");
|
||||
basinList.className = "b05-drainage__basins";
|
||||
@@ -412,6 +422,7 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
analyzeButton.disabled = true;
|
||||
status.hidden = false;
|
||||
status.textContent = "세부유역을 산정하는 중…";
|
||||
showProgress(null, "세부유역을 산정하는 중…");
|
||||
try {
|
||||
const chainages = !auto && pipeEditor.pipes().length > 0 ? pipeEditor.chainages() : undefined;
|
||||
const response = await fetchDrainageBasins(projectId, chainages);
|
||||
@@ -441,6 +452,7 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
status.textContent = error instanceof Error ? error.message : "세부유역 산정에 실패했습니다.";
|
||||
} finally {
|
||||
analyzeButton.disabled = false;
|
||||
showProgress(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,8 +515,10 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
backgroundImage.removeAttribute("src");
|
||||
status.hidden = false;
|
||||
status.textContent = "배경도를 불러오는 중…";
|
||||
showProgress(0, "배경도를 불러오는 중…");
|
||||
try {
|
||||
const nextMeta = await fetchVWorldMeta(activeProjectId, "satellite");
|
||||
showProgress(1 / 3, "도엽 레이어를 불러오는 중…");
|
||||
const loaded = await Promise.all(
|
||||
DRAINAGE_LAYERS.map(async (layer) => {
|
||||
try {
|
||||
@@ -531,12 +545,14 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
if (featureCount === 0) status.textContent = "도엽 레이어가 없습니다. B04에서 임포트하세요.";
|
||||
fitToRoute();
|
||||
scheduleDraw();
|
||||
showProgress(2 / 3, "세부유역을 산정하는 중…");
|
||||
// B04 분석 결과를 읽어 오는 것뿐이라 즉시 끝난다 — 페이지에 들어오면 바로 보여 준다.
|
||||
void analyze(true);
|
||||
} catch (error) {
|
||||
if (sequence !== loadSequence) return;
|
||||
status.hidden = false;
|
||||
status.textContent = error instanceof Error ? error.message : "배경도를 불러오지 못했습니다.";
|
||||
showProgress(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -409,15 +409,18 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
||||
|
||||
async function restoreSections(routeId: number): Promise<void> {
|
||||
let detail: SectionDetailResponse;
|
||||
profilePanel.setLoading("종단면 자료를 불러오는 중…");
|
||||
try {
|
||||
detail = await fetchSectionDetail(activeProjectId, routeId);
|
||||
} catch {
|
||||
// 종횡단 데이터 자체가 없는 경우(생성 실패·최초 진입)는 빈 안내로 둔다.
|
||||
currentSectionDetail = null;
|
||||
profilePanel.setLoading(null);
|
||||
profilePanel.clear();
|
||||
viewer.renderStationLines([], 0);
|
||||
return;
|
||||
}
|
||||
profilePanel.setLoading(null);
|
||||
try {
|
||||
renderSections(detail, routeId);
|
||||
// 복귀/최초 진입 시(클라이언트 목록이 비어 있을 때만) 확정된 비정규 측점을 사이드바에 복원한다.
|
||||
@@ -603,9 +606,9 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
||||
* 자료가 끝나는 순서대로 채운다. 3D 지형이 가장 느리므로 맨 마지막에 올리고, 그동안
|
||||
* 3D 뷰포트에 공통 프로그레스 서클을 띄운다(2026-08-01 사용자 지시). */
|
||||
const LOAD_STEP_COUNT = 5;
|
||||
const progress = createProgressCircle({ label: "화면 틀을 준비하는 중…" });
|
||||
// 하단 종단 패널(z-index 3)보다 아래에 둔다 — 패널을 볼 때 서클이 방해하지 않는다.
|
||||
progress.root.classList.add("b05-route__progress");
|
||||
// 3D 뷰포트 정중앙. 하단 종단 패널(z-index 3)보다 아래라 패널에 가려지는 것은 무방하다
|
||||
// (2026-08-01 사용자 지시).
|
||||
const progress = createProgressCircle({ label: "화면 틀을 준비하는 중…", overlay: true });
|
||||
viewer.root.append(progress.root);
|
||||
let loadedSteps = 0;
|
||||
function advanceLoading(label: string): void {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { LONG_PAD } from "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_Common";
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
import { createDrainagePanel } from "./B05_wf2_Route_UI_Drainage_Panel";
|
||||
import { createProgressCircle } from "@ui/ui_template_progress";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { saveProfileAlignment } from "./B05_wf2_Route_Api_Fetch";
|
||||
import type {
|
||||
@@ -272,10 +273,17 @@ export function createRouteProfilePanel(
|
||||
body.append(empty);
|
||||
// 종단면 본문 + 우측 배수유역 패널을 나란히 놓는 2단 구성.
|
||||
// 배수유역 패널이 이 안에 있으므로 하단 패널을 접으면 함께 사라진다(사용자 지시).
|
||||
// 그래프 영역 정중앙 로딩 서클 — 종단면 자료가 도착할 때까지 빈 안내만 보인다.
|
||||
// body는 그릴 때마다 자식이 통째로 교체되므로 서클은 감싸는 칸에 둔다.
|
||||
const progress = createProgressCircle({ overlay: true });
|
||||
progress.root.hidden = true;
|
||||
const bodyWrap = document.createElement("div");
|
||||
bodyWrap.className = "b05-route-profile__body-wrap";
|
||||
bodyWrap.append(body, progress.root);
|
||||
const content = document.createElement("div");
|
||||
content.className = "b05-route-profile__content";
|
||||
const drainagePanel = createDrainagePanel();
|
||||
content.append(body, drainagePanel.root);
|
||||
content.append(bodyWrap, drainagePanel.root);
|
||||
root.append(panelHandle.root, balanceBar, content);
|
||||
drainagePanel.load(projectId);
|
||||
|
||||
@@ -639,6 +647,11 @@ export function createRouteProfilePanel(
|
||||
},
|
||||
/** 배수유역도 패널 — 경로 확정 흐름에서 유역선 편집 저장 여부를 묻는 데 쓴다. */
|
||||
drainage: drainagePanel,
|
||||
/** 그래프 영역 로딩 서클. 문구를 주면 켜고 null이면 끈다. */
|
||||
setLoading(label: string | null) {
|
||||
progress.root.hidden = label === null;
|
||||
if (label !== null) progress.set(null, label);
|
||||
},
|
||||
dispose() {
|
||||
window.clearTimeout(resizeTimer);
|
||||
resizeObserver.disconnect();
|
||||
|
||||
@@ -69,6 +69,15 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 그래프 본문 + 로딩 서클을 겹치기 위한 칸. body는 그릴 때마다 자식이 교체된다. */
|
||||
.b05-route-profile__body-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.b05-route-profile__body {
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 auto;
|
||||
@@ -126,6 +135,7 @@
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.b05-route-profile.is-collapsed .b05-route-profile__body-wrap,
|
||||
.b05-route-profile.is-collapsed .b05-route-profile__body,
|
||||
.b05-route-profile.is-collapsed .b05-route-profile__balance {
|
||||
display: none;
|
||||
@@ -182,16 +192,6 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 진입 로딩 서클 — 3D 뷰포트 위쪽 가운데. 하단 종단 패널(z-index 3)보다 아래에 둬서
|
||||
패널을 볼 때 가리지 않는다(2026-08-01 사용자 지시). */
|
||||
.b05-route__progress {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 18%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* 뷰셋·표시 토글 버튼 묶음 — 좌상단(2026-08-01 사용자 지시).
|
||||
안내 문구가 우상단으로 옮겨져 좌상단이 비었다. */
|
||||
.b05-route__view-controls {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* 공통 프로그레스 서클 — 뷰포트 위에 얹는 원형 진행 표시. */
|
||||
/* 공통 프로그레스 서클 — 뷰포트 위에 얹는 원형 진행 표시.
|
||||
테두리 두께·색은 공통 로딩 스피너(.ui-spinner)와 같게 맞추고, 가운데에 진행률만 더한다. */
|
||||
.ui-progress-circle {
|
||||
--ui-progress-size: 96px;
|
||||
--ui-progress-size: 72px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -9,6 +10,15 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* 컨테이너 정중앙 오버레이 — 3D 뷰포트·지도·그래프 어디에나 같은 방식으로 얹는다.
|
||||
컨테이너에 position: relative 가 있어야 한다. */
|
||||
.ui-progress-circle--overlay {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ui-progress-circle__dial {
|
||||
position: relative;
|
||||
width: var(--ui-progress-size);
|
||||
@@ -24,7 +34,7 @@
|
||||
|
||||
.ui-progress-circle__track {
|
||||
fill: none;
|
||||
stroke: color-mix(in srgb, var(--color-border) 70%, transparent);
|
||||
stroke: var(--color-mist-violet);
|
||||
stroke-width: 8;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,12 @@ const RADIUS = 42;
|
||||
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
||||
|
||||
export interface ProgressCircleOptions {
|
||||
/** 지름(px). 기본 96. */
|
||||
/** 지름(px). 기본 72(공통 로딩 스피너와 같은 무게감). */
|
||||
size?: number;
|
||||
/** 서클 아래 안내 문구. */
|
||||
label?: string;
|
||||
/** 컨테이너 정중앙에 띄우는 오버레이로 만든다(컨테이너는 position: relative 여야 한다). */
|
||||
overlay?: boolean;
|
||||
}
|
||||
|
||||
export interface ProgressCircleHandle {
|
||||
@@ -31,7 +33,7 @@ export interface ProgressCircleHandle {
|
||||
|
||||
export function createProgressCircle(options: ProgressCircleOptions = {}): ProgressCircleHandle {
|
||||
const root = document.createElement("div");
|
||||
root.className = "ui-progress-circle";
|
||||
root.className = "ui-progress-circle" + (options.overlay ? " ui-progress-circle--overlay" : "");
|
||||
root.setAttribute("role", "status");
|
||||
root.setAttribute("aria-live", "polite");
|
||||
if (options.size) root.style.setProperty("--ui-progress-size", `${options.size}px`);
|
||||
|
||||
Reference in New Issue
Block a user