@wipco/sdui 1.0.1 → 1.1.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/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { CSSProperties, ReactNode, ComponentType } from 'react';
3
- import { S as SduiPage, a as SduiAction, b as SduiPropValue, c as SduiRule } from './types-C7y9CpgF.js';
4
- export { d as SduiArgRef, e as SduiCalcRef, f as SduiChild, g as SduiIndexRef, h as SduiItemRef, i as SduiNode, j as SduiPageQuery, k as SduiScalar, l as SduiStateRef } from './types-C7y9CpgF.js';
3
+ import { S as SduiPage, a as SduiAction, b as SduiPropValue, c as SduiRule } from './types-COjGKgmd.js';
4
+ export { d as SduiArgRef, e as SduiCalcRef, f as SduiChild, g as SduiIndexRef, h as SduiItemRef, i as SduiNode, j as SduiPageQuery, k as SduiScalar, l as SduiStateRef } from './types-COjGKgmd.js';
5
5
  import { S as SduiBoxProps, a as SduiContainerLayoutProps, b as SduiColumnsScale, c as SduiGapScale, d as SduiWeightScale, e as SduiTextAlignScale } from './layout-C3nRXMwb.js';
6
6
  export { C as CHILD_LAYOUT_PROPS, L as LAYOUT_CSS, f as SduiAlignScale, g as SduiChildLayoutProps, h as SduiSizeScale, i as SduiSpanScale, j as childLayoutFrom, k as columnsClass, l as containerLayoutStyle, m as joinClass, n as mergeStyle, t as typographyStyle } from './layout-C3nRXMwb.js';
7
7
  import { D as DatePickerProps, a as DateRangePickerProps, T as TimePickerProps } from './DatePicker-C_TT_SOw.js';
package/dist/index.js CHANGED
@@ -2181,6 +2181,9 @@ function nearestArgsFrame(scope) {
2181
2181
  }
2182
2182
  return void 0;
2183
2183
  }
2184
+ function hasExplicitPageHead(body) {
2185
+ return Array.isArray(body) && body.some((child) => isPlainObject(child) && child.component === "PageHead");
2186
+ }
2184
2187
  function isStateRef(v) {
2185
2188
  return isPlainObject(v) && typeof v.$state === "string";
2186
2189
  }
