@dimaan/ui 0.0.23 → 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/README.md CHANGED
@@ -4,6 +4,7 @@ Shared React UI component library for Diman company projects.
4
4
 
5
5
  Built with **React 19**, **TypeScript**, **Tailwind CSS v4**, and a CSS-first design token system. Distributed as dual ESM/CJS with full type definitions, intended for Vite-based dashboard apps.
6
6
 
7
+
7
8
  ## Documentation
8
9
 
9
10
  - **[USAGE.md](USAGE.md)** — full consumer guide: install, Tailwind setup, framework-specific examples (Next.js / Vite / Remix), theming, and pitfalls.
package/dist/index.cjs CHANGED
@@ -136,7 +136,7 @@ var buttonSizeClass = {
136
136
  icon: "h-9 w-9 shrink-0 rounded-md p-0",
137
137
  "icon-sm": "h-8 w-8 shrink-0 rounded-md p-0"
138
138
  };
139
- var buttonBaseClass = "group/button relative inline-flex items-center justify-center font-medium select-none whitespace-nowrap outline-none transition-[background-color,color,box-shadow,opacity] focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0";
139
+ var buttonBaseClass = "group/button relative inline-flex items-center justify-center font-medium select-none whitespace-nowrap outline-none transition-[background-color,color,box-shadow,opacity,transform] active:scale-[0.98] focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 motion-reduce:transition-none motion-reduce:active:scale-100 [&_svg]:pointer-events-none [&_svg]:shrink-0";
140
140
  var Button = react.forwardRef(function Button2({
141
141
  variant = "primary",
142
142
  size = "md",
@@ -2948,37 +2948,130 @@ function hasActiveFilters(filters, values) {
2948
2948
  }
2949
2949
  return false;
2950
2950
  }
2951
+ function DebouncedFilterInput({
2952
+ value,
2953
+ onChange,
2954
+ debounceMs,
2955
+ ariaLabel,
2956
+ placeholder,
2957
+ wrapperClassName,
2958
+ disabled
2959
+ }) {
2960
+ const [local, setLocal] = react.useState(value);
2961
+ const timerRef = react.useRef(null);
2962
+ const onChangeRef = react.useRef(onChange);
2963
+ onChangeRef.current = onChange;
2964
+ react.useEffect(() => {
2965
+ setLocal(value);
2966
+ if (timerRef.current) {
2967
+ clearTimeout(timerRef.current);
2968
+ timerRef.current = null;
2969
+ }
2970
+ }, [value]);
2971
+ react.useEffect(
2972
+ () => () => {
2973
+ if (timerRef.current) clearTimeout(timerRef.current);
2974
+ },
2975
+ []
2976
+ );
2977
+ const handleChange = (next) => {
2978
+ setLocal(next);
2979
+ if (timerRef.current) clearTimeout(timerRef.current);
2980
+ if (debounceMs <= 0) {
2981
+ onChangeRef.current(next);
2982
+ return;
2983
+ }
2984
+ timerRef.current = setTimeout(() => {
2985
+ timerRef.current = null;
2986
+ onChangeRef.current(next);
2987
+ }, debounceMs);
2988
+ };
2989
+ return /* @__PURE__ */ jsxRuntime.jsx(
2990
+ Input,
2991
+ {
2992
+ type: "search",
2993
+ "aria-label": ariaLabel,
2994
+ placeholder,
2995
+ value: local,
2996
+ onChange: (e) => handleChange(e.target.value),
2997
+ leadingIcon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { className: "size-4" }),
2998
+ wrapperClassName,
2999
+ disabled
3000
+ }
3001
+ );
3002
+ }
3003
+ var DEFAULT_TEXT_DEBOUNCE_MS = 400;
2951
3004
  function ListPageFilterBar({
2952
3005
  filters,
2953
3006
  values,
2954
3007
  onChange,
2955
3008
  disabled = false,
3009
+ mode = "live",
2956
3010
  labels
2957
3011
  }) {
3012
+ const manual = mode === "manual";
2958
3013
  const active = hasActiveFilters(filters, values);
3014
+ const appliedKey = JSON.stringify(values ?? {});
3015
+ const [draft, setDraft] = react.useState(values ?? {});
3016
+ react.useEffect(() => {
3017
+ if (manual) setDraft(values ?? {});
3018
+ }, [appliedKey, manual]);
3019
+ const effectiveValues = manual ? draft : values ?? {};
3020
+ const handleChange = (key, value) => {
3021
+ if (manual) {
3022
+ setDraft((prev) => ({ ...prev, [key]: value }));
3023
+ } else {
3024
+ onChange?.(key, value);
3025
+ }
3026
+ };
3027
+ const dirty = manual && (filters ?? []).some((filter) => {
3028
+ const next = draft[filter.key] ?? filterDefaultValue(filter);
3029
+ const current = values?.[filter.key] ?? filterDefaultValue(filter);
3030
+ return next !== current;
3031
+ });
3032
+ const apply = (event) => {
3033
+ event.preventDefault();
3034
+ for (const filter of filters ?? []) {
3035
+ const next = draft[filter.key] ?? filterDefaultValue(filter);
3036
+ const current = values?.[filter.key] ?? filterDefaultValue(filter);
3037
+ if (next !== current) onChange?.(filter.key, next);
3038
+ }
3039
+ };
2959
3040
  const reset = () => {
2960
3041
  for (const filter of filters ?? []) {
2961
3042
  onChange?.(filter.key, filterDefaultValue(filter));
2962
3043
  }
2963
3044
  };
3045
+ const controls = /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: filters?.map((filter) => /* @__PURE__ */ jsxRuntime.jsx(
3046
+ FilterControl,
3047
+ {
3048
+ filter,
3049
+ value: effectiveValues[filter.key],
3050
+ onChange: handleChange,
3051
+ disabled,
3052
+ mode
3053
+ },
3054
+ filter.key
3055
+ )) });
3056
+ const resetButton = active && !disabled ? /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "ghost", size: "sm", onClick: reset, children: [
3057
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: "size-4" }),
3058
+ labels.reset
3059
+ ] }) : null;
3060
+ if (manual) {
3061
+ return /* @__PURE__ */ jsxRuntime.jsxs("form", { "data-slot": "list-page-filter-bar", className: "space-y-3", onSubmit: apply, children: [
3062
+ controls,
3063
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-2", children: [
3064
+ resetButton,
3065
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { type: "submit", size: "sm", disabled: disabled || !dirty, children: labels.apply ?? "Apply" })
3066
+ ] })
3067
+ ] });
3068
+ }
2964
3069
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-slot": "list-page-filter-bar", className: "space-y-3", children: [
2965
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: filters?.map((filter) => /* @__PURE__ */ jsxRuntime.jsx(
2966
- FilterControl,
2967
- {
2968
- filter,
2969
- value: values?.[filter.key],
2970
- onChange,
2971
- disabled
2972
- },
2973
- filter.key
2974
- )) }),
2975
- active && !disabled ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsxs(Button, { variant: "ghost", size: "sm", onClick: reset, children: [
2976
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: "size-4" }),
2977
- labels.reset
2978
- ] }) }) : null
3070
+ controls,
3071
+ resetButton ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: resetButton }) : null
2979
3072
  ] });
