@iyulab/data-components 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,163 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.0] - 2026-08-02
4
+
5
+ ### Changed
6
+
7
+ - **UDataView 가 역할 토큰만 읽는다.** 다크 전용 규칙 92줄을 제거하고, 팔레트 참조
8
+ 4곳을 역할 토큰으로 옮겼다(`--u-blue-600` → `--u-primary-color`, `--u-blue-200` →
9
+ `--u-primary-color-weakest`). **두 테마의 계산색은 그대로다** — 브라우저에서
10
+ 변경 전후 계산값 스냅샷을 대조해 확인했다.
11
+
12
+ 제거한 다크 블록의 24개 선언은 전부 base 규칙과 **같은 토큰**을 가리키고 있었다.
13
+ 역할 토큰(`--u-bg-color`, `--u-border-color`, `--u-txt-color-weak` …)은 두 테마에서
14
+ 서로 다른 팔레트 단에 매핑돼 있어(예: `--u-bg-color` = neutral-0 라이트 /
15
+ neutral-100 다크) 테마 보정이 이미 토큰 층에 들어 있다. 따라서 그 블록은 토큰 시트가
16
+ 로드된 환경에서 계산값을 하나도 바꾸지 못했다.
17
+
18
+ - **USimpleSheet 의 다크 블록을 24개 선언 → 6개로 줄였다.** 지운 것은 base 와 같은 토큰을
19
+ 가리켜 계산값을 바꾸지 못하던 규칙들이다. **남긴 6개는 죽은 규칙이 아니다** — 유채색
20
+ 표면(계산 셀·선택된 열 헤더·강조된 드롭다운 항목)이며, 역할 층에 유채색 표면 토큰이
21
+ 없어서 손으로 메운 보정이다. 비활성화하면 실제로 색이 달라짐을 확인했다.
22
+ - **USimpleSheet 그림자 2곳이 `--u-shadow-color-*` 를 읽는다.** 종전에는 라이트/다크가
23
+ 각각 하드코딩된 `rgba(0,0,0,0.15)` / `0.5` 였다. 역할 토큰은 테마별로 불투명도가
24
+ 튜닝돼 있다(0.16/0.45, 0.12/0.35) — 다크에서 그림자가 아주 조금 옅어진다.
25
+ - **USimpleSheet 읽기전용 셀이 `--u-bg-color-disabled` 를 읽는다.** 이 토큰의 테마별
26
+ 매핑(neutral-50 라이트 / neutral-100 다크)이 종전 다크 블록이 손으로 쓴 단과 **정확히
27
+ 같다** — 그래서 해당 다크 규칙이 불필요해졌다. 계산색은 두 테마 모두 그대로다.
28
+
29
+ ### Added
30
+
31
+ - **UI 문구를 프로퍼티로 열었다** — URichTable 의 `loadingMessage`·`filterPlaceholder`·
32
+ `filterAllLabel`·`addRowLabel`·`pageInfoFormatter`, USimpleSheet 의 `noMatchMessage`.
33
+ 종전에는 `emptyMessage` 하나만 열려 있고 나머지는 한국어로 하드코딩돼 있었다.
34
+ `pageInfoFormatter` 만 함수인 것은 **언어마다 어순이 달라** 템플릿 문자열로는 열 수
35
+ 없기 때문이다.
36
+
37
+ ⚠로케일 레지스트리를 도입하지는 않았다 — 이 패키지에 없는 레이어를 새로 들이는
38
+ 일이고, 현재 수요가 그것을 요구할 만큼 모이지 않았다. 기존 `emptyMessage` 어법을
39
+ 따르는 순수 가산이다.
40
+
41
+ ### Changed
42
+
43
+ - **업스트림 토큰 채택 (`@iyulab/components` 1.15.0).** 이 릴리스에서 소비 측 우회 둘이
44
+ 사라졌다:
45
+ - **보조 텍스트 AA 우회 해제** — URichTable 4곳이 역할 토큰을 피해 고정값을 쓰고
46
+ 있었다(`--u-txt-color-weak` 가 라이트에서 2.68 로 AA 미달이었기 때문). 업스트림이
47
+ neutral-600(4.61)으로 고쳐져 **역할 토큰으로 되돌렸다.** 그 넷을 위해 남겨 둔
48
+ 다크 규칙도 함께 사라졌다 — 이제 이 색은 테마 오버라이드를 따른다.
49
+ - **유채색 표면 토큰 채택** — `--u-primary-bg-color`·`--u-success-bg-color`·
50
+ `--u-danger-bg-color` 로 옮겼다. 이 토큰들은 두 테마가 **같은 세기**가 되도록
51
+ 짝지어져 있어(라이트 shade-0 ↔ 다크 shade-100), 손으로 쓰던 다크 보정 2개가
52
+ 불필요해졌다. **다크에서 선택된 행·계산 셀 틴트가 제 세기로 보인다** —
53
+ 종전에는 팔레트 유채색이 다크에서 짓눌려 절반 이하로 옅었다.
54
+
55
+ ⇒ 다크 전용 규칙이 **8 → 3**, 팔레트 직접 참조가 **40 → 31** 로 줄었다.
56
+
57
+ ### Fixed
58
+
59
+ - **URichTable 템플릿에 인라인으로 박혀 있던 색 3곳**(행 메뉴 `⋯`, 행 오류 줄, 새 행
60
+ 표시)을 클래스 + 역할 토큰으로 옮겼다. `style="…#94a3b8"` 형태라 **스타일 시트만
61
+ 훑는 정리에는 보이지 않는 자리**였다. 색이 조금 바뀐다 — 행 오류 줄 배경
62
+ `#FEF2F2` → `#FFEBEE`, 행 메뉴 `#94A3B8` → `#9E9E9E`, 새 행 표시 `#86EFAC` →
63
+ `#A5D6A7`. (이 셋은 계산색 스냅샷이 덮지 못한다 — 렌더 조건이 내부 상태다.)
64
+
65
+ - ★**URichTable 에서 `_id` 없는 행을 넘기면 한 행 선택이 전체 선택으로 번지던 문제.**
66
+ 선택·확장·행 오류 상태가 전부 `_id` 로 추적되는데 그 값을 **어디서도 부여하지 않아**,
67
+ 소비자가 넣지 않으면 모든 행이 `undefined` 로 같은 키를 갖게 됐다. 이제 `_id` 가 없으면
68
+ **위치로 식별하고 개발 경고**를 낸다. 위치 기반 식별은 데이터가 재정렬·재페이징되면
69
+ 선택이 다른 행으로 옮겨가므로 안전망일 뿐이다 — 정렬·필터·페이지가 소비자 책임인
70
+ 컴포넌트이니 `_id` 를 주는 것이 옳다.
71
+
72
+ ### Documentation
73
+
74
+ - **`docs/URichTable.md` 신설.** 종전에는 README 가 *"문서 작성 예정"* 이라며 `types.ts`
75
+ 를 가리키고 있었다. 타입만으로는 드러나지 않는 두 계약을 먼저 적었다 — ⑴ `data` 는
76
+ 그대로 렌더되며 **정렬·필터·페이지는 소비자 책임**이다(헤더 클릭은 이벤트만 낸다),
77
+ ⑵ 각 행에 `_id` 가 필요하다. 속성·이벤트 9종·키보드/클립보드 표 포함.
78
+ - **README 의 Theming 절을 실제 동작에 맞게 고쳤다** — 토큰 시트가 필요하다는 것,
79
+ 없을 때의 폴백 규약, 브랜드 색 한 줄 변경, 그리고 알려진 제약 둘(AA 미달, Chromium
80
+ 한정 규칙)을 명시했다.
81
+
82
+ - **`--u-blue-50` 은 토큰 시트에 정의가 없다.** USimpleSheet 4곳(계산 셀·선택 셀·강조된
83
+ 드롭다운 항목)이 이 이름을 참조해 **두 테마 모두에서** 항상 리터럴 폴백으로 떨어지고
84
+ 있었다 — 즉 토큰 층과 무관하게 고정색이었다. 팔레트의 틴트 끝인 `--u-blue-0` 로
85
+ 고쳤다. **라이트에서 색이 바뀐다**: `#EFF6FF` → `#E3F2FD`. 다크는 종전과 동일하다.
86
+
87
+ - ★**URichTable 이 디자인 토큰을 읽기 시작한다 — 라이트 재도색.** 이 컴포넌트는 base
88
+ 레이어가 토큰을 **하나도** 쓰지 않고 고정 hex 를 박아 두었다. 즉 라이트에서 테마·브랜드
89
+ 설정에 전혀 반응하지 않았고, 다크는 그 위에 얹은 별도 재구현(57개 선언)이었다.
90
+ base 를 역할 토큰으로 옮기면서 다크 블록은 **2개 규칙**만 남았다.
91
+
92
+ **라이트에서 색이 바뀐다** — 주요 항목:
93
+
94
+ | 자리 | 전 | 후 |
95
+ |---|---|---|
96
+ | 테두리 | `#E2E8F0` | `#E0E0E0` (`--u-border-color`) |
97
+ | 입력 테두리 | `#D1D5DB` | `#E0E0E0` (`--u-input-border-color`) |
98
+ | 툴바·페이지네이션 배경 | `#F8FAFC` | `#FAFAFA` |
99
+ | 헤더 배경 | `#F1F5F9` | `#F5F5F5` |
100
+ | 주 버튼 | `#3B82F6` | `#1E88E5` (`--u-primary-color`) |
101
+ | 성공 버튼 | `#10B981` | `#43A047` (`--u-success-color`) |
102
+ | 선택된 행 | `#EFF6FF` | `#E3F2FD` |
103
+ | 편집·필터 행 | `#FEFCE8` | `#FFFDE7` |
104
+ | 오류 행 | `#FEF2F2` | `#FFEBEE` |
105
+ | 새 행 | `#F0FDF4` | `#E8F5E9` |
106
+
107
+ ⇒ 이제 `:root { --u-primary-color: … }` 한 줄로 이 표의 버튼·강조색이 따라온다.
108
+
109
+ **다크에서는 성공 버튼만 바뀐다**(green-500 → `--u-success-color`). 나머지가 종전
110
+ 값 그대로임을 계산색 스냅샷으로 확인했다 — 두 테마 × 14개 선택자(상태 틴트 포함:
111
+ 선택된 행·필터 줄·새 행). 스냅샷이 덮지 못한 자리는 `tbody tr.error`(내부 상태 필요)
112
+ 와 `.dropdown-item.highlighted` 두 곳이다.
113
+
114
+ ⚠**`:host` 가 이제 글자색을 정한다.** 종전에는 base 에 `color` 선언이 없어 페이지에서
115
+ **상속**받았고, 다크 블록만 그것을 덮었다. 이제 base 가 `--u-txt-color` 를 읽는다.
116
+ 토큰 시트를 쓰는 환경에서는 개선이지만, 시트 없이 페이지 글자색을 직접 지정하던
117
+ 소비자는 그 상속을 잃는다 — 필요하면 `--u-txt-color` 를 지정하세요.
118
+
119
+ ⚠**Firefox/Safari 에서 다크가 동작하기 시작한다.** 종전에는 다크가 전부
120
+ `:host-context()` 안에 있었고 그 선택자는 Chromium 전용이라, 다른 브라우저에서는
121
+ **다크 모드가 아예 적용되지 않았다.** 이제 색의 대부분이 토큰 층에서 테마를 따르므로
122
+ 전 브라우저에서 동작한다. 남은 2개 규칙(표면 높이·보조 텍스트)만 Chromium 한정으로
123
+ 남는다.
124
+
125
+ ### Removed
126
+
127
+ - **UDataView 의 `theme` 속성이 더 이상 색을 바꾸지 않는다.**
128
+ 토큰 시트를 로드한 환경에서는 원래부터 효과가 없었다(위 참조). 시트를 로드하지 않는
129
+ 환경에서는 요소에 `theme="dark"` 를 걸면 다크 고정값이 적용됐으나, 이제는 적용되지
130
+ 않는다 — 토큰 미공급 시 폴백은 라이트 기준 고정값이라는 규약을 따른다.
131
+ **다크가 필요하면 토큰 시트를 로드할 것**(`@iyulab/components/styles/tokens.css`).
132
+ 문서 단위 다크(`<html theme="dark">`)는 종전과 동일하게 동작한다.
133
+
134
+ ### Added
135
+
136
+ - 브라우저 테스트 프로젝트(playwright chromium). 계산색을 실제로 측정한다 —
137
+ 기존 유닛 테스트는 스타일 **문자열**에서 셀렉터를 세므로, 규칙이 적혀 있음은 보여도
138
+ 그 규칙이 무언가를 바꾸는지는 말하지 못한다. 위 24개 선언이 정확히 그 사각에 있었다.
139
+ - `data-view-colors.browser.test.ts` — 두 테마 계산색 스냅샷 회귀망
140
+ - `theme-resolution.browser.test.ts` — 역할 토큰의 테마별 매핑, 팔레트 틴트 비대칭
141
+
142
+ ### Known issues
143
+
144
+ - **유채색에는 테마 보정 층이 없다.** 역할 토큰의 중립 계열 17종은 두 테마에서 다른
145
+ 팔레트 단에 매핑되지만, `--u-primary/success/danger/warning-*` 는 양 테마가 같은 단을
146
+ 가리킨다. 그런데 팔레트의 틴트 세기는 계열마다 비대칭이다 — 바탕 대비 델타 실측:
147
+ 중립은 다크가 라이트의 1.34~1.62배, 청색은 0.38~0.54배, 적색은 0.25~0.58배,
148
+ 황색은 4.2~6.1배. 그래서 유채색 표면을 쓰는 컴포넌트는 다크 보정을 손으로 써야 한다.
149
+ - **"주색 위의 글자" 역할 토큰이 없다.** 활성 뷰 버튼은 다크에서 대비 3.45 이며
150
+ 흰 글자였다면 6.09 다. 팔레트를 직접 바꾸면 게시된 시각이 움직이므로 보류했다.
151
+ - **UI 문자열이 한국어로 하드코딩돼 있다** — `URichTable` 의 `로딩 중...`·`필터...`·
152
+ `전체`·`전체 N건 중 X-Y 표시`, `USimpleSheet` 의 `일치하는 항목 없음`. `emptyMessage`
153
+ 만 프로퍼티로 열려 있고 나머지는 소비자가 바꿀 수 없다. 이 패키지에는 로케일 레이어가
154
+ 없어 단순 프로퍼티 추가로 갈지 로케일 레지스트리를 도입할지가 설계 결정이다.
155
+ - ★**`--u-txt-color-weak` 의 라이트 매핑이 WCAG AA 에 미달한다** — neutral-500(#9E9E9E)
156
+ = 흰 배경 대비 **2.68**(AA 기준 4.5). 다크는 neutral-700 = **5.43** 으로 정상이므로
157
+ **라이트 한쪽만의 결함**이다. 그래서 URichTable 의 보조 텍스트 4곳은 역할 토큰으로
158
+ 옮기지 못했다(옮기면 4.76 → 2.68 로 후퇴). 업스트림에서 라이트 매핑을
159
+ **neutral-600(4.61)** 으로 한 단 내리면 해소된다.
160
+
3
161
  ## [0.7.1] - 2026-08-01
4
162
 
5
163
  ### Documentation
package/README.md CHANGED
@@ -124,12 +124,12 @@ const sheet = document.querySelector('u-simple-sheet'); // USimpleSheet | null
124
124
 
125
125
  - [USimpleSheet](./docs/USimpleSheet.md)
126
126
  - [UDataView](./docs/UDataView.md)
127
- - URichTable — 문서 작성 예정([types.ts](./src/components/u-rich-table/types.ts) 참고)
127
+ - [URichTable](./docs/URichTable.md)
128
128
  - [UDataGrid 마이그레이션 가이드](./docs/migrating-from-datagrid.md)
129
129
 
130
130
  ## Theming
131
131
 
132
- `@iyulab/components`의 테마 시스템을 사용합니다. 라이트/다크 모드 자동 지원.
132
+ `@iyulab/components`의 디자인 토큰을 읽습니다. **토큰 시트가 문서에 있어야 테마가 동작합니다.**
133
133
 
134
134
  ```typescript
135
135
  import { Theme } from '@iyulab/components';
@@ -138,6 +138,40 @@ Theme.init({ default: 'system' });
138
138
  Theme.set('dark'); // 'light' | 'dark' | 'system'
139
139
  ```
140
140
 
141
+ `Theme.init()`을 쓰지 않는다면 정적 진입점을 대신 임포트하세요:
142
+
143
+ ```typescript
144
+ import '@iyulab/components/styles/tokens.css';
145
+ ```
146
+
147
+ ### 토큰 시트가 없으면
148
+
149
+ 컴포넌트는 렌더되지만 **라이트 기준 고정값**으로 그려지며, 다크 테마와 테마 변수
150
+ 오버라이드가 적용되지 않습니다. 이는 의도된 폴백 규약입니다.
151
+
152
+ ### 브랜드 색 바꾸기
153
+
154
+ 색은 역할 토큰을 통해 읽으므로, 주색 한 줄로 버튼·선택 강조·포커스가 함께 따라옵니다:
155
+
156
+ ```css
157
+ :root { --u-primary-color: #7B1FA2; }
158
+ ```
159
+
160
+ ### 알려진 제약
161
+
162
+ - **표면 높이를 뜻하는 토큰이 없습니다** — 배경 역할 토큰은 상호작용 상태 이름
163
+ (`-hover`/`-active`/`-disabled`)뿐이라 "바탕보다 한 단 올라온 면"을 표현할 수 없습니다.
164
+ 라이트에서는 팔레트 음영이 흰 바탕 위에서 충분히 읽히지만 다크에서는 거의 검정이라,
165
+ 툴바·표 헤더 등 몇 자리는 `:host-context()`로 보정합니다. **이 선택자는
166
+ Firefox/Safari에서 동작하지 않습니다** — 해당 브라우저에서 그 면들이 배경에 묻힙니다.
167
+ 그 밖의 색은 전 브라우저에서 테마를 따릅니다.
168
+ - **경고(노랑) 표면에는 역할 토큰이 없습니다** — 라이트 황색 틴트가 거의 보이지 않아
169
+ 두 테마의 세기를 맞출 단 짝이 없습니다(업스트림 미해결). 필터 줄·편집 중 행은
170
+ 팔레트를 직접 읽습니다.
171
+
172
+ > `--u-txt-color-weak`의 WCAG AA 미달(2.68)은 `@iyulab/components` 1.15.0에서
173
+ > 해소됐습니다(4.61). 이 패키지가 쓰던 우회도 함께 제거됐습니다.
174
+
141
175
  ## License
142
176
 
143
177
  MIT
@@ -1,6 +1,5 @@
1
- import { css, unsafeCSS } from "lit";
2
- //#region src/components/data-view/UDataView.styles.ts
3
- var baseStyles = css`
1
+ import { css } from "lit";
2
+ var styles = [css`
4
3
  :host {
5
4
  display: block;
6
5
  width: 100%;
@@ -30,7 +29,12 @@ var baseStyles = css`
30
29
  }
31
30
 
32
31
  .view-toggles u-button[active] {
33
- background: var(--u-blue-600, #1E88E5);
32
+ background: var(--u-primary-color, #1E88E5);
33
+ /* 예외 1 — "주색 위의 글자" 역할 토큰이 없다.
34
+ 역할 층의 유채색은 전경 5단(--u-primary-color-*)뿐이고, 그 위에 얹는 글자색을
35
+ 가리키는 토큰은 없다. 실측: 라이트 #FFFFFF/#1E88E5 = 3.68, 다크 #000000/#2A659D
36
+ = 3.45 — 다크는 흰 글자였다면 6.09 였다. 즉 현재 값이 최선이 아니지만, 팔레트를
37
+ 직접 바꾸면 게시된 시각이 움직이므로 토큰 추가 결정까지 현행을 유지한다. */
34
38
  color: var(--u-neutral-0, #fff);
35
39
  }
36
40
 
@@ -65,15 +69,15 @@ var baseStyles = css`
65
69
  }
66
70
 
67
71
  .card:hover {
68
- border-color: var(--u-blue-600, #1E88E5);
72
+ border-color: var(--u-primary-color, #1E88E5);
69
73
  box-shadow: 0 4px 12px var(--u-shadow-color-weak, rgba(0, 0, 0, 0.08));
70
74
  transform: translateY(-2px);
71
75
  }
72
76
 
73
77
  .card.selected {
74
- border-color: var(--u-blue-600, #1E88E5);
78
+ border-color: var(--u-primary-color, #1E88E5);
75
79
  background: var(--u-bg-color-active, #EEEEEE);
76
- box-shadow: 0 0 0 3px var(--u-blue-200, #90CAF9);
80
+ box-shadow: 0 0 0 3px var(--u-primary-color-weakest, #90CAF9);
77
81
  }
78
82
 
79
83
  .card-content {
@@ -127,6 +131,11 @@ var baseStyles = css`
127
131
  }
128
132
 
129
133
  thead {
134
+ /* 예외 2 — 표면 높이 축이 없다.
135
+ 배경 역할 토큰은 상호작용 상태 이름(-hover/-active/-disabled)뿐이라,
136
+ "바탕보다 한 단 올라온 면"을 뜻하는 토큰이 없다. 헤더에 -hover 를 쓰면
137
+ 마우스 상태를 뜻하게 되므로 팔레트를 직접 읽는다. neutral-50 은 두 테마에서
138
+ 각각 #FAFAFA/#0A0A0A 로 반전되므로 다크 보정은 필요 없다. */
130
139
  background: var(--u-neutral-50, #f8fafc);
131
140
  position: sticky;
132
141
  top: 0;
@@ -175,88 +184,6 @@ var baseStyles = css`
175
184
  color: var(--u-txt-color-weak, #64748b);
176
185
  font-size: 1rem;
177
186
  }
178
- `;
179
- var DARK_PREFIXES = [
180
- ":host([theme=\"dark\"])",
181
- ":host-context([theme=\"dark\"])",
182
- ":host-context([data-theme=\"dark\"])"
183
- ];
184
- var darkRules = (host) => `
185
- ${host} .toolbar {
186
- background: var(--u-bg-color, #121212);
187
- border-color: var(--u-border-color, #3D3D3D);
188
- }
189
-
190
- ${host} .view-toggles u-button[active] {
191
- background: var(--u-blue-600, #87B8F5);
192
- color: var(--u-neutral-0, #000);
193
- }
194
-
195
- ${host} .info {
196
- color: var(--u-txt-color-weak, #8A8A8A);
197
- }
198
-
199
- ${host} .card {
200
- background: var(--u-bg-color, #121212);
201
- border-color: var(--u-border-color, #3D3D3D);
202
- }
203
-
204
- ${host} .card:hover {
205
- border-color: var(--u-blue-600, #87B8F5);
206
- box-shadow: 0 4px 12px var(--u-shadow-color-weak, rgba(0, 0, 0, 0.25));
207
- }
208
-
209
- ${host} .card.selected {
210
- border-color: var(--u-blue-600, #87B8F5);
211
- background: var(--u-bg-color-active, #3D3D3D);
212
- box-shadow: 0 0 0 3px var(--u-blue-200, #2B4F7E);
213
- }
214
-
215
- ${host} .card-field .label {
216
- color: var(--u-txt-color-weak, #8A8A8A);
217
- }
218
-
219
- ${host} .card-field .value {
220
- color: var(--u-txt-color, #D4D4D4);
221
- }
222
-
223
- ${host} .table-wrapper {
224
- border-color: var(--u-border-color, #3D3D3D);
225
- }
226
-
227
- ${host} table {
228
- background: var(--u-bg-color, #121212);
229
- }
230
-
231
- ${host} thead {
232
- background: var(--u-neutral-50, #0A0A0A);
233
- }
234
-
235
- ${host} th {
236
- color: var(--u-txt-color-weak, #8A8A8A);
237
- border-bottom-color: var(--u-border-color, #3D3D3D);
238
- }
239
-
240
- ${host} tbody tr:hover {
241
- background: var(--u-bg-color-hover, #2A2A2A);
242
- }
243
-
244
- ${host} tbody tr.selected {
245
- background: var(--u-bg-color-active, #3D3D3D);
246
- }
247
-
248
- ${host} tbody tr:not(:last-child) {
249
- border-bottom-color: var(--u-border-color-weak, #2A2A2A);
250
- }
251
-
252
- ${host} td {
253
- color: var(--u-txt-color, #D4D4D4);
254
- }
255
-
256
- ${host} .empty {
257
- color: var(--u-txt-color-weak, #8A8A8A);
258
- }
259
- `;
260
- var styles = [baseStyles, unsafeCSS(DARK_PREFIXES.map(darkRules).join("\n"))];
187
+ `];
261
188
  //#endregion
262
189
  export { styles };
@@ -48,6 +48,8 @@ export declare class USimpleSheet extends UElement {
48
48
  cols: number;
49
49
  /** 읽기 전용 모드 */
50
50
  readonly: boolean;
51
+ /** 드롭다운에 일치 항목이 없을 때 문구 */
52
+ noMatchMessage: string;
51
53
  /**
52
54
  * 명시 테마. 'dark'이면 다크 스타일을 적용합니다.
53
55
  * 미설정 시 조상 컨텍스트의 theme/data-theme 속성에 따릅니다 (:host-context, Chromium 한정).
@@ -25,6 +25,7 @@ var USimpleSheet = class USimpleSheet extends UElement {
25
25
  this.rows = 20;
26
26
  this.cols = 10;
27
27
  this.readonly = false;
28
+ this.noMatchMessage = "일치하는 항목 없음";
28
29
  this._sel = null;
29
30
  this._editing = null;
30
31
  this._editVal = "";
@@ -611,7 +612,7 @@ var USimpleSheet = class USimpleSheet extends UElement {
611
612
  </div>
612
613
  ` : noMatch && isStrict ? html`
613
614
  <div class="cell-dropdown">
614
- <div class="dropdown-empty">일치하는 항목 없음</div>
615
+ <div class="dropdown-empty">${this.noMatchMessage}</div>
615
616
  </div>
616
617
  ` : ""}
617
618
  ` : this._formatValue(value, c, r)}
@@ -924,6 +925,7 @@ __decorate([property({ type: Array }), __decorateMetadata("design:type", Array)]
924
925
  __decorate([property({ type: Number }), __decorateMetadata("design:type", Object)], USimpleSheet.prototype, "rows", void 0);
925
926
  __decorate([property({ type: Number }), __decorateMetadata("design:type", Object)], USimpleSheet.prototype, "cols", void 0);
926
927
  __decorate([property({ type: Boolean }), __decorateMetadata("design:type", Object)], USimpleSheet.prototype, "readonly", void 0);
928
+ __decorate([property({ type: String }), __decorateMetadata("design:type", Object)], USimpleSheet.prototype, "noMatchMessage", void 0);
927
929
  __decorate([property({
928
930
  type: String,
929
931
  reflect: true
@@ -125,6 +125,9 @@ var baseStyles = css`
125
125
  padding: 0 6px;
126
126
  font-size: 11px;
127
127
  color: var(--u-txt-color-weak, #64748b);
128
+ /* 예외 — 표면 높이 축이 없다. 행번호 여백은 "비활성"이 아니라 시트 바탕보다
129
+ 한 단 내려간 면이다. --u-bg-color-disabled 를 쓰면 다크에서 바탕(neutral-100)과
130
+ 같은 값이 되어 여백 구분이 사라진다 — 스냅샷이 그것을 잡았다. */
128
131
  background: var(--u-neutral-50, #f8fafc);
129
132
  border-right: 1px solid var(--u-border-color, #e2e8f0);
130
133
  border-bottom: 1px solid var(--u-border-color-weak, #f1f5f9);
@@ -159,7 +162,7 @@ var baseStyles = css`
159
162
  }
160
163
 
161
164
  .cell.selected {
162
- background: var(--u-blue-50, #eff6ff);
165
+ background: var(--u-primary-bg-color, #E3F2FD);
163
166
  }
164
167
 
165
168
  /* The anchor cell (top-left of selection) */
@@ -194,7 +197,7 @@ var baseStyles = css`
194
197
  background: var(--u-bg-color, #fff);
195
198
  box-sizing: border-box;
196
199
  z-index: 10;
197
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
200
+ box-shadow: 0 2px 8px var(--u-shadow-color-strong, rgba(0, 0, 0, 0.16));
198
201
  }
199
202
 
200
203
  /* Dropdown overlay */
@@ -210,7 +213,7 @@ var baseStyles = css`
210
213
  border: 1px solid var(--u-border-color, #e2e8f0);
211
214
  border-top: none;
212
215
  border-radius: 0 0 4px 4px;
213
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
216
+ box-shadow: 0 4px 12px var(--u-shadow-color-normal, rgba(0, 0, 0, 0.12));
214
217
  z-index: 20;
215
218
  }
216
219
 
@@ -229,7 +232,7 @@ var baseStyles = css`
229
232
  }
230
233
 
231
234
  .dropdown-item.highlighted {
232
- background: var(--u-blue-50, #eff6ff);
235
+ background: var(--u-primary-bg-color, #E3F2FD);
233
236
  color: var(--u-blue-800, #1e40af);
234
237
  }
235
238
 
@@ -247,7 +250,7 @@ var baseStyles = css`
247
250
 
248
251
  /* Readonly column/cell */
249
252
  .cell.cell-readonly {
250
- background: var(--u-neutral-50, #f8fafc);
253
+ background: var(--u-bg-color-disabled, #FAFAFA);
251
254
  color: var(--u-txt-color-weak, #64748b);
252
255
  cursor: default;
253
256
  }
@@ -257,12 +260,12 @@ var baseStyles = css`
257
260
  }
258
261
 
259
262
  .cell.cell-readonly.anchor {
260
- background: var(--u-neutral-50, #f8fafc);
263
+ background: var(--u-bg-color-disabled, #FAFAFA);
261
264
  }
262
265
 
263
266
  /* Computed column/cell */
264
267
  .cell.cell-computed {
265
- background: var(--u-blue-50, #eff6ff);
268
+ background: var(--u-primary-bg-color, #E3F2FD);
266
269
  color: var(--u-txt-color, #0f172a);
267
270
  font-style: italic;
268
271
  cursor: default;
@@ -273,7 +276,7 @@ var baseStyles = css`
273
276
  }
274
277
 
275
278
  .cell.cell-computed.anchor {
276
- background: var(--u-blue-50, #eff6ff);
279
+ background: var(--u-primary-bg-color, #E3F2FD);
277
280
  }
278
281
 
279
282
  /* Readonly mode (whole sheet) */
@@ -288,71 +291,14 @@ var DARK_PREFIXES = [
288
291
  ":host-context([data-theme=\"dark\"])"
289
292
  ];
290
293
  var darkRules = (host) => `
291
- ${host} .sheet-container {
292
- background: var(--u-bg-color, #121212);
293
- border-color: var(--u-border-color, #3D3D3D);
294
- }
295
-
296
- ${host} .sheet-container:focus-within {
297
- border-color: var(--u-blue-500, #6ba3e3);
298
- box-shadow: 0 0 0 2px var(--u-blue-100, #1e3a5f);
299
- }
300
-
301
- ${host} .corner {
302
- background: var(--u-neutral-100, #121212);
303
- border-color: var(--u-border-color, #3D3D3D);
304
- }
305
-
306
- ${host} .corner:hover {
307
- background: var(--u-neutral-200, #1E1E1E);
308
- }
309
-
310
- ${host} .col-header {
311
- background: var(--u-neutral-100, #121212);
312
- color: var(--u-txt-color-weak, #8A8A8A);
313
- border-color: var(--u-border-color, #3D3D3D);
314
- }
315
-
316
294
  ${host} .col-header.col-selected {
317
295
  background: var(--u-blue-100, #1e3a5f);
318
- color: var(--u-blue-800, #c2deff);
319
- }
320
-
321
- ${host} .row-num {
322
- background: var(--u-neutral-50, #0A0A0A);
323
- color: var(--u-txt-color-weak, #8A8A8A);
324
- border-right-color: var(--u-border-color, #3D3D3D);
325
- border-bottom-color: var(--u-border-color-weak, #2A2A2A);
326
- }
327
-
328
- ${host} .row-num.row-selected {
329
- background: var(--u-blue-100, #1e3a5f);
330
- color: var(--u-blue-800, #c2deff);
331
- }
332
-
333
- ${host} .cell {
334
- color: var(--u-txt-color, #D4D4D4);
335
- border-right-color: var(--u-border-color-weak, #2A2A2A);
336
- border-bottom-color: var(--u-border-color-weak, #2A2A2A);
337
- }
338
-
339
- ${host} .cell.cell-readonly {
340
- background: var(--u-neutral-100, #1E1E1E);
341
- color: var(--u-txt-color-weak, #8A8A8A);
342
296
  }
343
297
 
344
298
  ${host} .cell.cell-readonly.selected {
345
299
  background: var(--u-neutral-200, #2A2A2A);
346
300
  }
347
301
 
348
- ${host} .cell.cell-readonly.anchor {
349
- background: var(--u-neutral-100, #1E1E1E);
350
- }
351
-
352
- ${host} .cell.cell-computed {
353
- background: var(--u-blue-100, #1e3a5f);
354
- color: var(--u-txt-color, #D4D4D4);
355
- }
356
302
 
357
303
  ${host} .cell.cell-computed.selected {
358
304
  background: var(--u-blue-200, #2a4a6f);
@@ -362,48 +308,6 @@ var darkRules = (host) => `
362
308
  background: var(--u-blue-100, #1e3a5f);
363
309
  }
364
310
 
365
- ${host} .cell.selected {
366
- background: var(--u-blue-0, #0a1e3d);
367
- }
368
-
369
- ${host} .cell.anchor {
370
- background: var(--u-bg-color, #121212);
371
- outline-color: var(--u-blue-500, #6ba3e3);
372
- }
373
-
374
- ${host} .resize-handle:hover::after {
375
- background: var(--u-blue-500, #6ba3e3);
376
- }
377
-
378
- ${host} .cell-input {
379
- background: var(--u-bg-color, #121212);
380
- color: var(--u-txt-color, #D4D4D4);
381
- outline-color: var(--u-blue-500, #6ba3e3);
382
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
383
- }
384
-
385
- ${host} .cell-dropdown {
386
- background: var(--u-bg-color, #121212);
387
- border-color: var(--u-border-color, #3D3D3D);
388
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
389
- }
390
-
391
- ${host} .dropdown-item {
392
- color: var(--u-txt-color, #D4D4D4);
393
- }
394
-
395
- ${host} .dropdown-item:hover {
396
- background: var(--u-neutral-100, #1E1E1E);
397
- }
398
-
399
- ${host} .dropdown-item.highlighted {
400
- background: var(--u-blue-100, #1e3a5f);
401
- color: var(--u-blue-800, #c2deff);
402
- }
403
-
404
- ${host} .dropdown-empty {
405
- color: var(--u-txt-color-weak, #8A8A8A);
406
- }
407
311
  `;
408
312
  var styles = [baseStyles, unsafeCSS(DARK_PREFIXES.map(darkRules).join("\n"))];
409
313
  //#endregion
@@ -9,6 +9,19 @@ export declare class URichTable extends LitElement {
9
9
  currentPage: number;
10
10
  loading: boolean;
11
11
  emptyMessage: string;
12
+ /** 로딩 표시 문구 */
13
+ loadingMessage: string;
14
+ /** 필터 입력 placeholder */
15
+ filterPlaceholder: string;
16
+ /** 필터 select 의 "전체" 항목 문구 */
17
+ filterAllLabel: string;
18
+ /** 새 행 추가 버튼 문구 */
19
+ addRowLabel: string;
20
+ /**
21
+ * 페이지 정보 문구. (전체, 시작, 끝) 을 받아 문자열을 만든다.
22
+ * 언어마다 어순이 달라 템플릿 문자열이 아니라 함수로 연다.
23
+ */
24
+ pageInfoFormatter: (total: number, start: number, end: number) => string;
12
25
  selectable: boolean;
13
26
  editable: boolean;
14
27
  addable: boolean;
@@ -27,6 +40,18 @@ export declare class URichTable extends LitElement {
27
40
  revertRow(_rowId: string): void;
28
41
  setRowError(rowId: string, message: string): void;
29
42
  clearRowError(rowId: string): void;
43
+ /**
44
+ * 행의 식별자. 선택·확장·행 오류 상태가 전부 이 값으로 추적된다.
45
+ *
46
+ * ⚠`_id` 는 이 컴포넌트가 **부여하지 않는다.** 소비자가 넣어 주지 않으면 모든 행의
47
+ * `_id` 가 `undefined` 가 되고, Set 은 그 하나만 담으므로 **한 행을 고르면 전부
48
+ * 골라진다.** 그래서 없을 때는 위치를 대신 쓴다 — 다만 위치 기반 식별은 데이터가
49
+ * 재정렬·재페이징되면 **선택이 다른 행으로 옮겨간다.** 정렬/필터/페이지가 소비자
50
+ * 책임인 컴포넌트이므로, 실제 사용에서는 `_id` 를 주는 것이 옳다.
51
+ */
52
+ private _rowId;
53
+ private _warnedMissingRowId;
54
+ private _warnMissingRowId;
30
55
  getSelectedRows(): Record<string, unknown>[];
31
56
  render(): TemplateResult;
32
57
  private _renderToolbar;
@@ -15,6 +15,11 @@ var URichTable = class URichTable extends LitElement {
15
15
  this.currentPage = 1;
16
16
  this.loading = false;
17
17
  this.emptyMessage = "데이터가 없습니다";
18
+ this.loadingMessage = "로딩 중...";
19
+ this.filterPlaceholder = "필터...";
20
+ this.filterAllLabel = "전체";
21
+ this.addRowLabel = "+ 새 행";
22
+ this.pageInfoFormatter = (total, start, end) => `전체 ${total.toLocaleString()}건 중 ${start}-${end} 표시`;
18
23
  this.selectable = false;
19
24
  this.editable = false;
20
25
  this.addable = false;
@@ -29,6 +34,7 @@ var URichTable = class URichTable extends LitElement {
29
34
  this.filters = {};
30
35
  this.validationErrors = /* @__PURE__ */ new Map();
31
36
  this.rowErrors = /* @__PURE__ */ new Map();
37
+ this._warnedMissingRowId = false;
32
38
  this._lastSelectedIndex = -1;
33
39
  this._onGlobalKeyDown = (e) => {
34
40
  if (e.ctrlKey || e.metaKey) {
@@ -65,7 +71,8 @@ var URichTable = class URichTable extends LitElement {
65
71
  }
66
72
  if (e.key === " " && this.selectable) {
67
73
  e.preventDefault();
68
- const rowId = this.data[this.focusedCell.rowIndex]?._id;
74
+ const r = this.data[this.focusedCell.rowIndex];
75
+ const rowId = r ? this._rowId(r, this.focusedCell.rowIndex) : void 0;
69
76
  if (rowId) this._onRowSelect(rowId);
70
77
  }
71
78
  if (e.key === "Delete" && this.selectedIds.size > 0) for (const row of this.getSelectedRows()) this.dispatchEvent(new CustomEvent("row-delete", {
@@ -88,8 +95,28 @@ var URichTable = class URichTable extends LitElement {
88
95
  next.delete(rowId);
89
96
  this.rowErrors = next;
90
97
  }
98
+ /**
99
+ * 행의 식별자. 선택·확장·행 오류 상태가 전부 이 값으로 추적된다.
100
+ *
101
+ * ⚠`_id` 는 이 컴포넌트가 **부여하지 않는다.** 소비자가 넣어 주지 않으면 모든 행의
102
+ * `_id` 가 `undefined` 가 되고, Set 은 그 하나만 담으므로 **한 행을 고르면 전부
103
+ * 골라진다.** 그래서 없을 때는 위치를 대신 쓴다 — 다만 위치 기반 식별은 데이터가
104
+ * 재정렬·재페이징되면 **선택이 다른 행으로 옮겨간다.** 정렬/필터/페이지가 소비자
105
+ * 책임인 컴포넌트이므로, 실제 사용에서는 `_id` 를 주는 것이 옳다.
106
+ */
107
+ _rowId(row, index) {
108
+ const id = row._id;
109
+ if (id !== void 0 && id !== null) return String(id);
110
+ this._warnMissingRowId();
111
+ return `#${index}`;
112
+ }
113
+ _warnMissingRowId() {
114
+ if (this._warnedMissingRowId) return;
115
+ this._warnedMissingRowId = true;
116
+ console.warn("[@iyulab/data-components] u-rich-table: 행에 `_id` 가 없어 **위치**로 식별합니다. 데이터가 재정렬·재페이징되면 선택 상태가 다른 행으로 옮겨갑니다. 각 행에 고유한 `_id` 를 부여하세요.");
117
+ }
91
118
  getSelectedRows() {
92
- return this.data.filter((row) => this.selectedIds.has(row._id));
119
+ return this.data.filter((row, i) => this.selectedIds.has(this._rowId(row, i)));
93
120
  }
94
121
  render() {
95
122
  return html`
@@ -122,7 +149,7 @@ var URichTable = class URichTable extends LitElement {
122
149
  <div style="flex:1"></div>
123
150
  <slot name="toolbar-end"></slot>
124
151
  ${this.addable ? html`
125
- <button class="btn btn-success" @click=${this._onAddRowClick}>+ 새 행</button>
152
+ <button class="btn btn-success" @click=${this._onAddRowClick}>${this.addRowLabel}</button>
126
153
  ` : ""}
127
154
  </div>
128
155
  `;
@@ -157,10 +184,10 @@ var URichTable = class URichTable extends LitElement {
157
184
  ${this.columns.map((col) => html`
158
185
  <td>
159
186
  ${col.filterable !== false ? col.filterType === "select" && col.options ? html`<select @change=${(e) => this._onFilterChange(col.key, e.target.value)}>
160
- <option value="">전체</option>
187
+ <option value="">${this.filterAllLabel}</option>
161
188
  ${col.options.map((o) => html`<option value=${o.value}>${o.label}</option>`)}
162
189
  </select>` : html`<input
163
- placeholder="필터..."
190
+ placeholder=${this.filterPlaceholder}
164
191
  @input=${(e) => this._onFilterChange(col.key, e.target.value)} />` : ""}
165
192
  </td>
166
193
  `)}
@@ -169,10 +196,10 @@ var URichTable = class URichTable extends LitElement {
169
196
  `;
170
197
  }
171
198
  _renderBody() {
172
- if (this.loading) return html`<tr><td colspan=${this._colSpan()}><div class="loading-overlay">로딩 중...</div></td></tr>`;
199
+ if (this.loading) return html`<tr><td colspan=${this._colSpan()}><div class="loading-overlay">${this.loadingMessage}</div></td></tr>`;
173
200
  if (this.data.length === 0) return html`<tr><td colspan=${this._colSpan()}><div class="empty-message">${this.emptyMessage}</div></td></tr>`;
174
201
  return this.data.map((row, rowIdx) => {
175
- const rowId = row._id;
202
+ const rowId = this._rowId(row, rowIdx);
176
203
  const isSelected = this.selectedIds.has(rowId);
177
204
  const isExpanded = this.expandedIds.has(rowId);
178
205
  const hasError = this.rowErrors.has(rowId);
@@ -192,7 +219,7 @@ var URichTable = class URichTable extends LitElement {
192
219
  ` : ""}
193
220
  ${this.columns.map((col, colIdx) => this._renderCell(row, rowIdx, col, colIdx))}
194
221
  <td class="actions-cell">
195
- <span style="cursor:pointer;color:#94a3b8" @click=${() => this._onRowMenu(row)}>⋯</span>
222
+ <span class="row-menu" @click=${() => this._onRowMenu(row)}>⋯</span>
196
223
  </td>
197
224
  </tr>
198
225
  ${isExpanded && this.detailRenderer ? html`
@@ -201,7 +228,7 @@ var URichTable = class URichTable extends LitElement {
201
228
  </tr>
202
229
  ` : ""}
203
230
  ${hasError ? html`
204
- <tr><td colspan=${this._colSpan()} style="padding:2px 8px;background:#fef2f2;color:#ef4444;font-size:11px;">
231
+ <tr><td colspan=${this._colSpan()} class="row-error-cell">
205
232
  ${this.rowErrors.get(rowId)}
206
233
  </td></tr>
207
234
  ` : ""}
@@ -264,7 +291,7 @@ var URichTable = class URichTable extends LitElement {
264
291
  _renderNewRow() {
265
292
  return html`
266
293
  <tr class="new-row">
267
- ${this.selectable ? html`<td class="checkbox-cell"><span style="color:#86efac">+</span></td>` : ""}
294
+ ${this.selectable ? html`<td class="checkbox-cell"><span class="new-row-marker">+</span></td>` : ""}
268
295
  ${this.expandable ? html`<td></td>` : ""}
269
296
  ${this.columns.map((col, colIdx) => html`
270
297
  <td>
@@ -287,7 +314,7 @@ var URichTable = class URichTable extends LitElement {
287
314
  const end = Math.min(this.currentPage * this.pageSize, this.totalCount);
288
315
  return html`
289
316
  <div class="pagination">
290
- <span>전체 ${this.totalCount.toLocaleString()}건 ${start}-${end} 표시</span>
317
+ <span>${this.pageInfoFormatter(this.totalCount, start, end)}</span>
291
318
  <div class="page-buttons">
292
319
  <button ?disabled=${this.currentPage <= 1} @click=${() => this._onPageChange(this.currentPage - 1)}>◀</button>
293
320
  ${this._getPageNumbers(totalPages).map((p) => html`
@@ -306,7 +333,7 @@ var URichTable = class URichTable extends LitElement {
306
333
  `;
307
334
  }
308
335
  _onSelectAll(e) {
309
- if (e.target.checked) this.selectedIds = new Set(this.data.map((r) => r._id));
336
+ if (e.target.checked) this.selectedIds = new Set(this.data.map((r, i) => this._rowId(r, i)));
310
337
  else this.selectedIds = /* @__PURE__ */ new Set();
311
338
  this._fireSelectionChange();
312
339
  }
@@ -322,7 +349,7 @@ var URichTable = class URichTable extends LitElement {
322
349
  const start = Math.min(this._lastSelectedIndex, rowIdx);
323
350
  const end = Math.max(this._lastSelectedIndex, rowIdx);
324
351
  const next = new Set(this.selectedIds);
325
- for (let i = start; i <= end; i++) next.add(this.data[i]._id);
352
+ for (let i = start; i <= end; i++) next.add(this._rowId(this.data[i], i));
326
353
  this.selectedIds = next;
327
354
  this._fireSelectionChange();
328
355
  }
@@ -445,7 +472,7 @@ var URichTable = class URichTable extends LitElement {
445
472
  this.expandedIds = next;
446
473
  this.dispatchEvent(new CustomEvent("row-expand", {
447
474
  detail: {
448
- row: this.data.find((r) => r._id === rowId),
475
+ row: this.data.find((r, i) => this._rowId(r, i) === rowId),
449
476
  expanded
450
477
  },
451
478
  bubbles: true,
@@ -588,7 +615,7 @@ var URichTable = class URichTable extends LitElement {
588
615
  };
589
616
  }
590
617
  _selectAll() {
591
- this.selectedIds = new Set(this.data.map((r) => r._id));
618
+ this.selectedIds = new Set(this.data.map((r, i) => this._rowId(r, i)));
592
619
  this._fireSelectionChange();
593
620
  }
594
621
  static define(tagName = "u-rich-table") {
@@ -602,6 +629,11 @@ __decorate([property({ type: Number }), __decorateMetadata("design:type", Object
602
629
  __decorate([property({ type: Number }), __decorateMetadata("design:type", Object)], URichTable.prototype, "currentPage", void 0);
603
630
  __decorate([property({ type: Boolean }), __decorateMetadata("design:type", Object)], URichTable.prototype, "loading", void 0);
604
631
  __decorate([property({ type: String }), __decorateMetadata("design:type", Object)], URichTable.prototype, "emptyMessage", void 0);
632
+ __decorate([property({ type: String }), __decorateMetadata("design:type", Object)], URichTable.prototype, "loadingMessage", void 0);
633
+ __decorate([property({ type: String }), __decorateMetadata("design:type", Object)], URichTable.prototype, "filterPlaceholder", void 0);
634
+ __decorate([property({ type: String }), __decorateMetadata("design:type", Object)], URichTable.prototype, "filterAllLabel", void 0);
635
+ __decorate([property({ type: String }), __decorateMetadata("design:type", Object)], URichTable.prototype, "addRowLabel", void 0);
636
+ __decorate([property({ attribute: false }), __decorateMetadata("design:type", Function)], URichTable.prototype, "pageInfoFormatter", void 0);
605
637
  __decorate([property({ type: Boolean }), __decorateMetadata("design:type", Object)], URichTable.prototype, "selectable", void 0);
606
638
  __decorate([property({ type: Boolean }), __decorateMetadata("design:type", Object)], URichTable.prototype, "editable", void 0);
607
639
  __decorate([property({ type: Boolean }), __decorateMetadata("design:type", Object)], URichTable.prototype, "addable", void 0);
@@ -5,7 +5,8 @@ var richTableStyles = css`
5
5
  display: block;
6
6
  font-family: system-ui, -apple-system, sans-serif;
7
7
  font-size: 13px;
8
- border: 1px solid #e2e8f0;
8
+ color: var(--u-txt-color, #0f172a);
9
+ border: 1px solid var(--u-border-color, #e2e8f0);
9
10
  border-radius: 8px;
10
11
  overflow: hidden;
11
12
  }
@@ -15,25 +16,25 @@ var richTableStyles = css`
15
16
  align-items: center;
16
17
  gap: 8px;
17
18
  padding: 8px 12px;
18
- background: #f8fafc;
19
- border-bottom: 1px solid #e2e8f0;
19
+ background: var(--u-neutral-50, #f8fafc);
20
+ border-bottom: 1px solid var(--u-border-color, #e2e8f0);
20
21
  }
21
22
 
22
23
  .toolbar .selection-info {
23
24
  font-size: 12px;
24
- color: #64748b;
25
+ color: var(--u-txt-color-weak, #757575);
25
26
  }
26
27
 
27
28
  .toolbar .search-input {
28
29
  padding: 4px 10px;
29
- border: 1px solid #d1d5db;
30
+ border: 1px solid var(--u-input-border-color, #d1d5db);
30
31
  border-radius: 4px;
31
32
  font-size: 12px;
32
33
  outline: none;
33
34
  }
34
35
 
35
36
  .toolbar .search-input:focus {
36
- border-color: #3b82f6;
37
+ border-color: var(--u-input-border-color-focus, #3b82f6);
37
38
  box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
38
39
  }
39
40
 
@@ -43,13 +44,16 @@ var richTableStyles = css`
43
44
  border-radius: 4px;
44
45
  font-size: 12px;
45
46
  cursor: pointer;
47
+ /* 예외 — "색 배경 위의 글자" 역할 토큰이 없다. --u-txt-color-inverse 는
48
+ 다크에서 neutral-100(#121212) 이라 파랑/초록 버튼 위에서 읽히지 않는다.
49
+ 버튼 배경은 두 테마 모두 유채색이므로 흰 글자가 맞다. */
46
50
  color: white;
47
51
  }
48
52
 
49
- .toolbar .btn-primary { background: #3b82f6; }
50
- .toolbar .btn-primary:hover { background: #2563eb; }
51
- .toolbar .btn-success { background: #10b981; }
52
- .toolbar .btn-success:hover { background: #059669; }
53
+ .toolbar .btn-primary { background: var(--u-primary-color, #3b82f6); }
54
+ .toolbar .btn-primary:hover { background: var(--u-primary-color-strong, #2563eb); }
55
+ .toolbar .btn-success { background: var(--u-success-color, #10b981); }
56
+ .toolbar .btn-success:hover { background: var(--u-success-color-strong, #059669); }
53
57
 
54
58
  table {
55
59
  width: 100%;
@@ -58,10 +62,10 @@ var richTableStyles = css`
58
62
 
59
63
  thead th {
60
64
  padding: 8px;
61
- background: #f1f5f9;
65
+ background: var(--u-neutral-100, #f1f5f9);
62
66
  font-weight: 600;
63
67
  text-align: left;
64
- border-bottom: 2px solid #e2e8f0;
68
+ border-bottom: 2px solid var(--u-border-color, #e2e8f0);
65
69
  user-select: none;
66
70
  position: relative;
67
71
  }
@@ -71,55 +75,55 @@ var richTableStyles = css`
71
75
  }
72
76
 
73
77
  thead th.sortable:hover {
74
- background: #e2e8f0;
78
+ background: var(--u-bg-color-active, #e2e8f0);
75
79
  }
76
80
 
77
81
  .sort-indicator {
78
82
  font-size: 10px;
79
- color: #94a3b8;
83
+ color: var(--u-txt-color-weak, #94a3b8);
80
84
  margin-left: 4px;
81
85
  }
82
86
 
83
87
  .filter-row td {
84
88
  padding: 4px;
85
- background: #fefce8;
86
- border-bottom: 1px solid #e2e8f0;
89
+ background: var(--u-yellow-0, #fefce8);
90
+ border-bottom: 1px solid var(--u-border-color, #e2e8f0);
87
91
  }
88
92
 
89
93
  .filter-row input,
90
94
  .filter-row select {
91
95
  width: 100%;
92
96
  padding: 2px 6px;
93
- border: 1px solid #d1d5db;
97
+ border: 1px solid var(--u-input-border-color, #d1d5db);
94
98
  border-radius: 3px;
95
99
  font-size: 11px;
96
100
  outline: none;
97
101
  }
98
102
 
99
103
  tbody tr {
100
- border-bottom: 1px solid #e2e8f0;
104
+ border-bottom: 1px solid var(--u-border-color-weak, #e2e8f0);
101
105
  }
102
106
 
103
107
  tbody tr:hover {
104
- background: #f8fafc;
108
+ background: var(--u-bg-color-hover, #f8fafc);
105
109
  }
106
110
 
107
111
  tbody tr.selected {
108
- background: #eff6ff;
112
+ background: var(--u-primary-bg-color, #E3F2FD);
109
113
  }
110
114
 
111
115
  tbody tr.focused td.focused-cell {
112
- outline: 2px solid #3b82f6;
116
+ outline: 2px solid var(--u-primary-color, #3b82f6);
113
117
  outline-offset: -2px;
114
118
  }
115
119
 
116
120
  tbody tr.editing {
117
- background: #fefce8;
118
- outline: 2px solid #eab308;
121
+ background: var(--u-yellow-0, #fefce8);
122
+ outline: 2px solid var(--u-warning-color-weak, #eab308);
119
123
  }
120
124
 
121
125
  tbody tr.error {
122
- background: #fef2f2;
126
+ background: var(--u-danger-bg-color, #FFEBEE);
123
127
  }
124
128
 
125
129
  tbody td {
@@ -129,19 +133,19 @@ var richTableStyles = css`
129
133
  tbody td .cell-edit-input {
130
134
  width: 100%;
131
135
  padding: 4px 6px;
132
- border: 1px solid #3b82f6;
136
+ border: 1px solid var(--u-primary-color, #3b82f6);
133
137
  border-radius: 3px;
134
138
  font-size: 13px;
135
139
  outline: none;
136
140
  }
137
141
 
138
142
  tbody td .cell-edit-input.invalid {
139
- border-color: #ef4444;
143
+ border-color: var(--u-danger-color, #ef4444);
140
144
  }
141
145
 
142
146
  .validation-error {
143
147
  font-size: 10px;
144
- color: #ef4444;
148
+ color: var(--u-danger-color, #ef4444);
145
149
  margin-top: 2px;
146
150
  }
147
151
 
@@ -154,11 +158,11 @@ var richTableStyles = css`
154
158
  width: 30px;
155
159
  text-align: center;
156
160
  cursor: pointer;
157
- color: #94a3b8;
161
+ color: var(--u-txt-color-weak, #94a3b8);
158
162
  }
159
163
 
160
164
  .expand-cell:hover {
161
- color: #3b82f6;
165
+ color: var(--u-primary-color, #3b82f6);
162
166
  }
163
167
 
164
168
  .actions-cell {
@@ -168,31 +172,31 @@ var richTableStyles = css`
168
172
 
169
173
  .new-row td {
170
174
  padding: 4px 8px;
171
- background: #f0fdf4;
175
+ background: var(--u-success-bg-color, #E8F5E9);
172
176
  opacity: 0.8;
173
177
  }
174
178
 
175
179
  .new-row input {
176
180
  width: 100%;
177
181
  padding: 4px 6px;
178
- border: 1px dashed #86efac;
182
+ border: 1px dashed var(--u-success-color-weaker, #86efac);
179
183
  border-radius: 3px;
180
184
  background: transparent;
181
185
  font-size: 12px;
182
- color: #6b7280;
186
+ color: var(--u-txt-color-weak, #757575);
183
187
  outline: none;
184
188
  }
185
189
 
186
190
  .new-row input:focus {
187
- border-color: #10b981;
188
- background: white;
191
+ border-color: var(--u-success-color, #10b981);
192
+ background: var(--u-bg-color, white);
189
193
  opacity: 1;
190
194
  }
191
195
 
192
196
  .detail-row td {
193
197
  padding: 8px 8px 8px 70px;
194
- background: #f8fafc;
195
- border-bottom: 2px solid #e2e8f0;
198
+ background: var(--u-neutral-50, #f8fafc);
199
+ border-bottom: 2px solid var(--u-border-color, #e2e8f0);
196
200
  }
197
201
 
198
202
  .badge {
@@ -208,10 +212,10 @@ var richTableStyles = css`
208
212
  justify-content: space-between;
209
213
  align-items: center;
210
214
  padding: 8px 12px;
211
- background: #f8fafc;
212
- border-top: 1px solid #e2e8f0;
215
+ background: var(--u-neutral-50, #f8fafc);
216
+ border-top: 1px solid var(--u-border-color, #e2e8f0);
213
217
  font-size: 12px;
214
- color: #64748b;
218
+ color: var(--u-txt-color-weak, #757575);
215
219
  }
216
220
 
217
221
  .pagination .page-buttons {
@@ -222,22 +226,22 @@ var richTableStyles = css`
222
226
 
223
227
  .pagination button {
224
228
  padding: 2px 8px;
225
- border: 1px solid #d1d5db;
229
+ border: 1px solid var(--u-input-border-color, #d1d5db);
226
230
  border-radius: 3px;
227
- background: white;
231
+ background: var(--u-bg-color, white);
228
232
  font-size: 11px;
229
233
  cursor: pointer;
230
234
  }
231
235
 
232
236
  .pagination button.active {
233
- background: #3b82f6;
234
- color: white;
235
- border-color: #3b82f6;
237
+ background: var(--u-primary-color, #3b82f6);
238
+ color: var(--u-txt-color-inverse, white);
239
+ border-color: var(--u-input-border-color-focus, #3b82f6);
236
240
  }
237
241
 
238
242
  .pagination select {
239
243
  padding: 2px 4px;
240
- border: 1px solid #d1d5db;
244
+ border: 1px solid var(--u-input-border-color, #d1d5db);
241
245
  border-radius: 3px;
242
246
  font-size: 11px;
243
247
  margin-left: 8px;
@@ -248,7 +252,7 @@ var richTableStyles = css`
248
252
  align-items: center;
249
253
  justify-content: center;
250
254
  padding: 40px;
251
- color: #6b7280;
255
+ color: var(--u-txt-color-weak, #757575);
252
256
  }
253
257
 
254
258
  .empty-message {
@@ -256,177 +260,59 @@ var richTableStyles = css`
256
260
  align-items: center;
257
261
  justify-content: center;
258
262
  padding: 40px;
259
- color: #9ca3af;
260
- }
261
-
262
- /* ── Dark mode ──
263
- 외부에서 --u-* CSS 변수가 제공되면 그 값을 사용하고,
264
- 제공되지 않으면 자체 dark fallback 값을 적용한다. */
265
-
266
- :host-context([theme="dark"]) {
267
- border-color: var(--u-border-color, #3D3D3D);
268
- color: var(--u-txt-color, #D4D4D4);
269
- }
270
-
271
- :host-context([theme="dark"]) .toolbar {
272
- background: var(--u-neutral-200, #1E1E1E);
273
- border-bottom-color: var(--u-border-color, #3D3D3D);
274
- }
275
-
276
- :host-context([theme="dark"]) .toolbar .selection-info {
277
- color: var(--u-txt-color-weak, #8A8A8A);
278
- }
279
-
280
- :host-context([theme="dark"]) .toolbar .search-input {
281
- background: var(--u-input-bg-color, #1E1E1E);
282
- color: var(--u-txt-color, #D4D4D4);
283
- border-color: var(--u-input-border-color, #3D3D3D);
284
- }
285
-
286
- :host-context([theme="dark"]) .toolbar .search-input:focus {
287
- border-color: var(--u-blue-500, #6ba3e3);
288
- box-shadow: 0 0 0 2px rgba(107, 163, 227, 0.2);
289
- }
290
-
291
- :host-context([theme="dark"]) .toolbar .btn-primary {
292
- background: var(--u-blue-500, #6ba3e3);
293
- }
294
-
295
- :host-context([theme="dark"]) .toolbar .btn-primary:hover {
296
- background: var(--u-blue-600, #87B8F5);
297
- }
298
-
299
- :host-context([theme="dark"]) .toolbar .btn-success {
300
- background: var(--u-green-500, #66B584);
301
- }
302
-
303
- :host-context([theme="dark"]) .toolbar .btn-success:hover {
304
- background: var(--u-green-600, #81D19D);
305
- }
306
-
307
- :host-context([theme="dark"]) thead th {
308
- background: var(--u-neutral-200, #1E1E1E);
309
- color: var(--u-txt-color, #D4D4D4);
310
- border-bottom-color: var(--u-border-color, #3D3D3D);
263
+ color: var(--u-txt-color-weak, #9ca3af);
311
264
  }
312
265
 
313
- :host-context([theme="dark"]) thead th.sortable:hover {
314
- background: var(--u-neutral-300, #2A2A2A);
315
- }
316
-
317
- :host-context([theme="dark"]) .sort-indicator {
318
- color: var(--u-txt-color-weak, #8A8A8A);
319
- }
320
-
321
- :host-context([theme="dark"]) .filter-row td {
322
- background: var(--u-yellow-0, #2E2200);
323
- border-bottom-color: var(--u-border-color, #3D3D3D);
324
- }
325
-
326
- :host-context([theme="dark"]) .filter-row input,
327
- :host-context([theme="dark"]) .filter-row select {
328
- background: var(--u-input-bg-color, #1E1E1E);
329
- color: var(--u-txt-color, #D4D4D4);
330
- border-color: var(--u-input-border-color, #3D3D3D);
331
- }
332
266
 
333
- :host-context([theme="dark"]) tbody tr {
334
- border-bottom-color: var(--u-border-color-weak, #2A2A2A);
335
- }
336
-
337
- :host-context([theme="dark"]) tbody tr:hover {
338
- background: var(--u-neutral-200, #1E1E1E);
339
- }
340
-
341
- :host-context([theme="dark"]) tbody tr.selected {
342
- background: var(--u-blue-0, #0a1e3d);
343
- }
344
-
345
- :host-context([theme="dark"]) tbody tr.focused td.focused-cell {
346
- outline-color: var(--u-blue-500, #6ba3e3);
267
+ /* 종전에는 아래 셋이 템플릿 안에 style="…#94a3b8" 형태로 박혀 있었다.
268
+ .styles.ts 훑는 정리로는 보이지 않는 자리다. */
269
+ .row-menu {
270
+ cursor: pointer;
271
+ color: var(--u-txt-color-weak, #94a3b8);
347
272
  }
348
273
 
349
- :host-context([theme="dark"]) tbody tr.editing {
350
- background: var(--u-yellow-0, #2E2200);
351
- outline-color: var(--u-yellow-500, #D4A712);
274
+ .row-error-cell {
275
+ padding: 2px 8px;
276
+ background: var(--u-danger-bg-color, #FFEBEE);
277
+ color: var(--u-danger-color, #ef4444);
278
+ font-size: 11px;
352
279
  }
353
280
 
354
- :host-context([theme="dark"]) tbody tr.error {
355
- background: var(--u-red-0, #2E0A0A);
281
+ .new-row-marker {
282
+ color: var(--u-success-color-weaker, #86efac);
356
283
  }
357
284
 
358
- :host-context([theme="dark"]) tbody td .cell-edit-input {
359
- background: var(--u-input-bg-color, #1E1E1E);
360
- color: var(--u-txt-color, #D4D4D4);
361
- border-color: var(--u-blue-500, #6ba3e3);
362
- }
285
+ /* ── 다크 보정 ──
286
+ ⚠**여기 남은 규칙은 "다크 테마 구현"이 아니다.** 역할 토큰의 중립 계열은 두 테마에서
287
+ 서로 다른 팔레트 단에 매핑돼 있어(--u-bg-color = neutral-0 라이트 / neutral-100 다크,
288
+ --u-border-color = neutral-300 / neutral-400) 테마 보정이 토큰 층에 이미 들어 있다.
289
+ 그래서 base 규칙 하나로 두 테마가 성립한다.
363
290
 
364
- :host-context([theme="dark"]) tbody td .cell-edit-input.invalid {
365
- border-color: var(--u-red-500, #D66060);
366
- }
291
+ 종전에는 자리에 57개 선언이 있었고, base 레이어가 토큰을 전혀 쓰지 않았기 때문에
292
+ (Tailwind 계열 hex 하드코딩) 다크를 통째로 재구현할 수밖에 없었다. base 를 역할 토큰으로
293
+ 옮기면서 그 대부분이 불필요해졌다.
367
294
 
368
- :host-context([theme="dark"]) .validation-error {
369
- color: var(--u-red-500, #D66060);
370
- }
295
+ 남긴 것은 역할 층에 자리가 없는 하나뿐이다:
371
296
 
372
- :host-context([theme="dark"]) .expand-cell {
373
- color: var(--u-txt-color-weak, #8A8A8A);
374
- }
297
+ **표면 높이 축이 없다.** 배경 역할 토큰은 상호작용 상태 이름(-hover/-active/
298
+ -disabled)뿐이라 "바탕보다 한 단 올라온 면"을 뜻하는 토큰이 없다. 라이트에서는
299
+ neutral-50/100 이 흰 바탕 위에서 충분히 읽히지만, 다크에서는 그 단이 각각
300
+ #0A0A0A/#121212 로 거의 검정이라 툴바·헤더·페이지네이션이 배경에 묻힌다.
375
301
 
376
- :host-context([theme="dark"]) .expand-cell:hover {
377
- color: var(--u-blue-500, #6ba3e3);
378
- }
379
302
 
380
- :host-context([theme="dark"]) .new-row td {
381
- background: var(--u-green-0, #0D2818);
382
- }
383
-
384
- :host-context([theme="dark"]) .new-row input {
385
- border-color: var(--u-green-500, #66B584);
386
- color: var(--u-txt-color-weak, #8A8A8A);
387
- }
388
-
389
- :host-context([theme="dark"]) .new-row input:focus {
390
- border-color: var(--u-green-600, #81D19D);
391
- background: var(--u-input-bg-color, #1E1E1E);
392
- color: var(--u-txt-color, #D4D4D4);
393
- }
394
-
395
- :host-context([theme="dark"]) .detail-row td {
396
- background: var(--u-neutral-200, #1E1E1E);
397
- border-bottom-color: var(--u-border-color, #3D3D3D);
398
- }
303
+ ⚠:host-context Firefox/Safari 미지원이다. 그러나 위 base 마이그레이션으로
304
+ **색의 대부분이 이제 토큰 층에서 테마를 따르므로**, 그 브라우저에서도 다크가
305
+ 동작한다 — 종전에는 이 블록이 전부였기 때문에 다크가 아예 없었다. */
399
306
 
307
+ :host-context([theme="dark"]) .toolbar,
308
+ :host-context([theme="dark"]) thead th,
309
+ :host-context([theme="dark"]) .detail-row td,
400
310
  :host-context([theme="dark"]) .pagination {
401
311
  background: var(--u-neutral-200, #1E1E1E);
402
- border-top-color: var(--u-border-color, #3D3D3D);
403
- color: var(--u-txt-color-weak, #8A8A8A);
404
312
  }
405
313
 
406
314
  :host-context([theme="dark"]) .pagination button {
407
315
  background: var(--u-neutral-300, #2A2A2A);
408
- color: var(--u-txt-color, #D4D4D4);
409
- border-color: var(--u-border-color, #3D3D3D);
410
- }
411
-
412
- :host-context([theme="dark"]) .pagination button.active {
413
- background: var(--u-blue-500, #6ba3e3);
414
- color: var(--u-neutral-1000, #FFFFFF);
415
- border-color: var(--u-blue-500, #6ba3e3);
416
- }
417
-
418
- :host-context([theme="dark"]) .pagination select {
419
- background: var(--u-input-bg-color, #1E1E1E);
420
- color: var(--u-txt-color, #D4D4D4);
421
- border-color: var(--u-input-border-color, #3D3D3D);
422
- }
423
-
424
- :host-context([theme="dark"]) .loading-overlay {
425
- color: var(--u-txt-color-weak, #8A8A8A);
426
- }
427
-
428
- :host-context([theme="dark"]) .empty-message {
429
- color: var(--u-txt-color-disabled, #525252);
430
316
  }
431
317
  `;
432
318
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iyulab/data-components",
3
3
  "description": "iyulab data visualization components",
4
- "version": "0.7.1",
4
+ "version": "0.8.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-components",
@@ -58,7 +58,7 @@
58
58
  "lint:fix": "eslint src/ --fix"
59
59
  },
60
60
  "dependencies": {
61
- "@iyulab/components": "^1.8.0",
61
+ "@iyulab/components": "^1.15.0",
62
62
  "lit": "^3.3.3"
63
63
  },
64
64
  "peerDependencies": {