@wipco/sdui 0.2.0 → 0.4.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 (77) 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-FGRR3ROJ.js +497 -0
  10. package/dist/chunk-IL2KS4WS.js +25 -0
  11. package/dist/chunk-KADFN76X.js +541 -0
  12. package/dist/chunk-KBHI3IUR.js +40 -0
  13. package/dist/chunk-LZJTKVMO.js +767 -0
  14. package/dist/chunk-MOIU7MUA.js +282 -0
  15. package/dist/chunk-MSAV4J4I.js +77 -0
  16. package/dist/chunk-OOHHW4K4.js +159 -0
  17. package/dist/chunk-Q6SBH63R.js +190 -0
  18. package/dist/chunk-QZQDHSMD.js +78 -0
  19. package/dist/chunk-RH7ESUE6.js +309 -0
  20. package/dist/chunk-SEBPBUZR.js +260 -0
  21. package/dist/chunk-USKRM22W.js +39 -0
  22. package/dist/chunk-WNYWTFGW.js +61 -0
  23. package/dist/chunk-WVRCIVAD.js +72 -0
  24. package/dist/chunk-ZNITJ4KK.js +433 -0
  25. package/dist/chunk-ZQR3ULVH.js +193 -0
  26. package/dist/{chunk-QJHD6HVQ.js → chunk-ZUCI34QY.js} +106 -390
  27. package/dist/index.d.ts +419 -56
  28. package/dist/index.js +412 -803
  29. package/dist/meta.d.ts +188 -2
  30. package/dist/meta.js +1754 -9
  31. package/dist/rich.js +19 -4
  32. package/dist/styles.css +246 -0
  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
package/dist/rich.js CHANGED
@@ -1,5 +1,6 @@
1
- import { HoistedStyle, Spinner, EmptyState } from './chunk-QJHD6HVQ.js';
2
- export { DatePicker, DateRangePicker } from './chunk-QJHD6HVQ.js';
1
+ import { useSduiAssetResolver, HoistedStyle } from './chunk-FGRR3ROJ.js';
2
+ export { DatePicker, DateRangePicker } from './chunk-FGRR3ROJ.js';
3
+ import { Spinner, EmptyState } from './chunk-ZUCI34QY.js';
3
4
  import { useRef, useCallback, useMemo, useState, useEffect, useLayoutEffect } from 'react';
4
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
6
 
