@wipco/sdui 0.1.0 → 0.3.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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +27 -0
  3. package/dist/chunk-4SU2KYPU.js +59 -0
  4. package/dist/chunk-56KKWLNI.js +186 -0
  5. package/dist/chunk-7Y4J7EOK.js +115 -0
  6. package/dist/chunk-APCXOCAO.js +110 -0
  7. package/dist/chunk-AROOQ5W6.js +119 -0
  8. package/dist/chunk-DGPRX4MU.js +251 -0
  9. package/dist/chunk-IL2KS4WS.js +25 -0
  10. package/dist/chunk-KADFN76X.js +541 -0
  11. package/dist/chunk-KBHI3IUR.js +40 -0
  12. package/dist/chunk-LZJTKVMO.js +767 -0
  13. package/dist/chunk-MOIU7MUA.js +282 -0
  14. package/dist/chunk-MSAV4J4I.js +77 -0
  15. package/dist/chunk-OOHHW4K4.js +159 -0
  16. package/dist/chunk-Q6SBH63R.js +190 -0
  17. package/dist/chunk-QZQDHSMD.js +78 -0
  18. package/dist/chunk-RH7ESUE6.js +309 -0
  19. package/dist/chunk-SEBPBUZR.js +260 -0
  20. package/dist/chunk-USKRM22W.js +39 -0
  21. package/dist/chunk-WNYWTFGW.js +61 -0
  22. package/dist/chunk-WVRCIVAD.js +72 -0
  23. package/dist/chunk-YJMD6MOE.js +404 -0
  24. package/dist/chunk-ZNITJ4KK.js +433 -0
  25. package/dist/chunk-ZQR3ULVH.js +193 -0
  26. package/dist/chunk-ZUCI34QY.js +1290 -0
  27. package/dist/index.d.ts +391 -79
  28. package/dist/index.js +277 -758
  29. package/dist/meta.d.ts +10 -2
  30. package/dist/meta.js +268 -27
  31. package/dist/rich.js +3 -2
  32. package/dist/styles.css +334 -13
  33. package/dist/widgets/AutocompleteField.d.ts +96 -0
  34. package/dist/widgets/AutocompleteField.js +3 -0
  35. package/dist/widgets/ColumnBoard.d.ts +47 -0
  36. package/dist/widgets/ColumnBoard.js +2 -0
  37. package/dist/widgets/DiffView.d.ts +30 -0
  38. package/dist/widgets/DiffView.js +2 -0
  39. package/dist/widgets/Disclosure.d.ts +20 -0
  40. package/dist/widgets/Disclosure.js +2 -0
  41. package/dist/widgets/EventCalendar.d.ts +59 -0
  42. package/dist/widgets/EventCalendar.js +2 -0
  43. package/dist/widgets/ImageLightbox.d.ts +25 -0
  44. package/dist/widgets/ImageLightbox.js +2 -0
  45. package/dist/widgets/InlineEdit.d.ts +23 -0
  46. package/dist/widgets/InlineEdit.js +2 -0
  47. package/dist/widgets/JsonField.d.ts +24 -0
  48. package/dist/widgets/JsonField.js +2 -0
  49. package/dist/widgets/LiveClock.d.ts +21 -0
  50. package/dist/widgets/LiveClock.js +1 -0
  51. package/dist/widgets/OrgChart.d.ts +32 -0
  52. package/dist/widgets/OrgChart.js +2 -0
  53. package/dist/widgets/PeriodFilter.d.ts +48 -0
  54. package/dist/widgets/PeriodFilter.js +2 -0
  55. package/dist/widgets/PrintSheet.d.ts +23 -0
  56. package/dist/widgets/PrintSheet.js +2 -0
  57. package/dist/widgets/SearchableMultiPicker.d.ts +29 -0
  58. package/dist/widgets/SearchableMultiPicker.js +3 -0
  59. package/dist/widgets/SecureScreen.d.ts +25 -0
  60. package/dist/widgets/SecureScreen.js +2 -0
  61. package/dist/widgets/StackedMeter.d.ts +26 -0
  62. package/dist/widgets/StackedMeter.js +1 -0
  63. package/dist/widgets/Strip.d.ts +20 -0
  64. package/dist/widgets/Strip.js +1 -0
  65. package/dist/widgets/SummaryTable.d.ts +31 -0
  66. package/dist/widgets/SummaryTable.js +1 -0
  67. package/dist/widgets/TagMultiSelect.d.ts +25 -0
  68. package/dist/widgets/TagMultiSelect.js +2 -0
  69. package/dist/widgets/Timeline.d.ts +30 -0
  70. package/dist/widgets/Timeline.js +3 -0
  71. package/dist/widgets/TreePicker.d.ts +21 -0
  72. package/dist/widgets/TreePicker.js +3 -0
  73. package/dist/widgets/TreeView.d.ts +26 -0
  74. package/dist/widgets/TreeView.js +2 -0
  75. package/dist/widgets/index.d.ts +76 -0
  76. package/dist/widgets/index.js +69 -0
  77. package/package.json +93 -3
  78. package/dist/chunk-AR2JYE75.js +0 -889
package/dist/styles.css CHANGED
@@ -160,6 +160,108 @@
160
160
  --wp-z-toast: 110;
161
161
  }
162
162
 
