@wipco/sdui 0.2.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 (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-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-QJHD6HVQ.js → chunk-ZUCI34QY.js} +106 -390
  27. package/dist/index.d.ts +306 -53
  28. package/dist/index.js +276 -720
  29. package/dist/meta.d.ts +8 -0
  30. package/dist/meta.js +147 -8
  31. package/dist/rich.js +3 -2
  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/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-QJHD6HVQ.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-QJHD6HVQ.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,494 +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
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." });
487
495
  }
488
- function splitMultiValue(value) {
489
- return value.split(",").map((item) => item.trim()).filter(Boolean);
490
- }
491
- function joinMultiValue(values) {
492
- return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).sort().join(",");
493
- }
494
- function normalizeSearchText(value) {
495
- return value.toLowerCase().replace(/\s+/g, "");
496
- }
497
- function includesLooseSearch(text, keyword) {
498
- const normalizedKeyword = normalizeSearchText(keyword);
499
- return normalizedKeyword.length === 0 || normalizeSearchText(text).includes(normalizedKeyword);
500
- }
501
- function searchFieldKeys(fields) {
502
- return fields.flatMap((field) => {
503
- if (field.type === "dateRange") {
504
- return [field.fromKey ?? `${field.key}From`, field.toKey ?? `${field.key}To`];
505
- }
506
- if (field.type === "numberRange") {
507
- return [field.fromKey ?? `${field.key}Min`, field.toKey ?? `${field.key}Max`];
508
- }
509
- return [field.key];
510
- });
511
- }
512
- function searchValuesFromParams(fields, params) {
513
- const values = {};
514
- for (const key of searchFieldKeys(fields)) {
515
- const value = params.get(key);
516
- if (value) values[key] = value;
517
- }
518
- return values;
519
- }
520
- function cleanSearchValues(fields, values) {
521
- const allowed = new Set(searchFieldKeys(fields));
522
- const fieldsByKey = new Map(fields.map((field) => [field.key, field]));
523
- return Object.fromEntries(
524
- Object.entries(values).map(([key, value]) => {
525
- const field = fieldsByKey.get(key);
526
- const cleaned = field?.type === "select" || field?.type === "cascading" ? joinMultiValue(splitMultiValue(value)) : value.trim();
527
- return [key, cleaned];
528
- }).filter(([key, value]) => allowed.has(key) && value)
529
- );
530
- }
531
- function cascadingPathMap(options) {
532
- const paths = /* @__PURE__ */ new Map();
533
- const visit = (option, parents) => {
534
- const path = [...parents, option];
535
- paths.set(option.value, path);
536
- option.children?.forEach((child) => visit(child, path));
537
- };
538
- options.forEach((option) => visit(option, []));
539
- return paths;
540
- }
541
- function cascadingValuePaths(options) {
542
- const map = /* @__PURE__ */ new Map();
543
- for (const [value, path] of cascadingPathMap(options)) {
544
- map.set(
545
- value,
546
- path.map((option) => option.value)
547
- );
548
- }
549
- return map;
550
- }
551
- function cascadingPathLabel(pathMap, value) {
552
- const path = pathMap.get(value);
553
- return path?.length ? path.map((option) => option.label).join(" > ") : value;
554
- }
555
- function activeSearchCriteria(fields, values) {
556
- const criteria = [];
557
- for (const field of fields) {
558
- if (field.type === "select") {
559
- for (const value2 of splitMultiValue(values[field.key] ?? "")) {
560
- const label = field.options.find((o) => o.value === value2)?.label ?? value2;
561
- criteria.push({
562
- key: `${field.key}:${value2}`,
563
- label: `${field.label}: ${label}`,
564
- clearKeys: [field.key],
565
- valueKey: field.key,
566
- value: value2
567
- });
568
- }
569
- continue;
570
- }
571
- if (field.type === "cascading") {
572
- const pathMap = cascadingPathMap(field.options);
573
- for (const value2 of splitMultiValue(values[field.key] ?? "")) {
574
- criteria.push({
575
- key: `${field.key}:${value2}`,
576
- label: `${field.label}: ${cascadingPathLabel(pathMap, value2)}`,
577
- clearKeys: [field.key],
578
- valueKey: field.key,
579
- value: value2
580
- });
581
- }
582
- continue;
583
- }
584
- if (field.type === "dateRange" || field.type === "numberRange") {
585
- const fromKey = field.fromKey ?? `${field.key}${field.type === "dateRange" ? "From" : "Min"}`;
586
- const toKey = field.toKey ?? `${field.key}${field.type === "dateRange" ? "To" : "Max"}`;
587
- const from = values[fromKey];
588
- const to = values[toKey];
589
- if (from || to) {
590
- criteria.push({
591
- key: field.key,
592
- label: `${field.label}: ${from ?? ""}~${to ?? ""}`,
593
- clearKeys: [fromKey, toKey]
594
- });
595
- }
596
- continue;
597
- }
598
- const value = values[field.key];
599
- if (value) {
600
- criteria.push({
601
- key: field.key,
602
- label: `${field.label}: ${value}`,
603
- clearKeys: [field.key]
604
- });
605
- }
606
- }
607
- return criteria;
608
- }
496
+ var EMPTY_FILTERS = [];
609
497
  var EMPTY_FIELDS = [];
