@martinsura/ui 0.1.2 → 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",
@@ -651,7 +653,7 @@ var SwitchInput = ({
651
653
 
652
654
  // src/floating/layerStack.ts
653
655
  var FLOATING_ROOT_SELECTOR = "[data-ui-floating-root]";
654
- var DRAWER_ROOT_SELECTOR = "[data-ui-drawer-root]";
656
+ var LAYER_ROOT_SELECTOR = "[data-ui-layer-root]";
655
657
  function getTopmostElement(selector) {
656
658
  const elements = document.querySelectorAll(selector);
657
659
  return elements.length > 0 ? elements[elements.length - 1] : null;
@@ -662,8 +664,8 @@ function hasFloatingRootOpen() {
662
664
  function isTopmostFloatingRoot(element) {
663
665
  return element !== null && getTopmostElement(FLOATING_ROOT_SELECTOR) === element;
664
666
  }
665
- function isTopmostDrawerRoot(element) {
666
- return element !== null && getTopmostElement(DRAWER_ROOT_SELECTOR) === element;
667
+ function isTopmostLayerRoot(element) {
668
+ return element !== null && getTopmostElement(LAYER_ROOT_SELECTOR) === element;
667
669
  }
668
670
  function isTargetInsideFloatingRoot(target) {
669
671
  return target instanceof Element && target.closest(FLOATING_ROOT_SELECTOR) !== null;
@@ -2894,7 +2896,7 @@ var Drawer = ({ placement = "top", ...props }) => {
2894
2896
  if (isTargetInsideFloatingRoot(e.target)) {
2895
2897
  return;
2896
2898
  }
2897
- if (!isTopmostDrawerRoot(panelRef.current)) {
2899
+ if (!isTopmostLayerRoot(panelRef.current)) {
2898
2900
  return;
2899
2901
  }
2900
2902
  props.onClose(false);
@@ -2935,7 +2937,7 @@ var Drawer = ({ placement = "top", ...props }) => {
2935
2937
  "div",
2936
2938
  {
2937
2939
  ref: panelRef,
2938
- "data-ui-drawer-root": "",
2940
+ "data-ui-layer-root": "",
2939
2941
  className: tailwindMerge.twMerge(
2940
2942
  panelBase[placement],
2941
2943
  "bg-white shadow-xl transition-transform duration-220 ease-out",
@@ -3047,6 +3049,256 @@ var DrawerContent = ({ loading = false, children }) => {
3047
3049
  }
3048
3050
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
3049
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
+ };
3050
3302
  var Alert = ({ type = "info", message, description, closable = false, className, classNames }) => {
3051
3303
  const [closed, setClosed] = react.useState(false);
3052
3304
  if (closed) {
@@ -4285,6 +4537,10 @@ exports.HtmlInput = HtmlInput;
4285
4537
  exports.InputError = InputError;
4286
4538
  exports.InputField = InputField;
4287
4539
  exports.InputLabel = InputLabel;
4540
+ exports.Modal = Modal;
4541
+ exports.ModalContent = ModalContent;
4542
+ exports.ModalFooter = ModalFooter;
4543
+ exports.ModalTitle = ModalTitle;
4288
4544
  exports.MultiSelectInput = MultiSelectInput;
4289
4545
  exports.NotificationProvider = NotificationProvider;
4290
4546
  exports.NumberInput = NumberInput;
@@ -4311,6 +4567,7 @@ exports.registerIcons = registerIcons;
4311
4567
  exports.uiTheme = uiTheme;
4312
4568
  exports.useDrawer = useDrawer;
4313
4569
  exports.useGrid = useGrid;
4570
+ exports.useModal = useModal;
4314
4571
  exports.useNotification = useNotification;
4315
4572
  exports.useUploadConfig = useUploadConfig;
4316
4573
  //# sourceMappingURL=index.cjs.map