neiki-editor 2.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jindřich Stoklasa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,600 @@
1
+ <h1 align="center">Neiki Editor</h1>
2
+
3
+ <p align="center">
4
+ <img src="logo.png" alt="neiki-editor" width="400">
5
+ </p>
6
+
7
+ <p align="center">
8
+ <img src="https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E" alt="JavaScript">
9
+ <img src="https://img.shields.io/badge/php-%23777BB4.svg?style=for-the-badge&logo=php&logoColor=white" alt="PHP">
10
+ <img src="https://img.shields.io/badge/html5-%23E34F26.svg?style=for-the-badge&logo=html5&logoColor=white" alt="HTML5">
11
+ <img src="https://img.shields.io/badge/css-%23663399.svg?style=for-the-badge&logo=css&logoColor=white" alt="CSS">
12
+ <br>
13
+ <img src="https://img.shields.io/badge/License-MIT-2563EB?style=for-the-badge&logo=open-source-initiative&logoColor=white&labelColor=000F15&logoWidth=20" alt="License">
14
+ <img src="https://img.shields.io/badge/Version-2.2.1-2563EB?style=for-the-badge&logo=semantic-release&logoColor=white&labelColor=000F15&logoWidth=20" alt="Version">
15
+ </p>
16
+
17
+ <p align="center">
18
+ <b>Lightweight WYSIWYG Rich Text Editor</b><br>
19
+ <i>Easy to integrate, fully customizable, zero dependencies.</i>
20
+ </p>
21
+
22
+ <p align="center">
23
+ <img src="https://img.shields.io/badge/Features-30%2B%20Tools-3b82f6?style=flat&labelColor=383C43" />
24
+ <img src="https://img.shields.io/badge/Themes-Light%20%26%20Dark-8b5cf6?style=flat&labelColor=383C43" />
25
+ <img src="https://img.shields.io/badge/Setup-Zero%20Config-22c55e?style=flat&labelColor=383C43" />
26
+ <img src="https://img.shields.io/badge/Size-Lightweight-f97316?style=flat&labelColor=383C43" />
27
+ </p>
28
+
29
+ ---
30
+ <p align="center">
31
+ <img src="preview.png" alt="neiki-editor" width="902">
32
+ </p>
33
+
34
+ **Live version:** [https://neiki.eu/editor](https://neiki.eu/editor)
35
+
36
+ ---
37
+ ## 📦 Installation
38
+
39
+ ### CDN (Recommended)
40
+
41
+ ```html
42
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.2.1/dist/neiki-editor.css">
43
+ <script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.2.1/dist/neiki-editor.js"></script>
44
+ ```
45
+
46
+ ### Self-hosted
47
+
48
+ Download `neiki-editor.js` and `neiki-editor.css`, then include them in your project:
49
+
50
+ ```html
51
+ <link rel="stylesheet" href="path/to/neiki-editor.css">
52
+ <script src="path/to/neiki-editor.js"></script>
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 🚀 Quick Start
58
+
59
+ ```html
60
+ <textarea id="editor"></textarea>
61
+
62
+ <script>
63
+ const editor = new NeikiEditor('#editor');
64
+ </script>
65
+ ```
66
+
67
+ That's it — zero config required. The editor replaces the `<textarea>` with a full-featured WYSIWYG editor.
68
+
69
+ ---
70
+
71
+ ## ⚙️ Configuration
72
+
73
+ ```javascript
74
+ const editor = new NeikiEditor('#editor', {
75
+ placeholder: 'Start typing...',
76
+ minHeight: 300,
77
+ maxHeight: 600,
78
+ autofocus: false,
79
+ spellcheck: true,
80
+ readonly: false,
81
+ theme: 'light', // 'light' or 'dark'
82
+ toolbar: [
83
+ 'viewCode', 'undo', 'redo', 'findReplace', '|',
84
+ 'bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'removeFormat', '|',
85
+ 'heading', 'fontFamily', 'fontSize', '|',
86
+ 'foreColor', 'backColor', '|',
87
+ 'alignLeft', 'alignCenter', 'alignRight', 'alignJustify', '|',
88
+ 'indent', 'outdent', '|',
89
+ 'bulletList', 'numberedList', 'blockquote', 'horizontalRule', '|',
90
+ 'insertDropdown', '|',
91
+ 'moreMenu'
92
+ ],
93
+ onChange: function(content, editor) {
94
+ console.log('Content changed:', content);
95
+ },
96
+ onSave: function(content, editor) {
97
+ console.log('Save triggered:', content);
98
+ },
99
+ onReady: function(editor) {
100
+ console.log('Editor is ready!');
101
+ }
102
+ });
103
+ ```
104
+
105
+ ### Configuration Options
106
+
107
+ | Option | Type | Default | Description |
108
+ |--------|------|---------|-------------|
109
+ | `placeholder` | `string` | `'Start typing...'` | Placeholder text when editor is empty |
110
+ | `minHeight` | `number` | `300` | Minimum height in pixels |
111
+ | `maxHeight` | `number\|null` | `null` | Maximum height in pixels (enables scroll) |
112
+ | `autofocus` | `boolean` | `false` | Focus editor on initialization |
113
+ | `spellcheck` | `boolean` | `true` | Enable browser spellcheck |
114
+ | `readonly` | `boolean` | `false` | Make editor read-only |
115
+ | `theme` | `string` | `'light'` | `'light'` or `'dark'` |
116
+ | `language` | `string` | `'en'` | UI language: `'en'` (English) or `'cs'` (Czech) |
117
+ | `toolbar` | `array` | *(see above)* | Toolbar button configuration |
118
+ | `onChange` | `function\|null` | `null` | Callback on content change |
119
+ | `onSave` | `function\|null` | `null` | Callback on save (triggered by Ctrl+S or More menu → Save) |
120
+ | `onFocus` | `function\|null` | `null` | Callback when editor gains focus |
121
+ | `onBlur` | `function\|null` | `null` | Callback when editor loses focus |
122
+ | `onReady` | `function\|null` | `null` | Callback when editor is ready |
123
+
124
+ ---
125
+
126
+ ## 🔧 Toolbar Buttons
127
+
128
+ Use the `toolbar` array to customize which buttons appear and in what order. Use `'|'` for a visual separator between groups. Groups of buttons between separators wrap as whole units on smaller screens.
129
+
130
+ ### Text Formatting
131
+
132
+ | Button | Description |
133
+ |--------|-------------|
134
+ | `bold` | Bold text (**Ctrl+B**) |
135
+ | `italic` | Italic text (**Ctrl+I**) |
136
+ | `underline` | Underline text (**Ctrl+U**) |
137
+ | `strikethrough` | Strikethrough text |
138
+ | `subscript` | Subscript text |
139
+ | `superscript` | Superscript text |
140
+ | `removeFormat` | Remove all formatting |
141
+
142
+ > **Note:** When no text is selected, formatting commands (including Remove Formatting) automatically expand to the word at the cursor position.
143
+
144
+ ### Text Style
145
+
146
+ | Button | Type | Description |
147
+ |--------|------|-------------|
148
+ | `heading` | Select | Paragraph, H1, H2, H3, H4, H5, H6. Defaults to Paragraph. |
149
+ | `fontSize` | Widget | Font size widget with **[−]** / **[+]** buttons, text input, and dropdown presets: 8, 9, 10, 11, 12, 14, 18, 24, 30, 36, 48, 60, 72, 96 |
150
+ | `fontFamily` | Select | Sans Serif (Arial), Serif (Georgia), Monospace (Consolas), Cursive (Comic Sans MS) |
151
+ | `foreColor` | Color Picker | Text color picker with reset option |
152
+ | `backColor` | Color Picker | Background color picker with reset option |
153
+
154
+ ### Alignment & Lists
155
+
156
+ | Button | Description |
157
+ |--------|-------------|
158
+ | `alignLeft` | Align text left |
159
+ | `alignCenter` | Center text |
160
+ | `alignRight` | Align text right |
161
+ | `alignJustify` | Justify text |
162
+ | `bulletList` | Unordered list |
163
+ | `numberedList` | Ordered list |
164
+ | `indent` | Increase indent |
165
+ | `outdent` | Decrease indent |
166
+
167
+ ### Insert Dropdown
168
+
169
+ The `insertDropdown` toolbar item renders a single **Insert** button that opens a dropdown containing:
170
+
171
+ | Item | Description |
172
+ |------|-------------|
173
+ | **Link** | Insert/edit hyperlink (**Ctrl+K**) |
174
+ | **Image** | Insert image (URL or file upload → base64) |
175
+ | **Table** | Insert table with custom rows/columns |
176
+ | **Emoji** | Emoji picker (100+ emojis) |
177
+ | **Symbol** | Special characters (©, ®, €, π, Ω, arrows, etc.) |
178
+
179
+ You can still use `link`, `image`, `table`, `emoji`, `specialChars` as standalone toolbar buttons if preferred.
180
+
181
+ ### More Menu
182
+
183
+ The `moreMenu` toolbar item renders a **⋯** button (pushed to the right) that opens a dropdown containing:
184
+
185
+ | Item | Description |
186
+ |------|-------------|
187
+ | **Save** | Trigger the `onSave` callback |
188
+ | **Preview** | Open a document preview modal |
189
+ | **Download** | Download content as an HTML file |
190
+ | **Print** | Print editor content |
191
+ | **Autosave** | Toggle autosave to localStorage |
192
+ | **Clear all** | Clear all editor content |
193
+ | **Toggle Theme** | Switch between light/dark theme |
194
+ | **Fullscreen** | Toggle fullscreen mode |
195
+
196
+ ### Standalone Tools
197
+
198
+ | Button | Description |
199
+ |--------|-------------|
200
+ | `undo` | Undo (**Ctrl+Z**) |
201
+ | `redo` | Redo (**Ctrl+Y** / **Ctrl+Shift+Z**) |
202
+ | `findReplace` | Find & Replace with regex support |
203
+ | `viewCode` | Toggle HTML source editor |
204
+ | `blockquote` | Block quote |
205
+ | `horizontalRule` | Horizontal line |
206
+
207
+ ---
208
+
209
+ ## 🎨 Themes
210
+
211
+ Neiki Editor ships with **Light** and **Dark** themes.
212
+
213
+ ### Set theme on init:
214
+
215
+ ```javascript
216
+ const editor = new NeikiEditor('#editor', {
217
+ theme: 'dark'
218
+ });
219
+ ```
220
+
221
+ ### Toggle theme at runtime:
222
+
223
+ Use the **Toggle Theme** item in the More menu (⋯), or toggle programmatically:
224
+
225
+ ```javascript
226
+ editor.toggleTheme();
227
+ // or set a specific theme:
228
+ editor.setTheme('dark');
229
+ ```
230
+
231
+ The selected theme persists across page reloads via `localStorage`.
232
+
233
+ ---
234
+
235
+ ## 🌍 Localization (i18n)
236
+
237
+ Neiki Editor supports multiple UI languages. Currently available:
238
+
239
+ - **English** (`en`) — default
240
+ - **Czech** (`cs`)
241
+
242
+ ### Set language on init:
243
+
244
+ ```javascript
245
+ const editor = new NeikiEditor('#editor', {
246
+ language: 'cs' // Czech UI
247
+ });
248
+ ```
249
+
250
+ All toolbar tooltips, modal dialogs, status bar texts, and system messages are translated.
251
+
252
+ ---
253
+
254
+ ## 💾 Autosave
255
+
256
+ Autosave is accessible from the **More menu** (⋯) in the default toolbar. When activated:
257
+
258
+ - Content is saved to `localStorage` on every content change (debounced)
259
+ - The status bar shows "Autosaving..." / "Saved locally"
260
+ - Content is restored on page reload **only when autosave was enabled**
261
+
262
+ > **Note:** For production use (CMS, blog, etc.), use the `onSave` callback or `onChange` callback to save content to your database instead.
263
+
264
+ ---
265
+
266
+ ## 📋 API Methods
267
+
268
+ ### Content
269
+
270
+ ```javascript
271
+ editor.getContent(); // Get HTML content
272
+ editor.setContent('<p>Hello</p>'); // Set HTML content
273
+ editor.getText(); // Get plain text content
274
+ editor.isEmpty(); // Check if editor is empty
275
+
276
+ editor.getHTML(); // Alias for getContent()
277
+ editor.setHTML(html); // Alias for setContent()
278
+
279
+ editor.getJSON(); // Get structured JSON representation
280
+ editor.setJSON(json); // Set content from JSON
281
+ ```
282
+
283
+ ### Editor Control
284
+
285
+ ```javascript
286
+ editor.focus(); // Focus the editor
287
+ editor.blur(); // Blur the editor
288
+ editor.enable(); // Enable editing
289
+ editor.disable(); // Disable editing (read-only)
290
+ editor.destroy(); // Remove editor, restore original element
291
+ editor.toggleFullscreen(); // Toggle fullscreen mode
292
+ editor.toggleTheme(); // Toggle light/dark theme
293
+ editor.setTheme('dark'); // Set a specific theme
294
+ editor.triggerSave(); // Trigger onSave callback
295
+ editor.previewContent(); // Open preview modal
296
+ editor.downloadContent(); // Download content as HTML file
297
+ editor.clearAll(); // Clear all content
298
+ ```
299
+
300
+ ### Command Execution
301
+
302
+ ```javascript
303
+ editor.execCommand('bold'); // Execute a command
304
+ editor.insertHTML('<span>Hello</span>'); // Insert HTML at cursor
305
+ editor.wrapSelection('mark', { class: 'highlight' }); // Wrap selection
306
+ editor.unwrapSelection('mark'); // Unwrap selection
307
+ ```
308
+
309
+ ### Selection
310
+
311
+ ```javascript
312
+ editor.getSelection(); // Get current Selection object
313
+ ```
314
+
315
+ ---
316
+
317
+ ## 🔌 Plugin API
318
+
319
+ Extend the editor with custom plugins:
320
+
321
+ ```javascript
322
+ NeikiEditor.registerPlugin({
323
+ name: 'word-counter-alert',
324
+ icon: '<svg viewBox="0 0 24 24">...</svg>', // optional toolbar button
325
+ tooltip: 'Show Word Count',
326
+ action: function(editor) {
327
+ const text = editor.getContent().replace(/<[^>]*>/g, '');
328
+ const words = text.trim().split(/\s+/).filter(Boolean).length;
329
+ alert('Word count: ' + words);
330
+ },
331
+ init: function(editor) {
332
+ // Called once when editor initializes (optional)
333
+ console.log('Plugin initialized!');
334
+ }
335
+ });
336
+ ```
337
+
338
+ ### Plugin Properties
339
+
340
+ | Property | Type | Required | Description |
341
+ |----------|------|----------|-------------|
342
+ | `name` | `string` | ✅ | Unique plugin identifier |
343
+ | `icon` | `string` | ❌ | SVG icon for toolbar button |
344
+ | `tooltip` | `string` | ❌ | Button tooltip text |
345
+ | `action` | `function` | ❌ | Called when toolbar button is clicked |
346
+ | `init` | `function` | ❌ | Called once during editor initialization |
347
+
348
+ ### List Registered Plugins
349
+
350
+ ```javascript
351
+ NeikiEditor.getPlugins(); // Returns array of registered plugins
352
+ ```
353
+
354
+ ---
355
+
356
+ ## 📊 Table Features
357
+
358
+ Insert tables via the toolbar button with configurable rows, columns, and optional header row.
359
+
360
+ ### Table Context Menu
361
+
362
+ Right-click on any table cell to access:
363
+
364
+ - **Insert Row Above / Below**
365
+ - **Insert Column Left / Right**
366
+ - **Delete Row / Column / Table**
367
+ - **Merge Cells** — merge selected cells horizontally
368
+ - **Split Cell** — split a previously merged cell
369
+
370
+ ---
371
+
372
+ ## 🖼️ Image Support
373
+
374
+ ### Insert via URL
375
+
376
+ Click the **Image** toolbar button and paste a URL.
377
+
378
+ ### Upload from File
379
+
380
+ The Image dialog includes a file upload input. Selected images are converted to **base64** and embedded directly in the content.
381
+
382
+ ### Drag & Drop
383
+
384
+ Drag image files directly into the editor content area. Images are automatically converted to base64 and inserted at the drop position.
385
+
386
+ ---
387
+
388
+ ## 🔍 Find & Replace
389
+
390
+ Open with the toolbar button or implement via the modal API.
391
+
392
+ Features:
393
+ - **Case-sensitive** search toggle
394
+ - **Regular expression** support
395
+ - **Find Next** — navigate through matches with highlighting
396
+ - **Replace** — replace current match
397
+ - **Replace All** — replace all matches at once
398
+
399
+ ---
400
+
401
+ ## ✏️ Floating Toolbar
402
+
403
+ When you select text in the editor, a floating toolbar appears above the selection with quick access to:
404
+
405
+ - Bold, Italic, Underline, Strikethrough
406
+ - Insert Link
407
+
408
+ The toolbar follows the selection and disappears when the selection is cleared.
409
+
410
+ ---
411
+
412
+ ## 🖨️ Print
413
+
414
+ Click the **Print** button to open the browser print dialog with the editor content formatted for printing.
415
+
416
+ ---
417
+
418
+ ## ⌨️ Keyboard Shortcuts
419
+
420
+ | Shortcut | Action |
421
+ |----------|--------|
422
+ | **Ctrl+B** | Bold |
423
+ | **Ctrl+I** | Italic |
424
+ | **Ctrl+U** | Underline |
425
+ | **Ctrl+K** | Insert Link |
426
+ | **Ctrl+S** | Save (triggers `onSave` callback) |
427
+ | **Ctrl+Z** | Undo |
428
+ | **Ctrl+Y** / **Ctrl+Shift+Z** | Redo |
429
+ | **Tab** | Indent |
430
+ | **Shift+Tab** | Outdent |
431
+
432
+ ---
433
+
434
+ ## 📐 Status Bar
435
+
436
+ The editor includes a status bar at the bottom displaying:
437
+
438
+ - **Left side:** Word count and character count
439
+ - **Right side:** Autosave status (when enabled) and current block type (p, h1, h2, etc.)
440
+
441
+ ---
442
+
443
+ ## 🔗 Integration Examples
444
+
445
+ ### PHP Helper (Recommended)
446
+
447
+ Neiki Editor includes a PHP integration helper (`php/neiki-editor.php`) that provides asset loading, editor rendering, and HTML sanitization:
448
+
449
+ ```php
450
+ <?php require_once 'php/neiki-editor.php'; ?>
451
+ <!DOCTYPE html>
452
+ <html>
453
+ <head>
454
+ <?= NeikiEditor::assets() ?>
455
+ </head>
456
+ <body>
457
+ <form method="POST" action="save.php">
458
+ <?= NeikiEditor::render('content', $article->content, [
459
+ 'minHeight' => 400,
460
+ 'placeholder' => 'Write your article...'
461
+ ]) ?>
462
+ <button type="submit">Save</button>
463
+ </form>
464
+ </body>
465
+ </html>
466
+ ```
467
+
468
+ ```php
469
+ // save.php — sanitize before saving to database
470
+ require_once 'php/neiki-editor.php';
471
+ $clean = NeikiEditor::sanitize($_POST['content']);
472
+ $db->save($clean);
473
+ ```
474
+
475
+ #### PHP Helper Methods
476
+
477
+ | Method | Description |
478
+ |--------|-------------|
479
+ | `NeikiEditor::assets()` | Output CSS & JS tags (CDN). Call once per page. |
480
+ | `NeikiEditor::assets(true, '/path/to/dist')` | Use local files instead of CDN. |
481
+ | `NeikiEditor::render($id, $content, $options)` | Render textarea + init script. |
482
+ | `NeikiEditor::sanitize($html)` | Strip dangerous tags/attributes before DB save. |
483
+
484
+ ### PHP Form (Manual)
485
+
486
+ ```php
487
+ <form method="POST" action="save.php">
488
+ <textarea id="editor" name="content"><?= htmlspecialchars($article->content) ?></textarea>
489
+ <button type="submit">Save</button>
490
+ </form>
491
+
492
+ <script>
493
+ const editor = new NeikiEditor('#editor');
494
+ </script>
495
+ ```
496
+
497
+ ### AJAX Save
498
+
499
+ ```javascript
500
+ const editor = new NeikiEditor('#editor', {
501
+ onChange: debounce(function(content) {
502
+ fetch('/api/save', {
503
+ method: 'POST',
504
+ headers: { 'Content-Type': 'application/json' },
505
+ body: JSON.stringify({ content })
506
+ });
507
+ }, 2000)
508
+ });
509
+ ```
510
+
511
+ ### Vue.js
512
+
513
+ ```vue
514
+ <template>
515
+ <textarea ref="editor"></textarea>
516
+ </template>
517
+
518
+ <script>
519
+ export default {
520
+ mounted() {
521
+ this.editor = new NeikiEditor(this.$refs.editor, {
522
+ onChange: (content) => {
523
+ this.$emit('update:modelValue', content);
524
+ }
525
+ });
526
+ },
527
+ beforeUnmount() {
528
+ this.editor.destroy();
529
+ }
530
+ }
531
+ </script>
532
+ ```
533
+
534
+ ### React
535
+
536
+ ```jsx
537
+ import { useEffect, useRef } from 'react';
538
+
539
+ function NeikiEditorComponent({ value, onChange }) {
540
+ const ref = useRef(null);
541
+ const editorRef = useRef(null);
542
+
543
+ useEffect(() => {
544
+ editorRef.current = new NeikiEditor(ref.current, {
545
+ onChange: (content) => onChange?.(content)
546
+ });
547
+ return () => editorRef.current?.destroy();
548
+ }, []);
549
+
550
+ return <textarea ref={ref} defaultValue={value} />;
551
+ }
552
+ ```
553
+
554
+ ---
555
+
556
+ ## 🌐 Browser Support
557
+
558
+ | Browser | Support |
559
+ |---------|---------|
560
+ | Chrome | ✅ Latest |
561
+ | Firefox | ✅ Latest |
562
+ | Safari | ✅ Latest |
563
+ | Edge | ✅ Latest |
564
+ | Opera | ✅ Latest |
565
+
566
+ ---
567
+
568
+ ## 📁 File Structure
569
+
570
+ ```
571
+ neiki-editor/
572
+ ├── dist/
573
+ │ ├── neiki-editor.js # Editor core
574
+ │ └── neiki-editor.css # Editor styles
575
+ ├── demo/
576
+ │ └── index.html # Interactive demo page
577
+ │ └── logo.png # Demo logo
578
+ ├── php/
579
+ │ └── neiki-editor.php # PHP integration helper
580
+ ├── logo.png
581
+ ├── package.json
582
+ ├── README.md
583
+ ├── LICENSE
584
+ ├── CHANGELOG.md
585
+ ├── CONTRIBUTING.md
586
+ ├── CODE_OF_CONDUCT.md
587
+ └── SECURITY.md
588
+ ```
589
+
590
+ ---
591
+
592
+ ## 📄 License
593
+
594
+ This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
595
+
596
+ ---
597
+
598
+ <p align="center">
599
+ Made with ❤️ for the web community
600
+ </p>