fix(B05): 노선 편집 접선점을 늘 보이게, 라벨을 R+곡선 길이로, 중심 반대쪽에 배치

사용자 지적 3건 반영.

- **접선점 표기 복구** — 손잡이를 「고른 곡선만」으로 줄이면서 직선↔R 만나는 자리의
  **표기까지 없앴음**. 접선점은 손잡이이기 이전에 읽을 정보라 **늘 그림**(고른 곡선은
  속을 채워 도드라지게). 노드를 못 집던 문제는 집기 우선순위로 이미 풀려 겹치지 않음.
- **라벨을 반지름 + 곡선 길이 두 칸으로, [자동] 단추 삭제** — 교각 Δ 는 앞뒤 직선이
  정하므로 L = R·Δ 로 묶임. 길이를 받으면 R 로 바꿔 한 값만 보관. 칸을 비우면 자동.
- **라벨 자리를 곡선 중심의 반대쪽으로**(상하좌우) — 중심 쪽에 두면 곡선을 가림.
  중심 방향은 접선점 두 방향 단위벡터의 합(각 이등분선)으로 구함.
- ⚠ 라벨 글자가 안 읽히던 것 — `--color-surface-2` 가 이 테마에 없어 밝은 기본값으로
  떨어졌음. 모달 본체와 같은 토큰으로 교체(실측 배경 rgb(37,31,56)·글자 rgb(228,224,240)).
- 상자 높이가 늦게 자라 자리가 11px 어긋나던 것도 다음 프레임에 다시 맞추게 함.
- 700줄 유지 — 교각·중심방향은 `_Label`, 등고선 띠는 `_Input` 으로 옮겨 본체 687줄.

