@skalfa/skalfa-component 1.0.26 → 1.0.27

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.
@@ -0,0 +1,194 @@
1
+ "use client"
2
+
3
+ import { TextareaHTMLAttributes, ReactNode, Ref, useEffect } from "react";
4
+ import { Icon, type IconName } from "@skalfa/skalfa-icon";
5
+ import { cn, pcn, useInputHandler, useInputRandomId, useValidation, validation, ValidationRules } from "@utils";
6
+
7
+ type CT = "label" | "tip" | "error" | "base" | "icon";
8
+
9
+ export interface TextareaProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange"> {
10
+ label ?: string;
11
+ tip ?: string | ReactNode;
12
+ leftIcon ?: IconName | any;
13
+ rightIcon ?: IconName | any;
14
+
15
+ value ?: any;
16
+ invalid ?: string;
17
+
18
+ validations ?: ValidationRules;
19
+ onlyAlphabet ?: boolean;
20
+ uppercase ?: boolean;
21
+ lowercase ?: boolean;
22
+
23
+ onChange ?: (value: any) => any;
24
+ register ?: (name: string, validations?: ValidationRules) => void;
25
+ unregister ?: (name: string) => void;
26
+
27
+ ref ?: Ref<HTMLTextAreaElement>;
28
+
29
+ /** Use custom class with: "label::", "tip::", "error::", "base::", "icon::". */
30
+ className ?: string;
31
+ }
32
+
33
+ export function TextareaComponent({
34
+ label,
35
+ tip,
36
+ leftIcon,
37
+ rightIcon,
38
+ className = "",
39
+
40
+ value,
41
+ invalid,
42
+
43
+ validations,
44
+ onlyAlphabet,
45
+ uppercase,
46
+ lowercase,
47
+
48
+ register,
49
+ unregister,
50
+ onChange,
51
+
52
+ ref,
53
+ ...props
54
+ }: TextareaProps) {
55
+
56
+ // =========================>
57
+ // ## Initial
58
+ // =========================>
59
+ const inputHandler = useInputHandler(props.name, value, validations, register, false, unregister);
60
+ const randomId = useInputRandomId();
61
+
62
+ // =========================>
63
+ // ## Invalid handler
64
+ // =========================>
65
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
66
+
67
+ // =========================>
68
+ // ## Change value handler
69
+ // =========================>
70
+ useEffect(() => {
71
+ if (inputHandler.value && typeof inputHandler.value === "string") {
72
+ let newVal = onlyAlphabet ? inputHandler.value.replace(/[^A-Za-z ]+/g, "") : inputHandler.value;
73
+
74
+ if (uppercase) newVal = newVal.toUpperCase();
75
+ if (lowercase) newVal = newVal.toLowerCase();
76
+
77
+ if (validations && validation.hasRules(validations, "max")) {
78
+ newVal = newVal.slice(0, parseInt(validation.getRules(validations, "max") || "0"));
79
+ }
80
+
81
+ inputHandler.setValue(newVal);
82
+ }
83
+ }, [inputHandler.value, onlyAlphabet, uppercase, lowercase, validations]);
84
+
85
+ return (
86
+ <div className="relative flex flex-col gap-y-0.5 w-full">
87
+ {label && (
88
+ <label
89
+ htmlFor={randomId}
90
+ className={cn(
91
+ "input-label",
92
+ props.disabled && "input-label-disabled",
93
+ inputHandler.focus && "input-label-focus",
94
+ !!invalidMessage && "input-label-error",
95
+ pcn<CT>(className, "label"),
96
+ props.disabled && pcn<CT>(className, "label", "disabled"),
97
+ inputHandler.focus && pcn<CT>(className, "label", "focus"),
98
+ !!invalidMessage && pcn<CT>(className, "label", "error"),
99
+ )}
100
+ >
101
+ {label}
102
+ {validations && validation.hasRules(validations, "required") && <span className="text-danger ml-1">*</span>}
103
+ </label>
104
+ )}
105
+
106
+ {tip && (
107
+ <small
108
+ className={cn(
109
+ "input-tip",
110
+ props.disabled && "input-tip-disabled",
111
+ pcn<CT>(className, "tip"),
112
+ props.disabled && pcn<CT>(className, "tip", "disabled"),
113
+ )}
114
+ >
115
+ {tip}
116
+ </small>
117
+ )}
118
+
119
+ <div className="relative">
120
+ <textarea
121
+ {...props}
122
+ ref={ref}
123
+ id={randomId}
124
+ value={inputHandler.value ?? ""}
125
+ onChange={(e) => {
126
+ inputHandler.setValue(e.target.value);
127
+ inputHandler.setIdle(false);
128
+ if (onChange) onChange(e.target.value);
129
+ }}
130
+ onFocus={(e) => {
131
+ props.onFocus?.(e);
132
+ inputHandler.setFocus(true);
133
+ }}
134
+ onBlur={(e) => {
135
+ props.onBlur?.(e);
136
+ setTimeout(() => inputHandler.setFocus(false), 100);
137
+ }}
138
+ className={cn(
139
+ "input textarea min-h-[80px] py-2",
140
+ leftIcon && "input-with-left-icon",
141
+ rightIcon && "input-with-right-icon",
142
+ props.disabled && "input-disabled",
143
+ !!invalidMessage && "input-error",
144
+ pcn<CT>(className, "base"),
145
+ !!invalidMessage && pcn<CT>(className, "base", "error"),
146
+ )}
147
+ />
148
+
149
+ {leftIcon && (
150
+ <Icon
151
+ className={cn(
152
+ "input-icon",
153
+ "input-icon-left",
154
+ "top-3 -translate-y-0",
155
+ props.disabled && "input-icon-disabled",
156
+ inputHandler.focus && "input-icon-focus",
157
+ pcn<CT>(className, "icon"),
158
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
159
+ inputHandler.focus && pcn<CT>(className, "icon", "focus"),
160
+ )}
161
+ icon={leftIcon}
162
+ />
163
+ )}
164
+
165
+ {rightIcon && (
166
+ <Icon
167
+ className={cn(
168
+ "input-icon",
169
+ "input-icon-right",
170
+ "top-3 -translate-y-0",
171
+ props.disabled && "input-icon-disabled",
172
+ inputHandler.focus && "input-icon-focus",
173
+ pcn<CT>(className, "icon"),
174
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
175
+ inputHandler.focus && pcn<CT>(className, "icon", "focus"),
176
+ )}
177
+ icon={rightIcon}
178
+ />
179
+ )}
180
+ </div>
181
+
182
+ {invalidMessage && (
183
+ <small
184
+ className={cn(
185
+ "input-error-message",
186
+ pcn<CT>(className, "error"),
187
+ )}
188
+ >
189
+ {invalidMessage}
190
+ </small>
191
+ )}
192
+ </div>
193
+ );
194
+ }
@@ -0,0 +1,241 @@
1
+ import { useMemo } from "react";
2
+ import { cn } from "@utils";
3
+
4
+ export interface ContentWrapperProps {
5
+ content : string;
6
+ className ?: string;
7
+ }
8
+
9
+ export const COLOR_MAP: Record<string, { label: string; tw: string; css: string }> = {
10
+ normal: { label: "Normal", tw: "text-foreground", css: "var(--color-foreground, #1f2937)" },
11
+ light: { label: "Light", tw: "text-light-foreground", css: "var(--color-light-foreground, #9ca3af)" },
12
+ primary: { label: "Primary", tw: "text-primary", css: "var(--color-primary, #3b82f6)" },
13
+ secondary: { label: "Secondary", tw: "text-secondary", css: "var(--color-secondary, #8b5cf6)" },
14
+ warning: { label: "Warning", tw: "text-warning", css: "var(--color-warning, #f59e0b)" },
15
+ danger: { label: "Danger", tw: "text-danger", css: "var(--color-danger, #ef4444)" },
16
+ };
17
+
18
+ export function parseInlineFormats(text: string): string {
19
+ if (!text) return "";
20
+ let result = text;
21
+
22
+ result = result.replace(/\[color:(\w+)\](.*?)\[color\]/g, (_, color, content) => {
23
+ const colorInfo = COLOR_MAP[color];
24
+ const twClass = colorInfo?.tw || "text-foreground";
25
+ return `<span class="${twClass}" data-color="${color}">${content}</span>`;
26
+ });
27
+
28
+ result = result.replace(/\[size:(\d+)\](.*?)\[size\]/g, (_, size, content) => {
29
+ return `<span style="font-size:${size}px" data-size="${size}">${content}</span>`;
30
+ });
31
+
32
+ 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>`;
34
+ });
35
+
36
+ result = result.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
37
+ result = result.replace(/\/\/(.*?)\/\//g, "<em>$1</em>");
38
+ result = result.replace(/__(.*?)__/g, "<u>$1</u>");
39
+ result = result.replace(/--(.*?)--/g, "<s>$1</s>");
40
+
41
+ return result;
42
+ }
43
+
44
+ export function parseContentToHtml(content: string): string {
45
+ if (!content) return "";
46
+
47
+ const lines = content.split("\n");
48
+ const htmlParts: string[] = [];
49
+ let i = 0;
50
+
51
+ while (i < lines.length) {
52
+ const line = lines[i];
53
+
54
+ if (line.trim() === "---") {
55
+ htmlParts.push('<div class="skcontent-divider" contenteditable="false"><hr /></div>');
56
+ i++;
57
+ continue;
58
+ }
59
+
60
+ const headerMatch = line.match(/^##(.+?)##$/);
61
+ if (headerMatch) {
62
+ htmlParts.push(`<h2>${parseInlineFormats(headerMatch[1])}</h2>`);
63
+ i++;
64
+ continue;
65
+ }
66
+
67
+ if (line.match(/^\[list:bullet\]/)) {
68
+ const items: string[] = [];
69
+ while (i < lines.length) {
70
+ const bm = lines[i].match(/^\[list:bullet\](.*?)\[list\]$/);
71
+ if (!bm) break;
72
+ items.push(`<li>${parseInlineFormats(bm[1])}</li>`);
73
+ i++;
74
+ }
75
+ htmlParts.push(`<ul>${items.join("")}</ul>`);
76
+ continue;
77
+ }
78
+
79
+ if (line.match(/^\[list:number\]/)) {
80
+ const items: string[] = [];
81
+ while (i < lines.length) {
82
+ const nm = lines[i].match(/^\[list:number\](.*?)\[list\]$/);
83
+ if (!nm) break;
84
+ items.push(`<li>${parseInlineFormats(nm[1])}</li>`);
85
+ i++;
86
+ }
87
+ htmlParts.push(`<ol>${items.join("")}</ol>`);
88
+ continue;
89
+ }
90
+
91
+ const alignMatch = line.match(/^\[align:(left|center|right|justify)\](.*?)\[align\]$/);
92
+ if (alignMatch) {
93
+ const alignVal = alignMatch[1];
94
+ const alignStyle = alignVal === "left" ? "text-align:left" : alignVal === "center" ? "text-align:center" : alignVal === "right" ? "text-align:right" : "text-align:justify";
95
+ htmlParts.push(`<p style="${alignStyle}" class="text-${alignVal}">${parseInlineFormats(alignMatch[2])}</p>`);
96
+ i++;
97
+ continue;
98
+ }
99
+
100
+ if (line.trim() === "") {
101
+ htmlParts.push("<p><br></p>");
102
+ i++;
103
+ continue;
104
+ }
105
+
106
+ htmlParts.push(`<p>${parseInlineFormats(line)}</p>`);
107
+ i++;
108
+ }
109
+
110
+ return htmlParts.join("");
111
+ }
112
+
113
+ export function parseHtmlToContent(html: string): string {
114
+ if (!html) return "";
115
+
116
+ const parser = new DOMParser();
117
+ const doc = parser.parseFromString(`<div>${html}</div>`, "text/html");
118
+ const root = doc.body.firstElementChild;
119
+ if (!root) return "";
120
+
121
+ const lines: string[] = [];
122
+
123
+ for (let i = 0; i < root.childNodes.length; i++) {
124
+ const node = root.childNodes[i];
125
+
126
+ if (node.nodeType === Node.TEXT_NODE) {
127
+ const text = node.textContent?.trim();
128
+ if (text) lines.push(parseNodeToCustom(node));
129
+ continue;
130
+ }
131
+
132
+ if (node.nodeType !== Node.ELEMENT_NODE) continue;
133
+ const el = node as HTMLElement;
134
+ const tag = el.tagName.toLowerCase();
135
+
136
+ if (el.classList.contains("skcontent-divider") || tag === "hr") {
137
+ lines.push("---");
138
+ continue;
139
+ }
140
+
141
+ if (tag === "h2") {
142
+ lines.push(`##${parseChildrenToCustom(el)}##`);
143
+ continue;
144
+ }
145
+
146
+ if (tag === "ul") {
147
+ for (let j = 0; j < el.children.length; j++) {
148
+ const li = el.children[j];
149
+ lines.push(`[list:bullet]${parseChildrenToCustom(li as HTMLElement)}[list]`);
150
+ }
151
+ continue;
152
+ }
153
+
154
+ if (tag === "ol") {
155
+ for (let j = 0; j < el.children.length; j++) {
156
+ const li = el.children[j];
157
+ lines.push(`[list:number]${parseChildrenToCustom(li as HTMLElement)}[list]`);
158
+ }
159
+ continue;
160
+ }
161
+
162
+ if (tag === "p" || tag === "div") {
163
+ const align = el.style.textAlign || (el.classList.contains("text-center") ? "center" : el.classList.contains("text-right") ? "right" : el.classList.contains("text-justify") ? "justify" : "");
164
+ const content = parseChildrenToCustom(el);
165
+
166
+ if (!content || content === "\n" || el.innerHTML === "<br>" || el.innerHTML === "<br/>") {
167
+ lines.push("");
168
+ continue;
169
+ }
170
+
171
+ if (align && align !== "left" && align !== "start") {
172
+ lines.push(`[align:${align}]${content}[align]`);
173
+ } else {
174
+ lines.push(content);
175
+ }
176
+ continue;
177
+ }
178
+
179
+ const fallback = parseChildrenToCustom(el);
180
+ if (fallback) lines.push(fallback);
181
+ }
182
+
183
+ return lines.join("\n");
184
+ }
185
+
186
+ function parseChildrenToCustom(el: HTMLElement): string {
187
+ let result = "";
188
+ for (let i = 0; i < el.childNodes.length; i++) {
189
+ result += parseNodeToCustom(el.childNodes[i]);
190
+ }
191
+ return result;
192
+ }
193
+
194
+ function parseNodeToCustom(node: Node): string {
195
+ if (node.nodeType === Node.TEXT_NODE) {
196
+ return (node.textContent || "").replace(/\u200B/g, "");
197
+ }
198
+
199
+ if (node.nodeType !== Node.ELEMENT_NODE) return "";
200
+
201
+ const el = node as HTMLElement;
202
+ const tag = el.tagName.toLowerCase();
203
+ const inner = parseChildrenToCustom(el);
204
+
205
+ if (tag === "strong" || tag === "b") return `**${inner}**`;
206
+ if (tag === "em" || tag === "i") return `//${inner}//`;
207
+ if (tag === "u") return `__${inner}__`;
208
+ if (tag === "s" || tag === "strike" || tag === "del") return `--${inner}--`;
209
+
210
+ if (tag === "span" && el.dataset.color) {
211
+ return `[color:${el.dataset.color}]${inner}[color]`;
212
+ }
213
+
214
+ if (tag === "span" && el.dataset.size) {
215
+ return `[size:${el.dataset.size}]${inner}[size]`;
216
+ }
217
+
218
+ if (tag === "a") {
219
+ const href = el.getAttribute("href") || el.dataset.link || "";
220
+ return `[link:${href}]${inner}[link]`;
221
+ }
222
+
223
+ if (tag === "br") return "";
224
+ if (tag === "hr") return "---";
225
+
226
+ return inner;
227
+ }
228
+
229
+ export function ContentWrapperComponent({
230
+ content,
231
+ className,
232
+ }: ContentWrapperProps) {
233
+ const renderedHtml = useMemo(() => parseContentToHtml(content), [content]);
234
+
235
+ return (
236
+ <div
237
+ className={cn("skcontent-wrapper", className)}
238
+ dangerouslySetInnerHTML={{ __html: renderedHtml }}
239
+ />
240
+ );
241
+ }