- 输入侧:编辑消息不再静默销毁草稿(备份/回填)、补全浮层滚随、贴图 loading 态+并发上限复核、mention 失焦收口、富文本图文同粘、上传失败提示 - 消息流:图片点击 lightbox 放大、复制钮 focus 可见、复制为图片降级文案据实、JSON 块恢复可选、bucketSig 改哈希防长会话卡顿、user 长消息折叠、工具卡流式滚尾 - 会话框架:后台任务面板入点外/Esc 收起编排、恢复任务行级 busy+失败 toast、菜单底部翻转、搜索态统一清、归档禁改名对齐、listbox 语义+方向键、状态点 title、窄侧栏容器查询 - 跨端:删除目标不再抹其余目标完成态(前后端 GoalEntry 全量传递+失败回滚) - 边界条:MESSAGE_CAP 滚动淘汰不再静默,头部一键补齐完整会话,历史定位文案据实区分
405 lines
18 KiB
Vue
405 lines
18 KiB
Vue
<template>
|
|
<!-- ═══ 对话侧边栏(抽自 AiChat.vue 第一批,零行为变更;C4a 续拆:会话列表体下沉
|
|
conversation-sidebar/ConversationList.vue,本组件持壳层:开合/拖拽/头部/搜索框/就近 toast) ═══ -->
|
|
<transition name="sidebar-slide">
|
|
<div
|
|
v-if="store.state.sidebarOpen"
|
|
class="ai-conv-sidebar"
|
|
:style="{ '--sidebar-width': store.state.sidebarWidth + 'px' }"
|
|
>
|
|
<!-- 侧栏右边缘拖拽条(UX-2025-16):col-resize,拖动 mousemove 改 sidebarWidth,mouseup 持久化 -->
|
|
<div
|
|
class="ai-sidebar-resize-handle"
|
|
:title="t('aiChat.sidebarResizeHint')"
|
|
@mousedown.prevent="onResizeStart"
|
|
/>
|
|
<div class="ai-conv-sidebar-header">
|
|
<span class="ai-conv-sidebar-title">{{ t('aiChat.sidebarTitle') }}</span>
|
|
<button class="ai-btn-icon" @click="emit('confirm-new-conversation')" :title="t('aiChat.newConversation')" :aria-label="t('aiChat.newConversation')">
|
|
<svg aria-hidden="true" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
</button>
|
|
</div>
|
|
<!-- UX-06 §3.1: 对话搜索框(v-model 双绑 store.searchQuery;Ctrl+K 聚焦) -->
|
|
<div class="ai-conv-search">
|
|
<svg aria-hidden="true" class="ai-conv-search-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
<input
|
|
ref="searchInputRef"
|
|
v-model="store.state.searchQuery"
|
|
class="ai-conv-search-input"
|
|
:placeholder="t('aiChat.searchPlaceholder')"
|
|
:title="t('aiChat.searchShortcutHint')"
|
|
type="text"
|
|
@keydown="onSearchKeydown"
|
|
/>
|
|
<kbd class="ai-conv-search-kbd">{{ t('aiChat.searchShortcutHint') }}</kbd>
|
|
</div>
|
|
<!-- 脉络卡:同项目近期决策摘要,纯读侧,「带入上下文」追加进当前会话输入框 -->
|
|
<ContextBriefsCard />
|
|
<!-- 走查批E listbox 语义:会话项 role=option + aria-selected(列表体子件),
|
|
ArrowUp/Down 相邻移焦;分组折叠头为列表内辅助控件,不做 option 语义 -->
|
|
<div class="ai-conv-list" role="listbox" :aria-label="t('aiChat.sidebarTitle')">
|
|
<!-- C4a 拆分:会话列表体子件(搜索态平铺/时间分组/可恢复G5/归档/加载更多F4-1 +
|
|
项级重命名/更多操作菜单/切换过渡/标题淡入),待审批计数与可恢复清单经 props 下发,
|
|
删除确认/恢复动作经 emit 上抛 -->
|
|
<ConversationList
|
|
:pending-count-of="pendingCountOf"
|
|
:recoverable="recoverable"
|
|
:recovering-id="recoveringId"
|
|
@confirm-delete="emit('confirm-delete', $event)"
|
|
@recover="onRecover"
|
|
/>
|
|
</div>
|
|
<!-- UX走查 P2-7: 会话操作(rename/archive/pin)失败的就近反馈,定位侧栏顶部而非推进聊天流
|
|
(操作发起与视线焦点都在侧栏;且消息区被清空/切换后气泡可能不可见) -->
|
|
<Transition name="conv-toast">
|
|
<div v-if="convToast.visible" class="ai-conv-toast">{{ convToast.msg }}</div>
|
|
</Transition>
|
|
</div>
|
|
</transition>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
/**
|
|
* 对话侧边栏(壳层) — 抽自 AiChat.vue 第一批重构(零行为变更)。
|
|
*
|
|
* C4a 大文件拆分:会话列表体(搜索态平铺/时间分组/可恢复/归档/加载更多 + 项级重命名/
|
|
* 更多操作菜单/切换过渡/标题淡入)下沉 conversation-sidebar/ConversationList.vue,
|
|
* 本组件持壳层:开合过渡/宽度拖拽/头部新建/搜索框(Esc 清空+Ctrl+K 聚焦)/失败就近 toast。
|
|
*
|
|
* 设计:
|
|
* - store 共享(useAiStore 全局单例),子组件直接 useAiStore 读写 store.state/mutations,
|
|
* 与父组件 AiChat 等价(策略:store 共享则两边都用 useXxxStore,不走 props 传 store)。
|
|
* - 跨组件事件走 emit(ConversationList 上抛后本组件继续透传父 AiChat):
|
|
* - `confirm-delete`(id):删除需弹 ConfirmDialog,而 ConfirmDialog 由父渲染 +
|
|
* confirmDialog 实例归父所有(useConfirm 每次调用返回独立 ref,非全局单例),
|
|
* 故删除确认交父处理。父调原 confirmDeleteConversation(id)。
|
|
* - `confirm-new-conversation`:新建对话(F-09 决策e 直接新建,旧后台继续不中断),交父 confirmNewConversation。
|
|
* - 父 Ctrl+K 全局快捷键聚焦搜索框:经 defineExpose({ focusSearch }) 供父调,
|
|
* 父侧 ref 替代原 searchInputRef(零行为变更)。
|
|
*/
|
|
import { ref, computed, nextTick, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useAiStore } from '../../stores/ai'
|
|
import { aiApi } from '../../api/ai'
|
|
// V1 只读消费:跨会话待审批真相源(state.pendingApprovals 仅是活跃会话投影,后台会话挂起不进投影)
|
|
import { convPendingApprovals } from '../../composables/ai/aiShared'
|
|
// 无人值守边界徽标数据源:hydrateBoundaryBadges 补拉镜像(getConvBoundary 判 skip,
|
|
// fetchConvBoundary 补拉),徽标显隐消费在列表体子件(同源 reactive Map 两边读一致)
|
|
import { getConvBoundary, fetchConvBoundary } from '../../composables/ai/useTaskBoundary'
|
|
// UX走查 P2-7 只读订阅:rename/archive/pin 失败通知由 useAiConversations 发布,本组件弹就近 toast
|
|
// F4-1: 分页游标(reactive)+ 搜索拉满动作(「加载更多」按钮随列表体下沉,本组件只余「搜索即拉满」watch)
|
|
import { convOpFailNotice, convListPaging, loadAllConversations } from '../../composables/ai/useAiConversations'
|
|
import { useToast } from '../../composables/useToast'
|
|
import ConversationList from './conversation-sidebar/ConversationList.vue'
|
|
|
|
const emit = defineEmits<{
|
|
/** 删除对话(需二次确认),父调 confirmDeleteConversation(id) */
|
|
(e: 'confirm-delete', id: string): void
|
|
/** 新建对话(F-09 决策e 直接新建,旧后台继续不中断),父调 confirmNewConversation() */
|
|
(e: 'confirm-new-conversation'): void
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
const store = useAiStore()
|
|
|
|
// UX-06 §3.1: 对话搜索框引用(Ctrl+K 聚焦用)
|
|
const searchInputRef = ref<HTMLInputElement>()
|
|
|
|
/** 父 Ctrl+K 快捷键调用:聚焦搜索框(等价原 searchInputRef.value?.focus()) */
|
|
function focusSearch(): void {
|
|
nextTick(() => searchInputRef.value?.focus())
|
|
}
|
|
|
|
// UX走查 P3-1: 搜索框 Esc 清空(对齐 Tasks 搜索框行为)。同款 IME 守卫:组词中按 Esc 是
|
|
// 取消组词,keyup 阶段 isComposing 已复位拦不住,故挂 keydown
|
|
function onSearchKeydown(e: KeyboardEvent) {
|
|
if (e.isComposing || e.keyCode === 229) return
|
|
if (e.key === 'Escape') {
|
|
e.preventDefault()
|
|
store.state.searchQuery = ''
|
|
}
|
|
}
|
|
|
|
// UX走查 P2-7: rename/archive/pin 失败反馈改侧栏就近 toast(原以错误气泡推进聊天流,视线错位)。
|
|
// 每次失败发布新对象,watch 即重触发;useToast 自动重置计时与卸载清理
|
|
const { toast: convToast, showToast: showConvToast } = useToast()
|
|
watch(convOpFailNotice, (n) => {
|
|
if (n) showConvToast(t(`ai.${n.key}`, n.params ?? {}), 'error')
|
|
})
|
|
|
|
// ── V1: 跨会话待审批徽标(经 props.pendingCountOf 下发列表体) ──
|
|
// convPendingApprovals Map 本体非响应式(aiShared 注明渲染只依赖投影数组),故 computed 读 Map 的
|
|
// 同时锚定三类响应式源触发重算:
|
|
// - state.pendingApprovals(活跃会话投影,审批增删/切换恢复整体替换时必变)→ 活跃会话即时刷新;
|
|
// - conversations 列表(新建/删除/排序刷新);
|
|
// - pendingTick 2.5s 轮询兜底:后台会话新增/解除挂起不触碰任何响应式源(aiShared 只写裸 Map),
|
|
// 轻量周期重算补偿(徽标属弱实时信息,秒级延迟可接受)。
|
|
const pendingTick = ref(0)
|
|
let pendingPollTimer: ReturnType<typeof setInterval> | null = null
|
|
|
|
const pendingCountByConv = computed(() => {
|
|
void store.state.pendingApprovals.length
|
|
void store.state.conversations.length
|
|
void pendingTick.value
|
|
const out = new Map<string, number>()
|
|
for (const [convId, arr] of convPendingApprovals) {
|
|
if (arr.length > 0) out.set(convId, arr.length)
|
|
}
|
|
return out
|
|
})
|
|
/** 透传列表体的计数函数(子件模板内调用,函数体响应式读取由子件渲染效应追踪) */
|
|
function pendingCountOf(convId: string): number {
|
|
return pendingCountByConv.value.get(convId) ?? 0
|
|
}
|
|
|
|
// ── G5 可恢复分组(V49 断点续跑读侧:list_parked 桌面消费闭环最后一块) ──
|
|
// 数据源 ai_list_recoverable(loop_state='parked' 完备真相,不受侧栏 limit 截断),经 props 下发列表体;
|
|
// 恢复点击走 ai_recover_parked(注入恢复引导消息,重走 spawn 正典链路)后自然切换会话。
|
|
const recoverable = ref<{ id: string; title: string | null; model: string | null; updated_at: string }[]>([])
|
|
const recoveringId = ref<string | null>(null)
|
|
async function loadRecoverable() {
|
|
try {
|
|
recoverable.value = await aiApi.listRecoverable()
|
|
} catch (e) {
|
|
console.error('[G5] loadRecoverable failed', e)
|
|
}
|
|
}
|
|
// 走查批E:停靠收尾联动——后端 AiTaskParked(已保存现场并复位 generating)到达时刷新
|
|
// 可恢复清单,侧栏「可恢复」分组无需重启/重开侧栏即出现新停靠会话。事件经 ai-chat-event
|
|
// 广播通道分发(useAiEvents 主消费方之外,本处只筛 AiTaskParked,互不干扰)。
|
|
let _unlistenParked: (() => void) | null = null
|
|
/** 列表体恢复点击上抛:防重与 toast 反馈留本组件(与失败就近 toast 同层) */
|
|
async function onRecover(id: string) {
|
|
if (recoveringId.value) return
|
|
recoveringId.value = id
|
|
try {
|
|
await aiApi.recoverParked(id)
|
|
await store.switchConversation(id)
|
|
showConvToast(t('aiChat.recoverableDone'))
|
|
await loadRecoverable() // 恢复后 loop_state 已迁 running,刷新清单
|
|
} catch (e) {
|
|
console.error('[G5] recover failed', e)
|
|
showConvToast(t('aiChat.recoverableFail', { msg: String(e) }))
|
|
} finally {
|
|
recoveringId.value = null
|
|
}
|
|
}
|
|
|
|
// 搜索输入即拉满:搜索过滤(stores.filteredConversations)跑在已加载列表上,未拉满时
|
|
// 第 51 条起的会话搜不到(硬 50 截断=搜索失灵)——输入非空且还有未拉页时一次拉尽,
|
|
// 之后搜索自然覆盖全量;已拉满(hasMore=false)零开销不触发。
|
|
watch(() => store.state.searchQuery, (q) => {
|
|
if (q.trim() && convListPaging.hasMore && !convListPaging.loading) {
|
|
void loadAllConversations()
|
|
}
|
|
})
|
|
|
|
// 加载更多/搜索拉满追加的新会话补拉边界徽标镜像(A10 空洞随列表加深扩大;
|
|
// hydrate 对已入镜像的会话跳过,常规刷新时零 IPC)
|
|
watch(() => store.state.conversations.length, () => {
|
|
void hydrateBoundaryBadges()
|
|
})
|
|
|
|
// ── 侧栏宽度拖拽 ──
|
|
const resizeState = { startX: 0, startWidth: 0 }
|
|
function onResizeMove(e: MouseEvent) {
|
|
store.setSidebarWidth(resizeState.startWidth + (e.clientX - resizeState.startX))
|
|
}
|
|
function onResizeEnd() {
|
|
document.removeEventListener('mousemove', onResizeMove)
|
|
document.removeEventListener('mouseup', onResizeEnd)
|
|
document.body.classList.remove('ai-sidebar-resizing')
|
|
store.persistUiState()
|
|
}
|
|
function onResizeStart(e: MouseEvent) {
|
|
resizeState.startX = e.clientX
|
|
resizeState.startWidth = store.state.sidebarWidth
|
|
document.body.classList.add('ai-sidebar-resizing')
|
|
document.addEventListener('mousemove', onResizeMove)
|
|
document.addEventListener('mouseup', onResizeEnd)
|
|
}
|
|
|
|
onMounted(() => {
|
|
// G5 可恢复清单初载(parked 会话列表,断点续跑读侧)
|
|
void loadRecoverable()
|
|
// G5 走查批E:停靠收尾联动刷新(见 loadRecoverable 上方注)
|
|
void aiApi.onEvent((event) => {
|
|
if (event.type === 'AiTaskParked') void loadRecoverable()
|
|
}).then((unlisten) => { _unlistenParked = unlisten })
|
|
// V1:后台会话待审批变化兜底轮询(见 pendingCountByConv 注)
|
|
pendingPollTimer = setInterval(() => { pendingTick.value++ }, 2500)
|
|
// A10 边界盾标跨窗/重启空洞补拉:镜像 Map 是本窗口 JS context 内存态,分离窗口授权
|
|
// 或重启后(boundary_json 在库)侧栏盾标全消失。挂载时对列表会话补拉一轮填充镜像
|
|
// (fetch 静默降级:失败仅缺徽标不报错;已入镜像的会话跳过,不重复打 IPC)。
|
|
void hydrateBoundaryBadges()
|
|
})
|
|
|
|
/** A10:对侧栏全部会话补拉边界镜像(盾标徽标数据源)。
|
|
* 逐条并发 fire-and-forget;镜像已有条目跳过(配置卡/前次补拉已填,省 IPC)。
|
|
* 列表为空或全命中时零 IPC 开销。 */
|
|
async function hydrateBoundaryBadges(): Promise<void> {
|
|
const ids = store.state.conversations.map(c => c.id)
|
|
await Promise.allSettled(ids.map(id => {
|
|
if (getConvBoundary(id) !== null) return Promise.resolve()
|
|
return fetchConvBoundary(id)
|
|
}))
|
|
}
|
|
|
|
onBeforeUnmount(() => {
|
|
// 释放待审批徽标轮询计时器
|
|
if (pendingPollTimer) { clearInterval(pendingPollTimer); pendingPollTimer = null }
|
|
// 释放停靠事件监听(G5 走查批E)
|
|
if (_unlistenParked) { _unlistenParked(); _unlistenParked = null }
|
|
// 拖拽中途卸载兜底摘临时 document listener(原 AiChat 同段迁移)
|
|
document.removeEventListener('mousemove', onResizeMove)
|
|
document.removeEventListener('mouseup', onResizeEnd)
|
|
document.body.classList.remove('ai-sidebar-resizing')
|
|
})
|
|
|
|
defineExpose({
|
|
/** 聚焦搜索框(父 Ctrl+K 快捷键调,等价原 searchInputRef.value?.focus()) */
|
|
focusSearch,
|
|
})
|
|
</script>
|
|
|
|
<!-- ═══ ConversationSidebar 壳层 scoped 样式(第一批抽离遗漏修复: 从 AiChat.vue <style scoped> 迁移,
|
|
零视觉变更。原 AiChat scoped 不渗透子组件根,导致侧栏元素失配 → 本组件自持样式。
|
|
C4a 拆分:列表体样式(分组头/项/状态点/徽标/操作菜单/加载更多/空态)随子件迁
|
|
conversation-sidebar/ConversationList.vue <style scoped>,本组件只持壳层+搜索框+toast+开合过渡。
|
|
CSS 变量(--df-*)由根级 global.css 提供,共享同一份。.ai-btn-icon 共享按钮抽 components.css 全局。) ═══ -->
|
|
<style scoped>
|
|
/* ═══ 对话侧边栏 ═══ */
|
|
/* 宽度由 CSS 变量驱动(UX-2025-16 拖拽)::style 绑 --sidebar-width,不直接绑 width,
|
|
这样 sidebar-slide 过渡的 width:0(enter-from/leave-to)能正常覆盖,折叠/展开动画不破 */
|
|
.ai-conv-sidebar {
|
|
position: relative;
|
|
width: var(--sidebar-width, 200px);
|
|
min-width: var(--sidebar-width, 200px);
|
|
border-right: 0.5px solid var(--df-border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
background: var(--df-bg);
|
|
/* 走查批E:窄侧栏容器查询锚点(container-type: inline-size,对齐 Knowledge.vue 范式)。
|
|
列表体子件(ConversationList)以 @container (max-width: 160px) 消费:隐时间戳/缩徽标间距 */
|
|
container-type: inline-size;
|
|
}
|
|
/* 右边缘 2px 拖拽条。hover/active 显高亮;拖拽中 body.ai-sidebar-resizing 全局锁 cursor 防抖 */
|
|
.ai-sidebar-resize-handle {
|
|
position: absolute;
|
|
top: 0;
|
|
right: -1px; /* 跨过 0.5px border-right,确保命中区不被 border 遮挡 */
|
|
width: 4px; /* 命中区 4px(视觉 2px 由 background 控制),易抓取 */
|
|
height: 100%;
|
|
cursor: col-resize;
|
|
z-index: 5;
|
|
background: transparent;
|
|
transition: background 0.15s var(--df-ease);
|
|
}
|
|
.ai-sidebar-resize-handle:hover {
|
|
background: var(--df-accent);
|
|
opacity: 0.5;
|
|
}
|
|
/* 拖拽中全局锁 cursor + 禁选中文本(挂在 body 上,跨元素生效) */
|
|
body.ai-sidebar-resizing,
|
|
body.ai-sidebar-resizing * {
|
|
cursor: col-resize !important;
|
|
user-select: none !important;
|
|
}
|
|
.ai-conv-sidebar-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 10px;
|
|
border-bottom: 0.5px solid var(--df-border);
|
|
}
|
|
.ai-conv-sidebar-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--df-text);
|
|
}
|
|
|
|
/* UX-06 §3.1: 对话搜索框(wave2 u-agent .sb-search 范式:容器成框,卡底+focus 边框 info) */
|
|
.ai-conv-search {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
margin: 8px 10px;
|
|
padding: 0 8px;
|
|
height: 30px;
|
|
background: var(--df-bg-card);
|
|
border: 0.5px solid var(--df-border);
|
|
border-radius: var(--df-radius-sm);
|
|
transition: border-color 0.15s var(--df-ease);
|
|
}
|
|
.ai-conv-search:focus-within {
|
|
border-color: var(--df-info);
|
|
}
|
|
.ai-conv-search-icon {
|
|
flex: none;
|
|
color: var(--df-text-dim);
|
|
margin-right: 6px;
|
|
}
|
|
.ai-conv-search-input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
border: none;
|
|
outline: none;
|
|
background: transparent;
|
|
font-size: 12px;
|
|
color: var(--df-text);
|
|
}
|
|
/* 聚焦可见环:容器已随 :focus-within 变 info 边框,键盘环同色补强(scoped 特异性须盖全局兜底) */
|
|
.ai-conv-search-input:focus-visible { outline: 2px solid var(--df-info); outline-offset: 1px; }
|
|
.ai-conv-search-input::placeholder {
|
|
color: var(--df-text-dim);
|
|
}
|
|
.ai-conv-search-kbd {
|
|
flex: none;
|
|
font-family: inherit;
|
|
font-size: 10px;
|
|
color: var(--df-text-dim);
|
|
border: 0.5px solid var(--df-border);
|
|
border-radius: 4px;
|
|
padding: 0 4px;
|
|
user-select: none;
|
|
}
|
|
|
|
.ai-conv-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 4px 6px; /* u-agent .sb-list 范式;滚动条走 global ::-webkit-scrollbar token 体系 */
|
|
}
|
|
|
|
/* UX走查 P2-7: 会话操作失败就近 toast(仅 error 一型)。视觉对齐 AiChat .ai-toast,
|
|
但 AiChat 版是 scoped 样式渗透不进来,故自持一份;侧栏窄,max-width 收 85% */
|
|
.ai-conv-toast {
|
|
position: absolute;
|
|
top: 16px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
padding: 8px 14px;
|
|
border-radius: var(--df-radius);
|
|
font-size: 12px;
|
|
z-index: 30; /* 高于操作菜单(20),菜单展开时失败反馈不被盖 */
|
|
max-width: 85%;
|
|
overflow-wrap: anywhere;
|
|
box-shadow: var(--df-shadow-sm);
|
|
background: var(--df-danger-bg);
|
|
color: var(--df-danger);
|
|
border: 0.5px solid var(--df-danger);
|
|
}
|
|
.conv-toast-enter-active, .conv-toast-leave-active { transition: opacity 0.2s, transform 0.2s; }
|
|
.conv-toast-enter-from, .conv-toast-leave-to { opacity: 0; transform: translate(-50%, -8px); }
|
|
|
|
/* 侧边栏滑入动画 */
|
|
.sidebar-slide-enter-active { transition: width 0.2s var(--df-ease), opacity 0.15s; }
|
|
.sidebar-slide-leave-active { transition: width 0.15s var(--df-ease), opacity 0.1s; }
|
|
.sidebar-slide-enter-from, .sidebar-slide-leave-to {
|
|
width: 0;
|
|
min-width: 0;
|
|
opacity: 0;
|
|
overflow: hidden;
|
|
}
|
|
</style>
|