@@ -3035,7 +3038,7 @@ function SduiRenderer({
3035
3038
  // 컴포넌트(목록 조회 등)도 호스트가 건 잠금 안에서 움직이게 하기 위해서다(adapter.ts 주석).
3036
3039
  /* @__PURE__ */ jsx(SduiAdapterContext.Provider, { value: host, children: /* @__PURE__ */ jsx(SduiDispatchContext.Provider, { value: dispatch, children: /* @__PURE__ */ jsx(SduiRuntimeContext.Provider, { value: runtime, children: /* @__PURE__ */ jsx(SduiPermissionProvider, { can, children: /* @__PURE__ */ jsxs("div", { className: "sdui-root", children: [
3037
3040
  /* @__PURE__ */ jsx(HoistedStyle, { id: "wp-sdui-layout", css: LAYOUT_CSS }),
3038
- page.title || page.subtitle ? /* @__PURE__ */ jsx("div", { className: "wp-page-head", children: /* @__PURE__ */ jsxs("div", { className: "wp-page-head__text", children: [
3041
+ !hasExplicitPageHead(page.body) && (page.title || page.subtitle) ? /* @__PURE__ */ jsx("div", { className: "wp-page-head", children: /* @__PURE__ */ jsxs("div", { className: "wp-page-head__text", children: [
3039
3042
  page.title ? /* @__PURE__ */ jsx("h1", { className: "wp-page-head__title", children: page.title }) : null,
3040
3043
  page.subtitle ? /* @__PURE__ */ jsx("div", { className: "wp-page-head__sub", children: page.subtitle }) : null
3041
3044
  ] }) }) : null,
package/dist/meta.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { b as SduiPropValue, i as SduiNode } from './types-C7y9CpgF.js';
1
+ import { b as SduiPropValue, i as SduiNode } from './types-COjGKgmd.js';
2
2
 
3
3
  /**
4
4
  * 시각 빌더용 컴포넌트 메타데이터 스키마.
package/dist/meta.js CHANGED
@@ -3273,6 +3273,26 @@ var ChartMeta = {
3273
3273
  description: "\uB2E8\uC77C \uC2DC\uB9AC\uC988 \uCE74\uB974\uD14C\uC2DC\uC548 \uCC28\uD2B8\uB294 \uCF1C\uB3C4 \uC0DD\uB7B5\uB41C\uB2E4.",
3274
3274
  group: "appearance"
3275
3275
  },
3276
+ {
3277
+ name: "legendPlacement",
3278
+ kind: "enum",
3279
+ label: "\uBC94\uB840 \uC704\uCE58",
3280
+ default: "bottom",
3281
+ description: 'pie/donut \uC804\uC6A9. "\uC6B0\uCE21 \uC218\uCE58 \uD45C"\uB294 \uC810 \uBC94\uB840 \uB300\uC2E0 \uC804\uCCB4 \uD589 + \uC870\uAC01\uBCC4 \uAC12\xB7\uC810\uC720\uC728\uC744 \uCC28\uD2B8 \uC624\uB978\uCABD\uC5D0 \uC801\uB294\uB2E4.',
3282
+ options: [
3283
+ { value: "bottom", label: "\uC544\uB798 \uC810 \uBC94\uB840" },
3284
+ { value: "right", label: "\uC6B0\uCE21 \uC218\uCE58 \uD45C" }
3285
+ ],
3286
+ group: "appearance"
3287
+ },
3288
+ {
3289
+ name: "legendTotal",
3290
+ kind: "number",
3291
+ label: "\uC804\uCCB4 \uAC12 \uC7AC\uC815\uC758",
3292
+ bindable: true,
3293
+ description: '\uC6B0\uCE21 \uC218\uCE58 \uD45C\uC758 "\uC804\uCCB4" \uD589 \uAC12\uC774\uC790 \uC810\uC720\uC728 \uBD84\uBAA8. \uC0DD\uB7B5\uD558\uBA74 \uC870\uAC01 \uAC12\uC758 \uD569 \u2014 \uC870\uAC01\uC774 \uACB9\uCCD0 \uC138\uC5B4\uC9C0\uB294 \uC9D1\uACC4\uB294 \uC5EC\uAE30\uC5D0 \uC2E4\uC81C \uBAA8\uC218\uB97C \uBC14\uC778\uB529\uD55C\uB2E4.',
3294
+ group: "appearance"
3295
+ },
3276
3296
  {
3277
3297
  name: "donutCenter",
3278
3298
  kind: "object",
package/dist/rich.d.ts CHANGED
@@ -39,6 +39,19 @@ interface ChartProps {
39
39
  valueFormat?: ChartValueFormat;
40
40
  /** 범례 표시 여부. 기본 true (단일 시리즈 카르테시안 차트는 생략). */
41
41
  legend?: boolean;
42
+ /**
43
+ * pie/donut 전용: 범례 위치. `'right'`는 아래쪽 점 범례 대신 차트 오른쪽에
44
+ * 전체 행 + 조각별 값·점유율 표를 붙인다({@link SliceLegendTable}).
45
+ * 카르테시안 차트에서는 무시된다. 기본 'bottom'.
46
+ */
47
+ legendPlacement?: 'bottom' | 'right';
48
+ /**
49
+ * 우측 수치 표의 "전체" 값(점유율 분모) 재정의. 생략하면 조각 값의 합.
50
+ *
51
+ * 조각이 겹치는 집계(수리 중인 장비가 대여 상태 어느 한쪽에도 이미 세어진 경우 등)는
52
+ * 합이 실제 모수보다 커진다 — 화면의 다른 총계와 어긋나 보이지 않게 모수를 직접 준다.
53
+ */
54
+ legendTotal?: number;
42
55
  /** donut 중앙 오버레이 텍스트. */
43
56
  donutCenter?: {
44
57
  label?: string;
@@ -47,7 +60,7 @@ interface ChartProps {
47
60
  /** 데이터가 없을 때 보여줄 문구. */
48
61
  emptyText?: string;
49
62
  }
50
- declare function Chart({ type, data, xKey, series, height, valueFormat, legend, donutCenter, emptyText, }: ChartProps): React__default.JSX.Element;
63
+ declare function Chart({ type, data, xKey, series, height, valueFormat, legend, legendPlacement, legendTotal, donutCenter, emptyText, }: ChartProps): React__default.JSX.Element;
51
64
 
52
65
  /**
53
66
  * MapPreview — OpenLayers 기반 위치 미리보기 지도 (ERP `app/components/widgets/MapPreview.tsx` 이식).
package/dist/rich.js CHANGED
@@ -149,6 +149,65 @@ function HtmlLegend({ items }) {
149
149
  }
150
150
  );
151
151
  }
152
+ function SliceLegendTable({
153
+ rows,
154
+ format,
155
+ total
156
+ }) {
157
+ const denom = total ?? rows.reduce((sum, r) => sum + r.value, 0);
158
+ const share = (v) => denom > 0 ? `${(v / denom * 100).toLocaleString("ko-KR", { minimumFractionDigits: 1, maximumFractionDigits: 1 })}%` : "0.0%";
159
+ const valueStyle = {
160
+ fontSize: 13,
161
+ fontWeight: 600,
162
+ color: "var(--wp-text-strong)",
163
+ fontVariantNumeric: "tabular-nums",
164
+ whiteSpace: "nowrap"
165
+ };
166
+ return /* @__PURE__ */ jsxs(
167
+ "div",
168
+ {
169
+ style: {
170
+ flex: "none",
171
+ minWidth: 148,
172
+ display: "flex",
173
+ flexDirection: "column",
174
+ justifyContent: "center",
175
+ gap: "var(--wp-space-2)"
176
+ },
177
+ children: [
178
+ /* @__PURE__ */ jsxs(
179
+ "div",
180
+ {
181
+ style: {
182
+ display: "flex",
183
+ alignItems: "baseline",
184
+ justifyContent: "space-between",
185
+ gap: "var(--wp-space-4)",
186
+ paddingBottom: "var(--wp-space-2)",
187
+ borderBottom: "1px solid var(--wp-divider)"
188
+ },
189
+ children: [
190
+ /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "\uC804\uCCB4" }),
191
+ /* @__PURE__ */ jsx("span", { style: valueStyle, children: `${format(denom)} (${share(denom)})` })
192
+ ]
193
+ }
194
+ ),
195
+ rows.map((row, i) => /* @__PURE__ */ jsxs(
196
+ "div",
197
+ {
198
+ style: { display: "flex", alignItems: "center", gap: "var(--wp-space-2)" },
199
+ children: [
200
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, style: dotStyle(row.color) }),
201
+ /* @__PURE__ */ jsx("span", { className: "wp-caption", style: { flex: 1, minWidth: 0, overflowWrap: "anywhere" }, children: row.label }),
202
+ /* @__PURE__ */ jsx("span", { style: valueStyle, children: `${format(row.value)} (${share(row.value)})` })
203
+ ]
204
+ },
205
+ `${row.label}-${i}`
206
+ ))
207
+ ]
208
+ }
209
+ );
210
+ }
152
211
  var axisTick = { fill: "var(--wp-text-muted)", fontSize: 12 };
153
212
  function Chart({
154
213
  type,
@@ -158,6 +217,8 @@ function Chart({
158
217
  height = 300,
159
218
  valueFormat = "number",
160
219
  legend = true,
220
+ legendPlacement = "bottom",
221
+ legendTotal,
161
222
  donutCenter,
162
223
  emptyText
163
224
  }) {
@@ -218,9 +279,12 @@ function Chart({
218
279
  label: String(row[nameKey] ?? `\uD56D\uBAA9 ${i + 1}`),
219
280
  color: sliceColor(row, i)
220
281
  }));
221
- return shell(
222
- /* @__PURE__ */ jsxs(Fragment, { children: [
223
- /* @__PURE__ */ jsxs("div", { style: { position: "relative", width: "100%", height }, children: [
282
+ const sideLegend = legend && legendPlacement === "right";
283
+ const chartArea = /* @__PURE__ */ jsxs(
284
+ "div",
285
+ {
286
+ style: sideLegend ? { position: "relative", flex: "1 1 0%", minWidth: 0, height } : { position: "relative", width: "100%", height },
287
+ children: [
224
288
  /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(PieChart, { children: [
225
289
  /* @__PURE__ */ jsx(Tooltip, { content: tooltipContent }),
226
290
  /* @__PURE__ */ jsx(
@@ -259,7 +323,32 @@ function Chart({
259
323
  ]
260
324
  }
261
325
  )
262
- ] }),
326
+ ]
327
+ }
328
+ );
329
+ if (sideLegend) {
330
+ const sliceValue = (row) => {
331
+ const n = Number(row[valueKey]);
332
+ return Number.isFinite(n) ? n : 0;
333
+ };
334
+ const denomOverride = Number(legendTotal);
335
+ return shell(
336
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "var(--wp-space-6)" }, children: [
337
+ chartArea,
338
+ /* @__PURE__ */ jsx(
339
+ SliceLegendTable,
340
+ {
341
+ rows: rows.map((row, i) => ({ ...legendItems2[i], value: sliceValue(row) })),
342
+ format,
343
+ total: legendTotal !== void 0 && Number.isFinite(denomOverride) ? denomOverride : void 0
344
+ }
345
+ )
346
+ ] })
347
+ );
348
+ }
349
+ return shell(
350
+ /* @__PURE__ */ jsxs(Fragment, { children: [
351
+ chartArea,
263
352
  legend && /* @__PURE__ */ jsx(HtmlLegend, { items: legendItems2 })
264
353
  ] })
265
354
  );
@@ -397,7 +397,13 @@ interface SduiPageQuery {
397
397
  }
398
398
  interface SduiPage {
399
399
  version: 1;
400
- /** 문서 <title> 및 페이지 헤더에 쓰인다. */
400
+ /**
401
+ * 문서 <title> 및 페이지 헤더에 쓰인다.
402
+ *
403
+ * body **최상위**에 `PageHead` 노드가 있으면 자동 페이지 헤더는 접히고 그쪽이
404
+ * 시각 헤더가 된다 — 제목 줄 우측에 액션(필터·버튼)을 놓으려는 스펙이
405
+ * title(문서 제목)을 포기하지 않아도 되는 자리다.
406
+ */
401
407
  title?: string;
402
408
  subtitle?: string;
403
409
  /** 실시간 채널 구독 — 어댑터의 subscribe로 위임(이벤트 수신 시 조용한 재조회) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipco/sdui",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "WIPCO 서버 주도 UI(SDUI) 렌더러 — JSON 페이지 스펙을 React 컴포넌트로 렌더링",
5
5
  "keywords": [
6
6
  "sdui",