@olundot/ui 0.20.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @olundot/ui
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e5aadba: 호환성을 깨는 변경이다. `BulkActionBar`의 `placement="inline"`, `compactSummary`, `trailing`을 지웠다. 세 가지 모두 0.20에서 사용 중단으로 표시한 것이다.
8
+
9
+ - `placement`는 `"floating"`만 받는다. 넘기던 곳에서는 `placement`를 지운다.
10
+ - `compactSummary`와 `trailing`은 `inline`에서만 쓰였다. 넘기던 곳에서는 지운다.
11
+ - `DataTable`의 가상 목록 머리가 `inline` 바 아래로 내려가던 `top-[40px]` 보정을 지웠다.
12
+ - `idleContent`, `emptyHint`, `DataTable`의 `announceSelection`은 그대로다.
13
+
14
+ ### Patch Changes
15
+
16
+ - 77acbcb: `AlertDialog`와 `ConfirmDialog`는 `description`과 본문이 모두 없으면 `aria-describedby`를 붙이지 않는다. 전에는 화면에 없는 요소의 id를 가리켰다. 제목을 설명으로 다시 쓰지 않으며, Radix의 개발 경고는 그대로 난다. 설명이나 본문이 있으면 지금처럼 그 요소를 가리킨다.
17
+ - 77acbcb: `AlertDialog`(`ConfirmDialog` 포함)와 구조형 `Dialog` 폼 모드는 `open`이 `false`가 되면 `onConfirm`·`onSubmit`이 잡고 있던 실행 중 상태를 푼다. 소비자가 준 `loading`이 `true`면 그대로 잠긴다. 전에는 `onConfirm`·`onSubmit`이 끝날 때까지 남아, 그사이 다시 연 창의 확인 버튼이 로딩으로 돌고 취소·Escape가 막혔다가 앞 실행이 끝나면 창이 닫혔다.
18
+
19
+ - 닫히기 전에 시작한 실행은 취소하지 않고 끝까지 간다. 끝나도 다시 연 창을 닫지 않고, 실행 중 상태를 풀지 않으며, 실패를 `onError`로 넘기지 않는다. `onError`가 없으면 실패는 지금처럼 호출자에게 전파된다.
20
+ - 닫힌 동안에는 실행하지 않는다. 닫히는 애니메이션 중 남은 확인·제출 버튼을 눌러도 `onConfirm`·`onSubmit`을 부르지 않는다.
21
+ - 같은 열림 안에서 한 번만 실행, 실행 중 닫기 막기, 성공하면 한 번 닫기는 그대로다.
22
+ - 닫았다 다시 연 창에서 같은 요청을 다시 보내는 중복은 막지 않는다. 핸들러와 서버가 막는다.
23
+
24
+ ## 0.20.2
25
+
26
+ ### Patch Changes
27
+
28
+ - 8a28822: `FilterToolbar layout="grid"`은 640px 미만에서 `filters`도 시트로 보낸다. 줄에는 검색, 접힌 필터 버튼, `actions`만 남고, 시트에는 `filters`를 순서대로 둔 뒤 `collapsible`을 둔다.
29
+
30
+ - 폭을 재기 전 첫 화면에서도 640px 미만이면 줄 필터를 펼치지 않는다. 컨테이너 쿼리로 숨기고, 폭을 잰 뒤에는 줄에 그리지 않는다.
31
+ - `collapsible` 없이 `filters`만 있어도 640px 미만에서 버튼과 시트가 생긴다.
32
+ - `activeLineFilterCount`: `filters` 중 적용한 조건 수. 640px 미만에서 버튼 배지, 버튼 이름, 적용 개수 줄의 개수에 더한다.
33
+ - `onClearLineFilters`: 640px 미만의 "해제"가 `onClearFilters`와 함께 부른다.
34
+ - 격자의 `filters`와 `collapsible` 컨트롤은 줄과 시트를 오가며 새로 그려지므로 값은 `value`로 넘긴다. `defaultValue`로 둔 값은 시트를 닫으면 남지 않는다.
35
+ - 두 prop을 주지 않으면 640px 미만 개수와 "해제"는 `collapsible` 조건만 다룬다. 640px 이상 격자와 `layout="flow"`는 그대로다.
36
+ - 격자에서 `collapseBelow="never"`도 `collapsible`을 줄에 펼치지 않는다.
37
+ - 격자는 툴바 폭을 컨테이너 쿼리와 같은 기준(내용 상자, CSS `zoom` 전)으로 잰다.
38
+
3
39
  ## 0.20.1
4
40
 