610
- var EMPTY_VALUES = {};
611
- function SearchToolbar({
612
- search,
613
- onSearchChange,
614
- onSearch,
615
- advancedFields = EMPTY_FIELDS,
616
- advancedValues = EMPTY_VALUES,
617
- placeholder = "\uAC80\uC0C9",
618
- className,
619
- hideSearchInput = false
620
- }) {
621
- const [open, setOpen] = useState(false);
622
- const [draft, setDraft] = useState(advancedValues);
623
- const hasAdvanced = advancedFields.length > 0;
624
- useEffect(() => {
625
- if (!open) setDraft(advancedValues);
626
- }, [advancedValues, open]);
627
- const activeCriteria = useMemo(
628
- () => activeSearchCriteria(advancedFields, advancedValues),
629
- [advancedFields, advancedValues]
630
- );
631
- const submit = (event) => {
632
- event.preventDefault();
633
- onSearch(search.trim(), cleanSearchValues(advancedFields, advancedValues));
634
- setOpen(false);
635
- };
636
- const applyAdvancedSearch = () => {
637
- onSearch(search.trim(), cleanSearchValues(advancedFields, draft));
638
- setOpen(false);
639
- };
640
- const resetAdvancedSearch = () => {
641
- setDraft({});
642
- onSearch(search.trim(), {});
643
- setOpen(false);
644
- };
645
- const removeCriterion = (criterion) => {
646
- const nextValues = { ...advancedValues };
647
- if (criterion.valueKey && criterion.value !== void 0) {
648
- const remaining = splitMultiValue(nextValues[criterion.valueKey] ?? "").filter(
649
- (value) => value !== criterion.value
650
- );
651
- if (remaining.length > 0) nextValues[criterion.valueKey] = joinMultiValue(remaining);
652
- else delete nextValues[criterion.valueKey];
653
- } else {
654
- criterion.clearKeys.forEach((key) => delete nextValues[key]);
655
- }
656
- setDraft(nextValues);
657
- onSearch(search.trim(), cleanSearchValues(advancedFields, nextValues));
658
- };
659
- return /* @__PURE__ */ jsxs("form", { className: ["wp-row", className].filter(Boolean).join(" "), onSubmit: submit, children: [
660
- 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,
661
- !hideSearchInput ? /* @__PURE__ */ jsx(
662
- Input,
663
- {
664
- icon: "search",
665
- value: search,
666
- onChange: (event) => onSearchChange(event.target.value),
667
- placeholder
668
- }
669
- ) : null,
670
- hasAdvanced ? /* @__PURE__ */ jsx(
671
- Popover,
672
- {
673
- open,
674
- onClose: () => setOpen(false),
675
- alignLeft: false,
676
- width: "min(560px, 80vw)",
677
- trigger: /* @__PURE__ */ jsx(
678
- IconButton,
679
- {
680
- type: "button",
681
- icon: "sliders-horizontal",
682
- label: "\uC0C1\uC138\uAC80\uC0C9",
683
- "aria-expanded": open,
684
- style: open ? { background: "var(--wp-surface-active)" } : void 0,
685
- onClick: () => setOpen((value) => !value)
686
- }
687
- ),
688
- footer: /* @__PURE__ */ jsxs(Fragment, { children: [
689
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", icon: "refresh-cw", onClick: resetAdvancedSearch, children: "\uCD08\uAE30\uD654" }),
690
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: applyAdvancedSearch, children: "\uC801\uC6A9" })
691
- ] }),
692
- children: /* @__PURE__ */ jsx("div", { className: "wp-grid wp-grid--2", children: advancedFields.map((field) => /* @__PURE__ */ jsx(
693
- SearchFieldInput,
694
- {
695
- field,
696
- values: draft,
697
- onChange: (patch) => setDraft((current) => ({ ...current, ...patch }))
698
- },
699
- field.key
700
- )) })
701
- }
702
- ) : null
703
- ] });
704
- }
705
- function SearchFieldInput({
706
- field,
707
- values,
708
- onChange
709
- }) {
710
- if (field.type === "select") {
711
- return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsx(MultiSelectInput, { field, values, onChange }) });
712
- }
713
- if (field.type === "cascading") {
714
- return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsx(CascadingInput, { field, values, onChange }) });
715
- }
716
- if (field.type === "dateRange") {
717
- const fromKey = field.fromKey ?? `${field.key}From`;
718
- const toKey = field.toKey ?? `${field.key}To`;
719
- return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsxs("div", { className: "wp-row", children: [
720
- /* @__PURE__ */ jsx(
721
- Input,
722
- {
723
- type: "date",
724
- mono: true,
725
- value: values[fromKey] ?? "",
726
- onChange: (e) => onChange({ [fromKey]: e.target.value })
727
- }
728
- ),
729
- /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "~" }),
730
- /* @__PURE__ */ jsx(
731
- Input,
732
- {
733
- type: "date",
734
- mono: true,
735
- value: values[toKey] ?? "",
736
- onChange: (e) => onChange({ [toKey]: e.target.value })
737
- }
738
- )
739
- ] }) });
740
- }
741
- if (field.type === "numberRange") {
742
- const fromKey = field.fromKey ?? `${field.key}Min`;
743
- const toKey = field.toKey ?? `${field.key}Max`;
744
- return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsxs("div", { className: "wp-row", children: [
745
- /* @__PURE__ */ jsx(
746
- Input,
747
- {
748
- type: "number",
749
- mono: true,
750
- placeholder: field.minPlaceholder ?? "\uCD5C\uC18C",
751
- value: values[fromKey] ?? "",
752
- onChange: (e) => onChange({ [fromKey]: e.target.value })
753
- }
754
- ),
755
- /* @__PURE__ */ jsx("span", { className: "wp-caption", children: "~" }),
756
- /* @__PURE__ */ jsx(
757
- Input,
758
- {
759
- type: "number",
760
- mono: true,
761
- placeholder: field.maxPlaceholder ?? "\uCD5C\uB300",
762
- value: values[toKey] ?? "",
763
- onChange: (e) => onChange({ [toKey]: e.target.value })
764
- }
765
- )
766
- ] }) });
767
- }
768
- return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsx(
769
- Input,
770
- {
771
- placeholder: field.placeholder,
772
- value: values[field.key] ?? "",
773
- onChange: (e) => onChange({ [field.key]: e.target.value })
774
- }
775
- ) });
776
- }
777
- function multiSelectLabel(field, values) {
778
- if (values.length === 0) return field.allLabel ?? "\uC804\uCCB4";
779
- const labels = values.map((value) => field.options.find((item) => item.value === value)?.label ?? value);
780
- if (labels.length <= 2) return labels.join(", ");
781
- return `${labels[0]} \uC678 ${labels.length - 1}`;
782
- }
783
- function MultiSelectInput({
784
- field,
785
- values,
786
- onChange
787
- }) {
788
- const [open, setOpen] = useState(false);
789
- const selected = splitMultiValue(values[field.key] ?? "");
790
- const toggle = (value, checked) => {
791
- const next = checked ? [...selected, value] : selected.filter((item) => item !== value);
792
- onChange({ [field.key]: joinMultiValue(next) });
793
- };
794
- return /* @__PURE__ */ jsx(
795
- Popover,
796
- {
797
- open,
798
- onClose: () => setOpen(false),
799
- trigger: /* @__PURE__ */ jsx(
800
- Button,
801
- {
802
- type: "button",
803
- variant: "secondary",
804
- block: true,
805
- iconEnd: "chevron-down",
806
- onClick: () => setOpen((value) => !value),
807
- children: multiSelectLabel(field, selected)
808
- }
809
- ),
810
- children: /* @__PURE__ */ jsx("div", { className: "wp-col", children: field.options.map((option) => /* @__PURE__ */ jsx(
811
- Checkbox,
812
- {
813
- label: option.label,
814
- checked: selected.includes(option.value),
815
- onChange: (e) => toggle(option.value, e.target.checked)
816
- },
817
- option.value
818
- )) })
819
- }
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
820
507
  );
508
+ return clean.length > 0 ? clean : void 0;
821
509
  }
