@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.cjs CHANGED
@@ -417,12 +417,12 @@ var ErrorContext = react.createContext({
417
417
  var ErrorProvider = ({ resolveInputError, resolveServerError, children }) => /* @__PURE__ */ jsxRuntime.jsx(ErrorContext.Provider, { value: { resolveInputError, resolveServerError }, children });
418
418
  var useErrorResolver = () => react.useContext(ErrorContext).resolveInputError;
419
419
  var useServerError = () => react.useContext(ErrorContext).resolveServerError;
420
- var TextInput = ({
420
+ var TextInput = react.forwardRef(({
421
421
  size = "middle",
422
422
  password = false,
423
423
  newPassword = false,
424
424
  ...props
425
- }) => {
425
+ }, ref) => {
426
426
  const [showPassword, setShowPassword] = react.useState(false);
427
427
  const resolveError = useErrorResolver();
428
428
  const resolvedErrors = props.errorName ? resolveError(props.errorName) : [];
@@ -443,6 +443,7 @@ var TextInput = ({
443
443
  /* @__PURE__ */ jsxRuntime.jsx(
444
444
  "input",
445
445
  {
446
+ ref,
446
447
  type: inputType,
447
448
  autoComplete: newPassword ? "new-password" : void 0,
448
449
  placeholder: props.placeholder,
@@ -476,7 +477,8 @@ var TextInput = ({
476
477
  ] }),
477
478
  errorDisplay && /* @__PURE__ */ jsxRuntime.jsx(InputError, { error: String(errorDisplay), className: props.classNames?.error })
478
479
  ] });
479
- };
480
+ });
481
+ TextInput.displayName = "TextInput";
480
482
  var numberInputClass = inputBaseClass + " [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none";
481
483
  var NumberInput = ({
482
484
  size = "middle",
@@ -648,9 +650,28 @@ var SwitchInput = ({
648
650
  errorDisplay && /* @__PURE__ */ jsxRuntime.jsx(InputError, { error: String(errorDisplay), className: props.classNames?.error })
649
651
  ] });
650
652
  };
653
+
654
+ // src/floating/layerStack.ts
655
+ var FLOATING_ROOT_SELECTOR = "[data-ui-floating-root]";
656
+ var LAYER_ROOT_SELECTOR = "[data-ui-layer-root]";
657
+ function getTopmostElement(selector) {
658
+ const elements = document.querySelectorAll(selector);
659
+ return elements.length > 0 ? elements[elements.length - 1] : null;
660
+ }
661
+ function hasFloatingRootOpen() {
662
+ return getTopmostElement(FLOATING_ROOT_SELECTOR) !== null;
663
+ }
664
+ function isTopmostFloatingRoot(element) {
665
+ return element !== null && getTopmostElement(FLOATING_ROOT_SELECTOR) === element;
666
+ }
667
+ function isTopmostLayerRoot(element) {
668
+ return element !== null && getTopmostElement(LAYER_ROOT_SELECTOR) === element;
669
+ }
670
+ function isTargetInsideFloatingRoot(target) {
671
+ return target instanceof Element && target.closest(FLOATING_ROOT_SELECTOR) !== null;
672
+ }
651
673
  var GAP = 6;
652
674
  var VIEWPORT_MARGIN = 8;
653
- var FLOATING_ROOT_SELECTOR = "[data-ui-floating-root]";
654
675
  function calcPosition(triggerRect, panelRect, placement) {
655
676
  const spaceBelow = window.innerHeight - triggerRect.bottom - VIEWPORT_MARGIN;
656
677
  const spaceAbove = triggerRect.top - VIEWPORT_MARGIN;
@@ -757,7 +778,7 @@ var Dropdown = ({
757
778
  }
758
779
  };
759
780
  const handleKeyDown = (e) => {
760
- if (e.key === "Escape") {
781
+ if (e.key === "Escape" && isTopmostFloatingRoot(popupRef.current)) {
761
782
  setOpen(false);
762
783
  }
763
784
  };
@@ -1921,6 +1942,19 @@ var SelectDropdown = (props) => {
1921
1942
  react.useEffect(() => {
1922
1943
  if (props.isOpen) {
1923
1944
  document.addEventListener("mousedown", handleMouseDown);
1945
+ const handleKeyDown = (e) => {
1946
+ if (e.key === "Escape" && isTopmostFloatingRoot(popupRef.current)) {
1947
+ e.preventDefault();
1948
+ e.stopPropagation();
1949
+ e.stopImmediatePropagation();
1950
+ props.onClose();
1951
+ }
1952
+ };
1953
+ document.addEventListener("keydown", handleKeyDown);
1954
+ return () => {
1955
+ document.removeEventListener("mousedown", handleMouseDown);
1956
+ document.removeEventListener("keydown", handleKeyDown);
1957
+ };
1924
1958
  }
1925
1959
  return () => document.removeEventListener("mousedown", handleMouseDown);
1926
1960
  }, [props.isOpen, handleMouseDown]);
@@ -2270,6 +2304,19 @@ var CalendarPopup = (props) => {
2270
2304
  react.useEffect(() => {
2271
2305
  if (props.isOpen) {
2272
2306
  document.addEventListener("mousedown", handleMouseDown);
2307
+ const handleKeyDown = (e) => {
2308
+ if (e.key === "Escape" && isTopmostFloatingRoot(popupRef.current)) {
2309
+ e.preventDefault();
2310
+ e.stopPropagation();
2311
+ e.stopImmediatePropagation();
2312
+ props.onClose();
2313
+ }
2314
+ };
2315
+ document.addEventListener("keydown", handleKeyDown);
2316
+ return () => {
2317
+ document.removeEventListener("mousedown", handleMouseDown);
2318
+ document.removeEventListener("keydown", handleKeyDown);
2319
+ };
2273
2320
  }
2274
2321
  return () => document.removeEventListener("mousedown", handleMouseDown);
2275
2322
  }, [props.isOpen, handleMouseDown]);
@@ -2322,7 +2369,7 @@ var CalendarPopup = (props) => {
2322
2369
  ref: popupRef,
2323
2370
  "data-ui-floating-root": "",
2324
2371
  style: { top: pos.top, left: pos.left },
2325
- className: "fixed z-1000 bg-white border border-(--ui-border) rounded-(--ui-radius-lg) shadow-lg w-68",
2372
+ className: "fixed z-1003 bg-white border border-(--ui-border) rounded-(--ui-radius-lg) shadow-lg w-68",
2326
2373
  children: [
2327
2374
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-3 py-2 border-b border-(--ui-border)", children: [
2328
2375
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2821,6 +2868,7 @@ var Drawer = ({ placement = "top", ...props }) => {
2821
2868
  const [footer, setFooter] = react.useState(null);
2822
2869
  const [loading, setLoading] = react.useState(false);
2823
2870
  const frameRef = react.useRef(0);
2871
+ const panelRef = react.useRef(null);
2824
2872
  react.useEffect(() => {
2825
2873
  if (props.isOpen) {
2826
2874
  setRendered(true);
@@ -2839,9 +2887,19 @@ var Drawer = ({ placement = "top", ...props }) => {
2839
2887
  return;
2840
2888
  }
2841
2889
  const handleKeyDown = (e) => {
2842
- if (e.key === "Escape") {
2843
- props.onClose(false);
2890
+ if (e.key !== "Escape" || e.defaultPrevented) {
2891
+ return;
2892
+ }
2893
+ if (hasFloatingRootOpen()) {
2894
+ return;
2895
+ }
2896
+ if (isTargetInsideFloatingRoot(e.target)) {
2897
+ return;
2898
+ }
2899
+ if (!isTopmostLayerRoot(panelRef.current)) {
2900
+ return;
2844
2901
  }
2902
+ props.onClose(false);
2845
2903
  };
2846
2904
  document.body.style.overflow = "hidden";
2847
2905
  document.addEventListener("keydown", handleKeyDown);
@@ -2878,6 +2936,8 @@ var Drawer = ({ placement = "top", ...props }) => {
2878
2936
  /* @__PURE__ */ jsxRuntime.jsxs(
2879
2937
  "div",
2880
2938
  {
2939
+ ref: panelRef,
2940
+ "data-ui-layer-root": "",
2881
2941
  className: tailwindMerge.twMerge(
2882
2942
  panelBase[placement],
2883
2943
  "bg-white shadow-xl transition-transform duration-220 ease-out",
@@ -2989,6 +3049,256 @@ var DrawerContent = ({ loading = false, children }) => {
2989
3049
  }
2990
3050
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
2991
3051
  };
3052
+ var ModalContext = react.createContext(null);
3053
+ function useModal() {
3054
+ const ctx = react.useContext(ModalContext);
3055
+ if (!ctx) {
3056
+ throw new Error("useModal must be used inside Modal");
3057
+ }
3058
+ return ctx;
3059
+ }
3060
+ var TRANSITION_MS2 = 220;
3061
+ var sizeClass = {
3062
+ small: "w-full max-w-lg",
3063
+ middle: "w-full max-w-3xl",
3064
+ large: "w-full max-w-5xl",
3065
+ auto: "w-auto"
3066
+ };
3067
+ var Modal = ({
3068
+ placement = "center",
3069
+ size = "middle",
3070
+ width,
3071
+ maxWidth,
3072
+ closeOnEscape = true,
3073
+ closeOnOverlayClick = true,
3074
+ showCloseButton = true,
3075
+ hideHeader = false,
3076
+ destroyOnClose = true,
3077
+ preventClose = false,
3078
+ beforeClose,
3079
+ initialFocusRef,
3080
+ ...props
3081
+ }) => {
3082
+ const [rendered, setRendered] = react.useState(props.isOpen);
3083
+ const [visible, setVisible] = react.useState(false);
3084
+ const [title, setTitle] = react.useState("");
3085
+ const [footer, setFooter] = react.useState(null);
3086
+ const [loading, setLoading] = react.useState(false);
3087
+ const frameRef = react.useRef(0);
3088
+ const panelRef = react.useRef(null);
3089
+ react.useEffect(() => {
3090
+ if (props.isOpen) {
3091
+ setRendered(true);
3092
+ frameRef.current = requestAnimationFrame(() => {
3093
+ frameRef.current = requestAnimationFrame(() => setVisible(true));
3094
+ });
3095
+ } else {
3096
+ setVisible(false);
3097
+ if (!destroyOnClose) {
3098
+ return () => cancelAnimationFrame(frameRef.current);
3099
+ }
3100
+ const t = setTimeout(() => setRendered(false), TRANSITION_MS2);
3101
+ return () => clearTimeout(t);
3102
+ }
3103
+ return () => cancelAnimationFrame(frameRef.current);
3104
+ }, [destroyOnClose, props.isOpen]);
3105
+ react.useEffect(() => {
3106
+ if (!props.isOpen) {
3107
+ return;
3108
+ }
3109
+ const focusTarget = initialFocusRef?.current;
3110
+ if (!focusTarget) {
3111
+ return;
3112
+ }
3113
+ const timeout = window.setTimeout(() => focusTarget.focus(), TRANSITION_MS2 / 2);
3114
+ return () => window.clearTimeout(timeout);
3115
+ }, [initialFocusRef, props.isOpen]);
3116
+ react.useEffect(() => {
3117
+ if (!props.isOpen) {
3118
+ return;
3119
+ }
3120
+ const handleKeyDown = (e) => {
3121
+ if (!closeOnEscape || e.key !== "Escape" || e.defaultPrevented) {
3122
+ return;
3123
+ }
3124
+ if (hasFloatingRootOpen() || isTargetInsideFloatingRoot(e.target)) {
3125
+ return;
3126
+ }
3127
+ if (!isTopmostLayerRoot(panelRef.current)) {
3128
+ return;
3129
+ }
3130
+ void requestClose();
3131
+ };
3132
+ document.body.style.overflow = "hidden";
3133
+ document.addEventListener("keydown", handleKeyDown);
3134
+ return () => {
3135
+ document.body.style.overflow = "";
3136
+ document.removeEventListener("keydown", handleKeyDown);
3137
+ };
3138
+ }, [closeOnEscape, props.isOpen]);
3139
+ if (!rendered) {
3140
+ return null;
3141
+ }
3142
+ const requestClose = async () => {
3143
+ if (preventClose) {
3144
+ return;
3145
+ }
3146
+ const canClose = await beforeClose?.();
3147
+ if (canClose === false) {
3148
+ return;
3149
+ }
3150
+ props.onClose(false);
3151
+ };
3152
+ const close = () => {
3153
+ void requestClose();
3154
+ };
3155
+ const saveAndClose = (message) => {
3156
+ if (message) {
3157
+ notification.success(message);
3158
+ }
3159
+ props.onClose(true);
3160
+ };
3161
+ const hasFooter = footer !== null;
3162
+ const isFullscreen = placement === "fullscreen";
3163
+ return reactDom.createPortal(
3164
+ /* @__PURE__ */ jsxRuntime.jsxs(ModalContext.Provider, { value: { close, saveAndClose, setTitle, setFooter, setLoading }, children: [
3165
+ /* @__PURE__ */ jsxRuntime.jsx(
3166
+ "div",
3167
+ {
3168
+ className: tailwindMerge.twMerge(
3169
+ "fixed inset-0 z-1000 bg-black/40 transition-opacity duration-220",
3170
+ visible ? "opacity-100" : "opacity-0",
3171
+ !props.isOpen && "pointer-events-none",
3172
+ props.classNames?.overlay
3173
+ ),
3174
+ onClick: () => closeOnOverlayClick && close()
3175
+ }
3176
+ ),
3177
+ /* @__PURE__ */ jsxRuntime.jsx(
3178
+ "div",
3179
+ {
3180
+ className: tailwindMerge.twMerge(
3181
+ "fixed inset-0 z-1001 flex p-4 transition-all duration-220",
3182
+ isFullscreen ? "items-stretch justify-stretch p-0" : "items-center justify-center",
3183
+ !props.isOpen && "pointer-events-none"
3184
+ ),
3185
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
3186
+ "div",
3187
+ {
3188
+ ref: panelRef,
3189
+ "data-ui-layer-root": "",
3190
+ role: "dialog",
3191
+ "aria-modal": "true",
3192
+ "aria-hidden": !props.isOpen,
3193
+ style: !isFullscreen ? { width, maxWidth } : void 0,
3194
+ className: tailwindMerge.twMerge(
3195
+ "bg-white shadow-xl flex flex-col transition-all duration-220 ease-out",
3196
+ isFullscreen ? "h-full w-full rounded-none" : `${sizeClass[size]} max-h-[calc(100dvh-32px)] rounded-(--ui-radius-lg)`,
3197
+ visible ? "scale-100 opacity-100" : "scale-95 opacity-0",
3198
+ props.className,
3199
+ props.classNames?.panel
3200
+ ),
3201
+ onClick: (e) => e.stopPropagation(),
3202
+ children: [
3203
+ !hideHeader && /* @__PURE__ */ jsxRuntime.jsxs(
3204
+ "div",
3205
+ {
3206
+ className: tailwindMerge.twMerge(
3207
+ "flex items-center justify-between gap-2 bg-(--ui-primary) shrink-0 rounded-t-(--ui-radius-lg)",
3208
+ isFullscreen && "rounded-none",
3209
+ drawerLayoutClasses.header,
3210
+ props.classNames?.header
3211
+ ),
3212
+ children: [
3213
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: tailwindMerge.twMerge(componentTitleClasses.inverse, "truncate", props.classNames?.title), children: title }),
3214
+ showCloseButton && /* @__PURE__ */ jsxRuntime.jsx(
3215
+ "button",
3216
+ {
3217
+ type: "button",
3218
+ onClick: close,
3219
+ className: tailwindMerge.twMerge(
3220
+ "shrink-0 text-(--ui-primary-text)/70 hover:text-(--ui-primary-text) cursor-pointer transition-colors",
3221
+ props.classNames?.closeButton
3222
+ ),
3223
+ children: /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconX, { size: 18, strokeWidth: 1.5 })
3224
+ }
3225
+ )
3226
+ ]
3227
+ }
3228
+ ),
3229
+ /* @__PURE__ */ jsxRuntime.jsx(
3230
+ "div",
3231
+ {
3232
+ className: tailwindMerge.twMerge(
3233
+ "overflow-y-auto flex-1",
3234
+ isFullscreen ? "min-h-0" : "max-h-[calc(100dvh-120px)]",
3235
+ drawerLayoutClasses.body,
3236
+ props.classNames?.body
3237
+ ),
3238
+ children: loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: tailwindMerge.twMerge("flex items-center justify-center py-16", props.classNames?.loading), children: /* @__PURE__ */ jsxRuntime.jsx(Spinner, { size: "large", color: "primary" }) }) : props.children
3239
+ }
3240
+ ),
3241
+ hasFooter && /* @__PURE__ */ jsxRuntime.jsx(
3242
+ "div",
3243
+ {
3244
+ className: tailwindMerge.twMerge(
3245
+ "border-t border-(--ui-border) shrink-0 bg-white",
3246
+ isFullscreen ? "rounded-none" : "rounded-b-(--ui-radius-lg)",
3247
+ drawerLayoutClasses.footer,
3248
+ props.classNames?.footer
3249
+ ),
3250
+ children: footer
3251
+ }
3252
+ )
3253
+ ]
3254
+ }
3255
+ )
3256
+ }
3257
+ )
3258
+ ] }),
3259
+ document.body
3260
+ );
3261
+ };
3262
+ var ModalTitle = ({ children }) => {
3263
+ const { setTitle } = useModal();
3264
+ react.useEffect(() => {
3265
+ setTitle(String(children));
3266
+ return () => setTitle("");
3267
+ }, [children]);
3268
+ return null;
3269
+ };
3270
+ var alignClass2 = {
3271
+ left: "justify-start",
3272
+ center: "justify-center",
3273
+ right: "justify-end"
3274
+ };
3275
+ var ModalFooter = ({ children, align = "right" }) => {
3276
+ const { setFooter } = useModal();
3277
+ react.useEffect(() => {
3278
+ setFooter(
3279
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: tailwindMerge.twMerge("flex items-center gap-2", alignClass2[align]), children })
3280
+ );
3281
+ return () => setFooter(null);
3282
+ }, [align, children]);
3283
+ return null;
3284
+ };
3285
+ var ModalSkeleton = () => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "animate-pulse space-y-3", children: [
3286
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 bg-(--ui-surface-muted) rounded w-1/3" }),
3287
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-8 bg-(--ui-surface-muted) rounded" }),
3288
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 bg-(--ui-surface-muted) rounded w-1/3 mt-5" }),
3289
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-8 bg-(--ui-surface-muted) rounded" }),
3290
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 bg-(--ui-surface-muted) rounded w-1/3 mt-5" }),
3291
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-8 bg-(--ui-surface-muted) rounded" })
3292
+ ] });
3293
+ var ModalContent = ({ loading = false, children }) => {
3294
+ if (loading) {
3295
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-5", children: [
3296
+ /* @__PURE__ */ jsxRuntime.jsx(ModalSkeleton, {}),
3297
+ /* @__PURE__ */ jsxRuntime.jsx(Skeleton, { active: true, title: false, paragraph: { rows: 2 } })
3298
+ ] });
3299
+ }
3300
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
3301
+ };
2992
3302
  var Alert = ({ type = "info", message, description, closable = false, className, classNames }) => {
2993
3303
  const [closed, setClosed] = react.useState(false);
2994
3304
  if (closed) {
@@ -4227,6 +4537,10 @@ exports.HtmlInput = HtmlInput;
4227
4537
  exports.InputError = InputError;
4228
4538
  exports.InputField = InputField;
4229
4539
  exports.InputLabel = InputLabel;
4540
+ exports.Modal = Modal;
4541
+ exports.ModalContent = ModalContent;
4542
+ exports.ModalFooter = ModalFooter;
4543
+ exports.ModalTitle = ModalTitle;
4230
4544
  exports.MultiSelectInput = MultiSelectInput;
4231
4545
  exports.NotificationProvider = NotificationProvider;
4232
4546
  exports.NumberInput = NumberInput;
@@ -4253,6 +4567,7 @@ exports.registerIcons = registerIcons;
4253
4567
  exports.uiTheme = uiTheme;
4254
4568
  exports.useDrawer = useDrawer;
4255
4569
  exports.useGrid = useGrid;
4570
+ exports.useModal = useModal;
4256
4571
  exports.useNotification = useNotification;
4257
4572
  exports.useUploadConfig = useUploadConfig;
4258
4573
  //# sourceMappingURL=index.cjs.map