feat(ui): introduce UI transparency settings and glass effect toggle; enhance message display and scrolling behavior in conversation viewer

This commit is contained in:
Ivan committed 2026-04-16 00:52:28 -05:00
1 parent 2db12e0bde
commit 99cf1e1463
7 files changed
+1201 -1150

No files matched your search

+30 -3
View File
@@ -1,7 +1,8 @@
<template>
<div
:class="{ dark: config?.theme === 'dark' }"
class="h-screen w-full flex flex-col bg-slate-50 dark:bg-zinc-950 transition-colors"
class="h-screen w-full flex flex-col transition-colors"
:style="shellCanvasStyle"
>
<!-- emergency banner -->
<div
@@ -34,7 +35,11 @@
<RouterView v-if="$route.name === 'auth'" />
<template v-else>
<div v-if="isPopoutMode" class="flex flex-1 h-full w-full overflow-hidden bg-slate-50 dark:bg-zinc-950">
<div
v-if="isPopoutMode"
class="flex flex-1 h-full w-full overflow-hidden transition-colors"
:style="shellCanvasStyle"
>
<RouterView class="flex-1" />
</div>
@@ -122,7 +127,8 @@
<!-- middle -->
<div
ref="middle"
class="flex flex-1 w-full overflow-hidden bg-slate-50 dark:bg-zinc-950 transition-colors"
class="flex flex-1 w-full overflow-hidden transition-colors"
:style="shellCanvasStyle"
>
<!-- sidebar backdrop for mobile -->
<div
@@ -709,6 +715,17 @@ export default {
showWsDisconnectedBanner() {
return this.shellRunning && this.wsDisconnected && this.$route?.name !== "auth";
},
shellCanvasStyle() {
const raw = Number(this.config?.ui_transparency ?? 0);
const t = Number.isFinite(raw) ? Math.max(0, Math.min(100, raw)) : 0;
const factor = t / 100;
const alpha = 1 - factor * 0.42;
const isDark = this.config?.theme === "dark";
if (isDark) {
return { backgroundColor: `rgba(9, 9, 11, ${alpha})` };
}
return { backgroundColor: `rgba(248, 250, 252, ${alpha})` };
},
},
watch: {
$route(to, from) {
@@ -729,6 +746,7 @@ export default {
if (newConfig && "theme" in newConfig) {
this.applyThemePreference(newConfig.theme ?? "light");
}
this.applyShellAppearance();
},
deep: true,
},
@@ -758,6 +776,7 @@ export default {
// ignore
}
this.startShellAuthWatch();
this.applyShellAppearance();
if (ElectronUtils.isElectron()) {
window.electron.onProtocolLink((url) => {
this.handleProtocolLink(url);
@@ -981,6 +1000,14 @@ export default {
if (themeName && typeof themeName === "object" && "value" in themeName) {
themeName.value = mode;
}
this.applyShellAppearance();
},
applyShellAppearance() {
if (typeof document === "undefined") {
return;
}
const glassOn = this.config?.ui_glass_enabled !== false;
document.documentElement.dataset.uiGlass = glassOn ? "1" : "0";
},
getHashPopoutValue() {
const hash = window.location.hash || "";
@@ -2425,9 +2425,7 @@ export default {
},
logWebAudioFailure(stage, error) {
const appImage = Boolean(
window.electron &&
typeof navigator?.userAgent === "string" &&
navigator.userAgent.includes("AppImage"),
window.electron && typeof navigator?.userAgent === "string" && navigator.userAgent.includes("AppImage")
);
console.error(
`[CallPage:web-audio] ${stage}`,
@@ -2436,7 +2434,7 @@ export default {
isAppImage: appImage,
userAgent: navigator?.userAgent || "unknown",
},
error,
error
);
},
async disableWebAudioBridgeWithError(errorKey, error, stage = "unknown") {
@@ -2498,7 +2496,7 @@ export default {
await this.disableWebAudioBridgeWithError(
"call.web_audio_not_available",
new Error("navigator.mediaDevices is unavailable"),
"start-preflight-media-devices",
"start-preflight-media-devices"
);
return;
}
@@ -2508,7 +2506,7 @@ export default {
await this.disableWebAudioBridgeWithError(
"call.no_audio_input_found",
new Error("No audio input devices detected"),
"start-no-input-devices",
"start-no-input-devices"
);
return;
}
File diff suppressed because it is too large. Load diff
@@ -407,11 +407,7 @@
ref="messagesScroll"
class="flex-1 min-h-0 overflow-y-scroll bg-white dark:bg-zinc-950 transition-none"
style="overflow-anchor: none; overscroll-behavior-y: contain"
:class="
!messagesViewportReady
? 'invisible opacity-0 pointer-events-none select-none'
: ''
"
:class="!messagesViewportReady ? 'invisible opacity-0 pointer-events-none select-none' : ''"
:data-message-list-mode="useVirtualMessageList ? 'virtual' : 'reverse'"
:aria-busy="!messagesViewportReady ? 'true' : undefined"
@scroll="onMessagesScroll"
@@ -427,55 +423,55 @@
<ConversationMessageEntry :entry="entry" :cv="conversationViewerSelf" />
</template>
<!-- load previous -->
<button
v-show="!isLoadingPrevious && hasMorePrevious"
id="load-previous"
type="button"
class="flex items-center gap-2 mx-auto mt-4 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 px-4 py-2 hover:bg-gray-50 dark:hover:bg-zinc-800 rounded-full shadow-sm text-sm font-medium text-gray-700 dark:text-zinc-300 transition-colors"
@click="loadPrevious"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m15 11.25-3-3m0 0-3 3m3-3v7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
<span>Load Previous</span>
</button>
<button
v-show="!isLoadingPrevious && hasMorePrevious"
id="load-previous"
type="button"
class="flex items-center gap-2 mx-auto mt-4 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 px-4 py-2 hover:bg-gray-50 dark:hover:bg-zinc-800 rounded-full shadow-sm text-sm font-medium text-gray-700 dark:text-zinc-300 transition-colors"
@click="loadPrevious"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m15 11.25-3-3m0 0-3 3m3-3v7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
<span>Load Previous</span>
</button>
</div>
</template>
<template v-else>
<button
v-show="!isLoadingPrevious && hasMorePrevious"
id="load-previous"
type="button"
class="absolute top-2 left-1/2 z-20 -translate-x-1/2 flex items-center gap-2 bg-white/95 dark:bg-zinc-950/95 backdrop-blur border border-gray-200 dark:border-zinc-800 px-4 py-2 hover:bg-gray-50 dark:hover:bg-zinc-800 rounded-full shadow-sm text-sm font-medium text-gray-700 dark:text-zinc-300 transition-colors"
@click="loadPrevious"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
<button
v-show="!isLoadingPrevious && hasMorePrevious"
id="load-previous"
type="button"
class="absolute top-2 left-1/2 z-20 -translate-x-1/2 flex items-center gap-2 bg-white/95 dark:bg-zinc-950/95 backdrop-blur border border-gray-200 dark:border-zinc-800 px-4 py-2 hover:bg-gray-50 dark:hover:bg-zinc-800 rounded-full shadow-sm text-sm font-medium text-gray-700 dark:text-zinc-300 transition-colors"
@click="loadPrevious"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m15 11.25-3-3m0 0-3 3m3-3v7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
<span>Load Previous</span>
</button>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m15 11.25-3-3m0 0-3 3m3-3v7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
<span>Load Previous</span>
</button>
<ConversationMessageListVirtual
ref="messageListVirtual"
:groups="selectedPeerChatDisplayGroupsOldestFirst"
@@ -487,7 +483,10 @@
</div>
<Transition name="scroll-fab">
<div v-if="!autoScrollOnNewMessage && messagesViewportReady" class="flex justify-center pb-1.5 pt-0.5 shrink-0">
<div
v-if="!autoScrollOnNewMessage && messagesViewportReady"
class="flex justify-center pb-1.5 pt-0.5 shrink-0"
>
<button
type="button"
class="flex items-center justify-center size-8 rounded-full bg-white/90 dark:bg-zinc-800/90 backdrop-blur border border-gray-200 dark:border-zinc-700 shadow text-gray-500 dark:text-zinc-400 hover:bg-gray-100 dark:hover:bg-zinc-700 hover:text-gray-700 dark:hover:text-zinc-200 transition-colors"
@@ -500,11 +499,7 @@
</Transition>
<Transition name="scroll-fab">
<div
v-if="reactionPickerChatItem"
class="absolute inset-0 z-40"
@click.self="closeReactionPicker"
>
<div v-if="reactionPickerChatItem" class="absolute inset-0 z-40" @click.self="closeReactionPicker">
<div
ref="reactionPickerPanel"
class="absolute w-[min(24rem,calc(100%-1rem))] rounded-2xl overflow-hidden border border-gray-200 dark:border-zinc-700 shadow-2xl bg-white dark:bg-zinc-900"
@@ -515,7 +510,9 @@
@mousedown.prevent="onReactionPickerDragStart"
@touchstart.prevent="onReactionPickerDragStart"
>
<span class="text-xs font-medium text-gray-500 dark:text-zinc-400">{{ $t("messages.react") }}</span>
<span class="text-xs font-medium text-gray-500 dark:text-zinc-400">{{
$t("messages.react")
}}</span>
<button
type="button"
class="p-0.5 rounded hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-400 dark:text-zinc-500"
@@ -1744,7 +1741,7 @@ export default {
columbaReactionEmojis: COLUMBA_REACTION_EMOJIS,
reactionPickerChatItem: null,
reactionPickerPos: null,
_reactionDrag: null,
reactionDragState: null,
userStickers: [],
isStickerPickerOpen: false,
emojiStickerTab: "emoji",
@@ -1759,9 +1756,9 @@ export default {
windowWidth: typeof window !== "undefined" ? window.innerWidth : 1024,
peerHeaderCompact: false,
peerHeaderResizeObserver: null,
_scrollBottomGen: 0,
_prevScrollWantedLoadPrevious: false,
_initialLoadActive: false,
scrollBottomGen: 0,
prevScrollWantedLoadPrevious: false,
initialLoadActive: false,
messagesViewportReady: true,
};
},
@@ -2094,7 +2091,7 @@ export default {
this.saveDraft(oldPeer.destination_hash);
}
this.teardownPeerHeaderResizeObserver();
this._scrollBottomGen += 1;
this.scrollBottomGen += 1;
this.messagesViewportReady = false;
if (!newPeer) {
this.peerHeaderCompact = false;
@@ -2102,7 +2099,7 @@ export default {
this.checkIfSelectedPeerBlocked();
this.strangerBannerDismissed = false;
this.checkIfStrangerPeer();
this._prevScrollWantedLoadPrevious = false;
this.prevScrollWantedLoadPrevious = false;
this.initialLoad();
if (newPeer) {
this.loadDraft(newPeer.destination_hash);
@@ -2113,7 +2110,7 @@ export default {
},
useVirtualMessageList: {
handler(value) {
if (!value && !this._initialLoadActive) {
if (!value && !this.initialLoadActive) {
this.messagesViewportReady = true;
}
},
@@ -2182,7 +2179,7 @@ export default {
window.addEventListener("resize", this._onWindowResize);
},
beforeUnmount() {
this._scrollBottomGen += 1;
this.scrollBottomGen += 1;
this.teardownPeerHeaderResizeObserver();
if (this.selectedPeer) {
this.saveDraft(this.selectedPeer.destination_hash);
@@ -2455,13 +2452,13 @@ export default {
this.autoScrollOnNewMessage = isNearBottom(element);
const wantLoad = shouldLoadPreviousMessages(element);
if (wantLoad && !this._prevScrollWantedLoadPrevious) {
if (wantLoad && !this.prevScrollWantedLoadPrevious) {
this.loadPrevious();
}
this._prevScrollWantedLoadPrevious = wantLoad;
this.prevScrollWantedLoadPrevious = wantLoad;
},
async initialLoad() {
this._initialLoadActive = true;
this.initialLoadActive = true;
this.messagesViewportReady = false;
this.chatItems = [];
this.hasMorePrevious = true;
@@ -2469,7 +2466,7 @@ export default {
this.selectedPeerLxmfStampInfo = null;
this.selectedPeerSignalMetrics = null;
if (!this.selectedPeer) {
this._initialLoadActive = false;
this.initialLoadActive = false;
this.messagesViewportReady = true;
return;
}
@@ -2483,7 +2480,7 @@ export default {
await this.loadPrevious();
this._initialLoadActive = false;
this.initialLoadActive = false;
this.scrollMessagesToBottom();
this.autoLoadAudioAttachments();
@@ -2964,9 +2961,9 @@ export default {
);
},
scrollMessagesToBottom: function () {
this._scrollBottomGen += 1;
const gen = this._scrollBottomGen;
const stale = () => gen !== this._scrollBottomGen;
this.scrollBottomGen += 1;
const gen = this.scrollBottomGen;
const stale = () => gen !== this.scrollBottomGen;
this.$nextTick(() => {
if (stale()) return;
this.$nextTick(() => {
@@ -3007,7 +3004,6 @@ export default {
});
},
isLxmfMessageInUi: function (hash) {
return this.chatItems.findIndex((chatItem) => chatItem.lxmf_message?.hash === hash) !== -1;
},
@@ -3219,14 +3215,14 @@ export default {
closeReactionPicker() {
this.reactionPickerChatItem = null;
this.reactionPickerPos = null;
this._reactionDrag = null;
this.reactionDragState = null;
},
onReactionPickerDragStart(e) {
const evt = e.touches ? e.touches[0] : e;
const panel = this.$refs.reactionPickerPanel;
if (!panel) return;
const rect = panel.getBoundingClientRect();
this._reactionDrag = {
this.reactionDragState = {
startX: evt.clientX,
startY: evt.clientY,
originX: rect.left,
@@ -3234,11 +3230,11 @@ export default {
};
const onMove = (me) => {
const mv = me.touches ? me.touches[0] : me;
const dx = mv.clientX - this._reactionDrag.startX;
const dy = mv.clientY - this._reactionDrag.startY;
const dx = mv.clientX - this.reactionDragState.startX;
const dy = mv.clientY - this.reactionDragState.startY;
this.reactionPickerPos = {
x: this._reactionDrag.originX + dx,
y: this._reactionDrag.originY + dy,
x: this.reactionDragState.originX + dx,
y: this.reactionDragState.originY + dy,
};
};
const onUp = () => {
@@ -5246,7 +5242,9 @@ export default {
.scroll-fab-enter-active,
.scroll-fab-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
transition:
opacity 0.15s ease,
transform 0.15s ease;
}
.scroll-fab-enter-from,
.scroll-fab-leave-to {
@@ -887,6 +887,57 @@
</div>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("app.ui_transparency") }}
</div>
<div class="text-xs font-mono text-blue-500 dark:text-blue-400">
{{ Math.max(0, Math.min(100, Number(config.ui_transparency) || 0)) }}%
</div>
</div>
<div class="flex items-center gap-3">
<span class="text-xs text-gray-400">0</span>
<input
v-model.number="config.ui_transparency"
type="range"
min="0"
max="100"
step="1"
class="flex-1 h-1.5 bg-gray-200 dark:bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
@input="onUiTransparencyChange"
/>
<span class="text-xs text-gray-400">100</span>
</div>
<div class="text-xs text-gray-600 dark:text-gray-400">
{{ $t("app.ui_transparency_description") }}
</div>
</div>
<label class="setting-toggle">
<Toggle
id="ui-glass-enabled"
v-model="config.ui_glass_enabled"
@update:model-value="onUiGlassEnabledChange"
/>
<span class="setting-toggle__label">
<span class="setting-toggle__title">{{ $t("app.ui_glass_enabled") }}</span>
<span class="setting-toggle__description">{{
$t("app.ui_glass_enabled_description")
}}</span>
</span>
</label>
<div class="pt-1">
<button
type="button"
class="p-0 border-0 bg-transparent text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline cursor-pointer"
@click="resetAppearanceDefaults"
>
{{ $t("app.reset_appearance_defaults") }}
</button>
</div>
<div
class="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-300 border border-dashed border-gray-200 dark:border-zinc-800 rounded-2xl px-3 py-2"
>
@@ -1483,7 +1534,6 @@
<span class="setting-toggle__description">{{
$t("app.transport_toggle_description")
}}</span>
<span class="setting-toggle__hint">{{ $t("app.requires_restart") }}</span>
</span>
</label>
</div>
@@ -2008,8 +2058,6 @@
</div>
</section>
<!-- System / RNS Reload -->
<!--
<section class="settings-section break-inside-avoid">
<header class="settings-section__header">
<div>
@@ -2021,24 +2069,29 @@
<div class="settings-section__body space-y-4">
<div class="flex flex-col gap-3">
<button
class="btn btn--secondary w-full justify-center gap-2 py-3"
class="btn btn--secondary w-full justify-center py-3"
:disabled="reloadingRns"
@click="reloadRns"
>
<MaterialDesignIcon
:icon-name="reloadingRns ? 'refresh' : 'restart'"
class="w-5 h-5"
:class="{ 'animate-spin-reverse': reloadingRns }"
/>
<span>{{ reloadingRns ? $t("app.reloading_rns") : $t("app.reload_rns") }}</span>
<span>{{ $t("app.reload_rns") }}</span>
</button>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ $t("app.reload_rns_description") }}
</p>
<p
v-if="reloadRnsStatusMessage"
class="text-xs"
:class="
reloadingRns
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-500 dark:text-gray-400'
"
>
{{ reloadRnsStatusMessage }}
</p>
</div>
</div>
</section>
-->
</div>
<!-- Keyboard Shortcuts (Full width at bottom) -->
@@ -2149,6 +2202,8 @@ export default {
discovered_interfaces_max_return: 500,
message_font_size: 14,
message_icon_size: 28,
ui_transparency: 0,
ui_glass_enabled: true,
message_outbound_bubble_color: "#4f46e5",
message_inbound_bubble_color: null,
message_failed_bubble_color: "#ef4444",
@@ -2176,6 +2231,7 @@ export default {
lastRememberedInboundStampCost: 8,
shortcuts: [],
reloadingRns: false,
reloadRnsStatusMessage: "",
searchQuery: "",
trustedTelemetryPeers: [],
stickerCount: 0,
@@ -2280,6 +2336,9 @@ export default {
"app.theme",
"app.light_theme",
"app.dark_theme",
"app.ui_transparency",
"app.ui_glass_enabled",
"app.reset_appearance_defaults",
"Message Font Size",
"Icon Size",
"Message Bubbles",
@@ -2505,6 +2564,22 @@ export default {
this.shortcuts = json.shortcuts;
break;
}
case "reticulum_reload_status": {
const message = json.message || this.$t("app.reloading_rns");
this.reloadRnsStatusMessage = message;
this.reloadingRns = json.in_progress !== false;
const toastKey = "settings-rns-reload";
if (json.level === "error") {
ToastUtils.dismiss(toastKey);
ToastUtils.error(message, 7000);
} else if (json.level === "success") {
ToastUtils.dismiss(toastKey);
ToastUtils.success(message, 5000);
} else {
ToastUtils.info(message, 2500, toastKey);
}
break;
}
}
},
async getConfig() {
@@ -2660,6 +2735,53 @@ export default {
);
}, 1000);
},
onUiTransparencyChange() {
if (this.saveTimeouts.ui_transparency) clearTimeout(this.saveTimeouts.ui_transparency);
this.saveTimeouts.ui_transparency = setTimeout(async () => {
const n = Number(this.config.ui_transparency);
const v = Number.isFinite(n) ? Math.max(0, Math.min(100, Math.round(n))) : 0;
this.config.ui_transparency = v;
await this.updateConfig(
{
ui_transparency: v,
},
"ui_transparency"
);
}, 400);
},
async onUiGlassEnabledChange() {
await this.updateConfig(
{
ui_glass_enabled: this.config.ui_glass_enabled,
},
"ui_glass_enabled"
);
},
async resetAppearanceDefaults() {
this.config.theme = "light";
this.config.message_font_size = 14;
this.config.message_icon_size = 28;
this.config.ui_transparency = 0;
this.config.ui_glass_enabled = true;
this.config.message_outbound_bubble_color = "#4f46e5";
this.config.message_inbound_bubble_color = null;
this.config.message_failed_bubble_color = "#ef4444";
this.config.message_waiting_bubble_color = "#e5e7eb";
await this.updateConfig(
{
theme: "light",
message_font_size: 14,
message_icon_size: 28,
ui_transparency: 0,
ui_glass_enabled: true,
message_outbound_bubble_color: "#4f46e5",
message_inbound_bubble_color: null,
message_failed_bubble_color: "#ef4444",
message_waiting_bubble_color: "#e5e7eb",
},
"appearance"
);
},
async onMessageBubbleColorChange(type) {
const timeoutKey = `message_${type}_bubble_color`;
if (this.saveTimeouts[timeoutKey]) clearTimeout(this.saveTimeouts[timeoutKey]);
@@ -3090,13 +3212,19 @@ export default {
async onIsTransportEnabledChange() {
if (this.config.is_transport_enabled) {
try {
await window.api.post("/api/v1/reticulum/enable-transport");
const response = await window.api.post("/api/v1/reticulum/enable-transport");
if (response?.data?.message) {
ToastUtils.success(response.data.message);
}
} catch {
ToastUtils.error(this.$t("settings.failed_enable_transport"));
}
} else {
try {
await window.api.post("/api/v1/reticulum/disable-transport");
const response = await window.api.post("/api/v1/reticulum/disable-transport");
if (response?.data?.message) {
ToastUtils.success(response.data.message);
}
} catch {
ToastUtils.error(this.$t("settings.failed_disable_transport"));
}
@@ -3107,11 +3235,16 @@ export default {
try {
this.reloadingRns = true;
this.reloadRnsStatusMessage = this.$t("app.reloading_rns");
ToastUtils.loading(this.$t("app.reloading_rns"), 0, "settings-rns-reload");
const response = await window.api.post("/api/v1/reticulum/reload");
ToastUtils.success(response.data.message);
if (response?.data?.message) {
this.reloadRnsStatusMessage = response.data.message;
}
} catch {
ToastUtils.error(this.$t("settings.failed_reload_reticulum"));
} finally {
ToastUtils.dismiss("settings-rns-reload");
this.reloadingRns = false;
}
},
+12 -4
View File
@@ -26,6 +26,14 @@
@apply bg-sem-glass backdrop-blur border border-sem-border-card rounded-3xl shadow-xl px-4 py-4;
}
html[data-ui-glass="0"] .glass-card {
background-color: var(--mc-surface) !important;
border-color: var(--mc-border-card) !important;
@apply shadow-xl px-4 py-4;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
.input-field {
@apply bg-sem-surface-muted border border-sem-border text-sm rounded-2xl focus:ring-2 focus:ring-sem-focus focus:border-sem-focus-border block w-full p-2.5 text-sem-fg transition;
}
@@ -35,10 +43,10 @@ select.input-field option {
}
.primary-chip {
@apply inline-flex items-center justify-center gap-x-2 rounded-full bg-sem-accent px-4 py-2 text-xs font-bold text-white shadow-sm transition-all !important;
@apply inline-flex items-center justify-center gap-x-2 rounded-full bg-sem-action-primary px-4 py-2 text-xs font-bold text-white shadow-sm transition-all !important;
}
.primary-chip:hover {
@apply bg-sem-accent-hover -translate-y-0.5 !important;
@apply bg-sem-action-primary-hover -translate-y-0.5 !important;
}
.primary-chip:active {
@apply scale-95 translate-y-0 !important;
@@ -126,11 +134,11 @@ select.input-field option {
}
.file-input::file-selector-button {
@apply mr-2 border-0 rounded-md bg-sem-accent text-white px-3 py-1 text-xs font-semibold cursor-pointer hover:bg-sem-accent-hover transition;
@apply mr-2 border-0 rounded-md bg-sem-action-primary text-white px-3 py-1 text-xs font-semibold cursor-pointer hover:bg-sem-action-primary-hover transition;
}
.dark .file-input::file-selector-button {
@apply bg-sem-accent hover:bg-sem-accent-hover text-white;
@apply bg-sem-action-primary hover:bg-sem-action-primary-hover text-white;
}
.no-scrollbar::-webkit-scrollbar {
+16 -1
View File
@@ -6,9 +6,20 @@ const {
E2E_SCROLL_PEER_HASH,
} = require("./helpers");
async function waitForMessagesViewportReady(page) {
await page.waitForFunction(
() => {
const el = document.getElementById("messages");
return el != null && el.getAttribute("aria-busy") !== "true";
},
null,
{ timeout: 30000 }
);
}
async function scrollMetrics(page) {
await waitForMessagesViewportReady(page);
const loc = page.locator("#messages");
await expect(loc).toBeVisible({ timeout: 30000 });
return loc.evaluate((el) => ({
scrollTop: el.scrollTop,
scrollHeight: el.scrollHeight,
@@ -87,6 +98,7 @@ test.describe("Messages conversation scroll", () => {
.filter({ hasText: /E2E scroll seed/ })
.first()
.click();
await waitForMessagesViewportReady(page);
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
await expect(
page
@@ -112,6 +124,7 @@ test.describe("Messages conversation scroll", () => {
.filter({ hasText: /E2E scroll seed/ })
.first()
.click();
await waitForMessagesViewportReady(page);
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
await waitForMessagesOverflow(page);
@@ -163,6 +176,7 @@ test.describe("Messages conversation scroll", () => {
.filter({ hasText: /E2E scroll seed/ })
.first()
.click();
await waitForMessagesViewportReady(page);
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
await waitForMessagesOverflow(page);
@@ -205,6 +219,7 @@ test.describe("Messages conversation scroll", () => {
.filter({ hasText: /E2E scroll seed/ })
.first()
.click();
await waitForMessagesViewportReady(page);
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
await waitForMessagesOverflow(page);