sellmate-design-system-react 9.0.0-beta.36 → 9.0.0-beta.37

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.
@@ -1,10 +1,26 @@
1
1
  /** 오버레이 뷰포트 — 스토어를 구독해 SLoadingContainer 를 렌더한다. 직접 배치도 가능. */
2
2
  export declare function SLoadingViewport(): import("react").JSX.Element;
3
3
  export declare const loading: {
4
- /** 로딩 오버레이 표시 (메시지 갱신도 동일 호출) */
4
+ /**
5
+ * 로딩 오버레이 표시. 겹쳐 부르면 그만큼 쌓이고 **마지막 하나가 걷힐 때** 닫힌다.
6
+ * 반환값은 이 호출 하나만 걷는 함수다(두 번 불러도 한 번만 걷힌다) — `hide()` 대신 이것을
7
+ * 쓰면 짝이 어긋나지 않는다.
8
+ *
9
+ * ⚠️ 메시지만 바꾸려면 `update()` 를 쓴다. `show()` 를 다시 부르면 하나 더 쌓여
10
+ * `hide()` 한 번으로는 닫히지 않는다.
11
+ */
5
12
  show(options?: {
6
13
  message?: string;
14
+ }): () => void;
15
+ /** 표시 중인 메시지 갱신 — 쌓인 수는 그대로다 */
16
+ update(options: {
17
+ message?: string;
7
18
  }): void;
8
- /** 로딩 오버레이 숨김 */
19
+ /** 가장 나중의 show 하나를 걷는다. 남은 것이 없을 때만 오버레이가 닫힌다 */
9
20
  hide(): void;
21
+ /**
22
+ * 쌓인 것을 모두 걷고 즉시 닫는다. 화면 전환·에러 복구처럼 진행 중이던 작업의 짝을 더는
23
+ * 맞출 수 없을 때만 쓴다 — 평소에는 `hide()` 나 `show()` 가 준 함수로 하나씩 걷는다.
24
+ */
25
+ hideAll(): void;
10
26
  };
