kempo-ui 0.4.5 → 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 (170) 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/markdownEditorControls/Bold.js +11 -0
  8. package/dist/components/markdownEditorControls/BulletList.js +11 -0
  9. package/dist/components/markdownEditorControls/Code.js +11 -0
  10. package/dist/components/markdownEditorControls/FormatBlock.js +30 -0
  11. package/dist/components/markdownEditorControls/Heading.js +11 -0
  12. package/dist/components/markdownEditorControls/Image.js +93 -0
  13. package/dist/components/markdownEditorControls/Italic.js +11 -0
  14. package/dist/components/markdownEditorControls/Link.js +11 -0
  15. package/dist/components/markdownEditorControls/MarkdownEditorControl.js +39 -0
  16. package/dist/components/markdownEditorControls/Menu.js +30 -0
  17. package/dist/components/markdownEditorControls/NumberedList.js +11 -0
  18. package/dist/components/markdownEditorControls/Quote.js +11 -0
  19. package/dist/components/markdownEditorControls/SpeechToText.js +16 -0
  20. package/dist/components/markdownEditorControls/Strikethrough.js +11 -0
  21. package/dist/components/markdownEditorControls/Table.js +80 -0
  22. package/dist/utils/marked.esm.js +1 -0
  23. package/dist/utils/renderMarkdown.js +1 -0
  24. package/dist/utils/sanitizeHtml.js +1 -0
  25. package/docs/components/accordion.html +12 -0
  26. package/docs/components/aside.html +12 -0
  27. package/docs/components/calendar.html +12 -0
  28. package/docs/components/card.html +12 -0
  29. package/docs/components/chat.html +715 -0
  30. package/docs/components/code-editor.html +48 -0
  31. package/docs/components/color-picker.html +12 -0
  32. package/docs/components/combobox.html +12 -0
  33. package/docs/components/content-slider.html +12 -0
  34. package/docs/components/context.html +12 -0
  35. package/docs/components/dialog.html +12 -0
  36. package/docs/components/dropdown.html +12 -0
  37. package/docs/components/filter-list.html +12 -0
  38. package/docs/components/focus-capture.html +12 -0
  39. package/docs/components/html-editor.html +53 -0
  40. package/docs/components/hybrid-component.html +12 -0
  41. package/docs/components/icon.html +12 -0
  42. package/docs/components/import.html +12 -0
  43. package/docs/components/light-component.html +12 -0
  44. package/docs/components/markdown-editor.html +924 -0
  45. package/docs/components/nav-spacer.html +12 -0
  46. package/docs/components/nav.html +12 -0
  47. package/docs/components/photo-viewer.html +12 -0
  48. package/docs/components/progress.html +12 -0
  49. package/docs/components/resize.html +15 -0
  50. package/docs/components/rich-textarea.html +650 -0
  51. package/docs/components/shadow-component.html +12 -0
  52. package/docs/components/show-more.html +12 -0
  53. package/docs/components/slider.html +12 -0
  54. package/docs/components/sortable.html +12 -0
  55. package/docs/components/speech-to-text.html +12 -0
  56. package/docs/components/spinner.html +12 -0
  57. package/docs/components/split.html +12 -0
  58. package/docs/components/table.html +12 -0
  59. package/docs/components/tableControls.html +12 -0
  60. package/docs/components/tableCustomFields.html +12 -0
  61. package/docs/components/tableFetchRecords.html +12 -0
  62. package/docs/components/tableFieldSortHide.html +12 -0
  63. package/docs/components/tablePagination.html +12 -0
  64. package/docs/components/tablePlaceholder.html +12 -0
  65. package/docs/components/tableRecordEditing.html +12 -0
  66. package/docs/components/tableRecordFiltering.html +12 -0
  67. package/docs/components/tableRecordHiding.html +12 -0
  68. package/docs/components/tableRecordSearching.html +12 -0
  69. package/docs/components/tableRecordSelection.html +12 -0
  70. package/docs/components/tableRowControls.html +12 -0
  71. package/docs/components/tableServerSync.html +12 -0
  72. package/docs/components/tableSorting.html +12 -0
  73. package/docs/components/tabs.html +12 -0
  74. package/docs/components/tags.html +12 -0
  75. package/docs/components/text-to-speech.html +12 -0
  76. package/docs/components/theme-select.html +12 -0
  77. package/docs/components/theme-switcher.html +12 -0
  78. package/docs/components/time.html +12 -0
  79. package/docs/components/timestamp.html +12 -0
  80. package/docs/components/toast.html +12 -0
  81. package/docs/components/toggle.html +12 -0
  82. package/docs/components/tree.html +12 -0
  83. package/docs/components/voice-selector.html +12 -0
  84. package/docs/icons/done_all.svg +1 -0
  85. package/docs/icons/format_quote.svg +1 -0
  86. package/docs/icons/format_strikethrough.svg +1 -0
  87. package/docs/icons/send.svg +1 -0
  88. package/docs/icons/text_fields.svg +1 -0
  89. package/docs/index.html +30 -0
  90. package/docs/src/components/Chat.js +201 -0
  91. package/docs/src/components/CodeEditor.js +21 -2
  92. package/docs/src/components/Dropdown.js +1 -1
  93. package/docs/src/components/HtmlEditor.js +42 -3
  94. package/docs/src/components/MarkdownEditor.js +173 -0
  95. package/docs/src/components/Resize.js +12 -2
  96. package/docs/src/components/markdownEditorControls/Bold.js +11 -0
  97. package/docs/src/components/markdownEditorControls/BulletList.js +11 -0
  98. package/docs/src/components/markdownEditorControls/Code.js +11 -0
  99. package/docs/src/components/markdownEditorControls/FormatBlock.js +30 -0
  100. package/docs/src/components/markdownEditorControls/Heading.js +11 -0
  101. package/docs/src/components/markdownEditorControls/Image.js +93 -0
  102. package/docs/src/components/markdownEditorControls/Italic.js +11 -0
  103. package/docs/src/components/markdownEditorControls/Link.js +11 -0
  104. package/docs/src/components/markdownEditorControls/MarkdownEditorControl.js +39 -0
  105. package/docs/src/components/markdownEditorControls/Menu.js +30 -0
  106. package/docs/src/components/markdownEditorControls/NumberedList.js +11 -0
  107. package/docs/src/components/markdownEditorControls/Quote.js +11 -0
  108. package/docs/src/components/markdownEditorControls/SpeechToText.js +16 -0
  109. package/docs/src/components/markdownEditorControls/Strikethrough.js +11 -0
  110. package/docs/src/components/markdownEditorControls/Table.js +80 -0
  111. package/docs/src/utils/marked.esm.js +1 -0
  112. package/docs/src/utils/renderMarkdown.js +1 -0
  113. package/docs/src/utils/sanitizeHtml.js +1 -0
  114. package/docs/utils/context.html +12 -0
  115. package/docs/utils/cookie.html +12 -0
  116. package/docs/utils/debounce.html +12 -0
  117. package/docs/utils/drag.html +12 -0
  118. package/docs/utils/elevation.html +16 -10
  119. package/docs/utils/formatTimestamp.html +12 -0
  120. package/docs/utils/object.html +12 -0
  121. package/docs/utils/propConverters.html +12 -0
  122. package/docs/utils/sanitizeHtml.html +555 -0
  123. package/docs/utils/string.html +12 -0
  124. package/docs/utils/theme.html +12 -0
  125. package/docs/utils/toTitleCase.html +12 -0
  126. package/docs/utils/type.html +12 -0
  127. package/docs/utils/voice.html +12 -0
  128. package/docs/utils/wait.html +12 -0
  129. package/docs-src/components/chat.page.html +230 -0
  130. package/docs-src/components/code-editor.page.html +36 -0
  131. package/docs-src/components/html-editor.page.html +41 -0
  132. package/docs-src/components/markdown-editor.page.html +439 -0
  133. package/docs-src/components/resize.page.html +3 -0
  134. package/docs-src/index.page.html +18 -0
  135. package/docs-src/nav.fragment.html +12 -0
  136. package/docs-src/utils/elevation.page.html +4 -10
  137. package/docs-src/utils/sanitizeHtml.page.html +73 -0
  138. package/icons/done_all.svg +1 -0
  139. package/icons/format_quote.svg +1 -0
  140. package/icons/format_strikethrough.svg +1 -0
  141. package/icons/send.svg +1 -0
  142. package/icons/text_fields.svg +1 -0
  143. package/llms.txt +5 -2
  144. package/package.json +3 -2
  145. package/src/components/Chat.js +435 -0
  146. package/src/components/CodeEditor.js +58 -2
  147. package/src/components/Dropdown.js +1 -1
  148. package/src/components/HtmlEditor.js +84 -4
  149. package/src/components/MarkdownEditor.js +654 -0
  150. package/src/components/Resize.js +17 -1
  151. package/src/components/markdownEditorControls/Bold.js +30 -0
  152. package/src/components/markdownEditorControls/BulletList.js +30 -0
  153. package/src/components/markdownEditorControls/Code.js +39 -0
  154. package/src/components/markdownEditorControls/FormatBlock.js +102 -0
  155. package/src/components/markdownEditorControls/Heading.js +45 -0
  156. package/src/components/markdownEditorControls/Image.js +250 -0
  157. package/src/components/markdownEditorControls/Italic.js +30 -0
  158. package/src/components/markdownEditorControls/Link.js +74 -0
  159. package/src/components/markdownEditorControls/MarkdownEditorControl.js +158 -0
  160. package/src/components/markdownEditorControls/Menu.js +86 -0
  161. package/src/components/markdownEditorControls/NumberedList.js +69 -0
  162. package/src/components/markdownEditorControls/Quote.js +30 -0
  163. package/src/components/markdownEditorControls/SpeechToText.js +65 -0
  164. package/src/components/markdownEditorControls/Strikethrough.js +35 -0
  165. package/src/components/markdownEditorControls/Table.js +164 -0
  166. package/src/utils/marked.esm.js +77 -0
  167. package/src/utils/renderMarkdown.js +29 -0
  168. package/src/utils/sanitizeHtml.js +137 -0
  169. package/tests/components/Chat.browser-test.js +540 -0
  170. package/tests/utils/sanitizeHtml.browser-test.js +179 -0
