@wipco/sdui 1.0.0 → 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 +2 -2
- package/dist/index.js +23 -2
- package/dist/meta.d.ts +1 -1
- package/dist/meta.js +20 -0
- package/dist/rich.d.ts +14 -1
- package/dist/rich.js +93 -4
- package/dist/{types-C7y9CpgF.d.ts → types-COjGKgmd.d.ts} +7 -1
- package/package.json +5 -2
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-
|
|
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-
|
|
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
|
}
|
|
@@ -2503,6 +2506,11 @@ function SduiFragmentBody({
|
|
|
2503
2506
|
}
|
|
2504
2507
|
return /* @__PURE__ */ jsx(SduiFragmentChainContext.Provider, { value: nextChain, children: page.body.map((child, i) => renderChild(child, i, bodyScope, engine)) });
|
|
2505
2508
|
}
|
|
2509
|
+
function urlSeedSettled(state, seeded, baseline) {
|
|
2510
|
+
return Object.keys(seeded).every(
|
|
2511
|
+
(key) => Object.is(state[key], seeded[key]) || !Object.is(state[key], baseline[key])
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2506
2514
|
function SduiRenderer({
|
|
2507
2515
|
page,
|
|
2508
2516
|
registry = defaultRegistry,
|
|
@@ -2520,6 +2528,8 @@ function SduiRenderer({
|
|
|
2520
2528
|
});
|
|
2521
2529
|
const [state, setState] = useState(() => ({ ...initialState ?? {} }));
|
|
2522
2530
|
const urlSeeded = useRef(false);
|
|
2531
|
+
const urlSeedPending = useRef(null);
|
|
2532
|
+
const urlHomePath = useRef(typeof window !== "undefined" ? window.location.pathname : "");
|
|
2523
2533
|
const [toasts, setToasts] = useState([]);
|
|
2524
2534
|
const toastSeq = useRef(0);
|
|
2525
2535
|
const toastTimers = useRef([]);
|
|
@@ -2554,10 +2564,21 @@ function SduiRenderer({
|
|
|
2554
2564
|
}
|
|
2555
2565
|
}
|
|
2556
2566
|
}
|
|
2557
|
-
if (Object.keys(seeded).length > 0)
|
|
2567
|
+
if (Object.keys(seeded).length > 0) {
|
|
2568
|
+
const baseline = {};
|
|
2569
|
+
for (const key of Object.keys(seeded)) baseline[key] = stateRef.current[key];
|
|
2570
|
+
urlSeedPending.current = { seeded, baseline };
|
|
2571
|
+
setEntries(seeded);
|
|
2572
|
+
}
|
|
2558
2573
|
}, [page.urlState, setEntries, syncUrlState]);
|
|
2559
2574
|
useEffect(() => {
|
|
2560
2575
|
if (!syncUrlState || !page.urlState?.length || !urlSeeded.current || typeof window === "undefined") return;
|
|
2576
|
+
if (window.location.pathname !== urlHomePath.current) return;
|
|
2577
|
+
if (urlSeedPending.current) {
|
|
2578
|
+
const { seeded, baseline } = urlSeedPending.current;
|
|
2579
|
+
if (!urlSeedSettled(state, seeded, baseline)) return;
|
|
2580
|
+
urlSeedPending.current = null;
|
|
2581
|
+
}
|
|
2561
2582
|
const params = new URLSearchParams(window.location.search);
|
|
2562
2583
|
let changed = false;
|
|
2563
2584
|
for (const key of page.urlState) {
|
|
@@ -3017,7 +3038,7 @@ function SduiRenderer({
|
|
|
3017
3038
|
// 컴포넌트(목록 조회 등)도 호스트가 건 잠금 안에서 움직이게 하기 위해서다(adapter.ts 주석).
|
|
3018
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: [
|
|
3019
3040
|
/* @__PURE__ */ jsx(HoistedStyle, { id: "wp-sdui-layout", css: LAYOUT_CSS }),
|
|
3020
|
-
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: [
|
|
3021
3042
|
page.title ? /* @__PURE__ */ jsx("h1", { className: "wp-page-head__title", children: page.title }) : null,
|
|
3022
3043
|
page.subtitle ? /* @__PURE__ */ jsx("div", { className: "wp-page-head__sub", children: page.subtitle }) : null
|
|
3023
3044
|
] }) }) : null,
|
package/dist/meta.d.ts
CHANGED
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
/**
|
|
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.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "WIPCO 서버 주도 UI(SDUI) 렌더러 — JSON 페이지 스펙을 React 컴포넌트로 렌더링",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sdui",
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
"dev": "tsup --watch",
|
|
209
209
|
"typecheck": "tsc --noEmit",
|
|
210
210
|
"check:meta": "node scripts/check-meta.mjs",
|
|
211
|
-
"test": "tsx --test test/*.test.ts",
|
|
211
|
+
"test": "tsx --test test/*.test.ts test/*.test.tsx",
|
|
212
212
|
"prepack": "pnpm run build"
|
|
213
213
|
},
|
|
214
214
|
"peerDependencies": {
|
|
@@ -232,6 +232,9 @@
|
|
|
232
232
|
"devDependencies": {
|
|
233
233
|
"@types/react": "^19.0.0",
|
|
234
234
|
"@types/react-dom": "^19.0.0",
|
|
235
|
+
"jsdom": "^26.1.0",
|
|
236
|
+
"react": "^19.2.8",
|
|
237
|
+
"react-dom": "^19.2.8",
|
|
235
238
|
"tsup": "^8.5.0",
|
|
236
239
|
"tsx": "^4.20.0",
|
|
237
240
|
"typescript": "^5.7.0"
|