콘텐츠로 이동

다크 모드

lh-ui의 모든 컴포넌트는 기본적으로 밝은색과 어두운색을 지원합니다. 이것은 사후에 추가된 테마가 아니라, 토큰 시스템의 설계 전제입니다.

어두운색의 스위치는 JS가 아니라 CSS 선택器입니다:

styles/theme/common/var-dark.scss
[data-theme='dark'],
.dark {
--lh-bg-color: #2A2520;
--lh-text-primary: #E8E0D8;
// ... 118개의 덮어쓰기
}

두 가지 트리거 방식:

<!-- 방식 1: data 속성(권장, 의미가 명확) -->
<html data-theme="dark">
<!-- 방식 2: class(기존 코드 / 서드파티 전환 라이브러리와의 호환) -->
<body class="dark">

UI 계층(각 라이브러리의 ConfigProvider나 비즈니스 코드)이 이 속성 / 클래스명을 바꾸기만 하면, 모든 컴포넌트가 즉시 색을 바꿉니다 —— CSS 변수는 런타임에 평가되어 다시 렌더링의 비용이 없습니다.

function toggleDark(isDark: boolean) {
document.documentElement.dataset.theme = isDark ? 'dark' : 'light'
}

어두운색 파일은 118개만 쓰며, 185개 전체를 새로 쓰는 것이 아닙니다. 전환이 필요한 토큰만 덮어쓰고, 나머지는 밝은색 값을 따릅니다.

설계 원칙은 의미 토큰의 재지정이지, 컴포넌트별 재작성이 아닙니다:

// ✅ 좋은 방법: 어두운색에서 어두운색 의미 토큰을 가리킴
[data-theme='dark'],
.dark {
--lh-menu-item-color: var(--lh-text-primary);
--lh-table-cell-color: var(--lh-text-primary);
}
// ❌ 나쁜 방법: 어두운색에 구체적인 색을 고정
[data-theme='dark'],
.dark {
--lh-menu-item-color: rgba(255, 255, 255, 0.85); // text-primary와 분리되어, 이후 색 조정 시 두 곳을 고쳐야 함
}

이것은 다크 모드에서 가장 밟기 쉬운 함정이며, verify:styles의 7번째 항목이 존재하는 이유입니다.

var.scss에서 값이 #{$lh-foo} 형태인 토큰:

var.scss
:root {
--lh-menu-item-color: #{$lh-menu-item-color}; // SCSS 변수가 컴파일 타임에 구체적인 밝은색 값으로 치환됨
}

SCSS 변수는 컴파일 타임에 리터럴 값으로 구워지며, 다음과 동등합니다:

:root {
--lh-menu-item-color: rgba(0, 0, 0, 0.88); // 어두운색에서 자동으로 전환되지 않음!
}

결과: 어두운색 모드에서 메뉴 / 표 / 타임라인의 글자가 밝은색 값을 그대로 써서, 검은 바탕에 검은 글자로 보이지 않게 됩니다. 이런 누락은 실제로 발생했으며, 보충 기록은 0.4.0의 체인지셋을 보세요.

var-dark.scss에서 어두운색 의미 토큰을 명시적으로 다시 가리킵니다:

[data-theme='dark'],
.dark {
--lh-menu-item-color: var(--lh-text-primary);
}

연쇄 폴백은 런타임에 평가되어 안전

섹션 제목: “연쇄 폴백은 런타임에 평가되어 안전”

컴포넌트 SCSS 안의 연쇄 폴백:

