This commit is contained in:
Your Name
2026-08-03 10:00:05 +08:00
parent 0fb03d0bca
commit 01729b1e0b
109 changed files with 7577 additions and 1153 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+142 -107
View File
@@ -5,10 +5,10 @@
<span class="brand-badge">AI</span>
<div class="brand-copy">
<strong>{{ settings.siteName || 'AI Chat' }}</strong>
<p>统一文本与图片创作</p>
<p>TEXT · IMAGE · AGENT</p>
</div>
<button class="sidebar-close" type="button" aria-label="关闭会话列表" @click="chat.closeSidebar">
<IconX :size="19" :stroke-width="1.8" />
<IconX :size="18" :stroke-width="1.8" />
</button>
</div>
@@ -21,7 +21,7 @@
<div class="conversation-list">
<div class="conversation-heading">
<span>最近对话</span>
<span>{{ chat.conversations.length }}</span>
<span>{{ chat.conversations.length.toString().padStart(2, '0') }}</span>
</div>
<div
@@ -32,7 +32,9 @@
:aria-current="conv.id === chat.currentId ? 'page' : undefined"
@click="selectConv(conv.id)"
>
<IconMessageCircle class="conversation-icon" :size="17" :stroke-width="1.7" />
<span class="conversation-icon-wrap">
<IconMessageCircle class="conversation-icon" :size="16" :stroke-width="1.7" />
</span>
<span class="conv-title">{{ conv.title }}</span>
<button
class="delete-btn"
@@ -40,11 +42,11 @@
:aria-label="`删除对话:${conv.title}`"
@click.stop="handleDelete(conv.id)"
>
<IconTrash :size="15" :stroke-width="1.8" />
<IconTrash :size="14" :stroke-width="1.8" />
</button>
</div>
<p v-if="!chat.conversations.length" class="empty-tip">还没有会话先创建一个新对话</p>
<p v-if="!chat.conversations.length" class="empty-tip">还没有会话<br />点击上方按钮开始创作</p>
</div>
<div class="sidebar-footer">
@@ -54,6 +56,7 @@
<span class="user-name">{{ auth.user?.nickname || auth.user?.username }}</span>
<span class="user-level">{{ auth.isGuest ? '游客模式 · 自动保存' : (auth.user?.membership_name || '普通用户') }}</span>
</div>
<span class="user-online" aria-hidden="true" />
</div>
</div>
</aside>
@@ -122,45 +125,62 @@ async function handleDelete(id) {
<style scoped>
.sidebar {
width: var(--sidebar-width);
background: rgba(255, 255, 255, 0.94);
border-right: 1px solid var(--border);
position: relative;
display: flex;
width: var(--sidebar-width);
flex: 0 0 var(--sidebar-width);
flex-direction: column;
flex-shrink: 0;
transition: transform 0.24s cubic-bezier(0.16, 1, 0.3, 1);
backdrop-filter: blur(14px);
overflow: hidden;
border-right: 1px solid var(--border);
background:
radial-gradient(circle at 40% -12%, rgba(183, 243, 107, 0.075), transparent 24%),
rgba(13, 16, 21, 0.96);
box-shadow: 18px 0 50px rgba(0, 0, 0, 0.16);
backdrop-filter: blur(20px);
transition: transform 260ms var(--ease-spring);
}
.sidebar::after {
position: absolute;
top: 0;
right: -1px;
width: 1px;
height: 24%;
background: linear-gradient(to bottom, var(--accent), transparent);
box-shadow: 0 0 16px rgba(183, 243, 107, 0.42);
content: "";
}
.sidebar-top {
padding: 16px 14px 12px;
display: flex;
flex-direction: column;
gap: 14px;
gap: 17px;
padding: 18px 14px 13px;
}
.sidebar-brand {
display: flex;
min-height: 42px;
align-items: center;
gap: 10px;
min-height: 40px;
padding: 2px;
gap: 11px;
padding: 1px 2px;
}
.brand-badge {
display: grid;
width: 38px;
height: 38px;
flex: 0 0 auto;
place-items: center;
border: 1px solid rgba(183, 243, 107, 0.5);
border-radius: 12px;
background: var(--accent-soft);
border: 1px solid #d9e6fb;
color: var(--accent);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 700;
letter-spacing: -0.02em;
flex-shrink: 0;
background: var(--accent);
color: #11150e;
font-family: "Cascadia Code", Consolas, monospace;
font-size: 12px;
font-weight: 800;
letter-spacing: -0.04em;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 4px 0 #55782f, 0 10px 22px rgba(110, 166, 54, 0.16);
}
.brand-copy {
@@ -173,17 +193,19 @@ async function handleDelete(id) {
overflow: hidden;
color: var(--text-primary);
font-size: 14px;
font-weight: 650;
font-weight: 690;
letter-spacing: -0.02em;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-brand p {
margin-top: 2px;
margin-top: 3px;
color: var(--text-muted);
font-size: 11px;
line-height: 1.3;
font-family: "Cascadia Code", Consolas, monospace;
font-size: 8px;
letter-spacing: 0.1em;
}
.sidebar-close {
@@ -192,81 +214,99 @@ async function handleDelete(id) {
height: 34px;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text-secondary);
}
.new-chat-btn {
display: flex;
width: 100%;
min-height: 43px;
align-items: center;
justify-content: center;
gap: 7px;
width: 100%;
min-height: 42px;
padding: 0 14px;
background: var(--accent);
border: 1px solid rgba(183, 243, 107, 0.62);
border-radius: 12px;
color: #fff;
background: linear-gradient(180deg, var(--accent-hover), var(--accent));
color: #11150e;
font-size: 13px;
font-weight: 600;
box-shadow: 0 7px 18px rgba(45, 102, 218, 0.2);
transition: background 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
font-weight: 720;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.48), 0 5px 0 #577c2f, 0 13px 24px rgba(126, 183, 67, 0.13);
transition: transform 180ms var(--ease-spring), background 160ms ease, box-shadow 180ms ease;
}
.new-chat-btn:hover {
background: var(--accent-hover);
box-shadow: 0 9px 22px rgba(45, 102, 218, 0.24);
background: linear-gradient(180deg, #d1ff9c, var(--accent-hover));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.52), 0 6px 0 #5f8538, 0 15px 28px rgba(126, 183, 67, 0.18);
transform: translateY(-1px);
}
.new-chat-btn:active {
transform: scale(0.98);
transform: translateY(4px) scale(0.99);
box-shadow: inset 0 2px 5px rgba(68, 96, 37, 0.22), 0 1px 0 #577c2f;
}
.conversation-list {
flex: 1;
padding: 5px 10px 14px;
overflow-y: auto;
padding: 6px 9px 12px;
}
.conversation-heading {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 9px 7px;
padding: 10px 9px 8px;
color: var(--text-muted);
font-size: 11px;
font-family: "Cascadia Code", Consolas, monospace;
font-size: 9px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.conversation-item {
position: relative;
display: flex;
min-height: 43px;
align-items: center;
gap: 9px;
min-height: 42px;
margin-bottom: 2px;
padding: 8px 8px 8px 10px;
border-radius: 10px;
cursor: pointer;
margin-bottom: 3px;
padding: 7px 8px;
border: 1px solid transparent;
border-radius: 11px;
color: var(--text-secondary);
font-size: 13px;
transition: background 0.15s ease, color 0.15s ease;
cursor: pointer;
font-size: 12px;
transition: transform 170ms var(--ease-spring), border-color 160ms ease, background 160ms ease, color 160ms ease;
}
.conversation-item:hover {
background: var(--bg-tertiary);
border-color: var(--border);
background: rgba(255, 255, 255, 0.035);
color: var(--text-primary);
transform: translateX(2px);
}
.conversation-item.active {
background: var(--accent-soft);
box-shadow: inset 3px 0 0 var(--accent);
color: var(--text-primary);
font-weight: 500;
border-color: rgba(183, 243, 107, 0.25);
background: linear-gradient(90deg, rgba(183, 243, 107, 0.13), rgba(183, 243, 107, 0.04));
color: var(--accent);
}
.conversation-icon-wrap {
display: grid;
width: 28px;
height: 28px;
flex: 0 0 auto;
place-items: center;
border-radius: 8px;
background: rgba(255, 255, 255, 0.035);
}
.conversation-icon {
color: var(--text-muted);
flex-shrink: 0;
}
.conversation-item.active .conversation-icon {
@@ -281,16 +321,15 @@ async function handleDelete(id) {
}
.delete-btn {
display: grid;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
flex: 0 0 auto;
place-items: center;
border-radius: 8px;
color: var(--text-muted);
opacity: 0;
flex-shrink: 0;
transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
transition: opacity 150ms ease, background 150ms ease, color 150ms ease, transform 150ms ease;
}
.conversation-item:hover .delete-btn,
@@ -299,63 +338,78 @@ async function handleDelete(id) {
}
.delete-btn:hover {
background: #fff0f0;
background: var(--danger-soft);
color: var(--danger);
transform: rotate(3deg);
}
.empty-tip {
padding: 26px 18px;
margin: 18px 8px;
padding: 25px 16px;
border: 1px dashed var(--border-strong);
border-radius: 13px;
color: var(--text-muted);
font-size: 12px;
line-height: 1.6;
font-size: 11px;
line-height: 1.7;
text-align: center;
}
.sidebar-footer {
padding: 10px 12px 12px;
padding: 11px 13px 13px;
border-top: 1px solid var(--border);
background: rgba(0, 0, 0, 0.14);
}
.user-info {
position: relative;
display: flex;
align-items: center;
gap: 10px;
padding: 6px;
padding: 6px 4px;
}
.avatar {
display: grid;
width: 34px;
height: 34px;
border-radius: 11px;
background: #eef1f6;
color: var(--text-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 650;
flex-shrink: 0;
flex: 0 0 auto;
place-items: center;
border: 1px solid rgba(183, 243, 107, 0.28);
border-radius: 10px;
background: var(--accent-soft);
color: var(--accent);
font-size: 12px;
font-weight: 750;
}
.user-meta {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
flex-direction: column;
}
.user-name {
overflow: hidden;
color: var(--text-primary);
font-size: 13px;
font-weight: 550;
font-size: 12px;
font-weight: 650;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-level {
margin-top: 2px;
margin-top: 3px;
color: var(--text-muted);
font-size: 11px;
font-size: 10px;
}
.user-online {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 11px rgba(183, 243, 107, 0.58);
}
@media (max-width: 768px) {
@@ -363,30 +417,11 @@ async function handleDelete(id) {
position: fixed;
inset: 0 auto 0 0;
z-index: 100;
width: min(84vw, 304px);
width: min(86vw, 310px);
transform: translateX(-100%);
box-shadow: 20px 0 50px rgba(21, 32, 51, 0.14);
}
.sidebar.open {
transform: translateX(0);
}
.sidebar-close {
display: flex;
}
.delete-btn {
opacity: 0.68;
}
}
@media (prefers-reduced-motion: reduce) {
.sidebar,
.new-chat-btn,
.conversation-item,
.delete-btn {
transition: none;
}
.sidebar.open { transform: translateX(0); }
.sidebar-close { display: flex; }
.delete-btn { opacity: 0.66; }
}
</style>
@@ -1082,4 +1082,36 @@ function zoomByWheel(event) { zoom.value = Math.min(3, Math.max(0.25, zoom.value
@media(max-width:1100px){.ratio-options button{padding:0 7px}.ratio-options button>i{display:none}.workflow-panel{width:calc(100% - 24px)}.primary-workflow-button{min-width:96px;padding:0 12px}}
@media(max-width:800px){.workflow-panel{position:absolute;right:8px;bottom:calc(46dvh + 18px);left:8px;z-index:3;width:auto;margin:0}.outpaint-panel{overflow-x:auto;justify-content:flex-start}.outpaint-frame{width:min(94vw,calc(38dvh * var(--outpaint-ratio)));height:min(38dvh,520px);max-width:94vw}.restore-overlay{padding-bottom:16px}.restore-progress{min-height:46px;padding:0 12px;font-size:12px}}
@media(prefers-reduced-motion:reduce){.workflow-panel button:active,.tool-bar button:active{transform:none}}
/* Uiverse-inspired image workbench */
.workbench{background:#090b0f;color:var(--text-primary)}
.workbench-sidebar,.tool-bar,.canvas-footer{border-color:var(--border);background:#101319;color:var(--text-primary)}
.sidebar-header,.ops-panel,.command-log{border-color:var(--border)}
.sidebar-header span,.composer-footer span,.ops-title,.tool-note p,.outpaint-side-note p,.command-log strong{color:var(--text-muted)}
.icon-button,.tool-bar>button:not(.icon-button),.canvas-footer button,.selection-tools>button,.ratio-options button{color:var(--text-secondary)}
.icon-button:hover,.tool-bar>button:hover,.tool-bar>button.active,.canvas-footer button:hover,.selection-tools>button:hover,.selection-tools>button.active,.ratio-options button:hover,.ratio-options button.active{background:var(--accent-soft);color:var(--accent)}
.result-strip button img{border-color:var(--border);background:var(--bg-tertiary);box-shadow:0 10px 26px rgba(0,0,0,.28)}
.result-strip button.active img{border-color:var(--accent);box-shadow:0 0 0 3px rgba(183,243,107,.08),0 12px 28px rgba(0,0,0,.34)}
.tool-chip{border:1px solid rgba(183,243,107,.2);background:var(--accent-soft);color:var(--accent)}
.clear-mask,.command-tool,.status,.panel-status{color:var(--accent)}
.outpaint-side-note,.command-log li{border-color:var(--border);background:var(--surface-inset)}
.outpaint-side-note span{color:var(--accent)}
.command-text{color:var(--text-secondary)}
.suggestions button{border-color:var(--border);background:rgba(255,255,255,.02);color:var(--text-secondary)}
.suggestions button:hover{border-color:var(--accent-line);background:var(--accent-soft);color:var(--accent)}
.composer,.workflow-panel{border-color:var(--border-strong);background:var(--bg-secondary);box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 16px 40px rgba(0,0,0,.34)}
.composer textarea,.workflow-command input{color:var(--text-primary)}
.composer textarea::placeholder,.workflow-command input::placeholder{color:var(--text-muted)}
.composer-footer button,.workflow-command>button,.primary-workflow-button{border:1px solid rgba(183,243,107,.54);background:linear-gradient(180deg,var(--accent-hover),var(--accent));color:#11150e;box-shadow:inset 0 1px 0 rgba(255,255,255,.45),0 4px 0 #577c2f}
.composer-footer button:active,.workflow-command>button:active,.primary-workflow-button:active{transform:translateY(3px);box-shadow:0 1px 0 #577c2f}
.workflow-command>svg{color:var(--accent)}
.selection-tools,.panel-divider,.selection-tools>i{border-color:var(--border);background-color:var(--border)}
.background-toggle{color:var(--text-secondary)}
.canvas-shell{background-color:#0b0e12;background-image:radial-gradient(rgba(255,255,255,.05) .8px,transparent .8px);background-size:18px 18px}
.canvas-stage{background-color:#11151a;background-image:linear-gradient(45deg,#191e25 25%,transparent 25%),linear-gradient(-45deg,#191e25 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#191e25 75%),linear-gradient(-45deg,transparent 75%,#191e25 75%)}
.canvas-stage img{box-shadow:0 24px 64px rgba(0,0,0,.5)}
.outpaint-frame{border-color:var(--border-strong);background-color:#0e1217;background-image:radial-gradient(circle,#2b332d 1px,transparent 1.1px);box-shadow:inset 0 0 0 1px rgba(255,255,255,.025)}
.restore-progress{border-color:var(--border-strong);background:rgba(16,19,25,.96);color:var(--text-primary);box-shadow:0 18px 48px rgba(0,0,0,.48)}
.restore-progress>svg,.restore-progress button{color:var(--accent)}
@media(max-width:800px){.workbench-sidebar{border-color:var(--border-strong);background:var(--bg-secondary);box-shadow:0 22px 60px rgba(0,0,0,.5)}}
</style>
+90
View File
@@ -795,4 +795,94 @@ async function copyContent() {
animation: none;
}
}
/* Uiverse-inspired message surfaces */
.message {
margin-bottom: 32px;
}
.message-avatar {
border-color: var(--border-strong);
background: var(--surface-raised);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045), 0 7px 18px rgba(0, 0, 0, 0.22);
}
.message.user .message-avatar {
border-color: var(--border-strong);
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.message.assistant .message-avatar {
border-color: rgba(183, 243, 107, 0.28);
background: var(--accent-soft);
color: var(--accent);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 0 20px rgba(183, 243, 107, 0.06);
}
.message.user .message-content {
border-color: rgba(183, 243, 107, 0.23);
background:
linear-gradient(145deg, rgba(183, 243, 107, 0.12), rgba(183, 243, 107, 0.055)),
var(--bg-secondary);
color: var(--text-primary);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 10px 28px rgba(0, 0, 0, 0.18);
}
.message.assistant .message-content {
color: var(--text-primary);
}
.message-action-btn {
border: 1px solid transparent;
}
.message-action-btn:hover,
.message-action-btn:focus-visible {
border-color: var(--border);
background: var(--bg-tertiary);
color: var(--accent);
}
.message-content :deep(pre) {
border-color: var(--border);
background: #080a0d;
}
.message.user .message-content :deep(pre),
.message.user .message-content :deep(:not(pre) > code) {
background: rgba(4, 7, 5, 0.46);
}
.image-loading-grid i {
background: linear-gradient(112deg, #11151a 8%, #20262c 38%, #151b1c 64%, #11151a 92%);
background-size: 240% 100%;
}
.generation-progress {
border-color: rgba(183, 243, 107, 0.24);
background: rgba(13, 17, 21, 0.9);
color: var(--accent);
box-shadow: 0 7px 20px rgba(0, 0, 0, 0.26);
backdrop-filter: blur(10px);
}
.att-image,
.att-video {
border-color: var(--border-strong);
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.32);
}
.att-document {
border-color: var(--border);
background: var(--bg-secondary);
color: var(--text-primary);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 8px 22px rgba(0, 0, 0, 0.2);
}
.att-document:hover {
border-color: var(--accent-line);
background: var(--bg-tertiary);
color: var(--accent);
}
</style>
+97
View File
@@ -547,4 +547,101 @@ watch(
animation: none;
}
}
/* Uiverse-inspired dark component treatment */
.message-list {
padding-top: 32px;
background: transparent;
}
.scroll-bottom-btn {
border-color: var(--border-strong);
background: rgba(18, 22, 28, 0.92);
color: var(--text-secondary);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045), 0 12px 34px rgba(0, 0, 0, 0.36);
}
.scroll-bottom-btn:hover {
border-color: var(--accent-line);
background: #171c22;
color: var(--accent);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.055), 0 14px 38px rgba(0, 0, 0, 0.42);
}
.welcome-panel {
position: relative;
width: min(100%, 680px);
padding: 34px 26px 40px;
}
.welcome-panel::before {
display: block;
width: 46px;
height: 1px;
margin: 0 auto 22px;
background: var(--accent);
box-shadow: 0 0 14px rgba(183, 243, 107, 0.48);
content: "";
}
.welcome-icon {
width: 56px;
height: 56px;
margin-bottom: 23px;
border-color: rgba(183, 243, 107, 0.5);
border-radius: 15px;
background: var(--accent);
color: #11150e;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 5px 0 #577c2f, 0 15px 30px rgba(110, 166, 54, 0.17);
}
.welcome h2 {
max-width: 590px;
margin-right: auto;
margin-bottom: 13px;
margin-left: auto;
color: var(--text-primary);
font-size: clamp(27px, 3vw, 38px);
font-weight: 760;
letter-spacing: -0.055em;
line-height: 1.16;
text-wrap: balance;
}
.welcome p {
max-width: 500px;
color: var(--text-secondary);
line-height: 1.75;
}
.loading-state span {
border: 1px solid var(--border);
background: linear-gradient(100deg, #15191f 15%, #20262e 38%, #15191f 62%);
background-size: 220% 100%;
}
.typing-indicator {
width: min(100%, var(--input-max-width));
padding-left: 51px;
}
.typing-indicator span {
background: var(--accent);
box-shadow: 0 0 9px rgba(183, 243, 107, 0.22);
}
.image-preview-overlay {
background: rgba(4, 5, 7, 0.9);
}
.image-preview-close {
border-color: var(--border-strong);
background: rgba(18, 22, 28, 0.9);
}
@media (max-width: 768px) {
.message-list { padding-top: 20px; }
.welcome-panel { padding: 22px 8px 30px; }
.welcome h2 { font-size: 25px; }
}
</style>
@@ -289,4 +289,47 @@ async function copyDetail(item) {
transition: none;
}
}
/* Uiverse-inspired notification surfaces */
.notification-card {
border-color: var(--border-strong);
background:
radial-gradient(circle at 100% 0%, rgba(183, 243, 107, 0.05), transparent 32%),
rgba(16, 19, 25, 0.98);
color: var(--text-primary);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045), 0 20px 56px rgba(0, 0, 0, 0.46);
}
.notification-card::before {
background: var(--accent);
box-shadow: 0 0 12px rgba(183, 243, 107, 0.44);
}
.notification-icon {
border: 1px solid rgba(255, 126, 121, 0.22);
background: var(--danger-soft);
color: var(--danger);
}
.notification-close,
.copy-detail {
border-color: var(--border);
background: rgba(255, 255, 255, 0.025);
color: var(--text-secondary);
}
.notification-close:hover,
.copy-detail:hover {
border-color: var(--accent-line);
background: var(--accent-soft);
color: var(--accent);
}
.notification-detail,
.detail-body,
.detail-body pre {
border-color: var(--border);
background: var(--surface-inset);
color: var(--text-secondary);
}
</style>
+70
View File
@@ -0,0 +1,70 @@
<template>
<div class="theme-toggle" role="group" aria-label="界面主题">
<button
type="button"
:class="{ active: theme === 'light' }"
:aria-pressed="theme === 'light'"
aria-label="使用浅色主题"
title="浅色主题"
@click="setTheme('light')"
>
<span>浅色</span>
</button>
<button
type="button"
:class="{ active: theme === 'dark' }"
:aria-pressed="theme === 'dark'"
aria-label="使用深色主题"
title="深色主题"
@click="setTheme('dark')"
>
<span>深色</span>
</button>
</div>
</template>
<script setup>
import { useTheme } from '@/composables/useTheme'
const { theme, setTheme } = useTheme()
</script>
<style scoped>
.theme-toggle {
display: inline-grid;
grid-template-columns: repeat(2, 42px);
gap: 0;
padding: 2px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg-tertiary);
}
button {
display: grid;
width: 42px;
height: 30px;
place-items: center;
border: 1px solid transparent;
border-radius: 4px;
color: var(--text-muted);
font-size: 11px;
font-weight: 600;
transition: color 160ms ease, background 160ms ease, transform 160ms ease;
}
button:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
button:active {
transform: translateY(1px) scale(0.96);
}
button.active {
border-color: transparent;
background: var(--button-primary-bg);
color: var(--button-primary-text);
}
</style>
+56
View File
@@ -0,0 +1,56 @@
import { computed, ref } from 'vue'
const STORAGE_KEY = 'ai-chat-theme'
const DEFAULT_THEME = 'light'
function readStoredTheme() {
try {
return localStorage.getItem(STORAGE_KEY) === 'dark' ? 'dark' : DEFAULT_THEME
} catch {
return DEFAULT_THEME
}
}
const theme = ref(readStoredTheme())
let transitionTimer
function applyTheme(value, animate = false) {
const nextTheme = value === 'dark' ? 'dark' : 'light'
const root = document.documentElement
if (animate) {
root.classList.add('theme-transitioning')
window.clearTimeout(transitionTimer)
transitionTimer = window.setTimeout(() => root.classList.remove('theme-transitioning'), 260)
}
root.dataset.theme = nextTheme
root.style.colorScheme = nextTheme
theme.value = nextTheme
}
export function initializeTheme() {
applyTheme(theme.value)
}
export function useTheme() {
const isDark = computed(() => theme.value === 'dark')
function setTheme(value) {
const nextTheme = value === 'dark' ? 'dark' : 'light'
applyTheme(nextTheme, true)
try {
localStorage.setItem(STORAGE_KEY, nextTheme)
} catch {
// Theme still works for the current session when storage is unavailable.
}
}
return { theme, isDark, setTheme }
}
if (typeof window !== 'undefined') {
window.addEventListener('storage', event => {
if (event.key === STORAGE_KEY) applyTheme(event.newValue)
})
}
+3
View File
@@ -2,8 +2,11 @@ import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import { initializeTheme } from './composables/useTheme'
import './assets/main.css'
initializeTheme()
const app = createApp(App)
app.use(createPinia())
app.use(router)
+6
View File
@@ -258,6 +258,7 @@ export const useChatStore = defineStore('chat', () => {
attachments,
agent_id: agentId || null,
image_tool: options.imageTool || null,
voice_mode: !!options.voiceMode,
stream: true
})
})
@@ -288,6 +289,11 @@ export const useChatStore = defineStore('chat', () => {
const appendVisibleContent = (content) => {
if (!content) return
receivedContent += content
try {
options.onTextDelta?.(content)
} catch {
// 展示层或语音层回调失败不应中断主消息流。
}
if (!isAgentStream) {
fullContent += content
+6 -1
View File
@@ -17,6 +17,10 @@ export const useSettingsStore = defineStore('settings', () => {
})
const siteName = ref('AI Chat')
const allowRegister = ref(true)
const voicePersona = ref({
name: 'AI 客服',
greeting: '您好,请问有什么可以帮您?'
})
const models = ref([])
const agents = ref([])
const loaded = ref(false)
@@ -27,6 +31,7 @@ export const useSettingsStore = defineStore('settings', () => {
features.value = data.features
siteName.value = data.site_name
allowRegister.value = data.allow_register
voicePersona.value = data.voice_persona || voicePersona.value
loaded.value = true
}
@@ -40,5 +45,5 @@ export const useSettingsStore = defineStore('settings', () => {
agents.value = res.data.data || []
}
return { features, siteName, allowRegister, models, agents, loaded, loadPublic, loadModels, loadAgents }
return { features, siteName, allowRegister, voicePersona, models, agents, loaded, loadPublic, loadModels, loadAgents }
})
+91 -68
View File
@@ -11,13 +11,14 @@
<main class="chat-main">
<header class="chat-header">
<button class="menu-btn" type="button" aria-label="打开会话列表" @click="chat.toggleSidebar">
<IconMenu2 :size="21" :stroke-width="1.8" />
<IconMenu2 :size="20" :stroke-width="1.8" />
</button>
<div class="chat-heading">
<span class="chat-kicker">AI 创作空间</span>
<span class="chat-kicker"><i aria-hidden="true" /> AI 创作空间</span>
<h2 class="chat-title">{{ currentTitle }}</h2>
</div>
<div class="header-actions">
<ThemeToggle />
<a
v-if="auth.user?.role === 'admin'"
:href="adminUrl"
@@ -26,7 +27,7 @@
class="btn btn-ghost btn-sm admin-link"
aria-label="打开管理后台"
>
<IconSettings :size="17" :stroke-width="1.8" />
<IconSettings :size="16" :stroke-width="1.8" />
<span class="action-label">后台</span>
</a>
<button
@@ -35,8 +36,8 @@
:aria-label="auth.isGuest ? '登录账户' : '退出登录'"
@click="handleAccountAction"
>
<IconLogin v-if="auth.isGuest" :size="17" :stroke-width="1.8" />
<IconLogout v-else :size="17" :stroke-width="1.8" />
<IconLogin v-if="auth.isGuest" :size="16" :stroke-width="1.8" />
<IconLogout v-else :size="16" :stroke-width="1.8" />
<span class="action-label">{{ auth.isGuest ? '登录' : '退出' }}</span>
</button>
</div>
@@ -65,6 +66,7 @@ import { useSettingsStore } from '@/stores/settings'
import ChatSidebar from '@/components/ChatSidebar.vue'
import MessageList from '@/components/MessageList.vue'
import ChatInput from '@/components/ChatInput.vue'
import ThemeToggle from '@/components/ThemeToggle.vue'
import IconLogout from '@tabler/icons-vue/dist/esm/icons/IconLogout.mjs'
import IconLogin from '@tabler/icons-vue/dist/esm/icons/IconLogin.mjs'
import IconMenu2 from '@tabler/icons-vue/dist/esm/icons/IconMenu2.mjs'
@@ -93,11 +95,17 @@ onMounted(async () => {
}
})
async function handleSend({ content, attachments, agentId }) {
async function handleSend({ content, attachments, agentId, voiceMode, signal, onTextDelta, onComplete, onCancel, onError }) {
try {
await chat.sendMessage(content, attachments, agentId)
await chat.sendMessage(content, attachments, agentId, { onTextDelta, voiceMode, signal })
onComplete?.()
} catch (e) {
if (e?.name === 'AbortError' || signal?.aborted) {
onCancel?.()
return
}
notification.error(e)
onError?.(e)
}
}
@@ -118,114 +126,129 @@ function handleAccountAction() {
height: 100%;
min-height: 100dvh;
overflow: hidden;
background: linear-gradient(180deg, #f8faff 0%, var(--bg-primary) 100%);
background: var(--bg-primary);
}
.chat-main {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
min-width: 0;
background: transparent;
flex: 1;
flex-direction: column;
overflow: hidden;
background:
radial-gradient(circle at 82% -20%, rgba(183, 243, 107, 0.075), transparent 34%),
transparent;
}
.chat-main::before {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
background-image: radial-gradient(rgba(255, 255, 255, 0.055) 0.7px, transparent 0.7px);
background-size: 17px 17px;
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.62), transparent 74%);
content: "";
opacity: 0.28;
}
.chat-main > * {
position: relative;
z-index: 1;
}
.chat-header {
display: flex;
align-items: center;
gap: 10px;
height: var(--header-height);
flex: 0 0 auto;
align-items: center;
gap: 11px;
padding: 0 22px;
border-bottom: 1px solid rgba(225, 231, 239, 0.92);
background: rgba(255, 255, 255, 0.86);
backdrop-filter: blur(14px);
flex-shrink: 0;
border-bottom: 1px solid var(--border);
background: rgba(10, 12, 16, 0.76);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08);
backdrop-filter: blur(18px);
}
.menu-btn {
display: none;
width: 36px;
height: 36px;
width: 38px;
height: 38px;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 1px solid var(--border);
border-radius: 10px;
background: rgba(255, 255, 255, 0.025);
color: var(--text-secondary);
transition: background 0.16s ease, color 0.16s ease, transform 0.16s ease;
transition: background 160ms ease, border-color 160ms ease, color 160ms ease, transform 160ms ease;
}
.menu-btn:hover {
background: var(--bg-soft);
color: var(--text-primary);
border-color: var(--accent-line);
background: var(--accent-soft);
color: var(--accent);
}
.menu-btn:active {
transform: scale(0.96);
transform: translateY(1px) scale(0.97);
}
.chat-heading {
flex: 1;
min-width: 0;
flex: 1;
}
.chat-kicker {
display: inline-block;
margin-bottom: 1px;
font-size: 10px;
letter-spacing: 0.04em;
display: inline-flex;
align-items: center;
gap: 6px;
margin-bottom: 2px;
color: var(--text-muted);
font-size: 9px;
font-weight: 700;
letter-spacing: 0.11em;
text-transform: uppercase;
}
.chat-kicker i {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 11px rgba(183, 243, 107, 0.6);
}
.chat-title {
font-size: 16px;
font-weight: 650;
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
color: var(--text-primary);
font-size: 16px;
font-weight: 680;
letter-spacing: -0.025em;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-actions {
display: flex;
gap: 6px;
gap: 7px;
}
.btn-sm {
min-height: 36px;
padding: 7px 12px;
font-size: 13px;
padding: 7px 11px;
border-radius: 10px;
font-size: 12px;
box-shadow: none;
}
@media (max-width: 768px) {
.chat-header {
padding: 0 10px 0 8px;
}
.menu-btn {
display: flex;
}
.chat-kicker {
display: none;
}
.chat-title {
font-size: 17px;
}
.btn-sm {
width: 36px;
min-height: 36px;
padding: 0;
border-radius: 50%;
}
.action-label {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.menu-btn {
transition: none;
}
.chat-header { padding: 0 10px 0 8px; }
.menu-btn { display: flex; }
.chat-kicker { display: none; }
.chat-title { font-size: 16px; }
.btn-sm { width: 36px; min-height: 36px; padding: 0; }
.action-label { display: none; }
}
</style>
+178 -53
View File
@@ -1,38 +1,51 @@
<template>
<div class="auth-page">
<div class="auth-card">
<div class="auth-header">
<div class="logo">💬</div>
<h1>{{ settings.siteName }}</h1>
<p>登录您的账户</p>
<ThemeToggle class="auth-theme-toggle" />
<section class="auth-shell">
<aside class="auth-story">
<span class="auth-logo"><IconSparkles :size="22" :stroke-width="1.8" /></span>
<span class="auth-eyebrow">AI CREATIVE SPACE</span>
<h1>把想法放进来<br />让创作自然发生</h1>
<p>在同一个工作区完成对话图像生成与内容整理</p>
<span class="auth-note">TEXT · IMAGE · AGENT</span>
</aside>
<div class="auth-card">
<div class="auth-header">
<span class="auth-status"><i /> 账户登录</span>
<h2>{{ settings.siteName }}</h2>
<p>欢迎回来请登录您的账户</p>
</div>
<form @submit.prevent="handleLogin">
<div class="form-group">
<label>账号</label>
<input v-model="account" class="form-input" placeholder="用户名或邮箱" required />
</div>
<div class="form-group">
<label>密码</label>
<input v-model="password" type="password" class="form-input" placeholder="请输入密码" required />
</div>
<p v-if="error" class="form-error">{{ error }}</p>
<button type="submit" class="btn btn-primary auth-btn" :disabled="loading">
{{ loading ? '登录中...' : '进入创作空间' }}
</button>
</form>
<p class="auth-footer">
还没有账户
<router-link to="/register">立即注册</router-link>
</p>
</div>
<form @submit.prevent="handleLogin">
<div class="form-group">
<label>账号</label>
<input v-model="account" class="form-input" placeholder="用户名或邮箱" required />
</div>
<div class="form-group">
<label>密码</label>
<input v-model="password" type="password" class="form-input" placeholder="请输入密码" required />
</div>
<p v-if="error" class="form-error">{{ error }}</p>
<button type="submit" class="btn btn-primary auth-btn" :disabled="loading">
{{ loading ? '登录中...' : '登录' }}
</button>
</form>
<p class="auth-footer">
还没有账户
<router-link to="/register">立即注册</router-link>
</p>
</div>
</section>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import IconSparkles from '@tabler/icons-vue/dist/esm/icons/IconSparkles.mjs'
import ThemeToggle from '@/components/ThemeToggle.vue'
import { useAuthStore } from '@/stores/auth'
import { useSettingsStore } from '@/stores/settings'
@@ -64,54 +77,166 @@ async function handleLogin() {
<style scoped>
.auth-page {
min-height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
position: relative;
min-height: 100dvh;
display: grid;
place-items: center;
padding: 28px;
overflow: auto;
background:
radial-gradient(circle at 72% 16%, rgba(183, 243, 107, 0.08), transparent 26%),
var(--bg-primary);
}
.auth-theme-toggle {
position: absolute;
top: 18px;
right: 18px;
z-index: 3;
}
.auth-shell {
position: relative;
display: grid;
width: min(900px, 100%);
grid-template-columns: 1.08fr 0.92fr;
overflow: hidden;
border: 1px solid var(--border-strong);
border-radius: 22px;
background: var(--bg-secondary);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045), var(--shadow);
}
.auth-shell::after {
position: absolute;
top: 0;
left: 16%;
width: 36%;
height: 1px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
box-shadow: 0 0 17px rgba(183, 243, 107, 0.44);
content: "";
}
.auth-story {
position: relative;
display: flex;
min-height: 550px;
flex-direction: column;
justify-content: center;
padding: 54px;
overflow: hidden;
border-right: 1px solid var(--border);
background:
linear-gradient(135deg, rgba(183, 243, 107, 0.07), transparent 45%),
repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.018) 0 1px, transparent 1px 14px),
#0c0f13;
}
.auth-logo {
display: grid;
width: 46px;
height: 46px;
place-items: center;
margin-bottom: 46px;
border: 1px solid rgba(183, 243, 107, 0.5);
border-radius: 14px;
background: var(--accent);
color: #11150e;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.52), 0 5px 0 #55782f, 0 14px 28px rgba(110, 166, 54, 0.17);
}
.auth-eyebrow,
.auth-note {
color: var(--accent);
font-family: "Cascadia Code", Consolas, monospace;
font-size: 10px;
letter-spacing: 0.16em;
}
.auth-story h1 {
margin: 15px 0 19px;
font-size: clamp(36px, 4.7vw, 54px);
font-weight: 760;
letter-spacing: -0.06em;
line-height: 1.04;
text-wrap: balance;
}
.auth-story p {
max-width: 31ch;
color: var(--text-secondary);
font-size: 14px;
line-height: 1.75;
}
.auth-note {
position: absolute;
bottom: 30px;
left: 54px;
color: var(--text-muted);
font-size: 9px;
}
.auth-card {
width: 100%;
max-width: 400px;
padding: 40px 32px;
background: var(--bg-primary);
border-radius: 16px;
border: 1px solid var(--border);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
justify-content: center;
padding: 48px 42px;
background:
radial-gradient(circle at 100% 0%, rgba(183, 243, 107, 0.05), transparent 28%),
var(--bg-secondary);
}
.auth-header {
text-align: center;
margin-bottom: 32px;
margin-bottom: 30px;
}
.logo {
font-size: 48px;
margin-bottom: 12px;
.auth-status {
display: inline-flex;
align-items: center;
gap: 7px;
color: var(--text-muted);
font-size: 11px;
font-weight: 650;
letter-spacing: 0.06em;
}
.auth-header h1 {
font-size: 24px;
margin-bottom: 8px;
.auth-status i {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 12px rgba(183, 243, 107, 0.6);
}
.auth-header p {
.auth-header h2 {
margin: 12px 0 7px;
font-size: 27px;
letter-spacing: -0.045em;
}
.auth-header p,
.auth-footer {
color: var(--text-secondary);
font-size: 14px;
font-size: 13px;
}
.auth-btn {
width: 100%;
margin-top: 8px;
padding: 14px;
min-height: 47px;
margin-top: 7px;
}
.auth-footer {
margin-top: 25px;
text-align: center;
margin-top: 24px;
font-size: 14px;
color: var(--text-secondary);
}
@media (max-width: 720px) {
.auth-page { padding: 14px; }
.auth-shell { grid-template-columns: 1fr; }
.auth-story { display: none; }
.auth-card { min-height: 540px; padding: 40px 25px; }
}
</style>
+182 -57
View File
@@ -1,42 +1,55 @@
<template>
<div class="auth-page">
<div class="auth-card">
<div class="auth-header">
<div class="logo">💬</div>
<h1>{{ settings.siteName }}</h1>
<p>创建新账户</p>
<ThemeToggle class="auth-theme-toggle" />
<section class="auth-shell">
<aside class="auth-story">
<span class="auth-logo"><IconSparkles :size="22" :stroke-width="1.8" /></span>
<span class="auth-eyebrow">START CREATING</span>
<h1>一个账户<br />连接全部创作工具</h1>
<p>创建账户后即可保存会话并使用可用的模型与 Agent</p>
<span class="auth-note">TEXT · IMAGE · AGENT</span>
</aside>
<div class="auth-card">
<div class="auth-header">
<span class="auth-status"><i /> 创建账户</span>
<h2>{{ settings.siteName }}</h2>
<p>填写信息开始新的创作会话</p>
</div>
<form @submit.prevent="handleRegister">
<div class="form-group">
<label>用户名</label>
<input v-model="username" class="form-input" placeholder="3-50 个字符" required />
</div>
<div class="form-group">
<label>邮箱</label>
<input v-model="email" type="email" class="form-input" placeholder="your@email.com" required />
</div>
<div class="form-group">
<label>密码</label>
<input v-model="password" type="password" class="form-input" placeholder="至少 6 位" required />
</div>
<p v-if="error" class="form-error">{{ error }}</p>
<button type="submit" class="btn btn-primary auth-btn" :disabled="loading">
{{ loading ? '注册中...' : '创建并进入' }}
</button>
</form>
<p class="auth-footer">
已有账户
<router-link to="/login">立即登录</router-link>
</p>
</div>
<form @submit.prevent="handleRegister">
<div class="form-group">
<label>用户名</label>
<input v-model="username" class="form-input" placeholder="3-50 个字符" required />
</div>
<div class="form-group">
<label>邮箱</label>
<input v-model="email" type="email" class="form-input" placeholder="your@email.com" required />
</div>
<div class="form-group">
<label>密码</label>
<input v-model="password" type="password" class="form-input" placeholder="至少 6 位" required />
</div>
<p v-if="error" class="form-error">{{ error }}</p>
<button type="submit" class="btn btn-primary auth-btn" :disabled="loading">
{{ loading ? '注册中...' : '注册' }}
</button>
</form>
<p class="auth-footer">
已有账户
<router-link to="/login">立即登录</router-link>
</p>
</div>
</section>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import IconSparkles from '@tabler/icons-vue/dist/esm/icons/IconSparkles.mjs'
import ThemeToggle from '@/components/ThemeToggle.vue'
import { useAuthStore } from '@/stores/auth'
import { useSettingsStore } from '@/stores/settings'
@@ -72,54 +85,166 @@ async function handleRegister() {
<style scoped>
.auth-page {
min-height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
position: relative;
min-height: 100dvh;
display: grid;
place-items: center;
padding: 28px;
overflow: auto;
background:
radial-gradient(circle at 72% 16%, rgba(183, 243, 107, 0.08), transparent 26%),
var(--bg-primary);
}
.auth-theme-toggle {
position: absolute;
top: 18px;
right: 18px;
z-index: 3;
}
.auth-shell {
position: relative;
display: grid;
width: min(900px, 100%);
grid-template-columns: 1.08fr 0.92fr;
overflow: hidden;
border: 1px solid var(--border-strong);
border-radius: 22px;
background: var(--bg-secondary);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045), var(--shadow);
}
.auth-shell::after {
position: absolute;
top: 0;
left: 16%;
width: 36%;
height: 1px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
box-shadow: 0 0 17px rgba(183, 243, 107, 0.44);
content: "";
}
.auth-story {
position: relative;
display: flex;
min-height: 610px;
flex-direction: column;
justify-content: center;
padding: 54px;
overflow: hidden;
border-right: 1px solid var(--border);
background:
linear-gradient(135deg, rgba(183, 243, 107, 0.07), transparent 45%),
repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.018) 0 1px, transparent 1px 14px),
#0c0f13;
}
.auth-logo {
display: grid;
width: 46px;
height: 46px;
place-items: center;
margin-bottom: 46px;
border: 1px solid rgba(183, 243, 107, 0.5);
border-radius: 14px;
background: var(--accent);
color: #11150e;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.52), 0 5px 0 #55782f, 0 14px 28px rgba(110, 166, 54, 0.17);
}
.auth-eyebrow,
.auth-note {
color: var(--accent);
font-family: "Cascadia Code", Consolas, monospace;
font-size: 10px;
letter-spacing: 0.16em;
}
.auth-story h1 {
margin: 15px 0 19px;
font-size: clamp(36px, 4.7vw, 54px);
font-weight: 760;
letter-spacing: -0.06em;
line-height: 1.04;
text-wrap: balance;
}
.auth-story p {
max-width: 31ch;
color: var(--text-secondary);
font-size: 14px;
line-height: 1.75;
}
.auth-note {
position: absolute;
bottom: 30px;
left: 54px;
color: var(--text-muted);
font-size: 9px;
}
.auth-card {
width: 100%;
max-width: 400px;
padding: 40px 32px;
background: var(--bg-primary);
border-radius: 16px;
border: 1px solid var(--border);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
justify-content: center;
padding: 42px;
background:
radial-gradient(circle at 100% 0%, rgba(183, 243, 107, 0.05), transparent 28%),
var(--bg-secondary);
}
.auth-header {
text-align: center;
margin-bottom: 32px;
margin-bottom: 27px;
}
.logo {
font-size: 48px;
margin-bottom: 12px;
.auth-status {
display: inline-flex;
align-items: center;
gap: 7px;
color: var(--text-muted);
font-size: 11px;
font-weight: 650;
letter-spacing: 0.06em;
}
.auth-header h1 {
font-size: 24px;
margin-bottom: 8px;
.auth-status i {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 12px rgba(183, 243, 107, 0.6);
}
.auth-header p {
.auth-header h2 {
margin: 12px 0 7px;
font-size: 27px;
letter-spacing: -0.045em;
}
.auth-header p,
.auth-footer {
color: var(--text-secondary);
font-size: 14px;
font-size: 13px;
}
.auth-btn {
width: 100%;
margin-top: 8px;
padding: 14px;
min-height: 47px;
margin-top: 6px;
}
.auth-footer {
margin-top: 23px;
text-align: center;
margin-top: 24px;
font-size: 14px;
color: var(--text-secondary);
}
@media (max-width: 720px) {
.auth-page { padding: 14px; }
.auth-shell { grid-template-columns: 1fr; }
.auth-story { display: none; }
.auth-card { min-height: 640px; padding: 36px 25px; }
}
</style>