2980
3073
  }
2981
- function FilterControl({ filter, value, onChange, disabled }) {
3074
+ function FilterControl({ filter, value, onChange, disabled, mode }) {
2982
3075
  const spanClass = FILTER_SPAN_CLASS[filter.width ?? "default"];
2983
3076
  const ariaLabel = typeof filter.label === "string" ? filter.label : filter.key;
2984
3077
  switch (filter.type) {
@@ -2996,14 +3089,13 @@ function FilterControl({ filter, value, onChange, disabled }) {
2996
3089
  );
2997
3090
  case "text":
2998
3091
  return /* @__PURE__ */ jsxRuntime.jsx(
2999
- Input,
3092
+ DebouncedFilterInput,
3000
3093
  {
3001
- type: "search",
3002
- "aria-label": ariaLabel,
3003
- placeholder: filter.placeholder,
3004
3094
  value: value ?? "",
3005
- onChange: (e) => onChange?.(filter.key, e.target.value),
3006
- leadingIcon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { className: "size-4" }),
3095
+ onChange: (v) => onChange?.(filter.key, v),
3096
+ debounceMs: mode === "live" ? filter.debounceMs ?? DEFAULT_TEXT_DEBOUNCE_MS : 0,
3097
+ ariaLabel,
3098
+ placeholder: filter.placeholder,
3007
3099
  wrapperClassName: spanClass,
3008
3100
  disabled
3009
3101
  }
@@ -3035,13 +3127,22 @@ function FilterControl({ filter, value, onChange, disabled }) {
3035
3127
  );
3036
3128
  }
3037
3129
  }
3038
- var DEFAULT_LABELS = {
3130
+ var EN_LABELS2 = {
3039
3131
  reset: "Reset filters",
3132
+ apply: "Apply",
3040
3133
  emptyTitle: "No results",
3041
3134
  emptyDescription: "Try clearing the search or adjusting the filters.",
3042
3135
  noDataTitle: "No data yet",
3043
3136
  noDataDescription: "Nothing has been added here so far."
3044
3137
  };
3138
+ var AR_LABELS2 = {
3139
+ reset: "\u0625\u0639\u0627\u062F\u0629 \u062A\u0639\u064A\u064A\u0646 \u0627\u0644\u0641\u0644\u0627\u062A\u0631",
3140
+ apply: "\u062A\u0637\u0628\u064A\u0642",
3141
+ emptyTitle: "\u0644\u0627 \u062A\u0648\u062C\u062F \u0646\u062A\u0627\u0626\u062C",
3142
+ 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.",
3143
+ noDataTitle: "\u0644\u0627 \u062A\u0648\u062C\u062F \u0628\u064A\u0627\u0646\u0627\u062A \u0628\u0639\u062F",
3144
+ 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."
3145
+ };
3045
3146
  function ListPage({
3046
3147
  title,
3047
3148
  description,
@@ -3055,6 +3156,7 @@ function ListPage({
3055
3156
  filters,
3056
3157
  filterValues,
3057
3158
  onFilterChange,
3159
+ filterMode = "manual",
3058
3160
  enableRowSelection,
3059
3161
  bulkActions,
3060
3162
  pagination,
@@ -3066,7 +3168,8 @@ function ListPage({
3066
3168
  labels: labelsProp,
3067
3169
  className
3068
3170
  }) {
3069
- const labels = { ...DEFAULT_LABELS, ...labelsProp };
3171
+ const dir = useDirection();
3172
+ const labels = { ...dir === "rtl" ? AR_LABELS2 : EN_LABELS2, ...labelsProp };
3070
3173
  const showFilterBar = Boolean(filters?.length);
3071
3174
  const hasActiveQuery = react.useMemo(
3072
3175
  () => hasActiveFilters(filters, filterValues),
@@ -3086,8 +3189,8 @@ function ListPage({
3086
3189
  filters,
3087
3190
  values: filterValues,
3088
3191
  onChange: onFilterChange,
3089
- disabled: isLoading,
3090
- labels: { reset: labels.reset }
3192
+ mode: filterMode,
3193
+ labels: { reset: labels.reset, apply: labels.apply }
3091
3194
  }
3092
3195
  ) : null,
3093
3196
  tableMode === "loading" || tableMode === "rows" ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -3273,6 +3376,57 @@ var RadioGroupItem = react.forwardRef(function RadioGroupItem2({ className, radi
3273
3376
  );
3274
3377
  });
3275
3378
 
3379
+ // src/components/row-actions/rowActionsVariants.ts
3380
+ var rowActionsBaseClass = "inline-flex items-center gap-1";
3381
+ var rowActionsDestructiveClass = "text-destructive hover:bg-destructive/10 hover:text-destructive focus-visible:ring-destructive/40";
3382
+ var PRESETS = {
3383
+ view: { icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, {}), label: "View", destructive: false },
3384
+ edit: { icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, {}), label: "Edit", destructive: false },
3385
+ delete: { icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, {}), label: "Delete", destructive: true }
3386
+ };
3387
+ function isPreset(action) {
3388
+ return "kind" in action;
3389
+ }
3390
+ function resolveAction(action) {
3391
+ if (isPreset(action)) {
3392
+ const preset = PRESETS[action.kind];
3393
+ return {
3394
+ icon: preset.icon,
3395
+ label: action.label ?? preset.label,
3396
+ destructive: preset.destructive,
3397
+ onClick: action.onClick,
3398
+ disabled: action.disabled ?? false
3399
+ };
3400
+ }
3401
+ return {
3402
+ icon: action.icon,
3403
+ label: action.label,
3404
+ destructive: action.variant === "destructive",
3405
+ onClick: action.onClick,
3406
+ disabled: action.disabled ?? false
3407
+ };
3408
+ }
3409
+ function RowActions({ actions, size = "icon-sm", className }) {
3410
+ if (actions.length === 0) return null;
3411
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn(rowActionsBaseClass, className), children: actions.map((action, index) => {
3412
+ const resolved = resolveAction(action);
3413
+ return /* @__PURE__ */ jsxRuntime.jsx(
3414
+ Button,
3415
+ {
3416
+ type: "button",
3417
+ variant: "ghost",
3418
+ size,
3419
+ "aria-label": resolved.label,
3420
+ disabled: resolved.disabled,
3421
+ onClick: resolved.onClick,
3422
+ className: resolved.destructive ? rowActionsDestructiveClass : void 0,
3423
+ children: resolved.icon
3424
+ },
3425
+ index
3426
+ );
3427
+ }) });
3428
+ }
3429
+
3276
3430
  // src/components/switch/switchVariants.ts
3277
3431
  var switchTrackClass = {
3278
3432
  sm: "h-4 w-7",
@@ -3571,6 +3725,7 @@ exports.MultiSelect = MultiSelect;
3571
3725
  exports.PageHeader = PageHeader;
3572
3726
  exports.RadioGroup = RadioGroup;
3573
3727
  exports.RadioGroupItem = RadioGroupItem;
3728
+ exports.RowActions = RowActions;
3574
3729
  exports.Select = Select;
3575
3730
  exports.Sidebar = Sidebar;
3576
3731
  exports.SidebarFooter = SidebarFooter;
@@ -3684,6 +3839,8 @@ exports.radioItemBaseClass = radioItemBaseClass;
3684
3839
  exports.radioItemSizeClass = radioItemSizeClass;
3685
3840
  exports.radioLabelSizeClass = radioLabelSizeClass;
3686
3841
  exports.radioOptionRowClass = radioOptionRowClass;
3842
+ exports.rowActionsBaseClass = rowActionsBaseClass;
3843
+ exports.rowActionsDestructiveClass = rowActionsDestructiveClass;
3687
3844
  exports.selectBaseClass = selectBaseClass;
3688
3845
  exports.selectSizeClass = selectSizeClass;
3689
3846
  exports.selectVariantClass = selectVariantClass;