@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/index.js CHANGED
@@ -1,5 +1,9 @@
1
- import { Toast, Textarea, Tag, Tabs, Table, Switch, StatCard, Spinner, Select, ProgressBar, Pagination, Input, IconButton, Icon, Field, EmptyState, DateRangePicker, DatePicker, Checkbox, Card, Button, Badge, Alert, Dialog } from './chunk-AR2JYE75.js';
2
- export { Alert, Badge, Button, Card, Checkbox, DatePicker, DateRangePicker, Dialog, EmptyState, Field, Icon, IconButton, Input, Pagination, ProgressBar, Select, Spinner, StatCard, Switch, Table, Tabs, Tag, Textarea, Toast } from './chunk-AR2JYE75.js';
1
+ import { DateRangePicker, DatePicker, HoistedStyle } from './chunk-YJMD6MOE.js';
2
+ export { DatePicker, DateRangePicker } from './chunk-YJMD6MOE.js';
3
+ import { searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-KADFN76X.js';
4
+ export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-KADFN76X.js';
5
+ import { Toast, Textarea, Tag, Tabs, Table, Switch, StatCard, Spinner, Select, ProgressBar, Pagination, Input, IconButton, Icon, Field, EmptyState, Checkbox, Card, Button, Badge, Alert, SegmentedControl, Dialog } from './chunk-ZUCI34QY.js';
6
+ export { Alert, Avatar, Badge, Breadcrumb, Button, Card, Checkbox, Dialog, EmptyState, Field, Icon, IconButton, Input, Pagination, ProgressBar, Radio, SegmentedControl, Select, SidebarNav, Spinner, StatCard, Switch, Table, Tabs, Tag, Textarea, Toast, Tooltip } from './chunk-ZUCI34QY.js';
3
7
  import React, { createContext, useContext, useMemo, useState, useRef, useEffect, useCallback } from 'react';
4
8
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
9
 
@@ -229,6 +233,110 @@ function Show({ when, children }) {
229
233
  function Divider({ style }) {
230
234
  return /* @__PURE__ */ jsx("hr", { className: "wp-divider", style });
231
235
  }
236
+ function toNumber(value) {
237
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
238
+ if (typeof value === "string") {
239
+ const plain = value.replace(/,/g, "").trim();
240
+ if (plain !== "" && /^-?\d+(\.\d+)?$/.test(plain)) return Number(plain);
241
+ }
242
+ return null;
243
+ }
244
+ function fillTemplate(template, row) {
245
+ return template.replace(
246
+ /\{([\w.-]+)\}/g,
247
+ (_match, key) => encodeURIComponent(String(row[key] ?? ""))
248
+ );
249
+ }
250
+ function formatDate(raw, format) {
251
+ const date = new Date(raw);
252
+ if (Number.isNaN(date.getTime())) return raw;
253
+ if (format === "datetime") return date.toLocaleString("ko-KR");
254
+ if (format === "time") return date.toLocaleTimeString("ko-KR");
255
+ return date.toLocaleDateString("ko-KR");
256
+ }
257
+ function cellRenderer(cell, key, dispatch) {
258
+ if (!cell || typeof cell !== "object" || typeof cell.type !== "string") return void 0;
259
+ switch (cell.type) {
260
+ case "badge":
261
+ return (row) => {
262
+ const value = String(row[key] ?? "");
263
+ if (!value) return null;
264
+ return /* @__PURE__ */ jsx(Badge, { tone: cell.toneMap?.[value] ?? "neutral", children: value });
265
+ };
266
+ case "tag":
267
+ return (row) => {
268
+ const value = String(row[key] ?? "");
269
+ if (!value) return null;
270
+ return /* @__PURE__ */ jsx(Tag, { color: cell.colorMap?.[value], filled: cell.filled, children: value });
271
+ };
272
+ case "link":
273
+ return (row) => {
274
+ const label = String(row[cell.labelKey ?? key] ?? "");
275
+ if (!label) return null;
276
+ const to = fillTemplate(cell.to ?? "", row);
277
+ return (
278
+ // Button이 아니라 <a>인 이유: 링크는 마우스 오른쪽 클릭·새 탭이 되어야 하는데
279
+ // ButtonProps에 href가 없다. 클래스는 DS의 링크 버튼(.wp-btn--link)을 그대로 쓴다.
280
+ /* @__PURE__ */ jsxs(
281
+ "a",
282
+ {
283
+ className: "wp-btn wp-btn--link",
284
+ href: to,
285
+ onClick: (e) => {
286
+ e.preventDefault();
287
+ e.stopPropagation();
288
+ dispatch({ action: "navigate", to });
289
+ },
290
+ children: [
291
+ cell.icon ? /* @__PURE__ */ jsx(Icon, { name: cell.icon, size: 14 }) : null,
292
+ label
293
+ ]
294
+ }
295
+ )
296
+ );
297
+ };
298
+ case "progress":
299
+ return (row) => {
300
+ const value = toNumber(row[key]);
301
+ if (value === null) return String(row[key] ?? "");
302
+ const max = (cell.maxKey ? toNumber(row[cell.maxKey]) : null) ?? cell.max ?? 100;
303
+ return /* @__PURE__ */ jsx(ProgressBar, { value, max: max || 100, tone: cell.tone, showValue: cell.showValue, size: "sm" });
304
+ };
305
+ case "icon":
306
+ return (row) => {
307
+ const value = String(row[key] ?? "");
308
+ const name = cell.iconMap?.[value] ?? cell.icon;
309
+ if (!name) return value;
310
+ return (
311
+ // .wp-row 기본 간격(space-2)은 아이콘+글자 한 덩어리로 보이기엔 넓어 한 단계 좁힌다
312
+ /* @__PURE__ */ jsxs("span", { className: "wp-row", style: { gap: "var(--wp-space-1)" }, children: [
313
+ /* @__PURE__ */ jsx(Icon, { name, size: 14, title: cell.withLabel ? void 0 : value }),
314
+ cell.withLabel ? value : null
315
+ ] })
316
+ );
317
+ };
318
+ case "number":
319
+ return (row) => {
320
+ const value = toNumber(row[key]);
321
+ if (value === null) return String(row[key] ?? "");
322
+ const digits = Number.isFinite(cell.digits) ? Math.max(0, Math.min(6, Number(cell.digits))) : 0;
323
+ const text = new Intl.NumberFormat("ko-KR", {
324
+ minimumFractionDigits: digits,
325
+ maximumFractionDigits: digits
326
+ }).format(value);
327
+ return `${cell.prefix ?? ""}${text}${cell.suffix ?? ""}`;
328
+ };
329
+ case "date":
330
+ return (row) => {
331
+ const raw = String(row[key] ?? "");
332
+ return raw ? formatDate(raw, cell.format) : "";
333
+ };
334
+ case "text":
335
+ return void 0;
336
+ default:
337
+ return void 0;
338
+ }
339
+ }
232
340
  var defaultFetcher = async (endpoint, params, signal) => {
233
341
  const query = params.toString();
234
342
  const url = `${endpoint}${query ? endpoint.includes("?") ? "&" : "?" : ""}${query}`;
@@ -289,78 +397,8 @@ function useRemoteSearchPage(endpoint, params) {
289
397
  const reload = useCallback(() => setReloadKey((value) => value + 1), []);
290
398
  return { page, isLoading, error, reload };
291
399
  }
292
- var HOST_STYLE = { position: "relative", display: "inline-flex" };
293
- var PANEL_STYLE = {
294
- position: "absolute",
295
- top: "calc(100% + var(--wp-space-2))",
296
- minWidth: "100%",
297
- background: "var(--wp-surface-card)",
298
- border: "1px solid var(--wp-border-subtle)",
299
- borderRadius: "var(--wp-radius-lg)",
300
- boxShadow: "var(--wp-shadow-lg)",
301
- // 다이얼로그(--wp-z-dialog: 100)보다는 아래 — 모달 위로 삐져나오면 안 된다
302
- zIndex: 90
303
- };
304
- var BODY_STYLE = {
305
- maxHeight: "60vh",
306
- overflow: "auto",
307
- padding: "var(--wp-space-4)"
308
- };
309
- var FOOT_STYLE = {
310
- display: "flex",
311
- alignItems: "center",
312
- justifyContent: "space-between",
313
- gap: "var(--wp-space-2)",
314
- padding: "var(--wp-space-3)",
315
- borderTop: "1px solid var(--wp-divider)"
316
- };
317
- function Popover({
318
- open,
319
- onClose,
320
- trigger,
321
- footer,
322
- alignLeft = true,
323
- width,
324
- children
325
- }) {
326
- const hostRef = useRef(null);
327
- const closeRef = useRef(onClose);
328
- closeRef.current = onClose;
329
- useEffect(() => {
330
- if (!open) return;
331
- const closeOnOutside = (event) => {
332
- if (event.target instanceof Node && hostRef.current?.contains(event.target)) return;
333
- closeRef.current();
334
- };
335
- const closeOnEscape = (event) => {
336
- if (event.key === "Escape") closeRef.current();
337
- };
338
- document.addEventListener("pointerdown", closeOnOutside);
339
- document.addEventListener("keydown", closeOnEscape);
340
- return () => {
341
- document.removeEventListener("pointerdown", closeOnOutside);
342
- document.removeEventListener("keydown", closeOnEscape);
343
- };
344
- }, [open]);
345
- return /* @__PURE__ */ jsxs("div", { ref: hostRef, style: HOST_STYLE, children: [
346
- trigger,
347
- open ? /* @__PURE__ */ jsxs(
348
- "div",
349
- {
350
- style: {
351
- ...PANEL_STYLE,
352
- ...alignLeft ? { left: 0 } : { right: 0 },
353
- ...width ? { width } : null
354
- },
355
- children: [
356
- /* @__PURE__ */ jsx("div", { style: BODY_STYLE, children }),
357
- footer ? /* @__PURE__ */ jsx("div", { style: FOOT_STYLE, children: footer }) : null
358
- ]
359
- }
360
- ) : null
361
- ] });
362
- }
363
400
  var OVERSCAN = 10;
401
+ var CLIP_CSS = `.wp-table--clip tbody td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}`;
364
402
  function ResultTable({
365
403
  columns,
366
404
  rows,
@@ -395,47 +433,25 @@ function ResultTable({
395
433
  const table = /* @__PURE__ */ jsxs(
396
434
  "table",
397
435
  {
398
- className: "wp-table wp-table--hover",
436
+ className: `wp-table wp-table--hover${virtual ? " wp-table--sticky wp-table--clip" : ""}`,
399
437
  style: virtual ? { tableLayout: "fixed" } : void 0,
400
438
  children: [
401
439
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columns.map((column) => /* @__PURE__ */ jsx(
402
440
  "th",
403
441
  {
404
- style: {
405
- width: column.width,
406
- textAlign: column.align === "right" ? "right" : void 0,
407
- // wp-table--sticky 클래스가 없으므로 가상 모드에서만 직접 고정한다
408
- ...virtual ? { position: "sticky", top: 0, zIndex: 1, background: "var(--wp-surface-card)" } : null
409
- },
410
- children: column.sortable && onSort ? /* @__PURE__ */ jsxs(
411
- "button",
412
- {
413
- type: "button",
414
- onClick: () => onSort(column.key),
415
- style: {
416
- display: "inline-flex",
417
- alignItems: "center",
418
- gap: 3,
419
- background: "none",
420
- border: 0,
421
- padding: 0,
422
- cursor: "pointer",
423
- color: "inherit",
424
- font: "inherit",
425
- letterSpacing: "inherit",
426
- textTransform: "inherit"
427
- },
428
- children: [
429
- column.header,
430
- /* @__PURE__ */ jsx(
431
- Icon,
432
- {
433
- name: sort && sort.key === column.key ? sort.dir === "asc" ? "chevron-up" : "chevron-down" : "chevrons-up-down",
434
- size: 12
435
- }
436
- )
437
- ]
438
- }
442
+ style: { width: column.width, textAlign: column.align === "right" ? "right" : void 0 },
443
+ children: column.sortable && onSort ? (
444
+ // 인라인 style로는 :hover를 표현할 수 없다 — 색 피드백은 .wp-table__sort:hover가 낸다
445
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "wp-table__sort", onClick: () => onSort(column.key), children: [
446
+ column.header,
447
+ /* @__PURE__ */ jsx(
448
+ Icon,
449
+ {
450
+ name: sort && sort.key === column.key ? sort.dir === "asc" ? "chevron-up" : "chevron-down" : "chevrons-up-down",
451
+ size: 12
452
+ }
453
+ )
454
+ ] })
439
455
  ) : column.header
440
456
  },
441
457
  column.key
@@ -451,24 +467,13 @@ function ResultTable({
451
467
  ref: offset === 0 ? firstRowRef : void 0,
452
468
  "aria-selected": selected || void 0,
453
469
  onClick: onRowClick ? () => onRowClick(row) : void 0,
454
- style: {
455
- ...onRowClick ? { cursor: "pointer" } : null,
456
- ...selected ? { background: "var(--wp-surface-selected)" } : null
457
- },
470
+ style: onRowClick ? { cursor: "pointer" } : void 0,
458
471
  children: columns.map((column) => {
459
472
  const cls = [];
460
473
  if (column.align === "right") cls.push("wp-table__num");
461
474
  if (column.mono) cls.push("wp-table__mono");
462
475
  if (column.strong) cls.push("wp-table__strong");
463
- return /* @__PURE__ */ jsx(
464
- "td",
465
- {
466
- className: cls.join(" ") || void 0,
467
- style: virtual ? { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } : void 0,
468
- children: column.render ? column.render(row, index) : row[column.key]
469
- },
470
- column.key
471
- );
476
+ return /* @__PURE__ */ jsx("td", { className: cls.join(" ") || void 0, children: column.render ? column.render(row, index) : row[column.key] }, column.key);
472
477
  })
473
478
  },
474
479
  row.id ?? index
@@ -480,497 +485,40 @@ function ResultTable({
480
485
  }
481
486
  );
482
487
  if (!virtual) return table;
483
- return /* @__PURE__ */ jsx("div", { ref: scrollRef, onScroll: handleScroll, style: { height, overflowY: "auto" }, children: table });
488
+ return /* @__PURE__ */ jsxs("div", { ref: scrollRef, onScroll: handleScroll, style: { height, overflowY: "auto" }, children: [
489
+ /* @__PURE__ */ jsx(HoistedStyle, { id: "wp-table-clip", css: CLIP_CSS }),
490
+ table
491
+ ] });
484
492
  }
485
493
  function ResultEmptyState({ title }) {
486
- return (
487
- // 아이콘 세트에 scan-search가 없어 같은 뜻의 search로 대체
488
- /* @__PURE__ */ jsx(EmptyState, { icon: "search", title: `${title}\uC774(\uAC00) \uC5C6\uC2B5\uB2C8\uB2E4`, tight: true, children: "\uAC80\uC0C9 \uC870\uAC74\uC744 \uBC14\uAFB8\uAC70\uB098 \uCD08\uAE30\uD654\uD574 \uBCF4\uC138\uC694." })
489
- );
490
- }
491
- function splitMultiValue(value) {
492
- return value.split(",").map((item) => item.trim()).filter(Boolean);
493
- }
494
- function joinMultiValue(values) {
495
- return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).sort().join(",");
496
- }
497
- function normalizeSearchText(value) {
498
- return value.toLowerCase().replace(/\s+/g, "");
499
- }
500
- function includesLooseSearch(text, keyword) {
501
- const normalizedKeyword = normalizeSearchText(keyword);
502
- return normalizedKeyword.length === 0 || normalizeSearchText(text).includes(normalizedKeyword);
503
- }
504
- function searchFieldKeys(fields) {
505
- return fields.flatMap((field) => {
506
- if (field.type === "dateRange") {
507
- return [field.fromKey ?? `${field.key}From`, field.toKey ?? `${field.key}To`];
508
- }
509
- if (field.type === "numberRange") {
510
- return [field.fromKey ?? `${field.key}Min`, field.toKey ?? `${field.key}Max`];
511
- }
512
- return [field.key];
513
- });
514
- }
515
- function searchValuesFromParams(fields, params) {
516
- const values = {};
517
- for (const key of searchFieldKeys(fields)) {
518
- const value = params.get(key);
519
- if (value) values[key] = value;
520
- }
521
- return values;
522
- }
523
- function cleanSearchValues(fields, values) {
524
- const allowed = new Set(searchFieldKeys(fields));
525
- const fieldsByKey = new Map(fields.map((field) => [field.key, field]));
526
- return Object.fromEntries(
527
- Object.entries(values).map(([key, value]) => {
528
- const field = fieldsByKey.get(key);
529
- const cleaned = field?.type === "select" || field?.type === "cascading" ? joinMultiValue(splitMultiValue(value)) : value.trim();
530
- return [key, cleaned];
531
- }).filter(([key, value]) => allowed.has(key) && value)
532
- );
533
- }
534
- function cascadingPathMap(options) {
535
- const paths = /* @__PURE__ */ new Map();
536
- const visit = (option, parents) => {
537
- const path = [...parents, option];
538
- paths.set(option.value, path);
539
- option.children?.forEach((child) => visit(child, path));
540
- };
541
- options.forEach((option) => visit(option, []));
542
- return paths;
543
- }
544
- function cascadingValuePaths(options) {
545
- const map = /* @__PURE__ */ new Map();
546
- for (const [value, path] of cascadingPathMap(options)) {
547
- map.set(
548
- value,
549
- path.map((option) => option.value)
550
- );
551
- }
552
- return map;
553
- }
554
- function cascadingPathLabel(pathMap, value) {
555
- const path = pathMap.get(value);
556
- return path?.length ? path.map((option) => option.label).join(" > ") : value;
557
- }
558
- function activeSearchCriteria(fields, values) {
559
- const criteria = [];
560
- for (const field of fields) {
561
- if (field.type === "select") {
562
- for (const value2 of splitMultiValue(values[field.key] ?? "")) {
563
- const label = field.options.find((o) => o.value === value2)?.label ?? value2;
564
- criteria.push({
565
- key: `${field.key}:${value2}`,
566
- label: `${field.label}: ${label}`,
567
- clearKeys: [field.key],
568
- valueKey: field.key,
569
- value: value2
570
- });
571
- }
572
- continue;
573
- }
574
- if (field.type === "cascading") {
575
- const pathMap = cascadingPathMap(field.options);
576
- for (const value2 of splitMultiValue(values[field.key] ?? "")) {
577
- criteria.push({
578
- key: `${field.key}:${value2}`,
579
- label: `${field.label}: ${cascadingPathLabel(pathMap, value2)}`,
580
- clearKeys: [field.key],
581
- valueKey: field.key,
582
- value: value2
583
- });
584
- }
585
- continue;
586
- }
587
- if (field.type === "dateRange" || field.type === "numberRange") {
588
- const fromKey = field.fromKey ?? `${field.key}${field.type === "dateRange" ? "From" : "Min"}`;
589
- const toKey = field.toKey ?? `${field.key}${field.type === "dateRange" ? "To" : "Max"}`;
590
- const from = values[fromKey];
591
- const to = values[toKey];
592
- if (from || to) {
593
- criteria.push({
594
- key: field.key,
595
- label: `${field.label}: ${from ?? ""}~${to ?? ""}`,
596
- clearKeys: [fromKey, toKey]
597
- });
598
- }
599
- continue;
600
- }
601
- const value = values[field.key];
602
- if (value) {
603
- criteria.push({
604
- key: field.key,
605
- label: `${field.label}: ${value}`,
606
- clearKeys: [field.key]
607
- });
608
- }
609
- }
610
- return criteria;
494
+ return /* @__PURE__ */ jsx(EmptyState, { icon: "scan-search", title: `${title}\uC774(\uAC00) \uC5C6\uC2B5\uB2C8\uB2E4`, tight: true, children: "\uAC80\uC0C9 \uC870\uAC74\uC744 \uBC14\uAFB8\uAC70\uB098 \uCD08\uAE30\uD654\uD574 \uBCF4\uC138\uC694." });
611
495
  }
496
+ var EMPTY_FILTERS = [];
612
497
  var EMPTY_FIELDS = [];
613
- var EMPTY_VALUES = {};
614
- function SearchToolbar({
615
- search,
616
- onSearchChange,
617
- onSearch,
618
- advancedFields = EMPTY_FIELDS,
619
- advancedValues = EMPTY_VALUES,
620
- placeholder = "\uAC80\uC0C9",
621
- className,
622
- hideSearchInput = false
623
- }) {
624
- const [open, setOpen] = useState(false);
625
- const [draft, setDraft] = useState(advancedValues);
626
- const hasAdvanced = advancedFields.length > 0;
627
- useEffect(() => {
628
- if (!open) setDraft(advancedValues);
629
- }, [advancedValues, open]);
630
- const activeCriteria = useMemo(
631
- () => activeSearchCriteria(advancedFields, advancedValues),
632
- [advancedFields, advancedValues]
633
- );
634
- const submit = (event) => {
635
- event.preventDefault();
636
- onSearch(search.trim(), cleanSearchValues(advancedFields, advancedValues));
637
- setOpen(false);
638
- };
639
- const applyAdvancedSearch = () => {
640
- onSearch(search.trim(), cleanSearchValues(advancedFields, draft));
641
- setOpen(false);
642
- };
643
- const resetAdvancedSearch = () => {
644
- setDraft({});
645
- onSearch(search.trim(), {});
646
- setOpen(false);
647
- };
648
- const removeCriterion = (criterion) => {
649
- const nextValues = { ...advancedValues };
650
- if (criterion.valueKey && criterion.value !== void 0) {
651
- const remaining = splitMultiValue(nextValues[criterion.valueKey] ?? "").filter(
652
- (value) => value !== criterion.value
653
- );
654
- if (remaining.length > 0) nextValues[criterion.valueKey] = joinMultiValue(remaining);
655
- else delete nextValues[criterion.valueKey];
656
- } else {
657
- criterion.clearKeys.forEach((key) => delete nextValues[key]);
658
- }
659
- setDraft(nextValues);
660
- onSearch(search.trim(), cleanSearchValues(advancedFields, nextValues));
661
- };
662
- return /* @__PURE__ */ jsxs("form", { className: ["wp-row", className].filter(Boolean).join(" "), onSubmit: submit, children: [
663
- activeCriteria.length > 0 ? /* @__PURE__ */ jsx("div", { className: "wp-row", style: { flexWrap: "wrap" }, children: activeCriteria.map((criterion) => /* @__PURE__ */ jsx(Tag, { onRemove: () => removeCriterion(criterion), children: criterion.label }, criterion.key)) }) : null,
664
- !hideSearchInput ? /* @__PURE__ */ jsx(
665
- Input,
666
- {
667
- icon: "search",
668
- value: search,
669
- onChange: (event) => onSearchChange(event.target.value),
670
- placeholder
671
- }
672
- ) : null,
673
- hasAdvanced ? /* @__PURE__ */ jsx(
674
- Popover,
675
- {
676
- open,
677
- onClose: () => setOpen(false),
678
- alignLeft: false,
679
- width: "min(560px, 80vw)",
680
- trigger: /* @__PURE__ */ jsx(
681
- IconButton,
682
- {
683
- type: "button",
684
- icon: "settings",
685
- label: "\uC0C1\uC138\uAC80\uC0C9",
686
- "aria-expanded": open,
687
- style: open ? { background: "var(--wp-surface-active)" } : void 0,
688
- onClick: () => setOpen((value) => !value)
689
- }
690
- ),
691
- footer: /* @__PURE__ */ jsxs(Fragment, { children: [
692
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", icon: "refresh-cw", onClick: resetAdvancedSearch, children: "\uCD08\uAE30\uD654" }),
693
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: applyAdvancedSearch, children: "\uC801\uC6A9" })
694
- ] }),
695
- children: /* @__PURE__ */ jsx("div", { className: "wp-grid wp-grid--2", children: advancedFields.map((field) => /* @__PURE__ */ jsx(
696
- SearchFieldInput,
697
- {
698
- field,
699
- values: draft,
700
- onChange: (patch) => setDraft((current) => ({ ...current, ...patch }))
701
- },
702
- field.key
703
- )) })
704
- }
705
- ) : null
706
- ] });
707
- }
708
- function SearchFieldInput({
709
- field,
710
- values,
711
- onChange
712
- }) {
713
- if (field.type === "select") {
714
- return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsx(MultiSelectInput, { field, values, onChange }) });
715
- }
716
- if (field.type === "cascading") {
717
- return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsx(CascadingInput, { field, values, onChange }) });
718
- }
719
- if (field.type === "dateRange") {
720
- const fromKey = field.fromKey ?? `${field.key}From`;
721
- const toKey = field.toKey ?? `${field.key}To`;
722
- return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsxs("div", { className: "wp-row", children: [
723
- /* @__PURE__ */ jsx(
724
- Input,
725
- {
726
- type: "date",
727
- mono: true,
728
- value: values[fromKey] ?? "",
729
- onChange: (e) => onChange({ [fromKey]: e.target.value })
730
- }
731
- ),
732
- /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "~" }),
733
- /* @__PURE__ */ jsx(
734
- Input,
735
- {
736
- type: "date",
737
- mono: true,
738
- value: values[toKey] ?? "",
739
- onChange: (e) => onChange({ [toKey]: e.target.value })
740
- }
741
- )
742
- ] }) });
743
- }
744
- if (field.type === "numberRange") {
745
- const fromKey = field.fromKey ?? `${field.key}Min`;
746
- const toKey = field.toKey ?? `${field.key}Max`;
747
- return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsxs("div", { className: "wp-row", children: [
748
- /* @__PURE__ */ jsx(
749
- Input,
750
- {
751
- type: "number",
752
- mono: true,
753
- placeholder: field.minPlaceholder ?? "\uCD5C\uC18C",
754
- value: values[fromKey] ?? "",
755
- onChange: (e) => onChange({ [fromKey]: e.target.value })
756
- }
757
- ),
758
- /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "~" }),
759
- /* @__PURE__ */ jsx(
760
- Input,
761
- {
762
- type: "number",
763
- mono: true,
764
- placeholder: field.maxPlaceholder ?? "\uCD5C\uB300",
765
- value: values[toKey] ?? "",
766
- onChange: (e) => onChange({ [toKey]: e.target.value })
767
- }
768
- )
769
- ] }) });
770
- }
771
- return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsx(
772
- Input,
773
- {
774
- placeholder: field.placeholder,
775
- value: values[field.key] ?? "",
776
- onChange: (e) => onChange({ [field.key]: e.target.value })
777
- }
778
- ) });
779
- }
780
- function multiSelectLabel(field, values) {
781
- if (values.length === 0) return field.allLabel ?? "\uC804\uCCB4";
782
- const labels = values.map((value) => field.options.find((item) => item.value === value)?.label ?? value);
783
- if (labels.length <= 2) return labels.join(", ");
784
- return `${labels[0]} \uC678 ${labels.length - 1}`;
785
- }
786
- function MultiSelectInput({
787
- field,
788
- values,
789
- onChange
790
- }) {
791
- const [open, setOpen] = useState(false);
792
- const selected = splitMultiValue(values[field.key] ?? "");
793
- const toggle = (value, checked) => {
794
- const next = checked ? [...selected, value] : selected.filter((item) => item !== value);
795
- onChange({ [field.key]: joinMultiValue(next) });
796
- };
797
- return /* @__PURE__ */ jsx(
798
- Popover,
799
- {
800
- open,
801
- onClose: () => setOpen(false),
802
- trigger: /* @__PURE__ */ jsx(
803
- Button,
804
- {
805
- type: "button",
806
- variant: "secondary",
807
- block: true,
808
- iconEnd: "chevron-down",
809
- onClick: () => setOpen((value) => !value),
810
- children: multiSelectLabel(field, selected)
811
- }
812
- ),
813
- children: /* @__PURE__ */ jsx("div", { className: "wp-col", children: field.options.map((option) => /* @__PURE__ */ jsx(
814
- Checkbox,
815
- {
816
- label: option.label,
817
- checked: selected.includes(option.value),
818
- onChange: (e) => toggle(option.value, e.target.checked)
819
- },
820
- option.value
821
- )) })
822
- }
498
+ var PAGE_SIZE_CHOICES = [10, 20, 50, 100];
499
+ var DEFAULT_VIEWS = [
500
+ { value: "list", icon: "list", label: "\uBAA9\uB85D" },
501
+ { value: "grid", icon: "layout-grid", label: "\uCE74\uB4DC" }
502
+ ];
503
+ function normalizePageSizes(sizes) {
504
+ if (!Array.isArray(sizes)) return void 0;
505
+ const clean = [...new Set(sizes.map(Number).filter((n) => Number.isFinite(n) && n > 0).map(Math.floor))].sort(
506
+ (a, b) => a - b
823
507
  );
508
+ return clean.length > 0 ? clean : void 0;
824
509
  }
825
- function uniqueCascadingOptions(options) {
510
+ function resolveViewItems(views, disableGrid, disableList) {
511
+ const source = Array.isArray(views) && views.length > 0 ? views : DEFAULT_VIEWS.filter((v) => !(v.value === "grid" && disableGrid) && !(v.value === "list" && disableList));
826
512
  const seen = /* @__PURE__ */ new Set();
827
- return options.filter((option) => {
828
- if (seen.has(option.value)) return false;
829
- seen.add(option.value);
830
- return true;
831
- });
832
- }
833
- function cascadingLevels(options, selectedPaths) {
834
- const levels = [];
835
- let currentOptions = options;
836
- let depth = 0;
837
- while (currentOptions.length > 0) {
838
- levels.push(currentOptions);
839
- const selectedAtDepth = uniqueCascadingOptions(
840
- selectedPaths.map((path) => path[depth]).filter(
841
- (option) => Boolean(option && currentOptions.some((current) => current.value === option.value))
842
- )
843
- );
844
- const nextOptions = uniqueCascadingOptions(selectedAtDepth.flatMap((option) => option.children ?? []));
845
- if (nextOptions.length === 0) break;
846
- currentOptions = nextOptions;
847
- depth += 1;
513
+ const items = [];
514
+ for (const view of source) {
515
+ const base = DEFAULT_VIEWS.find((d) => d.value === view?.value);
516
+ if (!base || seen.has(base.value)) continue;
517
+ seen.add(base.value);
518
+ items.push({ value: base.value, icon: view.icon ?? base.icon, label: view.label ?? base.label });
848
519
  }
849
- return levels;
850
- }
851
- function selectedValuesForCascadingLevel(selectedPaths, depth, options) {
852
- const optionValues = new Set(options.map((option) => option.value));
853
- return Array.from(
854
- new Set(
855
- selectedPaths.map((path) => path[depth]?.value).filter((value) => Boolean(value) && optionValues.has(value))
856
- )
857
- );
858
- }
859
- function cascadingPathIncludes(pathMap, value, maybeAncestor) {
860
- return pathMap.get(value)?.some((option) => option.value === maybeAncestor) ?? false;
520
+ return items.length > 0 ? items : [DEFAULT_VIEWS[0]];
861
521
  }
862
- function cascadingValuesOverlap(leftValue, rightValue, pathMap) {
863
- return cascadingPathIncludes(pathMap, leftValue, rightValue) || cascadingPathIncludes(pathMap, rightValue, leftValue);
864
- }
865
- function cascadingLevelLabel(values, allLabel, pathMap, showPathLabels) {
866
- if (values.length === 0) return allLabel;
867
- const labels = values.map((value) => {
868
- if (showPathLabels) return cascadingPathLabel(pathMap, value);
869
- return pathMap.get(value)?.at(-1)?.label ?? value;
870
- });
871
- if (labels.length <= 2) return labels.join(", ");
872
- return `${labels[0]} \uC678 ${labels.length - 1}`;
873
- }
874
- function cascadingOptionLabel(option, pathMap, showPathLabels) {
875
- return showPathLabels ? cascadingPathLabel(pathMap, option.value) : option.label;
876
- }
877
- function CascadingInput({
878
- field,
879
- values,
880
- onChange
881
- }) {
882
- const pathMap = useMemo(() => cascadingPathMap(field.options), [field.options]);
883
- const selectedValues = useMemo(
884
- () => splitMultiValue(values[field.key] ?? "").filter((value) => pathMap.has(value)),
885
- [field.key, pathMap, values]
886
- );
887
- const selectedPaths = useMemo(
888
- () => selectedValues.map((value) => pathMap.get(value)).filter((path) => Boolean(path)),
889
- [pathMap, selectedValues]
890
- );
891
- const levels = useMemo(() => cascadingLevels(field.options, selectedPaths), [field.options, selectedPaths]);
892
- const setSelectedValues = (nextValues) => {
893
- onChange({ [field.key]: joinMultiValue(nextValues) });
894
- };
895
- const toggleOption = (option, nextChecked) => {
896
- const withoutOverlap = selectedValues.filter(
897
- (value) => !cascadingValuesOverlap(option.value, value, pathMap)
898
- );
899
- setSelectedValues(nextChecked ? [...withoutOverlap, option.value] : withoutOverlap);
900
- };
901
- const clearLevel = (depth) => {
902
- if (depth === 0) {
903
- setSelectedValues([]);
904
- return;
905
- }
906
- const parents = selectedValuesForCascadingLevel(selectedPaths, depth - 1, levels[depth - 1] ?? []);
907
- const rest = selectedValues.filter((value) => {
908
- const parentValue = pathMap.get(value)?.[depth - 1]?.value;
909
- return !parentValue || !parents.includes(parentValue);
910
- });
911
- setSelectedValues([...rest, ...parents]);
912
- };
913
- return /* @__PURE__ */ jsx("div", { className: "wp-col", children: levels.map((options, depth) => {
914
- const levelValues = selectedValuesForCascadingLevel(selectedPaths, depth, options);
915
- const showPathLabels = depth > 0 && selectedValuesForCascadingLevel(selectedPaths, depth - 1, levels[depth - 1] ?? []).length > 1;
916
- return /* @__PURE__ */ jsx(
917
- CascadingLevelSelect,
918
- {
919
- allLabel: depth === 0 ? field.allLabel ?? "\uC804\uCCB4" : "\uC804\uCCB4",
920
- options,
921
- pathMap,
922
- selectedValues: levelValues,
923
- showPathLabels,
924
- onClear: () => clearLevel(depth),
925
- onToggle: toggleOption
926
- },
927
- depth
928
- );
929
- }) });
930
- }
931
- function CascadingLevelSelect({
932
- options,
933
- pathMap,
934
- selectedValues,
935
- allLabel,
936
- showPathLabels,
937
- onClear,
938
- onToggle
939
- }) {
940
- const [open, setOpen] = useState(false);
941
- const selectedSet = new Set(selectedValues);
942
- return /* @__PURE__ */ jsx(
943
- Popover,
944
- {
945
- open,
946
- onClose: () => setOpen(false),
947
- trigger: /* @__PURE__ */ jsx(
948
- Button,
949
- {
950
- type: "button",
951
- variant: "secondary",
952
- block: true,
953
- iconEnd: "chevron-down",
954
- onClick: () => setOpen((value) => !value),
955
- children: cascadingLevelLabel(selectedValues, allLabel, pathMap, showPathLabels)
956
- }
957
- ),
958
- footer: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "xs", icon: "refresh-cw", onClick: onClear, children: allLabel }),
959
- children: /* @__PURE__ */ jsx("div", { className: "wp-col", style: { minWidth: 200 }, children: options.map((option) => /* @__PURE__ */ jsx(
960
- Checkbox,
961
- {
962
- label: cascadingOptionLabel(option, pathMap, showPathLabels),
963
- checked: selectedSet.has(option.value),
964
- onChange: (e) => onToggle(option, e.target.checked)
965
- },
966
- option.value
967
- )) })
968
- }
969
- );
970
- }
971
- var EMPTY_FILTERS = [];
972
- var EMPTY_FIELDS2 = [];
973
- var PAGE_SIZE_CHOICES = [10, 20, 50, 100];
974
522
  function SearchResults({
975
523
  item,
976
524
  columns,
@@ -978,6 +526,8 @@ function SearchResults({
978
526
  leftFilters = EMPTY_FILTERS,
979
527
  rightFilters = EMPTY_FILTERS,
980
528
  initialView = "list",
529
+ views,
530
+ pageSizes,
981
531
  className,
982
532
  searchParams,
983
533
  onParamsChange,
@@ -989,26 +539,28 @@ function SearchResults({
989
539
  headerActions,
990
540
  isLoading = false,
991
541
  hideHeader = false,
992
- advancedSearchFields = EMPTY_FIELDS2,
542
+ advancedSearchFields = EMPTY_FIELDS,
993
543
  searchPlaceholder,
994
544
  virtual
995
545
  }) {
996
546
  const hasActions = actions.length > 0;
547
+ const sizeChoices = normalizePageSizes(pageSizes) ?? PAGE_SIZE_CHOICES;
548
+ const defaultPageSize = sizeChoices[0];
997
549
  const page = Number(searchParams.get("page")) || 1;
998
- const pageSize = Number(searchParams.get("limit")) || 10;
999
- const pageSizeChoices = PAGE_SIZE_CHOICES.includes(pageSize) ? PAGE_SIZE_CHOICES : [...PAGE_SIZE_CHOICES, pageSize].sort((a, b) => a - b);
1000
- const applyViewConstraints = (candidate) => {
1001
- if (disableGrid && disableList) return "list";
1002
- if (candidate === "grid" && disableGrid) return "list";
1003
- if (candidate === "list" && disableList) return "grid";
1004
- return candidate;
1005
- };
550
+ const pageSize = Number(searchParams.get("limit")) || defaultPageSize;
551
+ const pageSizeChoices = sizeChoices.includes(pageSize) ? sizeChoices : [...sizeChoices, pageSize].sort((a, b) => a - b);
552
+ const viewItems = useMemo(
553
+ () => resolveViewItems(views, disableGrid, disableList),
554
+ [views, disableGrid, disableList]
555
+ );
556
+ const applyViewConstraints = (candidate) => viewItems.some((v) => v.value === candidate) ? candidate : viewItems[0].value;
1006
557
  const appliedSearch = searchParams.get("search") ?? "";
1007
558
  const [view, setView] = useState(() => applyViewConstraints(initialView));
1008
559
  const [search, setSearch] = useState(appliedSearch);
1009
560
  const [sort, setSort] = useState(null);
1010
561
  const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
1011
562
  const selectedRowsRef = useRef(/* @__PURE__ */ new Map());
563
+ const activeView = applyViewConstraints(view);
1012
564
  useEffect(() => {
1013
565
  setSearch(appliedSearch);
1014
566
  }, [appliedSearch]);
@@ -1097,7 +649,7 @@ function SearchResults({
1097
649
  const safePage = Math.min(Math.max(1, page), pageCount);
1098
650
  const virtualThreshold = typeof virtual === "number" ? virtual : 150;
1099
651
  const virtualEnabled = virtual === true || typeof virtual === "number";
1100
- const virtualListActive = virtualEnabled && view === "list" && processed.length > virtualThreshold;
652
+ const virtualListActive = virtualEnabled && activeView === "list" && processed.length > virtualThreshold;
1101
653
  const visibleItems = clientPaginated && !virtualListActive ? processed.slice((safePage - 1) * pageSize, safePage * pageSize) : processed;
1102
654
  const visibleIds = visibleItems.map((row) => row.id);
1103
655
  const allSelected = visibleIds.length > 0 && visibleIds.every((id) => selected.has(id));
@@ -1128,12 +680,12 @@ function SearchResults({
1128
680
  {
1129
681
  key: "__select",
1130
682
  header: /* @__PURE__ */ jsx(
1131
- SelectCheckbox,
683
+ Checkbox,
1132
684
  {
1133
685
  checked: allSelected,
1134
686
  indeterminate: !allSelected && someSelected,
1135
- label: `${title} \uC804\uCCB4 \uC120\uD0DD`,
1136
- onChange: (checked) => toggleSelectAll(checked)
687
+ "aria-label": `${title} \uC804\uCCB4 \uC120\uD0DD`,
688
+ onChange: (e) => toggleSelectAll(e.target.checked)
1137
689
  }
1138
690
  ),
1139
691
  width: "36px",
@@ -1189,7 +741,14 @@ function SearchResults({
1189
741
  placeholder: searchPlaceholder
1190
742
  }
1191
743
  ),
1192
- !disableGrid && !disableList ? /* @__PURE__ */ jsx(ViewToggle, { value: view, onChange: (next) => setView(applyViewConstraints(next)) }) : null,
744
+ viewItems.length > 1 ? /* @__PURE__ */ jsx(
745
+ SegmentedControl,
746
+ {
747
+ items: viewItems,
748
+ value: activeView,
749
+ onChange: (next) => setView(applyViewConstraints(next))
750
+ }
751
+ ) : null,
1193
752
  headerActions
1194
753
  ] })
1195
754
  ] }) : null,
@@ -1214,21 +773,16 @@ function SearchResults({
1214
773
  )) })
1215
774
  ] }) : null,
1216
775
  hasActions && selectedItems.length > 0 ? (
1217
- // 플로팅 벌크 액션 바 — 스크롤 위치와 무관하게 화면 하단 중앙에 뜬다
1218
- // (.wp-toolbar 클래스가 라이브러리 스타일시트에 없어 같은 값을 토큰으로 조립했다)
776
+ // 플로팅 벌크 액션 바 — 스크롤 위치와 무관하게 화면 하단 중앙에 뜬다.
777
+ // 생김새는 DS의 .wp-toolbar가 맡고, 여기서는 "화면 하단 중앙 고정"만 얹는다
778
+ // (--floating은 absolute라 스크롤을 따라오지 않는다).
1219
779
  /* @__PURE__ */ jsxs(
1220
780
  "div",
1221
781
  {
1222
782
  role: "toolbar",
783
+ className: "wp-toolbar",
1223
784
  "aria-label": `${title} \uC120\uD0DD \uD56D\uBAA9 \uC77C\uAD04 \uC791\uC5C5`,
1224
785
  style: {
1225
- display: "flex",
1226
- alignItems: "center",
1227
- gap: 6,
1228
- padding: 5,
1229
- background: "var(--wp-surface-card)",
1230
- border: "1px solid var(--wp-border-subtle)",
1231
- borderRadius: "var(--wp-radius-md)",
1232
786
  position: "fixed",
1233
787
  bottom: "var(--wp-space-6)",
1234
788
  left: "50%",
@@ -1244,7 +798,7 @@ function SearchResults({
1244
798
  selectedItems.length,
1245
799
  "\uAC74 \uC120\uD0DD\uB428"
1246
800
  ] }),
1247
- /* @__PURE__ */ jsx(ToolbarSeparator, {}),
801
+ /* @__PURE__ */ jsx("span", { className: "wp-toolbar__sep" }),
1248
802
  actions.map((action) => /* @__PURE__ */ jsx(
1249
803
  Button,
1250
804
  {
@@ -1256,13 +810,13 @@ function SearchResults({
1256
810
  },
1257
811
  action.label
1258
812
  )),
1259
- /* @__PURE__ */ jsx(ToolbarSeparator, {}),
813
+ /* @__PURE__ */ jsx("span", { className: "wp-toolbar__sep" }),
1260
814
  /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", icon: "x", onClick: () => setSelected(/* @__PURE__ */ new Set()), children: "\uC120\uD0DD \uD574\uC81C" })
1261
815
  ]
1262
816
  }
1263
817
  )
1264
818
  ) : null,
1265
- isLoading ? /* @__PURE__ */ jsx("div", { className: "wp-row", style: { justifyContent: "center", padding: "var(--wp-space-8) 0" }, children: /* @__PURE__ */ jsx(Spinner, { size: "lg", label: `${title} \uBD88\uB7EC\uC624\uB294 \uC911` }) }) : view === "list" ? /* @__PURE__ */ jsx(
819
+ isLoading ? /* @__PURE__ */ jsx("div", { className: "wp-row", style: { justifyContent: "center", padding: "var(--wp-space-8) 0" }, children: /* @__PURE__ */ jsx(Spinner, { size: "lg", label: `${title} \uBD88\uB7EC\uC624\uB294 \uC911` }) }) : activeView === "list" ? /* @__PURE__ */ jsx(
1266
820
  ResultTable,
1267
821
  {
1268
822
  columns: tableColumns,
@@ -1286,8 +840,11 @@ function SearchResults({
1286
840
  options: pageSizeChoices.map((size) => ({ value: String(size), label: `${size}\uAC74` })),
1287
841
  value: String(pageSize),
1288
842
  onChange: (e) => (
1289
- // 기본값 10은 파라미터를 지워 상태를 깔끔하게 유지하고, 페이지는 1로 되돌린다
1290
- updateParams({ limit: e.target.value === "10" ? null : e.target.value, page: null })
843
+ // 기본값(첫 후보)은 파라미터를 지워 상태를 깔끔하게 유지하고, 페이지는 1로 되돌린다
844
+ updateParams({
845
+ limit: e.target.value === String(defaultPageSize) ? null : e.target.value,
846
+ page: null
847
+ })
1291
848
  )
1292
849
  }
1293
850
  )
@@ -1319,99 +876,6 @@ function formatCell(value) {
1319
876
  if (value instanceof Date) return value.toLocaleString("ko-KR");
1320
877
  return value;
1321
878
  }
1322
- function ToolbarSeparator() {
1323
- return /* @__PURE__ */ jsx("span", { style: { width: 1, height: 20, background: "var(--wp-border-subtle)", flex: "none" } });
1324
- }
1325
- function SelectCheckbox({
1326
- checked,
1327
- indeterminate,
1328
- label,
1329
- onChange
1330
- }) {
1331
- const ref = useRef(null);
1332
- useEffect(() => {
1333
- if (ref.current) ref.current.indeterminate = indeterminate;
1334
- }, [indeterminate]);
1335
- return /* @__PURE__ */ jsxs("label", { className: "wp-check", children: [
1336
- /* @__PURE__ */ jsx(
1337
- "input",
1338
- {
1339
- ref,
1340
- type: "checkbox",
1341
- checked,
1342
- "aria-label": label,
1343
- onChange: (e) => onChange(e.target.checked)
1344
- }
1345
- ),
1346
- /* @__PURE__ */ jsx(
1347
- "span",
1348
- {
1349
- className: "wp-check__box",
1350
- style: indeterminate && !checked ? {
1351
- background: "var(--wp-action-primary-bg)",
1352
- borderColor: "var(--wp-action-primary-bg)",
1353
- color: "var(--wp-white)"
1354
- } : void 0,
1355
- children: /* @__PURE__ */ jsx(Icon, { name: indeterminate && !checked ? "minus" : "check", size: 12, strokeWidth: 2.5 })
1356
- }
1357
- )
1358
- ] });
1359
- }
1360
- function ViewToggle({ value, onChange }) {
1361
- const items = [
1362
- { value: "list", icon: "list", label: "\uBAA9\uB85D" },
1363
- // 아이콘 세트에 layout-grid가 없어 같은 뜻의 layout-dashboard로 대체
1364
- { value: "grid", icon: "layout-dashboard", label: "\uCE74\uB4DC" }
1365
- ];
1366
- return /* @__PURE__ */ jsx(
1367
- "div",
1368
- {
1369
- role: "tablist",
1370
- style: {
1371
- display: "inline-flex",
1372
- padding: 2,
1373
- gap: 2,
1374
- background: "var(--wp-surface-sunken)",
1375
- border: "1px solid var(--wp-border-subtle)",
1376
- borderRadius: "var(--wp-radius-md)"
1377
- },
1378
- children: items.map((tab) => {
1379
- const active = tab.value === value;
1380
- return /* @__PURE__ */ jsxs(
1381
- "button",
1382
- {
1383
- type: "button",
1384
- role: "tab",
1385
- "aria-selected": active,
1386
- onClick: () => onChange(tab.value),
1387
- style: {
1388
- display: "inline-flex",
1389
- alignItems: "center",
1390
- justifyContent: "center",
1391
- gap: 5,
1392
- height: 24,
1393
- padding: "0 10px",
1394
- border: 0,
1395
- borderRadius: "var(--wp-radius-sm)",
1396
- fontSize: 12,
1397
- fontWeight: 500,
1398
- whiteSpace: "nowrap",
1399
- cursor: "pointer",
1400
- background: active ? "var(--wp-surface-card)" : "transparent",
1401
- color: active ? "var(--wp-text-strong)" : "var(--wp-text-muted)",
1402
- boxShadow: active ? "var(--wp-shadow-sm)" : void 0
1403
- },
1404
- children: [
1405
- /* @__PURE__ */ jsx(Icon, { name: tab.icon, size: 14 }),
1406
- tab.label
1407
- ]
1408
- },
1409
- tab.value
1410
- );
1411
- })
1412
- }
1413
- );
1414
- }
1415
879
  function resolveRowPart(part, item) {
1416
880
  return typeof part === "function" ? part(item) : part;
1417
881
  }
@@ -1471,6 +935,7 @@ function FilterMultiSelect({
1471
935
  type: "button",
1472
936
  size: "sm",
1473
937
  variant: values.length > 0 ? "primary" : "secondary",
938
+ icon: "funnel",
1474
939
  iconEnd: "chevron-down",
1475
940
  onClick: () => setOpen((value) => !value),
1476
941
  children: label
@@ -1494,7 +959,7 @@ function FilterMultiSelect({
1494
959
  function toList(action) {
1495
960
  return Array.isArray(action) ? action : [action];
1496
961
  }
1497
- function mapColumns(columns) {
962
+ function mapColumns(columns, dispatch) {
1498
963
  return (columns ?? []).map((column) => ({
1499
964
  key: column.key,
1500
965
  title: column.title,
@@ -1503,14 +968,29 @@ function mapColumns(columns) {
1503
968
  ratio: column.ratio,
1504
969
  mono: column.mono,
1505
970
  strong: column.strong,
1506
- render: column.cell?.type === "badge" ? (row) => {
1507
- const value = String(row[column.key] ?? "");
1508
- if (!value) return null;
1509
- return /* @__PURE__ */ jsx(Badge, { tone: column.cell?.toneMap?.[value] ?? "neutral", children: value });
1510
- } : void 0
971
+ render: cellRenderer(column.cell, column.key, dispatch)
1511
972
  }));
1512
973
  }
1513
- function mapGridCard(gridCard) {
974
+ function matchesCondition(condition, row) {
975
+ if (!condition || typeof condition.key !== "string") return true;
976
+ const value = row[condition.key];
977
+ if (condition.equals !== void 0) return String(value ?? "") === String(condition.equals);
978
+ if (Array.isArray(condition.in)) return condition.in.some((v) => String(v) === String(value ?? ""));
979
+ if (condition.notEmpty === false) return true;
980
+ return value !== void 0 && value !== null && String(value) !== "";
981
+ }
982
+ function mapCardRows(rows, dispatch) {
983
+ return rows?.map((row) => {
984
+ const render = cellRenderer(row.cell, row.valueKey, dispatch);
985
+ return {
986
+ icon: row.icon ? /* @__PURE__ */ jsx(Icon, { name: row.icon, size: 14 }) : void 0,
987
+ label: row.label,
988
+ value: render ?? ((item) => String(item[row.valueKey] ?? "")),
989
+ show: row.showWhen ? (item) => matchesCondition(row.showWhen, item) : void 0
990
+ };
991
+ });
992
+ }
993
+ function mapGridCard(gridCard, dispatch) {
1514
994
  if (!gridCard) return void 0;
1515
995
  return {
1516
996
  title: (row) => String(row[gridCard.titleKey] ?? ""),
@@ -1520,12 +1000,34 @@ function mapGridCard(gridCard) {
1520
1000
  if (!value) return null;
1521
1001
  return { text: value, variant: gridCard.badge.toneMap?.[value] ?? "neutral" };
1522
1002
  } : void 0,
1523
- rows: gridCard.rows?.map((row) => ({
1524
- label: row.label,
1525
- value: (item) => String(item[row.valueKey] ?? "")
1526
- }))
1003
+ rows: mapCardRows(gridCard.rows, dispatch),
1004
+ leftRows: mapCardRows(gridCard.leftRows, dispatch),
1005
+ rightRows: mapCardRows(gridCard.rightRows, dispatch),
1006
+ note: gridCard.noteKey ? (row) => String(row[gridCard.noteKey] ?? "") || null : void 0,
1007
+ actions: gridCard.actions?.length ? (row) => (
1008
+ // 카드 전체가 클릭 대상(rowClick)일 수 있으므로 버튼 클릭은 여기서 전파를 끊는다
1009
+ /* @__PURE__ */ jsx("span", { className: "wp-row", onClick: (e) => e.stopPropagation(), children: gridCard.actions.map((action) => /* @__PURE__ */ jsx(
1010
+ Button,
1011
+ {
1012
+ size: "xs",
1013
+ icon: action.icon,
1014
+ variant: action.variant === "destructive" ? "danger" : action.variant === "outline" ? "secondary" : "ghost",
1015
+ onClick: () => toList(action.run).forEach((a) => dispatch(a, row)),
1016
+ children: action.label
1017
+ },
1018
+ action.label
1019
+ )) })
1020
+ ) : void 0
1527
1021
  };
1528
1022
  }
1023
+ function mapViews(views, gridCard, disableGrid, disableList) {
1024
+ if (!Array.isArray(views) || views.length === 0) return void 0;
1025
+ const gridOff = disableGrid ?? !gridCard;
1026
+ const kept = views.filter(
1027
+ (view) => !(view?.value === "grid" && gridOff) && !(view?.value === "list" && disableList === true)
1028
+ );
1029
+ return kept.length > 0 ? kept : void 0;
1030
+ }
1529
1031
  function mapActions(actions, dispatch) {
1530
1032
  return (actions ?? []).map((action) => ({
1531
1033
  label: action.label,
@@ -1547,6 +1049,8 @@ function SduiSearchResults({
1547
1049
  gridCard,
1548
1050
  actions,
1549
1051
  initialView,
1052
+ views,
1053
+ pageSizes,
1550
1054
  disableGrid,
1551
1055
  disableList,
1552
1056
  searchPlaceholder,
@@ -1562,12 +1066,14 @@ function SduiSearchResults({
1562
1066
  {
1563
1067
  title,
1564
1068
  item: { data: rows, total: rows.length },
1565
- columns: mapColumns(columns),
1566
- gridConfig: mapGridCard(gridCard),
1069
+ columns: mapColumns(columns, dispatch),
1070
+ gridConfig: mapGridCard(gridCard, dispatch),
1567
1071
  leftFilters,
1568
1072
  rightFilters,
1569
1073
  actions: mapActions(actions, dispatch),
1570
1074
  initialView,
1075
+ views: mapViews(views, gridCard, disableGrid, disableList),
1076
+ pageSizes,
1571
1077
  disableGrid: disableGrid ?? !gridCard,
1572
1078
  disableList,
1573
1079
  advancedSearchFields: fields,
@@ -1590,6 +1096,8 @@ function RemoteSearchResults({
1590
1096
  gridCard,
1591
1097
  actions,
1592
1098
  initialView,
1099
+ views,
1100
+ pageSizes,
1593
1101
  disableGrid,
1594
1102
  disableList,
1595
1103
  searchPlaceholder,
@@ -1604,12 +1112,13 @@ function RemoteSearchResults({
1604
1112
  initial.set("limit", String(pageSize));
1605
1113
  return initial;
1606
1114
  });
1115
+ const defaultPageSize = normalizePageSizes(pageSizes)?.[0] ?? 10;
1607
1116
  const query = useMemo(() => {
1608
1117
  const next = new URLSearchParams(params);
1609
1118
  if (!next.has("page")) next.set("page", "1");
1610
- if (!next.has("limit")) next.set("limit", "10");
1119
+ if (!next.has("limit")) next.set("limit", String(defaultPageSize));
1611
1120
  return next;
1612
- }, [params]);
1121
+ }, [params, defaultPageSize]);
1613
1122
  const { page, isLoading, error, reload } = useRemoteSearchPage(endpoint, query);
1614
1123
  if (error && page.data.length === 0) {
1615
1124
  return /* @__PURE__ */ jsx(
@@ -1627,12 +1136,14 @@ function RemoteSearchResults({
1627
1136
  {
1628
1137
  title,
1629
1138
  item: page,
1630
- columns: mapColumns(columns),
1631
- gridConfig: mapGridCard(gridCard),
1139
+ columns: mapColumns(columns, dispatch),
1140
+ gridConfig: mapGridCard(gridCard, dispatch),
1632
1141
  leftFilters,
1633
1142
  rightFilters,
1634
1143
  actions: mapActions(actions, dispatch),
1635
1144
  initialView,
1145
+ views: mapViews(views, gridCard, disableGrid, disableList),
1146
+ pageSizes,
1636
1147
  disableGrid: disableGrid ?? !gridCard,
1637
1148
  disableList,
1638
1149
  advancedSearchFields: fields,
@@ -1805,6 +1316,13 @@ var defaultRegistry = {
1805
1316
  // 권한 게이트 — 판정은 SduiRenderer의 can 프롭(permission.tsx)에 위임한다.
1806
1317
  // 주입 전까지는 통과시키되 console.error를 한 번 낸다(근거는 permission.tsx의 MISSING 주석).
1807
1318
  Can,
1319
+ // 0.3.0에서 이관한 것들은 여기 **없다.**
1320
+ // - DS 6종(Avatar·Breadcrumb·Radio·SegmentedControl·SidebarNav·Tooltip)은
1321
+ // components.tsx에 있고 export만 한다. 기본 어휘를 넓히는 것은 별개 결정이라,
1322
+ // 이름을 올리려면 `@wipco/sdui/meta`의 메타데이터를 같은 커밋에서 채워야 한다
1323
+ // (SduiComponentName이 그 Record의 키라 빠지면 컴파일이 막힌다).
1324
+ // - 표시 위젯 20종은 `widgetComponents` 묶음으로 나가 있다. 정적 import로 core에
1325
+ // 붙지 않게 하려는 것이고 이유는 위 Chart·MapPreview와 같다(widgets/index.ts 참고).
1808
1326
  // 검색+목록 화면. 스펙 이름은 ERP 어휘를 그대로 쓰고, 구현은 JSON 전용 어댑터를 가리킨다
1809
1327
  // (SearchResults 자체는 함수 프롭을 받는 타입드 컴포넌트라 스펙에서 직접 못 쓴다).
1810
1328
  SearchResults: SduiSearchResults,
@@ -2032,7 +1550,7 @@ function eventValue(arg) {
2032
1550
  }
2033
1551
  return arg;
2034
1552
  }
2035
- function toNumber(v) {
1553
+ function toNumber2(v) {
2036
1554
  if (typeof v === "number") return v;
2037
1555
  if (typeof v === "string") {
2038
1556
  const n = Number(v.replace(/,/g, ""));
@@ -2077,9 +1595,9 @@ function ruleError(rule, value, values, label, customValidators) {
2077
1595
  case "required":
2078
1596
  return empty || value === false ? rule.message ?? `${label}\uC744(\uB97C) \uC785\uB825\uD558\uC138\uC694` : null;
2079
1597
  case "min":
2080
- return !empty && toNumber(value) < rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value} \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
1598
+ return !empty && toNumber2(value) < rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value} \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
2081
1599
  case "max":
2082
- return !empty && toNumber(value) > rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value} \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4` : null;
1600
+ return !empty && toNumber2(value) > rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value} \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4` : null;
2083
1601
  case "minLength":
2084
1602
  return !empty && String(value).length < rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value}\uC790 \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
2085
1603
  case "maxLength":
@@ -2231,6 +1749,7 @@ function SduiRenderer({
2231
1749
  registry = defaultRegistry,
2232
1750
  adapter,
2233
1751
  syncUrlState = true,
1752
+ can,
2234
1753
  onCustomAction,
2235
1754
  customValidators
2236
1755
  }) {
@@ -2375,8 +1894,8 @@ function SduiRenderer({
2375
1894
  const resolved = args.map((arg) => resolveValue(arg, scope));
2376
1895
  const nums = (v) => {
2377
1896
  if (Array.isArray(v))
2378
- return v.map((row) => field && isPlainObject2(row) ? toNumber(row[field]) : toNumber(row));
2379
- return [toNumber(v)];
1897
+ return v.map((row) => field && isPlainObject2(row) ? toNumber2(row[field]) : toNumber2(row));
1898
+ return [toNumber2(v)];
2380
1899
  };
2381
1900
  switch (op) {
2382
1901
  case "sum":
@@ -2384,17 +1903,17 @@ function SduiRenderer({
2384
1903
  case "count":
2385
1904
  return resolved.reduce((a, v) => a + (Array.isArray(v) ? v.length : v == null ? 0 : 1), 0);
2386
1905
  case "sub": {
2387
- const list = resolved.map((v) => toNumber(v));
1906
+ const list = resolved.map((v) => toNumber2(v));
2388
1907
  return list.length === 0 ? 0 : list.reduce((a, b) => a - b);
2389
1908
  }
2390
1909
  case "mul":
2391
- return resolved.map((v) => toNumber(v)).reduce((a, b) => a * b, 1);
1910
+ return resolved.map((v) => toNumber2(v)).reduce((a, b) => a * b, 1);
2392
1911
  case "div": {
2393
- const [a, b] = resolved.map((v) => toNumber(v));
1912
+ const [a, b] = resolved.map((v) => toNumber2(v));
2394
1913
  return b === 0 ? 0 : a / b;
2395
1914
  }
2396
1915
  case "percent": {
2397
- const [a, b] = resolved.map((v) => toNumber(v));
1916
+ const [a, b] = resolved.map((v) => toNumber2(v));
2398
1917
  return b === 0 ? 0 : Math.round(a / b * 1e3) / 10;
2399
1918
  }
2400
1919
  case "concat":
@@ -2407,13 +1926,13 @@ function SduiRenderer({
2407
1926
  }
2408
1927
  // 수량 비교 — 정원 초과 차단(Button.disabled), 임계값 경고(Show.when) 등
2409
1928
  case "gt":
2410
- return toNumber(resolved[0]) > toNumber(resolved[1]);
1929
+ return toNumber2(resolved[0]) > toNumber2(resolved[1]);
2411
1930
  case "gte":
2412
- return toNumber(resolved[0]) >= toNumber(resolved[1]);
1931
+ return toNumber2(resolved[0]) >= toNumber2(resolved[1]);
2413
1932
  case "lt":
2414
- return toNumber(resolved[0]) < toNumber(resolved[1]);
1933
+ return toNumber2(resolved[0]) < toNumber2(resolved[1]);
2415
1934
  case "lte":
2416
- return toNumber(resolved[0]) <= toNumber(resolved[1]);
1935
+ return toNumber2(resolved[0]) <= toNumber2(resolved[1]);
2417
1936
  /** 부정 — 조건을 뒤집어 "없을 때만" 렌더링하는 데 쓴다 */
2418
1937
  case "not":
2419
1938
  return !resolved[0];
@@ -2607,14 +2126,14 @@ function SduiRenderer({
2607
2126
  }),
2608
2127
  [state, setEntries]
2609
2128
  );
2610
- return /* @__PURE__ */ jsx(SduiDispatchContext.Provider, { value: dispatch, children: /* @__PURE__ */ jsx(SduiRuntimeContext.Provider, { value: runtime, children: /* @__PURE__ */ jsxs("div", { className: "sdui-root", children: [
2129
+ return /* @__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: [
2611
2130
  page.title || page.subtitle ? /* @__PURE__ */ jsx("div", { className: "wp-page-head", children: /* @__PURE__ */ jsxs("div", { className: "wp-page-head__text", children: [
2612
2131
  page.title ? /* @__PURE__ */ jsx("h1", { className: "wp-page-head__title", children: page.title }) : null,
2613
2132
  page.subtitle ? /* @__PURE__ */ jsx("div", { className: "wp-page-head__sub", children: page.subtitle }) : null
2614
2133
  ] }) }) : null,
2615
2134
  /* @__PURE__ */ jsx("div", { className: "wp-col", children: Array.isArray(page.body) ? page.body.map((child, i) => renderChild(child, i, EMPTY_SCOPE, engine)) : /* @__PURE__ */ jsx(SpecErrorCard, { children: "[sdui] page.body\uB294 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4" }) }),
2616
2135
  toasts.length > 0 ? /* @__PURE__ */ jsx("div", { className: "wp-toast-region", children: toasts.map((t) => /* @__PURE__ */ jsx(Toast, { tone: t.tone, title: t.title, onClose: () => closeToast(t.id), children: t.message }, t.id)) }) : null
2617
- ] }) }) });
2136
+ ] }) }) }) });
2618
2137
  }
2619
2138
 
2620
- export { Actions, Can, Col, Divider, Grid, KeyValueList, NodeErrorBoundary, PageHead, RemoteSearchResults, ResultTable, Row, SduiDialog, SduiForm, SduiPermissionProvider, SduiRenderer, SduiSearchDataProvider, SduiSearchResults, SearchResults, SearchToolbar, Section, Show, Spacer, Text, cascadingValuePaths, cleanSearchValues, createRegistry, defaultAdapter, defaultRegistry, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue, useRemoteSearchPage, useSduiCan, useSduiDispatch, useSduiRuntime, useSearchFetcher, validateSpec };
2139
+ export { Actions, Can, Col, Divider, Grid, KeyValueList, NodeErrorBoundary, PageHead, RemoteSearchResults, ResultTable, Row, SduiDialog, SduiForm, SduiPermissionProvider, SduiRenderer, SduiSearchDataProvider, SduiSearchResults, SearchResults, Section, Show, Spacer, Text, cellRenderer, createRegistry, defaultAdapter, defaultRegistry, normalizePageSizes, useRemoteSearchPage, useSduiCan, useSduiDispatch, useSduiRuntime, useSearchFetcher, validateSpec };