@dimaan/ui 0.0.24 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1628,6 +1628,17 @@ interface ListPageSelectFilter extends ListPageFilterBase {
1628
1628
  interface ListPageTextFilter extends ListPageFilterBase {
1629
1629
  type: 'text';
1630
1630
  placeholder?: string;
1631
+ /**
1632
+ * Debounce window in milliseconds so a request fires only after the user
1633
+ * pauses typing — instead of on every keystroke. The displayed value still
1634
+ * updates instantly; only `onFilterChange` is delayed. **Live mode only**
1635
+ * (ignored when `filterMode="manual"`, which already emits on Apply).
1636
+ *
1637
+ * Defaults to `400` — text filters are debounced out of the box, so you don't
1638
+ * need to pass anything. Set a different number to tune it, or `0` to opt out
1639
+ * and emit on every keystroke.
1640
+ */
1641
+ debounceMs?: number;
1631
1642
  }
1632
1643
  /** Date filter — rendered as a `DatePicker`. ISO `YYYY-MM-DD`; empty means "no filter". */
1633
1644
  interface ListPageDateFilter extends ListPageFilterBase {
@@ -1667,6 +1678,8 @@ interface ListPageEmptyState {
1667
1678
  interface ListPageLabels extends TableLabels {
1668
1679
  /** "Reset filters" button label. */
1669
1680
  reset?: string;
1681
+ /** "Apply" button label — shown in `manual` filter mode (the default). */
1682
+ apply?: string;
1670
1683
  /** "No results matching filters" title. */
1671
1684
  emptyTitle?: string;
1672
1685
  /** "No results matching filters" description. */
@@ -1703,8 +1716,19 @@ interface ListPageProps<T> {
1703
1716
  filters?: ListPageFilter[];
1704
1717
  /** Current filter selections, keyed by `filter.key` (date values are ISO `YYYY-MM-DD`). */
1705
1718
  filterValues?: Record<string, string>;
1706
- /** Fires when any filter control changes. */
1719
+ /** Fires when a filter changes (`live`) or when Apply is pressed (`manual`). */
1707
1720
  onFilterChange?: (key: string, value: string) => void;
1721
+ /**
1722
+ * How filter edits reach `onFilterChange`. Defaults to `'manual'`.
1723
+ * - `'manual'` (default) — edits are held locally; an **Apply** button (a real
1724
+ * form submit, so Enter also applies) flushes them in one go. Filtering only
1725
+ * fires on submit, so it never refetches on every keystroke/selection.
1726
+ * - `'live'` — control changes fire `onFilterChange` as they happen. Text
1727
+ * filters are **debounced by default (400ms)** so they don't refetch on
1728
+ * every keystroke; tune it per filter with `debounceMs`, or set `0` to emit
1729
+ * immediately. Selects / dates emit instantly.
1730
+ */
1731
+ filterMode?: 'live' | 'manual';
1708
1732
  enableRowSelection?: boolean;
1709
1733
  bulkActions?: (selected: T[]) => ReactNode;
1710
1734
  /** Current page state from the consumer's data layer. */
@@ -1780,7 +1804,7 @@ interface ListPageProps<T> {
1780
1804
  * );
1781
1805
  * ```
1782
1806
  */
1783
- declare function ListPage<T>({ title, description, bordered, actions, data, columns, getRowId, isLoading, loadingRowCount, filters, filterValues, onFilterChange, enableRowSelection, bulkActions, pagination, onPaginationChange, totalCount, pageSizeOptions, emptyState, noDataState, labels: labelsProp, className, }: ListPageProps<T>): react_jsx_runtime.JSX.Element;
1807
+ declare function ListPage<T>({ title, description, bordered, actions, data, columns, getRowId, isLoading, loadingRowCount, filters, filterValues, onFilterChange, filterMode, enableRowSelection, bulkActions, pagination, onPaginationChange, totalCount, pageSizeOptions, emptyState, noDataState, labels: labelsProp, className, }: ListPageProps<T>): react_jsx_runtime.JSX.Element;
1784
1808
 
1785
1809
  interface MultiSelectLabels {
1786
1810
  /** Search input placeholder. Direction-aware default: `"Search…"` / `"بحث…"`. */
package/dist/index.d.ts CHANGED
@@ -1628,6 +1628,17 @@ interface ListPageSelectFilter extends ListPageFilterBase {
1628
1628
  interface ListPageTextFilter extends ListPageFilterBase {
1629
1629
  type: 'text';
1630
1630
  placeholder?: string;
1631
+ /**
1632
+ * Debounce window in milliseconds so a request fires only after the user
1633
+ * pauses typing — instead of on every keystroke. The displayed value still
1634
+ * updates instantly; only `onFilterChange` is delayed. **Live mode only**
1635
+ * (ignored when `filterMode="manual"`, which already emits on Apply).
1636
+ *
1637
+ * Defaults to `400` — text filters are debounced out of the box, so you don't
1638
+ * need to pass anything. Set a different number to tune it, or `0` to opt out
1639
+ * and emit on every keystroke.
1640
+ */
1641
+ debounceMs?: number;
1631
1642
  }
1632
1643
  /** Date filter — rendered as a `DatePicker`. ISO `YYYY-MM-DD`; empty means "no filter". */
1633
1644
  interface ListPageDateFilter extends ListPageFilterBase {
@@ -1667,6 +1678,8 @@ interface ListPageEmptyState {
1667
1678
  interface ListPageLabels extends TableLabels {
1668
1679
  /** "Reset filters" button label. */
1669
1680
  reset?: string;
1681
+ /** "Apply" button label — shown in `manual` filter mode (the default). */
1682
+ apply?: string;
1670
1683
  /** "No results matching filters" title. */
1671
1684
  emptyTitle?: string;
1672
1685
  /** "No results matching filters" description. */
@@ -1703,8 +1716,19 @@ interface ListPageProps<T> {
1703
1716
  filters?: ListPageFilter[];
1704
1717
  /** Current filter selections, keyed by `filter.key` (date values are ISO `YYYY-MM-DD`). */
1705
1718
  filterValues?: Record<string, string>;
1706
- /** Fires when any filter control changes. */
1719
+ /** Fires when a filter changes (`live`) or when Apply is pressed (`manual`). */
1707
1720
  onFilterChange?: (key: string, value: string) => void;
1721
+ /**
1722
+ * How filter edits reach `onFilterChange`. Defaults to `'manual'`.
1723
+ * - `'manual'` (default) — edits are held locally; an **Apply** button (a real
1724
+ * form submit, so Enter also applies) flushes them in one go. Filtering only
1725
+ * fires on submit, so it never refetches on every keystroke/selection.
1726
+ * - `'live'` — control changes fire `onFilterChange` as they happen. Text
1727
+ * filters are **debounced by default (400ms)** so they don't refetch on
1728
+ * every keystroke; tune it per filter with `debounceMs`, or set `0` to emit
1729
+ * immediately. Selects / dates emit instantly.
1730
+ */
1731
+ filterMode?: 'live' | 'manual';
1708
1732
  enableRowSelection?: boolean;
1709
1733
  bulkActions?: (selected: T[]) => ReactNode;
1710
1734
  /** Current page state from the consumer's data layer. */
@@ -1780,7 +1804,7 @@ interface ListPageProps<T> {
1780
1804
  * );
1781
1805
  * ```
1782
1806
  */
1783
- declare function ListPage<T>({ title, description, bordered, actions, data, columns, getRowId, isLoading, loadingRowCount, filters, filterValues, onFilterChange, enableRowSelection, bulkActions, pagination, onPaginationChange, totalCount, pageSizeOptions, emptyState, noDataState, labels: labelsProp, className, }: ListPageProps<T>): react_jsx_runtime.JSX.Element;
1807
+ declare function ListPage<T>({ title, description, bordered, actions, data, columns, getRowId, isLoading, loadingRowCount, filters, filterValues, onFilterChange, filterMode, enableRowSelection, bulkActions, pagination, onPaginationChange, totalCount, pageSizeOptions, emptyState, noDataState, labels: labelsProp, className, }: ListPageProps<T>): react_jsx_runtime.JSX.Element;
1784
1808
 
1785
1809
  interface MultiSelectLabels {
1786
1810
  /** Search input placeholder. Direction-aware default: `"Search…"` / `"بحث…"`. */
package/dist/index.js CHANGED
@@ -2920,37 +2920,130 @@ function hasActiveFilters(filters, values) {
2920
2920
  }
2921
2921
  return false;
2922
2922
  }
2923
+ function DebouncedFilterInput({
2924
+ value,
2925
+ onChange,
2926
+ debounceMs,
2927
+ ariaLabel,
2928
+ placeholder,
2929
+ wrapperClassName,
2930
+ disabled
2931
+ }) {
2932
+ const [local, setLocal] = useState(value);
2933
+ const timerRef = useRef(null);
2934
+ const onChangeRef = useRef(onChange);
2935
+ onChangeRef.current = onChange;
2936
+ useEffect(() => {
2937
+ setLocal(value);
2938
+ if (timerRef.current) {
2939
+ clearTimeout(timerRef.current);
2940
+ timerRef.current = null;
2941
+ }
2942
+ }, [value]);
2943
+ useEffect(
2944
+ () => () => {
2945
+ if (timerRef.current) clearTimeout(timerRef.current);
2946
+ },
2947
+ []
2948
+ );
2949
+ const handleChange = (next) => {
2950
+ setLocal(next);
2951
+ if (timerRef.current) clearTimeout(timerRef.current);
2952
+ if (debounceMs <= 0) {
2953
+ onChangeRef.current(next);
2954
+ return;
2955
+ }
2956
+ timerRef.current = setTimeout(() => {
2957
+ timerRef.current = null;
2958
+ onChangeRef.current(next);
2959
+ }, debounceMs);
2960
+ };
2961
+ return /* @__PURE__ */ jsx(
2962
+ Input,
2963
+ {
2964
+ type: "search",
2965
+ "aria-label": ariaLabel,
2966
+ placeholder,
2967
+ value: local,
2968
+ onChange: (e) => handleChange(e.target.value),
2969
+ leadingIcon: /* @__PURE__ */ jsx(Search, { className: "size-4" }),
2970
+ wrapperClassName,
2971
+ disabled
2972
+ }
2973
+ );
2974
+ }
2975
+ var DEFAULT_TEXT_DEBOUNCE_MS = 400;
2923
2976
  function ListPageFilterBar({
2924
2977
  filters,
2925
2978
  values,
2926
2979
  onChange,
2927
2980
  disabled = false,
2981
+ mode = "live",
2928
2982
  labels
2929
2983
  }) {
2984
+ const manual = mode === "manual";
2930
2985
  const active = hasActiveFilters(filters, values);
2986
+ const appliedKey = JSON.stringify(values ?? {});
2987
+ const [draft, setDraft] = useState(values ?? {});
2988
+ useEffect(() => {
2989
+ if (manual) setDraft(values ?? {});
2990
+ }, [appliedKey, manual]);
2991
+ const effectiveValues = manual ? draft : values ?? {};
2992
+ const handleChange = (key, value) => {
2993
+ if (manual) {
2994
+ setDraft((prev) => ({ ...prev, [key]: value }));
2995
+ } else {
2996
+ onChange?.(key, value);
2997
+ }
2998
+ };
2999
+ const dirty = manual && (filters ?? []).some((filter) => {
3000
+ const next = draft[filter.key] ?? filterDefaultValue(filter);
3001
+ const current = values?.[filter.key] ?? filterDefaultValue(filter);
3002
+ return next !== current;
3003
+ });
3004
+ const apply = (event) => {
3005
+ event.preventDefault();
3006
+ for (const filter of filters ?? []) {
3007
+ const next = draft[filter.key] ?? filterDefaultValue(filter);
3008
+ const current = values?.[filter.key] ?? filterDefaultValue(filter);
3009
+ if (next !== current) onChange?.(filter.key, next);
3010
+ }
3011
+ };
2931
3012
  const reset = () => {
2932
3013
  for (const filter of filters ?? []) {
2933
3014
  onChange?.(filter.key, filterDefaultValue(filter));
2934
3015
  }
2935
3016
  };
3017
+ const controls = /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: filters?.map((filter) => /* @__PURE__ */ jsx(
3018
+ FilterControl,
3019
+ {
3020
+ filter,
3021
+ value: effectiveValues[filter.key],
3022
+ onChange: handleChange,
3023
+ disabled,
3024
+ mode
3025
+ },
3026
+ filter.key
3027
+ )) });
3028
+ const resetButton = active && !disabled ? /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: reset, children: [
3029
+ /* @__PURE__ */ jsx(RefreshCw, { className: "size-4" }),
3030
+ labels.reset
3031
+ ] }) : null;
3032
+ if (manual) {
3033
+ return /* @__PURE__ */ jsxs("form", { "data-slot": "list-page-filter-bar", className: "space-y-3", onSubmit: apply, children: [
3034
+ controls,
3035
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
3036
+ resetButton,
3037
+ /* @__PURE__ */ jsx(Button, { type: "submit", size: "sm", disabled: disabled || !dirty, children: labels.apply ?? "Apply" })
3038
+ ] })
3039
+ ] });
3040
+ }
2936
3041
  return /* @__PURE__ */ jsxs("div", { "data-slot": "list-page-filter-bar", className: "space-y-3", children: [
2937
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: filters?.map((filter) => /* @__PURE__ */ jsx(
2938
- FilterControl,
2939
- {
2940
- filter,
2941
- value: values?.[filter.key],
2942
- onChange,
2943
- disabled
2944
- },
2945
- filter.key
2946
- )) }),
2947
- active && !disabled ? /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: reset, children: [
2948
- /* @__PURE__ */ jsx(RefreshCw, { className: "size-4" }),
2949
- labels.reset
2950
- ] }) }) : null
3042
+ controls,
3043
+ resetButton ? /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: resetButton }) : null
2951
3044
  ] });