.lh-button {
color: var(--lh-foo, var(--lh-bar, #333));
}

var()의 폴백은 런타임에 평가됩니다: 연쇄상의 어느 토큰에 어두운색 덮어쓰기가 있기만 하면, 연쇄 전체가 전환을 따릅니다. 반복 선언이 필요 없습니다.

대조표:

작성법 평가 시점 어두운색에서의 거동
var(--lh-foo, fallback) 런타임 ✅ 연쇄상 토큰에 어두운색 덮어쓰기가 있으면 전환
#{$lh-foo}(SCSS 보간) 컴파일 타임 ❌ 밝은색으로 구워져, var-dark에서 명시적 재지정이 필요
mix($lh-a, $lh-b) 등 SCSS 함수 컴파일 타임 ❌ 마찬가지

어두운 주색 프리셋의 명도 올리기

섹션 제목: “어두운 주색 프리셋의 명도 올리기”

5개 내장 프리셋 중 어두운 주색인 것(青花瓷 (청화백자) / 朱砂 (주사) / 水墨 (수묵))은 어두운색에서 주색이 페이지 바탕색에 “잠겨” 보입니다. buildPresetDarkVars가 자동으로 처리합니다:

판정 규칙: --lh-primary-color와 어두운색 페이지 바탕 #1A1A2E(DARK_PAGE_BG)의 대비가 3 미만(DARK_PRIMARY_CONTRAST_THRESHOLD)이면 어두운 주색으로 판정하고, 그런 뒤:

[data-theme='dark'],
.dark {
--lh-primary-color: color-mix(in srgb, <리터럴 값> 55%, #fff); /* 55% 밝게 */
--lh-primary-color-hover: color-mix(in srgb, var(--lh-primary-color) 70%, #fff);
--lh-primary-color-active: color-mix(in srgb, var(--lh-primary-color) 85%, #000);
--lh-color-on-primary: #101410; /* 밝게 한 뒤에는 흰 글자의 대비가 오히려 부족하여 먹글자로 */
--lh-primary-color-bg: color-mix(in srgb, var(--lh-primary-color) 25%, transparent);
--lh-selected-color: color-mix(in srgb, var(--lh-primary-color) 40%, #fff);
}

왜 주색 명도 올리기에 var()가 아니라 리터럴 값을 쓰는가: color-mix(in srgb, var(--lh-primary-color) 55%, #fff)는 CSS 자기 참조 순환을 만듭니다(--lh-primary-color의 값이 자기 자신을 참조). 따라서 이 한 줄은 리터럴 값을 대입하고, 나머지는 var()로 현재 주색을 따릅니다.

밝은 주색 프리셋(松花绿 (송화록) / 藤黄 (등황))은 명도 올리기가 필요 없지만, 여전히 --lh-primary-color-bg와 --lh-selected-color의 어두운색 재정의를 받습니다.

buildPresetStyleText가 생성하는 텍스트는 :root { ... } 블록이 앞, [data-theme='dark'], .dark { ... } 블록이 뒤입니다. 같은 명시도에서는 뒤가 이기므로, 밝은 주색 프리셋이 :root에 선언한 --lh-selected-color의 밝은색 값이 어두운색으로 새어 들어가지 않습니다.

이것이 “<style>만 쓸 수 있고 인라인 style는 쓸 수 없는”의 근원이기도 합니다 —— 인라인 style의 명시도 1,0,0,0,0이 var-dark.scss의 어두운색 변수 전체를 누르고, 어두운색이 곧바로 무효가 됩니다(테마 시스템 개요 참고).

主色与衍生语义

令牌明色暗色SCSS 源备注
--lh-primary-color #3D649E$lh-primary
--lh-primary-color-hover #5078AB$lh-primary-hover
--lh-primary-color-active #2B4C7C$lh-primary-active
--lh-primary-color-bg color-mix(in srgb, var(--lh-primary-color) 25%, transparent)$lh-primary-light
--lh-primary-color-border #2A3A4A$lh-primary-lighter
--lh-color-on-primary #fff$lh-on-primary

状态

令牌明色暗色SCSS 源备注
--lh-hover-bg-color rgba(255, 255, 255, 0.08)$lh-hover-bg
--lh-selected-color color-mix(in srgb, var(--lh-primary-color) 40%, #fff)—

功能色

令牌明色暗色SCSS 源备注
--lh-success-color #6FA36E$lh-success
--lh-success-color-hover #82B581$lh-success-hover
--lh-success-color-active #5B8C5A$lh-success-active
--lh-success-color-bg #1A2A1A$lh-success-light
--lh-warning-color #D4B76F$lh-warning
--lh-warning-color-hover #E0C680$lh-warning-hover
--lh-warning-color-active #C4A35A$lh-warning-active
--lh-warning-color-bg #2A251A$lh-warning-light
--lh-danger-color #C45050$lh-danger
--lh-danger-color-hover #D46060$lh-danger-hover
--lh-danger-color-active #B23A3A$lh-danger-active
--lh-danger-color-bg #2A1A1A$lh-danger-light
--lh-info-color #8F8F8F$lh-info
--lh-info-color-hover #A0A0A0$lh-info-hover
--lh-info-color-active #7A7A7A$lh-info-active
--lh-info-color-bg #1A1A1A$lh-info-light

国风装饰色

令牌明色暗色SCSS 源备注
--lh-gold-color #D4A843$lh-gold
--lh-gold-color-hover #E8C35C$lh-gold-hover
--lh-gold-color-active #B8860B$lh-gold-active
--lh-gold-color-bg #2A241A$lh-gold-light
--lh-gold-color-border #3A2E1A$lh-gold-lighter
--lh-vermilion-color #E85D4A$lh-vermilion
--lh-vermilion-color-hover #FF7B6B$lh-vermilion-hover
--lh-vermilion-color-active #C43A31$lh-vermilion-active
--lh-vermilion-color-bg #2A1A18$lh-vermilion-light
--lh-jade-color #7AAE7A$lh-jade
--lh-jade-color-hover #9CC89C$lh-jade-hover
--lh-jade-color-active #5B8C5A$lh-jade-active
--lh-jade-color-bg #1A2A1A$lh-jade-light
--lh-decoration-color rgba(212, 168, 67, 0.08)$lh-decoration-color

文本

令牌明色暗色SCSS 源备注
--lh-text-primary #E8E0D8$lh-text-primary
--lh-text-regular #C8C0B8$lh-text-regular
--lh-text-secondary #A09890$lh-text-secondary
--lh-text-placeholder #7A7270$lh-text-placeholder
--lh-text-disabled #5A5250$lh-text-disabled

边框

令牌明色暗色SCSS 源备注
--lh-border-color-base #3A3530$lh-border-color-base
--lh-border-color-light #302B28$lh-border-color-light
--lh-border-color-lighter #2A2520$lh-border-color-lighter
--lh-border-color-extra-light #242018$lh-border-color-extra-light

圆角

令牌明色暗色SCSS 源备注
--lh-border-radius-base 沿用明色$lh-border-radius-base
--lh-border-radius-small 沿用明色$lh-border-radius-small
--lh-border-radius-round 沿用明色$lh-border-radius-round
--lh-border-radius-circle 沿用明色$lh-border-radius-circle

背景

令牌明色暗色SCSS 源备注
--lh-bg-color #2A2520$lh-bg-color
--lh-bg-color-page #1E1A16$lh-bg-color-page
--lh-bg-color-disabled #242018$lh-bg-color-disabled
--lh-bg-color-overlay rgba(10, 8, 6, 0.7)$lh-bg-color-overlay

阴影

令牌明色暗色SCSS 源备注
--lh-box-shadow-base 0 2px 12px 0 rgba(0, 0, 0, 0.5)$lh-box-shadow-base
--lh-box-shadow-light 0 2px 4px 0 rgba(0, 0, 0, 0.4)$lh-box-shadow-light
--lh-box-shadow-lighter 0 0 4px 0 rgba(0, 0, 0, 0.3)$lh-box-shadow-lighter

字体

令牌明色暗色SCSS 源备注
--lh-font-family 沿用明色$lh-font-family
--lh-font-size-extra-large 沿用明色$lh-font-size-extra-large
--lh-font-size-large 沿用明色$lh-font-size-large
--lh-font-size-medium 沿用明色$lh-font-size-medium
--lh-font-size-base 沿用明色$lh-font-size-base
--lh-font-size-small 沿用明色$lh-font-size-small
--lh-font-size-extra-small 沿用明色$lh-font-size-extra-small
--lh-font-weight-primary 沿用明色$lh-font-weight-primary
--lh-font-weight-bold 沿用明色$lh-font-weight-bold
--lh-font-line-height-primary 沿用明色$lh-font-line-height-primary

组件级 · control

令牌明色暗色SCSS 源备注
--lh-control-height-mini 沿用明色$lh-control-height-mini
--lh-control-height-small 沿用明色$lh-control-height-small
--lh-control-height-medium 沿用明色$lh-control-height-medium
--lh-control-height-large 沿用明色$lh-control-height-large
--lh-control-height-x-large 沿用明色$lh-control-height-x-large

间距

令牌明色暗色SCSS 源备注
--lh-spacing-xs 沿用明色$lh-spacing-xs
--lh-spacing-sm 沿用明色$lh-spacing-sm
--lh-spacing-md 沿用明色$lh-spacing-md
--lh-spacing-base 沿用明色$lh-spacing-base
--lh-spacing-lg 沿用明色$lh-spacing-lg
--lh-spacing-xl 沿用明色$lh-spacing-xl
--lh-spacing-xxl 沿用明色$lh-spacing-xxl

层级

令牌明色暗色SCSS 源备注
--lh-z-index-normal 沿用明色$lh-z-index-normal
--lh-z-index-dropdown 沿用明色$lh-z-index-dropdown
--lh-z-index-sticky 沿用明色$lh-z-index-sticky
--lh-z-index-fixed 沿用明色$lh-z-index-fixed
--lh-z-index-modal-backdrop 沿用明色$lh-z-index-modal-backdrop
--lh-z-index-modal 沿用明色$lh-z-index-modal
--lh-z-index-popover 沿用明色$lh-z-index-popover
--lh-z-index-tooltip 沿用明色$lh-z-index-tooltip

过渡

令牌明色暗色SCSS 源备注
--lh-transition-duration-fast 沿用明色$lh-transition-duration-fast
--lh-transition-duration-base 沿用明色$lh-transition-duration-base
--lh-transition-duration-slow 沿用明色$lh-transition-duration-slow
--lh-transition-function-ease-in-out 沿用明色$lh-transition-function-ease-in-out

禁用态

令牌明色暗色SCSS 源备注
--lh-disabled-bg-color #242018$lh-disabled-bg-color
--lh-disabled-text-color #5A5250$lh-disabled-text-color
--lh-disabled-border-color #302B28$lh-disabled-border-color
--lh-disabled-opacity 0.5$lh-disabled-opacity

组件级 · popover

令牌明色暗色SCSS 源备注
--lh-popover-bg-color #2A2520$lh-popover-bg-color
--lh-popover-border-color #3A3530$lh-popover-border-color
--lh-popover-border-radius 沿用明色$lh-popover-border-radius
--lh-popover-padding 沿用明色$lh-popover-padding
--lh-popover-title-color #E8E0D8$lh-popover-title-color
--lh-popover-content-color #C8C0B8$lh-popover-content-color
--lh-popover-arrow-size 沿用明色$lh-popover-arrow-size

组件级 · popconfirm

令牌明色暗色SCSS 源备注
--lh-popconfirm-padding 沿用明色$lh-popconfirm-padding
--lh-popconfirm-icon-size 沿用明色$lh-popconfirm-icon-size

组件级 · drawer

令牌明色暗色SCSS 源备注
--lh-drawer-bg-color #2A2520$lh-drawer-bg-color
--lh-drawer-border-color #3A3530$lh-drawer-border-color
--lh-drawer-header-padding 沿用明色$lh-drawer-header-padding
--lh-drawer-body-padding 沿用明色$lh-drawer-body-padding
--lh-drawer-footer-padding 沿用明色$lh-drawer-footer-padding
--lh-drawer-title-color #E8E0D8$lh-drawer-title-color
--lh-drawer-close-size 沿用明色$lh-drawer-close-size

组件级 · dropdown

令牌明色暗色SCSS 源备注
--lh-dropdown-bg-color #2A2520$lh-dropdown-bg-color
--lh-dropdown-border-color #3A3530$lh-dropdown-border-color
--lh-dropdown-border-radius 沿用明色$lh-dropdown-border-radius
--lh-dropdown-padding 沿用明色$lh-dropdown-padding
--lh-dropdown-item-height 沿用明色$lh-dropdown-item-height
--lh-dropdown-item-padding 沿用明色$lh-dropdown-item-padding
--lh-dropdown-item-color #C8C0B8$lh-dropdown-item-color
--lh-dropdown-item-hover-bg-color var(--lh-hover-bg-color)$lh-dropdown-item-hover-bg-color
--lh-dropdown-divider-color #302B28$lh-dropdown-divider-color

组件级 · message

令牌明色暗色SCSS 源备注
--lh-message-bg #2A2520$lh-message-bg-color
--lh-message-border #3A3530$lh-message-border-color
--lh-message-color #C8C0B8$lh-message-content-color
--lh-message-shadow 沿用明色$lh-message-shadow

组件级 · notification

令牌明色暗色SCSS 源备注
--lh-notification-bg #2A2520$lh-notification-bg-color
--lh-notification-border #3A3530$lh-notification-border-color
--lh-notification-title-color #E8E0D8$lh-notification-title-color
--lh-notification-content-color #C8C0B8$lh-notification-content-color
--lh-notification-width 沿用明色$lh-notification-width
--lh-notification-gap 沿用明色$lh-notification-gap
--lh-notification-shadow 沿用明色$lh-notification-shadow

组件级 · loading

令牌明色暗色SCSS 源备注
--lh-loading-bg rgba(10, 8, 6, 62%)$lh-loading-bg-color
--lh-loading-text-color #C8C0B8$lh-loading-text-color

组件级 · timeline

令牌明色暗色SCSS 源备注
--lh-timeline-line #302B28$lh-timeline-line
--lh-timeline-dot var(--lh-info-color)$lh-timeline-dot
--lh-timeline-timestamp var(--lh-text-secondary)$lh-timeline-timestamp

组件级 · collapse

令牌明色暗色SCSS 源备注
--lh-collapse-border #302B28$lh-collapse-border

组件级 · rate

令牌明色暗色SCSS 源备注
--lh-rate-active var(--lh-warning-color)$lh-rate-active

组件级 · segmented

令牌明色暗色SCSS 源备注
--lh-segmented-bg #242018$lh-segmented-bg

组件级 · calendar

令牌明色暗色SCSS 源备注
--lh-calendar-border var(--lh-border-color-lighter)$lh-calendar-border

组件级 · affix

令牌明色暗色SCSS 源备注
--lh-affix-z-index 沿用明色$lh-affix-z-index

组件级 · backtop

令牌明色暗色SCSS 源备注
--lh-backtop-z-index 沿用明色$lh-backtop-z-index

组件级 · menu

令牌明色暗色SCSS 源备注
--lh-menu-bg #242018$lh-menu-bg
--lh-menu-padding 沿用明色$lh-menu-padding
--lh-menu-border #302B28$lh-menu-border
--lh-menu-height 沿用明色$lh-menu-height
--lh-menu-collapse-width 沿用明色$lh-menu-collapse-width
--lh-menu-item-height 沿用明色$lh-menu-item-height
--lh-menu-item-padding 沿用明色$lh-menu-item-padding
--lh-menu-item-color var(--lh-text-primary)$lh-menu-item-color
--lh-menu-item-hover-bg #302B28$lh-menu-item-hover-bg
--lh-menu-item-active-color var(--lh-selected-color)$lh-menu-item-active-color
--lh-menu-item-active-bg var(--lh-primary-color-bg)$lh-menu-item-active-bg
--lh-menu-arrow-color var(--lh-text-secondary)$lh-menu-arrow-color
--lh-menu-z-index 沿用明色$lh-menu-z-index

组件级 · table

令牌明色暗色SCSS 源备注
--lh-table-bg #242018$lh-table-bg
--lh-table-color var(--lh-text-regular)$lh-table-color
--lh-table-header-bg #2A2420$lh-table-header-bg
--lh-table-header-color var(--lh-text-primary)$lh-table-header-color
--lh-table-border #302B28$lh-table-border
--lh-table-row-hover-bg #2A2420$lh-table-row-hover-bg
--lh-table-stripe-bg color-mix(in srgb, var(--lh-bg-color-page) 60%, transparent)$lh-table-stripe-bg
--lh-table-cell-padding 沿用明色$lh-table-cell-padding

组件级 · layout

令牌明色暗色SCSS 源备注
--lh-layout-header-height 沿用明色$lh-layout-header-height
--lh-layout-header-bg #242018$lh-layout-header-bg
--lh-layout-aside-width 沿用明色$lh-layout-aside-width
--lh-layout-aside-bg #242018$lh-layout-aside-bg
--lh-layout-main-bg #1C1815$lh-layout-main-bg
--lh-layout-border #302B28$lh-layout-border

组件级 · row

令牌明色暗色SCSS 源备注
--lh-row-gutter 沿用明色$lh-row-gutter

组件级 · tree

令牌明色暗色SCSS 源备注
--lh-tree-color #D8D0C8$lh-tree-color
--lh-tree-padding 沿用明色$lh-tree-padding
--lh-tree-node-height 沿用明色$lh-tree-node-height
--lh-tree-node-hover-bg #2A2420$lh-tree-node-hover-bg
--lh-tree-node-selected-color var(--lh-selected-color)$lh-tree-node-selected-color
--lh-tree-node-selected-bg var(--lh-primary-color-bg)$lh-tree-node-selected-bg
--lh-tree-arrow-color var(--lh-text-secondary)$lh-tree-arrow-color

组件级 · transfer

令牌明色暗色SCSS 源备注
--lh-transfer-panel-width 沿用明色$lh-transfer-panel-width
--lh-transfer-panel-height 沿用明色$lh-transfer-panel-height
--lh-transfer-bg #242018$lh-transfer-bg
--lh-transfer-border #302B28$lh-transfer-border

组件级 · upload

令牌明色暗色SCSS 源备注
--lh-upload-bg #242018$lh-upload-bg
--lh-upload-border var(--lh-border-color-base)$lh-upload-border
--lh-upload-trigger-padding 沿用明色$lh-upload-trigger-padding

组件级 · anchor

令牌明色暗色SCSS 源备注
--lh-anchor-color var(--lh-text-regular)$lh-anchor-color

组件级 · image

令牌明色暗色SCSS 源备注
--lh-image-viewer-mask rgba(0, 0, 0, 80%)—

无障碍焦点环

令牌明色暗色SCSS 源备注
--lh-focus-color #7FA6D9$lh-primary
--lh-focus-ring-width 沿用明色—
--lh-focus-ring-offset 沿用明色—

어두운색 완전성을 검증하는 방법

섹션 제목: “어두운색 완전성을 검증하는 방법”

세 가지 수단, 약한 것부터 강한 것 순:

  1. pnpm verify:styles의 7번째 항목(어두운색 도달성): 정적 스캔으로 “밝은색이 구워졌으나 var-dark에 덮어쓰기가 없는” 토큰을 찾습니다. 이것이 CI 게이트입니다.
  2. pnpm verify:theme(AA 4.5:1): es/ 산출물을 읽어, 프리셋 테마의 밝은색 / 어두운색 양쪽에서의 대비를 계산합니다. 먼저 pnpm build가 필요합니다. CI는 실행하지 않으며, 수동으로 지킵니다.
  3. 시각 확인: docs 사이트 / 각 라이브러리 playground에서 다크 모드로 전환하여 컴포넌트별로 살핍니다.

새 토큰을 추가할 때의 자체 점검 목록:

  • 이 토큰은 어두운색에서 전환이 필요한가?
  • 필요하다면 var-dark.scss에 덮어쓰기를 보충했는가?
  • 그 값이 #{$lh-*} 보간인가? (그렇다 → 반드시 어두운색 덮어쓰기 보충)
  • 컴포넌트에서 이것을 소비하는 것이 var(--lh-x, fallback) 연쇄인가?

어두운색 완전성은 0.4.0에서 한 차례 체계적으로 보충되었습니다(메뉴 / 표 / 타임라인 등의 글자가 보이지 않던 문제, 체인지셋 참고). 나머지 알려진 항목은 “높이 토큰 전문 작업”과 함께 향후 처리 예정입니다.

Copyright © 2024-2026 FengMo WenXue