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.
- package/dist/components/Chat.js +201 -0
- package/dist/components/CodeEditor.js +21 -2
- package/dist/components/Dropdown.js +1 -1
- package/dist/components/HtmlEditor.js +42 -3
- package/dist/components/MarkdownEditor.js +173 -0
- package/dist/components/Resize.js +12 -2
- package/dist/components/TextToSpeech.js +4 -4
- package/dist/components/VoiceSelector.js +49 -0
- package/dist/components/markdownEditorControls/Bold.js +11 -0
- package/dist/components/markdownEditorControls/BulletList.js +11 -0
- package/dist/components/markdownEditorControls/Code.js +11 -0
- package/dist/components/markdownEditorControls/FormatBlock.js +30 -0
- package/dist/components/markdownEditorControls/Heading.js +11 -0
- package/dist/components/markdownEditorControls/Image.js +93 -0
- package/dist/components/markdownEditorControls/Italic.js +11 -0
- package/dist/components/markdownEditorControls/Link.js +11 -0
- package/dist/components/markdownEditorControls/MarkdownEditorControl.js +39 -0
- package/dist/components/markdownEditorControls/Menu.js +30 -0
- package/dist/components/markdownEditorControls/NumberedList.js +11 -0
- package/dist/components/markdownEditorControls/Quote.js +11 -0
- package/dist/components/markdownEditorControls/SpeechToText.js +16 -0
- package/dist/components/markdownEditorControls/Strikethrough.js +11 -0
- package/dist/components/markdownEditorControls/Table.js +80 -0
- package/dist/utils/marked.esm.js +1 -0
- package/dist/utils/renderMarkdown.js +1 -0
- package/dist/utils/sanitizeHtml.js +1 -0
- package/dist/utils/voice.js +1 -0
- package/docs/components/accordion.html +19 -0
- package/docs/components/aside.html +19 -0
- package/docs/components/calendar.html +19 -0
- package/docs/components/card.html +19 -0
- package/docs/components/chat.html +715 -0
- package/docs/components/code-editor.html +55 -0
- package/docs/components/color-picker.html +19 -0
- package/docs/components/combobox.html +19 -0
- package/docs/components/content-slider.html +19 -0
- package/docs/components/context.html +19 -0
- package/docs/components/dialog.html +19 -0
- package/docs/components/dropdown.html +19 -0
- package/docs/components/filter-list.html +19 -0
- package/docs/components/focus-capture.html +19 -0
- package/docs/components/html-editor.html +60 -0
- package/docs/components/hybrid-component.html +19 -0
- package/docs/components/icon.html +19 -0
- package/docs/components/import.html +19 -0
- package/docs/components/light-component.html +19 -0
- package/docs/components/markdown-editor.html +924 -0
- package/docs/components/nav-spacer.html +19 -0
- package/docs/components/nav.html +19 -0
- package/docs/components/photo-viewer.html +19 -0
- package/docs/components/progress.html +19 -0
- package/docs/components/resize.html +22 -0
- package/docs/components/rich-textarea.html +650 -0
- package/docs/components/shadow-component.html +19 -0
- package/docs/components/show-more.html +19 -0
- package/docs/components/slider.html +19 -0
- package/docs/components/sortable.html +19 -0
- package/docs/components/speech-to-text.html +19 -0
- package/docs/components/spinner.html +19 -0
- package/docs/components/split.html +19 -0
- package/docs/components/table.html +19 -0
- package/docs/components/tableControls.html +19 -0
- package/docs/components/tableCustomFields.html +19 -0
- package/docs/components/tableFetchRecords.html +19 -0
- package/docs/components/tableFieldSortHide.html +19 -0
- package/docs/components/tablePagination.html +19 -0
- package/docs/components/tablePlaceholder.html +19 -0
- package/docs/components/tableRecordEditing.html +19 -0
- package/docs/components/tableRecordFiltering.html +19 -0
- package/docs/components/tableRecordHiding.html +19 -0
- package/docs/components/tableRecordSearching.html +19 -0
- package/docs/components/tableRecordSelection.html +19 -0
- package/docs/components/tableRowControls.html +19 -0
- package/docs/components/tableServerSync.html +19 -0
- package/docs/components/tableSorting.html +19 -0
- package/docs/components/tabs.html +19 -0
- package/docs/components/tags.html +19 -0
- package/docs/components/text-to-speech.html +63 -1
- package/docs/components/theme-select.html +19 -0
- package/docs/components/theme-switcher.html +19 -0
- package/docs/components/time.html +19 -0
- package/docs/components/timestamp.html +19 -0
- package/docs/components/toast.html +19 -0
- package/docs/components/toggle.html +19 -0
- package/docs/components/tree.html +19 -0
- package/docs/components/voice-selector.html +610 -0
- package/docs/icons/done_all.svg +1 -0
- package/docs/icons/format_quote.svg +1 -0
- package/docs/icons/format_strikethrough.svg +1 -0
- package/docs/icons/send.svg +1 -0
- package/docs/icons/text_fields.svg +1 -0
- package/docs/index.html +49 -0
- package/docs/src/components/Chat.js +201 -0
- package/docs/src/components/CodeEditor.js +21 -2
- package/docs/src/components/Dropdown.js +1 -1
- package/docs/src/components/HtmlEditor.js +42 -3
- package/docs/src/components/MarkdownEditor.js +173 -0
- package/docs/src/components/Resize.js +12 -2
- package/docs/src/components/TextToSpeech.js +4 -4
- package/docs/src/components/VoiceSelector.js +49 -0
- package/docs/src/components/markdownEditorControls/Bold.js +11 -0
- package/docs/src/components/markdownEditorControls/BulletList.js +11 -0
- package/docs/src/components/markdownEditorControls/Code.js +11 -0
- package/docs/src/components/markdownEditorControls/FormatBlock.js +30 -0
- package/docs/src/components/markdownEditorControls/Heading.js +11 -0
- package/docs/src/components/markdownEditorControls/Image.js +93 -0
- package/docs/src/components/markdownEditorControls/Italic.js +11 -0
- package/docs/src/components/markdownEditorControls/Link.js +11 -0
- package/docs/src/components/markdownEditorControls/MarkdownEditorControl.js +39 -0
- package/docs/src/components/markdownEditorControls/Menu.js +30 -0
- package/docs/src/components/markdownEditorControls/NumberedList.js +11 -0
- package/docs/src/components/markdownEditorControls/Quote.js +11 -0
- package/docs/src/components/markdownEditorControls/SpeechToText.js +16 -0
- package/docs/src/components/markdownEditorControls/Strikethrough.js +11 -0
- package/docs/src/components/markdownEditorControls/Table.js +80 -0
- package/docs/src/utils/marked.esm.js +1 -0
- package/docs/src/utils/renderMarkdown.js +1 -0
- package/docs/src/utils/sanitizeHtml.js +1 -0
- package/docs/src/utils/voice.js +1 -0
- package/docs/utils/context.html +19 -0
- package/docs/utils/cookie.html +19 -0
- package/docs/utils/debounce.html +19 -0
- package/docs/utils/drag.html +19 -0
- package/docs/utils/elevation.html +23 -10
- package/docs/utils/formatTimestamp.html +19 -0
- package/docs/utils/object.html +19 -0
- package/docs/utils/propConverters.html +19 -0
- package/docs/utils/sanitizeHtml.html +555 -0
- package/docs/utils/string.html +19 -0
- package/docs/utils/theme.html +19 -0
- package/docs/utils/toTitleCase.html +19 -0
- package/docs/utils/type.html +19 -0
- package/docs/utils/voice.html +548 -0
- package/docs/utils/wait.html +19 -0
- package/docs-src/components/chat.page.html +230 -0
- package/docs-src/components/code-editor.page.html +36 -0
- package/docs-src/components/html-editor.page.html +41 -0
- package/docs-src/components/markdown-editor.page.html +439 -0
- package/docs-src/components/resize.page.html +3 -0
- package/docs-src/components/text-to-speech.page.html +44 -1
- package/docs-src/components/voice-selector.page.html +125 -0
- package/docs-src/index.page.html +30 -0
- package/docs-src/nav.fragment.html +19 -0
- package/docs-src/utils/elevation.page.html +4 -10
- package/docs-src/utils/sanitizeHtml.page.html +73 -0
- package/docs-src/utils/voice.page.html +66 -0
- package/icons/done_all.svg +1 -0
- package/icons/format_quote.svg +1 -0
- package/icons/format_strikethrough.svg +1 -0
- package/icons/send.svg +1 -0
- package/icons/text_fields.svg +1 -0
- package/llms.txt +7 -2
- package/package.json +3 -2
- package/src/components/Chat.js +435 -0
- package/src/components/CodeEditor.js +58 -2
- package/src/components/Dropdown.js +1 -1
- package/src/components/HtmlEditor.js +84 -4
- package/src/components/MarkdownEditor.js +654 -0
- package/src/components/Resize.js +17 -1
- package/src/components/TextToSpeech.js +13 -2
- package/src/components/VoiceSelector.js +271 -0
- package/src/components/markdownEditorControls/Bold.js +30 -0
- package/src/components/markdownEditorControls/BulletList.js +30 -0
- package/src/components/markdownEditorControls/Code.js +39 -0
- package/src/components/markdownEditorControls/FormatBlock.js +102 -0
- package/src/components/markdownEditorControls/Heading.js +45 -0
- package/src/components/markdownEditorControls/Image.js +250 -0
- package/src/components/markdownEditorControls/Italic.js +30 -0
- package/src/components/markdownEditorControls/Link.js +74 -0
- package/src/components/markdownEditorControls/MarkdownEditorControl.js +158 -0
- package/src/components/markdownEditorControls/Menu.js +86 -0
- package/src/components/markdownEditorControls/NumberedList.js +69 -0
- package/src/components/markdownEditorControls/Quote.js +30 -0
- package/src/components/markdownEditorControls/SpeechToText.js +65 -0
- package/src/components/markdownEditorControls/Strikethrough.js +35 -0
- package/src/components/markdownEditorControls/Table.js +164 -0
- package/src/utils/marked.esm.js +77 -0
- package/src/utils/renderMarkdown.js +29 -0
- package/src/utils/sanitizeHtml.js +137 -0
- package/src/utils/voice.js +103 -0
- package/tests/components/Chat.browser-test.js +540 -0
- package/tests/components/TextToSpeech.browser-test.js +150 -1
- package/tests/components/VoiceSelector.browser-test.js +374 -0
- package/tests/utils/sanitizeHtml.browser-test.js +179 -0
- package/tests/utils/voice.browser-test.js +128 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { html, css, nothing } from '../lit-all.min.js';
|
|
2
|
+
import ShadowComponent from './ShadowComponent.js';
|
|
3
|
+
import sanitizeHtml from '../utils/sanitizeHtml.js';
|
|
4
|
+
import renderMarkdown from '../utils/renderMarkdown.js';
|
|
5
|
+
import './MarkdownEditor.js';
|
|
6
|
+
import './markdownEditorControls/Bold.js';
|
|
7
|
+
import './markdownEditorControls/Italic.js';
|
|
8
|
+
import './markdownEditorControls/Code.js';
|
|
9
|
+
import './markdownEditorControls/Link.js';
|
|
10
|
+
import './markdownEditorControls/BulletList.js';
|
|
11
|
+
import './markdownEditorControls/NumberedList.js';
|
|
12
|
+
import './markdownEditorControls/Quote.js';
|
|
13
|
+
import './Spinner.js';
|
|
14
|
+
import './Icon.js';
|
|
15
|
+
|
|
16
|
+
const newId = () => {
|
|
17
|
+
if(typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'){
|
|
18
|
+
return crypto.randomUUID();
|
|
19
|
+
}
|
|
20
|
+
return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const STATUSES = new Set(['sending', 'delivered', 'read', 'failed']);
|
|
24
|
+
|
|
25
|
+
export default class Chat extends ShadowComponent {
|
|
26
|
+
static properties = {
|
|
27
|
+
enterNewline: { type: Boolean, reflect: true, attribute: 'enter-newline' },
|
|
28
|
+
// show-status is a string attr: absent = off, "" or "text" = text labels,
|
|
29
|
+
// "icons" = icon-only badges. <k-chat show-status> defaults to text mode.
|
|
30
|
+
showStatus: { type: String, reflect: true, attribute: 'show-status' },
|
|
31
|
+
placeholder: { type: String, reflect: true },
|
|
32
|
+
disabled: { type: Boolean, reflect: true },
|
|
33
|
+
messages: { state: true }
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
Lifecycle Callbacks
|
|
38
|
+
*/
|
|
39
|
+
constructor() {
|
|
40
|
+
super();
|
|
41
|
+
// Default matches Slack/Discord/Messages: Enter sends, Shift+Enter newline.
|
|
42
|
+
// Add the `enter-newline` attribute to invert (Enter inserts a newline,
|
|
43
|
+
// Shift+Enter or the Send button submits).
|
|
44
|
+
this.enterNewline = false;
|
|
45
|
+
this.showStatus = null;
|
|
46
|
+
this.placeholder = 'Type a message...';
|
|
47
|
+
this.disabled = false;
|
|
48
|
+
this.messages = [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
updated(changedProperties) {
|
|
52
|
+
super.updated(changedProperties);
|
|
53
|
+
if(changedProperties.has('messages')){
|
|
54
|
+
this.#scrollToBottom();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
Public Methods
|
|
60
|
+
*/
|
|
61
|
+
addMessage(msg = {}) {
|
|
62
|
+
const type = msg.type === 'outgoing' ? 'outgoing' : 'incoming';
|
|
63
|
+
const id = msg.id || newId();
|
|
64
|
+
const html = sanitizeHtml(msg.html || '');
|
|
65
|
+
const status = STATUSES.has(msg.status)
|
|
66
|
+
? msg.status
|
|
67
|
+
: (type === 'outgoing' ? 'delivered' : 'read');
|
|
68
|
+
const message = {
|
|
69
|
+
id,
|
|
70
|
+
type,
|
|
71
|
+
html,
|
|
72
|
+
status,
|
|
73
|
+
sender: msg.sender || '',
|
|
74
|
+
timestamp: msg.timestamp instanceof Date ? msg.timestamp : new Date()
|
|
75
|
+
};
|
|
76
|
+
this.messages = [...this.messages, message];
|
|
77
|
+
return id;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
updateMessage(id, updates = {}) {
|
|
81
|
+
let changed = false;
|
|
82
|
+
this.messages = this.messages.map(m => {
|
|
83
|
+
if(m.id !== id) return m;
|
|
84
|
+
changed = true;
|
|
85
|
+
const next = { ...m };
|
|
86
|
+
if(updates.html !== undefined) next.html = sanitizeHtml(updates.html);
|
|
87
|
+
if(updates.status !== undefined && STATUSES.has(updates.status)) next.status = updates.status;
|
|
88
|
+
if(updates.sender !== undefined) next.sender = updates.sender;
|
|
89
|
+
if(updates.timestamp instanceof Date) next.timestamp = updates.timestamp;
|
|
90
|
+
return next;
|
|
91
|
+
});
|
|
92
|
+
return changed;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
removeMessage(id) {
|
|
96
|
+
const before = this.messages.length;
|
|
97
|
+
this.messages = this.messages.filter(m => m.id !== id);
|
|
98
|
+
return this.messages.length !== before;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
clear() {
|
|
102
|
+
this.messages = [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
send() {
|
|
106
|
+
if(this.disabled) return null;
|
|
107
|
+
const editor = this.shadowRoot?.querySelector('k-markdown-editor');
|
|
108
|
+
if(!editor) return null;
|
|
109
|
+
const markdown = (editor.value || '').trim();
|
|
110
|
+
if(!markdown) return null;
|
|
111
|
+
// breaks:true makes single newlines render as <br>, matching Slack /
|
|
112
|
+
// iMessage convention where each typed Enter is a visible break.
|
|
113
|
+
const safe = sanitizeHtml(renderMarkdown(markdown, { breaks: true }));
|
|
114
|
+
const id = this.addMessage({
|
|
115
|
+
type: 'outgoing',
|
|
116
|
+
html: safe,
|
|
117
|
+
status: this.#statusEnabled ? 'sending' : 'delivered'
|
|
118
|
+
});
|
|
119
|
+
editor.clear();
|
|
120
|
+
this.dispatchEvent(new CustomEvent('send', {
|
|
121
|
+
detail: { id, html: safe, markdown },
|
|
122
|
+
bubbles: true
|
|
123
|
+
}));
|
|
124
|
+
return id;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/*
|
|
128
|
+
Event Handlers
|
|
129
|
+
*/
|
|
130
|
+
handleSendClick = () => this.send();
|
|
131
|
+
|
|
132
|
+
handleEditorKeydown = (e) => {
|
|
133
|
+
if(this.disabled) return;
|
|
134
|
+
if(e.key !== 'Enter' || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
135
|
+
// Default (no enter-newline attr): Enter sends, Shift+Enter inserts a newline.
|
|
136
|
+
// With enter-newline attr: Enter inserts a newline, Shift+Enter sends.
|
|
137
|
+
const shouldSend = this.enterNewline ? e.shiftKey : !e.shiftKey;
|
|
138
|
+
if(!shouldSend) return;
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
this.send();
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
Utility
|
|
145
|
+
*/
|
|
146
|
+
#scrollToBottom = () => {
|
|
147
|
+
this.updateComplete.then(() => {
|
|
148
|
+
const window = this.shadowRoot?.querySelector('.window');
|
|
149
|
+
if(window) window.scrollTop = window.scrollHeight;
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/*
|
|
154
|
+
Rendering
|
|
155
|
+
*/
|
|
156
|
+
get #lastOutgoingId() {
|
|
157
|
+
for(let i = this.messages.length - 1; i >= 0; i--){
|
|
158
|
+
if(this.messages[i].type === 'outgoing') return this.messages[i].id;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get #statusEnabled() {
|
|
164
|
+
return this.showStatus != null && this.showStatus !== 'false';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get #statusMode() {
|
|
168
|
+
return this.showStatus === 'icons' ? 'icons' : 'text';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// iMessage convention: per-message status only appears on the most recent
|
|
172
|
+
// outgoing message — older delivered/read messages don't carry an
|
|
173
|
+
// indicator. Sending or failed always show, regardless of position, so
|
|
174
|
+
// the user knows about in-flight or failed messages immediately.
|
|
175
|
+
#shouldShowStatus(message, lastOutgoingId) {
|
|
176
|
+
if(!this.#statusEnabled) return false;
|
|
177
|
+
if(message.type !== 'outgoing') return false;
|
|
178
|
+
if(message.status === 'sending' || message.status === 'failed') return true;
|
|
179
|
+
return message.id === lastOutgoingId;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#renderStatus(status) {
|
|
183
|
+
if(this.#statusMode === 'icons') return this.#renderStatusIcon(status);
|
|
184
|
+
return this.#renderStatusText(status);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#renderStatusText(status) {
|
|
188
|
+
if(status === 'sending'){
|
|
189
|
+
return html`<span class="status sending"><k-spinner size="xs"></k-spinner> Sending…</span>`;
|
|
190
|
+
}
|
|
191
|
+
if(status === 'delivered'){
|
|
192
|
+
return html`<span class="status">Delivered</span>`;
|
|
193
|
+
}
|
|
194
|
+
if(status === 'read'){
|
|
195
|
+
return html`<span class="status">Read</span>`;
|
|
196
|
+
}
|
|
197
|
+
if(status === 'failed'){
|
|
198
|
+
return html`<span class="status failed"><k-icon name="error"></k-icon> Not Delivered</span>`;
|
|
199
|
+
}
|
|
200
|
+
return nothing;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#renderStatusIcon(status) {
|
|
204
|
+
if(status === 'sending'){
|
|
205
|
+
return html`<span class="status status-icon-only"><k-spinner size="xs" title="Sending"></k-spinner></span>`;
|
|
206
|
+
}
|
|
207
|
+
if(status === 'delivered'){
|
|
208
|
+
return html`<span class="status status-icon-only"><k-icon name="check" title="Delivered"></k-icon></span>`;
|
|
209
|
+
}
|
|
210
|
+
if(status === 'read'){
|
|
211
|
+
return html`<span class="status status-icon-only"><k-icon name="done_all" title="Read"></k-icon></span>`;
|
|
212
|
+
}
|
|
213
|
+
if(status === 'failed'){
|
|
214
|
+
return html`<span class="status status-icon-only failed"><k-icon name="error" title="Not Delivered"></k-icon></span>`;
|
|
215
|
+
}
|
|
216
|
+
return nothing;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#renderMessage(message, lastOutgoingId) {
|
|
220
|
+
const showStatus = this.#shouldShowStatus(message, lastOutgoingId);
|
|
221
|
+
return html`
|
|
222
|
+
<div
|
|
223
|
+
class="message ${message.type} ${message.status}"
|
|
224
|
+
data-id=${message.id}
|
|
225
|
+
>
|
|
226
|
+
${message.sender ? html`<div class="sender">${message.sender}</div>` : nothing}
|
|
227
|
+
<div class="bubble">
|
|
228
|
+
<div class="content" .innerHTML=${message.html}></div>
|
|
229
|
+
</div>
|
|
230
|
+
${showStatus ? this.#renderStatus(message.status) : nothing}
|
|
231
|
+
</div>
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
render() {
|
|
236
|
+
const lastOutgoingId = this.#lastOutgoingId;
|
|
237
|
+
return html`
|
|
238
|
+
<div class="window" role="log" aria-live="polite">
|
|
239
|
+
${this.messages.map(m => this.#renderMessage(m, lastOutgoingId))}
|
|
240
|
+
</div>
|
|
241
|
+
<div class="input-area">
|
|
242
|
+
<k-markdown-editor
|
|
243
|
+
class="editor"
|
|
244
|
+
placeholder=${this.placeholder}
|
|
245
|
+
?disabled=${this.disabled}
|
|
246
|
+
@keydown=${this.handleEditorKeydown}
|
|
247
|
+
>
|
|
248
|
+
<div slot="controls-top" class="controls-top">
|
|
249
|
+
<slot name="controls-top">
|
|
250
|
+
<k-md-bold></k-md-bold>
|
|
251
|
+
<k-md-italic></k-md-italic>
|
|
252
|
+
<k-md-code></k-md-code>
|
|
253
|
+
<k-md-link></k-md-link>
|
|
254
|
+
<k-md-bullet-list></k-md-bullet-list>
|
|
255
|
+
<k-md-numbered-list></k-md-numbered-list>
|
|
256
|
+
<k-md-quote></k-md-quote>
|
|
257
|
+
</slot>
|
|
258
|
+
</div>
|
|
259
|
+
<div slot="controls-bottom" class="controls-bottom">
|
|
260
|
+
<slot name="controls-bottom"></slot>
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
class="send-btn primary"
|
|
264
|
+
?disabled=${this.disabled}
|
|
265
|
+
@click=${this.handleSendClick}
|
|
266
|
+
aria-label="Send message"
|
|
267
|
+
title="Send message"
|
|
268
|
+
>
|
|
269
|
+
<k-icon name="send"></k-icon>
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
</k-markdown-editor>
|
|
273
|
+
</div>
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/*
|
|
278
|
+
Styles
|
|
279
|
+
*/
|
|
280
|
+
static styles = css`
|
|
281
|
+
:host {
|
|
282
|
+
--window_min_height: 16rem;
|
|
283
|
+
--window_max_height: 32rem;
|
|
284
|
+
--chat_padding: 0.5rem;
|
|
285
|
+
--chat_gap: 0.5rem;
|
|
286
|
+
--message_radius: 1rem;
|
|
287
|
+
--message_max_width: 75%;
|
|
288
|
+
--bubble_padding: 0.5rem 0.75rem;
|
|
289
|
+
--bubble_bg__incoming: var(--c_bg__alt);
|
|
290
|
+
--bubble_bg__outgoing: var(--c_primary);
|
|
291
|
+
--bubble_tc__outgoing: white;
|
|
292
|
+
--send_btn_bg: var(--c_primary);
|
|
293
|
+
--send_btn_tc: white;
|
|
294
|
+
|
|
295
|
+
display: flex;
|
|
296
|
+
flex-direction: column;
|
|
297
|
+
border: 1px solid var(--c_border);
|
|
298
|
+
border-radius: var(--radius);
|
|
299
|
+
background: var(--c_bg);
|
|
300
|
+
/* Note: NO overflow:hidden on the host — that would clip the input
|
|
301
|
+
area's focus shadow. Each child clips its own content against the
|
|
302
|
+
shared rounded corners instead. */
|
|
303
|
+
}
|
|
304
|
+
:host([disabled]) {
|
|
305
|
+
opacity: 0.6;
|
|
306
|
+
}
|
|
307
|
+
.window {
|
|
308
|
+
flex: 1 1 auto;
|
|
309
|
+
min-height: var(--window_min_height);
|
|
310
|
+
max-height: var(--window_max_height);
|
|
311
|
+
overflow-y: auto;
|
|
312
|
+
padding: var(--chat_padding);
|
|
313
|
+
display: flex;
|
|
314
|
+
flex-direction: column;
|
|
315
|
+
gap: var(--chat_gap);
|
|
316
|
+
scroll-behavior: smooth;
|
|
317
|
+
/* Match the chat's outer rounded corners on the top edge so messages
|
|
318
|
+
don't render past them. */
|
|
319
|
+
border-top-left-radius: var(--radius);
|
|
320
|
+
border-top-right-radius: var(--radius);
|
|
321
|
+
}
|
|
322
|
+
.message {
|
|
323
|
+
display: flex;
|
|
324
|
+
flex-direction: column;
|
|
325
|
+
max-width: var(--message_max_width);
|
|
326
|
+
gap: 0.125rem;
|
|
327
|
+
}
|
|
328
|
+
.message.outgoing {
|
|
329
|
+
align-self: flex-end;
|
|
330
|
+
align-items: flex-end;
|
|
331
|
+
}
|
|
332
|
+
.message.incoming {
|
|
333
|
+
align-self: flex-start;
|
|
334
|
+
align-items: flex-start;
|
|
335
|
+
}
|
|
336
|
+
.sender {
|
|
337
|
+
font-size: 0.75rem;
|
|
338
|
+
color: var(--tc_muted);
|
|
339
|
+
}
|
|
340
|
+
.bubble {
|
|
341
|
+
padding: var(--bubble_padding);
|
|
342
|
+
border-radius: var(--message_radius);
|
|
343
|
+
background: var(--bubble_bg__incoming);
|
|
344
|
+
color: var(--tc);
|
|
345
|
+
word-wrap: break-word;
|
|
346
|
+
overflow-wrap: anywhere;
|
|
347
|
+
}
|
|
348
|
+
.message.outgoing .bubble {
|
|
349
|
+
background: var(--bubble_bg__outgoing);
|
|
350
|
+
color: var(--bubble_tc__outgoing);
|
|
351
|
+
}
|
|
352
|
+
.message.outgoing.sending .bubble {
|
|
353
|
+
opacity: 0.7;
|
|
354
|
+
}
|
|
355
|
+
.message.outgoing.failed .bubble {
|
|
356
|
+
background: var(--c_danger, #d32f2f);
|
|
357
|
+
}
|
|
358
|
+
.content {
|
|
359
|
+
line-height: 1.4;
|
|
360
|
+
}
|
|
361
|
+
.content p:first-child { margin-top: 0; }
|
|
362
|
+
.content p:last-child { margin-bottom: 0; }
|
|
363
|
+
/* iMessage-style small grey label below the message bubble. */
|
|
364
|
+
.status {
|
|
365
|
+
display: inline-flex;
|
|
366
|
+
align-items: center;
|
|
367
|
+
gap: 0.25rem;
|
|
368
|
+
font-size: 0.7rem;
|
|
369
|
+
color: var(--tc_muted);
|
|
370
|
+
margin-top: 0.125rem;
|
|
371
|
+
}
|
|
372
|
+
.status.failed {
|
|
373
|
+
color: var(--c_danger, #d32f2f);
|
|
374
|
+
}
|
|
375
|
+
.status k-spinner,
|
|
376
|
+
.status k-icon {
|
|
377
|
+
font-size: 0.8rem;
|
|
378
|
+
}
|
|
379
|
+
.status-icon-only {
|
|
380
|
+
font-size: 0.95rem;
|
|
381
|
+
}
|
|
382
|
+
.input-area {
|
|
383
|
+
position: relative;
|
|
384
|
+
border-top: 1px solid var(--c_border);
|
|
385
|
+
border-bottom-left-radius: var(--radius);
|
|
386
|
+
border-bottom-right-radius: var(--radius);
|
|
387
|
+
}
|
|
388
|
+
.editor {
|
|
389
|
+
/* Markdown editor fills the bottom of the chat. CSS custom properties
|
|
390
|
+
pierce shadow boundaries -- '--height' sets the initial size of the
|
|
391
|
+
editor's internal Resize wrapper. */
|
|
392
|
+
--height: 13rem;
|
|
393
|
+
display: block;
|
|
394
|
+
width: 100%;
|
|
395
|
+
}
|
|
396
|
+
.controls-top,
|
|
397
|
+
.controls-bottom {
|
|
398
|
+
display: flex;
|
|
399
|
+
align-items: center;
|
|
400
|
+
flex-wrap: wrap;
|
|
401
|
+
gap: 0.125rem;
|
|
402
|
+
}
|
|
403
|
+
.controls-bottom {
|
|
404
|
+
width: 100%;
|
|
405
|
+
}
|
|
406
|
+
.send-btn {
|
|
407
|
+
margin-left: auto;
|
|
408
|
+
display: inline-flex;
|
|
409
|
+
align-items: center;
|
|
410
|
+
justify-content: center;
|
|
411
|
+
width: 2rem;
|
|
412
|
+
height: 2rem;
|
|
413
|
+
padding: 0;
|
|
414
|
+
border: 1px solid transparent;
|
|
415
|
+
border-radius: 50%;
|
|
416
|
+
background: var(--c_primary);
|
|
417
|
+
color: white;
|
|
418
|
+
cursor: pointer;
|
|
419
|
+
transition: opacity var(--animation_ms);
|
|
420
|
+
}
|
|
421
|
+
.send-btn:hover:not(:disabled) {
|
|
422
|
+
opacity: 0.9;
|
|
423
|
+
}
|
|
424
|
+
.send-btn:focus-visible {
|
|
425
|
+
outline: none;
|
|
426
|
+
box-shadow: var(--focus_shadow);
|
|
427
|
+
}
|
|
428
|
+
.send-btn:disabled {
|
|
429
|
+
cursor: not-allowed;
|
|
430
|
+
opacity: 0.5;
|
|
431
|
+
}
|
|
432
|
+
`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
customElements.define('k-chat', Chat);
|
|
@@ -23,7 +23,10 @@ export default class CodeEditor extends ShadowComponent {
|
|
|
23
23
|
hasBottomToolbar: { type: Boolean, state: true },
|
|
24
24
|
fullscreen: { type: Boolean, reflect: true },
|
|
25
25
|
wordWrap: { type: Boolean },
|
|
26
|
-
minimapEnabled: { type: Boolean }
|
|
26
|
+
minimapEnabled: { type: Boolean },
|
|
27
|
+
disabled: { type: Boolean, reflect: true },
|
|
28
|
+
readonly: { type: Boolean, reflect: true },
|
|
29
|
+
required: { type: Boolean, reflect: true }
|
|
27
30
|
};
|
|
28
31
|
|
|
29
32
|
constructor() {
|
|
@@ -44,6 +47,9 @@ export default class CodeEditor extends ShadowComponent {
|
|
|
44
47
|
this.minimapEnabled = false;
|
|
45
48
|
this.fontSize = 14;
|
|
46
49
|
this.fullscreen = false;
|
|
50
|
+
this.disabled = false;
|
|
51
|
+
this.readonly = false;
|
|
52
|
+
this.required = false;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
/*
|
|
@@ -115,6 +121,32 @@ export default class CodeEditor extends ShadowComponent {
|
|
|
115
121
|
if(!this.fullscreen) this.monacoEditor?.layout({ width: 0, height: 0 });
|
|
116
122
|
requestAnimationFrame(() => this.monacoEditor?.layout());
|
|
117
123
|
}
|
|
124
|
+
if(changedProperties.has('disabled') || changedProperties.has('readonly')){
|
|
125
|
+
this.monacoEditor?.updateOptions({ readOnly: this.disabled || this.readonly });
|
|
126
|
+
}
|
|
127
|
+
if(
|
|
128
|
+
changedProperties.has('value') ||
|
|
129
|
+
changedProperties.has('required') ||
|
|
130
|
+
changedProperties.has('disabled')
|
|
131
|
+
){
|
|
132
|
+
this.#updateValidity();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#updateValidity = () => {
|
|
137
|
+
if(this.disabled){
|
|
138
|
+
this.internals.setValidity({});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if(this.required && !(this.getValue() || '').trim()){
|
|
142
|
+
this.internals.setValidity(
|
|
143
|
+
{ valueMissing: true },
|
|
144
|
+
'Please fill out this field.',
|
|
145
|
+
this.monacoContainer || this
|
|
146
|
+
);
|
|
147
|
+
} else {
|
|
148
|
+
this.internals.setValidity({});
|
|
149
|
+
}
|
|
118
150
|
}
|
|
119
151
|
|
|
120
152
|
async firstUpdated() {
|
|
@@ -193,7 +225,8 @@ export default class CodeEditor extends ShadowComponent {
|
|
|
193
225
|
scrollBeyondLastLine: false,
|
|
194
226
|
automaticLayout: true,
|
|
195
227
|
tabSize: 2,
|
|
196
|
-
padding: { top: 8 }
|
|
228
|
+
padding: { top: 8 },
|
|
229
|
+
readOnly: this.disabled || this.readonly
|
|
197
230
|
});
|
|
198
231
|
|
|
199
232
|
const monacoCSS = document.querySelector('link[href*="monaco"][href*="editor.main.css"]');
|
|
@@ -235,6 +268,10 @@ export default class CodeEditor extends ShadowComponent {
|
|
|
235
268
|
this.value = state;
|
|
236
269
|
}
|
|
237
270
|
|
|
271
|
+
formDisabledCallback(disabled) {
|
|
272
|
+
this.disabled = disabled;
|
|
273
|
+
}
|
|
274
|
+
|
|
238
275
|
/*
|
|
239
276
|
Public Methods
|
|
240
277
|
*/
|
|
@@ -460,6 +497,25 @@ export default class CodeEditor extends ShadowComponent {
|
|
|
460
497
|
[hidden] {
|
|
461
498
|
display: none !important;
|
|
462
499
|
}
|
|
500
|
+
:host([disabled]) {
|
|
501
|
+
opacity: 0.6;
|
|
502
|
+
}
|
|
503
|
+
/* disabled blocks all interaction -- toolbar AND editor. Monaco's
|
|
504
|
+
readOnly already prevents typing; pointer-events: none also stops
|
|
505
|
+
focus / cursor placement, matching native form control semantics. */
|
|
506
|
+
:host([disabled]) .toolbar-top,
|
|
507
|
+
:host([disabled]) .toolbar-bottom,
|
|
508
|
+
:host([disabled]) .editor-container {
|
|
509
|
+
pointer-events: none;
|
|
510
|
+
}
|
|
511
|
+
/* readonly keeps the editor interactive (so users can place a cursor
|
|
512
|
+
to select / copy) but mutes the toolbar so its buttons can't
|
|
513
|
+
mutate the document. */
|
|
514
|
+
:host([readonly]) .toolbar-top,
|
|
515
|
+
:host([readonly]) .toolbar-bottom {
|
|
516
|
+
pointer-events: none;
|
|
517
|
+
opacity: 0.5;
|
|
518
|
+
}
|
|
463
519
|
`;
|
|
464
520
|
|
|
465
521
|
render() {
|
|
@@ -257,7 +257,7 @@ export default class Dropdown extends ShadowComponent {
|
|
|
257
257
|
display: none;
|
|
258
258
|
position: fixed;
|
|
259
259
|
position-anchor: --dropdown-trigger;
|
|
260
|
-
z-index:
|
|
260
|
+
z-index: 30;
|
|
261
261
|
min-width: anchor-size(width);
|
|
262
262
|
background: var(--c_bg);
|
|
263
263
|
border: 1px solid var(--c_border);
|
|
@@ -29,7 +29,10 @@ export default class HtmlEditor extends ShadowComponent {
|
|
|
29
29
|
nodes: { type: String },
|
|
30
30
|
hasTopToolbar: { type: Boolean, state: true },
|
|
31
31
|
hasBottomToolbar: { type: Boolean, state: true },
|
|
32
|
-
fullscreen: { type: Boolean, reflect: true }
|
|
32
|
+
fullscreen: { type: Boolean, reflect: true },
|
|
33
|
+
disabled: { type: Boolean, reflect: true },
|
|
34
|
+
readonly: { type: Boolean, reflect: true },
|
|
35
|
+
required: { type: Boolean, reflect: true }
|
|
33
36
|
};
|
|
34
37
|
|
|
35
38
|
constructor() {
|
|
@@ -57,6 +60,9 @@ export default class HtmlEditor extends ShadowComponent {
|
|
|
57
60
|
this.minimapEnabled = false;
|
|
58
61
|
this.fontSize = 14;
|
|
59
62
|
this.fullscreen = false;
|
|
63
|
+
this.disabled = false;
|
|
64
|
+
this.readonly = false;
|
|
65
|
+
this.required = false;
|
|
60
66
|
this.lx = {};
|
|
61
67
|
this.debouncedSyncValue = debounce(() => this.syncValueFromLexical(), 300);
|
|
62
68
|
}
|
|
@@ -130,6 +136,35 @@ export default class HtmlEditor extends ShadowComponent {
|
|
|
130
136
|
if(changedProperties.has('fullscreen')){
|
|
131
137
|
requestAnimationFrame(() => this.monacoEditor?.layout());
|
|
132
138
|
}
|
|
139
|
+
if(changedProperties.has('disabled') || changedProperties.has('readonly')){
|
|
140
|
+
const ro = this.disabled || this.readonly;
|
|
141
|
+
this.lexicalEditor?.setEditable(!ro);
|
|
142
|
+
this.monacoEditor?.updateOptions({ readOnly: ro });
|
|
143
|
+
}
|
|
144
|
+
if(
|
|
145
|
+
changedProperties.has('value') ||
|
|
146
|
+
changedProperties.has('required') ||
|
|
147
|
+
changedProperties.has('disabled')
|
|
148
|
+
){
|
|
149
|
+
this.#updateValidity();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#updateValidity = () => {
|
|
154
|
+
if(this.disabled){
|
|
155
|
+
this.internals.setValidity({});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const empty = !(this.value || '').replace(/<[^>]+>/g, '').trim();
|
|
159
|
+
if(this.required && empty){
|
|
160
|
+
this.internals.setValidity(
|
|
161
|
+
{ valueMissing: true },
|
|
162
|
+
'Please fill out this field.',
|
|
163
|
+
this.lexicalContainer || this
|
|
164
|
+
);
|
|
165
|
+
} else {
|
|
166
|
+
this.internals.setValidity({});
|
|
167
|
+
}
|
|
133
168
|
}
|
|
134
169
|
|
|
135
170
|
async firstUpdated() {
|
|
@@ -268,6 +303,7 @@ export default class HtmlEditor extends ShadowComponent {
|
|
|
268
303
|
|
|
269
304
|
this.lexicalEditor = lexical.createEditor(editorConfig);
|
|
270
305
|
this.lexicalEditor.setRootElement(this.lexicalContainer);
|
|
306
|
+
if(this.disabled || this.readonly) this.lexicalEditor.setEditable(false);
|
|
271
307
|
this.lexicalEditor._window = new Proxy(window, {
|
|
272
308
|
get: (target, prop) => {
|
|
273
309
|
if(prop === 'getSelection') return () => this.shadowRoot.getSelection();
|
|
@@ -356,7 +392,8 @@ export default class HtmlEditor extends ShadowComponent {
|
|
|
356
392
|
scrollBeyondLastLine: false,
|
|
357
393
|
automaticLayout: true,
|
|
358
394
|
tabSize: 2,
|
|
359
|
-
padding: { top: 8 }
|
|
395
|
+
padding: { top: 8 },
|
|
396
|
+
readOnly: this.disabled || this.readonly
|
|
360
397
|
});
|
|
361
398
|
|
|
362
399
|
const monacoCSS = document.querySelector('link[href*="monaco"][href*="editor.main.css"]');
|
|
@@ -491,6 +528,10 @@ export default class HtmlEditor extends ShadowComponent {
|
|
|
491
528
|
this.value = state;
|
|
492
529
|
}
|
|
493
530
|
|
|
531
|
+
formDisabledCallback(disabled) {
|
|
532
|
+
this.disabled = disabled;
|
|
533
|
+
}
|
|
534
|
+
|
|
494
535
|
/*
|
|
495
536
|
Selection Management
|
|
496
537
|
*/
|
|
@@ -1147,7 +1188,7 @@ export default class HtmlEditor extends ShadowComponent {
|
|
|
1147
1188
|
<div class="editor-container">
|
|
1148
1189
|
<div
|
|
1149
1190
|
class="lexical-editor"
|
|
1150
|
-
contenteditable
|
|
1191
|
+
contenteditable=${(this.disabled || this.readonly) ? 'false' : 'true'}
|
|
1151
1192
|
?hidden=${this.mode !== 'visual'}
|
|
1152
1193
|
></div>
|
|
1153
1194
|
<div
|
|
@@ -1185,6 +1226,26 @@ export default class HtmlEditor extends ShadowComponent {
|
|
|
1185
1226
|
height: 100vh !important;
|
|
1186
1227
|
z-index: 10000;
|
|
1187
1228
|
}
|
|
1229
|
+
:host([disabled]) {
|
|
1230
|
+
opacity: 0.6;
|
|
1231
|
+
}
|
|
1232
|
+
/* disabled blocks all interaction -- toolbar AND editor. Lexical
|
|
1233
|
+
and Monaco both prevent typing on their own; pointer-events: none
|
|
1234
|
+
also stops focus / cursor placement, matching native form control
|
|
1235
|
+
semantics. */
|
|
1236
|
+
:host([disabled]) .toolbar-top,
|
|
1237
|
+
:host([disabled]) .toolbar-bottom,
|
|
1238
|
+
:host([disabled]) .editor-container {
|
|
1239
|
+
pointer-events: none;
|
|
1240
|
+
}
|
|
1241
|
+
/* readonly keeps the editor interactive (so users can place a cursor
|
|
1242
|
+
to select / copy) but mutes the toolbar so its buttons can't
|
|
1243
|
+
mutate the document. */
|
|
1244
|
+
:host([readonly]) .toolbar-top,
|
|
1245
|
+
:host([readonly]) .toolbar-bottom {
|
|
1246
|
+
pointer-events: none;
|
|
1247
|
+
opacity: 0.5;
|
|
1248
|
+
}
|
|
1188
1249
|
.toolbar-top,
|
|
1189
1250
|
.toolbar-bottom {
|
|
1190
1251
|
display: flex;
|
|
@@ -1219,7 +1280,7 @@ export default class HtmlEditor extends ShadowComponent {
|
|
|
1219
1280
|
overflow: auto;
|
|
1220
1281
|
}
|
|
1221
1282
|
.lexical-editor {
|
|
1222
|
-
padding: 1rem;
|
|
1283
|
+
padding: var(--editor_padding, 1rem);
|
|
1223
1284
|
border: 1px solid var(--border-color);
|
|
1224
1285
|
background: var(--bg-primary);
|
|
1225
1286
|
color: var(--text-primary);
|
|
@@ -1227,6 +1288,25 @@ export default class HtmlEditor extends ShadowComponent {
|
|
|
1227
1288
|
font-size: inherit;
|
|
1228
1289
|
line-height: 1.5;
|
|
1229
1290
|
outline: none;
|
|
1291
|
+
/* Always show a slim scrollbar when content overflows (macOS would
|
|
1292
|
+
otherwise hide overlay scrollbars when not actively scrolling,
|
|
1293
|
+
leaving users unaware they can scroll back). */
|
|
1294
|
+
scrollbar-width: thin;
|
|
1295
|
+
scrollbar-color: var(--c_border, rgba(128,128,128,0.4)) transparent;
|
|
1296
|
+
}
|
|
1297
|
+
.lexical-editor::-webkit-scrollbar {
|
|
1298
|
+
width: 8px;
|
|
1299
|
+
height: 8px;
|
|
1300
|
+
}
|
|
1301
|
+
.lexical-editor::-webkit-scrollbar-track {
|
|
1302
|
+
background: transparent;
|
|
1303
|
+
}
|
|
1304
|
+
.lexical-editor::-webkit-scrollbar-thumb {
|
|
1305
|
+
background: var(--c_border, rgba(128,128,128,0.4));
|
|
1306
|
+
border-radius: 4px;
|
|
1307
|
+
}
|
|
1308
|
+
.lexical-editor::-webkit-scrollbar-thumb:hover {
|
|
1309
|
+
background: rgba(128,128,128,0.7);
|
|
1230
1310
|
}
|
|
1231
1311
|
.lexical-editor:focus {
|
|
1232
1312
|
border-color: var(--primary-color);
|