kempo-ui 0.4.4 → 0.4.6

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 (185) hide show
  1. package/dist/components/Chat.js +201 -0
  2. package/dist/components/CodeEditor.js +21 -2
  3. package/dist/components/Dropdown.js +1 -1
  4. package/dist/components/HtmlEditor.js +42 -3
  5. package/dist/components/MarkdownEditor.js +173 -0
  6. package/dist/components/Resize.js +12 -2
  7. package/dist/components/TextToSpeech.js +4 -4
  8. package/dist/components/VoiceSelector.js +49 -0
  9. package/dist/components/markdownEditorControls/Bold.js +11 -0
  10. package/dist/components/markdownEditorControls/BulletList.js +11 -0
  11. package/dist/components/markdownEditorControls/Code.js +11 -0
  12. package/dist/components/markdownEditorControls/FormatBlock.js +30 -0
  13. package/dist/components/markdownEditorControls/Heading.js +11 -0
  14. package/dist/components/markdownEditorControls/Image.js +93 -0
  15. package/dist/components/markdownEditorControls/Italic.js +11 -0
  16. package/dist/components/markdownEditorControls/Link.js +11 -0
  17. package/dist/components/markdownEditorControls/MarkdownEditorControl.js +39 -0
  18. package/dist/components/markdownEditorControls/Menu.js +30 -0
  19. package/dist/components/markdownEditorControls/NumberedList.js +11 -0
  20. package/dist/components/markdownEditorControls/Quote.js +11 -0
  21. package/dist/components/markdownEditorControls/SpeechToText.js +16 -0
  22. package/dist/components/markdownEditorControls/Strikethrough.js +11 -0
  23. package/dist/components/markdownEditorControls/Table.js +80 -0
  24. package/dist/utils/marked.esm.js +1 -0
  25. package/dist/utils/renderMarkdown.js +1 -0
  26. package/dist/utils/sanitizeHtml.js +1 -0
  27. package/dist/utils/voice.js +1 -0
  28. package/docs/components/accordion.html +19 -0
  29. package/docs/components/aside.html +19 -0
  30. package/docs/components/calendar.html +19 -0
  31. package/docs/components/card.html +19 -0
  32. package/docs/components/chat.html +715 -0
  33. package/docs/components/code-editor.html +55 -0
  34. package/docs/components/color-picker.html +19 -0
  35. package/docs/components/combobox.html +19 -0
  36. package/docs/components/content-slider.html +19 -0
  37. package/docs/components/context.html +19 -0
  38. package/docs/components/dialog.html +19 -0
  39. package/docs/components/dropdown.html +19 -0
  40. package/docs/components/filter-list.html +19 -0
  41. package/docs/components/focus-capture.html +19 -0
  42. package/docs/components/html-editor.html +60 -0
  43. package/docs/components/hybrid-component.html +19 -0
  44. package/docs/components/icon.html +19 -0
  45. package/docs/components/import.html +19 -0
  46. package/docs/components/light-component.html +19 -0
  47. package/docs/components/markdown-editor.html +924 -0
  48. package/docs/components/nav-spacer.html +19 -0
  49. package/docs/components/nav.html +19 -0
  50. package/docs/components/photo-viewer.html +19 -0
  51. package/docs/components/progress.html +19 -0
  52. package/docs/components/resize.html +22 -0
  53. package/docs/components/rich-textarea.html +650 -0
  54. package/docs/components/shadow-component.html +19 -0
  55. package/docs/components/show-more.html +19 -0
  56. package/docs/components/slider.html +19 -0
  57. package/docs/components/sortable.html +19 -0
  58. package/docs/components/speech-to-text.html +19 -0
  59. package/docs/components/spinner.html +19 -0
  60. package/docs/components/split.html +19 -0
  61. package/docs/components/table.html +19 -0
  62. package/docs/components/tableControls.html +19 -0
  63. package/docs/components/tableCustomFields.html +19 -0
  64. package/docs/components/tableFetchRecords.html +19 -0
  65. package/docs/components/tableFieldSortHide.html +19 -0
  66. package/docs/components/tablePagination.html +19 -0
  67. package/docs/components/tablePlaceholder.html +19 -0
  68. package/docs/components/tableRecordEditing.html +19 -0
  69. package/docs/components/tableRecordFiltering.html +19 -0
  70. package/docs/components/tableRecordHiding.html +19 -0
  71. package/docs/components/tableRecordSearching.html +19 -0
  72. package/docs/components/tableRecordSelection.html +19 -0
  73. package/docs/components/tableRowControls.html +19 -0
  74. package/docs/components/tableServerSync.html +19 -0
  75. package/docs/components/tableSorting.html +19 -0
  76. package/docs/components/tabs.html +19 -0
  77. package/docs/components/tags.html +19 -0
  78. package/docs/components/text-to-speech.html +63 -1
  79. package/docs/components/theme-select.html +19 -0
  80. package/docs/components/theme-switcher.html +19 -0
  81. package/docs/components/time.html +19 -0
  82. package/docs/components/timestamp.html +19 -0
  83. package/docs/components/toast.html +19 -0
  84. package/docs/components/toggle.html +19 -0
  85. package/docs/components/tree.html +19 -0
  86. package/docs/components/voice-selector.html +610 -0
  87. package/docs/icons/done_all.svg +1 -0
  88. package/docs/icons/format_quote.svg +1 -0
  89. package/docs/icons/format_strikethrough.svg +1 -0
  90. package/docs/icons/send.svg +1 -0
  91. package/docs/icons/text_fields.svg +1 -0
  92. package/docs/index.html +49 -0
  93. package/docs/src/components/Chat.js +201 -0
  94. package/docs/src/components/CodeEditor.js +21 -2
  95. package/docs/src/components/Dropdown.js +1 -1
  96. package/docs/src/components/HtmlEditor.js +42 -3
  97. package/docs/src/components/MarkdownEditor.js +173 -0
  98. package/docs/src/components/Resize.js +12 -2
  99. package/docs/src/components/TextToSpeech.js +4 -4
  100. package/docs/src/components/VoiceSelector.js +49 -0
  101. package/docs/src/components/markdownEditorControls/Bold.js +11 -0
  102. package/docs/src/components/markdownEditorControls/BulletList.js +11 -0
  103. package/docs/src/components/markdownEditorControls/Code.js +11 -0
  104. package/docs/src/components/markdownEditorControls/FormatBlock.js +30 -0
  105. package/docs/src/components/markdownEditorControls/Heading.js +11 -0
  106. package/docs/src/components/markdownEditorControls/Image.js +93 -0
  107. package/docs/src/components/markdownEditorControls/Italic.js +11 -0
  108. package/docs/src/components/markdownEditorControls/Link.js +11 -0
  109. package/docs/src/components/markdownEditorControls/MarkdownEditorControl.js +39 -0
  110. package/docs/src/components/markdownEditorControls/Menu.js +30 -0
  111. package/docs/src/components/markdownEditorControls/NumberedList.js +11 -0
  112. package/docs/src/components/markdownEditorControls/Quote.js +11 -0
  113. package/docs/src/components/markdownEditorControls/SpeechToText.js +16 -0
  114. package/docs/src/components/markdownEditorControls/Strikethrough.js +11 -0
  115. package/docs/src/components/markdownEditorControls/Table.js +80 -0
  116. package/docs/src/utils/marked.esm.js +1 -0
  117. package/docs/src/utils/renderMarkdown.js +1 -0
  118. package/docs/src/utils/sanitizeHtml.js +1 -0
  119. package/docs/src/utils/voice.js +1 -0
  120. package/docs/utils/context.html +19 -0
  121. package/docs/utils/cookie.html +19 -0
  122. package/docs/utils/debounce.html +19 -0
  123. package/docs/utils/drag.html +19 -0
  124. package/docs/utils/elevation.html +23 -10
  125. package/docs/utils/formatTimestamp.html +19 -0
  126. package/docs/utils/object.html +19 -0
  127. package/docs/utils/propConverters.html +19 -0
  128. package/docs/utils/sanitizeHtml.html +555 -0
  129. package/docs/utils/string.html +19 -0
  130. package/docs/utils/theme.html +19 -0
  131. package/docs/utils/toTitleCase.html +19 -0
  132. package/docs/utils/type.html +19 -0
  133. package/docs/utils/voice.html +548 -0
  134. package/docs/utils/wait.html +19 -0
  135. package/docs-src/components/chat.page.html +230 -0
  136. package/docs-src/components/code-editor.page.html +36 -0
  137. package/docs-src/components/html-editor.page.html +41 -0
  138. package/docs-src/components/markdown-editor.page.html +439 -0
  139. package/docs-src/components/resize.page.html +3 -0
  140. package/docs-src/components/text-to-speech.page.html +44 -1
  141. package/docs-src/components/voice-selector.page.html +125 -0
  142. package/docs-src/index.page.html +30 -0
  143. package/docs-src/nav.fragment.html +19 -0
  144. package/docs-src/utils/elevation.page.html +4 -10
  145. package/docs-src/utils/sanitizeHtml.page.html +73 -0
  146. package/docs-src/utils/voice.page.html +66 -0
  147. package/icons/done_all.svg +1 -0
  148. package/icons/format_quote.svg +1 -0
  149. package/icons/format_strikethrough.svg +1 -0
  150. package/icons/send.svg +1 -0
  151. package/icons/text_fields.svg +1 -0
  152. package/llms.txt +7 -2
  153. package/package.json +3 -2
  154. package/src/components/Chat.js +435 -0
  155. package/src/components/CodeEditor.js +58 -2
  156. package/src/components/Dropdown.js +1 -1
  157. package/src/components/HtmlEditor.js +84 -4
  158. package/src/components/MarkdownEditor.js +654 -0
  159. package/src/components/Resize.js +17 -1
  160. package/src/components/TextToSpeech.js +13 -2
  161. package/src/components/VoiceSelector.js +271 -0
  162. package/src/components/markdownEditorControls/Bold.js +30 -0
  163. package/src/components/markdownEditorControls/BulletList.js +30 -0
  164. package/src/components/markdownEditorControls/Code.js +39 -0
  165. package/src/components/markdownEditorControls/FormatBlock.js +102 -0
  166. package/src/components/markdownEditorControls/Heading.js +45 -0
  167. package/src/components/markdownEditorControls/Image.js +250 -0
  168. package/src/components/markdownEditorControls/Italic.js +30 -0
  169. package/src/components/markdownEditorControls/Link.js +74 -0
  170. package/src/components/markdownEditorControls/MarkdownEditorControl.js +158 -0
  171. package/src/components/markdownEditorControls/Menu.js +86 -0
  172. package/src/components/markdownEditorControls/NumberedList.js +69 -0
  173. package/src/components/markdownEditorControls/Quote.js +30 -0
  174. package/src/components/markdownEditorControls/SpeechToText.js +65 -0
  175. package/src/components/markdownEditorControls/Strikethrough.js +35 -0
  176. package/src/components/markdownEditorControls/Table.js +164 -0
  177. package/src/utils/marked.esm.js +77 -0
  178. package/src/utils/renderMarkdown.js +29 -0
  179. package/src/utils/sanitizeHtml.js +137 -0
  180. package/src/utils/voice.js +103 -0
  181. package/tests/components/Chat.browser-test.js +540 -0
  182. package/tests/components/TextToSpeech.browser-test.js +150 -1
  183. package/tests/components/VoiceSelector.browser-test.js +374 -0
  184. package/tests/utils/sanitizeHtml.browser-test.js +179 -0
  185. package/tests/utils/voice.browser-test.js +128 -0