822
- 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));
823
512
  const seen = /* @__PURE__ */ new Set();
824
- return options.filter((option) => {
825
- if (seen.has(option.value)) return false;
826
- seen.add(option.value);
827
- return true;
828
- });
829
- }
830
- function cascadingLevels(options, selectedPaths) {
831
- const levels = [];
832
- let currentOptions = options;
833
- let depth = 0;
834
- while (currentOptions.length > 0) {
835
- levels.push(currentOptions);
836
- const selectedAtDepth = uniqueCascadingOptions(
837
- selectedPaths.map((path) => path[depth]).filter(
838
- (option) => Boolean(option && currentOptions.some((current) => current.value === option.value))
839
- )
840
- );
841
- const nextOptions = uniqueCascadingOptions(selectedAtDepth.flatMap((option) => option.children ?? []));
842
- if (nextOptions.length === 0) break;
843
- currentOptions = nextOptions;
844
- 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 });
845
519
  }
846
- return levels;
847
- }
848
- function selectedValuesForCascadingLevel(selectedPaths, depth, options) {
849
- const optionValues = new Set(options.map((option) => option.value));
850
- return Array.from(
851
- new Set(
852
- selectedPaths.map((path) => path[depth]?.value).filter((value) => Boolean(value) && optionValues.has(value))
853
- )
854
- );
855
- }
856
- function cascadingPathIncludes(pathMap, value, maybeAncestor) {
857
- return pathMap.get(value)?.some((option) => option.value === maybeAncestor) ?? false;
858
- }
859
- function cascadingValuesOverlap(leftValue, rightValue, pathMap) {
860
- return cascadingPathIncludes(pathMap, leftValue, rightValue) || cascadingPathIncludes(pathMap, rightValue, leftValue);
861
- }
862
- function cascadingLevelLabel(values, allLabel, pathMap, showPathLabels) {
863
- if (values.length === 0) return allLabel;
864
- const labels = values.map((value) => {
865
- if (showPathLabels) return cascadingPathLabel(pathMap, value);
866
- return pathMap.get(value)?.at(-1)?.label ?? value;
867
- });
868
- if (labels.length <= 2) return labels.join(", ");
869
- return `${labels[0]} \uC678 ${labels.length - 1}`;
870
- }
871
- function cascadingOptionLabel(option, pathMap, showPathLabels) {
872
- return showPathLabels ? cascadingPathLabel(pathMap, option.value) : option.label;
873
- }
874
- function CascadingInput({
875
- field,
876
- values,
877
- onChange
878
- }) {
879
- const pathMap = useMemo(() => cascadingPathMap(field.options), [field.options]);
880
- const selectedValues = useMemo(
881
- () => splitMultiValue(values[field.key] ?? "").filter((value) => pathMap.has(value)),
882
- [field.key, pathMap, values]
883
- );
884
- const selectedPaths = useMemo(
885
- () => selectedValues.map((value) => pathMap.get(value)).filter((path) => Boolean(path)),
886
- [pathMap, selectedValues]
887
- );
888
- const levels = useMemo(() => cascadingLevels(field.options, selectedPaths), [field.options, selectedPaths]);
889
- const setSelectedValues = (nextValues) => {
890
- onChange({ [field.key]: joinMultiValue(nextValues) });
891
- };
892
- const toggleOption = (option, nextChecked) => {
893
- const withoutOverlap = selectedValues.filter(
894
- (value) => !cascadingValuesOverlap(option.value, value, pathMap)
895
- );
896
- setSelectedValues(nextChecked ? [...withoutOverlap, option.value] : withoutOverlap);
897
- };
898
- const clearLevel = (depth) => {
899
- if (depth === 0) {
900
- setSelectedValues([]);
901
- return;
902
- }
903
- const parents = selectedValuesForCascadingLevel(selectedPaths, depth - 1, levels[depth - 1] ?? []);
904
- const rest = selectedValues.filter((value) => {
905
- const parentValue = pathMap.get(value)?.[depth - 1]?.value;
906
- return !parentValue || !parents.includes(parentValue);
907
- });
908
- setSelectedValues([...rest, ...parents]);
909
- };
910
- return /* @__PURE__ */ jsx("div", { className: "wp-col", children: levels.map((options, depth) => {
911
- const levelValues = selectedValuesForCascadingLevel(selectedPaths, depth, options);
912
- const showPathLabels = depth > 0 && selectedValuesForCascadingLevel(selectedPaths, depth - 1, levels[depth - 1] ?? []).length > 1;
913
- return /* @__PURE__ */ jsx(
914
- CascadingLevelSelect,
915
- {
916
- allLabel: depth === 0 ? field.allLabel ?? "\uC804\uCCB4" : "\uC804\uCCB4",
917
- options,
918
- pathMap,
919
- selectedValues: levelValues,
920
- showPathLabels,
921
- onClear: () => clearLevel(depth),
922
- onToggle: toggleOption
923
- },
924
- depth
925
- );
926
- }) });
520
+ return items.length > 0 ? items : [DEFAULT_VIEWS[0]];
927
521
  }