163
+ /*
164
+ * ---------- 토큰: 이관 컴포넌트 호환 셋 ----------
165
+ *
166
+ * 이 시트는 원래 값(13px, 600, 0.12s …)을 규칙 안에 직접 적어 왔다. 그런데 0.3.0에서
167
+ * 옮겨 온 위젯들은 자기 CSS를 `<style>`로 들고 다니고, 그 CSS가 ERP 스타일시트의
168
+ * **스케일 토큰**(--wp-size-*, --wp-weight-*, --wp-transition-* …)을 참조한다.
169
+ * 토큰을 풀어 쓰도록 고쳐 옮기면 한 줄 한 줄이 손으로 옮긴 값이 되어 이관 자체가
170
+ * 위험해지므로, 값은 그대로 두고 **여기서 토큰을 정의**한다. 값은 ERP 토큰 파일
171
+ * (src/styles/tokens/*.css)과 같다 — 두 곳의 그림이 어긋나지 않게 하려는 것이다.
172
+ *
173
+ * 위 :root의 기존 토큰은 하나도 건드리지 않았다. 이미 그 값으로 그려지는 화면이 있다.
174
+ * (그래서 --wp-space-8이 32px인 채로 남고 --wp-space-7도 32px이 된다. ERP는 7=32·8=40이라
175
+ * 어긋나지만, 이 시트를 쓰는 소비자와 ERP는 서로 다른 시트를 로드하므로 섞이지 않는다.)
176
+ */
177
+ :root {
178
+ /* 타이포 스케일 */
179
+ --wp-size-2xs: 11px;
180
+ --wp-size-xs: 12px;
181
+ --wp-size-sm: 13px;
182
+ --wp-size-md: 14px;
183
+ --wp-size-lg: 16px;
184
+ --wp-size-xl: 18px;
185
+ --wp-size-2xl: 22px;
186
+ --wp-weight-regular: 400;
187
+ --wp-weight-medium: 500;
188
+ --wp-weight-semibold: 600;
189
+ --wp-weight-bold: 700;
190
+ --wp-leading-tight: 1.2;
191
+ --wp-leading-snug: 1.35;
192
+ --wp-leading-normal: 1.5;
193
+ --wp-tracking-caps: 0.07em;
194
+
195
+ /* 형태 */
196
+ --wp-radius-xs: 2px;
197
+ --wp-radius-pill: 999px;
198
+ --wp-border-w: 1px;
199
+ /* 심볼의 육각형 — 아바타·핀·노드 배지가 쓴다 */
200
+ --wp-hex-clip: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
201
+
202
+ /* 간격 — 이관 CSS가 쓰는 것만 */
203
+ --wp-space-7: 32px;
204
+ --wp-row-h: 40px;
205
+ --wp-row-h-dense: 32px;
206
+ --wp-icon-xs: 14px;
207
+ --wp-icon-sm: 16px;
208
+ --wp-icon-md: 18px;
209
+ --wp-icon-lg: 20px;
210
+
211
+ /* 고도 */
212
+ --wp-shadow-xs: 0 1px 1px rgba(15, 19, 30, 0.04);
213
+ --wp-ring-focus: 0 0 0 3px rgba(10, 115, 201, 0.28);
214
+ --wp-ring-focus-inverse: 0 0 0 3px rgba(165, 205, 241, 0.42);
215
+ --wp-ring-danger: 0 0 0 3px rgba(202, 26, 29, 0.22);
216
+ /* 위 z-dialog(100)·z-toast(110)와 같은 축에 얹는다 — 툴팁이 가장 위다 */
217
+ --wp-z-sticky: 40;
218
+ --wp-z-panel: 60;
219
+ --wp-z-dropdown: 80;
220
+ --wp-z-tooltip: 120;
221
+
222
+ /* 모션 */
223
+ --wp-duration-fast: 120ms;
224
+ --wp-duration-base: 180ms;
225
+ --wp-ease-standard: cubic-bezier(0.2, 0, 0, 1);
226
+ --wp-transition-control: background-color var(--wp-duration-fast) var(--wp-ease-standard),
227
+ border-color var(--wp-duration-fast) var(--wp-ease-standard),
228
+ color var(--wp-duration-fast) var(--wp-ease-standard),
229
+ box-shadow var(--wp-duration-fast) var(--wp-ease-standard);
230
+ --wp-transition-transform: transform var(--wp-duration-base) var(--wp-ease-standard);
231
+ --wp-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
232
+
233
+ /* 별칭 — 이관 CSS가 의미 이름으로 참조하는 자리 */
234
+ --wp-radius-control: var(--wp-radius-md);
235
+ --wp-radius-card: var(--wp-radius-lg);
236
+ --wp-space-half: 2px;
237
+ --wp-border-brand: var(--wp-navy-600);
238
+ --wp-surface-brand-tint: var(--wp-navy-50);
239
+ --wp-surface-scrim: linear-gradient(180deg, rgba(10, 34, 58, 0) 0%, rgba(10, 34, 58, 0.72) 100%);
240
+ --wp-shadow-xl: 0 20px 48px rgba(15, 19, 30, 0.16), 0 4px 12px rgba(15, 19, 30, 0.08);
241
+ --wp-ring-selected: 0 0 0 1px var(--wp-navy-600);
242
+ --wp-blur-scrim: blur(2px);
243
+ --wp-z-overlay: 90;
244
+
245
+ /*
246
+ * 차트/도표 계열 색. Chart(@wipco/sdui/rich)와 StackedMeter·ColumnBoard가 공유한다 —
247
+ * 같은 화면의 막대와 미터가 다른 팔레트로 그려지면 같은 데이터로 안 읽힌다.
248
+ */
249
+ --wp-series-1: #1d2c60;
250
+ --wp-series-2: #0a73c9;
251
+ --wp-series-3: #c9aa49;
252
+ --wp-series-4: #12805a;
253
+ --wp-series-5: #ca1a1d;
254
+ --wp-series-6: #8e9bc2;
255
+ --wp-series-7: #5b6577;
256
+ --wp-series-8: #a5cdf1;
257
+ }
258
+ @media (prefers-reduced-motion: reduce) {
259
+ :root {
260
+ --wp-duration-fast: 0ms;
261
+ --wp-duration-base: 0ms;
262
+ }
263
+ }
264
+
163
265
  /* ---------- 베이스 ---------- */
164
266
  .sdui-root,