2952
3045
  }
2953
- function FilterControl({ filter, value, onChange, disabled }) {
3046
+ function FilterControl({ filter, value, onChange, disabled, mode }) {
2954
3047
  const spanClass = FILTER_SPAN_CLASS[filter.width ?? "default"];
2955
3048
  const ariaLabel = typeof filter.label === "string" ? filter.label : filter.key;
2956
3049
  switch (filter.type) {
@@ -2968,14 +3061,13 @@ function FilterControl({ filter, value, onChange, disabled }) {
2968
3061
  );
2969
3062
  case "text":
2970
3063
  return /* @__PURE__ */ jsx(
2971
- Input,
3064
+ DebouncedFilterInput,
2972
3065
  {
2973
- type: "search",
2974
- "aria-label": ariaLabel,
2975
- placeholder: filter.placeholder,
2976
3066
  value: value ?? "",
2977
- onChange: (e) => onChange?.(filter.key, e.target.value),
2978
- leadingIcon: /* @__PURE__ */ jsx(Search, { className: "size-4" }),
3067
+ onChange: (v) => onChange?.(filter.key, v),
3068
+ debounceMs: mode === "live" ? filter.debounceMs ?? DEFAULT_TEXT_DEBOUNCE_MS : 0,
3069
+ ariaLabel,
3070
+ placeholder: filter.placeholder,
2979
3071
  wrapperClassName: spanClass,
2980
3072
  disabled
2981
3073
  }
@@ -3007,13 +3099,22 @@ function FilterControl({ filter, value, onChange, disabled }) {
3007
3099
  );
3008
3100
  }
3009
3101
  }
3010
- var DEFAULT_LABELS = {
3102
+ var EN_LABELS2 = {
3011
3103
  reset: "Reset filters",
3104
+ apply: "Apply",
3012
3105
  emptyTitle: "No results",
3013
3106
  emptyDescription: "Try clearing the search or adjusting the filters.",
3014
3107
  noDataTitle: "No data yet",
3015
3108
  noDataDescription: "Nothing has been added here so far."
3016
3109
  };
3110
+ var AR_LABELS2 = {
3111
+ reset: "\u0625\u0639\u0627\u062F\u0629 \u062A\u0639\u064A\u064A\u0646 \u0627\u0644\u0641\u0644\u0627\u062A\u0631",
3112
+ apply: "\u062A\u0637\u0628\u064A\u0642",
3113
+ emptyTitle: "\u0644\u0627 \u062A\u0648\u062C\u062F \u0646\u062A\u0627\u0626\u062C",
3114
+ emptyDescription: "\u062C\u0631\u0651\u0628 \u0645\u0633\u062D \u0627\u0644\u0628\u062D\u062B \u0623\u0648 \u062A\u0639\u062F\u064A\u0644 \u0627\u0644\u0641\u0644\u0627\u062A\u0631.",
3115
+ noDataTitle: "\u0644\u0627 \u062A\u0648\u062C\u062F \u0628\u064A\u0627\u0646\u0627\u062A \u0628\u0639\u062F",
3116
+ noDataDescription: "\u0644\u0645 \u062A\u062A\u0645 \u0625\u0636\u0627\u0641\u0629 \u0623\u064A \u0634\u064A\u0621 \u0647\u0646\u0627 \u062D\u062A\u0649 \u0627\u0644\u0622\u0646."
3117
+ };
3017
3118
  function ListPage({
3018
3119
  title,
3019
3120
  description,
@@ -3027,6 +3128,7 @@ function ListPage({
3027
3128
  filters,
3028
3129
  filterValues,
3029
3130
  onFilterChange,
3131
+ filterMode = "manual",
3030
3132
  enableRowSelection,
3031
3133
  bulkActions,
3032
3134
  pagination,
@@ -3038,7 +3140,8 @@ function ListPage({
3038
3140
  labels: labelsProp,
3039
3141
  className
3040
3142
  }) {
3041
- const labels = { ...DEFAULT_LABELS, ...labelsProp };
3143
+ const dir = useDirection();
3144
+ const labels = { ...dir === "rtl" ? AR_LABELS2 : EN_LABELS2, ...labelsProp };
3042
3145
  const showFilterBar = Boolean(filters?.length);
3043
3146
  const hasActiveQuery = useMemo(
3044
3147
  () => hasActiveFilters(filters, filterValues),
@@ -3058,8 +3161,8 @@ function ListPage({
3058
3161
  filters,
3059
3162
  values: filterValues,
3060
3163
  onChange: onFilterChange,
3061
- disabled: isLoading,
3062
- labels: { reset: labels.reset }
3164
+ mode: filterMode,
3165
+ labels: { reset: labels.reset, apply: labels.apply }
3063
3166
  }
3064
3167
  ) : null,
3065
3168
  tableMode === "loading" || tableMode === "rows" ? /* @__PURE__ */ jsx(