928
- function CascadingLevelSelect({
929
- options,
930
- pathMap,
931
- selectedValues,
932
- allLabel,
933
- showPathLabels,
934
- onClear,
935
- onToggle
936
- }) {
937
- const [open, setOpen] = useState(false);
938
- const selectedSet = new Set(selectedValues);
939
- return /* @__PURE__ */ jsx(
940
- Popover,
941
- {
942
- open,
943
- onClose: () => setOpen(false),
944
- trigger: /* @__PURE__ */ jsx(
945
- Button,
946
- {
947
- type: "button",
948
- variant: "secondary",
949
- block: true,
950
- iconEnd: "chevron-down",
951
- onClick: () => setOpen((value) => !value),
952
- children: cascadingLevelLabel(selectedValues, allLabel, pathMap, showPathLabels)
953
- }
954
- ),
955
- footer: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "xs", icon: "refresh-cw", onClick: onClear, children: allLabel }),
956
- children: /* @__PURE__ */ jsx("div", { className: "wp-col", style: { minWidth: 200 }, children: options.map((option) => /* @__PURE__ */ jsx(
957
- Checkbox,
958
- {
959
- label: cascadingOptionLabel(option, pathMap, showPathLabels),
960
- checked: selectedSet.has(option.value),
961
- onChange: (e) => onToggle(option, e.target.checked)
962
- },
963
- option.value
964
- )) })
965
- }
966
- );
967
- }
968
- var EMPTY_FILTERS = [];
969
- var EMPTY_FIELDS2 = [];
970
- var PAGE_SIZE_CHOICES = [10, 20, 50, 100];
971
522
  function SearchResults({
972
523
  item,
973
524
  columns,
@@ -975,6 +526,8 @@ function SearchResults({
975
526
  leftFilters = EMPTY_FILTERS,
976
527
  rightFilters = EMPTY_FILTERS,
977
528
  initialView = "list",
529
+ views,
530
+ pageSizes,
978
531
  className,
979
532
  searchParams,
980
533
  onParamsChange,
@@ -986,26 +539,28 @@ function SearchResults({
986
539
  headerActions,
987
540
  isLoading = false,
988
541
  hideHeader = false,
989
- advancedSearchFields = EMPTY_FIELDS2,
542
+ advancedSearchFields = EMPTY_FIELDS,
990
543
  searchPlaceholder,
991
544
  virtual
992
545
  }) {
993
546
  const hasActions = actions.length > 0;
547
+ const sizeChoices = normalizePageSizes(pageSizes) ?? PAGE_SIZE_CHOICES;
548
+ const defaultPageSize = sizeChoices[0];
994
549
  const page = Number(searchParams.get("page")) || 1;
995
- const pageSize = Number(searchParams.get("limit")) || 10;
996
- const pageSizeChoices = PAGE_SIZE_CHOICES.includes(pageSize) ? PAGE_SIZE_CHOICES : [...PAGE_SIZE_CHOICES, pageSize].sort((a, b) => a - b);
997
- const applyViewConstraints = (candidate) => {
998
- if (disableGrid && disableList) return "list";
999
- if (candidate === "grid" && disableGrid) return "list";
1000
- if (candidate === "list" && disableList) return "grid";
1001
- return candidate;
1002
- };
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;
1003
557
  const appliedSearch = searchParams.get("search") ?? "";
1004
558
  const [view, setView] = useState(() => applyViewConstraints(initialView));
1005
559
  const [search, setSearch] = useState(appliedSearch);
1006
560
  const [sort, setSort] = useState(null);
1007
561
  const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
1008
562
  const selectedRowsRef = useRef(/* @__PURE__ */ new Map());
563
+ const activeView = applyViewConstraints(view);
1009
564
  useEffect(() => {
1010
565
  setSearch(appliedSearch);
1011
566
  }, [appliedSearch]);
@@ -1094,7 +649,7 @@ function SearchResults({
1094
649
  const safePage = Math.min(Math.max(1, page), pageCount);
1095
650
  const virtualThreshold = typeof virtual === "number" ? virtual : 150;
1096
651
  const virtualEnabled = virtual === true || typeof virtual === "number";
1097
- const virtualListActive = virtualEnabled && view === "list" && processed.length > virtualThreshold;
652
+ const virtualListActive = virtualEnabled && activeView === "list" && processed.length > virtualThreshold;
1098
653
  const visibleItems = clientPaginated && !virtualListActive ? processed.slice((safePage - 1) * pageSize, safePage * pageSize) : processed;
1099
654
  const visibleIds = visibleItems.map((row) => row.id);
1100
655
  const allSelected = visibleIds.length > 0 && visibleIds.every((id) => selected.has(id));
@@ -1125,12 +680,12 @@ function SearchResults({
1125
680
  {
1126
681
  key: "__select",
1127
682
  header: /* @__PURE__ */ jsx(
1128
- SelectCheckbox,
683
+ Checkbox,
1129
684
  {
1130
685
  checked: allSelected,
1131
686
  indeterminate: !allSelected && someSelected,
1132
- label: `${title} \uC804\uCCB4 \uC120\uD0DD`,
1133
- onChange: (checked) => toggleSelectAll(checked)
687
+ "aria-label": `${title} \uC804\uCCB4 \uC120\uD0DD`,
688
+ onChange: (e) => toggleSelectAll(e.target.checked)
1134
689
  }
1135
690
  ),
1136
691
  width: "36px",
@@ -1186,7 +741,14 @@ function SearchResults({
1186
741
  placeholder: searchPlaceholder
1187
742
  }
1188
743
  ),
1189
- !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,
1190
752
  headerActions
1191
753
  ] })
1192
754
  ] }) : null,
@@ -1211,21 +773,16 @@ function SearchResults({
1211
773
  )) })
1212
774
  ] }) : null,
