@ramesesinc/platform-core 0.1.6 → 0.1.8

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.
Files changed (87) hide show
  1. package/dist/components/action/LookupPage.js +9 -31
  2. package/dist/components/action/ViewPage.d.ts +2 -0
  3. package/dist/components/action/ViewPage.js +25 -31
  4. package/dist/components/common/UIComponent.js +4 -3
  5. package/dist/components/table/DataList.js +2 -2
  6. package/dist/components/view/PopupView.d.ts +13 -0
  7. package/dist/components/view/PopupView.js +25 -20
  8. package/dist/core/DataContext.d.ts +7 -4
  9. package/dist/core/DataContext.js +16 -4
  10. package/dist/core/Page.js +25 -26
  11. package/dist/core/PageCache.js +7 -7
  12. package/dist/core/PageContext.js +17 -7
  13. package/dist/core/PageViewContext.d.ts +13 -1
  14. package/dist/core/PageViewContext.js +75 -2
  15. package/dist/core/PopupContext.d.ts +49 -0
  16. package/dist/core/PopupContext.js +380 -0
  17. package/dist/core/RowContext.js +1 -1
  18. package/dist/core/WindowContext.d.ts +15 -0
  19. package/dist/core/WindowContext.js +28 -0
  20. package/dist/core/index.d.ts +16 -0
  21. package/dist/index.css +25 -7
  22. package/dist/lib/utils/BeanUtils.js +7 -7
  23. package/dist/templates/DataListTemplate.js +7 -2
  24. package/dist/templates/ExplorerTemplate.js +1 -1
  25. package/package.json +5 -5
  26. package/dist/components/action/AlertMessage.tsx +0 -38
  27. package/dist/components/action/Button.tsx +0 -230
  28. package/dist/components/action/CancelEdit.tsx +0 -40
  29. package/dist/components/action/DeleteData.tsx +0 -73
  30. package/dist/components/action/Edit.tsx +0 -40
  31. package/dist/components/action/LookupPage.tsx +0 -113
  32. package/dist/components/action/ProcessRunner.tsx +0 -337
  33. package/dist/components/action/Refresh.tsx +0 -35
  34. package/dist/components/action/SaveData.tsx +0 -74
  35. package/dist/components/action/SelectData.tsx +0 -47
  36. package/dist/components/action/Undo.tsx +0 -50
  37. package/dist/components/action/UpdateData.tsx +0 -49
  38. package/dist/components/action/UpdateState.tsx +0 -40
  39. package/dist/components/action/ViewBackPage.tsx +0 -46
  40. package/dist/components/action/ViewPage.tsx +0 -141
  41. package/dist/components/common/UIComponent.tsx +0 -86
  42. package/dist/components/common/UIInput.tsx +0 -49
  43. package/dist/components/common/UIMenu.tsx +0 -91
  44. package/dist/components/index.ts +0 -51
  45. package/dist/components/input/CodeEditor.tsx +0 -188
  46. package/dist/components/input/DateField.tsx +0 -274
  47. package/dist/components/input/DayPicker.tsx +0 -5
  48. package/dist/components/input/HtmlCode.tsx +0 -203
  49. package/dist/components/input/JsonCode.tsx +0 -205
  50. package/dist/components/input/MonthPicker.tsx +0 -5
  51. package/dist/components/input/ScriptCode.tsx +0 -195
  52. package/dist/components/input/Select.tsx +0 -78
  53. package/dist/components/input/SqlCode.tsx +0 -162
  54. package/dist/components/input/StringDecision.tsx +0 -64
  55. package/dist/components/input/Text.tsx +0 -57
  56. package/dist/components/input/YearPicker.tsx +0 -81
  57. package/dist/components/list/IconMenu.tsx +0 -115
  58. package/dist/components/list/TabMenu.tsx +0 -127
  59. package/dist/components/list/TreeMenu.tsx +0 -279
  60. package/dist/components/list/TxnTaskList.tsx +0 -198
  61. package/dist/components/output/Label.tsx +0 -50
  62. package/dist/components/table/DataList.tsx +0 -820
  63. package/dist/components/table/DataTable.tsx +0 -572
  64. package/dist/components/table/ListHandler.ts +0 -276
  65. package/dist/components/table/TableContext.tsx +0 -122
  66. package/dist/components/view/ComponentView.tsx +0 -102
  67. package/dist/components/view/FilterView.tsx +0 -21
  68. package/dist/components/view/HtmlForm.tsx +0 -176
  69. package/dist/components/view/HtmlView.tsx +0 -98
  70. package/dist/components/view/IFrameView.tsx +0 -48
  71. package/dist/components/view/Modal.tsx +0 -72
  72. package/dist/components/view/PageView.tsx +0 -131
  73. package/dist/components/view/PopupView.tsx +0 -160
  74. package/dist/components/view/RootView.tsx +0 -109
  75. package/dist/components/view/WizardView.tsx +0 -48
  76. package/dist/lib/layouts/BorderLayout.tsx +0 -31
  77. package/dist/lib/layouts/CardLayout.tsx +0 -73
  78. package/dist/lib/layouts/CenterLayout.tsx +0 -20
  79. package/dist/lib/layouts/GridLayout.tsx +0 -20
  80. package/dist/lib/layouts/HPanel.tsx +0 -31
  81. package/dist/lib/layouts/HorizontalLayout.tsx +0 -29
  82. package/dist/lib/layouts/MainLayout.tsx +0 -16
  83. package/dist/lib/layouts/PageLayout.tsx +0 -29
  84. package/dist/lib/layouts/VPanel.tsx +0 -27
  85. package/dist/lib/layouts/XLayout.tsx +0 -29
  86. package/dist/lib/layouts/YLayout.tsx +0 -29
  87. package/dist/lib/layouts/index.ts +0 -13
