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
@@ -7,7 +7,8 @@ export default class Resize extends ShadowComponent {
7
7
  /* Properties */
8
8
  static properties = {
9
9
  resizing: { type: String, reflect: true },
10
- dimention: { type: String, reflect: true }
10
+ dimention: { type: String, reflect: true },
11
+ disabled: { type: Boolean, reflect: true }
11
12
  };
12
13
 
13
14
  /*
@@ -17,6 +18,7 @@ export default class Resize extends ShadowComponent {
17
18
  super();
18
19
  this.resizing = '';
19
20
  this.dimension = '';
21
+ this.disabled = false;
20
22
  this.startSize = { width: 0, height: 0 };
21
23
  this.cleanupFuncs = [];
22
24
  }
@@ -35,6 +37,7 @@ export default class Resize extends ShadowComponent {
35
37
  Event Handlers
36
38
  */
37
39
  dragStartHandler = ({ element }) => {
40
+ if(this.disabled) return;
38
41
  const { width, height } = this.getBoundingClientRect();
39
42
  this.startSize.width = width;
40
43
  this.startSize.height = height;
@@ -46,14 +49,17 @@ export default class Resize extends ShadowComponent {
46
49
  };
47
50
 
48
51
  dragSideHandler = ({ x }) => {
52
+ if(this.disabled) return;
49
53
  this.style.width = this.startSize.width + x + 'px';
50
54
  };
51
55
 
52
56
  dragBottomHandler = ({ y }) => {
57
+ if(this.disabled) return;
53
58
  this.style.height = this.startSize.height + y + 'px';
54
59
  };
55
60
 
56
61
  dragCornerHandler = ({ x, y }) => {
62
+ if(this.disabled) return;
57
63
  this.style.width = this.startSize.width + x + 'px';
58
64
  this.style.height = this.startSize.height + y + 'px';
59
65
  };
@@ -177,6 +183,16 @@ export default class Resize extends ShadowComponent {
177
183
  :host([dimension="width"]) #corner {
178
184
  display: none;
179
185
  }
186
+ /* Disabled: handles can't capture pointer events, so a drag can't
187
+ start in the first place; visual cue: handles fade out and the
188
+ cursor reverts to default. */
189
+ :host([disabled]) #side,
190
+ :host([disabled]) #bottom,
191
+ :host([disabled]) #corner {
192
+ pointer-events: none;
193
+ cursor: default;
194
+ opacity: 0.4;
195
+ }
180
196
  `;
181
197
 
182
198
  /*
@@ -1,5 +1,6 @@
1
1
  import { html, css } from '../lit-all.min.js';
2
2
  import ShadowComponent from './ShadowComponent.js';
3
+ import { getVoice as getPreferredVoice } from '../utils/voice.js';
3
4
  import './Icon.js';
4
5
 
5
6
  export default class TextToSpeech extends ShadowComponent {
@@ -53,9 +54,19 @@ export default class TextToSpeech extends ShadowComponent {
53
54
  }
54
55
 
55
56
  resolveVoice = () => {
56
- if(!this.voice || this.#unsupported) return null;
57
+ if(this.#unsupported) return null;
58
+ // Use the explicit `voice` attribute first; if none is set, fall back to
59
+ // the user's saved preference from the voice utility (so a site-wide
60
+ // VoiceSelector can drive every TextToSpeech that hasn't opted out).
61
+ const source = this.voice || getPreferredVoice() || '';
62
+ if(!source) return null;
57
63
  const voices = window.speechSynthesis.getVoices();
58
- return voices.find(v => v.name === this.voice) || null;
64
+ const candidates = source.split(',').map(s => s.trim()).filter(Boolean);
65
+ for(const name of candidates){
66
+ const match = voices.find(v => v.name === name);
67
+ if(match) return match;
68
+ }
69
+ return null;
59
70
  };
60
71
 
61
72
  /*
@@ -0,0 +1,271 @@
1
+ import { html, css } from '../lit-all.min.js';
2
+ import ShadowComponent from './ShadowComponent.js';
3
+ import {
4
+ getVoice,
5
+ setVoice,
6
+ subscribeToVoice,
7
+ subscribeToAvailableVoices
8
+ } from '../utils/voice.js';
9
+
10
+ const primaryLang = (tag) => (tag || '').split('-')[0].toLowerCase();
11
+
12
+ const browserLanguage = () => {
13
+ if(typeof navigator === 'undefined') return 'en';
14
+ return primaryLang(navigator.language || 'en') || 'en';
15
+ };
16
+
17
+ let displayNames = null;
18
+ try {
19
+ if(typeof Intl !== 'undefined' && Intl.DisplayNames){
20
+ const locale = (typeof navigator !== 'undefined' && navigator.language) || 'en';
21
+ displayNames = new Intl.DisplayNames([locale], { type: 'language' });
22
+ }
23
+ } catch(e) {}
24
+
25
+ // Cache for native-name lookups (Intl.DisplayNames in the language's own locale)
26
+ // so we don't construct a new instance per option per render.
27
+ const nativeNameCache = new Map();
28
+ const getNativeName = (code) => {
29
+ if(nativeNameCache.has(code)) return nativeNameCache.get(code);
30
+ let native = '';
31
+ try {
32
+ if(typeof Intl !== 'undefined' && Intl.DisplayNames){
33
+ const dn = new Intl.DisplayNames([code], { type: 'language' });
34
+ native = dn.of(code) || '';
35
+ }
36
+ } catch(e) {}
37
+ // Capitalize the first character so dropdown labels look consistent —
38
+ // Intl.DisplayNames returns "español"/"français" lowercase, which looks
39
+ // off next to the capitalized localized name.
40
+ if(native){
41
+ native = native.charAt(0).toUpperCase() + native.slice(1);
42
+ }
43
+ nativeNameCache.set(code, native);
44
+ return native;
45
+ };
46
+
47
+ // Render a language code as "Localized (Native)" — e.g. "Spanish (Español)",
48
+ // "Japanese (日本語)". The native name is a deliberate accessibility escape
49
+ // hatch: if the user accidentally has the page in a language they can't read,
50
+ // they can still recognize their own language by its native name. When the
51
+ // localized and native names match (user already in that language) we skip
52
+ // the duplicate.
53
+ const formatLanguage = (code) => {
54
+ let localized = code;
55
+ if(displayNames){
56
+ try {
57
+ localized = displayNames.of(code) || code;
58
+ } catch(e) {}
59
+ }
60
+ const native = getNativeName(code);
61
+ if(native && native.toLowerCase() !== localized.toLowerCase()){
62
+ return `${localized} (${native})`;
63
+ }
64
+ return localized;
65
+ };
66
+
67
+ // Strip the parenthetical "(language (region))" that OS-level voices often
68
+ // append to their name (e.g. "Eddy (German (Germany))" → "Eddy"). The actual
69
+ // `value` we submit to SpeechSynthesis still uses the full name.
70
+ const displayVoiceName = (name) => {
71
+ const cleaned = name.split('(')[0].trim();
72
+ return cleaned || name;
73
+ };
74
+
75
+ export default class VoiceSelector extends ShadowComponent {
76
+ static properties = {
77
+ language: { type: String, reflect: true },
78
+ placeholder: { type: String, reflect: true },
79
+ disabled: { type: Boolean, reflect: true },
80
+ voices: { state: true },
81
+ selected: { state: true },
82
+ currentLang: { state: true }
83
+ };
84
+
85
+ #unsubVoices = null;
86
+ #unsubSelected = null;
87
+ #unsupported = false;
88
+ #langInitialized = false;
89
+
90
+ /*
91
+ Lifecycle Callbacks
92
+ */
93
+ constructor() {
94
+ super();
95
+ this.language = '';
96
+ this.placeholder = 'Browser default';
97
+ this.disabled = false;
98
+ this.voices = [];
99
+ this.selected = '';
100
+ this.currentLang = '';
101
+ }
102
+
103
+ connectedCallback() {
104
+ super.connectedCallback();
105
+ if(typeof window === 'undefined' || !window.speechSynthesis){
106
+ this.#unsupported = true;
107
+ return;
108
+ }
109
+ this.#unsubVoices = subscribeToAvailableVoices(list => {
110
+ this.voices = list;
111
+ this.#initLanguageOnce();
112
+ });
113
+ this.#unsubSelected = subscribeToVoice(name => {
114
+ this.selected = name || '';
115
+ // Keep the language dropdown in sync with externally-set voices
116
+ if(name){
117
+ const match = this.voices.find(v => v.name === name);
118
+ if(match && match.lang){
119
+ this.currentLang = primaryLang(match.lang);
120
+ }
121
+ }
122
+ });
123
+ }
124
+
125
+ disconnectedCallback() {
126
+ super.disconnectedCallback();
127
+ if(this.#unsubVoices) this.#unsubVoices();
128
+ if(this.#unsubSelected) this.#unsubSelected();
129
+ this.#unsubVoices = null;
130
+ this.#unsubSelected = null;
131
+ }
132
+
133
+ /*
134
+ Utility
135
+ */
136
+ #initLanguageOnce = () => {
137
+ if(this.#langInitialized || this.voices.length === 0) return;
138
+ this.#langInitialized = true;
139
+ // Priority: existing voice's language > attribute > navigator > first available
140
+ const fromVoice = this.selected
141
+ ? this.voices.find(v => v.name === this.selected)
142
+ : null;
143
+ if(fromVoice && fromVoice.lang){
144
+ this.currentLang = primaryLang(fromVoice.lang);
145
+ return;
146
+ }
147
+ if(this.language){
148
+ this.currentLang = primaryLang(this.language);
149
+ return;
150
+ }
151
+ const navLang = browserLanguage();
152
+ const langs = this.availableLanguages;
153
+ if(langs.includes(navLang)){
154
+ this.currentLang = navLang;
155
+ } else if(langs.length > 0){
156
+ this.currentLang = langs[0];
157
+ }
158
+ };
159
+
160
+ get availableLanguages() {
161
+ const langs = new Set();
162
+ for(const v of this.voices){
163
+ if(v.lang) langs.add(primaryLang(v.lang));
164
+ }
165
+ return [...langs].sort();
166
+ }
167
+
168
+ get voicesForCurrentLanguage() {
169
+ if(!this.currentLang) return this.voices;
170
+ return this.voices.filter(v => primaryLang(v.lang) === this.currentLang);
171
+ }
172
+
173
+ /*
174
+ Event Handlers
175
+ */
176
+ handleLanguageChange = (e) => {
177
+ if(this.disabled) return;
178
+ this.currentLang = e.target.value;
179
+ // If the saved voice no longer exists in this language, clear it
180
+ const stillValid = this.voicesForCurrentLanguage.some(v => v.name === this.selected);
181
+ if(!stillValid){
182
+ setVoice('');
183
+ }
184
+ };
185
+
186
+ handleVoiceChange = (e) => {
187
+ if(this.disabled) return;
188
+ const value = e.target.value;
189
+ setVoice(value);
190
+ this.dispatchEvent(new CustomEvent('change', {
191
+ detail: { voice: value, language: this.currentLang },
192
+ bubbles: true
193
+ }));
194
+ };
195
+
196
+ /*
197
+ Public Methods
198
+ */
199
+ refresh = () => {
200
+ if(this.#unsupported) return;
201
+ this.voices = window.speechSynthesis.getVoices();
202
+ this.#initLanguageOnce();
203
+ };
204
+
205
+ /*
206
+ Rendering
207
+ */
208
+ render() {
209
+ if(this.#unsupported){
210
+ return html`
211
+ <select disabled aria-label="Voice selector (unsupported)">
212
+ <option>Speech synthesis not supported</option>
213
+ </select>
214
+ `;
215
+ }
216
+ const langs = this.availableLanguages;
217
+ const voicesInLang = this.voicesForCurrentLanguage;
218
+ return html`
219
+ <div class="row">
220
+ <div class="col" style="min-width: 10rem; flex: 0 0 auto">
221
+ <select
222
+ class="lang"
223
+ aria-label="Language"
224
+ ?disabled=${this.disabled || langs.length === 0}
225
+ .value=${this.currentLang}
226
+ @change=${this.handleLanguageChange}
227
+ >
228
+ ${langs.map(code => html`
229
+ <option value=${code} ?selected=${code === this.currentLang}>${formatLanguage(code)}</option>
230
+ `)}
231
+ </select>
232
+ </div>
233
+ <div class="col">
234
+ <select
235
+ class="voice"
236
+ aria-label="Voice"
237
+ ?disabled=${this.disabled}
238
+ .value=${this.selected}
239
+ @change=${this.handleVoiceChange}
240
+ >
241
+ <option value="">${this.placeholder}</option>
242
+ ${voicesInLang.map(v => html`
243
+ <option
244
+ value=${v.name}
245
+ ?selected=${v.name === this.selected}
246
+ >${displayVoiceName(v.name)}</option>
247
+ `)}
248
+ </select>
249
+ </div>
250
+ </div>
251
+ `;
252
+ }
253
+
254
+ /*
255
+ Styles
256
+ */
257
+ static styles = css`
258
+ :host {
259
+ display: block;
260
+ }
261
+ :host([disabled]) {
262
+ opacity: 0.5;
263
+ pointer-events: none;
264
+ }
265
+ select {
266
+ width: 100%;
267
+ }
268
+ `;
269
+ }
270
+
271
+ customElements.define('k-voice-selector', VoiceSelector);
@@ -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 MarkdownBold extends MarkdownEditorControl {
6
+ constructor() {
7
+ super();
8
+ this.label = 'Bold (Cmd/Ctrl+B)';
9
+ }
10
+
11
+ command() {
12
+ this.wrapSelection('**', '**', 'bold 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_bold"></k-icon>
25
+ </button>
26
+ `;
27
+ }
28
+ }
29
+
30
+ customElements.define('k-md-bold', MarkdownBold);
@@ -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 MarkdownBulletList extends MarkdownEditorControl {
6
+ constructor() {
7
+ super();
8
+ this.label = 'Bulleted list';
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_list_bulleted"></k-icon>
25
+ </button>
26
+ `;
27
+ }
28
+ }
29
+
30
+ customElements.define('k-md-bullet-list', MarkdownBulletList);
@@ -0,0 +1,39 @@
1
+ import MarkdownEditorControl from './MarkdownEditorControl.js';
2
+ import { html } from '../../lit-all.min.js';
3
+ import '../Icon.js';
4
+
5
+ /*
6
+ Toggle inline code (backticks) for the selection. If the selection spans
7
+ multiple lines, a triple-backtick fenced block is inserted instead.
8
+ */
9
+ export default class MarkdownCode extends MarkdownEditorControl {
10
+ constructor() {
11
+ super();
12
+ this.label = 'Code';
13
+ }
14
+
15
+ command() {
16
+ const sel = this.getSelection();
17
+ if(sel.text.includes('\n')){
18
+ this.wrapSelection('```\n', '\n```', 'code');
19
+ } else {
20
+ this.wrapSelection('`', '`', 'code');
21
+ }
22
+ }
23
+
24
+ render() {
25
+ return html`
26
+ <button
27
+ type="button"
28
+ class=${this.btnClass}
29
+ title=${this.label}
30
+ aria-label=${this.label}
31
+ @click=${this.handleClick}
32
+ >
33
+ <k-icon name="code"></k-icon>
34
+ </button>
35
+ `;
36
+ }
37
+ }
38
+
39
+ customElements.define('k-md-code', MarkdownCode);
@@ -0,0 +1,102 @@
1
+ import MarkdownEditorControl from './MarkdownEditorControl.js';
2
+ import { html, css } from '../../lit-all.min.js';
3
+ import '../Icon.js';
4
+
5
+ /*
6
+ Heading-block picker designed to go inside a `<k-md-menu>` (or any
7
+ container that wants menu-item-styled controls). The `tag` attribute
8
+ selects which heading level to switch the current line(s) to:
9
+
10
+ tag="h1" .. "h6" — Heading 1-6 (sets the `#` prefix, swapping any
11
+ existing heading level for the new one)
12
+
13
+ Renders as a full-width button with an icon and a text label so it reads
14
+ cleanly as a menu item.
15
+ */
16
+ const TAG_LABELS = {
17
+ h1: 'Heading 1',
18
+ h2: 'Heading 2',
19
+ h3: 'Heading 3',
20
+ h4: 'Heading 4',
21
+ h5: 'Heading 5',
22
+ h6: 'Heading 6'
23
+ };
24
+
25
+ const TAG_ICONS = {
26
+ h1: 'format_h1',
27
+ h2: 'format_h2',
28
+ h3: 'format_h3',
29
+ h4: 'format_h4',
30
+ h5: 'format_h5',
31
+ h6: 'format_h6'
32
+ };
33
+
34
+ export default class MarkdownFormatBlock extends MarkdownEditorControl {
35
+ static properties = {
36
+ ...MarkdownEditorControl.properties,
37
+ tag: { type: String, reflect: true }
38
+ };
39
+
40
+ constructor() {
41
+ super();
42
+ this.tag = 'h2';
43
+ this.label = '';
44
+ }
45
+
46
+ command() {
47
+ const tag = (this.tag || 'h2').toLowerCase();
48
+ if(/^h[1-6]$/.test(tag)){
49
+ const lvl = parseInt(tag.slice(1), 10);
50
+ this.insertLinePrefix('#'.repeat(lvl) + ' ', /^#{1,6} /);
51
+ }
52
+ }
53
+
54
+ get #displayLabel() {
55
+ if(this.label) return this.label;
56
+ return TAG_LABELS[(this.tag || 'h2').toLowerCase()] || this.tag;
57
+ }
58
+
59
+ get #iconName() {
60
+ return TAG_ICONS[(this.tag || 'h2').toLowerCase()] || 'format_h2';
61
+ }
62
+
63
+ render() {
64
+ return html`
65
+ <button
66
+ type="button"
67
+ class=${this.btnClass}
68
+ title=${this.#displayLabel}
69
+ aria-label=${this.#displayLabel}
70
+ @click=${this.handleClick}
71
+ >
72
+ <k-icon name=${this.#iconName}></k-icon>
73
+ <span class="fb-label">${this.#displayLabel}</span>
74
+ </button>
75
+ `;
76
+ }
77
+
78
+ static styles = [
79
+ MarkdownEditorControl.styles,
80
+ css`
81
+ :host {
82
+ display: inline-flex;
83
+ }
84
+ .ctrl {
85
+ gap: 0.5rem;
86
+ justify-content: flex-start;
87
+ white-space: nowrap;
88
+ }
89
+ /* When this is slotted into something that stretches it (a menu),
90
+ the button fills the row instead of staying compact. */
91
+ :host([slot]) .ctrl,
92
+ :host(*) .ctrl {
93
+ width: 100%;
94
+ }
95
+ .fb-label {
96
+ font-size: 0.875rem;
97
+ }
98
+ `
99
+ ];
100
+ }
101
+
102
+ customElements.define('k-md-format-block', MarkdownFormatBlock);
@@ -0,0 +1,45 @@
1
+ import MarkdownEditorControl from './MarkdownEditorControl.js';
2
+ import { html } from '../../lit-all.min.js';
3
+ import '../Icon.js';
4
+
5
+ /*
6
+ Heading control. Defaults to H2 (the typical "section title" level inside
7
+ a comment / chat / form). Override the level with the `level` attribute
8
+ (1–6) — e.g. <k-md-heading level="3"></k-md-heading>.
9
+ */
10
+ export default class MarkdownHeading extends MarkdownEditorControl {
11
+ static properties = {
12
+ ...MarkdownEditorControl.properties,
13
+ level: { type: Number, reflect: true }
14
+ };
15
+
16
+ constructor() {
17
+ super();
18
+ this.level = 2;
19
+ this.label = 'Heading';
20
+ }
21
+
22
+ command() {
23
+ const lvl = Math.min(6, Math.max(1, Number(this.level) || 2));
24
+ // Pass a replacePattern so clicking a different heading level swaps the
25
+ // existing one out instead of stacking another `#` group on top.
26
+ this.insertLinePrefix('#'.repeat(lvl) + ' ', /^#{1,6} /);
27
+ }
28
+
29
+ render() {
30
+ const iconLevel = Math.min(6, Math.max(1, Number(this.level) || 2));
31
+ return html`
32
+ <button
33
+ type="button"
34
+ class=${this.btnClass}
35
+ title="${this.label} ${iconLevel}"
36
+ aria-label="${this.label} ${iconLevel}"
37
+ @click=${this.handleClick}
38
+ >
39
+ <k-icon name="format_h${iconLevel}"></k-icon>
40
+ </button>
41
+ `;
42
+ }
43
+ }
44
+
45
+ customElements.define('k-md-heading', MarkdownHeading);