1213
775
  hasActions && selectedItems.length > 0 ? (
1214
- // 플로팅 벌크 액션 바 — 스크롤 위치와 무관하게 화면 하단 중앙에 뜬다
1215
- // (.wp-toolbar 클래스가 라이브러리 스타일시트에 없어 같은 값을 토큰으로 조립했다)
776
+ // 플로팅 벌크 액션 바 — 스크롤 위치와 무관하게 화면 하단 중앙에 뜬다.
777
+ // 생김새는 DS의 .wp-toolbar가 맡고, 여기서는 "화면 하단 중앙 고정"만 얹는다
778
+ // (--floating은 absolute라 스크롤을 따라오지 않는다).
1216
779
  /* @__PURE__ */ jsxs(
1217
780
  "div",
1218
781
  {
1219
782
  role: "toolbar",
783
+ className: "wp-toolbar",
1220
784
  "aria-label": `${title} \uC120\uD0DD \uD56D\uBAA9 \uC77C\uAD04 \uC791\uC5C5`,
1221
785
  style: {
1222
- display: "flex",
1223
- alignItems: "center",
1224
- gap: 6,
1225
- padding: 5,
1226
- background: "var(--wp-surface-card)",
1227
- border: "1px solid var(--wp-border-subtle)",
1228
- borderRadius: "var(--wp-radius-md)",
1229
786
  position: "fixed",
1230
787
  bottom: "var(--wp-space-6)",
1231
788
  left: "50%",
@@ -1241,7 +798,7 @@ function SearchResults({
1241
798
  selectedItems.length,
1242
799
  "\uAC74 \uC120\uD0DD\uB428"
1243
800
  ] }),
1244
- /* @__PURE__ */ jsx(ToolbarSeparator, {}),
801
+ /* @__PURE__ */ jsx("span", { className: "wp-toolbar__sep" }),
1245
802
  actions.map((action) => /* @__PURE__ */ jsx(
1246
803
  Button,
1247
804
  {
@@ -1253,13 +810,13 @@ function SearchResults({
1253
810
  },
1254
811
  action.label
1255
812
  )),
1256
- /* @__PURE__ */ jsx(ToolbarSeparator, {}),
813
+ /* @__PURE__ */ jsx("span", { className: "wp-toolbar__sep" }),
1257
814
  /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", icon: "x", onClick: () => setSelected(/* @__PURE__ */ new Set()), children: "\uC120\uD0DD \uD574\uC81C" })
1258
815
  ]
1259
816
  }
1260
817
  )
1261
818
  ) : null,
1262
- 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(
1263
820
  ResultTable,
1264
821
  {
1265
822
  columns: tableColumns,
@@ -1283,8 +840,11 @@ function SearchResults({
1283
840
  options: pageSizeChoices.map((size) => ({ value: String(size), label: `${size}\uAC74` })),
1284
841
  value: String(pageSize),
1285
842
  onChange: (e) => (
1286
- // 기본값 10은 파라미터를 지워 상태를 깔끔하게 유지하고, 페이지는 1로 되돌린다
1287
- 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
+ })
1288
848
  )
1289
849
  }
1290
850
  )
@@ -1316,65 +876,6 @@ function formatCell(value) {
1316
876
  if (value instanceof Date) return value.toLocaleString("ko-KR");
1317
877
  return value;
1318
878
  }
1319
- function ToolbarSeparator() {
1320
- return /* @__PURE__ */ jsx("span", { style: { width: 1, height: 20, background: "var(--wp-border-subtle)", flex: "none" } });
1321
- }
1322
- function SelectCheckbox({
1323
- checked,
1324
- indeterminate,
1325
- label,
1326
- onChange
1327
- }) {
1328
- const ref = useRef(null);
1329
- useEffect(() => {
1330
- if (ref.current) ref.current.indeterminate = indeterminate;
1331
- }, [indeterminate]);
1332
- return /* @__PURE__ */ jsxs("label", { className: "wp-check", children: [
1333
- /* @__PURE__ */ jsx(
1334
- "input",
1335
- {
1336
- ref,
1337
- type: "checkbox",
1338
- checked,
1339
- "aria-label": label,
1340
- onChange: (e) => onChange(e.target.checked)
1341
- }
1342
- ),
1343
- /* @__PURE__ */ jsx(
1344
- "span",
1345
- {
1346
- className: "wp-check__box",
1347
- style: indeterminate && !checked ? {
1348
- background: "var(--wp-action-primary-bg)",
1349
- borderColor: "var(--wp-action-primary-bg)",
1350
- color: "var(--wp-white)"
1351
- } : void 0,
1352
- children: /* @__PURE__ */ jsx(Icon, { name: indeterminate && !checked ? "minus" : "check", size: 12, strokeWidth: 2.5 })
1353
- }
1354
- )
1355
- ] });
1356
- }
1357
- function ViewToggle({ value, onChange }) {
1358
- const items = [
1359
- { value: "list", icon: "list", label: "\uBAA9\uB85D" },
1360
- { value: "grid", icon: "layout-grid", label: "\uCE74\uB4DC" }
1361
- ];
1362
- return /* @__PURE__ */ jsx("div", { className: "wp-seg", role: "tablist", children: items.map((tab) => /* @__PURE__ */ jsxs(
1363
- "button",
1364
- {
1365
- type: "button",
1366
- role: "tab",
1367
- className: "wp-seg__item",
1368
- "aria-selected": tab.value === value,
1369
- onClick: () => onChange(tab.value),
1370
- children: [
1371
- /* @__PURE__ */ jsx(Icon, { name: tab.icon, size: 14 }),
1372
- tab.label
1373
- ]
1374
- },
1375
- tab.value
1376
- )) });
1377
- }
1378
879
  function resolveRowPart(part, item) {
1379
880
  return typeof part === "function" ? part(item) : part;
1380
881
  }
@@ -1434,6 +935,7 @@ function FilterMultiSelect({
1434
935
  type: "button",
1435
936
  size: "sm",
1436
937
  variant: values.length > 0 ? "primary" : "secondary",
938
+ icon: "funnel",
1437
939
  iconEnd: "chevron-down",
1438
940
  onClick: () => setOpen((value) => !value),
1439
941
  children: label
@@ -1457,7 +959,7 @@ function FilterMultiSelect({
1457
959
  function toList(action) {
1458
960
  return Array.isArray(action) ? action : [action];
1459
961
  }
1460
- function mapColumns(columns) {
962
+ function mapColumns(columns, dispatch) {
1461
963
  return (columns ?? []).map((column) => ({
1462
964
  key: column.key,
1463
965
  title: column.title,
@@ -1466,14 +968,29 @@ function mapColumns(columns) {
1466
968
  ratio: column.ratio,
1467
969
  mono: column.mono,
1468
970
  strong: column.strong,
1469
- render: column.cell?.type === "badge" ? (row) => {
1470
- const value = String(row[column.key] ?? "");
1471
- if (!value) return null;
1472
- return /* @__PURE__ */ jsx(Badge, { tone: column.cell?.toneMap?.[value] ?? "neutral", children: value });
1473
- } : void 0
971
+ render: cellRenderer(column.cell, column.key, dispatch)
1474
972
  }));
1475
973
  }
1476
- 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) {
1477
994
  if (!gridCard) return void 0;
1478
995
  return {
1479
996
  title: (row) => String(row[gridCard.titleKey] ?? ""),
@@ -1483,12 +1000,34 @@ function mapGridCard(gridCard) {
1483
1000
  if (!value) return null;
1484
1001
  return { text: value, variant: gridCard.badge.toneMap?.[value] ?? "neutral" };
1485
1002
  } : void 0,
1486
- rows: gridCard.rows?.map((row) => ({
1487
- label: row.label,
1488
- value: (item) => String(item[row.valueKey] ?? "")
1489
- }))
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
1490
1021
  };
1491
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
+ }
1492
1031
  function mapActions(actions, dispatch) {
1493
1032
  return (actions ?? []).map((action) => ({
1494
1033
  label: action.label,
@@ -1510,6 +1049,8 @@ function SduiSearchResults({
1510
1049
  gridCard,
1511
1050
  actions,
1512
1051
  initialView,
1052
+ views,
1053
+ pageSizes,
1513
1054
  disableGrid,
1514
1055
  disableList,
1515
1056
  searchPlaceholder,
@@ -1525,12 +1066,14 @@ function SduiSearchResults({
1525
1066
  {
1526
1067
  title,
1527
1068
  item: { data: rows, total: rows.length },
1528
- columns: mapColumns(columns),
1529
- gridConfig: mapGridCard(gridCard),
1069
+ columns: mapColumns(columns, dispatch),
1070
+ gridConfig: mapGridCard(gridCard, dispatch),
1530
1071
  leftFilters,
1531
1072
  rightFilters,
1532
1073
  actions: mapActions(actions, dispatch),
1533
1074
  initialView,
1075
+ views: mapViews(views, gridCard, disableGrid, disableList),
1076
+ pageSizes,
1534
1077
  disableGrid: disableGrid ?? !gridCard,
1535
1078
  disableList,
1536
1079
  advancedSearchFields: fields,
@@ -1553,6 +1096,8 @@ function RemoteSearchResults({
1553
1096
  gridCard,
1554
1097
  actions,
1555
1098
  initialView,
1099
+ views,
1100
+ pageSizes,
1556
1101
  disableGrid,
1557
1102
  disableList,
1558
1103
  searchPlaceholder,
@@ -1567,12 +1112,13 @@ function RemoteSearchResults({
1567
1112
  initial.set("limit", String(pageSize));
1568
1113
  return initial;
1569
1114
  });
1115
+ const defaultPageSize = normalizePageSizes(pageSizes)?.[0] ?? 10;
1570
1116
  const query = useMemo(() => {
1571
1117
  const next = new URLSearchParams(params);
1572
1118
  if (!next.has("page")) next.set("page", "1");
1573
- if (!next.has("limit")) next.set("limit", "10");
1119
+ if (!next.has("limit")) next.set("limit", String(defaultPageSize));
1574
1120
  return next;
1575
- }, [params]);
1121
+ }, [params, defaultPageSize]);
1576
1122
  const { page, isLoading, error, reload } = useRemoteSearchPage(endpoint, query);
1577
1123
  if (error && page.data.length === 0) {
1578
1124
  return /* @__PURE__ */ jsx(
@@ -1590,12 +1136,14 @@ function RemoteSearchResults({
1590
1136
  {
1591
1137
  title,
1592
1138
  item: page,
1593
- columns: mapColumns(columns),
1594
- gridConfig: mapGridCard(gridCard),
1139
+ columns: mapColumns(columns, dispatch),
1140
+ gridConfig: mapGridCard(gridCard, dispatch),
1595
1141
  leftFilters,
1596
1142
  rightFilters,
1597
1143
  actions: mapActions(actions, dispatch),
1598
1144
  initialView,
1145
+ views: mapViews(views, gridCard, disableGrid, disableList),
1146
+ pageSizes,
1599
1147
  disableGrid: disableGrid ?? !gridCard,
1600
1148
  disableList,
1601
1149
  advancedSearchFields: fields,
@@ -1768,6 +1316,13 @@ var defaultRegistry = {
1768
1316
  // 권한 게이트 — 판정은 SduiRenderer의 can 프롭(permission.tsx)에 위임한다.
1769
1317
  // 주입 전까지는 통과시키되 console.error를 한 번 낸다(근거는 permission.tsx의 MISSING 주석).
1770
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 참고).
1771
1326
  // 검색+목록 화면. 스펙 이름은 ERP 어휘를 그대로 쓰고, 구현은 JSON 전용 어댑터를 가리킨다
1772
1327
  // (SearchResults 자체는 함수 프롭을 받는 타입드 컴포넌트라 스펙에서 직접 못 쓴다).
1773
1328
  SearchResults: SduiSearchResults,
@@ -1995,7 +1550,7 @@ function eventValue(arg) {
1995
1550
  }
1996
1551
  return arg;
1997
1552
  }
1998
- function toNumber(v) {
1553
+ function toNumber2(v) {
1999
1554
  if (typeof v === "number") return v;
2000
1555
  if (typeof v === "string") {
2001
1556
  const n = Number(v.replace(/,/g, ""));
@@ -2040,9 +1595,9 @@ function ruleError(rule, value, values, label, customValidators) {
2040
1595
  case "required":
2041
1596
  return empty || value === false ? rule.message ?? `${label}\uC744(\uB97C) \uC785\uB825\uD558\uC138\uC694` : null;
2042
1597
  case "min":
2043
- 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;
2044
1599
  case "max":
2045
- 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;
2046
1601
  case "minLength":
2047
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;
2048
1603
  case "maxLength":
@@ -2194,6 +1749,7 @@ function SduiRenderer({
2194
1749
  registry = defaultRegistry,
2195
1750
  adapter,
2196
1751
  syncUrlState = true,
1752
+ can,
2197
1753
  onCustomAction,
2198
1754
  customValidators
2199
1755
  }) {
@@ -2338,8 +1894,8 @@ function SduiRenderer({
2338
1894
  const resolved = args.map((arg) => resolveValue(arg, scope));
2339
1895
  const nums = (v) => {
2340
1896
  if (Array.isArray(v))
2341
- return v.map((row) => field && isPlainObject2(row) ? toNumber(row[field]) : toNumber(row));
2342
- return [toNumber(v)];
1897
+ return v.map((row) => field && isPlainObject2(row) ? toNumber2(row[field]) : toNumber2(row));
1898
+ return [toNumber2(v)];
2343
1899
  };
2344
1900
  switch (op) {
2345
1901
  case "sum":
@@ -2347,17 +1903,17 @@ function SduiRenderer({
2347
1903
  case "count":
2348
1904
  return resolved.reduce((a, v) => a + (Array.isArray(v) ? v.length : v == null ? 0 : 1), 0);
2349
1905
  case "sub": {
2350
- const list = resolved.map((v) => toNumber(v));
1906
+ const list = resolved.map((v) => toNumber2(v));
2351
1907
  return list.length === 0 ? 0 : list.reduce((a, b) => a - b);
2352
1908
  }
2353
1909
  case "mul":
2354
- 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);
2355
1911
  case "div": {
2356
- const [a, b] = resolved.map((v) => toNumber(v));
1912
+ const [a, b] = resolved.map((v) => toNumber2(v));
2357
1913
  return b === 0 ? 0 : a / b;
2358
1914
  }
2359
1915
  case "percent": {
2360
- const [a, b] = resolved.map((v) => toNumber(v));
1916
+ const [a, b] = resolved.map((v) => toNumber2(v));
2361
1917
  return b === 0 ? 0 : Math.round(a / b * 1e3) / 10;
2362
1918
  }
2363
1919
  case "concat":
@@ -2370,13 +1926,13 @@ function SduiRenderer({
2370
1926
  }
2371
1927
  // 수량 비교 — 정원 초과 차단(Button.disabled), 임계값 경고(Show.when) 등
2372
1928
  case "gt":
2373
- return toNumber(resolved[0]) > toNumber(resolved[1]);
1929
+ return toNumber2(resolved[0]) > toNumber2(resolved[1]);
2374
1930
  case "gte":
2375
- return toNumber(resolved[0]) >= toNumber(resolved[1]);
1931
+ return toNumber2(resolved[0]) >= toNumber2(resolved[1]);
2376
1932
  case "lt":
2377
- return toNumber(resolved[0]) < toNumber(resolved[1]);
1933
+ return toNumber2(resolved[0]) < toNumber2(resolved[1]);
2378
1934
  case "lte":
2379
- return toNumber(resolved[0]) <= toNumber(resolved[1]);
1935
+ return toNumber2(resolved[0]) <= toNumber2(resolved[1]);
2380
1936
  /** 부정 — 조건을 뒤집어 "없을 때만" 렌더링하는 데 쓴다 */
2381
1937
  case "not":
2382
1938
  return !resolved[0];
@@ -2570,14 +2126,14 @@ function SduiRenderer({
2570
2126
  }),
2571
2127
  [state, setEntries]
2572
2128
  );
2573
- 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: [
2574
2130
  page.title || page.subtitle ? /* @__PURE__ */ jsx("div", { className: "wp-page-head", children: /* @__PURE__ */ jsxs("div", { className: "wp-page-head__text", children: [
2575
2131
  page.title ? /* @__PURE__ */ jsx("h1", { className: "wp-page-head__title", children: page.title }) : null,
2576
2132
  page.subtitle ? /* @__PURE__ */ jsx("div", { className: "wp-page-head__sub", children: page.subtitle }) : null
2577
2133
  ] }) }) : null,
2578
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" }) }),
2579
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
2580
- ] }) }) });
2136
+ ] }) }) }) });
2581
2137
  }
2582
2138
 
2583
- 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 };