@devchitchat/chat 4.2.3 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/preview
|
|
3
|
+
*
|
|
4
|
+
* Renders markdown text server-side and returns the HTML.
|
|
5
|
+
* Used by the client-side compose overlay preview tab.
|
|
6
|
+
*
|
|
7
|
+
* Body: { text: string }
|
|
8
|
+
* Returns: { html: string }
|
|
9
|
+
*/
|
|
10
|
+
import { sessionFromRequest } from '../../../src/context.js'
|
|
11
|
+
import { renderMarkdown } from '@devchitchat/index97/markdown'
|
|
12
|
+
|
|
13
|
+
export async function POST(req) {
|
|
14
|
+
const session = sessionFromRequest(req)
|
|
15
|
+
if (!session?.user) return new Response('Unauthorized', { status: 401 })
|
|
16
|
+
|
|
17
|
+
let body
|
|
18
|
+
try {
|
|
19
|
+
body = await req.json()
|
|
20
|
+
} catch {
|
|
21
|
+
return new Response('Bad Request', { status: 400 })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const text = String(body.text ?? '').slice(0, 50_000)
|
|
25
|
+
const { html } = renderMarkdown(text)
|
|
26
|
+
return Response.json({ html })
|
|
27
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<ul each="channels" key="channel_id" class="channel-list">
|
|
12
12
|
<template>
|
|
13
13
|
<li class="channel-item" cls="className" draggable="true">
|
|
14
|
-
<a attr="href:url" text="name" class="channel-link"
|
|
14
|
+
<a attr="href:url; title:name" text="name" class="channel-link"
|
|
15
15
|
data-channel-id=""
|
|
16
16
|
data-channel-name=""
|
|
17
17
|
data-channel-topic=""
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
</template>
|
|
24
24
|
{{#each channels}}
|
|
25
25
|
<li data-key="{{channel_id}}" class="channel-item {{className}}" cls="className" draggable="true">
|
|
26
|
-
<a attr="href:url" text="name" class="channel-link"
|
|
26
|
+
<a attr="href:url; title:name" text="name" class="channel-link"
|
|
27
27
|
href="{{base}}/channels/{{channel_id}}"
|
|
28
|
+
title="{{name}}"
|
|
28
29
|
data-channel-id="{{channel_id}}"
|
|
29
30
|
data-channel-name="{{name}}"
|
|
30
31
|
data-channel-topic="{{topic}}"
|
|
@@ -157,8 +158,36 @@
|
|
|
157
158
|
<textarea model="draft" id="message-input" placeholder="Message in {{channel.name}}" aria-label="Write a message"
|
|
158
159
|
rows="1" onkeydown="handleComposerKey">
|
|
159
160
|
</textarea>
|
|
161
|
+
<button onclick="toggleComposeMode" class="btn-compose-expand btn-icon" type="button"
|
|
162
|
+
title="Compose mode (Ctrl+E)" aria-label="Open compose mode">⛶</button>
|
|
160
163
|
<button onclick="sendMessage" class="btn-send" aria-label="Send message" type="button">Send</button>
|
|
161
164
|
</footer>
|
|
165
|
+
|
|
166
|
+
<!-- Compose overlay — full-panel editor with Write / Preview tabs -->
|
|
167
|
+
<div class="compose-overlay" id="compose-overlay" hidden>
|
|
168
|
+
<div class="compose-overlay-header">
|
|
169
|
+
<div class="compose-tabs" role="tablist">
|
|
170
|
+
<button class="compose-tab compose-tab--active" data-tab="write"
|
|
171
|
+
role="tab" aria-selected="true" type="button">Write</button>
|
|
172
|
+
<button class="compose-tab" data-tab="preview"
|
|
173
|
+
role="tab" aria-selected="false" type="button">Preview</button>
|
|
174
|
+
</div>
|
|
175
|
+
<button class="btn-compose-collapse btn-icon" id="btn-compose-collapse"
|
|
176
|
+
type="button" title="Collapse (Esc)" aria-label="Collapse compose mode">✕</button>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="attachment-chips" id="compose-chips" hidden></div>
|
|
179
|
+
<textarea class="compose-textarea" id="compose-textarea"
|
|
180
|
+
placeholder="Write your message… (Ctrl+Enter to send)" aria-label="Compose message"></textarea>
|
|
181
|
+
<div class="compose-preview message-text" id="compose-preview" hidden aria-live="polite"></div>
|
|
182
|
+
<footer class="compose-overlay-footer">
|
|
183
|
+
<button class="btn-urgent-toggle" id="compose-urgent-toggle" type="button"
|
|
184
|
+
title="Toggle urgent mode" aria-label="Toggle urgent send">🔔</button>
|
|
185
|
+
<button class="btn-attach btn-icon" id="compose-attach" type="button"
|
|
186
|
+
title="Attach file" aria-label="Attach file">📎</button>
|
|
187
|
+
<button class="btn-send" id="compose-send" type="button">Send</button>
|
|
188
|
+
</footer>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
162
191
|
</section><!-- /.chat-panel -->
|
|
163
192
|
|
|
164
193
|
<!-- Tile panel: sibling of chat-panel, never inside it -->
|
|
@@ -583,32 +583,197 @@ export default function CallIsland(root) {
|
|
|
583
583
|
}
|
|
584
584
|
|
|
585
585
|
function renderChips() {
|
|
586
|
-
|
|
586
|
+
const html = pendingAttachments.map((a, i) => `
|
|
587
587
|
<span class="attachment-chip" data-index="${i}">
|
|
588
588
|
<span class="attachment-chip-name">${escHtml(a.original_name)}</span>
|
|
589
589
|
<button type="button" class="attachment-chip-remove" data-index="${i}" aria-label="Remove ${escHtml(a.original_name)}">×</button>
|
|
590
590
|
</span>
|
|
591
591
|
`).join('')
|
|
592
|
+
chipsEl.innerHTML = html
|
|
592
593
|
chipsEl.hidden = pendingAttachments.length === 0
|
|
594
|
+
if (composeChipsEl) {
|
|
595
|
+
composeChipsEl.innerHTML = html
|
|
596
|
+
composeChipsEl.hidden = pendingAttachments.length === 0
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function _removeChipAt(idx) {
|
|
601
|
+
pendingAttachments.splice(idx, 1)
|
|
602
|
+
renderChips()
|
|
593
603
|
}
|
|
594
604
|
|
|
595
605
|
chipsEl.addEventListener('click', e => {
|
|
596
606
|
const btn = e.target.closest('.attachment-chip-remove')
|
|
597
607
|
if (!btn) return
|
|
598
|
-
|
|
599
|
-
pendingAttachments.splice(idx, 1)
|
|
600
|
-
renderChips()
|
|
608
|
+
_removeChipAt(parseInt(btn.dataset.index, 10))
|
|
601
609
|
})
|
|
602
610
|
|
|
603
611
|
function showComposerError(msg) {
|
|
612
|
+
const target = composeOpen ? composeChipsEl : chipsEl
|
|
613
|
+
if (!target) return
|
|
604
614
|
const chip = document.createElement('span')
|
|
605
615
|
chip.className = 'attachment-chip attachment-chip-error'
|
|
606
616
|
chip.textContent = msg
|
|
607
|
-
|
|
608
|
-
|
|
617
|
+
target.appendChild(chip)
|
|
618
|
+
target.hidden = false
|
|
609
619
|
setTimeout(() => chip.remove(), 5000)
|
|
610
620
|
}
|
|
611
621
|
|
|
622
|
+
// ── Compose overlay ───────────────────────────────────────────────────────
|
|
623
|
+
const composeOverlayEl = root.querySelector('#compose-overlay')
|
|
624
|
+
const composeTaEl = document.getElementById('compose-textarea')
|
|
625
|
+
const composePreviewEl = document.getElementById('compose-preview')
|
|
626
|
+
const composeCollapseBtn = document.getElementById('btn-compose-collapse')
|
|
627
|
+
const composeSendBtn = document.getElementById('compose-send')
|
|
628
|
+
const composeUrgentBtn = document.getElementById('compose-urgent-toggle')
|
|
629
|
+
const composeChipsEl = document.getElementById('compose-chips')
|
|
630
|
+
const composeBtnAttach = document.getElementById('compose-attach')
|
|
631
|
+
|
|
632
|
+
let composeOpen = false
|
|
633
|
+
|
|
634
|
+
function openComposeMode() {
|
|
635
|
+
if (composeOpen) return
|
|
636
|
+
composeOpen = true
|
|
637
|
+
if (composeTaEl) composeTaEl.value = draft()
|
|
638
|
+
messages.hidden = true
|
|
639
|
+
composerEl.hidden = true
|
|
640
|
+
if (composeOverlayEl) composeOverlayEl.hidden = false
|
|
641
|
+
_switchComposeTab('write')
|
|
642
|
+
requestAnimationFrame(() => {
|
|
643
|
+
if (!composeTaEl) return
|
|
644
|
+
composeTaEl.focus()
|
|
645
|
+
const len = composeTaEl.value.length
|
|
646
|
+
composeTaEl.setSelectionRange(len, len)
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function closeComposeMode() {
|
|
651
|
+
if (!composeOpen) return
|
|
652
|
+
composeOpen = false
|
|
653
|
+
messages.hidden = false
|
|
654
|
+
composerEl.hidden = false
|
|
655
|
+
if (composeOverlayEl) composeOverlayEl.hidden = true
|
|
656
|
+
textareaEl?.focus()
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function toggleComposeMode() {
|
|
660
|
+
composeOpen ? closeComposeMode() : openComposeMode()
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function _switchComposeTab(tab) {
|
|
664
|
+
root.querySelectorAll('.compose-tab').forEach(btn => {
|
|
665
|
+
const active = btn.dataset.tab === tab
|
|
666
|
+
btn.classList.toggle('compose-tab--active', active)
|
|
667
|
+
btn.setAttribute('aria-selected', String(active))
|
|
668
|
+
})
|
|
669
|
+
if (composeTaEl) composeTaEl.hidden = tab !== 'write'
|
|
670
|
+
if (composePreviewEl) composePreviewEl.hidden = tab !== 'preview'
|
|
671
|
+
if (tab === 'preview') _renderComposePreview()
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async function _renderComposePreview() {
|
|
675
|
+
const text = composeTaEl?.value ?? ''
|
|
676
|
+
if (!text.trim()) {
|
|
677
|
+
if (composePreviewEl) composePreviewEl.innerHTML = '<p style="color:var(--text-muted)">Nothing to preview yet.</p>'
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
try {
|
|
681
|
+
const res = await fetch(`${window.__BASE_PATH__}/api/preview`, {
|
|
682
|
+
method: 'POST',
|
|
683
|
+
headers: { 'Content-Type': 'application/json' },
|
|
684
|
+
body: JSON.stringify({ text }),
|
|
685
|
+
})
|
|
686
|
+
if (res.ok) {
|
|
687
|
+
const { html } = await res.json()
|
|
688
|
+
if (composePreviewEl) composePreviewEl.innerHTML = sanitizeHtml(html)
|
|
689
|
+
}
|
|
690
|
+
} catch {
|
|
691
|
+
if (composePreviewEl) composePreviewEl.textContent = text
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Sync compose textarea → draft signal
|
|
696
|
+
composeTaEl?.addEventListener('input', () => { draft.set(composeTaEl.value) })
|
|
697
|
+
|
|
698
|
+
// Ctrl+Enter / Cmd+Enter in compose textarea → send and collapse
|
|
699
|
+
composeTaEl?.addEventListener('keydown', e => {
|
|
700
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
701
|
+
e.preventDefault()
|
|
702
|
+
sendMessage()
|
|
703
|
+
closeComposeMode()
|
|
704
|
+
}
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
// Tab strip
|
|
708
|
+
root.querySelectorAll('.compose-tab').forEach(btn => {
|
|
709
|
+
btn.addEventListener('click', () => _switchComposeTab(btn.dataset.tab))
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
// Collapse, send, and attach buttons
|
|
713
|
+
composeCollapseBtn?.addEventListener('click', closeComposeMode)
|
|
714
|
+
composeSendBtn?.addEventListener('click', () => { sendMessage(); closeComposeMode() })
|
|
715
|
+
composeUrgentBtn?.addEventListener('click', () => toggleUrgentMode())
|
|
716
|
+
composeBtnAttach?.addEventListener('click', () => fileInputEl.click())
|
|
717
|
+
composeChipsEl?.addEventListener('click', e => {
|
|
718
|
+
const btn = e.target.closest('.attachment-chip-remove')
|
|
719
|
+
if (!btn) return
|
|
720
|
+
_removeChipAt(parseInt(btn.dataset.index, 10))
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
// Paste images into the compose textarea
|
|
724
|
+
composeTaEl?.addEventListener('paste', e => {
|
|
725
|
+
const items = [...(e.clipboardData?.items ?? [])]
|
|
726
|
+
const imageFiles = items
|
|
727
|
+
.filter(item => item.kind === 'file' && item.type.startsWith('image/'))
|
|
728
|
+
.map(item => {
|
|
729
|
+
const file = item.getAsFile()
|
|
730
|
+
if (!file) return null
|
|
731
|
+
if (!file.name) {
|
|
732
|
+
const ext = item.type.split('/')[1] ?? 'png'
|
|
733
|
+
return new File([file], `paste-${Date.now()}.${ext}`, { type: item.type })
|
|
734
|
+
}
|
|
735
|
+
return file
|
|
736
|
+
})
|
|
737
|
+
.filter(Boolean)
|
|
738
|
+
if (imageFiles.length === 0) return
|
|
739
|
+
e.preventDefault()
|
|
740
|
+
uploadFiles(imageFiles)
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
// Drag-and-drop files onto the compose overlay
|
|
744
|
+
let composeDropOverlayEl = null
|
|
745
|
+
function ensureComposeDropOverlay() {
|
|
746
|
+
if (composeDropOverlayEl) return composeDropOverlayEl
|
|
747
|
+
composeDropOverlayEl = document.createElement('div')
|
|
748
|
+
composeDropOverlayEl.className = 'drop-overlay'
|
|
749
|
+
composeDropOverlayEl.textContent = 'Drop to attach'
|
|
750
|
+
composeOverlayEl?.appendChild(composeDropOverlayEl)
|
|
751
|
+
return composeDropOverlayEl
|
|
752
|
+
}
|
|
753
|
+
composeOverlayEl?.addEventListener('dragover', e => {
|
|
754
|
+
if (!e.dataTransfer.types.includes('Files')) return
|
|
755
|
+
e.preventDefault()
|
|
756
|
+
ensureComposeDropOverlay().hidden = false
|
|
757
|
+
})
|
|
758
|
+
composeOverlayEl?.addEventListener('dragleave', e => {
|
|
759
|
+
if (composeOverlayEl.contains(e.relatedTarget)) return
|
|
760
|
+
if (composeDropOverlayEl) composeDropOverlayEl.hidden = true
|
|
761
|
+
})
|
|
762
|
+
composeOverlayEl?.addEventListener('drop', e => {
|
|
763
|
+
e.preventDefault()
|
|
764
|
+
if (composeDropOverlayEl) composeDropOverlayEl.hidden = true
|
|
765
|
+
const files = [...(e.dataTransfer.files ?? [])]
|
|
766
|
+
if (files.length > 0) uploadFiles(files)
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
// Ctrl+E / Cmd+E anywhere to toggle compose mode
|
|
770
|
+
document.addEventListener('keydown', e => {
|
|
771
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'e') {
|
|
772
|
+
e.preventDefault()
|
|
773
|
+
toggleComposeMode()
|
|
774
|
+
}
|
|
775
|
+
})
|
|
776
|
+
|
|
612
777
|
// ── Urgent send ───────────────────────────────────────────────────────────
|
|
613
778
|
const urgentMode = signal(false)
|
|
614
779
|
const composerFooter = root.querySelector('.composer')
|
|
@@ -618,6 +783,8 @@ export default function CallIsland(root) {
|
|
|
618
783
|
function toggleUrgentMode() {
|
|
619
784
|
urgentMode.set(!urgentMode())
|
|
620
785
|
composerFooter?.classList.toggle('composer-urgent', urgentMode())
|
|
786
|
+
composeOverlayEl?.classList.toggle('composer-urgent', urgentMode())
|
|
787
|
+
composeUrgentBtn?.classList.toggle('is-urgent', urgentMode())
|
|
621
788
|
}
|
|
622
789
|
|
|
623
790
|
function sendMessage({ priority } = {}) {
|
|
@@ -908,6 +1075,7 @@ export default function CallIsland(root) {
|
|
|
908
1075
|
|
|
909
1076
|
document.addEventListener('keydown', e => {
|
|
910
1077
|
if (e.key === 'Escape') {
|
|
1078
|
+
if (composeOpen) { closeComposeMode(); return }
|
|
911
1079
|
closeEmojiPicker()
|
|
912
1080
|
}
|
|
913
1081
|
})
|
|
@@ -1013,42 +1181,85 @@ export default function CallIsland(root) {
|
|
|
1013
1181
|
// ── Inline edit ────────────────────────────────────────────────────────────
|
|
1014
1182
|
|
|
1015
1183
|
function startInlineEdit(article) {
|
|
1016
|
-
if (article.querySelector('.message-edit-
|
|
1017
|
-
const textEl
|
|
1184
|
+
if (article.querySelector('.message-edit-wrap')) return
|
|
1185
|
+
const textEl = article.querySelector('.message-text')
|
|
1018
1186
|
if (!textEl) return
|
|
1019
1187
|
const rawText = article.dataset.rawText ?? ''
|
|
1020
1188
|
|
|
1189
|
+
// Build the edit widget
|
|
1190
|
+
const wrap = document.createElement('div')
|
|
1191
|
+
wrap.className = 'message-edit-wrap'
|
|
1192
|
+
|
|
1193
|
+
const tabStrip = document.createElement('div')
|
|
1194
|
+
tabStrip.className = 'message-edit-tabs'
|
|
1195
|
+
tabStrip.setAttribute('role', 'tablist')
|
|
1196
|
+
tabStrip.innerHTML = `
|
|
1197
|
+
<button class="message-edit-tab message-edit-tab--active" data-tab="write" role="tab" aria-selected="true" type="button">Write</button>
|
|
1198
|
+
<button class="message-edit-tab" data-tab="preview" role="tab" aria-selected="false" type="button">Preview</button>`
|
|
1199
|
+
|
|
1021
1200
|
const textarea = document.createElement('textarea')
|
|
1022
1201
|
textarea.className = 'message-edit-input'
|
|
1023
1202
|
textarea.value = rawText
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1203
|
+
|
|
1204
|
+
const preview = document.createElement('div')
|
|
1205
|
+
preview.className = 'message-edit-preview message-text'
|
|
1206
|
+
preview.hidden = true
|
|
1207
|
+
preview.setAttribute('aria-live', 'polite')
|
|
1027
1208
|
|
|
1028
1209
|
const toolbar = document.createElement('div')
|
|
1029
1210
|
toolbar.className = 'message-edit-toolbar'
|
|
1030
|
-
toolbar.innerHTML =
|
|
1031
|
-
|
|
1211
|
+
toolbar.innerHTML = `
|
|
1212
|
+
<span class="message-edit-hint">Ctrl+Enter to save · Esc to cancel</span>
|
|
1213
|
+
<button class="btn-ghost btn-edit-cancel" type="button">Cancel</button>
|
|
1214
|
+
<button class="btn-primary btn-edit-save" type="button">Save</button>`
|
|
1215
|
+
|
|
1216
|
+
wrap.append(tabStrip, textarea, preview, toolbar)
|
|
1217
|
+
textEl.replaceWith(wrap)
|
|
1218
|
+
textarea.focus()
|
|
1219
|
+
textarea.setSelectionRange(rawText.length, rawText.length)
|
|
1220
|
+
|
|
1221
|
+
// Tab switching
|
|
1222
|
+
tabStrip.addEventListener('click', async e => {
|
|
1223
|
+
const btn = e.target.closest('.message-edit-tab')
|
|
1224
|
+
if (!btn) return
|
|
1225
|
+
const tab = btn.dataset.tab
|
|
1226
|
+
tabStrip.querySelectorAll('.message-edit-tab').forEach(b => {
|
|
1227
|
+
b.classList.toggle('message-edit-tab--active', b === btn)
|
|
1228
|
+
b.setAttribute('aria-selected', String(b === btn))
|
|
1229
|
+
})
|
|
1230
|
+
textarea.hidden = tab !== 'write'
|
|
1231
|
+
preview.hidden = tab !== 'preview'
|
|
1232
|
+
if (tab === 'preview') {
|
|
1233
|
+
const text = textarea.value.trim()
|
|
1234
|
+
if (!text) { preview.innerHTML = '<p style="color:var(--text-muted)">Nothing to preview yet.</p>'; return }
|
|
1235
|
+
try {
|
|
1236
|
+
const res = await fetch(`${window.__BASE_PATH__}/api/preview`, {
|
|
1237
|
+
method: 'POST',
|
|
1238
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1239
|
+
body: JSON.stringify({ text }),
|
|
1240
|
+
})
|
|
1241
|
+
if (res.ok) { const { html } = await res.json(); preview.innerHTML = sanitizeHtml(html) }
|
|
1242
|
+
} catch { preview.textContent = text }
|
|
1243
|
+
}
|
|
1244
|
+
})
|
|
1032
1245
|
|
|
1033
1246
|
function cancel() {
|
|
1034
|
-
|
|
1035
|
-
toolbar.remove()
|
|
1247
|
+
wrap.replaceWith(textEl)
|
|
1036
1248
|
}
|
|
1037
1249
|
|
|
1038
1250
|
function save() {
|
|
1039
1251
|
const newText = textarea.value.trim()
|
|
1040
|
-
if (!newText) { cancel(); return }
|
|
1041
|
-
if (newText === rawText) { cancel(); return }
|
|
1042
|
-
// Optimistic: show new text immediately (unrendered), server will push rendered version via msg.edited
|
|
1252
|
+
if (!newText || newText === rawText) { cancel(); return }
|
|
1043
1253
|
textEl.textContent = newText
|
|
1044
1254
|
cancel()
|
|
1255
|
+
article.dataset.rawText = newText
|
|
1045
1256
|
ws.send({ t: 'msg.edit', body: { msg_id: article.dataset.msgId, channel_id: channelId, text: newText } })
|
|
1046
1257
|
}
|
|
1047
1258
|
|
|
1048
1259
|
toolbar.querySelector('.btn-edit-save').addEventListener('click', save)
|
|
1049
1260
|
toolbar.querySelector('.btn-edit-cancel').addEventListener('click', cancel)
|
|
1050
1261
|
textarea.addEventListener('keydown', e => {
|
|
1051
|
-
if (e.key === 'Enter'
|
|
1262
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); save() }
|
|
1052
1263
|
if (e.key === 'Escape') cancel()
|
|
1053
1264
|
})
|
|
1054
1265
|
}
|
|
@@ -1731,6 +1942,8 @@ export default function CallIsland(root) {
|
|
|
1731
1942
|
document.addEventListener('chatpanel:navigated', (e) => {
|
|
1732
1943
|
const { channelId: newId, name, topic, kind, seedSeq: newSeedSeq, seedFirstSeq: newFirstSeq, seedHasMore: newHasMore } = e.detail
|
|
1733
1944
|
|
|
1945
|
+
if (composeOpen && newId !== channelId) closeComposeMode()
|
|
1946
|
+
|
|
1734
1947
|
// Morph strips dynamically-added content (reactions, attachments, timestamps, etc.)
|
|
1735
1948
|
// but may preserve data-hydrated="1". Always clear and re-hydrate.
|
|
1736
1949
|
for (const a of messages.querySelectorAll('article.message[data-hydrated]')) {
|
|
@@ -1811,5 +2024,5 @@ export default function CallIsland(root) {
|
|
|
1811
2024
|
|
|
1812
2025
|
// ── Exports (rdbljs bindings) ──────────────────────────────────────────────
|
|
1813
2026
|
|
|
1814
|
-
return { draft, channelName, channelTopic, urgentMode, urgentClass, sendMessage, handleComposerKey, toggleUrgentMode }
|
|
2027
|
+
return { draft, channelName, channelTopic, urgentMode, urgentClass, sendMessage, handleComposerKey, toggleUrgentMode, toggleComposeMode }
|
|
1815
2028
|
}
|
|
@@ -92,15 +92,19 @@ details summary {
|
|
|
92
92
|
color: var(--text-secondary);
|
|
93
93
|
text-decoration: none;
|
|
94
94
|
flex: 1;
|
|
95
|
+
min-width: 0;
|
|
95
96
|
height: 2.3rem;
|
|
96
|
-
display:
|
|
97
|
-
|
|
97
|
+
display: block;
|
|
98
|
+
line-height: 2.3rem;
|
|
98
99
|
padding-left: .7rem;
|
|
99
100
|
padding-right: .7rem;
|
|
100
101
|
user-select: none;
|
|
101
102
|
-webkit-user-select: none;
|
|
102
103
|
-webkit-touch-callout: none;
|
|
103
104
|
transition: color var(--transition), background var(--transition);
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
white-space: nowrap;
|
|
107
|
+
text-overflow: ellipsis;
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
.channel-item a:hover {
|
|
@@ -753,6 +757,7 @@ details summary {
|
|
|
753
757
|
border-radius: 0;
|
|
754
758
|
}
|
|
755
759
|
|
|
760
|
+
.composer[hidden] { display: none; }
|
|
756
761
|
.composer {
|
|
757
762
|
margin-top: auto;
|
|
758
763
|
display: flex;
|
|
@@ -804,6 +809,97 @@ details summary {
|
|
|
804
809
|
.composer.composer-urgent textarea,
|
|
805
810
|
.composer.composer-urgent textarea:focus { border-color: var(--color-danger); }
|
|
806
811
|
|
|
812
|
+
/* Expand button in the composer bar */
|
|
813
|
+
.btn-compose-expand {
|
|
814
|
+
flex-shrink: 0;
|
|
815
|
+
align-self: center;
|
|
816
|
+
font-size: 16px;
|
|
817
|
+
opacity: 0.5;
|
|
818
|
+
transition: opacity var(--transition);
|
|
819
|
+
}
|
|
820
|
+
.btn-compose-expand:hover { opacity: 1; }
|
|
821
|
+
|
|
822
|
+
/* ── Compose overlay ─────────────────────────────────────────────────────── */
|
|
823
|
+
.compose-overlay {
|
|
824
|
+
display: flex;
|
|
825
|
+
flex-direction: column;
|
|
826
|
+
flex: 1;
|
|
827
|
+
min-height: 0;
|
|
828
|
+
background: var(--bg-base);
|
|
829
|
+
}
|
|
830
|
+
.compose-overlay[hidden] { display: none; }
|
|
831
|
+
|
|
832
|
+
.compose-overlay-header {
|
|
833
|
+
display: flex;
|
|
834
|
+
align-items: center;
|
|
835
|
+
justify-content: space-between;
|
|
836
|
+
padding: 6px 12px 0;
|
|
837
|
+
border-bottom: 1px solid var(--border);
|
|
838
|
+
flex-shrink: 0;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.compose-tabs { display: flex; }
|
|
842
|
+
|
|
843
|
+
.compose-tab {
|
|
844
|
+
background: none;
|
|
845
|
+
border: none;
|
|
846
|
+
border-bottom: 2px solid transparent;
|
|
847
|
+
padding: 8px 14px;
|
|
848
|
+
cursor: pointer;
|
|
849
|
+
color: var(--text-secondary);
|
|
850
|
+
font-size: 13px;
|
|
851
|
+
font-weight: 500;
|
|
852
|
+
transition: color var(--transition), border-color var(--transition);
|
|
853
|
+
margin-bottom: -1px; /* sit on top of the header border */
|
|
854
|
+
}
|
|
855
|
+
.compose-tab:hover { color: var(--text-primary); }
|
|
856
|
+
.compose-tab--active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
857
|
+
|
|
858
|
+
.btn-compose-collapse {
|
|
859
|
+
font-size: 14px;
|
|
860
|
+
opacity: 0.5;
|
|
861
|
+
transition: opacity var(--transition);
|
|
862
|
+
}
|
|
863
|
+
.btn-compose-collapse:hover { opacity: 1; }
|
|
864
|
+
|
|
865
|
+
.compose-textarea {
|
|
866
|
+
flex: 1;
|
|
867
|
+
min-height: 0;
|
|
868
|
+
resize: none;
|
|
869
|
+
border: none;
|
|
870
|
+
background: var(--bg-base);
|
|
871
|
+
color: var(--text-primary);
|
|
872
|
+
font-family: var(--font-sans);
|
|
873
|
+
font-size: 14px;
|
|
874
|
+
line-height: 1.6;
|
|
875
|
+
padding: 16px 20px;
|
|
876
|
+
outline: none;
|
|
877
|
+
overflow-y: auto;
|
|
878
|
+
}
|
|
879
|
+
.compose-textarea::placeholder { color: var(--text-muted); }
|
|
880
|
+
|
|
881
|
+
.compose-preview {
|
|
882
|
+
flex: 1;
|
|
883
|
+
min-height: 0;
|
|
884
|
+
overflow-y: auto;
|
|
885
|
+
padding: 16px 20px;
|
|
886
|
+
line-height: 1.6;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.compose-overlay-footer {
|
|
890
|
+
display: flex;
|
|
891
|
+
align-items: center;
|
|
892
|
+
justify-content: flex-end;
|
|
893
|
+
gap: 8px;
|
|
894
|
+
padding: 10px 16px;
|
|
895
|
+
padding-bottom: calc(10px + env(safe-area-inset-bottom));
|
|
896
|
+
border-top: 1px solid var(--border);
|
|
897
|
+
flex-shrink: 0;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/* Urgent mode inside compose overlay */
|
|
901
|
+
.compose-overlay.composer-urgent .compose-textarea { border-left: 3px solid var(--color-danger); }
|
|
902
|
+
|
|
807
903
|
/* ── Load-more sentinel ──────────────────────────────────────────────────── */
|
|
808
904
|
.load-more-sentinel {
|
|
809
905
|
height: 1px;
|
|
@@ -1800,35 +1896,80 @@ form.inline { display: inline; }
|
|
|
1800
1896
|
margin-left: 4px;
|
|
1801
1897
|
}
|
|
1802
1898
|
|
|
1899
|
+
.message-edit-wrap {
|
|
1900
|
+
grid-column: 1 / -1;
|
|
1901
|
+
border: 1px solid var(--accent);
|
|
1902
|
+
border-radius: var(--radius-md);
|
|
1903
|
+
overflow: hidden;
|
|
1904
|
+
background: var(--bg-input, var(--bg-sidebar));
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
.message-edit-tabs {
|
|
1908
|
+
display: flex;
|
|
1909
|
+
border-bottom: 1px solid var(--border);
|
|
1910
|
+
padding: 0 4px;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
.message-edit-tab {
|
|
1914
|
+
background: none;
|
|
1915
|
+
border: none;
|
|
1916
|
+
border-bottom: 2px solid transparent;
|
|
1917
|
+
padding: 6px 12px;
|
|
1918
|
+
cursor: pointer;
|
|
1919
|
+
color: var(--text-secondary);
|
|
1920
|
+
font-size: 12px;
|
|
1921
|
+
font-weight: 500;
|
|
1922
|
+
margin-bottom: -1px;
|
|
1923
|
+
transition: color var(--transition), border-color var(--transition);
|
|
1924
|
+
}
|
|
1925
|
+
.message-edit-tab:hover { color: var(--text-primary); }
|
|
1926
|
+
.message-edit-tab--active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
1927
|
+
|
|
1928
|
+
.message-edit-input[hidden] { display: none; }
|
|
1803
1929
|
.message-edit-input {
|
|
1930
|
+
display: block;
|
|
1804
1931
|
width: 100%;
|
|
1805
|
-
min-height:
|
|
1806
|
-
|
|
1932
|
+
min-height: 80px;
|
|
1933
|
+
max-height: 320px;
|
|
1934
|
+
padding: 10px 12px;
|
|
1807
1935
|
font: inherit;
|
|
1808
1936
|
font-size: inherit;
|
|
1809
1937
|
line-height: 1.5;
|
|
1810
|
-
border:
|
|
1811
|
-
|
|
1812
|
-
background: var(--bg-input, var(--bg-sidebar));
|
|
1938
|
+
border: none;
|
|
1939
|
+
background: transparent;
|
|
1813
1940
|
color: var(--text-primary);
|
|
1814
1941
|
resize: vertical;
|
|
1815
1942
|
box-sizing: border-box;
|
|
1816
|
-
|
|
1943
|
+
outline: none;
|
|
1944
|
+
overflow-y: auto;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
.message-edit-preview {
|
|
1948
|
+
min-height: 80px;
|
|
1949
|
+
max-height: 320px;
|
|
1950
|
+
padding: 10px 12px;
|
|
1951
|
+
overflow-y: auto;
|
|
1817
1952
|
}
|
|
1818
1953
|
|
|
1819
1954
|
.message-edit-toolbar {
|
|
1820
1955
|
display: flex;
|
|
1956
|
+
align-items: center;
|
|
1821
1957
|
gap: 6px;
|
|
1822
|
-
|
|
1823
|
-
|
|
1958
|
+
padding: 8px 10px;
|
|
1959
|
+
border-top: 1px solid var(--border);
|
|
1960
|
+
}
|
|
1961
|
+
.message-edit-hint {
|
|
1962
|
+
font-size: 11px;
|
|
1963
|
+
color: var(--text-muted);
|
|
1964
|
+
margin-right: auto;
|
|
1824
1965
|
}
|
|
1825
1966
|
.message-edit-toolbar .btn-primary {
|
|
1826
|
-
padding:
|
|
1827
|
-
font-size:
|
|
1967
|
+
padding: 5px 14px;
|
|
1968
|
+
font-size: 13px;
|
|
1828
1969
|
font-weight: 600;
|
|
1829
1970
|
}
|
|
1830
1971
|
.message-edit-toolbar .btn-ghost {
|
|
1831
|
-
font-size:
|
|
1972
|
+
font-size: 13px;
|
|
1832
1973
|
}
|
|
1833
1974
|
|
|
1834
1975
|
/* Hover action toolbar — floats above the message on hover (Discord-style) */
|