iframe 안 CAD가 다크 색을 하드코딩하고 있어 앱의 화이트/블랙 토글과 따로 놀던 것을 고친다. 색 정의처는 앱과 같은 ui_template_theme.css 하나로 모은다. - openwebcad가 ui_template_theme.css를 직접 import — App.css의 하드코딩 색 88개를 var(--color-*)로 전량 치환, CAD 골격용 별칭 토큰만 새로 붙임 - src/theme.ts 신설: 동일 오리진 localStorage(frd_theme) + storage 이벤트로 부모 토글을 리로드 없이 추종(부모 코드 수정 없음), 시스템 폴백 유지 - 캔버스 배경·그리드는 토큰 경유, 저장된 도면 색은 라이트 모드에서 그릴 때만 휘도 보정(paintColor) — 저장 데이터는 불변 - 테마 변경 시 bumpSceneVersion()으로 정적 장면 캐시 비트맵 재생성 - Button/LayerManager의 tailwind 색 유틸 제거, index.html의 bg-* 고정 제거 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
101 lines
3.7 KiB
TypeScript
101 lines
3.7 KiB
TypeScript
/**
|
|
* 호스트 앱(Aislo) 테마를 CAD iframe에 그대로 따라 붙인다.
|
|
*
|
|
* iframe은 호스트와 **같은 오리진**이라 `localStorage["frd_theme"]`를 공유한다.
|
|
* 부모가 토글하며 값을 쓰면 이 문서에는 `storage` 이벤트가 오므로, 부모 쪽에
|
|
* 별도 전달 코드(postMessage)를 넣지 않는다. 값이 없으면 미지정 상태로 두어
|
|
* `ui_template_theme.css`의 `prefers-color-scheme` 폴백이 그대로 먹는다.
|
|
*/
|
|
|
|
import { bumpSceneVersion } from './helpers/scene-version.ts';
|
|
|
|
const THEME_STORAGE_KEY = 'frd_theme';
|
|
|
|
/** 테마가 바뀔 때마다 비우는 색 캐시 (CSS 변수 조회·휘도 보정 결과). */
|
|
let cssVarCache = new Map<string, string>();
|
|
let paintCache = new Map<string, string>();
|
|
|
|
function applyTheme(value: string | null): void {
|
|
if (value === 'dark' || value === 'light') {
|
|
document.documentElement.setAttribute('data-theme', value);
|
|
} else {
|
|
document.documentElement.removeAttribute('data-theme');
|
|
}
|
|
cssVarCache = new Map();
|
|
paintCache = new Map();
|
|
// 정적 장면 비트맵은 배경·선 색까지 구워 두므로 테마가 바뀌면 다시 굽는다.
|
|
bumpSceneVersion();
|
|
}
|
|
|
|
/** 부팅 시 1회 호출 — 현재 테마 반영 + 이후 변경 구독. */
|
|
export function syncThemeFromHost(): void {
|
|
applyTheme(localStorage.getItem(THEME_STORAGE_KEY));
|
|
window.addEventListener('storage', (event) => {
|
|
if (event.key === THEME_STORAGE_KEY) applyTheme(event.newValue);
|
|
});
|
|
// 시스템 설정 폴백으로 도는 중이면 OS 전환도 따라간다.
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
if (!document.documentElement.hasAttribute('data-theme')) applyTheme(null);
|
|
});
|
|
}
|
|
|
|
/** 토큰 값 조회 (테마 바뀔 때까지 캐시). */
|
|
export function themeColor(varName: string, fallback: string): string {
|
|
const cached = cssVarCache.get(varName);
|
|
if (cached !== undefined) return cached;
|
|
const value =
|
|
getComputedStyle(document.documentElement).getPropertyValue(varName).trim() || fallback;
|
|
cssVarCache.set(varName, value);
|
|
return value;
|
|
}
|
|
|
|
export function isLightTheme(): boolean {
|
|
const attr = document.documentElement.getAttribute('data-theme');
|
|
if (attr === 'dark') return false;
|
|
if (attr === 'light') return true;
|
|
return !window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
}
|
|
|
|
function relativeLuminance(r: number, g: number, b: number): number {
|
|
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
|
}
|
|
|
|
/**
|
|
* 도면 색 보정 — 저장된 엔티티 색은 다크 배경 기준(밝은 회색·흰색)이라
|
|
* 라이트 모드 흰 종이 위에서는 안 보인다. **그릴 때만** 휘도를 낮춰 색상(색조)은
|
|
* 유지한 채 대비를 살린다. 저장 데이터는 건드리지 않는다.
|
|
*/
|
|
export function paintColor(color: string): string {
|
|
if (!isLightTheme()) return color;
|
|
const cached = paintCache.get(color);
|
|
if (cached !== undefined) return cached;
|
|
|
|
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color);
|
|
let result = color;
|
|
if (hex) {
|
|
const raw = hex[1];
|
|
const full =
|
|
raw.length === 3
|
|
? raw
|
|
.split('')
|
|
.map((c) => c + c)
|
|
.join('')
|
|
: raw;
|
|
const r = Number.parseInt(full.slice(0, 2), 16);
|
|
const g = Number.parseInt(full.slice(2, 4), 16);
|
|
const b = Number.parseInt(full.slice(4, 6), 16);
|
|
const luminance = relativeLuminance(r, g, b);
|
|
// 흰 배경에서 묻히는 밝기(0.58 초과)만 목표 휘도 0.3으로 눌러 준다.
|
|
if (luminance > 0.58) {
|
|
const scale = 0.3 / luminance;
|
|
const to = (v: number) =>
|
|
Math.round(Math.min(255, v * scale))
|
|
.toString(16)
|
|
.padStart(2, '0');
|
|
result = `#${to(r)}${to(g)}${to(b)}`;
|
|
}
|
|
}
|
|
paintCache.set(color, result);
|
|
return result;
|
|
}
|