Skip to content

Commit feffffd

Browse files
committed
feat: bottom sheets on mobile, long content collapsing, mobile code block improvements
1 parent 0cc7673 commit feffffd

File tree

2 files changed

+211
-15
lines changed

2 files changed

+211
-15
lines changed

client/css/app.css

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,38 @@ input[type=text]:focus {
651651
padding-left: 20px;
652652
}
653653

654+
.item-text-collapsed {
655+
position: relative;
656+
max-height: 9em;
657+
overflow: hidden;
658+
}
659+
660+
.item-text-collapsed::after {
661+
content: "";
662+
position: absolute;
663+
bottom: 0;
664+
left: 0;
665+
right: 0;
666+
height: 3em;
667+
background: linear-gradient(to bottom, transparent, #ffffff);
668+
pointer-events: none;
669+
}
670+
671+
.item-text-expand-btn {
672+
display: block;
673+
margin-top: 4px;
674+
font-size: 12px;
675+
color: var(--color-secondary);
676+
background: none;
677+
border: none;
678+
padding: 2px 0;
679+
cursor: pointer;
680+
text-align: left;
681+
}
682+
683+
.item-text-expand-btn:hover {
684+
color: var(--color-primary);
685+
}
654686

655687
/* ============================================================
656688
PREVIEW PANEL
@@ -1174,6 +1206,122 @@ input[type=text]:focus {
11741206
padding: 6px 12px;
11751207
font-size: 12px;
11761208
}
1209+
1210+
/* Backdrop */
1211+
.bottom-sheet-backdrop {
1212+
position: fixed;
1213+
inset: 0;
1214+
background: rgba(0, 0, 0, 0.4);
1215+
z-index: 9998;
1216+
display: none;
1217+
animation: fadeInBackdrop 0.2s ease;
1218+
}
1219+
1220+
.bottom-sheet-backdrop.open {
1221+
display: block;
1222+
}
1223+
1224+
@keyframes fadeInBackdrop {
1225+
from {
1226+
opacity: 0;
1227+
}
1228+
1229+
to {
1230+
opacity: 1;
1231+
}
1232+
}
1233+
1234+
/* Context menu becomes a bottom sheet */
1235+
.context-menu {
1236+
position: fixed;
1237+
top: auto !important;
1238+
left: 0 !important;
1239+
right: 0;
1240+
bottom: 0;
1241+
width: 100%;
1242+
max-width: 100%;
1243+
border-radius: 16px 16px 0 0;
1244+
padding: 8px 0 calc(8px + env(safe-area-inset-bottom));
1245+
z-index: 9999;
1246+
animation: slideUpSheet 0.25s ease;
1247+
box-sizing: border-box;
1248+
}
1249+
1250+
/* Drag handle */
1251+
.context-menu::before {
1252+
content: "";
1253+
display: block;
1254+
width: 36px;
1255+
height: 4px;
1256+
background: #d1d5db;
1257+
border-radius: 2px;
1258+
margin: 0 auto 10px;
1259+
}
1260+
1261+
.context-menu button {
1262+
padding: 14px 20px;
1263+
font-size: 15px;
1264+
border-radius: 0;
1265+
}
1266+
1267+
/* Move dropdown becomes a bottom sheet */
1268+
.move-dropdown {
1269+
position: fixed !important;
1270+
top: auto !important;
1271+
bottom: 0 !important;
1272+
left: 0 !important;
1273+
right: 0 !important;
1274+
width: 100% !important;
1275+
min-width: 100% !important;
1276+
border-radius: 16px 16px 0 0;
1277+
padding: 8px 0 calc(8px + env(safe-area-inset-bottom));
1278+
z-index: 9999;
1279+
animation: slideUpSheet 0.25s ease;
1280+
box-sizing: border-box;
1281+
}
1282+
1283+
.move-dropdown::before {
1284+
content: "";
1285+
display: block;
1286+
width: 36px;
1287+
height: 4px;
1288+
background: #d1d5db;
1289+
border-radius: 2px;
1290+
margin: 0 auto 10px;
1291+
}
1292+
1293+
.move-dropdown button {
1294+
padding: 14px 20px;
1295+
font-size: 15px;
1296+
border-radius: 0;
1297+
}
1298+
1299+
.move-dropdown .dropdown-label {
1300+
padding: 8px 20px 6px;
1301+
font-size: 12px;
1302+
}
1303+
1304+
.preview-panel pre {
1305+
overflow-x: scroll;
1306+
-webkit-overflow-scrolling: touch;
1307+
max-height: 300px;
1308+
font-size: 11px;
1309+
}
1310+
1311+
.markdown-body pre {
1312+
overflow-x: scroll;
1313+
-webkit-overflow-scrolling: touch;
1314+
}
1315+
1316+
@keyframes slideUpSheet {
1317+
from {
1318+
transform: translateY(100%);
1319+
}
1320+
1321+
to {
1322+
transform: translateY(0);
1323+
}
1324+
}
11771325
}
11781326

11791327

@@ -1572,6 +1720,10 @@ input[type=text]:focus {
15721720
background: #052e16;
15731721
}
15741722