5
41
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -903,6 +903,10 @@ interface FilterToolbarProps extends React.HTMLAttributes<HTMLDivElement> {
903
903
  collapseBelow?: FilterToolbarCollapseBelow;
904
904
  activeFilterCount?: number;
905
905
  onClearFilters?: () => void;
906
+ /** grid: applied conditions among `filters`. Under 640px the count adds them. */
907
+ activeLineFilterCount?: number;
908
+ /** grid: under 640px 해제 calls it with `onClearFilters`. */
909
+ onClearLineFilters?: () => void;
906
910
  collapsedLabel?: string;
907
911
  resultSummary?: React.ReactNode;
908
912
  clearAction?: React.ReactNode;
@@ -1130,9 +1134,9 @@ interface DataTableBaseProps<T> extends Omit<React.HTMLAttributes<HTMLDivElement
1130
1134
  selectionUnit?: string;
1131
1135
  /**
1132
1136
  * `false` drops the built-in screen-reader selection message — when a
1133
- * `BulkActionBar placement="inline"` already announces the count (it is a status
1134
- * region), or when the real selection includes rows that are not loaded (the
1135
- * built-in message only counts loaded rows). Default `true`.
1137
+ * `BulkActionBar` already announces the count (it is a status region), or when the
1138
+ * real selection includes rows that are not loaded (the built-in message only
1139
+ * counts loaded rows). Default `true`.
1136
1140
  */
1137
1141
  announceSelection?: boolean;
1138
1142
  activeRowId?: string;
@@ -1290,42 +1294,24 @@ interface BulkActionBarBaseProps extends React.HTMLAttributes<HTMLDivElement> {
1290
1294
  /** Count unit — `12명 선택됨`. Defaults to `개`. */
1291
1295
  unit?: string;
1292
1296
  /**
1293
- * Under the count in the floating card, next to it inline — e.g. an "이 조건의 3,214명
1294
- * 모두 선택" link or a cap notice. The card wraps it at full width, never clips it.
1297
+ * Under the count — e.g. an "이 조건의 3,214명 모두 선택" link or a cap notice. The card
1298
+ * wraps it at full width, never clips it.
1295
1299
  */
1296
1300
  summary?: React.ReactNode;
1297
- /**
1298
- * Inline only: a shorter `summary` (e.g. "모두 선택") swapped in when the band is too
1299
- * narrow to show `summary` on one line. Without it, `summary` is clipped, never wrapped.
1300
- *
1301
- * @deprecated Only `placement="inline"` uses it; removed together with `inline` in the next minor.
1302
- */
1303
- compactSummary?: React.ReactNode;
1301
+ /** Removed in 0.21 with `placement="inline"`. */
1302
+ compactSummary?: never;
1303
+ /** Removed in 0.21 with `placement="inline"`. */
1304
+ trailing?: never;
1304
1305
  /**
1305
1306
  * Shown instead of the bar while nothing is selected (e.g. `100 / 3,214명`).
1306
1307
  * Takes precedence over `emptyHint`; with neither, the bar hides at count 0.
1307
1308
  */
1308
1309
  idleContent?: React.ReactNode;
1309
1310
  }
1310
- type BulkActionBarProps = BulkActionBarBaseProps & ({
1311
+ interface BulkActionBarProps extends BulkActionBarBaseProps {
1311
1312
  /** `floating` (default) — card stuck to the bottom of the list. */
1312
1313
  placement?: "floating";
1313
- /** Not taken by the floating card: edit-mode controls go at the right end of the toolbar. */
1314
- trailing?: never;
1315
- } | {
1316
- /**
1317
- * `inline` — full-width band stuck to the top, placed where the status line sits
1318
- * under the toolbar. Render it BEFORE the list so it comes first in tab order.
1319
- * Deprecated: `inline` is removed in the next minor.
1320
- */
1321
- placement: "inline";
1322
- /**
1323
- * Inline only: after the clear (✕) button — e.g. an edit-mode "완료" button. On the
1324
- * idle line it sits at the right end, outside the `role="status"` region, so it is
1325
- * not re-read whenever `idleContent` changes.
1326
- */
1327
- trailing?: React.ReactNode;
1328
- });
1314
+ }
1329
1315
  /**
1330
1316
  * OLUN BulkActionBar — multi-select table/list actions.
1331
1317
  *
@@ -1335,13 +1321,7 @@ type BulkActionBarProps = BulkActionBarBaseProps & ({
1335
1321
  * they do not fit. `variant: "destructive"` is outline danger. The card is
1336
1322
  * min(360px, its slot − 32px) wide; while it is near the viewport bottom the document root
1337
1323
  * carries `--olun-bulk-bar-offset` (viewport bottom → card top) for toasts to clear it.
1338
- * DOM order stays count → actions → ✕. It takes no `trailing`.
1339
- *
1340
- * placement="inline" (deprecated) is a top-sticky band that replaces the status
1341
- * line under the toolbar, so it comes before the rows in tab order. The band and
1342
- * the idle line are one 40px line: a long `summary` is clipped or swapped for
1343
- * `compactSummary`, never wrapped. Its actions are borderless text/icon buttons
1344
- * between hairline dividers; `trailing` follows the clear button.
1324
+ * DOM order stays count → actions → ✕.
1345
1325
  *
1346
1326
  * `unit` sets the count unit, `idleContent` shows while nothing is selected.
1347
1327
  *
@@ -1352,7 +1332,7 @@ type BulkActionBarProps = BulkActionBarBaseProps & ({
1352
1332
  * { key: "del", label: "삭제", variant: "destructive", onClick: confirmDelete },
1353
1333
  * ]} />
1354
1334
  */
1355
- declare function BulkActionBar({ count, actions, onClear, emptyHint, placement, unit, summary, compactSummary, idleContent, trailing, className, style, ...props }: BulkActionBarProps): react_jsx_runtime.JSX.Element | null;
1335
+ declare function BulkActionBar({ count, actions, onClear, emptyHint, placement: _placement, compactSummary: _compactSummary, trailing: _trailing, unit, summary, idleContent, className, style, ...props }: BulkActionBarProps): react_jsx_runtime.JSX.Element | null;
1356
1336
 
1357
1337
  interface ConfirmDialogProps {
1358
1338
  open: boolean;
package/dist/index.js CHANGED
@@ -3800,6 +3800,7 @@ import { Dialog as DialogPrimitive3 } from "radix-ui";
3800
3800
  // src/lib/use-overlay-action.ts
3801
3801
  import * as React22 from "react";
3802
3802
  function useOverlayAction({
3803
+ open,
3803
3804
  action,
3804
3805
  onOpenChange,
3805
3806
  loading = false,
@@ -3808,7 +3809,14 @@ function useOverlayAction({
3808
3809
  }) {
3809
3810
  const [pending, setPending] = React22.useState(false);
3810
3811
  const busyRef = React22.useRef(false);
3812
+ const runIdRef = React22.useRef(0);
3811
3813
  const isBusy = loading || pending;
3814
+ React22.useEffect(() => {
3815
+ if (open) return;
3816
+ runIdRef.current += 1;
3817
+ busyRef.current = false;
3818
+ setPending(false);
3819
+ }, [open]);
3812
3820
  const guardOpenChange = React22.useCallback(
3813
3821
  (nextOpen) => {
3814
3822
  if (!nextOpen && isBusy) {
@@ -3819,22 +3827,25 @@ function useOverlayAction({
3819
3827
  [isBusy, onOpenChange]
3820
3828
  );
3821
3829
  const run = React22.useCallback(async () => {
3822
- if (busyRef.current || loading || disabled) {
3830
+ if (!open || busyRef.current || loading || disabled) {
3823
3831
  return;
3824
3832
  }
3833
+ const runId = ++runIdRef.current;
3825
3834
  busyRef.current = true;
3826
3835
  setPending(true);
3827
3836
  try {
3828
3837
  await action();
3829
- onOpenChange(false);
3838
+ if (runId === runIdRef.current) onOpenChange(false);
3830
3839
  } catch (error) {
3831
3840
  if (!onError) throw error;
3832
- onError(error);
3841
+ if (runId === runIdRef.current) onError(error);
3833
3842
  } finally {
3834
- busyRef.current = false;
3835
- setPending(false);
3843
+ if (runId === runIdRef.current) {
3844
+ busyRef.current = false;
3845
+ setPending(false);
3846
+ }
3836
3847
  }
3837
- }, [action, disabled, loading, onError, onOpenChange]);
3848
+ }, [action, disabled, loading, onError, onOpenChange, open]);
3838
3849
  return { isBusy, run, guardOpenChange };
3839
3850
  }
3840
3851
 
@@ -5019,6 +5030,7 @@ function StructuredDialog({
5019
5030
  const policy = OVERLAY_POLICY.dialog;
5020
5031
  const formMode = onSubmit !== void 0;
5021
5032
  const { isBusy, run, guardOpenChange } = useOverlayAction({
5033
+ open,
5022
5034
  action: onSubmit ?? noAction,
5023
5035
  onOpenChange,
5024
5036
  loading: formMode && loading,
@@ -5326,6 +5338,7 @@ function AlertDialogFrame({
5326
5338
  className: overlayBoxClassName("alert"),
5327
5339
  onOpenAutoFocus,
5328
5340
  onCloseAutoFocus: returnFocus.onCloseAutoFocus,
5341
+ ...description == null && children == null && { "aria-describedby": void 0 },
5329
5342
  children: [
5330
5343
  /* @__PURE__ */ jsx28(OverlayHeader, { kind: "alert", title, description }),
5331
5344
  children != null ? description != null ? /* @__PURE__ */ jsx28(OverlayBody, { kind: "alert", children }) : /* @__PURE__ */ jsx28(AlertDialogPrimitive2.Description, { asChild: true, children: /* @__PURE__ */ jsx28(OverlayBody, { kind: "alert", children }) }) : null,
@@ -5352,6 +5365,7 @@ function AlertDialog({
5352
5365
  acknowledgeLabel = "\uD655\uC778"
5353
5366
  }) {
5354
5367
  const { isBusy, run, guardOpenChange } = useOverlayAction({
5368
+ open,
5355
5369
  action: onConfirm ?? noop,
5356
5370
  onOpenChange,
5357
5371
  loading,
@@ -5737,14 +5751,8 @@ var collapsibleClassNames = {
5737
5751
  trigger: "hidden"
5738
5752
  }
5739
5753
  };
5740
- var gridCollapsibleClassNames = {
5741
- sm: { inline: "hidden", trigger: "" },
5742
- md: { inline: "hidden", trigger: "" },
5743
- lg: { inline: "hidden", trigger: "" },
5744
- never: { inline: "flex @[640px]:hidden", trigger: "hidden @[640px]:inline" }
5745
- };
5746
5754
  var gridSearchClassName = "order-1 min-w-[min(160px,100%)] max-w-full flex-[1_1_0%] @[640px]:min-w-0 @[640px]:flex-[3_1_0%]";
5747
- var gridFiltersClassName = "order-3 flex min-w-0 flex-[1_0_100%] flex-wrap items-center gap-[var(--space-2)] @[640px]:contents";
5755
+ var gridFiltersClassName = "order-3 hidden min-w-0 flex-[1_0_100%] flex-wrap items-center gap-[var(--space-2)] @[640px]:contents";
5748
5756
  var gridCellClassName = cn(
5749
5757
  "contents @[640px]:block @[640px]:order-3 @[640px]:min-w-0 @[640px]:flex-[3_1_0%]",
5750
5758
  "@[640px]:[&:has([data-slot=select-trigger])]:flex-[2_1_0%]",
@@ -5766,6 +5774,10 @@ function filterCells(node, prefix = "") {
5766
5774
  return React31.isValidElement(child) && child.type === React31.Fragment ? filterCells(child.props.children, `${key}/`) : [{ key, cell: child }];
5767
5775
  });
5768
5776
  }
5777
+ function contentWidth(element) {
5778
+ const style = getComputedStyle(element);
5779
+ return element.clientWidth - (parseFloat(style.paddingLeft) || 0) - (parseFloat(style.paddingRight) || 0);
5780
+ }
5769
5781
  function warnGridFilters(count) {
5770
5782
  if (typeof process === "undefined" || process.env.NODE_ENV === "production") return;
5771
5783
  console.warn(
@@ -5781,6 +5793,8 @@ var FilterToolbar = React31.forwardRef(
5781
5793
  collapseBelow = "lg",
5782
5794
  activeFilterCount: activeFilterCountProp,
5783
5795
  onClearFilters,
5796
+ activeLineFilterCount,
5797
+ onClearLineFilters,
5784
5798
  collapsedLabel = "\uD544\uD130",
5785
5799
  resultSummary,
5786
5800
  clearAction,
@@ -5796,6 +5810,7 @@ var FilterToolbar = React31.forwardRef(
5796
5810
  null
5797
5811
  );
5798
5812
  const [overlayOpen, setOverlayOpen] = React31.useState(false);
5813
+ const rootRef = React31.useRef(null);
5799
5814
  const triggerRef = React31.useRef(null);
5800
5815
  const rowRef = React31.useRef(null);
5801
5816
  const chipsRef = React31.useRef(null);
@@ -5803,28 +5818,31 @@ var FilterToolbar = React31.forwardRef(
5803
5818
  const pendingFocusRef = React31.useRef(null);
5804
5819
  const setRootRefs = React31.useCallback(
5805
5820
  (element) => {
5821
+ rootRef.current = element;
5806
5822
  setRootElement(element);
5807
5823
  if (typeof ref === "function") ref(element);
5808
5824
  else if (ref) ref.current = element;
5809
5825
  },
5810
5826
  [ref]
5811
5827
  );
5828
+ const isGrid = layout === "grid";
5812
5829
  React31.useEffect(() => {
5813
5830
  if (!rootElement) return;
5814
- const updateWidth = () => {
5815
- setContainerWidth(rootElement.getBoundingClientRect().width);
5831
+ const updateWidth = (entry) => {
5832
+ setContainerWidth(
5833
+ isGrid ? entry?.contentBoxSize?.[0]?.inlineSize ?? contentWidth(rootElement) : rootElement.getBoundingClientRect().width
5834
+ );
5816
5835
  };
5817
5836
  updateWidth();
5818
5837
  if (typeof ResizeObserver === "undefined") return;
5819
- const observer = new ResizeObserver(updateWidth);
5838
+ const observer = new ResizeObserver(([entry]) => updateWidth(entry));
5820
5839
  observer.observe(rootElement);
5821
5840
  return () => observer.disconnect();
5822
- }, [rootElement]);
5841
+ }, [rootElement, isGrid]);
5823
5842
  const handleOverlayOpenChange = React31.useCallback((open) => {
5824
5843
  setOverlayOpen(open);
5825
5844
  if (!open) queueMicrotask(() => triggerRef.current?.focus());
5826
5845
  }, []);
5827
- const isGrid = layout === "grid";
5828
5846
  const gridCells = isGrid ? filterCells(filters) : [];
5829
5847
  const filterCellCount = gridCells.length;
5830
5848
  React31.useEffect(() => {
@@ -5866,33 +5884,67 @@ var FilterToolbar = React31.forwardRef(
5866
5884
  const hasAppliedFilters = activeFilterCount > 0;
5867
5885
  const hasAppliedChips = Boolean(appliedFilters && appliedFilters.length > 0);
5868
5886
  const usesSheet = containerWidth !== null && containerWidth < 640;
5869
- const collapseClasses = (isGrid ? gridCollapsibleClassNames : collapsibleClassNames)[collapseBelow];
5870
- const collapseTriggerLabel = hasAppliedFilters ? `${collapsedLabel}, ${activeFilterCount}\uAC1C \uC801\uC6A9` : collapsedLabel;
5871
- const clearFiltersButton = onClearFilters ? /* @__PURE__ */ jsx32(
5887
+ const gridSheet = isGrid && Boolean(filters);
5888
+ const linesInSheet = gridSheet && usesSheet;
5889
+ const lineFilterCount = gridSheet ? activeLineFilterCount ?? 0 : 0;
5890
+ const narrowFilterCount = activeFilterCount + lineFilterCount;
5891
+ const hasCollapsed = Boolean(collapsible) || gridSheet;
5892
+ const outgrownSheet = isGrid && !collapsible && containerWidth !== null && containerWidth >= 640 && overlayOpen;
5893
+ React31.useEffect(() => {
5894
+ if (!outgrownSheet) return;
5895
+ setOverlayOpen(false);
5896
+ const ownerDocument = rootElement?.ownerDocument;
5897
+ const active = ownerDocument?.activeElement;
5898
+ if (!ownerDocument || active && active !== ownerDocument.body && active.isConnected) return;
5899
+ const cell = rowRef.current?.querySelector('[data-slot="filter-toolbar-cell"]');
5900
+ [...cell?.querySelectorAll(focusableSelector) ?? []].find(canTakeFocus)?.focus();
5901
+ }, [outgrownSheet, rootElement]);
5902
+ const inlineClassName = isGrid ? "hidden" : collapsibleClassNames[collapseBelow].inline;
5903
+ const triggerClassName = isGrid ? collapsible ? "" : "@[640px]:hidden" : collapsibleClassNames[collapseBelow].trigger;
5904
+ const triggerCount = linesInSheet ? narrowFilterCount : activeFilterCount;
5905
+ const collapseTriggerLabel = triggerCount > 0 ? `${collapsedLabel}, ${triggerCount}\uAC1C \uC801\uC6A9` : collapsedLabel;
5906
+ const narrowOnlyCount = !hasAppliedFilters && narrowFilterCount > 0;
5907
+ const countText = narrowFilterCount === activeFilterCount || narrowOnlyCount ? narrowFilterCount : /* @__PURE__ */ jsxs28(Fragment14, { children: [
5908
+ /* @__PURE__ */ jsx32("span", { className: "@[640px]:hidden", children: narrowFilterCount }),
5909
+ /* @__PURE__ */ jsx32("span", { className: "hidden @[640px]:inline", children: activeFilterCount })
5910
+ ] });
5911
+ const clearLineFilters = gridSheet ? onClearLineFilters : void 0;
5912
+ const clearNarrow = onClearFilters && clearLineFilters ? () => {
5913
+ onClearFilters();
5914
+ clearLineFilters();
5915
+ } : onClearFilters ?? clearLineFilters;
5916
+ const clearFilters = () => {
5917
+ const width = containerWidth ?? (rootRef.current ? contentWidth(rootRef.current) : null);
5918
+ (width !== null && width < 640 ? clearNarrow : onClearFilters)?.();
5919
+ };
5920
+ const clearFiltersButton = (linesInSheet ? clearNarrow : onClearFilters) ? /* @__PURE__ */ jsx32(
5872
5921
  "button",
5873
5922
  {
5874
5923
  type: "button",
5875
5924
  "data-slot": "filter-toolbar-clear-filters",
5876
5925
  className: "cursor-pointer border-0 bg-transparent p-0 text-body-sm font-medium text-text-secondary no-underline",
5877
- onClick: onClearFilters,
5926
+ onClick: clearFilters,
5878
5927
  children: "\uD574\uC81C"
5879
5928
  }
5880
5929
  ) : null;
5881
- const collapsedContent = collapsible ? /* @__PURE__ */ jsx32(
5930
+ const collapsedContent = hasCollapsed ? /* @__PURE__ */ jsxs28(
5882
5931
  "div",
5883
5932
  {
5884
5933
  "data-slot": "filter-toolbar-collapsed-content",
5885
5934
  className: "flex flex-col gap-[var(--space-3)] [&>*]:w-full",
5886
- children: collapsible
5935
+ children: [
5936
+ linesInSheet ? filters : null,
5937
+ collapsible
5938
+ ]
5887
5939
  }
5888
5940
  ) : null;
5889
- const collapseTrigger = collapsible ? /* @__PURE__ */ jsxs28(
5941
+ const collapseTrigger = hasCollapsed ? /* @__PURE__ */ jsxs28(
5890
5942
  "span",
5891
5943
  {
5892
5944
  "data-slot": "filter-toolbar-collapse-trigger",
5893
5945
  className: cn(
5894
5946
  "relative order-2 shrink-0 @[640px]:order-4",
5895
- collapseClasses.trigger
5947
+ triggerClassName
5896
5948
  ),
5897
5949
  children: [
5898
5950
  /* @__PURE__ */ jsx32(
@@ -5907,16 +5959,17 @@ var FilterToolbar = React31.forwardRef(
5907
5959
  onClick: () => handleOverlayOpenChange(true)
5908
5960
  }
5909
5961
  ),
5910
- hasAppliedFilters ? /* @__PURE__ */ jsx32(
5962
+ narrowFilterCount > 0 ? /* @__PURE__ */ jsx32(
5911
5963
  "span",
5912
5964
  {
5913
5965
  "aria-hidden": "true",
5914
5966
  "data-slot": "filter-toolbar-collapse-count",
5915
5967
  className: cn(
5916
5968
  chipCountVariants({ tone: "accent", size: "md" }),
5917
- "pointer-events-none absolute right-0 top-0 translate-x-1/3 -translate-y-1/3"
5969
+ "pointer-events-none absolute right-0 top-0 translate-x-1/3 -translate-y-1/3",
5970
+ narrowOnlyCount && "@[640px]:hidden"
5918
5971
  ),
5919
- children: activeFilterCount
5972
+ children: countText
5920
5973
  }
5921
5974
  ) : null
5922
5975
  ]
@@ -5952,7 +6005,7 @@ var FilterToolbar = React31.forwardRef(
5952
6005
  children: search
5953
6006
  }
5954
6007
  ) : null,
5955
- collapsible && usesSheet ? collapseTrigger : null,
6008
+ hasCollapsed && (usesSheet || !collapsible) ? collapseTrigger : null,
5956
6009
  collapsible && !usesSheet ? /* @__PURE__ */ jsxs28(Popover, { open: overlayOpen, onOpenChange: handleOverlayOpenChange, children: [
5957
6010
  /* @__PURE__ */ jsx32(PopoverAnchor, { asChild: true, children: collapseTrigger }),
5958
6011
  overlayOpen ? /* @__PURE__ */ jsxs28(
@@ -5971,7 +6024,7 @@ var FilterToolbar = React31.forwardRef(
5971
6024
  }
5972
6025
  ) : null
5973
6026
  ] }) : null,
5974
- filters ? /* @__PURE__ */ jsx32(
6027
+ filters && !linesInSheet ? /* @__PURE__ */ jsx32(
5975
6028
  "div",
5976
6029
  {
5977
6030
  "data-slot": "filter-toolbar-filters",
@@ -5993,7 +6046,7 @@ var FilterToolbar = React31.forwardRef(
5993
6046
  "data-slot": "filter-toolbar-collapsible",
5994
6047
  className: cn(
5995
6048
  "order-5 min-w-0 items-center gap-[var(--space-2)]",
5996
- collapseClasses.inline
6049
+ inlineClassName
5997
6050
  ),
5998
6051
  children: collapsible
5999
6052
  }
@@ -6009,7 +6062,7 @@ var FilterToolbar = React31.forwardRef(
6009
6062
  ]
6010
6063
  }
6011
6064
  ) : null,
6012
- collapsible && usesSheet && overlayOpen ? /* @__PURE__ */ jsx32(
6065
+ hasCollapsed && usesSheet && overlayOpen ? /* @__PURE__ */ jsx32(
6013
6066
  Sheet,
6014
6067
  {
6015
6068
  open: overlayOpen,
@@ -6070,28 +6123,31 @@ var FilterToolbar = React31.forwardRef(
6070
6123
  ]
6071
6124
  }
6072
6125
  ) : null,
6073
- hasAppliedFilters ? /* @__PURE__ */ jsxs28(
6126
+ narrowFilterCount > 0 ? /* @__PURE__ */ jsxs28(
6074
6127
  "div",
6075
6128
  {
6076
6129
  "data-slot": "filter-toolbar-applied",
6077
6130
  className: cn(
6078
6131
  "flex items-center gap-[var(--space-1)] text-[11px] text-text-tertiary",
6079
- hasAppliedChips && "@[640px]:hidden"
6132
+ (hasAppliedChips || narrowOnlyCount) && "@[640px]:hidden"
6080
6133
  ),
6081
6134
  children: [
6082
6135
  /* @__PURE__ */ jsxs28("span", { children: [
6083
6136
  "\uD544\uD130 ",
6084
- activeFilterCount,
6137
+ countText,
6085
6138
  "\uAC1C \uC801\uC6A9"
6086
6139
  ] }),
6087
- onClearFilters ? /* @__PURE__ */ jsxs28(Fragment14, { children: [
6088
- /* @__PURE__ */ jsx32("span", { "aria-hidden": "true", children: " \xB7 " }),
6140
+ clearNarrow ? /* @__PURE__ */ jsxs28(Fragment14, { children: [
6141
+ /* @__PURE__ */ jsx32("span", { "aria-hidden": "true", className: onClearFilters ? void 0 : "@[640px]:hidden", children: " \xB7 " }),
6089
6142
  /* @__PURE__ */ jsx32(
6090
6143
  "button",
6091
6144
  {
6092
6145
  type: "button",
6093
- className: "cursor-pointer border-0 bg-transparent p-0 text-[11px] text-text-secondary no-underline",
6094
- onClick: onClearFilters,
6146
+ className: cn(
6147
+ "cursor-pointer border-0 bg-transparent p-0 text-[11px] text-text-secondary no-underline",
6148
+ !onClearFilters && "@[640px]:hidden"
6149
+ ),
6150
+ onClick: clearFilters,
6095
6151
  children: "\uD574\uC81C"
6096
6152
  }
6097
6153
  )
@@ -8490,10 +8546,8 @@ function DataTableInner({
8490
8546
  role: "row",
8491
8547
  className: cn(
8492
8548
  "grid min-h-[40px] items-center bg-[var(--bg-raised)] text-[12px] font-semibold leading-snug text-text-tertiary",
8493
- // Virtual lists are long: the header sticks inside the scroll container,
8494
- // below an inline BulkActionBar band (40px, --z-sticky) rendered before
8495
- // the table — band on top, header under it, rows beneath both.
8496
- isVirtual && "sticky top-0 z-[2] [[data-slot=bulk-action-bar][data-placement=inline]:not([data-state=idle])~[data-slot=data-table]_&]:top-[40px]"
8549
+ // Virtual lists are long: the header sticks inside the scroll container.
8550
+ isVirtual && "sticky top-0 z-[2]"
8497
8551
  ),
8498
8552
  style: { gridTemplateColumns },
8499
8553
  children: [
@@ -8635,51 +8689,6 @@ DataTable.displayName = "DataTable";
8635
8689
  import * as React36 from "react";
8636
8690
  import { X as X4 } from "lucide-react";
8637
8691
  import { jsx as jsx39, jsxs as jsxs35 } from "react/jsx-runtime";
8638
- function ActionButton({ action }) {
8639
- const danger = action.variant === "destructive";
8640
- const off = action.disabled;
8641
- const Icon = action.icon;
8642
- return /* @__PURE__ */ jsxs35(
8643
- "button",
8644
- {
8645
- type: "button",
8646
- "data-slot": "bulk-action-bar-action",
8647
- "data-variant": action.variant ?? "default",
8648
- disabled: off,
8649
- title: action.title,
8650
- onClick: action.onClick,
8651
- className: cn(
8652
- "border-0 bg-transparent outline-none transition-[background-color,box-shadow,color] motion-reduce:transition-none",
8653
- "focus-visible:shadow-[0_0_0_2px_var(--focus-ring)]",
8654
- off ? "cursor-not-allowed text-text-disabled" : danger ? "cursor-pointer text-[var(--error)] hover:bg-[var(--error-bg)]" : "cursor-pointer text-text-secondary hover:bg-[var(--action-hover)] hover:text-foreground"
8655
- ),
8656
- style: {
8657
- display: "inline-flex",
8658
- alignItems: "center",
8659
- gap: 6,
8660
- height: 30,
8661
- padding: "0 10px",
8662
- borderRadius: "var(--radius-md)",
8663
- fontFamily: "var(--font-sans)",
8664
- fontSize: 13,
8665
- fontWeight: 500,
8666
- whiteSpace: "nowrap"
8667
- },
8668
- children: [
8669
- Icon ? /* @__PURE__ */ jsx39(
8670
- Icon,
8671
- {
8672
- "aria-hidden": "true",
8673
- "data-slot": "bulk-action-bar-action-icon",
8674
- size: 16,
8675
- strokeWidth: 1.75
8676
- }
8677
- ) : null,
8678
- action.label
8679
- ]
8680
- }
8681
- );
8682
- }
8683
8692
  function CardActionButton({ action }) {
8684
8693
  const Icon = action.icon;
8685
8694
  return /* @__PURE__ */ jsxs35(
@@ -8709,52 +8718,8 @@ function CardActionButton({ action }) {
8709
8718
  }
8710
8719
  );
8711
8720
  }
8712
- function Divider() {
8713
- return /* @__PURE__ */ jsx39(
8714
- "span",
8715
- {
8716
- "aria-hidden": "true",
8717
- "data-slot": "bulk-action-bar-divider",
8718
- style: {
8719
- width: 1,
8720
- height: 18,
8721
- flexShrink: 0,
8722
- background: "var(--border-subtle)"
8723
- }
8724
- }
8725
- );
8726
- }
8727
- function ClearButton({ onClear }) {
8728
- return /* @__PURE__ */ jsx39(
8729
- "button",
8730
- {
8731
- type: "button",
8732
- "aria-label": "\uC120\uD0DD \uD574\uC81C",
8733
- "data-slot": "bulk-action-bar-clear",
8734
- onClick: onClear,
8735
- className: cn(
8736
- "cursor-pointer border-0 bg-transparent text-text-secondary outline-none",
8737
- "transition-[background-color,box-shadow,color] motion-reduce:transition-none",
8738
- "hover:bg-[var(--action-hover)] hover:text-foreground",
8739
- "focus-visible:shadow-[0_0_0_2px_var(--focus-ring)]"
8740
- ),
8741
- style: {
8742
- display: "inline-flex",
8743
- width: 32,
8744
- height: 32,
8745
- flexShrink: 0,
8746
- alignItems: "center",
8747
- justifyContent: "center",
8748
- borderRadius: "var(--radius-md)"
8749
- },
8750
- children: /* @__PURE__ */ jsx39(X4, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" })
8751
- }
8752
- );
8753
- }
8754
8721
  var CLEAR_CLEARANCE = 32;
8755
- var INLINE_HEIGHT = 40;
8756
- var HIT_AREA = 44;
8757
- var HIT_INSET = 8;
8722
+ var IDLE_HEIGHT = 40;
8758
8723
  var FLOATING_BOTTOM = "calc(var(--space-4) + env(safe-area-inset-bottom))";
8759
8724
  var floatingStyle = {
8760
8725
  position: "sticky",
@@ -8771,28 +8736,10 @@ var floatingStyle = {
8771
8736
  padding: "var(--space-2) var(--space-4) var(--space-3)",
8772
8737
  boxShadow: "var(--shadow-8)"
8773
8738
  };
8774
- var inlineStyle = {
8775
- position: "sticky",
8776
- top: 0,
8777
- zIndex: "var(--z-sticky)",
8778
- display: "flex",
8779
- boxSizing: "border-box",
8780
- height: INLINE_HEIGHT,
8781
- alignItems: "center",
8782
- gap: "var(--space-2)",
8783
- borderRadius: "var(--radius-md)",
8784
- border: "1px solid var(--border)",
8785
- background: "var(--bg-surface)",
8786
- padding: "0 var(--space-1) 0 var(--space-3)",
8787
- boxShadow: "var(--shadow-2)",
8788
- fontFamily: "var(--font-sans)",
8789
- fontSize: 13,
8790
- fontVariantNumeric: "tabular-nums"
8791
- };
8792
8739
  var idleLineStyle = {
8793
8740
  display: "flex",
8794
8741
  boxSizing: "border-box",
8795
- height: INLINE_HEIGHT,
8742
+ height: IDLE_HEIGHT,
8796
8743
  overflow: "hidden",
8797
8744
  alignItems: "center",
8798
8745
  columnGap: "var(--space-3)",
@@ -8802,22 +8749,6 @@ var idleLineStyle = {
8802
8749
  fontVariantNumeric: "tabular-nums",
8803
8750
  color: "var(--text-secondary)"
8804
8751
  };
8805
- var warnedInline = false;
8806
- function warnInlineOnce() {
8807
- if (warnedInline || typeof process === "undefined" || process.env.NODE_ENV === "production") {
8808
- return;
8809
- }
8810
- warnedInline = true;
8811
- console.warn('BulkActionBar: placement="inline" is deprecated and removed in the next minor. Use "floating".');
8812
- }
8813
- var warnedTrailing = false;
8814
- function warnTrailingOnce() {
8815
- if (warnedTrailing || typeof process === "undefined" || process.env.NODE_ENV === "production") {
8816
- return;
8817
- }
8818
- warnedTrailing = true;
8819
- console.warn("BulkActionBar: the floating card ignores `trailing`. Put edit-mode controls at the right end of the toolbar.");
8820
- }
8821
8752
  var OFFSET_PROPERTY = "--olun-bulk-bar-offset";
8822
8753
  var LIFT_ZONE = 204;
8823
8754
  var cardOffsets = /* @__PURE__ */ new Map();
@@ -8921,104 +8852,31 @@ function useBottomOffset(card) {
8921
8852
  if (card && typeof ResizeObserver === "undefined") reportOffset(key, card);
8922
8853
  });
8923
8854
  }
8924
- function useCompactSummary(slot, enabled) {
8925
- const [fullWidth, setFullWidth] = React36.useState(null);
8926
- const measure = React36.useCallback(() => {
8927
- if (!slot) return;
8928
- setFullWidth((prev) => {
8929
- if (prev === null) return slot.scrollWidth > slot.clientWidth ? slot.scrollWidth : null;
8930
- return slot.clientWidth >= prev ? null : prev;
8931
- });
8932
- }, [slot]);
8933
- useIsomorphicLayoutEffect2(() => {
8934
- if (enabled) measure();
8935
- });
8936
- React36.useEffect(() => {
8937
- if (!enabled || !slot || typeof ResizeObserver === "undefined") return;
8938
- const observer = new ResizeObserver(measure);
8939
- observer.observe(slot);
8940
- return () => observer.disconnect();
8941
- }, [enabled, slot, measure]);
8942
- return enabled && fullWidth !== null;
8943
- }
8944
8855
  function BulkActionBar({
8945
8856
  count,
8946
8857
  actions,
8947
8858
  onClear,
8948
8859
  emptyHint,
8949
- placement = "floating",
8860
+ // Removed props are dropped here so a spread or untyped caller never leaks them to the DOM.
8861
+ placement: _placement,
8862
+ compactSummary: _compactSummary,
8863
+ trailing: _trailing,
8950
8864
  unit = "\uAC1C",
8951
8865
  summary,
8952
- compactSummary,
8953
8866
  idleContent,
8954
- trailing,
8955
8867
  className,
8956
8868
  style,
8957
8869
  ...props
8958
8870
  }) {
8959
- const inline = placement === "inline";
8960
- if (inline) warnInlineOnce();
8961
- const hasTrailing = trailing !== void 0 && trailing !== null;
8962
- if (!inline && hasTrailing) warnTrailingOnce();
8963
- const trailingSlot = inline && hasTrailing ? /* @__PURE__ */ jsx39(
8964
- "div",
8965
- {
8966
- "data-slot": "bulk-action-bar-trailing",
8967
- style: {
8968
- display: "flex",
8969
- flexShrink: 0,
8970
- alignItems: "center",
8971
- gap: "var(--space-2)"
8972
- },
8973
- children: trailing
8974
- }
8975
- ) : null;
8976
- const [summaryElement, setSummaryElement] = React36.useState(null);
8977
- const compact = useCompactSummary(
8978
- summaryElement,
8979
- inline && compactSummary !== void 0 && compactSummary !== null
8980
- );
8981
8871
  const [card, setCard] = React36.useState(null);
8982
8872
  useBottomOffset(card);
8983
- if (count === 0 && idleContent !== void 0 && trailingSlot) {
8984
- return /* @__PURE__ */ jsxs35(
8985
- "div",
8986
- {
8987
- "data-slot": "bulk-action-bar",
8988
- "data-placement": placement,
8989
- "data-state": "idle",
8990
- className,
8991
- style: { ...idleLineStyle, overflow: "visible", ...style },
8992
- ...props,
8993
- children: [
8994
- /* @__PURE__ */ jsx39(
8995
- "div",
8996
- {
8997
- role: "status",
8998
- "data-slot": "bulk-action-bar-idle",
8999
- style: {
9000
- display: "flex",
9001
- flex: "1 1 auto",
9002
- minWidth: 0,
9003
- alignItems: "center",
9004
- columnGap: "var(--space-3)",
9005
- overflow: "hidden"
9006
- },
9007
- children: idleContent
9008
- }
9009
- ),
9010
- trailingSlot
9011
- ]
9012
- }
9013
- );
9014
- }
9015
8873
  if (count === 0 && idleContent !== void 0) {
9016
8874
  return /* @__PURE__ */ jsx39(
9017
8875
  "div",
9018
8876
  {
9019
8877
  role: "status",
9020
8878
  "data-slot": "bulk-action-bar",
9021
- "data-placement": placement,
8879
+ "data-placement": "floating",
9022
8880
  "data-state": "idle",
9023
8881
  className,
9024
8882
  style: { ...idleLineStyle, ...style },
@@ -9034,38 +8892,25 @@ function BulkActionBar({
9034
8892
  "span",
9035
8893
  {
9036
8894
  "data-slot": "bulk-action-bar-count",
9037
- style: inline ? {
9038
- paddingRight: "var(--space-1)",
8895
+ style: {
8896
+ display: "flex",
8897
+ minHeight: 32,
8898
+ alignItems: "center",
8899
+ paddingRight: CLEAR_CLEARANCE,
9039
8900
  fontFamily: "var(--font-sans)",
9040
8901
  fontSize: 13,
9041
- fontWeight: 600,
8902
+ fontWeight: 500,
9042
8903
  fontVariantNumeric: "tabular-nums",
9043
8904
  color: "var(--text-primary)",
9044
- whiteSpace: "nowrap"
9045
- } : (
9046
- // First row, 32px like the ✕ beside it; the right padding keeps text clear of it,
9047
- // and a long unbroken unit or hint breaks instead of running under it.
9048
- {
9049
- display: "flex",
9050
- minHeight: 32,
9051
- alignItems: "center",
9052
- paddingRight: CLEAR_CLEARANCE,
9053
- fontFamily: "var(--font-sans)",
9054
- fontSize: 13,
9055
- fontWeight: 500,
9056
- fontVariantNumeric: "tabular-nums",
9057
- color: "var(--text-primary)",
9058
- wordBreak: "keep-all",
9059
- overflowWrap: "anywhere"
9060
- }
9061
- ),
8905
+ wordBreak: "keep-all",
8906
+ overflowWrap: "anywhere"
8907
+ },
9062
8908
  children: count > 0 ? `${count.toLocaleString("ko-KR")}${unit} \uC120\uD0DD\uB428` : emptyHint
9063
8909
  }
9064
8910
  );
9065
8911
  const summarySlot = summary !== void 0 && summary !== null ? /* @__PURE__ */ jsx39(
9066
8912
  "span",
9067
8913
  {
9068
- ref: setSummaryElement,
9069
8914
  "data-slot": "bulk-action-bar-summary",
9070
8915
  style: {
9071
8916
  display: "flex",
@@ -9075,98 +8920,15 @@ function BulkActionBar({
9075
8920
  fontFamily: "var(--font-sans)",
9076
8921
  fontSize: 13,
9077
8922
  color: "var(--text-secondary)",
9078
- ...inline ? (
9079
- // inline-size containment: the summary never widens the band's parent. The
9080
- // slot clips text, so it is as tall as an IconButton's hit area (overhanging
9081
- // the 40px band) and has that inset as its left gutter: a compact IconButton
9082
- // keeps its full 44px target. The right gutter stays 3px (focus ring) so
9083
- // clipped text keeps clear of the controls.
9084
- {
9085
- flex: "1 1 0",
9086
- alignSelf: "center",
9087
- height: HIT_AREA,
9088
- margin: `0 -3px 0 -${HIT_INSET}px`,
9089
- padding: `0 3px 0 ${HIT_INSET}px`,
9090
- overflow: "hidden",
9091
- whiteSpace: "nowrap",
9092
- contain: "inline-size"
9093
- }
9094
- ) : (
9095
- // Its own row at the card's full inner width; wraps, never clips.
9096
- { flexWrap: "wrap", lineHeight: 1.5, wordBreak: "keep-all", overflowWrap: "anywhere" }
9097
- )
8923
+ // Its own row at the card's full inner width; wraps, never clips.
8924
+ flexWrap: "wrap",
8925
+ lineHeight: 1.5,
8926
+ wordBreak: "keep-all",
8927
+ overflowWrap: "anywhere"
9098
8928
  },
9099
- children: compact ? compactSummary : summary
8929
+ children: summary
9100
8930
  }
9101
8931
  ) : null;
9102
- if (inline) {
9103
- const actionGroup = /* @__PURE__ */ jsx39(
9104
- "div",
9105
- {
9106
- "data-slot": "bulk-action-bar-actions",
9107
- style: {
9108
- display: "flex",
9109
- minWidth: 0,
9110
- flexWrap: "nowrap",
9111
- alignItems: "center",
9112
- justifyContent: "flex-end",
9113
- gap: 2
9114
- },
9115
- children: actions.map((action) => /* @__PURE__ */ jsx39(ActionButton, { action }, action.key))
9116
- }
9117
- );
9118
- return /* @__PURE__ */ jsxs35(
9119
- "div",
9120
- {
9121
- role: "toolbar",
9122
- "aria-label": "\uC120\uD0DD \uD56D\uBAA9 \uC77C\uAD04 \uC791\uC5C5",
9123
- "data-slot": "bulk-action-bar",
9124
- "data-placement": "inline",
9125
- className,
9126
- style: { ...inlineStyle, ...style },
9127
- ...props,
9128
- children: [
9129
- /* @__PURE__ */ jsxs35(
9130
- "div",
9131
- {
9132
- role: "status",
9133
- "data-slot": "bulk-action-bar-state",
9134
- style: {
9135
- display: "flex",
9136
- flex: "1 1 auto",
9137
- minWidth: 0,
9138
- alignItems: "center",
9139
- columnGap: "var(--space-3)"
9140
- },
9141
- children: [
9142
- countLabel,
9143
- summarySlot
9144
- ]
9145
- }
9146
- ),
9147
- /* @__PURE__ */ jsxs35(
9148
- "div",
9149
- {
9150
- "data-slot": "bulk-action-bar-controls",
9151
- style: {
9152
- display: "flex",
9153
- marginLeft: "auto",
9154
- alignItems: "center",
9155
- gap: "var(--space-1)"
9156
- },
9157
- children: [
9158
- count > 0 && actions.length > 0 ? /* @__PURE__ */ jsx39(Divider, {}) : null,
9159
- actionGroup,
9160
- /* @__PURE__ */ jsx39(Divider, {}),
9161
- /* @__PURE__ */ jsx39(ClearButton, { onClear })
9162
- ]
9163
- }
9164
- ),
9165
- trailingSlot
9166
- ]
9167
- }
9168
- );
9169
- }
9170
8932
  return /* @__PURE__ */ jsxs35(
9171
8933
  "div",
9172
8934
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olundot/ui",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "description": "OLUN. shared React UI components built on OLUN design tokens",
5
5
  "license": "MIT",
6
6
  "keywords": [