@@ -0,0 +1,250 @@
1
+ import MarkdownEditorControl from './MarkdownEditorControl.js';
2
+ import { html, css } from '../../lit-all.min.js';
3
+ import '../Icon.js';
4
+ import '../Dropdown.js';
5
+ import Dialog from '../Dialog.js';
6
+
7
+ /*
8
+ Image control. Asks the user for a URL and alt text, then inserts a
9
+ markdown image (`![alt](url)`) at the cursor.
10
+
11
+ The popup type is configurable via the `popup` attribute:
12
+ popup="dropdown" (default) — anchored popover next to the button
13
+ popup="dialog" — modal Dialog centered on the page
14
+
15
+ If the user has text selected when they click the button, it's pre-filled
16
+ into the alt text field.
17
+ */
18
+ export default class MarkdownImage extends MarkdownEditorControl {
19
+ static properties = {
20
+ ...MarkdownEditorControl.properties,
21
+ popup: { type: String, reflect: true }
22
+ };
23
+
24
+ constructor() {
25
+ super();
26
+ this.label = 'Image';
27
+ this.popup = 'dropdown';
28
+ }
29
+
30
+ /*
31
+ For dropdown mode the trigger button toggles the dropdown via Dropdown's
32
+ own click handling — we don't want our base-class handleClick (which
33
+ calls command()) to fire on top, or it would create a feedback loop.
34
+ Override command() to do nothing in dropdown mode; the Insert button
35
+ inside the dropdown drives insertion instead.
36
+ */
37
+ command() {
38
+ if(this.popup === 'dialog') this.#openDialog();
39
+ }
40
+
41
+ #insert(url, alt) {
42
+ if(!url) return false;
43
+ this.editor?.replaceSelection(`![${alt}](${url})`, { selectInserted: false });
44
+ return true;
45
+ }
46
+
47
+ /*
48
+ Dropdown flow — inline popover form.
49
+ */
50
+ handleDropdownOpened = () => {
51
+ requestAnimationFrame(() => {
52
+ this.#urlInput?.focus();
53
+ // Pre-fill alt text with the current selection (if any).
54
+ const sel = this.getSelection();
55
+ if(this.#altInput) this.#altInput.value = sel.text || '';
56
+ });
57
+ };
58
+
59
+ handleDropdownClosed = () => {
60
+ if(this.#urlInput) this.#urlInput.value = '';
61
+ if(this.#altInput) this.#altInput.value = '';
62
+ };
63
+
64
+ handleFormKeydown = (e) => {
65
+ if(e.key === 'Enter'){
66
+ e.preventDefault();
67
+ this.#submitFromDropdown();
68
+ } else if(e.key === 'Escape'){
69
+ this.shadowRoot?.querySelector('k-dropdown')?.close();
70
+ }
71
+ };
72
+
73
+ handleInsertClick = () => this.#submitFromDropdown();
74
+
75
+ handleCancelClick = () => {
76
+ this.shadowRoot?.querySelector('k-dropdown')?.close();
77
+ };
78
+
79
+ #submitFromDropdown() {
80
+ const url = this.#urlInput?.value.trim() || '';
81
+ const alt = this.#altInput?.value.trim() || '';
82
+ if(!url){
83
+ this.#urlInput?.focus();
84
+ return;
85
+ }
86
+ this.#insert(url, alt);
87
+ this.shadowRoot?.querySelector('k-dropdown')?.close();
88
+ }
89
+
90
+ get #urlInput() { return this.shadowRoot?.querySelector('.image-url'); }
91
+ get #altInput() { return this.shadowRoot?.querySelector('.image-alt'); }
92
+
93
+ /*
94
+ Dialog flow — modal version, kept for consumers who want it.
95
+ */
96
+ #openDialog() {
97
+ const editor = this.editor;
98
+ if(!editor) return;
99
+ const sel = this.getSelection();
100
+
101
+ const urlInput = document.createElement('input');
102
+ urlInput.type = 'url';
103
+ urlInput.placeholder = 'https://example.com/image.png';
104
+ urlInput.required = true;
105
+ urlInput.style.cssText = 'padding: 0.5rem; border: 1px solid var(--c_border); border-radius: var(--radius); font: inherit;';
106
+
107
+ const altInput = document.createElement('input');
108
+ altInput.type = 'text';
109
+ altInput.placeholder = 'Description for screen readers';
110
+ altInput.value = sel.text || '';
111
+ altInput.style.cssText = 'padding: 0.5rem; border: 1px solid var(--c_border); border-radius: var(--radius); font: inherit;';
112
+
113
+ const content = document.createElement('div');
114
+ content.className = 'p';
115
+ content.style.cssText = 'display: flex; flex-direction: column; gap: 1rem;';
116
+ content.innerHTML = `
117
+ <div style="display: flex; flex-direction: column; gap: 0.25rem;">
118
+ <label style="font-weight: bold;">Image URL</label>
119
+ </div>
120
+ <div style="display: flex; flex-direction: column; gap: 0.25rem;">
121
+ <label style="font-weight: bold;">Alt text</label>
122
+ </div>
123
+ `;
124
+ content.children[0].appendChild(urlInput);
125
+ content.children[1].appendChild(altInput);
126
+
127
+ Dialog.create(content, {
128
+ title: 'Insert Image',
129
+ cancelText: 'Cancel',
130
+ confirmText: 'Insert',
131
+ confirmClasses: 'success ml',
132
+ confirmAction: (event) => {
133
+ const url = urlInput.value.trim();
134
+ if(!url){
135
+ event.keepDialogOpen = true;
136
+ urlInput.focus();
137
+ return;
138
+ }
139
+ this.#insert(url, altInput.value.trim());
140
+ }
141
+ });
142
+ queueMicrotask(() => urlInput.focus());
143
+ }
144
+
145
+ render() {
146
+ if(this.popup === 'dialog'){
147
+ return html`
148
+ <button
149
+ type="button"
150
+ class=${this.btnClass}
151
+ title=${this.label}
152
+ aria-label=${this.label}
153
+ @click=${this.handleClick}
154
+ >
155
+ <k-icon name="image"></k-icon>
156
+ </button>
157
+ `;
158
+ }
159
+ return html`
160
+ <k-dropdown
161
+ @opened=${this.handleDropdownOpened}
162
+ @closed=${this.handleDropdownClosed}
163
+ >
164
+ <button
165
+ slot="trigger"
166
+ type="button"
167
+ class=${this.btnClass}
168
+ title=${this.label}
169
+ aria-label=${this.label}
170
+ >
171
+ <k-icon name="image"></k-icon>
172
+ </button>
173
+ <div class="image-form" @keydown=${this.handleFormKeydown}>
174
+ <label>Image URL</label>
175
+ <input
176
+ class="image-url"
177
+ type="url"
178
+ required
179
+ placeholder="https://example.com/image.png"
180
+ />
181
+ <label>Alt text</label>
182
+ <input
183
+ class="image-alt"
184
+ type="text"
185
+ placeholder="Description for screen readers"
186
+ />
187
+ <div class="image-actions">
188
+ <button type="button" class="no-btn" @click=${this.handleCancelClick}>Cancel</button>
189
+ <button type="button" class="no-btn primary" @click=${this.handleInsertClick}>Insert</button>
190
+ </div>
191
+ </div>
192
+ </k-dropdown>
193
+ `;
194
+ }
195
+
196
+ static styles = [
197
+ MarkdownEditorControl.styles,
198
+ css`
199
+ .image-form {
200
+ display: flex;
201
+ flex-direction: column;
202
+ gap: 0.5rem;
203
+ padding: 0.75rem;
204
+ min-width: 16rem;
205
+ }
206
+ .image-form label {
207
+ font-weight: bold;
208
+ font-size: 0.875rem;
209
+ }
210
+ .image-form input {
211
+ padding: 0.5rem;
212
+ border: 1px solid var(--c_border);
213
+ border-radius: var(--radius);
214
+ font: inherit;
215
+ background: var(--c_bg);
216
+ color: var(--tc);
217
+ }
218
+ .image-form input:focus-visible {
219
+ outline: none;
220
+ box-shadow: var(--focus_shadow);
221
+ }
222
+ .image-actions {
223
+ display: flex;
224
+ justify-content: flex-end;
225
+ gap: 0.5rem;
226
+ margin-top: 0.25rem;
227
+ }
228
+ .image-actions button {
229
+ padding: 0.4rem 0.8rem;
230
+ border: 1px solid var(--c_border);
231
+ border-radius: var(--radius);
232
+ background: var(--c_bg);
233
+ color: var(--tc);
234
+ cursor: pointer;
235
+ font: inherit;
236
+ }
237
+ .image-actions button.primary {
238
+ background: var(--c_primary);
239
+ color: white;
240
+ border-color: var(--c_primary);
241
+ }
242
+ .image-actions button:focus-visible {
243
+ outline: none;
244
+ box-shadow: var(--focus_shadow);
245
+ }
246
+ `
247
+ ];
248
+ }
249
+
250
+ customElements.define('k-md-image', MarkdownImage);
@@ -0,0 +1,30 @@
1
+ import MarkdownEditorControl from './MarkdownEditorControl.js';
2
+ import { html } from '../../lit-all.min.js';
3
+ import '../Icon.js';
4
+
5
+ export default class MarkdownItalic extends MarkdownEditorControl {
6
+ constructor() {
7
+ super();
8
+ this.label = 'Italic (Cmd/Ctrl+I)';
9
+ }
10
+
11
+ command() {
12
+ this.wrapSelection('_', '_', 'italic text');
13
+ }
14
+
15
+ render() {
16
+ return html`
17
+ <button
18
+ type="button"
19
+ class=${this.btnClass}
20
+ title=${this.label}
21
+ aria-label=${this.label}
22
+ @click=${this.handleClick}
23
+ >
24
+ <k-icon name="format_italic"></k-icon>
25
+ </button>
26
+ `;
27
+ }
28
+ }
29
+
30
+ customElements.define('k-md-italic', MarkdownItalic);
@@ -0,0 +1,74 @@
1
+ import MarkdownEditorControl from './MarkdownEditorControl.js';
2
+ import { html } from '../../lit-all.min.js';
3
+ import '../Icon.js';
4
+
5
+ /*
6
+ Link control. Wraps the selection in `[text](url)`. If the user has selected
7
+ text it becomes the link text and the URL placeholder is selected for them
8
+ to overtype. If nothing is selected, a `link text` placeholder is used and
9
+ the user can tab through to the URL.
10
+ */
11
+ export default class MarkdownLink extends MarkdownEditorControl {
12
+ constructor() {
13
+ super();
14
+ this.label = 'Link';
15
+ }
16
+
17
+ command() {
18
+ const sel = this.getSelection();
19
+ const editor = this.editor;
20
+ if(!editor) return;
21
+ const ta = editor.textarea;
22
+ if(!ta) return;
23
+
24
+ const fireInput = (next, selStart, selEnd) => {
25
+ ta.value = next;
26
+ ta.selectionStart = selStart;
27
+ ta.selectionEnd = selEnd;
28
+ ta.focus();
29
+ editor.value = next;
30
+ ta.dispatchEvent(new Event('input', { bubbles: true }));
31
+ };
32
+
33
+ // Toggle: if the selection (or just the cursor) sits anywhere inside an
34
+ // existing `[label](url)`, unwrap that whole link to just its label. This
35
+ // works whether the user has the label, the URL, or nothing selected —
36
+ // they just have to be somewhere inside the link.
37
+ const re = /\[([^\]]*)\]\([^)]*\)/g;
38
+ let m;
39
+ while((m = re.exec(ta.value)) !== null){
40
+ const matchStart = m.index;
41
+ const matchEnd = matchStart + m[0].length;
42
+ if(sel.start >= matchStart && sel.end <= matchEnd){
43
+ const label = m[1];
44
+ const next = ta.value.substring(0, matchStart) + label + ta.value.substring(matchEnd);
45
+ fireInput(next, matchStart, matchStart + label.length);
46
+ return;
47
+ }
48
+ }
49
+
50
+ // Otherwise insert a new link with the URL portion pre-selected so the
51
+ // user can immediately overtype it.
52
+ const text = sel.text || 'link text';
53
+ const inserted = `[${text}](url)`;
54
+ const next = ta.value.substring(0, sel.start) + inserted + ta.value.substring(sel.end);
55
+ const urlStart = sel.start + 1 + text.length + 2; // past `[text](`
56
+ fireInput(next, urlStart, urlStart + 'url'.length);
57
+ }
58
+
59
+ render() {
60
+ return html`
61
+ <button
62
+ type="button"
63
+ class=${this.btnClass}
64
+ title=${this.label}
65
+ aria-label=${this.label}
66
+ @click=${this.handleClick}
67
+ >
68
+ <k-icon name="link"></k-icon>
69
+ </button>
70
+ `;
71
+ }
72
+ }
73
+
74
+ customElements.define('k-md-link', MarkdownLink);
@@ -0,0 +1,158 @@
1
+ import ShadowComponent from '../ShadowComponent.js';
2
+ import { html, css } from '../../lit-all.min.js';
3
+
4
+ /*
5
+ Base class for <k-markdown-editor> control buttons. Subclass and override
6
+ `command()` (or `render()` for custom UI) to wire up a toolbar button to a
7
+ text manipulation. The editor is found via closest('k-markdown-editor');
8
+ helper methods on the editor (wrapSelection, insertLinePrefix,
9
+ replaceSelection, insertAtCursor) handle the textarea selection math.
10
+
11
+ Mirrors HtmlEditorControl in spirit, scoped to MarkdownEditor.
12
+ */
13
+
14
+ export default class MarkdownEditorControl extends ShadowComponent {
15
+ static properties = {
16
+ btnClass: { type: String, attribute: 'btn-class' },
17
+ label: { type: String, reflect: true },
18
+ hidden: { type: Boolean, reflect: true }
19
+ };
20
+
21
+ constructor() {
22
+ super();
23
+ this.btnClass = 'no-btn ctrl';
24
+ this.label = '';
25
+ this.hidesInPreviewMode = true;
26
+ }
27
+
28
+ connectedCallback() {
29
+ super.connectedCallback();
30
+ const editor = this.editor;
31
+ if(!editor) return;
32
+ this.boundEditor = editor;
33
+ this.modeHandler = () => {
34
+ const shouldHide = this.hidesInPreviewMode && this.boundEditor.mode === 'preview';
35
+ if(this.hidden !== shouldHide){
36
+ this.hidden = shouldHide;
37
+ }
38
+ };
39
+ // Reflect initial state
40
+ this.modeHandler();
41
+ editor.addEventListener('mode-change', this.modeHandler);
42
+ }
43
+
44
+ disconnectedCallback() {
45
+ super.disconnectedCallback();
46
+ if(this.boundEditor && this.modeHandler){
47
+ this.boundEditor.removeEventListener('mode-change', this.modeHandler);
48
+ }
49
+ this.boundEditor = null;
50
+ this.modeHandler = null;
51
+ }
52
+
53
+ /*
54
+ Editor lookup. `closest()` works when the control is slotted in light
55
+ DOM (the most common case). When the control is rendered as slot
56
+ fallback content it lives inside the editor's own shadow root, where
57
+ `closest()` can't cross the shadow boundary — fall through to the
58
+ shadow root's host in that case.
59
+ */
60
+ get editor() {
61
+ const direct = this.closest('k-markdown-editor');
62
+ if(direct) return direct;
63
+ const root = this.getRootNode();
64
+ if(root instanceof ShadowRoot && root.host?.tagName === 'K-MARKDOWN-EDITOR'){
65
+ return root.host;
66
+ }
67
+ return null;
68
+ }
69
+
70
+ /*
71
+ Convenience helpers — subclasses use these in `command()` to manipulate
72
+ the editor's textarea. Each is a thin pass-through with a guard.
73
+ */
74
+ wrapSelection(prefix, suffix, placeholder) {
75
+ this.editor?.wrapSelection(prefix, suffix, placeholder);
76
+ }
77
+
78
+ insertAtCursor(text) {
79
+ this.editor?.insertAtCursor(text);
80
+ }
81
+
82
+ insertLinePrefix(prefix, replacePattern) {
83
+ this.editor?.insertLinePrefix(prefix, replacePattern);
84
+ }
85
+
86
+ replaceInSelectedLines(pattern, replacement) {
87
+ this.editor?.replaceInSelectedLines(pattern, replacement);
88
+ }
89
+
90
+ replaceSelection(text, options) {
91
+ this.editor?.replaceSelection(text, options);
92
+ }
93
+
94
+ getSelection() {
95
+ return this.editor?.getSelection() || { start: 0, end: 0, text: '' };
96
+ }
97
+
98
+ /*
99
+ Override in subclasses to do the work. Default no-op.
100
+ */
101
+ command() {}
102
+
103
+ handleClick = (e) => {
104
+ e.preventDefault();
105
+ if(this.hidden) return;
106
+ this.command();
107
+ };
108
+
109
+ /*
110
+ Default render: a button with the provided label (or whatever you slot
111
+ in). Subclasses can override for custom UI (icon button, dropdown, etc.).
112
+ */
113
+ render() {
114
+ return html`
115
+ <button
116
+ type="button"
117
+ class=${this.btnClass}
118
+ title=${this.label}
119
+ aria-label=${this.label}
120
+ @click=${this.handleClick}
121
+ >
122
+ <slot>${this.label}</slot>
123
+ </button>
124
+ `;
125
+ }
126
+
127
+ static styles = css`
128
+ :host {
129
+ display: inline-flex;
130
+ }
131
+ :host([hidden]) {
132
+ display: none;
133
+ }
134
+ .ctrl {
135
+ display: inline-flex;
136
+ align-items: center;
137
+ justify-content: center;
138
+ padding: var(--spacer_h);
139
+ border: 1px solid transparent;
140
+ border-radius: var(--radius);
141
+ background: transparent;
142
+ color: var(--tc_muted);
143
+ cursor: pointer;
144
+ font: inherit;
145
+ transition: background var(--animation_ms), color var(--animation_ms);
146
+ }
147
+ .ctrl:hover {
148
+ background: var(--c_bg__alt);
149
+ color: var(--tc);
150
+ }
151
+ .ctrl:focus-visible {
152
+ outline: none;
153
+ box-shadow: var(--focus_shadow);
154
+ }
155
+ `;
156
+ }
157
+
158
+ customElements.define('k-md-control', MarkdownEditorControl);
@@ -0,0 +1,86 @@
1
+ import MarkdownEditorControl from './MarkdownEditorControl.js';
2
+ import { html, css } from '../../lit-all.min.js';
3
+ import '../Icon.js';
4
+ import '../Dropdown.js';
5
+
6
+ /*
7
+ Menu control. Renders as a single toolbar button that, when clicked, opens
8
+ a Dropdown containing whatever child controls have been slotted in. Useful
9
+ for grouping less-frequent actions (like the heading picker) so they don't
10
+ take up space in the main toolbar.
11
+
12
+ The trigger button content is fully customizable via the `trigger` slot —
13
+ drop in whatever combination of icons / text you want. If nothing is
14
+ slotted, a default `menu` icon is shown.
15
+
16
+ The default slot is the menu's items.
17
+
18
+ Example (default trigger):
19
+ <k-md-menu label="Heading">
20
+ <k-md-format-block tag="h1"></k-md-format-block>
21
+ ...
22
+ </k-md-menu>
23
+
24
+ Example (custom trigger showing a heading-range icon):
25
+ <k-md-menu label="Heading">
26
+ <span slot="trigger">
27
+ <k-icon name="format_h1"></k-icon> - <k-icon name="format_h6"></k-icon>
28
+ </span>
29
+ <k-md-format-block tag="h1"></k-md-format-block>
30
+ ...
31
+ </k-md-menu>
32
+ */
33
+ export default class MarkdownMenu extends MarkdownEditorControl {
34
+ constructor() {
35
+ super();
36
+ this.label = 'Menu';
37
+ }
38
+
39
+ /*
40
+ The trigger is wired to Dropdown's own click handling, so the
41
+ base-class handleClick (which calls command()) would fire on top and
42
+ create a feedback loop. command() is a no-op here; child controls inside
43
+ the dropdown drive their own commands.
44
+ */
45
+ command() {}
46
+
47
+ render() {
48
+ return html`
49
+ <k-dropdown>
50
+ <button
51
+ slot="trigger"
52
+ type="button"
53
+ class=${this.btnClass}
54
+ title=${this.label}
55
+ aria-label=${this.label}
56
+ >
57
+ <slot name="trigger">
58
+ <k-icon name="menu"></k-icon>
59
+ </slot>
60
+ </button>
61
+ <slot></slot>
62
+ </k-dropdown>
63
+ `;
64
+ }
65
+
66
+ static styles = [
67
+ MarkdownEditorControl.styles,
68
+ css`
69
+ :host {
70
+ display: inline-flex;
71
+ }
72
+ k-dropdown {
73
+ display: inline-flex;
74
+ }
75
+ /* Stretch slotted menu items so each one fills the dropdown
76
+ horizontally — Dropdown's built-in styles only target raw
77
+ <a>/<button> children, so custom-element items need this themselves. */
78
+ ::slotted(:not([slot="trigger"])) {
79
+ display: block;
80
+ width: 100%;
81
+ }
82
+ `
83
+ ];
84
+ }
85
+
86
+ customElements.define('k-md-menu', MarkdownMenu);
@@ -0,0 +1,69 @@
1
+ import MarkdownEditorControl from './MarkdownEditorControl.js';
2
+ import { html } from '../../lit-all.min.js';
3
+ import '../Icon.js';
4
+
5
+ /*
6
+ Numbered list control. Prefixes each non-empty selected line with `N. `
7
+ using sequential numbers. Markdown renderers accept all-1. as well, but
8
+ numbering them properly keeps the source readable.
9
+ */
10
+ export default class MarkdownNumberedList extends MarkdownEditorControl {
11
+ constructor() {
12
+ super();
13
+ this.label = 'Numbered list';
14
+ }
15
+
16
+ command() {
17
+ const editor = this.editor;
18
+ if(!editor) return;
19
+ const ta = editor.textarea;
20
+ if(!ta) return;
21
+ if(editor.mode !== 'write') editor.mode = 'write';
22
+ editor.updateComplete.then(() => {
23
+ ta.focus();
24
+ const { selectionStart: start, selectionEnd: end, value } = ta;
25
+ const lineStart = value.lastIndexOf('\n', start - 1) + 1;
26
+ const lineEndIdx = value.indexOf('\n', end);
27
+ const lineEnd = lineEndIdx === -1 ? value.length : lineEndIdx;
28
+ const block = value.substring(lineStart, lineEnd);
29
+ const lines = block.split('\n');
30
+ const numbered = /^\d+\. /;
31
+ const nonEmpty = lines.filter(l => l.length > 0);
32
+ const allNumbered = nonEmpty.length > 0 && nonEmpty.every(l => numbered.test(l));
33
+ let processed;
34
+ if(allNumbered){
35
+ processed = lines.map(l => l.replace(numbered, ''));
36
+ } else {
37
+ let n = 1;
38
+ processed = lines.map(l => {
39
+ if(!l) return l;
40
+ const stripped = l.replace(numbered, '');
41
+ return `${n++}. ${stripped}`;
42
+ });
43
+ }
44
+ const nextBlock = processed.join('\n');
45
+ const next = value.substring(0, lineStart) + nextBlock + value.substring(lineEnd);
46
+ ta.value = next;
47
+ ta.selectionStart = lineStart;
48
+ ta.selectionEnd = lineStart + nextBlock.length;
49
+ editor.value = next;
50
+ ta.dispatchEvent(new Event('input', { bubbles: true }));
51
+ });
52
+ }
53
+
54
+ render() {
55
+ return html`
56
+ <button
57
+ type="button"
58
+ class=${this.btnClass}
59
+ title=${this.label}
60
+ aria-label=${this.label}
61
+ @click=${this.handleClick}
62
+ >
63
+ <k-icon name="format_list_numbered"></k-icon>
64
+ </button>
65
+ `;
66
+ }
67
+ }
68
+
69
+ customElements.define('k-md-numbered-list', MarkdownNumberedList);
@@ -0,0 +1,30 @@
1
+ import MarkdownEditorControl from './MarkdownEditorControl.js';
2
+ import { html } from '../../lit-all.min.js';
3
+ import '../Icon.js';
4
+
5
+ export default class MarkdownQuote extends MarkdownEditorControl {
6
+ constructor() {
7
+ super();
8
+ this.label = 'Quote';
9
+ }
10
+
11
+ command() {
12
+ this.insertLinePrefix('> ');
13
+ }
14
+
15
+ render() {
16
+ return html`
17
+ <button
18
+ type="button"
19
+ class=${this.btnClass}
20
+ title=${this.label}
21
+ aria-label=${this.label}
22
+ @click=${this.handleClick}
23
+ >
24
+ <k-icon name="format_quote"></k-icon>
25
+ </button>
26
+ `;
27
+ }
28
+ }
29
+
30
+ customElements.define('k-md-quote', MarkdownQuote);