@martinsura/ui 0.1.1 → 0.1.3

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.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { cva } from 'class-variance-authority';
2
2
  import { twMerge } from 'tailwind-merge';
3
- import { IconDownload, IconPaperclip, IconFileSpreadsheet, IconX, IconTrashX, IconPlus, IconPencil, IconDotsVertical, IconEyeOff, IconEye, IconCheck, IconDroplet, IconDatabaseOff, IconChevronUp, IconChevronDown, IconSelector, IconCalendar, IconUsers, IconInfoCircle, IconAlertTriangle, IconCircleX, IconBold, IconItalic, IconUnderline, IconList, IconListNumbers, IconLink, IconUnlink, IconClearFormatting, IconUpload, IconAlertCircle, IconTrash, IconLoader2, IconChevronLeft, IconChevronRight, IconSearch } from '@tabler/icons-react';
3
+ import { IconEyeOff, IconEye, IconDownload, IconPaperclip, IconFileSpreadsheet, IconX, IconTrashX, IconPlus, IconPencil, IconDotsVertical, IconCheck, IconDroplet, IconDatabaseOff, IconChevronUp, IconChevronDown, IconSelector, IconCalendar, IconUsers, IconInfoCircle, IconAlertTriangle, IconCircleX, IconBold, IconItalic, IconUnderline, IconList, IconListNumbers, IconLink, IconUnlink, IconClearFormatting, IconUpload, IconAlertCircle, IconTrash, IconLoader2, IconChevronLeft, IconChevronRight, IconSearch } from '@tabler/icons-react';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
- import { createContext, useRef, useState, useLayoutEffect, useEffect, useMemo, useContext, useCallback, useId } from 'react';
5
+ import { createContext, forwardRef, useState, useContext, useRef, useLayoutEffect, useEffect, useMemo, useCallback, useId } from 'react';
6
6
  import { createPortal } from 'react-dom';
7
7
  import dayjs from 'dayjs';
8
8
 