1723+
.item-text-collapsed::after {
1724+
background: linear-gradient(to bottom, transparent, #1e2433);
1725+
}
1726+
15751727
#uploadStatus {
15761728
background: #1a1d27 !important;
15771729
color: #e5e7eb !important;
@@ -1966,6 +2118,10 @@ input[type=text]:focus {
19662118
background: #052e16;
19672119
}
19682120

2121+
.item-text-collapsed::after {
2122+
background: linear-gradient(to bottom, transparent, #1e2433);
2123+
}
2124+
19692125
#uploadStatus {
19702126
background: #1a1d27 !important;
19712127
color: #e5e7eb !important;

client/js/app.js

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -213,18 +213,30 @@ function renderText(text) {
213213
if (!text) return "";
214214
if (looksLikeMarkdown(text)) {
215215
const html = marked.parse(text);
216-
// highlight code blocks after parse
217216
const wrap = document.createElement("div");
218217
wrap.innerHTML = html;
219218
wrap.querySelectorAll("pre code").forEach(el => hljs.highlightElement(el));
220219
return `<div class="markdown-body">${wrap.innerHTML}</div>`;
221220
}
222-
// plain text — just escape and preserve newlines
223-
return text
221+
// plain text — escape and preserve newlines
222+
const escaped = text
224223
.replace(/&/g, "&amp;")
225224
.replace(/</g, "&lt;")
226225
.replace(/>/g, "&gt;")
227226
.replace(/\n/g, "<br>");
227+
228+
// collapse long plain text (more than 8 newlines)
229+
const lineCount = (text.match(/\n/g) || []).length;
230+
if (lineCount > 8) {
231+
const id = 'expand-' + Math.random().toString(36).slice(2, 8);
232+
return `<div class="item-text-collapsed" id="${id}">${escaped}</div>
233+
<button class="item-text-expand-btn" onclick="
234+
document.getElementById('${id}').classList.remove('item-text-collapsed');
235+
this.remove();
236+
">Show more</button>`;
237+
}
238+
239+
return escaped;
228240
}
229241

230242
const TEXT_EXTENSIONS = [
@@ -455,6 +467,7 @@ function toggleMoveDropdown(e, id, currentChannel) {
455467

456468
dropdown.classList.add("open");
457469
openDropdown = dropdown;
470+
if (window.innerWidth <= 640) openBottomSheet(dropdown);
458471
}
459472

460473
function toggleMoreMenu(e, id, currentChannel) {
@@ -1479,21 +1492,27 @@ function showChannelMenu(e, ch) {
14791492
</button>
14801493
`;
14811494

1482-
menu.style.top = e.clientY + "px";
1483-
menu.style.left = e.clientX + "px";
1484-
menu.classList.add("open");
1485-
1486-
requestAnimationFrame(() => {
1487-
const rect = menu.getBoundingClientRect();
1488-
if (rect.right > window.innerWidth - 8)
1489-
menu.style.left = (e.clientX - rect.width) + "px";
1490-
if (rect.bottom > window.innerHeight - 8)
1491-
menu.style.top = (e.clientY - rect.height) + "px";
1492-
});
1495+
if (window.innerWidth <= 640) {
1496+
// mobile — bottom sheet, no positioning needed
1497+
menu.classList.add("open");
1498+
openBottomSheet(menu);
1499+
} else {
1500+
// desktop — position by click coordinates
1501+
menu.style.top = e.clientY + "px";
1502+
menu.style.left = e.clientX + "px";
1503+
menu.classList.add("open");
1504+
requestAnimationFrame(() => {
1505+
const rect = menu.getBoundingClientRect();
1506+
if (rect.right > window.innerWidth - 8)
1507+
menu.style.left = (e.clientX - rect.width) + "px";
1508+
if (rect.bottom > window.innerHeight - 8)
1509+
menu.style.top = (e.clientY - rect.height) + "px";
1510+
});
1511+
}
14931512
}
14941513

14951514
document.addEventListener("click", () => {
1496-
document.getElementById("channelMenu").classList.remove("open");
1515+
closeAllBottomSheets();
14971516
});
14981517

14991518
document.addEventListener("contextmenu", (e) => {
@@ -1638,6 +1657,27 @@ document.addEventListener("keydown", e => {
16381657

16391658
});
16401659

1660+
// Create shared backdrop for bottom sheets
1661+
const bottomSheetBackdrop = document.createElement('div');
1662+
bottomSheetBackdrop.className = 'bottom-sheet-backdrop';
1663+
document.body.appendChild(bottomSheetBackdrop);
1664+
1665+
function openBottomSheet(el) {
1666+
bottomSheetBackdrop.classList.add('open');
1667+
bottomSheetBackdrop.onclick = () => closeAllBottomSheets();
1668+
}
1669+
1670+
function closeAllBottomSheets() {
1671+
bottomSheetBackdrop.classList.remove('open');
1672+
// close context menu
1673+
document.getElementById('channelMenu').classList.remove('open');
1674+
// close any open move dropdown
1675+
if (openDropdown) {
1676+
openDropdown.classList.remove('open');
1677+
openDropdown = null;
1678+
}
1679+
}
1680+
16411681
(async function init() {
16421682
await applyBranding();
16431683
await initName();

0 commit comments

Comments
 (0)