실화면 검증 — 접선점이 고르기 전에도 보임 / 곡선 길이 40 → 반지름 203.7m 로 따라옴 /
라벨이 중심 반대쪽(위)에 붙음 / [자동] 없음. 554 passed · 18 skipped, tsc 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-07 19:28:02 +09:00
co-authored by Claude Opus 5
parent f84b3d9451
commit 4300be8385
4 changed files with 194 additions and 108 deletions
+42 -50
View File
@@ -32,11 +32,16 @@ import type { RoutePlanCurve } from "./B05_Profile_Api_Replan";
import { buildEditedPolyline, dragHandleTo as curveDragTo } from "./B05_Profile_UI_RouteEdit_Curve";
import {
bindRouteEditNavigation,
contourBandRect,
handleAtScreen,
nodeAtScreen,
segmentAtScreen,
} from "./B05_Profile_UI_RouteEdit_Input";
import { createCurveLabel } from "./B05_Profile_UI_RouteEdit_Label";
import {
centerDirectionOf,
createCurveLabel,
deflectionRad,
} from "./B05_Profile_UI_RouteEdit_Label";
import {
bindHistoryControls,
createRouteEditHistory,
@@ -199,34 +204,6 @@ export async function openRouteEditModal(
context.restore();
}
/** 등고선을 보일 화면 사각형 — 노선 경계에 `CONTOUR_BAND_M` 를 두른 것.
*
* **매 프레임 다시 잰다** — 창 크기·배율·이동이 바뀌어도 띠가 노선을 따라간다. 노선이
* 아직 없으면 null(전체를 그린다). */
function contourBandRect(): { x: number; y: number; width: number; height: number } | null {
const line = plannedLine.length ? plannedLine : planned;
if (!meta || line.length < 2) return null;
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const [x, y] of line) {
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
}
// 사업지 좌표(m)에서 띠를 두르고 화면으로 옮긴다 — y 는 화면에서 뒤집힌다.
const [left, bottom] = toScreen([minX - CONTOUR_BAND_M, minY - CONTOUR_BAND_M]);
const [right, top] = toScreen([maxX + CONTOUR_BAND_M, maxY + CONTOUR_BAND_M]);
return {
x: Math.min(left, right),
y: Math.min(top, bottom),
width: Math.abs(right - left),
height: Math.abs(bottom - top),
};
}
function draw(): void {
if (closed) return;
const style = getComputedStyle(document.documentElement);
@@ -237,7 +214,9 @@ export async function openRouteEditModal(
context.save();
// 등고선은 **노선 둘레 300m 안**에서만 그린다 — 노선과 상관없는 산줄기까지 다 그리면
// 화면이 등고선으로 덮여 노선이 안 보인다(2026-09-07 사용자 지시 ⑥).
const band = contourBandRect();
const band = meta
? contourBandRect(plannedLine.length ? plannedLine : planned, toScreen, CONTOUR_BAND_M)
: null;
if (band) {
context.beginPath();
context.rect(band.x, band.y, band.width, band.height);
@@ -293,21 +272,21 @@ export async function openRouteEditModal(
// **속을 비우고 테두리를 굵게** 그린다 — 선·노드와 색이 같으면 눈에도 안 띄고 집기도 어렵다.
context.lineWidth = 2;
curveInfo.forEach((curve) => {
// **고른 곡선에만** 손잡이를 낸다(2026-09-07 사용자 지적 ④) — 전부 내놓으면 헤어핀에서
// 노드 위를 덮어 노드를 못 집는다. 먼저 노드를 눌러 고르고, 그 다음 손잡이를 끈다.
if (curve.node_first !== picked) return;
// **늘 보인다**(2026-09-07 사용자 지) — 직선이 곡선에 닿는 자리는 손잡이이기 이전에
// **읽을 정보**다. 한때 고른 곡선만 내보였더니 「표기가 다 사라졌다」는 지적을 받았다.
// 노드를 못 집던 문제는 집기 우선순위(노드가 먼저)로 따로 풀었으므로 다 내놓아도 된다.
const on = curveOn[curve.node_first] !== false;
if (!on) return; // 곡선을 지운 자리에는 손잡이도 없다.
if (!on) return; // 곡선을 지운 자리에는 접선점도 없다.
// 고른 곡선은 속을 채워 도드라지게 — 지금 끌 수 있는 것이 무엇인지 보이게.
const isPicked = curve.node_first === picked;
[curve.start, curve.end].forEach((point) => {
const [x, y] = toScreen([point[0], point[1]]);
context.beginPath();
context.rect(
x - CURVE_HANDLE_PX,
y - CURVE_HANDLE_PX,
CURVE_HANDLE_PX * 2,
CURVE_HANDLE_PX * 2,
);
context.fillStyle = style.getPropertyValue("--map-halo") || "rgba(255,255,255,0.95)";
const size = isPicked ? CURVE_HANDLE_PX + 1 : CURVE_HANDLE_PX;
context.rect(x - size, y - size, size * 2, size * 2);
context.fillStyle = isPicked
? style.getPropertyValue("--map-route") || "#f97316"
: style.getPropertyValue("--map-halo") || "rgba(255,255,255,0.95)";
context.fill();
context.strokeStyle = style.getPropertyValue("--map-route") || "#f97316";
context.stroke();
@@ -320,7 +299,7 @@ export async function openRouteEditModal(
/** 잡기 — 셈은 `_RouteEdit_Input` 몫이고, 여기서는 지금 상태를 건네준다. */
const handleAt = (px: number, py: number): { node: number; end: "start" | "end" } | null =>
handleAtScreen(curveInfo, picked, toScreen, px, py, NODE_HIT_PX + 2);
handleAtScreen(curveInfo, toScreen, px, py, NODE_HIT_PX + 2);
const nodeAt = (px: number, py: number): number =>
nodeAtScreen(planned, toScreen, px, py, NODE_HIT_PX);
const segmentAt = (px: number, py: number): number =>
@@ -374,7 +353,7 @@ export async function openRouteEditModal(
);
}
// ── R 라벨 — 고른 꺾임점 **옆에** 뜬다(2026-09-07 사용자 지시 ③). 그리기는 `_Label` 몫 ──
// ── 곡선 라벨 — 고른 꺾임점 옆(곡선 중심 반대쪽)에 뜬다. 그리기는 `_Label` 몫 ──
const curveLabelBox = createCurveLabel(canvas.parentElement!, {
onRadius: (value) => {
if (picked < 0) return;
@@ -382,16 +361,19 @@ export async function openRouteEditModal(
// 반지름만 바꾼 것이라 노드 자리는 그대로지만, 그려진 선은 낡았다.
applyEdit(value === null ? "반지름을 자동으로 되돌렸습니다." : "반지름을 바꿨습니다.");
},
onArcLength: (value) => {
if (picked < 0) return;
// 곡선 길이 L 과 반지름 R 은 L = R·Δ 로 묶여 있다(Δ = 교각, 앞뒤 직선이 정함).
// 그래서 길이를 받으면 반지름으로 바꿔 **한 값만** 들고 간다 — 두 벌로 두면 어긋난다.
const deflection = deflectionRad(nodeInfo[picked]?.inner_angle_deg);
curveRadius[picked] = value !== null && deflection > 1e-9 ? value / deflection : null;
applyEdit(value === null ? "반지름을 자동으로 되돌렸습니다." : "곡선 길이를 바꿨습니다.");
},
onCurveOn: (on) => {
if (picked < 0) return;
curveOn[picked] = on;
applyEdit(on ? "곡선을 넣었습니다." : "곡선을 지웠습니다.");
},
onAuto: () => {
if (picked < 0) return;
curveRadius[picked] = null; // 서버가 예정노선에 맞춰 다시 고른다.
applyEdit("반지름을 자동으로 되돌렸습니다.");
},
});
/** 고른 자리에 맞춰 라벨을 옮겨 그린다. 끝점은 곡선이 없으므로 라벨을 숨긴다. */
@@ -401,13 +383,23 @@ export async function openRouteEditModal(
return;
}
const forced = curveRadius[picked];
const pickedCurve = curveInfo.find((entry) => entry.node_first === picked);
const shown = forced ?? pickedCurve?.radius_m ?? null;
const deflection = deflectionRad(nodeInfo[picked]?.inner_angle_deg);
curveLabelBox.show({
seat: picked,
at: toScreen(planned[picked]),
centerDirection: pickedCurve
? centerDirectionOf(
toScreen([pickedCurve.apex[0], pickedCurve.apex[1]]),
toScreen(pickedCurve.start),
toScreen(pickedCurve.end),
)
: null,
canvas: { width: view.width, height: view.height },
curveOn: curveOn[picked] !== false,
radiusShown:
forced ?? curveInfo.find((entry) => entry.node_first === picked)?.radius_m ?? null,
radiusShown: shown,
arcLengthShown: shown === null || deflection <= 1e-9 ? null : shown * deflection,
forced: forced !== null,
innerAngleDeg: nodeInfo[picked]?.inner_angle_deg ?? null,
minRadiusM,
+36 -6
View File
@@ -126,14 +126,14 @@ export function nodeAtScreen(
return best;
}
/** 화면 좌표에 가장 가까운 **곡선 손잡이**(시작·끝점). 없으면 null.
/** 화면 좌표에 가장 가까운 **곡선 손잡이**(접선점). 없으면 null.
*
* **고른 곡선** 본다 — 그려 보이는 것만 잡혀야 한다. 잡은 것은 곡선 목록 자리가 아니라
* **노드 번호**로 돌려준다: 목록은 고칠 때마다 새로 나므로 자리로 들면 끄는 도중 엉뚱한
* 곡선을 가리킨다. */
* 곡선 **전부**를 본다 — 접선점은 늘 그려지므로 늘 잡혀야 한다. 노드를 못 집던 문제는
* 부르는 쪽에서 **노드를 먼저** 보는 것으로 풀었다(2026-09-07).
* 잡은 것은 곡선 목록 자리가 아니라 **노드 번호**로 돌려준다: 목록은 고칠 때마다 새로 나므로
* 자리로 들면 끄는 도중 엉뚱한 곡선을 가리킨다. */
export function handleAtScreen(
curves: Array<{ node_first: number; start: [number, number]; end: [number, number] }>,
picked: number,
toScreen: ScreenOf,
px: number,
py: number,
@@ -142,7 +142,6 @@ export function handleAtScreen(
let best: { node: number; end: "start" | "end" } | null = null;
let bestDistance = hitPx;
curves.forEach((curve) => {
if (curve.node_first !== picked) return;
(["start", "end"] as const).forEach((which) => {
const point = which === "start" ? curve.start : curve.end;
const [x, y] = toScreen([point[0], point[1]]);
@@ -181,3 +180,34 @@ export function segmentAtScreen(
}
return best;
}
/** 등고선을 보일 화면 사각형 — 노선 경계에 `bandM` 를 두른 것. 노선이 없으면 null.
*
* **매 프레임 다시 잰다** — 창 크기·배율·이동이 바뀌어도 띠가 노선을 따라간다. 띠 자체는
* 미터로 정해 두므로 **창 크기와 무관**하다(2026-09-07 사용자 지시 ⑥). */
export function contourBandRect(
line: Array<[number, number]>,
toScreen: ScreenOf,
bandM: number,
): { x: number; y: number; width: number; height: number } | null {
if (line.length < 2) return null;
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const [x, y] of line) {
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
}
// 사업지 좌표(m)에서 띠를 두르고 화면으로 옮긴다 — y 는 화면에서 뒤집힌다.
const [left, bottom] = toScreen([minX - bandM, minY - bandM]);
const [right, top] = toScreen([maxX + bandM, maxY + bandM]);
return {
x: Math.min(left, right),
y: Math.min(top, bottom),
width: Math.abs(right - left),
height: Math.abs(bottom - top),
};
}
+86 -26
View File
@@ -1,15 +1,47 @@
/* =============================================================================
* B05_Profile_UI_RouteEdit_Label.ts
* 고른 꺾임점 **옆에 뜨는 R 라벨** — 반지름을 바꾸고, 곡선을 지우고 넣는다.
* 고른 꺾임점 **옆에 뜨는 곡선 라벨** — 반지름과 곡선 길이로 곡선을 만진다.
*
* 왜 옮겼나(2026-09-07 사용자 지시 ③) — 예전에는 모달 **맨 아래 줄**이었다. 지금 고른 것이
* 지도 어디인지 눈으로 이어지지 않아, 값을 바꾸면서도 어느 곡선을 만지는지 몰랐다.
* 「해당 요소를 선택하면 우측에 라벨 같은 입력이 보이게」가 사용자의 말 그대로다.
*
* 캔버스 밖으로 나가지 않게 가장자리에서 **반대쪽으로 접어 넣는다** — 노선 끝의 꺾임점을
* 골라도 입력칸이 잘리지 않아야 한다.
* **R 과 곡선 길이 두 칸이 한 쌍이다**(2026-09-07 사용자 지시) — 교각(Δ)은 앞뒤 직선이
* 정하므로 둘은 L = R·Δ 로 묶여 있다. 한쪽을 고치면 다른 쪽이 따라온다. 「반지름 자동」
* 단추는 없앴다 — 칸을 비우는 것이 곧 자동이다.
*
* **자리는 곡선 중심의 반대쪽**(2026-09-07 사용자 지시) — 중심 쪽에 두면 라벨이 곡선을
* 가린다. 상하좌우 네 방향 중 중심에서 먼 쪽에 붙이고, 캔버스 밖으로 나가면 안으로 접는다.
* ========================================================================== */
/** 그 꺾임점의 **교각 Δ**(라디안) — 내각의 나머지. 곡선 길이 L = R·Δ 에 쓴다. */
export function deflectionRad(innerAngleDeg: number | null | undefined): number {
if (innerAngleDeg === null || innerAngleDeg === undefined) return 0;
return ((180 - innerAngleDeg) * Math.PI) / 180;
}
/** 곡선 중심이 있는 **화면 방향**(단위벡터) — 라벨을 그 반대쪽에 붙이는 데 쓴다.
*
* 중심은 접선점 둘이 이루는 각의 이등분선 위에 있다. 접선점은 교각점에서 앞뒤 직선을 따라
* 뻗은 자리이므로, 두 방향의 단위벡터를 더하면 그대로 중심 쪽이다. 셋이 한 점이면 null. */
export function centerDirectionOf(
apex: [number, number],
start: [number, number],
end: [number, number],
): [number, number] | null {
const arm = (point: [number, number]): [number, number] => {
const dx = point[0] - apex[0];
const dy = point[1] - apex[1];
const length = Math.hypot(dx, dy);
return length <= 1e-9 ? [0, 0] : [dx / length, dy / length];
};
const [ux, uy] = arm(start);
const [vx, vy] = arm(end);
const sx = ux + vx;
const sy = uy + vy;
const length = Math.hypot(sx, sy);
return length <= 1e-9 ? null : [sx / length, sy / length];
}
/** 라벨과 노드 사이 여백(px). */
const GAP_PX = 14;
/** 캔버스 가장자리에서 이만큼은 띄운다(px). */
@@ -20,11 +52,16 @@ export interface CurveLabelState {
seat: number;
/** 그 꺾임점의 화면 좌표(캔버스 기준 px). */
at: [number, number];
/** **곡선 중심이 있는 쪽**(화면 기준 방향벡터). 라벨은 이 반대쪽에 붙는다.
* 곡선이 없으면 null — 그때는 오른쪽에 둔다. */
centerDirection: [number, number] | null;
/** 캔버스 크기(px) — 라벨을 안쪽으로 접어 넣는 데 쓴다. */
canvas: { width: number; height: number };
curveOn: boolean;
/** 지금 보일 반지름(m). 곡선이 없으면 null. */
radiusShown: number | null;
/** 지금 보일 곡선 길이(m) = R·Δ. 곡선이 없으면 null. */
arcLengthShown: number | null;
/** 사용자가 못박은 값인지(아니면 자동). */
forced: boolean;
innerAngleDeg: number | null;
@@ -32,12 +69,12 @@ export interface CurveLabelState {
}
export interface CurveLabelHandlers {
/** R 칸을 고쳤다 — null 이면 「자동」. */
/** R 칸을 고쳤다 — null 이면 「자동」(칸을 비운 것). */
onRadius: (value: number | null) => void;
/** 곡선 길이 칸을 고쳤다 — null 이면 「자동」. */
onArcLength: (value: number | null) => void;
/** 곡선을 지우거나 넣었다. */
onCurveOn: (on: boolean) => void;
/** 반지름을 자동으로 되돌렸다. */
onAuto: () => void;
}
export interface CurveLabel {
@@ -55,10 +92,13 @@ export function createCurveLabel(host: HTMLElement, handlers: CurveLabelHandlers
<span class="b05-routeedit__curve-label"></span>
<button type="button" class="b05-routeedit__label-toggle" data-act="curve-toggle"></button>
</div>
<label class="b05-routeedit__curve-field">R
<label class="b05-routeedit__curve-field">반지름
<input type="number" class="b05-routeedit__curve-radius" min="1" step="0.5" />
<span>m</span>
<button type="button" class="b05-routeedit__label-auto" data-act="curve-auto">자동</button>
</label>
<label class="b05-routeedit__curve-field">곡선 길이
<input type="number" class="b05-routeedit__curve-arc" min="1" step="0.5" />
<span>m</span>
</label>
<span class="b05-routeedit__curve-info"></span>`;
host.append(root);
@@ -66,7 +106,7 @@ export function createCurveLabel(host: HTMLElement, handlers: CurveLabelHandlers
const seatText = root.querySelector<HTMLElement>(".b05-routeedit__curve-label")!;
const toggle = root.querySelector<HTMLButtonElement>('[data-act="curve-toggle"]')!;
const radius = root.querySelector<HTMLInputElement>(".b05-routeedit__curve-radius")!;
const auto = root.querySelector<HTMLButtonElement>('[data-act="curve-auto"]')!;
const arc = root.querySelector<HTMLInputElement>(".b05-routeedit__curve-arc")!;
const info = root.querySelector<HTMLElement>(".b05-routeedit__curve-info")!;
// 라벨 위에서 누른 것이 캔버스로 새어 나가면 노드가 딸려 움직인다.
@@ -75,23 +115,36 @@ export function createCurveLabel(host: HTMLElement, handlers: CurveLabelHandlers
root.addEventListener("contextmenu", (event) => event.stopPropagation());
let curveOn = true;
radius.addEventListener("change", () => {
const value = Number(radius.value);
handlers.onRadius(Number.isFinite(value) && value > 0 ? value : null);
});
const numberOf = (input: HTMLInputElement): number | null => {
const value = Number(input.value);
return input.value.trim() !== "" && Number.isFinite(value) && value > 0 ? value : null;
};
radius.addEventListener("change", () => handlers.onRadius(numberOf(radius)));
arc.addEventListener("change", () => handlers.onArcLength(numberOf(arc)));
toggle.addEventListener("click", () => handlers.onCurveOn(!curveOn));
auto.addEventListener("click", () => handlers.onAuto());
function place(at: [number, number], canvas: { width: number; height: number }): void {
// 먼저 보여야 크기를 잴 수 있다 — 그 다음 자리를 잡는다.
/** 라벨을 **곡선 중심의 반대쪽**에 붙인다 — 상하좌우 넷 중 하나. */
function place(state: CurveLabelState): void {
const width = root.offsetWidth;
const height = root.offsetHeight;
// 기본은 오른쪽. 오른쪽이 좁으면 왼쪽으로 접는다(사용자 말대로 「우측에」가 기본).
let left = at[0] + GAP_PX;
if (left + width > canvas.width - EDGE_PX) left = at[0] - GAP_PX - width;
left = Math.max(EDGE_PX, Math.min(left, canvas.width - width - EDGE_PX));
let top = at[1] - height / 2;
top = Math.max(EDGE_PX, Math.min(top, canvas.height - height - EDGE_PX));
const [nx, ny] = state.at;
const direction = state.centerDirection;
let left = nx + GAP_PX; // 곡선이 없으면 오른쪽이 기본이다.
let top = ny - height / 2;
if (direction) {
const [dx, dy] = direction;
if (Math.abs(dx) >= Math.abs(dy)) {
// 중심이 오른쪽이면 라벨은 왼쪽으로.
left = dx > 0 ? nx - GAP_PX - width : nx + GAP_PX;
top = ny - height / 2;
} else {
left = nx - width / 2;
top = dy > 0 ? ny - GAP_PX - height : ny + GAP_PX;
}
}
// 캔버스 밖으로 나가면 안으로 접는다 — 노선 끝을 골라도 칸이 잘리지 않게.
left = Math.max(EDGE_PX, Math.min(left, state.canvas.width - width - EDGE_PX));
top = Math.max(EDGE_PX, Math.min(top, state.canvas.height - height - EDGE_PX));
root.style.left = `${Math.round(left)}px`;
root.style.top = `${Math.round(top)}px`;
}
@@ -103,15 +156,22 @@ export function createCurveLabel(host: HTMLElement, handlers: CurveLabelHandlers
seatText.textContent = `${state.seat + 1}번째 꺾임점`;
toggle.textContent = state.curveOn ? "곡선 지우기" : "곡선 넣기";
radius.disabled = !state.curveOn;
auto.disabled = !state.curveOn || !state.forced;
arc.disabled = !state.curveOn;
radius.value =
state.radiusShown === null ? "" : String(Math.round(state.radiusShown * 10) / 10);
arc.value =
state.arcLengthShown === null ? "" : String(Math.round(state.arcLengthShown * 10) / 10);
const inner = state.innerAngleDeg;
info.textContent = state.curveOn
? `${state.forced ? "값 지정" : "자동"}${inner ? ` · 내각 ${Math.round(inner)}°` : ""}` +
` · 법정 하한 ${state.minRadiusM}m`
` · 법정 하한 ${state.minRadiusM}m · 칸을 비우면 자동`
: "곡선 없음 — 직선이 그대로 꺾입니다";
place(state.at, state.canvas);
place(state);
// 글자가 바뀌면 상자 높이가 한 박자 늦게 자란다 — 다음 그림 직전에 한 번 더 맞춘다
// (2026-09-07 실측: 처음 잰 높이 120px, 실제 131px 라 11px 어긋났음).
requestAnimationFrame(() => {
if (!root.hidden) place(state);
});
},
hide() {
root.hidden = true;
+30 -26
View File
@@ -140,13 +140,16 @@
display: flex;
flex-direction: column;
gap: 4px;
min-width: 11rem;
padding: var(--spacing-8, 8px);
border: 1px solid var(--color-border, #e5e7eb);
border-radius: var(--radius-6, 6px);
background: var(--color-surface-2, #f9fafb);
box-shadow: 0 4px 14px rgb(0 0 0 / 25%);
font-size: var(--font-size-13, 13px);
min-width: 12rem;
padding: var(--spacing-8);
border: 1px solid var(--color-border);
border-radius: var(--radius-8, 6px);
/* 모달 본체와 **같은 토큰**을 쓴다 — `--color-surface-2` 는 이 테마에 없어 밝은
기본값으로 떨어지면서 다크 화면에서 글자가 안 읽혔다(2026-09-07 실화면). */
background: var(--color-surface-raised);
color: var(--color-text-body);
box-shadow: 0 4px 14px rgb(0 0 0 / 45%);
font-size: var(--text-caption);
}
.b05-routeedit__label-head {
@@ -156,21 +159,18 @@
gap: var(--spacing-8, 8px);
}
.b05-routeedit__label-toggle,
.b05-routeedit__label-auto {
.b05-routeedit__label-toggle {
padding: 1px 6px;
border: 1px solid var(--color-border, #e5e7eb);
border: 1px solid var(--color-border);
border-radius: var(--radius-4, 4px);
background: var(--color-surface, #fff);
color: inherit;
background: var(--color-surface);
color: var(--color-text-body);
font: inherit;
font-size: var(--font-size-12, 12px);
cursor: pointer;
}
.b05-routeedit__label-toggle:disabled,
.b05-routeedit__label-auto:disabled {
color: var(--color-text-muted, #9ca3af);
.b05-routeedit__label-toggle:disabled {
color: var(--color-text-secondary);
cursor: default;
}
@@ -180,28 +180,32 @@
}
.b05-routeedit__curve-field {
display: inline-flex;
display: flex;
align-items: center;
gap: 4px;
justify-content: space-between;
gap: 6px;
white-space: nowrap;
}
.b05-routeedit__curve-radius {
width: 4.5rem;
.b05-routeedit__curve-radius,
.b05-routeedit__curve-arc {
width: 5rem;
padding: 2px 6px;
border: 1px solid var(--color-border, #e5e7eb);
border: 1px solid var(--color-border);
border-radius: var(--radius-4, 4px);
background: var(--color-surface);
color: var(--color-text-body);
font: inherit;
text-align: right;
}
.b05-routeedit__curve-radius:disabled {
background: var(--color-surface-3, #f3f4f6);
color: var(--color-text-muted, #9ca3af);
.b05-routeedit__curve-radius:disabled,
.b05-routeedit__curve-arc:disabled {
color: var(--color-text-secondary);
}
.b05-routeedit__curve-info {
max-width: 16rem;
color: var(--color-text-muted, #6b7280);
max-width: 15rem;
color: var(--color-text-secondary);
line-height: 1.35;
}