@@ -1,188 +0,0 @@
1
- import { useRef, useState, useCallback, useEffect } from "react";
2
- import Editor from "@monaco-editor/react";
3
- import UIComponent from "../common/UIComponent";
4
- import useUIInput, { UIInputProps } from "../common/UIInput";
5
- //import CopyButton from "../ui/CopyButton";
6
-
7
- type CodeEditorType = "sql" | "json" | "java" | "javascript" | "html" | "css" | "typescript" | "xml" | "yaml" | "markdown" | "plaintext";
8
-
9
- const languageMap: Record<CodeEditorType, string> = {
10
- sql: "sql",
11
- json: "json",
12
- java: "groovy",
13
- javascript: "javascript",
14
- html: "html",
15
- css: "css",
16
- typescript: "typescript",
17
- xml: "xml",
18
- yaml: "yaml",
19
- markdown: "markdown",
20
- plaintext: "plaintext",
21
- };
22
-
23
- type CodeEditorProps = UIInputProps & {
24
- type?: CodeEditorType;
25
- height?: number | string;
26
- width?: number | string;
27
- readOnly?: boolean;
28
- immediate?: boolean;
29
- showCopy?: boolean;
30
- };
31
-
32
- const CodeEditor = (props: CodeEditorProps) => {
33
- const {
34
- type = "plaintext",
35
- height = 300,
36
- width = "100%",
37
- readOnly = false,
38
- immediate = true,
39
- showCopy = true,
40
- } = props ?? {};
41
-
42
- const editorRef = useRef<any>(null);
43
- const valueRef = useRef<string>("");
44
- const [fontSize, setFontSize] = useState(14);
45
- const [error, setError] = useState("");
46
-
47
- const language = languageMap[type] ?? type;
48
-
49
- const onRefresh = () => {
50
- setEditorValue(getValue());
51
- };
52
-
53
- const { initialValue, getValue, setValue } = useUIInput({ ...props, onRefresh });
54
- valueRef.current = initialValue ?? "";
55
- const [editorValue, setEditorValue] = useState(valueRef.current);
56
-
57
- // Font size keyboard shortcuts
58
- const handleKeyDown = useCallback((e: KeyboardEvent) => {
59
- if (e.ctrlKey && e.key === "=") {
60
- e.preventDefault();
61
- setFontSize((prev) => Math.min(prev + 1, 40));
62
- }
63
- if (e.ctrlKey && e.key === "-") {
64
- e.preventDefault();
65
- setFontSize((prev) => Math.max(prev - 1, 8));
66
- }
67
- if (e.ctrlKey && e.key === "0") {
68
- e.preventDefault();
69
- setFontSize(14);
70
- }
71
- }, []);
72
-
73
- useEffect(() => {
74
- window.addEventListener("keydown", handleKeyDown);
75
- return () => window.removeEventListener("keydown", handleKeyDown);
76
- }, [handleKeyDown]);
77
-
78
- // JSON-specific validation
79
- const validateJson = (value: string) => {
80
- if (!value.trim()) {
81
- setError("");
82
- return;
83
- }
84
- try {
85
- JSON.parse(value);
86
- setError("");
87
- } catch (e: any) {
88
- setError(e.message);
89
- }
90
- };
91
-
92
- const handleEditorChange = (value?: string) => {
93
- const safeValue = value ?? "";
94
- setEditorValue(safeValue);
95
-
96
- if (type === "json") {
97
- validateJson(safeValue);
98
- }
99
-
100
- if (immediate) {
101
- setValue(safeValue);
102
- }
103
- };
104
-
105
- const handleEditorDidMount = (editor: any, monaco: any) => {
106
- editorRef.current = editor;
107
-
108
- if (type === "json") {
109
- monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
110
- validate: true,
111
- allowComments: false,
112
- schemas: [],
113
- enableSchemaRequest: true,
114
- });
115
- }
116
-
117
- editor.updateOptions({
118
- minimap: { enabled: false },
119
- fontSize,
120
- lineNumbers: "on",
121
- renderWhitespace: "selection",
122
- folding: true,
123
- bracketPairColorization: { enabled: true },
124
- formatOnPaste: true,
125
- formatOnType: true,
126
- });
127
- };
128
-
129
- // Sync font size if editor already mounted
130
- useEffect(() => {
131
- editorRef.current?.updateOptions({ fontSize });
132
- }, [fontSize]);
133
-
134
- /*
135
- //<CopyButton
136
- // item={editorValue}
137
- // copySize={15}
138
- // copiedSize={10}
139
- // classNameCopied="!px-[15px] !py-[10px]"
140
- ///>
141
- */
142
-
143
- return (
144
- <UIComponent {...(props ?? {})}>
145
- <div className="relative w-full bg-white">
146
- {showCopy && (
147
- <div className="absolute top-2 right-4 z-50">
148
- </div>
149
- )}
150
- <Editor
151
- height={height}
152
- width={width}
153
- language={language}
154
- value={editorValue}
155
- onChange={handleEditorChange}
156
- onMount={handleEditorDidMount}
157
- theme="vs-dark"
158
- options={{
159
- readOnly,
160
- domReadOnly: readOnly,
161
- padding: { top: 10 },
162
- selectOnLineNumbers: true,
163
- automaticLayout: true,
164
- minimap: { enabled: false },
165
- fontSize,
166
- wordWrap: "off",
167
- lineNumbers: "on",
168
- renderWhitespace: "selection",
169
- folding: true,
170
- bracketPairColorization: { enabled: true },
171
- formatOnPaste: true,
172
- formatOnType: true,
173
- scrollBeyondLastLine: false,
174
- smoothScrolling: true,
175
- cursorBlinking: "smooth",
176
- renderLineHighlight: "all",
177
- scrollbar: { vertical: "visible", horizontal: "visible" },
178
- }}
179
- />
180
- {error && type === "json" && (
181
- <p className="text-red-500 text-xs mt-1 px-1">{error}</p>
182
- )}
183
- </div>
184
- </UIComponent>
185
- );
186
- };
187
-
188
- export default CodeEditor;
@@ -1,274 +0,0 @@
1
- import { useRef, useState } from "react";
2
- import UIComponent from "../common/UIComponent";
3
- import useUIInput, { UIInputProps } from "../common/UIInput";
4
-
5
- type DateProps = UIInputProps & {
6
- required?: boolean;
7
- immediate?: boolean;
8
- min?: string; // ISO date string
9
- max?: string; // ISO date string
10
- };
11
-
12
- const DateField = (props: DateProps) => {
13
- const { name, immediate = true, min, max } = props ?? {};
14
- const [focused, setFocused] = useState(false);
15
- const [showCalendar, setShowCalendar] = useState(false);
16
- const inputRef = useRef<HTMLInputElement | null>(null);
17
- const calendarRef = useRef<HTMLDivElement | null>(null);
18
- const valueRef = useRef<string>("");
19
- const className = "border rounded px-2 py-1 w-full";
20
-
21
- const handleFocus = () => {
22
- setFocused(true);
23
- };
24
-
25
- const handleBlur = () => {
26
- if (!immediate) {
27
- setValue(inputValue);
28
- }
29
- setFocused(false);
30
- };
31
-
32
- const onRefresh = () => {
33
- setInputValue(getValue());
34
- };
35
-
36
- const { initialValue, getValue, setValue, binding } = useUIInput({
37
- ...props,
38
- onRefresh
39
- });
40
-
41
- valueRef.current = initialValue ?? "";
42
- const [inputValue, setInputValue] = useState(valueRef.current);
43
-
44
- // Get current date for calendar navigation - use useMemo or direct calculation
45
- const getCurrentDate = () => {
46
- if (inputValue && inputValue.match(/^\d{4}-\d{2}-\d{2}$/)) {
47
- // Parse ISO date string as local date to avoid timezone issues
48
- const parts = inputValue.split('-');
49
- const year = parseInt(parts[0], 10);
50
- const month = parseInt(parts[1], 10) - 1; // Month is 0-indexed
51
- const day = parseInt(parts[2], 10);
52
- return new globalThis.Date(year, month, day);
53
- }
54
- return new globalThis.Date();
55
- };
56
-
57
- const [calendarDate, setCalendarDate] = useState(() => getCurrentDate());
58
-
59
- const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
60
- const text = e.target.value ?? "";
61
-
62
- if (text !== inputValue) {
63
- valueRef.current = text;
64
- setInputValue(text);
65
-
66
- // Update calendar date when user types a valid date
67
- if (text && text.match(/^\d{4}-\d{2}-\d{2}$/)) {
68
- const parts = text.split('-');
69
- const year = parseInt(parts[0], 10);
70
- const month = parseInt(parts[1], 10) - 1;
71
- const day = parseInt(parts[2], 10);
72
- setCalendarDate(new globalThis.Date(year, month, day));
73
- }
74
- }
75
-
76
- if (immediate) {
77
- setValue(text);
78
- }
79
- };
80
-
81
- const toggleCalendar = () => {
82
- setShowCalendar(!showCalendar);
83
- if (!showCalendar) {
84
- setCalendarDate(getCurrentDate());
85
- }
86
- };
87
-
88
- const selectDate = (date: globalThis.Date) => {
89
- const dateString = date.toISOString().split("T")[0];
90
- valueRef.current = dateString;
91
- setInputValue(dateString);
92
- setValue(dateString);
93
- setShowCalendar(false);
94
- };
95
-
96
- const previousMonth = () => {
97
- const newDate = new globalThis.Date(calendarDate);
98
- newDate.setMonth(newDate.getMonth() - 1);
99
- setCalendarDate(newDate);
100
- };
101
-
102
- const nextMonth = () => {
103
- const newDate = new globalThis.Date(calendarDate);
104
- newDate.setMonth(newDate.getMonth() + 1);
105
- setCalendarDate(newDate);
106
- };
107
-
108
- // Generate calendar days
109
- const generateCalendarDays = () => {
110
- const year = calendarDate.getFullYear();
111
- const month = calendarDate.getMonth();
112
-
113
- const firstDay = new globalThis.Date(year, month, 1);
114
- const lastDay = new globalThis.Date(year, month + 1, 0);
115
- const startingDayOfWeek = firstDay.getDay();
116
- const daysInMonth = lastDay.getDate();
117
-
118
- const days: (globalThis.Date | null)[] = [];
119
-
120
- // Add empty cells for days before month starts
121
- for (let i = 0; i < startingDayOfWeek; i++) {
122
- days.push(null);
123
- }
124
-
125
- // Add days of month
126
- for (let day = 1; day <= daysInMonth; day++) {
127
- days.push(new globalThis.Date(year, month, day));
128
- }
129
-
130
- return days;
131
- };
132
-
133
- const isSelectedDate = (date: globalThis.Date) => {
134
- if (!inputValue || !inputValue.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
135
- // Parse ISO date string as local date
136
- const parts = inputValue.split('-');
137
- const selectedYear = parseInt(parts[0], 10);
138
- const selectedMonth = parseInt(parts[1], 10) - 1;
139
- const selectedDay = parseInt(parts[2], 10);
140
-
141
- return (
142
- date.getDate() === selectedDay &&
143
- date.getMonth() === selectedMonth &&
144
- date.getFullYear() === selectedYear
145
- );
146
- };
147
-
148
- const isToday = (date: globalThis.Date) => {
149
- const today = new globalThis.Date();
150
- return (
151
- date.getDate() === today.getDate() &&
152
- date.getMonth() === today.getMonth() &&
153
- date.getFullYear() === today.getFullYear()
154
- );
155
- };
156
-
157
- const monthNames = [
158
- "January", "February", "March", "April", "May", "June",
159
- "July", "August", "September", "October", "November", "December"
160
- ];
161
-
162
- const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
163
-
164
- const calendarDays = generateCalendarDays();
165
-
166
- return (
167
- <UIComponent {...(props ?? {})}>
168
- <div className="relative">
169
- <div className="flex gap-1">
170
- <input
171
- type="date"
172
- ref={inputRef}
173
- onChange={onChange}
174
- value={inputValue}
175
- onFocus={handleFocus}
176
- onBlur={handleBlur}
177
- className={className}
178
- min={min}
179
- max={max}
180
- />
181
- <button
182
- type="button"
183
- onClick={toggleCalendar}
184
- className="border rounded px-3 py-1 bg-white hover:bg-gray-50"
185
- >
186
- 📅
187
- </button>
188
- </div>
189
-
190
- {showCalendar && (
191
- <div
192
- ref={calendarRef}
193
- className="absolute z-10 mt-1 bg-white border rounded shadow-lg p-4"
194
- style={{ minWidth: "280px" }}
195
- >
196
- {/* Calendar Header */}
197
- <div className="flex justify-between items-center mb-4">
198
- <button
199
- type="button"
200
- onClick={previousMonth}
201
- className="px-2 py-1 hover:bg-gray-100 rounded"
202
- >
203
-
204
- </button>
205
- <div className="font-semibold">
206
- {monthNames[calendarDate.getMonth()]} {calendarDate.getFullYear()}
207
- </div>
208
- <button
209
- type="button"
210
- onClick={nextMonth}
211
- className="px-2 py-1 hover:bg-gray-100 rounded"
212
- >
213
-
214
- </button>
215
- </div>
216
-
217
- {/* Day Names */}
218
- <div className="grid grid-cols-7 gap-1 mb-2">
219
- {dayNames.map((day) => (
220
- <div
221
- key={day}
222
- className="text-center text-xs font-medium text-gray-600 py-1"
223
- >
224
- {day}
225
- </div>
226
- ))}
227
- </div>
228
-
229
- {/* Calendar Days */}
230
- <div className="grid grid-cols-7 gap-1">
231
- {calendarDays.map((date, index) => {
232
- if (!date) {
233
- return <div key={index} className="aspect-square" />;
234
- }
235
-
236
- const selected = isSelectedDate(date);
237
- const today = isToday(date);
238
-
239
- return (
240
- <button
241
- key={index}
242
- type="button"
243
- onClick={() => selectDate(date)}
244
- className={`
245
- aspect-square flex items-center justify-center rounded text-sm
246
- ${selected ? "bg-blue-500 text-white font-semibold" : ""}
247
- ${today && !selected ? "border-2 border-blue-500" : ""}
248
- ${!selected && !today ? "hover:bg-gray-100" : ""}
249
- `}
250
- >
251
- {date.getDate()}
252
- </button>
253
- );
254
- })}
255
- </div>
256
-
257
- {/* Close Button */}
258
- <div className="mt-4 flex justify-end">
259
- <button
260
- type="button"
261
- onClick={() => setShowCalendar(false)}
262
- className="px-3 py-1 text-sm border rounded hover:bg-gray-50"
263
- >
264
- Close
265
- </button>
266
- </div>
267
- </div>
268
- )}
269
- </div>
270
- </UIComponent>
271
- );
272
- };
273
-
274
- export default DateField;
@@ -1,5 +0,0 @@
1
- const DayPicker = () => {
2
- return <div>DayPicker</div>;
3
- };
4
-
5
- export default DayPicker;
@@ -1,203 +0,0 @@
1
- import Editor from "@monaco-editor/react";
2
- import { useCallback, useEffect, useRef, useState } from "react";
3
- import { usePageContext } from "../../core/PageContext";
4
- import CopyButton from "../../lib/components/CopyButton";
5
- import UIComponent from "../common/UIComponent";
6
- import useUIInput, { UIInputProps } from "../common/UIInput";
7
-
8
- type HtmlCodeProps = UIInputProps & {
9
- height?: number | string;
10
- width?: number | string;
11
- showCopy?: boolean;
12
- };
13
-
14
- const HtmlCode = (props: HtmlCodeProps) => {
15
- const { height = 300, width = "100%", showCopy = true } = props ?? {};
16
-
17
- const pageContext = usePageContext();
18
- const editorRef = useRef<any>(null);
19
- const [fontSize, setFontSize] = useState(14);
20
- const [error, setError] = useState("");
21
- const [readOnly, setReadOnly] = useState(true);
22
-
23
- const onRefresh = () => {
24
- const val = getValue();
25
- setEditorValue(val ?? "");
26
- };
27
-
28
- const { initialValue, getValue, setValue } = useUIInput({ ...props, onRefresh });
29
-
30
- const [editorValue, setEditorValue] = useState(() => initialValue ?? "");
31
-
32
- useEffect(() => {
33
- const unsubscribe = pageContext?.dependsTo("editable", (val: any) => {
34
- const isEditable = val === true || val === "true";
35
- setReadOnly(!isEditable);
36
-
37
- if (!isEditable) {
38
- setEditorValue(getValue() ?? "");
39
- setError("");
40
- }
41
- });
42
- return unsubscribe;
43
- }, []);
44
-
45
- useEffect(() => {
46
- return () => {
47
- editorRef.current = null;
48
- };
49
- }, []);
50
-
51
- const handleKeyDown = useCallback((e: KeyboardEvent) => {
52
- if (e.ctrlKey && e.key === "=") {
53
- e.preventDefault();
54
- setFontSize((prev) => Math.min(prev + 1, 40));
55
- }
56
- if (e.ctrlKey && e.key === "-") {
57
- e.preventDefault();
58
- setFontSize((prev) => Math.max(prev - 1, 8));
59
- }
60
- if (e.ctrlKey && e.key === "0") {
61
- e.preventDefault();
62
- setFontSize(14);
63
- }
64
- }, []);
65
-
66
- useEffect(() => {
67
- window.addEventListener("keydown", handleKeyDown);
68
- return () => window.removeEventListener("keydown", handleKeyDown);
69
- }, [handleKeyDown]);
70
-
71
- useEffect(() => {
72
- editorRef.current?.updateOptions({ fontSize });
73
- }, [fontSize]);
74
-
75
- useEffect(() => {
76
- editorRef.current?.updateOptions({ readOnly, domReadOnly: readOnly });
77
- }, [readOnly]);
78
-
79
- const validateHtml = (code: string) => {
80
- if (!code.trim()) {
81
- setError("");
82
- return;
83
- }
84
-
85
- const errors: string[] = [];
86
- const lines = code.split("\n");
87
- const tagStack: { tagName: string; line: number }[] = [];
88
- const tagRegex = /<\/?([a-zA-Z0-9-]+)(\s[^>]*)?>/g;
89
-
90
- for (let i = 0; i < lines.length; i++) {
91
- const line = lines[i];
92
- const matches = [...line.matchAll(tagRegex)];
93
-
94
- for (const match of matches) {
95
- const tagName = match[1].toLowerCase();
96
- const isClosing = match[0].startsWith("</");
97
- const selfClosing = /\/>$/.test(match[0]) || ["br", "hr", "img", "input", "meta", "link"].includes(tagName);
98
-
99
- if (!isClosing && !selfClosing) {
100
- tagStack.push({ tagName, line: i + 1 });
101
- } else if (isClosing) {
102
- const lastTag = tagStack.pop();
103
- if (!lastTag || lastTag.tagName !== tagName) {
104
- errors.push(`Line ${i + 1}: Unmatched closing tag </${tagName}>`);
105
- }
106
- }
107
- }
108
-
109
- if (i === 0 && !line.toLowerCase().includes("<!doctype")) {
110
- if (code.includes("<html")) {
111
- errors.push("Missing <!DOCTYPE html> declaration at the top");
112
- }
113
- }
114
-
115
- if (i === lines.length - 1 && code.includes("<html")) {
116
- if (!code.includes("<head")) errors.push("Missing <head> tag");
117
- if (!code.includes("<body")) errors.push("Missing <body> tag");
118
- }
119
- }
120
-
121
- if (tagStack.length > 0) {
122
- const unclosed = tagStack.map((t) => `<${t.tagName}> (opened on line ${t.line})`).join(", ");
123
- errors.push(`Unclosed tag(s): ${unclosed}`);
124
- }
125
-
126
- setError(errors.length > 0 ? errors.join("\n• ") : "");
127
- };
128
-
129
- const handleEditorChange = (value?: string) => {
130
- if (readOnly) return;
131
- const safeValue = value ?? "";
132
- setEditorValue(safeValue);
133
- setValue(safeValue); // write through immediately — HTML is always a string
134
- validateHtml(safeValue);
135
- };
136
-
137
- const handleEditorDidMount = (editor: any, monaco: any) => {
138
- editorRef.current = editor;
139
-
140
- editor.updateOptions({
141
- minimap: { enabled: false },
142
- fontSize,
143
- lineNumbers: "on",
144
- renderWhitespace: "selection",
145
- folding: true,
146
- bracketPairColorization: { enabled: true },
147
- formatOnPaste: true,
148
- formatOnType: true,
149
- wordWrap: "on",
150
- autoClosingBrackets: "always",
151
- autoClosingQuotes: "always",
152
- suggestOnTriggerCharacters: true,
153
- });
154
- };
155
-
156
- return (
157
- <UIComponent {...(props ?? {})}>
158
- <div className="relative w-full bg-white">
159
- {showCopy && (
160
- <div className="absolute top-2 right-4 z-50">
161
- <CopyButton item={editorValue} copySize={15} copiedSize={10} classNameCopied="!px-[15px] !py-[10px]" />
162
- </div>
163
- )}
164
- <Editor
165
- key="html-code-editor"
166
- height={height}
167
- width={width}
168
- language="html"
169
- value={editorValue}
170
- onChange={handleEditorChange}
171
- onMount={handleEditorDidMount}
172
- theme="vs-dark"
173
- options={{
174
- readOnly,
175
- domReadOnly: readOnly,
176
- padding: { top: 10 },
177
- selectOnLineNumbers: true,
178
- automaticLayout: true,
179
- minimap: { enabled: false },
180
- fontSize,
181
- wordWrap: "on",
182
- lineNumbers: "on",
183
- folding: true,
184
- bracketPairColorization: { enabled: true },
185
- formatOnPaste: true,
186
- formatOnType: true,
187
- scrollBeyondLastLine: false,
188
- smoothScrolling: true,
189
- cursorBlinking: "smooth",
190
- renderLineHighlight: "all",
191
- autoClosingBrackets: "always",
192
- autoClosingQuotes: "always",
193
- suggestOnTriggerCharacters: true,
194
- scrollbar: { vertical: "visible", horizontal: "visible" },
195
- }}
196
- />
197
- {error && !readOnly && <p className="text-red-500 text-xs mt-1 px-1 whitespace-pre-line">{error}</p>}
198
- </div>
199
- </UIComponent>
200
- );
201
- };
202
-
203
- export default HtmlCode;