package/dist/index.cjs CHANGED
@@ -6491,10 +6491,19 @@ var SLoadingContainer = /* @__PURE__ */ react.forwardRef(
6491
6491
  var state = { open: false, message: void 0 };
6492
6492
  var listeners = /* @__PURE__ */ new Set();
6493
6493
  var emit = () => listeners.forEach((l) => l());
6494
- var setState = (patch) => {
6495
- state = { ...state, ...patch };
6494
+ var entries = [];
6495
+ var seq = 0;
6496
+ var sync = () => {
6497
+ const top = entries[entries.length - 1];
6498
+ state = { open: entries.length > 0, message: top?.message };
6496
6499
  emit();
6497
6500
  };
6501
+ var release = (id) => {
6502
+ const next = entries.filter((e) => e.id !== id);
6503
+ if (next.length === entries.length) return;
6504
+ entries = next;
6505
+ sync();
6506
+ };
6498
6507
  var subscribe = (cb) => {
6499
6508
  listeners.add(cb);
6500
6509
  return () => listeners.delete(cb);
@@ -6514,14 +6523,41 @@ function ensureMounted() {
6514
6523
  client.createRoot(host).render(/* @__PURE__ */ jsxRuntime.jsx(SLoadingViewport, {}));
6515
6524
  }
6516
6525
  var loading = {
6517
- /** 로딩 오버레이 표시 (메시지 갱신도 동일 호출) */
6526
+ /**
6527
+ * 로딩 오버레이 표시. 겹쳐 부르면 그만큼 쌓이고 **마지막 하나가 걷힐 때** 닫힌다.
6528
+ * 반환값은 이 호출 하나만 걷는 함수다(두 번 불러도 한 번만 걷힌다) — `hide()` 대신 이것을
6529
+ * 쓰면 짝이 어긋나지 않는다.
6530
+ *
6531
+ * ⚠️ 메시지만 바꾸려면 `update()` 를 쓴다. `show()` 를 다시 부르면 하나 더 쌓여
6532
+ * `hide()` 한 번으로는 닫히지 않는다.
6533
+ */
6518
6534
  show(options = {}) {
6519
6535
  ensureMounted();
6520
- setState({ open: true, message: options.message });
6536
+ const id = ++seq;
6537
+ entries = [...entries, { id, message: options.message }];
6538
+ sync();
6539
+ return () => release(id);
6540
+ },
6541
+ /** 표시 중인 메시지 갱신 — 쌓인 수는 그대로다 */
6542
+ update(options) {
6543
+ const top = entries[entries.length - 1];
6544
+ if (!top) return;
6545
+ entries = [...entries.slice(0, -1), { ...top, message: options.message }];
6546
+ sync();
6521
6547
  },
6522
- /** 로딩 오버레이 숨김 */
6548
+ /** 가장 나중의 show 하나를 걷는다. 남은 것이 없을 때만 오버레이가 닫힌다 */
6523
6549
  hide() {
6524
- setState({ open: false });
6550
+ const top = entries[entries.length - 1];
6551
+ if (top) release(top.id);
6552
+ },
6553
+ /**
6554
+ * 쌓인 것을 모두 걷고 즉시 닫는다. 화면 전환·에러 복구처럼 진행 중이던 작업의 짝을 더는
6555
+ * 맞출 수 없을 때만 쓴다 — 평소에는 `hide()` 나 `show()` 가 준 함수로 하나씩 걷는다.
6556
+ */
6557
+ hideAll() {
6558
+ if (entries.length === 0) return;
6559
+ entries = [];
6560
+ sync();
6525
6561
  }
6526
6562
  };
6527
6563
  var SIZE_CONFIG = {
@@ -7940,7 +7976,7 @@ var MODAL_HOST_Z_INDEX = Z_INDEX.modalHost;
7940
7976
  var MODAL_HOST_ATTRIBUTE = "data-s-modal-host";
7941
7977
  var nextId = 0;
7942
7978
  var nextLayerIndex = 0;
7943
- var entries = [];
7979
+ var entries2 = [];
7944
7980
  function getModalHost() {
7945
7981
  if (typeof document === "undefined" || !document.body) return void 0;
7946
7982
  const existing = document.body.querySelector(`[${MODAL_HOST_ATTRIBUTE}]`);
@@ -7961,16 +7997,16 @@ function registerModalStackEntry() {
7961
7997
  id: ++nextId,
7962
7998
  layerIndex: nextLayerIndex++
7963
7999
  };
7964
- entries.push(entry);
8000
+ entries2.push(entry);
7965
8001
  let registered = true;
7966
8002
  return {
7967
8003
  layerIndex: entry.layerIndex,
7968
- isTop: () => entries[entries.length - 1] === entry,
8004
+ isTop: () => entries2[entries2.length - 1] === entry,
7969
8005
  unregister: () => {
7970
8006
  if (!registered) return;
7971
8007
  registered = false;
7972
- const index = entries.indexOf(entry);
7973
- if (index !== -1) entries.splice(index, 1);
8008
+ const index = entries2.indexOf(entry);
8009
+ if (index !== -1) entries2.splice(index, 1);
7974
8010
  }
7975
8011
  };
7976
8012
  }
@@ -7994,8 +8030,10 @@ function SModalContainer({
7994
8030
  const contentRef = react.useRef(null);
7995
8031
  const stackRegistrationRef = react.useRef(null);
7996
8032
  const [layerIndex, setLayerIndex] = react.useState(null);
8033
+ const closeRequested = react.useRef(false);
7997
8034
  react.useEffect(() => {
7998
8035
  if (open) {
8036
+ closeRequested.current = false;
7999
8037
  setRendered(true);
8000
8038
  return;
8001
8039
  }
@@ -8051,7 +8089,7 @@ function SModalContainer({
8051
8089
  {
8052
8090
  forceMount: rendered ? true : void 0,
8053
8091
  ref: contentRef,
8054
- className: "pointer-events-auto outline-none data-[state=open]:animate-modal-in data-[state=closed]:animate-modal-out",
8092
+ className: "pointer-events-auto outline-none data-[state=open]:animate-modal-in data-[state=closed]:pointer-events-none data-[state=closed]:animate-modal-out",
8055
8093
  style: { animationFillMode: "forwards" },
8056
8094
  onOpenAutoFocus: (e) => {
8057
8095
  e.preventDefault();
@@ -8082,6 +8120,8 @@ function SModalContainer({
8082
8120
  icon: "close",
8083
8121
  ariaLabel: "\uB2EB\uAE30",
8084
8122
  onClick: () => {
8123
+ if (closeRequested.current) return;
8124
+ closeRequested.current = true;
8085
8125
  onClose?.();
8086
8126
  onOpenChange?.(false);
8087
8127
  },
@@ -8155,6 +8195,16 @@ function SConfirmModal({
8155
8195
  }) {
8156
8196
  const iconName = TYPE_ICON[type];
8157
8197
  const mainStyle = MAIN_BUTTON_STYLE[mainButtonName ?? DEFAULT_MAIN_BUTTON[type]];
8198
+ const [settled, setSettled] = react.useState(false);
8199
+ react.useEffect(() => {
8200
+ if (open) setSettled(false);
8201
+ }, [open]);
8202
+ const settle = (emit3) => {
8203
+ if (settled) return;
8204
+ setSettled(true);
8205
+ emit3?.();
8206
+ onOpenChange?.(false);
8207
+ };
8158
8208
  const tagNode = tagSlot ?? (tagLabel ? /* @__PURE__ */ jsxRuntime.jsx(STag, { shape: tagShape, size: tagSize, color: tagColor, label: tagLabel }) : null);
8159
8209
  const optionNode = optionSlot ?? (slotLabel ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: slotLabel }) : null);
8160
8210
  const hasContentBox = tagNode != null || optionNode != null;
@@ -8167,7 +8217,7 @@ function SConfirmModal({
8167
8217
  persistent,
8168
8218
  ariaTitle: modalTitle,
8169
8219
  showClose: true,
8170
- onClose,
8220
+ onClose: () => settle(onClose),
8171
8221
  className: "w-fit min-w-[min(520px,calc(100dvw-48px))]",
8172
8222
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-auto flex-col items-center gap-[40px] px-[32px] py-[40px]", children: [
8173
8223
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex w-full min-h-0 flex-col items-center gap-[20px] overflow-y-auto", children: [
@@ -8217,10 +8267,8 @@ function SConfirmModal({
8217
8267
  color: "neutral",
8218
8268
  outline: true,
8219
8269
  size: "md",
8220
- onClick: () => {
8221
- onCancel?.();
8222
- onOpenChange?.(false);
8223
- },
8270
+ disabled: settled,
8271
+ onClick: () => settle(onCancel),
8224
8272
  label: subButtonLabel
8225
8273
  }
8226
8274
  ),
@@ -8229,10 +8277,8 @@ function SConfirmModal({
8229
8277
  {
8230
8278
  ...mainStyle,
8231
8279
  size: "md",
8232
- onClick: () => {
8233
- onOk?.();
8234
- onOpenChange?.(false);
8235
- },
8280
+ disabled: settled,
8281
+ onClick: () => settle(onOk),
8236
8282
  label: mainButtonLabel
8237
8283
  }
8238
8284
  )
@@ -8475,10 +8521,10 @@ function SDrawer({
8475
8521
  }, []);
8476
8522
  react.useEffect(() => {
8477
8523
  if (!rendered) return;
8478
- const sync = () => setViewportWidth(window.innerWidth);
8479
- sync();
8480
- window.addEventListener("resize", sync);
8481
- return () => window.removeEventListener("resize", sync);
8524
+ const sync2 = () => setViewportWidth(window.innerWidth);
8525
+ sync2();
8526
+ window.addEventListener("resize", sync2);
8527
+ return () => window.removeEventListener("resize", sync2);
8482
8528
  }, [rendered]);
8483
8529
  react.useEffect(() => {
8484
8530
  const panel2 = panelRef.current;
@@ -8724,7 +8770,7 @@ function SDrawer({
8724
8770
 
8725
8771
  // src/lib/modal-outlet.ts
8726
8772
  var EMPTY = [];
8727
- var entries2 = EMPTY;
8773
+ var entries3 = EMPTY;
8728
8774
  var listeners2 = /* @__PURE__ */ new Set();
8729
8775
  var outletRendered = false;
8730
8776
  var outletCount = 0;
@@ -8739,7 +8785,7 @@ function subscribeModalEntries(listener) {
8739
8785
  };
8740
8786
  }
8741
8787
  function getModalEntries() {
8742
- return entries2;
8788
+ return entries3;
8743
8789
  }
8744
8790
  function getServerModalEntries() {
8745
8791
  return EMPTY;
@@ -8758,9 +8804,9 @@ function registerModalOutlet() {
8758
8804
  function sweepOrphans() {
8759
8805
  if (outletCount > 0) return;
8760
8806
  outletRendered = false;
8761
- if (entries2 === EMPTY) return;
8762
- const orphans = entries2;
8763
- entries2 = EMPTY;
8807
+ if (entries3 === EMPTY) return;
8808
+ const orphans = entries3;
8809
+ entries3 = EMPTY;
8764
8810
  emit2();
8765
8811
  for (const entry of orphans) entry.onOrphan();
8766
8812
  }
@@ -8772,14 +8818,14 @@ function isModalOutletMounted() {
8772
8818
  }
8773
8819
  function addModalEntry(host, node, onOrphan) {
8774
8820
  const id = ++nextId2;
8775
- entries2 = [...entries2, { id, host, node, onOrphan }];
8821
+ entries3 = [...entries3, { id, host, node, onOrphan }];
8776
8822
  emit2();
8777
8823
  return id;
8778
8824
  }
8779
8825
  function removeModalEntry(id) {
8780
- const next = entries2.filter((entry) => entry.id !== id);
8781
- if (next.length === entries2.length) return;
8782
- entries2 = next;
8826
+ const next = entries3.filter((entry) => entry.id !== id);
8827
+ if (next.length === entries3.length) return;
8828
+ entries3 = next;
8783
8829
  emit2();
8784
8830
  }
8785
8831
  var EXIT_DURATION = 300;
@@ -8820,11 +8866,22 @@ var SModalRefImpl = class {
8820
8866
  else this.pendingPatches.push(patch);
8821
8867
  return this;
8822
8868
  }
8823
- // 닫힘 트리거 — 이미 닫힘이 시작됐으면 무시(중복 콜백 방지)
8824
- settle(emit3) {
8825
- if (this.dismissRequested) return;
8869
+ /**
8870
+ * 확정 콜백 1회 발화 — 이미 닫힘이 시작됐으면 무시한다. 발화했으면 true.
8871
+ *
8872
+ * 한 번 띄운 모달의 결말은 하나다(ok 이거나 cancel 이거나 close 다). 닫힘은 시작만 하고
8873
+ * 퇴장 애니메이션 동안 카드가 남아 있으므로, 그동안의 재클릭·키 반복이 두 번째 결말을
8874
+ * 만들지 않도록 여기서 막는다.
8875
+ */
8876
+ settleEmit(emit3) {
8877
+ if (this.dismissRequested) return false;
8826
8878
  this.dismissRequested = true;
8827
8879
  emit3();
8880
+ return true;
8881
+ }
8882
+ // 닫힘 트리거 — 이미 닫힘이 시작됐으면 무시(중복 콜백 방지)
8883
+ settle(emit3) {
8884
+ if (!this.settleEmit(emit3)) return;
8828
8885
  if (this.closeBinding) this.closeBinding();
8829
8886
  else this.pendingClose = true;
8830
8887
  }
@@ -8877,17 +8934,18 @@ var SModalRefImpl = class {
8877
8934
  _markDismissed() {
8878
8935
  this.dismissRequested = true;
8879
8936
  }
8880
- /** @internal confirm 버튼 경로에서 onOk 발화 */
8937
+ // 아래 경로는 컴포넌트가 스스로 닫으므로(onOpenChange(false)) 발화 가드만 태운다.
8938
+ /** @internal confirm 버튼 경로에서 onOk 발화 (닫히는 중의 재클릭·키 반복은 무시) */
8881
8939
  _emitOk() {
8882
- this.okFn?.();
8940
+ this.settleEmit(() => this.okFn?.());
8883
8941
  }
8884
- /** @internal confirm 버튼 경로에서 onCancel 발화 */
8942
+ /** @internal confirm 버튼 경로에서 onCancel 발화 (닫히는 중의 재클릭·키 반복은 무시) */
8885
8943
  _emitCancel() {
8886
- this.cancelFn?.();
8944
+ this.settleEmit(() => this.cancelFn?.());
8887
8945
  }
8888
- /** @internal 닫기(X) 경로에서 onClose 발화 */
8946
+ /** @internal 닫기(X) 경로에서 onClose 발화 (닫히는 중의 재클릭·키 반복은 무시) */
8889
8947
  _emitClose() {
8890
- this.closeFn?.();
8948
+ this.settleEmit(() => this.closeFn?.());
8891
8949
  }
8892
8950
  /** @internal loading 버튼 경로에서 onClick 발화 */
8893
8951
  _emitClick() {
@@ -9083,7 +9141,7 @@ function SModalEntryPortal({ entry }) {
9083
9141
  }
9084
9142
  function SModalOutlet() {
9085
9143
  markModalOutletRendered();
9086
- const entries3 = react.useSyncExternalStore(
9144
+ const entries4 = react.useSyncExternalStore(
9087
9145
  subscribeModalEntries,
9088
9146
  getModalEntries,
9089
9147
  getServerModalEntries
@@ -9096,7 +9154,7 @@ function SModalOutlet() {
9096
9154
  }
9097
9155
  return registerModalOutlet();
9098
9156
  }, []);
9099
- return /* @__PURE__ */ jsxRuntime.jsx(PortalContainerProvider, { value: null, children: /* @__PURE__ */ jsxRuntime.jsx(InsideModalProvider, { value: false, children: entries3.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(SModalEntryPortal, { entry }, entry.id)) }) });
9157
+ return /* @__PURE__ */ jsxRuntime.jsx(PortalContainerProvider, { value: null, children: /* @__PURE__ */ jsxRuntime.jsx(InsideModalProvider, { value: false, children: entries4.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(SModalEntryPortal, { entry }, entry.id)) }) });
9100
9158
  }
9101
9159
  var useIsomorphicLayoutEffect = typeof window !== "undefined" ? react.useLayoutEffect : react.useEffect;
9102
9160
  var SPageColumnContext = /* @__PURE__ */ react.createContext(null);
@@ -17870,10 +17928,10 @@ var useIsMobile = () => {
17870
17928
  react.useEffect(() => {
17871
17929
  if (typeof window.matchMedia !== "function") return;
17872
17930
  const query = window.matchMedia(`(max-width: ${MOBILE_MAX_WIDTH - 1}px)`);
17873
- const sync = () => setIsMobile(query.matches);
17874
- sync();
17875
- query.addEventListener("change", sync);
17876
- return () => query.removeEventListener("change", sync);
17931
+ const sync2 = () => setIsMobile(query.matches);
17932
+ sync2();
17933
+ query.addEventListener("change", sync2);
17934
+ return () => query.removeEventListener("change", sync2);
17877
17935
  }, []);
17878
17936
  return isMobile;
17879
17937
  };
@@ -19373,9 +19431,9 @@ var keywordEntries = (value) => {
19373
19431
  return [];
19374
19432
  };
19375
19433
  var CSV_PASTE_SEPARATOR = /[,\r\n\t]+/;
19376
- var newKeywordEntries = (entries3, candidates) => candidates.reduce((acc, raw) => {
19434
+ var newKeywordEntries = (entries4, candidates) => candidates.reduce((acc, raw) => {
19377
19435
  const next = raw.trim();
19378
- if (next === "" || entries3.includes(next) || acc.includes(next)) return acc;
19436
+ if (next === "" || entries4.includes(next) || acc.includes(next)) return acc;
19379
19437
  return [...acc, next];
19380
19438
  }, []);
19381
19439
  var withKeywordEntries = (field, current, nextEntries) => field.matchModes ? { keywords: nextEntries, mode: isKeywordValue(current) ? current.mode : "contains" } : nextEntries;
@@ -19639,13 +19697,13 @@ var SChipFilter = /* @__PURE__ */ react.forwardRef(function SChipFilter2({
19639
19697
  const field = fields.find((f) => f.key === key);
19640
19698
  if (field == null || field.type !== "keyword" || disabled || field.disabled) return null;
19641
19699
  const current = valuesRef.current[key];
19642
- const entries3 = keywordEntries(current);
19700
+ const entries4 = keywordEntries(current);
19643
19701
  const added = newKeywordEntries(
19644
- entries3,
19702
+ entries4,
19645
19703
  field.input === "csv" ? draft.text.split(CSV_PASTE_SEPARATOR) : [draft.text]
19646
19704
  );
19647
19705
  if (added.length === 0) return null;
19648
- const nextValue = withKeywordEntries(field, current, [...entries3, ...added]);
19706
+ const nextValue = withKeywordEntries(field, current, [...entries4, ...added]);
19649
19707
  const next = { ...valuesRef.current, [key]: nextValue };
19650
19708
  onValueChange?.(next);
19651
19709
  onFilterChange?.({ key, value: nextValue, values: next });
@@ -20666,7 +20724,7 @@ function KeywordEditor({
20666
20724
  onValueCommit,
20667
20725
  onDraftChange
20668
20726
  }) {
20669
- const entries3 = keywordEntries(value);
20727
+ const entries4 = keywordEntries(value);
20670
20728
  const [draftText, setDraftText] = react.useState("");
20671
20729
  const [mode, setMode] = react.useState(
20672
20730
  isKeywordValue(value) ? value.mode : "contains"
@@ -20688,16 +20746,16 @@ function KeywordEditor({
20688
20746
  };
20689
20747
  const changeMode = (next) => {
20690
20748
  setMode(next);
20691
- emit3(entries3, next);
20749
+ emit3(entries4, next);
20692
20750
  };
20693
- const newEntries = (candidates) => newKeywordEntries(entries3, candidates);
20751
+ const newEntries = (candidates) => newKeywordEntries(entries4, candidates);
20694
20752
  const addEntry = (raw) => {
20695
20753
  const added = newEntries([raw]);
20696
20754
  if (added.length === 0) {
20697
20755
  setDraft("");
20698
20756
  return;
20699
20757
  }
20700
- commit([...entries3, ...added]);
20758
+ commit([...entries4, ...added]);
20701
20759
  setDraft("");
20702
20760
  };
20703
20761
  const changeDraft = (next) => {
@@ -20708,7 +20766,7 @@ function KeywordEditor({
20708
20766
  const parts = next.split(",");
20709
20767
  const tail = parts.pop() ?? "";
20710
20768
  const added = newEntries(parts);
20711
- if (added.length > 0) commit([...entries3, ...added]);
20769
+ if (added.length > 0) commit([...entries4, ...added]);
20712
20770
  setDraft(tail);
20713
20771
  };
20714
20772
  const onPaste = (event) => {
@@ -20717,11 +20775,11 @@ function KeywordEditor({
20717
20775
  if (!CSV_PASTE_SEPARATOR.test(text)) return;
20718
20776
  event.preventDefault();
20719
20777
  const added = newEntries((draftText + text).split(CSV_PASTE_SEPARATOR));
20720
- if (added.length > 0) commit([...entries3, ...added]);
20778
+ if (added.length > 0) commit([...entries4, ...added]);
20721
20779
  setDraft("");
20722
20780
  };
20723
20781
  const removeEntry = (entry) => {
20724
- emit3(entries3.filter((e) => e !== entry));
20782
+ emit3(entries4.filter((e) => e !== entry));
20725
20783
  };
20726
20784
  const onKeyDown = (event) => {
20727
20785
  if (event.key === "Enter") {
@@ -20770,12 +20828,12 @@ function KeywordEditor({
20770
20828
  )
20771
20829
  ] })
20772
20830
  ] }),
20773
- entries3.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
20831
+ entries4.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
20774
20832
  "div",
20775
20833
  {
20776
20834
  className: "min-h-0 flex-1 overflow-auto border-t border-solid py-[var(--cmp-chipFilter-listbox-list-paddingY)]",
20777
20835
  style: { borderColor: CHIPFILTER.listboxBorder },
20778
- children: entries3.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(
20836
+ children: entries4.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(
20779
20837
  KeywordEntryRow,
20780
20838
  {
20781
20839
  label: optionLabel(field, entry),