@@ -411,12 +411,12 @@ var ErrorContext = createContext({
411
411
  var ErrorProvider = ({ resolveInputError, resolveServerError, children }) => /* @__PURE__ */ jsx(ErrorContext.Provider, { value: { resolveInputError, resolveServerError }, children });
412
412
  var useErrorResolver = () => useContext(ErrorContext).resolveInputError;
413
413
  var useServerError = () => useContext(ErrorContext).resolveServerError;
414
- var TextInput = ({
414
+ var TextInput = forwardRef(({
415
415
  size = "middle",
416
416
  password = false,
417
417
  newPassword = false,
418
418
  ...props
419
- }) => {
419
+ }, ref) => {
420
420
  const [showPassword, setShowPassword] = useState(false);
421
421
  const resolveError = useErrorResolver();
422
422
  const resolvedErrors = props.errorName ? resolveError(props.errorName) : [];
@@ -437,6 +437,7 @@ var TextInput = ({
437
437
  /* @__PURE__ */ jsx(
438
438
  "input",
439
439
  {
440
+ ref,
440
441
  type: inputType,
441
442
  autoComplete: newPassword ? "new-password" : void 0,
442
443
  placeholder: props.placeholder,
@@ -470,7 +471,8 @@ var TextInput = ({
470
471
  ] }),
471
472
  errorDisplay && /* @__PURE__ */ jsx(InputError, { error: String(errorDisplay), className: props.classNames?.error })
472
473
  ] });
473
- };
474
+ });
475
+ TextInput.displayName = "TextInput";
474
476
  var numberInputClass = inputBaseClass + " [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none";
475
477
  var NumberInput = ({
476
478
  size = "middle",
@@ -642,9 +644,28 @@ var SwitchInput = ({
642
644
  errorDisplay && /* @__PURE__ */ jsx(InputError, { error: String(errorDisplay), className: props.classNames?.error })
643
645
  ] });
644
646
  };
647
+
648
+ // src/floating/layerStack.ts
649
+ var FLOATING_ROOT_SELECTOR = "[data-ui-floating-root]";
650
+ var LAYER_ROOT_SELECTOR = "[data-ui-layer-root]";
651
+ function getTopmostElement(selector) {
652
+ const elements = document.querySelectorAll(selector);
653
+ return elements.length > 0 ? elements[elements.length - 1] : null;
654
+ }
655
+ function hasFloatingRootOpen() {
656
+ return getTopmostElement(FLOATING_ROOT_SELECTOR) !== null;
657
+ }
658
+ function isTopmostFloatingRoot(element) {
659
+ return element !== null && getTopmostElement(FLOATING_ROOT_SELECTOR) === element;
660
+ }
661
+ function isTopmostLayerRoot(element) {
662
+ return element !== null && getTopmostElement(LAYER_ROOT_SELECTOR) === element;
663
+ }
664
+ function isTargetInsideFloatingRoot(target) {
665
+ return target instanceof Element && target.closest(FLOATING_ROOT_SELECTOR) !== null;
666
+ }
645
667
  var GAP = 6;
646
668
  var VIEWPORT_MARGIN = 8;
647
- var FLOATING_ROOT_SELECTOR = "[data-ui-floating-root]";
648
669
  function calcPosition(triggerRect, panelRect, placement) {
649
670
  const spaceBelow = window.innerHeight - triggerRect.bottom - VIEWPORT_MARGIN;
650
671
  const spaceAbove = triggerRect.top - VIEWPORT_MARGIN;
@@ -751,7 +772,7 @@ var Dropdown = ({
751
772
  }
752
773
  };
753
774
  const handleKeyDown = (e) => {
754
- if (e.key === "Escape") {
775
+ if (e.key === "Escape" && isTopmostFloatingRoot(popupRef.current)) {
755
776
  setOpen(false);
756
777
  }
757
778
  };
@@ -1915,6 +1936,19 @@ var SelectDropdown = (props) => {
1915
1936
  useEffect(() => {
1916
1937
  if (props.isOpen) {
1917
1938
  document.addEventListener("mousedown", handleMouseDown);
1939
+ const handleKeyDown = (e) => {
1940
+ if (e.key === "Escape" && isTopmostFloatingRoot(popupRef.current)) {
1941
+ e.preventDefault();
1942
+ e.stopPropagation();
1943
+ e.stopImmediatePropagation();
1944
+ props.onClose();
1945
+ }
1946
+ };
1947
+ document.addEventListener("keydown", handleKeyDown);
1948
+ return () => {
1949
+ document.removeEventListener("mousedown", handleMouseDown);
1950
+ document.removeEventListener("keydown", handleKeyDown);
1951
+ };
1918
1952
  }
1919
1953
  return () => document.removeEventListener("mousedown", handleMouseDown);
1920
1954
  }, [props.isOpen, handleMouseDown]);
@@ -2264,6 +2298,19 @@ var CalendarPopup = (props) => {
2264
2298
  useEffect(() => {
2265
2299
  if (props.isOpen) {
2266
2300
  document.addEventListener("mousedown", handleMouseDown);
2301
+ const handleKeyDown = (e) => {
2302
+ if (e.key === "Escape" && isTopmostFloatingRoot(popupRef.current)) {
2303
+ e.preventDefault();
2304
+ e.stopPropagation();
2305
+ e.stopImmediatePropagation();
2306
+ props.onClose();
2307
+ }
2308
+ };
2309
+ document.addEventListener("keydown", handleKeyDown);
2310
+ return () => {
2311
+ document.removeEventListener("mousedown", handleMouseDown);
2312
+ document.removeEventListener("keydown", handleKeyDown);
2313
+ };
2267
2314
  }
2268
2315
  return () => document.removeEventListener("mousedown", handleMouseDown);
2269
2316
  }, [props.isOpen, handleMouseDown]);
@@ -2316,7 +2363,7 @@ var CalendarPopup = (props) => {
2316
2363
  ref: popupRef,
2317
2364
  "data-ui-floating-root": "",
2318
2365
  style: { top: pos.top, left: pos.left },
2319
- className: "fixed z-1000 bg-white border border-(--ui-border) rounded-(--ui-radius-lg) shadow-lg w-68",
2366
+ className: "fixed z-1003 bg-white border border-(--ui-border) rounded-(--ui-radius-lg) shadow-lg w-68",
2320
2367
  children: [
2321
2368
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 py-2 border-b border-(--ui-border)", children: [
2322
2369
  /* @__PURE__ */ jsx(
@@ -2815,6 +2862,7 @@ var Drawer = ({ placement = "top", ...props }) => {
2815
2862
  const [footer, setFooter] = useState(null);
2816
2863
  const [loading, setLoading] = useState(false);
2817
2864
  const frameRef = useRef(0);
2865
+ const panelRef = useRef(null);
2818
2866
  useEffect(() => {
2819
2867
  if (props.isOpen) {
2820
2868
  setRendered(true);
@@ -2833,9 +2881,19 @@ var Drawer = ({ placement = "top", ...props }) => {
2833
2881
  return;
2834
2882
  }
2835
2883
  const handleKeyDown = (e) => {
2836
- if (e.key === "Escape") {
2837
- props.onClose(false);
2884
+ if (e.key !== "Escape" || e.defaultPrevented) {
2885
+ return;
2886
+ }
2887
+ if (hasFloatingRootOpen()) {
2888
+ return;
2889
+ }
2890
+ if (isTargetInsideFloatingRoot(e.target)) {
2891
+ return;
2892
+ }
2893
+ if (!isTopmostLayerRoot(panelRef.current)) {
2894
+ return;
2838
2895
  }
2896
+ props.onClose(false);
2839
2897
  };
2840
2898
  document.body.style.overflow = "hidden";
2841
2899
  document.addEventListener("keydown", handleKeyDown);
@@ -2872,6 +2930,8 @@ var Drawer = ({ placement = "top", ...props }) => {
2872
2930
  /* @__PURE__ */ jsxs(
2873
2931
  "div",
2874
2932
  {
2933
+ ref: panelRef,
2934
+ "data-ui-layer-root": "",
2875
2935
  className: twMerge(
2876
2936
  panelBase[placement],
2877
2937
  "bg-white shadow-xl transition-transform duration-220 ease-out",
@@ -2983,6 +3043,256 @@ var DrawerContent = ({ loading = false, children }) => {
2983
3043
  }
2984
3044
  return /* @__PURE__ */ jsx(Fragment, { children });
2985
3045
  };
3046
+ var ModalContext = createContext(null);
3047
+ function useModal() {
3048
+ const ctx = useContext(ModalContext);
3049
+ if (!ctx) {
3050
+ throw new Error("useModal must be used inside Modal");
3051
+ }
3052
+ return ctx;
3053
+ }
3054
+ var TRANSITION_MS2 = 220;
3055
+ var sizeClass = {
3056
+ small: "w-full max-w-lg",
3057
+ middle: "w-full max-w-3xl",
3058
+ large: "w-full max-w-5xl",
3059
+ auto: "w-auto"
3060
+ };
3061
+ var Modal = ({
3062
+ placement = "center",
3063
+ size = "middle",
3064
+ width,
3065
+ maxWidth,
3066
+ closeOnEscape = true,
3067
+ closeOnOverlayClick = true,
3068
+ showCloseButton = true,
3069
+ hideHeader = false,
3070
+ destroyOnClose = true,
3071
+ preventClose = false,
3072
+ beforeClose,
3073
+ initialFocusRef,
3074
+ ...props
3075
+ }) => {
3076
+ const [rendered, setRendered] = useState(props.isOpen);
3077
+ const [visible, setVisible] = useState(false);
3078
+ const [title, setTitle] = useState("");
3079
+ const [footer, setFooter] = useState(null);
3080
+ const [loading, setLoading] = useState(false);
3081
+ const frameRef = useRef(0);
3082
+ const panelRef = useRef(null);
3083
+ useEffect(() => {
3084
+ if (props.isOpen) {
3085
+ setRendered(true);
3086
+ frameRef.current = requestAnimationFrame(() => {
3087
+ frameRef.current = requestAnimationFrame(() => setVisible(true));
3088
+ });
3089
+ } else {
3090
+ setVisible(false);
3091
+ if (!destroyOnClose) {
3092
+ return () => cancelAnimationFrame(frameRef.current);
3093
+ }
3094
+ const t = setTimeout(() => setRendered(false), TRANSITION_MS2);
3095
+ return () => clearTimeout(t);
3096
+ }
3097
+ return () => cancelAnimationFrame(frameRef.current);
3098
+ }, [destroyOnClose, props.isOpen]);
3099
+ useEffect(() => {
3100
+ if (!props.isOpen) {
3101
+ return;
3102
+ }
3103
+ const focusTarget = initialFocusRef?.current;
3104
+ if (!focusTarget) {
3105
+ return;
3106
+ }
3107
+ const timeout = window.setTimeout(() => focusTarget.focus(), TRANSITION_MS2 / 2);
3108
+ return () => window.clearTimeout(timeout);
3109
+ }, [initialFocusRef, props.isOpen]);
3110
+ useEffect(() => {
3111
+ if (!props.isOpen) {
3112
+ return;
3113
+ }
3114
+ const handleKeyDown = (e) => {
3115
+ if (!closeOnEscape || e.key !== "Escape" || e.defaultPrevented) {
3116
+ return;
3117
+ }
3118
+ if (hasFloatingRootOpen() || isTargetInsideFloatingRoot(e.target)) {
3119
+ return;
3120
+ }
3121
+ if (!isTopmostLayerRoot(panelRef.current)) {
3122
+ return;
3123
+ }
3124
+ void requestClose();
3125
+ };
3126
+ document.body.style.overflow = "hidden";
3127
+ document.addEventListener("keydown", handleKeyDown);
3128
+ return () => {
3129
+ document.body.style.overflow = "";
3130
+ document.removeEventListener("keydown", handleKeyDown);
3131
+ };
3132
+ }, [closeOnEscape, props.isOpen]);
3133
+ if (!rendered) {
3134
+ return null;
3135
+ }
3136
+ const requestClose = async () => {
3137
+ if (preventClose) {
3138
+ return;
3139
+ }
3140
+ const canClose = await beforeClose?.();
3141
+ if (canClose === false) {
3142
+ return;
3143
+ }
3144
+ props.onClose(false);
3145
+ };
3146
+ const close = () => {
3147
+ void requestClose();
3148
+ };
3149
+ const saveAndClose = (message) => {
3150
+ if (message) {
3151
+ notification.success(message);
3152
+ }
3153
+ props.onClose(true);
3154
+ };
3155
+ const hasFooter = footer !== null;
3156
+ const isFullscreen = placement === "fullscreen";
3157
+ return createPortal(
3158
+ /* @__PURE__ */ jsxs(ModalContext.Provider, { value: { close, saveAndClose, setTitle, setFooter, setLoading }, children: [
3159
+ /* @__PURE__ */ jsx(
3160
+ "div",
3161
+ {
3162
+ className: twMerge(
3163
+ "fixed inset-0 z-1000 bg-black/40 transition-opacity duration-220",
3164
+ visible ? "opacity-100" : "opacity-0",
3165
+ !props.isOpen && "pointer-events-none",
3166
+ props.classNames?.overlay
3167
+ ),
3168
+ onClick: () => closeOnOverlayClick && close()
3169
+ }
3170
+ ),
3171
+ /* @__PURE__ */ jsx(
3172
+ "div",
3173
+ {
3174
+ className: twMerge(
3175
+ "fixed inset-0 z-1001 flex p-4 transition-all duration-220",
3176
+ isFullscreen ? "items-stretch justify-stretch p-0" : "items-center justify-center",
3177
+ !props.isOpen && "pointer-events-none"
3178
+ ),
3179
+ children: /* @__PURE__ */ jsxs(
3180
+ "div",
3181
+ {
3182
+ ref: panelRef,
3183
+ "data-ui-layer-root": "",
3184
+ role: "dialog",
3185
+ "aria-modal": "true",
3186
+ "aria-hidden": !props.isOpen,
3187
+ style: !isFullscreen ? { width, maxWidth } : void 0,
3188
+ className: twMerge(
3189
+ "bg-white shadow-xl flex flex-col transition-all duration-220 ease-out",
3190
+ isFullscreen ? "h-full w-full rounded-none" : `${sizeClass[size]} max-h-[calc(100dvh-32px)] rounded-(--ui-radius-lg)`,
3191
+ visible ? "scale-100 opacity-100" : "scale-95 opacity-0",
3192
+ props.className,
3193
+ props.classNames?.panel
3194
+ ),
3195
+ onClick: (e) => e.stopPropagation(),
3196
+ children: [
3197
+ !hideHeader && /* @__PURE__ */ jsxs(
3198
+ "div",
3199
+ {
3200
+ className: twMerge(
3201
+ "flex items-center justify-between gap-2 bg-(--ui-primary) shrink-0 rounded-t-(--ui-radius-lg)",
3202
+ isFullscreen && "rounded-none",
3203
+ drawerLayoutClasses.header,
3204
+ props.classNames?.header
3205
+ ),
3206
+ children: [
3207
+ /* @__PURE__ */ jsx("span", { className: twMerge(componentTitleClasses.inverse, "truncate", props.classNames?.title), children: title }),
3208
+ showCloseButton && /* @__PURE__ */ jsx(
3209
+ "button",
3210
+ {
3211
+ type: "button",
3212
+ onClick: close,
3213
+ className: twMerge(
3214
+ "shrink-0 text-(--ui-primary-text)/70 hover:text-(--ui-primary-text) cursor-pointer transition-colors",
3215
+ props.classNames?.closeButton
3216
+ ),
3217
+ children: /* @__PURE__ */ jsx(IconX, { size: 18, strokeWidth: 1.5 })
3218
+ }
3219
+ )
3220
+ ]
3221
+ }
3222
+ ),
3223
+ /* @__PURE__ */ jsx(
3224
+ "div",
3225
+ {
3226
+ className: twMerge(
3227
+ "overflow-y-auto flex-1",
3228
+ isFullscreen ? "min-h-0" : "max-h-[calc(100dvh-120px)]",
3229
+ drawerLayoutClasses.body,
3230
+ props.classNames?.body
3231
+ ),
3232
+ children: loading ? /* @__PURE__ */ jsx("div", { className: twMerge("flex items-center justify-center py-16", props.classNames?.loading), children: /* @__PURE__ */ jsx(Spinner, { size: "large", color: "primary" }) }) : props.children
3233
+ }
3234
+ ),
3235
+ hasFooter && /* @__PURE__ */ jsx(
3236
+ "div",
3237
+ {
3238
+ className: twMerge(
3239
+ "border-t border-(--ui-border) shrink-0 bg-white",
3240
+ isFullscreen ? "rounded-none" : "rounded-b-(--ui-radius-lg)",
3241
+ drawerLayoutClasses.footer,
3242
+ props.classNames?.footer
3243
+ ),
3244
+ children: footer
3245
+ }
3246
+ )
3247
+ ]
3248
+ }
3249
+ )
3250
+ }
3251
+ )
3252
+ ] }),
3253
+ document.body
3254
+ );
3255
+ };
3256
+ var ModalTitle = ({ children }) => {
3257
+ const { setTitle } = useModal();
3258
+ useEffect(() => {
3259
+ setTitle(String(children));
3260
+ return () => setTitle("");
3261
+ }, [children]);
3262
+ return null;
3263
+ };
3264
+ var alignClass2 = {
3265
+ left: "justify-start",
3266
+ center: "justify-center",
3267
+ right: "justify-end"
3268
+ };
3269
+ var ModalFooter = ({ children, align = "right" }) => {
3270
+ const { setFooter } = useModal();
3271
+ useEffect(() => {
3272
+ setFooter(
3273
+ /* @__PURE__ */ jsx("div", { className: twMerge("flex items-center gap-2", alignClass2[align]), children })
3274
+ );
3275
+ return () => setFooter(null);
3276
+ }, [align, children]);
3277
+ return null;
3278
+ };
3279
+ var ModalSkeleton = () => /* @__PURE__ */ jsxs("div", { className: "animate-pulse space-y-3", children: [
3280
+ /* @__PURE__ */ jsx("div", { className: "h-4 bg-(--ui-surface-muted) rounded w-1/3" }),
3281
+ /* @__PURE__ */ jsx("div", { className: "h-8 bg-(--ui-surface-muted) rounded" }),
3282
+ /* @__PURE__ */ jsx("div", { className: "h-4 bg-(--ui-surface-muted) rounded w-1/3 mt-5" }),
3283
+ /* @__PURE__ */ jsx("div", { className: "h-8 bg-(--ui-surface-muted) rounded" }),
3284
+ /* @__PURE__ */ jsx("div", { className: "h-4 bg-(--ui-surface-muted) rounded w-1/3 mt-5" }),
3285
+ /* @__PURE__ */ jsx("div", { className: "h-8 bg-(--ui-surface-muted) rounded" })
3286
+ ] });
3287
+ var ModalContent = ({ loading = false, children }) => {
3288
+ if (loading) {
3289
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
3290
+ /* @__PURE__ */ jsx(ModalSkeleton, {}),
3291
+ /* @__PURE__ */ jsx(Skeleton, { active: true, title: false, paragraph: { rows: 2 } })
3292
+ ] });
3293
+ }
3294
+ return /* @__PURE__ */ jsx(Fragment, { children });
3295
+ };
2986
3296
  var Alert = ({ type = "info", message, description, closable = false, className, classNames }) => {
2987
3297
  const [closed, setClosed] = useState(false);
2988
3298
  if (closed) {
@@ -4192,6 +4502,6 @@ function initUI(config) {
4192
4502
  r.style.setProperty("--ui-empty-compact-description", layout.empty.compactDescription);
4193
4503
  }
4194
4504
 
4195
- export { ActiveTag, Alert, Avatar, Badge, Button, CheckboxInput, ColorInput, DateInput, Drawer, DrawerContent, DrawerFooter, DrawerTitle, Dropdown, Empty, ErrorProvider, FilterCheckboxInput, FilterDateInput, FilterDateRangePopover, FilterNumberInput, FilterSelectGroupPopover, FilterSelectInput, FilterTextInput, FlagTag, Grid, GridFilters, HtmlInput, InputError, InputField, InputLabel, MultiSelectInput, NotificationProvider, NumberInput, Panel, Popconfirm, Pretty, SelectInput, ServerError, Skeleton, SortDirection, Spinner, SwitchInput, Table, Tabs, Tag, TextInput, Tooltip, UploadInput, UploadProvider, getIcon, initUI, notification, registerIcons, uiTheme, useDrawer, useGrid, useNotification, useUploadConfig };
4505
+ export { ActiveTag, Alert, Avatar, Badge, Button, CheckboxInput, ColorInput, DateInput, Drawer, DrawerContent, DrawerFooter, DrawerTitle, Dropdown, Empty, ErrorProvider, FilterCheckboxInput, FilterDateInput, FilterDateRangePopover, FilterNumberInput, FilterSelectGroupPopover, FilterSelectInput, FilterTextInput, FlagTag, Grid, GridFilters, HtmlInput, InputError, InputField, InputLabel, Modal, ModalContent, ModalFooter, ModalTitle, MultiSelectInput, NotificationProvider, NumberInput, Panel, Popconfirm, Pretty, SelectInput, ServerError, Skeleton, SortDirection, Spinner, SwitchInput, Table, Tabs, Tag, TextInput, Tooltip, UploadInput, UploadProvider, getIcon, initUI, notification, registerIcons, uiTheme, useDrawer, useGrid, useModal, useNotification, useUploadConfig };
4196
4506
  //# sourceMappingURL=index.js.map
4197
4507
  //# sourceMappingURL=index.js.map