@skalfa/skalfa-component 1.0.35 → 1.0.37

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
@@ -176,6 +176,7 @@ exports.TypographyTipsComponent = require("./typography/TypographyTips.component
176
176
 
177
177
  // Static export for ./wrap/ContentWrapper.component
178
178
  exports.COLOR_MAP = require("./wrap/ContentWrapper.component").COLOR_MAP;
179
+ exports.formatLinkUrl = require("./wrap/ContentWrapper.component").formatLinkUrl;
179
180
  exports.parseInlineFormats = require("./wrap/ContentWrapper.component").parseInlineFormats;
180
181
  exports.parseContentToHtml = require("./wrap/ContentWrapper.component").parseContentToHtml;
181
182
  exports.parseHtmlToContent = require("./wrap/ContentWrapper.component").parseHtmlToContent;
@@ -9,7 +9,7 @@ const _utils_1 = require("@utils");
9
9
  const BottomSheet_component_1 = require("../modal/BottomSheet.component");
10
10
  const Button_component_1 = require("../button/Button.component");
11
11
  const Modal_component_1 = require("../modal/Modal.component");
12
- const InputImageComponent = ({ name, label, tip, value, disabled, aspect = "1/1", invalid, validations, onChange, register, unregister, className = "", }) => {
12
+ const InputImageComponent = ({ name, label, tip, value, disabled, aspect, invalid, validations, onChange, register, unregister, className = "", }) => {
13
13
  const { isSm } = (0, _utils_1.useResponsive)();
14
14
  const randomId = (0, _utils_1.useInputRandomId)();
15
15
  const inputRef = (0, react_1.useRef)(null);
@@ -19,6 +19,9 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect = "1/1"
19
19
  const [openCrop, setOpenCrop] = (0, react_1.useState)(false);
20
20
  const inputHandler = (0, _utils_1.useInputHandler)(name, value, validations, register, true, unregister);
21
21
  const [invalidMessage, setInvalidMessage] = (0, _utils_1.useValidation)(inputHandler.value, validations, invalid, inputHandler.idle);
22
+ const isCroppingEnabled = !!aspect;
23
+ const currentAspect = aspect || "1/1";
24
+ const containerAspect = (preview && !isCroppingEnabled) ? undefined : currentAspect;
22
25
  (0, react_1.useEffect)(() => {
23
26
  if (value) {
24
27
  const url = typeof value === "object" ? URL.createObjectURL(value) : value;
@@ -48,7 +51,14 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect = "1/1"
48
51
  setInvalidMessage("Format gambar tidak diperbolehkan (JPG/PNG/WebP)");
49
52
  return;
50
53
  }
51
- openCropper(file);
54
+ if (isCroppingEnabled) {
55
+ openCropper(file);
56
+ }
57
+ else {
58
+ const url = URL.createObjectURL(file);
59
+ setPreview(url);
60
+ onChange?.(file);
61
+ }
52
62
  };
53
63
  const onCropDone = (file) => {
54
64
  const url = URL.createObjectURL(file);
@@ -69,17 +79,27 @@ const InputImageComponent = ({ name, label, tip, value, disabled, aspect = "1/1"
69
79
  e.preventDefault();
70
80
  setDrag(false);
71
81
  const file = e.dataTransfer.files?.[0];
72
- file && openCropper(file);
82
+ if (!file)
83
+ return;
84
+ if (isCroppingEnabled) {
85
+ openCropper(file);
86
+ }
87
+ else {
88
+ const url = URL.createObjectURL(file);
89
+ setPreview(url);
90
+ onChange?.(file);
91
+ }
73
92
  };
74
93
  return ((0, jsx_runtime_1.jsxs)("div", { className: "input-container w-full", children: [label && ((0, jsx_runtime_1.jsxs)("label", { htmlFor: randomId, className: (0, _utils_1.cn)("input-label", disabled && "input-label-disabled", inputHandler.focus && "input-label-focus", !!invalidMessage && "input-label-error", (0, _utils_1.pcn)(className, "label")), children: [label, validations && _utils_1.validation.hasRules(validations, "required") && (0, jsx_runtime_1.jsx)("span", { className: "text-danger ml-1", children: "*" })] })), tip && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-tip", disabled && "input-tip-disabled", (0, _utils_1.pcn)(className, "tip")), children: tip })), (0, jsx_runtime_1.jsx)("label", { htmlFor: randomId, children: (0, jsx_runtime_1.jsxs)("div", { className: (0, _utils_1.cn)("input-image-dropzone", drag ? "input-image-dropzone-drag" : "", invalidMessage && "input-image-dropzone-error", (0, _utils_1.pcn)(className, "input")), style: {
75
- aspectRatio: aspect,
76
- backgroundImage: preview ? `url(${preview})` : undefined,
94
+ position: "relative",
95
+ aspectRatio: containerAspect,
96
+ backgroundImage: preview && containerAspect ? `url(${preview})` : undefined,
77
97
  backgroundSize: "cover",
78
98
  backgroundPosition: "center",
79
99
  }, onDragOver: (e) => e.preventDefault(), onDragEnter: (e) => {
80
100
  e.preventDefault();
81
101
  setDrag(true);
82
- }, onDragLeave: () => setDrag(false), onDrop: onDropFile, children: [(0, jsx_runtime_1.jsxs)("div", { className: "input-image-dropzone-content", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: drag ? "solid/hand-holding" : "solid/images", className: "text-3xl" }), (0, jsx_runtime_1.jsx)("p", { className: "input-image-dropzone-text", children: drag ? "Letakkan di sini" : preview ? "Klik atau seret untuk ganti Gambar" : "Klik atau seret gambar" })] }), (0, jsx_runtime_1.jsx)("input", { id: randomId, ref: inputRef, type: "file", accept: "image/*", className: "hidden", disabled: disabled, onChange: onUpload })] }) }), preview && !disabled && ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { icon: "solid/times", paint: "danger", size: "sm", className: "absolute top-10 right-4", onClick: remove })), invalidMessage && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-error-message", (0, _utils_1.pcn)(className, "error")), children: invalidMessage })), !isSm ? ((0, jsx_runtime_1.jsx)(Modal_component_1.ModalComponent, { show: openCrop, onClose: onCropCancel, title: "Sesuaikan Gambar", className: "w-max max-w-[350px]", children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(aspect.replace(":", "/")), onDone: onCropDone }) })) })) : ((0, jsx_runtime_1.jsx)(BottomSheet_component_1.BottomSheetComponent, { show: openCrop, onClose: onCropCancel, size: 400, children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(aspect.replace(":", "/")), onDone: onCropDone }) })) }))] }));
102
+ }, onDragLeave: () => setDrag(false), onDrop: onDropFile, children: [preview && !containerAspect ? ((0, jsx_runtime_1.jsxs)("div", { className: "relative group w-full h-full flex items-center justify-center", children: [(0, jsx_runtime_1.jsx)("img", { src: preview, alt: "Preview", className: "w-full h-auto max-h-[400px] object-contain rounded-lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex flex-col items-center justify-center text-white rounded-lg", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: "solid/images", className: "text-3xl mb-2" }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-center", children: "Klik atau seret untuk ganti Gambar" })] })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "input-image-dropzone-content", children: [(0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: drag ? "solid/hand-holding" : "solid/images", className: "text-3xl" }), (0, jsx_runtime_1.jsx)("p", { className: "input-image-dropzone-text", children: drag ? "Letakkan di sini" : preview ? "Klik atau seret untuk ganti Gambar" : "Klik atau seret gambar" })] })), (0, jsx_runtime_1.jsx)("input", { id: randomId, ref: inputRef, type: "file", accept: "image/*", className: "hidden", disabled: disabled, onChange: onUpload })] }) }), preview && !disabled && ((0, jsx_runtime_1.jsx)(Button_component_1.ButtonComponent, { icon: "solid/times", paint: "danger", size: "sm", className: "absolute top-10 right-4", onClick: remove })), invalidMessage && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-error-message", (0, _utils_1.pcn)(className, "error")), children: invalidMessage })), !isSm ? ((0, jsx_runtime_1.jsx)(Modal_component_1.ModalComponent, { show: openCrop, onClose: onCropCancel, title: "Sesuaikan Gambar", className: "w-max max-w-[350px]", children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), onDone: onCropDone }) })) })) : ((0, jsx_runtime_1.jsx)(BottomSheet_component_1.BottomSheetComponent, { show: openCrop, onClose: onCropCancel, size: 400, children: cropSrc && ((0, jsx_runtime_1.jsx)("div", { className: "p-4", children: (0, jsx_runtime_1.jsx)(exports.CanvasCropper, { src: cropSrc, aspect: eval(currentAspect.replace(":", "/")), onDone: onCropDone }) })) }))] }));
83
103
  };
84
104
  exports.InputImageComponent = InputImageComponent;
85
105
  const CanvasCropper = ({ src, aspect, onDone, }) => {
@@ -7,6 +7,7 @@ export declare const COLOR_MAP: Record<string, {
7
7
  tw: string;
8
8
  css: string;
9
9
  }>;
10
+ export declare function formatLinkUrl(url: string): string;
10
11
  export declare function parseInlineFormats(text: string): string;
11
12
  export declare function parseContentToHtml(content: string): string;
12
13
  export declare function parseHtmlToContent(html: string): string;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.COLOR_MAP = void 0;
4
+ exports.formatLinkUrl = formatLinkUrl;
4
5
  exports.parseInlineFormats = parseInlineFormats;
5
6
  exports.parseContentToHtml = parseContentToHtml;
6
7
  exports.parseHtmlToContent = parseHtmlToContent;
@@ -16,6 +17,15 @@ exports.COLOR_MAP = {
16
17
  warning: { label: "Warning", tw: "text-warning", css: "var(--color-warning, #f59e0b)" },
17
18
  danger: { label: "Danger", tw: "text-danger", css: "var(--color-danger, #ef4444)" },
18
19
  };
20
+ function formatLinkUrl(url) {
21
+ if (!url)
22
+ return "";
23
+ const hasProtocol = /^(https?:\/\/|\/\/|\/|#|mailto:|tel:|javascript:)/i.test(url);
24
+ if (hasProtocol) {
25
+ return url;
26
+ }
27
+ return `https://${url}`;
28
+ }
19
29
  function parseInlineFormats(text) {
20
30
  if (!text)
21
31
  return "";
@@ -29,7 +39,8 @@ function parseInlineFormats(text) {
29
39
  return `<span style="font-size:${size}px" data-size="${size}">${content}</span>`;
30
40
  });
31
41
  result = result.replace(/\[link:(.*?)\](.*?)\[link\]/g, (_, url, content) => {
32
- return `<a href="${url}" class="text-primary underline" data-link="${url}" target="_blank" rel="noopener noreferrer">${content}</a>`;
42
+ const formattedUrl = formatLinkUrl(url);
43
+ return `<a href="${formattedUrl}" class="text-primary underline" data-link="${formattedUrl}" target="_blank" rel="noopener noreferrer">${content}</a>`;
33
44
  });
34
45
  result = result.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
35
46
  result = result.replace(/\/\/(.*?)\/\//g, "<em>$1</em>");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skalfa/skalfa-component",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "Reusable UI components for Skalfa App.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -37,7 +37,7 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
37
37
 
38
38
  value,
39
39
  disabled,
40
- aspect = "1/1",
40
+ aspect,
41
41
  invalid,
42
42
  validations,
43
43
 
@@ -59,6 +59,10 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
59
59
  const inputHandler = useInputHandler(name, value, validations, register, true, unregister);
60
60
  const [invalidMessage, setInvalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
61
61
 
62
+ const isCroppingEnabled = !!aspect;
63
+ const currentAspect = aspect || "1/1";
64
+ const containerAspect = (preview && !isCroppingEnabled) ? undefined : currentAspect;
65
+
62
66
  useEffect(() => {
63
67
  if (value) {
64
68
  const url = typeof value === "object" ? URL.createObjectURL(value) : value;
@@ -91,7 +95,13 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
91
95
  return;
92
96
  }
93
97
 
94
- openCropper(file);
98
+ if (isCroppingEnabled) {
99
+ openCropper(file);
100
+ } else {
101
+ const url = URL.createObjectURL(file);
102
+ setPreview(url);
103
+ onChange?.(file);
104
+ }
95
105
  };
96
106
 
97
107
  const onCropDone = (file: File) => {
@@ -117,7 +127,15 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
117
127
  setDrag(false);
118
128
 
119
129
  const file = e.dataTransfer.files?.[0];
120
- file && openCropper(file);
130
+ if (!file) return;
131
+
132
+ if (isCroppingEnabled) {
133
+ openCropper(file);
134
+ } else {
135
+ const url = URL.createObjectURL(file);
136
+ setPreview(url);
137
+ onChange?.(file);
138
+ }
121
139
  };
122
140
 
123
141
  return (
@@ -157,8 +175,9 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
157
175
  pcn<CT>(className, "input")
158
176
  )}
159
177
  style={{
160
- aspectRatio: aspect,
161
- backgroundImage: preview ? `url(${preview})` : undefined,
178
+ position: "relative",
179
+ aspectRatio: containerAspect,
180
+ backgroundImage: preview && containerAspect ? `url(${preview})` : undefined,
162
181
  backgroundSize: "cover",
163
182
  backgroundPosition: "center",
164
183
  }}
@@ -170,10 +189,24 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
170
189
  onDragLeave={() => setDrag(false)}
171
190
  onDrop={onDropFile}
172
191
  >
173
- <div className="input-image-dropzone-content">
174
- <Icon icon={drag ? "solid/hand-holding" : "solid/images"} className="text-3xl" />
175
- <p className="input-image-dropzone-text">{drag ? "Letakkan di sini" : preview ? "Klik atau seret untuk ganti Gambar" : "Klik atau seret gambar"}</p>
176
- </div>
192
+ {preview && !containerAspect ? (
193
+ <div className="relative group w-full h-full flex items-center justify-center">
194
+ <img
195
+ src={preview}
196
+ alt="Preview"
197
+ className="w-full h-auto max-h-[400px] object-contain rounded-lg"
198
+ />
199
+ <div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex flex-col items-center justify-center text-white rounded-lg">
200
+ <Icon icon="solid/images" className="text-3xl mb-2" />
201
+ <p className="text-xs text-center">Klik atau seret untuk ganti Gambar</p>
202
+ </div>
203
+ </div>
204
+ ) : (
205
+ <div className="input-image-dropzone-content">
206
+ <Icon icon={drag ? "solid/hand-holding" : "solid/images"} className="text-3xl" />
207
+ <p className="input-image-dropzone-text">{drag ? "Letakkan di sini" : preview ? "Klik atau seret untuk ganti Gambar" : "Klik atau seret gambar"}</p>
208
+ </div>
209
+ )}
177
210
 
178
211
  <input
179
212
  id={randomId}
@@ -209,7 +242,7 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
209
242
  <div className="p-4">
210
243
  <CanvasCropper
211
244
  src={cropSrc}
212
- aspect={eval(aspect.replace(":", "/"))}
245
+ aspect={eval(currentAspect.replace(":", "/"))}
213
246
  onDone={onCropDone}
214
247
  />
215
248
  </div>
@@ -221,7 +254,7 @@ export const InputImageComponent: React.FC<InputImageProps> = ({
221
254
  <div className="p-4">
222
255
  <CanvasCropper
223
256
  src={cropSrc}
224
- aspect={eval(aspect.replace(":", "/"))}
257
+ aspect={eval(currentAspect.replace(":", "/"))}
225
258
  onDone={onCropDone}
226
259
  />
227
260
  </div>
@@ -15,6 +15,15 @@ export const COLOR_MAP: Record<string, { label: string; tw: string; css: string
15
15
  danger: { label: "Danger", tw: "text-danger", css: "var(--color-danger, #ef4444)" },
16
16
  };
17
17
 
18
+ export function formatLinkUrl(url: string): string {
19
+ if (!url) return "";
20
+ const hasProtocol = /^(https?:\/\/|\/\/|\/|#|mailto:|tel:|javascript:)/i.test(url);
21
+ if (hasProtocol) {
22
+ return url;
23
+ }
24
+ return `https://${url}`;
25
+ }
26
+
18
27
  export function parseInlineFormats(text: string): string {
19
28
  if (!text) return "";
20
29
  let result = text;
@@ -30,7 +39,8 @@ export function parseInlineFormats(text: string): string {
30
39
  });
31
40
 
32
41
  result = result.replace(/\[link:(.*?)\](.*?)\[link\]/g, (_, url, content) => {
33
- return `<a href="${url}" class="text-primary underline" data-link="${url}" target="_blank" rel="noopener noreferrer">${content}</a>`;
42
+ const formattedUrl = formatLinkUrl(url);
43
+ return `<a href="${formattedUrl}" class="text-primary underline" data-link="${formattedUrl}" target="_blank" rel="noopener noreferrer">${content}</a>`;
34
44
  });
35
45
 
36
46
  result = result.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");