@@ -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);
@@ -0,0 +1,65 @@
1
+ import MarkdownEditorControl from './MarkdownEditorControl.js';
2
+ import { html, css } from '../../lit-all.min.js';
3
+ import '../SpeechToText.js';
4
+
5
+ /*
6
+ Speech-to-text control. Reuses the standalone `<k-speech-to-text>`
7
+ component (which provides its own mic-button UI and handles the Web
8
+ Speech API) and just listens for its `end` event to drop the final
9
+ transcript into the editor at the cursor.
10
+ */
11
+ export default class MarkdownSpeechToText extends MarkdownEditorControl {
12
+ static properties = {
13
+ ...MarkdownEditorControl.properties,
14
+ language: { type: String, reflect: true },
15
+ continuous: { type: Boolean, reflect: true }
16
+ };
17
+
18
+ constructor() {
19
+ super();
20
+ this.label = 'Speech to text';
21
+ this.language = 'en-US';
22
+ this.continuous = false;
23
+ }
24
+
25
+ /*
26
+ The trigger button lives inside the embedded `<k-speech-to-text>`,
27
+ which handles its own click-to-listen logic. Our base-class
28
+ handleClick (which calls command()) is unused; the speech component
29
+ drives the lifecycle and we just react to the `end` event.
30
+ */
31
+ command() {}
32
+
33
+ handleEnd = (e) => {
34
+ const text = (e.detail?.text || '').trim();
35
+ if(!text) return;
36
+ // Trailing space so successive recordings don't run together.
37
+ this.editor?.replaceSelection(text + ' ', { selectInserted: false });
38
+ };
39
+
40
+ render() {
41
+ return html`
42
+ <k-speech-to-text
43
+ language=${this.language}
44
+ ?continuous=${this.continuous}
45
+ @end=${this.handleEnd}
46
+ ></k-speech-to-text>
47
+ `;
48
+ }
49
+
50
+ static styles = [
51
+ MarkdownEditorControl.styles,
52
+ css`
53
+ :host {
54
+ margin: var(--spacer_q);
55
+ }
56
+ k-speech-to-text {
57
+ /* Shrink the default 2.5rem circular mic button so it sits
58
+ comfortably alongside other toolbar controls. */
59
+ --btn_size: 2rem;
60
+ }
61
+ `
62
+ ];
63
+ }
64
+
65
+ customElements.define('k-md-speech-to-text', MarkdownSpeechToText);
@@ -0,0 +1,35 @@
1
+ import MarkdownEditorControl from './MarkdownEditorControl.js';
2
+ import { html } from '../../lit-all.min.js';
3
+ import '../Icon.js';
4
+
5
+ /*
6
+ GFM strikethrough — wraps the selection in `~~...~~`. marked renders this
7
+ to `<del>` (allowed by sanitizeHtml's default tag set), so it survives
8
+ the sanitize pass.
9
+ */
10
+ export default class MarkdownStrikethrough extends MarkdownEditorControl {
11
+ constructor() {
12
+ super();
13
+ this.label = 'Strikethrough';
14
+ }
15
+
16
+ command() {
17
+ this.wrapSelection('~~', '~~', 'strikethrough');
18
+ }
19
+
20
+ render() {
21
+ return html`
22
+ <button
23
+ type="button"
24
+ class=${this.btnClass}
25
+ title=${this.label}
26
+ aria-label=${this.label}
27
+ @click=${this.handleClick}
28
+ >
29
+ <k-icon name="format_strikethrough"></k-icon>
30
+ </button>
31
+ `;
32
+ }
33
+ }
34
+
35
+ customElements.define('k-md-strikethrough', MarkdownStrikethrough);
@@ -0,0 +1,164 @@
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
+ Table control. Opens a small dropdown with two number inputs (cols × rows)
8
+ and a Create button, then inserts a GFM markdown table with that many
9
+ empty cells at the cursor.
10
+ */
11
+ export default class MarkdownTable extends MarkdownEditorControl {
12
+ constructor() {
13
+ super();
14
+ this.label = 'Table';
15
+ }
16
+
17
+ /*
18
+ The trigger is wired to Dropdown's own click handling, so the
19
+ base-class handleClick (which calls command()) would fire on top and
20
+ create a feedback loop. command() is a no-op here; the Insert button
21
+ inside the dropdown drives insertion.
22
+ */
23
+ command() {}
24
+
25
+ handleDropdownOpened = () => {
26
+ requestAnimationFrame(() => {
27
+ const cols = this.shadowRoot?.querySelector('.tbl-cols');
28
+ if(cols){ cols.value = '3'; cols.select(); }
29
+ const rows = this.shadowRoot?.querySelector('.tbl-rows');
30
+ if(rows) rows.value = '2';
31
+ });
32
+ };
33
+
34
+ handleFormKeydown = (e) => {
35
+ if(e.key === 'Enter'){
36
+ e.preventDefault();
37
+ this.#submit();
38
+ } else if(e.key === 'Escape'){
39
+ this.shadowRoot?.querySelector('k-dropdown')?.close();
40
+ }
41
+ };
42
+
43
+ handleInsertClick = () => this.#submit();
44
+
45
+ #submit() {
46
+ const cols = this.#clamp(this.shadowRoot?.querySelector('.tbl-cols')?.value, 1, 20);
47
+ const rows = this.#clamp(this.shadowRoot?.querySelector('.tbl-rows')?.value, 1, 50);
48
+ if(!cols || !rows) return;
49
+ const editor = this.editor;
50
+ if(!editor) return;
51
+ editor.replaceSelection(this.#buildTable(cols, rows), { selectInserted: false });
52
+ this.shadowRoot?.querySelector('k-dropdown')?.close();
53
+ }
54
+
55
+ #clamp(raw, min, max) {
56
+ const n = parseInt(raw, 10);
57
+ if(!Number.isFinite(n)) return 0;
58
+ return Math.max(min, Math.min(max, n));
59
+ }
60
+
61
+ #buildTable(cols, rows) {
62
+ const headerCells = Array.from({ length: cols }, (_, i) => ` Header ${i + 1} `).join('|');
63
+ const separatorCells = Array.from({ length: cols }, () => ' --- ').join('|');
64
+ const bodyCells = Array.from({ length: cols }, () => ' ').join('|');
65
+ const lines = [
66
+ `|${headerCells}|`,
67
+ `|${separatorCells}|`,
68
+ ...Array.from({ length: rows }, () => `|${bodyCells}|`)
69
+ ];
70
+ // Surround with newlines so the table starts on its own line and the
71
+ // user's cursor lands on a fresh line below it.
72
+ return `\n${lines.join('\n')}\n`;
73
+ }
74
+
75
+ render() {
76
+ return html`
77
+ <k-dropdown @opened=${this.handleDropdownOpened}>
78
+ <button
79
+ slot="trigger"
80
+ type="button"
81
+ class=${this.btnClass}
82
+ title=${this.label}
83
+ aria-label=${this.label}
84
+ >
85
+ <k-icon name="table"></k-icon>
86
+ </button>
87
+ <div class="tbl-form" @keydown=${this.handleFormKeydown}>
88
+ <input
89
+ class="tbl-cols"
90
+ type="number"
91
+ min="1"
92
+ max="20"
93
+ inputmode="numeric"
94
+ aria-label="Columns"
95
+ value="3"
96
+ />
97
+ <span class="tbl-x" aria-hidden="true">×</span>
98
+ <input
99
+ class="tbl-rows"
100
+ type="number"
101
+ min="1"
102
+ max="50"
103
+ inputmode="numeric"
104
+ aria-label="Rows"
105
+ value="2"
106
+ />
107
+ <button type="button" class="primary" @click=${this.handleInsertClick}>Insert</button>
108
+ </div>
109
+ </k-dropdown>
110
+ `;
111
+ }
112
+
113
+ static styles = [
114
+ MarkdownEditorControl.styles,
115
+ css`
116
+ .tbl-form {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 0.4rem;
120
+ padding: 0.5rem;
121
+ }
122
+ .tbl-form input {
123
+ width: 2.5rem;
124
+ padding: 0.35rem 0.4rem;
125
+ border: 1px solid var(--c_border);
126
+ border-radius: var(--radius);
127
+ font: inherit;
128
+ background: var(--c_bg);
129
+ color: var(--tc);
130
+ text-align: center;
131
+ /* Hide the up/down spinner buttons that take up extra width. */
132
+ -moz-appearance: textfield;
133
+ }
134
+ .tbl-form input::-webkit-inner-spin-button,
135
+ .tbl-form input::-webkit-outer-spin-button {
136
+ -webkit-appearance: none;
137
+ margin: 0;
138
+ }
139
+ .tbl-form input:focus-visible {
140
+ outline: none;
141
+ box-shadow: var(--focus_shadow);
142
+ }
143
+ .tbl-x {
144
+ color: var(--tc_muted);
145
+ font-weight: bold;
146
+ }
147
+ .tbl-form button.primary {
148
+ padding: 0.4rem 0.8rem;
149
+ border: 1px solid var(--c_primary);
150
+ border-radius: var(--radius);
151
+ background: var(--c_primary);
152
+ color: white;
153
+ cursor: pointer;
154
+ font: inherit;
155
+ }
156
+ .tbl-form button.primary:focus-visible {
157
+ outline: none;
158
+ box-shadow: var(--focus_shadow);
159
+ }
160
+ `
161
+ ];
162
+ }
163
+
164
+ customElements.define('k-md-table', MarkdownTable);