165
267
  .wp-app {
@@ -270,7 +372,27 @@
270
372
  .wp-btn--lg { height: 40px; padding: 0 16px; font-size: 14px; }
271
373
  .wp-btn--xl { height: 48px; padding: 0 20px; font-size: 15px; }
272
374
  .wp-btn--block { width: 100%; }
273
- .wp-btn--loading { pointer-events: none; }
375
+ /*
376
+ * loading 스피너는 **CSS가 그린다**. 컴포넌트는 엘리먼트를 넣지 않는다(components.tsx 주석 참고) —
377
+ * 그리는 주체가 둘이면 원이 두 개 돈다. 라벨은 투명하게 눕히고 그 위에 원을 얹는다.
378
+ */
379
+ .wp-btn--loading { pointer-events: none; color: transparent !important; position: relative; }
380
+ .wp-btn--loading::after {
381
+ content: ''; position: absolute; width: 14px; height: 14px; border-radius: 50%;
382
+ border: 1.5px solid currentColor; border-top-color: transparent;
383
+ animation: wp-spin 0.7s linear infinite; color: var(--wp-action-primary-fg);
384
+ }
385
+ .wp-btn--primary.wp-btn--loading::after, .wp-btn--danger.wp-btn--loading::after {
386
+ border-color: rgba(255, 255, 255, 0.85); border-top-color: transparent;
387
+ }
388
+ .wp-btn--secondary.wp-btn--loading::after, .wp-btn--ghost.wp-btn--loading::after {
389
+ border-color: var(--wp-slate-500); border-top-color: transparent;
390
+ }
391
+ /* as="a"/"span"은 disabled 속성이 없다 — 비활성 표기는 aria-disabled로도 걸린다. */
392
+ .wp-btn[aria-disabled='true']:not(.wp-btn--loading) {
393
+ cursor: not-allowed; background: var(--wp-action-disabled-bg);
394
+ border-color: transparent; color: var(--wp-action-disabled-fg);
395
+ }
274
396
 
275
397
  .wp-iconbtn {
276
398
  display: inline-flex; align-items: center; justify-content: center;
@@ -283,8 +405,18 @@
283
405
  .wp-iconbtn:focus-visible { outline: 2px solid var(--wp-border-focus); outline-offset: 1px; }
284
406
  .wp-iconbtn--sm { width: 28px; height: 28px; }
285
407
  .wp-iconbtn--xs { width: 24px; height: 24px; }
408
+ .wp-iconbtn--lg { width: 40px; height: 40px; }
286
409
  .wp-iconbtn--danger { color: var(--wp-text-danger); }
287
410
  .wp-iconbtn--danger:hover { background: var(--wp-red-50); }
411
+ /* solid — 주 동작이 아이콘 하나뿐인 자리(뷰어 툴바의 닫기 등). */
412
+ .wp-iconbtn--solid { background: var(--wp-action-primary-bg); color: var(--wp-action-primary-fg); }
413
+ .wp-iconbtn--solid:hover:not(:disabled) { background: var(--wp-action-primary-bg-hover); color: var(--wp-white); }
414
+ /* onchrome — 네이비 사이드바/탑바 위. 밝은 글자에 반투명 호버. */
415
+ .wp-iconbtn--onchrome { color: var(--wp-chrome-fg-muted); }
416
+ .wp-iconbtn--onchrome:hover:not(:disabled) { background: var(--wp-chrome-item-hover); color: var(--wp-chrome-fg); }
417
+ .wp-iconbtn:disabled { cursor: not-allowed; color: var(--wp-text-disabled); background: transparent; }
418
+ /* pressed(토글 켜짐) — 선택 틴트. 클래스가 아니라 aria로 거는 이유는 상태가 곧 접근성 값이라서다. */
419
+ .wp-iconbtn[aria-pressed='true'] { background: var(--wp-surface-selected); color: var(--wp-text-brand); }
288
420
  /* outlined — 툴바에서 배경이 같은 표면 위에 놓일 때 눌리는 자리임을 하드라인으로 보인다. */
289
421
  .wp-iconbtn--outlined {
290
422
  border: 1px solid var(--wp-action-secondary-border);
@@ -295,6 +427,24 @@
295
427
  .wp-iconbtn--outlined:hover { background: var(--wp-action-secondary-bg-hover); border-color: var(--wp-border-strong); }
296
428
  .wp-iconbtn--outlined:active { background: var(--wp-action-secondary-bg-active); }
297
429
 
430
+ /* 세그먼티드 — 배타 뷰 전환(목록/카드, 2D/3D). 항목 2~4개까지, 그 이상은 Select다. */
431
+ .wp-seg {
432
+ display: inline-flex; padding: 2px; gap: 2px;
433
+ background: var(--wp-surface-sunken); border: 1px solid var(--wp-border-subtle);
434
+ border-radius: var(--wp-radius-md);
435
+ }
436
+ .wp-seg__item {
437
+ display: inline-flex; align-items: center; justify-content: center; gap: 5px;
438
+ height: 24px; padding: 0 10px; border: 0; background: transparent;
439
+ border-radius: var(--wp-radius-sm);
440
+ font-family: inherit; font-size: 12px; font-weight: 600; white-space: nowrap;
441
+ color: var(--wp-text-muted); cursor: pointer; transition: background 0.12s, color 0.12s;
442
+ }
443
+ .wp-seg__item:hover { color: var(--wp-text-strong); }
444
+ .wp-seg__item[aria-selected='true'] { background: var(--wp-surface-card); color: var(--wp-text-strong); box-shadow: var(--wp-shadow-sm); }
445
+ .wp-seg__item:focus-visible { outline: 2px solid var(--wp-border-focus); outline-offset: 1px; }
446
+ .wp-seg--lg .wp-seg__item { height: 30px; padding: 0 14px; font-size: 13px; }
447
+
298
448
  /* ---------- 입력 ---------- */
299
449
  .wp-input, .wp-textarea, .wp-select {
300
450
  font-family: inherit; font-size: 13px; color: var(--wp-text-default);
@@ -354,6 +504,8 @@
354
504
  display: inline-flex; align-items: center; justify-content: center; transition: background 0.12s, border-color 0.12s;
355
505
  }
356
506
  .wp-check input:checked + .wp-check__box { background: var(--wp-action-primary-bg); border-color: var(--wp-action-primary-bg); color: var(--wp-white); }
507
+ /* 부분 선택 — 체크와 같은 채움에 글리프만 빼기(컴포넌트가 minus로 바꿔 넣는다). */
508
+ .wp-check input:indeterminate + .wp-check__box { background: var(--wp-navy-600); border-color: var(--wp-navy-600); color: var(--wp-white); }
357
509
  .wp-check input:focus-visible + .wp-check__box { outline: 2px solid var(--wp-border-focus); outline-offset: 1px; }
358
510
  .wp-check__text { display: flex; flex-direction: column; gap: 2px; }
359
511
  .wp-check__desc { font-size: 12px; color: var(--wp-text-muted); }
@@ -494,6 +646,11 @@
494
646
  .wp-tab:hover { color: var(--wp-text-default); }
495
647
  .wp-tab[aria-selected='true'] { color: var(--wp-text-brand); border-bottom-color: var(--wp-accent); }
496
648
  .wp-tabs--lg .wp-tab { height: 42px; font-size: 14px; }
649
+ /* onchrome — 네이비 탑바 위. 밝은 글자에 빨간 인디케이터. */
650
+ .wp-tabs--onchrome { border-bottom-color: var(--wp-chrome-border); }
651
+ .wp-tabs--onchrome .wp-tab { color: var(--wp-chrome-fg-muted); }
652
+ .wp-tabs--onchrome .wp-tab:hover { color: var(--wp-chrome-fg); }
653
+ .wp-tabs--onchrome .wp-tab[aria-selected='true'] { color: var(--wp-white); border-bottom-color: var(--wp-accent); }
497
654
  .wp-tab__count {
498
655
  font-size: 11px; font-weight: 700; padding: 1px 6px;
499
656
  background: var(--wp-slate-100); border-radius: var(--wp-radius-full); color: var(--wp-text-muted);
@@ -510,6 +667,15 @@
510
667
  .wp-table td { padding: 10px 12px; border-bottom: 1px solid var(--wp-border-subtle); vertical-align: middle; }
511
668
  .wp-table--dense td { padding: 6px 12px; }
512
669
  .wp-table--hover tbody tr:hover { background: var(--wp-surface-hover); }
670
+ .wp-table tbody tr[aria-selected='true'] { background: var(--wp-surface-selected); }
671
+ /* 정렬 헤더 — th의 타이포를 그대로 물려받아 버튼처럼 보이지 않게 한다. */
672
+ .wp-table__sort {
673
+ display: inline-flex; align-items: center; gap: 3px;
674
+ border: none; background: none; padding: 0; cursor: pointer;
675
+ color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit;
676
+ }
677
+ .wp-table__sort:hover { color: var(--wp-text-default); }
678
+ .wp-table--sticky thead th { position: sticky; top: 0; z-index: 1; }
513
679
  .wp-table__num { text-align: right; font-variant-numeric: tabular-nums; }
514
680
  .wp-table__mono { font-family: var(--wp-font-mono); font-size: 12px; }
515
681
  .wp-table__strong { font-weight: 600; color: var(--wp-text-strong); }
@@ -548,21 +714,32 @@
548
714
  }
549
715
  .wp-spinner--sm { width: 12px; height: 12px; border-width: 2px; }
550
716
  .wp-spinner--lg { width: 24px; height: 24px; border-width: 3px; }
717
+ /* inverse — 네이비/뷰어 표면 위에서는 밝은 쪽으로 뒤집는다. */
718
+ .wp-spinner--inverse { border-color: rgba(255, 255, 255, 0.28); border-top-color: var(--wp-white); }
551
719
  @keyframes wp-spin { to { transform: rotate(360deg); } }
552
720
 
553
- .wp-progress { display: flex; flex-direction: column; gap: 5px; }
554
- /* showValue를 켜면 라벨 줄이 "라벨 … 값" 두 칸이 된다(라벨이 없으면 값만 오른쪽에). */
555
- .wp-progress__label { display: flex; align-items: baseline; gap: var(--wp-space-2); font-size: 12px; color: var(--wp-text-muted); }
556
- /* 값은 라벨과 같은 무게로 흐리게 둔다 — 눈에 먼저 걸려야 하는 것은 막대 자체다. */
557
- .wp-progress__value { margin-left: auto; flex: none; text-align: right; }
558
- .wp-progress__track { height: 6px; background: var(--wp-slate-100); border-radius: var(--wp-radius-full); overflow: hidden; }
559
- .wp-progress__fill { height: 100%; border-radius: var(--wp-radius-full); transition: width 0.25s; }
560
- .wp-progress__fill--brand { background: var(--wp-navy-600); }
561
- .wp-progress__fill--success { background: var(--wp-green-600); }
562
- .wp-progress__fill--warning { background: var(--wp-gold-600); }
563
- .wp-progress__fill--danger { background: var(--wp-red-600); }
721
+ /*
722
+ * 진척/용량 막대. 클래스 어휘는 .wp-meter*다 — .wp-progress*가 아니다.
723
+ * 디자인 시스템 쪽 이름이 meter이고, ProgressBar가 그 마크업을 그대로 낸다.
724
+ */
725
+ .wp-meter { width: 100%; height: 6px; background: var(--wp-slate-200); border-radius: var(--wp-radius-full); overflow: hidden; }
726
+ .wp-meter--sm { height: 4px; }
727
+ .wp-meter--lg { height: 8px; }
728
+ .wp-meter__fill { height: 100%; background: var(--wp-navy-600); border-radius: var(--wp-radius-full); transition: width 0.25s; }
729
+ .wp-meter__fill--success { background: var(--wp-green-600); }
730
+ .wp-meter__fill--warning { background: var(--wp-gold-600); }
731
+ .wp-meter__fill--danger { background: var(--wp-red-600); }
564
732
  /* info — 브랜드 네이비와 헷갈리지 않게 시그널 블루 쪽을 쓴다. */
565
- .wp-progress__fill--info { background: var(--wp-blue-600); }
733
+ .wp-meter__fill--info { background: var(--wp-blue-500); }
734
+ /* striped — 진행 중인 작업에만. 정지된 값에 쓰면 움직이는 것처럼 읽힌다. */
735
+ .wp-meter__fill--striped {
736
+ background-image: linear-gradient(115deg, rgba(255, 255, 255, 0.22) 25%, transparent 25% 50%,
737
+ rgba(255, 255, 255, 0.22) 50% 75%, transparent 75%);
738
+ background-size: 10px 10px;
739
+ }
740
+ /* 라벨 없이 showValue만 켠 모양 — 값이 막대 오른쪽 옆에 붙는다. */
741
+ .wp-meter-row { display: flex; align-items: center; gap: 10px; }
742
+ .wp-meter-row__val { font-size: 12px; color: var(--wp-text-muted); font-variant-numeric: tabular-nums; min-width: 34px; text-align: right; }
566
743
 
567
744
  /* ---------- 페이지네이션 ---------- */
568
745
  .wp-pager { display: flex; align-items: center; gap: 4px; width: 100%; }
@@ -613,3 +790,147 @@
613
790
  display: flex; justify-content: flex-end; gap: var(--wp-space-2);
614
791
  padding: 14px 20px; border-top: 1px solid var(--wp-border-subtle);
615
792
  }
793
+
794
+ /* ══════════════════════════════════════════════════════════════
795
+ ERP에서 이관한 블록 (0.3.0)
796
+
797
+ 컴포넌트만 옮기고 CSS를 두고 오면 무스타일로 나온다. 아래 다섯 묶음은
798
+ components.tsx 끝의 DS 6종이 그대로 참조하는 클래스다.
799
+ (.wp-seg는 0.2.0에 이미 와 있어서 여기 없다 — 컴포넌트만 늦게 온 경우였다.)
800
+ ══════════════════════════════════════════════════════════════ */
801
+
802
+ /* ---------- 툴바 ---------- */
803
+ /*
804
+ * 떠 있는 컨트롤 묶음. search/SearchToolbar가 "라이브러리에 .wp-toolbar가 없다"는
805
+ * 이유로 인라인 style로 조립하고 있었고 그래서 그림자가 빠져 있었다.
806
+ */
807
+ .wp-toolbar {
808
+ display: flex; align-items: center; gap: 6px; padding: 5px;
809
+ background: var(--wp-surface-card);
810
+ border: var(--wp-border-w) solid var(--wp-border-subtle);
811
+ border-radius: var(--wp-radius-md); box-shadow: var(--wp-shadow-md);
812
+ }
813
+ .wp-toolbar--vertical { flex-direction: column; }
814
+ .wp-toolbar--floating { position: absolute; z-index: var(--wp-z-panel); }
815
+ .wp-toolbar__sep { width: 1px; height: 20px; background: var(--wp-border-subtle); flex: none; }
816
+ .wp-toolbar--vertical .wp-toolbar__sep { width: 20px; height: 1px; }
817
+
818
+ /* ---------- 아바타 ---------- */
819
+ .wp-avatar {
820
+ display: inline-flex; align-items: center; justify-content: center;
821
+ width: 28px; height: 28px; background: var(--wp-navy-600); color: var(--wp-white);
822
+ font-size: var(--wp-size-2xs); font-weight: var(--wp-weight-semibold); letter-spacing: 0.02em;
823
+ overflow: hidden; flex: none; user-select: none;
824
+ }
825
+ .wp-avatar--hex { clip-path: var(--wp-hex-clip); border-radius: 0; }
826
+ .wp-avatar--round { border-radius: 50%; }
827
+ .wp-avatar--square { border-radius: var(--wp-radius-sm); }
828
+ .wp-avatar--xs { width: 20px; height: 20px; font-size: 9px; }
829
+ .wp-avatar--sm { width: 24px; height: 24px; font-size: 10px; }
830
+ .wp-avatar--lg { width: 36px; height: 36px; font-size: var(--wp-size-xs); }
831
+ .wp-avatar--xl { width: 48px; height: 48px; font-size: var(--wp-size-md); }
832
+ .wp-avatar--muted { background: var(--wp-slate-300); color: var(--wp-slate-700); }
833
+ .wp-avatar--accent { background: var(--wp-red-600); }
834
+ .wp-avatar img { width: 100%; height: 100%; object-fit: cover; }
835
+ .wp-avatar-stack { display: flex; }
836
+ .wp-avatar-stack > * + * { margin-left: -6px; }
837
+
838
+ /* ---------- 사이드바 내비 ---------- */
839
+ .wp-nav { display: flex; flex-direction: column; gap: 1px; }
840
+ .wp-nav__section {
841
+ padding: 14px 12px 5px; font-size: var(--wp-size-2xs); font-weight: var(--wp-weight-semibold);
842
+ letter-spacing: var(--wp-tracking-caps); text-transform: uppercase;
843
+ color: var(--wp-chrome-fg-muted); opacity: 0.8;
844
+ }
845
+ .wp-nav__item {
846
+ position: relative; display: flex; align-items: center; gap: 9px; height: 32px; padding: 0 10px;
847
+ border-radius: var(--wp-radius-md); font-size: var(--wp-size-sm); font-weight: var(--wp-weight-medium);
848
+ color: var(--wp-chrome-fg-muted); cursor: pointer; transition: var(--wp-transition-control);
849
+ white-space: nowrap; text-decoration: none;
850
+ }
851
+ .wp-nav__item:hover { background: var(--wp-chrome-item-hover); color: var(--wp-chrome-fg); text-decoration: none; }
852
+ .wp-nav__item[aria-current='page'] { background: var(--wp-chrome-item-active); color: var(--wp-white); }
853
+ /* 활성 표시는 좌측 빨간 에지 — 배경 틴트만으로는 네이비 위에서 잘 안 읽힌다. */
854
+ .wp-nav__item[aria-current='page']::before {
855
+ content: ''; position: absolute; left: 0; top: 7px; bottom: 7px; width: 2px;
856
+ border-radius: var(--wp-radius-pill); background: var(--wp-accent);
857
+ }
858
+ .wp-nav__item:focus-visible { outline: none; box-shadow: var(--wp-ring-focus-inverse); }
859
+ .wp-nav__icon { flex: none; display: flex; opacity: 0.9; }
860
+ .wp-nav__label { flex: 1; overflow: hidden; text-overflow: ellipsis; }
861
+ .wp-nav__meta { font-size: var(--wp-size-2xs); color: var(--wp-chrome-fg-muted); font-variant-numeric: tabular-nums; }
862
+ /* light — 흰 표면(설정 하위 내비) 위. 크롬 토큰 대신 본문 토큰으로 뒤집는다. */
863
+ .wp-nav--light .wp-nav__item { color: var(--wp-text-muted); }
864
+ .wp-nav--light .wp-nav__item:hover { background: var(--wp-surface-hover); color: var(--wp-text-strong); }
865
+ .wp-nav--light .wp-nav__item[aria-current='page'] { background: var(--wp-surface-selected); color: var(--wp-text-brand); }
866
+ .wp-nav--light .wp-nav__section { color: var(--wp-text-subtle); }
867
+ .wp-nav--collapsed .wp-nav__item { justify-content: center; padding: 0; }
868
+ .wp-nav--collapsed .wp-nav__label,
869
+ .wp-nav--collapsed .wp-nav__meta,
870
+ .wp-nav--collapsed .wp-nav__section { display: none; }
871
+
872
+ /* ---------- 빵부스러기 ---------- */
873
+ .wp-crumbs { display: flex; align-items: center; gap: 5px; font-size: var(--wp-size-xs); color: var(--wp-text-subtle); min-width: 0; }
874
+ .wp-crumbs__item {
875
+ display: inline-flex; align-items: center; gap: 5px; color: var(--wp-text-muted);
876
+ text-decoration: none; white-space: nowrap;
877
+ transition: color var(--wp-duration-fast) var(--wp-ease-standard);
878
+ }
879
+ .wp-crumbs__item:hover { color: var(--wp-text-strong); text-decoration: none; }
880
+ .wp-crumbs__sep { color: var(--wp-slate-300); display: flex; }
881
+ .wp-crumbs__current {
882
+ color: var(--wp-text-strong); font-weight: var(--wp-weight-medium);
883
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
884
+ }
885
+
886
+ /* ---------- 툴팁 ---------- */
887
+ .wp-tooltip {
888
+ position: absolute; display: inline-flex; align-items: center; gap: 6px; max-width: 240px;
889
+ padding: 5px 8px; background: var(--wp-slate-900); color: var(--wp-white);
890
+ border-radius: var(--wp-radius-sm); font-size: var(--wp-size-xs); line-height: 1.35;
891
+ box-shadow: var(--wp-shadow-md); white-space: nowrap;
892
+ z-index: var(--wp-z-tooltip); pointer-events: none;
893
+ }
894
+ .wp-tooltip--wrap { white-space: normal; }
895
+ .wp-tooltip__key {
896
+ font-family: var(--wp-font-mono); font-size: 10px; padding: 1px 4px; border-radius: 2px;
897
+ background: rgba(255, 255, 255, 0.14); color: rgba(255, 255, 255, 0.86);
898
+ }
899
+ .wp-tooltip::after { content: ''; position: absolute; width: 6px; height: 6px; background: inherit; transform: rotate(45deg); }
900
+ .wp-tooltip--top::after { bottom: -3px; left: 50%; margin-left: -3px; }
901
+ .wp-tooltip--bottom::after { top: -3px; left: 50%; margin-left: -3px; }
902
+ .wp-tooltip--left::after { right: -3px; top: 50%; margin-top: -3px; }
903
+ .wp-tooltip--right::after { left: -3px; top: 50%; margin-top: -3px; }
904
+ .wp-tooltip-host { position: relative; display: inline-flex; }
905
+
906
+ /* ---------- 라디오 ---------- */
907
+ /* Checkbox와 .wp-check 마크업을 공유하고 상자 모양만 다르다 — 위 체크박스 절 뒤에 와야 한다. */
908
+ .wp-check__box--radio { border-radius: 50%; }
909
+ .wp-check input:checked + .wp-check__box--radio { background: var(--wp-white); border: 5px solid var(--wp-navy-600); }
910
+
911
+ /* ---------- 스크롤 상자 ---------- */
912
+ /* TreeView가 목록 높이를 제한할 때 쓴다. min-height:0이 없으면 flex 자식이 안 줄어든다. */
913
+ .wp-scroll { overflow: auto; min-height: 0; }
914
+
915
+ /* ---------- 드롭다운 패널 ---------- */
916
+ /*
917
+ * ERP는 이 모양을 앱 셸 CSS(src/globals.css의 .erp-popover)로 냈고, 라이브러리에는
918
+ * 클래스가 없어 search/popover.tsx가 같은 값을 인라인 style로 조립하고 있었다.
919
+ * 클래스를 여기로 들여와 AutocompleteField 같은 이관 위젯이 그대로 동작하게 한다.
920
+ * (이름을 erp-*로 둔 이유는 widgets/index.ts 주석과 같다 — 이관 중 두 사본이 겹쳐도
921
+ * 규칙이 한 벌만 남아야 한다.)
922
+ */
923
+ .erp-popover-host { position: relative; }
924
+ .erp-popover {
925
+ position: absolute; top: calc(100% + var(--wp-space-2)); right: 0; min-width: 100%;
926
+ background: var(--wp-surface-card);
927
+ border: var(--wp-border-w) solid var(--wp-border-subtle);
928
+ border-radius: var(--wp-radius-lg); box-shadow: var(--wp-shadow-lg);
929
+ z-index: var(--wp-z-dropdown);
930
+ }
931
+ .erp-popover--left { right: auto; left: 0; }
932
+ .erp-popover__body { max-height: 60vh; overflow: auto; padding: var(--wp-space-4); }
933
+ .erp-popover__foot {
934
+ display: flex; align-items: center; justify-content: space-between; gap: var(--wp-space-2);
935
+ padding: var(--wp-space-3); border-top: var(--wp-border-w) solid var(--wp-divider);
936
+ }
@@ -0,0 +1,96 @@
1
+ import React__default from 'react';
2
+
3
+ /**
4
+ * AutocompleteField — 디바운스 원격/정적 자동완성 검색 필드.
5
+ *
6
+ * - `search`가 주어지면 그 함수로 원격 검색하고, 없으면 options 배열을
7
+ * includesLooseSearch로 로컬 필터링한다(데모/폴백).
8
+ * - 입력만으로는 절대 값이 확정되지 않는다 — 목록에서 선택해야 onChange(value, option),
9
+ * 선택 해제 시에만 onChange(null)이 호출된다.
10
+ * - 선택되면 입력창 대신 "선택됨 카드"(circle-check + 라벨 + 부가정보 + 변경 버튼)를 보여준다.
11
+ *
12
+ * ## 조회를 왜 주입으로 받나
13
+ *
14
+ * ERP판은 `endpoint` 프롭을 받아 `erpFetch(endpoint?q=…)`를 직접 부르고, 503을
15
+ * `ErpApiError`로 알아보아 "백엔드 미연동" 안내로 떨어뜨렸다. 그 셋(API_BASE 규약,
16
+ * 웹 쿠키/데스크톱 Bearer 이중 세션, 503=데모 모드 강등)은 전부 **호스트 앱의 계약**이라
17
+ * 라이브러리가 알 수 없다. 그래서 여기는 "키워드를 주면 옵션 목록을 돌려주는 함수"
18
+ * 하나만 요구하고, `endpoint`를 그 함수로 바꾸는 어댑터는 소비 앱에 남는다:
19
+ *
20
+ * ```tsx
21
+ * <AutocompleteField
22
+ * search={async (keyword, signal) => {
23
+ * const sep = endpoint.includes('?') ? '&' : '?';
24
+ * try {
25
+ * return toAutocompleteOptions(
26
+ * await erpFetch(`${endpoint}${sep}q=${encodeURIComponent(keyword)}`, { signal }),
27
+ * );
28
+ * } catch (error) {
29
+ * if (error instanceof ErpApiError && error.status === 503) {
30
+ * return { options: [], status: 'offline' };
31
+ * }
32
+ * throw error;
33
+ * }
34
+ * }}
35
+ * />
36
+ * ```
37
+ *
38
+ * 응답 모양 정규화(`toAutocompleteOptions`)는 여기서 함께 내보낸다 — 배열/`items`/`data`/
39
+ * `results`/`rows` 래핑을 푸는 규칙까지 앱마다 다시 짜게 할 이유가 없다.
40
+ */
41
+
42
+ type AutocompleteOption = Record<string, unknown>;
43
+ /**
44
+ * 원격 검색 결과.
45
+ *
46
+ * 배열만 돌려주면 정상 결과로 본다. "백엔드가 아직 없어 못 불러온 것"과 "불러왔는데
47
+ * 없는 것"은 사용자에게 다른 말을 해야 하므로, 전자는 `status: 'offline'`으로 구분해
48
+ * 돌려준다(ERP의 503→데모 모드 강등이 이 자리로 들어온다). 던진 예외는 전부 'error'다.
49
+ */
50
+ interface AutocompleteSearchResult {
51
+ options: AutocompleteOption[];
52
+ status?: 'ready' | 'offline';
53
+ }
54
+ /**
55
+ * 원격 검색 주입점. 키워드를 받아 옵션 목록을 돌려준다.
56
+ *
57
+ * 낡은 응답을 버리는 일은 호출자가 신경 쓰지 않아도 된다 — 컴포넌트가 요청 순번으로
58
+ * 걸러낸다. 조회 자체를 중단하고 싶으면 `signal`을 fetch에 그대로 넘기면 된다.
59
+ */
60
+ type AutocompleteSearchFn = (keyword: string, signal: AbortSignal) => Promise<AutocompleteOption[] | AutocompleteSearchResult>;
61
+ interface AutocompleteFieldProps {
62
+ /**
63
+ * 원격 검색 함수. 생략하면 `options` 로컬 필터링만 한다.
64
+ * ERP의 `endpoint` 프롭은 이 함수를 만드는 앱 쪽 어댑터가 계속 소유한다(위 주석 참고).
65
+ */
66
+ search?: AutocompleteSearchFn;
67
+ /** 정적 옵션(데모/폴백) — `search`가 없을 때 로컬 검색 대상. */
68
+ options?: AutocompleteOption[];
69
+ /** 옵션에서 표시 라벨을 읽을 키. 기본 'label'. */
70
+ labelKey?: string;
71
+ /** 옵션에서 값을 읽을 키. 기본 'value'. */
72
+ valueKey?: string;
73
+ /** 두 번째 줄(부가 정보)을 읽을 키. */
74
+ subKey?: string;
75
+ /** 확정된 값(제어 모드). 빈 문자열이면 미선택으로 본다. */
76
+ value?: string;
77
+ /** 제어 모드에서 카드에 보여줄 라벨(원격 값 등 로컬에서 라벨을 알 수 없을 때). */
78
+ selectedLabel?: string;
79
+ /** 목록에서 선택 시 (value, option), 선택 해제 시 (null). */
80
+ onChange?: (value: string | null, option?: AutocompleteOption) => void;
81
+ placeholder?: string;
82
+ /** 검색을 시작하는 최소 글자 수. 기본 1. */
83
+ minChars?: number;
84
+ /** 디바운스(ms). 기본 300. */
85
+ debounceMs?: number;
86
+ invalid?: boolean;
87
+ disabled?: boolean;
88
+ }
89
+ /**
90
+ * 응답이 배열이 아니면 items/data/results/rows 래핑까지 풀어 준다.
91
+ * 조회를 앱이 맡게 됐으므로 이 정규화도 함께 내보낸다 — 앱마다 다시 짤 규칙이 아니다.
92
+ */
93
+ declare function toAutocompleteOptions(payload: unknown): AutocompleteOption[];
94
+ declare function AutocompleteField({ search, options, labelKey, valueKey, subKey, value, selectedLabel, onChange, placeholder, minChars, debounceMs, invalid, disabled, }: AutocompleteFieldProps): React__default.JSX.Element;
95
+
96
+ export { AutocompleteField, type AutocompleteFieldProps, type AutocompleteOption, type AutocompleteSearchFn, type AutocompleteSearchResult, toAutocompleteOptions };
@@ -0,0 +1,3 @@
1
+ export { AutocompleteField, toAutocompleteOptions } from '../chunk-MOIU7MUA.js';
2
+ import '../chunk-KADFN76X.js';
3
+ import '../chunk-ZUCI34QY.js';
@@ -0,0 +1,47 @@
1
+ import React__default from 'react';
2
+
3
+ /**
4
+ * ColumnBoard — 칸반식 분류 보드 (버튼 이동, 드래그 없음).
5
+ *
6
+ * - 컬럼 헤더: 라벨 + 건수 Badge, tone에 따라 상태 틴트(--wp-status-*-bg/border/fg).
7
+ * - 항목 카드: 제목/부제/뱃지, onItemClick이 있으면 본문이 버튼이 된다.
8
+ * - 이동: 카드 우측 chevron 메뉴(erp-popover)에서 다른 컬럼을 골라 onMove({ id, toColumn }).
9
+ * (SDUI 액션은 첫 콜백 인자만 이벤트 값으로 받으므로 단일 객체로 전달한다)
10
+ * onMove 미연결 시 버튼은 비활성 + 사유 title.
11
+ * - 선택: selectable이면 카드마다 Checkbox. selectedIds가 주어지면 제어 모드,
12
+ * 아니면 내부 상태로 동작하며 변경 시 onSelectChange(ids).
13
+ * - 정의되지 않은 컬럼 키를 가진 항목은 보드 아래 미분류 안내로 집계한다.
14
+ */
15
+
16
+ type ColumnBoardTone = 'neutral' | 'info' | 'success' | 'warning' | 'danger';
17
+ interface ColumnBoardColumn {
18
+ key: string;
19
+ label: string;
20
+ tone?: ColumnBoardTone;
21
+ }
22
+ interface ColumnBoardItem {
23
+ id: string;
24
+ /** 소속 컬럼 key */
25
+ column: string;
26
+ title: string;
27
+ sub?: string;
28
+ badge?: string;
29
+ }
30
+ interface ColumnBoardProps {
31
+ columns: ColumnBoardColumn[];
32
+ items: ColumnBoardItem[];
33
+ /** 이동 확정 시 { id, toColumn } 단일 객체 — SDUI 이벤트 값으로 그대로 저장 가능. 미연결이면 이동 버튼 비활성. */
34
+ onMove?: (move: {
35
+ id: string;
36
+ toColumn: string;
37
+ }) => void;
38
+ /** 카드 본문 클릭 시 (item). 미연결이면 본문은 클릭 불가 표시 없음. */
39
+ onItemClick?: (item: ColumnBoardItem) => void;
40
+ selectable?: boolean;
41
+ /** 제어 모드 선택 목록 — 없으면 내부 상태로 동작 */
42
+ selectedIds?: string[];
43
+ onSelectChange?: (ids: string[]) => void;
44
+ }
45
+ declare function ColumnBoard({ columns, items, onMove, onItemClick, selectable, selectedIds, onSelectChange, }: ColumnBoardProps): React__default.JSX.Element;
46
+
47
+ export { ColumnBoard, type ColumnBoardColumn, type ColumnBoardItem, type ColumnBoardProps, type ColumnBoardTone };
@@ -0,0 +1,2 @@
1
+ export { ColumnBoard } from '../chunk-Q6SBH63R.js';
2
+ import '../chunk-ZUCI34QY.js';
@@ -0,0 +1,30 @@
1
+ import React__default from 'react';
2
+
3
+ /**
4
+ * DiffView — 변경 전/후 비교 뷰.
5
+ *
6
+ * - fields: 스칼라 값 비교. 라벨(wp-kv 스타일) + 이전 값(취소선, subtle)
7
+ * + arrow-right 아이콘 + 이후 값(strong, success 틴트 배경).
8
+ * - lists: 목록 비교. 추가 항목 success Badge '+', 제거 danger Badge '−',
9
+ * 유지 neutral Badge.
10
+ * - 값이 없는 쪽은 '—'로 표시한다(취소선/틴트 없음).
11
+ */
12
+
13
+ interface DiffField {
14
+ label: string;
15
+ before?: React__default.ReactNode;
16
+ after?: React__default.ReactNode;
17
+ }
18
+ interface DiffList {
19
+ label: string;
20
+ added?: string[];
21
+ removed?: string[];
22
+ kept?: string[];
23
+ }
24
+ interface DiffViewProps {
25
+ fields?: DiffField[];
26
+ lists?: DiffList[];
27
+ }
28
+ declare function DiffView({ fields, lists }: DiffViewProps): React__default.JSX.Element;
29
+
30
+ export { type DiffField, type DiffList, DiffView, type DiffViewProps };
@@ -0,0 +1,2 @@
1
+ export { DiffView } from '../chunk-WVRCIVAD.js';
2
+ import '../chunk-ZUCI34QY.js';
@@ -0,0 +1,20 @@
1
+ import React__default from 'react';
2
+
3
+ /**
4
+ * Disclosure — 접기/펼치기 행.
5
+ *
6
+ * - chevron-right 아이콘이 펼침 상태에서 90도 회전(var(--wp-transition-transform))한다.
7
+ * - controlled-with-fallback: open이 주어지면 그대로 따르고, 없으면 내부 상태로 동작한다.
8
+ * - aria-expanded / aria-controls 연결, 내용 영역은 hidden 토글로 상태를 보존한다.
9
+ */
10
+
11
+ interface DisclosureProps {
12
+ summary: React__default.ReactNode;
13
+ children?: React__default.ReactNode;
14
+ defaultOpen?: boolean;
15
+ open?: boolean;
16
+ onOpenChange?: (open: boolean) => void;
17
+ }
18
+ declare function Disclosure({ summary, children, defaultOpen, open, onOpenChange, }: DisclosureProps): React__default.JSX.Element;
19
+
20
+ export { Disclosure, type DisclosureProps };
@@ -0,0 +1,2 @@
1
+ export { Disclosure } from '../chunk-WNYWTFGW.js';
2
+ import '../chunk-ZUCI34QY.js';