@@ -489,6 +490,7 @@ function MapPreview({
489
490
  const [ready, setReady] = useState(false);
490
491
  const ol = useOptionalModule(loadOl);
491
492
  const olModule = ol.status === "ready" ? ol.module : null;
493
+ const resolveAsset = useSduiAssetResolver();
492
494
  const markersKey = useMemo(() => JSON.stringify(Array.isArray(markers) ? markers : []), [markers]);
493
495
  const centerLat = center?.lat;
494
496
  const centerLng = center?.lng;
@@ -535,7 +537,20 @@ function MapPreview({
535
537
  });
536
538
  const map = new OlMap({
537
539
  target: container,
538
- layers: [new TileLayer({ source: new OSM() }), new VectorLayer({ source: new VectorSource({ features }) })],
540
+ layers: [
541
+ new TileLayer({
542
+ source: new OSM({
543
+ // OL 기본 구현과 같되 URL만 관문에 태운다. `null`이면 src를 아예 안 넣는다 —
544
+ // 빈 문자열을 넣으면 브라우저가 현재 문서 URL로 요청을 한 번 더 낸다.
545
+ tileLoadFunction: (tile, src) => {
546
+ const next = resolveAsset(src, { component: "MapPreview", prop: "src", kind: "attribute" });
547
+ if (next === null) return;
548
+ tile.getImage().src = next;
549
+ }
550
+ })
551
+ }),
552
+ new VectorLayer({ source: new VectorSource({ features }) })
553
+ ],
539
554
  // OSM 라이선스상 저작자 표기(Attribution)는 항상 유지한다.
540
555
  controls: defaultControls({ attribution: true, rotate: false, zoom: interactive }),
541
556
  interactions: interactive ? defaultInteractions() : defaultInteractions({
@@ -597,7 +612,7 @@ function MapPreview({
597
612
  map.setTarget(void 0);
598
613
  setReady(false);
599
614
  };
600
- }, [olModule, markersKey, centerLat, centerLng, zoom, interactive]);
615
+ }, [olModule, markersKey, centerLat, centerLng, zoom, interactive, resolveAsset]);
601
616
  if (ol.status !== "ready") {
602
617
  return /* @__PURE__ */ jsx(
603
618
  DependencyPlaceholder,
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 {
@@ -688,3 +790,147 @@
688
790
  display: flex; justify-content: flex-end; gap: var(--wp-space-2);
689
791
  padding: 14px 20px; border-top: 1px solid var(--wp-border-subtle);
690
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';
@@ -0,0 +1,59 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * EventCalendar — 일정 캘린더 위젯 (월 / 주 / 일 / 일정 뷰).
5
+ *
6
+ * 여러 날에 걸친 일정은 주 단위로 잘라(lane packing) 막대로 그린다.
7
+ * 주 경계에서 잘린 쪽은 모서리를 각지게 두어 이어짐을 표현하고,
8
+ * 월 뷰에서는 최대 3줄까지만 보여준 뒤 '+N' 칩으로 넘김을 알린다.
9
+ * view/date는 controlled-with-fallback: prop이 오면 그 값을 따르고,
10
+ * 없으면 내부 상태로 동작한다.
11
+ */
12
+ interface EventCalendarEvent {
13
+ id: string;
14
+ title: string;
15
+ /** YYYY-MM-DD */
16
+ start: string;
17
+ /** YYYY-MM-DD, 포함(inclusive). 없으면 start와 동일. */
18
+ end?: string;
19
+ type?: string;
20
+ status?: string;
21
+ /** CSS 색상. 없으면 type별로 --wp-series-N을 순환 배정. */
22
+ color?: string;
23
+ }
24
+ interface EventCalendarTypeOption {
25
+ value: string;
26
+ label: string;
27
+ color?: string;
28
+ }
29
+ type EventCalendarView = 'month' | 'week' | 'day' | 'agenda';
30
+ /** 뷰 전환 버튼 한 칸. `label`을 생략하면 기본 라벨(월/주/일/일정)을 쓴다. */
31
+ interface EventCalendarViewItem {
32
+ value: EventCalendarView;
33
+ label?: string;
34
+ /** 라벨 앞에 붙일 아이콘 이름 */
35
+ icon?: string;
36
+ }
37
+ interface EventCalendarProps {
38
+ events: EventCalendarEvent[];
39
+ view?: EventCalendarView;
40
+ onViewChange?: (view: EventCalendarView) => void;
41
+ /**
42
+ * 노출할 뷰와 그 라벨·아이콘·순서. 생략하면 월/주/일/일정 넷이 그대로 나온다.
43
+ *
44
+ * `view`는 **어느 뷰로 시작할지**만 정했지 **어떤 뷰를 고를 수 있는지**는 정하지
45
+ * 못했다 — 월만 쓰는 화면에도 주/일/일정 버튼이 보였다. 문자열 축약형(`["month"]`)도
46
+ * 받는다. 모르는 값·중복은 버리고, 전부 걸러지면 기본 넷으로 되돌린다.
47
+ */
48
+ views?: Array<EventCalendarViewItem | EventCalendarView>;
49
+ /** 기준 날짜(YYYY-MM-DD). 기본 오늘. */
50
+ date?: string;
51
+ onDateChange?: (date: string) => void;
52
+ /** 유형 필터 칩. 주어지면 칩 토글로 해당 type 일정을 숨기거나 보인다. */
53
+ types?: EventCalendarTypeOption[];
54
+ onEventClick?: (event: EventCalendarEvent) => void;
55
+ selectedId?: string;
56
+ }
57
+ declare const EventCalendar: ({ events, view, onViewChange, views, date, onDateChange, types, onEventClick, selectedId, }: EventCalendarProps) => React.JSX.Element;
58
+
59
+ export { EventCalendar, type EventCalendarEvent, type EventCalendarProps, type EventCalendarTypeOption, type EventCalendarView, type EventCalendarViewItem };
@@ -0,0 +1,2 @@
1
+ export { EventCalendar } from '../chunk-LZJTKVMO.js';
2
+ import '../chunk-ZUCI34QY.js';
@@ -0,0 +1,25 @@
1
+ import React__default from 'react';
2
+
3
+ /**
4
+ * ImageLightbox — 이미지 라이트박스 뷰어.
5
+ * 썸네일(또는 커스텀 트리거) 클릭 → 전체 화면 오버레이:
6
+ * - 휠 줌 1x–8x (커서 위치 기준 앵커)
7
+ * - 확대 상태에서 드래그 패닝 (pointer capture, 경계 클램프)
8
+ * - 툴바: 축소 / 확대 / 원래 크기 / 닫기 + 배율 % 표시
9
+ * - Escape·배경 클릭으로 닫힘, 트리거 클릭은 stopPropagation (행 클릭과 공존)
10
+ */
11
+
12
+ interface ImageLightboxProps {
13
+ /** 원본 이미지 URL */
14
+ src: string;
15
+ alt?: string;
16
+ /** 기본 썸네일 <img>에 적용할 클래스 */
17
+ thumbClassName?: string;
18
+ /** 기본 썸네일 <img>에 적용할 스타일 */
19
+ thumbStyle?: React__default.CSSProperties;
20
+ /** 커스텀 트리거 — 주면 기본 썸네일 대신 렌더된다 */
21
+ children?: React__default.ReactNode;
22
+ }
23
+ declare function ImageLightbox({ src, alt, thumbClassName, thumbStyle, children }: ImageLightboxProps): React__default.JSX.Element;
24
+
25
+ export { ImageLightbox, type ImageLightboxProps };
@@ -0,0 +1,2 @@
1
+ export { ImageLightbox } from '../chunk-RH7ESUE6.js';
2
+ import '../chunk-ZUCI34QY.js';
@@ -0,0 +1,23 @@
1
+ import React__default from 'react';
2
+
3
+ /**
4
+ * InlineEdit — 텍스트를 제자리에서 수정하는 위젯.
5
+ *
6
+ * - 보기 모드: 값 + 연필 IconButton(hover/focus 시 표시, 터치 기기에서는 항상 표시).
7
+ * - 편집 모드: DS Input(autoFocus, Enter=저장, Escape=취소) + 확인/취소 IconButton.
8
+ * - 비동기 onSave 동안 확인 버튼이 로딩(회전 아이콘) 상태가 되고, 실패하면
9
+ * 편집 모드를 유지한 채 danger 캡션을 보여준다(503은 데모 모드 안내).
10
+ */
11
+
12
+ interface InlineEditProps {
13
+ value: string;
14
+ onSave: (value: string) => void | Promise<void>;
15
+ type?: 'text' | 'number';
16
+ mono?: boolean;
17
+ disabled?: boolean;
18
+ /** 값이 비었을 때 표시할 텍스트. 기본 '—' */
19
+ emptyText?: string;
20
+ }
21
+ declare function InlineEdit({ value, onSave, type, mono, disabled, emptyText, }: InlineEditProps): React__default.JSX.Element;
22
+
23
+ export { InlineEdit, type InlineEditProps };
@@ -0,0 +1,2 @@
1
+ export { InlineEdit } from '../chunk-OOHHW4K4.js';
2
+ import '../chunk-ZUCI34QY.js';
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * JsonField — JSON 편집용 텍스트 영역.
5
+ *
6
+ * - 객체가 들어오면 2칸 들여쓰기로 정렬해 보여준다(문자열이면 파싱 시도 후 정렬, 실패 시 원문 유지).
7
+ * - blur 시 JSON.parse로 검증: 성공하면 정렬 후 onChange(parsed), 실패하면 오류 위치(행/열)를
8
+ * danger 캡션으로 보여주고 입력한 원문을 그대로 유지한다.
9
+ * - controlled-with-fallback: value가 실제로 바뀔 때만 내부 텍스트를 동기화한다
10
+ * (편집 중이거나 표현이 같으면 사용자가 입력한 원문을 덮어쓰지 않는다).
11
+ */
12
+ interface JsonFieldProps {
13
+ /** 객체 또는 JSON 문자열 */
14
+ value?: unknown;
15
+ onChange?: (parsed: unknown) => void;
16
+ /** 기본 6 */
17
+ rows?: number;
18
+ invalid?: boolean;
19
+ disabled?: boolean;
20
+ placeholder?: string;
21
+ }
22
+ declare function JsonField({ value, onChange, rows, invalid, disabled, placeholder, }: JsonFieldProps): React.JSX.Element;
23
+
24
+ export { JsonField, type JsonFieldProps };