tulih-editor 0.1.0

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 (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +331 -0
  3. package/dist/tulih-editor.css +1 -0
  4. package/dist/tulih-editor.es.js +3051 -0
  5. package/dist/tulih-editor.es.js.map +1 -0
  6. package/dist/tulih-editor.umd.js +8 -0
  7. package/dist/tulih-editor.umd.js.map +1 -0
  8. package/dist/types/core/Editor.d.ts +20 -0
  9. package/dist/types/core/PluginManager.d.ts +22 -0
  10. package/dist/types/core/helpers.d.ts +22 -0
  11. package/dist/types/index.d.ts +5 -0
  12. package/dist/types/plugins/align.d.ts +3 -0
  13. package/dist/types/plugins/autoLinkify.d.ts +3 -0
  14. package/dist/types/plugins/autosave.d.ts +3 -0
  15. package/dist/types/plugins/block.d.ts +3 -0
  16. package/dist/types/plugins/caseTransform.d.ts +3 -0
  17. package/dist/types/plugins/codeBlock.d.ts +3 -0
  18. package/dist/types/plugins/colors.d.ts +3 -0
  19. package/dist/types/plugins/darkMode.d.ts +3 -0
  20. package/dist/types/plugins/direction.d.ts +3 -0
  21. package/dist/types/plugins/dragDrop.d.ts +3 -0
  22. package/dist/types/plugins/emoji.d.ts +3 -0
  23. package/dist/types/plugins/emojiAutocomplete.d.ts +3 -0
  24. package/dist/types/plugins/findReplace.d.ts +3 -0
  25. package/dist/types/plugins/floatingToolbar.d.ts +3 -0
  26. package/dist/types/plugins/fontFamily.d.ts +3 -0
  27. package/dist/types/plugins/fontSize.d.ts +3 -0
  28. package/dist/types/plugins/fullscreen.d.ts +3 -0
  29. package/dist/types/plugins/history.d.ts +3 -0
  30. package/dist/types/plugins/hr.d.ts +3 -0
  31. package/dist/types/plugins/iframe.d.ts +3 -0
  32. package/dist/types/plugins/image.d.ts +3 -0
  33. package/dist/types/plugins/imageProps.d.ts +3 -0
  34. package/dist/types/plugins/imageTools.d.ts +3 -0
  35. package/dist/types/plugins/indent.d.ts +3 -0
  36. package/dist/types/plugins/index.d.ts +2 -0
  37. package/dist/types/plugins/inline.d.ts +3 -0
  38. package/dist/types/plugins/inlineCode.d.ts +3 -0
  39. package/dist/types/plugins/keyboardShortcuts.d.ts +3 -0
  40. package/dist/types/plugins/lineHeight.d.ts +3 -0
  41. package/dist/types/plugins/link.d.ts +3 -0
  42. package/dist/types/plugins/linkTooltip.d.ts +3 -0
  43. package/dist/types/plugins/list.d.ts +3 -0
  44. package/dist/types/plugins/markdown.d.ts +3 -0
  45. package/dist/types/plugins/mediaEmbed.d.ts +3 -0
  46. package/dist/types/plugins/pasteImage.d.ts +3 -0
  47. package/dist/types/plugins/pastePlain.d.ts +3 -0
  48. package/dist/types/plugins/pre.d.ts +3 -0
  49. package/dist/types/plugins/readOnly.d.ts +3 -0
  50. package/dist/types/plugins/shortcutCustomizer.d.ts +3 -0
  51. package/dist/types/plugins/shortcutsHelp.d.ts +3 -0
  52. package/dist/types/plugins/source.d.ts +3 -0
  53. package/dist/types/plugins/specialChars.d.ts +3 -0
  54. package/dist/types/plugins/statusBar.d.ts +3 -0
  55. package/dist/types/plugins/subSuper.d.ts +3 -0
  56. package/dist/types/plugins/table.d.ts +3 -0
  57. package/dist/types/plugins/tableBg.d.ts +3 -0
  58. package/dist/types/plugins/tableTools.d.ts +3 -0
  59. package/dist/types/plugins/toolbarCollapse.d.ts +3 -0
  60. package/dist/types/plugins/unlink.d.ts +3 -0
  61. package/dist/types/plugins/wordCount.d.ts +3 -0
  62. package/dist/types/types.d.ts +226 -0
  63. package/package.json +66 -0
  64. package/src/core/Editor.ts +460 -0
  65. package/src/core/PluginManager.ts +140 -0
  66. package/src/core/helpers.ts +209 -0
  67. package/src/css.d.ts +2 -0
  68. package/src/index.ts +87 -0
  69. package/src/plugins/align.ts +72 -0
  70. package/src/plugins/autoLinkify.ts +34 -0
  71. package/src/plugins/autosave.ts +69 -0
  72. package/src/plugins/block.ts +32 -0
  73. package/src/plugins/caseTransform.ts +54 -0
  74. package/src/plugins/codeBlock.ts +93 -0
  75. package/src/plugins/colors.ts +68 -0
  76. package/src/plugins/darkMode.ts +123 -0
  77. package/src/plugins/direction.ts +30 -0
  78. package/src/plugins/dragDrop.ts +68 -0
  79. package/src/plugins/emoji.ts +188 -0
  80. package/src/plugins/emojiAutocomplete.ts +183 -0
  81. package/src/plugins/findReplace.ts +229 -0
  82. package/src/plugins/floatingToolbar.ts +258 -0
  83. package/src/plugins/fontFamily.ts +41 -0
  84. package/src/plugins/fontSize.ts +32 -0
  85. package/src/plugins/fullscreen.ts +36 -0
  86. package/src/plugins/history.ts +14 -0
  87. package/src/plugins/hr.ts +118 -0
  88. package/src/plugins/iframe.ts +88 -0
  89. package/src/plugins/image.ts +107 -0
  90. package/src/plugins/imageProps.ts +119 -0
  91. package/src/plugins/imageTools.ts +344 -0
  92. package/src/plugins/indent.ts +29 -0
  93. package/src/plugins/index.ts +101 -0
  94. package/src/plugins/inline.ts +17 -0
  95. package/src/plugins/inlineCode.ts +21 -0
  96. package/src/plugins/keyboardShortcuts.ts +92 -0
  97. package/src/plugins/lineHeight.ts +40 -0
  98. package/src/plugins/link.ts +344 -0
  99. package/src/plugins/linkTooltip.ts +63 -0
  100. package/src/plugins/list.ts +141 -0
  101. package/src/plugins/markdown.ts +61 -0
  102. package/src/plugins/mediaEmbed.ts +44 -0
  103. package/src/plugins/pasteImage.ts +61 -0
  104. package/src/plugins/pastePlain.ts +43 -0
  105. package/src/plugins/pre.ts +11 -0
  106. package/src/plugins/readOnly.ts +46 -0
  107. package/src/plugins/shortcutCustomizer.ts +125 -0
  108. package/src/plugins/shortcutsHelp.ts +51 -0
  109. package/src/plugins/source.ts +77 -0
  110. package/src/plugins/specialChars.ts +64 -0
  111. package/src/plugins/statusBar.ts +85 -0
  112. package/src/plugins/subSuper.ts +20 -0
  113. package/src/plugins/table.ts +166 -0
  114. package/src/plugins/tableBg.ts +11 -0
  115. package/src/plugins/tableTools.ts +475 -0
  116. package/src/plugins/toolbarCollapse.ts +14 -0
  117. package/src/plugins/unlink.ts +29 -0
  118. package/src/plugins/wordCount.ts +34 -0
  119. package/src/styles/base.css +258 -0
  120. package/src/styles/editor.css +309 -0
  121. package/src/styles/index.css +6 -0
  122. package/src/types.ts +278 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hendra Randy Nomura
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,331 @@
1
+ # TulihEditor
2
+
3
+ A lightweight, **zero-runtime-dependency** WYSIWYG rich text editor for the browser,
4
+ built on `contenteditable` and `document.execCommand()`. Written in **TypeScript**, it
5
+ ships a modular plugin system with **47 built-in plugins** and ships its own CSS — no
6
+ Bootstrap or CSS framework required on your page.
7
+
8
+ - 🧩 **Plugin architecture** — every feature is a self-contained plugin
9
+ - 🪶 **Tiny & dependency-free** — ~36 KB gzipped, no runtime dependencies
10
+ - 🎨 **Self-contained styling** — works without Bootstrap; icons via Tabler Icons (auto-loaded)
11
+ - 🔒 **Built-in HTML sanitizer** — configurable allowed tags/attributes, URL scheme allow-lists, iframe domain allow-list
12
+ - 📦 **ESM + UMD builds** — `import` it or drop a `<script>` tag
13
+ - ⌨️ **TypeScript-first** — full type definitions for the API and the plugin contract
14
+
15
+ > Ported to a standalone package from the original implementation that lived inside a
16
+ > Laravel app. The runtime behavior is identical; this repo is the framework-agnostic,
17
+ > open-source distribution.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install tulih-editor
23
+ ```
24
+
25
+ ```js
26
+ import TulihEditor from 'tulih-editor';
27
+ import 'tulih-editor/style.css';
28
+ ```
29
+
30
+ Or via a `<script>` tag (UMD global `TulihEditor`):
31
+
32
+ ```html
33
+ <link rel="stylesheet" href="https://unpkg.com/tulih-editor/dist/tulih-editor.css">
34
+ <script src="https://unpkg.com/tulih-editor/dist/tulih-editor.umd.js"></script>
35
+ ```
36
+
37
+ The toolbar icons use [Tabler Icons](https://tabler.io/icons); the webfont (pinned to the
38
+ `v3` line) is loaded automatically from jsDelivr on first use, only if a `tabler-icons`
39
+ stylesheet is not already present on the page. To self-host instead, add your own
40
+ `tabler-icons` stylesheet to the page before TulihEditor loads — the auto-loader will
41
+ detect it and skip the CDN request.
42
+
43
+ ## Basic usage
44
+
45
+ ```html
46
+ <textarea data-tulih-editor>Initial content here</textarea>
47
+ ```
48
+
49
+ The editor auto-attaches to every `<textarea data-tulih-editor>` on `DOMContentLoaded`.
50
+
51
+ ### Manual initialization
52
+
53
+ ```ts
54
+ const editor = TulihEditor.attach('#my-textarea', {
55
+ height: 500,
56
+ features: {
57
+ table: false,
58
+ source: false,
59
+ },
60
+ });
61
+
62
+ editor.getContent(); // get HTML
63
+ editor.setContent('<p>New content</p>');
64
+ editor.getText(); // get plain text
65
+ editor.getWordCount(); // number of words
66
+ editor.getCharCount(); // number of characters
67
+ editor.setReadOnly(true);
68
+ editor.isReadOnly();
69
+ editor.destroy(); // remove editor, restore the textarea
70
+ ```
71
+
72
+ ## Options
73
+
74
+ | Option | Type | Default | Description |
75
+ |--------|------|---------|-------------|
76
+ | `features` | `object` | all `true` | Enable/disable individual features (plugins) |
77
+ | `minimal` | `boolean` | `false` | Hide the classic toolbar; floating toolbar only |
78
+ | `toolbar` | `string[][]` | auto | Custom toolbar layout (groups of plugin names) |
79
+ | `height` | `number \| string` | `300px` | Editor body height (a number = pixels) |
80
+ | `sanitize` | `object` | — | Custom HTML sanitization rules |
81
+ | `allowedUrlSchemes` | `string[]` | `http, https, mailto, tel` | Allowed URL schemes for links |
82
+ | `allowedImageSchemes` | `string[]` | `http, https, data` | Allowed URL schemes for images |
83
+ | `iframeAllowlist` | `string[]` | youtube.com, vimeo.com, … | Allowed iframe domains |
84
+ | `iframeSandbox` | `string` | `allow-scripts allow-same-origin allow-presentation` | `sandbox` attr for embeds |
85
+ | `iframeAllow` | `string` | `accelerometer; autoplay; …` | `allow` attr for embeds |
86
+ | `readOnly` | `boolean` | `false` | Start in read-only mode |
87
+
88
+ ### Custom toolbar layout
89
+
90
+ ```ts
91
+ TulihEditor.attach('textarea', {
92
+ toolbar: [
93
+ ['inline', 'inlineCode', 'subSuper', 'removeFormat'],
94
+ ['block', 'align', 'colors', 'tableBg'],
95
+ ['link', 'image', 'iframe', 'table', 'hr', 'emoji'],
96
+ ['pastePlain', 'findReplace', 'indent', 'direction', 'unlink'],
97
+ ['source', 'history', 'fullscreen', 'readOnly'],
98
+ ],
99
+ });
100
+ ```
101
+
102
+ Each inner array is one toolbar group (separated by a divider). Plugin names not listed
103
+ are hidden.
104
+
105
+ ### Image upload handler
106
+
107
+ ```ts
108
+ TulihEditor.onUploadImage = (file, callback) => {
109
+ const formData = new FormData();
110
+ formData.append('image', file);
111
+ fetch('/upload', { method: 'POST', body: formData })
112
+ .then((r) => r.json())
113
+ .then((data) => callback(data.url));
114
+ };
115
+ ```
116
+
117
+ Used for both **drag & drop** and **paste image** from the clipboard. If no handler is set,
118
+ images are inlined as `data:` URLs.
119
+
120
+ ### Image browse handler
121
+
122
+ ```ts
123
+ TulihEditor.setBrowseImage((context, callback) => {
124
+ FileManager.open((url) => callback(url));
125
+ });
126
+ ```
127
+
128
+ ## Feature toggles
129
+
130
+ Each plugin can be disabled via `features`. In **normal mode** all features are enabled by
131
+ default. Set `minimal: true` to hide the classic toolbar — in minimal mode only the
132
+ floating toolbar is active and other features are off by default:
133
+
134
+ ```ts
135
+ TulihEditor.attach('textarea', { minimal: true });
136
+ // opt specific features back in:
137
+ TulihEditor.attach('textarea', { minimal: true, features: { statusBar: true } });
138
+ ```
139
+
140
+ ```ts
141
+ TulihEditor.attach('textarea', {
142
+ features: {
143
+ block: false, // hide heading/block select
144
+ link: false, // hide link button
145
+ emoji: false, // hide emoji picker
146
+ darkMode: false, // hide dark-mode toggle
147
+ },
148
+ });
149
+ ```
150
+
151
+ ## All plugins
152
+
153
+ | Plugin | Order | Toolbar | Description |
154
+ |--------|-------|---------|-------------|
155
+ | `autoLinkify` | 1 | — | Auto-convert typed/pasted URLs into clickable links |
156
+ | `mediaEmbed` | 2 | — | Auto-convert YouTube/Vimeo URL to embed |
157
+ | `dragDrop` | 3 | — | Drag & drop image files into the editor |
158
+ | `linkTooltip` | 4 | — | Hover tooltip showing link URL |
159
+ | `keyboardShortcuts` | 5 | — | Customizable Ctrl+B/I/U/Z/Y/S/K shortcuts |
160
+ | `markdown` | 5 | — | Markdown shortcuts (`# `, `- `, `> `, `1. `) |
161
+ | `emojiAutocomplete` | 6 | — | Type `:` then letters for emoji suggestions |
162
+ | `shortcutCustomizer` | 6 | Button | Modal UI to customize keyboard shortcuts |
163
+ | `block` | 10 | Select | Block format: Paragraph, H1–H6, Quote, Code |
164
+ | `inline` | 20 | Buttons | Bold, Italic, Underline, Strikethrough, quote |
165
+ | `pre` | 21 | Button | Insert preformatted text |
166
+ | `inlineCode` | 22 | Button | Wrap selection in `<code>` |
167
+ | `codeBlock` | 23 | Button | Insert code block (16 languages) |
168
+ | `pastePlain` | 24 | Button | Toggle paste-as-plain-text |
169
+ | `indent` | 25 | Buttons | Indent / Outdent |
170
+ | `list` | 25 | Selects | Ordered/unordered list styles |
171
+ | `subSuper` | 26 | Buttons | Subscript / Superscript |
172
+ | `lineHeight` | 26 | Select | Line height 1–3 |
173
+ | `fontSize` | 27 | Select | Font size 8px–72px |
174
+ | `fontFamily` | 28 | Select | Font family (11 fonts) |
175
+ | `specialChars` | 29 | Button | Special characters popup (~280 symbols) |
176
+ | `colors` | 30 | Color inputs | Text color / Background color |
177
+ | `findReplace` | 31 | Button | Find & replace popup |
178
+ | `caseTransform` | 31 | Button | UPPER / lower / Title / Sentence case |
179
+ | `align` | 40 | Buttons | Left, Center, Right, Justify |
180
+ | `link` | 50 | Button + Modal | Link insertion; inline edit on click |
181
+ | `image` | 51 | Button + Modal | Image insertion with URL or browse |
182
+ | `imageProps` | 52 | Modal (dblclick) | Image properties: size, alt, class, CSS |
183
+ | `iframe` | 53 | Button + Modal | Iframe embed with sandbox/allow |
184
+ | `table` | 54 | Button + Grid | Table insertion (10×10 grid) + properties |
185
+ | `hr` | 55 | Button | Horizontal rule with style popup |
186
+ | `tableTools` | 55 | Floating bar | Rows/cols, merge, header, caption, cell bg, valign |
187
+ | `tableBg` | 56 | Color input | Table cell background color |
188
+ | `imageTools` | 56 | Floating bar | Resize handles, border, rounded corners |
189
+ | `unlink` | 58 | Button | Remove link from selection |
190
+ | `floatingToolbar` | 58 | — | Mini toolbar near a text selection |
191
+ | `darkMode` | 59 | Button | Toggle dark mode |
192
+ | `emoji` | 60 | Button + Popup | Emoji picker grid with search |
193
+ | `direction` | 61 | Buttons | LTR / RTL text direction |
194
+ | `source` | 65 | Button | Toggle source (HTML) mode |
195
+ | `history` | 70 | Buttons | Undo / Redo |
196
+ | `readOnly` | 75 | Button | Toggle read-only mode |
197
+ | `fullscreen` | 75 | Button | Toggle fullscreen |
198
+ | `shortcutsHelp` | 90 | Button | Keyboard shortcuts reference |
199
+ | `statusBar` | 100 | Status bar | Live tag indicator, word/char count, autosave badge |
200
+ | `autosave` | 210 | — | Auto-save to localStorage with Saved/Unsaved badge |
201
+ | `toolbarCollapse` | 1000 | Button | Collapse/expand the toolbar |
202
+
203
+ ## Keyboard shortcuts
204
+
205
+ | Shortcut | Action |
206
+ |----------|--------|
207
+ | Ctrl+B | Bold |
208
+ | Ctrl+I | Italic |
209
+ | Ctrl+U | Underline |
210
+ | Ctrl+S | Strikethrough |
211
+ | Ctrl+Z | Undo |
212
+ | Ctrl+Shift+Z / Ctrl+Y | Redo |
213
+ | Ctrl+K | Open link dialog |
214
+
215
+ Shortcuts can be rebound via the **shortcut customizer** button, or programmatically:
216
+
217
+ ```js
218
+ localStorage.setItem('te-shortcut-bold', JSON.stringify({ key: 'b', ctrl: true, shift: false }));
219
+ ```
220
+
221
+ Each key follows the format `te-shortcut-{action}`.
222
+
223
+ ## Plugin system
224
+
225
+ Every feature is a plugin implementing the `Plugin` interface:
226
+
227
+ ```ts
228
+ import type { Plugin, PluginContext } from 'tulih-editor';
229
+
230
+ const myPlugin: Plugin = {
231
+ name: 'myPlugin', // unique name (also the feature flag key)
232
+ order: 45, // toolbar position (divider per 10-range)
233
+ deps: ['link'], // optional dependencies
234
+ toolbarHTML: '<button type="button" class="btn btn-sm btn-light" data-cmd="myCmd">My</button>',
235
+ modalHTML: '<div class="te-modal">…</div>', // optional
236
+ css: '.te-my { color: red; }', // optional, injected once
237
+ init(ctx: PluginContext) {
238
+ const btn = ctx.wrapper.querySelector('[data-cmd="myCmd"]');
239
+ btn?.addEventListener('click', () => { /* … */ });
240
+ },
241
+ destroy(ctx: PluginContext) { /* cleanup */ },
242
+ };
243
+
244
+ export default myPlugin;
245
+ ```
246
+
247
+ ### The plugin context (`ctx`)
248
+
249
+ | Property | Description |
250
+ |----------|-------------|
251
+ | `wrapper` | Wrapper element |
252
+ | `editor` | Contenteditable body (`.te-editor`) |
253
+ | `toolbar` | Toolbar element |
254
+ | `textarea` | Original textarea |
255
+ | `features` | Resolved feature flags |
256
+ | `options` | Resolved options |
257
+ | `utils` | `sanitizeHTML`, `isSafeUrl`, `isAllowedIframeUrl` |
258
+ | `TulihEditor` | The global TulihEditor object |
259
+ | `saveSel()` / `restoreSel()` | Save / restore the selection range |
260
+ | `updateActiveStates()` | Refresh toolbar active states |
261
+
262
+ ### Registering a custom plugin
263
+
264
+ ```ts
265
+ import TulihEditor from 'tulih-editor';
266
+ import myPlugin from './myPlugin';
267
+
268
+ TulihEditor.pluginManager.register('myPlugin', myPlugin);
269
+
270
+ TulihEditor.attach('textarea', { features: { myPlugin: true } });
271
+ ```
272
+
273
+ ## Project structure
274
+
275
+ ```
276
+ tulih-editor/
277
+ ├── src/
278
+ │ ├── index.ts # entry point + public API
279
+ │ ├── types.ts # public TypeScript types
280
+ │ ├── styles/
281
+ │ │ ├── base.css # framework-free primitives (buttons/forms/grid)
282
+ │ │ ├── editor.css # editor theme + layout
283
+ │ │ └── index.css # stylesheet entry (imported by index.ts)
284
+ │ ├── core/
285
+ │ │ ├── Editor.ts # editor instance (class)
286
+ │ │ ├── PluginManager.ts # plugin registry & lifecycle
287
+ │ │ └── helpers.ts # shared DOM utilities
288
+ │ └── plugins/
289
+ │ ├── index.ts # registers the 47 default plugins
290
+ │ └── *.ts # one file per plugin
291
+ ├── test/ # jsdom end-to-end tests
292
+ └── dist/ # build output (gitignored)
293
+ ```
294
+
295
+ ## Development
296
+
297
+ ```bash
298
+ npm install
299
+ npm run dev # Vite dev server (see examples/)
300
+ npm run typecheck # tsc --noEmit
301
+ npm run build # ESM + UMD bundles, CSS, and .d.ts declarations
302
+ npm test # jsdom end-to-end tests (run after build)
303
+ ```
304
+
305
+ Build output (`dist/`):
306
+
307
+ ```
308
+ tulih-editor.es.js # ESM bundle
309
+ tulih-editor.umd.js # UMD bundle (global: TulihEditor)
310
+ tulih-editor.css # stylesheet (base + theme)
311
+ types/ # .d.ts declarations
312
+ ```
313
+
314
+ ## Browser support
315
+
316
+ Modern evergreen browsers (Chrome, Firefox, Safari, Edge).
317
+
318
+ ## Credits
319
+
320
+ - Toolbar icons: [Tabler Icons](https://tabler.io/icons) by Paweł Kuna — licensed under
321
+ [MIT](https://github.com/tabler/tabler-icons/blob/main/LICENSE). The webfont is referenced
322
+ from a CDN at runtime and is **not** bundled or redistributed by this package. If you
323
+ choose to self-host/bundle the Tabler webfont, include its MIT license notice alongside it.
324
+
325
+ ## Contributing
326
+
327
+ Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
328
+
329
+ ## License
330
+
331
+ [MIT](LICENSE) © Hendra Randy Nomura
@@ -0,0 +1 @@
1
+ :where(.te-container,.te-float-toolbar,.te-float-link-input,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-emoji-popup,.te-sc-popup,.te-codeblock-popup) .btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;font-family:inherit;font-size:1rem;font-weight:400;line-height:1.5;text-align:center;text-decoration:none;vertical-align:middle;white-space:nowrap;cursor:pointer;-webkit-user-select:none;user-select:none;padding:.375rem .75rem;border:1px solid transparent;border-radius:.375rem;background-color:transparent;color:#212529;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}:where(.te-container,.te-float-toolbar,.te-float-link-input,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-emoji-popup,.te-sc-popup,.te-codeblock-popup) .btn:focus-visible{outline:2px solid rgba(26,26,26,.35);outline-offset:1px}:where(.te-container,.te-float-toolbar,.te-float-link-input,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-emoji-popup,.te-sc-popup,.te-codeblock-popup) .btn:disabled,:where(.te-container,.te-float-toolbar,.te-float-link-input,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-emoji-popup,.te-sc-popup,.te-codeblock-popup) .btn[disabled]{opacity:.5;pointer-events:none}:where(.te-container,.te-float-toolbar,.te-float-link-input,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-emoji-popup,.te-sc-popup,.te-codeblock-popup) .btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}:where(.te-container,.te-float-toolbar,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-emoji-popup,.te-sc-popup,.te-codeblock-popup) .btn-light{color:#3a3a3a;background-color:#f8f9fa;border-color:transparent}:where(.te-container,.te-float-toolbar,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-emoji-popup,.te-sc-popup,.te-codeblock-popup) .btn-light:hover{background-color:#f2ede2}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-hr-popup,.te-fr-popup,.te-codeblock-popup,.te-table-tools) .btn-primary{color:#fff;background-color:#1a1a1a;border-color:#1a1a1a}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-hr-popup,.te-fr-popup,.te-codeblock-popup,.te-table-tools) .btn-primary:hover{background-color:#333;border-color:#333}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-hr-popup,.te-fr-popup,.te-codeblock-popup,.te-table-tools) .btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-hr-popup,.te-fr-popup,.te-codeblock-popup,.te-table-tools) .btn-secondary:hover{background-color:#5c636a;border-color:#565e64}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-codeblock-popup) .btn-outline-secondary{color:#3a3a3a;border-color:#dee2e6}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-codeblock-popup) .btn-outline-secondary:hover{background-color:#f2ede2;border-color:#ccc}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-codeblock-popup) .btn-outline-danger{color:#c1272d;border-color:#c1272d}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-codeblock-popup) .btn-outline-danger:hover{background-color:#c1272d;color:#fff}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-codeblock-popup) .btn-outline-primary{color:#e8a317;border-color:#e8a317}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-codeblock-popup) .btn-outline-primary:hover{background-color:#e8a317;color:#fff}:where(.te-container) .btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em;border:0;border-radius:.375rem;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;cursor:pointer;opacity:.5}:where(.te-container) .btn-close:hover{opacity:.8}:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup,.te-float-link-input,.te-link-edit-popup) .form-control,:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup,.te-float-link-input,.te-link-edit-popup) .form-select{display:block;width:100%;padding:.375rem .75rem;font-family:inherit;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;border:1px solid #ced4da;border-radius:.375rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup,.te-float-link-input,.te-link-edit-popup) .form-control:focus,:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup,.te-float-link-input,.te-link-edit-popup) .form-select:focus{outline:0;border-color:#86b7fe;box-shadow:0 0 0 .2rem #0d6efd33}:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup) .form-select{padding-right:2.25rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px}:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup) .form-control-sm,:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup) .form-select-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup) .form-select-sm{padding-right:2rem}:where(.te-container,.te-hr-popup) .form-control-color{width:3rem;height:calc(1.5em + .75rem + 2px);padding:.25rem}:where(.te-container) .form-label{display:inline-block;margin-bottom:.5rem;font-size:.9rem;font-weight:500;color:#212529}:where(.te-container) .form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}:where(.te-container) .form-check-input{width:1em;height:1em;margin-top:.25em;margin-left:-1.5em;vertical-align:top;background-color:#fff;border:1px solid rgba(0,0,0,.25);border-radius:.25em;-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer}:where(.te-container) .form-check-input:checked{background-color:#1a1a1a;border-color:#1a1a1a;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center;background-size:contain}:where(.te-container) .form-check-label{cursor:pointer}:where(.te-container) .row{display:flex;flex-wrap:wrap;margin-right:-.5rem;margin-left:-.5rem}:where(.te-container) .row>.col,:where(.te-container) .row>[class^=col-],:where(.te-container) .row>[class*=" col-"]{flex:1 0 0%;padding-right:.5rem;padding-left:.5rem}:where(.te-container) .col-6{flex:0 0 auto;width:50%}:where(.te-container) .col-4{flex:0 0 auto;width:33.3333%}:where(.te-container) .col-3{flex:0 0 auto;width:25%}:where(.te-container) .g-2{gap:.5rem 0}:where(.te-container) .mb-0{margin-bottom:0!important}:where(.te-container) .mb-2{margin-bottom:.5rem!important}:where(.te-container) .mb-3{margin-bottom:1rem!important}:where(.te-container) .mt-2{margin-top:.5rem!important}:where(.te-container) .mt-3{margin-top:1rem!important}:where(.te-container) .small{font-size:.875em}:where(.te-container) .w-100{width:100%!important}:where(.te-container) .d-block{display:block!important}:where(.te-container) .d-flex{display:flex!important}:where(.te-container) .text-muted{color:#6c757d!important}.te-container{max-width:900px;position:relative}.te-toolbar{display:flex;flex-wrap:wrap;gap:6px;border:1px solid #dee2e6;background:#fbf9f4;padding:8px;border-bottom:none;border-radius:.375rem .375rem 0 0}.te-toolbar .btn,.te-toolbar .btn-light{color:#3a3a3a;background:transparent;border-color:transparent}.te-toolbar .btn:hover,.te-toolbar .btn-light:hover{background:#f2ede2}.te-toolbar .btn.active,.te-toolbar .btn-light.active{background:#fbead6;border-color:#fbead6}.te-toolbar .btn-outline-secondary{color:#3a3a3a;border-color:#dee2e6}.te-toolbar select{touch-action:manipulation}.te-dropdown-icon{display:inline-flex;align-items:center;vertical-align:middle;margin-right:2px}.te-dropdown-icon svg{width:18px;height:18px;stroke-width:2}.te-dropdown-icon-text{font-size:13px;font-weight:600;line-height:1;color:#3a3a3a}.te-toolbar .btn-primary,.te-toolbar .btn.btn-primary{background:#1a1a1a;border-color:#1a1a1a;color:#fff}.te-toolbar .btn-primary:hover,.te-toolbar .btn.btn-primary:hover{background:#333;border-color:#333}.te-toolbar .btn-outline-danger,.te-toolbar .btn.btn-outline-danger{color:#c1272d;border-color:#c1272d}.te-toolbar .btn-outline-danger:hover,.te-toolbar .btn.btn-outline-danger:hover{background:#c1272d;color:#fff}.te-toolbar .btn-outline-primary,.te-toolbar .btn.btn-outline-primary{color:#e8a317;border-color:#e8a317}.te-toolbar .btn-outline-primary:hover,.te-toolbar .btn.btn-outline-primary:hover{background:#e8a317;color:#fff}.te-divider{width:1px;background:#dee2e6;align-self:stretch;margin:0 4px}.te-editor{height:300px;overflow:auto;border:1px solid #dee2e6;border-radius:0;padding:12px;background:#fff}.te-editor:focus{outline:none;box-shadow:inset 0 0 0 2px #0d6efd40}.te-toolbar .btn .ti{pointer-events:none}.te-editor[placeholder]:empty:before{content:attr(placeholder);color:#6c757d}.te-toolbar select.form-select{width:auto;padding-right:2rem}.te-status{color:#6c757d;font-size:.875rem}@keyframes te-modal-in{0%{opacity:0;transform:scale(.95) translateY(-10px)}to{opacity:1;transform:scale(1) translateY(0)}}@keyframes te-backdrop-in{0%{opacity:0}to{opacity:1}}.te-modal{position:absolute;top:0;right:0;bottom:0;left:0;display:none;align-items:center;justify-content:center;z-index:2500;overflow-y:auto;padding:1rem}.te-modal.is-open{display:flex}.te-modal.is-open .te-modal-dialog{animation:te-modal-in .2s ease-out}.te-modal.is-open .te-modal-backdrop{animation:te-backdrop-in .2s ease-out}.te-modal-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;background:#0006}.te-modal-dialog{position:relative;background:#fff;border-radius:.75rem;box-shadow:0 20px 60px #0000002e;width:540px;max-width:100%;overflow:hidden;z-index:1}.te-modal-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;background:#fbf9f4;border-bottom:1px solid #eee}.te-modal-header .te-modal-title{font-size:1.05rem;font-weight:600;color:#1a1a1a}.te-modal-header .btn-close{font-size:.85rem;opacity:.5;transition:opacity .15s}.te-modal-header .btn-close:hover{opacity:.8}.te-modal-body{padding:1.25rem;color:#212529;max-height:60vh;overflow-y:auto}.te-modal-footer{display:flex;gap:.5rem;justify-content:flex-end;padding:1rem 1.25rem;background:#fbf9f4;border-top:1px solid #eee}.te-modal .btn-primary{background:#1a1a1a;border-color:#1a1a1a;color:#fff}.te-modal .btn-primary:hover{background:#333;border-color:#333}.te-modal .btn-outline-secondary{color:#3a3a3a;border-color:#dee2e6}.te-modal .btn-outline-secondary:hover{background:#f2ede2;border-color:#ccc}.te-modal .btn-outline-danger{color:#c1272d;border-color:#c1272d}.te-modal .btn-outline-danger:hover{background:#c1272d;color:#fff}.te-modal .btn-outline-primary{color:#e8a317;border-color:#e8a317}.te-modal .btn-outline-primary:hover{background:#e8a317;color:#fff}.te-editor a{text-decoration:none}.te-editor .te-table{width:100%;border-collapse:collapse}.te-editor .te-table td,.te-editor .te-table th{border:1px solid #dee2e6;padding:6px}@media(max-width:576px){.te-toolbar .btn{padding:.35rem .5rem;font-size:.8rem}.te-toolbar select.form-select{font-size:.8rem;padding:.2rem 1.5rem .2rem .5rem}.te-editor{padding:8px;font-size:.95rem}.te-container .te-toolbar{gap:3px;padding:4px}.te-divider{margin:0 2px}.te-modal-dialog{border-radius:.5rem}.te-modal-header,.te-modal-body,.te-modal-footer{padding:.75rem 1rem}.te-modal-body{max-height:50vh}}.te-minimal .te-toolbar{display:none}